#[path = "../src/loopback_modules.rs"]
mod loopback_modules;
use loopback_modules::{LoopbackFetcher, serve_modules};
use blitz_dom::DocumentConfig;
use blitz_script::ScriptDocument;
fn config_with_base(base_url: &str) -> DocumentConfig {
DocumentConfig {
base_url: Some(base_url.to_owned()),
..Default::default()
}
}
fn eval_string(doc: &mut ScriptDocument, expression: &str) -> String {
match doc.eval_json(expression) {
Ok(serde_json::Value::String(value)) => value,
other => panic!("expected {expression} to be a string, got {other:?}"),
}
}
#[test]
fn an_inline_module_is_parsed_in_module_goal() {
let mut doc = ScriptDocument::from_html(
r#"<script type="module">
const value = await Promise.resolve("module ran");
globalThis.result = value;
</script>"#,
DocumentConfig::default(),
);
doc.run_until_idle().expect("script startup completes");
assert_eq!(eval_string(&mut doc, "globalThis.result"), "module ran");
}
#[test]
fn a_classic_script_still_runs_unchanged() {
let mut doc = ScriptDocument::from_html(
r#"<div id="root"></div>
<script>
const el = document.createElement("span");
el.textContent = "classic";
document.getElementById("root").appendChild(el);
globalThis.classicRan = "yes";
</script>"#,
DocumentConfig::default(),
);
doc.run_until_idle().expect("script startup completes");
assert_eq!(eval_string(&mut doc, "globalThis.classicRan"), "yes");
assert_eq!(
eval_string(&mut doc, "document.getElementById('root').textContent"),
"classic"
);
}
#[test]
fn import_meta_url_is_the_module_url() {
let mut doc = ScriptDocument::from_html(
r#"<script type="module">globalThis.metaUrl = import.meta.url;</script>"#,
config_with_base("https://example.invalid/page/index.html"),
);
doc.run_until_idle().expect("script startup completes");
assert_eq!(
eval_string(&mut doc, "globalThis.metaUrl"),
"https://example.invalid/page/index.html"
);
}
#[test]
fn a_module_imports_a_relative_module_over_the_fetcher() {
let (origin, server) = serve_modules(vec![
(
"/app.js",
r#"import { greeting } from "./dep.js";
globalThis.result = greeting + " from the loader";"#
.to_owned(),
),
("/dep.js", r#"export const greeting = "hello";"#.to_owned()),
]);
let mut doc = ScriptDocument::from_html(
r#"<script type="module" src="/app.js"></script>"#,
config_with_base(&format!("{origin}/index.html")),
)
.with_fetcher(LoopbackFetcher);
doc.run_until_idle().expect("script startup completes");
assert_eq!(
eval_string(&mut doc, "globalThis.result"),
"hello from the loader"
);
let requested = server.join().expect("the server thread finishes");
assert!(
requested.iter().any(|path| path == "/dep.js"),
"the imported module should have been fetched, saw: {requested:?}"
);
}
#[test]
fn a_module_imported_twice_is_one_instance() {
let (origin, server) = serve_modules(vec![
(
"/app.js",
r#"import { bump, count } from "./counter.js";
import "./sibling.js";
bump();
globalThis.result = String(count());"#
.to_owned(),
),
(
"/sibling.js",
r#"import { bump } from "./counter.js";
bump();"#
.to_owned(),
),
(
"/counter.js",
r#"let n = 0;
export function bump() { n += 1; }
export function count() { return n; }"#
.to_owned(),
),
]);
let mut doc = ScriptDocument::from_html(
r#"<script type="module" src="/app.js"></script>"#,
config_with_base(&format!("{origin}/index.html")),
)
.with_fetcher(LoopbackFetcher);
doc.run_until_idle().expect("script startup completes");
assert_eq!(eval_string(&mut doc, "globalThis.result"), "2");
let requested = server.join().expect("the server thread finishes");
let counter_fetches = requested
.iter()
.filter(|path| *path == "/counter.js")
.count();
assert_eq!(
counter_fetches, 1,
"the shared module should be fetched once, saw: {requested:?}"
);
}
#[test]
fn a_nomodule_fallback_is_skipped() {
let mut doc = ScriptDocument::from_html(
r#"<script type="module">globalThis.ran = "module";</script>
<script nomodule>globalThis.ran = "fallback";</script>"#,
DocumentConfig::default(),
);
doc.run_until_idle().expect("script startup completes");
assert_eq!(eval_string(&mut doc, "globalThis.ran"), "module");
}
#[test]
fn an_import_map_resolves_a_bare_specifier() {
let (origin, server) = serve_modules(vec![
(
"/app.js",
r#"import { greeting } from "shared";
globalThis.result = greeting;"#
.to_owned(),
),
(
"/vendor/shared.js",
r#"export const greeting = "mapped";"#.to_owned(),
),
]);
let mut doc = ScriptDocument::from_html(
r#"<script type="importmap">
{"imports": {"shared": "/vendor/shared.js"}}
</script>
<script type="module" src="/app.js"></script>"#,
config_with_base(&format!("{origin}/index.html")),
)
.with_fetcher(LoopbackFetcher);
doc.run_until_idle().expect("script startup completes");
assert_eq!(eval_string(&mut doc, "globalThis.result"), "mapped");
let requested = server.join().expect("the server thread finishes");
assert!(
requested.iter().any(|path| path == "/vendor/shared.js"),
"the mapped URL should have been fetched, saw: {requested:?}"
);
}
#[test]
fn dynamic_import_from_a_classic_script_resolves_against_the_script() {
let (origin, server) = serve_modules(vec![
(
"/assets/entry.js",
r#"globalThis.ready = import("./chunk.js").then((m) => {
globalThis.result = m.value;
});"#
.to_owned(),
),
(
"/assets/chunk.js",
r#"export const value = "dynamic";"#.to_owned(),
),
]);
let mut doc = ScriptDocument::from_html(
r#"<script src="/assets/entry.js"></script>"#,
config_with_base(&format!("{origin}/index.html")),
)
.with_fetcher(LoopbackFetcher);
doc.run_until_idle().expect("script startup completes");
assert_eq!(eval_string(&mut doc, "globalThis.result"), "dynamic");
let requested = server.join().expect("the server thread finishes");
assert!(
requested.iter().any(|path| path == "/assets/chunk.js"),
"the chunk should be resolved beside its script, saw: {requested:?}"
);
}
#[test]
fn an_unmapped_bare_specifier_reports_what_is_wrong() {
let mut doc = ScriptDocument::from_html(
r#"<script type="module">
globalThis.error = "";
try {
await import("preact");
} catch (e) {
globalThis.error = String(e);
}
</script>"#,
config_with_base("https://example.invalid/index.html"),
);
doc.run_until_idle().expect("script startup completes");
let error = eval_string(&mut doc, "globalThis.error");
assert!(
error.contains("bare module specifier") && error.contains("preact"),
"the error should name the specifier and the reason, got: {error}"
);
}
#[test]
fn a_json_module_exports_its_document_as_the_default() {
let (origin, server) = serve_modules(vec![
(
"/app.js",
r#"import config from "./config.json" with { type: "json" };
globalThis.result = config.title;"#
.to_owned(),
),
("/config.json", r#"{"title": "from json"}"#.to_owned()),
]);
let mut doc = ScriptDocument::from_html(
r#"<script type="module" src="/app.js"></script>"#,
config_with_base(&format!("{origin}/index.html")),
)
.with_fetcher(LoopbackFetcher);
doc.run_until_idle().expect("script startup completes");
assert_eq!(eval_string(&mut doc, "globalThis.result"), "from json");
let _ = server.join();
}
#[test]
fn a_module_awaiting_a_timer_settles_on_a_later_poll() {
let mut doc = ScriptDocument::from_html(
r#"<script type="module">
await new Promise((resolve) => setTimeout(resolve, 1));
globalThis.result = "settled";
</script>"#,
DocumentConfig::default(),
);
doc.run_until_idle().expect("script startup completes");
assert_eq!(eval_string(&mut doc, "globalThis.result"), "settled");
}
#[test]
fn deferred_scripts_run_after_the_parser_blocking_ones() {
let (origin, server) = serve_modules(vec![
(
"/m.js",
r#"globalThis.order.push("m.js external module");"#.to_owned(),
),
(
"/c.js",
r#"globalThis.order.push("c.js external classic");"#.to_owned(),
),
(
"/d.js",
r#"globalThis.order.push("d.js external classic defer");"#.to_owned(),
),
]);
let mut doc = ScriptDocument::from_html(
r#"<script>
globalThis.order = [];
document.addEventListener("DOMContentLoaded", () => {
globalThis.order.push("DOMContentLoaded");
});
</script>
<script type="module">globalThis.order.push("A inline module");</script>
<script>globalThis.order.push("B inline classic");</script>
<script type="module" src="/m.js"></script>
<script src="/c.js"></script>
<script defer src="/d.js"></script>
<script>globalThis.order.push("C inline classic after everything");</script>
<script type="module">globalThis.order.push("D inline module last");</script>"#,
config_with_base(&format!("{origin}/index.html")),
)
.with_fetcher(LoopbackFetcher);
doc.run_until_idle().expect("script startup completes");
assert_eq!(
eval_string(&mut doc, "globalThis.order.join('|')"),
[
"B inline classic",
"c.js external classic",
"C inline classic after everything",
"A inline module",
"m.js external module",
"d.js external classic defer",
"D inline module last",
"DOMContentLoaded",
]
.join("|")
);
let _ = server.join();
}