fix: restore Journal history only after matching query data

This commit is contained in:
2026-10-31 16:04:14 +09:00
parent df8cd2353b
commit af074254ec
2 changed files with 50 additions and 27 deletions
@@ -19,8 +19,7 @@ const root = join(dirname(fromFileUrl(import.meta.url)), "../../..");
const artifacts = join(root, "target/web-ux/t-737-browser-interactions"); const artifacts = join(root, "target/web-ux/t-737-browser-interactions");
const subjectPath = `/w/${fixture.workspaceId}/subjektiv/${fixture.subjectId}`; const subjectPath = `/w/${fixture.workspaceId}/subjektiv/${fixture.subjectId}`;
const journalPath = `${subjectPath}/journal`; const journalPath = `${subjectPath}/journal`;
const memoryPath = const memoryPath = `${subjectPath}/memories/${fixture.memoryId}?change_id=change-current`;
`${subjectPath}/memories/${fixture.memoryId}?change_id=change-current`;
const claim = "Keep subject Memory provenance typed and visible"; const claim = "Keep subject Memory provenance typed and visible";
const entryTitle = "Release milestone 00 · release [a+b] %_"; const entryTitle = "Release milestone 00 · release [a+b] %_";
const detailPath = `${journalPath}?journal=journal-release&entry=entry-00`; const detailPath = `${journalPath}?journal=journal-release&entry=entry-00`;
@@ -87,9 +86,7 @@ Deno.test("Journal fixture UI keeps scoped navigation, bounded links and distinc
requests.push({ requests.push({
path, path,
method: request.method(), method: request.method(),
payload: request.method() === "POST" payload: request.method() === "POST" ? request.postDataJSON() : null,
? request.postDataJSON()
: null,
}); });
} }
}); });
@@ -169,18 +166,14 @@ Deno.test("Journal fixture UI keeps scoped navigation, bounded links and distinc
fixture.literalQuery, fixture.literalQuery,
); );
await page.getByRole("button", { name: "検索", exact: true }).click(); await page.getByRole("button", { name: "検索", exact: true }).click();
await page.waitForURL((url) => await page.waitForURL((url) => url.searchParams.get("q") === fixture.literalQuery);
url.searchParams.get("q") === fixture.literalQuery
);
await page.getByRole("link", { await page.getByRole("link", {
name: new RegExp(entryTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")), name: new RegExp(entryTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")),
}).waitFor(); }).waitFor();
assertEquals(await page.locator(".entry-list > li").count(), 16); assertEquals(await page.locator(".entry-list > li").count(), 16);
await page.getByRole("link", { name: "次のページ →", exact: true }) await page.getByRole("link", { name: "次のページ →", exact: true })
.click(); .click();
await page.waitForURL((url) => await page.waitForURL((url) => url.searchParams.get("after") === "fixture-entries-2");
url.searchParams.get("after") === "fixture-entries-2"
);
assertEquals( assertEquals(
new URL(page.url()).searchParams.get("q"), new URL(page.url()).searchParams.get("q"),
fixture.literalQuery, fixture.literalQuery,
@@ -199,9 +192,7 @@ Deno.test("Journal fixture UI keeps scoped navigation, bounded links and distinc
r.payload.query === fixture.literalQuery r.payload.query === fixture.literalQuery
); );
assert( assert(
searchRequests.some((r) => searchRequests.some((r) => r.payload?.after === "fixture-entries-2"),
r.payload?.after === "fixture-entries-2"
),
); );
}, },
); );
@@ -219,9 +210,7 @@ Deno.test("Journal fixture UI keeps scoped navigation, bounded links and distinc
await page.setViewportSize(viewport); await page.setViewportSize(viewport);
await page.emulateMedia({ colorScheme: viewport.theme }); await page.emulateMedia({ colorScheme: viewport.theme });
await page.goto( await page.goto(
`${base}${journalPath}?q=${ `${base}${journalPath}?q=${encodeURIComponent(fixture.literalQuery)}`,
encodeURIComponent(fixture.literalQuery)
}`,
); );
const target = page.locator(".entry-list a").filter({ const target = page.locator(".entry-list a").filter({
hasText: "Release milestone 12", hasText: "Release milestone 12",
@@ -288,7 +277,45 @@ Deno.test("Journal fixture UI keeps scoped navigation, bounded links and distinc
await page.locator("[data-journal-view='detail']").waitFor(); await page.locator("[data-journal-view='detail']").waitFor();
assertEquals(page.url(), detailUrl); assertEquals(page.url(), detailUrl);
await restoreTop(page, detailTop); await restoreTop(page, detailTop);
await page.goBack(); let listBlocked = false;
const releaseList = Promise.withResolvers<void>();
const continuedList = Promise.withResolvers<void>();
await page.route("**/journal-browser", async (route) => {
if (route.request().postDataJSON()?.action === "entries") {
listBlocked = true;
await releaseList.promise;
try {
await route.continue();
} finally {
continuedList.resolve();
}
} else {
await route.continue();
}
});
try {
const requested = page.waitForRequest((request) =>
request.url().endsWith("/journal-browser") &&
request.postDataJSON()?.action === "entries"
);
await page.goBack();
await requested;
await page.evaluate(() =>
new Promise<void>((resolve) => {
const frame = (globalThis as any).requestAnimationFrame;
frame(() => frame(() => resolve()));
})
);
assertEquals(
await page.locator("[data-journal-view]").count(),
0,
"a query-changing history restore must wait for its own Journal data",
);
} finally {
releaseList.resolve();
if (listBlocked) await continuedList.promise;
await page.unrouteAll({ behavior: "wait" });
}
await page.getByRole("searchbox").waitFor(); await page.getByRole("searchbox").waitFor();
assertEquals(page.url(), listUrl); assertEquals(page.url(), listUrl);
assertEquals( assertEquals(
@@ -391,9 +418,7 @@ Deno.test("Journal fixture UI keeps scoped navigation, bounded links and distinc
.waitFor(); .waitFor();
assertEquals(await popup.locator(".related-list a").count(), 0); assertEquals(await popup.locator(".related-list a").count(), 0);
await popup.close(); await popup.close();
const candidateRequests = requests.filter((r) => const candidateRequests = requests.filter((r) => r.payload?.action === "memory_sources");
r.payload?.action === "memory_sources"
);
assert(candidateRequests.length > 0); assert(candidateRequests.length > 0);
for (const r of candidateRequests) { for (const r of candidateRequests) {
assertEquals(r.payload?.selector.candidate_id, fixture.candidateId); assertEquals(r.payload?.selector.candidate_id, fixture.candidateId);
@@ -538,8 +563,7 @@ Deno.test("Journal fixture UI keeps scoped navigation, bounded links and distinc
for ( for (
const state of [ const state of [
{ {
path: path: `/w/${fixture.workspaceId}/subjektiv/empty-subject/journal?view=sessions`,
`/w/${fixture.workspaceId}/subjektiv/empty-subject/journal?view=sessions`,
text: "Journalはまだありません。", text: "Journalはまだありません。",
role: "status" as const, role: "status" as const,
}, },
@@ -620,9 +644,7 @@ Deno.test("Journal fixture UI keeps scoped navigation, bounded links and distinc
[], [],
); );
for ( for (
const request of requests.filter((r) => const request of requests.filter((r) => r.path.endsWith("/journal-browser"))
r.path.endsWith("/journal-browser")
)
) { ) {
assertEquals(request.method, "POST"); assertEquals(request.method, "POST");
if (request.payload?.limit != null) { if (request.payload?.limit != null) {
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/state';
import DeferredPage from '#lib/workspace/navigation/DeferredPage.svelte'; import DeferredPage from '#lib/workspace/navigation/DeferredPage.svelte';
import ReadingPosition from '#lib/workspace/memory/ReadingPosition.svelte'; import ReadingPosition from '#lib/workspace/memory/ReadingPosition.svelte';
import PageContent from './PageContent.svelte'; import PageContent from './PageContent.svelte';
@@ -10,7 +11,7 @@
restore: (saved: { top: number }) => { position = saved; }, restore: (saved: { top: number }) => { position = saved; },
}; };
</script> </script>
<DeferredPage {data} title="Journal"> <DeferredPage {data} title="Journal" identity={`${page.url.pathname}${page.url.search}`}>
{#snippet children(resolved)} {#snippet children(resolved)}
<PageContent data={resolved} /> <PageContent data={resolved} />
<ReadingPosition {position} restored={() => { position = null; }} /> <ReadingPosition {position} restored={() => { position = null; }} />