mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-23 09:42:53 +00:00
test(app): consolidate browser setup and rebalance CI shards
This commit is contained in:
@@ -405,9 +405,9 @@ jobs:
|
||||
run: npm run test:e2e --workspace=@getpaseo/app -- --shard=${{ env.PLAYWRIGHT_SHARD }}
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
# Weight file groups by observed duration; raw test counts overload
|
||||
# early agent/Changes specs. Required check names remain unchanged.
|
||||
PWTEST_SHARD_WEIGHTS: "111:172:137:160"
|
||||
# Balance contiguous file groups using CI durations and the trimmed
|
||||
# inventory. Required check names and worker counts stay unchanged.
|
||||
PWTEST_SHARD_WEIGHTS: "183:185:181:202"
|
||||
|
||||
- name: Upload test artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -460,7 +460,7 @@ async function expectHiddenStreamingSubmissionOrderAfterWorkspaceEviction(
|
||||
const target = await seedMockAgentWorkspace({
|
||||
repoPrefix: `submission-hidden-stream-${testInfo.workerIndex}-`,
|
||||
title: "Hidden streaming submission",
|
||||
model: "ten-second-stream",
|
||||
model: "e2e-fast-stream",
|
||||
});
|
||||
const evictionAgents = await Promise.all(
|
||||
Array.from({ length: WORKSPACE_DECK_MAX_MOUNTED_WORKSPACES }, (_unused, index) =>
|
||||
@@ -549,7 +549,7 @@ async function expectCompletedSubmissionClearsAfterMissedRunningTransition(
|
||||
const agent = await seedMockAgentWorkspace({
|
||||
repoPrefix: `submission-missed-running-${testInfo.workerIndex}-`,
|
||||
title: "Submission missed running transition",
|
||||
model: "ten-second-stream",
|
||||
model: "e2e-fast-stream",
|
||||
});
|
||||
try {
|
||||
await openAgentRoute(page, { workspaceId: agent.workspaceId, agentId: agent.agentId });
|
||||
@@ -700,7 +700,7 @@ async function expectCanonicalOrderWinsAcrossOverlappingClients(
|
||||
const agent = await seedMockAgentWorkspace({
|
||||
repoPrefix: `submission-cross-client-order-${testInfo.workerIndex}-`,
|
||||
title: "Cross-client submission order",
|
||||
model: "ten-second-stream",
|
||||
model: "e2e-fast-stream",
|
||||
});
|
||||
const localPrompt = "Send this after the other client turn.";
|
||||
const remotePrompt = "Commit this other client turn first.";
|
||||
|
||||
@@ -36,15 +36,25 @@ test.describe("Host agent skills", () => {
|
||||
page,
|
||||
skills,
|
||||
}) => {
|
||||
await page.setViewportSize({ width: 1280, height: 900 });
|
||||
const custom = skills.available.slice(0, 2);
|
||||
const cancelledAddition = skills.available[2]!;
|
||||
await openAgentSkillsSettings(page, skills);
|
||||
|
||||
await openSkillSelection(page);
|
||||
await test.step("the desktop sheet gives the bundled label its intended inset", async () => {
|
||||
await expectBundledSkillsInset(page);
|
||||
});
|
||||
await chooseCustomSkills(page, custom);
|
||||
await saveSkillSelection(page);
|
||||
await expectInstalledSkills(skills, custom);
|
||||
await expectSavedSelection(skills, { mode: "custom", skills: custom });
|
||||
await test.step("installation ignores agent config environment overrides", async () => {
|
||||
await expect(access(`${skills.home}/../ignored-claude-config/skills`)).rejects.toThrow(
|
||||
"ENOENT",
|
||||
);
|
||||
await expect(access(`${skills.home}/../ignored-codex-home/skills`)).rejects.toThrow("ENOENT");
|
||||
});
|
||||
|
||||
await openSkillSelection(page);
|
||||
await toggleSkill(page, cancelledAddition);
|
||||
@@ -150,23 +160,6 @@ test.describe("Host agent skills", () => {
|
||||
await expectInstalledSkills(skills, secondSelection.skills);
|
||||
await expectSavedSelection(skills, secondSelection);
|
||||
});
|
||||
|
||||
test("ignores agent config environment overrides", async ({ skills }) => {
|
||||
await skills.install({ mode: "custom", skills: [skills.available[0]!] });
|
||||
|
||||
await expect(
|
||||
access(`${skills.home}/../ignored-claude-config/skills`).then(
|
||||
() => true,
|
||||
() => false,
|
||||
),
|
||||
).resolves.toBe(false);
|
||||
await expect(
|
||||
access(`${skills.home}/../ignored-codex-home/skills`).then(
|
||||
() => true,
|
||||
() => false,
|
||||
),
|
||||
).resolves.toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
function allSkillsCard(page: Parameters<typeof openSkillSelection>[0]) {
|
||||
@@ -192,24 +185,14 @@ test.describe("Agent skills sheet inset", () => {
|
||||
await openSkillSelection(page);
|
||||
await waitForSettledPosition(page.getByTestId("skill-selection-sheet"));
|
||||
|
||||
expect({
|
||||
aboveLabel: await readVerticalGap(allSkillsCard(page), bundledSkillsLabel(page)),
|
||||
belowLabel: await readVerticalGap(bundledSkillsLabel(page), skillListCard(page)),
|
||||
}).toEqual({ aboveLabel: 16, belowLabel: 12 });
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("desktop", () => {
|
||||
test.use({ viewport: { width: 1280, height: 900 } });
|
||||
|
||||
test("keeps more space above the bundled skills label than below", async ({ page, skills }) => {
|
||||
await openAgentSkillsSettings(page, skills);
|
||||
await openSkillSelection(page);
|
||||
|
||||
expect({
|
||||
aboveLabel: await readVerticalGap(allSkillsCard(page), bundledSkillsLabel(page)),
|
||||
belowLabel: await readVerticalGap(bundledSkillsLabel(page), skillListCard(page)),
|
||||
}).toEqual({ aboveLabel: 16, belowLabel: 12 });
|
||||
await expectBundledSkillsInset(page);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function expectBundledSkillsInset(page: Parameters<typeof openSkillSelection>[0]) {
|
||||
expect({
|
||||
aboveLabel: await readVerticalGap(allSkillsCard(page), bundledSkillsLabel(page)),
|
||||
belowLabel: await readVerticalGap(bundledSkillsLabel(page), skillListCard(page)),
|
||||
}).toEqual({ aboveLabel: 16, belowLabel: 12 });
|
||||
}
|
||||
|
||||
@@ -51,7 +51,9 @@ test.describe("Archive tab reconciliation", () => {
|
||||
await tempRepo?.cleanup();
|
||||
});
|
||||
|
||||
test("non-UI archive prunes the archived tab across open pages and reload", async ({ page }) => {
|
||||
test("non-UI archive prunes open tabs and keeps the session archived when reopened", async ({
|
||||
page,
|
||||
}) => {
|
||||
const archived = await createIdleAgent(client, {
|
||||
cwd: tempRepo.path,
|
||||
workspaceId,
|
||||
@@ -65,28 +67,51 @@ test.describe("Archive tab reconciliation", () => {
|
||||
const passivePage = await page.context().newPage();
|
||||
|
||||
try {
|
||||
await primeAdditionalPage(passivePage);
|
||||
await resetSeededPageState(page);
|
||||
await resetSeededPageState(passivePage);
|
||||
await openSessions(page);
|
||||
await expectSessionRowVisible(page, archived.title);
|
||||
await expectSessionRowVisible(page, surviving.title);
|
||||
await openSessions(passivePage);
|
||||
await expectSessionRowVisible(passivePage, archived.title);
|
||||
await expectSessionRowVisible(passivePage, surviving.title);
|
||||
await openWorkspaceWithAgents(page, [archived, surviving]);
|
||||
await openWorkspaceWithAgents(passivePage, [archived, surviving]);
|
||||
await test.step("open both sessions in two pages", async () => {
|
||||
await primeAdditionalPage(passivePage);
|
||||
await resetSeededPageState(page);
|
||||
await resetSeededPageState(passivePage);
|
||||
await openSessions(page);
|
||||
await expectSessionRowVisible(page, archived.title);
|
||||
await expectSessionRowVisible(page, surviving.title);
|
||||
await openSessions(passivePage);
|
||||
await expectSessionRowVisible(passivePage, archived.title);
|
||||
await expectSessionRowVisible(passivePage, surviving.title);
|
||||
await openWorkspaceWithAgents(page, [archived, surviving]);
|
||||
await openWorkspaceWithAgents(passivePage, [archived, surviving]);
|
||||
});
|
||||
|
||||
await archiveAgentFromDaemon(client, archived.id);
|
||||
await expectWorkspaceArchiveOutcome(page, {
|
||||
archivedAgentId: archived.id,
|
||||
survivingAgentId: surviving.id,
|
||||
const archivedAt = await fetchAgentArchivedAt(client, archived.id);
|
||||
expect(archivedAt).not.toBeNull();
|
||||
|
||||
await test.step("prune the archived tab across both pages and reload", async () => {
|
||||
await expectWorkspaceArchiveOutcome(page, {
|
||||
archivedAgentId: archived.id,
|
||||
survivingAgentId: surviving.id,
|
||||
});
|
||||
await expectWorkspaceArchiveOutcome(passivePage, {
|
||||
archivedAgentId: archived.id,
|
||||
survivingAgentId: surviving.id,
|
||||
});
|
||||
await reloadWorkspace(passivePage, surviving.workspaceId);
|
||||
await expectWorkspaceTabHidden(passivePage, archived.id);
|
||||
});
|
||||
await expectWorkspaceArchiveOutcome(passivePage, {
|
||||
archivedAgentId: archived.id,
|
||||
survivingAgentId: surviving.id,
|
||||
|
||||
await test.step("reopen the archived session without unarchiving it", async () => {
|
||||
await openSessions(page);
|
||||
await expectSessionRowArchived(page, archived.title);
|
||||
|
||||
await clickSessionRow(page, archived.title);
|
||||
|
||||
await expectArchivedAgentFocused(page, archived.id);
|
||||
await expectAgentTabActive(page, archived.id);
|
||||
expect(await fetchAgentArchivedAt(client, archived.id)).toBe(archivedAt);
|
||||
|
||||
await expect(page).toHaveURL(buildHostWorkspaceRoute(getServerId(), archived.workspaceId), {
|
||||
timeout: 30_000,
|
||||
});
|
||||
});
|
||||
await reloadWorkspace(passivePage, surviving.workspaceId);
|
||||
await expectWorkspaceTabHidden(passivePage, archived.id);
|
||||
} finally {
|
||||
await passivePage.close();
|
||||
}
|
||||
@@ -123,35 +148,4 @@ test.describe("Archive tab reconciliation", () => {
|
||||
await passivePage.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("clicking an archived session navigates without unarchiving it", async ({ page }) => {
|
||||
const archived = await createIdleAgent(client, {
|
||||
cwd: tempRepo.path,
|
||||
workspaceId,
|
||||
title: `unarchive-archived-${randomUUID().slice(0, 8)}`,
|
||||
});
|
||||
const surviving = await createIdleAgent(client, {
|
||||
cwd: tempRepo.path,
|
||||
workspaceId,
|
||||
title: `unarchive-control-${randomUUID().slice(0, 8)}`,
|
||||
});
|
||||
|
||||
await resetSeededPageState(page);
|
||||
await openWorkspaceWithAgents(page, [archived, surviving]);
|
||||
await archiveAgentFromDaemon(client, archived.id);
|
||||
const archivedAt = await fetchAgentArchivedAt(client, archived.id);
|
||||
expect(archivedAt).not.toBeNull();
|
||||
await openSessions(page);
|
||||
await expectSessionRowArchived(page, archived.title);
|
||||
|
||||
await clickSessionRow(page, archived.title);
|
||||
|
||||
await expectArchivedAgentFocused(page, archived.id);
|
||||
await expectAgentTabActive(page, archived.id);
|
||||
expect(await fetchAgentArchivedAt(client, archived.id)).toBe(archivedAt);
|
||||
|
||||
await expect(page).toHaveURL(buildHostWorkspaceRoute(getServerId(), archived.workspaceId), {
|
||||
timeout: 30_000,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -90,7 +90,7 @@ test.describe("Assistant fork menu", () => {
|
||||
await expectLiveAssistantText(page, visibleAfterFork);
|
||||
});
|
||||
|
||||
test("focuses a forked assistant turn in a new workspace draft tab", async ({
|
||||
test("focuses an existing-workspace fork draft and keeps its attachment after submission", async ({
|
||||
page,
|
||||
seedForkWorkspace,
|
||||
}) => {
|
||||
@@ -101,55 +101,44 @@ test.describe("Assistant fork menu", () => {
|
||||
model: "ten-second-stream",
|
||||
});
|
||||
|
||||
await openAgentRoute(page, session);
|
||||
await expectComposerVisible(page);
|
||||
await awaitAssistantMessage(page);
|
||||
await session.client.waitForFinish(session.agentId, 45_000);
|
||||
await test.step("complete a second assistant turn in the visible source tab", async () => {
|
||||
await openAgentRoute(page, session);
|
||||
await expectComposerVisible(page);
|
||||
await awaitAssistantMessage(page);
|
||||
await session.client.waitForFinish(session.agentId, 45_000);
|
||||
|
||||
await submitMessage(page, "emit 1 coalesced agent stream updates while this tab is visible.");
|
||||
await session.client.waitForFinish(session.agentId, 45_000);
|
||||
await awaitAssistantMessage(page);
|
||||
|
||||
const agentTab = page.getByTestId(`workspace-tab-agent_${session.agentId}`);
|
||||
await expect(agentTab).toHaveAttribute("aria-selected", "true");
|
||||
|
||||
await forkMostRecentAssistantTurnToNewTab(page);
|
||||
|
||||
const selectedTab = page
|
||||
.getByTestId("workspace-tabs-row")
|
||||
.getByRole("button")
|
||||
.and(page.locator('[aria-selected="true"]'));
|
||||
await expect(selectedTab).toHaveAttribute("data-testid", /^workspace-tab-draft_/, {
|
||||
timeout: 30_000,
|
||||
});
|
||||
await expect(agentTab).toHaveAttribute("aria-selected", "false");
|
||||
await expectChatHistoryAttachment(page);
|
||||
});
|
||||
|
||||
test("keeps the fork attachment after submitting an existing-workspace draft tab", async ({
|
||||
page,
|
||||
seedForkWorkspace,
|
||||
}) => {
|
||||
const session = await seedForkWorkspace({
|
||||
repoPrefix: "assistant-fork-tab-submit-",
|
||||
title: "Assistant fork tab submit",
|
||||
initialPrompt: "emit 1 coalesced agent stream updates for assistant fork tab submit.",
|
||||
model: "ten-second-stream",
|
||||
await submitMessage(page, "emit 1 coalesced agent stream updates while this tab is visible.");
|
||||
await session.client.waitForFinish(session.agentId, 45_000);
|
||||
await awaitAssistantMessage(page);
|
||||
});
|
||||
|
||||
await openAgentRoute(page, session);
|
||||
await expectComposerVisible(page);
|
||||
await awaitAssistantMessage(page);
|
||||
await session.client.waitForFinish(session.agentId, 45_000);
|
||||
await test.step("fork the latest turn into the selected draft tab", async () => {
|
||||
const agentTab = page.getByTestId(`workspace-tab-agent_${session.agentId}`);
|
||||
await expect(agentTab).toHaveAttribute("aria-selected", "true");
|
||||
|
||||
await forkMostRecentAssistantTurnToNewTab(page);
|
||||
await expectChatHistoryAttachment(page);
|
||||
await forkMostRecentAssistantTurnToNewTab(page);
|
||||
|
||||
await submitMessage(page, "");
|
||||
const selectedTab = page
|
||||
.getByTestId("workspace-tabs-row")
|
||||
.getByRole("button")
|
||||
.and(page.locator('[aria-selected="true"]'));
|
||||
await expect(selectedTab).toHaveAttribute("data-testid", /^workspace-tab-draft_/, {
|
||||
timeout: 30_000,
|
||||
});
|
||||
await expect(agentTab).toHaveAttribute("aria-selected", "false");
|
||||
await expectChatHistoryAttachment(page);
|
||||
});
|
||||
|
||||
const userMessage = page.getByTestId("user-message").filter({ hasText: "Chat history" }).last();
|
||||
await expect(userMessage).toBeVisible({ timeout: 30_000 });
|
||||
await expect(userMessage).not.toContainText("Source agent:");
|
||||
await test.step("submit that draft with its chat-history attachment", async () => {
|
||||
await submitMessage(page, "");
|
||||
|
||||
const userMessage = page
|
||||
.getByTestId("user-message")
|
||||
.filter({ hasText: "Chat history" })
|
||||
.last();
|
||||
await expect(userMessage).toBeVisible({ timeout: 30_000 });
|
||||
await expect(userMessage).not.toContainText("Source agent:");
|
||||
});
|
||||
});
|
||||
|
||||
test("forks an assistant turn into New Workspace and keeps the attachment across host changes", async ({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, test } from "../support/fixtures";
|
||||
import { expect, test, type Page } from "../support/fixtures";
|
||||
import { trackPromptJumpRequests } from "../support/helpers/agent-timeline-gate";
|
||||
import {
|
||||
clickChatOutlineRowEdge,
|
||||
@@ -50,6 +50,43 @@ import {
|
||||
const WIDE_VIEWPORT = { width: 1440, height: 900 };
|
||||
const LOADED_TURNS = 16;
|
||||
|
||||
async function navigateLoadedOutline(page: Page, agent: LongTimelineAgent): Promise<void> {
|
||||
await test.step("preview across the row", async () => {
|
||||
await expectOneActiveChatOutlinePrompt(page);
|
||||
await hoverChatOutlinePrompt(page, 2);
|
||||
await expectChatOutlinePreview(page, agent.prompts[1]);
|
||||
|
||||
await pointAtChatOutlineRowEdge(page, 7);
|
||||
await expectChatOutlinePreview(page, agent.prompts[6]);
|
||||
|
||||
await movePointerOffChatOutline(page);
|
||||
await expectNoChatOutlinePreview(page);
|
||||
});
|
||||
await test.step("jump, dismiss preview, and retain bare selection", async () => {
|
||||
await clickChatOutlineRowEdge(page, 4);
|
||||
await expectChatOutlinePromptToRemainBare(page, 4);
|
||||
await expectTimelinePromptLandedBelowTop(page, agent.prompts[3]);
|
||||
await expectActiveChatOutlinePrompt(page, 4);
|
||||
await movePointerOffChatOutline(page);
|
||||
await expectNoChatOutlinePreview(page);
|
||||
});
|
||||
await test.step("track the active prompt while jumping and scrolling", async () => {
|
||||
await expectOneActiveChatOutlinePrompt(page);
|
||||
await scrollTimelineToNewestLoadedEdge(page);
|
||||
await expectTimelinePromptVisible(page, agent.newestPrompt);
|
||||
await expectActiveChatOutlinePromptMovedFrom(page, 4);
|
||||
});
|
||||
await test.step("clamp the newest prompt at maximum scroll", async () => {
|
||||
await clickChatOutlineRowEdge(page, LOADED_TURNS);
|
||||
await expectTimelineAtMaximumScrollWithPromptVisible(page, agent.newestPrompt);
|
||||
});
|
||||
}
|
||||
|
||||
async function jumpToNewerLoadedPrompt(page: Page, agent: LongTimelineAgent): Promise<void> {
|
||||
await clickChatOutlineRowEdge(page, 60);
|
||||
await expectTimelinePromptLandedBelowTop(page, agent.prompts[59]);
|
||||
}
|
||||
|
||||
test.describe("desktop chat outline", () => {
|
||||
test("keeps the prompt marked while reading split Markdown blocks and after completion", async ({
|
||||
page,
|
||||
@@ -183,28 +220,14 @@ test.describe("desktop chat outline", () => {
|
||||
await expectChatOutlinePrompts(page, LOADED_TURNS);
|
||||
});
|
||||
|
||||
test("previews the prompt under the pointer, anywhere across its row", async ({ page }) => {
|
||||
await hoverChatOutlinePrompt(page, 2);
|
||||
await expectChatOutlinePreview(page, agent.prompts[1]);
|
||||
|
||||
await pointAtChatOutlineRowEdge(page, 7);
|
||||
await expectChatOutlinePreview(page, agent.prompts[6]);
|
||||
|
||||
await movePointerOffChatOutline(page);
|
||||
await expectNoChatOutlinePreview(page);
|
||||
test("previews, jumps, and tracks the reader through loaded prompts", async ({ page }) => {
|
||||
await navigateLoadedOutline(page, agent);
|
||||
});
|
||||
|
||||
test("does not preview while crossing the rail toward the sidebar", async ({ page }) => {
|
||||
await expectNoChatOutlinePreviewWhileCrossingToSidebar(page);
|
||||
});
|
||||
|
||||
test("closes the preview when the pointer leaves after a jump", async ({ page }) => {
|
||||
await clickChatOutlineRowEdge(page, 4);
|
||||
await movePointerOffChatOutline(page);
|
||||
|
||||
await expectNoChatOutlinePreview(page);
|
||||
});
|
||||
|
||||
test("previews and jumps to the focused prompt from the keyboard", async ({ page }) => {
|
||||
await focusChatOutlinePrompt(page, 1);
|
||||
await expectChatOutlinePreview(page, agent.prompts[0]);
|
||||
@@ -215,32 +238,6 @@ test.describe("desktop chat outline", () => {
|
||||
await expectActiveChatOutlinePrompt(page, 1);
|
||||
});
|
||||
|
||||
test("moves the active mark as the reader jumps and scrolls", async ({ page }) => {
|
||||
await expectOneActiveChatOutlinePrompt(page);
|
||||
|
||||
await clickChatOutlineRowEdge(page, 4);
|
||||
await expectTimelinePromptLandedBelowTop(page, agent.prompts[3]);
|
||||
await expectActiveChatOutlinePrompt(page, 4);
|
||||
|
||||
await scrollTimelineToNewestLoadedEdge(page);
|
||||
await expectTimelinePromptVisible(page, agent.newestPrompt);
|
||||
await expectActiveChatOutlinePromptMovedFrom(page, 4);
|
||||
});
|
||||
|
||||
test("keeps a clicked prompt free of persistent selection chrome", async ({ page }) => {
|
||||
await clickChatOutlineRowEdge(page, 4);
|
||||
|
||||
await expectChatOutlinePromptToRemainBare(page, 4);
|
||||
});
|
||||
|
||||
test("clamps the newest prompt at maximum scroll while keeping it visible", async ({
|
||||
page,
|
||||
}) => {
|
||||
await clickChatOutlineRowEdge(page, LOADED_TURNS);
|
||||
|
||||
await expectTimelineAtMaximumScrollWithPromptVisible(page, agent.newestPrompt);
|
||||
});
|
||||
|
||||
test("keeps the current prompt active throughout a long live assistant turn", async ({
|
||||
page,
|
||||
}) => {
|
||||
@@ -282,21 +279,11 @@ test.describe("desktop chat outline", () => {
|
||||
await expectTimelinePromptLandedBelowTop(page, agent.prompts[29]);
|
||||
|
||||
expect(history.requestCount()).toBe(requestsBeforeJump);
|
||||
});
|
||||
|
||||
test("pins a newer loaded prompt when jumping downward from old history", async ({ page }) => {
|
||||
const history = await holdOlderHistoryPages(page, agent);
|
||||
await openAgentTimeline(page, agent);
|
||||
await expectChatOutlinePrompts(page, 80);
|
||||
await scrollThroughOlderHistoryPages(page, 2, history);
|
||||
await scrollTimelineToNewestLoadedEdge(page);
|
||||
|
||||
await clickChatOutlineRowEdge(page, 30);
|
||||
await expectTimelinePromptLandedBelowTop(page, agent.prompts[29]);
|
||||
|
||||
await clickChatOutlineRowEdge(page, 60);
|
||||
|
||||
await expectTimelinePromptLandedBelowTop(page, agent.prompts[59]);
|
||||
await test.step(
|
||||
"jump downward from old history to a newer loaded prompt",
|
||||
jumpToNewerLoadedPrompt.bind(null, page, agent),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -173,6 +173,14 @@ for (const layout of [
|
||||
});
|
||||
}
|
||||
|
||||
async function captureContextThemes(page: Page, surface: string, state: string) {
|
||||
for (const theme of ["light", "dark"] as const) {
|
||||
await page.emulateMedia({ colorScheme: theme });
|
||||
await expect(contextWindowDetails(page)).toBeVisible();
|
||||
await qaScreenshot(page, `${surface}-${theme}-${state}`);
|
||||
}
|
||||
}
|
||||
|
||||
type OpenDetails = (page: Page) => Promise<Locator>;
|
||||
type Shot = (state: string) => Promise<void>;
|
||||
|
||||
@@ -241,140 +249,138 @@ async function expectOnlyContextWindowWithoutUsage(
|
||||
await shot("unsupported");
|
||||
}
|
||||
|
||||
for (const theme of ["light", "dark"] as const) {
|
||||
test(`the context window hover card shows the agent's usage (${theme})`, async ({
|
||||
page,
|
||||
agent,
|
||||
}) => {
|
||||
test.setTimeout(240_000);
|
||||
const [claude] = claudeAndCodexReports();
|
||||
const usage = await scriptAgentUsage(page);
|
||||
await page.emulateMedia({ colorScheme: theme });
|
||||
await page.setViewportSize(DESKTOP);
|
||||
await openAgent(page, agent);
|
||||
const shot: Shot = (state) => qaScreenshot(page, `hover-card-desktop-${theme}-${state}`);
|
||||
test("the context window hover card shows the agent's usage in both themes", async ({
|
||||
page,
|
||||
agent,
|
||||
}) => {
|
||||
test.setTimeout(240_000);
|
||||
const [claude] = claudeAndCodexReports();
|
||||
const usage = await scriptAgentUsage(page);
|
||||
await page.emulateMedia({ colorScheme: "light" });
|
||||
await page.setViewportSize(DESKTOP);
|
||||
await openAgent(page, agent);
|
||||
const shot: Shot = (state) => captureContextThemes(page, "hover-card-desktop", state);
|
||||
|
||||
await test.step("reports stream in one card at a time, each with Refresh", async () => {
|
||||
const card = await expectCardsToStreamIn(page, usage, hoverContextWindowMeter, shot);
|
||||
await expectRefreshButtons(card, 2);
|
||||
await shot("ready");
|
||||
expect(usage.agentRequests()).toEqual([agent.agentId]);
|
||||
});
|
||||
|
||||
await test.step("the card stays open while the pointer moves onto it", async () => {
|
||||
const card = contextWindowDetails(page);
|
||||
await moveOntoContextWindowDetails(page, card);
|
||||
await page.waitForTimeout(300);
|
||||
await expect(card).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step("Refresh keeps the card open and replaces it with the new report", async () => {
|
||||
const refreshed = gate();
|
||||
usage.answerNext({ stream: [refreshed.promise, expiredLogin(onWorkLogin(claude!))] });
|
||||
const card = contextWindowDetails(page);
|
||||
await refreshUsageCard(card, "Claude");
|
||||
await expect(card.getByRole("button", { name: "Refresh Claude" })).toBeDisabled();
|
||||
// Past the card's close grace, with the pointer still on the card.
|
||||
await page.waitForTimeout(500);
|
||||
await expect(card).toBeVisible();
|
||||
refreshed.open();
|
||||
await expect(card.getByText(LOGIN_EXPIRED)).toBeVisible();
|
||||
expect(usage.refreshedAgents()).toEqual([agent.agentId]);
|
||||
});
|
||||
|
||||
await test.step("after a resume under another login, only that login shows", async () => {
|
||||
// Refresh can retain focus; return to the composer before testing pointer dismissal.
|
||||
await composerLocator(page).focus();
|
||||
// Closed while the old login's report is still on its way.
|
||||
await leaveContextWindowMeter(page);
|
||||
const slowSource = gate();
|
||||
usage.answerNext({ stream: [slowSource.promise, onWorkLogin(claude!)] });
|
||||
const closedEarly = await hoverContextWindowMeter(page);
|
||||
await expect(closedEarly.getByText("Loading usage...", { exact: true })).toBeVisible();
|
||||
await leaveContextWindowMeter(page);
|
||||
|
||||
const resumed = gate();
|
||||
const finished = gate();
|
||||
usage.answerNext({
|
||||
stream: [resumed.promise, onPersonalLogin(claude!), finished.promise],
|
||||
});
|
||||
const card = await hoverContextWindowMeter(page);
|
||||
await expect(card.getByText("Loading usage...", { exact: true })).toBeVisible();
|
||||
await expect(card.getByText("work@example.com", { exact: true })).toHaveCount(0);
|
||||
|
||||
// The old login's report lands during the new request, before the new login's.
|
||||
slowSource.open();
|
||||
resumed.open();
|
||||
await expect(
|
||||
usageCard(card, "claude:personal").getByText("personal@example.com", { exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(card.getByText("work@example.com", { exact: true })).toHaveCount(0);
|
||||
finished.open();
|
||||
});
|
||||
|
||||
await test.step("a report with a problem shows it on the card", async () => {
|
||||
usage.answerNext([expiredLogin(onWorkLogin(claude!))]);
|
||||
await reloadAgent(page);
|
||||
const card = await hoverContextWindowMeter(page);
|
||||
await expect(card.getByText(LOGIN_EXPIRED)).toBeVisible();
|
||||
await shot("problem");
|
||||
});
|
||||
|
||||
await test.step("a failed request says so in a sentence", async () => {
|
||||
await expectFailedRequestSentence(page, usage, hoverContextWindowMeter, shot);
|
||||
});
|
||||
|
||||
await test.step("a host without usage reports shows only the context window", async () => {
|
||||
await expectOnlyContextWindowWithoutUsage(page, usage, hoverContextWindowMeter, shot);
|
||||
// One request per open that had usage; this one sends none.
|
||||
expect(usage.agentRequests()).toHaveLength(6);
|
||||
});
|
||||
await test.step("reports stream in one card at a time, each with Refresh", async () => {
|
||||
const card = await expectCardsToStreamIn(page, usage, hoverContextWindowMeter, shot);
|
||||
await expectRefreshButtons(card, 2);
|
||||
await shot("ready");
|
||||
expect(usage.agentRequests()).toEqual([agent.agentId]);
|
||||
});
|
||||
|
||||
test(`the context window sheet shows the agent's usage and refreshes it (${theme})`, async ({
|
||||
page,
|
||||
agent,
|
||||
}) => {
|
||||
test.setTimeout(240_000);
|
||||
const [claude] = claudeAndCodexReports();
|
||||
const usage = await scriptAgentUsage(page);
|
||||
await page.emulateMedia({ colorScheme: theme });
|
||||
await page.setViewportSize(COMPACT);
|
||||
await openAgent(page, agent);
|
||||
const shot: Shot = (state) => qaScreenshot(page, `sheet-compact-${theme}-${state}`);
|
||||
|
||||
await test.step("reports stream in one card at a time, each with Refresh", async () => {
|
||||
const sheet = await expectCardsToStreamIn(page, usage, pressContextWindowMeter, shot);
|
||||
await expectRefreshButtons(sheet, 2);
|
||||
// Without hover, each card prints its freshness instead of a Refresh tooltip.
|
||||
await expect(sheet.getByText("Updated just now", { exact: true })).toHaveCount(2);
|
||||
await shot("ready");
|
||||
expect(usage.agentRequests()).toEqual([agent.agentId]);
|
||||
});
|
||||
|
||||
await test.step("Refresh replaces the card with the source's new report", async () => {
|
||||
usage.answerNext([expiredLogin(onWorkLogin(claude!))]);
|
||||
const sheet = contextWindowDetails(page);
|
||||
await refreshUsageCard(sheet, "Claude");
|
||||
await expect(sheet.getByText(LOGIN_EXPIRED)).toBeVisible();
|
||||
expect(usage.refreshedAgents()).toEqual([agent.agentId]);
|
||||
await shot("problem");
|
||||
});
|
||||
|
||||
await test.step("Close dismisses the sheet", async () => {
|
||||
await closeContextWindowSheet(page);
|
||||
});
|
||||
|
||||
await test.step("a failed request says so in a sentence", async () => {
|
||||
await expectFailedRequestSentence(page, usage, pressContextWindowMeter, shot);
|
||||
});
|
||||
|
||||
await test.step("a host without usage reports shows only the context window", async () => {
|
||||
await expectOnlyContextWindowWithoutUsage(page, usage, pressContextWindowMeter, shot);
|
||||
expect(usage.agentRequests()).toHaveLength(3);
|
||||
});
|
||||
await test.step("the card stays open while the pointer moves onto it", async () => {
|
||||
const card = contextWindowDetails(page);
|
||||
await moveOntoContextWindowDetails(page, card);
|
||||
await page.waitForTimeout(300);
|
||||
await expect(card).toBeVisible();
|
||||
});
|
||||
}
|
||||
|
||||
await test.step("Refresh keeps the card open and replaces it with the new report", async () => {
|
||||
const refreshed = gate();
|
||||
usage.answerNext({ stream: [refreshed.promise, expiredLogin(onWorkLogin(claude!))] });
|
||||
const card = contextWindowDetails(page);
|
||||
await refreshUsageCard(card, "Claude");
|
||||
await expect(card.getByRole("button", { name: "Refresh Claude" })).toBeDisabled();
|
||||
// Past the card's close grace, with the pointer still on the card.
|
||||
await page.waitForTimeout(500);
|
||||
await expect(card).toBeVisible();
|
||||
refreshed.open();
|
||||
await expect(card.getByText(LOGIN_EXPIRED)).toBeVisible();
|
||||
expect(usage.refreshedAgents()).toEqual([agent.agentId]);
|
||||
});
|
||||
|
||||
await test.step("after a resume under another login, only that login shows", async () => {
|
||||
// Refresh can retain focus; return to the composer before testing pointer dismissal.
|
||||
await composerLocator(page).focus();
|
||||
// Closed while the old login's report is still on its way.
|
||||
await leaveContextWindowMeter(page);
|
||||
const slowSource = gate();
|
||||
usage.answerNext({ stream: [slowSource.promise, onWorkLogin(claude!)] });
|
||||
const closedEarly = await hoverContextWindowMeter(page);
|
||||
await expect(closedEarly.getByText("Loading usage...", { exact: true })).toBeVisible();
|
||||
await leaveContextWindowMeter(page);
|
||||
|
||||
const resumed = gate();
|
||||
const finished = gate();
|
||||
usage.answerNext({
|
||||
stream: [resumed.promise, onPersonalLogin(claude!), finished.promise],
|
||||
});
|
||||
const card = await hoverContextWindowMeter(page);
|
||||
await expect(card.getByText("Loading usage...", { exact: true })).toBeVisible();
|
||||
await expect(card.getByText("work@example.com", { exact: true })).toHaveCount(0);
|
||||
|
||||
// The old login's report lands during the new request, before the new login's.
|
||||
slowSource.open();
|
||||
resumed.open();
|
||||
await expect(
|
||||
usageCard(card, "claude:personal").getByText("personal@example.com", { exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(card.getByText("work@example.com", { exact: true })).toHaveCount(0);
|
||||
finished.open();
|
||||
});
|
||||
|
||||
await test.step("a report with a problem shows it on the card", async () => {
|
||||
usage.answerNext([expiredLogin(onWorkLogin(claude!))]);
|
||||
await reloadAgent(page);
|
||||
const card = await hoverContextWindowMeter(page);
|
||||
await expect(card.getByText(LOGIN_EXPIRED)).toBeVisible();
|
||||
await shot("problem");
|
||||
});
|
||||
|
||||
await test.step("a failed request says so in a sentence", async () => {
|
||||
await expectFailedRequestSentence(page, usage, hoverContextWindowMeter, shot);
|
||||
});
|
||||
|
||||
await test.step("a host without usage reports shows only the context window", async () => {
|
||||
await expectOnlyContextWindowWithoutUsage(page, usage, hoverContextWindowMeter, shot);
|
||||
// One request per open that had usage; this one sends none.
|
||||
expect(usage.agentRequests()).toHaveLength(6);
|
||||
});
|
||||
});
|
||||
|
||||
test("the context window sheet shows the agent's usage and refreshes it in both themes", async ({
|
||||
page,
|
||||
agent,
|
||||
}) => {
|
||||
test.setTimeout(240_000);
|
||||
const [claude] = claudeAndCodexReports();
|
||||
const usage = await scriptAgentUsage(page);
|
||||
await page.emulateMedia({ colorScheme: "light" });
|
||||
await page.setViewportSize(COMPACT);
|
||||
await openAgent(page, agent);
|
||||
const shot: Shot = (state) => captureContextThemes(page, "sheet-compact", state);
|
||||
|
||||
await test.step("reports stream in one card at a time, each with Refresh", async () => {
|
||||
const sheet = await expectCardsToStreamIn(page, usage, pressContextWindowMeter, shot);
|
||||
await expectRefreshButtons(sheet, 2);
|
||||
// Without hover, each card prints its freshness instead of a Refresh tooltip.
|
||||
await expect(sheet.getByText("Updated just now", { exact: true })).toHaveCount(2);
|
||||
await shot("ready");
|
||||
expect(usage.agentRequests()).toEqual([agent.agentId]);
|
||||
});
|
||||
|
||||
await test.step("Refresh replaces the card with the source's new report", async () => {
|
||||
usage.answerNext([expiredLogin(onWorkLogin(claude!))]);
|
||||
const sheet = contextWindowDetails(page);
|
||||
await refreshUsageCard(sheet, "Claude");
|
||||
await expect(sheet.getByText(LOGIN_EXPIRED)).toBeVisible();
|
||||
expect(usage.refreshedAgents()).toEqual([agent.agentId]);
|
||||
await shot("problem");
|
||||
});
|
||||
|
||||
await test.step("Close dismisses the sheet", async () => {
|
||||
await closeContextWindowSheet(page);
|
||||
});
|
||||
|
||||
await test.step("a failed request says so in a sentence", async () => {
|
||||
await expectFailedRequestSentence(page, usage, pressContextWindowMeter, shot);
|
||||
});
|
||||
|
||||
await test.step("a host without usage reports shows only the context window", async () => {
|
||||
await expectOnlyContextWindowWithoutUsage(page, usage, pressContextWindowMeter, shot);
|
||||
expect(usage.agentRequests()).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
test("context details support keyboard entry and dismiss when the viewport moves", async ({
|
||||
page,
|
||||
@@ -420,36 +426,38 @@ test("context details support keyboard entry and dismiss when the viewport moves
|
||||
await expect(details).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("pointer hovering context details preserves composer focus", async ({ page, agent }) => {
|
||||
test("context details preserve composer focus on hover and leave Find available after keyboard dismissal", async ({
|
||||
page,
|
||||
agent,
|
||||
}) => {
|
||||
const usage = await scriptAgentUsage(page);
|
||||
await page.setViewportSize(DESKTOP);
|
||||
await openAgent(page, agent);
|
||||
const [claude] = claudeAndCodexReports();
|
||||
usage.answerNext([onPersonalLogin(claude!)]);
|
||||
const input = composerLocator(page);
|
||||
await input.focus();
|
||||
await hoverContextWindowMeter(page);
|
||||
await expect(input).toBeFocused();
|
||||
await leaveContextWindowMeter(page);
|
||||
await expect(input).toBeFocused();
|
||||
});
|
||||
|
||||
test("Find remains available after dismissing context details", async ({ page, agent }) => {
|
||||
const usage = await scriptAgentUsage(page);
|
||||
await page.setViewportSize(DESKTOP);
|
||||
await openAgent(page, agent);
|
||||
const [claude] = claudeAndCodexReports();
|
||||
usage.answerNext([onPersonalLogin(claude!)]);
|
||||
const meter = page.getByTestId("context-window-meter");
|
||||
const trigger = page.getByRole("button", {
|
||||
name: (await meter.getAttribute("aria-label"))!,
|
||||
exact: true,
|
||||
await test.step("pointer entry and dismissal preserve composer focus", async () => {
|
||||
usage.answerNext([onPersonalLogin(claude!)]);
|
||||
const input = composerLocator(page);
|
||||
await input.focus();
|
||||
await hoverContextWindowMeter(page);
|
||||
await expect(input).toBeFocused();
|
||||
await leaveContextWindowMeter(page);
|
||||
await expect(input).toBeFocused();
|
||||
});
|
||||
|
||||
await test.step("keyboard dismissal restores trigger focus and Find remains available", async () => {
|
||||
usage.answerNext([onPersonalLogin(claude!)]);
|
||||
const meter = page.getByTestId("context-window-meter");
|
||||
const trigger = page.getByRole("button", {
|
||||
name: (await meter.getAttribute("aria-label"))!,
|
||||
exact: true,
|
||||
});
|
||||
await trigger.focus();
|
||||
await expect(contextWindowDetails(page)).toBeVisible();
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(contextWindowDetails(page)).toHaveCount(0);
|
||||
await expect(trigger).toBeFocused();
|
||||
await page.keyboard.press("ControlOrMeta+f");
|
||||
await expect(page.getByRole("textbox", { name: "Find in pane", exact: true })).toBeFocused();
|
||||
});
|
||||
await trigger.focus();
|
||||
await expect(contextWindowDetails(page)).toBeVisible();
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(contextWindowDetails(page)).toHaveCount(0);
|
||||
await expect(trigger).toBeFocused();
|
||||
await page.keyboard.press("ControlOrMeta+f");
|
||||
await expect(page.getByRole("textbox", { name: "Find in pane", exact: true })).toBeFocused();
|
||||
});
|
||||
|
||||
@@ -105,25 +105,41 @@ async function seedAgentWithFileLink(input: LinkedFile) {
|
||||
}
|
||||
|
||||
test.describe("CodeMirror workspace file editing", () => {
|
||||
test("opens explicit screenshot links and explains missing files", async ({ page }) => {
|
||||
test("opens screenshot and non-ASCII Markdown links and explains missing files", async ({
|
||||
page,
|
||||
}) => {
|
||||
const session = await seedMockAgentWorkspace({
|
||||
repoPrefix: "screenshot-markdown-link-",
|
||||
title: "Screenshot file links",
|
||||
initialPrompt: "Show screenshots",
|
||||
featureValues: {
|
||||
mockAssistantResponse:
|
||||
"[Capture](.dev/tablet-review/capture.png) [Missing capture](.dev/tablet-review/missing.png)",
|
||||
"[Capture](.dev/tablet-review/capture.png) [报告](docs/reports/开户赠金.md) [Missing capture](.dev/tablet-review/missing.png)",
|
||||
},
|
||||
});
|
||||
try {
|
||||
await mkdir(path.join(session.cwd, ".dev/tablet-review"), { recursive: true });
|
||||
await writeFile(path.join(session.cwd, ".dev/tablet-review/capture.png"), RED_PIXEL);
|
||||
await mkdir(path.join(session.cwd, "docs/reports"), { recursive: true });
|
||||
await writeFile(path.join(session.cwd, "docs/reports/开户赠金.md"), "# Report opened\n");
|
||||
await openAgentRoute(page, session);
|
||||
await test.step("Open the linked screenshot", async () => {
|
||||
await openAssistantMarkdownLink(page, "Capture");
|
||||
await expectFileTabOpen(page, ".dev/tablet-review/capture.png");
|
||||
await expect(page.getByTestId("image-file-preview-canvas")).toBeVisible();
|
||||
});
|
||||
await test.step("Open the non-ASCII report named by its Markdown link", async () => {
|
||||
await returnToAssistant(page, session.agentId);
|
||||
await openAssistantMarkdownLink(page, "报告");
|
||||
await expectFileTabOpen(page, "docs/reports/开户赠金.md");
|
||||
await expect(
|
||||
page
|
||||
.getByTestId("workspace-file-pane")
|
||||
.filter({ visible: true })
|
||||
.getByText("Report opened", { exact: true }),
|
||||
).toBeVisible();
|
||||
await page.screenshot({ path: test.info().outputPath("non-ascii-file-opened.png") });
|
||||
});
|
||||
await test.step("Explain why the missing screenshot cannot open", async () => {
|
||||
await returnToAssistant(page, session.agentId);
|
||||
await openAssistantMarkdownLink(page, "Missing capture");
|
||||
@@ -136,31 +152,6 @@ test.describe("CodeMirror workspace file editing", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("opens the non-ASCII file named by an assistant markdown link", async ({ page }) => {
|
||||
const session = await seedMockAgentWorkspace({
|
||||
repoPrefix: "encoded-markdown-link-",
|
||||
title: "Non-ASCII file link",
|
||||
initialPrompt: "Show the report link",
|
||||
featureValues: { mockAssistantResponse: "[报告](docs/reports/开户赠金.md)" },
|
||||
});
|
||||
try {
|
||||
await mkdir(path.join(session.cwd, "docs/reports"), { recursive: true });
|
||||
await writeFile(path.join(session.cwd, "docs/reports/开户赠金.md"), "# Report opened\n");
|
||||
await openAgentRoute(page, session);
|
||||
await openAssistantMarkdownLink(page, "报告");
|
||||
await expectFileTabOpen(page, "docs/reports/开户赠金.md");
|
||||
await expect(
|
||||
page
|
||||
.getByTestId("workspace-file-pane")
|
||||
.filter({ visible: true })
|
||||
.getByText("Report opened", { exact: true }),
|
||||
).toBeVisible();
|
||||
await page.screenshot({ path: test.info().outputPath("non-ascii-file-opened.png") });
|
||||
} finally {
|
||||
await session.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
for (const order of ["raw-first", "href-first"] as const) {
|
||||
test(`opens literal and encoded percent filenames independently ${order}`, async ({ page }) => {
|
||||
const session = await seedMockAgentWorkspace({
|
||||
@@ -690,7 +681,37 @@ test.describe("CodeMirror workspace file editing", () => {
|
||||
const imagePath = path.join(workspace.repoPath, "pixel.png");
|
||||
await writeFile(markdownPath, "# First heading\n", "utf8");
|
||||
await writeFile(imagePath, RED_PIXEL);
|
||||
await mkdir(path.join(workspace.repoPath, "docs"), { recursive: true });
|
||||
await mkdir(path.join(workspace.repoPath, "data", "cache"), { recursive: true });
|
||||
await writeFile(path.join(workspace.repoPath, "docs", "sibling.png"), RED_PIXEL);
|
||||
await writeFile(path.join(workspace.repoPath, "data", "cache", "hero.png"), RED_PIXEL);
|
||||
await writeFile(
|
||||
path.join(workspace.repoPath, "docs", "guide.md"),
|
||||
[
|
||||
"# Guide",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
await workspace.navigateTo();
|
||||
await test.step("render sibling and parent-relative images in Markdown preview", async () => {
|
||||
await openFileExplorer(page);
|
||||
await expandFolder(page, "docs");
|
||||
await openFileFromExplorer(page, "guide.md");
|
||||
await expectFileTabOpen(page, "docs/guide.md");
|
||||
|
||||
const guidePane = page.getByTestId("workspace-file-pane").filter({ visible: true });
|
||||
await expect(guidePane.getByText("Guide", { exact: true })).toBeVisible();
|
||||
await expectMarkdownImageLoaded(page, "sibling");
|
||||
await expectMarkdownImageLoaded(page, "hero");
|
||||
await guidePane.screenshot({
|
||||
path: test.info().outputPath("markdown-relative-images.png"),
|
||||
});
|
||||
});
|
||||
await openWorkspaceFile(page, "notes.md");
|
||||
|
||||
const visibleFilePane = page.getByTestId("workspace-file-pane").filter({ visible: true });
|
||||
@@ -724,84 +745,30 @@ test.describe("CodeMirror workspace file editing", () => {
|
||||
await expect.poll(() => image.getAttribute("src")).not.toBe(initialSource);
|
||||
});
|
||||
|
||||
test("renders local relative images inside Markdown preview", async ({ page, withWorkspace }) => {
|
||||
test.setTimeout(90_000);
|
||||
const workspace = await withWorkspace({ prefix: "file-md-relative-image-" });
|
||||
await mkdir(path.join(workspace.repoPath, "docs"), { recursive: true });
|
||||
await mkdir(path.join(workspace.repoPath, "data", "cache"), { recursive: true });
|
||||
await writeFile(path.join(workspace.repoPath, "docs", "sibling.png"), RED_PIXEL);
|
||||
await writeFile(path.join(workspace.repoPath, "data", "cache", "hero.png"), RED_PIXEL);
|
||||
await writeFile(
|
||||
path.join(workspace.repoPath, "docs", "guide.md"),
|
||||
[
|
||||
"# Guide",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
await workspace.navigateTo();
|
||||
await openFileExplorer(page);
|
||||
await expandFolder(page, "docs");
|
||||
await openFileFromExplorer(page, "guide.md");
|
||||
await expectFileTabOpen(page, "docs/guide.md");
|
||||
|
||||
const visibleFilePane = page.getByTestId("workspace-file-pane").filter({ visible: true });
|
||||
await expect(visibleFilePane.getByText("Guide", { exact: true })).toBeVisible();
|
||||
await expectMarkdownImageLoaded(page, "sibling");
|
||||
await expectMarkdownImageLoaded(page, "hero");
|
||||
await visibleFilePane.screenshot({
|
||||
path: test.info().outputPath("markdown-relative-images.png"),
|
||||
});
|
||||
});
|
||||
|
||||
test("previews and refreshes an HTML plan while preserving source access", async ({
|
||||
test("isolates an HTML preview, blocks fetch egress, and refreshes while retaining source access", async ({
|
||||
page,
|
||||
withWorkspace,
|
||||
}) => {
|
||||
test.setTimeout(90_000);
|
||||
const workspace = await withWorkspace({ prefix: "file-editing-html-preview-" });
|
||||
const htmlPath = path.join(workspace.repoPath, "plan.html");
|
||||
await writeFile(
|
||||
htmlPath,
|
||||
"<!doctype html><html><body><h1>Visual plan</h1></body></html>",
|
||||
"utf8",
|
||||
);
|
||||
await workspace.navigateTo();
|
||||
await openWorkspaceFile(page, "plan.html");
|
||||
|
||||
const preview = htmlPreview(page);
|
||||
await expect(preview.host).toBeVisible();
|
||||
await expect(preview.host).toHaveAttribute("sandbox", /allow-scripts/);
|
||||
await expect(preview.host).not.toHaveAttribute("sandbox", /allow-same-origin/);
|
||||
await expect(preview.document.getByRole("heading", { name: "Visual plan" })).toBeVisible();
|
||||
|
||||
await writeFile(
|
||||
htmlPath,
|
||||
"<!doctype html><html><body><h1>Updated plan</h1></body></html>",
|
||||
"utf8",
|
||||
);
|
||||
await expect(preview.document.getByRole("heading", { name: "Updated plan" })).toBeVisible();
|
||||
|
||||
await selectFileView(page, "Source");
|
||||
await expect(page.getByTestId("file-source-editor")).toBeVisible();
|
||||
await expect(preview.host).toHaveCount(0);
|
||||
await selectFileView(page, "Preview");
|
||||
await expect(preview.host).toBeVisible();
|
||||
});
|
||||
|
||||
test("runs inline scripts without allowing fetch egress", async ({ page, withWorkspace }) => {
|
||||
test.setTimeout(90_000);
|
||||
const workspace = await withWorkspace({ prefix: "file-editing-html-csp-" });
|
||||
await writeFile(
|
||||
path.join(workspace.repoPath, "probe.html"),
|
||||
`<!doctype html><html><head><title>probe</title></head><body>
|
||||
<h1>Visual plan</h1>
|
||||
<h1 id="script-result">Inline script did not run</h1>
|
||||
<p id="network-result">Network request not attempted</p>
|
||||
<p id="document-mode">Standards mode not detected</p>
|
||||
<p id="parent">?</p><p id="storage">?</p><p id="cookie">?</p>
|
||||
<script>
|
||||
function report(id, label, probe) {
|
||||
try { probe(); document.getElementById(id).textContent = label + " reachable"; }
|
||||
catch (error) { document.getElementById(id).textContent = label + " blocked"; }
|
||||
}
|
||||
report("parent", "Parent DOM", function () { return parent.document.body; });
|
||||
report("storage", "Storage", function () { return localStorage.length; });
|
||||
report("cookie", "Cookies", function () { return document.cookie; });
|
||||
</script>
|
||||
|
||||
<script>
|
||||
document.getElementById("script-result").textContent = "Inline script ran";
|
||||
if (document.compatMode === "CSS1Compat") {
|
||||
@@ -822,6 +789,8 @@ test.describe("CodeMirror workspace file editing", () => {
|
||||
await openWorkspaceFile(page, "probe.html");
|
||||
|
||||
const preview = htmlPreview(page);
|
||||
await expect(preview.host).toBeVisible();
|
||||
await expect(preview.document.getByRole("heading", { name: "Visual plan" })).toBeVisible();
|
||||
await expect(
|
||||
preview.document.getByRole("heading", { name: "Inline script ran" }),
|
||||
).toBeVisible();
|
||||
@@ -832,34 +801,26 @@ test.describe("CodeMirror workspace file editing", () => {
|
||||
preview.document.getByText("Network request blocked", { exact: true }),
|
||||
).toBeVisible();
|
||||
expect(blockedRequests).toEqual([]);
|
||||
});
|
||||
|
||||
test("isolates HTML plans from the app origin and storage", async ({ page, withWorkspace }) => {
|
||||
test.setTimeout(90_000);
|
||||
const workspace = await withWorkspace({ prefix: "file-editing-html-origin-" });
|
||||
await writeFile(
|
||||
path.join(workspace.repoPath, "origin.html"),
|
||||
`<!doctype html><html><body>
|
||||
<p id="parent">?</p><p id="storage">?</p><p id="cookie">?</p>
|
||||
<script>
|
||||
function report(id, label, probe) {
|
||||
try { probe(); document.getElementById(id).textContent = label + " reachable"; }
|
||||
catch (error) { document.getElementById(id).textContent = label + " blocked"; }
|
||||
}
|
||||
report("parent", "Parent DOM", function () { return parent.document.body; });
|
||||
report("storage", "Storage", function () { return localStorage.length; });
|
||||
report("cookie", "Cookies", function () { return document.cookie; });
|
||||
</script>
|
||||
</body></html>`,
|
||||
"utf8",
|
||||
);
|
||||
await workspace.navigateTo();
|
||||
await openWorkspaceFile(page, "origin.html");
|
||||
|
||||
const preview = htmlPreview(page);
|
||||
await expect(preview.document.getByText("Parent DOM blocked", { exact: true })).toBeVisible();
|
||||
await expect(preview.document.getByText("Storage blocked", { exact: true })).toBeVisible();
|
||||
await expect(preview.document.getByText("Cookies blocked", { exact: true })).toBeVisible();
|
||||
await test.step("isolate the preview from app origin and storage", async () => {
|
||||
await expect(preview.host).toHaveAttribute("sandbox", /allow-scripts/);
|
||||
await expect(preview.host).not.toHaveAttribute("sandbox", /allow-same-origin/);
|
||||
await expect(preview.document.getByText("Parent DOM blocked", { exact: true })).toBeVisible();
|
||||
await expect(preview.document.getByText("Storage blocked", { exact: true })).toBeVisible();
|
||||
await expect(preview.document.getByText("Cookies blocked", { exact: true })).toBeVisible();
|
||||
});
|
||||
await test.step("refresh a changed HTML plan and switch to source and back", async () => {
|
||||
await writeFile(
|
||||
path.join(workspace.repoPath, "probe.html"),
|
||||
"<!doctype html><html><body><h1>Updated plan</h1></body></html>",
|
||||
"utf8",
|
||||
);
|
||||
await expect(preview.document.getByRole("heading", { name: "Updated plan" })).toBeVisible();
|
||||
await selectFileView(page, "Source");
|
||||
await expect(page.getByTestId("file-source-editor")).toBeVisible();
|
||||
await expect(preview.host).toHaveCount(0);
|
||||
await selectFileView(page, "Preview");
|
||||
await expect(preview.host).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test("persists Vim keybindings and reports Vim mode with cursor position", async ({
|
||||
|
||||
@@ -88,11 +88,23 @@ async function openNewWorkspaceFromContextProject(
|
||||
await expectNewWorkspaceProjectSelected(page, "Context project");
|
||||
}
|
||||
|
||||
const test = base.extend<{ crossHostProject: CrossHostProjectScenario }>({
|
||||
crossHostProject: async ({ page: _page }, provide) => {
|
||||
const secondaryDaemon: IsolatedHostDaemon = await startIsolatedHostDaemon(
|
||||
"new-workspace-project-preservation-secondary",
|
||||
);
|
||||
const test = base.extend<
|
||||
{ crossHostProject: CrossHostProjectScenario },
|
||||
{ secondaryDaemon: IsolatedHostDaemon }
|
||||
>({
|
||||
secondaryDaemon: [
|
||||
async ({ e2eWorker }, provide) => {
|
||||
void e2eWorker;
|
||||
const daemon = await startIsolatedHostDaemon("new-workspace-project-preservation-secondary");
|
||||
try {
|
||||
await provide(daemon);
|
||||
} finally {
|
||||
await daemon.close();
|
||||
}
|
||||
},
|
||||
{ scope: "worker" },
|
||||
],
|
||||
crossHostProject: async ({ page: _page, secondaryDaemon }, provide) => {
|
||||
const contextRepo = await createTempGitRepo("new-workspace-context-");
|
||||
const primarySharedRepo = await createTempGitRepo("new-workspace-paseo-primary-", {
|
||||
originUrl: SHARED_REMOTE_URL,
|
||||
@@ -177,7 +189,6 @@ const test = base.extend<{ crossHostProject: CrossHostProjectScenario }>({
|
||||
await primaryDuplicateRepo.cleanup().catch(() => undefined);
|
||||
await secondaryFallbackRepo.cleanup().catch(() => undefined);
|
||||
await secondarySharedRepo.cleanup().catch(() => undefined);
|
||||
await secondaryDaemon.close().catch(() => undefined);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -226,10 +226,19 @@ test.describe("narrow touch browser", () => {
|
||||
await source(page).press("ControlOrMeta+f");
|
||||
await expect(query(page)).toBeFocused();
|
||||
await query(page).fill("needle");
|
||||
await page.getByRole("button", { name: "Next match" }).tap();
|
||||
await expect(status(page)).toHaveText("2 of 2");
|
||||
await expectFindInsidePane(page);
|
||||
await page.screenshot({ path: testInfo.outputPath("touch-find.png") });
|
||||
await test.step("reveal the first match below the floating Find widget", async () => {
|
||||
await expect(status(page)).toHaveText("1 of 2");
|
||||
await expectActiveMatchUncovered(page);
|
||||
});
|
||||
await test.step("tap the next uncovered match while controls stay inside the pane", async () => {
|
||||
await page.getByRole("button", { name: "Next match" }).tap();
|
||||
await expect(status(page)).toHaveText("2 of 2");
|
||||
await expectActiveMatchUncovered(page);
|
||||
await expectFindInsidePane(page);
|
||||
await page.screenshot({ path: testInfo.outputPath("touch-find.png") });
|
||||
await page.screenshot({ path: testInfo.outputPath("touch-match-uncovered.png") });
|
||||
await expect(query(page)).toHaveValue("needle");
|
||||
});
|
||||
await page.getByRole("button", { name: "Close Find" }).tap();
|
||||
await expect(source(page)).toBeFocused();
|
||||
});
|
||||
@@ -332,33 +341,6 @@ for (const startingField of ["Find in pane", "Replace with"] as const) {
|
||||
}
|
||||
|
||||
test.describe("the active match stays visible under the floating widget", () => {
|
||||
test.describe("narrow touch browser", () => {
|
||||
test.use({ hasTouch: true });
|
||||
test("reveals a short file's match that the widget would cover", async ({
|
||||
page,
|
||||
withWorkspace,
|
||||
}, testInfo) => {
|
||||
const workspace = await withWorkspace({ prefix: "pane-find-cover-touch-" });
|
||||
await writeFile(path.join(workspace.repoPath, "touch.txt"), "touch needle\nnext needle\n");
|
||||
await workspace.navigateTo();
|
||||
await openSource(page, "touch.txt");
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await source(page).focus();
|
||||
await source(page).press("ControlOrMeta+f");
|
||||
await expect(query(page)).toBeFocused();
|
||||
await query(page).fill("needle");
|
||||
await expect(status(page)).toHaveText("1 of 2");
|
||||
await expectActiveMatchUncovered(page);
|
||||
await page.getByRole("button", { name: "Next match" }).tap();
|
||||
await expect(status(page)).toHaveText("2 of 2");
|
||||
await expectActiveMatchUncovered(page);
|
||||
await page.screenshot({ path: testInfo.outputPath("touch-match-uncovered.png") });
|
||||
await expect(query(page)).toHaveValue("needle");
|
||||
await page.getByRole("button", { name: "Close Find" }).tap();
|
||||
await expect(source(page)).toBeFocused();
|
||||
});
|
||||
});
|
||||
|
||||
test("reveals a match beneath the top-right widget in a narrow desktop pane", async ({
|
||||
page,
|
||||
withWorkspace,
|
||||
|
||||
@@ -316,7 +316,9 @@ test.describe("Sidebar workspace list", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("project with GitHub remote shows its selected folder name in sidebar", async ({ page }) => {
|
||||
test("opens a GitHub-backed workspace with folder metadata and pointer and keyboard hover cards", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = await seedWorkspace({
|
||||
repoPrefix: "sidebar-remote-",
|
||||
repo: { withRemote: true, originUrl: GITHUB_REMOTE_URL },
|
||||
@@ -335,6 +337,46 @@ test.describe("Sidebar workspace list", () => {
|
||||
|
||||
await expect(projectRow).toBeVisible({ timeout: 30_000 });
|
||||
await expect(projectRow).not.toContainText("test-owner/test-repo");
|
||||
expect(workspace.workspaceName).toBe("main");
|
||||
const row = await waitForSidebarWorkspace(page, workspace.workspaceId);
|
||||
await expect(row).toContainText("main");
|
||||
|
||||
await test.step("inspect host metadata before opening the workspace", async () => {
|
||||
const hoverCard = await openWorkspaceHoverCard(page, workspace.workspaceId);
|
||||
await expect(page.getByTestId("hover-card-workspace-host")).toHaveText("localhost");
|
||||
await expect(hoverCard).not.toContainText(/\b(Online|Connecting|Offline|Error|Idle)\b/);
|
||||
});
|
||||
|
||||
await test.step("open the workspace and inspect its selected folder header", async () => {
|
||||
await openWorkspaceFromSidebar(page, workspace.workspaceId);
|
||||
await expectWorkspaceHeader(page, {
|
||||
title: workspace.workspaceName,
|
||||
subtitle: projectName,
|
||||
});
|
||||
await movePointerAwayFromWorkspaceHoverCard(page);
|
||||
await expectWorkspaceHoverCardClosed(page);
|
||||
});
|
||||
|
||||
await test.step("dismiss a clicked row's hover card when the pointer leaves", async () => {
|
||||
await row.click();
|
||||
await expect(row).toBeFocused();
|
||||
await expectWorkspaceHoverCardOpen(page);
|
||||
await movePointerAwayFromWorkspaceHoverCard(page);
|
||||
await expectWorkspaceHoverCardClosed(page);
|
||||
});
|
||||
|
||||
await test.step("retain keyboard preview until Escape or focus leaves the row", async () => {
|
||||
await focusWorkspaceRowWithKeyboard(page, workspace.workspaceId);
|
||||
await expectWorkspaceHoverCardOpen(page);
|
||||
await movePointerAwayFromWorkspaceHoverCard(page);
|
||||
await expectWorkspaceHoverCardStaysOpen(page);
|
||||
await page.keyboard.press("Escape");
|
||||
await expectWorkspaceHoverCardClosed(page);
|
||||
await focusWorkspaceRowWithKeyboard(page, workspace.workspaceId);
|
||||
await expectWorkspaceHoverCardOpen(page);
|
||||
await page.keyboard.press("Tab");
|
||||
await expectWorkspaceHoverCardClosed(page);
|
||||
});
|
||||
} finally {
|
||||
await workspace.cleanup();
|
||||
}
|
||||
@@ -354,107 +396,6 @@ test.describe("Sidebar workspace list", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("workspace header uses the selected folder name instead of its GitHub remote", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = await seedWorkspace({
|
||||
repoPrefix: "sidebar-header-",
|
||||
repo: { withRemote: true, originUrl: GITHUB_REMOTE_URL },
|
||||
});
|
||||
|
||||
try {
|
||||
const projectName = path.basename(workspace.repoPath);
|
||||
await gotoAppShell(page);
|
||||
await waitForSidebarProject(page, projectName);
|
||||
await waitForSidebarWorkspace(page, workspace.workspaceId);
|
||||
await openWorkspaceFromSidebar(page, workspace.workspaceId);
|
||||
|
||||
await expectWorkspaceHeader(page, {
|
||||
title: workspace.workspaceName,
|
||||
subtitle: projectName,
|
||||
});
|
||||
} finally {
|
||||
await workspace.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("git project shows branch name in workspace row", async ({ page }) => {
|
||||
const workspace = await seedWorkspace({ repoPrefix: "sidebar-branch-" });
|
||||
|
||||
try {
|
||||
await gotoAppShell(page);
|
||||
await waitForSidebarProject(page, path.basename(workspace.repoPath));
|
||||
|
||||
expect(workspace.workspaceName).toBe("main");
|
||||
await expect(await waitForSidebarWorkspace(page, workspace.workspaceId)).toContainText(
|
||||
"main",
|
||||
);
|
||||
} finally {
|
||||
await workspace.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("workspace hover card shows host as metadata", async ({ page }) => {
|
||||
const workspace = await seedWorkspace({ repoPrefix: "sidebar-hover-host-" });
|
||||
|
||||
try {
|
||||
await gotoAppShell(page);
|
||||
await waitForSidebarProject(page, path.basename(workspace.repoPath));
|
||||
|
||||
const hoverCard = await openWorkspaceHoverCard(page, workspace.workspaceId);
|
||||
await expect(page.getByTestId("hover-card-workspace-host")).toHaveText("localhost");
|
||||
await expect(hoverCard).not.toContainText(/\b(Online|Connecting|Offline|Error|Idle)\b/);
|
||||
} finally {
|
||||
await workspace.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("workspace hover card closes when the pointer leaves a clicked row", async ({ page }) => {
|
||||
const workspace = await seedWorkspace({ repoPrefix: "sidebar-hover-click-leave-" });
|
||||
|
||||
try {
|
||||
await gotoAppShell(page);
|
||||
await waitForSidebarProject(page, path.basename(workspace.repoPath));
|
||||
const row = await openWorkspaceFromSidebar(page, workspace.workspaceId);
|
||||
await movePointerAwayFromWorkspaceHoverCard(page);
|
||||
await expectWorkspaceHoverCardClosed(page);
|
||||
|
||||
await row.click();
|
||||
await expect(row).toBeFocused();
|
||||
await expectWorkspaceHoverCardOpen(page);
|
||||
await movePointerAwayFromWorkspaceHoverCard(page);
|
||||
await expectWorkspaceHoverCardClosed(page);
|
||||
} finally {
|
||||
await workspace.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("workspace hover card follows keyboard focus on a row", async ({ page }) => {
|
||||
const workspace = await seedWorkspace({ repoPrefix: "sidebar-hover-keyboard-" });
|
||||
|
||||
try {
|
||||
await gotoAppShell(page);
|
||||
await waitForSidebarProject(page, path.basename(workspace.repoPath));
|
||||
await openWorkspaceFromSidebar(page, workspace.workspaceId);
|
||||
await movePointerAwayFromWorkspaceHoverCard(page);
|
||||
await expectWorkspaceHoverCardClosed(page);
|
||||
|
||||
await focusWorkspaceRowWithKeyboard(page, workspace.workspaceId);
|
||||
await expectWorkspaceHoverCardOpen(page);
|
||||
await movePointerAwayFromWorkspaceHoverCard(page);
|
||||
await expectWorkspaceHoverCardStaysOpen(page);
|
||||
await page.keyboard.press("Escape");
|
||||
await expectWorkspaceHoverCardClosed(page);
|
||||
|
||||
await focusWorkspaceRowWithKeyboard(page, workspace.workspaceId);
|
||||
await expectWorkspaceHoverCardOpen(page);
|
||||
await page.keyboard.press("Tab");
|
||||
await expectWorkspaceHoverCardClosed(page);
|
||||
} finally {
|
||||
await workspace.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("marks a finished workspace unread until it is opened again", async ({ page }) => {
|
||||
const workspace = await seedMockAgentWorkspace({
|
||||
repoPrefix: "sidebar-mark-unread-",
|
||||
@@ -495,26 +436,21 @@ test.describe("Sidebar workspace list", () => {
|
||||
test.describe("Mobile sidebar panelState transition", () => {
|
||||
test.use({ viewport: { width: 390, height: 844 } });
|
||||
|
||||
test("showMobileAgent open and close transition", async ({ page }) => {
|
||||
test("opens mobile navigation beside the close button and closes it again", async ({ page }) => {
|
||||
await gotoAppShell(page);
|
||||
await expectMobileAgentSidebarHidden(page);
|
||||
await openMobileAgentSidebar(page);
|
||||
await expectMobileAgentSidebarVisible(page);
|
||||
await test.step("place the close button beside the first navigation row", async () => {
|
||||
const firstRow = await page.getByTestId("sidebar-global-new-workspace").boundingBox();
|
||||
const closeButton = await page.getByTestId("sidebar-close").boundingBox();
|
||||
if (!firstRow || !closeButton) throw new Error("first nav row and close button must render");
|
||||
expect(closeButton.x).toBeGreaterThanOrEqual(firstRow.x + firstRow.width);
|
||||
});
|
||||
await closeMobileAgentSidebar(page);
|
||||
await expectMobileAgentSidebarHidden(page);
|
||||
});
|
||||
|
||||
test("places the close button beside the first nav row instead of over it", async ({ page }) => {
|
||||
await gotoAppShell(page);
|
||||
await openMobileAgentSidebar(page);
|
||||
await expectMobileAgentSidebarVisible(page);
|
||||
|
||||
const firstRow = await page.getByTestId("sidebar-global-new-workspace").boundingBox();
|
||||
const closeButton = await page.getByTestId("sidebar-close").boundingBox();
|
||||
if (!firstRow || !closeButton) throw new Error("first nav row and close button must render");
|
||||
expect(closeButton.x).toBeGreaterThanOrEqual(firstRow.x + firstRow.width);
|
||||
});
|
||||
|
||||
test("keeps a pinned workspace rendered while the retained sidebar is closed", async ({
|
||||
page,
|
||||
}) => {
|
||||
@@ -587,15 +523,14 @@ test.describe("Half-screen desktop layout", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("keeps the pinned sidebar at half of a 14-inch Mac display", async ({ page }) => {
|
||||
await gotoAppShell(page);
|
||||
await expect(page.getByTestId("sidebar-global-new-workspace")).toBeVisible();
|
||||
await expect(page.getByTestId("agent-list-backdrop")).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("keeps the left toggle center-owned without left window controls", async ({ page }) => {
|
||||
await gotoAppShell(page);
|
||||
|
||||
await test.step("start with pinned navigation at half of a 14-inch Mac display", async () => {
|
||||
await expect(page.getByTestId("sidebar-global-new-workspace")).toBeVisible();
|
||||
await expect(page.getByTestId("agent-list-backdrop")).not.toBeVisible();
|
||||
});
|
||||
|
||||
const openToggle = page.getByTestId("menu-button");
|
||||
const openIcon = openToggle.locator("svg").first();
|
||||
await expect(openIcon).toBeVisible();
|
||||
|
||||
@@ -477,49 +477,59 @@ for (const shape of ["seven-day-only", "Spark"] as const) {
|
||||
});
|
||||
}
|
||||
|
||||
for (const theme of ["light", "dark"] as const) {
|
||||
for (const [size, viewport] of Object.entries({
|
||||
desktop: { width: 1440, height: 900 },
|
||||
compact: { width: 390, height: 844 },
|
||||
})) {
|
||||
test(`Usage Settings cog ${size} ${theme}`, async ({ page }, testInfo) => {
|
||||
test.setTimeout(120_000);
|
||||
await page.addInitScript((value) => {
|
||||
const key = "@paseo:app-settings";
|
||||
const current = JSON.parse(localStorage.getItem(key) ?? "{}");
|
||||
localStorage.setItem(key, JSON.stringify({ ...current, theme: value }));
|
||||
}, theme);
|
||||
const usage = await installUsageReportsFixture(page, {
|
||||
lists: [() => claudeAndCodexReports()],
|
||||
});
|
||||
await page.setViewportSize(viewport);
|
||||
await gotoAppShell(page);
|
||||
if (size === "compact") await openCompactSidebar(page);
|
||||
await openUsageFromIcon(page);
|
||||
const screen = page.getByTestId(`usage-host-${getServerId()}`);
|
||||
await expect(screen.getByText("Claude", { exact: true })).toBeVisible({ timeout: 30_000 });
|
||||
const settings = page.getByTestId("usage-options-menu");
|
||||
await expect(settings).toHaveAccessibleName("Settings");
|
||||
await expect(page.getByTestId("usage-display-used")).toHaveCount(0);
|
||||
await captureSettingsState(page, testInfo, `${size}-${theme}-cog`);
|
||||
for (const [size, viewport] of Object.entries({
|
||||
desktop: { width: 1440, height: 900 },
|
||||
compact: { width: 390, height: 844 },
|
||||
})) {
|
||||
test(`Usage Settings cog ${size} in both themes`, async ({ page }, testInfo) => {
|
||||
test.setTimeout(120_000);
|
||||
await page.addInitScript(() => {
|
||||
const key = "@paseo:app-settings";
|
||||
const current = JSON.parse(localStorage.getItem(key) ?? "{}");
|
||||
localStorage.setItem(key, JSON.stringify({ ...current, theme: "auto" }));
|
||||
});
|
||||
await page.emulateMedia({ colorScheme: "light" });
|
||||
const usage = await installUsageReportsFixture(page, {
|
||||
lists: [() => claudeAndCodexReports()],
|
||||
});
|
||||
await page.setViewportSize(viewport);
|
||||
await gotoAppShell(page);
|
||||
if (size === "compact") await openCompactSidebar(page);
|
||||
await openUsageFromIcon(page);
|
||||
const screen = page.getByTestId(`usage-host-${getServerId()}`);
|
||||
await expect(screen.getByText("Claude", { exact: true })).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
await openUsageOptions(page);
|
||||
await expectUsageOptionsFitContent(page);
|
||||
await expect(page.getByText("Pinned windows show in the sidebar footer")).toBeVisible();
|
||||
await expect(page.getByTestId("usage-display-used")).toHaveAttribute("aria-selected", "true");
|
||||
await captureSettingsState(page, testInfo, `${size}-${theme}-settings`);
|
||||
await showUsageAs(page, "remaining");
|
||||
await expect(screen.getByText("69% left")).toBeVisible();
|
||||
await showUsageAs(page, "used");
|
||||
await expect(pinRow(screen, "Claude", "Session")).toHaveAccessibleName(
|
||||
/^Pin Claude Session, 31% · resets /,
|
||||
);
|
||||
await openUsageOptions(page);
|
||||
await closeUsageOptions(page);
|
||||
for (const theme of ["light", "dark"] as const) {
|
||||
await test.step(`the cog and its settings work in ${theme}`, async () => {
|
||||
await page.emulateMedia({ colorScheme: theme });
|
||||
const settings = page.getByTestId("usage-options-menu");
|
||||
await expect(settings).toHaveAccessibleName("Settings");
|
||||
await expect(page.getByTestId("usage-display-used")).toHaveCount(0);
|
||||
await captureSettingsState(page, testInfo, `${size}-${theme}-cog`);
|
||||
|
||||
await openUsageOptions(page);
|
||||
await expectUsageOptionsFitContent(page);
|
||||
await expect(page.getByText("Pinned windows show in the sidebar footer")).toBeVisible();
|
||||
await expect(page.getByTestId("usage-display-used")).toHaveAttribute(
|
||||
"aria-selected",
|
||||
"true",
|
||||
);
|
||||
await captureSettingsState(page, testInfo, `${size}-${theme}-settings`);
|
||||
await showUsageAs(page, "remaining");
|
||||
await expect(screen.getByText("69% left")).toBeVisible();
|
||||
await showUsageAs(page, "used");
|
||||
await expect(pinRow(screen, "Claude", "Session")).toHaveAccessibleName(
|
||||
/^Pin Claude Session, 31% · resets /,
|
||||
);
|
||||
await openUsageOptions(page);
|
||||
await closeUsageOptions(page);
|
||||
});
|
||||
}
|
||||
await test.step("refresh still works after changing the display settings", async () => {
|
||||
await refreshAllUsage(page);
|
||||
await expect.poll(() => forcedRefreshes(usage)).toEqual([{ forceRefresh: true }]);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function captureSettingsState(
|
||||
|
||||
@@ -42,7 +42,7 @@ async function seedViewedTimelineScenario(
|
||||
config: ViewedAgentConfig = {
|
||||
provider: "mock",
|
||||
modeId: "load-test",
|
||||
model: "ten-second-stream",
|
||||
model: "e2e-fast-stream",
|
||||
},
|
||||
) =>
|
||||
workspace.client.createAgent({
|
||||
|
||||
Reference in New Issue
Block a user