mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
test(e2e): drive the bottom sheet on WebKit with an iPhone profile
A webkit-iphone project runs one spec: drag, spring back, flick, scroll before drag, a slider sideways, the three closes, focus return, the page's scroll position and pull-to-refresh. CI installs WebKit beside Chromium.
This commit is contained in:
1 parent
55211c0e69
commit
72e6fd6620
3 files changed
+504
-1
No files matched your search
@@ -81,7 +81,9 @@ jobs:
|
||||
restore-keys: |
|
||||
playwright-${{ runner.os }}-
|
||||
|
||||
- run: pnpm exec playwright install --with-deps chromium
|
||||
# WebKit for the one phone spec that needs Safari's engine
|
||||
# (`bottom-sheet.spec.ts`, the `webkit-iphone` project).
|
||||
- run: pnpm exec playwright install --with-deps chromium webkit
|
||||
|
||||
# `pnpm build` (not bare `next build`): the prebuild hook generates
|
||||
# the versioned service-worker stamp AND the static i18n catalog
|
||||
|
||||
@@ -0,0 +1,485 @@
|
||||
import type { Locator, Page } from "@playwright/test";
|
||||
|
||||
import { mockDay } from "./setup/day-mock";
|
||||
import { expect, test } from "./setup/test";
|
||||
import { MOBILE_ROUTES_STORAGE_STATE_PATH } from "./setup/global-setup";
|
||||
|
||||
/**
|
||||
* The bottom sheet's drag on WebKit with an iPhone profile (the
|
||||
* `webkit-iphone` project), against the three kinds of bottom sheet the app
|
||||
* has: the More sheet (a plain `<Sheet side="bottom">`), the day sheet (the
|
||||
* preview step) and a capture form (`ResponsiveSheet` on a phone).
|
||||
*
|
||||
* Playwright can tap on WebKit but not move a touch, so a drag is dispatched
|
||||
* through the page as the browser would deliver it: `pointerdown` /
|
||||
* `pointermove` / `pointerup` with `pointerType: "touch"` and the matching
|
||||
* `touchstart` / `touchmove` / `touchend`, on the element under the finger,
|
||||
* paced in real time so the release speed is real. Nothing calls into the
|
||||
* sheet's own code. What synthetic events cannot do is scroll natively, so
|
||||
* "content scrolls first" is proven as "a scrolled body does not move the
|
||||
* sheet", with the scroll position set the way a scroll would leave it.
|
||||
*
|
||||
* Stable hooks only: `bottom-nav-more` / `bottom-nav-more-sheet`,
|
||||
* `day-panel` with `data-sheet-snap`, `day-body`, `day-footer`,
|
||||
* `sheet-grip`, `sheet-close-button`, `sheet-overlay`, `capture-picker-*`.
|
||||
*/
|
||||
|
||||
interface Drag {
|
||||
dx?: number;
|
||||
dy: number;
|
||||
/** Duration of the movement, ms. */
|
||||
ms: number;
|
||||
steps?: number;
|
||||
/** Rest before lifting the finger, ms (a slow, deliberate release). */
|
||||
holdMs?: number;
|
||||
}
|
||||
|
||||
/** A touch drag from (x, y), dispatched as WebKit delivers one. */
|
||||
async function touchDrag(page: Page, x: number, y: number, drag: Drag) {
|
||||
await page.evaluate(
|
||||
async ({ x, y, dx, dy, ms, steps, holdMs }) => {
|
||||
const target = document.elementFromPoint(x, y);
|
||||
if (!target) throw new Error(`nothing under ${x},${y}`);
|
||||
const id = 11;
|
||||
const touchAt = (cx: number, cy: number) => {
|
||||
const init = {
|
||||
identifier: id,
|
||||
target,
|
||||
clientX: cx,
|
||||
clientY: cy,
|
||||
pageX: cx,
|
||||
pageY: cy,
|
||||
screenX: cx,
|
||||
screenY: cy,
|
||||
radiusX: 4,
|
||||
radiusY: 4,
|
||||
force: 1,
|
||||
};
|
||||
try {
|
||||
return new Touch(init);
|
||||
} catch {
|
||||
return init as unknown as Touch;
|
||||
}
|
||||
};
|
||||
const touchEvent = (type: string, cx: number, cy: number) => {
|
||||
const touch = touchAt(cx, cy);
|
||||
const touches = type === "touchend" ? [] : [touch];
|
||||
try {
|
||||
return new TouchEvent(type, {
|
||||
touches,
|
||||
targetTouches: touches,
|
||||
changedTouches: [touch],
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
composed: true,
|
||||
});
|
||||
} catch {
|
||||
const event = new Event(type, {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
composed: true,
|
||||
});
|
||||
Object.defineProperty(event, "touches", { value: touches });
|
||||
Object.defineProperty(event, "targetTouches", { value: touches });
|
||||
Object.defineProperty(event, "changedTouches", { value: [touch] });
|
||||
return event;
|
||||
}
|
||||
};
|
||||
const fire = (
|
||||
pointer: "pointerdown" | "pointermove" | "pointerup",
|
||||
cx: number,
|
||||
cy: number,
|
||||
) => {
|
||||
target.dispatchEvent(
|
||||
new PointerEvent(pointer, {
|
||||
pointerId: id,
|
||||
pointerType: "touch",
|
||||
isPrimary: true,
|
||||
clientX: cx,
|
||||
clientY: cy,
|
||||
buttons: pointer === "pointerup" ? 0 : 1,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
const touch = {
|
||||
pointerdown: "touchstart",
|
||||
pointermove: "touchmove",
|
||||
pointerup: "touchend",
|
||||
}[pointer];
|
||||
target.dispatchEvent(touchEvent(touch, cx, cy));
|
||||
};
|
||||
const wait = (t: number) => new Promise((r) => setTimeout(r, t));
|
||||
fire("pointerdown", x, y);
|
||||
for (let i = 1; i <= steps; i += 1) {
|
||||
await wait(ms / steps);
|
||||
fire("pointermove", x + (dx * i) / steps, y + (dy * i) / steps);
|
||||
}
|
||||
if (holdMs) await wait(holdMs);
|
||||
fire("pointerup", x + dx, y + dy);
|
||||
},
|
||||
{
|
||||
x,
|
||||
y,
|
||||
dx: drag.dx ?? 0,
|
||||
dy: drag.dy,
|
||||
ms: drag.ms,
|
||||
steps: drag.steps ?? Math.max(4, Math.round(drag.ms / 16)),
|
||||
holdMs: drag.holdMs ?? 0,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The browser's pointer capture for a dispatched pointer. A real touch is
|
||||
* captured by the browser; a dispatched one is not, and Radix's slider only
|
||||
* follows a pointer it holds. This gives it the same bookkeeping, nothing
|
||||
* more: events still go to the element they are dispatched on.
|
||||
*/
|
||||
async function emulateCapture(page: Page) {
|
||||
await page.addInitScript(() => {
|
||||
const held = new WeakMap<Element, Set<number>>();
|
||||
Element.prototype.setPointerCapture = function (id: number) {
|
||||
const set = held.get(this) ?? new Set<number>();
|
||||
set.add(id);
|
||||
held.set(this, set);
|
||||
};
|
||||
Element.prototype.hasPointerCapture = function (id: number) {
|
||||
return held.get(this)?.has(id) ?? false;
|
||||
};
|
||||
Element.prototype.releasePointerCapture = function (id: number) {
|
||||
held.get(this)?.delete(id);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function centre(locator: Locator) {
|
||||
const box = await locator.boundingBox();
|
||||
if (!box) throw new Error("not on screen");
|
||||
return { x: box.x + box.width / 2, y: box.y + box.height / 2, box };
|
||||
}
|
||||
|
||||
async function topOf(locator: Locator) {
|
||||
return (await locator.boundingBox())?.y ?? NaN;
|
||||
}
|
||||
|
||||
/** Waits for the sheet to come to rest (open slide or settle finished). */
|
||||
async function settled(locator: Locator) {
|
||||
await expect
|
||||
.poll(() =>
|
||||
locator.evaluate((el) =>
|
||||
el.getAnimations().every((a) => a.playState !== "running"),
|
||||
),
|
||||
)
|
||||
.toBe(true);
|
||||
await locator.page().waitForTimeout(300);
|
||||
}
|
||||
|
||||
const more = (page: Page) =>
|
||||
page.locator('[data-testid="bottom-nav-more-sheet"]');
|
||||
|
||||
async function openMore(page: Page) {
|
||||
await page.locator('[data-testid="bottom-nav-more"]').tap();
|
||||
await expect(more(page)).toBeVisible();
|
||||
await settled(more(page));
|
||||
}
|
||||
|
||||
function isoDaysAgo(days: number): string {
|
||||
const d = new Date(Date.now() - days * 86_400_000);
|
||||
return d.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
test.describe("the bottom sheet", () => {
|
||||
test.use({ storageState: MOBILE_ROUTES_STORAGE_STATE_PATH });
|
||||
|
||||
test.beforeEach(async ({ page, context }, testInfo) => {
|
||||
const baseURL = testInfo.project.use.baseURL ?? "http://localhost:3000";
|
||||
await context.addCookies([
|
||||
{ name: "healthlog-locale", value: "en", url: baseURL },
|
||||
]);
|
||||
await mockDay(page);
|
||||
});
|
||||
|
||||
test("a slow drag past a quarter of its height closes it", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/mood");
|
||||
await openMore(page);
|
||||
const title = more(page).getByRole("heading").first();
|
||||
const { x, y } = await centre(title);
|
||||
const { box } = await centre(more(page));
|
||||
await touchDrag(page, x, y, {
|
||||
dy: box.height * 0.4,
|
||||
ms: 700,
|
||||
holdMs: 160,
|
||||
});
|
||||
await expect(more(page)).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("a short drag springs back", async ({ page }) => {
|
||||
await page.goto("/mood");
|
||||
await openMore(page);
|
||||
await expect(more(page)).toBeVisible();
|
||||
const before = await topOf(more(page));
|
||||
const { x, y } = await centre(more(page).getByRole("heading").first());
|
||||
// Dragging: the sheet follows the finger and the backdrop lightens.
|
||||
const during = page.evaluate(
|
||||
() =>
|
||||
new Promise<{ moved: number; opacity: number }>((resolve) => {
|
||||
setTimeout(() => {
|
||||
const sheet = document.querySelector(
|
||||
'[data-testid="bottom-nav-more-sheet"]',
|
||||
) as HTMLElement;
|
||||
const overlay = document.querySelector(
|
||||
'[data-slot="sheet-overlay"]',
|
||||
) as HTMLElement;
|
||||
resolve({
|
||||
moved: new DOMMatrix(getComputedStyle(sheet).transform).f,
|
||||
opacity: Number(getComputedStyle(overlay).opacity),
|
||||
});
|
||||
}, 380);
|
||||
}),
|
||||
);
|
||||
await touchDrag(page, x, y, { dy: 60, ms: 300, holdMs: 200 });
|
||||
const seen = await during;
|
||||
expect(seen.moved).toBeGreaterThan(40);
|
||||
expect(seen.opacity).toBeLessThan(1);
|
||||
await settled(more(page));
|
||||
await expect(more(page)).toBeVisible();
|
||||
expect(Math.abs((await topOf(more(page))) - before)).toBeLessThan(1);
|
||||
});
|
||||
|
||||
test("a fast flick closes it", async ({ page }) => {
|
||||
await page.goto("/mood");
|
||||
await openMore(page);
|
||||
const { x, y } = await centre(more(page).getByRole("heading").first());
|
||||
const { box } = await centre(more(page));
|
||||
// Well short of a quarter of the height, but fast.
|
||||
await touchDrag(page, x, y, {
|
||||
dy: Math.min(70, box.height * 0.2),
|
||||
ms: 40,
|
||||
steps: 4,
|
||||
});
|
||||
await expect(more(page)).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("the backdrop, the X and Escape close it; focus goes back to the trigger", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/mood");
|
||||
const trigger = page.locator('[data-testid="bottom-nav-more"]');
|
||||
|
||||
await openMore(page);
|
||||
await page.touchscreen.tap(195, 40);
|
||||
await expect(more(page)).toHaveCount(0);
|
||||
|
||||
await openMore(page);
|
||||
const close = more(page).locator('[data-slot="sheet-close-button"]');
|
||||
const closeBox = (await close.boundingBox())!;
|
||||
expect(closeBox.width).toBeGreaterThanOrEqual(44);
|
||||
expect(closeBox.height).toBeGreaterThanOrEqual(44);
|
||||
await close.tap();
|
||||
await expect(more(page)).toHaveCount(0);
|
||||
|
||||
// The keyboard path: open from the focused trigger, close with Escape.
|
||||
await trigger.focus();
|
||||
await page.keyboard.press("Enter");
|
||||
await expect(more(page)).toBeVisible();
|
||||
await expect(more(page)).toHaveAttribute("role", "dialog");
|
||||
await expect(more(page)).toHaveAttribute("aria-modal", "true");
|
||||
await expect(more(page)).toHaveAccessibleName(/more/i);
|
||||
await expect
|
||||
.poll(() =>
|
||||
more(page).evaluate((el) => el.contains(document.activeElement)),
|
||||
)
|
||||
.toBe(true);
|
||||
// Tab stays inside.
|
||||
for (let i = 0; i < 14; i += 1) await page.keyboard.press("Tab");
|
||||
expect(
|
||||
await more(page).evaluate((el) => el.contains(document.activeElement)),
|
||||
).toBe(true);
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(more(page)).toHaveCount(0);
|
||||
await expect(trigger).toBeFocused();
|
||||
});
|
||||
|
||||
test("the page behind keeps its scroll position and is never pulled to refresh", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
const main = page.locator("#main-content");
|
||||
await expect(main).toBeVisible();
|
||||
await page.waitForTimeout(800);
|
||||
|
||||
// Watch for the refresh disc for the rest of the test.
|
||||
await page.evaluate(() => {
|
||||
(window as unknown as { ptrSeen: number }).ptrSeen = 0;
|
||||
new MutationObserver(() => {
|
||||
if (document.querySelector('[data-slot="pull-to-refresh-indicator"]')) {
|
||||
(window as unknown as { ptrSeen: number }).ptrSeen += 1;
|
||||
}
|
||||
}).observe(document.body, { childList: true, subtree: true });
|
||||
});
|
||||
const ptrSeen = () =>
|
||||
page.evaluate(() => (window as unknown as { ptrSeen: number }).ptrSeen);
|
||||
|
||||
// The control: the same pull on the page itself does arm the disc, so
|
||||
// the watch above can see it (short of the refresh threshold).
|
||||
await touchDrag(page, 195, 320, { dy: 90, ms: 400, holdMs: 120 });
|
||||
await expect.poll(ptrSeen).toBeGreaterThan(0);
|
||||
await page.waitForTimeout(300);
|
||||
await page.evaluate(() => {
|
||||
(window as unknown as { ptrSeen: number }).ptrSeen = 0;
|
||||
});
|
||||
|
||||
// A pull on the sheet, short (it springs back) and long (it closes),
|
||||
// from the dashboard at the top: the page behind never takes it.
|
||||
await openMore(page);
|
||||
const { x, y } = await centre(more(page).getByRole("heading").first());
|
||||
await touchDrag(page, x, y, { dy: 60, ms: 400, holdMs: 150 });
|
||||
await settled(more(page));
|
||||
const { box } = await centre(more(page));
|
||||
await touchDrag(page, x, y, { dy: box.height * 0.5, ms: 600, holdMs: 150 });
|
||||
await expect(more(page)).toHaveCount(0);
|
||||
expect(await ptrSeen()).toBe(0);
|
||||
|
||||
// Scrolled down, the page is where it was after the sheet has gone.
|
||||
await main.evaluate((el) => el.scrollTo({ top: 400 }));
|
||||
const before = await main.evaluate((el) => el.scrollTop);
|
||||
expect(before).toBeGreaterThan(0);
|
||||
await openMore(page);
|
||||
const at = await centre(more(page).getByRole("heading").first());
|
||||
const sheetBox = (await more(page).boundingBox())!;
|
||||
await touchDrag(page, at.x, at.y, {
|
||||
dy: sheetBox.height * 0.5,
|
||||
ms: 600,
|
||||
holdMs: 150,
|
||||
});
|
||||
await expect(more(page)).toHaveCount(0);
|
||||
expect(await main.evaluate((el) => el.scrollTop)).toBe(before);
|
||||
expect(await ptrSeen()).toBe(0);
|
||||
});
|
||||
|
||||
test("the day opens on its preview step, rises on a pull up, and its content scrolls before it drags", async ({
|
||||
page,
|
||||
}) => {
|
||||
// A short window, so the day's content is longer than its sheet on
|
||||
// both steps and the body really scrolls.
|
||||
await page.setViewportSize({ width: 390, height: 480 });
|
||||
await page.goto(`/mood?day=${isoDaysAgo(3)}`);
|
||||
const panel = page.locator('[data-slot="day-panel"]');
|
||||
await expect(panel).toBeVisible();
|
||||
await expect(panel).toHaveAttribute("data-shell", "bottom");
|
||||
await expect(panel).toHaveAttribute("data-sheet-snap", "preview");
|
||||
await settled(panel);
|
||||
const viewport = page.viewportSize()!;
|
||||
|
||||
// On the preview step the footer stands on the window's bottom edge.
|
||||
const footer = panel.locator('[data-slot="day-footer"]');
|
||||
const footerBox = (await footer.boundingBox())!;
|
||||
expect(
|
||||
Math.abs(footerBox.y + footerBox.height - viewport.height),
|
||||
).toBeLessThan(2);
|
||||
|
||||
// A pull up on the content raises it to the full 90 dvh.
|
||||
const body = panel.locator('[data-slot="day-body"]');
|
||||
const b = await centre(body);
|
||||
await touchDrag(page, b.x, b.y, { dy: -180, ms: 500, holdMs: 150 });
|
||||
await expect(panel).toHaveAttribute("data-sheet-snap", "full");
|
||||
await settled(panel);
|
||||
expect(await topOf(panel)).toBeCloseTo(viewport.height * 0.1, -1);
|
||||
|
||||
// Scrolled content: a pull down scrolls (the browser's), the sheet stays.
|
||||
const scrollable = await body.evaluate(
|
||||
(el) => el.scrollHeight > el.clientHeight,
|
||||
);
|
||||
expect(scrollable).toBe(true);
|
||||
await body.evaluate((el) => el.scrollTo({ top: 60 }));
|
||||
const top = await topOf(panel);
|
||||
const inner = await centre(body);
|
||||
await touchDrag(page, inner.x, inner.y, {
|
||||
dy: 260,
|
||||
ms: 500,
|
||||
holdMs: 150,
|
||||
});
|
||||
await settled(panel);
|
||||
await expect(panel).toHaveAttribute("data-sheet-snap", "full");
|
||||
expect(Math.abs((await topOf(panel)) - top)).toBeLessThan(1);
|
||||
|
||||
// At its very top the same pull drags the sheet: down to the preview
|
||||
// step, and from there out.
|
||||
await body.evaluate((el) => el.scrollTo({ top: 0 }));
|
||||
await touchDrag(page, inner.x, inner.y - 60, {
|
||||
dy: 150,
|
||||
ms: 500,
|
||||
holdMs: 150,
|
||||
});
|
||||
await expect(panel).toHaveAttribute("data-sheet-snap", "preview");
|
||||
await settled(panel);
|
||||
const grip = panel.locator('[data-slot="sheet-grip"]');
|
||||
const g = await centre(grip);
|
||||
await touchDrag(page, g.x, g.y, { dy: 300, ms: 600, holdMs: 150 });
|
||||
await expect(panel).toHaveCount(0);
|
||||
await expect(page).toHaveURL(/\/mood$/);
|
||||
});
|
||||
|
||||
test("the grip is a button that switches the day's two heights", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto(`/mood?day=${isoDaysAgo(3)}`);
|
||||
const panel = page.locator('[data-slot="day-panel"]');
|
||||
await expect(panel).toHaveAttribute("data-sheet-snap", "preview");
|
||||
const grip = panel.locator('[data-slot="sheet-grip"]');
|
||||
await expect(grip).toHaveAttribute("aria-expanded", "false");
|
||||
await grip.tap();
|
||||
await expect(panel).toHaveAttribute("data-sheet-snap", "full");
|
||||
await expect(grip).toHaveAttribute("aria-expanded", "true");
|
||||
});
|
||||
|
||||
test("a slider inside a sheet moves sideways and the sheet stays", async ({
|
||||
page,
|
||||
}) => {
|
||||
await emulateCapture(page);
|
||||
await page.goto("/mood");
|
||||
await page.locator('[data-testid="bottom-nav-capture"]').tap();
|
||||
await page.locator('[data-testid="capture-picker-symptom"]').tap();
|
||||
const form = page.locator(
|
||||
'[data-slot="responsive-sheet-content"][data-state="open"]',
|
||||
);
|
||||
await expect(form).toBeVisible();
|
||||
await settled(form);
|
||||
const slider = form.locator('[data-slot="slider"]').first();
|
||||
await slider.scrollIntoViewIfNeeded();
|
||||
await settled(form);
|
||||
const top = await topOf(form);
|
||||
const s = await centre(slider);
|
||||
const thumb = form.locator('[role="slider"]').first();
|
||||
const valueOf = async () =>
|
||||
(await thumb.count()) ? thumb.getAttribute("aria-valuenow") : null;
|
||||
const before = await valueOf();
|
||||
// A sideways drag with a little vertical wobble, across the track.
|
||||
await touchDrag(page, s.box.x + 8, s.y, {
|
||||
dx: s.box.width * 0.7,
|
||||
dy: 6,
|
||||
ms: 400,
|
||||
});
|
||||
await expect.poll(valueOf).not.toBe(before);
|
||||
await settled(form);
|
||||
await expect(form).toBeVisible();
|
||||
expect(Math.abs((await topOf(form)) - top)).toBeLessThan(1);
|
||||
});
|
||||
|
||||
test("under reduced motion it opens without a slide", async ({ page }) => {
|
||||
await page.emulateMedia({ reducedMotion: "reduce" });
|
||||
await page.goto("/mood");
|
||||
await page.locator('[data-testid="bottom-nav-more"]').tap();
|
||||
await expect(more(page)).toBeVisible();
|
||||
expect(
|
||||
await more(page).evaluate(
|
||||
(el) =>
|
||||
el.getAnimations().filter((a) => a.playState === "running").length,
|
||||
),
|
||||
).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -113,6 +113,8 @@ export default defineConfig({
|
||||
testIgnore: [
|
||||
"v137-record-session-fence-offline.spec.ts",
|
||||
"pwa-offline.spec.ts",
|
||||
// Runs only in the WebKit phone project.
|
||||
"bottom-sheet.spec.ts",
|
||||
],
|
||||
use: {
|
||||
...devices["Desktop Chrome"],
|
||||
@@ -228,6 +230,8 @@ export default defineConfig({
|
||||
// Runs only in the service-worker project.
|
||||
"v137-record-session-fence-offline.spec.ts",
|
||||
"pwa-offline.spec.ts",
|
||||
// Runs only in the WebKit phone project.
|
||||
"bottom-sheet.spec.ts",
|
||||
// The Apple Health import journey uploads an archive into the one
|
||||
// shared account and its refusal control asserts that the account's
|
||||
// Apple-Health row count did not move. A second project running the
|
||||
@@ -304,6 +308,18 @@ export default defineConfig({
|
||||
colorScheme: "dark",
|
||||
},
|
||||
},
|
||||
{
|
||||
// The bottom sheet's drag, on the engine it is for: iOS Safari is
|
||||
// WebKit, and Chromium's touch scrolling and pointer capture behave
|
||||
// differently enough that a pass there proves little about an iPhone.
|
||||
// One spec only, so the extra browser costs the shards seconds.
|
||||
name: "webkit-iphone",
|
||||
testMatch: ["bottom-sheet.spec.ts"],
|
||||
use: {
|
||||
...devices["iPhone 13"],
|
||||
colorScheme: "dark",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
// Spin up the production build for E2E. `pnpm build` is run
|
||||
|
||||
Reference in new issue
Block a user