testing-editor-harness

v2026.09.25

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

GitHub
安装命令
npx skhub add seed-hypermedia/testing-editor-harness
Markdown
SKILL.md

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/pnpm if mise/corepack is rate-limited.
  • The harness uses Vite: pnpm test:harness runs vite --config e2e/vite.config.ts on http://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=1 mounts the actual DocumentEditor with a mock document machine.
  • fixture=allBlocks renders one of every block type defined in e2e/test-app/TestEditor.tsx.
  • badges=1 gives 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" or aria-label="Comment on selection", inside a bg-popover ... rounded-md border ... shadow-md element

Useful runtime globals

  • window.TEST_EDITOR (?real=1) — exposes hoverActionsBlockId(), blockToolsBlockId(), getSelection(), getBlocks(), etc.
  • window.TEST_MACHINE (?real=1) — exposes state() and send() for the document machine actor.
  • window.TEST_BLOCK_TOOL_CALLS — records {copyLink, comment} calls made by the BlockHoverActions card 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=1 is required in the harness to render .bn-supernumber-badge widgets.
  • Emulation.setTouchEmulationEnabled is deprecated in newer CDP versions; if it stops working, use Emulation.setEmitTouchEventsForMouse or a newer Emulation domain method.
  • The allBlocks fixture intentionally uses a broken web-embed URL and a draft embed; expect Error loading embed and Draft card content — these are not failures.
  • Do not set a TextSelection to block-node boundary positions (e.g. 0, 1, or 19); this produces a blockChildren/blockNode endpoint warning and may fail to trigger the desired UI.
发现
标签

此技能尚未发布标签。

版本
最新版本元数据

版本

v2026.09.25

发布时间

2026年9月25日

分类

未分类

许可证

Apache-2.0

源路径

.agents/skills/testing-editor-harness

默认分支

main

最新提交

063400a

Tree SHA

b9fe7d2