test(web): add responsive text hierarchy E2E (#3237)

This commit is contained in:
José Almeida
2026-09-01 19:37:21 +00:00
committed by GitHub
parent 070c881812
commit b4f1933812
4 changed files with 499 additions and 4 deletions
@@ -0,0 +1,167 @@
import type { Locator } from "@playwright/test";
import { expect, test } from "../../fixtures/test-base";
import {
assertLocatorWithinViewportX,
assertNoDescendantOverflowsRight,
assertNoDocumentHorizontalOverflow,
} from "../../helpers/layout-assertions";
import { waitForFiniteAnimations } from "../../helpers/animations";
import { SessionPage } from "../../pages/session-page";
const LONG_TASK_TITLE = `Desktop cleanup confirmation ${"X".repeat(31)}`;
async function assertDesktopDeleteDialog(dialog: Locator): Promise<void> {
await waitForFiniteAnimations(dialog);
const title = dialog.locator('[data-slot="alert-dialog-title"]');
const description = dialog.locator('[data-slot="alert-dialog-description"]');
await expect(title).toBeVisible();
await expect(description).toBeVisible();
await expect(title).toHaveCSS("text-wrap", "balance");
await expect(description).toHaveCSS("text-wrap", "pretty");
await expect(description).toHaveCSS("text-align", "left");
const labelledBy = await dialog.getAttribute("aria-labelledby");
const describedBy = await dialog.getAttribute("aria-describedby");
expect(labelledBy).toBe(await title.getAttribute("id"));
expect(describedBy).toBe(await description.getAttribute("id"));
expect(await description.locator("p").count()).toBeGreaterThan(0);
expect(await description.locator("ul, ol").count()).toBeGreaterThan(0);
await assertLocatorWithinViewportX(dialog, "desktop delete dialog");
await assertNoDescendantOverflowsRight(dialog, "desktop delete dialog");
await assertNoDocumentHorizontalOverflow(dialog.page(), "desktop delete dialog");
const footer = dialog.locator('[data-slot="alert-dialog-footer"]');
const cancel = dialog.locator('[data-slot="alert-dialog-cancel"]');
const deleteAction = dialog.locator('[data-slot="alert-dialog-action"]');
const [footerBox, cancelBox, deleteBox] = await Promise.all([
footer.boundingBox(),
cancel.boundingBox(),
deleteAction.boundingBox(),
]);
if (!footerBox || !cancelBox || !deleteBox) {
throw new Error("desktop delete dialog footer has no rendered action boxes");
}
expect(Math.round(cancelBox.height)).toBeLessThan(44);
expect(Math.round(deleteBox.height)).toBeLessThan(44);
expect(cancelBox.y).toBeCloseTo(deleteBox.y, 1);
expect(deleteBox.x + deleteBox.width).toBeCloseTo(footerBox.x + footerBox.width, 1);
await expect(deleteAction).toHaveAttribute("data-variant", "destructive");
await expect(deleteAction).not.toHaveClass(/bg-primary/);
}
test.describe("Desktop confirmation text hierarchy", () => {
// @covers AC-UI-SURFACE-TEXT-HIERARCHY-001.1, AC-UI-SURFACE-TEXT-HIERARCHY-001.2
// @covers AC-UI-SURFACE-TEXT-HIERARCHY-001.3, AC-UI-SURFACE-TEXT-HIERARCHY-001.5
// @covers AC-UI-TASK-CLEANUP-CONFIRMATION-001.5, AC-UI-TASK-CLEANUP-CONFIRMATION-001.6
// @covers AC-UI-TASK-CLEANUP-CONFIRMATION-001.7
test("keeps the fine-pointer archive popover wide and the sidebar row stable", async ({
testPage,
apiClient,
seedData,
}) => {
await testPage.setViewportSize({ width: 900, height: 700 });
const title = "Desktop archive popover geometry target";
const task = await apiClient.seedTask(seedData.workspaceId, title, {
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
});
await testPage.goto(`/t/${task.task_id}`);
const session = new SessionPage(testPage);
await session.waitForLoad();
const taskRow = session.sidebarTaskItem(title);
await expect(taskRow).toBeVisible();
const heightBefore = await taskRow.evaluate(
(element) => element.getBoundingClientRect().height,
);
await session.openSidebarMenuAndClick(title, "Archive");
const popover = testPage.getByTestId("task-archive-confirm-popover");
await expect(popover).toBeVisible();
await waitForFiniteAnimations(popover);
const popoverDescription = popover.locator('[data-slot="popover-description"]');
await expect(popoverDescription).toBeVisible();
await expect(popoverDescription).toHaveCSS("text-wrap", "pretty");
const [heightOpen, popoverBox, viewport] = await Promise.all([
taskRow.evaluate((element) => element.getBoundingClientRect().height),
popover.boundingBox(),
testPage.evaluate(() => ({ width: window.innerWidth, height: window.innerHeight })),
]);
if (!popoverBox) throw new Error("desktop archive popover has no rendered layout box");
expect(heightOpen).toBeCloseTo(heightBefore, 3);
expect(popoverBox.width).toBeGreaterThan(256);
expect(popoverBox.x).toBeGreaterThanOrEqual(0);
expect(popoverBox.x + popoverBox.width).toBeLessThanOrEqual(viewport.width);
expect(popoverBox.y).toBeGreaterThanOrEqual(0);
expect(popoverBox.y + popoverBox.height).toBeLessThanOrEqual(viewport.height);
await assertNoDocumentHorizontalOverflow(testPage, "desktop archive popover");
await popover.getByRole("button", { name: "Cancel", exact: true }).click();
await expect(popover).toBeHidden();
const heightAfter = await taskRow.evaluate((element) => element.getBoundingClientRect().height);
expect(heightAfter).toBeCloseTo(heightBefore, 3);
});
// @covers AC-UI-SURFACE-TEXT-HIERARCHY-001.1, AC-UI-SURFACE-TEXT-HIERARCHY-001.2
// @covers AC-UI-SURFACE-TEXT-HIERARCHY-001.3, AC-UI-SURFACE-TEXT-HIERARCHY-001.5
// @covers AC-UI-TASK-CLEANUP-CONFIRMATION-001.5, AC-UI-TASK-CLEANUP-CONFIRMATION-001.6
// @covers AC-UI-TASK-CLEANUP-CONFIRMATION-001.7
test("preserves Dialog and Delete behavior with compact desktop actions", async ({
testPage,
apiClient,
seedData,
}) => {
const task = await apiClient.seedTask(seedData.workspaceId, LONG_TASK_TITLE, {
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
});
const survivor = await apiClient.seedTask(seedData.workspaceId, "Desktop delete survivor", {
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
});
await testPage.goto(`/t/${task.task_id}`);
const session = new SessionPage(testPage);
await session.waitForLoad();
await expect(session.taskInSidebar(LONG_TASK_TITLE)).toBeVisible();
await session.openSidebarMenuAndClick(LONG_TASK_TITLE, "Rename");
const renameDialog = testPage.locator('[data-slot="dialog-content"]:visible').last();
await expect(renameDialog).toBeVisible();
const renameTitle = renameDialog.locator('[data-slot="dialog-title"]');
await expect(renameTitle).toBeVisible();
await expect(renameTitle).toHaveCSS("text-wrap", "balance");
const renameLabelledBy = await renameDialog.getAttribute("aria-labelledby");
expect(renameLabelledBy).toBe(await renameTitle.getAttribute("id"));
await assertLocatorWithinViewportX(renameDialog, "desktop rename Dialog");
await renameDialog.getByRole("button", { name: "Cancel", exact: true }).click();
await expect(renameDialog).toBeHidden();
await session.openSidebarMenuAndClick(LONG_TASK_TITLE, "Delete");
const deleteDialog = testPage.getByRole("alertdialog");
await expect(deleteDialog).toBeVisible();
await assertDesktopDeleteDialog(deleteDialog);
await deleteDialog.locator('[data-slot="alert-dialog-cancel"]').click();
await expect(deleteDialog).toBeHidden();
expect((await apiClient.getTask(task.task_id)).id).toBe(task.task_id);
await session.openSidebarMenuAndClick(LONG_TASK_TITLE, "Delete");
await testPage.getByRole("alertdialog").locator('[data-slot="alert-dialog-action"]').click();
await expect
.poll(
async () => (await apiClient.rawRequest("GET", `/api/v1/tasks/${task.task_id}`)).status,
{ timeout: 15_000, message: "the desktop Delete action should remove the task" },
)
.toBe(404);
await expect(session.taskInSidebar(LONG_TASK_TITLE)).toHaveCount(0, { timeout: 15_000 });
await expect(session.taskInSidebar("Desktop delete survivor")).toBeVisible({
timeout: 15_000,
});
expect((await apiClient.getTask(survivor.task_id)).id).toBe(survivor.task_id);
});
});
@@ -0,0 +1,304 @@
import type { Locator, Page } from "@playwright/test";
import { expect, test } from "../../fixtures/test-base";
import {
assertLocatorWithinViewportX,
assertNoDescendantOverflowsRight,
assertNoDocumentHorizontalOverflow,
} from "../../helpers/layout-assertions";
import { waitForFiniteAnimations } from "../../helpers/animations";
import {
setAgentRuntimeAvailability,
stubAgentRuntimeRestart,
} from "../../helpers/agent-runtime-availability";
import { MobileKanbanPage } from "../../pages/mobile-kanban-page";
import { SessionPage } from "../../pages/session-page";
const LONG_TASK_TITLE = `Mobile cleanup confirmation ${"X".repeat(32)}`;
async function enablePseudoLocale(page: Page): Promise<void> {
await page.evaluate(() => {
document.cookie = "kandev_locale=pseudo; path=/; max-age=31536000; SameSite=Lax";
});
await page.reload();
await expect(page.locator("html")).toHaveAttribute("lang", "pseudo", { timeout: 15_000 });
}
async function openDeleteDialog(page: Page, title: string) {
const session = new SessionPage(page);
await session.mobileSessionMenu.tap();
const drawer = page
.getByTestId("mobile-task-switcher-list")
.locator("xpath=ancestor::*[@data-slot='drawer-content'][1]");
await expect(drawer).toBeVisible();
const taskRow = drawer.getByTestId("sidebar-task-item").filter({ hasText: title });
await expect(taskRow).toBeVisible();
await taskRow.locator("button.mobile-task-actions-button").tap();
const menu = page.locator('[data-slot="context-menu-content"]:visible').last();
await expect(menu).toBeVisible();
const deleteItem = menu.locator('[role="menuitem"]:has(svg.tabler-icon-trash)');
await expect(deleteItem).toHaveCount(1);
await deleteItem.tap();
const dialog = page.getByRole("alertdialog");
await expect(dialog).toBeVisible();
return { dialog, drawer, taskRow };
}
async function dialogMetrics(dialog: Locator) {
return dialog.evaluate((element) => {
const rect = element.getBoundingClientRect();
const scrollOwners = Array.from(element.querySelectorAll<HTMLElement>("*"))
.filter((candidate) => {
const style = getComputedStyle(candidate);
return (
(style.overflowY === "auto" || style.overflowY === "scroll") &&
candidate.scrollHeight > candidate.clientHeight + 1
);
})
.map((candidate) => candidate.dataset.slot ?? candidate.tagName.toLowerCase());
const style = getComputedStyle(element);
return {
x: rect.x,
y: rect.y,
right: rect.right,
bottom: rect.bottom,
width: rect.width,
height: rect.height,
viewportWidth: window.innerWidth,
viewportHeight: window.innerHeight,
rootOverflowY: style.overflowY,
scrollOwners,
};
});
}
async function assertPhoneDeleteDialog(dialog: Locator, width: number): Promise<void> {
await waitForFiniteAnimations(dialog);
const title = dialog.locator('[data-slot="alert-dialog-title"]');
const description = dialog.locator('[data-slot="alert-dialog-description"]');
await expect(title).toBeVisible();
await expect(description).toBeVisible();
await expect(title).toHaveCSS("text-wrap", "balance");
await expect(description).toHaveCSS("text-wrap", "pretty");
await expect(description).toHaveCSS("text-align", "left");
await expect(description).toHaveCSS("overflow-wrap", /anywhere|break-word/);
const labelledBy = await dialog.getAttribute("aria-labelledby");
const describedBy = await dialog.getAttribute("aria-describedby");
expect(labelledBy).toBe(await title.getAttribute("id"));
expect(describedBy).toBe(await description.getAttribute("id"));
expect(await description.locator("p").count()).toBeGreaterThan(0);
expect(await description.locator("ul, ol").count()).toBeGreaterThan(0);
await assertLocatorWithinViewportX(dialog, `phone delete dialog at ${width}px`);
await assertNoDescendantOverflowsRight(dialog, `phone delete dialog at ${width}px`);
await assertNoDocumentHorizontalOverflow(pageFor(dialog), `phone delete dialog at ${width}px`);
const metrics = await dialogMetrics(dialog);
expect(metrics.x).toBeGreaterThanOrEqual(15);
expect(metrics.right).toBeLessThanOrEqual(metrics.viewportWidth - 15);
expect(metrics.y).toBeGreaterThanOrEqual(0);
expect(metrics.bottom).toBeLessThanOrEqual(metrics.viewportHeight);
expect(metrics.rootOverflowY).not.toMatch(/auto|scroll/);
if (width === 320) {
expect(metrics.scrollOwners.length).toBeGreaterThan(0);
}
const footer = dialog.locator('[data-slot="alert-dialog-footer"]');
const cancel = dialog.locator('[data-slot="alert-dialog-cancel"]');
const deleteAction = dialog.locator('[data-slot="alert-dialog-action"]');
await expect(cancel).toBeVisible();
await expect(deleteAction).toBeVisible();
const [footerBox, cancelBox, deleteBox] = await Promise.all([
footer.boundingBox(),
cancel.boundingBox(),
deleteAction.boundingBox(),
]);
if (!footerBox || !cancelBox || !deleteBox) {
throw new Error("phone delete dialog footer has no rendered action boxes");
}
expect(Math.round(cancelBox.height)).toBeGreaterThanOrEqual(44);
expect(Math.round(deleteBox.height)).toBeGreaterThanOrEqual(44);
expect(cancelBox.width).toBeGreaterThanOrEqual(footerBox.width - 2);
expect(deleteBox.width).toBeGreaterThanOrEqual(footerBox.width - 2);
await expect(cancel).toBeInViewport();
await expect(deleteAction).toBeInViewport();
await expect(deleteAction).toHaveAttribute("data-variant", "destructive");
await expect(deleteAction).not.toHaveClass(/bg-primary/);
}
function pageFor(locator: Locator): Page {
return locator.page();
}
test.describe("Mobile confirmation text hierarchy", () => {
// @covers AC-UI-SURFACE-TEXT-HIERARCHY-001.1, AC-UI-SURFACE-TEXT-HIERARCHY-001.2
// @covers AC-UI-SURFACE-TEXT-HIERARCHY-001.3, AC-UI-SURFACE-TEXT-HIERARCHY-001.5
// @covers AC-UI-TASK-CLEANUP-CONFIRMATION-001.5, AC-UI-TASK-CLEANUP-CONFIRMATION-001.6
// @covers AC-UI-TASK-CLEANUP-CONFIRMATION-001.7
test("keeps a pseudo-localized delete confirmation contained at 320px and 393px", async ({
testPage,
apiClient,
seedData,
}) => {
test.setTimeout(120_000);
await testPage.setViewportSize({ width: 320, height: 400 });
const task = await apiClient.seedTask(seedData.workspaceId, LONG_TASK_TITLE, {
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
});
await apiClient.seedTask(seedData.workspaceId, "Mobile cleanup confirmation child", {
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
parent_id: task.task_id,
});
await testPage.goto(`/t/${task.task_id}`);
await expect(testPage.getByTestId("mobile-task-layout")).toBeVisible();
await enablePseudoLocale(testPage);
for (const width of [320, 393]) {
await testPage.setViewportSize({ width, height: 400 });
const { dialog, drawer } = await openDeleteDialog(testPage, LONG_TASK_TITLE);
const drawerTitle = drawer.locator('[data-slot="drawer-title"]');
await expect(drawerTitle).toBeVisible();
await expect(drawerTitle).toHaveCSS("text-wrap", "balance");
await assertPhoneDeleteDialog(dialog, width);
await dialog.locator('[data-slot="alert-dialog-cancel"]').tap();
await expect(dialog).toBeHidden();
expect((await apiClient.getTask(task.task_id)).id).toBe(task.task_id);
}
const { dialog } = await openDeleteDialog(testPage, LONG_TASK_TITLE);
await dialog.locator('[data-slot="alert-dialog-action"]').tap();
await expect
.poll(
async () => (await apiClient.rawRequest("GET", `/api/v1/tasks/${task.task_id}`)).status,
{ timeout: 15_000, message: "the mobile Delete action should remove the task" },
)
.toBe(404);
});
// @covers AC-UI-SURFACE-TEXT-HIERARCHY-001.1, AC-UI-SURFACE-TEXT-HIERARCHY-001.3
// @covers AC-UI-TASK-CLEANUP-CONFIRMATION-001.4, AC-UI-TASK-CLEANUP-CONFIRMATION-001.5
test("keeps the phone Drawer and inline archive confirmation touch-safe", async ({
testPage,
apiClient,
seedData,
}) => {
await testPage.setViewportSize({ width: 393, height: 640 });
const title = "Mobile inline archive confirmation target";
const task = await apiClient.seedTask(seedData.workspaceId, title, {
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
});
await testPage.goto(`/t/${task.task_id}`);
await expect(testPage.getByTestId("mobile-task-layout")).toBeVisible();
const session = new SessionPage(testPage);
await session.mobileSessionMenu.tap();
const drawer = testPage
.getByTestId("mobile-task-switcher-list")
.locator("xpath=ancestor::*[@data-slot='drawer-content'][1]");
await expect(drawer).toBeVisible();
const drawerTitle = drawer.locator('[data-slot="drawer-title"]');
await expect(drawerTitle).toHaveCSS("text-wrap", "balance");
const taskRow = drawer.getByTestId("sidebar-task-item").filter({ hasText: title });
await expect(taskRow).toBeVisible();
await taskRow.locator("button.mobile-task-actions-button").tap();
const menu = testPage.locator('[data-slot="context-menu-content"]:visible').last();
await menu.getByRole("menuitem", { name: "Archive", exact: true }).tap();
const confirmation = taskRow.getByTestId("task-archive-inline-confirmation");
await expect(confirmation).toBeVisible();
await expect(testPage.getByRole("alertdialog")).toHaveCount(0);
await expect(confirmation).toContainText(title);
await assertNoDocumentHorizontalOverflow(testPage, "phone inline archive confirmation");
for (const button of [
confirmation.getByRole("button", { name: "Cancel", exact: true }),
confirmation.getByTestId("archive-task-confirm"),
]) {
const box = await button.boundingBox();
if (!box) throw new Error("phone inline archive action has no rendered hitbox");
expect(Math.round(box.width)).toBeGreaterThanOrEqual(44);
expect(Math.round(box.height)).toBeGreaterThanOrEqual(44);
await expect(button).toBeInViewport();
}
await confirmation.getByRole("button", { name: "Cancel", exact: true }).tap();
await expect(confirmation).toBeHidden();
expect((await apiClient.getTask(task.task_id)).id).toBe(task.task_id);
});
// @covers AC-UI-SURFACE-TEXT-HIERARCHY-001.1, AC-UI-SURFACE-TEXT-HIERARCHY-001.3
// @covers AC-UI-SURFACE-TEXT-HIERARCHY-001.5
test("uses balanced Alert titles and pretty inline Alert prose on a phone", async ({
testPage,
apiClient,
seedData,
}) => {
await testPage.setViewportSize({ width: 320, height: 480 });
const task = await apiClient.seedTask(seedData.workspaceId, "Mobile runtime alert target", {
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
});
await stubAgentRuntimeRestart(testPage);
await testPage.goto(`/t/${task.task_id}`);
await expect(testPage.getByTestId("mobile-task-layout")).toBeVisible();
await setAgentRuntimeAvailability(testPage, {
status: "unavailable",
reason: "agentctl_exited",
occurred_at: "2026-08-08T14:22:52Z",
});
const alert = testPage.getByTestId("agent-runtime-alert");
await expect(alert).toBeVisible();
const title = alert.locator('[data-slot="alert-title"]');
const description = alert.locator('[data-slot="alert-description"]');
await expect(title).toHaveCSS("text-wrap", "balance");
await expect(description).toHaveCSS("text-wrap", "pretty");
await assertNoDescendantOverflowsRight(alert, "phone runtime Alert");
await assertNoDocumentHorizontalOverflow(testPage, "phone runtime Alert");
const action = alert.getByRole("button").first();
await expect(action).toBeVisible({ timeout: 15_000 });
const actionBox = await action.boundingBox();
if (!actionBox) throw new Error("phone runtime Alert action has no rendered hitbox");
expect(Math.round(actionBox.height)).toBeGreaterThanOrEqual(44);
await expect(action).toBeInViewport();
await setAgentRuntimeAvailability(testPage, { status: "available" });
await expect(alert).toHaveCount(0);
});
// @covers AC-UI-SURFACE-TEXT-HIERARCHY-001.1, AC-UI-SURFACE-TEXT-HIERARCHY-001.3
// @covers AC-UI-SURFACE-TEXT-HIERARCHY-001.5
test("keeps the mobile board Drawer title and description inside the viewport", async ({
testPage,
}) => {
await testPage.setViewportSize({ width: 393, height: 640 });
const mobile = new MobileKanbanPage(testPage);
await mobile.goto();
await mobile.boardNavigator.tap();
const drawer = testPage.getByTestId("mobile-board-navigator-drawer");
await expect(drawer).toBeVisible();
const title = drawer.locator('[data-slot="drawer-title"]');
const description = drawer.locator('[data-slot="drawer-description"]');
await expect(title).toHaveCSS("text-wrap", "balance");
await expect(description).toHaveCSS("text-wrap", "pretty");
await assertLocatorWithinViewportX(drawer, "mobile board navigator Drawer");
await assertNoDescendantOverflowsRight(drawer, "mobile board navigator Drawer");
await assertNoDocumentHorizontalOverflow(testPage, "mobile board navigator Drawer");
});
});
+2 -2
View File
@@ -1,6 +1,6 @@
---
created: 2026-08-31
status: in-progress
status: completed
requirements:
- REQ-UI-SURFACE-TEXT-HIERARCHY-001
- REQ-UI-TASK-CLEANUP-CONFIRMATION-001
@@ -107,7 +107,7 @@ the final composition.
- [x] [Task 02: Normalize structured app confirmations](task-02-normalize-structured-app-confirmations.md) (completed)
- [x] [Task 03: Refine task cleanup confirmations](task-03-refine-task-cleanup-confirmations.md) (completed)
- [x] [Task 04: Normalize structured task confirmations](task-04-normalize-structured-task-confirmations.md) (completed)
- [ ] [Task 05: Add responsive text hierarchy E2E](task-05-add-responsive-text-hierarchy-e2e.md)
- [x] [Task 05: Add responsive text hierarchy E2E](task-05-add-responsive-text-hierarchy-e2e.md) (completed)
## Risks
@@ -1,7 +1,7 @@
---
id: "05-add-responsive-text-hierarchy-e2e"
title: "Add responsive text hierarchy E2E"
status: pending
status: completed
wave: 3
depends_on:
- "02-normalize-structured-app-confirmations"
@@ -111,4 +111,28 @@ pnpm exec eslint e2e/tests/task/mobile-confirmation-text-hierarchy.spec.ts e2e/t
## Results
Pending implementation.
- RED baseline on the pre-fix base: the mobile suite had 3 of 4 tests fail on
missing balanced Drawer and Alert titles, and the desktop suite had 2 of 2
tests fail on missing pretty archive prose and balanced Dialog titles.
- Integrated Task 04 RED: the mobile suite passed 4 tests, while the desktop
suite had 1 failure because the archive PopoverDescription computed
`text-wrap: wrap` instead of `pretty`. Task 04 added the narrow `wide`
popover contract fix before final verification.
- Added six rendered tests across phone and desktop flows. They cover real
task deletion, archive, Alert, board Drawer, Dialog, and Delete surfaces
with long content, pseudo-localized copy, computed wrapping, structured
alignment, viewport and document containment, scroll ownership, action
geometry, semantic Delete styling, Cancel, and task survival.
- GREEN: `pnpm e2e:run --project mobile-chrome
tests/task/mobile-confirmation-text-hierarchy.spec.ts` (4 tests).
- GREEN: `pnpm e2e:run --project chromium
tests/task/confirmation-text-hierarchy.spec.ts` (2 tests).
- GREEN: `pnpm e2e:run --project chromium
tests/task/dialog-long-text-overflow.spec.ts
tests/task/sidebar-delete-confirm.spec.ts` (2 tests).
- GREEN: `pnpm run e2e:sleep-ratchet` and focused ESLint for both new specs.
- Review remediation synchronized the completed parent plan and removed
redundant direct computed-style reads in favor of Playwright's retrying CSS
assertions. Final reruns remained green: mobile 4/4, desktop 2/2, and existing
long-dialog/delete regressions 2/2; focused lint, Prettier, the sleep ratchet,
diff checks, and specification lint also passed.