use super::*;
#[test]
fn element_outer_html_getter_serializes_element_and_descendants() -> Result<()> {
let html = r#"
<div id='example'>
<p>Content</p>
<p>Further Elaborated</p>
</div>
<button id='run'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const example = document.getElementById('example');
const serialized = example.outerHTML;
document.getElementById('result').textContent = [
serialized.includes('<div id="example">'),
serialized.includes('<p>Content</p>'),
serialized.includes('<p>Further Elaborated</p>')
].join(':');
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text("#result", "true:true:true")?;
Ok(())
}
#[test]
fn element_outer_html_set_replaces_node_but_old_reference_stays_same_object() -> Result<()> {
let html = r#"
<div id='host'><p id='target'>Original</p></div>
<button id='run'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const p = document.getElementById('target');
const before = p.tagName;
p.outerHTML = '<div id="target">Replaced</div>';
const after = p.tagName;
const live = document.getElementById('target').tagName;
const oldGone = document.querySelectorAll('p#target').length;
document.getElementById('result').textContent =
[before, after, live, oldGone].join(':');
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text("#result", "P:P:DIV:0")?;
Ok(())
}
#[test]
fn element_outer_html_set_null_removes_target_element() -> Result<()> {
let html = r#"
<div id='host'><span id='x'>X</span><b id='y'>Y</b></div>
<button id='run'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const x = document.getElementById('x');
x.outerHTML = null;
const host = document.getElementById('host');
document.getElementById('result').textContent =
host.textContent + ':' +
document.querySelectorAll('#x').length + ':' +
document.querySelectorAll('#y').length;
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text("#result", "Y:0:1")?;
Ok(())
}
#[test]
fn element_outer_html_set_on_detached_element_is_noop() -> Result<()> {
let html = r#"
<button id='run'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const div = document.createElement('div');
div.id = 'detached';
div.textContent = 'A';
div.outerHTML = '<section id="detached">B</section>';
document.getElementById('result').textContent =
div.outerHTML + ':' + div.textContent;
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text("#result", "<div id=\"detached\">A</div>:A")?;
Ok(())
}
#[test]
fn element_outer_html_set_on_document_child_throws() -> Result<()> {
let html = r#"
<html><body><button id='run'>run</button></body></html>
<script>
document.getElementById('run').addEventListener('click', () => {
document.documentElement.outerHTML = '<html><body>X</body></html>';
});
</script>
"#;
let mut h = Harness::from_html(html)?;
match h.click("#run") {
Err(Error::ScriptRuntime(message)) => {
assert!(
message.contains("NoModificationAllowedError"),
"unexpected runtime error message: {message}"
);
}
other => panic!("expected runtime error, got: {other:?}"),
}
Ok(())
}
#[test]
fn element_outer_html_set_sanitizes_scripts_and_dangerous_attrs() -> Result<()> {
let html = r#"
<div id='box'>old</div>
<button id='run'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const box = document.getElementById('box');
box.outerHTML =
'<div id="box">' +
'<script id="evil">document.getElementById("result").textContent = "pwned";</script>' +
'<a id="link" href="javascript:alert(1)" onclick="alert(1)">safe</a>' +
'</div>';
const next = document.getElementById('box');
const link = document.getElementById('link');
document.getElementById('result').textContent =
document.querySelectorAll('#evil').length + ':' +
link.hasAttribute('onclick') + ':' +
link.hasAttribute('href') + ':' +
next.outerHTML;
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text(
"#result",
"0:false:false:<div id=\"box\"><a id=\"link\">safe</a></div>",
)?;
Ok(())
}
#[test]
fn element_outer_html_set_html_8_5_13_2_6_4_9_table_parent_reprocesses_cell_start_tags()
-> Result<()> {
let html = r#"
<table id='scores'>
<caption id='target'>before</caption>
</table>
<button id='run'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const target = document.getElementById('target');
target.outerHTML = '<td id="late">Late</td><th id="bonus">Bonus</th>';
document.getElementById('result').textContent = [
document.querySelectorAll('#scores > tbody').length,
document.querySelectorAll('#scores > tr').length,
document.querySelectorAll('#scores > td').length,
document.querySelectorAll('#scores > th').length,
document.querySelectorAll('#scores > tbody > tr').length,
document.querySelectorAll('#scores > tbody > tr > td').length,
document.querySelectorAll('#scores > tbody > tr > th').length,
Array.from(document.querySelectorAll('#scores > tbody > tr > *'))
.map((cell) => cell.textContent)
.join(',')
].join(':');
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text("#result", "1:0:0:0:1:1:1:Late,Bonus")?;
Ok(())
}
#[test]
fn element_outer_html_set_html_8_5_13_2_6_4_13_tbody_parent_wraps_direct_cells_in_row() -> Result<()>
{
let html = r#"
<table id='scores'>
<tbody id='body'>
<tr id='target'><td>Old</td></tr>
</tbody>
</table>
<button id='run'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const target = document.getElementById('target');
target.outerHTML = '<td id="a">Alpha</td><th id="b">Beta</th>';
document.getElementById('result').textContent = [
document.querySelectorAll('#scores > tbody > tr').length,
document.querySelectorAll('#scores > tbody > td').length,
document.querySelectorAll('#scores > tbody > th').length,
document.querySelectorAll('#scores > tbody > tr > td').length,
document.querySelectorAll('#scores > tbody > tr > th').length,
Array.from(document.querySelectorAll('#scores > tbody > tr > *'))
.map((cell) => cell.textContent)
.join(',')
].join(':');
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text("#result", "1:0:0:1:1:Alpha,Beta")?;
Ok(())
}