diff --git a/internal/webviewbridgesmoke/checks.go b/internal/webviewbridgesmoke/checks.go index ffe7eb1b..f8c8ee95 100644 --- a/internal/webviewbridgesmoke/checks.go +++ b/internal/webviewbridgesmoke/checks.go @@ -97,7 +97,7 @@ var registry = []check{ }, { name: "app-info-window-sane", - 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.", + 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.", run: checkAppInfo, }, { @@ -120,6 +120,11 @@ var registry = []check{ 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.", run: checkStickyClickToEdit, }, + { + name: "drawio-editor-layout", + 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.", + run: checkDrawioEditorLayout, + }, { name: "sticky-border-color-flip", 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) { if info.OS != "darwin" { return "", fmt.Errorf("expected os darwin, got %q", info.OS) } - // The app's real window set (ADR-0033): the named main window plus - // the two auxiliary windows. Asserted by name, not count, so a - // missing or unexpected window is named in the failure. - want := map[string]bool{mainWindowName: false, "quickpanel": false, "approvalprompt": false} + // The app's real window set (ADR-0033, plus the menu-bar panel's + // own attachable window -- goal 0189's SystemTray.AttachWindow): + // the named main window plus the three auxiliary windows. Asserted + // by name, not count, so a missing or unexpected window is named + // in the failure. + want := map[string]bool{mainWindowName: false, "quickpanel": false, "approvalprompt": false, "traypanel": false} mainVisible := false for _, w := range info.Windows { seen, expected := want[w.Name] @@ -177,7 +184,7 @@ func checkAppInfo(c mcpCaller) (string, error) { if !mainVisible { return "", fmt.Errorf("main window reported not visible") } - return fmt.Sprintf("os=%s windows=main+quickpanel+approvalprompt, main visible", info.OS), nil + return fmt.Sprintf("os=%s windows=main+quickpanel+approvalprompt+traypanel, main visible", info.OS), nil } // pollJSEval retries a boolean-returning js_eval snippet until it's diff --git a/internal/webviewbridgesmoke/checks_drawio.go b/internal/webviewbridgesmoke/checks_drawio.go new file mode 100644 index 00000000..9986b293 --- /dev/null +++ b/internal/webviewbridgesmoke/checks_drawio.go @@ -0,0 +1,152 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "time" +) + +// A minimal two-page drawio file: page switching only exists with 2+ +// pages, so a single-page fixture could never exercise the editor's +// tab bar at all. +const drawioTwoPageFixture = ` + + + + + + + + + + + + + + + + + + + + + + +` + +// landTwoPageDiagram writes the fixture to a throwaway path and lands +// it as a diagram board object through the same RPC a native drop +// uses, waiting until the board face renders and settles. +func landTwoPageDiagram(c mcpCaller) error { + dir, err := os.MkdirTemp("", "mill-smoke-drawio") + if err != nil { + return err + } + fixturePath := filepath.Join(dir, "two-page-smoke.drawio") + if err := os.WriteFile(fixturePath, []byte(drawioTwoPageFixture), 0o600); err != nil { + return err + } + parentID, err := gettingStartedParentID(c) + if err != nil { + return err + } + var obj struct { + ID string `json:"ID"` + } + if err := callBoundJSON(c, "github.com/alicoding/mill/internal/services/atlassvc.AtlasService.CreateBoardObject", + []any{"diagram", map[string]any{"mirrorPath": fixturePath, "title": "smoke two-page"}, map[string]any{"X": 620, "Y": 340}, parentID}, &obj); err != nil { + return err + } + bodySel := `[data-testid="atlas-drawio-page-body"]` + if err := pollJSEval(c, fmt.Sprintf(`return !!document.querySelector('%s');`, bodySel), 15*time.Second); err != nil { + return fmt.Errorf("diagram board face never rendered after CreateBoardObject: %w", err) + } + if err := waitForNodeStable(c, bodySel); err != nil { + return fmt.Errorf("board never settled before the band click: %w", err) + } + return nil +} + +// bandSel is the diagram's chrome band -- its ONLY selection surface +// (the vendored viewer captures body clicks for pan/zoom). +const drawioBandSel = `.react-flow__node [data-testid="atlas-board-object-frame"]` + +// openDrawioEditor double-clicks the band to open the embedded editor. +// Two bridge clicks land faster than the OS double-click threshold +// reliably here; a synthesized dblclick is the one escape hatch, +// allowed because the bridge has no native dblclick primitive and the +// double-click GATE itself is proven by e2e. +func openDrawioEditor(c mcpCaller, dialogSel string) error { + for i := 0; i < 2; i++ { + if _, err := c.call("mouse_click", withWindow(map[string]any{"selector": drawioBandSel})); err != nil { + return err + } + } + if pollJSEval(c, fmt.Sprintf(`return !!document.querySelector('%s iframe');`, dialogSel), 20*time.Second) == nil { + return nil + } + if err := pollJSEval(c, fmt.Sprintf(`const band = document.querySelector('%s'); + if (!band) return false; + band.dispatchEvent(new MouseEvent('dblclick', {bubbles: true, cancelable: true})); + return true;`, drawioBandSel), 3*time.Second); err != nil { + return fmt.Errorf("no chrome band to double-click: %w", err) + } + if err := pollJSEval(c, fmt.Sprintf(`return !!document.querySelector('%s iframe');`, dialogSel), 20*time.Second); err != nil { + return fmt.Errorf("embedded editor dialog never opened: %w", err) + } + return nil +} + +func checkDrawioEditorLayout(c mcpCaller) (string, error) { + if err := landTwoPageDiagram(c); err != nil { + return "", err + } + + // Selecting the band must produce the shared NodeResizer's full + // handle set in the real engine, exactly as Chromium renders it. + if _, err := c.call("mouse_click", withWindow(map[string]any{"selector": drawioBandSel})); err != nil { + return "", err + } + if err := pollJSEval(c, `return document.querySelectorAll('.react-flow__resize-control').length >= 8;`, 5*time.Second); err != nil { + return "", fmt.Errorf("resize handles never appeared after selecting the diagram's chrome band: %w", err) + } + + dialogSel := `[data-component="atlas-drawio-editor-dialog"]` + if err := openDrawioEditor(c, dialogSel); err != nil { + return "", err + } + + // The clipping class this check pins (goal 0259): the editor's + // bottom chrome -- where drawio's own page tabs live -- must sit + // INSIDE the real window, and the tab bar itself must be present + // and reachable for a multi-page file. Same-origin iframe, so its + // document is directly measurable. + if err := pollJSEval(c, fmt.Sprintf(`const f = document.querySelector('%s iframe'); + const d = f && f.contentDocument; + return !!(d && d.querySelector('.geTabContainer'));`, dialogSel), 30*time.Second); err != nil { + return "", fmt.Errorf("the editor's page-tab bar (.geTabContainer) never appeared for a two-page file: %w", err) + } + verdict, err := c.call("js_eval", withWindow(map[string]any{ + "js": fmt.Sprintf(`const dlg = document.querySelector('%s'); + const f = dlg.querySelector('iframe'); + const dr = dlg.getBoundingClientRect(); + const fr = f.getBoundingClientRect(); + const tabs = f.contentDocument.querySelector('.geTabContainer'); + const tr = tabs.getBoundingClientRect(); + const tabsBottomInWindow = fr.top + tr.bottom; + const H = window.innerHeight; + if (dr.bottom > H + 1) return "dialog-overflows:" + Math.round(dr.bottom) + ">" + H; + if (fr.bottom > dr.bottom + 1) return "iframe-overflows-dialog:" + Math.round(fr.bottom) + ">" + Math.round(dr.bottom); + if (tabsBottomInWindow > H + 1) return "tabs-clipped:" + Math.round(tabsBottomInWindow) + ">" + H; + if (tr.height < 8) return "tabs-zero-height"; + return "ok dialog=" + Math.round(dr.height) + "px iframe=" + Math.round(fr.height) + "px tabsBottom=" + Math.round(tabsBottomInWindow) + "px window=" + H + "px";`, dialogSel), + })) + if err != nil { + return "", err + } + if len(verdict) < 2 || verdict[:2] != "ok" { + return "", fmt.Errorf("editor layout broken in the real engine: %s", verdict) + } + 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 +} diff --git a/internal/webviewbridgesmoke/checks_test.go b/internal/webviewbridgesmoke/checks_test.go index bac86321..6e38555c 100644 --- a/internal/webviewbridgesmoke/checks_test.go +++ b/internal/webviewbridgesmoke/checks_test.go @@ -125,18 +125,20 @@ func TestCheckIsolatedDataBadge(t *testing.T) { }) } -// realWindows is the app's true three-window shape (ADR-0033) as -// app_info reports it. +// realWindows is the app's true window shape (ADR-0033 plus the +// menu-bar panel's attachable window, goal 0189) as app_info reports +// it. func realWindows() []map[string]any { return []map[string]any{ {"name": "main", "visible": true}, {"name": "quickpanel", "visible": false}, {"name": "approvalprompt", "visible": false}, + {"name": "traypanel", "visible": false}, } } func TestCheckAppInfo(t *testing.T) { - t.Run("darwin, real three-window shape", func(t *testing.T) { + t.Run("darwin, real window set", func(t *testing.T) { f := newFakeCaller() f.onJSON("app_info", map[string]any{"os": "darwin", "windows": realWindows()}) detail, err := checkAppInfo(f) diff --git a/internal/webviewbridgesmoke/main.go b/internal/webviewbridgesmoke/main.go index 3fbab709..6349b638 100644 --- a/internal/webviewbridgesmoke/main.go +++ b/internal/webviewbridgesmoke/main.go @@ -28,10 +28,22 @@ import ( const ( mcpHost = "127.0.0.1" - mcpPort = 9099 appBootTimeout = 30 * time.Second ) +// mcpPort defaults to the bridge's own 9099 but yields to +// MILL_SMOKE_MCP_PORT -- the installed production app holds 9099 +// itself (its release build ships the bridge), so a smoke run beside +// a live Mill needs its own port rather than a quit-and-relaunch. +var mcpPort = func() int { + if v := os.Getenv("MILL_SMOKE_MCP_PORT"); v != "" { + if n, err := strconv.Atoi(v); err == nil && n > 0 && n < 65536 { + return n + } + } + return 9099 +}() + func main() { if err := run(); err != nil { fmt.Fprintf(os.Stderr, "webview-bridge-smoke: %v\n", err)