Skip unrelated required checks without losing their names (#2542)
* fix(ci): skip unrelated required checks cleanly Matrix jobs had to start no-op runners to preserve interpolated required-check names. Static named jobs let GitHub report genuine skipped conclusions while keeping those names stable. * fix(ci): gate changes to install patching script * fix(ci): route cross-package test contracts narrowly * fix(ci): keep routed contracts merge-blocking * fix(server): exclude nested test utilities from builds * fix(ci): cover app-owned native and browser domains * refactor(ci): align tests with runtime domains Own browser specs, desktop specs, and Electron-only app code by stable directories so PR routing does not depend on filename conventions. Keep desktop integration coverage inside the required desktop check. * fix(ci): keep routing and desktop suites isolated The routing regression must run before dependency installation, and desktop Vitest must not collect Playwright-owned specs. * fix(ci): isolate desktop e2e runtime * perf(test): isolate Playwright workers * perf(test): parallelize Playwright within specs * fix(test): keep Playwright file scheduling * fix(test): share dynamic Metro fixture * fix(test): preserve own-host restart fixtures * fix(ci): preserve routed contracts after rebase * Stabilize Playwright wheel-scroll assertions
This commit is contained in:
@@ -0,0 +1,609 @@
|
||||
import { expect, test, type Page } from "../support/fixtures";
|
||||
import { composerLocator, expectComposerVisible } from "../support/helpers/composer";
|
||||
import {
|
||||
openAgentRoute,
|
||||
seedMockAgentWorkspace,
|
||||
type MockAgentWorkspace,
|
||||
} from "../support/helpers/mock-agent";
|
||||
import { expectWorkspaceTabVisible, openSessions } from "../support/helpers/archive-tab";
|
||||
import { daemonWsRoutePattern } from "../support/helpers/daemon-port";
|
||||
import { getServerId } from "../support/helpers/server-id";
|
||||
import { switchWorkspaceViaSidebar } from "../support/helpers/workspace-ui";
|
||||
|
||||
const TEST_COMMANDS = [
|
||||
{
|
||||
name: "tdd",
|
||||
description: "Write a red test, verify it fails for the right reason, implement to green",
|
||||
argumentHint: "",
|
||||
},
|
||||
{
|
||||
name: "help",
|
||||
description: "Show help for the current agent session and available slash commands",
|
||||
argumentHint: "",
|
||||
},
|
||||
{
|
||||
name: "hello",
|
||||
description: "Insert a friendly greeting prompt into the current composer",
|
||||
argumentHint: "",
|
||||
},
|
||||
{
|
||||
name: "heapdump",
|
||||
description: "Dump the JavaScript heap for local desktop debugging",
|
||||
argumentHint: "",
|
||||
},
|
||||
{
|
||||
name: "health",
|
||||
description: "Show runtime health checks and connection diagnostics",
|
||||
argumentHint: "",
|
||||
},
|
||||
{
|
||||
name: "history",
|
||||
description: "Summarize recent session history",
|
||||
argumentHint: "",
|
||||
},
|
||||
{
|
||||
name: "handoff",
|
||||
description: "Prepare a complete handoff note for another agent",
|
||||
argumentHint: "[agent]",
|
||||
},
|
||||
{
|
||||
name: "hover",
|
||||
description: "Audit hover behavior in desktop web surfaces",
|
||||
argumentHint: "",
|
||||
},
|
||||
{
|
||||
name: "harness",
|
||||
description: "Inspect the local test harness configuration",
|
||||
argumentHint: "",
|
||||
},
|
||||
{
|
||||
name: "hydrate",
|
||||
description: "Refresh persisted state used by the current workspace",
|
||||
argumentHint: "",
|
||||
},
|
||||
{
|
||||
name: "highlight",
|
||||
description: "Highlight important changes in the active diff",
|
||||
argumentHint: "",
|
||||
},
|
||||
{
|
||||
name: "home",
|
||||
description: "Navigate back to the workspace home surface",
|
||||
argumentHint: "",
|
||||
},
|
||||
{
|
||||
name: "host",
|
||||
description: "Inspect host connection metadata",
|
||||
argumentHint: "",
|
||||
},
|
||||
] as const;
|
||||
|
||||
interface PopoverFrame {
|
||||
exists: boolean;
|
||||
top: number;
|
||||
bottom: number;
|
||||
height: number;
|
||||
opacity: number;
|
||||
display: string;
|
||||
visibility: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface PopoverFrameRecorderWindow extends Window {
|
||||
__composerAutocompleteFrames?: PopoverFrame[];
|
||||
__stopComposerAutocompleteFrameRecorder?: () => void;
|
||||
}
|
||||
|
||||
async function getTopTestIdAtPoint(page: Page, x: number, y: number) {
|
||||
return page.evaluate(
|
||||
([pointX, pointY]) => {
|
||||
const element = document.elementFromPoint(pointX, pointY);
|
||||
return element?.closest("[data-testid]")?.getAttribute("data-testid") ?? null;
|
||||
},
|
||||
[x, y],
|
||||
);
|
||||
}
|
||||
|
||||
async function installListCommandsStub(page: Page): Promise<void> {
|
||||
await page.routeWebSocket(daemonWsRoutePattern(), (ws) => {
|
||||
const server = ws.connectToServer();
|
||||
|
||||
ws.onMessage((message) => {
|
||||
server.send(message);
|
||||
});
|
||||
|
||||
server.onMessage((message) => {
|
||||
if (typeof message !== "string") {
|
||||
ws.send(message);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(message) as {
|
||||
type?: string;
|
||||
message?: {
|
||||
type?: string;
|
||||
payload?: {
|
||||
commands?: unknown;
|
||||
error?: string | null;
|
||||
};
|
||||
};
|
||||
};
|
||||
if (
|
||||
parsed.type === "session" &&
|
||||
parsed.message?.type === "list_commands_response" &&
|
||||
parsed.message.payload
|
||||
) {
|
||||
parsed.message.payload.commands = TEST_COMMANDS;
|
||||
parsed.message.payload.error = null;
|
||||
ws.send(JSON.stringify(parsed));
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// Forward non-JSON frames unchanged.
|
||||
}
|
||||
|
||||
ws.send(message);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function openAppWideNewWorkspace(page: Page): Promise<void> {
|
||||
await page.getByTestId("sidebar-global-new-workspace").first().click();
|
||||
await page.waitForURL((url) => url.pathname === "/new", { timeout: 30_000 });
|
||||
}
|
||||
|
||||
async function openSettingsThenBackToWorkspace(page: Page): Promise<void> {
|
||||
await page.getByTestId("sidebar-settings").filter({ visible: true }).first().click();
|
||||
await expect(page).toHaveURL(/\/settings\/general$/, { timeout: 30_000 });
|
||||
await page.getByTestId("settings-back-to-workspace").click();
|
||||
await page.waitForURL((url) => url.pathname.includes("/workspace/"), { timeout: 30_000 });
|
||||
}
|
||||
|
||||
async function expectSingleCurrentWorkspaceDeckEntry(
|
||||
page: Page,
|
||||
input: { expectedDeckEntryCount: number; serverId: string; workspaceId: string },
|
||||
): Promise<void> {
|
||||
const summary = await page
|
||||
.locator('[data-testid^="workspace-deck-entry-"]')
|
||||
.evaluateAll((elements, target) => {
|
||||
const currentTestId = `workspace-deck-entry-${target.serverId}:${target.workspaceId}`;
|
||||
const entries = elements.map((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return {
|
||||
testId: element.getAttribute("data-testid"),
|
||||
hasLayout: rect.width > 0 && rect.height > 0,
|
||||
};
|
||||
});
|
||||
const currentEntries = entries.filter((entry) => entry.testId === currentTestId);
|
||||
return {
|
||||
totalDeckEntryCount: entries.length,
|
||||
currentWorkspaceEntryCount: currentEntries.length,
|
||||
visibleCurrentWorkspaceEntryCount: currentEntries.filter((entry) => entry.hasLayout).length,
|
||||
hiddenCurrentWorkspaceEntryCount: currentEntries.filter((entry) => !entry.hasLayout).length,
|
||||
};
|
||||
}, input);
|
||||
|
||||
expect(summary).toEqual({
|
||||
totalDeckEntryCount: input.expectedDeckEntryCount,
|
||||
currentWorkspaceEntryCount: 1,
|
||||
visibleCurrentWorkspaceEntryCount: 1,
|
||||
hiddenCurrentWorkspaceEntryCount: 0,
|
||||
});
|
||||
}
|
||||
|
||||
async function openReadyMockAgent(
|
||||
page: Page,
|
||||
options?: { expectWorkspaceTab?: boolean },
|
||||
): Promise<{
|
||||
cleanup: () => Promise<void>;
|
||||
}> {
|
||||
const session = await seedMockAgentWorkspace({
|
||||
repoPrefix: "autocomplete-popover-",
|
||||
title: "Autocomplete popover regression",
|
||||
});
|
||||
|
||||
try {
|
||||
await openAgentRoute(page, session);
|
||||
if (options?.expectWorkspaceTab !== false) {
|
||||
await expectWorkspaceTabVisible(page, session.agentId);
|
||||
}
|
||||
await expectComposerVisible(page);
|
||||
return { cleanup: session.cleanup };
|
||||
} catch (error) {
|
||||
await session.cleanup();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function visiblePopoverBox(
|
||||
page: Page,
|
||||
): Promise<{ top: number; bottom: number; height: number }> {
|
||||
const popover = page.getByTestId("composer-autocomplete-popover");
|
||||
await expect(popover).toBeVisible({ timeout: 30_000 });
|
||||
await expect
|
||||
.poll(
|
||||
async () =>
|
||||
popover.evaluate((element) => {
|
||||
const style = window.getComputedStyle(element);
|
||||
return Number(style.opacity);
|
||||
}),
|
||||
{ timeout: 30_000 },
|
||||
)
|
||||
.toBeGreaterThan(0.95);
|
||||
const box = await popover.boundingBox();
|
||||
if (!box) {
|
||||
throw new Error("Autocomplete popover did not produce a bounding box.");
|
||||
}
|
||||
return {
|
||||
top: box.y,
|
||||
bottom: box.y + box.height,
|
||||
height: box.height,
|
||||
};
|
||||
}
|
||||
|
||||
async function startPopoverFrameRecorder(page: Page): Promise<void> {
|
||||
await page.evaluate(() => {
|
||||
const win = window as PopoverFrameRecorderWindow;
|
||||
win.__composerAutocompleteFrames = [];
|
||||
let active = true;
|
||||
|
||||
const record = () => {
|
||||
if (!active) return;
|
||||
|
||||
const element = document.querySelector('[data-testid="composer-autocomplete-popover"]');
|
||||
if (element instanceof HTMLElement) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(element);
|
||||
win.__composerAutocompleteFrames?.push({
|
||||
exists: true,
|
||||
top: rect.top,
|
||||
bottom: rect.bottom,
|
||||
height: rect.height,
|
||||
opacity: Number(style.opacity),
|
||||
display: style.display,
|
||||
visibility: style.visibility,
|
||||
timestamp: performance.now(),
|
||||
});
|
||||
} else {
|
||||
win.__composerAutocompleteFrames?.push({
|
||||
exists: false,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
height: 0,
|
||||
opacity: 0,
|
||||
display: "none",
|
||||
visibility: "hidden",
|
||||
timestamp: performance.now(),
|
||||
});
|
||||
}
|
||||
|
||||
window.requestAnimationFrame(record);
|
||||
};
|
||||
|
||||
win.__stopComposerAutocompleteFrameRecorder = () => {
|
||||
active = false;
|
||||
};
|
||||
window.requestAnimationFrame(record);
|
||||
});
|
||||
}
|
||||
|
||||
async function stopPopoverFrameRecorder(page: Page): Promise<PopoverFrame[]> {
|
||||
return page.evaluate(() => {
|
||||
const win = window as PopoverFrameRecorderWindow;
|
||||
win.__stopComposerAutocompleteFrameRecorder?.();
|
||||
return win.__composerAutocompleteFrames ?? [];
|
||||
});
|
||||
}
|
||||
|
||||
function visiblePopoverFrames(frames: PopoverFrame[]): PopoverFrame[] {
|
||||
return frames.filter(
|
||||
(frame) =>
|
||||
frame.exists &&
|
||||
frame.height > 0 &&
|
||||
frame.opacity > 0.95 &&
|
||||
frame.display !== "none" &&
|
||||
frame.visibility !== "hidden",
|
||||
);
|
||||
}
|
||||
|
||||
function formatFrame(frame: PopoverFrame | undefined): string {
|
||||
if (!frame) return "none";
|
||||
return JSON.stringify({
|
||||
top: Math.round(frame.top),
|
||||
bottom: Math.round(frame.bottom),
|
||||
height: Math.round(frame.height),
|
||||
opacity: frame.opacity,
|
||||
});
|
||||
}
|
||||
|
||||
function expectPopoverFramesStable(frames: PopoverFrame[]): void {
|
||||
const visibleFrames = visiblePopoverFrames(frames);
|
||||
expect(visibleFrames.length).toBeGreaterThan(0);
|
||||
|
||||
const finalFrame = visibleFrames[visibleFrames.length - 1];
|
||||
const jumpingFrame = visibleFrames.find(
|
||||
(frame) =>
|
||||
Math.abs(frame.top - finalFrame.top) > 4 ||
|
||||
Math.abs(frame.bottom - finalFrame.bottom) > 4 ||
|
||||
Math.abs(frame.height - finalFrame.height) > 4,
|
||||
);
|
||||
|
||||
expect(
|
||||
jumpingFrame,
|
||||
`expected first visible popover paint to be stable; first=${formatFrame(
|
||||
visibleFrames[0],
|
||||
)} jumping=${formatFrame(jumpingFrame)} final=${formatFrame(finalFrame)}`,
|
||||
).toBeUndefined();
|
||||
}
|
||||
|
||||
function expectPopoverDoesNotDisappearAfterFirstVisible(frames: PopoverFrame[]): void {
|
||||
const firstVisibleIndex = frames.findIndex(
|
||||
(frame) =>
|
||||
frame.exists &&
|
||||
frame.height > 0 &&
|
||||
frame.opacity > 0.95 &&
|
||||
frame.display !== "none" &&
|
||||
frame.visibility !== "hidden",
|
||||
);
|
||||
expect(firstVisibleIndex).toBeGreaterThanOrEqual(0);
|
||||
|
||||
const hiddenFrame = frames
|
||||
.slice(firstVisibleIndex)
|
||||
.find((frame) => !frame.exists || frame.opacity < 0.95);
|
||||
expect(
|
||||
hiddenFrame,
|
||||
`expected mounted popover to stay visible while filtering; hidden=${formatFrame(hiddenFrame)}`,
|
||||
).toBeUndefined();
|
||||
}
|
||||
|
||||
test.describe("Composer autocomplete", () => {
|
||||
test("stays visible after returning from app-wide routes", async ({ page }) => {
|
||||
await installListCommandsStub(page);
|
||||
const serverId = getServerId();
|
||||
const sessions: MockAgentWorkspace[] = [];
|
||||
|
||||
try {
|
||||
sessions.push(
|
||||
await seedMockAgentWorkspace({
|
||||
repoPrefix: "autocomplete-new-route-a-",
|
||||
title: "Autocomplete new route A",
|
||||
}),
|
||||
);
|
||||
sessions.push(
|
||||
await seedMockAgentWorkspace({
|
||||
repoPrefix: "autocomplete-new-route-b-",
|
||||
title: "Autocomplete new route B",
|
||||
}),
|
||||
);
|
||||
sessions.push(
|
||||
await seedMockAgentWorkspace({
|
||||
repoPrefix: "autocomplete-new-route-c-",
|
||||
title: "Autocomplete new route C",
|
||||
}),
|
||||
);
|
||||
|
||||
const [first, second, third] = sessions;
|
||||
|
||||
await openAgentRoute(page, first);
|
||||
await expectComposerVisible(page, { timeout: 30_000 });
|
||||
|
||||
await openAppWideNewWorkspace(page);
|
||||
await switchWorkspaceViaSidebar({ page, serverId, workspaceId: second.workspaceId });
|
||||
await expectComposerVisible(page, { timeout: 30_000 });
|
||||
await expectSingleCurrentWorkspaceDeckEntry(page, {
|
||||
expectedDeckEntryCount: 2,
|
||||
serverId,
|
||||
workspaceId: second.workspaceId,
|
||||
});
|
||||
|
||||
await openSettingsThenBackToWorkspace(page);
|
||||
await expectComposerVisible(page, { timeout: 30_000 });
|
||||
await expectSingleCurrentWorkspaceDeckEntry(page, {
|
||||
expectedDeckEntryCount: 2,
|
||||
serverId,
|
||||
workspaceId: second.workspaceId,
|
||||
});
|
||||
|
||||
await openSessions(page);
|
||||
await switchWorkspaceViaSidebar({ page, serverId, workspaceId: third.workspaceId });
|
||||
await expectComposerVisible(page, { timeout: 30_000 });
|
||||
await expectSingleCurrentWorkspaceDeckEntry(page, {
|
||||
expectedDeckEntryCount: sessions.length,
|
||||
serverId,
|
||||
workspaceId: third.workspaceId,
|
||||
});
|
||||
|
||||
await openAppWideNewWorkspace(page);
|
||||
await switchWorkspaceViaSidebar({ page, serverId, workspaceId: first.workspaceId });
|
||||
await expectComposerVisible(page, { timeout: 30_000 });
|
||||
await expectSingleCurrentWorkspaceDeckEntry(page, {
|
||||
expectedDeckEntryCount: sessions.length,
|
||||
serverId,
|
||||
workspaceId: first.workspaceId,
|
||||
});
|
||||
|
||||
await composerLocator(page).fill("/");
|
||||
const popover = page
|
||||
.getByTestId("composer-autocomplete-popover")
|
||||
.filter({ hasText: "/help", visible: true })
|
||||
.first();
|
||||
await expect(popover).toBeInViewport({ timeout: 30_000 });
|
||||
} finally {
|
||||
await Promise.allSettled(sessions.map((session) => session.cleanup()));
|
||||
}
|
||||
});
|
||||
|
||||
test("does not flash at the wrong position on the first slash command paint", async ({
|
||||
page,
|
||||
}) => {
|
||||
await installListCommandsStub(page);
|
||||
const agent = await openReadyMockAgent(page);
|
||||
|
||||
try {
|
||||
const input = composerLocator(page);
|
||||
await expect(input).toBeEditable({ timeout: 30_000 });
|
||||
await input.click();
|
||||
|
||||
await startPopoverFrameRecorder(page);
|
||||
await page.keyboard.type("/");
|
||||
await expect(page.getByText("/help", { exact: true })).toBeVisible({ timeout: 30_000 });
|
||||
await page.waitForTimeout(250);
|
||||
const frames = await stopPopoverFrameRecorder(page);
|
||||
|
||||
expectPopoverFramesStable(frames);
|
||||
} finally {
|
||||
await agent.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("does not jump when deleting a slash command search", async ({ page }) => {
|
||||
await installListCommandsStub(page);
|
||||
const agent = await openReadyMockAgent(page);
|
||||
|
||||
try {
|
||||
const input = composerLocator(page);
|
||||
await expect(input).toBeEditable({ timeout: 30_000 });
|
||||
|
||||
await input.fill("/he");
|
||||
const popover = page.getByTestId("composer-autocomplete-popover");
|
||||
await expect(popover.getByText("/help", { exact: true }).first()).toBeVisible({
|
||||
timeout: 30_000,
|
||||
});
|
||||
const beforeDelete = await visiblePopoverBox(page);
|
||||
|
||||
await input.press("Backspace");
|
||||
await expect(input).toHaveValue("/h");
|
||||
await expect(popover.getByText("/history", { exact: true }).first()).toBeVisible({
|
||||
timeout: 30_000,
|
||||
});
|
||||
const afterDelete = await visiblePopoverBox(page);
|
||||
|
||||
expect(Math.abs(afterDelete.bottom - beforeDelete.bottom)).toBeLessThanOrEqual(4);
|
||||
expect(afterDelete.height).toBeGreaterThan(beforeDelete.height);
|
||||
} finally {
|
||||
await agent.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("shrinks to filtered slash command results without moving the bottom edge", async ({
|
||||
page,
|
||||
}) => {
|
||||
await installListCommandsStub(page);
|
||||
const agent = await openReadyMockAgent(page);
|
||||
|
||||
try {
|
||||
const input = composerLocator(page);
|
||||
await expect(input).toBeEditable({ timeout: 30_000 });
|
||||
|
||||
await input.fill("/");
|
||||
const popover = page.getByTestId("composer-autocomplete-popover");
|
||||
await expect(popover.getByText("/help", { exact: true }).first()).toBeVisible({
|
||||
timeout: 30_000,
|
||||
});
|
||||
const allCommands = await visiblePopoverBox(page);
|
||||
|
||||
await input.fill("/tdd");
|
||||
await expect(popover.getByText("/tdd", { exact: true }).first()).toBeVisible({
|
||||
timeout: 30_000,
|
||||
});
|
||||
const oneCommand = await visiblePopoverBox(page);
|
||||
|
||||
expect(Math.abs(oneCommand.bottom - allCommands.bottom)).toBeLessThanOrEqual(4);
|
||||
expect(oneCommand.height).toBeLessThan(allCommands.height - 40);
|
||||
} finally {
|
||||
await agent.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("stays visible while filtering slash command results", async ({ page }) => {
|
||||
await installListCommandsStub(page);
|
||||
const agent = await openReadyMockAgent(page);
|
||||
|
||||
try {
|
||||
const input = composerLocator(page);
|
||||
await expect(input).toBeEditable({ timeout: 30_000 });
|
||||
|
||||
await input.fill("/");
|
||||
const popover = page.getByTestId("composer-autocomplete-popover");
|
||||
await expect(popover.getByText("/help", { exact: true }).first()).toBeVisible({
|
||||
timeout: 30_000,
|
||||
});
|
||||
|
||||
await startPopoverFrameRecorder(page);
|
||||
await page.keyboard.type("tdd", { delay: 40 });
|
||||
await expect(popover.getByText("/tdd", { exact: true }).first()).toBeVisible({
|
||||
timeout: 30_000,
|
||||
});
|
||||
await page.waitForTimeout(250);
|
||||
const frames = await stopPopoverFrameRecorder(page);
|
||||
|
||||
expectPopoverDoesNotDisappearAfterFirstVisible(frames);
|
||||
} finally {
|
||||
await agent.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("stays anchored to the composer when the desktop sidebar is open", async ({ page }) => {
|
||||
await installListCommandsStub(page);
|
||||
const agent = await openReadyMockAgent(page);
|
||||
|
||||
try {
|
||||
await expect(page.getByTestId("sidebar-sessions")).toBeVisible({ timeout: 30_000 });
|
||||
const input = composerLocator(page);
|
||||
await expect(input).toBeEditable({ timeout: 30_000 });
|
||||
|
||||
await input.fill("/");
|
||||
const popover = page.getByTestId("composer-autocomplete-popover");
|
||||
await expect(popover.getByText("/help", { exact: true }).first()).toBeVisible({
|
||||
timeout: 30_000,
|
||||
});
|
||||
|
||||
const composerBox = await page.getByTestId("message-input-root").boundingBox();
|
||||
const popoverBox = await popover.boundingBox();
|
||||
expect(composerBox).not.toBeNull();
|
||||
expect(popoverBox).not.toBeNull();
|
||||
|
||||
expect(Math.abs(popoverBox!.x - composerBox!.x)).toBeLessThanOrEqual(4);
|
||||
expect(Math.abs(popoverBox!.width - composerBox!.width)).toBeLessThanOrEqual(4);
|
||||
} finally {
|
||||
await agent.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test.describe("compact sidebar layering", () => {
|
||||
test.use({ viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true });
|
||||
|
||||
test("keeps the mobile agent sidebar above autocomplete", async ({ page }) => {
|
||||
await installListCommandsStub(page);
|
||||
const agent = await openReadyMockAgent(page, { expectWorkspaceTab: false });
|
||||
|
||||
try {
|
||||
const input = composerLocator(page);
|
||||
await expect(input).toBeEditable({ timeout: 30_000 });
|
||||
|
||||
await input.fill("/");
|
||||
const popover = page.getByTestId("composer-autocomplete-popover");
|
||||
await expect(popover.getByText("/help", { exact: true }).first()).toBeVisible({
|
||||
timeout: 30_000,
|
||||
});
|
||||
|
||||
await page.getByRole("button", { name: "Open menu" }).click();
|
||||
await expect(page.getByTestId("sidebar-sessions")).toBeInViewport({ timeout: 5_000 });
|
||||
|
||||
const popoverBox = await popover.boundingBox();
|
||||
expect(popoverBox).not.toBeNull();
|
||||
|
||||
const topTestId = await getTopTestIdAtPoint(
|
||||
page,
|
||||
popoverBox!.x + popoverBox!.width / 2,
|
||||
popoverBox!.y + popoverBox!.height / 2,
|
||||
);
|
||||
|
||||
expect(topTestId).not.toBe("composer-autocomplete-popover");
|
||||
} finally {
|
||||
await agent.cleanup();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user