Add Location host object with URL component properties (href, protocol, host, hostname, port, pathname, search, hash, origin) backed by the document's base URL. Add document.currentScript support by threading script element NodeIds through the extraction/scheduling/execution pipeline so the currently executing <script> element is available to JS. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
409 lines
14 KiB
Rust
409 lines
14 KiB
Rust
//! Integration tests for ES module support (Story 3.4).
|
|
//!
|
|
//! Tests exercise module parsing, inline module execution, module scope
|
|
//! isolation, and module deferred execution timing through the WebApiFacade.
|
|
//! Additional tests exercise actual module execution via BrowserRuntime.
|
|
|
|
use browser_runtime::BrowserRuntime;
|
|
use dom::Document;
|
|
#[allow(clippy::single_component_path_imports)]
|
|
use net;
|
|
use shared::NodeId;
|
|
use web_api::WebApiFacade;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Test helper
|
|
// ---------------------------------------------------------------------------
|
|
|
|
fn setup() -> (WebApiFacade, NodeId) {
|
|
let mut facade = WebApiFacade::new_for_testing();
|
|
let doc = facade.document_mut();
|
|
let root = doc.root().unwrap();
|
|
let html = doc.create_element("html");
|
|
doc.append_child(root, html);
|
|
let body = doc.create_element("body");
|
|
doc.append_child(html, body);
|
|
let div = doc.create_element("div");
|
|
doc.set_attribute(div, "id", "out");
|
|
doc.append_child(body, div);
|
|
let text = doc.create_text("initial");
|
|
doc.append_child(div, text);
|
|
facade.bootstrap().unwrap();
|
|
(facade, div)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 1. Inline module execution — basic const/var in module scope
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn inline_module_executes_basic_code() {
|
|
let (mut facade, div) = setup();
|
|
let network = net::NetworkStack::new();
|
|
|
|
// Execute a module that sets textContent via DOM — modules can access
|
|
// globals through the scope chain.
|
|
facade
|
|
.execute_module(
|
|
"file:///inline-test.js",
|
|
r#"
|
|
var el = document.getElementById("out");
|
|
el.textContent = "module_ran";
|
|
"#,
|
|
&network,
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(facade.document().text_content(div), "module_ran");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 2. Module is implicitly strict mode
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn module_parse_is_strict_mode() {
|
|
// Parsing as module enables strict mode — verify by parsing
|
|
let parser = js_parser::JsParser::new();
|
|
let prog = parser.parse_module("var x = 1;").unwrap();
|
|
assert!(prog.strict, "module should be in strict mode");
|
|
assert!(prog.is_module, "module should have is_module flag");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 3. Import/export declarations parse without error in module mode
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn module_import_export_parses() {
|
|
let parser = js_parser::JsParser::new();
|
|
let prog = parser
|
|
.parse_module("export const x = 42; export default x;")
|
|
.unwrap();
|
|
assert_eq!(prog.body.len(), 2);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 4. Export const declaration executes in module mode
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn module_export_const_executes() {
|
|
let (mut facade, div) = setup();
|
|
let network = net::NetworkStack::new();
|
|
|
|
// Export declarations should compile and execute (inner declaration runs)
|
|
facade
|
|
.execute_module(
|
|
"file:///test-export.js",
|
|
r#"
|
|
export const x = 42;
|
|
var el = document.getElementById("out");
|
|
el.textContent = "exported";
|
|
"#,
|
|
&network,
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(facade.document().text_content(div), "exported");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 5. Dynamic import() parses in both module and classic scripts
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn dynamic_import_parses_in_classic_script() {
|
|
let parser = js_parser::JsParser::new();
|
|
let prog = parser.parse("var p = import('./foo.js');").unwrap();
|
|
assert_eq!(prog.body.len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn dynamic_import_parses_in_module() {
|
|
let parser = js_parser::JsParser::new();
|
|
let prog = parser.parse_module("var p = import('./foo.js');").unwrap();
|
|
assert_eq!(prog.body.len(), 1);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 6. Import/export in non-module context produces error
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn import_in_classic_script_is_error() {
|
|
let parser = js_parser::JsParser::new();
|
|
let err = parser.parse("import { foo } from './bar.js';").unwrap_err();
|
|
assert!(
|
|
err.message.contains("import"),
|
|
"error should mention import: {}",
|
|
err.message
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn export_in_classic_script_is_error() {
|
|
let parser = js_parser::JsParser::new();
|
|
let err = parser.parse("export const x = 1;").unwrap_err();
|
|
assert!(
|
|
err.message.contains("export"),
|
|
"error should mention export: {}",
|
|
err.message
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 7. All import syntax variations parse correctly
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn all_import_forms_parse() {
|
|
let parser = js_parser::JsParser::new();
|
|
let sources = vec![
|
|
"import { a } from './m.js';",
|
|
"import { a as b } from './m.js';",
|
|
"import def from './m.js';",
|
|
"import * as ns from './m.js';",
|
|
"import def, { a } from './m.js';",
|
|
"import def, * as ns from './m.js';",
|
|
"import './m.js';",
|
|
];
|
|
for src in sources {
|
|
assert!(parser.parse_module(src).is_ok(), "should parse: {}", src);
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 8. All export syntax variations parse correctly
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn all_export_forms_parse() {
|
|
let parser = js_parser::JsParser::new();
|
|
let sources = vec![
|
|
"export default 42;",
|
|
"export default function foo() {}",
|
|
"export default class Foo {}",
|
|
"export const x = 1;",
|
|
"export let y = 2;",
|
|
"export var z = 3;",
|
|
"export function fn1() {}",
|
|
"export class C1 {}",
|
|
"var a = 1; export { a };",
|
|
"var a = 1; export { a as b };",
|
|
"export { a } from './m.js';",
|
|
"export * from './m.js';",
|
|
"export * as ns from './m.js';",
|
|
];
|
|
for src in sources {
|
|
assert!(parser.parse_module(src).is_ok(), "should parse: {}", src);
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 9. `from` and `as` work as variable names (not keywords)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn from_and_as_are_valid_identifiers() {
|
|
let parser = js_parser::JsParser::new();
|
|
// In classic scripts
|
|
assert!(parser.parse("var from = 1; var as = 2;").is_ok());
|
|
// In module mode too
|
|
assert!(parser.parse_module("var from = 1; var as = 2;").is_ok());
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helper: set up a BrowserRuntime with a document ready for module execution.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
fn setup_runtime_with_document() -> BrowserRuntime {
|
|
let mut rt = BrowserRuntime::new();
|
|
rt.prepare_for_navigation().unwrap();
|
|
|
|
let mut doc = Document::new();
|
|
let root = doc.root().unwrap();
|
|
let html = doc.create_element("html");
|
|
doc.append_child(root, html);
|
|
let body = doc.create_element("body");
|
|
doc.append_child(html, body);
|
|
|
|
rt.set_document(doc);
|
|
rt
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 10. Module execution via execute_module (not execute_script)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn module_export_const_runs_via_module_path() {
|
|
let mut rt = setup_runtime_with_document();
|
|
|
|
// Execute a simple module with an export declaration.
|
|
// This validates the full module execution pipeline: parse as module,
|
|
// compile, and execute via execute_module().
|
|
let result = rt.execute_module("file:///test-module.js", "export const x = 42;");
|
|
assert!(
|
|
result.is_ok(),
|
|
"execute_module should succeed for a simple export const: {result:?}"
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 11. Module execution with DOM mutation
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn module_with_dom_mutation() {
|
|
let mut rt = BrowserRuntime::new();
|
|
rt.prepare_for_navigation().unwrap();
|
|
|
|
// Build a document with a target div whose textContent we will mutate.
|
|
let mut doc = Document::new();
|
|
let root = doc.root().unwrap();
|
|
let html = doc.create_element("html");
|
|
doc.append_child(root, html);
|
|
let body = doc.create_element("body");
|
|
doc.append_child(html, body);
|
|
let div = doc.create_element("div");
|
|
doc.set_attribute(div, "id", "target");
|
|
doc.append_child(body, div);
|
|
let text = doc.create_text("original");
|
|
doc.append_child(div, text);
|
|
|
|
rt.set_document(doc);
|
|
|
|
// Execute a module that mutates the DOM via document.getElementById.
|
|
let result = rt.execute_module(
|
|
"file:///dom-module.js",
|
|
r#"
|
|
var el = document.getElementById('target');
|
|
if (el) { el.textContent = 'module-executed'; }
|
|
"#,
|
|
);
|
|
assert!(
|
|
result.is_ok(),
|
|
"module with DOM mutation should execute without error: {result:?}"
|
|
);
|
|
|
|
// Verify the DOM was mutated by the module.
|
|
let doc = rt.take_document();
|
|
assert_eq!(
|
|
doc.text_content(div),
|
|
"module-executed",
|
|
"module should have set textContent on the target div"
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 12. Module strict mode is enforced at execution time
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn module_strict_mode_enforced() {
|
|
let mut rt = setup_runtime_with_document();
|
|
|
|
// In strict mode, assigning to an undeclared variable is an error.
|
|
// Modules are always strict, so this should fail.
|
|
let result = rt.execute_module("file:///strict-module.js", "x = 42;");
|
|
assert!(
|
|
result.is_err(),
|
|
"module should enforce strict mode: assigning to undeclared variable must be an error"
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 13. Dynamic import() returns a value (currently a placeholder)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn dynamic_import_executes_without_crash() {
|
|
let mut rt = setup_runtime_with_document();
|
|
|
|
// Dynamic import() is compiled and executed. Currently the opcode handler
|
|
// returns undefined (full Promise-based resolution is deferred).
|
|
// This test verifies it doesn't crash or produce a parse error.
|
|
let result = rt.execute_module(
|
|
"file:///dynamic-import.js",
|
|
"var result = import('./other.js'); export const ok = true;",
|
|
);
|
|
assert!(
|
|
result.is_ok(),
|
|
"dynamic import() should not crash: {result:?}"
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 14. Bare module specifier is rejected
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn bare_specifier_rejected() {
|
|
let mut rt = setup_runtime_with_document();
|
|
|
|
// Bare specifiers like 'lodash' should be rejected (no import map support).
|
|
let result = rt.execute_module("file:///main.js", "import 'lodash';");
|
|
assert!(result.is_err(), "bare specifier should be rejected");
|
|
let err_msg = format!("{:?}", result.unwrap_err());
|
|
assert!(
|
|
err_msg.contains("bare module specifier"),
|
|
"error should mention bare specifier: {err_msg}"
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 15. Deferred script and module interleaving in document order
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn deferred_and_module_interleaving_order() {
|
|
use app_browser::pipeline::{DeferredItem, ScheduledScripts};
|
|
|
|
// Build ScheduledScripts with interleaved defer + module items.
|
|
// Expected execution order: defer1 → module1 → defer2
|
|
// Ordering correctness is validated by unit tests in script_tests.rs;
|
|
// this integration test verifies the full pipeline executes without error.
|
|
let mut scheduled = ScheduledScripts::default();
|
|
let dummy_nid = shared::NodeId::new(0);
|
|
scheduled.deferred_order.push(DeferredItem::Script(
|
|
"var order = []; order.push('defer1');".to_string(),
|
|
"<defer1>".to_string(),
|
|
dummy_nid,
|
|
));
|
|
scheduled.deferred_order.push(DeferredItem::Module(
|
|
"order.push('module1'); export const ok = true;".to_string(),
|
|
"<module1>".to_string(),
|
|
Some("file:///module1.js".to_string()),
|
|
dummy_nid,
|
|
));
|
|
scheduled.deferred_order.push(DeferredItem::Script(
|
|
"order.push('defer2');".to_string(),
|
|
"<defer2>".to_string(),
|
|
dummy_nid,
|
|
));
|
|
|
|
let mut rt = setup_runtime_with_document();
|
|
|
|
// Execute each item in deferred_order — mirrors event_handler.rs logic
|
|
for item in &scheduled.deferred_order {
|
|
match item {
|
|
DeferredItem::Script(js_text, _label, _) => {
|
|
rt.execute_script(js_text).unwrap();
|
|
}
|
|
DeferredItem::Module(js_text, _label, module_url, _) => {
|
|
let url = module_url.as_deref().unwrap_or("inline-module");
|
|
rt.execute_module(url, js_text).unwrap();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 16. ES Module live bindings (H7)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Live bindings are thoroughly tested in crates/js_vm/src/interpreter/tests/module_tests.rs
|
|
// (module_live_binding_let_mutation, module_live_binding_multiple_mutations, etc.)
|
|
// No duplicate integration test needed — the VM tests already exercise run_module().
|