// Caller supplies an isolated browser context. No display/apply operation is performed. import assert from "node:assert/strict"; export async function checkFontFavoritesBrowser(page, baseUrl, fontId = "default") { const errors = []; const onError = error => errors.push(error.message); page.on("pageerror", onError); const catalog = await (await page.request.get(`${baseUrl}/api/fonts`)).json(); const originallyFavorite = catalog.favorite_ids.includes(fontId); const item = catalog.items.find(item => item.id === fontId); assert.ok(item); const update = favorite => page.request.put(`${baseUrl}/api/fonts/favorite`, { data: { id: fontId, favorite } }); const rows = () => page.locator(".font-option-row").filter({ has: page.locator(`[data-font-id="${fontId}"]`) }); const draft = () => page.evaluate(async () => JSON.stringify((await import("/static/scene-board.js")).getScene())); try { await update(false); await page.goto(`${baseUrl}/#canvas`); await page.locator("#new-text-layer").click(); await page.locator("#create-text").click(); const input = page.locator("#text-font"); await input.click(); assert.equal(await page.locator(".font-option-group").first().textContent(), "收藏字体"); const before = await draft(); const selected = await input.inputValue(); await rows().locator("button").first().click(); await page.waitForFunction(id => document.querySelectorAll(`[data-font-id="${id}"]`).length === 2, fontId); assert.equal(await rows().locator('[aria-pressed="true"]').count(), 2); assert.equal(await draft(), before); assert.equal(await input.inputValue(), selected); const ids = await page.locator('[role="option"]').evaluateAll(options => options.map(option => option.id)); assert.equal(new Set(ids).size, ids.length); await rows().locator("button").first().click(); await page.locator(".font-favorite-dialog button").first().click(); assert.equal(await rows().count(), 2); await rows().locator("button").first().click(); await page.keyboard.press("Escape"); assert.equal(await rows().count(), 2); await input.fill(item.family); assert.equal(await rows().count(), 2); await input.press("Home"); await input.press("ArrowDown"); const active = await input.getAttribute("aria-activedescendant"); assert.equal(await page.locator(`[id="${active}"]`).count(), 1); await input.press("End"); await input.press("Escape"); await input.click(); await input.press("Tab"); assert.equal(await page.locator(".font-favorite-button:focus").count(), 1); await page.keyboard.press("Enter"); await page.locator(".font-favorite-dialog button").last().click(); await page.waitForFunction(id => document.querySelectorAll(`[data-font-id="${id}"]`).length === 1, fontId); assert.equal(await draft(), before); // A failed save keeps both catalog and UI unchanged. await page.route("**/api/fonts/favorite", route => route.fulfill({ status: 500, contentType: "application/json", body: '{"detail":"injected failure"}' })); await rows().locator("button").click(); await page.locator("#font-feedback[data-state=error]").waitFor(); assert.equal(await rows().count(), 1); assert.equal(await rows().locator('[aria-pressed="false"]').count(), 1); await page.unroute("**/api/fonts/favorite"); // Another browser changes the shared record; reopening reads it. await input.press("Escape"); const other = await page.context().browser().newContext(); try { const response = await other.request.put(`${baseUrl}/api/fonts/favorite`, { data: { id: fontId, favorite: true } }); assert.equal(response.status(), 200); } finally { await other.close(); } await input.click(); await page.waitForFunction(id => document.querySelectorAll(`[data-font-id="${id}"]`).length === 2, fontId); assert.equal(await draft(), before); const bounds = await page.locator("#text-font-options").boundingBox(); assert.ok(bounds.x >= 0 && bounds.x + bounds.width <= page.viewportSize().width + 1); assert.deepEqual(errors, []); return { passed: true, width: page.viewportSize().width, duplicateOptions: true, sceneUnchanged: true, errors }; } finally { await page.unroute("**/api/fonts/favorite"); await update(originallyFavorite); page.off("pageerror", onError); } }