|
| 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