From abedd44b717639d5670f38893b6f48169d228144 Mon Sep 17 00:00:00 2001 From: Richard Lundeen Date: Sun, 25 Oct 2026 14:17:31 -0700 Subject: [PATCH 1/3] TEST Replace flaky converter popup geometry checks Check keyboard scrolling and selection at the existing viewports instead of sampling popup animation frames or forcing scroll position. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- frontend/README.md | 5 ++++ frontend/e2e/registry.spec.ts | 46 +++-------------------------------- 2 files changed, 8 insertions(+), 43 deletions(-) diff --git a/frontend/README.md b/frontend/README.md index c58e517b91..58d1cdcf12 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -274,6 +274,11 @@ Conversation-selection helpers check the toggle's `aria-expanded` state, not drawer visibility during an animation. Use accessible row clicks and wait for the destination URL and transcript before asserting its contents. +Converter Registry viewport checks use **End** to reach the last converter, then +select it with a click. They check user actions, not pixel positions in each +animation frame. Do not use programmatic scrolling that can conflict with the +dropdown's keyboard focus. + ```bash # Seeded integration (no credentials needed) npx playwright test --project seeded diff --git a/frontend/e2e/registry.spec.ts b/frontend/e2e/registry.spec.ts index 7ee3085052..e16683305f 100644 --- a/frontend/e2e/registry.spec.ts +++ b/frontend/e2e/registry.spec.ts @@ -517,56 +517,16 @@ test.describe("Converter Registry", () => { test.describe(name, () => { test.use({ viewport, deviceScaleFactor }); - test("keeps a long converter list vertical, stable, and scrollable", async ({ page }) => { + test("reaches and selects the last converter in a long list", async ({ page }) => { await page.getByRole("button", { name: "New Converter" }).click(); const trigger = page.getByRole("combobox", { name: "Converter type" }); await trigger.click(); const listbox = page.getByRole("listbox"); await expect(listbox).toBeVisible(); - const anchor = await trigger.evaluate((button) => { - const { x, y, width, height } = button.parentElement!.getBoundingClientRect(); - return { x, y, width, height }; - }); - await expect.poll(async () => { - const box = (await listbox.boundingBox())!; - return Math.abs(box.x - anchor.x); - }).toBeLessThanOrEqual(1); - - // Sample successive frames: a single bounding box can miss position oscillation. - const samples = await listbox.evaluate(async (element) => { - const bounds = []; - for (let frame = 0; frame < 60; frame++) { - await new Promise((resolve) => requestAnimationFrame(() => resolve())); - const { x, y, width, height } = element.getBoundingClientRect(); - bounds.push({ x, y, width, height }); - } - return bounds; - }); - - for (const box of samples) { - expect(Math.abs(box.x - anchor.x)).toBeLessThanOrEqual(1); - expect(Math.abs(box.width - anchor.width)).toBeLessThanOrEqual(1); - expect(box.height).toBeGreaterThan(0); - expect(box.x).toBeGreaterThanOrEqual(-1); - expect(box.y).toBeGreaterThanOrEqual(-1); - expect(box.x + box.width).toBeLessThanOrEqual(viewport.width + 1); - expect(box.y + box.height).toBeLessThanOrEqual(viewport.height + 1); - expect( - box.y >= anchor.y + anchor.height - 1 - || box.y + box.height <= anchor.y + 1, - ).toBe(true); - expect(Math.abs(box.y - samples[0].y)).toBeLessThanOrEqual(1); - expect(Math.abs(box.height - samples[0].height)).toBeLessThanOrEqual(1); - } - - const scroll = await listbox.evaluate((element) => ({ - height: element.clientHeight, - contentHeight: element.scrollHeight, - })); - expect(scroll.contentHeight).toBeGreaterThan(scroll.height); const lastOption = listbox.getByRole("option").last(); - await lastOption.scrollIntoViewIfNeeded(); + await expect(lastOption).not.toBeInViewport(); + await trigger.press("End"); await expect(lastOption).toBeInViewport(); await lastOption.click(); await expect(listbox).toBeHidden(); From df934d1926f48d8a7137bf597fae003e4a6eacda Mon Sep 17 00:00:00 2001 From: Richard Lundeen Date: Sun, 25 Oct 2026 14:31:04 -0700 Subject: [PATCH 2/3] TEST Focus frontend E2E checks on user-visible failures Remove cosmetic position and size checks while keeping containment, scrolling, focus, actions, and minimum touch targets. Drop README test details and keep concise guidance in the test instructions. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../frontend-test.instructions.md | 2 + frontend/README.md | 5 - frontend/e2e/accessibility.spec.ts | 26 ---- frontend/e2e/chat.spec.ts | 20 +-- frontend/e2e/config.spec.ts | 32 +---- frontend/e2e/datasets.spec.ts | 20 +-- frontend/e2e/labels-operation-picker.spec.ts | 35 +---- frontend/e2e/registry.spec.ts | 10 -- frontend/e2e/touch-targets.spec.ts | 129 ------------------ 9 files changed, 14 insertions(+), 265 deletions(-) diff --git a/.github/instructions/frontend-test.instructions.md b/.github/instructions/frontend-test.instructions.md index c74b7cea95..5add9ec224 100644 --- a/.github/instructions/frontend-test.instructions.md +++ b/.github/instructions/frontend-test.instructions.md @@ -445,6 +445,8 @@ Files excluded from coverage: `main.tsx`, `vite-env.d.ts`, `services/api.ts` (th - E2E tests live in `frontend/e2e/`. - They test full user flows against a running backend. +- Check user-visible failures: missing or clipped content, unreachable controls, broken actions, scrolling, and focus. +- Do not lock tests to exact coordinates, animation frames, or cosmetic sizes. Keep size checks for explicit usability requirements, such as minimum touch targets. - Do NOT mock APIs in E2E tests — they exercise the real stack. - Run with `npm run test:e2e` (headless) or `npm run test:e2e:headed`. diff --git a/frontend/README.md b/frontend/README.md index 58d1cdcf12..c58e517b91 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -274,11 +274,6 @@ Conversation-selection helpers check the toggle's `aria-expanded` state, not drawer visibility during an animation. Use accessible row clicks and wait for the destination URL and transcript before asserting its contents. -Converter Registry viewport checks use **End** to reach the last converter, then -select it with a click. They check user actions, not pixel positions in each -animation frame. Do not use programmatic scrolling that can conflict with the -dropdown's keyboard focus. - ```bash # Seeded integration (no credentials needed) npx playwright test --project seeded diff --git a/frontend/e2e/accessibility.spec.ts b/frontend/e2e/accessibility.spec.ts index 667c73be33..5b4a667ff3 100644 --- a/frontend/e2e/accessibility.spec.ts +++ b/frontend/e2e/accessibility.spec.ts @@ -401,29 +401,3 @@ test.describe("Accessibility", () => { await expectTourContainedAndActionable(page, "desktop"); }); }); - -test.describe("Visual Consistency", () => { - test("should render without layout shifts", async ({ page }) => { - await page.goto("/"); - - // Wait for initial render then navigate to chat to measure the chat ribbon - await expect(page.getByTitle("Technique.execute")).toBeVisible(); - await page.getByTitle("Technique.execute").click(); - const anchor = page.getByTestId("new-attack-btn"); - await expect(anchor).toBeVisible(); - - // Take measurements - const initialBox = await anchor.boundingBox(); - - // Wait a moment for any delayed renders - await page.waitForTimeout(500); - - // Verify position hasn't changed - const finalBox = await anchor.boundingBox(); - - if (initialBox && finalBox) { - expect(finalBox.x).toBe(initialBox.x); - expect(finalBox.y).toBe(initialBox.y); - } - }); -}); diff --git a/frontend/e2e/chat.spec.ts b/frontend/e2e/chat.spec.ts index ebdb50212b..2229d97e71 100644 --- a/frontend/e2e/chat.spec.ts +++ b/frontend/e2e/chat.spec.ts @@ -226,7 +226,7 @@ test.describe("Chat Functionality", () => { await expect(badge).toContainText(/gpt-4o-mock/); }); - test("should overlay conversations without shrinking mobile chat and restore focus", async ({ page }) => { + test("should open mobile conversations and restore focus on close", async ({ page }) => { await page.route(/\/api\/attacks\/[^/]+\/conversations/, async (route) => { await route.fulfill({ status: 200, @@ -252,30 +252,14 @@ test.describe("Chat Functionality", () => { await expect(getMessageByText(page, "Start a mobile conversation")).toBeVisible(); await page.setViewportSize({ width: 390, height: 844 }); - const chatArea = page.getByTestId("chat-area"); const toggleButton = page.getByRole("button", { name: "Toggle conversations panel" }); await expect(toggleButton).toBeEnabled(); - const beforeOpen = await chatArea.boundingBox(); await toggleButton.click(); const drawer = page.getByRole("dialog", { name: "Attack Conversations" }); await expect(drawer).toBeVisible(); - const afterOpen = await chatArea.boundingBox(); - const viewport = page.viewportSize(); - if (!beforeOpen || !afterOpen || !viewport) { - throw new Error("Expected chat and drawer layout bounds"); - } - - expect(Math.abs(afterOpen.width - beforeOpen.width)).toBeLessThanOrEqual(1); - await expect.poll(async () => { - const drawerBounds = await drawer.boundingBox(); - return drawerBounds ? drawerBounds.x : -1; - }).toBeGreaterThanOrEqual(0); - await expect.poll(async () => { - const drawerBounds = await drawer.boundingBox(); - return drawerBounds ? drawerBounds.x + drawerBounds.width : Number.POSITIVE_INFINITY; - }).toBeLessThanOrEqual(viewport.width); + await expect(drawer).toBeInViewport({ ratio: 1 }); await page.keyboard.press("Escape"); await expect(drawer).not.toBeVisible(); diff --git a/frontend/e2e/config.spec.ts b/frontend/e2e/config.spec.ts index d9519ef5e5..0096e2c49d 100644 --- a/frontend/e2e/config.spec.ts +++ b/frontend/e2e/config.spec.ts @@ -560,13 +560,7 @@ test.describe("Responsive Target Registry", () => { await page.getByRole("button", { name: /new target/i }).click(); const dialog = page.getByRole("dialog"); - const dialogBox = await dialog.boundingBox(); - if (!dialogBox) { - throw new Error("Expected the Create Target dialog to be visible"); - } - if (viewport.name === "desktop") { - expect(dialogBox.width).toBeLessThanOrEqual(640); - } + await expect(dialog).toBeVisible(); await selectTargetType(page, dialog, "RoundRobinTarget"); const addTargetSelect = dialog.locator("select"); @@ -610,7 +604,7 @@ test.describe("Responsive Target Registry", () => { ); }); - test(`should fit and align filter selections at ${viewport.name} width`, async ({ + test(`should fit and reset filter selections at ${viewport.name} width`, async ({ page, }) => { await page.setViewportSize({ @@ -654,25 +648,11 @@ test.describe("Responsive Target Registry", () => { expect(widths.scrollWidth).toBeLessThanOrEqual(widths.clientWidth); } - // Wrapped dropdowns start under the first one, and the reset button centers on the first row. - const filterBoxes = await page - .getByTestId("target-filters") - .getByRole("combobox") - .evaluateAll((filters) => - filters.map((filter) => { - const box = filter.parentElement?.getBoundingClientRect(); - return { left: box?.left ?? 0, centerY: box ? box.top + box.height / 2 : 0 }; - }) - ); - expect(filterBoxes).toHaveLength(4); - for (const { left } of filterBoxes) { - expect(left).toBeGreaterThanOrEqual(filterBoxes[0].left); - } - const resetBox = await page.getByTestId("target-reset-filters-btn").boundingBox(); - if (!resetBox) { - throw new Error("Expected a visible reset button"); + await page.getByRole("button", { name: "Reset all filters", exact: true }).click(); + for (const { filterName, summary } of selections) { + await expect(page.getByRole("combobox", { name: filterName, exact: true })).not.toHaveValue(summary); } - expect(Math.abs(resetBox.y + resetBox.height / 2 - filterBoxes[0].centerY)).toBeLessThanOrEqual(1); + await expect(page.getByTestId("target-row-filter-chat")).toBeVisible(); const config = page.getByTestId("target-config"); const configWidth = await config.evaluate((element) => ({ diff --git a/frontend/e2e/datasets.spec.ts b/frontend/e2e/datasets.spec.ts index 25b9c2d744..8ce147de91 100644 --- a/frontend/e2e/datasets.spec.ts +++ b/frontend/e2e/datasets.spec.ts @@ -91,9 +91,7 @@ async function expectLongLabelStaysInsideCard(page: Page): Promise { range.selectNodeContents(textHost); const text = range.getBoundingClientRect(); const cardBox = article.getBoundingClientRect(); - const badgeBox = textHost.getBoundingClientRect(); return { - cardWidth: cardBox.width, cardRight: cardBox.right, cardLeft: cardBox.left, cardTop: cardBox.top, @@ -102,18 +100,15 @@ async function expectLongLabelStaysInsideCard(page: Page): Promise { textRight: text.right, textTop: text.top, textBottom: text.bottom, - badgeHeight: badgeBox.height, scrollWidth: textHost.scrollWidth, clientWidth: textHost.clientWidth, }; }, LONG_HARM_CATEGORY); - expect(metrics.cardWidth).toBeLessThanOrEqual(360); expect(metrics.textLeft).toBeGreaterThanOrEqual(metrics.cardLeft - 1); expect(metrics.textRight).toBeLessThanOrEqual(metrics.cardRight + 1); expect(metrics.textTop).toBeGreaterThanOrEqual(metrics.cardTop - 1); expect(metrics.textBottom).toBeLessThanOrEqual(metrics.cardBottom + 1); - expect(metrics.badgeHeight).toBeGreaterThan(20); expect(metrics.scrollWidth).toBeLessThanOrEqual(metrics.clientWidth + 1); } @@ -131,11 +126,10 @@ test.describe("dataset catalog layout", () => { await expect(cards).toHaveCount(2); const [first, second] = await cards.evaluateAll((articles: Element[]) => articles.map((article: Element) => { - const { left, right, top } = article.getBoundingClientRect(); - return { left, right, top }; + const { left, right } = article.getBoundingClientRect(); + return { left, right }; }), ); - expect(first.top).toBe(second.top); expect(second.left).toBeGreaterThan(first.right); }); @@ -454,7 +448,6 @@ test.describe("dataset example table", () => { range.selectNodeContents(textHost); const text = range.getBoundingClientRect(); const cellBox = host.getBoundingClientRect(); - const badgeBox = textHost.getBoundingClientRect(); return { textLeft: text.left, textRight: text.right, @@ -464,7 +457,6 @@ test.describe("dataset example table", () => { cellRight: cellBox.right, cellTop: cellBox.top, cellBottom: cellBox.bottom, - badgeHeight: badgeBox.height, scrollWidth: textHost.scrollWidth, clientWidth: textHost.clientWidth, }; @@ -474,14 +466,11 @@ test.describe("dataset example table", () => { expect(metrics.textRight).toBeLessThanOrEqual(metrics.cellRight + 1); expect(metrics.textTop).toBeGreaterThanOrEqual(metrics.cellTop - 1); expect(metrics.textBottom).toBeLessThanOrEqual(metrics.cellBottom + 1); - expect(metrics.badgeHeight).toBeGreaterThan(20); expect(metrics.scrollWidth).toBeLessThanOrEqual(metrics.clientWidth + 1); const tableLayout = await table.evaluate((host) => ({ - columnWidth: host.querySelector("th")?.getBoundingClientRect().width ?? 0, wrapperWidth: host.parentElement?.clientWidth ?? 0, scrollWidth: host.parentElement?.scrollWidth ?? 0, })); - expect(tableLayout.columnWidth).toBeGreaterThanOrEqual(140); expect(tableLayout.scrollWidth).toBeGreaterThan(tableLayout.wrapperWidth); await table.getByRole("button", { name: "Show alpha" }).click(); await expect(page.getByRole("region", { name: "Example detail" })).toBeVisible(); @@ -501,18 +490,13 @@ test.describe("dataset example table", () => { const termBox = term.getBoundingClientRect(); const valueBox = value.getBoundingClientRect(); return { - termLeft: termBox.left, termBottom: termBox.bottom, - valueLeft: valueBox.left, valueTop: valueBox.top, - valueWidth: valueBox.width, clientWidth: value.clientWidth, scrollWidth: value.scrollWidth, }; }); expect(dimensions.valueTop).toBeGreaterThanOrEqual(dimensions.termBottom); - expect(dimensions.valueLeft).toBeCloseTo(dimensions.termLeft, 0); - expect(dimensions.valueWidth).toBeGreaterThanOrEqual(250); expect(dimensions.scrollWidth).toBeLessThanOrEqual(dimensions.clientWidth + 1); } expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(390); diff --git a/frontend/e2e/labels-operation-picker.spec.ts b/frontend/e2e/labels-operation-picker.spec.ts index 15621580b7..bc21922b92 100644 --- a/frontend/e2e/labels-operation-picker.spec.ts +++ b/frontend/e2e/labels-operation-picker.spec.ts @@ -10,7 +10,6 @@ import { READY_RUNTIME, waitForAppReady } from "./_runtime"; // These tests measure the rendered box in a real browser. // --------------------------------------------------------------------------- -const LIST_MAX_HEIGHT = 240; const LONG_OPERATION = "op_2026_08_a_very_long_operation_name_that_would_be_clipped"; function operations(count: number): string[] { @@ -194,29 +193,6 @@ test.describe("operation picker placement", () => { await expect(popover.getByRole("button", { name: "Add", exact: true })).toBeVisible(); }); - test("caps the list height and anchors it to the input", async ({ page }) => { - await page.setViewportSize({ width: 1280, height: 800 }); - await setupMocks(page, operations(60)); - const listbox = await openOperationPicker(page); - - const box = (await listbox.boundingBox())!; - const input = (await page - .getByTestId("edit-label-operation") - .boundingBox())!; - - expect(box.height).toBeLessThanOrEqual(LIST_MAX_HEIGHT); - // Opens below the input and stays attached to it. - expect(box.y).toBeGreaterThanOrEqual(input.y + input.height); - expect(box.y - (input.y + input.height)).toBeLessThan(16); - - // The options that do not fit are reachable by scrolling, not lost. - const scroll = await listbox.evaluate((el) => ({ - scrollHeight: el.scrollHeight, - clientHeight: el.clientHeight, - })); - expect(scroll.scrollHeight).toBeGreaterThan(scroll.clientHeight); - }); - test("keeps the list on screen when it opens above the input", async ({ page, }) => { @@ -239,11 +215,8 @@ test.describe("operation picker placement", () => { .boundingBox())!; const viewport = page.viewportSize()!; - expect(input.y).toBeGreaterThanOrEqual(LIST_MAX_HEIGHT); - expect(viewport.height - (input.y + input.height)).toBeLessThan(LIST_MAX_HEIGHT); expect(box.y).toBeLessThan(input.y); expect(box.y + box.height).toBeLessThanOrEqual(input.y); - expect(input.y - (box.y + box.height)).toBeLessThan(16); expect(box.y).toBeGreaterThanOrEqual(0); expect(box.y + box.height).toBeLessThanOrEqual(viewport.height); }); @@ -290,17 +263,15 @@ test.describe("operation picker placement", () => { expect(overflow.scrollWidth).toBeLessThanOrEqual(overflow.clientWidth); }); - test("shrinks below the cap when the window is too short for it", async ({ + test("keeps the list on screen in a short window", async ({ page, }) => { - // Shorter than the 240px cap, so a flat cap would hang off the screen. const viewportHeight = 200; await page.setViewportSize({ width: 1280, height: viewportHeight }); await setupMocks(page, operations(60)); const listbox = await openOperationPicker(page); const box = (await listbox.boundingBox())!; - expect(box.height).toBeLessThan(LIST_MAX_HEIGHT); expect(box.y).toBeGreaterThanOrEqual(0); expect(box.y + box.height).toBeLessThanOrEqual(viewportHeight); }); @@ -593,7 +564,7 @@ test.describe("finishing an edit another way", () => { await expect(page.getByTestId("edit-label-operator")).toHaveValue("alice"); }); - test("keeps compact metadata inputs visible and creation first on mobile", async ({ + test("keeps metadata inputs visible and creation first on mobile", async ({ page, }) => { await page.setViewportSize({ width: 360, height: 800 }); @@ -604,8 +575,6 @@ test.describe("finishing an edit another way", () => { const operation = page.getByRole("combobox", { name: "Operation" }); await expect(operator).toBeVisible(); await expect(operation).toBeVisible(); - expect(await operator.evaluate(input => input.parentElement?.getBoundingClientRect().width)).toBeLessThanOrEqual(100); - expect(await operation.evaluate(input => input.parentElement?.getBoundingClientRect().width)).toBeLessThanOrEqual(140); await operation.click(); await expect(page.getByRole("option").first()).toHaveText("New operation…"); await operation.fill("missing"); diff --git a/frontend/e2e/registry.spec.ts b/frontend/e2e/registry.spec.ts index e16683305f..9c80f5f086 100644 --- a/frontend/e2e/registry.spec.ts +++ b/frontend/e2e/registry.spec.ts @@ -499,16 +499,6 @@ test.describe("Converter Registry", () => { await expect(dialog.getByLabel("Registry name")).toHaveValue(""); }); - test("uses the available viewport height for the converter type list", async ({ page }) => { - await page.getByRole("button", { name: "New Converter" }).click(); - await page.getByRole("combobox", { name: "Converter type" }).click(); - - const listbox = page.getByRole("listbox"); - await expect(listbox).toBeVisible(); - const bounds = await listbox.boundingBox(); - expect(bounds?.height).toBeGreaterThan(300); - }); - for (const { name, viewport, deviceScaleFactor } of [ { name: "wide short screen", viewport: { width: 1920, height: 600 }, deviceScaleFactor: 1 }, { name: "scaled laptop", viewport: { width: 1280, height: 640 }, deviceScaleFactor: 1.5 }, diff --git a/frontend/e2e/touch-targets.spec.ts b/frontend/e2e/touch-targets.spec.ts index dc6eed9e55..d473600f2d 100644 --- a/frontend/e2e/touch-targets.spec.ts +++ b/frontend/e2e/touch-targets.spec.ts @@ -8,7 +8,6 @@ import { TOUR_STEPS } from "../src/components/Tour/tourSteps"; const TOUR_STEP_COUNT = TOUR_STEPS.length; const MOBILE_VIEWPORT = { width: 390, height: 844 }; -const DESKTOP_VIEWPORT = { width: 1280, height: 800 }; const MINIMUM_TOUCH_TARGET_SIZE = 44; const LONG_SCORE_VALUE = "a".repeat(200); @@ -463,23 +462,6 @@ async function expectMinimumTouchTargets(locator: Locator): Promise { } } -async function expectCompactDesktopTarget(locator: Locator): Promise { - await expect(locator).toBeVisible(); - const box = await locator.boundingBox(); - if (!box) { - throw new Error("Expected a visible desktop target"); - } - expect(box.height).toBeLessThan(MINIMUM_TOUCH_TARGET_SIZE); -} - -async function expectCompactDesktopTargets(locator: Locator): Promise { - const count = await locator.count(); - expect(count).toBeGreaterThan(0); - for (let index = 0; index < count; index += 1) { - await expectCompactDesktopTarget(locator.nth(index)); - } -} - async function expectNoDocumentOverflow(page: Page): Promise { const dimensions = await page.evaluate(() => ({ viewportWidth: document.documentElement.clientWidth, @@ -492,20 +474,6 @@ async function expectNoDocumentOverflow(page: Page): Promise { expect(dimensions.bodyWidth).toBeLessThanOrEqual(dimensions.viewportWidth); } -async function startChatWithMessages(page: Page): Promise { - await page.getByRole("button", { name: "Registry", exact: true }).click(); - await expect(page.getByText("gpt-4o-mobile", { exact: true })).toBeVisible(); - await page.getByRole("combobox", { name: "Default objective target", exact: true }).selectOption({ index: 1 }); - await page.getByRole("button", { name: "Technique.execute", exact: true }).click(); - await page.getByTestId("chat-input").fill( - "Assess this deterministic mobile prompt" - ); - await page.getByTestId("send-message-btn").click(); - await expect( - page.getByText("Deterministic assistant response for touch-target tests.") - ).toBeVisible(); -} - test.beforeEach(async ({ page }) => { await installTouchTargetMocks(page); }); @@ -866,100 +834,3 @@ test.describe("Mobile touch targets", () => { ); }); }); - -test("preserves compact desktop controls and existing sidebar dimensions", async ({ - page, -}) => { - await page.setViewportSize(DESKTOP_VIEWPORT); - await page.goto("/"); - await expect(page.getByTestId("home-view")).toBeVisible(); - - const sidebarHome = page.getByRole("button", { - name: "Home", - exact: true, - }); - const sidebarBox = await sidebarHome.boundingBox(); - expect(sidebarBox?.width).toBe(44); - expect(sidebarBox?.height).toBe(44); - - await expectCompactDesktopTarget(page.getByTestId("labels-icon-btn")); - await expectCompactDesktopTarget( - page.getByTestId("home-configure-target-btn") - ); - await expectCompactDesktopTarget( - page.getByTestId("home-open-attack-home-attack-001") - ); - - await page - .getByRole("button", { name: "Registry", exact: true }) - .click(); - await expect(page.getByText("gpt-4o-mobile", { exact: true })).toBeVisible(); - await expectCompactDesktopTarget( - page.getByRole("button", { name: "Refresh", exact: true }) - ); - await expectCompactDesktopTarget(page.getByRole("combobox", { name: "Filter by type:", exact: true })); - await expectCompactDesktopTarget(page.getByRole("combobox", { name: "Filter by input:", exact: true })); - await expectCompactDesktopTarget(page.getByRole("combobox", { name: "Filter by output:", exact: true })); - await expectCompactDesktopTarget(page.getByRole("combobox", { name: "Filter by capability:", exact: true })); - await page.getByRole("combobox", { name: "Filter by capability:", exact: true }).click(); - await expectCompactDesktopTargets(page.getByRole("menuitemcheckbox")); - await page.keyboard.press("Escape"); - await expect(page.getByRole("menuitemcheckbox")).toHaveCount(0); - await expectCompactDesktopTarget(page.getByRole("button", { name: "Reset all filters", exact: true })); - await expectCompactDesktopTarget( - page.getByRole("combobox", { name: "Default objective target", exact: true }) - ); - await expectCompactDesktopTarget( - page.getByRole("combobox", { name: "Default adversarial target", exact: true }) - ); - await expectCompactDesktopTarget( - page.getByRole("button", { name: "Expand inner targets" }) - ); - - await page.goto("/config"); - await page.getByRole("tab", { name: "Initializers", exact: true }).click(); - await expectCompactDesktopTarget( - page.getByRole("button", { name: "Browse available initializers" }) - ); - - await startChatWithMessages(page); - await expectCompactDesktopTarget(page.getByTestId("toggle-panel-btn")); - await expectCompactDesktopTarget( - page.getByRole("button", { name: "Attach files" }) - ); - await expectCompactDesktopTarget(page.getByTestId("chat-input")); - await expectCompactDesktopTarget(page.getByTestId("copy-to-input-btn-1")); - - await page.getByTestId("toggle-panel-btn").click(); - await expect(page.getByTestId("conversation-panel")).toBeVisible(); - await expectCompactDesktopTarget(page.getByTestId("close-panel-btn")); - await expectCompactDesktopTarget( - page.getByTestId("star-btn-mobile-conversation-001") - ); - await page.getByTestId("close-panel-btn").click(); - - await page.getByRole("button", { name: "Home", exact: true }).click(); - await page.getByTestId("start-tour").click(); - await expect(page.getByText(`1 of ${TOUR_STEP_COUNT}`)).toBeVisible(); - await expectCompactDesktopTarget( - page.getByRole("button", { name: "Close", exact: true }) - ); - await expectCompactDesktopTarget( - page.getByRole("button", { name: "Next", exact: true }) - ); - - await page.goto("/history"); - await expect(page.getByTestId("refresh-btn")).toBeVisible(); - await expectCompactDesktopTarget(page.getByTestId("refresh-btn")); - await expectCompactDesktopTarget(page.getByTestId("attack-type-filter")); - const openAttack = page.getByTestId("open-attack-home-attack-001"); - await openAttack.scrollIntoViewIfNeeded(); - await expectCompactDesktopTarget(openAttack); - await expectCompactDesktopTarget(page.getByTestId("next-page-btn")); - - await page.goto("/datasets"); - await expect(page.getByRole("heading", { name: "Datasets" })).toBeVisible(); - await expectCompactDesktopTarget(page.getByRole("combobox", { name: "Modalities" })); - await expectCompactDesktopTarget(page.getByRole("combobox", { name: "Harm categories" })); - await expectCompactDesktopTarget(page.getByRole("combobox", { name: "Sort datasets" })); -}); From 0ee6cabe7fbc17d09da3f186c3a88c0c2578557e Mon Sep 17 00:00:00 2001 From: Richard Lundeen Date: Mon, 26 Oct 2026 10:40:55 -0700 Subject: [PATCH 3/3] TEST Restore converter popup viewport containment Check the whole popup before keyboard navigation or clicks can scroll it into view. Keep the behavior-based flow without frame sampling or exact geometry checks. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- frontend/e2e/registry.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/e2e/registry.spec.ts b/frontend/e2e/registry.spec.ts index 9c80f5f086..46ec84786c 100644 --- a/frontend/e2e/registry.spec.ts +++ b/frontend/e2e/registry.spec.ts @@ -514,6 +514,7 @@ test.describe("Converter Registry", () => { const listbox = page.getByRole("listbox"); await expect(listbox).toBeVisible(); + await expect(listbox).toBeInViewport({ ratio: 1 }); const lastOption = listbox.getByRole("option").last(); await expect(lastOption).not.toBeInViewport(); await trigger.press("End");