Testing the editor E2E harness
SkillDev toolsHow to run and manually test the editor E2E harness for the @shm/editor package, including mobile viewport emulation, supernumber badges, and mobile comment UX interactions.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Testing the editor E2E harness skill
What this skill tells your AI
The instructions your AI receives, as published by seed-hypermedia/seed in .agents/skills/testing-editor-harness/SKILL.md and read by ahel’s review.
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.
Signals
- GitHub stars
- 56
- Forks
- 12
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
testing-editor-harness- Source
- github.com/seed-hypermedia/seed