feat: move Subject navigation and routes into subjektiv
This commit is contained in:
@@ -17,10 +17,10 @@ import {
|
||||
|
||||
const root = join(dirname(fromFileUrl(import.meta.url)), "../../..");
|
||||
const artifacts = join(root, "target/web-ux/t-737-browser-interactions");
|
||||
const subjectPath = `/w/${fixture.workspaceId}/memory/${fixture.subjectId}`;
|
||||
const subjectPath = `/w/${fixture.workspaceId}/subjektiv/${fixture.subjectId}`;
|
||||
const journalPath = `${subjectPath}/journal`;
|
||||
const memoryPath =
|
||||
`${subjectPath}/${fixture.memoryId}?change_id=change-current`;
|
||||
`${subjectPath}/memories/${fixture.memoryId}?change_id=change-current`;
|
||||
const claim = "Keep subject Memory provenance typed and visible";
|
||||
const entryTitle = "Release milestone 00 · release [a+b] %_";
|
||||
const detailPath = `${journalPath}?journal=journal-release&entry=entry-00`;
|
||||
@@ -262,7 +262,7 @@ Deno.test("Journal fixture UI keeps scoped navigation, bounded links and distinc
|
||||
.waitFor();
|
||||
assertEquals(
|
||||
new URL(page.url()).pathname,
|
||||
`${subjectPath}/${fixture.memoryId}`,
|
||||
`${subjectPath}/memories/${fixture.memoryId}`,
|
||||
);
|
||||
assertEquals(
|
||||
new URL(page.url()).searchParams.get("change_id"),
|
||||
@@ -539,7 +539,7 @@ Deno.test("Journal fixture UI keeps scoped navigation, bounded links and distinc
|
||||
const state of [
|
||||
{
|
||||
path:
|
||||
`/w/${fixture.workspaceId}/memory/empty-subject/journal?view=sessions`,
|
||||
`/w/${fixture.workspaceId}/subjektiv/empty-subject/journal?view=sessions`,
|
||||
text: "Journalはまだありません。",
|
||||
role: "status" as const,
|
||||
},
|
||||
@@ -570,7 +570,7 @@ Deno.test("Journal fixture UI keeps scoped navigation, bounded links and distinc
|
||||
await assertNoPageWideOverflow(page);
|
||||
}
|
||||
await page.goto(
|
||||
`${base}${subjectPath}/memory-resolved-0002?change_id=change-earlier`,
|
||||
`${base}${subjectPath}/memories/memory-resolved-0002?change_id=change-earlier`,
|
||||
);
|
||||
await page.locator("#related-journal").getByText(
|
||||
"対応するJournalは未接続",
|
||||
@@ -591,7 +591,7 @@ Deno.test("Journal fixture UI keeps scoped navigation, bounded links and distinc
|
||||
r.payload.selector.candidate_id === fixture.candidateId
|
||||
));
|
||||
await page.goto(
|
||||
`${base}${subjectPath}/memory-sources-failure?change_id=change-current`,
|
||||
`${base}${subjectPath}/memories/memory-sources-failure?change_id=change-current`,
|
||||
);
|
||||
await page.getByRole("alert").filter({
|
||||
hasText: "Journal参照を取得できません。",
|
||||
|
||||
@@ -16,7 +16,8 @@ if (
|
||||
}
|
||||
|
||||
// 既存Memory scenarioとpaginationを変えないよう、明示選択時だけ有効にする。
|
||||
const permissionsMode = Deno.args[3] === "permissions";
|
||||
const navigationMode = Deno.args[3] === "navigation";
|
||||
const permissionsMode = Deno.args[3] === "permissions" || navigationMode;
|
||||
if (permissionsMode) {
|
||||
// 権限の証拠は親から渡された実製品buildを使って採取する。
|
||||
// build欠落時に/healthを成功させたり、fixture HTMLで代替したりしない。
|
||||
@@ -105,6 +106,12 @@ if (permissionsMode) {
|
||||
...permissionFixtureSubjects.map(([id, role]) => subject(id, role, "active", "fingerprint-0")),
|
||||
);
|
||||
}
|
||||
if (navigationMode) {
|
||||
subjects.push(
|
||||
subject("staging", "Subject named staging", "active", "fingerprint-0"),
|
||||
subject("encoded/Subject ?#日本%", "Encoded identity Subject", "active", "fingerprint-0"),
|
||||
);
|
||||
}
|
||||
const pagedSubject = subject(pagedSubjectId, "Subject on the next page", "active", "fingerprint-1");
|
||||
|
||||
function connectedWorker(displayName: string) {
|
||||
@@ -513,6 +520,9 @@ Deno.serve({ hostname: "127.0.0.1", port }, async (request) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (navigationMode && url.pathname === `/api/w/${workspaceId}/memory/staging`) {
|
||||
return json({ limit: 200, returned_count: 0, total_valid_count: 0, invalid_count: 0, truncated: false, order: "fixture", record_authority: "synthetic-http", items: [], diagnostics: [] });
|
||||
}
|
||||
const subjectsPath = `/api/w/${workspaceId}/subjektiv/subjects`;
|
||||
if (url.pathname === subjectsPath) {
|
||||
if (request.method === "POST") {
|
||||
|
||||
@@ -45,7 +45,7 @@ async function openSubjectPage(
|
||||
baseUrl: string,
|
||||
mobile = false,
|
||||
): Promise<void> {
|
||||
await page.goto(`${baseUrl}/w/${workspaceId}/memory/${encodeURIComponent(subjectId)}`);
|
||||
await page.goto(`${baseUrl}/w/${workspaceId}/subjektiv/${encodeURIComponent(subjectId)}`);
|
||||
const heading = page.locator("h1", { hasText: "Release coordination" });
|
||||
await heading.waitFor({ state: "attached" });
|
||||
if (mobile) {
|
||||
@@ -189,10 +189,10 @@ Deno.test("production subject Memory and Worker launch shells preserve exact sco
|
||||
await assertNoPageWideOverflow(page);
|
||||
await assertMainOwnsVerticalScroll(page);
|
||||
assertEquals(
|
||||
await page.locator('aside a[href$="/memory"]', { hasText: "Subjects" }).count(),
|
||||
await page.locator('aside a[href$="/subjektiv"]', { hasText: "Subjects" }).count(),
|
||||
1,
|
||||
);
|
||||
assertEquals(await page.locator("aside a", { hasText: "Staging" }).count(), 0);
|
||||
assertEquals(await page.locator("aside a", { hasText: "Staging" }).count(), 1);
|
||||
|
||||
if (scenario.label === "wheel") {
|
||||
assertEquals(
|
||||
@@ -269,7 +269,7 @@ Deno.test("production subject Memory and Worker launch shells preserve exact sco
|
||||
const detailContext = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const detailPage = await detailContext.newPage();
|
||||
await detailPage.goto(
|
||||
`${baseUrl}/w/${workspaceId}/memory/${encodeURIComponent(subjectId)}/${
|
||||
`${baseUrl}/w/${workspaceId}/subjektiv/${encodeURIComponent(subjectId)}/memories/${
|
||||
encodeURIComponent(memoryId)
|
||||
}`,
|
||||
);
|
||||
@@ -324,7 +324,7 @@ Deno.test("production subject Memory and Worker launch shells preserve exact sco
|
||||
|
||||
const paginationContext = await browser.newContext({ viewport: { width: 768, height: 900 } });
|
||||
const paginationPage = await paginationContext.newPage();
|
||||
await paginationPage.goto(`${baseUrl}/w/${workspaceId}/memory`);
|
||||
await paginationPage.goto(`${baseUrl}/w/${workspaceId}/subjektiv`);
|
||||
await paginationPage.getByRole("heading", { name: "Subjects", level: 1 }).waitFor();
|
||||
assertEquals(
|
||||
await paginationPage.getByText(
|
||||
@@ -354,7 +354,7 @@ Deno.test("production subject Memory and Worker launch shells preserve exact sco
|
||||
);
|
||||
|
||||
await paginationPage.goto(
|
||||
`${baseUrl}/w/${workspaceId}/memory/${encodeURIComponent(subjectId)}`,
|
||||
`${baseUrl}/w/${workspaceId}/subjektiv/${encodeURIComponent(subjectId)}`,
|
||||
);
|
||||
await paginationPage.getByRole("heading", { name: "Current Memories", level: 2 }).waitFor();
|
||||
await paginationPage.getByRole("navigation", { name: "Current Memory pages" }).getByRole(
|
||||
@@ -382,7 +382,7 @@ Deno.test("production subject Memory and Worker launch shells preserve exact sco
|
||||
}
|
||||
await route.continue();
|
||||
});
|
||||
await emptyPage.goto(`${baseUrl}/w/${workspaceId}/memory`);
|
||||
await emptyPage.goto(`${baseUrl}/w/${workspaceId}/subjektiv`);
|
||||
await emptyPage.getByText("No Memory subjects.").waitFor();
|
||||
await emptyPage.getByRole("button", { name: "New Subject" }).click();
|
||||
const emptyRole = emptyPage.getByRole("textbox", { name: "Role" });
|
||||
@@ -415,7 +415,7 @@ Deno.test("production subject Memory and Worker launch shells preserve exact sco
|
||||
mutationPaths.push(new URL(request.url()).pathname);
|
||||
}
|
||||
});
|
||||
await createPage.goto(`${baseUrl}/w/${workspaceId}/memory?cursor=${subjectPageCursor}`);
|
||||
await createPage.goto(`${baseUrl}/w/${workspaceId}/subjektiv?cursor=${subjectPageCursor}`);
|
||||
await createPage.getByText("Subject on the next page").waitFor();
|
||||
await createPage.getByRole("button", { name: "New Subject" }).click();
|
||||
const createRole = createPage.getByRole("textbox", { name: "Role" });
|
||||
@@ -436,7 +436,7 @@ Deno.test("production subject Memory and Worker launch shells preserve exact sco
|
||||
"Challenge unsupported assumptions before release.",
|
||||
);
|
||||
await createPage.getByRole("button", { name: "Create Subject" }).click();
|
||||
await createPage.waitForURL("**/memory/created-subject-0001");
|
||||
await createPage.waitForURL("**/subjektiv/created-subject-0001");
|
||||
await createPage.getByRole("heading", { name: "Release steward", level: 1 }).waitFor();
|
||||
assertEquals(
|
||||
await createPage.getByRole("article", { name: "User-managed Subject behavior" }).count(),
|
||||
@@ -539,7 +539,7 @@ Deno.test("production subject Memory and Worker launch shells preserve exact sco
|
||||
];
|
||||
for (const stateCase of stateCases) {
|
||||
const page = await stateContext.newPage();
|
||||
await page.goto(`${baseUrl}/w/${workspaceId}/memory/${stateCase.id}`);
|
||||
await page.goto(`${baseUrl}/w/${workspaceId}/subjektiv/${stateCase.id}`);
|
||||
const state = page.getByRole(stateCase.role).filter({ hasText: stateCase.text });
|
||||
await state.waitFor();
|
||||
assert((await state.textContent())?.includes(stateCase.text));
|
||||
|
||||
@@ -24,7 +24,7 @@ await Deno.mkdir(output);
|
||||
const port = await freePort();
|
||||
const base = `http://127.0.0.1:${port}`;
|
||||
const subject = "empty-subject";
|
||||
const subjectRoute = `/w/memory-review/memory/${subject}`;
|
||||
const subjectRoute = `/w/memory-review/${mode === "before" ? "memory" : "subjektiv"}/${subject}`;
|
||||
const captures: unknown[] = [];
|
||||
const failures: string[] = [];
|
||||
const faults: { kind: string; message: string; expected: boolean }[] = [];
|
||||
@@ -233,7 +233,7 @@ try {
|
||||
faultMode = "normal";
|
||||
const secondTab = await context.newPage();
|
||||
try {
|
||||
await secondTab.goto(`${base}${subjectRoute}/conversation`);
|
||||
await secondTab.goto(`${base}${subjectRoute}/chat`);
|
||||
await secondTab.getByRole("textbox").fill("Independent input from another tab");
|
||||
await secondTab.getByRole("button", { name: "Send message", exact: true }).click();
|
||||
await secondTab.getByText(
|
||||
|
||||
@@ -257,7 +257,7 @@ try {
|
||||
};
|
||||
manifest.scenarios.push(scenario);
|
||||
try {
|
||||
await page.goto(`${base}/w/memory-review/memory`);
|
||||
await page.goto(`${base}/w/memory-review/subjektiv`);
|
||||
await page.getByRole("heading", { name: "Subjects", level: 1, exact: true }).waitFor();
|
||||
await page.getByRole("button", { name: "New Subject", exact: true }).click();
|
||||
const role = page.getByRole("textbox", { name: "Role", exact: true });
|
||||
@@ -354,7 +354,7 @@ try {
|
||||
await page.getByRole("alert").scrollIntoViewIfNeeded();
|
||||
await capture(page, observation, `${prefix}-later-403-reopened-pending`);
|
||||
await page.getByRole("button", { name: "Create Subject", exact: true }).click();
|
||||
await page.waitForURL("**/memory/created-subject-0001");
|
||||
await page.waitForURL("**/subjektiv/created-subject-0001");
|
||||
await page.getByRole("heading", { name: roleText.trim(), level: 1, exact: true })
|
||||
.waitFor();
|
||||
assertEquals(
|
||||
|
||||
@@ -0,0 +1,426 @@
|
||||
/** SubjectのIA移行を実production buildと隔離HTTPで検証する。Backend認可の証拠ではない。 */
|
||||
import { assert, assertEquals } from "@std/assert";
|
||||
import { dirname, fromFileUrl, join, resolve } from "@std/path";
|
||||
import { chromium, type Page } from "playwright";
|
||||
import { sha256File } from "../src/artifacts.ts";
|
||||
import { startOwnedProcesses, stopOwnedProcesses } from "../src/processes.ts";
|
||||
import { freePort } from "./memory_document_harness.ts";
|
||||
|
||||
const root = resolve(dirname(fromFileUrl(import.meta.url)), "../../..");
|
||||
const [buildArg, runId] = Deno.args;
|
||||
if (!buildArg || !runId || !/^[a-zA-Z0-9._-]+$/.test(runId)) {
|
||||
throw new Error(
|
||||
"usage: subject_navigation_review.ts <actual-build> <unique-run-id>",
|
||||
);
|
||||
}
|
||||
const build = resolve(buildArg);
|
||||
await Deno.stat(join(build, "index.html"));
|
||||
const output = join(root, "target/web-ux/t817", runId);
|
||||
await Deno.mkdir(output, { recursive: true });
|
||||
const port = await freePort(), base = `http://127.0.0.1:${port}`;
|
||||
const ws = "/w/memory-review", subjects = `${ws}/subjektiv`;
|
||||
const subject =
|
||||
"release-coordination-subject-with-a-deliberately-long-stable-identity-for-responsive-review";
|
||||
const detail = `${subjects}/${subject}`;
|
||||
const memory =
|
||||
"memory-decision-with-a-deliberately-long-stable-identity-for-overflow-review-0000000001";
|
||||
const captures: Record<string, unknown>[] = [],
|
||||
failures: string[] = [],
|
||||
faults: Record<string, unknown>[] = [];
|
||||
const manifest = {
|
||||
evidence:
|
||||
"actual-static-Web-build / synthetic-HTTP / NOT-real-Backend-auth-or-Runtime",
|
||||
source: new TextDecoder().decode(
|
||||
(await new Deno.Command("git", { args: ["rev-parse", "HEAD"], cwd: root })
|
||||
.output()).stdout,
|
||||
).trim(),
|
||||
buildIndexSha256: await sha256File(join(build, "index.html")),
|
||||
runnerSha256: await sha256File(fromFileUrl(import.meta.url)),
|
||||
browser: "",
|
||||
captures,
|
||||
faults,
|
||||
failures,
|
||||
status: "running",
|
||||
visualVerdict: "UNREVIEWED",
|
||||
};
|
||||
const save = () =>
|
||||
Deno.writeTextFile(
|
||||
join(output, "review-context.json"),
|
||||
JSON.stringify(manifest, null, 2),
|
||||
);
|
||||
const owned = await startOwnedProcesses(
|
||||
[{
|
||||
id: "navigation-synthetic-http",
|
||||
command: Deno.execPath(),
|
||||
cwd: root,
|
||||
args: [
|
||||
"run",
|
||||
"--config",
|
||||
"tools/web-ux/deno.json",
|
||||
"-A",
|
||||
"tools/web-ux/browser-tests/memory_document_fixture_server.ts",
|
||||
String(port),
|
||||
build,
|
||||
"3",
|
||||
"navigation",
|
||||
],
|
||||
readyUrl: `${base}/health`,
|
||||
}],
|
||||
join(output, "scenario.json"),
|
||||
join(output, "logs"),
|
||||
[],
|
||||
);
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
manifest.browser = browser.version();
|
||||
try {
|
||||
for (const colorScheme of ["light", "dark"] as const) {
|
||||
for (const width of [1440, 768, 390]) {
|
||||
const context = await browser.newContext({
|
||||
colorScheme,
|
||||
viewport: { width, height: 900 },
|
||||
reducedMotion: "reduce",
|
||||
});
|
||||
const page = await context.newPage();
|
||||
page.setDefaultTimeout(15000);
|
||||
let expectedFailure = false;
|
||||
const requests: string[] = [];
|
||||
page.on("request", (r) => requests.push(new URL(r.url()).pathname));
|
||||
page.on(
|
||||
"pageerror",
|
||||
(e) =>
|
||||
faults.push({
|
||||
kind: "pageerror",
|
||||
message: e.message,
|
||||
expected: false,
|
||||
}),
|
||||
);
|
||||
page.on("console", (m) => {
|
||||
if (m.type() === "error") {
|
||||
faults.push({
|
||||
kind: "console",
|
||||
message: m.text(),
|
||||
expected: expectedFailure,
|
||||
});
|
||||
}
|
||||
});
|
||||
page.on(
|
||||
"requestfailed",
|
||||
(r) =>
|
||||
faults.push({
|
||||
kind: "request",
|
||||
path: new URL(r.url()).pathname,
|
||||
message: r.failure()?.errorText,
|
||||
expected: r.failure()?.errorText === "net::ERR_ABORTED",
|
||||
}),
|
||||
);
|
||||
page.on("response", (r) => {
|
||||
if (r.status() >= 400) {
|
||||
faults.push({
|
||||
kind: "http",
|
||||
path: new URL(r.url()).pathname,
|
||||
status: r.status(),
|
||||
expected: expectedFailure,
|
||||
});
|
||||
}
|
||||
});
|
||||
// 公開chat APIの外側だけを合成。会話UI・loader・routerは本番のまま。
|
||||
await context.route(
|
||||
"**/api/w/memory-review/chat/conversations**",
|
||||
(route) =>
|
||||
route.fulfill({
|
||||
json:
|
||||
new URL(route.request().url()).pathname.endsWith("/conversations")
|
||||
? {
|
||||
id: "conversation",
|
||||
subject_id: "empty-subject",
|
||||
participant_account_id: "owner",
|
||||
last_user_message_id: null,
|
||||
}
|
||||
: {
|
||||
conversation: {
|
||||
id: "conversation",
|
||||
subject_id: "empty-subject",
|
||||
participant_account_id: "owner",
|
||||
last_user_message_id: null,
|
||||
},
|
||||
messages: [],
|
||||
has_more: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
async function direct(path: string, selector: string, canonical = path) {
|
||||
const response = await page.goto(base + path);
|
||||
assertEquals(response?.status(), 200);
|
||||
assert(response?.headers()["content-type"].includes("text/html"));
|
||||
await page.waitForURL(base + canonical);
|
||||
await page.locator(selector).waitFor();
|
||||
assertEquals(
|
||||
new URL(page.url()).pathname + new URL(page.url()).search +
|
||||
new URL(page.url()).hash,
|
||||
canonical,
|
||||
);
|
||||
}
|
||||
async function selected(label = "Subjects") {
|
||||
const nav = page.getByRole("navigation", {
|
||||
name: "Workspace sections",
|
||||
exact: true,
|
||||
includeHidden: true,
|
||||
});
|
||||
assertEquals(
|
||||
await nav.getByRole("link", {
|
||||
name: label,
|
||||
exact: true,
|
||||
includeHidden: true,
|
||||
}).getAttribute("aria-current"),
|
||||
"page",
|
||||
);
|
||||
const other = label === "Subjects" ? "Staging" : "Subjects";
|
||||
assertEquals(
|
||||
await nav.getByRole("link", {
|
||||
name: other,
|
||||
exact: true,
|
||||
includeHidden: true,
|
||||
}).getAttribute("aria-current"),
|
||||
null,
|
||||
);
|
||||
}
|
||||
async function capture(id: string) {
|
||||
const image = `${colorScheme}-${width}-${id}.png`;
|
||||
await page.screenshot({ path: join(output, image) });
|
||||
const overflow = await page.evaluate(
|
||||
"document.documentElement.scrollWidth > innerWidth",
|
||||
);
|
||||
assertEquals(overflow, false, image);
|
||||
const legacyLinks = await page.locator(
|
||||
"main a[href], .sidebar-sections a[href], .workspace-breadcrumbs a[href]",
|
||||
).evaluateAll((links) =>
|
||||
links.map((l) => l.getAttribute("href")!).filter((h) =>
|
||||
/\/memory(?:\/|$)/.test(h) && !h.includes("/memory/staging")
|
||||
)
|
||||
);
|
||||
assertEquals(legacyLinks, []);
|
||||
captures.push({
|
||||
image,
|
||||
sha256: await sha256File(join(output, image)),
|
||||
route: page.url(),
|
||||
colorScheme,
|
||||
width,
|
||||
mainWidth: await page.locator("main").evaluate((el) =>
|
||||
el.clientWidth
|
||||
),
|
||||
overflow,
|
||||
alerts: await page.getByRole("alert").allTextContents(),
|
||||
snapshot: await page.locator("body").ariaSnapshot(),
|
||||
});
|
||||
await save();
|
||||
}
|
||||
try {
|
||||
await direct(subjects, '[data-memory-view="subjects"]');
|
||||
await selected();
|
||||
await capture("list");
|
||||
if (width === 390) {
|
||||
await page.getByRole("button", { name: "Show sidebar", exact: true })
|
||||
.click();
|
||||
await page.getByRole("navigation", {
|
||||
name: "Workspace sections",
|
||||
exact: true,
|
||||
}).waitFor();
|
||||
await capture("mobile-navigation");
|
||||
await page.getByRole("button", { name: "Hide sidebar", exact: true })
|
||||
.click();
|
||||
}
|
||||
await page.locator(`a.subject-row[href="${detail}"]`).click();
|
||||
await page.locator(".subject-overview").waitFor();
|
||||
assertEquals(new URL(page.url()).pathname, detail);
|
||||
await selected();
|
||||
await capture("detail");
|
||||
const breadcrumbs = page.getByRole("navigation", {
|
||||
name: "Current workspace location",
|
||||
});
|
||||
assertEquals(
|
||||
await breadcrumbs.getByRole("link", { name: "Subjects", exact: true })
|
||||
.getAttribute("href"),
|
||||
subjects,
|
||||
);
|
||||
await page.getByRole("link", { name: /Journal ·/ }).click();
|
||||
await page.locator('[data-journal-view="list"] .entry-list').waitFor();
|
||||
assertEquals(new URL(page.url()).pathname, detail + "/journal");
|
||||
await capture("journal");
|
||||
await page.goBack();
|
||||
await page.locator(".subject-overview").waitFor();
|
||||
await page.goForward();
|
||||
await page.locator('[data-journal-view="list"]').waitFor();
|
||||
await direct(
|
||||
`${detail}/memories/${memory}?change_id=change-earlier#related-journal`,
|
||||
'[data-memory-view="detail"]',
|
||||
);
|
||||
assertEquals(
|
||||
await page.locator('.workspace-breadcrumbs a[href$="/memories"]')
|
||||
.count(),
|
||||
0,
|
||||
);
|
||||
await capture("memory-history");
|
||||
await direct(
|
||||
subjects + "/permissions-edit/permissions",
|
||||
"#permissions-heading",
|
||||
);
|
||||
await page.getByRole("combobox", { name: "Operation", exact: true })
|
||||
.waitFor();
|
||||
await selected();
|
||||
await capture("permissions-owner");
|
||||
await direct(
|
||||
subjects + "/permissions-reader/permissions",
|
||||
"#permissions-heading",
|
||||
);
|
||||
await page.getByRole("status").filter({ hasText: "Read-only." })
|
||||
.waitFor();
|
||||
await capture("permissions-reader");
|
||||
await direct(
|
||||
subjects + "/empty-subject/chat",
|
||||
".conversation-page textarea",
|
||||
);
|
||||
await page.getByText("No public messages yet.").waitFor();
|
||||
await selected();
|
||||
await capture("chat");
|
||||
await direct(`${ws}/memory/staging`, ".staging-summary-grid");
|
||||
await selected("Staging");
|
||||
await capture("staging");
|
||||
await direct(subjects + "/staging", ".subject-overview");
|
||||
await selected();
|
||||
const encoded = encodeURIComponent("encoded/Subject ?#日本%");
|
||||
await direct(
|
||||
`${ws}/memory/${encoded}?cursor=a%2Fb%26c#subject-heading`,
|
||||
".subject-overview",
|
||||
`${subjects}/${encoded}?cursor=a%2Fb%26c#subject-heading`,
|
||||
);
|
||||
assert(
|
||||
requests.includes(
|
||||
`/api/w/memory-review/subjektiv/subjects/${encoded}`,
|
||||
),
|
||||
);
|
||||
await page.reload();
|
||||
await page.locator(".subject-overview").waitFor();
|
||||
// 旧URLの曖昧性はstatic予約語優先。新memories namespaceで同名IDを読める。
|
||||
for (
|
||||
const id of [
|
||||
"permissions",
|
||||
"journal",
|
||||
"chat",
|
||||
"conversation",
|
||||
"a/b ?#%",
|
||||
]
|
||||
) {
|
||||
await direct(
|
||||
`${detail}/memories/${encodeURIComponent(id)}`,
|
||||
'[data-memory-view="detail"]',
|
||||
);
|
||||
}
|
||||
for (
|
||||
const [old, next, selector] of [
|
||||
[
|
||||
`${ws}/memory?cursor=a%2Fb#memory-subjects-heading`,
|
||||
`${subjects}?cursor=a%2Fb#memory-subjects-heading`,
|
||||
'[data-memory-view="subjects"]',
|
||||
],
|
||||
[
|
||||
`${ws}/memory/${subject}?cursor=a%2Fb#subject-heading`,
|
||||
`${detail}?cursor=a%2Fb#subject-heading`,
|
||||
".subject-overview",
|
||||
],
|
||||
[
|
||||
`${ws}/memory/${subject}/${memory}?change_id=change-earlier#related-journal`,
|
||||
`${detail}/memories/${memory}?change_id=change-earlier#related-journal`,
|
||||
'[data-memory-view="detail"]',
|
||||
],
|
||||
[
|
||||
`${ws}/memory/${subject}/journal?journal=journal-release#journal-title`,
|
||||
`${detail}/journal?journal=journal-release#journal-title`,
|
||||
"[data-journal-view]",
|
||||
],
|
||||
[
|
||||
`${ws}/memory/permissions-edit/permissions#permissions-heading`,
|
||||
`${subjects}/permissions-edit/permissions#permissions-heading`,
|
||||
"#permissions-heading",
|
||||
],
|
||||
[
|
||||
`${ws}/memory/empty-subject/conversation?from=bookmark#conversation-subject-heading`,
|
||||
`${subjects}/empty-subject/chat?from=bookmark#conversation-subject-heading`,
|
||||
".conversation-page textarea",
|
||||
],
|
||||
[
|
||||
`${ws}/memory/empty-subject/chat`,
|
||||
`${subjects}/empty-subject/chat`,
|
||||
".conversation-page textarea",
|
||||
],
|
||||
]
|
||||
) await direct(old, selector, next);
|
||||
// replace遷移は旧entryを残さず、戻る/進むで同じURLを往復しない。
|
||||
await direct(subjects, '[data-memory-view="subjects"]');
|
||||
await direct(`${ws}/memory/${subject}`, ".subject-overview", detail);
|
||||
await page.getByRole("link", { name: /Journal ·/ }).click();
|
||||
await page.locator("[data-journal-view]").waitFor();
|
||||
await page.goBack();
|
||||
await page.waitForURL(base + detail);
|
||||
await page.goBack();
|
||||
await page.waitForURL(base + subjects);
|
||||
await page.goForward();
|
||||
await page.waitForURL(base + detail);
|
||||
const beforeUnknown = requests.length;
|
||||
await direct(
|
||||
`${ws}/memory/empty-subject/unknown/deeper`,
|
||||
".legacy-subject-route",
|
||||
);
|
||||
await page.getByText(
|
||||
"This legacy path has no corresponding Subject page.",
|
||||
).waitFor();
|
||||
assertEquals(
|
||||
requests.slice(beforeUnknown).filter((p) =>
|
||||
p.includes("/subjektiv/subjects/")
|
||||
),
|
||||
[],
|
||||
);
|
||||
// 読取り拒否を空一覧/新権限付与へ変えず、UIがBackend失敗をそのまま示す。
|
||||
expectedFailure = true;
|
||||
await context.route(
|
||||
"**/subjektiv/subjects/empty-subject",
|
||||
(route) =>
|
||||
route.fulfill({
|
||||
status: 403,
|
||||
json: { error: "Forbidden", message: "synthetic read denied" },
|
||||
}),
|
||||
);
|
||||
await direct(
|
||||
subjects + "/empty-subject/chat",
|
||||
'.conversation-page [role="alert"]',
|
||||
);
|
||||
await capture("chat-denied");
|
||||
expectedFailure = false;
|
||||
} catch (e) {
|
||||
failures.push(
|
||||
`${colorScheme}-${width}: ${e instanceof Error ? e.stack : e}`,
|
||||
);
|
||||
await page.screenshot({
|
||||
path: join(output, `${colorScheme}-${width}-failure.png`),
|
||||
});
|
||||
throw e;
|
||||
} finally {
|
||||
await context.close();
|
||||
await save();
|
||||
}
|
||||
}
|
||||
}
|
||||
assertEquals(failures, []);
|
||||
assertEquals(faults.filter((f) => !f.expected), []);
|
||||
manifest.status = "passed-automated-not-visual-verdict";
|
||||
} finally {
|
||||
await browser.close();
|
||||
await stopOwnedProcesses(owned);
|
||||
await save();
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
output,
|
||||
status: manifest.status,
|
||||
captures: captures.length,
|
||||
}),
|
||||
);
|
||||
@@ -194,7 +194,7 @@ async function capturePoint(page: Page, id: string, expectedErrors: string[] = [
|
||||
await assertNoPageWideOverflow(page);
|
||||
}
|
||||
async function open(page: Page, base: string, id: string, state = "ready") {
|
||||
await page.goto(`${base}/w/memory-review/memory/${id}/permissions`);
|
||||
await page.goto(`${base}/w/memory-review/subjektiv/${id}/permissions`);
|
||||
await page.locator(`[data-permission-state=${state}][aria-busy=false]`).waitFor();
|
||||
}
|
||||
async function add(
|
||||
@@ -268,7 +268,7 @@ try {
|
||||
const page = await context.newPage();
|
||||
observe(page);
|
||||
// 通常のSubject詳細から管理導線へ進む。
|
||||
await page.goto(`${base}/w/memory-review/memory/permissions-empty`);
|
||||
await page.goto(`${base}/w/memory-review/subjektiv/permissions-empty`);
|
||||
await page.locator(".subject-overview").waitFor();
|
||||
await page.locator('main a[href$="/permissions"]').click();
|
||||
await page.locator("[data-permission-state=ready]").waitFor();
|
||||
|
||||
Reference in New Issue
Block a user