From 0ec06c83976c634c761d4c099f12915aadeb795f Mon Sep 17 00:00:00 2001
From: Basi Angulo <6586019+redbasi@users.noreply.github.com>
Date: Sat, 12 Sep 2026 17:07:16 -0700
Subject: [PATCH 01/22] test: stabilize browser fixtures and assertions
---
playwright.config.mjs | 12 ++-
tests/e2e/day-selection-feedback.spec.js | 24 ++---
tests/e2e/element-branding.spec.js | 5 +-
tests/e2e/helpers.js | 36 +++++++
tests/e2e/responsive-layout.spec.js | 15 +--
tests/e2e/screen-reader-semantics.spec.js | 4 +-
tests/e2e/swipe-gestures.spec.js | 110 +++++-----------------
7 files changed, 94 insertions(+), 112 deletions(-)
diff --git a/playwright.config.mjs b/playwright.config.mjs
index ce3bb9d..52d942a 100644
--- a/playwright.config.mjs
+++ b/playwright.config.mjs
@@ -22,6 +22,7 @@ export default defineConfig({
timeout: 5_000
},
forbidOnly: Boolean(process.env["CI"]),
+ failOnFlakyTests: true,
fullyParallel: true,
globalSetup: "./tests/e2e/global-setup.js",
outputDir: "test-results/playwright",
@@ -34,19 +35,26 @@ export default defineConfig({
},
{
name: "firefox",
+ grepInvert: /@chromium-input/u,
use: {
...devices["Desktop Firefox"]
}
},
{
name: "webkit",
+ grepInvert: /@chromium-input/u,
use: {
...devices["Desktop Safari"]
}
}
],
reporter: process.env["CI"]
- ? [["github"], ["line"]]
+ ? [
+ ["github"],
+ ["line"],
+ ["json", { outputFile: "test-results/browser-results.json" }],
+ ["html", { open: "never", outputFolder: "test-results/browser-report" }]
+ ]
: [["list"]],
retries: process.env["CI"] ? 1 : 0,
testDir: "./tests/e2e",
@@ -61,5 +69,5 @@ export default defineConfig({
trace: "retain-on-failure",
video: "off"
},
- workers: 2
+ workers: process.env["CI"] ? 1 : 2
});
diff --git a/tests/e2e/day-selection-feedback.spec.js b/tests/e2e/day-selection-feedback.spec.js
index ec48fff..a7aef66 100644
--- a/tests/e2e/day-selection-feedback.spec.js
+++ b/tests/e2e/day-selection-feedback.spec.js
@@ -1,6 +1,6 @@
import { expect, test } from "@playwright/test";
-import { expectExampleReady, expectOnlyOneGridTabStop } from "./helpers.js";
+import { expectLibraryFixtureReady, expectOnlyOneGridTabStop } from "./helpers.js";
const ANIMATION_NAME = "lfc-day-selection-reveal";
const CONFIRM_ANIMATION_NAME = "lfc-day-selection-confirm";
@@ -8,11 +8,11 @@ const FEEDBACK_ANIMATION_NAMES = [ANIMATION_NAME, CONFIRM_ANIMATION_NAME].sort()
const TARGET_DATE = "2026-08-31";
const SECOND_TARGET_DATE = "2026-08-30";
-test.describe("when motion is allowed", () => {
+test.describe("when motion is allowed", { tag: "@firefox-regression" }, () => {
test.use({ reducedMotion: "no-preference" });
test("an immediate pointer click produces one coherent selection transition with a seamless cleanup", async ({ page }) => {
- await expectExampleReady(page, "/examples/advanced/");
+ await expectLibraryFixtureReady(page, { calendar: true });
const grid = page.getByRole("grid");
const target = grid.locator(`button[data-lfc-date="${TARGET_DATE}"]`);
await armSelectionProbe(target);
@@ -84,7 +84,7 @@ test.describe("when motion is allowed", () => {
});
test("native Today navigation stays settled while direct current-day selection transitions", async ({ page }) => {
- await expectExampleReady(page, "/examples/advanced/");
+ await expectLibraryFixtureReady(page, { calendar: true });
const grid = page.getByRole("grid");
const title = page.locator(".lfc-calendar-title-label-full");
await page.locator(".lfc-calendar-nav-button-next").click();
@@ -124,7 +124,7 @@ test.describe("when motion is allowed", () => {
});
test("rapid reselection cancels stale feedback and leaves only the latest transition", async ({ page }) => {
- await expectExampleReady(page, "/examples/advanced/");
+ await expectLibraryFixtureReady(page, { calendar: true });
const grid = page.getByRole("grid");
const firstTarget = grid.locator(`button[data-lfc-date="${TARGET_DATE}"]`);
await armSelectionProbe(firstTarget);
@@ -153,7 +153,7 @@ test.describe("when motion is allowed", () => {
});
test("keyboard activation receives the same transition and immediate semantics", async ({ page }) => {
- await expectExampleReady(page, "/examples/advanced/");
+ await expectLibraryFixtureReady(page, { calendar: true });
const grid = page.getByRole("grid");
const target = grid.locator(`button[data-lfc-date="${TARGET_DATE}"]`);
await target.focus();
@@ -189,15 +189,11 @@ test.describe("when motion is allowed", () => {
});
});
-test.describe("with trusted touch input", () => {
+test.describe("with trusted touch input", { tag: "@chromium-input" }, () => {
test.use({ hasTouch: true, reducedMotion: "no-preference" });
- test.skip(
- ({ browserName }) => browserName !== "chromium",
- "Trusted touch injection uses Chromium CDP, which Playwright does not expose for Firefox or WebKit."
- );
test("a tap hands off pressed-day feedback to the selection transition", async ({ page }) => {
- await expectExampleReady(page, "/examples/advanced/");
+ await expectLibraryFixtureReady(page, { calendar: true });
const grid = page.getByRole("grid");
const target = grid.locator(`button[data-lfc-date="${TARGET_DATE}"]`);
await armSelectionProbe(target);
@@ -239,7 +235,7 @@ test.describe("when reduced motion is requested", () => {
test.use({ reducedMotion: "reduce" });
test("pointer and keyboard selection render their settled state immediately", async ({ page }) => {
- await expectExampleReady(page, "/examples/advanced/");
+ await expectLibraryFixtureReady(page, { calendar: true });
const grid = page.getByRole("grid");
const pointerTarget = grid.locator(`button[data-lfc-date="${TARGET_DATE}"]`);
await pointerTarget.click();
@@ -268,7 +264,7 @@ test.describe("when forced colors are active", () => {
test.use({ forcedColors: "active", reducedMotion: "reduce" });
test("selected and focused state remains visibly outlined without decorative feedback", async ({ page }) => {
- await expectExampleReady(page, "/examples/advanced/");
+ await expectLibraryFixtureReady(page, { calendar: true });
const grid = page.getByRole("grid");
const target = grid.locator(`button[data-lfc-date="${TARGET_DATE}"]`);
await target.focus();
diff --git a/tests/e2e/element-branding.spec.js b/tests/e2e/element-branding.spec.js
index 13a3bd3..b8b4255 100644
--- a/tests/e2e/element-branding.spec.js
+++ b/tests/e2e/element-branding.spec.js
@@ -1,8 +1,9 @@
import { expect, test } from "@playwright/test";
+import { expectLibraryFixtureReady } from "./helpers.js";
+
test("a forwarding DOM proxy cannot cross the public host boundary", async ({ page }) => {
- const response = await page.goto("/examples/basic/", { waitUntil: "domcontentloaded" });
- expect(response?.ok()).toBe(true);
+ await expectLibraryFixtureReady(page);
const result = await page.evaluate(async () => {
const { createCalendar } = await import("/dist/index.js");
diff --git a/tests/e2e/helpers.js b/tests/e2e/helpers.js
index 21f4ac0..0cfebbf 100644
--- a/tests/e2e/helpers.js
+++ b/tests/e2e/helpers.js
@@ -2,6 +2,17 @@ import AxeBuilder from "@axe-core/playwright";
import { expect } from "@playwright/test";
const READY_SELECTOR = 'html[data-test-ready="true"]';
+const LIBRARY_FIXTURE_ROUTE = "/examples/basic/?library-fixture=true";
+const LIBRARY_FIXTURE_DOCUMENT = `
+
+
+
+
+Calendar browser fixture
+
+
+
+`;
const WCAG_TAGS = Object.freeze([
"wcag2a",
"wcag2aa",
@@ -10,6 +21,31 @@ const WCAG_TAGS = Object.freeze([
"wcag22aa"
]);
+export async function expectLibraryFixtureReady(page, { calendar = false } = {}) {
+ //Keep the repository server's CSP and security headers; replace only the example document.
+ await page.route((url) => url.searchParams.get("library-fixture") === "true", async (route) => {
+ const response = await route.fetch();
+ await route.fulfill({ body: LIBRARY_FIXTURE_DOCUMENT, response });
+ }, { times: 1 });
+ const response = await page.goto(LIBRARY_FIXTURE_ROUTE, { waitUntil: "load" });
+ expect(response?.ok(), "Expected the isolated library document to load.").toBe(true);
+ await expect(page.locator(READY_SELECTOR)).toHaveCount(1);
+ if (calendar) {
+ await page.evaluate(async () => {
+ const { createCalendar } = await import("/dist/index.js");
+ const host = document.querySelector("[data-my-calendar]");
+ const instance = createCalendar(host, {
+ events: [],
+ initialDate: "2026-08-06",
+ now: () => new Date("2026-08-07T02:00:00.000Z"),
+ onDaySelect: () => {}
+ });
+ instance.render();
+ });
+ await expect(page.getByRole("grid")).toBeVisible();
+ }
+}
+
export async function expectExampleReady(page, route) {
const response = await page.goto(route, { waitUntil: "commit" });
expect(response?.ok(), `Expected ${route} to return a successful response.`).toBe(true);
diff --git a/tests/e2e/responsive-layout.spec.js b/tests/e2e/responsive-layout.spec.js
index 77cb579..71ba23f 100644
--- a/tests/e2e/responsive-layout.spec.js
+++ b/tests/e2e/responsive-layout.spec.js
@@ -2,6 +2,7 @@ import { expect, test } from "@playwright/test";
import {
expectExampleReady,
+ expectLibraryFixtureReady,
gridEventActions
} from "./helpers.js";
@@ -463,7 +464,7 @@ async function expectCompactToolbarVisualLayout(page, layout) {
async function prepareWeekLayoutFixturePage(page) {
await page.setViewportSize({ height: COMPACT_VIEWPORT_HEIGHT, width: 1_100 });
- await expectExampleReady(page, "/examples/advanced/");
+ await expectLibraryFixtureReady(page);
}
async function mountWeekLayoutFixture(
@@ -733,7 +734,7 @@ async function mountResponsiveMultipleEventFixture(
width = 768
} = {}
) {
- await expectExampleReady(page, "/examples/advanced/");
+ await expectLibraryFixtureReady(page);
await page.addStyleTag({
content: `
.my-responsive-marker {
@@ -1376,7 +1377,7 @@ test("short month labels use the strict 24rem content-box boundary without runti
expect(stability).toEqual({ callsStable: true, nodesStable: true, providerCalls: 1 });
});
-test("default equal rows grow intrinsically and remain stable across responsive layout", async ({ page }) => {
+test("default equal rows grow intrinsically and remain stable across responsive layout", { tag: "@firefox-regression" }, async ({ page }) => {
await prepareWeekLayoutFixturePage(page);
const { host } = await mountWeekLayoutFixture(page);
const initial = await getWeekLayoutGeometry(host);
@@ -1412,9 +1413,11 @@ test("default equal rows grow intrinsically and remain stable across responsive
expect(grown.anchorMarkerBlockSize).toBeGreaterThanOrEqual(192);
expectEqualWeekHeights(grown);
expectWeekLayoutContained(grown);
- expect(Math.min(...grown.weekHeights) - Math.max(...initial.weekHeights))
- .toBeGreaterThan(48);
- expect(grown.calendarBlockSize - initial.calendarBlockSize).toBeGreaterThan(6 * 48);
+ const rowGrowth = grown.weekHeights.map((height, index) => height - initial.weekHeights[index]);
+ expect(Math.min(...rowGrowth), "Every equal row must grow to contain the taller marker.")
+ .toBeGreaterThan(0);
+ expect(grown.calendarBlockSize - initial.calendarBlockSize)
+ .toBeCloseTo(rowGrowth.reduce((total, growth) => total + growth, 0), 0);
for (const width of [390, 768]) {
await setWeekLayoutFixtureWidth(page, width);
diff --git a/tests/e2e/screen-reader-semantics.spec.js b/tests/e2e/screen-reader-semantics.spec.js
index cd641a2..6348ce6 100644
--- a/tests/e2e/screen-reader-semantics.spec.js
+++ b/tests/e2e/screen-reader-semantics.spec.js
@@ -1,6 +1,6 @@
import { expect, test } from "@playwright/test";
-import { expectExampleReady, expectOnlyOneGridTabStop } from "./helpers.js";
+import { expectExampleReady, expectLibraryFixtureReady, expectOnlyOneGridTabStop } from "./helpers.js";
const ANNOUNCEMENT_FIXTURE_SELECTOR = "#my-screen-reader-announcement-fixture";
const FAILURE_FIXTURE_SELECTOR = "#my-screen-reader-failure-fixture";
@@ -163,7 +163,7 @@ test.describe("screen-reader-facing browser semantics", () => {
});
test("routes progress and failures through polite and assertive live regions without stealing focus", async ({ page }) => {
- await expectExampleReady(page, "/examples/advanced/");
+ await expectLibraryFixtureReady(page);
const announcementHost = await mountAnnouncementFixture(page);
const politeLive = announcementHost.getByRole("status");
const assertiveLive = announcementHost.getByRole("alert");
diff --git a/tests/e2e/swipe-gestures.spec.js b/tests/e2e/swipe-gestures.spec.js
index cbd9381..02f0f45 100644
--- a/tests/e2e/swipe-gestures.spec.js
+++ b/tests/e2e/swipe-gestures.spec.js
@@ -1,13 +1,11 @@
import { expect, test } from "@playwright/test";
-import { expectExampleReady } from "./helpers.js";
+import { expectExampleReady, expectLibraryFixtureReady } from "./helpers.js";
const HOST_SELECTOR = "[data-my-calendar]";
const MONTH_SELECTOR = "[data-my-state-month]";
const VIEWPORT_SELECTOR = ".lfc-calendar-swipe-viewport";
const WHEEL_BURST_MAXIMUM_SPAN_MILLISECONDS = 120;
-const CHROMIUM_INPUT_REASON =
- "Trusted touch and pen injection uses Chromium CDP, which Playwright does not expose for Firefox or WebKit.";
async function createChromiumInputClient(page, reducedMotion) {
const client = await page.context().newCDPSession(page);
@@ -19,10 +17,6 @@ async function createChromiumInputClient(page, reducedMotion) {
return client;
}
-function skipWithoutChromiumInput(browserName) {
- test.skip(browserName !== "chromium", CHROMIUM_INPUT_REASON);
-}
-
async function dispatchTouch(client, type, points) {
await client.send("Input.dispatchTouchEvent", {
touchPoints: points.map(({ id = 0, x, y }) => ({
@@ -72,6 +66,7 @@ async function runTouchGesture(page, client, start, deltaX, deltaY, cancel = fal
}
async function mountCalendarFixture(page, options = {}) {
+ await expectLibraryFixtureReady(page);
await page.evaluate(async (fixtureOptions) => {
const { createCalendar } = await import("/dist/index.js");
const fixture = document.createElement("section");
@@ -218,11 +213,8 @@ function pagingLaneLabels(host, direction) {
test.describe("native month pager", () => {
test.use({ reducedMotion: "no-preference" });
- test.beforeEach(async ({ page }) => {
- await expectExampleReady(page, "/examples/advanced/");
- });
-
test("renders hidden semantic lanes around one live grid and settles the current snap", async ({ page }) => {
+ await expectExampleReady(page, "/examples/advanced/");
const host = page.locator(HOST_SELECTOR);
const viewport = host.locator(VIEWPORT_SELECTOR);
await expect(host).toHaveAttribute("data-lfc-swipe-enabled", "true");
@@ -332,7 +324,7 @@ test.describe("native month pager", () => {
await expect(viewport).not.toHaveAttribute("aria-hidden");
});
- test("scrollend requests one month while current and bounded snaps do not", async ({ page }) => {
+ test("scrollend requests one month while current and bounded snaps do not", { tag: "@firefox-regression" }, async ({ page }) => {
const host = await mountCalendarFixture(page, {
maxDate: "2026-09-30",
minDate: "2026-08-01"
@@ -401,7 +393,7 @@ test.describe("native month pager", () => {
await expect(nextLabels.compact).toHaveText(septemberCompact);
});
- test("layout geometry remains exact through an ancestor transform and RTL recentering", async ({ page }) => {
+ test("layout geometry remains exact through an ancestor transform and RTL recentering", { tag: "@firefox-regression" }, async ({ page }) => {
const host = await mountCalendarFixture(page);
const ruleInstalled = await page.evaluate(() => {
for (const sheet of document.styleSheets) {
@@ -467,7 +459,7 @@ test.describe("native month pager", () => {
)).toBe(5);
});
- test("wheel burst timestamps remain one transaction across pager rerenders", async ({ page }) => {
+ test("wheel burst timestamps remain one transaction across pager rerenders", { tag: "@firefox-regression" }, async ({ page }) => {
const host = await mountCalendarFixture(page);
for (const wheelTimeIncrement of [WHEEL_BURST_MAXIMUM_SPAN_MILLISECONDS + 1, 10, 10]) {
await setPagerPosition(host, "next", true, 1, wheelTimeIncrement);
@@ -484,59 +476,8 @@ test.describe("native month pager", () => {
await expectPagerClean(host);
});
- test("a trusted rapid three-event horizontal wheel burst commits exactly one page", async ({ page }) => {
- const host = await mountCalendarFixture(page);
- const point = await gesturePoint(host.locator(
- '.lfc-calendar-day-button[data-lfc-date="2026-08-13"]'
- ), 16);
- await page.evaluate(() => {
- const burst = [];
- Object.defineProperty(window, "__lfcTrustedWheelBurst", {
- configurable: true,
- value: burst
- });
- document.addEventListener("wheel", (event) => {
- if (Math.abs(event.deltaX) > Math.abs(event.deltaY)) {
- burst.push({
- isTrusted: event.isTrusted,
- timeStamp: event.timeStamp
- });
- }
- }, { capture: true, passive: true });
- });
- await page.mouse.move(point.x, point.y);
- for (let eventIndex = 0; eventIndex < 3; eventIndex += 1) {
- await page.mouse.wheel(90, 0);
- }
-
- await expect.poll(() => page.evaluate(() =>
- window.__lfcTrustedWheelBurst.length
- )).toBe(3);
- const trustedBurst = await page.evaluate(() => window.__lfcTrustedWheelBurst);
- expect(trustedBurst).toHaveLength(3);
- expect(trustedBurst.every((event) => event.isTrusted)).toBe(true);
- const maximumObservedGap = Math.max(
- ...trustedBurst.slice(1).map((event, eventIndex) =>
- event.timeStamp - trustedBurst[eventIndex].timeStamp
- )
- );
- test.skip(
- maximumObservedGap > WHEEL_BURST_MAXIMUM_SPAN_MILLISECONDS,
- "The runner did not deliver the trusted wheel events within one burst window."
- );
- await expect.poll(() => page.evaluate(() =>
- window.__lfcSwipeFixture.calendar.getState().displayedMonth.month
- )).toBe(9);
- await page.waitForTimeout(300);
- expect(await page.evaluate(() => ({
- month: window.__lfcSwipeFixture.calendar.getState().displayedMonth.month,
- requests: window.__lfcSwipeFixture.observations.requests.length
- }))).toEqual({ month: 9, requests: 2 });
- await expectPagerClean(host);
- });
-
- test("trusted touch returns short pulls, commits once in LTR and RTL, and stops at bounds", async ({ browserName, page }) => {
- skipWithoutChromiumInput(browserName);
+ test("trusted touch returns short pulls, commits once in LTR and RTL, and stops at bounds", { tag: "@chromium-input" }, async ({ page }) => {
+ await expectExampleReady(page, "/examples/advanced/");
const client = await createChromiumInputClient(page, "no-preference");
const host = page.locator(HOST_SELECTOR);
const viewport = host.locator(VIEWPORT_SELECTOR);
@@ -576,8 +517,8 @@ test.describe("native month pager", () => {
await expect(actionResult).toHaveText(rtlAction ?? "");
});
- test("trusted vertical, multi-touch, and canceled gestures preserve the current month", async ({ browserName, page }) => {
- skipWithoutChromiumInput(browserName);
+ test("trusted vertical, multi-touch, and canceled gestures preserve the current month", { tag: "@chromium-input" }, async ({ page }) => {
+ await expectExampleReady(page, "/examples/advanced/");
const client = await createChromiumInputClient(page, "no-preference");
const host = page.locator(HOST_SELECTOR);
const viewport = host.locator(VIEWPORT_SELECTOR);
@@ -610,6 +551,7 @@ test.describe("native month pager", () => {
});
test("trusted wheel pages horizontally and leaves vertical wheel unhandled", async ({ page }) => {
+ await expectExampleReady(page, "/examples/advanced/");
const host = page.locator(HOST_SELECTOR);
let point = await gesturePoint(page.locator(
'.lfc-calendar-day-button[data-lfc-date="2026-08-13"]'
@@ -648,10 +590,9 @@ test.describe("native month pager", () => {
await expectPagerClean(host);
});
- test("trusted pen input is not intercepted", async ({ browserName, page }) => {
- skipWithoutChromiumInput(browserName);
- const client = await createChromiumInputClient(page, "no-preference");
+ test("trusted pen input is not intercepted", { tag: "@chromium-input" }, async ({ page }) => {
const host = await mountCalendarFixture(page);
+ const client = await createChromiumInputClient(page, "no-preference");
const viewport = host.locator(VIEWPORT_SELECTOR);
await viewport.evaluate((element) => {
@@ -675,23 +616,23 @@ test.describe("native month pager", () => {
await dispatchPen(client, "mousePressed", point, true);
await dispatchPen(client, "mouseMoved", { x: point.x + 80, y: point.y }, true);
await dispatchPen(client, "mouseReleased", { x: point.x + 80, y: point.y }, false);
- const penOutcome = await page.evaluate(() => ({
+ const penOutcome = await host.evaluate((element) => ({
events: window.__lfcPenEvents,
- hasInlineStyle: document.querySelector("[data-my-calendar]")?.matches("[style]") ||
- document.querySelector("[data-my-calendar] [style]") !== null
+ hasInlineStyle: element.matches("[style]") || element.querySelector("[style]") !== null
}));
expect(penOutcome.events.length).toBeGreaterThanOrEqual(3);
expect(penOutcome.events.every((event) =>
event.isTrusted && !event.defaultPrevented && !event.hasCapture
)).toBe(true);
expect(penOutcome.hasInlineStyle).toBe(false);
- await expect(page.locator(MONTH_SELECTOR)).toHaveText("2026-08-01");
+ expect(await page.evaluate(() =>
+ window.__lfcSwipeFixture.calendar.getState().displayedMonth.month
+ )).toBe(8);
});
- test("held touch cannot double-navigate across refetch or programmatic navigation", async ({ browserName, page }) => {
- skipWithoutChromiumInput(browserName);
- const client = await createChromiumInputClient(page, "no-preference");
+ test("held touch cannot double-navigate across refetch or programmatic navigation", { tag: "@chromium-input" }, async ({ page }) => {
const host = await mountCalendarFixture(page);
+ const client = await createChromiumInputClient(page, "no-preference");
const viewport = host.locator(VIEWPORT_SELECTOR);
let point = await gesturePoint(viewport);
@@ -770,10 +711,9 @@ test.describe("native month pager", () => {
}))).toEqual({ month: 8, requests: 1, state: null });
});
- test("swipe false ignores trusted touch paging", async ({ browserName, page }) => {
- skipWithoutChromiumInput(browserName);
- const client = await createChromiumInputClient(page, "no-preference");
+ test("swipe false ignores trusted touch paging", { tag: "@chromium-input" }, async ({ page }) => {
const host = await mountCalendarFixture(page, { swipe: false });
+ const client = await createChromiumInputClient(page, "no-preference");
const point = await gesturePoint(host.locator(VIEWPORT_SELECTOR));
await runTouchGesture(page, client, point, -120, 0);
await page.waitForTimeout(200);
@@ -788,11 +728,9 @@ test.describe("native month pager", () => {
test.describe("native pager with reduced motion", () => {
test.use({ reducedMotion: "reduce" });
- test("tracks directly and has no post-terminal snap interpolation or authored animation", async ({ browserName, page }) => {
- skipWithoutChromiumInput(browserName);
- await expectExampleReady(page, "/examples/advanced/");
- const client = await createChromiumInputClient(page, "reduce");
+ test("tracks directly and has no post-terminal snap interpolation or authored animation", { tag: "@chromium-input" }, async ({ page }) => {
const host = await mountCalendarFixture(page);
+ const client = await createChromiumInputClient(page, "reduce");
const viewport = host.locator(VIEWPORT_SELECTOR);
expect(await page.evaluate(() => ({
noPreference: matchMedia("(prefers-reduced-motion: no-preference)").matches,
From 571191300b8834df45897bb2745bb4d3137eb948 Mon Sep 17 00:00:00 2001
From: Basi Angulo <6586019+redbasi@users.noreply.github.com>
Date: Sat, 12 Sep 2026 17:09:31 -0700
Subject: [PATCH 02/22] ci: reject flaky browser results
---
.github/workflows/ci.yml | 53 ++++++++++++++++++++++-
scripts/qualify-firefox.mjs | 46 ++++++++++++++++++++
scripts/tests/workflow-contracts.test.mjs | 28 +++++++++++-
3 files changed, 124 insertions(+), 3 deletions(-)
create mode 100644 scripts/qualify-firefox.mjs
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 472e752..cf13eb8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -55,8 +55,8 @@ jobs:
- name: Run the complete local quality gate
run: npm run check
- - name: Upload browser diagnostics on failure
- if: failure()
+ - name: Upload browser results and failure diagnostics
+ if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: browser-diagnostics-${{ github.run_id }}
@@ -65,3 +65,52 @@ jobs:
playwright-report/
test-results/
retention-days: 7
+
+ firefox-qualification:
+ name: Qualify Firefox (${{ matrix.os }})
+ runs-on: ${{ matrix.os }}
+ timeout-minutes: 90
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, windows-latest]
+
+ steps:
+ - name: Check out source without persisted credentials
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+
+ - name: Set up exact Node
+ uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
+ with:
+ node-version: ${{ env.LFC_NODE_VERSION }}
+ package-manager-cache: false
+
+ - name: Set up exact npm
+ shell: bash
+ working-directory: ${{ runner.temp }}
+ run: npm install --global "npm@${LFC_NPM_VERSION}" --ignore-scripts
+
+ - name: Install locked development tooling
+ run: npm ci --ignore-scripts
+
+ - name: Install pinned Firefox
+ run: npx playwright install --with-deps firefox
+
+ - name: Build package and examples
+ run: npm run build
+
+ - name: Repeat regressions and qualify three complete Firefox runs
+ env:
+ DEBUG: pw:browser
+ run: node scripts/qualify-firefox.mjs
+
+ - name: Upload Firefox qualification evidence
+ if: always()
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: firefox-qualification-${{ matrix.os }}-${{ github.run_id }}
+ if-no-files-found: ignore
+ path: test-results/firefox-qualification/
+ retention-days: 7
diff --git a/scripts/qualify-firefox.mjs b/scripts/qualify-firefox.mjs
new file mode 100644
index 0000000..5b30997
--- /dev/null
+++ b/scripts/qualify-firefox.mjs
@@ -0,0 +1,46 @@
+import { mkdir, writeFile } from "node:fs/promises";
+import { join } from "node:path";
+
+import { assertSupportedNodeVersion } from "./lib/node-version.mjs";
+import { REPOSITORY_ROOT, run } from "./lib/process.mjs";
+
+assertSupportedNodeVersion("Firefox qualification");
+
+const reportRoot = join(REPOSITORY_ROOT, "test-results", "firefox-qualification");
+const runs = [
+ { name: "regressions-20", arguments: ["--grep=@firefox-regression", "--repeat-each=20"] },
+ ...Array.from({ length: 3 }, (_value, index) => ({
+ name: `complete-${String(index + 1)}`,
+ arguments: []
+ }))
+];
+await mkdir(reportRoot, { recursive: true });
+for (const qualification of runs) {
+ const output = join(reportRoot, qualification.name);
+ await mkdir(output, { recursive: true });
+ await writeFile(join(output, "environment.json"), `${JSON.stringify({
+ arch: process.arch,
+ node: process.version,
+ platform: process.platform,
+ retries: 0,
+ workers: 1
+ }, null, 2)}\n`);
+ await run(process.execPath, [
+ join(REPOSITORY_ROOT, "node_modules", "@playwright", "test", "cli.js"),
+ "test",
+ "--project=firefox",
+ "--workers=1",
+ "--retries=0",
+ "--max-failures=1",
+ "--reporter=line,json,html",
+ `--output=${join(output, "artifacts")}`,
+ ...qualification.arguments
+ ], {
+ env: {
+ ...process.env,
+ PLAYWRIGHT_HTML_OPEN: "never",
+ PLAYWRIGHT_HTML_OUTPUT_DIR: join(output, "html"),
+ PLAYWRIGHT_JSON_OUTPUT_FILE: join(output, "results.json")
+ }
+ });
+}
diff --git a/scripts/tests/workflow-contracts.test.mjs b/scripts/tests/workflow-contracts.test.mjs
index 164741c..5aa4eb6 100644
--- a/scripts/tests/workflow-contracts.test.mjs
+++ b/scripts/tests/workflow-contracts.test.mjs
@@ -608,7 +608,7 @@ void test("release gates and Playwright retain the complete browser-engine matri
void test("workflow artifacts use bounded purpose-specific retention", async () => {
const expectations = new Map([
- ["ci.yml", [7]],
+ ["ci.yml", [7, 7]],
["deploy-examples.yml", [1, 1]],
["prepare-alpha.yml", [1]],
["publish-alpha.yml", [7, 30, 30]],
@@ -658,3 +658,29 @@ void test("workflow dependency caches stay disabled", async () => {
);
}
});
+
+void test("browser gates reject flakes and retain reproducible Firefox qualification evidence", async () => {
+ const source = await workflow("ci.yml");
+ const qualification = job(source, "firefox-qualification");
+ assert.equal(playwrightConfiguration.failOnFlakyTests, true);
+ assert.match(qualification, /os: \[ubuntu-latest, windows-latest\]/u);
+ assert.match(qualification, /node scripts\/qualify-firefox\.mjs/u);
+ assert.match(qualification, /if: always\(\)[\s\S]*?path: test-results\/firefox-qualification\//u);
+ const config = await readFile(join(REPOSITORY_ROOT, "playwright.config.mjs"), "utf8");
+ assert.match(config, /workers: process\.env\["CI"\] \? 1 : 2/u);
+ assert.match(config, /retries: process\.env\["CI"\] \? 1 : 0/u);
+ for (const project of playwrightConfiguration.projects) {
+ if (project.name === "chromium") {
+ assert.equal(project.grepInvert, undefined);
+ } else {
+ assert.equal(project.grepInvert?.test("@chromium-input"), true);
+ }
+ }
+ const qualifier = await readFile(join(REPOSITORY_ROOT, "scripts", "qualify-firefox.mjs"), "utf8");
+ assert.match(qualifier, /--repeat-each=20/u);
+ assert.match(qualifier, /length: 3/u);
+ assert.match(qualifier, /--retries=0/u);
+ assert.match(qualifier, /--workers=1/u);
+ assert.match(qualifier, /PLAYWRIGHT_JSON_OUTPUT_FILE/u);
+ assert.match(qualifier, /PLAYWRIGHT_HTML_OUTPUT_DIR/u);
+});
From a7e2985f998abc65ad0a76555676c668fbd9ee03 Mon Sep 17 00:00:00 2001
From: Basi Angulo <6586019+redbasi@users.noreply.github.com>
Date: Sat, 12 Sep 2026 17:33:18 -0700
Subject: [PATCH 03/22] feat: adopt Vite Baseline compatibility policy
---
ACCESSIBILITY.md | 2 +
CONTRIBUTING.md | 1 +
DESIGN.md | 6 +-
docs/browser-support.md | 34 +-
docs/code-style.md | 4 +
examples/advanced/theme.css | 9 +
examples/example.css | 43 ++
examples/index.css | 1 +
package-lock.json | 754 ++++++++++++++++++++++++-
package.json | 8 +-
scripts/lib/browser-targets.mjs | 82 +++
scripts/report-browser-targets.mjs | 8 +
scripts/tests/browser-targets.test.mjs | 36 ++
src/internal/dom/month-picker.ts | 86 ++-
src/internal/dom/palette.ts | 73 +++
src/internal/dom/structure.ts | 2 +-
src/internal/runtime/coordinator.ts | 5 +
src/styles/agenda.css | 1 +
src/styles/base.css | 2 +
src/styles/month-grid.css | 20 +-
src/styles/preferences.css | 26 -
src/styles/responsive.css | 4 +
src/styles/tokens.css | 88 ++-
src/styles/toolbar.css | 12 +
tests/e2e/baseline-fallbacks.js | 19 +
tests/e2e/browser-baseline.spec.js | 80 +++
tests/e2e/theme-integration.spec.js | 153 +++++
27 files changed, 1456 insertions(+), 103 deletions(-)
create mode 100644 scripts/lib/browser-targets.mjs
create mode 100644 scripts/report-browser-targets.mjs
create mode 100644 scripts/tests/browser-targets.test.mjs
create mode 100644 src/internal/dom/palette.ts
create mode 100644 tests/e2e/baseline-fallbacks.js
create mode 100644 tests/e2e/browser-baseline.spec.js
create mode 100644 tests/e2e/theme-integration.spec.js
diff --git a/ACCESSIBILITY.md b/ACCESSIBILITY.md
index 994d066..c4bcae5 100644
--- a/ACCESSIBILITY.md
+++ b/ACCESSIBILITY.md
@@ -43,6 +43,8 @@ Replacing event input keeps package-owned focus on the same day or event occurre
The presented month and year is a native button inside the configured `h1` through `h6`; a dedicated visible date span alone labels the grid. The button opens the default `popover="auto"` surface exposed as a dialog, synchronizes its labelled month and year controls, and focuses Month. A successful Jump accepts only an in-range month, while Cancel makes no date change; both paths close the popover and return focus to the month/year trigger. Invalid form input remains open for correction. Escape also dismisses the popover and restores trigger focus. Pointer/outside light-dismiss closes it without stealing focus from the user's new target. The package-owned labels are localized through `chooseMonthYear`, `jumpToMonthYear`, `month`, `year`, `jump`, and `cancel`.
+Where Popover is unavailable, the same picker uses a native modal `