use std::io::Write;
use std::path::{Path, PathBuf};
use super::assets::trace_assets;
use super::bundle::open_private;
use super::jsonfmt::{js_string, utf16_len, Json, JsonObject};
use super::raw_reader::{diff_controls, index_of, read_raw_trace, RawTrace};
use super::reader::TraceError;
use super::schema::TraceFiles;
pub const DEFAULT_MAX_INLINE_BYTES: usize = 8 * 1024 * 1024;
pub fn replay_summary(manifest: &JsonObject) -> String {
let replay = manifest.get("replay");
let on = |name: &str| {
replay
.and_then(|replay| replay.get(name))
.is_some_and(Json::truthy)
};
let covered: Vec<&str> = [
("mutations", "DOM mutations"),
("childListPositions", "insertion positions and removals"),
("liveState", "live control state"),
("identifiers", "page and frame identities"),
]
.into_iter()
.filter(|(name, _)| on(name))
.map(|(_, label)| label)
.collect();
if covered.is_empty() {
"partial diagnostic replay: checkpoints only, nothing between them".to_string()
} else {
format!(
"partial diagnostic replay: checkpoints, {}",
covered.join(", ")
)
}
}
fn embed(data: &Json) -> String {
data.to_compact()
.replace('<', "\\u003c")
.replace('>', "\\u003e")
.replace('\u{2028}', "\\u2028")
.replace('\u{2029}', "\\u2029")
}
fn collect_viewer_data(opened: &RawTrace, max_inline: usize) -> Result<Json, TraceError> {
let mut html = JsonObject::new();
let mut state = JsonObject::new();
let mut mutations = JsonObject::new();
let mut diffs = JsonObject::new();
let mut elided = Vec::new();
for checkpoint in &opened.checkpoints {
let key = js_string(checkpoint.get("index"));
let index = index_of(checkpoint);
let body = match index {
Some(index) => opened.html(index)?,
None => None,
};
match body {
Some(body) if utf16_len(&body) > max_inline => {
elided.push(checkpoint.get("index").cloned().unwrap_or(Json::Null));
html.insert(key.clone(), Json::Null);
}
body => html.insert(key.clone(), Json::from(body)),
}
let captured = match index {
Some(index) => opened.state(index)?,
None => None,
};
state.insert(key.clone(), captured.unwrap_or(Json::Null));
let batches = match index {
Some(index) => opened.mutations(index)?,
None => Vec::new(),
};
mutations.insert(key, Json::Array(batches));
}
for checkpoint in &opened.checkpoints {
let key = js_string(checkpoint.get("index"));
let previous = checkpoint
.get("index")
.and_then(Json::as_f64)
.map(|index| js_string(Some(&Json::Number(index - 1.0))))
.and_then(|previous| state.get(&previous))
.filter(|previous| previous.truthy());
let changes = match previous {
Some(previous) => diff_controls(Some(previous), state.get(&key))
.into_iter()
.map(Json::Object)
.collect(),
None => Vec::new(),
};
diffs.insert(key, Json::Array(changes));
}
let checkpoints = opened
.checkpoints
.iter()
.cloned()
.map(Json::Object)
.collect::<Vec<_>>();
Ok(Json::Object(
JsonObject::new()
.with("manifest", opened.manifest.clone())
.with("events", opened.events.clone())
.with("checkpoints", checkpoints)
.with("html", html)
.with("state", state)
.with("mutations", mutations)
.with("diffs", diffs)
.with("elided", elided),
))
}
fn or_text(value: Option<&Json>, fallback: &str) -> String {
match value {
None | Some(Json::Null) => fallback.to_string(),
value => js_string(value),
}
}
pub(crate) fn render_raw_viewer(
opened: &RawTrace,
max_inline: usize,
) -> Result<String, TraceError> {
let data = collect_viewer_data(opened, max_inline)?;
let manifest = &opened.manifest;
let counts = manifest.get("counts");
let count = |name: &str| or_text(counts.and_then(|counts| counts.get(name)), "0");
let assets = &trace_assets().viewer;
Ok(format!(
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<!-- Inert by default: no network, no captured scripts, images only from data URLs. -->
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; frame-src 'self' data:;">
<title>Browser Commander trace</title>
<style>{style}</style>
</head>
<body>
<header>
<h1>Browser Commander trace</h1>
<div class="meta">
mode {mode} · outcome {outcome} · engine {engine} ·
{checkpoints} checkpoints · {events} events ·
{dropped} dropped · started {started}
</div>
<div class="meta replay">{replay}</div>
</header>
<aside><ol id="timeline"></ol></aside>
<main>
<div class="controls">
<button id="play" type="button">Replay mutations</button>
<button id="step-forward" type="button">Step</button>
<button id="reset" type="button">Reset</button>
<span id="step"></span>
</div>
<iframe id="stage" sandbox referrerpolicy="no-referrer" title="captured page"></iframe>
<div class="panel" id="details"></div>
<div class="panel" id="diff"></div>
</main>
<script id="trace-data" type="application/json">{data}</script>
<script>{script}</script>
</body>
</html>
"#,
style = assets.style,
mode = js_string(manifest.get("mode")),
outcome = js_string(manifest.get("outcome")),
engine = or_text(manifest.get("engine"), "unknown"),
checkpoints = count("checkpoints"),
events = count("events"),
dropped = or_text(manifest.get("dropped"), "0"),
started = or_text(manifest.get("startedAt"), "unknown"),
replay = replay_summary(manifest),
data = embed(&data),
script = assets.script,
))
}
pub fn render_trace_viewer(
bundle: impl AsRef<Path>,
max_inline_bytes: usize,
) -> Result<String, TraceError> {
render_raw_viewer(&read_raw_trace(bundle.as_ref())?, max_inline_bytes)
}
pub fn write_trace_viewer(bundle: impl AsRef<Path>) -> Result<PathBuf, TraceError> {
let opened = read_raw_trace(bundle.as_ref())?;
let document = render_raw_viewer(&opened, DEFAULT_MAX_INLINE_BYTES)?;
let target = opened.path.join(TraceFiles::VIEWER);
open_private(&target, false)
.and_then(|mut file| file.write_all(document.as_bytes()))
.map_err(|source| TraceError::Io {
path: target.clone(),
source,
})?;
Ok(target)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn replay_summary_names_what_is_covered() {
let none = JsonObject::new();
assert_eq!(
replay_summary(&none),
"partial diagnostic replay: checkpoints only, nothing between them"
);
let some = JsonObject::new().with(
"replay",
JsonObject::new()
.with("mutations", true)
.with("identifiers", true),
);
assert_eq!(
replay_summary(&some),
"partial diagnostic replay: checkpoints, DOM mutations, page and frame identities"
);
}
#[test]
fn embedded_data_cannot_close_its_script() {
let data = Json::from("</script>\u{2028}");
assert_eq!(embed(&data), "\"\\u003c/script\\u003e\\u2028\"");
}
}