refactor: organize E2E tests with page objects

This commit is contained in:
2026-10-07 21:13:40 +09:00
parent 1e03e8f4ff
commit 48ef396ec1
28 changed files with 1494 additions and 834 deletions
+2 -1
View File
@@ -28,10 +28,11 @@ test("mutations require exact Origin even with same-origin Fetch Metadata", asyn
test("server functions also reject foreign identity and cross-site calls", async ({
page,
deckPage,
request,
}) => {
const serverRequest = page.waitForRequest((request) => request.url().includes("/_serverFn/"));
await page.goto("/deck");
await deckPage.goTo();
const url = (await serverRequest).url();
expect(
(
+54 -39
View File
@@ -1,83 +1,96 @@
import { openDatabase } from "../../src/features/storage/database.server";
import { expect, test } from "../fixtures";
const origin = "http://127.0.0.1:4173";
test("creates the owner once and enters onboarding with a real session", async ({
page,
context,
a11y,
authPage,
onboardingPage,
homePage,
}) => {
const db = openDatabase(process.env.TWITTER_LITE_E2E_DB_PATH ?? "");
db.$client.exec("DELETE FROM workspace_owner; DELETE FROM auth_throttle;");
db.$client.close();
await context.clearCookies();
await page.goto("/", { waitUntil: "networkidle" });
await homePage.goTo();
await page.waitForLoadState("networkidle");
await expect(page).toHaveURL(/\/setup$/);
expect((await a11y().analyze()).violations).toEqual([]);
await page.getByLabel("Your name", { exact: true }).fill("Yuta");
await page.getByLabel("Email address").fill("[email protected]");
await page.getByLabel("Password", { exact: true }).fill("E2E-only-passphrase-2026");
await page.getByLabel("Confirm password", { exact: true }).fill("E2E-only-passphrase-2026");
await page.getByLabel("Setup code").fill("isolated-e2e-setup-token-not-for-production");
await page.getByRole("button", { name: "Create account", exact: true }).click();
await authPage.createAccount({
name: "Yuta",
email: "[email protected]",
password: "E2E-only-passphrase-2026",
setupCode: "isolated-e2e-setup-token-not-for-production",
});
await expect(page).toHaveURL(/\/onboarding$/);
await page.waitForLoadState("networkidle");
await page.getByRole("button", { name: "Continue", exact: true }).click();
await page.getByRole("button", { name: "Open workspace" }).click();
await expect(page.getByRole("heading", { name: "Good morning, Yuta" })).toBeVisible();
await onboardingPage.complete();
await expect(homePage.greeting("Yuta")).toBeVisible();
});
test("requires login for documents, server functions, and live updates", async ({
page,
context,
authPage,
workspacePage,
deckPage,
journalPage,
baseURL,
}) => {
const serverRequest = page.waitForRequest((request) => request.url().includes("/_serverFn/"));
await page.goto("/deck");
await deckPage.goTo();
const serverUrl = (await serverRequest).url();
await context.clearCookies();
for (const path of ["/api/research/events", serverUrl]) {
const result = await context.request.get(path, {
headers: { Origin: origin, "Sec-Fetch-Site": "same-origin" },
headers: { Origin: new URL("/", baseURL).origin, "Sec-Fetch-Site": "same-origin" },
});
expect(result.status()).toBe(401);
}
await page.goto("/journal");
await journalPage.goTo();
await expect(page).toHaveURL(/\/login(?:\?|$)/);
await expect(page.getByRole("heading", { name: "Welcome back." })).toBeVisible();
await expect(page.getByRole("link", { name: "Journal", exact: true })).toHaveCount(0);
await expect(authPage.welcome).toBeVisible();
await expect(workspacePage.navigationLink("Journal")).toHaveCount(0);
});
test("signs in and revokes the session on sign out", async ({ page, context, a11y }) => {
test("signs in and revokes the session on sign out", async ({
page,
context,
a11y,
authPage,
workspacePage,
connectedAccounts,
}) => {
await context.clearCookies();
await page.goto("/login", { waitUntil: "networkidle" });
await authPage.goTo();
expect((await a11y().analyze()).violations).toEqual([]);
await page.getByLabel("Email address").fill("[email protected]");
await page.getByLabel("Password", { exact: true }).fill("Wrong-passphrase-2026");
await page.getByRole("button", { name: "Sign in", exact: true }).click();
await expect(page.getByRole("alert")).toContainText("Invalid email or password.");
await page.getByLabel("Password", { exact: true }).fill("E2E-only-passphrase-2026");
await page.getByRole("button", { name: "Sign in", exact: true }).click();
await expect(page).toHaveURL(`${origin}/`);
await authPage.signIn("[email protected]", "Wrong-passphrase-2026");
await expect(authPage.error).toContainText("Invalid email or password.");
await authPage.password.fill("E2E-only-passphrase-2026");
await authPage.signInButton.click();
await expect(page).toHaveURL("/");
await page.waitForLoadState("networkidle");
const token = (await context.cookies()).find((cookie) => cookie.name === "workspace_session");
expect(token?.httpOnly).toBe(true);
expect(token?.sameSite).toBe("Lax");
await page.getByRole("button", { name: "Manage connected accounts" }).click();
await page.getByRole("button", { name: "Sign out", exact: true }).click();
await connectedAccounts.open();
await connectedAccounts.signOut.click();
await expect(page).toHaveURL(/\/login(?:\?|$)/);
const replay = await context.request.get("/api/auth", {
headers: { Cookie: `workspace_session=${token?.value}` },
});
expect((await replay.json()).owner).toBeNull();
await page.goBack();
await expect(page.getByRole("link", { name: "Home", exact: true })).toHaveCount(0);
await expect(workspacePage.navigationLink("Home")).toHaveCount(0);
});
test("requires onboarding and remembers its completion and name", async ({
page,
request,
a11y,
onboardingPage,
homePage,
}) => {
const db = openDatabase(process.env.TWITTER_LITE_E2E_DB_PATH ?? "");
db.$client
@@ -85,17 +98,17 @@ test("requires onboarding and remembers its completion and name", async ({
.run();
try {
expect((await request.get("/api/research/events")).status()).toBe(403);
await page.goto("/", { waitUntil: "networkidle" });
await homePage.goTo();
await page.waitForLoadState("networkidle");
await expect(page).toHaveURL(/\/onboarding$/);
expect((await a11y().analyze()).violations).toEqual([]);
await page.getByLabel("What should we call you?").fill("Yuta Test");
await page.getByRole("button", { name: "Continue", exact: true }).click();
await page.getByRole("button", { name: "Open workspace" }).click();
await expect(page.getByRole("heading", { name: "Good morning, Yuta Test" })).toBeVisible();
await onboardingPage.name.fill("Yuta Test");
await onboardingPage.complete();
await expect(homePage.greeting("Yuta Test")).toBeVisible();
await page.reload();
await expect(page.getByRole("heading", { name: "Good morning, Yuta Test" })).toBeVisible();
await page.goto("/onboarding");
await expect(page).toHaveURL(`${origin}/`);
await expect(homePage.greeting("Yuta Test")).toBeVisible();
await onboardingPage.goTo();
await expect(page).toHaveURL("/");
} finally {
db.$client
.prepare("UPDATE workspace_owner SET name = ?, onboarding_completed_at = ? WHERE id = 1")
@@ -108,6 +121,8 @@ test("rejects cross-origin login and further account registration", async ({
request,
page,
context,
authPage,
baseURL,
}) => {
expect(
(
@@ -118,7 +133,7 @@ test("rejects cross-origin login and further account registration", async ({
).status(),
).toBe(403);
const result = await request.post("/api/auth", {
headers: { Origin: origin },
headers: { Origin: new URL("/", baseURL).origin },
data: {
action: "setup",
email: "[email protected]",
@@ -129,6 +144,6 @@ test("rejects cross-origin login and further account registration", async ({
});
expect(result.status()).toBe(409);
await context.clearCookies();
await page.goto("/setup");
await authPage.goTo("setup");
await expect(page).toHaveURL(/\/login(?:\?|$)/);
});
+35 -37
View File
@@ -1,52 +1,50 @@
import { expect, test } from "../fixtures";
test("opens account management after hydration without inheriting real OAuth configuration", async ({
page,
deckPage,
workspacePage,
connectedAccounts,
}) => {
await page.goto("/deck");
await expect(page.getByRole("button", { name: "Rename", includeHidden: true })).toBeEnabled();
const manage = page.getByRole("button", { name: "Manage connected accounts" });
if (!(await manage.isVisible())) {
await page.getByRole("button", { name: "Toggle sidebar" }).click();
}
await expect(manage).toBeEnabled();
await manage.click();
const dialog = page.getByRole("dialog", { name: "Connected accounts" });
await expect(dialog).toBeVisible();
await expect(dialog.getByText("No Mastodon servers have been configured yet.")).toBeVisible();
await expect(dialog.getByRole("button", { name: "Authorize with Mastodon" })).toHaveCount(0);
await page.keyboard.press("Escape");
await expect(dialog).toHaveCount(0);
await expect(manage).toBeFocused();
await deckPage.goTo();
await expect(workspacePage.researchReady).toBeEnabled();
await workspacePage.ensureSidebarOpen();
await expect(workspacePage.manageAccounts).toBeEnabled();
await connectedAccounts.open();
await expect(connectedAccounts.dialog).toBeVisible();
await expect(connectedAccounts.unconfiguredMastodon).toBeVisible();
await expect(connectedAccounts.authorizeMastodon).toHaveCount(0);
await connectedAccounts.close();
await expect(connectedAccounts.dialog).toHaveCount(0);
await expect(workspacePage.manageAccounts).toBeFocused();
});
test("opens account management from the inbox footer and restores focus on close", async ({
page,
readerPage,
workspacePage,
connectedAccounts,
a11y,
}) => {
await page.goto("/inbox");
await page.waitForLoadState("networkidle");
const manage = page.getByRole("button", { name: "Manage connected accounts" });
await expect(manage).toBeVisible();
await manage.click();
const dialog = page.getByRole("dialog", { name: "Connected accounts" });
await expect(dialog).toBeVisible();
await expect(dialog.getByText("No Mastodon servers have been configured yet.")).toBeVisible();
await readerPage.goTo();
await expect(workspacePage.manageAccounts).toBeVisible();
await connectedAccounts.open();
await expect(connectedAccounts.dialog).toBeVisible();
await expect(connectedAccounts.unconfiguredMastodon).toBeVisible();
expect((await a11y().analyze()).violations).toEqual([]);
await page.keyboard.press("Escape");
await expect(dialog).toHaveCount(0);
await expect(manage).toBeFocused();
await expect(page.getByRole("textbox", { name: "Search articles" })).toBeVisible();
await connectedAccounts.close();
await expect(connectedAccounts.dialog).toHaveCount(0);
await expect(workspacePage.manageAccounts).toBeFocused();
await expect(readerPage.searchInput).toBeVisible();
});
test("rejects an unbound OAuth callback without contacting an instance", async ({ page }) => {
test("rejects an unbound OAuth callback without contacting an instance", async ({
page,
deckPage,
workspacePage,
}) => {
await page.goto("/oauth/mastodon/callback?state=invalid&code=not-a-real-code");
await expect(page).toHaveURL(/\/deck\?mastodon=failed$/);
await expect(page.getByRole("alert")).toContainText("Could not complete Mastodon authorization.");
await expect(page.getByRole("button", { name: "Rename", includeHidden: true })).toBeEnabled();
const manage = page.getByRole("button", { name: "Manage connected accounts" });
if (!(await manage.isVisible())) {
await page.getByRole("button", { name: "Toggle sidebar" }).click();
}
await expect(page.getByRole("button", { name: "Manage connected accounts" })).toBeEnabled();
await expect(deckPage.alert).toContainText("Could not complete Mastodon authorization.");
await expect(workspacePage.researchReady).toBeEnabled();
await workspacePage.ensureSidebarOpen();
await expect(workspacePage.manageAccounts).toBeEnabled();
});
+167 -192
View File
@@ -1,152 +1,124 @@
import type { Page } from "@playwright/test";
import { expect, test } from "../fixtures";
import { closeMobileSidebar, showDeckManagement } from "../workspace";
import { DeckPage } from "../models/DeckPage";
async function addColumn(
page: Page,
title: string,
query: string,
profile = "e2e",
kind = "search",
) {
await showDeckManagement(page);
await page.getByRole("button", { name: "Add column", exact: true }).click();
const editor = page.getByRole("form", { name: "Add column", exact: true });
await editor.getByLabel("Column name", { exact: true }).fill(title);
await editor
.getByRole("combobox", { name: "Connection profile", exact: true })
.selectOption({ label: profile });
await editor.getByRole("combobox", { name: "Column type", exact: true }).selectOption(kind);
await editor
.getByLabel(kind === "search" ? "Twitter search query" : kind === "user" ? "User" : "List", {
exact: true,
})
.fill(query);
await editor.getByRole("button", { name: "Add and search" }).click();
await closeMobileSidebar(page);
await expect(
page.getByRole("region", { name: title, exact: true }).locator("article").first(),
).toBeVisible();
}
test.beforeEach(async ({ page }) => {
await page.goto("/deck");
await expect(page.getByRole("button", { name: "Save as deck", exact: true })).toBeEnabled();
await page.getByRole("button", { name: "Save as deck", exact: true }).click();
await showDeckManagement(page);
await expect(page.getByText("Saved to server · Shared across devices")).toBeVisible();
await closeMobileSidebar(page);
test.beforeEach(async ({ deckPage, workspacePage }) => {
await deckPage.goTo();
await expect(deckPage.saveAsDeck).toBeEnabled();
await deckPage.saveAsDeck.click();
await workspacePage.showDeckManagement();
await expect(deckPage.savedNotice).toBeVisible();
await workspacePage.closeMobileSidebar();
});
test("binds identical searches to independent profiles through paging, editing and reload", async ({
page,
deckPage,
}) => {
await addColumn(page, "通常の接続", "same-query");
await addColumn(page, "別の接続", "same-query", "alternate");
const first = page.getByRole("region", { name: "通常の接続", exact: true });
const second = page.getByRole("region", { name: "別の接続", exact: true });
await expect(first.getByText("Latest · all page 1", { exact: true })).toBeVisible();
await expect(second.getByText("alternate · Latest · all page 1", { exact: true })).toBeVisible();
await first.getByRole("button", { name: "Load more" }).click();
await expect(first.locator("article")).toHaveCount(2);
await expect(second.locator("article")).toHaveCount(1);
await second.getByRole("button", { name: "Load more" }).click();
await expect(second.getByText("alternate · Latest · all page 2")).toBeVisible();
await second.getByRole("button", { name: "Actions for 別の接続" }).click();
await page.getByRole("menuitem", { name: "Edit", exact: true }).click();
await page
.getByRole("combobox", { name: "Connection profile", exact: true })
.selectOption({ label: "e2e" });
await page.getByRole("button", { name: "Save changes" }).click();
await expect(second.getByText("Latest · all page 1", { exact: true })).toBeVisible();
await expect(second.getByText("alternate · Latest · all page 1")).toHaveCount(0);
await deckPage.addColumn("通常の接続", "same-query");
await deckPage.addColumn("別の接続", "same-query", "alternate");
await expect(deckPage.columnPostText("通常の接続", "Latest · all page 1", true)).toBeVisible();
await expect(
deckPage.columnPostText("別の接続", "alternate · Latest · all page 1", true),
).toBeVisible();
await deckPage.loadMore("通常の接続");
await expect(deckPage.columnPosts("通常の接続")).toHaveCount(2);
await expect(deckPage.columnPosts("別の接続")).toHaveCount(1);
await deckPage.loadMore("別の接続");
await expect(
deckPage.columnPostText("別の接続", "alternate · Latest · all page 2"),
).toBeVisible();
await deckPage.openColumnActions("別の接続");
await deckPage.editColumn.click();
await deckPage.editorProfile.selectOption({ label: "e2e" });
await deckPage.saveChanges.click();
await expect(deckPage.columnPostText("別の接続", "Latest · all page 1", true)).toBeVisible();
await expect(deckPage.columnPostText("別の接続", "alternate · Latest · all page 1")).toHaveCount(
0,
);
await page.reload();
await expect(second.getByText("Latest · all page 1", { exact: true })).toBeVisible();
await expect(first.locator("article")).toHaveCount(1);
await expect(deckPage.columnPostText("別の接続", "Latest · all page 1", true)).toBeVisible();
await expect(deckPage.columnPosts("通常の接続")).toHaveCount(1);
});
test("creates, switches, renames and deletes persisted deck profiles", async ({ page }) => {
await page.getByRole("button", { name: "Rename" }).click();
await page.getByLabel("Research topic / deck name").fill("最初の調査");
await page.getByRole("button", { name: "Save name" }).click();
await addColumn(page, "最初の観点", "first-deck");
await showDeckManagement(page);
await page.getByRole("button", { name: "Create deck", exact: true }).click();
await page.getByLabel("New deck name").fill("別の調査");
await page.getByRole("button", { name: "Create", exact: true }).click();
await expect(page.locator("[data-testid='deck-column']")).toHaveCount(0);
await addColumn(page, "別の観点", "second-deck", "alternate");
await showDeckManagement(page);
await page.getByLabel("Deck profile", { exact: true }).selectOption({ label: "最初の調査" });
await closeMobileSidebar(page);
await expect(page.locator("[data-testid='deck-column'] h2")).toHaveText(["最初の観点"]);
test("creates, switches, renames and deletes persisted deck profiles", async ({
page,
deckPage,
workspacePage,
}) => {
await deckPage.rename.click();
await deckPage.deckName.fill("最初の調査");
await deckPage.saveName.click();
await deckPage.addColumn("最初の観点", "first-deck");
await workspacePage.showDeckManagement();
await deckPage.createDeck.click();
await deckPage.newDeckName.fill("別の調査");
await deckPage.confirmCreate.click();
await expect(deckPage.columns).toHaveCount(0);
await deckPage.addColumn("別の観点", "second-deck", "alternate");
await workspacePage.showDeckManagement();
await deckPage.deckProfile.selectOption({ label: "最初の調査" });
await workspacePage.closeMobileSidebar();
await expect(deckPage.columnTitles).toHaveText(["最初の観点"]);
await page.reload();
await expect(page.getByRole("heading", { level: 1 })).toHaveText("最初の調査");
await showDeckManagement(page);
await page.getByLabel("Deck profile", { exact: true }).selectOption({ label: "別の調査" });
await closeMobileSidebar(page);
await expect(page.locator("[data-testid='deck-column'] h2")).toHaveText(["別の観点"]);
await showDeckManagement(page);
await page.getByRole("button", { name: "Delete deck", exact: true }).click();
await page.getByRole("button", { name: "Delete", exact: true }).click();
await expect(deckPage.heading).toHaveText("最初の調査");
await workspacePage.showDeckManagement();
await deckPage.deckProfile.selectOption({ label: "別の調査" });
await workspacePage.closeMobileSidebar();
await expect(deckPage.columnTitles).toHaveText(["別の観点"]);
await workspacePage.showDeckManagement();
await deckPage.deleteDeck.click();
await deckPage.confirmDelete.click();
await page.reload();
await expect(page.getByRole("heading", { level: 1 })).toHaveText("最初の調査");
await showDeckManagement(page);
await expect(page.getByLabel("Deck profile", { exact: true }).locator("option")).toHaveCount(1);
await expect(deckPage.heading).toHaveText("最初の調査");
await workspacePage.showDeckManagement();
await expect(deckPage.profileOptions).toHaveCount(1);
});
test("reads user and list timelines within columns and reorders, deletes and restores them", async ({
page,
deckPage,
}) => {
await addColumn(page, "アカウント", "yuta", "alternate", "user");
await addColumn(page, "リストの投稿", "1234567890", "e2e", "list");
const user = page.getByRole("region", { name: "アカウント", exact: true });
const list = page.getByRole("region", { name: "リストの投稿", exact: true });
await expect(user.getByText("alternate profile page 1")).toBeVisible();
await expect(list.getByText("list page 1")).toBeVisible();
await list.getByRole("button", { name: "Load more" }).click();
await expect(list.getByText("list page 2")).toBeVisible();
await list.getByRole("button", { name: "Actions for リストの投稿" }).click();
await page.getByRole("menuitem", { name: "Move リストの投稿 left" }).click();
await expect(page.locator("[data-testid='deck-column'] h2")).toHaveText([
"リストの投稿",
"アカウント",
]);
await list.getByRole("button", { name: "Actions for リストの投稿" }).click();
await page.getByRole("menuitem", { name: "Delete リストの投稿" }).click();
await page.getByRole("button", { name: "Undo" }).click();
await expect(page.locator("[data-testid='deck-column'] h2")).toHaveText([
"リストの投稿",
"アカウント",
]);
await deckPage.addColumn("アカウント", "yuta", "alternate", "user");
await deckPage.addColumn("リストの投稿", "1234567890", "e2e", "list");
await expect(deckPage.columnPostText("アカウント", "alternate profile page 1")).toBeVisible();
await expect(deckPage.columnPostText("リストの投稿", "list page 1")).toBeVisible();
await deckPage.loadMore("リストの投稿");
await expect(deckPage.columnPostText("リストの投稿", "list page 2")).toBeVisible();
await deckPage.openColumnActions("リストの投稿");
await deckPage.columnAction("Move リストの投稿 left").click();
await expect(deckPage.columnTitles).toHaveText(["リストの投稿", "アカウント"]);
await deckPage.openColumnActions("リストの投稿");
await deckPage.columnAction("Delete リストの投稿").click();
await deckPage.undo.click();
await expect(deckPage.columnTitles).toHaveText(["リストの投稿", "アカウント"]);
await page.reload();
await expect(user.getByText("alternate profile page 1")).toBeVisible();
await expect(list.getByText("list page 1")).toBeVisible();
await expect(deckPage.columnPostText("アカウント", "alternate profile page 1")).toBeVisible();
await expect(deckPage.columnPostText("リストの投稿", "list page 1")).toBeVisible();
});
test("keeps other columns usable when pagination fails and retries that column", async ({
page,
deckPage,
}, testInfo) => {
await addColumn(page, "失敗する検索", `retry-deck-${testInfo.project.name}`);
await addColumn(page, "別の検索", "independent-deck");
const failed = page.getByRole("region", { name: "失敗する検索", exact: true });
const other = page.getByRole("region", { name: "別の検索", exact: true });
await failed.getByRole("button", { name: "Load more" }).click();
await expect(failed.getByRole("alert")).toBeVisible();
await expect(failed.locator("article")).toHaveCount(1);
await other.getByRole("button", { name: "Load more" }).click();
await expect(other.locator("article")).toHaveCount(2);
await failed.getByRole("button", { name: "Retry" }).click();
await expect(failed.locator("article")).toHaveCount(2);
await deckPage.addColumn("失敗する検索", `retry-deck-${testInfo.project.name}`);
await deckPage.addColumn("別の検索", "independent-deck");
await deckPage.loadMore("失敗する検索");
await expect(deckPage.columnError("失敗する検索")).toBeVisible();
await expect(deckPage.columnPosts("失敗する検索")).toHaveCount(1);
await deckPage.loadMore("別の検索");
await expect(deckPage.columnPosts("別の検索")).toHaveCount(2);
await deckPage.retryColumn("失敗する検索");
await expect(deckPage.columnPosts("失敗する検索")).toHaveCount(2);
});
test("keeps the deck and editor accessible within narrow viewports", async ({
page,
deckPage,
workspacePage,
a11y,
}, testInfo) => {
await addColumn(page, "日本語の反応", "WebMCP lang:ja");
await addColumn(page, "海外の反応", "WebMCP lang:en", "alternate");
await addColumn(page, "公式アカウント", "OpenAI", "e2e", "user");
await deckPage.addColumn("日本語の反応", "WebMCP lang:ja");
await deckPage.addColumn("海外の反応", "WebMCP lang:en", "alternate");
await deckPage.addColumn("公式アカウント", "OpenAI", "e2e", "user");
for (const width of [320, 375, 414, 768]) {
await page.setViewportSize({ width, height: 900 });
expect(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth)).toBe(
@@ -155,7 +127,7 @@ test("keeps the deck and editor accessible within narrow viewports", async ({
expect(await page.evaluate(() => document.documentElement.scrollHeight <= innerHeight)).toBe(
true,
);
const board = await page.locator("[data-testid='deck-board']").boundingBox();
const board = await deckPage.board.boundingBox();
expect(board).not.toBeNull();
expect((board?.y ?? 0) + (board?.height ?? 0)).toBeLessThanOrEqual(901);
}
@@ -163,15 +135,15 @@ test("keeps the deck and editor accessible within narrow viewports", async ({
width: testInfo.project.name === "desktop" ? 1440 : 393,
height: 900,
});
await expect(page.getByRole("navigation", { name: "Columns" })).toHaveCount(0);
await expect(page.getByRole("heading", { name: "公式アカウント", exact: true })).toBeVisible();
await expect(deckPage.columnNavigation).toHaveCount(0);
await expect(deckPage.columnHeading("公式アカウント")).toBeVisible();
expect((await a11y().analyze()).violations).toEqual([]);
await page.screenshot({
path: testInfo.outputPath("deck-populated.png"),
fullPage: true,
});
await showDeckManagement(page);
await page.getByRole("button", { name: "Add column", exact: true }).click();
await workspacePage.showDeckManagement();
await deckPage.addColumnButton.click();
expect((await a11y().analyze()).violations).toEqual([]);
await page.screenshot({
path: testInfo.outputPath("deck.png"),
@@ -181,57 +153,53 @@ test("keeps the deck and editor accessible within narrow viewports", async ({
test("closes the column editor with Escape and restores focus without applying edits", async ({
page,
deckPage,
workspacePage,
}) => {
await showDeckManagement(page);
const opener = page.getByRole("button", { name: "Add column", exact: true });
await workspacePage.showDeckManagement();
const opener = deckPage.addColumnButton;
await opener.click();
await expect(page.getByRole("dialog", { name: "Add column", exact: true })).toBeVisible();
await page.getByLabel("Column name", { exact: true }).fill("キャンセルする観点");
await expect(deckPage.columnDialog).toBeVisible();
await deckPage.columnName.fill("キャンセルする観点");
await page.keyboard.press("Escape");
await expect(page.getByRole("dialog", { name: "Add column", exact: true })).toHaveCount(0);
await expect(deckPage.columnDialog).toHaveCount(0);
await expect(opener).toBeFocused();
await expect(page.locator("[data-testid='deck-column']")).toHaveCount(0);
await expect(deckPage.columns).toHaveCount(0);
await opener.click();
await expect(page.getByLabel("Column name", { exact: true })).toHaveValue("");
await expect(deckPage.columnName).toHaveValue("");
});
test("does not overwrite unreadable storage on reload", async ({ page }) => {
test("does not overwrite unreadable storage on reload", async ({ page, deckPage }) => {
await page.evaluate(() => localStorage.setItem("twitter-lite-research-deck", "{broken"));
await page.reload();
await expect(page.getByRole("alert")).toContainText("Unable to load");
await expect(deckPage.alert).toContainText("Unable to load");
expect(await page.evaluate(() => localStorage.getItem("twitter-lite-research-deck"))).toBe(
"{broken",
);
});
test("selects a list from the column profile catalog and refreshes it on profile change", async ({
page,
deckPage,
workspacePage,
}) => {
await showDeckManagement(page);
await page.getByRole("button", { name: "Add column", exact: true }).click();
await page.getByLabel("Column name", { exact: true }).fill("購読リスト");
await page
.getByRole("combobox", { name: "Connection profile", exact: true })
.selectOption({ label: "e2e" });
await page.getByRole("combobox", { name: "Column type", exact: true }).selectOption("list");
await expect(page.getByRole("button", { name: "My E2E List", exact: true })).toBeVisible();
await page
.getByRole("combobox", { name: "Connection profile", exact: true })
.selectOption({ label: "alternate" });
await expect(page.getByRole("button", { name: "My E2E List", exact: true })).toHaveCount(0);
await page.getByRole("button", { name: "Alternate List", exact: true }).click();
await expect(page.getByLabel("List", { exact: true })).toHaveValue("1234567890");
await page.getByRole("button", { name: "Add and search" }).click();
await closeMobileSidebar(page);
await expect(
page
.getByRole("region", { name: "購読リスト", exact: true })
.getByText("list page 1", { exact: true }),
).toBeVisible();
await workspacePage.showDeckManagement();
await deckPage.addColumnButton.click();
await deckPage.columnName.fill("購読リスト");
await deckPage.editorProfile.selectOption({ label: "e2e" });
await deckPage.columnType.selectOption("list");
await expect(deckPage.primaryList).toBeVisible();
await deckPage.editorProfile.selectOption({ label: "alternate" });
await expect(deckPage.primaryList).toHaveCount(0);
await deckPage.alternateList.click();
await expect(deckPage.listId).toHaveValue("1234567890");
await deckPage.addAndSearch.click();
await workspacePage.closeMobileSidebar();
await expect(deckPage.columnPostText("購読リスト", "list page 1", true)).toBeVisible();
});
test("keeps an open draft through remote edits and rejects its stale save", async ({
page,
deckPage,
browser,
baseURL,
}) => {
@@ -242,21 +210,22 @@ test("keeps an open draft through remote edits and rejects its stale save", asyn
});
try {
const device = await other.newPage();
await device.goto("/deck");
await page.getByRole("button", { name: "Rename" }).click();
await page.getByLabel("Research topic / deck name").fill("編集中の名前");
await device.getByRole("button", { name: "Rename" }).click();
await device.getByLabel("Research topic / deck name").fill("別の端末の変更");
await device.getByRole("button", { name: "Save name" }).click();
await expect(device.getByRole("heading", { level: 1 })).toHaveText("別の端末の変更");
const deviceDeck = new DeckPage(device);
await deviceDeck.goTo();
await deckPage.rename.click();
await deckPage.deckName.fill("編集中の名前");
await deviceDeck.rename.click();
await deviceDeck.deckName.fill("別の端末の変更");
await deviceDeck.saveName.click();
await expect(deviceDeck.heading).toHaveText("別の端末の変更");
await page.evaluate(() => window.dispatchEvent(new Event("focus")));
await expect(page.getByLabel("Research topic / deck name")).toHaveValue("編集中の名前");
await page.getByRole("button", { name: "Save name" }).click();
await expect(page.getByRole("alert")).toContainText("This deck changed on another device");
await expect(page.getByLabel("Research topic / deck name")).toHaveValue("編集中の名前");
await expect(deckPage.deckName).toHaveValue("編集中の名前");
await deckPage.saveName.click();
await expect(deckPage.alert).toContainText("This deck changed on another device");
await expect(deckPage.deckName).toHaveValue("編集中の名前");
await page.keyboard.press("Escape");
await page.getByRole("button", { name: "Reload latest deck" }).click();
await expect(page.getByRole("heading", { level: 1 })).toHaveText("別の端末の変更");
await deckPage.reloadLatest.click();
await expect(deckPage.heading).toHaveText("別の端末の変更");
} finally {
await other.close();
}
@@ -264,6 +233,8 @@ test("keeps an open draft through remote edits and rejects its stale save", asyn
test("imports old browser decks explicitly and avoids duplicate imports after retry", async ({
page,
deckPage,
workspacePage,
}) => {
const legacy = {
version: 2,
@@ -294,40 +265,44 @@ test("imports old browser decks explicitly and avoids duplicate imports after re
legacy,
);
await page.reload();
await showDeckManagement(page);
const selector = page.getByLabel("Deck profile", { exact: true });
await expect(selector.locator("option")).toHaveCount(1);
await closeMobileSidebar(page);
await page.getByRole("button", { name: "Import legacy decks" }).click();
await showDeckManagement(page);
await expect(selector.locator("option")).toHaveCount(2);
await workspacePage.showDeckManagement();
const selector = deckPage.deckProfile;
await expect(deckPage.profileOptions).toHaveCount(1);
await workspacePage.closeMobileSidebar();
await deckPage.importLegacy.click();
await workspacePage.showDeckManagement();
await expect(deckPage.profileOptions).toHaveCount(2);
await selector.selectOption({ label: "以前の調査" });
await closeMobileSidebar(page);
await expect(page.getByText("Latest · all page 1", { exact: true })).toBeVisible();
await workspacePage.closeMobileSidebar();
await expect(deckPage.postText("Latest · all page 1")).toBeVisible();
await page.evaluate(
(legacy) => localStorage.setItem("twitter-lite-research-deck", JSON.stringify(legacy)),
legacy,
);
await page.reload();
await page.getByRole("button", { name: "Import legacy decks" }).click();
await expect(page.getByRole("button", { name: "Import legacy decks" })).toHaveCount(0);
await showDeckManagement(page);
await expect(selector.locator("option")).toHaveCount(2);
await deckPage.importLegacy.click();
await expect(deckPage.importLegacy).toHaveCount(0);
await workspacePage.showDeckManagement();
await expect(deckPage.profileOptions).toHaveCount(2);
});
test("preserves a new deck dialog and its draft across the mobile breakpoint", async ({ page }) => {
await showDeckManagement(page);
await page.getByRole("button", { name: "Create deck", exact: true }).click();
const dialog = page.getByRole("dialog", { name: "Create deck", exact: true });
await dialog.getByLabel("New deck name").fill("画面を回転しても残る調査");
test("preserves a new deck dialog and its draft across the mobile breakpoint", async ({
page,
deckPage,
workspacePage,
}) => {
await workspacePage.showDeckManagement();
await deckPage.createDeck.click();
const dialog = deckPage.createDialog;
await deckPage.newDeckName.fill("画面を回転しても残る調査");
for (const width of [1024, 375, 1024]) {
await page.setViewportSize({ width, height: 900 });
await expect(dialog).toHaveCount(1);
await expect(dialog).toBeVisible();
await expect(dialog.getByLabel("New deck name")).toHaveValue("画面を回転しても残る調査");
await expect(page.locator("#new-deck-title")).toHaveCount(1);
await expect(deckPage.newDeckName).toHaveValue("画面を回転しても残る調査");
await expect(deckPage.newDeckTitleInput).toHaveCount(1);
}
await dialog.getByRole("button", { name: "Create", exact: true }).click();
await deckPage.confirmCreate.click();
await expect(dialog).toHaveCount(0);
await expect(page.getByRole("heading", { level: 1 })).toHaveText("画面を回転しても残る調査");
await expect(deckPage.heading).toHaveText("画面を回転しても残る調査");
});
+99 -101
View File
@@ -3,187 +3,185 @@ import { expect, test } from "../fixtures";
test("executes routine steps and records a measurement in the shared journal", async ({
page,
a11y,
homePage,
journalPage,
workspacePage,
}, testInfo) => {
await page.goto("/");
await homePage.goTo();
await page.waitForLoadState("networkidle");
await page.getByRole("button", { name: /Morning routine 0 of 3/ }).click();
await homePage.morningRoutine.click();
await expect(page).toHaveURL(/activity=task/);
await page.getByRole("checkbox", { name: /Drink water/ }).check();
await expect(page.getByRole("button", { name: "Finish routine" })).toBeDisabled();
await page.getByRole("checkbox", { name: /Get ready/ }).check();
await page.getByRole("checkbox", { name: /Have breakfast/ }).check();
await homePage.routineStep(/Drink water/).check();
await expect(homePage.finishRoutine).toBeDisabled();
await homePage.routineStep(/Get ready/).check();
await homePage.routineStep(/Have breakfast/).check();
expect((await a11y().analyze()).violations).toEqual([]);
await page.screenshot({ path: testInfo.outputPath("home-routine.png"), fullPage: true });
await page.getByRole("button", { name: "Finish routine" }).click();
await page.getByRole("button", { name: /Record weight A quick check-in/ }).click();
await page.getByRole("spinbutton", { name: "Weight", exact: true }).fill("65.2");
await page.getByRole("button", { name: "Save measurement" }).click();
await expect(
page.getByRole("button", { name: /Record weight Recorded · 65.2 kg/ }),
).toBeVisible();
await page.getByRole("link", { name: "Journal", exact: true }).first().click();
await expect(page.getByRole("article", { name: "Weight entry" })).toContainText("65.2 kg");
await page.getByRole("link", { name: "Home", exact: true }).first().click();
await expect(page.getByRole("button", { name: "Restore Morning routine" })).toBeVisible();
await homePage.finishRoutine.click();
await homePage.recordMeasurement("65.2");
await expect(homePage.recordedWeight("65.2")).toBeVisible();
await workspacePage.navigateTo("Journal");
await expect(journalPage.weightEntries).toContainText("65.2 kg");
await workspacePage.navigateTo("Home");
await expect(homePage.restoreTask("Morning routine")).toBeVisible();
});
test("shows the day flow and conversation context at desktop width", async ({ page }, testInfo) => {
test("shows the day flow and conversation context at desktop width", async ({
page,
homePage,
messagesPage,
}, testInfo) => {
await page.setViewportSize({ width: 1440, height: 1080 });
await page.goto("/");
await homePage.goTo();
await page.waitForLoadState("networkidle");
await expect(page.getByRole("region", { name: "Today’s plan" })).toBeVisible();
await expect(homePage.todayPlan).toBeVisible();
await page.screenshot({ path: testInfo.outputPath("home-desktop.png") });
await page.goto("/support?conversation=design-review");
await messagesPage.goTo("?conversation=design-review");
await page.waitForLoadState("networkidle");
await expect(page.getByRole("textbox", { name: "Reply", exact: true })).toBeVisible();
await expect(messagesPage.reply).toBeVisible();
await page.screenshot({ path: testInfo.outputPath("messages-desktop.png") });
});
test("previews a natural-language memo and shares the executed record with journal", async ({
page,
homePage,
journalPage,
workspacePage,
}) => {
await page.goto("/");
await homePage.goTo();
await page.waitForLoadState("networkidle");
const memo = "明日のレビューは導線から確認する";
await page.getByRole("textbox", { name: "Home input" }).fill(`Note: ${memo}`);
await page.getByRole("button", { name: "Preview action" }).click();
const execute = page.getByRole("button", { name: "Apply", exact: true });
await homePage.previewCommand(`Note: ${memo}`);
const execute = homePage.applyAction;
await expect(execute).toBeVisible();
await execute.click();
await page.getByRole("link", { name: "Journal", exact: true }).first().click();
await expect(page.getByRole("article", { name: "Note entry" })).toContainText(memo);
await page.getByRole("link", { name: "Home", exact: true }).first().click();
await expect(page.getByRole("textbox", { name: "Home input" })).toBeVisible();
await workspacePage.navigateTo("Journal");
await expect(journalPage.noteEntries).toContainText(memo);
await workspacePage.navigateTo("Home");
await expect(homePage.input).toBeVisible();
});
test("keeps the home usable on narrow screens", async ({ page, a11y }, testInfo) => {
await page.goto("/");
test("keeps the home usable on narrow screens", async ({ page, a11y, homePage }, testInfo) => {
await homePage.goTo();
await page.waitForLoadState("networkidle");
for (const width of [320, 375, 414, 768]) {
await page.setViewportSize({ width, height: 850 });
await expect(page.getByRole("textbox", { name: "Home input" })).toBeVisible();
await expect(homePage.input).toBeVisible();
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBe(width);
await page.screenshot({ path: testInfo.outputPath(`home-${width}.png`) });
if (width === 320) expect((await a11y().analyze()).violations).toEqual([]);
}
});
test("adds tasks only after confirmation and can defer and restore them", async ({ page }) => {
await page.goto("/");
test("adds tasks only after confirmation and can defer and restore them", async ({
page,
homePage,
}) => {
await homePage.goTo();
await page.waitForLoadState("networkidle");
const input = page.getByRole("textbox", { name: "Home input" });
const complete = page.getByRole("button", {
name: "Complete grocery shopping",
exact: true,
});
await input.fill("Add grocery shopping");
await page.getByRole("button", { name: "Preview action" }).click();
const complete = homePage.completeTask("grocery shopping");
await homePage.previewCommand("Add grocery shopping");
await expect(complete).toHaveCount(0);
await page.getByRole("button", { name: "Apply", exact: true }).click();
await homePage.applyAction.click();
await expect(complete).toBeVisible();
await input.fill("Defer grocery shopping");
await page.getByRole("button", { name: "Preview action" }).click();
await homePage.previewCommand("Defer grocery shopping");
await expect(complete).toBeVisible();
await page.getByRole("button", { name: "Apply", exact: true }).click();
await homePage.applyAction.click();
await expect(complete).toHaveCount(0);
await page.getByRole("button", { name: "Restore grocery shopping", exact: true }).click();
await homePage.restoreTask("grocery shopping").click();
await complete.click();
await expect(complete).toHaveCount(0);
await expect(
page.getByRole("button", { name: "Restore grocery shopping", exact: true }),
).toBeVisible();
await expect(homePage.restoreTask("grocery shopping")).toBeVisible();
});
test("opens a conversation page from home and retains outcomes on return", async ({
page,
a11y,
homePage,
messagesPage,
workspacePage,
}) => {
await page.goto("/");
await homePage.goTo();
await page.waitForLoadState("networkidle");
const conversationLink = page.getByRole("link", { name: "Open conversation with Haruka Aoki" });
const conversationLink = homePage.conversation("Haruka Aoki");
await expect(conversationLink).toHaveAttribute("href", /conversation=design-review/);
await conversationLink.click();
await expect(page).toHaveURL(/\/support\?.*conversation=design-review/);
await expect(page.getByRole("dialog")).toHaveCount(0);
await expect(page.getByRole("textbox", { name: "Reply", exact: true })).toBeVisible();
await expect(messagesPage.dialogs).toHaveCount(0);
await expect(messagesPage.reply).toBeVisible();
expect((await a11y().analyze()).violations).toEqual([]);
await page.goBack();
await expect(conversationLink).toBeVisible();
await page.goForward();
await expect(page.getByRole("textbox", { name: "Reply", exact: true })).toBeVisible();
await expect(messagesPage.reply).toBeVisible();
await page.reload();
await page.waitForLoadState("networkidle");
await expect(page.getByRole("textbox", { name: "Reply", exact: true })).toBeVisible();
await page.getByRole("button", { name: "Leave for later" }).click();
await page.getByRole("link", { name: "Home", exact: true }).first().click();
await expect(messagesPage.reply).toBeVisible();
await messagesPage.leaveForLater.click();
await workspacePage.navigateTo("Home");
await expect(conversationLink).toContainText("Set aside");
await conversationLink.click();
await expect(page).toHaveURL(/conversation=design-review/);
await expect(page.getByRole("button", { name: "Move to Inbox" })).toBeVisible();
await page.getByRole("button", { name: "Move to Inbox" }).click();
const back = page.getByRole("button", { name: "Back to messages" });
if (await back.isVisible()) await back.click();
await page.getByRole("link", { name: "Home", exact: true }).first().click();
await expect(messagesPage.moveToInbox).toBeVisible();
await messagesPage.moveToInbox.click();
await messagesPage.showConversationList();
await workspacePage.navigateTo("Home");
await expect(conversationLink).toBeVisible();
});
test("updates the prose brief as priorities change and opens the referenced conversation", async ({
page,
homePage,
messagesPage,
workspacePage,
}) => {
await page.goto("/");
await homePage.goTo();
await page.waitForLoadState("networkidle");
const brief = page.getByRole("region", { name: "Daily brief" });
const brief = homePage.dailyBrief;
await expect(brief).toContainText("Review the documents");
await expect(brief).toContainText("3 conversations");
await page.getByRole("button", { name: "Complete Review the documents", exact: true }).click();
await homePage.completeTask("Review the documents").click();
await expect(brief).not.toContainText("Review the documents");
await brief.getByRole("button", { name: "Reply to Haruka Aoki" }).click();
await page.getByRole("button", { name: "Leave for later" }).click();
await page.getByRole("link", { name: "Home", exact: true }).first().click();
await homePage.openBriefConversation("Haruka Aoki");
await messagesPage.leaveForLater.click();
await workspacePage.navigateTo("Home");
await expect(brief).toContainText("2 conversations");
await expect(brief).not.toContainText("Haruka Aoki");
await page.getByRole("textbox", { name: "Home input" }).fill("再開地点:資料の2ページ目から");
await page.getByRole("button", { name: "Preview action" }).click();
await page.getByRole("button", { name: "Apply", exact: true }).click();
await homePage.previewCommand("再開地点:資料の2ページ目から");
await homePage.applyAction.click();
await expect(brief).toContainText("資料の2ページ目から");
});
test("keeps completed actions in their original positions with visible outcomes", async ({
page,
a11y,
homePage,
messagesPage,
workspacePage,
}, testInfo) => {
await page.goto("/");
await homePage.goTo();
await page.waitForLoadState("networkidle");
const titles = page.locator("[data-testid='execution-flow-title']");
const titles = homePage.flowTitles;
const before = await titles.allTextContents();
await page.getByRole("button", { name: "Complete Review the documents", exact: true }).click();
await expect(
page.getByRole("button", { name: "Restore Review the documents", exact: true }),
).toHaveAttribute("aria-pressed", "true");
await expect(
page.getByRole("button", { name: "Review the documents Completed", exact: true }),
).toBeVisible();
await page.getByRole("link", { name: "Open conversation with Haruka Aoki" }).click();
await page.getByRole("button", { name: "Review reply", exact: true }).click();
await page
.getByRole("dialog", { name: "Review reply", exact: true })
.getByRole("button", { name: "Try sending" })
.click();
await page.getByRole("link", { name: "Home", exact: true }).first().click();
await expect(
page.getByRole("link", { name: "Open conversation with Haruka Aoki" }),
).toContainText("Reply recorded");
await page.getByRole("button", { name: /Record weight A quick check-in/ }).click();
await page.getByRole("spinbutton", { name: "Weight", exact: true }).fill("65.2");
await page.getByRole("button", { name: "Save measurement" }).click();
await expect(
page.getByRole("button", { name: /Record weight Recorded · 65.2 kg/ }),
).toBeVisible();
await homePage.completeTask("Review the documents").click();
await expect(homePage.restoreTask("Review the documents")).toHaveAttribute(
"aria-pressed",
"true",
);
await expect(homePage.completedTask("Review the documents")).toBeVisible();
await homePage.conversation("Haruka Aoki").click();
await messagesPage.reviewReply.click();
await messagesPage.trySending.click();
await workspacePage.navigateTo("Home");
await expect(homePage.conversation("Haruka Aoki")).toContainText("Reply recorded");
await homePage.recordMeasurement("65.2");
await expect(homePage.recordedWeight("65.2")).toBeVisible();
expect(await titles.allTextContents()).toEqual(before);
expect((await a11y().analyze()).violations).toEqual([]);
await page.screenshot({ path: testInfo.outputPath("completion-in-place.png") });
await page.getByRole("button", { name: "Restore Review the documents", exact: true }).click();
await expect(
page.getByRole("button", { name: "Complete Review the documents", exact: true }),
).toHaveAttribute("aria-pressed", "false");
await homePage.restoreTask("Review the documents").click();
await expect(homePage.completeTask("Review the documents")).toHaveAttribute(
"aria-pressed",
"false",
);
});
+42 -51
View File
@@ -1,75 +1,66 @@
import { expect, test } from "../fixtures";
test("selects articles and keeps the reader focused on their content", async ({ page }) => {
await page.goto("/inbox");
await page.waitForLoadState("networkidle");
const rows = page.locator("[data-testid='inbox-article-row']");
await expect(rows).toHaveCount(9);
test("selects articles and keeps the reader focused on their content", async ({ readerPage }) => {
await readerPage.goTo();
await expect(readerPage.rows).toHaveCount(9);
const articleTitle = "Context for an agent can be brief";
await rows.filter({ hasText: articleTitle }).click();
const reader = page.getByRole("article", { name: articleTitle, exact: true });
await readerPage.selectArticle(articleTitle);
const reader = readerPage.article(articleTitle);
await expect(reader).toBeVisible();
await expect(reader.getByRole("heading", { name: articleTitle, exact: true })).toBeVisible();
await expect(readerPage.articleHeading(articleTitle)).toBeVisible();
await expect(
reader.getByText("Start with a one-sentence goal, then add the constraints that must hold.", {
exact: false,
}),
).toBeVisible();
await expect(page.getByText("FOR YOU", { exact: true })).toHaveCount(0);
await expect(page.getByText("Mock", { exact: true })).toHaveCount(0);
await expect(page.getByText("Why this article", { exact: true })).toHaveCount(0);
await expect(page.getByRole("button", { name: /For you|Read later|Unread only/ })).toHaveCount(0);
const back = page.getByRole("button", { name: "Back to articles" });
if (await back.isVisible()) await back.click();
await rows.filter({ hasText: "Three columns create a rhythm for reading" }).click();
await expect(
page.getByRole("article", {
name: "Three columns create a rhythm for reading",
exact: true,
}),
readerPage.articleText(
articleTitle,
"Start with a one-sentence goal, then add the constraints that must hold.",
),
).toBeVisible();
await expect(readerPage.forYouLabel).toHaveCount(0);
await expect(readerPage.mockLabel).toHaveCount(0);
await expect(readerPage.articleExplanation).toHaveCount(0);
await expect(readerPage.legacyFilters).toHaveCount(0);
await readerPage.backToArticles();
await readerPage.selectArticle("Three columns create a rhythm for reading");
await expect(readerPage.article("Three columns create a rhythm for reading")).toBeVisible();
});
test("recovers from empty search and shares navigation with Research", async ({ page, a11y }) => {
await page.goto("/inbox");
await page.waitForLoadState("networkidle");
const rows = page.locator("[data-testid='inbox-article-row']");
await page.getByRole("textbox", { name: "Search articles" }).fill("nonexistent-query-xyz");
await expect(rows).toHaveCount(0);
await expect(page.getByText("No articles found", { exact: true })).toBeVisible();
await page.getByRole("button", { name: "Clear search" }).click();
await expect(rows).toHaveCount(9);
test("recovers from empty search and shares navigation with Research", async ({
page,
readerPage,
workspacePage,
a11y,
}) => {
await readerPage.goTo();
await readerPage.search("nonexistent-query-xyz");
await expect(readerPage.rows).toHaveCount(0);
await expect(readerPage.emptyState).toBeVisible();
await readerPage.clearSearch();
await expect(readerPage.rows).toHaveCount(9);
expect((await a11y().analyze()).violations).toEqual([]);
await expect(page.getByRole("button", { name: "Edit interests", exact: true })).toHaveCount(0);
await page.getByRole("link", { name: "Research", exact: true }).click();
await expect(readerPage.editInterestsButton).toHaveCount(0);
await workspacePage.navigateTo("Research");
await expect(page).toHaveURL(/\/deck$/);
await page.getByRole("link", { name: "Reader", exact: true }).click();
await workspacePage.navigateTo("Reader");
await expect(page).toHaveURL(/\/inbox$/);
await expect(rows).toHaveCount(9);
await expect(readerPage.rows).toHaveCount(9);
});
test("keeps article navigation readable without horizontal overflow", async ({
page,
readerPage,
a11y,
}, testInfo) => {
await page.goto("/inbox");
await page.waitForLoadState("networkidle");
await readerPage.goTo();
for (const width of [320, 375, 414, 768]) {
await page.setViewportSize({ width, height: 850 });
await expect(page.getByRole("textbox", { name: "Search articles" })).toBeVisible();
await expect(readerPage.searchInput).toBeVisible();
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBe(width);
await page.locator("[data-testid='inbox-article-row']").first().click();
const reader = page.getByRole("article", {
name: "Thinking in the browser: a new workflow with AI agents",
exact: true,
});
await expect(reader).toBeVisible();
await readerPage.openFirstArticle();
await expect(
readerPage.article("Thinking in the browser: a new workflow with AI agents"),
).toBeVisible();
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBe(width);
await page.screenshot({
path: testInfo.outputPath(`inbox-reader-${width}.png`),
});
await page.screenshot({ path: testInfo.outputPath(`inbox-reader-${width}.png`) });
if (width === 320) expect((await a11y().analyze()).violations).toEqual([]);
const back = page.getByRole("button", { name: "Back to articles" });
if (await back.isVisible()) await back.click();
await readerPage.backToArticles();
}
});
+4 -3
View File
@@ -4,6 +4,7 @@ test("HTTP MCP persists decks visible in Research without browser credentials",
playwright,
baseURL,
page,
deckPage,
}) => {
const client = await playwright.request.newContext({
baseURL,
@@ -59,8 +60,8 @@ test("HTTP MCP persists decks visible in Research without browser credentials",
});
expect(fetched.ok).toBe(true);
expect(fetched.posts.length).toBeGreaterThan(0);
await page.goto(`/deck?deck=${created.deck.id}`);
await expect(page.getByRole("heading", { level: 1 })).toHaveText("HTTP MCP research");
await deckPage.goTo(`?deck=${created.deck.id}`);
await expect(deckPage.heading).toHaveText("HTTP MCP research");
const updated = await tool("replace_deck", {
deckId: created.deck.id,
expectedRevision: 1,
@@ -72,7 +73,7 @@ test("HTTP MCP persists decks visible in Research without browser credentials",
await tool("delete_deck", { deckId: created.deck.id, expectedRevision: 1 }),
).toMatchObject({ ok: false, error: { code: "conflict" } });
await page.reload();
await expect(page.getByRole("heading", { level: 1 })).toHaveText("Updated via HTTP");
await expect(deckPage.heading).toHaveText("Updated via HTTP");
expect(
await tool("delete_deck", { deckId: created.deck.id, expectedRevision: 2 }),
).toMatchObject({ ok: true });
+96 -110
View File
@@ -1,10 +1,11 @@
import type { DeckColumn } from "../../src/features/decks/model";
import type { ResearchRun } from "../../src/features/research/model";
import { expect, test } from "../fixtures";
import { closeMobileSidebar, showDeckManagement, showResearch } from "../workspace";
import { ResearchPage } from "../models/ResearchPage";
import { WorkspacePage } from "../models/WorkspacePage";
test("pushes the current research snapshot on each SSE connection", async ({ page }) => {
await page.goto("/deck");
test("pushes the current research snapshot on each SSE connection", async ({ page, deckPage }) => {
await deckPage.goTo();
const snapshots = await page.evaluate(async () => {
const connect = () =>
new Promise<unknown>((resolve, reject) => {
@@ -34,18 +35,18 @@ test("pushes the current research snapshot on each SSE connection", async ({ pag
test("restores the conversation and growing temporary deck from reconnect snapshots", async ({
page,
deckPage,
researchPage,
workspacePage,
}) => {
await page.goto("/deck");
await showDeckManagement(page);
await page.getByRole("button", { name: "Add column", exact: true }).click();
const accounts = page.getByRole("combobox", {
name: "Connection profile",
exact: true,
});
await deckPage.goTo();
await workspacePage.showDeckManagement();
await deckPage.addColumnButton.click();
const accounts = deckPage.editorProfile;
await accounts.selectOption({ label: "e2e" });
const connectionId = await accounts.inputValue();
await page.keyboard.press("Escape");
await closeMobileSidebar(page);
await workspacePage.closeMobileSidebar();
const run: ResearchRun = {
id: "36ad8cc7-318c-46ae-94fa-28d30336f027",
threadId: "conversation-test",
@@ -90,17 +91,14 @@ test("restores the conversation and growing temporary deck from reconnect snapsh
body: `data: ${JSON.stringify({ configured: true, run })}\n\n`,
}),
);
await page.goto(`/deck?run=${run.id}`);
await showResearch(page);
const conversation = page.getByRole("log", { name: "Research conversation" });
await deckPage.goTo(`?run=${run.id}`);
await workspacePage.showResearch();
const conversation = researchPage.conversation;
await expect(conversation).toContainText("既存の観点を使って調査して");
await expect(conversation).toContainText("list_decks: 完了");
await expect(conversation).toContainText("既存の検索条件を再利用しています。");
await expect(conversation.locator('[data-slot="bubble-content"]')).not.toHaveCSS(
"background-color",
"rgba(0, 0, 0, 0)",
);
await expect(page.locator("[data-testid='deck-column'] h2")).toHaveText(["最初の観点"]);
await expect(researchPage.bubbles).not.toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
await expect(deckPage.columnTitles).toHaveText(["最初の観点"]);
run.deckVersion = 2;
run.status = "complete";
run.messages.push({
@@ -121,52 +119,47 @@ test("restores the conversation and growing temporary deck from reconnect snapsh
},
});
await page.reload();
await showResearch(page);
await workspacePage.showResearch();
await expect(conversation).toContainText("既存の観点を使って調査して");
await expect(conversation).toContainText("比較する観点を追加しました。");
await expect(page.locator("[data-testid='deck-column'] h2")).toHaveText([
"最初の観点",
"比較の観点",
]);
await closeMobileSidebar(page);
await expect(page.getByRole("button", { name: "Save as deck", exact: true })).toBeEnabled();
await showResearch(page);
const accountPicker = page.getByRole("button", {
name: "Connected accounts · 1",
});
await expect(deckPage.columnTitles).toHaveText(["最初の観点", "比較の観点"]);
await workspacePage.closeMobileSidebar();
await expect(deckPage.saveAsDeck).toBeEnabled();
await workspacePage.showResearch();
const accountPicker = researchPage.accountPicker(1);
await accountPicker.click();
await expect(accountPicker).toHaveAttribute("aria-expanded", "true");
await expect(page.getByRole("checkbox", { name: "e2e · Twitter" })).toBeChecked();
await expect(researchPage.account("e2e · Twitter")).toBeChecked();
await accountPicker.click();
await expect(accountPicker).toHaveAttribute("aria-expanded", "false");
await page.getByLabel("Message", { exact: true }).fill("この観点を詳しく");
await expect(page.getByRole("button", { name: "Send", exact: true })).toBeEnabled();
await page.getByRole("button", { name: "Close sidebar", exact: true }).click();
await researchPage.composer.fill("この観点を詳しく");
await expect(researchPage.send).toBeEnabled();
await workspacePage.closeSidebar.click();
run.deckVersion = 3;
if (run.deck) run.deck.title = "閉じている間に更新された調査";
await expect(page.getByRole("heading", { level: 1 })).toHaveText("閉じている間に更新された調査", {
await expect(deckPage.heading).toHaveText("閉じている間に更新された調査", {
timeout: 15000,
});
await showResearch(page);
await expect(page.getByLabel("Message", { exact: true })).toHaveValue("この観点を詳しく");
await workspacePage.showResearch();
await expect(researchPage.composer).toHaveValue("この観点を詳しく");
await page.setViewportSize({ width: 600, height: 900 });
await expect(page.locator('[data-sidebar="sidebar"][data-mobile="true"]')).toHaveCount(1);
await showResearch(page);
await expect(page.getByLabel("Message", { exact: true })).toHaveValue("この観点を詳しく");
await page.getByRole("button", { name: "Connected accounts · 1" }).click();
await page.getByRole("checkbox", { name: "e2e · Twitter" }).uncheck();
await expect(workspacePage.mobileSidebar).toHaveCount(1);
await workspacePage.showResearch();
await expect(researchPage.composer).toHaveValue("この観点を詳しく");
await researchPage.accountPicker(1).click();
await researchPage.account("e2e · Twitter").uncheck();
await page.setViewportSize({ width: 1100, height: 900 });
await expect(page.locator('[data-sidebar="sidebar"][data-mobile="true"]')).toHaveCount(0);
await showResearch(page);
await expect(page.getByLabel("Message", { exact: true })).toHaveValue("この観点を詳しく");
await page.getByRole("button", { name: "Connected accounts · 0" }).click();
await expect(page.getByRole("checkbox", { name: "e2e · Twitter" })).not.toBeChecked();
await expect(workspacePage.mobileSidebar).toHaveCount(0);
await workspacePage.showResearch();
await expect(researchPage.composer).toHaveValue("この観点を詳しく");
await researchPage.accountPicker(0).click();
await expect(researchPage.account("e2e · Twitter")).not.toBeChecked();
await page.setViewportSize({ width: 600, height: 900 });
await expect(page.locator('[data-sidebar="sidebar"][data-mobile="true"]')).toHaveCount(1);
await showResearch(page);
await expect(page.getByLabel("Message", { exact: true })).toHaveValue("この観点を詳しく");
await page.getByRole("button", { name: "Connected accounts · 0" }).click();
await expect(page.getByRole("checkbox", { name: "e2e · Twitter" })).not.toBeChecked();
await expect(workspacePage.mobileSidebar).toHaveCount(1);
await workspacePage.showResearch();
await expect(researchPage.composer).toHaveValue("この観点を詳しく");
await researchPage.accountPicker(0).click();
await expect(researchPage.account("e2e · Twitter")).not.toBeChecked();
});
test("rejects a different tailnet owner at the SSE endpoint", async ({ request }) => {
@@ -178,20 +171,20 @@ test("rejects a different tailnet owner at the SSE endpoint", async ({ request }
test("renders Markdown and reveals an unloaded citation in an offscreen deck column", async ({
page,
deckPage,
researchPage,
workspacePage,
a11y,
}) => {
await page.emulateMedia({ reducedMotion: "reduce" });
await page.goto("/deck");
await showDeckManagement(page);
await page.getByRole("button", { name: "Add column", exact: true }).click();
const accounts = page.getByRole("combobox", {
name: "Connection profile",
exact: true,
});
await deckPage.goTo();
await workspacePage.showDeckManagement();
await deckPage.addColumnButton.click();
const accounts = deckPage.editorProfile;
await accounts.selectOption({ label: "e2e" });
const connectionId = await accounts.inputValue();
await page.keyboard.press("Escape");
await closeMobileSidebar(page);
await workspacePage.closeMobileSidebar();
const source: DeckColumn = {
id: "citation-column-2",
title: "引用の観点",
@@ -252,27 +245,22 @@ test("renders Markdown and reveals an unloaded citation in an offscreen deck col
body: `data: ${JSON.stringify({ configured: true, run })}\n\n`,
}),
);
await page.goto(`/deck?run=${run.id}`);
await showResearch(page);
const chat = page.getByRole("log", { name: "Research conversation" });
await expect(chat.getByRole("heading", { name: "調査結果" })).toBeVisible();
await expect(chat.getByRole("heading", { name: "調査結果" })).toHaveCSS("font-weight", "600");
await expect(chat.locator("ul")).toHaveCSS("list-style-type", "disc");
await expect(chat.locator("strong").filter({ hasText: "重要な発見" })).toBeVisible();
await expect(chat.getByRole("table")).toBeVisible();
await expect(page.getByText("AIが調査時に取得した引用本文", { exact: true })).toHaveCount(0);
await chat.getByRole("link", { name: "根拠の投稿" }).click();
const card = page.locator(
'[data-testid="research-post"][data-post-url="https://twitter.com/author/status/123456789"]',
);
await deckPage.goTo(`?run=${run.id}`);
await workspacePage.showResearch();
await expect(researchPage.messageHeading("調査結果")).toBeVisible();
await expect(researchPage.messageHeading("調査結果")).toHaveCSS("font-weight", "600");
await expect(researchPage.messageLists).toHaveCSS("list-style-type", "disc");
await expect(researchPage.emphasis("重要な発見")).toBeVisible();
await expect(researchPage.messageTables).toBeVisible();
await expect(deckPage.postText("AIが調査時に取得した引用本文")).toHaveCount(0);
await researchPage.citation("根拠の投稿").click();
const card = deckPage.post("https://twitter.com/author/status/123456789");
await expect(card).toBeVisible();
await expect(card).toHaveAttribute("data-citation-highlight", "true");
await expect(card).toBeFocused();
await expect(card).toContainText("Post saved during research");
await expect(card).toContainText("AIが調査時に取得した引用本文");
expect(
await page.locator("[data-testid='deck-board']").evaluate((element) => element.scrollLeft),
).toBeGreaterThan(0);
expect(await deckPage.board.evaluate((element) => element.scrollLeft)).toBeGreaterThan(0);
expect(page.context().pages()).toHaveLength(1);
await expect(page).toHaveURL(/citation=twitter%3A123456789/);
const bookmarked = new URL(page.url());
@@ -284,45 +272,47 @@ test("renders Markdown and reveals an unloaded citation in an offscreen deck col
});
await page.goto(bookmarked.toString());
await expect(card).toBeVisible();
await expect(card).toHaveClass(/citation-highlight/);
await expect(card).toHaveAttribute("data-citation-highlight", "true");
await expect(page).toHaveURL(/citation=twitter%3A123456789/);
await expect(page.locator("[data-testid='deck-column']")).toHaveCount(3);
await expect(deckPage.columns).toHaveCount(3);
expect(submitted).toEqual([]);
expect((await a11y().analyze()).violations).toEqual([]);
});
test("explains unconfigured Codex and prevents starting research without affecting the deck", async ({
page,
deckPage,
researchPage,
workspacePage,
a11y,
}) => {
await page.goto("/deck");
const heading = page.getByRole("heading", { level: 1 });
await deckPage.goTo();
const heading = deckPage.heading;
await expect(heading).toBeVisible();
const originalTitle = await heading.textContent();
await showResearch(page);
const panel = page.getByRole("complementary", { name: "Research chat" });
await workspacePage.showResearch();
const panel = researchPage.panel;
await expect(panel).toBeVisible();
await expect(
panel.getByText("The connection to Codex on your home server is not configured."),
).toBeVisible();
await expect(panel.getByRole("button", { name: "Send", exact: true })).toBeDisabled();
await expect(panel.getByLabel("Message", { exact: true })).toBeDisabled();
await expect(researchPage.unconfiguredNotice).toBeVisible();
await expect(researchPage.send).toBeDisabled();
await expect(researchPage.composer).toBeDisabled();
const accessibility = await a11y().analyze();
expect(accessibility.violations).toEqual([]);
await closeMobileSidebar(page);
await workspacePage.closeMobileSidebar();
await expect(heading).toHaveText(originalTitle ?? "");
await showDeckManagement(page);
await page.getByRole("button", { name: "Create deck", exact: true }).click();
await page.getByLabel("New deck name").fill("手動の調査");
await page.getByRole("button", { name: "Create", exact: true }).click();
await closeMobileSidebar(page);
await workspacePage.showDeckManagement();
await deckPage.createDeck.click();
await deckPage.newDeckName.fill("手動の調査");
await deckPage.confirmCreate.click();
await workspacePage.closeMobileSidebar();
await expect(heading).toHaveText("手動の調査");
await showResearch(page);
await workspacePage.showResearch();
await expect(panel).toBeVisible();
});
test("keeps browser conversation selection independent and restores it on reload", async ({
page,
researchPage,
workspacePage,
browser,
context,
}) => {
@@ -353,26 +343,22 @@ test("keeps browser conversation selection independent and restores it on reload
});
try {
const secondPage = await other.newPage();
await page.goto(`/deck?run=${first}`, { waitUntil: "domcontentloaded" });
await showResearch(page);
await secondPage.goto(`/deck?run=${second}`, {
const secondResearch = new ResearchPage(secondPage);
const secondWorkspace = new WorkspacePage(secondPage);
await researchPage.goTo(`?run=${first}`, { waitUntil: "domcontentloaded" });
await workspacePage.showResearch();
await secondResearch.goTo(`?run=${second}`, {
waitUntil: "domcontentloaded",
});
await showResearch(secondPage);
await expect(page.getByRole("log", { name: "Research conversation" })).toContainText(
"Browser one conversation",
);
await expect(secondPage.getByRole("log", { name: "Research conversation" })).toContainText(
"Browser two conversation",
);
await secondPage.getByRole("button", { name: "New chat", exact: true }).click();
await secondWorkspace.showResearch();
await expect(researchPage.conversation).toContainText("Browser one conversation");
await expect(secondResearch.conversation).toContainText("Browser two conversation");
await secondResearch.newChat.click();
await expect(secondPage).toHaveURL(/run=new/);
await page.reload({ waitUntil: "domcontentloaded" });
await showResearch(page);
await workspacePage.showResearch();
await expect(page).toHaveURL(new RegExp(`run=${first}`));
await expect(page.getByRole("log", { name: "Research conversation" })).toContainText(
"Browser one conversation",
);
await expect(researchPage.conversation).toContainText("Browser one conversation");
} finally {
await other.close();
db.$client.prepare("DELETE FROM research_sessions WHERE id IN (?, ?)").run(first, second);
+59 -73
View File
@@ -6,102 +6,86 @@ const coffeeSubject = "Catch up over coffee";
test("reviews an edited reply and keeps the simulated result in history", async ({
page,
a11y,
messagesPage,
}) => {
await page.goto("/support");
await messagesPage.goTo();
await page.waitForLoadState("networkidle");
const rows = page.locator("[data-testid='support-row']");
const rows = messagesPage.rows;
await expect(rows).toHaveCount(3);
await rows.filter({ hasText: reviewSubject }).click();
await page.locator("[data-testid='conversation-suggestions'] summary:visible").click();
await page.getByRole("button", { name: /^Polite/ }).click();
const reply = page.getByRole("textbox", { name: "Reply", exact: true });
await messagesPage.conversation(reviewSubject).click();
await messagesPage.chooseReplySuggestion(/^Polite/);
const reply = messagesPage.reply;
await expect(reply).toHaveValue(/Thank you for sharing/);
const edited = "I reviewed the materials. Let’s discuss the first-time experience.";
await reply.fill(edited);
await page.locator("[data-testid='conversation-scheduling'] summary:visible").click();
const slot = "Mon, Oct 5, 14:00–14:30";
await page.getByRole("combobox", { name: "Suggested times" }).selectOption(slot);
await page.getByRole("button", { name: "Add to reply" }).click();
await messagesPage.addSuggestedTime(slot);
await expect(reply).toHaveValue(`${edited}\n\nWould ${slot} work for you?`);
await page.getByRole("button", { name: "Review reply" }).click();
const dialog = page.getByRole("dialog", { name: "Review reply" });
await messagesPage.reviewReply.click();
const dialog = messagesPage.reviewDialog;
await expect(dialog).toContainText(edited);
await expect(dialog).toContainText(slot);
await expect(dialog).toContainText("No message will be sent");
expect((await a11y().analyze()).violations).toEqual([]);
await dialog.getByRole("button", { name: "Back", exact: true }).click();
await messagesPage.editReply.click();
await expect(reply).toHaveValue(new RegExp(edited));
await page.getByRole("button", { name: "Review reply" }).click();
await dialog.getByRole("button", { name: "Try sending" }).click();
await messagesPage.reviewReply.click();
await messagesPage.trySending.click();
await expect(dialog).toHaveCount(0);
await expect(rows.filter({ hasText: reviewSubject })).toHaveCount(0);
await page.getByRole("button", { name: "Archive", exact: true }).click();
await rows.filter({ hasText: reviewSubject }).click();
await expect(page.locator("[data-testid='support-outcome']")).toContainText(edited);
await expect(page.locator("[data-testid='support-outcome']")).toContainText(slot);
await expect(messagesPage.conversation(reviewSubject)).toHaveCount(0);
await messagesPage.selectView("Archive");
await messagesPage.conversation(reviewSubject).click();
await expect(messagesPage.outcome).toContainText(edited);
await expect(messagesPage.outcome).toContainText(slot);
});
test("defers a conversation and restores it to the current queue", async ({ page }) => {
await page.goto("/support");
test("defers a conversation and restores it to the current queue", async ({
page,
messagesPage,
}) => {
await messagesPage.goTo();
await page.waitForLoadState("networkidle");
const rows = page.locator("[data-testid='support-row']");
await rows.filter({ hasText: coffeeSubject }).click();
await expect(rows.filter({ hasText: coffeeSubject })).toHaveAttribute("aria-current", "true");
await page
.getByRole("textbox", { name: "Reply", exact: true })
.fill("土曜の予定を確認してから返事するね。");
await page.getByRole("button", { name: "Leave for later" }).click();
await expect(rows.filter({ hasText: coffeeSubject })).toHaveCount(0);
await page.getByRole("button", { name: "Set aside", exact: true }).click();
await rows.filter({ hasText: coffeeSubject }).click();
await expect(page.getByRole("textbox", { name: "Reply", exact: true })).toHaveValue(
"土曜の予定を確認してから返事するね。",
);
await page.getByRole("button", { name: "Move to Inbox" }).click();
const back = page.getByRole("button", { name: "Back to messages" });
if (await back.isVisible()) await back.click();
await expect(rows.filter({ hasText: coffeeSubject })).toBeVisible();
await expect(page.getByRole("button", { name: "Inbox", exact: true })).toHaveAttribute(
"aria-pressed",
"true",
);
await messagesPage.conversation(coffeeSubject).click();
await expect(messagesPage.conversation(coffeeSubject)).toHaveAttribute("aria-current", "true");
await messagesPage.reply.fill("土曜の予定を確認してから返事するね。");
await messagesPage.leaveForLater.click();
await expect(messagesPage.conversation(coffeeSubject)).toHaveCount(0);
await messagesPage.selectView("Set aside");
await messagesPage.conversation(coffeeSubject).click();
await expect(messagesPage.reply).toHaveValue("土曜の予定を確認してから返事するね。");
await messagesPage.moveToInbox.click();
await messagesPage.showConversationList();
await expect(messagesPage.conversation(coffeeSubject)).toBeVisible();
await expect(messagesPage.inbox).toHaveAttribute("aria-pressed", "true");
});
test("keeps corrections as new records and summarizes only selected current facts", async ({
page,
a11y,
journalPage,
}) => {
await page.goto("/journal");
await journalPage.goTo();
await page.waitForLoadState("networkidle");
await expect(page.getByRole("button", { name: "Save entry", exact: true })).toBeDisabled();
await page.getByRole("textbox", { name: "Entry text" }).fill("次はレビュー資料の導線を確認する");
await page.getByRole("button", { name: "Save entry", exact: true }).click();
await expect(page.getByRole("article", { name: "Note entry" })).toContainText(
"Occurred: Unknown",
);
await page.getByRole("button", { name: "Weight", exact: true }).click();
await page.getByRole("spinbutton", { name: "Weight", exact: true }).fill("65.2");
await page.getByRole("textbox", { name: "Source" }).fill("自宅の体重計");
await page.getByRole("button", { name: "Save entry", exact: true }).click();
await page
.getByRole("article", { name: "Weight entry" })
.getByRole("button", { name: "Correct" })
.click();
const correction = page.getByRole("dialog", { name: "Correct entry" });
await correction.getByRole("spinbutton", { name: "Weight", exact: true }).fill("65.8");
await correction.getByRole("button", { name: "Add correction" }).click();
const weights = page.getByRole("article", { name: "Weight entry" });
await expect(journalPage.saveEntry).toBeDisabled();
await journalPage.entryText.fill("次はレビュー資料の導線を確認する");
await journalPage.saveEntry.click();
await expect(journalPage.noteEntries).toContainText("Occurred: Unknown");
await journalPage.weightTab.click();
await journalPage.weight.fill("65.2");
await journalPage.source.fill("自宅の体重計");
await journalPage.saveEntry.click();
await journalPage.correctWeight("65.8");
const weights = journalPage.weightEntries;
await expect(weights).toHaveCount(2);
await expect(weights.filter({ hasText: "65.2 kg" })).toContainText("Original");
await expect(weights.filter({ hasText: "65.8 kg" })).toContainText("Correction");
await page.getByRole("button", { name: "Summarize entries", exact: true }).click();
const review = page.getByRole("dialog", { name: "Summarize entries" });
const summary = review.getByRole("textbox", { name: "Entry summary" });
await journalPage.summarizeEntries.click();
const summary = journalPage.summary;
await expect(summary).toHaveValue(/65.8 kg/);
await expect(summary).not.toHaveValue(/65.2 kg/);
await expect(summary).toHaveValue(/Source: 自宅の体重計/);
await expect(summary).toHaveValue(/Entries with an unknown event time: 2 entries/);
await review.getByRole("checkbox", { name: /次はレビュー資料/ }).uncheck();
await journalPage.excludeFromSummary(/次はレビュー資料/);
await expect(summary).not.toHaveValue(/次はレビュー資料/);
await expect(summary).toHaveValue(
/Medication and weight are unknown for periods without entries/,
@@ -112,22 +96,24 @@ test("keeps corrections as new records and summarizes only selected current fact
test("fits communication and journal interactions in narrow screens", async ({
page,
a11y,
messagesPage,
journalPage,
workspacePage,
}, testInfo) => {
for (const width of [320, 375, 414, 768]) {
await page.setViewportSize({ width, height: 850 });
await page.goto("/support");
await messagesPage.goTo();
await page.waitForLoadState("networkidle");
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBe(width);
await page.locator("[data-testid='support-row']").first().click();
await expect(page.getByRole("textbox", { name: "Reply", exact: true })).toBeVisible();
await messagesPage.rows.first().click();
await expect(messagesPage.reply).toBeVisible();
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBe(width);
if (width === 320) expect((await a11y().analyze()).violations).toEqual([]);
await page.screenshot({ path: testInfo.outputPath(`messages-${width}.png`) });
const back = page.getByRole("button", { name: "Back to messages" });
if (await back.isVisible()) await back.click();
await page.getByRole("link", { name: "Journal", exact: true }).click();
await page.getByRole("button", { name: "Weight", exact: true }).click();
await expect(page.getByRole("spinbutton", { name: "Weight", exact: true })).toBeVisible();
await messagesPage.showConversationList();
await workspacePage.navigateTo("Journal");
await journalPage.weightTab.click();
await expect(journalPage.weight).toBeVisible();
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBe(width);
if (width === 320) expect((await a11y().analyze()).violations).toEqual([]);
}
+56 -51
View File
@@ -1,96 +1,101 @@
import { expect, test } from "../fixtures";
test("restores article selection with browser history and reload", async ({ page }) => {
await page.goto("/inbox", { waitUntil: "networkidle" });
const rows = page.locator("[data-testid='inbox-article-row']");
const heading = await rows.nth(1).locator("[data-testid='inbox-row-title']").innerText();
test("restores article selection with browser history and reload", async ({ page, readerPage }) => {
await readerPage.goTo();
const rows = readerPage.rows;
const heading = await readerPage.rowTitle(1).innerText();
await rows.nth(1).click();
await expect(page.locator("[data-testid='inbox-article-title']")).toHaveText(heading);
await expect(readerPage.articleTitle).toHaveText(heading);
const selectedUrl = page.url();
expect(new URL(selectedUrl).searchParams.get("article")).toBeTruthy();
await page.reload({ waitUntil: "networkidle" });
await expect(page.locator("[data-testid='inbox-article-title']")).toHaveText(heading ?? "");
await expect(readerPage.articleTitle).toHaveText(heading ?? "");
await page.goBack();
await expect(page.locator("[data-testid='inbox-workspace']")).toHaveAttribute(
"data-reading",
"false",
);
await expect(readerPage.workspace).toHaveAttribute("data-reading", "false");
await page.goForward();
await expect(page).toHaveURL(selectedUrl);
await expect(page.locator("[data-testid='inbox-article-title']")).toHaveText(heading ?? "");
await expect(readerPage.articleTitle).toHaveText(heading ?? "");
});
test("restores search and replaces typing history", async ({ page }) => {
await page.goto("/", { waitUntil: "networkidle" });
await page.getByRole("link", { name: "Reader", exact: true }).click();
const search = page.getByRole("textbox", { name: "Search articles" });
test("restores search and replaces typing history", async ({
page,
readerPage,
homePage,
workspacePage,
}) => {
await homePage.goTo();
await page.waitForLoadState("networkidle");
await workspacePage.navigateTo("Reader");
const search = readerPage.searchInput;
await search.pressSequentially("browser");
await expect(search).toHaveValue("browser");
await expect.poll(() => new URL(page.url()).searchParams.get("q")).toBe("browser");
const count = await page.locator("[data-testid='inbox-article-row']").count();
const count = await readerPage.rows.count();
await page.reload({ waitUntil: "networkidle" });
await expect(search).toHaveValue("browser");
await expect(page.locator("[data-testid='inbox-article-row']")).toHaveCount(count);
await expect(readerPage.rows).toHaveCount(count);
await page.goBack();
await expect(page).toHaveURL("http://127.0.0.1:4173/");
await expect(page).toHaveURL("/");
});
test("restores account dialog without losing underlying article", async ({ page }) => {
await page.goto("/inbox", { waitUntil: "networkidle" });
await page.getByRole("button", { name: "Manage connected accounts" }).click();
test("restores account dialog without losing underlying article", async ({
page,
readerPage,
connectedAccounts,
}) => {
await readerPage.goTo();
await connectedAccounts.open();
await expect.poll(() => new URL(page.url()).searchParams.get("accounts")).toBe("true");
await page.reload({ waitUntil: "networkidle" });
await expect(page.getByRole("dialog")).toBeVisible();
await expect(connectedAccounts.dialog).toBeVisible();
await page.goBack();
await expect(page.getByRole("dialog")).toHaveCount(0);
await expect(connectedAccounts.dialog).toHaveCount(0);
await page.goForward();
await expect(page.getByRole("dialog")).toBeVisible();
await expect(connectedAccounts.dialog).toBeVisible();
});
test("restores a message view and conversation from its URL", async ({ page }) => {
await page.goto("/support", { waitUntil: "networkidle" });
await page.getByRole("button", { name: "All", exact: true }).click();
const row = page.locator("[data-testid='support-row']").nth(1);
const heading = await row.locator("span").first().innerText();
test("restores a message view and conversation from its URL", async ({ page, messagesPage }) => {
await messagesPage.goTo();
await page.waitForLoadState("networkidle");
await messagesPage.selectView("All");
const row = messagesPage.rows.nth(1);
const heading = await messagesPage.rowName(1).innerText();
await row.click();
await expect(page.locator("[data-testid='support-toolbar'] > span").first()).toHaveText(heading);
await expect(messagesPage.toolbarName).toHaveText(heading);
const url = page.url();
expect(new URL(url).searchParams.get("view")).toBe("all");
expect(new URL(url).searchParams.get("conversation")).toBeTruthy();
await page.reload({ waitUntil: "networkidle" });
await expect(page.locator("[data-testid='support-toolbar'] > span").first()).toHaveText(
heading ?? "",
);
await expect(page.locator("[data-testid='support-workspace']")).toHaveAttribute(
"data-reading",
"true",
);
await expect(messagesPage.toolbarName).toHaveText(heading ?? "");
await expect(messagesPage.workspace).toHaveAttribute("data-reading", "true");
});
test("keeps a deep link through login", async ({ page, context }) => {
test("keeps a deep link through login", async ({ page, context, readerPage, authPage }) => {
await context.clearCookies();
await page.goto("/inbox?q=browser", { waitUntil: "networkidle" });
await readerPage.goTo("?q=browser");
await expect(page).toHaveURL(/\/login\?returnTo=/);
await page.getByLabel("Email address").fill("[email protected]");
await page.getByLabel("Password", { exact: true }).fill("E2E-only-passphrase-2026");
await page.getByRole("button", { name: "Sign in", exact: true }).click();
await authPage.signIn("[email protected]", "E2E-only-passphrase-2026");
await expect(page).toHaveURL(/\/inbox\?q=browser$/);
await expect(page.getByRole("textbox", { name: "Search articles" })).toHaveValue("browser");
await expect(readerPage.searchInput).toHaveValue("browser");
});
test("debounces search and cancels pending input on history navigation", async ({ page }) => {
await page.goto("/support", { waitUntil: "networkidle" });
await page.getByRole("button", { name: "All", exact: true }).click();
test("debounces search and cancels pending input on history navigation", async ({
page,
messagesPage,
}) => {
await messagesPage.goTo();
await page.waitForLoadState("networkidle");
await messagesPage.selectView("All");
await expect(page).toHaveURL(/view=all/);
await page.clock.install();
const input = page.getByRole("textbox", { name: "Search messages" });
const input = messagesPage.searchInput;
await input.fill("not-committed");
await expect(input).toHaveValue("not-committed");
expect(new URL(page.url()).searchParams.has("q")).toBe(false);
await page.clock.runFor(200);
expect(new URL(page.url()).searchParams.has("q")).toBe(false);
await page.goBack();
await expect(page).toHaveURL("http://127.0.0.1:4173/support");
await expect(page).toHaveURL("/support");
await page.clock.runFor(400);
await expect(input).toHaveValue("");
expect(new URL(page.url()).searchParams.has("q")).toBe(false);
@@ -99,10 +104,10 @@ test("debounces search and cancels pending input on history navigation", async (
await expect.poll(() => new URL(page.url()).searchParams.get("q")).toBe("final search");
});
test("waits for IME composition to finish before searching", async ({ page }) => {
await page.goto("/inbox", { waitUntil: "networkidle" });
test("waits for IME composition to finish before searching", async ({ page, readerPage }) => {
await readerPage.goTo();
await page.clock.install();
const input = page.getByRole("textbox", { name: "Search articles" });
const input = readerPage.searchInput;
await input.dispatchEvent("compositionstart");
await input.fill("browser");
await page.clock.runFor(1000);
+47 -90
View File
@@ -1,127 +1,84 @@
import { expect, test } from "../fixtures";
test("filters body systems and opens lab history", async ({ page, a11y }) => {
await page.goto("/vitals");
await page.waitForLoadState("networkidle");
await expect(page.getByRole("heading", { name: "Body", exact: true })).toBeVisible();
const systems = page.locator("[data-testid='vital-system-card']");
await expect(systems).toHaveCount(8);
await page
.getByRole("group", { name: "Body systems", exact: true })
.getByRole("button", { name: "Cardiovascular", exact: true })
.click();
await expect(systems).toHaveCount(1);
test("filters body systems and opens lab history", async ({ page, vitalsPage, a11y }) => {
await vitalsPage.goTo();
await expect(vitalsPage.bodyHeading).toBeVisible();
await expect(vitalsPage.systems).toHaveCount(8);
await vitalsPage.selectSystem("Cardiovascular");
await expect(vitalsPage.systems).toHaveCount(1);
await expect(page).toHaveURL(/vitalTab=Cardiovascular/);
await page.reload();
await page.waitForLoadState("networkidle");
await expect(systems).toHaveCount(1);
await systems.first().click();
const dialog = page.getByRole("dialog", { name: "Cardiovascular" });
await expect(vitalsPage.systems).toHaveCount(1);
await vitalsPage.openFirstSystem();
const dialog = vitalsPage.systemDialog("Cardiovascular");
await expect(dialog).toContainText("68");
await page.goBack();
await expect(dialog).not.toBeVisible();
await page.goForward();
await expect(dialog).toBeVisible();
await dialog.getByRole("button", { name: "Close", exact: true }).last().click();
await page.getByRole("button", { name: "Lab history", exact: true }).click();
await expect(page.locator("[data-testid='vital-history']")).toContainText("Jul 28");
await expect(page.locator("[data-testid='vital-history']")).toContainText("93");
await vitalsPage.closeSystem("Cardiovascular");
await vitalsPage.showLabHistory();
await expect(vitalsPage.history).toContainText("Jul 28");
await expect(vitalsPage.history).toContainText("93");
await page.reload();
await page.waitForLoadState("networkidle");
await expect(page.locator("[data-testid='vital-history']")).toContainText("Jul 28");
await page.getByRole("button", { name: "Systems", exact: true }).click();
await page
.getByRole("group", { name: "Body systems", exact: true })
.getByRole("button", { name: "Overview", exact: true })
.click();
await expect(systems).toHaveCount(8);
await expect(vitalsPage.history).toContainText("Jul 28");
await vitalsPage.showSystems();
await vitalsPage.selectSystem("Overview");
await expect(vitalsPage.systems).toHaveCount(8);
expect((await a11y().analyze()).violations).toEqual([]);
});
test("explores environment charts and simulated climate controls", async ({ page, a11y }) => {
await page.goto("/vitals");
await page.waitForLoadState("networkidle");
const menu = page.getByRole("button", { name: "Open Health menu" });
if (await menu.isVisible()) await menu.click();
await page
.getByRole("complementary", { name: "Health navigation" })
.getByRole("button", { name: "Environment", exact: true })
.click();
await expect(page.getByRole("heading", { name: "Environment", exact: true })).toBeVisible();
await page.getByRole("button", { name: "7D", exact: true }).click();
await page
.getByRole("group", { name: "Trend metric" })
.getByRole("button", { name: "CO₂", exact: true })
.click();
await expect(
page.getByRole("img", { name: "7D CO₂ simulated environmental readings" }),
).toBeVisible();
test("explores environment charts and simulated climate controls", async ({
page,
vitalsPage,
a11y,
}) => {
await vitalsPage.goTo();
await vitalsPage.navigateTo("Environment");
await expect(vitalsPage.environmentHeading).toBeVisible();
await vitalsPage.selectTrendPeriod("7D");
await vitalsPage.selectTrendMetric("CO₂");
await expect(vitalsPage.trendChart("7D", "CO₂")).toBeVisible();
await page.reload();
await page.waitForLoadState("networkidle");
await expect(
page.getByRole("img", { name: "7D CO₂ simulated environmental readings" }),
).toBeVisible();
await page
.locator("[data-testid='vital-metric']")
.filter({ hasText: "Room temperature" })
.click();
const dialog = page.getByRole("dialog", { name: "Room temperature" });
await expect(vitalsPage.trendChart("7D", "CO₂")).toBeVisible();
await vitalsPage.openMetric("Room temperature");
const dialog = vitalsPage.metricDialog("Room temperature");
await expect(dialog).toContainText("64.0");
await page.reload();
await page.waitForLoadState("networkidle");
await expect(dialog).toContainText("64.0");
await dialog.getByRole("button", { name: "Back to overview" }).click();
await page.getByRole("button", { name: "Sync now", exact: true }).click();
await expect(
page.getByRole("status").filter({ hasText: "Sample readings refreshed" }),
).toBeVisible();
await vitalsPage.closeMetric("Room temperature");
await vitalsPage.syncReadings();
await expect(vitalsPage.refreshedStatus).toBeVisible();
expect((await a11y().analyze()).violations).toEqual([]);
await page.getByRole("button", { name: "Climate control", exact: true }).click();
await page.getByRole("button", { name: "Cool", exact: true }).click();
await expect(page.getByRole("button", { name: "Cool", exact: true })).toHaveAttribute(
"aria-pressed",
"true",
);
const temperature = page.getByRole("slider", { name: "Target temperature" });
await vitalsPage.openClimateControl();
await vitalsPage.enableCooling();
await expect(vitalsPage.coolButton).toHaveAttribute("aria-pressed", "true");
const temperature = vitalsPage.targetTemperature;
await temperature.focus();
await temperature.press("ArrowRight");
await expect(temperature).toHaveValue("69");
await page.getByRole("button", { name: "Air purifier · On" }).click();
await expect(page.getByRole("button", { name: "Air purifier · Off" })).toHaveAttribute(
"aria-pressed",
"false",
);
await vitalsPage.turnOffPurifier();
await expect(vitalsPage.purifierOffButton).toHaveAttribute("aria-pressed", "false");
await page.reload();
await page.waitForLoadState("networkidle");
await expect(temperature).toHaveValue("68");
await expect(page.getByRole("button", { name: "Auto", exact: true })).toHaveAttribute(
"aria-pressed",
"true",
);
await expect(page.getByRole("button", { name: "Air purifier · On" })).toHaveAttribute(
"aria-pressed",
"true",
);
await expect(vitalsPage.autoButton).toHaveAttribute("aria-pressed", "true");
await expect(vitalsPage.purifierOnButton).toHaveAttribute("aria-pressed", "true");
});
test("keeps both dashboards within the viewport", async ({ page }, testInfo) => {
await page.goto("/vitals");
await page.waitForLoadState("networkidle");
test("keeps both dashboards within the viewport", async ({ page, vitalsPage }, testInfo) => {
await vitalsPage.goTo();
for (const width of [375, 768, 1440]) {
await page.setViewportSize({ width, height: 1000 });
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBe(width);
await page.screenshot({ path: testInfo.outputPath(`body-${width}.png`) });
const menu = page.getByRole("button", { name: "Open Health menu" });
if (await menu.isVisible()) await menu.click();
const navigation = page.getByRole("complementary", {
name: "Health navigation",
});
await navigation.getByRole("button", { name: "Environment", exact: true }).click();
await vitalsPage.navigateTo("Environment");
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBe(width);
await page.screenshot({
path: testInfo.outputPath(`environment-${width}.png`),
});
if (await menu.isVisible()) await menu.click();
await navigation.getByRole("button", { name: "Body", exact: true }).click();
await page.screenshot({ path: testInfo.outputPath(`environment-${width}.png`) });
await vitalsPage.navigateTo("Body");
}
});
+43 -38
View File
@@ -1,10 +1,11 @@
import type { Page } from "@playwright/test";
import Database from "better-sqlite3";
import { expect, test } from "../fixtures";
import { DeckPage } from "../models/DeckPage";
type NativeTesting = {
listTools(): { name: string }[];
executeTool(name: string, input: string): Promise<string | null>;
type NativeModelContext = {
getTools(): Promise<{ name: string }[]>;
executeTool(tool: { name: string }, input: string): Promise<string | null>;
};
type ToolResult = {
content: { type: string; text: string }[];
@@ -13,13 +14,12 @@ type ToolResult = {
test.use({
launchOptions: {
executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE,
args: ["--enable-blink-features=WebMCP,WebMCPTesting"],
args: ["--enable-blink-features=WebMCP"],
},
});
async function toolNames(page: Page) {
return page.evaluate(() =>
(navigator as Navigator & { modelContextTesting: NativeTesting }).modelContextTesting
.listTools()
return page.evaluate(async () =>
(await (document as Document & { modelContext: NativeModelContext }).modelContext.getTools())
.map((tool) => tool.name)
.sort(),
);
@@ -30,10 +30,12 @@ async function executeTool(
input: Record<string, unknown> = {},
): Promise<ToolResult> {
const result = await page.evaluate(
({ name, input }) =>
(
navigator as Navigator & { modelContextTesting: NativeTesting }
).modelContextTesting.executeTool(name, JSON.stringify(input)),
async ({ name, input }) => {
const model = (document as Document & { modelContext: NativeModelContext }).modelContext;
const tool = (await model.getTools()).find((candidate) => candidate.name === name);
if (!tool) throw new Error(`WebMCP tool not found: ${name}`);
return model.executeTool(tool, JSON.stringify(input));
},
{ name, input },
);
expect(result).not.toBeNull();
@@ -59,10 +61,10 @@ function savedCount() {
}
let accounts: Record<string, string>;
let pageErrors: string[];
test.beforeEach(async ({ page }) => {
test.beforeEach(async ({ page, deckPage }) => {
pageErrors = [];
page.on("pageerror", (error) => pageErrors.push(error.message));
await page.goto("/deck");
await deckPage.goTo();
await expect
.poll(() => toolNames(page))
.toEqual([
@@ -91,6 +93,7 @@ test("creates temporary research, edits it, persists explicitly and reopens it o
page,
browser,
baseURL,
deckPage,
}) => {
const created = await executeTool(page, "set_deck", {
title: "WebMCPの反応",
@@ -103,19 +106,16 @@ test("creates temporary research, edits it, persists explicitly and reopens it o
const deck = decoded(created).deck;
expect(deck.persisted).toBe(false);
expect(savedCount()).toEqual({ count: 0 });
await expect(page.getByText("alternate · Latest · all page 1", { exact: true })).toBeVisible();
await page
.getByRole("region", { name: "日本語", exact: true })
.getByRole("button", { name: "Actions for 日本語" })
.click();
await page.getByRole("menuitem", { name: "Edit", exact: true }).click();
await expect(deckPage.postText("alternate · Latest · all page 1")).toBeVisible();
await deckPage.openColumnActions("日本語");
await deckPage.editColumn.click();
const replaced = await executeTool(page, "set_deck", {
deckId: deck.id,
title: "更新した調査",
columns: [deck.columns[1]],
});
expect(replaced.isError).not.toBe(true);
await expect(page.getByRole("dialog")).toHaveCount(0);
await expect(deckPage.dialogs).toHaveCount(0);
expect(savedCount()).toEqual({ count: 0 });
expect((await executeTool(page, "save_deck", { deckId: deck.id })).isError).not.toBe(true);
expect((await executeTool(page, "save_deck", { deckId: deck.id })).isError).not.toBe(true);
@@ -127,19 +127,21 @@ test("creates temporary research, edits it, persists explicitly and reopens it o
});
try {
const device = await other.newPage();
await device.goto("/deck");
await expect(device.getByRole("heading", { level: 1 })).toHaveText("更新した調査");
await expect(
device.getByText("alternate · Latest · all page 1", { exact: true }),
).toBeVisible();
const deviceDeck = new DeckPage(device);
await deviceDeck.goTo();
await expect(deviceDeck.heading).toHaveText("更新した調査");
await expect(deviceDeck.postText("alternate · Latest · all page 1")).toBeVisible();
} finally {
await other.close();
}
await page.reload();
await expect(page.getByRole("heading", { level: 1 })).toHaveText("更新した調査");
await expect(deckPage.heading).toHaveText("更新した調査");
});
test("requires revisions for saved mutations and rejects stale overwrites", async ({ page }) => {
test("requires revisions for saved mutations and rejects stale overwrites", async ({
page,
deckPage,
}) => {
const deck = decoded(
await executeTool(page, "set_deck", { title: "保存済み", columns: [] }),
).deck;
@@ -173,7 +175,7 @@ test("requires revisions for saved mutations and rejects stale overwrites", asyn
})
).isError,
).toBe(true);
await expect(page.getByRole("heading", { level: 1 })).toHaveText("変更");
await expect(deckPage.heading).toHaveText("変更");
expect(
(
await executeTool(page, "delete_deck", {
@@ -195,6 +197,7 @@ test("requires revisions for saved mutations and rejects stale overwrites", asyn
test("keeps temporary views independent and loses only unsaved views on reload", async ({
page,
deckPage,
}) => {
const first = decoded(
await executeTool(page, "set_deck", { title: "一時の調査A", columns: [] }),
@@ -203,7 +206,7 @@ test("keeps temporary views independent and loses only unsaved views on reload",
await executeTool(page, "set_deck", { title: "一時の調査B", columns: [] }),
).deck;
await executeTool(page, "select_deck", { deckId: first.id });
await expect(page.getByRole("heading", { level: 1 })).toHaveText("一時の調査A");
await expect(deckPage.heading).toHaveText("一時の調査A");
await executeTool(page, "delete_deck", { deckId: second.id });
expect(
decoded(await executeTool(page, "list_decks")).decks.some(
@@ -212,10 +215,10 @@ test("keeps temporary views independent and loses only unsaved views on reload",
).toBe(false);
expect(savedCount()).toEqual({ count: 0 });
await page.reload();
await expect(page.getByRole("heading", { level: 1 })).toHaveText("New research");
await expect(deckPage.heading).toHaveText("New research");
});
test("rejects invalid input without changing the active view", async ({ page }) => {
test("rejects invalid input without changing the active view", async ({ page, deckPage }) => {
await executeTool(page, "set_deck", {
title: "残す調査",
columns: [column("残す観点", accounts.e2e ?? "")],
@@ -234,13 +237,14 @@ test("rejects invalid input without changing the active view", async ({ page })
expect((await executeTool(page, "set_deck", { title: "変更しない", columns })).isError).toBe(
true,
);
await expect(page.locator("[data-testid='deck-column'] h2")).toHaveText(["残す観点"]);
await expect(deckPage.columnTitles).toHaveText(["残す観点"]);
}
expect(savedCount()).toEqual({ count: 0 });
});
test("keeps failed temporary saves visible and allows retry without duplicate decks", async ({
page,
deckPage,
}) => {
const deck = decoded(
await executeTool(page, "set_deck", { title: "未保存の調査", columns: [] }),
@@ -249,21 +253,24 @@ test("keeps failed temporary saves visible and allows retry without duplicate de
route.request().method() === "POST" ? route.abort() : route.continue(),
);
expect((await executeTool(page, "save_deck", { deckId: deck.id })).isError).toBe(true);
await expect(page.getByRole("heading", { level: 1 })).toHaveText("未保存の調査");
await expect(deckPage.heading).toHaveText("未保存の調査");
expect(decoded(await executeTool(page, "get_deck")).deck.persisted).toBe(false);
await page.unroute("**/_serverFn/**");
expect((await executeTool(page, "save_deck", { deckId: deck.id })).isError).not.toBe(true);
expect(savedCount()).toEqual({ count: 1 });
});
test("reads and pages bound accounts without mixing sibling columns", async ({ page }) => {
test("reads and pages bound accounts without mixing sibling columns", async ({
page,
deckPage,
}) => {
const deck = decoded(
await executeTool(page, "set_deck", {
title: "接続を比較",
columns: [column("通常", accounts.e2e ?? ""), column("別の接続", accounts.alternate ?? "")],
}),
).deck;
await expect(page.getByText("alternate · Latest · all page 1", { exact: true })).toBeVisible();
await expect(deckPage.postText("alternate · Latest · all page 1")).toBeVisible();
const id = deck.columns[1].id;
const loaded = decoded(await executeTool(page, "get_column_posts", { columnId: id }));
expect(loaded.column.connectionId).toBe(accounts.alternate);
@@ -275,9 +282,7 @@ test("reads and pages bound accounts without mixing sibling columns", async ({ p
expect(more.posts.map((post: { text: string }) => post.text)).toEqual([
"alternate · Latest · all page 2",
]);
await expect(
page.getByRole("region", { name: "通常", exact: true }).locator("article"),
).toHaveCount(1);
await expect(deckPage.columnPosts("通常")).toHaveCount(1);
await executeTool(page, "set_deck", { title: "別の一時ビュー", columns: [] });
expect((await executeTool(page, "get_column_posts", { columnId: id })).isError).toBe(true);
});
+12 -13
View File
@@ -1,20 +1,19 @@
import { expect, test } from "../fixtures";
import { closeMobileSidebar, showResearch } from "../workspace";
test("keeps the sidebar, composer and deck reachable at compact widths", async ({
page,
deckPage,
researchPage,
workspacePage,
a11y,
}, testInfo) => {
await page.goto("/deck");
await deckPage.goTo();
for (const width of [320, 375, 414, 768]) {
await page.setViewportSize({ width, height: 850 });
await showResearch(page);
const chat = page.getByRole("complementary", { name: "Research chat" });
await workspacePage.showResearch();
const chat = researchPage.panel;
await expect(chat).toBeVisible();
const composer = chat.getByRole("textbox", {
name: "Message",
exact: true,
});
const composer = researchPage.composer;
await expect(composer).toBeVisible();
const bounds = await composer.boundingBox();
if (!bounds) throw new Error("Composer must have a visible bounding box");
@@ -26,10 +25,10 @@ test("keeps the sidebar, composer and deck reachable at compact widths", async (
path: testInfo.outputPath(`workspace-${width}.png`),
});
if (width === 320) expect((await a11y().analyze()).violations).toEqual([]);
await closeMobileSidebar(page);
await workspacePage.closeMobileSidebar();
}
await page.getByRole("button", { name: "Toggle sidebar" }).click();
await expect(page.getByRole("complementary", { name: "Research chat" })).toBeHidden();
await page.getByRole("button", { name: "Research chat", exact: true }).click();
await expect(page.getByRole("complementary", { name: "Research chat" })).toBeVisible();
await workspacePage.toggleSidebar.click();
await expect(researchPage.panel).toBeHidden();
await workspacePage.researchChat.click();
await expect(researchPage.panel).toBeVisible();
});