feat: add URL-driven workspace state and streamed Codex research
This commit is contained in:
@@ -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
@@ -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(?:\?|$)/);
|
||||
});
|
||||
|
||||
@@ -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
@@ -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
@@ -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ページ目から");
|
||||
});
|
||||
|
||||
@@ -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
@@ -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
@@ -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([]);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user