Testing the editor E2E harness
Devin Secrets Needed
None.
Environment
- Work from
frontend/packages/editor. - Use Node 22.22.x and the pnpm fallback at
/home/ubuntu/.local/bin/pnpmifmise/corepack is rate-limited. - The harness uses Vite:
pnpm test:harnessrunsvite --config e2e/vite.config.tsonhttp://localhost:5180.
Running the harness
export PATH="/home/ubuntu/.local/bin:/home/ubuntu/.local/share/mise/installs/node/22.22.0/bin:$PATH"
cd /home/ubuntu/repos/seed/frontend/packages/editor
pnpm test:harness
Open the browser at a real-mode fixture URL, e.g.:
http://localhost:5180/?real=1&fixture=allBlocks&badges=1
real=1mounts the actualDocumentEditorwith a mock document machine.fixture=allBlocksrenders one of every block type defined ine2e/test-app/TestEditor.tsx.badges=1gives every fixture block a mock citation/comment count so supernumber badges render.
Mobile viewport emulation
The Chrome for Testing instance in this environment listens on remote-debugging port 29229. Use CDP to set a mobile viewport without keeping DevTools open:
// /tmp/set-mobile-viewport.js
const http = require('http');
const WebSocket = require('/home/ubuntu/repos/seed/node_modules/ws');
http.get('http://localhost:29229/json/list', (res) => {
let data = '';
res.on('data', (c) => (data += c));
res.on('end', () => {
const pages = JSON.parse(data);
const page = pages.find((p) => p.type === 'page' && (p.url.includes('localhost:5180') || p.url === 'about:blank'));
if (!page) { console.error('No suitable page found'); process.exit(1); }
const ws = new WebSocket(page.webSocketDebuggerUrl);
ws.on('open', () => {
ws.send(JSON.stringify({id: 1, method: 'Page.navigate', params: {url: 'http://localhost:5180/?real=1&fixture=allBlocks&badges=1'}}));
setTimeout(() => {
ws.send(JSON.stringify({id: 2, method: 'Emulation.setDeviceMetricsOverride', params: {
width: 390, height: 844, deviceScaleFactor: 2, mobile: true,
screenWidth: 390, screenHeight: 844,
}}));
ws.send(JSON.stringify({id: 3, method: 'Emulation.setTouchEmulationEnabled', params: {enabled: true}}));
setTimeout(() => ws.close(), 500);
}, 300);
});
});
});
Verify with window.innerWidth and window.innerHeight in the console.
Key DOM selectors
- Editor container:
[data-testid="editor-container"] - Block nodes:
[data-node-type="blockNode"][data-id] - Paragraph content:
[data-node-type="blockNode"][data-id="p-top"] [data-content-type="paragraph"] - Supernumber badges:
.bn-supernumber-badge - Block hover actions card:
[data-bn-block-hover-actions="true"] - Range selection bubble: buttons with
aria-label="Copy link to selection"oraria-label="Comment on selection", inside abg-popover ... rounded-md border ... shadow-mdelement
Useful runtime globals
window.TEST_EDITOR(?real=1) — exposeshoverActionsBlockId(),blockToolsBlockId(),getSelection(),getBlocks(), etc.window.TEST_MACHINE(?real=1) — exposesstate()andsend()for the document machine actor.window.TEST_BLOCK_TOOL_CALLS— records{copyLink, comment}calls made by theBlockHoverActionscard in?real=1.
Triggering touch interactions
The BlockHoverActions plugin only responds to pointerdown/pointerup events whose pointerType is not mouse. A real mouse click will not open the mobile card. In a real touch scenario, a tap also produces a click, which useReadOnlyClickToEdit handles and may start editing. For recording/diagnostic purposes, dispatch synthetic pointer events directly:
const content = document.querySelector('[data-node-type="blockNode"][data-id="p-top"] [data-content-type="paragraph"]');
const rect = content.getBoundingClientRect();
const x = rect.left + rect.width / 2;
const y = rect.top + rect.height / 2;
content.dispatchEvent(new PointerEvent('pointerdown', {pointerType:'touch', bubbles:true, isPrimary:true, clientX:x, clientY:y}));
await new Promise(r => setTimeout(r, 30));
content.dispatchEvent(new PointerEvent('pointerup', {pointerType:'touch', bubbles:true, isPrimary:true, clientX:x, clientY:y}));
Triggering RangeSelection
On a real touch device, long-pressing text and adjusting the selection handles creates the selection, and the RangeSelection plugin opens the bubble after touchend/10 ms settle.
In the harness under CDP device emulation, a real mouse drag across the read-only contenteditable=false editor may leave only a collapsed caret. To demonstrate the bubble reliably, set both the native selection and the ProseMirror selection, then dispatch mouseup on the editor DOM:
const view = window.TEST_EDITOR.editor._tiptapEditor.view;
const TextSelection = view.state.selection.constructor;
view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.doc, 3, 18)));
const content = document.querySelector('[data-node-type="blockNode"][data-id="p-top"] [data-content-type="paragraph"]');
const textNode = Array.from(content.childNodes).find(n => n.nodeType === Node.TEXT_NODE);
if (textNode) {
const sel = window.getSelection();
sel.removeAllRanges();
const range = document.createRange();
range.setStart(textNode, 0);
range.setEnd(textNode, textNode.length);
sel.addRange(range);
}
await new Promise(r => setTimeout(r, 20));
view.dom.dispatchEvent(new MouseEvent('mouseup', {bubbles: true, cancelable: true, clientX: 100, clientY: 100}));
Common pitfalls
?badges=1is required in the harness to render.bn-supernumber-badgewidgets.Emulation.setTouchEmulationEnabledis deprecated in newer CDP versions; if it stops working, useEmulation.setEmitTouchEventsForMouseor a newerEmulationdomain method.- The
allBlocksfixture intentionally uses a broken web-embed URL and a draft embed; expectError loading embedandDraft cardcontent — these are not failures. - Do not set a
TextSelectionto block-node boundary positions (e.g. 0, 1, or 19); this produces ablockChildren/blockNodeendpoint warning and may fail to trigger the desired UI.