From 521e44c92c83d8746d763a357cf733e5f7dfe421 Mon Sep 17 00:00:00 2001 From: Kayshen-X Date: Sat, 20 Jun 2026 19:24:59 +0800 Subject: [PATCH] test(web): add rust web browser smoke --- .github/workflows/rust-multiplatform.yml | 17 +- .github/workflows/rust-release.yml | 40 ++-- .github/workflows/wasm-bundle-build.yml | 15 +- crates/op-host-services/src/web_static.rs | 44 ++++- .../src/web_static/index.html | 40 +--- crates/op-host-web/Cargo.toml | 32 ++- crates/op-host-web/smoke/step-1b.html | 72 ++++--- crates/op-host-web/src/dom_io.rs | 3 +- crates/op-host-web/src/file_actions.rs | 3 +- crates/op-host-web/src/iconify_web.rs | 3 +- crates/op-host-web/src/lib.rs | 4 +- crates/op-host-web/src/live_sync.rs | 4 +- crates/op-host-web/src/raf_pump.rs | 3 +- crates/op-host-web/src/repaint_ctx.rs | 12 +- crates/op-host-web/src/web_ai_transport.rs | 3 +- crates/op-host-web/src/web_chat.rs | 3 +- crates/op-host-web/src/web_clipboard.rs | 3 +- crates/op-host-web/src/web_fonts.rs | 3 +- crates/op-host-web/tests/ci_workflow.rs | 142 ++++++++++++++ package.json | 3 +- scripts/start-web-rust.sh | 40 ++++ tools/check-web-browser-smoke.sh | 184 ++++++++++++++++++ 22 files changed, 523 insertions(+), 150 deletions(-) create mode 100755 scripts/start-web-rust.sh create mode 100755 tools/check-web-browser-smoke.sh diff --git a/.github/workflows/rust-multiplatform.yml b/.github/workflows/rust-multiplatform.yml index 019051be5..5adfd28d1 100644 --- a/.github/workflows/rust-multiplatform.yml +++ b/.github/workflows/rust-multiplatform.yml @@ -130,19 +130,16 @@ jobs: - uses: Swatinem/rust-cache@v2 with: key: wasm32 - # Step 1b §1.2 wasm32-clean compile guard — without `skia` feature this - # is a stub mount (validates host canvas, does not paint). The real - # render artifact uses the SAME wasm32-unknown-unknown target plus - # `--features skia` (post C-hard.2 lock-in 2026-05-09) and ships - # via a separate Phase A job once the CI-side C-hard pipeline - # (brew emscripten install + EMSDK + .wasm.a symlink hack + - # wasm-bindgen + wasm-opt) lands. Until then this job only proves - # the public surface compiles wasm32-clean. + # Step 1b wasm32-clean compile guard. The default `web` feature is a + # stub mount that validates the canvas and does not paint; it stays here + # to prove the public surface remains wasm32-clean without the production + # bundle deps. The real deployable renderer is built in + # wasm-bundle-build.yml with the `canvaskit` feature and the + # tools/check-wasm-bundle.sh gate. - run: cargo build -p op-host-web --target wasm32-unknown-unknown --release --no-default-features --features web # Artifact upload deliberately removed: the stub `.wasm` from this # path would mislead downstream consumers into thinking the shell can - # render. Restore the upload step once the skia-enabled build runs on - # this CI target. + # render. The deployable artifact is uploaded by wasm-bundle-build.yml. mobile-check: name: ${{ matrix.label }} (cargo check only) diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index 780757ec2..7ddb8dd3e 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -58,10 +58,10 @@ name: Rust release artifacts # The `build` job builds the real `op-host-desktop` crate (winit + skia-safe # runner that drives the Rust editor); its shipped executable keeps the # stable `openpencil-desktop` name. The `op` CLI (crates/op-cli, binary name -# `op`) is built in the same cargo invocation. The wasm web bundle is -# currently DEFERRED (see comment block below); when the CI-side C-hard -# pipeline (emscripten install + EMSDK + symlink hack + wasm-bindgen + -# wasm-opt) lands it will re-enter the matrix. +# `op`) is built in the same cargo invocation. The Rust web bundle is built by +# `.github/workflows/wasm-bundle-build.yml` as a separate deployable CanvasKit +# artifact; this workflow currently keeps the native desktop release matrix +# independent from that web artifact. on: push: @@ -340,30 +340,20 @@ jobs: OpenPencil-*-linux.deb if-no-files-found: ignore - # Phase 1b release wasm artifact: explicitly DEFERRED (not silently - # dropped). The local C-hard pipeline (vendor/skia-safe-op fork + - # crates/wasm-libc-shim) does produce a runtime-loadable - # wasm32-unknown-unknown bundle today (verified 2026-05-09: 0 env.* - # imports, 597 KiB gzip). Re-introducing the release job requires - # CI-side automation that we have NOT yet built: - # 1. install brew emscripten 5.0.7 + EMSDK shim on ubuntu-latest - # 2. set EMSDK env var for the cargo build - # 3. run `cargo build … --features skia --release` - # 4. apply the `.wasm.a` → `.a` symlink hack in the - # skia-bindings out/ dir (manual today; tracked as a follow-up - # automation item — fold into `vendor/skia-safe-op` build.rs) - # 5. run `wasm-bindgen --target web` + `wasm-opt -Oz` - # 6. browser-side manual smoke (Phase E in spec) before publishing - # - # Until that pipeline lands, tagged releases ship desktop artifacts - # only. This is a CONSCIOUS choice — see plan §Cheat-sheet for the - # tracking item. + # Web bundle release note: + # The deployable Rust web bundle is produced by + # `.github/workflows/wasm-bundle-build.yml`, which builds the current + # CanvasKit feature set, runs `tools/check-wasm-bundle.sh`, size-gates the + # wasm output, and uploads the `op-web-bundle` artifact. This desktop release + # workflow still ships only native desktop + CLI artifacts; attach or merge the + # `op-web-bundle` artifact here only when tagged releases are meant to publish + # the standalone web bundle from the same draft. release-draft: name: Create / update GitHub Release draft - # `wasm` job deferred (see comment block above); when the - # CI-side C-hard pipeline lands, re-add the job and append its - # name to this `needs:` list. + # Native release artifacts only. The deployable Rust web bundle is built by + # wasm-bundle-build.yml and can be attached separately when release policy + # calls for publishing the web bundle from this draft. needs: [build] runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/v') diff --git a/.github/workflows/wasm-bundle-build.yml b/.github/workflows/wasm-bundle-build.yml index 72e5274e5..921dedad3 100644 --- a/.github/workflows/wasm-bundle-build.yml +++ b/.github/workflows/wasm-bundle-build.yml @@ -89,16 +89,22 @@ jobs: echo "Installing wasm-bindgen-cli $version" cargo install wasm-bindgen-cli --version "$version" --locked - - name: Install binaryen (wasm-opt) + - name: Install binaryen + headless server runtime deps run: | sudo apt-get update - sudo apt-get install -y binaryen + sudo apt-get install -y \ + binaryen \ + pkg-config \ + libfreetype-dev \ + libfontconfig1-dev \ + fonts-noto-cjk wasm-opt --version - - name: Verify node + gzip are present (script prerequisites) + - name: Verify node + gzip + Chrome are present (script prerequisites) run: | node --version gzip --version | head -n1 + google-chrome --version || chromium --version || chromium-browser --version # Single source of truth: run the local gate script verbatim. It performs # cargo build (canvaskit) -> wasm-bindgen --target web -> 0-env-import @@ -109,6 +115,9 @@ jobs: - name: Build + size-gate the canvaskit bundle run: bash tools/check-wasm-bundle.sh + - name: Browser smoke the canvaskit bundle + daemon + run: OPENPENCIL_SKIP_WASM_BUILD=1 bash tools/check-web-browser-smoke.sh + # Assemble the deployable layout the daemon's `web_static.rs` expects: the # wasm-bindgen `pkg/` output PLUS the vendored CanvasKit artifact under a # `canvaskit/` subdir, all inside a single `web-bundle/` directory. This diff --git a/crates/op-host-services/src/web_static.rs b/crates/op-host-services/src/web_static.rs index 8754f004e..b73cb5be8 100644 --- a/crates/op-host-services/src/web_static.rs +++ b/crates/op-host-services/src/web_static.rs @@ -4,8 +4,9 @@ //! - `GET /` (and `/index.html`) — the embedded host page //! (`web_static/index.html`, baked in via `include_str!` so serving works //! from any cwd). The page loads the wasm-bindgen JS glue from `/pkg/` -//! and calls `mount('op')`; the hidden IME textarea is created by -//! `mount()` itself, so the page only carries the ``. +//! and calls `mount_ck('op')`; the hidden IME textarea is created by the +//! CanvasKit mount itself, so the page only carries the ``. +//! - `GET /smoke/step-1b.html` — the CanvasKit-only browser smoke harness. //! - `GET /pkg/` — the wasm-bindgen output files from the resolved //! bundle directory, with correct MIME types (`application/wasm`, //! `text/javascript`). @@ -30,6 +31,10 @@ const INDEX_HTML: &str = include_str!("web_static/index.html"); /// 404 help page served when the wasm bundle cannot be found. const MISSING_BUNDLE_HTML: &str = include_str!("web_static/missing_bundle.html"); +/// CanvasKit-only browser smoke harness. Kept in the web crate next to the +/// bundle it exercises; embedded here so the daemon can serve it from any cwd. +const SMOKE_HTML: &str = include_str!("../../op-host-web/smoke/step-1b.html"); + /// The simple-icons brand-logo catalog (~4.8 MB). Embedded here so it can be /// both (a) registered with the shared icon catalog at native GUI startup and /// (b) served to the web client, which deliberately omits it from the wasm @@ -191,6 +196,13 @@ pub fn handle_static_request(path: &str, bundle_dir: Option<&Path>) -> Option missing_bundle_reply(), }); } + if path == "/smoke/step-1b.html" { + return Some(StaticReply { + status: "200 OK", + content_type: "text/html; charset=utf-8", + body: SMOKE_HTML.as_bytes().to_vec(), + }); + } if let Some(file) = path.strip_prefix("/pkg/") { // Serve flat bundle files AND nested ones: wasm-bindgen with a JS // `module = "..."` snippet emits `snippets//src/.js`, which @@ -416,10 +428,14 @@ mod tests { assert_eq!(reply.status, "200 OK"); assert_eq!(reply.content_type, "text/html; charset=utf-8"); let body = String::from_utf8(reply.body).expect("utf8"); - // The host page loads the glue and mounts on the canvas. + // The host page loads the glue, resets through the daemon, and mounts + // the production CanvasKit shell on the canvas. assert!(body.contains("/pkg/op_host_web.js"), "{body}"); assert!(body.contains("fetch('/api/mcp/sync-reset'"), "{body}"); - assert!(body.contains("mount('op')"), "{body}"); + assert!(body.contains("await mod.mount_ck('op')"), "{body}"); + assert!(body.contains("data-op-smoke"), "{body}"); + assert!(!body.contains("mod.mount('op')"), "{body}"); + assert!(!body.contains("EMSDK"), "{body}"); let _ = std::fs::remove_dir_all(&dir); } @@ -443,13 +459,27 @@ mod tests { body.contains("canvas.style.width = `${cssWidth}px`"), "{body}" ); - assert!(body.contains("requestIdleCallback"), "{body}"); assert!( body.contains("sizeCanvasForDisplay(STEADY_CANVAS_PIXEL_BUDGET)"), "{body}" ); - assert!(body.contains("window.requestAnimationFrame"), "{body}"); - assert!(body.contains("window.__opShell.resize()"), "{body}"); + assert!(body.contains("await mod.mount_ck('op')"), "{body}"); + assert!(!body.contains("window.__opShell.resize()"), "{body}"); + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn smoke_page_serves_canvaskit_mount_harness() { + let dir = stub_bundle("smoke"); + let reply = handle_static_request("/smoke/step-1b.html", Some(&dir)).expect("static route"); + assert_eq!(reply.status, "200 OK"); + assert_eq!(reply.content_type, "text/html; charset=utf-8"); + let body = String::from_utf8(reply.body).expect("utf8"); + assert!(body.contains("mod.mount_ck('op')"), "{body}"); + assert!(body.contains("data-op-smoke"), "{body}"); + assert!(body.contains("--features canvaskit"), "{body}"); + assert!(!body.contains("mod.mount('op')"), "{body}"); + assert!(!body.contains("EMSDK"), "{body}"); let _ = std::fs::remove_dir_all(&dir); } diff --git a/crates/op-host-services/src/web_static/index.html b/crates/op-host-services/src/web_static/index.html index da9bac66c..96f3ad25b 100644 --- a/crates/op-host-services/src/web_static/index.html +++ b/crates/op-host-services/src/web_static/index.html @@ -40,14 +40,17 @@ bundle (JS glue + .wasm) is served from `/pkg/*` by the same daemon. The hidden IME composition