Skip to content

Commit f3e0db3

Browse files
committed
test(device): link helpers next to the link tests, submitting via the form
Review feedback (two threads): the link-flow device helpers belong next to the tests that use them, not in the shared lib — moved here from editorPage/gestures. typeAndSubmit also changes how it submits, answering why it dispatched a synthetic Enter: the on-screen keyboard's action key is unreachable by any automation channel (see README), and the dispatched keydown only worked while the popovers had key handlers. With submission running off the form's submit event, an untrusted keydown does nothing — the helper was silently broken by the form rework. requestSubmit() is the browser's own submission path and exercises the popover's real onSubmit wiring; the IME's own action-key choice stays a manual release check.
1 parent 8ce4a4b commit f3e0db3

4 files changed

Lines changed: 103 additions & 6 deletions

File tree

tests/device/formattingToolbar.device.test.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,20 @@ import {
88
} from "vite-plus/test";
99

1010
import { activeDevices } from "./devices.js";
11-
import { tapElement, typeAndSubmit } from "./lib/gestures.js";
11+
import { tapElement } from "./lib/gestures.js";
1212
import {
1313
docState,
14-
LINK_POPOVER,
1514
MOBILE_TOOLBAR,
1615
openExample,
17-
openLinkPopover,
18-
selectFirstWord,
1916
startEditing,
2017
viewportHeight,
2118
} from "./lib/editorPage.js";
19+
import {
20+
LINK_POPOVER,
21+
openLinkPopover,
22+
selectFirstWord,
23+
typeAndSubmit,
24+
} from "./linkPopover.js";
2225
import { browserStackCredentials, DeviceSession } from "./lib/webdriver.js";
2326

2427
const KEYBOARD_MIN_HEIGHT = 150;

tests/device/lib/editorPage.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,3 @@ export async function startEditing(session: DeviceSession): Promise<void> {
9090
verifyTimeoutMs: 15_000,
9191
});
9292
}
93-

tests/device/lib/gestures.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,4 +170,3 @@ export async function typeText(
170170
await session.typeKeys(text);
171171
}
172172
}
173-

tests/device/linkPopover.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/**
2+
* Helpers for the create-link flow on real devices — next to the tests that
3+
* use them, since only the link tests speak these concepts.
4+
*/
5+
import { MOBILE_TOOLBAR, PARAGRAPH, startEditing } from "./lib/editorPage.js";
6+
import { tapElement } from "./lib/gestures.js";
7+
import type { DeviceSession } from "./lib/webdriver.js";
8+
9+
export const LINK_BUTTON = `${MOBILE_TOOLBAR} [data-test="createLink"]`;
10+
export const LINK_POPOVER = ".bn-form-popover";
11+
12+
/**
13+
* Selects the first word of the first paragraph via a DOM range (ProseMirror
14+
* syncs its selection from `selectionchange`, so no editor handle is needed).
15+
* iOS intermittently collapses programmatic selections, so the wait re-applies
16+
* the range on every poll until the toolbar's link button confirms the editor
17+
* sees a non-empty selection.
18+
*/
19+
export async function selectFirstWord(session: DeviceSession): Promise<void> {
20+
const applyAndCheck = `
21+
if (getSelection().isCollapsed) {
22+
const p = document.querySelector(${JSON.stringify(PARAGRAPH)});
23+
const textNode = [...p.childNodes].find((n) => n.nodeType === 3) || p.firstChild;
24+
const range = document.createRange();
25+
range.setStart(textNode, 0);
26+
range.setEnd(textNode, Math.min(7, textNode.textContent.length));
27+
const selection = getSelection();
28+
selection.removeAllRanges();
29+
selection.addRange(range);
30+
}
31+
return {
32+
ok: !getSelection().isCollapsed
33+
&& !!document.querySelector(${JSON.stringify(LINK_BUTTON)}),
34+
};`;
35+
await session.waitFor("selection + link button", applyAndCheck, 25_000);
36+
}
37+
38+
/**
39+
* Opens the create-link popover from the mobile toolbar and waits for its URL
40+
* input to hold focus. A mis-aimed tap (iOS chrome-offset guessing) can hit
41+
* the keyboard's accessory bar and collapse the whole editing state, so each
42+
* attempt rebuilds editing + selection from scratch before tapping.
43+
*/
44+
export async function openLinkPopover(session: DeviceSession): Promise<void> {
45+
let lastError: Error | undefined;
46+
for (let attempt = 0; attempt < 4; attempt++) {
47+
await startEditing(session);
48+
await selectFirstWord(session);
49+
await session.exec(`
50+
const toolbar = document.querySelector(${JSON.stringify(MOBILE_TOOLBAR)});
51+
toolbar.querySelectorAll('*').forEach((el) => {
52+
if (el.scrollWidth > el.clientWidth + 5) el.scrollLeft = el.scrollWidth;
53+
});`);
54+
try {
55+
await tapElement(session, LINK_BUTTON, {
56+
keyboard: "open",
57+
verify: `
58+
const active = document.activeElement;
59+
return {
60+
ok: !!document.querySelector(${JSON.stringify(LINK_POPOVER)})
61+
&& active && active.tagName === 'INPUT'
62+
&& active.getAttribute('name') === 'url',
63+
};`,
64+
});
65+
return;
66+
} catch (error) {
67+
lastError = error as Error;
68+
}
69+
}
70+
throw new Error(`Could not open the link popover: ${lastError?.message}`);
71+
}
72+
73+
/**
74+
* Types into a popover field and submits it the way the browser does:
75+
* `requestSubmit()` on the enclosing form, which runs the popover's real
76+
* `onSubmit` wiring. A dispatched Enter keydown cannot do this — synthetic
77+
* events trigger no default action, and the popovers commit through the
78+
* form's `submit` event rather than key handlers. Tapping the on-screen
79+
* keyboard's action key isn't an option either: no automation channel
80+
* reaches it (see the README) — which also means the IME's own choice of
81+
* action stays a manual release check.
82+
*/
83+
export async function typeAndSubmit(
84+
session: DeviceSession,
85+
css: string,
86+
text: string,
87+
): Promise<void> {
88+
await session.elementValue(css, text);
89+
await session.exec(
90+
`const el = document.querySelector(arguments[0]);
91+
if (el && el.form) {
92+
el.form.requestSubmit();
93+
}`,
94+
[css],
95+
);
96+
}

0 commit comments

Comments
 (0)