Files
rust_browser/tests/js_modules.rs
Zachary D. Rowitsch 6917c062a1
Some checks failed
ci / fast (linux) (push) Has been cancelled
Fix ES module fifth code review issues (§3.4)
Fixes 3 HIGH and 4 MEDIUM issues found in fifth adversarial code review:
- `import * as ns` namespace imports now work (declare in VM env)
- Circular dependencies resolve correctly (pre-create export cells)
- Destructured exports (`export const {a,b} = obj`) collect binding names
- Removed dead ModuleEnvironment writes, dead namespace cache field
- Removed duplicate integration test, enabled #[ignore] interleaving test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 11:05:58 -04:00

405 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();
scheduled.deferred_order.push(DeferredItem::Script(
"var order = []; order.push('defer1');".to_string(),
"<defer1>".to_string(),
));
scheduled.deferred_order.push(DeferredItem::Module(
"order.push('module1'); export const ok = true;".to_string(),
"<module1>".to_string(),
Some("file:///module1.js".to_string()),
));
scheduled.deferred_order.push(DeferredItem::Script(
"order.push('defer2');".to_string(),
"<defer2>".to_string(),
));
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().