The user page at /u/:username now opens with a compact identity block (40px avatar, name, handle, clamped bio) instead of a hero header with a share button, and the list beneath it follows the user's column setting like Explore does. In grid mode the header spans every column via a new ColumnGrid header slot, exposed through PagedMemoList.renderHeader. Loading shows a skeleton after the standard delay, not-found renders a body instead of a toast fired from render, and visitors get an explicit empty message. Saved views now apply on profile routes too. A single profile-route matcher in lib/memo-views replaces five separate spellings, isMemoCollectionRoute names the routes where views, calendar days and tags narrow the list in place, and the sidebar shows the Views section on every such route for signed-in users. The never-taken navigation-target branch in the collection sidebar is removed along with its props.
128 lines
4.7 KiB
TypeScript
128 lines
4.7 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { buildMemoFilter } from "@/hooks/useMemoFilters";
|
|
import {
|
|
BUILTIN_TASKS_VIEW_FILTER,
|
|
BUILTIN_TASKS_VIEW_ID,
|
|
getMemoScopePath,
|
|
getMemoViewId,
|
|
getProfileUsername,
|
|
isMemoCollectionRoute,
|
|
isMemoScopeRoute,
|
|
resolveMemoScope,
|
|
} from "@/lib/memo-views";
|
|
import { Visibility } from "@/types/proto/api/v1/memo_service_pb";
|
|
|
|
describe("memo scopes", () => {
|
|
it("resolves collection, profile, and detail routes", () => {
|
|
expect(resolveMemoScope("/archived")).toBe("archived");
|
|
expect(resolveMemoScope("/explore")).toBe("explore");
|
|
expect(resolveMemoScope("/u/steven", { currentUsername: "steven" })).toBe("home");
|
|
expect(resolveMemoScope("/u/maya", { currentUsername: "steven" })).toBe("explore");
|
|
expect(resolveMemoScope("/memos/123", { detailFrom: "/archived?filter=tagSearch%3Awork" })).toBe("archived");
|
|
expect(resolveMemoScope("/memos/123", { memoArchived: true })).toBe("archived");
|
|
expect(resolveMemoScope("/settings", { fallback: "explore" })).toBe("explore");
|
|
});
|
|
|
|
it("maps collection routes while limiting primary scope paths to Home and Explore", () => {
|
|
expect(isMemoScopeRoute("/")).toBe(true);
|
|
expect(isMemoScopeRoute("/explore")).toBe(true);
|
|
expect(isMemoScopeRoute("/archived")).toBe(true);
|
|
expect(isMemoScopeRoute("/attachments")).toBe(false);
|
|
expect(getMemoScopePath("home")).toBe("/");
|
|
expect(getMemoScopePath("explore")).toBe("/explore");
|
|
});
|
|
|
|
it("treats user profiles as collection routes where views and filters apply in place", () => {
|
|
expect(isMemoCollectionRoute("/")).toBe(true);
|
|
expect(isMemoCollectionRoute("/archived")).toBe(true);
|
|
expect(isMemoCollectionRoute("/u/steven")).toBe(true);
|
|
expect(isMemoCollectionRoute("/u/steven/?view=map")).toBe(true);
|
|
expect(getProfileUsername("/u/j%C3%BAlia/")).toBe("júlia");
|
|
expect(getProfileUsername("/u/steven/memos")).toBeUndefined();
|
|
expect(isMemoScopeRoute("/u/steven")).toBe(false);
|
|
expect(isMemoCollectionRoute("/memos/123")).toBe(false);
|
|
expect(isMemoCollectionRoute("/attachments")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("memo views", () => {
|
|
it("uses a collision-safe built-in Tasks view", () => {
|
|
expect(BUILTIN_TASKS_VIEW_ID).not.toBe("tasks");
|
|
expect(BUILTIN_TASKS_VIEW_FILTER).toBe("has_task_list && has_incomplete_tasks");
|
|
expect(getMemoViewId("users/steven/views/work")).toBe("work");
|
|
});
|
|
|
|
it("composes Tasks with search, tags, creator, and visibility", () => {
|
|
expect(
|
|
buildMemoFilter({
|
|
creatorName: "users/steven",
|
|
currentMemoView: BUILTIN_TASKS_VIEW_ID,
|
|
filters: [
|
|
{ factor: "contentSearch", value: "plan" },
|
|
{ factor: "tagSearch", value: "work" },
|
|
],
|
|
includePinned: false,
|
|
visibilities: [Visibility.PUBLIC],
|
|
}),
|
|
).toBe(
|
|
'creator == "users/steven" && has_task_list && has_incomplete_tasks && content.contains("plan") && tag in ["work"] && visibility in ["PUBLIC"]',
|
|
);
|
|
});
|
|
|
|
it("maps the Space audience without falling back to Private", () => {
|
|
expect(
|
|
buildMemoFilter({
|
|
filters: [],
|
|
includePinned: false,
|
|
visibilities: [Visibility.PUBLIC, Visibility.PROTECTED, Visibility.SPACE],
|
|
}),
|
|
).toBe('visibility in ["PUBLIC", "PROTECTED", "SPACE"]');
|
|
});
|
|
|
|
it("maps property filter factors to their CEL flags", () => {
|
|
expect(
|
|
buildMemoFilter({
|
|
filters: [
|
|
{ factor: "property.hasLink", value: "" },
|
|
{ factor: "property.hasTaskList", value: "" },
|
|
{ factor: "property.hasCode", value: "" },
|
|
{ factor: "property.hasLocation", value: "" },
|
|
],
|
|
includePinned: false,
|
|
}),
|
|
).toBe("has_link && has_task_list && has_code && has_location");
|
|
});
|
|
|
|
it("uses a custom memo view filter when Tasks is not selected", () => {
|
|
expect(
|
|
buildMemoFilter({
|
|
currentMemoView: "work",
|
|
filters: [],
|
|
includePinned: false,
|
|
selectedMemoViewFilter: 'tag in ["work"]',
|
|
}),
|
|
).toBe('tag in ["work"]');
|
|
});
|
|
|
|
it("builds display-time filters from valid local calendar-day boundaries", () => {
|
|
const start = new Date(2026, 7, 2);
|
|
const end = new Date(start);
|
|
end.setDate(end.getDate() + 1);
|
|
|
|
expect(
|
|
buildMemoFilter({
|
|
filters: [{ factor: "displayTime", value: "2026-08-02" }],
|
|
includePinned: false,
|
|
}),
|
|
).toBe(`created_ts >= timestamp(${Math.floor(start.getTime() / 1000)}) && created_ts < timestamp(${Math.floor(end.getTime() / 1000)})`);
|
|
});
|
|
|
|
it("ignores invalid display-time filter values", () => {
|
|
expect(
|
|
buildMemoFilter({
|
|
filters: [{ factor: "displayTime", value: "2026-02-30" }],
|
|
includePinned: false,
|
|
}),
|
|
).toBeUndefined();
|
|
});
|
|
});
|