mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-30 05:02:58 +00:00
636 lines
26 KiB
TypeScript
636 lines
26 KiB
TypeScript
import { createMockIdleAgent, openWorkspaceWithAgents } from "../support/helpers/archive-tab";
|
|
import { seedWorkspace } from "../support/helpers/seed-client";
|
|
import { expect, test, type Page } from "../support/fixtures";
|
|
import {
|
|
expectNearBottom,
|
|
scrollAgentChatToBottom,
|
|
waitForScrollableChat,
|
|
} from "../support/helpers/agent-bottom-anchor";
|
|
import { awaitAssistantMessage } from "../support/helpers/agent-stream";
|
|
import {
|
|
attachComposerTestImage,
|
|
pasteComposerTestImage,
|
|
enterComposerFullscreen,
|
|
exitComposerFullscreen,
|
|
resizeComposerViewport,
|
|
selectComposerDraftRange,
|
|
expectComposerDraftSelection,
|
|
expectComposerFullscreenControls,
|
|
sendComposerWithButton,
|
|
queueComposerWithButton,
|
|
configureComposerQueueSend,
|
|
activateRetainedComposerTab,
|
|
switchComposerTabWithKeyboard,
|
|
expectWorkspaceHeaderHidden,
|
|
expectWorkspaceHeaderVisible,
|
|
recordVisibleComposerGeometry,
|
|
expectNoCollapsedVisibleComposer,
|
|
expectAttachmentPill,
|
|
expectQueuedMessageButton,
|
|
startRunningMockAgent,
|
|
composerLocator,
|
|
expectComposerVisible,
|
|
expectComposerFullscreenFitsVisibleViewport,
|
|
} from "../support/helpers/composer";
|
|
import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent";
|
|
import {
|
|
closeMobileAgentSidebar,
|
|
expectMobileAgentSidebarHidden,
|
|
expectMobileAgentSidebarVisible,
|
|
swipeOpenMobileAgentSidebar,
|
|
} from "../support/helpers/sidebar";
|
|
|
|
async function composerHeight(page: Page): Promise<number> {
|
|
return composerLocator(page).evaluate((element) => element.getBoundingClientRect().height);
|
|
}
|
|
|
|
async function expectComposerHeight(page: Page, expected: number): Promise<void> {
|
|
await expect.poll(() => composerHeight(page)).toBe(expected);
|
|
}
|
|
|
|
async function pressComposerKeyAndMeasureNextPaint(page: Page, key: string): Promise<number> {
|
|
const composer = composerLocator(page);
|
|
await installNextComposerPaintProbe(page);
|
|
await composer.press(key);
|
|
return readNextComposerPaintHeight(page);
|
|
}
|
|
|
|
async function typeComposerTextAndMeasureNextPaint(page: Page, text: string): Promise<number> {
|
|
const composer = composerLocator(page);
|
|
await installNextComposerPaintProbe(page);
|
|
await composer.pressSequentially(text);
|
|
return readNextComposerPaintHeight(page);
|
|
}
|
|
|
|
async function installNextComposerPaintProbe(page: Page): Promise<void> {
|
|
await composerLocator(page).evaluate((element) => {
|
|
Reflect.set(
|
|
globalThis,
|
|
"__composerNextPaintHeight",
|
|
new Promise<number>((resolve) => {
|
|
element.addEventListener(
|
|
"input",
|
|
() => {
|
|
requestAnimationFrame(() => resolve(element.getBoundingClientRect().height));
|
|
},
|
|
{ once: true },
|
|
);
|
|
}),
|
|
);
|
|
});
|
|
}
|
|
|
|
async function readNextComposerPaintHeight(page: Page): Promise<number> {
|
|
return page.evaluate(() => Reflect.get(globalThis, "__composerNextPaintHeight"));
|
|
}
|
|
|
|
test("blank composer lines remain present and keep their measured height", async ({ page }) => {
|
|
const agent = await seedMockAgentWorkspace({
|
|
repoPrefix: "composer-whitespace-",
|
|
title: "Composer whitespace",
|
|
});
|
|
|
|
try {
|
|
await openAgentRoute(page, agent);
|
|
await expectComposerVisible(page);
|
|
const composer = composerLocator(page);
|
|
const blankLines = "\n\n\n\n\n";
|
|
const collapsedHeight = await composerHeight(page);
|
|
|
|
await test.step("remeasure the current draft when the window narrows", async () => {
|
|
await page.setViewportSize({ width: 1280, height: 1200 });
|
|
const text = "A sentence that wraps when the editor becomes narrower. ".repeat(8);
|
|
await composer.fill(text);
|
|
await expect(composer).toHaveValue(text);
|
|
await expect.poll(() => composerHeight(page)).toBeGreaterThan(collapsedHeight);
|
|
const wideHeight = await composerHeight(page);
|
|
await page.setViewportSize({ width: 480, height: 1200 });
|
|
await expect.poll(() => composerHeight(page)).toBeGreaterThan(wideHeight);
|
|
await expect(composer).toHaveValue(text);
|
|
await page.setViewportSize({ width: 1280, height: 720 });
|
|
});
|
|
|
|
await test.step("grow the composer with blank lines followed by text", async () => {
|
|
await composer.fill(`${blankLines}x`);
|
|
await expect(composer).toHaveValue(`${blankLines}x`);
|
|
await expect.poll(() => composerHeight(page)).toBeGreaterThan(collapsedHeight);
|
|
});
|
|
|
|
await test.step("delete only the text without losing the blank lines or height", async () => {
|
|
const expandedHeight = await composerHeight(page);
|
|
await composer.press("Backspace");
|
|
await expect(composer).toHaveValue(blankLines);
|
|
await expectComposerHeight(page, expandedHeight);
|
|
});
|
|
|
|
await test.step("grow on the first paint of a newline and stay stable when it receives a glyph", async () => {
|
|
await composer.fill("alpha\nbeta");
|
|
const filledLineHeight = await composerHeight(page);
|
|
const trailingLineHeight = await pressComposerKeyAndMeasureNextPaint(page, "Shift+Enter");
|
|
expect(trailingLineHeight).toBeGreaterThan(filledLineHeight);
|
|
|
|
const filledTrailingLineHeight = await typeComposerTextAndMeasureNextPaint(page, "x");
|
|
await expect(composer).toHaveValue("alpha\nbeta\nx");
|
|
expect(filledTrailingLineHeight).toBe(trailingLineHeight);
|
|
});
|
|
} finally {
|
|
await agent.cleanup();
|
|
}
|
|
});
|
|
|
|
test("composer growth keeps a bottom-pinned chat at the bottom", async ({ page }) => {
|
|
test.setTimeout(90_000);
|
|
const agent = await seedMockAgentWorkspace({
|
|
repoPrefix: "composer-bottom-anchor-",
|
|
title: "Composer bottom anchor",
|
|
initialPrompt: "Produce enough content to make the chat scrollable.",
|
|
model: "ten-second-stream",
|
|
});
|
|
|
|
try {
|
|
await openAgentRoute(page, agent);
|
|
await expectComposerVisible(page);
|
|
await awaitAssistantMessage(page);
|
|
await waitForScrollableChat(page, { minScrollableDistance: 200, timeout: 30_000 });
|
|
await scrollAgentChatToBottom(page);
|
|
|
|
const composer = composerLocator(page);
|
|
await composer.fill("alpha");
|
|
for (let line = 0; line < 8; line += 1) {
|
|
await composer.press("Shift+Enter");
|
|
}
|
|
|
|
await expectNearBottom(page);
|
|
} finally {
|
|
await agent.cleanup();
|
|
}
|
|
});
|
|
|
|
/** Freeze the page's timers and animation frames until `page.clock.resume()`. */
|
|
async function pausePageClock(page: Page): Promise<void> {
|
|
const now = Date.now();
|
|
await page.clock.install({ time: now });
|
|
// The installed clock advances between protocol calls; pause at a future time.
|
|
await page.clock.pauseAt(now + 60_000);
|
|
}
|
|
|
|
/** The send error shares the footer with Send, so whatever keeps Send visible keeps it visible. */
|
|
async function expectFullscreenErrorAboveSend(page: Page): Promise<void> {
|
|
const frame = page.getByTestId("composer-fullscreen");
|
|
const alertBox = (await frame.getByRole("alert").boundingBox())!;
|
|
const sendBox = (await page
|
|
.getByRole("button", { name: "Send message", exact: true })
|
|
.boundingBox())!;
|
|
const editorBox = (await composerLocator(page).boundingBox())!;
|
|
expect(alertBox.y).toBeGreaterThanOrEqual(editorBox.y + editorBox.height);
|
|
expect(alertBox.y + alertBox.height).toBeLessThanOrEqual(sendBox.y);
|
|
}
|
|
|
|
async function writeCompactDraft(page: Page, text: string): Promise<void> {
|
|
await composerLocator(page).fill(text);
|
|
await expect(composerLocator(page)).toHaveValue(text);
|
|
}
|
|
|
|
test("compact long drafts stop growing before the header", async ({ page }) => {
|
|
const agent = await seedMockAgentWorkspace({
|
|
repoPrefix: "composer-cap-",
|
|
title: "Composer cap",
|
|
});
|
|
try {
|
|
await resizeComposerViewport(page, "portrait");
|
|
await openAgentRoute(page, agent);
|
|
await expectComposerVisible(page);
|
|
await writeCompactDraft(page, "A long draft line\n".repeat(40));
|
|
await expect.poll(() => composerHeight(page)).toBeLessThanOrEqual(160);
|
|
} finally {
|
|
await agent.cleanup();
|
|
}
|
|
});
|
|
|
|
test("compact fullscreen editing preserves the live draft and selection and returns to chat after send", async ({
|
|
page,
|
|
}) => {
|
|
const agent = await seedMockAgentWorkspace({
|
|
repoPrefix: "composer-fullscreen-",
|
|
title: "Fullscreen composer",
|
|
});
|
|
try {
|
|
await resizeComposerViewport(page, "portrait");
|
|
await openAgentRoute(page, agent);
|
|
await expectComposerVisible(page);
|
|
await test.step("the affordance appears after two rendered lines, including soft wraps", async () => {
|
|
await attachComposerTestImage(page);
|
|
await expectAttachmentPill(page, "composer-image-attachment-pill");
|
|
await writeCompactDraft(page, "First line\nSecond line");
|
|
await expect(page.getByRole("button", { name: "Fullscreen", exact: true })).toHaveCount(0);
|
|
await writeCompactDraft(page, "First line\nSecond line\nThird line");
|
|
await expect(page.getByRole("button", { name: "Fullscreen", exact: true })).toBeVisible();
|
|
await writeCompactDraft(
|
|
page,
|
|
"A sentence with enough words to wrap across several rendered lines in a narrow input. ".repeat(
|
|
4,
|
|
),
|
|
);
|
|
await expect(page.getByRole("button", { name: "Fullscreen", exact: true })).toBeVisible();
|
|
});
|
|
await test.step("fullscreen covers the header and keeps the live draft and selection", async () => {
|
|
await selectComposerDraftRange(page, 4, 17);
|
|
await enterComposerFullscreen(page);
|
|
await expectComposerFullscreenControls(page);
|
|
await expectWorkspaceHeaderHidden(page);
|
|
await expectComposerDraftSelection(page, 4, 17);
|
|
await expect(page.getByTestId("composer-image-attachment-pill")).toHaveCount(0);
|
|
await writeCompactDraft(page, "Edited fullscreen draft\nSecond line\nThird line");
|
|
await selectComposerDraftRange(page, 3, 12);
|
|
});
|
|
await test.step("exit restores the inline composer with the fullscreen edits", async () => {
|
|
await exitComposerFullscreen(page);
|
|
await expectWorkspaceHeaderVisible(page);
|
|
await expect(composerLocator(page)).toHaveValue(
|
|
"Edited fullscreen draft\nSecond line\nThird line",
|
|
);
|
|
await expectComposerDraftSelection(page, 3, 12);
|
|
await expectAttachmentPill(page, "composer-image-attachment-pill");
|
|
await expect(composerLocator(page)).toBeFocused();
|
|
});
|
|
await test.step("sending inline after a fullscreen edit clears the visible editor", async () => {
|
|
await sendComposerWithButton(page);
|
|
await expect(composerLocator(page)).toHaveValue("");
|
|
await expect(page.getByTestId("composer-image-attachment-pill")).toHaveCount(0);
|
|
await writeCompactDraft(page, "Typed after the fullscreen cycle");
|
|
await sendComposerWithButton(page);
|
|
await expect(composerLocator(page)).toHaveValue("");
|
|
});
|
|
await test.step("sending inside fullscreen returns to chat and clears the editor", async () => {
|
|
await writeCompactDraft(page, "First line\nSecond line\nThird line");
|
|
await enterComposerFullscreen(page);
|
|
await writeCompactDraft(page, "Short draft");
|
|
await expectComposerFullscreenControls(page);
|
|
await selectComposerDraftRange(page, 0, 5);
|
|
await resizeComposerViewport(page, "landscape");
|
|
await expectComposerFullscreenControls(page);
|
|
await expectComposerDraftSelection(page, 0, 5);
|
|
await expect(composerLocator(page)).toHaveValue("Short draft");
|
|
await expect(composerLocator(page)).toBeFocused();
|
|
await resizeComposerViewport(page, "portrait");
|
|
await sendComposerWithButton(page);
|
|
await expect(composerLocator(page)).toHaveValue("");
|
|
await expectWorkspaceHeaderVisible(page);
|
|
await expect(page.getByTestId("composer-fullscreen")).toHaveCount(0);
|
|
});
|
|
await test.step("typing and sending keep working after leaving fullscreen again", async () => {
|
|
await writeCompactDraft(page, "First line\nSecond line\nThird line");
|
|
await enterComposerFullscreen(page);
|
|
await writeCompactDraft(page, "Typed inside fullscreen");
|
|
await expect(page.getByRole("button", { name: "Send message", exact: true })).toBeEnabled();
|
|
await exitComposerFullscreen(page);
|
|
await expectWorkspaceHeaderVisible(page);
|
|
await expect(composerLocator(page)).toHaveValue("Typed inside fullscreen");
|
|
await writeCompactDraft(page, "Typed inside fullscreen, finished inline");
|
|
await sendComposerWithButton(page);
|
|
await expect(composerLocator(page)).toHaveValue("");
|
|
await writeCompactDraft(page, "One more inline message");
|
|
await sendComposerWithButton(page);
|
|
await expect(composerLocator(page)).toHaveValue("");
|
|
});
|
|
} finally {
|
|
await agent.cleanup();
|
|
}
|
|
});
|
|
|
|
test("a caret inside an unpublished edit survives a breakpoint remount", async ({ page }) => {
|
|
const agent = await seedMockAgentWorkspace({
|
|
repoPrefix: "composer-remount-caret-",
|
|
title: "Remount caret",
|
|
});
|
|
try {
|
|
await resizeComposerViewport(page, "portrait");
|
|
await openAgentRoute(page, agent);
|
|
await expectComposerVisible(page);
|
|
await writeCompactDraft(page, "Short draft");
|
|
await selectComposerDraftRange(page, 5, 5);
|
|
// The draft owner publishes web edits after the next paint. A rotation inside that
|
|
// window remounts the editor before the edit has reached the draft store; the paused
|
|
// clock holds the page there instead of racing the frame.
|
|
await pausePageClock(page);
|
|
await composerLocator(page).press("X");
|
|
await resizeComposerViewport(page, "landscape");
|
|
await page.clock.resume();
|
|
await expect(composerLocator(page)).toHaveValue("ShortX draft");
|
|
await expectComposerDraftSelection(page, 6, 6);
|
|
await expect(composerLocator(page)).toBeFocused();
|
|
await composerLocator(page).press("Y");
|
|
await expect(composerLocator(page)).toHaveValue("ShortXY draft");
|
|
} finally {
|
|
await agent.cleanup();
|
|
}
|
|
});
|
|
|
|
test("fullscreen submit rejection retains the editable draft and supports retry", async ({
|
|
page,
|
|
}) => {
|
|
const agent = await seedMockAgentWorkspace({
|
|
repoPrefix: "composer-fullscreen-retry-",
|
|
title: "Fullscreen retry",
|
|
featureValues: { mockPromptRejections: 1 },
|
|
});
|
|
const draft = "First line\nSecond line\nThird line";
|
|
try {
|
|
await resizeComposerViewport(page, "portrait");
|
|
await openAgentRoute(page, agent);
|
|
await expectComposerVisible(page);
|
|
await writeCompactDraft(page, draft);
|
|
await enterComposerFullscreen(page);
|
|
await sendComposerWithButton(page);
|
|
await expectWorkspaceHeaderVisible(page);
|
|
await expect(page.getByRole("alert")).toHaveText("Requested mock prompt rejection");
|
|
await enterComposerFullscreen(page);
|
|
await expectFullscreenErrorAboveSend(page);
|
|
await expect(composerLocator(page)).toHaveValue(draft);
|
|
await expect(composerLocator(page)).toBeEditable();
|
|
await expect(page.getByRole("button", { name: "Send message", exact: true })).toBeEnabled();
|
|
await sendComposerWithButton(page);
|
|
await expect(composerLocator(page)).toHaveValue("");
|
|
await expectWorkspaceHeaderVisible(page);
|
|
} finally {
|
|
await agent.cleanup();
|
|
}
|
|
});
|
|
|
|
async function blurComposer(page: Page): Promise<void> {
|
|
await composerLocator(page).blur();
|
|
await expect(composerLocator(page)).not.toBeFocused();
|
|
}
|
|
|
|
/** Mid-band at the given height and near the editor's bottom edge. */
|
|
async function reservedBandPoints(
|
|
page: Page,
|
|
bandX: number,
|
|
topY: number,
|
|
): Promise<Array<{ x: number; y: number }>> {
|
|
const editor = (await composerLocator(page).boundingBox())!;
|
|
return [
|
|
{ x: bandX, y: topY },
|
|
{ x: bandX, y: editor.y + editor.height - 6 },
|
|
];
|
|
}
|
|
|
|
test("the reserved band beside the inline editor focuses it", async ({ page }) => {
|
|
const agent = await seedMockAgentWorkspace({
|
|
repoPrefix: "composer-band-focus-",
|
|
title: "Composer band focus",
|
|
});
|
|
try {
|
|
await resizeComposerViewport(page, "portrait");
|
|
await openAgentRoute(page, agent);
|
|
await expectComposerVisible(page);
|
|
const exitButton = page.getByRole("button", { name: "Exit fullscreen", exact: true });
|
|
const fullscreenButton = page.getByRole("button", { name: "Fullscreen", exact: true });
|
|
const longDraft = "Band first line\nBand second line\nBand third line";
|
|
await writeCompactDraft(page, longDraft);
|
|
await expect(fullscreenButton).toBeVisible();
|
|
const button = (await fullscreenButton.boundingBox())!;
|
|
const bandX = button.x + button.width / 2;
|
|
|
|
await test.step("with the fullscreen button shown, the band under it focuses the editor", async () => {
|
|
for (const point of await reservedBandPoints(page, bandX, button.y + button.height + 10)) {
|
|
await blurComposer(page);
|
|
await page.mouse.click(point.x, point.y);
|
|
await expect(composerLocator(page)).toBeFocused();
|
|
await expect(composerLocator(page)).toHaveValue(longDraft);
|
|
await expect(exitButton).toHaveCount(0);
|
|
}
|
|
});
|
|
|
|
await test.step("the button itself still toggles fullscreen", async () => {
|
|
await fullscreenButton.click();
|
|
await expectComposerFullscreenControls(page);
|
|
await exitComposerFullscreen(page);
|
|
await expect(exitButton).toHaveCount(0);
|
|
await expect(composerLocator(page)).toHaveValue(longDraft);
|
|
});
|
|
|
|
await test.step("with no fullscreen button, the whole band still focuses the editor", async () => {
|
|
await writeCompactDraft(page, "Short");
|
|
await expect(fullscreenButton).toHaveCount(0);
|
|
const editor = (await composerLocator(page).boundingBox())!;
|
|
for (const point of await reservedBandPoints(page, bandX, editor.y + 6)) {
|
|
await blurComposer(page);
|
|
await page.mouse.click(point.x, point.y);
|
|
await expect(composerLocator(page)).toBeFocused();
|
|
await expect(composerLocator(page)).toHaveValue("Short");
|
|
await expect(exitButton).toHaveCount(0);
|
|
}
|
|
});
|
|
} finally {
|
|
await agent.cleanup();
|
|
}
|
|
});
|
|
|
|
test("fullscreen Send keeps the configured queue behavior while an agent runs", async ({
|
|
page,
|
|
}) => {
|
|
await configureComposerQueueSend(page);
|
|
await resizeComposerViewport(page, "portrait");
|
|
const agent = await startRunningMockAgent(page, {
|
|
prefix: "composer-fullscreen-queue-",
|
|
model: "one-minute-stream",
|
|
prompt: "Stay running for fullscreen queue test.",
|
|
});
|
|
try {
|
|
await writeCompactDraft(page, "Queued first line\nSecond line\nThird line");
|
|
await enterComposerFullscreen(page);
|
|
await expectComposerFullscreenControls(page);
|
|
await queueComposerWithButton(page);
|
|
await expect(composerLocator(page)).toHaveValue("");
|
|
await expectWorkspaceHeaderVisible(page);
|
|
await expectQueuedMessageButton(page);
|
|
} finally {
|
|
await agent.cleanup();
|
|
}
|
|
});
|
|
|
|
test("image paste stays attached to the active editor across fullscreen entry and exit", async ({
|
|
page,
|
|
}) => {
|
|
const agent = await seedMockAgentWorkspace({
|
|
repoPrefix: "composer-paste-remount-",
|
|
title: "Composer paste",
|
|
});
|
|
try {
|
|
await resizeComposerViewport(page, "portrait");
|
|
await openAgentRoute(page, agent);
|
|
await expectComposerVisible(page);
|
|
await writeCompactDraft(page, "First line\nSecond line\nThird line");
|
|
expect(await pasteComposerTestImage(page)).toBe(true);
|
|
await expect(page.getByTestId("composer-image-attachment-pill")).toHaveCount(1);
|
|
await enterComposerFullscreen(page);
|
|
expect(await pasteComposerTestImage(page)).toBe(true);
|
|
await exitComposerFullscreen(page);
|
|
await expect(page.getByTestId("composer-image-attachment-pill")).toHaveCount(2);
|
|
expect(await pasteComposerTestImage(page)).toBe(true);
|
|
await expect(page.getByTestId("composer-image-attachment-pill")).toHaveCount(3);
|
|
} finally {
|
|
await agent.cleanup();
|
|
}
|
|
});
|
|
|
|
test("retained agent tabs preserve fullscreen editing and paste when switched away and back", async ({
|
|
page,
|
|
}) => {
|
|
const workspace = await seedWorkspace({ repoPrefix: "composer-retained-fullscreen-" });
|
|
try {
|
|
const first = await createMockIdleAgent(workspace.client, {
|
|
cwd: workspace.repoPath,
|
|
workspaceId: workspace.workspaceId,
|
|
title: "First retained chat",
|
|
});
|
|
const second = await createMockIdleAgent(workspace.client, {
|
|
cwd: workspace.repoPath,
|
|
workspaceId: workspace.workspaceId,
|
|
title: "Second retained chat",
|
|
});
|
|
await openWorkspaceWithAgents(page, [first, second]);
|
|
await resizeComposerViewport(page, "portrait");
|
|
const draft = "Live first line\nLive second line\nLive third line";
|
|
await writeCompactDraft(page, draft);
|
|
await enterComposerFullscreen(page);
|
|
await expectWorkspaceHeaderHidden(page);
|
|
await selectComposerDraftRange(page, 4, 17);
|
|
// Fullscreen covers the compact header, so a phone leaves the tab by exiting first;
|
|
// a desktop-width window still has the tab shortcut.
|
|
await resizeComposerViewport(page, "landscape");
|
|
await switchComposerTabWithKeyboard(page, "previous");
|
|
await expect(page.getByRole("button", { name: "Exit fullscreen", exact: true })).toHaveCount(0);
|
|
// The exit action belongs to the active pane: Escape here leaves the hidden tab alone.
|
|
await composerLocator(page).press("Escape");
|
|
await switchComposerTabWithKeyboard(page, "next");
|
|
await expectComposerFullscreenControls(page);
|
|
await switchComposerTabWithKeyboard(page, "previous");
|
|
await expect(page.getByRole("button", { name: "Exit fullscreen", exact: true })).toHaveCount(0);
|
|
await resizeComposerViewport(page, "portrait");
|
|
await expectWorkspaceHeaderVisible(page);
|
|
await writeCompactDraft(page, "Other tab draft");
|
|
await shrinkVisibleViewport(page);
|
|
await activateRetainedComposerTab(page, second.title);
|
|
await expectComposerFullscreenControls(page);
|
|
await expectWorkspaceHeaderHidden(page);
|
|
await expect(composerLocator(page)).toHaveValue(draft);
|
|
await expectComposerFullscreenFitsVisibleViewport(page);
|
|
await expectComposerDraftSelection(page, 4, 17);
|
|
await expect(composerLocator(page)).toBeFocused();
|
|
expect(await pasteComposerTestImage(page)).toBe(true);
|
|
await exitComposerFullscreen(page);
|
|
await expectAttachmentPill(page, "composer-image-attachment-pill");
|
|
} finally {
|
|
await workspace.cleanup();
|
|
}
|
|
});
|
|
|
|
test("the compact sidebar over a fullscreen editor leaves the exit action alone", async ({
|
|
page,
|
|
}) => {
|
|
const agent = await seedMockAgentWorkspace({
|
|
repoPrefix: "composer-fullscreen-sidebar-",
|
|
title: "Fullscreen under sidebar",
|
|
});
|
|
try {
|
|
await resizeComposerViewport(page, "portrait");
|
|
await openAgentRoute(page, agent);
|
|
const draft = "Line one\nLine two\nLine three";
|
|
await writeCompactDraft(page, draft);
|
|
await enterComposerFullscreen(page);
|
|
await expectWorkspaceHeaderHidden(page);
|
|
// Fullscreen hides the header's menu button; the edge swipe still opens the list over it.
|
|
await swipeOpenMobileAgentSidebar(page);
|
|
await expectMobileAgentSidebarVisible(page);
|
|
// The list covers the pane, so the editor's exit action is not armed.
|
|
await page.keyboard.press("Escape");
|
|
await closeMobileAgentSidebar(page);
|
|
await expectMobileAgentSidebarHidden(page);
|
|
await expectComposerFullscreenControls(page);
|
|
await expect(composerLocator(page)).toHaveValue(draft);
|
|
await exitComposerFullscreen(page);
|
|
await expectWorkspaceHeaderVisible(page);
|
|
} finally {
|
|
await agent.cleanup();
|
|
}
|
|
});
|
|
|
|
test("retained inline composers keep usable capacity through hidden tab geometry", async ({
|
|
page,
|
|
}) => {
|
|
const workspace = await seedWorkspace({ repoPrefix: "composer-retained-capacity-" });
|
|
try {
|
|
const first = await createMockIdleAgent(workspace.client, {
|
|
cwd: workspace.repoPath,
|
|
workspaceId: workspace.workspaceId,
|
|
title: "First capacity chat",
|
|
});
|
|
const second = await createMockIdleAgent(workspace.client, {
|
|
cwd: workspace.repoPath,
|
|
workspaceId: workspace.workspaceId,
|
|
title: "Second capacity chat",
|
|
});
|
|
await openWorkspaceWithAgents(page, [first, second]);
|
|
await resizeComposerViewport(page, "portrait");
|
|
const draft = "Retained long line\n".repeat(40);
|
|
await writeCompactDraft(page, draft);
|
|
await recordVisibleComposerGeometry(page);
|
|
await activateRetainedComposerTab(page, first.title);
|
|
await writeCompactDraft(page, "Other visible draft");
|
|
await activateRetainedComposerTab(page, second.title);
|
|
await expect(composerLocator(page)).toHaveValue(draft);
|
|
await expect.poll(() => composerHeight(page)).toBeLessThanOrEqual(160);
|
|
await expect(page.getByRole("button", { name: "Send message", exact: true })).toBeVisible();
|
|
await expectNoCollapsedVisibleComposer(page);
|
|
} finally {
|
|
await workspace.cleanup();
|
|
}
|
|
});
|
|
|
|
async function shrinkVisibleViewport(page: Page): Promise<void> {
|
|
const cdp = await page.context().newCDPSession(page);
|
|
await cdp.send("Emulation.setPageScaleFactor", { pageScaleFactor: 2 });
|
|
await expect.poll(() => page.evaluate(() => window.visualViewport!.height)).toBe(422);
|
|
await cdp.detach();
|
|
}
|
|
|
|
async function panVisibleViewport(page: Page): Promise<void> {
|
|
const cdp = await page.context().newCDPSession(page);
|
|
await cdp.send("Input.dispatchTouchEvent", {
|
|
type: "touchStart",
|
|
touchPoints: [{ x: 4, y: 300 }],
|
|
});
|
|
for (let y = 290; y >= 100; y -= 10) {
|
|
await cdp.send("Input.dispatchTouchEvent", {
|
|
type: "touchMove",
|
|
touchPoints: [{ x: 4, y }],
|
|
});
|
|
}
|
|
await cdp.send("Input.dispatchTouchEvent", { type: "touchEnd", touchPoints: [] });
|
|
await expect.poll(() => page.evaluate(() => window.visualViewport!.offsetTop)).toBeGreaterThan(0);
|
|
await cdp.detach();
|
|
}
|
|
|
|
test("fullscreen Send follows the visible viewport when it shrinks and pans", async ({ page }) => {
|
|
const agent = await seedMockAgentWorkspace({
|
|
repoPrefix: "composer-visible-viewport-",
|
|
title: "Visible viewport composer",
|
|
});
|
|
try {
|
|
await resizeComposerViewport(page, "portrait");
|
|
await openAgentRoute(page, agent);
|
|
await expectComposerVisible(page);
|
|
await writeCompactDraft(page, "First line\nSecond line\nThird line");
|
|
await enterComposerFullscreen(page);
|
|
await expectComposerFullscreenFitsVisibleViewport(page);
|
|
await shrinkVisibleViewport(page);
|
|
await expectComposerFullscreenFitsVisibleViewport(page);
|
|
await panVisibleViewport(page);
|
|
await expectComposerFullscreenFitsVisibleViewport(page);
|
|
} finally {
|
|
await agent.cleanup();
|
|
}
|
|
});
|