use super::*;
#[test]
fn attribute_reflection_html_2_3_1_boolean_attributes_use_presence_not_token_value() -> Result<()> {
let html = r#"
<input id='field' required='false'>
<video id='media' controls='false'></video>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const field = document.getElementById('field');
const media = document.getElementById('media');
const initial =
field.required + ':' +
field.hasAttribute('required') + ':' +
field.getAttribute('required') + ':' +
media.controls + ':' +
media.hasAttribute('controls');
field.required = false;
media.controls = false;
const removed =
field.required + ':' +
field.hasAttribute('required') + ':' +
media.controls + ':' +
media.hasAttribute('controls');
field.required = true;
media.controls = true;
const restored =
field.required + ':' +
field.hasAttribute('required') + ':' +
media.controls + ':' +
media.hasAttribute('controls');
document.getElementById('result').textContent =
initial + '|' + removed + '|' + restored;
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text(
"#result",
"true:true:false:true:true|false:false:false:false|true:true:true:true",
)?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_3_2_enumerated_content_editable_normalizes_and_rejects_invalid_assignment()
-> Result<()> {
let html = r#"
<div id='box' contenteditable='bogus'>editable</div>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const initial =
document.getElementById('box').contentEditable + ':' +
document.getElementById('box').getAttribute('contenteditable');
document.getElementById('box').contentEditable = 'plaintext-only';
const valid =
document.getElementById('box').contentEditable + ':' +
document.getElementById('box').getAttribute('contenteditable');
let invalid = 'no-error';
try {
document.getElementById('box').contentEditable = 'definitely-invalid';
} catch (err) {
invalid = String(err).indexOf('SyntaxError') >= 0 ? 'syntax' : String(err);
}
const afterInvalid =
document.getElementById('box').contentEditable + ':' +
document.getElementById('box').getAttribute('contenteditable');
document.getElementById('result').textContent =
initial + '|' + valid + '|' + invalid + '|' + afterInvalid;
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text(
"#result",
"inherit:bogus|plaintext-only:plaintext-only|syntax|plaintext-only:plaintext-only",
)?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_3_3_numeric_tabindex_parses_and_serializes_through_shared_logic()
-> Result<()> {
let html = r#"
<div id='box' tabindex='not-a-number'>panel</div>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const box = document.getElementById('box');
const initial = box.tabIndex + ':' + box.getAttribute('tabindex');
box.tabIndex = 7.9;
const fromFloat = box.tabIndex + ':' + box.getAttribute('tabindex');
box.tabIndex = '12';
const fromString = box.tabIndex + ':' + box.getAttribute('tabindex');
document.getElementById('result').textContent =
initial + '|' + fromFloat + '|' + fromString;
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text("#result", "-1:not-a-number|7:7|12:12")?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_6_1_url_cite_getter_resolves_relative_against_document_base()
-> Result<()> {
let html = r#"
<base href='https://app.local/'>
<blockquote id='quote' cite='/docs/rfc.html'>Quote</blockquote>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const quote = document.getElementById('quote');
const initial = quote.cite + ':' + quote.getAttribute('cite');
quote.cite = 'notes/spec.html';
const assigned = quote.cite + ':' + quote.getAttribute('cite');
quote.removeAttribute('cite');
const removed = quote.cite + ':' + (quote.getAttribute('cite') === null);
document.getElementById('result').textContent =
initial + '|' + assigned + '|' + removed;
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text(
"#result",
"https://app.local/docs/rfc.html:/docs/rfc.html|https://app.local/notes/spec.html:notes/spec.html|:true",
)?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_3_2_draggable_enumerated_defaults_and_keyword_normalization_work()
-> Result<()> {
let html = r#"
<a id='link' href='/docs/spec'>spec</a>
<img id='img' alt='preview'>
<div id='box' draggable='auto'>box</div>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const link = document.getElementById('link');
const img = document.getElementById('img');
const box = document.getElementById('box');
const initial = link.draggable + ':' + img.draggable + ':' + box.draggable;
box.setAttribute('draggable', 'TRUE');
const normalizedTrue = box.draggable;
box.setAttribute('draggable', 'invalid-token');
const fallbackDefault = box.draggable;
link.draggable = false;
const assignedFalse = link.draggable + ':' + link.getAttribute('draggable');
link.draggable = true;
const assignedTrue = link.draggable + ':' + link.getAttribute('draggable');
document.getElementById('result').textContent =
initial + '|' + normalizedTrue + '|' + fallbackDefault + '|' +
assignedFalse + '|' + assignedTrue;
});
</script>
"#;
let mut h = Harness::from_html_with_url("https://app.local/index.html", html)?;
h.click("#run")?;
h.assert_text(
"#result",
"true:true:false|true|false|false:false|true:true",
)?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_3_2_spellcheck_and_translate_enumerated_defaults_and_keyword_normalization_work()
-> Result<()> {
let html = r#"
<textarea id='ta'></textarea>
<div id='editable' contenteditable='true'>editable</div>
<div id='plain'>plain</div>
<p id='trans' translate='maybe'>text</p>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const ta = document.getElementById('ta');
const editable = document.getElementById('editable');
const plain = document.getElementById('plain');
const trans = document.getElementById('trans');
const initial =
ta.spellcheck + ':' +
editable.spellcheck + ':' +
plain.spellcheck + ':' +
trans.translate;
plain.spellcheck = true;
const spellcheckAssigned = plain.spellcheck + ':' + plain.getAttribute('spellcheck');
trans.translate = false;
const translateFalse = trans.translate + ':' + trans.getAttribute('translate');
trans.setAttribute('translate', 'YES');
const translateTrue = trans.translate + ':' + trans.getAttribute('translate');
document.getElementById('result').textContent =
initial + '|' + spellcheckAssigned + '|' + translateFalse + '|' + translateTrue;
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text(
"#result",
"true:true:false:true|true:true|false:no|true:YES",
)?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_6_1_url_form_action_getter_resolves_submitter_owner_and_document_defaults()
-> Result<()> {
let html = r#"
<form id='f' action='/submit'></form>
<button id='owned-button' type='submit' form='f'>submit</button>
<input id='owned-input' type='submit' form='f' value='send'>
<button id='orphan-button' type='submit'>orphan</button>
<input id='orphan-input' type='submit' value='orphan-input'>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const form = document.getElementById('f');
const ownedButton = document.getElementById('owned-button');
const ownedInput = document.getElementById('owned-input');
const orphanButton = document.getElementById('orphan-button');
const orphanInput = document.getElementById('orphan-input');
const initial = [
ownedButton.formAction + ':' + (ownedButton.getAttribute('formaction') === null),
ownedInput.formAction + ':' + (ownedInput.getAttribute('formaction') === null),
orphanButton.formAction + ':' + (orphanButton.getAttribute('formaction') === null),
orphanInput.formAction + ':' + (orphanInput.getAttribute('formaction') === null)
].join(',');
form.action = 'next';
const linked = [ownedButton.formAction, ownedInput.formAction].join(',');
ownedButton.formAction = 'button-submit';
ownedInput.formAction = 'input-submit';
const assigned = [
ownedButton.formAction + ':' + ownedButton.getAttribute('formaction'),
ownedInput.formAction + ':' + ownedInput.getAttribute('formaction')
].join(',');
ownedButton.removeAttribute('formaction');
ownedInput.removeAttribute('formaction');
form.removeAttribute('action');
const removed = [
ownedButton.formAction + ':' + (ownedButton.getAttribute('formaction') === null),
ownedInput.formAction + ':' + (ownedInput.getAttribute('formaction') === null),
orphanButton.formAction,
orphanInput.formAction
].join(',');
document.getElementById('result').textContent =
initial + '|' + linked + '|' + assigned + '|' + removed;
});
</script>
"#;
let mut h = Harness::from_html_with_url("https://app.local/page/index.html", html)?;
h.click("#run")?;
h.assert_text(
"#result",
"https://app.local/submit:true,https://app.local/submit:true,https://app.local/page/index.html:true,https://app.local/page/index.html:true|https://app.local/page/next,https://app.local/page/next|https://app.local/page/button-submit:button-submit,https://app.local/page/input-submit:input-submit|https://app.local/page/index.html:true,https://app.local/page/index.html:true,https://app.local/page/index.html,https://app.local/page/index.html",
)?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_3_3_numeric_size_span_canvas_dimensions_boundary_rules_work()
-> Result<()> {
let html = r#"
<table>
<col id='col' span='0'>
<tr><td>cell</td></tr>
</table>
<select id='sel' size='-2'>
<option>One</option>
</select>
<canvas id='canvas' width='oops' height='-5'></canvas>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const sel = document.getElementById('sel');
const col = document.getElementById('col');
const canvas = document.getElementById('canvas');
const initial =
sel.size + ':' + sel.getAttribute('size') + ':' +
col.span + ':' + col.getAttribute('span') + ':' +
canvas.width + 'x' + canvas.height + ':' +
canvas.getAttribute('width') + ':' + canvas.getAttribute('height');
sel.size = -3;
col.span = 0;
canvas.width = -7;
canvas.height = 5.9;
const updated =
sel.size + ':' + sel.getAttribute('size') + ':' +
col.span + ':' + col.getAttribute('span') + ':' +
canvas.width + 'x' + canvas.height + ':' +
canvas.getAttribute('width') + ':' + canvas.getAttribute('height');
document.getElementById('result').textContent = initial + '|' + updated;
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text("#result", "1:-2:1:0:300x150:oops:-5|1:0:1:1:0x5:0:5")?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_3_2_enumerated_invalid_empty_and_case_variants_follow_default_matrix()
-> Result<()> {
let html = r#"
<a id='link' href='/docs'>docs</a>
<div id='box' draggable='TrUe'>box</div>
<div id='plain'>plain</div>
<p id='trans'>translate</p>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const link = document.getElementById('link');
const box = document.getElementById('box');
const plain = document.getElementById('plain');
const trans = document.getElementById('trans');
const before = [
link.draggable,
box.draggable,
plain.spellcheck,
trans.translate
].join(':');
box.setAttribute('draggable', 'FALSE');
const dragFalse = box.draggable + ':' + box.getAttribute('draggable');
box.setAttribute('draggable', 'invalid');
const dragInvalid = box.draggable;
box.setAttribute('draggable', '');
const dragEmpty = box.draggable;
plain.setAttribute('spellcheck', '');
const spellEmpty = plain.spellcheck + ':' + plain.getAttribute('spellcheck');
plain.setAttribute('spellcheck', 'FaLsE');
const spellFalse = plain.spellcheck + ':' + plain.getAttribute('spellcheck');
plain.setAttribute('spellcheck', 'invalid');
const spellInvalid = plain.spellcheck;
trans.setAttribute('translate', 'NO');
const transNo = trans.translate + ':' + trans.getAttribute('translate');
trans.setAttribute('translate', '');
const transEmpty = trans.translate + ':' + trans.getAttribute('translate');
trans.setAttribute('translate', 'invalid');
const transInvalid = trans.translate;
document.getElementById('result').textContent = [
before,
dragFalse,
dragInvalid,
dragEmpty,
spellEmpty,
spellFalse,
spellInvalid,
transNo,
transEmpty,
transInvalid
].join('|');
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text(
"#result",
"true:true:false:true|false:FALSE|false|false|true:|false:FaLsE|false|false:NO|true:|true",
)?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_6_1_url_action_src_form_action_poster_and_cite_getters_resolve_relative_and_empty_states()
-> Result<()> {
let html = r#"
<form id='f' action='/submit'></form>
<img id='img' src='/img/start.png' alt='preview'>
<button id='submit' type='submit' formaction='/override'>send</button>
<video id='video' poster='/img/poster.png'></video>
<blockquote id='quote' cite='/spec/rfc'></blockquote>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const f = document.getElementById('f');
const img = document.getElementById('img');
const submit = document.getElementById('submit');
const video = document.getElementById('video');
const quote = document.getElementById('quote');
const before = [
f.action + ':' + f.getAttribute('action'),
img.src + ':' + img.getAttribute('src'),
submit.formAction + ':' + submit.getAttribute('formaction'),
video.poster + ':' + video.getAttribute('poster'),
quote.cite + ':' + quote.getAttribute('cite')
].join(',');
f.action = 'next';
img.src = 'img/next.png';
submit.formAction = 'send';
video.poster = 'img/poster-next.png';
quote.cite = 'spec/next';
const assigned = [
f.action + ':' + f.getAttribute('action'),
img.src + ':' + img.getAttribute('src'),
submit.formAction + ':' + submit.getAttribute('formaction'),
video.poster + ':' + video.getAttribute('poster'),
quote.cite + ':' + quote.getAttribute('cite')
].join(',');
f.removeAttribute('action');
img.removeAttribute('src');
submit.removeAttribute('formaction');
video.removeAttribute('poster');
quote.removeAttribute('cite');
const removed = [
f.action + ':' + (f.getAttribute('action') === null),
img.src + ':' + (img.getAttribute('src') === null),
submit.formAction + ':' + (submit.getAttribute('formaction') === null),
video.poster + ':' + (video.getAttribute('poster') === null),
quote.cite + ':' + (quote.getAttribute('cite') === null)
].join(',');
document.getElementById('result').textContent =
before + '|' + assigned + '|' + removed;
});
</script>
"#;
let mut h = Harness::from_html_with_url("https://app.local/base/page.html", html)?;
h.click("#run")?;
h.assert_text(
"#result",
"https://app.local/submit:/submit,https://app.local/img/start.png:/img/start.png,https://app.local/override:/override,https://app.local/img/poster.png:/img/poster.png,https://app.local/spec/rfc:/spec/rfc|https://app.local/base/next:next,https://app.local/base/img/next.png:img/next.png,https://app.local/base/send:send,https://app.local/base/img/poster-next.png:img/poster-next.png,https://app.local/base/spec/next:spec/next|https://app.local/base/page.html:true,:true,https://app.local/base/page.html:true,:true,:true",
)?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_3_3_numeric_col_row_span_and_select_size_clamp_rules_work()
-> Result<()> {
let html = r#"
<table>
<colgroup>
<col id='col' span='5000'>
</colgroup>
<tbody>
<tr><td id='cell' colspan='2000' rowspan='70000'>cell</td></tr>
</tbody>
</table>
<select id='sel'>
<option>One</option>
</select>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const col = document.getElementById('col');
const cell = document.getElementById('cell');
const sel = document.getElementById('sel');
const initial = [
col.span + ':' + col.getAttribute('span'),
cell.colSpan + ':' + cell.getAttribute('colspan'),
cell.rowSpan + ':' + cell.getAttribute('rowspan'),
sel.size + ':' + (sel.getAttribute('size') === null)
].join(',');
col.span = 5000;
cell.colSpan = 0;
cell.rowSpan = 0;
sel.size = -99;
const clampedFloor = [
col.span + ':' + col.getAttribute('span'),
cell.colSpan + ':' + cell.getAttribute('colspan'),
cell.rowSpan + ':' + cell.getAttribute('rowspan'),
sel.size + ':' + sel.getAttribute('size')
].join(',');
cell.colSpan = 4096;
cell.rowSpan = 999999;
sel.size = 3.9;
const clampedUpper = [
cell.colSpan + ':' + cell.getAttribute('colspan'),
cell.rowSpan + ':' + cell.getAttribute('rowspan'),
sel.size + ':' + sel.getAttribute('size')
].join(',');
document.getElementById('result').textContent =
initial + '|' + clampedFloor + '|' + clampedUpper;
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text(
"#result",
"1000:5000,1000:2000,65534:70000,1:true|1000:1000,1:1,0:0,1:0|1000:1000,65534:65534,3:3",
)?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_6_1_url_missing_defaults_for_form_action_img_src_and_anchor_href_work()
-> Result<()> {
let html = r#"
<form id='f'></form>
<img id='img' alt='preview'>
<a id='link'>link</a>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const f = document.getElementById('f');
const img = document.getElementById('img');
const link = document.getElementById('link');
const initial = [
f.action,
img.src,
link.href,
f.getAttribute('action') === null,
img.getAttribute('src') === null,
link.getAttribute('href') === null
].join(':');
f.setAttribute('action', '');
img.setAttribute('src', '');
link.setAttribute('href', '');
const emptyAttrs = [
f.action + ':' + f.getAttribute('action'),
img.src + ':' + img.getAttribute('src'),
link.href + ':' + link.getAttribute('href')
].join(',');
f.removeAttribute('action');
img.removeAttribute('src');
link.removeAttribute('href');
const removed = [
f.action,
img.src,
link.href,
f.getAttribute('action') === null,
img.getAttribute('src') === null,
link.getAttribute('href') === null
].join(':');
document.getElementById('result').textContent =
initial + '|' + emptyAttrs + '|' + removed;
});
</script>
"#;
let mut h = Harness::from_html_with_url("https://app.local/base/page.html", html)?;
h.click("#run")?;
h.assert_text(
"#result",
"https://app.local/base/page.html:::true:true:true|https://app.local/base/page.html:,https://app.local/base/page.html:,https://app.local/base/page.html:|https://app.local/base/page.html:::true:true:true",
)?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_3_3_parser_fast_path_matches_col_row_span_reflection_rules()
-> Result<()> {
let html = r#"
<table>
<colgroup>
<col id='col' span='4'>
</colgroup>
<tbody>
<tr><td id='cell' colspan='2' rowspan='3'>cell</td></tr>
</tbody>
</table>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const cell = document.getElementById('cell');
const col = document.getElementById('col');
const initial = [
cell.colSpan,
cell.rowSpan,
col.span
].join(':');
cell.colSpan = 0;
cell.rowSpan = 70000;
col.span = 0;
const updated = [
cell.colSpan + ':' + cell.getAttribute('colspan'),
cell.rowSpan + ':' + cell.getAttribute('rowspan'),
col.span + ':' + col.getAttribute('span')
].join(',');
document.getElementById('result').textContent = initial + '|' + updated;
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text("#result", "2:3:4|1:1,65534:65534,1:1")?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_3_3_numeric_input_textarea_and_select_size_rows_cols_and_maxlength_work()
-> Result<()> {
let html = r#"
<input id='field' type='text' size='0' maxlength='oops'>
<textarea id='ta' rows='0' cols='-4' maxlength='nan'></textarea>
<select id='sel' size='0'>
<option>One</option>
</select>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const field = document.getElementById('field');
const ta = document.getElementById('ta');
const sel = document.getElementById('sel');
const initial = [
field.size + ':' + field.getAttribute('size'),
field.maxLength + ':' + field.getAttribute('maxlength'),
ta.rows + ':' + ta.getAttribute('rows'),
ta.cols + ':' + ta.getAttribute('cols'),
ta.maxLength + ':' + ta.getAttribute('maxlength'),
sel.size + ':' + sel.getAttribute('size')
].join(',');
field.size = -7;
field.maxLength = 12;
ta.rows = 0;
ta.cols = -3;
ta.maxLength = 9;
sel.size = -2;
const updated = [
field.size + ':' + field.getAttribute('size'),
field.maxLength + ':' + field.getAttribute('maxlength'),
ta.rows + ':' + ta.getAttribute('rows'),
ta.cols + ':' + ta.getAttribute('cols'),
ta.maxLength + ':' + ta.getAttribute('maxlength'),
sel.size + ':' + sel.getAttribute('size')
].join(',');
document.getElementById('result').textContent = initial + '|' + updated;
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text(
"#result",
"20:0,-1:oops,2:0,20:-4,-1:nan,1:0|1:1,12:12,1:1,1:1,9:9,1:0",
)?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_3_3_numeric_input_minlength_and_maxlength_boundary_and_roundtrip_work()
-> Result<()> {
let html = r#"
<input id='field' type='text' minlength='oops' maxlength='oops'>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const field = document.getElementById('field');
const initial = [
field.minLength + ':' + field.getAttribute('minlength'),
field.maxLength + ':' + field.getAttribute('maxlength')
].join(',');
field.minLength = -5;
field.maxLength = -9;
const negative = [
field.minLength + ':' + field.getAttribute('minlength'),
field.maxLength + ':' + field.getAttribute('maxlength')
].join(',');
field.minLength = NaN;
field.maxLength = NaN;
const nan = [
field.minLength + ':' + field.getAttribute('minlength'),
field.maxLength + ':' + field.getAttribute('maxlength')
].join(',');
field.minLength = 'token';
field.maxLength = 'token';
const nonNumericString = [
field.minLength + ':' + field.getAttribute('minlength'),
field.maxLength + ':' + field.getAttribute('maxlength')
].join(',');
field.minLength = '7';
field.maxLength = '12';
const numericString = [
field.minLength + ':' + field.getAttribute('minlength'),
field.maxLength + ':' + field.getAttribute('maxlength')
].join(',');
field.setAttribute('minlength', '-3');
field.setAttribute('maxlength', 'bad');
const fromInvalidAttr = [
field.minLength + ':' + field.getAttribute('minlength'),
field.maxLength + ':' + field.getAttribute('maxlength')
].join(',');
document.getElementById('result').textContent = [
initial,
negative,
nan,
nonNumericString,
numericString,
fromInvalidAttr
].join('|');
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text(
"#result",
"-1:oops,-1:oops|-1:-1,-1:-1|0:0,0:0|0:0,0:0|7:7,12:12|-1:-3,-1:bad",
)?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_3_3_numeric_textarea_rows_cols_upper_bound_and_token_matrix_work()
-> Result<()> {
let html = r#"
<textarea id='ta' rows='NaN' cols='0'></textarea>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const ta = document.getElementById('ta');
const initial = [
ta.rows + ':' + ta.getAttribute('rows'),
ta.cols + ':' + ta.getAttribute('cols')
].join(',');
ta.setAttribute('rows', '-5');
ta.setAttribute('cols', 'words');
const tokenMatrix = [
ta.rows + ':' + ta.getAttribute('rows'),
ta.cols + ':' + ta.getAttribute('cols')
].join(',');
ta.rows = 0;
ta.cols = -2;
const clampedFloor = [
ta.rows + ':' + ta.getAttribute('rows'),
ta.cols + ':' + ta.getAttribute('cols')
].join(',');
ta.rows = 2147483647;
ta.cols = 2147483647;
const upperBound = [
ta.rows + ':' + ta.getAttribute('rows'),
ta.cols + ':' + ta.getAttribute('cols')
].join(',');
ta.rows = 4294967295;
ta.cols = 9000000000;
const clampedUpper = [
ta.rows + ':' + ta.getAttribute('rows'),
ta.cols + ':' + ta.getAttribute('cols')
].join(',');
document.getElementById('result').textContent = [
initial,
tokenMatrix,
clampedFloor,
upperBound,
clampedUpper
].join('|');
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text(
"#result",
"2:NaN,20:0|2:-5,20:words|1:1,1:1|2147483647:2147483647,2147483647:2147483647|2147483647:2147483647,2147483647:2147483647",
)?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_3_2_enumerated_dir_autocapitalize_autocomplete_missing_invalid_and_case_variants_work()
-> Result<()> {
let html = r#"
<div id='plain'>plain</div>
<bdi id='bdi'>bdi</bdi>
<input id='field' type='text'>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const plain = document.getElementById('plain');
const bdi = document.getElementById('bdi');
const field = document.getElementById('field');
const initial = [
plain.dir + ':' + (plain.getAttribute('dir') === null),
bdi.dir + ':' + (bdi.getAttribute('dir') === null),
field.autocapitalize + ':' + (field.getAttribute('autocapitalize') === null),
field.autocomplete + ':' + (field.getAttribute('autocomplete') === null)
].join(',');
plain.setAttribute('dir', 'RtL');
field.setAttribute('autocapitalize', 'WoRdS');
field.setAttribute('autocomplete', 'ON');
const caseVariants = [
plain.dir + ':' + plain.getAttribute('dir'),
field.autocapitalize + ':' + field.getAttribute('autocapitalize'),
field.autocomplete + ':' + field.getAttribute('autocomplete')
].join(',');
plain.setAttribute('dir', 'invalid-dir');
field.setAttribute('autocapitalize', 'unexpected');
field.setAttribute('autocomplete', 'not-a-keyword');
const invalid = [
plain.dir + ':' + plain.getAttribute('dir'),
field.autocapitalize + ':' + field.getAttribute('autocapitalize'),
field.autocomplete + ':' + field.getAttribute('autocomplete')
].join(',');
plain.removeAttribute('dir');
field.removeAttribute('autocapitalize');
field.removeAttribute('autocomplete');
const removed = [
plain.dir + ':' + (plain.getAttribute('dir') === null),
field.autocapitalize + ':' + (field.getAttribute('autocapitalize') === null),
field.autocomplete + ':' + (field.getAttribute('autocomplete') === null)
].join(',');
document.getElementById('result').textContent = [
initial,
caseVariants,
invalid,
removed
].join('|');
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text(
"#result",
":true,auto:true,:true,:true|RtL:RtL,WoRdS:WoRdS,ON:ON|invalid-dir:invalid-dir,unexpected:unexpected,not-a-keyword:not-a-keyword|:true,:true,:true",
)?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_3_2_generic_element_shadow_define_property_delete_and_fast_path_parity_work()
-> Result<()> {
let html = r#"
<div id='box' class='alpha beta' lang='en' dir='rtl'>box</div>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const box = document.getElementById('box');
Object.defineProperty(box, 'id', {
value: 'shadow-id',
enumerable: true,
configurable: true
});
Object.defineProperty(box, 'className', {
value: 'shadow-class',
writable: false,
configurable: true
});
Object.defineProperty(box, 'lang', {
get() { return this.getAttribute('lang') + ':getter'; },
set(value) { this.setAttribute('lang', value + '-set'); },
configurable: true
});
Object.defineProperty(box, 'dir', {
value: 'shadow-dir',
configurable: true
});
const shadow = [
box.id + ':' + box['id'] + ':' + box.getAttribute('id'),
box.className + ':' + box['className'] + ':' + box.getAttribute('class'),
box.lang + ':' + box['lang'] + ':' + box.getAttribute('lang'),
box.dir + ':' + box['dir'] + ':' + box.getAttribute('dir'),
String(Reflect.set(box, 'className', 'ignored') === false),
String(Reflect.set(box, 'lang', 'fr')),
box.lang + ':' + box['lang'] + ':' + box.getAttribute('lang'),
String(Object.keys(box).includes('id')),
String(Object.getOwnPropertyDescriptor(box, 'dir').configurable === true)
].join(',');
const deleted = [
String(delete box.id) + ':' + box.id + ':' + box['id'] + ':' + box.getAttribute('id'),
String(delete box.className) + ':' + box.className + ':' + box['className'] + ':' + box.getAttribute('class'),
String(delete box.lang) + ':' + box.lang + ':' + box['lang'] + ':' + box.getAttribute('lang'),
String(delete box.dir) + ':' + box.dir + ':' + box['dir'] + ':' + box.getAttribute('dir')
].join(',');
document.getElementById('result').textContent = shadow + '|' + deleted;
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text(
"#result",
"shadow-id:shadow-id:box,shadow-class:shadow-class:alpha beta,en:getter:en:getter:en,shadow-dir:shadow-dir:rtl,true,true,fr-set:getter:fr-set:getter:fr-set,true,true|true:box:box:box,true:alpha beta:alpha beta:alpha beta,true:fr-set:fr-set:fr-set,true:rtl:rtl:rtl",
)?;
Ok(())
}
#[test]
fn attribute_reflection_html_3_2_6_remaining_global_attributes_shadow_define_property_delete_and_fast_path_parity_work()
-> Result<()> {
let html = r#"
<div
id='box'
accesskey='h'
autocapitalize='characters'
autocorrect='on'
contenteditable='true'
draggable='true'
enterkeyhint='search'
hidden
inert
inputmode='numeric'
nonce='seed'
popover='auto'
spellcheck='true'
tabindex='4'
title='tip'
translate='no'
>box</div>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const box = document.getElementById('box');
Object.defineProperty(box, 'accessKey', {
value: 'shadow-key',
writable: true,
configurable: true
});
Object.defineProperty(box, 'autocapitalize', {
value: 'shadow-cap',
writable: true,
configurable: true
});
Object.defineProperty(box, 'autocorrect', {
value: 'shadow-correct',
writable: true,
configurable: true
});
Object.defineProperty(box, 'contentEditable', {
value: 'shadow-edit',
writable: true,
configurable: true
});
Object.defineProperty(box, 'draggable', {
value: 'shadow-drag',
writable: true,
configurable: true
});
Object.defineProperty(box, 'enterKeyHint', {
value: 'shadow-enter',
writable: true,
configurable: true
});
Object.defineProperty(box, 'hidden', {
value: 'shadow-hidden',
writable: true,
configurable: true
});
Object.defineProperty(box, 'inert', {
value: 'shadow-inert',
writable: true,
configurable: true
});
Object.defineProperty(box, 'inputMode', {
value: 'shadow-mode',
writable: true,
configurable: true
});
Object.defineProperty(box, 'nonce', {
value: 'shadow-nonce',
writable: true,
configurable: true
});
Object.defineProperty(box, 'popover', {
get() { return this.getAttribute('data-popover-shadow') || 'shadow-pop'; },
set(value) { this.setAttribute('data-popover-shadow', value); },
configurable: true
});
Object.defineProperty(box, 'spellcheck', {
value: 'shadow-spell',
writable: true,
configurable: true
});
Object.defineProperty(box, 'tabIndex', {
value: 'shadow-tab',
writable: true,
configurable: true
});
Object.defineProperty(box, 'title', {
value: 'shadow-title',
writable: true,
configurable: true
});
Object.defineProperty(box, 'translate', {
value: 'shadow-translate',
writable: true,
configurable: true
});
const shadowed = [
box.accessKey + ':' + box['accessKey'] + ':' + box.getAttribute('accesskey'),
box.autocapitalize + ':' + box['autocapitalize'] + ':' + box.getAttribute('autocapitalize'),
box.autocorrect + ':' + box['autocorrect'] + ':' + box.getAttribute('autocorrect'),
box.contentEditable + ':' + box['contentEditable'] + ':' + box.getAttribute('contenteditable'),
box.draggable + ':' + box['draggable'] + ':' + box.getAttribute('draggable'),
box.enterKeyHint + ':' + box['enterKeyHint'] + ':' + box.getAttribute('enterkeyhint'),
box.hidden + ':' + box['hidden'] + ':' + box.hasAttribute('hidden'),
box.inert + ':' + box['inert'] + ':' + box.hasAttribute('inert'),
box.inputMode + ':' + box['inputMode'] + ':' + box.getAttribute('inputmode'),
box.nonce + ':' + box['nonce'] + ':' + box.getAttribute('nonce'),
box.popover + ':' + box['popover'] + ':' + box.getAttribute('popover') + ':' + (box.getAttribute('data-popover-shadow') === null),
box.spellcheck + ':' + box['spellcheck'] + ':' + box.getAttribute('spellcheck'),
box.tabIndex + ':' + box['tabIndex'] + ':' + box.getAttribute('tabindex'),
box.title + ':' + box['title'] + ':' + box.getAttribute('title'),
box.translate + ':' + box['translate'] + ':' + box.getAttribute('translate')
].join(',');
box.accessKey = 'set-key';
box.autocapitalize = 'set-cap';
box.autocorrect = 'set-correct';
box.contentEditable = 'set-edit';
box.draggable = 'set-drag';
box.enterKeyHint = 'set-enter';
box.hidden = 'set-hidden';
box.inert = 'set-inert';
box.inputMode = 'set-mode';
box.nonce = 'set-nonce';
box.popover = 'set-pop';
box.spellcheck = 'set-spell';
box.tabIndex = 'set-tab';
box.title = 'set-title';
box.translate = 'set-translate';
const assigned = [
box.accessKey + ':' + box.getAttribute('accesskey'),
box.autocapitalize + ':' + box.getAttribute('autocapitalize'),
box.autocorrect + ':' + box.getAttribute('autocorrect'),
box.contentEditable + ':' + box.getAttribute('contenteditable'),
box.draggable + ':' + box.getAttribute('draggable'),
box.enterKeyHint + ':' + box.getAttribute('enterkeyhint'),
box.hidden + ':' + box.hasAttribute('hidden'),
box.inert + ':' + box.hasAttribute('inert'),
box.inputMode + ':' + box.getAttribute('inputmode'),
box.nonce + ':' + box.getAttribute('nonce'),
box.popover + ':' + box.getAttribute('popover') + ':' + box.getAttribute('data-popover-shadow'),
box.spellcheck + ':' + box.getAttribute('spellcheck'),
box.tabIndex + ':' + box.getAttribute('tabindex'),
box.title + ':' + box.getAttribute('title'),
box.translate + ':' + box.getAttribute('translate')
].join(',');
const restored = [
String(delete box.accessKey) + ':' + box.accessKey + ':' + box.getAttribute('accesskey'),
String(delete box.autocapitalize) + ':' + box.autocapitalize + ':' + box.getAttribute('autocapitalize'),
String(delete box.autocorrect) + ':' + box.autocorrect + ':' + box.getAttribute('autocorrect'),
String(delete box.contentEditable) + ':' + box.contentEditable + ':' + box.getAttribute('contenteditable'),
String(delete box.draggable) + ':' + box.draggable + ':' + box.getAttribute('draggable'),
String(delete box.enterKeyHint) + ':' + box.enterKeyHint + ':' + box.getAttribute('enterkeyhint'),
String(delete box.inert) + ':' + box.inert + ':' + box.hasAttribute('inert'),
String(delete box.inputMode) + ':' + box.inputMode + ':' + box.getAttribute('inputmode'),
String(delete box.nonce) + ':' + box.nonce + ':' + box.getAttribute('nonce'),
String(delete box.popover) + ':' + box.popover + ':' + box.getAttribute('popover') + ':' + box.getAttribute('data-popover-shadow'),
String(delete box.spellcheck) + ':' + box.spellcheck + ':' + box.getAttribute('spellcheck'),
String(delete box.tabIndex) + ':' + box.tabIndex + ':' + box.getAttribute('tabindex'),
String(delete box.title) + ':' + box.title + ':' + box.getAttribute('title'),
String(delete box.translate) + ':' + box.translate + ':' + box.getAttribute('translate')
].join(',');
document.getElementById('result').textContent =
shadowed + '|' + assigned + '|' + restored;
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text(
"#result",
"shadow-key:shadow-key:h,shadow-cap:shadow-cap:characters,shadow-correct:shadow-correct:on,shadow-edit:shadow-edit:true,shadow-drag:shadow-drag:true,shadow-enter:shadow-enter:search,shadow-hidden:shadow-hidden:true,shadow-inert:shadow-inert:true,shadow-mode:shadow-mode:numeric,shadow-nonce:shadow-nonce:,shadow-pop:shadow-pop:auto:true,shadow-spell:shadow-spell:true,shadow-tab:shadow-tab:4,shadow-title:shadow-title:tip,shadow-translate:shadow-translate:no|set-key:h,set-cap:characters,set-correct:on,set-edit:true,set-drag:true,set-enter:search,set-hidden:true,set-inert:true,set-mode:numeric,set-nonce:,set-pop:auto:set-pop,set-spell:true,set-tab:4,set-title:tip,set-translate:no|true:h:h,true:characters:characters,true:on:on,true:true:true,true:true:true,true:search:search,true:true:true,true:numeric:numeric,true:seed:,true:auto:auto:set-pop,true:true:true,true:4:4,true:tip:tip,true:false:no",
)?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_6_1_generic_reflected_property_shadow_define_property_delete_and_fast_path_parity_work()
-> Result<()> {
let html = r#"
<base href='https://app.local/'>
<label id='lab' for='user'>User</label>
<blockquote id='quote' cite='/docs/spec'>Quote</blockquote>
<details id='panel' open><summary>Open</summary><p>Body</p></details>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const label = document.getElementById('lab');
const quote = document.getElementById('quote');
const panel = document.getElementById('panel');
Object.defineProperty(label, 'htmlFor', {
get() { return this.getAttribute('for') + ':getter'; },
set(value) { this.setAttribute('for', value + '-set'); },
configurable: true
});
Object.defineProperty(quote, 'cite', {
value: 'shadow-cite',
writable: false,
enumerable: true,
configurable: true
});
Object.defineProperty(panel, 'open', {
value: false,
writable: false,
enumerable: true,
configurable: true
});
const shadow = [
document.getElementById('lab').htmlFor + ':' + label['htmlFor'] + ':' + label.getAttribute('for'),
String(Reflect.set(label, 'htmlFor', 'alt')),
document.getElementById('lab').htmlFor + ':' + label.getAttribute('for'),
document.getElementById('quote').cite + ':' + quote['cite'] + ':' + quote.getAttribute('cite'),
String(Reflect.set(quote, 'cite', 'ignored') === false),
String(Object.keys(quote).includes('cite')),
String(document.getElementById('panel').open) + ':' + String(panel['open']) + ':' + String(panel.hasAttribute('open')),
String(Reflect.set(panel, 'open', true) === false)
].join(',');
const deleted = [
String(delete label.htmlFor) + ':' + document.getElementById('lab').htmlFor + ':' + label.getAttribute('for'),
String(delete quote.cite) + ':' + document.getElementById('quote').cite + ':' + quote.getAttribute('cite'),
String(delete panel.open) + ':' + String(document.getElementById('panel').open) + ':' + String(panel.hasAttribute('open'))
].join(',');
document.getElementById('result').textContent = shadow + '|' + deleted;
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text(
"#result",
"user:getter:user:getter:user,true,alt-set:getter:alt-set,shadow-cite:shadow-cite:/docs/spec,true,true,false:false:true,true|true:alt-set:alt-set,true:https://app.local/docs/spec:/docs/spec,true:true:true",
)?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_6_1_url_backed_property_shadow_define_property_delete_and_fast_path_parity_work()
-> Result<()> {
let html = r#"
<a id='link' href='/docs/start'>Link</a>
<audio id='audio' src='/media/track.mp3'></audio>
<video id='video' poster='/img/poster.png'></video>
<form id='owner' action='/submit/fallback'></form>
<button id='submitter' type='submit' form='owner' formaction='/submit/override'>Save</button>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const link = document.getElementById('link');
const audio = document.getElementById('audio');
const video = document.getElementById('video');
const submitter = document.getElementById('submitter');
const seen = {
href: 'shadow-href',
src: 'shadow-src',
poster: 'shadow-poster',
formAction: 'shadow-form'
};
Object.defineProperty(link, 'href', {
get() { return seen.href; },
set(value) { seen.href = 'set:' + value; },
configurable: true
});
Object.defineProperty(audio, 'src', {
get() { return seen.src; },
set(value) { seen.src = 'set:' + value; },
configurable: true
});
Object.defineProperty(video, 'poster', {
get() { return seen.poster; },
set(value) { seen.poster = 'set:' + value; },
configurable: true
});
Object.defineProperty(submitter, 'formAction', {
get() { return seen.formAction; },
set(value) { seen.formAction = 'set:' + value; },
configurable: true
});
link.href = '/docs/next';
audio.src = '/media/next.mp3';
video.poster = '/img/next.png';
submitter.formAction = '/submit/next';
const shadow = [
[link.href, link['href'], seen.href, link.getAttribute('href')].join(','),
[audio.src, audio['src'], seen.src, audio.getAttribute('src')].join(','),
[video.poster, video['poster'], seen.poster, video.getAttribute('poster')].join(','),
[submitter.formAction, submitter['formAction'], seen.formAction, submitter.getAttribute('formaction')].join(',')
].join(';');
Reflect.set(link, 'href', 'reflect-href');
Reflect.set(audio, 'src', 'reflect-src');
Reflect.set(video, 'poster', 'reflect-poster');
Reflect.set(submitter, 'formAction', 'reflect-form');
const reflected = [
[link.href, link['href'], seen.href, link.getAttribute('href')].join(','),
[audio.src, audio['src'], seen.src, audio.getAttribute('src')].join(','),
[video.poster, video['poster'], seen.poster, video.getAttribute('poster')].join(','),
[submitter.formAction, submitter['formAction'], seen.formAction, submitter.getAttribute('formaction')].join(',')
].join(';');
delete link.href;
delete audio.src;
delete video.poster;
delete submitter.formAction;
const restored = [
[link.href, link['href']].join(','),
[audio.src, audio['src']].join(','),
[video.poster, video['poster']].join(','),
[submitter.formAction, submitter['formAction']].join(',')
].join(';');
document.getElementById('result').textContent = [
shadow,
reflected,
restored
].join('|');
});
</script>
"#;
let mut h = Harness::from_html_with_url("https://app.local/base/index.html", html)?;
h.click("#run")?;
h.assert_text(
"#result",
"set:/docs/next,set:/docs/next,set:/docs/next,/docs/start;set:/media/next.mp3,set:/media/next.mp3,set:/media/next.mp3,/media/track.mp3;set:/img/next.png,set:/img/next.png,set:/img/next.png,/img/poster.png;set:/submit/next,set:/submit/next,set:/submit/next,/submit/override|set:reflect-href,set:reflect-href,set:reflect-href,/docs/start;set:reflect-src,set:reflect-src,set:reflect-src,/media/track.mp3;set:reflect-poster,set:reflect-poster,set:reflect-poster,/img/poster.png;set:reflect-form,set:reflect-form,set:reflect-form,/submit/override|https://app.local/docs/start,https://app.local/docs/start;https://app.local/media/track.mp3,https://app.local/media/track.mp3;https://app.local/img/poster.png,https://app.local/img/poster.png;https://app.local/submit/override,https://app.local/submit/override",
)?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_6_1_resource_url_property_shadow_define_property_delete_and_fast_path_parity_work()
-> Result<()> {
let html = r#"
<a id='link' href='/docs' attributionsrc='https://source.test/register'>Link</a>
<img id='photo' src='/img/hero.png' srcset='/img/hero-1x.png 1x, /img/hero-2x.png 2x'>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const link = document.getElementById('link');
const photo = document.getElementById('photo');
const shadow = {
attributionSrc: 'shadow-attr',
srcset: 'shadow-srcset'
};
Object.defineProperty(link, 'attributionSrc', {
get() { return shadow.attributionSrc; },
set(value) { shadow.attributionSrc = 'set:' + value; },
configurable: true
});
Object.defineProperty(photo, 'srcset', {
get() { return shadow.srcset; },
set(value) { shadow.srcset = 'set:' + value; },
configurable: true
});
document.getElementById('link').attributionSrc = 'next-attr';
photo.srcset = 'next-srcset';
const first = [
document.getElementById('link').attributionSrc,
link['attributionSrc'],
shadow.attributionSrc,
link.getAttribute('attributionsrc'),
photo.srcset,
photo['srcset'],
shadow.srcset,
photo.getAttribute('srcset')
].join(':');
Reflect.set(link, 'attributionSrc', 'reflect-attr');
Reflect.set(photo, 'srcset', 'reflect-srcset');
const second = [
document.getElementById('link').attributionSrc,
link['attributionSrc'],
shadow.attributionSrc,
link.getAttribute('attributionsrc'),
photo.srcset,
photo['srcset'],
shadow.srcset,
photo.getAttribute('srcset')
].join(':');
delete link.attributionSrc;
delete photo.srcset;
const third = [
document.getElementById('link').attributionSrc,
link['attributionSrc'],
link.getAttribute('attributionsrc'),
photo.srcset,
photo['srcset'],
photo.getAttribute('srcset')
].join(':');
document.getElementById('result').textContent = [
first,
second,
third
].join('|');
});
</script>
"#;
let mut h = Harness::from_html_with_url("https://app.local/base/index.html", html)?;
h.click("#run")?;
h.assert_text(
"#result",
"set:next-attr:set:next-attr:set:next-attr:https://source.test/register:set:next-srcset:set:next-srcset:set:next-srcset:/img/hero-1x.png 1x, /img/hero-2x.png 2x|set:reflect-attr:set:reflect-attr:set:reflect-attr:https://source.test/register:set:reflect-srcset:set:reflect-srcset:set:reflect-srcset:/img/hero-1x.png 1x, /img/hero-2x.png 2x|https://source.test/register:https://source.test/register:https://source.test/register:/img/hero-1x.png 1x, /img/hero-2x.png 2x:/img/hero-1x.png 1x, /img/hero-2x.png 2x:/img/hero-1x.png 1x, /img/hero-2x.png 2x",
)?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_6_1_non_form_plain_property_shadow_define_property_delete_and_fast_path_parity_work()
-> Result<()> {
let html = r#"
<div id='box' slot='hero' role='note' elementtiming='paint'></div>
<dialog id='dialog' closedby='none'></dialog>
<map id='map' name='zones'></map>
<time id='stamp' datetime='2024-01-01'></time>
<br id='line' clear='left'>
<table><caption id='cap' align='bottom'>Cap</caption></table>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const box = document.getElementById('box');
const dialog = document.getElementById('dialog');
const map = document.getElementById('map');
const stamp = document.getElementById('stamp');
const line = document.getElementById('line');
const cap = document.getElementById('cap');
const shadow = {
slot: 'shadow-slot',
role: 'shadow-role',
elementTiming: 'shadow-paint',
closedBy: 'shadow-closed',
name: 'shadow-name',
dateTime: 'shadow-time',
clear: 'shadow-clear',
align: 'shadow-align'
};
function shadowAccessor(key) {
return {
get() { return shadow[key]; },
set(value) { shadow[key] = 'set:' + value; },
configurable: true
};
}
Object.defineProperty(box, 'slot', shadowAccessor('slot'));
Object.defineProperty(box, 'role', shadowAccessor('role'));
Object.defineProperty(box, 'elementTiming', shadowAccessor('elementTiming'));
Object.defineProperty(dialog, 'closedBy', shadowAccessor('closedBy'));
Object.defineProperty(map, 'name', shadowAccessor('name'));
Object.defineProperty(stamp, 'dateTime', shadowAccessor('dateTime'));
Object.defineProperty(line, 'clear', shadowAccessor('clear'));
Object.defineProperty(cap, 'align', shadowAccessor('align'));
document.getElementById('box').slot = 'next-slot';
box.role = 'group';
box.elementTiming = 'next-paint';
dialog.closedBy = 'any';
map.name = 'next-name';
stamp.dateTime = '2025-05-01';
line.clear = 'right';
cap.align = 'top';
const first = [
[box.slot, box['slot'], shadow.slot, box.getAttribute('slot')].join(','),
[box.role, box['role'], shadow.role, box.getAttribute('role')].join(','),
[box.elementTiming, box['elementTiming'], shadow.elementTiming, box.getAttribute('elementtiming')].join(','),
[dialog.closedBy, dialog['closedBy'], shadow.closedBy, dialog.getAttribute('closedby')].join(','),
[map.name, map['name'], shadow.name, map.getAttribute('name')].join(','),
[stamp.dateTime, stamp['dateTime'], shadow.dateTime, stamp.getAttribute('datetime')].join(','),
[line.clear, line['clear'], shadow.clear, line.getAttribute('clear')].join(','),
[cap.align, cap['align'], shadow.align, cap.getAttribute('align')].join(',')
].join(';');
Reflect.set(box, 'slot', 'reflect-slot');
Reflect.set(box, 'role', 'region');
Reflect.set(box, 'elementTiming', 'reflect-paint');
Reflect.set(dialog, 'closedBy', 'close-request');
Reflect.set(map, 'name', 'reflect-name');
Reflect.set(stamp, 'dateTime', '2030-12-31');
Reflect.set(line, 'clear', 'all');
Reflect.set(cap, 'align', 'left');
const second = [
[box.slot, box['slot'], shadow.slot, box.getAttribute('slot')].join(','),
[box.role, box['role'], shadow.role, box.getAttribute('role')].join(','),
[box.elementTiming, box['elementTiming'], shadow.elementTiming, box.getAttribute('elementtiming')].join(','),
[dialog.closedBy, dialog['closedBy'], shadow.closedBy, dialog.getAttribute('closedby')].join(','),
[map.name, map['name'], shadow.name, map.getAttribute('name')].join(','),
[stamp.dateTime, stamp['dateTime'], shadow.dateTime, stamp.getAttribute('datetime')].join(','),
[line.clear, line['clear'], shadow.clear, line.getAttribute('clear')].join(','),
[cap.align, cap['align'], shadow.align, cap.getAttribute('align')].join(',')
].join(';');
delete box.slot;
delete box.role;
delete box.elementTiming;
delete dialog.closedBy;
delete map.name;
delete stamp.dateTime;
delete line.clear;
delete cap.align;
const third = [
[box.slot, box['slot'], box.getAttribute('slot')].join(','),
[box.role, box['role'], box.getAttribute('role')].join(','),
[box.elementTiming, box['elementTiming'], box.getAttribute('elementtiming')].join(','),
[dialog.closedBy, dialog['closedBy'], dialog.getAttribute('closedby')].join(','),
[map.name, map['name'], map.getAttribute('name')].join(','),
[stamp.dateTime, stamp['dateTime'], stamp.getAttribute('datetime')].join(','),
[line.clear, line['clear'], line.getAttribute('clear')].join(','),
[cap.align, cap['align'], cap.getAttribute('align')].join(',')
].join(';');
document.getElementById('result').textContent = [first, second, third].join('|');
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text(
"#result",
"set:next-slot,set:next-slot,set:next-slot,hero;set:group,set:group,set:group,note;set:next-paint,set:next-paint,set:next-paint,paint;set:any,set:any,set:any,none;set:next-name,set:next-name,set:next-name,zones;set:2025-05-01,set:2025-05-01,set:2025-05-01,2024-01-01;set:right,set:right,set:right,left;set:top,set:top,set:top,bottom|set:reflect-slot,set:reflect-slot,set:reflect-slot,hero;set:region,set:region,set:region,note;set:reflect-paint,set:reflect-paint,set:reflect-paint,paint;set:close-request,set:close-request,set:close-request,none;set:reflect-name,set:reflect-name,set:reflect-name,zones;set:2030-12-31,set:2030-12-31,set:2030-12-31,2024-01-01;set:all,set:all,set:all,left;set:left,set:left,set:left,bottom|hero,hero,hero;note,note,note;paint,paint,paint;none,none,none;zones,zones,zones;2024-01-01,2024-01-01,2024-01-01;left,left,left;bottom,bottom,bottom",
)?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_6_1_non_form_hyperlink_and_media_plain_property_shadow_define_property_delete_and_fast_path_parity_work()
-> Result<()> {
let html = r#"
<a
id='link'
href='/docs'
download='report.txt'
hreflang='en'
ping='/ping'
referrerpolicy='origin'
rel='noopener'
target='_blank'>Link</a>
<map name='zones'>
<area
id='area'
alt='hot'
charset='utf-8'
coords='1,2,3'
rev='legacy'
shape='circle'
nohref>
</map>
<audio id='audio' autoplay controls loop muted></audio>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const link = document.getElementById('link');
const area = document.getElementById('area');
const audio = document.getElementById('audio');
const shadow = {
download: 'shadow-download',
hreflang: 'shadow-hreflang',
ping: 'shadow-ping',
referrerPolicy: 'shadow-referrer',
rel: 'shadow-rel',
target: 'shadow-target',
alt: 'shadow-alt',
charset: 'shadow-charset',
coords: 'shadow-coords',
rev: 'shadow-rev',
shape: 'shadow-shape',
noHref: 'shadow-nohref',
autoplay: 'shadow-autoplay',
controls: 'shadow-controls',
loop: 'shadow-loop',
muted: 'shadow-muted'
};
function shadowAccessor(key) {
return {
get() { return shadow[key]; },
set(value) { shadow[key] = 'set:' + value; },
configurable: true
};
}
Object.defineProperty(link, 'download', shadowAccessor('download'));
Object.defineProperty(link, 'hreflang', shadowAccessor('hreflang'));
Object.defineProperty(link, 'ping', shadowAccessor('ping'));
Object.defineProperty(link, 'referrerPolicy', shadowAccessor('referrerPolicy'));
Object.defineProperty(link, 'rel', shadowAccessor('rel'));
Object.defineProperty(link, 'target', shadowAccessor('target'));
Object.defineProperty(area, 'alt', shadowAccessor('alt'));
Object.defineProperty(area, 'charset', shadowAccessor('charset'));
Object.defineProperty(area, 'coords', shadowAccessor('coords'));
Object.defineProperty(area, 'rev', shadowAccessor('rev'));
Object.defineProperty(area, 'shape', shadowAccessor('shape'));
Object.defineProperty(area, 'noHref', shadowAccessor('noHref'));
Object.defineProperty(audio, 'autoplay', shadowAccessor('autoplay'));
Object.defineProperty(audio, 'controls', shadowAccessor('controls'));
Object.defineProperty(audio, 'loop', shadowAccessor('loop'));
Object.defineProperty(audio, 'muted', shadowAccessor('muted'));
document.getElementById('link').download = 'next.txt';
link.hreflang = 'fr';
link.ping = '/next-ping';
link.referrerPolicy = 'strict-origin';
link.rel = 'nofollow';
link.target = '_self';
area.alt = 'next-alt';
area.charset = 'shift-jis';
area.coords = '4,5,6';
area.rev = 'next-rev';
area.shape = 'rect';
area.noHref = false;
audio.autoplay = false;
audio.controls = false;
audio.loop = false;
audio.muted = false;
const first = [
[link.download, link['download'], shadow.download, link.getAttribute('download')].join(','),
[link.hreflang, link['hreflang'], shadow.hreflang, link.getAttribute('hreflang')].join(','),
[link.ping, link['ping'], shadow.ping, link.getAttribute('ping')].join(','),
[link.referrerPolicy, link['referrerPolicy'], shadow.referrerPolicy, link.getAttribute('referrerpolicy')].join(','),
[link.rel, link['rel'], shadow.rel, link.getAttribute('rel')].join(','),
[link.target, link['target'], shadow.target, link.getAttribute('target')].join(','),
[area.alt, area['alt'], shadow.alt, area.getAttribute('alt')].join(','),
[area.charset, area['charset'], shadow.charset, area.getAttribute('charset')].join(','),
[area.coords, area['coords'], shadow.coords, area.getAttribute('coords')].join(','),
[area.rev, area['rev'], shadow.rev, area.getAttribute('rev')].join(','),
[area.shape, area['shape'], shadow.shape, area.getAttribute('shape')].join(','),
[area.noHref, area['noHref'], shadow.noHref, String(area.hasAttribute('nohref'))].join(','),
[audio.autoplay, audio['autoplay'], shadow.autoplay, String(audio.hasAttribute('autoplay'))].join(','),
[audio.controls, audio['controls'], shadow.controls, String(audio.hasAttribute('controls'))].join(','),
[audio.loop, audio['loop'], shadow.loop, String(audio.hasAttribute('loop'))].join(','),
[audio.muted, audio['muted'], shadow.muted, String(audio.hasAttribute('muted'))].join(',')
].join(';');
Reflect.set(link, 'download', 'reflect-download');
Reflect.set(link, 'hreflang', 'es');
Reflect.set(link, 'ping', '/reflect-ping');
Reflect.set(link, 'referrerPolicy', 'no-referrer');
Reflect.set(link, 'rel', 'ugc');
Reflect.set(link, 'target', '_top');
Reflect.set(area, 'alt', 'reflect-alt');
Reflect.set(area, 'charset', 'utf-16');
Reflect.set(area, 'coords', '7,8,9');
Reflect.set(area, 'rev', 'reflect-rev');
Reflect.set(area, 'shape', 'poly');
Reflect.set(area, 'noHref', true);
Reflect.set(audio, 'autoplay', true);
Reflect.set(audio, 'controls', true);
Reflect.set(audio, 'loop', true);
Reflect.set(audio, 'muted', true);
const second = [
[link.download, link['download'], shadow.download, link.getAttribute('download')].join(','),
[link.hreflang, link['hreflang'], shadow.hreflang, link.getAttribute('hreflang')].join(','),
[link.ping, link['ping'], shadow.ping, link.getAttribute('ping')].join(','),
[link.referrerPolicy, link['referrerPolicy'], shadow.referrerPolicy, link.getAttribute('referrerpolicy')].join(','),
[link.rel, link['rel'], shadow.rel, link.getAttribute('rel')].join(','),
[link.target, link['target'], shadow.target, link.getAttribute('target')].join(','),
[area.alt, area['alt'], shadow.alt, area.getAttribute('alt')].join(','),
[area.charset, area['charset'], shadow.charset, area.getAttribute('charset')].join(','),
[area.coords, area['coords'], shadow.coords, area.getAttribute('coords')].join(','),
[area.rev, area['rev'], shadow.rev, area.getAttribute('rev')].join(','),
[area.shape, area['shape'], shadow.shape, area.getAttribute('shape')].join(','),
[area.noHref, area['noHref'], shadow.noHref, String(area.hasAttribute('nohref'))].join(','),
[audio.autoplay, audio['autoplay'], shadow.autoplay, String(audio.hasAttribute('autoplay'))].join(','),
[audio.controls, audio['controls'], shadow.controls, String(audio.hasAttribute('controls'))].join(','),
[audio.loop, audio['loop'], shadow.loop, String(audio.hasAttribute('loop'))].join(','),
[audio.muted, audio['muted'], shadow.muted, String(audio.hasAttribute('muted'))].join(',')
].join(';');
delete link.download;
delete link.hreflang;
delete link.ping;
delete link.referrerPolicy;
delete link.rel;
delete link.target;
delete area.alt;
delete area.charset;
delete area.coords;
delete area.rev;
delete area.shape;
delete area.noHref;
delete audio.autoplay;
delete audio.controls;
delete audio.loop;
delete audio.muted;
const third = [
[link.download, link['download'], link.getAttribute('download')].join(','),
[link.hreflang, link['hreflang'], link.getAttribute('hreflang')].join(','),
[link.ping, link['ping'], link.getAttribute('ping')].join(','),
[link.referrerPolicy, link['referrerPolicy'], link.getAttribute('referrerpolicy')].join(','),
[link.rel, link['rel'], link.getAttribute('rel')].join(','),
[link.target, link['target'], link.getAttribute('target')].join(','),
[area.alt, area['alt'], area.getAttribute('alt')].join(','),
[area.charset, area['charset'], area.getAttribute('charset')].join(','),
[area.coords, area['coords'], area.getAttribute('coords')].join(','),
[area.rev, area['rev'], area.getAttribute('rev')].join(','),
[area.shape, area['shape'], area.getAttribute('shape')].join(','),
[String(area.noHref), String(area['noHref']), String(area.hasAttribute('nohref'))].join(','),
[String(audio.autoplay), String(audio['autoplay']), String(audio.hasAttribute('autoplay'))].join(','),
[String(audio.controls), String(audio['controls']), String(audio.hasAttribute('controls'))].join(','),
[String(audio.loop), String(audio['loop']), String(audio.hasAttribute('loop'))].join(','),
[String(audio.muted), String(audio['muted']), String(audio.hasAttribute('muted'))].join(',')
].join(';');
document.getElementById('result').textContent = [first, second, third].join('|');
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text(
"#result",
"set:next.txt,set:next.txt,set:next.txt,report.txt;set:fr,set:fr,set:fr,en;set:/next-ping,set:/next-ping,set:/next-ping,/ping;set:strict-origin,set:strict-origin,set:strict-origin,origin;set:nofollow,set:nofollow,set:nofollow,noopener;set:_self,set:_self,set:_self,_blank;set:next-alt,set:next-alt,set:next-alt,hot;set:shift-jis,set:shift-jis,set:shift-jis,utf-8;set:4,5,6,set:4,5,6,set:4,5,6,1,2,3;set:next-rev,set:next-rev,set:next-rev,legacy;set:rect,set:rect,set:rect,circle;set:false,set:false,set:false,true;set:false,set:false,set:false,true;set:false,set:false,set:false,true;set:false,set:false,set:false,true;set:false,set:false,set:false,true|set:reflect-download,set:reflect-download,set:reflect-download,report.txt;set:es,set:es,set:es,en;set:/reflect-ping,set:/reflect-ping,set:/reflect-ping,/ping;set:no-referrer,set:no-referrer,set:no-referrer,origin;set:ugc,set:ugc,set:ugc,noopener;set:_top,set:_top,set:_top,_blank;set:reflect-alt,set:reflect-alt,set:reflect-alt,hot;set:utf-16,set:utf-16,set:utf-16,utf-8;set:7,8,9,set:7,8,9,set:7,8,9,1,2,3;set:reflect-rev,set:reflect-rev,set:reflect-rev,legacy;set:poly,set:poly,set:poly,circle;set:true,set:true,set:true,true;set:true,set:true,set:true,true;set:true,set:true,set:true,true;set:true,set:true,set:true,true;set:true,set:true,set:true,true|report.txt,report.txt,report.txt;en,en,en;/ping,/ping,/ping;origin,origin,origin;noopener,noopener,noopener;_blank,_blank,_blank;hot,hot,hot;utf-8,utf-8,utf-8;1,2,3,1,2,3,1,2,3;legacy,legacy,legacy;circle,circle,circle;true,true,true;true,true,true;true,true,true;true,true,true;true,true,true",
)?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_6_1_url_anchor_search_hash_and_credentials_delimiter_normalization_work()
-> Result<()> {
let html = r#"
<a id='link' href='https://u:p@example.com/path?x=1#start'>link</a>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const link = document.getElementById('link');
const initial = [
link.search,
link.hash,
link.href
].join(',');
link.search = '';
link.hash = '';
const cleared = [
link.search,
link.hash,
link.href
].join(',');
link.search = 'q=2';
link.hash = 'frag';
const prefixed = [
link.search,
link.hash,
link.href
].join(',');
link.search = '?';
link.hash = '#';
const delimitersOnly = [
link.search,
link.hash,
link.href
].join(',');
link.username = '';
link.password = 'secret';
const passwordOnly = [
link.username,
link.password,
link.href
].join(',');
link.password = '';
const credentialsCleared = [
link.username,
link.password,
link.href
].join(',');
document.getElementById('result').textContent = [
initial,
cleared,
prefixed,
delimitersOnly,
passwordOnly,
credentialsCleared
].join('|');
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text(
"#result",
"?x=1,#start,https://u:p@example.com/path?x=1#start|,,https://u:p@example.com/path|?q=2,#frag,https://u:p@example.com/path?q=2#frag|,,https://u:p@example.com/path?#|,secret,https://:secret@example.com/path?#|,,https://example.com/path?#",
)?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_6_1_url_base_href_changes_update_baseuri_form_action_and_submitter_formaction()
-> Result<()> {
let html = r#"
<base id='base' href='https://app.local/v1/'>
<form id='f' action='submit'></form>
<button id='btn' type='submit' form='f'>button</button>
<input id='inp' type='submit' form='f' value='input'>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const base = document.getElementById('base');
const form = document.getElementById('f');
const btn = document.getElementById('btn');
const inp = document.getElementById('inp');
const initial = [
document.baseURI,
form.action,
btn.formAction,
inp.formAction
].join(',');
base.setAttribute('href', '/v2/');
const movedBase = [
document.baseURI,
form.action,
btn.formAction,
inp.formAction
].join(',');
btn.formAction = 'override';
inp.formAction = 'send';
const override = [
btn.formAction + ':' + btn.getAttribute('formaction'),
inp.formAction + ':' + inp.getAttribute('formaction')
].join(',');
btn.removeAttribute('formaction');
inp.removeAttribute('formaction');
base.setAttribute('href', '');
const resetBase = [
document.baseURI,
form.action,
btn.formAction,
inp.formAction
].join(',');
form.action = 'next';
const formAssigned = [
form.action,
btn.formAction,
inp.formAction
].join(',');
document.getElementById('result').textContent = [
initial,
movedBase,
override,
resetBase,
formAssigned
].join('|');
});
</script>
"#;
let mut h = Harness::from_html_with_url("https://app.local/root/page.html", html)?;
h.click("#run")?;
h.assert_text(
"#result",
"https://app.local/v1/,https://app.local/v1/submit,https://app.local/v1/submit,https://app.local/v1/submit|https://app.local/v2/,https://app.local/v2/submit,https://app.local/v2/submit,https://app.local/v2/submit|https://app.local/v2/override:override,https://app.local/v2/send:send|https://app.local/root/page.html,https://app.local/root/submit,https://app.local/root/submit,https://app.local/root/submit|https://app.local/root/next,https://app.local/root/next,https://app.local/root/next",
)?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_3_3_numeric_input_min_max_step_invalid_tokens_nan_infinity_and_boundaries_affect_validity()
-> Result<()> {
let html = r#"
<input id='num' type='number' min='foo' max='Infinity' step='NaN' value='3'>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const num = document.getElementById('num');
const initial = [
num.validity.rangeUnderflow,
num.validity.rangeOverflow,
num.validity.stepMismatch,
num.validity.badInput,
num.checkValidity()
].join(':');
num.value = '3.5';
const defaultStep = [
num.validity.stepMismatch,
num.checkValidity()
].join(':');
num.step = '0.5';
const explicitStep = [
num.validity.stepMismatch,
num.checkValidity()
].join(':');
num.min = '2';
num.max = '4';
num.value = '1.5';
const underflow = [
num.validity.rangeUnderflow,
num.validity.rangeOverflow,
num.validity.stepMismatch,
num.checkValidity()
].join(':');
num.value = '4.5';
const overflow = [
num.validity.rangeUnderflow,
num.validity.rangeOverflow,
num.validity.stepMismatch,
num.checkValidity()
].join(':');
num.step = 'any';
num.value = '3.3';
const stepAny = [
num.validity.stepMismatch,
num.checkValidity()
].join(':');
num.min = 'NaN';
num.max = '-Infinity';
num.step = 'Infinity';
num.value = '3.7';
const invalidTokens = [
num.validity.rangeUnderflow,
num.validity.rangeOverflow,
num.validity.stepMismatch,
num.checkValidity()
].join(':');
document.getElementById('result').textContent = [
initial,
defaultStep,
explicitStep,
underflow,
overflow,
stepAny,
invalidTokens
].join('|');
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text(
"#result",
"false:false:false:false:true|true:false|false:true|true:false:false:false|false:true:false:false|false:true|false:false:true:false",
)?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_3_3_numeric_minlength_maxlength_threshold_boundaries_for_input_and_textarea_validity()
-> Result<()> {
let html = r#"
<input id='field' type='text' minlength='3' maxlength='5'>
<textarea id='ta' minlength='2' maxlength='4'></textarea>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const field = document.getElementById('field');
const ta = document.getElementById('ta');
const initial = [
field.validity.tooShort + ':' + field.validity.tooLong + ':' + field.checkValidity(),
ta.validity.tooShort + ':' + ta.validity.tooLong + ':' + ta.checkValidity()
].join(',');
field.value = 'ab';
ta.value = 'a';
const belowMin = [
field.validity.tooShort + ':' + field.validity.tooLong + ':' + field.checkValidity(),
ta.validity.tooShort + ':' + ta.validity.tooLong + ':' + ta.checkValidity()
].join(',');
field.value = 'abc';
ta.value = 'ab';
const atMin = [
field.validity.tooShort + ':' + field.validity.tooLong + ':' + field.checkValidity(),
ta.validity.tooShort + ':' + ta.validity.tooLong + ':' + ta.checkValidity()
].join(',');
field.value = 'abcdef';
ta.value = 'abcde';
const aboveMax = [
field.validity.tooShort + ':' + field.validity.tooLong + ':' + field.checkValidity(),
ta.validity.tooShort + ':' + ta.validity.tooLong + ':' + ta.checkValidity()
].join(',');
field.value = 'abcde';
ta.value = 'abcd';
const atMax = [
field.validity.tooShort + ':' + field.validity.tooLong + ':' + field.checkValidity(),
ta.validity.tooShort + ':' + ta.validity.tooLong + ':' + ta.checkValidity()
].join(',');
document.getElementById('result').textContent = [
initial,
belowMin,
atMin,
aboveMax,
atMax
].join('|');
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text(
"#result",
"false:false:true,false:false:true|true:false:false,true:false:false|false:false:true,false:false:true|false:true:false,false:true:false|false:false:true,false:false:true",
)?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_3_3_parser_fast_path_matches_action_formaction_autocomplete_size_and_length_reflection()
-> Result<()> {
let html = r#"
<form id='f' action='/submit'></form>
<button id='btn' type='submit' form='f'>button</button>
<input id='field' type='text' size='0' minlength='oops' maxlength='oops' autocomplete='off'>
<textarea id='ta' rows='0' cols='0'></textarea>
<select id='sel' size='0'>
<option>One</option>
</select>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const initial = [
document.getElementById('f').action,
document.getElementById('btn').formAction,
document.getElementById('field').autocomplete,
document.getElementById('field').size,
document.getElementById('field').minLength,
document.getElementById('field').maxLength,
document.getElementById('ta').rows,
document.getElementById('ta').cols,
document.getElementById('sel').size
].join(',');
document.getElementById('f').action = 'next';
document.getElementById('btn').formAction = 'send';
document.getElementById('field').autocomplete = 'on';
document.getElementById('field').size = -3;
document.getElementById('field').minLength = '7';
document.getElementById('field').maxLength = '12';
document.getElementById('ta').rows = 0;
document.getElementById('ta').cols = -1;
document.getElementById('sel').size = -2;
const updated = [
document.getElementById('f').action,
document.getElementById('btn').formAction,
document.getElementById('field').autocomplete,
document.getElementById('field').size,
document.getElementById('field').minLength,
document.getElementById('field').maxLength,
document.getElementById('ta').rows,
document.getElementById('ta').cols,
document.getElementById('sel').size
].join(',');
document.getElementById('btn').removeAttribute('formaction');
const removed = [
document.getElementById('f').action,
document.getElementById('btn').formAction,
document.getElementById('btn').getAttribute('formaction') === null
].join(',');
document.getElementById('result').textContent = [
initial,
updated,
removed
].join('|');
});
</script>
"#;
let mut h = Harness::from_html_with_url("https://app.local/base/page.html", html)?;
h.click("#run")?;
h.assert_text(
"#result",
"https://app.local/submit,https://app.local/submit,off,20,-1,-1,2,20,1|https://app.local/base/next,https://app.local/base/send,on,1,7,12,1,1,1|https://app.local/base/next,https://app.local/base/next,true",
)?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_6_1_url_anchor_setter_special_and_opaque_protocol_host_port_pathname_matrix_work()
-> Result<()> {
let html = r#"
<a id='special' href='https://user:pw@example.com:8443/a/b?x=1#h'>special</a>
<a id='opaque' href='mailto:person@example.com?subject=Hi#frag'>opaque</a>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const special = document.getElementById('special');
const opaque = document.getElementById('opaque');
const initial = [
special.protocol + ',' + special.host + ',' + special.pathname + ',' + special.href,
opaque.protocol + ',' + opaque.host + ',' + opaque.pathname + ',' + opaque.href
].join(';');
special.protocol = 'http:';
special.host = 'api.example.test:9090';
special.hostname = 'cdn.example.test';
special.port = '7070';
special.pathname = 'docs';
opaque.protocol = 'foo:';
opaque.host = 'ignored.test:1234';
opaque.hostname = 'ignored2.test';
opaque.port = '5678';
opaque.pathname = 'new/path';
const updated = [
special.protocol + ',' + special.host + ',' + special.pathname + ',' + special.href,
opaque.protocol + ',' + opaque.host + ',' + opaque.pathname + ',' + opaque.href
].join(';');
document.getElementById('result').textContent = [
initial,
updated
].join('|');
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text(
"#result",
"https:,example.com:8443,/a/b,https://user:pw@example.com:8443/a/b?x=1#h;mailto:,,person@example.com,mailto:person@example.com?subject=Hi#frag|http:,cdn.example.test:7070,/docs,http://user:pw@cdn.example.test:7070/docs?x=1#h;foo:,,person@example.com,foo:person@example.com?subject=Hi#frag",
)?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_6_1_url_anchor_default_port_and_protocol_switch_matrix_work()
-> Result<()> {
let html = r#"
<a id='http-default' href='http://example.com:81/path?x=1#h'>http</a>
<a id='https-default' href='https://example.com:80/path?x=1#h'>https</a>
<a id='special-to-special' href='https://u:p@example.com:80/a/b?x=1#h'>special</a>
<a id='special-to-opaque' href='http://example.com:80/a?x=1#h'>no-op</a>
<a id='opaque-to-special' href='mailto:person@example.com?subject=Hi#frag'>opaque</a>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const httpDefault = document.getElementById('http-default');
const httpsDefault = document.getElementById('https-default');
const specialToSpecial = document.getElementById('special-to-special');
const specialToOpaque = document.getElementById('special-to-opaque');
const opaqueToSpecial = document.getElementById('opaque-to-special');
const initial = [
[httpDefault.port, httpDefault.host, httpDefault.href].join(','),
[httpsDefault.port, httpsDefault.host, httpsDefault.href].join(','),
[specialToSpecial.protocol, specialToSpecial.host, specialToSpecial.href].join(','),
[specialToOpaque.protocol, specialToOpaque.href].join(','),
[opaqueToSpecial.protocol, opaqueToSpecial.href].join(',')
].join(';');
httpDefault.port = '80';
httpsDefault.host = 'example.com:443';
specialToSpecial.protocol = 'http:';
specialToOpaque.protocol = 'mailto:';
opaqueToSpecial.protocol = 'https:';
const updated = [
[httpDefault.port, httpDefault.host, httpDefault.href].join(','),
[httpsDefault.port, httpsDefault.host, httpsDefault.href].join(','),
[specialToSpecial.protocol, specialToSpecial.host, specialToSpecial.href].join(','),
[specialToOpaque.protocol, specialToOpaque.href].join(','),
[opaqueToSpecial.protocol, opaqueToSpecial.href].join(',')
].join(';');
document.getElementById('result').textContent = [initial, updated].join('|');
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text(
"#result",
"81,example.com:81,http://example.com:81/path?x=1#h;80,example.com:80,https://example.com:80/path?x=1#h;https:,example.com:80,https://u:p@example.com:80/a/b?x=1#h;http:,http://example.com/a?x=1#h;mailto:,mailto:person@example.com?subject=Hi#frag|,example.com,http://example.com/path?x=1#h;,example.com,https://example.com/path?x=1#h;http:,example.com,http://u:p@example.com/a/b?x=1#h;http:,http://example.com/a?x=1#h;mailto:,mailto:person@example.com?subject=Hi#frag",
)?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_6_1_url_anchor_username_password_setter_is_noop_for_no_host_and_file_urls()
-> Result<()> {
let html = r#"
<a id='mail' href='mailto:m.bluth@example.com?subject=Hi'>mail</a>
<a id='data' href='data:text/plain,hello'>data</a>
<a id='file' href='file:///report.txt'>file</a>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const mail = document.getElementById('mail');
const data = document.getElementById('data');
const file = document.getElementById('file');
const initial = [
mail.username + ':' + mail.password + ':' + mail.href,
data.username + ':' + data.password + ':' + data.href,
file.username + ':' + file.password + ':' + file.href
].join(';');
mail.username = 'alice';
mail.password = 'secret';
data.username = 'alice';
data.password = 'secret';
file.username = 'alice';
file.password = 'secret';
const updated = [
mail.username + ':' + mail.password + ':' + mail.href,
data.username + ':' + data.password + ':' + data.href,
file.username + ':' + file.password + ':' + file.href
].join(';');
document.getElementById('result').textContent = [
initial,
updated
].join('|');
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text(
"#result",
"::mailto:m.bluth@example.com?subject=Hi;::data:text/plain,hello;::file:///report.txt|::mailto:m.bluth@example.com?subject=Hi;::data:text/plain,hello;::file:///report.txt",
)?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_6_1_url_anchor_file_host_setter_matrix_work() -> Result<()> {
let html = r#"
<a id='server' href='file://server/share/file.txt'>server</a>
<a id='local' href='file://localhost/Users/me/test.txt'>local</a>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const server = document.getElementById('server');
const local = document.getElementById('local');
const initial = [
[server.href, server.host, server.hostname, server.port].join(','),
[local.href, local.host, local.hostname, local.port].join(',')
].join(';');
server.port = '8080';
local.port = '8080';
const afterPort = [
[server.href, server.host, server.hostname, server.port].join(','),
[local.href, local.host, local.hostname, local.port].join(',')
].join(';');
server.host = 'example.com';
local.hostname = 'example.com';
const afterHost = [
[server.href, server.host, server.hostname, server.port].join(','),
[local.href, local.host, local.hostname, local.port].join(',')
].join(';');
server.host = 'localhost';
local.host = 'localhost';
const afterLocalhost = [
[server.href, server.host, server.hostname, server.port].join(','),
[local.href, local.host, local.hostname, local.port].join(',')
].join(';');
server.host = 'localhost:8080';
local.host = 'example.com:8080';
const blockedPort = [
[server.href, server.host, server.hostname, server.port].join(','),
[local.href, local.host, local.hostname, local.port].join(',')
].join(';');
document.getElementById('result').textContent =
[initial, afterPort, afterHost, afterLocalhost, blockedPort].join('|');
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text(
"#result",
"file://server/share/file.txt,server,server,;file:///Users/me/test.txt,,,|file://server/share/file.txt,server,server,;file:///Users/me/test.txt,,,|file://example.com/share/file.txt,example.com,example.com,;file://example.com/Users/me/test.txt,example.com,example.com,|file:///share/file.txt,,,;file:///Users/me/test.txt,,,|file:///share/file.txt,,,;file:///Users/me/test.txt,,,",
)?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_6_1_url_anchor_invalid_file_href_and_origin_work() -> Result<()> {
let html = r#"
<a id='bad' href='file://server:8080/share/file.txt'>bad</a>
<a id='local' href='FiLe://LOCALHOST/Users/Me/Report.txt'>local</a>
<a id='server' href='FiLe://SeRVer/Share/File.txt'>server</a>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const bad = document.getElementById('bad');
const local = document.getElementById('local');
const server = document.getElementById('server');
document.getElementById('result').textContent = [
[bad.getAttribute('href'), bad.href].join(','),
[local.href, local.origin, local.host, local.hostname].join(','),
[server.href, server.origin, server.host, server.hostname].join(',')
].join('|');
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text(
"#result",
"file://server:8080/share/file.txt,file://server:8080/share/file.txt|file:///Users/Me/Report.txt,null,,|file://server/Share/File.txt,null,server,server",
)?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_6_1_url_anchor_invalid_absolute_subproperties_are_empty_and_setters_noop()
-> Result<()> {
let html = r#"
<a id='bad' href='http://example.com:abc/'>bad</a>
<a id='ipv6' href='http://[::1/'>ipv6</a>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const bad = document.getElementById('bad');
const ipv6 = document.getElementById('ipv6');
const initial = [
'href=' + bad.href,
'protocol=' + bad.protocol,
'host=' + bad.host,
'hostname=' + bad.hostname,
'port=' + bad.port,
'pathname=' + bad.pathname,
'origin=' + bad.origin,
'username=' + bad.username,
'password=' + bad.password,
'search=' + bad.search,
'hash=' + bad.hash
].join(',');
const invalidIpv6 = [
'href=' + ipv6.href,
'protocol=' + ipv6.protocol,
'host=' + ipv6.host,
'pathname=' + ipv6.pathname,
'origin=' + ipv6.origin
].join(',');
bad.protocol = 'https:';
bad.host = 'example.com:9090';
bad.hostname = 'example.com';
bad.port = '9090';
bad.pathname = '/docs';
bad.search = 'x=1';
bad.hash = 'frag';
const after = [
'attr=' + bad.getAttribute('href'),
'href=' + bad.href,
'protocol=' + bad.protocol,
'host=' + bad.host,
'pathname=' + bad.pathname,
'origin=' + bad.origin
].join(',');
document.getElementById('result').textContent =
[initial, invalidIpv6, after].join('|');
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text(
"#result",
"href=http://example.com:abc/,protocol=:,host=,hostname=,port=,pathname=,origin=,username=,password=,search=,hash=|href=http://[::1/,protocol=:,host=,pathname=,origin=|attr=http://example.com:abc/,href=http://example.com:abc/,protocol=:,host=,pathname=,origin=",
)?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_6_1_url_area_and_link_null_url_getters_match_anchor_work()
-> Result<()> {
let html = r#"
<a id='anchor-missing'>anchor missing</a>
<a id='anchor-bad' href='http://example.com:abc/path'>anchor bad</a>
<map name='zones'>
<area id='area-missing' alt='area missing'>
<area id='area-bad' href='http://example.com:abc/path' alt='area bad'>
</map>
<link id='link-missing' rel='stylesheet'>
<link id='link-bad' rel='stylesheet' href='http://example.com:abc/path'>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
function state(node) {
return [
'href=' + node.href,
'protocol=' + node.protocol,
'host=' + node.host,
'pathname=' + node.pathname,
'origin=' + node.origin
].join(',');
}
document.getElementById('run').addEventListener('click', () => {
const anchorMissing = document.getElementById('anchor-missing');
const anchorBad = document.getElementById('anchor-bad');
const areaMissing = document.getElementById('area-missing');
const areaBad = document.getElementById('area-bad');
const linkMissing = document.getElementById('link-missing');
const linkBad = document.getElementById('link-bad');
areaBad.search = 'x=1';
linkBad.hash = 'frag';
document.getElementById('result').textContent = [
state(anchorMissing),
state(areaMissing),
state(linkMissing),
state(anchorBad),
state(areaBad),
state(linkBad),
'areaAttr=' + areaBad.getAttribute('href'),
'linkAttr=' + linkBad.getAttribute('href')
].join('|');
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text(
"#result",
"href=,protocol=:,host=,pathname=,origin=|href=,protocol=:,host=,pathname=,origin=|href=,protocol=:,host=,pathname=,origin=|href=http://example.com:abc/path,protocol=:,host=,pathname=,origin=|href=http://example.com:abc/path,protocol=:,host=,pathname=,origin=|href=http://example.com:abc/path,protocol=:,host=,pathname=,origin=|areaAttr=http://example.com:abc/path|linkAttr=http://example.com:abc/path",
)?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_6_1_url_hyperlink_credentials_and_delimiter_encoding_work()
-> Result<()> {
let html = r#"
<a id='anchor' href='https://u:p@example.com/base'>anchor</a>
<map name='zones'>
<area id='area' href='https://u:p@example.com/base' alt='area'>
</map>
<link id='link' rel='stylesheet' href='https://u:p@example.com/base'>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
function mutate(node) {
node.username = 'a@b';
node.password = 'p@q:r';
node.pathname = '\\docs\\a b';
node.search = "a'b";
node.hash = 'x`y';
return [
node.href,
node.username,
node.password,
node.pathname,
node.search,
node.hash,
node.getAttribute('href')
].join(',');
}
document.getElementById('run').addEventListener('click', () => {
document.getElementById('result').textContent = [
mutate(document.getElementById('anchor')),
mutate(document.getElementById('area')),
mutate(document.getElementById('link'))
].join('|');
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text(
"#result",
"https://a%40b:p%40q%3Ar@example.com/docs/a%20b?a%27b#x%60y,a%40b,p%40q%3Ar,/docs/a%20b,?a%27b,#x%60y,https://a%40b:p%40q%3Ar@example.com/docs/a%20b?a%27b#x%60y|https://a%40b:p%40q%3Ar@example.com/docs/a%20b?a%27b#x%60y,a%40b,p%40q%3Ar,/docs/a%20b,?a%27b,#x%60y,https://a%40b:p%40q%3Ar@example.com/docs/a%20b?a%27b#x%60y|https://a%40b:p%40q%3Ar@example.com/docs/a%20b?a%27b#x%60y,a%40b,p%40q%3Ar,/docs/a%20b,?a%27b,#x%60y,https://a%40b:p%40q%3Ar@example.com/docs/a%20b?a%27b#x%60y",
)?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_6_1_url_hyperlink_authority_and_percent_residual_work() -> Result<()>
{
let html = r#"
<a id='anchor' href='https://user%zz:pa%2fss@example.com/%2f%zz?x=%2f%zz#y=%2f%zz'>anchor</a>
<map name='zones'>
<area id='area' href='https://user%zz:pa%2fss@example.com/%2f%zz?x=%2f%zz#y=%2f%zz' alt='area'>
</map>
<link id='link' rel='stylesheet' href='https://user%zz:pa%2fss@example.com/%2f%zz?x=%2f%zz#y=%2f%zz'>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
function mutate(node) {
const initial = [
node.href,
node.username,
node.password,
node.pathname,
node.search,
node.hash
].join(',');
node.host = 'ExA%41mple.ORG:0099';
const afterHost = [node.href, node.host].join(',');
node.host = 'exa%mple.org:77';
const afterBadHost = [node.href, node.host].join(',');
node.username = 'a%zz';
node.password = 'b%2f';
node.pathname = '%2f%zz';
node.search = '%2f%zz';
node.hash = '%2f%zz';
return [
initial,
afterHost,
afterBadHost,
[
node.href,
node.username,
node.password,
node.pathname,
node.search,
node.hash,
node.getAttribute('href')
].join(',')
].join(';');
}
document.getElementById('run').addEventListener('click', () => {
document.getElementById('result').textContent = [
mutate(document.getElementById('anchor')),
mutate(document.getElementById('area')),
mutate(document.getElementById('link'))
].join('|');
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text(
"#result",
"https://user%zz:pa%2fss@example.com/%2f%zz?x=%2f%zz#y=%2f%zz,user%zz,pa%2fss,/%2f%zz,?x=%2f%zz,#y=%2f%zz;https://user%zz:pa%2fss@exaample.org:99/%2f%zz?x=%2f%zz#y=%2f%zz,exaample.org:99;https://user%zz:pa%2fss@exaample.org:99/%2f%zz?x=%2f%zz#y=%2f%zz,exaample.org:99;https://a%zz:b%2f@exaample.org:99/%2f%zz?%2f%zz#%2f%zz,a%zz,b%2f,/%2f%zz,?%2f%zz,#%2f%zz,https://a%zz:b%2f@exaample.org:99/%2f%zz?%2f%zz#%2f%zz|https://user%zz:pa%2fss@example.com/%2f%zz?x=%2f%zz#y=%2f%zz,user%zz,pa%2fss,/%2f%zz,?x=%2f%zz,#y=%2f%zz;https://user%zz:pa%2fss@exaample.org:99/%2f%zz?x=%2f%zz#y=%2f%zz,exaample.org:99;https://user%zz:pa%2fss@exaample.org:99/%2f%zz?x=%2f%zz#y=%2f%zz,exaample.org:99;https://a%zz:b%2f@exaample.org:99/%2f%zz?%2f%zz#%2f%zz,a%zz,b%2f,/%2f%zz,?%2f%zz,#%2f%zz,https://a%zz:b%2f@exaample.org:99/%2f%zz?%2f%zz#%2f%zz|https://user%zz:pa%2fss@example.com/%2f%zz?x=%2f%zz#y=%2f%zz,user%zz,pa%2fss,/%2f%zz,?x=%2f%zz,#y=%2f%zz;https://user%zz:pa%2fss@exaample.org:99/%2f%zz?x=%2f%zz#y=%2f%zz,exaample.org:99;https://user%zz:pa%2fss@exaample.org:99/%2f%zz?x=%2f%zz#y=%2f%zz,exaample.org:99;https://a%zz:b%2f@exaample.org:99/%2f%zz?%2f%zz#%2f%zz,a%zz,b%2f,/%2f%zz,?%2f%zz,#%2f%zz,https://a%zz:b%2f@exaample.org:99/%2f%zz?%2f%zz#%2f%zz",
)?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_6_1_url_hyperlink_malformed_query_and_host_code_point_work()
-> Result<()> {
let html = r#"
<a id='anchor' href='https://base.test/path?a=%zz&b=%E0%A4&c=%C3%28'>anchor</a>
<map name='zones'>
<area id='area' href='https://base.test/path?a=%zz&b=%E0%A4&c=%C3%28' alt='area'>
</map>
<link id='link' rel='stylesheet' href='https://base.test/path?a=%zz&b=%E0%A4&c=%C3%28'>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
function state(node) {
const initialUrl = new URL(node.href);
const initial = [
node.href,
node.search,
initialUrl.searchParams.get('a'),
initialUrl.searchParams.get('b'),
initialUrl.searchParams.get('c'),
initialUrl.searchParams.toString()
].join(',');
node.hostname = '\uFF21example.com';
const afterFullwidth = [node.href, node.host].join(',');
node.hostname = '\u00E9xample.com';
const afterUnicode = [node.href, node.host].join(',');
node.search = '?b=%E0%A4&a=%zz&a=1';
const parsed = new URL(node.href);
const afterSearch = [
node.href,
node.search,
parsed.searchParams.getAll('a').join(':'),
parsed.searchParams.get('b'),
parsed.searchParams.toString(),
node.getAttribute('href')
].join(',');
const mutated = new URL(node.href);
mutated.searchParams.sort();
mutated.searchParams.set('a', '%zz');
node.href = mutated.href;
const afterMutation = [
node.href,
node.search,
mutated.searchParams.getAll('a').join(':'),
mutated.searchParams.get('b'),
mutated.searchParams.toString(),
node.getAttribute('href')
].join(',');
node.hostname = 'example\u3002com';
const afterDot = [node.href, node.host].join(',');
node.hostname = '%00example.com';
const afterInvalid = [node.href, node.host].join(',');
return [
initial,
afterFullwidth,
afterUnicode,
afterSearch,
afterMutation,
afterDot,
afterInvalid
].join(';');
}
document.getElementById('run').addEventListener('click', () => {
document.getElementById('result').textContent = [
state(document.getElementById('anchor')),
state(document.getElementById('area')),
state(document.getElementById('link'))
].join('|');
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text(
"#result",
"https://base.test/path?a=%zz&b=%E0%A4&c=%C3%28,?a=%zz&b=%E0%A4&c=%C3%28,%zz,\u{FFFD},\u{FFFD}(,a=%25zz&b=%EF%BF%BD&c=%EF%BF%BD%28;https://aexample.com/path?a=%zz&b=%E0%A4&c=%C3%28,aexample.com;https://xn--xample-9ua.com/path?a=%zz&b=%E0%A4&c=%C3%28,xn--xample-9ua.com;https://xn--xample-9ua.com/path?b=%E0%A4&a=%zz&a=1,?b=%E0%A4&a=%zz&a=1,%zz:1,\u{FFFD},b=%EF%BF%BD&a=%25zz&a=1,https://xn--xample-9ua.com/path?b=%E0%A4&a=%zz&a=1;https://xn--xample-9ua.com/path?a=%25zz&b=%EF%BF%BD,?a=%25zz&b=%EF%BF%BD,%zz,\u{FFFD},a=%25zz&b=%EF%BF%BD,https://xn--xample-9ua.com/path?a=%25zz&b=%EF%BF%BD;https://example.com/path?a=%25zz&b=%EF%BF%BD,example.com;https://example.com/path?a=%25zz&b=%EF%BF%BD,example.com|https://base.test/path?a=%zz&b=%E0%A4&c=%C3%28,?a=%zz&b=%E0%A4&c=%C3%28,%zz,\u{FFFD},\u{FFFD}(,a=%25zz&b=%EF%BF%BD&c=%EF%BF%BD%28;https://aexample.com/path?a=%zz&b=%E0%A4&c=%C3%28,aexample.com;https://xn--xample-9ua.com/path?a=%zz&b=%E0%A4&c=%C3%28,xn--xample-9ua.com;https://xn--xample-9ua.com/path?b=%E0%A4&a=%zz&a=1,?b=%E0%A4&a=%zz&a=1,%zz:1,\u{FFFD},b=%EF%BF%BD&a=%25zz&a=1,https://xn--xample-9ua.com/path?b=%E0%A4&a=%zz&a=1;https://xn--xample-9ua.com/path?a=%25zz&b=%EF%BF%BD,?a=%25zz&b=%EF%BF%BD,%zz,\u{FFFD},a=%25zz&b=%EF%BF%BD,https://xn--xample-9ua.com/path?a=%25zz&b=%EF%BF%BD;https://example.com/path?a=%25zz&b=%EF%BF%BD,example.com;https://example.com/path?a=%25zz&b=%EF%BF%BD,example.com|https://base.test/path?a=%zz&b=%E0%A4&c=%C3%28,?a=%zz&b=%E0%A4&c=%C3%28,%zz,\u{FFFD},\u{FFFD}(,a=%25zz&b=%EF%BF%BD&c=%EF%BF%BD%28;https://aexample.com/path?a=%zz&b=%E0%A4&c=%C3%28,aexample.com;https://xn--xample-9ua.com/path?a=%zz&b=%E0%A4&c=%C3%28,xn--xample-9ua.com;https://xn--xample-9ua.com/path?b=%E0%A4&a=%zz&a=1,?b=%E0%A4&a=%zz&a=1,%zz:1,\u{FFFD},b=%EF%BF%BD&a=%25zz&a=1,https://xn--xample-9ua.com/path?b=%E0%A4&a=%zz&a=1;https://xn--xample-9ua.com/path?a=%25zz&b=%EF%BF%BD,?a=%25zz&b=%EF%BF%BD,%zz,\u{FFFD},a=%25zz&b=%EF%BF%BD,https://xn--xample-9ua.com/path?a=%25zz&b=%EF%BF%BD;https://example.com/path?a=%25zz&b=%EF%BF%BD,example.com;https://example.com/path?a=%25zz&b=%EF%BF%BD,example.com",
)?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_3_3_numeric_validity_recomputes_after_min_max_step_mutations_across_supported_types()
-> Result<()> {
let html = r#"
<input id='n' type='number' value='5' min='0' max='10' step='1'>
<input id='r' type='range' value='7' min='0' max='10' step='5'>
<input id='d' type='date' value='2026-01-10' min='2026-01-01' max='2026-01-31' step='1'>
<input id='t' type='time' value='10:37' min='09:00' max='11:00' step='60'>
<input id='dt' type='datetime-local' value='2026-01-01T10:37' min='2026-01-01T09:00' max='2026-01-01T12:00' step='60'>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
function state(id) {
const el = document.getElementById(id);
return [
el.validity.rangeUnderflow,
el.validity.rangeOverflow,
el.validity.stepMismatch,
el.checkValidity()
].join(':');
}
document.getElementById('run').addEventListener('click', () => {
const initial = [
state('n'),
state('r'),
state('d'),
state('t'),
state('dt')
].join(',');
const n = document.getElementById('n');
n.min = '6';
const nUnderflow = state('n');
n.min = '0';
n.max = '4';
const nOverflow = state('n');
n.max = '10';
n.step = '2';
const nStepMismatch = state('n');
n.step = 'any';
const nStepAny = state('n');
const r = document.getElementById('r');
const rInitialMismatch = state('r');
r.min = '2';
const rMinBaseAligned = state('r');
const d = document.getElementById('d');
d.min = '2026-01-15';
const dUnderflow = state('d');
d.min = '2026-01-01';
d.max = '2026-01-05';
const dOverflow = state('d');
d.max = '2026-01-31';
d.step = '7';
const dStepMismatch = state('d');
d.min = '2026-01-03';
const dStepAligned = state('d');
const t = document.getElementById('t');
t.min = '10:40';
const tUnderflow = state('t');
t.min = '09:00';
t.max = '10:00';
const tOverflow = state('t');
t.max = '11:00';
t.step = '900';
const tStepMismatch = state('t');
t.min = '10:07';
const tStepAligned = state('t');
const dt = document.getElementById('dt');
dt.min = '2026-01-01T10:40';
const dtUnderflow = state('dt');
dt.min = '2026-01-01T09:00';
dt.max = '2026-01-01T10:00';
const dtOverflow = state('dt');
dt.max = '2026-01-01T12:00';
dt.step = '900';
const dtStepMismatch = state('dt');
dt.min = '2026-01-01T10:07';
const dtStepAligned = state('dt');
document.getElementById('result').textContent = [
initial,
nUnderflow,
nOverflow,
nStepMismatch,
nStepAny,
rInitialMismatch,
rMinBaseAligned,
dUnderflow,
dOverflow,
dStepMismatch,
dStepAligned,
tUnderflow,
tOverflow,
tStepMismatch,
tStepAligned,
dtUnderflow,
dtOverflow,
dtStepMismatch,
dtStepAligned
].join('|');
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text(
"#result",
"false:false:false:true,false:false:false:true,false:false:false:true,false:false:false:true,false:false:false:true|true:false:false:false|false:true:false:false|false:false:true:false|false:false:false:true|false:false:false:true|false:false:false:true|true:false:false:false|false:true:false:false|false:false:true:false|false:false:false:true|true:false:false:false|false:true:false:false|false:false:true:false|false:false:false:true|true:false:false:false|false:true:false:false|false:false:true:false|false:false:false:true",
)?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_3_3_numeric_step_any_and_time_wrapped_range_validity_matrix_work()
-> Result<()> {
let html = r#"
<input id='num' type='number' min='2' max='4' step='any' value='3.3'>
<input id='date' type='date' min='2026-01-01' max='2026-01-31' step='any' value='2026-01-10'>
<input id='time-ok' type='time' min='23:00' max='01:00' step='1800' value='00:30'>
<input id='time-step-bad' type='time' min='23:00' max='01:00' step='1800' value='00:45'>
<input id='time-range-bad' type='time' min='23:00' max='01:00' step='1800' value='02:00'>
<input id='dt' type='datetime-local' min='2026-01-01T09:00' max='2026-01-01T12:00' step='any' value='2026-01-01T10:37'>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
function state(id) {
const el = document.getElementById(id);
return [
el.validity.rangeUnderflow,
el.validity.rangeOverflow,
el.validity.stepMismatch,
el.checkValidity()
].join(':');
}
document.getElementById('run').addEventListener('click', () => {
const initial = [
state('num'),
state('date'),
state('time-ok'),
state('time-step-bad'),
state('time-range-bad'),
state('dt')
].join(',');
document.getElementById('time-step-bad').step = 'any';
const timeStepAny = state('time-step-bad');
document.getElementById('dt').step = '90';
const dtExplicitStep = state('dt');
document.getElementById('result').textContent = [
initial,
timeStepAny,
dtExplicitStep
].join('|');
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text(
"#result",
"false:false:false:true,false:false:false:true,false:false:false:true,false:false:true:false,true:true:false:false,false:false:false:true|false:false:false:true|false:false:true:false",
)?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_3_3_numeric_step_base_prefers_min_then_value_attribute_and_rounding_boundary_work()
-> Result<()> {
let html = r#"
<input id='n' type='number' value='0.2' step='0.1'>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const n = document.getElementById('n');
n.value = '0.3';
const valueBase = [
n.validity.stepMismatch,
n.checkValidity()
].join(':');
n.min = '0.25';
const minBase = [
n.validity.stepMismatch,
n.checkValidity()
].join(':');
n.value = '0.35000000005';
const nearBoundary = [
n.validity.stepMismatch,
n.checkValidity()
].join(':');
n.value = '0.3501';
const farFromBoundary = [
n.validity.stepMismatch,
n.checkValidity()
].join(':');
document.getElementById('result').textContent = [
valueBase,
minBase,
nearBoundary,
farFromBoundary
].join('|');
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text("#result", "false:true|true:false|false:true|true:false")?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_3_3_parser_fast_path_matches_min_max_step_reflection_with_bracket_and_member_chain_access()
-> Result<()> {
let html = r#"
<input id='num' type='number' min='1' max='9' step='2' value='5'>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const initial = [
document.getElementById('num').min,
document.getElementById('num').max,
document.getElementById('num').step,
document.getElementById('num').validity['stepMismatch']
].join(',');
document.getElementById('num').min = '3';
document.getElementById('num').max = '11';
document.getElementById('num').step = '4';
const updated = [
document.getElementById('num').min + ':' + document.getElementById('num').getAttribute('min'),
document.getElementById('num').max + ':' + document.getElementById('num').getAttribute('max'),
document.getElementById('num').step + ':' + document.getElementById('num').getAttribute('step'),
document.getElementById('num').validity['stepMismatch']
].join(',');
document.getElementById('num').min = '';
document.getElementById('num').step = 'any';
const cleared = [
document.getElementById('num').min + ':' + document.getElementById('num').getAttribute('min'),
document.getElementById('num').step + ':' + document.getElementById('num').getAttribute('step'),
document.getElementById('num').validity['stepMismatch']
].join(',');
document.getElementById('result').textContent = [
initial,
updated,
cleared
].join('|');
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text("#result", "1,9,2,false|3:3,11:11,4:4,true|:,any:any,false")?;
Ok(())
}
#[test]
fn attribute_reflection_html_2_3_3_parser_static_bracket_assignment_matches_min_max_step_reflection_and_expando_paths()
-> Result<()> {
let html = r#"
<input id='num' type='number' min='1' max='9' step='2' value='5'>
<div id='box'></div>
<button id='run' type='button'>run</button>
<p id='result'></p>
<script>
document.getElementById('run').addEventListener('click', () => {
const initial = [
document.getElementById('num').min,
document.getElementById('num').max,
document.getElementById('num').step,
document.getElementById('box').min === undefined,
document.getElementById('box').step === undefined
].join(',');
document.getElementById('num')['min'] = '3';
document.getElementById('num')['max'] = '11';
document.getElementById('num')['step'] = '4';
document.getElementById('box')['min'] = 'shadow-min';
document.getElementById('box')['step'] = 'shadow-step';
const updated = [
document.getElementById('num').min + ':' + document.getElementById('num').getAttribute('min'),
document.getElementById('num').max + ':' + document.getElementById('num').getAttribute('max'),
document.getElementById('num').step + ':' + document.getElementById('num').getAttribute('step'),
document.getElementById('num').validity.stepMismatch,
document.getElementById('box').min + ':' + (document.getElementById('box').getAttribute('min') === null),
document.getElementById('box').step + ':' + (document.getElementById('box').getAttribute('step') === null)
].join(',');
document.getElementById('num')['min'] = '';
document.getElementById('num')['step'] = 'any';
document.getElementById('box')['max'] = 7;
const cleared = [
document.getElementById('num').min + ':' + document.getElementById('num').getAttribute('min'),
document.getElementById('num').step + ':' + document.getElementById('num').getAttribute('step'),
document.getElementById('num').validity.stepMismatch,
document.getElementById('box').max + ':' + (document.getElementById('box').getAttribute('max') === null)
].join(',');
document.getElementById('result').textContent = [
initial,
updated,
cleared
].join('|');
});
</script>
"#;
let mut h = Harness::from_html(html)?;
h.click("#run")?;
h.assert_text(
"#result",
"1,9,2,true,true|3:3,11:11,4:4,true,shadow-min:true,shadow-step:true|:,any:any,false,7:true",
)?;
Ok(())
}