Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions internal/webviewbridgesmoke/checks.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
{
Expand All @@ -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.",
Expand Down Expand Up @@ -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]
Expand All @@ -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
Expand Down
152 changes: 152 additions & 0 deletions internal/webviewbridgesmoke/checks_drawio.go
Original file line number Diff line number Diff line change
@@ -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 = `<mxfile host="app.diagrams.net" type="device">
<diagram id="smoke-page-one" name="Overview">
<mxGraphModel dx="800" dy="600" grid="1" gridSize="10" page="1" pageScale="1" pageWidth="850" pageHeight="1100">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="a1" value="Alpha" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="120" y="120" width="120" height="60" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
<diagram id="smoke-page-two" name="Detail">
<mxGraphModel dx="800" dy="600" grid="1" gridSize="10" page="1" pageScale="1" pageWidth="850" pageHeight="1100">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="b1" value="Gamma" style="rounded=1;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="200" y="160" width="140" height="70" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>`

// 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
}
8 changes: 5 additions & 3 deletions internal/webviewbridgesmoke/checks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 13 additions & 1 deletion internal/webviewbridgesmoke/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading