tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit fb45177212c8fba300fb2d968174e87e1dab6fd5
parent 66629a4928953f615e9d39186f3d8ccf2367b74d
Author: Stephanie Y Zhang <stephanie.zhang@microsoft.com>
Date:   Thu, 30 Oct 2025 18:57:35 +0000

Bug 1996383 [wpt PR 55656] - [FormControlRange] Programmatic updates for value changes, a=testonly

Automatic update from web-platform-tests
[FormControlRange] Programmatic updates for value changes

This CL adds support for programmatic value mutations (e.g. .value= and
setRangeText()). User-driven live updates remain handled via
beforeinput/input snapshots from CL:7001481.

On programmatic changes, FormControlRange updates by diffing the old and
new values within the prior selection range. Full-value replacements
collapse to [0,0]; partial edits shift, contract, or collapse endpoints
around the edited region.

For setRangeText(), the automatic full-value diff is skipped so a
precise, span-bounded update can be emitted without duplicate updates.

Follow-up CLs will extend coverage to richer edit scenarios (e.g.
drag/drop, IME).

Explainer:
github.com/MicrosoftEdge/MSEdgeExplainers/blob/main/FormControlRange/explainer.md

Low-Coverage-Reason: COVERAGE_UNDERREPORTED
Bug: 421421332
Change-Id: Ia8bda2e60196dcfc0ca63b90841243efb08055f2
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7004105
Reviewed-by: Ana Sollano Kim <ansollan@microsoft.com>
Commit-Queue: Stephanie Zhang <stephanie.zhang@microsoft.com>
Reviewed-by: Mason Freed <masonf@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1535389}

--

wpt-commits: 515585b874b18eb985af1bbb9d6b4f2f2897ff46
wpt-pr: 55656

Diffstat:
Atesting/web-platform/tests/dom/ranges/tentative/FormControlRange-programmatic-updates.html | 314+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtesting/web-platform/tests/dom/ranges/tentative/FormControlRange-range-updates.html | 8++++++--
Mtesting/web-platform/tests/dom/ranges/tentative/FormControlRange-toString.html | 18++++++++++++------
3 files changed, 332 insertions(+), 8 deletions(-)

diff --git a/testing/web-platform/tests/dom/ranges/tentative/FormControlRange-programmatic-updates.html b/testing/web-platform/tests/dom/ranges/tentative/FormControlRange-programmatic-updates.html @@ -0,0 +1,314 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body></body> +<script> +'use strict'; + +// Verifies that FormControlRange updates its start/end offsets for +// programmatic text mutations mirroring DOM Range behavior in the controlโ€™s +// value space. Each test first exercises a DOM Range in a contenteditable div, +// then performs the equivalent operation with FormControlRange to ensure +// behavior parity. + +const controls = ['input', 'textarea']; + +function setup(control, value) { + document.body.innerHTML = control === 'input' ? '<input type="text">' : '<textarea></textarea>'; + const element = document.body.firstElementChild; + element.value = value; + element.focus(); + return element; +} + +function makeFormControlRange(element, start, end) { + const range = new FormControlRange(); + range.setFormControlRange(element, start, end); + return range; +} + +function setupEditable(value) { + // Create a contenteditable div with a single text node. + const editable = document.createElement('div'); + editable.setAttribute('contenteditable', 'true'); + editable.textContent = value; + document.body.appendChild(editable); + const text = editable.firstChild; + return { editable, text }; +} + +function makeDomRange(text, start, end) { + // Construct a DOM Range in the given text node. + const r = document.createRange(); + r.setStart(text, start); + r.setEnd(text, end); + return r; +} + +controls.forEach(control => { + test(() => { + // DOM: full replace (shorter) collapses to [0,0]. + const d1 = setupEditable('ABCDEFG'); + const domRange1 = makeDomRange(d1.text, 2, 5); + d1.text.replaceData(0, 7, 'XY'); + assert_equals(domRange1.startOffset, 0, 'DOM shorter: start collapsed to 0'); + assert_equals(domRange1.endOffset, 0, 'DOM shorter: end collapsed to 0'); + + // FormControlRange: full .value replacement (shorter) collapses to [0,0]. + let element = setup(control, 'ABCDEFG'); + let range = makeFormControlRange(element, 2, 5); + element.value = 'XY'; + assert_equals(range.startOffset, 0, 'FormControlRange shorter: start collapsed to 0'); + assert_equals(range.endOffset, 0, 'FormControlRange shorter: end collapsed to 0'); + + // DOM: full replace (longer) collapses to [0,0]. + const d2 = setupEditable('ABC'); + const domRange2 = makeDomRange(d2.text, 1, 3); + d2.text.replaceData(0, 3, 'ABCDEFGHIJKLMNOP'); + assert_equals(domRange2.startOffset, 0, 'DOM longer: start collapsed to 0'); + assert_equals(domRange2.endOffset, 0, 'DOM longer: end collapsed to 0'); + + // FormControlRange: full .value replacement (longer) collapses to [0,0]. + element = setup(control, 'ABC'); + range = makeFormControlRange(element, 1, 3); + element.value = 'ABCDEFGHIJKLMNOP'; + assert_equals(range.startOffset, 0, 'FormControlRange longer: start collapsed to 0'); + assert_equals(range.endOffset, 0, 'FormControlRange longer: end collapsed to 0'); + }, `Full replace collapses to start (shorter & longer) (${control})`); + + test(() => { + // DOM: full replace when prior range spans whole old value; collapse to [0,0]. + const d = setupEditable('ABCDE'); + const domRange = makeDomRange(d.text, 0, 5); + d.text.replaceData(0, 5, 'VWXYZ'); + assert_equals(domRange.startOffset, 0, 'DOM whole-old: start collapsed to 0'); + assert_equals(domRange.endOffset, 0, 'DOM whole-old: end collapsed to 0'); + + // FormControlRange: same scenario. + const element = setup(control, 'ABCDE'); + const range = makeFormControlRange(element, 0, 5); + element.value = 'VWXYZ'; + assert_equals(range.startOffset, 0, 'FormControlRange whole-old: start collapsed to 0'); + assert_equals(range.endOffset, 0, 'FormControlRange whole-old: end collapsed to 0'); + }, `Full replace from whole-old range collapses to 0 (${control})`); + + test(() => { + // DOM: full replace with equal length; collapse to [0,0]. + const d = setupEditable('ABCDE'); + const domRange = makeDomRange(d.text, 1, 4); + d.text.replaceData(0, 5, 'VWXYZ'); + assert_equals(domRange.startOffset, 0, 'DOM equal-length: start collapsed to 0'); + assert_equals(domRange.endOffset, 0, 'DOM equal-length: end collapsed to 0'); + + // FormControlRange: same scenario. + const element = setup(control, 'ABCDE'); + const range = makeFormControlRange(element, 1, 4); + element.value = 'VWXYZ'; + assert_equals(range.startOffset, 0, 'FormControlRange equal-length: start collapsed to 0'); + assert_equals(range.endOffset, 0, 'FormControlRange equal-length: end collapsed to 0'); + }, `Full replace (equal length) collapses to 0 (${control})`); + + test(() => { + // DOM: replace [3,7) with "XX". + const d = setupEditable('0123456789'); + const domRange = makeDomRange(d.text, 2, 8); + d.text.replaceData(3, 4, 'XX'); + assert_equals(d.editable.textContent, '012XX789', 'DOM value reflects replace'); + assert_equals(domRange.startOffset, 2, 'DOM start unchanged before replaced segment'); + assert_equals(domRange.endOffset, 6, 'DOM end adjusted by net delta'); + + // FormControlRange: same replace via setRangeText. + const element = setup(control, '0123456789'); + const range = makeFormControlRange(element, 2, 8); + element.setRangeText('XX', 3, 7); + assert_equals(element.value, '012XX789', 'FormControlRange value reflects setRangeText replace'); + assert_equals(range.startOffset, 2, 'FormControlRange start unchanged before replaced segment'); + assert_equals(range.endOffset, 6, 'FormControlRange end adjusted by net delta'); + }, `Partial replacement adjusts end (${control})`); + + test(() => { + // DOM: no mutation; range unchanged. + const d = setupEditable('HELLO'); + const domRange = makeDomRange(d.text, 1, 4); + assert_equals(domRange.startOffset, 1, 'DOM start unchanged on no-op'); + assert_equals(domRange.endOffset, 4, 'DOM end unchanged on no-op'); + + // FormControlRange: setting same value (no-op); range unchanged. + const element = setup(control, 'HELLO'); + const range = makeFormControlRange(element, 1, 4); + element.value = 'HELLO'; + assert_equals(range.startOffset, 1, 'FormControlRange start unchanged on no-op'); + assert_equals(range.endOffset, 4, 'FormControlRange end unchanged on no-op'); + }, `No-op leaves range unchanged (${control})`); + + test(() => { + // DOM: insert before the range; shift both endpoints by +1. + const d = setupEditable('ABCDE'); + const domRange = makeDomRange(d.text, 2, 4); + d.text.insertData(1, 'Q'); + assert_equals(d.editable.textContent, 'AQBCDE', 'DOM value after insertion before range'); + assert_equals(domRange.startOffset, 3, 'DOM start +1'); + assert_equals(domRange.endOffset, 5, 'DOM end +1'); + + // FormControlRange: same insert via setRangeText. + const element = setup(control, 'ABCDE'); + const range = makeFormControlRange(element, 2, 4); + element.setRangeText('Q', 1, 1); + assert_equals(element.value, 'AQBCDE', 'FormControlRange value after insertion before range'); + assert_equals(range.startOffset, 3, 'FormControlRange start +1'); + assert_equals(range.endOffset, 5, 'FormControlRange end +1'); + assert_equals(range.toString(), 'CD', 'FormControlRange range text stable'); + }, `Insertion before range shifts both endpoints (${control})`); + + test(() => { + // DOM: delete [2,3) inside the range; end shrinks by 1. + const d = setupEditable('ABCDE'); + const domRange = makeDomRange(d.text, 1, 5); + d.text.deleteData(2, 1); + assert_equals(d.editable.textContent, 'ABDE', 'DOM value after interior deletion'); + assert_equals(domRange.startOffset, 1, 'DOM start unchanged'); + assert_equals(domRange.endOffset, 4, 'DOM end -1'); + + // FormControlRange: same delete via setRangeText. + const element = setup(control, 'ABCDE'); + const range = makeFormControlRange(element, 1, 5); + element.setRangeText('', 2, 3); + assert_equals(element.value, 'ABDE', 'FormControlRange value after interior deletion'); + assert_equals(range.startOffset, 1, 'FormControlRange start unchanged'); + assert_equals(range.endOffset, 4, 'FormControlRange end -1'); + assert_equals(range.toString(), 'BDE', 'FormControlRange range reflects deletion'); + }, `Interior deletion shrinks end (${control})`); + + test(() => { + // DOM: replace before the range with net -2. + const d = setupEditable('ABCDEFGHIJ'); + const domRange = makeDomRange(d.text, 7, 10); + d.text.replaceData(2, 3, 'Z'); + assert_equals(d.editable.textContent, 'ABZFGHIJ', 'DOM value after before-range shrink'); + assert_equals(domRange.startOffset, 5, 'DOM start -2'); + assert_equals(domRange.endOffset, 8, 'DOM end -2'); + + // FormControlRange: same replacement via setRangeText. + const element = setup(control, 'ABCDEFGHIJ'); + const range = makeFormControlRange(element, 7, 10); + element.setRangeText('Z', 2, 5); + assert_equals(element.value, 'ABZFGHIJ', 'FormControlRange value after before-range shrink'); + assert_equals(range.startOffset, 5, 'FormControlRange start -2'); + assert_equals(range.endOffset, 8, 'FormControlRange end -2'); + assert_equals(range.toString(), 'HIJ', 'FormControlRange text unchanged'); + }, `Before-range shrink shifts left (${control})`); + + test(() => { + // DOM: edits after the range; unchanged. + const d = setupEditable('ABCDEFGHIJ'); + const domRange = makeDomRange(d.text, 2, 5); + d.text.replaceData(7, 2, 'WXYZ'); + assert_equals(d.editable.textContent, 'ABCDEFGWXYZJ', 'DOM value after after-range grow'); + assert_equals(domRange.startOffset, 2, 'DOM start unchanged'); + assert_equals(domRange.endOffset, 5, 'DOM end unchanged'); + + // FormControlRange: same replacement via setRangeText. + const element = setup(control, 'ABCDEFGHIJ'); + const range = makeFormControlRange(element, 2, 5); + element.setRangeText('WXYZ', 7, 9); + assert_equals(element.value, 'ABCDEFGWXYZJ', 'FormControlRange value after after-range grow'); + assert_equals(range.startOffset, 2, 'FormControlRange start unchanged'); + assert_equals(range.endOffset, 5, 'FormControlRange end unchanged'); + assert_equals(range.toString(), 'CDE', 'FormControlRange text unchanged'); + }, `After-range grow leaves range unchanged (${control})`); + + test(() => { + // DOM: superset replacement collapses to start of change. + const d = setupEditable('ABCDEFG'); + const domRange = makeDomRange(d.text, 2, 5); + d.text.replaceData(1, 5, 'Q'); + assert_equals(d.editable.textContent, 'AQG', 'DOM value after superset replacement'); + assert_equals(domRange.startOffset, 1, 'DOM collapsed to change start'); + assert_equals(domRange.endOffset, 1, 'DOM collapsed'); + + // FormControlRange: same replacement via setRangeText. + const element = setup(control, 'ABCDEFG'); + const range = makeFormControlRange(element, 2, 5); + element.setRangeText('Q', 1, 6); + assert_equals(element.value, 'AQG', 'FormControlRange value after superset replacement'); + assert_equals(range.startOffset, 1, 'FormControlRange collapsed to change start'); + assert_equals(range.endOffset, 1, 'FormControlRange collapsed'); + assert_true(range.collapsed, 'FormControlRange collapsed flag true'); + }, `Superset replacement collapses to change start (${control})`); + + test(() => { + // DOM: insert exactly at range.start; start unchanged, end advances. + const d = setupEditable('ABCDE'); + const domRange = makeDomRange(d.text, 2, 4); + d.text.insertData(2, 'QQ'); + assert_equals(d.editable.textContent, 'ABQQCDE', 'DOM value after insert at start boundary'); + assert_equals(domRange.startOffset, 2, 'DOM start unchanged at boundary'); + assert_equals(domRange.endOffset, 6, 'DOM end +2'); + + // FormControlRange: same insertion via setRangeText. + const element = setup(control, 'ABCDE'); + const range = makeFormControlRange(element, 2, 4); + element.setRangeText('QQ', 2, 2); + assert_equals(element.value, 'ABQQCDE', 'FormControlRange value after insert at start boundary'); + assert_equals(range.startOffset, 2, 'FormControlRange start unchanged at boundary'); + assert_equals(range.endOffset, 6, 'FormControlRange end +2'); + }, `Insert at range.start extends end (${control})`); + + test(() => { + // DOM: insert exactly at range.end; both boundaries unchanged. + const d = setupEditable('ABCDE'); + const domRange = makeDomRange(d.text, 2, 4); + d.text.insertData(4, 'QQ'); + assert_equals(d.editable.textContent, 'ABCDQQE', 'DOM value after insert at end boundary'); + assert_equals(domRange.startOffset, 2, 'DOM start unchanged'); + assert_equals(domRange.endOffset, 4, 'DOM end unchanged at boundary'); + + // FormControlRange: same insertion via setRangeText. + const element = setup(control, 'ABCDE'); + const range = makeFormControlRange(element, 2, 4); + element.setRangeText('QQ', 4, 4); + assert_equals(element.value, 'ABCDQQE', 'FormControlRange value after insert at end boundary'); + assert_equals(range.startOffset, 2, 'FormControlRange start unchanged'); + assert_equals(range.endOffset, 4, 'FormControlRange end unchanged at boundary'); + }, `Insert at range.end leaves range unchanged (${control})`); + + test(() => { + // DOM: replace [1,3) (emoji is 2 code units) with two emojis (4 code units). + const d = setupEditable('A๐Ÿ˜€BC'); + const domRange = makeDomRange(d.text, 1, 4); + d.text.replaceData(1, 2, '๐Ÿ™‚๐Ÿ™‚'); + assert_equals(domRange.startOffset, 1, 'DOM start unchanged inside growth'); + assert_equals(domRange.endOffset, 6, 'DOM end +2 code units'); + + // FormControlRange: same replacement via setRangeText. + const element = setup(control, 'A๐Ÿ˜€BC'); + const range = makeFormControlRange(element, 1, 4); + element.setRangeText('๐Ÿ™‚๐Ÿ™‚', 1, 3); + assert_equals(range.startOffset, 1, 'FormControlRange start unchanged inside growth'); + assert_equals(range.endOffset, 6, 'FormControlRange end +2 code units'); + assert_true(range.toString().length >= 3, 'FormControlRange range text non-empty after expansion'); + }, `Surrogate pair expansion grows end (${control})`); + + test(() => { + // DOM: chained interior edits accumulate deltas. + const d = setupEditable('012345'); + const domRange = makeDomRange(d.text, 2, 5); + d.text.replaceData(3, 1, 'XX'); // [3,4) -> "XX" + d.text.deleteData(3, 2); // delete the "XX" + assert_equals(d.editable.textContent, '01245', 'DOM final value after chained edits'); + assert_equals(domRange.startOffset, 2, 'DOM start unchanged across interior edits'); + assert_equals(domRange.endOffset, 4, 'DOM end reflects cumulative delta'); + + // FormControlRange: same edits via setRangeText. + const element = setup(control, '012345'); + const range = makeFormControlRange(element, 2, 5); + element.setRangeText('XX', 3, 4); + element.setRangeText('', 3, 5); + assert_equals(element.value, '01245', 'FormControlRange final value after chained edits'); + assert_equals(range.startOffset, 2, 'FormControlRange start unchanged across interior edits'); + assert_equals(range.endOffset, 4, 'FormControlRange end reflects cumulative delta'); + assert_equals(range.toString(), '24', 'FormControlRange final range text'); + }, `Chained interior edits cumulatively adjust range (${control})`); +}); +</script> diff --git a/testing/web-platform/tests/dom/ranges/tentative/FormControlRange-range-updates.html b/testing/web-platform/tests/dom/ranges/tentative/FormControlRange-range-updates.html @@ -99,7 +99,11 @@ test(() => { range.setFormControlRange(textarea, 1, 5); assert_equals(range.toString(), "rigi"); + // Full .value= replacement collapses the range to the start (0,0). textarea.value = "Modified"; - assert_equals(range.toString(), "odif"); -}, "FormControlRange reflects value changes on same element."); + assert_true(range.collapsed, "range collapses on full-value replacement"); + assert_equals(range.startOffset, 0); + assert_equals(range.endOffset, 0); + assert_equals(range.toString(), ""); +}, "FormControlRange collapses on full .value= replacement."); </script> diff --git a/testing/web-platform/tests/dom/ranges/tentative/FormControlRange-toString.html b/testing/web-platform/tests/dom/ranges/tentative/FormControlRange-toString.html @@ -9,11 +9,13 @@ test(() => { const range = new FormControlRange(); range.setFormControlRange(input, 0, 4); - assert_equals(range.toString(), "Test"); - + // Full replacement collapses to start. input.value = "New"; - assert_equals(range.toString(), "New"); -}, "FormControlRange toString() reflects current value."); + assert_true(range.collapsed, "range collapses on full replacement"); + assert_equals(range.startOffset, 0); + assert_equals(range.endOffset, 0); + assert_equals(range.toString(), ""); +}, "FormControlRange collapses on full .value= replacement."); test(() => { document.body.innerHTML = '<input type="text" value="Hello">'; @@ -21,9 +23,13 @@ test(() => { const range = new FormControlRange(); range.setFormControlRange(input, 1, 4); + // Full replacement (shorter) also collapses to start. input.value = "Hi"; - assert_equals(range.toString(), "i"); -}, "FormControlRange toString() clamps to available length."); + assert_true(range.collapsed, "range collapses on full replacement (shorter)"); + assert_equals(range.startOffset, 0); + assert_equals(range.endOffset, 0); + assert_equals(range.toString(), ""); +}, "FormControlRange collapses on full .value= replacement (shorter)."); test(() => { document.body.innerHTML = '<textarea>LongText</textarea>';