diff --git a/CLAUDE.md b/CLAUDE.md index 88f8057..493e7a4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -175,6 +175,7 @@ Line-based, one command per line. `#` starts a comment. |---|---| | `viewport ` | Set viewport for subsequent `goto`s (default 800x600). | | `goto ` | Load a page. Replaces prior state. | +| `goto_as ` | Load a local HTML fixture while assigning it an explicit base URL. Useful for offline, origin-sensitive tests. | | `screenshot ` | Write PNG of current viewport. | | `dump_dom ` | Write indented text dump of the DOM. | | `dump_console ` | Write captured console.log/warn/error output. | @@ -193,6 +194,12 @@ and SVG. Run it after non-trivial changes: cargo run -p we-e2e -- --scenario crates/e2e/scenarios/smoke.we --out-dir crates/e2e/artifacts ``` +Service worker registration coverage also has an offline scenario: + +```sh +cargo run -p we-e2e -- --scenario crates/e2e/scenarios/service_worker.we --out-dir crates/e2e/artifacts +``` + Outputs land in `crates/e2e/artifacts/` (gitignored). View the PNGs to spot regressions; the harness only catches *crashes* and assertion failures automatically — visual diffs still need your eyes. diff --git a/crates/browser/tests/wpt.rs b/crates/browser/tests/wpt.rs index 97b3307..402aa12 100644 --- a/crates/browser/tests/wpt.rs +++ b/crates/browser/tests/wpt.rs @@ -476,7 +476,7 @@ fn collect_results_from_vm(vm: &mut we_js::vm::Vm) -> Vec { } /// Run a single WPT test HTML file. -fn run_test_file(path: &Path) -> TestFileResult { +fn run_test_file(path: &Path, test_root: &Path) -> TestFileResult { let html = match std::fs::read_to_string(path) { Ok(s) => s, Err(e) => return TestFileResult::Skip(format!("read error: {e}")), @@ -488,8 +488,9 @@ fn run_test_file(path: &Path) -> TestFileResult { } // Wrap in catch_unwind to handle panics gracefully. - let result = - std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| run_test_file_inner(&html))); + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + run_test_file_inner(&html, path, test_root) + })); match result { Ok(r) => r, @@ -498,7 +499,7 @@ fn run_test_file(path: &Path) -> TestFileResult { } /// Inner implementation of test file execution (may panic). -fn run_test_file_inner(html: &str) -> TestFileResult { +fn run_test_file_inner(html: &str, path: &Path, test_root: &Path) -> TestFileResult { // Parse HTML to DOM. let doc = we_html::parse_html(html); @@ -507,6 +508,13 @@ fn run_test_file_inner(html: &str) -> TestFileResult { let mut vm = we_js::vm::Vm::new(); vm.set_console_output(Box::new(RcConsole(console.clone()))); vm.attach_document(doc); + let rel = path.strip_prefix(test_root).unwrap_or(path); + let test_url = rel.to_string_lossy().replace('\\', "/"); + let test_url = we_url::Url::parse(&format!("https://wpt.test/{test_url}")).ok(); + if let Some(url) = test_url.clone() { + vm.set_document_url(url); + vm.set_document_origin("https://wpt.test"); + } // Set instruction limit. vm.set_instruction_limit(INSTRUCTION_LIMIT); @@ -543,6 +551,10 @@ fn run_test_file_inner(html: &str) -> TestFileResult { // Re-attach document. vm.attach_document(doc); + if let Some(url) = test_url { + vm.set_document_url(url); + vm.set_document_origin("https://wpt.test"); + } if scripts.is_empty() { return TestFileResult::Skip("no inline scripts".to_string()); @@ -689,6 +701,7 @@ fn wpt_dom_tests_inner() { "dom/collections", "html/dom", "console", + "service-workers", ]; let mut all_files: Vec = Vec::new(); @@ -726,7 +739,7 @@ fn wpt_dom_tests_inner() { .or_insert_with(CategoryStats::new); let file_name = rel.to_string_lossy().to_string(); - match run_test_file(path) { + match run_test_file(path, &test_dir) { TestFileResult::Executed(subtests) => { for subtest in &subtests { match subtest.status { diff --git a/crates/e2e/pages/21_service_worker_registration.html b/crates/e2e/pages/21_service_worker_registration.html new file mode 100644 index 0000000..66b4bd9 --- /dev/null +++ b/crates/e2e/pages/21_service_worker_registration.html @@ -0,0 +1,24 @@ + + + + + Service worker registration e2e + + +

Service worker registration

+

pending

+ + + diff --git a/crates/e2e/scenarios/service_worker.we b/crates/e2e/scenarios/service_worker.we new file mode 100644 index 0000000..5ca6e89 --- /dev/null +++ b/crates/e2e/scenarios/service_worker.we @@ -0,0 +1,11 @@ +# Service worker registration regression. The page is loaded from disk but gets +# an HTTPS document URL so registration is same-origin and network-free. + +viewport 800 600 +goto_as https://we.test/app/index.html crates/e2e/pages/21_service_worker_registration.html +dump_console service_worker.console.txt +assert_console_contains "sw-ready:active:https://we.test/app/" +assert_console_contains "registrations:1" +dump_dom service_worker.dom.txt +assert_dom_contains "sw-ready:active:https://we.test/app/" +screenshot service_worker.png diff --git a/crates/e2e/src/render.rs b/crates/e2e/src/render.rs index 4071562..ee5b8fa 100644 --- a/crates/e2e/src/render.rs +++ b/crates/e2e/src/render.rs @@ -50,6 +50,29 @@ pub fn render(arg: &str, width: u32, height: u32) -> Result Result { + let text = std::fs::read_to_string(file).map_err(|e| format!("read {file}: {e}"))?; + let base_url = Url::parse(base_url).map_err(|e| format!("parse base URL {base_url}: {e}"))?; + render_loaded( + LoadedHtml { + text, + base_url, + http_csp: PolicyList::new(), + }, + width, + height, + ) +} + fn render_loaded(loaded: LoadedHtml, width: u32, height: u32) -> Result { let mut warnings = Vec::new(); diff --git a/crates/e2e/src/scenario.rs b/crates/e2e/src/scenario.rs index d991476..5c71e5a 100644 --- a/crates/e2e/src/scenario.rs +++ b/crates/e2e/src/scenario.rs @@ -6,6 +6,7 @@ //! //! ```text //! goto # load a page (replaces any prior state) +//! goto_as # load a local page with an explicit base URL //! viewport # set viewport before next goto (default 800x600) //! screenshot # write current screenshot //! dump_dom # write DOM tree dump @@ -26,6 +27,7 @@ use crate::render::{render, RenderOutput}; #[derive(Debug, Clone)] pub enum Cmd { Goto(String), + GotoAs { url: String, path: PathBuf }, Viewport(u32, u32), Screenshot(PathBuf), DumpDom(PathBuf), @@ -70,6 +72,18 @@ fn parse_line(line: &str) -> Result { } Ok(Cmd::Goto(rest.to_string())) } + "goto_as" => { + let mut it = rest.split_whitespace(); + let url = it.next().ok_or("goto_as: missing URL")?.to_string(); + let path = it.next().ok_or("goto_as: missing path")?; + if it.next().is_some() { + return Err("goto_as: expected exactly URL and path".into()); + } + Ok(Cmd::GotoAs { + url, + path: PathBuf::from(path), + }) + } "viewport" => { let mut it = rest.split_whitespace(); let w: u32 = it @@ -150,6 +164,33 @@ pub fn run(scenario: &Scenario, out_dir: &Path) -> RunReport { current = None; } }, + Cmd::GotoAs { url, path } => { + match crate::render::render_file_as_url( + &path.to_string_lossy(), + url, + viewport.0, + viewport.1, + ) { + Ok(out) => { + warnings.extend(out.warnings.iter().cloned()); + steps.push(( + *lineno, + true, + format!("goto_as {url} {} ok", path.display()), + )); + current = Some(out); + } + Err(e) => { + steps.push(( + *lineno, + false, + format!("goto_as {url} {}: {e}", path.display()), + )); + all_ok = false; + current = None; + } + } + } Cmd::Viewport(w, h) => { viewport = (*w, *h); steps.push((*lineno, true, format!("viewport {w}x{h}"))); diff --git a/crates/js/src/service_worker.rs b/crates/js/src/service_worker.rs index 65a0f83..9de1020 100644 --- a/crates/js/src/service_worker.rs +++ b/crates/js/src/service_worker.rs @@ -675,6 +675,23 @@ mod tests { assert!(loaded.records()[0].script_changed(b"body-v2")); } + #[test] + fn registering_existing_scope_replaces_script_bytes_for_update_detection() { + let mut store = RegistrationStore::new(); + let base = base_url(); + store + .register(&base, "/sw-v1.js", Some("/app/"), b"body-v1".to_vec()) + .unwrap(); + let updated = store + .register(&base, "/sw-v2.js", Some("/app/"), b"body-v2".to_vec()) + .unwrap(); + + assert_eq!(store.records().len(), 1); + assert_eq!(updated.script_url, "https://example.com/sw-v2.js"); + assert_eq!(store.records()[0].script_bytes, b"body-v2"); + assert!(store.records()[0].script_changed(b"body-v3")); + } + fn eval_with_service_worker(source: &str) -> String { let program = Parser::parse(source).expect("parse failed"); let func = compiler::compile(&program).expect("compile failed"); diff --git a/tests/wpt/service-workers/registration-lifecycle.html b/tests/wpt/service-workers/registration-lifecycle.html new file mode 100644 index 0000000..47b2279 --- /dev/null +++ b/tests/wpt/service-workers/registration-lifecycle.html @@ -0,0 +1,19 @@ + + +Service worker registration lifecycle + + + diff --git a/tests/wpt/service-workers/security-and-unregister.html b/tests/wpt/service-workers/security-and-unregister.html new file mode 100644 index 0000000..0fc6e2e --- /dev/null +++ b/tests/wpt/service-workers/security-and-unregister.html @@ -0,0 +1,17 @@ + + +Service worker security and unregister + + +