From 8407c518bb24df74c9eb2c00adc49795ddc431ba Mon Sep 17 00:00:00 2001 From: turtton Date: Mon, 14 Sep 2026 23:59:53 +0900 Subject: [PATCH 1/3] =?UTF-8?q?test(booskiff-web):=20=E8=AA=8D=E8=A8=BC?= =?UTF-8?q?=E6=B8=88=E3=81=BF=20/drive=20=E3=83=95=E3=83=AB=E3=83=AA?= =?UTF-8?q?=E3=83=AD=E3=83=BC=E3=83=89=E3=81=AE=20resume=20DOM=20=E5=9B=9E?= =?UTF-8?q?=E5=B8=B0=E3=83=86=E3=82=B9=E3=83=88=E3=82=92=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=20(#21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SSR + resumeMount 経路は mock E2E のカバレッジ穴だった。修正前に red (folder-create-submit が2要素) を確認済み。 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../e2e/tests/drive-resume.spec.ts | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 apps/booskiff-web/e2e/tests/drive-resume.spec.ts diff --git a/apps/booskiff-web/e2e/tests/drive-resume.spec.ts b/apps/booskiff-web/e2e/tests/drive-resume.spec.ts new file mode 100644 index 0000000..c4b9273 --- /dev/null +++ b/apps/booskiff-web/e2e/tests/drive-resume.spec.ts @@ -0,0 +1,45 @@ +import { expect, test } from "@playwright/test"; +import { folderRow, loginViaUi, runId } from "./helpers"; + +// Regression for #19 / #21: an authenticated FULL page load of /drive (SSR + +// resumeMount, then CheckSession -> LoadDrive -> *Loaded patches) must not +// corrupt the client DOM. The observed corruption was duplicated folder +// section elements (folder-create-submit / folder-list rendered twice), +// children leaking between sections, and dead event handlers. +test("authenticated /drive full reload keeps a single, working folder section", async ({ + page, +}) => { + await loginViaUi(page); + + // Full page load: SSR HTML + resumeMount + session/drive data patches. + // Register response waits BEFORE the navigation so no patch is missed. + const filesLoaded = page.waitForResponse( + (response) => + response.url().includes("/api/files") && + response.request().method() === "GET", + ); + const foldersLoaded = page.waitForResponse( + (response) => + response.url().includes("/api/folders") && + response.request().method() === "GET", + ); + const billingLoaded = page.waitForResponse( + (response) => response.url().includes("/api/billing/status"), + ); + await page.goto("/drive"); + await filesLoaded; + await foldersLoaded; + await billingLoaded; + await expect(page.getByTestId("drive-page")).toBeVisible(); + + // Folder section elements must exist exactly once (no duplication). + await expect(page.getByTestId("folder-create-submit")).toHaveCount(1); + await expect(page.getByTestId("folder-list")).toHaveCount(1); + await expect(page.getByTestId("folder-name-input")).toHaveCount(1); + + // Event handlers must survive resume: folder creation still works. + const name = `resume-${runId}`; + await page.getByTestId("folder-name-input").fill(name); + await page.getByTestId("folder-create-submit").click(); + await expect(folderRow(page, name)).toBeVisible({ timeout: 15_000 }); +}); From 24dd5b7bbd431ae57fdbd1abc0bf8cec341580e0 Mon Sep 17 00:00:00 2001 From: turtton Date: Tue, 15 Sep 2026 00:00:08 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix(booskiff-web):=20SSR=20resume=20?= =?UTF-8?q?=E6=99=82=E3=81=AE=E7=A9=BA=E3=83=86=E3=82=AD=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=83=8E=E3=83=BC=E3=83=89=E8=B5=B7=E5=9B=A0=E3=81=AE=20DOM=20?= =?UTF-8?q?=E7=A0=B4=E6=90=8D=E3=82=92=E4=BF=AE=E6=AD=A3=20(#21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原因: 「何も描画しない」場所の HE.text "" は HTML パースで消滅するが、Flame の hydration は SSR DOM と仮想 children を index のみで対応付けるため、errorBanner 以降の全兄弟が誤った DOM ノードに紐付き、後続 patch が誤ノードを置換・挿入・削除していた (セクション重複・子ノード混入・ハンドラ不活化)。非表示 div (emptySlot) に置き換え、SSR/client 双方に実ノードを常駐させて index 対応を維持する。追加した回帰テストが red → green になることを確認。 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- apps/booskiff-web/src/App/View/Drive.purs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/apps/booskiff-web/src/App/View/Drive.purs b/apps/booskiff-web/src/App/View/Drive.purs index a3fb103..4d18e23 100644 --- a/apps/booskiff-web/src/App/View/Drive.purs +++ b/apps/booskiff-web/src/App/View/Drive.purs @@ -33,9 +33,17 @@ view model = , fileSection model ] +-- | Invisible stand-in for "render nothing" slots. An empty text node never +-- | survives HTML parsing, but Flame's hydration matches SSR DOM to virtual +-- | children purely by index — a vanished placeholder shifts every following +-- | sibling onto the wrong node and later patches then corrupt the DOM +-- | (duplicated sections, leaked children, dead handlers; #19/#21). +emptySlot :: Html Message +emptySlot = HE.div [ HA.style { display: "none" } ] [] + errorBanner :: Maybe String -> Html Message errorBanner = case _ of - Nothing -> HE.text "" + Nothing -> emptySlot Just msg -> HE.div [ HA.class' @@ -96,8 +104,8 @@ uploadSection model = ] [ HE.text "アップロード" ] ] - , maybe (HE.text "") progressView model.upload - , maybe (HE.text "") uploadErrorView (uploadErrorFor model) + , maybe emptySlot progressView model.upload + , maybe emptySlot uploadErrorView (uploadErrorFor model) ] uploadErrorFor :: Model -> Maybe String @@ -166,7 +174,7 @@ folderSection model = allButton :: Maybe String -> Html Message allButton = case _ of - Nothing -> HE.text "" + Nothing -> emptySlot Just _ -> HE.button [ HA.class' ("text-sm " <> T.navLink) From 9631e4a7ed28244bd562bc775f630a84370c04d9 Mon Sep 17 00:00:00 2001 From: turtton Date: Tue, 15 Sep 2026 00:00:25 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix(booskiff-web):=20#19=20workaround=20?= =?UTF-8?q?=E3=82=92=E8=A7=A3=E9=99=A4=E3=81=97=20real=20=E3=83=AD?= =?UTF-8?q?=E3=82=B0=E3=82=A4=E3=83=B3=E3=81=AE=20return=5Fto=20=E3=82=92?= =?UTF-8?q?=20/drive=20=E3=81=AB=E5=BE=A9=E5=B8=B0=20(#21)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DOM 破損の修正により、OAuth callback 後の /drive フルロードが安全になったため、BFF の next を return_to=/login (SPA 経路回避策) から /drive に戻す。real E2E の永続化検証も API 確認から UI リロード確認 (page.goto("/drive") + folder-list 表示) に復帰。scripts/e2e-real.sh green を確認。 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- apps/booskiff-web/bff/auth-real.test.ts | 2 +- apps/booskiff-web/bff/auth-real.ts | 5 +++-- apps/booskiff-web/e2e/real/tests/auth.spec.ts | 18 ++++++++---------- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/apps/booskiff-web/bff/auth-real.test.ts b/apps/booskiff-web/bff/auth-real.test.ts index cf14ab3..6b699fd 100644 --- a/apps/booskiff-web/bff/auth-real.test.ts +++ b/apps/booskiff-web/bff/auth-real.test.ts @@ -60,7 +60,7 @@ test("returns OAuth next URL and Kratos cookies without an app session when logi // Then: OAuth 開始 URL と Kratos cookie のみを返す。 expect(response?.status).toBe(200); expect(await response?.json()).toEqual({ - authenticated: true, username: "alice@example.test", next: "/auth/oauth/start?return_to=/login", + authenticated: true, username: "alice@example.test", next: "/auth/oauth/start?return_to=/drive", }); expect(response?.headers.getSetCookie().some((cookie) => cookie.startsWith("ory_kratos_session="))).toBe(true); expect(response?.headers.getSetCookie().some((cookie) => cookie.startsWith("booskiff_session="))).toBe(false); diff --git a/apps/booskiff-web/bff/auth-real.ts b/apps/booskiff-web/bff/auth-real.ts index b6bd808..bd2f57c 100644 --- a/apps/booskiff-web/bff/auth-real.ts +++ b/apps/booskiff-web/bff/auth-real.ts @@ -112,8 +112,9 @@ export async function realLogin(req: Request, config: RealAuthConfig, adapter: S const headers = new Headers({ "Content-Type": "application/json" }); jar.applyToResponse(headers); // XHR は 302 で cross-origin Hydra chain を進められないため、トップレベル遷移用 URL を渡す。 - // return_to=/login は #19 の SPA 経路回避策。修正後に /drive へ戻すことを再検討する。 - return new Response(JSON.stringify({ authenticated: true, username: email, next: "/auth/oauth/start?return_to=/login" }), { status: 200, headers }); + // return_to=/drive: callback 後の /drive フルロードで発生していた resume DOM 破損 (#19) は + // #21 で修正済みのため、SPA 経路回避策 (return_to=/login) は解除する。 + return new Response(JSON.stringify({ authenticated: true, username: email, next: "/auth/oauth/start?return_to=/drive" }), { status: 200, headers }); } catch (err) { console.error("Kratos login error:", err); return Response.json({ error: "Authentication service unavailable" }, { status: 502 }); diff --git a/apps/booskiff-web/e2e/real/tests/auth.spec.ts b/apps/booskiff-web/e2e/real/tests/auth.spec.ts index 1b5f5a1..b8f1a9f 100644 --- a/apps/booskiff-web/e2e/real/tests/auth.spec.ts +++ b/apps/booskiff-web/e2e/real/tests/auth.spec.ts @@ -16,10 +16,8 @@ async function enterCredentials(page: Page): Promise { }).toPass({ timeout: 20_000 }); // Real-mode login success hands off to /auth/oauth/start automatically (the // BFF `next` URL), so the Hydra consent screen arrives through the UI flow - // alone. return_to=/login is applied BFF-side: a direct full load of /drive - // after the OAuth callback corrupts the resumed DOM (issue #19), so landing - // on /login and letting the session check navigate client-side follows the - // healthy SPA path. + // alone. return_to=/drive: the OAuth callback lands on a full /drive load, + // whose resumed DOM stays intact since the issue #19 fix (#21). await expect(page.getByRole("button", { name: "Allow", exact: true })).toBeVisible({ timeout: 30_000 }); } @@ -89,12 +87,12 @@ test("real login grants a cookie session, drive access, and logout", async ({ pa const created = page.waitForResponse((response) => new URL(response.url()).pathname === "/api/folders" && response.request().method() === "POST"); await page.getByTestId("folder-create-submit").click(); expect((await created).ok()).toBe(true); - // Persistence is verified through the API: a full /drive reload re-triggers the - // resumed-DOM corruption noted above, so the browser-side reload assertion is - // intentionally left to the follow-up fix. - const folders = await page.request.get("/api/folders"); - expect(folders.ok()).toBe(true); - expect(JSON.stringify(await folders.json())).toContain(folder); + // Persistence is verified through the UI after a full /drive reload: SSR + + // resumeMount must render the stored folder without the DOM corruption that + // issue #19 documented (fixed in #21). + await page.goto("/drive"); + await expect(page.getByTestId("drive-page")).toBeVisible(); + await expect(page.getByTestId("folder-list")).toContainText(folder); await page.getByTestId("logout-button").click(); await expect(page).toHaveURL(/\/login$/);