Skip to content

Commit 6c24b7d

Browse files
committed
fix(test): the android instance now tests true phone geometry
The shared browser setup forced its 1280x720 iframe onto every project — on the android instance (a 393x727 phone window) the harness then scaled that desktop-width iframe down to fit, so every suite without its own per-test viewport was silently testing desktop layout, optically shrunk. Positional input was displaced by the same transform, which had been misread as 'mouse idioms don't translate to touch emulation'. The setup now sizes the iframe per project. Touch emulation also gets self-healing: Chromium's beyond-viewport screenshot capture (captureBeyondViewport, sent by Playwright for any element taller than the viewport) can silently drop the context's touch emulation. A restoreTouchEmulation command (persistent CDP session — Emulation overrides revert when their session detaches) re-arms it before every android test, and ensureTouchEmulation runs as an automatic assertion right after, so no suite calls it manually anymore. The assert stays because it guards a different failure than the heal: the mechanism itself breaking (provider contextOptions silently ignored, an upgrade rewiring the provider). At true geometry the include list is re-grounded on one principle, stated per entry in the config: a suite runs on this instance when it can go red for a mobile-conditional reason no other suite here pins. form/ drops out — its popover suite drives the desktop link toolbar (hover, clipped at phone width) and its Enter mechanics are pinned red-first by mobile/ and keyboardhandlers/. copypaste/ drops out — the clipboard path has no platform conditionals at all, and its Enter presses are setup scaffolding for routes androidEnter pins directly. emojipicker/ stays: Enter-to-select goes through the suggestion menu's own key handling, a distinct consumer of the synthesized-Enter route.
1 parent e297246 commit 6c24b7d

6 files changed

Lines changed: 83 additions & 28 deletions

File tree

tests/src/end-to-end/mobile/linkSubmit.test.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { render } from "vitest-browser-react";
1212
import { page, userEvent } from "../../utils/context.js";
1313
import { EDITOR_SELECTOR, LINK_BUTTON_SELECTOR } from "../../utils/const.js";
1414
import { waitForSelector } from "../../utils/editor.js";
15-
import { ensureTouchEmulation } from "../../utils/ensureTouchEmulation.js";
1615

1716
const MOBILE_TOOLBAR_SELECTOR = ".bn-mobile-formatting-toolbar";
1817

@@ -33,7 +32,6 @@ const MOBILE_TOOLBAR_SELECTOR = ".bn-mobile-formatting-toolbar";
3332
// test below; the IME's choice itself stays a release-checklist item.
3433

3534
beforeEach(async () => {
36-
ensureTouchEmulation();
3735
await page.viewport(393, 727);
3836
});
3937

tests/src/end-to-end/mobile/mobileToolbar.test.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { render } from "vitest-browser-react";
55
import { page, userEvent } from "../../utils/context.js";
66
import { EDITOR_SELECTOR, LINK_BUTTON_SELECTOR } from "../../utils/const.js";
77
import { focusOnEditor, waitForSelector } from "../../utils/editor.js";
8-
import { ensureTouchEmulation } from "../../utils/ensureTouchEmulation.js";
98

109
const MOBILE_TOOLBAR_SELECTOR = ".bn-mobile-formatting-toolbar";
1110
const LINK_POPOVER_SELECTOR = ".bn-form-popover";
@@ -41,7 +40,6 @@ function activeUrlInput() {
4140
}
4241

4342
beforeEach(async () => {
44-
ensureTouchEmulation();
4543
await page.viewport(VIEWPORT_WIDTH, KEYBOARD_CLOSED);
4644
await render(<App />);
4745
await waitForSelector(EDITOR_SELECTOR);

tests/src/end-to-end/mobile/popoverScroll.test.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { render } from "vitest-browser-react";
1212
import { page, userEvent } from "../../utils/context.js";
1313
import { EDITOR_SELECTOR, LINK_BUTTON_SELECTOR } from "../../utils/const.js";
1414
import { waitForSelector } from "../../utils/editor.js";
15-
import { ensureTouchEmulation } from "../../utils/ensureTouchEmulation.js";
1615

1716
const MOBILE_TOOLBAR_SELECTOR = ".bn-mobile-formatting-toolbar";
1817

@@ -24,7 +23,6 @@ const MOBILE_TOOLBAR_SELECTOR = ".bn-mobile-formatting-toolbar";
2423
// scroll-into-view chased it to its pre-positioned spot.
2524

2625
beforeEach(async () => {
27-
ensureTouchEmulation();
2826
await page.viewport(393, 727);
2927
});
3028

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { BrowserCommand } from "vite-plus/test/node";
2+
3+
/**
4+
* Re-applies the touch emulation the android instance's Playwright
5+
* `contextOptions` established. Chromium's beyond-viewport screenshot
6+
* capture (`Page.captureScreenshot` with `captureBeyondViewport: true`,
7+
* which Playwright sends for any element taller than the viewport) can
8+
* silently drop the context's emulation overrides — `maxTouchPoints`
9+
* becomes 0 for every later test. `vitestSetup.browser.ts` calls this
10+
* before each file on the android instance.
11+
*
12+
* The CDP session is deliberately cached and never detached:
13+
* Emulation-domain overrides revert when the session that set them
14+
* detaches (learned the hard way — a detaching version of this command
15+
* *caused* the exact poison it was meant to heal).
16+
*/
17+
const sessions = new WeakMap<object, Promise<unknown>>();
18+
19+
export const restoreTouchEmulation: BrowserCommand<[]> = async (ctx) => {
20+
let session = sessions.get(ctx.page);
21+
if (session === undefined) {
22+
session = ctx.context.newCDPSession(ctx.page);
23+
sessions.set(ctx.page, session);
24+
}
25+
const cdp = (await session) as {
26+
send(method: string, params: object): Promise<unknown>;
27+
};
28+
// Exactly what Playwright sends for `hasTouch: true` — and nothing more.
29+
// In particular NOT `Emulation.setEmitTouchEventsForMouse`: that converts
30+
// real mouse events into touch events, which breaks every userEvent click
31+
// (learned the hard way; Playwright never enables it).
32+
await cdp.send("Emulation.setTouchEmulationEnabled", { enabled: true });
33+
};

tests/vite.config.browser.ts

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { defineConfig, type UserConfig } from "vite-plus";
55
import { playwright } from "vite-plus/test/browser/providers/playwright";
66
import { positionalMouse } from "./src/utils/positionalMouse.js";
77
import { imeComposition } from "./src/utils/imeComposition.js";
8+
import { restoreTouchEmulation } from "./src/utils/restoreTouchEmulation.js";
89

910
// 1280x720 matches the old Playwright defaults so visual baselines have room.
1011
// Used as the playwright context viewport for every browser instance.
@@ -141,7 +142,7 @@ export default defineConfig(
141142
// still show in the HTML report (errors + stack traces don't depend
142143
// on these shots), so disable them. See `e2e:report` to view.
143144
screenshotFailures: false,
144-
commands: { positionalMouse, imeComposition },
145+
commands: { positionalMouse, imeComposition, restoreTouchEmulation },
145146
instances: [
146147
{
147148
browser: "chromium",
@@ -188,29 +189,28 @@ export default defineConfig(
188189
hasTouch: true,
189190
},
190191
}),
191-
// Mobile-specific tests plus the screenshot-free behavioural
192-
// suites where Android genuinely differs (IME key handling,
193-
// suggestion menus). Those only pass under this emulation with
194-
// the Enter fix in this change — before it, every test that
195-
// presses Enter to make a second block failed here.
196-
//
197-
// Keep iframe-screenshotting suites (the exporters'
198-
// `screenshotFull` previews) out permanently: Playwright's
199-
// element-screenshot path for iframe elements drops the
200-
// context's touch emulation for later files (see
201-
// utils/ensureTouchEmulation.ts). Individual tests that drive
202-
// selection or resizing with positional mouse drags carry
203-
// `skipIf(onAndroid)` guards. Not included: indentation (drives
204-
// the desktop floating toolbar, clipped at phone width).
192+
// One principle decides membership: a suite runs here when it can
193+
// go red for a mobile-conditional reason no other suite here
194+
// already pins. Tests whose driving idiom doesn't translate to
195+
// touch emulation (positional mouse drags) carry
196+
// `skipIf(onAndroid)` guards; product behavior is never
197+
// skipped. No blanket screenshot suites — android baselines
198+
// would double maintenance for viewport-independent artifacts;
199+
// mobile visuals get curated tests with their own baselines.
200+
// (form/ and copypaste/ were tried and dropped: no distinct
201+
// mobile-conditional failure mode — see #3031.)
205202
include: [
203+
// Mobile-specific product behavior: the toolbar/popover
204+
// lifecycle, IME delivery routes, touch link taps.
206205
"./src/end-to-end/mobile/**/*.test.tsx",
207-
// The popover form-submission suites are this instance's
208-
// reason to exist — the bugs they guard were Android bugs;
209-
// the platform-contract suites pin the browser facts they
210-
// rest on under the same mobile emulation.
211-
"./src/end-to-end/form/**/*.test.tsx",
206+
// The browser facts Form.Root rests on (implicit submission,
207+
// composition), re-asserted under mobile emulation flags.
212208
"./src/end-to-end/platform/**/*.test.tsx",
209+
// Synthesized Enter through the keymap chain — the #3001
210+
// fix's primary consumer, exercised across every handler.
213211
"./src/end-to-end/keyboardhandlers/**/*.test.tsx",
212+
// Synthesized Enter through the suggestion menu's own key
213+
// handling — a distinct consumer from the keymap chain.
214214
"./src/end-to-end/emojipicker/**/*.test.tsx",
215215
],
216216
},

tests/vitestSetup.browser.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { afterEach, beforeAll, beforeEach } from "vite-plus/test";
2-
import { page } from "vite-plus/test/browser";
2+
import { commands, page } from "vite-plus/test/browser";
3+
4+
import { ensureTouchEmulation } from "./src/utils/ensureTouchEmulation.js";
35

46
// Browser-mode setup. Unlike the jsdom `vitestSetup.ts`, we don't mock
57
// ClipboardEvent/DragEvent/matchMedia here — the real browser provides them.
@@ -14,7 +16,15 @@ import { page } from "vite-plus/test/browser";
1416
// resizes that iframe. Run before all tests in the file so every test sees the
1517
// right size from the first render.
1618
beforeAll(async () => {
17-
await page.viewport(1280, 720);
19+
// On the android instance the outer window is a 393x727 phone (provider
20+
// contextOptions) — the iframe must match it exactly. A larger iframe gets
21+
// scaled down by the harness's fit-to-window transform, so captures come
22+
// out phone-*sized* but contain a shrunken desktop-width layout.
23+
if (/android/i.test(navigator.userAgent)) {
24+
await page.viewport(393, 727);
25+
} else {
26+
await page.viewport(1280, 720);
27+
}
1828

1929
// Match the playground's editor framing so screenshots line up with what
2030
// users see at https://www.blocknotejs.org/examples (max-width 731px,
@@ -25,6 +35,24 @@ beforeAll(async () => {
2535
document.head.appendChild(style);
2636
});
2737

38+
// Chromium's beyond-viewport screenshot capture (any `toMatchScreenshot` of
39+
// an element taller than the viewport — Playwright sends
40+
// `captureBeyondViewport: true`) can silently drop the context's touch
41+
// emulation for every later test. Before every test on the android instance:
42+
// re-arm the emulation, then assert it actually holds — the assert is what
43+
// catches the deeper failure class where the *mechanism* breaks (provider
44+
// contextOptions silently ignored, a vitest upgrade rewiring the provider,
45+
// this very command regressing). No suite needs to call
46+
// `ensureTouchEmulation` itself.
47+
beforeEach(async () => {
48+
if (/android/i.test(navigator.userAgent)) {
49+
await (
50+
commands as unknown as { restoreTouchEmulation(): Promise<void> }
51+
).restoreTouchEmulation();
52+
ensureTouchEmulation();
53+
}
54+
});
55+
2856
beforeEach(() => {
2957
(window as Window & { __TEST_OPTIONS?: any }).__TEST_OPTIONS = {};
3058
});

0 commit comments

Comments
 (0)