Skip to content

Commit 15081f6

Browse files
committed
docs: split md viewer test guide into CLAUDE-markdown-viewer.md
Move detailed markdown viewer architecture, postMessage protocol, test helpers, and debugging guide from CLAUDE.md to a dedicated src-mdviewer/CLAUDE-markdown-viewer.md. Keep CLAUDE.md concise with a reference link.
1 parent b342173 commit 15081f6

2 files changed

Lines changed: 99 additions & 0 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ Use `exec_js` to run JS in the Phoenix browser runtime. jQuery `$()` is global.
3838
- **Never use `awaits(number)`** (fixed-time waits) in tests — they cause flaky failures. Always use `awaitsFor(condition)` to wait for a specific condition to become true.
3939
- Use `editor.*` APIs (e.g. `editor.document.getText()`, `editor.getCursorPos()`, `editor.setSelection()`) instead of accessing `editor._codeMirror` directly.
4040
- Tests should be independent — no shared mutable state between `it()` blocks. Use `FILE_CLOSE` with `{ _forceClose: true }` to clean up.
41+
- For markdown viewer/live preview architecture, test patterns, and debugging — see `src-mdviewer/CLAUDE-markdown-viewer.md`.
4142

4243
## Running Tests via MCP
4344

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Markdown Viewer/Editor — Development & Testing Guide
2+
3+
## Architecture
4+
5+
The markdown viewer (`src-mdviewer/`) is a standalone web app loaded inside an iframe in Phoenix's Live Preview panel. It communicates with Phoenix via postMessage.
6+
7+
### Iframe nesting (in tests)
8+
```
9+
Test Runner Window
10+
└── Test Phoenix iframe (testWindow)
11+
├── CM5 editor (CodeMirror)
12+
├── Live Preview panel
13+
│ └── #panel-md-preview-frame (md viewer iframe)
14+
│ ├── #viewer-content (contenteditable in edit mode)
15+
│ ├── #format-bar, #link-popover
16+
│ └── embedded-toolbar (reader/edit toggle, cursor sync btn)
17+
└── MarkdownSync.js (listens for postMessage from md iframe)
18+
```
19+
20+
### Key source files
21+
- `src-mdviewer/src/bridge.js` — postMessage bridge between Phoenix and md iframe. Handles file switching, content sync, keyboard shortcuts, edit mode.
22+
- `src-mdviewer/src/core/doc-cache.js` — Document DOM cache with LRU eviction for file switching.
23+
- `src-mdviewer/src/components/editor.js` — Contenteditable WYSIWYG editing, Turndown HTML→Markdown conversion.
24+
- `src-mdviewer/src/components/embedded-toolbar.js` — Reader/edit toggle, cursor sync, format buttons.
25+
- `src-mdviewer/src/components/format-bar.js` — Floating format bar on text selection (bold, italic, underline, link).
26+
- `src-mdviewer/src/components/link-popover.js` — Link popover for editing/removing links in edit mode.
27+
- `src-mdviewer/src/components/viewer.js` — Reader mode click handling, link interception, copy buttons.
28+
- `src/extensionsIntegrated/Phoenix-live-preview/MarkdownSync.js` — Phoenix-side sync: CM↔iframe content, cursor, scroll, selection.
29+
30+
## Communication: postMessage (reliable in both directions)
31+
32+
- **Phoenix → iframe**: `iframe.contentWindow.postMessage({ type: "MDVIEWR_SET_EDIT_MODE", ... })` — mode switches, file content updates
33+
- **iframe → Phoenix**: bridge.js calls `window.parent.postMessage({ type: "MDVIEWR_EVENT", eventName: "...", ... })` — keyboard shortcuts, content changes, cursor sync, link clicks
34+
- MarkdownSync.js listens for these messages and acts on them (scroll CM, open URLs, handle undo/redo)
35+
36+
### Message types (Phoenix → iframe)
37+
| Type | Purpose |
38+
|------|---------|
39+
| `MDVIEWR_SET_EDIT_MODE` | Toggle edit/reader mode |
40+
| `MDVIEWR_SWITCH_FILE` | Switch to a new file with markdown content |
41+
| `MDVIEWR_CONTENT_UPDATE` | Update content from CM edits |
42+
| `MDVIEWR_SCROLL_TO_LINE` | Scroll viewer to a source line (cursor sync) |
43+
| `MDVIEWR_HIGHLIGHT_SELECTION` | Highlight blocks corresponding to CM selection |
44+
45+
### Event names (iframe → Phoenix via `MDVIEWR_EVENT`)
46+
| eventName | Purpose |
47+
|-----------|---------|
48+
| `mdviewrContentChanged` | Editor content changed (sync to CM) |
49+
| `mdviewrEditModeChanged` | Edit/reader mode toggled |
50+
| `mdviewrKeyboardShortcut` | Forwarded shortcut (Ctrl+S, Ctrl+Shift+F, etc.) |
51+
| `mdviewrUndo` / `mdviewrRedo` | Undo/redo requests |
52+
| `mdviewrScrollSync` | Scroll sync from edit mode click |
53+
| `mdviewrSelectionSync` | Selection sync from viewer to CM |
54+
| `mdviewrCursorSyncToggle` | Cursor sync button toggled |
55+
| `embeddedIframeFocusEditor` | Reader mode click — refocus CM, scroll to source line |
56+
| `embeddedIframeHrefClick` | Link click — opens URL via `NativeApp.openURLInDefaultBrowser` |
57+
| `embeddedEscapeKeyPressed` | Escape key — refocus Phoenix editor |
58+
59+
## Integration Tests
60+
61+
Test file: `test/spec/md-editor-integ-test.js`
62+
Category: `livepreview`, Suite: `livepreview:Markdown Editor`
63+
64+
### Accessing the md iframe from tests
65+
The md iframe is **directly DOM-accessible** (no sandbox in test mode):
66+
```js
67+
testWindow.document.getElementById("panel-md-preview-frame") // iframe element
68+
iframe.contentDocument // query #viewer-content, #format-bar, etc.
69+
iframe.contentWindow // access __setEditModeForTest, __getCurrentContent, etc.
70+
```
71+
72+
### Test helpers exposed on iframe window (`__` prefix)
73+
- `win.__setEditModeForTest(bool)` — toggle edit/reader mode
74+
- `win.__getCurrentContent()` — get the markdown source currently loaded in viewer
75+
- `win.__getActiveFilePath()` — current file path in viewer
76+
- `win.__isSuppressingContentChange()` — true during re-render (wait for false before asserting)
77+
- `win.__triggerContentSync()` — force content sync after `execCommand` formatting
78+
- `win.__getCacheKeys()` / `win.__getWorkingSetPaths()` — inspect doc cache state
79+
80+
### Key test patterns
81+
- **Wait for sync**: `_waitForMdPreviewReady(editor)` — mandatory after every file switch. Verifies iframe visible, bridge initialized, content rendered, and `editor.document.getText()` matches `win.__getCurrentContent()`.
82+
- **Formatting**: Use `_execCommandInMdIframe("bold")` — browsers reject `execCommand` from untrusted `KeyboardEvent`s, so synthetic key events don't work for formatting.
83+
- **Keyboard shortcuts**: Use `_dispatchKeyInMdIframe(key)` — bridge.js captures these and forwards via postMessage to MarkdownSync.
84+
- **Clicking elements**: Click directly on iframe DOM elements (e.g. `paragraph.click()`). The bridge.js click handler fires and sends the appropriate postMessage. Always test the real click flow.
85+
- **Editor APIs**: Use `editor.document.getText()`, `editor.setCursorPos()`, `editor.setSelection()`, `editor.getSelectedText()`, `editor.replaceRange()`, `editor.lineCount()`, `editor.getLine()` — never access `editor._codeMirror` directly.
86+
87+
### Rules
88+
- **Never use `awaits(number)`** — always use `awaitsFor(condition)`.
89+
- **Tests must be independent** — no shared mutable state between `it()` blocks. Use `FILE_CLOSE` with `{ _forceClose: true }` to clean up.
90+
- **Test real behavior** — use actual DOM clicks and CM API calls, not fabricated postMessages.
91+
- **Negative assertions** — move state to a known position first, perform the action, then verify state didn't change.
92+
- **Function interception** — save originals in `beforeAll`, restore in `afterAll` to guard against test failures.
93+
94+
### Debugging test failures
95+
- **Stale DOM refs**: After toolbar re-render or file switch, re-query with `_getMdIFrameDoc().getElementById(...)`.
96+
- **Dirty state**: Check if a prior test left cursor sync disabled, edit mode on, etc. Tests should clean up.
97+
- **Fixture files**: Live in `test/spec/LiveDevelopment-Markdown-test-files/`. After modifying, run `npm run build` and reload the test runner.
98+
- **Test checklist**: `src-mdviewer/to-create-tests.md` tracks what's covered and what's pending.

0 commit comments

Comments
 (0)