feat: add URL-driven workspace state and streamed Codex research

This commit is contained in:
2026-09-28 20:08:28 +09:00
parent 85d23a328b
commit 8085ad2f90
207 changed files with 15287 additions and 16026 deletions
+25 -35
View File
@@ -1,63 +1,53 @@
import { expect, test } from '../fixtures'
import { expect, test } from "../fixtures";
test('owner can open the app but missing or foreign identity is forbidden', async ({
request,
}) => {
expect((await request.get('/')).status()).toBe(200)
test("owner can open the app but missing or foreign identity is forbidden", async ({ request }) => {
expect((await request.get("/")).status()).toBe(200);
expect((await request.get("/", { headers: { "Tailscale-User-Login": "" } })).status()).toBe(403);
expect(
(
await request.get('/', { headers: { 'Tailscale-User-Login': '' } })
).status(),
).toBe(403)
expect(
(
await request.get('/', {
headers: { 'Tailscale-User-Login': '[email protected]' },
await request.get("/", {
headers: { "Tailscale-User-Login": "[email protected]" },
})
).status(),
).toBe(403)
})
).toBe(403);
});
test('mutations require exact Origin even with same-origin Fetch Metadata', async ({
request,
}) => {
expect((await request.post('/')).status()).toBe(403)
test("mutations require exact Origin even with same-origin Fetch Metadata", async ({ request }) => {
expect((await request.post("/")).status()).toBe(403);
expect(
(
await request.post('/', {
await request.post("/", {
headers: {
Origin: 'https://other.invalid',
'Sec-Fetch-Site': 'same-origin',
Origin: "https://other.invalid",
"Sec-Fetch-Site": "same-origin",
},
})
).status(),
).toBe(403)
})
).toBe(403);
});
test('server functions also reject foreign identity and cross-site calls', async ({
test("server functions also reject foreign identity and cross-site calls", async ({
page,
request,
}) => {
const serverRequest = page.waitForRequest((request) =>
request.url().includes('/_serverFn/'),
)
await page.goto('/deck')
const url = (await serverRequest).url()
const serverRequest = page.waitForRequest((request) => request.url().includes("/_serverFn/"));
await page.goto("/deck");
const url = (await serverRequest).url();
expect(
(
await request.get(url, {
headers: {
'Tailscale-User-Login': '[email protected]',
'Sec-Fetch-Site': 'same-origin',
"Tailscale-User-Login": "[email protected]",
"Sec-Fetch-Site": "same-origin",
},
})
).status(),
).toBe(403)
).toBe(403);
expect(
(
await request.get(url, {
headers: { 'Sec-Fetch-Site': 'cross-site' },
headers: { "Sec-Fetch-Site": "cross-site" },
})
).status(),
).toBe(403)
})
).toBe(403);
});
+98 -136
View File
@@ -1,172 +1,134 @@
import { openDatabase } from '../../src/features/storage/database.server'
import { expect, test } from '../fixtures'
import { openDatabase } from "../../src/features/storage/database.server";
import { expect, test } from "../fixtures";
const origin = 'http://127.0.0.1:4173'
const origin = "http://127.0.0.1:4173";
test('creates the owner once and enters onboarding with a real session', async ({
test("creates the owner once and enters onboarding with a real session", async ({
page,
context,
a11y,
}) => {
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 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 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()
})
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 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 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();
});
test('requires login for documents, server functions, and live updates', async ({
test("requires login for documents, server functions, and live updates", async ({
page,
context,
}) => {
const serverRequest = page.waitForRequest((request) =>
request.url().includes('/_serverFn/'),
)
await page.goto('/deck')
const serverUrl = (await serverRequest).url()
await context.clearCookies()
for (const path of ['/api/research/events', serverUrl]) {
const serverRequest = page.waitForRequest((request) => request.url().includes("/_serverFn/"));
await page.goto("/deck");
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' },
})
expect(result.status()).toBe(401)
headers: { Origin: origin, "Sec-Fetch-Site": "same-origin" },
});
expect(result.status()).toBe(401);
}
await page.goto('/journal')
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 page.goto("/journal");
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);
});
test('signs in and revokes the session on sign out', async ({
page,
context,
a11y,
}) => {
await context.clearCookies()
await page.goto('/login', { waitUntil: 'networkidle' })
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 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 expect(page).toHaveURL(/\/login$/)
const replay = await context.request.get('/api/auth', {
test("signs in and revokes the session on sign out", async ({ page, context, a11y }) => {
await context.clearCookies();
await page.goto("/login", { waitUntil: "networkidle" });
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 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 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)
})
});
expect((await replay.json()).owner).toBeNull();
await page.goBack();
await expect(page.getByRole("link", { name: "Home", exact: true })).toHaveCount(0);
});
test('requires onboarding and remembers its completion and name', async ({
test("requires onboarding and remembers its completion and name", async ({
page,
request,
a11y,
}) => {
const db = openDatabase(process.env.TWITTER_LITE_E2E_DB_PATH ?? '')
const db = openDatabase(process.env.TWITTER_LITE_E2E_DB_PATH ?? "");
db.$client
.prepare(
'UPDATE workspace_owner SET onboarding_completed_at = NULL WHERE id = 1',
)
.run()
.prepare("UPDATE workspace_owner SET onboarding_completed_at = NULL WHERE id = 1")
.run();
try {
expect((await request.get('/api/research/events')).status()).toBe(403)
await page.goto('/', { waitUntil: '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 page.reload()
await expect(
page.getByRole('heading', { name: 'Good morning, Yuta Test' }),
).toBeVisible()
await page.goto('/onboarding')
await expect(page).toHaveURL(`${origin}/`)
expect((await request.get("/api/research/events")).status()).toBe(403);
await page.goto("/", { waitUntil: "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 page.reload();
await expect(page.getByRole("heading", { name: "Good morning, Yuta Test" })).toBeVisible();
await page.goto("/onboarding");
await expect(page).toHaveURL(`${origin}/`);
} finally {
db.$client
.prepare(
'UPDATE workspace_owner SET name = ?, onboarding_completed_at = ? WHERE id = 1',
)
.run('Yuta', Date.now())
db.$client.close()
.prepare("UPDATE workspace_owner SET name = ?, onboarding_completed_at = ? WHERE id = 1")
.run("Yuta", Date.now());
db.$client.close();
}
})
});
test('rejects cross-origin login and further account registration', async ({
test("rejects cross-origin login and further account registration", async ({
request,
page,
context,
}) => {
expect(
(
await request.post('/api/auth', {
headers: { Origin: 'https://other.invalid' },
data: { action: 'logout' },
await request.post("/api/auth", {
headers: { Origin: "https://other.invalid" },
data: { action: "logout" },
})
).status(),
).toBe(403)
const result = await request.post('/api/auth', {
).toBe(403);
const result = await request.post("/api/auth", {
headers: { Origin: origin },
data: {
action: 'setup',
email: '[email protected]',
name: 'Intruder',
password: 'Long-enough-password',
setupToken: 'arbitrary-token',
action: "setup",
email: "[email protected]",
name: "Intruder",
password: "Long-enough-password",
setupToken: "arbitrary-token",
},
})
expect(result.status()).toBe(409)
await context.clearCookies()
await page.goto('/setup')
await expect(page).toHaveURL(/\/login$/)
})
});
expect(result.status()).toBe(409);
await context.clearCookies();
await page.goto("/setup");
await expect(page).toHaveURL(/\/login(?:\?|$)/);
});
+40 -58
View File
@@ -1,70 +1,52 @@
import { expect, test } from '../fixtures'
import { expect, test } from "../fixtures";
test('opens account management after hydration without inheriting real OAuth configuration', async ({
test("opens account management after hydration without inheriting real OAuth configuration", async ({
page,
}) => {
await page.goto('/deck')
await expect(
page.getByRole('button', { name: 'Rename', includeHidden: true }),
).toBeEnabled()
const manage = page.getByRole('button', { name: 'Manage connected accounts' })
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 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 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();
});
test('opens account management from the inbox footer and restores focus on close', async ({
test("opens account management from the inbox footer and restores focus on close", async ({
page,
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()
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 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();
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();
});
test('rejects an unbound OAuth callback without contacting an instance', async ({
page,
}) => {
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' })
test("rejects an unbound OAuth callback without contacting an instance", async ({ page }) => {
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 page.getByRole("button", { name: "Toggle sidebar" }).click();
}
await expect(
page.getByRole('button', { name: 'Manage connected accounts' }),
).toBeEnabled()
})
await expect(page.getByRole("button", { name: "Manage connected accounts" })).toBeEnabled();
});
+248 -342
View File
@@ -1,296 +1,230 @@
import type { Page } from '@playwright/test'
import { expect, test } from '../fixtures'
import { closeMobileSidebar, showDeckManagement } from '../workspace'
import type { Page } from "@playwright/test";
import { expect, test } from "../fixtures";
import { closeMobileSidebar, showDeckManagement } from "../workspace";
async function addColumn(
page: Page,
title: string,
query: string,
profile = 'e2e',
kind = 'search',
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 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 })
.getByRole("combobox", { name: "Connection profile", exact: true })
.selectOption({ label: profile });
await editor.getByRole("combobox", { name: "Column type", exact: true }).selectOption(kind);
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)
.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()
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)
})
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('binds identical searches to independent profiles through paging, editing and reload', async ({
test("binds identical searches to independent profiles through paging, editing and reload", async ({
page,
}) => {
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 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 page.reload()
await expect(
second.getByText('Latest · all page 1', { exact: true }),
).toBeVisible()
await expect(first.locator('article')).toHaveCount(1)
})
.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 page.reload();
await expect(second.getByText("Latest · all page 1", { exact: true })).toBeVisible();
await expect(first.locator("article")).toHaveCount(1);
});
test('creates, switches, renames and deletes persisted deck profiles', async ({
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(".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(".deck-column h2")).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(".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 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);
});
test("reads user and list timelines within columns and reorders, deletes and restores them", 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('.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('.deck-column h2')).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('.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 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 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(".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(".deck-column h2")).toHaveText(["リストの投稿", "アカウント"]);
await page.reload();
await expect(user.getByText("alternate profile page 1")).toBeVisible();
await expect(list.getByText("list page 1")).toBeVisible();
});
test('reads user and list timelines within columns and reorders, deletes and restores them', async ({
page,
}) => {
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('.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('.deck-column h2')).toHaveText([
'リストの投稿',
'アカウント',
])
await page.reload()
await expect(user.getByText('alternate profile page 1')).toBeVisible()
await expect(list.getByText('list page 1')).toBeVisible()
})
test('keeps other columns usable when pagination fails and retries that column', async ({
test("keeps other columns usable when pagination fails and retries that column", async ({
page,
}, 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 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);
});
test('keeps the deck and editor accessible within narrow viewports', async ({
test("keeps the deck and editor accessible within narrow viewports", async ({
page,
a11y,
}, testInfo) => {
await addColumn(page, '日本語の反応', 'WebMCP lang:ja')
await addColumn(page, '海外の反応', 'WebMCP lang:en', 'alternate')
await addColumn(page, '公式アカウント', 'OpenAI', 'e2e', 'user')
await addColumn(page, "日本語の反応", "WebMCP lang:ja");
await addColumn(page, "海外の反応", "WebMCP lang:en", "alternate");
await addColumn(page, "公式アカウント", "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(false)
expect(
await page.evaluate(
() => document.documentElement.scrollHeight <= innerHeight,
),
).toBe(true)
const board = await page.locator('.deck-board').boundingBox()
expect(board).not.toBeNull()
expect((board?.y ?? 0) + (board?.height ?? 0)).toBeLessThanOrEqual(901)
await page.setViewportSize({ width, height: 900 });
expect(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth)).toBe(
false,
);
expect(await page.evaluate(() => document.documentElement.scrollHeight <= innerHeight)).toBe(
true,
);
const board = await page.locator(".deck-board").boundingBox();
expect(board).not.toBeNull();
expect((board?.y ?? 0) + (board?.height ?? 0)).toBeLessThanOrEqual(901);
}
await page.setViewportSize({
width: testInfo.project.name === 'desktop' ? 1440 : 393,
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()
expect((await a11y().analyze()).violations).toEqual([])
});
await expect(page.getByRole("navigation", { name: "Columns" })).toHaveCount(0);
await expect(page.getByRole("heading", { name: "公式アカウント", exact: true })).toBeVisible();
expect((await a11y().analyze()).violations).toEqual([]);
await page.screenshot({
path: testInfo.outputPath('deck-populated.png'),
path: testInfo.outputPath("deck-populated.png"),
fullPage: true,
})
await showDeckManagement(page)
await page.getByRole('button', { name: 'Add column', exact: true }).click()
expect((await a11y().analyze()).violations).toEqual([])
});
await showDeckManagement(page);
await page.getByRole("button", { name: "Add column", exact: true }).click();
expect((await a11y().analyze()).violations).toEqual([]);
await page.screenshot({
path: testInfo.outputPath('deck.png'),
path: testInfo.outputPath("deck.png"),
fullPage: true,
})
})
});
});
test('closes the column editor with Escape and restores focus without applying edits', async ({
test("closes the column editor with Escape and restores focus without applying edits", async ({
page,
}) => {
await showDeckManagement(page)
const opener = page.getByRole('button', { name: 'Add column', exact: true })
await opener.click()
await expect(
page.getByRole('dialog', { name: 'Add column', exact: true }),
).toBeVisible()
await page
.getByLabel('Column name', { exact: true })
.fill('キャンセルする観点')
await page.keyboard.press('Escape')
await expect(
page.getByRole('dialog', { name: 'Add column', exact: true }),
).toHaveCount(0)
await expect(opener).toBeFocused()
await expect(page.locator('.deck-column')).toHaveCount(0)
await opener.click()
await expect(page.getByLabel('Column name', { exact: true })).toHaveValue('')
})
await showDeckManagement(page);
const opener = page.getByRole("button", { name: "Add column", exact: true });
await opener.click();
await expect(page.getByRole("dialog", { name: "Add column", exact: true })).toBeVisible();
await page.getByLabel("Column name", { exact: true }).fill("キャンセルする観点");
await page.keyboard.press("Escape");
await expect(page.getByRole("dialog", { name: "Add column", exact: true })).toHaveCount(0);
await expect(opener).toBeFocused();
await expect(page.locator(".deck-column")).toHaveCount(0);
await opener.click();
await expect(page.getByLabel("Column name", { exact: true })).toHaveValue("");
});
test('does not overwrite unreadable storage on reload', async ({ page }) => {
await page.evaluate(() =>
localStorage.setItem('twitter-lite-research-deck', '{broken'),
)
await page.reload()
await expect(page.getByRole('alert')).toContainText('Unable to load')
expect(
await page.evaluate(() =>
localStorage.getItem('twitter-lite-research-deck'),
),
).toBe('{broken')
})
test("does not overwrite unreadable storage on reload", async ({ page }) => {
await page.evaluate(() => localStorage.setItem("twitter-lite-research-deck", "{broken"));
await page.reload();
await expect(page.getByRole("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 ({
test("selects a list from the column profile catalog and refreshes it on profile change", async ({
page,
}) => {
await showDeckManagement(page)
await page.getByRole('button', { name: 'Add column', exact: true }).click()
await page.getByLabel('Column name', { exact: true }).fill('購読リスト')
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' })
.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: '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)
.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()
})
.getByRole("region", { name: "購読リスト", exact: true })
.getByText("list page 1", { exact: true }),
).toBeVisible();
});
test('keeps an open draft through remote edits and rejects its stale save', async ({
test("keeps an open draft through remote edits and rejects its stale save", async ({
page,
browser,
baseURL,
@@ -298,124 +232,96 @@ test('keeps an open draft through remote edits and rejects its stale save', asyn
const other = await browser.newContext({
storageState: await page.context().storageState(),
baseURL,
extraHTTPHeaders: { 'Tailscale-User-Login': '[email protected]' },
})
extraHTTPHeaders: { "Tailscale-User-Login": "[email protected]" },
});
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(
'別の端末の変更',
)
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 page.keyboard.press('Escape')
await page.getByRole('button', { name: 'Reload latest deck' }).click()
await expect(page.getByRole('heading', { level: 1 })).toHaveText(
'別の端末の変更',
)
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("別の端末の変更");
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 page.keyboard.press("Escape");
await page.getByRole("button", { name: "Reload latest deck" }).click();
await expect(page.getByRole("heading", { level: 1 })).toHaveText("別の端末の変更");
} finally {
await other.close()
await other.close();
}
})
});
test('imports old browser decks explicitly and avoids duplicate imports after retry', async ({
test("imports old browser decks explicitly and avoids duplicate imports after retry", async ({
page,
}) => {
const legacy = {
version: 2,
activeDeckId: 'old',
activeDeckId: "old",
decks: [
{
id: 'old',
title: '以前の調査',
id: "old",
title: "以前の調査",
columns: [
{
id: 'a',
title: '以前の観点',
profileName: 'e2e',
id: "a",
title: "以前の観点",
profileName: "e2e",
source: {
platform: 'twitter',
kind: 'search',
query: 'WebMCP',
product: 'Latest',
platform: "twitter",
kind: "search",
query: "WebMCP",
product: "Latest",
following: false,
},
},
],
},
],
}
};
await page.evaluate(
(legacy) =>
localStorage.setItem(
'twitter-lite-research-deck',
JSON.stringify(legacy),
),
(legacy) => localStorage.setItem("twitter-lite-research-deck", JSON.stringify(legacy)),
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 selector.selectOption({ label: '以前の調査' })
await closeMobileSidebar(page)
await expect(
page.getByText('Latest · all page 1', { exact: true }),
).toBeVisible()
);
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 selector.selectOption({ label: "以前の調査" });
await closeMobileSidebar(page);
await expect(page.getByText("Latest · all page 1", { exact: true })).toBeVisible();
await page.evaluate(
(legacy) =>
localStorage.setItem(
'twitter-lite-research-deck',
JSON.stringify(legacy),
),
(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 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);
});
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 }) => {
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("画面を回転しても残る調査");
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 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 dialog.getByRole('button', { name: 'Create', exact: true }).click()
await expect(dialog).toHaveCount(0)
await expect(page.getByRole('heading', { level: 1 })).toHaveText(
'画面を回転しても残る調査',
)
})
await dialog.getByRole("button", { name: "Create", exact: true }).click();
await expect(dialog).toHaveCount(0);
await expect(page.getByRole("heading", { level: 1 })).toHaveText("画面を回転しても残る調査");
});
+94 -120
View File
@@ -1,139 +1,113 @@
import { expect, test } from '../fixtures'
import { expect, test } from "../fixtures";
test('previews a natural-language memo and shares the executed record with journal', async ({
test("previews a natural-language memo and shares the executed record with journal", async ({
page,
}) => {
await page.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 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 page.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 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();
});
test('keeps the home usable on narrow screens', async ({
page,
a11y,
}, testInfo) => {
await page.goto('/')
await page.waitForLoadState('networkidle')
test("keeps the home usable on narrow screens", async ({ page, a11y }, testInfo) => {
await page.goto("/");
await page.waitForLoadState("networkidle");
for (const width of [320, 375, 768]) {
await page.setViewportSize({ width, height: 850 })
await expect(
page.getByRole('textbox', { name: 'Home 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([])
await page.setViewportSize({ width, height: 850 });
await expect(page.getByRole("textbox", { name: "Home 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('/')
await page.waitForLoadState('networkidle')
const input = page.getByRole('textbox', { name: 'Home input' })
const complete = page.getByRole('button', {
name: 'Complete grocery shopping',
test("adds tasks only after confirmation and can defer and restore them", async ({ page }) => {
await page.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()
await expect(complete).toHaveCount(0)
await page.getByRole('button', { name: 'Apply', exact: true }).click()
await expect(complete).toBeVisible()
await input.fill('Defer grocery shopping')
await page.getByRole('button', { name: 'Preview action' }).click()
await expect(complete).toBeVisible()
await page.getByRole('button', { name: 'Apply', exact: true }).click()
await expect(complete).toHaveCount(0)
await page.locator('.home-rest summary').click()
await page
.getByRole('button', { name: 'Restore grocery shopping', exact: true })
.click()
await complete.click()
await expect(complete).toHaveCount(0)
await page.locator('.home-rest summary').click()
});
await input.fill("Add grocery shopping");
await page.getByRole("button", { name: "Preview action" }).click();
await expect(complete).toHaveCount(0);
await page.getByRole("button", { name: "Apply", exact: true }).click();
await expect(complete).toBeVisible();
await input.fill("Defer grocery shopping");
await page.getByRole("button", { name: "Preview action" }).click();
await expect(complete).toBeVisible();
await page.getByRole("button", { name: "Apply", exact: true }).click();
await expect(complete).toHaveCount(0);
await page.locator(".home-rest summary").click();
await page.getByRole("button", { name: "Restore grocery shopping", exact: true }).click();
await complete.click();
await expect(complete).toHaveCount(0);
await page.locator(".home-rest summary").click();
await expect(
page.getByRole('button', { name: 'Restore grocery shopping', exact: true }),
).toBeVisible()
})
page.getByRole("button", { name: "Restore grocery shopping", exact: true }),
).toBeVisible();
});
test('resolves communication within home and carries the status to the shared queue', async ({
test("resolves communication within home and carries the status to the shared queue", async ({
page,
a11y,
}) => {
await page.goto('/')
await page.waitForLoadState('networkidle')
await page
.getByRole('button', { name: 'Open conversation with Haruka Aoki' })
.click()
const dialog = page.getByRole('dialog', {
name: 'Conversation with Haruka Aoki',
})
await page.goto("/");
await page.waitForLoadState("networkidle");
await page.getByRole("button", { name: "Open conversation with Haruka Aoki" }).click();
const dialog = page.getByRole("dialog", {
name: "Conversation with Haruka Aoki",
});
await expect(dialog.getByRole("textbox", { name: "Reply", exact: true })).toBeVisible();
expect((await a11y().analyze()).violations).toEqual([]);
await dialog.getByRole("button", { name: "Leave for later" }).click();
await expect(dialog).toHaveCount(0);
await expect(
dialog.getByRole('textbox', { name: 'Reply', exact: true }),
).toBeVisible()
expect((await a11y().analyze()).violations).toEqual([])
await dialog.getByRole('button', { name: 'Leave for later' }).click()
await expect(dialog).toHaveCount(0)
page.getByRole("button", { name: "Open conversation with Haruka Aoki" }),
).toHaveCount(0);
await page.getByRole("link", { name: "Messages", exact: true }).first().click();
const rows = page.locator(".support-row");
await expect(rows.filter({ hasText: "Schedule next week’s design review" })).toHaveCount(0);
await page.getByRole("button", { name: "Later", exact: true }).click();
await rows.filter({ hasText: "Schedule next week’s design review" }).click();
await page.getByRole("button", { name: "Move to Now" }).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(
page.getByRole('button', { name: 'Open conversation with Haruka Aoki' }),
).toHaveCount(0)
await page
.getByRole('link', { name: 'Messages', exact: true })
.first()
.click()
const rows = page.locator('.support-row')
await expect(
rows.filter({ hasText: 'Schedule next week’s design review' }),
).toHaveCount(0)
await page.getByRole('button', { name: 'Later', exact: true }).click()
await rows.filter({ hasText: 'Schedule next week’s design review' }).click()
await page.getByRole('button', { name: 'Move to Now' }).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(
page.getByRole('button', { name: 'Open conversation with Haruka Aoki' }),
).toBeVisible()
})
page.getByRole("button", { name: "Open conversation with Haruka Aoki" }),
).toBeVisible();
});
test('updates the prose brief as priorities change and opens the referenced conversation', async ({
test("updates the prose brief as priorities change and opens the referenced conversation", async ({
page,
}) => {
await page.goto('/')
await page.waitForLoadState('networkidle')
const brief = page.getByRole('region', { name: 'Daily brief' })
await expect(brief).toContainText('Review the documents')
await expect(brief).toContainText('4 conversations')
await page
.getByRole('button', { name: 'Complete Review the documents', exact: true })
.click()
await expect(brief).not.toContainText('Review the documents')
await brief.getByRole('button', { name: 'Reply to Haruka Aoki' }).click()
const dialog = page.getByRole('dialog', {
name: 'Conversation with Haruka Aoki',
await page.goto("/");
await page.waitForLoadState("networkidle");
const brief = page.getByRole("region", { name: "Daily brief" });
await expect(brief).toContainText("Review the documents");
await expect(brief).toContainText("4 conversations");
await page.getByRole("button", { name: "Complete Review the documents", exact: true }).click();
await expect(brief).not.toContainText("Review the documents");
await brief.getByRole("button", { name: "Reply to Haruka Aoki" }).click();
const dialog = page.getByRole("dialog", {
name: "Conversation with Haruka Aoki",
exact: true,
})
await dialog.getByRole('button', { name: 'Leave for later' }).click()
await expect(brief).toContainText('3 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 expect(brief).toContainText('資料の2ページ目から')
})
});
await dialog.getByRole("button", { name: "Leave for later" }).click();
await expect(brief).toContainText("3 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 expect(brief).toContainText("資料の2ページ目から");
});
+60 -86
View File
@@ -1,101 +1,75 @@
import { expect, test } from '../fixtures'
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('.inbox-article-row')
await expect(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 expect(reader).toBeVisible()
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(".inbox-article-row");
await expect(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 expect(reader).toBeVisible();
await expect(reader.getByRole("heading", { name: articleTitle, exact: true })).toBeVisible();
await expect(
reader.getByRole('heading', { name: articleTitle, exact: true }),
).toBeVisible()
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(
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',
page.getByRole("article", {
name: "Three columns create a rhythm for reading",
exact: true,
}),
).toBeVisible()
})
).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('.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)
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(page).toHaveURL(/\/deck$/)
await page.getByRole('link', { name: 'Reader', exact: true }).click()
await expect(page).toHaveURL(/\/inbox$/)
await expect(rows).toHaveCount(9)
})
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(".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);
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(page).toHaveURL(/\/deck$/);
await page.getByRole("link", { name: "Reader", exact: true }).click();
await expect(page).toHaveURL(/\/inbox$/);
await expect(rows).toHaveCount(9);
});
test('keeps article navigation readable without horizontal overflow', async ({
test("keeps article navigation readable without horizontal overflow", async ({
page,
a11y,
}, testInfo) => {
await page.goto('/inbox')
await page.waitForLoadState('networkidle')
await page.goto("/inbox");
await page.waitForLoadState("networkidle");
for (const width of [320, 375, 414, 768]) {
await page.setViewportSize({ width, height: 850 })
await expect(
page.getByRole('textbox', { name: 'Search articles' }),
).toBeVisible()
expect(
await page.evaluate(() => document.documentElement.scrollWidth),
).toBe(width)
await page.locator('.inbox-article-row').first().click()
const reader = page.getByRole('article', {
name: 'Thinking in the browser: a new workflow with AI agents',
await page.setViewportSize({ width, height: 850 });
await expect(page.getByRole("textbox", { name: "Search articles" })).toBeVisible();
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBe(width);
await page.locator(".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()
expect(
await page.evaluate(() => document.documentElement.scrollWidth),
).toBe(width)
});
await expect(reader).toBeVisible();
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBe(width);
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()
});
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();
}
})
});
+293 -266
View File
@@ -1,351 +1,378 @@
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 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";
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 }) => {
await page.goto("/deck");
const snapshots = await page.evaluate(async () => {
const connect = () =>
new Promise<unknown>((resolve, reject) => {
const events = new EventSource('/api/research/events')
const events = new EventSource("/api/research/events");
const timer = setTimeout(() => {
events.close()
reject(new Error('SSE timed out'))
}, 5000)
events.close();
reject(new Error("SSE timed out"));
}, 5000);
events.onmessage = (event) => {
clearTimeout(timer)
events.close()
resolve(JSON.parse(event.data))
}
clearTimeout(timer);
events.close();
resolve(JSON.parse(event.data));
};
events.onerror = () => {
clearTimeout(timer)
events.close()
reject(new Error('SSE failed'))
}
})
return [await connect(), await connect()]
})
clearTimeout(timer);
events.close();
reject(new Error("SSE failed"));
};
});
return [await connect(), await connect()];
});
expect(snapshots).toEqual([
{ configured: false, run: null, history: [] },
{ configured: false, run: null, history: [] },
])
})
]);
});
test('restores the conversation and growing temporary deck from reconnect snapshots', async ({
test("restores the conversation and growing temporary deck from reconnect snapshots", async ({
page,
}) => {
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',
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 accounts.selectOption({ label: 'e2e' })
const connectionId = await accounts.inputValue()
await page.keyboard.press('Escape')
await closeMobileSidebar(page)
});
await accounts.selectOption({ label: "e2e" });
const connectionId = await accounts.inputValue();
await page.keyboard.press("Escape");
await closeMobileSidebar(page);
const run: ResearchRun = {
id: '36ad8cc7-318c-46ae-94fa-28d30336f027',
threadId: 'conversation-test',
id: "36ad8cc7-318c-46ae-94fa-28d30336f027",
threadId: "conversation-test",
connectionIds: [connectionId],
topic: '既存の観点を使って調査して',
status: 'running',
topic: "既存の観点を使って調査して",
status: "running",
startedAt: Date.now(),
deckVersion: 1,
citations: [],
message: '既存の検索条件を再利用しています。',
message: "既存の検索条件を再利用しています。",
messages: [
{ id: 'user-1', role: 'user', text: '既存の観点を使って調査して' },
{ id: 'tool-1', role: 'tool', text: 'list_decks: 完了' },
{ id: "user-1", role: "user", text: "既存の観点を使って調査して" },
{ id: "tool-1", role: "tool", text: "list_decks: 完了" },
{
id: 'assistant-1',
role: 'assistant',
text: '既存の検索条件を再利用しています。',
id: "assistant-1",
role: "assistant",
text: "既存の検索条件を再利用しています。",
},
],
deck: {
id: 'generated-plan',
title: '会話からの調査',
id: "generated-plan",
title: "会話からの調査",
columns: [
{
id: 'one',
title: '最初の観点',
id: "one",
title: "最初の観点",
connectionId,
source: {
platform: 'twitter',
kind: 'search',
query: 'WebMCP',
product: 'Latest',
platform: "twitter",
kind: "search",
query: "WebMCP",
product: "Latest",
following: false,
},
},
],
},
}
await page.route('**/api/research/events', (route) =>
};
await page.route("**/api/research/events*", (route) =>
route.fulfill({
contentType: 'text/event-stream',
contentType: "text/event-stream",
body: `data: ${JSON.stringify({ configured: true, run })}\n\n`,
}),
)
await page.reload()
await showResearch(page)
const conversation = page.getByRole('log', { name: 'Research 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('.deck-column h2')).toHaveText(['最初の観点'])
run.deckVersion = 2
run.status = 'complete'
);
await page.goto(`/deck?run=${run.id}`);
await showResearch(page);
const conversation = page.getByRole("log", { name: "Research 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(".deck-column h2")).toHaveText(["最初の観点"]);
run.deckVersion = 2;
run.status = "complete";
run.messages.push({
id: 'assistant-2',
role: 'assistant',
text: '比較する観点を追加しました。',
})
id: "assistant-2",
role: "assistant",
text: "比較する観点を追加しました。",
});
run.deck?.columns.push({
id: 'two',
title: '比較の観点',
id: "two",
title: "比較の観点",
connectionId,
source: {
platform: 'twitter',
kind: 'search',
query: 'WebMCP lang:ja',
product: 'Latest',
platform: "twitter",
kind: "search",
query: "WebMCP lang:ja",
product: "Latest",
following: false,
},
})
await page.reload()
await showResearch(page)
await expect(conversation).toContainText('既存の観点を使って調査して')
await expect(conversation).toContainText('比較する観点を追加しました。')
await expect(page.locator('.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 accountPicker.click()
await expect(accountPicker).toHaveAttribute('aria-expanded', 'true')
await expect(
page.getByRole('checkbox', { name: '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()
run.deckVersion = 3
if (run.deck) run.deck.title = '閉じている間に更新された調査'
await expect(page.getByRole('heading', { level: 1 })).toHaveText(
'閉じている間に更新された調査',
{ timeout: 15000 },
)
await showResearch(page)
await expect(page.getByLabel('Message', { exact: true })).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 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 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 page.reload();
await showResearch(page);
await expect(conversation).toContainText("既存の観点を使って調査して");
await expect(conversation).toContainText("比較する観点を追加しました。");
await expect(page.locator(".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 accountPicker.click();
await expect(accountPicker).toHaveAttribute("aria-expanded", "true");
await expect(page.getByRole("checkbox", { name: "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();
run.deckVersion = 3;
if (run.deck) run.deck.title = "閉じている間に更新された調査";
await expect(page.getByRole("heading", { level: 1 })).toHaveText("閉じている間に更新された調査", {
timeout: 15000,
});
await showResearch(page);
await expect(page.getByLabel("Message", { exact: true })).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 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 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();
});
test('rejects a different tailnet owner at the SSE endpoint', async ({
request,
}) => {
const response = await request.get('/api/research/events', {
headers: { 'Tailscale-User-Login': '[email protected]' },
})
expect(response.status()).toBe(403)
})
test("rejects a different tailnet owner at the SSE endpoint", async ({ request }) => {
const response = await request.get("/api/research/events", {
headers: { "Tailscale-User-Login": "[email protected]" },
});
expect(response.status()).toBe(403);
});
test('renders Markdown and reveals an unloaded citation in an offscreen deck column', async ({
test("renders Markdown and reveals an unloaded citation in an offscreen deck column", async ({
page,
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',
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 accounts.selectOption({ label: 'e2e' })
const connectionId = await accounts.inputValue()
await page.keyboard.press('Escape')
await closeMobileSidebar(page)
});
await accounts.selectOption({ label: "e2e" });
const connectionId = await accounts.inputValue();
await page.keyboard.press("Escape");
await closeMobileSidebar(page);
const source: DeckColumn = {
id: 'citation-column-2',
title: '引用の観点',
id: "citation-column-2",
title: "引用の観点",
connectionId,
source: {
platform: 'twitter',
kind: 'search',
query: 'citation-query-2',
product: 'Latest',
platform: "twitter",
kind: "search",
query: "citation-query-2",
product: "Latest",
following: false,
},
}
const columns: DeckColumn[] = ['概要', '比較'].map((title, index) => ({
};
const columns: DeckColumn[] = ["概要", "比較"].map((title, index) => ({
id: `citation-column-${index}`,
title,
connectionId,
source: {
platform: 'twitter',
kind: 'search',
platform: "twitter",
kind: "search",
query: `citation-query-${index}`,
product: 'Latest',
product: "Latest",
following: false,
},
}))
columns.push(source)
}));
columns.push(source);
const run: ResearchRun = {
id: '973ae084-18ed-4674-8899-c75851e51d1b',
topic: '引用を確認',
status: 'complete',
id: "973ae084-18ed-4674-8899-c75851e51d1b",
topic: "引用を確認",
status: "complete",
startedAt: Date.now(),
deckVersion: 1,
message: '',
deck: { id: 'citation-deck', title: '引用付きの調査', columns },
message: "",
deck: { id: "citation-deck", title: "引用付きの調査", columns },
messages: [
{
id: 'answer',
role: 'assistant',
text: '## 調査結果\n\n**重要な発見**です。\n\n- [根拠の投稿](https://x.com/author/status/123456789?s=20)\n\n| 観点 | 結果 |\n| --- | --- |\n| 投稿 | 確認済み |\n\n`コード`も表示します。',
id: "answer",
role: "assistant",
text: "## 調査結果\n\n**重要な発見**です。\n\n- [根拠の投稿](https://x.com/author/status/123456789?s=20)\n\n| 観点 | 結果 |\n| --- | --- |\n| 投稿 | 確認済み |\n\n`コード`も表示します。",
},
],
citations: [
{
column: source,
post: {
key: 'twitter:123456789',
nativeId: '123456789',
platform: 'twitter',
url: 'https://twitter.com/author/status/123456789',
text: 'AIが調査時に取得した引用本文',
author: { name: '引用元の著者', handle: 'author' },
key: "twitter:123456789",
nativeId: "123456789",
platform: "twitter",
url: "https://twitter.com/author/status/123456789",
text: "AIが調査時に取得した引用本文",
author: { name: "引用元の著者", handle: "author" },
},
},
],
}
await page.route('**/api/research/events', (route) =>
};
await page.route("**/api/research/events*", (route) =>
route.fulfill({
contentType: 'text/event-stream',
contentType: "text/event-stream",
body: `data: ${JSON.stringify({ configured: true, run })}\n\n`,
}),
)
await page.reload()
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()
);
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(
'.research-post[data-post-url="https://twitter.com/author/status/123456789"]',
)
await expect(card).toBeVisible()
await expect(card).toHaveClass(/citation-highlight/)
await expect(card).toBeFocused()
await expect(card).toContainText('Post saved during research')
await expect(card).toContainText('AIが調査時に取得した引用本文')
);
await expect(card).toBeVisible();
await expect(card).toHaveClass(/citation-highlight/);
await expect(card).toBeFocused();
await expect(card).toContainText("Post saved during research");
await expect(card).toContainText("AIが調査時に取得した引用本文");
expect(
await page.locator('.deck-board').evaluate((element) => element.scrollLeft),
).toBeGreaterThan(0)
expect(page.context().pages()).toHaveLength(1)
expect((await a11y().analyze()).violations).toEqual([])
})
await page.locator(".deck-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());
bookmarked.searchParams.set("run", run.id);
bookmarked.searchParams.set("deck", "citation-deck");
const submitted: string[] = [];
page.on("request", (request) => {
if (request.method() === "POST") submitted.push(request.url());
});
await page.goto(bookmarked.toString());
await expect(card).toBeVisible();
await expect(card).toHaveClass(/citation-highlight/);
await expect(page).toHaveURL(/citation=twitter%3A123456789/);
await expect(page.locator(".deck-column")).toHaveCount(3);
expect(submitted).toEqual([]);
expect((await a11y().analyze()).violations).toEqual([]);
});
test('explains unconfigured Codex and prevents starting research without affecting the deck', async ({
test("explains unconfigured Codex and prevents starting research without affecting the deck", async ({
page,
a11y,
}) => {
await page.goto('/deck')
const heading = page.getByRole('heading', { level: 1 })
await expect(heading).toBeVisible()
const originalTitle = await heading.textContent()
await showResearch(page)
const panel = page.getByRole('complementary', { name: 'Research chat' })
await expect(panel).toBeVisible()
await page.goto("/deck");
const heading = page.getByRole("heading", { level: 1 });
await expect(heading).toBeVisible();
const originalTitle = await heading.textContent();
await showResearch(page);
const panel = page.getByRole("complementary", { name: "Research chat" });
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()
const accessibility = await a11y().analyze()
expect(accessibility.violations).toEqual([])
await closeMobileSidebar(page)
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 expect(heading).toHaveText('手動の調査')
await showResearch(page)
await expect(panel).toBeVisible()
})
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();
const accessibility = await a11y().analyze();
expect(accessibility.violations).toEqual([]);
await closeMobileSidebar(page);
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 expect(heading).toHaveText("手動の調査");
await showResearch(page);
await expect(panel).toBeVisible();
});
test("keeps browser conversation selection independent and restores it on reload", async ({
page,
browser,
context,
}) => {
const { openDatabase } = await import("../../src/features/storage/database.server");
const { createResearchStore } = await import("../../src/features/research/repository.server");
const db = openDatabase(process.env.TWITTER_LITE_E2E_DB_PATH ?? "");
const store = createResearchStore(db);
const first = crypto.randomUUID();
const second = crypto.randomUUID();
for (const [id, text] of [
[first, "Browser one conversation"],
[second, "Browser two conversation"],
] as const) {
store.save({
id,
topic: text,
status: "complete",
startedAt: Date.now(),
deckVersion: 0,
message: text,
messages: [{ id: `${id}-message`, role: "assistant", text }],
citations: [],
});
}
const other = await browser.newContext({
storageState: await context.storageState(),
extraHTTPHeaders: { "Tailscale-User-Login": "[email protected]" },
});
try {
const secondPage = await other.newPage();
await page.goto(`/deck?run=${first}`, { waitUntil: "domcontentloaded" });
await showResearch(page);
await secondPage.goto(`/deck?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 expect(secondPage).toHaveURL(/run=new/);
await page.reload({ waitUntil: "domcontentloaded" });
await showResearch(page);
await expect(page).toHaveURL(new RegExp(`run=${first}`));
await expect(page.getByRole("log", { name: "Research conversation" })).toContainText(
"Browser one conversation",
);
} finally {
await other.close();
db.$client.prepare("DELETE FROM research_sessions WHERE id IN (?, ?)").run(first, second);
db.$client.close();
}
});
+110 -141
View File
@@ -1,158 +1,127 @@
import { expect, test } from '../fixtures'
import { expect, test } from "../fixtures";
const reviewSubject = 'Schedule next week’s design review'
const coffeeSubject = 'Catch up over coffee'
const reviewSubject = "Schedule next week’s design review";
const coffeeSubject = "Catch up over coffee";
test('reviews an edited reply and keeps the simulated result in history', async ({
test("reviews an edited reply and keeps the simulated result in history", async ({
page,
a11y,
}) => {
await page.goto('/support')
await page.waitForLoadState('networkidle')
const rows = page.locator('.support-row')
await expect(rows).toHaveCount(3)
await rows.filter({ hasText: reviewSubject }).click()
await page.getByRole('button', { name: 'Polite', exact: true }).click()
const reply = page.getByRole('textbox', { name: 'Reply', exact: true })
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)
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 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 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 expect(reply).toHaveValue(new RegExp(edited))
await page.getByRole('button', { name: 'Review reply' }).click()
await dialog.getByRole('button', { name: 'Try sending' }).click()
await expect(dialog).toHaveCount(0)
await expect(rows.filter({ hasText: reviewSubject })).toHaveCount(0)
await page.getByRole('button', { name: 'History', exact: true }).click()
await rows.filter({ hasText: reviewSubject }).click()
await expect(page.locator('.support-outcome')).toContainText(edited)
await expect(page.locator('.support-outcome')).toContainText(slot)
})
await page.goto("/support");
await page.waitForLoadState("networkidle");
const rows = page.locator(".support-row");
await expect(rows).toHaveCount(3);
await rows.filter({ hasText: reviewSubject }).click();
await page.getByRole("button", { name: "Polite", exact: true }).click();
const reply = page.getByRole("textbox", { name: "Reply", exact: true });
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);
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 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 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 expect(reply).toHaveValue(new RegExp(edited));
await page.getByRole("button", { name: "Review reply" }).click();
await dialog.getByRole("button", { name: "Try sending" }).click();
await expect(dialog).toHaveCount(0);
await expect(rows.filter({ hasText: reviewSubject })).toHaveCount(0);
await page.getByRole("button", { name: "History", exact: true }).click();
await rows.filter({ hasText: reviewSubject }).click();
await expect(page.locator(".support-outcome")).toContainText(edited);
await expect(page.locator(".support-outcome")).toContainText(slot);
});
test('defers a conversation and restores it to the current queue', async ({
page,
}) => {
await page.goto('/support')
await page.waitForLoadState('networkidle')
const rows = page.locator('.support-row')
await rows.filter({ hasText: coffeeSubject }).click()
test("defers a conversation and restores it to the current queue", async ({ page }) => {
await page.goto("/support");
await page.waitForLoadState("networkidle");
const rows = page.locator(".support-row");
await rows.filter({ hasText: coffeeSubject }).click();
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: 'Later', 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 Now' }).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: 'Now', exact: true }),
).toHaveAttribute('aria-pressed', 'true')
})
.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: "Later", 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 Now" }).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: "Now", exact: true })).toHaveAttribute(
"aria-pressed",
"true",
);
});
test('keeps corrections as new records and summarizes only selected current facts', async ({
test("keeps corrections as new records and summarizes only selected current facts", async ({
page,
a11y,
}) => {
await page.goto('/journal')
await page.waitForLoadState('networkidle')
await expect(
page.getByRole('button', { name: 'Save entry', exact: true }),
).toBeDisabled()
await page.goto("/journal");
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('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(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 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 expect(summary).not.toHaveValue(/次はレビュー資料/)
.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(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 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 expect(summary).not.toHaveValue(/次はレビュー資料/);
await expect(summary).toHaveValue(
/Medication and weight are unknown for periods without entries/,
)
expect((await a11y().analyze()).violations).toEqual([])
})
);
expect((await a11y().analyze()).violations).toEqual([]);
});
test('fits communication and journal interactions in narrow screens', async ({
page,
a11y,
}) => {
test("fits communication and journal interactions in narrow screens", async ({ page, a11y }) => {
for (const width of [320, 375, 768]) {
await page.setViewportSize({ width, height: 850 })
await page.goto('/support')
await page.waitForLoadState('networkidle')
expect(
await page.evaluate(() => document.documentElement.scrollWidth),
).toBe(width)
await page.locator('.support-row').first().click()
await expect(
page.getByRole('textbox', { name: 'Reply', exact: true }),
).toBeVisible()
expect(
await page.evaluate(() => document.documentElement.scrollWidth),
).toBe(width)
if (width === 320) expect((await a11y().analyze()).violations).toEqual([])
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()
expect(
await page.evaluate(() => document.documentElement.scrollWidth),
).toBe(width)
if (width === 320) expect((await a11y().analyze()).violations).toEqual([])
await page.setViewportSize({ width, height: 850 });
await page.goto("/support");
await page.waitForLoadState("networkidle");
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBe(width);
await page.locator(".support-row").first().click();
await expect(page.getByRole("textbox", { name: "Reply", exact: true })).toBeVisible();
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBe(width);
if (width === 320) expect((await a11y().analyze()).violations).toEqual([]);
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();
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBe(width);
if (width === 320) expect((await a11y().analyze()).violations).toEqual([]);
}
})
});
+106
View File
@@ -0,0 +1,106 @@
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(".inbox-article-row");
const heading = await rows.nth(1).locator(".inbox-row-title").innerText();
await rows.nth(1).click();
await expect(page.locator(".inbox-article-title")).toHaveText(heading);
const selectedUrl = page.url();
expect(new URL(selectedUrl).searchParams.get("article")).toBeTruthy();
await page.reload({ waitUntil: "networkidle" });
await expect(page.locator(".inbox-article-title")).toHaveText(heading ?? "");
await page.goBack();
await expect(page.locator(".inbox-workspace")).toHaveAttribute("data-reading", "false");
await page.goForward();
await expect(page).toHaveURL(selectedUrl);
await expect(page.locator(".inbox-article-title")).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" });
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(".inbox-article-row").count();
await page.reload({ waitUntil: "networkidle" });
await expect(search).toHaveValue("browser");
await expect(page.locator(".inbox-article-row")).toHaveCount(count);
await page.goBack();
await expect(page).toHaveURL("http://127.0.0.1:4173/");
});
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();
await expect.poll(() => new URL(page.url()).searchParams.get("accounts")).toBe("true");
await page.reload({ waitUntil: "networkidle" });
await expect(page.getByRole("dialog")).toBeVisible();
await page.goBack();
await expect(page.getByRole("dialog")).toHaveCount(0);
await page.goForward();
await expect(page.getByRole("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(".support-row").nth(1);
const heading = await row.locator("span").first().innerText();
await row.click();
await expect(page.locator(".support-toolbar > span").first()).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(".support-toolbar > span").first()).toHaveText(heading ?? "");
await expect(page.locator(".support-workspace")).toHaveAttribute("data-reading", "true");
});
test("keeps a deep link through login", async ({ page, context }) => {
await context.clearCookies();
await page.goto("/inbox?q=browser", { waitUntil: "networkidle" });
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 expect(page).toHaveURL(/\/inbox\?q=browser$/);
await expect(page.getByRole("textbox", { name: "Search articles" })).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();
await expect(page).toHaveURL(/view=all/);
await page.clock.install();
const input = page.getByRole("textbox", { name: "Search messages" });
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 page.clock.runFor(400);
await expect(input).toHaveValue("");
expect(new URL(page.url()).searchParams.has("q")).toBe(false);
await input.fill("final search");
await page.clock.runFor(300);
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" });
await page.clock.install();
const input = page.getByRole("textbox", { name: "Search articles" });
await input.dispatchEvent("compositionstart");
await input.fill("browser");
await page.clock.runFor(1000);
await expect(input).toHaveValue("browser");
expect(new URL(page.url()).searchParams.has("q")).toBe(false);
await input.dispatchEvent("compositionend", { data: "browser" });
await page.clock.runFor(300);
await expect.poll(() => new URL(page.url()).searchParams.get("q")).toBe("browser");
});
+110 -101
View File
@@ -1,115 +1,124 @@
import { expect, test } from '../fixtures'
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('.vital-system-card')
await expect(systems).toHaveCount(8)
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(".vital-system-card");
await expect(systems).toHaveCount(8);
await page
.locator('.vital-body-page > .vital-tabs')
.getByRole('button', { name: 'Cardiovascular', exact: true })
.click()
await expect(systems).toHaveCount(1)
await systems.first().click()
const dialog = page.getByRole('dialog', { name: 'Cardiovascular' })
await expect(dialog).toContainText('68')
await dialog
.getByRole('button', { name: 'Close', exact: true })
.last()
.click()
await page.getByRole('button', { name: 'Lab history', exact: true }).click()
await expect(page.locator('.vital-history')).toContainText('Jul 28')
await expect(page.locator('.vital-history')).toContainText('93')
await page.getByRole('button', { name: 'Systems', exact: true }).click()
.getByRole("group", { name: "Body systems", exact: true })
.getByRole("button", { name: "Cardiovascular", exact: true })
.click();
await expect(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(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(".vital-history")).toContainText("Jul 28");
await expect(page.locator(".vital-history")).toContainText("93");
await page.reload();
await page.waitForLoadState("networkidle");
await expect(page.locator(".vital-history")).toContainText("Jul 28");
await page.getByRole("button", { name: "Systems", exact: true }).click();
await page
.locator('.vital-body-page > .vital-tabs')
.getByRole('button', { name: 'Overview', exact: true })
.click()
await expect(systems).toHaveCount(8)
expect((await a11y().analyze()).violations).toEqual([])
})
.getByRole("group", { name: "Body systems", exact: true })
.getByRole("button", { name: "Overview", exact: true })
.click();
await expect(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()
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()
.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()
.getByRole("group", { name: "Trend metric" })
.getByRole("button", { name: "CO₂", exact: true })
.click();
await expect(
page.getByRole('img', { name: '7D CO₂ simulated environmental readings' }),
).toBeVisible()
await page
.locator('.vital-metric')
.filter({ hasText: 'Room temperature' })
.click()
const dialog = page.getByRole('dialog', { name: 'Room temperature' })
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()
page.getByRole("img", { name: "7D CO₂ simulated environmental readings" }),
).toBeVisible();
await page.reload();
await page.waitForLoadState("networkidle");
await expect(
page.getByRole('status').filter({ hasText: 'Sample readings refreshed' }),
).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()
page.getByRole("img", { name: "7D CO₂ simulated environmental readings" }),
).toBeVisible();
await page.locator(".vital-metric").filter({ hasText: "Room temperature" }).click();
const dialog = page.getByRole("dialog", { name: "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('button', { name: 'Cool', exact: true }),
).toHaveAttribute('aria-pressed', 'true')
const temperature = page.getByRole('slider', { name: 'Target temperature' })
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')
})
page.getByRole("status").filter({ hasText: "Sample readings refreshed" }),
).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 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 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",
);
});
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 }, testInfo) => {
await page.goto("/vitals");
await page.waitForLoadState("networkidle");
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()
expect(
await page.evaluate(() => document.documentElement.scrollWidth),
).toBe(width)
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();
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()
});
if (await menu.isVisible()) await menu.click();
await navigation.getByRole("button", { name: "Body", exact: true }).click();
}
})
});
+162 -211
View File
@@ -1,30 +1,28 @@
import type { Page } from '@playwright/test'
import Database from 'better-sqlite3'
import { expect, test } from '../fixtures'
import type { Page } from "@playwright/test";
import Database from "better-sqlite3";
import { expect, test } from "../fixtures";
type NativeTesting = {
listTools(): { name: string }[]
executeTool(name: string, input: string): Promise<string | null>
}
listTools(): { name: string }[];
executeTool(name: string, input: string): Promise<string | null>;
};
type ToolResult = {
content: { type: string; text: string }[]
isError?: boolean
}
content: { type: string; text: string }[];
isError?: boolean;
};
test.use({
launchOptions: {
executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE,
args: ['--enable-blink-features=WebMCP,WebMCPTesting'],
args: ["--enable-blink-features=WebMCP,WebMCPTesting"],
},
})
});
async function toolNames(page: Page) {
return page.evaluate(() =>
(
navigator as Navigator & { modelContextTesting: NativeTesting }
).modelContextTesting
(navigator as Navigator & { modelContextTesting: NativeTesting }).modelContextTesting
.listTools()
.map((tool) => tool.name)
.sort(),
)
);
}
async function executeTool(
page: Page,
@@ -37,296 +35,249 @@ async function executeTool(
navigator as Navigator & { modelContextTesting: NativeTesting }
).modelContextTesting.executeTool(name, JSON.stringify(input)),
{ name, input },
)
expect(result).not.toBeNull()
return JSON.parse(result ?? '{}')
);
expect(result).not.toBeNull();
return JSON.parse(result ?? "{}");
}
function decoded(result: ToolResult) {
return JSON.parse(result.content[0]?.text ?? '{}')
return JSON.parse(result.content[0]?.text ?? "{}");
}
const column = (title: string, connectionId: string) => ({
title,
connectionId,
source: { kind: 'search', query: 'WebMCP' },
})
source: { kind: "search", query: "WebMCP" },
});
function savedCount() {
const db = new Database(process.env.TWITTER_LITE_E2E_DB_PATH ?? '', {
const db = new Database(process.env.TWITTER_LITE_E2E_DB_PATH ?? "", {
readonly: true,
})
});
try {
return db.prepare('SELECT count(*) AS count FROM decks').get()
return db.prepare("SELECT count(*) AS count FROM decks").get();
} finally {
db.close()
db.close();
}
}
let accounts: Record<string, string>
let pageErrors: string[]
let accounts: Record<string, string>;
let pageErrors: string[];
test.beforeEach(async ({ page }) => {
pageErrors = []
page.on('pageerror', (error) => pageErrors.push(error.message))
await page.goto('/deck')
pageErrors = [];
page.on("pageerror", (error) => pageErrors.push(error.message));
await page.goto("/deck");
await expect
.poll(() => toolNames(page))
.toEqual([
'delete_deck',
'get_column_posts',
'get_deck',
'list_connections',
'list_decks',
'load_more_column',
'save_deck',
'select_deck',
'set_deck',
])
"delete_deck",
"get_column_posts",
"get_deck",
"list_connections",
"list_decks",
"load_more_column",
"save_deck",
"select_deck",
"set_deck",
]);
await expect
.poll(
async () =>
decoded(await executeTool(page, 'list_connections')).connections
?.length,
)
.toBe(2)
.poll(async () => decoded(await executeTool(page, "list_connections")).connections?.length)
.toBe(2);
accounts = Object.fromEntries(
decoded(await executeTool(page, 'list_connections')).connections.map(
(connection: { displayName: string; id: string }) => [
connection.displayName,
connection.id,
],
decoded(await executeTool(page, "list_connections")).connections.map(
(connection: { displayName: string; id: string }) => [connection.displayName, connection.id],
),
)
})
test.afterEach(() => expect(pageErrors).toEqual([]))
);
});
test.afterEach(() => expect(pageErrors).toEqual([]));
test('creates temporary research, edits it, persists explicitly and reopens it on another device', async ({
test("creates temporary research, edits it, persists explicitly and reopens it on another device", async ({
page,
browser,
baseURL,
}) => {
const created = await executeTool(page, 'set_deck', {
title: 'WebMCPの反応',
const created = await executeTool(page, "set_deck", {
title: "WebMCPの反応",
columns: [
{ ...column('日本語', accounts.e2e ?? ''), id: 'new' },
column('別の接続', accounts.alternate ?? ''),
{ ...column("日本語", accounts.e2e ?? ""), id: "new" },
column("別の接続", accounts.alternate ?? ""),
],
})
expect(created.isError).not.toBe(true)
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()
});
expect(created.isError).not.toBe(true);
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()
const replaced = await executeTool(page, 'set_deck', {
.getByRole("region", { name: "日本語", exact: true })
.getByRole("button", { name: "Actions for 日本語" })
.click();
await page.getByRole("menuitem", { name: "Edit", exact: true }).click();
const replaced = await executeTool(page, "set_deck", {
deckId: deck.id,
title: '更新した調査',
title: "更新した調査",
columns: [deck.columns[1]],
})
expect(replaced.isError).not.toBe(true)
await expect(page.getByRole('dialog')).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)
expect(savedCount()).toEqual({ count: 1 })
});
expect(replaced.isError).not.toBe(true);
await expect(page.getByRole("dialog")).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);
expect(savedCount()).toEqual({ count: 1 });
const other = await browser.newContext({
storageState: await page.context().storageState(),
baseURL,
extraHTTPHeaders: { 'Tailscale-User-Login': '[email protected]' },
})
extraHTTPHeaders: { "Tailscale-User-Login": "[email protected]" },
});
try {
const device = await other.newPage()
await device.goto('/deck')
await expect(device.getByRole('heading', { level: 1 })).toHaveText(
'更新した調査',
)
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()
device.getByText("alternate · Latest · all page 1", { exact: true }),
).toBeVisible();
} finally {
await other.close()
await other.close();
}
await page.reload()
await expect(page.getByRole('heading', { level: 1 })).toHaveText(
'更新した調査',
)
})
await page.reload();
await expect(page.getByRole("heading", { level: 1 })).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 }) => {
const deck = decoded(
await executeTool(page, 'set_deck', { title: '保存済み', columns: [] }),
).deck
await executeTool(page, 'save_deck', { deckId: deck.id })
await executeTool(page, "set_deck", { title: "保存済み", columns: [] }),
).deck;
await executeTool(page, "save_deck", { deckId: deck.id });
expect(
(
await executeTool(page, 'set_deck', {
await executeTool(page, "set_deck", {
deckId: deck.id,
title: '変更',
title: "変更",
columns: [],
})
).isError,
).toBe(true)
).toBe(true);
expect(
(
await executeTool(page, 'set_deck', {
await executeTool(page, "set_deck", {
deckId: deck.id,
expectedRevision: 1,
title: '変更',
title: "変更",
columns: [],
})
).isError,
).not.toBe(true)
).not.toBe(true);
expect(
(
await executeTool(page, 'set_deck', {
await executeTool(page, "set_deck", {
deckId: deck.id,
expectedRevision: 1,
title: '古い変更',
title: "古い変更",
columns: [],
})
).isError,
).toBe(true)
await expect(page.getByRole('heading', { level: 1 })).toHaveText('変更')
).toBe(true);
await expect(page.getByRole("heading", { level: 1 })).toHaveText("変更");
expect(
(
await executeTool(page, 'delete_deck', {
await executeTool(page, "delete_deck", {
deckId: deck.id,
expectedRevision: 1,
})
).isError,
).toBe(true)
).toBe(true);
expect(
(
await executeTool(page, 'delete_deck', {
await executeTool(page, "delete_deck", {
deckId: deck.id,
expectedRevision: 2,
})
).isError,
).not.toBe(true)
expect(savedCount()).toEqual({ count: 0 })
})
).not.toBe(true);
expect(savedCount()).toEqual({ count: 0 });
});
test('keeps temporary views independent and loses only unsaved views on reload', async ({
test("keeps temporary views independent and loses only unsaved views on reload", async ({
page,
}) => {
const first = decoded(
await executeTool(page, 'set_deck', { title: '一時の調査A', columns: [] }),
).deck
await executeTool(page, "set_deck", { title: "一時の調査A", columns: [] }),
).deck;
const second = decoded(
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 executeTool(page, 'delete_deck', { deckId: second.id })
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 executeTool(page, "delete_deck", { deckId: second.id });
expect(
decoded(await executeTool(page, 'list_decks')).decks.some(
decoded(await executeTool(page, "list_decks")).decks.some(
(deck: { id: string }) => deck.id === second.id,
),
).toBe(false)
expect(savedCount()).toEqual({ count: 0 })
await page.reload()
await expect(page.getByRole('heading', { level: 1 })).toHaveText(
'New research',
)
})
).toBe(false);
expect(savedCount()).toEqual({ count: 0 });
await page.reload();
await expect(page.getByRole("heading", { level: 1 })).toHaveText("New research");
});
test('rejects invalid input without changing the active view', async ({
page,
}) => {
await executeTool(page, 'set_deck', {
title: '残す調査',
columns: [column('残す観点', accounts.e2e ?? '')],
})
const duplicate = { ...column('不正', accounts.e2e ?? ''), id: 'duplicate' }
test("rejects invalid input without changing the active view", async ({ page }) => {
await executeTool(page, "set_deck", {
title: "残す調査",
columns: [column("残す観点", accounts.e2e ?? "")],
});
const duplicate = { ...column("不正", accounts.e2e ?? ""), id: "duplicate" };
for (const columns of [
[duplicate, duplicate],
[column('不明', 'missing')],
[column("不明", "missing")],
[
{
...column('不正', accounts.e2e ?? ''),
source: { kind: 'list', target: 'invalid' },
...column("不正", accounts.e2e ?? ""),
source: { kind: "list", target: "invalid" },
},
],
]) {
expect(
(await executeTool(page, 'set_deck', { title: '変更しない', columns }))
.isError,
).toBe(true)
await expect(page.locator('.deck-column h2')).toHaveText(['残す観点'])
expect((await executeTool(page, "set_deck", { title: "変更しない", columns })).isError).toBe(
true,
);
await expect(page.locator(".deck-column h2")).toHaveText(["残す観点"]);
}
expect(savedCount()).toEqual({ count: 0 })
})
expect(savedCount()).toEqual({ count: 0 });
});
test('keeps failed temporary saves visible and allows retry without duplicate decks', async ({
test("keeps failed temporary saves visible and allows retry without duplicate decks", async ({
page,
}) => {
const deck = decoded(
await executeTool(page, 'set_deck', { title: '未保存の調査', columns: [] }),
).deck
await page.route('**/_serverFn/**', (route) =>
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(
'未保存の調査',
)
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 })
})
await executeTool(page, "set_deck", { title: "未保存の調査", columns: [] }),
).deck;
await page.route("**/_serverFn/**", (route) =>
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("未保存の調査");
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 }) => {
const deck = decoded(
await executeTool(page, 'set_deck', {
title: '接続を比較',
columns: [
column('通常', accounts.e2e ?? ''),
column('別の接続', accounts.alternate ?? ''),
],
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()
const id = deck.columns[1].id
const loaded = decoded(
await executeTool(page, 'get_column_posts', { columnId: id }),
)
expect(loaded.column.connectionId).toBe(accounts.alternate)
).deck;
await expect(page.getByText("alternate · Latest · all page 1", { exact: true })).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);
expect(loaded.posts.map((post: { text: string }) => post.text)).toEqual([
'alternate · Latest · all page 1',
])
const more = decoded(
await executeTool(page, 'load_more_column', { columnId: id }),
)
expect(more).toMatchObject({ loadedCount: 2, offset: 1, hasMore: false })
"alternate · Latest · all page 1",
]);
const more = decoded(await executeTool(page, "load_more_column", { columnId: id }));
expect(more).toMatchObject({ loadedCount: 2, offset: 1, hasMore: false });
expect(more.posts.map((post: { text: string }) => post.text)).toEqual([
'alternate · Latest · all page 2',
])
"alternate · Latest · all page 2",
]);
await expect(
page.getByRole('region', { name: '通常', exact: true }).locator('article'),
).toHaveCount(1)
await executeTool(page, 'set_deck', { title: '別の一時ビュー', columns: [] })
expect(
(await executeTool(page, 'get_column_posts', { columnId: id })).isError,
).toBe(true)
})
page.getByRole("region", { name: "通常", exact: true }).locator("article"),
).toHaveCount(1);
await executeTool(page, "set_deck", { title: "別の一時ビュー", columns: [] });
expect((await executeTool(page, "get_column_posts", { columnId: id })).isError).toBe(true);
});
+26 -32
View File
@@ -1,41 +1,35 @@
import { expect, test } from '../fixtures'
import { closeMobileSidebar, showResearch } from '../workspace'
import { expect, test } from "../fixtures";
import { closeMobileSidebar, showResearch } from "../workspace";
test('keeps the sidebar, composer and deck reachable at compact widths', async ({
test("keeps the sidebar, composer and deck reachable at compact widths", async ({
page,
a11y,
}, testInfo) => {
await page.goto('/deck')
await page.goto("/deck");
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 expect(chat).toBeVisible()
const composer = chat.getByRole('textbox', {
name: 'Message',
await page.setViewportSize({ width, height: 850 });
await showResearch(page);
const chat = page.getByRole("complementary", { name: "Research chat" });
await expect(chat).toBeVisible();
const composer = chat.getByRole("textbox", {
name: "Message",
exact: true,
})
await expect(composer).toBeVisible()
const bounds = await composer.boundingBox()
if (!bounds) throw new Error('Composer must have a visible bounding box')
expect(bounds.x).toBeGreaterThanOrEqual(0)
expect(bounds.x + bounds.width).toBeLessThanOrEqual(width)
expect(bounds.y + bounds.height).toBeLessThanOrEqual(850)
expect(
await page.evaluate(() => document.documentElement.scrollWidth),
).toBe(width)
});
await expect(composer).toBeVisible();
const bounds = await composer.boundingBox();
if (!bounds) throw new Error("Composer must have a visible bounding box");
expect(bounds.x).toBeGreaterThanOrEqual(0);
expect(bounds.x + bounds.width).toBeLessThanOrEqual(width);
expect(bounds.y + bounds.height).toBeLessThanOrEqual(850);
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBe(width);
await page.screenshot({
path: testInfo.outputPath(`workspace-${width}.png`),
})
if (width === 320) expect((await a11y().analyze()).violations).toEqual([])
await closeMobileSidebar(page)
});
if (width === 320) expect((await a11y().analyze()).violations).toEqual([]);
await closeMobileSidebar(page);
}
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 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();
});