From fb00436564bb58f64bc4743f01894bdeca3e0b6a Mon Sep 17 00:00:00 2001 From: lbb00 Date: Thu, 3 Sep 2026 20:25:53 +0800 Subject: [PATCH 01/13] =?UTF-8?q?feat(devtools):=20=E5=B0=8F=E7=A8=8B?= =?UTF-8?q?=E5=BA=8F=E5=BC=80=E5=8F=91=E9=A1=B5=E6=94=B9=E4=B8=BA=E7=8B=AC?= =?UTF-8?q?=E7=AB=8B=E7=AA=97=E5=8F=A3=EF=BC=8C=E5=8F=AF=E5=90=8C=E6=97=B6?= =?UTF-8?q?=E5=BC=80=E5=A4=9A=E4=B8=AA=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 项目列表窗口不再被开发页顶替,它一直留着;点开一个项目就新开一个工作台窗口,模拟器、右侧面板、底部调试面板和编辑器都属于各自的窗口。关掉其中一个只销毁它自己的会话和资源,关掉最后一个也不退出应用。 一个窗口一个 http 端口会把每个窗口推到不同的 origin。浏览器存储是按 scheme + host + 端口分桶的,IndexedDB、OPFS、Cache Storage、Service Worker 注册、Web Locks 和 BroadcastChannel 都在里面,所以窗口一关,它写下的 VS Code 工作区状态(打开的标签页、视图状态、资源管理器展开情况)就跟着没了;重开同一个项目拿到的是一个空桶,而且每换一个端口就多留一份没人回收的 Service Worker 注册和缓存。改成整个进程共用一个监听器,每个窗口用 `/w//` 这段路径前缀区分,所有工作台就都在同一个 origin 上了。这个前缀只用来路由,不是安全边界——同 origin 的页面本来就能互相访问路径,和以前用一个全局服务时的处境一样。监听器还要比最后一个窗口活得久:关掉所有项目再开一个是很平常的切换动作,端口一放就换 origin,之前存的东西就找不回来了。只有 `closeAllCoiHosts` 会真的关掉它。 顺带修的三处错误处理:请求路径里的畸形百分号转义由窗口自己的 handler 回 400,因为只有它知道这是一段请求路径;共享监听器那层兜住的异常改成回 500 并打日志,走到那里说明是 handler 自己没处理,回 400 等于把责任推给调用方还把 bug 藏起来;静态文件的读流挂上 error 监听,文件在 stat 之后消失时销毁响应——这个错误是异步到的,那时 200 和 Content-Length 已经发出去了,不接的话就是主进程里一个没人处理的异常。 e2e 这边,`workbench` fixture 原来按窗口列举顺序取第一个工作台窗口。现在多个项目窗口可以同时存在,前一个 spec 留下的窗口会被后一个 spec 当成自己的(extension-panels 就读到了 tabbar-app 的 WXML 面板)。改成按当前 describe 打开的项目路径找窗口。editor-project-switch 也补上了收尾,它开过的两个项目都会关掉,关不掉就报错而不是咽下去——关不掉正是这个收尾要防的事。 pawl 的 test-coverage 基线一起重录了。九个包里八个都在 vitest 配置里把 `coverage.include` 钉到整个 `src/**`,只有 dimina-electron-runtime 没钉,所以它记录的 83.68 是在一个残缺分母上量出来的:真实的 `src/**` 有 3116 行,基线只数了 1549 行。补上之后同分母对比,本次新增的测试把覆盖行数从 1140 抬到 1701,涨了 18 个百分点;掉的是分母定义,不是覆盖率。 Pawl-Accept: test-coverage 76.36 Co-Authored-By: Claude Opus 5 (1M context) --- packages/devtools/e2e/appdata-edit.spec.ts | 8 +- .../devtools/e2e/automator-compat.spec.ts | 13 +- .../e2e/automator-label-semantics.spec.ts | 8 +- packages/devtools/e2e/automator/automator.ts | 21 +- packages/devtools/e2e/automator/element.ts | 11 +- .../devtools/e2e/automator/mini-program.ts | 21 +- packages/devtools/e2e/automator/page.ts | 15 +- packages/devtools/e2e/broken-project.spec.ts | 14 +- .../devtools/e2e/console-filter-live.spec.ts | 7 +- .../devtools/e2e/console-filter-reset.spec.ts | 4 +- packages/devtools/e2e/devtools-panel.spec.ts | 26 +- .../devtools/e2e/devtools-tab-order.spec.ts | 4 +- packages/devtools/e2e/dialog-zorder.spec.ts | 67 +- packages/devtools/e2e/disk-sync.spec.ts | 4 +- .../e2e/dock-consolidation-smoke.spec.ts | 54 +- .../e2e/dock-devtools-position-preset.spec.ts | 34 +- .../e2e/dock-keepalive-and-collapse.spec.ts | 32 +- packages/devtools/e2e/dock-real-drag.spec.ts | 43 +- .../e2e/dock-resize-sync-regressions.spec.ts | 44 +- .../e2e/dock-resize-sync-regressions2.spec.ts | 50 +- .../devtools/e2e/dock-resize-sync.spec.ts | 46 +- .../devtools/e2e/dock-separator-drag.spec.ts | 52 +- .../devtools/e2e/dock-tab-reorder.spec.ts | 37 +- .../devtools/e2e/editor-hot-reload.spec.ts | 28 +- .../e2e/editor-project-switch.spec.ts | 55 +- .../editor-theme-boot-no-white-flash.spec.ts | 5 +- packages/devtools/e2e/extension-host.spec.ts | 19 +- .../devtools/e2e/extension-panels.spec.ts | 32 +- .../devtools/e2e/failure-behaviors.spec.ts | 21 +- packages/devtools/e2e/fixtures.ts | 50 +- packages/devtools/e2e/helpers.ts | 170 ++- .../e2e/home-button-no-tabbar.spec.ts | 6 +- packages/devtools/e2e/home-button.spec.ts | 6 +- packages/devtools/e2e/host-sidebar.spec.ts | 72 +- .../e2e/host-toolbar-lifecycle.spec.ts | 215 ++-- .../devtools/e2e/host-toolbar-port.spec.ts | 41 +- .../devtools/e2e/host-toolbar-replay.spec.ts | 191 +-- packages/devtools/e2e/host-toolbar.spec.ts | 143 ++- .../internal-devtools-console-reopen.spec.ts | 9 +- .../e2e/internal-devtools-window.spec.ts | 18 +- packages/devtools/e2e/ipc-workflow.spec.ts | 13 +- packages/devtools/e2e/minigame-launch.spec.ts | 15 +- ...native-host-console-project-switch.spec.ts | 15 +- .../e2e/native-host-current-page.spec.ts | 31 +- .../e2e/native-host-devtools-console.spec.ts | 4 +- ...ive-host-devtools-elements-respawn.spec.ts | 16 +- .../e2e/native-host-devtools-elements.spec.ts | 10 +- .../native-host-network-response-body.spec.ts | 4 +- .../e2e/native-host-open-in-editor.spec.ts | 23 +- .../devtools/e2e/native-host-render.spec.ts | 25 +- .../e2e/native-host-thumbnail-capture.spec.ts | 7 +- .../native-host-websocket-contract.spec.ts | 4 +- .../e2e/native-host-websocket-panel.spec.ts | 14 +- ...native-host-white-screen-reconcile.spec.ts | 7 +- .../e2e/native-host-wx-method.spec.ts | 4 +- .../e2e/native-host-wxml-highlight.spec.ts | 42 +- packages/devtools/e2e/panel-switching.spec.ts | 18 +- packages/devtools/e2e/qdml-filetypes.spec.ts | 8 +- .../e2e/recompile-button-recompiles.spec.ts | 7 +- .../devtools/e2e/relaunch-resilience.spec.ts | 70 +- packages/devtools/e2e/settings.spec.ts | 24 +- ...andalone-devtools-flood-cold-start.spec.ts | 9 +- .../standalone-devtools-window-churn.spec.ts | 70 +- packages/devtools/e2e/toolbar-visual.spec.ts | 48 +- packages/devtools/e2e/wal-audit.spec.ts | 4 +- packages/devtools/e2e/workbench-probe.ts | 9 +- packages/devtools/src/main/app/app.ts | 1057 +++++------------ .../app/close-invalid-project-no-quit.test.ts | 146 +-- .../app/close-with-active-session.test.ts | 143 ++- .../src/main/app/double-close-quit.test.ts | 185 +-- packages/devtools/src/main/app/editor-view.ts | 90 ++ .../devtools/src/main/app/global-mirrors.ts | 61 + .../src/main/app/host-sidebar-default.ts | 55 + .../main/app/instance-ipc-extension.test.ts | 70 +- .../main/app/instance-simulator-api.test.ts | 149 ++- ...nal-devtools-global-console-wiring.test.ts | 90 +- ...nal-devtools-global-network-wiring.test.ts | 78 +- .../main/app/main-window-close-guard.test.ts | 104 ++ .../app/menu-active-project-context.test.ts | 102 ++ packages/devtools/src/main/app/menu-setup.ts | 70 ++ .../devtools/src/main/app/project-window.ts | 146 +++ .../src/main/app/quit-flag-onclose.test.ts | 84 +- packages/devtools/src/main/app/servers.ts | 85 ++ .../main/app/simulator-api-app-level.test.ts | 85 ++ ...ui-extension-active-project-window.test.ts | 106 ++ .../app/ui-extension-registration.test.ts | 184 +++ .../src/main/app/ui-extension-targets.ts | 140 +++ .../main/app/window-close-reveal.harness.ts | 84 ++ .../devtools/src/main/app/window-events.ts | 45 + .../src/main/app/window-runtime-services.ts | 222 ++++ .../app/workbench-window-close-hook.test.ts | 167 +++ .../workbench-window-open-teardown.test.ts | 179 +++ .../workbench-window-reveal-on-close.test.ts | 95 ++ .../devtools/src/main/app/workbench-window.ts | 266 +++++ .../eslint-workbench-context-gate.test.ts | 3 +- packages/devtools/src/main/ipc/app.ts | 11 +- ...bridge-router-api-fail-passthrough.test.ts | 2 +- .../ipc/bridge-router-api-namespaces.test.ts | 2 +- .../ipc/bridge-router-app-lifecycle.test.ts | 5 +- .../src/main/ipc/bridge-router-census.test.ts | 2 +- .../bridge-router-connection-reset.test.ts | 2 +- .../main/ipc/bridge-router-debugtap.test.ts | 2 +- .../ipc/bridge-router-diagnostics.test.ts | 2 +- .../bridge-router-dispose-awaitable.test.ts | 2 +- .../ipc/bridge-router-entry-page-path.test.ts | 2 +- .../main/ipc/bridge-router-keep-api.test.ts | 2 +- ...idge-router-logic-bundle-fail-loud.test.ts | 2 +- .../bridge-router-minigame-load-order.test.ts | 2 +- .../bridge-router-missing-page-guard.test.ts | 2 +- ...ridge-router-multi-session-binding.test.ts | 2 +- ...ridge-router-multi-session-overlap.test.ts | 2 +- .../main/ipc/bridge-router-nav-guard.test.ts | 2 +- .../ipc/bridge-router-page-open-guard.test.ts | 2 +- .../ipc/bridge-router-registered-apis.test.ts | 2 +- .../bridge-router-registry-no-leak.test.ts | 2 +- .../bridge-router-request-watchdog.test.ts | 2 +- .../bridge-router-resolve-current-app.test.ts | 4 +- .../bridge-router-resource-base-url.test.ts | 2 +- .../ipc/bridge-router-root-page-close.test.ts | 2 +- ...idge-router-runtime-status-dispose.test.ts | 2 +- ...dge-router-runtime-status-failures.test.ts | 2 +- ...ridge-router-runtime-status-launch.test.ts | 2 +- .../bridge-router-service-host-ready.test.ts | 12 +- .../bridge-router-simulator-destroyed.test.ts | 2 +- .../ipc/bridge-router-spawn-fallback.test.ts | 2 +- .../bridge-router-spawn-teardown-race.test.ts | 2 +- .../bridge-router-storage-wxml-notify.test.ts | 12 +- .../src/main/ipc/internal-devtools.ts | 7 +- packages/devtools/src/main/ipc/popover.ts | 13 +- .../devtools/src/main/ipc/project-create.ts | 11 +- packages/devtools/src/main/ipc/project-fs.ts | 34 +- .../src/main/ipc/projects-edit-dialog.test.ts | 2 +- packages/devtools/src/main/ipc/projects.ts | 27 +- packages/devtools/src/main/ipc/session.ts | 23 +- packages/devtools/src/main/ipc/settings.ts | 28 +- .../devtools/src/main/ipc/simulator-module.ts | 14 +- packages/devtools/src/main/ipc/simulator.ts | 20 +- packages/devtools/src/main/ipc/tooltip.ts | 15 +- packages/devtools/src/main/ipc/views.ts | 39 +- .../devtools-backend-before-quit.harness.ts | 331 +----- .../src/main/runtime/devtools-backend.ts | 16 +- .../runtime/devtools-runtime-mock.harness.ts | 347 ++++++ .../src/main/services/app-services.ts | 113 ++ .../services/automation/connection-target.ts | 70 ++ .../services/automation/console-bridge.ts | 123 ++ .../main/services/automation/index.test.ts | 44 +- .../src/main/services/automation/index.ts | 187 +-- .../automation/port-sender-routing.test.ts | 108 ++ .../automation/target-binding.test.ts | 278 +++++ .../src/main/services/fs-watch-sse.ts | 19 +- .../mcp/active-window-repoint.test.ts | 168 +++ .../services/mcp/connect-generation.test.ts | 206 ++++ .../src/main/services/mcp/connection-owner.ts | 98 ++ .../mcp/connection-window-binding.test.ts | 315 +++++ .../devtools/src/main/services/mcp/index.ts | 14 +- .../services/mcp/opened-project.close.test.ts | 47 + .../main/services/mcp/opened-project.test.ts | 115 ++ .../src/main/services/mcp/opened-project.ts | 101 ++ .../src/main/services/mcp/server.test.ts | 2 +- .../mcp/target-manager.select.test.ts | 24 +- .../src/main/services/mcp/target-manager.ts | 369 ++++-- .../context-tools.native-overview.test.ts | 30 +- .../tools/project-close-pinned-window.test.ts | 106 ++ .../services/mcp/tools/project-tools.test.ts | 138 ++- .../main/services/mcp/tools/project-tools.ts | 53 +- .../services/mcp/window-scoped-state.test.ts | 182 +++ packages/devtools/src/main/services/module.ts | 18 +- .../main/services/simulator-storage/index.ts | 42 +- .../simulator-storage/multi-window.test.ts | 252 ++++ .../simulator-storage/window-scope.ts | 19 + .../main/services/simulator/ui-extensions.ts | 25 +- .../window-contexts/context-router.test.ts | 113 ++ .../window-contexts/context-router.ts | 89 ++ .../main/services/workbench-coi-host.test.ts | 472 ++++++++ .../src/main/services/workbench-coi-host.ts | 251 ++++ .../workbench-coi-server-decode.test.ts | 109 ++ .../services/workbench-coi-server.test.ts | 132 ++ .../src/main/services/workbench-coi-server.ts | 142 +-- .../services/workbench-coi-shutdown.test.ts | 75 ++ .../main/services/workbench-coi-shutdown.ts | 108 ++ .../services/workbench-coi-static.test.ts | 166 +++ .../src/main/services/workbench-coi-static.ts | 84 ++ .../src/main/services/workbench-context.ts | 53 +- .../services/workspace/workspace-referer.ts | 37 + .../services/workspace/workspace-service.ts | 24 +- .../src/main/utils/ipc-context-source.ts | 53 + .../devtools/src/main/utils/ipc-registry.ts | 201 +++- .../devtools/src/main/utils/sender-policy.ts | 24 +- .../src/main/windows/main-window/create.ts | 10 +- .../main-window/devtools-shortcut.test.ts | 156 +++ .../src/main/windows/main-window/events.ts | 42 +- .../runtime/host-sidebar-advertiser.test.ts | 55 + .../src/preload/runtime/wait-for-slot-root.ts | 42 +- .../src/renderer/entries/workbench/index.html | 34 + .../src/renderer/entries/workbench/main.tsx | 57 + .../controllers/use-layout-store.test.ts | 139 ++- .../controllers/use-layout-store.ts | 152 ++- .../project-runtime/project-runtime.tsx | 2 +- .../modules/main/main-edit-dialog.test.tsx | 1 - .../main/main-open-project-window.test.tsx | 77 ++ .../src/renderer/modules/main/main.tsx | 82 +- .../src/renderer/shared/api/view-api.ts | 15 +- packages/devtools/src/shared/ipc-channels.ts | 11 +- packages/devtools/src/shared/types.ts | 46 +- packages/devtools/vite.config.ts | 1 + packages/dimina-electron-runtime/src/index.ts | 9 +- .../src/main/ipc/bridge-router-ipc-mux.ts | 64 + .../ipc/bridge-router-multi-window.test.ts | 412 +++++++ .../ipc/bridge-router-protocol-mux.test.ts | 159 +++ .../main/ipc/bridge-router-protocol-mux.ts | 95 ++ .../bridge-router-service-invoke-page.test.ts | 404 +++++++ .../ipc/bridge-router-service-page.test.ts | 97 ++ .../src/main/ipc/bridge-router.ts | 132 +- .../main/services/console-forward/index.ts | 4 +- .../main/services/simulator/referer.test.ts | 85 ++ .../src/main/services/simulator/referer.ts | 55 +- .../views/simulator-session-policy.ts | 17 +- .../dimina-electron-runtime/vitest.config.ts | 20 + packages/electron-deck/src/main/index.ts | 8 + .../electron-deck/src/main/ipc-mux.test.ts | 383 ++++++ packages/electron-deck/src/main/ipc-mux.ts | 118 ++ .../src/main/main-entry-electron-free.test.ts | 105 ++ .../src/main/window-hosts.test.ts | 153 +++ .../electron-deck/src/main/window-hosts.ts | 35 + .../workbench/src/contributed-extensions.ts | 9 +- packages/workbench/src/host-base-url.ts | 12 + packages/workbench/src/main.ts | 27 +- .../src/workspace/disk-workspace-source.ts | 6 +- pawl.snapshot.json | 81 +- 229 files changed, 12961 insertions(+), 3363 deletions(-) create mode 100644 packages/devtools/src/main/app/editor-view.ts create mode 100644 packages/devtools/src/main/app/global-mirrors.ts create mode 100644 packages/devtools/src/main/app/host-sidebar-default.ts create mode 100644 packages/devtools/src/main/app/main-window-close-guard.test.ts create mode 100644 packages/devtools/src/main/app/menu-active-project-context.test.ts create mode 100644 packages/devtools/src/main/app/menu-setup.ts create mode 100644 packages/devtools/src/main/app/project-window.ts create mode 100644 packages/devtools/src/main/app/servers.ts create mode 100644 packages/devtools/src/main/app/simulator-api-app-level.test.ts create mode 100644 packages/devtools/src/main/app/ui-extension-active-project-window.test.ts create mode 100644 packages/devtools/src/main/app/ui-extension-registration.test.ts create mode 100644 packages/devtools/src/main/app/ui-extension-targets.ts create mode 100644 packages/devtools/src/main/app/window-close-reveal.harness.ts create mode 100644 packages/devtools/src/main/app/window-events.ts create mode 100644 packages/devtools/src/main/app/window-runtime-services.ts create mode 100644 packages/devtools/src/main/app/workbench-window-close-hook.test.ts create mode 100644 packages/devtools/src/main/app/workbench-window-open-teardown.test.ts create mode 100644 packages/devtools/src/main/app/workbench-window-reveal-on-close.test.ts create mode 100644 packages/devtools/src/main/app/workbench-window.ts create mode 100644 packages/devtools/src/main/runtime/devtools-runtime-mock.harness.ts create mode 100644 packages/devtools/src/main/services/app-services.ts create mode 100644 packages/devtools/src/main/services/automation/connection-target.ts create mode 100644 packages/devtools/src/main/services/automation/console-bridge.ts create mode 100644 packages/devtools/src/main/services/automation/port-sender-routing.test.ts create mode 100644 packages/devtools/src/main/services/automation/target-binding.test.ts create mode 100644 packages/devtools/src/main/services/mcp/active-window-repoint.test.ts create mode 100644 packages/devtools/src/main/services/mcp/connect-generation.test.ts create mode 100644 packages/devtools/src/main/services/mcp/connection-owner.ts create mode 100644 packages/devtools/src/main/services/mcp/connection-window-binding.test.ts create mode 100644 packages/devtools/src/main/services/mcp/opened-project.close.test.ts create mode 100644 packages/devtools/src/main/services/mcp/opened-project.test.ts create mode 100644 packages/devtools/src/main/services/mcp/opened-project.ts create mode 100644 packages/devtools/src/main/services/mcp/tools/project-close-pinned-window.test.ts create mode 100644 packages/devtools/src/main/services/mcp/window-scoped-state.test.ts create mode 100644 packages/devtools/src/main/services/simulator-storage/multi-window.test.ts create mode 100644 packages/devtools/src/main/services/simulator-storage/window-scope.ts create mode 100644 packages/devtools/src/main/services/window-contexts/context-router.test.ts create mode 100644 packages/devtools/src/main/services/window-contexts/context-router.ts create mode 100644 packages/devtools/src/main/services/workbench-coi-host.test.ts create mode 100644 packages/devtools/src/main/services/workbench-coi-host.ts create mode 100644 packages/devtools/src/main/services/workbench-coi-server-decode.test.ts create mode 100644 packages/devtools/src/main/services/workbench-coi-shutdown.test.ts create mode 100644 packages/devtools/src/main/services/workbench-coi-shutdown.ts create mode 100644 packages/devtools/src/main/services/workbench-coi-static.test.ts create mode 100644 packages/devtools/src/main/services/workbench-coi-static.ts create mode 100644 packages/devtools/src/main/services/workspace/workspace-referer.ts create mode 100644 packages/devtools/src/main/utils/ipc-context-source.ts create mode 100644 packages/devtools/src/main/windows/main-window/devtools-shortcut.test.ts create mode 100644 packages/devtools/src/renderer/entries/workbench/index.html create mode 100644 packages/devtools/src/renderer/entries/workbench/main.tsx create mode 100644 packages/devtools/src/renderer/modules/main/main-open-project-window.test.tsx create mode 100644 packages/dimina-electron-runtime/src/main/ipc/bridge-router-ipc-mux.ts create mode 100644 packages/dimina-electron-runtime/src/main/ipc/bridge-router-multi-window.test.ts create mode 100644 packages/dimina-electron-runtime/src/main/ipc/bridge-router-protocol-mux.test.ts create mode 100644 packages/dimina-electron-runtime/src/main/ipc/bridge-router-protocol-mux.ts create mode 100644 packages/dimina-electron-runtime/src/main/ipc/bridge-router-service-invoke-page.test.ts create mode 100644 packages/dimina-electron-runtime/src/main/ipc/bridge-router-service-page.test.ts create mode 100644 packages/dimina-electron-runtime/src/main/services/simulator/referer.test.ts create mode 100644 packages/electron-deck/src/main/ipc-mux.test.ts create mode 100644 packages/electron-deck/src/main/ipc-mux.ts create mode 100644 packages/electron-deck/src/main/main-entry-electron-free.test.ts create mode 100644 packages/electron-deck/src/main/window-hosts.test.ts create mode 100644 packages/electron-deck/src/main/window-hosts.ts create mode 100644 packages/workbench/src/host-base-url.ts diff --git a/packages/devtools/e2e/appdata-edit.spec.ts b/packages/devtools/e2e/appdata-edit.spec.ts index c55d2dc4..594be29b 100644 --- a/packages/devtools/e2e/appdata-edit.spec.ts +++ b/packages/devtools/e2e/appdata-edit.spec.ts @@ -7,18 +7,18 @@ test.describe('AppData Panel Edit Write-Back', () => { useSharedProject(test, DEMO_APP_DIR) - test('editing a value in the tree round-trips through service→render setData and re-renders from the pushed snapshot', async ({ mainWindow }) => { - await mainWindow.getByRole('tab', { name: 'AppData' }).click() + test('editing a value in the tree round-trips through service→render setData and re-renders from the pushed snapshot', async ({ workbench }) => { + await workbench.getByRole('tab', { name: 'AppData' }).click() // Pages sidebar lists the running page; the demo app's first page is // pages/index/index and its bridge auto-activates (useActiveBridgeId // follows the simulator's active page path), so the data tree for it is // already the visible one once data arrives. - const pages = mainWindow.getByTestId('appdata-pages') + const pages = workbench.getByTestId('appdata-pages') await expect(pages).toBeVisible({ timeout: 30_000 }) await expect(pages).toContainText('pages/index/index', { timeout: 30_000 }) - const tree = mainWindow.getByTestId('appdata-tree') + const tree = workbench.getByTestId('appdata-tree') await expect(tree).toBeVisible({ timeout: 30_000 }) // The root row starts expanded but its children start collapsed; open diff --git a/packages/devtools/e2e/automator-compat.spec.ts b/packages/devtools/e2e/automator-compat.spec.ts index 049acea3..3fdc5cc4 100644 --- a/packages/devtools/e2e/automator-compat.spec.ts +++ b/packages/devtools/e2e/automator-compat.spec.ts @@ -117,7 +117,9 @@ test.describe('miniprogram-automator protocol compatibility', () => { // The following tests open a project first test.describe('with project open', () => { test.beforeAll(async () => { - await openProjectInUI(mainWindow, DEMO_APP_DIR, { waitMs: 8000 }) + // Return value not captured — the rest of this block drives the app + // purely over the automation-protocol WebSocket, never a Page object. + await openProjectInUI(electronApp, DEMO_APP_DIR, { waitMs: 8000 }) await waitForSimulatorWebview(electronApp) // NATIVE-HOST readiness gate. The page DOM is no longer in a same-document @@ -149,7 +151,7 @@ test.describe('miniprogram-automator protocol compatibility', () => { }) test.afterAll(async () => { - await closeProject(mainWindow).catch(() => {}) + await closeProject(electronApp).catch(() => {}) }) test('App.getPageStack returns stack', async () => { @@ -269,7 +271,10 @@ test.describe('npm miniprogram-automator package', () => { 100, ) as number - await openProjectInUI(smokeMainWindow, DEMO_APP_DIR, { waitMs: 8000 }) + // Return value not captured — this block hands off to the npm + // `miniprogram-automator` package's own `automator.connect(wsEndpoint)`, + // never a Page object of ours. + await openProjectInUI(smokeElectronApp, DEMO_APP_DIR, { waitMs: 8000 }) await waitForSimulatorWebview(smokeElectronApp) await new Promise((r) => setTimeout(r, 2000)) @@ -282,7 +287,7 @@ test.describe('npm miniprogram-automator package', () => { if (miniProgram) { miniProgram.disconnect() } - await closeProject(smokeMainWindow).catch(() => {}) + await closeProject(smokeElectronApp).catch(() => {}) await smokeElectronApp?.close().catch(() => {}) }) diff --git a/packages/devtools/e2e/automator-label-semantics.spec.ts b/packages/devtools/e2e/automator-label-semantics.spec.ts index bac15d28..eeaccc2e 100644 --- a/packages/devtools/e2e/automator-label-semantics.spec.ts +++ b/packages/devtools/e2e/automator-label-semantics.spec.ts @@ -248,7 +248,11 @@ test.beforeAll(async () => { 100, ) as number - await openProjectInUI(mainWindow, PROBE_APP_DIR, { waitMs: 8000 }) + // Return value not captured — everything after this drives the probe purely + // through the automation-protocol WebSocket (`miniProgram`/`page`) or scans + // `electronApp`'s webContents by URL marker (`trustedClick`), never the + // workbench window's own Page. + await openProjectInUI(electronApp, PROBE_APP_DIR, { waitMs: 8000 }) await waitForSimulatorWebview(electronApp) miniProgram = await automator.connect({ wsEndpoint: `ws://127.0.0.1:${autoPort}` }) @@ -268,7 +272,7 @@ test.beforeAll(async () => { test.afterAll(async () => { miniProgram?.disconnect() - await closeProject(mainWindow).catch(() => {}) + await closeProject(electronApp).catch(() => {}) await electronApp?.close().catch(() => {}) }) diff --git a/packages/devtools/e2e/automator/automator.ts b/packages/devtools/e2e/automator/automator.ts index 349fa79e..457f1da6 100644 --- a/packages/devtools/e2e/automator/automator.ts +++ b/packages/devtools/e2e/automator/automator.ts @@ -12,7 +12,7 @@ import fs from 'fs' import path from 'path' import { fileURLToPath } from 'url' import { MiniProgram } from './mini-program' -import { openProjectInUI, waitForSimulatorWebview } from '../helpers' +import { findMainWindow, openProjectInUI, waitForSimulatorWebview } from '../helpers' const __dirname = path.dirname(fileURLToPath(import.meta.url)) @@ -73,9 +73,10 @@ export class Automator { }, }) - // Wait for the main window - const mainWindow = await electronApp.firstWindow() - await mainWindow.waitForLoadState('domcontentloaded') + // Wait for the project-list window before we can move it off-screen and + // open a project into its own workbench window. + const listWindow = await findMainWindow(electronApp) + await listWindow.waitForLoadState('domcontentloaded') // Move off-screen so it doesn't steal focus await electronApp.evaluate(async ({ BrowserWindow }) => { @@ -92,8 +93,8 @@ export class Automator { } }) - // Open the project - await openProjectInUI(mainWindow, projectPath, { + // Open the project — this opens (and returns) the project's own workbench window. + const workbench = await openProjectInUI(electronApp, projectPath, { waitMs: compileWaitMs, }) @@ -101,18 +102,18 @@ export class Automator { await waitForSimulatorWebview(electronApp) } - return new MiniProgram(electronApp, mainWindow, projectPath) + return new MiniProgram(electronApp, workbench, projectPath) } /** * Connect to an already-running devtools instance. - * Requires electronApp and mainWindow from Playwright fixtures. + * Requires electronApp and the project's workbench window from Playwright fixtures. */ static connect( electronApp: ElectronApplication, - mainWindow: PwPage, + workbench: PwPage, projectPath: string, ): MiniProgram { - return new MiniProgram(electronApp, mainWindow, projectPath) + return new MiniProgram(electronApp, workbench, projectPath) } } diff --git a/packages/devtools/e2e/automator/element.ts b/packages/devtools/e2e/automator/element.ts index 44ea4220..ba86bba7 100644 --- a/packages/devtools/e2e/automator/element.ts +++ b/packages/devtools/e2e/automator/element.ts @@ -26,18 +26,19 @@ function inIframe(expression: string): string { export class Element { readonly electronApp: ElectronApplication - readonly mainWindow: PwPage + /** The project's own workbench window (each open project gets one). */ + readonly workbench: PwPage readonly selector: string readonly index: number constructor( electronApp: ElectronApplication, - mainWindow: PwPage, + workbench: PwPage, selector: string, index: number, ) { this.electronApp = electronApp - this.mainWindow = mainWindow + this.workbench = workbench this.selector = selector this.index = index } @@ -238,7 +239,7 @@ export class Element { ) if (!exists) return null const combinedSelector = `${this.selector} ${childSelector}` - return new Element(this.electronApp, this.mainWindow, combinedSelector, 0) + return new Element(this.electronApp, this.workbench, combinedSelector, 0) } /** Find all child elements matching a selector. */ @@ -254,7 +255,7 @@ export class Element { const combinedSelector = `${this.selector} ${childSelector}` const elements: Element[] = [] for (let i = 0; i < count; i++) { - elements.push(new Element(this.electronApp, this.mainWindow, combinedSelector, i)) + elements.push(new Element(this.electronApp, this.workbench, combinedSelector, i)) } return elements } diff --git a/packages/devtools/e2e/automator/mini-program.ts b/packages/devtools/e2e/automator/mini-program.ts index d8eb9525..7e05caaf 100644 --- a/packages/devtools/e2e/automator/mini-program.ts +++ b/packages/devtools/e2e/automator/mini-program.ts @@ -20,16 +20,17 @@ import { export class MiniProgram { readonly electronApp: ElectronApplication - readonly mainWindow: PwPage + /** The project's own workbench window (each open project gets one). */ + readonly workbench: PwPage readonly projectPath: string constructor( electronApp: ElectronApplication, - mainWindow: PwPage, + workbench: PwPage, projectPath: string, ) { this.electronApp = electronApp - this.mainWindow = mainWindow + this.workbench = workbench this.projectPath = projectPath } @@ -51,7 +52,7 @@ export class MiniProgram { /** Get the current active page. */ async currentPage(): Promise { const pagePath = await this.currentPagePath() - return new Page(this.electronApp, this.mainWindow, pagePath) + return new Page(this.electronApp, this.workbench, pagePath) } /** @@ -179,7 +180,7 @@ export class MiniProgram { ) // Final settle for late wx:for / setData renders inside the new page - await this.mainWindow.waitForTimeout(500) + await this.workbench.waitForTimeout(500) return this.currentPage() } @@ -215,7 +216,7 @@ export class MiniProgram { ) // Wait for content to render - await this.mainWindow.waitForTimeout(2000) + await this.workbench.waitForTimeout(2000) return this.currentPage() } @@ -227,7 +228,7 @@ export class MiniProgram { /** Navigate back by going to history.back(). */ async navigateBack(): Promise { await evalInSimulator(this.electronApp, `history.back()`) - await this.mainWindow.waitForTimeout(1500) + await this.workbench.waitForTimeout(1500) return this.currentPage() } @@ -325,7 +326,7 @@ export class MiniProgram { /** Invoke a devtools IPC handler from the renderer process. */ async ipcInvoke(channel: string, ...args: unknown[]): Promise { - return ipcInvoke(this.mainWindow, channel, ...args) + return ipcInvoke(this.workbench, channel, ...args) } // ── Screenshot ────────────────────────────────────────────────────── @@ -346,12 +347,12 @@ export class MiniProgram { /** Wait for a specific amount of time. */ async waitFor(ms: number): Promise { - await this.mainWindow.waitForTimeout(ms) + await this.workbench.waitForTimeout(ms) } /** Close the mini program and the devtools app. */ async close(): Promise { - await closeProject(this.mainWindow).catch(() => {}) + await closeProject(this.electronApp, { projectDir: this.projectPath }).catch(() => {}) await this.electronApp.close().catch(() => {}) } } diff --git a/packages/devtools/e2e/automator/page.ts b/packages/devtools/e2e/automator/page.ts index e58f18e0..8c2bbc05 100644 --- a/packages/devtools/e2e/automator/page.ts +++ b/packages/devtools/e2e/automator/page.ts @@ -30,16 +30,17 @@ function inIframe(expression: string): string { export class Page { readonly electronApp: ElectronApplication - readonly mainWindow: PwPage + /** The project's own workbench window (each open project gets one). */ + readonly workbench: PwPage readonly path: string constructor( electronApp: ElectronApplication, - mainWindow: PwPage, + workbench: PwPage, path: string, ) { this.electronApp = electronApp - this.mainWindow = mainWindow + this.workbench = workbench this.path = path } @@ -56,7 +57,7 @@ export class Page { inIframe(`return _doc.querySelector('${escaped}') !== null`), ) if (!exists) return null - return new Element(this.electronApp, this.mainWindow, selector, 0) + return new Element(this.electronApp, this.workbench, selector, 0) } /** @@ -70,7 +71,7 @@ export class Page { ) const elements: Element[] = [] for (let i = 0; i < count; i++) { - elements.push(new Element(this.electronApp, this.mainWindow, selector, i)) + elements.push(new Element(this.electronApp, this.workbench, selector, i)) } return elements } @@ -129,7 +130,7 @@ export class Page { async waitFor(predicate: () => Promise): Promise async waitFor(arg: number | string | (() => Promise)): Promise { if (typeof arg === 'number') { - await this.mainWindow.waitForTimeout(arg) + await this.workbench.waitForTimeout(arg) } else if (typeof arg === 'string') { await this.waitForSelector(arg) } else { @@ -149,7 +150,7 @@ export class Page { timeout, 300, ) - return new Element(this.electronApp, this.mainWindow, selector, 0) + return new Element(this.electronApp, this.workbench, selector, 0) } // ── Evaluate in iframe ────────────────────────────────────────────── diff --git a/packages/devtools/e2e/broken-project.spec.ts b/packages/devtools/e2e/broken-project.spec.ts index d70a8668..db6ba96d 100644 --- a/packages/devtools/e2e/broken-project.spec.ts +++ b/packages/devtools/e2e/broken-project.spec.ts @@ -2,7 +2,7 @@ import fs from 'fs' import os from 'os' import path from 'path' import { test, expect } from './fixtures' -import { ipcInvoke, closeProject, DEMO_APP_DIR } from './helpers' +import { ipcInvoke, DEMO_APP_DIR } from './helpers' import { ProjectChannel } from '../src/shared/ipc-channels' interface OpenProjectResult { @@ -123,7 +123,11 @@ test.describe('devkit dev server does not serve HTML for missing asset paths', ( `server returned HTML SPA fallback (status=${res.status}) for a JSON asset path; first 80 chars: ${JSON.stringify(body.slice(0, 80))}`, ).toBe(false) } finally { - await closeProject(mainWindow) + // These tests drive `ProjectChannel.Open` directly, so the session lives + // in the list window's own WorkbenchContext (no real workbench window is + // ever spawned) — close it via the same channel rather than + // `closeProject`, which only tears down real workbench BrowserWindows. + await ipcInvoke(mainWindow, ProjectChannel.Close).catch(() => {}) } }) @@ -143,7 +147,11 @@ test.describe('devkit dev server does not serve HTML for missing asset paths', ( const looksLikeHtml = /^\s* {}) } }) }) diff --git a/packages/devtools/e2e/console-filter-live.spec.ts b/packages/devtools/e2e/console-filter-live.spec.ts index e395c04a..78e52696 100644 --- a/packages/devtools/e2e/console-filter-live.spec.ts +++ b/packages/devtools/e2e/console-filter-live.spec.ts @@ -243,7 +243,10 @@ test.describe('Right-panel Console [service] de-noise filter (live)', () => { 10000, 100, ) - await openProjectInUI(mainWindow, FIXTURE_DIR, { waitMs: 20000 }) + // No workbench Page is kept: every project-scoped assertion below reaches + // the right-panel front-end / service host through electronApp.evaluate + // by webContents URL, never through a Playwright Page. + await openProjectInUI(electronApp, FIXTURE_DIR, { waitMs: 20000 }) await waitForSimulatorWebview(electronApp) await pollUntil( () => evalInSimulator(electronApp, `(() => !!document.querySelector('.device-shell-root'))()`).catch(() => false), @@ -279,7 +282,7 @@ test.describe('Right-panel Console [service] de-noise filter (live)', () => { }) test.afterAll(async () => { - await closeProject(mainWindow).catch(() => {}) + await closeProject(electronApp).catch(() => {}) await electronApp?.close().catch(() => {}) }) diff --git a/packages/devtools/e2e/console-filter-reset.spec.ts b/packages/devtools/e2e/console-filter-reset.spec.ts index 1a6826d3..4ba9dd57 100644 --- a/packages/devtools/e2e/console-filter-reset.spec.ts +++ b/packages/devtools/e2e/console-filter-reset.spec.ts @@ -52,9 +52,9 @@ test.describe('Console filter box is cleared on fresh project open', () => { useSharedProject(test, DEMO_APP_DIR) - test('stale console.textFilter is removed and the box is emptied', async ({ mainWindow, electronApp }) => { + test('stale console.textFilter is removed and the box is emptied', async ({ workbench, electronApp }) => { // Open the Console panel so the embedded Chrome DevTools front-end is mounted. - const consoleTab = mainWindow.getByRole('tab', { name: 'Console' }) + const consoleTab = workbench.getByRole('tab', { name: 'Console' }) await consoleTab.click() await expect(consoleTab).toHaveAttribute('data-active', 'true') diff --git a/packages/devtools/e2e/devtools-panel.spec.ts b/packages/devtools/e2e/devtools-panel.spec.ts index fc3deb78..bd2db596 100644 --- a/packages/devtools/e2e/devtools-panel.spec.ts +++ b/packages/devtools/e2e/devtools-panel.spec.ts @@ -9,14 +9,14 @@ test.describe('Simulator Panel', () => { useSharedProject(test, DEMO_APP_DIR) - test('toolbar has compile and simulator toggle buttons', async ({ mainWindow }) => { - expect(await findButtonByText(mainWindow, '普通编译')).toBe(true) - await expect(mainWindow.getByRole('group', { name: '面板可见性' })).toBeVisible() - await expect(mainWindow.getByTestId('layout-toolbar-toggle-simulator')).toBeVisible() + test('toolbar has compile and simulator toggle buttons', async ({ workbench }) => { + expect(await findButtonByText(workbench, '普通编译')).toBe(true) + await expect(workbench.getByRole('group', { name: '面板可见性' })).toBeVisible() + await expect(workbench.getByTestId('layout-toolbar-toggle-simulator')).toBeVisible() }) - test('toolbar has built-in right panel tabs', async ({ mainWindow }) => { - const tabLabels = await mainWindow.evaluate(() => { + test('toolbar has built-in right panel tabs', async ({ workbench }) => { + const tabLabels = await workbench.evaluate(() => { const buttons = document.querySelectorAll('button') const labels: string[] = [] buttons.forEach((btn) => { @@ -31,22 +31,22 @@ test.describe('Simulator Panel', () => { expect(tabLabels).toEqual(expect.arrayContaining(['WXML', 'AppData', 'Storage'])) }) - test('can toggle simulator panel visibility', async ({ mainWindow }) => { + test('can toggle simulator panel visibility', async ({ workbench }) => { // Under native-host (now the default runtime) the simulator is a // main-process WebContentsView, NOT a renderer `` — SimulatorPanel // deliberately skips the `` (Electron forbids nesting webviews, so // DeviceShell's per-page render-host webviews can only attach to a top-level - // WCV). So `mainWindow.locator('webview')` is 0 in BOTH states and can't + // WCV). So `workbench.locator('webview')` is 0 in BOTH states and can't // gate visibility. // - // The observable visibility signal in the main-window DOM is the + // The observable visibility signal in the workbench DOM is the // SimulatorPanel itself: its device-picker `` // carrying the device options, e.g. `iPhone SE`) mounts when the simulator // cell is in the compiled layout and unmounts when the cell is pruned. The // toolbar toggle flips `layoutStore.simulatorVisible`, which the layout // compile pass turns into the cell being present/absent (collapseInvisibleCells). - const deviceSelect = mainWindow.locator('select:has(option[value="iPhone SE"])') - const toggle = mainWindow.getByTestId('layout-toolbar-toggle-simulator') + const deviceSelect = workbench.locator('select:has(option[value="iPhone SE"])') + const toggle = workbench.getByTestId('layout-toolbar-toggle-simulator') await expect(deviceSelect).toHaveCount(1) @@ -57,8 +57,8 @@ test.describe('Simulator Panel', () => { await expect(deviceSelect).toHaveCount(1) }) - test('right panel tabs are rendered in the main window', async ({ mainWindow }) => { - const tabLabels = await mainWindow.evaluate(() => { + test('right panel tabs are rendered in the workbench window', async ({ workbench }) => { + const tabLabels = await workbench.evaluate(() => { const buttons = document.querySelectorAll('button') const labels: string[] = [] buttons.forEach((btn) => { diff --git a/packages/devtools/e2e/devtools-tab-order.spec.ts b/packages/devtools/e2e/devtools-tab-order.spec.ts index 050521ff..520763a5 100644 --- a/packages/devtools/e2e/devtools-tab-order.spec.ts +++ b/packages/devtools/e2e/devtools-tab-order.spec.ts @@ -38,9 +38,9 @@ test.describe('Right-panel DevTools tab bar (default order, Sources kept)', () = useSharedProject(test, DEMO_APP_DIR) - test('Elements / Console / Sources / Network all visible; Network click selects Network', async ({ mainWindow, electronApp }) => { + test('Elements / Console / Sources / Network all visible; Network click selects Network', async ({ workbench, electronApp }) => { // Open the Console panel so the embedded Chrome DevTools front-end is mounted. - const consoleTab = mainWindow.getByRole('tab', { name: 'Console' }) + const consoleTab = workbench.getByRole('tab', { name: 'Console' }) await consoleTab.click() await expect(consoleTab).toHaveAttribute('data-active', 'true') diff --git a/packages/devtools/e2e/dialog-zorder.spec.ts b/packages/devtools/e2e/dialog-zorder.spec.ts index 3a3f7531..dcd483fe 100644 --- a/packages/devtools/e2e/dialog-zorder.spec.ts +++ b/packages/devtools/e2e/dialog-zorder.spec.ts @@ -1,7 +1,7 @@ import { test, expect, _electron, type ElectronApplication, type Page } from '@playwright/test' import path from 'path' import { fileURLToPath } from 'url' -import { DEMO_APP_DIR, openProjectInUI, pollUntil, findMainWindow } from './helpers' +import { DEMO_APP_DIR, openProjectInUI, pollUntil } from './helpers' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const TOOLBAR_FIXTURES = path.resolve(__dirname, 'fixtures', 'host-toolbar') @@ -24,7 +24,30 @@ test.describe('Dialog overlay z-order (real Electron): update dialog stays above test.describe.configure({ mode: 'serial' }) let electronApp: ElectronApplication - let mainWindow: Page + let workbench: Page + + /** + * `instance.context` (dialog-zorder-entry.js's `onSetup`) is the + * PROJECT-LIST window's own context — the simulator/host-toolbar this spec + * checks z-order against only exist on an OPEN project's own window (the + * list window never runs a mini-app), so every check below reaches the + * workbench window's context through `instance.projectWindows()`, not + * `instance.context`. + */ + interface DialogZorderInstance { + projectWindows(): Array<{ + context: { + views: { + hostToolbar: { loadFile(p: string): Promise } + getSimulatorWebContentsId(): number | null + getHostToolbarWebContentsId(): number | null + showUpdateDialog(info: { version: string; downloadUrl: string }): void + getUpdateDialogWebContentsId(): number | null + markOverlayReady(id: number): void + } + } + }> + } test.beforeAll(async () => { const entryPath = path.resolve(__dirname, 'dialog-zorder-entry.js') @@ -32,25 +55,19 @@ test.describe('Dialog overlay z-order (real Electron): update dialog stays above args: [entryPath], env: { ...process.env, NODE_ENV: 'test' }, }) - mainWindow = await findMainWindow(electronApp) - await mainWindow.waitForLoadState('domcontentloaded') - await openProjectInUI(mainWindow, DEMO_APP_DIR, { waitMs: 20_000 }) + workbench = await openProjectInUI(electronApp, DEMO_APP_DIR, { waitMs: 20_000 }) // Give the host-toolbar a live, sized strip so it actually attaches to // the window's contentView — the occlusion bug required an ACTUAL native // view mounted above the main window's renderer, not just a placeholder // waiting for content. await electronApp.evaluate((_electronMods, file) => { - const g = globalThis as unknown as { - __e2eDialogZorderInstance: { - context: { views: { hostToolbar: { loadFile(p: string): Promise } } } - } - } - return g.__e2eDialogZorderInstance.context.views.hostToolbar.loadFile(file) + const g = globalThis as unknown as { __e2eDialogZorderInstance: DialogZorderInstance } + return g.__e2eDialogZorderInstance.projectWindows()[0].context.views.hostToolbar.loadFile(file) }, path.join(TOOLBAR_FIXTURES, 'toolbar-64.html')) await pollUntil( - () => mainWindow.evaluate(() => { + () => workbench.evaluate(() => { const el = document.querySelector('[data-area="host-toolbar"]') return el ? Math.round(el.getBoundingClientRect().height) : -1 }), @@ -69,34 +86,20 @@ test.describe('Dialog overlay z-order (real Electron): update dialog stays above test('showUpdateDialog attaches its WCV above the live simulator and host-toolbar WCVs', async () => { const simulatorWcId = await electronApp.evaluate(() => { - const g = globalThis as unknown as { - __e2eDialogZorderInstance: { context: { views: { getSimulatorWebContentsId(): number | null } } } - } - return g.__e2eDialogZorderInstance.context.views.getSimulatorWebContentsId() + const g = globalThis as unknown as { __e2eDialogZorderInstance: DialogZorderInstance } + return g.__e2eDialogZorderInstance.projectWindows()[0].context.views.getSimulatorWebContentsId() }) expect(simulatorWcId, 'simulator WCV must be live before the z-order check is meaningful').not.toBeNull() const toolbarWcId = await electronApp.evaluate(() => { - const g = globalThis as unknown as { - __e2eDialogZorderInstance: { context: { views: { getHostToolbarWebContentsId(): number | null } } } - } - return g.__e2eDialogZorderInstance.context.views.getHostToolbarWebContentsId() + const g = globalThis as unknown as { __e2eDialogZorderInstance: DialogZorderInstance } + return g.__e2eDialogZorderInstance.projectWindows()[0].context.views.getHostToolbarWebContentsId() }) expect(toolbarWcId, 'host-toolbar WCV must be live before the z-order check is meaningful').not.toBeNull() const dialogWcId = await electronApp.evaluate(() => { - const g = globalThis as unknown as { - __e2eDialogZorderInstance: { - context: { - views: { - showUpdateDialog(info: { version: string; downloadUrl: string }): void - getUpdateDialogWebContentsId(): number | null - markOverlayReady(id: number): void - } - } - } - } - const views = g.__e2eDialogZorderInstance.context.views + const g = globalThis as unknown as { __e2eDialogZorderInstance: DialogZorderInstance } + const views = g.__e2eDialogZorderInstance.projectWindows()[0].context.views views.showUpdateDialog({ version: '2.0.0', downloadUrl: 'https://example.com/2.0.0.dmg' }) const id = views.getUpdateDialogWebContentsId() if (id !== null) views.markOverlayReady(id) diff --git a/packages/devtools/e2e/disk-sync.spec.ts b/packages/devtools/e2e/disk-sync.spec.ts index 7dcaa485..b0e3cc49 100644 --- a/packages/devtools/e2e/disk-sync.spec.ts +++ b/packages/devtools/e2e/disk-sync.spec.ts @@ -72,9 +72,9 @@ test.describe('fs-core disk↔editor sync (embedded workbench)', () => { // the first test pays the attach+ready wait. useSharedProject(test, DEMO_APP_DIR, { openOptions: { waitMs: 60_000 }, openTimeoutMs: 120_000 }) let workbenchReady = false - test.beforeEach(async ({ mainWindow, electronApp }) => { + test.beforeEach(async ({ workbench, electronApp }) => { if (workbenchReady) return - const status = await attachWorkbenchAndWaitReady(mainWindow, electronApp) + const status = await attachWorkbenchAndWaitReady(workbench, electronApp) expect(status, 'workbench must reach a ready status before driving the sync engine').toMatch( /workbench-ready|exthost-alive/, ) diff --git a/packages/devtools/e2e/dock-consolidation-smoke.spec.ts b/packages/devtools/e2e/dock-consolidation-smoke.spec.ts index 49426849..b8ef54b1 100644 --- a/packages/devtools/e2e/dock-consolidation-smoke.spec.ts +++ b/packages/devtools/e2e/dock-consolidation-smoke.spec.ts @@ -11,12 +11,12 @@ import { test, expect, _electron, type ElectronApplication, type Page as PwPage } from '@playwright/test' import path from 'path' import { fileURLToPath } from 'url' -import { openProjectInUI, closeProject, DEMO_APP_DIR, findMainWindow } from './helpers' +import { openProjectInUI, closeProject, DEMO_APP_DIR } from './helpers' const __dirname = path.dirname(fileURLToPath(import.meta.url)) let electronApp: ElectronApplication -let mainWindow: PwPage +let workbench: PwPage test.beforeAll(async () => { const appPath = path.resolve(__dirname, 'electron-entry.js') @@ -29,30 +29,28 @@ test.beforeAll(async () => { args: [appPath, `--user-data-dir=${userDataDir}`], env: { ...process.env, NODE_ENV: 'test' }, }) - mainWindow = await findMainWindow(electronApp) - await mainWindow.waitForLoadState('domcontentloaded') // Offscreen + blur so the smoke never steals focus. await electronApp.evaluate(async ({ BrowserWindow }) => { const win = BrowserWindow.getAllWindows()[0] if (win) { win.setPosition(-2000, -2000); win.blur() } }) - await openProjectInUI(mainWindow, DEMO_APP_DIR) + workbench = await openProjectInUI(electronApp, DEMO_APP_DIR) }) test.afterAll(async () => { - try { await closeProject(mainWindow) } catch { /* best effort */ } + try { await closeProject(electronApp) } catch { /* best effort */ } await electronApp.close() }) test('the sole dock layout renders (no flag, no FrameTree)', async () => { // DockView groups exist; the legacy FrameTree sim splitter does NOT. - await mainWindow.waitForSelector('[data-deck-group]', { timeout: 15000 }) - const groupCount = await mainWindow.locator('[data-deck-group]').count() + await workbench.waitForSelector('[data-deck-group]', { timeout: 15000 }) + const groupCount = await workbench.locator('[data-deck-group]').count() expect(groupCount, 'at least one dock group must render').toBeGreaterThanOrEqual(1) // No legacy FrameTree markers (the old path is deleted). - expect(await mainWindow.locator('[data-splitter="sim"]').count()).toBe(0) - expect(await mainWindow.locator('[data-area="native-simulator"]').count()).toBeGreaterThanOrEqual(0) + expect(await workbench.locator('[data-splitter="sim"]').count()).toBe(0) + expect(await workbench.locator('[data-area="native-simulator"]').count()).toBeGreaterThanOrEqual(0) }) test('the five debug tabs surface as deck tabs; simulator + editor are tabless structural panels', async () => { @@ -61,7 +59,7 @@ test('the five debug tabs surface as deck tabs; simulator + editor are tabless s // STRUCTURAL panels (`hideTab:true`): they own their own region and draw their // own chrome, so the dock renders NO tab for them — they are present as // tabless `[data-deck-panel-body]` bodies instead. - const tabIds = await mainWindow.evaluate(() => + const tabIds = await workbench.evaluate(() => Array.from(document.querySelectorAll('[data-deck-tab]')).map( (el) => el.getAttribute('data-deck-tab'), ), @@ -71,7 +69,7 @@ test('the five debug tabs surface as deck tabs; simulator + editor are tabless s } for (const id of ['simulator', 'editor']) { expect(tabIds, `structural panel '${id}' must NOT render a deck tab (hideTab)`).not.toContain(id) - const bodyCount = await mainWindow.locator(`[data-deck-panel-body="${id}"]`).count() + const bodyCount = await workbench.locator(`[data-deck-panel-body="${id}"]`).count() expect(bodyCount, `structural panel '${id}' must be mounted as a tabless dock body`).toBeGreaterThanOrEqual(1) } }) @@ -82,15 +80,15 @@ test('simulator chrome (device picker + page-path bar) renders in the dock — n // `[data-area="native-simulator"]` (the WCV anchor) AND the chrome around it. // The simulator panel is the active leaf in the default tree, so its body is // mounted. - await mainWindow.waitForSelector('[data-deck-panel-body="simulator"]', { timeout: 10000 }) - const region = mainWindow.locator('[data-area="native-simulator"]') + await workbench.waitForSelector('[data-deck-panel-body="simulator"]', { timeout: 10000 }) + const region = workbench.locator('[data-area="native-simulator"]') expect(await region.count(), 'the simulator WCV anchor region must render').toBeGreaterThanOrEqual(1) }) test('the simulator native WCV follows its slot (non-zero live bounds)', async () => { // The simulator slot must publish a non-zero rect to main, and the simulator // WebContentsView must be live (it loads simulator.html). - const slotRect = await mainWindow.evaluate(() => { + const slotRect = await workbench.evaluate(() => { const el = document.querySelector('[data-area="native-simulator"]') if (!el) return null const r = el.getBoundingClientRect() @@ -114,16 +112,16 @@ test('the simulator native WCV follows its slot (non-zero live bounds)', async ( test('switching a debug tab keeps the dock alive and mounts the new body (data refresh seam)', async () => { // Activate the WXML tab, then the Storage tab. Each activation mounts that // panel body (DockDebugTab fires the per-tab refresh on activation — M3). - const wxmlTab = mainWindow.locator('[data-deck-tab="wxml"]').first() + const wxmlTab = workbench.locator('[data-deck-tab="wxml"]').first() await wxmlTab.click() - await mainWindow.waitForSelector('[data-deck-panel-body="wxml"]', { timeout: 8000 }) + await workbench.waitForSelector('[data-deck-panel-body="wxml"]', { timeout: 8000 }) - const storageTab = mainWindow.locator('[data-deck-tab="storage"]').first() + const storageTab = workbench.locator('[data-deck-tab="storage"]').first() await storageTab.click() - await mainWindow.waitForSelector('[data-deck-panel-body="storage"]', { timeout: 8000 }) + await workbench.waitForSelector('[data-deck-panel-body="storage"]', { timeout: 8000 }) // The dock is still mounted after the switches. - expect(await mainWindow.locator('[data-deck-group]').count()).toBeGreaterThanOrEqual(1) + expect(await workbench.locator('[data-deck-group]').count()).toBeGreaterThanOrEqual(1) }) test('changing the device re-pins the simulator width live (device-width fidelity)', async () => { @@ -131,14 +129,14 @@ test('changing the device re-pins the simulator width live (device-width fidelit // width only at mount, so DockableLayout re-pins via setConstraint on a device // change. Switch the device inside the simulator body. - const changed = await mainWindow.evaluate(() => { + const changed = await workbench.evaluate(() => { const body = document.querySelector('[data-deck-panel-body="simulator"]') const select = body?.querySelector('select') as HTMLSelectElement | null if (!select || select.options.length < 2) return null @@ -153,8 +151,8 @@ test('changing the device re-pins the simulator width live (device-width fidelit // The region width must settle to a (different) positive value — the constraint // re-pin flowed through. We assert it stays positive and finite; an exact px // match depends on device metadata, so we assert it changed OR stayed valid. - await mainWindow.waitForTimeout(500) - const after = await mainWindow.evaluate(() => { + await workbench.waitForTimeout(500) + const after = await workbench.evaluate(() => { const el = document.querySelector('[data-area="native-simulator"]') return el ? el.getBoundingClientRect().width : 0 }) @@ -169,7 +167,7 @@ test('the drop seam rejects re-docking a structural panel — no-op, no tree mut // within-group tab reordering is covered by dock-tab-reorder.spec.ts.) // Snapshot editor's enclosing group id + the total group count BEFORE the drop. - const before = await mainWindow.evaluate(() => ({ + const before = await workbench.evaluate(() => ({ editorGroupId: document .querySelector('[data-deck-panel-body="editor"]') ?.closest('[data-deck-group]') @@ -178,7 +176,7 @@ test('the drop seam rejects re-docking a structural panel — no-op, no tree mut })) expect(before.editorGroupId, 'editor must start mounted inside a dock group').not.toBeNull() - const seamReached = await mainWindow.evaluate(() => { + const seamReached = await workbench.evaluate(() => { const groups = Array.from(document.querySelectorAll('[data-deck-group]')) as Array< HTMLElement & { __deckHandleDrop?: (panelId: string, zone: string) => void } > @@ -188,11 +186,11 @@ test('the drop seam rejects re-docking a structural panel — no-op, no tree mut return true }) expect(seamReached, 'the __deckHandleDrop seam must be reachable on a group').toBe(true) - await mainWindow.waitForSelector('[data-deck-group]', { timeout: 5000 }) + await workbench.waitForSelector('[data-deck-group]', { timeout: 5000 }) // The rejected drop changed NOTHING: editor sits in the same group and the // group count is unchanged (no split spawned a new group). - const after = await mainWindow.evaluate(() => ({ + const after = await workbench.evaluate(() => ({ editorGroupId: document .querySelector('[data-deck-panel-body="editor"]') ?.closest('[data-deck-group]') diff --git a/packages/devtools/e2e/dock-devtools-position-preset.spec.ts b/packages/devtools/e2e/dock-devtools-position-preset.spec.ts index 5010677a..1a93a3c5 100644 --- a/packages/devtools/e2e/dock-devtools-position-preset.spec.ts +++ b/packages/devtools/e2e/dock-devtools-position-preset.spec.ts @@ -42,12 +42,12 @@ import { } from '@playwright/test' import path from 'path' import { fileURLToPath } from 'url' -import { openProjectInUI, closeProject, DEMO_APP_DIR, findMainWindow } from './helpers' +import { openProjectInUI, closeProject, DEMO_APP_DIR } from './helpers' const __dirname = path.dirname(fileURLToPath(import.meta.url)) let electronApp: ElectronApplication -let mainWindow: PwPage +let workbench: PwPage test.beforeAll(async () => { const appPath = path.resolve(__dirname, 'electron-entry.js') @@ -61,18 +61,16 @@ test.beforeAll(async () => { args: [appPath, `--user-data-dir=${userDataDir}`], env: { ...process.env, NODE_ENV: 'test' }, }) - mainWindow = await findMainWindow(electronApp) - await mainWindow.waitForLoadState('domcontentloaded') await electronApp.evaluate(async ({ BrowserWindow }) => { const win = BrowserWindow.getAllWindows()[0] if (win) { win.setPosition(-2000, -2000); win.blur() } }) - await openProjectInUI(mainWindow, DEMO_APP_DIR) - await mainWindow.waitForSelector('[data-deck-group]', { timeout: 15000 }) + workbench = await openProjectInUI(electronApp, DEMO_APP_DIR) + await workbench.waitForSelector('[data-deck-group]', { timeout: 15000 }) }) test.afterAll(async () => { - try { await closeProject(mainWindow) } catch { /* best effort */ } + try { await closeProject(electronApp) } catch { /* best effort */ } await electronApp.close() }) @@ -97,10 +95,10 @@ async function editorWidth(page: PwPage): Promise { const MIN_HEALTHY_EDITOR_WIDTH = 50 test('[needs-real-electron] Bug #3: rightOfSimulator -> belowSimulator does not collapse the editor width', async () => { - await clickPreset(mainWindow, 'rightOfSimulator') - await clickPreset(mainWindow, 'belowSimulator') + await clickPreset(workbench, 'rightOfSimulator') + await clickPreset(workbench, 'belowSimulator') - const w = await editorWidth(mainWindow) + const w = await editorWidth(workbench) expect( w, `editor width after rightOfSimulator->belowSimulator must stay healthy (got ${w}px — a collapse to a few px is Bug #3)`, @@ -108,18 +106,18 @@ test('[needs-real-electron] Bug #3: rightOfSimulator -> belowSimulator does not }) test('[regress] inEditor -> belowSimulator keeps the editor visible (no root child-count change; must stay healthy)', async () => { - await clickPreset(mainWindow, 'inEditor') - await clickPreset(mainWindow, 'belowSimulator') + await clickPreset(workbench, 'inEditor') + await clickPreset(workbench, 'belowSimulator') - const w = await editorWidth(mainWindow) + const w = await editorWidth(workbench) expect(w).toBeGreaterThan(MIN_HEALTHY_EDITOR_WIDTH) }) test('[regress] inEditor <-> rightOfSimulator keeps the editor visible in both directions (neither pins a nested split)', async () => { - await clickPreset(mainWindow, 'inEditor') - await clickPreset(mainWindow, 'rightOfSimulator') - expect(await editorWidth(mainWindow)).toBeGreaterThan(MIN_HEALTHY_EDITOR_WIDTH) + await clickPreset(workbench, 'inEditor') + await clickPreset(workbench, 'rightOfSimulator') + expect(await editorWidth(workbench)).toBeGreaterThan(MIN_HEALTHY_EDITOR_WIDTH) - await clickPreset(mainWindow, 'inEditor') - expect(await editorWidth(mainWindow)).toBeGreaterThan(MIN_HEALTHY_EDITOR_WIDTH) + await clickPreset(workbench, 'inEditor') + expect(await editorWidth(workbench)).toBeGreaterThan(MIN_HEALTHY_EDITOR_WIDTH) }) diff --git a/packages/devtools/e2e/dock-keepalive-and-collapse.spec.ts b/packages/devtools/e2e/dock-keepalive-and-collapse.spec.ts index 3f1cb38b..73b01b84 100644 --- a/packages/devtools/e2e/dock-keepalive-and-collapse.spec.ts +++ b/packages/devtools/e2e/dock-keepalive-and-collapse.spec.ts @@ -32,12 +32,12 @@ import { } from '@playwright/test' import path from 'path' import { fileURLToPath } from 'url' -import { openProjectInUI, closeProject, DEMO_APP_DIR, findMainWindow } from './helpers' +import { openProjectInUI, closeProject, DEMO_APP_DIR } from './helpers' const __dirname = path.dirname(fileURLToPath(import.meta.url)) let electronApp: ElectronApplication -let mainWindow: PwPage +let workbench: PwPage test.beforeAll(async () => { const appPath = path.resolve(__dirname, 'electron-entry.js') @@ -51,19 +51,17 @@ test.beforeAll(async () => { args: [appPath, `--user-data-dir=${userDataDir}`], env: { ...process.env, NODE_ENV: 'test' }, }) - mainWindow = await findMainWindow(electronApp) - await mainWindow.waitForLoadState('domcontentloaded') // Offscreen + blur so the spec never steals focus. await electronApp.evaluate(async ({ BrowserWindow }) => { const win = BrowserWindow.getAllWindows()[0] if (win) { win.setPosition(-2000, -2000); win.blur() } }) - await openProjectInUI(mainWindow, DEMO_APP_DIR) - await mainWindow.waitForSelector('[data-deck-group]', { timeout: 15000 }) + workbench = await openProjectInUI(electronApp, DEMO_APP_DIR) + await workbench.waitForSelector('[data-deck-group]', { timeout: 15000 }) }) test.afterAll(async () => { - try { await closeProject(mainWindow) } catch { /* best effort */ } + try { await closeProject(electronApp) } catch { /* best effort */ } await electronApp.close() }) @@ -134,10 +132,10 @@ test('A3: a kept-alive debug body is NOT remounted across a tab switch (DOM iden // build a brand-new element and lose the imperatively-set attribute; (2) a // scrollTop on the first scrollable descendant. Both must survive A→B→A iff // the body was kept alive (display:none) rather than unmounted. - await activateTab(mainWindow, 'wxml') + await activateTab(workbench, 'wxml') const stamp = `keepalive-${Date.now()}` - const stamped = await mainWindow.evaluate((mark) => { + const stamped = await workbench.evaluate((mark) => { const body = document.querySelector('[data-deck-panel-body="wxml"]') as HTMLElement | null if (!body) return { ok: false, reason: 'no wxml body' } @@ -174,8 +172,8 @@ test('A3: a kept-alive debug body is NOT remounted across a tab switch (DOM iden // Switch AWAY to storage, then assert wxml stayed MOUNTED but hidden (the // keepalive contract: inactive body is display:none, NOT removed). - await activateTab(mainWindow, 'storage') - const wxmlHiddenButMounted = await mainWindow.evaluate(() => { + await activateTab(workbench, 'storage') + const wxmlHiddenButMounted = await workbench.evaluate(() => { const body = document.querySelector('[data-deck-panel-body="wxml"]') as HTMLElement | null if (!body) return { mounted: false, display: null as string | null } return { mounted: true, display: getComputedStyle(body).display } @@ -185,8 +183,8 @@ test('A3: a kept-alive debug body is NOT remounted across a tab switch (DOM iden // Switch BACK to wxml and assert the stamped element + scroll survived — i.e. // the body was the SAME instance, never remounted. - await activateTab(mainWindow, 'wxml') - const survived = await mainWindow.evaluate((mark) => { + await activateTab(workbench, 'wxml') + const survived = await workbench.evaluate((mark) => { const body = document.querySelector('[data-deck-panel-body="wxml"]') as HTMLElement | null if (!body) return { ok: false, markFound: false, scrollTop: null as number | null, hadScrollProbe: false } const marked = body.querySelector(`[data-e2e-keepalive-mark="${mark}"]`) @@ -225,10 +223,10 @@ test('B1: the simulator native WCV collapses (detaches, kept alive) when the sim // main maps the zero-area rect to COLLAPSE, `removeChildView`-ing the WCV from // the contentView tree (detach) while keeping its WebContents alive. So the // collapse is observed as `alive && bounds === null`. - const toggle = mainWindow.locator('[data-testid="layout-toolbar-toggle-simulator"]') + const toggle = workbench.locator('[data-testid="layout-toolbar-toggle-simulator"]') // Baseline: the simulator WCV is attached with live, non-zero bounds. - await mainWindow.waitForTimeout(500) + await workbench.waitForTimeout(500) const active = await simulatorView(electronApp) expect(active.alive, 'simulator WebContents must be alive while shown').toBe(true) expect(active.bounds, 'shown simulator WCV must be ATTACHED with live bounds').not.toBeNull() @@ -239,7 +237,7 @@ test('B1: the simulator native WCV collapses (detaches, kept alive) when the sim // Hide the simulator via the real toolbar toggle → detach-but-keep-alive. await toggle.click() - await mainWindow.waitForTimeout(800) + await workbench.waitForTimeout(800) const collapsed = await simulatorView(electronApp) expect( @@ -254,7 +252,7 @@ test('B1: the simulator native WCV collapses (detaches, kept alive) when the sim // Show the simulator again → the slot re-mounts → the anchor re-publishes a // non-zero rect → main re-attaches the WCV and restores its bounds. await toggle.click() - await mainWindow.waitForTimeout(800) + await workbench.waitForTimeout(800) const restored = await simulatorView(electronApp) expect(restored.alive, 'simulator WebContents must still be alive after being shown again').toBe(true) diff --git a/packages/devtools/e2e/dock-real-drag.spec.ts b/packages/devtools/e2e/dock-real-drag.spec.ts index 80a94a2c..df18be16 100644 --- a/packages/devtools/e2e/dock-real-drag.spec.ts +++ b/packages/devtools/e2e/dock-real-drag.spec.ts @@ -47,13 +47,12 @@ import { DEMO_APP_DIR, installConsoleCollector, readConsoleErrors, - findMainWindow, } from './helpers' const __dirname = path.dirname(fileURLToPath(import.meta.url)) let electronApp: ElectronApplication -let mainWindow: PwPage +let workbench: PwPage test.beforeAll(async () => { const appPath = path.resolve(__dirname, 'electron-entry.js') @@ -67,20 +66,18 @@ test.beforeAll(async () => { args: [appPath, `--user-data-dir=${userDataDir}`], env: { ...process.env, NODE_ENV: 'test' }, }) - mainWindow = await findMainWindow(electronApp) - await mainWindow.waitForLoadState('domcontentloaded') await installConsoleCollector(electronApp) // Offscreen + blur so the drag test never steals focus. await electronApp.evaluate(async ({ BrowserWindow }) => { const win = BrowserWindow.getAllWindows()[0] if (win) { win.setPosition(-2000, -2000); win.blur() } }) - await openProjectInUI(mainWindow, DEMO_APP_DIR) - await mainWindow.waitForSelector('[data-deck-group]', { timeout: 15000 }) + workbench = await openProjectInUI(electronApp, DEMO_APP_DIR) + await workbench.waitForSelector('[data-deck-group]', { timeout: 15000 }) }) test.afterAll(async () => { - try { await closeProject(mainWindow) } catch { /* best effort */ } + try { await closeProject(electronApp) } catch { /* best effort */ } await electronApp.close() }) @@ -282,14 +279,14 @@ test('CENTER over a locked editor group: indicator shows center, but a reorder-o // of wxml onto the editor group must be REJECTED (no churn), even though the // geometry-driven hover indicator still paints `center` (the gate is at drop // time, not hover time). Drives both PanelCapabilities gates with real geometry. - const before = await dockFingerprint(mainWindow) + const before = await dockFingerprint(workbench) const wxmlGroupBefore = before.groupOf['wxml'] const editorGroupBefore = before.groupOf['editor'] expect(wxmlGroupBefore, 'wxml must be docked before the drag').toBeTruthy() expect(editorGroupBefore, 'editor must be docked before the drag').toBeTruthy() expect(wxmlGroupBefore, 'wxml and editor start in DIFFERENT groups').not.toBe(editorGroupBefore) - const r = await realDragTab(mainWindow, 'wxml', 'editor', 0.5, 0.5) + const r = await realDragTab(workbench, 'wxml', 'editor', 0.5, 0.5) expect(r.error, `drag must not throw: ${r.error}`).toBeNull() expect(r.ok, 'drag sequence must run').toBe(true) // The live indicator paints `center` while hovering the interior (presentation @@ -297,8 +294,8 @@ test('CENTER over a locked editor group: indicator shows center, but a reorder-o expect(r.indicatorSeen, 'a drop-zone indicator must appear during dragover').toBe(true) expect(r.zoneAtHover, 'interior hover must compute the center zone').toBe('center') - await mainWindow.waitForTimeout(300) - const after = await dockFingerprint(mainWindow) + await workbench.waitForTimeout(300) + const after = await dockFingerprint(workbench) // The drop is rejected on BOTH gates: wxml stays in its own group, never joins // editor, and the overall group membership is unchanged. @@ -313,20 +310,20 @@ test('LEFT band over a locked editor group: indicator shows left, but a reorder- // draggable:false). So an edge (far-left band) drop of console onto the editor // group is REJECTED: no new split, console stays put. The hover indicator still // paints `left` (geometry-only presentation). - const before = await dockFingerprint(mainWindow) + const before = await dockFingerprint(workbench) const consoleGroupBefore = before.groupOf['console'] expect(consoleGroupBefore, 'console must be docked before the drag').toBeTruthy() const splitsBefore = before.splits.length // Drop console onto the far-left 5% band of the group that owns editor. - const r = await realDragTab(mainWindow, 'console', 'editor', 0.05, 0.5) + const r = await realDragTab(workbench, 'console', 'editor', 0.05, 0.5) expect(r.error, `drag must not throw: ${r.error}`).toBeNull() // The indicator paints `left` over the left band (presentation is geometry-only). expect(r.indicatorSeen, 'a drop-zone indicator must appear during dragover').toBe(true) expect(r.zoneAtHover, 'far-left hover must compute the left zone').toBe('left') - await mainWindow.waitForTimeout(300) - const after = await dockFingerprint(mainWindow) + await workbench.waitForTimeout(300) + const after = await dockFingerprint(workbench) // The edge drop is rejected: no split is introduced and console stays in its // original group (it never tears out toward the editor region). @@ -337,7 +334,7 @@ test('LEFT band over a locked editor group: indicator shows left, but a reorder- }) test('self-drop center of a reorder-only debug tab stays WITHIN its own group (never leaves, never crashes)', async () => { - const before = await dockFingerprint(mainWindow) + const before = await dockFingerprint(workbench) // Pick a real DRAGGABLE source: a debug tab (the source must own a // `[data-deck-tab]`). The structural simulator/editor panels are tabless // (hideTab) and draggable:false, so they can never be a drag source. @@ -349,11 +346,11 @@ test('self-drop center of a reorder-only debug tab stays WITHIN its own group (n // A center drop onto its OWN group is the one motion `reorder-only` permits — it // REORDERS within the group (it never leaves). The exact resulting index is // pointer-derived; the invariant is: same group, same membership set, no crash. - const r = await realDragTab(mainWindow, selfPanel, selfPanel, 0.5, 0.5) + const r = await realDragTab(workbench, selfPanel, selfPanel, 0.5, 0.5) expect(r.error, `self-drop must not throw: ${r.error}`).toBeNull() - await mainWindow.waitForTimeout(200) - const after = await dockFingerprint(mainWindow) + await workbench.waitForTimeout(200) + const after = await dockFingerprint(workbench) // The panel stays in its own group (reorder-only never tears out). expect(after.groupOf[selfPanel], 'self-center drop keeps the panel in its own group').toBe(selfGroupBefore) // Group membership (as a SET) is unchanged — only the within-group ORDER may shift. @@ -373,19 +370,19 @@ test('native anchor: the simulator (draggable:false) cannot be torn out — a dr // its live WCV bounds are unchanged. const beforeBounds = await simulatorBounds(electronApp) expect(beforeBounds, 'simulator WCV must have live bounds').not.toBeNull() - const before = await dockFingerprint(mainWindow) + const before = await dockFingerprint(workbench) const simGroupBefore = before.groupOf['simulator'] expect(simGroupBefore, 'simulator must be docked').toBeTruthy() // The simulator has no `[data-deck-tab]`, so realDragTab cannot pick it up — the // gesture fails to even start, which IS the contract (a draggable:false panel // can never be lifted). - const r = await realDragTab(mainWindow, 'simulator', 'editor', 0.5, 0.95) + const r = await realDragTab(workbench, 'simulator', 'editor', 0.5, 0.95) expect(r.ok, 'a draggable:false panel cannot start a drag (no source tab)').toBe(false) expect(r.error, 'the absent tab is the reason the drag never starts').toMatch(/no source tab simulator/) - await mainWindow.waitForTimeout(300) - const after = await dockFingerprint(mainWindow) + await workbench.waitForTimeout(300) + const after = await dockFingerprint(workbench) // The simulator stays in its group and the tree is unchanged. expect(after.groupOf['simulator'], 'simulator never leaves its group').toBe(simGroupBefore) expect(after.groupOf, 'a failed simulator drag must not churn the tree').toEqual(before.groupOf) diff --git a/packages/devtools/e2e/dock-resize-sync-regressions.spec.ts b/packages/devtools/e2e/dock-resize-sync-regressions.spec.ts index e5fa932d..94eca698 100644 --- a/packages/devtools/e2e/dock-resize-sync-regressions.spec.ts +++ b/packages/devtools/e2e/dock-resize-sync-regressions.spec.ts @@ -52,12 +52,12 @@ import { } from '@playwright/test' import path from 'path' import { fileURLToPath } from 'url' -import { openProjectInUI, closeProject, DEMO_APP_DIR, findMainWindow } from './helpers' +import { openProjectInUI, closeProject, DEMO_APP_DIR } from './helpers' const __dirname = path.dirname(fileURLToPath(import.meta.url)) let electronApp: ElectronApplication -let mainWindow: PwPage +let workbench: PwPage test.beforeAll(async () => { const appPath = path.resolve(__dirname, 'electron-entry.js') @@ -71,19 +71,17 @@ test.beforeAll(async () => { args: [appPath, `--user-data-dir=${userDataDir}`], env: { ...process.env, NODE_ENV: 'test' }, }) - mainWindow = await findMainWindow(electronApp) - await mainWindow.waitForLoadState('domcontentloaded') // Offscreen + blur so the spec never steals focus (real pointer drags below). await electronApp.evaluate(async ({ BrowserWindow }) => { const win = BrowserWindow.getAllWindows()[0] if (win) { win.setPosition(-2000, -2000); win.blur() } }) - await openProjectInUI(mainWindow, DEMO_APP_DIR) - await mainWindow.waitForSelector('[data-deck-group]', { timeout: 15000 }) + workbench = await openProjectInUI(electronApp, DEMO_APP_DIR) + await workbench.waitForSelector('[data-deck-group]', { timeout: 15000 }) }) test.afterAll(async () => { - try { await closeProject(mainWindow) } catch { /* best effort */ } + try { await closeProject(electronApp) } catch { /* best effort */ } await electronApp.close() }) @@ -185,10 +183,10 @@ function topShare(sizes: number[]): number { test('[needs-real-electron] R1: a sub-0.5% real splitter drag PERSISTS to the model (v3 normalizes it above FLEX_RATIO_TOLERANCE)', async () => { // col-main is the column split [editor | debug], both flexible. Reset to a // clean 50/50 so the small drag's flexible delta is unambiguous. - await applyLayout(mainWindow, 'col-main', [50, 50]) - await mainWindow.waitForTimeout(300) + await applyLayout(workbench, 'col-main', [50, 50]) + await workbench.waitForTimeout(300) - const before = await splitInfo(mainWindow, 'col-main') + const before = await splitInfo(workbench, 'col-main') expect(before, 'col-main must render with a resize handle').not.toBeNull() expect(before!.handle, 'col-main must expose a [data-deck-resize-handle]').not.toBeNull() expect(before!.axis).toBe('h') @@ -202,10 +200,10 @@ test('[needs-real-electron] R1: a sub-0.5% real splitter drag PERSISTS to the mo const dy = 3 // observed: container ~908px → 0.5% ≈ 4.5px, so 3px is sub-0.5% expect(dy, `the drag (${dy}px) must be under 0.5% of the container (${halfPctPx.toFixed(2)}px) to exercise the epsilon`).toBeLessThan(halfPctPx) - await dragHandle(mainWindow, 'col-main', [{ dx: 0, dy }]) - await mainWindow.waitForTimeout(400) + await dragHandle(workbench, 'col-main', [{ dx: 0, dy }]) + await workbench.waitForTimeout(400) - const after = await splitInfo(mainWindow, 'col-main') + const after = await splitInfo(workbench, 'col-main') // SANITY: the real pointer drag DID move the VISIBLE split — rrp committed the // small move (so this is a genuine completed user drag, not a no-op gesture). @@ -255,7 +253,7 @@ test('[needs-real-electron] R2: a fixed-px split does NOT corrupt the flexible c // is also 100 ⇒ 100==100 within `FLEX_RATIO_TOLERANCE` ⇒ the echo (and any // ratio-preserving spontaneous re-measure) is SKIPPED ⇒ the raw seed weight 1 // is never overwritten. - const root = await mainWindow.evaluate(() => { + const root = await workbench.evaluate(() => { const s = document.querySelector('[data-deck-split="dock-root"]') if (!s) return null return { @@ -311,18 +309,18 @@ test('[needs-real-electron] R3: a drag that returns to origin does NOT freeze la // (≈)its origin (net-zero change), but with no gate flag there is nothing left // armed — so a later programmatic `setSizes` syncs the visible split // immediately. - await applyLayout(mainWindow, 'col-main', [50, 50]) - await mainWindow.waitForTimeout(300) + await applyLayout(workbench, 'col-main', [50, 50]) + await workbench.waitForTimeout(300) - const before = await splitInfo(mainWindow, 'col-main') + const before = await splitInfo(workbench, 'col-main') expect(before!.handle, 'col-main must expose a handle').not.toBeNull() const beforeTop = before!.sizes[0]! // Real pointer drag AWAY (+80px) then BACK to the exact origin, then release. - await dragHandle(mainWindow, 'col-main', [{ dx: 0, dy: 80 }, { dx: 0, dy: 0 }]) - await mainWindow.waitForTimeout(400) + await dragHandle(workbench, 'col-main', [{ dx: 0, dy: 80 }, { dx: 0, dy: 0 }]) + await workbench.waitForTimeout(400) - const afterDrag = await splitInfo(mainWindow, 'col-main') + const afterDrag = await splitInfo(workbench, 'col-main') // The drag returned to origin: the visible split is back where it started. expect( Math.abs(afterDrag!.sizes[0]! - beforeTop), @@ -330,10 +328,10 @@ test('[needs-real-electron] R3: a drag that returns to origin does NOT freeze la ).toBeLessThanOrEqual(4) // Now drive a PROGRAMMATIC setSizes to a dramatically different ratio. - await applyLayout(mainWindow, 'col-main', [10, 90]) - await mainWindow.waitForTimeout(500) + await applyLayout(workbench, 'col-main', [10, 90]) + await workbench.waitForTimeout(500) - const afterProg = await splitInfo(mainWindow, 'col-main') + const afterProg = await splitInfo(workbench, 'col-main') // The model + mirror update regardless (setSizes mutates the model). expect(afterProg!.sizesAttr, 'the programmatic setSizes lands in the model').toBe('10,90') diff --git a/packages/devtools/e2e/dock-resize-sync-regressions2.spec.ts b/packages/devtools/e2e/dock-resize-sync-regressions2.spec.ts index e983f121..2fa34ab4 100644 --- a/packages/devtools/e2e/dock-resize-sync-regressions2.spec.ts +++ b/packages/devtools/e2e/dock-resize-sync-regressions2.spec.ts @@ -42,12 +42,12 @@ import { import path from 'path' import fs from 'fs' import { fileURLToPath } from 'url' -import { openProjectInUI, closeProject, DEMO_APP_DIR, findMainWindow } from './helpers' +import { openProjectInUI, closeProject, DEMO_APP_DIR } from './helpers' const __dirname = path.dirname(fileURLToPath(import.meta.url)) let electronApp: ElectronApplication -let mainWindow: PwPage +let workbench: PwPage const SPEC_USERDATA = 'dock-resize-sync-regressions2' @@ -66,19 +66,17 @@ test.beforeAll(async () => { args: [appPath, `--user-data-dir=${userDataDir}`], env: { ...process.env, NODE_ENV: 'test' }, }) - mainWindow = await findMainWindow(electronApp) - await mainWindow.waitForLoadState('domcontentloaded') // Offscreen + blur so the spec never steals focus. await electronApp.evaluate(async ({ BrowserWindow }) => { const win = BrowserWindow.getAllWindows()[0] if (win) { win.setPosition(-2000, -2000); win.blur() } }) - await openProjectInUI(mainWindow, DEMO_APP_DIR) - await mainWindow.waitForSelector('[data-deck-group]', { timeout: 15000 }) + workbench = await openProjectInUI(electronApp, DEMO_APP_DIR) + await workbench.waitForSelector('[data-deck-group]', { timeout: 15000 }) }) test.afterAll(async () => { - try { await closeProject(mainWindow) } catch { /* best effort */ } + try { await closeProject(electronApp) } catch { /* best effort */ } await electronApp.close() }) @@ -140,11 +138,11 @@ test('[needs-real-electron] R-KB: a keyboard (Arrow-key) resize persists to the // Reset to a clean, symmetric [50,50] via the programmatic seam so the start // state is deterministic and the keyboard move is unambiguous. - const reset = await applyLayout(mainWindow, split, [50, 50]) + const reset = await applyLayout(workbench, split, [50, 50]) expect(reset, 'the col-main write-back seam must be reachable to seed [50,50]').toBe(true) - await mainWindow.waitForTimeout(400) + await workbench.waitForTimeout(400) - const before = await panelSizes(mainWindow, split) + const before = await panelSizes(workbench, split) expect(before, `the ${split} split must render with two flexible panels`).not.toBeNull() expect(before!.axis).toBe('h') expect(before!.sizes.length, 'col-main must have exactly two stacked panels').toBe(2) @@ -158,7 +156,7 @@ test('[needs-real-electron] R-KB: a keyboard (Arrow-key) resize persists to the // SEPARATOR (rrp's listener reads `e.currentTarget`). For a vertical split // ArrowUp shrinks the top panel by 5% per press. We press several times so // the move is well above measurement noise. - const kb = await mainWindow.evaluate((id) => { + const kb = await workbench.evaluate((id) => { const splitEl = document.querySelector(`[data-deck-split="${id}"]`) if (!splitEl) return { ok: false, why: 'no split element', role: null, tabIndex: null } const handle = splitEl.querySelector('[data-deck-resize-handle]') as HTMLElement | null @@ -184,9 +182,9 @@ test('[needs-real-electron] R-KB: a keyboard (Arrow-key) resize persists to the expect(kb.role, 'rrp resize handle must be role="separator" (keyboard-operable)').toBe('separator') expect(kb.tabIndex, 'rrp resize handle must be focusable (tabIndex 0)').toBe(0) - await mainWindow.waitForTimeout(500) + await workbench.waitForTimeout(500) - const after = await panelSizes(mainWindow, split) + const after = await panelSizes(workbench, split) expect(after, 'col-main must still render after the keyboard resize').not.toBeNull() expect(after!.sizes.length).toBe(2) @@ -260,14 +258,14 @@ test.fixme( // whose flexible ratios are unchanged, so v3 SKIPS its write-back. const split = 'dock-root' - const rootBefore = await panelSizes(mainWindow, split) + const rootBefore = await panelSizes(workbench, split) expect(rootBefore, 'the root split must render').not.toBeNull() expect(rootBefore!.sizes.length, 'root split has two children (sim | main)').toBe(2) const weightsBefore = rootBefore!.sizesAttr expect(weightsBefore, 'root split must mirror its raw weights').toBeTruthy() // Locate the root split's DIRECT resize handle and its midpoint. - const handleBox = await mainWindow.evaluate(() => { + const handleBox = await workbench.evaluate(() => { const splitEl = document.querySelector('[data-deck-split="dock-root"]') if (!splitEl) return null const handle = splitEl.querySelector('[data-deck-resize-handle]') as HTMLElement | null @@ -283,19 +281,19 @@ test.fixme( // exactly what rrp listens to (pointerdown/move/up + setPointerCapture). const cx = handleBox!.x const cy = handleBox!.y - await mainWindow.mouse.move(cx, cy) - await mainWindow.mouse.down() - for (const dx of [15, 30, 45, 60]) await mainWindow.mouse.move(cx + dx, cy, { steps: 4 }) - await mainWindow.waitForTimeout(50) - for (const dx of [45, 30, 15, 0]) await mainWindow.mouse.move(cx + dx, cy, { steps: 4 }) - await mainWindow.mouse.up() - await mainWindow.waitForTimeout(300) + await workbench.mouse.move(cx, cy) + await workbench.mouse.down() + for (const dx of [15, 30, 45, 60]) await workbench.mouse.move(cx + dx, cy, { steps: 4 }) + await workbench.waitForTimeout(50) + for (const dx of [45, 30, 15, 0]) await workbench.mouse.move(cx + dx, cy, { steps: 4 }) + await workbench.mouse.up() + await workbench.waitForTimeout(300) - const weightsAfterDrag = (await panelSizes(mainWindow, split))!.sizesAttr + const weightsAfterDrag = (await panelSizes(workbench, split))!.sizesAttr // Trigger a SPONTANEOUS re-measure WITHOUT any user resize: change the device // so the fixed-px sim leaf re-pins → the root re-measures → `onLayoutChanged`. - const changed = await mainWindow.evaluate(() => { + const changed = await workbench.evaluate(() => { const body = document.querySelector('[data-deck-panel-body="simulator"]') const select = body?.querySelector('select') as HTMLSelectElement | null if (!select || select.options.length < 2) return null @@ -306,9 +304,9 @@ test.fixme( return { from: cur, to: next, label: select.options[next]!.textContent } }) expect(changed, 'the device