name: graph-macro description: Drive the ZenUML Graph (DrawIO) macro through any entry point — slash-menu insert, in-editor Edit, copy-then-edit fallback, or view-mode Edit — and into the inner DrawIO editor to append a timestamp shape and Publish. Classifies the save outcome as success or did-not-persist, and records whether the paywall was observed. Use whenever you need to verify a Graph macro insert / edit / save end-to-end via Playwright MCP without re-deriving the Forge → DrawIO iframe chain. Triggers on "insert graph macro", "edit graph macro", "publish graph", "spot check drawio publish", "test graph save", "verify graph publish on lite", or any ad-hoc Graph-macro MCP spot check on lite-stg / full-stg / dia-stg / zenuml-stg / prod.
Graph (DrawIO) macro — MCP-driven spot-check playbook
A single, MCP-readable playbook that takes you from "Confluence page open in Chrome" to "Graph macro inserted or edited, Publish attempted, outcome classified." Avoids re-deriving the Forge → DrawIO iframe chain, the slash-menu keystroke trap, the paywall placement, and the silent persistence-layer block — each is captured inline below with a code citation so you can trust the why, not just the what.
When to use this skill vs an adjacent one
| If you need to … | Use |
|---|---|
| Spot-check the Graph macro insert / edit / publish flow via MCP | this skill |
| Edit a non-Graph macro (no DrawIO inner iframe) | /edit-macro |
| Verify the cross-page-copy writeback behaviour specifically | /copy-macro |
| Run release-gating validation (not ad-hoc) | /pvt-drawio or /pvt-edit |
| Set up a fresh page from scratch (login + page creation) | /smoke-test, then come back to this skill |
This skill is variant-agnostic. It reports observations; the caller (you) interprets the result against the variant under test using the table in the final section.
Preconditions
- Chrome already logged in to a Confluence site.
- A Confluence page URL is open in the active Chrome tab (view or edit mode is fine — the skill clicks Confluence's page-level Edit button itself if Insert is needed and the page is in view mode).
- Playwright MCP tools (
mcp__playwright__*) available.
You do not need to specify a variant. Drive whichever site is open; interpret the report at the end.
Selector inventory (referenced throughout)
| Element | Selector |
|---|---|
| Forge editor modal | [data-testid="custom-ui-modal-dialog"] |
| Forge app iframe inside the modal | [data-testid="hosted-resources-iframe"] |
| Inner DrawIO iframe | nested iframe reached via the Playwright locator chain — never page.frames().find() (URL-match is fragile across drawio/editor vs drawio/editor.html) |
| Macro extension wrap (in Edit Mode) | .extensionView-content-wrap |
| Confluence native macro pencil / edit toolbar (Edit Mode fallback) | TBD — pin on first run by inspecting the floating selection toolbar that appears after clicking a macro in the page editor, then update this table |
| Paywall "Continue editing" button | [data-testid="continue-editing-btn"] (defined in src/components/UpgradePrompt/UpgradePrompt.vue:51) |
The TBD is acceptable slack — pin it the first time this skill drives a page with our Edit button blocked, and record the verified selector here in-place.
Entry-point decision tree
Pick the entry section by looking at the page's current state. Do not ask the caller; observe.
What's on the page?
├── No Graph macro yet
│ └── Section A: Insert via slash menu
│ (requires page in Edit Mode; if currently in View Mode,
│ click Confluence's page-level Edit button first and wait
│ for the editor toolbar)
│
└── Graph macro already on page
├── Page in View Mode → Section E3: click our viewer's Edit button
│
└── Page in Edit Mode → Section E1 with E2 auto-fallback:
├── Try E1: click our viewer's Edit button (short timeout ≤ 3 s)
└── If E1 not clickable → E2: click Confluence's native macro
pencil / edit toolbar button
(E2 is the documented fallback for just-copied macros —
see /copy-macro)
All entry sections converge on the same postcondition: the Forge editor modal [data-testid="custom-ui-modal-dialog"] is open. Section C runs from there.
Record entry_path_used in the final report (A, E1, E2, or E3) so spot checks investigating copy-related bugs can verify they hit the expected path.
Section A — Insert via slash menu
Preconditions: page in Edit Mode. If currently in View Mode, click Confluence's page-level Edit button and wait until the editor toolbar mounts before continuing.
Steps:
Click in the page body to position the cursor.
Type
/thengraphusingmcp__playwright__browser_press_key(orbrowser_typeone character at a time).Keystroke trap: ProseMirror (Confluence's editor) watches
inputevents.mcp__playwright__browser_fill_formbypasses those events and the slash menu never opens. Use real keystrokes.Wait for the slash menu to render the Graph option, then click "Graph (DrawIO)" specifically. Do not pick any other Graph-named option.
Wait for
[data-testid="custom-ui-modal-dialog"]to appear.
Convergence: Forge modal is open. Proceed to Section C.
Section E1 + E2 — Edit in page Edit Mode (auto-fallback)
Documented as one section with two steps so the fallback is intrinsic, not an indirection.
Preconditions: page in Edit Mode, Graph macro already present on the page.
Step E1 (preferred)
- Drill to our viewer iframe:
.extensionView-content-wrap→[data-testid="hosted-resources-iframe"]→ our viewer. - Inside that iframe, locate our viewer toolbar's Edit button.
- Click with a short timeout (≤ 3 s) — this is a clickability probe, not a long wait.
- If
[data-testid="custom-ui-modal-dialog"]appears → recordentry_path_used = E1, proceed to Section C. - If the click times out or has no effect → fall through to E2. Do not retry E1.
Step E2 (fallback — just-copied-macro path)
- Click the
.extensionView-content-wraponce to select the macro in the page editor. - Confluence renders a floating selection toolbar with a pencil / edit icon — its own native affordance, distinct from our app.
- Click that pencil / edit icon (selector TBD on first run — update the selector inventory once pinned).
- Wait for
[data-testid="custom-ui-modal-dialog"]. Recordentry_path_used = E2.
Why the fallback exists: Our Edit button is not clickable immediately after Confluence's copy operation — see
/copy-macrofor the underlying writeback race. Always fall through to E2 if E1 doesn't respond within ~3 s; do not retry E1 with longer timeouts.
Convergence: Forge modal is open. Proceed to Section C.
Section E3 — Edit in View Mode
Preconditions: page in View Mode (do not click Confluence's page Edit button), Graph macro already rendered.
Steps:
- Locate our viewer iframe directly. In View Mode there is no
.extensionView-content-wrapenvelope — the chain is shorter than in Edit Mode. - Find the Edit button on the viewer toolbar.
- Click it; wait for
[data-testid="custom-ui-modal-dialog"]. Recordentry_path_used = E3.
E2 is not available in View Mode — Confluence's native macro toolbar only appears inside the page editor. Our Edit button is generally more reliably clickable here than in E1 because there is no editor-mount race.
Convergence: Forge modal is open. Proceed to Section C.
Section C — Shared in-editor tail
Runs after any of A / E1 / E2 / E3 leaves [data-testid="custom-ui-modal-dialog"] open. Six steps: C0 paywall check, C1 drill, C2 timestamp, C3 publish, C4 outcome watcher, C5 report.
C0 — Paywall check (immediately after editor opens)
The paywall fires at editor entry, not at Publish. Handle it before drilling.
- Inside the Forge iframe, check for
[data-testid="continue-editing-btn"]with a short timeout (~2 s — the overlay mounts synchronously with the editor). - If present (
paywall_observed = yes):- Capture the most recent
paywall_triggeredevent fromapi.mixpanel.comto recordaction_type(page_editorfor edit-blocked,page_editor_createfor create-blocked). The skill only drives the editor surface, sofullscreen_viewershould never appear here — if it does, flag it as an anomaly in the report. - Click
[data-testid="continue-editing-btn"](this emitsPAYWALL_CONTINUED_EDITINGpersrc/components/UpgradePrompt/UpgradePrompt.vue:110). - Wait for
[data-testid="continue-editing-btn"]to disappear.
- Capture the most recent
- If absent: record
paywall_observed = noand proceed.
Why paywall belongs here, not at Publish:
src/utils/paywall/mountPaywallGate.ts:121-165(tryPageEditorPaywall) runs synchronously during the editor's mount path.src/components/UpgradePrompt/PaywallGate.vue:7-17documents that the editor mounts underneath the overlay: "Save is gated byshouldBlockActionsin the persistence layer, so the modal serves as the visible reminder — an editor mounted here cannot actually persist changes." After dismissing "Continue editing", the DrawIO canvas is interactive — but a subsequent Publish click is silently dropped by the persistence layer. The modal does not close; no second paywall appears.
Dismiss only via the button, not the backdrop. Backdrop click fires
MODAL_DISMISSEDinstead ofPAYWALL_CONTINUED_EDITING, which would skew the continued-editing rate the team tracks.
C1 — Drill outer → inner DrawIO iframe
- From
[data-testid="custom-ui-modal-dialog"], locate[data-testid="hosted-resources-iframe"](Forge app iframe). - Inside that iframe, locate the nested inner DrawIO iframe via Playwright locator chain.
Both
drawio/editoranddrawio/editor.htmlappear in different builds.page.frames().find()URL-matching can pick the wrong frame. Use the locator chain instead.
C2 — Append a timestamp text shape (without overwriting)
The goal of this step is to make the published result observable (you can see when the spot check ran) and non-destructive (existing canvas content untouched).
Timestamp content: edit-test YYYY-MM-DD HH:mm UTC — for example edit-test 2026-05-24 10:23 UTC. The edit-test prefix is a recognisable marker for grep / future debugging; UTC keeps it sortable and unambiguous.
Primary mechanism (UI-driven):
- Double-click an empty area near the canvas top-left (offset
(20, 20)is safe for most diagrams). - DrawIO's shape picker opens → type
text→ press Enter to place a text shape. - Type the timestamp content.
- Press Escape to commit.
Fallback mechanism (API-driven, use if primary is flaky against the deployed DrawIO build):
Use mcp__playwright__browser_evaluate inside the inner DrawIO iframe to call mxGraph's editorUi.editor.graph.insertVertex(...) directly. More invasive but bypasses UI-version drift in DrawIO's shape picker.
Don't click on or near existing shapes — that may select / modify them. The top-left margin works as a default; if the diagram has content there, pick another empty spot but never overlap existing geometry.
C3 — Click Publish in the inner DrawIO frame
The trap that costs everyone time: Publish lives in the inner DrawIO frame, not the outer Forge modal. The Forge modal chrome does not own that button. This is the same for Connect and Forge; do not search the outer modal for Publish. Verified in
tests/e2e-tests/helpers/MacroFlowHelper.ts(clickEditorPublish(page, { nested: 'drawio' })).
Click Publish inside the inner DrawIO iframe drilled in C1.
C4 — Watch for save completion (single watcher)
Wait up to 15 s for two things together:
[data-testid="custom-ui-modal-dialog"]disappears, AND- Our viewer iframe re-renders with the new timestamp shape visible.
| Result | Classification |
|---|---|
| Both happen within 15 s | outcome = success |
| Timeout | outcome = did-not-persist |
There is no second paywall watcher at this step — the paywall has already been observed (or not) in C0. If outcome = did-not-persist and paywall_observed = yes, the save was silently dropped by shouldBlockActions — that is the expected pattern on a paywalled Lite space, not a bug.
If outcome = did-not-persist and paywall_observed = no, capture mcp__playwright__browser_console_messages for the report — this is a real failure.
C5 — Structured report
Emit this exact shape so the caller can interpret without follow-up questions:
Outcome: <success | did-not-persist>
Entry path used: <A | E1 | E2 | E3>
Timestamp injected: edit-test YYYY-MM-DD HH:mm UTC
Time to outcome: <Ns>
Paywall observed: <yes | no>
action_type: <page_editor | page_editor_create> (only if yes; fullscreen_viewer here = anomaly)
Console errors (only if did-not-persist AND paywall_observed=no):
[<error 1>, <error 2>, …]
Caller-side interpretation hints
| Variant under test | Expected pattern |
|---|---|
| Lite (paywalled space) | paywall_observed = yes + outcome = did-not-persist |
| Lite (non-paywalled space) | paywall_observed = no + outcome = success |
| Full / Diagramly | paywall_observed = no + outcome = success |
| Suspect — investigate | paywall_observed = yes + outcome = success (save went through despite paywall — broken gate?), or paywall_observed = no + outcome = did-not-persist (real save failure with no paywall — read the console errors) |
If action_type = fullscreen_viewer shows up in the report, that's also an anomaly — this skill only ever drives the editor surface, so the fullscreen-viewer paywall path should never be hit here.