Skip to content

Commit 010277a

Browse files
alicodingclaude
andauthored
feat: real-engine drawio editor-layout smoke check (goal 0259), traypanel in the window model, overridable bridge port (#536)
drawio-editor-layout drives the REAL WKWebView: lands a two-page fixture via the drop RPC, asserts band-click selection produces the full resize-handle set, opens the embedded editor, and measures that the dialog, its iframe, and drawio's own page-tab bar all sit inside the real window (goal 0259 -- both engines now measure correct at harness sizes; the check is the permanent regression net). PASSED against a 618px-tall real window. Riders the run itself surfaced: the app-info window model learns the traypanel window (stale since the menu-bar surface landed -- it failed every smoke run); the harness port yields to MILL_SMOKE_MCP_PORT so the smoke can run beside a live installed Mill -- which itself holds 9099 because the production build ships the bridge, recorded separately as a P1 (BACKLOG 0P0-BRIDGE), as is the real-engine sticky shift-click failure the same run caught (0P0-STICKY). Claude-Session: https://claude.ai/code/session_012im1JxQQV2ahnXzZDdVmZq Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 541906a commit 010277a

4 files changed

Lines changed: 183 additions & 10 deletions

File tree

internal/webviewbridgesmoke/checks.go

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ var registry = []check{
9797
},
9898
{
9999
name: "app-info-window-sane",
100-
reason: "the real Wails process reports the app's true three-window shape on darwin (main + quickpanel + approvalprompt, ADR-0033) -- proves a genuine desktop process booted with its real window set, not just that a binary exists.",
100+
reason: "the real Wails process reports the app's true window set on darwin (main + quickpanel + approvalprompt + traypanel, ADR-0033 + goal 0189) -- proves a genuine desktop process booted with its real window set, not just that a binary exists.",
101101
run: checkAppInfo,
102102
},
103103
{
@@ -120,6 +120,11 @@ var registry = []check{
120120
reason: "owner-reported on the installed beta.724 (WebKit): click-to-edit a sticky got jumpy and the editor never opened; Chromium layers all pass -- the exact engine-parity class this registry exists for.",
121121
run: checkStickyClickToEdit,
122122
},
123+
{
124+
name: "drawio-editor-layout",
125+
reason: "owner-reported on the installed build (goal 0259): the embedded drawio editor's bottom page-tab bar sat outside the visible window with no scroll to it, and the diagram's resize frame seemed absent -- both measure CORRECT in Chromium at harness sizes, the exact engine/window-divergence class this registry exists for. Asserts, in the real WKWebView: band-click selection produces the full resize-handle set, and the editor dialog, its iframe, and the two-page tab bar all sit inside the real window.",
126+
run: checkDrawioEditorLayout,
127+
},
123128
{
124129
name: "sticky-border-color-flip",
125130
reason: "the second burned class: a selected sticky note's border-color flip to the accent token (AtlasStickyNode.module.css) -- a real Note, created live via AtlasService.CreateNote so the check exercises the same call_bound_method path an agent driving Mill would use.",
@@ -151,10 +156,12 @@ func checkAppInfo(c mcpCaller) (string, error) {
151156
if info.OS != "darwin" {
152157
return "", fmt.Errorf("expected os darwin, got %q", info.OS)
153158
}
154-
// The app's real window set (ADR-0033): the named main window plus
155-
// the two auxiliary windows. Asserted by name, not count, so a
156-
// missing or unexpected window is named in the failure.
157-
want := map[string]bool{mainWindowName: false, "quickpanel": false, "approvalprompt": false}
159+
// The app's real window set (ADR-0033, plus the menu-bar panel's
160+
// own attachable window -- goal 0189's SystemTray.AttachWindow):
161+
// the named main window plus the three auxiliary windows. Asserted
162+
// by name, not count, so a missing or unexpected window is named
163+
// in the failure.
164+
want := map[string]bool{mainWindowName: false, "quickpanel": false, "approvalprompt": false, "traypanel": false}
158165
mainVisible := false
159166
for _, w := range info.Windows {
160167
seen, expected := want[w.Name]
@@ -177,7 +184,7 @@ func checkAppInfo(c mcpCaller) (string, error) {
177184
if !mainVisible {
178185
return "", fmt.Errorf("main window reported not visible")
179186
}
180-
return fmt.Sprintf("os=%s windows=main+quickpanel+approvalprompt, main visible", info.OS), nil
187+
return fmt.Sprintf("os=%s windows=main+quickpanel+approvalprompt+traypanel, main visible", info.OS), nil
181188
}
182189

183190
// pollJSEval retries a boolean-returning js_eval snippet until it's
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"time"
8+
)
9+
10+
// A minimal two-page drawio file: page switching only exists with 2+
11+
// pages, so a single-page fixture could never exercise the editor's
12+
// tab bar at all.
13+
const drawioTwoPageFixture = `<mxfile host="app.diagrams.net" type="device">
14+
<diagram id="smoke-page-one" name="Overview">
15+
<mxGraphModel dx="800" dy="600" grid="1" gridSize="10" page="1" pageScale="1" pageWidth="850" pageHeight="1100">
16+
<root>
17+
<mxCell id="0" />
18+
<mxCell id="1" parent="0" />
19+
<mxCell id="a1" value="Alpha" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
20+
<mxGeometry x="120" y="120" width="120" height="60" as="geometry" />
21+
</mxCell>
22+
</root>
23+
</mxGraphModel>
24+
</diagram>
25+
<diagram id="smoke-page-two" name="Detail">
26+
<mxGraphModel dx="800" dy="600" grid="1" gridSize="10" page="1" pageScale="1" pageWidth="850" pageHeight="1100">
27+
<root>
28+
<mxCell id="0" />
29+
<mxCell id="1" parent="0" />
30+
<mxCell id="b1" value="Gamma" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
31+
<mxGeometry x="200" y="160" width="140" height="70" as="geometry" />
32+
</mxCell>
33+
</root>
34+
</mxGraphModel>
35+
</diagram>
36+
</mxfile>`
37+
38+
// landTwoPageDiagram writes the fixture to a throwaway path and lands
39+
// it as a diagram board object through the same RPC a native drop
40+
// uses, waiting until the board face renders and settles.
41+
func landTwoPageDiagram(c mcpCaller) error {
42+
dir, err := os.MkdirTemp("", "mill-smoke-drawio")
43+
if err != nil {
44+
return err
45+
}
46+
fixturePath := filepath.Join(dir, "two-page-smoke.drawio")
47+
if err := os.WriteFile(fixturePath, []byte(drawioTwoPageFixture), 0o600); err != nil {
48+
return err
49+
}
50+
parentID, err := gettingStartedParentID(c)
51+
if err != nil {
52+
return err
53+
}
54+
var obj struct {
55+
ID string `json:"ID"`
56+
}
57+
if err := callBoundJSON(c, "github.com/alicoding/mill/internal/services/atlassvc.AtlasService.CreateBoardObject",
58+
[]any{"diagram", map[string]any{"mirrorPath": fixturePath, "title": "smoke two-page"}, map[string]any{"X": 620, "Y": 340}, parentID}, &obj); err != nil {
59+
return err
60+
}
61+
bodySel := `[data-testid="atlas-drawio-page-body"]`
62+
if err := pollJSEval(c, fmt.Sprintf(`return !!document.querySelector('%s');`, bodySel), 15*time.Second); err != nil {
63+
return fmt.Errorf("diagram board face never rendered after CreateBoardObject: %w", err)
64+
}
65+
if err := waitForNodeStable(c, bodySel); err != nil {
66+
return fmt.Errorf("board never settled before the band click: %w", err)
67+
}
68+
return nil
69+
}
70+
71+
// bandSel is the diagram's chrome band -- its ONLY selection surface
72+
// (the vendored viewer captures body clicks for pan/zoom).
73+
const drawioBandSel = `.react-flow__node [data-testid="atlas-board-object-frame"]`
74+
75+
// openDrawioEditor double-clicks the band to open the embedded editor.
76+
// Two bridge clicks land faster than the OS double-click threshold
77+
// reliably here; a synthesized dblclick is the one escape hatch,
78+
// allowed because the bridge has no native dblclick primitive and the
79+
// double-click GATE itself is proven by e2e.
80+
func openDrawioEditor(c mcpCaller, dialogSel string) error {
81+
for i := 0; i < 2; i++ {
82+
if _, err := c.call("mouse_click", withWindow(map[string]any{"selector": drawioBandSel})); err != nil {
83+
return err
84+
}
85+
}
86+
if pollJSEval(c, fmt.Sprintf(`return !!document.querySelector('%s iframe');`, dialogSel), 20*time.Second) == nil {
87+
return nil
88+
}
89+
if err := pollJSEval(c, fmt.Sprintf(`const band = document.querySelector('%s');
90+
if (!band) return false;
91+
band.dispatchEvent(new MouseEvent('dblclick', {bubbles: true, cancelable: true}));
92+
return true;`, drawioBandSel), 3*time.Second); err != nil {
93+
return fmt.Errorf("no chrome band to double-click: %w", err)
94+
}
95+
if err := pollJSEval(c, fmt.Sprintf(`return !!document.querySelector('%s iframe');`, dialogSel), 20*time.Second); err != nil {
96+
return fmt.Errorf("embedded editor dialog never opened: %w", err)
97+
}
98+
return nil
99+
}
100+
101+
func checkDrawioEditorLayout(c mcpCaller) (string, error) {
102+
if err := landTwoPageDiagram(c); err != nil {
103+
return "", err
104+
}
105+
106+
// Selecting the band must produce the shared NodeResizer's full
107+
// handle set in the real engine, exactly as Chromium renders it.
108+
if _, err := c.call("mouse_click", withWindow(map[string]any{"selector": drawioBandSel})); err != nil {
109+
return "", err
110+
}
111+
if err := pollJSEval(c, `return document.querySelectorAll('.react-flow__resize-control').length >= 8;`, 5*time.Second); err != nil {
112+
return "", fmt.Errorf("resize handles never appeared after selecting the diagram's chrome band: %w", err)
113+
}
114+
115+
dialogSel := `[data-component="atlas-drawio-editor-dialog"]`
116+
if err := openDrawioEditor(c, dialogSel); err != nil {
117+
return "", err
118+
}
119+
120+
// The clipping class this check pins (goal 0259): the editor's
121+
// bottom chrome -- where drawio's own page tabs live -- must sit
122+
// INSIDE the real window, and the tab bar itself must be present
123+
// and reachable for a multi-page file. Same-origin iframe, so its
124+
// document is directly measurable.
125+
if err := pollJSEval(c, fmt.Sprintf(`const f = document.querySelector('%s iframe');
126+
const d = f && f.contentDocument;
127+
return !!(d && d.querySelector('.geTabContainer'));`, dialogSel), 30*time.Second); err != nil {
128+
return "", fmt.Errorf("the editor's page-tab bar (.geTabContainer) never appeared for a two-page file: %w", err)
129+
}
130+
verdict, err := c.call("js_eval", withWindow(map[string]any{
131+
"js": fmt.Sprintf(`const dlg = document.querySelector('%s');
132+
const f = dlg.querySelector('iframe');
133+
const dr = dlg.getBoundingClientRect();
134+
const fr = f.getBoundingClientRect();
135+
const tabs = f.contentDocument.querySelector('.geTabContainer');
136+
const tr = tabs.getBoundingClientRect();
137+
const tabsBottomInWindow = fr.top + tr.bottom;
138+
const H = window.innerHeight;
139+
if (dr.bottom > H + 1) return "dialog-overflows:" + Math.round(dr.bottom) + ">" + H;
140+
if (fr.bottom > dr.bottom + 1) return "iframe-overflows-dialog:" + Math.round(fr.bottom) + ">" + Math.round(dr.bottom);
141+
if (tabsBottomInWindow > H + 1) return "tabs-clipped:" + Math.round(tabsBottomInWindow) + ">" + H;
142+
if (tr.height < 8) return "tabs-zero-height";
143+
return "ok dialog=" + Math.round(dr.height) + "px iframe=" + Math.round(fr.height) + "px tabsBottom=" + Math.round(tabsBottomInWindow) + "px window=" + H + "px";`, dialogSel),
144+
}))
145+
if err != nil {
146+
return "", err
147+
}
148+
if len(verdict) < 2 || verdict[:2] != "ok" {
149+
return "", fmt.Errorf("editor layout broken in the real engine: %s", verdict)
150+
}
151+
return "board face selectable via band with full resize handles; editor dialog, iframe, and the two-page tab bar all inside the real window (" + verdict + ")", nil
152+
}

internal/webviewbridgesmoke/checks_test.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,18 +125,20 @@ func TestCheckIsolatedDataBadge(t *testing.T) {
125125
})
126126
}
127127

128-
// realWindows is the app's true three-window shape (ADR-0033) as
129-
// app_info reports it.
128+
// realWindows is the app's true window shape (ADR-0033 plus the
129+
// menu-bar panel's attachable window, goal 0189) as app_info reports
130+
// it.
130131
func realWindows() []map[string]any {
131132
return []map[string]any{
132133
{"name": "main", "visible": true},
133134
{"name": "quickpanel", "visible": false},
134135
{"name": "approvalprompt", "visible": false},
136+
{"name": "traypanel", "visible": false},
135137
}
136138
}
137139

138140
func TestCheckAppInfo(t *testing.T) {
139-
t.Run("darwin, real three-window shape", func(t *testing.T) {
141+
t.Run("darwin, real window set", func(t *testing.T) {
140142
f := newFakeCaller()
141143
f.onJSON("app_info", map[string]any{"os": "darwin", "windows": realWindows()})
142144
detail, err := checkAppInfo(f)

internal/webviewbridgesmoke/main.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,22 @@ import (
2828

2929
const (
3030
mcpHost = "127.0.0.1"
31-
mcpPort = 9099
3231
appBootTimeout = 30 * time.Second
3332
)
3433

34+
// mcpPort defaults to the bridge's own 9099 but yields to
35+
// MILL_SMOKE_MCP_PORT -- the installed production app holds 9099
36+
// itself (its release build ships the bridge), so a smoke run beside
37+
// a live Mill needs its own port rather than a quit-and-relaunch.
38+
var mcpPort = func() int {
39+
if v := os.Getenv("MILL_SMOKE_MCP_PORT"); v != "" {
40+
if n, err := strconv.Atoi(v); err == nil && n > 0 && n < 65536 {
41+
return n
42+
}
43+
}
44+
return 9099
45+
}()
46+
3547
func main() {
3648
if err := run(); err != nil {
3749
fmt.Fprintf(os.Stderr, "webview-bridge-smoke: %v\n", err)

0 commit comments

Comments
 (0)