test(web): add rust web browser smoke
This commit is contained in:
parent
26c39966e7
commit
521e44c92c
17
.github/workflows/rust-multiplatform.yml
vendored
17
.github/workflows/rust-multiplatform.yml
vendored
|
|
@ -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)
|
||||
|
|
|
|||
40
.github/workflows/rust-release.yml
vendored
40
.github/workflows/rust-release.yml
vendored
|
|
@ -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')
|
||||
|
|
|
|||
15
.github/workflows/wasm-bundle-build.yml
vendored
15
.github/workflows/wasm-bundle-build.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 `<canvas>`.
|
||||
//! and calls `mount_ck('op')`; the hidden IME textarea is created by the
|
||||
//! CanvasKit mount itself, so the page only carries the `<canvas>`.
|
||||
//! - `GET /smoke/step-1b.html` — the CanvasKit-only browser smoke harness.
|
||||
//! - `GET /pkg/<file>` — 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<St
|
|||
None => 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/<hash>/src/<file>.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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,14 +40,17 @@
|
|||
bundle (JS glue + .wasm) is served from `/pkg/*` by the same daemon.
|
||||
|
||||
The hidden IME composition <textarea> the web shell needs is created
|
||||
by `mount()` itself (op-host-web `lib.rs`), so this page only has to
|
||||
provide the <canvas>.
|
||||
by `mount_ck()` itself, so this page only has to provide the <canvas>.
|
||||
-->
|
||||
<canvas id="op"></canvas>
|
||||
<pre id="op-error"></pre>
|
||||
<script type="module">
|
||||
const errorPane = document.getElementById('op-error');
|
||||
const markSmoke = (state) => {
|
||||
document.documentElement.setAttribute('data-op-smoke', state);
|
||||
};
|
||||
const showError = (msg) => {
|
||||
markSmoke('error');
|
||||
errorPane.textContent = msg;
|
||||
errorPane.style.display = 'block';
|
||||
};
|
||||
|
|
@ -86,45 +89,22 @@
|
|||
};
|
||||
// Size the canvas to the window BEFORE mount — the shell reads the
|
||||
// canvas width/height attributes when it builds its raster surface.
|
||||
markSmoke('booting');
|
||||
sizeCanvasForDisplay(BOOT_CANVAS_PIXEL_BUDGET);
|
||||
try {
|
||||
await resetSyncDocument();
|
||||
const mod = await import('/pkg/op_host_web.js');
|
||||
await mod.default();
|
||||
// The CanvasKit bundle (renders skia on WebGL2 via /canvaskit/) exposes
|
||||
// `mount_ck` and owns its own resize via DOM listeners; the legacy
|
||||
// embedded-skia bundle exposes `mount` + a `WebShell.resize()` handle.
|
||||
if (typeof mod.mount_ck === 'function') {
|
||||
sizeCanvasForDisplay(STEADY_CANVAS_PIXEL_BUDGET);
|
||||
await mod.mount_ck('op');
|
||||
} else {
|
||||
// Keep the returned WebShell alive for the page lifetime: it owns
|
||||
// the DOM listeners + closures into the wasm module.
|
||||
window.__opShell = mod.mount('op');
|
||||
const idle = window.requestIdleCallback || ((cb) => window.setTimeout(cb, 250));
|
||||
idle(() => {
|
||||
if (sizeCanvasForDisplay(STEADY_CANVAS_PIXEL_BUDGET)) {
|
||||
window.__opShell.resize();
|
||||
}
|
||||
});
|
||||
let resizeRaf = 0;
|
||||
window.addEventListener('resize', () => {
|
||||
if (resizeRaf) return;
|
||||
resizeRaf = window.requestAnimationFrame(() => {
|
||||
resizeRaf = 0;
|
||||
if (sizeCanvasForDisplay(STEADY_CANVAS_PIXEL_BUDGET)) {
|
||||
window.__opShell.resize();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
sizeCanvasForDisplay(STEADY_CANVAS_PIXEL_BUDGET);
|
||||
await mod.mount_ck('op');
|
||||
markSmoke('ok');
|
||||
} catch (e) {
|
||||
showError(
|
||||
[
|
||||
'OpenPencil web shell failed to load.',
|
||||
'',
|
||||
'Expected the wasm-bindgen bundle at /pkg/op_host_web.js.',
|
||||
'Rebuild it with tools/check-wasm-bundle.sh (needs EMSDK), or',
|
||||
'Rebuild it with tools/check-wasm-bundle.sh, or',
|
||||
'point the daemon at an existing bundle directory via the',
|
||||
'OPENPENCIL_WEB_BUNDLE_DIR environment variable.',
|
||||
'',
|
||||
|
|
|
|||
|
|
@ -20,36 +20,32 @@ op-editor-ui = { path = "../op-editor-ui" }
|
|||
# single source of truth; `op-pen-loader` bridges it into the
|
||||
# read-only paint `Document` the shared widgets consume.
|
||||
#
|
||||
# Both are `optional` + behind the `skia` feature for the SAME reason
|
||||
# `skia-safe` is: the host (`widget_host`) is `#[cfg(feature="skia")]`-
|
||||
# gated, so the skia-free CI stub baseline
|
||||
# (`--no-default-features --features web`) never needs them. This
|
||||
# matters because `op-pen-loader` pulls `jian-skia` transitively
|
||||
# (`pen_document_to_document` runs `SkiaMeasure` for text layout);
|
||||
# keeping it optional preserves the wasm32-clean stub baseline.
|
||||
# These stay optional and are pulled only by `canvaskit`, so the wasm32-clean
|
||||
# stub baseline (`--no-default-features --features web`) never compiles the
|
||||
# full editor pipeline. `op-pen-loader` disables `skia-measure` below; the web
|
||||
# shell uses CanvasKit/browser text measurement instead of the native Skia
|
||||
# measurement path.
|
||||
op-editor-core = { path = "../op-editor-core", optional = true }
|
||||
op-editor-host-core = { path = "../op-editor-host-core", optional = true, default-features = false }
|
||||
# default-features = false drops `skia-measure`; the `skia` feature re-enables
|
||||
# it so only the CanvasKit build stays skia-free (estimate text measurement).
|
||||
# default-features = false drops `skia-measure` for the browser bundle.
|
||||
op-pen-loader = { path = "../op-pen-loader", optional = true, default-features = false }
|
||||
# P4b web AI-streaming foundation: the codegen/AI pipeline. Optional + behind
|
||||
# the `codegen` feature (which also pulls `skia`) so the wasm32-clean stub
|
||||
# baseline (`--no-default-features --features web`) never compiles it. NOTE:
|
||||
# the new web modules it backs (web_ai_transport / raf_pump / web_clipboard)
|
||||
# are UNVERIFIED in-browser — run tools/check-wasm-bundle.sh and a browser smoke test.
|
||||
# Web AI/codegen foundation. Optional + behind `canvaskit`, so the wasm32-clean
|
||||
# stub baseline never compiles it. End-to-end browser AI still depends on the
|
||||
# desktop daemon routes; unit tests cover the pure SSE parsing and bundle checks
|
||||
# cover wasm linkability.
|
||||
op-codegen = { path = "../op-codegen", features = ["ai"], optional = true }
|
||||
# P5 web codegen session: `build_codegen_input` resolves the selection into
|
||||
# `PenNode` subtrees (jian-ops-schema), serializes them (serde_json), and
|
||||
# constructs the pipeline `CodegenInput` whose param enums come from op-ai.
|
||||
# All optional + behind `codegen` so the wasm32-clean stub baseline never
|
||||
# pulls them. NOTE: UNVERIFIED in-browser; use tools/check-wasm-bundle.sh.
|
||||
# All optional + behind `canvaskit` so the wasm32-clean stub baseline never
|
||||
# pulls them.
|
||||
op-ai = { path = "../op-ai", optional = true }
|
||||
jian-ops-schema = { path = "../../vendor/jian/crates/jian-ops-schema", optional = true }
|
||||
serde_json = { workspace = true, optional = true }
|
||||
# Browser file-IO P0: Figma `.fig` parsing + clipboard-HTML decoding for the
|
||||
# web shell. Pure-Rust (flate2/miniz_oxide + ruzstd + base64 — wasm32-clean,
|
||||
# no C toolchain), so the same crate the desktop host uses compiles for the
|
||||
# browser bundle. Optional + behind `codegen` with the rest of the document
|
||||
# browser bundle. Optional + behind `canvaskit` with the rest of the document
|
||||
# pipeline deps so the wasm32-clean stub baseline never pulls it.
|
||||
op-figma = { path = "../op-figma", optional = true }
|
||||
# Step 1b §3.6: handwritten DOM mirror consumes accesskit::TreeUpdate (0.24).
|
||||
|
|
@ -79,7 +75,7 @@ features = [
|
|||
"Clipboard",
|
||||
# Browser file-IO P0 (paste / drag-drop / hidden file input): the
|
||||
# ClipboardEvent / DataTransfer / DragEvent / File* / HtmlInputElement
|
||||
# bindings below are only *called* from the `codegen`-gated dom_io glue,
|
||||
# bindings below are only *called* from the `canvaskit`-gated dom_io glue,
|
||||
# but web-sys feature flags are compile-time binding toggles — they keep
|
||||
# the `web` stub baseline compiling unchanged.
|
||||
"ClipboardEvent",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Step 1b WebShell Smoke</title>
|
||||
<title>CanvasKit WebShell Smoke</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
|
|
@ -36,51 +36,52 @@
|
|||
<div id="status"></div>
|
||||
<canvas id="op" width="960" height="640"></canvas>
|
||||
<script type="module">
|
||||
// Phase A smoke harness for the WebShell red-rect demo.
|
||||
// Smoke harness for the production CanvasKit WebShell.
|
||||
//
|
||||
// CURRENT STATE (post C-hard.2, 2026-05-09):
|
||||
// - Skia C++ + skia-bindings + skia-safe + shell-web build to
|
||||
// wasm32-unknown-unknown via the vendor/skia-safe-op fork
|
||||
// (build_support/platform/wasm_unknown.rs).
|
||||
// - The libc / libcxx / libm shim crate (crates/wasm-libc-shim)
|
||||
// resolves the previously-missing env.* imports. Verified via
|
||||
// `WebAssembly.Module.imports()`: 22 imports, all from
|
||||
// `./op_host_web_bg.js`; 0 env.* imports.
|
||||
// - The bundle is structurally LinkError-free; a LinkError here
|
||||
// is now a REGRESSION signal, not the expected state.
|
||||
// CURRENT STATE:
|
||||
// - `tools/check-wasm-bundle.sh` builds `op-host-web` with the
|
||||
// `canvaskit` feature, runs wasm-bindgen, asserts 0 env.* imports,
|
||||
// runs wasm-opt, and size-gates the wasm bundle.
|
||||
// - CanvasKit itself is served separately from `/canvaskit/`.
|
||||
// - A LinkError here is a regression in the current CanvasKit bundle.
|
||||
//
|
||||
// What this harness verifies:
|
||||
// - module loads (LinkError ⇒ regression in libc shim or fork)
|
||||
// - mount() returns a live WebShell
|
||||
// - paint_phase_a draws a red rect via skia raster + put_image_data
|
||||
// - module loads
|
||||
// - mount_ck() resolves and paints the Rust editor shell through CanvasKit
|
||||
//
|
||||
// What this harness does NOT verify (Phase E manual smoke):
|
||||
// - the red rect is actually visible to a human in a browser tab
|
||||
// - latency is within the 16ms / 60fps budget
|
||||
// - DPI scale + canvas resize behave correctly
|
||||
// What this harness does NOT verify:
|
||||
// - all daemon-backed routes are available
|
||||
// - AI / file / export endpoints work end to end
|
||||
// - performance is within frame budget under a large document
|
||||
//
|
||||
// Round 2 C-R2-1 fix: assign the returned WebShell to
|
||||
// `window.__opShell` so the GC keeps it alive for the page
|
||||
// lifetime; the shell may hold closures into the wasm module.
|
||||
// Keep the returned promise reachable for the page lifetime; the shell
|
||||
// holds browser event closures into the wasm module.
|
||||
const status = document.getElementById('status');
|
||||
const markSmoke = (state) => {
|
||||
document.documentElement.setAttribute('data-op-smoke', state);
|
||||
};
|
||||
const log = (cls, msg) => {
|
||||
const block = document.createElement('pre');
|
||||
block.className = cls;
|
||||
block.textContent = msg;
|
||||
status.appendChild(block);
|
||||
};
|
||||
markSmoke('booting');
|
||||
try {
|
||||
const mod = await import('../pkg/op_host_web.js');
|
||||
await mod.default();
|
||||
window.__opShell = mod.mount('op');
|
||||
log('ok', 'WebShell mounted; canvas should show centered red rect.');
|
||||
window.__opShell = mod.mount_ck('op');
|
||||
await window.__opShell;
|
||||
markSmoke('ok');
|
||||
log('ok', 'CanvasKit WebShell mounted; the editor canvas should be visible.');
|
||||
} catch (e) {
|
||||
markSmoke('error');
|
||||
const isLinkError =
|
||||
e instanceof WebAssembly.LinkError ||
|
||||
(e && e.constructor && e.constructor.name === 'LinkError') ||
|
||||
(e && e.message && e.message.startsWith('WebAssembly.instantiate()'));
|
||||
const header = isLinkError
|
||||
? 'WebShell wasm bundle failed to link — REGRESSION (post C-hard.2 the bundle is supposed to be LinkError-free).'
|
||||
? 'CanvasKit WebShell wasm bundle failed to link — REGRESSION.'
|
||||
: 'WebShell loader could not be imported.';
|
||||
log(
|
||||
'error',
|
||||
|
|
@ -90,20 +91,17 @@
|
|||
'Expected file: ../pkg/op_host_web.js',
|
||||
'',
|
||||
'How to rebuild the bundle (when the file is missing or stale):',
|
||||
' 1. brew install emscripten # 5.0.7 + EMSDK shim',
|
||||
' 2. mkdir -p ~/.emsdk/upstream && \\',
|
||||
' ln -sfn /opt/homebrew/opt/emscripten/libexec ~/.emsdk/upstream/emscripten',
|
||||
' 3. EMSDK="$HOME/.emsdk" cargo build -p op-host-web \\',
|
||||
' --target wasm32-unknown-unknown --features skia --release',
|
||||
' 4. (one-time) ln -sf libfoo.wasm.a libfoo.a in target/.../skia-bindings-*/out/skia/',
|
||||
' 5. wasm-bindgen --target web \\',
|
||||
' --out-dir crates/op-host-web/pkg \\',
|
||||
' target/wasm32-unknown-unknown/release/op_host_web.wasm',
|
||||
' 1. bash tools/check-wasm-bundle.sh',
|
||||
' # equivalent cargo step uses --features canvaskit',
|
||||
' 2. serve crates/op-host-web/pkg at /pkg/',
|
||||
' 3. serve crates/op-host-web/assets/canvaskit at /canvaskit/',
|
||||
'',
|
||||
'For the normal local path, run:',
|
||||
' op start --web',
|
||||
'',
|
||||
'A LinkError after a clean rebuild means a new env.* import',
|
||||
'snuck back in (skia upgrade, new shim gap, etc). Inspect via',
|
||||
' wasm-objdump --details --section=Import ../pkg/op_host_web_bg.wasm',
|
||||
'and add the missing symbol to crates/wasm-libc-shim/src/imp.rs.',
|
||||
'snuck back in. Run tools/check-wasm-bundle.sh and inspect',
|
||||
'the import guard failure.',
|
||||
'',
|
||||
'Underlying error: ' + (e && e.message ? e.message : String(e)),
|
||||
].join('\n'),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
// UNVERIFIED in-browser: run tools/check-wasm-bundle.sh and a browser smoke test.
|
||||
// Browser boundary: FileReader, Blob downloads, paste, and drag/drop require
|
||||
// browser smoke; pure ingest/export behavior is covered in file_actions tests.
|
||||
//! Browser file-IO glue — the web consumer of
|
||||
//! `editor_ui.pending_file_action` plus the DOM paste / drag-drop
|
||||
//! listeners. The pure serialize / ingest logic lives in
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
// UNVERIFIED in-browser: run tools/check-wasm-bundle.sh and a browser smoke test.
|
||||
// Browser boundary: these pure helpers are unit-tested; DOM picker/download
|
||||
// behavior lives in dom_io and still needs browser smoke.
|
||||
//! Pure (DOM-free) document IO helpers behind the browser file-IO
|
||||
//! glue (`dom_io`). Everything here is plain Rust over
|
||||
//! `op_editor_core` / `op_pen_loader` / `op_figma`, so the unit tests
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
// UNVERIFIED: needs EMSDK wasm32 build + browser; run tools/check-wasm-bundle.sh
|
||||
// Browser/daemon boundary: Iconify XHR uses the public CORS API while the brand
|
||||
// catalog comes from the daemon-served asset; parsing/apply logic is unit-tested.
|
||||
//! Web Iconify bridge — the browser consumer of the icon picker's
|
||||
//! `icon_picker_load_more_request` flag.
|
||||
//!
|
||||
|
|
|
|||
|
|
@ -33,8 +33,8 @@ mod widget_host;
|
|||
// drive the CanvasKit `CkInner` through one trait.
|
||||
#[cfg(feature = "canvaskit")]
|
||||
mod repaint_ctx;
|
||||
// Pure web_sys IO (no skia) — compiled always so the `web` stub still
|
||||
// compile-checks on wasm32 without EMSDK.
|
||||
// Pure web_sys IO (no native Skia/C toolchain) — compiled always so the `web`
|
||||
// stub still compile-checks on wasm32.
|
||||
mod live_sync;
|
||||
#[cfg(feature = "canvaskit")]
|
||||
mod live_sync_glue;
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
//! talk to the web-canvas daemon (`op-host-desktop`'s `web_canvas_server`,
|
||||
//! run via `--serve-web` / `op start --web`).
|
||||
//!
|
||||
//! This module is PURE `web_sys` IO (no skia, no `op_editor_core`), so it
|
||||
//! compile-checks on the wasm32 web stub WITHOUT an EMSDK toolchain — i.e. the
|
||||
//! This module is PURE `web_sys` IO (no native Skia/C toolchain and no
|
||||
//! `op_editor_core`), so it compile-checks on the wasm32 web stub. The
|
||||
//! highest-risk part of the live-sync glue (the `web_sys` XHR/interval calls)
|
||||
//! is verified by `cargo check -p op-host-web --target wasm32-unknown-unknown`.
|
||||
//! The protocol decisions (version gating, push baselines, apply + repaint)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
// UNVERIFIED in-browser: run tools/check-wasm-bundle.sh and a browser smoke test.
|
||||
// Browser boundary: requestAnimationFrame scheduling needs browser smoke; the
|
||||
// CanvasKit bundle gate covers wasm linkability.
|
||||
//! A `requestAnimationFrame` loop that ticks while work is in flight.
|
||||
//!
|
||||
//! Used to drain streamed AI deltas into the transcript ~once per frame (the
|
||||
|
|
|
|||
|
|
@ -2,13 +2,11 @@
|
|||
//! live-sync, codegen, file IO, icon search, fonts) and whichever rendering
|
||||
//! host owns the shell.
|
||||
//!
|
||||
//! Both mount paths wrap their state in `Rc<RefCell<_>>` and expose the same
|
||||
//! two things the integration modules need: the shared `WidgetHost` (where all
|
||||
//! `EditorState` lives) and a `repaint()` that re-paints through that path's
|
||||
//! backend. The skia raster mount (`Inner`, `lib.rs`) and the CanvasKit GPU
|
||||
//! mount (`CkInner`, `canvaskit.rs`) each implement this trait, so the modules
|
||||
//! can be written once against `Rc<RefCell<impl RepaintContext>>` instead of a
|
||||
//! concrete `Inner`.
|
||||
//! The production CanvasKit mount wraps its state in `Rc<RefCell<_>>` and
|
||||
//! exposes the two things the integration modules need: the shared `WidgetHost`
|
||||
//! (where all `EditorState` lives) and a `repaint()` that re-paints through the
|
||||
//! CanvasKit backend. Keeping the trait lets those modules stay independent of
|
||||
//! the concrete `CkInner` type.
|
||||
|
||||
use crate::widget_host::WidgetHost;
|
||||
use wasm_bindgen::JsValue;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
// UNVERIFIED in-browser: run tools/check-wasm-bundle.sh and a browser smoke test.
|
||||
// Browser/daemon boundary: XHR SSE needs a running desktop daemon; pure SSE
|
||||
// parsing is unit-tested and the CanvasKit bundle gate covers wasm linkability.
|
||||
//! XHR-based SSE transport to the desktop daemon's AI SSE endpoints.
|
||||
//!
|
||||
//! Mirrors `live_sync.rs` (no `wasm-bindgen-futures`): an `XmlHttpRequest`
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
// UNVERIFIED in-browser: run tools/check-wasm-bundle.sh and a browser smoke test.
|
||||
// Browser/daemon boundary: chat streams through /api/ai/standard on the desktop
|
||||
// daemon; this module keeps that web architecture instead of bypassing it.
|
||||
//! Web chat session — drives a real AI turn through the desktop daemon's
|
||||
//! `/api/ai/standard` route instead of the offline echo stub.
|
||||
//!
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
// UNVERIFIED in-browser: run tools/check-wasm-bundle.sh and a browser smoke test.
|
||||
// Browser boundary: clipboard writes and download clicks require real browser
|
||||
// gestures; the CanvasKit bundle gate covers wasm linkability.
|
||||
//! Browser clipboard write + file download (Blob + anchor).
|
||||
//!
|
||||
//! The two browser-side actions the codegen panel needs: copy generated source
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
// UNVERIFIED in-browser: run tools/check-wasm-bundle.sh and a browser smoke test.
|
||||
// Browser boundary: Local Font Access permission and CanvasKit font-byte loading
|
||||
// need browser smoke; pure classifiers and fallback routing are unit-tested.
|
||||
//! Browser Local Font Access bridge for the web host.
|
||||
//!
|
||||
//! The wasm Skia backend cannot see operating-system fonts by itself. The
|
||||
|
|
|
|||
|
|
@ -19,3 +19,145 @@ fn rust_check_runs_canvaskit_widget_host_tests() {
|
|||
"rust-check must enable the production CanvasKit web host feature"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_workflow_documents_current_canvaskit_bundle_path() {
|
||||
let workflow = std::fs::read_to_string(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../../.github/workflows/rust-release.yml"
|
||||
))
|
||||
.expect("rust-release workflow is readable");
|
||||
|
||||
assert!(
|
||||
workflow.contains("wasm-bundle-build.yml"),
|
||||
"release workflow notes should point at the current CanvasKit bundle workflow"
|
||||
);
|
||||
assert!(
|
||||
!workflow.contains("--features skia"),
|
||||
"release workflow must not describe the retired skia wasm build path"
|
||||
);
|
||||
assert!(
|
||||
!workflow.contains("EMSDK"),
|
||||
"release workflow must not describe the retired EMSDK wasm build path"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_smoke_page_uses_current_canvaskit_bundle_command() {
|
||||
let smoke = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/smoke/step-1b.html"))
|
||||
.expect("web smoke page is readable");
|
||||
|
||||
assert!(
|
||||
smoke.contains("--features canvaskit"),
|
||||
"web smoke page should show the current CanvasKit bundle build command"
|
||||
);
|
||||
assert!(
|
||||
smoke.contains("mod.mount_ck('op')"),
|
||||
"web smoke page should mount the production CanvasKit shell"
|
||||
);
|
||||
assert!(
|
||||
!smoke.contains("mod.mount('op')"),
|
||||
"web smoke page must not mount the default wasm stub"
|
||||
);
|
||||
assert!(
|
||||
!smoke.contains("--features skia"),
|
||||
"web smoke page must not point users at the retired skia wasm build"
|
||||
);
|
||||
assert!(
|
||||
!smoke.contains("EMSDK"),
|
||||
"web smoke page must not require the retired EMSDK setup"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_integration_notes_avoid_legacy_ambiguous_markers() {
|
||||
let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||
let files = [
|
||||
"Cargo.toml",
|
||||
"src/dom_io.rs",
|
||||
"src/file_actions.rs",
|
||||
"src/iconify_web.rs",
|
||||
"src/lib.rs",
|
||||
"src/live_sync.rs",
|
||||
"src/raf_pump.rs",
|
||||
"src/web_ai_transport.rs",
|
||||
"src/web_chat.rs",
|
||||
"src/web_clipboard.rs",
|
||||
"src/web_fonts.rs",
|
||||
];
|
||||
|
||||
for file in files {
|
||||
let text = std::fs::read_to_string(manifest_dir.join(file))
|
||||
.unwrap_or_else(|err| panic!("{file} is readable: {err}"));
|
||||
assert!(
|
||||
!text.contains("UNVERIFIED in-browser"),
|
||||
"{file} should name the concrete smoke/daemon boundary instead of a blanket UNVERIFIED marker"
|
||||
);
|
||||
assert!(
|
||||
!text.contains("EMSDK"),
|
||||
"{file} should describe the current CanvasKit/web-sys boundary instead of the retired EMSDK path"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn browser_smoke_script_and_ci_cover_canvas_and_daemon_paths() {
|
||||
let repo = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
let script = std::fs::read_to_string(repo.join("tools/check-web-browser-smoke.sh"))
|
||||
.expect("browser smoke script is readable");
|
||||
let workflow = std::fs::read_to_string(repo.join(".github/workflows/wasm-bundle-build.yml"))
|
||||
.expect("wasm bundle workflow is readable");
|
||||
|
||||
assert!(
|
||||
script.contains("--dump-dom"),
|
||||
"browser smoke should drive a real headless browser, not only curl"
|
||||
);
|
||||
assert!(
|
||||
script.contains("/smoke/step-1b.html"),
|
||||
"browser smoke should cover the pure CanvasKit mount harness"
|
||||
);
|
||||
assert!(
|
||||
script.contains("/api/mcp/server"),
|
||||
"browser smoke should cover the daemon health path"
|
||||
);
|
||||
assert!(
|
||||
script.contains("data-op-smoke=\"ok\""),
|
||||
"browser smoke should assert the DOM success marker written after mount"
|
||||
);
|
||||
assert!(
|
||||
workflow.contains("tools/check-web-browser-smoke.sh"),
|
||||
"bundle workflow should run the browser smoke after building the bundle"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_rust_publish_has_docker_and_start_script_entrypoints() {
|
||||
let repo = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
let dockerfile =
|
||||
std::fs::read_to_string(repo.join("Dockerfile.web-rust")).expect("Dockerfile readable");
|
||||
let start_script = std::fs::read_to_string(repo.join("scripts/start-web-rust.sh"))
|
||||
.expect("web rust start script is readable");
|
||||
let package =
|
||||
std::fs::read_to_string(repo.join("package.json")).expect("package.json readable");
|
||||
|
||||
assert!(
|
||||
dockerfile.contains("op-host-web-server --serve-web"),
|
||||
"web Rust Docker image should publish through the headless serve-web daemon"
|
||||
);
|
||||
assert!(
|
||||
start_script.contains("tools/check-wasm-bundle.sh"),
|
||||
"start script should build or verify the deployable CanvasKit bundle"
|
||||
);
|
||||
assert!(
|
||||
start_script.contains("op-host-web-server") && start_script.contains("--serve-web"),
|
||||
"start script should publish through op-host-web-server --serve-web"
|
||||
);
|
||||
assert!(
|
||||
start_script.contains("OPENPENCIL_WEB_BUNDLE_DIR"),
|
||||
"start script should point the daemon at the built web bundle explicitly"
|
||||
);
|
||||
assert!(
|
||||
package.contains("\"shell:web:serve\""),
|
||||
"package scripts should expose the web Rust start script"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,8 @@
|
|||
"cargo:clippy": "cargo clippy --workspace --all-targets -- -D warnings",
|
||||
"cargo:wasm-check": "cargo check --target wasm32-unknown-unknown -p op-host-web --no-default-features --features web",
|
||||
"cargo:deny": "cargo deny check && cargo deny --target wasm32-unknown-unknown check bans",
|
||||
"shell:wasm:dev": "echo 'Build the browser bundle: bash tools/check-wasm-bundle.sh (op-host-web --features canvaskit -> wasm-bindgen -> wasm-opt). Serve it: op start --web (raw form: openpencil-desktop --serve-web <port>)'"
|
||||
"shell:wasm:dev": "echo 'Build the browser bundle: bash tools/check-wasm-bundle.sh (op-host-web --features canvaskit -> wasm-bindgen -> wasm-opt). Serve it: op start --web (raw form: openpencil-desktop --serve-web <port>)'",
|
||||
"shell:web:serve": "bash scripts/start-web-rust.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.18.2",
|
||||
|
|
|
|||
40
scripts/start-web-rust.sh
Executable file
40
scripts/start-web-rust.sh
Executable file
|
|
@ -0,0 +1,40 @@
|
|||
#!/usr/bin/env bash
|
||||
# Build and serve the Rust CanvasKit web editor without Docker.
|
||||
#
|
||||
# This is the script-publish counterpart to Dockerfile.web-rust. It builds the
|
||||
# deployable wasm bundle, builds the GUI-free op-host-web-server daemon, then
|
||||
# serves the browser editor through `--serve-web`.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
PORT="${OPENPENCIL_SERVE_PORT:-3100}"
|
||||
HOST="${OPENPENCIL_SERVE_HOST:-127.0.0.1}"
|
||||
DOC_PATH="${1:-}"
|
||||
|
||||
if [ "${OPENPENCIL_SKIP_WASM_BUILD:-0}" != "1" ]; then
|
||||
bash tools/check-wasm-bundle.sh
|
||||
fi
|
||||
|
||||
if [ "${OPENPENCIL_SKIP_SERVER_BUILD:-0}" != "1" ]; then
|
||||
cargo build -p op-host-web-server --release
|
||||
fi
|
||||
|
||||
SERVER_BIN="${OPENPENCIL_WEB_SERVER_BIN:-target/release/op-host-web-server}"
|
||||
if [ ! -x "$SERVER_BIN" ]; then
|
||||
echo "error: server binary is not executable: $SERVER_BIN" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export OPENPENCIL_WEB_BUNDLE_DIR="${OPENPENCIL_WEB_BUNDLE_DIR:-$PWD/crates/op-host-web/pkg}"
|
||||
export OPENPENCIL_CANVASKIT_DIR="${OPENPENCIL_CANVASKIT_DIR:-$PWD/crates/op-host-web/assets/canvaskit}"
|
||||
|
||||
echo "OpenPencil Rust web editor: http://${HOST}:${PORT}/"
|
||||
echo "bundle: ${OPENPENCIL_WEB_BUNDLE_DIR}"
|
||||
echo "canvaskit: ${OPENPENCIL_CANVASKIT_DIR}"
|
||||
|
||||
if [ -n "$DOC_PATH" ]; then
|
||||
exec "$SERVER_BIN" --serve-web "$PORT" "$DOC_PATH" --host "$HOST"
|
||||
else
|
||||
exec "$SERVER_BIN" --serve-web "$PORT" --host "$HOST"
|
||||
fi
|
||||
184
tools/check-web-browser-smoke.sh
Executable file
184
tools/check-web-browser-smoke.sh
Executable file
|
|
@ -0,0 +1,184 @@
|
|||
#!/usr/bin/env bash
|
||||
# Browser smoke for the Rust CanvasKit web bundle.
|
||||
#
|
||||
# Covers both browser paths:
|
||||
# 1. `/smoke/step-1b.html` — pure CanvasKit mount harness.
|
||||
# 2. `/` — daemon-served host page, including `/api/mcp/sync-reset`.
|
||||
#
|
||||
# The script intentionally uses Chrome/Chromium headless directly instead of a
|
||||
# new test dependency. The pages write `data-op-smoke="ok"` only after the wasm
|
||||
# bundle has loaded and `mount_ck()` has resolved.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
fail() {
|
||||
printf 'FAIL: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
need() {
|
||||
command -v "$1" >/dev/null 2>&1 || {
|
||||
printf 'missing prerequisite: %s\n' "$1" >&2
|
||||
exit 2
|
||||
}
|
||||
}
|
||||
|
||||
find_chrome() {
|
||||
if [ -n "${OPENPENCIL_CHROME_BIN:-}" ]; then
|
||||
printf '%s\n' "$OPENPENCIL_CHROME_BIN"
|
||||
return
|
||||
fi
|
||||
for bin in google-chrome-stable google-chrome chromium chromium-browser; do
|
||||
if command -v "$bin" >/dev/null 2>&1; then
|
||||
command -v "$bin"
|
||||
return
|
||||
fi
|
||||
done
|
||||
for app in \
|
||||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
|
||||
"/Applications/Chromium.app/Contents/MacOS/Chromium"; do
|
||||
if [ -x "$app" ]; then
|
||||
printf '%s\n' "$app"
|
||||
return
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
pick_port() {
|
||||
python3 - <<'PY'
|
||||
import socket
|
||||
s = socket.socket()
|
||||
s.bind(("127.0.0.1", 0))
|
||||
print(s.getsockname()[1])
|
||||
s.close()
|
||||
PY
|
||||
}
|
||||
|
||||
wait_for_health() {
|
||||
local url="$1"
|
||||
local tries="${OPENPENCIL_BROWSER_SMOKE_WAIT_TRIES:-100}"
|
||||
for _ in $(seq 1 "$tries"); do
|
||||
if curl -fsS "$url" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
assert_contains() {
|
||||
local file="$1"
|
||||
local needle="$2"
|
||||
local label="$3"
|
||||
if ! grep -Fq "$needle" "$file"; then
|
||||
printf 'FAIL: %s\nexpected to find: %s\n--- %s ---\n' "$label" "$needle" "$file" >&2
|
||||
sed -n '1,220p' "$file" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
chrome_dump() {
|
||||
local url="$1"
|
||||
local out="$2"
|
||||
local profile="$TMP_DIR/chrome-$(basename "$out" .html)"
|
||||
local timeout="${OPENPENCIL_BROWSER_SMOKE_TIMEOUT_SECS:-45}"
|
||||
mkdir -p "$profile"
|
||||
"$CHROME_BIN" \
|
||||
--headless=new \
|
||||
--no-sandbox \
|
||||
--disable-dev-shm-usage \
|
||||
--disable-background-networking \
|
||||
--disable-component-update \
|
||||
--disable-default-apps \
|
||||
--disable-sync \
|
||||
--ignore-gpu-blocklist \
|
||||
--metrics-recording-only \
|
||||
--no-default-browser-check \
|
||||
--no-first-run \
|
||||
--use-gl=swiftshader \
|
||||
--enable-unsafe-swiftshader \
|
||||
--window-size=1280,800 \
|
||||
--user-data-dir="$profile" \
|
||||
--virtual-time-budget="${OPENPENCIL_BROWSER_SMOKE_VIRTUAL_TIME_MS:-15000}" \
|
||||
--dump-dom "$url" >"$out" 2>"$out.stderr" &
|
||||
local chrome_pid=$!
|
||||
for _ in $(seq 1 "$timeout"); do
|
||||
if grep -Fq 'data-op-smoke="ok"' "$out" 2>/dev/null; then
|
||||
kill "$chrome_pid" >/dev/null 2>&1 || true
|
||||
wait "$chrome_pid" >/dev/null 2>&1 || true
|
||||
return 0
|
||||
fi
|
||||
if ! kill -0 "$chrome_pid" >/dev/null 2>&1; then
|
||||
wait "$chrome_pid"
|
||||
return $?
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
kill "$chrome_pid" >/dev/null 2>&1 || true
|
||||
wait "$chrome_pid" >/dev/null 2>&1 || true
|
||||
printf 'headless Chrome timed out for %s after %ss\n' "$url" "$timeout" >&2
|
||||
return 124
|
||||
}
|
||||
|
||||
need curl
|
||||
need python3
|
||||
|
||||
CHROME_BIN="$(find_chrome)" || {
|
||||
printf 'missing prerequisite: Chrome/Chromium (set OPENPENCIL_CHROME_BIN)\n' >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
if [ "${OPENPENCIL_SKIP_WASM_BUILD:-0}" != "1" ]; then
|
||||
bash tools/check-wasm-bundle.sh
|
||||
fi
|
||||
|
||||
SERVER_BIN="${OPENPENCIL_WEB_SERVER_BIN:-target/release/op-host-web-server}"
|
||||
if [ "${OPENPENCIL_SKIP_SERVER_BUILD:-0}" != "1" ]; then
|
||||
cargo build -p op-host-web-server --release >/dev/null
|
||||
fi
|
||||
[ -x "$SERVER_BIN" ] || fail "server binary is not executable: $SERVER_BIN"
|
||||
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
SERVER_PID=""
|
||||
cleanup() {
|
||||
if [ -n "$SERVER_PID" ] && kill -0 "$SERVER_PID" >/dev/null 2>&1; then
|
||||
kill "$SERVER_PID" >/dev/null 2>&1 || true
|
||||
wait "$SERVER_PID" >/dev/null 2>&1 || true
|
||||
fi
|
||||
rm -rf "$TMP_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
PORT="${OPENPENCIL_WEB_SMOKE_PORT:-$(pick_port)}"
|
||||
BASE_URL="http://127.0.0.1:${PORT}"
|
||||
SERVER_LOG="$TMP_DIR/server.log"
|
||||
|
||||
OPENPENCIL_WEB_BUNDLE_DIR="${OPENPENCIL_WEB_BUNDLE_DIR:-$PWD/crates/op-host-web/pkg}" \
|
||||
OPENPENCIL_CANVASKIT_DIR="${OPENPENCIL_CANVASKIT_DIR:-$PWD/crates/op-host-web/assets/canvaskit}" \
|
||||
"$SERVER_BIN" --serve-web "$PORT" --host 127.0.0.1 >"$SERVER_LOG" 2>&1 &
|
||||
SERVER_PID="$!"
|
||||
|
||||
wait_for_health "$BASE_URL/api/mcp/server" || {
|
||||
printf 'server did not become healthy\n--- server log ---\n' >&2
|
||||
cat "$SERVER_LOG" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
health="$TMP_DIR/health.json"
|
||||
curl -fsS "$BASE_URL/api/mcp/server" >"$health"
|
||||
assert_contains "$health" '"mode":"web-canvas"' "daemon health should report web-canvas mode"
|
||||
curl -fsS "$BASE_URL/pkg/op_host_web.js" >/dev/null
|
||||
curl -fsS "$BASE_URL/canvaskit/canvaskit.js" >/dev/null
|
||||
curl -fsS "$BASE_URL/assets/iconify-catalog-brands.json" >/dev/null
|
||||
|
||||
pure="$TMP_DIR/pure-smoke.html"
|
||||
chrome_dump "$BASE_URL/smoke/step-1b.html" "$pure"
|
||||
assert_contains "$pure" 'data-op-smoke="ok"' "pure CanvasKit smoke should mount"
|
||||
|
||||
daemon="$TMP_DIR/daemon-smoke.html"
|
||||
chrome_dump "$BASE_URL/" "$daemon"
|
||||
assert_contains "$daemon" 'data-op-smoke="ok"' "daemon host page should mount"
|
||||
|
||||
printf 'PASS: browser smoke mounted CanvasKit via /smoke/step-1b.html and daemon /\n'
|
||||
Loading…
Reference in a new issue