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