diff --git a/.agents/skills/perftest/SKILL.md b/.agents/skills/perftest/SKILL.md new file mode 100644 index 000000000000..261f515ef6ec --- /dev/null +++ b/.agents/skills/perftest/SKILL.md @@ -0,0 +1,205 @@ +--- +name: perftest +description: > + Run Firefox performance tests locally or in CI. Use when the user asks how to run a + perf test, wants to reproduce a performance alert or regression locally, needs the + mach invocation for Raptor, Talos, MozPerftest, AWSY, or browsertime, wants to push + perf tests to try, or mentions `mach try perf`, `mach perftest`, `mach raptor`, + `mach talos-test`, an alert summary ID, PerfCompare, or Compare View. Covers picking + the right harness, finding a test's name, and the local-vs-CI tradeoff. + Not for analyzing an existing Firefox profile (use profiler-analysis) or for + SpiderMonkey/JS engine benchmarking (use js-perf-investigation). +allowed-tools: + - Bash(./mach perftest --help:*) + - Bash(./mach raptor --help:*) + - Bash(./mach raptor --print-tests:*) + - Bash(./mach talos-test --help:*) + - Bash(./mach talos-test --print-tests:*) + - Bash(./mach talos-test --print-suites:*) + - Bash(./mach awsy-test --help:*) + - Bash(./mach try perf --help:*) + - Bash(./mach try perf --no-push:*) + - Read + - Grep + - Glob +--- + +# Running Firefox performance tests + +Any of these tests can be run locally. Whether a local run will *answer the +question* is a separate matter — check that first. + +## Local or CI? Check the platform before running anything + +**If the work is about a regression, compare the alert's platform to the machine +you are on.** A performance alert is platform-specific: the platform is in the +bug title and in the `Platform` column of the alert summary table. If they don't +match, a local run cannot reproduce the regression, and a clean local result +means nothing. + +| Situation | Do this | +|---|---| +| Alert platform ≠ your OS (e.g. Linux alert, you're on macOS) | Push to CI. A local run is not evidence. | +| Alert is on Android | Push to CI unless you have the device/emulator set up. | +| Alert platform = your OS | Local run is worth trying, but treat it as directional only. | +| No regression involved — writing a test, debugging a harness | Run locally. | + +Say this out loud to the user when it applies. Someone on a Mac chasing a Linux +alert will otherwise spend an afternoon on local runs that cannot show the +regression, and read the flat result as "already fixed". + +Even when the platform matches, CI hardware differs from a dev machine, so a +local number can disagree with CI in both directions. Use CI to decide whether a +patch regressed or fixed something; use local runs to iterate quickly on a fix +and to debug the test itself. + +`./mach try perf --alert ` is the shortest path when the platform doesn't +match — it runs what alerted, on the platform it alerted on. To target platforms +by hand, `--platforms` accepts `linux`, `macosx`, `windows`, `android`, +`android-a55`, and `desktop`. + +## Pick the harness + +| Harness | Command | What it covers | +|---|---|---| +| Raptor (incl. browsertime) | `./mach raptor` | Page load, benchmarks (speedometer, etc.), most desktop + mobile | +| Talos | `./mach talos-test` | Older desktop-only suites (tp5, damp, sessionrestore) | +| MozPerftest | `./mach perftest` | Custom scripts, mobile startup, xpcshell, alert replay | +| AWSY | `./mach awsy-test` | Memory usage | + +A test belongs to exactly one harness. If you don't know which, find the test in +`testing/perfdocs/generated/test-list.md` — it is generated from the in-tree +manifests and is the authoritative index. + +## Run locally + +A local build is required (`./mach build`), or pass an explicit binary. + +``` +./mach raptor -t speedometer-desktop # Raptor benchmark suite +./mach raptor -t google-search # Raptor page-load test +./mach talos-test -a damp # Talos, by active test +./mach talos-test --suite svgr # Talos, by suite +./mach perftest perftest_script.js # MozPerftest, by path +./mach perftest # MozPerftest, interactive picker +./mach awsy-test # AWSY +``` + +`./mach raptor -t` accepts either a suite name as printed by `--print-tests` +(`speedometer-desktop`) or an individual test defined inside that suite's TOML +(`speedometer3`, from `testing/raptor/raptor/tests/benchmarks/speedometer-desktop.toml`). +`--print-tests` only lists the suite level, so if a name from a bug or an alert +isn't in that output, grep the TOMLs under `testing/raptor/raptor/tests/` for it +before concluding it doesn't exist. + +Useful across harnesses: + +- `--app {firefox,chrome,geckoview,fenix,...}` — target a different browser +- `-b/--binary PATH` — test a binary other than your objdir build +- `--gecko-profile` — capture a profile during the run (then use `profiler-analysis`) + +MozPerftest writes results to a top-level `artifacts/` folder by default +(`--output` to change it). + +### Cut run time: always lower the post-startup delay + +Raptor waits `POST_DELAY_DEFAULT = 30000` ms after each browser start before the +test begins (`testing/raptor/raptor/perftest.py`). That is 30s **per browser +cycle**, and it dominates wall-clock on short tests. Drop it to 1 ms by default: + +``` +./mach raptor -t google-search --post-startup-delay 1 +./mach perftest test.js --browsertime-extra-options 'browsertime.post_startup_delay=1' +./mach try perf --extra-args post-startup-delay=1 +``` + +The spelling differs per entry point — Raptor takes a real flag in ms, MozPerftest +passes it through to browsertime as a `key=value` pair (comma-separate several), +and `mach try perf` uses `--extra-args`. Talos and AWSY have no equivalent. + +Use it for iterating on a fix, debugging a test, or confirming a test runs at all. +Leave it at the default when the number itself has to be trustworthy — a shorter +settle time means the browser is still warming up, which adds noise and shifts +results away from what CI measures. + +For a fully custom page-load run, Raptor exposes a generic `browsertime` test: + +``` +./mach raptor -t browsertime \ + --browsertime-arg test_script=pageload \ + --browsertime-arg browsertime.url=https://example.com \ + --browsertime-arg iterations=3 +``` + +`test_script` accepts `pageload`, `interactive`, or a path. This generic test is +local-only. Use `./mach raptor`, not `./mach browsertime`, when you care about +profiles — `./mach browsertime` does not symbolicate. + +## Find a test's name + +``` +./mach raptor --print-tests +./mach talos-test --print-tests +./mach talos-test --print-suites +``` + +Or read the generated docs, which include per-test descriptions: +`testing/perfdocs/generated/{raptor,talos,mozperftest,awsy,test-list}.md`. + +## Run in CI + +`./mach try perf` is the perf-specific try selector. It shows *categories* of +tasks rather than raw task names, so you don't need to know platform strings. + +``` +./mach try perf # interactive category selector +./mach try perf -q "speedometer" # non-interactive, query the categories +./mach try perf --no-push # print the selected tasks, push nothing +``` + +It creates **two** pushes: one with your patches, and one on the base revision +they sit on. It prints a PerfCompare link that compares them once both finish. + +Flags worth knowing: + +- `--show-all` / `--full` — fall back to the fuzzy selector over every task. + Some tests (e.g. the mobile startup ones) exist only here, not in a category. +- `--single-run` — skip the base push and the comparison +- `--variants fission live-sites profiling ...` — expand the category list +- `--platforms` / `--apps` — narrow what the selector offers +- `--chrome`, `--safari`, `--custom-car` — include other browsers (off by default) +- `-t/--tests amazon speedometer3` — select every task running these tests +- `--rebuild N` — run each selected task N times +- `--extra-args post-startup-delay=1` — cut 30s per browser cycle + +## Reproduce a performance alert + +Given an alert summary ID from a regression bug (Perfherder's alert table): + +``` +./mach try perf --alert 12345 # CI: run everything that alerted, vs. base +./mach perftest 12345 # local: run the alerting tests +./mach perftest 12345 --alert-exact # use CI's exact command/options +./mach perftest 12345 --alert-tests webaudio # only these tests from the alert +``` + +`--alert-exact` pulls the options from the task that triggered the alert, which +is what you want when a local run disagrees with CI. + +## Gotchas + +- **The base push is cached.** `--rebuild N` only applies to the first try run + made against a given base revision. Clear it with `--clear-cache`. +- **`--no-push` still computes everything** — it is the cheap way to check what a + category expands to before spending CI time. +- Pushing to try is outward-facing; confirm with the user before running a + `./mach try perf` that actually pushes. +- Perf runs are slow. Redirect output to a file under `artifacts/` and read that, + rather than piping through `tail`/`grep` and re-running. + +## Reference + +- `testing/performance/perftest-in-a-nutshell/perfdocs/index.md` — end-to-end + guide from alert to fix +- `testing/performance/mach-try-perf/perfdocs/` — try perf and CompareView +- `python/mozperftest/perfdocs/` — MozPerftest running/writing/developing diff --git a/.claude/skills/perftest/SKILL.md b/.claude/skills/perftest/SKILL.md new file mode 100644 index 000000000000..261f515ef6ec --- /dev/null +++ b/.claude/skills/perftest/SKILL.md @@ -0,0 +1,205 @@ +--- +name: perftest +description: > + Run Firefox performance tests locally or in CI. Use when the user asks how to run a + perf test, wants to reproduce a performance alert or regression locally, needs the + mach invocation for Raptor, Talos, MozPerftest, AWSY, or browsertime, wants to push + perf tests to try, or mentions `mach try perf`, `mach perftest`, `mach raptor`, + `mach talos-test`, an alert summary ID, PerfCompare, or Compare View. Covers picking + the right harness, finding a test's name, and the local-vs-CI tradeoff. + Not for analyzing an existing Firefox profile (use profiler-analysis) or for + SpiderMonkey/JS engine benchmarking (use js-perf-investigation). +allowed-tools: + - Bash(./mach perftest --help:*) + - Bash(./mach raptor --help:*) + - Bash(./mach raptor --print-tests:*) + - Bash(./mach talos-test --help:*) + - Bash(./mach talos-test --print-tests:*) + - Bash(./mach talos-test --print-suites:*) + - Bash(./mach awsy-test --help:*) + - Bash(./mach try perf --help:*) + - Bash(./mach try perf --no-push:*) + - Read + - Grep + - Glob +--- + +# Running Firefox performance tests + +Any of these tests can be run locally. Whether a local run will *answer the +question* is a separate matter — check that first. + +## Local or CI? Check the platform before running anything + +**If the work is about a regression, compare the alert's platform to the machine +you are on.** A performance alert is platform-specific: the platform is in the +bug title and in the `Platform` column of the alert summary table. If they don't +match, a local run cannot reproduce the regression, and a clean local result +means nothing. + +| Situation | Do this | +|---|---| +| Alert platform ≠ your OS (e.g. Linux alert, you're on macOS) | Push to CI. A local run is not evidence. | +| Alert is on Android | Push to CI unless you have the device/emulator set up. | +| Alert platform = your OS | Local run is worth trying, but treat it as directional only. | +| No regression involved — writing a test, debugging a harness | Run locally. | + +Say this out loud to the user when it applies. Someone on a Mac chasing a Linux +alert will otherwise spend an afternoon on local runs that cannot show the +regression, and read the flat result as "already fixed". + +Even when the platform matches, CI hardware differs from a dev machine, so a +local number can disagree with CI in both directions. Use CI to decide whether a +patch regressed or fixed something; use local runs to iterate quickly on a fix +and to debug the test itself. + +`./mach try perf --alert ` is the shortest path when the platform doesn't +match — it runs what alerted, on the platform it alerted on. To target platforms +by hand, `--platforms` accepts `linux`, `macosx`, `windows`, `android`, +`android-a55`, and `desktop`. + +## Pick the harness + +| Harness | Command | What it covers | +|---|---|---| +| Raptor (incl. browsertime) | `./mach raptor` | Page load, benchmarks (speedometer, etc.), most desktop + mobile | +| Talos | `./mach talos-test` | Older desktop-only suites (tp5, damp, sessionrestore) | +| MozPerftest | `./mach perftest` | Custom scripts, mobile startup, xpcshell, alert replay | +| AWSY | `./mach awsy-test` | Memory usage | + +A test belongs to exactly one harness. If you don't know which, find the test in +`testing/perfdocs/generated/test-list.md` — it is generated from the in-tree +manifests and is the authoritative index. + +## Run locally + +A local build is required (`./mach build`), or pass an explicit binary. + +``` +./mach raptor -t speedometer-desktop # Raptor benchmark suite +./mach raptor -t google-search # Raptor page-load test +./mach talos-test -a damp # Talos, by active test +./mach talos-test --suite svgr # Talos, by suite +./mach perftest perftest_script.js # MozPerftest, by path +./mach perftest # MozPerftest, interactive picker +./mach awsy-test # AWSY +``` + +`./mach raptor -t` accepts either a suite name as printed by `--print-tests` +(`speedometer-desktop`) or an individual test defined inside that suite's TOML +(`speedometer3`, from `testing/raptor/raptor/tests/benchmarks/speedometer-desktop.toml`). +`--print-tests` only lists the suite level, so if a name from a bug or an alert +isn't in that output, grep the TOMLs under `testing/raptor/raptor/tests/` for it +before concluding it doesn't exist. + +Useful across harnesses: + +- `--app {firefox,chrome,geckoview,fenix,...}` — target a different browser +- `-b/--binary PATH` — test a binary other than your objdir build +- `--gecko-profile` — capture a profile during the run (then use `profiler-analysis`) + +MozPerftest writes results to a top-level `artifacts/` folder by default +(`--output` to change it). + +### Cut run time: always lower the post-startup delay + +Raptor waits `POST_DELAY_DEFAULT = 30000` ms after each browser start before the +test begins (`testing/raptor/raptor/perftest.py`). That is 30s **per browser +cycle**, and it dominates wall-clock on short tests. Drop it to 1 ms by default: + +``` +./mach raptor -t google-search --post-startup-delay 1 +./mach perftest test.js --browsertime-extra-options 'browsertime.post_startup_delay=1' +./mach try perf --extra-args post-startup-delay=1 +``` + +The spelling differs per entry point — Raptor takes a real flag in ms, MozPerftest +passes it through to browsertime as a `key=value` pair (comma-separate several), +and `mach try perf` uses `--extra-args`. Talos and AWSY have no equivalent. + +Use it for iterating on a fix, debugging a test, or confirming a test runs at all. +Leave it at the default when the number itself has to be trustworthy — a shorter +settle time means the browser is still warming up, which adds noise and shifts +results away from what CI measures. + +For a fully custom page-load run, Raptor exposes a generic `browsertime` test: + +``` +./mach raptor -t browsertime \ + --browsertime-arg test_script=pageload \ + --browsertime-arg browsertime.url=https://example.com \ + --browsertime-arg iterations=3 +``` + +`test_script` accepts `pageload`, `interactive`, or a path. This generic test is +local-only. Use `./mach raptor`, not `./mach browsertime`, when you care about +profiles — `./mach browsertime` does not symbolicate. + +## Find a test's name + +``` +./mach raptor --print-tests +./mach talos-test --print-tests +./mach talos-test --print-suites +``` + +Or read the generated docs, which include per-test descriptions: +`testing/perfdocs/generated/{raptor,talos,mozperftest,awsy,test-list}.md`. + +## Run in CI + +`./mach try perf` is the perf-specific try selector. It shows *categories* of +tasks rather than raw task names, so you don't need to know platform strings. + +``` +./mach try perf # interactive category selector +./mach try perf -q "speedometer" # non-interactive, query the categories +./mach try perf --no-push # print the selected tasks, push nothing +``` + +It creates **two** pushes: one with your patches, and one on the base revision +they sit on. It prints a PerfCompare link that compares them once both finish. + +Flags worth knowing: + +- `--show-all` / `--full` — fall back to the fuzzy selector over every task. + Some tests (e.g. the mobile startup ones) exist only here, not in a category. +- `--single-run` — skip the base push and the comparison +- `--variants fission live-sites profiling ...` — expand the category list +- `--platforms` / `--apps` — narrow what the selector offers +- `--chrome`, `--safari`, `--custom-car` — include other browsers (off by default) +- `-t/--tests amazon speedometer3` — select every task running these tests +- `--rebuild N` — run each selected task N times +- `--extra-args post-startup-delay=1` — cut 30s per browser cycle + +## Reproduce a performance alert + +Given an alert summary ID from a regression bug (Perfherder's alert table): + +``` +./mach try perf --alert 12345 # CI: run everything that alerted, vs. base +./mach perftest 12345 # local: run the alerting tests +./mach perftest 12345 --alert-exact # use CI's exact command/options +./mach perftest 12345 --alert-tests webaudio # only these tests from the alert +``` + +`--alert-exact` pulls the options from the task that triggered the alert, which +is what you want when a local run disagrees with CI. + +## Gotchas + +- **The base push is cached.** `--rebuild N` only applies to the first try run + made against a given base revision. Clear it with `--clear-cache`. +- **`--no-push` still computes everything** — it is the cheap way to check what a + category expands to before spending CI time. +- Pushing to try is outward-facing; confirm with the user before running a + `./mach try perf` that actually pushes. +- Perf runs are slow. Redirect output to a file under `artifacts/` and read that, + rather than piping through `tail`/`grep` and re-running. + +## Reference + +- `testing/performance/perftest-in-a-nutshell/perfdocs/index.md` — end-to-end + guide from alert to fix +- `testing/performance/mach-try-perf/perfdocs/` — try perf and CompareView +- `python/mozperftest/perfdocs/` — MozPerftest running/writing/developing diff --git a/accessible/pdf/PdfStructTreeBuilder.cpp b/accessible/pdf/PdfStructTreeBuilder.cpp index bd7143fdf8d5..af0b35042587 100644 --- a/accessible/pdf/PdfStructTreeBuilder.cpp +++ b/accessible/pdf/PdfStructTreeBuilder.cpp @@ -234,9 +234,13 @@ void PdfStructTreeBuilder::BuildStructSubtree( if (!cell) { break; } + // Query each axis separately so one doesn't suppress the other's implicit + // headers. nsTArray accHeaders; cell->ColHeaderCells(&accHeaders); - cell->RowHeaderCells(&accHeaders); + nsTArray accRowHeaders; + cell->RowHeaderCells(&accRowHeaders); + accHeaders.AppendElements(std::move(accRowHeaders)); std::vector pdfHeaders; pdfHeaders.reserve(accHeaders.Length()); for (Accessible* accHeader : accHeaders) { diff --git a/accessible/tests/browser/pdfOutput/browser_structTree.js b/accessible/tests/browser/pdfOutput/browser_structTree.js index 139bf0683712..246534435657 100644 --- a/accessible/tests/browser/pdfOutput/browser_structTree.js +++ b/accessible/tests/browser/pdfOutput/browser_structTree.js @@ -94,12 +94,11 @@ addPdfStructTreeTest( role: "TR", children: [ { - // XXX pdf.js doesn't support attributes yet, so we can't - // test scope, headers, col/row span, etc. role: "TH", children: [ { role: "NonStruct", children: [{ content: ["tc1"] }] }, ], + scope: "Column", }, { role: "TH", @@ -109,6 +108,8 @@ addPdfStructTreeTest( children: [{ content: [" ", "tc2"] }], }, ], + structId: "id1", + scope: "Column", }, ], }, @@ -120,6 +121,8 @@ addPdfStructTreeTest( children: [ { role: "NonStruct", children: [{ content: ["tc3"] }] }, ], + structId: "id2", + scope: "Row", }, { role: "TD", @@ -129,6 +132,82 @@ addPdfStructTreeTest( children: [{ content: [" ", "tc4"] }], }, ], + headers: ["id1", "id2"], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + { chrome: true, topLevel: true } +); + +// A headers attribute naming only a column header retains implicit row headers. +addPdfStructTreeTest( + "testTableExplicitHeaders", + ` + + + +
c1c2
r1d
+ `, + [ + { + role: "Root", + children: [ + { + role: "Document", + children: [ + { + role: "Table", + children: [ + { + role: "TR", + children: [ + { + role: "TH", + children: [ + { role: "NonStruct", children: [{ content: ["c1"] }] }, + ], + scope: "Column", + }, + { + role: "TH", + children: [ + { + role: "NonStruct", + children: [{ content: [" ", "c2"] }], + }, + ], + structId: "id1", + scope: "Column", + }, + ], + }, + { + role: "TR", + children: [ + { + role: "TH", + children: [ + { role: "NonStruct", children: [{ content: ["r1"] }] }, + ], + structId: "id2", + scope: "Row", + }, + { + role: "TD", + children: [ + { + role: "NonStruct", + children: [{ content: [" ", "d"] }], + }, + ], + headers: ["id1", "id2"], }, ], }, diff --git a/accessible/tests/browser/pdfOutput/head.js b/accessible/tests/browser/pdfOutput/head.js index 861557b4cfc3..3f1f751af692 100644 --- a/accessible/tests/browser/pdfOutput/head.js +++ b/accessible/tests/browser/pdfOutput/head.js @@ -40,36 +40,57 @@ delete window.gDisableAccServiceInit; * match up the ids and separately test the struct tree and content items, this * function finds marked content items and inserts their strings directly into a * .content array on the struct tree node. + * + * Structure ids are generated from a counter covering every Accessible in the + * document, so they depend on things the tests don't describe; e.g. they differ + * between the topLevel and iframe variants of the same test. They are therefore + * renumbered in tree order. A cell can reference a header which comes later in + * the tree, so the headers references are remapped after the walk, once all ids + * are known. */ -function simplifyStructTreeNode(node, contentItems) { - if (node.type == "content") { - // Find the associated content items and append their strings to - // node.content. - node.content = []; - let inMarked = false; - for (const item of contentItems) { - if (item.type == "beginMarkedContentProps" && item.id == node.id) { - inMarked = true; - continue; - } - if (!inMarked) { - continue; - } - if (item.str) { - node.content.push(item.str); - continue; - } - if (item.type == "endMarkedContent") { - break; +function simplifyStructTree(root, contentItems) { + const structIds = new Map(); + const nodesWithHeaders = []; + const walk = node => { + if (node.type == "content") { + // Find the associated content items and append their strings to + // node.content. + node.content = []; + let inMarked = false; + for (const item of contentItems) { + if (item.type == "beginMarkedContentProps" && item.id == node.id) { + inMarked = true; + continue; + } + if (!inMarked) { + continue; + } + if (item.str) { + node.content.push(item.str); + continue; + } + if (item.type == "endMarkedContent") { + break; + } } + delete node.type; + delete node.id; } - delete node.type; - delete node.id; - } - if (node.children) { - for (const child of node.children) { - simplifyStructTreeNode(child, contentItems); + if (node.structId) { + const newId = `id${structIds.size + 1}`; + structIds.set(node.structId, newId); + node.structId = newId; } + if (node.headers) { + nodesWithHeaders.push(node); + } + for (const child of node.children || []) { + walk(child); + } + }; + walk(root); + for (const node of nodesWithHeaders) { + node.headers = node.headers.map(id => structIds.get(id) || id); } } @@ -231,7 +252,7 @@ async function assertPdfStructTree(pdf, pageTrees) { const contentItems = ( await page.getTextContent({ includeMarkedContent: true }) ).items; - simplifyStructTreeNode(actualTree, contentItems); + simplifyStructTree(actualTree, contentItems); SimpleTest.isDeeply( actualTree, pageTrees[p], diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index d66fdfccd051..1074e738b4d5 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -493,6 +493,15 @@ pref("browser.urlbar.deduplication.enabled", true); pref("browser.urlbar.scotchBonnet.enableOverride", true); +// Whether the search button declines to be the target of the toolbar tab stop +// in front of the address bar and the search bar, so that Tab lands on the +// input and the button is reached with Shift+Tab from there. +#ifdef NIGHTLY_BUILD +pref("browser.urlbar.searchModeSwitcher.skipTabStop", true); +#else +pref("browser.urlbar.searchModeSwitcher.skipTabStop", false); +#endif + pref("browser.urlbar.trackerCount.featureGate", true); pref("browser.urlbar.trackerCount.enabled", true); diff --git a/browser/base/content/browser-main.js b/browser/base/content/browser-main.js index 1ff101aecefc..041c1c1ccc75 100644 --- a/browser/base/content/browser-main.js +++ b/browser/base/content/browser-main.js @@ -20,8 +20,10 @@ Services.scriptloader.loadSubScript("chrome://browser/content/browser-unified-extensions.js", this); Services.scriptloader.loadSubScript("chrome://browser/content/tabbrowser/drag-and-drop.js", this); Services.scriptloader.loadSubScript("chrome://browser/content/tabbrowser/split-view-footer.js", this); + Services.scriptloader.loadSubScript("chrome://browser/content/tabbrowser/status-panel.js", this); Services.scriptloader.loadSubScript("chrome://browser/content/tabbrowser/tab.js", this); Services.scriptloader.loadSubScript("chrome://browser/content/tabbrowser/tabbrowser.js", this); + Services.scriptloader.loadSubScript("chrome://browser/content/tabbrowser/tab-bar-visibility.js", this); Services.scriptloader.loadSubScript("chrome://browser/content/tabbrowser/tab-context-menu.js", this); Services.scriptloader.loadSubScript("chrome://browser/content/tabbrowser/tabgroup.js", this); Services.scriptloader.loadSubScript("chrome://browser/content/tabbrowser/tabgroup-menu.js", this); diff --git a/browser/base/content/browser-toolbarKeyNav.js b/browser/base/content/browser-toolbarKeyNav.js index 5b27fe275d08..70f932140818 100644 --- a/browser/base/content/browser-toolbarKeyNav.js +++ b/browser/base/content/browser-toolbarKeyNav.js @@ -19,6 +19,9 @@ * In addition to linear navigation with tab and arrows, users can also type * the first (or first few) characters of a button's name to jump directly to * that button. + * Controls can opt out of all of this with keyNav="false", or keep arrow + * navigation but decline to be a tab stop's target with keyNav="skipTabStop", + * which is for controls that are reachable from an adjacent tab stop. */ ToolbarKeyboardNavigator = { @@ -156,6 +159,11 @@ ToolbarKeyboardNavigator = { }, _focusButton(aButton) { + if (aButton.hasAttribute("tabindex")) { + // The button manages its own tabindex. + aButton.focus(); + return; + } // Toolbar buttons aren't focusable because if they were, clicking them // would focus them, which is undesirable. Therefore, we must make a // button focusable only when we want to focus it. @@ -205,6 +213,9 @@ ToolbarKeyboardNavigator = { walker.currentNode = aEvent.target; let button = walker.nextNode(); + while (button?.getAttribute("keyNav") == "skipTabStop") { + button = walker.nextNode(); + } if (!button || !this._isButton(button)) { // If we think we're moving backward, and focus came from outside the // toolbox, we might actually have wrapped around. In this case, the diff --git a/browser/base/content/test/about/browser_aboutCertError_notYetValid.js b/browser/base/content/test/about/browser_aboutCertError_notYetValid.js index e612431d6fe2..75d59f8901ac 100644 --- a/browser/base/content/test/about/browser_aboutCertError_notYetValid.js +++ b/browser/base/content/test/about/browser_aboutCertError_notYetValid.js @@ -32,7 +32,7 @@ add_task(async function testNotYetValidCert() { await SpecialPowers.spawn(browser, [certBase64], async cert => { const mockErrorInfo = { errorCodeString: "MOZILLA_PKIX_ERROR_NOT_YET_VALID_CERTIFICATE", - errorIsOverridable: false, + errorIsOverridable: true, channelStatus: 0, overridableErrorCategory: "expired-or-not-yet-valid", validNotBefore: Date.now() + 1000 * 1000, @@ -55,6 +55,7 @@ add_task(async function testNotYetValidCert() { netErrorCard.resolvedErrorId = "MOZILLA_PKIX_ERROR_NOT_YET_VALID_CERTIFICATE"; netErrorCard.errorConfig = netErrorCard.getErrorConfig(); + netErrorCard.hideExceptionButton = netErrorCard.shouldHideExceptionButton(); await netErrorCard.getUpdateComplete(); netErrorCard.advancedButton.scrollIntoView(); diff --git a/browser/base/content/test/about/browser_aboutCertError_revoked.js b/browser/base/content/test/about/browser_aboutCertError_revoked.js index a731732325a6..e5a09b15f694 100644 --- a/browser/base/content/test/about/browser_aboutCertError_revoked.js +++ b/browser/base/content/test/about/browser_aboutCertError_revoked.js @@ -56,10 +56,10 @@ add_task(async function checkRevokedCertificateAdvancedCopy() { netErrorCard.domainMismatchNamesPromise = null; netErrorCard.certificateErrorText = null; netErrorCard.certificateErrorTextPromise = null; + netErrorCard.errorConfig = netErrorCard.getErrorConfig(); netErrorCard.hideExceptionButton = netErrorCard.shouldHideExceptionButton( info.errorCodeString ); - netErrorCard.errorConfig = netErrorCard.getErrorConfig(); netErrorCard.requestUpdate(); await netErrorCard.getUpdateComplete(); diff --git a/browser/base/content/test/about/browser_aboutCertError_untrustedIssuer.js b/browser/base/content/test/about/browser_aboutCertError_untrustedIssuer.js index a06785ee7c58..2e21ee26cac5 100644 --- a/browser/base/content/test/about/browser_aboutCertError_untrustedIssuer.js +++ b/browser/base/content/test/about/browser_aboutCertError_untrustedIssuer.js @@ -52,8 +52,8 @@ add_task(async function checkUntrustedCertIssuerCopy() { const info = Cu.cloneInto(mockErrorInfo, netErrorCard); netErrorCard.errorInfo = info; netErrorCard.resolvedErrorId = "SEC_ERROR_UNTRUSTED_ISSUER"; - netErrorCard.hideExceptionButton = netErrorCard.shouldHideExceptionButton(); netErrorCard.errorConfig = netErrorCard.getErrorConfig(); + netErrorCard.hideExceptionButton = netErrorCard.shouldHideExceptionButton(); await netErrorCard.getUpdateComplete(); netErrorCard.advancedButton.scrollIntoView(); diff --git a/browser/base/content/test/about/browser_aboutNetError_invalid_cert_noUserFix.js b/browser/base/content/test/about/browser_aboutNetError_invalid_cert_noUserFix.js index b341ed5c13e8..a72535816651 100644 --- a/browser/base/content/test/about/browser_aboutNetError_invalid_cert_noUserFix.js +++ b/browser/base/content/test/about/browser_aboutNetError_invalid_cert_noUserFix.js @@ -51,6 +51,7 @@ add_task(async function checkNoUserFixCertErrors() { }; const info = Cu.cloneInto(mockErrorInfo, netErrorCard); netErrorCard.errorInfo = info; + netErrorCard.resolvedErrorId = errorCode; netErrorCard.errorConfig = netErrorCard.getErrorConfig(); netErrorCard.advancedShowing = false; netErrorCard.hideExceptionButton = netErrorCard.shouldHideExceptionButton( @@ -72,9 +73,7 @@ add_task(async function checkNoUserFixCertErrors() { () => netErrorCard.whyDangerous, `The 'Why Dangerous' copy should be rendered for ${errorCode}.` ); - const l10nId = netErrorCard.getNSSErrorWhyDangerousL10nId( - netErrorCard.whyDangerous.dataset.l10nId - ); + const l10nId = netErrorCard.getNSSErrorWhyDangerousL10nId(errorCode); Assert.ok( netErrorCard.advancedShowing, diff --git a/browser/base/content/test/contextMenu/file_blocked_image_protocols.html b/browser/base/content/test/contextMenu/file_blocked_image_protocols.html index fd7be43d8653..fb0339d84473 100644 --- a/browser/base/content/test/contextMenu/file_blocked_image_protocols.html +++ b/browser/base/content/test/contextMenu/file_blocked_image_protocols.html @@ -5,7 +5,7 @@ - + diff --git a/browser/base/content/test/keyboard/browser_toolbarKeyNav.js b/browser/base/content/test/keyboard/browser_toolbarKeyNav.js index 10bd5ad738ee..dc05186e19b8 100644 --- a/browser/base/content/test/keyboard/browser_toolbarKeyNav.js +++ b/browser/base/content/test/keyboard/browser_toolbarKeyNav.js @@ -134,6 +134,9 @@ add_setup(async function () { // onorous and creates issues with existing tests without improving test // coverage, so disable it herein. ["browser.taskbarTabs.enabled", false], + // The tab stops these tests walk are otherwise channel-dependent. The + // skipping variant is covered by testTabStopsSkippingSearchButton. + ["browser.urlbar.searchModeSwitcher.skipTabStop", false], ], }); resetToolbarWithoutDevEditionButtons(); @@ -259,6 +262,48 @@ add_task(async function testTabStopsPageLoaded() { await doTestTabStopsPageLoaded(true); }); +// Test tab stops with the search button declining to be one. +add_task(async function testTabStopsSkippingSearchButton() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.searchModeSwitcher.skipTabStop", true]], + }); + const searchButton = "#urlbar-container .searchmode-switcher"; + + AddHomeBesideReload(); + await withNewBlankTab(async function () { + startFromUrlBar(); + await expectFocusAfterKey("Shift+Tab", searchButton, true); + await expectFocusAfterKey("Tab", gURLBar.inputField); + await expectFocusAfterKey("Shift+Tab", searchButton, true); + if (sidebarRevampEnabled) { + await expectFocusAfterKey("Shift+Tab", "sidebar-button"); + await expectFocusAfterKey("ArrowRight", "home-button"); + } else { + await expectFocusAfterKey("Shift+Tab", "home-button"); + } + await expectFocusAfterKey("Tab", gURLBar.inputField); + }); + RemoveHomeButton(); + + await BrowserTestUtils.withNewTab("https://example.com", async function () { + await waitUntilReloadEnabled(); + startFromUrlBar(); + await expectFocusAfterKey("Shift+Tab", searchButton, true); + if (sidebarRevampEnabled) { + await expectFocusAfterKey("Shift+Tab", "sidebar-button"); + await expectFocusAfterKey("ArrowRight", "reload-button"); + } else { + await expectFocusAfterKey("Shift+Tab", "reload-button"); + } + // The first site information button takes the tab stop the search button + // declined. + await expectFocusAfterKey("Tab", "trust-icon-container"); + await expectFocusAfterKey("Tab", gURLBar.inputField); + }); + + await SpecialPowers.popPrefEnv(); +}); + // Test tab stops with a notification anchor visible. // The notification anchor should not get its own tab stop. add_task(async function testTabStopsWithNotification() { diff --git a/browser/base/content/test/performance/browser_startup_content.js b/browser/base/content/test/performance/browser_startup_content.js index 73443c82b83d..90f6b957ae89 100644 --- a/browser/base/content/test/performance/browser_startup_content.js +++ b/browser/base/content/test/performance/browser_startup_content.js @@ -46,6 +46,10 @@ const known_scripts = { // Extensions "resource://gre/modules/ExtensionProcessScript.sys.mjs", "resource://gre/modules/ExtensionUtils.sys.mjs", + + // Enterprise policies + "resource://gre/modules/EnterprisePolicies.sys.mjs", + "resource://gre/modules/EnterprisePoliciesContent.sys.mjs", ]), frameScripts: new Set([ // Test related diff --git a/browser/components/aiwindow/ui/test/browser/browser_aiwindow_smartbar_context_paste.js b/browser/components/aiwindow/ui/test/browser/browser_aiwindow_smartbar_context_paste.js index bb9681118862..bc8e6ca84edc 100644 --- a/browser/components/aiwindow/ui/test/browser/browser_aiwindow_smartbar_context_paste.js +++ b/browser/components/aiwindow/ui/test/browser/browser_aiwindow_smartbar_context_paste.js @@ -230,7 +230,9 @@ add_task(async function test_smartbar_context_menu_paste_and_go_submits() { () => smartbar.value === "" ); Assert.ok( - loadURL.calledWith(sinon.match({ url: PASTE_URL })), + loadURL.calledWith( + sinon.match({ loadRequest: { urlLoad: { url: PASTE_URL } } }) + ), "Paste and Go loads the pasted URL" ); diff --git a/browser/components/aiwindow/ui/test/browser/browser_aiwindow_smartbar_suggestions.js b/browser/components/aiwindow/ui/test/browser/browser_aiwindow_smartbar_suggestions.js index 152c2083bdca..4181332c3986 100644 --- a/browser/components/aiwindow/ui/test/browser/browser_aiwindow_smartbar_suggestions.js +++ b/browser/components/aiwindow/ui/test/browser/browser_aiwindow_smartbar_suggestions.js @@ -224,7 +224,7 @@ add_task(async function test_smartbar_click_on_suggestion_navigates() { "controller.loadURL should be called when clicking a suggestion" ); Assert.equal( - loadURLStub.firstCall.args[0].url, + loadURLStub.firstCall.args[0].loadRequest.urlLoad.url, testUrl, "Should navigate to the test URL" ); diff --git a/browser/components/aiwindow/ui/test/browser/head.js b/browser/components/aiwindow/ui/test/browser/head.js index f273c7d9724e..93f240ec1079 100644 --- a/browser/components/aiwindow/ui/test/browser/head.js +++ b/browser/components/aiwindow/ui/test/browser/head.js @@ -806,9 +806,9 @@ async function stubLoadURL(browser, { captureURL = false } = {}) { if (capture) { content._stubLoadURLCalled = false; content._stubLoadedURL = null; - smartbar.controller.loadURL = ({ url }) => { + smartbar.controller.loadURL = ({ loadRequest }) => { content._stubLoadURLCalled = true; - content._stubLoadedURL = url; + content._stubLoadedURL = loadRequest.urlLoad?.url ?? null; return {}; }; } else { diff --git a/browser/components/enterprisepolicies/Policies.sys.mjs b/browser/components/enterprisepolicies/Policies.sys.mjs index 81f9ed43b783..ddc961a5a177 100644 --- a/browser/components/enterprisepolicies/Policies.sys.mjs +++ b/browser/components/enterprisepolicies/Policies.sys.mjs @@ -3464,6 +3464,10 @@ export var Policies = { features.http = !policies.HttpsOnly; } + if ("DisableServiceWorkers" in policies) { + features.serviceworkers = !policies.DisableServiceWorkers; + } + return features; }, diff --git a/browser/components/enterprisepolicies/schemas/policies-schema.json b/browser/components/enterprisepolicies/schemas/policies-schema.json index a811bb7ecec0..e95be6e58f2c 100644 --- a/browser/components/enterprisepolicies/schemas/policies-schema.json +++ b/browser/components/enterprisepolicies/schemas/policies-schema.json @@ -3657,6 +3657,9 @@ }, "HttpsOnly": { "type": "boolean" + }, + "DisableServiceWorkers": { + "type": "boolean" } } } diff --git a/browser/components/enterprisepolicies/tests/browser/browser.toml b/browser/components/enterprisepolicies/tests/browser/browser.toml index 7ba43d385ed5..08c7ca723ee9 100644 --- a/browser/components/enterprisepolicies/tests/browser/browser.toml +++ b/browser/components/enterprisepolicies/tests/browser/browser.toml @@ -169,6 +169,13 @@ support-files = [ "shared-worker.js", ] +["browser_policy_sitepolicies_serviceworkers.js"] +support-files = [ + "sitepolicies_sw_fetch.html", + "sitepolicies_sw_framed.html", + "sw_fetch_intercept.js", +] + ["browser_policy_support_menu.js"] ["browser_policy_translateenabled.js"] diff --git a/browser/components/enterprisepolicies/tests/browser/browser_policy_sitepolicies_serviceworkers.js b/browser/components/enterprisepolicies/tests/browser/browser_policy_sitepolicies_serviceworkers.js new file mode 100644 index 000000000000..44fcfb08e04c --- /dev/null +++ b/browser/components/enterprisepolicies/tests/browser/browser_policy_sitepolicies_serviceworkers.js @@ -0,0 +1,470 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +const SUPPORT_FILES_PATH = + "browser/browser/components/enterprisepolicies/tests/browser"; + +const API_STATUS_ID = "api-status"; +const INTERCEPT_STATUS_ID = "intercept-status"; + +function unregisterAllServiceWorkers() { + let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService( + Ci.nsIServiceWorkerManager + ); + let regs = swm.getAllRegistrations(); + let promises = []; + for (let i = 0; i < regs.length; i++) { + let reg = regs.queryElementAt(i, Ci.nsIServiceWorkerRegistrationInfo); + let { promise, resolve, reject } = Promise.withResolvers(); + swm.unregister( + reg.principal, + { unregisterSucceeded: resolve, unregisterFailed: reject }, + reg.scope + ); + promises.push(promise); + } + return Promise.all(promises); +} + +async function waitForFrames(browser, count) { + await TestUtils.waitForCondition(() => { + let children = browser.browsingContext.children; + if (children.length != count) { + return false; + } + return children.every(child => child.currentURI.spec != "about:blank"); + }, `Waiting for ${count} frame(s) to load`); +} + +async function waitForStatus(bc, swExpected, description) { + let apiExpected = swExpected ? "sw-available" : "sw-not-supported"; + let interceptExpected = swExpected ? "sw-intercepted" : "sw-not-intercepted"; + await TestUtils.waitForCondition( + () => + SpecialPowers.spawn(bc, [], () => { + let apiEl = content.document.getElementById("api-status"); + let interceptEl = content.document.getElementById("intercept-status"); + return ( + apiEl && + apiEl.textContent != "pending" && + interceptEl && + interceptEl.textContent != "pending" + ); + }), + `Waiting for elements to be updated in test page` + ); + + let [apiStatus, interceptStatus] = await SpecialPowers.spawn(bc, [], () => { + return [ + content.document.getElementById("api-status").textContent, + content.document.getElementById("intercept-status").textContent, + ]; + }); + + Assert.equal(apiStatus, apiExpected, `${description} (api-status)`); + Assert.equal( + interceptStatus, + interceptExpected, + `${description} (intercept-status)` + ); +} + +async function goBack(browser) { + let pageShown = BrowserTestUtils.waitForContentEvent(browser, "pageshow"); + browser.browsingContext.goBack(); + await pageShown; +} + +add_task(async function test_serviceworker_api_hidden_on_blocked_site() { + // Pre-register SWs for both origins before applying policy. + await setupPolicyEngineWithJson({ policies: {} }); + + await BrowserTestUtils.withNewTab( + `https://example.org/${SUPPORT_FILES_PATH}/sitepolicies_sw_fetch.html`, + async browser => { + await waitForStatus( + browser.browsingContext, + true, + "SW registered for example.org" + ); + Assert.equal( + browser.browsingContext.serviceWorkersDisabledByPolicy, + false, + "BrowsingContext flag is false for example.org with no policy" + ); + } + ); + + await BrowserTestUtils.withNewTab( + `https://example.com/${SUPPORT_FILES_PATH}/sitepolicies_sw_fetch.html`, + async browser => { + await waitForStatus( + browser.browsingContext, + true, + "SW registered for example.com" + ); + Assert.equal( + browser.browsingContext.serviceWorkersDisabledByPolicy, + false, + "BrowsingContext flag is false for example.com with no policy" + ); + } + ); + + await setupPolicyEngineWithJson({ + policies: { + SitePolicies: [ + { + Match: ["*.example.com"], + Policies: { DisableServiceWorkers: true }, + }, + ], + }, + }); + + // Tab starting at a blocked site: verify status across two forward + // navigations, then verify both prior entries are in the BFCache and that + // back-navigations restore the correct state. + await BrowserTestUtils.withNewTab( + `https://example.com/${SUPPORT_FILES_PATH}/sitepolicies_sw_fetch.html`, + async browser => { + await waitForStatus( + browser.browsingContext, + false, + "navigator.serviceWorker should be hidden on blocked top-level site" + ); + Assert.equal( + browser.browsingContext.serviceWorkersDisabledByPolicy, + true, + "BrowsingContext flag is true for blocked site" + ); + + BrowserTestUtils.startLoadingURIString( + browser, + `https://example.org/${SUPPORT_FILES_PATH}/sitepolicies_sw_fetch.html` + ); + await BrowserTestUtils.browserLoaded(browser); + await waitForStatus( + browser.browsingContext, + true, + "navigator.serviceWorker should work on non-blocked top-level site" + ); + Assert.equal( + browser.browsingContext.serviceWorkersDisabledByPolicy, + false, + "BrowsingContext flag is false for non-blocked site" + ); + + BrowserTestUtils.startLoadingURIString( + browser, + `https://example.com/${SUPPORT_FILES_PATH}/sitepolicies_sw_fetch.html` + ); + await BrowserTestUtils.browserLoaded(browser); + await waitForStatus( + browser.browsingContext, + false, + "navigator.serviceWorker should be hidden on blocked top-level site after navigation" + ); + Assert.equal( + browser.browsingContext.serviceWorkersDisabledByPolicy, + true, + "BrowsingContext flag is true for blocked site after navigation" + ); + + let sh = browser.browsingContext.sessionHistory; + Assert.ok( + sh.getEntryAtIndex(sh.index - 1).isInBFCache, + "example.org entry is in BFCache" + ); + Assert.ok( + sh.getEntryAtIndex(sh.index - 2).isInBFCache, + "first example.com entry is in BFCache" + ); + + await goBack(browser); + await waitForStatus( + browser.browsingContext, + true, + "navigator.serviceWorker should work on non-blocked site after back navigation" + ); + Assert.equal( + browser.browsingContext.serviceWorkersDisabledByPolicy, + false, + "BrowsingContext flag is false after back navigation to non-blocked site" + ); + + await goBack(browser); + await waitForStatus( + browser.browsingContext, + false, + "navigator.serviceWorker should be hidden on blocked site after second back navigation" + ); + Assert.equal( + browser.browsingContext.serviceWorkersDisabledByPolicy, + true, + "BrowsingContext flag is true after second back navigation to blocked site" + ); + } + ); + + // Tab starting at a non-blocked site: same checks in the other direction. + await BrowserTestUtils.withNewTab( + `https://example.org/${SUPPORT_FILES_PATH}/sitepolicies_sw_fetch.html`, + async browser => { + await waitForStatus( + browser.browsingContext, + true, + "navigator.serviceWorker should work on non-blocked top-level site" + ); + Assert.equal( + browser.browsingContext.serviceWorkersDisabledByPolicy, + false, + "BrowsingContext flag is false for non-blocked site" + ); + + BrowserTestUtils.startLoadingURIString( + browser, + `https://example.com/${SUPPORT_FILES_PATH}/sitepolicies_sw_fetch.html` + ); + await BrowserTestUtils.browserLoaded(browser); + await waitForStatus( + browser.browsingContext, + false, + "navigator.serviceWorker should be hidden on blocked top-level site" + ); + Assert.equal( + browser.browsingContext.serviceWorkersDisabledByPolicy, + true, + "BrowsingContext flag is true for blocked site" + ); + + BrowserTestUtils.startLoadingURIString( + browser, + `https://example.org/${SUPPORT_FILES_PATH}/sitepolicies_sw_fetch.html` + ); + await BrowserTestUtils.browserLoaded(browser); + await waitForStatus( + browser.browsingContext, + true, + "navigator.serviceWorker should work on non-blocked top-level site after navigation" + ); + Assert.equal( + browser.browsingContext.serviceWorkersDisabledByPolicy, + false, + "BrowsingContext flag is false for non-blocked site after navigation" + ); + + let sh = browser.browsingContext.sessionHistory; + Assert.ok( + sh.getEntryAtIndex(sh.index - 1).isInBFCache, + "example.com entry is in BFCache" + ); + Assert.ok( + sh.getEntryAtIndex(sh.index - 2).isInBFCache, + "first example.org entry is in BFCache" + ); + + await goBack(browser); + await waitForStatus( + browser.browsingContext, + false, + "navigator.serviceWorker should be hidden on blocked site after back navigation" + ); + Assert.equal( + browser.browsingContext.serviceWorkersDisabledByPolicy, + true, + "BrowsingContext flag is true after back navigation to blocked site" + ); + + await goBack(browser); + await waitForStatus( + browser.browsingContext, + true, + "navigator.serviceWorker should work on non-blocked site after second back navigation" + ); + Assert.equal( + browser.browsingContext.serviceWorkersDisabledByPolicy, + false, + "BrowsingContext flag is false after second back navigation to non-blocked site" + ); + } + ); + + await unregisterAllServiceWorkers(); +}); + +// When a blocked site is the top-level, SW should be hidden in all frames +// regardless of frame origin. When a non-blocked site is the top-level, SW +// should work in all frames even if the frame's own origin is blocked. +add_task(async function test_serviceworker_subframe_semantics() { + // Pre-register SWs for both origins before applying policy. + await setupPolicyEngineWithJson({ policies: {} }); + + await BrowserTestUtils.withNewTab( + `https://example.org/${SUPPORT_FILES_PATH}/sitepolicies_sw_fetch.html`, + async browser => { + await waitForStatus( + browser.browsingContext, + true, + "SW registered for example.org" + ); + } + ); + + await BrowserTestUtils.withNewTab( + `https://example.com/${SUPPORT_FILES_PATH}/sitepolicies_sw_fetch.html`, + async browser => { + await waitForStatus( + browser.browsingContext, + true, + "SW registered for example.com" + ); + } + ); + + await setupPolicyEngineWithJson({ + policies: { + SitePolicies: [ + { + Match: ["*.example.com"], + Policies: { DisableServiceWorkers: true }, + }, + ], + }, + }); + + await BrowserTestUtils.withNewTab( + `https://example.org/${SUPPORT_FILES_PATH}/sitepolicies_sw_framed.html`, + async browser => { + await waitForFrames(browser, 2); + + await waitForStatus( + browser.browsingContext.children[0], + true, + "SW should work in example.com frame when top-level is not blocked" + ); + await waitForStatus( + browser.browsingContext.children[1], + true, + "SW should work in example.org frame when top-level is not blocked" + ); + Assert.equal( + browser.browsingContext.serviceWorkersDisabledByPolicy, + false, + "BrowsingContext flag is false when top-level is not blocked" + ); + + BrowserTestUtils.startLoadingURIString( + browser, + `https://example.com/${SUPPORT_FILES_PATH}/sitepolicies_sw_framed.html` + ); + await BrowserTestUtils.browserLoaded(browser); + await waitForFrames(browser, 2); + + await waitForStatus( + browser.browsingContext.children[0], + false, + "SW should be hidden in example.com frame when top-level is blocked" + ); + await waitForStatus( + browser.browsingContext.children[1], + false, + "SW should be hidden in example.org frame when top-level is blocked" + ); + Assert.equal( + browser.browsingContext.serviceWorkersDisabledByPolicy, + true, + "BrowsingContext flag is true when top-level is blocked" + ); + + BrowserTestUtils.startLoadingURIString( + browser, + `https://example.org/${SUPPORT_FILES_PATH}/sitepolicies_sw_framed.html` + ); + await BrowserTestUtils.browserLoaded(browser); + await waitForFrames(browser, 2); + + await waitForStatus( + browser.browsingContext.children[0], + true, + "SW should work in example.com frame after navigating back to unblocked top-level" + ); + await waitForStatus( + browser.browsingContext.children[1], + true, + "SW should work in example.org frame after navigating back to unblocked top-level" + ); + Assert.equal( + browser.browsingContext.serviceWorkersDisabledByPolicy, + false, + "BrowsingContext flag is false after navigating back to non-blocked top-level" + ); + } + ); + + await unregisterAllServiceWorkers(); +}); + +add_task(async function test_serviceworker_registration_survives_policy() { + // Register a service worker on example.com with no policy active. + await setupPolicyEngineWithJson({ policies: {} }); + + await BrowserTestUtils.withNewTab( + `https://example.com/${SUPPORT_FILES_PATH}/sitepolicies_sw_fetch.html`, + async browser => { + await waitForStatus( + browser.browsingContext, + true, + "SW registered for example.com" + ); + Assert.equal( + browser.browsingContext.serviceWorkersDisabledByPolicy, + false, + "BrowsingContext flag is false before policy is applied" + ); + } + ); + + let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService( + Ci.nsIServiceWorkerManager + ); + Assert.greater( + swm.getAllRegistrations().length, + 0, + "Service worker should be registered" + ); + + // Apply a policy that disables service workers for example.com. + await setupPolicyEngineWithJson({ + policies: { + SitePolicies: [ + { + Match: ["*.example.com"], + Policies: { DisableServiceWorkers: true }, + }, + ], + }, + }); + + // The registration should still exist — the policy blocks the API and + // interception based on the top-level site, not the registration itself. + let regsAfterPolicy = swm.getAllRegistrations(); + let exampleComRegs = []; + for (let i = 0; i < regsAfterPolicy.length; i++) { + let reg = regsAfterPolicy.queryElementAt( + i, + Ci.nsIServiceWorkerRegistrationInfo + ); + if (reg.principal.host.endsWith("example.com")) { + exampleComRegs.push(reg); + } + } + Assert.greater( + exampleComRegs.length, + 0, + "Service worker registration for example.com should survive policy application" + ); + + await unregisterAllServiceWorkers(); +}); diff --git a/browser/components/enterprisepolicies/tests/browser/sitepolicies_sw_fetch.html b/browser/components/enterprisepolicies/tests/browser/sitepolicies_sw_fetch.html new file mode 100644 index 000000000000..600ae093cb79 --- /dev/null +++ b/browser/components/enterprisepolicies/tests/browser/sitepolicies_sw_fetch.html @@ -0,0 +1,40 @@ + + + + + + +
pending
+
pending
+ + + + diff --git a/browser/components/enterprisepolicies/tests/browser/sitepolicies_sw_framed.html b/browser/components/enterprisepolicies/tests/browser/sitepolicies_sw_framed.html new file mode 100644 index 000000000000..6ffdf43cdb37 --- /dev/null +++ b/browser/components/enterprisepolicies/tests/browser/sitepolicies_sw_framed.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/browser/components/enterprisepolicies/tests/browser/sw_fetch_intercept.js b/browser/components/enterprisepolicies/tests/browser/sw_fetch_intercept.js new file mode 100644 index 000000000000..ef10985fd9a3 --- /dev/null +++ b/browser/components/enterprisepolicies/tests/browser/sw_fetch_intercept.js @@ -0,0 +1,17 @@ +self.addEventListener("install", () => { + self.skipWaiting(); +}); + +self.addEventListener("activate", event => { + event.waitUntil(self.clients.claim()); +}); + +self.addEventListener("fetch", event => { + if (event.request.url.includes("sw-intercepted-resource")) { + event.respondWith(new Response("intercepted")); + } else if (event.request.mode === "navigate") { + // Proxy navigation requests so the SW becomes the controller for pages + // loaded within an already-controlled tab (e.g. same-tab back-navigation). + event.respondWith(fetch(event.request)); + } +}); diff --git a/browser/components/enterprisepolicies/tests/xpcshell/test_sitepolicies.js b/browser/components/enterprisepolicies/tests/xpcshell/test_sitepolicies.js index 09eac391c24a..a03f8a59eb66 100644 --- a/browser/components/enterprisepolicies/tests/xpcshell/test_sitepolicies.js +++ b/browser/components/enterprisepolicies/tests/xpcshell/test_sitepolicies.js @@ -380,3 +380,105 @@ add_task(async function test_httpsOnlyPolicy() { assertHttpState("http://example.net/", true); assertHttpState("http://example.com/", false); }); + +function assertServiceWorkerState(url, isAllowed) { + let uri = Services.io.newURI(url); + let siteUri = Services.io.newURI( + Services.scriptSecurityManager.createContentPrincipal(uri, {}) + .siteOriginNoSuffix + ); + + Assert.equal( + Services.policies.isAllowedForURI("serviceworkers", siteUri), + isAllowed, + `Policy service should return the expected service worker state for ${url} (site: ${siteUri})` + ); +} + +add_task(async function test_disableServiceWorkersPolicy() { + // Empty policies allow service workers everywhere. + await setupPolicyEngineWithJson({ + policies: { + SitePolicies: [], + }, + }); + + assertServiceWorkerState("https://example.net/", true); + assertServiceWorkerState("https://example.org/", true); + assertServiceWorkerState("https://example.com/", true); + + // Simple match case. + await setupPolicyEngineWithJson({ + policies: { + SitePolicies: [ + { + Match: ["*.example.com"], + Policies: { + DisableServiceWorkers: true, + }, + }, + ], + }, + }); + + assertServiceWorkerState("https://example.net/", true); + assertServiceWorkerState("https://example.org/", true); + assertServiceWorkerState("https://example.com/", false); + assertServiceWorkerState("https://sub.example.com/", false); + + // No match implies all sites, with Exceptions acting as an allowlist. + await setupPolicyEngineWithJson({ + policies: { + SitePolicies: [ + { + Exceptions: ["*.example.com"], + Policies: { + DisableServiceWorkers: true, + }, + }, + ], + }, + }); + + assertServiceWorkerState("https://example.net/", false); + assertServiceWorkerState("https://example.com/", true); + + // DisableServiceWorkers: false explicitly allows. + await setupPolicyEngineWithJson({ + policies: { + SitePolicies: [ + { + Policies: { + DisableServiceWorkers: false, + }, + }, + ], + }, + }); + + assertServiceWorkerState("https://example.net/", true); + assertServiceWorkerState("https://example.com/", true); + + // DisableServiceWorkers coexists with DisableJit and HttpsOnly in same entry. + await setupPolicyEngineWithJson({ + policies: { + SitePolicies: [ + { + Match: ["*.example.com"], + Policies: { + DisableJit: true, + HttpsOnly: true, + DisableServiceWorkers: true, + }, + }, + ], + }, + }); + + assertServiceWorkerState("https://example.net/", true); + assertServiceWorkerState("https://example.com/", false); + assertJitState("https://example.net/", true); + assertJitState("https://example.com/", false); + assertHttpState("http://example.net/", true); + assertHttpState("http://example.com/", false); +}); diff --git a/browser/components/firefoxview/SyncedTabsController.sys.mjs b/browser/components/firefoxview/SyncedTabsController.sys.mjs index bfac64cc9f8e..8e605aa5adb5 100644 --- a/browser/components/firefoxview/SyncedTabsController.sys.mjs +++ b/browser/components/firefoxview/SyncedTabsController.sys.mjs @@ -129,7 +129,11 @@ export class SyncedTabsController { const win = event.target.documentGlobal; const { switchToTabHavingURI } = win.docShell.chromeEventHandler.documentGlobal; - switchToTabHavingURI("about:preferences#sync", true, {}); + switchToTabHavingURI( + "about:preferences?action=choose-what-to-sync#sync", + true, + {} + ); break; } } diff --git a/browser/components/firefoxview/firefox-view-synced-tabs-error-handler.sys.mjs b/browser/components/firefoxview/firefox-view-synced-tabs-error-handler.sys.mjs index 462247093b90..e9ec02cb2f02 100644 --- a/browser/components/firefoxview/firefox-view-synced-tabs-error-handler.sys.mjs +++ b/browser/components/firefoxview/firefox-view-synced-tabs-error-handler.sys.mjs @@ -206,9 +206,9 @@ export const SyncedTabsErrorHandler = { buttonLabel: "firefoxview-tabpickup-network-offline-primarybutton", }, [ErrorType.SYNC_DISCONNECTED]: { - header: "firefoxview-tabpickup-sync-error-header-2", - description: "firefoxview-tabpickup-generic-sync-error-description-2", - buttonLabel: "firefoxview-tabpickup-sync-error-primarybutton", + header: "firefoxview-syncedtabs-synctabs-header-2", + description: "firefoxview-syncedtabs-synctabs-description-2", + buttonLabel: "firefoxview-tabpickup-synctabs-primarybutton-2", }, [ErrorType.PASSWORD_LOCKED]: { header: "firefoxview-tabpickup-password-locked-header-2", diff --git a/browser/components/firefoxview/tests/browser/browser_syncedtabs_errors_firefoxview.js b/browser/components/firefoxview/tests/browser/browser_syncedtabs_errors_firefoxview.js index e95964f8e628..394b0ac2e1fb 100644 --- a/browser/components/firefoxview/tests/browser/browser_syncedtabs_errors_firefoxview.js +++ b/browser/components/firefoxview/tests/browser/browser_syncedtabs_errors_firefoxview.js @@ -23,11 +23,14 @@ const SIGN_IN_HEADER_L10N_ID = isNovaEnabled ? "firefoxview-syncedtabs-signin-header-3" : "firefoxview-syncedtabs-signin-header-2"; const DISCONNECTED_DESCRIPTION_L10N_ID = isNovaEnabled - ? "firefoxview-tabpickup-generic-sync-error-description-2" + ? "firefoxview-syncedtabs-synctabs-description-2" : "firefoxview-tabpickup-sync-disconnected-description"; const DISCONNECTED_HEADER_L10N_ID = isNovaEnabled - ? "firefoxview-tabpickup-sync-error-header-2" + ? "firefoxview-syncedtabs-synctabs-header-2" : "firefoxview-tabpickup-sync-disconnected-header"; +const DISCONNECTED_BUTTON_L10N_ID = isNovaEnabled + ? "firefoxview-tabpickup-synctabs-primarybutton-2" + : "firefoxview-tabpickup-sync-disconnected-primarybutton"; const PASSWORD_LOCKED_DESCRIPTION_L10N_ID = isNovaEnabled ? "firefoxview-tabpickup-password-locked-description-2" : "firefoxview-tabpickup-password-locked-description"; @@ -98,10 +101,7 @@ add_task(async function test_network_offline() { let syncedTabsComponent = document.querySelector( "view-syncedtabs:not([slot=syncedtabs])" ); - await TestUtils.waitForCondition( - () => syncedTabsComponent.fullyUpdated, - "The synced tabs component to be fully updated" - ); + await syncedTabsComponent.updateComplete; await BrowserTestUtils.waitForMutationCondition( syncedTabsComponent.emptyState, { attributeFilter: ["headerlabel"] }, @@ -110,13 +110,13 @@ add_task(async function test_network_offline() { OFFLINE_HEADER_L10N_ID ); + const setupStateChanged = TestUtils.topicObserved( + "firefox-view.setupstate.changed" + ); syncedTabsComponent.emptyState .querySelector("moz-button[data-action='network-offline']") .buttonEl.click(); - - await TestUtils.waitForCondition( - () => TabsSetupFlowManager.tryToClearError.calledOnce - ); + await setupStateChanged; ok( TabsSetupFlowManager.tryToClearError.calledOnce, @@ -150,10 +150,7 @@ add_task(async function test_sync_error() { let syncedTabsComponent = document.querySelector( "view-syncedtabs:not([slot=syncedtabs])" ); - await TestUtils.waitForCondition( - () => syncedTabsComponent.fullyUpdated, - "Waiting for the synced tabs component to be fully updated" - ); + await syncedTabsComponent.updateComplete; await BrowserTestUtils.waitForMutationCondition( syncedTabsComponent.emptyState, { attributeFilter: ["headerlabel"] }, @@ -200,10 +197,7 @@ add_task(async function test_sync_admin_disabled() { let syncedTabsComponent = document.querySelector( "view-syncedtabs:not([slot=syncedtabs])" ); - await TestUtils.waitForCondition( - () => syncedTabsComponent.fullyUpdated, - "The synced tabs component has finished updating." - ); + await syncedTabsComponent.updateComplete; await BrowserTestUtils.waitForMutationCondition( syncedTabsComponent.emptyState, { attributeFilter: ["headerlabel"] }, @@ -262,10 +256,7 @@ add_task(async function test_sync_error_signed_out() { let syncedTabsComponent = document.querySelector( "view-syncedtabs:not([slot=syncedtabs])" ); - await TestUtils.waitForCondition( - () => syncedTabsComponent.fullyUpdated, - "The synced tabs component has finished updating." - ); + await syncedTabsComponent.updateComplete; await BrowserTestUtils.waitForMutationCondition( syncedTabsComponent.emptyState.shadowRoot, { childList: true, subtree: true }, @@ -301,10 +292,7 @@ add_task(async function test_sync_disconnected_error() { "view-syncedtabs:not([slot=syncedtabs])" ); info("Waiting for the synced tabs error step to be visible"); - await TestUtils.waitForCondition( - () => syncedTabsComponent.fullyUpdated, - "The synced tabs component has finished updating." - ); + await syncedTabsComponent.updateComplete; await BrowserTestUtils.waitForMutationCondition( syncedTabsComponent.emptyState.shadowRoot, { childList: true, subtree: true }, @@ -325,12 +313,17 @@ add_task(async function test_sync_disconnected_error() { let preferencesTabPromise = BrowserTestUtils.waitForNewTab( browser.getTabBrowser(), - "about:preferences#sync", + "about:preferences?action=choose-what-to-sync#sync", true ); let emptyStateButton = syncedTabsComponent.emptyState.querySelector( "moz-button[data-action='sync-disconnected']" ); + Assert.equal( + document.l10n.getAttributes(emptyStateButton).id, + DISCONNECTED_BUTTON_L10N_ID, + "Call-to-action button has correct text when sync's been disconnected." + ); EventUtils.synthesizeMouseAtCenter(emptyStateButton.buttonEl, {}, content); let preferencesTab = await preferencesTabPromise; await BrowserTestUtils.removeTab(preferencesTab); @@ -355,10 +348,7 @@ add_task(async function test_password_change_disconnect_error() { let syncedTabsComponent = document.querySelector( "view-syncedtabs:not([slot=syncedtabs])" ); - await TestUtils.waitForCondition( - () => syncedTabsComponent.fullyUpdated, - "The synced tabs component has finished updating." - ); + await syncedTabsComponent.updateComplete; await BrowserTestUtils.waitForMutationCondition( syncedTabsComponent.emptyState.shadowRoot, { childList: true, subtree: true }, @@ -391,10 +381,7 @@ add_task(async function test_multiple_errors() { let syncedTabsComponent = document.querySelector( "view-syncedtabs:not([slot=syncedtabs])" ); - await TestUtils.waitForCondition( - () => syncedTabsComponent.fullyUpdated, - "The synced tabs component has finished updating." - ); + await syncedTabsComponent.updateComplete; info("Waiting for the primary password error message to be shown"); await BrowserTestUtils.waitForMutationCondition( syncedTabsComponent.emptyState.shadowRoot, @@ -426,10 +413,7 @@ add_task(async function test_multiple_errors() { Services.obs.notifyObservers(null, UIState.ON_UPDATE); info("Waiting for the sync error message to be shown"); - await TestUtils.waitForCondition( - () => syncedTabsComponent.fullyUpdated, - "The synced tabs component has finished updating." - ); + await syncedTabsComponent.updateComplete; await BrowserTestUtils.waitForMutationCondition( syncedTabsComponent.emptyState, { attributeFilter: ["headerlabel"] }, diff --git a/browser/components/resistfingerprinting/test/browser/browser_timezone.js b/browser/components/resistfingerprinting/test/browser/browser_timezone.js index 2cd284da8e02..a7ee9bd98734 100644 --- a/browser/components/resistfingerprinting/test/browser/browser_timezone.js +++ b/browser/components/resistfingerprinting/test/browser/browser_timezone.js @@ -145,6 +145,82 @@ add_task(async function test_timezone_exempt() { await SpecialPowers.popPrefEnv(); }); +// Verify that an exempted domain is still exempt when the first-party domain +// and partition key are serialized in site format, i.e. "(https,example.net)". +add_task(async function test_timezone_exempt_site_keyed_partition() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.resistFingerprinting.exemptedDomains", "example.net"], + ["privacy.resistFingerprinting", true], + ["privacy.firstparty.isolate", true], + ["privacy.firstparty.isolate.use_site", true], + ["privacy.dynamic_firstparty.use_site", true], + ], + }); + + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: TEST_PATH + "file_dummy.html", + forceNewProcess: true, + }); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + SpecialPowers.Cu.getJSTestingFunctions().setTimeZone("PST8PDT"); + + function test() { + is( + Intl.DateTimeFormat("en-US").resolvedOptions().timeZone, + "America/Los_Angeles", + "Content should use default time zone" + ); + } + + // Run test in the context of the page. + Cu.exportFunction(is, content, { defineAs: "is" }); + content.eval(`(${test})()`); + }); + + BrowserTestUtils.removeTab(tab); + + await SpecialPowers.popPrefEnv(); +}); + +// Verify that `exemptedDomains` matching is case-insensitive. +add_task(async function test_timezone_exempt_mixed_case() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.resistFingerprinting.exemptedDomains", "Example.NET"], + ["privacy.resistFingerprinting", true], + ], + }); + + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: TEST_PATH + "file_dummy.html", + forceNewProcess: true, + }); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + SpecialPowers.Cu.getJSTestingFunctions().setTimeZone("PST8PDT"); + + function test() { + is( + Intl.DateTimeFormat("en-US").resolvedOptions().timeZone, + "America/Los_Angeles", + "Content should use default time zone" + ); + } + + // Run test in the context of the page. + Cu.exportFunction(is, content, { defineAs: "is" }); + content.eval(`(${test})()`); + }); + + BrowserTestUtils.removeTab(tab); + + await SpecialPowers.popPrefEnv(); +}); + // Verify that we are still spoofing for domains not `exemptedDomains` list. add_task(async function test_timezone_exempt_wrong_domain() { await SpecialPowers.pushPrefEnv({ diff --git a/browser/components/sidebar/sidebar-customize.mjs b/browser/components/sidebar/sidebar-customize.mjs index 3bd88b123806..0773a607a95a 100644 --- a/browser/components/sidebar/sidebar-customize.mjs +++ b/browser/components/sidebar/sidebar-customize.mjs @@ -27,6 +27,7 @@ const VISIBILITY_SETTING_PREF = "sidebar.visibility"; const EXPAND_ON_HOVER_PREF = "sidebar.expandOnHover"; const POSITION_SETTING_PREF = "sidebar.position_start"; const TAB_DIRECTION_SETTING_PREF = "sidebar.verticalTabs"; +const HOVER_PREVIEW_PREF = "sidebar.openTabsPanel.hoverPreview.enabled"; export class SidebarCustomize extends SidebarPage { constructor() { @@ -67,10 +68,20 @@ export class SidebarCustomize extends SidebarPage { this.expandOnHoverEnabled = newValue; } ); + XPCOMUtils.defineLazyPreferenceGetter( + this.#prefValues, + "hoverPreviewEnabled", + HOVER_PREVIEW_PREF, + true, + (_aPreference, _previousValue, newValue) => { + this.hoverPreviewEnabled = newValue; + } + ); this.visibility = this.#prefValues.visibility; this.isPositionStart = this.#prefValues.isPositionStart; this.verticalTabsEnabled = this.#prefValues.verticalTabsEnabled; this.expandOnHoverEnabled = this.#prefValues.expandOnHoverEnabled; + this.hoverPreviewEnabled = this.#prefValues.hoverPreviewEnabled; this.boundObserve = (...args) => this.observe(...args); } @@ -81,6 +92,7 @@ export class SidebarCustomize extends SidebarPage { isPositionStart: { type: Boolean }, verticalTabsEnabled: { type: Boolean }, expandOnHoverEnabled: { type: Boolean }, + hoverPreviewEnabled: { type: Boolean }, }; static queries = { @@ -92,6 +104,7 @@ export class SidebarCustomize extends SidebarPage { verticalTabsInput: "#vertical-tabs", expandOnHoverInput: "#expand-on-hover", openToolsFromSidebarInput: "#open-tools-from-sidebar", + hoverPreviewInput: "#hover-preview", }; connectedCallback() { @@ -189,7 +202,22 @@ export class SidebarCustomize extends SidebarPage { label=${ifDefined(tool.tooltiptext)} @change=${e => this.onToggleToolInput(e, tool.commandID)} ?checked=${!tool.disabled} - > + > + ${when( + tool.view === "viewOpenTabsSidebar" && !tool.disabled, + () => html` + + ` + )} + `; } @@ -371,6 +399,11 @@ export class SidebarCustomize extends SidebarPage { } } + #toggleHoverPreview(e) { + e.stopPropagation(); + Services.prefs.setBoolPref(HOVER_PREVIEW_PREF, e.target.checked); + } + #handleTabDirectionChange({ target: { checked } }) { const verticalTabsEnabled = checked; Services.prefs.setBoolPref(TAB_DIRECTION_SETTING_PREF, verticalTabsEnabled); diff --git a/browser/components/sidebar/tests/browser/browser_customize_sidebar.js b/browser/components/sidebar/tests/browser/browser_customize_sidebar.js index 9304dc9cf9a3..b2b5eb8be06e 100644 --- a/browser/components/sidebar/tests/browser/browser_customize_sidebar.js +++ b/browser/components/sidebar/tests/browser/browser_customize_sidebar.js @@ -476,3 +476,63 @@ add_task(async function test_settings_synchronized_across_windows() { SidebarController.hide(); await BrowserTestUtils.closeWindow(newWindow); }); + +add_task(async function test_open_tabs_hover_preview_setting() { + // The preceding task leaves vertical tabs on and the sidebar moved, which + // shifts the Tools list down the panel. Start from a known layout so the + // synthesized clicks land on the checkbox. + await SpecialPowers.pushPrefEnv({ + set: [ + ["sidebar.openTabsPanel.enabled", true], + [VERTICAL_TABS_PREF, false], + [SIDEBAR_VISIBILITY_PREF, "always-show"], + [POSITION_SETTING_PREF, true], + ], + }); + + const panel = await showCustomizePanel(window); + const { contentWindow } = SidebarController.browser; + await BrowserTestUtils.waitForMutationCondition( + panel.shadowRoot, + { subtree: true, childList: true }, + () => panel.hoverPreviewInput + ); + ok( + panel.hoverPreviewInput.checked, + "The hover preview setting is on by default." + ); + + panel.hoverPreviewInput.scrollIntoView({ block: "center" }); + EventUtils.synthesizeMouseAtCenter( + panel.hoverPreviewInput, + {}, + contentWindow + ); + await TestUtils.waitForCondition( + () => !Services.prefs.getBoolPref(HOVER_PREVIEW_PREF), + "Waiting for the hover preview to be turned off." + ); + ok( + !Services.prefs.getBoolPref(HOVER_PREVIEW_PREF), + "Unchecking the setting turns the hover preview off." + ); + + panel.hoverPreviewInput.scrollIntoView({ block: "center" }); + EventUtils.synthesizeMouseAtCenter( + panel.hoverPreviewInput, + {}, + contentWindow + ); + await TestUtils.waitForCondition( + () => Services.prefs.getBoolPref(HOVER_PREVIEW_PREF), + "Waiting for the hover preview to be turned back on." + ); + ok( + Services.prefs.getBoolPref(HOVER_PREVIEW_PREF), + "Checking it again turns the hover preview back on." + ); + + SidebarController.hide(); + Services.prefs.clearUserPref(HOVER_PREVIEW_PREF); + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/sidebar/tests/browser/head.js b/browser/components/sidebar/tests/browser/head.js index 4325beb6fc1c..ca86333e6043 100644 --- a/browser/components/sidebar/tests/browser/head.js +++ b/browser/components/sidebar/tests/browser/head.js @@ -24,6 +24,7 @@ function imageBufferFromDataURI(encodedImageData) { const SIDEBAR_VISIBILITY_PREF = "sidebar.visibility"; const POSITION_SETTING_PREF = "sidebar.position_start"; const VERTICAL_TABS_PREF = "sidebar.verticalTabs"; +const HOVER_PREVIEW_PREF = "sidebar.openTabsPanel.hoverPreview.enabled"; const kPrefCustomizationState = "browser.uiCustomization.state"; const kPrefCustomizationHorizontalTabstrip = "browser.uiCustomization.horizontalTabstrip"; diff --git a/browser/components/tabbrowser/content/status-panel.js b/browser/components/tabbrowser/content/status-panel.js new file mode 100644 index 000000000000..1200576f64de --- /dev/null +++ b/browser/components/tabbrowser/content/status-panel.js @@ -0,0 +1,154 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var StatusPanel = { + // This is useful for debugging (set to `true` in the interesting state for + // the panel to remain in that state). + _frozen: false, + + get panel() { + delete this.panel; + this.panel = document.getElementById("statuspanel"); + this.panel.addEventListener( + "transitionend", + this._onTransitionEnd.bind(this) + ); + this.panel.addEventListener( + "transitioncancel", + this._onTransitionEnd.bind(this) + ); + return this.panel; + }, + + get isVisible() { + return !this.panel.hasAttribute("inactive"); + }, + + update() { + if (BrowserHandler.kiosk || this._frozen) { + return; + } + let text; + let type; + let types = ["overLink"]; + if (XULBrowserWindow.busyUI) { + types.push("status"); + } + types.push("defaultStatus"); + for (type of types) { + if ((text = XULBrowserWindow[type])) { + break; + } + } + + // If it's a long data: URI that uses base64 encoding, truncate to + // a reasonable length rather than trying to display the entire thing. + // We can't shorten arbitrary URIs like this, as bidi etc might mean + // we need the trailing characters for display. But a base64-encoded + // data-URI is plain ASCII, so this is OK for status panel display. + // (See bug 1484071.) + let textCropped = false; + if (text.length > 500 && text.match(/^data:[^,]+;base64,/)) { + text = text.substring(0, 500) + "\u2026"; + textCropped = true; + } + + if (this._labelElement.value != text || (text && !this.isVisible)) { + this.panel.setAttribute("previoustype", this.panel.getAttribute("type")); + this.panel.setAttribute("type", type); + + this._label = text; + this._labelElement.setAttribute( + "crop", + type == "overLink" && !textCropped ? "center" : "end" + ); + } + }, + + get _labelElement() { + delete this._labelElement; + return (this._labelElement = document.getElementById("statuspanel-label")); + }, + + set _label(val) { + if (!this.isVisible) { + this.panel.removeAttribute("mirror"); + this.panel.removeAttribute("sizelimit"); + } + + if ( + this.panel.getAttribute("type") == "status" && + this.panel.getAttribute("previoustype") == "status" + ) { + // Before updating the label, set the panel's current width as its + // min-width to let the panel grow but not shrink and prevent + // unnecessary flicker while loading pages. We only care about the + // panel's width once it has been painted, so we can do this + // without flushing layout. + this.panel.style.minWidth = + window.windowUtils.getBoundsWithoutFlushing(this.panel).width + "px"; + } else { + this.panel.style.minWidth = ""; + } + + if (val) { + this._labelElement.value = val; + if (this.panel.hidden) { + this.panel.hidden = false; + // This ensures that the "inactive" attribute removal triggers a + // transition. + getComputedStyle(this.panel).display; + } + this.panel.removeAttribute("inactive"); + MousePosTracker.addListener(this); + } else { + this.panel.setAttribute("inactive", "true"); + MousePosTracker.removeListener(this); + } + }, + + _onTransitionEnd() { + if (!this.isVisible) { + this.panel.hidden = true; + } + }, + + getMouseTargetRect() { + let container = this.panel.parentNode; + let panelRect = window.windowUtils.getBoundsWithoutFlushing(this.panel); + let containerRect = window.windowUtils.getBoundsWithoutFlushing(container); + + return { + top: panelRect.top, + bottom: panelRect.bottom, + left: RTL_UI ? containerRect.right - panelRect.width : containerRect.left, + right: RTL_UI + ? containerRect.right + : containerRect.left + panelRect.width, + }; + }, + + onMouseEnter() { + this._mirror(); + }, + + onMouseLeave() { + this._mirror(); + }, + + _mirror() { + if (this._frozen) { + return; + } + if (this.panel.hasAttribute("mirror")) { + this.panel.removeAttribute("mirror"); + } else { + this.panel.setAttribute("mirror", "true"); + } + + if (!this.panel.hasAttribute("sizelimit")) { + this.panel.setAttribute("sizelimit", "true"); + } + }, +}; diff --git a/browser/components/tabbrowser/content/tab-bar-visibility.js b/browser/components/tabbrowser/content/tab-bar-visibility.js new file mode 100644 index 000000000000..e705d7023f17 --- /dev/null +++ b/browser/components/tabbrowser/content/tab-bar-visibility.js @@ -0,0 +1,67 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var TabBarVisibility = { + _initialUpdateDone: false, + + update(force = false) { + let isPopup = !window.toolbar.visible; + let isTaskbarTab = document.documentElement.hasAttribute("taskbartab"); + let isSingleTabWindow = isPopup || isTaskbarTab; + + let hasVerticalTabs = + !isSingleTabWindow && + Services.prefs.getBoolPref("sidebar.verticalTabs", false); + + // When `gBrowser` has not been initialized, we're opening a new window and + // assume only a single tab is loading. + let hasSingleTab = !gBrowser || gBrowser.visibleTabs.length == 1; + + // To prevent tabs being lost, hiding the tabs toolbar should only work + // when only a single tab is visible or tabs are displayed elsewhere. + let hideTabsToolbar = + (isSingleTabWindow && hasSingleTab) || hasVerticalTabs; + + // We only want a non-customized titlebar for popups. It should not be the + // case, but if a popup window contains more than one tab we re-enable + // titlebar customization and display tabs. + CustomTitlebar.allowedBy("non-popup", !(isPopup && hasSingleTab)); + + // Update the browser chrome. + + let tabsToolbar = document.getElementById("TabsToolbar"); + let navbar = document.getElementById("nav-bar"); + + gNavToolbox.toggleAttribute("tabs-hidden", hideTabsToolbar); + // Should the nav-bar look and function like a titlebar? + navbar.classList.toggle( + "browser-titlebar", + CustomTitlebar.enabled && hideTabsToolbar + ); + + if ( + hideTabsToolbar == tabsToolbar.collapsed && + !force && + this._initialUpdateDone + ) { + // No further updates needed, `TabsToolbar` already matches the expected + // visibilty. + return; + } + this._initialUpdateDone = true; + + tabsToolbar.collapsed = hideTabsToolbar; + + // Stylize close menu items based on tab visibility. When a window will only + // ever have a single tab, only show the option to close the tab, and + // simplify the text since we don't need to disambiguate from closing the window. + document.getElementById("menu_closeWindow").hidden = hideTabsToolbar; + document.l10n.setAttributes( + document.getElementById("menu_close"), + hideTabsToolbar + ? "tabbrowser-menuitem-close" + : "tabbrowser-menuitem-close-tab" + ); + }, +}; diff --git a/browser/components/tabbrowser/content/tab-hover-preview.mjs b/browser/components/tabbrowser/content/tab-hover-preview.mjs index 8e2285494514..d1cc408d53f3 100644 --- a/browser/components/tabbrowser/content/tab-hover-preview.mjs +++ b/browser/components/tabbrowser/content/tab-hover-preview.mjs @@ -208,6 +208,25 @@ export default class TabHoverPanelSet { } #doDeactivate(panel) { + // Hiding a popup that has not finished showing cancels the in-flight show, + // so popupshown never fires. Mark the panel inactive now and complete the + // hide once the show settles, unless it gets reactivated in the meantime. + if (panel.panelElement.state == "showing") { + if (this.#activePanel == panel) { + this.#activePanel = null; + } + panel.panelElement.addEventListener( + "popupshown", + () => { + if (this.#activePanel != panel) { + this.#doDeactivate(panel); + } + }, + { once: true } + ); + return; + } + panel.onBeforeHide(); panel.panelElement.hidePopup(); this.panelOpener.clear(panel); diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js index b872cec66cb4..f573987d18bb 100644 --- a/browser/components/tabbrowser/content/tabbrowser.js +++ b/browser/components/tabbrowser/content/tabbrowser.js @@ -36,59 +36,6 @@ /** * Updates the User Context UI indicators if the browser is in a non-default context */ - function updateUserContextUIIndicator() { - function replaceContainerClass(classType, element, value) { - let prefix = "identity-" + classType + "-"; - if (value && element.classList.contains(prefix + value)) { - return; - } - for (let className of element.classList) { - if (className.startsWith(prefix)) { - element.classList.remove(className); - } - } - if (value) { - element.classList.add(prefix + value); - } - } - - let hbox = document.getElementById("userContext-icons"); - - let userContextId = gBrowser.selectedBrowser.getAttribute("usercontextid"); - if (!userContextId) { - // The container-creation panel can temporarily reveal this indicator to - // use it as its anchor; don't hide it again while that panel is up. - let creationPanel = document.getElementById("containerCreation-panel"); - if (creationPanel && creationPanel.state != "closed") { - return; - } - replaceContainerClass("color", hbox, ""); - hbox.hidden = true; - return; - } - - let identity = - ContextualIdentityService.getPublicIdentityFromId(userContextId); - if (!identity) { - replaceContainerClass("color", hbox, ""); - hbox.hidden = true; - return; - } - - replaceContainerClass("color", hbox, identity.color); - - let label = ContextualIdentityService.getUserContextLabel(userContextId); - document.getElementById("userContext-label").textContent = label; - // Also set the container label as the tooltip so we can only show the icon - // in small windows. - hbox.setAttribute("tooltiptext", label); - - let indicator = document.getElementById("userContext-indicator"); - replaceContainerClass("icon", indicator, identity.icon); - - hbox.hidden = false; - } - async function getTotalMemoryUsage() { const procInfo = await ChromeUtils.requestProcInfo(); let totalMemoryUsage = procInfo.memory; @@ -149,7 +96,7 @@ // Sync dialog cannot be used inside drop event handler. let answer = await tabbrowser.OpenInTabsUtils.promiseConfirmOpenInTabs( links.length, - window + tabbrowser.documentGlobal ); if (!answer) { return; @@ -159,7 +106,9 @@ let urls = []; let postDatas = []; for (let link of links) { - let data = await UrlbarUtils.getShortcutOrURIAndPostData(link.url); + let data = await tabbrowser.UrlbarUtils.getShortcutOrURIAndPostData( + link.url + ); urls.push(data.url); postDatas.push(data.postData); } @@ -822,7 +771,7 @@ tab.setAttribute("usercontextid", userContextId); ContextualIdentityService.setTabStyle(tab); } - updateUserContextUIIndicator(); + this.#updateUserContextUIIndicator(); this.#tabForBrowser.set(browser, tab); @@ -846,6 +795,64 @@ ); } + #updateUserContextUIIndicator() { + function replaceContainerClass(classType, element, value) { + let prefix = "identity-" + classType + "-"; + if (value && element.classList.contains(prefix + value)) { + return; + } + for (let className of element.classList) { + if (className.startsWith(prefix)) { + element.classList.remove(className); + } + } + if (value) { + element.classList.add(prefix + value); + } + } + + let hbox = this.ownerDocument.getElementById("userContext-icons"); + + let userContextId = this.selectedBrowser.getAttribute("usercontextid"); + if (!userContextId) { + // The container-creation panel can temporarily reveal this indicator to + // use it as its anchor; don't hide it again while that panel is up. + let creationPanel = this.ownerDocument.getElementById( + "containerCreation-panel" + ); + if (creationPanel && creationPanel.state != "closed") { + return; + } + replaceContainerClass("color", hbox, ""); + hbox.hidden = true; + return; + } + + let identity = + ContextualIdentityService.getPublicIdentityFromId(userContextId); + if (!identity) { + replaceContainerClass("color", hbox, ""); + hbox.hidden = true; + return; + } + + replaceContainerClass("color", hbox, identity.color); + + let label = ContextualIdentityService.getUserContextLabel(userContextId); + this.ownerDocument.getElementById("userContext-label").textContent = + label; + // Also set the container label as the tooltip so we can only show the icon + // in small windows. + hbox.setAttribute("tooltiptext", label); + + let indicator = this.ownerDocument.getElementById( + "userContext-indicator" + ); + replaceContainerClass("icon", indicator, identity.icon); + + hbox.hidden = false; + } + /** * BEGIN FORWARDED BROWSER PROPERTIES. IF YOU ADD A PROPERTY TO THE BROWSER ELEMENT * MAKE SURE TO ADD IT HERE AS WELL. @@ -1963,7 +1970,7 @@ } } - updateUserContextUIIndicator(); + this.#updateUserContextUIIndicator(); gPermissionPanel.updateSharingIndicator(); // Enable touch events to start a native dragging @@ -3099,7 +3106,7 @@ // We don't want to update the container icon and identifier if // this is not the selected browser. if (aTab.selected) { - updateUserContextUIIndicator(); + this.#updateUserContextUIIndicator(); } // Only fire this event if the tab is already in the DOM @@ -9500,6 +9507,14 @@ this._requestCount = aOrigRequestCount || 0; } + get #documentGlobal() { + return this._tab.documentGlobal; + } + + get #tabbrowser() { + return this._tab.documentGlobal.gBrowser; + } + destroy() { delete this._tab; delete this._browser; @@ -9507,7 +9522,7 @@ _callProgressListeners(...args) { args.unshift(this._browser); - return gBrowser._callProgressListeners.apply(gBrowser, args); + return this.#tabbrowser._callProgressListeners(...args); } _shouldShowProgress(aRequest) { @@ -9564,7 +9579,7 @@ if (this._totalProgress && this._tab.hasAttribute("busy")) { this._tab.setAttribute("progress", "true"); - gBrowser._tabAttrModified(this._tab, ["progress"]); + this.#tabbrowser._tabAttrModified(this._tab, ["progress"]); } this._callProgressListeners("onProgressChange", [ @@ -9674,14 +9689,18 @@ originalLocation )) ) { - gBrowser.setInitialTabTitle(this._tab, originalLocation.spec, { - isURL: true, - }); + this.#tabbrowser.setInitialTabTitle( + this._tab, + originalLocation.spec, + { + isURL: true, + } + ); this._browser.browsingContext.nonWebControlledLoadingURI = originalLocation; - if (this._tab.selected && !gBrowser.userTypedValue) { - gURLBar.setURI(); + if (this._tab.selected && !this.#tabbrowser.userTypedValue) { + this.#documentGlobal.gURLBar.setURI(); } } } @@ -9698,12 +9717,12 @@ aWebProgress.isTopLevel ) { this._tab.setAttribute("busy", "true"); - gBrowser._tabAttrModified(this._tab, ["busy"]); + this.#tabbrowser._tabAttrModified(this._tab, ["busy"]); this._tab._notselectedsinceload = !this._tab.selected; } if (this._tab.selected) { - gBrowser._isBusy = true; + this.#tabbrowser._isBusy = true; } } } else if (aStateFlags & STATE_STOP && aStateFlags & STATE_IS_NETWORK) { @@ -9722,7 +9741,7 @@ aWebProgress.isTopLevel && !aWebProgress.isLoadingDocument && Components.isSuccessCode(aStatus) && - !gBrowser.tabAnimationsInProgress && + !this.#tabbrowser.tabAnimationsInProgress && !gReduceMotion ) { if (this._tab._notselectedsinceload) { @@ -9765,7 +9784,7 @@ aStatus != Cr.NS_BINDING_CANCELLED_OLD_LOAD && !isNavigating ) { - gURLBar.setURI(); + this.#documentGlobal.gURLBar.setURI(); } } else if (isSuccessful) { this._browser.urlbarChangeTracker.finishedLoad(); @@ -9789,7 +9808,7 @@ // new tabs behavior is set to open a blank page. // This is a no-op unless this._browser.documentURI is in // FAVICON_DEFAULTS. - gBrowser.setDefaultIcon(this._tab, this._browser.documentURI); + this.#tabbrowser.setDefaultIcon(this._tab, this._browser.documentURI); } // For keyword URIs clear the user typed value since they will be changed into real URIs @@ -9798,11 +9817,11 @@ } if (this._tab.selected) { - gBrowser._isBusy = false; + this.#tabbrowser._isBusy = false; } if (modifiedAttrs.length) { - gBrowser._tabAttrModified(this._tab, modifiedAttrs); + this.#tabbrowser._tabAttrModified(this._tab, modifiedAttrs); } } @@ -9880,16 +9899,18 @@ // attribute here. if (isErrorPage && this._tab.hasAttribute("busy")) { this._tab.removeAttribute("busy"); - gBrowser._tabAttrModified(this._tab, ["busy"]); + this.#tabbrowser._tabAttrModified(this._tab, ["busy"]); } if (!isSameDocument) { // If the browser was playing audio, we should remove the playing state. if (this._tab.hasAttribute("soundplaying")) { - clearTimeout(this._tab._soundPlayingAttrRemovalTimer); + this.#documentGlobal.clearTimeout( + this._tab._soundPlayingAttrRemovalTimer + ); this._tab._soundPlayingAttrRemovalTimer = 0; this._tab.removeAttribute("soundplaying"); - gBrowser._tabAttrModified(this._tab, ["soundplaying"]); + this.#tabbrowser._tabAttrModified(this._tab, ["soundplaying"]); } // If the browser was previously muted, we should restore the muted state. @@ -9897,8 +9918,8 @@ this._tab.linkedBrowser.browsingContext?.mediaController?.mute(); } - if (gBrowser.isFindBarInitialized(this._tab)) { - let findBar = gBrowser.getCachedFindBar(this._tab); + if (this.#tabbrowser.isFindBarInitialized(this._tab)) { + let findBar = this.#tabbrowser.getCachedFindBar(this._tab); // Close the Find toolbar if we're in old-style TAF mode if (findBar.findMode != findBar.FIND_NORMAL) { @@ -9911,7 +9932,7 @@ // context, see https://bugzilla.mozilla.org/show_bug.cgi?id=585653 // and https://github.com/whatwg/html/issues/2174 if (!isReload) { - gBrowser.setTabTitle(this._tab); + this.#tabbrowser.setTabTitle(this._tab); } // Don't clear the favicon if this tab is in the pending @@ -9932,7 +9953,7 @@ } if (!isReload && aWebProgress.isLoadingDocument) { - let triggerer = gBrowser._getTriggeringPrincipalFromHistory( + let triggerer = this.#tabbrowser._getTriggeringPrincipalFromHistory( this._browser ); // Typing a url, searching or clicking a bookmark will load a new @@ -9941,7 +9962,7 @@ if (triggerer && triggerer.isSystemPrincipal) { // Reset the related tab map so that the next tab opened will be related // to this new document and not to tabs opened by the previous one. - gBrowser.clearRelatedTabs(); + this.#tabbrowser.clearRelatedTabs(); } } @@ -9952,10 +9973,10 @@ this._browser.originalURI = aRequest.originalURI; } - if (!gBrowser._allowTransparentBrowser) { + if (!this.#tabbrowser._allowTransparentBrowser) { this._browser.toggleAttribute( "transparent", - AIWindow.isAIWindowActive(window) && + AIWindow.isAIWindowActive(this.#documentGlobal) && AIWindow.isAIWindowContentPage(aLocation) ); } @@ -9964,20 +9985,20 @@ let userContextId = this._browser.getAttribute("usercontextid") || 0; if (this._browser.registeredOpenURI) { let uri = this._browser.registeredOpenURI; - gBrowser.UrlbarProviderOpenTabs.unregisterOpenTab( + this.#tabbrowser.UrlbarProviderOpenTabs.unregisterOpenTab( uri.spec, userContextId, this._tab.group?.id, - PrivateBrowsingUtils.isWindowPrivate(window) + PrivateBrowsingUtils.isWindowPrivate(this.#documentGlobal) ); delete this._browser.registeredOpenURI; } if (!isBlankPageURL(aLocation.spec)) { - gBrowser.UrlbarProviderOpenTabs.registerOpenTab( + this.#tabbrowser.UrlbarProviderOpenTabs.registerOpenTab( aLocation.spec, userContextId, this._tab.group?.id, - PrivateBrowsingUtils.isWindowPrivate(window) + PrivateBrowsingUtils.isWindowPrivate(this.#documentGlobal) ); this._browser.registeredOpenURI = aLocation; @@ -9989,11 +10010,13 @@ } } - if (this._tab != gBrowser.selectedTab) { - let tabCacheIndex = gBrowser._tabLayerCache.indexOf(this._tab); + if (this._tab != this.#tabbrowser.selectedTab) { + let tabCacheIndex = this.#tabbrowser._tabLayerCache.indexOf( + this._tab + ); if (tabCacheIndex != -1) { - gBrowser._tabLayerCache.splice(tabCacheIndex, 1); - gBrowser._getSwitcher().cleanUpTabAfterEviction(this._tab); + this.#tabbrowser._tabLayerCache.splice(tabCacheIndex, 1); + this.#tabbrowser._getSwitcher().cleanUpTabAfterEviction(this._tab); } } } @@ -10085,7 +10108,7 @@ loadURIOptions.loadFlags |= loadURIOptions.flags | LOAD_FLAGS_NONE; delete loadURIOptions.flags; loadURIOptions.hasValidUserGestureActivation ??= - document.hasValidTransientUserGestureActivation; + browser.ownerDocument.hasValidTransientUserGestureActivation; }, _loadFlagsToFixupFlags(browser, loadFlags) { @@ -10132,9 +10155,10 @@ uriString, { loadFlags, globalHistoryOptions } ) { + let { SponsorProtection } = browser.getTabBrowser(); if (globalHistoryOptions?.triggeringSponsoredURL) { if (globalHistoryOptions.triggeringSource == "newtab") { - gBrowser.SponsorProtection.addProtectedBrowser(browser); + SponsorProtection.addProtectedBrowser(browser); } try { @@ -10158,7 +10182,7 @@ ); } catch (e) {} } else { - gBrowser.SponsorProtection.removeProtectedBrowser(browser); + SponsorProtection.removeProtectedBrowser(browser); } if (globalHistoryOptions?.triggeringSearchEngine) { @@ -10232,218 +10256,3 @@ }, }; } // end private scope for gBrowser - -var StatusPanel = { - // This is useful for debugging (set to `true` in the interesting state for - // the panel to remain in that state). - _frozen: false, - - get panel() { - delete this.panel; - this.panel = document.getElementById("statuspanel"); - this.panel.addEventListener( - "transitionend", - this._onTransitionEnd.bind(this) - ); - this.panel.addEventListener( - "transitioncancel", - this._onTransitionEnd.bind(this) - ); - return this.panel; - }, - - get isVisible() { - return !this.panel.hasAttribute("inactive"); - }, - - update() { - if (BrowserHandler.kiosk || this._frozen) { - return; - } - let text; - let type; - let types = ["overLink"]; - if (XULBrowserWindow.busyUI) { - types.push("status"); - } - types.push("defaultStatus"); - for (type of types) { - if ((text = XULBrowserWindow[type])) { - break; - } - } - - // If it's a long data: URI that uses base64 encoding, truncate to - // a reasonable length rather than trying to display the entire thing. - // We can't shorten arbitrary URIs like this, as bidi etc might mean - // we need the trailing characters for display. But a base64-encoded - // data-URI is plain ASCII, so this is OK for status panel display. - // (See bug 1484071.) - let textCropped = false; - if (text.length > 500 && text.match(/^data:[^,]+;base64,/)) { - text = text.substring(0, 500) + "\u2026"; - textCropped = true; - } - - if (this._labelElement.value != text || (text && !this.isVisible)) { - this.panel.setAttribute("previoustype", this.panel.getAttribute("type")); - this.panel.setAttribute("type", type); - - this._label = text; - this._labelElement.setAttribute( - "crop", - type == "overLink" && !textCropped ? "center" : "end" - ); - } - }, - - get _labelElement() { - delete this._labelElement; - return (this._labelElement = document.getElementById("statuspanel-label")); - }, - - set _label(val) { - if (!this.isVisible) { - this.panel.removeAttribute("mirror"); - this.panel.removeAttribute("sizelimit"); - } - - if ( - this.panel.getAttribute("type") == "status" && - this.panel.getAttribute("previoustype") == "status" - ) { - // Before updating the label, set the panel's current width as its - // min-width to let the panel grow but not shrink and prevent - // unnecessary flicker while loading pages. We only care about the - // panel's width once it has been painted, so we can do this - // without flushing layout. - this.panel.style.minWidth = - window.windowUtils.getBoundsWithoutFlushing(this.panel).width + "px"; - } else { - this.panel.style.minWidth = ""; - } - - if (val) { - this._labelElement.value = val; - if (this.panel.hidden) { - this.panel.hidden = false; - // This ensures that the "inactive" attribute removal triggers a - // transition. - getComputedStyle(this.panel).display; - } - this.panel.removeAttribute("inactive"); - MousePosTracker.addListener(this); - } else { - this.panel.setAttribute("inactive", "true"); - MousePosTracker.removeListener(this); - } - }, - - _onTransitionEnd() { - if (!this.isVisible) { - this.panel.hidden = true; - } - }, - - getMouseTargetRect() { - let container = this.panel.parentNode; - let panelRect = window.windowUtils.getBoundsWithoutFlushing(this.panel); - let containerRect = window.windowUtils.getBoundsWithoutFlushing(container); - - return { - top: panelRect.top, - bottom: panelRect.bottom, - left: RTL_UI ? containerRect.right - panelRect.width : containerRect.left, - right: RTL_UI - ? containerRect.right - : containerRect.left + panelRect.width, - }; - }, - - onMouseEnter() { - this._mirror(); - }, - - onMouseLeave() { - this._mirror(); - }, - - _mirror() { - if (this._frozen) { - return; - } - if (this.panel.hasAttribute("mirror")) { - this.panel.removeAttribute("mirror"); - } else { - this.panel.setAttribute("mirror", "true"); - } - - if (!this.panel.hasAttribute("sizelimit")) { - this.panel.setAttribute("sizelimit", "true"); - } - }, -}; - -var TabBarVisibility = { - _initialUpdateDone: false, - - update(force = false) { - let isPopup = !window.toolbar.visible; - let isTaskbarTab = document.documentElement.hasAttribute("taskbartab"); - let isSingleTabWindow = isPopup || isTaskbarTab; - - let hasVerticalTabs = - !isSingleTabWindow && - Services.prefs.getBoolPref("sidebar.verticalTabs", false); - - // When `gBrowser` has not been initialized, we're opening a new window and - // assume only a single tab is loading. - let hasSingleTab = !gBrowser || gBrowser.visibleTabs.length == 1; - - // To prevent tabs being lost, hiding the tabs toolbar should only work - // when only a single tab is visible or tabs are displayed elsewhere. - let hideTabsToolbar = - (isSingleTabWindow && hasSingleTab) || hasVerticalTabs; - - // We only want a non-customized titlebar for popups. It should not be the - // case, but if a popup window contains more than one tab we re-enable - // titlebar customization and display tabs. - CustomTitlebar.allowedBy("non-popup", !(isPopup && hasSingleTab)); - - // Update the browser chrome. - - let tabsToolbar = document.getElementById("TabsToolbar"); - let navbar = document.getElementById("nav-bar"); - - gNavToolbox.toggleAttribute("tabs-hidden", hideTabsToolbar); - // Should the nav-bar look and function like a titlebar? - navbar.classList.toggle( - "browser-titlebar", - CustomTitlebar.enabled && hideTabsToolbar - ); - - if ( - hideTabsToolbar == tabsToolbar.collapsed && - !force && - this._initialUpdateDone - ) { - // No further updates needed, `TabsToolbar` already matches the expected - // visibilty. - return; - } - this._initialUpdateDone = true; - - tabsToolbar.collapsed = hideTabsToolbar; - - // Stylize close menu items based on tab visibility. When a window will only - // ever have a single tab, only show the option to close the tab, and - // simplify the text since we don't need to disambiguate from closing the window. - document.getElementById("menu_closeWindow").hidden = hideTabsToolbar; - document.l10n.setAttributes( - document.getElementById("menu_close"), - hideTabsToolbar - ? "tabbrowser-menuitem-close" - : "tabbrowser-menuitem-close-tab" - ); - }, -}; diff --git a/browser/components/tabbrowser/jar.mn b/browser/components/tabbrowser/jar.mn index e59f70d5d804..43710e410342 100644 --- a/browser/components/tabbrowser/jar.mn +++ b/browser/components/tabbrowser/jar.mn @@ -12,8 +12,10 @@ browser.jar: content/browser/tabbrowser/opentabs-splitview.css (content/opentabs-splitview.css) content/browser/tabbrowser/opentabs-splitview.mjs (content/opentabs-splitview.mjs) content/browser/tabbrowser/split-view-footer.js (content/split-view-footer.js) + content/browser/tabbrowser/status-panel.js (content/status-panel.js) content/browser/tabbrowser/tab.js (content/tab.js) content/browser/tabbrowser/tab-context-menu.js (content/tab-context-menu.js) + content/browser/tabbrowser/tab-bar-visibility.js (content/tab-bar-visibility.js) content/browser/tabbrowser/tab-groups-list.mjs (content/tab-groups-list.mjs) content/browser/tabbrowser/tab-hover-preview.mjs (content/tab-hover-preview.mjs) content/browser/tabbrowser/tabbrowser.js (content/tabbrowser.js) diff --git a/browser/components/tabbrowser/test/browser/tabs/browser.toml b/browser/components/tabbrowser/test/browser/tabs/browser.toml index 5fdd07b7bf95..9ecf05c9e378 100644 --- a/browser/components/tabbrowser/test/browser/tabs/browser.toml +++ b/browser/components/tabbrowser/test/browser/tabs/browser.toml @@ -653,6 +653,9 @@ tags = "vertical-tabs" ["browser_tab_preview.js"] tags = "vertical-tabs" +skip-if = [ + "os == 'linux' && os_version == '24.04' && arch == 'x86_64' && display == 'x11' && opt && a11y_checks", # Bug 2010318 +] ["browser_tab_splitview.js"] diff --git a/browser/components/tabbrowser/test/browser/tabs/browser_tab_preview.js b/browser/components/tabbrowser/test/browser/tabs/browser_tab_preview.js index 8127b32d91bf..ffe4e3f94f55 100644 --- a/browser/components/tabbrowser/test/browser/tabs/browser_tab_preview.js +++ b/browser/components/tabbrowser/test/browser/tabs/browser_tab_preview.js @@ -190,6 +190,16 @@ add_setup(async function () { ], }); + // These tests drive hover with synthesized mouseover events, which set the + // hover target without moving the OS pointer. Any real mouse event that + // arrives afterwards resynchronises hover to wherever the pointer physically + // is and dismisses the preview, and merely showing the panel is enough to + // produce one. Drop input that the tests did not generate. + EventUtils.disableNonTestMouseEvents(true); + registerCleanupFunction(() => { + EventUtils.disableNonTestMouseEvents(false); + }); + await resetState(); registerCleanupFunction(async function () { await resetState(); diff --git a/browser/components/urlbar/UrlbarParentController.sys.mjs b/browser/components/urlbar/UrlbarParentController.sys.mjs index f9442c6884be..9eca9ae7e115 100644 --- a/browser/components/urlbar/UrlbarParentController.sys.mjs +++ b/browser/components/urlbar/UrlbarParentController.sys.mjs @@ -12,6 +12,7 @@ import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; * @import {UrlbarView} from "chrome://browser/content/urlbar/UrlbarView.mjs" * @import {WindowMode} from "moz-src:///browser/components/urlbar/content/UrlbarInputBase.mjs" * @import {SearchEngineInfo} from "chrome://browser/content/urlbar/SearchEngineStore.mjs" + * @import {UrlbarLoadRequest} from "chrome://browser/content/urlbar/UrlbarShared.mjs" */ const lazy = {}; @@ -300,8 +301,9 @@ export class UrlbarParentController { * The id of the browser committed at Enter; its per-tab data and navigation * epoch are read here, defaulting to the selected browser. * @returns {Promise} - * `{ heuristicResult }` to pick, `{ fixup: { url, postData, keywordAsSent } }` - * to load, or `{}` when the browser navigated in the meanwhile. + * `{ heuristicResult }` to pick, + * `{ fixup: { url, postData: ?string, keywordAsSent } }` to load, or `{}` + * when the browser navigated in the meanwhile. */ async resolveFallbackNavigation({ searchString, @@ -372,7 +374,16 @@ export class UrlbarParentController { Services.uriFixup.getFixupURIInfo(searchString, flags); return navigated() ? {} - : { fixup: { url: preferredURI.spec, postData, keywordAsSent } }; + : { + fixup: { + url: preferredURI.spec, + // Post data only happens if the default engine is POST (rare) + postData: postData + ? lazy.UrlbarUtils.getPostDataString(postData) + : null, + keywordAsSent, + }, + }; } catch (fixupEx) { // uriFixup can throw; swallow it so the resolve never rejects. console.error(fixupEx); @@ -966,8 +977,8 @@ export class UrlbarParentController { * revert the input. * * @param {object} loadData - * @param {string} loadData.url - * The URL to load. + * @param {UrlbarLoadRequest} loadData.loadRequest + * What to load. * @param {string} loadData.where * Where to open, per `openTrustedLinkIn`. * @param {object} loadData.params @@ -984,11 +995,17 @@ export class UrlbarParentController { * browser to hand `focusBrowser` on the deferred-Enter keyup -- a * content-process input can't resolve the selected browser itself. */ - loadURL({ url, where, params, browserId, userTypedValue }) { + loadURL({ loadRequest, where, params, browserId, userTypedValue }) { let browser = this.resolveTargetBrowser(browserId) || this.browserWindow.gBrowser.selectedBrowser; + let { url, postData } = lazy.UrlbarUtils.loadRequestToUrl(loadRequest); + if (!url) { + return { reverted: true, browserId: browser.browserId }; + } + params.postData = postData; + if (this.#isAddressbar) { this.#prepareAddressbarLoad({ browser, diff --git a/browser/components/urlbar/UrlbarPrefs.sys.mjs b/browser/components/urlbar/UrlbarPrefs.sys.mjs index 6a75bcce82df..14894036af46 100644 --- a/browser/components/urlbar/UrlbarPrefs.sys.mjs +++ b/browser/components/urlbar/UrlbarPrefs.sys.mjs @@ -259,6 +259,9 @@ const PREF_URLBAR_DEFAULTS = /** @type {PreferenceDefinition[]} */ ([ // for mdn suggestions. ["mdn.showLessFrequentlyCount", 0], + // The maximum number of tab mentions the Smartbar suggests. + ["mentions.maxResults", 5], + // Comma-separated list of client variants to send to Merino ["merino.clientVariants", ""], @@ -470,6 +473,11 @@ const PREF_URLBAR_DEFAULTS = /** @type {PreferenceDefinition[]} */ ([ // search host. ["scotchBonnet.persistSearchMode", false], + // Whether the search button declines to be the target of the toolbar tab + // stop in front of the input. The shipping default is set in firefox.js, + // where it's enabled on Nightly only. + ["searchModeSwitcher.skipTabStop", false], + // Feature gate pref for search restrict keywords being shown in the urlbar. ["searchRestrictKeywords.featureGate", false], @@ -769,7 +777,11 @@ const PREF_OTHER_DEFAULTS = /** @type {PreferenceDefinition[]} */ ([ ["browser.search.suggest.enabled", true], ["browser.search.suggest.enabled.private", false], ["browser.search.widget.new", true], + ["browser.settings-redesign.enabled", true], + ["browser.smartwindow.agent.enabled", false], + ["browser.smartwindow.smartbarMentions.loglevel", "Error"], ["keyword.enabled", true], + ["privacy.query_stripping.strip_on_share.enabled", true], ["security.insecure_connection_text.enabled", true], [TelemetryReportingPolicy.TOU_ACCEPTED_DATE_PREF, 0], ["ui.popup.disable_autohide", false], diff --git a/browser/components/urlbar/UrlbarUtils.sys.mjs b/browser/components/urlbar/UrlbarUtils.sys.mjs index ffa9f56b76be..977148fef2b5 100644 --- a/browser/components/urlbar/UrlbarUtils.sys.mjs +++ b/browser/components/urlbar/UrlbarUtils.sys.mjs @@ -8,7 +8,7 @@ */ /** - * @import {URIFixupPrimitives} from "chrome://browser/content/urlbar/UrlbarShared.mjs" + * @import {UrlbarLoadRequest, URIFixupPrimitives} from "chrome://browser/content/urlbar/UrlbarShared.mjs" * @import {Query} from "./UrlbarProvidersManager.sys.mjs" * @import {SearchEngine} from "moz-src:///toolkit/components/search/SearchEngine.sys.mjs" * @import {SmartbarInput} from "chrome://browser/content/urlbar/SmartbarInput.mjs" @@ -183,6 +183,22 @@ export var UrlbarUtils = { return mimeStream.QueryInterface(Ci.nsIInputStream); }, + /** + * Converts nsIInputStream to string. Throws unless the stream is a MIME + * stream wrapping a string stream, as built by `getPostDataStream` and by + * search engine submissions. + * + * @param {nsIInputStream} postData + * The stream to unwrap. + * @returns {string} + * The wrapped post data. + */ + getPostDataString(postData) { + return postData + .QueryInterface(Ci.nsIMIMEInputStream) + .data.QueryInterface(Ci.nsISupportsCString).data; + }, + /** * Returns the group for a result. * @@ -296,34 +312,44 @@ export var UrlbarUtils = { * The element associated with the result that was selected or picked, if * available. For results that have multiple selectable children, the URL * may be taken from a child element rather than the result. - * @returns {object} - * An object: `{ url, postData }` + * @returns {{url: ?string, postData: ?nsIInputStream}} * `url` will be null if the result doesn't have a URL. `postData` will be * null if the result doesn't have post data. */ getUrlFromResult(result, { element = null } = {}) { - if ( - result.payload.engine && - (result.type == UrlbarShared.RESULT_TYPE.SEARCH || - result.type == UrlbarShared.RESULT_TYPE.DYNAMIC) - ) { - let query = - element?.dataset.query || - result.payload.suggestion || - result.payload.query; - if (query) { - const engine = lazy.SearchService.getEngineByName( - result.payload.engine - ); - let [url, postData] = this.getSearchQueryUrl(engine, query); - return { url, postData }; + let loadRequest = UrlbarShared.getLoadRequestFromResult(result, { + element, + }); + if (!loadRequest) { + return { url: null, postData: null }; + } + + return this.loadRequestToUrl(loadRequest); + }, + + /** + * Resolves a load request to the url and post data to load. + * + * @param {UrlbarLoadRequest} loadRequest + * What to load. + * @returns {{url: ?string, postData: ?nsIInputStream}} + * `url` will be null when the search engine wasn't found. + */ + loadRequestToUrl(loadRequest) { + if (loadRequest.engineSearch) { + let { engineName, query } = loadRequest.engineSearch; + let engine = lazy.SearchService.getEngineByName(engineName); + if (!engine) { + return { url: null, postData: null }; } + let [url, postData] = this.getSearchQueryUrl(engine, query); + return { url, postData }; } return { - url: result.payload.url ?? null, - postData: result.payload.postData - ? this.getPostDataStream(result.payload.postData) + url: loadRequest.urlLoad.url, + postData: loadRequest.urlLoad.postData + ? this.getPostDataStream(loadRequest.urlLoad.postData) : null, }; }, diff --git a/browser/components/urlbar/content/SearchModeSwitcher.mjs b/browser/components/urlbar/content/SearchModeSwitcher.mjs index ca6cc57aad1b..3470ea4d97df 100644 --- a/browser/components/urlbar/content/SearchModeSwitcher.mjs +++ b/browser/components/urlbar/content/SearchModeSwitcher.mjs @@ -22,25 +22,20 @@ ChromeUtils.defineESModuleGetters(lazy, { SearchUIUtils: "moz-src:///browser/components/search/SearchUIUtils.sys.mjs", }); -ChromeUtils.defineLazyGetter(lazy, "SearchModeSwitcherL10n", () => { - return new Localization(["browser/browser.ftl"]); -}); +/** @type {Localization} */ +let l10n; -const { XPCOMUtils } = ChromeUtils.importESModule( - "resource://gre/modules/XPCOMUtils.sys.mjs" -); - -XPCOMUtils.defineLazyPreferenceGetter( - lazy, - "settingsRedesignEnabled", - "browser.settings-redesign.enabled", - true -); +function getL10n() { + l10n ??= new Localization(["browser/browser.ftl"]); + return l10n; +} // Default icon used for engines that do not have icons loaded. const DEFAULT_ENGINE_ICON = "chrome://browser/skin/search-engine-placeholder@2x.png"; +const SKIP_TAB_STOP_PREF = "searchModeSwitcher.skipTabStop"; + /** * Implements the SearchModeSwitcher in the urlbar. */ @@ -210,6 +205,16 @@ export class SearchModeSwitcher { this.#input.setUnifiedSearchButtonAvailability(true); return; } + if (event.type == "focusin") { + this.#button.tabIndex = 0; + return; + } + if (event.type == "focusout") { + if (!this.#input.contains(event.relatedTarget)) { + this.#button.tabIndex = -1; + } + return; + } if (event.type == "showing") { this.#onPopupShowing(); return; @@ -358,8 +363,19 @@ export class SearchModeSwitcher { return; } + if (pref == SKIP_TAB_STOP_PREF) { + if (this.#isEnabled()) { + if (UrlbarPrefs.get(pref)) { + this.#enableSkipTabStop(); + } else { + this.#disableSkipTabStop(); + } + } + return; + } + if (this.#input.sapName == "searchbar") { - // The searchbar cares about neither of the two prefs. + // The searchbar cares about neither of the two remaining prefs. return; } @@ -459,7 +475,9 @@ export class SearchModeSwitcher { // all local search modes regardless of the prefs. this.#engines = searchEngines.concat( UrlbarShared.LOCAL_SEARCH_MODES.filter( - engine => lazy.settingsRedesignEnabled || UrlbarPrefs.get(engine.pref) + engine => + UrlbarPrefs.get("browser.settings-redesign.enabled") || + UrlbarPrefs.get(engine.pref) ) ); } @@ -573,9 +591,7 @@ export class SearchModeSwitcher { async #getSearchModeLabel(source) { let mode = UrlbarShared.LOCAL_SEARCH_MODES.find(m => m.source == source); - let [str] = await lazy.SearchModeSwitcherL10n.formatMessages([ - { id: mode.uiLabel }, - ]); + let [str] = await getL10n().formatMessages([{ id: mode.uiLabel }]); return str.value; } @@ -850,6 +866,10 @@ export class SearchModeSwitcher { this.#button.addEventListener("focus", this); this.#button.addEventListener("keydown", this); + if (UrlbarPrefs.get(SKIP_TAB_STOP_PREF)) { + this.#enableSkipTabStop(); + } + this.#panelList.addEventListener("showing", this); this.#panelList.addEventListener("hidden", this); @@ -867,6 +887,8 @@ export class SearchModeSwitcher { this.#button.removeEventListener("focus", this); this.#button.removeEventListener("keydown", this); + this.#disableSkipTabStop(); + this.#panelList.removeEventListener("showing", this); this.#panelList.removeEventListener("hidden", this); @@ -876,6 +898,25 @@ export class SearchModeSwitcher { this.#input.removeEventListener("searchmodechanged", this); } + /** + * The button precedes the input, so it's what the toolbar tab stop in front + * of the widget redirects to. Declining that redirect and joining the tab + * order only while the widget has focus makes Tab land on the input, with + * the button reached by Shift+Tab from there. + */ + #enableSkipTabStop() { + this.#button.setAttribute("keyNav", "skipTabStop"); + this.#input.addEventListener("focusin", this); + this.#input.addEventListener("focusout", this); + } + + #disableSkipTabStop() { + this.#button.removeAttribute("keyNav"); + this.#button.tabIndex = -1; + this.#input.removeEventListener("focusin", this); + this.#input.removeEventListener("focusout", this); + } + /** * @param {string|undefined} icon * The icon. Pass undefined to use the default engine icon. diff --git a/browser/components/urlbar/content/SmartbarInput.mjs b/browser/components/urlbar/content/SmartbarInput.mjs index 3735b4258542..9763a7a972d3 100644 --- a/browser/components/urlbar/content/SmartbarInput.mjs +++ b/browser/components/urlbar/content/SmartbarInput.mjs @@ -48,6 +48,7 @@ const { AppConstants } = ChromeUtils.importESModule( * @import { AIWindow } from "moz-src:///browser/components/aiwindow/ui/components/ai-window/ai-window.mjs" * @import { SmartwindowSmartbarGlow } from "moz-src:///browser/components/aiwindow/ui/components/smartwindow-smartbar-glow/smartwindow-smartbar-glow.mjs" * @import { WindowMode } from "moz-src:///browser/components/urlbar/content/UrlbarInputBase.mjs" + * @import { UrlbarLoadRequest } from "chrome://browser/content/urlbar/UrlbarShared.mjs" */ /** @@ -80,15 +81,12 @@ const lazy = XPCOMUtils.declareLazy({ service: "@mozilla.org/url-query-string-stripper;1", iid: Ci.nsIURLQueryStringStripper, }, - QUERY_STRIPPING_STRIP_ON_SHARE: { - pref: "privacy.query_stripping.strip_on_share.enabled", - default: false, - }, - logger: () => UrlbarShared.getLogger({ prefix: "SmartbarInput" }), getCurrentTabUrl: "moz-src:///browser/components/aiwindow/ui/modules/ChatUtils.sys.mjs", }); +const logger = () => UrlbarShared.getLogger({ prefix: "SmartbarInput" }); + const UNLIMITED_MAX_RESULTS = 99; const MAX_INPUT_LENGTH = 32000; @@ -326,7 +324,7 @@ ${ // get the main browser window. this.window = this.documentGlobal; if (!this.window.gBrowser) { - lazy.logger.debug(`gBrowser not available, get the browser window.`); + logger().debug(`gBrowser not available, get the browser window.`); this.window = window.browsingContext.topChromeWindow; } @@ -884,16 +882,19 @@ ${ } } - #lazy = XPCOMUtils.declareLazy({ - valueFormatter: () => new lazy.UrlbarValueFormatter(this), - addSearchEngineHelper: () => new AddSearchEngineHelper(this), - }); + #addSearchEngineHelper; + + #valueFormatter; /** * Manages the Add Search Engine contextual menu entries. */ get addSearchEngineHelper() { - return this.#lazy.addSearchEngineHelper; + return (this.#addSearchEngineHelper ??= new AddSearchEngineHelper(this)); + } + + #getValueFormatter() { + return (this.#valueFormatter ??= new lazy.UrlbarValueFormatter(this)); } get sapName() { @@ -1121,7 +1122,7 @@ ${ formatValue() { // The editor may not exist if the toolbar is not visible. if (this.#isAddressbar && this.editor) { - this.#lazy.valueFormatter.update(); + this.#getValueFormatter().update(); } } @@ -1832,7 +1833,9 @@ ${ }); this.#dispatchSmartbarCommitEvent(event, value); this.#loadURL({ - url: fixupInfo.preferredURI.spec, + loadRequest: { + urlLoad: { url: fixupInfo.preferredURI.spec, postData: null }, + }, event, where: this.controller.whereToOpen(event), params: { @@ -2125,7 +2128,6 @@ ${ // Use the current value if we don't have a UrlbarResult e.g. because the // view is closed. let url = this.untrimmedValue; - openParams.postData = null; if (!url) { return; @@ -2177,7 +2179,13 @@ ${ if (this.#isSmartbarMode) { this.#dispatchSmartbarCommitEvent(event, this.untrimmedValue); } - this.#loadURL({ url, event, where, params: openParams, browserId }); + this.#loadURL({ + loadRequest: { urlLoad: { url, postData: null } }, + event, + where, + params: openParams, + browserId, + }); return; } @@ -2213,7 +2221,6 @@ ${ if (heuristicResult) { this.pickResult({ result: heuristicResult, event, browserId }); } else if (fixup) { - openParams.postData = fixup.postData; if (!fixup.keywordAsSent) { // `fixup.url` is not a search engine url, so we annotate if the // untrimmed value contained a scheme, to potentially be later @@ -2223,7 +2230,9 @@ ${ ); } this.#loadURL({ - url: fixup.url, + loadRequest: { + urlLoad: { url: fixup.url, postData: fixup.postData }, + }, event, where, params: openParams, @@ -2297,7 +2306,7 @@ ${ */ pickElement(element, event) { let result = this.view.getResultFromElement(element); - lazy.logger.debug( + logger().debug( `pickElement ${element} with event ${event?.type}, result: ${result}` ); if (!result) { @@ -2421,7 +2430,9 @@ ${ windowMode: this.windowMode, }); this.#loadURL({ - url: this._untrimmedValue, + loadRequest: { + urlLoad: { url: this._untrimmedValue, postData: null }, + }, event, where, params: openParams, @@ -2430,10 +2441,9 @@ ${ return; } - let { url, postData } = resultUrl - ? { url: resultUrl, postData: null } - : lazy.UrlbarUtils.getUrlFromResult(result, { element }); - openParams.postData = postData; + let loadRequest = resultUrl + ? { urlLoad: { url: resultUrl, postData: null } } + : UrlbarShared.getLoadRequestFromResult(result, { element }); let isSplitViewActive = this.window.gBrowser.selectedTab.splitview; switch (result.type) { @@ -2461,7 +2471,7 @@ ${ UrlbarPrefs.get("browser.fixup.dns_first_for_single_words") && UrlbarShared.looksLikeSingleWordHost(originalUntrimmedValue) ) { - url = originalUntrimmedValue; + loadRequest.urlLoad.url = originalUntrimmedValue; } // Annotate if the untrimmed value contained a scheme, to later potentially // be upgraded by schemeless HTTPS-First. @@ -2508,7 +2518,7 @@ ${ }); this.controller.switchToTab({ - url, + url: result.payload.url, searchString, userContextId: result.payload.userContext?.id, tabGroup: result.payload.tabGroup, @@ -2585,7 +2595,7 @@ ${ break; } case UrlbarShared.RESULT_TYPE.TIP: { - if (url) { + if (loadRequest) { break; } this.handleRevert(); @@ -2601,8 +2611,8 @@ ${ return; } case UrlbarShared.RESULT_TYPE.DYNAMIC: { - if (!url) { - // If we're not loading a URL, the engagement is done. First revert + if (!loadRequest) { + // If we're not loading anything, the engagement is done. First revert // and then record the engagement since providers expect the urlbar to // be reverted when they're notified of the engagement, but before // reverting, copy the search mode since it's nulled on revert. @@ -2681,12 +2691,13 @@ ${ } } - if (!url) { - throw new Error(`Invalid url for result ${JSON.stringify(result)}`); + if (!loadRequest) { + throw new Error(`No load request for result ${JSON.stringify(result)}`); } - // Record input history but only in non-private windows. - if (!this.isPrivate) { + // Record input history but only in non-private windows and for url loads. + if (!this.isPrivate && loadRequest.urlLoad) { + let url = loadRequest.urlLoad.url; let input; if (!result.heuristic) { input = this._lastSearchString; @@ -2720,7 +2731,7 @@ ${ windowMode: this.windowMode, } ) - .catch(e => lazy.logger.error(e)); + .catch(e => logger().error(e)); } this.controller.engagementEvent.record(event, { @@ -2753,14 +2764,13 @@ ${ this.#dispatchSmartbarCommitEvent(event, this.untrimmedValue, action); } this.#loadURL({ - url, + loadRequest, event, where, params: openParams, resultDetails: { source: result.source, type: result.type, - searchTerm: result.payload.suggestion ?? result.payload.query, }, browserId, }); @@ -4838,7 +4848,7 @@ ${ // Only trim value if the directionality doesn't change to RTL and we're not // showing a strikeout https protocol. return this.controller.isTextDirectionRTL(trimmedValue) || - this.#lazy.valueFormatter.willShowFormattedMixedContentProtocol(val) + this.#getValueFormatter().willShowFormattedMixedContentProtocol(val) ? val : trimmedValue; } @@ -5003,7 +5013,7 @@ ${ this.view.close({ elementPicked: true }); this.#loadURL({ - url, + loadRequest: { urlLoad: { url, postData: null } }, event, where, params: { @@ -5024,8 +5034,6 @@ ${ * * @property {object} [triggeringPrincipal] * The principal that the action was triggered from. - * @property {nsIInputStream} [postData] - * The POST data associated with a search submission. * @property {boolean} [allowInheritPrincipal] * Whether the principal can be inherited. * @property {nsILoadInfo.SchemelessInputType} [schemelessInput] @@ -5038,8 +5046,6 @@ ${ * * @property {Values} [type] * Details of the result type, if any. - * @property {string} [searchTerm] - * Search term of the result source, if any. * @property {Values} [source] * Details of the result source, if any. */ @@ -5048,8 +5054,8 @@ ${ * Loads the url in the appropriate place. * * @param {object} options - * @param {string} options.url - * The URL to open. + * @param {UrlbarLoadRequest} options.loadRequest + * What to load. * @param {Event} options.event * The event that triggered to load the url. * @param {string} options.where @@ -5064,37 +5070,13 @@ ${ * load to the tab selected when it was committed. */ async #loadURL({ - url, + loadRequest, event, where, params, resultDetails = null, browserId = null, }) { - let userTypedValue; - if (this.#isAddressbar && where == "current") { - // Make sure URL is formatted properly (don't show punycode). - let formattedURL = url; - try { - formattedURL = losslessDecodeURI(new URL(url).URI); - } catch {} - - this.value = - lazy.UrlbarUtils.isPersistedSearchTermsEnabled() && - resultDetails?.searchTerm - ? resultDetails.searchTerm - : formattedURL; - userTypedValue = this.value; - } - - params.allowThirdPartyFixup = true; - - if (where == "current") { - params.indicateErrorPageLoad = true; - params.allowPinnedTabHostChange = true; - params.allowPopups = url.startsWith("javascript:"); - } - let keyDownEnterDeferred; if ( this._keyDownEnterDeferred && @@ -5113,6 +5095,31 @@ ${ keyDownEnterDeferred = this._keyDownEnterDeferred; } + let userTypedValue; + if (this.#isAddressbar && where == "current") { + if (loadRequest.engineSearch) { + this.value = loadRequest.engineSearch.query; + } else { + let { url } = loadRequest.urlLoad; + // Make sure URL is formatted properly (don't show punycode). + try { + this.value = losslessDecodeURI(new URL(url).URI); + } catch { + this.value = url; + } + } + userTypedValue = this.value; + } + + params.allowThirdPartyFixup = true; + + if (where == "current") { + params.indicateErrorPageLoad = true; + params.allowPinnedTabHostChange = true; + params.allowPopups = + loadRequest.urlLoad?.url.startsWith("javascript:") ?? false; + } + // Ensure the window gets the `private` feature if the current window // is private, unless the caller explicitly requested not to. if (this.isPrivate && !("private" in params)) { @@ -5133,17 +5140,13 @@ ${ // Notify about the start of navigation. this.#notifyStartNavigation(resultDetails); - let loadStatus = this.controller.loadURL({ - url, + let loadStatus = await this.controller.loadURL({ + loadRequest, where, params, browserId, userTypedValue, }); - // In the message-passing path, loadURL returns a promise. - if (loadStatus.then) { - loadStatus = await loadStatus; - } // Hand the loaded browser's id to the deferred-Enter key up handler so it // can focus it parent-side. keyDownEnterDeferred?.resolve(loadStatus.browserId); @@ -5324,7 +5327,7 @@ ${ .filter(Boolean) .join("@"); } catch (ex) { - lazy.logger.error("Should only try to untrim valid URLs"); + logger().error("Should only try to untrim valid URLs"); } if (!this.#selectedText.startsWith(prePathMinusPort)) { selectionStart += offset; @@ -5369,7 +5372,7 @@ ${ // Register a listener that hides the menu item if there is nothing to copy. contextMenu.addEventListener("popupshowing", () => { // feature is not enabled - if (!lazy.QUERY_STRIPPING_STRIP_ON_SHARE) { + if (!UrlbarPrefs.get("privacy.query_stripping.strip_on_share.enabled")) { stripOnShare.setAttribute("hidden", true); return; } @@ -6062,7 +6065,7 @@ ${ } _on_blur(event) { - lazy.logger.debug("Blur Event"); + logger().debug("Blur Event"); // We cannot count every blur events after a missed engagement as abandoment // because the user may have clicked on some view element that executes // a command causing a focus change. For example opening preferences from @@ -6181,7 +6184,7 @@ ${ _on_contextmenu(event) { if (!this.#isSmartbarMode) { - this.#lazy.addSearchEngineHelper.refreshContextMenu(); + this.addSearchEngineHelper.refreshContextMenu(); } // Context menu opened via keyboard shortcut. @@ -6193,7 +6196,7 @@ ${ } _on_focus(event) { - lazy.logger.debug("Focus Event"); + logger().debug("Focus Event"); if (!this._hideFocus) { this.toggleAttribute("focused", true); } diff --git a/browser/components/urlbar/content/SmartbarInputUtils.mjs b/browser/components/urlbar/content/SmartbarInputUtils.mjs index 46627402bd5d..943bd429ff8f 100644 --- a/browser/components/urlbar/content/SmartbarInputUtils.mjs +++ b/browser/components/urlbar/content/SmartbarInputUtils.mjs @@ -5,6 +5,8 @@ import { MultilineEditor } from "chrome://browser/content/multilineeditor/multiline-editor.mjs"; import { createMentionsPlugin } from "chrome://browser/content/multilineeditor/plugins/MentionsPlugin.mjs"; import { createCommandsPlugin } from "chrome://browser/content/multilineeditor/plugins/CommandsPlugin.mjs"; +import UrlbarPrefs from "chrome://browser/content/urlbar/UrlbarContentPrefs.mjs"; +import { UrlbarShared } from "chrome://browser/content/urlbar/UrlbarShared.mjs"; /** * @import {SmartbarInput} from "chrome://browser/content/urlbar/SmartbarInput.mjs" @@ -25,29 +27,11 @@ ChromeUtils.defineESModuleGetters(lazy, { "moz-src:///browser/components/urlbar/SmartbarMentionsPanelSearch.sys.mjs", }); -const { XPCOMUtils } = ChromeUtils.importESModule( - "resource://gre/modules/XPCOMUtils.sys.mjs" -); - -XPCOMUtils.defineLazyPreferenceGetter( - lazy, - "maxResults", - "browser.urlbar.mentions.maxResults" -); - -XPCOMUtils.defineLazyPreferenceGetter( - lazy, - "agentEnabled", - "browser.smartwindow.agent.enabled", - false -); - -ChromeUtils.defineLazyGetter(lazy, "log", function () { - return console.createInstance({ +const logger = () => + UrlbarShared.getLogger({ prefix: "SmartbarMentionsPanel", maxLogLevelPref: "browser.smartwindow.smartbarMentions.loglevel", }); -}); // Debounce delay for the mention suggestions query. const MENTION_QUERY_DEBOUNCE_MS = 150; @@ -73,7 +57,10 @@ const COMMAND_TRIGGER = "inline-command"; * @returns {boolean} */ function isAgentCommandAvailable() { - return lazy.agentEnabled && lazy.MonitorUIUtils.isMonitorRegionSupported(); + return ( + UrlbarPrefs.get("browser.smartwindow.agent.enabled") && + lazy.MonitorUIUtils.isMonitorRegionSupported() + ); } /** @@ -166,7 +153,7 @@ function getMentionSuggestions(mentionSearch, searchString) { seen.add(item.url); return true; }) - .slice(0, lazy.maxResults) + .slice(0, UrlbarPrefs.get("mentions.maxResults")) .map(({ url, title, icon }) => ({ id: url, label: title, @@ -183,7 +170,7 @@ function getMentionSuggestions(mentionSearch, searchString) { totalCount: deduplicated.length, }; } catch (e) { - lazy.log.error("Error querying tabs:", e); + logger().error("Error querying tabs:", e); return { groups: [], totalCount: 0 }; } } diff --git a/browser/components/urlbar/content/UrlbarChildController.mjs b/browser/components/urlbar/content/UrlbarChildController.mjs index a17a4de079b6..547d25088063 100644 --- a/browser/components/urlbar/content/UrlbarChildController.mjs +++ b/browser/components/urlbar/content/UrlbarChildController.mjs @@ -966,8 +966,6 @@ export class UrlbarChildController { /** @type {HTMLElement} */ const switcher = this.input.querySelector(".searchmode-switcher"); - // Set tabindex to be focusable. - switcher.setAttribute("tabindex", "-1"); // Remove blur listener to avoid closing urlbar view panel. this.input.inputField.removeEventListener("blur", this.input); // Move the focus. @@ -978,8 +976,6 @@ export class UrlbarChildController { "blur", /** @type {(e: FocusEvent) => void} */ e => { - switcher.removeAttribute("tabindex"); - let relatedTarget = /** @type {HTMLElement} */ (e.relatedTarget); if ( this.input.hasAttribute("focused") && diff --git a/browser/components/urlbar/content/UrlbarInputBase.mjs b/browser/components/urlbar/content/UrlbarInputBase.mjs index 94e4be7e0c3e..16ba2b2deab9 100644 --- a/browser/components/urlbar/content/UrlbarInputBase.mjs +++ b/browser/components/urlbar/content/UrlbarInputBase.mjs @@ -24,6 +24,7 @@ import { UrlbarShared } from "chrome://browser/content/urlbar/UrlbarShared.mjs"; * @import { SuggestBackendMerino } from "moz-src:///browser/components/urlbar/private/SuggestBackendMerino.sys.mjs" * @import { PartialSearchEngine } from "chrome://browser/content/urlbar/SearchEngineStore.mjs" * @import { BrowserSearchTelemetry } from "moz-src:///browser/components/search/BrowserSearchTelemetry.sys.mjs" + * @import { UrlbarLoadRequest } from "chrome://browser/content/urlbar/UrlbarShared.mjs" */ /** @@ -81,13 +82,10 @@ const lazy = XPCOMUtils.declareLazy({ service: "@mozilla.org/url-query-string-stripper;1", iid: Ci.nsIURLQueryStringStripper, }, - QUERY_STRIPPING_STRIP_ON_SHARE: { - pref: "privacy.query_stripping.strip_on_share.enabled", - default: false, - }, - logger: () => UrlbarShared.getLogger({ prefix: "Input" }), }); +const logger = () => UrlbarShared.getLogger({ prefix: "Input" }); + const UNLIMITED_MAX_RESULTS = 99; let getBoundsWithoutFlushing = element => @@ -677,16 +675,19 @@ ${ } } - #lazy = XPCOMUtils.declareLazy({ - valueFormatter: () => new lazy.UrlbarValueFormatter(this), - addSearchEngineHelper: () => new AddSearchEngineHelper(this), - }); + #addSearchEngineHelper; + + #valueFormatter; /** * Manages the Add Search Engine contextual menu entries. */ get addSearchEngineHelper() { - return this.#lazy.addSearchEngineHelper; + return (this.#addSearchEngineHelper ??= new AddSearchEngineHelper(this)); + } + + #getValueFormatter() { + return (this.#valueFormatter ??= new lazy.UrlbarValueFormatter(this)); } get sapName() { @@ -774,7 +775,7 @@ ${ formatValue() { // The editor may not exist if the toolbar is not visible. if (this.#isAddressbar && this.editor) { - this.#lazy.valueFormatter.update(); + this.#getValueFormatter().update(); } } @@ -1415,7 +1416,6 @@ ${ // Use the current value if we don't have a UrlbarResult e.g. because the // view is closed. let url = this.untrimmedValue; - openParams.postData = null; if (!url) { this.#handleEmptyValueNavigation(event); @@ -1464,7 +1464,13 @@ ${ openParams.schemelessInput = this.#getSchemelessInput( this.untrimmedValue ); - this.#loadURL({ url, event, where, params: openParams, browserId }); + this.#loadURL({ + loadRequest: { urlLoad: { url, postData: null } }, + event, + where, + params: openParams, + browserId, + }); return; } @@ -1501,7 +1507,6 @@ ${ if (heuristicResult) { this.pickResult({ result: heuristicResult, event, browserId }); } else if (fixup) { - openParams.postData = fixup.postData; if (!fixup.keywordAsSent) { // `fixup.url` is not a search engine url, so we annotate if the // untrimmed value contained a scheme, to potentially be later @@ -1511,7 +1516,9 @@ ${ ); } this.#loadURL({ - url: fixup.url, + loadRequest: { + urlLoad: { url: fixup.url, postData: fixup.postData }, + }, event, where, params: openParams, @@ -1607,7 +1614,7 @@ ${ */ pickElement(element, event) { let result = this.view.getResultFromElement(element); - lazy.logger.debug( + logger().debug( `pickElement ${element} with event ${event?.type}, result: ${result}` ); if (!result) { @@ -1772,7 +1779,9 @@ ${ windowMode: this.windowMode, }); this.#loadURL({ - url: this._untrimmedValue, + loadRequest: { + urlLoad: { url: this._untrimmedValue, postData: null }, + }, event, where, params: openParams, @@ -1781,10 +1790,9 @@ ${ return; } - let { url, postData } = resultUrl - ? { url: resultUrl, postData: null } - : lazy.UrlbarUtils.getUrlFromResult(result, { element }); - openParams.postData = postData; + let loadRequest = resultUrl + ? { urlLoad: { url: resultUrl, postData: null } } + : UrlbarShared.getLoadRequestFromResult(result, { element }); switch (result.type) { case UrlbarShared.RESULT_TYPE.URL: { @@ -1811,7 +1819,7 @@ ${ UrlbarPrefs.get("browser.fixup.dns_first_for_single_words") && UrlbarShared.looksLikeSingleWordHost(originalUntrimmedValue) ) { - url = originalUntrimmedValue; + loadRequest.urlLoad.url = originalUntrimmedValue; } // Annotate if the untrimmed value contained a scheme, to later potentially // be upgraded by schemeless HTTPS-First. @@ -1856,7 +1864,7 @@ ${ }); this.controller.switchToTab({ - url, + url: result.payload.url, searchString, userContextId: result.payload.userContext?.id, tabGroup: result.payload.tabGroup, @@ -1967,7 +1975,7 @@ ${ break; } case UrlbarShared.RESULT_TYPE.TIP: { - if (url) { + if (loadRequest) { break; } this.handleRevert(); @@ -1982,8 +1990,8 @@ ${ return; } case UrlbarShared.RESULT_TYPE.DYNAMIC: { - if (!url) { - // If we're not loading a URL, the engagement is done. First revert + if (!loadRequest) { + // If we're not loading anything, the engagement is done. First revert // and then record the engagement since providers expect the urlbar to // be reverted when they're notified of the engagement, but before // reverting, copy the search mode since it's nulled on revert. @@ -2069,14 +2077,15 @@ ${ } } - if (!url) { - throw new Error(`Invalid url for result ${JSON.stringify(result)}`); + if (!loadRequest) { + throw new Error(`No load request for result ${JSON.stringify(result)}`); } - // Record input history but only in non-private windows. - if (!this.isPrivate) { + // Record input history but only in non-private windows and for url loads. + if (!this.isPrivate && loadRequest.urlLoad) { + let url = loadRequest.urlLoad.url; let input; - if (!result.heuristic && result.type != UrlbarShared.RESULT_TYPE.SEARCH) { + if (!result.heuristic) { input = this._lastSearchString; } else if ( result.autofill?.type == "adaptive_url" || @@ -2131,7 +2140,7 @@ ${ windowMode: this.windowMode, } ) - .catch(e => lazy.logger.error(e)); + .catch(e => logger().error(e)); } this.controller.engagementEvent.record(event, { @@ -2144,14 +2153,13 @@ ${ }); this.#loadURL({ - url, + loadRequest, event, where, params: openParams, resultDetails: { source: result.source, type: result.type, - searchTerm: result.payload.suggestion ?? result.payload.query, }, keepViewOpen, browserId, @@ -4015,7 +4023,7 @@ ${ // Only trim value if the directionality doesn't change to RTL and we're not // showing a strikeout https protocol. return this.controller.isTextDirectionRTL(trimmedValue) || - this.#lazy.valueFormatter.willShowFormattedMixedContentProtocol(val) + this.#getValueFormatter().willShowFormattedMixedContentProtocol(val) ? val : trimmedValue; } @@ -4146,7 +4154,7 @@ ${ this.view.close({ elementPicked: true }); this.#loadURL({ - url, + loadRequest: { urlLoad: { url, postData: null } }, event, where, params: { @@ -4167,8 +4175,6 @@ ${ * * @property {object} [triggeringPrincipal] * The principal that the action was triggered from. - * @property {nsIInputStream} [postData] - * The POST data associated with a search submission. * @property {boolean} [allowInheritPrincipal] * Whether the principal can be inherited. * @property {nsILoadInfo.SchemelessInputType} [schemelessInput] @@ -4181,8 +4187,6 @@ ${ * * @property {Values} [type] * Details of the result type, if any. - * @property {string} [searchTerm] - * Search term of the result source, if any. * @property {Values} [source] * Details of the result source, if any. */ @@ -4191,8 +4195,8 @@ ${ * Loads the url in the appropriate place. * * @param {object} options - * @param {string} options.url - * The URL to open. + * @param {UrlbarLoadRequest} options.loadRequest + * What to load. * @param {Event} options.event * The event that triggered to load the url. * @param {string} options.where @@ -4209,7 +4213,7 @@ ${ * load to the tab selected when it was committed. */ async #loadURL({ - url, + loadRequest, event, where, params, @@ -4217,30 +4221,6 @@ ${ keepViewOpen = false, browserId = null, }) { - let userTypedValue; - if (this.#isAddressbar && where == "current") { - // Make sure URL is formatted properly (don't show punycode). - let formattedURL = url; - try { - formattedURL = losslessDecodeURI(new URL(url).URI); - } catch {} - - this.value = - lazy.UrlbarUtils.isPersistedSearchTermsEnabled() && - resultDetails?.searchTerm - ? resultDetails.searchTerm - : formattedURL; - userTypedValue = this.value; - } - - params.allowThirdPartyFixup = true; - - if (where == "current") { - params.indicateErrorPageLoad = true; - params.allowPinnedTabHostChange = this.#isAddressbar; - params.allowPopups = url.startsWith("javascript:"); - } - let keyDownEnterDeferred; if ( this._keyDownEnterDeferred && @@ -4259,6 +4239,31 @@ ${ keyDownEnterDeferred = this._keyDownEnterDeferred; } + let userTypedValue; + if (this.#isAddressbar && where == "current") { + if (loadRequest.engineSearch) { + this.value = loadRequest.engineSearch.query; + } else { + let { url } = loadRequest.urlLoad; + // Make sure URL is formatted properly (don't show punycode). + try { + this.value = losslessDecodeURI(new URL(url).URI); + } catch { + this.value = url; + } + } + userTypedValue = this.value; + } + + params.allowThirdPartyFixup = true; + + if (where == "current") { + params.indicateErrorPageLoad = true; + params.allowPinnedTabHostChange = this.#isAddressbar; + params.allowPopups = + loadRequest.urlLoad?.url.startsWith("javascript:") ?? false; + } + // Ensure the window gets the `private` feature if the current window // is private, unless the caller explicitly requested not to. if (this.isPrivate && !("private" in params)) { @@ -4279,17 +4284,13 @@ ${ // Notify about the start of navigation. this.#notifyStartNavigation(resultDetails); - let loadStatus = this.controller.loadURL({ - url, + let loadStatus = await this.controller.loadURL({ + loadRequest, where, params, browserId, userTypedValue, }); - // In the message-passing path, loadURL returns a promise. - if (loadStatus.then) { - loadStatus = await loadStatus; - } // Hand the loaded browser's id to the deferred-Enter key up handler so it // can focus it parent-side. keyDownEnterDeferred?.resolve(loadStatus.browserId); @@ -4468,7 +4469,7 @@ ${ .filter(Boolean) .join("@"); } catch (ex) { - lazy.logger.error("Should only try to untrim valid URLs"); + logger().error("Should only try to untrim valid URLs"); } if (!this.#selectedText.startsWith(prePathMinusPort)) { selectionStart += offset; @@ -4512,7 +4513,7 @@ ${ // Register a listener that hides the menu item if there is nothing to copy. this.#addContextMenuListener(() => { // feature is not enabled - if (!lazy.QUERY_STRIPPING_STRIP_ON_SHARE) { + if (!UrlbarPrefs.get("privacy.query_stripping.strip_on_share.enabled")) { stripOnShare.setAttribute("hidden", true); return; } @@ -5185,7 +5186,7 @@ ${ return; } - lazy.logger.debug("Blur Event"); + logger().debug("Blur Event"); // We cannot count every blur events after a missed engagement as abandoment // because the user may have clicked on some view element that executes // a command causing a focus change. For example opening preferences from @@ -5310,7 +5311,7 @@ ${ } _on_contextmenu(event) { - this.#lazy.addSearchEngineHelper.refreshContextMenu(event); + this.addSearchEngineHelper.refreshContextMenu(event); // Context menu opened via keyboard shortcut. if (!event.button) { @@ -5321,7 +5322,7 @@ ${ } _on_focus(event) { - lazy.logger.debug("Focus Event"); + logger().debug("Focus Event"); if (!this._hideFocus) { this.toggleAttribute("focused", true); } diff --git a/browser/components/urlbar/content/UrlbarShared.mjs b/browser/components/urlbar/content/UrlbarShared.mjs index 2ab8b6f2ae16..7b8fd0e571fe 100644 --- a/browser/components/urlbar/content/UrlbarShared.mjs +++ b/browser/components/urlbar/content/UrlbarShared.mjs @@ -32,6 +32,30 @@ import UrlbarPrefs from "chrome://browser/content/urlbar/UrlbarContentPrefs.mjs" * Has a value and an accesskey attribute. */ +/** + * @typedef {object} UrlLoad + * A direct URL load. + * @property {string} url + * The url to load. + * @property {?string} postData + * The post data, or null for a GET. + */ + +/** + * @typedef {object} EngineSearchLoad + * A search to submit to an engine. + * @property {string} engineName + * The name of the search engine. + * @property {string} query + * The query. + */ + +/** + * @typedef {{urlLoad: UrlLoad, engineSearch?: never} | + * {engineSearch: EngineSearchLoad, urlLoad?: never}} UrlbarLoadRequest + * Either a URL or an engine search. + */ + /** * @typedef {object} URIFixupPrimitives * The parts of an `nsIURIFixupInfo` that survive the actor boundary, so a @@ -467,10 +491,13 @@ export const UrlbarShared = { * @param {object} [options] * @param {string} [options.prefix] * Prefix to use for the logged messages. + * @param {string} [options.maxLogLevelPref] + * The pref holding the maximum log level. It has to be known to + * `UrlbarPrefs`, which is how the logger reads it outside chrome. * @returns {Console} * The console logger. */ - getLogger({ prefix = "" } = {}) { + getLogger({ prefix = "", maxLogLevelPref = "browser.urlbar.loglevel" } = {}) { let logger = loggers.get(prefix); if (logger) { return logger; @@ -478,14 +505,55 @@ export const UrlbarShared = { let fullPrefix = `URLBar${prefix ? " - " + prefix : ""}`; if (console.createInstance) { - logger = createLoggerChrome(fullPrefix); + logger = createLoggerChrome(fullPrefix, maxLogLevelPref); } else { - logger = createLoggerContent(fullPrefix); + logger = createLoggerContent(fullPrefix, maxLogLevelPref); } loggers.set(prefix, logger); return logger; }, + /** + * Extracts the URL from a result. + * + * @param {UrlbarResult} result + * The result to extract from. + * @param {object} options + * Options object. + * @param {HTMLElement} [options.element] + * The element associated with the result that was selected or picked, if + * available. For results that have multiple selectable children, the URL + * may be taken from a child element rather than the result. + * @returns {?UrlbarLoadRequest} + * Null if the result has nothing to load. + */ + getLoadRequestFromResult(result, { element = null } = {}) { + if ( + result.payload.engine && + (result.type == UrlbarShared.RESULT_TYPE.SEARCH || + result.type == UrlbarShared.RESULT_TYPE.DYNAMIC) + ) { + let query = + element?.dataset.query || + result.payload.suggestion || + result.payload.query; + if (query) { + return { engineSearch: { query, engineName: result.payload.engine } }; + } + } + + if (!result.payload.url) { + return null; + } + + return { + urlLoad: { + url: result.payload.url, + postData: result.payload.postData ?? null, + }, + }; + }, + /** * Deep-equality check for plain JSON-like data (arrays, objects, primitives), * so content-realm modules needn't import ObjectUtils (a system module). Not a @@ -1464,13 +1532,11 @@ export const UrlbarShared = { * Create a logger that uses `console.createInstance`. * * @param {string} prefix + * @param {string} maxLogLevelPref * @returns {Console} */ -function createLoggerChrome(prefix) { - let logger = console.createInstance({ - prefix, - maxLogLevelPref: "browser.urlbar.loglevel", - }); +function createLoggerChrome(prefix, maxLogLevelPref) { + let logger = console.createInstance({ prefix, maxLogLevelPref }); // Casting from ConsoleInstance to Console. Note that it is technically not a // `Console` because it is missing the chrome-only property `createInstance`. return /** @type {Console} */ (/** @type {unknown} */ (logger)); @@ -1480,9 +1546,10 @@ function createLoggerChrome(prefix) { * Create a logger that uses the global `console`. * * @param {string} prefix + * @param {string} maxLogLevelPref * @returns {Console} */ -function createLoggerContent(prefix) { +function createLoggerContent(prefix, maxLogLevelPref) { let tag = `[${prefix}]`; const LEVEL_NUMBERS = { all: 0, @@ -1496,9 +1563,12 @@ function createLoggerContent(prefix) { }; const LEVELS = ["debug", "log", "info", "trace", "warn", "error"]; + // UrlbarPrefs names prefs in the `browser.urlbar.` branch relative to it. + let levelPref = maxLogLevelPref.replace(/^browser\.urlbar\./, ""); + let shouldLog = level => { let maxLevel = - LEVEL_NUMBERS[UrlbarPrefs.get("loglevel").toLowerCase()] ?? + LEVEL_NUMBERS[UrlbarPrefs.get(levelPref).toLowerCase()] ?? LEVEL_NUMBERS.warn; return maxLevel <= LEVEL_NUMBERS[level]; }; diff --git a/browser/components/urlbar/content/UrlbarView.mjs b/browser/components/urlbar/content/UrlbarView.mjs index 2fc2434352f1..51fe02a4dc4b 100644 --- a/browser/components/urlbar/content/UrlbarView.mjs +++ b/browser/components/urlbar/content/UrlbarView.mjs @@ -12,7 +12,6 @@ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { UrlbarSearchOneOffs: "moz-src:///browser/components/urlbar/UrlbarSearchOneOffs.sys.mjs", - UrlbarUtils: "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs", }); // Query selector for selectable elements in results. @@ -1935,7 +1934,7 @@ export class UrlbarView { noWrap.appendChild(titleSeparator); item._elements.set("titleSeparator", titleSeparator); - if (Services.prefs.getBoolPref("browser.nova.enabled", false)) { + if (UrlbarPrefs.get("browser.nova.enabled")) { let userContext = this.#createElement("span"); userContext.classList.add( "urlbarView-user-context", @@ -3664,7 +3663,7 @@ export class UrlbarView { : "urlbar-result-action-switch-tab", }); - if (!Services.prefs.getBoolPref("browser.nova.enabled", false)) { + if (!UrlbarPrefs.get("browser.nova.enabled")) { this.#updateOtherActionChicletsProton(result, actionNode); return; } @@ -4564,11 +4563,11 @@ export class UrlbarView { // as if it were hovered while the context menu is open. row.toggleAttribute("menu-trigger", true); - // Disable the context menu if the result does not return url. - let url = lazy.UrlbarUtils.getUrlFromResult(row.result, { + // Disable the context menu if the result does not return a load request. + let loadRequest = UrlbarShared.getLoadRequestFromResult(row.result, { element: row, - })?.url; - event.target.toggleAttribute("disabled", !url); + }); + event.target.toggleAttribute("disabled", !loadRequest); } else if ( event.target.id == "urlbarView-context-menu-open-in-container-tab-popup" ) { diff --git a/browser/components/urlbar/tests/browser/browser_dns_first_for_single_words.js b/browser/components/urlbar/tests/browser/browser_dns_first_for_single_words.js index 036730826b12..0226610551a4 100644 --- a/browser/components/urlbar/tests/browser/browser_dns_first_for_single_words.js +++ b/browser/components/urlbar/tests/browser/browser_dns_first_for_single_words.js @@ -24,11 +24,19 @@ add_task(async function test() { * to the docshell that will first execute a DNS request. */ async function testVal(str, passthrough) { - sandbox.stub(gURLBar.controller, "loadURL").callsFake(({ url }) => { + sandbox.stub(gURLBar.controller, "loadURL").callsFake(({ loadRequest }) => { if (passthrough) { - Assert.equal(url, str, "Should pass the unmodified search string"); + Assert.equal( + loadRequest.urlLoad.url, + str, + "Should pass a url load for the unmodified search string" + ); } else { - Assert.ok(url.startsWith("http"), "Should pass an url"); + Assert.ok( + loadRequest.engineSearch || + loadRequest.urlLoad.url.startsWith("http"), + "Should pass an engine search or a fixed up url" + ); } return {}; }); diff --git a/browser/components/urlbar/tests/browser/browser_edit_invalid_url.js b/browser/components/urlbar/tests/browser/browser_edit_invalid_url.js index 11b5fc4766d2..65eaddf6efbf 100644 --- a/browser/components/urlbar/tests/browser/browser_edit_invalid_url.js +++ b/browser/components/urlbar/tests/browser/browser_edit_invalid_url.js @@ -85,7 +85,7 @@ async function waitforLoadURL() { let sandbox = sinon.createSandbox(); let loadedUrl = await new Promise(resolve => sandbox.stub(gURLBar.controller, "loadURL").callsFake(options => { - resolve(options.url); + resolve(options.loadRequest.urlLoad.url); return {}; }) ); diff --git a/browser/components/urlbar/tests/browser/browser_enter.js b/browser/components/urlbar/tests/browser/browser_enter.js index f6d86cd866b4..8d9491e24c23 100644 --- a/browser/components/urlbar/tests/browser/browser_enter.js +++ b/browser/components/urlbar/tests/browser/browser_enter.js @@ -182,18 +182,6 @@ add_task(async function searchOnEnterSoon() { "The input field in urlbar still has focus" ); - // Check the caret position. - Assert.equal( - gURLBar.selectionStart, - gURLBar.value.length, - "The selectionStart indicates at ending of the value" - ); - Assert.equal( - gURLBar.selectionEnd, - gURLBar.value.length, - "The selectionEnd indicates at ending of the value" - ); - // Keyup both key as soon as pagehide event happens. EventUtils.synthesizeKey("x", { type: "keyup" }); EventUtils.synthesizeKey("KEY_Enter", { type: "keyup" }); @@ -208,18 +196,6 @@ add_task(async function searchOnEnterSoon() { const result = await onResult; is(result, "unload", "Keyup event is not captured."); - // Check the caret position again. - Assert.equal( - gURLBar.selectionStart, - 0, - "The selectionStart indicates at beginning of the value" - ); - Assert.equal( - gURLBar.selectionEnd, - 0, - "The selectionEnd indicates at beginning of the value" - ); - // Cleanup. await onLoad; BrowserTestUtils.removeTab(tab); @@ -267,6 +243,42 @@ add_task(async function searchByMultipleEnters() { BrowserTestUtils.removeTab(tab); }); +// Make sure the caret stays at the end after enter keydown, but +// goes to the beginning when the browser is focused (Bug 1676054). +add_task(async function goToBeginningAfterKeyup() { + info("Search on Enter, keeping the key down"); + const tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + START_VALUE + ); + const url = "https://example.com/some/url"; + + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + gURLBar.value = url; + EventUtils.synthesizeKey("KEY_Enter", { type: "keydown" }); + + // Check the caret position a bit after keydown. + // It should still be at the end + await TestUtils.waitForTick(); + Assert.equal( + gURLBar.selectionStart, + gURLBar.value.length, + "The selectionStart indicates at ending of the value" + ); + + // Check the caret position again after keyup when the browser is focused. + // It should now be at the beginning for spoof protection and usability. + // The parent focuses the browser and replies, so the caret moves a hop after. + EventUtils.synthesizeKey("KEY_Enter", { type: "keyup" }); + await TestUtils.waitForCondition( + () => !gURLBar.selectionStart && !gURLBar.selectionEnd, + "The caret moved to the beginning of the value" + ); + + // Cleanup. + BrowserTestUtils.removeTab(tab); +}); + add_task(async function typeCharWhileProcessingEnter() { info("Typing a char while processing enter key"); const tab = await BrowserTestUtils.openNewForegroundTab( diff --git a/browser/components/urlbar/tests/browser/browser_handleCommand_fallback.js b/browser/components/urlbar/tests/browser/browser_handleCommand_fallback.js index cefb047d3f22..e288304478dd 100644 --- a/browser/components/urlbar/tests/browser/browser_handleCommand_fallback.js +++ b/browser/components/urlbar/tests/browser/browser_handleCommand_fallback.js @@ -54,11 +54,11 @@ add_task(async function () { return new Promise(resolve => { sandbox .stub(gURLBar.controller, "loadURL") - .callsFake(({ url, where }) => { + .callsFake(({ loadRequest, where }) => { sandbox.restore(); // The remaining options are optional and apply only to some cases, so // we could not use deepEqual with them. - resolve([url, where]); + resolve([loadRequest, where]); return {}; }); }); @@ -116,13 +116,15 @@ add_task(async function no_heuristic_test() { async function promiseLoadURL() { return new Promise(resolve => { - sinon.stub(gURLBar.controller, "loadURL").callsFake(({ url, where }) => { - gURLBar.controller.loadURL.restore(); - // The remaining options are optional and apply only to some cases, so - // we could not use deepEqual with them. - resolve([url, where]); - return {}; - }); + sinon + .stub(gURLBar.controller, "loadURL") + .callsFake(({ loadRequest, where }) => { + gURLBar.controller.loadURL.restore(); + // The remaining options are optional and apply only to some cases, so + // we could not use deepEqual with them. + resolve([loadRequest, where]); + return {}; + }); }); } @@ -138,10 +140,10 @@ add_task(async function no_heuristic_test() { let promise = promiseLoadURL(); gURLBar.value = value; EventUtils.synthesizeKey("KEY_Enter"); - // The loaded url should always be a valid url, so this should never throw. + // The fallback always fixes up to a valid url, so this should never throw. // Awaiting it also lets the message path round-trip the fallback before we // check the stub below. - new URL((await promise)[0]); + new URL((await promise)[0].urlLoad.url); Assert.ok(stub.called, "invoked getHeuristicResult"); } }); diff --git a/browser/components/urlbar/tests/browser/searchbar/browser_searchbar_keyboard_navigation.js b/browser/components/urlbar/tests/browser/searchbar/browser_searchbar_keyboard_navigation.js index d36281f2f420..ec498d8121fb 100644 --- a/browser/components/urlbar/tests/browser/searchbar/browser_searchbar_keyboard_navigation.js +++ b/browser/components/urlbar/tests/browser/searchbar/browser_searchbar_keyboard_navigation.js @@ -9,6 +9,12 @@ let searchbar; let widgetAfterSearchbar; add_setup(async function () { + // The tab order asserted below is otherwise channel-dependent. The skipping + // variant is covered by test_tabOrder_skipTabStop. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.searchModeSwitcher.skipTabStop", false]], + }); + searchbar = document.getElementById("searchbar-new"); await SearchTestUtils.updateRemoteSettingsConfig([ { identifier: "engine1" }, @@ -63,6 +69,36 @@ add_task(async function test_tabOrder() { searchbar.handleRevert(); }); +add_task(async function test_tabOrder_skipTabStop() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.searchModeSwitcher.skipTabStop", true]], + }); + let searchModeSwitcher = searchbar.querySelector(".searchmode-switcher"); + + searchbar.focus(); + Assert.ok(searchbar.focused); + + EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }); + Assert.equal( + document.activeElement, + searchModeSwitcher, + "Shift+Tab reaches the search button" + ); + EventUtils.synthesizeKey("KEY_Tab"); + Assert.ok( + searchbar.focused, + "Tab from the search button returns to the input" + ); + + EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }); + EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }); + Assert.ok(gURLBar.focused); + EventUtils.synthesizeKey("KEY_Tab"); + Assert.ok(searchbar.focused, "Tab skips the search button"); + + await SpecialPowers.popPrefEnv(); +}); + add_task(async function test_openCloseResultsPanel() { await SearchbarTestUtils.promisePopupOpen(window, () => { searchbar.focus(); diff --git a/browser/extensions/pictureinpicture/data/picture_in_picture_overrides.js b/browser/extensions/pictureinpicture/data/picture_in_picture_overrides.js index b04942e86059..d53a1b677e1b 100644 --- a/browser/extensions/pictureinpicture/data/picture_in_picture_overrides.js +++ b/browser/extensions/pictureinpicture/data/picture_in_picture_overrides.js @@ -117,6 +117,12 @@ let AVAILABLE_PIP_OVERRIDES; "https://player.ceskatelevize.cz/*": { showHiddenTextTracks: true, }, + "https://sport.ceskatelevize.cz/*": { + showHiddenTextTracks: true, + }, + "https://www.ceskatelevize.cz/zive/*": { + showHiddenTextTracks: true, + }, }, cnbc: { diff --git a/browser/installer/windows/nsis/install_dir_helpers.nsh b/browser/installer/windows/nsis/install_dir_helpers.nsh index 20362f8d4b12..9145e2256c6b 100644 --- a/browser/installer/windows/nsis/install_dir_helpers.nsh +++ b/browser/installer/windows/nsis/install_dir_helpers.nsh @@ -59,4 +59,15 @@ !macroend !define UseExistingInstallPathIfNoInstallDirArg "!insertmacro UseExistingInstallPathIfNoInstallDirArg" +Function GetProfileDirExisted + Push $0 + ${GetLocalAppDataFolder} $0 + ${If} ${FileExists} "$0\Mozilla\Firefox" + StrCpy $0 "true" + ${Else} + StrCpy $0 "false" + ${EndIf} + Exch $0 +FunctionEnd + !endif diff --git a/browser/installer/windows/nsis/installer.nsi b/browser/installer/windows/nsis/installer.nsi index 785e5ccfba55..4a402f71f6ab 100755 --- a/browser/installer/windows/nsis/installer.nsi +++ b/browser/installer/windows/nsis/installer.nsi @@ -58,6 +58,7 @@ Var RegHive Var SetAsDefault Var HadOldInstall Var InstallExisted +Var ProfDirExisted Var DefaultInstDir Var IntroPhaseStart Var OptionsPhaseStart @@ -296,6 +297,10 @@ Section "-InstallStartCleanup" Call CheckIfInstallExisted + ; Set $ProfDirExisted; must run before any profile initialization + Call GetProfileDirExisted + Pop $ProfDirExisted + ; Delete the app exe if present to prevent launching the app while we are ; installing. ClearErrors @@ -1066,13 +1071,7 @@ Function WriteInstallationTelemetryData ; Check for top-level profile directory ; Note: This is the same check used to set $HadExistingProfile in stub.nsi - ${GetLocalAppDataFolder} $0 - ${If} ${FileExists} "$0\Mozilla\Firefox" - StrCpy $1 "true" - ${Else} - StrCpy $1 "false" - ${EndIf} - ${JSONSet} "profdir_existed" /value $1 + ${JSONSet} "profdir_existed" /value $ProfDirExisted ${GetParameters} $0 ${GetOptions} $0 "/LaunchedFromStub" $1 @@ -1552,6 +1551,7 @@ Function .onInit StrCpy $SetAsDefault true StrCpy $HadOldInstall false StrCpy $InstallExisted "" + StrCpy $ProfDirExisted "" StrCpy $DefaultInstDir $INSTDIR StrCpy $IntroPhaseStart 0 StrCpy $OptionsPhaseStart 0 diff --git a/browser/installer/windows/nsis/test_stub.nsi b/browser/installer/windows/nsis/test_stub.nsi index ea9a7a7dd8c4..c6efb4a867a4 100644 --- a/browser/installer/windows/nsis/test_stub.nsi +++ b/browser/installer/windows/nsis/test_stub.nsi @@ -184,6 +184,9 @@ Function .onInit ${UnitTest} TestGetHadExistingProfileFailure ${UnitTest} TestGetHadExistingProfileSuccess + ${UnitTest} TestGetProfileDirExistedFailure + ${UnitTest} TestGetProfileDirExistedSuccess + ${UnitTest} TestIsInstallerLaunchedByDesktopLauncherNoParameter ${UnitTest} TestIsInstallerLaunchedByDesktopLauncherUnknownParameter ${UnitTest} TestIsInstallerLaunchedByDesktopLauncherSuccess @@ -680,6 +683,32 @@ Function TestGetHadExistingProfileSuccess RMDir /r $MockLocalAppDataFolder FunctionEnd +Function TestGetProfileDirExistedFailure + GetTempFileName $0 + Delete $0 + CreateDirectory $0 + StrCpy $MockLocalAppDataFolder $0 + + Call GetProfileDirExisted + Pop $0 + ${AssertEqual} 0 "false" + + RMDir $MockLocalAppDataFolder +FunctionEnd + +Function TestGetProfileDirExistedSuccess + GetTempFileName $0 + Delete $0 + CreateDirectory "$0\Mozilla\Firefox" + StrCpy $MockLocalAppDataFolder $0 + + Call GetProfileDirExisted + Pop $0 + ${AssertEqual} 0 "true" + + RMDir /r $MockLocalAppDataFolder +FunctionEnd + Function TestIsInstallerLaunchedByDesktopLauncherNoParameter StrCpy $MockParameters "" Call IsInstallerLaunchedByDesktopLauncher diff --git a/browser/locales/en-US/browser/sidebar.ftl b/browser/locales/en-US/browser/sidebar.ftl index d70c20901413..bb15a5a87f9b 100644 --- a/browser/locales/en-US/browser/sidebar.ftl +++ b/browser/locales/en-US/browser/sidebar.ftl @@ -87,6 +87,10 @@ sidebar-show-on-the-left = # hovers over it. expand-sidebar-on-hover = .label = Expand sidebar on hover +# Option to show a preview of the most recently active tabs when the mouse +# pointer hovers over the Open Tabs button in the sidebar. +sidebar-show-preview-on-hover = + .label = Show preview on hover sidebar-manage-extensions2 = Manage all extensions ## Labels for sidebar context menu items diff --git a/browser/themes/shared/identity-block/identity-block.css b/browser/themes/shared/identity-block/identity-block.css index fc59a9deb365..d809a89d8eef 100644 --- a/browser/themes/shared/identity-block/identity-block.css +++ b/browser/themes/shared/identity-block/identity-block.css @@ -408,6 +408,12 @@ max-width: 0; overflow: hidden; box-sizing: border-box; + /* Also zero the padding so max-width: 0 can reach a true 0 width; + otherwise the leading digit shows as a sliver through the padding + band. !important is required because the first-visit intro + animation holds the padding, and animations outrank normal + declarations in the cascade. */ + padding: 0 !important; } } @media (prefers-reduced-motion: reduce) { diff --git a/build/clang-plugin/FinalCycleCollectingIsupportsChecker.cpp b/build/clang-plugin/FinalCycleCollectingIsupportsChecker.cpp index a264c0701623..2ddcb58a0225 100644 --- a/build/clang-plugin/FinalCycleCollectingIsupportsChecker.cpp +++ b/build/clang-plugin/FinalCycleCollectingIsupportsChecker.cpp @@ -14,7 +14,7 @@ void FinalCycleCollectingIsupportsChecker::registerMatchers( // uses `override`; NS_DECL_CYCLE_COLLECTING_ISUPPORTS_FINAL uses `final`. AstMatcher->addMatcher( cxxRecordDecl( - isFinal(), isInPath("dom/html"), + isFinal(), isInPath("/dom/"), has(cxxMethodDecl(hasName("AddRef"), isOverride(), unless(isFinal()), isExpandedFromMacro( "NS_DECL_CYCLE_COLLECTING_ISUPPORTS_META")) diff --git a/devtools/client/definitions.js b/devtools/client/definitions.js index dc43575f878a..19a887cd5781 100644 --- a/devtools/client/definitions.js +++ b/devtools/client/definitions.js @@ -844,71 +844,28 @@ function createHighlightButton({ highlighterTypes, id }) { isToolSupported: toolbox => toolbox.commands.descriptorFront.isTabDescriptor, async onClick(event, toolbox) { - // @backward-compat { version 154 } Firefox 154 started supporting toggling - // global highlighters via Target Actor Configuration. The else branch can be later removed. - const { targetConfigurationCommand } = toolbox.commands; - if (await targetConfigurationCommand.supports("enabledHighlighters")) { - const { configuration } = targetConfigurationCommand; - let highlighters = configuration.enabledHighlighters || []; - // Check if all the highlighters were enabled - if (highlighterTypes.every(type => highlighters.includes(type))) { - // Disable the highlighters - highlighters = highlighters.filter( - type => !highlighterTypes.includes(type) - ); - } else { - // Enable the highlighters - highlighters = [...highlighters, ...highlighterTypes]; - } - // Instruct the backend to toggle the highlighters on/off - await targetConfigurationCommand.updateConfiguration({ - enabledHighlighters: highlighters, - }); - } else { - const inspectorFront = await toolbox.target.getFront("inspector"); - await Promise.all( - highlighterTypes.map(async name => { - const highlighter = - await inspectorFront.getOrCreateHighlighterByType(name); - - if (highlighter.isShown()) { - await highlighter.hide(); - } else { - await highlighter.show(); - } - }) - ); - } - }, - isChecked(toolbox) { const { targetConfigurationCommand } = toolbox.commands; const { configuration } = targetConfigurationCommand; - // Note that we cannot query targetConfigurationCommand.supports as this is an async function, - // so fallback on looking if the enabledHighlighters configuration key exists - // - // @backward-compat { version 154 } Firefox 154 started supporting toggling - // global highlighters via Target Actor Configuration. The else branch can be later removed. - if ("enabledHighlighters" in configuration) { - const highlighters = configuration.enabledHighlighters || []; - const isChecked = highlighterTypes.every(type => - highlighters.includes(type) + let highlighters = configuration.enabledHighlighters || []; + // Check if all the highlighters were enabled + if (highlighterTypes.every(type => highlighters.includes(type))) { + // Disable the highlighters + highlighters = highlighters.filter( + type => !highlighterTypes.includes(type) ); - return isChecked; + } else { + // Enable the highlighters + highlighters = [...highlighters, ...highlighterTypes]; } - // if the inspector doesn't exist, then the highlighter has not yet been connected - // to the front end. - const inspectorFront = toolbox.target.getCachedFront("inspector"); - if (!inspectorFront) { - // initialize the inspector front asyncronously. There is a potential for buggy - // behavior here, but we need to change how the buttons get data (have them - // consume data from reducers rather than writing our own version) in order to - // fix this properly. - return false; - } - - return highlighterTypes.every(name => - inspectorFront.getKnownHighlighter(name)?.isShown() - ); + // Instruct the backend to toggle the highlighters on/off + await targetConfigurationCommand.updateConfiguration({ + enabledHighlighters: highlighters, + }); + }, + isChecked(toolbox) { + const { configuration } = toolbox.commands.targetConfigurationCommand; + const highlighters = configuration.enabledHighlighters || []; + return highlighterTypes.every(type => highlighters.includes(type)); }, isToggle: true, }; diff --git a/devtools/client/framework/test/browser_toolbox_rulers_button_highlighter_reload.js b/devtools/client/framework/test/browser_toolbox_rulers_button_highlighter_reload.js index 53804a0b521f..83a4044d9b05 100644 --- a/devtools/client/framework/test/browser_toolbox_rulers_button_highlighter_reload.js +++ b/devtools/client/framework/test/browser_toolbox_rulers_button_highlighter_reload.js @@ -122,7 +122,7 @@ add_task(async function testToolboxDestroy() { await waitFor(async () => { if ( (await isRulersHighlighterVisible()) && - (await isRulersHighlighterVisible()) + (await isViewportSizeHighlighterVisible()) ) { return true; } @@ -149,6 +149,62 @@ add_task(async function testToolboxDestroy() { ); }); +/** + * Bug 2063982. Check that disabling the ruler highlighter destroys the + * highlighter whether the inspector was started or not. + */ +add_task(async function testRulerDisabled() { + for (const toolId of ["inspector", "webconsole"]) { + await pushPref("devtools.command-button-rulers.enabled", true); + const tab = await addTab(TEST_URL); + const toolbox = await gDevTools.showToolboxForTab(tab, { toolId }); + + // Sanity check + is( + await isRulersHighlighterVisible(), + false, + "Rulers highlighter is not shown at first" + ); + is( + await isViewportSizeHighlighterVisible(), + false, + "ViewportSize highlighter is not shown at first" + ); + + info("Show the rulers"); + await clickRulersButton(toolbox, true); + + info("Wait for the rulers highlighters to be visible"); + await waitFor(async () => { + if ( + (await isRulersHighlighterVisible()) && + (await isViewportSizeHighlighterVisible()) + ) { + return true; + } + return false; + }, "Ruler highlighters are both visible"); + + // Go to the options panel and disable the highlighter + const { panelDoc } = await toolbox.selectTool("options"); + const cbx = panelDoc.getElementById("command-button-rulers"); + cbx.click(); + + info("Wait for the rulers highlighters to be hidden"); + await waitFor(async () => { + if ( + (await isRulersHighlighterVisible()) || + (await isViewportSizeHighlighterVisible()) + ) { + return false; + } + return true; + }, "Ruler highlighters are both hidden"); + + await toolbox.destroy(); + } +}); + function getRulersButton(toolbox) { return toolbox.doc.querySelector("#command-button-rulers"); } diff --git a/devtools/client/framework/toolbox.js b/devtools/client/framework/toolbox.js index 4d12bbca0da8..bf1800b51f4b 100644 --- a/devtools/client/framework/toolbox.js +++ b/devtools/client/framework/toolbox.js @@ -2527,19 +2527,12 @@ class Toolbox extends EventEmitter { * page is going to navigate */ updateToolboxButtonsVisibility({ fromWillNavigate = false } = {}) { - const inspectorFront = this.target.getCachedFront("inspector"); - let toggledHighlighters = false; for (const button of this.toolbarButtons) { button.isVisible = this.#commandIsVisible(button); // We want to hide highlighters when the toolbox button is disabled from the options panel - if ( - inspectorFront && - button.highlighterTypes && - !button.isVisible && - button.isChecked - ) { + if (button.highlighterTypes && !button.isVisible && button.isChecked) { button.onClick({}); toggledHighlighters = true; } diff --git a/devtools/client/fronts/inspector.js b/devtools/client/fronts/inspector.js index 1b68d546fad1..05bb6e1b9fc9 100644 --- a/devtools/client/fronts/inspector.js +++ b/devtools/client/fronts/inspector.js @@ -64,20 +64,6 @@ class InspectorFront extends FrontClassWithSpec(inspectorSpec) { onAvailable: this.noopStylesheetListener, }); - // @backward-compat { version 154 } Firefox 154 started supporting toggling - // global highlighters via Target Actor Configuration. We no longer need to watch - // for will-navigate to clear frontend highlighters and can remove this code. - const { configuration } = - this.targetFront.commands.targetConfigurationCommand; - if ("enabledHighlighters" in configuration) { - await resourceCommand.watchResources( - [resourceCommand.TYPES.DOCUMENT_EVENT], - { - onAvailable: this.#documentEventListener, - } - ); - } - // Bail out if the inspector is closed while watchResources was pending if (this.isDestroyed()) { return null; @@ -109,26 +95,6 @@ class InspectorFront extends FrontClassWithSpec(inspectorSpec) { await this.walker.reparentRemoteFrame(); } - // @backward-compat { version 154 } Firefox 154 started supporting toggling - // global highlighters via Target Actor Configuration. We no longer need to watch - // for will-navigate to clear frontend highlighters and can remove this code. - #documentEventListener = resources => { - const willNavigate = resources.some( - resource => - resource.name == "will-navigate" && resource.targetFront.isTopLevel - ); - if (!willNavigate) { - return; - } - // Manually clear the highlighters on the frontend, to replicate what happens - // on the backend and avoid keeping defunct enabled highlighters - this._highlighters.clear(); - }; - - hasHighlighter(type) { - return this._highlighters.has(type); - } - async _getPageStyle() { this.pageStyle = await super.getPageStyle(); } @@ -158,9 +124,6 @@ class InspectorFront extends FrontClassWithSpec(inspectorSpec) { resourceCommand.unwatchResources([resourceCommand.TYPES.STYLESHEET], { onAvailable: this.noopStylesheetListener, }); - resourceCommand.unwatchResources([resourceCommand.TYPES.DOCUMENT_EVENT], { - onAvailable: this.#documentEventListener, - }); this.resourceCommand = null; this.walker = null; diff --git a/devtools/client/inspector/compatibility/test/browser/browser.toml b/devtools/client/inspector/compatibility/test/browser/browser.toml index 761ec357e5c0..d6198e111d10 100644 --- a/devtools/client/inspector/compatibility/test/browser/browser.toml +++ b/devtools/client/inspector/compatibility/test/browser/browser.toml @@ -1,5 +1,5 @@ [DEFAULT] -tags = "devtools devtools-compat-data" +tags = "devtools" subsuite = "devtools" support-files = [ "head.js", @@ -31,6 +31,9 @@ support-files = [ ["browser_compatibility_issue-node.js"] +["browser_compatibility_live-data.js"] +tags = "devtools devtools-compat-data" + ["browser_compatibility_settings.js"] ["browser_compatibility_throbber.js"] diff --git a/devtools/client/inspector/compatibility/test/browser/browser_compatibility_css-property_issue.js b/devtools/client/inspector/compatibility/test/browser/browser_compatibility_css-property_issue.js index b141c4e851ec..73d8e2eaba6d 100644 --- a/devtools/client/inspector/compatibility/test/browser/browser_compatibility_css-property_issue.js +++ b/devtools/client/inspector/compatibility/test/browser/browser_compatibility_css-property_issue.js @@ -16,6 +16,7 @@ const TEST_URI = ` text-box-edge: text; user-modify: read-only; stroke-color: red; + -moz-orient: horizontal; } div { overflow-anchor: auto; @@ -50,7 +51,13 @@ const TEST_DATA_SELECTED = [ deprecated: false, experimental: true, }, - // TODO: Write a test for it when we have a property with no MDN url nor spec url Bug 1840910 + { + type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY, + property: "-moz-orient", + // Neither MDN url nor spec url, so the property is not rendered as a link + deprecated: false, + experimental: false, + }, ]; const TEST_DATA_ALL = [ @@ -67,14 +74,11 @@ const TEST_DATA_ALL = [ add_task(async function () { await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + // This test relies on the mock dataset, see + // devtools/shared/compatibility/dataset/mock-css-properties.json const { allElementsPane, selectedElementPane } = await openCompatibilityView(); - // If the test fail because the properties used are no longer in the dataset, or they - // now have mdn/spec url although we expected them not to, uncomment the next line - // to get all the properties in the dataset that don't have a MDN url. - // logCssCompatDataPropertiesWithoutMDNUrl() - info("Check the content of the issue list on the selected element"); await assertIssueList(selectedElementPane, TEST_DATA_SELECTED); diff --git a/devtools/client/inspector/compatibility/test/browser/browser_compatibility_live-data.js b/devtools/client/inspector/compatibility/test/browser/browser_compatibility_live-data.js new file mode 100644 index 000000000000..d9cc6e5de2cc --- /dev/null +++ b/devtools/client/inspector/compatibility/test/browser/browser_compatibility_live-data.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Smoke test making sure the compatibility panel still reports issues when using the live +// MDN compatibility data. All the other tests of this folder run against a mock dataset, +// see devtools/shared/compatibility/dataset/mock-css-properties.json. +// Assertions here should stay loose enough to survive any MDN compatibility data update. + +const { + updateTargetBrowsers, +} = require("resource://devtools/client/inspector/compatibility/actions/compatibility.js"); + +// Targeting the very first version of Firefox, so that any property which is not as old +// as the browser itself is reported as unsupported. +const TARGET_BROWSERS = [{ id: "firefox", name: "Firefox", version: "1" }]; + +// `aspect-ratio` is deliberately not part of the mock dataset, so it can only be reported +// as an issue when the live dataset is used. +const PROPERTY = "aspect-ratio"; + +const TEST_URI = ` + + +`; + +add_task(async function () { + await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); + + const { inspector, selectedElementPane } = await openCompatibilityView({ + mockDataset: false, + }); + + info("Update the target browsers for this test"); + await inspector.store.dispatch(updateTargetBrowsers(TARGET_BROWSERS)); + + info(`Wait for the issue about ${PROPERTY} to be displayed`); + const issueEl = await waitFor( + () => getIssueItem(PROPERTY, selectedElementPane), + `The issue for ${PROPERTY} is displayed` + ); + + const unsupportedBrowsers = JSON.parse(issueEl.dataset.qaUnsupportedBrowsers); + Assert.greater( + unsupportedBrowsers.length, + 0, + `The issue for ${PROPERTY} lists unsupported browsers` + ); + + const url = JSON.parse(issueEl.dataset.qaUrl); + ok(url.startsWith("https://"), `The issue for ${PROPERTY} has a url: ${url}`); +}); diff --git a/devtools/client/inspector/compatibility/test/browser/head.js b/devtools/client/inspector/compatibility/test/browser/head.js index 18efba1a9e0e..849a021a39d7 100644 --- a/devtools/client/inspector/compatibility/test/browser/head.js +++ b/devtools/client/inspector/compatibility/test/browser/head.js @@ -18,7 +18,19 @@ const { toCamelCase, } = require("resource://devtools/client/inspector/compatibility/utils/cases.js"); -async function openCompatibilityView() { +/** + * Open the compatibility view. + * + * @param {object} options + * @param {boolean} options.mockDataset + * Set to false to run against the live MDN compatibility data instead of the mock + * dataset. Only meant for the smoke test checking the live data. + */ +async function openCompatibilityView({ mockDataset = true } = {}) { + if (mockDataset) { + await setMockCompatibilityDataset(); + } + info("Open the compatibility view"); const { inspector } = await openInspectorSidebarTab("compatibilityview"); await Promise.all([ @@ -126,7 +138,7 @@ async function assertIssueList(panel, expectedIssues) { ) .join("\n"), }), - "The brower item has the expected title attribute" + "The browser item has the expected title attribute" ); } } @@ -190,9 +202,6 @@ async function assertIssueList(panel, expectedIssues) { "span", `No link rendered for ${property}` ); - - const { link } = await simulateLinkClick(propertyEl); - is(link, null, `Click on ${property} does not navigate`); } } } diff --git a/devtools/client/inspector/rules/test/browser_part2.toml b/devtools/client/inspector/rules/test/browser_part2.toml index 85dd641a9e82..f48cee2f7df8 100644 --- a/devtools/client/inspector/rules/test/browser_part2.toml +++ b/devtools/client/inspector/rules/test/browser_part2.toml @@ -57,19 +57,14 @@ support-files = [ ] ["browser_rules_css-compatibility-add-rename-rule.js"] -tags = "devtools-compat-data" ["browser_rules_css-compatibility-check-add-fix.js"] -tags = "devtools-compat-data" ["browser_rules_css-compatibility-learn-more-link.js"] -tags = "devtools-compat-data" ["browser_rules_css-compatibility-toggle-rules.js"] -tags = "devtools-compat-data" ["browser_rules_css-compatibility-tooltip-telemetry.js"] -tags = "devtools-compat-data" ["browser_rules_element_specific_pseudo_classes.js"] diff --git a/devtools/client/inspector/rules/test/browser_rules_css-compatibility-add-rename-rule.js b/devtools/client/inspector/rules/test/browser_rules_css-compatibility-add-rename-rule.js index e6706cd0cfe9..5767e019f497 100644 --- a/devtools/client/inspector/rules/test/browser_rules_css-compatibility-add-rename-rule.js +++ b/devtools/client/inspector/rules/test/browser_rules_css-compatibility-add-rename-rule.js @@ -99,6 +99,7 @@ const TEST_DATA_RENAME_RULE = [ ]; add_task(async function () { + await setMockCompatibilityDataset(); await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); const { inspector, view } = await openRuleView(); diff --git a/devtools/client/inspector/rules/test/browser_rules_css-compatibility-check-add-fix.js b/devtools/client/inspector/rules/test/browser_rules_css-compatibility-check-add-fix.js index fe04f416f87e..9e9637ddad10 100644 --- a/devtools/client/inspector/rules/test/browser_rules_css-compatibility-check-add-fix.js +++ b/devtools/client/inspector/rules/test/browser_rules_css-compatibility-check-add-fix.js @@ -97,6 +97,7 @@ const TEST_DATA_FIX_EXPERIMENTAL_SUPPORTED = [ ]; add_task(async function () { + await setMockCompatibilityDataset(); await pushPref( "devtools.inspector.compatibility.target-browsers", JSON.stringify(TARGET_BROWSERS) diff --git a/devtools/client/inspector/rules/test/browser_rules_css-compatibility-learn-more-link.js b/devtools/client/inspector/rules/test/browser_rules_css-compatibility-learn-more-link.js index 721c7be87813..a4c8edd1dcf7 100644 --- a/devtools/client/inspector/rules/test/browser_rules_css-compatibility-learn-more-link.js +++ b/devtools/client/inspector/rules/test/browser_rules_css-compatibility-learn-more-link.js @@ -11,6 +11,7 @@ const TEST_URI = ` body { user-select: none; stroke-color: red; + -moz-orient: horizontal; } @@ -36,20 +37,23 @@ const TEST_DATA_INITIAL = [ expectedLearnMoreUrl: "https://drafts.csswg.org/fill-stroke-3/#stroke-color", }, - // TODO: Add a test for it when we have another property with no MDN url nor spec url Bug 1840910 + "-moz-orient": { + value: "horizontal", + expected: COMPATIBILITY_TOOLTIP_MESSAGE.default, + // Neither MDN url nor spec url, so there is no link at all + expectedLearnMoreUrl: null, + }, }, ], }, ]; add_task(async function () { + // This test relies on the mock dataset, see + // devtools/shared/compatibility/dataset/mock-css-properties.json + await setMockCompatibilityDataset(); await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); const { inspector, view } = await openRuleView(); - // If the test fail because the properties used are no longer in the dataset, or they - // now have mdn/spec url although we expected them not to, uncomment the next line - // to get all the properties in the dataset that don't have a MDN url. - // logCssCompatDataPropertiesWithoutMDNUrl() - await runCSSCompatibilityTests(view, inspector, TEST_DATA_INITIAL); }); diff --git a/devtools/client/inspector/rules/test/browser_rules_css-compatibility-toggle-rules.js b/devtools/client/inspector/rules/test/browser_rules_css-compatibility-toggle-rules.js index c836755be9a8..de0837ed7b80 100644 --- a/devtools/client/inspector/rules/test/browser_rules_css-compatibility-toggle-rules.js +++ b/devtools/client/inspector/rules/test/browser_rules_css-compatibility-toggle-rules.js @@ -118,6 +118,7 @@ const TEST_DATA_TOGGLE_INLINE = [ ]; add_task(async function () { + await setMockCompatibilityDataset(); await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); const { inspector, view } = await openRuleView(); diff --git a/devtools/client/inspector/rules/test/browser_rules_css-compatibility-tooltip-telemetry.js b/devtools/client/inspector/rules/test/browser_rules_css-compatibility-tooltip-telemetry.js index 7509634225d8..d3bc178ffff6 100644 --- a/devtools/client/inspector/rules/test/browser_rules_css-compatibility-tooltip-telemetry.js +++ b/devtools/client/inspector/rules/test/browser_rules_css-compatibility-tooltip-telemetry.js @@ -34,6 +34,7 @@ const TEST_DATA = [ ]; add_task(async function () { + await setMockCompatibilityDataset(); await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI)); Services.fog.testResetFOG(); const { inspector, view } = await openRuleView(); diff --git a/devtools/client/inspector/test/highlighter/browser_inspector_highlighter-rulers_03.js b/devtools/client/inspector/test/highlighter/browser_inspector_highlighter-rulers_03.js index 1d95022a9322..329bc4bb5422 100644 --- a/devtools/client/inspector/test/highlighter/browser_inspector_highlighter-rulers_03.js +++ b/devtools/client/inspector/test/highlighter/browser_inspector_highlighter-rulers_03.js @@ -81,8 +81,8 @@ async function hasRightLabelsContent(highlighterFront, highlighterTestFront) { return getWindowDimensions(content); } ); - const windowHeight = Math.round(windowDimensions.height); - const windowWidth = Math.round(windowDimensions.width); + const windowHeight = windowDimensions.height.toFixed(1); + const windowWidth = windowDimensions.width.toFixed(1); const windowText = windowWidth + "px \u00D7 " + windowHeight + "px"; info("Wait until the rulers dimension tooltip have the proper text"); diff --git a/devtools/client/inspector/test/highlighter/browser_inspector_highlighter-viewport-size.js b/devtools/client/inspector/test/highlighter/browser_inspector_highlighter-viewport-size.js index 125c2476be5f..e7bdd0159a67 100644 --- a/devtools/client/inspector/test/highlighter/browser_inspector_highlighter-viewport-size.js +++ b/devtools/client/inspector/test/highlighter/browser_inspector_highlighter-viewport-size.js @@ -155,8 +155,8 @@ async function hasRightLabelsContent(highlighterFront, highlighterTestFront) { return getWindowDimensions(content); } ); - const windowHeight = Math.round(windowDimensions.height); - const windowWidth = Math.round(windowDimensions.width); + const windowHeight = windowDimensions.height.toFixed(1); + const windowWidth = windowDimensions.width.toFixed(1); const windowText = `${windowWidth}px \u00D7 ${windowHeight}px`; const dimensionText = diff --git a/devtools/client/shared/test/shared-head.js b/devtools/client/shared/test/shared-head.js index 24d69a27e1e3..8d902f862815 100644 --- a/devtools/client/shared/test/shared-head.js +++ b/devtools/client/shared/test/shared-head.js @@ -2367,31 +2367,11 @@ function simulateLinkClick(element) { } /** - * Since the MDN data is updated frequently, it might happen that the properties used in - * this test are not in the dataset anymore/now have URLs. - * This function will return properties in the dataset that don't have MDN url so you - * can easily find a replacement. + * Use mocked MDN compat data. Should be called before the toolbox starts. + * See devtools/shared/compatibility/dataset/mock-css-properties.json. */ -function logCssCompatDataPropertiesWithoutMDNUrl() { - const cssPropertiesCompatData = require("resource://devtools/shared/compatibility/dataset/css-properties.json"); - - function walk(node) { - for (const propertyName in node) { - const property = node[propertyName]; - if (property.__compat) { - if (!property.__compat.mdn_url) { - dump( - `"${propertyName}" - MDN URL: ${ - property.__compat.mdn_url || "❌" - } - Spec URL: ${property.__compat.spec_url || "❌"}\n` - ); - } - } else if (typeof property == "object") { - walk(property); - } - } - } - walk(cssPropertiesCompatData); +async function setMockCompatibilityDataset() { + await pushPref("devtools.compatibility.use-mock-dataset", true); } /** diff --git a/devtools/server/actors/compatibility/compatibility.js b/devtools/server/actors/compatibility/compatibility.js index 86c6a3472be5..b73ac52e0886 100644 --- a/devtools/server/actors/compatibility/compatibility.js +++ b/devtools/server/actors/compatibility/compatibility.js @@ -9,11 +9,10 @@ const { compatibilitySpec, } = require("resource://devtools/shared/specs/compatibility.js"); -loader.lazyGetter(this, "mdnCompatibility", () => { - const MDNCompatibility = require("resource://devtools/server/actors/compatibility/lib/MDNCompatibility.js"); - const cssPropertiesCompatData = require("resource://devtools/shared/compatibility/dataset/css-properties.json"); - return new MDNCompatibility(cssPropertiesCompatData); -}); +const MDNCompatibility = require("resource://devtools/server/actors/compatibility/lib/MDNCompatibility.js"); +const { + getCSSPropertiesCompatData, +} = require("resource://devtools/shared/compatibility/compatibility-dataset.js"); class CompatibilityActor extends Actor { /** @@ -35,11 +34,16 @@ class CompatibilityActor extends Actor { constructor(inspector) { super(inspector.conn, compatibilitySpec); this.inspector = inspector; + // Note: getCSSPropertiesCompatData() will either pick the real or mocked + // MDN dataset, so it should not be persisted beyond the lifetime of the + // actor. + this.mdnCompatibility = new MDNCompatibility(getCSSPropertiesCompatData()); } destroy() { super.destroy(); this.inspector = null; + this.mdnCompatibility = null; } form() { @@ -84,7 +88,7 @@ class CompatibilityActor extends Actor { */ getCSSDeclarationBlockIssues(domRulesDeclarations, targetBrowsers) { return domRulesDeclarations.map(declarationBlock => - mdnCompatibility.getCSSDeclarationBlockIssues( + this.mdnCompatibility.getCSSDeclarationBlockIssues( declarationBlock, targetBrowsers ) @@ -146,7 +150,7 @@ class CompatibilityActor extends Actor { } } - return mdnCompatibility.getCSSDeclarationBlockIssues( + return this.mdnCompatibility.getCSSDeclarationBlockIssues( declarations, targetBrowsers ); diff --git a/devtools/server/actors/compatibility/lib/test/xpcshell/test_mdn-compatibility-live-data.js b/devtools/server/actors/compatibility/lib/test/xpcshell/test_mdn-compatibility-live-data.js new file mode 100644 index 000000000000..1ac39e0b024e --- /dev/null +++ b/devtools/server/actors/compatibility/lib/test/xpcshell/test_mdn-compatibility-live-data.js @@ -0,0 +1,143 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Smoke test for the real MDN compatibility dataset. The behavior of MDNCompatibility is +// covered by test_mdn-compatibility.js, which uses a mock dataset. Here we only check that +// the vendored dataset can still be loaded and used, without asserting on any value which +// could legitimately change when the data is updated. + +const { + TARGET_BROWSER_ID, +} = require("resource://devtools/shared/compatibility/constants.js"); +const { + getCSSPropertiesCompatData, +} = require("resource://devtools/shared/compatibility/compatibility-dataset.js"); +const MDNCompatibility = require("resource://devtools/server/actors/compatibility/lib/MDNCompatibility.js"); + +let cssPropertiesCompatData; + +add_setup(() => { + // Make sure we use the live dataset, even if it should be the default. + Services.prefs.setBoolPref("devtools.compatibility.use-mock-dataset", false); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("devtools.compatibility.use-mock-dataset"); + }); + cssPropertiesCompatData = getCSSPropertiesCompatData(); +}); + +// A dataset smaller than this would mean the update script dropped most of the data. +const MINIMUM_PROPERTIES_COUNT = 100; + +add_task(function test_dataset_shape() { + const properties = Object.keys(cssPropertiesCompatData); + Assert.greater( + properties.length, + MINIMUM_PROPERTIES_COUNT, + "The dataset contains a plausible number of properties" + ); + + info("Check the shape of every compat table in the dataset"); + let checkedSupportEntries = 0; + for (const property of properties) { + const compatTable = getCompatTable(cssPropertiesCompatData, property); + if (!compatTable) { + // Aliases only hold a `_aliasOf` property. + Assert.equal( + typeof cssPropertiesCompatData[property]._aliasOf, + "string", + `"${property}" has no compat table and is an alias` + ); + continue; + } + + for (const [browserId, supportEntries] of Object.entries( + compatTable.support + )) { + Assert.ok( + TARGET_BROWSER_ID.includes(browserId), + `"${property}" only has support data for target browsers, got "${browserId}"` + ); + Assert.ok( + Array.isArray(supportEntries), + `The support data of "${property}" for "${browserId}" is an array` + ); + + for (const { added } of supportEntries) { + // `added` is set by the update script from `version_added`, which can either be a + // version number, a boolean, or null when the data is unknown. + Assert.ok( + typeof added === "number" || + typeof added === "boolean" || + added === null || + added === undefined, + `The parsed version of "${property}" for "${browserId}" is usable, got "${added}"` + ); + checkedSupportEntries++; + } + } + } + + Assert.greater(checkedSupportEntries, 0, "Support entries were checked"); +}); + +add_task(function test_issues_shape() { + const mdnCompatibility = new MDNCompatibility(cssPropertiesCompatData); + const browsers = TARGET_BROWSER_ID.map(id => ({ id, version: "1" })); + + // Targeting version 1 of every browser, so that any modern property is unsupported and + // reported as an issue whatever the data says. + const declarations = [ + { name: "background-color" }, + { name: "border-block-color" }, + { name: "user-select" }, + { name: "--custom-property" }, + { name: "this-property-does-not-exist" }, + ]; + + const issues = mdnCompatibility.getCSSDeclarationBlockIssues( + declarations, + browsers + ); + Assert.greater(issues.length, 0, "Issues were reported"); + + for (const issue of issues) { + Assert.ok(!!issue.type, `The issue for "${issue.property}" has a type`); + Assert.ok( + declarations.some( + ({ name }) => name === issue.property || issue.aliases?.includes(name) + ), + `The issue for "${issue.property}" matches one of the declarations` + ); + Assert.equal( + typeof issue.deprecated, + "boolean", + `The issue for "${issue.property}" has a deprecated flag` + ); + Assert.equal( + typeof issue.experimental, + "boolean", + `The issue for "${issue.property}" has an experimental flag` + ); + Assert.ok( + Array.isArray(issue.unsupportedBrowsers), + `The issue for "${issue.property}" has a list of unsupported browsers` + ); + } +}); + +function getCompatTable(dataset, property) { + const node = dataset[property]; + if (node.__compat) { + return node.__compat; + } + + // Some properties store their compat data in a context node, e.g. `flex_context`. + for (const field in node) { + if (field.endsWith("_context")) { + return node[field].__compat; + } + } + + return null; +} diff --git a/devtools/server/actors/compatibility/lib/test/xpcshell/test_mdn-compatibility.js b/devtools/server/actors/compatibility/lib/test/xpcshell/test_mdn-compatibility.js index e1947def2266..ea969ea114c8 100644 --- a/devtools/server/actors/compatibility/lib/test/xpcshell/test_mdn-compatibility.js +++ b/devtools/server/actors/compatibility/lib/test/xpcshell/test_mdn-compatibility.js @@ -3,14 +3,17 @@ "use strict"; // Test for the MDN compatibility diagnosis module. +// This runs against the mock dataset so that it is not impacted by MDN compatibility +// data updates. See test_mdn-compatibility-live-data.js for the smoke test running +// against the real dataset. const { COMPATIBILITY_ISSUE_TYPE, } = require("resource://devtools/shared/constants.js"); const MDNCompatibility = require("resource://devtools/server/actors/compatibility/lib/MDNCompatibility.js"); -const cssPropertiesCompatData = require("resource://devtools/shared/compatibility/dataset/css-properties.json"); - -const mdnCompatibility = new MDNCompatibility(cssPropertiesCompatData); +const { + getCSSPropertiesCompatData, +} = require("resource://devtools/shared/compatibility/compatibility-dataset.js"); const FIREFOX_1 = { id: "firefox", @@ -27,6 +30,12 @@ const FIREFOX_69 = { version: "69", }; +// Above the version in which -moz-user-focus was removed. +const FIREFOX_130 = { + id: "firefox", + version: "130", +}; + const FIREFOX_ANDROID_1 = { id: "firefox_android", version: "1", @@ -104,6 +113,26 @@ const TEST_DATA = [ }, ], }, + { + description: + "Test for a property whose support is unknown in one of the browsers. Unknown " + + "support is considered as supported, to avoid reporting issues we are not sure about", + declarations: [{ name: "animation-timeline" }], + browsers: [FIREFOX_69, FIREFOX_ANDROID_1], + expectedIssues: [ + { + type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY, + property: "animation-timeline", + url: "https://developer.mozilla.org/docs/Web/CSS/Reference/Properties/animation-timeline", + specUrl: + "https://drafts.csswg.org/css-animations-2/#animation-timeline", + deprecated: false, + experimental: false, + // Firefox is not listed, although the dataset has no version for it. + unsupportedBrowsers: [FIREFOX_ANDROID_1], + }, + ], + }, { description: "Test for an aliased property not supported in all browsers with prefix needed", @@ -155,7 +184,8 @@ const TEST_DATA = [ expectedIssues: [], }, { - description: "Test for a property defined with prefix", + description: + "Test for a property defined with prefix, on versions before it was removed", declarations: [{ name: "-moz-user-focus" }], browsers: [FIREFOX_1, FIREFOX_60, FIREFOX_69], expectedIssues: [ @@ -170,9 +200,48 @@ const TEST_DATA = [ }, ], }, + { + description: "Test for a property which was removed", + declarations: [{ name: "-moz-user-focus" }], + browsers: [FIREFOX_69, FIREFOX_130], + expectedIssues: [ + { + type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY, + property: "-moz-user-focus", + url: "https://developer.mozilla.org/docs/Web/CSS/Reference/Properties/-moz-user-focus", + specUrl: undefined, + deprecated: true, + experimental: false, + unsupportedBrowsers: [FIREFOX_130], + }, + ], + }, + { + description: "Test for an experimental property with no MDN url", + declarations: [{ name: "stroke-color" }], + browsers: [FIREFOX_69], + expectedIssues: [ + { + type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY, + property: "stroke-color", + url: undefined, + specUrl: "https://drafts.csswg.org/fill-stroke-3/#stroke-color", + deprecated: false, + experimental: true, + unsupportedBrowsers: [FIREFOX_69], + }, + ], + }, ]; add_task(() => { + Services.prefs.setBoolPref("devtools.compatibility.use-mock-dataset", true); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("devtools.compatibility.use-mock-dataset"); + }); + + const mdnCompatibility = new MDNCompatibility(getCSSPropertiesCompatData()); + for (const { description, declarations, diff --git a/devtools/server/actors/compatibility/lib/test/xpcshell/xpcshell.toml b/devtools/server/actors/compatibility/lib/test/xpcshell/xpcshell.toml index 009f1612a766..eb70a79db051 100644 --- a/devtools/server/actors/compatibility/lib/test/xpcshell/xpcshell.toml +++ b/devtools/server/actors/compatibility/lib/test/xpcshell/xpcshell.toml @@ -1,6 +1,9 @@ [DEFAULT] -tags = "devtools devtools-compat-data" +tags = "devtools" head = "head.js" firefox-appdir = "browser" +["test_mdn-compatibility-live-data.js"] +tags = "devtools devtools-compat-data" + ["test_mdn-compatibility.js"] diff --git a/devtools/server/actors/highlighters/viewport-size.js b/devtools/server/actors/highlighters/viewport-size.js index 6f88fc740c75..f17fcb46fcbe 100644 --- a/devtools/server/actors/highlighters/viewport-size.js +++ b/devtools/server/actors/highlighters/viewport-size.js @@ -107,7 +107,12 @@ class ViewportSizeHighlighter { const { window } = this.env; const { innerHeight, innerWidth } = window; const infobarId = "viewport-size-highlighter-viewport-infobar-container"; - const textContent = innerWidth + "px \u00D7 " + innerHeight + "px"; + // We're getting un-rounded inner(Height|Width), but here 1 decimal should be enough. + // Note: we're not using Intl.NumberFormat with maximumFractionDigits as the size + // "strings" could have different length while resizing the window, which will make + // the highlighter look very jittery. + const textContent = + innerWidth.toFixed(1) + "px \u00D7 " + innerHeight.toFixed(1) + "px"; this.markup.getElement(infobarId).setTextContent(textContent); } diff --git a/devtools/server/tests/browser/browser.toml b/devtools/server/tests/browser/browser.toml index c19bd3656b8a..8b0264ee3d41 100644 --- a/devtools/server/tests/browser/browser.toml +++ b/devtools/server/tests/browser/browser.toml @@ -130,7 +130,6 @@ skip-if = [ ["browser_canvasframe_helper_06.js"] ["browser_compatibility_cssIssues.js"] -tags = "devtools-compat-data" ["browser_connectToFrame.js"] diff --git a/devtools/server/tests/browser/browser_compatibility_cssIssues.js b/devtools/server/tests/browser/browser_compatibility_cssIssues.js index 922565404f5b..4a15b990b161 100644 --- a/devtools/server/tests/browser/browser_compatibility_cssIssues.js +++ b/devtools/server/tests/browser/browser_compatibility_cssIssues.js @@ -110,6 +110,8 @@ async function testNodeCssIssues(selector, walker, compatibility, expected) { } add_task(async function () { + await setMockCompatibilityDataset(); + const { inspector, walker, target } = await initInspectorFront(URL); const compatibility = await inspector.getCompatibilityFront(); diff --git a/devtools/shared/compatibility/README.md b/devtools/shared/compatibility/README.md index 946cb0259117..203a8593c0dd 100644 --- a/devtools/shared/compatibility/README.md +++ b/devtools/shared/compatibility/README.md @@ -21,6 +21,18 @@ Before submitting for review, run our internal tests: - `./mach xpcshell-test --tag devtools-compat-data` - `./mach mochitest --subsuite devtools --tag devtools-compat-data` +## Tests and the mock dataset + +Only the tests tagged `devtools-compat-data` use this dataset, and they avoid asserting on +values which can change, so a failure there means the data or its structure changed. + +All the other tests use `dataset/mock-css-properties.json`, which has real CSS property names +but mocked compatibility data. To opt in, call `setMockCompatibilityDataset()` (from +`devtools/client/shared/test/shared-head.js`) before opening the toolbox. If you +need an issue the mock dataset doesn't cover, add the property to it. Try to stay +as close as possible to the real data, and avoid changing anything but the values +ot the compatibility properties. + ## Reviewing browser data updates (RemoteSettings) The browsers data are stored in a RemoteSettings collection, and updates are handled by a script in https://github.com/firefox-devtools/remote-settings-mdn-browser-compat-data . diff --git a/devtools/shared/compatibility/compatibility-dataset.js b/devtools/shared/compatibility/compatibility-dataset.js new file mode 100644 index 000000000000..5df676d61194 --- /dev/null +++ b/devtools/shared/compatibility/compatibility-dataset.js @@ -0,0 +1,32 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const CSS_PROPERTIES_DATASET_URL = + "resource://devtools/shared/compatibility/dataset/css-properties.json"; +const MOCK_CSS_PROPERTIES_DATASET_URL = + "resource://testing-common/devtools/compatibility/mock-css-properties.json"; + +// Preference to enable using the mocked dataset. Defaults to false. +const MOCK_DATASET_PREF = "devtools.compatibility.use-mock-dataset"; + +/** + * Return the MDN compatibility data for CSS properties, either local snapshot + * of https://github.com/mdn/browser-compat-data, unless a test opted into the + * mock dataset via devtools.compatibility.use-mock-dataset=true. + * + * @returns {object} The compat data, keyed by CSS property name. + */ +function getCSSPropertiesCompatData() { + return require( + Services.prefs.getBoolPref(MOCK_DATASET_PREF, false) + ? MOCK_CSS_PROPERTIES_DATASET_URL + : CSS_PROPERTIES_DATASET_URL + ); +} + +module.exports = { + getCSSPropertiesCompatData, +}; diff --git a/devtools/shared/compatibility/dataset/mock-css-properties.json b/devtools/shared/compatibility/dataset/mock-css-properties.json new file mode 100644 index 000000000000..6dda497345a0 --- /dev/null +++ b/devtools/shared/compatibility/dataset/mock-css-properties.json @@ -0,0 +1,431 @@ +{ + "background-color": { + "__compat": { + "mdn_url": "https://developer.mozilla.org/docs/Web/CSS/Reference/Properties/background-color", + "spec_url": "https://drafts.csswg.org/css-backgrounds/#background-color", + "status": { + "deprecated": false, + "experimental": false, + "standard_track": true + }, + "support": { + "chrome": [{ "added": 1 }], + "chrome_android": [{ "added": 18 }], + "edge": [{ "added": 12 }], + "firefox": [{ "added": 1 }], + "firefox_android": [{ "added": 4 }], + "ie": [{ "added": 4 }], + "safari": [{ "added": 1 }], + "safari_ios": [{ "added": 1 }] + }, + "tags": ["web-features:background-color"] + } + }, + "color": { + "__compat": { + "mdn_url": "https://developer.mozilla.org/docs/Web/CSS/Reference/Properties/color", + "spec_url": [ + "https://drafts.csswg.org/css-color/#the-color-property", + "https://w3c.github.io/svgwg/svg2-draft/painting.html#ColorProperty" + ], + "status": { + "deprecated": false, + "experimental": false, + "standard_track": true + }, + "support": { + "chrome": [{ "added": 1 }], + "chrome_android": [{ "added": 18 }], + "edge": [{ "added": 12 }], + "firefox": [{ "added": 1 }], + "firefox_android": [{ "added": 4 }], + "ie": [{ "added": 3 }], + "safari": [{ "added": 1 }], + "safari_ios": [{ "added": 1 }] + }, + "tags": ["web-features:color"] + } + }, + "border-block-color": { + "__compat": { + "mdn_url": "https://developer.mozilla.org/docs/Web/CSS/Reference/Properties/border-block-color", + "spec_url": "https://drafts.csswg.org/css-logical/#propdef-border-block-color", + "status": { + "deprecated": false, + "experimental": false, + "standard_track": true + }, + "support": { + "chrome": [{ "added": 87 }], + "chrome_android": [{ "added": 87 }], + "edge": [{ "added": 87 }], + "firefox": [{ "added": 66 }], + "firefox_android": [{ "added": 66 }], + "ie": [{ "added": false }], + "safari": [{ "added": 14.1 }], + "safari_ios": [{ "added": 14.5 }] + }, + "tags": ["web-features:logical-properties"] + } + }, + "grid-column": { + "__compat": { + "mdn_url": "https://developer.mozilla.org/docs/Web/CSS/Reference/Properties/grid-column", + "spec_url": "https://drafts.csswg.org/css-grid/#placement-shorthands", + "status": { + "deprecated": false, + "experimental": false, + "standard_track": true + }, + "support": { + "chrome": [{ "added": 57 }], + "chrome_android": [{ "added": 57 }], + "edge": [{ "added": 16 }], + "firefox": [{ "added": 52 }], + "firefox_android": [{ "added": 52 }], + "ie": [{ "added": false }], + "safari": [{ "added": 10.1 }], + "safari_ios": [{ "added": 10.3 }] + }, + "tags": ["web-features:grid"] + } + }, + "ruby-align": { + "__compat": { + "mdn_url": "https://developer.mozilla.org/docs/Web/CSS/Reference/Properties/ruby-align", + "spec_url": "https://drafts.csswg.org/css-ruby/#ruby-align-property", + "status": { + "deprecated": false, + "experimental": false, + "standard_track": true + }, + "support": { + "chrome": [{ "added": 128 }], + "chrome_android": [{ "added": 128 }], + "edge": [{ "added": 128 }], + "firefox": [{ "added": 38 }], + "firefox_android": [{ "added": 38 }], + "ie": [{ "added": false }], + "safari": [{ "added": 18.2 }], + "safari_ios": [{ "added": 18.2 }] + }, + "tags": ["web-features:ruby-align"] + } + }, + "clip": { + "__compat": { + "mdn_url": "https://developer.mozilla.org/docs/Web/CSS/Reference/Properties/clip", + "spec_url": "https://drafts.csswg.org/css-masking/#propdef-clip", + "status": { + "deprecated": true, + "experimental": false, + "standard_track": true + }, + "support": { + "chrome": [{ "added": 1 }], + "chrome_android": [{ "added": 18 }], + "edge": [{ "added": 12 }], + "firefox": [{ "added": 1 }], + "firefox_android": [{ "added": 4 }], + "ie": [{ "added": 4 }], + "safari": [{ "added": 1 }], + "safari_ios": [{ "added": 1 }] + }, + "tags": ["web-features:clip"] + } + }, + "-moz-float-edge": { + "__compat": { + "mdn_url": "https://developer.mozilla.org/docs/Web/CSS/Reference/Properties/-moz-float-edge", + "status": { + "deprecated": true, + "experimental": false, + "standard_track": false + }, + "support": { + "chrome": [{ "added": false }], + "chrome_android": [{ "added": false }], + "edge": [{ "added": false }], + "firefox": [{ "added": 1 }], + "firefox_android": [{ "added": 4 }], + "ie": [{ "added": false }], + "safari": [{ "added": false }], + "safari_ios": [{ "added": false }] + } + } + }, + "-moz-user-focus": { + "__compat": { + "mdn_url": "https://developer.mozilla.org/docs/Web/CSS/Reference/Properties/-moz-user-focus", + "status": { + "deprecated": true, + "experimental": false, + "standard_track": false + }, + "support": { + "chrome": [{ "added": false }], + "chrome_android": [{ "added": false }], + "edge": [{ "added": false }], + "firefox": [{ "version_last": "121", "added": 1, "removed": 122 }], + "firefox_android": [ + { "version_last": "121", "added": 4, "removed": 122 } + ], + "ie": [{ "added": false }], + "safari": [{ "added": false }], + "safari_ios": [{ "added": false }] + } + } + }, + "animation-timeline": { + "__compat": { + "mdn_url": "https://developer.mozilla.org/docs/Web/CSS/Reference/Properties/animation-timeline", + "spec_url": "https://drafts.csswg.org/css-animations-2/#animation-timeline", + "status": { + "deprecated": false, + "experimental": false, + "standard_track": true + }, + "support": { + "chrome": [{ "added": 115 }], + "chrome_android": [{ "added": 115 }], + "edge": [{ "added": 115 }], + "firefox": [{ "added": null }], + "firefox_android": [{ "added": false }], + "ie": [{ "added": false }], + "safari": [{ "added": 26 }], + "safari_ios": [{ "added": 26 }] + }, + "tags": ["web-features:scroll-driven-animations"] + } + }, + "overflow-anchor": { + "__compat": { + "mdn_url": "https://developer.mozilla.org/docs/Web/CSS/Reference/Properties/overflow-anchor", + "spec_url": "https://drafts.csswg.org/css-scroll-anchoring/#exclusion-api", + "status": { + "deprecated": false, + "experimental": false, + "standard_track": true + }, + "support": { + "chrome": [{ "added": 56 }], + "chrome_android": [{ "added": 56 }], + "edge": [{ "added": 79 }], + "firefox": [{ "added": 66 }], + "firefox_android": [{ "added": 66 }], + "ie": [{ "added": false }], + "safari": [{ "added": false }], + "safari_ios": [{ "added": false }] + }, + "tags": ["web-features:overflow-anchor"] + } + }, + "font-variant-alternates": { + "__compat": { + "mdn_url": "https://developer.mozilla.org/docs/Web/CSS/Reference/Properties/font-variant-alternates", + "spec_url": "https://drafts.csswg.org/css-fonts/#font-variant-alternates-prop", + "status": { + "deprecated": false, + "experimental": false, + "standard_track": true + }, + "support": { + "chrome": [{ "added": 111 }], + "chrome_android": [{ "added": 111 }], + "edge": [{ "added": 111 }], + "firefox": [{ "added": 34 }], + "firefox_android": [{ "added": 34 }], + "ie": [{ "added": false }], + "safari": [{ "added": 9.1 }], + "safari_ios": [{ "added": 9.3 }] + }, + "tags": ["web-features:font-variant-alternates"] + } + }, + "text-box-edge": { + "__compat": { + "mdn_url": "https://developer.mozilla.org/docs/Web/CSS/Reference/Properties/text-box-edge", + "spec_url": "https://drafts.csswg.org/css-inline-3/#text-box-edge", + "status": { + "deprecated": false, + "experimental": false, + "standard_track": true + }, + "support": { + "chrome": [{ "added": 133 }], + "chrome_android": [{ "added": 133 }], + "edge": [{ "added": 133 }], + "firefox": [{ "added": false }], + "firefox_android": [{ "added": false }], + "ie": [{ "added": false }], + "safari": [{ "added": 18.2 }], + "safari_ios": [{ "added": 18.2 }] + }, + "tags": ["web-features:text-box"] + } + }, + "stroke-color": { + "__compat": { + "spec_url": "https://drafts.csswg.org/fill-stroke-3/#stroke-color", + "status": { + "deprecated": false, + "experimental": true, + "standard_track": true + }, + "support": { + "chrome": [{ "added": false }], + "chrome_android": [{ "added": false }], + "edge": [{ "added": false }], + "firefox": [{ "added": false }], + "firefox_android": [{ "added": false }], + "ie": [{ "added": false }], + "safari": [{ "added": 11.1 }], + "safari_ios": [{ "added": 11.3 }] + }, + "tags": ["web-features:svg"] + } + }, + "-moz-orient": { + "__compat": { + "status": { + "deprecated": false, + "experimental": false, + "standard_track": false + }, + "support": { + "chrome": [{ "added": false }], + "chrome_android": [{ "added": false }], + "edge": [{ "added": false }], + "firefox": [{ "added": 6 }], + "firefox_android": [{ "added": 6 }], + "ie": [{ "added": false }], + "safari": [{ "added": false }], + "safari_ios": [{ "added": false }] + } + } + }, + "user-select": { + "__compat": { + "mdn_url": "https://developer.mozilla.org/docs/Web/CSS/Reference/Properties/user-select", + "spec_url": "https://drafts.csswg.org/css-ui/#content-selection", + "status": { + "deprecated": false, + "experimental": false, + "standard_track": true + }, + "support": { + "chrome": [{ "added": 54 }, { "prefix": "-webkit-", "added": 1 }], + "chrome_android": [ + { "added": 54 }, + { "prefix": "-webkit-", "added": 18 } + ], + "edge": [ + { "added": 79 }, + { "prefix": "-webkit-", "added": 12 }, + { "prefix": "-ms-", "version_last": "18", "added": 12, "removed": 79 } + ], + "firefox": [ + { "added": 69 }, + { "prefix": "-webkit-", "added": 49 }, + { "prefix": "-moz-", "added": 1 } + ], + "firefox_android": [ + { "added": 79 }, + { "prefix": "-webkit-", "added": 49 }, + { "prefix": "-moz-", "added": 4 } + ], + "ie": [{ "prefix": "-ms-", "added": 10 }], + "safari": [ + { "prefix": "-webkit-", "added": 3 }, + { "prefix": "-khtml-", "version_last": "2", "added": 2, "removed": 3 } + ], + "safari_ios": [{ "prefix": "-webkit-", "added": 3 }] + }, + "tags": ["web-features:user-select"] + }, + "_aliasOf": "user-select" + }, + "user-modify": { + "__compat": { + "mdn_url": "https://developer.mozilla.org/docs/Web/CSS/Reference/Properties/user-modify", + "status": { + "deprecated": true, + "experimental": false, + "standard_track": false + }, + "support": { + "chrome": [{ "prefix": "-webkit-", "added": 1 }], + "chrome_android": [{ "prefix": "-webkit-", "added": 18 }], + "edge": [{ "prefix": "-webkit-", "added": 12 }], + "firefox": [ + { + "partial_implementation": true, + "prefix": "-moz-", + "version_last": "131", + "added": 1, + "removed": 132 + } + ], + "firefox_android": [ + { + "partial_implementation": true, + "prefix": "-moz-", + "version_last": "131", + "added": 4, + "removed": 132 + } + ], + "ie": [{ "added": false }], + "safari": [ + { "prefix": "-webkit-", "added": 3 }, + { "prefix": "-khtml-", "version_last": "2", "added": 2, "removed": 3 } + ], + "safari_ios": [{ "prefix": "-webkit-", "added": 5 }] + } + }, + "_aliasOf": "user-modify" + }, + "text-size-adjust": { + "__compat": { + "mdn_url": "https://developer.mozilla.org/docs/Web/CSS/Reference/Properties/text-size-adjust", + "spec_url": "https://drafts.csswg.org/css-size-adjust/#adjustment-control", + "status": { + "deprecated": false, + "experimental": true, + "standard_track": true + }, + "support": { + "chrome": [{ "added": 54 }], + "chrome_android": [{ "added": 54 }], + "edge": [ + { "added": 79 }, + { + "prefix": "-webkit-", + "version_last": "18", + "added": 12, + "removed": 79 + } + ], + "firefox": [{ "added": false }], + "firefox_android": [ + { "prefix": "-webkit-", "added": 49 }, + { "prefix": "-moz-", "added": 14 } + ], + "ie": [{ "added": false }], + "safari": [{ "added": false }], + "safari_ios": [{ "prefix": "-webkit-", "added": 1 }] + }, + "tags": ["web-features:text-size-adjust"] + }, + "_aliasOf": "text-size-adjust" + }, + "-webkit-text-size-adjust": { "_aliasOf": "text-size-adjust" }, + "-moz-text-size-adjust": { "_aliasOf": "text-size-adjust" }, + "-webkit-user-modify": { "_aliasOf": "user-modify" }, + "-moz-user-modify": { "_aliasOf": "user-modify" }, + "-khtml-user-modify": { "_aliasOf": "user-modify" }, + "-webkit-user-select": { "_aliasOf": "user-select" }, + "-ms-user-select": { "_aliasOf": "user-select" }, + "-moz-user-select": { "_aliasOf": "user-select" }, + "-khtml-user-select": { "_aliasOf": "user-select" } +} diff --git a/devtools/shared/compatibility/dataset/moz.build b/devtools/shared/compatibility/dataset/moz.build index 73acb9d20a96..9324c302bae0 100644 --- a/devtools/shared/compatibility/dataset/moz.build +++ b/devtools/shared/compatibility/dataset/moz.build @@ -5,3 +5,8 @@ DevToolsModules( "css-properties.json", ) + +# Available during tests at resource://testing-common/devtools/compatibility +TESTING_JS_MODULES.devtools.compatibility += [ + "mock-css-properties.json", +] diff --git a/devtools/shared/compatibility/moz.build b/devtools/shared/compatibility/moz.build index 357e460aa2a8..34fa17ba9903 100644 --- a/devtools/shared/compatibility/moz.build +++ b/devtools/shared/compatibility/moz.build @@ -7,6 +7,7 @@ DIRS += [ ] DevToolsModules( + "compatibility-dataset.js", "compatibility-user-settings.js", "constants.js", "helpers.js", diff --git a/docshell/base/BrowsingContext.h b/docshell/base/BrowsingContext.h index aba8b50b04a5..3750c24728fa 100644 --- a/docshell/base/BrowsingContext.h +++ b/docshell/base/BrowsingContext.h @@ -245,6 +245,7 @@ struct EmbedderColorSchemes { * top BCs. */ \ FIELD(AuthorStyleDisabledDefault, bool) \ FIELD(ServiceWorkersTestingEnabled, bool) \ + FIELD(ServiceWorkersDisabledByPolicy, bool) \ FIELD(MediumOverride, nsString) \ /* DevTools override for prefers-color-scheme */ \ FIELD(PrefersColorSchemeOverride, dom::PrefersColorSchemeOverride) \ @@ -1090,6 +1091,10 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache { return GetServiceWorkersTestingEnabled(); } + bool ServiceWorkersDisabledByPolicy() const { + return GetServiceWorkersDisabledByPolicy(); + } + void GetMediumOverride(nsAString& aOverride) const { aOverride = GetMediumOverride(); } @@ -1309,6 +1314,11 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache { return IsTop(); } + bool CanSet(FieldIndex, bool, + ContentParent*) { + return IsTop(); + } + bool CanSet(FieldIndex, const nsCString&, ContentParent*) { return IsTop(); diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp index 0090fb7b48df..450379a21159 100644 --- a/docshell/base/nsDocShell.cpp +++ b/docshell/base/nsDocShell.cpp @@ -12668,9 +12668,8 @@ bool nsDocShell::ServiceWorkerAllowedToControlWindow(nsIPrincipal* aPrincipal, StorageAccess storage = StorageAllowedForNewWindow(aPrincipal, aURI, parentInner); - // If the partitioned service worker is enabled, service worker is allowed to - // control the window if partition is enabled. - if (StaticPrefs::privacy_partition_serviceWorkers() && parentInner) { + // A service worker is allowed to control the window if partition is enabled. + if (parentInner) { RefPtr doc = parentInner->GetExtantDoc(); if (doc && StoragePartitioningEnabled(storage, doc->CookieJarSettings())) { diff --git a/docshell/base/nsDocShellLoadState.cpp b/docshell/base/nsDocShellLoadState.cpp index 3f2a9ce70888..ba31280e8a44 100644 --- a/docshell/base/nsDocShellLoadState.cpp +++ b/docshell/base/nsDocShellLoadState.cpp @@ -30,6 +30,7 @@ #include "mozilla/dom/NavigationUtils.h" #include "mozilla/dom/ProcessIsolation.h" #include "mozilla/dom/SessionHistoryEntry.h" +#include "mozilla/dom/ServiceWorkerUtils.h" #include "mozilla/dom/nsHTTPSOnlyUtils.h" #include "mozilla/net/DocumentLoadListener.h" #include "mozilla/StaticPrefs_browser.h" @@ -1487,6 +1488,16 @@ nsLoadFlags nsDocShellLoadState::CalculateChannelLoadFlags( // interception to occur. See step 12.1 of the SW HandleFetch algorithm. if (IsForceReloadType(loadType)) { loadFlags |= nsIChannel::LOAD_BYPASS_SERVICE_WORKER; + } else if (aBrowsingContext->IsTopContent()) { + // For the top-level site the uri to load determines whether service workers + // are blocked by policy. + if (dom::IsServiceWorkersDisabledByPolicy(mURI)) { + loadFlags |= nsIChannel::LOAD_BYPASS_SERVICE_WORKER; + } + } else if (aBrowsingContext->Top()->ServiceWorkersDisabledByPolicy()) { + // Otherwise use the state of the top-level site to determine whether + // service workers are blocked. + loadFlags |= nsIChannel::LOAD_BYPASS_SERVICE_WORKER; } return loadFlags; @@ -1549,8 +1560,8 @@ const char* nsDocShellLoadState::ValidateWithOriginalState( return "HasSpeculativeListener"; } - // FIXME: Consider calculating less information in the target process so that - // we can validate more properties more easily. + // FIXME: Consider calculating less information in the target process so + // that we can validate more properties more easily. // FIXME: Identify what other flags will not change when sent through a // content process. diff --git a/dom/base/nsContentUtils.cpp b/dom/base/nsContentUtils.cpp index 345d11d5e03b..142eccd29998 100644 --- a/dom/base/nsContentUtils.cpp +++ b/dom/base/nsContentUtils.cpp @@ -2840,6 +2840,28 @@ inline bool SchemeSaysShouldNotResistFingerprinting(nsIPrincipal* aPrincipal) { return !isContentAccessibleAboutURI; } +// mFirstPartyDomain and mPartitionKey are serialized either as a bare host +// ("example.com") or in site format ("(https,example.com[,port][,f])"), +// depending on privacy.firstparty.isolate.use_site and +// privacy.dynamic_firstparty.use_site respectively. Those two prefs are +// independent, and ParsePartitionKey() only consults the latter, so detect the +// format here instead of letting it decide for a first-party domain. +inline void TopLevelInfoToBaseDomain(const nsAString& aInfo, + nsAString& aBaseDomain) { + if (aInfo.IsEmpty() || aInfo.First() != '(') { + aBaseDomain = aInfo; + return; + } + + nsAutoString scheme; + int32_t port; + bool foreignByAncestorContext; + if (!OriginAttributes::ParsePartitionKey(aInfo, scheme, aBaseDomain, port, + foreignByAncestorContext)) { + aBaseDomain.Truncate(); + } +} + inline bool PartionKeyIsAlsoExempted( const mozilla::OriginAttributes& aOriginAttributes) { // If we've gotten here we have (probably) passed the CookieJarSettings @@ -2850,15 +2872,18 @@ inline bool PartionKeyIsAlsoExempted( // instatiated from a state where we could have been partitioned. // So perform this last-ditch check for that scenario. // We arbitrarily use https as the scheme, but it doesn't matter. - nsresult rv = NS_ERROR_NOT_INITIALIZED; - nsCOMPtr uri; + nsAutoString baseDomain; if (StaticPrefs::privacy_firstparty_isolate() && !aOriginAttributes.mFirstPartyDomain.IsEmpty()) { - rv = NS_NewURI(getter_AddRefs(uri), - u"https://"_ns + aOriginAttributes.mFirstPartyDomain); + TopLevelInfoToBaseDomain(aOriginAttributes.mFirstPartyDomain, baseDomain); } else if (!aOriginAttributes.mPartitionKey.IsEmpty()) { - rv = NS_NewURI(getter_AddRefs(uri), - u"https://"_ns + aOriginAttributes.mPartitionKey); + TopLevelInfoToBaseDomain(aOriginAttributes.mPartitionKey, baseDomain); + } + + nsresult rv = NS_ERROR_NOT_INITIALIZED; + nsCOMPtr uri; + if (!baseDomain.IsEmpty()) { + rv = NS_NewURI(getter_AddRefs(uri), u"https://"_ns + baseDomain); } if (!NS_FAILED(rv)) { diff --git a/dom/base/nsGlobalWindowInner.cpp b/dom/base/nsGlobalWindowInner.cpp index 5cbc43326cb4..4d954f6dccf7 100644 --- a/dom/base/nsGlobalWindowInner.cpp +++ b/dom/base/nsGlobalWindowInner.cpp @@ -79,7 +79,6 @@ #include "mozilla/StaticPrefs_docshell.h" #include "mozilla/StaticPrefs_dom.h" #include "mozilla/StaticPrefs_extensions.h" -#include "mozilla/StaticPrefs_privacy.h" #include "mozilla/StorageAccess.h" #include "mozilla/StoragePrincipalHelper.h" #include "mozilla/TelemetryHistogramEnums.h" @@ -1987,10 +1986,7 @@ nsresult nsGlobalWindowInner::EnsureClientSource() { nsCOMPtr foreignPartitionedPrincipal; nsresult rv = StoragePrincipalHelper::GetPrincipal( - this, - StaticPrefs::privacy_partition_serviceWorkers() - ? StoragePrincipalHelper::eForeignPartitionedPrincipal - : StoragePrincipalHelper::eRegularPrincipal, + this, StoragePrincipalHelper::eForeignPartitionedPrincipal, getter_AddRefs(foreignPartitionedPrincipal)); NS_ENSURE_SUCCESS(rv, rv); @@ -3718,29 +3714,29 @@ void nsGlobalWindowInner::SetName(const nsAString& aName, FORWARD_TO_OUTER_OR_THROW(SetNameOuter, (aName, aError), aError, ); } -double nsGlobalWindowInner::GetInnerWidth(ErrorResult& aError) { - FORWARD_TO_OUTER_OR_THROW(GetInnerWidthOuter, (aError), aError, 0); +double nsGlobalWindowInner::GetInnerWidth(CallerType aCallerType, + ErrorResult& aError) { + FORWARD_TO_OUTER_OR_THROW(GetInnerWidthOuter, (aCallerType, aError), aError, + 0); } -nsresult nsGlobalWindowInner::GetInnerWidth(double* aWidth) { +nsresult nsGlobalWindowInner::GetInnerWidth(CallerType aCallerType, + double* aWidth) { ErrorResult rv; - // Callee doesn't care about the caller type, but play it safe. - *aWidth = GetInnerWidth(rv); + *aWidth = GetInnerWidth(aCallerType, rv); return rv.StealNSResult(); } -double nsGlobalWindowInner::GetInnerHeight(ErrorResult& aError) { - // We ignore aCallerType; we only have that argument because some other things - // called by GetReplaceableWindowCoord need it. If this ever changes, fix - // nsresult nsGlobalWindowInner::GetInnerHeight(double* aInnerWidth) - // to actually take a useful CallerType and pass it in here. - FORWARD_TO_OUTER_OR_THROW(GetInnerHeightOuter, (aError), aError, 0); +double nsGlobalWindowInner::GetInnerHeight(CallerType aCallerType, + ErrorResult& aError) { + FORWARD_TO_OUTER_OR_THROW(GetInnerHeightOuter, (aCallerType, aError), aError, + 0); } -nsresult nsGlobalWindowInner::GetInnerHeight(double* aHeight) { +nsresult nsGlobalWindowInner::GetInnerHeight(CallerType aCallerType, + double* aHeight) { ErrorResult rv; - // Callee doesn't care about the caller type, but play it safe. - *aHeight = GetInnerHeight(rv); + *aHeight = GetInnerHeight(aCallerType, rv); return rv.StealNSResult(); } diff --git a/dom/base/nsGlobalWindowInner.h b/dom/base/nsGlobalWindowInner.h index 65339914b080..32b7f81cf083 100644 --- a/dom/base/nsGlobalWindowInner.h +++ b/dom/base/nsGlobalWindowInner.h @@ -1006,12 +1006,16 @@ class nsGlobalWindowInner final : public mozilla::dom::EventTarget, JS::Handle aValue, mozilla::ErrorResult& aError); - MOZ_CAN_RUN_SCRIPT nsresult GetInnerWidth(double* aWidth) override; - MOZ_CAN_RUN_SCRIPT nsresult GetInnerHeight(double* aHeight) override; + MOZ_CAN_RUN_SCRIPT nsresult + GetInnerWidth(mozilla::dom::CallerType aCallerType, double* aWidth) override; + MOZ_CAN_RUN_SCRIPT nsresult GetInnerHeight( + mozilla::dom::CallerType aCallerType, double* aHeight) override; public: - MOZ_CAN_RUN_SCRIPT double GetInnerWidth(mozilla::ErrorResult& aError); - MOZ_CAN_RUN_SCRIPT double GetInnerHeight(mozilla::ErrorResult& aError); + MOZ_CAN_RUN_SCRIPT double GetInnerWidth(mozilla::dom::CallerType aCallerType, + mozilla::ErrorResult& aError); + MOZ_CAN_RUN_SCRIPT double GetInnerHeight(mozilla::dom::CallerType aCallerType, + mozilla::ErrorResult& aError); int32_t GetScreenX(mozilla::dom::CallerType aCallerType, mozilla::ErrorResult& aError); int32_t GetScreenY(mozilla::dom::CallerType aCallerType, diff --git a/dom/base/nsGlobalWindowOuter.cpp b/dom/base/nsGlobalWindowOuter.cpp index b9caf10ddbec..e9dd6d630632 100644 --- a/dom/base/nsGlobalWindowOuter.cpp +++ b/dom/base/nsGlobalWindowOuter.cpp @@ -3430,7 +3430,8 @@ CSSToLayoutDeviceScale nsGlobalWindowOuter::CSSToDevScaleForBaseWindow( return scale; } -nsresult nsGlobalWindowOuter::GetInnerSize(CSSSize& aSize) { +nsresult nsGlobalWindowOuter::GetInnerSize(CSSSize& aSize, + CallerType aCallerType) { if (mDoc && mDoc->IsTopLevelContentDocument() && nsLayoutUtils::ShouldHandleMetaViewport(mDoc)) { // Window.inner{Width,Height} depend on minimum-scale size and to get the @@ -3465,6 +3466,10 @@ nsresult nsGlobalWindowOuter::GetInnerSize(CSSSize& aSize) { aSize = CSSPixel::FromAppUnits(innerSize); + if (aCallerType == dom::CallerType::System) { + return NS_OK; + } + switch (StaticPrefs::dom_innerSize_rounding()) { case 1: aSize.width = std::roundf(aSize.width); @@ -3481,28 +3486,20 @@ nsresult nsGlobalWindowOuter::GetInnerSize(CSSSize& aSize) { return NS_OK; } -double nsGlobalWindowOuter::GetInnerWidthOuter(ErrorResult& aError) { +double nsGlobalWindowOuter::GetInnerWidthOuter(CallerType aCallerType, + ErrorResult& aError) { CSSSize size; - aError = GetInnerSize(size); + aError = GetInnerSize(size, aCallerType); return size.width; } -nsresult nsGlobalWindowOuter::GetInnerWidth(double* aInnerWidth) { - FORWARD_TO_INNER_WITH_STRONG_REF(GetInnerWidth, (aInnerWidth), - NS_ERROR_UNEXPECTED); -} - -double nsGlobalWindowOuter::GetInnerHeightOuter(ErrorResult& aError) { +double nsGlobalWindowOuter::GetInnerHeightOuter(CallerType aCallerType, + ErrorResult& aError) { CSSSize size; - aError = GetInnerSize(size); + aError = GetInnerSize(size, aCallerType); return size.height; } -nsresult nsGlobalWindowOuter::GetInnerHeight(double* aInnerHeight) { - FORWARD_TO_INNER_WITH_STRONG_REF(GetInnerHeight, (aInnerHeight), - NS_ERROR_UNEXPECTED); -} - CSSIntSize nsGlobalWindowOuter::GetOuterSize(CallerType aCallerType, ErrorResult& aError) { if (nsIGlobalObject::ShouldResistFingerprinting(aCallerType, diff --git a/dom/base/nsGlobalWindowOuter.h b/dom/base/nsGlobalWindowOuter.h index fa95821e5198..af32af0ad79b 100644 --- a/dom/base/nsGlobalWindowOuter.h +++ b/dom/base/nsGlobalWindowOuter.h @@ -637,16 +637,14 @@ class nsGlobalWindowOuter final : public mozilla::dom::EventTarget, virtual bool IsInSyncOperation() override; public: - MOZ_CAN_RUN_SCRIPT double GetInnerWidthOuter(mozilla::ErrorResult& aError); - - protected: - MOZ_CAN_RUN_SCRIPT nsresult GetInnerWidth(double* aInnerWidth) override; + MOZ_CAN_RUN_SCRIPT double GetInnerWidthOuter( + mozilla::dom::CallerType aCallerType, mozilla::ErrorResult& aError); public: - MOZ_CAN_RUN_SCRIPT double GetInnerHeightOuter(mozilla::ErrorResult& aError); + MOZ_CAN_RUN_SCRIPT double GetInnerHeightOuter( + mozilla::dom::CallerType aCallerType, mozilla::ErrorResult& aError); protected: - MOZ_CAN_RUN_SCRIPT nsresult GetInnerHeight(double* aInnerHeight) override; int32_t GetScreenXOuter(mozilla::dom::CallerType aCallerType, mozilla::ErrorResult& aError); int32_t GetScreenYOuter(mozilla::dom::CallerType aCallerType, @@ -785,7 +783,8 @@ class nsGlobalWindowOuter final : public mozilla::dom::EventTarget, int32_t GetScrollBoundaryOuter(mozilla::Side aSide); // Outer windows only. - MOZ_CAN_RUN_SCRIPT nsresult GetInnerSize(mozilla::CSSSize& aSize); + MOZ_CAN_RUN_SCRIPT nsresult + GetInnerSize(mozilla::CSSSize& aSize, mozilla::dom::CallerType aCallerType); mozilla::CSSIntSize GetOuterSize(mozilla::dom::CallerType aCallerType, mozilla::ErrorResult& aError); nsRect GetInnerScreenRect(); diff --git a/dom/base/nsPIDOMWindow.h b/dom/base/nsPIDOMWindow.h index eb82e2eb1220..faf5116fbece 100644 --- a/dom/base/nsPIDOMWindow.h +++ b/dom/base/nsPIDOMWindow.h @@ -590,8 +590,10 @@ class nsPIDOMWindowInner : public mozIDOMWindow { virtual nsresult GetControllers(nsIControllers** aControllers) = 0; - MOZ_CAN_RUN_SCRIPT virtual nsresult GetInnerWidth(double* aWidth) = 0; - MOZ_CAN_RUN_SCRIPT virtual nsresult GetInnerHeight(double* aHeight) = 0; + MOZ_CAN_RUN_SCRIPT virtual nsresult GetInnerWidth( + mozilla::dom::CallerType aCallerType, double* aWidth) = 0; + MOZ_CAN_RUN_SCRIPT virtual nsresult GetInnerHeight( + mozilla::dom::CallerType aCallerType, double* aHeight) = 0; virtual already_AddRefed GetComputedStyle( mozilla::dom::Element& aElt, const nsAString& aPseudoElt, @@ -1092,9 +1094,6 @@ class nsPIDOMWindowOuter : public mozIDOMWindowProxy { const nsAString& aOptions, nsIArray* aArguments, mozilla::dom::BrowsingContext** _retval) = 0; - MOZ_CAN_RUN_SCRIPT virtual nsresult GetInnerWidth(double* aWidth) = 0; - MOZ_CAN_RUN_SCRIPT virtual nsresult GetInnerHeight(double* aHeight) = 0; - virtual mozilla::dom::Element* GetFrameElement() = 0; virtual bool Closed() = 0; diff --git a/dom/chrome-webidl/BrowsingContext.webidl b/dom/chrome-webidl/BrowsingContext.webidl index 91b2d3ccaad0..7b3dca8a55d7 100644 --- a/dom/chrome-webidl/BrowsingContext.webidl +++ b/dom/chrome-webidl/BrowsingContext.webidl @@ -328,6 +328,9 @@ BrowsingContext includes LoadContextMixin; [Exposed=Window, ChromeOnly] interface CanonicalBrowsingContext : BrowsingContext { + // Whether enterprise policy has disabled service workers for the top-level site. + readonly attribute boolean serviceWorkersDisabledByPolicy; + // Top-level only download folder override for WebDriver BiDi's. [SetterThrows] attribute DOMString downloadFolderOverride; diff --git a/dom/clients/manager/ClientChannelHelper.cpp b/dom/clients/manager/ClientChannelHelper.cpp index a17df8c6ed86..def48e59d469 100644 --- a/dom/clients/manager/ClientChannelHelper.cpp +++ b/dom/clients/manager/ClientChannelHelper.cpp @@ -8,7 +8,6 @@ #include "ClientSource.h" #include "MainThreadUtils.h" #include "mozilla/AntiTrackingUtils.h" -#include "mozilla/StaticPrefs_privacy.h" #include "mozilla/StoragePrincipalHelper.h" #include "mozilla/dom/ClientsBinding.h" #include "mozilla/dom/ServiceWorkerDescriptor.h" @@ -111,9 +110,7 @@ class ClientChannelHelper : public nsIInterfaceRequestor, nsCOMPtr foreignPartitionedPrincipal; rv = StoragePrincipalHelper::GetPrincipal( aNewChannel, - StaticPrefs::privacy_partition_serviceWorkers() - ? StoragePrincipalHelper::eForeignPartitionedPrincipal - : StoragePrincipalHelper::eRegularPrincipal, + StoragePrincipalHelper::eForeignPartitionedPrincipal, getter_AddRefs(foreignPartitionedPrincipal)); NS_ENSURE_SUCCESS(rv, rv); reservedClient.reset(); @@ -141,10 +138,7 @@ class ClientChannelHelper : public nsIInterfaceRequestor, nsCOMPtr foreignPartitionedPrincipal; rv = StoragePrincipalHelper::GetPrincipal( - aNewChannel, - StaticPrefs::privacy_partition_serviceWorkers() - ? StoragePrincipalHelper::eForeignPartitionedPrincipal - : StoragePrincipalHelper::eRegularPrincipal, + aNewChannel, StoragePrincipalHelper::eForeignPartitionedPrincipal, getter_AddRefs(foreignPartitionedPrincipal)); NS_ENSURE_SUCCESS(rv, rv); @@ -310,10 +304,7 @@ nsresult AddClientChannelHelperInternal(nsIChannel* aChannel, nsCOMPtr channelForeignPartitionedPrincipal; nsresult rv = StoragePrincipalHelper::GetPrincipal( - aChannel, - StaticPrefs::privacy_partition_serviceWorkers() - ? StoragePrincipalHelper::eForeignPartitionedPrincipal - : StoragePrincipalHelper::eRegularPrincipal, + aChannel, StoragePrincipalHelper::eForeignPartitionedPrincipal, getter_AddRefs(channelForeignPartitionedPrincipal)); NS_ENSURE_SUCCESS(rv, rv); diff --git a/dom/documentpip/DocumentPictureInPicture.cpp b/dom/documentpip/DocumentPictureInPicture.cpp index 3c95acf8fce3..cb2fc309c069 100644 --- a/dom/documentpip/DocumentPictureInPicture.cpp +++ b/dom/documentpip/DocumentPictureInPicture.cpp @@ -78,8 +78,10 @@ void DocumentPictureInPicture::OnPiPResized() { int x = innerWindow->GetScreenLeft(CallerType::System, IgnoreErrors()); int y = innerWindow->GetScreenTop(CallerType::System, IgnoreErrors()); - int width = static_cast(innerWindow->GetInnerWidth(IgnoreErrors())); - int height = static_cast(innerWindow->GetInnerHeight(IgnoreErrors())); + int width = static_cast(std::round( + innerWindow->GetInnerWidth(dom::CallerType::System, IgnoreErrors()))); + int height = static_cast(std::round( + innerWindow->GetInnerHeight(dom::CallerType::System, IgnoreErrors()))); mPreviousExtent = Some(CSSIntRect(x, y, width, height)); diff --git a/dom/html/test/browser_ImageDocument_svg_zoom.js b/dom/html/test/browser_ImageDocument_svg_zoom.js index f0df2282a331..0a869f248d53 100644 --- a/dom/html/test/browser_ImageDocument_svg_zoom.js +++ b/dom/html/test/browser_ImageDocument_svg_zoom.js @@ -14,7 +14,9 @@ function test_once() { ); is( Math.round(rect.height), - content.innerHeight, + // We need to retrieve this through wrappedJSObject so it's not considered + // a system call + content.wrappedJSObject.innerHeight, "Should fill the viewport and not overflow" ); }); diff --git a/dom/media/webrtc/CodecInfo.cpp b/dom/media/webrtc/CodecInfo.cpp index 57e76ffa32b7..fa47a89c8ae2 100644 --- a/dom/media/webrtc/CodecInfo.cpp +++ b/dom/media/webrtc/CodecInfo.cpp @@ -31,19 +31,16 @@ media::DecodeSupportSet SupportsVideoDecodeForWebrtc( // Implementation class that samples codec preferences once at construction. class CodecInfoImpl final : public WebrtcCodecInfo { public: - CodecInfoImpl() : CodecInfoImpl(OverrideRtxPreference::NoOverride) {} - explicit CodecInfoImpl(const OverrideRtxPreference aOverrideRtxPreference) - : mPrefs([aOverrideRtxPreference] { - return DefaultCodecPreferences(aOverrideRtxPreference); - }()), + CodecInfoImpl() + : mPrefs(), mAudioCodecs([this] { - nsTArray> codecs; - EnumerateDefaultAudioCodecs(codecs, mPrefs); + AutoTArray, 5> codecs; + EnumerateDefaultAudioCodecs(&codecs, mPrefs); return codecs; }()), mVideoCodecs([this] { - nsTArray> codecs; - EnumerateDefaultVideoCodecs(codecs, mPrefs); + AutoTArray, 10> codecs; + EnumerateDefaultVideoCodecs(&codecs, mPrefs); return codecs; }()) {} diff --git a/dom/media/webrtc/jsapi/DefaultCodecPreferences.cpp b/dom/media/webrtc/jsapi/DefaultCodecPreferences.cpp index 113c6ed7c5ee..95dae38e2d79 100644 --- a/dom/media/webrtc/jsapi/DefaultCodecPreferences.cpp +++ b/dom/media/webrtc/jsapi/DefaultCodecPreferences.cpp @@ -4,10 +4,10 @@ #include "DefaultCodecPreferences.h" -#include "PeerConnectionImpl.h" #include "gmp/GMPUtils.h" #include "libwebrtcglue/VideoConduit.h" #include "mozilla/StaticPrefs_media.h" +#include "nsTArray.h" namespace mozilla { @@ -113,61 +113,43 @@ bool DefaultCodecPreferences::RedUlpfecEnabledStatic() { } void EnumerateDefaultVideoCodecs( - nsTArray>& aSupportedCodecs, - const OverrideRtxPreference aOverrideRtxPreference) { - const DefaultCodecPreferences prefs(aOverrideRtxPreference); - EnumerateDefaultVideoCodecs(aSupportedCodecs, prefs); -} - -void EnumerateDefaultVideoCodecs( - nsTArray>& aSupportedCodecs, + nsTArray>* aSupportedCodecs, const JsepCodecPreferences& aPrefs) { + MOZ_ASSERT(aSupportedCodecs); // Supported video codecs. // Note: order here implies priority for building offers! - aSupportedCodecs.AppendElement( - JsepVideoCodecDescription::CreateDefaultVP8(aPrefs)); - aSupportedCodecs.AppendElement( - JsepVideoCodecDescription::CreateDefaultVP9(aPrefs)); - aSupportedCodecs.AppendElement( - JsepVideoCodecDescription::CreateDefaultH264_1(aPrefs)); - aSupportedCodecs.AppendElement( - JsepVideoCodecDescription::CreateDefaultH264_0(aPrefs)); - aSupportedCodecs.AppendElement( + AutoTArray, 10> codecs; + codecs.AppendElement(JsepVideoCodecDescription::CreateDefaultVP8(aPrefs)); + codecs.AppendElement(JsepVideoCodecDescription::CreateDefaultVP9(aPrefs)); + codecs.AppendElement(JsepVideoCodecDescription::CreateDefaultH264_1(aPrefs)); + codecs.AppendElement(JsepVideoCodecDescription::CreateDefaultH264_0(aPrefs)); + codecs.AppendElement( JsepVideoCodecDescription::CreateDefaultH264Baseline_1(aPrefs)); - aSupportedCodecs.AppendElement( + codecs.AppendElement( JsepVideoCodecDescription::CreateDefaultH264Baseline_0(aPrefs)); - aSupportedCodecs.AppendElement( - JsepVideoCodecDescription::CreateDefaultAV1(aPrefs)); - aSupportedCodecs.AppendElement( - JsepVideoCodecDescription::CreateDefaultUlpFec(aPrefs)); - aSupportedCodecs.AppendElement( - JsepApplicationCodecDescription::CreateDefault()); - aSupportedCodecs.AppendElement( - JsepVideoCodecDescription::CreateDefaultRed(aPrefs)); + codecs.AppendElement(JsepVideoCodecDescription::CreateDefaultAV1(aPrefs)); + codecs.AppendElement(JsepVideoCodecDescription::CreateDefaultUlpFec(aPrefs)); + codecs.AppendElement(JsepApplicationCodecDescription::CreateDefault()); + codecs.AppendElement(JsepVideoCodecDescription::CreateDefaultRed(aPrefs)); CompareCodecPriority comparator; - std::stable_sort(aSupportedCodecs.begin(), aSupportedCodecs.end(), - comparator); + std::stable_sort(codecs.begin(), codecs.end(), comparator); + + aSupportedCodecs->AppendElements(std::move(codecs)); } void EnumerateDefaultAudioCodecs( - nsTArray>& aSupportedCodecs) { - const auto prefs = PeerConnectionImpl::GetDefaultCodecPreferences(); - EnumerateDefaultAudioCodecs(aSupportedCodecs, prefs); -} - -void EnumerateDefaultAudioCodecs( - nsTArray>& aSupportedCodecs, + nsTArray>* aSupportedCodecs, const JsepCodecPreferences& aPrefs) { - aSupportedCodecs.AppendElement( + aSupportedCodecs->AppendElement( JsepAudioCodecDescription::CreateDefaultOpus(aPrefs)); - aSupportedCodecs.AppendElement( + aSupportedCodecs->AppendElement( JsepAudioCodecDescription::CreateDefaultG722(aPrefs)); - aSupportedCodecs.AppendElement( + aSupportedCodecs->AppendElement( JsepAudioCodecDescription::CreateDefaultPCMU(aPrefs)); - aSupportedCodecs.AppendElement( + aSupportedCodecs->AppendElement( JsepAudioCodecDescription::CreateDefaultPCMA(aPrefs)); - aSupportedCodecs.AppendElement( + aSupportedCodecs->AppendElement( JsepAudioCodecDescription::CreateDefaultTelephoneEvent()); } diff --git a/dom/media/webrtc/jsapi/DefaultCodecPreferences.h b/dom/media/webrtc/jsapi/DefaultCodecPreferences.h index a2e2df7e7f34..e6ce3b183b92 100644 --- a/dom/media/webrtc/jsapi/DefaultCodecPreferences.h +++ b/dom/media/webrtc/jsapi/DefaultCodecPreferences.h @@ -21,115 +21,62 @@ void EnumerateDefaultVideoCodecs( const OverrideRtxPreference aOverrideRtxPreference); void EnumerateDefaultVideoCodecs( - nsTArray>& aSupportedCodecs, + nsTArray>* aSupportedCodecs, const JsepCodecPreferences& aPrefs); void EnumerateDefaultAudioCodecs( - nsTArray>& aSupportedCodecs); - -void EnumerateDefaultAudioCodecs( - nsTArray>& aSupportedCodecs, + nsTArray>* aSupportedCodecs, const JsepCodecPreferences& aPrefs); -class DefaultCodecPreferences final : public JsepCodecPreferences { +class DefaultCodecPreferences : public JsepCodecPreferences { public: - explicit DefaultCodecPreferences( - const OverrideRtxPreference aOverrideRtxPreference) - : mOverrideRtxEnabled(aOverrideRtxPreference) {} - bool AV1Enabled() const override { return mAV1Enabled; } bool AV1Preferred() const override { return mAV1Preferred; } bool H264Enabled() const override { return mH264Enabled; } - bool SoftwareH264Enabled() const override { return mSoftwareH264Enabled; } - bool HardwareH264Enabled() const { return mHardwareH264Enabled; } - + bool HardwareH264Enabled() const override { return mHardwareH264Enabled; } bool SendingH264PacketizationModeZeroSupported() const override { return mSendingH264PacketizationModeZeroSupported; } - bool H264BaselineDisabled() const override { return mH264BaselineDisabled; } - uint8_t H264Level() const override { return mH264Level; } - uint32_t H264MaxBr() const override { return mH264MaxBr; } - uint32_t H264MaxMbps() const override { return mH264MaxMbps; } - bool VP9Enabled() const override { return mVP9Enabled; } - bool VP9Preferred() const override { return mVP9Preferred; } - uint32_t VP8MaxFs() const override { return mVP8MaxFs; } - uint32_t VP8MaxFr() const override { return mVP8MaxFr; } - bool UseTmmbr() const override { return mUseTmmbr; } - bool UseRemb() const override { return mUseRemb; } - - bool UseRtx() const override { - if (mOverrideRtxEnabled == OverrideRtxPreference::NoOverride) { - return mUseRtx; - } - return mOverrideRtxEnabled == OverrideRtxPreference::OverrideWithEnabled; - } - + bool UseRtx() const override { return mUseRtx; } bool UseTransportCC() const override { return mUseTransportCC; } - bool UseAudioTransportCC() const override { return mUseAudioTransportCC; } - bool UseAudioFec() const override { return mUseAudioFec; } - bool RedUlpfecEnabled() const override { return mRedUlpfecEnabled; } + private: static bool AV1EnabledStatic(); - static bool AV1PreferredStatic(); - static bool H264EnabledStatic(); - static bool SoftwareH264EnabledStatic(); - static bool HardwareH264EnabledStatic(); - static bool SendingH264PacketizationModeZeroSupportedStatic(); - static bool H264BaselineDisabledStatic(); - static uint8_t H264LevelStatic(); - static uint32_t H264MaxBrStatic(); - static uint32_t H264MaxMbpsStatic(); - static bool VP9EnabledStatic(); - static bool VP9PreferredStatic(); - static uint32_t VP8MaxFsStatic(); - static uint32_t VP8MaxFrStatic(); - static bool UseTmmbrStatic(); - static bool UseRembStatic(); - static bool UseRtxStatic(); - static bool UseTransportCCStatic(); - static bool UseAudioTransportCCStatic(); - static bool UseAudioFecStatic(); - static bool RedUlpfecEnabledStatic(); - // This is to accommodate the behavior of - // RTCRtpTransceiver::SetCodecPreferences - const OverrideRtxPreference mOverrideRtxEnabled = - OverrideRtxPreference::NoOverride; - const bool mAV1Enabled = AV1EnabledStatic(); const bool mAV1Preferred = AV1PreferredStatic(); const bool mH264Enabled = H264EnabledStatic(); @@ -153,5 +100,31 @@ class DefaultCodecPreferences final : public JsepCodecPreferences { const bool mUseAudioFec = UseAudioFecStatic(); const bool mRedUlpfecEnabled = RedUlpfecEnabledStatic(); }; + +class DefaultCodecPreferencesWithRtxOverride : public DefaultCodecPreferences { + public: + explicit DefaultCodecPreferencesWithRtxOverride( + OverrideRtxPreference aOverrideRtxPreference) + : mOverrideRtxEnabled(aOverrideRtxPreference) {} + + // Allows copying but changing the OverrideRTX flag. + DefaultCodecPreferencesWithRtxOverride( + const DefaultCodecPreferences& aPrefs, + OverrideRtxPreference aOverrideRtxPreference) + : DefaultCodecPreferences(aPrefs), + mOverrideRtxEnabled(aOverrideRtxPreference) {} + + bool UseRtx() const override { + if (mOverrideRtxEnabled == OverrideRtxPreference::NoOverride) { + return DefaultCodecPreferences::UseRtx(); + } + return mOverrideRtxEnabled == OverrideRtxPreference::OverrideWithEnabled; + } + + // This is to accommodate the behavior of + // RTCRtpTransceiver::SetCodecPreferences + const OverrideRtxPreference mOverrideRtxEnabled = + OverrideRtxPreference::NoOverride; +}; } // namespace mozilla #endif // DOM_MEDIA_WEBRTC_JSAPI_DEFAULTCODECPREFERENCES_H_ diff --git a/dom/media/webrtc/jsapi/PeerConnectionImpl.cpp b/dom/media/webrtc/jsapi/PeerConnectionImpl.cpp index 235de963bc85..cae29f526999 100644 --- a/dom/media/webrtc/jsapi/PeerConnectionImpl.cpp +++ b/dom/media/webrtc/jsapi/PeerConnectionImpl.cpp @@ -31,7 +31,6 @@ #include "mozilla/IceServerParser.h" #include "mozilla/IntegerPrintfMacros.h" #include "mozilla/Sprintf.h" -#include "mozilla/StaticPrefs_media.h" #include "mozilla/glean/DomMediaWebrtcMetrics.h" #include "mozilla/media/MediaUtils.h" #include "nsEffectiveTLDService.h" @@ -350,6 +349,27 @@ bool IsPrivateBrowsing(nsPIDOMWindowInner* aWindow) { return loadContext && loadContext->UsePrivateBrowsing(); } +static void RecordCodecTelemetry(const JsepCodecPreferences& aPrefs) { + if (WebrtcVideoConduit::HasH264Hardware()) { + glean::webrtc::has_h264_hardware + .EnumGet(glean::webrtc::HasH264HardwareLabel::eTrue) + .Add(); + } + + glean::webrtc::software_h264_enabled + .EnumGet(static_cast( + aPrefs.SoftwareH264Enabled())) + .Add(); + glean::webrtc::hardware_h264_enabled + .EnumGet(static_cast( + aPrefs.HardwareH264Enabled())) + .Add(); + glean::webrtc::h264_enabled + .EnumGet( + static_cast(aPrefs.H264Enabled())) + .Add(); +} + PeerConnectionImpl::PeerConnectionImpl(const GlobalObject* aGlobal) : mTimeCard(MOZ_LOG_TEST(logModuleInfo, LogLevel::Error) ? create_timecard() : nullptr), @@ -525,17 +545,20 @@ nsresult PeerConnectionImpl::Initialize(PeerConnectionObserver& aObserver, return res; } - std::vector> preferredCodecs; - SetupPreferredCodecs(preferredCodecs); + AutoTArray, 16> preferredCodecs; + EnumerateDefaultVideoCodecs(&preferredCodecs, mPrefs); + EnumerateDefaultAudioCodecs(&preferredCodecs, mPrefs); mJsepSession->SetDefaultCodecs(preferredCodecs); + RecordCodecTelemetry(mPrefs); + // We use this to sort the list of codecs once everything is configured CompareCodecPriority comparator; // Sort by priority mJsepSession->SortCodecs(comparator); - std::vector preferredHeaders; - SetupPreferredRtpExtensions(preferredHeaders); + AutoTArray preferredHeaders; + GetDefaultRtpExtensions(mPrefs, &preferredHeaders); for (const auto& header : preferredHeaders) { mJsepSession->AddRtpExtension(header.mMediaType, header.extensionname, @@ -642,28 +665,6 @@ RefPtr PeerConnectionImpl::Identity() const { return mCertificate->CreateDtlsIdentity(); } -void RecordCodecTelemetry() { - const auto prefs = PeerConnectionImpl::GetDefaultCodecPreferences(); - if (WebrtcVideoConduit::HasH264Hardware()) { - glean::webrtc::has_h264_hardware - .EnumGet(glean::webrtc::HasH264HardwareLabel::eTrue) - .Add(); - } - - glean::webrtc::software_h264_enabled - .EnumGet(static_cast( - prefs.SoftwareH264Enabled())) - .Add(); - glean::webrtc::hardware_h264_enabled - .EnumGet(static_cast( - prefs.HardwareH264Enabled())) - .Add(); - glean::webrtc::h264_enabled - .EnumGet( - static_cast(prefs.H264Enabled())) - .Add(); -} - // Data channels won't work without a window, so in order for the C++ unit // tests to work (it doesn't have a window available) we ifdef the following // two implementations. @@ -2114,73 +2115,64 @@ void PeerConnectionImpl::SendWarningToConsole(const nsCString& aWarning) { "WebRTC"_ns, mWindow->WindowID()); } -void PeerConnectionImpl::GetDefaultVideoCodecs( - std::vector>& aSupportedCodecs, - const OverrideRtxPreference aOverrideRtxPreference) { - nsTArray> codecs; - EnumerateDefaultVideoCodecs(codecs, aOverrideRtxPreference); - aSupportedCodecs.reserve(codecs.Length()); - for (auto& codec : codecs) { - aSupportedCodecs.emplace_back(std::move(codec)); - } -} - -void PeerConnectionImpl::GetDefaultAudioCodecs( - std::vector>& aSupportedCodecs) { - nsTArray> codecs; - EnumerateDefaultAudioCodecs(codecs); - aSupportedCodecs.reserve(codecs.Length()); - for (auto& codec : codecs) { - aSupportedCodecs.emplace_back(std::move(codec)); - } -} - void PeerConnectionImpl::GetDefaultRtpExtensions( - std::vector& aRtpExtensions) { - RtpExtensionHeader audioLevel = {JsepMediaType::kAudio, - SdpDirectionAttribute::Direction::kSendrecv, - webrtc::RtpExtension::kAudioLevelUri}; - aRtpExtensions.push_back(std::move(audioLevel)); + const JsepCodecPreferences& aPrefs, + nsTArray* aRtpExtensions) { + MOZ_ASSERT(aRtpExtensions); + RtpExtensionHeader audioLevel = { + JsepMediaType::kAudio, SdpDirectionAttribute::Direction::kSendrecv, + nsLiteralCString(webrtc::RtpExtension::kAudioLevelUri)}; + aRtpExtensions->AppendElement(std::move(audioLevel)); RtpExtensionHeader csrcAudioLevels = { JsepMediaType::kAudio, SdpDirectionAttribute::Direction::kRecvonly, - webrtc::RtpExtension::kCsrcAudioLevelsUri}; - aRtpExtensions.push_back(std::move(csrcAudioLevels)); + nsLiteralCString(webrtc::RtpExtension::kCsrcAudioLevelsUri)}; + aRtpExtensions->AppendElement(std::move(csrcAudioLevels)); RtpExtensionHeader mid = {JsepMediaType::kAudioVideo, SdpDirectionAttribute::Direction::kSendrecv, - webrtc::RtpExtension::kMidUri}; - aRtpExtensions.push_back(std::move(mid)); + nsLiteralCString(webrtc::RtpExtension::kMidUri)}; + aRtpExtensions->AppendElement(std::move(mid)); - RtpExtensionHeader absSendTime = {JsepMediaType::kVideo, - SdpDirectionAttribute::Direction::kSendrecv, - webrtc::RtpExtension::kAbsSendTimeUri}; - aRtpExtensions.push_back(std::move(absSendTime)); + RtpExtensionHeader absSendTime = { + JsepMediaType::kVideo, SdpDirectionAttribute::Direction::kSendrecv, + nsLiteralCString(webrtc::RtpExtension::kAbsSendTimeUri)}; + aRtpExtensions->AppendElement(std::move(absSendTime)); RtpExtensionHeader timestampOffset = { JsepMediaType::kVideo, SdpDirectionAttribute::Direction::kSendrecv, - webrtc::RtpExtension::kTimestampOffsetUri}; - aRtpExtensions.push_back(std::move(timestampOffset)); + nsLiteralCString(webrtc::RtpExtension::kTimestampOffsetUri)}; + aRtpExtensions->AppendElement(std::move(timestampOffset)); RtpExtensionHeader playoutDelay = { JsepMediaType::kVideo, SdpDirectionAttribute::Direction::kRecvonly, - webrtc::RtpExtension::kPlayoutDelayUri}; - aRtpExtensions.push_back(std::move(playoutDelay)); + nsLiteralCString(webrtc::RtpExtension::kPlayoutDelayUri)}; + aRtpExtensions->AppendElement(std::move(playoutDelay)); - RtpExtensionHeader transportSequenceNumber = { - GetDefaultCodecPreferences().UseAudioTransportCC() - ? JsepMediaType::kAudioVideo - : JsepMediaType::kVideo, - SdpDirectionAttribute::Direction::kSendrecv, - webrtc::RtpExtension::kTransportSequenceNumberUri}; - aRtpExtensions.push_back(std::move(transportSequenceNumber)); + JsepMediaType transportSequenceNumberMediaType = JsepMediaType::kNone; + if (aPrefs.UseAudioTransportCC() && aPrefs.UseTransportCC()) { + transportSequenceNumberMediaType = JsepMediaType::kAudioVideo; + } else if (aPrefs.UseAudioTransportCC()) { + transportSequenceNumberMediaType = JsepMediaType::kAudio; + } else if (aPrefs.UseTransportCC()) { + transportSequenceNumberMediaType = JsepMediaType::kVideo; + } + if (transportSequenceNumberMediaType != JsepMediaType::kNone) { + RtpExtensionHeader transportSequenceNumber = { + transportSequenceNumberMediaType, + SdpDirectionAttribute::Direction::kSendrecv, + nsLiteralCString(webrtc::RtpExtension::kTransportSequenceNumberUri)}; + aRtpExtensions->AppendElement(std::move(transportSequenceNumber)); + } } +/* static */ void PeerConnectionImpl::GetCapabilities( const nsAString& aKind, dom::Nullable& aResult, sdp::Direction aDirection) { - std::vector> codecs; - std::vector headers; + DefaultCodecPreferences prefs; + AutoTArray, 16> codecs; + AutoTArray headers; auto mediaType = JsepMediaType::kNone; if (aKind.EqualsASCII("video")) { @@ -2188,16 +2180,16 @@ void PeerConnectionImpl::GetCapabilities( // RTX is supported by default, so I am not sure if that was necessary. // When it has been explicitly disabled by pref, is there a point in // forcing it here? - GetDefaultVideoCodecs(codecs, OverrideRtxPreference::NoOverride); + EnumerateDefaultVideoCodecs(&codecs, prefs); mediaType = JsepMediaType::kVideo; } else if (aKind.EqualsASCII("audio")) { - GetDefaultAudioCodecs(codecs); + EnumerateDefaultAudioCodecs(&codecs, prefs); mediaType = JsepMediaType::kAudio; } else { return; } - GetDefaultRtpExtensions(headers); + GetDefaultRtpExtensions(prefs, &headers); bool haveAddedRtx = false; @@ -2250,28 +2242,6 @@ void PeerConnectionImpl::GetCapabilities( } } -void PeerConnectionImpl::SetupPreferredCodecs( - std::vector>& aPreferredCodecs) { - GetDefaultVideoCodecs(aPreferredCodecs, OverrideRtxPreference::NoOverride); - GetDefaultAudioCodecs(aPreferredCodecs); -} - -void PeerConnectionImpl::SetupPreferredRtpExtensions( - std::vector& aPreferredheaders) { - GetDefaultRtpExtensions(aPreferredheaders); - - if (!Preferences::GetBool("media.navigator.video.use_transport_cc", false)) { - aPreferredheaders.erase( - std::remove_if( - aPreferredheaders.begin(), aPreferredheaders.end(), - [&](const RtpExtensionHeader& header) { - return header.extensionname == - webrtc::RtpExtension::kTransportSequenceNumberUri; - }), - aPreferredheaders.end()); - } -} - nsresult PeerConnectionImpl::CalculateFingerprint( const nsACString& algorithm, std::vector* fingerprint) const { DtlsDigest digest(algorithm); diff --git a/dom/media/webrtc/jsapi/PeerConnectionImpl.h b/dom/media/webrtc/jsapi/PeerConnectionImpl.h index 8d8894e47386..dfda4b5b9c43 100644 --- a/dom/media/webrtc/jsapi/PeerConnectionImpl.h +++ b/dom/media/webrtc/jsapi/PeerConnectionImpl.h @@ -175,7 +175,7 @@ class PeerConnectionImpl final struct RtpExtensionHeader { JsepMediaType mMediaType; SdpDirectionAttribute::Direction direction; - std::string extensionname; + nsCString extensionname; }; JSObject* WrapObject(JSContext* aCx, @@ -185,11 +185,6 @@ class PeerConnectionImpl final static already_AddRefed Constructor( const dom::GlobalObject& aGlobal); - static DefaultCodecPreferences GetDefaultCodecPreferences( - const OverrideRtxPreference aOverrideRtxPreference = - OverrideRtxPreference::NoOverride) { - return DefaultCodecPreferences(aOverrideRtxPreference); - } // DataConnection observers void NotifyDataChannel(already_AddRefed aChannel, const nsACString& aLabel, bool aOrdered, @@ -578,24 +573,13 @@ class PeerConnectionImpl final bool LongTermStatsIsDisabled() const { return mDisableLongTermStats; } - static void GetDefaultVideoCodecs( - std::vector>& aSupportedCodecs, - const OverrideRtxPreference aOverrideRtxPreference); - - static void GetDefaultAudioCodecs( - std::vector>& aSupportedCodecs); - static void GetDefaultRtpExtensions( - std::vector& aRtpExtensions); + const JsepCodecPreferences& aPrefs, + nsTArray* aRtpExtensions); static void GetCapabilities(const nsAString& aKind, dom::Nullable& aResult, sdp::Direction aDirection); - static void SetupPreferredCodecs( - std::vector>& aPreferredCodecs); - - static void SetupPreferredRtpExtensions( - std::vector& aPreferredheaders); void BreakCycles(); @@ -872,6 +856,10 @@ class PeerConnectionImpl final RefPtr mCall; + public: + const DefaultCodecPreferences mPrefs; + + private: // See Bug 1642419, this can be removed when all sites are working with RTX. bool mRtxIsAllowed = true; diff --git a/dom/media/webrtc/jsapi/RTCRtpReceiver.cpp b/dom/media/webrtc/jsapi/RTCRtpReceiver.cpp index 80d7daa60729..93dc097d4656 100644 --- a/dom/media/webrtc/jsapi/RTCRtpReceiver.cpp +++ b/dom/media/webrtc/jsapi/RTCRtpReceiver.cpp @@ -839,7 +839,7 @@ void RTCRtpReceiver::UpdateVideoConduit() { if (GetJsepTransceiver().HasBundleLevel() && (!GetJsepTransceiver().mRecvTrack.GetNegotiatedDetails() || !GetJsepTransceiver().mRecvTrack.GetNegotiatedDetails()->GetExt( - webrtc::RtpExtension::kMidUri))) { + nsLiteralCString(webrtc::RtpExtension::kMidUri)))) { mCallThread->Dispatch( NewRunnableMethod("VideoSessionConduit::DisableSsrcChanges", conduit, &VideoSessionConduit::DisableSsrcChanges)); @@ -897,7 +897,7 @@ void RTCRtpReceiver::UpdateAudioConduit() { if (GetJsepTransceiver().HasBundleLevel() && (!GetJsepTransceiver().mRecvTrack.GetNegotiatedDetails() || !GetJsepTransceiver().mRecvTrack.GetNegotiatedDetails()->GetExt( - webrtc::RtpExtension::kMidUri))) { + nsLiteralCString(webrtc::RtpExtension::kMidUri)))) { mCallThread->Dispatch( NewRunnableMethod("AudioSessionConduit::DisableSsrcChanges", conduit, &AudioSessionConduit::DisableSsrcChanges)); @@ -1003,7 +1003,7 @@ void RTCRtpReceiver::SyncFromJsep(const JsepTransceiver& aJsepTransceiver) { } void RTCRtpReceiver::SyncToJsep(JsepTransceiver& aJsepTransceiver) const { - if (!mTransceiver->GetPreferredCodecs().empty()) { + if (!mTransceiver->GetPreferredCodecs().IsEmpty()) { aJsepTransceiver.mRecvTrack.PopulateCodecs( mTransceiver->GetPreferredCodecs(), mTransceiver->GetPreferredCodecsInUse()); diff --git a/dom/media/webrtc/jsapi/RTCRtpSender.cpp b/dom/media/webrtc/jsapi/RTCRtpSender.cpp index cdc665c58d39..4685bd109897 100644 --- a/dom/media/webrtc/jsapi/RTCRtpSender.cpp +++ b/dom/media/webrtc/jsapi/RTCRtpSender.cpp @@ -821,7 +821,7 @@ already_AddRefed RTCRtpSender::SetParameters( // Coverts a list of JsepCodecDescription to a list of // dom::RTCRtpCodecParameters auto toDomCodecParametersList = - [](const std::vector>& aJsepCodec) + [](const nsTArray>& aJsepCodec) -> dom::Sequence { dom::Sequence codecs; for (const auto& codec : aJsepCodec) { @@ -896,15 +896,11 @@ already_AddRefed RTCRtpSender::SetParameters( if (choosableCodecs.Length() == 0) { // If choosableCodecs is still an empty list, set choosableCodecs to the // list of implemented send codecs for transceiver's kind. - std::vector> codecs; + AutoTArray, 16> codecs; if (mTransceiver->IsVideo()) { - auto useRtx = - Preferences::GetBool("media.peerconnection.video.use_rtx", false) - ? OverrideRtxPreference::OverrideWithEnabled - : OverrideRtxPreference::OverrideWithDisabled; - PeerConnectionImpl::GetDefaultVideoCodecs(codecs, useRtx); + EnumerateDefaultVideoCodecs(&codecs, mPc->mPrefs); } else { - PeerConnectionImpl::GetDefaultAudioCodecs(codecs); + EnumerateDefaultAudioCodecs(&codecs, mPc->mPrefs); } choosableCodecs = toDomCodecParametersList(codecs); } diff --git a/dom/media/webrtc/jsapi/RTCRtpTransceiver.cpp b/dom/media/webrtc/jsapi/RTCRtpTransceiver.cpp index 976c4ace60cc..74c75740a136 100644 --- a/dom/media/webrtc/jsapi/RTCRtpTransceiver.cpp +++ b/dom/media/webrtc/jsapi/RTCRtpTransceiver.cpp @@ -980,8 +980,7 @@ void RTCRtpTransceiver::ToDomHeaderExtensions( aDetails.ForEachRTPHeaderExtension( [&](const SdpExtmapAttributeList::Extmap& aExtmap) { RTCRtpHeaderExtensionParameters ext; - ext.mUri.Construct( - NS_ConvertUTF8toUTF16(aExtmap.extensionname.c_str())); + ext.mUri.Construct(NS_ConvertUTF8toUTF16(aExtmap.extensionname)); ext.mId.Construct(aExtmap.entry); // We do not negotiate RFC 6904 encrypted header extensions. When we do, // this should report encrypted=true with the inner (unwrapped) URI. @@ -1009,7 +1008,6 @@ void RTCRtpTransceiver::SetCodecPreferences( nsTArray aCodecsFiltered; OverrideRtxPreference rtxOverride = OverrideRtxPreference::OverrideWithDisabled; - ; bool useableCodecs = false; // kind = transciever's kind. @@ -1076,13 +1074,17 @@ void RTCRtpTransceiver::SetCodecPreferences( // If we passed an empty list, we should restore the default list, including // RTX - mPreferredCodecs.clear(); - std::vector> defaultCodecs; + mPreferredCodecs.Clear(); + AutoTArray, 16> defaultCodecs; if (kind.EqualsLiteral("video")) { - PeerConnectionImpl::GetDefaultVideoCodecs(defaultCodecs, rtxOverride); + EnumerateDefaultVideoCodecs( + &defaultCodecs, + DefaultCodecPreferencesWithRtxOverride(mPc->mPrefs, rtxOverride)); } else if (kind.EqualsLiteral("audio")) { - PeerConnectionImpl::GetDefaultAudioCodecs(defaultCodecs); + EnumerateDefaultAudioCodecs( + &defaultCodecs, + DefaultCodecPreferencesWithRtxOverride(mPc->mPrefs, rtxOverride)); } if (!aCodecsFiltered.IsEmpty()) { @@ -1122,13 +1124,13 @@ void RTCRtpTransceiver::SetCodecPreferences( if ((mimeType.Find(defaultCodec->mName) != kNotFound) && (inputCodec.mClockRate == defaultCodec->mClock) && channelsMatch && sdpFmtpLinesMatch) { - mPreferredCodecs.emplace_back(defaultCodec->Clone()); + mPreferredCodecs.EmplaceBack(defaultCodec->Clone()); break; } } } } else { - mPreferredCodecs.swap(defaultCodecs); + mPreferredCodecs = std::move(defaultCodecs); mPreferredCodecsInUse = false; } } diff --git a/dom/media/webrtc/jsapi/RTCRtpTransceiver.h b/dom/media/webrtc/jsapi/RTCRtpTransceiver.h index bc45bc189052..e17c1dfe4ae3 100644 --- a/dom/media/webrtc/jsapi/RTCRtpTransceiver.h +++ b/dom/media/webrtc/jsapi/RTCRtpTransceiver.h @@ -211,7 +211,7 @@ class RTCRtpTransceiver : public nsISupports, public nsWrapperCache { Canonical& CanonicalMid() { return mMid; } Canonical& CanonicalSyncGroup() { return mSyncGroup; } - const std::vector>& GetPreferredCodecs() { + const nsTArray>& GetPreferredCodecs() { return mPreferredCodecs; } @@ -278,7 +278,7 @@ class RTCRtpTransceiver : public nsISupports, public nsWrapperCache { // Preferred codecs to be negotiated set by calling // setCodecPreferences. - std::vector> mPreferredCodecs; + nsTArray> mPreferredCodecs; // Identifies if a preferred list and order of codecs is to be used. // This is true if setCodecPreferences was called successfully and passed // codecs (not empty). diff --git a/dom/media/webrtc/jsep/JsepCodecDescription.h b/dom/media/webrtc/jsep/JsepCodecDescription.h index db2015019ab4..dfd6e6b9c853 100644 --- a/dom/media/webrtc/jsep/JsepCodecDescription.h +++ b/dom/media/webrtc/jsep/JsepCodecDescription.h @@ -29,6 +29,7 @@ class JsepCodecPreferences { virtual bool AV1Enabled() const = 0; virtual bool AV1Preferred() const = 0; virtual bool H264Enabled() const = 0; + virtual bool HardwareH264Enabled() const = 0; virtual bool SoftwareH264Enabled() const = 0; virtual bool SendingH264PacketizationModeZeroSupported() const = 0; virtual bool H264BaselineDisabled() const = 0; diff --git a/dom/media/webrtc/jsep/JsepSession.h b/dom/media/webrtc/jsep/JsepSession.h index 6ed05890103f..1afe6f73bc7f 100644 --- a/dom/media/webrtc/jsep/JsepSession.h +++ b/dom/media/webrtc/jsep/JsepSession.h @@ -15,6 +15,7 @@ #include "mozilla/UniquePtr.h" #include "mozilla/dom/PeerConnectionObserverEnumsBinding.h" #include "nsError.h" +#include "nsTArray.h" #include "sdp/Sdp.h" namespace mozilla { @@ -108,29 +109,24 @@ class JsepSession { const std::vector& value) = 0; virtual nsresult AddRtpExtension( - JsepMediaType mediaType, const std::string& extensionName, + JsepMediaType mediaType, const nsACString& extensionName, SdpDirectionAttribute::Direction direction) = 0; virtual nsresult AddAudioRtpExtension( - const std::string& extensionName, + const nsACString& extensionName, SdpDirectionAttribute::Direction direction) = 0; virtual nsresult AddVideoRtpExtension( - const std::string& extensionName, + const nsACString& extensionName, SdpDirectionAttribute::Direction direction) = 0; virtual nsresult AddAudioVideoRtpExtension( - const std::string& extensionName, + const nsACString& extensionName, SdpDirectionAttribute::Direction direction) = 0; - // Kinda gross to be locking down the data structure type like this, but - // returning by value is problematic due to the lack of stl move semantics in - // our build config, since we can't use UniquePtr in the container. The - // alternative is writing a raft of accessor functions that allow arbitrary - // manipulation (which will be unwieldy), or allowing functors to be injected - // that manipulate the data structure (still pretty unwieldy). - virtual std::vector>& Codecs() = 0; + virtual Span> Codecs() = 0; template void ForEachCodec(UnaryFunction& function) { - std::for_each(Codecs().begin(), Codecs().end(), function); + Span codecs = Codecs(); + std::for_each(codecs.begin(), codecs.end(), function); for (auto& transceiver : GetTransceivers()) { transceiver.mSendTrack.ForEachCodec(function); transceiver.mRecvTrack.ForEachCodec(function); @@ -139,7 +135,8 @@ class JsepSession { template void SortCodecs(BinaryPredicate& sorter) { - std::stable_sort(Codecs().begin(), Codecs().end(), sorter); + Span codecs = Codecs(); + std::stable_sort(codecs.begin(), codecs.end(), sorter); for (auto& transceiver : GetTransceivers()) { transceiver.mSendTrack.SortCodecs(sorter); transceiver.mRecvTrack.SortCodecs(sorter); @@ -312,7 +309,7 @@ class JsepSession { } virtual void SetDefaultCodecs( - const std::vector>& aPreferredCodecs) = 0; + const nsTArray>& aPreferredCodecs) = 0; // See Bug 1642419, this can be removed when all sites are working with RTX. void SetRtxIsAllowed(bool aRtxIsAllowed) { mRtxIsAllowed = aRtxIsAllowed; } diff --git a/dom/media/webrtc/jsep/JsepSessionImpl.cpp b/dom/media/webrtc/jsep/JsepSessionImpl.cpp index 412ef79db678..4ac2b9b96b32 100644 --- a/dom/media/webrtc/jsep/JsepSessionImpl.cpp +++ b/dom/media/webrtc/jsep/JsepSessionImpl.cpp @@ -77,7 +77,7 @@ JsepSessionImpl::JsepSessionImpl(const JsepSessionImpl& aOrig) mSdpHelper(&mLastError), mParser(MakeUnique()) { for (const auto& codec : aOrig.mSupportedCodecs) { - mSupportedCodecs.emplace_back(codec->Clone()); + mSupportedCodecs.EmplaceBack(codec->Clone()); } } @@ -209,7 +209,7 @@ nsresult JsepSessionImpl::AddDtlsFingerprint( } nsresult JsepSessionImpl::AddRtpExtension( - JsepMediaType mediaType, const std::string& extensionName, + JsepMediaType mediaType, const nsACString& extensionName, SdpDirectionAttribute::Direction direction) { mLastError.clear(); @@ -233,26 +233,27 @@ nsresult JsepSessionImpl::AddRtpExtension( mediaType, {freeEntry, direction, // do we want to specify direction? - direction != SdpDirectionAttribute::kSendrecv, extensionName, ""}}; + direction != SdpDirectionAttribute::kSendrecv, nsCString(extensionName), + ""_ns}}; mRtpExtensions.push_back(std::move(extMediaType)); return NS_OK; } nsresult JsepSessionImpl::AddAudioRtpExtension( - const std::string& extensionName, + const nsACString& extensionName, SdpDirectionAttribute::Direction direction) { return AddRtpExtension(JsepMediaType::kAudio, extensionName, direction); } nsresult JsepSessionImpl::AddVideoRtpExtension( - const std::string& extensionName, + const nsACString& extensionName, SdpDirectionAttribute::Direction direction) { return AddRtpExtension(JsepMediaType::kVideo, extensionName, direction); } nsresult JsepSessionImpl::AddAudioVideoRtpExtension( - const std::string& extensionName, + const nsACString& extensionName, SdpDirectionAttribute::Direction direction) { return AddRtpExtension(JsepMediaType::kAudioVideo, extensionName, direction); } @@ -477,20 +478,22 @@ std::vector JsepSessionImpl::GetRtpExtensions( if (includes_send && StaticPrefs::media_peerconnection_video_use_dd() && msection.GetAttributeList().HasAttribute( SdpAttribute::kSimulcastAttribute)) { - AddVideoRtpExtension(webrtc::RtpExtension::kDependencyDescriptorUri, - SdpDirectionAttribute::kSendonly); + AddVideoRtpExtension( + nsLiteralCString(webrtc::RtpExtension::kDependencyDescriptorUri), + SdpDirectionAttribute::kSendonly); } if (msection.GetAttributeList().HasAttribute( SdpAttribute::kRidAttribute)) { // We need RID support // TODO: Would it be worth checking that the direction is sane? - AddVideoRtpExtension(webrtc::RtpExtension::kRidUri, + AddVideoRtpExtension(nsLiteralCString(webrtc::RtpExtension::kRidUri), SdpDirectionAttribute::kSendonly); if (mRtxIsAllowed && Preferences::GetBool("media.peerconnection.video.use_rtx", false)) { - AddVideoRtpExtension(webrtc::RtpExtension::kRepairedRidUri, - SdpDirectionAttribute::kSendonly); + AddVideoRtpExtension( + nsLiteralCString(webrtc::RtpExtension::kRepairedRidUri), + SdpDirectionAttribute::kSendonly); } } break; @@ -2286,11 +2289,11 @@ nsresult JsepSessionImpl::SetupIds() { } void JsepSessionImpl::SetDefaultCodecs( - const std::vector>& aPreferredCodecs) { - mSupportedCodecs.clear(); + const nsTArray>& aPreferredCodecs) { + mSupportedCodecs.Clear(); for (const auto& codec : aPreferredCodecs) { - mSupportedCodecs.emplace_back(codec->Clone()); + mSupportedCodecs.EmplaceBack(codec->Clone()); } } diff --git a/dom/media/webrtc/jsep/JsepSessionImpl.h b/dom/media/webrtc/jsep/JsepSessionImpl.h index b442d5513ab1..95bf60344a9f 100644 --- a/dom/media/webrtc/jsep/JsepSessionImpl.h +++ b/dom/media/webrtc/jsep/JsepSessionImpl.h @@ -48,7 +48,7 @@ class JsepSessionCopyableStuff { size_t mTransportIdCounter = 0; std::vector mRtpExtensions; std::set mExtmapEntriesEverUsed; - std::map mExtmapEntriesEverNegotiated; + std::map mExtmapEntriesEverNegotiated; std::string mDefaultRemoteStreamId; std::string mCNAME; // Used to prevent duplicate local SSRCs. Not used to prevent local/remote or @@ -96,25 +96,25 @@ class JsepSessionImpl : public JsepSession, public JsepSessionCopyableStuff { const nsACString& algorithm, const std::vector& value) override; virtual nsresult AddRtpExtension( - JsepMediaType mediaType, const std::string& extensionName, + JsepMediaType mediaType, const nsACString& extensionName, SdpDirectionAttribute::Direction direction) override; virtual nsresult AddAudioRtpExtension( - const std::string& extensionName, + const nsACString& extensionName, SdpDirectionAttribute::Direction direction = SdpDirectionAttribute::Direction::kSendrecv) override; virtual nsresult AddVideoRtpExtension( - const std::string& extensionName, + const nsACString& extensionName, SdpDirectionAttribute::Direction direction = SdpDirectionAttribute::Direction::kSendrecv) override; virtual nsresult AddAudioVideoRtpExtension( - const std::string& extensionName, + const nsACString& extensionName, SdpDirectionAttribute::Direction direction = SdpDirectionAttribute::Direction::kSendrecv) override; - virtual std::vector>& Codecs() override { - return mSupportedCodecs; + virtual Span> Codecs() override { + return Span(mSupportedCodecs); } virtual Result CreateOffer(const JsepOfferOptions& options, @@ -181,9 +181,8 @@ class JsepSessionImpl : public JsepSession, public JsepSessionCopyableStuff { virtual bool CheckNegotiationNeeded() const override; - virtual void SetDefaultCodecs( - const std::vector>& aPreferredCodecs) - override; + virtual void SetDefaultCodecs(const nsTArray>& + aPreferredCodecs) override; private: friend class JsepSessionTest; @@ -281,7 +280,7 @@ class JsepSessionImpl : public JsepSession, public JsepSessionCopyableStuff { UniquePtr mCurrentRemoteDescription; UniquePtr mPendingLocalDescription; UniquePtr mPendingRemoteDescription; - std::vector> mSupportedCodecs; + nsTArray> mSupportedCodecs; SdpHelper mSdpHelper; UniquePtr mParser; }; diff --git a/dom/media/webrtc/jsep/JsepTrack.cpp b/dom/media/webrtc/jsep/JsepTrack.cpp index 884d0694c82b..b3abae598139 100644 --- a/dom/media/webrtc/jsep/JsepTrack.cpp +++ b/dom/media/webrtc/jsep/JsepTrack.cpp @@ -104,11 +104,11 @@ std::vector JsepTrack::GetRtxSsrcs() const { } void JsepTrack::PopulateCodecs( - const std::vector>& prototype, + const nsTArray>& aPreferredCodecs, bool aUsePreferredCodecsOrder) { mPrototypeCodecs.clear(); mUsePreferredCodecsOrder = aUsePreferredCodecsOrder; - for (const auto& prototypeCodec : prototype) { + for (const auto& prototypeCodec : aPreferredCodecs) { if (prototypeCodec->Type() == mType) { mPrototypeCodecs.emplace_back(prototypeCodec->Clone()); mPrototypeCodecs.back()->mDirection = mDirection; diff --git a/dom/media/webrtc/jsep/JsepTrack.h b/dom/media/webrtc/jsep/JsepTrack.h index 3589c84894e8..92b192a74a41 100644 --- a/dom/media/webrtc/jsep/JsepTrack.h +++ b/dom/media/webrtc/jsep/JsepTrack.h @@ -53,8 +53,8 @@ class JsepTrackNegotiatedDetails { } const SdpExtmapAttributeList::Extmap* GetExt( - const std::string& ext_name) const { - auto it = mExtmap.find(ext_name); + const nsACString& ext_name) const { + auto it = mExtmap.find(nsCString(ext_name)); if (it != mExtmap.end()) { return &it->second; } @@ -76,7 +76,7 @@ class JsepTrackNegotiatedDetails { private: friend class JsepTrack; - std::map mExtmap; + std::map mExtmap; std::vector> mEncodings; uint32_t mTias; // bits per second RtpRtcpConfig mRtpRtcpConf; @@ -178,11 +178,11 @@ class JsepTrack { bool GetReceptive() const { return mReceptive; } void PopulatePreferredCodecs( - const std::vector>& aPreferredCodecs, + const nsTArray>& aPreferredCodecs, bool aUsePreferredCodecsOrder); virtual void PopulateCodecs( - const std::vector>& prototype, + const nsTArray>& prototype, bool aUsePreferredCodecsOrder = false); template diff --git a/dom/media/webrtc/libwebrtcglue/WebrtcMediaDataDecoderCodec.cpp b/dom/media/webrtc/libwebrtcglue/WebrtcMediaDataDecoderCodec.cpp index 07450dbfbba0..8fdacab0087a 100644 --- a/dom/media/webrtc/libwebrtcglue/WebrtcMediaDataDecoderCodec.cpp +++ b/dom/media/webrtc/libwebrtcglue/WebrtcMediaDataDecoderCodec.cpp @@ -28,8 +28,9 @@ bool WebrtcMediaDataDecoder::IsCodecEnabled(webrtc::VideoCodecType aCodec) { return StaticPrefs::media_navigator_mediadatadecoder_vpx_enabled(); case webrtc::VideoCodecType::kVideoCodecH264: return StaticPrefs::media_navigator_mediadatadecoder_h264_enabled(); - case webrtc::VideoCodecType::kVideoCodecGeneric: case webrtc::VideoCodecType::kVideoCodecAV1: + return StaticPrefs::media_navigator_mediadatadecoder_av1_enabled(); + case webrtc::VideoCodecType::kVideoCodecGeneric: case webrtc::VideoCodecType::kVideoCodecH265: return false; } diff --git a/dom/media/webrtc/sdp/RsdparsaSdpAttributeList.cpp b/dom/media/webrtc/sdp/RsdparsaSdpAttributeList.cpp index faae1dedb3a3..f67b84d0b2dc 100644 --- a/dom/media/webrtc/sdp/RsdparsaSdpAttributeList.cpp +++ b/dom/media/webrtc/sdp/RsdparsaSdpAttributeList.cpp @@ -1188,7 +1188,7 @@ void RsdparsaSdpAttributeList::LoadExtmap(RustAttributeList* attributeList) { } auto extmaps = MakeUnique(); for (const auto& rustExtmap : rustExtmaps) { - std::string name(convertStringView(rustExtmap.url)); + nsCString name(convertStringView(rustExtmap.url)); SdpDirectionAttribute::Direction direction; bool directionSpecified = rustExtmap.direction_specified; switch (rustExtmap.direction) { @@ -1205,7 +1205,7 @@ void RsdparsaSdpAttributeList::LoadExtmap(RustAttributeList* attributeList) { direction = SdpDirectionAttribute::kInactive; break; } - std::string extensionAttributes( + nsCString extensionAttributes( convertStringView(rustExtmap.extension_attributes)); extmaps->PushEntry(rustExtmap.id, direction, directionSpecified, name, extensionAttributes); diff --git a/dom/media/webrtc/sdp/SdpAttribute.cpp b/dom/media/webrtc/sdp/SdpAttribute.cpp index 5b150bbbd80e..5a5c0442efbc 100644 --- a/dom/media/webrtc/sdp/SdpAttribute.cpp +++ b/dom/media/webrtc/sdp/SdpAttribute.cpp @@ -90,7 +90,7 @@ void SdpExtmapAttributeList::Serialize(std::ostream& os) const { os << "/" << i->direction; } os << " " << i->extensionname; - if (i->extensionattributes.length()) { + if (i->extensionattributes.Length()) { os << " " << i->extensionattributes; } os << CRLF; diff --git a/dom/media/webrtc/sdp/SdpAttribute.h b/dom/media/webrtc/sdp/SdpAttribute.h index aea6bad03509..220471d1d8c2 100644 --- a/dom/media/webrtc/sdp/SdpAttribute.h +++ b/dom/media/webrtc/sdp/SdpAttribute.h @@ -340,17 +340,17 @@ class SdpExtmapAttributeList : public SdpAttribute { uint16_t entry; SdpDirectionAttribute::Direction direction; bool direction_specified; - std::string extensionname; - std::string extensionattributes; + nsCString extensionname; + nsCString extensionattributes; }; void PushEntry(const uint16_t entry, const SdpDirectionAttribute::Direction direction, const bool direction_specified, - const std::string& extensionname, - const std::string& extensionattributes = "") { - Extmap value = {entry, direction, direction_specified, extensionname, - extensionattributes}; + const nsACString& extensionname, + const nsACString& extensionattributes = ""_ns) { + Extmap value = {entry, direction, direction_specified, + nsCString(extensionname), nsCString(extensionattributes)}; mExtmaps.push_back(std::move(value)); } diff --git a/dom/media/webrtc/sdp/SipccSdpAttributeList.cpp b/dom/media/webrtc/sdp/SipccSdpAttributeList.cpp index e9f004a9cd9e..beeba36ccae1 100644 --- a/dom/media/webrtc/sdp/SipccSdpAttributeList.cpp +++ b/dom/media/webrtc/sdp/SipccSdpAttributeList.cpp @@ -903,7 +903,8 @@ void SipccSdpAttributeList::LoadExtmap(sdp_t* sdp, const uint16_t level, } extmaps->PushEntry(extmap->id, dir, extmap->media_direction_specified, - extmap->uri, extmap->extension_attributes); + nsDependentCString(extmap->uri), + nsDependentCString(extmap->extension_attributes)); } if (!extmaps->mExtmaps.empty()) { diff --git a/dom/performance/Performance.cpp b/dom/performance/Performance.cpp index 34dbabbfe6e2..1a01ab1ee10e 100644 --- a/dom/performance/Performance.cpp +++ b/dom/performance/Performance.cpp @@ -640,7 +640,11 @@ Maybe> Performance::GetTimeStampsForMarker( // to the file handle. Otherwise, return false and sMarkerFile // is NULL. static bool MaybeOpenMarkerFile() { - if (!getenv("MOZ_USE_PERFORMANCE_MARKER_FILE")) { + // Read the environment only once. This runs on every performance.measure() + // call, and getenv is slow on Windows. + static const bool sMarkerFileEnabled = + !!getenv("MOZ_USE_PERFORMANCE_MARKER_FILE"); + if (!sMarkerFileEnabled) { return false; } diff --git a/dom/security/test/csp/test_service_worker.html b/dom/security/test/csp/test_service_worker.html index 1c274990f88c..2090ee8d6b50 100644 --- a/dom/security/test/csp/test_service_worker.html +++ b/dom/security/test/csp/test_service_worker.html @@ -38,7 +38,6 @@ onload = function() { ["dom.serviceWorkers.exemptFromPerDomainMax", true], ["dom.serviceWorkers.enabled", true], ["dom.serviceWorkers.testing.enabled", true], - ["privacy.partition.serviceWorkers", true], ]}, loadNextTest); } diff --git a/dom/serviceworkers/ServiceWorkerInterceptController.cpp b/dom/serviceworkers/ServiceWorkerInterceptController.cpp index a36150eaf01b..0ccd15ac2b8b 100644 --- a/dom/serviceworkers/ServiceWorkerInterceptController.cpp +++ b/dom/serviceworkers/ServiceWorkerInterceptController.cpp @@ -7,7 +7,6 @@ #include "ServiceWorkerManager.h" #include "mozilla/BasePrincipal.h" #include "mozilla/StaticPrefs_dom.h" -#include "mozilla/StaticPrefs_privacy.h" #include "mozilla/StorageAccess.h" #include "mozilla/StoragePrincipalHelper.h" #include "mozilla/dom/CanonicalBrowsingContext.h" @@ -111,10 +110,7 @@ ServiceWorkerInterceptController::ShouldPrepareForIntercept( nsCOMPtr principal; nsresult rv = StoragePrincipalHelper::GetPrincipal( - aChannel, - StaticPrefs::privacy_partition_serviceWorkers() - ? StoragePrincipalHelper::eForeignPartitionedPrincipal - : StoragePrincipalHelper::eRegularPrincipal, + aChannel, StoragePrincipalHelper::eForeignPartitionedPrincipal, getter_AddRefs(principal)); NS_ENSURE_SUCCESS(rv, rv); @@ -143,7 +139,6 @@ ServiceWorkerInterceptController::ShouldPrepareForIntercept( (storageAccess == StorageAccess::ePrivateBrowsing && StaticPrefs::dom_serviceWorkers_privateBrowsing_enabled()) || (ShouldPartitionStorage(storageAccess) && - StaticPrefs::privacy_partition_serviceWorkers() && StoragePartitioningEnabled(storageAccess, cookieJarSettings) && (!principal->GetIsInPrivateBrowsing() || StaticPrefs::dom_serviceWorkers_privateBrowsing_enabled())); diff --git a/dom/serviceworkers/ServiceWorkerManager.cpp b/dom/serviceworkers/ServiceWorkerManager.cpp index b8b214c80c75..fe4af86c7bda 100644 --- a/dom/serviceworkers/ServiceWorkerManager.cpp +++ b/dom/serviceworkers/ServiceWorkerManager.cpp @@ -34,7 +34,6 @@ #include "mozilla/Result.h" #include "mozilla/ScopeExit.h" #include "mozilla/StaticPrefs_extensions.h" -#include "mozilla/StaticPrefs_privacy.h" #include "mozilla/StoragePrincipalHelper.h" #include "mozilla/dom/BindingUtils.h" #include "mozilla/dom/ClientHandle.h" @@ -931,7 +930,7 @@ RefPtr ServiceWorkerManager::Register( auto lifetime = DetermineLifetimeForClient(aClientInfo); uint16_t ipAddressSpace = 0; - auto policyContainerArgs = aClientInfo.GetPolicyContainerArgs(); + const auto& policyContainerArgs = aClientInfo.GetPolicyContainerArgs(); if (policyContainerArgs.isSome()) { ipAddressSpace = static_cast(policyContainerArgs->ipAddressSpace()); @@ -1871,7 +1870,7 @@ nsresult ServiceWorkerManager::PrincipalInfoToScopeKey( return NS_ERROR_FAILURE; } - auto content = aPrincipalInfo.get_ContentPrincipalInfo(); + const auto& content = aPrincipalInfo.get_ContentPrincipalInfo(); nsAutoCString suffix; content.attrs().CreateSuffix(suffix); @@ -2247,11 +2246,9 @@ void ServiceWorkerManager::DispatchFetchEvent(nsIInterceptedChannel* aChannel, // non-subresource request means the URI contains the principal OriginAttributes attrs = loadInfo->GetOriginAttributes(); - if (StaticPrefs::privacy_partition_serviceWorkers()) { - StoragePrincipalHelper::GetOriginAttributes( - internalChannel, attrs, - StoragePrincipalHelper::eForeignPartitionedPrincipal); - } + StoragePrincipalHelper::GetOriginAttributes( + internalChannel, attrs, + StoragePrincipalHelper::eForeignPartitionedPrincipal); nsCOMPtr principal = BasePrincipal::CreateContentPrincipal(uri, attrs); @@ -2435,10 +2432,6 @@ bool ServiceWorkerManager::IsAvailable(nsIPrincipal* aPrincipal, nsIURI* aURI, nsCOMPtr loadInfo = aChannel->LoadInfo(); if (storageAccess <= StorageAccess::eDeny) { - if (!StaticPrefs::privacy_partition_serviceWorkers()) { - return false; - } - nsCOMPtr cookieJarSettings; loadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings)); diff --git a/dom/serviceworkers/ServiceWorkerUtils.cpp b/dom/serviceworkers/ServiceWorkerUtils.cpp index ac848afdaf9b..18e846f67b85 100644 --- a/dom/serviceworkers/ServiceWorkerUtils.cpp +++ b/dom/serviceworkers/ServiceWorkerUtils.cpp @@ -22,6 +22,7 @@ #include "nsCOMPtr.h" #include "nsContentPolicyUtils.h" #include "nsIContentSecurityPolicy.h" +#include "nsIEnterprisePolicies.h" #include "nsIGlobalObject.h" #include "nsIPrincipal.h" #include "nsIURL.h" @@ -29,6 +30,31 @@ namespace mozilla::dom { +bool IsServiceWorkersDisabledByPolicy(nsIURI* aURI) { + if (!aURI) { + return false; + } + + // The policy service only controls http-like requests + if (!net::SchemeIsHttpOrHttps(aURI)) { + return false; + } + + nsCOMPtr policyService = + do_GetService("@mozilla.org/enterprisepolicies;1"); + if (!policyService) { + return false; + } + + bool isAllowed = true; + if (NS_FAILED(policyService->IsAllowedForURI("serviceworkers"_ns, aURI, + &isAllowed))) { + return false; + } + + return !isAllowed; +} + static bool IsServiceWorkersTestingEnabledInGlobal(JSObject* const aGlobal) { if (const nsCOMPtr innerWindow = Navigator::GetWindowFromGlobal(aGlobal)) { @@ -70,6 +96,16 @@ bool ServiceWorkersEnabled(JSContext* aCx, JSObject* aGlobal) { return false; } } + + // Check whether service workers are disabled by an enterprise policy. + if (const nsCOMPtr innerWindow = + Navigator::GetWindowFromGlobal(jsGlobal)) { + if (BrowsingContext* bc = innerWindow->GetBrowsingContext()) { + if (bc->Top()->ServiceWorkersDisabledByPolicy()) { + return false; + } + } + } } if (IsSecureContextOrObjectIsFromSecureContext(aCx, jsGlobal)) { @@ -106,7 +142,6 @@ bool ServiceWorkersStorageAllowedForGlobal(nsIGlobalObject* aGlobal) { (storageAllowed == StorageAccess::ePrivateBrowsing && StaticPrefs::dom_serviceWorkers_privateBrowsing_enabled()) || (ShouldPartitionStorage(storageAllowed) && - StaticPrefs::privacy_partition_serviceWorkers() && StoragePartitioningEnabled(storageAllowed, cookieJarSettings) && (!principal->GetIsInPrivateBrowsing() || StaticPrefs::dom_serviceWorkers_privateBrowsing_enabled()))); @@ -126,7 +161,6 @@ bool ServiceWorkersStorageAllowedForClient( (storageAllowed == StorageAccess::ePrivateBrowsing && StaticPrefs::dom_serviceWorkers_privateBrowsing_enabled()) || (ShouldPartitionStorage(storageAllowed) && - StaticPrefs::privacy_partition_serviceWorkers() && /* note: no call to StoragePartitioningEnabled here */ (!info.IsPrivateBrowsing() || StaticPrefs::dom_serviceWorkers_privateBrowsing_enabled()))); diff --git a/dom/serviceworkers/ServiceWorkerUtils.h b/dom/serviceworkers/ServiceWorkerUtils.h index 1ce12c3428b8..020e8e3ac1fb 100644 --- a/dom/serviceworkers/ServiceWorkerUtils.h +++ b/dom/serviceworkers/ServiceWorkerUtils.h @@ -59,6 +59,8 @@ bool ServiceWorkerRegistrationDataIsValid( void ServiceWorkerScopeIsValid(nsIPrincipal* aPrincipal, nsIURI* aScopeURI, ErrorResult& aRv); +bool IsServiceWorkersDisabledByPolicy(nsIURI* aURI); + // Performs key spec validation steps of // https://w3c.github.io/ServiceWorker/#start-register-algorithm and // https://w3c.github.io/ServiceWorker/#register-algorithm as well as CSP diff --git a/dom/svg/SVGElement.cpp b/dom/svg/SVGElement.cpp index ace82c2b663a..6399604bca95 100644 --- a/dom/svg/SVGElement.cpp +++ b/dom/svg/SVGElement.cpp @@ -1180,8 +1180,8 @@ bool SVGElement::UpdateDeclarationBlockFromTransform( ? aTransform->GetAnimValue() : aTransform->GetBaseValue(); // TODO: Maybe make SVGTransform use StyleTransformOperation directly? - for (size_t i = 0, len = transforms.Length(); i < len; ++i) { - SVGTransformToCSS(transforms[i], operations); + for (const auto& transform : transforms) { + SVGTransformToCSS(transform, operations); } } Servo_DeclarationBlock_SetTransform(&aBlock, eCSSProperty_transform, diff --git a/dom/svg/SVGFEColorMatrixElement.cpp b/dom/svg/SVGFEColorMatrixElement.cpp index 3ee08675e3a2..602d4432251b 100644 --- a/dom/svg/SVGFEColorMatrixElement.cpp +++ b/dom/svg/SVGFEColorMatrixElement.cpp @@ -90,7 +90,7 @@ FilterPrimitiveDescription SVGFEColorMatrixElement::GetPrimitiveDescription( atts.mValues.AppendElements(Span(identityMatrix)); } else { atts.mType = type; - if (values.Length()) { + if (!values.IsEmpty()) { atts.mValues.AppendElements(&values[0], values.Length()); } } diff --git a/dom/svg/SVGNumberList.h b/dom/svg/SVGNumberList.h index 2902583d911c..524973c91726 100644 --- a/dom/svg/SVGNumberList.h +++ b/dom/svg/SVGNumberList.h @@ -33,6 +33,7 @@ class SVGNumberList { friend class dom::DOMSVGNumber; friend class dom::DOMSVGNumberList; friend class SVGAnimatedNumberList; + using const_iterator = FallibleTArray::const_iterator; public: SVGNumberList() = default; @@ -59,12 +60,8 @@ class SVGNumberList { const float& operator[](uint32_t aIndex) const { return mNumbers[aIndex]; } - [[nodiscard]] FallibleTArray::const_iterator begin() const { - return mNumbers.begin(); - } - [[nodiscard]] FallibleTArray::const_iterator end() const { - return mNumbers.end(); - } + [[nodiscard]] const_iterator begin() const { return mNumbers.begin(); } + [[nodiscard]] const_iterator end() const { return mNumbers.end(); } bool operator==(const SVGNumberList& rhs) const { return mNumbers == rhs.mNumbers; diff --git a/dom/svg/SVGPathData.cpp b/dom/svg/SVGPathData.cpp index 35d2376bc683..57e7afc7cf86 100644 --- a/dom/svg/SVGPathData.cpp +++ b/dom/svg/SVGPathData.cpp @@ -675,7 +675,7 @@ void SVGPathData::GetMarkerPositioningData(Span aPath, } // Set the angle of the mark at the start of this segment: - if (aMarks->Length()) { + if (!aMarks->IsEmpty()) { SVGMark& mark = aMarks->LastElement(); if (!cmd.IsMove() && prevSeg && prevSeg->IsMove()) { // start of new subpath diff --git a/dom/svg/SVGPointList.h b/dom/svg/SVGPointList.h index f9f7b604bb16..e4497643c626 100644 --- a/dom/svg/SVGPointList.h +++ b/dom/svg/SVGPointList.h @@ -37,6 +37,7 @@ class SVGPointList { friend class SVGAnimatedPointList; friend class dom::DOMSVGPointList; friend class dom::DOMSVGPoint; + using const_iterator = FallibleTArray::const_iterator; public: SVGPointList() = default; @@ -63,12 +64,8 @@ class SVGPointList { const Point& operator[](uint32_t aIndex) const { return mItems[aIndex]; } - [[nodiscard]] FallibleTArray::const_iterator begin() const { - return mItems.begin(); - } - [[nodiscard]] FallibleTArray::const_iterator end() const { - return mItems.end(); - } + [[nodiscard]] const_iterator begin() const { return mItems.begin(); } + [[nodiscard]] const_iterator end() const { return mItems.end(); } bool operator==(const SVGPointList& rhs) const { // memcmp can be faster than |mItems == rhs.mItems| diff --git a/dom/svg/SVGTransformList.h b/dom/svg/SVGTransformList.h index 408b94cf7342..c31cdcf0f43c 100644 --- a/dom/svg/SVGTransformList.h +++ b/dom/svg/SVGTransformList.h @@ -29,6 +29,7 @@ class SVGTransformList { friend class SVGAnimatedTransformList; friend class dom::DOMSVGTransform; friend class dom::DOMSVGTransformList; + using const_iterator = FallibleTArray::const_iterator; public: SVGTransformList() = default; @@ -57,6 +58,9 @@ class SVGTransformList { return mItems[aIndex]; } + [[nodiscard]] const_iterator begin() const { return mItems.begin(); } + [[nodiscard]] const_iterator end() const { return mItems.end(); } + bool operator==(const SVGTransformList& rhs) const { return mItems == rhs.mItems; } diff --git a/dom/svg/SVGTransformListSMILType.cpp b/dom/svg/SVGTransformListSMILType.cpp index a210c3f3375b..327f8e47b453 100644 --- a/dom/svg/SVGTransformListSMILType.cpp +++ b/dom/svg/SVGTransformListSMILType.cpp @@ -260,11 +260,11 @@ bool SVGTransformListSMILType::AppendTransforms(const SVGTransformList& aList, if (!transforms.SetCapacity(transforms.Length() + aList.Length(), fallible)) return false; - for (uint32_t i = 0; i < aList.Length(); ++i) { + for (const auto& item : aList) { // No need to check the return value below since we have already allocated // the necessary space MOZ_ALWAYS_TRUE( - transforms.AppendElement(SVGTransformSMILData(aList[i]), fallible)); + transforms.AppendElement(SVGTransformSMILData(item), fallible)); } return true; } diff --git a/dom/tests/browser/browser.toml b/dom/tests/browser/browser.toml index 4c767690af14..026ad0fc4b23 100644 --- a/dom/tests/browser/browser.toml +++ b/dom/tests/browser/browser.toml @@ -201,6 +201,11 @@ support-files = [ "redirect_server.sjs", ] +["browser_scriptCache_responseHeaders.js"] +support-files = [ + "responseHeaders_server.sjs", +] + ["browser_sessionStorage_navigation.js"] support-files = [ "file_empty.html", diff --git a/dom/tests/browser/browser_scriptCache_responseHeaders.js b/dom/tests/browser/browser_scriptCache_responseHeaders.js new file mode 100644 index 000000000000..ef32e3910fb2 --- /dev/null +++ b/dom/tests/browser/browser_scriptCache_responseHeaders.js @@ -0,0 +1,74 @@ +const TEST_URL = "https://example.com/browser/dom/tests/browser/dummy.html"; +const SCRIPT_NAME = "responseHeaders_server.sjs"; +const TEST_SCRIPT_URL = + "https://example.com/browser/dom/tests/browser/" + SCRIPT_NAME; + +function getCounter(tab, query) { + const browser = tab.linkedBrowser; + const scriptPath = SCRIPT_NAME + query; + return SpecialPowers.spawn(browser, [scriptPath], async scriptPath => { + const { promise, resolve } = Promise.withResolvers(); + + const script = content.document.createElement("script"); + script.src = scriptPath; + script.addEventListener("load", resolve); + content.document.body.appendChild(script); + + await promise; + + return parseInt(content.document.body.getAttribute("counter")); + }); +} + +add_task(async function test_redirectCache() { + await SpecialPowers.pushPrefEnv({ + set: [["dom.script_loader.experimental.navigation_cache", true]], + }); + registerCleanupFunction(() => SpecialPowers.popPrefEnv()); + + const tests = [ + { + query: "?vary", + cachedCounter: false, + log: ",vary,vary", + }, + { + query: "?normal", + cachedCounter: true, + log: ",normal", + }, + ]; + + for (const { query, cachedCounter, log } of tests) { + ChromeUtils.clearResourceCache(); + Services.cache2.clear(); + + const resetResponse = await fetch(TEST_SCRIPT_URL + "?reset"); + is(await resetResponse.text(), "reset", "Server state should be reset"); + + const tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: TEST_URL, + }); + + is( + await getCounter(tab, query), + 0, + "counter should be 0 for the first load." + ); + + await BrowserTestUtils.reloadTab(tab); + + const counter = await getCounter(tab, query); + if (cachedCounter) { + is(counter, 0, "cache should be used for " + query); + } else { + is(counter, 1, "cache should not be used for " + query); + } + + const logResponse = await fetch(TEST_SCRIPT_URL + "?log"); + is(await logResponse.text(), log, "Log should match"); + + BrowserTestUtils.removeTab(tab); + } +}); diff --git a/dom/tests/browser/responseHeaders_server.sjs b/dom/tests/browser/responseHeaders_server.sjs new file mode 100644 index 000000000000..79f922c92319 --- /dev/null +++ b/dom/tests/browser/responseHeaders_server.sjs @@ -0,0 +1,37 @@ +function handleRequest(request, response) { + if (request.queryString == "reset") { + setState("counter", "0"); + setState("log", ""); + + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/text", false); + const body = "reset"; + response.bodyOutputStream.write(body, body.length); + return; + } + + if (request.queryString == "log") { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/text", false); + const body = getState("log"); + response.bodyOutputStream.write(body, body.length); + return; + } + + setState("log", getState("log") + "," + request.queryString); + + let counter = parseInt(getState("counter")); + setState("counter", (counter + 1).toString()); + + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Cache-Control", "max-age=10000", false); + if (request.queryString == "vary") { + response.setHeader("Vary", "Cookie", false); + } + response.setHeader("Content-Type", "text/javascript", false); + const body = ` +document.body.setAttribute("counter", "${counter}"); +document.cookie = "${counter}"; +`; + response.bodyOutputStream.write(body, body.length); +} diff --git a/dom/webidl/Window.webidl b/dom/webidl/Window.webidl index 87efe769a8e2..c41910158471 100644 --- a/dom/webidl/Window.webidl +++ b/dom/webidl/Window.webidl @@ -312,8 +312,8 @@ partial interface Window { [Throws, ChromeOnly] undefined moveResize(long x, long y, long w, long h); // viewport - [Replaceable, Throws] readonly attribute double innerWidth; - [Replaceable, Throws] readonly attribute double innerHeight; + [Replaceable, Throws, NeedsCallerType] readonly attribute double innerWidth; + [Replaceable, Throws, NeedsCallerType] readonly attribute double innerHeight; // viewport scrolling undefined scroll(unrestricted double x, unrestricted double y); diff --git a/editor/libeditor/tests/mochitest.toml b/editor/libeditor/tests/mochitest.toml index c35b5a26efd2..6e2213b3bbb0 100644 --- a/editor/libeditor/tests/mochitest.toml +++ b/editor/libeditor/tests/mochitest.toml @@ -37,6 +37,8 @@ skip-if = [ ["test_backspace_vs.html"] +["test_blur_in_compositionend.html"] + ["test_bug46555.html"] ["test_bug200416.html"] diff --git a/editor/libeditor/tests/test_blur_in_compositionend.html b/editor/libeditor/tests/test_blur_in_compositionend.html new file mode 100644 index 000000000000..62b10ca9e510 --- /dev/null +++ b/editor/libeditor/tests/test_blur_in_compositionend.html @@ -0,0 +1,58 @@ + + + + + Test that blurring in the compositionend handler doesn't cause a second compositionend to be fired. + + + + + +
+ + + diff --git a/gfx/thebes/gfxFcPlatformFontList.cpp b/gfx/thebes/gfxFcPlatformFontList.cpp index 8192d2712481..1c5233a2968a 100644 --- a/gfx/thebes/gfxFcPlatformFontList.cpp +++ b/gfx/thebes/gfxFcPlatformFontList.cpp @@ -2200,15 +2200,14 @@ FontVisibility gfxFcPlatformFontList::GetVisibilityForFamily( return FontVisibility::User; case Device::Linux_Fedora_any: + // We have no font list for this Fedora version + return FontVisibility::Unknown; + case Device::Linux_Fedora_39: if (FamilyInList(aName, kBaseFonts_Fedora_39)) { return FontVisibility::Base; } - if (sFontVisibilityDevice == Device::Linux_Fedora_39) { - return FontVisibility::User; - } - // For Fedora_any, fall through to also check Fedora 38 list. - [[fallthrough]]; + return FontVisibility::User; case Device::Linux_Fedora_38: if (FamilyInList(aName, kBaseFonts_Fedora_38)) { @@ -2246,11 +2245,13 @@ gfxFcPlatformFontList::GetFilteredPlatformFontLists() { break; case Device::Linux_Fedora_any: + // No font list for this Fedora version; see GetVisibilityForFamily(). + break; + case Device::Linux_Fedora_39: fontLists.AppendElement(std::make_pair(kBaseFonts_Fedora_39, std::size(kBaseFonts_Fedora_39))); - // For Fedora_any, fall through to also check Fedora 38 list. - [[fallthrough]]; + break; case Device::Linux_Fedora_38: fontLists.AppendElement(std::make_pair(kBaseFonts_Fedora_38, diff --git a/js/src/jit/MacroAssembler.cpp b/js/src/jit/MacroAssembler.cpp index 94e2ac3040af..3adba89886c9 100644 --- a/js/src/jit/MacroAssembler.cpp +++ b/js/src/jit/MacroAssembler.cpp @@ -5547,13 +5547,11 @@ void MacroAssembler::randomDouble(Register rng, FloatRegister dest, load64(state0Addr, s1Reg); // s1 ^= s1 << 23; - move64(s1Reg, s0Reg); - lshift64(Imm32(23), s1Reg); + lshift64(Imm32(23), s1Reg, s0Reg); xor64(s0Reg, s1Reg); // s1 ^= s1 >> 17 - move64(s1Reg, s0Reg); - rshift64(Imm32(17), s1Reg); + rshift64(Imm32(17), s1Reg, s0Reg); xor64(s0Reg, s1Reg); // const uint64_t s0 = mState[1]; @@ -6165,24 +6163,6 @@ uint8_t MacroAssembler::getByteAtOffset(size_t offset) const { #endif } -// This is an InstructionBytes source that reads bytes from an assembler buffer. -class InstructionBytesFromMasm : public wasm::InstructionBytes { - const MacroAssembler& masm_; - uint32_t baseOffset_ = 0; - - public: - explicit InstructionBytesFromMasm(const MacroAssembler& masm, - uint32_t baseOffset) - : masm_(masm), baseOffset_(baseOffset) { - MOZ_ASSERT(baseOffset < masm.readableSize()); - } - bool isU32aligned() const override { return (baseOffset_ & 3) == 0; } - uint8_t get(size_t offset) const override { - MOZ_ASSERT(offset < 16); - return masm_.getByteAtOffset(baseOffset_ + offset); - } -}; - mozilla::Atomic ctr(0); void MacroAssembler::appendAndVerify(wasm::Trap trap, wasm::TrapMachineInsn insn, @@ -6193,8 +6173,8 @@ void MacroAssembler::appendAndVerify(wasm::Trap trap, // length `fcr.length()` and kind `insn`. Ask SummarizeTrapInstruction // to look at it and check it agrees. if (!oom() && fcr.isValid()) { - InstructionBytesFromMasm insnSource(*this, fcr.offset()); - wasm::SummarizeResult summary = SummarizeTrapInstruction(insnSource); + wasm::SummarizeResult summary = + wasm::SummarizeTrapInstruction(*this, fcr.offset()); // The instruction must be identifiable MOZ_ASSERT(summary.identified()); // .. and have the correct kind and length @@ -10311,8 +10291,7 @@ void MacroAssembler::hashAndScrambleValue(ValueOperand value, Register result, // uint32_t v2 = static_cast(static_cast(aValue) >> 32); #ifdef JS_PUNBOX64 auto r64 = Register64(temp); - move64(value.toRegister64(), r64); - rshift64Arithmetic(Imm32(32), r64); + rshift64Arithmetic(Imm32(32), value.toRegister64(), r64); #else move32(value.typeReg(), temp); #endif @@ -11072,8 +11051,7 @@ void MacroAssembler::fuzzilliHashDouble(FloatRegister src, Register result, # ifdef JS_PUNBOX64 // Move the high word into |result|. - move64(r64, Register64(result)); - rshift64(Imm32(32), Register64(result)); + rshift64(Imm32(32), r64, Register64(result)); # endif // Add the high and low words of |r64|. diff --git a/js/src/jit/MacroAssembler.h b/js/src/jit/MacroAssembler.h index 54838efd0d5c..385f94ffabae 100644 --- a/js/src/jit/MacroAssembler.h +++ b/js/src/jit/MacroAssembler.h @@ -1441,8 +1441,12 @@ class MacroAssembler : public MacroAssemblerSpecific { Register dest) PER_ARCH; inline void lshift64(Imm32 imm, Register64 dest) PER_ARCH; + inline void lshift64(Imm32 imm, Register64 src, Register64 dest) PER_ARCH; inline void rshift64(Imm32 imm, Register64 dest) PER_ARCH; + inline void rshift64(Imm32 imm, Register64 src, Register64 dest) PER_ARCH; inline void rshift64Arithmetic(Imm32 imm, Register64 dest) PER_ARCH; + inline void rshift64Arithmetic(Imm32 imm, Register64 src, + Register64 dest) PER_ARCH; // On x86_shared these have the constraint that shift must be in CL. inline void lshift32(Register shift, Register srcDest) PER_SHARED_ARCH; diff --git a/js/src/jit/arm/MacroAssembler-arm-inl.h b/js/src/jit/arm/MacroAssembler-arm-inl.h index 66aa033f45d7..ed79b7a384ca 100644 --- a/js/src/jit/arm/MacroAssembler-arm-inl.h +++ b/js/src/jit/arm/MacroAssembler-arm-inl.h @@ -754,6 +754,15 @@ void MacroAssembler::lshift64(Imm32 imm, Register64 dest) { } } +void MacroAssembler::lshift64(Imm32 imm, Register64 src, Register64 dest) { + MOZ_ASSERT(0 <= imm.value && imm.value < 64); + MOZ_ASSERT(dest.low != src.high); + if (src != dest) { + move64(src, dest); + } + lshift64(imm, dest); +} + void MacroAssembler::lshift64(Register unmaskedShift, Register64 dest) { // dest.high = dest.high << shift | dest.low << shift - 32 | dest.low >> 32 - // shift Note: one of the two dest.low shift will always yield zero due to @@ -865,6 +874,16 @@ void MacroAssembler::rshift64Arithmetic(Imm32 imm, Register64 dest) { } } +void MacroAssembler::rshift64Arithmetic(Imm32 imm, Register64 src, + Register64 dest) { + MOZ_ASSERT(0 <= imm.value && imm.value < 64); + MOZ_ASSERT(dest.low != src.high); + if (src != dest) { + move64(src, dest); + } + rshift64Arithmetic(imm, dest); +} + void MacroAssembler::rshift64Arithmetic(Register unmaskedShift, Register64 dest) { Label proceed; @@ -930,6 +949,15 @@ void MacroAssembler::rshift64(Imm32 imm, Register64 dest) { } } +void MacroAssembler::rshift64(Imm32 imm, Register64 src, Register64 dest) { + MOZ_ASSERT(0 <= imm.value && imm.value < 64); + MOZ_ASSERT(dest.low != src.high); + if (src != dest) { + move64(src, dest); + } + rshift64(imm, dest); +} + void MacroAssembler::rshift64(Register unmaskedShift, Register64 dest) { // dest.low = dest.low >> shift | dest.high >> shift - 32 | dest.high << 32 - // shift Note: one of the two dest.high shifts will always yield zero due to diff --git a/js/src/jit/arm64/MacroAssembler-arm64-inl.h b/js/src/jit/arm64/MacroAssembler-arm64-inl.h index c2a4a64d33e1..545d39714f86 100644 --- a/js/src/jit/arm64/MacroAssembler-arm64-inl.h +++ b/js/src/jit/arm64/MacroAssembler-arm64-inl.h @@ -791,6 +791,11 @@ void MacroAssembler::lshift64(Imm32 imm, Register64 dest) { lshiftPtr(imm, dest.reg); } +void MacroAssembler::lshift64(Imm32 imm, Register64 src, Register64 dest) { + MOZ_ASSERT(0 <= imm.value && imm.value < 64); + lshiftPtr(imm, src.reg, dest.reg); +} + void MacroAssembler::lshift64(Register shift, Register64 srcDest) { Lsl(ARMRegister(srcDest.reg, 64), ARMRegister(srcDest.reg, 64), ARMRegister(shift, 64)); @@ -889,6 +894,11 @@ void MacroAssembler::rshift64(Imm32 imm, Register64 dest) { rshiftPtr(imm, dest.reg); } +void MacroAssembler::rshift64(Imm32 imm, Register64 src, Register64 dest) { + MOZ_ASSERT(0 <= imm.value && imm.value < 64); + rshiftPtr(imm, src.reg, dest.reg); +} + void MacroAssembler::rshift64(Register shift, Register64 srcDest) { Lsr(ARMRegister(srcDest.reg, 64), ARMRegister(srcDest.reg, 64), ARMRegister(shift, 64)); @@ -898,6 +908,12 @@ void MacroAssembler::rshift64Arithmetic(Imm32 imm, Register64 dest) { Asr(ARMRegister(dest.reg, 64), ARMRegister(dest.reg, 64), imm.value); } +void MacroAssembler::rshift64Arithmetic(Imm32 imm, Register64 src, + Register64 dest) { + MOZ_ASSERT(0 <= imm.value && imm.value < 64); + rshiftPtrArithmetic(imm, src.reg, dest.reg); +} + void MacroAssembler::rshift64Arithmetic(Register shift, Register64 srcDest) { Asr(ARMRegister(srcDest.reg, 64), ARMRegister(srcDest.reg, 64), ARMRegister(shift, 64)); diff --git a/js/src/jit/loong64/MacroAssembler-loong64-inl.h b/js/src/jit/loong64/MacroAssembler-loong64-inl.h index e387af15de21..1d40aa2f9716 100644 --- a/js/src/jit/loong64/MacroAssembler-loong64-inl.h +++ b/js/src/jit/loong64/MacroAssembler-loong64-inl.h @@ -731,6 +731,11 @@ void MacroAssembler::lshift64(Imm32 imm, Register64 dest) { as_slli_d(dest.reg, dest.reg, imm.value); } +void MacroAssembler::lshift64(Imm32 imm, Register64 src, Register64 dest) { + MOZ_ASSERT(0 <= imm.value && imm.value < 64); + as_slli_d(dest.reg, src.reg, imm.value); +} + void MacroAssembler::lshiftPtr(Register shift, Register dest) { as_sll_d(dest, dest, shift); } @@ -790,11 +795,22 @@ void MacroAssembler::rshift64(Imm32 imm, Register64 dest) { as_srli_d(dest.reg, dest.reg, imm.value); } +void MacroAssembler::rshift64(Imm32 imm, Register64 src, Register64 dest) { + MOZ_ASSERT(0 <= imm.value && imm.value < 64); + as_srli_d(dest.reg, src.reg, imm.value); +} + void MacroAssembler::rshift64Arithmetic(Imm32 imm, Register64 dest) { MOZ_ASSERT(0 <= imm.value && imm.value < 64); as_srai_d(dest.reg, dest.reg, imm.value); } +void MacroAssembler::rshift64Arithmetic(Imm32 imm, Register64 src, + Register64 dest) { + MOZ_ASSERT(0 <= imm.value && imm.value < 64); + as_srai_d(dest.reg, src.reg, imm.value); +} + void MacroAssembler::rshift64Arithmetic(Register shift, Register64 dest) { as_sra_d(dest.reg, dest.reg, shift); } diff --git a/js/src/jit/mips64/MacroAssembler-mips64-inl.h b/js/src/jit/mips64/MacroAssembler-mips64-inl.h index 3f3a97c267b3..8828d246f3eb 100644 --- a/js/src/jit/mips64/MacroAssembler-mips64-inl.h +++ b/js/src/jit/mips64/MacroAssembler-mips64-inl.h @@ -367,6 +367,11 @@ void MacroAssembler::lshift64(Imm32 imm, Register64 dest) { ma_dsll(dest.reg, dest.reg, imm); } +void MacroAssembler::lshift64(Imm32 imm, Register64 src, Register64 dest) { + MOZ_ASSERT(0 <= imm.value && imm.value < 64); + ma_dsll(dest.reg, src.reg, imm); +} + void MacroAssembler::lshift64(Register shift, Register64 dest) { ma_dsll(dest.reg, dest.reg, shift); } @@ -389,6 +394,11 @@ void MacroAssembler::rshift64(Imm32 imm, Register64 dest) { ma_dsrl(dest.reg, dest.reg, imm); } +void MacroAssembler::rshift64(Imm32 imm, Register64 src, Register64 dest) { + MOZ_ASSERT(0 <= imm.value && imm.value < 64); + ma_dsrl(dest.reg, src.reg, imm); +} + void MacroAssembler::rshift64(Register shift, Register64 dest) { ma_dsrl(dest.reg, dest.reg, shift); } @@ -412,6 +422,12 @@ void MacroAssembler::rshift64Arithmetic(Imm32 imm, Register64 dest) { ma_dsra(dest.reg, dest.reg, imm); } +void MacroAssembler::rshift64Arithmetic(Imm32 imm, Register64 src, + Register64 dest) { + MOZ_ASSERT(0 <= imm.value && imm.value < 64); + ma_dsra(dest.reg, src.reg, imm); +} + void MacroAssembler::rshift64Arithmetic(Register shift, Register64 dest) { ma_dsra(dest.reg, dest.reg, shift); } diff --git a/js/src/jit/riscv64/MacroAssembler-riscv64-inl.h b/js/src/jit/riscv64/MacroAssembler-riscv64-inl.h index a09275f16561..0017d77354e3 100644 --- a/js/src/jit/riscv64/MacroAssembler-riscv64-inl.h +++ b/js/src/jit/riscv64/MacroAssembler-riscv64-inl.h @@ -1593,6 +1593,12 @@ void MacroAssembler::lshift64(Imm32 imm, Register64 dest) { MOZ_ASSERT(0 <= imm.value && imm.value < 64); slli(dest.reg, dest.reg, imm.value); } + +void MacroAssembler::lshift64(Imm32 imm, Register64 src, Register64 dest) { + MOZ_ASSERT(0 <= imm.value && imm.value < 64); + slli(dest.reg, src.reg, imm.value); +} + void MacroAssembler::lshiftPtr(Register shift, Register dest) { sll(dest, dest, shift); } @@ -1946,6 +1952,12 @@ void MacroAssembler::rshift64Arithmetic(Imm32 imm, Register64 dest) { srai(dest.reg, dest.reg, imm.value); } +void MacroAssembler::rshift64Arithmetic(Imm32 imm, Register64 src, + Register64 dest) { + MOZ_ASSERT(0 <= imm.value && imm.value < 64); + srai(dest.reg, src.reg, imm.value); +} + void MacroAssembler::rshift64Arithmetic(Register shift, Register64 dest) { sra(dest.reg, dest.reg, shift); } @@ -1959,6 +1971,11 @@ void MacroAssembler::rshift64(Imm32 imm, Register64 dest) { srli(dest.reg, dest.reg, imm.value); } +void MacroAssembler::rshift64(Imm32 imm, Register64 src, Register64 dest) { + MOZ_ASSERT(0 <= imm.value && imm.value < 64); + srli(dest.reg, src.reg, imm.value); +} + void MacroAssembler::rshiftPtrArithmetic(Imm32 imm, Register dest) { rshiftPtrArithmetic(imm, dest, dest); } diff --git a/js/src/jit/shared/Assembler-shared.h b/js/src/jit/shared/Assembler-shared.h index 68e982dbe8e4..83cc605e4dad 100644 --- a/js/src/jit/shared/Assembler-shared.h +++ b/js/src/jit/shared/Assembler-shared.h @@ -760,6 +760,7 @@ class AssemblerShared { wasm::CallSites& callSites() { return callSites_; } wasm::CallSiteTargetVector& callSiteTargets() { return callSiteTargets_; } wasm::TrapSites& trapSites() { return trapSites_; } + const wasm::TrapSites& trapSites() const { return trapSites_; } wasm::SymbolicAccessVector& symbolicAccesses() { return symbolicAccesses_; } wasm::TryNoteVector& tryNotes() { return tryNotes_; } wasm::CodeRangeUnwindInfoVector& codeRangeUnwindInfos() { diff --git a/js/src/jit/wasm32/MacroAssembler-wasm32-inl.h b/js/src/jit/wasm32/MacroAssembler-wasm32-inl.h index 29705754cf81..2f8787014763 100644 --- a/js/src/jit/wasm32/MacroAssembler-wasm32-inl.h +++ b/js/src/jit/wasm32/MacroAssembler-wasm32-inl.h @@ -180,12 +180,25 @@ void MacroAssembler::rshiftPtrArithmetic(Register shift, Register srcDest) { void MacroAssembler::lshift64(Imm32 imm, Register64 dest) { MOZ_CRASH(); } +void MacroAssembler::lshift64(Imm32 imm, Register64 src, Register64 dest) { + MOZ_CRASH(); +} + void MacroAssembler::rshift64(Imm32 imm, Register64 dest) { MOZ_CRASH(); } +void MacroAssembler::rshift64(Imm32 imm, Register64 src, Register64 dest) { + MOZ_CRASH(); +} + void MacroAssembler::rshift64Arithmetic(Imm32 imm, Register64 dest) { MOZ_CRASH(); } +void MacroAssembler::rshift64Arithmetic(Imm32 imm, Register64 src, + Register64 dest) { + MOZ_CRASH(); +} + void MacroAssembler::lshiftPtr(Register shift, Register srcDest) { MOZ_CRASH(); } diff --git a/js/src/jit/x64/MacroAssembler-x64-inl.h b/js/src/jit/x64/MacroAssembler-x64-inl.h index 76ca787bf733..9cac54796f11 100644 --- a/js/src/jit/x64/MacroAssembler-x64-inl.h +++ b/js/src/jit/x64/MacroAssembler-x64-inl.h @@ -410,6 +410,11 @@ void MacroAssembler::lshift64(Imm32 imm, Register64 dest) { lshiftPtr(imm, dest.reg); } +void MacroAssembler::lshift64(Imm32 imm, Register64 src, Register64 dest) { + MOZ_ASSERT(0 <= imm.value && imm.value < 64); + lshiftPtr(imm, src.reg, dest.reg); +} + void MacroAssembler::lshift64(Register shift, Register64 srcDest) { if (Assembler::HasBMI2()) { shlxq(srcDest.reg, shift, srcDest.reg); @@ -460,6 +465,11 @@ void MacroAssembler::rshift64(Imm32 imm, Register64 dest) { rshiftPtr(imm, dest.reg); } +void MacroAssembler::rshift64(Imm32 imm, Register64 src, Register64 dest) { + MOZ_ASSERT(0 <= imm.value && imm.value < 64); + rshiftPtr(imm, src.reg, dest.reg); +} + void MacroAssembler::rshift64(Register shift, Register64 srcDest) { if (Assembler::HasBMI2()) { shrxq(srcDest.reg, shift, srcDest.reg); @@ -513,6 +523,12 @@ void MacroAssembler::rshift64Arithmetic(Imm32 imm, Register64 dest) { rshiftPtrArithmetic(imm, dest.reg); } +void MacroAssembler::rshift64Arithmetic(Imm32 imm, Register64 src, + Register64 dest) { + MOZ_ASSERT(0 <= imm.value && imm.value < 64); + rshiftPtrArithmetic(imm, src.reg, dest.reg); +} + void MacroAssembler::rshift64Arithmetic(Register shift, Register64 srcDest) { if (Assembler::HasBMI2()) { sarxq(srcDest.reg, shift, srcDest.reg); diff --git a/js/src/jit/x86/MacroAssembler-x86-inl.h b/js/src/jit/x86/MacroAssembler-x86-inl.h index 0e20242b916c..29c47e69053f 100644 --- a/js/src/jit/x86/MacroAssembler-x86-inl.h +++ b/js/src/jit/x86/MacroAssembler-x86-inl.h @@ -481,6 +481,19 @@ void MacroAssembler::lshift64(Imm32 imm, Register64 dest) { xorl(dest.low, dest.low); } +void MacroAssembler::lshift64(Imm32 imm, Register64 src, Register64 dest) { + MOZ_ASSERT(0 <= imm.value && imm.value < 64); + MOZ_ASSERT(dest.low != src.high); + if (src.low != dest.low) { + movl(src.low, dest.low); + } + if (src.high != dest.high) { + movl(src.high, dest.high); + } + + lshift64(imm, dest); +} + void MacroAssembler::lshift64(Register shift, Register64 srcDest) { MOZ_ASSERT(shift == ecx); MOZ_ASSERT(srcDest.low != ecx && srcDest.high != ecx); @@ -529,6 +542,19 @@ void MacroAssembler::rshift64(Imm32 imm, Register64 dest) { xorl(dest.high, dest.high); } +void MacroAssembler::rshift64(Imm32 imm, Register64 src, Register64 dest) { + MOZ_ASSERT(0 <= imm.value && imm.value < 64); + MOZ_ASSERT(dest.low != src.high); + if (src.low != dest.low) { + movl(src.low, dest.low); + } + if (src.high != dest.high) { + movl(src.high, dest.high); + } + + rshift64(imm, dest); +} + void MacroAssembler::rshift64(Register shift, Register64 srcDest) { MOZ_ASSERT(shift == ecx); MOZ_ASSERT(srcDest.low != ecx && srcDest.high != ecx); @@ -579,6 +605,20 @@ void MacroAssembler::rshift64Arithmetic(Imm32 imm, Register64 dest) { sarl(Imm32(0x1f), dest.high); } +void MacroAssembler::rshift64Arithmetic(Imm32 imm, Register64 src, + Register64 dest) { + MOZ_ASSERT(0 <= imm.value && imm.value < 64); + MOZ_ASSERT(dest.low != src.high); + if (src.low != dest.low) { + movl(src.low, dest.low); + } + if (src.high != dest.high) { + movl(src.high, dest.high); + } + + rshift64Arithmetic(imm, dest); +} + void MacroAssembler::rshift64Arithmetic(Register shift, Register64 srcDest) { MOZ_ASSERT(shift == ecx); MOZ_ASSERT(srcDest.low != ecx && srcDest.high != ecx); diff --git a/js/src/wasm/WasmBCClass.h b/js/src/wasm/WasmBCClass.h index 6e9b6936bda1..2957c9054ae3 100644 --- a/js/src/wasm/WasmBCClass.h +++ b/js/src/wasm/WasmBCClass.h @@ -944,22 +944,28 @@ struct BaseCompiler final { // instruction immediately after a trap instruction (the "resume" // instruction), or the instruction immediately following a no-op (when // debugging is enabled). + // + // The `Maybe` argument indicates the reason for creating the map. + // `Nothing` means the map is for a call; `Some(t)` means it is for a trap of + // kind `t`. See further comments on StackMapGenerator::createStackMap. // Create a vanilla stackmap. - [[nodiscard]] bool createStackMap(const char* who); + [[nodiscard]] bool createStackMap(Maybe reason); // Create a stackmap as vanilla, but for a custom assembler offset. - [[nodiscard]] bool createStackMap(const char* who, + [[nodiscard]] bool createStackMap(Maybe reason, CodeOffset assemblerOffset); // Create a stack map as vanilla, and note the presence of a ref-typed // DebugFrame on the stack. [[nodiscard]] bool createStackMap( - const char* who, HasDebugFrameWithLiveRefs debugFrameWithLiveRefs); + Maybe reason, HasDebugFrameWithLiveRefs debugFrameWithLiveRefs); - // Creates a stack map for an aborting trap instruction that will be emitted - // OOL. - [[nodiscard]] bool createAbortingOutOfLineTrapStackMap(StackMap** result); + // When compiling for debugging, creates a stack map for a non-resuming trap + // instruction of kind `t1`, and, if specified, `t2`. When not compiling for + // debugging, no stackmap is generated. + [[nodiscard]] bool createDebugOnlyStackMapForNonResumingTrap( + StackMap** result, Trap t1, Trap t2 = Trap::Limit); //////////////////////////////////////////////////////////// // diff --git a/js/src/wasm/WasmBCCodegen-inl.h b/js/src/wasm/WasmBCCodegen-inl.h index 151676c78c16..fe5aa851fd21 100644 --- a/js/src/wasm/WasmBCCodegen-inl.h +++ b/js/src/wasm/WasmBCCodegen-inl.h @@ -217,11 +217,13 @@ void BaseCompiler::trap(Trap t) { } masm.propagateOOM( - createStackMap("BaseCompiler::trap", HasDebugFrameWithLiveRefs::Maybe)); + createStackMap(mozilla::Some(t), HasDebugFrameWithLiveRefs::Maybe)); } void BaseCompiler::trap(Trap t, const TrapSiteDesc& trapSite, StackMap* stackMap) { + // TODO: ideally we should check that the resumability of `t` was taken into + // account when constructing `stackMap`. masm.wasmTrap(t, trapSite); if (stackMap && !stackMaps_->add(masm.currentOffset(), stackMap)) { diff --git a/js/src/wasm/WasmBCFrame.cpp b/js/src/wasm/WasmBCFrame.cpp index 61a2e368f726..3f66eab11058 100644 --- a/js/src/wasm/WasmBCFrame.cpp +++ b/js/src/wasm/WasmBCFrame.cpp @@ -139,41 +139,51 @@ void BaseLocalIter::operator++(int) { // // Stack map methods. -bool BaseCompiler::createStackMap(const char* who) { +bool BaseCompiler::createStackMap(Maybe reason) { const ExitStubMapVector noExtras; StackMap* stackMap; - return stackMapGenerator_.createStackMap( - who, noExtras, HasDebugFrameWithLiveRefs::No, stk_, &stackMap) && + return stackMapGenerator_.createStackMap(reason, noExtras, + HasDebugFrameWithLiveRefs::No, stk_, + &stackMap) && (!stackMap || stackMaps_->add(masm.currentOffset(), stackMap)); } -bool BaseCompiler::createStackMap(const char* who, CodeOffset assemblerOffset) { +bool BaseCompiler::createStackMap(Maybe reason, + CodeOffset assemblerOffset) { const ExitStubMapVector noExtras; StackMap* stackMap; - return stackMapGenerator_.createStackMap( - who, noExtras, HasDebugFrameWithLiveRefs::No, stk_, &stackMap) && + return stackMapGenerator_.createStackMap(reason, noExtras, + HasDebugFrameWithLiveRefs::No, stk_, + &stackMap) && (!stackMap || stackMaps_->add(assemblerOffset.offset(), stackMap)); } bool BaseCompiler::createStackMap( - const char* who, HasDebugFrameWithLiveRefs debugFrameWithLiveRefs) { + Maybe reason, HasDebugFrameWithLiveRefs debugFrameWithLiveRefs) { const ExitStubMapVector noExtras; StackMap* stackMap; return stackMapGenerator_.createStackMap( - who, noExtras, debugFrameWithLiveRefs, stk_, &stackMap) && + reason, noExtras, debugFrameWithLiveRefs, stk_, &stackMap) && (!stackMap || stackMaps_->add(masm.currentOffset(), stackMap)); } -[[nodiscard]] bool BaseCompiler::createAbortingOutOfLineTrapStackMap( - StackMap** result) { +bool BaseCompiler::createDebugOnlyStackMapForNonResumingTrap(StackMap** result, + Trap t1, Trap t2) { + // `t1`, and, if specified `t2`, definitely won't resume. + MOZ_ASSERT(t1 != Trap::Limit); + MOZ_ASSERT(!TrapMightResume(t1)); + MOZ_ASSERT_IF(t2 != Trap::Limit, !TrapMightResume(t2)); + if (MOZ_LIKELY(!compilerEnv_.debugEnabled())) { *result = nullptr; return true; } + // We can use either `t1` or `t2` (when valid) here, since ::createStackMap + // cares only about their resumability, and we established that above. ExitStubMapVector extras; return stackMapGenerator_.createStackMap( - "OutOfLineTrap", extras, HasDebugFrameWithLiveRefs::Maybe, stk_, result); + Some(t1), extras, HasDebugFrameWithLiveRefs::Maybe, stk_, result); } bool MachineStackTracker::cloneTo(MachineStackTracker* dst) { @@ -192,7 +202,7 @@ bool StackMapGenerator::generateStackmapEntriesForTrapExit( } bool StackMapGenerator::createStackMap( - const char* who, const ExitStubMapVector& extras, + Maybe reason, const ExitStubMapVector& extras, HasDebugFrameWithLiveRefs debugFrameWithLiveRefs, const StkVector& stk, wasm::StackMap** result) { // Always initialize the result value @@ -289,18 +299,38 @@ bool StackMapGenerator::createStackMap( MOZ_ASSERT_IF(framePushedAtEntryToBody.isNothing(), stk.empty()); MOZ_ASSERT_IF(framePushedExcludingArgs.isNothing(), stk.empty()); + // In this loop, we tolerate roots in registers only in the case where we + // definitely won't resume execution after the instruction with which this + // stackmap is associated. That means the stackmap can't be for a call; it + // must be for a definitely-non-resumable trap. + bool allowRefsInRegs = + // the stackmap isn't for a call + reason.isSome() && + // (implied: the stackmap is for trap) which is not resumable + !TrapMightResume(reason.value()); + for (const Stk& v : stk) { + // If refs in regs aren't allowed, hard assert that we don't have them. + // Failure of this assertion is serious and should be investigated. + if (MOZ_LIKELY(!allowRefsInRegs)) { + MOZ_RELEASE_ASSERT(v.kind() != Stk::RegisterRef); + } + + // Now filter out everything except refs in memory. If refs in regs are + // allowable then we will ignore them; that's OK because we have established + // above that we won't be resuming after the associated trap is handled + // (refs in regs are never allowed for stackmaps associated with calls). + #ifndef DEBUG - // We don't track roots in registers, per rationale below, so if this - // doesn't hold, something is seriously wrong, and we're likely to get a - // GC-related crash. - MOZ_RELEASE_ASSERT(v.kind() != Stk::RegisterRef); + // Ignore everything except refs in memory. if (v.kind() != Stk::MemRef) { continue; } + #else - // Take the opportunity to check everything we reasonably can about - // operand stack elements. + // The same; ignore everything except refs in memory. However, take the + // opportunity to check everything we reasonably can about operand stack + // elements. switch (v.kind()) { case Stk::MemI32: case Stk::MemI64: @@ -355,15 +385,21 @@ bool StackMapGenerator::createStackMap( MOZ_ASSERT(v.refval() == 0); continue; case Stk::RegisterRef: - // This can't happen, per rationale above. - MOZ_CRASH("createStackMap: operand stack contains RegisterRef"); + // This assertion holds because of the release-assertion at the top of + // the loop. + MOZ_RELEASE_ASSERT(allowRefsInRegs); + // The associated instruction isn't resumable, so we tolerate the + // register. + continue; default: MOZ_CRASH("createStackMap: unknown operand stack element"); } #endif + // v.offs() holds masm.framePushed() at the point immediately after it // was pushed on the stack. Since it's still on the stack, // masm.framePushed() can't be less. + MOZ_ASSERT(v.kind() == Stk::MemRef); MOZ_ASSERT(v.offs() <= framePushedExcludingArgs.value()); uint32_t offsFromMapLowest = framePushedExcludingArgs.value() - v.offs(); MOZ_ASSERT(0 == offsFromMapLowest % sizeof(void*)); diff --git a/js/src/wasm/WasmBCFrame.h b/js/src/wasm/WasmBCFrame.h index 7965f55fb68f..c64072385cb7 100644 --- a/js/src/wasm/WasmBCFrame.h +++ b/js/src/wasm/WasmBCFrame.h @@ -1380,12 +1380,22 @@ struct StackMapGenerator { [[nodiscard]] bool generateStackmapEntriesForTrapExit( const ArgTypeVector& args, ExitStubMapVector* extras); - // Creates a stackmap incorporating pointers from the current operand - // stack |stk|, incorporating possible extra pointers in |extra| at the - // lower addressed end, and possibly with the associated frame having a - // DebugFrame that must be traced, as indicated by |debugFrameWithLiveRefs|. + // Creates a stackmap incorporating pointers from the current operand stack + // |stk|, incorporating possible extra pointers in |extra| at the lower + // addressed end, and possibly with the associated frame having a DebugFrame + // that must be traced, as indicated by |debugFrameWithLiveRefs|. + // + // `reason` says something about the instruction for which the stackmap is + // being made. When it is `Nothing`, the stack map is for a call instruction, + // which is assumed to be resumable. When it is `Some(Trap t)`, it is for a + // Trap of kind `t`, and whether or not it is resumable depends on `t`. + // + // If the stackmap is for a resumable trap, register-resident references in + // the local frame are disallowed (by MOZ_CRASH-ing). For non-resumable + // traps, they are tolerated on the basis that, once the trap happens, the + // frame is dead, so there is no need to trace it (for GC). [[nodiscard]] bool createStackMap( - const char* who, const ExitStubMapVector& extras, + Maybe reason, const ExitStubMapVector& extras, HasDebugFrameWithLiveRefs debugFrameWithLiveRefs, const StkVector& stk, wasm::StackMap** result); }; diff --git a/js/src/wasm/WasmBaselineCompile.cpp b/js/src/wasm/WasmBaselineCompile.cpp index dc7cd932daed..be07ff0b7b21 100644 --- a/js/src/wasm/WasmBaselineCompile.cpp +++ b/js/src/wasm/WasmBaselineCompile.cpp @@ -147,6 +147,7 @@ #include "wasm/WasmBCFrame.h" #include "wasm/WasmBCRegDefs.h" #include "wasm/WasmBCStk.h" +#include "wasm/WasmGC.h" #include "wasm/WasmValType.h" #include "jit/MacroAssembler-inl.h" @@ -268,7 +269,8 @@ bool BaseCompiler::addInterruptCheck() { &ok); trap(wasm::Trap::CheckInterrupt); masm.bind(&ok); - return createStackMap("addInterruptCheck"); + // stackmap for: interrupt check + return createStackMap(Some(wasm::Trap::CheckInterrupt)); } void BaseCompiler::checkDivideByZero(RegI32 rhs) { @@ -480,7 +482,7 @@ static uint32_t BlockSizeToDownwardsStep(size_t blockBytecodeSize) { bool BaseCompiler::beginFunction() { AutoCreatedBy acb(masm, "(wasm)BaseCompiler::beginFunction"); - JitSpew(JitSpew_Codegen, "# ========================================"); + JitSpew(JitSpew_Codegen, "# ================================"); JitSpew(JitSpew_Codegen, "# Emitting wasm baseline code"); JitSpew(JitSpew_Codegen, "# beginFunction: start of function prologue for index %d", @@ -558,7 +560,8 @@ bool BaseCompiler::beginFunction() { ExitStubMapVector extras; StackMap* functionEntryStackMap; if (!stackMapGenerator_.generateStackmapEntriesForTrapExit(args, &extras) || - !stackMapGenerator_.createStackMap("stack check", extras, + // stackmap for: stack overflow check + !stackMapGenerator_.createStackMap(Some(Trap::StackOverflow), extras, HasDebugFrameWithLiveRefs::No, stk_, &functionEntryStackMap)) { return false; @@ -668,7 +671,8 @@ bool BaseCompiler::beginFunction() { if (compilerEnv_.debugEnabled()) { insertBreakablePoint(CallSiteKind::EnterFrame); - if (!createStackMap("debug: enter-frame breakpoint")) { + // stackmap for: debug: enter-frame breakpoint + if (!createStackMap(Nothing() /* stackmap pertains to a call */)) { return false; } } @@ -729,13 +733,15 @@ bool BaseCompiler::endFunction() { // it can be clobbered, and/or modified by the debug trap. saveRegisterReturnValues(resultType); insertBreakablePoint(CallSiteKind::Breakpoint); - if (!createStackMap("debug: return-point breakpoint", + // stackmap for: debug: return-point breakpoint + if (!createStackMap(Nothing() /* stackmap pertains to a call */, HasDebugFrameWithLiveRefs::Maybe)) { return false; } insertBreakablePoint(CallSiteKind::LeaveFrame); - if (!createStackMap("debug: leave-frame breakpoint", + // stackmap for: debug: leave-frame breakpoint + if (!createStackMap(Nothing() /* stackmap pertains to a call */, HasDebugFrameWithLiveRefs::Maybe)) { return false; } @@ -1657,7 +1663,8 @@ bool BaseCompiler::insertDebugCollapseFrame() { } insertBreakablePoint(CallSiteKind::CollapseFrame); - return createStackMap("debug: collapse-frame breakpoint", + // stackmap for: debug: collapse-frame breakpoint + return createStackMap(Nothing() /* stackmap pertains to a call */, HasDebugFrameWithLiveRefs::Maybe); } @@ -2058,7 +2065,8 @@ bool BaseCompiler::callIndirect(uint32_t funcTypeIndex, uint32_t tableIndex, CalleeDesc callee = CalleeDesc::wasmTable(codeMeta_, table, tableIndex, callIndirectId); StackMap* oobTrapStackMap; - if (!createAbortingOutOfLineTrapStackMap(&oobTrapStackMap)) { + if (!createDebugOnlyStackMapForNonResumingTrap(&oobTrapStackMap, + Trap::OutOfBounds)) { return false; } OutOfLineCode* oob = addOutOfLineCode(new (alloc_) OutOfLineTrap( @@ -2094,7 +2102,8 @@ bool BaseCompiler::callIndirect(uint32_t funcTypeIndex, uint32_t tableIndex, Label* nullCheckFailed = nullptr; #ifndef WASM_HAS_HEAPREG StackMap* nullTrapStackMap; - if (!createAbortingOutOfLineTrapStackMap(&nullTrapStackMap)) { + if (!createDebugOnlyStackMapForNonResumingTrap(&nullTrapStackMap, + Trap::IndirectCallToNull)) { return false; } OutOfLineCode* nullref = addOutOfLineCode(new (alloc_) OutOfLineTrap( @@ -5459,7 +5468,8 @@ bool BaseCompiler::emitCall() { raOffset = callDefinition(funcIndex, baselineCall); } - if (!createStackMap("emitCall", raOffset)) { + // stackmap for: emitCall + if (!createStackMap(Nothing() /* stackmap pertains to a call */, raOffset)) { return false; } @@ -5570,10 +5580,14 @@ bool BaseCompiler::emitCallIndirect() { /*tailCall*/ false, &fastCallOffset, &slowCallOffset)) { return false; } - if (!createStackMap("emitCallIndirect", fastCallOffset)) { + // stackmap for: emitCallIndirect (fast) + if (!createStackMap(Nothing() /* stackmap pertains to a call */, + fastCallOffset)) { return false; } - if (!createStackMap("emitCallIndirect", slowCallOffset)) { + // stackmap for: emitCallIndirect (slow) + if (!createStackMap(Nothing() /* stackmap pertains to a call */, + slowCallOffset)) { return false; } @@ -5695,10 +5709,14 @@ bool BaseCompiler::emitCallRef() { &slowCallOffset)) { return false; } - if (!createStackMap("emitCallRef", fastCallOffset)) { + // stackmap for: emitCallRef (fast) + if (!createStackMap(Nothing() /* stackmap pertains to a call */, + fastCallOffset)) { return false; } - if (!createStackMap("emitCallRef", slowCallOffset)) { + // stackmap for: emitCallRef (slow) + if (!createStackMap(Nothing() /* stackmap pertains to a call */, + slowCallOffset)) { return false; } @@ -5806,7 +5824,8 @@ bool BaseCompiler::emitUnaryMathBuiltinCall(SymbolicAddress callee, } CodeOffset raOffset = builtinCall(callee, baselineCall); - if (!createStackMap("emitUnaryMathBuiltin[..]", raOffset)) { + // stackmap for: emitUnaryMathBuiltinCall + if (!createStackMap(Nothing() /* stackmap pertains to a call */, raOffset)) { return false; } @@ -5849,7 +5868,8 @@ bool BaseCompiler::emitDivOrModI64BuiltinCall(SymbolicAddress callee, masm.passABIArg(rhs.low); CodeOffset raOffset = masm.callWithABI( bytecodeOffset(), callee, mozilla::Some(fr.getInstancePtrOffset())); - if (!createStackMap("emitDivOrModI64Bui[..]", raOffset)) { + // stackmap for: emitDivOrModI64BuiltinCall + if (!createStackMap(Nothing() /* stackmap pertains to a call */, raOffset)) { return false; } @@ -5881,7 +5901,8 @@ bool BaseCompiler::emitConvertInt64ToFloatingCallout(SymbolicAddress callee, CodeOffset raOffset = masm.callWithABI( bytecodeOffset(), callee, mozilla::Some(fr.getInstancePtrOffset()), resultType == ValType::F32 ? ABIType::Float32 : ABIType::Float64); - if (!createStackMap("emitConvertInt64To[..]", raOffset)) { + // stackmap for: emitConvertInt64ToFloatingCallout + if (!createStackMap(Nothing() /* stackmap pertains to a call */, raOffset)) { return false; } @@ -5925,7 +5946,8 @@ bool BaseCompiler::emitConvertFloatingToInt64Callout(SymbolicAddress callee, masm.passABIArg(doubleInput, ABIType::Float64); CodeOffset raOffset = masm.callWithABI( bytecodeOffset(), callee, mozilla::Some(fr.getInstancePtrOffset())); - if (!createStackMap("emitConvertFloatin[..]", raOffset)) { + // stackmap for: emitConvertFloatingToInt64Callout + if (!createStackMap(Nothing() /* stackmap pertains to a call */, raOffset)) { return false; } @@ -6587,11 +6609,14 @@ bool BaseCompiler::emitInstanceCall(const SymbolicAddressSignature& builtin) { CodeOffset trapStackMapKey; builtinInstanceMethodCall(builtin, instanceArg, baselineCall, &callStackMapKey, &trapStackMapKey); - if (!createStackMap("emitInstanceCall-call", callStackMapKey)) { + if (!createStackMap(Nothing() /* stackmap pertains to a call */, + callStackMapKey)) { return false; } if (trapStackMapKey.bound() && - !createStackMap("emitInstanceCall-trap", trapStackMapKey)) { + // FIXME: this is a kludge in that it assumes that the trap kind created + // by builtinInstanceMethodCall is ThrowReported. + !createStackMap(Some(wasm::Trap::ThrowReported), trapStackMapKey)) { return false; } endCall(baselineCall, stackSpace); @@ -9169,7 +9194,8 @@ bool BaseCompiler::emitRefCast(bool nullable) { RegRef ref = popRef(); StackMap* trapStackMap; - if (!createAbortingOutOfLineTrapStackMap(&trapStackMap)) { + if (!createDebugOnlyStackMapForNonResumingTrap(&trapStackMap, + Trap::BadCast)) { return false; } OutOfLineCode* ool = addOutOfLineCode( @@ -10710,7 +10736,8 @@ bool BaseCompiler::emitBody() { sync(); insertBreakablePoint(CallSiteKind::Breakpoint); - if (!createStackMap("debug: per-insn breakpoint")) { + // stackmap for: debug: per-insn breakpoint + if (!createStackMap(Nothing() /* stackmap pertains to a call */)) { return false; } previousBreakablePoint_ = masm.currentOffset(); @@ -12697,6 +12724,23 @@ bool js::wasm::BaselineCompileFunctions(const CodeMetadata& codeMeta, } for (const FuncCompileInput& func : inputs) { + JitSpew(JitSpew_Codegen, + "# ================================" + "================================"); + JitSpew(JitSpew_Codegen, + "# j::w::BaselineCompileFunctions: BEGIN function index %d", + (int)func.index); + +#ifdef DEBUG + // Snapshot the "frontier" of the trapsite vectors so we can determine + // which ones are added to during compilation of this function. + mozilla::EnumeratedArray + trapSitesBefore; + for (Trap kind : mozilla::MakeEnumeratedRange(Trap::Limit)) { + trapSitesBefore[kind] = uint32_t(masm.trapSites().length(kind)); + } +#endif + Decoder d(func.begin, func.end, func.bytecodeOffset, error); // Build the local types vector. @@ -12751,6 +12795,31 @@ bool js::wasm::BaselineCompileFunctions(const CodeMetadata& codeMeta, // Accumulate observed feature usage code->featureUsage |= f.iter_.featureUsage(); + +#ifdef DEBUG + // Get a second snapshot of the frontier of the TrapSite vectors, and + // use this to check that traps that need a stackmap, actually have one. + mozilla::EnumeratedArray + trapSitesAfter; + for (Trap kind : mozilla::MakeEnumeratedRange(Trap::Limit)) { + trapSitesAfter[kind] = uint32_t(masm.trapSites().length(kind)); + } + + // Do the check. This asserts if the check fails. + auto checkThisTrapKind = [](Trap t) -> bool { + // Temporary setting, to make all of this a no-op. + return false; + }; + CheckStackMapsForTraps(masm, code->stackMaps, trapSitesBefore, + trapSitesAfter, checkThisTrapKind); +#endif + + JitSpew(JitSpew_Codegen, + "# j::w::BaselineCompileFunctions: END function index %d", + (int)func.index); + JitSpew(JitSpew_Codegen, + "# ================================" + "================================"); } masm.finish(); diff --git a/js/src/wasm/WasmCodegenTypes.h b/js/src/wasm/WasmCodegenTypes.h index f66d12fe8b54..39be11fe8904 100644 --- a/js/src/wasm/WasmCodegenTypes.h +++ b/js/src/wasm/WasmCodegenTypes.h @@ -535,6 +535,8 @@ class TrapSitesForKind { // We subtract one so that this check is not idempotent on 32-bit systems. static constexpr size_t MAX_LENGTH = UINT32_MAX - 1; + uint32_t getPCoffset(uint32_t index) const { return pcOffsets_[index]; } + uint32_t length() const { size_t result = pcOffsets_.length(); // Enforced by dynamic checks in mutation functions. @@ -710,6 +712,8 @@ class TrapSites { public: explicit TrapSites() = default; + const TrapSitesForKind& get(Trap trap) const { return array_[trap]; } + bool empty() const { for (Trap trap : mozilla::MakeEnumeratedRange(Trap::Limit)) { if (!array_[trap].empty()) { @@ -761,6 +765,8 @@ class TrapSites { } } + size_t length(Trap trap) const { return array_[trap].length(); } + [[nodiscard]] bool lookup(uint32_t trapInstructionOffset, const InliningContext& inliningContext, Trap* kindOut, diff --git a/js/src/wasm/WasmCompile.cpp b/js/src/wasm/WasmCompile.cpp index a37b2b379b7f..c405385f4565 100644 --- a/js/src/wasm/WasmCompile.cpp +++ b/js/src/wasm/WasmCompile.cpp @@ -270,15 +270,28 @@ SharedCompileArgs CompileArgs::build(JSContext* cx, ion = false; } + // true iff the user requested debug code, and we're able to honour that. + bool forceDebug = JS::Prefs::wasm_baseline_debug() && baseline; + // Debug information such as source view or debug traps will require - // additional memory and permanently stay in baseline code, so we try to - // only enable it when a developer actually cares: when the debugger tab - // is open. - bool debug = cx->realm() && cx->realm()->debuggerObservesWasm(); + // additional memory and permanently stay in baseline code, so we try to only + // enable it when a developer actually cares: when the debugger tab is open. + // Or when --setpref=wasm_baseline_debug=true is given to the shell and + // we have baseline available. + bool debug = + (cx->realm() && cx->realm()->debuggerObservesWasm()) || forceDebug; bool forceTiering = cx->options().testWasmAwaitTier2() || JitOptions.wasmDelayTier2; + if (forceDebug) { + // If --setpref=wasm_baseline_debug=true is specified and we can honour it + // (because baseline is available), disable Ion and tiering so as to avoid + // failures below. + ion = false; + forceTiering = false; + } + // The Available() predicates should ensure no failure here, but // when we're fuzzing we allow inconsistent switches and the check may thus // fail. Let it go to a run-time error instead of crashing. diff --git a/js/src/wasm/WasmConstants.h b/js/src/wasm/WasmConstants.h index e453d7fdc537..87c53486dc2b 100644 --- a/js/src/wasm/WasmConstants.h +++ b/js/src/wasm/WasmConstants.h @@ -244,6 +244,10 @@ enum class Trap { Limit }; +// Returns `true` if there is any possibility that a trap of kind `t` might +// resume. Only returns `false` if `t` definitely won't resume. +bool TrapMightResume(Trap t); + #ifdef JS_JITSPEW const char* NameOfTrap(Trap t); #endif diff --git a/js/src/wasm/WasmGC.cpp b/js/src/wasm/WasmGC.cpp index 0dad061fc81e..059136d9f817 100644 --- a/js/src/wasm/WasmGC.cpp +++ b/js/src/wasm/WasmGC.cpp @@ -17,6 +17,7 @@ #include "wasm/WasmGC.h" #include "wasm/WasmInstance.h" +#include "wasm/WasmSummarizeInsn.h" #include "jit/MacroAssembler-inl.h" @@ -198,6 +199,12 @@ const char* wasm::NameOfTrap(Trap t) { return "StackOverflow"; case Trap::CheckInterrupt: return "CheckInterrupt"; +# ifdef ENABLE_WASM_JSPI + case Trap::ThrowSuspendError: + return "ThrowSuspendError"; +# endif + case Trap::Unimplemented: + return "Unimplemented"; case Trap::ThrowReported: return "ThrowReported"; case Trap::Limit: @@ -208,6 +215,33 @@ const char* wasm::NameOfTrap(Trap t) { } #endif +bool wasm::TrapMightResume(Trap t) { + switch (t) { + case Trap::CheckInterrupt: + return true; + case Trap::Unreachable: + case Trap::IntegerOverflow: + case Trap::InvalidConversionToInteger: + case Trap::IntegerDivideByZero: + case Trap::OutOfBounds: + case Trap::UnalignedAccess: + case Trap::IndirectCallToNull: + case Trap::IndirectCallBadSig: + case Trap::NullPointerDereference: + case Trap::BadCast: + case Trap::StackOverflow: +#ifdef ENABLE_WASM_JSPI + case Trap::ThrowSuspendError: +#endif + case Trap::Unimplemented: + case Trap::ThrowReported: + return false; + case Trap::Limit: + break; + } + MOZ_CRASH(); +} + bool wasm::GenerateStackmapEntriesForTrapExit( const ArgTypeVector& args, const RegisterOffsets& trapExitLayout, const size_t trapExitLayoutNumWords, ExitStubMapVector* extras) { @@ -407,57 +441,238 @@ void wasm::CheckWholeCellLastElementCache(MacroAssembler& masm, } #ifdef DEBUG -bool wasm::IsPlausibleStackMapKey(const uint8_t* nextPC) { +bool wasm::IsPlausibleStackMapKey(const uint8_t* base, + uint32_t stackmapOffset) { + // See block comment at the declaration of this function for explanation. + const uint8_t* nextPC = base + size_t(stackmapOffset); + + // Most stackmaps are associated with call instructions. Look backwards to + // see if that's plausible, while being aware of the limitations described in + // the abovementioned block comment, at least for targets with variable-length + // insn encodings (x86, x64, Arm-Thumb2 [which we don't generate], + // RiscV-compressed [which we don't currently generate]). + # if defined(JS_CODEGEN_X64) || defined(JS_CODEGEN_X86) const uint8_t* insn = nextPC; - return (insn[-2] == 0x0F && insn[-1] == 0x0B) || // ud2 - (insn[-2] == 0xFF && (insn[-1] & 0xF8) == 0xD0) || // call *%r_ - insn[-5] == 0xE8; // call simm32 - -# elif defined(JS_CODEGEN_ARM) - const uint32_t* insn = (const uint32_t*)nextPC; - return ((uintptr_t(insn) & 3) == 0) && // must be ARM, not Thumb - (insn[-1] == 0xe7f000f0 || // udf - (insn[-1] & 0xfffffff0) == 0xe12fff30 || // blx reg (ARM, enc A1) - (insn[-1] & 0x0f000000) == 0x0b000000); // bl.cc simm24 (ARM, enc A1) + if ((insn[-2] == 0xFF && (insn[-1] & 0xF8) == 0xD0) || // call *%r_ + insn[-5] == 0xE8) { // call simm32 + return true; + } # elif defined(JS_CODEGEN_ARM64) - const uint32_t hltInsn = 0xd4a00000; + if ((uintptr_t(nextPC) & 3) != 0) { + return false; // misaligned + } const uint32_t* insn = (const uint32_t*)nextPC; - return ((uintptr_t(insn) & 3) == 0) && - (insn[-1] == hltInsn || // hlt - (insn[-1] & 0xfffffc1f) == 0xd63f0000 || // blr reg - (insn[-1] & 0xfc000000) == 0x94000000); // bl simm26 + if (((insn[-1] & 0xfffffc1f) == 0xd63f0000) || // blr reg + ((insn[-1] & 0xfc000000) == 0x94000000)) { // bl simm26 + return true; + } + +# elif defined(JS_CODEGEN_ARM) + if ((uintptr_t(nextPC) & 3) != 0) { + return false; // misaligned or Thumb + } + const uint32_t* insn = (const uint32_t*)nextPC; + if (((insn[-1] & 0xfffffff0) == 0xe12fff30) || // blx reg (ARM, enc A1) + ((insn[-1] & 0x0f000000) == 0x0b000000)) { // bl.cc simm24 (ARM, enc A1) + return true; + } + +# elif defined(JS_CODEGEN_RISCV64) + if ((uintptr_t(nextPC) & 3) != 0) { + return false; // misaligned + } + const uint32_t* insn = (const uint32_t*)nextPC; + if (((insn[-1] & kBaseOpcodeMask) == JALR) || // jalr + ((insn[-1] & kBaseOpcodeMask) == JAL) || // jal + ((insn[-2] & kBaseOpcodeMask) == JAL && + insn[-1] == 0x00000013 /* addi zero, zero, 0 */)) { // jal; nop + return true; + } # elif defined(JS_CODEGEN_MIPS64) - // TODO (bug 1699696): Implement this. As for the platforms above, we need to - // enumerate all code sequences that can precede the stackmap location. + // TODO (bug 1699696): Implement this. As with the platforms above, we need + // to identify call instructions that we generate. + // FIXME (also for Loong64): this should be implemented properly. Not doing + // so increases the likelyhood of the trap/stackmap machinery having + // undetected bugs on these targets. return true; + # elif defined(JS_CODEGEN_LOONG64) // TODO(loong64): Implement IsValidStackMapKey. return true; -# elif defined(JS_CODEGEN_RISCV64) - const uint32_t* insn = reinterpret_cast(nextPC); - return (((uintptr_t(insn) & 3) == 0) && - ((insn[-1] == 0x00006037 && insn[-2] == 0x00100073) || // break; - ((insn[-1] & kBaseOpcodeMask) == JALR) || // jalr - ((insn[-1] & kBaseOpcodeMask) == JAL) || // jal - ((insn[-2] & kBaseOpcodeMask) == JAL && - insn[-1] == 0x00000013 /* addi zero, zero, 0 */) || // jal; nop - (insn[-1] == 0xc0035073))); // "csrwi csr_cycle, 0x6"; + # else - MOZ_CRASH("IsValidStackMapKey: requires implementation on this platform"); + MOZ_CRASH( + "IsValidStackMapKey: call-instruction identification " + "requires implementation on this platform"); # endif + + // It's not associated with a call instruction, so it must instead be + // associated with some instruction that can trap. We can check that by using + // SummarizeTrapInstruction, and for that we need the start point of the + // instruction, but what we have to hand is the start point of the following + // instruction. So the best we can do is to use SummarizeTrapInstruction to + // inspect up to one-instruction-length's worth of byte offsets before + // `nextPC`, to see if any of them is a trapping instruction of the right kind + // and length. + + // The minimum and maximum instruction lengths we are looking for, with + // clamping so we don't back up into negative offset land. + uint32_t minLen = + std::min(stackmapOffset, FaultingCodeRange::minInsnLength()); + uint32_t maxLen = + std::min(stackmapOffset, FaultingCodeRange::maxInsnLength()); + + uint32_t len = 0; + bool found = false; + SummarizeResult summary; + + // Inspect instructions in `nextPC -minLen, -(minLen+1), .. -maxLen`, to see + // if any of them are a trapping instruction of the right kind. + for (len = minLen; len <= maxLen; len++) { + const uint8_t* maybeTrappingInsn = (uint8_t*)nextPC - len; + summary = SummarizeTrapInstruction(maybeTrappingInsn); + if (!summary.identified()) { + // Wasn't identified. In effect, not found. + continue; + } + if (summary.length() != len) { + // Was identified, but is the wrong length. + continue; + } + // So we've found something plausible. + found = true; + MOZ_ASSERT(maybeTrappingInsn + len == nextPC); + break; + } + + if (!found) { + // There's no mention of a trapping instruction that immediately precedes + // `nextPC`. Give up. Note that we can also get here if + // SummarizeTrapInstruction fails to identify a valid trapping insn, or + // fails to compute its length. That would be a bug in + // SummarizeTrapInstruction, which should be fixed. + return false; + } + + // The instruction was identified and has the right length. + MOZ_ASSERT(summary.kind() != TrapMachineInsn::INVALID); + return true; } #endif void StackMaps::checkInvariants(const uint8_t* base) const { #ifdef DEBUG - // Chech that each entry points from the stackmap structure points - // to a plausible instruction. + // Check that each entry in the stackmap hash table points to a plausible + // instruction. for (auto iter = codeOffsetToStackMap_.iter(); !iter.done(); iter.next()) { - MOZ_ASSERT(IsPlausibleStackMapKey(base + iter.get().key()), + MOZ_ASSERT(IsPlausibleStackMapKey(base, iter.get().key()), "wasm stackmap does not reference a valid insn"); } #endif } + +#ifdef DEBUG +void wasm::CheckStackMapsForTraps(const jit::MacroAssembler& masm, + const StackMaps& stackMaps, + const TrapSitesFrontierArray& trapSitesBefore, + const TrapSitesFrontierArray& trapSitesAfter, + bool (*checkThisTrapKind)(Trap)) { + // Collect some stats + uint32_t numTrapsTotal = 0; + uint32_t numTrapsToCheck = 0; + for (Trap kind : mozilla::MakeEnumeratedRange(Trap::Limit)) { + MOZ_ASSERT(trapSitesBefore[kind] <= trapSitesAfter[kind]); + size_t kindCount = trapSitesAfter[kind] - trapSitesBefore[kind]; + numTrapsTotal += kindCount; + if (checkThisTrapKind(kind)) { + numTrapsToCheck += kindCount; + } + } + + JitSpew(JitSpew_Codegen, + "Verifying stackmaps: %u trapSites total, %u to check", numTrapsTotal, + numTrapsToCheck); + + // Perform the actual checking + bool stackmapCheckOK = true; + + for (Trap kind : mozilla::MakeEnumeratedRange(Trap::Limit)) { + size_t kindCount = trapSitesAfter[kind] - trapSitesBefore[kind]; + if (kindCount > 0 && !checkThisTrapKind(kind)) { + JitSpew(JitSpew_Codegen, " skipping %zu trapSites of kind %s", + kindCount, NameOfTrap(kind)); + continue; + } + + const TrapSitesForKind& sitesForKind = masm.trapSites().get(kind); + // Strictly speaking, this doesn't need to be true. However, we expect + // callers to use this routine to check trapsites they have just generated, + // not to check some arbitrary slice of older trapsites. + MOZ_ASSERT(sitesForKind.length() == trapSitesAfter[kind]); + + for (uint32_t tsi = trapSitesBefore[kind]; tsi < trapSitesAfter[kind]; + tsi++) { + // This is the offset of the first byte of the trapping instruction. In + // order to verify it has a stackmap, we need to find the associated + // stackmap key, by adding the length of the instruction onto the + // trapsite key. And we find the length by asking + // SummarizeTrapInstruction. + uint32_t trapsiteKey = sitesForKind.getPCoffset(tsi); + + SummarizeResult summary = SummarizeTrapInstruction(masm, trapsiteKey); + if (!summary.identified()) { + JitSpew(JitSpew_Codegen, + " FAIL could not identify trap insn at 0x%06x", + trapsiteKey); + // Failure of this is serious: it means the trapsiteKey we got doesn't + // point at a plausible trap instruction. + MOZ_CRASH( + "wasm::CheckStackMapsForTraps: " + "could not identify trapping instruction"); + } + + JitSpew(JitSpew_Codegen, + " checking tsKey=0x%06x tsKind=%s " + "insn.len=%u insn.tmi=%s", + trapsiteKey, NameOfTrap(kind), summary.length(), + ToString(summary.kind())); + + // So this is the stack map key we expect. + uint32_t stackmapKey = trapsiteKey + summary.length(); + const StackMap* stackmap = stackMaps.lookup(stackmapKey); + stackmapCheckOK = stackmapCheckOK && !!stackmap; + JitSpew(JitSpew_Codegen, " %s tsKey=0x%06x smKey=0x%06x kind=%s", + stackmap ? "present" : "MISSING", trapsiteKey, stackmapKey, + NameOfTrap(kind)); + if (!stackmap) { + // There are two reasons we can get here. The more obvious is that + // there isn't a stackmap corresponding to the trap at `trapsiteKey`, + // so the fix is to add one. + // + // The less obvious reason is that there is a stackmap, but + // SummarizeTrapInstruction computes an incorrect `summary.length()`, + // hence `stackmapKey` is wrong. On x86_{32,64}, instruction length + // computation is non-trivial, so this is a real possibility that + // should be investigated. + // + // Recall that the key used to create a stackmap for a trap is derived + // from the trapping instruction's FaultingCodeRange, which is derived + // from assembler offsets, and is not computed by + // SummarizeTrapInstruction. Hence, technically, an incorrect + // `stackmapKey` value indicates an inconsistency between the length + // from the FaultingCodeRange and the length as computed by + // SummarizeTrapInstruction. Given that the FaultingCodeRange + // mechanism is very simple, it's more likely SummarizeTrapInstruction + // is wrong. + MOZ_CRASH( + "wasm::CheckStackMapsForTraps: " + "trapping instruction lacks a stackmap"); + } + } + } + MOZ_ASSERT(stackmapCheckOK); +} +#endif diff --git a/js/src/wasm/WasmGC.h b/js/src/wasm/WasmGC.h index 5cd7ae59d7d9..0a0f03127ed1 100644 --- a/js/src/wasm/WasmGC.h +++ b/js/src/wasm/WasmGC.h @@ -622,14 +622,54 @@ void CheckWholeCellLastElementCache(jit::MacroAssembler& masm, jit::Label* skipBarrier); #ifdef DEBUG -// Check (approximately) whether `nextPC` is a valid code address for a -// stackmap created by this compiler. This is done by examining the -// instruction at `nextPC`. The matching is inexact, so it may err on the -// side of returning `true` if it doesn't know. Doing so reduces the -// effectiveness of the MOZ_ASSERTs that use this function, so at least for -// the four primary platforms we should keep it as exact as possible. +// Check (approximately) whether `base + stackmapOffset` is a valid key (code +// address) for a wasm stackmap. This is done by examining the instruction +// immediately preceding `base + stackmapOffset`, since stackmaps are keyed by +// the address/offset of the first byte of the instruction following the +// instruction with which the stackmap is associated. +// +// The matching is inexact, so it may err on the side of returning `true` if it +// doesn't know. Doing so reduces the effectiveness of the MOZ_ASSERTs that use +// this function, so at least for the four primary platforms we should keep it +// as exact as possible. +// +// The matching is unavoidably inexact at least on x86/x86_64, since we don't +// know the start point of the previous instruction, and so have to resort to +// looking backwards from the start point we've been given. That's problematic +// because instructions don't parse uniquely "backwards". For example, we might +// hope to identify UD2 (which is 0F 0B) by checking +// +// key[-2] == 0x0F && key[-1] == 0B +// +// but because constants are stored at the end of instructions, +// +// movl $0x0B0F1234, %eax +// +// would also end with 0F 0B. This introduces some inaccuracy into the process, +// but it is in the direction of false positives, which we tolerate since this +// is a debug-only facility we use for identifying obviously-bogus stackmap +// keys. Also, the above ambiguity is expected to be rare in practice. +// +// In short: +// * it is OK to claim an invalid key is valid (`true` is returned) +// * it is not OK to claim a valid key is invalid (`false` returned) +bool IsPlausibleStackMapKey(const uint8_t* base, uint32_t stackmapOffset); -bool IsPlausibleStackMapKey(const uint8_t* nextPC); +using TrapSitesFrontierArray = + mozilla::EnumeratedArray; + +// Check that traps have an associated stack map. The check is performed only +// for traps `t` for which `checkThisTrapKind` returns `true`. For each such +// `t`, trap site indices to be checked are taken from `trapSitesBefore[t]` to +// `trapSitesAfter[t] - 1`. The trap sites and instructions to inspect are to +// be found in `masm`, and the corresponding stack maps in `stackMaps`. +// +// Returns without comment on success; MOZ_ASSERTs on failure. +void CheckStackMapsForTraps(const jit::MacroAssembler& masm, + const StackMaps& stackMaps, + const TrapSitesFrontierArray& trapSitesBefore, + const TrapSitesFrontierArray& trapSitesAfter, + bool (*checkThisTrapKind)(Trap)); #endif } // namespace wasm diff --git a/js/src/wasm/WasmIonCompile.cpp b/js/src/wasm/WasmIonCompile.cpp index 5effbbea3f46..4c014e1c84c6 100644 --- a/js/src/wasm/WasmIonCompile.cpp +++ b/js/src/wasm/WasmIonCompile.cpp @@ -11212,6 +11212,16 @@ bool wasm::IonCompileFunctions(const CodeMetadata& codeMeta, "# wasm::IonCompileFunctions: starting on function index %d", (int)func.index); +#ifdef DEBUG + // Snapshot the "frontier" of the trapsite vectors so we can determine + // which ones are added to during compilation of this function. + mozilla::EnumeratedArray + trapSitesBefore; + for (Trap kind : mozilla::MakeEnumeratedRange(Trap::Limit)) { + trapSitesBefore[kind] = uint32_t(masm.trapSites().length(kind)); + } +#endif + Decoder d(func.begin, func.end, func.bytecodeOffset, error); // Build the local types vector. @@ -11285,6 +11295,24 @@ bool wasm::IonCompileFunctions(const CodeMetadata& codeMeta, return false; } +#ifdef DEBUG + // Get a second snapshot of the frontier of the TrapSite vectors, and + // use this to check that traps that need a stackmap, actually have one. + mozilla::EnumeratedArray + trapSitesAfter; + for (Trap kind : mozilla::MakeEnumeratedRange(Trap::Limit)) { + trapSitesAfter[kind] = uint32_t(masm.trapSites().length(kind)); + } + + // Do the check. This asserts if the check fails. + auto checkThisTrapKind = [](Trap t) -> bool { + // Temporary setting, to make all of this a no-op. + return false; + }; + CheckStackMapsForTraps(masm, code->stackMaps, trapSitesBefore, + trapSitesAfter, checkThisTrapKind); +#endif + JitSpew(JitSpew_Codegen, "# wasm::IonCompileFunctions: completed function index %d", (int)func.index); diff --git a/js/src/wasm/WasmSummarizeInsn.cpp b/js/src/wasm/WasmSummarizeInsn.cpp index 45040690d61b..05fc56a462e0 100644 --- a/js/src/wasm/WasmSummarizeInsn.cpp +++ b/js/src/wasm/WasmSummarizeInsn.cpp @@ -20,6 +20,63 @@ using namespace js::jit; namespace js { namespace wasm { +// ==================================================================== +// === InstructionBytes + +// A virtual base class that provides instruction bytes for +// SummarizeTrapInstruction to examine. The object is conceptually +// regarded as "pointing" at the first byte of the instruction. +class InstructionBytes { + public: + // Get the byte at `offset` from the first byte of the instruction + virtual uint8_t get(size_t offset) const = 0; + // Convenience function. Check whether the first byte of the instruction + // would be 32-bit aligned. In case of doubt, return `true`. + virtual bool isU32aligned() const = 0; + // Convenience function. Fetch a U32, little-endianly. + uint32_t getU32LittleEndian(size_t offset) const { + MOZ_ASSERT((offset & 3) == 0); + uint32_t word = 0; + for (uint32_t i = 0; i < 4; i++) { + word = (word << 8) | uint32_t(get(offset + (3 - i))); + } + return word; + } +}; + +// An InstructionBytes source that pulls bytes out of arbitrary memory. +class InstructionBytesAbsolute : public InstructionBytes { + const uint8_t* insn_ = nullptr; + + public: + explicit InstructionBytesAbsolute(const uint8_t* insn) : insn_(insn) {} + bool isU32aligned() const override { return (uintptr_t(insn_) & 3) == 0; } + uint8_t get(size_t offset) const override { + MOZ_ASSERT(offset < 16); + return insn_[offset]; + } +}; + +// An InstructionBytes source that reads bytes from an assembler buffer. +class InstructionBytesFromMasm : public wasm::InstructionBytes { + const MacroAssembler& masm_; + uint32_t baseOffset_ = 0; + + public: + explicit InstructionBytesFromMasm(const MacroAssembler& masm, + uint32_t baseOffset) + : masm_(masm), baseOffset_(baseOffset) { + MOZ_ASSERT(baseOffset < masm.readableSize()); + } + bool isU32aligned() const override { return (baseOffset_ & 3) == 0; } + uint8_t get(size_t offset) const override { + return masm_.getByteAtOffset(size_t(baseOffset_) + offset); + } +}; + +// ==================================================================== +// === SummarizeTrapInstruction + // Sources of documentation of instruction-set encoding: // // Documentation for the ARM instruction sets can be found at @@ -182,7 +239,7 @@ static uint8_t ImmediateSizeFromOperationSize(uint8_t opSizeInBytes) { } } -SummarizeResult SummarizeTrapInstruction(const InstructionBytes& insn) { +static SummarizeResult SummarizeTrapInstruction(const InstructionBytes& insn) { // A note on computing instruction lengths. Almost all instructions include // a so-called ModR/M (modrm) byte. If the modrm byte has been determined // to be at `delta + N` then the length of the instruction as a whole is @@ -1012,7 +1069,7 @@ SummarizeResult SummarizeTrapInstruction(const InstructionBytes& insn) { #elif defined(JS_CODEGEN_ARM64) -SummarizeResult SummarizeTrapInstruction(const InstructionBytes& insn) { +static SummarizeResult SummarizeTrapInstruction(const InstructionBytes& insn) { // Check instruction alignment. MOZ_ASSERT(insn.isU32aligned()); @@ -1362,7 +1419,7 @@ SummarizeResult SummarizeTrapInstruction(const InstructionBytes& insn) { #elif defined(JS_CODEGEN_ARM) -SummarizeResult SummarizeTrapInstruction(const InstructionBytes& insn) { +static SummarizeResult SummarizeTrapInstruction(const InstructionBytes& insn) { // Almost all AArch32 instructions that use the ARM encoding (not Thumb) use // bits 31:28 as the guarding condition. Since we do not expect to // encounter conditional loads or stores, most of the following is hardcoded @@ -1560,7 +1617,7 @@ SummarizeResult SummarizeTrapInstruction(const InstructionBytes& insn) { #elif defined(JS_CODEGEN_RISCV64) -SummarizeResult SummarizeTrapInstruction(const InstructionBytes& insn) { +static SummarizeResult SummarizeTrapInstruction(const InstructionBytes& insn) { // Check instruction alignment. MOZ_ASSERT(insn.isU32aligned()); @@ -2151,7 +2208,7 @@ SummarizeResult SummarizeTrapInstruction(const InstructionBytes& insn) { #elif defined(JS_CODEGEN_NONE) -SummarizeResult SummarizeTrapInstruction(const InstructionBytes& insn) { +static SummarizeResult SummarizeTrapInstruction(const InstructionBytes& insn) { MOZ_CRASH(); } @@ -2163,11 +2220,17 @@ SummarizeResult SummarizeTrapInstruction(const InstructionBytes& insn) { #endif // defined(JS_CODEGEN_*) -// Convenience function that calls the above. +// External interface SummarizeResult SummarizeTrapInstruction(const uint8_t* insn) { const InstructionBytesAbsolute iba(insn); return SummarizeTrapInstruction(iba); } +SummarizeResult SummarizeTrapInstruction(const MacroAssembler& masm, + uint32_t offset) { + const InstructionBytesFromMasm ibfm(masm, offset); + return SummarizeTrapInstruction(ibfm); +} + } // namespace wasm } // namespace js diff --git a/js/src/wasm/WasmSummarizeInsn.h b/js/src/wasm/WasmSummarizeInsn.h index d939bd000885..bf50238120a5 100644 --- a/js/src/wasm/WasmSummarizeInsn.h +++ b/js/src/wasm/WasmSummarizeInsn.h @@ -5,45 +5,14 @@ #ifndef wasm_WasmSummarizeInsn_h #define wasm_WasmSummarizeInsn_h +#include "mozilla/Assertions.h" + +#include "jit/MacroAssembler.h" #include "wasm/WasmCodegenTypes.h" // TrapMachineInsn namespace js { namespace wasm { -// A virtual base class that provides instruction bytes for -// SummarizeTrapInstruction to examine. The object is conceptually -// regarded as "pointing" at the first byte of the instruction. -class InstructionBytes { - public: - // Get the byte at `offset` from the first byte of the instruction - virtual uint8_t get(size_t offset) const = 0; - // Convenience function. Check whether the first byte of the instruction - // would be 32-bit aligned. In case of doubt, return `true`. - virtual bool isU32aligned() const = 0; - // Convenience function. Fetch a U32, little-endianly. - uint32_t getU32LittleEndian(size_t offset) const { - MOZ_ASSERT((offset & 3) == 0); - uint32_t word = 0; - for (uint32_t i = 0; i < 4; i++) { - word = (word << 8) | uint32_t(get(offset + (3 - i))); - } - return word; - } -}; - -// A child of the above, that pulls bytes out of arbitrary memory. -class InstructionBytesAbsolute : public InstructionBytes { - const uint8_t* insn_; - - public: - explicit InstructionBytesAbsolute(const uint8_t* insn) : insn_(insn) {} - bool isU32aligned() const override { return (uintptr_t(insn_) & 3) == 0; } - uint8_t get(size_t offset) const override { - MOZ_ASSERT(offset < 16); - return insn_[offset]; - } -}; - // SummarizeResult holds the result of a call to SummarizeTrapInstruction. // If the instruction has been identified and its length computed, // Status::Identified is set, and the instruction's TrapMachineInsn value and @@ -95,13 +64,13 @@ class SummarizeResult { // created by wasm-baseline or -Ion. So it doesn't need to handle the whole // complexity of the machine's instruction set. It only needs to handle the // subset used by the trappable instructions we actually generate. - -// The main entry point. -SummarizeResult SummarizeTrapInstruction(const InstructionBytes& insn); - -// Convenience function that calls the above. SummarizeResult SummarizeTrapInstruction(const uint8_t* insn); +// And here's a variant that reads from an assembler buffer, at the given +// offset. +SummarizeResult SummarizeTrapInstruction(const jit::MacroAssembler& masm, + uint32_t offset); + } // namespace wasm } // namespace js diff --git a/js/xpconnect/src/XPCShellImpl.cpp b/js/xpconnect/src/XPCShellImpl.cpp index c56b9ccf4223..10e9e990da62 100644 --- a/js/xpconnect/src/XPCShellImpl.cpp +++ b/js/xpconnect/src/XPCShellImpl.cpp @@ -62,7 +62,6 @@ # include "mozilla/mscom/ProcessRuntime.h" # include "mozilla/ScopeExit.h" # include "mozilla/WinDllServices.h" -# include "mozilla/WindowsBCryptInitialization.h" # include # if defined(MOZ_SANDBOX) @@ -1307,10 +1306,6 @@ int XRE_XPCShellMain(int argc, char** argv, char** envp, } # endif // defined(MOZ_SANDBOX) - { - DebugOnly result = WindowsBCryptInitialization(); - MOZ_ASSERT(result); - } #endif // defined(XP_WIN) #ifdef MOZ_CODE_COVERAGE diff --git a/layout/generic/nsBlockFrame.cpp b/layout/generic/nsBlockFrame.cpp index 5dccb5a7f997..614a8fd41024 100644 --- a/layout/generic/nsBlockFrame.cpp +++ b/layout/generic/nsBlockFrame.cpp @@ -5803,11 +5803,14 @@ bool nsBlockFrame::IsLastInlineLine(LineIterator aLine) { } bool nsBlockFrame::IsLastFormattedLine(LineIterator aLine) { + // Check if any later lines are non-empty/non-invisible for (LineIterator line = aLine.next(); line != LinesEnd(); ++line) { if (line->GetChildCount() > 0 && (line->IsBlock() || !line->IsPhantom())) { return false; } } + + // Check if any continuations have non-empty/non-invisible lines nsBlockFrame* nextInFlow = (nsBlockFrame*)GetNextInFlow(); while (nextInFlow) { for (const auto& line : nextInFlow->Lines()) { @@ -5817,6 +5820,15 @@ bool nsBlockFrame::IsLastFormattedLine(LineIterator aLine) { } nextInFlow = (nsBlockFrame*)nextInFlow->GetNextInFlow(); } + + // Check if any child frames of this line have overflow frames + // that will be pulled into the next line when it is reflowed + for (nsIFrame* f : aLine->ChildFrames()) { + if (f->GetProperty(nsContainerFrame::OverflowProperty())) { + return false; + } + } + return true; } diff --git a/layout/svg/SVGGradientFrame.cpp b/layout/svg/SVGGradientFrame.cpp index d8f6aa6e15c6..eebcced86ec1 100644 --- a/layout/svg/SVGGradientFrame.cpp +++ b/layout/svg/SVGGradientFrame.cpp @@ -99,7 +99,6 @@ uint16_t SVGGradientFrame::GetEnumValue(uint32_t aIndex, nsIContent* aDefault) { } uint16_t SVGGradientFrame::GetGradientUnits() { - // This getter is called every time the others are called - maybe cache it? return GetEnumValue(dom::SVGGradientElement::GRADIENTUNITS); } @@ -129,11 +128,11 @@ SVGGradientFrame* SVGGradientFrame::GetGradientTransformFrame( } gfxMatrix SVGGradientFrame::GetGradientTransform( - nsIFrame* aSource, const gfxRect* aOverrideBounds) { + nsIFrame* aSource, uint16_t aGradientUnits, + const gfxRect* aOverrideBounds) { gfxMatrix bboxMatrix; - uint16_t gradientUnits = GetGradientUnits(); - if (gradientUnits != SVG_UNIT_TYPE_USERSPACEONUSE) { - NS_ASSERTION(gradientUnits == SVG_UNIT_TYPE_OBJECTBOUNDINGBOX, + if (aGradientUnits != SVG_UNIT_TYPE_USERSPACEONUSE) { + NS_ASSERTION(aGradientUnits == SVG_UNIT_TYPE_OBJECTBOUNDINGBOX, "Unknown gradientUnits type"); // objectBoundingBox is the default anyway @@ -267,7 +266,7 @@ already_AddRefed SVGGradientFrame::GetPaintServerPattern( return MakeAndAddRef(DeviceColor()); } - if (nStops == 1 || GradientVectorLengthIsZero()) { + if (nStops == 1 || GradientVectorLengthIsZero(gradientUnits)) { // The gradient paints a single colour, using the stop-color of the last // gradient step if there are more than one. return MakeAndAddRef(ToDeviceColor(stops.LastElement().mColor)); @@ -276,7 +275,8 @@ already_AddRefed SVGGradientFrame::GetPaintServerPattern( // Get the transform list (if there is one). We do this after the returns // above since this call can be expensive when "gradientUnits" is set to // "objectBoundingBox" (since that requiring a GetBBox() call). - gfxMatrix patternMatrix = GetGradientTransform(aSource, aOverrideBounds); + gfxMatrix patternMatrix = + GetGradientTransform(aSource, gradientUnits, aOverrideBounds); if (patternMatrix.IsSingular()) { return nullptr; } @@ -293,7 +293,7 @@ already_AddRefed SVGGradientFrame::GetPaintServerPattern( return nullptr; } - RefPtr gradient = CreateGradient(); + RefPtr gradient = CreateGradient(gradientUnits); if (!gradient) { return nullptr; } @@ -327,17 +327,17 @@ already_AddRefed SVGGradientFrame::GetPaintServerPattern( // Private (helper) methods -float SVGGradientFrame::GetLengthValue(const SVGAnimatedLength& aLength) { +float SVGGradientFrame::GetLengthValue(uint16_t aGradientUnits, + const SVGAnimatedLength& aLength) { // Object bounding box units are handled by setting the appropriate // transform in GetGradientTransform, but we need to handle user // space units as part of the individual Get* routines. Fixes 323669. - uint16_t gradientUnits = GetGradientUnits(); - if (gradientUnits == SVG_UNIT_TYPE_USERSPACEONUSE) { + if (aGradientUnits == SVG_UNIT_TYPE_USERSPACEONUSE) { return SVGUtils::UserSpace(mSource, &aLength); } - NS_ASSERTION(gradientUnits == SVG_UNIT_TYPE_OBJECTBOUNDINGBOX, + NS_ASSERTION(aGradientUnits == SVG_UNIT_TYPE_OBJECTBOUNDINGBOX, "Unknown gradientUnits type"); if (aLength.IsPercentage()) { @@ -439,7 +439,8 @@ nsresult SVGLinearGradientFrame::AttributeChanged(int32_t aNameSpaceID, //---------------------------------------------------------------------- -float SVGLinearGradientFrame::GetLengthValue(uint32_t aIndex) { +float SVGLinearGradientFrame::GetLengthValue(uint16_t aGradientUnits, + uint32_t aIndex) { dom::SVGLinearGradientElement* lengthElement = GetLinearGradientWithLength( aIndex, static_cast(GetContent())); // We passed in mContent as a fallback, so, assuming mContent is non-null, the @@ -447,7 +448,8 @@ float SVGLinearGradientFrame::GetLengthValue(uint32_t aIndex) { MOZ_ASSERT(lengthElement, "Got unexpected null element from GetLinearGradientWithLength"); - return GetLengthValue(lengthElement->mLengthAttributes[aIndex]); + return GetLengthValue(aGradientUnits, + lengthElement->mLengthAttributes[aIndex]); } dom::SVGLinearGradientElement* @@ -464,18 +466,28 @@ SVGLinearGradientFrame::GetLinearGradientWithLength( return SVGGradientFrame::GetLinearGradientWithLength(aIndex, aDefault); } -bool SVGLinearGradientFrame::GradientVectorLengthIsZero() { - return GetLengthValue(dom::SVGLinearGradientElement::ATTR_X1) == - GetLengthValue(dom::SVGLinearGradientElement::ATTR_X2) && - GetLengthValue(dom::SVGLinearGradientElement::ATTR_Y1) == - GetLengthValue(dom::SVGLinearGradientElement::ATTR_Y2); +bool SVGLinearGradientFrame::GradientVectorLengthIsZero( + uint16_t aGradientUnits) { + return GetLengthValue(aGradientUnits, + dom::SVGLinearGradientElement::ATTR_X1) == + GetLengthValue(aGradientUnits, + dom::SVGLinearGradientElement::ATTR_X2) && + GetLengthValue(aGradientUnits, + dom::SVGLinearGradientElement::ATTR_Y1) == + GetLengthValue(aGradientUnits, + dom::SVGLinearGradientElement::ATTR_Y2); } -already_AddRefed SVGLinearGradientFrame::CreateGradient() { - float x1 = GetLengthValue(dom::SVGLinearGradientElement::ATTR_X1); - float y1 = GetLengthValue(dom::SVGLinearGradientElement::ATTR_Y1); - float x2 = GetLengthValue(dom::SVGLinearGradientElement::ATTR_X2); - float y2 = GetLengthValue(dom::SVGLinearGradientElement::ATTR_Y2); +already_AddRefed SVGLinearGradientFrame::CreateGradient( + uint16_t aGradientUnits) { + float x1 = + GetLengthValue(aGradientUnits, dom::SVGLinearGradientElement::ATTR_X1); + float y1 = + GetLengthValue(aGradientUnits, dom::SVGLinearGradientElement::ATTR_Y1); + float x2 = + GetLengthValue(aGradientUnits, dom::SVGLinearGradientElement::ATTR_X2); + float y2 = + GetLengthValue(aGradientUnits, dom::SVGLinearGradientElement::ATTR_Y2); return MakeAndAddRef(x1, y1, x2, y2); } @@ -514,7 +526,8 @@ nsresult SVGRadialGradientFrame::AttributeChanged(int32_t aNameSpaceID, //---------------------------------------------------------------------- -float SVGRadialGradientFrame::GetLengthValue(uint32_t aIndex, +float SVGRadialGradientFrame::GetLengthValue(uint16_t aGradientUnits, + uint32_t aIndex, Maybe aDefaultValue) { dom::SVGRadialGradientElement* lengthElement = GetRadialGradientWithLength( aIndex, !aDefaultValue @@ -527,7 +540,8 @@ float SVGRadialGradientFrame::GetLengthValue(uint32_t aIndex, "Got unexpected null element from GetRadialGradientWithLength"); return lengthElement - ? GetLengthValue(lengthElement->mLengthAttributes[aIndex]) + ? GetLengthValue(aGradientUnits, + lengthElement->mLengthAttributes[aIndex]) : *aDefaultValue; } @@ -545,25 +559,39 @@ SVGRadialGradientFrame::GetRadialGradientWithLength( return SVGGradientFrame::GetRadialGradientWithLength(aIndex, aDefault); } -bool SVGRadialGradientFrame::GradientVectorLengthIsZero() { - float cx = GetLengthValue(dom::SVGRadialGradientElement::ATTR_CX); - float cy = GetLengthValue(dom::SVGRadialGradientElement::ATTR_CY); - float r = GetLengthValue(dom::SVGRadialGradientElement::ATTR_R); +bool SVGRadialGradientFrame::GradientVectorLengthIsZero( + uint16_t aGradientUnits) { + float cx = + GetLengthValue(aGradientUnits, dom::SVGRadialGradientElement::ATTR_CX); + float cy = + GetLengthValue(aGradientUnits, dom::SVGRadialGradientElement::ATTR_CY); + float r = + GetLengthValue(aGradientUnits, dom::SVGRadialGradientElement::ATTR_R); // If fx or fy are not set, use cx/cy instead - float fx = GetLengthValue(dom::SVGRadialGradientElement::ATTR_FX, cx); - float fy = GetLengthValue(dom::SVGRadialGradientElement::ATTR_FY, cy); - float fr = GetLengthValue(dom::SVGRadialGradientElement::ATTR_FR); + float fx = GetLengthValue(aGradientUnits, + dom::SVGRadialGradientElement::ATTR_FX, cx); + float fy = GetLengthValue(aGradientUnits, + dom::SVGRadialGradientElement::ATTR_FY, cy); + float fr = + GetLengthValue(aGradientUnits, dom::SVGRadialGradientElement::ATTR_FR); return cx == fx && cy == fy && r == fr; } -already_AddRefed SVGRadialGradientFrame::CreateGradient() { - float cx = GetLengthValue(dom::SVGRadialGradientElement::ATTR_CX); - float cy = GetLengthValue(dom::SVGRadialGradientElement::ATTR_CY); - float r = GetLengthValue(dom::SVGRadialGradientElement::ATTR_R); +already_AddRefed SVGRadialGradientFrame::CreateGradient( + uint16_t aGradientUnits) { + float cx = + GetLengthValue(aGradientUnits, dom::SVGRadialGradientElement::ATTR_CX); + float cy = + GetLengthValue(aGradientUnits, dom::SVGRadialGradientElement::ATTR_CY); + float r = + GetLengthValue(aGradientUnits, dom::SVGRadialGradientElement::ATTR_R); // If fx or fy are not set, use cx/cy instead - float fx = GetLengthValue(dom::SVGRadialGradientElement::ATTR_FX, cx); - float fy = GetLengthValue(dom::SVGRadialGradientElement::ATTR_FY, cy); - float fr = GetLengthValue(dom::SVGRadialGradientElement::ATTR_FR); + float fx = GetLengthValue(aGradientUnits, + dom::SVGRadialGradientElement::ATTR_FX, cx); + float fy = GetLengthValue(aGradientUnits, + dom::SVGRadialGradientElement::ATTR_FY, cy); + float fr = + GetLengthValue(aGradientUnits, dom::SVGRadialGradientElement::ATTR_FR); return MakeAndAddRef(fx, fy, fr, cx, cy, r); } diff --git a/layout/svg/SVGGradientFrame.h b/layout/svg/SVGGradientFrame.h index 777fc5d2a83b..b7025ef28a6b 100644 --- a/layout/svg/SVGGradientFrame.h +++ b/layout/svg/SVGGradientFrame.h @@ -74,12 +74,13 @@ class SVGGradientFrame : public SVGPaintServerFrame { SVGGradientFrame* GetGradientTransformFrame(SVGGradientFrame* aDefault); // Will be singular for gradientUnits="objectBoundingBox" with an empty bbox. - gfxMatrix GetGradientTransform(nsIFrame* aSource, + gfxMatrix GetGradientTransform(nsIFrame* aSource, uint16_t aGradientUnits, const gfxRect* aOverrideBounds); protected: - virtual bool GradientVectorLengthIsZero() = 0; - virtual already_AddRefed CreateGradient() = 0; + virtual bool GradientVectorLengthIsZero(uint16_t aGradientUnits) = 0; + virtual already_AddRefed CreateGradient( + uint16_t aGradientUnits) = 0; // Accessors to lookup gradient attributes uint16_t GetEnumValue(uint32_t aIndex, nsIContent* aDefault); @@ -88,7 +89,8 @@ class SVGGradientFrame : public SVGPaintServerFrame { } uint16_t GetGradientUnits(); uint16_t GetSpreadMethod(); - float GetLengthValue(const SVGAnimatedLength& aLength); + float GetLengthValue(uint16_t aGradientUnits, + const SVGAnimatedLength& aLength); // Gradient-type-specific lookups since the length values differ between // linear and radial gradients @@ -144,12 +146,12 @@ class SVGLinearGradientFrame final : public SVGGradientFrame { protected: using SVGGradientFrame::GetLengthValue; - float GetLengthValue(uint32_t aIndex); + float GetLengthValue(uint16_t aGradientUnits, uint32_t aIndex); mozilla::dom::SVGLinearGradientElement* GetLinearGradientWithLength( uint32_t aIndex, mozilla::dom::SVGLinearGradientElement* aDefault) override; - bool GradientVectorLengthIsZero() override; - already_AddRefed CreateGradient() override; + bool GradientVectorLengthIsZero(uint16_t aGradientUnits) override; + already_AddRefed CreateGradient(uint16_t aGradientUnits) override; }; // ------------------------------------------------------------------------- @@ -186,15 +188,17 @@ class SVGRadialGradientFrame final : public SVGGradientFrame { protected: using SVGGradientFrame::GetLengthValue; - float GetLengthValue(uint32_t aIndex, Maybe aDefaultValue = Nothing()); - float GetLengthValue(uint32_t aIndex, float aDefaultValue) { - return GetLengthValue(aIndex, Some(aDefaultValue)); + float GetLengthValue(uint16_t aGradientUnits, uint32_t aIndex, + Maybe aDefaultValue = Nothing()); + float GetLengthValue(uint16_t aGradientUnits, uint32_t aIndex, + float aDefaultValue) { + return GetLengthValue(aGradientUnits, aIndex, Some(aDefaultValue)); } mozilla::dom::SVGRadialGradientElement* GetRadialGradientWithLength( uint32_t aIndex, mozilla::dom::SVGRadialGradientElement* aDefault) override; - bool GradientVectorLengthIsZero() override; - already_AddRefed CreateGradient() override; + bool GradientVectorLengthIsZero(uint16_t aGradientUnits) override; + already_AddRefed CreateGradient(uint16_t aGradientUnits) override; }; } // namespace mozilla diff --git a/media/webrtc/signaling/gtest/MockJsepCodecPreferences.h b/media/webrtc/signaling/gtest/MockJsepCodecPreferences.h index 174df18b6615..f10a0056f511 100644 --- a/media/webrtc/signaling/gtest/MockJsepCodecPreferences.h +++ b/media/webrtc/signaling/gtest/MockJsepCodecPreferences.h @@ -11,14 +11,15 @@ namespace mozilla { /* -This provides a stable set of codec preferences for unit tests. In order to -change a preference, you can set the member variable to the desired value. -*/ + * This provides a stable set of codec preferences for unit tests. In order to + * change a preference, you can set the member variable to the desired value. + */ struct MockJsepCodecPreferences : public JsepCodecPreferences { bool AV1Enabled() const override { return mAv1Enabled; } bool AV1Preferred() const override { return mAv1Preferred; } bool H264Enabled() const override { return mH264Enabled; } bool SoftwareH264Enabled() const override { return mSoftwareH264Enabled; } + bool HardwareH264Enabled() const override { return mHardwareH264Enabled; } bool SendingH264PacketizationModeZeroSupported() const override { return mH264PacketizationModeZeroSupported; } @@ -42,6 +43,7 @@ struct MockJsepCodecPreferences : public JsepCodecPreferences { bool mAv1Preferred = false; bool mH264Enabled = true; bool mSoftwareH264Enabled = true; + bool mHardwareH264Enabled = false; bool mH264PacketizationModeZeroSupported = true; bool mH264BaselineDisabled = StaticPrefs::GetPrefDefault_media_navigator_video_disable_h264_baseline(); diff --git a/media/webrtc/signaling/gtest/jsep_session_unittest.cpp b/media/webrtc/signaling/gtest/jsep_session_unittest.cpp index 4bc883f8b9da..d27a0b933071 100644 --- a/media/webrtc/signaling/gtest/jsep_session_unittest.cpp +++ b/media/webrtc/signaling/gtest/jsep_session_unittest.cpp @@ -78,8 +78,10 @@ class JsepSessionTest : public JsepSessionTestBase, EXPECT_EQ(NS_OK, mSessionOff->Init()); EXPECT_EQ(NS_OK, mSessionAns->Init()); - std::vector> preferredCodecs; - PeerConnectionImpl::SetupPreferredCodecs(preferredCodecs); + DefaultCodecPreferences prefs; + AutoTArray, 16> preferredCodecs; + EnumerateDefaultVideoCodecs(&preferredCodecs, prefs); + EnumerateDefaultAudioCodecs(&preferredCodecs, prefs); for (auto& codec : preferredCodecs) { // Make H264 P0 recvonly everywhere for better test coverage. // TODO: For unit testing JSEP, the preferred codecs list should be @@ -94,8 +96,8 @@ class JsepSessionTest : public JsepSessionTestBase, mSessionOff->SetDefaultCodecs(preferredCodecs); mSessionAns->SetDefaultCodecs(preferredCodecs); - std::vector preferredHeaders; - PeerConnectionImpl::SetupPreferredRtpExtensions(preferredHeaders); + AutoTArray preferredHeaders; + PeerConnectionImpl::GetDefaultRtpExtensions(prefs, &preferredHeaders); for (const auto& header : preferredHeaders) { mSessionOff->AddRtpExtension(header.mMediaType, header.extensionname, @@ -1406,7 +1408,7 @@ class JsepSessionTest : public JsepSessionTestBase, return parsed; } - std::string SetExtmap(const std::string& aSdp, const std::string& aUri, + std::string SetExtmap(const std::string& aSdp, const nsACString& aUri, uint16_t aId, uint16_t* aOldId = nullptr) { UniquePtr munge(Parse(aSdp)); for (size_t i = 0; i < munge->GetMediaSectionCount(); ++i) { @@ -1434,7 +1436,7 @@ class JsepSessionTest : public JsepSessionTestBase, return munge->ToString(); } - uint16_t GetExtmap(const std::string& aSdp, const std::string& aUri) { + uint16_t GetExtmap(const std::string& aSdp, const nsACString& aUri) { UniquePtr parsed(Parse(aSdp)); for (size_t i = 0; i < parsed->GetMediaSectionCount(); ++i) { auto& attrs = parsed->GetMediaSection(i).GetAttributeList(); @@ -3905,7 +3907,7 @@ TEST_F(JsepSessionTest, ValidateNoFmtpLineForRedInOfferAndAnswer) { ASSERT_TRUE(offerTransceivers[1].mSendTrack.GetNegotiatedDetails()); ASSERT_TRUE(offerTransceivers[1].mRecvTrack.GetNegotiatedDetails()); // Note that the number of recv/send codecs here differ because some codecs - // are recvonly. See SetupPreferredCodecs above. + // are recvonly. See EnumerateDefault*Codecs above. ASSERT_EQ(7U, offerTransceivers[1] .mSendTrack.GetNegotiatedDetails() ->GetEncoding(0) @@ -4587,9 +4589,9 @@ TEST_F(JsepSessionTest, TestExtmap) { // csrc-audio-level will be 2 for both // mid will be 3 for both // video related extensions take 4 - 7 - mSessionOff->AddAudioRtpExtension("foo"); // Default mapping of 8 - mSessionOff->AddAudioRtpExtension("bar"); // Default mapping of 9 - mSessionAns->AddAudioRtpExtension("bar"); // Default mapping of 8 + mSessionOff->AddAudioRtpExtension("foo"_ns); // Default mapping of 8 + mSessionOff->AddAudioRtpExtension("bar"_ns); // Default mapping of 9 + mSessionAns->AddAudioRtpExtension("bar"_ns); // Default mapping of 8 std::string offer = CreateOffer(); SetLocalOffer(offer, CHECK_SUCCESS); SetRemoteOffer(offer, CHECK_SUCCESS); @@ -4604,23 +4606,23 @@ TEST_F(JsepSessionTest, TestExtmap) { ASSERT_TRUE(offerMediaAttrs.HasAttribute(SdpAttribute::kExtmapAttribute)); auto& offerExtmap = offerMediaAttrs.GetExtmap().mExtmaps; ASSERT_EQ(6U, offerExtmap.size()); - ASSERT_EQ("urn:ietf:params:rtp-hdrext:ssrc-audio-level", + ASSERT_EQ("urn:ietf:params:rtp-hdrext:ssrc-audio-level"_ns, offerExtmap[0].extensionname); ASSERT_EQ(1U, offerExtmap[0].entry); - ASSERT_EQ("urn:ietf:params:rtp-hdrext:csrc-audio-level", + ASSERT_EQ("urn:ietf:params:rtp-hdrext:csrc-audio-level"_ns, offerExtmap[1].extensionname); ASSERT_EQ(2U, offerExtmap[1].entry); - ASSERT_EQ("urn:ietf:params:rtp-hdrext:sdes:mid", + ASSERT_EQ("urn:ietf:params:rtp-hdrext:sdes:mid"_ns, offerExtmap[2].extensionname); ASSERT_EQ(3U, offerExtmap[2].entry); ASSERT_EQ( "http://www.ietf.org/id/" - "draft-holmer-rmcat-transport-wide-cc-extensions-01", + "draft-holmer-rmcat-transport-wide-cc-extensions-01"_ns, offerExtmap[3].extensionname); ASSERT_EQ(7U, offerExtmap[3].entry); - ASSERT_EQ("foo", offerExtmap[4].extensionname); + ASSERT_EQ("foo"_ns, offerExtmap[4].extensionname); ASSERT_EQ(8U, offerExtmap[4].entry); - ASSERT_EQ("bar", offerExtmap[5].extensionname); + ASSERT_EQ("bar"_ns, offerExtmap[5].extensionname); ASSERT_EQ(9U, offerExtmap[5].entry); UniquePtr parsedAnswer(Parse(answer)); @@ -4630,19 +4632,19 @@ TEST_F(JsepSessionTest, TestExtmap) { ASSERT_TRUE(answerMediaAttrs.HasAttribute(SdpAttribute::kExtmapAttribute)); auto& answerExtmap = answerMediaAttrs.GetExtmap().mExtmaps; ASSERT_EQ(4U, answerExtmap.size()); - ASSERT_EQ("urn:ietf:params:rtp-hdrext:ssrc-audio-level", + ASSERT_EQ("urn:ietf:params:rtp-hdrext:ssrc-audio-level"_ns, answerExtmap[0].extensionname); ASSERT_EQ(1U, answerExtmap[0].entry); - ASSERT_EQ("urn:ietf:params:rtp-hdrext:sdes:mid", + ASSERT_EQ("urn:ietf:params:rtp-hdrext:sdes:mid"_ns, answerExtmap[1].extensionname); ASSERT_EQ(3U, answerExtmap[1].entry); ASSERT_EQ( "http://www.ietf.org/id/" - "draft-holmer-rmcat-transport-wide-cc-extensions-01", + "draft-holmer-rmcat-transport-wide-cc-extensions-01"_ns, answerExtmap[2].extensionname); ASSERT_EQ(7U, answerExtmap[2].entry); // We ensure that the entry for "bar" matches what was in the offer - ASSERT_EQ("bar", answerExtmap[3].extensionname); + ASSERT_EQ("bar"_ns, answerExtmap[3].extensionname); ASSERT_EQ(9U, answerExtmap[3].entry); } @@ -4669,17 +4671,17 @@ TEST_F(JsepSessionTest, TestExtmapDefaults) { auto& offerAudioExtmap = offerAudioMediaAttrs.GetExtmap().mExtmaps; ASSERT_EQ(4U, offerAudioExtmap.size()); - ASSERT_EQ("urn:ietf:params:rtp-hdrext:ssrc-audio-level", + ASSERT_EQ("urn:ietf:params:rtp-hdrext:ssrc-audio-level"_ns, offerAudioExtmap[0].extensionname); ASSERT_EQ(1U, offerAudioExtmap[0].entry); - ASSERT_EQ("urn:ietf:params:rtp-hdrext:csrc-audio-level", + ASSERT_EQ("urn:ietf:params:rtp-hdrext:csrc-audio-level"_ns, offerAudioExtmap[1].extensionname); ASSERT_EQ(2U, offerAudioExtmap[1].entry); - ASSERT_EQ("urn:ietf:params:rtp-hdrext:sdes:mid", + ASSERT_EQ("urn:ietf:params:rtp-hdrext:sdes:mid"_ns, offerAudioExtmap[2].extensionname); ASSERT_EQ( "http://www.ietf.org/id/" - "draft-holmer-rmcat-transport-wide-cc-extensions-01", + "draft-holmer-rmcat-transport-wide-cc-extensions-01"_ns, offerAudioExtmap[3].extensionname); ASSERT_EQ(7U, offerAudioExtmap[3].entry); @@ -4691,20 +4693,20 @@ TEST_F(JsepSessionTest, TestExtmapDefaults) { ASSERT_EQ(5U, offerVideoExtmap.size()); ASSERT_EQ(3U, offerVideoExtmap[0].entry); - ASSERT_EQ("urn:ietf:params:rtp-hdrext:sdes:mid", + ASSERT_EQ("urn:ietf:params:rtp-hdrext:sdes:mid"_ns, offerVideoExtmap[0].extensionname); - ASSERT_EQ("http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time", + ASSERT_EQ("http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time"_ns, offerVideoExtmap[1].extensionname); ASSERT_EQ(4U, offerVideoExtmap[1].entry); - ASSERT_EQ("urn:ietf:params:rtp-hdrext:toffset", + ASSERT_EQ("urn:ietf:params:rtp-hdrext:toffset"_ns, offerVideoExtmap[2].extensionname); ASSERT_EQ(5U, offerVideoExtmap[2].entry); - ASSERT_EQ("http://www.webrtc.org/experiments/rtp-hdrext/playout-delay", + ASSERT_EQ("http://www.webrtc.org/experiments/rtp-hdrext/playout-delay"_ns, offerVideoExtmap[3].extensionname); ASSERT_EQ(6U, offerVideoExtmap[3].entry); ASSERT_EQ( "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-" - "extensions-01", + "extensions-01"_ns, offerVideoExtmap[4].extensionname); ASSERT_EQ(7U, offerVideoExtmap[4].entry); @@ -4718,15 +4720,15 @@ TEST_F(JsepSessionTest, TestExtmapDefaults) { auto& answerAudioExtmap = answerAudioMediaAttrs.GetExtmap().mExtmaps; ASSERT_EQ(3U, answerAudioExtmap.size()); - ASSERT_EQ("urn:ietf:params:rtp-hdrext:ssrc-audio-level", + ASSERT_EQ("urn:ietf:params:rtp-hdrext:ssrc-audio-level"_ns, answerAudioExtmap[0].extensionname); ASSERT_EQ(1U, answerAudioExtmap[0].entry); - ASSERT_EQ("urn:ietf:params:rtp-hdrext:sdes:mid", + ASSERT_EQ("urn:ietf:params:rtp-hdrext:sdes:mid"_ns, answerAudioExtmap[1].extensionname); ASSERT_EQ(3U, answerAudioExtmap[1].entry); ASSERT_EQ( "http://www.ietf.org/id/" - "draft-holmer-rmcat-transport-wide-cc-extensions-01", + "draft-holmer-rmcat-transport-wide-cc-extensions-01"_ns, answerAudioExtmap[2].extensionname); ASSERT_EQ(7U, answerAudioExtmap[2].entry); @@ -4738,17 +4740,17 @@ TEST_F(JsepSessionTest, TestExtmapDefaults) { ASSERT_EQ(4U, answerVideoExtmap.size()); ASSERT_EQ(3U, answerVideoExtmap[0].entry); - ASSERT_EQ("urn:ietf:params:rtp-hdrext:sdes:mid", + ASSERT_EQ("urn:ietf:params:rtp-hdrext:sdes:mid"_ns, answerVideoExtmap[0].extensionname); - ASSERT_EQ("http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time", + ASSERT_EQ("http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time"_ns, answerVideoExtmap[1].extensionname); ASSERT_EQ(4U, answerVideoExtmap[1].entry); - ASSERT_EQ("urn:ietf:params:rtp-hdrext:toffset", + ASSERT_EQ("urn:ietf:params:rtp-hdrext:toffset"_ns, answerVideoExtmap[2].extensionname); ASSERT_EQ(5U, answerVideoExtmap[2].entry); ASSERT_EQ( "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-" - "extensions-01", + "extensions-01"_ns, answerVideoExtmap[3].extensionname); ASSERT_EQ(7U, answerVideoExtmap[3].entry); } @@ -4760,12 +4762,12 @@ TEST_F(JsepSessionTest, TestExtmapWithDuplicates) { // csrc-audio-level will be 2 for both // mid will be 3 for both // video related extensions take 4 - 7 - mSessionOff->AddAudioRtpExtension("foo"); // Default mapping of 8 - mSessionOff->AddAudioRtpExtension("bar"); // Default mapping of 9 - mSessionOff->AddAudioRtpExtension("bar"); // Should be ignored - mSessionOff->AddAudioRtpExtension("bar"); // Should be ignored - mSessionOff->AddAudioRtpExtension("baz"); // Default mapping of 10 - mSessionOff->AddAudioRtpExtension("bar"); // Should be ignored + mSessionOff->AddAudioRtpExtension("foo"_ns); // Default mapping of 8 + mSessionOff->AddAudioRtpExtension("bar"_ns); // Default mapping of 9 + mSessionOff->AddAudioRtpExtension("bar"_ns); // Should be ignored + mSessionOff->AddAudioRtpExtension("bar"_ns); // Should be ignored + mSessionOff->AddAudioRtpExtension("baz"_ns); // Default mapping of 10 + mSessionOff->AddAudioRtpExtension("bar"_ns); // Should be ignored std::string offer = CreateOffer(); UniquePtr parsedOffer(Parse(offer)); @@ -4775,25 +4777,25 @@ TEST_F(JsepSessionTest, TestExtmapWithDuplicates) { ASSERT_TRUE(offerMediaAttrs.HasAttribute(SdpAttribute::kExtmapAttribute)); auto& offerExtmap = offerMediaAttrs.GetExtmap().mExtmaps; ASSERT_EQ(7U, offerExtmap.size()); - ASSERT_EQ("urn:ietf:params:rtp-hdrext:ssrc-audio-level", + ASSERT_EQ("urn:ietf:params:rtp-hdrext:ssrc-audio-level"_ns, offerExtmap[0].extensionname); ASSERT_EQ(1U, offerExtmap[0].entry); - ASSERT_EQ("urn:ietf:params:rtp-hdrext:csrc-audio-level", + ASSERT_EQ("urn:ietf:params:rtp-hdrext:csrc-audio-level"_ns, offerExtmap[1].extensionname); ASSERT_EQ(2U, offerExtmap[1].entry); - ASSERT_EQ("urn:ietf:params:rtp-hdrext:sdes:mid", + ASSERT_EQ("urn:ietf:params:rtp-hdrext:sdes:mid"_ns, offerExtmap[2].extensionname); ASSERT_EQ(3U, offerExtmap[2].entry); ASSERT_EQ( "http://www.ietf.org/id/" - "draft-holmer-rmcat-transport-wide-cc-extensions-01", + "draft-holmer-rmcat-transport-wide-cc-extensions-01"_ns, offerExtmap[3].extensionname); ASSERT_EQ(7U, offerExtmap[3].entry); - ASSERT_EQ("foo", offerExtmap[4].extensionname); + ASSERT_EQ("foo"_ns, offerExtmap[4].extensionname); ASSERT_EQ(8U, offerExtmap[4].entry); - ASSERT_EQ("bar", offerExtmap[5].extensionname); + ASSERT_EQ("bar"_ns, offerExtmap[5].extensionname); ASSERT_EQ(9U, offerExtmap[5].entry); - ASSERT_EQ("baz", offerExtmap[6].extensionname); + ASSERT_EQ("baz"_ns, offerExtmap[6].extensionname); ASSERT_EQ(10U, offerExtmap[6].entry); } @@ -5027,25 +5029,27 @@ TEST_F(JsepSessionTest, TestNegotiatedExtmapStability) { ASSERT_TRUE(audioRecv); ASSERT_TRUE(videoSend); ASSERT_TRUE(videoRecv); - ASSERT_EQ( - 11U, - audioSend->GetExt("urn:ietf:params:rtp-hdrext:ssrc-audio-level")->entry); - ASSERT_EQ( - 11U, - audioRecv->GetExt("urn:ietf:params:rtp-hdrext:ssrc-audio-level")->entry); + ASSERT_EQ(11U, + audioSend->GetExt("urn:ietf:params:rtp-hdrext:ssrc-audio-level"_ns) + ->entry); + ASSERT_EQ(11U, + audioRecv->GetExt("urn:ietf:params:rtp-hdrext:ssrc-audio-level"_ns) + ->entry); ASSERT_EQ(12U, - videoSend->GetExt("urn:ietf:params:rtp-hdrext:toffset")->entry); + videoSend->GetExt("urn:ietf:params:rtp-hdrext:toffset"_ns)->entry); ASSERT_EQ(12U, - videoRecv->GetExt("urn:ietf:params:rtp-hdrext:toffset")->entry); + videoRecv->GetExt("urn:ietf:params:rtp-hdrext:toffset"_ns)->entry); ASSERT_EQ( 13U, videoSend - ->GetExt("http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time") + ->GetExt( + "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time"_ns) ->entry); ASSERT_EQ( 13U, videoRecv - ->GetExt("http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time") + ->GetExt( + "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time"_ns) ->entry); SwapOfferAnswerRoles(); @@ -5062,9 +5066,9 @@ TEST_F(JsepSessionTest, TestNegotiatedExtmapCollision) { // ssrc-audio-level will be extmap 1 for both // csrc-audio-level will be 2 for both // mid will be 3 for both - mSessionAns->AddAudioRtpExtension("foo"); - mSessionAns->AddAudioRtpExtension("bar"); - mSessionAns->AddAudioRtpExtension("baz"); + mSessionAns->AddAudioRtpExtension("foo"_ns); + mSessionAns->AddAudioRtpExtension("bar"_ns); + mSessionAns->AddAudioRtpExtension("baz"_ns); // Set up an offer that uses the same extmap entries, but for different // things, causing collisions. @@ -5105,18 +5109,18 @@ TEST_F(JsepSessionTest, TestNegotiatedExtmapCollision) { auto* audioRecv = transceivers[0].mRecvTrack.GetNegotiatedDetails(); ASSERT_TRUE(audioSend); ASSERT_TRUE(audioRecv); - ASSERT_EQ(1U, audioSend->GetExt("foo")->entry); - ASSERT_EQ(1U, audioRecv->GetExt("foo")->entry); - ASSERT_EQ(2U, audioSend->GetExt("bar")->entry); - ASSERT_EQ(2U, audioRecv->GetExt("bar")->entry); - ASSERT_EQ(3U, audioSend->GetExt("baz")->entry); - ASSERT_EQ(3U, audioRecv->GetExt("baz")->entry); - ASSERT_EQ( - 11U, - audioSend->GetExt("urn:ietf:params:rtp-hdrext:ssrc-audio-level")->entry); - ASSERT_EQ( - 11U, - audioRecv->GetExt("urn:ietf:params:rtp-hdrext:ssrc-audio-level")->entry); + ASSERT_EQ(1U, audioSend->GetExt("foo"_ns)->entry); + ASSERT_EQ(1U, audioRecv->GetExt("foo"_ns)->entry); + ASSERT_EQ(2U, audioSend->GetExt("bar"_ns)->entry); + ASSERT_EQ(2U, audioRecv->GetExt("bar"_ns)->entry); + ASSERT_EQ(3U, audioSend->GetExt("baz"_ns)->entry); + ASSERT_EQ(3U, audioRecv->GetExt("baz"_ns)->entry); + ASSERT_EQ(11U, + audioSend->GetExt("urn:ietf:params:rtp-hdrext:ssrc-audio-level"_ns) + ->entry); + ASSERT_EQ(11U, + audioRecv->GetExt("urn:ietf:params:rtp-hdrext:ssrc-audio-level"_ns) + ->entry); SwapOfferAnswerRoles(); // Make sure a reoffer uses the negotiated extmap @@ -5150,7 +5154,7 @@ TEST_F(JsepSessionTest, TestExtmapAnswerChangesId) { std::string answer = CreateAnswer(); std::string mungedAnswer = - SetExtmap(answer, "urn:ietf:params:rtp-hdrext:sdes:mid", 14); + SetExtmap(answer, "urn:ietf:params:rtp-hdrext:sdes:mid"_ns, 14); JsepSession::Result result = mSessionOff->SetRemoteDescription(kJsepSdpAnswer, mungedAnswer); ASSERT_TRUE(result.mError.isSome()); @@ -5174,7 +5178,7 @@ TEST_F(JsepSessionTest, TestExtmapChangeId) { SetLocalOffer(offer, ALL_CHECKS); uint16_t oldId = 0; std::string mungedOffer = - SetExtmap(offer, "urn:ietf:params:rtp-hdrext:sdes:mid", 14, &oldId); + SetExtmap(offer, "urn:ietf:params:rtp-hdrext:sdes:mid"_ns, 14, &oldId); ASSERT_NE(oldId, 0); SetRemoteOffer(mungedOffer, ALL_CHECKS); @@ -5182,7 +5186,7 @@ TEST_F(JsepSessionTest, TestExtmapChangeId) { SetLocalAnswer(answer, ALL_CHECKS); std::string mungedAnswer = - SetExtmap(answer, "urn:ietf:params:rtp-hdrext:sdes:mid", oldId); + SetExtmap(answer, "urn:ietf:params:rtp-hdrext:sdes:mid"_ns, oldId); SetRemoteAnswer(mungedAnswer, ALL_CHECKS); } @@ -5198,12 +5202,12 @@ TEST_F(JsepSessionTest, TestExtmapSwap) { OfferAnswer(); std::string offer = CreateOffer(); - uint16_t midId = GetExtmap(offer, "urn:ietf:params:rtp-hdrext:sdes:mid"); + uint16_t midId = GetExtmap(offer, "urn:ietf:params:rtp-hdrext:sdes:mid"_ns); uint16_t ssrcLevelId = 0; std::string mungedOffer = - SetExtmap(offer, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", midId, + SetExtmap(offer, "urn:ietf:params:rtp-hdrext:ssrc-audio-level"_ns, midId, &ssrcLevelId); - mungedOffer = SetExtmap(mungedOffer, "urn:ietf:params:rtp-hdrext:sdes:mid", + mungedOffer = SetExtmap(mungedOffer, "urn:ietf:params:rtp-hdrext:sdes:mid"_ns, ssrcLevelId); JsepSession::Result result = @@ -5230,8 +5234,8 @@ TEST_F(JsepSessionTest, TestExtmapReuse) { ASSERT_TRUE(offerMediaAttrs.HasAttribute(SdpAttribute::kExtmapAttribute)); auto offerExtmap = offerMediaAttrs.GetExtmap(); for (auto& ext : offerExtmap.mExtmaps) { - if (ext.extensionname == "urn:ietf:params:rtp-hdrext:ssrc-audio-level") { - ext.extensionname = "foo"; + if (ext.extensionname == "urn:ietf:params:rtp-hdrext:ssrc-audio-level"_ns) { + ext.extensionname = "foo"_ns; } } @@ -5260,7 +5264,7 @@ TEST_F(JsepSessionTest, TestExtmapReuseAfterRenegotiation) { SetLocalOffer(offer, ALL_CHECKS); // Passing 0 removes urn:ietf:params:rtp-hdrext:ssrc-audio-level std::string mungedOffer = - SetExtmap(offer, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", 0); + SetExtmap(offer, "urn:ietf:params:rtp-hdrext:ssrc-audio-level"_ns, 0); SetRemoteOffer(mungedOffer, ALL_CHECKS); std::string answer = CreateAnswer(); @@ -5279,8 +5283,9 @@ TEST_F(JsepSessionTest, TestExtmapReuseAfterRenegotiation) { ASSERT_TRUE(offerMediaAttrs.HasAttribute(SdpAttribute::kExtmapAttribute)); auto offerExtmap = offerMediaAttrs.GetExtmap(); for (auto& ext : offerExtmap.mExtmaps) { - if (ext.extensionname == "urn:ietf:params:rtp-hdrext:ssrc-audio-level") { - ext.extensionname = "foo"; + if (ext.extensionname == + "urn:ietf:params:rtp-hdrext:ssrc-audio-level"_ns) { + ext.extensionname = "foo"_ns; } } diff --git a/media/webrtc/signaling/gtest/jsep_track_unittest.cpp b/media/webrtc/signaling/gtest/jsep_track_unittest.cpp index 1fca4edf15e9..269d84dae851 100644 --- a/media/webrtc/signaling/gtest/jsep_track_unittest.cpp +++ b/media/webrtc/signaling/gtest/jsep_track_unittest.cpp @@ -13,7 +13,6 @@ #include "api/rtp_parameters.h" #include "gmock/gmock.h" #include "gtest/gtest.h" -#include "jsapi/DefaultCodecPreferences.h" #include "jsapi/RTCRtpTransceiver.h" #include "jsep/JsepTrack.h" #include "sdp/SdpHelper.h" @@ -33,18 +32,18 @@ class JsepTrackTestBase : public ::testing::Test { }; struct CodecOverrides { - bool addFecCodecs = false; - bool preferRed = false; - bool addDtmfCodec = false; - bool enableRemb = true; - bool enableTransportCC = true; - bool enableAudioTransportCC = true; - bool enableRtx = true; + bool mAddFecCodecs = false; + bool mPreferRed = false; + bool mAddDtmfCodec = false; + bool mEnableRemb = true; + bool mEnableTransportCC = true; + bool mEnableAudioTransportCC = true; + bool mEnableRtx = true; void ApplyToPrefs(MockJsepCodecPreferences& aPrefs) const { - aPrefs.mUseRemb = enableRemb; - aPrefs.mUseTransportCC = enableTransportCC; - aPrefs.mUseAudioTransportCC = enableAudioTransportCC; - aPrefs.mUseRtx = enableRtx; + aPrefs.mUseRemb = mEnableRemb; + aPrefs.mUseTransportCC = mEnableTransportCC; + aPrefs.mUseAudioTransportCC = mEnableAudioTransportCC; + aPrefs.mUseRtx = mEnableRtx; } }; @@ -73,98 +72,95 @@ class JsepTrackTest : public JsepTrackTestBase { } } - std::vector> MakeCodecs( - const CodecOverrides overrides) const { + nsTArray> MakeCodecs( + const CodecOverrides aOverrides) const { MockJsepCodecPreferences prefs; - overrides.ApplyToPrefs(prefs); + aOverrides.ApplyToPrefs(prefs); - prefs.mUseRemb = overrides.enableRemb; - prefs.mUseTransportCC = overrides.enableTransportCC; JsepCodecPreferences& prefsRef = prefs; std::cout << "CodecPrefrences: " << prefsRef << "\n"; - std::vector> results; - results.emplace_back(JsepAudioCodecDescription::CreateDefaultOpus(prefs)); - results.emplace_back(JsepAudioCodecDescription::CreateDefaultG722(prefs)); - if (overrides.addDtmfCodec) { - results.emplace_back( + AutoTArray, 16> results; + results.EmplaceBack(JsepAudioCodecDescription::CreateDefaultOpus(prefs)); + results.EmplaceBack(JsepAudioCodecDescription::CreateDefaultG722(prefs)); + if (aOverrides.mAddDtmfCodec) { + results.EmplaceBack( JsepAudioCodecDescription::CreateDefaultTelephoneEvent()); } - if (overrides.addFecCodecs && overrides.preferRed) { - results.emplace_back(JsepVideoCodecDescription::CreateDefaultRed(prefs)); + if (aOverrides.mAddFecCodecs && aOverrides.mPreferRed) { + results.EmplaceBack(JsepVideoCodecDescription::CreateDefaultRed(prefs)); } - results.emplace_back(JsepVideoCodecDescription::CreateDefaultVP8(prefs)); - results.emplace_back(JsepVideoCodecDescription::CreateDefaultH264_1(prefs)); - results.emplace_back(JsepVideoCodecDescription::CreateDefaultAV1(prefs)); + results.EmplaceBack(JsepVideoCodecDescription::CreateDefaultVP8(prefs)); + results.EmplaceBack(JsepVideoCodecDescription::CreateDefaultH264_1(prefs)); + results.EmplaceBack(JsepVideoCodecDescription::CreateDefaultAV1(prefs)); - if (overrides.addFecCodecs) { - if (!overrides.preferRed) { - results.emplace_back( - JsepVideoCodecDescription::CreateDefaultRed(prefs)); + if (aOverrides.mAddFecCodecs) { + if (!aOverrides.mPreferRed) { + results.EmplaceBack(JsepVideoCodecDescription::CreateDefaultRed(prefs)); } - results.emplace_back( + results.EmplaceBack( JsepVideoCodecDescription::CreateDefaultUlpFec(prefs)); } - results.emplace_back(new JsepApplicationCodecDescription( + results.EmplaceBack(new JsepApplicationCodecDescription( "webrtc-datachannel", 256, 5999, 499)); - return results; + return std::move(results); } - void Init(SdpMediaSection::MediaType type) { + void Init(SdpMediaSection::MediaType aType) { InitCodecs(CodecOverrides{}); - InitTracks(type); - InitSdp(type); + InitTracks(aType); + InitSdp(aType); } struct SplitOverrides { - CodecOverrides offer = {}; - CodecOverrides answer = {}; + CodecOverrides mOffer = {}; + CodecOverrides mAnswer = {}; }; - void InitCodecs(const CodecOverrides& overrides) { - mOffCodecs = MakeCodecs(overrides); - mAnsCodecs = MakeCodecs(overrides); + void InitCodecs(const CodecOverrides& aOverrides) { + mOffCodecs = MakeCodecs(aOverrides); + mAnsCodecs = MakeCodecs(aOverrides); } - void InitCodecs(const SplitOverrides& overrides) { - mOffCodecs = MakeCodecs(overrides.offer); - mAnsCodecs = MakeCodecs(overrides.answer); + void InitCodecs(const SplitOverrides& aOverrides) { + mOffCodecs = MakeCodecs(aOverrides.mOffer); + mAnsCodecs = MakeCodecs(aOverrides.mAnswer); } - void InitTracks(SdpMediaSection::MediaType type) { - mSendOff = JsepTrack(type, sdp::kSend); - if (type != SdpMediaSection::MediaType::kApplication) { + void InitTracks(SdpMediaSection::MediaType aType) { + mSendOff = JsepTrack(aType, sdp::kSend); + if (aType != SdpMediaSection::MediaType::kApplication) { mSendOff.UpdateStreamIds(std::vector(1, "stream_id")); } - mRecvOff = JsepTrack(type, sdp::kRecv); + mRecvOff = JsepTrack(aType, sdp::kRecv); mSendOff.PopulateCodecs(mOffCodecs); mRecvOff.PopulateCodecs(mOffCodecs); - mSendAns = JsepTrack(type, sdp::kSend); - if (type != SdpMediaSection::MediaType::kApplication) { + mSendAns = JsepTrack(aType, sdp::kSend); + if (aType != SdpMediaSection::MediaType::kApplication) { mSendAns.UpdateStreamIds(std::vector(1, "stream_id")); } - mRecvAns = JsepTrack(type, sdp::kRecv); + mRecvAns = JsepTrack(aType, sdp::kRecv); mSendAns.PopulateCodecs(mAnsCodecs); mRecvAns.PopulateCodecs(mAnsCodecs); } - void InitSdp(SdpMediaSection::MediaType type) { + void InitSdp(SdpMediaSection::MediaType aType) { std::vector msids(1, "*"); std::string error; SdpHelper helper(&error); mOffer.reset(new SipccSdp(SdpOrigin("", 0, 0, sdp::kIPv4, ""))); - mOffer->AddMediaSection(type, SdpDirectionAttribute::kSendrecv, 0, - SdpHelper::GetProtocolForMediaType(type), + mOffer->AddMediaSection(aType, SdpDirectionAttribute::kSendrecv, 0, + SdpHelper::GetProtocolForMediaType(aType), sdp::kIPv4, "0.0.0.0"); // JsepTrack doesn't set msid-semantic helper.SetupMsidSemantic(msids, mOffer.get()); mAnswer.reset(new SipccSdp(SdpOrigin("", 0, 0, sdp::kIPv4, ""))); - mAnswer->AddMediaSection(type, SdpDirectionAttribute::kSendrecv, 0, - SdpHelper::GetProtocolForMediaType(type), + mAnswer->AddMediaSection(aType, SdpDirectionAttribute::kSendrecv, 0, + SdpHelper::GetProtocolForMediaType(aType), sdp::kIPv4, "0.0.0.0"); // JsepTrack doesn't set msid-semantic helper.SetupMsidSemantic(msids, mAnswer.get()); @@ -422,8 +418,8 @@ class JsepTrackTest : public JsepTrackTestBase { JsepTrack mRecvOff; JsepTrack mSendAns; JsepTrack mRecvAns; - std::vector> mOffCodecs; - std::vector> mAnsCodecs; + nsTArray> mOffCodecs; + nsTArray> mAnsCodecs; UniquePtr mOffer; UniquePtr mAnswer; SsrcGenerator mSsrcGenerator; @@ -480,7 +476,7 @@ TEST_F(JsepTrackTest, CheckForAnsweringWithExtmapAllowMixedWhenNotOffered) { // Appends a sendrecv a=extmap entry to an msection. static void AddExtmap(SdpMediaSection& aMsection, uint16_t aId, - const std::string& aUri, + const nsACString& aUri, const SdpDirectionAttribute::Direction aDir = SdpDirectionAttribute::kSendrecv) { auto& attrs = aMsection.GetAttributeList(); @@ -503,7 +499,7 @@ TEST_F(JsepTrackTest, TwoByteExtIdKeptOnSendWhenExtmapAllowMixed) { GetOffer().GetAttributeList().SetAttribute( MakeUnique(SdpAttribute::kExtmapAllowMixedAttribute)); CreateAnswer(); - const std::string uri = "urn:ietf:params:rtp-hdrext:toffset"; + const nsLiteralCString uri = "urn:ietf:params:rtp-hdrext:toffset"_ns; AddExtmap(GetAnswer(), 15, uri); Negotiate(); ASSERT_TRUE(mSendAns.GetNegotiatedDetails()); @@ -518,7 +514,7 @@ TEST_F(JsepTrackTest, TwoByteExtIdDroppedFromSendWithoutExtmapAllowMixed) { GetOffer().GetAttributeList().RemoveAttribute( SdpAttribute::kExtmapAllowMixedAttribute); CreateAnswer(); - const std::string uri = "urn:ietf:params:rtp-hdrext:toffset"; + const nsLiteralCString uri = "urn:ietf:params:rtp-hdrext:toffset"_ns; AddExtmap(GetAnswer(), 15, uri); Negotiate(); ASSERT_TRUE(mSendAns.GetNegotiatedDetails()); @@ -537,7 +533,7 @@ TEST_F(JsepTrackTest, GetOffer().GetAttributeList().RemoveAttribute( SdpAttribute::kExtmapAllowMixedAttribute); CreateAnswer(); - const std::string uri = webrtc::RtpExtension::kDependencyDescriptorUri; + const nsLiteralCString uri(webrtc::RtpExtension::kDependencyDescriptorUri); AddExtmap(GetAnswer(), 5, uri); Negotiate(); ASSERT_TRUE(mSendAns.GetNegotiatedDetails()); @@ -545,10 +541,9 @@ TEST_F(JsepTrackTest, } TEST_F(JsepTrackTest, CheckForMismatchedAudioCodecAndVideoTrack) { - std::vector> offerCodecs; - // make codecs including telephone-event (an audio codec) - offerCodecs = MakeCodecs({.addDtmfCodec = true}); + const nsTArray> offerCodecs = + MakeCodecs({.mAddDtmfCodec = true}); JsepTrack videoTrack(SdpMediaSection::kVideo, sdp::kSend); videoTrack.UpdateStreamIds(std::vector(1, "stream_id")); // populate codecs and then make sure we don't have any audio codecs @@ -607,7 +602,7 @@ TEST_F(JsepTrackTest, CheckVideoTrackWithHackedDtmfSdp) { TEST_F(JsepTrackTest, AudioNegotiationOffererDtmf) { InitCodecs( - {.offer = {.addDtmfCodec = true}, .answer = {.addDtmfCodec = false}}); + {.mOffer = {.mAddDtmfCodec = true}, .mAnswer = {.mAddDtmfCodec = false}}); InitTracks(SdpMediaSection::kAudio); InitSdp(SdpMediaSection::kAudio); @@ -645,7 +640,7 @@ TEST_F(JsepTrackTest, AudioNegotiationOffererDtmf) { TEST_F(JsepTrackTest, AudioNegotiationAnswererDtmf) { InitCodecs( - {.offer = {.addDtmfCodec = false}, .answer = {.addDtmfCodec = true}}); + {.mOffer = {.mAddDtmfCodec = false}, .mAnswer = {.mAddDtmfCodec = true}}); InitTracks(SdpMediaSection::kAudio); InitSdp(SdpMediaSection::kAudio); @@ -683,7 +678,7 @@ TEST_F(JsepTrackTest, AudioNegotiationAnswererDtmf) { TEST_F(JsepTrackTest, AudioNegotiationOffererAnswererDtmf) { InitCodecs( - {.offer = {.addDtmfCodec = true}, .answer = {.addDtmfCodec = true}}); + {.mOffer = {.mAddDtmfCodec = true}, .mAnswer = {.mAddDtmfCodec = true}}); InitTracks(SdpMediaSection::kAudio); InitSdp(SdpMediaSection::kAudio); @@ -729,7 +724,7 @@ TEST_F(JsepTrackTest, AudioNegotiationOffererAnswererDtmf) { TEST_F(JsepTrackTest, AudioNegotiationDtmfOffererNoFmtpAnswererFmtp) { InitCodecs( - {.offer = {.addDtmfCodec = true}, .answer = {.addDtmfCodec = true}}); + {.mOffer = {.mAddDtmfCodec = true}, .mAnswer = {.mAddDtmfCodec = true}}); mExpectDifferingFmtp = true; @@ -788,7 +783,7 @@ TEST_F(JsepTrackTest, AudioNegotiationDtmfOffererNoFmtpAnswererFmtp) { TEST_F(JsepTrackTest, AudioNegotiationDtmfOffererFmtpAnswererNoFmtp) { InitCodecs( - {.offer = {.addDtmfCodec = true}, .answer = {.addDtmfCodec = true}}); + {.mOffer = {.mAddDtmfCodec = true}, .mAnswer = {.mAddDtmfCodec = true}}); mExpectDifferingFmtp = true; @@ -847,7 +842,7 @@ TEST_F(JsepTrackTest, AudioNegotiationDtmfOffererFmtpAnswererNoFmtp) { TEST_F(JsepTrackTest, AudioNegotiationDtmfOffererNoFmtpAnswererNoFmtp) { InitCodecs( - {.offer = {.addDtmfCodec = true}, .answer = {.addDtmfCodec = true}}); + {.mOffer = {.mAddDtmfCodec = true}, .mAnswer = {.mAddDtmfCodec = true}}); mExpectDifferingFmtp = true; @@ -907,7 +902,7 @@ TEST_F(JsepTrackTest, AudioNegotiationDtmfOffererNoFmtpAnswererNoFmtp) { TEST_F(JsepTrackTest, VideoNegotationOffererFEC) { InitCodecs( - {.offer = {.addFecCodecs = true}, .answer = {.addFecCodecs = false}}); + {.mOffer = {.mAddFecCodecs = true}, .mAnswer = {.mAddFecCodecs = false}}); InitTracks(SdpMediaSection::kVideo); InitSdp(SdpMediaSection::kVideo); @@ -942,7 +937,7 @@ TEST_F(JsepTrackTest, VideoNegotationOffererFEC) { TEST_F(JsepTrackTest, VideoNegotationAnswererFEC) { InitCodecs( - {.offer = {.addFecCodecs = false}, .answer = {.addFecCodecs = true}}); + {.mOffer = {.mAddFecCodecs = false}, .mAnswer = {.mAddFecCodecs = true}}); InitTracks(SdpMediaSection::kVideo); InitSdp(SdpMediaSection::kVideo); @@ -977,7 +972,7 @@ TEST_F(JsepTrackTest, VideoNegotationAnswererFEC) { TEST_F(JsepTrackTest, VideoNegotationOffererAnswererFEC) { InitCodecs( - {.offer = {.addFecCodecs = true}, .answer = {.addFecCodecs = true}}); + {.mOffer = {.mAddFecCodecs = true}, .mAnswer = {.mAddFecCodecs = true}}); InitTracks(SdpMediaSection::kVideo); InitSdp(SdpMediaSection::kVideo); @@ -1003,8 +998,8 @@ TEST_F(JsepTrackTest, VideoNegotationOffererAnswererFEC) { } TEST_F(JsepTrackTest, VideoNegotationOffererAnswererFECPreferred) { - InitCodecs({.offer = {.addFecCodecs = true, .preferRed = true}, - .answer = {.addFecCodecs = true}}); + InitCodecs({.mOffer = {.mAddFecCodecs = true, .mPreferRed = true}, + .mAnswer = {.mAddFecCodecs = true}}); InitTracks(SdpMediaSection::kVideo); InitSdp(SdpMediaSection::kVideo); @@ -1033,13 +1028,12 @@ TEST_F(JsepTrackTest, VideoNegotationOffererAnswererFECPreferred) { // Make sure we only put the right things in the fmtp:122 120/.... line TEST_F(JsepTrackTest, VideoNegotationOffererAnswererFECMismatch) { - InitCodecs({.offer = {.addFecCodecs = true, .preferRed = true}, - .answer = {.addFecCodecs = true}}); + InitCodecs({.mOffer = {.mAddFecCodecs = true, .mPreferRed = true}, + .mAnswer = {.mAddFecCodecs = true}}); // remove h264 & AV1 from answer codecs - ASSERT_EQ("H264", mAnsCodecs[3]->mName); - ASSERT_EQ("AV1", mAnsCodecs[4]->mName); - mAnsCodecs.erase(mAnsCodecs.begin() + 4); - mAnsCodecs.erase(mAnsCodecs.begin() + 3); + mAnsCodecs.RemoveElementsBy([](const auto& aCodec) { + return aCodec->mName == "H264" || aCodec->mName == "AV1"; + }); InitTracks(SdpMediaSection::kVideo); InitSdp(SdpMediaSection::kVideo); @@ -1068,17 +1062,17 @@ TEST_F(JsepTrackTest, VideoNegotationOffererAnswererFECMismatch) { TEST_F(JsepTrackTest, VideoNegotationOffererAnswererFECZeroVP9Codec) { MockJsepCodecPreferences prefs; - mOffCodecs = MakeCodecs({.addFecCodecs = true}); + mOffCodecs = MakeCodecs({.mAddFecCodecs = true}); auto vp9 = JsepVideoCodecDescription::CreateDefaultVP9(prefs); vp9->mDefaultPt = "0"; - mOffCodecs.push_back(std::move(vp9)); + mOffCodecs.AppendElement(std::move(vp9)); - ASSERT_EQ(9U, mOffCodecs.size()); + ASSERT_EQ(9U, mOffCodecs.Length()); JsepVideoCodecDescription& red = static_cast(*mOffCodecs[5]); ASSERT_EQ("red", red.mName); - mAnsCodecs = MakeCodecs({.addFecCodecs = true}); + mAnsCodecs = MakeCodecs({.mAddFecCodecs = true}); InitTracks(SdpMediaSection::kVideo); InitSdp(SdpMediaSection::kVideo); @@ -1095,8 +1089,8 @@ TEST_F(JsepTrackTest, VideoNegotationOffererAnswererFECZeroVP9Codec) { TEST_F(JsepTrackTest, VideoNegotiationOfferRemb) { // enable remb on the offer codecs - InitCodecs({.offer = {.enableRemb = true, .enableTransportCC = false}, - .answer = {.enableRemb = false, .enableTransportCC = false}}); + InitCodecs({.mOffer = {.mEnableRemb = true, .mEnableTransportCC = false}, + .mAnswer = {.mEnableRemb = false, .mEnableTransportCC = false}}); InitTracks(SdpMediaSection::kVideo); InitSdp(SdpMediaSection::kVideo); OfferAnswer(); @@ -1122,8 +1116,8 @@ TEST_F(JsepTrackTest, VideoNegotiationOfferRemb) { } TEST_F(JsepTrackTest, VideoNegotiationAnswerRemb) { - InitCodecs({.offer = {.enableRemb = false, .enableTransportCC = false}, - .answer = {.enableRemb = true, .enableTransportCC = false}}); + InitCodecs({.mOffer = {.mEnableRemb = false, .mEnableTransportCC = false}, + .mAnswer = {.mEnableRemb = true, .mEnableTransportCC = false}}); // enable remb on the answer codecs ((JsepVideoCodecDescription&)*mAnsCodecs[2]).EnableRemb(); InitTracks(SdpMediaSection::kVideo); @@ -1150,8 +1144,8 @@ TEST_F(JsepTrackTest, VideoNegotiationAnswerRemb) { } TEST_F(JsepTrackTest, VideoNegotiationOfferAnswerRemb) { - InitCodecs({.offer = {.enableRemb = true, .enableTransportCC = false}, - .answer = {.enableRemb = true, .enableTransportCC = false}}); + InitCodecs({.mOffer = {.mEnableRemb = true, .mEnableTransportCC = false}, + .mAnswer = {.mEnableRemb = true, .mEnableTransportCC = false}}); // enable remb on the offer and answer codecs ((JsepVideoCodecDescription&)*mOffCodecs[2]).EnableRemb(); ((JsepVideoCodecDescription&)*mAnsCodecs[2]).EnableRemb(); @@ -1183,7 +1177,7 @@ TEST_F(JsepTrackTest, VideoNegotiationOfferAnswerRemb) { } TEST_F(JsepTrackTest, AudioNegotiationOfferTransportCC) { - InitCodecs({.enableAudioTransportCC = false}); + InitCodecs({.mEnableAudioTransportCC = false}); // enable TransportCC on the offer codecs ((JsepAudioCodecDescription&)*mOffCodecs[0]).EnableTransportCC(); InitTracks(SdpMediaSection::kAudio); @@ -1210,7 +1204,7 @@ TEST_F(JsepTrackTest, AudioNegotiationOfferTransportCC) { } TEST_F(JsepTrackTest, AudioNegotiationAnswerTransportCC) { - InitCodecs({.enableAudioTransportCC = false}); + InitCodecs({.mEnableAudioTransportCC = false}); // enable TransportCC on the answer codecs ((JsepAudioCodecDescription&)*mAnsCodecs[0]).EnableTransportCC(); InitTracks(SdpMediaSection::kAudio); @@ -1237,7 +1231,7 @@ TEST_F(JsepTrackTest, AudioNegotiationAnswerTransportCC) { } TEST_F(JsepTrackTest, AudioNegotiationOfferAnswerTransportCC) { - InitCodecs({.enableAudioTransportCC = false}); + InitCodecs({.mEnableAudioTransportCC = false}); // enable TransportCC on the offer and answer codecs ((JsepAudioCodecDescription&)*mOffCodecs[0]).EnableTransportCC(); ((JsepAudioCodecDescription&)*mAnsCodecs[0]).EnableTransportCC(); @@ -1273,7 +1267,7 @@ TEST_F(JsepTrackTest, AudioNegotiationOfferAnswerTransportCC) { } TEST_F(JsepTrackTest, AudioTransportCCFbSetUnsetWhenAnswerRejects) { - InitCodecs({.enableAudioTransportCC = false}); + InitCodecs({.mEnableAudioTransportCC = false}); // Offer enables TransportCC, answer does not. After negotiation TC is // dropped; AudioCodecConfig::mTransportCCFbSet must reflect that even though // JsepAudioCodecDescription::mTransportCCEnabled stays true on the offerer. @@ -1322,8 +1316,8 @@ TEST_F(JsepTrackTest, AudioTransportCCFbSetWhenBothSidesNegotiate) { } TEST_F(JsepTrackTest, VideoNegotiationOfferTransportCC) { - InitCodecs({.offer = {.enableRemb = false, .enableTransportCC = true}, - .answer = {.enableRemb = false, .enableTransportCC = false}}); + InitCodecs({.mOffer = {.mEnableRemb = false, .mEnableTransportCC = true}, + .mAnswer = {.mEnableRemb = false, .mEnableTransportCC = false}}); // enable TransportCC on the offer codecs ((JsepVideoCodecDescription&)*mOffCodecs[2]).EnableTransportCC(); InitTracks(SdpMediaSection::kVideo); @@ -1350,8 +1344,8 @@ TEST_F(JsepTrackTest, VideoNegotiationOfferTransportCC) { } TEST_F(JsepTrackTest, VideoNegotiationAnswerTransportCC) { - InitCodecs({.offer = {.enableRemb = false, .enableTransportCC = false}, - .answer = {.enableRemb = false, .enableTransportCC = true}}); + InitCodecs({.mOffer = {.mEnableRemb = false, .mEnableTransportCC = false}, + .mAnswer = {.mEnableRemb = false, .mEnableTransportCC = true}}); // enable TransportCC on the answer codecs ((JsepVideoCodecDescription&)*mAnsCodecs[2]).EnableTransportCC(); InitTracks(SdpMediaSection::kVideo); @@ -1378,7 +1372,7 @@ TEST_F(JsepTrackTest, VideoNegotiationAnswerTransportCC) { } TEST_F(JsepTrackTest, VideoNegotiationOfferAnswerTransportCC) { - InitCodecs({.enableRemb = false, .enableTransportCC = true}); + InitCodecs({.mEnableRemb = false, .mEnableTransportCC = true}); // enable TransportCC on the offer and answer codecs ((JsepVideoCodecDescription&)*mOffCodecs[2]).EnableTransportCC(); ((JsepVideoCodecDescription&)*mAnsCodecs[2]).EnableTransportCC(); @@ -1526,9 +1520,8 @@ TEST_F(JsepTrackTest, DataChannelDraft21) { TEST_F(JsepTrackTest, DataChannelDraft21AnswerWithDifferentPort) { InitCodecs(CodecOverrides{}); - mOffCodecs.pop_back(); - mOffCodecs.emplace_back(new JsepApplicationCodecDescription( - "webrtc-datachannel", 256, 4555, 10544)); + mOffCodecs.LastElement() = MakeUnique( + "webrtc-datachannel", 256, 4555, 10544); InitTracks(SdpMediaSection::kApplication); InitSdp(SdpMediaSection::kApplication); @@ -1803,8 +1796,8 @@ TEST_F(JsepTrackTest, RtcpFbWithPayloadTypeAsymmetry) { } TEST_F(JsepTrackTest, OfferRedUlpfecNoRtx) { - InitCodecs({.offer = {.addFecCodecs = true, .enableRtx = false}, - .answer = {.addFecCodecs = true, .enableRtx = true}}); + InitCodecs({.mOffer = {.mAddFecCodecs = true, .mEnableRtx = false}, + .mAnswer = {.mAddFecCodecs = true, .mEnableRtx = true}}); InitTracks(SdpMediaSection::kVideo); InitSdp(SdpMediaSection::kVideo); OfferAnswer(); @@ -1834,8 +1827,8 @@ TEST_F(JsepTrackTest, OfferRedUlpfecNoRtx) { } TEST_F(JsepTrackTest, AnswerRedUlpfecNoRtx) { - InitCodecs({.offer = {.addFecCodecs = true, .enableRtx = true}, - .answer = {.addFecCodecs = true, .enableRtx = false}}); + InitCodecs({.mOffer = {.mAddFecCodecs = true, .mEnableRtx = true}, + .mAnswer = {.mAddFecCodecs = true, .mEnableRtx = false}}); InitTracks(SdpMediaSection::kVideo); InitSdp(SdpMediaSection::kVideo); OfferAnswer(); @@ -1872,9 +1865,9 @@ TEST_F(JsepTrackTest, AnswerRedUlpfecNoRtx) { TEST_F(JsepTrackTest, AudioSdpFmtpLine) { mOffCodecs = MakeCodecs( - {.addFecCodecs = true, .preferRed = true, .addDtmfCodec = true}); + {.mAddFecCodecs = true, .mPreferRed = true, .mAddDtmfCodec = true}); mAnsCodecs = MakeCodecs( - {.addFecCodecs = true, .preferRed = true, .addDtmfCodec = true}); + {.mAddFecCodecs = true, .mPreferRed = true, .mAddDtmfCodec = true}); InitTracks(SdpMediaSection::kAudio); InitSdp(SdpMediaSection::kAudio); OfferAnswer(); @@ -1908,9 +1901,9 @@ TEST_F(JsepTrackTest, AudioSdpFmtpLine) { TEST_F(JsepTrackTest, NonDefaultAudioSdpFmtpLine) { mOffCodecs = MakeCodecs( - {.addFecCodecs = true, .preferRed = true, .addDtmfCodec = true}); + {.mAddFecCodecs = true, .mPreferRed = true, .mAddDtmfCodec = true}); mAnsCodecs = MakeCodecs( - {.addFecCodecs = true, .preferRed = true, .addDtmfCodec = true}); + {.mAddFecCodecs = true, .mPreferRed = true, .mAddDtmfCodec = true}); for (auto& codec : mOffCodecs) { if (codec->mName == "opus") { @@ -2009,9 +2002,9 @@ TEST_F(JsepTrackTest, OpusPtimeNegotiatedFromRemoteFmtp) { TEST_F(JsepTrackTest, VideoSdpFmtpLine) { mOffCodecs = MakeCodecs( - {.addFecCodecs = true, .preferRed = true, .addDtmfCodec = true}); + {.mAddFecCodecs = true, .mPreferRed = true, .mAddDtmfCodec = true}); mAnsCodecs = MakeCodecs( - {.addFecCodecs = true, .preferRed = true, .addDtmfCodec = true}); + {.mAddFecCodecs = true, .mPreferRed = true, .mAddDtmfCodec = true}); InitTracks(SdpMediaSection::kVideo); InitSdp(SdpMediaSection::kVideo); OfferAnswer(); @@ -2054,9 +2047,9 @@ TEST_F(JsepTrackTest, VideoSdpFmtpLine) { TEST_F(JsepTrackTest, NonDefaultVideoSdpFmtpLine) { mOffCodecs = MakeCodecs( - {.addFecCodecs = true, .preferRed = true, .addDtmfCodec = true}); + {.mAddFecCodecs = true, .mPreferRed = true, .mAddDtmfCodec = true}); mAnsCodecs = MakeCodecs( - {.addFecCodecs = true, .preferRed = true, .addDtmfCodec = true}); + {.mAddFecCodecs = true, .mPreferRed = true, .mAddDtmfCodec = true}); for (auto& codec : mOffCodecs) { if (codec->mName == "VP8" || codec->mName == "H264") { @@ -2136,8 +2129,8 @@ TEST(JsepTrackRecvPayloadTypesTest, SingleTrackPTsAreUnique) { constexpr auto audio = SdpMediaSection::MediaType::kAudio; - std::vector> codecs; - codecs.emplace_back( + AutoTArray, 16> codecs; + codecs.AppendElement( MakeUnique("1", "codec1", 48000, 1)); SipccSdp offer1(SdpOrigin("", 0, 0, sdp::kIPv4, "")); @@ -2176,12 +2169,10 @@ TEST(JsepTrackRecvPayloadTypesTest, DoubleTrackPTsAreUnique) { constexpr auto audio = SdpMediaSection::MediaType::kAudio; - std::vector> codecs1; - codecs1.emplace_back( + AutoTArray, 16> codecs1, codecs2; + codecs1.AppendElement( MakeUnique("1", "codec1", 48000, 1)); - - std::vector> codecs2; - codecs2.emplace_back( + codecs2.AppendElement( MakeUnique("2", "codec1", 48000, 1)); SipccSdp offer1(SdpOrigin("", 0, 0, sdp::kIPv4, "")); @@ -2244,12 +2235,10 @@ TEST(JsepTrackRecvPayloadTypesTest, DoubleTrackPTsAreDuplicates) { constexpr auto audio = SdpMediaSection::MediaType::kAudio; - std::vector> codecs1; - codecs1.emplace_back( + AutoTArray, 16> codecs1, codecs2; + codecs1.AppendElement( MakeUnique("1", "codec1", 48000, 1)); - - std::vector> codecs2; - codecs2.emplace_back( + codecs2.AppendElement( MakeUnique("1", "codec1", 48000, 1)); SipccSdp offer1(SdpOrigin("", 0, 0, sdp::kIPv4, "")); @@ -2311,16 +2300,15 @@ TEST(JsepTrackRecvPayloadTypesTest, DoubleTrackPTsOverlap) { constexpr auto audio = SdpMediaSection::MediaType::kAudio; - std::vector> codecs1; - codecs1.emplace_back( + AutoTArray, 16> codecs1, codecs2; + codecs1.AppendElement( MakeUnique("1", "codec1", 48000, 1)); - codecs1.emplace_back( + codecs1.AppendElement( MakeUnique("2", "codec2", 48000, 1)); - std::vector> codecs2; - codecs2.emplace_back( + codecs2.AppendElement( MakeUnique("1", "codec1", 48000, 1)); - codecs2.emplace_back( + codecs2.AppendElement( MakeUnique("3", "codec2", 48000, 1)); SipccSdp offer1(SdpOrigin("", 0, 0, sdp::kIPv4, "")); @@ -2383,16 +2371,15 @@ TEST(JsepTrackRecvPayloadTypesTest, DoubleTrackPTsDuplicateAfterRenegotiation) { constexpr auto audio = SdpMediaSection::MediaType::kAudio; - std::vector> codecs1; - codecs1.emplace_back( + AutoTArray, 16> codecs1, codecs2; + codecs1.AppendElement( MakeUnique("1", "codec1", 48000, 1)); - codecs1.emplace_back( + codecs1.AppendElement( MakeUnique("2", "codec2", 48000, 1)); - std::vector> codecs2; - codecs2.emplace_back( + codecs2.AppendElement( MakeUnique("3", "codec1", 48000, 1)); - codecs2.emplace_back( + codecs2.AppendElement( MakeUnique("4", "codec2", 48000, 1)); // First negotiation. diff --git a/media/webrtc/signaling/gtest/moz.build b/media/webrtc/signaling/gtest/moz.build index 0f1033ed439b..29c31783412c 100644 --- a/media/webrtc/signaling/gtest/moz.build +++ b/media/webrtc/signaling/gtest/moz.build @@ -44,6 +44,7 @@ if CONFIG["MOZ_WIDGET_TOOLKIT"] != "uikit" and not ( "jsep_track_unittest.cpp", "mediapipeline_unittest.cpp", "MockCall.cpp", + "peer_connection_unittest.cpp", "sdp_unittests.cpp", "videoconduit_unittests.cpp", ] diff --git a/media/webrtc/signaling/gtest/peer_connection_unittest.cpp b/media/webrtc/signaling/gtest/peer_connection_unittest.cpp new file mode 100644 index 000000000000..38369ce83c18 --- /dev/null +++ b/media/webrtc/signaling/gtest/peer_connection_unittest.cpp @@ -0,0 +1,82 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "MockJsepCodecPreferences.h" +#include "PeerConnectionImpl.h" +#include "api/rtp_parameters.h" +#include "gtest/gtest.h" + +namespace mozilla { + +static int RtpExtensionHeaderUriComparator( + const PeerConnectionImpl::RtpExtensionHeader& aHeader, const char* aUri) { + // TODO bug 2051688, bug 2051711: use operator<=>. + std::strong_ordering ord = + std::string_view(aHeader.extensionname.get()) <=> std::string_view(aUri); + return ord == 0 ? 0 : ord < 0 ? -1 : 1; +} + +static const PeerConnectionImpl::RtpExtensionHeader* FindExtension( + const nsTArray& aHeaders, + const char* aUri) { + auto idx = aHeaders.IndexOf(aUri, 0, &RtpExtensionHeaderUriComparator); + if (idx == aHeaders.NoIndex) { + return nullptr; + } + + return &aHeaders[idx]; +} + +TEST(PeerConnectionImplTest, GetDefaultRtpExtensionsTransportCCBoth) +{ + MockJsepCodecPreferences prefs; + prefs.mUseTransportCC = true; + prefs.mUseAudioTransportCC = true; + AutoTArray headers; + PeerConnectionImpl::GetDefaultRtpExtensions(prefs, &headers); + const auto* ext = + FindExtension(headers, webrtc::RtpExtension::kTransportSequenceNumberUri); + ASSERT_NE(nullptr, ext); + EXPECT_EQ(JsepMediaType::kAudioVideo, ext->mMediaType); +} + +TEST(PeerConnectionImplTest, GetDefaultRtpExtensionsTransportCCVideoOnly) +{ + MockJsepCodecPreferences prefs; + prefs.mUseTransportCC = true; + prefs.mUseAudioTransportCC = false; + AutoTArray headers; + PeerConnectionImpl::GetDefaultRtpExtensions(prefs, &headers); + const auto* ext = + FindExtension(headers, webrtc::RtpExtension::kTransportSequenceNumberUri); + ASSERT_NE(nullptr, ext); + EXPECT_EQ(JsepMediaType::kVideo, ext->mMediaType); +} + +TEST(PeerConnectionImplTest, GetDefaultRtpExtensionsTransportCCAudioOnly) +{ + MockJsepCodecPreferences prefs; + prefs.mUseTransportCC = false; + prefs.mUseAudioTransportCC = true; + AutoTArray headers; + PeerConnectionImpl::GetDefaultRtpExtensions(prefs, &headers); + const auto* ext = + FindExtension(headers, webrtc::RtpExtension::kTransportSequenceNumberUri); + ASSERT_NE(nullptr, ext); + EXPECT_EQ(JsepMediaType::kAudio, ext->mMediaType); +} + +TEST(PeerConnectionImplTest, GetDefaultRtpExtensionsTransportCCNeither) +{ + MockJsepCodecPreferences prefs; + prefs.mUseTransportCC = false; + prefs.mUseAudioTransportCC = false; + AutoTArray headers; + PeerConnectionImpl::GetDefaultRtpExtensions(prefs, &headers); + EXPECT_EQ(nullptr, + FindExtension(headers, + webrtc::RtpExtension::kTransportSequenceNumberUri)); +} + +} // namespace mozilla diff --git a/media/webrtc/signaling/gtest/sdp_unittests.cpp b/media/webrtc/signaling/gtest/sdp_unittests.cpp index af9e3682453f..ecf75238293d 100644 --- a/media/webrtc/signaling/gtest/sdp_unittests.cpp +++ b/media/webrtc/signaling/gtest/sdp_unittests.cpp @@ -3400,20 +3400,20 @@ TEST_P(NewSdpTest, CheckExtmap) { ASSERT_EQ(1U, extmaps[0].entry); ASSERT_FALSE(extmaps[0].direction_specified); - ASSERT_EQ("urn:ietf:params:rtp-hdrext:ssrc-audio-level", + ASSERT_EQ("urn:ietf:params:rtp-hdrext:ssrc-audio-level"_ns, extmaps[0].extensionname); - ASSERT_EQ("", extmaps[0].extensionattributes); + ASSERT_EQ(""_ns, extmaps[0].extensionattributes); ASSERT_EQ(2U, extmaps[1].entry); ASSERT_TRUE(extmaps[1].direction_specified); ASSERT_EQ(SdpDirectionAttribute::kSendonly, extmaps[1].direction); - ASSERT_EQ("some_extension", extmaps[1].extensionname); - ASSERT_EQ("", extmaps[1].extensionattributes); + ASSERT_EQ("some_extension"_ns, extmaps[1].extensionname); + ASSERT_EQ(""_ns, extmaps[1].extensionattributes); ASSERT_EQ(3U, extmaps[2].entry); ASSERT_FALSE(extmaps[2].direction_specified); - ASSERT_EQ("some_other_extension", extmaps[2].extensionname); - ASSERT_EQ("some_params some more params", extmaps[2].extensionattributes); + ASSERT_EQ("some_other_extension"_ns, extmaps[2].extensionname); + ASSERT_EQ("some_params some more params"_ns, extmaps[2].extensionattributes); } TEST_P(NewSdpTest, CheckRtcpFb) { diff --git a/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/AutoSave.kt b/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/AutoSave.kt index 1e07dc240caf..15115fc1c309 100644 --- a/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/AutoSave.kt +++ b/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/AutoSave.kt @@ -28,7 +28,6 @@ import kotlinx.coroutines.launch import mozilla.components.browser.state.selector.normalTabs import mozilla.components.browser.state.selector.selectedTab import mozilla.components.browser.state.state.BrowserState -import mozilla.components.browser.state.state.TabPartition import mozilla.components.browser.state.store.BrowserStore import mozilla.components.lib.state.ext.flow import mozilla.components.support.base.log.logger.Logger @@ -194,7 +193,6 @@ private class StateMonitoring(private val autoSave: AutoSave) { selectedTabId = state.selectedTabId, tabs = state.normalTabs.size, loading = state.selectedTab?.content?.loading, - tabPartitions = state.tabPartitions, ) } .distinctUntilChanged() @@ -219,9 +217,6 @@ private class StateMonitoring(private val autoSave: AutoSave) { } else if (lastObservation!!.loading != observation.loading && observation.loading == false) { autoSave.logger.info("Save: Load finished") true - } else if (lastObservation!!.tabPartitions != observation.tabPartitions) { - autoSave.logger.info("Save: Tab partitions changed") - true } else { false } @@ -237,6 +232,5 @@ private class StateMonitoring(private val autoSave: AutoSave) { val selectedTabId: String?, val tabs: Int, val loading: Boolean?, - val tabPartitions: Map, ) } diff --git a/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/RecoverableBrowserState.kt b/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/RecoverableBrowserState.kt index f5fabbe6eb72..66e8beb4666c 100644 --- a/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/RecoverableBrowserState.kt +++ b/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/RecoverableBrowserState.kt @@ -4,7 +4,6 @@ package mozilla.components.browser.session.storage -import mozilla.components.browser.state.state.TabPartition import mozilla.components.browser.state.state.recover.RecoverableTab /** @@ -12,14 +11,11 @@ import mozilla.components.browser.state.state.recover.RecoverableTab * * @param tabs The list of restored tabs. * @param selectedTabId The ID of the selected tab in [tabs]. Or `null` if no selection was restored. - * @param tabPartitions A mapping of IDs to the corresponding [TabPartition]. A partition is used to store tab groups - * for a specific feature. * @param isTranslationsEngineSupported The last persisted value of whether the translations engine supports the device * architecture, or `null` if it was never determined before persisting. */ data class RecoverableBrowserState( val tabs: List, val selectedTabId: String?, - val tabPartitions: Map, val isTranslationsEngineSupported: Boolean? = null, ) diff --git a/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/serialize/BrowserStateReader.kt b/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/serialize/BrowserStateReader.kt index b7bb3b324b8b..2f1f9f0e01f7 100644 --- a/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/serialize/BrowserStateReader.kt +++ b/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/serialize/BrowserStateReader.kt @@ -13,8 +13,6 @@ import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.LastMediaAccessState import mozilla.components.browser.state.state.ReaderState import mozilla.components.browser.state.state.SessionState -import mozilla.components.browser.state.state.TabGroup -import mozilla.components.browser.state.state.TabPartition import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.state.recover.RecoverableTab import mozilla.components.browser.state.state.recover.TabState @@ -82,7 +80,6 @@ private fun JsonReader.browsingSession( var version = 1 // Initially we didn't save a version. If there's none then we assume it is version 1. var tabs: List? = null - var tabPartitions: Map = emptyMap() var selectedIndex: Int? = null var selectedTabId: String? = null var isTranslationsEngineSupported: Boolean? = null @@ -94,7 +91,7 @@ private fun JsonReader.browsingSession( Keys.SELECTED_TAB_ID_KEY -> selectedTabId = nextStringOrNull() Keys.TRANSLATIONS_ENGINE_IS_SUPPORTED_KEY -> isTranslationsEngineSupported = nextBooleanOrNull() Keys.SESSION_STATE_TUPLES_KEY -> tabs = tabs(engine, restoreSessionId, restoreParentId, predicate) - Keys.TAB_PARTITIONS_KEY -> tabPartitions = tabPartitions() + else -> skipValue() } } @@ -114,7 +111,7 @@ private fun JsonReader.browsingSession( selectedTabId = tabs.sortedByDescending { it.state.lastAccess }.first().state.id } - RecoverableBrowserState(tabs, selectedTabId, tabPartitions, isTranslationsEngineSupported) + RecoverableBrowserState(tabs, selectedTabId, isTranslationsEngineSupported) } else { null } @@ -155,6 +152,7 @@ private fun JsonReader.tab( when (nextName()) { Keys.SESSION_KEY -> tab = tabSession() Keys.ENGINE_SESSION_KEY -> engineSessionState = engine.createSessionStateFrom(this) + else -> skipValue() } } @@ -202,7 +200,7 @@ private fun JsonReader.tabSession(): RecoverableTab { beginObject() while (hasNext()) { - when (val name = nextName()) { + when (nextName()) { Keys.SESSION_URL_KEY -> url = nextString() Keys.SESSION_UUID_KEY -> id = nextString() Keys.SESSION_CONTEXT_ID_KEY -> contextId = nextStringOrNull() @@ -226,7 +224,7 @@ private fun JsonReader.tabSession(): RecoverableTab { Keys.SESSION_EXTERNAL_SOURCE_PACKAGE_CATEGORY -> externalSourceCategory = nextIntOrNull() Keys.SESSION_DEPRECATED_SOURCE_KEY -> nextString() Keys.SESSION_DESKTOP_MODE -> desktopMode = nextBoolean() - else -> throw IllegalArgumentException("Unknown session key: $name") + else -> skipValue() } } @@ -273,78 +271,3 @@ private fun JsonReader.tabSession(): RecoverableTab { ), ) } - -private fun JsonReader.tabPartitions(): Map { - beginArray() - - val tabPartitions = mutableMapOf() - while (peek() != JsonToken.END_ARRAY) { - val tabPartition = tabPartition() - tabPartitions[tabPartition.id] = tabPartition - } - - endArray() - - return tabPartitions -} - -private fun JsonReader.tabPartition(): TabPartition { - beginObject() - - var id: String? = null - var tabGroups: List = emptyList() - - while (hasNext()) { - when (nextName()) { - Keys.TAB_PARTITION_ID_KEY -> id = nextString() - Keys.TAB_PARTITION_GROUPS_KEY -> { - val groups = mutableListOf() - beginArray() - while (peek() != JsonToken.END_ARRAY) { - groups.add(group()) - } - endArray() - tabGroups = groups - } - } - } - - endObject() - - return TabPartition( - id = requireNotNull(id), - tabGroups = tabGroups, - ) -} - -private fun JsonReader.group(): TabGroup { - beginObject() - - var id: String? = null - var name: String? = null - val tabIds = mutableSetOf() - - while (hasNext()) { - when (nextName()) { - Keys.TAB_GROUP_ID_KEY -> id = nextString() - Keys.TAB_GROUP_NAME_KEY -> name = nextString() - Keys.TAB_GROUP_TAB_IDS_KEY -> { - beginArray() - - while (peek() != JsonToken.END_ARRAY) { - tabIds.add(nextString()) - } - - endArray() - } - } - } - - endObject() - - return TabGroup( - id = requireNotNull(id), - name = requireNotNull(name), - tabIds = tabIds, - ) -} diff --git a/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/serialize/BrowserStateWriter.kt b/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/serialize/BrowserStateWriter.kt index 6fd94e92761b..000874d33a11 100644 --- a/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/serialize/BrowserStateWriter.kt +++ b/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/serialize/BrowserStateWriter.kt @@ -8,8 +8,6 @@ import android.util.AtomicFile import android.util.JsonWriter import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.SessionState -import mozilla.components.browser.state.state.TabGroup -import mozilla.components.browser.state.state.TabPartition import mozilla.components.browser.state.state.TabSessionState import mozilla.components.concept.engine.EngineSessionState import mozilla.components.support.ktx.util.streamJSON @@ -56,16 +54,6 @@ private fun JsonWriter.state(state: BrowserState) { endArray() - name(Keys.TAB_PARTITIONS_KEY) - - beginArray() - - state.tabPartitions.values.forEach { partition -> - partition(partition) - } - - endArray() - endObject() } @@ -161,49 +149,6 @@ private fun JsonWriter.tab(tab: TabSessionState) { endObject() } -/** Writes a [TabPartition] to [JsonWriter]. */ -private fun JsonWriter.partition(partition: TabPartition) { - beginObject() - - name(Keys.TAB_PARTITION_ID_KEY) - value(partition.id) - - name(Keys.TAB_PARTITION_GROUPS_KEY) - - beginArray() - - partition.tabGroups.forEach { group -> - group(group) - } - - endArray() - - endObject() -} - -/** Writes a [TabGroup] to [JsonWriter]. */ -private fun JsonWriter.group(group: TabGroup) { - beginObject() - - name(Keys.TAB_GROUP_ID_KEY) - value(group.id) - - name(Keys.TAB_GROUP_NAME_KEY) - value(group.name) - - name(Keys.TAB_GROUP_TAB_IDS_KEY) - - beginArray() - - group.tabIds.forEach { tabId -> - value(tabId) - } - - endArray() - - endObject() -} - /** Writes a (nullable) [EngineSessionState] to [JsonWriter]. */ private fun JsonWriter.engineSession(engineSessionState: EngineSessionState?) { if (engineSessionState == null) { diff --git a/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/serialize/Keys.kt b/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/serialize/Keys.kt index 09f0ef1c0b2f..26a3ba624674 100644 --- a/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/serialize/Keys.kt +++ b/mobile/android/android-components/components/browser/session-storage/src/main/java/mozilla/components/browser/session/storage/serialize/Keys.kt @@ -45,11 +45,4 @@ internal object Keys { const val ENGINE_SESSION_KEY = "engineSession" const val VERSION_KEY = "version" - - const val TAB_PARTITIONS_KEY = "tabPartitions" - const val TAB_PARTITION_ID_KEY = "id" - const val TAB_PARTITION_GROUPS_KEY = "tabGroups" - const val TAB_GROUP_ID_KEY = "id" - const val TAB_GROUP_NAME_KEY = "name" - const val TAB_GROUP_TAB_IDS_KEY = "tabIds" } diff --git a/mobile/android/android-components/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/AutoSaveTest.kt b/mobile/android/android-components/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/AutoSaveTest.kt index 335194dca13b..69c11489efa1 100644 --- a/mobile/android/android-components/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/AutoSaveTest.kt +++ b/mobile/android/android-components/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/AutoSaveTest.kt @@ -16,11 +16,8 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.runTest import mozilla.components.browser.state.action.ContentAction -import mozilla.components.browser.state.action.TabGroupAction import mozilla.components.browser.state.action.TabListAction import mozilla.components.browser.state.state.BrowserState -import mozilla.components.browser.state.state.TabGroup -import mozilla.components.browser.state.state.TabPartition import mozilla.components.browser.state.state.createTab import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.engine.Engine @@ -317,136 +314,6 @@ class AutoSaveTest { } } - @Test - fun `AutoSave - when tab partition gets added`() { - runTest(testDispatcher) { - val state = BrowserState() - val store = BrowserStore(state) - - val sessionStorage: SessionStorage = mock() - - val autoSave = - AutoSave( - store = store, - sessionStorage = sessionStorage, - minimumIntervalMs = 0, - ) - .whenSessionsChange(scope) - - testDispatcher.scheduler.advanceUntilIdle() - - assertNull(autoSave.saveJob) - verify(sessionStorage, never()).save(any()) - - store.dispatch( - TabGroupAction.AddTabGroupAction( - partition = "partition", - group = TabGroup(id = "group", name = "Group", tabIds = emptySet()), - ) - ) - - testDispatcher.scheduler.advanceUntilIdle() - - autoSave.saveJob?.join() - - verify(sessionStorage).save(any()) - } - } - - @Test - fun `AutoSave - when tab partition gets removed`() { - runTest(testDispatcher) { - val state = - BrowserState( - tabPartitions = - mapOf( - "partition" to - TabPartition( - id = "partition", - tabGroups = listOf(TabGroup(id = "group", name = "Group")), - ) - ) - ) - val store = BrowserStore(state) - - val sessionStorage: SessionStorage = mock() - - val autoSave = - AutoSave( - store = store, - sessionStorage = sessionStorage, - minimumIntervalMs = 0, - ) - .whenSessionsChange(scope) - - testDispatcher.scheduler.advanceUntilIdle() - - assertNull(autoSave.saveJob) - verify(sessionStorage, never()).save(any()) - - store.dispatch( - TabGroupAction.RemoveTabGroupAction( - partition = "partition", - group = "group", - ) - ) - - testDispatcher.scheduler.advanceUntilIdle() - - autoSave.saveJob?.join() - - verify(sessionStorage).save(any()) - } - } - - @Test - fun `AutoSave - when tab group in partition gets updated`() { - runTest(testDispatcher) { - val state = - BrowserState( - tabs = listOf(createTab("https://www.mozilla.org", id = "mozilla")), - tabPartitions = - mapOf( - "partition" to - TabPartition( - id = "partition", - tabGroups = listOf(TabGroup("group", "Group")), - ) - ), - ) - val store = BrowserStore(state) - - val sessionStorage: SessionStorage = mock() - - val autoSave = - AutoSave( - store = store, - sessionStorage = sessionStorage, - minimumIntervalMs = 0, - ) - .whenSessionsChange(scope) - - testDispatcher.scheduler.advanceUntilIdle() - - assertNull(autoSave.saveJob) - verify(sessionStorage, never()).save(any()) - - store.dispatch( - TabGroupAction.AddTabAction( - partition = "partition", - group = "group", - tabId = "mozilla", - ) - ) - - testDispatcher.scheduler.advanceUntilIdle() - - autoSave.saveJob?.join() - - verify(sessionStorage).save(any()) - } - } - @Test fun `AutoSave - periodically in foreground`() { val engine: Engine = mock() diff --git a/mobile/android/android-components/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/SessionStorageTest.kt b/mobile/android/android-components/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/SessionStorageTest.kt index 4dd69b0e3088..a9360f650472 100644 --- a/mobile/android/android-components/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/SessionStorageTest.kt +++ b/mobile/android/android-components/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/SessionStorageTest.kt @@ -11,8 +11,6 @@ import mozilla.components.browser.state.ext.getUrl import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.EngineState import mozilla.components.browser.state.state.ReaderState -import mozilla.components.browser.state.state.TabGroup -import mozilla.components.browser.state.state.TabPartition import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.state.createTab import mozilla.components.browser.state.state.recover.RecoverableTab @@ -33,7 +31,7 @@ import org.mockito.Mockito.verify @RunWith(AndroidJUnit4::class) class SessionStorageTest { @Test - fun `Restored browser state should contain tabs and tab partitions of saved state`() { + fun `Restored browser state should contain tabs of saved state`() { // Build the state val engineSessionState1 = FakeEngineSessionState("engineState1") @@ -44,15 +42,10 @@ class SessionStorageTest { val tab2 = createTab("https://getpocket.com", id = "tab2", contextId = "context2") val tab3 = createTab("https://www.firefox.com", id = "tab3", parent = tab1) - val tabGroup = TabGroup(id = "group1", name = "Group 1", tabIds = setOf("a")) - val tabPartition = TabPartition(id = "testFeaturePartition1", tabGroups = listOf(tabGroup)) - val tabPartitions = mapOf("testFeaturePartition1" to tabPartition) - val state = BrowserState( tabs = listOf(tab1, tab2, tab3), selectedTabId = tab1.id, - tabPartitions = tabPartitions, ) // Persist the state @@ -70,7 +63,6 @@ class SessionStorageTest { assertEquals(3, restoredState.tabs.size) assertEquals("tab1", restoredState.selectedTabId) - assertEquals(tabPartitions, restoredState.tabPartitions) tab1.assertSameAs(restoredState.tabs[0]) tab2.assertSameAs(restoredState.tabs[1]) diff --git a/mobile/android/android-components/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/serialize/BrowserStateWriterReaderTest.kt b/mobile/android/android-components/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/serialize/BrowserStateWriterReaderTest.kt index 0acbcb58245c..0c1bd81c40b2 100644 --- a/mobile/android/android-components/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/serialize/BrowserStateWriterReaderTest.kt +++ b/mobile/android/android-components/components/browser/session-storage/src/test/java/mozilla/components/browser/session/storage/serialize/BrowserStateWriterReaderTest.kt @@ -19,8 +19,6 @@ import mozilla.components.browser.state.state.LastMediaAccessState import mozilla.components.browser.state.state.PackageCategory import mozilla.components.browser.state.state.ReaderState import mozilla.components.browser.state.state.SessionState -import mozilla.components.browser.state.state.TabGroup -import mozilla.components.browser.state.state.TabPartition import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.state.TranslationsBrowserState import mozilla.components.browser.state.state.createTab @@ -458,18 +456,14 @@ class BrowserStateWriterReaderTest { } @Test - fun `Read and write tabs and tab partitions`() { + fun `Read and write tabs`() { val engineState = createFakeEngineState() val engine = createFakeEngine(engineState) val tab = createTab(url = "https://www.mozilla.org", id = "mozilla") - val tabGroup = TabGroup(id = "group1", name = "Group 1", tabIds = setOf("mozilla")) - val tabPartition = TabPartition(id = "testFeaturePartition1", tabGroups = listOf(tabGroup)) - val tabPartitions = mapOf("testFeaturePartition1" to tabPartition) val state = BrowserState( tabs = listOf(tab), - tabPartitions = tabPartitions, selectedTabId = "mozilla", ) @@ -484,15 +478,6 @@ class BrowserStateWriterReaderTest { assertNotNull(restoredState) assertEquals("https://www.mozilla.org", restoredState.tabs[0].state.url) - assertEquals(1, restoredState.tabPartitions.size) - - val restoredPartition = restoredState.tabPartitions["testFeaturePartition1"] - assertNotNull(restoredPartition) - assertEquals("testFeaturePartition1", restoredPartition.id) - assertEquals(1, restoredPartition.tabGroups.size) - assertEquals("group1", restoredPartition.tabGroups[0].id) - assertEquals("Group 1", restoredPartition.tabGroups[0].name) - assertEquals(setOf("mozilla"), restoredPartition.tabGroups[0].tabIds) } @Test diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/action/BrowserAction.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/action/BrowserAction.kt index af279d685f4b..4c9c0a9e742f 100644 --- a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/action/BrowserAction.kt +++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/action/BrowserAction.kt @@ -22,8 +22,6 @@ import mozilla.components.browser.state.state.ReaderState import mozilla.components.browser.state.state.SearchState import mozilla.components.browser.state.state.SecurityInfo import mozilla.components.browser.state.state.SessionState -import mozilla.components.browser.state.state.TabGroup -import mozilla.components.browser.state.state.TabPartition import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.state.TrackingProtectionState import mozilla.components.browser.state.state.UndoHistoryState @@ -208,13 +206,11 @@ sealed class TabListAction : BrowserAction() { * @property tabs the [TabSessionState]s to restore. * @property selectedTabId the ID of the tab to select. * @property restoreLocation [RestoreLocation] indicating where to restore [tabs]. - * @property tabPartitions a mapping of IDs to the corresponding [TabPartition]. */ data class RestoreAction( val tabs: List, val selectedTabId: String? = null, val restoreLocation: RestoreLocation, - val tabPartitions: Map = emptyMap(), ) : TabListAction() { /** Indicates what location the tabs should be restored at */ @@ -239,89 +235,6 @@ sealed class TabListAction : BrowserAction() { object RemoveAllNormalTabsAction : TabListAction() } -/** [BrowserAction] implementations related to updating tab partitions and groups inside [BrowserState]. */ -sealed class TabGroupAction : BrowserAction() { - /** - * Adds a new group to [BrowserState.tabPartitions]. If the corresponding partition doesn't exist it will be - * created. - * - * @property partition the ID of the partition the group belongs to. - * @property group the [TabGroup] to add. - */ - data class AddTabGroupAction( - val partition: String, - val group: TabGroup, - ) : TabGroupAction() - - /** - * Removes a group from [BrowserState.tabPartitions]. Empty partitions will be be removed i.e., if the last group in - * a partition is removed, the partition is removed as well. - * - * @property partition the ID of the partition the group belongs to. - * @property group the ID of the group to remove. - */ - data class RemoveTabGroupAction( - val partition: String, - val group: String, - ) : TabGroupAction() - - /** - * Adds the provided tab to a group in [BrowserState]. - * - * @property partition the ID of the partition the group belongs to. If the corresponding partition doesn't exist it - * will be created. - * @property group the ID of the group. - * @property tabId the ID of the tab to add to the group. If the corresponding tab is already in the group, it won't - * be added again. - */ - data class AddTabAction( - val partition: String, - val group: String, - val tabId: String, - ) : TabGroupAction() - - /** - * Adds the provided tabs to a group in [BrowserState]. - * - * @property partition the ID of the partition the group belongs to. If the corresponding partition doesn't exist it - * will be created. - * @property group the ID of the group. - * @property tabIds the IDs of the tabs to add to the group. If a tab is already in the group, it won't be added - * again. - */ - data class AddTabsAction( - val partition: String, - val group: String, - val tabIds: Set, - ) : TabGroupAction() - - /** - * Removes the provided tab from a group in [BrowserState]. - * - * @property partition the ID of the partition the group belongs to. - * @property group the ID of the group. - * @property tabId the ID of the tab to remove from the group. - */ - data class RemoveTabAction( - val partition: String, - val group: String, - val tabId: String, - ) : TabGroupAction() - - /** - * Removes the provided tabs from a group in [BrowserState]. - * - * @property partition the ID of the partition the group belongs to. - * @property group the ID of the group. - * @property tabIds the IDs of the tabs to remove from the group. - */ - data class RemoveTabsAction( - val partition: String, - val group: String, - val tabIds: Set, - ) : TabGroupAction() -} - /** [BrowserAction] implementations dealing with "undo" after removing a tab. */ sealed class UndoAction : BrowserAction() { /** Adds the list of [tabs] to [UndoHistoryState] with the given [tag]. */ diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/BrowserStateReducer.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/BrowserStateReducer.kt index b910c0c08599..59970d6a9b0c 100644 --- a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/BrowserStateReducer.kt +++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/BrowserStateReducer.kt @@ -29,7 +29,6 @@ import mozilla.components.browser.state.action.SearchAction import mozilla.components.browser.state.action.ShareResourceAction import mozilla.components.browser.state.action.SystemAction import mozilla.components.browser.state.action.SystemPermissionRequestAction -import mozilla.components.browser.state.action.TabGroupAction import mozilla.components.browser.state.action.TabListAction import mozilla.components.browser.state.action.TrackingProtectionAction import mozilla.components.browser.state.action.TranslationsAction @@ -63,7 +62,6 @@ internal object BrowserStateReducer { is ReaderAction -> ReaderStateReducer.reduce(state, action) is SystemAction -> SystemReducer.reduce(state, action) is TabListAction -> TabListReducer.reduce(state, action) - is TabGroupAction -> TabGroupReducer.reduce(state, action) is TrackingProtectionAction -> TrackingProtectionStateReducer.reduce(state, action) is TranslationsAction -> TranslationsStateReducer.reduce(state, action) is WebExtensionAction -> WebExtensionReducer.reduce(state, action) diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/TabGroupReducer.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/TabGroupReducer.kt deleted file mode 100644 index f2d212fcd99b..000000000000 --- a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/TabGroupReducer.kt +++ /dev/null @@ -1,153 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.browser.state.reducer - -import mozilla.components.browser.state.action.TabGroupAction -import mozilla.components.browser.state.state.BrowserState -import mozilla.components.browser.state.state.TabGroup -import mozilla.components.browser.state.state.TabPartition -import mozilla.components.browser.state.state.TabSessionState -import mozilla.components.browser.state.state.getGroupById - -internal object TabGroupReducer { - - /** [TabGroupAction] reducer function for modifying tab groups in [BrowserState.tabPartitions]. */ - fun reduce(state: BrowserState, action: TabGroupAction): BrowserState { - return when (action) { - is TabGroupAction.AddTabGroupAction -> { - action.group.tabIds.forEach { state.assertTabExists(it) } - state.addTabGroup(action.partition, action.group) - } - - is TabGroupAction.RemoveTabGroupAction -> { - state.removeTabGroup(action.partition, action.group) - } - - is TabGroupAction.AddTabAction -> { - state.assertTabExists(action.tabId) - - if (!state.groupExists(action.partition, action.group)) { - state.addTabGroup(action.partition, TabGroup(action.group, tabIds = setOf(action.tabId))) - } else { - state.updateTabGroup(action.partition, action.group) { - it.copy(tabIds = it.tabIds + action.tabId) - } - } - } - - is TabGroupAction.AddTabsAction -> { - action.tabIds.forEach { state.assertTabExists(it) } - - if (!state.groupExists(action.partition, action.group)) { - state.addTabGroup(action.partition, TabGroup(action.group, tabIds = action.tabIds)) - } else { - state.updateTabGroup(action.partition, action.group) { - it.copy(tabIds = it.tabIds + action.tabIds) - } - } - } - - is TabGroupAction.RemoveTabAction -> { - state.updateTabGroup(action.partition, action.group) { - it.copy(tabIds = it.tabIds - action.tabId) - } - } - - is TabGroupAction.RemoveTabsAction -> { - state.updateTabGroup(action.partition, action.group) { - it.copy(tabIds = it.tabIds - action.tabIds) - } - } - } - } -} - -/** Adds the provided tab group and creates the partition if needed. */ -private fun BrowserState.addTabGroup(partitionId: String, group: TabGroup): BrowserState { - val partition = tabPartitions[partitionId] - val updatedPartition = - if (partition != null) { - require(partition.getGroupById(group.id) == null) { - "Tab group with same ID already exists" - } - partition.copy(tabGroups = partition.tabGroups + group) - } else { - TabPartition(partitionId, tabGroups = listOf(group)) - } - return copy(tabPartitions = tabPartitions + (partitionId to updatedPartition)) -} - -/** Removes a tab group from the provided partition. */ -private fun BrowserState.removeTabGroup(partitionId: String, groupId: String): BrowserState { - val partition = tabPartitions[partitionId] - val group = partition?.getGroupById(groupId) - return if (group != null) { - val updatedPartition = partition.copy(tabGroups = partition.tabGroups - group) - if (updatedPartition.tabGroups.isEmpty()) { - copy(tabPartitions = tabPartitions - partitionId) - } else { - copy(tabPartitions = tabPartitions + (partitionId to updatedPartition)) - } - } else { - this - } -} - -/** Checks if a tab group exists in the provided partition. */ -private fun BrowserState.groupExists(partitionId: String, groupId: String): Boolean { - return tabPartitions[partitionId]?.getGroupById(groupId) != null -} - -/** - * Checks that the provided tab exists and throws an [IllegalArgumentException] otherwise. - * - * @param tabId the id of the [TabSessionState] to check. - */ -private fun BrowserState.assertTabExists(tabId: String) { - require(tabs.find { it.id == tabId } != null) { - "Tab does not exist" - } -} - -/** Utility function to update a [TabGroup] within a [TabPartition] in [BrowserState]. */ -private fun BrowserState.updateTabGroup( - partitionId: String, - groupId: String, - update: (TabGroup) -> TabGroup, -): BrowserState { - return updateTabPartition(partitionId) { partition -> - partition.updateTabGroup(groupId, update) - } -} - -/** Updates the specified tab partition by invoking [update]. */ -private inline fun BrowserState.updateTabPartition( - partitionId: String, - crossinline update: (TabPartition) -> TabPartition, -): BrowserState { - val partition = tabPartitions[partitionId] ?: return this - return copy(tabPartitions = tabPartitions + (partitionId to update(partition))) -} - -/** Updates the specified tab group within this partition by invoking [update]. */ -private inline fun TabPartition.updateTabGroup( - groupId: String, - crossinline update: (TabGroup) -> TabGroup, -): TabPartition { - return tabGroups.update(groupId, update)?.let { - copy(tabGroups = it) - } ?: this -} - -/** Updates the provided tab group by invoking [update]. */ -private inline fun List.update( - groupId: String, - crossinline update: (TabGroup) -> TabGroup, -): List? { - val groupIndex = indexOfFirst { it.id == groupId } - if (groupIndex == -1) return null - - return subList(0, groupIndex) + update(get(groupIndex)) + subList(groupIndex + 1, size) -} diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/TabListReducer.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/TabListReducer.kt index 317c8fae57f7..1fd22ad485be 100644 --- a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/TabListReducer.kt +++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/TabListReducer.kt @@ -9,7 +9,6 @@ import mozilla.components.browser.state.action.TabListAction import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.selector.selectedTab import mozilla.components.browser.state.state.BrowserState -import mozilla.components.browser.state.state.TabPartition import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.state.recover.toTabSessionStates @@ -130,7 +129,6 @@ internal object TabListReducer { state.copy( tabs = updatedTabList, selectedTabId = updatedSelection, - tabPartitions = state.tabPartitions.removeTabs(setOf(action.tabId)), ) } } @@ -168,7 +166,6 @@ internal object TabListReducer { state.copy( tabs = updatedTabList, selectedTabId = updatedSelection, - tabPartitions = state.tabPartitions.removeTabs(action.tabIds.toSet()), ) } } @@ -203,15 +200,8 @@ internal object TabListReducer { } } - val combinedTabPartitions = - mergeTabPartitions( - currentTabPartitions = state.tabPartitions, - restoredTabPartitions = action.tabPartitions, - ) - state.copy( tabs = combinedTabList, - tabPartitions = combinedTabPartitions, selectedTabId = if (action.selectedTabId != null && state.selectedTabId == null) { // We only want to update the selected tab if none has been already selected. Otherwise we @@ -229,7 +219,6 @@ internal object TabListReducer { state.copy( tabs = emptyList(), selectedTabId = null, - tabPartitions = state.tabPartitions.removeAllTabs(), ) } @@ -247,7 +236,6 @@ internal object TabListReducer { } else { state.selectedTabId }, - tabPartitions = state.tabPartitions.removeTabs(partition.first.map { it.id }.toSet()), ) } @@ -265,7 +253,6 @@ internal object TabListReducer { } else { state.selectedTabId }, - tabPartitions = state.tabPartitions.removeTabs(partition.second.map { it.id }.toSet()), ) } } @@ -359,59 +346,3 @@ private fun requireUniqueTab(state: BrowserState, tab: TabSessionState) { "Tab with same ID already exists" } } - -/** Removes references to the provided tabs from all [TabPartition]s. */ -private fun Map.removeTabs(removedTabIds: Set) = mapValues { - val partition = it.value - partition.copy( - tabGroups = - partition.tabGroups.map { group -> - group.copy(tabIds = group.tabIds - removedTabIds) - } - ) -} - -/** Removes references to the provided tabs from all [TabPartition]s. */ -private fun Map.removeAllTabs() = mapValues { - val partition = it.value - partition.copy(tabGroups = partition.tabGroups.map { group -> group.copy(tabIds = emptySet()) }) -} - -private fun mergeTabPartitions( - currentTabPartitions: Map, - restoredTabPartitions: Map, -): Map { - val combinedTabPartitions = currentTabPartitions.toMutableMap() - - restoredTabPartitions.forEach { (id, restoredPartition) -> - val existingPartition = combinedTabPartitions[id] - combinedTabPartitions[id] = - if (existingPartition != null) { - mergeTabGroups(existingPartition, restoredPartition) - } else { - restoredPartition - } - } - - return combinedTabPartitions -} - -private fun mergeTabGroups( - currentTabPartition: TabPartition, - restoredTabPartition: TabPartition, -): TabPartition { - val combinedTabGroups = currentTabPartition.tabGroups.toMutableList() - - restoredTabPartition.tabGroups.forEach { restoredGroup -> - val existingGroupIndex = combinedTabGroups.indexOfFirst { it.id == restoredGroup.id } - if (existingGroupIndex != -1) { - val existingGroup = combinedTabGroups[existingGroupIndex] - combinedTabGroups[existingGroupIndex] = - existingGroup.copy(tabIds = existingGroup.tabIds + restoredGroup.tabIds) - } else { - combinedTabGroups.add(restoredGroup) - } - } - - return currentTabPartition.copy(tabGroups = combinedTabGroups) -} diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/BrowserState.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/BrowserState.kt index e393248d63d6..a4a0c638e039 100644 --- a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/BrowserState.kt +++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/BrowserState.kt @@ -15,8 +15,6 @@ import mozilla.components.lib.state.State * Value type that represents the complete state of the browser/engine. * * @property tabs the list of open tabs, defaults to an empty list. - * @property tabPartitions a mapping of IDs to the corresponding [TabPartition]. A partition is used to store tab groups - * for a specific feature. * @property closedTabs the list of recently closed tabs if a [RecentlyClosedMiddleware] is added, defaults to an empty * list. * @property selectedTabId the ID of the currently selected (active) tab. @@ -43,7 +41,6 @@ import mozilla.components.lib.state.State */ data class BrowserState( val tabs: List = emptyList(), - val tabPartitions: Map = emptyMap(), val customTabs: List = emptyList(), val closedTabs: List = emptyList(), val selectedTabId: String? = null, diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/TabPartition.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/TabPartition.kt deleted file mode 100644 index c9dd924569c1..000000000000 --- a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/TabPartition.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.browser.state.state - -import java.util.UUID - -/** - * Value type representing a tab partition. Partitions can overlap i.e., a tab can be in multiple partitions at the same - * time. - * - * @property id The ID of a tab partition. This should uniquely identify the feature responsible for managing those - * groups. - * @property tabGroups The groups of tabs in this partition. A partition can have one or more groups, depending on use - * case. Empty partitions will be removed by the system. - */ -data class TabPartition( - val id: String, - val tabGroups: List = emptyList(), -) - -/** - * Value type representing a tab group. - * - * @property id The unique ID of this tab group. - * @property name The name of this tab group. - * @property tabIds The IDs of all tabs in this group. - */ -data class TabGroup( - val id: String = UUID.randomUUID().toString(), - val name: String = "", - val tabIds: Set = emptySet(), -) - -/** - * Returns the first tab group matching the provided [name], or null if no match was found. Note that we allow multiple - * groups with the same name in a partition but disambiguation needs to be handled on a feature level. - */ -fun TabPartition.getGroupByName(name: String) = - this.tabGroups.firstOrNull { - it.name.equals(name, ignoreCase = true) - } - -/** Returns the tab group matching the provided [id], or null if not match was found. */ -fun TabPartition.getGroupById(id: String) = - this.tabGroups.firstOrNull { - it.id == id - } - -/** - * Check if a [TabPartition] has no tabs - * - * @return true if the [TabPartition] has no tabs, false otherwise. - */ -fun TabPartition?.isEmpty(): Boolean { - return this?.tabGroups?.filter { tabGroup -> tabGroup.tabIds.isNotEmpty() }.isNullOrEmpty() -} - -/** - * Check if a [TabPartition] has tabs - * - * @return true if the [TabPartition] has tabs, false otherwise. - */ -fun TabPartition?.isNotEmpty(): Boolean { - return isEmpty().not() -} diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/UndoHistoryState.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/UndoHistoryState.kt index 0c8bee5daa39..b40efc0bac5b 100644 --- a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/UndoHistoryState.kt +++ b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/state/UndoHistoryState.kt @@ -16,11 +16,9 @@ import mozilla.components.browser.state.state.recover.RecoverableTab * removing/restoring the wrong state in a multi-threaded environment. * @param tabs List of previously removed tabs. * @param selectedTabId Id of the tab in [tabs] that was selected and should get reselected on restore. - * @param tabPartitions a mapping of IDs to the corresponding [TabPartition]. */ data class UndoHistoryState( val tag: String = "", val tabs: List = emptyList(), val selectedTabId: String? = null, - val tabPartitions: Map = emptyMap(), ) diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/TabGroupActionTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/TabGroupActionTest.kt index 03a170a36f52..8b137891791f 100644 --- a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/TabGroupActionTest.kt +++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/TabGroupActionTest.kt @@ -1,318 +1 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package mozilla.components.browser.state.action - -import kotlin.test.assertNotNull -import mozilla.components.browser.state.reducer.BrowserStateReducer -import mozilla.components.browser.state.state.BrowserState -import mozilla.components.browser.state.state.TabGroup -import mozilla.components.browser.state.state.TabPartition -import mozilla.components.browser.state.state.createTab -import mozilla.components.browser.state.state.getGroupById -import mozilla.components.browser.state.state.getGroupByName -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNull -import org.junit.Assert.assertSame -import org.junit.Assert.assertTrue -import org.junit.Test - -class TabGroupActionTest { - - @Test - fun `AddTabGroupAction - Adds provided group and creates partition if needed`() { - var state = BrowserState() - - val partition = "testFeaturePartition" - val testGroup = TabGroup("test", "testGroup") - state = - BrowserStateReducer.reduce( - state, - TabGroupAction.AddTabGroupAction(partition = partition, group = testGroup), - ) - - val expectedPartition = state.tabPartitions[partition] - assertNotNull(expectedPartition) - assertSame(testGroup, expectedPartition.getGroupById(testGroup.id)) - assertSame(testGroup, expectedPartition.getGroupByName(testGroup.name)) - } - - @Test - fun `AddTabGroupAction - Adds provided group with tabs`() { - var state = - BrowserState( - tabs = - listOf( - createTab(id = "tab1", url = "https://firefox.com"), - createTab(id = "tab2", url = "https://mozilla.org"), - ) - ) - - val partition = "testFeaturePartition" - val testGroup = TabGroup("test", tabIds = setOf("tab1", "tab2")) - state = - BrowserStateReducer.reduce( - state, - TabGroupAction.AddTabGroupAction(partition = partition, group = testGroup), - ) - - val expectedPartition = state.tabPartitions[partition] - assertNotNull(expectedPartition) - assertSame(testGroup, expectedPartition.getGroupById(testGroup.id)) - assertEquals(setOf("tab1", "tab2"), expectedPartition.getGroupById(testGroup.id)?.tabIds) - } - - @Test - fun `RemoveTabGroupAction - Removes provided group`() { - val tabGroup1 = TabGroup("test1", tabIds = setOf("tab1", "tab2")) - val tabGroup2 = TabGroup("test2", tabIds = setOf("tab1", "tab2")) - val tabPartition = TabPartition("testFeaturePartition", tabGroups = listOf(tabGroup1, tabGroup2)) - - var state = - BrowserState( - tabs = - listOf( - createTab(id = "tab1", url = "https://firefox.com"), - createTab(id = "tab2", url = "https://mozilla.org"), - ), - tabPartitions = mapOf("testFeaturePartition" to tabPartition), - ) - - assertNotNull(state.tabPartitions[tabPartition.id]?.getGroupById(tabGroup1.id)) - assertNotNull(state.tabPartitions[tabPartition.id]?.getGroupById(tabGroup2.id)) - state = BrowserStateReducer.reduce(state, TabGroupAction.RemoveTabGroupAction(tabPartition.id, tabGroup1.id)) - assertNull(state.tabPartitions[tabPartition.id]?.getGroupById(tabGroup1.id)) - assertNotNull(state.tabPartitions[tabPartition.id]?.getGroupById(tabGroup2.id)) - } - - @Test - fun `RemoveTabGroupAction - Empty partitions are removed`() { - val tabGroup = TabGroup("test1", tabIds = setOf("tab1", "tab2")) - val tabPartition = TabPartition("testFeaturePartition", tabGroups = listOf(tabGroup)) - - var state = - BrowserState( - tabs = - listOf( - createTab(id = "tab1", url = "https://firefox.com"), - createTab(id = "tab2", url = "https://mozilla.org"), - ), - tabPartitions = mapOf("testFeaturePartition" to tabPartition), - ) - - assertNotNull(state.tabPartitions[tabPartition.id]?.getGroupById(tabGroup.id)) - state = BrowserStateReducer.reduce(state, TabGroupAction.RemoveTabGroupAction(tabPartition.id, tabGroup.id)) - assertNull(state.tabPartitions[tabPartition.id]) - } - - @Test - fun `AddTabAction - Adds provided tab to groups`() { - val tabGroup = TabGroup("test1", tabIds = emptySet()) - val tabPartition = TabPartition("testFeaturePartition", tabGroups = listOf(tabGroup)) - val tab = createTab(id = "tab1", url = "https://firefox.com") - - var state = - BrowserState( - tabs = listOf(tab), - tabPartitions = mapOf("testFeaturePartition" to tabPartition), - ) - - state = BrowserStateReducer.reduce(state, TabGroupAction.AddTabAction(tabPartition.id, tabGroup.id, tab.id)) - - val expectedPartition = state.tabPartitions[tabPartition.id] - assertNotNull(expectedPartition) - val expectedGroup = expectedPartition.getGroupById(tabGroup.id) - assertNotNull(expectedGroup) - assertTrue(expectedGroup.tabIds.contains(tab.id)) - } - - @Test - fun `AddTabAction - Creates partition if needed`() { - val tabGroup = TabGroup("test1", tabIds = emptySet()) - val tabPartition = TabPartition("testFeaturePartition") - val tab = createTab(id = "tab1", url = "https://firefox.com") - - var state = BrowserState(tabs = listOf(tab)) - - state = BrowserStateReducer.reduce(state, TabGroupAction.AddTabAction(tabPartition.id, tabGroup.id, tab.id)) - - val expectedPartition = state.tabPartitions[tabPartition.id] - assertNotNull(expectedPartition) - val expectedGroup = expectedPartition.getGroupById(tabGroup.id) - assertNotNull(expectedGroup) - assertTrue(expectedGroup.tabIds.contains(tab.id)) - } - - @Test - fun `AddTabAction - Doesn't add tab if already in group`() { - val tabGroup = TabGroup("test1", tabIds = setOf("tab1")) - val tabPartition = TabPartition("testFeaturePartition", tabGroups = listOf(tabGroup)) - val tab = createTab(id = "tab1", url = "https://firefox.com") - - var state = - BrowserState( - tabs = listOf(tab), - tabPartitions = mapOf("testFeaturePartition" to tabPartition), - ) - - state = BrowserStateReducer.reduce(state, TabGroupAction.AddTabAction(tabPartition.id, tabGroup.id, tab.id)) - - val expectedPartition = state.tabPartitions[tabPartition.id] - assertNotNull(expectedPartition) - val expectedGroup = expectedPartition.getGroupById(tabGroup.id) - assertNotNull(expectedGroup) - assertTrue(expectedGroup.tabIds.contains(tab.id)) - assertEquals(1, expectedGroup.tabIds.size) - } - - @Test - fun `AddTabsAction - Adds provided tab to groups`() { - val tabGroup = TabGroup("test1", tabIds = emptySet()) - val tabPartition = TabPartition("testFeaturePartition", tabGroups = listOf(tabGroup)) - val tab1 = createTab(id = "tab1", url = "https://firefox.com") - val tab2 = createTab(id = "tab2", url = "https://mozilla.org") - - var state = - BrowserState( - tabs = listOf(tab1, tab2), - tabPartitions = mapOf("testFeaturePartition" to tabPartition), - ) - - state = - BrowserStateReducer.reduce( - state, - TabGroupAction.AddTabsAction(tabPartition.id, tabGroup.id, setOf(tab1.id, tab2.id)), - ) - - val expectedPartition = state.tabPartitions[tabPartition.id] - assertNotNull(expectedPartition) - val expectedGroup = expectedPartition.getGroupById(tabGroup.id) - assertNotNull(expectedGroup) - assertTrue(expectedGroup.tabIds.contains(tab1.id)) - assertTrue(expectedGroup.tabIds.contains(tab2.id)) - } - - @Test - fun `AddTabsAction - Creates partition if needed`() { - val tabGroup = TabGroup("test1", tabIds = emptySet()) - val tabPartition = TabPartition("testFeaturePartition") - val tab1 = createTab(id = "tab1", url = "https://firefox.com") - val tab2 = createTab(id = "tab2", url = "https://mozilla.org") - - var state = BrowserState(tabs = listOf(tab1, tab2)) - - state = - BrowserStateReducer.reduce( - state, - TabGroupAction.AddTabsAction(tabPartition.id, tabGroup.id, setOf(tab1.id, tab2.id)), - ) - - val expectedPartition = state.tabPartitions[tabPartition.id] - assertNotNull(expectedPartition) - val expectedGroup = expectedPartition.getGroupById(tabGroup.id) - assertNotNull(expectedGroup) - assertTrue(expectedGroup.tabIds.contains(tab1.id)) - assertTrue(expectedGroup.tabIds.contains(tab2.id)) - } - - @Test - fun `AddTabsAction - Doesn't add tabs if already in group`() { - val tabGroup = TabGroup("test1", tabIds = setOf("tab1")) - val tabPartition = TabPartition("testFeaturePartition", tabGroups = listOf(tabGroup)) - val tab1 = createTab(id = "tab1", url = "https://firefox.com") - val tab2 = createTab(id = "tab2", url = "https://mozilla.org") - - var state = - BrowserState( - tabs = listOf(tab1, tab2), - tabPartitions = mapOf("testFeaturePartition" to tabPartition), - ) - - state = - BrowserStateReducer.reduce( - state, - TabGroupAction.AddTabsAction(tabPartition.id, tabGroup.id, setOf(tab1.id, tab2.id)), - ) - - val expectedPartition = state.tabPartitions[tabPartition.id] - assertNotNull(expectedPartition) - val expectedGroup = expectedPartition.getGroupById(tabGroup.id) - assertNotNull(expectedGroup) - assertTrue(expectedGroup.tabIds.contains(tab1.id)) - assertTrue(expectedGroup.tabIds.contains(tab2.id)) - assertEquals(2, expectedGroup.tabIds.size) - } - - @Test - fun `AddTabsAction - Creates partition if needed but only adds distinct tabs`() { - val tabGroup = TabGroup("test1", tabIds = emptySet()) - val tabPartition = TabPartition("testFeaturePartition") - val tab1 = createTab(id = "tab1", url = "https://firefox.com") - - var state = BrowserState(tabs = listOf(tab1)) - - state = - BrowserStateReducer.reduce( - state, - TabGroupAction.AddTabsAction(tabPartition.id, tabGroup.id, setOf(tab1.id, tab1.id)), - ) - - val expectedPartition = state.tabPartitions[tabPartition.id] - assertNotNull(expectedPartition) - val expectedGroup = expectedPartition.getGroupById(tabGroup.id) - assertNotNull(expectedGroup) - assertTrue(expectedGroup.tabIds.contains(tab1.id)) - assertEquals(1, expectedGroup.tabIds.size) - } - - @Test - fun `RemoveTabAction - Removes tab from group`() { - val tab1 = createTab(id = "tab1", url = "https://firefox.com") - val tab2 = createTab(id = "tab2", url = "https://mozilla.org") - val tabGroup = TabGroup("test1", tabIds = setOf(tab1.id, tab2.id)) - val tabPartition = TabPartition("testFeaturePartition", tabGroups = listOf(tabGroup)) - - var state = - BrowserState( - tabs = listOf(tab1, tab2), - tabPartitions = mapOf("testFeaturePartition" to tabPartition), - ) - - state = BrowserStateReducer.reduce(state, TabGroupAction.RemoveTabAction(tabPartition.id, tabGroup.id, tab1.id)) - - val expectedPartition = state.tabPartitions[tabPartition.id] - assertNotNull(expectedPartition) - val expectedGroup = expectedPartition.getGroupById(tabGroup.id) - assertNotNull(expectedGroup) - assertFalse(expectedGroup.tabIds.contains(tab1.id)) - assertTrue(expectedGroup.tabIds.contains(tab2.id)) - } - - @Test - fun `RemoveTabsAction - Removes tabs from group`() { - val tab1 = createTab(id = "tab1", url = "https://firefox.com") - val tab2 = createTab(id = "tab2", url = "https://mozilla.org") - val tabGroup = TabGroup("test1", tabIds = setOf(tab1.id, tab2.id)) - val tabPartition = TabPartition("testFeaturePartition", tabGroups = listOf(tabGroup)) - - var state = - BrowserState( - tabs = listOf(tab1, tab2), - tabPartitions = mapOf("testFeaturePartition" to tabPartition), - ) - - state = - BrowserStateReducer.reduce( - state, - TabGroupAction.RemoveTabsAction(tabPartition.id, tabGroup.id, setOf(tab1.id, tab2.id)), - ) - - val expectedPartition = state.tabPartitions[tabPartition.id] - assertNotNull(expectedPartition) - val expectedGroup = expectedPartition.getGroupById(tabGroup.id) - assertNotNull(expectedGroup) - assertTrue(expectedGroup.tabIds.isEmpty()) - } -} diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/TabListActionTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/TabListActionTest.kt index 2afc164ad121..5ecfb323ecfc 100644 --- a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/TabListActionTest.kt +++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/action/TabListActionTest.kt @@ -11,12 +11,9 @@ import mozilla.components.browser.state.selector.privateTabs import mozilla.components.browser.state.selector.selectedTab import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.SessionState -import mozilla.components.browser.state.state.TabGroup -import mozilla.components.browser.state.state.TabPartition import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.state.createCustomTab import mozilla.components.browser.state.state.createTab -import mozilla.components.browser.state.state.getGroupById import mozilla.components.browser.state.state.recover.RecoverableTab import mozilla.components.browser.state.state.recover.TabState import mozilla.components.support.test.mock @@ -179,30 +176,6 @@ class TabListActionTest { assertEquals("https://www.firefox.com", state.tabs[0].content.url) } - @Test - fun `RemoveTabAction - Removes tab from partition`() { - val tabGroup = TabGroup("test1", tabIds = setOf("a", "b")) - val tabPartition = TabPartition("testPartition", tabGroups = listOf(tabGroup)) - - var state = - BrowserState( - tabs = - listOf( - createTab(id = "a", url = "https://www.mozilla.org"), - createTab(id = "b", url = "https://www.firefox.com"), - ), - tabPartitions = mapOf(tabPartition.id to tabPartition), - ) - - state = BrowserStateReducer.reduce(state, TabListAction.RemoveTabAction("a")) - assertEquals(1, state.tabs.size) - assertEquals("https://www.firefox.com", state.tabs[0].content.url) - assertEquals( - setOf("b"), - state.tabPartitions[tabPartition.id]?.getGroupById(tabGroup.id)?.tabIds, - ) - } - @Test fun `RemoveTabsAction - Removes SessionState`() { var state = @@ -221,29 +194,6 @@ class TabListActionTest { assertEquals("https://www.getpocket.com", state.tabs[0].content.url) } - @Test - fun `RemoveTabsAction - Removes tabs from partition`() { - val tabGroup = TabGroup("test1", tabIds = setOf("a", "b")) - val tabPartition = TabPartition("testPartition", tabGroups = listOf(tabGroup)) - - var state = - BrowserState( - tabs = - listOf( - createTab(id = "a", url = "https://www.mozilla.org"), - createTab(id = "b", url = "https://www.firefox.com"), - ), - tabPartitions = mapOf(tabPartition.id to tabPartition), - ) - - state = BrowserStateReducer.reduce(state, TabListAction.RemoveTabsAction(listOf("a", "b"))) - assertEquals(0, state.tabs.size) - assertEquals( - 0, - state.tabPartitions[tabPartition.id]?.getGroupById(tabGroup.id)?.tabIds?.size, - ) - } - @Test fun `RemoveTabAction - Noop for unknown id`() { var state = @@ -660,142 +610,6 @@ class TabListActionTest { assertEquals("d", state.selectedTabId) } - @Test - fun `RestoreAction - Adds restored tabs and tab partitions and updates selected tab`() { - var state = BrowserState() - - assertEquals(0, state.tabs.size) - assertEquals(0, state.tabPartitions.size) - - val restoredTabs = - listOf( - RecoverableTab( - engineSessionState = null, - state = - TabState( - id = "a", - url = "https://www.mozilla.org", - private = false, - ), - ), - RecoverableTab( - engineSessionState = null, - state = TabState(id = "b", url = "https://www.firefox.com", private = true), - ), - RecoverableTab( - engineSessionState = null, - state = TabState(id = "c", url = "https://www.example.org", private = true), - ), - ) - val tabGroup = TabGroup(id = "group1", name = "Group 1", tabIds = setOf("a")) - val tabPartition = TabPartition(id = "testFeaturePartition", tabGroups = listOf(tabGroup)) - val restoredTabPartitions = mapOf("testFeaturePartition" to tabPartition) - - state = - BrowserStateReducer.reduce( - state, - TabListAction.RestoreAction( - tabs = restoredTabs, - selectedTabId = "c", - restoreLocation = TabListAction.RestoreAction.RestoreLocation.BEGINNING, - tabPartitions = restoredTabPartitions, - ), - ) - - assertEquals(3, state.tabs.size) - assertEquals("a", state.tabs[0].id) - assertEquals("b", state.tabs[1].id) - assertEquals("c", state.tabs[2].id) - assertEquals("c", state.selectedTabId) - assertEquals(restoredTabPartitions, state.tabPartitions) - } - - @Test - fun `RestoreAction - Merges the existing tab partitions with the restored tab partitions`() { - val tabGroup = TabGroup(id = "group1", name = "Group 1", tabIds = setOf("a")) - val tabPartition = TabPartition(id = "testFeaturePartition1", tabGroups = listOf(tabGroup)) - val tabPartitions = mapOf("testFeaturePartition1" to tabPartition) - var state = BrowserState(tabPartitions = tabPartitions) - - assertEquals(0, state.tabs.size) - assertEquals(tabPartitions, state.tabPartitions) - - val restoredTabs = - listOf( - RecoverableTab( - engineSessionState = null, - state = - TabState( - id = "a", - url = "https://www.mozilla.org", - private = false, - ), - ), - RecoverableTab( - engineSessionState = null, - state = TabState(id = "b", url = "https://www.firefox.com", private = true), - ), - RecoverableTab( - engineSessionState = null, - state = TabState(id = "c", url = "https://www.example.org", private = true), - ), - ) - val restoredTabGroups = - listOf( - TabGroup(id = "group1", name = "Group 1", tabIds = setOf("b")), - TabGroup(id = "group2", name = "Group 2", tabIds = setOf("c")), - ) - val restoredTabPartitions = - mapOf( - "testFeaturePartition1" to - TabPartition( - id = "testFeaturePartition1", - tabGroups = restoredTabGroups, - ), - "testFeaturePartition2" to - TabPartition( - id = "testFeaturePartition2", - tabGroups = emptyList(), - ), - ) - - state = - BrowserStateReducer.reduce( - state, - TabListAction.RestoreAction( - tabs = restoredTabs, - selectedTabId = "c", - restoreLocation = TabListAction.RestoreAction.RestoreLocation.BEGINNING, - tabPartitions = restoredTabPartitions, - ), - ) - - assertEquals(3, state.tabs.size) - assertEquals("a", state.tabs[0].id) - assertEquals("b", state.tabs[1].id) - assertEquals("c", state.tabs[2].id) - assertEquals("c", state.selectedTabId) - - val expectedTabPartitions = - mapOf( - "testFeaturePartition1" to - TabPartition( - id = "testFeaturePartition1", - tabGroups = - listOf( - TabGroup("group1", name = "Group 1", tabIds = setOf("a", "b")), - TabGroup("group2", name = "Group 2", tabIds = setOf("c")), - ), - ), - "testFeaturePartition2" to - TabPartition( - id = "testFeaturePartition2", - tabGroups = emptyList(), - ), - ) - assertEquals(expectedTabPartitions, state.tabPartitions) - } - @Test fun `RestoreAction - Adds restored tabs to the beginning of existing tabs without updating selection`() { val initialState = @@ -1292,29 +1106,6 @@ class TabListActionTest { assertEquals("a2", state.customTabs.last().id) } - @Test - fun `RemoveAllTabsAction - Removes tabs from partition`() { - val tabGroup = TabGroup("test1", tabIds = setOf("a", "b")) - val tabPartition = TabPartition("testPartition", tabGroups = listOf(tabGroup)) - - var state = - BrowserState( - tabs = - listOf( - createTab(id = "a", url = "https://www.mozilla.org"), - createTab(id = "b", url = "https://www.firefox.com", private = true), - ), - tabPartitions = mapOf(tabPartition.id to tabPartition), - ) - - state = BrowserStateReducer.reduce(state, TabListAction.RemoveAllTabsAction()) - assertEquals(0, state.tabs.size) - assertEquals( - 0, - state.tabPartitions[tabPartition.id]?.getGroupById(tabGroup.id)?.tabIds?.size, - ) - } - @Test fun `RemoveAllPrivateTabsAction - Removes only private tabs`() { var state = @@ -1361,34 +1152,6 @@ class TabListActionTest { assertEquals("a1", state.customTabs.last().id) } - @Test - fun `RemoveAllPrivateTabsAction - Removes tabs from partition`() { - val normalTabGroup = TabGroup("test1", tabIds = setOf("a")) - val privateTabGroup = TabGroup("test2", tabIds = setOf("b")) - val tabPartition = TabPartition("testPartition", tabGroups = listOf(normalTabGroup, privateTabGroup)) - - var state = - BrowserState( - tabs = - listOf( - createTab(id = "a", url = "https://www.mozilla.org"), - createTab(id = "b", url = "https://www.firefox.com", private = true), - ), - tabPartitions = mapOf(tabPartition.id to tabPartition), - ) - - state = BrowserStateReducer.reduce(state, TabListAction.RemoveAllPrivateTabsAction) - assertEquals(1, state.tabs.size) - assertEquals( - 1, - state.tabPartitions[tabPartition.id]?.getGroupById(normalTabGroup.id)?.tabIds?.size, - ) - assertEquals( - 0, - state.tabPartitions[tabPartition.id]?.getGroupById(privateTabGroup.id)?.tabIds?.size, - ) - } - @Test fun `RemoveAllNormalTabsAction - Removes only normal (non-private) tabs`() { var state = @@ -1436,34 +1199,6 @@ class TabListActionTest { assertEquals("a1", state.customTabs.last().id) } - @Test - fun `RemoveAllNormalTabsAction - Removes tabs from partition`() { - val normalTabGroup = TabGroup("test1", tabIds = setOf("a")) - val privateTabGroup = TabGroup("test2", tabIds = setOf("b")) - val tabPartition = TabPartition("testPartition", tabGroups = listOf(normalTabGroup, privateTabGroup)) - - var state = - BrowserState( - tabs = - listOf( - createTab(id = "a", url = "https://www.mozilla.org"), - createTab(id = "b", url = "https://www.firefox.com", private = true), - ), - tabPartitions = mapOf(tabPartition.id to tabPartition), - ) - - state = BrowserStateReducer.reduce(state, TabListAction.RemoveAllNormalTabsAction) - assertEquals(1, state.tabs.size) - assertEquals( - 0, - state.tabPartitions[tabPartition.id]?.getGroupById(normalTabGroup.id)?.tabIds?.size, - ) - assertEquals( - 1, - state.tabPartitions[tabPartition.id]?.getGroupById(privateTabGroup.id)?.tabIds?.size, - ) - } - @Test fun `AddMultipleTabsAction - Adds multiple tabs and updates selection`() { var state = BrowserState() diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/state/TabPartitionTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/state/TabPartitionTest.kt deleted file mode 100644 index 4e4980b18430..000000000000 --- a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/state/TabPartitionTest.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.browser.state.state - -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Test - -class TabPartitionTest { - - @Test - fun `GIVEN a null tab partition THEN tab partition is empty`() { - val tabPartition: TabPartition? = null - - assertTrue(tabPartition.isEmpty()) - assertFalse(tabPartition.isNotEmpty()) - } - - @Test - fun `GIVEN a tab partition with no tab group THEN tab partition is empty`() { - val tabPartition = TabPartition("test") - - assertTrue(tabPartition.isEmpty()) - assertFalse(tabPartition.isNotEmpty()) - } - - @Test - fun `GIVEN a tab partition with empty tab groups THEN tab partition is empty`() { - val tabPartition = TabPartition("test", listOf(TabGroup(), TabGroup())) - - assertTrue(tabPartition.isEmpty()) - assertFalse(tabPartition.isNotEmpty()) - } - - @Test - fun `GIVEN a tab partition with non-empty tab group THEN tab partition is not empty`() { - val tabPartition = TabPartition("test", listOf(TabGroup("test", "test", setOf("tab1")))) - - assertTrue(tabPartition.isNotEmpty()) - assertFalse(tabPartition.isEmpty()) - } - - @Test - fun `GIVEN a tab partition with non-empty tab group THEN get group by name will return the group`() { - val tabPartition = TabPartition("test", listOf(TabGroup("test id", "abc", setOf("tab1", "tab2")))) - - assertTrue(tabPartition.getGroupByName("abc") != null) - assertEquals(setOf("tab1", "tab2"), tabPartition.getGroupByName("abc")?.tabIds) - assertTrue(tabPartition.isNotEmpty()) - assertFalse(tabPartition.isEmpty()) - } - - @Test - fun `GIVEN a tab partition with non-empty tab group THEN get group by ID will return the group`() { - val tabPartition = TabPartition("test", listOf(TabGroup("test id", "abc", setOf("tab1", "tab2")))) - - assertTrue(tabPartition.getGroupById("test id") != null) - assertEquals(setOf("tab1", "tab2"), tabPartition.getGroupById("test id")?.tabIds) - assertTrue(tabPartition.isNotEmpty()) - assertFalse(tabPartition.isEmpty()) - } -} diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/store/BrowserStoreExceptionTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/store/BrowserStoreExceptionTest.kt index 768d2efb2f78..29687d1fe7d4 100644 --- a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/store/BrowserStoreExceptionTest.kt +++ b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/store/BrowserStoreExceptionTest.kt @@ -5,11 +5,8 @@ package mozilla.components.browser.state.store import androidx.test.ext.junit.runners.AndroidJUnit4 -import mozilla.components.browser.state.action.TabGroupAction import mozilla.components.browser.state.action.TabListAction import mozilla.components.browser.state.state.BrowserState -import mozilla.components.browser.state.state.TabGroup -import mozilla.components.browser.state.state.TabPartition import mozilla.components.browser.state.state.createTab import mozilla.components.browser.state.state.recover.toRecoverableTab import org.junit.Test @@ -80,79 +77,4 @@ class BrowserStoreExceptionTest { store.dispatch(TabListAction.AddMultipleTabsAction(tabs = listOf(tab1, tab2))) } - - @Test(expected = IllegalArgumentException::class) - fun `AddTabGroupAction - Exception is thrown when group already exists`() { - val partitionId = "testFeaturePartition" - val testGroup = TabGroup("test") - val store = - BrowserStore( - BrowserState( - tabPartitions = - mapOf( - partitionId to - TabPartition( - partitionId, - tabGroups = listOf(testGroup), - ) - ) - ) - ) - - store.dispatch( - TabGroupAction.AddTabGroupAction( - partition = partitionId, - group = testGroup, - ) - ) - } - - @Test(expected = IllegalArgumentException::class) - fun `AddTabGroupAction - Asserts that tabs exist`() { - val store = BrowserStore() - - val partition = "testFeaturePartition" - val testGroup = TabGroup("test", tabIds = setOf("invalid")) - store.dispatch( - TabGroupAction.AddTabGroupAction( - partition = partition, - group = testGroup, - ) - ) - } - - @Test(expected = IllegalArgumentException::class) - fun `AddTabAction - Asserts that tab exists when adding to group`() { - val tabGroup = TabGroup("test1", tabIds = emptySet()) - val tabPartition = TabPartition("testFeaturePartition", tabGroups = listOf(tabGroup)) - - val store = - BrowserStore( - BrowserState( - tabs = listOf(), - tabPartitions = mapOf("testFeaturePartition" to tabPartition), - ) - ) - - val tab = createTab(id = "tab1", url = "https://firefox.com") - store.dispatch(TabGroupAction.AddTabAction(tabPartition.id, tabGroup.id, tab.id)) - } - - @Test(expected = IllegalArgumentException::class) - fun `AddTabsAction - Asserts that tabs exist when adding to group`() { - val tabGroup = TabGroup("test1", tabIds = emptySet()) - val tabPartition = TabPartition("testFeaturePartition", tabGroups = listOf(tabGroup)) - val tab1 = createTab(id = "tab1", url = "https://firefox.com") - val tab2 = createTab(id = "tab2", url = "https://mozilla.org") - - val store = - BrowserStore( - BrowserState( - tabs = listOf(tab1), - tabPartitions = mapOf("testFeaturePartition" to tabPartition), - ) - ) - - store.dispatch(TabGroupAction.AddTabsAction(tabPartition.id, tabGroup.id, setOf(tab1.id, tab2.id))) - } } diff --git a/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabsAdapter.kt b/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabsAdapter.kt index 5224da6d69ee..3aebc94bfd41 100644 --- a/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabsAdapter.kt +++ b/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabsAdapter.kt @@ -8,7 +8,6 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter -import mozilla.components.browser.state.state.TabPartition import mozilla.components.browser.state.state.TabSessionState import mozilla.components.concept.base.images.ImageLoader @@ -69,7 +68,7 @@ open class TabsAdapter( } } - override fun updateTabs(tabs: List, tabPartition: TabPartition?, selectedTabId: String?) { + override fun updateTabs(tabs: List, selectedTabId: String?) { this.selectedTabId = selectedTabId submitList(tabs) diff --git a/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabsTray.kt b/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabsTray.kt index 6e40a1ce0947..d2cfc2c7c1be 100644 --- a/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabsTray.kt +++ b/mobile/android/android-components/components/browser/tabstray/src/main/java/mozilla/components/browser/tabstray/TabsTray.kt @@ -4,7 +4,6 @@ package mozilla.components.browser.tabstray -import mozilla.components.browser.state.state.TabPartition import mozilla.components.browser.state.state.TabSessionState /** An interface to display a list of tabs. */ @@ -21,5 +20,5 @@ interface TabsTray { } /** Called when the list of tabs are updated. */ - fun updateTabs(tabs: List, tabPartition: TabPartition?, selectedTabId: String?) + fun updateTabs(tabs: List, selectedTabId: String?) } diff --git a/mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/TabsAdapterTest.kt b/mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/TabsAdapterTest.kt index 2b9203b215e6..b18e83e0c1c3 100644 --- a/mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/TabsAdapterTest.kt +++ b/mobile/android/android-components/components/browser/tabstray/src/test/java/mozilla/components/browser/tabstray/TabsAdapterTest.kt @@ -72,7 +72,6 @@ class TabsAdapterTest { createTab(id = "A", url = "https://www.mozilla.org"), createTab(id = "B", url = "https://www.firefox.com"), ), - tabPartition = null, selectedTabId = "A", ) assertEquals(2, adapter.itemCount) @@ -90,8 +89,7 @@ class TabsAdapterTest { adapter.updateTabs( listOf(tab), - null, - "A", + tab.id, ) adapter.onBindViewHolder(holder, 0) @@ -105,7 +103,7 @@ class TabsAdapterTest { val holder = spy(TestTabViewHolder(View(testContext))) val tab = createTab(id = "A", url = "https://www.mozilla.org") - adapter.updateTabs(listOf(mock(), tab), tabPartition = null, selectedTabId = "A") + adapter.updateTabs(listOf(mock(), tab), selectedTabId = "A") adapter.onBindViewHolder(holder, 0, listOf(PAYLOAD_HIGHLIGHT_SELECTED_ITEM)) verify(holder, never()).updateSelectedTabIndicator(ArgumentMatchers.anyBoolean()) @@ -119,7 +117,7 @@ class TabsAdapterTest { val adapter = TabsAdapter(delegate = mock()) val holder = spy(TestTabViewHolder(View(testContext))) val tab = createTab(id = "A", url = "https://www.mozilla.org") - adapter.updateTabs(listOf(mock(), tab), tabPartition = null, selectedTabId = "A") + adapter.updateTabs(listOf(mock(), tab), selectedTabId = "A") adapter.onBindViewHolder(holder, 0, listOf(PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM)) verify(holder, never()).updateSelectedTabIndicator(ArgumentMatchers.anyBoolean()) @@ -139,7 +137,7 @@ class TabsAdapterTest { verify(payloads, never()).isEmpty() verify(payloads, never()).contains(ArgumentMatchers.anyInt()) - adapter.updateTabs(emptyList(), tabPartition = null, selectedTabId = null) + adapter.updateTabs(emptyList(), selectedTabId = null) adapter.onBindViewHolder(holder, 0, payloads) // verify that calls we expect further down are not happening after the null check verify(payloads, never()).isEmpty() @@ -152,7 +150,7 @@ class TabsAdapterTest { val holder = TestTabViewHolder(View(testContext)) val emptyPayloads = spy(arrayListOf()) - adapter.updateTabs(listOf(mock()), tabPartition = null, selectedTabId = null) + adapter.updateTabs(listOf(mock()), selectedTabId = null) adapter.onBindViewHolder(holder, 0, emptyPayloads) diff --git a/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/middleware/undo/UndoMiddleware.kt b/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/middleware/undo/UndoMiddleware.kt index 196dfce06261..5c5b0eb53408 100644 --- a/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/middleware/undo/UndoMiddleware.kt +++ b/mobile/android/android-components/components/feature/session/src/main/java/mozilla/components/feature/session/middleware/undo/UndoMiddleware.kt @@ -133,8 +133,7 @@ class UndoMiddleware( val undoHistory = state.undoHistory val tabs = undoHistory.tabs - val tabPartitions = undoHistory.tabPartitions - if (tabs.isEmpty() && tabPartitions.isEmpty()) { + if (tabs.isEmpty()) { logger.debug("No recoverable tabs or tab partitions for undo.") return@launch } @@ -143,7 +142,6 @@ class UndoMiddleware( TabListAction.RestoreAction( tabs = tabs, restoreLocation = TabListAction.RestoreAction.RestoreLocation.AT_INDEX, - tabPartitions = tabPartitions, ) ) diff --git a/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/TabPartitionKeys.kt b/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/TabPartitionKeys.kt deleted file mode 100644 index 14ddd8d9af6d..000000000000 --- a/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/TabPartitionKeys.kt +++ /dev/null @@ -1,10 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package mozilla.components.feature.tabs - -/** Keys used to identify tab partitions. */ -internal object TabPartitionKeys { - const val TAB_GROUPS = "TAB_GROUPS" -} diff --git a/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/TabsUseCases.kt b/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/TabsUseCases.kt index 4829f844ef8a..2087319cc8b8 100644 --- a/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/TabsUseCases.kt +++ b/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/TabsUseCases.kt @@ -12,7 +12,6 @@ import mozilla.components.browser.session.storage.SessionStorage import mozilla.components.browser.state.action.ContentAction import mozilla.components.browser.state.action.EngineAction import mozilla.components.browser.state.action.RestoreCompleteAction -import mozilla.components.browser.state.action.TabGroupAction import mozilla.components.browser.state.action.TabListAction import mozilla.components.browser.state.action.TabListAction.RestoreAction.RestoreLocation import mozilla.components.browser.state.action.TranslationsAction @@ -22,8 +21,6 @@ import mozilla.components.browser.state.selector.findNormalOrPrivateTabByUrlIgno import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.selector.selectedTab import mozilla.components.browser.state.state.SessionState -import mozilla.components.browser.state.state.TabGroup -import mozilla.components.browser.state.state.TabPartition import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.state.createTab import mozilla.components.browser.state.state.recover.RecoverableTab @@ -277,20 +274,17 @@ class TabsUseCases( * @param tabs The list of tabs to restore. * @param selectTabId The ID of the selected tab in [tabs]. Or `null` if no selection was restored. * @param restoreLocation [RestoreLocation] indicating where to restore [tabs]. - * @param tabPartitions a mapping of IDs to the corresponding [TabPartition]. */ operator fun invoke( tabs: List, selectTabId: String? = null, restoreLocation: RestoreLocation = RestoreLocation.END, - tabPartitions: Map = emptyMap(), ) { store.dispatch( TabListAction.RestoreAction( tabs = tabs, selectedTabId = selectTabId, restoreLocation = restoreLocation, - tabPartitions = tabPartitions, ) ) } @@ -310,7 +304,6 @@ class TabsUseCases( } invoke( tabs = state.tabs, - tabPartitions = state.tabPartitions, selectTabId = state.selectedTabId, restoreLocation = restoreLocation, ) @@ -563,146 +556,6 @@ class TabsUseCases( } } - /** Use case for adding a tab group to a tab partition. */ - class AddTabGroupUseCase(private val store: BrowserStore) { - /** - * Adds a new tab group. If the corresponding partition doesn't exist it will be created. - * - * @property group The [TabGroup] to add. - */ - operator fun invoke(group: TabGroup) { - store.dispatch( - TabGroupAction.AddTabGroupAction( - partition = TabPartitionKeys.TAB_GROUPS, - group = group, - ) - ) - } - } - - /** Use case for closing a tab group and its associated tabs in a tab partition. */ - class CloseTabGroupUseCase(private val store: BrowserStore) { - /** - * Removes a tab group and provided list of tabs. - * - * @property group The [TabGroup] to remove. - * @property tabIds The IDs of the tabs to remove. - */ - operator fun invoke( - group: String, - tabIds: List, - ) { - store.dispatch(TabListAction.RemoveTabsAction(tabIds = tabIds)) - store.dispatch( - TabGroupAction.RemoveTabGroupAction( - partition = TabPartitionKeys.TAB_GROUPS, - group = group, - ) - ) - } - } - - /** Use case for removing a tab group in a tab partition. This will ungroup the tabs in the group. */ - class RemoveTabGroupUseCase(private val store: BrowserStore) { - /** - * Removes a tab group in a tab partition. - * - * @property group The [TabGroup] to remove. - */ - operator fun invoke(group: String) { - store.dispatch( - TabGroupAction.RemoveTabGroupAction( - partition = TabPartitionKeys.TAB_GROUPS, - group = group, - ) - ) - } - } - - /** Use case for adding tabs to a group. */ - class AddTabsInGroupUseCase(private val store: BrowserStore) { - /** - * Adds the provided tab to a group. - * - * @property group The ID of the group. - * @property tabId The ID of the tab to add to the group. If the corresponding tab is already in the group, it - * won't be added again. - */ - operator fun invoke( - group: String, - tabId: String, - ) { - store.dispatch( - TabGroupAction.AddTabAction( - partition = TabPartitionKeys.TAB_GROUPS, - group = group, - tabId = tabId, - ) - ) - } - - /** - * Adds the provided tabs to a group. - * - * @property group The ID of the group. - * @property tabIds The IDs of the tabs to add to the group. If a tab is already in the group, it won't be added - * again. - */ - operator fun invoke( - group: String, - tabIds: Set, - ) { - store.dispatch( - TabGroupAction.AddTabsAction( - partition = TabPartitionKeys.TAB_GROUPS, - group = group, - tabIds = tabIds, - ) - ) - } - } - - /** Use case for removing tabs from a group. */ - class RemoveTabsInGroupUseCase(private val store: BrowserStore) { - /** - * Removes the provided tab from a group. - * - * @property group The ID of the group. - * @property tabId The ID of the tab to remove from the group. - */ - operator fun invoke( - group: String, - tabId: String, - ) { - store.dispatch( - TabGroupAction.RemoveTabAction( - partition = TabPartitionKeys.TAB_GROUPS, - group = group, - tabId = tabId, - ) - ) - } - - /** - * Removes the provided tabs from a group. - * - * @property group The ID of the group. - * @property tabIds The IDs of the tabs to remove from the group. - */ - operator fun invoke( - group: String, - tabIds: Set, - ) { - store.dispatch( - TabGroupAction.RemoveTabsAction( - partition = TabPartitionKeys.TAB_GROUPS, - group = group, - tabIds = tabIds, - ) - ) - } - } - val selectTab: SelectTabUseCase by lazy { DefaultSelectTabUseCase(store) } val removeTab: RemoveTabUseCase by lazy { DefaultRemoveTabUseCase(store) } val addTab: AddNewTabUseCase by lazy { AddNewTabUseCase(store) } @@ -723,9 +576,4 @@ class TabsUseCases( val duplicateTab: DuplicateTabUseCase by lazy { DuplicateTabUseCase(store) } val moveTabs: MoveTabsUseCase by lazy { MoveTabsUseCase(store) } val migratePrivateTabUseCase: MigratePrivateTabUseCase by lazy { MigratePrivateTabUseCase(store) } - val addTabGroup: AddTabGroupUseCase by lazy { AddTabGroupUseCase(store) } - val closeTabGroup: CloseTabGroupUseCase by lazy { CloseTabGroupUseCase(store) } - val removeTabGroup: RemoveTabGroupUseCase by lazy { RemoveTabGroupUseCase(store) } - val addTabsInGroup: AddTabsInGroupUseCase by lazy { AddTabsInGroupUseCase(store) } - val removeTabsInGroup: RemoveTabsInGroupUseCase by lazy { RemoveTabsInGroupUseCase(store) } } diff --git a/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/ext/BrowserState.kt b/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/ext/BrowserState.kt index 635987b99a9e..621e3242a10d 100644 --- a/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/ext/BrowserState.kt +++ b/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/ext/BrowserState.kt @@ -5,9 +5,7 @@ package mozilla.components.feature.tabs.ext import mozilla.components.browser.state.state.BrowserState -import mozilla.components.browser.state.state.TabPartition import mozilla.components.browser.state.state.TabSessionState -import mozilla.components.feature.tabs.TabPartitionKeys.TAB_GROUPS import mozilla.components.feature.tabs.tabstray.Tabs /** @@ -31,6 +29,3 @@ internal fun BrowserState.toTabList( return Pair(tabStates, selectedTabId) } - -/** Returns the [TabPartition] associated with [TAB_GROUPS]. */ -fun BrowserState.tabGroupsPartition(): TabPartition? = this.tabPartitions[TAB_GROUPS] diff --git a/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/tabstray/TabsFeature.kt b/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/tabstray/TabsFeature.kt index 5a3c7044d5fd..1355ae7417c5 100644 --- a/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/tabstray/TabsFeature.kt +++ b/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/tabstray/TabsFeature.kt @@ -5,7 +5,6 @@ package mozilla.components.feature.tabs.tabstray import androidx.annotation.VisibleForTesting -import mozilla.components.browser.state.state.TabPartition import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.tabstray.TabsTray @@ -16,14 +15,12 @@ import mozilla.components.support.base.feature.LifecycleAwareFeature * Feature implementation for connecting a tabs tray implementation with the session module. * * @param defaultTabsFilter A tab filter that is used for the initial presenting of tabs. - * @param defaultTabPartitionsFilter A tab partition filter that is used for the initial presenting of tabs. * @param onCloseTray a callback invoked when the last tab is closed. */ class TabsFeature( private val tabsTray: TabsTray, private val store: BrowserStore, private val onCloseTray: () -> Unit = {}, - private val defaultTabPartitionsFilter: (Map) -> TabPartition? = { null }, private val defaultTabsFilter: (TabSessionState) -> Boolean = { true }, ) : LifecycleAwareFeature { @VisibleForTesting @@ -32,7 +29,6 @@ class TabsFeature( tabsTray, store, defaultTabsFilter, - defaultTabPartitionsFilter, closeTabsTray = onCloseTray, ) @@ -56,6 +52,6 @@ class TabsFeature( val state = store.state val (tabs, selectedTabId) = state.toTabList(tabsFilter) - tabsTray.updateTabs(tabs, null, selectedTabId) + tabsTray.updateTabs(tabs, selectedTabId) } } diff --git a/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/tabstray/TabsTrayPresenter.kt b/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/tabstray/TabsTrayPresenter.kt index 442308fb9dc5..004aeb7fce70 100644 --- a/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/tabstray/TabsTrayPresenter.kt +++ b/mobile/android/android-components/components/feature/tabs/src/main/java/mozilla/components/feature/tabs/tabstray/TabsTrayPresenter.kt @@ -11,7 +11,6 @@ import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChangedBy import mozilla.components.browser.state.state.BrowserState -import mozilla.components.browser.state.state.TabPartition import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.tabstray.TabsTray @@ -27,7 +26,6 @@ class TabsTrayPresenter( private val tabsTray: TabsTray, private val store: BrowserStore, internal var tabsFilter: (TabSessionState) -> Boolean, - internal var tabPartitionsFilter: (Map) -> TabPartition?, private val mainDispatcher: CoroutineDispatcher = Dispatchers.Main, private val closeTabsTray: () -> Unit, ) { @@ -44,7 +42,7 @@ class TabsTrayPresenter( private suspend fun collect(flow: Flow) { flow - .distinctUntilChangedBy { Pair(it.toTabs(tabsFilter), tabPartitionsFilter(it.tabPartitions)) } + .distinctUntilChangedBy { it.toTabs(tabsFilter) } .collect { state -> val (tabs, selectedTabId) = state.toTabList(tabsFilter) // Do not invoke the callback on start if this is the initial state. @@ -52,7 +50,7 @@ class TabsTrayPresenter( closeTabsTray.invoke() } - tabsTray.updateTabs(tabs, tabPartitionsFilter(state.tabPartitions), selectedTabId) + tabsTray.updateTabs(tabs, selectedTabId) initialOpen = false } diff --git a/mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/TabsUseCasesTest.kt b/mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/TabsUseCasesTest.kt index b8e0aa0ccc13..b1721b1be40f 100644 --- a/mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/TabsUseCasesTest.kt +++ b/mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/TabsUseCasesTest.kt @@ -17,11 +17,8 @@ import mozilla.components.browser.state.engine.EngineMiddleware import mozilla.components.browser.state.selector.findNormalOrPrivateTabByUrl import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.selector.selectedTab -import mozilla.components.browser.state.state.TabGroup -import mozilla.components.browser.state.state.TabPartition import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.state.createTab -import mozilla.components.browser.state.state.getGroupById import mozilla.components.browser.state.state.recover.RecoverableTab import mozilla.components.browser.state.state.recover.toRecoverableTab import mozilla.components.browser.state.store.BrowserStore @@ -30,7 +27,6 @@ import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.engine.EngineSession.LoadUrlFlags import mozilla.components.concept.engine.EngineSessionState import mozilla.components.concept.storage.HistoryMetadataKey -import mozilla.components.feature.tabs.ext.tabGroupsPartition import mozilla.components.support.test.any import mozilla.components.support.test.argumentCaptor import mozilla.components.support.test.mock @@ -406,7 +402,6 @@ class TabsUseCasesTest { RecoverableBrowserState( tabs = restoredTabs.map { it.toRecoverableTab() }, selectedTabId = null, - tabPartitions = emptyMap(), ) val sessionStorage: SessionStorage = mock() whenever(sessionStorage.restore(any())).thenReturn(recoverableBrowserState) @@ -423,21 +418,18 @@ class TabsUseCasesTest { } @Test - fun `GIVEN a recoverable browser state with tabs and partitions in storage WHEN browsing session is restored THEN restore the tabs and partition from storage`() = + fun `GIVEN a recoverable browser state with tabs in storage WHEN browsing session is restored THEN restore the tabs from storage`() = runTest(testDispatcher) { val restoredTabs = listOf( createTab(id = "tab1", url = "https://mozilla.org"), createTab(id = "tab2", url = "https://firefox.com"), ) - val tabGroup = TabGroup("group1", tabIds = setOf("tab1")) - val tabPartition = TabPartition("testFeaturePartition", tabGroups = listOf(tabGroup)) - val restoredTabPartitions = mapOf("testFeaturePartition" to tabPartition) + val recoverableBrowserState = RecoverableBrowserState( tabs = restoredTabs.map { it.toRecoverableTab() }, selectedTabId = null, - tabPartitions = restoredTabPartitions, ) val sessionStorage: SessionStorage = mock() whenever(sessionStorage.restore(any())).thenReturn(recoverableBrowserState) @@ -449,7 +441,6 @@ class TabsUseCasesTest { assertEquals(restoredTab.id, store.state.tabs[index].id) assertEquals(restoredTab.content.url, store.state.tabs[index].content.url) } - assertEquals(restoredTabPartitions, store.state.tabPartitions) } @Test @@ -460,7 +451,6 @@ class TabsUseCasesTest { RecoverableBrowserState( tabs = restoredTabs.map { it.toRecoverableTab() }, selectedTabId = null, - tabPartitions = emptyMap(), isTranslationsEngineSupported = true, ) val sessionStorage: SessionStorage = mock() @@ -479,7 +469,6 @@ class TabsUseCasesTest { RecoverableBrowserState( tabs = restoredTabs.map { it.toRecoverableTab() }, selectedTabId = null, - tabPartitions = emptyMap(), isTranslationsEngineSupported = null, ) val sessionStorage: SessionStorage = mock() @@ -765,94 +754,4 @@ class TabsUseCasesTest { tabsUseCases.migratePrivateTabUseCase("invalid-tab-id") } } - - @Test - fun `WHEN AddTabGroupUseCase is invoked THEN group is added to the tab groups partition`() { - val tab = createTab("https://mozilla.org") - store.dispatch(TabListAction.AddTabAction(tab)) - - val group = TabGroup(id = "group1", name = "Group 1", tabIds = setOf(tab.id)) - tabsUseCases.addTabGroup(group = group) - - val partition = store.state.tabGroupsPartition() - assertNotNull(partition) - assertEquals(TabPartitionKeys.TAB_GROUPS, partition.id) - assertEquals(1, partition.tabGroups.size) - assertEquals(group, partition.getGroupById("group1")) - } - - @Test - fun `WHEN CloseTabGroupUseCase is invoked THEN group and tabs are removed from the tab groups partition`() { - val tab = createTab("https://mozilla.org") - store.dispatch(TabListAction.AddTabAction(tab)) - - val group = TabGroup(id = "group1", name = "Group 1", tabIds = setOf(tab.id)) - tabsUseCases.addTabGroup(group = group) - - assertEquals(1, store.state.tabs.size) - assertEquals(1, store.state.tabGroupsPartition()?.tabGroups?.size) - assertEquals(group, store.state.tabGroupsPartition()?.tabGroups?.first()) - - tabsUseCases.closeTabGroup( - group = "group1", - tabIds = listOf(tab.id), - ) - - assertEquals(0, store.state.tabs.size) - assertNull(store.state.tabGroupsPartition()) - } - - @Test - fun `WHEN RemoveTabGroupUseCase is invoked THEN group is removed from the tab groups partition`() { - val tab = createTab("https://mozilla.org") - store.dispatch(TabListAction.AddTabAction(tab)) - - val group = TabGroup(id = "group1", name = "Group 1", tabIds = setOf(tab.id)) - tabsUseCases.addTabGroup(group = group) - - assertEquals(1, store.state.tabs.size) - assertEquals(1, store.state.tabGroupsPartition()?.tabGroups?.size) - assertEquals(group, store.state.tabGroupsPartition()?.tabGroups?.first()) - - tabsUseCases.removeTabGroup(group = "group1") - - assertEquals(1, store.state.tabs.size) - assertEquals(tab, store.state.tabs.first()) - assertNull(store.state.tabGroupsPartition()) - } - - @Test - fun `WHEN AddTabsInGroupUseCase is invoked THEN tabs are added to group in the tab groups partition`() { - val tab1 = createTab("https://mozilla.org") - val tab2 = createTab("https://firefox.com") - store.dispatch(TabListAction.AddMultipleTabsAction(tabs = listOf(tab1, tab2))) - - tabsUseCases.addTabsInGroup(group = "group1", tabId = tab1.id) - var group = store.state.tabGroupsPartition()?.getGroupById("group1") - assertNotNull(group) - assertEquals(setOf(tab1.id), group.tabIds) - - tabsUseCases.addTabsInGroup(group = "group1", tabIds = setOf(tab2.id)) - group = store.state.tabGroupsPartition()?.getGroupById("group1") - assertEquals(setOf(tab1.id, tab2.id), group?.tabIds) - } - - @Test - fun `WHEN RemoveTabsInGroupUseCase is invoked THEN tabs are removed from group in the tab groups partition`() { - val tab1 = createTab("https://mozilla.org") - val tab2 = createTab("https://firefox.com") - store.dispatch(TabListAction.AddMultipleTabsAction(tabs = listOf(tab1, tab2))) - tabsUseCases.addTabsInGroup( - group = "group1", - tabIds = setOf(tab1.id, tab2.id), - ) - - tabsUseCases.removeTabsInGroup(group = "group1", tabId = tab1.id) - var group = store.state.tabGroupsPartition()?.getGroupById("group1") - assertEquals(setOf(tab2.id), group?.tabIds) - - tabsUseCases.removeTabsInGroup(group = "group1", tabIds = setOf(tab2.id)) - group = store.state.tabGroupsPartition()?.getGroupById("group1") - assertTrue(group?.tabIds?.isEmpty() == true) - } } diff --git a/mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/tabstray/TabsFeatureTest.kt b/mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/tabstray/TabsFeatureTest.kt index 2df071150ab7..f7498365d5c2 100644 --- a/mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/tabstray/TabsFeatureTest.kt +++ b/mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/tabstray/TabsFeatureTest.kt @@ -101,7 +101,7 @@ class TabsFeatureTest { tabsFeature.filterTabs(filter) verify(presenter).tabsFilter = filter - verify(tabsTray).updateTabs(emptyList(), null, null) + verify(tabsTray).updateTabs(emptyList(), null) } @Test diff --git a/mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/tabstray/TabsTrayPresenterTest.kt b/mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/tabstray/TabsTrayPresenterTest.kt index 9ee71d1c56b7..25f2866cf77b 100644 --- a/mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/tabstray/TabsTrayPresenterTest.kt +++ b/mobile/android/android-components/components/feature/tabs/src/test/java/mozilla/components/feature/tabs/tabstray/TabsTrayPresenterTest.kt @@ -9,7 +9,6 @@ import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.runTest import mozilla.components.browser.state.action.TabListAction import mozilla.components.browser.state.state.BrowserState -import mozilla.components.browser.state.state.TabPartition import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.state.createTab import mozilla.components.browser.state.store.BrowserStore @@ -47,7 +46,6 @@ class TabsTrayPresenterTest { tabsTray, store, closeTabsTray = {}, - tabPartitionsFilter = { null }, tabsFilter = { true }, mainDispatcher = testDispatcher, ) @@ -89,7 +87,6 @@ class TabsTrayPresenterTest { tabsTray, store, closeTabsTray = {}, - tabPartitionsFilter = { null }, tabsFilter = { true }, mainDispatcher = testDispatcher, ) @@ -130,7 +127,6 @@ class TabsTrayPresenterTest { tabsTray, store, closeTabsTray = {}, - tabPartitionsFilter = { null }, tabsFilter = { true }, mainDispatcher = testDispatcher, ) @@ -175,7 +171,6 @@ class TabsTrayPresenterTest { tabsTray, store, closeTabsTray = {}, - tabPartitionsFilter = { null }, tabsFilter = { true }, mainDispatcher = testDispatcher, ) @@ -218,7 +213,6 @@ class TabsTrayPresenterTest { tabsTray, store, closeTabsTray = {}, - tabPartitionsFilter = { null }, tabsFilter = { true }, mainDispatcher = testDispatcher, ) @@ -257,7 +251,6 @@ class TabsTrayPresenterTest { tabsTray, store, closeTabsTray = {}, - tabPartitionsFilter = { null }, tabsFilter = { it.content.private }, mainDispatcher = testDispatcher, ) @@ -293,7 +286,6 @@ class TabsTrayPresenterTest { TabsTrayPresenter( tabsTray, store, - tabPartitionsFilter = { null }, tabsFilter = { true }, closeTabsTray = { closed = true }, mainDispatcher = testDispatcher, @@ -334,7 +326,6 @@ class TabsTrayPresenterTest { TabsTrayPresenter( tabsTray, store, - tabPartitionsFilter = { null }, tabsFilter = { true }, closeTabsTray = { closed = true }, mainDispatcher = testDispatcher, @@ -375,7 +366,6 @@ class TabsTrayPresenterTest { TabsTrayPresenter( tabsTray, store, - tabPartitionsFilter = { null }, tabsFilter = { it.content.private }, closeTabsTray = { invoked = true }, mainDispatcher = testDispatcher, @@ -392,7 +382,7 @@ private class MockedTabsTray : TabsTray { var updateTabs: List? = null var selectedTabId: String? = null - override fun updateTabs(tabs: List, tabPartition: TabPartition?, selectedTabId: String?) { + override fun updateTabs(tabs: List, selectedTabId: String?) { updateTabs = tabs this.selectedTabId = selectedTabId } diff --git a/mobile/android/android-components/docs/changelog.md b/mobile/android/android-components/docs/changelog.md index c818f4e0371c..4777e772d520 100644 --- a/mobile/android/android-components/docs/changelog.md +++ b/mobile/android/android-components/docs/changelog.md @@ -5,6 +5,8 @@ permalink: /changelog/ --- # 155.0 (In Development) +* **browser-state** + * ⚠️ **Breaking change**: Removed `TabPartition` and `TabGroup` from `BrowserState`. # 154.0 * **browser-icons** diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt index 91140f13cfcc..7f14840ed3bf 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt @@ -44,7 +44,6 @@ import mozilla.components.lib.shake.detectShakes import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import mozilla.components.support.ktx.kotlin.isContentUrl -import mozilla.components.support.utils.DefaultDateTimeProvider import org.mozilla.fenix.GleanMetrics.Translations import org.mozilla.fenix.R import org.mozilla.fenix.browser.store.BrowserScreenAction.ReaderModeStatusUpdated @@ -72,7 +71,6 @@ import org.mozilla.fenix.ext.navigateSafe import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.runIfFragmentIsAttached import org.mozilla.fenix.home.HomeFragment -import org.mozilla.fenix.ipprotection.store.IPProtectionOnboardingPrompt import org.mozilla.fenix.ipprotection.store.Surface as IPProtectionSurface import org.mozilla.fenix.nimbus.FxNimbus import org.mozilla.fenix.onboarding.OnboardingFragmentDirections @@ -91,7 +89,6 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler, SystemIns private val openInAppOnboardingObserver = ViewBoundFeatureWrapper() private val translationsBinding = ViewBoundFeatureWrapper() private val translationsBannerIntegration = ViewBoundFeatureWrapper() - private val ipProtectionOnboardingPrompt = ViewBoundFeatureWrapper() private val continuousOnboardingFeature = ViewBoundFeatureWrapper() private var qrScanFenixFeature: ViewBoundFeatureWrapper? = ViewBoundFeatureWrapper() @@ -161,7 +158,6 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler, SystemIns initBrowserToolbarComposableUpdates(view) initTranslationsUpdates(context = context, rootView = view) - initIPProtectionOnboarding(context, view) initContinuousOnboardingFeature() thumbnailsFeature.set( @@ -322,25 +318,6 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler, SystemIns } } - private fun initIPProtectionOnboarding(context: Context, rootView: View) { - ipProtectionOnboardingPrompt.set( - feature = - IPProtectionOnboardingPrompt( - repository = context.components.ipProtectionPromptRepository, - timeProvider = DefaultDateTimeProvider(), - store = context.components.ipProtection.store, - onShowOnboarding = { - findNavController() - .navigate( - BrowserFragmentDirections.actionGlobalIpProtectionDialog(IPProtectionSurface.BROWSER) - ) - }, - ), - owner = this, - view = rootView, - ) - } - private fun initContinuousOnboardingFeature() { ContinuousOnboardingFeature.register( fragment = this, @@ -357,6 +334,10 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler, SystemIns ), ) }, + navigateToIpProtection = { + findNavController() + .navigate(BrowserFragmentDirections.actionGlobalIpProtectionDialog(IPProtectionSurface.BROWSER)) + }, ) } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/usecases/FenixBrowserUseCases.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/usecases/FenixBrowserUseCases.kt index bd1f7dbed08d..afff0fc61a85 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/usecases/FenixBrowserUseCases.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/usecases/FenixBrowserUseCases.kt @@ -139,19 +139,6 @@ class FenixBrowserUseCases( ) } - /** - * Adds a new homepage ("about:home") tab to the provided tab group. - * - * @param group The ID of the group. - */ - fun addNewHomepageTabInGroup(group: String) { - val tabId = addNewHomepageTab() - tabsUseCases.addTabsInGroup( - group = group, - tabId = tabId, - ) - } - /** Loads the homepage ("about:home"). */ fun navigateToHomepage() { loadUrlUseCase.invoke(url = ABOUT_HOME_URL) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt index 44ce58f61b89..0b31a0fd3823 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt @@ -151,7 +151,6 @@ import org.mozilla.fenix.home.topsites.getTopSitesConfig import org.mozilla.fenix.home.ui.HomeSwipeIntegration import org.mozilla.fenix.home.ui.Homepage import org.mozilla.fenix.home.ui.WallpaperBackground -import org.mozilla.fenix.ipprotection.store.IPProtectionOnboardingPrompt import org.mozilla.fenix.ipprotection.store.Surface as IPProtectionSurface import org.mozilla.fenix.messaging.DefaultMessageController import org.mozilla.fenix.messaging.FenixMessageSurfaceId @@ -268,7 +267,6 @@ class HomeFragment : Fragment() { private val topSitesBinding = ViewBoundFeatureWrapper() private val trackersBlockedFeature = ViewBoundFeatureWrapper() private val ipProtectionWarningBinding = ViewBoundFeatureWrapper() - private val ipProtectionOnboardingPrompt = ViewBoundFeatureWrapper() private val continuousOnboardingFeature = ViewBoundFeatureWrapper() private val homepageEdgeToEdgeFeature = ViewBoundFeatureWrapper() @@ -1373,23 +1371,6 @@ class HomeFragment : Fragment() { owner = this, view = view, ) - - ipProtectionOnboardingPrompt.set( - feature = - IPProtectionOnboardingPrompt( - repository = requireComponents.ipProtectionPromptRepository, - timeProvider = DefaultDateTimeProvider(), - store = requireComponents.ipProtection.store, - onShowOnboarding = { - findNavController() - .navigate( - HomeFragmentDirections.actionGlobalIpProtectionDialog(IPProtectionSurface.HOMEPAGE) - ) - }, - ), - owner = this, - view = view, - ) } private fun initContinuousOnboardingFeature() { @@ -1408,6 +1389,10 @@ class HomeFragment : Fragment() { ), ) }, + navigateToIpProtection = { + findNavController() + .navigate(HomeFragmentDirections.actionGlobalIpProtectionDialog(IPProtectionSurface.HOMEPAGE)) + }, ) } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/continuous/ContinuousOnboardingFeature.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/continuous/ContinuousOnboardingFeature.kt index 37ece07933e0..5b68a0bda664 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/continuous/ContinuousOnboardingFeature.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/continuous/ContinuousOnboardingFeature.kt @@ -15,6 +15,9 @@ import androidx.annotation.VisibleForTesting import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.Fragment +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import mozilla.components.feature.ipprotection.store.IPProtectionStore import mozilla.components.support.base.feature.LifecycleAwareFeature import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import mozilla.components.support.base.log.logger.Logger @@ -22,6 +25,8 @@ import mozilla.components.support.utils.DateTimeProvider import mozilla.components.support.utils.DefaultDateTimeProvider import org.mozilla.fenix.R import org.mozilla.fenix.ext.components +import org.mozilla.fenix.ipprotection.store.IPProtectionOnboardingPrompt +import org.mozilla.fenix.ipprotection.store.IPProtectionPromptRepository import org.mozilla.fenix.onboarding.DismissedMethod import org.mozilla.fenix.onboarding.OnboardingTelemetryRecorder import org.mozilla.fenix.onboarding.OnboardingTelemetryRecorder.Companion.ET_CARD_CLOSE_BUTTON @@ -31,27 +36,60 @@ import org.mozilla.fenix.onboarding.view.OnboardingPageUiData import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.utils.Settings +/** + * Dependencies required to observe IP Protection eligibility and show its onboarding prompt. + * + * @property store The [IPProtectionStore] to observe for eligibility and account status changes. + * @property promptRepository Source of truth for whether the onboarding prompt is still allowed to appear. + * @property navigateToIpProtection Callback for when the IP Protection onboarding prompt should be shown to the user. + */ +class IPProtectionOnboardingConfig( + val store: IPProtectionStore, + val promptRepository: IPProtectionPromptRepository, + val navigateToIpProtection: () -> Unit, +) + /** * Manages the continuous onboarding flow shown after initial onboarding. * * Based on the user's current onboarding stage and device capabilities, this feature may: - * - request the default browser role on day 2 or day 3, - * - show a notification-permission onboarding card once the default-browser step is satisfied, or - * - show a Firefox Sync sign-in card on day 7. + * - on day 2 or day 3, request the default browser role, followed by a notification-permission onboarding card if + * available, skipping either step if already satisfied, + * - on day 5, show a Firefox Sync sign-in card, or skip it if already signed in, or + * - on day 7, show the IP Protection onboarding prompt, or skip it if already satisfied. */ class ContinuousOnboardingFeature( private val activity: Activity, private val launcher: ActivityResultLauncher, private val settings: Settings, private val telemetryRecorder: OnboardingTelemetryRecorder, - private val stageProvider: ContinuousOnboardingStageProvider, private val navigateToSyncSignIn: () -> Unit, + private val ipProtectionOnboardingConfig: IPProtectionOnboardingConfig, + private val stageProvider: ContinuousOnboardingStageProvider = ContinuousOnboardingStageProviderDefault(settings), private val dateTimeProvider: DateTimeProvider = DefaultDateTimeProvider(), + ipProtectionMainDispatcher: CoroutineDispatcher = Dispatchers.Main, ) : LifecycleAwareFeature { private val logger = Logger("ContinuousOnboardingFeatureDefault") @VisibleForTesting internal var pendingStage: ContinuousOnboardingStage = ContinuousOnboardingStage.NONE + /** + * Observes the IP Protection store, showing the IP Protection onboarding prompt once the user becomes eligible and + * [IPProtectionPromptRepository] allows it. + */ + private val ipProtectionBinding = + IPProtectionOnboardingPrompt( + repository = ipProtectionOnboardingConfig.promptRepository, + timeProvider = dateTimeProvider, + mainDispatcher = ipProtectionMainDispatcher, + store = ipProtectionOnboardingConfig.store, + onShowOnboarding = { + logger.info("Showing IP Protection onboarding prompt.") + ipProtectionOnboardingConfig.navigateToIpProtection() + markStageCompleted(ContinuousOnboardingStage.DAY_7) + }, + ) + override fun start() { if (!shouldShowContinuousOnboarding()) return @@ -61,7 +99,7 @@ class ContinuousOnboardingFeature( ContinuousOnboardingStage.DAY_2, ContinuousOnboardingStage.DAY_3 -> maybeRequestDefaultBrowserRole(stage) - ContinuousOnboardingStage.DAY_7 -> + ContinuousOnboardingStage.DAY_5 -> if (!settings.signedInFxaAccount) { showSyncCardDialog() } else { @@ -72,13 +110,20 @@ class ContinuousOnboardingFeature( markStageCompleted(stage) } + ContinuousOnboardingStage.DAY_7 -> { + logger.info("Observing IP Protection eligibility for day 7 onboarding.") + ipProtectionBinding.start() + } + ContinuousOnboardingStage.NONE -> { logger.info("No continuous onboarding stage to show.") } } } - override fun stop() = Unit + override fun stop() { + ipProtectionBinding.stop() + } /** * Returns whether the continuous onboarding flow is already active. @@ -137,7 +182,7 @@ class ContinuousOnboardingFeature( private fun showSyncCardDialog() { logger.info("Showing sync card dialog.") - val stage = ContinuousOnboardingStage.DAY_7 + val stage = ContinuousOnboardingStage.DAY_5 val onCloseButtonClicked = { logger.info("Closed the sync card dialog.") markStageCompleted(stage) @@ -350,6 +395,7 @@ class ContinuousOnboardingFeature( when (stage) { ContinuousOnboardingStage.DAY_2 -> settings.secondDayOnboardingCompletedTimestamp = now ContinuousOnboardingStage.DAY_3 -> settings.thirdDayOnboardingCompletedTimestamp = now + ContinuousOnboardingStage.DAY_5 -> settings.fifthDayOnboardingCompletedTimestamp = now ContinuousOnboardingStage.DAY_7 -> settings.seventhDayOnboardingCompletedTimestamp = now ContinuousOnboardingStage.NONE -> Unit } @@ -367,6 +413,7 @@ class ContinuousOnboardingFeature( * @param launcher The [ActivityResultLauncher] used to request system roles. * @param telemetryRecorder Used to record onboarding telemetry. * @param navigateToSyncSignIn Invoked when the user chooses to sign in to Firefox Sync. + * @param navigateToIpProtection Invoked when the IP Protection onboarding prompt should be shown. */ fun register( fragment: Fragment, @@ -374,8 +421,10 @@ class ContinuousOnboardingFeature( launcher: ActivityResultLauncher, telemetryRecorder: OnboardingTelemetryRecorder, navigateToSyncSignIn: () -> Unit, + navigateToIpProtection: () -> Unit, ) { - val settings = fragment.requireContext().components.settings + val components = fragment.requireContext().components + val settings = components.settings binding.set( feature = @@ -384,8 +433,13 @@ class ContinuousOnboardingFeature( launcher = launcher, settings = settings, telemetryRecorder = telemetryRecorder, - stageProvider = ContinuousOnboardingStageProviderDefault(settings), navigateToSyncSignIn = navigateToSyncSignIn, + ipProtectionOnboardingConfig = + IPProtectionOnboardingConfig( + store = components.ipProtection.store, + promptRepository = components.ipProtectionPromptRepository, + navigateToIpProtection = navigateToIpProtection, + ), ), owner = fragment.viewLifecycleOwner, view = fragment.requireView(), diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/continuous/ContinuousOnboardingStage.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/continuous/ContinuousOnboardingStage.kt index 00ccb42bf5ff..d5903eb19f59 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/continuous/ContinuousOnboardingStage.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/continuous/ContinuousOnboardingStage.kt @@ -8,6 +8,7 @@ package org.mozilla.fenix.onboarding.continuous enum class ContinuousOnboardingStage { DAY_2, DAY_3, + DAY_5, DAY_7, NONE, } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/continuous/ContinuousOnboardingStageProvider.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/continuous/ContinuousOnboardingStageProvider.kt index 8e3504bc895d..054ba3b49144 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/continuous/ContinuousOnboardingStageProvider.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/continuous/ContinuousOnboardingStageProvider.kt @@ -20,7 +20,7 @@ interface ContinuousOnboardingStageProvider { } private const val ONE_DAY = 1L -private const val FOUR_DAYS = 4L +private const val TWO_DAYS = 2L /** * Default implementation of [ContinuousOnboardingStageProvider]. @@ -48,6 +48,7 @@ class ContinuousOnboardingStageProviderDefault( return when { shouldShowDay2(today, zoneId) -> ContinuousOnboardingStage.DAY_2 shouldShowDay3(today, zoneId) -> ContinuousOnboardingStage.DAY_3 + shouldShowDay5(today, zoneId) -> ContinuousOnboardingStage.DAY_5 shouldShowDay7(today, zoneId) -> ContinuousOnboardingStage.DAY_7 else -> ContinuousOnboardingStage.NONE } @@ -70,11 +71,20 @@ class ContinuousOnboardingStageProviderDefault( return result } + private fun Settings.shouldShowDay5(today: LocalDate, zoneId: ZoneId): Boolean { + val result = + fifthDayOnboardingCompletedTimestamp == -1L && + thirdDayOnboardingCompletedTimestamp != -1L && + thirdDayOnboardingCompletedTimestamp.daysElapsedTo(today, zoneId) >= TWO_DAYS + logger.info("shouldShowDay5: $result") + return result + } + private fun Settings.shouldShowDay7(today: LocalDate, zoneId: ZoneId): Boolean { val result = seventhDayOnboardingCompletedTimestamp == -1L && - thirdDayOnboardingCompletedTimestamp != -1L && - thirdDayOnboardingCompletedTimestamp.daysElapsedTo(today, zoneId) >= FOUR_DAYS + fifthDayOnboardingCompletedTimestamp != -1L && + fifthDayOnboardingCompletedTimestamp.daysElapsedTo(today, zoneId) >= TWO_DAYS logger.info("shouldShowDay7: $result") return result } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SecretSettingsFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SecretSettingsFragment.kt index a8a4fb94305b..bd5246d3b1f5 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SecretSettingsFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/SecretSettingsFragment.kt @@ -460,12 +460,6 @@ class SecretSettingsFragment : PreferenceFragmentCompat(), SystemInsetsPaddedFra onPreferenceChangeListener = SharedPreferenceUpdater() } - requirePreference(R.string.pref_key_tab_groups).apply { - isVisible = Config.channel.isNightlyOrDebug - isChecked = settings.tabGroupsEnabled - onPreferenceChangeListener = SharedPreferenceUpdater() - } - requirePreference(R.string.pref_key_tab_groups_drag_and_drop).apply { isVisible = Config.channel.isNightlyOrDebug isChecked = settings.tabGroupsDragAndDropEnabled diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/TabsSettingsFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/TabsSettingsFragment.kt index 96a3b6a3576b..b0b13f841438 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/TabsSettingsFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/TabsSettingsFragment.kt @@ -31,6 +31,7 @@ class TabsSettingsFragment : PreferenceFragmentCompat(), SystemInsetsPaddedFragm private lateinit var inactiveTabsCategory: PreferenceCategory private lateinit var inactiveTabs: SwitchPreferenceCompat private lateinit var privacyReport: SwitchPreferenceCompat + private lateinit var tabGroups: SwitchPreferenceCompat private val args by navArgs() override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { @@ -78,6 +79,12 @@ class TabsSettingsFragment : PreferenceFragmentCompat(), SystemInsetsPaddedFragm it.onPreferenceChangeListener = SharedPreferenceUpdater() } + tabGroups = + requirePreference(R.string.pref_key_tab_groups).also { + it.isChecked = requireComponents.settings.tabGroupsEnabled + it.onPreferenceChangeListener = SharedPreferenceUpdater() + } + inactiveTabsCategory = requirePreference(R.string.pref_key_inactive_tabs_category).also { it.isEnabled = diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ui/TabManagementFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ui/TabManagementFragment.kt index e6f8feb1dbda..656645adda82 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ui/TabManagementFragment.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ui/TabManagementFragment.kt @@ -655,6 +655,7 @@ class TabManagementFragment : Fragment() { restoredState.config.copy( displayTabsInGrid = settings.gridTabView, homepageAsNewTabEnabled = settings.enableHomepageAsNewTab, + tabGroupsEnabled = settings.tabGroupsEnabled, ) ) ?: createInitialState(args, settings), middlewares = diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt index a62a2a600946..35dcc7090fb7 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt @@ -2377,6 +2377,13 @@ class Settings( default = -1L, ) + /** The completion timestamp of the fifth day of continuous onboarding. */ + var fifthDayOnboardingCompletedTimestamp by + longPreference( + key = appContext.getPreferenceKey(R.string.pref_key_continuous_onboarding_day_five_completed_timestamp), + default = -1L, + ) + /** The completion timestamp of the seventh day of continuous onboarding. */ var seventhDayOnboardingCompletedTimestamp by longPreference( diff --git a/mobile/android/fenix/app/src/main/res/values/preference_keys.xml b/mobile/android/fenix/app/src/main/res/values/preference_keys.xml index 8e9c40ad5a18..5fd33cc0e6ec 100644 --- a/mobile/android/fenix/app/src/main/res/values/preference_keys.xml +++ b/mobile/android/fenix/app/src/main/res/values/preference_keys.xml @@ -130,6 +130,7 @@ pref_key_continuous_onboarding_enabled pref_key_continuous_onboarding_day_two_completed_timestamp pref_key_continuous_onboarding_day_three_completed_timestamp + pref_key_continuous_onboarding_day_five_completed_timestamp pref_key_continuous_onboarding_day_seven_completed_timestamp diff --git a/mobile/android/fenix/app/src/main/res/values/static_strings.xml b/mobile/android/fenix/app/src/main/res/values/static_strings.xml index 25096b63d633..892116751c78 100644 --- a/mobile/android/fenix/app/src/main/res/values/static_strings.xml +++ b/mobile/android/fenix/app/src/main/res/values/static_strings.xml @@ -175,7 +175,6 @@ Show Voice Search in Display Toolbar - Enable Tab Groups Enable Tab Groups Drag and Drop Enable Live Reorder for Drag and Drop Enable Tab Groups Onboarding diff --git a/mobile/android/fenix/app/src/main/res/values/strings.xml b/mobile/android/fenix/app/src/main/res/values/strings.xml index cea2cf7d55c9..3ae57d29e4d0 100644 --- a/mobile/android/fenix/app/src/main/res/values/strings.xml +++ b/mobile/android/fenix/app/src/main/res/values/strings.xml @@ -1355,6 +1355,8 @@ Move old tabs to inactive Tabs you haven’t viewed for two weeks get moved to the inactive section. + + Enable Tab Groups diff --git a/mobile/android/fenix/app/src/main/res/xml/secret_settings_preferences.xml b/mobile/android/fenix/app/src/main/res/xml/secret_settings_preferences.xml index 3fb4d17766b6..f483f8ccc3fa 100644 --- a/mobile/android/fenix/app/src/main/res/xml/secret_settings_preferences.xml +++ b/mobile/android/fenix/app/src/main/res/xml/secret_settings_preferences.xml @@ -128,10 +128,6 @@ android:key="@string/pref_key_tab_manager_opening_animation" android:title="@string/preferences_tab_manager_opening_animation" app:iconSpaceReserved="false" /> - + + diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/usecases/FenixBrowserUseCasesTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/usecases/FenixBrowserUseCasesTest.kt index 85bc7bf6b869..d5b9e28a133f 100644 --- a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/usecases/FenixBrowserUseCasesTest.kt +++ b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/usecases/FenixBrowserUseCasesTest.kt @@ -426,44 +426,6 @@ class FenixBrowserUseCasesTest { } } - @Test - fun `WHEN add new homepage tab in group use case is invoked with default partition THEN create a new homepage tab and add it to group`() { - val tabId = "new-tab-id" - val group = "test-group" - appStore = AppStore(initialState = AppState(mode = BrowsingMode.Normal)) - useCases = - FenixBrowserUseCases( - appStore = appStore, - tabsUseCases = tabsUseCases, - loadUrlUseCase = loadUrlUseCase, - searchUseCases = searchUseCases, - homepageTitle = homepageTitle, - profiler = profiler, - ) - - every { - tabsUseCases.addTab.invoke( - url = any(), - title = any(), - private = any(), - ) - } returns tabId - - useCases.addNewHomepageTabInGroup(group = group) - - verifyOrder { - tabsUseCases.addTab.invoke( - url = ABOUT_HOME_URL, - title = homepageTitle, - private = false, - ) - tabsUseCases.addTabsInGroup( - group = group, - tabId = tabId, - ) - } - } - companion object { private const val PROFILER_START_TIME = Double.MAX_VALUE } diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/debugsettings/info/SecretSettingsKeysProviderTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/debugsettings/info/SecretSettingsKeysProviderTest.kt index 9b3c509c64cf..41a088817629 100644 --- a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/debugsettings/info/SecretSettingsKeysProviderTest.kt +++ b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/debugsettings/info/SecretSettingsKeysProviderTest.kt @@ -21,7 +21,6 @@ class SecretSettingsKeysProviderTest { assertTrue(keys.contains(testContext.getString(R.string.pref_key_allow_third_party_root_certs))) assertTrue(keys.contains(testContext.getString(R.string.pref_key_native_share_sheet))) - assertTrue(keys.contains(testContext.getString(R.string.pref_key_tab_groups))) assertFalse(keys.contains(testContext.getString(R.string.pref_key_show_debug_info))) assertFalse(keys.contains(testContext.getString(R.string.pref_key_custom_glean_server_url))) } diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/onboarding/continuous/ContinuousOnboardingFeatureTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/onboarding/continuous/ContinuousOnboardingFeatureTest.kt index b3b45403ca9b..ccfb0b511180 100644 --- a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/onboarding/continuous/ContinuousOnboardingFeatureTest.kt +++ b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/onboarding/continuous/ContinuousOnboardingFeatureTest.kt @@ -14,6 +14,12 @@ import androidx.compose.ui.platform.ComposeView import androidx.core.app.ActivityOptionsCompat import androidx.test.filters.SdkSuppress import kotlin.test.assertNotNull +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest +import mozilla.components.feature.ipprotection.store.IPProtectionAction +import mozilla.components.feature.ipprotection.store.IPProtectionStore +import mozilla.components.feature.ipprotection.store.state.AccountStatus +import mozilla.components.feature.ipprotection.store.state.EligibilityStatus import mozilla.components.support.test.robolectric.testContext import mozilla.components.support.utils.DateTimeProvider import mozilla.components.support.utils.FakeDateTimeProvider @@ -29,6 +35,7 @@ import org.mozilla.fenix.GleanMetrics.Onboarding import org.mozilla.fenix.R import org.mozilla.fenix.ext.components import org.mozilla.fenix.helpers.FenixGleanTestRule +import org.mozilla.fenix.ipprotection.FakeIPProtectionPromptRepository import org.mozilla.fenix.onboarding.OnboardingReason import org.mozilla.fenix.onboarding.OnboardingTelemetryRecorder import org.mozilla.fenix.onboarding.view.Action @@ -44,11 +51,15 @@ private const val CONTINUOUS_ONBOARDING_DIALOG_TAG = "continuous_onboarding_dial class ContinuousOnboardingFeatureTest { @get:Rule val gleanTestRule = FenixGleanTestRule(testContext) + private val testDispatcher = StandardTestDispatcher() + private lateinit var activity: Activity private lateinit var settings: Settings private lateinit var telemetryRecorder: OnboardingTelemetryRecorder private lateinit var stageProvider: ContinuousOnboardingStageProvider private lateinit var dateTimeProvider: DateTimeProvider + private lateinit var ipProtectionStore: IPProtectionStore + private lateinit var ipProtectionPromptRepository: FakeIPProtectionPromptRepository private lateinit var feature: ContinuousOnboardingFeature @Before @@ -62,6 +73,8 @@ class ContinuousOnboardingFeatureTest { ) dateTimeProvider = FakeDateTimeProvider() stageProvider = FakeContinuousOnboardingStageProvider() + ipProtectionStore = IPProtectionStore() + ipProtectionPromptRepository = FakeIPProtectionPromptRepository() feature = ContinuousOnboardingFeature( activity = activity, @@ -71,6 +84,13 @@ class ContinuousOnboardingFeatureTest { stageProvider = stageProvider, dateTimeProvider = dateTimeProvider, navigateToSyncSignIn = {}, + ipProtectionOnboardingConfig = + IPProtectionOnboardingConfig( + store = ipProtectionStore, + promptRepository = ipProtectionPromptRepository, + navigateToIpProtection = {}, + ), + ipProtectionMainDispatcher = testDispatcher, ) } @@ -153,7 +173,7 @@ class ContinuousOnboardingFeatureTest { ) }, ) - val actualState = feature.getSyncOnboardingPageState(ContinuousOnboardingStage.DAY_7) + val actualState = feature.getSyncOnboardingPageState(ContinuousOnboardingStage.DAY_5) assertEquals(expectedState.imageRes, actualState.imageRes) assertEquals(expectedState.title, actualState.title) @@ -164,7 +184,7 @@ class ContinuousOnboardingFeatureTest { @Test fun `WHEN sync primary button is clicked THEN sign-in telemetry is recorded`() { - val pageState = feature.getSyncOnboardingPageState(ContinuousOnboardingStage.DAY_7) + val pageState = feature.getSyncOnboardingPageState(ContinuousOnboardingStage.DAY_5) pageState.primaryButton.onClick() @@ -181,7 +201,7 @@ class ContinuousOnboardingFeatureTest { assertNotNull(Onboarding.dismissed.testGetValue()) assertEquals("completed", Onboarding.dismissed.testGetValue()!!.single().extra!!["method"]) - assertEquals(-1L, settings.seventhDayOnboardingCompletedTimestamp) + assertEquals(-1L, settings.fifthDayOnboardingCompletedTimestamp) } @Test @@ -197,8 +217,15 @@ class ContinuousOnboardingFeatureTest { stageProvider = stageProvider, dateTimeProvider = dateTimeProvider, navigateToSyncSignIn = navigateToSyncSignIn, + ipProtectionOnboardingConfig = + IPProtectionOnboardingConfig( + store = ipProtectionStore, + promptRepository = ipProtectionPromptRepository, + navigateToIpProtection = {}, + ), + ipProtectionMainDispatcher = testDispatcher, ) - val pageState = feature.getSyncOnboardingPageState(ContinuousOnboardingStage.DAY_7) + val pageState = feature.getSyncOnboardingPageState(ContinuousOnboardingStage.DAY_5) pageState.primaryButton.onClick() @@ -207,7 +234,7 @@ class ContinuousOnboardingFeatureTest { @Test fun `WHEN sync secondary button is clicked THEN skip sign-in telemetry is recorded`() { - val pageState = feature.getSyncOnboardingPageState(ContinuousOnboardingStage.DAY_7) + val pageState = feature.getSyncOnboardingPageState(ContinuousOnboardingStage.DAY_5) pageState.secondaryButton!!.onClick() @@ -224,12 +251,12 @@ class ContinuousOnboardingFeatureTest { assertNotNull(Onboarding.dismissed.testGetValue()) assertEquals("skipped", Onboarding.dismissed.testGetValue()!!.single().extra!!["method"]) - assertEquals(dateTimeProvider.currentTimeMillis(), settings.seventhDayOnboardingCompletedTimestamp) + assertEquals(dateTimeProvider.currentTimeMillis(), settings.fifthDayOnboardingCompletedTimestamp) } @Test fun `WHEN sync impression event fires THEN sign-in card telemetry is recorded`() { - val pageState = feature.getSyncOnboardingPageState(ContinuousOnboardingStage.DAY_7) + val pageState = feature.getSyncOnboardingPageState(ContinuousOnboardingStage.DAY_5) pageState.onRecordImpressionEvent() @@ -241,7 +268,7 @@ class ContinuousOnboardingFeatureTest { @Test fun `WHEN no sync button is clicked THEN no sign-in telemetry is recorded`() { - feature.getSyncOnboardingPageState(ContinuousOnboardingStage.DAY_7) + feature.getSyncOnboardingPageState(ContinuousOnboardingStage.DAY_5) assertNull(Onboarding.signIn.testGetValue()) assertNull(Onboarding.skipSignIn.testGetValue()) @@ -351,6 +378,194 @@ class ContinuousOnboardingFeatureTest { assertNull(Onboarding.turnOnNotificationsCard.testGetValue()) } + // IP Protection integration + + @Test + fun `WHEN stage is DAY_7 AND eligibility and account become ready AND repository allows THEN navigateToIpProtection is invoked and seventh day timestamp is saved`() = + runTest(testDispatcher) { + settings.continuousOnboardingFeatureEnabled = true + var navigateToIpProtectionInvoked = false + val feature = + ContinuousOnboardingFeature( + activity = activity, + launcher = FakeActivityResultLauncher(), + settings = settings, + telemetryRecorder = telemetryRecorder, + stageProvider = FakeContinuousOnboardingStageProvider(ContinuousOnboardingStage.DAY_7), + dateTimeProvider = dateTimeProvider, + navigateToSyncSignIn = {}, + ipProtectionOnboardingConfig = + IPProtectionOnboardingConfig( + store = ipProtectionStore, + promptRepository = ipProtectionPromptRepository, + navigateToIpProtection = { navigateToIpProtectionInvoked = true }, + ), + ipProtectionMainDispatcher = testDispatcher, + ) + + feature.start() + ipProtectionStore.dispatch(IPProtectionAction.EligibilityChanged(EligibilityStatus.Eligible)) + ipProtectionStore.dispatch(IPProtectionAction.AccountStateChanged(AccountStatus.NoAccount)) + testDispatcher.scheduler.advanceUntilIdle() + + assertTrue(navigateToIpProtectionInvoked) + assertEquals(dateTimeProvider.currentTimeMillis(), settings.seventhDayOnboardingCompletedTimestamp) + } + + @Test + fun `WHEN stage is DAY_7 AND repository does not allow the prompt THEN navigateToIpProtection is not invoked`() = + runTest(testDispatcher) { + settings.continuousOnboardingFeatureEnabled = true + var navigateToIpProtectionInvoked = false + val feature = + ContinuousOnboardingFeature( + activity = activity, + launcher = FakeActivityResultLauncher(), + settings = settings, + telemetryRecorder = telemetryRecorder, + stageProvider = FakeContinuousOnboardingStageProvider(ContinuousOnboardingStage.DAY_7), + dateTimeProvider = dateTimeProvider, + navigateToSyncSignIn = {}, + ipProtectionOnboardingConfig = + IPProtectionOnboardingConfig( + store = ipProtectionStore, + promptRepository = FakeIPProtectionPromptRepository(canShowIPProtectionPrompt = false), + navigateToIpProtection = { navigateToIpProtectionInvoked = true }, + ), + ipProtectionMainDispatcher = testDispatcher, + ) + + feature.start() + ipProtectionStore.dispatch(IPProtectionAction.EligibilityChanged(EligibilityStatus.Eligible)) + ipProtectionStore.dispatch(IPProtectionAction.AccountStateChanged(AccountStatus.NoAccount)) + testDispatcher.scheduler.advanceUntilIdle() + + assertFalse(navigateToIpProtectionInvoked) + assertEquals(-1L, settings.seventhDayOnboardingCompletedTimestamp) + } + + @Test + fun `WHEN feature is stopped THEN the IP Protection binding stops observing eligibility`() = + runTest(testDispatcher) { + settings.continuousOnboardingFeatureEnabled = true + var navigateToIpProtectionInvoked = false + val feature = + ContinuousOnboardingFeature( + activity = activity, + launcher = FakeActivityResultLauncher(), + settings = settings, + telemetryRecorder = telemetryRecorder, + stageProvider = FakeContinuousOnboardingStageProvider(ContinuousOnboardingStage.DAY_7), + dateTimeProvider = dateTimeProvider, + navigateToSyncSignIn = {}, + ipProtectionOnboardingConfig = + IPProtectionOnboardingConfig( + store = ipProtectionStore, + promptRepository = ipProtectionPromptRepository, + navigateToIpProtection = { navigateToIpProtectionInvoked = true }, + ), + ipProtectionMainDispatcher = testDispatcher, + ) + + feature.start() + feature.stop() + ipProtectionStore.dispatch(IPProtectionAction.EligibilityChanged(EligibilityStatus.Eligible)) + ipProtectionStore.dispatch(IPProtectionAction.AccountStateChanged(AccountStatus.NoAccount)) + testDispatcher.scheduler.advanceUntilIdle() + + assertFalse(navigateToIpProtectionInvoked) + } + + @Test + fun `WHEN feature is stopped and started again THEN the IP Protection binding still observes eligibility`() = + runTest(testDispatcher) { + settings.continuousOnboardingFeatureEnabled = true + var navigateToIpProtectionInvoked = false + val feature = + ContinuousOnboardingFeature( + activity = activity, + launcher = FakeActivityResultLauncher(), + settings = settings, + telemetryRecorder = telemetryRecorder, + stageProvider = FakeContinuousOnboardingStageProvider(ContinuousOnboardingStage.DAY_7), + dateTimeProvider = dateTimeProvider, + navigateToSyncSignIn = {}, + ipProtectionOnboardingConfig = + IPProtectionOnboardingConfig( + store = ipProtectionStore, + promptRepository = ipProtectionPromptRepository, + navigateToIpProtection = { navigateToIpProtectionInvoked = true }, + ), + ipProtectionMainDispatcher = testDispatcher, + ) + + // Simulates the fragment view going through a stop/start cycle (e.g. backgrounding + // the app) before eligibility changes, reusing the same IP Protection binding. + feature.start() + feature.stop() + feature.start() + ipProtectionStore.dispatch(IPProtectionAction.EligibilityChanged(EligibilityStatus.Eligible)) + ipProtectionStore.dispatch(IPProtectionAction.AccountStateChanged(AccountStatus.NoAccount)) + testDispatcher.scheduler.advanceUntilIdle() + + assertTrue(navigateToIpProtectionInvoked) + assertEquals(dateTimeProvider.currentTimeMillis(), settings.seventhDayOnboardingCompletedTimestamp) + } + + @Test + fun `WHEN feature is stopped without ever starting THEN no exception is thrown`() { + val feature = + ContinuousOnboardingFeature( + activity = activity, + launcher = FakeActivityResultLauncher(), + settings = settings, + telemetryRecorder = telemetryRecorder, + stageProvider = stageProvider, + dateTimeProvider = dateTimeProvider, + navigateToSyncSignIn = {}, + ipProtectionOnboardingConfig = + IPProtectionOnboardingConfig( + store = ipProtectionStore, + promptRepository = ipProtectionPromptRepository, + navigateToIpProtection = {}, + ), + ipProtectionMainDispatcher = testDispatcher, + ) + + feature.stop() + } + + @Test + fun `WHEN stage is DAY_7 AND feature is disabled THEN navigateToIpProtection is not invoked`() = + runTest(testDispatcher) { + settings.continuousOnboardingFeatureEnabled = false + var navigateToIpProtectionInvoked = false + val feature = + ContinuousOnboardingFeature( + activity = activity, + launcher = FakeActivityResultLauncher(), + settings = settings, + telemetryRecorder = telemetryRecorder, + stageProvider = FakeContinuousOnboardingStageProvider(ContinuousOnboardingStage.DAY_7), + dateTimeProvider = dateTimeProvider, + navigateToSyncSignIn = {}, + ipProtectionOnboardingConfig = + IPProtectionOnboardingConfig( + store = ipProtectionStore, + promptRepository = ipProtectionPromptRepository, + navigateToIpProtection = { navigateToIpProtectionInvoked = true }, + ), + ipProtectionMainDispatcher = testDispatcher, + ) + + feature.start() + ipProtectionStore.dispatch(IPProtectionAction.EligibilityChanged(EligibilityStatus.Eligible)) + ipProtectionStore.dispatch(IPProtectionAction.AccountStateChanged(AccountStatus.NoAccount)) + testDispatcher.scheduler.advanceUntilIdle() + + assertFalse(navigateToIpProtectionInvoked) + } + // markStageCompleted @Test @@ -359,6 +574,7 @@ class ContinuousOnboardingFeatureTest { assertEquals(dateTimeProvider.currentTimeMillis(), settings.secondDayOnboardingCompletedTimestamp) assertEquals(-1L, settings.thirdDayOnboardingCompletedTimestamp) + assertEquals(-1L, settings.fifthDayOnboardingCompletedTimestamp) assertEquals(-1L, settings.seventhDayOnboardingCompletedTimestamp) } @@ -368,6 +584,17 @@ class ContinuousOnboardingFeatureTest { assertEquals(-1L, settings.secondDayOnboardingCompletedTimestamp) assertEquals(dateTimeProvider.currentTimeMillis(), settings.thirdDayOnboardingCompletedTimestamp) + assertEquals(-1L, settings.fifthDayOnboardingCompletedTimestamp) + assertEquals(-1L, settings.seventhDayOnboardingCompletedTimestamp) + } + + @Test + fun `WHEN DAY_5 stage is completed THEN fifth day timestamp is saved`() { + feature.markStageCompleted(ContinuousOnboardingStage.DAY_5) + + assertEquals(-1L, settings.secondDayOnboardingCompletedTimestamp) + assertEquals(-1L, settings.thirdDayOnboardingCompletedTimestamp) + assertEquals(dateTimeProvider.currentTimeMillis(), settings.fifthDayOnboardingCompletedTimestamp) assertEquals(-1L, settings.seventhDayOnboardingCompletedTimestamp) } @@ -377,6 +604,7 @@ class ContinuousOnboardingFeatureTest { assertEquals(-1L, settings.secondDayOnboardingCompletedTimestamp) assertEquals(-1L, settings.thirdDayOnboardingCompletedTimestamp) + assertEquals(-1L, settings.fifthDayOnboardingCompletedTimestamp) assertEquals(dateTimeProvider.currentTimeMillis(), settings.seventhDayOnboardingCompletedTimestamp) } @@ -386,6 +614,7 @@ class ContinuousOnboardingFeatureTest { assertEquals(-1L, settings.secondDayOnboardingCompletedTimestamp) assertEquals(-1L, settings.thirdDayOnboardingCompletedTimestamp) + assertEquals(-1L, settings.fifthDayOnboardingCompletedTimestamp) assertEquals(-1L, settings.seventhDayOnboardingCompletedTimestamp) } @@ -441,6 +670,13 @@ class ContinuousOnboardingFeatureTest { stageProvider = fakeStageProvider, dateTimeProvider = dateTimeProvider, navigateToSyncSignIn = {}, + ipProtectionOnboardingConfig = + IPProtectionOnboardingConfig( + store = ipProtectionStore, + promptRepository = ipProtectionPromptRepository, + navigateToIpProtection = {}, + ), + ipProtectionMainDispatcher = testDispatcher, ) feature.pendingStage = ContinuousOnboardingStage.DAY_2 @@ -462,6 +698,13 @@ class ContinuousOnboardingFeatureTest { stageProvider = fakeStageProvider, dateTimeProvider = dateTimeProvider, navigateToSyncSignIn = {}, + ipProtectionOnboardingConfig = + IPProtectionOnboardingConfig( + store = ipProtectionStore, + promptRepository = ipProtectionPromptRepository, + navigateToIpProtection = {}, + ), + ipProtectionMainDispatcher = testDispatcher, ) val decorView = activity.window.decorView as ViewGroup decorView.addView( diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/onboarding/continuous/ContinuousOnboardingStageProviderTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/onboarding/continuous/ContinuousOnboardingStageProviderTest.kt index dd5dd03cfdc4..419ec94abd7b 100644 --- a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/onboarding/continuous/ContinuousOnboardingStageProviderTest.kt +++ b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/onboarding/continuous/ContinuousOnboardingStageProviderTest.kt @@ -96,7 +96,7 @@ class ContinuousOnboardingStageProviderTest { } @Test - fun `WHEN day 3 shown today and day 7 not shown THEN get stage returns NONE`() { + fun `WHEN day 3 shown today and day 5 not shown THEN get stage returns NONE`() { settings.secondDayOnboardingCompletedTimestamp = day1Millis settings.thirdDayOnboardingCompletedTimestamp = day2Millis @@ -104,10 +104,68 @@ class ContinuousOnboardingStageProviderTest { } @Test - fun `WHEN day 3 shown 3 days ago and day 7 not shown THEN get stage returns NONE`() { + fun `WHEN day 3 shown 1 day ago and day 5 not shown THEN get stage returns NONE`() { settings.secondDayOnboardingCompletedTimestamp = day1Millis settings.thirdDayOnboardingCompletedTimestamp = day2Millis + assertEquals( + ContinuousOnboardingStage.NONE, + getStage(day2Millis + ONE_DAY_MILLIS), + ) + } + + @Test + fun `WHEN day 3 shown 2 days ago and day 5 not shown THEN get stage returns DAY_5`() { + settings.secondDayOnboardingCompletedTimestamp = day1Millis + settings.thirdDayOnboardingCompletedTimestamp = day2Millis + + assertEquals( + ContinuousOnboardingStage.DAY_5, + getStage(day2Millis + 2 * ONE_DAY_MILLIS), + ) + } + + @Test + fun `WHEN day 3 shown 3 days ago and day 5 not shown THEN get stage returns DAY_5`() { + settings.secondDayOnboardingCompletedTimestamp = day1Millis + settings.thirdDayOnboardingCompletedTimestamp = day2Millis + + assertEquals( + ContinuousOnboardingStage.DAY_5, + getStage(day2Millis + 3 * ONE_DAY_MILLIS), + ) + } + + @Test + fun `WHEN day 3 shown 2 days ago and day 5 already shown THEN get stage returns NONE`() { + settings.secondDayOnboardingCompletedTimestamp = day1Millis + settings.thirdDayOnboardingCompletedTimestamp = day2Millis + settings.fifthDayOnboardingCompletedTimestamp = day2Millis + 2 * ONE_DAY_MILLIS + + assertEquals( + ContinuousOnboardingStage.NONE, + getStage(day2Millis + 2 * ONE_DAY_MILLIS), + ) + } + + @Test + fun `WHEN day 5 shown today and day 7 not shown THEN get stage returns NONE`() { + settings.secondDayOnboardingCompletedTimestamp = day1Millis + settings.thirdDayOnboardingCompletedTimestamp = day2Millis + settings.fifthDayOnboardingCompletedTimestamp = day2Millis + 2 * ONE_DAY_MILLIS + + assertEquals( + ContinuousOnboardingStage.NONE, + getStage(day2Millis + 2 * ONE_DAY_MILLIS), + ) + } + + @Test + fun `WHEN day 5 shown 1 day ago and day 7 not shown THEN get stage returns NONE`() { + settings.secondDayOnboardingCompletedTimestamp = day1Millis + settings.thirdDayOnboardingCompletedTimestamp = day2Millis + settings.fifthDayOnboardingCompletedTimestamp = day2Millis + 2 * ONE_DAY_MILLIS + assertEquals( ContinuousOnboardingStage.NONE, getStage(day2Millis + 3 * ONE_DAY_MILLIS), @@ -115,9 +173,10 @@ class ContinuousOnboardingStageProviderTest { } @Test - fun `WHEN day 3 shown 4 days ago and day 7 not shown THEN get stage returns DAY_7`() { + fun `WHEN day 5 shown 2 days ago and day 7 not shown THEN get stage returns DAY_7`() { settings.secondDayOnboardingCompletedTimestamp = day1Millis settings.thirdDayOnboardingCompletedTimestamp = day2Millis + settings.fifthDayOnboardingCompletedTimestamp = day2Millis + 2 * ONE_DAY_MILLIS assertEquals( ContinuousOnboardingStage.DAY_7, @@ -126,9 +185,10 @@ class ContinuousOnboardingStageProviderTest { } @Test - fun `WHEN day 3 shown 5 days ago and day 7 not shown THEN get stage returns DAY_7`() { + fun `WHEN day 5 shown 3 days ago and day 7 not shown THEN get stage returns DAY_7`() { settings.secondDayOnboardingCompletedTimestamp = day1Millis settings.thirdDayOnboardingCompletedTimestamp = day2Millis + settings.fifthDayOnboardingCompletedTimestamp = day2Millis + 2 * ONE_DAY_MILLIS assertEquals( ContinuousOnboardingStage.DAY_7, @@ -137,9 +197,10 @@ class ContinuousOnboardingStageProviderTest { } @Test - fun `WHEN day 3 shown 4 days ago and day 7 already shown THEN get stage returns NONE`() { + fun `WHEN day 5 shown 2 days ago and day 7 already shown THEN get stage returns NONE`() { settings.secondDayOnboardingCompletedTimestamp = day1Millis settings.thirdDayOnboardingCompletedTimestamp = day2Millis + settings.fifthDayOnboardingCompletedTimestamp = day2Millis + 2 * ONE_DAY_MILLIS settings.seventhDayOnboardingCompletedTimestamp = day2Millis + 4 * ONE_DAY_MILLIS assertEquals( @@ -152,6 +213,7 @@ class ContinuousOnboardingStageProviderTest { fun `WHEN all stages completed THEN returns NONE`() { settings.secondDayOnboardingCompletedTimestamp = day1Millis settings.thirdDayOnboardingCompletedTimestamp = day2Millis + settings.fifthDayOnboardingCompletedTimestamp = day2Millis + 2 * ONE_DAY_MILLIS settings.seventhDayOnboardingCompletedTimestamp = day2Millis + 4 * ONE_DAY_MILLIS assertEquals( diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml index f762b331cdbf..3941e102e9ad 100644 --- a/modules/libpref/init/StaticPrefList.yaml +++ b/modules/libpref/init/StaticPrefList.yaml @@ -10122,6 +10122,14 @@ mirror: always set_spidermonkey_pref: always +# When true, causes wasm baseline to generate code that can be navigated by the +# JS debugger. This is mostly for testing only. +- name: javascript.options.wasm_baseline_debug + type: bool + value: false + mirror: always + set_spidermonkey_pref: always + # Support for pretenuring allocations based on their allocation site. - name: javascript.options.site_based_pretenuring type: bool @@ -13705,6 +13713,13 @@ value: true mirror: always + # Use MediaDataDecoder API for AV1 in WebRTC. This includes hardware + # acceleration for decoding. +- name: media.navigator.mediadatadecoder_av1_enabled + type: RelaxedAtomicBool + value: true + mirror: always + #if defined(MOZ_WIDGET_GTK) # Use hardware acceleration for VP8 decoding on Linux. - name: media.navigator.mediadatadecoder_vp8_hardware_enabled @@ -15502,17 +15517,6 @@ value: 40 mirror: always -# How long a single unchanging holder may keep nsHostResolver's DB lock while -# another thread waits, before we treat the lock as wedged and crash, naming the -# thread and code site holding it. The lock is normally held for microseconds. -# The timer restarts whenever the holder changes, so slow-but-progressing -# contention never trips this. Only has an effect in builds where -# MOZ_DIAGNOSTIC_ASSERT is fatal. 0 disables the check. -- name: network.dns.db_lock_timeout_ms - type: RelaxedAtomicUint32 - value: 3000 - mirror: always - - name: network.dns.max_any_priority_threads type: RelaxedAtomicUint32 value: 24 @@ -18279,12 +18283,6 @@ value: false mirror: always -# Partition the service workers unconditionally when dFPI is enabled. -- name: privacy.partition.serviceWorkers - type: RelaxedAtomicBool - value: true - mirror: always - # Enables / disables the strip on share feature which strips query parameters # when copying/sharing in-content links or from the url bar. - name: privacy.query_stripping.strip_on_share.enabled diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js index b826f4361974..335fa81cd419 100644 --- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -4051,7 +4051,11 @@ pref("extensions.formautofill.addresses.supported", "detect"); // Use ML for address form field detection. #if defined(XP_WIN) || defined(XP_MACOSX) -pref("extensions.formautofill.useml", true); + #if MOZ_UPDATE_CHANNEL != release && MOZ_UPDATE_CHANNEL != esr + pref("extensions.formautofill.useml", true); + #else + pref("extensions.formautofill.useml", false); + #endif #else pref("extensions.formautofill.useml", false); #endif diff --git a/mots.yaml b/mots.yaml index e563d6bdb70b..a5126412ad8d 100644 --- a/mots.yaml +++ b/mots.yaml @@ -8,7 +8,7 @@ # documentation and how to modify this file. repo: mozilla-central created_at: '2021-10-14T12:50:40.073465' -updated_at: '2026-08-10T15:43:04.147480+00:00' +updated_at: '2026-08-14T22:20:03.123725+00:00' export: path: ./docs/mots/index.md format: md @@ -2996,23 +2996,6 @@ modules: - *lina machine_name: core_storage - - name: 'Core: String' - description: '' - includes: - - xpcom/string/**/* - meta: - group: dev-tech-xpcom - url: '{ref}`String Guide`' - components: - - Core::String - owners_emeritus: - - David Baron - peers_emeritus: - - Eric Rahm - owners: [] - peers: [] - machine_name: core_string - - name: 'Core: Style System' description: CSS style sheet handling; style data computation includes: @@ -3486,6 +3469,7 @@ modules: - xpcom/ioutils/**/* - xpcom/reflect/**/* - xpcom/rust/**/* + - xpcom/string/**/* - xpcom/system/**/* - xpcom/tests/**/* - xpcom/threads/**/* @@ -3493,6 +3477,7 @@ modules: meta: owners_emeritus: - Benjamin Smedberg + - David Baron (String) peers_emeritus: - Doug Turner - Eric Rahm @@ -4869,5 +4854,5 @@ modules: - Ryan Tilder group: dev-platform hashes: - config: 047f362e10b5d3c0d197644c02c888be932b73dd - export: b38e97f0df6cea50fe3a137e2bb1a48c1fb3c0f2 + config: 96fa3e78dff983949b24a845149735502b9efeb9 + export: de8a46a6ef62ee1e3822e46a0f8ab016338ac7b1 diff --git a/netwerk/dns/DNSByTypeRecord.h b/netwerk/dns/DNSByTypeRecord.h index 79133d09e5e6..ed5cab7b05ef 100644 --- a/netwerk/dns/DNSByTypeRecord.h +++ b/netwerk/dns/DNSByTypeRecord.h @@ -32,6 +32,7 @@ struct IPCTypeRecord { TypeRecordResultType mData; uint32_t mTTL = 0; bool mIsTRR = false; + bool mFromStaleCache = false; }; } // namespace net @@ -40,7 +41,7 @@ struct IPCTypeRecord { namespace IPC { DEFINE_IPC_SERIALIZER_WITH_FIELDS(mozilla::net::IPCTypeRecord, mData, mTTL, - mIsTRR); + mIsTRR, mFromStaleCache); DEFINE_IPC_SERIALIZER_WITH_FIELDS(mozilla::net::SVCB, mSvcFieldPriority, mSvcDomainName, mEchConfig, mODoHConfig, diff --git a/netwerk/dns/DNSRequestChild.cpp b/netwerk/dns/DNSRequestChild.cpp index b771d7f40bbe..f92451c6aa0a 100644 --- a/netwerk/dns/DNSRequestChild.cpp +++ b/netwerk/dns/DNSRequestChild.cpp @@ -57,6 +57,7 @@ class ChildDNSRecord : public nsIDNSAddrRecord { nsITRRSkipReason::value mTRRSkipReason = nsITRRSkipReason::TRR_UNSET; uint32_t mTTL = 0; TimeStamp mLastUpdate = mozilla::TimeStamp::NowLoRes(); + bool mFromStaleCache = false; }; NS_IMPL_ISUPPORTS(ChildDNSRecord, nsIDNSRecord, nsIDNSAddrRecord) @@ -78,6 +79,7 @@ ChildDNSRecord::ChildDNSRecord(const DNSRecord& reply, mAddresses = addrs.Clone(); mTTL = reply.ttl(); mLastUpdate = reply.lastUpdate(); + mFromStaleCache = reply.fromStaleCache(); } //----------------------------------------------------------------------------- @@ -212,6 +214,12 @@ ChildDNSRecord::GetLastUpdate(TimeStamp* aLastUpdate) { return NS_OK; } +NS_IMETHODIMP +ChildDNSRecord::GetFromStaleCache(bool* aResult) { + *aResult = mFromStaleCache; + return NS_OK; +} + class ChildDNSByTypeRecord : public nsIDNSByTypeRecord, public nsIDNSTXTRecord, public nsIDNSHTTPSSVCRecord, @@ -225,7 +233,7 @@ class ChildDNSByTypeRecord : public nsIDNSByTypeRecord, explicit ChildDNSByTypeRecord(const TypeRecordResultType& reply, const nsACString& aHost, uint32_t aTTL, - bool aIsTRR); + bool aIsTRR, bool aFromStaleCache); private: virtual ~ChildDNSByTypeRecord() = default; @@ -234,6 +242,7 @@ class ChildDNSByTypeRecord : public nsIDNSByTypeRecord, bool mAllRecordsExcluded = false; uint32_t mTTL = 0; bool mIsTRR = false; + bool mFromStaleCache = false; }; NS_IMPL_ISUPPORTS(ChildDNSByTypeRecord, nsIDNSByTypeRecord, nsIDNSRecord, @@ -241,11 +250,19 @@ NS_IMPL_ISUPPORTS(ChildDNSByTypeRecord, nsIDNSByTypeRecord, nsIDNSRecord, ChildDNSByTypeRecord::ChildDNSByTypeRecord(const TypeRecordResultType& reply, const nsACString& aHost, - uint32_t aTTL, bool aIsTRR) + uint32_t aTTL, bool aIsTRR, + bool aFromStaleCache) : DNSHTTPSSVCRecordBase(aHost) { mResults = reply; mTTL = aTTL; mIsTRR = aIsTRR; + mFromStaleCache = aFromStaleCache; +} + +NS_IMETHODIMP +ChildDNSByTypeRecord::GetFromStaleCache(bool* aResult) { + *aResult = mFromStaleCache; + return NS_OK; } NS_IMETHODIMP @@ -532,7 +549,8 @@ bool DNSRequestSender::OnRecvLookupCompleted(const DNSRequestResponse& reply) { MOZ_ASSERT(mType != nsIDNSService::RESOLVE_TYPE_DEFAULT); mResultRecord = new ChildDNSByTypeRecord( reply.get_IPCTypeRecord().mData, mHost, - reply.get_IPCTypeRecord().mTTL, reply.get_IPCTypeRecord().mIsTRR); + reply.get_IPCTypeRecord().mTTL, reply.get_IPCTypeRecord().mIsTRR, + reply.get_IPCTypeRecord().mFromStaleCache); break; } default: diff --git a/netwerk/dns/DNSRequestParent.cpp b/netwerk/dns/DNSRequestParent.cpp index 1678e0ebad13..50f3fec578bc 100644 --- a/netwerk/dns/DNSRequestParent.cpp +++ b/netwerk/dns/DNSRequestParent.cpp @@ -102,6 +102,7 @@ DNSRequestHandler::OnLookupComplete(nsICancelable* request, if (byTypeRec) { IPCTypeRecord result; byTypeRec->GetResults(&result.mData); + byTypeRec->GetFromStaleCache(&result.mFromStaleCache); if (nsCOMPtr rec = do_QueryInterface(aRecord)) { rec->GetTtl(&result.mTTL); rec->IsTRR(&result.mIsTRR); @@ -142,11 +143,14 @@ DNSRequestHandler::OnLookupComplete(nsICancelable* request, TimeStamp lastUpdate; rec->GetLastUpdate(&lastUpdate); + bool fromStaleCache = false; + rec->GetFromStaleCache(&fromStaleCache); + SendLookupCompletedHelper( mIPCActor, - DNSRequestResponse(DNSRecord(cname, array, trrFetchDuration, - trrFetchDurationNetworkOnly, isTRR, - effectiveTRRMode, ttl, lastUpdate))); + DNSRequestResponse(DNSRecord( + cname, array, trrFetchDuration, trrFetchDurationNetworkOnly, isTRR, + effectiveTRRMode, ttl, lastUpdate, fromStaleCache))); } else { SendLookupCompletedHelper(mIPCActor, DNSRequestResponse(status)); } diff --git a/netwerk/dns/PDNSRequestParams.ipdlh b/netwerk/dns/PDNSRequestParams.ipdlh index c5f6120f0df6..aec3fae1ec23 100644 --- a/netwerk/dns/PDNSRequestParams.ipdlh +++ b/netwerk/dns/PDNSRequestParams.ipdlh @@ -24,6 +24,10 @@ struct DNSRecord TRRMode effectiveTRRMode; uint32_t ttl; TimeStamp lastUpdate; + // Grace-period status of the entry this answer came from, sampled when the + // lookup completed. The receiving side has no expiration times of its own to + // recompute it from. + bool fromStaleCache; }; union DNSRequestResponse diff --git a/netwerk/dns/nsDNSService2.cpp b/netwerk/dns/nsDNSService2.cpp index d4b0585a4020..11d0b319d66f 100644 --- a/netwerk/dns/nsDNSService2.cpp +++ b/netwerk/dns/nsDNSService2.cpp @@ -366,6 +366,11 @@ nsDNSRecord::GetLastUpdate(mozilla::TimeStamp* aLastUpdate) { return mHostRecord->GetLastUpdate(aLastUpdate); } +NS_IMETHODIMP +nsDNSRecord::GetFromStaleCache(bool* aResult) { + return mHostRecord->GetFromStaleCache(aResult); +} + class nsDNSByTypeRecord : public nsIDNSByTypeRecord, public nsIDNSTXTRecord, public nsIDNSHTTPSSVCRecord { @@ -461,6 +466,11 @@ nsDNSByTypeRecord::GetResults(mozilla::net::TypeRecordResultType* aResults) { return NS_OK; } +NS_IMETHODIMP +nsDNSByTypeRecord::GetFromStaleCache(bool* aResult) { + return mHostRecord->GetFromStaleCache(aResult); +} + NS_IMETHODIMP nsDNSByTypeRecord::GetTtl(uint32_t* aTtl) { return mHostRecord->GetTtl(aTtl); } diff --git a/netwerk/dns/nsHostRecord.cpp b/netwerk/dns/nsHostRecord.cpp index 6119e4736bd1..8a1977d97033 100644 --- a/netwerk/dns/nsHostRecord.cpp +++ b/netwerk/dns/nsHostRecord.cpp @@ -167,6 +167,12 @@ bool nsHostRecord::HasUsableResult(const mozilla::TimeStamp& now, return HasUsableResultInternal(now, queryFlags); } +nsresult nsHostRecord::GetFromStaleCache(bool* aResult) { + NS_ENSURE_ARG(aResult); + *aResult = CheckExpiration(mozilla::TimeStamp::NowLoRes()) == EXP_GRACE; + return NS_OK; +} + //---------------------------------------------------------------------------- // AddrHostRecord //---------------------------------------------------------------------------- diff --git a/netwerk/dns/nsHostRecord.h b/netwerk/dns/nsHostRecord.h index b7cc5b7127b3..d60ff869e244 100644 --- a/netwerk/dns/nsHostRecord.h +++ b/netwerk/dns/nsHostRecord.h @@ -123,6 +123,8 @@ class nsHostRecord : public mozilla::LinkedListElement>, DNS_PRIORITY_HIGH, }; + nsresult GetFromStaleCache(bool* aResult); + protected: friend class nsHostResolver; friend class mozilla::net::HostRecordQueue; diff --git a/netwerk/dns/nsHostResolver.cpp b/netwerk/dns/nsHostResolver.cpp index 2effcc0dcac7..2831816b6107 100644 --- a/netwerk/dns/nsHostResolver.cpp +++ b/netwerk/dns/nsHostResolver.cpp @@ -225,7 +225,7 @@ void nsHostResolver::ClearPendingQueue( // right now, so we need to mark them to get re-resolved on completion! void nsHostResolver::FlushCache(bool aTrrToo, bool aFlushEvictionQueue) { - mozilla::net::AutoResolverWriteLock dbLock(mDBLock); + mozilla::AutoWriteLock dbLock(mDBLock); MutexAutoLock queueLock(mQueue.mLock); if (aFlushEvictionQueue) { @@ -266,7 +266,7 @@ void nsHostResolver::Shutdown() { nsTArray shutdownCallbacks; { - mozilla::net::AutoResolverWriteLock dbLock(mDBLock); + mozilla::AutoWriteLock dbLock(mDBLock); MutexAutoLock queueLock(mQueue.mLock); mShutdown = true; @@ -320,7 +320,7 @@ nsresult nsHostResolver::GetHostRecord( const nsACString& host, const nsACString& aTrrServer, uint16_t type, nsIDNSService::DNSFlags flags, uint16_t af, bool pb, const nsCString& originSuffix, nsHostRecord** result) { - mozilla::net::AutoResolverWriteLock dbLock(mDBLock); + mozilla::AutoWriteLock dbLock(mDBLock); nsHostKey key(host, aTrrServer, type, flags, af, pb, originSuffix); RefPtr rec = @@ -490,12 +490,7 @@ nsresult nsHostResolver::ResolveHost(const nsACString& aHost, RefPtr result; nsresult status = NS_OK, rv = NS_OK; { - MOZ_DIAGNOSTIC_ASSERT(!mDBLock.LockedForWritingByCurrentThread(), - "Re-entered ResolveHost with mDBLock already held"); - MOZ_DIAGNOSTIC_ASSERT(!mDBLock.LockedForReadingByCurrentThread(), - "ResolveHost called with mDBLock held for reading"); - - mozilla::net::AutoResolverWriteLock dbLock(mDBLock); + mozilla::AutoWriteLock dbLock(mDBLock); MutexAutoLock queueLock(mQueue.mLock); if (mShutdown) { @@ -866,7 +861,7 @@ void nsHostResolver::DetachCallback( RefPtr callback(aCallback); { - mozilla::net::AutoResolverWriteLock dbLock(mDBLock); + mozilla::AutoWriteLock dbLock(mDBLock); MutexAutoLock queueLock(mQueue.mLock); nsAutoCString originSuffix; @@ -1460,7 +1455,7 @@ nsHostResolver::LookupStatus nsHostResolver::CompleteLookup( CallbackArray callbacks; LookupStatus result; { - AutoResolverWriteLock dbLock(mDBLock); + AutoWriteLock dbLock(mDBLock); MutexAutoLock queueLock(mQueue.mLock); result = CompleteLookupLocked(rec, status, aNewRRSet, pb, aOriginsuffix, aReason, aTRRRequest, callbacks); @@ -1654,7 +1649,7 @@ nsHostResolver::LookupStatus nsHostResolver::CompleteLookupByType( CallbackArray callbacks; LookupStatus result; { - AutoResolverWriteLock dbLock(mDBLock); + AutoWriteLock dbLock(mDBLock); MutexAutoLock queueLock(mQueue.mLock); result = CompleteLookupByTypeLocked(rec, status, aResult, aReason, aTtl, pb, callbacks); @@ -1767,7 +1762,7 @@ void nsHostResolver::CancelAsyncRequest( RefPtr rec; { - mozilla::net::AutoResolverWriteLock dbLock(mDBLock); + mozilla::AutoWriteLock dbLock(mDBLock); MutexAutoLock queueLock(mQueue.mLock); nsAutoCString originSuffix; @@ -1800,7 +1795,7 @@ void nsHostResolver::CancelAsyncRequest( } size_t nsHostResolver::SizeOfIncludingThis(MallocSizeOf mallocSizeOf) const { - mozilla::net::AutoResolverReadLock dbLock(mDBLock); + mozilla::AutoReadLock dbLock(mDBLock); size_t n = mallocSizeOf(this); @@ -1919,7 +1914,7 @@ nsresult nsHostResolver::Create(nsHostResolver** result) { } void nsHostResolver::GetDNSCacheEntries(nsTArray* args) { - mozilla::net::AutoResolverReadLock dbLock(mDBLock); + mozilla::AutoReadLock dbLock(mDBLock); for (const auto& recordEntry : mRecordDB) { // We don't pay attention to address literals, only resolved domains. // Also require a host. diff --git a/netwerk/dns/nsHostResolver.h b/netwerk/dns/nsHostResolver.h index bec3e11a9895..b3d4d7c5e58f 100644 --- a/netwerk/dns/nsHostResolver.h +++ b/netwerk/dns/nsHostResolver.h @@ -26,201 +26,12 @@ #include "nsTArray.h" #include "nscore.h" #include "prnetdb.h" -#include "prthread.h" namespace mozilla { namespace net { class TRR; class TRRQuery; -// A lock holder leaves no stack frame of its own, so a wedged RWLock appears in -// a hang report as waiters with no owner at all -- undiagnosable, which is the -// situation in bug 2059597. In diagnostic builds mDBLock is a DiagnosticRWLock -// that records who holds it and lets a blocked writer give up after a timeout -// and crash naming the holder; elsewhere it is a plain RWLock. Use the -// ResolverRWLock / AutoResolver*Lock aliases below rather than either directly. -#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED - -class MOZ_CAPABILITY("rwlock") DiagnosticRWLock final : public RWLock { - public: - explicit DiagnosticRWLock(const char* aName) : RWLock(aName) {} - - void WriteLock() MOZ_CAPABILITY_ACQUIRE() { - RWLock::WriteLock(); - // Recorded only once the lock is held, so a thread still blocked acquiring - // it never appears to be the owner. - mWriter = PR_GetCurrentThread(); - ++mAcquisitions; - } - - [[nodiscard]] bool TryWriteLock() MOZ_TRY_ACQUIRE(true) { - bool locked = RWLock::TryWriteLock(); - if (locked) { - mWriter = PR_GetCurrentThread(); - ++mAcquisitions; - } - return locked; - } - - void WriteUnlock() MOZ_EXCLUSIVE_RELEASE() { - mWriter = nullptr; - RWLock::WriteUnlock(); - } - - void ReadLock() MOZ_ACQUIRE_SHARED() { - RWLock::ReadLock(); - NoteReadAcquired(); - } - - void ReadUnlock() MOZ_RELEASE_SHARED() { - NoteReadReleased(); - RWLock::ReadUnlock(); - } - - // Acquires the write lock, but rather than blocking forever on a wedged lock, - // crashes after network.dns.db_lock_timeout_ms naming whoever holds it. - void WriteLockOrDiagnose() - MOZ_CAPABILITY_ACQUIRE() MOZ_NO_THREAD_SAFETY_ANALYSIS { - uint32_t timeoutMs = StaticPrefs::network_dns_db_lock_timeout_ms(); - if (timeoutMs) { - if (TryWriteLock()) { - return; - } - // This lock is taken on every lookup and normally held for microseconds, - // so ordinary contention must not pay a sleep. Yield briefly first; the - // holder almost always finishes within a yield or two. - TimeStamp start = TimeStamp::Now(); - while ((TimeStamp::Now() - start).ToMilliseconds() < 2.0) { - PR_Sleep(PR_INTERVAL_NO_WAIT); - if (TryWriteLock()) { - return; - } - } - // Still blocked after yielding: either a long legitimate hold or a wedged - // lock. Only crash if nobody acquired the lock at all during the wait -- - // slow-but-progressing contention keeps bumping mAcquisitions. The poll - // count additionally rules out this thread having been descheduled (or - // the process suspended) for the whole window, which would otherwise be - // indistinguishable from a wedge. - uint32_t acquisitions = mAcquisitions; - start = TimeStamp::Now(); - uint32_t polls = 0; - while (!TryWriteLock()) { - if (uint32_t current = mAcquisitions; current != acquisitions) { - acquisitions = current; - start = TimeStamp::Now(); - polls = 0; - } else if (++polls >= 10 && - (TimeStamp::Now() - start).ToMilliseconds() > timeoutMs) { - CrashWithHolder(); - } - PR_Sleep(PR_MillisecondsToInterval(50)); - } - return; - } - WriteLock(); - } - - bool LockedForWritingByCurrentThread() const { - return mWriter == PR_GetCurrentThread(); - } - - bool LockedForReadingByCurrentThread() const { - return FindReaderSlot(PR_GetCurrentThread()) < kMaxTrackedReaders; - } - - private: - // Two read sites, on distinct threads: SizeOfIncludingThis from the memory - // reporter on the main thread, and GetDNSCacheEntries from the dashboard on - // the socket thread. A further concurrent reader simply goes unrecorded. - static constexpr size_t kMaxTrackedReaders = 2; - - void NoteReadAcquired() { - PRThread* self = PR_GetCurrentThread(); - ++mAcquisitions; - for (auto& slot : mReaders) { - if (slot.compareExchange(nullptr, self)) { - return; - } - } - // More concurrent readers than slots; this one goes unrecorded. - } - - void NoteReadReleased() { - size_t i = FindReaderSlot(PR_GetCurrentThread()); - if (i < kMaxTrackedReaders) { - mReaders[i] = nullptr; - } - } - - size_t FindReaderSlot(PRThread* aThread) const { - for (size_t i = 0; i < kMaxTrackedReaders; ++i) { - if (mReaders[i] == aThread) { - return i; - } - } - return kMaxTrackedReaders; - } - - [[noreturn]] void CrashWithHolder() { - if (PRThread* writer = mWriter) { - MOZ_CRASH_UNSAFE_PRINTF("nsHostResolver DB lock wedged by writer on %s", - ThreadName(writer)); - } - for (auto& slot : mReaders) { - if (PRThread* reader = slot) { - // The thread identifies the site: SizeOfIncludingThis runs on the main - // thread, GetDNSCacheEntries on the socket thread. - MOZ_CRASH_UNSAFE_PRINTF("nsHostResolver DB lock wedged by reader on %s", - ThreadName(reader)); - } - } - // No holder recorded: either an untracked reader, an acquisition that - // bypassed this class, or corrupted lock state. - MOZ_CRASH("nsHostResolver DB lock wedged by an unrecorded holder"); - } - - static const char* ThreadName(PRThread* aThread) { - const char* name = PR_GetThreadName(aThread); - return name ? name : "unnamed"; - } - - // Relaxed is sufficient: purely diagnostic, and always published while the - // lock itself provides the ordering for the data it guards. - Atomic mWriter{nullptr}; - Atomic mReaders[kMaxTrackedReaders]{}; - // Bumped on every acquisition, so a waiter can tell a wedged lock from one - // that is merely busy. - Atomic mAcquisitions{0}; -}; - -// Write-locks a DiagnosticRWLock, crashing rather than hanging forever if the -// lock is wedged. -class MOZ_SCOPED_CAPABILITY MOZ_RAII AutoDiagnosticWriteLock final { - public: - explicit AutoDiagnosticWriteLock(DiagnosticRWLock& aLock) - MOZ_CAPABILITY_ACQUIRE(aLock) - : mLock(aLock) { - mLock.WriteLockOrDiagnose(); - } - ~AutoDiagnosticWriteLock() MOZ_CAPABILITY_RELEASE() { mLock.WriteUnlock(); } - - private: - DiagnosticRWLock& mLock; -}; - -using ResolverRWLock = DiagnosticRWLock; -using AutoResolverReadLock = BaseAutoReadLock; -using AutoResolverWriteLock = AutoDiagnosticWriteLock; - -#else // !MOZ_DIAGNOSTIC_ASSERT_ENABLED - -using ResolverRWLock = RWLock; -using AutoResolverReadLock = AutoReadLock; -using AutoResolverWriteLock = AutoWriteLock; - -#endif // MOZ_DIAGNOSTIC_ASSERT_ENABLED - // Clamped to 1 so DequeueNextRecord can always make progress. static inline uint32_t MaxResolverThreadsAnyPriority() { return std::max(StaticPrefs::network_dns_max_any_priority_threads(), 1u); @@ -490,7 +301,7 @@ class nsHostResolver : public nsISupports, public AHostResolver { // mutable so SizeOfIncludingThis can be const // Protects mRecordDB. When held together with mQueue.mLock, always acquire // mDBLock first. - mutable mozilla::net::ResolverRWLock mDBLock{"nsHostResolver.mDBLock"}; + mutable mozilla::RWLock mDBLock{"nsHostResolver.mDBLock"}; mozilla::net::HostRecordQueue mQueue; nsRefPtrHashtable, nsHostRecord> mRecordDB MOZ_GUARDED_BY(mDBLock); diff --git a/netwerk/dns/nsIDNSByTypeRecord.idl b/netwerk/dns/nsIDNSByTypeRecord.idl index 2de5b87d25e8..86591dee2272 100644 --- a/netwerk/dns/nsIDNSByTypeRecord.idl +++ b/netwerk/dns/nsIDNSByTypeRecord.idl @@ -41,6 +41,13 @@ interface nsIDNSByTypeRecord : nsIDNSRecord readonly attribute unsigned long type; [noscript] readonly attribute TypeResult results; + + /** + * True if this answer was served from a stale cache entry, i.e. one past + * its TTL but still within the grace period. Happy Eyeballs uses this to + * revalidate the answer with a cache-bypassing lookup (Optimistic DNS). + */ + readonly attribute boolean fromStaleCache; }; [scriptable, builtinclass, uuid(2a71750d-cb21-45f1-9e1c-666d18dd7645)] diff --git a/netwerk/dns/nsIDNSRecord.idl b/netwerk/dns/nsIDNSRecord.idl index f1c425d9f9dd..d1049e8a1c76 100644 --- a/netwerk/dns/nsIDNSRecord.idl +++ b/netwerk/dns/nsIDNSRecord.idl @@ -158,4 +158,11 @@ interface nsIDNSAddrRecord : nsIDNSRecord * Returns the timestamp when this record is updated. */ [noscript] readonly attribute TimeStamp lastUpdate; + + /** + * True if this answer was served from a stale cache entry, i.e. one past + * its TTL but still within the grace period. Happy Eyeballs uses this to + * revalidate the answer with a cache-bypassing lookup (Optimistic DNS). + */ + readonly attribute boolean fromStaleCache; }; diff --git a/netwerk/ipc/DocumentLoadListener.cpp b/netwerk/ipc/DocumentLoadListener.cpp index bf27eca89e9c..c25a77ff7c15 100644 --- a/netwerk/ipc/DocumentLoadListener.cpp +++ b/netwerk/ipc/DocumentLoadListener.cpp @@ -36,6 +36,7 @@ #include "mozilla/dom/ProcessIsolation.h" #include "mozilla/dom/ReferrerInfo.h" #include "mozilla/dom/RemoteWebProgressRequest.h" +#include "mozilla/dom/ServiceWorkerUtils.h" #include "mozilla/dom/SessionHistoryEntry.h" #include "mozilla/dom/WindowGlobalParent.h" #include "mozilla/dom/ipc/IdType.h" @@ -2697,6 +2698,17 @@ void DocumentLoadListener::TriggerRedirectToRealChannel( RedirectToRealChannelFinished(rv); return; } + + // Update the enterprise ServiceWorder policy on the destination + // BrowsingContext before sending the navigation to the content process. + // Since IPC messages from the parent to a given content process are + // ordered, the content process will see the correct + // ServiceWorkersDisabledByPolicy value before it begins loading the + // document, preventing scripts from observing a stale value. + if (aDestinationBrowsingContext->IsTopContent()) { + (void)aDestinationBrowsingContext->SetServiceWorkersDisabledByPolicy( + dom::IsServiceWorkersDisabledByPolicy(docURI)); + } } // Ensure that the BrowsingContextGroup which will finish this load has the diff --git a/netwerk/metrics.yaml b/netwerk/metrics.yaml index 794403a44101..f737f48b6c6b 100644 --- a/netwerk/metrics.yaml +++ b/netwerk/metrics.yaml @@ -1805,6 +1805,32 @@ networking: - VersionNegotiation - WrongRole + http_3_max_consecutive_ptos: + type: custom_distribution + unit: integer + description: > + The longest run of consecutive probe timeouts (PTOs) an established + HTTP/3 connection saw over its lifetime, recorded once when the + connection is closed. A long run means the path went dark (a black + hole): this is the condition neqo's max_pto black-hole detector acts + on, so the distribution shows how often established connections break + and, once the detector is enabled, whether it closes them sooner. 0 + means the connection saw no PTO. + range_min: 0 + range_max: 16 + bucket_count: 17 + histogram_type: linear + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=2060066 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=2060066 + data_sensitivity: + - technical + notification_emails: + - necko@mozilla.com + - minden@mozilla.com + expires: never + http_3_quic_frame_count: type: labeled_counter description: > diff --git a/netwerk/protocol/http/ConnectionEstablisher.cpp b/netwerk/protocol/http/ConnectionEstablisher.cpp index f415c67ef919..78229da9476b 100644 --- a/netwerk/protocol/http/ConnectionEstablisher.cpp +++ b/netwerk/protocol/http/ConnectionEstablisher.cpp @@ -151,6 +151,15 @@ SingleDNSAddrRecord::GetLastUpdate(mozilla::TimeStamp* aLastUpdate) { return NS_OK; } +NS_IMETHODIMP +SingleDNSAddrRecord::GetFromStaleCache(bool* aResult) { + // Happy Eyeballs reads staleness directly off the resolved DNS record to feed + // the state machine; the per-address record it hands to the connection does + // not carry it, and nothing downstream reads it. + *aResult = false; + return NS_OK; +} + NS_IMETHODIMP SingleDNSAddrRecord::GetNextAddr(uint16_t aPort, NetAddr* aAddr) { if (mDone) { diff --git a/netwerk/protocol/http/HappyEyeballsConnectionAttempt.cpp b/netwerk/protocol/http/HappyEyeballsConnectionAttempt.cpp index de888302975e..2e117cb98ec7 100644 --- a/netwerk/protocol/http/HappyEyeballsConnectionAttempt.cpp +++ b/netwerk/protocol/http/HappyEyeballsConnectionAttempt.cpp @@ -546,7 +546,8 @@ nsresult HappyEyeballsConnectionAttempt::ProcessHappyEyeballsOutput() { LOG(("HappyEyeballsEvent::Tag::SendDnsQuery id=%" PRIu64 " hostname=%s", event.send_dns_query.id, dnsHostname.get())); DNSLookup(event.send_dns_query.record_type, - SetupDnsFlags(event.send_dns_query.record_type), + SetupDnsFlags(event.send_dns_query.record_type, + event.send_dns_query.allow_stale), event.send_dns_query.id, dnsHostname); break; } @@ -639,7 +640,7 @@ nsresult HappyEyeballsConnectionAttempt::ProcessHappyEyeballsOutput() { Result HappyEyeballsConnectionAttempt::SetupDnsFlags( - happy_eyeballs::DnsRecordType aType) { + happy_eyeballs::DnsRecordType aType, bool aAllowStale) { LOG(("HappyEyeballsConnectionAttempt::SetupDnsFlags [this=%p aType=%d] ", this, static_cast(aType))); @@ -649,6 +650,15 @@ HappyEyeballsConnectionAttempt::SetupDnsFlags( dnsFlags = nsIDNSService::RESOLVE_BYPASS_CACHE; } + // Optimistic DNS: happy-eyeballs sends this query to revalidate an answer it + // received from a stale (expired) cache entry, so it must not be served from + // that same stale entry. Bypassing the cache forces a fresh lookup; the stale + // entry is left in place so concurrent consumers can still use it while the + // revalidation is in flight. + if (!aAllowStale) { + dnsFlags |= nsIDNSService::RESOLVE_BYPASS_CACHE; + } + // Fallback attempt after TRR-resolved addresses failed to connect: bypass TRR // and the (TRR-populated) cache to re-resolve natively. if (mRetryWithoutTRR) { @@ -1968,7 +1978,7 @@ nsresult HappyEyeballsConnectionAttempt::OnARecord(nsIDNSRecord* aRecord, } nsTArray emptyArray; rv = happy_eyeballs_process_dns_response_a(mHappyEyeballs, aId, &emptyArray, - mDnsMetadata.mIsTRR); + mDnsMetadata.mIsTRR, false); if (NS_FAILED(rv)) { return rv; } @@ -1993,8 +2003,12 @@ nsresult HappyEyeballsConnectionAttempt::OnARecord(nsIDNSRecord* aRecord, MaybeBuildOriginCoalescingKeys(); } + bool aFromStaleCache = false; + (void)addrRecord->GetFromStaleCache(&aFromStaleCache); + rv = happy_eyeballs_process_dns_response_a( - mHappyEyeballs, aId, &ipv4Addresses, mDnsMetadata.mIsTRR); + mHappyEyeballs, aId, &ipv4Addresses, mDnsMetadata.mIsTRR, + aFromStaleCache); if (NS_FAILED(rv)) { return rv; } @@ -2025,7 +2039,7 @@ nsresult HappyEyeballsConnectionAttempt::OnAAAARecord(nsIDNSRecord* aRecord, } nsTArray emptyArray; rv = happy_eyeballs_process_dns_response_aaaa( - mHappyEyeballs, aId, &emptyArray, mDnsMetadata.mIsTRR); + mHappyEyeballs, aId, &emptyArray, mDnsMetadata.mIsTRR, false); if (NS_FAILED(rv)) { return rv; } @@ -2050,8 +2064,12 @@ nsresult HappyEyeballsConnectionAttempt::OnAAAARecord(nsIDNSRecord* aRecord, MaybeBuildOriginCoalescingKeys(); } + bool aaaaFromStaleCache = false; + (void)addrRecord->GetFromStaleCache(&aaaaFromStaleCache); + rv = happy_eyeballs_process_dns_response_aaaa( - mHappyEyeballs, aId, &ipv6Addresses, mDnsMetadata.mIsTRR); + mHappyEyeballs, aId, &ipv6Addresses, mDnsMetadata.mIsTRR, + aaaaFromStaleCache); if (NS_FAILED(rv)) { return rv; } @@ -2113,12 +2131,18 @@ nsresult HappyEyeballsConnectionAttempt::OnHTTPSRecord(nsIDNSRecord* aRecord, if (!httpsRecord || NS_FAILED(status)) { nsTArray emptyArray; (void)happy_eyeballs_process_dns_response_https( - mHappyEyeballs, aId, &emptyArray, mDnsMetadata.mIsTRR); + mHappyEyeballs, aId, &emptyArray, mDnsMetadata.mIsTRR, false); return ProcessHappyEyeballsOutput(); } bool httpsIsTRR = false; (void)httpsRecord->IsTRR(&httpsIsTRR); + + bool httpsFromStaleCache = false; + if (nsCOMPtr byTypeRec = do_QueryInterface(aRecord)) { + (void)byTypeRec->GetFromStaleCache(&httpsFromStaleCache); + } + if (httpsIsTRR) { mDnsMetadata.mIsTRR = true; mDnsMetadata.mEffectiveTRRMode = @@ -2136,8 +2160,8 @@ nsresult HappyEyeballsConnectionAttempt::OnHTTPSRecord(nsIDNSRecord* aRecord, (void)httpsRecord->GetRecords(svcbRecords); if (svcbRecords.IsEmpty()) { nsTArray emptyArray; - (void)happy_eyeballs_process_dns_response_https(mHappyEyeballs, aId, - &emptyArray, httpsIsTRR); + (void)happy_eyeballs_process_dns_response_https( + mHappyEyeballs, aId, &emptyArray, httpsIsTRR, false); return ProcessHappyEyeballsOutput(); } @@ -2223,8 +2247,8 @@ nsresult HappyEyeballsConnectionAttempt::OnHTTPSRecord(nsIDNSRecord* aRecord, serviceInfos.AppendElement(std::move(svcInfo)); } - (void)happy_eyeballs_process_dns_response_https(mHappyEyeballs, aId, - &serviceInfos, httpsIsTRR); + (void)happy_eyeballs_process_dns_response_https( + mHappyEyeballs, aId, &serviceInfos, httpsIsTRR, httpsFromStaleCache); return ProcessHappyEyeballsOutput(); } diff --git a/netwerk/protocol/http/HappyEyeballsConnectionAttempt.h b/netwerk/protocol/http/HappyEyeballsConnectionAttempt.h index 495bf72a48e4..1bf8bf81e7d7 100644 --- a/netwerk/protocol/http/HappyEyeballsConnectionAttempt.h +++ b/netwerk/protocol/http/HappyEyeballsConnectionAttempt.h @@ -213,7 +213,7 @@ class HappyEyeballsConnectionAttempt final : public ConnectionAttempt, // DNS lookups Result SetupDnsFlags( - happy_eyeballs::DnsRecordType aType); + happy_eyeballs::DnsRecordType aType, bool aAllowStale); void DNSLookup(happy_eyeballs::DnsRecordType aType, Result aFlags, uint64_t aId, const nsACString& aHostname); diff --git a/netwerk/protocol/http/happy_eyeballs_glue/src/lib.rs b/netwerk/protocol/http/happy_eyeballs_glue/src/lib.rs index 4ce64368afe9..808234484009 100644 --- a/netwerk/protocol/http/happy_eyeballs_glue/src/lib.rs +++ b/netwerk/protocol/http/happy_eyeballs_glue/src/lib.rs @@ -157,6 +157,7 @@ pub unsafe extern "C" fn happy_eyeballs_process_dns_response_a( id: u64, addrs: *const ThinVec, is_trr: bool, + stale: bool, ) -> nsresult { let Some(he) = (unsafe { he.as_mut() }) else { debug_assert!(false, "unexpected null he pointer"); @@ -168,7 +169,7 @@ pub unsafe extern "C" fn happy_eyeballs_process_dns_response_a( return NS_ERROR_INVALID_ARG; }; - he.process_dns_response_a(id, addrs, is_trr) + he.process_dns_response_a(id, addrs, is_trr, stale) } #[no_mangle] @@ -177,6 +178,7 @@ pub unsafe extern "C" fn happy_eyeballs_process_dns_response_aaaa( id: u64, addrs: *const ThinVec, is_trr: bool, + stale: bool, ) -> nsresult { let Some(he) = (unsafe { he.as_mut() }) else { debug_assert!(false, "unexpected null he pointer"); @@ -188,7 +190,7 @@ pub unsafe extern "C" fn happy_eyeballs_process_dns_response_aaaa( return NS_ERROR_INVALID_ARG; }; - he.process_dns_response_aaaa(id, addrs, is_trr) + he.process_dns_response_aaaa(id, addrs, is_trr, stale) } #[no_mangle] @@ -197,6 +199,7 @@ pub unsafe extern "C" fn happy_eyeballs_process_dns_response_https( id: u64, service_infos: *const ThinVec, is_trr: bool, + stale: bool, ) -> nsresult { let Some(he) = (unsafe { he.as_mut() }) else { debug_assert!(false, "unexpected null he pointer"); @@ -208,7 +211,7 @@ pub unsafe extern "C" fn happy_eyeballs_process_dns_response_https( return NS_ERROR_INVALID_ARG; }; - he.process_dns_response_https(id, service_infos, is_trr) + he.process_dns_response_https(id, service_infos, is_trr, stale) } #[no_mangle] @@ -288,6 +291,7 @@ impl HappyEyeballs { id: u64, net_addrs: &ThinVec, is_trr: bool, + stale: bool, ) -> nsresult { let id: happy_eyeballs::Id = id.into(); let mut addrs = Vec::with_capacity(net_addrs.len()); @@ -303,15 +307,11 @@ impl HappyEyeballs { addrs.push(ipv4); } - self.profiler.dns_response(id, &addrs); + self.profiler.dns_response(id, &addrs, stale); self.metrics.dns_response(id, !addrs.is_empty(), is_trr); let result = happy_eyeballs::DnsResult::A(Ok(addrs)); - let input = happy_eyeballs::Input::DnsResult { - id, - result, - stale: false, - }; + let input = happy_eyeballs::Input::DnsResult { id, result, stale }; self.inner.process_input(input, Instant::now()); NS_OK @@ -322,6 +322,7 @@ impl HappyEyeballs { id: u64, net_addrs: &ThinVec, is_trr: bool, + stale: bool, ) -> nsresult { let id: happy_eyeballs::Id = id.into(); let mut addrs = Vec::with_capacity(net_addrs.len()); @@ -338,15 +339,11 @@ impl HappyEyeballs { addrs.push(ipv6); } - self.profiler.dns_response(id, &addrs); + self.profiler.dns_response(id, &addrs, stale); self.metrics.dns_response(id, !addrs.is_empty(), is_trr); let result = happy_eyeballs::DnsResult::Aaaa(Ok(addrs)); - let input = happy_eyeballs::Input::DnsResult { - id, - result, - stale: false, - }; + let input = happy_eyeballs::Input::DnsResult { id, result, stale }; self.inner.process_input(input, Instant::now()); NS_OK @@ -357,6 +354,7 @@ impl HappyEyeballs { id: u64, service_infos: &ThinVec, is_trr: bool, + stale: bool, ) -> nsresult { let id: happy_eyeballs::Id = id.into(); let mut infos = Vec::new(); @@ -426,15 +424,11 @@ impl HappyEyeballs { }); } - self.profiler.dns_response_https(id, &infos); + self.profiler.dns_response_https(id, &infos, stale); self.metrics.dns_response_https(id, &infos, is_trr); let result = happy_eyeballs::DnsResult::Https(Ok(infos)); - let input = happy_eyeballs::Input::DnsResult { - id, - result, - stale: false, - }; + let input = happy_eyeballs::Input::DnsResult { id, result, stale }; self.inner.process_input(input, Instant::now()); NS_OK @@ -492,20 +486,15 @@ impl HappyEyeballs { record_type, allow_stale, }) => { - // Optimistic DNS is not wired up on the C++ side: DnsResult - // inputs are always reported fresh, so happy-eyeballs never - // schedules a revalidation query that forbids a stale answer. - debug_assert!( - allow_stale, - "optimistic DNS is not wired up on the C++ side" - ); - self.profiler.dns_query_started(id, record_type); + self.profiler + .dns_query_started(id, record_type, allow_stale); self.metrics.dns_query_started(id, record_type); let hostname: String = hostname.into(); dns_hostname.assign(hostname.as_bytes()); *ret_event = Output::SendDnsQuery { id: id.into(), record_type: record_type.into(), + allow_stale, }; } Some(happy_eyeballs::Output::Timer { duration, .. }) => { @@ -699,6 +688,10 @@ pub enum Output { SendDnsQuery { id: u64, record_type: DnsRecordType, + /// Whether the resolver may answer this query from a stale (expired) + /// cache entry. `false` for the follow-up query that revalidates a + /// stale answer, which must come from a fresh lookup. + allow_stale: bool, }, Timer { duration_ms: u64, diff --git a/netwerk/protocol/http/happy_eyeballs_glue/src/profiler.rs b/netwerk/protocol/http/happy_eyeballs_glue/src/profiler.rs index f15399d2941a..3d8eea4649cf 100644 --- a/netwerk/protocol/http/happy_eyeballs_glue/src/profiler.rs +++ b/netwerk/protocol/http/happy_eyeballs_glue/src/profiler.rs @@ -63,6 +63,11 @@ struct DnsMarker { record_type: String, outcome: Outcome, response: String, + // Optimistic DNS: whether this query was a cache-bypassing revalidation of a + // stale answer, and whether the answer it received was served from a stale + // (past-TTL, grace-period) cache entry. + revalidation: bool, + stale: bool, } impl ProfilerMarker for DnsMarker { @@ -77,6 +82,8 @@ impl ProfilerMarker for DnsMarker { schema.add_key_label_format("record_type", "Record Type", Format::UniqueString); schema.add_key_label_format("outcome", "Outcome", Format::UniqueString); schema.add_key_label_format("response", "Response", Format::SanitizedString); + schema.add_key_label_format("revalidation", "Revalidation", Format::String); + schema.add_key_label_format("stale", "Stale", Format::String); schema.add_key_label_format("flow", "Flow", Format::Flow); schema } @@ -86,6 +93,8 @@ impl ProfilerMarker for DnsMarker { json_writer.unique_string_property("record_type", &self.record_type); json_writer.unique_string_property("outcome", self.outcome.as_str()); json_writer.string_property("response", &self.response); + json_writer.bool_property("revalidation", self.revalidation); + json_writer.bool_property("stale", self.stale); json_writer.unique_string_property("flow", unsafe { std::str::from_utf8_unchecked(&self.flow.to_hex()) }); @@ -176,6 +185,7 @@ impl ProfilerMarker for LifetimeMarker { struct DnsInfo { start: ProfilerTime, record_type: happy_eyeballs::DnsRecordType, + revalidation: bool, } struct ConnInfo { @@ -277,6 +287,7 @@ impl Profiler { &mut self, id: happy_eyeballs::Id, record_type: happy_eyeballs::DnsRecordType, + allow_stale: bool, ) { if !gecko_profiler::is_active() { return; @@ -286,6 +297,7 @@ impl Profiler { DnsInfo { start: ProfilerTime::now(), record_type, + revalidation: !allow_stale, }, ); } @@ -294,6 +306,7 @@ impl Profiler { &mut self, id: happy_eyeballs::Id, addrs: &[impl std::fmt::Display], + stale: bool, ) { let Some(info) = self.dns_infos.remove(&id) else { return; @@ -312,6 +325,8 @@ impl Profiler { record_type: format!("{:?}", info.record_type), outcome: Outcome::Success, response: response.join(", "), + revalidation: info.revalidation, + stale, }, ); } @@ -320,6 +335,7 @@ impl Profiler { &mut self, id: happy_eyeballs::Id, infos: &[happy_eyeballs::ServiceInfo], + stale: bool, ) { let Some(dns_info) = self.dns_infos.remove(&id) else { return; @@ -367,6 +383,8 @@ impl Profiler { record_type: format!("{:?}", dns_info.record_type), outcome: Outcome::Success, response: response.join("; "), + revalidation: dns_info.revalidation, + stale, }, ); } @@ -448,6 +466,8 @@ impl Drop for Profiler { record_type: format!("{:?}", info.record_type), outcome: Outcome::Cancelled, response: String::new(), + revalidation: info.revalidation, + stale: false, }, ); } diff --git a/netwerk/socket/neqo_glue/src/lib.rs b/netwerk/socket/neqo_glue/src/lib.rs index a1bf3292ab7a..9727eec11403 100644 --- a/netwerk/socket/neqo_glue/src/lib.rs +++ b/netwerk/socket/neqo_glue/src/lib.rs @@ -714,6 +714,19 @@ impl NeqoHttp3Conn { }; glean::http_3_quic_version.get(version_label).add(1); + // neqo's `pto_counts` is a sliding histogram: the highest set bucket is + // the longest run of consecutive PTOs the connection saw, i.e. how deep + // it fell into a black hole. Record that run length once per connection. + let max_consecutive_ptos = i64::try_from( + stats + .pto_counts + .iter() + .rposition(|&count| count > 0) + .map_or(0, |i| i + 1), + ) + .unwrap_or(i64::MAX); + glean::http_3_max_consecutive_ptos.accumulate_single_sample_signed(max_consecutive_ptos); + if !static_prefs::pref!("network.http.http3.use_nspr_for_io") && static_prefs::pref!("network.http.http3.ecn_report") { diff --git a/netwerk/test/gtest/TestDiagnosticRWLock.cpp b/netwerk/test/gtest/TestDiagnosticRWLock.cpp deleted file mode 100644 index 22c6dbc41eee..000000000000 --- a/netwerk/test/gtest/TestDiagnosticRWLock.cpp +++ /dev/null @@ -1,46 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -// Verifies the diagnostic added in bug 2059597: when nsHostResolver's DB lock -// stays held, a blocked writer gives up after network.dns.db_lock_timeout_ms -// and crashes (naming the holder) instead of hanging forever. - -#include "gtest/gtest.h" -#include "mozilla/Preferences.h" -#include "mozilla/gtest/MozHelpers.h" -#include "nsHostResolver.h" - -// The DiagnosticRWLock mechanism only exists in diagnostic builds. Death tests -// re-exec the test binary under the threadsafe style, which has no standalone -// executable on Android, so restrict this to platforms with usable death tests. -#if defined(MOZ_DIAGNOSTIC_ASSERT_ENABLED) && !defined(ANDROID) && \ - defined(GTEST_HAS_DEATH_TEST) - -// Holding the read lock and then requesting the write lock on the same thread -// wedges it: TryWriteLock can never succeed and no further acquisition bumps -// the counter, so WriteLockOrDiagnose() must crash rather than return. -[[maybe_unused]] static void WedgeWriteLock() MOZ_NO_THREAD_SAFETY_ANALYSIS { - ZERO_GDB_SLEEP(); - mozilla::net::DiagnosticRWLock lock("test.DiagnosticRWLock"); - lock.ReadLock(); - lock.WriteLockOrDiagnose(); -} - -// The crash reason only reaches stderr (where the death matcher reads it) in -// builds that print it; elsewhere just require that the process died. -# if defined(DEBUG) || defined(MOZ_ASAN) || defined(FUZZING) -# define WEDGE_CRASH_REGEX "nsHostResolver DB lock wedged" -# else -# define WEDGE_CRASH_REGEX "" -# endif - -// The "DeathTest" suite suffix makes gtest run this before non-death tests, -// minimizing the number of live threads present at fork() time. -TEST(DiagnosticRWLockDeathTest, WedgedLockCrashes) -{ - mozilla::Preferences::SetUint("network.dns.db_lock_timeout_ms", 3000); - ASSERT_DEATH_WRAP(WedgeWriteLock(), WEDGE_CRASH_REGEX); -} - -#endif // MOZ_DIAGNOSTIC_ASSERT_ENABLED && !ANDROID && GTEST_HAS_DEATH_TEST diff --git a/netwerk/test/gtest/moz.build b/netwerk/test/gtest/moz.build index c3c40caae4fa..b49601c0d0ad 100644 --- a/netwerk/test/gtest/moz.build +++ b/netwerk/test/gtest/moz.build @@ -57,7 +57,6 @@ UNIFIED_SOURCES += [ SOURCES += [ "TestCacheCrypto.cpp", "TestCacheEntryWriterHang.cpp", - "TestDiagnosticRWLock.cpp", "TestHappyEyeballsConnectionAttempt.cpp", ] diff --git a/netwerk/test/unit/test_happy_eyeballs_optimistic_dns.js b/netwerk/test/unit/test_happy_eyeballs_optimistic_dns.js new file mode 100644 index 000000000000..fc2ffbe6e900 --- /dev/null +++ b/netwerk/test/unit/test_happy_eyeballs_optimistic_dns.js @@ -0,0 +1,166 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Optimistic DNS: an address served from a stale (past-TTL, grace-period) cache +// entry is raced right away, and Happy Eyeballs revalidates it with its own +// cache-bypassing lookup. Here the stale address refuses the connection, so the +// request can only succeed if that revalidation happens and its address is +// raced too. +// +// Serving a stale entry also makes the resolver renew it in the background, and +// that renewal alone is enough to reach the working address: it updates the +// shared host record, and a lookup that lands after it sees the new address. So +// the revalidated answer is delayed here, which keeps the renewal from +// completing before Happy Eyeballs has consumed the stale answer and failed on +// it. TRR_ONLY mode keeps native resolution, which answers 127.0.0.1 for +// everything in TRR tests, out of the picture as well. + +var { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +const { NodeHTTP2Server } = ChromeUtils.importESModule( + "resource://testing-common/NodeServer.sys.mjs" +); + +const mockController = Cc[ + "@mozilla.org/network/mock-network-controller;1" +].getService(Ci.nsIMockNetworkLayerController); + +const HOST = "optimistic-dns.example.com"; +// The stale answer. Connecting to it is blocked, and refused instantly. +const STALE_ADDR = "127.0.0.2"; +// The revalidated answer, where the HTTP/2 server listens. +const FRESH_ADDR = "127.0.0.1"; +// Lifetime of the seeded answer, and the wait that ages it into the grace +// period. The revalidated answer outlives the test instead, so that it is never +// itself served stale. +const STALE_TTL_SECONDS = 1; +const STALE_WAIT_MS = STALE_TTL_SECONDS * 1000 + 100; +const FRESH_TTL_SECONDS = 55; +// How long the revalidated answer is held back. Long enough that a lookup +// served from the stale entry cannot observe it, and well short of the TRR +// request timeout in TRR_ONLY mode. +const REVALIDATION_DELAY_MS = 1000; + +let trrServer; +let server; +let originURL; +let originPort; + +function openChan(expectFailure) { + let chan = NetUtil.newChannel({ + uri: originURL, + loadUsingSystemPrincipal: true, + contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT, + }).QueryInterface(Ci.nsIHttpChannel); + chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI; + return new Promise(resolve => { + chan.asyncOpen( + new ChannelListener( + req => resolve(req), + null, + (expectFailure ? CL_EXPECT_FAILURE : 0) | CL_ALLOW_UNKNOWN_CL + ) + ); + }); +} + +function registerAnswer(addr, ttl, delay) { + return trrServer.registerDoHAnswers(HOST, "A", { + answers: [{ name: HOST, ttl, type: "A", flush: false, data: addr }], + delay, + }); +} + +add_setup(async function setup() { + trr_test_setup(); + + Services.prefs.setBoolPref("network.http.happy_eyeballs_enabled", true); + Services.prefs.setBoolPref("network.socket.attach_mock_network_layer", true); + // TRR_ONLY: every answer then comes from the DoH server, which is what lets + // the test control when the revalidated one shows up. + Services.prefs.setIntPref("network.trr.mode", 3); + + let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u"); + + server = new NodeHTTP2Server(); + await server.start(0, [HOST]); + originPort = server.port(); + originURL = `https://${HOST}:${originPort}/`; + await server.registerPathHandler("/", (req, resp) => { + resp.writeHead(200, { "Content-Type": "text/plain" }); + resp.end("ok"); + }); + + trrServer = new TRRServer(); + await trrServer.start(); + Services.prefs.setCharPref( + "network.trr.uri", + `https://foo.example.com:${trrServer.port()}/dns-query` + ); + // A single address family keeps the stale answer and its revalidation + // unambiguous. + await trrServer.registerDoHAnswers(HOST, "AAAA", { answers: [] }); + + mockController.blockTCPConnect( + mockController.createScriptableNetAddr(STALE_ADDR, originPort) + ); + + registerCleanupFunction(async () => { + // Turn TRR off before dropping its URI, so that nothing looks up the + // default (non-local) resolver on the way out. + Services.prefs.clearUserPref("network.trr.mode"); + Services.prefs.clearUserPref("network.trr.uri"); + trr_clear_prefs(); + Services.prefs.clearUserPref("network.http.happy_eyeballs_enabled"); + Services.prefs.clearUserPref("network.socket.attach_mock_network_layer"); + mockController.clearBlockedTCPConnect(); + try { + await trrServer.stop(); + await server.stop(); + } catch (e) { + info("Error stopping servers: " + e); + } + }); +}); + +add_task(async function test_stale_answer_revalidated_and_raced() { + // Seed the cache with the address that cannot be connected to, driving the + // request through Happy Eyeballs so the entry is keyed exactly as the one the + // real request below is served from. + await registerAnswer(STALE_ADDR, STALE_TTL_SECONDS, 0); + let seed = await openChan(true); + Assert.equal( + seed.status, + Cr.NS_ERROR_CONNECTION_REFUSED, + "seeding connection to the stale address should be refused" + ); + + // From now on the name resolves to the working address, but only for a query + // that reaches the resolver, and only after a delay. + await registerAnswer(FRESH_ADDR, FRESH_TTL_SECONDS, REVALIDATION_DELAY_MS); + + // Age the seeded entry past its TTL so the next lookup is served from its + // grace period. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, STALE_WAIT_MS)); + + let req = await openChan(false); + Assert.equal( + req.QueryInterface(Ci.nsIHttpChannel).responseStatus, + 200, + "request should succeed on the revalidated address" + ); + Assert.equal( + req.QueryInterface(Ci.nsIHttpChannelInternal).remoteAddress, + FRESH_ADDR, + "should have connected to the revalidated address, not the stale one" + ); +}); diff --git a/netwerk/test/unit/xpcshell.toml b/netwerk/test/unit/xpcshell.toml index 0f6b85ad1db0..b2295a772dbc 100644 --- a/netwerk/test/unit/xpcshell.toml +++ b/netwerk/test/unit/xpcshell.toml @@ -868,6 +868,13 @@ skip-if = [ "os == 'win' && msix", ] +["test_happy_eyeballs_optimistic_dns.js"] +run-sequentially = ["true"] # node server exceptions dont replay well +skip-if = [ + "os == 'android'", + "os == 'win' && msix", +] + ["test_happy_eyeballs_tcp_fallback.js"] run-sequentially = ["true"] # node server exceptions dont replay well skip-if = [ @@ -1848,6 +1855,7 @@ run-if = [ ["test_trr_bench.js"] skip-if = [ "verify", + "os == 'android'", # intermittent ADB port exhaustion ] run-sequentially = ["true"] # We want to check performance diff --git a/python/sites/lint.txt b/python/sites/lint.txt index 4bd2ffc1c31a..bfbd93891db4 100644 --- a/python/sites/lint.txt +++ b/python/sites/lint.txt @@ -1,7 +1,7 @@ requires-python:>=3.9 pypi:Sphinx==7.1.2 pypi:alabaster==0.7.13 -pypi:codespell==2.4.2 +pypi:codespell==2.4.3 pypi:dataclasses==0.6 pypi:distlib==0.3.7 pypi:docutils==0.18.1 diff --git a/python/sites/python-test.txt b/python/sites/python-test.txt index e0c7d5b91165..2d002200691f 100644 --- a/python/sites/python-test.txt +++ b/python/sites/python-test.txt @@ -3,7 +3,7 @@ pth:testing/manifest pypi:Flask==2.1.3 pypi:Sphinx==7.1.2 pypi:alabaster==0.7.13 -pypi:codespell==2.4.2 +pypi:codespell==2.4.3 pypi:dataclasses==0.6 pypi:docutils==0.18.1 pypi:fluent.pygments==1.0 diff --git a/remote/marionette/actors/MarionetteCommandsChild.sys.mjs b/remote/marionette/actors/MarionetteCommandsChild.sys.mjs index ff780d6511d6..6616710a9aa8 100644 --- a/remote/marionette/actors/MarionetteCommandsChild.sys.mjs +++ b/remote/marionette/actors/MarionetteCommandsChild.sys.mjs @@ -601,8 +601,10 @@ export class MarionetteCommandsChild extends JSWindowActorChild { rect = new DOMRect( win.pageXOffset, win.pageYOffset, - win.innerWidth, - win.innerHeight + // Bug 2055445 made system calls to innerWidth/innerHeight return non-rounded + // values. So round them up again to keep the behavior the same as it was before. + Math.round(win.innerWidth), + Math.round(win.innerHeight) ); } diff --git a/remote/webdriver-bidi/modules/windowglobal/browsingContext.sys.mjs b/remote/webdriver-bidi/modules/windowglobal/browsingContext.sys.mjs index f67cf41751b2..21fc7bf1b0bf 100644 --- a/remote/webdriver-bidi/modules/windowglobal/browsingContext.sys.mjs +++ b/remote/webdriver-bidi/modules/windowglobal/browsingContext.sys.mjs @@ -136,8 +136,10 @@ class BrowsingContextModule extends WindowGlobalBiDiModule { return new DOMRect( viewport.pageLeft, viewport.pageTop, - win.innerWidth, - win.innerHeight + // Bug 2055445 made system calls to innerWidth/innerHeight return non-rounded + // values. So round them up again to keep the behavior the same as it was before. + Math.round(win.innerWidth), + Math.round(win.innerHeight) ); } diff --git a/security/sandbox/win/src/sandboxbroker/sandboxBroker.cpp b/security/sandbox/win/src/sandboxbroker/sandboxBroker.cpp index caa5cbf14e91..38a9e895b631 100644 --- a/security/sandbox/win/src/sandboxbroker/sandboxBroker.cpp +++ b/security/sandbox/win/src/sandboxbroker/sandboxBroker.cpp @@ -27,9 +27,8 @@ #include "mozilla/Preferences.h" #include "mozilla/SHA1.h" #include "mozilla/SandboxSettings.h" -#include "mozilla/StaticPrefs_network.h" +#include "mozilla/StaticPrefs_media.h" #include "mozilla/StaticPrefs_security.h" -#include "mozilla/StaticPrefs_widget.h" #include "mozilla/StaticPtr.h" #include "mozilla/UniquePtr.h" #include "mozilla/WinDllServices.h" @@ -1226,6 +1225,16 @@ void SandboxBroker::SetSecurityLevelForContentProcess(int32_t aSandboxLevel, // that path to fall-back to the normal loading path. config->SetForceKnownDllLoadingFallback(); + // If platform video encoding is not remote it requires the KsecDD device. + if (!StaticPrefs::media_use_remote_encoder_video_platform()) { + result = config->AllowFileAccess(sandbox::FileSemantics::kAllowAny, + LR"(\Device\KsecDD)"); + if (sandbox::SBOX_ALL_OK != result) { + NS_ERROR("Failed to add rule for KsecDD."); + LOG_E("Failed (ResultCode %d) to add read access to KsecDD", result); + } + } + // We should be able to remove access to these media registry keys below // once encoding has moved out of the content process (bug 1972552). diff --git a/services/settings/static-dumps/main/doh-config.json b/services/settings/static-dumps/main/doh-config.json index 030fa7556955..06fe8d7f7f85 100644 --- a/services/settings/static-dumps/main/doh-config.json +++ b/services/settings/static-dumps/main/doh-config.json @@ -1,15 +1,65 @@ { "data": [ { + "schema": 1750069590373, "providers": "cloudflare-global, nextdns-global", "rolloutEnabled": false, "steeringEnabled": false, "steeringProviders": "", "autoDefaultEnabled": false, "autoDefaultProviders": "", + "androidRolloutEnabled": false, "id": "global", - "last_modified": 1621943462970 + "last_modified": 1750144862239 + }, + { + "schema": 1750069547721, + "providers": "cloudflare-global, nextdns-global", + "rolloutEnabled": true, + "steeringEnabled": false, + "steeringProviders": "", + "autoDefaultEnabled": false, + "autoDefaultProviders": "", + "androidRolloutEnabled": true, + "id": "US", + "last_modified": 1750144862236 + }, + { + "schema": 1750069584709, + "providers": "cira-CA, cloudflare-global, nextdns-global", + "rolloutEnabled": true, + "steeringEnabled": true, + "steeringProviders": "shaw-CA", + "autoDefaultEnabled": false, + "autoDefaultProviders": "", + "androidRolloutEnabled": false, + "id": "CA", + "last_modified": 1750144862234 + }, + { + "schema": 1750069578221, + "providers": "cloudflare-global", + "rolloutEnabled": true, + "steeringEnabled": false, + "steeringProviders": "", + "autoDefaultEnabled": false, + "autoDefaultProviders": "", + "androidRolloutEnabled": false, + "id": "UA", + "last_modified": 1750144862232 + }, + { + "schema": 1750069572955, + "providers": "cloudflare-global", + "rolloutEnabled": true, + "steeringEnabled": false, + "steeringProviders": "", + "autoDefaultEnabled": false, + "autoDefaultProviders": "", + "androidRolloutEnabled": false, + "id": "RU", + "last_modified": 1750144862229 } ], - "timestamp": 1621943462970 + "timestamp": 1750144862239 } diff --git a/services/settings/static-dumps/main/doh-providers.json b/services/settings/static-dumps/main/doh-providers.json index b2e62b7b015d..c6f6c21b1333 100644 --- a/services/settings/static-dumps/main/doh-providers.json +++ b/services/settings/static-dumps/main/doh-providers.json @@ -1,23 +1,52 @@ { "data": [ - { - "uri": "https://firefox.dns.nextdns.io/", - "UIName": "NextDNS", - "schema": 1621819183640, - "autoDefault": false, - "canonicalName": "", - "id": "nextdns-global", - "last_modified": 1621943542621 - }, { "uri": "https://mozilla.cloudflare-dns.com/dns-query", "UIName": "Cloudflare", - "schema": 1621819221428, + "schema": 1769155849324, + "http3First": true, "autoDefault": true, "canonicalName": "", "id": "cloudflare-global", - "last_modified": 1621943542615 + "last_modified": 1769156310991 + }, + { + "uri": "https://dns.shaw.ca/dns-query", + "UIName": "Shaw", + "schema": 1647348478990, + "autoDefault": false, + "canonicalName": "dns.shaw.ca", + "id": "shaw-CA", + "last_modified": 1647549722107 + }, + { + "uri": "https://doh.xfinity.com/dns-query", + "name": "comcast", + "UIName": "", + "schema": 1634568673384, + "autoDefault": false, + "canonicalName": "doh-discovery.xfinity.com", + "id": "comcast-US", + "last_modified": 1634631885669 + }, + { + "uri": "https://firefox.dns.nextdns.io/", + "UIName": "NextDNS", + "schema": 1630593825113, + "autoDefault": false, + "canonicalName": "", + "id": "nextdns-global", + "last_modified": 1630594403007 + }, + { + "uri": "https://private.canadianshield.cira.ca/dns-query", + "UIName": "CIRA Canadian Shield", + "schema": 1625590329744, + "autoDefault": false, + "canonicalName": "", + "id": "cira-CA", + "last_modified": 1625740199826 } ], - "timestamp": 1621943542621 + "timestamp": 1769156310991 } diff --git a/services/sync/modules/addonutils.sys.mjs b/services/sync/modules/addonutils.sys.mjs index e462b30dc0ce..1809e4e29f24 100644 --- a/services/sync/modules/addonutils.sys.mjs +++ b/services/sync/modules/addonutils.sys.mjs @@ -115,7 +115,7 @@ AddonUtilsInternal.prototype = { try { addon.enable(); } catch (e) { - this._log.error("Failed to enable the incoming theme", e); + log.error("Failed to enable the incoming theme", e); } finally { // If something went wrong with enabling the theme, we don't have a good // way to retry -- so we'll clear it rather than keeping the pref around diff --git a/servo/components/style/context.rs b/servo/components/style/context.rs index 82812c7486d8..91338c49b06d 100644 --- a/servo/components/style/context.rs +++ b/servo/components/style/context.rs @@ -644,6 +644,8 @@ pub struct ThreadLocalStyleContext { pub rule_cache: RuleCache, /// The bloom filter used to fast-reject selector-matching. pub bloom_filter: StyleBloom, + /// The DOM depth of the element we're currently styling. + pub current_dom_depth: usize, /// A set of tasks to be run (on the parent thread) in sequential mode after /// the rest of the styling is complete. This is useful for /// infrequently-needed non-threadsafe operations. @@ -670,6 +672,7 @@ impl ThreadLocalStyleContext { sharing_cache: StyleSharingCache::new(), rule_cache: RuleCache::new(), bloom_filter: StyleBloom::new(), + current_dom_depth: 0, tasks: SequentialTaskList(Vec::new()), statistics: PerThreadTraversalStatistics::default(), stack_limit_checker: StackLimitChecker::new( diff --git a/servo/components/style/driver.rs b/servo/components/style/driver.rs index d46e414d489a..532491f4ef7c 100644 --- a/servo/components/style/driver.rs +++ b/servo/components/style/driver.rs @@ -12,7 +12,7 @@ use crate::context::{ThreadLocalStyleContext, TraversalStatistics}; use crate::dom::{SendNode, TElement, TNode}; use crate::parallel; use crate::scoped_tls::ScopedTLS; -use crate::traversal::{DomTraversal, PerLevelTraversalData, PreTraverseToken}; +use crate::traversal::{DomTraversal, PreTraverseToken}; use std::collections::VecDeque; use std::time::Instant; @@ -124,13 +124,14 @@ where let send_root = unsafe { SendNode::new(root.as_node()) }; with_pool_in_place_scope(work_unit_max, pool, |maybe_scope| { let mut tlc = scoped_tls.ensure(parallel::create_thread_local_context); + tlc.current_dom_depth = send_root.depth(); + let mut context = StyleContext { shared: traversal.shared_context(), thread_local: &mut tlc, }; let mut discovered = VecDeque::with_capacity(work_unit_max * 2); - let current_dom_depth = send_root.depth(); let opaque_root = send_root.opaque(); discovered.push_back(send_root); parallel::style_trees( @@ -138,7 +139,6 @@ where discovered, opaque_root, work_unit_max, - PerLevelTraversalData { current_dom_depth }, maybe_scope, traversal, &scoped_tls, diff --git a/servo/components/style/gecko/traversal.rs b/servo/components/style/gecko/traversal.rs index 89b4c51366fc..400fcd70b240 100644 --- a/servo/components/style/gecko/traversal.rs +++ b/servo/components/style/gecko/traversal.rs @@ -7,7 +7,7 @@ use crate::context::{SharedStyleContext, StyleContext}; use crate::dom::{TElement, TNode}; use crate::gecko::wrapper::{GeckoElement, GeckoNode}; -use crate::traversal::{recalc_style_at, DomTraversal, PerLevelTraversalData}; +use crate::traversal::{recalc_style_at, DomTraversal}; /// This is the simple struct that Gecko uses to encapsulate a DOM traversal for /// styling. @@ -25,7 +25,6 @@ impl<'a> RecalcStyleOnly<'a> { impl<'recalc, 'le> DomTraversal> for RecalcStyleOnly<'recalc> { fn process_preorder( &self, - traversal_data: &PerLevelTraversalData, context: &mut StyleContext>, node: GeckoNode<'le>, note_child: F, @@ -34,7 +33,7 @@ impl<'recalc, 'le> DomTraversal> for RecalcStyleOnly<'recalc> { if let Some(el) = node.as_element() { let mut data = unsafe { el.ensure_data() }; - recalc_style_at(self, traversal_data, context, el, &mut data, note_child); + recalc_style_at(self, context, el, &mut data, note_child); } } diff --git a/servo/components/style/parallel.rs b/servo/components/style/parallel.rs index 40946a8cbed5..e678819b9535 100644 --- a/servo/components/style/parallel.rs +++ b/servo/components/style/parallel.rs @@ -25,7 +25,7 @@ use crate::context::{StyleContext, ThreadLocalStyleContext}; use crate::dom::{OpaqueNode, SendNode, TElement}; use crate::scoped_tls::ScopedTLS; -use crate::traversal::{DomTraversal, PerLevelTraversalData}; +use crate::traversal::DomTraversal; use std::collections::VecDeque; /// The minimum stack size for a thread in the styling pool, in kilobytes. @@ -84,7 +84,7 @@ fn distribute_one_chunk<'a, 'scope, E, D>( items: VecDeque>, traversal_root: OpaqueNode, work_unit_max: usize, - traversal_data: PerLevelTraversalData, + dom_depth: usize, scope: &'a rayon::ScopeFifo<'scope>, traversal: &'scope D, tls: &'scope ScopedTLS<'scope, ThreadLocalStyleContext>, @@ -96,6 +96,7 @@ fn distribute_one_chunk<'a, 'scope, E, D>( #[cfg(feature = "gecko")] gecko_profiler_label!(Layout, StyleComputation); let mut tlc = tls.ensure(create_thread_local_context); + tlc.current_dom_depth = dom_depth; let mut context = StyleContext { shared: traversal.shared_context(), thread_local: &mut *tlc, @@ -105,7 +106,6 @@ fn distribute_one_chunk<'a, 'scope, E, D>( items, traversal_root, work_unit_max, - traversal_data, Some(scope), traversal, tls, @@ -118,7 +118,7 @@ fn distribute_work<'a, 'scope, E, D>( mut items: impl Iterator>, traversal_root: OpaqueNode, work_unit_max: usize, - traversal_data: PerLevelTraversalData, + dom_depth: usize, scope: &'a rayon::ScopeFifo<'scope>, traversal: &'scope D, tls: &'scope ScopedTLS<'scope, ThreadLocalStyleContext>, @@ -136,7 +136,7 @@ fn distribute_work<'a, 'scope, E, D>( chunk, traversal_root, work_unit_max, - traversal_data, + dom_depth, scope, traversal, tls, @@ -151,7 +151,6 @@ pub fn style_trees<'a, 'scope, E, D>( mut discovered: VecDeque>, traversal_root: OpaqueNode, work_unit_max: usize, - mut traversal_data: PerLevelTraversalData, scope: Option<&'a rayon::ScopeFifo<'scope>>, traversal: &'scope D, tls: &'scope ScopedTLS<'scope, ThreadLocalStyleContext>, @@ -168,7 +167,7 @@ pub fn style_trees<'a, 'scope, E, D>( let mut nodes_remaining_at_current_depth = discovered.len(); while let Some(node) = discovered.pop_front() { let mut children_to_process = 0isize; - traversal.process_preorder(&traversal_data, context, *node, |n| { + traversal.process_preorder(context, *node, |n| { children_to_process += 1; discovered.push_back(unsafe { SendNode::new(n) }); }); @@ -186,13 +185,11 @@ pub fn style_trees<'a, 'scope, E, D>( && scope.is_some() { let kept_work = std::cmp::max(nodes_remaining_at_current_depth, local_queue_size); - let mut traversal_data_copy = traversal_data.clone(); - traversal_data_copy.current_dom_depth += 1; distribute_work( discovered.range(kept_work..).cloned(), traversal_root, work_unit_max, - traversal_data_copy, + context.thread_local.current_dom_depth + 1, scope.unwrap(), traversal, tls, @@ -201,7 +198,7 @@ pub fn style_trees<'a, 'scope, E, D>( } if nodes_remaining_at_current_depth == 0 { - traversal_data.current_dom_depth += 1; + context.thread_local.current_dom_depth += 1; nodes_remaining_at_current_depth = discovered.len(); } } diff --git a/servo/components/style/sharing/mod.rs b/servo/components/style/sharing/mod.rs index 30c58d0b2812..f2c9f0482507 100644 --- a/servo/components/style/sharing/mod.rs +++ b/servo/components/style/sharing/mod.rs @@ -68,7 +68,7 @@ use crate::applicable_declarations::ApplicableDeclarationBlock; use crate::bloom::StyleBloom; use crate::computed_value_flags::ComputedValueFlags; use crate::context::{CascadeInputs, SharedStyleContext, StyleContext}; -use crate::dom::{SendElement, TElement}; +use crate::dom::{SendElement, TElement, TNode}; use crate::properties::ComputedValues; use crate::selector_map::RelevantAttributes; use crate::style_resolver::{PrimaryStyle, ResolvedElementStyles}; @@ -658,6 +658,7 @@ impl StyleSharingCache { "Inserting into cache: {:?} with parent {:?}", element, parent ); + debug_assert_eq!(element.as_node().depth(), dom_depth); let cache = self.cache_mut_at(dom_depth); if cache.dom_depth != dom_depth { @@ -715,6 +716,7 @@ impl StyleSharingCache { } let dom_depth = bloom_filter.matching_depth(); + debug_assert_eq!(target.element.as_node().depth(), dom_depth); let cache = self.cache_mut_at(dom_depth); if cache.dom_depth != dom_depth { debug!( @@ -898,6 +900,7 @@ impl StyleSharingCache { target: E, dom_depth: usize, ) -> Option { + debug_assert_eq!(target.as_node().depth(), dom_depth); if shared_context.options.disable_style_sharing_cache { return None; } diff --git a/servo/components/style/style_resolver.rs b/servo/components/style/style_resolver.rs index 7ea09de3acf2..17a0314bd16c 100644 --- a/servo/components/style/style_resolver.rs +++ b/servo/components/style/style_resolver.rs @@ -8,7 +8,7 @@ use crate::applicable_declarations::ApplicableDeclarationList; use crate::computed_value_flags::ComputedValueFlags; use crate::context::{CascadeInputs, ElementCascadeInputs, StyleContext}; use crate::data::{EagerPseudoStyles, ElementStyles}; -use crate::dom::TElement; +use crate::dom::{TElement, TNode}; use crate::matching::MatchMethods; use crate::properties::longhands::display::computed_value::T as Display; use crate::properties::{ComputedValues, FirstLineReparenting}; @@ -173,6 +173,7 @@ where rule_inclusion: RuleInclusion, pseudo_resolution: PseudoElementResolution, ) -> Self { + debug_assert_eq!(element.as_node().depth(), context.thread_local.current_dom_depth); Self { element, context, @@ -228,13 +229,12 @@ where && inputs.included_cascade_flags.is_empty(); if may_reuse { - let dom_depth = self.context.thread_local.bloom_filter.matching_depth(); let cached = self.context.thread_local.sharing_cache.lookup_by_rules( self.context.shared, parent_style.unwrap(), &inputs, self.element, - dom_depth, + self.context.thread_local.current_dom_depth, ); if let Some(mut primary_style) = cached { self.context.thread_local.statistics.styles_reused += 1; diff --git a/servo/components/style/traversal.rs b/servo/components/style/traversal.rs index e073776a9f03..0f821e23c294 100644 --- a/servo/components/style/traversal.rs +++ b/servo/components/style/traversal.rs @@ -24,21 +24,8 @@ use std::collections::HashMap; pub type UndisplayedStyleCache = HashMap>; -/// A per-traversal-level chunk of data. This is sent down by the traversal, and -/// currently only holds the dom depth for the bloom filter. -/// -/// NB: Keep this as small as possible, please! -#[derive(Clone, Copy, Debug)] -pub struct PerLevelTraversalData { - /// The current dom depth. - /// - /// This is kept with cooperation from the traversal code and the bloom - /// filter. - pub current_dom_depth: usize, -} - /// We use this structure, rather than just returning a boolean from pre_traverse, -/// to enfore that callers process root invalidations before starting the traversal. +/// to enforce that callers process root invalidations before starting the traversal. pub struct PreTraverseToken(Option); impl PreTraverseToken { /// Whether we should traverse children. @@ -61,7 +48,6 @@ pub trait DomTraversal: Sync { /// the traversal. fn process_preorder( &self, - data: &PerLevelTraversalData, context: &mut StyleContext, node: E::ConcreteNode, note_child: F, @@ -305,6 +291,7 @@ where for ancestor in ancestors_requiring_style_resolution.iter().rev() { context.thread_local.bloom_filter.assert_complete(*ancestor); + context.thread_local.current_dom_depth = context.thread_local.bloom_filter.matching_depth(); // Actually `PseudoElementResolution` doesn't really matter here. // (but it does matter below!). @@ -330,6 +317,7 @@ where } context.thread_local.bloom_filter.assert_complete(element); + context.thread_local.current_dom_depth = context.thread_local.bloom_filter.matching_depth(); let styles: ElementStyles = StyleResolverForElement::new( element, context, @@ -351,7 +339,6 @@ where #[allow(unsafe_code)] pub fn recalc_style_at( _traversal: &D, - traversal_data: &PerLevelTraversalData, context: &mut StyleContext, element: E, data: &mut ElementData, @@ -386,7 +373,7 @@ pub fn recalc_style_at( // Compute style for this element if necessary. if let Some(restyle_kind) = restyle_kind { - child_restyle_hint = compute_style(traversal_data, context, element, data, restyle_kind); + child_restyle_hint = compute_style(context, element, data, restyle_kind); if !element.matches_user_and_content_rules() { // We must always cascade native anonymous subtrees, since they @@ -493,7 +480,6 @@ where } fn compute_style( - traversal_data: &PerLevelTraversalData, context: &mut StyleContext, element: E, data: &mut ElementData, @@ -522,12 +508,12 @@ where context .thread_local .bloom_filter - .insert_parents_recovering(element, traversal_data.current_dom_depth); + .insert_parents_recovering(element, context.thread_local.current_dom_depth); context.thread_local.bloom_filter.assert_complete(element); debug_assert_eq!( context.thread_local.bloom_filter.matching_depth(), - traversal_data.current_dom_depth + context.thread_local.current_dom_depth ); // This is only relevant for animations as of right now. @@ -556,11 +542,12 @@ where resolver.resolve_style_with_default_parents() }; + let dom_depth = context.thread_local.current_dom_depth; context.thread_local.sharing_cache.insert_if_possible( &element, &new_styles.primary, Some(&mut target), - traversal_data.current_dom_depth, + dom_depth, &context.shared, ); @@ -617,7 +604,7 @@ where &element, &new_styles.primary, None, - traversal_data.current_dom_depth, + context.thread_local.current_dom_depth, &context.shared, ); } diff --git a/servo/ports/geckolib/glue.rs b/servo/ports/geckolib/glue.rs index 46bad54c51b7..6f4fb60cbc17 100644 --- a/servo/ports/geckolib/glue.rs +++ b/servo/ports/geckolib/glue.rs @@ -1144,6 +1144,7 @@ pub extern "C" fn Servo_StyleSet_GetBaseComputedValuesForElement( unsafe { &*snapshots }, ); let mut tlc = ThreadLocalStyleContext::new(); + tlc.current_dom_depth = element.as_node().depth(); let context = StyleContext { shared: &shared, thread_local: &mut tlc, diff --git a/taskcluster/gecko_taskgraph/target_tasks.py b/taskcluster/gecko_taskgraph/target_tasks.py index 3fb7fdcf00fb..9cda36a039ee 100644 --- a/taskcluster/gecko_taskgraph/target_tasks.py +++ b/taskcluster/gecko_taskgraph/target_tasks.py @@ -1745,8 +1745,7 @@ def retrigger_perftests_autoland_commits(full_task_graph, parameters, graph_conf - "perftest-android-hw-a55-aarch64-shippable-startup-fenix-cold-view-nav-start", - "perftest-android-hw-a55-aarch64-shippable-startup-fenix-homeview-startup", - "perftest-android-hw-a55-aarch64-shippable-startup-fenix-newssite-applink-startup", - - "perftest-android-hw-a55-aarch64-shippable-startup-fenix-shopify-applink-startup", - - "perftest-android-hw-a55-aarch64-shippable-startup-fenix-tab-restore-shopify" + - "perftest-android-hw-a55-aarch64-shippable-startup-fenix-tab-restore-newssite" - "test-windows11-64-24h2-shippable/opt-browsertime-benchmark-firefox-speedometer3", """ retrigger_count = 4 diff --git a/taskcluster/kinds/perftest/android.yml b/taskcluster/kinds/perftest/android.yml index fd4d483bbebe..481fd5a1f9fb 100644 --- a/taskcluster/kinds/perftest/android.yml +++ b/taskcluster/kinds/perftest/android.yml @@ -208,7 +208,7 @@ hw-a55-aarch64-shippable-startup-fenix-shopify-applink-startup: platform: android-hw-a55-14-0-aarch64-shippable/opt attributes: cron: false - run-on-projects: [autoland] + run-on-projects: [] fetches: build: - artifact: target.arm64-v8a.apk @@ -1089,7 +1089,7 @@ hw-a55-record-websites: testing/performance/perftest_record.js hw-a55-background-resource-fenix: - worker-type: t-bitbar-gw-perf-a55 + worker-type: t-lambda-perf-a55 run-on-projects: [trunk-only] description: Run background resource test with Fenix on a Samsung A55 treeherder: @@ -1120,7 +1120,7 @@ hw-a55-background-resource-fenix: --hooks testing/performance/android-resource/hooks_android_resource.py hw-a55-background-resource-chrome: - worker-type: t-bitbar-gw-perf-a55 + worker-type: t-lambda-perf-a55 run-on-projects: [mozilla-central] description: Run background CPU test with Chrome on a Samsung A55 treeherder: @@ -1145,7 +1145,7 @@ hw-a55-background-resource-chrome: --hooks testing/performance/android-resource/hooks_android_resource.py hw-a55-foreground-resource-fenix: - worker-type: t-bitbar-gw-perf-a55 + worker-type: t-lambda-perf-a55 run-on-projects: [trunk-only] description: Run background resource test with Fenix on a Samsung A55 treeherder: @@ -1176,7 +1176,7 @@ hw-a55-foreground-resource-fenix: --hooks testing/performance/android-resource/hooks_android_resource.py hw-a55-foreground-resource-chrome: - worker-type: t-bitbar-gw-perf-a55 + worker-type: t-lambda-perf-a55 run-on-projects: [mozilla-central] description: Run background CPU test with Chrome on a Samsung A55 treeherder: diff --git a/testing/mozharness/mozharness/mozilla/testing/per_test_base.py b/testing/mozharness/mozharness/mozilla/testing/per_test_base.py index 1ead31a62b06..576901bb0557 100644 --- a/testing/mozharness/mozharness/mozilla/testing/per_test_base.py +++ b/testing/mozharness/mozharness/mozilla/testing/per_test_base.py @@ -27,6 +27,8 @@ class SingleTestMixin: # Use self._map_test_path_to_source(test_machine_path, source_path) to add a mapping. self.test_src_path = {} self.per_test_log_index = 1 + # Path of the testsummary log for the harness run currently being set up. + self.test_summary_file = None def _map_test_path_to_source(self, test_machine_path, source_path): test_machine_path = test_machine_path.replace(os.sep, posixpath.sep) @@ -595,5 +597,30 @@ class SingleTestMixin: error_summary_file = os.path.join( dir, "%s%s_errorsummary.log" % (test_suite, index) ) - test_summary_file = os.path.join(dir, "summary.jsonl") + test_summary_file = os.path.join( + dir, "%s%s_testsummary.jsonl" % (test_suite, index) + ) + self.test_summary_file = test_summary_file return raw_log_file, error_summary_file, test_summary_file + + def append_test_summary(self, dir): + """ + Append the last harness run's testsummary log to the task's summary.jsonl. + + Each harness invocation truncates the file it was given, so the per-run + logs are concatenated into a single fixed-name artifact. Suites that do + not pass --log-testsummary produce no file, in which case this is a no-op. + """ + part = self.test_summary_file + self.test_summary_file = None + if not part or not os.path.exists(part): + return + with open(part, encoding="utf-8") as fh: + contents = fh.read() + os.remove(part) + if not contents: + return + if not contents.endswith("\n"): + contents += "\n" + with open(os.path.join(dir, "summary.jsonl"), "a", encoding="utf-8") as fh: + fh.write(contents) diff --git a/testing/mozharness/scripts/android_emulator_unittest.py b/testing/mozharness/scripts/android_emulator_unittest.py index b9997b51be4f..0c377b369bdd 100644 --- a/testing/mozharness/scripts/android_emulator_unittest.py +++ b/testing/mozharness/scripts/android_emulator_unittest.py @@ -582,6 +582,7 @@ class AndroidEmulatorTest( parser.formatter = ref_formatter.ReftestFormatter() self.run_command(final_cmd, cwd=cwd, env=env, output_parser=parser) + self.append_test_summary(self.query_abs_dirs()["abs_blob_upload_dir"]) tbpl_status, log_level, summary = parser.evaluate_parser( 0, previous_summary=summary ) diff --git a/testing/mozharness/scripts/android_hardware_unittest.py b/testing/mozharness/scripts/android_hardware_unittest.py index 0e6cfd36e134..27b4c6cb918d 100644 --- a/testing/mozharness/scripts/android_hardware_unittest.py +++ b/testing/mozharness/scripts/android_hardware_unittest.py @@ -506,6 +506,7 @@ class AndroidHardwareTest( parser.formatter = ref_formatter.ReftestFormatter() self.run_command(final_cmd, cwd=cwd, env=env, output_parser=parser) + self.append_test_summary(self.query_abs_dirs()["abs_blob_upload_dir"]) tbpl_status, log_level, summary = parser.evaluate_parser(0, summary) parser.append_tinderboxprint_line(self.test_suite) diff --git a/testing/mozharness/scripts/desktop_unittest.py b/testing/mozharness/scripts/desktop_unittest.py index cb26dfe01b82..fc59f78c40fb 100755 --- a/testing/mozharness/scripts/desktop_unittest.py +++ b/testing/mozharness/scripts/desktop_unittest.py @@ -1472,6 +1472,8 @@ class DesktopUnittest(TestingMixin, MercurialScript, MozbaseMixin, CodeCoverageM env=final_env, ) + self.append_test_summary(dirs["abs_blob_upload_dir"]) + if self.per_test_coverage: self.add_per_test_coverage_report( final_env, suite, per_test_args[-1] diff --git a/testing/mozharness/test/python.toml b/testing/mozharness/test/python.toml index 99549e0f9c8a..7dab26e5c189 100644 --- a/testing/mozharness/test/python.toml +++ b/testing/mozharness/test/python.toml @@ -21,4 +21,6 @@ subsuite = "mozharness" ["test_mozilla_merkle.py"] +["test_mozilla_per_test_base.py"] + ["test_mozilla_structured.py"] diff --git a/testing/mozharness/test/test_mozilla_per_test_base.py b/testing/mozharness/test/test_mozilla_per_test_base.py new file mode 100644 index 000000000000..c4a519893d46 --- /dev/null +++ b/testing/mozharness/test/test_mozilla_per_test_base.py @@ -0,0 +1,111 @@ +import json +import os +import shutil +import tempfile +import unittest + +import mozunit +from mozharness.mozilla.testing.per_test_base import SingleTestMixin + + +class Harness(SingleTestMixin): + """Minimal host for the mixin: get_indexed_logs() reads these two flags.""" + + def __init__(self, verify_enabled=False, per_test_coverage=False): + super().__init__() + self.verify_enabled = verify_enabled + self.per_test_coverage = per_test_coverage + + +class TestTestSummaryLogs(unittest.TestCase): + def setUp(self): + self.dir = tempfile.mkdtemp(suffix=".mozharness") + + def tearDown(self): + shutil.rmtree(self.dir, ignore_errors=True) + + @property + def summary(self): + return os.path.join(self.dir, "summary.jsonl") + + def write_part(self, harness, contents): + with open(harness.test_summary_file, "w", encoding="utf-8") as fh: + fh.write(contents) + + def read_summary(self): + with open(self.summary, encoding="utf-8") as fh: + return fh.read() + + def test_verify_mode_gives_each_run_a_distinct_summary(self): + harness = Harness(verify_enabled=True) + _, _, first = harness.get_indexed_logs(self.dir, "mochitest-plain") + _, _, second = harness.get_indexed_logs(self.dir, "mochitest-plain") + + self.assertEqual( + os.path.basename(first), "mochitest-plain-test1_testsummary.jsonl" + ) + self.assertEqual( + os.path.basename(second), "mochitest-plain-test2_testsummary.jsonl" + ) + + def test_summary_is_unindexed_outside_verify_mode(self): + harness = Harness() + _, _, path = harness.get_indexed_logs(self.dir, "xpcshell") + + self.assertEqual(os.path.basename(path), "xpcshell_testsummary.jsonl") + self.assertEqual(harness.test_summary_file, path) + + def test_parts_are_concatenated_in_order_and_removed(self): + harness = Harness(verify_enabled=True) + + harness.get_indexed_logs(self.dir, "xpcshell") + first = harness.test_summary_file + self.write_part(harness, '{"action": "test_start", "test": "a"}\n') + harness.append_test_summary(self.dir) + + harness.get_indexed_logs(self.dir, "xpcshell") + second = harness.test_summary_file + self.write_part(harness, '{"action": "test_start", "test": "b"}\n') + harness.append_test_summary(self.dir) + + tests = [json.loads(line)["test"] for line in self.read_summary().splitlines()] + self.assertEqual(tests, ["a", "b"]) + self.assertFalse(os.path.exists(first)) + self.assertFalse(os.path.exists(second)) + self.assertIsNone(harness.test_summary_file) + + def test_missing_part_is_a_noop(self): + harness = Harness() + harness.get_indexed_logs(self.dir, "reftest") + + harness.append_test_summary(self.dir) + + self.assertFalse(os.path.exists(self.summary)) + self.assertIsNone(harness.test_summary_file) + + def test_part_without_trailing_newline_does_not_glue(self): + harness = Harness(verify_enabled=True) + + harness.get_indexed_logs(self.dir, "mochitest-plain") + self.write_part(harness, '{"action": "test_start", "test": "a"}') + harness.append_test_summary(self.dir) + + harness.get_indexed_logs(self.dir, "mochitest-plain") + self.write_part(harness, '{"action": "test_start", "test": "b"}\n') + harness.append_test_summary(self.dir) + + lines = self.read_summary().splitlines() + self.assertEqual([json.loads(line)["test"] for line in lines], ["a", "b"]) + + def test_empty_part_does_not_create_a_summary(self): + harness = Harness() + harness.get_indexed_logs(self.dir, "xpcshell") + self.write_part(harness, "") + + harness.append_test_summary(self.dir) + + self.assertFalse(os.path.exists(self.summary)) + + +if __name__ == "__main__": + mozunit.main() diff --git a/testing/performance/mobile-startup/cvne-newssite.sh b/testing/performance/mobile-startup/cvne-newssite.sh index f53b79159fda..1bad421b9dfe 100755 --- a/testing/performance/mobile-startup/cvne-newssite.sh +++ b/testing/performance/mobile-startup/cvne-newssite.sh @@ -5,76 +5,12 @@ #description: Runs the newssite applink startup(cvne) test for chrome/fenix SCRIPT_PATH="testing/performance/mobile-startup/android_startup_videoapplink.py" -CA_PEM="netwerk/test/unit/http2-ca.pem" -SERVER_CERT="testing/raptor/browsertime/utils/http2-cert.pem" -SERVER_KEY="testing/raptor/browsertime/utils/http2-cert.key" -SERVER_SCRIPT="testing/performance/mobile-startup/http2-server.js" -SITE_DIR="testing/performance/mobile-startup/newssite-nuxt" -# Add fetched node to PATH (CI provides it via linux64-node). -if [ -d "${MOZ_FETCHES_DIR}/node/bin" ]; then - export PATH="${MOZ_FETCHES_DIR}/node/bin:${PATH}" -fi +source testing/performance/mobile-startup/newssite-setup.sh -# Probe for root: try su -c first, then check if adb -# is already running as root (emulators / adb root). -if adb shell su -c 'id' >/dev/null 2>&1; then - SHELL_CMD="adb shell su -c" - HAS_ROOT=1 -elif [ "$(adb shell id -u 2>/dev/null | tr -d '\r')" = "0" ]; then - SHELL_CMD="adb shell" - HAS_ROOT=1 -else - HAS_ROOT=0 -fi - -if [ "$HAS_ROOT" = "1" ]; then - # Install the test CA so Chrome trusts the server cert (Chrome reads - # user-installed CAs from cacerts-added natively). - # The filename must be the subject hash of the CA cert with a .0 suffix. - CA_HASH=$(openssl x509 -subject_hash -noout -in $CA_PEM) - adb push $CA_PEM /sdcard/Download/ca.pem - $SHELL_CMD 'mkdir -p /data/misc/user/0/cacerts-added' - $SHELL_CMD "cp /sdcard/Download/ca.pem /data/misc/user/0/cacerts-added/${CA_HASH}.0" - $SHELL_CMD "chown system:system /data/misc/user/0/cacerts-added/${CA_HASH}.0" - $SHELL_CMD "chmod 644 /data/misc/user/0/cacerts-added/${CA_HASH}.0" - - # Start HTTP/2 server with TLS. - node $SERVER_SCRIPT $SITE_DIR $SERVER_CERT $SERVER_KEY \ - > $TESTING_DIR/server.log 2>&1 & - SERVER_PID=$! - sleep 2 - if ! kill -0 $SERVER_PID 2>/dev/null; then - echo "ERROR: HTTP/2 server failed to start (PID $SERVER_PID). Server log:" - cat $TESTING_DIR/server.log - exit 1 - fi - TEST_URL="https://localhost:8000" - echo "HTTP/2 TLS server started with PID $SERVER_PID" -else - # No root: plain HTTP. - $PYTHON_PATH_SHELL_SCRIPT -m http.server \ - --directory $SITE_DIR \ - > $TESTING_DIR/server.log 2>&1 & - SERVER_PID=$! - sleep 2 - if ! kill -0 $SERVER_PID 2>/dev/null; then - echo "ERROR: HTTP server failed to start (PID $SERVER_PID). Server log:" - cat $TESTING_DIR/server.log - exit 1 - fi - TEST_URL="http://localhost:8000" - echo "HTTP server started with PID $SERVER_PID" -fi - -# Reroute localhost:8000 on the device to the host. -adb reverse tcp:8000 tcp:8000 +start_newssite_server # Run the Python script $PYTHON_PATH_SHELL_SCRIPT $SCRIPT_PATH $APP cold_view_nav_end $TEST_URL -# Remove all reverse rules -adb reverse --remove-all - -# Kill server -kill $SERVER_PID +stop_newssite_server diff --git a/testing/performance/mobile-startup/expected_startup_screenshots/chrome-m-mobile_restore-a55.png b/testing/performance/mobile-startup/expected_startup_screenshots/chrome-m-mobile_restore-a55.png index d8113aaef346..8f1da41ab343 100644 Binary files a/testing/performance/mobile-startup/expected_startup_screenshots/chrome-m-mobile_restore-a55.png and b/testing/performance/mobile-startup/expected_startup_screenshots/chrome-m-mobile_restore-a55.png differ diff --git a/testing/performance/mobile-startup/expected_startup_screenshots/chrome-m-mobile_restore-p6.png b/testing/performance/mobile-startup/expected_startup_screenshots/chrome-m-mobile_restore-p6.png index d1f1a4111155..ff203140b693 100644 Binary files a/testing/performance/mobile-startup/expected_startup_screenshots/chrome-m-mobile_restore-p6.png and b/testing/performance/mobile-startup/expected_startup_screenshots/chrome-m-mobile_restore-p6.png differ diff --git a/testing/performance/mobile-startup/expected_startup_screenshots/chrome-m-mobile_restore-s24.png b/testing/performance/mobile-startup/expected_startup_screenshots/chrome-m-mobile_restore-s24.png index 83b9c76453f4..09d7da3734aa 100644 Binary files a/testing/performance/mobile-startup/expected_startup_screenshots/chrome-m-mobile_restore-s24.png and b/testing/performance/mobile-startup/expected_startup_screenshots/chrome-m-mobile_restore-s24.png differ diff --git a/testing/performance/mobile-startup/expected_startup_screenshots/fenix-mobile_restore-a55.png b/testing/performance/mobile-startup/expected_startup_screenshots/fenix-mobile_restore-a55.png index fc49e4209842..c89b1a05ea38 100644 Binary files a/testing/performance/mobile-startup/expected_startup_screenshots/fenix-mobile_restore-a55.png and b/testing/performance/mobile-startup/expected_startup_screenshots/fenix-mobile_restore-a55.png differ diff --git a/testing/performance/mobile-startup/expected_startup_screenshots/fenix-mobile_restore-p6.png b/testing/performance/mobile-startup/expected_startup_screenshots/fenix-mobile_restore-p6.png index 1904edfc1d0a..07f1f1b3f282 100644 Binary files a/testing/performance/mobile-startup/expected_startup_screenshots/fenix-mobile_restore-p6.png and b/testing/performance/mobile-startup/expected_startup_screenshots/fenix-mobile_restore-p6.png differ diff --git a/testing/performance/mobile-startup/expected_startup_screenshots/fenix-mobile_restore-s24.png b/testing/performance/mobile-startup/expected_startup_screenshots/fenix-mobile_restore-s24.png index af5db3333050..4be868bedb07 100644 Binary files a/testing/performance/mobile-startup/expected_startup_screenshots/fenix-mobile_restore-s24.png and b/testing/performance/mobile-startup/expected_startup_screenshots/fenix-mobile_restore-s24.png differ diff --git a/testing/performance/mobile-startup/newssite-setup.sh b/testing/performance/mobile-startup/newssite-setup.sh new file mode 100644 index 000000000000..ce2a7156ba8e --- /dev/null +++ b/testing/performance/mobile-startup/newssite-setup.sh @@ -0,0 +1,80 @@ +#!/bin/bash + +# Shared newssite server setup for the mobile startup tests. This is meant to be +# sourced, not executed: `start_newssite_server` sets SERVER_PID and TEST_URL for +# the caller, and `stop_newssite_server` tears everything down. + +CA_PEM="netwerk/test/unit/http2-ca.pem" +SERVER_CERT="testing/raptor/browsertime/utils/http2-cert.pem" +SERVER_KEY="testing/raptor/browsertime/utils/http2-cert.key" +SERVER_SCRIPT="testing/performance/mobile-startup/http2-server.js" +SITE_DIR="testing/performance/mobile-startup/newssite-nuxt" + +start_newssite_server() { + # Add fetched node to PATH (CI provides it via linux64-node). + if [ -d "${MOZ_FETCHES_DIR}/node/bin" ]; then + export PATH="${MOZ_FETCHES_DIR}/node/bin:${PATH}" + fi + + # Probe for root: try su -c first, then check if adb + # is already running as root (emulators / adb root). + if adb shell su -c 'id' >/dev/null 2>&1; then + SHELL_CMD="adb shell su -c" + HAS_ROOT=1 + elif [ "$(adb shell id -u 2>/dev/null | tr -d '\r')" = "0" ]; then + SHELL_CMD="adb shell" + HAS_ROOT=1 + else + HAS_ROOT=0 + fi + + if [ "$HAS_ROOT" = "1" ]; then + # Install the test CA so Chrome trusts the server cert (Chrome reads + # user-installed CAs from cacerts-added natively). + # The filename must be the subject hash of the CA cert with a .0 suffix. + CA_HASH=$(openssl x509 -subject_hash -noout -in $CA_PEM) + adb push $CA_PEM /sdcard/Download/ca.pem + $SHELL_CMD 'mkdir -p /data/misc/user/0/cacerts-added' + $SHELL_CMD "cp /sdcard/Download/ca.pem /data/misc/user/0/cacerts-added/${CA_HASH}.0" + $SHELL_CMD "chown system:system /data/misc/user/0/cacerts-added/${CA_HASH}.0" + $SHELL_CMD "chmod 644 /data/misc/user/0/cacerts-added/${CA_HASH}.0" + + # Start HTTP/2 server with TLS. + node $SERVER_SCRIPT $SITE_DIR $SERVER_CERT $SERVER_KEY \ + > $TESTING_DIR/server.log 2>&1 & + SERVER_PID=$! + sleep 2 + if ! kill -0 $SERVER_PID 2>/dev/null; then + echo "ERROR: HTTP/2 server failed to start (PID $SERVER_PID). Server log:" + cat $TESTING_DIR/server.log + exit 1 + fi + TEST_URL="https://localhost:8000" + echo "HTTP/2 TLS server started with PID $SERVER_PID" + else + # No root: plain HTTP. + $PYTHON_PATH_SHELL_SCRIPT -m http.server \ + --directory $SITE_DIR \ + > $TESTING_DIR/server.log 2>&1 & + SERVER_PID=$! + sleep 2 + if ! kill -0 $SERVER_PID 2>/dev/null; then + echo "ERROR: HTTP server failed to start (PID $SERVER_PID). Server log:" + cat $TESTING_DIR/server.log + exit 1 + fi + TEST_URL="http://localhost:8000" + echo "HTTP server started with PID $SERVER_PID" + fi + + # Reroute localhost:8000 on the device to the host. + adb reverse tcp:8000 tcp:8000 +} + +stop_newssite_server() { + # Remove all reverse rules + adb reverse --remove-all + + # Kill server + kill $SERVER_PID +} diff --git a/testing/performance/mobile-startup/restore.sh b/testing/performance/mobile-startup/restore.sh index f43db88c4e19..473f912dccce 100755 --- a/testing/performance/mobile-startup/restore.sh +++ b/testing/performance/mobile-startup/restore.sh @@ -1,11 +1,16 @@ #!/bin/bash -#name: tab-restore-shopify +#name: tab-restore-newssite #owner: perftest -#description: Runs the shopify mobile restore test for chrome/fenix +#description: Runs the newssite mobile restore test for chrome/fenix -# Path to the Python script SCRIPT_PATH="testing/performance/mobile-startup/android_startup_videoapplink.py" +source testing/performance/mobile-startup/newssite-setup.sh + +start_newssite_server + # Run the Python script -$PYTHON_PATH_SHELL_SCRIPT $SCRIPT_PATH $APP mobile_restore https://theme-crave-demo.myshopify.com +$PYTHON_PATH_SHELL_SCRIPT $SCRIPT_PATH $APP mobile_restore $TEST_URL + +stop_newssite_server diff --git a/testing/profiles/common/user.js b/testing/profiles/common/user.js index aa509e9b483c..9181e2513a9d 100644 --- a/testing/profiles/common/user.js +++ b/testing/profiles/common/user.js @@ -30,6 +30,16 @@ user_pref("browser.preonboarding.enabled", false); // Tell the search service we are running in the US. This also has the desired // side-effect of preventing our geoip lookup. user_pref("browser.search.region", "US"); +// The shipped doh-config dump enables the DoH rollout in the US, so without +// this tests would run heuristics and possibly switch to TRR mode 2 midway. +// This pref takes priority over the Remote Settings config. DoH's own tests +// clear it and drive the config themselves. +user_pref("doh-rollout.enabled", false); +// The shipped doh-providers dump marks Cloudflare as http3First, so on Nightly +// TRR would attempt HTTP/3 against the DoH endpoint. Tests that exercise TRR +// override that endpoint to a local address where nothing speaks HTTP/3, and +// the attempt only ends when the request times out. +user_pref("network.trr.allow_default_http3_first", false); // disable infobar for tests user_pref("browser.search.removeEngineInfobar.enabled", false); // We do not wish to display datareporting policy notifications as it might diff --git a/testing/web-platform/meta/clipboard-apis/permissions-policy/clipboard-read/clipboard-read-enabled-on-self-origin-by-permissions-policy.tentative.https.sub.html.ini b/testing/web-platform/meta/clipboard-apis/permissions-policy/clipboard-read/clipboard-read-enabled-on-self-origin-by-permissions-policy.tentative.https.sub.html.ini index ca033c71b4f8..837ecadfd035 100644 --- a/testing/web-platform/meta/clipboard-apis/permissions-policy/clipboard-read/clipboard-read-enabled-on-self-origin-by-permissions-policy.tentative.https.sub.html.ini +++ b/testing/web-platform/meta/clipboard-apis/permissions-policy/clipboard-read/clipboard-read-enabled-on-self-origin-by-permissions-policy.tentative.https.sub.html.ini @@ -1,8 +1,8 @@ [clipboard-read-enabled-on-self-origin-by-permissions-policy.tentative.https.sub.html] expected: - if (os == "linux"): [TIMEOUT, OK] - if os == "win": TIMEOUT - if os == "mac": TIMEOUT + if os == "linux": [TIMEOUT, OK] + if os == "android": OK + TIMEOUT [Permissions-Policy header clipboard-read=self allows the top-level document.] expected: if (os == "linux"): [TIMEOUT, FAIL] diff --git a/testing/web-platform/meta/css/css-backgrounds/border-image-width-007.xht.ini b/testing/web-platform/meta/css/css-backgrounds/border-image-width-007.xht.ini index 4f81dea584a6..983e5b92ac8c 100644 --- a/testing/web-platform/meta/css/css-backgrounds/border-image-width-007.xht.ini +++ b/testing/web-platform/meta/css/css-backgrounds/border-image-width-007.xht.ini @@ -1,3 +1,3 @@ [border-image-width-007.xht] disabled: - if (os == "android"): bug 1550895 (frequently fails on geckoview) + if os == "android": bug 1550895 (frequently fails on geckoview) diff --git a/testing/web-platform/meta/css/css-borders/corner-shape/corner-shape-squircle.html.ini b/testing/web-platform/meta/css/css-borders/corner-shape/corner-shape-squircle.html.ini new file mode 100644 index 000000000000..82f3fc2d1879 --- /dev/null +++ b/testing/web-platform/meta/css/css-borders/corner-shape/corner-shape-squircle.html.ini @@ -0,0 +1,2 @@ +[corner-shape-squircle.html] + expected: FAIL diff --git a/testing/web-platform/meta/css/css-borders/corner-shape/corner-shape-superellipse-concave.html.ini b/testing/web-platform/meta/css/css-borders/corner-shape/corner-shape-superellipse-concave.html.ini new file mode 100644 index 000000000000..8be0db6efc46 --- /dev/null +++ b/testing/web-platform/meta/css/css-borders/corner-shape/corner-shape-superellipse-concave.html.ini @@ -0,0 +1,2 @@ +[corner-shape-superellipse-concave.html] + expected: FAIL diff --git a/testing/web-platform/meta/css/css-borders/corner-shape/corner-shape-superellipse-convex.html.ini b/testing/web-platform/meta/css/css-borders/corner-shape/corner-shape-superellipse-convex.html.ini new file mode 100644 index 000000000000..0450c32f626a --- /dev/null +++ b/testing/web-platform/meta/css/css-borders/corner-shape/corner-shape-superellipse-convex.html.ini @@ -0,0 +1,2 @@ +[corner-shape-superellipse-convex.html] + expected: FAIL diff --git a/testing/web-platform/meta/css/css-borders/corner-shape/corner-shape-superellipse-squircle.html.ini b/testing/web-platform/meta/css/css-borders/corner-shape/corner-shape-superellipse-squircle.html.ini new file mode 100644 index 000000000000..39d05637c013 --- /dev/null +++ b/testing/web-platform/meta/css/css-borders/corner-shape/corner-shape-superellipse-squircle.html.ini @@ -0,0 +1,2 @@ +[corner-shape-superellipse-squircle.html] + expected: FAIL diff --git a/testing/web-platform/meta/css/css-cascade/revert-rule-cycle.tentative.html.ini b/testing/web-platform/meta/css/css-cascade/revert-rule-cycle.tentative.html.ini new file mode 100644 index 000000000000..c1eaf536e896 --- /dev/null +++ b/testing/web-platform/meta/css/css-cascade/revert-rule-cycle.tentative.html.ini @@ -0,0 +1,6 @@ +[revert-rule-cycle.tentative.html] + [Cycle between revert-rule !important and revert-layer resolves to unset instead of lower layers] + expected: FAIL + + [Multi-layer chain cycle with revert-rule !important resolves to unset instead of lower layers] + expected: FAIL diff --git a/testing/web-platform/meta/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-align-content-001.html.ini b/testing/web-platform/meta/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-align-content-001.html.ini new file mode 100644 index 000000000000..ece847f01179 --- /dev/null +++ b/testing/web-platform/meta/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-align-content-001.html.ini @@ -0,0 +1,2 @@ +[column-grid-lanes-oof-align-content-001.html] + expected: FAIL diff --git a/testing/web-platform/meta/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-align-content-002.html.ini b/testing/web-platform/meta/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-align-content-002.html.ini new file mode 100644 index 000000000000..5a0c1e41c060 --- /dev/null +++ b/testing/web-platform/meta/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-align-content-002.html.ini @@ -0,0 +1,2 @@ +[column-grid-lanes-oof-align-content-002.html] + expected: FAIL diff --git a/testing/web-platform/meta/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-align-content-003.html.ini b/testing/web-platform/meta/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-align-content-003.html.ini new file mode 100644 index 000000000000..f7bc1903f48a --- /dev/null +++ b/testing/web-platform/meta/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-align-content-003.html.ini @@ -0,0 +1,2 @@ +[column-grid-lanes-oof-align-content-003.html] + expected: FAIL diff --git a/testing/web-platform/meta/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-fill-reverse-001.html.ini b/testing/web-platform/meta/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-fill-reverse-001.html.ini new file mode 100644 index 000000000000..b736a43a897e --- /dev/null +++ b/testing/web-platform/meta/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-fill-reverse-001.html.ini @@ -0,0 +1,2 @@ +[column-grid-lanes-oof-fill-reverse-001.html] + expected: FAIL diff --git a/testing/web-platform/meta/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-fill-reverse-002.html.ini b/testing/web-platform/meta/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-fill-reverse-002.html.ini new file mode 100644 index 000000000000..0705694290e5 --- /dev/null +++ b/testing/web-platform/meta/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-fill-reverse-002.html.ini @@ -0,0 +1,2 @@ +[column-grid-lanes-oof-fill-reverse-002.html] + expected: FAIL diff --git a/testing/web-platform/meta/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-fill-reverse-003.html.ini b/testing/web-platform/meta/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-fill-reverse-003.html.ini new file mode 100644 index 000000000000..3b11a17d398b --- /dev/null +++ b/testing/web-platform/meta/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-fill-reverse-003.html.ini @@ -0,0 +1,2 @@ +[column-grid-lanes-oof-fill-reverse-003.html] + expected: FAIL diff --git a/testing/web-platform/meta/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-justify-content-001.html.ini b/testing/web-platform/meta/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-justify-content-001.html.ini new file mode 100644 index 000000000000..a0e8a76671eb --- /dev/null +++ b/testing/web-platform/meta/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-justify-content-001.html.ini @@ -0,0 +1,2 @@ +[column-grid-lanes-oof-justify-content-001.html] + expected: FAIL diff --git a/testing/web-platform/meta/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-align-content-001.html.ini b/testing/web-platform/meta/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-align-content-001.html.ini new file mode 100644 index 000000000000..987588b766a6 --- /dev/null +++ b/testing/web-platform/meta/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-align-content-001.html.ini @@ -0,0 +1,2 @@ +[row-grid-lanes-oof-align-content-001.html] + expected: FAIL diff --git a/testing/web-platform/meta/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-fill-reverse-001.html.ini b/testing/web-platform/meta/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-fill-reverse-001.html.ini new file mode 100644 index 000000000000..b525449aa37e --- /dev/null +++ b/testing/web-platform/meta/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-fill-reverse-001.html.ini @@ -0,0 +1,2 @@ +[row-grid-lanes-oof-fill-reverse-001.html] + expected: FAIL diff --git a/testing/web-platform/meta/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-fill-reverse-002.html.ini b/testing/web-platform/meta/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-fill-reverse-002.html.ini new file mode 100644 index 000000000000..c0cf404d70ea --- /dev/null +++ b/testing/web-platform/meta/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-fill-reverse-002.html.ini @@ -0,0 +1,2 @@ +[row-grid-lanes-oof-fill-reverse-002.html] + expected: FAIL diff --git a/testing/web-platform/meta/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-fill-reverse-003.html.ini b/testing/web-platform/meta/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-fill-reverse-003.html.ini new file mode 100644 index 000000000000..5ec81e940f79 --- /dev/null +++ b/testing/web-platform/meta/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-fill-reverse-003.html.ini @@ -0,0 +1,2 @@ +[row-grid-lanes-oof-fill-reverse-003.html] + expected: FAIL diff --git a/testing/web-platform/meta/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-justify-content-001.html.ini b/testing/web-platform/meta/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-justify-content-001.html.ini new file mode 100644 index 000000000000..93299c8c2e35 --- /dev/null +++ b/testing/web-platform/meta/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-justify-content-001.html.ini @@ -0,0 +1,2 @@ +[row-grid-lanes-oof-justify-content-001.html] + expected: FAIL diff --git a/testing/web-platform/meta/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-justify-content-002.html.ini b/testing/web-platform/meta/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-justify-content-002.html.ini new file mode 100644 index 000000000000..0858dd1e6f23 --- /dev/null +++ b/testing/web-platform/meta/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-justify-content-002.html.ini @@ -0,0 +1,2 @@ +[row-grid-lanes-oof-justify-content-002.html] + expected: FAIL diff --git a/testing/web-platform/meta/css/css-overflow/line-clamp/block-ellipsis-039.html.ini b/testing/web-platform/meta/css/css-overflow/line-clamp/block-ellipsis-039.html.ini new file mode 100644 index 000000000000..f7df27c657e3 --- /dev/null +++ b/testing/web-platform/meta/css/css-overflow/line-clamp/block-ellipsis-039.html.ini @@ -0,0 +1,2 @@ +[block-ellipsis-039.html] + expected: FAIL diff --git a/testing/web-platform/meta/css/css-overflow/line-clamp/block-ellipsis-040.html.ini b/testing/web-platform/meta/css/css-overflow/line-clamp/block-ellipsis-040.html.ini new file mode 100644 index 000000000000..a5c92af38bc0 --- /dev/null +++ b/testing/web-platform/meta/css/css-overflow/line-clamp/block-ellipsis-040.html.ini @@ -0,0 +1,2 @@ +[block-ellipsis-040.html] + expected: FAIL diff --git a/testing/web-platform/meta/css/css-overflow/line-clamp/block-ellipsis-041.html.ini b/testing/web-platform/meta/css/css-overflow/line-clamp/block-ellipsis-041.html.ini new file mode 100644 index 000000000000..26ad74ce46b0 --- /dev/null +++ b/testing/web-platform/meta/css/css-overflow/line-clamp/block-ellipsis-041.html.ini @@ -0,0 +1,2 @@ +[block-ellipsis-041.html] + expected: FAIL diff --git a/testing/web-platform/meta/css/css-overflow/line-clamp/line-clamp-with-floats-011.html.ini b/testing/web-platform/meta/css/css-overflow/line-clamp/line-clamp-with-floats-011.html.ini new file mode 100644 index 000000000000..46a2cc7e933b --- /dev/null +++ b/testing/web-platform/meta/css/css-overflow/line-clamp/line-clamp-with-floats-011.html.ini @@ -0,0 +1,2 @@ +[line-clamp-with-floats-011.html] + expected: FAIL diff --git a/testing/web-platform/meta/css/css-overflow/line-clamp/line-clamp-with-floats-012.html.ini b/testing/web-platform/meta/css/css-overflow/line-clamp/line-clamp-with-floats-012.html.ini new file mode 100644 index 000000000000..7adc4ef6dee2 --- /dev/null +++ b/testing/web-platform/meta/css/css-overflow/line-clamp/line-clamp-with-floats-012.html.ini @@ -0,0 +1,2 @@ +[line-clamp-with-floats-012.html] + expected: FAIL diff --git a/testing/web-platform/meta/css/css-pseudo/highlight-cascade/highlight-cascade-parent-style-change.html.ini b/testing/web-platform/meta/css/css-pseudo/highlight-cascade/highlight-cascade-parent-style-change.html.ini new file mode 100644 index 000000000000..1987d4af3c40 --- /dev/null +++ b/testing/web-platform/meta/css/css-pseudo/highlight-cascade/highlight-cascade-parent-style-change.html.ini @@ -0,0 +1,3 @@ +[highlight-cascade-parent-style-change.html] + [A highlight pseudo-element inherits from the parent's new style, not its previous one] + expected: FAIL diff --git a/testing/web-platform/meta/css/css-pseudo/highlight-cascade/highlight-cascade-shadow-boundary.html.ini b/testing/web-platform/meta/css/css-pseudo/highlight-cascade/highlight-cascade-shadow-boundary.html.ini new file mode 100644 index 000000000000..0daa708c2215 --- /dev/null +++ b/testing/web-platform/meta/css/css-pseudo/highlight-cascade/highlight-cascade-shadow-boundary.html.ini @@ -0,0 +1,6 @@ +[highlight-cascade-shadow-boundary.html] + [A highlight pseudo-element in a shadow tree inherits from the shadow host's] + expected: FAIL + + [A highlight pseudo-element of slotted content inherits through the slot] + expected: FAIL diff --git a/testing/web-platform/meta/css/css-typed-om/the-stylepropertymap/properties/logical.html.ini b/testing/web-platform/meta/css/css-typed-om/the-stylepropertymap/properties/logical.html.ini index 2a0c77d82357..d93934ea6345 100644 --- a/testing/web-platform/meta/css/css-typed-om/the-stylepropertymap/properties/logical.html.ini +++ b/testing/web-platform/meta/css/css-typed-om/the-stylepropertymap/properties/logical.html.ini @@ -260,15 +260,6 @@ [Setting 'border-block-start' to a length: calc(0px + 0em) throws TypeError] expected: FAIL - [Can set 'border-block-start-width' to the 'thin' keyword: thin] - expected: FAIL - - [Can set 'border-block-start-width' to the 'medium' keyword: medium] - expected: FAIL - - [Can set 'border-block-start-width' to the 'thick' keyword: thick] - expected: FAIL - [Can set 'border-block-end' to CSS-wide keywords: initial] expected: FAIL @@ -296,15 +287,6 @@ [Setting 'border-block-end' to a length: calc(0px + 0em) throws TypeError] expected: FAIL - [Can set 'border-block-end-width' to the 'thin' keyword: thin] - expected: FAIL - - [Can set 'border-block-end-width' to the 'medium' keyword: medium] - expected: FAIL - - [Can set 'border-block-end-width' to the 'thick' keyword: thick] - expected: FAIL - [Can set 'border-inline-start' to CSS-wide keywords: initial] expected: FAIL @@ -332,15 +314,6 @@ [Setting 'border-inline-start' to a length: calc(0px + 0em) throws TypeError] expected: FAIL - [Can set 'border-inline-start-width' to the 'thin' keyword: thin] - expected: FAIL - - [Can set 'border-inline-start-width' to the 'medium' keyword: medium] - expected: FAIL - - [Can set 'border-inline-start-width' to the 'thick' keyword: thick] - expected: FAIL - [Can set 'border-inline-end' to CSS-wide keywords: initial] expected: FAIL @@ -368,15 +341,6 @@ [Setting 'border-inline-end' to a length: calc(0px + 0em) throws TypeError] expected: FAIL - [Can set 'border-inline-end-width' to the 'thin' keyword: thin] - expected: FAIL - - [Can set 'border-inline-end-width' to the 'medium' keyword: medium] - expected: FAIL - - [Can set 'border-inline-end-width' to the 'thick' keyword: thick] - expected: FAIL - [Can set 'border-block' to CSS-wide keywords: initial] expected: FAIL diff --git a/testing/web-platform/meta/css/css-values/ident-function-computed.html.ini b/testing/web-platform/meta/css/css-values/ident-function-computed.html.ini index 07af534cf623..afd46390db17 100644 --- a/testing/web-platform/meta/css/css-values/ident-function-computed.html.ini +++ b/testing/web-platform/meta/css/css-values/ident-function-computed.html.ini @@ -31,3 +31,12 @@ [Property view-transition-name value 'ident("myident" 42)'] expected: FAIL + + [Property view-transition-name value 'ident("--prop" calc(1 + 2))'] + expected: FAIL + + [Property view-transition-name value 'ident(5px)'] + expected: FAIL + + [Property view-transition-name value 'ident(rgb(1, 2, 3))'] + expected: FAIL diff --git a/testing/web-platform/meta/css/css-values/ident-function-parsing.html.ini b/testing/web-platform/meta/css/css-values/ident-function-parsing.html.ini index f17ffbe04d1d..bd55017f6b92 100644 --- a/testing/web-platform/meta/css/css-values/ident-function-parsing.html.ini +++ b/testing/web-platform/meta/css/css-values/ident-function-parsing.html.ini @@ -37,3 +37,9 @@ [e.style['view-transition-name'\] = "ident(\\"myident\\" 42)" should set the property value] expected: FAIL + + [e.style['view-transition-name'\] = "ident(rgb(1, 2, 3))" should set the property value] + expected: FAIL + + [e.style['view-transition-name'\] = "ident(5px)" should set the property value] + expected: FAIL diff --git a/testing/web-platform/meta/css/css-values/ident-function-substitution.html.ini b/testing/web-platform/meta/css/css-values/ident-function-substitution.html.ini new file mode 100644 index 000000000000..c2613e11324d --- /dev/null +++ b/testing/web-platform/meta/css/css-values/ident-function-substitution.html.ini @@ -0,0 +1,27 @@ +[ident-function-substitution.html] + [var() in the ident() argument] + expected: FAIL + + [attr() in the ident() argument] + expected: FAIL + + [ident() in the ident() argument] + expected: FAIL + + [ident() building the var() name argument from an attribute] + expected: FAIL + + [sibling-index() in the ident() argument] + expected: FAIL + + [a math function over sibling-index() in the ident() argument] + expected: FAIL + + [an argument that is not + makes ident() invalid at computed-value time] + expected: FAIL + + [an ident() with no attr() involved does not taint the property it names] + expected: FAIL + + [ident() substituting a value the property does not accept] + expected: FAIL diff --git a/testing/web-platform/meta/css/css-values/if-conditionals.html.ini b/testing/web-platform/meta/css/css-values/if-conditionals.html.ini index 0893930f0480..08c86edc732d 100644 --- a/testing/web-platform/meta/css/css-values/if-conditionals.html.ini +++ b/testing/web-platform/meta/css/css-values/if-conditionals.html.ini @@ -604,3 +604,6 @@ [CSS Values and Units Test: CSS inline if() function 201] expected: FAIL + + [CSS Values and Units Test: CSS inline if() function 202] + expected: FAIL diff --git a/testing/web-platform/meta/css/css-values/random-in-if.tentative.html.ini b/testing/web-platform/meta/css/css-values/random-in-if.tentative.html.ini index 6711760777a6..fc774d0b125a 100644 --- a/testing/web-platform/meta/css/css-values/random-in-if.tentative.html.ini +++ b/testing/web-platform/meta/css/css-values/random-in-if.tentative.html.ini @@ -1,10 +1,4 @@ [random-in-if.tentative.html] - [random() should not be allowed in if() style() condition] - expected: FAIL - - [random() in var() should not be allowed in if() style() condition] - expected: FAIL - [random() with different property names should not be shared in if() declaration value] expected: FAIL @@ -16,3 +10,18 @@ [random() with same property name on different elements in if() declaration value should be equal] expected: FAIL + + [random() should be allowed in if() style() condition] + expected: FAIL + + [random() in var() should be allowed in if() style() condition] + expected: FAIL + + [Sharing random() in if() style() condition within same property] + expected: FAIL + + [Sharing property-index-scoped random() in if() style() condition across different properties] + expected: FAIL + + [Sharing element-scoped random() in if() style() condition across different elements] + expected: FAIL diff --git a/testing/web-platform/meta/css/css-variables/var-ident-function.html.ini b/testing/web-platform/meta/css/css-variables/var-ident-function.html.ini index 2220161770dc..fd1c35c636eb 100644 --- a/testing/web-platform/meta/css/css-variables/var-ident-function.html.ini +++ b/testing/web-platform/meta/css/css-variables/var-ident-function.html.ini @@ -7,3 +7,6 @@ [ident() causing lookup of invalid custom property, fallback, CSS-wide keyword] expected: FAIL + + [ident() is substituted on custom properties] + expected: FAIL diff --git a/testing/web-platform/meta/css/css-variables/var-parsing.html.ini b/testing/web-platform/meta/css/css-variables/var-parsing.html.ini new file mode 100644 index 000000000000..83e8d3e05884 --- /dev/null +++ b/testing/web-platform/meta/css/css-variables/var-parsing.html.ini @@ -0,0 +1,24 @@ +[var-parsing.html] + [e.style['width'\] = "var(--x ())" should set the property value] + expected: FAIL + + [e.style['width'\] = "var(--x () )" should set the property value] + expected: FAIL + + [e.style['width'\] = "var(--x() )" should set the property value] + expected: FAIL + + [e.style['width'\] = "var(--x (),)" should set the property value] + expected: FAIL + + [e.style['width'\] = "var(--x(),)" should set the property value] + expected: FAIL + + [e.style['width'\] = "var({--x})" should set the property value] + expected: FAIL + + [e.style['width'\] = "var({--x}, 10px)" should set the property value] + expected: FAIL + + [e.style['width'\] = "var({--x, --y})" should set the property value] + expected: FAIL diff --git a/testing/web-platform/meta/css/css-variables/variable-declaration-29.html.ini b/testing/web-platform/meta/css/css-variables/variable-declaration-29.html.ini new file mode 100644 index 000000000000..815d162d51a8 --- /dev/null +++ b/testing/web-platform/meta/css/css-variables/variable-declaration-29.html.ini @@ -0,0 +1,2 @@ +[variable-declaration-29.html] + expected: FAIL diff --git a/testing/web-platform/meta/css/css-variables/variable-reference-name-substitution-attr-taint.html.ini b/testing/web-platform/meta/css/css-variables/variable-reference-name-substitution-attr-taint.html.ini new file mode 100644 index 000000000000..875d155c43bc --- /dev/null +++ b/testing/web-platform/meta/css/css-variables/variable-reference-name-substitution-attr-taint.html.ini @@ -0,0 +1,9 @@ +[variable-reference-name-substitution-attr-taint.html] + [attr()-tainted name argument does not invalidate values that are not URLs] + expected: FAIL + + [attr()-tainted name argument substitutes normally into a custom property] + expected: FAIL + + [untainted substituted name argument does not taint the value] + expected: FAIL diff --git a/testing/web-platform/meta/css/css-variables/variable-reference-name-substitution.html.ini b/testing/web-platform/meta/css/css-variables/variable-reference-name-substitution.html.ini new file mode 100644 index 000000000000..41cc91d2937b --- /dev/null +++ b/testing/web-platform/meta/css/css-variables/variable-reference-name-substitution.html.ini @@ -0,0 +1,63 @@ +[variable-reference-name-substitution.html] + [var() name comes from another var()] + expected: FAIL + + [var() name comes from a chain of var()s] + expected: FAIL + + [invalid substituted name falls back] + expected: FAIL + + [unset name-providing var() falls back] + expected: FAIL + + [whitespace around substituted name] + expected: FAIL + + [name argument substituting to nothing falls back] + expected: FAIL + + [multi-token substituted name falls back] + expected: FAIL + + [dimension-token substituted name falls back] + expected: FAIL + + [string substituted name falls back] + expected: FAIL + + [-- as substituted name falls back] + expected: FAIL + + [{}-wrapped literal name argument] + expected: FAIL + + [{}-wrapped substituted name argument] + expected: FAIL + + [{}-wrapped name argument with whitespace] + expected: FAIL + + [{}-wrapped name argument with fallback] + expected: FAIL + + [var() name comes from attr()] + expected: FAIL + + [var() name from attr() that is not a name falls back] + expected: FAIL + + [var() name comes from if()] + expected: FAIL + + [var() name comes from random-item()] + expected: FAIL + + [substituted name resolves a registered property] + expected: FAIL + + [substituted name of a guaranteed-invalid registered property uses the fallback] + expected: FAIL + + [fallback of an unparsed name is not syntax checked] + expected: FAIL diff --git a/testing/web-platform/meta/css/css-variables/variable-reference.html.ini b/testing/web-platform/meta/css/css-variables/variable-reference.html.ini index e68f894c0a69..c958fc6041b1 100644 --- a/testing/web-platform/meta/css/css-variables/variable-reference.html.ini +++ b/testing/web-platform/meta/css/css-variables/variable-reference.html.ini @@ -7,3 +7,24 @@ [width: var(--prop,);] expected: if (os == "linux") and not debug: [PASS, FAIL] + + [width: var(prop);] + expected: FAIL + + [width: var(-prop);] + expected: FAIL + + [width: var(--prop 20px);] + expected: FAIL + + [width: var(--prop, var(prop));] + expected: FAIL + + [width: var(--prop, var(-prop));] + expected: FAIL + + [width: var(20px);] + expected: FAIL + + [width: var(var(--prop));] + expected: FAIL diff --git a/testing/web-platform/meta/css/css-variables/variable-supports-30.html.ini b/testing/web-platform/meta/css/css-variables/variable-supports-30.html.ini new file mode 100644 index 000000000000..10f0e68bf4e9 --- /dev/null +++ b/testing/web-platform/meta/css/css-variables/variable-supports-30.html.ini @@ -0,0 +1,2 @@ +[variable-supports-30.html] + expected: FAIL diff --git a/testing/web-platform/meta/css/css-variables/variable-supports-64.html.ini b/testing/web-platform/meta/css/css-variables/variable-supports-64.html.ini new file mode 100644 index 000000000000..3b18a6502e99 --- /dev/null +++ b/testing/web-platform/meta/css/css-variables/variable-supports-64.html.ini @@ -0,0 +1,2 @@ +[variable-supports-64.html] + expected: FAIL diff --git a/testing/web-platform/meta/css/css-view-transitions/__dir__.ini b/testing/web-platform/meta/css/css-view-transitions/__dir__.ini index 0d13ecb690fd..fc913cbd9358 100644 --- a/testing/web-platform/meta/css/css-view-transitions/__dir__.ini +++ b/testing/web-platform/meta/css/css-view-transitions/__dir__.ini @@ -1,3 +1,4 @@ prefs: [dom.viewTransitions.enabled:true] disabled: if useDrawSnapshot: Not expected to work +leak-threshold: [default:51200] diff --git a/testing/web-platform/meta/css/css-view-transitions/old-content-container-writing-modes.html.ini b/testing/web-platform/meta/css/css-view-transitions/old-content-container-writing-modes.html.ini index 8929713b0d33..c24cad8ff18e 100644 --- a/testing/web-platform/meta/css/css-view-transitions/old-content-container-writing-modes.html.ini +++ b/testing/web-platform/meta/css/css-view-transitions/old-content-container-writing-modes.html.ini @@ -1,4 +1,4 @@ [old-content-container-writing-modes.html] expected: - if (os == "win") and not swgl: [PASS, FAIL] if (os == "linux") and not fission and not swgl: [PASS, TIMEOUT] + if (os == "win") and not swgl: [PASS, FAIL] diff --git a/testing/web-platform/meta/css/css-viewport/zoom/text-decoration-thickness.html.ini b/testing/web-platform/meta/css/css-viewport/zoom/text-decoration-thickness.html.ini deleted file mode 100644 index 30dacdaf33ca..000000000000 --- a/testing/web-platform/meta/css/css-viewport/zoom/text-decoration-thickness.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[text-decoration-thickness.html] - expected: FAIL diff --git a/testing/web-platform/meta/document-picture-in-picture/returns-window-with-document.https.html.ini b/testing/web-platform/meta/document-picture-in-picture/returns-window-with-document.https.html.ini index f439dbf0249f..b7d30cf26b85 100644 --- a/testing/web-platform/meta/document-picture-in-picture/returns-window-with-document.https.html.ini +++ b/testing/web-platform/meta/document-picture-in-picture/returns-window-with-document.https.html.ini @@ -7,6 +7,8 @@ [requestWindow resolves with the PiP window] expected: - if (os == "win") and not debug and (processor == "x86_64"): PASS - if (os == "linux") and not tsan: PASS + if (processor == "x86_64") and not tsan and (os == "win") and not debug: PASS + if (processor == "x86_64") and not tsan and (os == "mac"): PASS + if (processor == "x86_64") and not tsan and (os == "linux"): PASS + if (processor == "x86") and debug: [FAIL, PASS] [PASS, FAIL] diff --git a/testing/web-platform/meta/dom/events/scrolling/scrollend-event-fires-for-repeat-key-ending-after-scroll-container-end-is-reached.html.ini b/testing/web-platform/meta/dom/events/scrolling/scrollend-event-fires-for-repeat-key-ending-after-scroll-container-end-is-reached.html.ini index ea9ded59ab19..dd64c8e35133 100644 --- a/testing/web-platform/meta/dom/events/scrolling/scrollend-event-fires-for-repeat-key-ending-after-scroll-container-end-is-reached.html.ini +++ b/testing/web-platform/meta/dom/events/scrolling/scrollend-event-fires-for-repeat-key-ending-after-scroll-container-end-is-reached.html.ini @@ -2,4 +2,3 @@ [scrollend event is fired after repeated Page Down key presses reach the end of the scroll region.] expected: if asan and fission: [PASS, FAIL] - if asan and not fission: FAIL diff --git a/testing/web-platform/meta/domparsing/tentative/positional-template-child.html.ini b/testing/web-platform/meta/domparsing/tentative/positional-template-child.html.ini new file mode 100644 index 000000000000..16f1a965cab6 --- /dev/null +++ b/testing/web-platform/meta/domparsing/tentative/positional-template-child.html.ini @@ -0,0 +1,36 @@ +[positional-template-child.html] + [afterHTML on a direct child of a template element throws HierarchyRequestError] + expected: FAIL + + [beforeHTML on a direct child of a template element throws HierarchyRequestError] + expected: FAIL + + [replaceWithHTML on a direct child of a template element throws HierarchyRequestError] + expected: FAIL + + [streamAfterHTML on a direct child of a template element throws HierarchyRequestError] + expected: FAIL + + [streamBeforeHTML on a direct child of a template element throws HierarchyRequestError] + expected: FAIL + + [streamReplaceWithHTML on a direct child of a template element throws HierarchyRequestError] + expected: FAIL + + [afterHTMLUnsafe on a direct child of a template element throws HierarchyRequestError] + expected: FAIL + + [beforeHTMLUnsafe on a direct child of a template element throws HierarchyRequestError] + expected: FAIL + + [replaceWithHTMLUnsafe on a direct child of a template element throws HierarchyRequestError] + expected: FAIL + + [streamAfterHTMLUnsafe on a direct child of a template element throws HierarchyRequestError] + expected: FAIL + + [streamBeforeHTMLUnsafe on a direct child of a template element throws HierarchyRequestError] + expected: FAIL + + [streamReplaceWithHTMLUnsafe on a direct child of a template element throws HierarchyRequestError] + expected: FAIL diff --git a/testing/web-platform/meta/domparsing/tentative/stream-append-html-unsafe.html.ini b/testing/web-platform/meta/domparsing/tentative/stream-append-html-unsafe.html.ini index 1a7440c3adac..02f29b79ab7b 100644 --- a/testing/web-platform/meta/domparsing/tentative/stream-append-html-unsafe.html.ini +++ b/testing/web-platform/meta/domparsing/tentative/stream-append-html-unsafe.html.ini @@ -37,3 +37,6 @@ [streamAppendHTMLUnsafe should not execute scripts when disconnected] expected: FAIL + + [streamAppendHTMLUnsafe should not execute unclosed scripts on abort] + expected: FAIL diff --git a/testing/web-platform/meta/domparsing/tentative/stream-append-html.html.ini b/testing/web-platform/meta/domparsing/tentative/stream-append-html.html.ini index 4e7a33e06d1b..8fa433db22b4 100644 --- a/testing/web-platform/meta/domparsing/tentative/stream-append-html.html.ini +++ b/testing/web-platform/meta/domparsing/tentative/stream-append-html.html.ini @@ -46,3 +46,6 @@ [streamAppendHTML on ShadowRoot uses safe sanitizer] expected: FAIL + + [streamAppendHTML with a chunk whose toString throws should reject] + expected: FAIL diff --git a/testing/web-platform/meta/domparsing/tentative/stream-html-defer-async-script.html.ini b/testing/web-platform/meta/domparsing/tentative/stream-html-defer-async-script.html.ini index 4ca295587156..f8cee9b56b1b 100644 --- a/testing/web-platform/meta/domparsing/tentative/stream-html-defer-async-script.html.ini +++ b/testing/web-platform/meta/domparsing/tentative/stream-html-defer-async-script.html.ini @@ -4,3 +4,9 @@ [element.streamHTMLUnsafe with defer/async scripts] expected: FAIL + + [element.streamAppendHTMLUnsafe with defer/module scripts should not execute on abort] + expected: FAIL + + [element.streamHTMLUnsafe with defer/module scripts should not execute on abort] + expected: FAIL diff --git a/testing/web-platform/meta/domparsing/tentative/stream-html-unsafe.html.ini b/testing/web-platform/meta/domparsing/tentative/stream-html-unsafe.html.ini index 9ff633b28628..d3c75d2f5ee9 100644 --- a/testing/web-platform/meta/domparsing/tentative/stream-html-unsafe.html.ini +++ b/testing/web-platform/meta/domparsing/tentative/stream-html-unsafe.html.ini @@ -40,3 +40,6 @@ [streamHTMLUnsafe should not execute scripts when disconnected] expected: FAIL + + [streamHTMLUnsafe should not execute unclosed scripts on abort] + expected: FAIL diff --git a/testing/web-platform/meta/domparsing/tentative/stream-positional-template-child.html.ini b/testing/web-platform/meta/domparsing/tentative/stream-positional-template-child.html.ini new file mode 100644 index 000000000000..675968d840a2 --- /dev/null +++ b/testing/web-platform/meta/domparsing/tentative/stream-positional-template-child.html.ini @@ -0,0 +1,18 @@ +[stream-positional-template-child.html] + [streamAfterHTML on a child of a template content] + expected: FAIL + + [streamBeforeHTML on a child of a template content] + expected: FAIL + + [streamReplaceWithHTML on a child of a template content] + expected: FAIL + + [streamAfterHTMLUnsafe on a child of a template content] + expected: FAIL + + [streamBeforeHTMLUnsafe on a child of a template content] + expected: FAIL + + [streamReplaceWithHTMLUnsafe on a child of a template content] + expected: FAIL diff --git a/testing/web-platform/meta/domparsing/tentative/stream-positional.html.ini b/testing/web-platform/meta/domparsing/tentative/stream-positional.html.ini index e7cb1f0105e5..f17fb0ef2e9d 100644 --- a/testing/web-platform/meta/domparsing/tentative/stream-positional.html.ini +++ b/testing/web-platform/meta/domparsing/tentative/stream-positional.html.ini @@ -214,3 +214,33 @@ [streamReplaceWithHTMLUnsafe throw if parent is a DocumentFragment] expected: FAIL + + [streamAppendHTML on HTMLTemplateElement appends to content] + expected: FAIL + + [streamPrependHTML on HTMLTemplateElement prepends to content] + expected: FAIL + + [streamBeforeHTML on child of HTMLTemplateElement content streams before child] + expected: FAIL + + [streamAfterHTML on child of HTMLTemplateElement content streams after child] + expected: FAIL + + [streamReplaceWithHTML on child of HTMLTemplateElement content replaces child] + expected: FAIL + + [streamAppendHTMLUnsafe on HTMLTemplateElement appends to content] + expected: FAIL + + [streamPrependHTMLUnsafe on HTMLTemplateElement prepends to content] + expected: FAIL + + [streamBeforeHTMLUnsafe on child of HTMLTemplateElement content streams before child] + expected: FAIL + + [streamAfterHTMLUnsafe on child of HTMLTemplateElement content streams after child] + expected: FAIL + + [streamReplaceWithHTMLUnsafe on child of HTMLTemplateElement content replaces child] + expected: FAIL diff --git a/testing/web-platform/meta/fetch/local-network-access/service-worker.tentative.https.html.ini b/testing/web-platform/meta/fetch/local-network-access/service-worker.tentative.https.html.ini index 482972f6a24a..67adba7eb615 100644 --- a/testing/web-platform/meta/fetch/local-network-access/service-worker.tentative.https.html.ini +++ b/testing/web-platform/meta/fetch/local-network-access/service-worker.tentative.https.html.ini @@ -5,14 +5,25 @@ if product == "firefox_android": Service worker LNA tests unstable on Android - Bug 2042339 [LNA Service Worker Public to Loopback WebTransport with permission] disabled: LNA not yet enforced for WebTransport - Bug 2042527 + [LNA Service Worker Public to Loopback WebTransport without permission] disabled: LNA not yet enforced for WebTransport - Bug 2042527 + [LNA Service Worker Public navigate to Loopback main frame without permission] expected: - if (os == "linux"): [TIMEOUT, PASS] + if (os == "linux") and debug: [NOTRUN, PASS, TIMEOUT] + if (os == "linux") and not debug: [PASS, TIMEOUT] + [LNA Service Worker Public navigate to Loopback in iframe with permission] expected: - if (os == "linux"): [PASS, NOTRUN] + if (os == "linux") and debug: [NOTRUN, PASS] + if (os == "linux") and not debug: [PASS, NOTRUN] + [LNA Service Worker Public navigate to Loopback in iframe without permission] expected: - if (os == "linux"): [PASS, NOTRUN] + if (os == "linux") and debug: [NOTRUN, PASS] + if (os == "linux") and not debug: [PASS, NOTRUN] + + [LNA Service Worker Public to Loopback WebSocket without permission] + expected: + if (os == "linux") and debug: TIMEOUT diff --git a/testing/web-platform/meta/fetch/metadata/window-open.https.sub.html.ini b/testing/web-platform/meta/fetch/metadata/window-open.https.sub.html.ini index 466d7d940bfe..967610272498 100644 --- a/testing/web-platform/meta/fetch/metadata/window-open.https.sub.html.ini +++ b/testing/web-platform/meta/fetch/metadata/window-open.https.sub.html.ini @@ -1,6 +1,6 @@ [window-open.https.sub.html] expected: - if (os == "mac") and (processor == "x86_64"): [TIMEOUT, OK, ERROR] + if (os == "mac") and (processor == "x86_64"): [OK, ERROR, TIMEOUT] [OK, ERROR] [Cross-site window, forced, reloaded] expected: [PASS, FAIL] @@ -9,11 +9,3 @@ expected: if (os == "win") and debug and (processor == "x86_64"): PASS [PASS, FAIL] - - [Same-site window, user-activated] - expected: - if (os == "mac") and (processor == "x86_64"): TIMEOUT - - [Same-origin window, user-activated] - expected: - if (os == "mac") and (processor == "x86_64"): TIMEOUT diff --git a/testing/web-platform/meta/html/browsers/the-window-object/self-et-al.window.js.ini b/testing/web-platform/meta/html/browsers/the-window-object/self-et-al.window.js.ini index 71d5056705ee..91e031dc50c6 100644 --- a/testing/web-platform/meta/html/browsers/the-window-object/self-et-al.window.js.ini +++ b/testing/web-platform/meta/html/browsers/the-window-object/self-et-al.window.js.ini @@ -1,5 +1,5 @@ [self-et-al.window.html] max-asserts: 3 expected: - if not asan and (os == "linux") and not fission: TIMEOUT + if not asan and (processor == "x86") and debug: [OK, TIMEOUT] if asan: [OK, TIMEOUT] diff --git a/testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/svg-linear-gradient-outside-subtree-ignored.tentative.html.ini b/testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/svg-linear-gradient-outside-subtree-ignored.tentative.html.ini new file mode 100644 index 000000000000..0343d60c6356 --- /dev/null +++ b/testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/svg-linear-gradient-outside-subtree-ignored.tentative.html.ini @@ -0,0 +1,3 @@ +[svg-linear-gradient-outside-subtree-ignored.tentative.html] + [drawElementImage does not use SVG resources from outside the subtree] + expected: FAIL diff --git a/testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/svg-pattern-outside-subtree-ignored.tentative.html.ini b/testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/svg-pattern-outside-subtree-ignored.tentative.html.ini new file mode 100644 index 000000000000..bcf12ad9edea --- /dev/null +++ b/testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/svg-pattern-outside-subtree-ignored.tentative.html.ini @@ -0,0 +1,3 @@ +[svg-pattern-outside-subtree-ignored.tentative.html] + [drawElementImage does not use SVG patterns outside the subtree] + expected: FAIL diff --git a/testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/svg-radial-gradient-outside-subtree-ignored.tentative.html.ini b/testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/svg-radial-gradient-outside-subtree-ignored.tentative.html.ini new file mode 100644 index 000000000000..2f5623f9fbc1 --- /dev/null +++ b/testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/svg-radial-gradient-outside-subtree-ignored.tentative.html.ini @@ -0,0 +1,3 @@ +[svg-radial-gradient-outside-subtree-ignored.tentative.html] + [drawElementImage does not use SVG resources from outside the subtree] + expected: FAIL diff --git a/testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/svg-use-outside-subtree-images-ignored.tentative.https.sub.html.ini b/testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/svg-use-outside-subtree-images-ignored.tentative.https.sub.html.ini new file mode 100644 index 000000000000..94643d38b8a1 --- /dev/null +++ b/testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/svg-use-outside-subtree-images-ignored.tentative.https.sub.html.ini @@ -0,0 +1,3 @@ +[svg-use-outside-subtree-images-ignored.tentative.https.sub.html] + [drawElementImage does not use cross-origin SVG use content from outside the subtree] + expected: FAIL diff --git a/testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/visited-link-svg-use-outside-subtree-ignored.tentative.html.ini b/testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/visited-link-svg-use-outside-subtree-ignored.tentative.html.ini new file mode 100644 index 000000000000..d4f75086e5b7 --- /dev/null +++ b/testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/visited-link-svg-use-outside-subtree-ignored.tentative.html.ini @@ -0,0 +1,3 @@ +[visited-link-svg-use-outside-subtree-ignored.tentative.html] + [drawElementImage does not leak visited colors in SVG use content from outside the subtree] + expected: FAIL diff --git a/testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/visited-link-color-ignored.tentative.html.ini b/testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/visited-link/visited-link-color-ignored.tentative.html.ini similarity index 100% rename from testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/visited-link-color-ignored.tentative.html.ini rename to testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/visited-link/visited-link-color-ignored.tentative.html.ini diff --git a/testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/visited-link-currentcolor-ignored.tentative.html.ini b/testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/visited-link/visited-link-currentcolor-ignored.tentative.html.ini similarity index 100% rename from testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/visited-link-currentcolor-ignored.tentative.html.ini rename to testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/visited-link/visited-link-currentcolor-ignored.tentative.html.ini diff --git a/testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/visited-link-decoration-color-ignored.tentative.html.ini b/testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/visited-link/visited-link-decoration-color-ignored.tentative.html.ini similarity index 100% rename from testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/visited-link-decoration-color-ignored.tentative.html.ini rename to testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/visited-link/visited-link-decoration-color-ignored.tentative.html.ini diff --git a/testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/visited-link-svg-fill-stroke-color-ignored.tentative.html.ini b/testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/visited-link/visited-link-svg-fill-stroke-color-ignored.tentative.html.ini similarity index 100% rename from testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/visited-link-svg-fill-stroke-color-ignored.tentative.html.ini rename to testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/visited-link/visited-link-svg-fill-stroke-color-ignored.tentative.html.ini diff --git a/testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/visited-link/visited-link-svg-linear-gradiant-ignored.tentative.html.ini b/testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/visited-link/visited-link-svg-linear-gradiant-ignored.tentative.html.ini new file mode 100644 index 000000000000..777c5ab1a494 --- /dev/null +++ b/testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/visited-link/visited-link-svg-linear-gradiant-ignored.tentative.html.ini @@ -0,0 +1,3 @@ +[visited-link-svg-linear-gradiant-ignored.tentative.html] + [drawElementImage does not reveal visited link colors] + expected: FAIL diff --git a/testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/visited-link/visited-link-svg-pattern-ignored.tentative.html.ini b/testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/visited-link/visited-link-svg-pattern-ignored.tentative.html.ini new file mode 100644 index 000000000000..227b0b69e261 --- /dev/null +++ b/testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/visited-link/visited-link-svg-pattern-ignored.tentative.html.ini @@ -0,0 +1,3 @@ +[visited-link-svg-pattern-ignored.tentative.html] + [drawElementImage does not reveal visited link colors] + expected: FAIL diff --git a/testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/visited-link/visited-link-svg-radiant-gradiant-ignored.tentative.html.ini b/testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/visited-link/visited-link-svg-radiant-gradiant-ignored.tentative.html.ini new file mode 100644 index 000000000000..eaa360b76c25 --- /dev/null +++ b/testing/web-platform/meta/html/canvas/element/manual/draw-element-image/privacy/visited-link/visited-link-svg-radiant-gradiant-ignored.tentative.html.ini @@ -0,0 +1,3 @@ +[visited-link-svg-radiant-gradiant-ignored.tentative.html] + [drawElementImage does not reveal visited link colors] + expected: FAIL diff --git a/testing/web-platform/meta/html/semantics/embedded-content/media-elements/mime-types/canPlayType.html.ini b/testing/web-platform/meta/html/semantics/embedded-content/media-elements/mime-types/canPlayType.html.ini index ba67c1ca90a3..ecf2b407f24b 100644 --- a/testing/web-platform/meta/html/semantics/embedded-content/media-elements/mime-types/canPlayType.html.ini +++ b/testing/web-platform/meta/html/semantics/embedded-content/media-elements/mime-types/canPlayType.html.ini @@ -37,3 +37,6 @@ [video/ogg codecs order] expected: PRECONDITION_FAILED + + [audio/mp4; codecs="iamf.001.001.Opus" (optional)] + expected: PRECONDITION_FAILED diff --git a/testing/web-platform/meta/html/semantics/embedded-content/media-elements/track/track-element/track-remove-quickly.html.ini b/testing/web-platform/meta/html/semantics/embedded-content/media-elements/track/track-element/track-remove-quickly.html.ini index 1c16d64fcb7d..61d2a9a4343d 100644 --- a/testing/web-platform/meta/html/semantics/embedded-content/media-elements/track/track-element/track-remove-quickly.html.ini +++ b/testing/web-platform/meta/html/semantics/embedded-content/media-elements/track/track-element/track-remove-quickly.html.ini @@ -1,3 +1,3 @@ [track-remove-quickly.html] expected: - if (os == "android") and fission: [OK, TIMEOUT] + if (os == "android") and not debug: [OK, TIMEOUT] diff --git a/testing/web-platform/meta/html/semantics/forms/the-select-element/customizable-select/appearance-base-and-base-select.tentative.html.ini b/testing/web-platform/meta/html/semantics/forms/the-select-element/customizable-select/appearance-base-and-base-select.tentative.html.ini new file mode 100644 index 000000000000..b4ee019da59d --- /dev/null +++ b/testing/web-platform/meta/html/semantics/forms/the-select-element/customizable-select/appearance-base-and-base-select.tentative.html.ini @@ -0,0 +1,4 @@ +[appearance-base-and-base-select.tentative.html] + expected: + if (os == "win") and debug and swgl: [PASS, FAIL] + if (os == "win") and debug and not swgl: [PASS, FAIL] diff --git a/testing/web-platform/meta/html/semantics/forms/the-select-element/customizable-select/picker-and-slotted.html.ini b/testing/web-platform/meta/html/semantics/forms/the-select-element/customizable-select/picker-and-slotted.html.ini new file mode 100644 index 000000000000..a5cc8660098d --- /dev/null +++ b/testing/web-platform/meta/html/semantics/forms/the-select-element/customizable-select/picker-and-slotted.html.ini @@ -0,0 +1,3 @@ +[picker-and-slotted.html] + expected: + if not debug and (os == "win"): [PASS, FAIL] diff --git a/testing/web-platform/meta/html/semantics/permission-element/camera/camera-element-use-cases.tentative.https.html.ini b/testing/web-platform/meta/html/semantics/permission-element/camera/camera-element-use-cases.https.html.ini similarity index 74% rename from testing/web-platform/meta/html/semantics/permission-element/camera/camera-element-use-cases.tentative.https.html.ini rename to testing/web-platform/meta/html/semantics/permission-element/camera/camera-element-use-cases.https.html.ini index 9065560eb9f2..c381a9521b64 100644 --- a/testing/web-platform/meta/html/semantics/permission-element/camera/camera-element-use-cases.tentative.https.html.ini +++ b/testing/web-platform/meta/html/semantics/permission-element/camera/camera-element-use-cases.https.html.ini @@ -1,3 +1,3 @@ -[camera-element-use-cases.tentative.https.html] +[camera-element-use-cases.https.html] [A camera element with video constraints requests camera permission and yields single video capture stream on user click] expected: FAIL diff --git a/testing/web-platform/meta/html/semantics/permission-element/camera/camera-error-scenarios.tentative.https.html.ini b/testing/web-platform/meta/html/semantics/permission-element/camera/camera-error-scenarios.https.html.ini similarity index 80% rename from testing/web-platform/meta/html/semantics/permission-element/camera/camera-error-scenarios.tentative.https.html.ini rename to testing/web-platform/meta/html/semantics/permission-element/camera/camera-error-scenarios.https.html.ini index f938b9b18561..e1c5109efee9 100644 --- a/testing/web-platform/meta/html/semantics/permission-element/camera/camera-error-scenarios.tentative.https.html.ini +++ b/testing/web-platform/meta/html/semantics/permission-element/camera/camera-error-scenarios.https.html.ini @@ -1,4 +1,4 @@ -[camera-error-scenarios.tentative.https.html] +[camera-error-scenarios.https.html] [Denying camera permission prompt triggers oncancel event and sets NotAllowedError] expected: FAIL diff --git a/testing/web-platform/meta/html/semantics/permission-element/camera/camera-set-constraints.tentative.https.html.ini b/testing/web-platform/meta/html/semantics/permission-element/camera/camera-set-constraints.https.html.ini similarity index 80% rename from testing/web-platform/meta/html/semantics/permission-element/camera/camera-set-constraints.tentative.https.html.ini rename to testing/web-platform/meta/html/semantics/permission-element/camera/camera-set-constraints.https.html.ini index 8258cdf5d4ff..249ac9a950f9 100644 --- a/testing/web-platform/meta/html/semantics/permission-element/camera/camera-set-constraints.tentative.https.html.ini +++ b/testing/web-platform/meta/html/semantics/permission-element/camera/camera-set-constraints.https.html.ini @@ -1,4 +1,4 @@ -[camera-set-constraints.tentative.https.html] +[camera-set-constraints.https.html] [HTMLCameraElement initializes video track and never audio track] expected: FAIL diff --git a/testing/web-platform/meta/html/semantics/permission-element/camera/camera-track-attribute.tentative.https.html.ini b/testing/web-platform/meta/html/semantics/permission-element/camera/camera-track-attribute.https.html.ini similarity index 65% rename from testing/web-platform/meta/html/semantics/permission-element/camera/camera-track-attribute.tentative.https.html.ini rename to testing/web-platform/meta/html/semantics/permission-element/camera/camera-track-attribute.https.html.ini index 8194c353130d..fe376bac6ef4 100644 --- a/testing/web-platform/meta/html/semantics/permission-element/camera/camera-track-attribute.tentative.https.html.ini +++ b/testing/web-platform/meta/html/semantics/permission-element/camera/camera-track-attribute.https.html.ini @@ -1,3 +1,3 @@ -[camera-track-attribute.tentative.https.html] +[camera-track-attribute.https.html] [HTMLCameraElement has track attribute and ontrack event handler] expected: FAIL diff --git a/testing/web-platform/meta/html/semantics/permission-element/camera/idlharness.tentative.window.js.ini b/testing/web-platform/meta/html/semantics/permission-element/camera/idlharness.window.js.ini similarity index 97% rename from testing/web-platform/meta/html/semantics/permission-element/camera/idlharness.tentative.window.js.ini rename to testing/web-platform/meta/html/semantics/permission-element/camera/idlharness.window.js.ini index 03f6ddde02b8..e0b4cdf41067 100644 --- a/testing/web-platform/meta/html/semantics/permission-element/camera/idlharness.tentative.window.js.ini +++ b/testing/web-platform/meta/html/semantics/permission-element/camera/idlharness.window.js.ini @@ -1,4 +1,4 @@ -[idlharness.tentative.window.html] +[idlharness.window.html] [HTMLCameraElement interface: existence and properties of interface object] expected: FAIL diff --git a/testing/web-platform/meta/html/semantics/permission-element/microphone/idlharness.tentative.window.js.ini b/testing/web-platform/meta/html/semantics/permission-element/microphone/idlharness.window.js.ini similarity index 97% rename from testing/web-platform/meta/html/semantics/permission-element/microphone/idlharness.tentative.window.js.ini rename to testing/web-platform/meta/html/semantics/permission-element/microphone/idlharness.window.js.ini index 06880af76ad4..1d1daf2d56dc 100644 --- a/testing/web-platform/meta/html/semantics/permission-element/microphone/idlharness.tentative.window.js.ini +++ b/testing/web-platform/meta/html/semantics/permission-element/microphone/idlharness.window.js.ini @@ -1,4 +1,4 @@ -[idlharness.tentative.window.html] +[idlharness.window.html] [HTMLMicrophoneElement interface: existence and properties of interface object] expected: FAIL diff --git a/testing/web-platform/meta/html/semantics/permission-element/microphone/microphone-element-use-cases.tentative.https.html.ini b/testing/web-platform/meta/html/semantics/permission-element/microphone/microphone-element-use-cases.https.html.ini similarity index 74% rename from testing/web-platform/meta/html/semantics/permission-element/microphone/microphone-element-use-cases.tentative.https.html.ini rename to testing/web-platform/meta/html/semantics/permission-element/microphone/microphone-element-use-cases.https.html.ini index 47328312db3d..7227cf6d1136 100644 --- a/testing/web-platform/meta/html/semantics/permission-element/microphone/microphone-element-use-cases.tentative.https.html.ini +++ b/testing/web-platform/meta/html/semantics/permission-element/microphone/microphone-element-use-cases.https.html.ini @@ -1,3 +1,3 @@ -[microphone-element-use-cases.tentative.https.html] +[microphone-element-use-cases.https.html] [A microphone element with audio constraints requests microphone permission and yields single audio capture track on user click] expected: FAIL diff --git a/testing/web-platform/meta/html/semantics/permission-element/microphone/microphone-error-scenarios.tentative.https.html.ini b/testing/web-platform/meta/html/semantics/permission-element/microphone/microphone-error-scenarios.https.html.ini similarity index 80% rename from testing/web-platform/meta/html/semantics/permission-element/microphone/microphone-error-scenarios.tentative.https.html.ini rename to testing/web-platform/meta/html/semantics/permission-element/microphone/microphone-error-scenarios.https.html.ini index b50ae4cd9774..696406fda64a 100644 --- a/testing/web-platform/meta/html/semantics/permission-element/microphone/microphone-error-scenarios.tentative.https.html.ini +++ b/testing/web-platform/meta/html/semantics/permission-element/microphone/microphone-error-scenarios.https.html.ini @@ -1,4 +1,4 @@ -[microphone-error-scenarios.tentative.https.html] +[microphone-error-scenarios.https.html] [Denying microphone permission prompt triggers oncancel event and sets NotAllowedError] expected: FAIL diff --git a/testing/web-platform/meta/html/semantics/permission-element/microphone/microphone-set-constraints.tentative.https.html.ini b/testing/web-platform/meta/html/semantics/permission-element/microphone/microphone-set-constraints.https.html.ini similarity index 80% rename from testing/web-platform/meta/html/semantics/permission-element/microphone/microphone-set-constraints.tentative.https.html.ini rename to testing/web-platform/meta/html/semantics/permission-element/microphone/microphone-set-constraints.https.html.ini index 525bb96f9fb8..4d1564a8bcb6 100644 --- a/testing/web-platform/meta/html/semantics/permission-element/microphone/microphone-set-constraints.tentative.https.html.ini +++ b/testing/web-platform/meta/html/semantics/permission-element/microphone/microphone-set-constraints.https.html.ini @@ -1,4 +1,4 @@ -[microphone-set-constraints.tentative.https.html] +[microphone-set-constraints.https.html] [HTMLMicrophoneElement initializes audio track and never video track] expected: FAIL diff --git a/testing/web-platform/meta/html/semantics/permission-element/microphone/microphone-track-attribute.tentative.https.html.ini b/testing/web-platform/meta/html/semantics/permission-element/microphone/microphone-track-attribute.https.html.ini similarity index 64% rename from testing/web-platform/meta/html/semantics/permission-element/microphone/microphone-track-attribute.tentative.https.html.ini rename to testing/web-platform/meta/html/semantics/permission-element/microphone/microphone-track-attribute.https.html.ini index 3ad3f705ca47..f26b16db24d1 100644 --- a/testing/web-platform/meta/html/semantics/permission-element/microphone/microphone-track-attribute.tentative.https.html.ini +++ b/testing/web-platform/meta/html/semantics/permission-element/microphone/microphone-track-attribute.https.html.ini @@ -1,3 +1,3 @@ -[microphone-track-attribute.tentative.https.html] +[microphone-track-attribute.https.html] [HTMLMicrophoneElement has track attribute and ontrack event handler] expected: FAIL diff --git a/testing/web-platform/meta/html/semantics/permission-element/usermedia/idlharness.tentative.window.js.ini b/testing/web-platform/meta/html/semantics/permission-element/usermedia/idlharness.window.js.ini similarity index 99% rename from testing/web-platform/meta/html/semantics/permission-element/usermedia/idlharness.tentative.window.js.ini rename to testing/web-platform/meta/html/semantics/permission-element/usermedia/idlharness.window.js.ini index 9223ac3ca82f..a71cee0c88a8 100644 --- a/testing/web-platform/meta/html/semantics/permission-element/usermedia/idlharness.tentative.window.js.ini +++ b/testing/web-platform/meta/html/semantics/permission-element/usermedia/idlharness.window.js.ini @@ -1,4 +1,4 @@ -[idlharness.tentative.window.html] +[idlharness.window.html] [HTMLUserMediaElement interface: existence and properties of interface object] expected: FAIL @@ -20,6 +20,12 @@ [HTMLUserMediaElement interface: attribute onstream] expected: FAIL + [HTMLUserMediaElement interface: attribute oncancel] + expected: FAIL + + [HTMLUserMediaElement interface: attribute onerror] + expected: FAIL + [HTMLUserMediaElement interface: attribute stream] expected: FAIL @@ -103,9 +109,3 @@ [HTMLGeolocationElement interface: attribute onvalidationstatuschange] expected: FAIL - - [HTMLUserMediaElement interface: attribute onerror] - expected: FAIL - - [HTMLUserMediaElement interface: attribute oncancel] - expected: FAIL diff --git a/testing/web-platform/meta/html/semantics/permission-element/usermedia/invalid-css-properties.tentative.html.ini b/testing/web-platform/meta/html/semantics/permission-element/usermedia/invalid-css-properties.html.ini similarity index 63% rename from testing/web-platform/meta/html/semantics/permission-element/usermedia/invalid-css-properties.tentative.html.ini rename to testing/web-platform/meta/html/semantics/permission-element/usermedia/invalid-css-properties.html.ini index b44c7a1ed244..fb93513adba2 100644 --- a/testing/web-platform/meta/html/semantics/permission-element/usermedia/invalid-css-properties.tentative.html.ini +++ b/testing/web-platform/meta/html/semantics/permission-element/usermedia/invalid-css-properties.html.ini @@ -1,3 +1,3 @@ -[invalid-css-properties.tentative.html] +[invalid-css-properties.html] [None of the listed properties should be applied] expected: FAIL diff --git a/testing/web-platform/meta/html/semantics/permission-element/usermedia/legacy-mode/legacy-mode.tentative.https.html.ini b/testing/web-platform/meta/html/semantics/permission-element/usermedia/legacy-mode/legacy-mode.https.html.ini similarity index 89% rename from testing/web-platform/meta/html/semantics/permission-element/usermedia/legacy-mode/legacy-mode.tentative.https.html.ini rename to testing/web-platform/meta/html/semantics/permission-element/usermedia/legacy-mode/legacy-mode.https.html.ini index 2d6cc798850a..58d652305524 100644 --- a/testing/web-platform/meta/html/semantics/permission-element/usermedia/legacy-mode/legacy-mode.tentative.https.html.ini +++ b/testing/web-platform/meta/html/semantics/permission-element/usermedia/legacy-mode/legacy-mode.https.html.ini @@ -1,4 +1,4 @@ -[legacy-mode.tentative.https.html] +[legacy-mode.https.html] [isTypeSupported existence and behavior] expected: FAIL diff --git a/testing/web-platform/meta/html/semantics/permission-element/usermedia/no-children-rendered.tentative.html.ini b/testing/web-platform/meta/html/semantics/permission-element/usermedia/no-children-rendered.html.ini similarity index 67% rename from testing/web-platform/meta/html/semantics/permission-element/usermedia/no-children-rendered.tentative.html.ini rename to testing/web-platform/meta/html/semantics/permission-element/usermedia/no-children-rendered.html.ini index b18b22545c42..c6cfb2a7242d 100644 --- a/testing/web-platform/meta/html/semantics/permission-element/usermedia/no-children-rendered.tentative.html.ini +++ b/testing/web-platform/meta/html/semantics/permission-element/usermedia/no-children-rendered.html.ini @@ -1,3 +1,3 @@ -[no-children-rendered.tentative.html] +[no-children-rendered.html] [The usermedia element should have no end tag or contents] expected: FAIL diff --git a/testing/web-platform/meta/html/semantics/permission-element/usermedia/no-focus.tentative.html.ini b/testing/web-platform/meta/html/semantics/permission-element/usermedia/no-focus.html.ini similarity index 77% rename from testing/web-platform/meta/html/semantics/permission-element/usermedia/no-focus.tentative.html.ini rename to testing/web-platform/meta/html/semantics/permission-element/usermedia/no-focus.html.ini index 803599c45a0b..a62fd27b1a1d 100644 --- a/testing/web-platform/meta/html/semantics/permission-element/usermedia/no-focus.tentative.html.ini +++ b/testing/web-platform/meta/html/semantics/permission-element/usermedia/no-focus.html.ini @@ -1,3 +1,3 @@ -[no-focus.tentative.html] +[no-focus.html] [UserMedia element is not focusable by script without user activation] expected: FAIL diff --git a/testing/web-platform/meta/html/semantics/permission-element/usermedia/set-constraints-combinations.tentative.https.html.ini b/testing/web-platform/meta/html/semantics/permission-element/usermedia/set-constraints-combinations.https.html.ini similarity index 79% rename from testing/web-platform/meta/html/semantics/permission-element/usermedia/set-constraints-combinations.tentative.https.html.ini rename to testing/web-platform/meta/html/semantics/permission-element/usermedia/set-constraints-combinations.https.html.ini index 8ae306fe9ed9..476aa25b9cce 100644 --- a/testing/web-platform/meta/html/semantics/permission-element/usermedia/set-constraints-combinations.tentative.https.html.ini +++ b/testing/web-platform/meta/html/semantics/permission-element/usermedia/set-constraints-combinations.https.html.ini @@ -1,4 +1,4 @@ -[set-constraints-combinations.tentative.https.html] +[set-constraints-combinations.https.html] [Case 0: No constraints] expected: FAIL diff --git a/testing/web-platform/meta/html/semantics/permission-element/usermedia/usermedia-cancel-prompt.tentative.https.html.ini b/testing/web-platform/meta/html/semantics/permission-element/usermedia/usermedia-cancel-prompt.https.html.ini similarity index 66% rename from testing/web-platform/meta/html/semantics/permission-element/usermedia/usermedia-cancel-prompt.tentative.https.html.ini rename to testing/web-platform/meta/html/semantics/permission-element/usermedia/usermedia-cancel-prompt.https.html.ini index b5ac05ed0aa4..35e261cb2de9 100644 --- a/testing/web-platform/meta/html/semantics/permission-element/usermedia/usermedia-cancel-prompt.tentative.https.html.ini +++ b/testing/web-platform/meta/html/semantics/permission-element/usermedia/usermedia-cancel-prompt.https.html.ini @@ -1,3 +1,3 @@ -[usermedia-cancel-prompt.tentative.https.html] +[usermedia-cancel-prompt.https.html] [Denying PEPC prompt triggers oncancel event and sets NotAllowedError] expected: FAIL diff --git a/testing/web-platform/meta/html/semantics/permission-element/usermedia/usermedia-iframe.tentative.https.html.ini b/testing/web-platform/meta/html/semantics/permission-element/usermedia/usermedia-iframe.https.html.ini similarity index 73% rename from testing/web-platform/meta/html/semantics/permission-element/usermedia/usermedia-iframe.tentative.https.html.ini rename to testing/web-platform/meta/html/semantics/permission-element/usermedia/usermedia-iframe.https.html.ini index bce7707401c3..13d96e6c48d4 100644 --- a/testing/web-platform/meta/html/semantics/permission-element/usermedia/usermedia-iframe.tentative.https.html.ini +++ b/testing/web-platform/meta/html/semantics/permission-element/usermedia/usermedia-iframe.https.html.ini @@ -1,3 +1,3 @@ -[usermedia-iframe.tentative.https.html] +[usermedia-iframe.https.html] [A usermedia element in an iframe without permissions policy delegation should be blocked] expected: FAIL diff --git a/testing/web-platform/meta/html/semantics/permission-element/usermedia/usermedia-set-constraints-sanitization.tentative.https.html.ini b/testing/web-platform/meta/html/semantics/permission-element/usermedia/usermedia-set-constraints-sanitization.https.html.ini similarity index 67% rename from testing/web-platform/meta/html/semantics/permission-element/usermedia/usermedia-set-constraints-sanitization.tentative.https.html.ini rename to testing/web-platform/meta/html/semantics/permission-element/usermedia/usermedia-set-constraints-sanitization.https.html.ini index b72afee6a4d0..ee00c2478eaa 100644 --- a/testing/web-platform/meta/html/semantics/permission-element/usermedia/usermedia-set-constraints-sanitization.tentative.https.html.ini +++ b/testing/web-platform/meta/html/semantics/permission-element/usermedia/usermedia-set-constraints-sanitization.https.html.ini @@ -1,3 +1,3 @@ -[usermedia-set-constraints-sanitization.tentative.https.html] +[usermedia-set-constraints-sanitization.https.html] [HTMLUserMediaElement setConstraints sanitization handles video and audio constraint dictionaries correctly] expected: FAIL diff --git a/testing/web-platform/meta/html/semantics/permission-element/usermedia/usermedia-untrusted-click.tentative.https.html.ini b/testing/web-platform/meta/html/semantics/permission-element/usermedia/usermedia-untrusted-click.https.html.ini similarity index 65% rename from testing/web-platform/meta/html/semantics/permission-element/usermedia/usermedia-untrusted-click.tentative.https.html.ini rename to testing/web-platform/meta/html/semantics/permission-element/usermedia/usermedia-untrusted-click.https.html.ini index 11013531aa86..0f07fff69b32 100644 --- a/testing/web-platform/meta/html/semantics/permission-element/usermedia/usermedia-untrusted-click.tentative.https.html.ini +++ b/testing/web-platform/meta/html/semantics/permission-element/usermedia/usermedia-untrusted-click.https.html.ini @@ -1,3 +1,3 @@ -[usermedia-untrusted-click.tentative.https.html] +[usermedia-untrusted-click.https.html] [Untrusted click on usermedia element should fail with InvalidStateError] expected: FAIL diff --git a/testing/web-platform/meta/html/user-activation/navigate-to-crossorigin-redirect.html.ini b/testing/web-platform/meta/html/user-activation/navigate-to-crossorigin-redirect.html.ini index 846e948aef04..2b0491522c10 100644 --- a/testing/web-platform/meta/html/user-activation/navigate-to-crossorigin-redirect.html.ini +++ b/testing/web-platform/meta/html/user-activation/navigate-to-crossorigin-redirect.html.ini @@ -1,8 +1,10 @@ [navigate-to-crossorigin-redirect.html] expected: - if not debug and (os == "mac") and (version == "OS X 14.7.5"): [TIMEOUT, OK] - if debug: [TIMEOUT, OK] + if (os == "mac") and (version == "OS X 15.3"): TIMEOUT + if (os == "mac") and (version == "OS X 14.7.5"): [TIMEOUT, OK] + if (os == "linux") and debug: [TIMEOUT, OK] [User activation propagation across a same-origin navigation] expected: - if not debug and (os == "mac") and (version == "OS X 14.7.5"): [TIMEOUT, PASS] - if debug: [TIMEOUT, PASS] + if (os == "mac") and (version == "OS X 15.3"): TIMEOUT + if (os == "mac") and (version == "OS X 14.7.5"): [TIMEOUT, PASS] + if (os == "linux") and debug: [TIMEOUT, PASS] diff --git a/testing/web-platform/meta/html/user-activation/navigate-to-sameorigin.html.ini b/testing/web-platform/meta/html/user-activation/navigate-to-sameorigin.html.ini index 398602097d13..c58464d20f96 100644 --- a/testing/web-platform/meta/html/user-activation/navigate-to-sameorigin.html.ini +++ b/testing/web-platform/meta/html/user-activation/navigate-to-sameorigin.html.ini @@ -1,9 +1,9 @@ [navigate-to-sameorigin.html] expected: - if (os == "mac") and (version == "OS X 15.3"): TIMEOUT - if (os == "mac") and (version == "OS X 14.7.5"): [OK, TIMEOUT] - if (os == "linux") and debug: [OK, TIMEOUT] + if not debug and (os == "mac") and (version == "OS X 14.7.5"): [TIMEOUT, OK] + if debug: [TIMEOUT, OK] [User activation propagation across a same-origin navigation] expected: - if (os == "mac") and (version == "OS X 15.3"): [TIMEOUT, FAIL] + if (os == "mac") and (version == "OS X 14.7.5"): [TIMEOUT, FAIL] + if (os == "linux") and debug: [TIMEOUT, FAIL] [FAIL, TIMEOUT] diff --git a/testing/web-platform/meta/largest-contentful-paint/text-fragment-union.html.ini b/testing/web-platform/meta/largest-contentful-paint/text-fragment-union.html.ini new file mode 100644 index 000000000000..3f65c134606c --- /dev/null +++ b/testing/web-platform/meta/largest-contentful-paint/text-fragment-union.html.ini @@ -0,0 +1,4 @@ +[text-fragment-union.html] + expected: ERROR + [Text fragment union: wrapped text LCP size reflects the union of all line boxes.] + expected: TIMEOUT diff --git a/testing/web-platform/meta/long-animation-frame/conditional-measure-time-resolve.html.ini b/testing/web-platform/meta/long-animation-frame/conditional-measure-time-resolve.html.ini new file mode 100644 index 000000000000..011c43081ba2 --- /dev/null +++ b/testing/web-platform/meta/long-animation-frame/conditional-measure-time-resolve.html.ini @@ -0,0 +1,2 @@ +[conditional-measure-time-resolve.html] + expected: ERROR diff --git a/testing/web-platform/meta/long-animation-frame/conditional-tracing-buffer-limit.html.ini b/testing/web-platform/meta/long-animation-frame/conditional-tracing-buffer-limit.html.ini deleted file mode 100644 index d3504b239345..000000000000 --- a/testing/web-platform/meta/long-animation-frame/conditional-tracing-buffer-limit.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[conditional-tracing-buffer-limit.html] - expected: ERROR diff --git a/testing/web-platform/meta/long-animation-frame/conditional-tracing.html.ini b/testing/web-platform/meta/long-animation-frame/conditional-tracing.html.ini deleted file mode 100644 index e806802ed23e..000000000000 --- a/testing/web-platform/meta/long-animation-frame/conditional-tracing.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[conditional-tracing.html] - expected: ERROR diff --git a/testing/web-platform/meta/long-animation-frame/conditional-user-timing-basic.html.ini b/testing/web-platform/meta/long-animation-frame/conditional-user-timing-basic.html.ini new file mode 100644 index 000000000000..d64c1880991b --- /dev/null +++ b/testing/web-platform/meta/long-animation-frame/conditional-user-timing-basic.html.ini @@ -0,0 +1,2 @@ +[conditional-user-timing-basic.html] + expected: ERROR diff --git a/testing/web-platform/meta/long-animation-frame/conditional-user-timing-buffer-limit.html.ini b/testing/web-platform/meta/long-animation-frame/conditional-user-timing-buffer-limit.html.ini new file mode 100644 index 000000000000..55f8e00d2dd1 --- /dev/null +++ b/testing/web-platform/meta/long-animation-frame/conditional-user-timing-buffer-limit.html.ini @@ -0,0 +1,2 @@ +[conditional-user-timing-buffer-limit.html] + expected: ERROR diff --git a/testing/web-platform/meta/media-source/idlharness.any.js.ini b/testing/web-platform/meta/media-source/idlharness.any.js.ini new file mode 100644 index 000000000000..2930f9a69211 --- /dev/null +++ b/testing/web-platform/meta/media-source/idlharness.any.js.ini @@ -0,0 +1,392 @@ +[idlharness.any.worker.html] + [Partial interface AudioTrack: valid exposure set] + expected: FAIL + + [Partial interface VideoTrack: valid exposure set] + expected: FAIL + + [Partial interface TextTrack: valid exposure set] + expected: FAIL + + [MediaSource interface: existence and properties of interface object] + expected: FAIL + + [MediaSource interface object length] + expected: FAIL + + [MediaSource interface object name] + expected: FAIL + + [MediaSource interface: existence and properties of interface prototype object] + expected: FAIL + + [MediaSource interface: existence and properties of interface prototype object's "constructor" property] + expected: FAIL + + [MediaSource interface: existence and properties of interface prototype object's @@unscopables property] + expected: FAIL + + [MediaSource interface: attribute handle] + expected: FAIL + + [MediaSource interface: attribute sourceBuffers] + expected: FAIL + + [MediaSource interface: attribute activeSourceBuffers] + expected: FAIL + + [MediaSource interface: attribute readyState] + expected: FAIL + + [MediaSource interface: attribute duration] + expected: FAIL + + [MediaSource interface: attribute onsourceopen] + expected: FAIL + + [MediaSource interface: attribute onsourceended] + expected: FAIL + + [MediaSource interface: attribute onsourceclose] + expected: FAIL + + [MediaSource interface: attribute canConstructInDedicatedWorker] + expected: FAIL + + [MediaSource interface: operation addSourceBuffer(DOMString)] + expected: FAIL + + [MediaSource interface: operation removeSourceBuffer(SourceBuffer)] + expected: FAIL + + [MediaSource interface: operation endOfStream(optional EndOfStreamError)] + expected: FAIL + + [MediaSource interface: operation setLiveSeekableRange(double, double)] + expected: FAIL + + [MediaSource interface: operation clearLiveSeekableRange()] + expected: FAIL + + [MediaSource interface: operation isTypeSupported(DOMString)] + expected: FAIL + + [MediaSourceHandle interface: existence and properties of interface object] + expected: FAIL + + [MediaSourceHandle interface object length] + expected: FAIL + + [MediaSourceHandle interface object name] + expected: FAIL + + [MediaSourceHandle interface: existence and properties of interface prototype object] + expected: FAIL + + [MediaSourceHandle interface: existence and properties of interface prototype object's "constructor" property] + expected: FAIL + + [MediaSourceHandle interface: existence and properties of interface prototype object's @@unscopables property] + expected: FAIL + + [SourceBuffer interface: existence and properties of interface object] + expected: FAIL + + [SourceBuffer interface object length] + expected: FAIL + + [SourceBuffer interface object name] + expected: FAIL + + [SourceBuffer interface: existence and properties of interface prototype object] + expected: FAIL + + [SourceBuffer interface: existence and properties of interface prototype object's "constructor" property] + expected: FAIL + + [SourceBuffer interface: existence and properties of interface prototype object's @@unscopables property] + expected: FAIL + + [SourceBuffer interface: attribute mode] + expected: FAIL + + [SourceBuffer interface: attribute updating] + expected: FAIL + + [SourceBuffer interface: attribute buffered] + expected: FAIL + + [SourceBuffer interface: attribute timestampOffset] + expected: FAIL + + [SourceBuffer interface: member audioTracks] + expected: FAIL + + [SourceBuffer interface: member videoTracks] + expected: FAIL + + [SourceBuffer interface: member textTracks] + expected: FAIL + + [SourceBuffer interface: attribute appendWindowStart] + expected: FAIL + + [SourceBuffer interface: attribute appendWindowEnd] + expected: FAIL + + [SourceBuffer interface: attribute onupdatestart] + expected: FAIL + + [SourceBuffer interface: attribute onupdate] + expected: FAIL + + [SourceBuffer interface: attribute onupdateend] + expected: FAIL + + [SourceBuffer interface: attribute onerror] + expected: FAIL + + [SourceBuffer interface: attribute onabort] + expected: FAIL + + [SourceBuffer interface: operation appendBuffer(BufferSource)] + expected: FAIL + + [SourceBuffer interface: operation abort()] + expected: FAIL + + [SourceBuffer interface: operation changeType(DOMString)] + expected: FAIL + + [SourceBuffer interface: operation remove(double, unrestricted double)] + expected: FAIL + + [SourceBufferList interface: existence and properties of interface object] + expected: FAIL + + [SourceBufferList interface object length] + expected: FAIL + + [SourceBufferList interface object name] + expected: FAIL + + [SourceBufferList interface: existence and properties of interface prototype object] + expected: FAIL + + [SourceBufferList interface: existence and properties of interface prototype object's "constructor" property] + expected: FAIL + + [SourceBufferList interface: existence and properties of interface prototype object's @@unscopables property] + expected: FAIL + + [SourceBufferList interface: attribute length] + expected: FAIL + + [SourceBufferList interface: attribute onaddsourcebuffer] + expected: FAIL + + [SourceBufferList interface: attribute onremovesourcebuffer] + expected: FAIL + + [ManagedMediaSource interface: existence and properties of interface object] + expected: FAIL + + [ManagedMediaSource interface object length] + expected: FAIL + + [ManagedMediaSource interface object name] + expected: FAIL + + [ManagedMediaSource interface: existence and properties of interface prototype object] + expected: FAIL + + [ManagedMediaSource interface: existence and properties of interface prototype object's "constructor" property] + expected: FAIL + + [ManagedMediaSource interface: existence and properties of interface prototype object's @@unscopables property] + expected: FAIL + + [ManagedMediaSource interface: attribute streaming] + expected: FAIL + + [ManagedMediaSource interface: attribute onstartstreaming] + expected: FAIL + + [ManagedMediaSource interface: attribute onendstreaming] + expected: FAIL + + [BufferedChangeEvent interface: existence and properties of interface object] + expected: FAIL + + [BufferedChangeEvent interface object length] + expected: FAIL + + [BufferedChangeEvent interface object name] + expected: FAIL + + [BufferedChangeEvent interface: existence and properties of interface prototype object] + expected: FAIL + + [BufferedChangeEvent interface: existence and properties of interface prototype object's "constructor" property] + expected: FAIL + + [BufferedChangeEvent interface: existence and properties of interface prototype object's @@unscopables property] + expected: FAIL + + [BufferedChangeEvent interface: attribute addedRanges] + expected: FAIL + + [BufferedChangeEvent interface: attribute removedRanges] + expected: FAIL + + [ManagedSourceBuffer interface: existence and properties of interface object] + expected: FAIL + + [ManagedSourceBuffer interface object length] + expected: FAIL + + [ManagedSourceBuffer interface object name] + expected: FAIL + + [ManagedSourceBuffer interface: existence and properties of interface prototype object] + expected: FAIL + + [ManagedSourceBuffer interface: existence and properties of interface prototype object's "constructor" property] + expected: FAIL + + [ManagedSourceBuffer interface: existence and properties of interface prototype object's @@unscopables property] + expected: FAIL + + [ManagedSourceBuffer interface: attribute onbufferedchange] + expected: FAIL + + +[idlharness.any.html] + [Partial interface AudioTrack: valid exposure set] + expected: FAIL + + [Partial interface VideoTrack: valid exposure set] + expected: FAIL + + [Partial interface TextTrack: valid exposure set] + expected: FAIL + + [MediaSource interface: attribute canConstructInDedicatedWorker] + expected: FAIL + + [MediaSourceHandle interface: existence and properties of interface object] + expected: FAIL + + [MediaSourceHandle interface object length] + expected: FAIL + + [MediaSourceHandle interface object name] + expected: FAIL + + [MediaSourceHandle interface: existence and properties of interface prototype object] + expected: FAIL + + [MediaSourceHandle interface: existence and properties of interface prototype object's "constructor" property] + expected: FAIL + + [MediaSourceHandle interface: existence and properties of interface prototype object's @@unscopables property] + expected: FAIL + + [SourceBuffer interface: attribute audioTracks] + expected: FAIL + + [SourceBuffer interface: attribute videoTracks] + expected: FAIL + + [SourceBuffer interface: attribute textTracks] + expected: FAIL + + [SourceBuffer interface: sourceBuffer must inherit property "audioTracks" with the proper type] + expected: FAIL + + [SourceBuffer interface: sourceBuffer must inherit property "videoTracks" with the proper type] + expected: FAIL + + [SourceBuffer interface: sourceBuffer must inherit property "textTracks" with the proper type] + expected: FAIL + + [ManagedMediaSource interface: existence and properties of interface object] + expected: FAIL + + [ManagedMediaSource interface object length] + expected: FAIL + + [ManagedMediaSource interface object name] + expected: FAIL + + [ManagedMediaSource interface: existence and properties of interface prototype object] + expected: FAIL + + [ManagedMediaSource interface: existence and properties of interface prototype object's "constructor" property] + expected: FAIL + + [ManagedMediaSource interface: existence and properties of interface prototype object's @@unscopables property] + expected: FAIL + + [ManagedMediaSource interface: attribute streaming] + expected: FAIL + + [ManagedMediaSource interface: attribute onstartstreaming] + expected: FAIL + + [ManagedMediaSource interface: attribute onendstreaming] + expected: FAIL + + [BufferedChangeEvent interface: existence and properties of interface object] + expected: FAIL + + [BufferedChangeEvent interface object length] + expected: FAIL + + [BufferedChangeEvent interface object name] + expected: FAIL + + [BufferedChangeEvent interface: existence and properties of interface prototype object] + expected: FAIL + + [BufferedChangeEvent interface: existence and properties of interface prototype object's "constructor" property] + expected: FAIL + + [BufferedChangeEvent interface: existence and properties of interface prototype object's @@unscopables property] + expected: FAIL + + [BufferedChangeEvent interface: attribute addedRanges] + expected: FAIL + + [BufferedChangeEvent interface: attribute removedRanges] + expected: FAIL + + [ManagedSourceBuffer interface: existence and properties of interface object] + expected: FAIL + + [ManagedSourceBuffer interface object length] + expected: FAIL + + [ManagedSourceBuffer interface object name] + expected: FAIL + + [ManagedSourceBuffer interface: existence and properties of interface prototype object] + expected: FAIL + + [ManagedSourceBuffer interface: existence and properties of interface prototype object's "constructor" property] + expected: FAIL + + [ManagedSourceBuffer interface: existence and properties of interface prototype object's @@unscopables property] + expected: FAIL + + [ManagedSourceBuffer interface: attribute onbufferedchange] + expected: FAIL + + [AudioTrack interface: attribute sourceBuffer] + expected: FAIL + + [VideoTrack interface: attribute sourceBuffer] + expected: FAIL + + [TextTrack interface: attribute sourceBuffer] + expected: FAIL diff --git a/testing/web-platform/meta/media-source/idlharness.window.js.ini b/testing/web-platform/meta/media-source/idlharness.window.js.ini deleted file mode 100644 index 37ea95423028..000000000000 --- a/testing/web-platform/meta/media-source/idlharness.window.js.ini +++ /dev/null @@ -1,131 +0,0 @@ -[idlharness.window.html] - expected: - if (os == "win") and debug and (processor == "x86_64"): [OK, ERROR] - [SourceBuffer interface: sourceBuffer must inherit property "textTracks" with the proper type] - expected: FAIL - - [SourceBuffer interface: attribute videoTracks] - expected: FAIL - - [SourceBuffer interface: attribute textTracks] - expected: FAIL - - [SourceBuffer interface: attribute audioTracks] - expected: FAIL - - [SourceBuffer interface: sourceBuffer must inherit property "audioTracks" with the proper type] - expected: FAIL - - [SourceBuffer interface: sourceBuffer must inherit property "videoTracks" with the proper type] - expected: FAIL - - [VideoTrack interface: attribute sourceBuffer] - expected: FAIL - - [TextTrack interface: attribute sourceBuffer] - expected: FAIL - - [AudioTrack interface: attribute sourceBuffer] - expected: FAIL - - [Partial interface AudioTrack: valid exposure set] - expected: FAIL - - [Partial interface VideoTrack: valid exposure set] - expected: FAIL - - [Partial interface TextTrack: valid exposure set] - expected: FAIL - - [MediaSource interface: attribute canConstructInDedicatedWorker] - expected: FAIL - - [MediaSourceHandle interface: existence and properties of interface object] - expected: FAIL - - [MediaSourceHandle interface object length] - expected: FAIL - - [MediaSourceHandle interface object name] - expected: FAIL - - [MediaSourceHandle interface: existence and properties of interface prototype object] - expected: FAIL - - [MediaSourceHandle interface: existence and properties of interface prototype object's "constructor" property] - expected: FAIL - - [MediaSourceHandle interface: existence and properties of interface prototype object's @@unscopables property] - expected: FAIL - - [ManagedMediaSource interface: existence and properties of interface object] - expected: FAIL - - [ManagedMediaSource interface object length] - expected: FAIL - - [ManagedMediaSource interface object name] - expected: FAIL - - [ManagedMediaSource interface: existence and properties of interface prototype object] - expected: FAIL - - [ManagedMediaSource interface: existence and properties of interface prototype object's "constructor" property] - expected: FAIL - - [ManagedMediaSource interface: existence and properties of interface prototype object's @@unscopables property] - expected: FAIL - - [ManagedMediaSource interface: attribute streaming] - expected: FAIL - - [ManagedMediaSource interface: attribute onstartstreaming] - expected: FAIL - - [ManagedMediaSource interface: attribute onendstreaming] - expected: FAIL - - [BufferedChangeEvent interface: existence and properties of interface object] - expected: FAIL - - [BufferedChangeEvent interface object length] - expected: FAIL - - [BufferedChangeEvent interface object name] - expected: FAIL - - [BufferedChangeEvent interface: existence and properties of interface prototype object] - expected: FAIL - - [BufferedChangeEvent interface: existence and properties of interface prototype object's "constructor" property] - expected: FAIL - - [BufferedChangeEvent interface: existence and properties of interface prototype object's @@unscopables property] - expected: FAIL - - [BufferedChangeEvent interface: attribute addedRanges] - expected: FAIL - - [BufferedChangeEvent interface: attribute removedRanges] - expected: FAIL - - [ManagedSourceBuffer interface: existence and properties of interface object] - expected: FAIL - - [ManagedSourceBuffer interface object length] - expected: FAIL - - [ManagedSourceBuffer interface object name] - expected: FAIL - - [ManagedSourceBuffer interface: existence and properties of interface prototype object] - expected: FAIL - - [ManagedSourceBuffer interface: existence and properties of interface prototype object's "constructor" property] - expected: FAIL - - [ManagedSourceBuffer interface: existence and properties of interface prototype object's @@unscopables property] - expected: FAIL - - [ManagedSourceBuffer interface: attribute onbufferedchange] - expected: FAIL diff --git a/testing/web-platform/meta/mediacapture-record/passthrough/MediaRecorder-passthrough.https.html.ini b/testing/web-platform/meta/mediacapture-record/passthrough/MediaRecorder-passthrough.https.html.ini index 1567426fcc74..f715c41b20cd 100644 --- a/testing/web-platform/meta/mediacapture-record/passthrough/MediaRecorder-passthrough.https.html.ini +++ b/testing/web-platform/meta/mediacapture-record/passthrough/MediaRecorder-passthrough.https.html.ini @@ -1,21 +1,31 @@ [MediaRecorder-passthrough.https.html] bug: 1709960 expected: - if (os == "mac") and debug: [OK, TIMEOUT] + if (os == "mac") and debug: [TIMEOUT, OK] if (os == "mac") and not debug: [OK, TIMEOUT] [PeerConnection passthrough MediaRecorder receives VP9 after onstart with a video stream.] bug: 1709960 - expected: FAIL + expected: + if (processor == "aarch64") and debug: NOTRUN + FAIL [PeerConnection passthrough MediaRecorder receives VP9 after onstart with a audio/video stream.] bug: 1709960 - expected: FAIL + expected: + if (processor == "aarch64") and debug: NOTRUN + FAIL [PeerConnection passthrough MediaRecorder receives VP8 after onstart with a video stream.] bug: 1709960 + expected: + if (processor == "aarch64") and debug: TIMEOUT [PeerConnection passthrough MediaRecorder receives VP8 after onstart with a audio/video stream.] bug: 1709960 + expected: + if (processor == "aarch64") and debug: NOTRUN [PeerConnection passthrough MediaRecorder should be prepared to handle the codec switching from VP8 to VP9] bug: 1709960 + expected: + if (processor == "aarch64") and debug: NOTRUN diff --git a/testing/web-platform/meta/mediacapture-streams/MediaStreamTrack-iframe-audio-transfer.https.html.ini b/testing/web-platform/meta/mediacapture-streams/MediaStreamTrack-iframe-audio-transfer.https.html.ini index 207ee7d6c658..c82f751df9b6 100644 --- a/testing/web-platform/meta/mediacapture-streams/MediaStreamTrack-iframe-audio-transfer.https.html.ini +++ b/testing/web-platform/meta/mediacapture-streams/MediaStreamTrack-iframe-audio-transfer.https.html.ini @@ -1,11 +1,13 @@ [MediaStreamTrack-iframe-audio-transfer.https.html] expected: - if os == "win": ERROR - if os == "linux": ERROR + if (os == "mac") and not debug and (processor == "x86_64"): [OK, ERROR, TIMEOUT] + if (os == "mac") and not debug and (processor == "aarch64"): [ERROR, OK, TIMEOUT] + if (os == "mac") and debug: [ERROR, OK, TIMEOUT] if os == "android": [OK, TIMEOUT] - [ERROR, OK, TIMEOUT] + ERROR [MediaStreamTrack transfer to iframe] expected: + if (os == "mac") and not debug and (processor == "x86_64"): FAIL if (os == "mac") and debug: [TIMEOUT, FAIL] if os == "android": FAIL TIMEOUT diff --git a/testing/web-platform/meta/mediacapture-streams/parallel-capture-requests.https.html.ini b/testing/web-platform/meta/mediacapture-streams/parallel-capture-requests.https.html.ini index a22298243bf2..61d6628be7ce 100644 --- a/testing/web-platform/meta/mediacapture-streams/parallel-capture-requests.https.html.ini +++ b/testing/web-platform/meta/mediacapture-streams/parallel-capture-requests.https.html.ini @@ -1,10 +1,12 @@ [parallel-capture-requests.https.html] [getDisplayMedia() and parallel getUserMedia()] expected: + if (os == "mac") and not debug and (processor == "x86_64"): FAIL if (os == "mac") and debug: [PASS, FAIL] if os == "android": FAIL [getUserMedia() and parallel getDisplayMedia()] expected: + if (os == "mac") and not debug and (processor == "x86_64"): FAIL if (os == "mac") and debug: [PASS, FAIL] if os == "android": FAIL diff --git a/testing/web-platform/meta/mozilla-sync b/testing/web-platform/meta/mozilla-sync index d2056406e677..c887ffda74e1 100644 --- a/testing/web-platform/meta/mozilla-sync +++ b/testing/web-platform/meta/mozilla-sync @@ -1 +1 @@ -upstream: b66a69abddc5bb494d500e4c18debbb2e363ff0b +upstream: 816bbf3ebae17dc6866deb65b2286b1a1c162819 diff --git a/testing/web-platform/meta/navigation-api/scroll-behavior/manual-scroll-clears-target-when-fragment-does-not-exist.html.ini b/testing/web-platform/meta/navigation-api/scroll-behavior/manual-scroll-clears-target-when-fragment-does-not-exist.html.ini new file mode 100644 index 000000000000..861bce62e618 --- /dev/null +++ b/testing/web-platform/meta/navigation-api/scroll-behavior/manual-scroll-clears-target-when-fragment-does-not-exist.html.ini @@ -0,0 +1,3 @@ +[manual-scroll-clears-target-when-fragment-does-not-exist.html] + [scroll: scroll() should clear the CSS :target element when the fragment does not exist] + expected: FAIL diff --git a/testing/web-platform/meta/navigation-api/scroll-behavior/manual-scroll-clears-target-when-no-fragment.html.ini b/testing/web-platform/meta/navigation-api/scroll-behavior/manual-scroll-clears-target-when-no-fragment.html.ini new file mode 100644 index 000000000000..80e0f51d4b9a --- /dev/null +++ b/testing/web-platform/meta/navigation-api/scroll-behavior/manual-scroll-clears-target-when-no-fragment.html.ini @@ -0,0 +1,3 @@ +[manual-scroll-clears-target-when-no-fragment.html] + [scroll: scroll() should clear the CSS :target element when the destination url contains no fragment] + expected: FAIL diff --git a/testing/web-platform/meta/pointerevents/pointerlock/pointerevent_coordinates_when_locked.html.ini b/testing/web-platform/meta/pointerevents/pointerlock/pointerevent_coordinates_when_locked.html.ini index fdae3ec6665f..8f7c206ff4ca 100644 --- a/testing/web-platform/meta/pointerevents/pointerlock/pointerevent_coordinates_when_locked.html.ini +++ b/testing/web-platform/meta/pointerevents/pointerlock/pointerevent_coordinates_when_locked.html.ini @@ -3,6 +3,7 @@ if (os == "linux") and debug and fission: [OK, ERROR] [mouse Test pointerevent coordinates when pointer is locked] expected: + if (os == "mac") and (processor == "x86_64"): PASS + if (os == "mac") and (processor == "aarch64"): FAIL if os == "win": PASS - if os == "mac": FAIL [PASS, FAIL] diff --git a/testing/web-platform/meta/push-api/worker-subscribe.https.window.js.ini b/testing/web-platform/meta/push-api/worker-subscribe.https.window.js.ini index fea366c387c3..5f27bd1b6b78 100644 --- a/testing/web-platform/meta/push-api/worker-subscribe.https.window.js.ini +++ b/testing/web-platform/meta/push-api/worker-subscribe.https.window.js.ini @@ -1,4 +1,4 @@ [worker-subscribe.https.window.html] [Subscribing within a worker] expected: - if (os == "android"): FAIL + if os == "android": FAIL diff --git a/testing/web-platform/meta/referrer-policy/gen/top.http-rp/same-origin/sharedworker-module.http.html.ini b/testing/web-platform/meta/referrer-policy/gen/top.http-rp/same-origin/sharedworker-module.http.html.ini new file mode 100644 index 000000000000..b68d7a4b07f2 --- /dev/null +++ b/testing/web-platform/meta/referrer-policy/gen/top.http-rp/same-origin/sharedworker-module.http.html.ini @@ -0,0 +1,3 @@ +[sharedworker-module.http.html] + expected: + if (os == "android") and not debug: [OK, TIMEOUT] diff --git a/testing/web-platform/meta/resource-timing/tentative/initiator-url/module-script-from-document.html.ini b/testing/web-platform/meta/resource-timing/tentative/initiator-url/module-script-from-document.html.ini new file mode 100644 index 000000000000..29e791269f2d --- /dev/null +++ b/testing/web-platform/meta/resource-timing/tentative/initiator-url/module-script-from-document.html.ini @@ -0,0 +1,15 @@ +[module-script-from-document.html] + [The initiator Url for module-script-imported.js?label=document-src initiatorUrl from document must be 'http://web-platform.test:8000/resource-timing/tentative/initiator-url/module-script-from-document.html'] + expected: FAIL + + [The initiator Url for module-script-imported.js?label=document-static initiatorUrl from document must be 'http://web-platform.test:8000/resource-timing/tentative/initiator-url/module-script-from-document.html'] + expected: FAIL + + [The initiator Url for module-script-imported.js?label=document-dynamic initiatorUrl from document must be 'http://web-platform.test:8000/resource-timing/tentative/initiator-url/module-script-from-document.html'] + expected: FAIL + + [The initiator Url for module-script-imported.js?label=document-add-script initiatorUrl from document must be 'http://web-platform.test:8000/resource-timing/tentative/initiator-url/module-script-from-document.html'] + expected: FAIL + + [The initiator Url for module-script-imported.js?label=document-set-timeout initiatorUrl from document must be 'http://web-platform.test:8000/resource-timing/tentative/initiator-url/module-script-from-document.html'] + expected: FAIL diff --git a/testing/web-platform/meta/resource-timing/tentative/initiator-url/module-script-from-dynamic-importer.html.ini b/testing/web-platform/meta/resource-timing/tentative/initiator-url/module-script-from-dynamic-importer.html.ini new file mode 100644 index 000000000000..aec0e76c4ba4 --- /dev/null +++ b/testing/web-platform/meta/resource-timing/tentative/initiator-url/module-script-from-dynamic-importer.html.ini @@ -0,0 +1,9 @@ +[module-script-from-dynamic-importer.html] + [The initiator Url for dynamic-module-importer-static initiatorUrl from module-script-importer-module-dynamic.js must be 'http://web-platform.test:8000/resource-timing/resources/module-script-importer-module-dynamic.js'] + expected: FAIL + + [The initiator Url for dynamic-module-importer-dynamic initiatorUrl from module-script-importer-module-dynamic.js must be 'http://web-platform.test:8000/resource-timing/resources/module-script-importer-module-dynamic.js'] + expected: FAIL + + [The initiator Url for dynamic-module-importer-add-script initiatorUrl from module-script-importer-module-dynamic.js must be 'http://web-platform.test:8000/resource-timing/resources/module-script-importer-module-dynamic.js'] + expected: FAIL diff --git a/testing/web-platform/meta/resource-timing/tentative/initiator-url/module-script-from-static-importer.html.ini b/testing/web-platform/meta/resource-timing/tentative/initiator-url/module-script-from-static-importer.html.ini new file mode 100644 index 000000000000..d7bef073c235 --- /dev/null +++ b/testing/web-platform/meta/resource-timing/tentative/initiator-url/module-script-from-static-importer.html.ini @@ -0,0 +1,12 @@ +[module-script-from-static-importer.html] + [The initiator Url for static-module-importer-static initiatorUrl from module-script-importer-module-static.js must be 'http://web-platform.test:8000/resource-timing/resources/module-script-importer-module-static.js'] + expected: FAIL + + [The initiator Url for static-module-importer-dynamic initiatorUrl from module-script-importer-module-static.js must be 'http://web-platform.test:8000/resource-timing/resources/module-script-importer-module-static.js'] + expected: FAIL + + [The initiator Url for static-module-importer-add-script initiatorUrl from module-script-importer-module-static.js must be 'http://web-platform.test:8000/resource-timing/resources/module-script-importer-module-static.js'] + expected: FAIL + + [The initiator Url for classic-importer-dynamic initiatorUrl from module-script-importer-classic.js must be 'http://web-platform.test:8000/resource-timing/resources/module-script-importer-classic.js'] + expected: FAIL diff --git a/testing/web-platform/meta/resource-timing/tentative/initiator-url/module-script-from-worker.html.ini b/testing/web-platform/meta/resource-timing/tentative/initiator-url/module-script-from-worker.html.ini new file mode 100644 index 000000000000..60f7e31fab42 --- /dev/null +++ b/testing/web-platform/meta/resource-timing/tentative/initiator-url/module-script-from-worker.html.ini @@ -0,0 +1,6 @@ +[module-script-from-worker.html] + [The initiator Url for worker-importer-static initiatorUrl from worker must be 'http://web-platform.test:8000/resource-timing/resources/module-script-worker.js'] + expected: FAIL + + [The initiator Url for a script imported from a worker must be the worker script] + expected: FAIL diff --git a/testing/web-platform/meta/screen-capture/getdisplaymedia-framerate.https.html.ini b/testing/web-platform/meta/screen-capture/getdisplaymedia-framerate.https.html.ini index 7cbac5572e55..46cae0dd7fff 100644 --- a/testing/web-platform/meta/screen-capture/getdisplaymedia-framerate.https.html.ini +++ b/testing/web-platform/meta/screen-capture/getdisplaymedia-framerate.https.html.ini @@ -1,5 +1,5 @@ [getdisplaymedia-framerate.https.html] [getDisplayMedia() must adhere to frameRate if set] expected: - if (os == "mac") and not debug and (processor == "aarch64"): FAIL + if (os == "mac") and not debug: FAIL if os == "android": FAIL diff --git a/testing/web-platform/meta/screen-capture/getdisplaymedia-settings.https.html.ini b/testing/web-platform/meta/screen-capture/getdisplaymedia-settings.https.html.ini index 392c331d302e..039e376ef098 100644 --- a/testing/web-platform/meta/screen-capture/getdisplaymedia-settings.https.html.ini +++ b/testing/web-platform/meta/screen-capture/getdisplaymedia-settings.https.html.ini @@ -4,4 +4,5 @@ [getDisplayMedia() and facingMode] expected: + if (os == "mac") and not debug: FAIL if os == "android": FAIL diff --git a/testing/web-platform/meta/screen-capture/getdisplaymedia.https.html.ini b/testing/web-platform/meta/screen-capture/getdisplaymedia.https.html.ini index 0e2196c8b1da..950edb12f908 100644 --- a/testing/web-platform/meta/screen-capture/getdisplaymedia.https.html.ini +++ b/testing/web-platform/meta/screen-capture/getdisplaymedia.https.html.ini @@ -48,10 +48,187 @@ [applyConstraints(width or height) must downscale precisely] expected: - if not fission and debug: [PASS, FAIL] + if (os == "linux") and debug and not fission: [PASS, FAIL] + if (os == "mac") and not debug: FAIL [getDisplayMedia({"windowAudio":"invalid"}) must fail with TypeError] expected: FAIL [getDisplayMedia({"audioSelection":"invalid"}) must fail with TypeError] expected: FAIL + + [getDisplayMedia() resolves with stream with video track] + expected: + if (os == "mac") and not debug: FAIL + + [getDisplayMedia({"audio":true}) must succeed with video maybe audio] + expected: + if (os == "mac") and not debug: FAIL + + [getDisplayMedia({"systemAudio":"include"}) must succeed] + expected: + if (os == "mac") and not debug: FAIL + + [getDisplayMedia({video: {"height":60}}) must be downscaled precisely] + expected: + if (os == "mac") and not debug: FAIL + + [applyConstraints({"height":{"max":0}}) for display media must fail with OverconstrainedError] + expected: + if (os == "mac") and not debug: FAIL + + [applyConstraints({"width":{"max":0}}) for display media must fail with OverconstrainedError] + expected: + if (os == "mac") and not debug: FAIL + + [getDisplayMedia({"video":true,"audio":false}) must succeed with video] + expected: + if (os == "mac") and not debug: FAIL + + [getDisplayMedia({"selfBrowserSurface":"exclude"}) must succeed] + expected: + if (os == "mac") and not debug: FAIL + + [getDisplayMedia({video: {"width":{"max":360},"height":{"max":240}}}) must be constrained] + expected: + if (os == "mac") and not debug: FAIL + + [getDisplayMedia({video: {"height":118}}) must be downscaled precisely] + expected: + if (os == "mac") and not debug: FAIL + + [getDisplayMedia({"surfaceSwitching":"include"}) must succeed] + expected: + if (os == "mac") and not debug: FAIL + + [getDisplayMedia({video: {"frameRate":{"max":4},"height":{"max":240}}}) must be constrained] + expected: + if (os == "mac") and not debug: FAIL + + [getDisplayMedia({video: {"width":{"max":360}}}) must be constrained] + expected: + if (os == "mac") and not debug: FAIL + + [getDisplayMedia({video: {"frameRate":{"max":4},"width":{"max":360},"height":{"max":240}}}) must be constrained] + expected: + if (os == "mac") and not debug: FAIL + + [getDisplayMedia({video: {"width":160}}) must be downscaled precisely] + expected: + if (os == "mac") and not debug: FAIL + + [getDisplayMedia({}) must succeed with video] + expected: + if (os == "mac") and not debug: FAIL + + [getDisplayMedia({"video":true}) must succeed with video] + expected: + if (os == "mac") and not debug: FAIL + + [getDisplayMedia({"selfBrowserSurface":"include"}) must succeed] + expected: + if (os == "mac") and not debug: FAIL + + [getDisplayMedia({"video":true,"audio":true}) must succeed with video maybe audio] + expected: + if (os == "mac") and not debug: FAIL + + [getDisplayMedia({"audioSelection":"preferred"}) must succeed] + expected: + if (os == "mac") and not debug: FAIL + + [getDisplayMedia({"video":{}}) must succeed with video] + expected: + if (os == "mac") and not debug: FAIL + + [applyConstraints({"height":{"max":-1}}) for display media must fail with OverconstrainedError] + expected: + if (os == "mac") and not debug: FAIL + + [applyConstraints({"frameRate":{"max":0}}) for display media must fail with OverconstrainedError] + expected: + if (os == "mac") and not debug: FAIL + + [getDisplayMedia({"systemAudio":"exclude"}) must succeed] + expected: + if (os == "mac") and not debug: FAIL + + [getDisplayMedia({"video":{"displaySurface":"monitor"}}) must succeed] + expected: + if (os == "mac") and not debug: FAIL + + [getDisplayMedia({"video":{"displaySurface":"browser"}}) must succeed] + expected: + if (os == "mac") and not debug: FAIL + + [applyConstraints({"width":{"min":100,"max":10}}) for display media must fail with OverconstrainedError] + expected: + if (os == "mac") and not debug: FAIL + + [getDisplayMedia({"video":{"displaySurface":"window"}}) must succeed] + expected: + if (os == "mac") and not debug: FAIL + + [getDisplayMedia({video: {"height":120}}) must be downscaled precisely] + expected: + if (os == "mac") and not debug: FAIL + + [getDisplayMedia({"windowAudio":"window"}) must succeed] + expected: + if (os == "mac") and not debug: FAIL + + [applyConstraints({"width":{"max":-1}}) for display media must fail with OverconstrainedError] + expected: + if (os == "mac") and not debug: FAIL + + [getDisplayMedia({"windowAudio":"system"}) must succeed] + expected: + if (os == "mac") and not debug: FAIL + + [applyConstraints({"frameRate":{"max":-1}}) for display media must fail with OverconstrainedError] + expected: + if (os == "mac") and not debug: FAIL + + [getDisplayMedia({"audio":false}) must succeed with video] + expected: + if (os == "mac") and not debug: FAIL + + [getDisplayMedia({"surfaceSwitching":"exclude"}) must succeed] + expected: + if (os == "mac") and not debug: FAIL + + [getDisplayMedia({video: {"height":{"max":240}}}) must be constrained] + expected: + if (os == "mac") and not debug: FAIL + + [getDisplayMedia({video: {"frameRate":{"max":4},"width":{"max":360}}}) must be constrained] + expected: + if (os == "mac") and not debug: FAIL + + [getDisplayMedia({"windowAudio":"exclude"}) must succeed] + expected: + if (os == "mac") and not debug: FAIL + + [getDisplayMedia(undefined) must succeed with video] + expected: + if (os == "mac") and not debug: FAIL + + [getDisplayMedia({video: {"frameRate":{"max":4}}}) must be constrained] + expected: + if (os == "mac") and not debug: FAIL + + [applyConstraints({"frameRate":{"min":100,"max":10}}) for display media must fail with OverconstrainedError] + expected: + if (os == "mac") and not debug: FAIL + + [applyConstraints({"height":{"min":100,"max":10}}) for display media must fail with OverconstrainedError] + expected: + if (os == "mac") and not debug: FAIL + + [getDisplayMedia({video: {"width":80}}) must be downscaled precisely] + expected: + if (os == "mac") and not debug: FAIL + + [getDisplayMedia({video: {"width":158}}) must be downscaled precisely] + expected: + if (os == "mac") and not debug: FAIL diff --git a/testing/web-platform/meta/screen-capture/permissions-policy-audio+video.https.sub.html.ini b/testing/web-platform/meta/screen-capture/permissions-policy-audio+video.https.sub.html.ini index 5763be50164b..991ecf3e535a 100644 --- a/testing/web-platform/meta/screen-capture/permissions-policy-audio+video.https.sub.html.ini +++ b/testing/web-platform/meta/screen-capture/permissions-policy-audio+video.https.sub.html.ini @@ -1,10 +1,12 @@ [permissions-policy-audio+video.https.sub.html] [Default "display-capture" permissions policy ["self"\] allows the top-level document.] expected: + if (os == "mac") and not debug: FAIL if os == "android": FAIL [Default "display-capture" permissions policy ["self"\] allows same-origin iframes.] expected: + if (os == "mac") and not debug: FAIL if os == "android": FAIL [Default "display-capture" permissions policy ["self"\] disallows cross-origin iframes.] @@ -17,4 +19,5 @@ [permissions policy "display-capture" can be enabled in cross-origin iframes using "allow" attribute.] expected: + if (os == "mac") and not debug: FAIL if os == "android": FAIL diff --git a/testing/web-platform/meta/screen-capture/permissions-policy-audio.https.sub.html.ini b/testing/web-platform/meta/screen-capture/permissions-policy-audio.https.sub.html.ini index a548f81c813c..5af457a71789 100644 --- a/testing/web-platform/meta/screen-capture/permissions-policy-audio.https.sub.html.ini +++ b/testing/web-platform/meta/screen-capture/permissions-policy-audio.https.sub.html.ini @@ -1,10 +1,12 @@ [permissions-policy-audio.https.sub.html] [Default "display-capture" permissions policy ["self"\] allows the top-level document.] expected: + if (os == "mac") and not debug: FAIL if os == "android": FAIL [Default "display-capture" permissions policy ["self"\] allows same-origin iframes.] expected: + if (os == "mac") and not debug: FAIL if os == "android": FAIL [Default "display-capture" permissions policy ["self"\] disallows cross-origin iframes.] @@ -17,4 +19,5 @@ [permissions policy "display-capture" can be enabled in cross-origin iframes using "allow" attribute.] expected: + if (os == "mac") and not debug: FAIL if os == "android": FAIL diff --git a/testing/web-platform/meta/screen-capture/permissions-policy-video.https.sub.html.ini b/testing/web-platform/meta/screen-capture/permissions-policy-video.https.sub.html.ini index 0c20b780f116..06d4cec0adb9 100644 --- a/testing/web-platform/meta/screen-capture/permissions-policy-video.https.sub.html.ini +++ b/testing/web-platform/meta/screen-capture/permissions-policy-video.https.sub.html.ini @@ -4,10 +4,12 @@ if (os == "linux") and not debug: [OK, ERROR] [Default "display-capture" permissions policy ["self"\] allows the top-level document.] expected: + if (os == "mac") and not debug: FAIL if os == "android": FAIL [Default "display-capture" permissions policy ["self"\] allows same-origin iframes.] expected: + if (os == "mac") and not debug: FAIL if os == "android": FAIL [Default "display-capture" permissions policy ["self"\] disallows cross-origin iframes.] @@ -20,4 +22,5 @@ [permissions policy "display-capture" can be enabled in cross-origin iframes using "allow" attribute.] expected: + if (os == "mac") and not debug: FAIL if os == "android": FAIL diff --git a/testing/web-platform/meta/service-workers/service-worker/resource-timing-cross-origin-server-timing.https.html.ini b/testing/web-platform/meta/service-workers/service-worker/resource-timing-cross-origin-server-timing.https.html.ini deleted file mode 100644 index 10cbc002caa1..000000000000 --- a/testing/web-platform/meta/service-workers/service-worker/resource-timing-cross-origin-server-timing.https.html.ini +++ /dev/null @@ -1,3 +0,0 @@ -[resource-timing-cross-origin-server-timing.https.html] - [Timing allow check for subresource responses handled by Service Worker] - expected: FAIL diff --git a/testing/web-platform/meta/svg/struct/scripted/currentScale-outermost.html.ini b/testing/web-platform/meta/svg/struct/scripted/currentScale-outermost.html.ini new file mode 100644 index 000000000000..192aeab86144 --- /dev/null +++ b/testing/web-platform/meta/svg/struct/scripted/currentScale-outermost.html.ini @@ -0,0 +1,3 @@ +[currentScale-outermost.html] + [currentScale is inert on a non-outermost (nested) svg element] + expected: FAIL diff --git a/testing/web-platform/meta/svg/types/scripted/SVGAnimatedEnumeration-initial-values.html.ini b/testing/web-platform/meta/svg/types/scripted/SVGAnimatedEnumeration-initial-values.html.ini new file mode 100644 index 000000000000..c751425f721e --- /dev/null +++ b/testing/web-platform/meta/svg/types/scripted/SVGAnimatedEnumeration-initial-values.html.ini @@ -0,0 +1,6 @@ +[SVGAnimatedEnumeration-initial-values.html] + [SVGAnimatedEnumeration, initial values, SVGFEGaussianBlurElement.prototype.edgeMode (remove)] + expected: FAIL + + [SVGAnimatedEnumeration, initial values, SVGFEGaussianBlurElement.prototype.edgeMode (invalid value)] + expected: FAIL diff --git a/testing/web-platform/meta/svg/types/scripted/SVGAnimatedEnumeration-invalid-values.html.ini b/testing/web-platform/meta/svg/types/scripted/SVGAnimatedEnumeration-invalid-values.html.ini new file mode 100644 index 000000000000..ab4d0683e6de --- /dev/null +++ b/testing/web-platform/meta/svg/types/scripted/SVGAnimatedEnumeration-invalid-values.html.ini @@ -0,0 +1,6 @@ +[SVGAnimatedEnumeration-invalid-values.html] + [SVGAnimatedEnumeration, invalid values, SVGFEGaussianBlurElement.prototype.edgeMode (empty string)] + expected: FAIL + + [SVGAnimatedEnumeration, invalid values, SVGFEGaussianBlurElement.prototype.edgeMode (content attribute is not rewritten)] + expected: FAIL diff --git a/testing/web-platform/meta/svg/types/scripted/SVGAnimatedEnumeration-keywords.html.ini b/testing/web-platform/meta/svg/types/scripted/SVGAnimatedEnumeration-keywords.html.ini new file mode 100644 index 000000000000..786e624be9b0 --- /dev/null +++ b/testing/web-platform/meta/svg/types/scripted/SVGAnimatedEnumeration-keywords.html.ini @@ -0,0 +1,6 @@ +[SVGAnimatedEnumeration-keywords.html] + [SVGAnimatedEnumeration, keyword values, SVGFEGaussianBlurElement.prototype.edgeMode (keywords)] + expected: FAIL + + [SVGAnimatedEnumeration, keyword values, SVGFEGaussianBlurElement.prototype.edgeMode (case-sensitive)] + expected: FAIL diff --git a/testing/web-platform/meta/web-extensions/browser.storage.extension.js.ini b/testing/web-platform/meta/web-extensions/browser.storage.extension.js.ini index 6d815e05f5b9..1680851bb242 100644 --- a/testing/web-platform/meta/web-extensions/browser.storage.extension.js.ini +++ b/testing/web-platform/meta/web-extensions/browser.storage.extension.js.ini @@ -1,21 +1,17 @@ [browser.storage.extension.html] expected: - if (os == "linux") and not tsan and debug and not fission: [OK, TIMEOUT] - if (os == "linux") and not tsan and not debug and asan: [OK, TIMEOUT] - if (os == "linux") and not tsan and not debug and not asan: [OK, TIMEOUT] - if (os == "mac") and (processor == "x86_64") and (version == "OS X 15.3"): TIMEOUT + if (os == "linux") and fission and not debug and asan: [TIMEOUT, OK] + if (os == "linux") and fission and not debug and not asan: [OK, TIMEOUT] if (os == "win") and debug: [OK, TIMEOUT] - if (os == "linux") and tsan: [TIMEOUT, OK] + if (os == "linux") and not fission: [OK, TIMEOUT] if os == "android": ERROR [testStorageOnChanged] expected: - if (os == "linux") and not tsan and debug and not fission: [PASS, NOTRUN] - if (os == "linux") and not tsan and not debug and asan: [PASS, NOTRUN] - if (os == "linux") and not tsan and not debug and not asan: [PASS, NOTRUN] + if (os == "linux") and fission and not debug and asan: [NOTRUN, PASS] + if (os == "linux") and fission and not debug and not asan: [PASS, NOTRUN] if (os == "win") and debug and (processor == "x86_64"): [PASS, FAIL] if (os == "win") and debug and (processor == "x86"): [PASS, FAIL, NOTRUN] - if (os == "mac") and (processor == "x86_64") and (version == "OS X 15.3"): NOTRUN - if (os == "linux") and tsan: [NOTRUN, PASS] + if (os == "linux") and not fission: [PASS, NOTRUN] [testStorageSetAccessLevelTrustedContexts] expected: FAIL diff --git a/testing/web-platform/meta/webmcp/declarative/executeTool-respondWith-circular-object.https.html.ini b/testing/web-platform/meta/webmcp/declarative/executeTool-respondWith-circular-object.https.html.ini new file mode 100644 index 000000000000..6162fdeb12b0 --- /dev/null +++ b/testing/web-platform/meta/webmcp/declarative/executeTool-respondWith-circular-object.https.html.ini @@ -0,0 +1,3 @@ +[executeTool-respondWith-circular-object.https.html] + [Declarative tool executeTool() rejects when respondWith() receives a circular object] + expected: FAIL diff --git a/testing/web-platform/meta/webmcp/imperative/executeTool-error-window-onerror.https.html.ini b/testing/web-platform/meta/webmcp/imperative/executeTool-error-window-onerror.https.html.ini index 7a5fb9f6d16b..703b55fd629d 100644 --- a/testing/web-platform/meta/webmcp/imperative/executeTool-error-window-onerror.https.html.ini +++ b/testing/web-platform/meta/webmcp/imperative/executeTool-error-window-onerror.https.html.ini @@ -1,3 +1,6 @@ [executeTool-error-window-onerror.https.html] [Failed tool execution does not trigger window.onerror] expected: FAIL + + [Tool execution returning circular object rejects and does not trigger window.onerror] + expected: FAIL diff --git a/testing/web-platform/meta/webmcp/imperative/exposedTo-invalid-origins.https.html.ini b/testing/web-platform/meta/webmcp/imperative/exposedTo-invalid-origins.https.html.ini index 63251f179798..e7139fc4419c 100644 --- a/testing/web-platform/meta/webmcp/imperative/exposedTo-invalid-origins.https.html.ini +++ b/testing/web-platform/meta/webmcp/imperative/exposedTo-invalid-origins.https.html.ini @@ -64,3 +64,6 @@ [registerTool() with abort signal reason because signal is processed before `exposedTo`] expected: FAIL + + [Aborting a signal from a rejected registration must not unregister a later valid tool with the same name] + expected: FAIL diff --git a/testing/web-platform/meta/websockets/constructor/option-bag.any.js.ini b/testing/web-platform/meta/websockets/constructor/option-bag.any.js.ini new file mode 100644 index 000000000000..95afc2f14b41 --- /dev/null +++ b/testing/web-platform/meta/websockets/constructor/option-bag.any.js.ini @@ -0,0 +1,30 @@ +[option-bag.any.html?wss] + [Empty option bag should be accepted] + expected: FAIL + + [Option bag with protocols array should be accepted] + expected: FAIL + + +[option-bag.any.worker.html?default] + [Empty option bag should be accepted] + expected: FAIL + + [Option bag with protocols array should be accepted] + expected: FAIL + + +[option-bag.any.html?default] + [Empty option bag should be accepted] + expected: FAIL + + [Option bag with protocols array should be accepted] + expected: FAIL + + +[option-bag.any.worker.html?wss] + [Empty option bag should be accepted] + expected: FAIL + + [Option bag with protocols array should be accepted] + expected: FAIL diff --git a/testing/web-platform/mozilla/meta/media-capabilities/decodingInfo-webrtc-power-efficient.any.js.ini b/testing/web-platform/mozilla/meta/media-capabilities/decodingInfo-webrtc-power-efficient.any.js.ini index 3260edba3279..2838dd6e76d0 100644 --- a/testing/web-platform/mozilla/meta/media-capabilities/decodingInfo-webrtc-power-efficient.any.js.ini +++ b/testing/web-platform/mozilla/meta/media-capabilities/decodingInfo-webrtc-power-efficient.any.js.ini @@ -12,6 +12,10 @@ expected: if os == "mac": FAIL + [decodingInfo: video/AV1 7680x4320 is not powerEfficient] + expected: + if os == "mac": FAIL + [decodingInfo-webrtc-power-efficient.any.html] [decodingInfo: video/VP8 7680x4320 is not powerEfficient] @@ -26,3 +30,7 @@ [decodingInfo: video/H264 7680x4320 is not powerEfficient] expected: if os == "mac": FAIL + + [decodingInfo: video/AV1 7680x4320 is not powerEfficient] + expected: + if os == "mac": FAIL diff --git a/testing/web-platform/tests/content-security-policy/object-src/object-src-pdf-byte-range-allowed.html b/testing/web-platform/tests/content-security-policy/object-src/object-src-pdf-byte-range-allowed.html new file mode 100644 index 000000000000..0a044d6742bd --- /dev/null +++ b/testing/web-platform/tests/content-security-policy/object-src/object-src-pdf-byte-range-allowed.html @@ -0,0 +1,32 @@ + + + + + Byte-range requests for an object's PDF are governed by object-src, not connect-src + + + + + + + + diff --git a/testing/web-platform/tests/content-security-policy/support/incremental-pdf.py b/testing/web-platform/tests/content-security-policy/support/incremental-pdf.py new file mode 100644 index 000000000000..7cfd1797f8b2 --- /dev/null +++ b/testing/web-platform/tests/content-security-policy/support/incremental-pdf.py @@ -0,0 +1,46 @@ +import os +import re +import time + +from wptserve.utils import isomorphic_decode + +BYTE_RANGE_RE = re.compile(r"bytes=(\d+)-(\d+)?$") + + +def main(request, response): + chunk_size = int(request.GET.first(b"chunksize", b"1024")) + chunk_delay = float(request.GET.first(b"chunkdelay", b"16")) / 1E3 + + path = os.path.join(os.path.dirname(os.path.abspath(__file__)), u"linearized.pdf") + with open(path, u"rb") as file: + content = file.read() + + total_length = len(content) + first_byte = 0 + last_byte = total_length - 1 + + range_header = request.headers.get(b"Range", b"") + if range_header: + match = BYTE_RANGE_RE.match(isomorphic_decode(range_header)) + if not match: + response.status = 416 + return b"" + first_byte = int(match.group(1)) + if match.group(2) is not None: + last_byte = min(int(match.group(2)), last_byte) + response.status = 206 + response.headers.set(b"Content-Range", b"bytes %d-%d/%d" % (first_byte, last_byte, total_length)) + else: + response.status = 200 + + content = content[first_byte:last_byte + 1] + + response.headers.set(b"Content-Type", b"application/pdf") + response.headers.set(b"Accept-Ranges", b"bytes") + response.headers.set(b"Content-Length", b"%d" % len(content)) + response.headers.set(b"Cache-Control", b"no-cache, no-store, must-revalidate") + response.write_status_headers() + + for offset in range(0, len(content), chunk_size): + response.writer.write_content(content[offset:offset + chunk_size]) + time.sleep(chunk_delay) diff --git a/testing/web-platform/tests/content-security-policy/support/linearized.pdf b/testing/web-platform/tests/content-security-policy/support/linearized.pdf new file mode 100644 index 000000000000..1369212bc6fb Binary files /dev/null and b/testing/web-platform/tests/content-security-policy/support/linearized.pdf differ diff --git a/testing/web-platform/tests/core-aam/aamtests/role/alert.py b/testing/web-platform/tests/core-aam/aamtests/role/alert.py index 0c24a2518619..7c5a2035d63f 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/alert.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/alert.py @@ -25,10 +25,15 @@ def test_atspi(atspi, session, inline): # # Role: ROLE_SYSTEM_ALERT # # Event: EVENT_SYSTEM_ALERT: . -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Group -# # Localized Control Type: alert -# # LiveSetting: Assertive (2) +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Group + # Localized Control Type: alert + # LiveSetting: Assertive (2) + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Group + assert node.CurrentLocalizedControlType == "alert" + assert node.GetCurrentPropertyValue(uia.PropertyId.LiveSetting) == 2 diff --git a/testing/web-platform/tests/core-aam/aamtests/role/alertdialog.py b/testing/web-platform/tests/core-aam/aamtests/role/alertdialog.py index 24f48bf01f23..07a3c4108915 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/alertdialog.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/alertdialog.py @@ -25,8 +25,11 @@ def test_atspi(atspi, session, inline): # # Role: ROLE_SYSTEM_DIALOG # # Event: EVENT_SYSTEM_ALERT: . -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Pane +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Pane + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Pane diff --git a/testing/web-platform/tests/core-aam/aamtests/role/application.py b/testing/web-platform/tests/core-aam/aamtests/role/application.py index bd525013af99..95eb3212682b 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/application.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/application.py @@ -24,9 +24,13 @@ def test_atspi(atspi, session, inline): # # Spec: # # Role: ROLE_SYSTEM_APPLICATION -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Pane -# # Localized Control Type: application +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Pane + # Localized Control Type: application + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Pane + assert node.CurrentLocalizedControlType == "application" diff --git a/testing/web-platform/tests/core-aam/aamtests/role/article.py b/testing/web-platform/tests/core-aam/aamtests/role/article.py index 60859a5d2295..8de25db0b80d 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/article.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/article.py @@ -28,9 +28,13 @@ def test_atspi(atspi, session, inline): # # State: STATE_SYSTEM_READONLY # # Object Attribute: xml-roles:article -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Group -# # Localized Control Type: article +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Group + # Localized Control Type: article + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Group + assert node.CurrentLocalizedControlType == "article" diff --git a/testing/web-platform/tests/core-aam/aamtests/role/banner.py b/testing/web-platform/tests/core-aam/aamtests/role/banner.py index 1c5ec76c9bd5..e0081432a9a5 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/banner.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/banner.py @@ -27,11 +27,17 @@ def test_atspi(atspi, session, inline): # # Role: IA2_ROLE_LANDMARK # # Object Attribute: xml-roles:banner -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Group -# # Localized Control Type: banner -# # Landmark Type: Custom -# # Localized Landmark Type: banner +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Group + # Localized Control Type: banner + # Landmark Type: Custom + # Localized Landmark Type: banner + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Group + assert node.CurrentLocalizedControlType == "banner" + assert node.GetCurrentPropertyValue(uia.PropertyId.LandmarkType) == uia.LandmarkType.Custom + assert node.GetCurrentPropertyValue(uia.PropertyId.LocalizedLandmarkType) == "banner" diff --git a/testing/web-platform/tests/core-aam/aamtests/role/blockquote.py b/testing/web-platform/tests/core-aam/aamtests/role/blockquote.py index bc74a2cf236f..38b02a185ade 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/blockquote.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/blockquote.py @@ -36,10 +36,13 @@ def test_ia2(ia2, session, inline): assert ia2.get_msaa_role(node) == "ROLE_SYSTEM_GROUPING" -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# node = uia.find_node("test", session.url) -# -# # Spec: -# # Control Type: Group -# # Localized Control Type: blockquote +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Group + # Localized Control Type: blockquote + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Group + assert node.CurrentLocalizedControlType == "blockquote" diff --git a/testing/web-platform/tests/core-aam/aamtests/role/button.py b/testing/web-platform/tests/core-aam/aamtests/role/button.py index 0d16175f970f..01f6fcbff486 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/button.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/button.py @@ -41,9 +41,12 @@ def test_axapi(axapi, session, inline, test_html): # # Spec: # # Role: ROLE_SYSTEM_PUSHBUTTON -# @pytest.mark.parametrize("test_html", TEST_HTML.values(), ids=TEST_HTML.keys()) -# def test_uia(uia, session, inline): -# session.url = inline(test_html) -# -# # Spec: -# # Control Type: Button +@pytest.mark.parametrize("test_html", TEST_HTML.values(), ids=TEST_HTML.keys()) +def test_uia(uia, session, inline, test_html): + session.url = inline(test_html) + + # Spec: + # Control Type: Button + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Button diff --git a/testing/web-platform/tests/core-aam/aamtests/role/button_haspopup.py b/testing/web-platform/tests/core-aam/aamtests/role/button_haspopup.py index 45e08e1b959a..002f8f754924 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/button_haspopup.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/button_haspopup.py @@ -43,9 +43,12 @@ def test_axapi(axapi, session, inline, test_html): # # Spec: # # Role: ROLE_SYSTEM_BUTTONMENU -# @pytest.mark.parametrize("test_html", TEST_HTML.values(), ids=TEST_HTML.keys()) -# def test_uia(uia, session, inline, test_html): -# session.url = inline(test_html) -# -# # Spec: -# # Control Type: Button +@pytest.mark.parametrize("test_html", TEST_HTML.values(), ids=TEST_HTML.keys()) +def test_uia(uia, session, inline, test_html): + session.url = inline(test_html) + + # Spec: + # Control Type: Button + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Button diff --git a/testing/web-platform/tests/core-aam/aamtests/role/button_pressed.py b/testing/web-platform/tests/core-aam/aamtests/role/button_pressed.py index 2895f8627de7..0dbacf4c3378 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/button_pressed.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/button_pressed.py @@ -41,9 +41,12 @@ def test_axapi(axapi, session, inline, test_html): # # Role: ROLE_SYSTEM_PUSHBUTTON # # Role: IA2_ROLE_TOGGLE_BUTTON -# @pytest.mark.parametrize("test_html", TEST_HTML.values(), ids=TEST_HTML.keys()) -# def test_uia(uia, session, inline, test_html): -# session.url = inline(test_html) -# -# # Spec: -# # Control Type: Button +@pytest.mark.parametrize("test_html", TEST_HTML.values(), ids=TEST_HTML.keys()) +def test_uia(uia, session, inline, test_html): + session.url = inline(test_html) + + # Spec: + # Control Type: Button + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Button diff --git a/testing/web-platform/tests/core-aam/aamtests/role/caption.py b/testing/web-platform/tests/core-aam/aamtests/role/caption.py index a7b03ca03585..b5b16def77d6 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/caption.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/caption.py @@ -25,8 +25,11 @@ def test_atspi(atspi, session, inline): # # Role: ROLE_SYSTEM_GROUPING # # Role: IA2_ROLE_CAPTION -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Text +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Text + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Text diff --git a/testing/web-platform/tests/core-aam/aamtests/role/cell.py b/testing/web-platform/tests/core-aam/aamtests/role/cell.py index 6d978e1cda42..57af8897615d 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/cell.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/cell.py @@ -27,11 +27,17 @@ def test_atspi(atspi, session, inline): # # Role: ROLE_SYSTEM_CELL # # Interface: IAccessibleTableCell -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: DataItem -# # Localized Control Type: item -# # Control Pattern: GridItem -# # Control Pattern: TableItem +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: DataItem + # Localized Control Type: item + # Control Pattern: GridItem + # Control Pattern: TableItem + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.DataItem + assert node.CurrentLocalizedControlType == "item" + assert node.GetCurrentPropertyValue(uia.PropertyId.IsGridItemPatternAvailable) + assert node.GetCurrentPropertyValue(uia.PropertyId.IsTableItemPatternAvailable) diff --git a/testing/web-platform/tests/core-aam/aamtests/role/checkbox.py b/testing/web-platform/tests/core-aam/aamtests/role/checkbox.py index 0ff1892df3c2..7e2bbea965c4 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/checkbox.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/checkbox.py @@ -26,9 +26,16 @@ def test_atspi(atspi, session, inline): # # Role: ROLE_SYSTEM_CHECKBUTTON # # See also: aria-checked in the State and Property Mapping Tables -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Checkbox -# # See also: aria-checked in the State and Property Mapping Tables +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: CheckBox + # See also: aria-checked in the State and Property Mapping Tables + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.CheckBox + + assert node.GetCurrentPropertyValue(uia.PropertyId.IsTogglePatternAvailable) + toggle_pattern = node.GetCurrentPattern(uia.PatternId.Toggle) + assert toggle_pattern and toggle_pattern.CurrentToggleState == 0 diff --git a/testing/web-platform/tests/core-aam/aamtests/role/code-role.py b/testing/web-platform/tests/core-aam/aamtests/role/code-role.py index a442f06818b9..fb6fd4797f0d 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/code-role.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/code-role.py @@ -27,9 +27,13 @@ def test_atspi(atspi, session, inline): # # Role: IA2_ROLE_TEXT_FRAME # # Object Attribute: xml-roles:code -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Text -# # Localized Control Type: code +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Text + # Localized Control Type: code + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Text + assert node.CurrentLocalizedControlType == "code" diff --git a/testing/web-platform/tests/core-aam/aamtests/role/columnheader.py b/testing/web-platform/tests/core-aam/aamtests/role/columnheader.py index e065e5fd1af1..3a6a87e6b04c 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/columnheader.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/columnheader.py @@ -27,11 +27,17 @@ def test_atspi(atspi, session, inline): # # Role: ROLE_SYSTEM_COLUMNHEADER # # Interface: IAccessibleTableCell -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: DataItem -# # Localized Control Type: column header -# # Control Pattern: GridItem -# # Control Pattern: TableItem +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: DataItem + # Localized Control Type: column header + # Control Pattern: GridItem + # Control Pattern: TableItem + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.DataItem + assert node.CurrentLocalizedControlType == "column header" + assert node.GetCurrentPropertyValue(uia.PropertyId.IsGridItemPatternAvailable) + assert node.GetCurrentPropertyValue(uia.PropertyId.IsTableItemPatternAvailable) diff --git a/testing/web-platform/tests/core-aam/aamtests/role/combobox.py b/testing/web-platform/tests/core-aam/aamtests/role/combobox.py index b3e2f2f92185..e503f1ebd5bb 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/combobox.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/combobox.py @@ -30,8 +30,11 @@ def test_atspi(atspi, session, inline): # # State: STATE_SYSTEM_HASPOPUP # # State: STATE_SYSTEM_COLLAPSED: if aria-expanded is not "true" -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Combobox +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: ComboBox + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.ComboBox diff --git a/testing/web-platform/tests/core-aam/aamtests/role/comment.py b/testing/web-platform/tests/core-aam/aamtests/role/comment.py index e71a019e9502..789ecc70504d 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/comment.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/comment.py @@ -26,9 +26,13 @@ def test_atspi(atspi, session, inline): # # Role: IA2_ROLE_COMMENT # # Object Attribute: xml-roles:comment -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Group -# # Localized Control Type: comment +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Group + # Localized Control Type: comment + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Group + assert node.CurrentLocalizedControlType == "comment" diff --git a/testing/web-platform/tests/core-aam/aamtests/role/complementary.py b/testing/web-platform/tests/core-aam/aamtests/role/complementary.py index 90f721462eba..3999846834f3 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/complementary.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/complementary.py @@ -27,11 +27,17 @@ def test_atspi(atspi, session, inline): # # Role: IA2_ROLE_LANDMARK # # Object Attribute: xml-roles:complementary -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Group -# # Localized Control Type: complementary -# # Landmark Type: Custom -# # Localized Landmark Type: complementary +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Group + # Localized Control Type: complementary + # Landmark Type: Custom + # Localized Landmark Type: complementary + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Group + assert node.CurrentLocalizedControlType == "complementary" + assert node.GetCurrentPropertyValue(uia.PropertyId.LandmarkType) == uia.LandmarkType.Custom + assert node.GetCurrentPropertyValue(uia.PropertyId.LocalizedLandmarkType) == "complementary" diff --git a/testing/web-platform/tests/core-aam/aamtests/role/contentinfo.py b/testing/web-platform/tests/core-aam/aamtests/role/contentinfo.py index 8fc764106124..464d9357780d 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/contentinfo.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/contentinfo.py @@ -27,11 +27,17 @@ def test_atspi(atspi, session, inline): # # Role: IA2_ROLE_LANDMARK # # Object Attribute: xml-roles:contentinfo -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Group -# # Localized Control Type: content information -# # Landmark Type: Custom -# # Localized Landmark Type: content information +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Group + # Localized Control Type: content information + # Landmark Type: Custom + # Localized Landmark Type: content information + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Group + assert node.CurrentLocalizedControlType == "content information" + assert node.GetCurrentPropertyValue(uia.PropertyId.LandmarkType) == uia.LandmarkType.Custom + assert node.GetCurrentPropertyValue(uia.PropertyId.LocalizedLandmarkType) == "content information" \ No newline at end of file diff --git a/testing/web-platform/tests/core-aam/aamtests/role/definition.py b/testing/web-platform/tests/core-aam/aamtests/role/definition.py index ccc2829a7ec5..3e6874d0a4c8 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/definition.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/definition.py @@ -26,9 +26,13 @@ def test_atspi(atspi, session, inline): # # Spec: # # Object Attribute: xml-roles:definition -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Group -# # Localized Control Type: definition +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Group + # Localized Control Type: definition + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Group + assert node.CurrentLocalizedControlType == "definition" diff --git a/testing/web-platform/tests/core-aam/aamtests/role/deletion.py b/testing/web-platform/tests/core-aam/aamtests/role/deletion.py index dd05ed870f75..f4bf9e97a26e 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/deletion.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/deletion.py @@ -27,9 +27,13 @@ def test_atspi(atspi, session, inline): # # Spec: # # Role: IA2_ROLE_CONTENT_DELETION -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Text -# # Localized Control Type: deletion +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Text + # Localized Control Type: deletion + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Text + assert node.CurrentLocalizedControlType == "deletion" diff --git a/testing/web-platform/tests/core-aam/aamtests/role/dialog.py b/testing/web-platform/tests/core-aam/aamtests/role/dialog.py index 45570e31f83c..3d5621faede4 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/dialog.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/dialog.py @@ -24,8 +24,11 @@ def test_atspi(atspi, session, inline): # # Spec: # # Role: ROLE_SYSTEM_DIALOG -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Pane +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Pane + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Pane diff --git a/testing/web-platform/tests/core-aam/aamtests/role/directory.py b/testing/web-platform/tests/core-aam/aamtests/role/directory.py index 9a05447f6d5a..770227690d7e 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/directory.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/directory.py @@ -24,8 +24,11 @@ def test_atspi(atspi, session, inline): # # Spec: # # Role: ROLE_SYSTEM_LIST -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: List +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: List + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.List diff --git a/testing/web-platform/tests/core-aam/aamtests/role/document.py b/testing/web-platform/tests/core-aam/aamtests/role/document.py index f007a52a2741..3733ee81b715 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/document.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/document.py @@ -25,8 +25,11 @@ def test_atspi(atspi, session, inline): # # Role: ROLE_SYSTEM_DOCUMENT # # State: STATE_SYSTEM_READONLY -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Document +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Document + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Document diff --git a/testing/web-platform/tests/core-aam/aamtests/role/emphasis.py b/testing/web-platform/tests/core-aam/aamtests/role/emphasis.py index ccb46ac14efe..a368e22e01d3 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/emphasis.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/emphasis.py @@ -27,9 +27,13 @@ def test_atspi(atspi, session, inline): # # Role: IA2_ROLE_TEXT_FRAME # # Object Attribute: xml-roles:emphasis -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Text -# # Localized Control Type: emphasis +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Text + # Localized Control Type: emphasis + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Text + assert node.CurrentLocalizedControlType == "emphasis" diff --git a/testing/web-platform/tests/core-aam/aamtests/role/feed.py b/testing/web-platform/tests/core-aam/aamtests/role/feed.py index 538ef656aa23..f14cabadf939 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/feed.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/feed.py @@ -27,9 +27,13 @@ def test_atspi(atspi, session, inline): # # Role: ROLE_SYSTEM_GROUPING # # Object Attribute: xml-roles:feed -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Group -# # Localized Control Type: feed +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Group + # Localized Control Type: feed + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Group + assert node.CurrentLocalizedControlType == "feed" diff --git a/testing/web-platform/tests/core-aam/aamtests/role/figure.py b/testing/web-platform/tests/core-aam/aamtests/role/figure.py index 4e5ff09e8a45..5dcdabda6137 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/figure.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/figure.py @@ -27,9 +27,13 @@ def test_atspi(atspi, session, inline): # # Role: ROLE_SYSTEM_GROUPING # # Object Attribute: xml-roles:figure -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Group -# # Localized Control Type: figure +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Group + # Localized Control Type: figure + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Group + assert node.CurrentLocalizedControlType == "figure" diff --git a/testing/web-platform/tests/core-aam/aamtests/role/form.py b/testing/web-platform/tests/core-aam/aamtests/role/form.py index 3bdaf20445ec..49bb069cfb48 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/form.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/form.py @@ -27,10 +27,15 @@ def test_atspi(atspi, session, inline): # # Role: IA2_ROLE_FORM # # Object Attribute: xml-roles:form -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Group -# # Localized Control Type: form -# # Landmark Type: Form +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Group + # Localized Control Type: form + # Landmark Type: Form + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Group + assert node.CurrentLocalizedControlType == "form" + assert node.GetCurrentPropertyValue(uia.PropertyId.LandmarkType) == uia.LandmarkType.Form diff --git a/testing/web-platform/tests/core-aam/aamtests/role/generic.py b/testing/web-platform/tests/core-aam/aamtests/role/generic.py index 14721e521875..f22c6b6f04b2 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/generic.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/generic.py @@ -25,8 +25,11 @@ def test_atspi(atspi, session, inline): # # Role: ROLE_SYSTEM_GROUPING # # Role: IA2_ROLE_SECTION -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Group +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Group + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Group diff --git a/testing/web-platform/tests/core-aam/aamtests/role/grid.py b/testing/web-platform/tests/core-aam/aamtests/role/grid.py index cca58932bc18..6a54f9e45aaf 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/grid.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/grid.py @@ -38,11 +38,17 @@ def test_atspi(atspi, session, inline): # # Method: IAccessible::accSelect() # # Method: IAccessible::get_accSelection() -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: DataGrid -# # Control Pattern: Grid -# # Control Pattern: Table -# # Control Pattern: Selection +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: DataGrid + # Control Pattern: Grid + # Control Pattern: Table + # Control Pattern: Selection + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.DataGrid + assert node.GetCurrentPropertyValue(uia.PropertyId.IsGridItemPatternAvailable) + assert node.GetCurrentPropertyValue(uia.PropertyId.IsTableItemPatternAvailable) + assert node.GetCurrentPropertyValue(uia.PropertyId.IsSelectionPatternAvailable) diff --git a/testing/web-platform/tests/core-aam/aamtests/role/gridcell.py b/testing/web-platform/tests/core-aam/aamtests/role/gridcell.py index 4a9f18751aae..8ed622f47c6d 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/gridcell.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/gridcell.py @@ -27,13 +27,23 @@ def test_atspi(atspi, session, inline): # # Role: ROLE_SYSTEM_CELL # # Interface: IAccessibleTableCell -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: DataItem -# # Localized Control Type: item -# # Control Pattern: SelectionItem -# # Control Pattern: GridItem -# # Control Pattern: TableItem -# # SelectionItem.SelectionContainer: grid +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: DataItem + # Localized Control Type: item + # Control Pattern: GridItem + # Control Pattern: TableItem + # Control Pattern: SelectionItem + # SelectionItem.SelectionContainer: grid + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.DataItem + assert node.CurrentLocalizedControlType == "item" + assert node.GetCurrentPropertyValue(uia.PropertyId.IsGridItemPatternAvailable) + assert node.GetCurrentPropertyValue(uia.PropertyId.IsTableItemPatternAvailable) + + assert node.GetCurrentPropertyValue(uia.PropertyId.IsSelectionPatternAvailable) + selection_container = node.GetCurrentPattern(uia.PatternId.SelectionItem).CurrentSelectionContainer + assert selection_container.CurrentControlType == uia.ControlType.DataGrid diff --git a/testing/web-platform/tests/core-aam/aamtests/role/group.py b/testing/web-platform/tests/core-aam/aamtests/role/group.py index 87d6d29c1186..40fd65999569 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/group.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/group.py @@ -24,8 +24,11 @@ def test_atspi(atspi, session, inline): # # Spec: # # Role: ROLE_SYSTEM_GROUPING -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Group +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Group + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Group diff --git a/testing/web-platform/tests/core-aam/aamtests/role/heading.py b/testing/web-platform/tests/core-aam/aamtests/role/heading.py index d33a6b80e5d0..007430a67a58 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/heading.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/heading.py @@ -25,9 +25,13 @@ def test_atspi(atspi, session, inline): # # Role: IA2_ROLE_HEADING # # Object Attribute: xml-roles:heading -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Text -# # Localized Control Type: heading +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Text + # Localized Control Type: heading + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Text + assert node.CurrentLocalizedControlType == "heading" diff --git a/testing/web-platform/tests/core-aam/aamtests/role/image.py b/testing/web-platform/tests/core-aam/aamtests/role/image.py index a6cbb53fa3c1..e38385f9d2a1 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/image.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/image.py @@ -27,8 +27,11 @@ def test_atspi(atspi, session, inline): # # Role: ROLE_SYSTEM_GRAPHIC # # Interface: IAccessibleImage -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Image +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Image + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Image diff --git a/testing/web-platform/tests/core-aam/aamtests/role/img.py b/testing/web-platform/tests/core-aam/aamtests/role/img.py index 9a08118490e1..fba3e2e7d61a 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/img.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/img.py @@ -27,8 +27,11 @@ def test_atspi(atspi, session, inline): # # Role: ROLE_SYSTEM_GRAPHIC # # Interface: IAccessibleImage -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Image +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Image + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Image diff --git a/testing/web-platform/tests/core-aam/aamtests/role/insertion.py b/testing/web-platform/tests/core-aam/aamtests/role/insertion.py index 4c9e281d19ce..14a2badde370 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/insertion.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/insertion.py @@ -27,9 +27,13 @@ def test_atspi(atspi, session, inline): # # Spec: # # Role: IA2_ROLE_CONTENT_INSERTION -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Text -# # Localized Control Type: insertion +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Text + # Localized Control Type: insertion + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Text + assert node.CurrentLocalizedControlType == "insertion" diff --git a/testing/web-platform/tests/core-aam/aamtests/role/link.py b/testing/web-platform/tests/core-aam/aamtests/role/link.py index 0d37d6eb8487..82273d496ec4 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/link.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/link.py @@ -57,10 +57,14 @@ def test_ia2(ia2, session, inline, test_html): assert ia2.get_hyperlink_interface(node) is not None -# @pytest.mark.parametrize("test_html", TEST_HTML.values(), ids=TEST_HTML.keys()) -# def test_uia(uia, session, inline, test_html): -# session.url = inline(test_html) -# -# # Spec: -# # Control Type: HyperLink -# # Control Pattern: Value +@pytest.mark.parametrize("test_html", TEST_HTML.values(), ids=TEST_HTML.keys()) +def test_uia(uia, session, inline, test_html): + session.url = inline(test_html) + + # Spec: + # Control Type: HyperLink + # Control Pattern: Value + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Hyperlink + assert node.GetCurrentPropertyValue(uia.PropertyId.IsValuePatternAvailable) diff --git a/testing/web-platform/tests/core-aam/aamtests/role/list.py b/testing/web-platform/tests/core-aam/aamtests/role/list.py index 55291fb94e92..8c16a67f3da1 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/list.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/list.py @@ -25,8 +25,11 @@ def test_atspi(atspi, session, inline): # # Role: ROLE_SYSTEM_LIST # # State: STATE_SYSTEM_READONLY -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: List +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: List + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.List diff --git a/testing/web-platform/tests/core-aam/aamtests/role/listbox.py b/testing/web-platform/tests/core-aam/aamtests/role/listbox.py index 66a1a012d9dd..70d0d8bab6a5 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/listbox.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/listbox.py @@ -29,9 +29,13 @@ def test_atspi(atspi, session, inline): # # Method: IAccessible::accSelect() # # Method: IAccessible::get_accSelection() -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: List -# # Control Pattern: Selection +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: List + # Control Pattern: Selection + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.List + assert node.GetCurrentPropertyValue(uia.PropertyId.IsSelectionPatternAvailable) diff --git a/testing/web-platform/tests/core-aam/aamtests/role/listbox_in_combobox.py b/testing/web-platform/tests/core-aam/aamtests/role/listbox_in_combobox.py index 6ce8c2f3a657..f9b94270bf69 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/listbox_in_combobox.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/listbox_in_combobox.py @@ -29,9 +29,13 @@ def test_atspi(atspi, session, inline): # # Method: IAccessible::accSelect() # # Method: IAccessible::get_accSelection() -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: List -# # Control Pattern: Selection +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: List + # Control Pattern: Selection + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.List + assert node.GetCurrentPropertyValue(uia.PropertyId.IsSelectionPatternAvailable) diff --git a/testing/web-platform/tests/core-aam/aamtests/role/listitem.py b/testing/web-platform/tests/core-aam/aamtests/role/listitem.py index ef568c0e0f2e..1b8ced41dbbd 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/listitem.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/listitem.py @@ -25,10 +25,18 @@ def test_atspi(atspi, session, inline): # # Role: ROLE_SYSTEM_LISTITEM # # State: STATE_SYSTEM_READONLY -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: ListItem -# # Control Pattern: SelectionItem -# # SelectionItem.SelectionContainer: list +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: ListItem + # Control Pattern: SelectionItem + # SelectionItem.SelectionContainer: list + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.ListItem + + # Todo: Check if this is a bug in the AAM, commenting out for now. + # assert node.GetCurrentPropertyValue(uia.PropertyId.IsSelectionItemPatternAvailable) + # selection_pattern = node.GetCurrentPattern(uia.PatternId.SelectionItem) + # assert selection_pattern and selection_pattern.CurrentIsSelected == 0 diff --git a/testing/web-platform/tests/core-aam/aamtests/role/log.py b/testing/web-platform/tests/core-aam/aamtests/role/log.py index 29259ef45e6b..a96dce1073c5 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/log.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/log.py @@ -35,10 +35,15 @@ def test_atspi(atspi, session, inline): # # Object Attribute: live:polite # # Object Attribute: container-live-role:log -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Group -# # Localized Control Type: log -# # LiveSetting: Polite (1) +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Group + # Localized Control Type: log + # LiveSetting: Polite (1) + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Group + assert node.CurrentLocalizedControlType == "log" + assert node.GetCurrentPropertyValue(uia.PropertyId.LiveSetting) == 1 diff --git a/testing/web-platform/tests/core-aam/aamtests/role/main.py b/testing/web-platform/tests/core-aam/aamtests/role/main.py index 35c6100a82e4..dccc954e460e 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/main.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/main.py @@ -34,3 +34,16 @@ def test_atspi(atspi, session, inline): # # Control Type: Group # # Localized Control Type: main # # Landmark Type: Main + +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Group + # Localized Control Type: main + # Landmark Type: Main + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Group + assert node.CurrentLocalizedControlType == "main" + assert node.GetCurrentPropertyValue(uia.PropertyId.LandmarkType) == uia.LandmarkType.Main diff --git a/testing/web-platform/tests/core-aam/aamtests/role/mark.py b/testing/web-platform/tests/core-aam/aamtests/role/mark.py index fd46d5a18ffd..47352e400df8 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/mark.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/mark.py @@ -29,8 +29,11 @@ def test_atspi(atspi, session, inline): # # Role: IA2_ROLE_MARK # # Object Attribute: xml-roles:mark -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Group +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Group + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Group diff --git a/testing/web-platform/tests/core-aam/aamtests/role/marquee.py b/testing/web-platform/tests/core-aam/aamtests/role/marquee.py index 3050079d5206..0e5c525ae1b5 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/marquee.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/marquee.py @@ -31,3 +31,14 @@ def test_atspi(atspi, session, inline): # # Spec: # # Control Type: Group # # Localized Control Type: marquee + +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Group + # Localized Control Type: marquee + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Group + assert node.CurrentLocalizedControlType == "marquee" diff --git a/testing/web-platform/tests/core-aam/aamtests/role/math-role.py b/testing/web-platform/tests/core-aam/aamtests/role/math-role.py index ee5df17752a6..50b1dd90f8d7 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/math-role.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/math-role.py @@ -24,9 +24,13 @@ def test_atspi(atspi, session, inline): # # Spec: # # Role: ROLE_SYSTEM_EQUATION -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Group -# # Localized Control Type: math +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Group + # Localized Control Type: math + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Group + assert node.CurrentLocalizedControlType == "math" diff --git a/testing/web-platform/tests/core-aam/aamtests/role/menu.py b/testing/web-platform/tests/core-aam/aamtests/role/menu.py index 29bea78322ca..252bb6fc6e3a 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/menu.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/menu.py @@ -29,8 +29,11 @@ def test_atspi(atspi, session, inline): # # Method: IAccessible::accSelect() # # Method: IAccessible::get_accSelection() -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Menu +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Menu + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Menu diff --git a/testing/web-platform/tests/core-aam/aamtests/role/menubar.py b/testing/web-platform/tests/core-aam/aamtests/role/menubar.py index 77867ba07bfe..d5b92ed98ecd 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/menubar.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/menubar.py @@ -29,8 +29,11 @@ def test_atspi(atspi, session, inline): # # Method: IAccessible::accSelect() # # Method: IAccessible::get_accSelection() -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: MenuBar +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: MenuBar + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.MenuBar diff --git a/testing/web-platform/tests/core-aam/aamtests/role/menuitem.py b/testing/web-platform/tests/core-aam/aamtests/role/menuitem.py index 811f2809f218..7b5eba886fcb 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/menuitem.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/menuitem.py @@ -29,3 +29,13 @@ def test_atspi(atspi, session, inline): # # # Spec: # # Control Type: MenuItem + +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: MenuItem + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.MenuItem + diff --git a/testing/web-platform/tests/core-aam/aamtests/role/menuitemcheckbox.py b/testing/web-platform/tests/core-aam/aamtests/role/menuitemcheckbox.py index f7930267424b..bfab0f19ea3d 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/menuitemcheckbox.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/menuitemcheckbox.py @@ -27,10 +27,17 @@ def test_atspi(atspi, session, inline): # # Role: IA2_ROLE_CHECK_MENU_ITEM # # See also: aria-checked in the State and Property Mapping Tables -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: MenuItem -# # Control Pattern: Toggle -# # See also: aria-checked in the State and Property Mapping Tables +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: MenuItem + # Control Pattern: Toggle + # See also: aria-checked in the State and Property Mapping Tables + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.MenuItem + + assert node.GetCurrentPropertyValue(uia.PropertyId.IsTogglePatternAvailable) + toggle_pattern = node.GetCurrentPattern(uia.PatternId.Toggle) + assert toggle_pattern and toggle_pattern.CurrentToggleState == 0 diff --git a/testing/web-platform/tests/core-aam/aamtests/role/menuitemradio.py b/testing/web-platform/tests/core-aam/aamtests/role/menuitemradio.py index 2694cac8d8e1..f8b99abc04e9 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/menuitemradio.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/menuitemradio.py @@ -27,11 +27,22 @@ def test_atspi(atspi, session, inline): # # Role: IA2_ROLE_RADIO_MENU_ITEM # # See also: aria-checked in the State and Property Mapping Tables -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: MenuItem -# # Control Pattern: Toggle -# # Control Pattern: SelectionItem -# # See also: aria-checked in the State and Property Mapping Tables +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: MenuItem + # Control Pattern: Toggle + # Control Pattern: SelectionItem + # See also: aria-checked in the State and Property Mapping Tables + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.MenuItem + + assert node.GetCurrentPropertyValue(uia.PropertyId.IsTogglePatternAvailable) + toggle_pattern = node.GetCurrentPattern(uia.PatternId.Toggle) + assert toggle_pattern and toggle_pattern.CurrentToggleState == 0 + + assert node.GetCurrentPropertyValue(uia.PropertyId.IsSelectionItemPatternAvailable) + selection_pattern = node.GetCurrentPattern(uia.PatternId.SelectionItem) + assert selection_pattern and selection_pattern.CurrentIsSelected == 0 diff --git a/testing/web-platform/tests/core-aam/aamtests/role/meter.py b/testing/web-platform/tests/core-aam/aamtests/role/meter.py index e7d2e675a27c..1bfade9ed94b 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/meter.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/meter.py @@ -27,10 +27,15 @@ def test_atspi(atspi, session, inline): # # Role: IA2_ROLE_LEVEL_BAR # # Interface: IAccessibleValue -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: ProgressBar -# # Localized Control Type: meter -# # Control Pattern: RangeValue +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: ProgressBar + # Localized Control Type: meter + # Control Pattern: RangeValue + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.ProgressBar + assert node.CurrentLocalizedControlType == "meter" + assert node.GetCurrentPropertyValue(uia.PropertyId.IsRangeValuePatternAvailable) diff --git a/testing/web-platform/tests/core-aam/aamtests/role/navigation.py b/testing/web-platform/tests/core-aam/aamtests/role/navigation.py index a8ed2f4252d4..e47cc3c916ac 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/navigation.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/navigation.py @@ -27,10 +27,15 @@ def test_atspi(atspi, session, inline): # # Role: IA2_ROLE_LANDMARK # # Object Attribute: xml-roles:navigation -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Group -# # Localized Control Type: navigation -# # Landmark Type: Navigation +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Group + # Localized Control Type: navigation + # Landmark Type: Navigation + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Group + assert node.CurrentLocalizedControlType == "navigation" + assert node.GetCurrentPropertyValue(uia.PropertyId.LandmarkType) == uia.LandmarkType.Navigation diff --git a/testing/web-platform/tests/core-aam/aamtests/role/note.py b/testing/web-platform/tests/core-aam/aamtests/role/note.py index 6a678212b0e7..9219d648e121 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/note.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/note.py @@ -24,9 +24,13 @@ def test_atspi(atspi, session, inline): # # Spec: # # Role: IA2_ROLE_NOTE -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Group -# # Localized Control Type: note +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Group + # Localized Control Type: note + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Group + assert node.CurrentLocalizedControlType == "note" diff --git a/testing/web-platform/tests/core-aam/aamtests/role/option.py b/testing/web-platform/tests/core-aam/aamtests/role/option.py index f71600e9fa73..d9d981ea4574 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/option.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/option.py @@ -26,10 +26,16 @@ def test_atspi(atspi, session, inline): # # Role: ROLE_SYSTEM_LISTITEM # # See also: aria-checked in the State and Property Mapping Tables -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: ListItem -# # Control Pattern: Invoke -# # See also: aria-checked in the State and Property Mapping Tables +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: ListItem + # Control Pattern: Invoke + # See also: aria-checked in the State and Property Mapping Tables + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.ListItem + + assert node.GetCurrentPropertyValue(uia.PropertyId.IsInvokePatternAvailable) + # aria-checked is not mapped when undefined as in the HTML fragment above. diff --git a/testing/web-platform/tests/core-aam/aamtests/role/option_in_combobox.py b/testing/web-platform/tests/core-aam/aamtests/role/option_in_combobox.py index 7f200f702798..3c8fadeb20ae 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/option_in_combobox.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/option_in_combobox.py @@ -26,10 +26,14 @@ def test_atspi(atspi, session, inline): # # Role: ROLE_SYSTEM_LISTITEM # # See also: aria-checked in the State and Property Mapping Tables -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: ListItem -# # Control Pattern: Invoke -# # See also: aria-checked in the State and Property Mapping Tables +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: ListItem + # Control Pattern: Invoke + # See also: aria-checked in the State and Property Mapping Tables + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.ListItem + assert node.GetCurrentPropertyValue(uia.PropertyId.IsInvokePatternAvailable) \ No newline at end of file diff --git a/testing/web-platform/tests/core-aam/aamtests/role/paragraph.py b/testing/web-platform/tests/core-aam/aamtests/role/paragraph.py index 1d7656f80e60..5b357a1f45d0 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/paragraph.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/paragraph.py @@ -25,8 +25,11 @@ def test_atspi(atspi, session, inline): # # Role: ROLE_SYSTEM_GROUPING # # Role: IA2_ROLE_PARAGRAPH -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Text +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Text + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Text diff --git a/testing/web-platform/tests/core-aam/aamtests/role/progressbar.py b/testing/web-platform/tests/core-aam/aamtests/role/progressbar.py index 0fb6be380140..7338b0d3faa5 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/progressbar.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/progressbar.py @@ -29,8 +29,13 @@ def test_atspi(atspi, session, inline): # # State: STATE_SYSTEM_READONLY # # Interface: IAccessibleValue -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: ProgressBar
Control Pattern: RangeValue if aria-valuenow, aria-valuemax, or aria-valuemin is present +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: ProgressBar + # Control Pattern: RangeValue if aria-valuenow, aria-valuemax, or aria-valuemin is present + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.ProgressBar + assert node.GetCurrentPropertyValue(uia.PropertyId.IsRangeValuePatternAvailable) diff --git a/testing/web-platform/tests/core-aam/aamtests/role/radio.py b/testing/web-platform/tests/core-aam/aamtests/role/radio.py index 1429875bfcef..8e92e71fa50c 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/radio.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/radio.py @@ -26,11 +26,22 @@ def test_atspi(atspi, session, inline): # # Role: ROLE_SYSTEM_RADIOBUTTON # # See also: aria-checked in the State and Property Mapping Tables -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: RadioButton -# # Control Pattern: Toggle -# # Control Pattern: SelectionItem -# # See also: aria-checked in the State and Property Mapping Tables +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: RadioButton + # Control Pattern: Toggle + # Control Pattern: SelectionItem + # See also: aria-checked in the State and Property Mapping Tables + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.RadioButton + + assert node.GetCurrentPropertyValue(uia.PropertyId.IsTogglePatternAvailable) + toggle_pattern = node.GetCurrentPattern(uia.PatternId.Toggle) + assert toggle_pattern and toggle_pattern.CurrentToggleState == 0 + + assert node.GetCurrentPropertyValue(uia.PropertyId.IsSelectionItemPatternAvailable) + selection_pattern = node.GetCurrentPattern(uia.PatternId.SelectionItem) + assert selection_pattern and selection_pattern.CurrentIsSelected == 0 diff --git a/testing/web-platform/tests/core-aam/aamtests/role/radiogroup.py b/testing/web-platform/tests/core-aam/aamtests/role/radiogroup.py index 318c9667bfcc..22a80c121bbc 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/radiogroup.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/radiogroup.py @@ -24,8 +24,11 @@ def test_atspi(atspi, session, inline): # # Spec: # # Role: ROLE_SYSTEM_GROUPING -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: List +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: List + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.List diff --git a/testing/web-platform/tests/core-aam/aamtests/role/region.py b/testing/web-platform/tests/core-aam/aamtests/role/region.py index 08ce52ff1355..104dc62bce1a 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/region.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/region.py @@ -27,11 +27,17 @@ def test_atspi(atspi, session, inline): # # Role: IA2_ROLE_LANDMARK # # Object Attribute: xml-roles:region -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Group -# # Localized Control Type: region -# # Landmark Type: Custom -# # Localized Landmark Type: region +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Group + # Localized Control Type: region + # Landmark Type: Custom + # Localized Landmark Type: region + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Group + assert node.CurrentLocalizedControlType == "region" + assert node.GetCurrentPropertyValue(uia.PropertyId.LandmarkType) == uia.LandmarkType.Custom + assert node.GetCurrentPropertyValue(uia.PropertyId.LocalizedLandmarkType) == "region" diff --git a/testing/web-platform/tests/core-aam/aamtests/role/row.py b/testing/web-platform/tests/core-aam/aamtests/role/row.py index 43dd07910f19..8c2b68834705 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/row.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/row.py @@ -24,10 +24,18 @@ def test_atspi(atspi, session, inline): # # Spec: # # Role: ROLE_SYSTEM_ROW -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: DataItem -# # Localized Control Type: row -# # Control Pattern: SelectionItem +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: DataItem + # Localized Control Type: row + # Control Pattern: SelectionItem + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.DataItem + assert node.CurrentLocalizedControlType == "row" + + assert node.GetCurrentPropertyValue(uia.PropertyId.IsSelectionItemPatternAvailable) + selection_pattern = node.GetCurrentPattern(uia.PatternId.SelectionItem) + assert selection_pattern and selection_pattern.CurrentIsSelected == 0 diff --git a/testing/web-platform/tests/core-aam/aamtests/role/row_in_treegrid.py b/testing/web-platform/tests/core-aam/aamtests/role/row_in_treegrid.py index 3359600049f6..dffd93a5b156 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/row_in_treegrid.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/row_in_treegrid.py @@ -24,10 +24,18 @@ def test_atspi(atspi, session, inline): # # Spec: # # Role: ROLE_SYSTEM_OUTLINEITEM -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: DataItem -# # Localized Control Type: row -# # Control Pattern: SelectionItem +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: DataItem + # Localized Control Type: row + # Control Pattern: SelectionItem + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.DataItem + assert node.CurrentLocalizedControlType == "row" + + assert node.GetCurrentPropertyValue(uia.PropertyId.IsSelectionItemPatternAvailable) + selection_pattern = node.GetCurrentPattern(uia.PatternId.SelectionItem) + assert selection_pattern and selection_pattern.CurrentIsSelected == 0 diff --git a/testing/web-platform/tests/core-aam/aamtests/role/rowgroup.py b/testing/web-platform/tests/core-aam/aamtests/role/rowgroup.py index 37667c9410d7..00af9c32c9c1 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/rowgroup.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/rowgroup.py @@ -23,8 +23,11 @@ def test_atspi(atspi, session, inline): # # Spec: # # Role: ROLE_SYSTEM_GROUPING -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Group +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Group + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Group diff --git a/testing/web-platform/tests/core-aam/aamtests/role/rowheader.py b/testing/web-platform/tests/core-aam/aamtests/role/rowheader.py index 20264024d8cc..a89bd7e573fb 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/rowheader.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/rowheader.py @@ -27,8 +27,11 @@ def test_atspi(atspi, session, inline): # # Role: ROLE_SYSTEM_ROWHEADER # # Interface: IAccessibleTableCell -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: HeaderItem +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: HeaderItem + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.HeaderItem diff --git a/testing/web-platform/tests/core-aam/aamtests/role/scrollbar.py b/testing/web-platform/tests/core-aam/aamtests/role/scrollbar.py index 8317bf608560..4fa92a7e4bbd 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/scrollbar.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/scrollbar.py @@ -28,9 +28,13 @@ def test_atspi(atspi, session, inline): # # Role: ROLE_SYSTEM_SCROLLBAR # # Interface: IAccessibleValue -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: ScrollBar -# # Control Pattern: RangeValue +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: ScrollBar + # Control Pattern: RangeValue + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.ScrollBar + assert node.GetCurrentPropertyValue(uia.PropertyId.IsRangeValuePatternAvailable) diff --git a/testing/web-platform/tests/core-aam/aamtests/role/search.py b/testing/web-platform/tests/core-aam/aamtests/role/search.py index 87dc0261e5e5..0ddd04d60112 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/search.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/search.py @@ -27,10 +27,15 @@ def test_atspi(atspi, session, inline): # # Role: IA2_ROLE_LANDMARK # # Object Attribute: xml-roles:search -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Group -# # Localized Control Type: search -# # Landmark Type: Search +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Group + # Localized Control Type: search + # Landmark Type: Search + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Group + assert node.CurrentLocalizedControlType == "search" + assert node.GetCurrentPropertyValue(uia.PropertyId.LandmarkType) == uia.LandmarkType.Search diff --git a/testing/web-platform/tests/core-aam/aamtests/role/searchbox.py b/testing/web-platform/tests/core-aam/aamtests/role/searchbox.py index dc18ba3f8c14..b5c13814ff8a 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/searchbox.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/searchbox.py @@ -31,9 +31,13 @@ def test_atspi(atspi, session, inline): # # Role: ROLE_SYSTEM_TEXT # # Object Attribute: text-input-type:search -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Edit -# # Localized Control Type: search box +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Edit + # Localized Control Type: search box + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Edit + assert node.CurrentLocalizedControlType == "search box" diff --git a/testing/web-platform/tests/core-aam/aamtests/role/sectionfooter.py b/testing/web-platform/tests/core-aam/aamtests/role/sectionfooter.py index 08bc573004e7..e7d8912bfd28 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/sectionfooter.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/sectionfooter.py @@ -26,9 +26,13 @@ def test_atspi(atspi, session, inline): # # Role: ROLE_SYSTEM_GROUPING # # Object Attribute: xml-roles:sectionfooter -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Group -# # Localized Control Type: section footer +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Group + # Localized Control Type: section footer + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Group + assert node.CurrentLocalizedControlType == "section footer" diff --git a/testing/web-platform/tests/core-aam/aamtests/role/sectionheader.py b/testing/web-platform/tests/core-aam/aamtests/role/sectionheader.py index d142016e1963..5ecb2da3e8be 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/sectionheader.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/sectionheader.py @@ -26,9 +26,13 @@ def test_atspi(atspi, session, inline): # # Role: ROLE_SYSTEM_GROUPING # # Object Attribute: xml-roles:sectionheader -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Group -# # Localized Control Type: section header +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Group + # Localized Control Type: section header + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Group + assert node.CurrentLocalizedControlType == "section header" diff --git a/testing/web-platform/tests/core-aam/aamtests/role/separator.py b/testing/web-platform/tests/core-aam/aamtests/role/separator.py index 55da31ea8a23..80d15da27940 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/separator.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/separator.py @@ -24,8 +24,11 @@ def test_atspi(atspi, session, inline): # # Spec: # # Role: ROLE_SYSTEM_SEPARATOR -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Separator +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Separator + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Separator diff --git a/testing/web-platform/tests/core-aam/aamtests/role/separator_focusable.py b/testing/web-platform/tests/core-aam/aamtests/role/separator_focusable.py index 3f3b14eb28aa..7381da84165c 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/separator_focusable.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/separator_focusable.py @@ -28,9 +28,13 @@ def test_atspi(atspi, session, inline): # # Role: ROLE_SYSTEM_SEPARATOR # # Interface: IAccessibleValue -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Thumb -# # Control Pattern: RangeValue +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Thumb + # Control Pattern: RangeValue + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Thumb + assert node.GetCurrentPropertyValue(uia.PropertyId.IsRangeValuePatternAvailable) diff --git a/testing/web-platform/tests/core-aam/aamtests/role/slider.py b/testing/web-platform/tests/core-aam/aamtests/role/slider.py index ec77c17d3adb..f7d755770ce8 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/slider.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/slider.py @@ -28,9 +28,13 @@ def test_atspi(atspi, session, inline): # # Role: ROLE_SYSTEM_SLIDER # # Interface: IAccessibleValue -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Slider -# # Control Pattern: RangeValue +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Slider + # Control Pattern: RangeValue + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Slider + assert node.GetCurrentPropertyValue(uia.PropertyId.IsRangeValuePatternAvailable) diff --git a/testing/web-platform/tests/core-aam/aamtests/role/spinbutton.py b/testing/web-platform/tests/core-aam/aamtests/role/spinbutton.py index 3edf0667f330..c183aa95acb1 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/spinbutton.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/spinbutton.py @@ -28,9 +28,13 @@ def test_atspi(atspi, session, inline): # # Role: ROLE_SYSTEM_SPINBUTTON # # Interface: IAccessibleValue -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Spinner -# # Control Pattern: RangeValue +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Spinner + # Control Pattern: RangeValue + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Spinner + assert node.GetCurrentPropertyValue(uia.PropertyId.IsRangeValuePatternAvailable) diff --git a/testing/web-platform/tests/core-aam/aamtests/role/status.py b/testing/web-platform/tests/core-aam/aamtests/role/status.py index c8f4fa2775f8..d33f981f0013 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/status.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/status.py @@ -33,10 +33,15 @@ def test_atspi(atspi, session, inline): # # Object Attribute: live:polite # # Object Attribute: container-live-role:status -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Group -# # Localized Control Type: status -# # LiveSetting: Polite (1) +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Group + # Localized Control Type: status + # LiveSetting: Polite (1) + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Group + assert node.CurrentLocalizedControlType == "status" + assert node.GetCurrentPropertyValue(uia.PropertyId.LiveSetting) == 1 diff --git a/testing/web-platform/tests/core-aam/aamtests/role/strong.py b/testing/web-platform/tests/core-aam/aamtests/role/strong.py index 5a8a7812de59..780abf443428 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/strong.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/strong.py @@ -27,9 +27,13 @@ def test_atspi(atspi, session, inline): # # Role: IA2_ROLE_TEXT_FRAME # # Object Attribute: xml-roles:strong -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Text -# # Localized Control Type: strong +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Text + # Localized Control Type: strong + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Text + assert node.CurrentLocalizedControlType == "strong" diff --git a/testing/web-platform/tests/core-aam/aamtests/role/subscript.py b/testing/web-platform/tests/core-aam/aamtests/role/subscript.py index 4d112f96d0a4..c181ae3aa961 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/subscript.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/subscript.py @@ -26,9 +26,23 @@ def test_atspi(atspi, session, inline): # # Role: IA2_ROLE_TEXT_FRAME # # Text Attribute: text-position:sub -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Text -# # Styles used are exposed by IsSubscript attribute of the TextRange Control Pattern implemented on the accessible object.: IsSubscript: attribute of the TextRange Control Pattern implemented on the accessible object. +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Text + # Styles used are exposed by IsSubscript attribute of the Text Control Pattern implemented on the accessible object.: IsSubscript: attribute of the TextRange Control Pattern implemented on the accessible object. + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Text + + # Chrome exposes this as a textChild pattern, which is not a violation of UIA. + assert (node.GetCurrentPropertyValue(uia.PropertyId.IsTextPatternAvailable) or node.GetCurrentPropertyValue(uia.PropertyId.IsTextChildPatternAvailable)) + + text_child = node.GetCurrentPattern(uia.PatternId.TextChild) + assert text_child is not None + + text_range = text_child.TextRange + + assert text_range.IsSubscript + assert text_range.IsSuperscript == False diff --git a/testing/web-platform/tests/core-aam/aamtests/role/suggestion.py b/testing/web-platform/tests/core-aam/aamtests/role/suggestion.py index a419c9f81c96..bbb84cdf1fca 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/suggestion.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/suggestion.py @@ -27,9 +27,13 @@ def test_atspi(atspi, session, inline): # # Role: IA2_ROLE_SUGGESTION # # Object Attribute: xml-roles:suggestion -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Group -# # Localized Control Type: suggestion +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Group + # Localized Control Type: suggestion + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Group + assert node.CurrentLocalizedControlType == "suggestion" diff --git a/testing/web-platform/tests/core-aam/aamtests/role/superscript.py b/testing/web-platform/tests/core-aam/aamtests/role/superscript.py index 1cfb531c15db..bce204b7d1c3 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/superscript.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/superscript.py @@ -26,9 +26,23 @@ def test_atspi(atspi, session, inline): # # Role: IA2_ROLE_TEXT_FRAME # # Text Attribute: text-position:super -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Text -# # Styles used are exposed by IsSuperscript attribute of the TextRange Control Pattern implemented on the accessible object.: IsSuperscript: attribute of the TextRange Control Pattern implemented on the accessible object. +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Text + # Styles used are exposed by IsSuperscript attribute of the TextRange Control Pattern implemented on the accessible object.: IsSuperscript: attribute of the TextRange Control Pattern implemented on the accessible object. + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Text + + # Chrome exposes this as a TextChild pattern, which is not a violation of UIA. + assert (node.GetCurrentPropertyValue(uia.PropertyId.IsTextPatternAvailable) or node.GetCurrentPropertyValue(uia.PropertyId.IsTextChildPatternAvailable)) + + text_child = node.GetCurrentPattern(uia.PatternId.TextChild) + assert text_child is not None + + text_range = text_child.TextRange + + assert text_range.IsSuperscript + assert text_range.IsSubscript == False \ No newline at end of file diff --git a/testing/web-platform/tests/core-aam/aamtests/role/switch.py b/testing/web-platform/tests/core-aam/aamtests/role/switch.py index 249fb847f45f..1a42e6d9e5b2 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/switch.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/switch.py @@ -30,11 +30,19 @@ def test_atspi(atspi, session, inline): # # Object Attribute: xml-roles:switch # # See also: aria-checked in the State and Property Mapping Tables -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Button -# # Localized Control Type: toggleswitch -# # Control Pattern: Toggle -# # See also: aria-checked in the State and Property Mapping Tables +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Button + # Localized Control Type: toggleswitch + # Control Pattern: Toggle + # See also: aria-checked in the State and Property Mapping Tables + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Button + assert node.CurrentLocalizedControlType == "toggleswitch" + + assert node.GetCurrentPropertyValue(uia.PropertyId.IsTogglePatternAvailable) + toggle_pattern = node.GetCurrentPattern(uia.PatternId.Toggle) + assert toggle_pattern and toggle_pattern.CurrentToggleState == 0 diff --git a/testing/web-platform/tests/core-aam/aamtests/role/tab.py b/testing/web-platform/tests/core-aam/aamtests/role/tab.py index 86a93c7c9cf6..b68957486295 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/tab.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/tab.py @@ -26,8 +26,11 @@ def test_atspi(atspi, session, inline): # # Role: ROLE_SYSTEM_PAGETAB # # State: STATE_SYSTEM_SELECTED: if focus is inside tabpanel associated with aria-labelledby -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: TabItem +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: TabItem + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.TabItem diff --git a/testing/web-platform/tests/core-aam/aamtests/role/table.py b/testing/web-platform/tests/core-aam/aamtests/role/table.py index 427b714a48e4..2e7163496847 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/table.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/table.py @@ -33,10 +33,15 @@ def test_atspi(atspi, session, inline): # # Object Attribute: xml-roles:table # # Interface: IAccessibleTable2 -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Table -# # Control Pattern: Grid -# # Control Pattern: Table +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Table + # Control Pattern: Grid + # Control Pattern: Table + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Table + assert node.GetCurrentPropertyValue(uia.PropertyId.IsGridPatternAvailable) + assert node.GetCurrentPropertyValue(uia.PropertyId.IsTablePatternAvailable) diff --git a/testing/web-platform/tests/core-aam/aamtests/role/tablist.py b/testing/web-platform/tests/core-aam/aamtests/role/tablist.py index 05e0bb5d760f..33558658ff19 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/tablist.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/tablist.py @@ -29,9 +29,16 @@ def test_atspi(atspi, session, inline): # # Method: IAccessible::accSelect() # # Method: IAccessible::get_accSelection() -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Tab -# # Control Pattern: Selection +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Tab + # Control Pattern: Selection + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Tab + + assert node.GetCurrentPropertyValue(uia.PropertyId.IsSelectionItemPatternAvailable) + selection_pattern = node.GetCurrentPattern(uia.PatternId.SelectionItem) + assert selection_pattern and selection_pattern.CurrentIsSelected == 0 diff --git a/testing/web-platform/tests/core-aam/aamtests/role/tabpanel.py b/testing/web-platform/tests/core-aam/aamtests/role/tabpanel.py index a2a6f7dd290a..1fea7f3a63e0 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/tabpanel.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/tabpanel.py @@ -24,8 +24,11 @@ def test_atspi(atspi, session, inline): # # Spec: # # Role: ROLE_SYSTEM_PANE: or ROLE_SYSTEM_PROPERTYPAGE -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Pane +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Pane + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Pane diff --git a/testing/web-platform/tests/core-aam/aamtests/role/term.py b/testing/web-platform/tests/core-aam/aamtests/role/term.py index 303da210b3bd..5f4b96ba68d6 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/term.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/term.py @@ -25,9 +25,13 @@ def test_atspi(atspi, session, inline): # # Role: IA2_ROLE_TEXT_FRAME # # Object Attribute: xml-roles:term -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Text -# # Localized Control Type: term +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Text + # Localized Control Type: term + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Text + assert node.CurrentLocalizedControlType == "term" diff --git a/testing/web-platform/tests/core-aam/aamtests/role/textbox.py b/testing/web-platform/tests/core-aam/aamtests/role/textbox.py index 3c2861064c9b..f8a217bae283 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/textbox.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/textbox.py @@ -44,8 +44,11 @@ def test_atspi_readonly(atspi, session, inline): # # Role: ROLE_SYSTEM_TEXT # # State: IA2_STATE_SINGLE_LINE -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Edit +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Edit + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Edit diff --git a/testing/web-platform/tests/core-aam/aamtests/role/textbox_multiline.py b/testing/web-platform/tests/core-aam/aamtests/role/textbox_multiline.py index fd1951b084e9..df37c4aa897e 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/textbox_multiline.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/textbox_multiline.py @@ -44,8 +44,11 @@ def test_atspi_readonly(atspi, session, inline): # # Role: ROLE_SYSTEM_TEXT # # State: IA2_STATE_MULTI_LINE -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Edit +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Edit + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Edit diff --git a/testing/web-platform/tests/core-aam/aamtests/role/time-role.py b/testing/web-platform/tests/core-aam/aamtests/role/time-role.py index 83a48ef2e093..ea78b293cc50 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/time-role.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/time-role.py @@ -27,10 +27,14 @@ def test_atspi(atspi, session, inline): # # Role: ROLE_SYSTEM_GROUPING # # Object Attribute: xml-roles:time -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Text -# # Localized Control Type: time -# # Note: create a separate UIA Control of type Text. This is different from most UIA text mappings, which only create ranges in the page text pattern. +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Text + # Localized Control Type: time + # Note: create a separate UIA Control of type Text. This is different from most UIA text mappings, which only create ranges in the page text pattern. + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Text + assert node.CurrentLocalizedControlType == "time" diff --git a/testing/web-platform/tests/core-aam/aamtests/role/timer.py b/testing/web-platform/tests/core-aam/aamtests/role/timer.py index d0d43e74edc3..2a8b7ba88c0b 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/timer.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/timer.py @@ -24,9 +24,13 @@ def test_atspi(atspi, session, inline): # # Spec: # # Object Attribute: xml-roles:timer -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Group -# # Localized Control Type: timer +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Group + # Localized Control Type: timer + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Group + assert node.CurrentLocalizedControlType == "timer" diff --git a/testing/web-platform/tests/core-aam/aamtests/role/toolbar.py b/testing/web-platform/tests/core-aam/aamtests/role/toolbar.py index b8821eb9bac7..203394c9080e 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/toolbar.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/toolbar.py @@ -24,8 +24,11 @@ def test_atspi(atspi, session, inline): # # Spec: # # Role: ROLE_SYSTEM_TOOLBAR -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: ToolBar +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: ToolBar + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.ToolBar diff --git a/testing/web-platform/tests/core-aam/aamtests/role/tooltip.py b/testing/web-platform/tests/core-aam/aamtests/role/tooltip.py index 52b0703ac6ca..ed66b7b978ba 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/tooltip.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/tooltip.py @@ -24,8 +24,11 @@ def test_atspi(atspi, session, inline): # # Spec: # # Role: ROLE_SYSTEM_TOOLTIP -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: ToolTip +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: ToolTip + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.ToolTip diff --git a/testing/web-platform/tests/core-aam/aamtests/role/tree.py b/testing/web-platform/tests/core-aam/aamtests/role/tree.py index 9cf27f41a25f..2b3c30e3e9b5 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/tree.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/tree.py @@ -29,8 +29,12 @@ def test_atspi(atspi, session, inline): # # Method: IAccessible::accSelect() # # Method: IAccessible::get_accSelection() -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: Tree +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: Tree + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.Tree + diff --git a/testing/web-platform/tests/core-aam/aamtests/role/treegrid.py b/testing/web-platform/tests/core-aam/aamtests/role/treegrid.py index 510ef703b27d..5718a6418d61 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/treegrid.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/treegrid.py @@ -32,8 +32,11 @@ def test_atspi(atspi, session, inline): # # Method: IAccessible::accSelect() # # Method: IAccessible::get_accSelection() -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: DataGrid +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: DataGrid + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.DataGrid diff --git a/testing/web-platform/tests/core-aam/aamtests/role/treeitem.py b/testing/web-platform/tests/core-aam/aamtests/role/treeitem.py index 83ac84880e0b..6b7bbbc5b7bf 100644 --- a/testing/web-platform/tests/core-aam/aamtests/role/treeitem.py +++ b/testing/web-platform/tests/core-aam/aamtests/role/treeitem.py @@ -26,9 +26,12 @@ def test_atspi(atspi, session, inline): # # Role: ROLE_SYSTEM_OUTLINEITEM # # See also: aria-checked in the State and Property Mapping Tables -# def test_uia(uia, session, inline): -# session.url = inline(TEST_HTML) -# -# # Spec: -# # Control Type: TreeItem -# # See also: aria-checked in the State and Property Mapping Tables +def test_uia(uia, session, inline): + session.url = inline(TEST_HTML) + + # Spec: + # Control Type: TreeItem + # See also: aria-checked in the State and Property Mapping Tables + + node = uia.find_node("test", session.url) + assert node.CurrentControlType == uia.ControlType.TreeItem diff --git a/testing/web-platform/tests/core-aam/aamtests/support/atspi_wrapper.py b/testing/web-platform/tests/core-aam/aamtests/support/atspi_wrapper.py index 145b2b647ae3..097999facf13 100644 --- a/testing/web-platform/tests/core-aam/aamtests/support/atspi_wrapper.py +++ b/testing/web-platform/tests/core-aam/aamtests/support/atspi_wrapper.py @@ -113,8 +113,6 @@ class AtspiWrapper(ApiWrapper[Atspi.Accessible]): tab = relation.get_target(0) if self._is_ready(tab, self.test_url): return tab - else: - return None continue for i in range(Atspi.Accessible.get_child_count(node)): diff --git a/testing/web-platform/tests/core-aam/aamtests/support/fixtures_a11y_api.py b/testing/web-platform/tests/core-aam/aamtests/support/fixtures_a11y_api.py index 4cf85804e8cf..285469f931f3 100644 --- a/testing/web-platform/tests/core-aam/aamtests/support/fixtures_a11y_api.py +++ b/testing/web-platform/tests/core-aam/aamtests/support/fixtures_a11y_api.py @@ -45,11 +45,14 @@ def axapi(session, default_timeout): @pytest.fixture -def uia(session): +def uia(session, default_timeout): if platform != "win32": pytest.skip("NOT_APPLICABLE") - # TODO: Make UiaWrapper and return it + from .uia_wrapper import UiaWrapper + + pid, product_name = pid_from(session.capabilities) + return UiaWrapper(pid, product_name, default_timeout) @pytest.fixture diff --git a/testing/web-platform/tests/core-aam/aamtests/support/uia_wrapper.py b/testing/web-platform/tests/core-aam/aamtests/support/uia_wrapper.py new file mode 100644 index 000000000000..31ed0b1268d0 --- /dev/null +++ b/testing/web-platform/tests/core-aam/aamtests/support/uia_wrapper.py @@ -0,0 +1,317 @@ +from __future__ import annotations + +from typing import Any, Optional + +import comtypes +import comtypes.client + +from .api_wrapper import ApiWrapper + +comtypes.CoInitialize() + +# Load the UI Automation type library. This exposes the IUIAutomation +# interface, the CUIAutomation coclass, and the UIA_* property, control type, +# pattern, and tree scope id constants. +UIA = comtypes.client.GetModule("UIAutomationCore.dll") + +# The IUIAutomation entry point, used to obtain the root element, create +# property conditions, and walk the tree. +_automation = comtypes.client.CreateObject( + UIA.CUIAutomation, interface=UIA.IUIAutomation +) + + +class UiaConstant(int): + """An integer that prints its human-readable name in test failures.""" + + def __new__(cls, value: int, name: str): + obj = super().__new__(cls, value) + obj.name = name + return obj + + def __repr__(self) -> str: + # This is what Pytest will show in the error log + return f"<{self.name}: {int(self)}>" + + +class _ConstantsProxy: + """Dynamically routes dot-notation lookups to UIA integer constants.""" + def __init__(self, mapping: dict[int, str]): + self._id_to_name = mapping + # Reverse the {id: "Name"} mapping into {"Name": id} + self._name_to_id = {name: val for val, name in mapping.items()} + + def __getattr__(self, name: str) -> UiaConstant: + if name in self._name_to_id: + return UiaConstant(self._name_to_id[name], name) + raise AttributeError(f"UIA constant '{name}' does not exist.") + + def __dir__(self) -> list[str]: + # Exposes the names to IDE autocompletion and dir() calls + return list(self._name_to_id.keys()) + + +# ---- Maps for turning UIA constants into human readable names. + +# Generate a mapping of control type id to human readable name, e.g. 50000 -> "Button", 50001 -> "Calendar", etc. +UIA_CONTROL_TYPE_MAP = { + value: name[len("UIA_"):-len("ControlTypeId")] + for name, value in vars(UIA).items() + if name.startswith("UIA_") and name.endswith("ControlTypeId") +} + +# Generate a mapping of property id to human readable name, e.g. 30000 -> "AutomationId", 30001 -> "Name", etc. +UIA_PROPERTY_ID_MAP = { + value: name[len("UIA_"):-len("PropertyId")] + for name, value in vars(UIA).items() + if name.startswith("UIA_") and name.endswith("PropertyId") +} + +# Generate a mapping of event id to human readable name, e.g. 20000 -> "AutomationFocusChangedEvent", 20001 -> "AutomationPropertyChangedEvent", etc. +UIA_EVENT_ID_MAP = { + value: name[len("UIA_"):-len("EventId")] + for name, value in vars(UIA).items() + if name.startswith("UIA_") and name.endswith("EventId") +} + +# Generate a mapping of pattern id to human readable name, e.g. 10000 -> "InvokePattern", 10001 -> "SelectionPattern", etc. +UIA_PATTERN_ID_MAP = { + value: name[len("UIA_"):-len("PatternId")] + for name, value in vars(UIA).items() + if name.startswith("UIA_") and name.endswith("PatternId") +} + +# Generate a mapping of landmark type id to human readable name, e.g. 8000 -> "Custom", 80001 -> "Form", etc. +UIA_LANDMARK_TYPE_ID_MAP = { + value: name[len("UIA_"):-len("LandmarkTypeId")] + for name, value in vars(UIA).items() + if name.startswith("UIA_") and name.endswith("LandmarkTypeId") +} + +UIA_TEXT_ATTRIBUTE_ID_MAP = { + value: name[len("UIA_"):-len("AttributeId")] + for name, value in vars(UIA).items() + if name.startswith("UIA_") and name.endswith("AttributeId") +} +UIA_TEXT_ATTRIBUTE_NAME_MAP = {name: value for value, name in UIA_TEXT_ATTRIBUTE_ID_MAP.items()} + +# Master map used to decode properties back into UiaConstants +_VALUE_MAPS = { + "ControlType": UIA_CONTROL_TYPE_MAP, + "PropertyId": UIA_PROPERTY_ID_MAP, + "EventId": UIA_EVENT_ID_MAP, + "PatternId": UIA_PATTERN_ID_MAP, + "LandmarkType": UIA_LANDMARK_TYPE_ID_MAP, + "TextAttribute": UIA_TEXT_ATTRIBUTE_ID_MAP, +} + + +# ---- UiaObject wrapper allows mostly for easier reading and writing. + +class UiaObject: + """A single, transparent proxy for Elements, Patterns, and TextRanges.""" + + def __init__(self, obj: Any): + object.__setattr__(self, "_obj", obj) + + def __getattr__(self, name: str) -> Any: + obj = self._obj + + # 1. Intercept TextAttributes natively + if name in UIA_TEXT_ATTRIBUTE_NAME_MAP and hasattr(obj, "GetAttributeValue"): + attr_id = UIA_TEXT_ATTRIBUTE_NAME_MAP[name] + raw_attr = obj.GetAttributeValue(attr_id) + + # 2. Native + elif hasattr(obj, name): + raw_attr = getattr(obj, name) + + else: + raise AttributeError( + f"Wrapped UIA object has no attribute '{name}'. " + f"Available attributes: {dir(obj)}" + ) + + # 4. Handle methods natively + if callable(raw_attr): + def method_proxy(*args, **kwargs): + unwrapped_args = [_unwrap_arg(arg) for arg in args] + result = raw_attr(*unwrapped_args, **kwargs) + + # --- Decode GetPropertyValue Integers --- + if name in ("GetCurrentPropertyValue", "GetCachedPropertyValue") and args: + prop_name = UIA_PROPERTY_ID_MAP.get(int(args[0]), "") + return _decode(prop_name, result) + + # --- Cast Patterns Before Wrapping --- + if name in ("GetCurrentPattern", "GetCachedPattern") and result and args: + pattern_name = UIA_PATTERN_ID_MAP.get(int(args[0])) + if pattern_name: + interface = getattr(UIA, f"IUIAutomation{pattern_name}Pattern", None) + if interface: + result = result.QueryInterface(interface) + + return _wrap_object(result) + return method_proxy + + # 5. Handle properties natively and decode them + result = _wrap_object(raw_attr) + + base_name = name + if name.startswith("Current"): base_name = name[7:] + elif name.startswith("Cached"): base_name = name[6:] + + return _decode(base_name, result) + + def __setattr__(self, name: str, value: Any): + if name.startswith("_"): + object.__setattr__(self, name, value) + else: + setattr(self._obj, name, value) + + +# ---- Wrapping & Decoding helpers + +def _decode(name: str, result: Any) -> Any: + """Decodes raw integers into UiaConstants if a map exists for the property.""" + if type(result) is int and name in _VALUE_MAPS: + str_name = _VALUE_MAPS[name].get(result, "Unknown") + return UiaConstant(result, str_name) + + if isinstance(result, (list, tuple)) and all(type(x) is int for x in result): + return [_decode(name, x) for x in result] + + return result + +def _unwrap_arg(arg: Any) -> Any: + """Unwraps proxy objects to hand raw COM pointers back to native UIA methods.""" + return arg._obj if isinstance(arg, UiaObject) else arg + +def _wrap_object(result: Any) -> Any: + """Dynamically wraps raw object UIA COM returns into the universal proxy.""" + if result is None: + return None + + if hasattr(result, "Length") and hasattr(result, "GetElement"): + return [_wrap_object(result.GetElement(i)) for i in range(result.Length)] + + type_name = type(result).__name__ + + if "Element" in type_name or "TextRange" in type_name or "Pattern" in type_name: + return UiaObject(result) + + return result + + +# ---- Main API Wrapper + +class UiaWrapper(ApiWrapper[UiaObject]): + ControlType = _ConstantsProxy(UIA_CONTROL_TYPE_MAP) + PropertyId = _ConstantsProxy(UIA_PROPERTY_ID_MAP) + EventId = _ConstantsProxy(UIA_EVENT_ID_MAP) + PatternId = _ConstantsProxy(UIA_PATTERN_ID_MAP) + LandmarkType = _ConstantsProxy(UIA_LANDMARK_TYPE_ID_MAP) + TextAttribute = _ConstantsProxy(UIA_TEXT_ATTRIBUTE_ID_MAP) + + @property + def api_name(self) -> str: + return "UIA" + + def find_node(self, dom_id: str, url: str) -> Optional[UiaObject]: + """ + :param dom_id: The dom id of the node to test. + :param url: The url of the test. + """ + if self.test_url != url or not self.document: + self.test_url = url + self.document = self._poll_for( + self._find_tab, + f"Timeout looking for url: {self.test_url}", + ) + + test_node = self._poll_for( + lambda: self._find_node_by_id(self.document, dom_id), + f"Timeout looking for node with id {dom_id} in accessibility API UIA.", + ) + + return test_node + + def _find_browser(self) -> Optional[UiaObject]: + """Find the UIA element representing the browser's top level window. + + :return: The browser element or None. + """ + if self.pid and self.pid != 0: + return self._find_browser_by_pid() + return self._find_browser_by_name() + + def _find_browser_by_pid(self) -> Optional[UiaElement]: + """Find the browser window by matching the process id. + + :return: The browser element or None. + """ + root = _wrap_object(_automation.GetRootElement()) + condition = _automation.CreatePropertyCondition( + UIA.UIA_ProcessIdPropertyId, self.pid + ) + return root.FindFirst(UIA.TreeScope_Children, condition) + + def _find_browser_by_name(self) -> Optional[UiaObject]: + """Find the browser window by matching the product name. + + Used when no pid is available (e.g. servo passes pid 0). + + :return: The browser element or None. + """ + root = _automation.GetRootElement() + walker = _automation.ControlViewWalker + element = _wrap_object(walker.GetFirstChildElement(root)) + while element: + name = element.CurrentName or "" + if self.product_name.lower() in name.lower(): + return element + element = _wrap_object(walker.GetNextSiblingElement(_unwrap_arg(element))) + return None + + def _find_tab(self) -> Optional[UiaObject]: + """Find the document with the test url. + + :return: The element representing the test document or None. + """ + condition = _automation.CreatePropertyCondition( + UIA.UIA_ControlTypePropertyId, UIA.UIA_DocumentControlTypeId + ) + wrapped_root = _wrap_object(self.root) + + documents = wrapped_root.FindAll(UIA.TreeScope_Descendants, condition) + for document in documents: + if self._document_url(document) == self.test_url: + return document + return None + + def _document_url(self, document: UiaObject) -> Optional[str]: + """Return the url of a document element. + + Browsers expose the document url via the UIA Value property, mirroring + IAccessible2's accValue on the document. + + :param document: A document control element. + :return: The url string or None. + """ + return document.GetCurrentPropertyValue(UIA.UIA_ValueValuePropertyId) + + def _find_node_by_id( + self, root: UiaObject, dom_id: str + ) -> Optional[UiaObject]: + """Find the UIA element with a specified dom_id. + + Browsers expose the DOM id via the UIA AutomationId property. + + :param root: The root node to search from. + :param dom_id: The DOM id. + :return: The element or None if not found. + """ + condition = _automation.CreatePropertyCondition( + UIA.UIA_AutomationIdPropertyId, dom_id + ) + return root.FindFirst(UIA.TreeScope_Descendants, condition) diff --git a/testing/web-platform/tests/css/css-borders/corner-shape/corner-shape-squircle-ref.html b/testing/web-platform/tests/css/css-borders/corner-shape/corner-shape-squircle-ref.html new file mode 100644 index 000000000000..cc7105a9b4fc --- /dev/null +++ b/testing/web-platform/tests/css/css-borders/corner-shape/corner-shape-squircle-ref.html @@ -0,0 +1,22 @@ + + +CSS Borders and Box Decorations 4: 'corner-shape: squircle' on all corners — reference + + +
diff --git a/testing/web-platform/tests/css/css-borders/corner-shape/corner-shape-squircle.html b/testing/web-platform/tests/css/css-borders/corner-shape/corner-shape-squircle.html new file mode 100644 index 000000000000..eed68c2b0664 --- /dev/null +++ b/testing/web-platform/tests/css/css-borders/corner-shape/corner-shape-squircle.html @@ -0,0 +1,19 @@ + + +CSS Borders and Box Decorations 4: 'corner-shape: squircle' on all corners + + + + + +
diff --git a/testing/web-platform/tests/css/css-borders/corner-shape/corner-shape-superellipse-concave-ref.html b/testing/web-platform/tests/css/css-borders/corner-shape/corner-shape-superellipse-concave-ref.html new file mode 100644 index 000000000000..6e171256c3b4 --- /dev/null +++ b/testing/web-platform/tests/css/css-borders/corner-shape/corner-shape-superellipse-concave-ref.html @@ -0,0 +1,11 @@ + + +CSS Borders and Box Decorations 4: per-corner concave 'corner-shape' superellipse values — reference + + +
diff --git a/testing/web-platform/tests/css/css-borders/corner-shape/corner-shape-superellipse-concave.html b/testing/web-platform/tests/css/css-borders/corner-shape/corner-shape-superellipse-concave.html new file mode 100644 index 000000000000..749a6e922827 --- /dev/null +++ b/testing/web-platform/tests/css/css-borders/corner-shape/corner-shape-superellipse-concave.html @@ -0,0 +1,19 @@ + + +CSS Borders and Box Decorations 4: per-corner concave 'corner-shape' superellipse values + + + + + +
diff --git a/testing/web-platform/tests/css/css-borders/corner-shape/corner-shape-superellipse-convex-ref.html b/testing/web-platform/tests/css/css-borders/corner-shape/corner-shape-superellipse-convex-ref.html new file mode 100644 index 000000000000..e6859dad1c4b --- /dev/null +++ b/testing/web-platform/tests/css/css-borders/corner-shape/corner-shape-superellipse-convex-ref.html @@ -0,0 +1,11 @@ + + +CSS Borders and Box Decorations 4: per-corner convex 'corner-shape' superellipse values — reference + + +
diff --git a/testing/web-platform/tests/css/css-borders/corner-shape/corner-shape-superellipse-convex.html b/testing/web-platform/tests/css/css-borders/corner-shape/corner-shape-superellipse-convex.html new file mode 100644 index 000000000000..cabc234c302a --- /dev/null +++ b/testing/web-platform/tests/css/css-borders/corner-shape/corner-shape-superellipse-convex.html @@ -0,0 +1,19 @@ + + +CSS Borders and Box Decorations 4: per-corner convex 'corner-shape' superellipse values + + + + + +
diff --git a/testing/web-platform/tests/css/css-borders/corner-shape/corner-shape-superellipse-squircle-ref.html b/testing/web-platform/tests/css/css-borders/corner-shape/corner-shape-superellipse-squircle-ref.html new file mode 100644 index 000000000000..eb7662228abc --- /dev/null +++ b/testing/web-platform/tests/css/css-borders/corner-shape/corner-shape-superellipse-squircle-ref.html @@ -0,0 +1,22 @@ + + +CSS Borders and Box Decorations 4: 'corner-shape: superellipse(2)' on all corners — reference + + +
diff --git a/testing/web-platform/tests/css/css-borders/corner-shape/corner-shape-superellipse-squircle.html b/testing/web-platform/tests/css/css-borders/corner-shape/corner-shape-superellipse-squircle.html new file mode 100644 index 000000000000..48db2b9f0e0c --- /dev/null +++ b/testing/web-platform/tests/css/css-borders/corner-shape/corner-shape-superellipse-squircle.html @@ -0,0 +1,19 @@ + + +CSS Borders and Box Decorations 4: 'corner-shape: superellipse(2)' on all corners + + + + + +
diff --git a/testing/web-platform/tests/css/css-cascade/revert-rule-cycle.tentative.html b/testing/web-platform/tests/css/css-cascade/revert-rule-cycle.tentative.html new file mode 100644 index 000000000000..c641437a12e8 --- /dev/null +++ b/testing/web-platform/tests/css/css-cascade/revert-rule-cycle.tentative.html @@ -0,0 +1,88 @@ + +The revert-rule keyword: cycle resolution + + + + + + + + + +
+
+
+
+ + diff --git a/testing/web-platform/tests/css/css-fonts/cjk-kerning.html b/testing/web-platform/tests/css/css-fonts/cjk-kerning.html index 5851757c8486..e8da775531e4 100644 --- a/testing/web-platform/tests/css/css-fonts/cjk-kerning.html +++ b/testing/web-platform/tests/css/css-fonts/cjk-kerning.html @@ -11,7 +11,7 @@ + + + +
+
1
+
2
+
3
+
+
+
+
+ + +
+
1
+
2
+
3
+
+
+
+
+ + diff --git a/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-align-content-001.html b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-align-content-001.html new file mode 100644 index 000000000000..b757a9dbe69d --- /dev/null +++ b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-align-content-001.html @@ -0,0 +1,60 @@ + + + + + CSS Grid Lanes Test: OOF item static positions don't change with content alignment in columns + + + + + + +
+ 1 + 2 + 3 +
+
+
+
+ +
+ 1 + 2 + 3 +
+
+
+
+ + diff --git a/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-align-content-002-ref.html b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-align-content-002-ref.html new file mode 100644 index 000000000000..e528d79eee4d --- /dev/null +++ b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-align-content-002-ref.html @@ -0,0 +1,60 @@ + + + + + + + + +
+
1
+
2
+
3
+
X
+
X
+
X
+
+ + +
+
1
+
2
+
3
+
X
+
X
+
X
+
+ + diff --git a/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-align-content-002.html b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-align-content-002.html new file mode 100644 index 000000000000..dae8d12407b1 --- /dev/null +++ b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-align-content-002.html @@ -0,0 +1,62 @@ + + + + + CSS Grid Lanes Test: OOF static positions don't change with content alignment in columns + + + + + + +
+ 1 + 2 + 3 +
X
+
X
+
X
+
+ +
+ 1 + 2 + 3 +
X
+
X
+
X
+
+ + diff --git a/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-align-content-003-ref.html b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-align-content-003-ref.html new file mode 100644 index 000000000000..a62da10dbdbb --- /dev/null +++ b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-align-content-003-ref.html @@ -0,0 +1,57 @@ + + + + + + + + +
+
1
+
2
+
3
+
+
+
+
+ + +
+
1
+
2
+
3
+
+
+
+
+ + diff --git a/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-align-content-003.html b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-align-content-003.html new file mode 100644 index 000000000000..28fc12c26fed --- /dev/null +++ b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-align-content-003.html @@ -0,0 +1,60 @@ + + + + + CSS Grid Lanes Test: OOF static positions don't change with content alignment in columns + + + + + + +
+ 1 + 2 + 3 +
+
+
+
+ +
+ 1 + 2 + 3 +
+
+
+
+ + diff --git a/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-fill-reverse-001-ref.html b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-fill-reverse-001-ref.html new file mode 100644 index 000000000000..6ba2ccb214fd --- /dev/null +++ b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-fill-reverse-001-ref.html @@ -0,0 +1,47 @@ + + + + + + + +
+
1
+
2
+
3
+
+
+
+
+ + diff --git a/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-fill-reverse-001.html b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-fill-reverse-001.html new file mode 100644 index 000000000000..8caa31df0385 --- /dev/null +++ b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-fill-reverse-001.html @@ -0,0 +1,55 @@ + + + + + CSS Grid Lanes Test: fill-reverse accounts for OOF static positions in column grid-lanes with indefinite block size + + + + + + + +
+ 1 + 2 + 3 +
+
+
+
+ + diff --git a/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-fill-reverse-002-ref.html b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-fill-reverse-002-ref.html new file mode 100644 index 000000000000..87b6c8600fe5 --- /dev/null +++ b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-fill-reverse-002-ref.html @@ -0,0 +1,45 @@ + + + + + + + +
+
1
+
2
+
3
+
4
+
+
+ + diff --git a/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-fill-reverse-002.html b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-fill-reverse-002.html new file mode 100644 index 000000000000..5c0172a35c84 --- /dev/null +++ b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-fill-reverse-002.html @@ -0,0 +1,57 @@ + + + + + CSS Grid Lanes Test: fill-reverse accounts for OOF static positions with explicit grid-column placement in column grid-lanes + + + + + + + +
+ 1 + 2 + 3 + 4 +
+
+ + diff --git a/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-fill-reverse-003-ref.html b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-fill-reverse-003-ref.html new file mode 100644 index 000000000000..3dc029b2b76e --- /dev/null +++ b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-fill-reverse-003-ref.html @@ -0,0 +1,37 @@ + + + + + + + + +
+
1
+
2
+
3
+
+ + diff --git a/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-fill-reverse-003.html b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-fill-reverse-003.html new file mode 100644 index 000000000000..4c7029059631 --- /dev/null +++ b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-fill-reverse-003.html @@ -0,0 +1,49 @@ + + + + + CSS Grid Lanes Test: fill-reverse OOF static position with no in-flow items in column grid-lanes + + + + + + + +
+
1
+
2
+
3
+
+ + diff --git a/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-justify-content-001-ref.html b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-justify-content-001-ref.html new file mode 100644 index 000000000000..edf1a2acb91d --- /dev/null +++ b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-justify-content-001-ref.html @@ -0,0 +1,57 @@ + + + + + + + + +
+
1
+
2
+
3
+
+
+
+
+ + +
+
1
+
2
+
3
+
+
+
+
+ + diff --git a/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-justify-content-001.html b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-justify-content-001.html new file mode 100644 index 000000000000..69c4bdabcaee --- /dev/null +++ b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/column-grid-lanes-oof-justify-content-001.html @@ -0,0 +1,65 @@ + + + + + CSS Grid Lanes Test: justify-content accounts for OOF static positions in column grid-lanes + + + + + + + +
+ 1 + 2 + 3 +
+
+
+
+ + +
+ 1 + 2 + 3 +
+
+
+
+ + diff --git a/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-align-content-001-ref.html b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-align-content-001-ref.html new file mode 100644 index 000000000000..adcd3899c2a9 --- /dev/null +++ b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-align-content-001-ref.html @@ -0,0 +1,57 @@ + + + + + + + + +
+
1
+
2
+
3
+
+
+
+
+ + +
+
1
+
2
+
3
+
+
+
+
+ + diff --git a/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-align-content-001.html b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-align-content-001.html new file mode 100644 index 000000000000..8dcfacfa472f --- /dev/null +++ b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-align-content-001.html @@ -0,0 +1,61 @@ + + + + + CSS Grid Lanes Test: OOF item static positions aren't affected by content alignment in row + + + + + + +
+ 1 + 2 + 3 +
+
+
+
+ +
+ 1 + 2 + 3 +
+
+
+
+ + diff --git a/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-fill-reverse-001-ref.html b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-fill-reverse-001-ref.html new file mode 100644 index 000000000000..c8a12d7a46a1 --- /dev/null +++ b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-fill-reverse-001-ref.html @@ -0,0 +1,50 @@ + + + + + + + + +
+
1
+
2
+
3
+
+
+
+
+ + diff --git a/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-fill-reverse-001.html b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-fill-reverse-001.html new file mode 100644 index 000000000000..626b52b5a1b4 --- /dev/null +++ b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-fill-reverse-001.html @@ -0,0 +1,56 @@ + + + + + CSS Grid Lanes Test: fill-reverse sets OOF static position to inline-end in row grid-lanes + + + + + + + +
+ 1 + 2 + 3 +
+
+
+
+ + diff --git a/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-fill-reverse-002-ref.html b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-fill-reverse-002-ref.html new file mode 100644 index 000000000000..43f869407333 --- /dev/null +++ b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-fill-reverse-002-ref.html @@ -0,0 +1,63 @@ + + + + + + + + +
+
+
1
+
3
+
+
+
2
+
4
+
+
+
+ + diff --git a/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-fill-reverse-002.html b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-fill-reverse-002.html new file mode 100644 index 000000000000..0a9077159e2e --- /dev/null +++ b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-fill-reverse-002.html @@ -0,0 +1,57 @@ + + + + + CSS Grid Lanes Test: fill-reverse accounts for OOF static positions with explicit grid-row placement in row grid-lanes + + + + + + + +
+ 1 + 2 + 3 + 4 +
+
+ + diff --git a/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-fill-reverse-003-ref.html b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-fill-reverse-003-ref.html new file mode 100644 index 000000000000..da29c5f3aeb7 --- /dev/null +++ b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-fill-reverse-003-ref.html @@ -0,0 +1,37 @@ + + + + + + + + +
+
1
+
2
+
3
+
+ + diff --git a/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-fill-reverse-003.html b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-fill-reverse-003.html new file mode 100644 index 000000000000..de5dde8a983d --- /dev/null +++ b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-fill-reverse-003.html @@ -0,0 +1,49 @@ + + + + + CSS Grid Lanes Test: fill-reverse OOF static position with no in-flow items in row grid-lanes + + + + + + + +
+
1
+
2
+
3
+
+ + diff --git a/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-justify-content-001-ref.html b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-justify-content-001-ref.html new file mode 100644 index 000000000000..e27247825fc5 --- /dev/null +++ b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-justify-content-001-ref.html @@ -0,0 +1,58 @@ + + + + + + + + +
+
1
+
2
+
3
+
+
+
+
+ + +
+
1
+
2
+
3
+
+
+
+
+ + diff --git a/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-justify-content-001.html b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-justify-content-001.html new file mode 100644 index 000000000000..091181cccfe6 --- /dev/null +++ b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-justify-content-001.html @@ -0,0 +1,61 @@ + + + + + CSS Grid Lanes Test: OOF item static positions aren't affected by content alignment in row + + + + + + +
+ 1 + 2 + 3 +
+
+
+
+ +
+ 1 + 2 + 3 +
+
+
+
+ + diff --git a/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-justify-content-002-ref.html b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-justify-content-002-ref.html new file mode 100644 index 000000000000..dab1d2af87c7 --- /dev/null +++ b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-justify-content-002-ref.html @@ -0,0 +1,65 @@ + + + + + + + + +
+
+
1
+
2
+
3
+
+
+
+
+
+ + +
+
+
1
+
2
+
3
+
+
+
+
+
+ + diff --git a/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-justify-content-002.html b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-justify-content-002.html new file mode 100644 index 000000000000..46c8b35e1e18 --- /dev/null +++ b/testing/web-platform/tests/css/css-grid/grid-lanes/abspos/row-grid-lanes-oof-justify-content-002.html @@ -0,0 +1,68 @@ + + + + + CSS Grid Lanes Test: OOF item static positions aren't affected by content alignment in row + + + + + + +
+
+ 1 + 2 + 3 +
+
+
+
+
+ +
+
+ 1 + 2 + 3 +
+
+
+
+
+ + diff --git a/testing/web-platform/tests/css/css-grid/grid-lanes/alignment/row-fill-reverse-align-items-indefinite-size-001-ref.html b/testing/web-platform/tests/css/css-grid/grid-lanes/alignment/row-fill-reverse-align-items-indefinite-size-001-ref.html index 470bd9c4f1d4..c321f9f10a27 100644 --- a/testing/web-platform/tests/css/css-grid/grid-lanes/alignment/row-fill-reverse-align-items-indefinite-size-001-ref.html +++ b/testing/web-platform/tests/css/css-grid/grid-lanes/alignment/row-fill-reverse-align-items-indefinite-size-001-ref.html @@ -81,6 +81,5 @@ item 6 - diff --git a/testing/web-platform/tests/css/css-grid/grid-lanes/alignment/row-fill-reverse-align-items-indefinite-size-001.html b/testing/web-platform/tests/css/css-grid/grid-lanes/alignment/row-fill-reverse-align-items-indefinite-size-001.html index 3c3e3e804d14..6f050ab9e8dd 100644 --- a/testing/web-platform/tests/css/css-grid/grid-lanes/alignment/row-fill-reverse-align-items-indefinite-size-001.html +++ b/testing/web-platform/tests/css/css-grid/grid-lanes/alignment/row-fill-reverse-align-items-indefinite-size-001.html @@ -59,6 +59,5 @@ item 5 item 6 - diff --git a/testing/web-platform/tests/css/css-inline/text-box-trim/text-box-trim-first-line-pseudo-004-ref.html b/testing/web-platform/tests/css/css-inline/text-box-trim/text-box-trim-first-line-pseudo-004-ref.html new file mode 100644 index 000000000000..604d543d94f5 --- /dev/null +++ b/testing/web-platform/tests/css/css-inline/text-box-trim/text-box-trim-first-line-pseudo-004-ref.html @@ -0,0 +1,18 @@ + +text-box-trim trims the last line when ::first-line is present and the last line follows a forced break + + + + +
+
AAAA
BBBB
+
diff --git a/testing/web-platform/tests/css/css-inline/text-box-trim/text-box-trim-first-line-pseudo-004.html b/testing/web-platform/tests/css/css-inline/text-box-trim/text-box-trim-first-line-pseudo-004.html new file mode 100644 index 000000000000..0de95973eb63 --- /dev/null +++ b/testing/web-platform/tests/css/css-inline/text-box-trim/text-box-trim-first-line-pseudo-004.html @@ -0,0 +1,23 @@ + +text-box-trim trims the last line when ::first-line is present and the last line follows a forced line break + + + + + + +
+
AAAA
BBBB
+
diff --git a/testing/web-platform/tests/css/css-inline/text-box-trim/text-box-trim-first-line-pseudo-005-ref.html b/testing/web-platform/tests/css/css-inline/text-box-trim/text-box-trim-first-line-pseudo-005-ref.html new file mode 100644 index 000000000000..db7d6a0ce587 --- /dev/null +++ b/testing/web-platform/tests/css/css-inline/text-box-trim/text-box-trim-first-line-pseudo-005-ref.html @@ -0,0 +1,19 @@ + +text-box-trim trims the last line when ::first-line is present and the last line follows a wrap + + + + +
+
AAAA BBBB
+
diff --git a/testing/web-platform/tests/css/css-inline/text-box-trim/text-box-trim-first-line-pseudo-005.html b/testing/web-platform/tests/css/css-inline/text-box-trim/text-box-trim-first-line-pseudo-005.html new file mode 100644 index 000000000000..ec82e26e9b7c --- /dev/null +++ b/testing/web-platform/tests/css/css-inline/text-box-trim/text-box-trim-first-line-pseudo-005.html @@ -0,0 +1,24 @@ + +text-box-trim trims the last line when ::first-line is present and the last line follows a wrap + + + + + + +
+
AAAA BBBB
+
diff --git a/testing/web-platform/tests/css/css-inline/text-box-trim/text-box-trim-fragmented-last-inline-box-001-ref.html b/testing/web-platform/tests/css/css-inline/text-box-trim/text-box-trim-fragmented-last-inline-box-001-ref.html new file mode 100644 index 000000000000..48ec6c0ecad5 --- /dev/null +++ b/testing/web-platform/tests/css/css-inline/text-box-trim/text-box-trim-fragmented-last-inline-box-001-ref.html @@ -0,0 +1,18 @@ + +text-box-trim trims the last line when it consists of a fragmented inline (due to <br>) + + + + +
+
AAAA
BBBB
CCCC
+
diff --git a/testing/web-platform/tests/css/css-inline/text-box-trim/text-box-trim-fragmented-last-inline-box-001.html b/testing/web-platform/tests/css/css-inline/text-box-trim/text-box-trim-fragmented-last-inline-box-001.html new file mode 100644 index 000000000000..b6fa551cabf8 --- /dev/null +++ b/testing/web-platform/tests/css/css-inline/text-box-trim/text-box-trim-fragmented-last-inline-box-001.html @@ -0,0 +1,22 @@ + +text-box-trim trims the last line when it consists of a fragmented inline (due to forced line break) + + + + + + +
+
+ AAAA
BBBB
CCCC
+
+
diff --git a/testing/web-platform/tests/css/css-inline/text-box-trim/text-box-trim-fragmented-last-inline-box-002-ref.html b/testing/web-platform/tests/css/css-inline/text-box-trim/text-box-trim-fragmented-last-inline-box-002-ref.html new file mode 100644 index 000000000000..6b09e8a9a146 --- /dev/null +++ b/testing/web-platform/tests/css/css-inline/text-box-trim/text-box-trim-fragmented-last-inline-box-002-ref.html @@ -0,0 +1,19 @@ + +text-box-trim trims the last line when it consists of a fragmented inline (due to wrapping) + + + + +
+
AAAA BBBB CCCC
+
diff --git a/testing/web-platform/tests/css/css-inline/text-box-trim/text-box-trim-fragmented-last-inline-box-002.html b/testing/web-platform/tests/css/css-inline/text-box-trim/text-box-trim-fragmented-last-inline-box-002.html new file mode 100644 index 000000000000..860dd1c8999f --- /dev/null +++ b/testing/web-platform/tests/css/css-inline/text-box-trim/text-box-trim-fragmented-last-inline-box-002.html @@ -0,0 +1,23 @@ + +text-box-trim trims the last line when it consists of a fragmented inline (due to wrapping) + + + + + + +
+
+ AAAA BBBB CCCC +
+
diff --git a/testing/web-platform/tests/css/css-masking/clip-path/animations/clip-path-animation-inset-rounded-100-percent-ref.html b/testing/web-platform/tests/css/css-masking/clip-path/animations/clip-path-animation-inset-rounded-100-percent-ref.html new file mode 100644 index 000000000000..32b0d5d1c99f --- /dev/null +++ b/testing/web-platform/tests/css/css-masking/clip-path/animations/clip-path-animation-inset-rounded-100-percent-ref.html @@ -0,0 +1,13 @@ + + + +
+ + \ No newline at end of file diff --git a/testing/web-platform/tests/css/css-masking/clip-path/animations/clip-path-animation-inset-rounded-100-percent.html b/testing/web-platform/tests/css/css-masking/clip-path/animations/clip-path-animation-inset-rounded-100-percent.html new file mode 100644 index 000000000000..77789bb46609 --- /dev/null +++ b/testing/web-platform/tests/css/css-masking/clip-path/animations/clip-path-animation-inset-rounded-100-percent.html @@ -0,0 +1,26 @@ + + + + + + + + +
+ + + + \ No newline at end of file diff --git a/testing/web-platform/tests/css/css-overflow/line-clamp/block-ellipsis-039.html b/testing/web-platform/tests/css/css-overflow/line-clamp/block-ellipsis-039.html new file mode 100644 index 000000000000..d1a81de76d52 --- /dev/null +++ b/testing/web-platform/tests/css/css-overflow/line-clamp/block-ellipsis-039.html @@ -0,0 +1,33 @@ + + +CSS Overflow: block-ellipsis root-inline strut + + + + + + +

This test passes if the two boxes below are identical, including having the same height. + +

+ TEST TEST TEST + +
+
+ TEST + … +
diff --git a/testing/web-platform/tests/css/css-overflow/line-clamp/block-ellipsis-040.html b/testing/web-platform/tests/css/css-overflow/line-clamp/block-ellipsis-040.html new file mode 100644 index 000000000000..1c2f3fa3ba5b --- /dev/null +++ b/testing/web-platform/tests/css/css-overflow/line-clamp/block-ellipsis-040.html @@ -0,0 +1,37 @@ + + +CSS Overflow: Room for block-ellipsis not by clearance + + + + + + + +

Test passes if there is a filled green square and no red. + +

+ + 1 + 2 + 3 + 4 + 5 +
+ diff --git a/testing/web-platform/tests/css/css-overflow/line-clamp/block-ellipsis-041.html b/testing/web-platform/tests/css/css-overflow/line-clamp/block-ellipsis-041.html new file mode 100644 index 000000000000..dc8f11d7ac0e --- /dev/null +++ b/testing/web-platform/tests/css/css-overflow/line-clamp/block-ellipsis-041.html @@ -0,0 +1,28 @@ + + +CSS Overflow: No clearance if block-ellipsis fits + + + + + + +

Test passes if there is a “…” below and no red. + +

+ + FAIL FAIL FAIL FAIL +
+ diff --git a/testing/web-platform/tests/css/css-overflow/line-clamp/line-clamp-with-floats-011.html b/testing/web-platform/tests/css/css-overflow/line-clamp/line-clamp-with-floats-011.html new file mode 100644 index 000000000000..e410eaa4b487 --- /dev/null +++ b/testing/web-platform/tests/css/css-overflow/line-clamp/line-clamp-with-floats-011.html @@ -0,0 +1,38 @@ + + +CSS Overflow: line-clamp hidden and clipped floats don't count as scrollable overflow + + + + + + + +

Test passes if there is a filled green square and no red. + +

+ +
diff --git a/testing/web-platform/tests/css/css-overflow/line-clamp/line-clamp-with-floats-012.html b/testing/web-platform/tests/css/css-overflow/line-clamp/line-clamp-with-floats-012.html new file mode 100644 index 000000000000..524eec50d3cf --- /dev/null +++ b/testing/web-platform/tests/css/css-overflow/line-clamp/line-clamp-with-floats-012.html @@ -0,0 +1,58 @@ + + +CSS Overflow: line-clamp clips floats at the block-end only + + + + + + + +

Test passes if there is a filled green square and no red.

+ +
..X
+
+ AAA + X + K + OOO + P +
+ diff --git a/testing/web-platform/tests/css/css-overflow/line-clamp/reference/block-ellipsis-039-ref.html b/testing/web-platform/tests/css/css-overflow/line-clamp/reference/block-ellipsis-039-ref.html new file mode 100644 index 000000000000..0f44c45f03fe --- /dev/null +++ b/testing/web-platform/tests/css/css-overflow/line-clamp/reference/block-ellipsis-039-ref.html @@ -0,0 +1,27 @@ + + +Test reference + + + +

This test passes if the two boxes below are identical, including having the same height. + +

+ TEST + … +
+
+ TEST + … +
diff --git a/testing/web-platform/tests/css/css-overflow/line-clamp/reference/block-ellipsis-041-ref.html b/testing/web-platform/tests/css/css-overflow/line-clamp/reference/block-ellipsis-041-ref.html new file mode 100644 index 000000000000..2dc26d84ba65 --- /dev/null +++ b/testing/web-platform/tests/css/css-overflow/line-clamp/reference/block-ellipsis-041-ref.html @@ -0,0 +1,22 @@ + + +Test reference + + + +

Test passes if there is a “…” below and no red. + +

+ … +
+ diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-parent-style-change.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-parent-style-change.html new file mode 100644 index 000000000000..1e799b3a7db4 --- /dev/null +++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-parent-style-change.html @@ -0,0 +1,30 @@ + + +CSS Pseudo-Elements Test: highlight cascade: inheritance after a parent style change + + + + + +
text
+ diff --git a/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-shadow-boundary.html b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-shadow-boundary.html new file mode 100644 index 000000000000..8422dfe3c666 --- /dev/null +++ b/testing/web-platform/tests/css/css-pseudo/highlight-cascade/highlight-cascade-shadow-boundary.html @@ -0,0 +1,29 @@ + + +CSS Pseudo-Elements Test: highlight cascade: inheritance across a shadow boundary + + + + + +
+
text
+ diff --git a/testing/web-platform/tests/css/css-typed-om/the-stylepropertymap/properties/logical.html b/testing/web-platform/tests/css/css-typed-om/the-stylepropertymap/properties/logical.html index 2e451d6aa99c..f06369b03185 100644 --- a/testing/web-platform/tests/css/css-typed-om/the-stylepropertymap/properties/logical.html +++ b/testing/web-platform/tests/css/css-typed-om/the-stylepropertymap/properties/logical.html @@ -13,6 +13,10 @@ + +
+
+ diff --git a/testing/web-platform/tests/css/css-values/if-conditionals.html b/testing/web-platform/tests/css/css-values/if-conditionals.html index 35b21e275647..9bf852e24d5f 100644 --- a/testing/web-platform/tests/css/css-values/if-conditionals.html +++ b/testing/web-platform/tests/css/css-values/if-conditionals.html @@ -767,4 +767,7 @@ // Equality of attr-tainted if() test_if_with_custom_properties('if(style(--x: attr(data-attr type(*))): true_value; else: false_value)', [['--x', 'attr']], 'true_value'); test_if_with_custom_properties('if(style(--x: attr): true_value; else: false_value)', [['--x', 'attr(data-attr type(*))']], 'true_value'); + + // The else keyword is recognized after substitution, so a var() that expands to else matches. + test_if_with_custom_properties('if(var(--else): true_value; else: false_value)', [['--else', 'else']], 'true_value'); diff --git a/testing/web-platform/tests/css/css-values/random-in-if.tentative.html b/testing/web-platform/tests/css/css-values/random-in-if.tentative.html index afc68363b630..43bca6b19135 100644 --- a/testing/web-platform/tests/css/css-values/random-in-if.tentative.html +++ b/testing/web-platform/tests/css/css-values/random-in-if.tentative.html @@ -1,6 +1,7 @@ CSS Values and Units Test: random() in if() + @@ -47,11 +48,11 @@ test(() => { const elComputedValue = getComputedStyle(el).getPropertyValue('--unregistered'); - assert_equals(elComputedValue, 'false'); + assert_equals(elComputedValue, 'true'); } finally { document.body.removeChild(holder); } -}, `random() should not be allowed in if() style() condition`); +}, `random() should be allowed in if() style() condition`); test(() => { const holder = document.createElement('div'); @@ -63,11 +64,177 @@ test(() => { const elComputedValue = getComputedStyle(el).getPropertyValue('--unregistered'); - assert_equals(elComputedValue, 'false'); + assert_equals(elComputedValue, 'true'); } finally { document.body.removeChild(holder); } -}, `random() in var() should not be allowed in if() style() condition`); +}, `random() in var() should be allowed in if() style() condition`); + +test(() => { + const holder = document.createElement('div'); + document.body.appendChild(holder); + try { + let allSame = true; + let allSamePropertyScoped = true; + let allSamePropertyIndexScoped = true; + for(let i = 0; i < 30; i++) { + const prop1 = `--unregistered-${i}`; + const prop2 = `--unregistered2-${i}`; + const prop3 = `--unregistered3-${i}`; + const el = document.createElement('div'); + el.style.setProperty(prop1, 'if(style(random(element-scoped, 0, 1) > 0.5): true; else: false;) if(style(random(element-scoped, 0, 1) > 0.5): true; else: false;)'); + el.style.setProperty(prop2, 'if(style(random(property-scoped, 0, 1) > 0.5): true; else: false;) if(style(random(property-scoped, 0, 1) > 0.5): true; else: false;)'); + el.style.setProperty(prop3, 'if(style(random(property-index-scoped, 0, 1) > 0.5): true; else: false;) if(style(random(property-index-scoped, 0, 1) > 0.5): true; else: false;)'); + holder.appendChild(el); + + const elComputedValue1 = getComputedStyle(el).getPropertyValue(prop1); + let [val11, val12] = elComputedValue1.split(' '); + if (val11 != val12) { + allSame = false; + } + + const elComputedValue2 = getComputedStyle(el).getPropertyValue(prop2); + let [val21, val22] = elComputedValue2.split(' '); + if (val21 != val22) { + allSamePropertyScoped = false; + } + + const elComputedValue3 = getComputedStyle(el).getPropertyValue(prop3); + let [val31, val32] = elComputedValue3.split(' '); + if (val31 != val32) { + allSamePropertyIndexScoped = false; + } + } + assert_equals(allSame, true); + assert_equals(allSamePropertyScoped, true); + assert_equals(allSamePropertyIndexScoped, false); + } finally { + document.body.removeChild(holder); + } +}, `Sharing random() in if() style() condition within same property`); + +test(() => { + const holder = document.createElement('div'); + document.body.appendChild(holder); + try { + let allSame = true; + for(let i = 0; i < 30; i++) { + const prop1 = `--unregistered-${i}`; + const prop2 = `--unregistered2-${i}`; + const el = document.createElement('div'); + el.style.setProperty(prop1, 'if(style(random(element-scoped, 0, 1) > 0.5): true; else: false;)'); + el.style.setProperty(prop2, 'if(style(random(element-scoped, 0, 1) > 0.5): true; else: false;)'); + holder.appendChild(el); + const elComputedValue1 = getComputedStyle(el).getPropertyValue(prop1); + const elComputedValue2 = getComputedStyle(el).getPropertyValue(prop2); + if (elComputedValue1 != elComputedValue2) { + allSame = false; + } + } + assert_equals(allSame, true); + } finally { + document.body.removeChild(holder); + } +}, `Sharing element-scoped random() in if() style() condition across different properties`); + +test(() => { + const holder = document.createElement('div'); + document.body.appendChild(holder); + try { + let allSame = true; + for(let i = 0; i < 30; i++) { + const prop1 = `--unregistered-${i}`; + const prop2 = `--unregistered2-${i}`; + const el = document.createElement('div'); + el.style.setProperty(prop1, 'if(style(random(property-index-scoped, 0, 1) > 0.5): true; else: false;)'); + el.style.setProperty(prop2, 'if(style(random(property-index-scoped, 0, 1) > 0.5): true; else: false;)'); + holder.appendChild(el); + const elComputedValue1 = getComputedStyle(el).getPropertyValue(prop1); + const elComputedValue2 = getComputedStyle(el).getPropertyValue(prop2); + if (elComputedValue1 != elComputedValue2) { + allSame = false; + } + } + assert_equals(allSame, false); + } finally { + document.body.removeChild(holder); + } +}, `Sharing property-index-scoped random() in if() style() condition across different properties`); + +test(() => { + const holder = document.createElement('div'); + document.body.appendChild(holder); + try { + let allSame = true; + for(let i = 0; i < 30; i++) { + const prop = `--unregistered-${i}`; + const el1 = document.createElement('div'); + el1.style.setProperty(prop, 'if(style(random(property-index-scoped, 0, 1) > 0.5): true; else: false;)'); + const el2 = document.createElement('div'); + el2.style.setProperty(prop, 'if(style(random(property-index-scoped, 0, 1) > 0.5): true; else: false;)'); + holder.appendChild(el1); + holder.appendChild(el2); + const elComputedValue1 = getComputedStyle(el1).getPropertyValue(prop); + const elComputedValue2 = getComputedStyle(el2).getPropertyValue(prop); + if (elComputedValue1 != elComputedValue2) { + allSame = false; + } + } + assert_equals(allSame, true); + } finally { + document.body.removeChild(holder); + } +}, `Sharing property-index-scoped random() in if() style() condition across different elements`); + +test(() => { + const holder = document.createElement('div'); + document.body.appendChild(holder); + try { + let allSame = true; + for(let i = 0; i < 30; i++) { + const prop = `--unregistered-${i}`; + const el1 = document.createElement('div'); + el1.style.setProperty(prop, 'if(style(random(property-scoped, 0, 1) > 0.5): true; else: false;)'); + const el2 = document.createElement('div'); + el2.style.setProperty(prop, 'if(style(random(property-scoped, 0, 1) > 0.5): true; else: false;)'); + holder.appendChild(el1); + holder.appendChild(el2); + const elComputedValue1 = getComputedStyle(el1).getPropertyValue(prop); + const elComputedValue2 = getComputedStyle(el2).getPropertyValue(prop); + if (elComputedValue1 != elComputedValue2) { + allSame = false; + } + } + assert_equals(allSame, true); + } finally { + document.body.removeChild(holder); + } +}, `Sharing property-scoped random() in if() style() condition across different elements`); + +test(() => { + const holder = document.createElement('div'); + document.body.appendChild(holder); + try { + let allSame = true; + for(let i = 0; i < 30; i++) { + const prop = `--unregistered-${i}`; + const el1 = document.createElement('div'); + el1.style.setProperty(prop, 'if(style(random(element-scoped, 0, 1) > 0.5): true; else: false;)'); + const el2 = document.createElement('div'); + el2.style.setProperty(prop, 'if(style(random(element-scoped, 0, 1) > 0.5): true; else: false;)'); + holder.appendChild(el1); + holder.appendChild(el2); + const elComputedValue1 = getComputedStyle(el1).getPropertyValue(prop); + const elComputedValue2 = getComputedStyle(el2).getPropertyValue(prop); + if (elComputedValue1 != elComputedValue2) { + allSame = false; + } + } + assert_equals(allSame, false); + } finally { + document.body.removeChild(holder); + } +}, `Sharing element-scoped random() in if() style() condition across different elements`); test(() => { const holder = document.createElement('div'); diff --git a/testing/web-platform/tests/css/css-variables/var-ident-function.html b/testing/web-platform/tests/css/css-variables/var-ident-function.html index 3453cd5b6494..38bcb7aaf43e 100644 --- a/testing/web-platform/tests/css/css-variables/var-ident-function.html +++ b/testing/web-platform/tests/css/css-variables/var-ident-function.html @@ -27,8 +27,8 @@

This text must be green.

diff --git a/testing/web-platform/tests/css/css-variables/variable-reference-name-substitution-attr-taint.html b/testing/web-platform/tests/css/css-variables/variable-reference-name-substitution-attr-taint.html new file mode 100644 index 000000000000..229f272a3f56 --- /dev/null +++ b/testing/web-platform/tests/css/css-variables/variable-reference-name-substitution-attr-taint.html @@ -0,0 +1,92 @@ + + + + attr()-taint propagates through the var() name argument + + + + + + + + +
+ + + diff --git a/testing/web-platform/tests/css/css-variables/variable-reference-name-substitution.html b/testing/web-platform/tests/css/css-variables/variable-reference-name-substitution.html new file mode 100644 index 000000000000..80cab7b01417 --- /dev/null +++ b/testing/web-platform/tests/css/css-variables/variable-reference-name-substitution.html @@ -0,0 +1,158 @@ + + + + var() name argument is an arbitrary substitution value + + + + + + + + +
+ + + diff --git a/testing/web-platform/tests/css/css-variables/variable-reference.html b/testing/web-platform/tests/css/css-variables/variable-reference.html index fb3ae56ebcb3..5bed1ade9352 100644 --- a/testing/web-platform/tests/css/css-variables/variable-reference.html +++ b/testing/web-platform/tests/css/css-variables/variable-reference.html @@ -37,13 +37,13 @@ { cssText: "width: var(--prop,);", expectedPropertyValue: "var(--prop,)" }, { cssText: "width: var();", expectedPropertyValue: "" }, - { cssText: "width: var(prop);", expectedPropertyValue: "" }, - { cssText: "width: var(-prop);", expectedPropertyValue: "" }, - { cssText: "width: var(--prop 20px);", expectedPropertyValue: "" }, - { cssText: "width: var(--prop, var(prop));", expectedPropertyValue: "" }, - { cssText: "width: var(--prop, var(-prop));", expectedPropertyValue: "" }, - { cssText: "width: var(20px);", expectedPropertyValue: "" }, - { cssText: "width: var(var(--prop));", expectedPropertyValue: "" }, + { cssText: "width: var(prop);", expectedPropertyValue: "var(prop)" }, + { cssText: "width: var(-prop);", expectedPropertyValue: "var(-prop)" }, + { cssText: "width: var(--prop 20px);", expectedPropertyValue: "var(--prop 20px)" }, + { cssText: "width: var(--prop, var(prop));", expectedPropertyValue: "var(--prop, var(prop))" }, + { cssText: "width: var(--prop, var(-prop));", expectedPropertyValue: "var(--prop, var(-prop))" }, + { cssText: "width: var(20px);", expectedPropertyValue: "var(20px)" }, + { cssText: "width: var(var(--prop));", expectedPropertyValue: "var(var(--prop))" }, ]; testcases.forEach(function (testcase) { diff --git a/testing/web-platform/tests/css/css-variables/variable-supports-30.html b/testing/web-platform/tests/css/css-variables/variable-supports-30.html index 937b7613e613..a7aa50506137 100644 --- a/testing/web-platform/tests/css/css-variables/variable-supports-30.html +++ b/testing/web-platform/tests/css/css-variables/variable-supports-30.html @@ -3,13 +3,16 @@ http://creativecommons.org/publicdomain/zero/1.0/ --> -CSS Test: Test a passing non-custom property declaration in an @supports rule whose value contains a variable reference with a dimension token as the variable name. +CSS Test: A non-custom property declaration in an @supports rule whose value is a var() reference with a dimension token as the name argument is supported. diff --git a/testing/web-platform/tests/css/css-variables/variable-supports-64.html b/testing/web-platform/tests/css/css-variables/variable-supports-64.html index 2b3eadf24f09..56ace9ea9b3f 100644 --- a/testing/web-platform/tests/css/css-variables/variable-supports-64.html +++ b/testing/web-platform/tests/css/css-variables/variable-supports-64.html @@ -3,13 +3,16 @@ http://creativecommons.org/publicdomain/zero/1.0/ --> -CSS Test: Test a failing custom property declaration in an @supports rule whose value is a variable reference with a dimension token as the variable name. +CSS Test: A custom property declaration in an @supports rule whose value is a var() reference with a dimension token as the name argument is supported. diff --git a/testing/web-platform/tests/css/css-viewport/zoom/text-decoration-thickness.html b/testing/web-platform/tests/css/css-viewport/zoom/text-decoration-thickness.html index 4e47a1209610..81544ad55f50 100644 --- a/testing/web-platform/tests/css/css-viewport/zoom/text-decoration-thickness.html +++ b/testing/web-platform/tests/css/css-viewport/zoom/text-decoration-thickness.html @@ -5,9 +5,12 @@ + + + + + + + + + + + + + + +
+ + + +
+
+ + + + diff --git a/testing/web-platform/tests/html/canvas/element/manual/draw-element-image/privacy/svg-pattern-outside-subtree-ignored.tentative.html b/testing/web-platform/tests/html/canvas/element/manual/draw-element-image/privacy/svg-pattern-outside-subtree-ignored.tentative.html new file mode 100644 index 000000000000..71ab5f6d901f --- /dev/null +++ b/testing/web-platform/tests/html/canvas/element/manual/draw-element-image/privacy/svg-pattern-outside-subtree-ignored.tentative.html @@ -0,0 +1,55 @@ + + + + drawElementImage does not use SVG patterns outside the subtree + + + + + + + + + + + + + + + + + + + + + + + diff --git a/testing/web-platform/tests/html/canvas/element/manual/draw-element-image/privacy/svg-radial-gradient-outside-subtree-ignored.tentative.html b/testing/web-platform/tests/html/canvas/element/manual/draw-element-image/privacy/svg-radial-gradient-outside-subtree-ignored.tentative.html new file mode 100644 index 000000000000..6d540f62a333 --- /dev/null +++ b/testing/web-platform/tests/html/canvas/element/manual/draw-element-image/privacy/svg-radial-gradient-outside-subtree-ignored.tentative.html @@ -0,0 +1,62 @@ + + + + drawElementImage does not use SVG resources from outside the subtree + + + + + + + + + + + + + + + + + + +
+ + + +
+
+ + + + diff --git a/testing/web-platform/tests/html/canvas/element/manual/draw-element-image/privacy/svg-use-outside-subtree-images-ignored.tentative.https.sub.html b/testing/web-platform/tests/html/canvas/element/manual/draw-element-image/privacy/svg-use-outside-subtree-images-ignored.tentative.https.sub.html new file mode 100644 index 000000000000..b54d08232eb3 --- /dev/null +++ b/testing/web-platform/tests/html/canvas/element/manual/draw-element-image/privacy/svg-use-outside-subtree-images-ignored.tentative.https.sub.html @@ -0,0 +1,57 @@ + + + + drawElementImage does not use cross-origin SVG use content from outside the subtree + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/testing/web-platform/tests/html/canvas/element/manual/draw-element-image/privacy/visited-link-svg-use-outside-subtree-ignored.tentative.html b/testing/web-platform/tests/html/canvas/element/manual/draw-element-image/privacy/visited-link-svg-use-outside-subtree-ignored.tentative.html new file mode 100644 index 000000000000..11e148c0c303 --- /dev/null +++ b/testing/web-platform/tests/html/canvas/element/manual/draw-element-image/privacy/visited-link-svg-use-outside-subtree-ignored.tentative.html @@ -0,0 +1,53 @@ + + + + drawElementImage does not leak visited colors in SVG use content from outside the subtree + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/testing/web-platform/tests/html/canvas/element/manual/draw-element-image/privacy/visited-link-color-ignored.tentative.html b/testing/web-platform/tests/html/canvas/element/manual/draw-element-image/privacy/visited-link/visited-link-color-ignored.tentative.html similarity index 100% rename from testing/web-platform/tests/html/canvas/element/manual/draw-element-image/privacy/visited-link-color-ignored.tentative.html rename to testing/web-platform/tests/html/canvas/element/manual/draw-element-image/privacy/visited-link/visited-link-color-ignored.tentative.html diff --git a/testing/web-platform/tests/html/canvas/element/manual/draw-element-image/privacy/visited-link-currentcolor-ignored.tentative.html b/testing/web-platform/tests/html/canvas/element/manual/draw-element-image/privacy/visited-link/visited-link-currentcolor-ignored.tentative.html similarity index 100% rename from testing/web-platform/tests/html/canvas/element/manual/draw-element-image/privacy/visited-link-currentcolor-ignored.tentative.html rename to testing/web-platform/tests/html/canvas/element/manual/draw-element-image/privacy/visited-link/visited-link-currentcolor-ignored.tentative.html diff --git a/testing/web-platform/tests/html/canvas/element/manual/draw-element-image/privacy/visited-link-decoration-color-ignored.tentative.html b/testing/web-platform/tests/html/canvas/element/manual/draw-element-image/privacy/visited-link/visited-link-decoration-color-ignored.tentative.html similarity index 100% rename from testing/web-platform/tests/html/canvas/element/manual/draw-element-image/privacy/visited-link-decoration-color-ignored.tentative.html rename to testing/web-platform/tests/html/canvas/element/manual/draw-element-image/privacy/visited-link/visited-link-decoration-color-ignored.tentative.html diff --git a/testing/web-platform/tests/html/canvas/element/manual/draw-element-image/privacy/visited-link-svg-fill-stroke-color-ignored.tentative.html b/testing/web-platform/tests/html/canvas/element/manual/draw-element-image/privacy/visited-link/visited-link-svg-fill-stroke-color-ignored.tentative.html similarity index 100% rename from testing/web-platform/tests/html/canvas/element/manual/draw-element-image/privacy/visited-link-svg-fill-stroke-color-ignored.tentative.html rename to testing/web-platform/tests/html/canvas/element/manual/draw-element-image/privacy/visited-link/visited-link-svg-fill-stroke-color-ignored.tentative.html diff --git a/testing/web-platform/tests/html/canvas/element/manual/draw-element-image/privacy/visited-link/visited-link-svg-linear-gradiant-ignored.tentative.html b/testing/web-platform/tests/html/canvas/element/manual/draw-element-image/privacy/visited-link/visited-link-svg-linear-gradiant-ignored.tentative.html new file mode 100644 index 000000000000..61a0330e5000 --- /dev/null +++ b/testing/web-platform/tests/html/canvas/element/manual/draw-element-image/privacy/visited-link/visited-link-svg-linear-gradiant-ignored.tentative.html @@ -0,0 +1,52 @@ + + + + drawElementImage does not reveal visited link colors + + + + + + + + + + + + + diff --git a/testing/web-platform/tests/html/canvas/element/manual/draw-element-image/privacy/visited-link/visited-link-svg-pattern-ignored.tentative.html b/testing/web-platform/tests/html/canvas/element/manual/draw-element-image/privacy/visited-link/visited-link-svg-pattern-ignored.tentative.html new file mode 100644 index 000000000000..d78724b4f8f3 --- /dev/null +++ b/testing/web-platform/tests/html/canvas/element/manual/draw-element-image/privacy/visited-link/visited-link-svg-pattern-ignored.tentative.html @@ -0,0 +1,51 @@ + + + + drawElementImage does not reveal visited link colors + + + + + + + + + + + + + diff --git a/testing/web-platform/tests/html/canvas/element/manual/draw-element-image/privacy/visited-link/visited-link-svg-radiant-gradiant-ignored.tentative.html b/testing/web-platform/tests/html/canvas/element/manual/draw-element-image/privacy/visited-link/visited-link-svg-radiant-gradiant-ignored.tentative.html new file mode 100644 index 000000000000..39d9499061f9 --- /dev/null +++ b/testing/web-platform/tests/html/canvas/element/manual/draw-element-image/privacy/visited-link/visited-link-svg-radiant-gradiant-ignored.tentative.html @@ -0,0 +1,52 @@ + + + + drawElementImage does not reveal visited link colors + + + + + + + + + + + + + diff --git a/testing/web-platform/tests/html/semantics/embedded-content/media-elements/mime-types/canPlayType.html b/testing/web-platform/tests/html/semantics/embedded-content/media-elements/mime-types/canPlayType.html index 855f02d3b19e..0ef0d406b54f 100644 --- a/testing/web-platform/tests/html/semantics/embedded-content/media-elements/mime-types/canPlayType.html +++ b/testing/web-platform/tests/html/semantics/embedded-content/media-elements/mime-types/canPlayType.html @@ -118,7 +118,7 @@ function type_codecs_test(type, audioCodecs, videoCodecs) { }, type + ' with and without codecs'); } -type_codecs_test('audio/mp4', ['mp4a.40.2'], []); +type_codecs_test('audio/mp4', ['mp4a.40.2', 'iamf.001.001.Opus'], []); type_codecs_test('audio/ogg', ['opus', 'vorbis'], []); type_codecs_test('audio/wav', ['1'], []); type_codecs_test('audio/webm', ['opus', 'vorbis'], []); diff --git a/testing/web-platform/tests/html/semantics/forms/the-select-element/customizable-select/all-revert-crash.html b/testing/web-platform/tests/html/semantics/forms/the-select-element/customizable-select/all-revert-crash.html new file mode 100644 index 000000000000..509b84ef9c01 --- /dev/null +++ b/testing/web-platform/tests/html/semantics/forms/the-select-element/customizable-select/all-revert-crash.html @@ -0,0 +1,12 @@ + + + + + diff --git a/testing/web-platform/tests/html/semantics/permission-element/camera/camera-element-use-cases.tentative.https.html b/testing/web-platform/tests/html/semantics/permission-element/camera/camera-element-use-cases.https.html similarity index 100% rename from testing/web-platform/tests/html/semantics/permission-element/camera/camera-element-use-cases.tentative.https.html rename to testing/web-platform/tests/html/semantics/permission-element/camera/camera-element-use-cases.https.html diff --git a/testing/web-platform/tests/html/semantics/permission-element/camera/camera-error-scenarios.tentative.https.html b/testing/web-platform/tests/html/semantics/permission-element/camera/camera-error-scenarios.https.html similarity index 100% rename from testing/web-platform/tests/html/semantics/permission-element/camera/camera-error-scenarios.tentative.https.html rename to testing/web-platform/tests/html/semantics/permission-element/camera/camera-error-scenarios.https.html diff --git a/testing/web-platform/tests/html/semantics/permission-element/camera/camera-set-constraints.tentative.https.html b/testing/web-platform/tests/html/semantics/permission-element/camera/camera-set-constraints.https.html similarity index 100% rename from testing/web-platform/tests/html/semantics/permission-element/camera/camera-set-constraints.tentative.https.html rename to testing/web-platform/tests/html/semantics/permission-element/camera/camera-set-constraints.https.html diff --git a/testing/web-platform/tests/html/semantics/permission-element/camera/camera-track-attribute.tentative.https.html b/testing/web-platform/tests/html/semantics/permission-element/camera/camera-track-attribute.https.html similarity index 100% rename from testing/web-platform/tests/html/semantics/permission-element/camera/camera-track-attribute.tentative.https.html rename to testing/web-platform/tests/html/semantics/permission-element/camera/camera-track-attribute.https.html diff --git a/testing/web-platform/tests/html/semantics/permission-element/camera/idlharness.tentative.window.js b/testing/web-platform/tests/html/semantics/permission-element/camera/idlharness.window.js similarity index 100% rename from testing/web-platform/tests/html/semantics/permission-element/camera/idlharness.tentative.window.js rename to testing/web-platform/tests/html/semantics/permission-element/camera/idlharness.window.js diff --git a/testing/web-platform/tests/html/semantics/permission-element/microphone/idlharness.tentative.window.js b/testing/web-platform/tests/html/semantics/permission-element/microphone/idlharness.window.js similarity index 100% rename from testing/web-platform/tests/html/semantics/permission-element/microphone/idlharness.tentative.window.js rename to testing/web-platform/tests/html/semantics/permission-element/microphone/idlharness.window.js diff --git a/testing/web-platform/tests/html/semantics/permission-element/microphone/microphone-element-use-cases.tentative.https.html b/testing/web-platform/tests/html/semantics/permission-element/microphone/microphone-element-use-cases.https.html similarity index 100% rename from testing/web-platform/tests/html/semantics/permission-element/microphone/microphone-element-use-cases.tentative.https.html rename to testing/web-platform/tests/html/semantics/permission-element/microphone/microphone-element-use-cases.https.html diff --git a/testing/web-platform/tests/html/semantics/permission-element/microphone/microphone-error-scenarios.tentative.https.html b/testing/web-platform/tests/html/semantics/permission-element/microphone/microphone-error-scenarios.https.html similarity index 100% rename from testing/web-platform/tests/html/semantics/permission-element/microphone/microphone-error-scenarios.tentative.https.html rename to testing/web-platform/tests/html/semantics/permission-element/microphone/microphone-error-scenarios.https.html diff --git a/testing/web-platform/tests/html/semantics/permission-element/microphone/microphone-set-constraints.tentative.https.html b/testing/web-platform/tests/html/semantics/permission-element/microphone/microphone-set-constraints.https.html similarity index 100% rename from testing/web-platform/tests/html/semantics/permission-element/microphone/microphone-set-constraints.tentative.https.html rename to testing/web-platform/tests/html/semantics/permission-element/microphone/microphone-set-constraints.https.html diff --git a/testing/web-platform/tests/html/semantics/permission-element/microphone/microphone-track-attribute.tentative.https.html b/testing/web-platform/tests/html/semantics/permission-element/microphone/microphone-track-attribute.https.html similarity index 100% rename from testing/web-platform/tests/html/semantics/permission-element/microphone/microphone-track-attribute.tentative.https.html rename to testing/web-platform/tests/html/semantics/permission-element/microphone/microphone-track-attribute.https.html diff --git a/testing/web-platform/tests/html/semantics/permission-element/usermedia/idlharness.tentative.window.js b/testing/web-platform/tests/html/semantics/permission-element/usermedia/idlharness.window.js similarity index 100% rename from testing/web-platform/tests/html/semantics/permission-element/usermedia/idlharness.tentative.window.js rename to testing/web-platform/tests/html/semantics/permission-element/usermedia/idlharness.window.js diff --git a/testing/web-platform/tests/html/semantics/permission-element/usermedia/invalid-css-properties.tentative.html b/testing/web-platform/tests/html/semantics/permission-element/usermedia/invalid-css-properties.html similarity index 100% rename from testing/web-platform/tests/html/semantics/permission-element/usermedia/invalid-css-properties.tentative.html rename to testing/web-platform/tests/html/semantics/permission-element/usermedia/invalid-css-properties.html diff --git a/testing/web-platform/tests/html/semantics/permission-element/usermedia/legacy-mode/legacy-mode.tentative.https.html b/testing/web-platform/tests/html/semantics/permission-element/usermedia/legacy-mode/legacy-mode.https.html similarity index 100% rename from testing/web-platform/tests/html/semantics/permission-element/usermedia/legacy-mode/legacy-mode.tentative.https.html rename to testing/web-platform/tests/html/semantics/permission-element/usermedia/legacy-mode/legacy-mode.https.html diff --git a/testing/web-platform/tests/html/semantics/permission-element/usermedia/no-children-rendered.tentative.html b/testing/web-platform/tests/html/semantics/permission-element/usermedia/no-children-rendered.html similarity index 100% rename from testing/web-platform/tests/html/semantics/permission-element/usermedia/no-children-rendered.tentative.html rename to testing/web-platform/tests/html/semantics/permission-element/usermedia/no-children-rendered.html diff --git a/testing/web-platform/tests/html/semantics/permission-element/usermedia/no-focus.tentative.html b/testing/web-platform/tests/html/semantics/permission-element/usermedia/no-focus.html similarity index 100% rename from testing/web-platform/tests/html/semantics/permission-element/usermedia/no-focus.tentative.html rename to testing/web-platform/tests/html/semantics/permission-element/usermedia/no-focus.html diff --git a/testing/web-platform/tests/html/semantics/permission-element/usermedia/set-constraints-combinations.tentative.https.html b/testing/web-platform/tests/html/semantics/permission-element/usermedia/set-constraints-combinations.https.html similarity index 100% rename from testing/web-platform/tests/html/semantics/permission-element/usermedia/set-constraints-combinations.tentative.https.html rename to testing/web-platform/tests/html/semantics/permission-element/usermedia/set-constraints-combinations.https.html diff --git a/testing/web-platform/tests/html/semantics/permission-element/usermedia/usermedia-cancel-prompt.tentative.https.html b/testing/web-platform/tests/html/semantics/permission-element/usermedia/usermedia-cancel-prompt.https.html similarity index 100% rename from testing/web-platform/tests/html/semantics/permission-element/usermedia/usermedia-cancel-prompt.tentative.https.html rename to testing/web-platform/tests/html/semantics/permission-element/usermedia/usermedia-cancel-prompt.https.html diff --git a/testing/web-platform/tests/html/semantics/permission-element/usermedia/usermedia-iframe.tentative.https.html b/testing/web-platform/tests/html/semantics/permission-element/usermedia/usermedia-iframe.https.html similarity index 100% rename from testing/web-platform/tests/html/semantics/permission-element/usermedia/usermedia-iframe.tentative.https.html rename to testing/web-platform/tests/html/semantics/permission-element/usermedia/usermedia-iframe.https.html diff --git a/testing/web-platform/tests/html/semantics/permission-element/usermedia/usermedia-set-constraints-sanitization.tentative.https.html b/testing/web-platform/tests/html/semantics/permission-element/usermedia/usermedia-set-constraints-sanitization.https.html similarity index 100% rename from testing/web-platform/tests/html/semantics/permission-element/usermedia/usermedia-set-constraints-sanitization.tentative.https.html rename to testing/web-platform/tests/html/semantics/permission-element/usermedia/usermedia-set-constraints-sanitization.https.html diff --git a/testing/web-platform/tests/html/semantics/permission-element/usermedia/usermedia-untrusted-click.tentative.https.html b/testing/web-platform/tests/html/semantics/permission-element/usermedia/usermedia-untrusted-click.https.html similarity index 100% rename from testing/web-platform/tests/html/semantics/permission-element/usermedia/usermedia-untrusted-click.tentative.https.html rename to testing/web-platform/tests/html/semantics/permission-element/usermedia/usermedia-untrusted-click.https.html diff --git a/testing/web-platform/tests/html/semantics/popovers/popover-hint-dialog-dismisses-unrelated-auto.html b/testing/web-platform/tests/html/semantics/popovers/popover-hint-dialog-dismisses-unrelated-auto.html deleted file mode 100644 index 4c8f17dc4272..000000000000 --- a/testing/web-platform/tests/html/semantics/popovers/popover-hint-dialog-dismisses-unrelated-auto.html +++ /dev/null @@ -1,56 +0,0 @@ - - -Showing a dialog nested in a hint popover dismisses unrelated auto popovers - - - - - - -
Hint - Dialog -
-
Unrelated auto
- - diff --git a/testing/web-platform/tests/interfaces/media-source.idl b/testing/web-platform/tests/interfaces/media-source.idl index de153e615a48..b5960c867e4c 100644 --- a/testing/web-platform/tests/interfaces/media-source.idl +++ b/testing/web-platform/tests/interfaces/media-source.idl @@ -53,9 +53,9 @@ interface SourceBuffer : EventTarget { readonly attribute boolean updating; readonly attribute TimeRanges buffered; attribute double timestampOffset; - readonly attribute AudioTrackList audioTracks; - readonly attribute VideoTrackList videoTracks; - readonly attribute TextTrackList textTracks; + [Exposed=Window] readonly attribute AudioTrackList audioTracks; + [Exposed=Window] readonly attribute VideoTrackList videoTracks; + [Exposed=Window] readonly attribute TextTrackList textTracks; attribute double appendWindowStart; attribute unrestricted double appendWindowEnd; diff --git a/testing/web-platform/tests/largest-contentful-paint/text-fragment-union.html b/testing/web-platform/tests/largest-contentful-paint/text-fragment-union.html new file mode 100644 index 000000000000..cf243768e018 --- /dev/null +++ b/testing/web-platform/tests/largest-contentful-paint/text-fragment-union.html @@ -0,0 +1,54 @@ + + +Largest Contentful Paint: text fragment union across line wraps. + + + + + + diff --git a/testing/web-platform/tests/lint.ignore b/testing/web-platform/tests/lint.ignore index 7fa400b28738..4442f8b152f0 100644 --- a/testing/web-platform/tests/lint.ignore +++ b/testing/web-platform/tests/lint.ignore @@ -445,6 +445,7 @@ SET TIMEOUT: html/webappapis/dynamic-markup-insertion/opening-the-input-stream/c SET TIMEOUT: html/webappapis/timers/* SET TIMEOUT: portals/history/resources/portal-harness.js SET TIMEOUT: requestidlecallback/deadline-after-expired-timer.html +SET TIMEOUT: resource-timing/tentative/initiator-url/module-script-from-document.html SET TIMEOUT: resource-timing/tentative/initiator-url/set-timeout.html SET TIMEOUT: resource-timing/tentative/initiator-url/set-timeout.any.js SET TIMEOUT: resources/* diff --git a/testing/web-platform/tests/long-animation-frame/conditional-measure-time-resolve.html b/testing/web-platform/tests/long-animation-frame/conditional-measure-time-resolve.html new file mode 100644 index 000000000000..c84e88821f55 --- /dev/null +++ b/testing/web-platform/tests/long-animation-frame/conditional-measure-time-resolve.html @@ -0,0 +1,231 @@ + + +Long Animation Frame Timing: conditional measure time resolve + + + + + + +

Long Animation Frame: Conditional Measure Time Resolve

+
+ + diff --git a/testing/web-platform/tests/long-animation-frame/conditional-tracing.html b/testing/web-platform/tests/long-animation-frame/conditional-user-timing-basic.html similarity index 56% rename from testing/web-platform/tests/long-animation-frame/conditional-tracing.html rename to testing/web-platform/tests/long-animation-frame/conditional-user-timing-basic.html index 5e9b1612943e..61532c28de2c 100644 --- a/testing/web-platform/tests/long-animation-frame/conditional-tracing.html +++ b/testing/web-platform/tests/long-animation-frame/conditional-user-timing-basic.html @@ -1,6 +1,6 @@ -Long Animation Frame Timing: basic +Long Animation Frame Timing: conditional user timing basic @@ -15,9 +15,6 @@ setup(() => "userTimingEntries" in PerformanceLongAnimationFrameTiming.prototype, 'conditional user timing for LoAF not implemented')); -function close_enough(actual, expected) { - return actual > expected*0.9 && actual < expected*1.1; -} const segment_duration = very_long_frame_duration / 2; promise_test(async t => { @@ -25,6 +22,7 @@ promise_test(async t => { performance.markConditional("mark0"); busy_wait(segment_duration); performance.markConditional("mark1"); + performance.measureConditional("measure0", "mark0", "mark1"); }, t); assert_not_equals(loaf_entry, "timeout"); @@ -45,6 +43,21 @@ promise_test(async t => { const d1 = mark_st(1) - mark_st(0); assert_true(close_enough(d1, segment_duration)); + const conditional_measure_entries = Array.from(loaf_entry.userTimingEntries) + .filter(entry => entry.entryType === "measure-conditional"); + assert_equals(conditional_measure_entries.length, 1); + assert_equals(conditional_measure_entries[0].name, "measure0"); + // The measure startTime should match the start mark's startTime. + assert_approx_equals(conditional_measure_entries[0].startTime, mark_st(0), 1e-6); + // The measure duration reflects the delay of |segment_duration|. + assert_true(close_enough(conditional_measure_entries[0].duration, segment_duration)); + + // Verify userTimingEntries are sorted by startTime. + const all_entries = Array.from(loaf_entry.userTimingEntries); + for(let i = 0; i < all_entries.length - 1; i++) { + assert_true(all_entries[i].startTime <= all_entries[i + 1].startTime); + } + }, 'conditional user timing from a long busy wait'); promise_test(async t => { @@ -53,6 +66,7 @@ promise_test(async t => { performance.markConditional("mark0"); busy_wait(segment_duration) performance.markConditional("mark1"); + performance.measureConditional("measure0", "mark0", "mark1"); resolve(); })); }, t); @@ -74,6 +88,21 @@ promise_test(async t => { const d1 = mark_st(1) - mark_st(0); assert_true(close_enough(d1, segment_duration)); + const conditional_measure_entries = Array.from(loaf_entry.userTimingEntries) + .filter(entry => entry.entryType === "measure-conditional"); + assert_equals(conditional_measure_entries.length, 1); + assert_equals(conditional_measure_entries[0].name, "measure0"); + // The measure startTime should match the start mark's startTime. + assert_approx_equals(conditional_measure_entries[0].startTime, mark_st(0), 1e-6); + // The measure duration reflects the delay of |segment_duration|. + assert_true(close_enough(conditional_measure_entries[0].duration, segment_duration)); + + // Verify userTimingEntries are sorted by startTime. + const all_entries = Array.from(loaf_entry.userTimingEntries); + for(let i = 0; i < all_entries.length - 1; i++) { + assert_true(all_entries[i].startTime <= all_entries[i + 1].startTime); + } + }, 'conditional user timing from a requestAnimationFrame'); promise_test(async t => { @@ -81,10 +110,12 @@ promise_test(async t => { performance.markConditional("mark0"); busy_wait(segment_duration); performance.markConditional("mark1"); + performance.measureConditional("measure0", "mark0", "mark1"); await new Promise(resolve => requestAnimationFrame(() => { performance.markConditional("mark2"); busy_wait(segment_duration) performance.markConditional("mark3"); + performance.measureConditional("measure1", "mark2", "mark3"); resolve(); })); }, t); @@ -113,6 +144,23 @@ promise_test(async t => { assert_true(close_enough(mark_st(i * 2 + 1) - mark_st(i * 2), segment_duration)); } + const conditional_measure_entries = Array.from(loaf_entry.userTimingEntries) + .filter(entry => entry.entryType === "measure-conditional"); + assert_equals(conditional_measure_entries.length, 2); + for(let i = 0; i < 2; i++) { + assert_equals(conditional_measure_entries[i].name, "measure" + i); + // The measure startTime should match the start mark's startTime. + assert_approx_equals(conditional_measure_entries[i].startTime, mark_st(i * 2), 1e-6); + // The measure duration reflects the delay of |segment_duration|. + assert_true(close_enough(conditional_measure_entries[i].duration, segment_duration)); + } + + // Verify userTimingEntries are sorted by startTime. + const all_entries = Array.from(loaf_entry.userTimingEntries); + for(let i = 0; i < all_entries.length - 1; i++) { + assert_true(all_entries[i].startTime <= all_entries[i + 1].startTime); + } + }, 'conditional user timing from a task and a requestAnimationFrame'); promise_test(async t => { @@ -124,16 +172,21 @@ promise_test(async t => { performance.markConditional("mark0"); busy_wait(segment_duration); performance.markConditional("mark1"); + performance.measureConditional("measure0", "mark0", "mark1"); requestAnimationFrame(() => { performance.markConditional("mark2"); busy_wait(segment_duration) performance.markConditional("mark3"); + performance.measureConditional("measure1", "mark2", "mark3"); }); - new ResizeObserver(() => { + const observer = new ResizeObserver(() => { + observer.disconnect(); performance.markConditional("mark4"); busy_wait(segment_duration); performance.markConditional("mark5"); - }).observe(element); + performance.measureConditional("measure2", "mark4", "mark5"); + }); + observer.observe(element); }, t); @@ -161,6 +214,23 @@ promise_test(async t => { assert_true(close_enough(mark_st( i * 2 + 1) - mark_st(i * 2), segment_duration)); } + const conditional_measure_entries = Array.from(loaf_entry.userTimingEntries) + .filter(entry => entry.entryType === "measure-conditional"); + assert_equals(conditional_measure_entries.length, 3); + for(let i = 0; i < 3; i++) { + assert_equals(conditional_measure_entries[i].name, "measure" + i); + // The measure startTime should match the start mark's startTime. + assert_approx_equals(conditional_measure_entries[i].startTime, mark_st(i * 2), 1e-6); + // The measure duration reflects the delay of |segment_duration|. + assert_true(close_enough(conditional_measure_entries[i].duration, segment_duration)); + } + + // Verify userTimingEntries are sorted by startTime. + const all_entries = Array.from(loaf_entry.userTimingEntries); + for(let i = 0; i < all_entries.length - 1; i++) { + assert_true(all_entries[i].startTime <= all_entries[i + 1].startTime); + } + }, 'conditional user timing from a task, a requestAnimationFrame, and layout change'); diff --git a/testing/web-platform/tests/long-animation-frame/conditional-tracing-buffer-limit.html b/testing/web-platform/tests/long-animation-frame/conditional-user-timing-buffer-limit.html similarity index 51% rename from testing/web-platform/tests/long-animation-frame/conditional-tracing-buffer-limit.html rename to testing/web-platform/tests/long-animation-frame/conditional-user-timing-buffer-limit.html index 78967cf01694..4bc94803187b 100644 --- a/testing/web-platform/tests/long-animation-frame/conditional-tracing-buffer-limit.html +++ b/testing/web-platform/tests/long-animation-frame/conditional-user-timing-buffer-limit.html @@ -40,7 +40,8 @@ promise_test(async t => { performance.markConditional("mark3_" + i); } }); - new ResizeObserver(() => { + const observer = new ResizeObserver(() => { + observer.disconnect(); for (let i = 0; i < 50; i++) { performance.markConditional("mark4_" + i); } @@ -48,7 +49,8 @@ promise_test(async t => { for (let i = 0; i < 50; i++) { performance.markConditional("mark5_" + i); } - }).observe(element); + }); + observer.observe(element); }, t); @@ -58,6 +60,52 @@ promise_test(async t => { .filter(entry => entry.entryType === "mark-conditional"); assert_equals(conditional_mark_entries.length, kConditionalUserTimingBufferSize); -}, 'conditional user timing entries are capped at ' + kConditionalUserTimingBufferSize); +}, 'conditional mark entries are capped at ' + kConditionalUserTimingBufferSize); + +promise_test(async t => { + const loaf_entry = await expect_long_frame(async (t, busy_wait) => { + + const element = document.createElement("div"); + document.body.appendChild(element); + t.add_cleanup(() => element.remove()); + for (let i = 0; i < 30; i++) { + performance.markConditional("mark_repeat"); + } + busy_wait(segment_duration); + for (let i = 0; i < 30; i++) { + performance.markConditional("mark1_" + i); + performance.measureConditional("measure1_" + i, "mark_repeat", "mark1_" + i); + } + requestAnimationFrame(() => { + for (let i = 0; i < 30; i++) { + performance.markConditional("mark2_" + i); + } + busy_wait(segment_duration); + for (let i = 0; i < 30; i++) { + performance.markConditional("mark3_" + i); + performance.measureConditional("measure3_" + i, "mark2_" + i, "mark3_" + i); + } + }); + const observer = new ResizeObserver(() => { + observer.disconnect(); + for (let i = 0; i < 30; i++) { + performance.markConditional("mark4_" + i); + } + busy_wait(segment_duration); + for (let i = 0; i < 30; i++) { + performance.markConditional("mark5_" + i); + performance.measureConditional("measure5_" + i, "mark4_" + i, "mark5_" + i); + } + }); + observer.observe(element); + + }, t); + + assert_not_equals(loaf_entry, "timeout"); + + const all_entries = Array.from(loaf_entry.userTimingEntries); + assert_equals(all_entries.length, kConditionalUserTimingBufferSize); + +}, 'Mixed conditional user timing entries are capped at ' + kConditionalUserTimingBufferSize); diff --git a/testing/web-platform/tests/long-animation-frame/resources/utils.js b/testing/web-platform/tests/long-animation-frame/resources/utils.js index 17fb3d0af78e..7cd3e3f93ec7 100644 --- a/testing/web-platform/tests/long-animation-frame/resources/utils.js +++ b/testing/web-platform/tests/long-animation-frame/resources/utils.js @@ -147,3 +147,9 @@ function test_promise_script(cb, resolve_or_reject, invoker, label) { function test_self_script_block(cb, invoker, type) { test_loaf_script(cb, invoker, type); } + +function close_enough(actual, expected) { + const diff = Math.abs(actual - expected); + const max_abs = Math.max(Math.abs(actual), Math.abs(expected)); + return diff * 10 < max_abs + 1e-9; +} diff --git a/testing/web-platform/tests/media-source/idlharness.window.js b/testing/web-platform/tests/media-source/idlharness.any.js similarity index 79% rename from testing/web-platform/tests/media-source/idlharness.window.js rename to testing/web-platform/tests/media-source/idlharness.any.js index 9300f67fe04f..b2c957d2c8d3 100644 --- a/testing/web-platform/tests/media-source/idlharness.window.js +++ b/testing/web-platform/tests/media-source/idlharness.any.js @@ -1,3 +1,4 @@ +// META: global=window,dedicatedworker // META: script=/resources/WebIDLParser.js // META: script=/resources/idlharness.js // META: timeout=long @@ -10,6 +11,13 @@ idl_test( ['media-source'], ['dom', 'html', 'url'], async idl_array => { + // Setting up a SourceBuffer object in a worker needs a media element on the + // main thread; only add objects in a Window. Interface exposure is checked + // in both scopes. + if (!GLOBAL.isWindow()) { + return; + } + idl_array.add_objects({ MediaSource: ['mediaSource'], SourceBuffer: ['sourceBuffer'], diff --git a/testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-clears-target-when-fragment-does-not-exist.html b/testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-clears-target-when-fragment-does-not-exist.html new file mode 100644 index 000000000000..c3e2cf82957b --- /dev/null +++ b/testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-clears-target-when-fragment-does-not-exist.html @@ -0,0 +1,34 @@ + + + + +
+
+ + diff --git a/testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-clears-target-when-no-fragment.html b/testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-clears-target-when-no-fragment.html new file mode 100644 index 000000000000..234f257f3a97 --- /dev/null +++ b/testing/web-platform/tests/navigation-api/scroll-behavior/manual-scroll-clears-target-when-no-fragment.html @@ -0,0 +1,35 @@ + + + + +
+
+ + diff --git a/testing/web-platform/tests/resource-timing/resources/module-script-appender.js b/testing/web-platform/tests/resource-timing/resources/module-script-appender.js new file mode 100644 index 000000000000..0ae34beac651 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/module-script-appender.js @@ -0,0 +1,9 @@ +// Classic (non-module) script. Defines a helper that loads +// module-script-imported.js by adding a module script element to the document. +function appendModuleScript(label) { + const script = document.createElement('script'); + script.type = 'module'; + script.src = getUrl( + `/resource-timing/resources/module-script-imported.js?label=${label}`); + document.head.appendChild(script); +} diff --git a/testing/web-platform/tests/resource-timing/resources/module-script-imported.js b/testing/web-platform/tests/resource-timing/resources/module-script-imported.js new file mode 100644 index 000000000000..7abe5b97737e --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/module-script-imported.js @@ -0,0 +1 @@ +// To be imported to test initiatorUrl for resourceTiming. diff --git a/testing/web-platform/tests/resource-timing/resources/module-script-importer-classic.js b/testing/web-platform/tests/resource-timing/resources/module-script-importer-classic.js new file mode 100644 index 000000000000..69af55425151 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/module-script-importer-classic.js @@ -0,0 +1,3 @@ +// To be loaded as a classic script. A classic script cannot statically import. +// This script is the initiator of the one imported here. +import('/resource-timing/resources/module-script-imported.js?label=classic-importer-dynamic'); diff --git a/testing/web-platform/tests/resource-timing/resources/module-script-importer-module-dynamic.js b/testing/web-platform/tests/resource-timing/resources/module-script-importer-module-dynamic.js new file mode 100644 index 000000000000..a2834045dd24 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/module-script-importer-module-dynamic.js @@ -0,0 +1,11 @@ +// To be dynamically imported. This script is the initiator of the resources +// loaded here. +import './module-script-imported.js?label=dynamic-module-importer-static'; +import('./module-script-imported.js?label=dynamic-module-importer-dynamic'); + +// Dynamically add a module script element that loads the target. +const script = document.createElement('script'); +script.type = 'module'; +script.src = + '/resource-timing/resources/module-script-imported.js?label=dynamic-module-importer-add-script'; +document.head.appendChild(script); diff --git a/testing/web-platform/tests/resource-timing/resources/module-script-importer-module-static.js b/testing/web-platform/tests/resource-timing/resources/module-script-importer-module-static.js new file mode 100644 index 000000000000..bf5ba1db718a --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/module-script-importer-module-static.js @@ -0,0 +1,11 @@ +// To be statically imported. This script is the initiator of the resources +// loaded here. +import './module-script-imported.js?label=static-module-importer-static'; +import('./module-script-imported.js?label=static-module-importer-dynamic'); + +// Dynamically add a module script element that loads the target. +const script = document.createElement('script'); +script.type = 'module'; +script.src = + '/resource-timing/resources/module-script-imported.js?label=static-module-importer-add-script'; +document.head.appendChild(script); diff --git a/testing/web-platform/tests/resource-timing/resources/module-script-worker.js b/testing/web-platform/tests/resource-timing/resources/module-script-worker.js new file mode 100644 index 000000000000..0714be072f94 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/resources/module-script-worker.js @@ -0,0 +1,27 @@ +// Module worker importer. It imports the same target module both statically (at +// instantiation) and dynamically (while running); this worker script is the +// initiator of each import. The static import is observed in the owner +// document's timeline; the dynamic import is observed here and posted back. +import './module-script-imported.js?label=worker-importer-static'; + +const dynamicLabel = 'worker-importer-dynamic'; +const dynamicResource = './module-script-imported.js?label=' + dynamicLabel; + +const observe_entry_no_timeout = entryName => new Promise(resolve => { + new PerformanceObserver((list, observer) => { + for (const entry of list.getEntries()) { + if (entry.name.endsWith(entryName)) { + resolve(entry); + observer.disconnect(); + return; + } + } + }).observe({type: 'resource', buffered: true}); +}); + +self.onmessage = async () => { + await import(dynamicResource); + const entry = await observe_entry_no_timeout( + 'module-script-imported.js?label=' + dynamicLabel); + postMessage({result: entry.initiatorUrl, expected: self.location.href}); +}; diff --git a/testing/web-platform/tests/resource-timing/resources/test-initiator.js b/testing/web-platform/tests/resource-timing/resources/test-initiator.js index 6e13a02f21bb..d55c16f82208 100644 --- a/testing/web-platform/tests/resource-timing/resources/test-initiator.js +++ b/testing/web-platform/tests/resource-timing/resources/test-initiator.js @@ -1,4 +1,10 @@ +// Returns the absolute URL for |path| (which must start with '/') in the +// current origin. Requires /common/get-host-info.sub.js to be loaded first. +// TODO(crbug.com/40919714): Use this in the other tests under +// tentative/initiator-url/. +const getUrl = path => get_host_info()['ORIGIN'] + path; + const with_timeout_message = async (promise, message, timeout = 1000) => { return Promise.race([ promise, diff --git a/testing/web-platform/tests/resource-timing/tentative/initiator-url/module-script-from-document.html b/testing/web-platform/tests/resource-timing/tentative/initiator-url/module-script-from-document.html new file mode 100644 index 000000000000..0582aff8fc37 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/tentative/initiator-url/module-script-from-document.html @@ -0,0 +1,51 @@ + + +Resource Timing - initiatorUrl for module scripts requested by the document + + + + + + + + + + + + + + + + diff --git a/testing/web-platform/tests/resource-timing/tentative/initiator-url/module-script-from-dynamic-importer.html b/testing/web-platform/tests/resource-timing/tentative/initiator-url/module-script-from-dynamic-importer.html new file mode 100644 index 000000000000..b43ec1e9236a --- /dev/null +++ b/testing/web-platform/tests/resource-timing/tentative/initiator-url/module-script-from-dynamic-importer.html @@ -0,0 +1,26 @@ + + +Resource Timing - initiatorUrl for module scripts requested by a dynamically loaded importer + + + + + + diff --git a/testing/web-platform/tests/resource-timing/tentative/initiator-url/module-script-from-static-importer.html b/testing/web-platform/tests/resource-timing/tentative/initiator-url/module-script-from-static-importer.html new file mode 100644 index 000000000000..e47a1d4d2c2e --- /dev/null +++ b/testing/web-platform/tests/resource-timing/tentative/initiator-url/module-script-from-static-importer.html @@ -0,0 +1,34 @@ + + +Resource Timing - initiatorUrl for module scripts requested by a statically loaded importer + + + + + + + + + + + diff --git a/testing/web-platform/tests/resource-timing/tentative/initiator-url/module-script-from-worker.html b/testing/web-platform/tests/resource-timing/tentative/initiator-url/module-script-from-worker.html new file mode 100644 index 000000000000..3e66dc524216 --- /dev/null +++ b/testing/web-platform/tests/resource-timing/tentative/initiator-url/module-script-from-worker.html @@ -0,0 +1,34 @@ + + +Resource Timing - initiatorUrl for module scripts requested by a worker + + + + + + diff --git a/testing/web-platform/tests/service-workers/service-worker/resource-timing-cross-origin-server-timing.https.html b/testing/web-platform/tests/service-workers/service-worker/resource-timing-cross-origin-server-timing.https.html index 5dda0876fb88..0b53b5d35cf3 100644 --- a/testing/web-platform/tests/service-workers/service-worker/resource-timing-cross-origin-server-timing.https.html +++ b/testing/web-platform/tests/service-workers/service-worker/resource-timing-cross-origin-server-timing.https.html @@ -73,16 +73,25 @@ promise_test(async t => { description: 'Cross-origin CORS response without TAO' }); - // 3. Cross-origin CORS response with TAO: filtered response (kCors), timing allow check fails for client. + // 3. Cross-origin CORS response with TAO: filtered response (kCors), timing allow check passes for client. const remote_cors_tao = `${host_info.HTTPS_REMOTE_ORIGIN}${base_path()}resources/server-timing.py?cors=1&tao=1`; await test_fetch({ target: remote_cors_tao, mode: 'cors', - expect_timing_allow: false, + expect_timing_allow: true, description: 'Cross-origin CORS response with TAO' }); - // 4. Synthetic response created inside Service Worker: synthetic response (kDefault), timing allow check passes. + // 4. Cross-origin opaque response (no-cors) with TAO: filtered response (kOpaque), timing allow check passes for client. + const remote_opaque_tao = `${host_info.HTTPS_REMOTE_ORIGIN}${base_path()}resources/server-timing.py?tao=1`; + await test_fetch({ + target: remote_opaque_tao, + mode: 'no-cors', + expect_timing_allow: true, + description: 'Cross-origin opaque response (no-cors) with TAO' + }); + + // 5. Synthetic response created inside Service Worker: synthetic response (kDefault), timing allow check passes. await test_fetch({ target: 'synthetic', mode: 'cors', @@ -90,7 +99,7 @@ promise_test(async t => { description: 'Synthetic response from ServiceWorker' }); - // 5. Same-origin response (kBasic): timing allow check passes. + // 6. Same-origin response (kBasic): timing allow check passes. const same_origin_url = `${host_info.HTTPS_ORIGIN}${base_path()}resources/server-timing.py`; await test_fetch({ target: same_origin_url, diff --git a/testing/web-platform/tests/service-workers/service-worker/resources/server-timing.py b/testing/web-platform/tests/service-workers/service-worker/resources/server-timing.py index c4ffcc9c1b43..2e5b4bfc7e07 100644 --- a/testing/web-platform/tests/service-workers/service-worker/resources/server-timing.py +++ b/testing/web-platform/tests/service-workers/service-worker/resources/server-timing.py @@ -10,6 +10,8 @@ def main(request, response): headers.append((b"Timing-Allow-Origin", b"*")) if b"cors" in request.GET: headers.append((b"Access-Control-Allow-Origin", b"*")) + headers.append((b"Access-Control-Expose-Headers", + b"Server-Timing, Timing-Allow-Origin")) # 1x1 transparent PNG png_data = decodebytes( diff --git a/testing/web-platform/tests/svg/struct/scripted/currentScale-outermost.html b/testing/web-platform/tests/svg/struct/scripted/currentScale-outermost.html new file mode 100644 index 000000000000..477b10fcd58b --- /dev/null +++ b/testing/web-platform/tests/svg/struct/scripted/currentScale-outermost.html @@ -0,0 +1,43 @@ + + +SVGSVGElement.currentScale on outermost vs non-outermost svg + + + + + + + + + +
+ +
+
+
+ + + + + diff --git a/testing/web-platform/tests/svg/types/scripted/SVGAnimatedEnumeration-initial-values.html b/testing/web-platform/tests/svg/types/scripted/SVGAnimatedEnumeration-initial-values.html index e577aa32a355..3cdbf69c94cd 100644 --- a/testing/web-platform/tests/svg/types/scripted/SVGAnimatedEnumeration-initial-values.html +++ b/testing/web-platform/tests/svg/types/scripted/SVGAnimatedEnumeration-initial-values.html @@ -20,6 +20,8 @@ assert_initial_values([ { interface: 'SVGFEDisplacementMapElement', attributes: [ 'xChannelSelector', 'yChannelSelector' ], xChannelSelector: { initial: SVGFEDisplacementMapElement.SVG_CHANNEL_A, valid: 'R' }, yChannelSelector: { initial: SVGFEDisplacementMapElement.SVG_CHANNEL_A, valid: 'G' } }, + { interface: 'SVGFEGaussianBlurElement', attributes: [ 'edgeMode' ], + edgeMode: { initial: SVGFEGaussianBlurElement.SVG_EDGEMODE_NONE, valid: 'wrap' } }, { interface: 'SVGFEMorphologyElement', attributes: [ 'operator' ], operator: { initial: SVGFEMorphologyElement.SVG_MORPHOLOGY_OPERATOR_ERODE, valid: 'dilate' } }, { interface: 'SVGFETurbulenceElement', attributes: [ 'stitchTiles', 'type' ], diff --git a/testing/web-platform/tests/svg/types/scripted/SVGAnimatedEnumeration-invalid-values.html b/testing/web-platform/tests/svg/types/scripted/SVGAnimatedEnumeration-invalid-values.html new file mode 100644 index 000000000000..6d075c6baf84 --- /dev/null +++ b/testing/web-platform/tests/svg/types/scripted/SVGAnimatedEnumeration-invalid-values.html @@ -0,0 +1,107 @@ + +SVGAnimatedEnumeration, invalid values + + + diff --git a/testing/web-platform/tests/svg/types/scripted/SVGAnimatedEnumeration-keywords.html b/testing/web-platform/tests/svg/types/scripted/SVGAnimatedEnumeration-keywords.html new file mode 100644 index 000000000000..af6c22863696 --- /dev/null +++ b/testing/web-platform/tests/svg/types/scripted/SVGAnimatedEnumeration-keywords.html @@ -0,0 +1,175 @@ + +SVGAnimatedEnumeration, keyword values + + + diff --git a/testing/web-platform/tests/tools/ci/ci_wptrunner_infrastructure.sh b/testing/web-platform/tests/tools/ci/ci_wptrunner_infrastructure.sh index 7039ae7412f3..beeabe267f38 100755 --- a/testing/web-platform/tests/tools/ci/ci_wptrunner_infrastructure.sh +++ b/testing/web-platform/tests/tools/ci/ci_wptrunner_infrastructure.sh @@ -11,7 +11,7 @@ CHANNEL="$2" run_infra_test() { echo "### Running Infrastructure Tests for $1 ###" - ./tools/ci/taskcluster-run.py "$1" "$2" -- --log-tbpl=- --log-wptreport="../artifacts/wptreport-$1.json" --logcat-dir="../artifacts/" --metadata=infrastructure/metadata/ --include=infrastructure/ + ./tools/ci/taskcluster-run.py "$1" "$2" -- --log-tbpl=- --log-wptreport=../artifacts/wpt_report.json --logcat-dir="../artifacts/" --metadata=infrastructure/metadata/ --include=infrastructure/ } main() { diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/firefox.py b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/firefox.py index 0c1829d41d6b..d76263310f2e 100644 --- a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/firefox.py +++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/firefox.py @@ -129,6 +129,9 @@ def browser_kwargs(logger, test_type, run_info_data, config, subsuite, **kwargs) "gmp_path": kwargs["gmp_path"] if "gmp_path" in kwargs else None, "debug_test": kwargs["debug_test"]} + if test_type == "aamtest": + browser_kwargs["env"] = {"GNOME_ACCESSIBILITY": "1"} + if test_type in ("wdspec", "aamtest"): browser_kwargs["webdriver_binary"] = kwargs["webdriver_binary"] browser_kwargs["webdriver_args"] = kwargs["webdriver_args"].copy() diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executorchrome.py b/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executorchrome.py index 1898695fb3ff..58422bc92f83 100644 --- a/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executorchrome.py +++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executorchrome.py @@ -15,6 +15,7 @@ from .base import strip_server from .executorwebdriver import ( WebDriverBaseProtocolPart, WebDriverCrashtestExecutor, + WebDriverAccessibilityProtocolPart, WebDriverFedCMProtocolPart, WebDriverPrintRefTestExecutor, WebDriverProtocol, @@ -28,6 +29,8 @@ from .protocol import LeakProtocolPart, ProtocolPart here = os.path.dirname(__file__) +AXNode = Mapping[str, Any] + def _update_capabilities_if_extension_test( browser: Any, capabilities: Optional[MutableMapping[str, Any]] ) -> Optional[MutableMapping[str, Any]]: @@ -191,6 +194,105 @@ class ChromeDriverFedCMProtocolPart(WebDriverFedCMProtocolPart): f"{self.parent.vendor_prefix}/fedcm/confirmidplogin") +class ChromeDriverAccessibilityProtocolPart(WebDriverAccessibilityProtocolPart): + def setup(self): + super().setup() + self._nodes_by_id = {} + + def teardown(self): + try: + self.parent.cdp.execute_cdp_command("Accessibility.disable") + except error.WebDriverException: + pass + + def get_accessibility_properties_for_element(self, element): + node = self._get_ax_node_for_element(element) + return self._serialize_node(node) if node else {} + + def get_accessibility_properties_for_accessibility_node(self, id): + node = self._find_ax_node_by_ax_node_id(id) + return self._serialize_node(node) if node else {} + + def _get_full_ax_tree(self) -> Mapping[str, AXNode]: + self.parent.cdp.execute_cdp_command("Accessibility.enable") + node_array = self.parent.cdp.execute_cdp_command( + "Accessibility.getFullAXTree", + {} + ).get("nodes", []) + + return {node["nodeId"]: node for node in node_array} + + def _find_ax_node_by_ax_node_id(self, ax_node_id: str) -> Optional[AXNode]: + full_ax_tree = self._get_full_ax_tree() + node = full_ax_tree.get(ax_node_id, None) + + return node + + def _get_ax_node_for_element(self, element: Any) -> Optional[AXNode]: + # Parse the ID, then hand it off to the shared helper + parsed_ids = self._extract_chromedriver_ids(element.id) + + if parsed_ids and parsed_ids.get("element"): + return self._get_ax_node_by_backend_node_id(parsed_ids["element"]) + + return None + + def _get_ax_node_by_backend_node_id(self, backend_node_id: str) -> Optional[AXNode]: + """Shared CDP call to fetch an accessibility node by its backend ID.""" + ax_tree = self.parent.cdp.execute_cdp_command( + "Accessibility.getPartialAXTree", + { + "backendNodeId": int(backend_node_id), + "fetchRelatives": False, + } + ) + nodes: list[AXNode] = ax_tree.get("nodes", []) + return nodes[0] if nodes else None + + def _serialize_node(self, node: AXNode) -> Mapping[str, Any]: + # TODO: Define an approach to handle ignored items as this is different + # browsers by browser and might make testing the subtree harder. + + rv: dict[str,Any] = { + "accessibilityId": node["nodeId"], + "children": node.get("childIds", []), + } + + if "parentId" in node: + rv["parent"] = node["parentId"] + + # Parse native fields + if "role" in node: + rv["role"] = node["role"].get("value") + if "name" in node: + rv["label"] = node["name"].get("value") + if "value" in node: + rv["value"] = node["value"].get("value") + if "description" in node: + rv["description"] = node["description"].get("value") + + # We only support a subset of properties for now outside of the native fields. + allowed_properties = {'checked', 'pressed', 'level', + 'multiline', 'orientation', 'required', + 'roledescription', 'selected'} + + for prop in node.get("properties", []): + if prop["name"] in allowed_properties: + rv[prop["name"]] = prop["value"].get("value") + + return rv + + @staticmethod + def _extract_chromedriver_ids(element_id_string: str) -> Optional[Mapping[str, str]]: + """ + Extracts Frame, Document, and Element IDs from a ChromeDriver id. + Expected format: f.[hash].d.[hash].e.[id] + """ + pattern = r"^f\.(?P[^.]+)\.d\.(?P[^.]+)\.e\.(?P.+)$" + match = re.match(pattern, element_id_string) + + return match.groupdict() if match else None + class ChromeDriverDevToolsProtocolPart(ProtocolPart): """A low-level API for sending Chrome DevTools Protocol [0] commands directly to the browser. @@ -244,6 +346,7 @@ class ChromeDriverTracingProtocolPart(ProtocolPart): class ChromeDriverProtocol(WebDriverProtocol): implements = [ + ChromeDriverAccessibilityProtocolPart, ChromeDriverBaseProtocolPart, ChromeDriverDevToolsProtocolPart, ChromeDriverFedCMProtocolPart, @@ -269,6 +372,7 @@ class ChromeDriverProtocol(WebDriverProtocol): class ChromeDriverBidiProtocol(WebDriverBidiProtocol): implements = [ + ChromeDriverAccessibilityProtocolPart, ChromeDriverBaseProtocolPart, ChromeDriverDevToolsProtocolPart, ChromeDriverFedCMProtocolPart, diff --git a/testing/web-platform/tests/wai-aria/scripts/aria-utils.js b/testing/web-platform/tests/wai-aria/scripts/aria-utils.js index 29942f27cfe2..37d88c314273 100644 --- a/testing/web-platform/tests/wai-aria/scripts/aria-utils.js +++ b/testing/web-platform/tests/wai-aria/scripts/aria-utils.js @@ -225,7 +225,7 @@ const AriaUtils = { promise_test(async t => { const actual = await test_driver.get_accessibility_properties_for_element(el); for (const key in expected) { - assert_equals(actual[key], expected[key], `${key}: ${el.outerHTML}`); + assert_equals(String(actual[key]), expected[key], `${key}: ${el.outerHTML}`); } }, testName); } diff --git a/testing/web-platform/tests/webaudio/the-audio-api/rendersizehint-smoke-tests.https.html b/testing/web-platform/tests/webaudio/the-audio-api/rendersizehint-smoke-tests.https.html index 952b679be2c4..d622e23480ba 100644 --- a/testing/web-platform/tests/webaudio/the-audio-api/rendersizehint-smoke-tests.https.html +++ b/testing/web-platform/tests/webaudio/the-audio-api/rendersizehint-smoke-tests.https.html @@ -4,12 +4,12 @@ + + + +
+
+ + + diff --git a/testing/web-platform/tests/webmcp/imperative/executeTool-error-window-onerror.https.html b/testing/web-platform/tests/webmcp/imperative/executeTool-error-window-onerror.https.html index f709215cdee4..a29449f2ce3f 100644 --- a/testing/web-platform/tests/webmcp/imperative/executeTool-error-window-onerror.https.html +++ b/testing/web-platform/tests/webmcp/imperative/executeTool-error-window-onerror.https.html @@ -38,6 +38,39 @@ promise_test(async t => { assert_false(errorFired, 'window.onerror/error event should not be fired'); }, 'Failed tool execution does not trigger window.onerror'); + +promise_test(async t => { + let errorFired = false; + const errorHandler = () => { + errorFired = true; + }; + window.addEventListener('error', errorHandler); + t.add_cleanup(() => { + window.removeEventListener('error', errorHandler); + }); + + // Register a tool that returns a circular object when executed. + await document.modelContext.registerTool({ + name: 'circular_tool', + description: 'A tool that returns a circular object', + execute: () => { + let a = {}; + a['a'] = a; + return a; + } + }); + + const tools = await document.modelContext.getTools(); + const tool = tools.find(t => t.name === 'circular_tool'); + assert_true(!!tool, 'Should find the registered tool'); + + // `executeTool()` should reject since serializing the return value throws a TypeError. + await promise_rejects_dom(t, 'UnknownError', + document.modelContext.executeTool(tool, '{}'), + 'executeTool() rejects when the tool execution returns a circular object'); + + assert_false(errorFired, 'window.onerror/error event should not be fired'); +}, 'Tool execution returning circular object rejects and does not trigger window.onerror'); diff --git a/testing/web-platform/tests/webmcp/imperative/exposedTo-invalid-origins.https.html b/testing/web-platform/tests/webmcp/imperative/exposedTo-invalid-origins.https.html index 5b69a308ed00..80927e046c4e 100644 --- a/testing/web-platform/tests/webmcp/imperative/exposedTo-invalid-origins.https.html +++ b/testing/web-platform/tests/webmcp/imperative/exposedTo-invalid-origins.https.html @@ -52,6 +52,47 @@ promise_test(async t => { execute: () => {} }, { signal, exposedTo: ['about:blank#invalidOrigin']}) ) }, "registerTool() with abort signal reason because signal is processed before `exposedTo`"); + +promise_test(async t => { + const ac1 = new AbortController(); + + // 1. Attempt an invalid tool registration whose promise rejects with SecurityError. + const p1 = document.modelContext.registerTool({ + name: 'target_tool', + description: 'Target tool', + execute: async () => 'callback1' + }, { signal: ac1.signal, exposedTo: ['http://insecure.example'] }); + await promise_rejects_dom(t, 'SecurityError', p1, 'First registration should fail with SecurityError'); + + // 2. Valid tool registration with callback2 under the same tool name. + const ac2 = new AbortController(); + await document.modelContext.registerTool({ + name: 'target_tool', + description: 'Target tool', + execute: async () => 'callback2' + }, { signal: ac2.signal }); + + // 3. Cache the RegisteredTool object. + const [tool] = await document.modelContext.getTools(); + assert_true(!!tool, 'target_tool should be registered'); + + // 4. Abort the signal from the first (rejected) registration. + ac1.abort(); + + // 5. Attempt to register a replacement callback3 under the same name. + // Since ac1.abort() must not unregister target_tool, this duplicate registration must reject with InvalidStateError. + const p3 = document.modelContext.registerTool({ + name: 'target_tool', + description: 'Target tool replacement', + execute: async () => 'callback3' + }); + await promise_rejects_dom(t, 'InvalidStateError', p3, 'Duplicate registration should reject with InvalidStateError'); + + // 6. Execute the cached RegisteredTool. + // This must execute callback2 and resolve to 'callback2', not callback3. + const result = await document.modelContext.executeTool(tool, '{}'); + assert_equals(result, 'callback2', 'Executing the cached RegisteredTool must run callback2'); +}, 'Aborting a signal from a rejected registration must not unregister a later valid tool with the same name'); diff --git a/testing/web-platform/tests/webnn/conformance_tests/resample2d-gather-shape-divergence.https.any.js b/testing/web-platform/tests/webnn/conformance_tests/resample2d-gather-shape-divergence.https.any.js new file mode 100644 index 000000000000..4817825fce1b --- /dev/null +++ b/testing/web-platform/tests/webnn/conformance_tests/resample2d-gather-shape-divergence.https.any.js @@ -0,0 +1,112 @@ +// META: title=test resample2d output shape agrees with the backend across a gather +// META: global=window +// META: variant=?cpu +// META: variant=?gpu +// META: variant=?npu +// META: script=../resources/utils.js +// META: timeout=long + +'use strict'; + +// https://www.w3.org/TR/webnn/#api-mlgraphbuilder-resample2d-method +// https://www.w3.org/TR/webnn/#api-mlgraphbuilder-gather-method +// +// Regression test for a resample2d shape divergence. The output size is +// floor(input size * scale), which WebNN validates in double precision. A +// backend that re-derives it in float32 disagrees above 2^24, where float32 +// cannot represent consecutive integers: for size 2^24 + 1 and scale 1.0 it +// rounds down to 2^24, under-allocating the axis by one element. A downstream +// gather, clamped to WebNN's (larger) shape, then reads out of bounds at the +// last index. +// +// The assertion does not check for a specific value, since backends may sample +// different values near 2^24. Instead it exposes `resampled` as a second output +// and checks that gathering the last index reads the same element as a direct +// read of it. That holds only when the gather stayed in bounds, i.e. when the +// shapes agree; an under-allocated axis clamps the gather elsewhere and breaks +// the equality. + +// 2^24 + 1: the smallest positive integer float32 cannot represent exactly. +// Input byte length is 4 * 16777217 ~= 64 MiB, below the tensor byte length +// limit. +const kResampledDim = 16777217; +const kLastIndex = kResampledDim - 1; // Largest index WebNN admits: 16777216. + +let mlContext; + +promise_setup(async () => { + assert_implements(navigator.ml, 'missing navigator.ml'); + mlContext = await navigator.ml.createContext(contextOptions); +}); + +promise_test(async () => { + const builder = new MLGraphBuilder(mlContext); + + // Rank-4 input with the large dimension on axis 2. + const input = builder.input( + 'input', {dataType: 'float32', shape: [1, 1, kResampledDim, 1]}); + + // Identity resample (scale 1.0). WebNN's validated output shape keeps axis 2 + // at kResampledDim; a float32 re-derivation would shrink it by one. + const resampled = builder.resample2d( + input, {mode: 'nearest-neighbor', scales: [1, 1], axes: [2, 3]}); + assert_equals(resampled.shape[2], kResampledDim, + 'resample2d output preserves the axis-2 dimension'); + + // Gather the last WebNN-valid element along the resampled axis. + const indices = builder.input('indices', {dataType: 'int32', shape: [1]}); + const gathered = builder.gather(resampled, indices, {axis: 2}); + assert_array_equals(gathered.shape, [1, 1, 1, 1], 'gather output shape'); + + const [inputTensor, indicesTensor, resampledTensor, gatheredTensor, mlGraph] = + await Promise.all([ + mlContext.createTensor({ + dataType: 'float32', + shape: [1, 1, kResampledDim, 1], + writable: true, + }), + mlContext.createTensor({dataType: 'int32', shape: [1], writable: true}), + mlContext.createTensor({ + dataType: 'float32', + shape: [1, 1, kResampledDim, 1], + readable: true, + }), + mlContext.createTensor( + {dataType: 'float32', shape: [1, 1, 1, 1], readable: true}), + // Expose `resampled` as an output so its last element can be read back + // and compared against the gather result. + builder.build({'resampled': resampled, 'gathered': gathered}), + ]); + + // Distinct non-zero sentinels at kLastIndex and kLastIndex - 1. On a + // correctly sized backend the gather reads kLastIndex in bounds, so + // gathered[0] equals a direct read of resampled[kLastIndex] regardless of the + // values. If the axis is under-allocated, both the gather and that direct + // read touch an element the backend never produced; the resulting behavior is + // backend-defined (it may throw, clamp, or read adjacent memory). The + // distinct sentinels give a mismatch something to show - e.g. a backend that + // clamps the gather to its last valid index would read a different sentinel + // than the unwritten kLastIndex. This is best-effort: a zero-filled input + // could let both sides read 0 and hide the regression. The exact values are + // never asserted; they only need to differ. + const inputData = new Float32Array(kResampledDim); + inputData[kLastIndex - 1] = 5; + inputData[kLastIndex] = 42; + mlContext.writeTensor(inputTensor, inputData); + mlContext.writeTensor(indicesTensor, new Int32Array([kLastIndex])); + + mlContext.dispatch( + mlGraph, {'input': inputTensor, 'indices': indicesTensor}, + {'resampled': resampledTensor, 'gathered': gatheredTensor}); + + const resampledData = + new Float32Array(await mlContext.readTensor(resampledTensor)); + const gatheredData = + new Float32Array(await mlContext.readTensor(gatheredTensor)); + + assert_equals( + gatheredData[0], resampledData[kLastIndex], + 'gather at the last WebNN-valid index reads the same element as a direct ' + + 'read of that index, proving the resample2d output shape matches the ' + + 'backend allocation (no out-of-bounds clamp)'); +}, 'resample2d output shape (from scales) matches backend allocation at the 2^24 boundary'); diff --git a/testing/web-platform/tests/websockets/constructor/option-bag.any.js b/testing/web-platform/tests/websockets/constructor/option-bag.any.js new file mode 100644 index 000000000000..b8f8168b7cb1 --- /dev/null +++ b/testing/web-platform/tests/websockets/constructor/option-bag.any.js @@ -0,0 +1,26 @@ +// META: title=WebSockets: option bag constructor argument +// META: script=../constants.sub.js +// META: variant=?default +// META: variant=?wss + +async_test(function(t) { + const ws = new WebSocket(SCHEME_DOMAIN_PORT + '/echo', {}); + ws.onopen = t.step_func(function(e) { + ws.close(); + t.done(); + }); + ws.onerror = t.unreached_func("error event should not have fired"); +}, "Empty option bag should be accepted"); + +async_test(function(t) { + const ws = new WebSocket(SCHEME_DOMAIN_PORT + '/protocol_array', { protocols: ['foobar', 'foobar2'] }); + ws.onmessage = t.step_func(function(e) { + assert_equals(ws.protocol, 'foobar'); + assert_equals(e.data, 'foobar', 'message content should be "foobar"'); + ws.onclose = t.step_func(function(e) { + t.done(); + }); + ws.close(); + }); + ws.onerror = t.unreached_func("error event should not have fired"); +}, "Option bag with protocols array should be accepted"); diff --git a/third_party/js/prosemirror/LICENSE b/third_party/js/prosemirror/LICENSE index c34a4f121f66..7e2295b30744 100644 --- a/third_party/js/prosemirror/LICENSE +++ b/third_party/js/prosemirror/LICENSE @@ -1,4 +1,4 @@ -Copyright (C) 2015-2016 by Marijn Haverbeke and others +Copyright (C) 2015-2017 by Marijn Haverbeke and others Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/third_party/libwebrtc/common_video/BUILD.gn b/third_party/libwebrtc/common_video/BUILD.gn index 868d09a2eb1e..eb3cc07f948a 100644 --- a/third_party/libwebrtc/common_video/BUILD.gn +++ b/third_party/libwebrtc/common_video/BUILD.gn @@ -97,6 +97,10 @@ rtc_library("common_video") { "../rtc_base/containers:flat_map", ] } + + if (!is_ios) { + defines = [ "HAVE_LIBYUV_JPEG" ] + } } rtc_source_set("frame_counts") { diff --git a/third_party/libwebrtc/common_video/common_video_gn/moz.build b/third_party/libwebrtc/common_video/common_video_gn/moz.build index 731a68d3323c..16645ce2d192 100644 --- a/third_party/libwebrtc/common_video/common_video_gn/moz.build +++ b/third_party/libwebrtc/common_video/common_video_gn/moz.build @@ -20,6 +20,7 @@ DEFINES["WEBRTC_LIBRARY_IMPL"] = True DEFINES["WEBRTC_MOZILLA_BUILD"] = True DEFINES["WEBRTC_NON_STATIC_TRACE_EVENT_HANDLERS"] = "0" DEFINES["WEBRTC_STRICT_FIELD_TRIALS"] = "0" +DEFINES["HAVE_LIBYUV_JPEG"] = True FINAL_LIBRARY = "xul" diff --git a/third_party/libwebrtc/common_video/libyuv/include/webrtc_libyuv.h b/third_party/libwebrtc/common_video/libyuv/include/webrtc_libyuv.h index b18957b6be68..a1899dba88b1 100644 --- a/third_party/libwebrtc/common_video/libyuv/include/webrtc_libyuv.h +++ b/third_party/libwebrtc/common_video/libyuv/include/webrtc_libyuv.h @@ -84,6 +84,22 @@ int ConvertFromI420(const VideoFrame& src_frame, int dst_sample_size, uint8_t* dst_frame); +int ConvertToI420(const uint8_t* sample, + size_t sample_size, + uint8_t* dst_y, + int dst_stride_y, + uint8_t* dst_u, + int dst_stride_u, + uint8_t* dst_v, + int dst_stride_v, + int src_width, + int src_height, + int src_stride, + int dst_width, + int dst_height, + uint32_t rotation, + uint32_t fourcc); + scoped_refptr ScaleVideoFrameBuffer( const I420BufferInterface& source, int dst_width, diff --git a/third_party/libwebrtc/common_video/libyuv/libyuv_unittest.cc b/third_party/libwebrtc/common_video/libyuv/libyuv_unittest.cc index 6b47ec65ea01..448ace8dfcea 100644 --- a/third_party/libwebrtc/common_video/libyuv/libyuv_unittest.cc +++ b/third_party/libwebrtc/common_video/libyuv/libyuv_unittest.cc @@ -172,12 +172,12 @@ TEST_F(TestLibYuv, ConvertTest) { EXPECT_EQ(0, ConvertFromI420(*orig_frame_, VideoType::kRGB24, 0, res_rgb_buffer2.get())); - ret = libyuv::ConvertToI420( + ret = ConvertToI420( res_rgb_buffer2.get(), 0, res_i420_buffer->MutableDataY(), res_i420_buffer->StrideY(), res_i420_buffer->MutableDataU(), res_i420_buffer->StrideU(), res_i420_buffer->MutableDataV(), - res_i420_buffer->StrideV(), 0, 0, width_, height_, - res_i420_buffer->width(), res_i420_buffer->height(), libyuv::kRotate0, + res_i420_buffer->StrideV(), width_, height_, 0, res_i420_buffer->width(), + res_i420_buffer->height(), static_cast(libyuv::kRotate0), ConvertVideoType(VideoType::kRGB24)); EXPECT_EQ(0, ret); @@ -196,12 +196,12 @@ TEST_F(TestLibYuv, ConvertTest) { EXPECT_EQ(0, ConvertFromI420(*orig_frame_, VideoType::kUYVY, 0, out_uyvy_buffer.get())); - ret = libyuv::ConvertToI420( + ret = ConvertToI420( out_uyvy_buffer.get(), 0, res_i420_buffer->MutableDataY(), res_i420_buffer->StrideY(), res_i420_buffer->MutableDataU(), res_i420_buffer->StrideU(), res_i420_buffer->MutableDataV(), - res_i420_buffer->StrideV(), 0, 0, width_, height_, - res_i420_buffer->width(), res_i420_buffer->height(), libyuv::kRotate0, + res_i420_buffer->StrideV(), width_, height_, 0, res_i420_buffer->width(), + res_i420_buffer->height(), static_cast(libyuv::kRotate0), ConvertVideoType(VideoType::kUYVY)); EXPECT_EQ(0, ret); @@ -218,12 +218,12 @@ TEST_F(TestLibYuv, ConvertTest) { EXPECT_EQ(0, ConvertFromI420(*orig_frame_, VideoType::kYUY2, 0, out_yuy2_buffer.get())); - ret = libyuv::ConvertToI420( + ret = ConvertToI420( out_yuy2_buffer.get(), 0, res_i420_buffer->MutableDataY(), res_i420_buffer->StrideY(), res_i420_buffer->MutableDataU(), res_i420_buffer->StrideU(), res_i420_buffer->MutableDataV(), - res_i420_buffer->StrideV(), 0, 0, width_, height_, - res_i420_buffer->width(), res_i420_buffer->height(), libyuv::kRotate0, + res_i420_buffer->StrideV(), width_, height_, 0, res_i420_buffer->width(), + res_i420_buffer->height(), static_cast(libyuv::kRotate0), ConvertVideoType(VideoType::kYUY2)); EXPECT_EQ(0, ret); @@ -242,12 +242,12 @@ TEST_F(TestLibYuv, ConvertTest) { EXPECT_EQ(0, ConvertFromI420(*orig_frame_, VideoType::kRGB565, 0, out_rgb565_buffer.get())); - ret = libyuv::ConvertToI420( + ret = ConvertToI420( out_rgb565_buffer.get(), 0, res_i420_buffer->MutableDataY(), res_i420_buffer->StrideY(), res_i420_buffer->MutableDataU(), res_i420_buffer->StrideU(), res_i420_buffer->MutableDataV(), - res_i420_buffer->StrideV(), 0, 0, width_, height_, - res_i420_buffer->width(), res_i420_buffer->height(), libyuv::kRotate0, + res_i420_buffer->StrideV(), width_, height_, 0, res_i420_buffer->width(), + res_i420_buffer->height(), static_cast(libyuv::kRotate0), ConvertVideoType(VideoType::kRGB565)); EXPECT_EQ(0, ret); @@ -269,12 +269,12 @@ TEST_F(TestLibYuv, ConvertTest) { EXPECT_EQ(0, ConvertFromI420(*orig_frame_, VideoType::kARGB, 0, out_argb8888_buffer.get())); - ret = libyuv::ConvertToI420( + ret = ConvertToI420( out_argb8888_buffer.get(), 0, res_i420_buffer->MutableDataY(), res_i420_buffer->StrideY(), res_i420_buffer->MutableDataU(), res_i420_buffer->StrideU(), res_i420_buffer->MutableDataV(), - res_i420_buffer->StrideV(), 0, 0, width_, height_, - res_i420_buffer->width(), res_i420_buffer->height(), libyuv::kRotate0, + res_i420_buffer->StrideV(), width_, height_, 0, res_i420_buffer->width(), + res_i420_buffer->height(), static_cast(libyuv::kRotate0), ConvertVideoType(VideoType::kARGB)); EXPECT_EQ(0, ret); diff --git a/third_party/libwebrtc/common_video/libyuv/webrtc_libyuv.cc b/third_party/libwebrtc/common_video/libyuv/webrtc_libyuv.cc index 9dd12b42f223..8527f34163b4 100644 --- a/third_party/libwebrtc/common_video/libyuv/webrtc_libyuv.cc +++ b/third_party/libwebrtc/common_video/libyuv/webrtc_libyuv.cc @@ -10,8 +10,10 @@ #include "common_video/libyuv/include/webrtc_libyuv.h" +#include #include #include +#include #include "api/scoped_refptr.h" #include "api/video/i420_buffer.h" @@ -23,6 +25,7 @@ #include "third_party/libyuv/include/libyuv/convert.h" #include "third_party/libyuv/include/libyuv/convert_from.h" #include "third_party/libyuv/include/libyuv/planar_functions.h" +#include "third_party/libyuv/include/libyuv/rotate.h" #include "third_party/libyuv/include/libyuv/scale.h" #include "third_party/libyuv/include/libyuv/video_common.h" @@ -146,6 +149,323 @@ int ConvertFromI420(const VideoFrame& src_frame, ConvertVideoType(dst_video_type)); } +int ConvertToI420(const uint8_t* sample, + size_t sample_size, + uint8_t* dst_y, + int dst_stride_y, + uint8_t* dst_u, + int dst_stride_u, + uint8_t* dst_v, + int dst_stride_v, + int src_width, + int src_height, + int src_stride, + int dst_width, + int dst_height, + uint32_t rotation, + uint32_t fourcc) { + if (src_height == INT_MIN || dst_height == INT_MIN) { + return -1; + } + + const int abs_src_height = (src_height < 0) ? -src_height : src_height; + const int abs_dst_height = (dst_height < 0) ? -dst_height : dst_height; + + if (!dst_y || !dst_u || !dst_v || !sample || src_width <= 0 || + src_width > INT_MAX / 4 || dst_width <= 0 || src_height == 0 || + dst_height == 0 || dst_width > src_width || + abs_dst_height > abs_src_height) { + return -1; + } + + uint32_t format = libyuv::CanonicalFourCC(fourcc); + + // Calculate the unpadded stride if no explicit value is set. + if (src_stride == 0) { + switch (format) { + case libyuv::FOURCC_YUY2: + case libyuv::FOURCC_UYVY: + case libyuv::FOURCC_RGBP: + case libyuv::FOURCC_RGBO: + case libyuv::FOURCC_R444: + src_stride = src_width * 2; + break; + case libyuv::FOURCC_24BG: + case libyuv::FOURCC_RAW: + src_stride = src_width * 3; + break; + case libyuv::FOURCC_ARGB: + case libyuv::FOURCC_BGRA: + case libyuv::FOURCC_ABGR: + case libyuv::FOURCC_RGBA: + src_stride = src_width * 4; + break; + case libyuv::FOURCC_I400: + case libyuv::FOURCC_NV12: + case libyuv::FOURCC_NV21: + case libyuv::FOURCC_I420: + case libyuv::FOURCC_YV12: + case libyuv::FOURCC_I422: + case libyuv::FOURCC_YV16: + // Follow the V4L2 definition for strides of subsampled formats: + // > To avoid ambiguities drivers must return a bytesperline value + // > rounded up to a multiple of the scale factor. + // https://www.kernel.org/doc/html/v7.1/userspace-api/media/v4l/pixfmt-v4l2.html + src_stride = (src_width + 1) & ~1; + break; + case libyuv::FOURCC_I444: + case libyuv::FOURCC_YV24: + src_stride = src_width; + break; + case libyuv::FOURCC_MJPG: + break; + default: + return -1; + } + } else { + switch (format) { + case libyuv::FOURCC_YUY2: + case libyuv::FOURCC_UYVY: + case libyuv::FOURCC_I400: + case libyuv::FOURCC_NV12: + case libyuv::FOURCC_NV21: + case libyuv::FOURCC_I420: + case libyuv::FOURCC_YV12: + case libyuv::FOURCC_I422: + case libyuv::FOURCC_YV16: + // Follow the V4L2 definition for strides of subsampled formats: + // > To avoid ambiguities drivers must return a bytesperline value + // > rounded up to a multiple of the scale factor. + // https://www.kernel.org/doc/html/v7.1/userspace-api/media/v4l/pixfmt-v4l2.html + if ((src_stride % 2) != 0) + return -1; + break; + default: + break; + } + } + + int r = 0; + const bool need_buf = + (rotation && format != libyuv::FOURCC_I420 && + format != libyuv::FOURCC_NV12 && format != libyuv::FOURCC_NV21 && + format != libyuv::FOURCC_YV12) || + dst_y == sample; + const int inv_dst_height = + (src_height < 0) ? -abs_dst_height : abs_dst_height; + uint8_t* rotate_buffer = NULL; + uint8_t* tmp_y; + uint8_t* tmp_u; + uint8_t* tmp_v; + int tmp_y_stride; + int tmp_u_stride; + int tmp_v_stride; + + // One pass rotation is available for some formats. For the rest, convert + // to I420 (with optional vertical flipping) into a temporary I420 buffer, + // and then rotate the I420 to the final destination buffer. + // For in-place conversion, if destination dst_y is same as source sample, + // also enable temporary buffer. + if (need_buf) { + size_t y_size = (size_t)dst_width * abs_dst_height; + size_t uv_size = (size_t)((dst_width + 1) / 2) * ((abs_dst_height + 1) / 2); + if (uv_size > SIZE_MAX / 2 || y_size > SIZE_MAX - uv_size * 2) { + return -1; // Invalid size. + } + const size_t rotate_buffer_size = y_size + uv_size * 2; + rotate_buffer = new uint8_t[rotate_buffer_size]; + if (!rotate_buffer) { + return 1; // Out of memory runtime error. + } + tmp_y = dst_y; + tmp_u = dst_u; + tmp_v = dst_v; + tmp_y_stride = dst_stride_y; + tmp_u_stride = dst_stride_u; + tmp_v_stride = dst_stride_v; + dst_y = rotate_buffer; + dst_u = dst_y + y_size; + dst_v = dst_u + uv_size; + dst_stride_y = dst_width; + dst_stride_u = dst_stride_v = ((dst_width + 1) / 2); + } + + switch (format) { + // Single plane formats + case libyuv::FOURCC_YUY2: { + r = libyuv::YUY2ToI420(sample, src_stride, dst_y, dst_stride_y, dst_u, + dst_stride_u, dst_v, dst_stride_v, dst_width, + inv_dst_height); + break; + } + case libyuv::FOURCC_UYVY: { + r = libyuv::UYVYToI420(sample, src_stride, dst_y, dst_stride_y, dst_u, + dst_stride_u, dst_v, dst_stride_v, dst_width, + inv_dst_height); + break; + } + case libyuv::FOURCC_RGBP: + r = libyuv::RGB565ToI420(sample, src_stride, dst_y, dst_stride_y, dst_u, + dst_stride_u, dst_v, dst_stride_v, dst_width, + inv_dst_height); + break; + case libyuv::FOURCC_RGBO: + r = libyuv::ARGB1555ToI420(sample, src_stride, dst_y, dst_stride_y, dst_u, + dst_stride_u, dst_v, dst_stride_v, dst_width, + inv_dst_height); + break; + case libyuv::FOURCC_R444: + r = libyuv::ARGB4444ToI420(sample, src_stride, dst_y, dst_stride_y, dst_u, + dst_stride_u, dst_v, dst_stride_v, dst_width, + inv_dst_height); + break; + case libyuv::FOURCC_24BG: + r = libyuv::RGB24ToI420(sample, src_stride, dst_y, dst_stride_y, dst_u, + dst_stride_u, dst_v, dst_stride_v, dst_width, + inv_dst_height); + break; + case libyuv::FOURCC_RAW: + r = libyuv::RAWToI420(sample, src_stride, dst_y, dst_stride_y, dst_u, + dst_stride_u, dst_v, dst_stride_v, dst_width, + inv_dst_height); + break; + case libyuv::FOURCC_ARGB: + r = libyuv::ARGBToI420(sample, src_stride, dst_y, dst_stride_y, dst_u, + dst_stride_u, dst_v, dst_stride_v, dst_width, + inv_dst_height); + break; + case libyuv::FOURCC_BGRA: + r = libyuv::BGRAToI420(sample, src_stride, dst_y, dst_stride_y, dst_u, + dst_stride_u, dst_v, dst_stride_v, dst_width, + inv_dst_height); + break; + case libyuv::FOURCC_ABGR: + r = libyuv::ABGRToI420(sample, src_stride, dst_y, dst_stride_y, dst_u, + dst_stride_u, dst_v, dst_stride_v, dst_width, + inv_dst_height); + break; + case libyuv::FOURCC_RGBA: + r = libyuv::RGBAToI420(sample, src_stride, dst_y, dst_stride_y, dst_u, + dst_stride_u, dst_v, dst_stride_v, dst_width, + inv_dst_height); + break; + case libyuv::FOURCC_I400: + r = libyuv::I400ToI420(sample, src_stride, dst_y, dst_stride_y, dst_u, + dst_stride_u, dst_v, dst_stride_v, dst_width, + inv_dst_height); + break; + // Biplanar formats + case libyuv::FOURCC_NV12: { + const uint8_t* src_y = sample; + const uint8_t* src_uv = src_y + ((ptrdiff_t)src_stride * abs_src_height); + r = libyuv::NV12ToI420Rotate(src_y, src_stride, src_uv, src_stride, dst_y, + dst_stride_y, dst_u, dst_stride_u, dst_v, + dst_stride_v, dst_width, inv_dst_height, + (libyuv::RotationMode)rotation); + break; + } + case libyuv::FOURCC_NV21: { + const uint8_t* src_y = sample; + const uint8_t* src_uv = src_y + ((ptrdiff_t)src_stride * abs_src_height); + // Call NV12 but with dst_u and dst_v parameters swapped. + r = libyuv::NV12ToI420Rotate(src_y, src_stride, src_uv, src_stride, dst_y, + dst_stride_y, dst_v, dst_stride_v, dst_u, + dst_stride_u, dst_width, inv_dst_height, + (libyuv::RotationMode)rotation); + break; + } + // Triplanar formats + case libyuv::FOURCC_I420: + case libyuv::FOURCC_YV12: { + const uint8_t* src_y = sample; + const uint8_t* src_u; + const uint8_t* src_v; + // Follow the V4L2 definition: + // > When the image format is planar the bytesperline value applies to the + // > first plane and is divided by the same factor as the width field for + // > the other planes. + // https://www.kernel.org/doc/html/v7.1/userspace-api/media/v4l/pixfmt-v4l2.html + int halfstride = src_stride / 2; + int halfheight = (abs_src_height + 1) / 2; + if (format == libyuv::FOURCC_YV12) { + src_v = src_y + (ptrdiff_t)src_stride * abs_src_height; + src_u = src_v + halfstride * (ptrdiff_t)halfheight; + } else { + src_u = src_y + (ptrdiff_t)src_stride * abs_src_height; + src_v = src_u + halfstride * (ptrdiff_t)halfheight; + } + r = libyuv::I420Rotate(src_y, src_stride, src_u, halfstride, src_v, + halfstride, dst_y, dst_stride_y, dst_u, + dst_stride_u, dst_v, dst_stride_v, dst_width, + inv_dst_height, (libyuv::RotationMode)rotation); + break; + } + case libyuv::FOURCC_I422: + case libyuv::FOURCC_YV16: { + const uint8_t* src_y = sample; + const uint8_t* src_u; + const uint8_t* src_v; + // Follow the V4L2 definition: + // > When the image format is planar the bytesperline value applies to the + // > first plane and is divided by the same factor as the width field for + // > the other planes. + // https://www.kernel.org/doc/html/v7.1/userspace-api/media/v4l/pixfmt-v4l2.html + int halfstride = src_stride / 2; + if (format == libyuv::FOURCC_YV16) { + src_v = src_y + (ptrdiff_t)src_stride * abs_src_height; + src_u = src_v + halfstride * (ptrdiff_t)abs_src_height; + } else { + src_u = src_y + (ptrdiff_t)src_stride * abs_src_height; + src_v = src_u + halfstride * (ptrdiff_t)abs_src_height; + } + r = libyuv::I422ToI420(src_y, src_stride, src_u, halfstride, src_v, + halfstride, dst_y, dst_stride_y, dst_u, + dst_stride_u, dst_v, dst_stride_v, dst_width, + inv_dst_height); + break; + } + case libyuv::FOURCC_I444: + case libyuv::FOURCC_YV24: { + const uint8_t* src_y = sample; + const uint8_t* src_u; + const uint8_t* src_v; + if (format == libyuv::FOURCC_YV24) { + src_v = src_y + src_stride * (ptrdiff_t)abs_src_height; + src_u = src_v + src_stride * (ptrdiff_t)abs_src_height; + } else { + src_u = src_y + src_stride * (ptrdiff_t)abs_src_height; + src_v = src_u + src_stride * (ptrdiff_t)abs_src_height; + } + r = libyuv::I444ToI420(src_y, src_stride, src_u, src_stride, src_v, + src_stride, dst_y, dst_stride_y, dst_u, + dst_stride_u, dst_v, dst_stride_v, dst_width, + inv_dst_height); + break; + } +#ifdef HAVE_LIBYUV_JPEG + case libyuv::FOURCC_MJPG: + r = libyuv::MJPGToI420(sample, sample_size, dst_y, dst_stride_y, dst_u, + dst_stride_u, dst_v, dst_stride_v, src_width, + abs_src_height, dst_width, inv_dst_height); + break; +#endif + default: + r = -1; // unknown fourcc - return failure code. + } + + if (need_buf) { + if (!r) { + r = libyuv::I420Rotate(dst_y, dst_stride_y, dst_u, dst_stride_u, dst_v, + dst_stride_v, tmp_y, tmp_y_stride, tmp_u, + tmp_u_stride, tmp_v, tmp_v_stride, dst_width, + abs_dst_height, (libyuv::RotationMode)rotation); + } + free(rotate_buffer); + } + + return r; +} + scoped_refptr ScaleI420ABuffer( const I420ABufferInterface& buffer, int target_width, diff --git a/third_party/libwebrtc/modules/video_capture/linux/video_capture_pipewire.cc b/third_party/libwebrtc/modules/video_capture/linux/video_capture_pipewire.cc index 8118f5e2b2a6..5ca96108e9dd 100644 --- a/third_party/libwebrtc/modules/video_capture/linux/video_capture_pipewire.cc +++ b/third_party/libwebrtc/modules/video_capture/linux/video_capture_pipewire.cc @@ -363,36 +363,6 @@ void VideoCaptureModulePipeWire::OnFormatChanged(const struct spa_pod* format) { spa_pod_builder_push_object(&builder, &frame, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers); - if (media_subtype == SPA_MEDIA_SUBTYPE_raw) { - // Enforce stride without padding. - size_t stride; - switch (configured_capability_.videoType) { - case VideoType::kI420: - case VideoType::kNV12: - stride = configured_capability_.width; - break; - case VideoType::kYUY2: - case VideoType::kUYVY: - case VideoType::kRGB565: - stride = configured_capability_.width * 2; - break; - case VideoType::kRGB24: - case VideoType::kBGR24: - stride = configured_capability_.width * 3; - break; - case VideoType::kARGB: - case VideoType::kABGR: - case VideoType::kBGRA: - stride = configured_capability_.width * 4; - break; - default: - RTC_LOG(LS_ERROR) << "Unsupported video format."; - return; - } - spa_pod_builder_add(&builder, SPA_PARAM_BUFFERS_stride, SPA_POD_Int(stride), - 0); - } - const int buffer_types = (1 << SPA_DATA_DmaBuf) | (1 << SPA_DATA_MemFd) | (1 << SPA_DATA_MemPtr); spa_pod_builder_add( @@ -471,17 +441,6 @@ void VideoCaptureModulePipeWire::ProcessBuffers() { h = static_cast( spa_buffer_find_meta_data(spaBuffer, SPA_META_Header, sizeof(*h))); - struct spa_meta_videotransform* videotransform; - videotransform = - static_cast(spa_buffer_find_meta_data( - spaBuffer, SPA_META_VideoTransform, sizeof(*videotransform))); - if (videotransform) { - VideoRotation rotation = - VideorotationFromPipeWireTransform(videotransform->transform); - SetCaptureRotation(rotation); - SetApplyRotation(rotation != kVideoRotation_0); - } - if (h->flags & SPA_META_HEADER_FLAG_CORRUPTED) { RTC_LOG(LS_INFO) << "Dropping corruped frame."; pw_stream_queue_buffer(stream_, buffer); @@ -496,6 +455,19 @@ void VideoCaptureModulePipeWire::ProcessBuffers() { continue; } + SetStride(spaBuffer->datas[0].chunk->stride); + + struct spa_meta_videotransform* videotransform; + videotransform = + static_cast(spa_buffer_find_meta_data( + spaBuffer, SPA_META_VideoTransform, sizeof(*videotransform))); + if (videotransform) { + VideoRotation rotation = + VideorotationFromPipeWireTransform(videotransform->transform); + SetCaptureRotation(rotation); + SetApplyRotation(rotation != kVideoRotation_0); + } + if (spaBuffer->datas[0].type == SPA_DATA_DmaBuf || spaBuffer->datas[0].type == SPA_DATA_MemFd) { ScopedBuf frame; diff --git a/third_party/libwebrtc/modules/video_capture/video_capture.h b/third_party/libwebrtc/modules/video_capture/video_capture.h index d88b63466eb2..563f00c3da46 100644 --- a/third_party/libwebrtc/modules/video_capture/video_capture.h +++ b/third_party/libwebrtc/modules/video_capture/video_capture.h @@ -155,6 +155,9 @@ class VideoCaptureModule : public RefCountInterface { // Return whether the rotation is applied or left pending. virtual bool GetApplyRotation() = 0; + virtual void SetStride(int32_t stride) {}; + virtual int32_t GetStride() { return 0; }; + // Mozilla: TrackingId setter for use in profiler markers. virtual void SetTrackingId(uint32_t aTrackingIdProcId) {} diff --git a/third_party/libwebrtc/modules/video_capture/video_capture_impl.cc b/third_party/libwebrtc/modules/video_capture/video_capture_impl.cc index f4f9827ef55a..ed83ab6e57c4 100644 --- a/third_party/libwebrtc/modules/video_capture/video_capture_impl.cc +++ b/third_party/libwebrtc/modules/video_capture/video_capture_impl.cc @@ -31,7 +31,6 @@ #include "rtc_base/time_utils.h" #include "rtc_base/trace_event.h" #include "system_wrappers/include/clock.h" -#include "third_party/libyuv/include/libyuv/convert.h" #include "third_party/libyuv/include/libyuv/rotate.h" namespace webrtc { @@ -94,6 +93,7 @@ VideoCaptureImpl::VideoCaptureImpl(Clock* clock) _lastProcessFrameTimeNanos(clock->TimeInMicroseconds() * 1000), _rotateFrame(kVideoRotation_0), apply_rotation_(false), + stride_(0), clock_(clock) { _requestedCapability.width = kDefaultWidth; _requestedCapability.height = kDefaultHeight; @@ -230,11 +230,11 @@ int32_t VideoCaptureImpl::IncomingFrame(uint8_t* videoFrame, std::swap(dst_width, dst_height); } - const int conversionResult = libyuv::ConvertToI420( + const int conversionResult = ConvertToI420( videoFrame, videoFrameLength, buffer->MutableDataY(), buffer->StrideY(), buffer->MutableDataU(), buffer->StrideU(), buffer->MutableDataV(), - buffer->StrideV(), 0, 0, // No Cropping - width, height, dst_width, dst_height, rotation_mode, + buffer->StrideV(), width, height, stride_, dst_width, dst_height, + static_cast(rotation_mode), ConvertVideoType(frameInfo.videoType)); if (conversionResult != 0) { RTC_LOG(LS_ERROR) << "Failed to convert capture frame from type " @@ -300,6 +300,16 @@ bool VideoCaptureImpl::GetApplyRotation() { return apply_rotation_; } +void VideoCaptureImpl::SetStride(int32_t stride) { + MutexLock lock(&api_lock_); + stride_ = stride; +} + +int32_t VideoCaptureImpl::GetStride() { + MutexLock lock(&api_lock_); + return stride_; +} + void VideoCaptureImpl::UpdateFrameCount() { RTC_CHECK_RUNS_SERIALIZED(&capture_checker_); diff --git a/third_party/libwebrtc/modules/video_capture/video_capture_impl.h b/third_party/libwebrtc/modules/video_capture/video_capture_impl.h index ec404f098d4a..3ad11498b976 100644 --- a/third_party/libwebrtc/modules/video_capture/video_capture_impl.h +++ b/third_party/libwebrtc/modules/video_capture/video_capture_impl.h @@ -70,6 +70,8 @@ class RTC_EXPORT VideoCaptureImpl : public VideoCaptureModule { int32_t SetCaptureRotation(VideoRotation rotation) override; bool SetApplyRotation(bool enable) override; bool GetApplyRotation() override; + void SetStride(int32_t stride) override; + int32_t GetStride() override; const char* CurrentDeviceName() const override; @@ -131,6 +133,10 @@ class RTC_EXPORT VideoCaptureImpl : public VideoCaptureModule { // Indicate whether rotation should be applied before delivered externally. bool apply_rotation_ RTC_GUARDED_BY(api_lock_); + // Explicit input buffer stride. Left to 0 implies implicit stride based on + // format and width. + int32_t stride_ RTC_GUARDED_BY(api_lock_); + Clock* const clock_; }; } // namespace videocapturemodule diff --git a/third_party/libwebrtc/moz-patch-stack/5da5a6e4e0.no-op-cherry-pick-msg b/third_party/libwebrtc/moz-patch-stack/5da5a6e4e0.no-op-cherry-pick-msg new file mode 100644 index 000000000000..d8704cffe6ac --- /dev/null +++ b/third_party/libwebrtc/moz-patch-stack/5da5a6e4e0.no-op-cherry-pick-msg @@ -0,0 +1 @@ +We cherry-picked this in bug 2056029. diff --git a/third_party/llama.cpp/exceptions.patch b/third_party/llama.cpp/exceptions.patch index c41ffb04b37c..d3831da7451e 100644 --- a/third_party/llama.cpp/exceptions.patch +++ b/third_party/llama.cpp/exceptions.patch @@ -207,12 +207,12 @@ diff --git a/src/models/models.h b/src/models/models.h index 19a4d3c5eaf4..2ac8415a3639 100644 --- a/src/models/models.h +++ b/src/models/models.h -@@ -7,8 +7,6 @@ +@@ -6,6 +6,8 @@ + // note: almost all graphs require at least sqrtf, so include cmath globally #include ++ ++#include "moz-overrides.h" --#include "moz-overrides.h" -- // // base classes - // diff --git a/third_party/llama.cpp/include-order.patch b/third_party/llama.cpp/include-order.patch new file mode 100644 index 000000000000..565a7391d235 --- /dev/null +++ b/third_party/llama.cpp/include-order.patch @@ -0,0 +1,175 @@ +diff --git a/src/llama-model.cpp b/src/llama-model.cpp +--- a/src/llama-model.cpp ++++ b/src/llama-model.cpp +@@ -15,8 +15,6 @@ + #include "llama-memory-hybrid-iswa.h" + #include "llama-memory-recurrent.h" + +-#include "models/models.h" +- + #include "ggml.h" + #include "ggml-cpp.h" + +@@ -35,6 +33,8 @@ + #include + #include + ++#include "models/models.h" ++ + static llama_model * llama_model_mapping(llm_arch arch, const llama_model_params & params) { + switch (arch) { + case LLM_ARCH_LLAMA: +diff --git a/src/models/chameleon.cpp b/src/models/chameleon.cpp +--- a/src/models/chameleon.cpp ++++ b/src/models/chameleon.cpp +@@ -1,5 +1,5 @@ ++#include + #include "models.h" +-#include + + void llama_model_chameleon::load_arch_hparams(llama_model_loader & ml) { + ml.get_key(LLM_KV_ATTENTION_LAYERNORM_RMS_EPS, hparams.f_norm_rms_eps); +diff --git a/src/models/deepseek32.cpp b/src/models/deepseek32.cpp +--- a/src/models/deepseek32.cpp ++++ b/src/models/deepseek32.cpp +@@ -1,7 +1,6 @@ +-#include "models.h" +- + #include "llama-kv-cache.h" + #include "llama-kv-cache-dsa.h" ++#include "models.h" + + void llama_model_deepseek32::load_arch_hparams(llama_model_loader & ml) { + ml.get_key(LLM_KV_EXPERT_FEED_FORWARD_LENGTH, hparams.n_ff_exp); +diff --git a/src/models/delta-net-base.cpp b/src/models/delta-net-base.cpp +--- a/src/models/delta-net-base.cpp ++++ b/src/models/delta-net-base.cpp +@@ -1,7 +1,6 @@ +-#include "models.h" +- + #include "llama-impl.h" + #include "llama-memory-recurrent.h" ++#include "models.h" + + // utility to get one slice from the third dimension + // input dim: [x, y, c, b] +diff --git a/src/models/granite.cpp b/src/models/granite.cpp +--- a/src/models/granite.cpp ++++ b/src/models/granite.cpp +@@ -1,7 +1,6 @@ ++#include + #include "models.h" + +-#include +- + void llama_model_granite::load_arch_hparams(llama_model_loader & ml) { + ml.get_key(LLM_KV_ATTENTION_LAYERNORM_RMS_EPS, hparams.f_norm_rms_eps); + ml.get_key(LLM_KV_LOGIT_SCALE, hparams.f_logit_scale); +diff --git a/src/models/kimi-linear.cpp b/src/models/kimi-linear.cpp +--- a/src/models/kimi-linear.cpp ++++ b/src/models/kimi-linear.cpp +@@ -1,5 +1,5 @@ ++#include "llama-memory-recurrent.h" + #include "models.h" +-#include "llama-memory-recurrent.h" + + void llama_model_kimi_linear::load_arch_hparams(llama_model_loader & ml) { + ml.get_key(LLM_KV_ATTENTION_LAYERNORM_RMS_EPS, hparams.f_norm_rms_eps); +diff --git a/src/models/lfm2.cpp b/src/models/lfm2.cpp +--- a/src/models/lfm2.cpp ++++ b/src/models/lfm2.cpp +@@ -1,6 +1,6 @@ +-#include "models.h" + #include "../llama-memory-hybrid-iswa.h" + #include "../llama-memory-hybrid.h" ++#include "models.h" + + void llama_model_lfm2::load_arch_hparams(llama_model_loader & ml) { + ml.get_key(LLM_KV_SHORTCONV_L_CACHE, hparams.n_shortconv_l_cache); +diff --git a/src/models/lfm2moe.cpp b/src/models/lfm2moe.cpp +--- a/src/models/lfm2moe.cpp ++++ b/src/models/lfm2moe.cpp +@@ -1,6 +1,6 @@ +-#include "models.h" + #include "../llama-memory-hybrid-iswa.h" + #include "../llama-memory-hybrid.h" ++#include "models.h" + + void llama_model_lfm2moe::load_arch_hparams(llama_model_loader & ml) { + ml.get_key(LLM_KV_SHORTCONV_L_CACHE, hparams.n_shortconv_l_cache); +diff --git a/src/models/mamba-base.cpp b/src/models/mamba-base.cpp +--- a/src/models/mamba-base.cpp ++++ b/src/models/mamba-base.cpp +@@ -1,7 +1,6 @@ ++#include "llama-memory-recurrent.h" + #include "models.h" + +-#include "llama-memory-recurrent.h" +- + llm_build_mamba_base::llm_build_mamba_base(const llm_graph_params & params) : llm_graph_context(params) {} + + ggml_tensor * llm_build_mamba_base::build_mamba_layer(llm_graph_input_rs * inp, +diff --git a/src/models/plamo2.cpp b/src/models/plamo2.cpp +--- a/src/models/plamo2.cpp ++++ b/src/models/plamo2.cpp +@@ -1,5 +1,5 @@ ++#include "llama-memory-recurrent.h" + #include "models.h" +-#include "llama-memory-recurrent.h" + + void llama_model_plamo2::load_arch_hparams(llama_model_loader & ml) { + ml.get_key(LLM_KV_ATTENTION_LAYERNORM_RMS_EPS, hparams.f_norm_rms_eps); +diff --git a/src/models/qwen35.cpp b/src/models/qwen35.cpp +--- a/src/models/qwen35.cpp ++++ b/src/models/qwen35.cpp +@@ -1,5 +1,5 @@ ++#include "llama-memory-recurrent.h" + #include "models.h" +-#include "llama-memory-recurrent.h" + + void llama_model_qwen35::load_arch_hparams(llama_model_loader & ml) { + ml.get_key(LLM_KV_ATTENTION_LAYERNORM_RMS_EPS, hparams.f_norm_rms_eps); +diff --git a/src/models/qwen35moe.cpp b/src/models/qwen35moe.cpp +--- a/src/models/qwen35moe.cpp ++++ b/src/models/qwen35moe.cpp +@@ -1,5 +1,5 @@ ++#include "llama-memory-recurrent.h" + #include "models.h" +-#include "llama-memory-recurrent.h" + + void llama_model_qwen35moe::load_arch_hparams(llama_model_loader & ml) { + ml.get_key(LLM_KV_EXPERT_FEED_FORWARD_LENGTH, hparams.n_ff_exp, false); +diff --git a/src/models/qwen3next.cpp b/src/models/qwen3next.cpp +--- a/src/models/qwen3next.cpp ++++ b/src/models/qwen3next.cpp +@@ -1,5 +1,5 @@ ++#include "llama-memory-recurrent.h" + #include "models.h" +-#include "llama-memory-recurrent.h" + + void llama_model_qwen3next::load_arch_hparams(llama_model_loader & ml) { + ml.get_key(LLM_KV_EXPERT_FEED_FORWARD_LENGTH, hparams.n_ff_exp, false); +diff --git a/src/models/rwkv6-base.cpp b/src/models/rwkv6-base.cpp +--- a/src/models/rwkv6-base.cpp ++++ b/src/models/rwkv6-base.cpp +@@ -1,7 +1,6 @@ ++#include "llama-memory-recurrent.h" + #include "models.h" + +-#include "llama-memory-recurrent.h" +- + llm_build_rwkv6_base::llm_build_rwkv6_base(const llama_model & model, const llm_graph_params & params) : + llm_graph_context(params), + model(model) {} +diff --git a/src/models/rwkv7-base.cpp b/src/models/rwkv7-base.cpp +--- a/src/models/rwkv7-base.cpp ++++ b/src/models/rwkv7-base.cpp +@@ -1,7 +1,6 @@ ++#include "llama-memory-recurrent.h" + #include "models.h" + +-#include "llama-memory-recurrent.h" +- + llm_build_rwkv7_base::llm_build_rwkv7_base(const llama_model & model, const llm_graph_params & params) : + llm_graph_context(params), + model(model) {} diff --git a/third_party/llama.cpp/moz.yaml b/third_party/llama.cpp/moz.yaml index 9073bb4b0325..fbb3967b32d2 100644 --- a/third_party/llama.cpp/moz.yaml +++ b/third_party/llama.cpp/moz.yaml @@ -2,7 +2,7 @@ schema: 1 bugzilla: product: Core - component: "xul" + component: "Machine Learning: General" origin: name: llama.cpp @@ -89,3 +89,4 @@ vendoring: - profiler-hooks.patch # add thread callbacks for Firefox Profiler integration - missing-includes.patch # missing include cerrno in gguf.cpp, include mutex in llama-model-loader.cpp - skip-metadata-dump.patch # skip dumping metadata strings that proved costly in profiling + - include-order.patch # include system headers before models.h, which pulls in moz-overrides.h diff --git a/third_party/pipewire/00-pipewire-dont-include-conf-header.patch b/third_party/pipewire/00-pipewire-dont-include-conf-header.patch deleted file mode 100644 index e18fc268cdd0..000000000000 --- a/third_party/pipewire/00-pipewire-dont-include-conf-header.patch +++ /dev/null @@ -1,12 +0,0 @@ -diff --git a/pipewire/pipewire.h b/pipewire/pipewire.h -index 0c495ed..870f2f1 100644 ---- a/pipewire/pipewire.h -+++ b/pipewire/pipewire.h -@@ -13,7 +13,6 @@ extern "C" { - - #include - #include --#include - #include - #include - #include diff --git a/third_party/pipewire/moz.yaml b/third_party/pipewire/moz.yaml index c06ebf0bbce8..95054852937a 100644 --- a/third_party/pipewire/moz.yaml +++ b/third_party/pipewire/moz.yaml @@ -46,9 +46,6 @@ vendoring: - 'libpipewire' - 'generate_version.sh' - 'README.md' - patches: - # Don't include to avoid build failures - - '00-pipewire-dont-include-conf-header.patch' update-actions: - action: run-script script: generate_version.sh diff --git a/toolkit/components/antitracking/StoragePrincipalHelper.cpp b/toolkit/components/antitracking/StoragePrincipalHelper.cpp index f9257474b019..7f833ca88f35 100644 --- a/toolkit/components/antitracking/StoragePrincipalHelper.cpp +++ b/toolkit/components/antitracking/StoragePrincipalHelper.cpp @@ -11,7 +11,6 @@ #include "mozilla/extensions/WebExtensionPolicy.h" #include "mozilla/net/CookieJarSettings.h" #include "mozilla/ScopeExit.h" -#include "mozilla/StaticPrefs_privacy.h" #include "mozilla/StorageAccess.h" #include "nsContentUtils.h" #include "nsICookieJarSettings.h" @@ -371,12 +370,6 @@ bool StoragePrincipalHelper::ShouldUsePartitionPrincipalForServiceWorker( nsIDocShell* aDocShell) { MOZ_ASSERT(aDocShell); - // We don't use the partitioned principal for service workers if it's - // disabled. - if (!StaticPrefs::privacy_partition_serviceWorkers()) { - return false; - } - RefPtr document = aDocShell->GetExtantDocument(); // If we cannot get the document from the docShell, we turn to get its @@ -422,12 +415,6 @@ bool StoragePrincipalHelper::ShouldUsePartitionPrincipalForServiceWorker( dom::WorkerPrivate* aWorkerPrivate) { MOZ_ASSERT(aWorkerPrivate); - // We don't use the partitioned principal for service workers if it's - // disabled. - if (!StaticPrefs::privacy_partition_serviceWorkers()) { - return false; - } - nsCOMPtr cookieJarSettings = aWorkerPrivate->CookieJarSettings(); diff --git a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_iframe_initiated_load.js b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_iframe_initiated_load.js index ca7d3b38dc74..3ed7502f38b1 100644 --- a/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_iframe_initiated_load.js +++ b/toolkit/components/antitracking/bouncetrackingprotection/test/browser/browser_bouncetracking_iframe_initiated_load.js @@ -15,6 +15,11 @@ let bounceTrackingProtection = Cc[ "@mozilla.org/bounce-tracking-protection;1" ].getService(Ci.nsIBounceTrackingProtection); +registerCleanupFunction(() => { + // Clear the state after all the tasks. + bounceTrackingProtection.clearAll(); +}); + add_setup(async function () { await SpecialPowers.pushPrefEnv({ set: [ diff --git a/toolkit/components/antitracking/test/browser/browser_partitionedServiceWorkers.js b/toolkit/components/antitracking/test/browser/browser_partitionedServiceWorkers.js index 64ad29f7fdbc..6f8cc0277a23 100644 --- a/toolkit/components/antitracking/test/browser/browser_partitionedServiceWorkers.js +++ b/toolkit/components/antitracking/test/browser/browser_partitionedServiceWorkers.js @@ -1,48 +1,7 @@ /* import-globals-from storageAccessAPIHelpers.js */ PartitionedStorageHelper.runTest( - "ServiceWorkers - disable partitioning", - async (win3rdParty, win1stParty, allowed) => { - // Partitioned serviceWorkers are disabled in third-party context. - await win3rdParty.navigator.serviceWorker.register("empty.js").then( - _ => { - ok( - allowed, - "Success: ServiceWorker cannot be used unless storage access is granted" - ); - }, - _ => { - ok( - !allowed, - "Failed: ServiceWorker cannot be used unless storage access is granted" - ); - } - ); - - await win1stParty.navigator.serviceWorker.register("empty.js").then( - _ => { - ok(true, "Success: ServiceWorker should be available!"); - }, - _ => { - ok(false, "Failed: ServiceWorker should be available!"); - } - ); - }, - - // Cleanup callback - clearSiteTestData, - - [ - ["dom.serviceWorkers.exemptFromPerDomainMax", true], - ["dom.ipc.processCount", 1], - ["dom.serviceWorkers.enabled", true], - ["dom.serviceWorkers.testing.enabled", true], - ["privacy.partition.serviceWorkers", false], - ] -); - -PartitionedStorageHelper.runTest( - "ServiceWorkers - enable partitioning", + "ServiceWorkers - partitioning", async (win3rdParty, win1stParty) => { // Partitioned serviceWorkers are enabled in third-party context. await win3rdParty.navigator.serviceWorker.register("empty.js").then( @@ -78,7 +37,6 @@ PartitionedStorageHelper.runTest( ["dom.ipc.processCount", 1], ["dom.serviceWorkers.enabled", true], ["dom.serviceWorkers.testing.enabled", true], - ["privacy.partition.serviceWorkers", true], ] ); @@ -117,7 +75,6 @@ PartitionedStorageHelper.runTestInNormalAndPrivateMode( ["dom.ipc.processCount", 1], ["dom.serviceWorkers.enabled", true], ["dom.serviceWorkers.testing.enabled", true], - ["privacy.partition.serviceWorkers", true], ] ); @@ -198,7 +155,6 @@ PartitionedStorageHelper.runTestInNormalAndPrivateMode( ["dom.ipc.processCount", 1], ["dom.serviceWorkers.enabled", true], ["dom.serviceWorkers.testing.enabled", true], - ["privacy.partition.serviceWorkers", true], ] ); @@ -297,7 +253,6 @@ PartitionedStorageHelper.runTestInNormalAndPrivateMode( ["dom.ipc.processCount", 1], ["dom.serviceWorkers.enabled", true], ["dom.serviceWorkers.testing.enabled", true], - ["privacy.partition.serviceWorkers", true], ] ); @@ -378,7 +333,6 @@ PartitionedStorageHelper.runTestInNormalAndPrivateMode( ["dom.ipc.processCount", 1], ["dom.serviceWorkers.enabled", true], ["dom.serviceWorkers.testing.enabled", true], - ["privacy.partition.serviceWorkers", true], ] ); @@ -462,7 +416,6 @@ PartitionedStorageHelper.runTestInNormalAndPrivateMode( ["dom.ipc.processCount", 1], ["dom.serviceWorkers.enabled", true], ["dom.serviceWorkers.testing.enabled", true], - ["privacy.partition.serviceWorkers", true], ] ); @@ -536,7 +489,6 @@ PartitionedStorageHelper.runTestInNormalAndPrivateMode( ["dom.ipc.processCount", 1], ["dom.serviceWorkers.enabled", true], ["dom.serviceWorkers.testing.enabled", true], - ["privacy.partition.serviceWorkers", true], ] ); @@ -595,89 +547,11 @@ PartitionedStorageHelper.runTestInNormalAndPrivateMode( ["dom.ipc.processCount", 1], ["dom.serviceWorkers.enabled", true], ["dom.serviceWorkers.testing.enabled", true], - ["privacy.partition.serviceWorkers", true], ] ); PartitionedStorageHelper.runTest( - "ServiceWorkers - Private Browsing with partitioning disabled with SW PBM disabled", - async (win3rdParty, win1stParty) => { - ok( - !win3rdParty.navigator.serviceWorker, - "ServiceWorker should not be available in PBM with SW PBM pref set to false" - ); - ok( - !win1stParty.navigator.serviceWorker, - "ServiceWorker should not be available in PBM with SW PBM pref set to false" - ); - }, - - // Cleanup callback - clearSiteTestData, - - [ - ["dom.serviceWorkers.privateBrowsing.enabled", false], - ["dom.serviceWorkers.exemptFromPerDomainMax", true], - ["dom.ipc.processCount", 1], - ["dom.serviceWorkers.enabled", true], - ["dom.serviceWorkers.testing.enabled", true], - ["privacy.partition.serviceWorkers", false], - ], - - { - runInPrivateWindow: true, - } -); - -PartitionedStorageHelper.runTest( - "ServiceWorkers - Private Browsing with partitioning disabled with SW PBM enabled", - async (win3rdParty, win1stParty, allowed) => { - // Partitioned serviceWorkers are disabled in third-party context. - await win3rdParty.navigator.serviceWorker.register("empty.js").then( - _ => { - ok( - allowed, - `Success: ServiceWorker cannot be used unless storage access is granted (allowed: ${allowed})` - ); - }, - _ => { - ok( - !allowed, - `Success: ServiceWorker cannot be used unless storage access is granted (allowed: ${allowed})` - ); - } - ); - - await win1stParty.navigator.serviceWorker.register("empty.js").then( - _ => { - ok(true, "Success: ServiceWorker should be available!"); - }, - _ => { - ok(false, "Failed: ServiceWorker should be available!"); - } - ); - }, - - // Cleanup callback - clearSiteTestData, - - [ - ["dom.serviceWorkers.privateBrowsing.enabled", true], - ["dom.cache.privateBrowsing.enabled", true], - ["dom.serviceWorkers.exemptFromPerDomainMax", true], - ["dom.ipc.processCount", 1], - ["dom.serviceWorkers.enabled", true], - ["dom.serviceWorkers.testing.enabled", true], - ["privacy.partition.serviceWorkers", false], - ], - - { - runInPrivateWindow: true, - } -); - -PartitionedStorageHelper.runTest( - "ServiceWorkers - Private Browsing with partitioning enabled with SW PBM disabled", + "ServiceWorkers - Private Browsing with SW PBM disabled", async (win3rdParty, win1stParty) => { ok( !win3rdParty.navigator.serviceWorker, @@ -698,7 +572,6 @@ PartitionedStorageHelper.runTest( ["dom.ipc.processCount", 1], ["dom.serviceWorkers.enabled", true], ["dom.serviceWorkers.testing.enabled", true], - ["privacy.partition.serviceWorkers", true], ], { @@ -707,7 +580,7 @@ PartitionedStorageHelper.runTest( ); PartitionedStorageHelper.runTest( - "ServiceWorkers - Private Browsing with partitioning enabled with SW PBM enabled", + "ServiceWorkers - Private Browsing with SW PBM enabled", async (win3rdParty, win1stParty) => { // Partitioned serviceWorkers are enabled in third-party context. await win3rdParty.navigator.serviceWorker.register("empty.js").then( @@ -745,7 +618,6 @@ PartitionedStorageHelper.runTest( ["dom.ipc.processCount", 1], ["dom.serviceWorkers.enabled", true], ["dom.serviceWorkers.testing.enabled", true], - ["privacy.partition.serviceWorkers", true], ], { diff --git a/toolkit/components/doh/test/browser/head.js b/toolkit/components/doh/test/browser/head.js index a92be62e4e7a..12fee75c8be8 100644 --- a/toolkit/components/doh/test/browser/head.js +++ b/toolkit/components/doh/test/browser/head.js @@ -82,6 +82,18 @@ async function setup() { await DoHController._uninit(); await DoHConfigController._uninit(); } catch (e) {} + + // The shipped doh-config dump enables the rollout in the US, which is the + // region test profiles run in, so DoH may have already self-enabled and + // written doh-rollout prefs before the first test in the file runs. Keep + // FIRST_RUN_PREF, which DoHController.init() always sets and which gates + // the rollback that setup() waits for below. + for (let pref of Object.values(prefs)) { + if (pref != prefs.FIRST_RUN_PREF) { + Services.prefs.clearUserPref(pref); + } + } + SpecialPowers.pushPrefEnv({ set: [["security.notification_enable_delay", 0]], }); diff --git a/toolkit/components/enterprisepolicies/EnterprisePoliciesContent.sys.mjs b/toolkit/components/enterprisepolicies/EnterprisePoliciesContent.sys.mjs index 529d43f866e6..04c7a16ad193 100644 --- a/toolkit/components/enterprisepolicies/EnterprisePoliciesContent.sys.mjs +++ b/toolkit/components/enterprisepolicies/EnterprisePoliciesContent.sys.mjs @@ -55,6 +55,10 @@ export class EnterprisePoliciesManagerContent { } isAllowed(feature) { + if (this.status == Ci.nsIEnterprisePolicies.INACTIVE) { + return true; + } + let disallowedFeatures = Services.cpmm.sharedData.get( "EnterprisePolicies:DisallowedFeatures" ); @@ -62,6 +66,10 @@ export class EnterprisePoliciesManagerContent { } isAllowedForURI(feature, uri) { + if (this.status == Ci.nsIEnterprisePolicies.INACTIVE) { + return true; + } + return lazy.SitePolicyUtils.isAllowedForURI( this, this.sitePolicies, diff --git a/toolkit/components/enterprisepolicies/tests/EnterprisePolicyTesting.sys.mjs b/toolkit/components/enterprisepolicies/tests/EnterprisePolicyTesting.sys.mjs index d7687c75461f..e0b85a7276aa 100644 --- a/toolkit/components/enterprisepolicies/tests/EnterprisePolicyTesting.sys.mjs +++ b/toolkit/components/enterprisepolicies/tests/EnterprisePolicyTesting.sys.mjs @@ -128,6 +128,13 @@ export var PoliciesPrefTracker = { ); this._originalFunc = PoliciesUtils.setDefaultPref; PoliciesUtils.setDefaultPref = this.hoistedSetDefaultPref.bind(this); + + // Web serial support is automatically disabled by default by enterprise policies, we want to + // reset that state at the end of the test to avoid the harness complaining about a changed + // preference. + this._webSerialState = Services.prefs + .getDefaultBranch("") + .getBoolPref("dom.webserial.enabled", true); }, stop() { @@ -138,6 +145,10 @@ export var PoliciesPrefTracker = { ); PoliciesUtils.setDefaultPref = this._originalFunc; this._originalFunc = null; + + Services.prefs + .getDefaultBranch("") + .setBoolPref("dom.webserial.enabled", this._webSerialState); }, hoistedSetDefaultPref(prefName, prefValue, locked = false) { diff --git a/toolkit/components/pdfjs/content/build/pdf.mjs b/toolkit/components/pdfjs/content/build/pdf.mjs index af9027c5a585..c4e10a2666ff 100644 --- a/toolkit/components/pdfjs/content/build/pdf.mjs +++ b/toolkit/components/pdfjs/content/build/pdf.mjs @@ -21,8 +21,8 @@ */ /** - * pdfjsVersion = 6.3.72 - * pdfjsBuild = 71a3c6a89 + * pdfjsVersion = 6.3.183 + * pdfjsBuild = 48bb93b89 */ ;// ./src/shared/util.js @@ -538,7 +538,9 @@ class FeatureTest { } class Util { static get hexNums() { - return shadow(this, "hexNums", Array.from(Array(256).keys(), n => n.toString(16).padStart(2, "0"))); + return shadow(this, "hexNums", Array.from({ + length: 256 + }, (_, n) => n.toString(16).padStart(2, "0"))); } static makeHexColor(r, g, b) { return `#${this.hexNums[r]}${this.hexNums[g]}${this.hexNums[b]}`; @@ -2062,7 +2064,7 @@ class FloatingToolbar { } ;// ./src/shared/internal_evt.js -const INTERNAL_EVT = "73d553f8-709f-4713-892b-c46926003d23"; +const INTERNAL_EVT = "df51a2ca-766d-4bd1-bd4e-9faff090d03c"; const internalOpt = Object.freeze({ internal: INTERNAL_EVT }); @@ -4785,6 +4787,15 @@ class Comment { function preventDefault(evt) { evt.preventDefault(); } +const MIN_TOUCH_SPAN = 1e-4; +function stopTouchEvent(evt) { + if (evt.cancelable) { + stopEvent(evt); + return true; + } + evt.stopPropagation(); + return false; +} class TouchManager { #container; #isPinching = false; @@ -4793,8 +4804,11 @@ class TouchManager { #onPinchStart; #onPinching; #onPinchEnd; + #onPanning; + #ownsGesture = false; #pointerDownAC = null; #signal; + #touchIds = new Set(); #touchInfo = null; #touchManagerAC; #touchMoveAC = null; @@ -4805,6 +4819,7 @@ class TouchManager { onPinchStart = null, onPinching = null, onPinchEnd = null, + onPanning = null, signal }) { this.#container = container; @@ -4813,6 +4828,7 @@ class TouchManager { this.#onPinchStart = onPinchStart; this.#onPinching = onPinching; this.#onPinchEnd = onPinchEnd; + this.#onPanning = onPanning; this.#touchManagerAC = new AbortController(); this.#signal = AbortSignal.any([signal, this.#touchManagerAC.signal]); container.addEventListener("touchstart", this.#onTouchStart.bind(this), { @@ -4827,32 +4843,15 @@ class TouchManager { if (this.#isPinchingDisabled?.()) { return; } - if (evt.touches.length === 1) { - if (this.#pointerDownAC) { - return; - } - const pointerDownAC = this.#pointerDownAC = new AbortController(); - const signal = AbortSignal.any([this.#signal, pointerDownAC.signal]); - const container = this.#container; - const opts = { - capture: true, - signal, - passive: false - }; - const cancelPointerDown = e => { - if (e.pointerType === "touch") { - this.#pointerDownAC?.abort(); - this.#pointerDownAC = null; - } - }; - container.addEventListener("pointerdown", e => { - if (e.pointerType === "touch") { - stopEvent(e); - cancelPointerDown(e); - } - }, opts); - container.addEventListener("pointerup", cancelPointerDown, opts); - container.addEventListener("pointercancel", cancelPointerDown, opts); + this.#pruneTouchIds(evt); + const touchIds = this.#touchIds; + for (const { + identifier + } of evt.changedTouches) { + touchIds.add(identifier); + } + if (touchIds.size === 1) { + this.#armPointerDown(); return; } if (!this.#touchMoveAC) { @@ -4875,31 +4874,94 @@ class TouchManager { container.addEventListener("pointerup", preventDefault, opt); this.#onPinchStart?.(); } - stopEvent(evt); - if (evt.touches.length !== 2 || this.#isPinchingStopped?.()) { + this.#ownsGesture = stopTouchEvent(evt); + this.#setTouchInfo(evt); + } + #armPointerDown() { + if (this.#pointerDownAC) { + return; + } + const pointerDownAC = this.#pointerDownAC = new AbortController(); + const signal = AbortSignal.any([this.#signal, pointerDownAC.signal]); + const container = this.#container; + const opts = { + capture: true, + signal, + passive: false + }; + const cancelPointerDown = e => { + if (e.pointerType === "touch") { + this.#pointerDownAC?.abort(); + this.#pointerDownAC = null; + } + }; + container.addEventListener("pointerdown", e => { + if (e.pointerType === "touch") { + stopEvent(e); + cancelPointerDown(e); + } + }, opts); + container.addEventListener("pointerup", cancelPointerDown, opts); + container.addEventListener("pointercancel", cancelPointerDown, opts); + } + #pruneTouchIds(evt) { + const previous = this.#touchIds; + if (previous.size === 0) { + return; + } + const touchIds = this.#touchIds = new Set(); + for (const { + identifier + } of evt.touches) { + if (previous.has(identifier)) { + touchIds.add(identifier); + } + } + } + #getTrackedTouches(evt) { + const touchIds = this.#touchIds; + const touches = []; + for (const touch of evt.touches) { + if (touchIds.has(touch.identifier)) { + touches.push(touch); + } + } + return touches; + } + #setTouchInfo(evt) { + const touches = this.#getTrackedTouches(evt); + if (touches.length !== 2 || this.#isPinchingStopped?.()) { this.#touchInfo = null; return; } - let [touch0, touch1] = evt.touches; - if (touch0.identifier > touch1.identifier) { - [touch0, touch1] = [touch1, touch0]; - } + const [touch0, touch1] = touches; this.#touchInfo = { touch0X: touch0.screenX, touch0Y: touch0.screenY, touch1X: touch1.screenX, - touch1Y: touch1.screenY + touch1Y: touch1.screenY, + panX: (touch0.clientX + touch1.clientX) / 2, + panY: (touch0.clientY + touch1.clientY) / 2 }; } #onTouchMove(evt) { - if (!this.#touchInfo || evt.touches.length !== 2) { + if (!this.#touchInfo) { return; } - stopEvent(evt); - let [touch0, touch1] = evt.touches; - if (touch0.identifier > touch1.identifier) { - [touch0, touch1] = [touch1, touch0]; + const touches = this.#getTrackedTouches(evt); + if (touches.length !== 2) { + return; } + const wasOwned = this.#ownsGesture; + this.#ownsGesture = stopTouchEvent(evt); + if (!this.#ownsGesture) { + return; + } + if (!wasOwned) { + this.#setTouchInfo(evt); + return; + } + const [touch0, touch1] = touches; const { screenX: screen0X, screenY: screen0Y @@ -4913,15 +4975,26 @@ class TouchManager { touch0X: pTouch0X, touch0Y: pTouch0Y, touch1X: pTouch1X, - touch1Y: pTouch1Y + touch1Y: pTouch1Y, + panX: pPanX, + panY: pPanY } = touchInfo; const prevGapX = pTouch1X - pTouch0X; const prevGapY = pTouch1Y - pTouch0Y; const currGapX = screen1X - screen0X; const currGapY = screen1Y - screen0Y; - const distance = Math.hypot(currGapX, currGapY) || 1; - const pDistance = Math.hypot(prevGapX, prevGapY) || 1; - if (!this.#isPinching && Math.abs(pDistance - distance) <= this.MIN_TOUCH_DISTANCE_TO_PINCH) { + const panX = (touch0.clientX + touch1.clientX) / 2; + const panY = (touch0.clientY + touch1.clientY) / 2; + touchInfo.panX = panX; + touchInfo.panY = panY; + const dx = panX - pPanX; + const dy = panY - pPanY; + const distance = Math.hypot(currGapX, currGapY); + const pDistance = Math.hypot(prevGapX, prevGapY); + if (distance < MIN_TOUCH_SPAN || pDistance < MIN_TOUCH_SPAN || !this.#isPinching && Math.abs(pDistance - distance) <= this.MIN_TOUCH_DISTANCE_TO_PINCH) { + if (dx || dy) { + this.#onPanning?.(dx, dy); + } return; } touchInfo.touch0X = screen0X; @@ -4930,28 +5003,41 @@ class TouchManager { touchInfo.touch1Y = screen1Y; if (!this.#isPinching) { this.#isPinching = true; + if (dx || dy) { + this.#onPanning?.(dx, dy); + } return; } - const origin = [(touch0.clientX + touch1.clientX) / 2, (touch0.clientY + touch1.clientY) / 2]; - this.#onPinching?.(origin, pDistance, distance); + this.#onPinching?.([pPanX, pPanY], pDistance, distance, dx, dy); } #onTouchEnd(evt) { - if (evt.touches.length >= 2) { + this.#pruneTouchIds(evt); + if (this.#touchIds.size >= 2) { + this.#setTouchInfo(evt); return; } + const wasTracking = !!this.#touchInfo; + this.#endGesture(); + if (this.#touchIds.size === 1) { + this.#armPointerDown(); + } + if (wasTracking) { + stopTouchEvent(evt); + } + } + #endGesture() { + this.#touchInfo = null; + this.#isPinching = false; + this.#ownsGesture = false; if (this.#touchMoveAC) { this.#touchMoveAC.abort(); this.#touchMoveAC = null; this.#onPinchEnd?.(); } - if (!this.#touchInfo) { - return; - } - stopEvent(evt); - this.#touchInfo = null; - this.#isPinching = false; } destroy() { + this.#endGesture(); + this.#touchIds.clear(); this.#touchManagerAC?.abort(); this.#touchManagerAC = null; this.#pointerDownAC?.abort(); @@ -5297,9 +5383,6 @@ class AnnotationEditor { style.left = `${(100 * x).toFixed(2)}%`; style.top = `${(100 * y).toFixed(2)}%`; this._onTranslating(x, y); - div.scrollIntoView({ - block: "nearest" - }); } _onTranslating(x, y) {} _onTranslated(x, y) {} @@ -6021,6 +6104,9 @@ class AnnotationEditor { this.#prevDragX = x; this.#prevDragY = y; this._uiManager.dragSelectedEditors(tx, ty); + this.div.scrollIntoView({ + block: "nearest" + }); }, opts); window.addEventListener("touchmove", stopEvent, opts); window.addEventListener("pointerdown", e => { @@ -6264,6 +6350,8 @@ class AnnotationEditor { if (!this.isEmpty()) { this.commit(); } + this.#touchManager?.destroy(); + this.#touchManager = null; if (this.parent) { this.parent.remove(this); } else { @@ -6283,8 +6371,6 @@ class AnnotationEditor { this.#telemetryTimeouts = null; } this.parent = null; - this.#touchManager?.destroy(); - this.#touchManager = null; this.#fakeAnnotation?.remove(); this.#fakeAnnotation = null; } @@ -11584,7 +11670,7 @@ class CanvasGraphics { continue; } const spacing = (glyph.isSpace ? wordSpacing : 0) + charSpacing; - const operatorList = font.charProcOperatorList[glyph.operatorListId]; + const operatorList = font.charProcOperatorList.get(glyph.operatorListId); if (!operatorList) { warn(`Type3 character "${glyph.operatorListId}" is not available.`); } else if (this.contentVisible) { @@ -12015,10 +12101,6 @@ class CanvasGraphics { transform = transform.slice(); transform[4] -= rect[0]; transform[5] -= rect[1]; - rect = rect.slice(); - rect[0] = rect[1] = 0; - rect[2] = width; - rect[3] = height; Util.singularValueDecompose2dScale(getCurrentTransform(this.ctx), XY); const { viewportScale @@ -14353,7 +14435,7 @@ function getDocument(src = {}) { } const docParams = { docId, - apiVersion: "6.3.72", + apiVersion: "6.3.183", data, password, disableAutoFetch, @@ -14598,9 +14680,6 @@ class PDFDocumentProxy { getDownloadInfo() { return this._transport.downloadInfoCapability.promise; } - getRawData(data) { - return this._transport.getRawData(data); - } cleanup(keepLoadedFonts = false) { return this._transport.startCleanup(keepLoadedFonts || this.isPureXfa); } @@ -15795,9 +15874,6 @@ class WorkerTransport { getMarkInfo() { return this.messageHandler.sendWithPromise("GetMarkInfo", null); } - getRawData(data) { - return this.messageHandler.sendWithPromise("GetRawData", data); - } async startCleanup(keepLoadedFonts = false) { if (this.destroyed) { return; @@ -16016,8 +16092,8 @@ class InternalRenderTask { } } } -const version = "6.3.72"; -const build = "71a3c6a89"; +const version = "6.3.183"; +const build = "48bb93b89"; ;// ./src/display/editor/color_picker.js @@ -16131,7 +16207,7 @@ class ColorPicker { type: AnnotationEditorParamsType.HIGHLIGHT_COLOR, value: color }); - this.updateColor(color); + this.update(color); } _colorSelectFromKeyboard(event) { if (event.target === this.#button) { @@ -16233,7 +16309,7 @@ class ColorPicker { focusVisible: this.#dropdownWasFromKeyboard }); } - updateColor(color) { + update(color) { if (this.#buttonSwatch) { this.#buttonSwatch.style.backgroundColor = color; } @@ -16961,7 +17037,7 @@ class AnnotationElement { if (!quadPoints) { return; } - const [rectBlX, rectBlY, rectTrX, rectTrY] = this.data.rect.map(x => Math.fround(x)); + const [rectBlX, rectBlY, rectTrX, rectTrY] = this.data.rect.map(Math.fround); if (quadPoints.length === 8) { const [trX, trY, blX, blY] = quadPoints.subarray(2, 6); if (rectTrX === trX && rectTrY === trY && rectBlX === blX && rectBlY === blY) { @@ -20667,10 +20743,859 @@ class FreeTextEditor extends AnnotationEditor { } } +;// ./src/display/editor/draw.js + + + + +class DrawingOptions { + #svgProperties = Object.create(null); + updateProperty(name, value) { + this[name] = value; + this.updateSVGProperty(name, value); + } + updateProperties(properties) { + if (!properties) { + return; + } + for (const [name, value] of Object.entries(properties)) { + if (!name.startsWith("_")) { + this.updateProperty(name, value); + } + } + } + updateSVGProperty(name, value) { + this.#svgProperties[name] = value; + } + toSVGProperties() { + const root = this.#svgProperties; + this.#svgProperties = Object.create(null); + return { + root + }; + } + reset() { + this.#svgProperties = Object.create(null); + } + updateAll(options = this) { + this.updateProperties(options); + } + clone() { + unreachable("Not implemented"); + } +} +class DrawingEditor extends AnnotationEditor { + #internalDiv = null; + #mustBeCommitted; + _clipPathId = null; + _colorPicker = null; + _drawId = null; + _drawOutlines = null; + _focusDrawId = null; + static _currentDrawId = -1; + static _currentParent = null; + static #currentDraw = null; + static #currentDrawingAC = null; + static #currentDrawingOptions = null; + static #currentClipPathId = null; + static _INNER_MARGIN = 3; + constructor(params) { + super(params); + this.#mustBeCommitted = params.mustBeCommitted || false; + this._addOutlines(params); + } + onUpdatedColor() { + this._colorPicker?.update(this.color); + super.onUpdatedColor(); + } + onUpdatedOpacity() { + this._colorPicker?.updateOpacity?.(this.opacity); + } + _addOutlines(params) { + if (params.drawOutlines) { + this.#createDrawOutlines(params); + this.#addToDrawLayer(); + } + } + #createDrawOutlines({ + drawOutlines, + drawId, + drawingOptions, + clipPathId + }) { + this._drawOutlines = drawOutlines; + this._drawingOptions ||= drawingOptions; + if (!this.annotationElementId) { + this._uiManager.a11yAlert(AnnotationEditor._l10nAlert[this.editorType]); + } + if (drawId >= 0) { + this._drawId = drawId; + this._clipPathId = clipPathId ?? null; + this.parent.drawLayer.finalizeDraw(drawId, drawOutlines.defaultProperties); + this.#createFocusOutline(this.parent); + } else { + this._drawId = this.#createDrawing(drawOutlines, this.parent); + } + this.#updateBbox(drawOutlines.box); + } + #createDrawing(drawOutlines, parent) { + const { + id, + clipPathId + } = parent.drawLayer.draw(DrawingEditor._mergeSVGProperties(this._drawingOptions.toSVGProperties(), drawOutlines.defaultSVGProperties), false, this.constructor._hasClipPath); + if (this.constructor._hasClipPath) { + this._clipPathId = clipPathId; + } + this.#createFocusOutline(parent); + return id; + } + #createFocusOutline(parent) { + const properties = this._drawOutlines.getFocusSVGProperties(this.#rotationAngle); + if (properties) { + this._focusDrawId = parent.drawLayer.drawOutline(properties, this._drawOutlines.focusMustRemoveSelfIntersections); + } + } + #updateFocusOutline(angle = this.#rotationAngle) { + if (this._focusDrawId === null) { + return; + } + this.parent?.drawLayer.updateProperties(this._focusDrawId, this._drawOutlines.getFocusSVGProperties(angle)); + } + #toggleFocusOutlineClass(rootClass) { + if (this._focusDrawId !== null) { + this.parent?.drawLayer.updateProperties(this._focusDrawId, { + rootClass + }); + } + } + #updateVisibility() { + const { + parent, + _drawId, + _focusDrawId, + _isVisible + } = this; + if (!parent || _drawId === null) { + return; + } + const rootClass = { + hidden: !_isVisible + }; + parent.drawLayer.updateProperties(_drawId, { + rootClass + }); + if (_focusDrawId !== null) { + parent.drawLayer.updateProperties(_focusDrawId, { + rootClass + }); + } + } + static _mergeSVGProperties(p1, p2) { + const p1Keys = new Set(Object.keys(p1)); + for (const [key, value] of Object.entries(p2)) { + if (p1Keys.has(key)) { + Object.assign(p1[key], value); + } else { + p1[key] = value; + } + } + return p1; + } + static getDefaultDrawingOptions(_options) { + unreachable("Not implemented"); + } + static get typesMap() { + unreachable("Not implemented"); + } + static get isDrawer() { + return true; + } + static get _hasClipPath() { + return false; + } + static get _hasDrawClass() { + return true; + } + static get supportMultipleDrawings() { + return false; + } + get _drawRotation() { + return this.rotation; + } + get _opacityName() { + return this.constructor.typesMap.get(this.opacityType); + } + get #rotationAngle() { + return (this.parentRotation - this._drawRotation + 360) % 360; + } + static updateDefaultParams(type, value) { + const propertyName = this.typesMap.get(type); + if (propertyName) { + this._defaultDrawingOptions.updateProperty(propertyName, value); + } + if (this._currentParent) { + DrawingEditor.#currentDraw.updateProperty(propertyName, value); + this._currentParent.drawLayer.updateProperties(this._currentDrawId, this._defaultDrawingOptions.toSVGProperties()); + } + } + updateParams(type, value) { + const propertyName = this.constructor.typesMap.get(type); + if (propertyName) { + this._updateProperty(type, propertyName, value); + } + } + static get defaultPropertiesToUpdate() { + const properties = []; + const options = this._defaultDrawingOptions; + for (const [type, name] of this.typesMap) { + properties.push([type, options[name]]); + } + return properties; + } + get propertiesToUpdate() { + const properties = []; + const { + _drawingOptions + } = this; + for (const [type, name] of this.constructor.typesMap) { + properties.push([type, _drawingOptions[name]]); + } + return properties; + } + _updateProperty(type, name, value) { + const options = this._drawingOptions; + const savedValue = options[name]; + const setter = val => { + options.updateProperty(name, val); + const bbox = this._drawOutlines.updateProperty(name, val); + if (bbox) { + this.#updateBbox(bbox); + } + this.parent?.drawLayer.updateProperties(this._drawId, options.toSVGProperties()); + if (type === this.colorType) { + this.onUpdatedColor(); + } else if (type === this.opacityType) { + this.onUpdatedOpacity(); + } + }; + this.addCommands({ + cmd: setter.bind(this, value), + undo: setter.bind(this, savedValue), + post: this._uiManager.updateUI.bind(this._uiManager, this), + mustExec: true, + type, + overwriteIfSameType: true, + keepUndo: true + }); + } + _updateColorAndOpacity(color, opacity, type = this.colorAndOpacityType) { + const colorName = this.constructor.typesMap.get(this.colorType); + const opacityName = this._opacityName; + const options = this._drawingOptions; + const savedColor = options[colorName]; + const savedOpacity = options[opacityName]; + const setter = (c, op) => { + options.updateProperty(colorName, c); + options.updateProperty(opacityName, op); + this._drawOutlines.updateProperty(colorName, c); + this._drawOutlines.updateProperty(opacityName, op); + this.parent?.drawLayer.updateProperties(this._drawId, options.toSVGProperties()); + this.onUpdatedColor(); + this.onUpdatedOpacity(); + }; + this.addCommands({ + cmd: setter.bind(this, color, opacity), + undo: setter.bind(this, savedColor, savedOpacity), + post: this._uiManager.updateUI.bind(this._uiManager, this), + mustExec: true, + type, + overwriteIfSameType: true, + keepUndo: true + }); + } + _onResizing() { + this.parent?.drawLayer.updateProperties(this._drawId, DrawingEditor._mergeSVGProperties(this._drawOutlines.getPathResizingSVGProperties(this.#convertToDrawSpace()), { + bbox: this.#rotateBox() + })); + } + _onResized() { + this.parent?.drawLayer.updateProperties(this._drawId, DrawingEditor._mergeSVGProperties(this._drawOutlines.getPathResizedSVGProperties(this.#convertToDrawSpace()), { + bbox: this.#rotateBox() + })); + this.#updateFocusOutline(); + } + _onTranslating(_x, _y) { + this.parent?.drawLayer.updateProperties(this._drawId, { + bbox: this.#rotateBox() + }); + } + _onTranslated() { + this.parent?.drawLayer.updateProperties(this._drawId, DrawingEditor._mergeSVGProperties(this._drawOutlines.getPathTranslatedSVGProperties(this.#convertToDrawSpace(), this.parentDimensions), { + bbox: this.#rotateBox() + })); + } + _onStartDragging() { + this.parent?.drawLayer.updateProperties(this._drawId, { + rootClass: { + moving: true + } + }); + } + _onStopDragging() { + this.parent?.drawLayer.updateProperties(this._drawId, { + rootClass: { + moving: false + } + }); + } + get _mustBeDisabledOnCommit() { + return true; + } + commit() { + super.commit(); + if (this._mustBeDisabledOnCommit) { + this.disableEditMode(); + this.disableEditing(); + } + } + disableEditing() { + super.disableEditing(); + this.div.classList.toggle("disabled", true); + } + enableEditing() { + super.enableEditing(); + this.div.classList.toggle("disabled", false); + } + getBaseTranslation() { + return [0, 0]; + } + get isResizable() { + return true; + } + onceAdded(focus) { + if (!this.annotationElementId) { + this.parent.addUndoableEditor(this); + } + this._isDraggable = true; + if (this.#mustBeCommitted) { + this.#mustBeCommitted = false; + this.commit(); + this.parent.setSelected(this); + if (focus && this.isOnScreen) { + this.div.focus(); + } + } + } + remove() { + this._uiManager.removeShouldRescale(this); + this.#cleanDrawLayer(); + super.remove(); + } + rebuild() { + if (!this.parent) { + return; + } + super.rebuild(); + if (this.div === null) { + return; + } + this.#addToDrawLayer(); + this.#updateBbox(this._drawOutlines.box); + if (!this.isAttachedToDOM) { + this.parent.add(this); + } + } + setParent(parent) { + let mustBeSelected = false; + if (this.parent && !parent) { + this._uiManager.removeShouldRescale(this); + this.#cleanDrawLayer(); + } else if (parent) { + this._uiManager.addShouldRescale(this); + this.#addToDrawLayer(parent); + mustBeSelected = !this.parent && this.div?.classList.contains("selectedEditor"); + } + super.setParent(parent); + this.#updateVisibility(); + if (mustBeSelected) { + this.select(); + } + } + #cleanDrawLayer() { + if (this._drawId === null || !this.parent) { + return; + } + const { + drawLayer + } = this.parent; + drawLayer.remove(this._drawId); + this._drawId = null; + if (this._focusDrawId !== null) { + drawLayer.remove(this._focusDrawId); + this._focusDrawId = null; + } + this._drawingOptions.reset(); + } + #addToDrawLayer(parent = this.parent) { + if (this._drawId !== null && this.parent === parent) { + return; + } + if (this._drawId !== null) { + const { + drawLayer + } = this.parent; + drawLayer.updateParent(this._drawId, parent.drawLayer); + if (this._focusDrawId !== null) { + drawLayer.updateParent(this._focusDrawId, parent.drawLayer); + } + return; + } + this._drawingOptions.updateAll(); + this._drawId = this.#createDrawing(this._drawOutlines, parent); + if (this._clipPathId && this.#internalDiv) { + this.#internalDiv.style.clipPath = this._clipPathId; + } + } + #convertToParentSpace([x, y, width, height]) { + const { + parentDimensions: [pW, pH], + _drawRotation: rotation + } = this; + switch (rotation) { + case 90: + return [y, 1 - x, width * (pH / pW), height * (pW / pH)]; + case 180: + return [1 - x, 1 - y, width, height]; + case 270: + return [1 - y, x, width * (pH / pW), height * (pW / pH)]; + default: + return [x, y, width, height]; + } + } + #convertToDrawSpace() { + const { + x, + y, + width, + height, + parentDimensions: [pW, pH], + _drawRotation: rotation + } = this; + switch (rotation) { + case 90: + return [1 - y, x, width * (pW / pH), height * (pH / pW)]; + case 180: + return [1 - x, 1 - y, width, height]; + case 270: + return [y, 1 - x, width * (pW / pH), height * (pH / pW)]; + default: + return [x, y, width, height]; + } + } + #updateBbox(bbox) { + [this.x, this.y, this.width, this.height] = this.#convertToParentSpace(bbox); + if (this.div) { + this.fixAndSetPosition(); + this.setDims(); + } + this._onResized(); + } + #rotateBox(parentRotation = this.parentRotation) { + const { + x, + y, + width, + height, + _drawRotation: rotation, + parentDimensions: [pW, pH] + } = this; + switch ((rotation * 4 + parentRotation) / 90) { + case 1: + return [1 - y - height, x, height, width]; + case 2: + return [1 - x - width, 1 - y - height, width, height]; + case 3: + return [y, 1 - x - width, height, width]; + case 4: + return [x, y - width * (pW / pH), height * (pH / pW), width * (pW / pH)]; + case 5: + return [1 - y, x, width * (pW / pH), height * (pH / pW)]; + case 6: + return [1 - x - height * (pH / pW), 1 - y, height * (pH / pW), width * (pW / pH)]; + case 7: + return [y - width * (pW / pH), 1 - x - height * (pH / pW), width * (pW / pH), height * (pH / pW)]; + case 8: + return [x - width, y - height, width, height]; + case 9: + return [1 - y, x - width, height, width]; + case 10: + return [1 - x, 1 - y, width, height]; + case 11: + return [y - height, 1 - x, height, width]; + case 12: + return [x - height * (pH / pW), y, height * (pH / pW), width * (pW / pH)]; + case 13: + return [1 - y - width * (pW / pH), x - height * (pH / pW), width * (pW / pH), height * (pH / pW)]; + case 14: + return [1 - x, 1 - y - width * (pW / pH), height * (pH / pW), width * (pW / pH)]; + case 15: + return [y, 1 - x, width * (pW / pH), height * (pH / pW)]; + default: + return [x, y, width, height]; + } + } + rotate(parentRotation = this.parentRotation) { + if (!this.parent || this._drawId === null) { + return; + } + const angle = (parentRotation - this._drawRotation + 360) % 360; + this.parent.drawLayer.updateProperties(this._drawId, DrawingEditor._mergeSVGProperties({ + bbox: this.#rotateBox(parentRotation) + }, this._drawOutlines.updateRotation(angle))); + this.#updateFocusOutline(angle); + } + show(visible = this._isVisible) { + super.show(visible); + this.#updateVisibility(); + } + select() { + super.select(); + this.#toggleFocusOutlineClass({ + hovered: false, + selected: true + }); + } + unselect() { + super.unselect(); + this.#toggleFocusOutlineClass({ + selected: false + }); + } + pointerover() { + if (!this.isSelected) { + this.#toggleFocusOutlineClass({ + hovered: true + }); + } + } + pointerleave() { + if (!this.isSelected) { + this.#toggleFocusOutlineClass({ + hovered: false + }); + } + } + onScaleChanging() { + if (!this.parent) { + return; + } + const bbox = this._drawOutlines.updateParentDimensions(this.parentDimensions, this.parent.scale); + if (bbox) { + this.#updateBbox(bbox); + } + } + static onScaleChangingWhenDrawing() {} + render() { + if (this.div) { + return this.div; + } + let baseX, baseY; + if (this._isCopy) { + baseX = this.x; + baseY = this.y; + } + const div = super.render(); + if (this.constructor._hasDrawClass) { + div.classList.add("draw"); + } + const drawDiv = this.#internalDiv = document.createElement("div"); + div.append(drawDiv); + drawDiv.setAttribute("aria-hidden", "true"); + drawDiv.className = "internal"; + if (this._clipPathId) { + drawDiv.style.clipPath = this._clipPathId; + } + bindEvents(this, drawDiv, ["pointerover", "pointerleave"]); + this.setDims(); + this._uiManager.addShouldRescale(this); + this.disableEditing(); + if (this._isCopy) { + this._moveAfterPaste(baseX, baseY); + } + return div; + } + static createDrawerInstance(_params) { + unreachable("Not implemented"); + } + static _getDrawingTarget(_parent, { + target + }) { + return target; + } + static _getPointerCoords({ + offsetX, + offsetY, + clientX, + clientY + }, referenceEvent = null) { + if (!referenceEvent) { + return [offsetX, offsetY]; + } + let deltaX = clientX - referenceEvent.clientX; + let deltaY = clientY - referenceEvent.clientY; + switch (this._currentParent.viewport.rotation) { + case 90: + [deltaX, deltaY] = [deltaY, -deltaX]; + break; + case 180: + [deltaX, deltaY] = [-deltaX, -deltaY]; + break; + case 270: + [deltaX, deltaY] = [-deltaY, deltaX]; + break; + } + return [referenceEvent.offsetX + deltaX, referenceEvent.offsetY + deltaY]; + } + static _addDrawingListeners(_target, _signal) {} + static _endDrawingSession(isAborted = false) { + return this._currentParent.endDrawingSession(isAborted); + } + static startDrawing(parent, uiManager, isLTR, event) { + const { + pointerId, + pointerType + } = event; + if (CurrentPointers.isInitializedAndDifferentPointerType(pointerType)) { + return; + } + const target = this._getDrawingTarget(parent, event); + const [x, y] = this._getPointerCoords(event); + const { + viewport: { + rotation + } + } = parent; + const { + x: boxX, + y: boxY, + width: parentWidth, + height: parentHeight + } = target.getBoundingClientRect(); + const ac = DrawingEditor.#currentDrawingAC = new AbortController(); + const signal = parent.combinedSignal(ac); + CurrentPointers.setPointer(pointerType, pointerId); + window.addEventListener("pointerup", e => { + if (CurrentPointers.isSamePointerIdOrRemove(e.pointerId)) { + this._endDraw(e); + } + }, { + signal + }); + window.addEventListener("pointercancel", e => { + if (CurrentPointers.isSamePointerIdOrRemove(e.pointerId)) { + this._endDrawingSession(); + } + }, { + signal + }); + window.addEventListener("pointerdown", e => { + if (!CurrentPointers.isSamePointerType(e.pointerType)) { + return; + } + CurrentPointers.initializeAndAddPointerId(e.pointerId); + if (DrawingEditor.#currentDraw.isCancellable()) { + DrawingEditor.#currentDraw.removeLastElement(); + if (DrawingEditor.#currentDraw.isEmpty()) { + this._endDrawingSession(true); + } else { + this._endDraw(null); + } + } + }, { + capture: true, + passive: false, + signal + }); + window.addEventListener("contextmenu", noContextMenu, { + signal + }); + target.addEventListener("pointermove", this._drawMove.bind(this), { + signal + }); + target.addEventListener("touchmove", e => { + if (CurrentPointers.isSameTimeStamp(e.timeStamp)) { + stopEvent(e); + } + }, { + signal + }); + this._addDrawingListeners(target, signal); + parent.toggleDrawing(); + uiManager._editorUndoBar?.hide(); + if (DrawingEditor.#currentDraw) { + parent.drawLayer.updateProperties(this._currentDrawId, DrawingEditor.#currentDraw.startNew(x, y, parentWidth, parentHeight, rotation)); + return; + } + uiManager.updateUIForDefaultProperties(this); + DrawingEditor.#currentDraw = this.createDrawerInstance({ + x, + y, + box: [boxX, boxY, parentWidth, parentHeight], + rotation, + parent, + isLTR + }); + DrawingEditor.#currentDrawingOptions = this.getDefaultDrawingOptions(); + this._currentParent = parent; + const { + id, + clipPathId + } = parent.drawLayer.draw(this._mergeSVGProperties(DrawingEditor.#currentDrawingOptions.toSVGProperties(), DrawingEditor.#currentDraw.defaultSVGProperties), true, this._hasClipPath); + this._currentDrawId = id; + DrawingEditor.#currentClipPathId = this._hasClipPath ? clipPathId : null; + } + static _drawMove(event) { + CurrentPointers.isSameTimeStamp(event.timeStamp); + if (!DrawingEditor.#currentDraw) { + return; + } + if (!CurrentPointers.isSamePointerId(event.pointerId)) { + return; + } + if (CurrentPointers.isUsingMultiplePointers()) { + this._endDraw(event); + return; + } + let properties; + const coalesced = event.getCoalescedEvents?.(); + if (coalesced?.length) { + const points = []; + for (const sample of coalesced) { + points.push(...this._getPointerCoords(sample, event)); + } + properties = DrawingEditor.#currentDraw.addPoints(points); + } else { + properties = DrawingEditor.#currentDraw.add(...this._getPointerCoords(event)); + } + this._currentParent.drawLayer.updateProperties(this._currentDrawId, properties); + CurrentPointers.setTimeStamp(event.timeStamp); + stopEvent(event); + } + static _cleanup(all) { + if (all) { + this._currentDrawId = -1; + this._currentParent = null; + DrawingEditor.#currentDraw = null; + DrawingEditor.#currentDrawingOptions = null; + DrawingEditor.#currentClipPathId = null; + CurrentPointers.clearTimeStamp(); + } + if (DrawingEditor.#currentDrawingAC) { + DrawingEditor.#currentDrawingAC.abort(); + DrawingEditor.#currentDrawingAC = null; + CurrentPointers.clearPointerIds(); + } + } + static _endDraw(event) { + const parent = this._currentParent; + if (!parent) { + return; + } + parent.toggleDrawing(true); + this._cleanup(false); + parent.drawLayer.updateProperties(this._currentDrawId, event?.target === parent.div ? DrawingEditor.#currentDraw.end(...this._getPointerCoords(event)) : DrawingEditor.#currentDraw.end()); + if (this.supportMultipleDrawings) { + const draw = DrawingEditor.#currentDraw; + const drawId = this._currentDrawId; + const lastElement = draw.getLastElement(); + parent.addCommands({ + cmd: () => { + parent.drawLayer.updateProperties(drawId, draw.setLastElement(lastElement)); + }, + undo: () => { + parent.drawLayer.updateProperties(drawId, draw.removeLastElement()); + }, + mustExec: false, + type: AnnotationEditorParamsType.DRAW_STEP + }); + return; + } + this.endDrawing(false); + } + static endDrawing(isAborted) { + const parent = this._currentParent; + if (!parent) { + return null; + } + parent.toggleDrawing(true); + parent.cleanUndoStack(AnnotationEditorParamsType.DRAW_STEP); + if (!DrawingEditor.#currentDraw.isEmpty()) { + const { + pageDimensions: [pageWidth, pageHeight], + scale + } = parent; + const editor = parent.createAndAddNewEditor({ + offsetX: 0, + offsetY: 0 + }, false, { + drawId: this._currentDrawId, + clipPathId: DrawingEditor.#currentClipPathId, + drawOutlines: DrawingEditor.#currentDraw.getOutlines(pageWidth * scale, pageHeight * scale, scale, this._INNER_MARGIN), + drawingOptions: DrawingEditor.#currentDrawingOptions, + mustBeCommitted: !isAborted + }); + this._cleanup(true); + return editor; + } + parent.drawLayer.remove(this._currentDrawId); + this._cleanup(true); + return null; + } + createDrawingOptions(_data) {} + static deserializeDraw(_pageX, _pageY, _pageWidth, _pageHeight, _innerMargin, _data, _uiManager) { + unreachable("Not implemented"); + } + static async deserialize(data, parent, uiManager) { + const { + rawDims: { + pageWidth, + pageHeight, + pageX, + pageY + } + } = parent.viewport; + const drawOutlines = this.deserializeDraw(pageX, pageY, pageWidth, pageHeight, this._INNER_MARGIN, data, uiManager); + const editor = await super.deserialize(data, parent, uiManager); + editor.createDrawingOptions(data); + editor.#createDrawOutlines({ + drawOutlines + }); + editor.#addToDrawLayer(); + editor.onScaleChanging(); + editor.rotate(); + return editor; + } + serializeDraw(isForCopying) { + const [pageX, pageY] = this.pageTranslation; + const [pageWidth, pageHeight] = this.pageDimensions; + return this._drawOutlines.serialize([pageX, pageY, pageWidth, pageHeight], isForCopying); + } + renderAnnotationElement(annotation) { + annotation.updateEdited({ + rect: this.getPDFRect() + }); + return null; + } + static canCreateNewEmptyEditor() { + return false; + } +} + ;// ./src/display/editor/drawers/outline.js class Outline { static PRECISION = 1e-4; + focusOutline = null; toSVGPath() { unreachable("Abstract method `toSVGPath` must be implemented."); } @@ -20680,6 +21605,50 @@ class Outline { serialize(_bbox, _rotation) { unreachable("Abstract method `serialize` must be implemented."); } + get defaultSVGProperties() { + unreachable("Abstract getter `defaultSVGProperties` must be implemented."); + } + get defaultProperties() { + return this.defaultSVGProperties; + } + getFocusSVGProperties(_rotation) { + return null; + } + get focusMustRemoveSelfIntersections() { + return false; + } + updateProperty(_name, _value) { + return null; + } + updateParentDimensions(_dimensions, _scale) { + return null; + } + serializeQuadPoints(_pageTranslation, _pageDimensions) { + return null; + } + updateRotation(_rotation) { + return {}; + } + getPathResizingSVGProperties(_bbox) { + return {}; + } + getPathResizedSVGProperties(_bbox) { + return {}; + } + getPathTranslatedSVGProperties(_bbox, _parentDimensions) { + return {}; + } + static _rotateBox([x, y, width, height], angle) { + switch (angle) { + case 90: + return [1 - y - height, x, height, width]; + case 180: + return [1 - x - width, 1 - y - height, width, height]; + case 270: + return [y, 1 - x - width, height, width]; + } + return [x, y, width, height]; + } static _rescale(src, tx, ty, sx, sy, dest) { dest ||= new Float32Array(src.length); for (let i = 0, ii = src.length; i < ii; i += 2) { @@ -20744,10 +21713,7 @@ class FreeDrawOutliner { static #MIN_DIST = 8; static #MIN_DIFF = 2; static #MIN = FreeDrawOutliner.#MIN_DIST + FreeDrawOutliner.#MIN_DIFF; - constructor({ - x, - y - }, box, scaleFactor, thickness, isLTR, innerMargin = 0) { + constructor(x, y, box, scaleFactor, thickness, isLTR, innerMargin = 0) { this.#box = box; this.#thickness = thickness * scaleFactor; this.#isLTR = isLTR; @@ -20761,16 +21727,25 @@ class FreeDrawOutliner { isEmpty() { return isNaN(this.#last[8]); } + isCancellable() { + return this.#points.length <= 10; + } + removeLastElement() { + this.#last.fill(NaN); + this.#top.length = this.#bottom.length = this.#points.length = 0; + return { + path: { + d: "" + } + }; + } #getLastCoords() { const lastTop = this.#last.subarray(4, 6); const lastBottom = this.#last.subarray(16, 18); const [x, y, width, height] = this.#box; return [(this.#lastX + (lastTop[0] - lastBottom[0]) / 2 - x) / width, (this.#lastY + (lastTop[1] - lastBottom[1]) / 2 - y) / height, (this.#lastX + (lastBottom[0] - lastTop[0]) / 2 - x) / width, (this.#lastY + (lastBottom[1] - lastTop[1]) / 2 - y) / height]; } - add({ - x, - y - }) { + add(x, y) { this.#lastX = x; this.#lastY = y; const [layerX, layerY, layerWidth, layerHeight] = this.#box; @@ -21064,8 +22039,17 @@ class FreeDrawOutline extends Outline { get box() { return this.#bbox; } - newOutliner(point, box, scaleFactor, thickness, isLTR, innerMargin = 0) { - return new FreeDrawOutliner(point, box, scaleFactor, thickness, isLTR, innerMargin); + newOutliner(x, y, box, scaleFactor, thickness, isLTR, innerMargin = 0) { + return new FreeDrawOutliner(x, y, box, scaleFactor, thickness, isLTR, innerMargin); + } + updateThickness(thickness) { + const outline = this.getNewOutline(thickness); + this.#outline = outline.#outline; + this.#points = outline.#points; + this.#bbox.set(outline.#bbox); + this.firstPoint = outline.firstPoint; + this.lastPoint = outline.lastPoint; + return this.#bbox; } getNewOutline(thickness, innerMargin) { const [x, y, width, height] = this.#bbox; @@ -21074,15 +22058,10 @@ class FreeDrawOutline extends Outline { const sy = height * layerHeight; const tx = x * layerWidth + layerX; const ty = y * layerHeight + layerY; - const outliner = this.newOutliner({ - x: this.#points[0] * sx + tx, - y: this.#points[1] * sy + ty - }, this.#box, this.#scaleFactor, thickness, this.#isLTR, innerMargin ?? this.#innerMargin); - for (let i = 2; i < this.#points.length; i += 2) { - outliner.add({ - x: this.#points[i] * sx + tx, - y: this.#points[i + 1] * sy + ty - }); + const points = this.#points; + const outliner = this.newOutliner(points[0] * sx + tx, points[1] * sy + ty, this.#box, this.#scaleFactor, thickness, this.#isLTR, innerMargin ?? this.#innerMargin); + for (let i = 2, ii = points.length; i < ii; i += 2) { + outliner.add(points[i] * sx + tx, points[i + 1] * sy + ty); } return outliner.getOutlines(); } @@ -21092,6 +22071,39 @@ class FreeDrawOutline extends Outline { +function getHighlightSVGProperties(outline) { + return { + bbox: outline.box, + root: { + viewBox: "0 0 1 1" + }, + rootClass: { + highlight: true, + free: outline.isFree + }, + path: { + d: outline.toSVGPath() + } + }; +} +function getHighlightFocusSVGProperties(outline, rotation) { + const { + focusOutline + } = outline; + return { + bbox: Outline._rotateBox(focusOutline.box, rotation), + root: { + "data-main-rotation": rotation + }, + rootClass: { + highlightOutline: true, + free: outline.isFree + }, + path: { + d: focusOutline.toSVGPath() + } + }; +} class HighlightOutliner { #box; #firstPoint; @@ -21291,6 +22303,7 @@ class HighlightOutliner { } class HighlightOutline extends Outline { #box; + #boxes = null; #outlines; constructor(outlines, box, firstPoint, lastPoint) { super(); @@ -21299,6 +22312,48 @@ class HighlightOutline extends Outline { this.firstPoint = firstPoint; this.lastPoint = lastPoint; } + static build(boxes, isLTR) { + const outline = new HighlightOutliner(boxes, 0.001).getOutlines(); + outline.#boxes = boxes; + outline.focusOutline = new HighlightOutliner(boxes, 0.0025, 0.001, isLTR).getOutlines(); + return outline; + } + get isFree() { + return false; + } + get defaultSVGProperties() { + return getHighlightSVGProperties(this); + } + getFocusSVGProperties(rotation) { + return getHighlightFocusSVGProperties(this, rotation); + } + updateRotation(rotation) { + return { + root: { + "data-main-rotation": rotation + } + }; + } + serializeQuadPoints([pageX, pageY], [pageWidth, pageHeight]) { + const boxes = this.#boxes; + const quadPoints = new Float32Array(boxes.length * 8); + let i = 0; + for (const { + x, + y, + width, + height + } of boxes) { + const sx = x * pageWidth + pageX; + const sy = (1 - y) * pageHeight + pageY; + quadPoints[i] = quadPoints[i + 4] = sx; + quadPoints[i + 1] = quadPoints[i + 3] = sy; + quadPoints[i + 2] = quadPoints[i + 6] = sx + width * pageWidth; + quadPoints[i + 5] = quadPoints[i + 7] = sy - height * pageHeight; + i += 8; + } + return quadPoints; + } toSVGPath() { const buffer = []; for (const polygon of this.#outlines) { @@ -21342,9 +22397,108 @@ class FreeHighlightOutliner extends FreeDrawOutliner { return new FreeHighlightOutline(outline, points, box, scaleFactor, innerMargin, isLTR); } } +class FreeHighlightDrawer { + #outliner; + #thickness; + constructor(x, y, box, scaleFactor, thickness, isLTR, innerMargin) { + this.#outliner = new FreeHighlightOutliner(x, y, box, scaleFactor, thickness, isLTR, innerMargin); + this.#thickness = thickness; + } + add(x, y) { + return this.#outliner.add(x, y) ? { + path: { + d: this.#outliner.toSVGPath() + } + } : null; + } + addPoints(points) { + let hasChanged = false; + for (let i = 0, ii = points.length; i < ii; i += 2) { + hasChanged = this.#outliner.add(points[i], points[i + 1]) || hasChanged; + } + return hasChanged ? { + path: { + d: this.#outliner.toSVGPath() + } + } : null; + } + end(x, y) { + return x === undefined ? null : this.add(x, y); + } + isEmpty() { + return this.#outliner.isEmpty(); + } + isCancellable() { + return this.#outliner.isCancellable(); + } + removeLastElement() { + return this.#outliner.removeLastElement(); + } + updateProperty(_name, _value) { + return null; + } + getOutlines() { + const outlines = this.#outliner.getOutlines(); + outlines.buildFocusOutline(2 * this.#thickness); + return outlines; + } + get defaultSVGProperties() { + return { + bbox: [0, 0, 1, 1], + root: { + viewBox: "0 0 1 1" + }, + rootClass: { + highlight: true, + free: true + }, + path: { + d: this.#outliner.toSVGPath() + } + }; + } +} class FreeHighlightOutline extends FreeDrawOutline { - newOutliner(point, box, scaleFactor, thickness, isLTR, innerMargin = 0) { - return new FreeHighlightOutliner(point, box, scaleFactor, thickness, isLTR, innerMargin); + static #EXTRA_THICKNESS = 1.5; + newOutliner(x, y, box, scaleFactor, thickness, isLTR, innerMargin = 0) { + return new FreeHighlightOutliner(x, y, box, scaleFactor, thickness, isLTR, innerMargin); + } + get isFree() { + return true; + } + buildFocusOutline(thickness) { + this.focusOutline = this.getNewOutline(thickness / 2 + FreeHighlightOutline.#EXTRA_THICKNESS, 0.0025); + } + get defaultSVGProperties() { + return getHighlightSVGProperties(this); + } + getFocusSVGProperties(rotation) { + return getHighlightFocusSVGProperties(this, rotation); + } + get focusMustRemoveSelfIntersections() { + return true; + } + updateRotation(rotation) { + return { + root: { + "data-main-rotation": rotation + } + }; + } + updateProperty(name, value) { + if (name !== "thickness") { + return null; + } + const bbox = this.updateThickness(value / 2); + this.buildFocusOutline(value); + return bbox; + } + getPathResizedSVGProperties() { + return { + path: { + d: this.toSVGPath() + } + }; } } @@ -21356,33 +22510,35 @@ class FreeHighlightOutline extends FreeDrawOutline { -class HighlightEditor extends AnnotationEditor { + +class HighlightDrawingOptions extends DrawingOptions { + constructor(properties = null) { + super(); + super.updateProperties(properties); + } + updateSVGProperty(name, value) { + if (name !== "thickness") { + super.updateSVGProperty(name, value); + } + } + clone() { + const clone = new HighlightDrawingOptions(); + clone.updateAll(this); + return clone; + } +} +class HighlightEditor extends DrawingEditor { #anchorNode = null; #anchorOffset = 0; - #boxes; - #clipPathId = null; - #colorPicker = null; - #focusOutlines = null; #focusNode = null; #focusOffset = 0; - #highlightDiv = null; - #highlightOutlines = null; - #id = null; - #isFreeHighlight = false; - #firstPoint = null; - #lastPoint = null; - #outlineId = null; - #text = ""; - #thickness; #methodOfCreation = ""; - static _defaultColor = null; - static _defaultOpacity = 1; - static _defaultThickness = 12; + #text = ""; + static _DEFAULT_OPACITY = 1; + static _DEFAULT_THICKNESS = 12; + static _defaultDrawingOptions = null; static _type = "highlight"; static _editorType = AnnotationEditorType.HIGHLIGHT; - static _freeHighlightId = -1; - static _freeHighlight = null; - static _freeHighlightClipId = ""; static get _keyboardManager() { const proto = HighlightEditor.prototype; return shadow(this, "_keyboardManager", new KeyboardManager([[["ArrowLeft"], proto._moveCaret, { @@ -21400,37 +22556,88 @@ class HighlightEditor extends AnnotationEditor { ...params, name: "highlightEditor" }); - this.color = params.color || HighlightEditor._defaultColor; - this.#thickness = params.thickness || HighlightEditor._defaultThickness; - this.opacity = params.opacity || HighlightEditor._defaultOpacity; - this.#boxes = params.boxes || null; - this.#methodOfCreation = params.methodOfCreation || ""; + this.#anchorNode = params.anchorNode || null; + this.#anchorOffset = params.anchorOffset || 0; + this.#focusNode = params.focusNode || null; + this.#focusOffset = params.focusOffset || 0; + this.#methodOfCreation = params.methodOfCreation || (this._drawOutlines?.isFree ? "main_toolbar" : ""); this.#text = params.text || ""; this._isDraggable = false; this.defaultL10nId = "pdfjs-editor-highlight-editor"; - if (params.highlightId > -1) { - this.#isFreeHighlight = true; - this.#createFreeOutlines(params); - this.#addToDrawLayer(); - } else if (this.#boxes) { - this.#anchorNode = params.anchorNode; - this.#anchorOffset = params.anchorOffset; - this.#focusNode = params.focusNode; - this.#focusOffset = params.focusOffset; - this.#createOutlines(); - this.#addToDrawLayer(); - this.rotate(this.rotation); + this.rotate(); + } + static initialize(l10n, uiManager) { + AnnotationEditor.initialize(l10n, uiManager); + this._defaultDrawingOptions ||= new HighlightDrawingOptions({ + fill: uiManager.highlightColors?.values().next().value || "#fff066", + "fill-opacity": HighlightEditor._DEFAULT_OPACITY, + thickness: HighlightEditor._DEFAULT_THICKNESS + }); + } + static getDefaultDrawingOptions(options) { + const clone = this._defaultDrawingOptions.clone(); + clone.updateProperties(options); + return clone; + } + static get typesMap() { + return shadow(this, "typesMap", new Map([[AnnotationEditorParamsType.HIGHLIGHT_COLOR, "fill"], [AnnotationEditorParamsType.HIGHLIGHT_THICKNESS, "thickness"]])); + } + static get isDrawer() { + return false; + } + static get _hasClipPath() { + return true; + } + static get _hasDrawClass() { + return false; + } + _addOutlines(params) { + const { + boxes, + drawOutlines + } = params; + if (!boxes && !drawOutlines) { + return; } - if (!this.annotationElementId) { - this._uiManager.a11yAlert(AnnotationEditor._l10nAlert.highlight); + this._drawingOptions ||= params.drawingOptions || HighlightEditor.getDefaultDrawingOptions(); + if (boxes) { + params = { + ...params, + drawOutlines: HighlightOutline.build(boxes, this._uiManager.direction === "ltr") + }; } + super._addOutlines(params); + } + get colorType() { + return AnnotationEditorParamsType.HIGHLIGHT_COLOR; + } + get color() { + return this._drawingOptions.fill; + } + get opacity() { + return this._drawingOptions["fill-opacity"]; + } + get _opacityName() { + return "fill-opacity"; + } + get _drawRotation() { + return this._drawOutlines?.isFree ? this.rotation : 0; + } + get isResizable() { + return false; + } + get _mustBeDisabledOnCommit() { + return false; + } + get _mustFixPosition() { + return !this._drawOutlines?.isFree; } get telemetryInitialData() { return { action: "added", - type: this.#isFreeHighlight ? "free_highlight" : "highlight", + type: this._drawOutlines.isFree ? "free_highlight" : "highlight", color: this._uiManager.getNonHCMColorName(this.color), - thickness: this.#thickness, + thickness: this._drawingOptions.thickness, methodOfCreation: this.#methodOfCreation }; } @@ -21445,219 +22652,54 @@ class HighlightEditor extends AnnotationEditor { numberOfColors: data.get("color").size }; } - #createOutlines() { - const outliner = new HighlightOutliner(this.#boxes, 0.001); - this.#highlightOutlines = outliner.getOutlines(); - [this.x, this.y, this.width, this.height] = this.#highlightOutlines.box; - const outlinerForOutline = new HighlightOutliner(this.#boxes, 0.0025, 0.001, this._uiManager.direction === "ltr"); - this.#focusOutlines = outlinerForOutline.getOutlines(); - const { - firstPoint - } = this.#highlightOutlines; - this.#firstPoint = [(firstPoint[0] - this.x) / this.width, (firstPoint[1] - this.y) / this.height]; - const { - lastPoint - } = this.#focusOutlines; - this.#lastPoint = [(lastPoint[0] - this.x) / this.width, (lastPoint[1] - this.y) / this.height]; - } - #createFreeOutlines({ - highlightOutlines, - highlightId, - clipPathId - }) { - this.#highlightOutlines = highlightOutlines; - const extraThickness = 1.5; - this.#focusOutlines = highlightOutlines.getNewOutline(this.#thickness / 2 + extraThickness, 0.0025); - if (highlightId >= 0) { - this.#id = highlightId; - this.#clipPathId = clipPathId; - this.parent.drawLayer.finalizeDraw(highlightId, { - bbox: highlightOutlines.box, - path: { - d: highlightOutlines.toSVGPath() - } - }); - this.#outlineId = this.parent.drawLayer.drawOutline({ - rootClass: { - highlightOutline: true, - free: true - }, - bbox: this.#focusOutlines.box, - path: { - d: this.#focusOutlines.toSVGPath() - } - }, true); - } else if (this.parent) { - const angle = this.parent.viewport.rotation; - this.parent.drawLayer.updateProperties(this.#id, { - bbox: HighlightEditor.#rotateBbox(this.#highlightOutlines.box, (angle - this.rotation + 360) % 360), - path: { - d: highlightOutlines.toSVGPath() - } - }); - this.parent.drawLayer.updateProperties(this.#outlineId, { - bbox: HighlightEditor.#rotateBbox(this.#focusOutlines.box, angle), - path: { - d: this.#focusOutlines.toSVGPath() - } - }); - } - const [x, y, width, height] = highlightOutlines.box; - switch (this.rotation) { - case 0: - this.x = x; - this.y = y; - this.width = width; - this.height = height; - break; - case 90: - { - const [pageWidth, pageHeight] = this.parentDimensions; - this.x = y; - this.y = 1 - x; - this.width = width * pageHeight / pageWidth; - this.height = height * pageWidth / pageHeight; - break; - } - case 180: - this.x = 1 - x; - this.y = 1 - y; - this.width = width; - this.height = height; - break; - case 270: - { - const [pageWidth, pageHeight] = this.parentDimensions; - this.x = 1 - y; - this.y = x; - this.width = width * pageHeight / pageWidth; - this.height = height * pageWidth / pageHeight; - break; - } - } - const { - firstPoint - } = highlightOutlines; - this.#firstPoint = [(firstPoint[0] - x) / width, (firstPoint[1] - y) / height]; - const { - lastPoint - } = this.#focusOutlines; - this.#lastPoint = [(lastPoint[0] - x) / width, (lastPoint[1] - y) / height]; - } - static initialize(l10n, uiManager) { - AnnotationEditor.initialize(l10n, uiManager); - HighlightEditor._defaultColor ||= uiManager.highlightColors?.values().next().value || "#fff066"; - } - static updateDefaultParams(type, value) { - switch (type) { - case AnnotationEditorParamsType.HIGHLIGHT_COLOR: - HighlightEditor._defaultColor = value; - break; - case AnnotationEditorParamsType.HIGHLIGHT_THICKNESS: - HighlightEditor._defaultThickness = value; - break; - } - } translateInPage(x, y) {} get toolbarPosition() { - return this.#lastPoint; + return this.#relativeToBox(this._drawOutlines.focusOutline.lastPoint); } get commentButtonPosition() { - return this.#firstPoint; + return this.#relativeToBox(this._drawOutlines.firstPoint); + } + #relativeToBox([pointX, pointY]) { + const [x, y, width, height] = this._drawOutlines.box; + return [(pointX - x) / width, (pointY - y) / height]; } updateParams(type, value) { switch (type) { case AnnotationEditorParamsType.HIGHLIGHT_COLOR: - this.#updateColor(value); + this._updateColorAndOpacity(value, HighlightEditor._DEFAULT_OPACITY, type); + this._reportTelemetry({ + action: "color_changed", + color: this._uiManager.getNonHCMColorName(value) + }, true); break; case AnnotationEditorParamsType.HIGHLIGHT_THICKNESS: - this.#updateThickness(value); + super.updateParams(type, value); + this._reportTelemetry({ + action: "thickness_changed", + thickness: value + }, true); break; } } - static get defaultPropertiesToUpdate() { - return [[AnnotationEditorParamsType.HIGHLIGHT_COLOR, HighlightEditor._defaultColor], [AnnotationEditorParamsType.HIGHLIGHT_THICKNESS, HighlightEditor._defaultThickness]]; - } get propertiesToUpdate() { - return [[AnnotationEditorParamsType.HIGHLIGHT_COLOR, this.color || HighlightEditor._defaultColor], [AnnotationEditorParamsType.HIGHLIGHT_THICKNESS, this.#thickness || HighlightEditor._defaultThickness], [AnnotationEditorParamsType.HIGHLIGHT_FREE, this.#isFreeHighlight]]; - } - onUpdatedColor() { - this.parent?.drawLayer.updateProperties(this.#id, { - root: { - fill: this.color, - "fill-opacity": this.opacity - } - }); - this.#colorPicker?.updateColor(this.color); - super.onUpdatedColor(); - } - #updateColor(color) { - const setColorAndOpacity = (col, opa) => { - this.color = col; - this.opacity = opa; - this.onUpdatedColor(); - }; - const savedColor = this.color; - const savedOpacity = this.opacity; - this.addCommands({ - cmd: setColorAndOpacity.bind(this, color, HighlightEditor._defaultOpacity), - undo: setColorAndOpacity.bind(this, savedColor, savedOpacity), - post: this._uiManager.updateUI.bind(this._uiManager, this), - mustExec: true, - type: AnnotationEditorParamsType.HIGHLIGHT_COLOR, - overwriteIfSameType: true, - keepUndo: true - }); - this._reportTelemetry({ - action: "color_changed", - color: this._uiManager.getNonHCMColorName(color) - }, true); - } - #updateThickness(thickness) { - const savedThickness = this.#thickness; - const setThickness = th => { - this.#thickness = th; - this.#changeThickness(th); - }; - this.addCommands({ - cmd: setThickness.bind(this, thickness), - undo: setThickness.bind(this, savedThickness), - post: this._uiManager.updateUI.bind(this._uiManager, this), - mustExec: true, - type: AnnotationEditorParamsType.INK_THICKNESS, - overwriteIfSameType: true, - keepUndo: true - }); - this._reportTelemetry({ - action: "thickness_changed", - thickness - }, true); + const properties = super.propertiesToUpdate; + properties.push([AnnotationEditorParamsType.HIGHLIGHT_FREE, this._drawOutlines.isFree]); + return properties; } get toolbarButtons() { if (this._uiManager.highlightColors) { - const colorPicker = this.#colorPicker = new ColorPicker({ + this._colorPicker = new ColorPicker({ editor: this }); - return [["colorPicker", colorPicker]]; + return [["colorPicker", this._colorPicker]]; } return super.toolbarButtons; } - disableEditing() { - super.disableEditing(); - this.div.classList.toggle("disabled", true); - } - enableEditing() { - super.enableEditing(); - this.div.classList.toggle("disabled", false); - } fixAndSetPosition() { - return super.fixAndSetPosition(this.#getRotation()); - } - getBaseTranslation() { - return [0, 0]; + return super.fixAndSetPosition(this._drawRotation); } getRect(tx, ty) { - return super.getRect(tx, ty, this.#getRotation()); + return super.getRect(tx, ty, this._drawRotation); } onceAdded(focus) { if (!this.annotationElementId) { @@ -21668,129 +22710,11 @@ class HighlightEditor extends AnnotationEditor { } } remove() { - this.#cleanDrawLayer(); this._reportTelemetry({ action: "deleted" }); super.remove(); } - rebuild() { - if (!this.parent) { - return; - } - super.rebuild(); - if (this.div === null) { - return; - } - this.#addToDrawLayer(); - if (!this.isAttachedToDOM) { - this.parent.add(this); - } - } - setParent(parent) { - let mustBeSelected = false; - if (this.parent && !parent) { - this.#cleanDrawLayer(); - } else if (parent) { - this.#addToDrawLayer(parent); - mustBeSelected = !this.parent && this.div?.classList.contains("selectedEditor"); - } - super.setParent(parent); - this.show(this._isVisible); - if (mustBeSelected) { - this.select(); - } - } - #changeThickness(thickness) { - if (!this.#isFreeHighlight) { - return; - } - this.#createFreeOutlines({ - highlightOutlines: this.#highlightOutlines.getNewOutline(thickness / 2) - }); - this.fixAndSetPosition(); - this.setDims(); - } - #cleanDrawLayer() { - if (this.#id === null || !this.parent) { - return; - } - this.parent.drawLayer.remove(this.#id); - this.#id = null; - this.parent.drawLayer.remove(this.#outlineId); - this.#outlineId = null; - } - #addToDrawLayer(parent = this.parent) { - if (this.#id !== null) { - return; - } - ({ - id: this.#id, - clipPathId: this.#clipPathId - } = parent.drawLayer.draw({ - bbox: this.#highlightOutlines.box, - root: { - viewBox: "0 0 1 1", - fill: this.color, - "fill-opacity": this.opacity - }, - rootClass: { - highlight: true, - free: this.#isFreeHighlight - }, - path: { - d: this.#highlightOutlines.toSVGPath() - } - }, false, true)); - this.#outlineId = parent.drawLayer.drawOutline({ - rootClass: { - highlightOutline: true, - free: this.#isFreeHighlight - }, - bbox: this.#focusOutlines.box, - path: { - d: this.#focusOutlines.toSVGPath() - } - }, this.#isFreeHighlight); - if (this.#highlightDiv) { - this.#highlightDiv.style.clipPath = this.#clipPathId; - } - } - static #rotateBbox([x, y, width, height], angle) { - switch (angle) { - case 90: - return [1 - y - height, x, height, width]; - case 180: - return [1 - x - width, 1 - y - height, width, height]; - case 270: - return [y, 1 - x - width, height, width]; - } - return [x, y, width, height]; - } - rotate(angle) { - const { - drawLayer - } = this.parent; - let box; - if (this.#isFreeHighlight) { - angle = (angle - this.rotation + 360) % 360; - box = HighlightEditor.#rotateBbox(this.#highlightOutlines.box, angle); - } else { - box = HighlightEditor.#rotateBbox([this.x, this.y, this.width, this.height], angle); - } - drawLayer.updateProperties(this.#id, { - bbox: box, - root: { - "data-main-rotation": angle - } - }); - drawLayer.updateProperties(this.#outlineId, { - bbox: HighlightEditor.#rotateBbox(this.#focusOutlines.box, angle), - root: { - "data-main-rotation": angle - } - }); - } render() { if (this.div) { return this.div; @@ -21800,41 +22724,16 @@ class HighlightEditor extends AnnotationEditor { div.setAttribute("aria-label", this.#text); div.setAttribute("role", "mark"); } - if (this.#isFreeHighlight) { + if (this._drawOutlines.isFree) { div.classList.add("free"); } else { - this.div.addEventListener("keydown", this.#keydown.bind(this), { + div.addEventListener("keydown", this.#keydown.bind(this), { signal: this._uiManager._signal }); } - const highlightDiv = this.#highlightDiv = document.createElement("div"); - div.append(highlightDiv); - highlightDiv.setAttribute("aria-hidden", "true"); - highlightDiv.className = "internal"; - highlightDiv.style.clipPath = this.#clipPathId; - this.setDims(); - bindEvents(this, this.#highlightDiv, ["pointerover", "pointerleave"]); this.enableEditing(); return div; } - pointerover() { - if (!this.isSelected) { - this.parent?.drawLayer.updateProperties(this.#outlineId, { - rootClass: { - hovered: true - } - }); - } - } - pointerleave() { - if (!this.isSelected) { - this.parent?.drawLayer.updateProperties(this.#outlineId, { - rootClass: { - hovered: false - } - }); - } - } #keydown(event) { HighlightEditor._keyboardManager.exec(this, event); } @@ -21862,102 +22761,38 @@ class HighlightEditor extends AnnotationEditor { selection.setPosition(this.#focusNode, this.#focusOffset); } } - select() { - super.select(); - if (!this.#outlineId) { - return; - } - this.parent?.drawLayer.updateProperties(this.#outlineId, { - rootClass: { - hovered: false, - selected: true - } - }); - } unselect() { super.unselect(); - if (!this.#outlineId) { - return; - } - this.parent?.drawLayer.updateProperties(this.#outlineId, { - rootClass: { - selected: false - } - }); - if (!this.#isFreeHighlight) { + if (!this._drawOutlines.isFree) { this.#setCaret(false); } } - get _mustFixPosition() { - return !this.#isFreeHighlight; + static createDrawerInstance({ + x, + y, + box, + parent, + isLTR + }) { + return new FreeHighlightDrawer(x, y, box, parent.scale, this._defaultDrawingOptions.thickness / 2, isLTR, 0.001); } - show(visible = this._isVisible) { - super.show(visible); - if (this.parent) { - this.parent.drawLayer.updateProperties(this.#id, { - rootClass: { - hidden: !visible - } - }); - this.parent.drawLayer.updateProperties(this.#outlineId, { - rootClass: { - hidden: !visible - } - }); - } + static _getDrawingTarget(parent, { + target + }) { + return target.closest(".textLayer"); } - #getRotation() { - return this.#isFreeHighlight ? this.rotation : 0; - } - #serializeBoxes() { - if (this.#isFreeHighlight) { - return null; - } - const [pageWidth, pageHeight] = this.pageDimensions; - const [pageX, pageY] = this.pageTranslation; - const boxes = this.#boxes; - const quadPoints = new Float32Array(boxes.length * 8); - let i = 0; - for (const { - x, - y, - width, - height - } of boxes) { - const sx = x * pageWidth + pageX; - const sy = (1 - y) * pageHeight + pageY; - quadPoints[i] = quadPoints[i + 4] = sx; - quadPoints[i + 1] = quadPoints[i + 3] = sy; - quadPoints[i + 2] = quadPoints[i + 6] = sx + width * pageWidth; - quadPoints[i + 5] = quadPoints[i + 7] = sy - height * pageHeight; - i += 8; - } - return quadPoints; - } - #serializeOutlines(rect) { - return this.#highlightOutlines.serialize(rect, this.#getRotation()); - } - static startHighlighting(parent, isLTR, { - target: textLayer, + static _getPointerCoords({ x, y }) { - const { - x: layerX, - y: layerY, - width: parentWidth, - height: parentHeight - } = textLayer.getBoundingClientRect(); - const ac = new AbortController(); - const signal = parent.combinedSignal(ac); - const pointerUpCallback = e => { - ac.abort(); - this.#endHighlight(parent, e); - }; - window.addEventListener("blur", pointerUpCallback, { - signal + return [x, y]; + } + static _addDrawingListeners(target, signal) { + target.classList.add("free"); + signal.addEventListener("abort", () => target.classList.remove("free"), { + once: true }); - window.addEventListener("pointerup", pointerUpCallback, { + window.addEventListener("blur", () => this._endDraw(null), { signal }); window.addEventListener("pointerdown", stopEvent, { @@ -21965,58 +22800,50 @@ class HighlightEditor extends AnnotationEditor { passive: false, signal }); - window.addEventListener("contextmenu", noContextMenu, { - signal + } + static _endDrawingSession(isAborted = false) { + return this.endDrawing(isAborted); + } + createDrawingOptions({ + color, + opacity, + thickness + }) { + const { + _defaultDrawingOptions: defaults, + _DEFAULT_OPACITY + } = HighlightEditor; + this._drawingOptions = HighlightEditor.getDefaultDrawingOptions({ + fill: Util.makeHexColor(...color), + "fill-opacity": opacity || _DEFAULT_OPACITY, + thickness: thickness || defaults.thickness }); - textLayer.addEventListener("pointermove", this.#highlightMove.bind(this, parent), { - signal - }); - this._freeHighlight = new FreeHighlightOutliner({ - x, - y - }, [layerX, layerY, parentWidth, parentHeight], parent.scale, this._defaultThickness / 2, isLTR, 0.001); - ({ - id: this._freeHighlightId, - clipPathId: this._freeHighlightClipId - } = parent.drawLayer.draw({ - bbox: [0, 0, 1, 1], - root: { - viewBox: "0 0 1 1", - fill: this._defaultColor, - "fill-opacity": this._defaultOpacity - }, - rootClass: { - highlight: true, - free: true - }, - path: { - d: this._freeHighlight.toSVGPath() + } + static deserializeDraw(pageX, pageY, pageWidth, pageHeight, _innerMargin, data, uiManager) { + const { + quadPoints + } = data; + if (quadPoints) { + const boxes = []; + for (let i = 0, ii = quadPoints.length; i < ii; i += 8) { + boxes.push({ + x: (quadPoints[i] - pageX) / pageWidth, + y: 1 - (quadPoints[i + 1] - pageY) / pageHeight, + width: (quadPoints[i + 2] - quadPoints[i]) / pageWidth, + height: (quadPoints[i + 1] - quadPoints[i + 5]) / pageHeight + }); } - }, true, true)); - } - static #highlightMove(parent, event) { - if (this._freeHighlight.add(event)) { - parent.drawLayer.updateProperties(this._freeHighlightId, { - path: { - d: this._freeHighlight.toSVGPath() - } - }); + return HighlightOutline.build(boxes, uiManager.direction === "ltr"); } - } - static #endHighlight(parent, event) { - if (!this._freeHighlight.isEmpty()) { - parent.createAndAddNewEditor(event, false, { - highlightId: this._freeHighlightId, - highlightOutlines: this._freeHighlight.getOutlines(), - clipPathId: this._freeHighlightClipId, - methodOfCreation: "main_toolbar" - }); - } else { - parent.drawLayer.remove(this._freeHighlightId); + const thickness = data.thickness || this._defaultDrawingOptions.thickness; + const points = (data.inkLists || data.outlines.points)[0]; + const outliner = new FreeHighlightOutliner(points[0] - pageX, pageHeight - (points[1] - pageY), [0, 0, pageWidth, pageHeight], 1, thickness / 2, true, 0.001); + for (let i = 0, ii = points.length; i < ii; i += 2) { + outliner.add(points[i] - pageX, pageHeight - (points[i + 1] - pageY)); } - this._freeHighlightId = -1; - this._freeHighlight = null; - this._freeHighlightClipId = ""; + const outlines = outliner.getOutlines(); + outlines.buildFocusOutline(thickness); + return outlines; } static async deserialize(data, parent, uiManager) { let initialData = null; @@ -22046,7 +22873,6 @@ class HighlightEditor extends AnnotationEditor { color: Array.from(color), opacity, quadPoints, - boxes: null, pageIndex: pageNumber - 1, rect: rect.slice(0), rotation, @@ -22087,7 +22913,6 @@ class HighlightEditor extends AnnotationEditor { color: Array.from(color), thickness, inkLists, - boxes: null, pageIndex: pageNumber - 1, rect: rect.slice(0), rotation, @@ -22101,77 +22926,11 @@ class HighlightEditor extends AnnotationEditor { modificationDate }; } - const { - color, - quadPoints, - inkLists, - outlines, - opacity - } = data; const editor = await super.deserialize(data, parent, uiManager); - editor.color = Util.makeHexColor(...color); - editor.opacity = opacity || 1; - if (inkLists) { - editor.#thickness = data.thickness; - } editor._initialData = initialData; if (data.comment) { editor.setCommentData(data); } - const [pageWidth, pageHeight] = editor.pageDimensions; - const [pageX, pageY] = editor.pageTranslation; - if (quadPoints) { - const boxes = editor.#boxes = []; - for (let i = 0; i < quadPoints.length; i += 8) { - boxes.push({ - x: (quadPoints[i] - pageX) / pageWidth, - y: 1 - (quadPoints[i + 1] - pageY) / pageHeight, - width: (quadPoints[i + 2] - quadPoints[i]) / pageWidth, - height: (quadPoints[i + 1] - quadPoints[i + 5]) / pageHeight - }); - } - editor.#createOutlines(); - editor.#addToDrawLayer(); - editor.rotate(editor.rotation); - } else if (inkLists || outlines) { - editor.#isFreeHighlight = true; - const points = (inkLists || outlines.points)[0]; - const point = { - x: points[0] - pageX, - y: pageHeight - (points[1] - pageY) - }; - const outliner = new FreeHighlightOutliner(point, [0, 0, pageWidth, pageHeight], 1, editor.#thickness / 2, true, 0.001); - for (let i = 0, ii = points.length; i < ii; i += 2) { - point.x = points[i] - pageX; - point.y = pageHeight - (points[i + 1] - pageY); - outliner.add(point); - } - const { - id, - clipPathId - } = parent.drawLayer.draw({ - bbox: [0, 0, 1, 1], - root: { - viewBox: "0 0 1 1", - fill: editor.color, - "fill-opacity": editor._defaultOpacity - }, - rootClass: { - highlight: true, - free: true - }, - path: { - d: outliner.toSVGPath() - } - }, true, true); - editor.#createFreeOutlines({ - highlightOutlines: outliner.getOutlines(), - highlightId: id, - clipPathId - }); - editor.#addToDrawLayer(); - editor.rotate(editor.parentRotation); - } return editor; } serialize(isForCopying = false) { @@ -22181,14 +22940,13 @@ class HighlightEditor extends AnnotationEditor { if (this.deleted) { return this.serializeDeleted(); } - const color = AnnotationEditor._colorManager.convert(this._uiManager.getNonHCMColor(this.color)); const serialized = super.serialize(isForCopying); Object.assign(serialized, { - color, + color: AnnotationEditor._colorManager.convert(this._uiManager.getNonHCMColor(this.color)), opacity: this.opacity, - thickness: this.#thickness, - quadPoints: this.#serializeBoxes(), - outlines: this.#serializeOutlines(serialized.rect) + thickness: this._drawingOptions.thickness, + quadPoints: this._drawOutlines.serializeQuadPoints(this.pageTranslation, this.pageDimensions), + outlines: this._drawOutlines.serialize(serialized.rect, this._drawRotation) }); this.addComment(serialized); if (this.annotationElementId && !this.#hasElementChanged(serialized)) { @@ -22214,673 +22972,6 @@ class HighlightEditor extends AnnotationEditor { }); return null; } - static canCreateNewEmptyEditor() { - return false; - } -} - -;// ./src/display/editor/draw.js - - - - -class DrawingOptions { - #svgProperties = Object.create(null); - updateProperty(name, value) { - this[name] = value; - this.updateSVGProperty(name, value); - } - updateProperties(properties) { - if (!properties) { - return; - } - for (const [name, value] of Object.entries(properties)) { - if (!name.startsWith("_")) { - this.updateProperty(name, value); - } - } - } - updateSVGProperty(name, value) { - this.#svgProperties[name] = value; - } - toSVGProperties() { - const root = this.#svgProperties; - this.#svgProperties = Object.create(null); - return { - root - }; - } - reset() { - this.#svgProperties = Object.create(null); - } - updateAll(options = this) { - this.updateProperties(options); - } - clone() { - unreachable("Not implemented"); - } -} -class DrawingEditor extends AnnotationEditor { - #drawOutlines = null; - #mustBeCommitted; - _colorPicker = null; - _drawId = null; - static _currentDrawId = -1; - static _currentParent = null; - static #currentDraw = null; - static #currentDrawingAC = null; - static #currentDrawingOptions = null; - static _INNER_MARGIN = 3; - constructor(params) { - super(params); - this.#mustBeCommitted = params.mustBeCommitted || false; - this._addOutlines(params); - } - onUpdatedColor() { - this._colorPicker?.update(this.color); - super.onUpdatedColor(); - } - onUpdatedOpacity() { - this._colorPicker?.updateOpacity?.(this.opacity); - } - _addOutlines(params) { - if (params.drawOutlines) { - this.#createDrawOutlines(params); - this.#addToDrawLayer(); - } - } - #createDrawOutlines({ - drawOutlines, - drawId, - drawingOptions - }) { - this.#drawOutlines = drawOutlines; - this._drawingOptions ||= drawingOptions; - if (!this.annotationElementId) { - this._uiManager.a11yAlert(AnnotationEditor._l10nAlert[this.editorType]); - } - if (drawId >= 0) { - this._drawId = drawId; - this.parent.drawLayer.finalizeDraw(drawId, drawOutlines.defaultProperties); - } else { - this._drawId = this.#createDrawing(drawOutlines, this.parent); - } - this.#updateBbox(drawOutlines.box); - } - #createDrawing(drawOutlines, parent) { - const { - id - } = parent.drawLayer.draw(DrawingEditor._mergeSVGProperties(this._drawingOptions.toSVGProperties(), drawOutlines.defaultSVGProperties), false, false); - return id; - } - static _mergeSVGProperties(p1, p2) { - const p1Keys = new Set(Object.keys(p1)); - for (const [key, value] of Object.entries(p2)) { - if (p1Keys.has(key)) { - Object.assign(p1[key], value); - } else { - p1[key] = value; - } - } - return p1; - } - static getDefaultDrawingOptions(_options) { - unreachable("Not implemented"); - } - static get typesMap() { - unreachable("Not implemented"); - } - static get isDrawer() { - return true; - } - static get supportMultipleDrawings() { - return false; - } - static updateDefaultParams(type, value) { - const propertyName = this.typesMap.get(type); - if (propertyName) { - this._defaultDrawingOptions.updateProperty(propertyName, value); - } - if (this._currentParent) { - DrawingEditor.#currentDraw.updateProperty(propertyName, value); - this._currentParent.drawLayer.updateProperties(this._currentDrawId, this._defaultDrawingOptions.toSVGProperties()); - } - } - updateParams(type, value) { - const propertyName = this.constructor.typesMap.get(type); - if (propertyName) { - this._updateProperty(type, propertyName, value); - } - } - static get defaultPropertiesToUpdate() { - const properties = []; - const options = this._defaultDrawingOptions; - for (const [type, name] of this.typesMap) { - properties.push([type, options[name]]); - } - return properties; - } - get propertiesToUpdate() { - const properties = []; - const { - _drawingOptions - } = this; - for (const [type, name] of this.constructor.typesMap) { - properties.push([type, _drawingOptions[name]]); - } - return properties; - } - _updateProperty(type, name, value) { - const options = this._drawingOptions; - const savedValue = options[name]; - const setter = val => { - options.updateProperty(name, val); - const bbox = this.#drawOutlines.updateProperty(name, val); - if (bbox) { - this.#updateBbox(bbox); - } - this.parent?.drawLayer.updateProperties(this._drawId, options.toSVGProperties()); - if (type === this.colorType) { - this.onUpdatedColor(); - } else if (type === this.opacityType) { - this.onUpdatedOpacity(); - } - }; - this.addCommands({ - cmd: setter.bind(this, value), - undo: setter.bind(this, savedValue), - post: this._uiManager.updateUI.bind(this._uiManager, this), - mustExec: true, - type, - overwriteIfSameType: true, - keepUndo: true - }); - } - _updateColorAndOpacity(color, opacity) { - const colorName = this.constructor.typesMap.get(this.colorType); - const opacityName = this.constructor.typesMap.get(this.opacityType); - const options = this._drawingOptions; - const savedColor = options[colorName]; - const savedOpacity = options[opacityName]; - const setter = (c, op) => { - options.updateProperty(colorName, c); - options.updateProperty(opacityName, op); - this.#drawOutlines.updateProperty(colorName, c); - this.#drawOutlines.updateProperty(opacityName, op); - this.parent?.drawLayer.updateProperties(this._drawId, options.toSVGProperties()); - this.onUpdatedColor(); - this.onUpdatedOpacity(); - }; - this.addCommands({ - cmd: setter.bind(this, color, opacity), - undo: setter.bind(this, savedColor, savedOpacity), - post: this._uiManager.updateUI.bind(this._uiManager, this), - mustExec: true, - type: AnnotationEditorParamsType.INK_COLOR_AND_OPACITY, - overwriteIfSameType: true, - keepUndo: true - }); - } - _onResizing() { - this.parent?.drawLayer.updateProperties(this._drawId, DrawingEditor._mergeSVGProperties(this.#drawOutlines.getPathResizingSVGProperties(this.#convertToDrawSpace()), { - bbox: this.#rotateBox() - })); - } - _onResized() { - this.parent?.drawLayer.updateProperties(this._drawId, DrawingEditor._mergeSVGProperties(this.#drawOutlines.getPathResizedSVGProperties(this.#convertToDrawSpace()), { - bbox: this.#rotateBox() - })); - } - _onTranslating(_x, _y) { - this.parent?.drawLayer.updateProperties(this._drawId, { - bbox: this.#rotateBox() - }); - } - _onTranslated() { - this.parent?.drawLayer.updateProperties(this._drawId, DrawingEditor._mergeSVGProperties(this.#drawOutlines.getPathTranslatedSVGProperties(this.#convertToDrawSpace(), this.parentDimensions), { - bbox: this.#rotateBox() - })); - } - _onStartDragging() { - this.parent?.drawLayer.updateProperties(this._drawId, { - rootClass: { - moving: true - } - }); - } - _onStopDragging() { - this.parent?.drawLayer.updateProperties(this._drawId, { - rootClass: { - moving: false - } - }); - } - commit() { - super.commit(); - this.disableEditMode(); - this.disableEditing(); - } - disableEditing() { - super.disableEditing(); - this.div.classList.toggle("disabled", true); - } - enableEditing() { - super.enableEditing(); - this.div.classList.toggle("disabled", false); - } - getBaseTranslation() { - return [0, 0]; - } - get isResizable() { - return true; - } - onceAdded(focus) { - if (!this.annotationElementId) { - this.parent.addUndoableEditor(this); - } - this._isDraggable = true; - if (this.#mustBeCommitted) { - this.#mustBeCommitted = false; - this.commit(); - this.parent.setSelected(this); - if (focus && this.isOnScreen) { - this.div.focus(); - } - } - } - remove() { - this.#cleanDrawLayer(); - super.remove(); - } - rebuild() { - if (!this.parent) { - return; - } - super.rebuild(); - if (this.div === null) { - return; - } - this.#addToDrawLayer(); - this.#updateBbox(this.#drawOutlines.box); - if (!this.isAttachedToDOM) { - this.parent.add(this); - } - } - setParent(parent) { - let mustBeSelected = false; - if (this.parent && !parent) { - this._uiManager.removeShouldRescale(this); - this.#cleanDrawLayer(); - } else if (parent) { - this._uiManager.addShouldRescale(this); - this.#addToDrawLayer(parent); - mustBeSelected = !this.parent && this.div?.classList.contains("selectedEditor"); - } - super.setParent(parent); - if (mustBeSelected) { - this.select(); - } - } - #cleanDrawLayer() { - if (this._drawId === null || !this.parent) { - return; - } - this.parent.drawLayer.remove(this._drawId); - this._drawId = null; - this._drawingOptions.reset(); - } - #addToDrawLayer(parent = this.parent) { - if (this._drawId !== null && this.parent === parent) { - return; - } - if (this._drawId !== null) { - this.parent.drawLayer.updateParent(this._drawId, parent.drawLayer); - return; - } - this._drawingOptions.updateAll(); - this._drawId = this.#createDrawing(this.#drawOutlines, parent); - } - #convertToParentSpace([x, y, width, height]) { - const { - parentDimensions: [pW, pH], - rotation - } = this; - switch (rotation) { - case 90: - return [y, 1 - x, width * (pH / pW), height * (pW / pH)]; - case 180: - return [1 - x, 1 - y, width, height]; - case 270: - return [1 - y, x, width * (pH / pW), height * (pW / pH)]; - default: - return [x, y, width, height]; - } - } - #convertToDrawSpace() { - const { - x, - y, - width, - height, - parentDimensions: [pW, pH], - rotation - } = this; - switch (rotation) { - case 90: - return [1 - y, x, width * (pW / pH), height * (pH / pW)]; - case 180: - return [1 - x, 1 - y, width, height]; - case 270: - return [y, 1 - x, width * (pW / pH), height * (pH / pW)]; - default: - return [x, y, width, height]; - } - } - #updateBbox(bbox) { - [this.x, this.y, this.width, this.height] = this.#convertToParentSpace(bbox); - if (this.div) { - this.fixAndSetPosition(); - this.setDims(); - } - this._onResized(); - } - #rotateBox() { - const { - x, - y, - width, - height, - rotation, - parentRotation, - parentDimensions: [pW, pH] - } = this; - switch ((rotation * 4 + parentRotation) / 90) { - case 1: - return [1 - y - height, x, height, width]; - case 2: - return [1 - x - width, 1 - y - height, width, height]; - case 3: - return [y, 1 - x - width, height, width]; - case 4: - return [x, y - width * (pW / pH), height * (pH / pW), width * (pW / pH)]; - case 5: - return [1 - y, x, width * (pW / pH), height * (pH / pW)]; - case 6: - return [1 - x - height * (pH / pW), 1 - y, height * (pH / pW), width * (pW / pH)]; - case 7: - return [y - width * (pW / pH), 1 - x - height * (pH / pW), width * (pW / pH), height * (pH / pW)]; - case 8: - return [x - width, y - height, width, height]; - case 9: - return [1 - y, x - width, height, width]; - case 10: - return [1 - x, 1 - y, width, height]; - case 11: - return [y - height, 1 - x, height, width]; - case 12: - return [x - height * (pH / pW), y, height * (pH / pW), width * (pW / pH)]; - case 13: - return [1 - y - width * (pW / pH), x - height * (pH / pW), width * (pW / pH), height * (pH / pW)]; - case 14: - return [1 - x, 1 - y - width * (pW / pH), height * (pH / pW), width * (pW / pH)]; - case 15: - return [y, 1 - x, width * (pW / pH), height * (pH / pW)]; - default: - return [x, y, width, height]; - } - } - rotate() { - if (!this.parent) { - return; - } - this.parent.drawLayer.updateProperties(this._drawId, DrawingEditor._mergeSVGProperties({ - bbox: this.#rotateBox() - }, this.#drawOutlines.updateRotation((this.parentRotation - this.rotation + 360) % 360))); - } - onScaleChanging() { - if (!this.parent) { - return; - } - this.#updateBbox(this.#drawOutlines.updateParentDimensions(this.parentDimensions, this.parent.scale)); - } - static onScaleChangingWhenDrawing() {} - render() { - if (this.div) { - return this.div; - } - let baseX, baseY; - if (this._isCopy) { - baseX = this.x; - baseY = this.y; - } - const div = super.render(); - div.classList.add("draw"); - const drawDiv = document.createElement("div"); - div.append(drawDiv); - drawDiv.setAttribute("aria-hidden", "true"); - drawDiv.className = "internal"; - this.setDims(); - this._uiManager.addShouldRescale(this); - this.disableEditing(); - if (this._isCopy) { - this._moveAfterPaste(baseX, baseY); - } - return div; - } - static createDrawerInstance(_x, _y, _parentWidth, _parentHeight, _rotation) { - unreachable("Not implemented"); - } - static startDrawing(parent, uiManager, _isLTR, event) { - const { - target, - offsetX: x, - offsetY: y, - pointerId, - pointerType - } = event; - if (CurrentPointers.isInitializedAndDifferentPointerType(pointerType)) { - return; - } - const { - viewport: { - rotation - } - } = parent; - const { - width: parentWidth, - height: parentHeight - } = target.getBoundingClientRect(); - const ac = DrawingEditor.#currentDrawingAC = new AbortController(); - const signal = parent.combinedSignal(ac); - CurrentPointers.setPointer(pointerType, pointerId); - window.addEventListener("pointerup", e => { - if (CurrentPointers.isSamePointerIdOrRemove(e.pointerId)) { - this._endDraw(e); - } - }, { - signal - }); - window.addEventListener("pointercancel", e => { - if (CurrentPointers.isSamePointerIdOrRemove(e.pointerId)) { - this._currentParent.endDrawingSession(); - } - }, { - signal - }); - window.addEventListener("pointerdown", e => { - if (!CurrentPointers.isSamePointerType(e.pointerType)) { - return; - } - CurrentPointers.initializeAndAddPointerId(e.pointerId); - if (DrawingEditor.#currentDraw.isCancellable()) { - DrawingEditor.#currentDraw.removeLastElement(); - if (DrawingEditor.#currentDraw.isEmpty()) { - this._currentParent.endDrawingSession(true); - } else { - this._endDraw(null); - } - } - }, { - capture: true, - passive: false, - signal - }); - window.addEventListener("contextmenu", noContextMenu, { - signal - }); - target.addEventListener("pointermove", this._drawMove.bind(this), { - signal - }); - target.addEventListener("touchmove", e => { - if (CurrentPointers.isSameTimeStamp(e.timeStamp)) { - stopEvent(e); - } - }, { - signal - }); - parent.toggleDrawing(); - uiManager._editorUndoBar?.hide(); - if (DrawingEditor.#currentDraw) { - parent.drawLayer.updateProperties(this._currentDrawId, DrawingEditor.#currentDraw.startNew(x, y, parentWidth, parentHeight, rotation)); - return; - } - uiManager.updateUIForDefaultProperties(this); - DrawingEditor.#currentDraw = this.createDrawerInstance(x, y, parentWidth, parentHeight, rotation); - DrawingEditor.#currentDrawingOptions = this.getDefaultDrawingOptions(); - this._currentParent = parent; - ({ - id: this._currentDrawId - } = parent.drawLayer.draw(this._mergeSVGProperties(DrawingEditor.#currentDrawingOptions.toSVGProperties(), DrawingEditor.#currentDraw.defaultSVGProperties), true, false)); - } - static _drawMove(event) { - CurrentPointers.isSameTimeStamp(event.timeStamp); - if (!DrawingEditor.#currentDraw) { - return; - } - const { - offsetX, - offsetY, - pointerId - } = event; - if (!CurrentPointers.isSamePointerId(pointerId)) { - return; - } - if (CurrentPointers.isUsingMultiplePointers()) { - this._endDraw(event); - return; - } - this._currentParent.drawLayer.updateProperties(this._currentDrawId, DrawingEditor.#currentDraw.add(offsetX, offsetY)); - CurrentPointers.setTimeStamp(event.timeStamp); - stopEvent(event); - } - static _cleanup(all) { - if (all) { - this._currentDrawId = -1; - this._currentParent = null; - DrawingEditor.#currentDraw = null; - DrawingEditor.#currentDrawingOptions = null; - CurrentPointers.clearTimeStamp(); - } - if (DrawingEditor.#currentDrawingAC) { - DrawingEditor.#currentDrawingAC.abort(); - DrawingEditor.#currentDrawingAC = null; - CurrentPointers.clearPointerIds(); - } - } - static _endDraw(event) { - const parent = this._currentParent; - if (!parent) { - return; - } - parent.toggleDrawing(true); - this._cleanup(false); - if (event?.target === parent.div) { - parent.drawLayer.updateProperties(this._currentDrawId, DrawingEditor.#currentDraw.end(event.offsetX, event.offsetY)); - } - if (this.supportMultipleDrawings) { - const draw = DrawingEditor.#currentDraw; - const drawId = this._currentDrawId; - const lastElement = draw.getLastElement(); - parent.addCommands({ - cmd: () => { - parent.drawLayer.updateProperties(drawId, draw.setLastElement(lastElement)); - }, - undo: () => { - parent.drawLayer.updateProperties(drawId, draw.removeLastElement()); - }, - mustExec: false, - type: AnnotationEditorParamsType.DRAW_STEP - }); - return; - } - this.endDrawing(false); - } - static endDrawing(isAborted) { - const parent = this._currentParent; - if (!parent) { - return null; - } - parent.toggleDrawing(true); - parent.cleanUndoStack(AnnotationEditorParamsType.DRAW_STEP); - if (!DrawingEditor.#currentDraw.isEmpty()) { - const { - pageDimensions: [pageWidth, pageHeight], - scale - } = parent; - const editor = parent.createAndAddNewEditor({ - offsetX: 0, - offsetY: 0 - }, false, { - drawId: this._currentDrawId, - drawOutlines: DrawingEditor.#currentDraw.getOutlines(pageWidth * scale, pageHeight * scale, scale, this._INNER_MARGIN), - drawingOptions: DrawingEditor.#currentDrawingOptions, - mustBeCommitted: !isAborted - }); - this._cleanup(true); - return editor; - } - parent.drawLayer.remove(this._currentDrawId); - this._cleanup(true); - return null; - } - createDrawingOptions(_data) {} - static deserializeDraw(_pageX, _pageY, _pageWidth, _pageHeight, _innerWidth, _data) { - unreachable("Not implemented"); - } - static async deserialize(data, parent, uiManager) { - const { - rawDims: { - pageWidth, - pageHeight, - pageX, - pageY - } - } = parent.viewport; - const drawOutlines = this.deserializeDraw(pageX, pageY, pageWidth, pageHeight, this._INNER_MARGIN, data); - const editor = await super.deserialize(data, parent, uiManager); - editor.createDrawingOptions(data); - editor.#createDrawOutlines({ - drawOutlines - }); - editor.#addToDrawLayer(); - editor.onScaleChanging(); - editor.rotate(); - return editor; - } - serializeDraw(isForCopying) { - const [pageX, pageY] = this.pageTranslation; - const [pageWidth, pageHeight] = this.pageDimensions; - return this.#drawOutlines.serialize([pageX, pageY, pageWidth, pageHeight], isForCopying); - } - renderAnnotationElement(annotation) { - annotation.updateEdited({ - rect: this.getPDFRect() - }); - return null; - } - static canCreateNewEmptyEditor() { - return false; - } } ;// ./src/display/editor/drawers/inkdraw.js @@ -22889,6 +22980,7 @@ class DrawingEditor extends AnnotationEditor { class InkDrawOutliner { #last = new Float64Array(6); + #tip = new Float64Array(2); #line; #lines; #rotation; @@ -22912,6 +23004,7 @@ class InkDrawOutliner { points: this.#points }]; this.#last.set(line, 0); + this.#tip.set([x, y], 0); } updateProperty(name, value) { if (name === "stroke-width") { @@ -22928,39 +23021,66 @@ class InkDrawOutliner { return this.#points.length <= 10; } add(x, y) { + if (this.#add(x, y)) { + this.toSVGPath(); + } + return { + path: { + d: this.#toSVGPathWithTip() + } + }; + } + addPoints(points) { + let needsPathUpdate = false; + for (let i = 0, ii = points.length; i < ii; i += 2) { + if (!this.#add(points[i], points[i + 1])) { + continue; + } + needsPathUpdate = true; + if (this.#points.length <= 6) { + this.toSVGPath(); + needsPathUpdate = false; + } + } + if (needsPathUpdate) { + this.toSVGPath(); + } + return { + path: { + d: this.#toSVGPathWithTip() + } + }; + } + #add(x, y) { [x, y] = this.#normalizePoint(x, y); + this.#tip.set([x, y], 0); const [x1, y1, x2, y2] = this.#last.subarray(2, 6); const diffX = x - x2; const diffY = y - y2; const d = Math.hypot(this.#parentWidth * diffX, this.#parentHeight * diffY); if (d <= 2) { - return null; + return false; } this.#points.push(x, y); if (isNaN(x1)) { this.#last.set([x2, y2, x, y], 2); this.#line.push(NaN, NaN, NaN, NaN, x, y); - return { - path: { - d: this.toSVGPath() - } - }; + return true; } if (isNaN(this.#last[0])) { this.#line.splice(6, 6); } this.#last.set([x1, y1, x2, y2, x, y], 0); this.#line.push(...Outline.createBezierPoints(x1, y1, x2, y2, x, y)); - return { - path: { - d: this.toSVGPath() - } - }; + return true; } end(x, y) { - const change = this.add(x, y); - if (change) { - return change; + if (x !== undefined && this.#add(x, y)) { + return { + path: { + d: this.toSVGPath() + } + }; } if (this.#points.length === 2) { return { @@ -22969,7 +23089,11 @@ class InkDrawOutliner { } }; } - return null; + return { + path: { + d: this.#lastSVGPath + } + }; } startNew(x, y, parentWidth, parentHeight, rotation) { this.#parentWidth = parentWidth; @@ -22978,6 +23102,7 @@ class InkDrawOutliner { [x, y] = this.#normalizePoint(x, y); const line = this.#line = [NaN, NaN, NaN, NaN, x, y]; this.#points = [x, y]; + this.#tip.set([x, y], 0); const last = this.#lines.at(-1); if (last) { last.line = new Float32Array(last.line); @@ -23031,6 +23156,16 @@ class InkDrawOutliner { } }; } + #toSVGPathWithTip() { + const tipX = Outline.svgRound(this.#tip[0]); + const tipY = Outline.svgRound(this.#tip[1]); + if (this.#points.length === 2) { + const firstX = Outline.svgRound(this.#line[4]); + const firstY = Outline.svgRound(this.#line[5]); + return `${this.#lastSVGPath} M ${firstX} ${firstY} L ${tipX} ${tipY}`; + } + return `${this.#lastSVGPath} L ${tipX} ${tipY}`; + } toSVGPath() { const firstX = Outline.svgRound(this.#line[4]); const firstY = Outline.svgRound(this.#line[5]); @@ -23603,8 +23738,13 @@ class InkEditor extends DrawingEditor { static get typesMap() { return shadow(this, "typesMap", new Map([[AnnotationEditorParamsType.INK_THICKNESS, "stroke-width"], [AnnotationEditorParamsType.INK_COLOR, "stroke"], [AnnotationEditorParamsType.INK_OPACITY, "stroke-opacity"]])); } - static createDrawerInstance(x, y, parentWidth, parentHeight, rotation) { - return new InkDrawOutliner(x, y, parentWidth, parentHeight, rotation, this._defaultDrawingOptions["stroke-width"]); + static createDrawerInstance({ + x, + y, + box: [,, width, height], + rotation + }) { + return new InkDrawOutliner(x, y, width, height, rotation, this._defaultDrawingOptions["stroke-width"]); } static deserializeDraw(pageX, pageY, pageWidth, pageHeight, innerMargin, data) { return InkDrawOutline.deserialize(pageX, pageY, pageWidth, pageHeight, innerMargin, data); @@ -25809,20 +25949,7 @@ class AnnotationEditorLayer { return; } this.#uiManager.showAllEditors("highlight", true, true); - this.#textLayer.div.classList.add("free"); - this.toggleDrawing(); - HighlightEditor.startHighlighting(this, this.#uiManager.direction === "ltr", { - target: this.#textLayer.div, - x: event.x, - y: event.y - }); - this.#textLayer.div.addEventListener("pointerup", () => { - this.#textLayer.div.classList.remove("free"); - this.toggleDrawing(true); - }, { - once: true, - signal: this.#uiManager._signal - }); + HighlightEditor.startDrawing(this, this.#uiManager, this.#uiManager.direction === "ltr", event); event.preventDefault(); } } @@ -26997,9 +27124,8 @@ globalThis.pdfjsLib = { updateUrlHash: updateUrlHash, Util: Util, VerbosityLevel: VerbosityLevel, - version: (/* inlined export .version */"6.3.72"), + version: version, XfaLayer: XfaLayer }; -const __webpack_exports__version = (/* inlined export .version */"6.3.72"); -export { AbortException, AnnotationEditorLayer, AnnotationEditorParamsType, AnnotationEditorType, AnnotationEditorUIManager, AnnotationLayer, AnnotationMode, AnnotationType, CSSConstants, ColorPicker, DOMSVGFactory, DrawLayer, FeatureTest, GlobalWorkerOptions, ImageKind, InvalidPDFException, MathClamp, OPS, OutputScale, PDFDataRangeTransport, PDFDateString, PDFWorker, PasswordException, PasswordResponses, PermissionFlag, PixelsPerInch, RenderingCancelledException, ResponseException, SignatureExtractor, SupportedImageMimeTypes, TextLayer, TextLayerImages, TouchManager, Util, VerbosityLevel, XfaLayer, applyOpacity, build, createValidAbsoluteUrl, fetchData, findContrastColor, getDocument, getFilenameFromUrl, getPdfFilenameFromUrl, getRGB, getRGBA, getUuid, isDataScheme, isPdfFile, isValidExplicitDest, makeArr, makeMap, makeObj, makeSet, noContextMenu, normalizeUnicode, renderRichText, setLayerDimensions, shadow, stopEvent, updateUrlHash, __webpack_exports__version as version }; +export { AbortException, AnnotationEditorLayer, AnnotationEditorParamsType, AnnotationEditorType, AnnotationEditorUIManager, AnnotationLayer, AnnotationMode, AnnotationType, CSSConstants, ColorPicker, DOMSVGFactory, DrawLayer, FeatureTest, GlobalWorkerOptions, ImageKind, InvalidPDFException, MathClamp, OPS, OutputScale, PDFDataRangeTransport, PDFDateString, PDFWorker, PasswordException, PasswordResponses, PermissionFlag, PixelsPerInch, RenderingCancelledException, ResponseException, SignatureExtractor, SupportedImageMimeTypes, TextLayer, TextLayerImages, TouchManager, Util, VerbosityLevel, XfaLayer, applyOpacity, build, createValidAbsoluteUrl, fetchData, findContrastColor, getDocument, getFilenameFromUrl, getPdfFilenameFromUrl, getRGB, getRGBA, getUuid, isDataScheme, isPdfFile, isValidExplicitDest, makeArr, makeMap, makeObj, makeSet, noContextMenu, normalizeUnicode, renderRichText, setLayerDimensions, shadow, stopEvent, updateUrlHash, version }; diff --git a/toolkit/components/pdfjs/content/build/pdf.scripting.mjs b/toolkit/components/pdfjs/content/build/pdf.scripting.mjs index cf639a8e36f9..51377cf9ba7f 100644 --- a/toolkit/components/pdfjs/content/build/pdf.scripting.mjs +++ b/toolkit/components/pdfjs/content/build/pdf.scripting.mjs @@ -21,8 +21,8 @@ */ /** - * pdfjsVersion = 6.3.72 - * pdfjsBuild = 71a3c6a89 + * pdfjsVersion = 6.3.183 + * pdfjsBuild = 48bb93b89 */ ;// ./src/scripting_api/constants.js @@ -451,7 +451,7 @@ class Field extends PDFObject { if (!Array.isArray(indices)) { indices = [indices]; } - if (!indices.every(i => typeof i === "number" && Number.isInteger(i) && i >= 0 && i < this.numItems)) { + if (!indices.every(i => Number.isInteger(i) && i >= 0 && i < this.numItems)) { return; } indices.sort(); @@ -3301,14 +3301,16 @@ class ProxyHandler { ;// ./src/scripting_api/util.js class Util extends PDFObject { + #createDateActionsBound = this.#createDateActions.bind(this); + #createScandDataBound = this.#createScandData.bind(this); #dateActionsCache = null; + #scandCache = null; + #months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; + #days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; + MILLISECONDS_IN_DAY = 86400000; + MILLISECONDS_IN_WEEK = 604800000; constructor(data) { super(data); - this._scandCache = new Map(); - this._months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; - this._days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; - this.MILLISECONDS_IN_DAY = 86400000; - this.MILLISECONDS_IN_WEEK = 604800000; this._externalCall = data.externalCall; } printf(...args) { @@ -3324,7 +3326,7 @@ class Util extends PDFObject { const ZERO = 4; const HASH = 8; let i = 0; - return args[0].replaceAll(pattern, function (match, nDecSep, cFlags, nWidth, nPrecision, cConvChar) { + return args[0].replaceAll(pattern, function (_, nDecSep, cFlags, nWidth, nPrecision, cConvChar) { if (cConvChar !== "d" && cConvChar !== "f" && cConvChar !== "s" && cConvChar !== "x") { const buf = ["%"]; for (const str of [nDecSep, cFlags, nWidth, nPrecision, cConvChar]) { @@ -3442,12 +3444,12 @@ class Util extends PDFObject { return this.printd("m/d/yy h:MM:ss tt", oDate); } const handlers = { - mmmm: data => this._months[data.month], - mmm: data => this._months[data.month].substring(0, 3), + mmmm: data => this.#months[data.month], + mmm: data => this.#months[data.month].substring(0, 3), mm: data => (data.month + 1).toString().padStart(2, "0"), m: data => (data.month + 1).toString(), - dddd: data => this._days[data.dayOfWeek], - ddd: data => this._days[data.dayOfWeek].substring(0, 3), + dddd: data => this.#days[data.dayOfWeek], + ddd: data => this.#days[data.dayOfWeek].substring(0, 3), dd: data => data.day.toString().padStart(2, "0"), d: data => data.day.toString(), yyyy: data => data.year.toString().padStart(4, "0"), @@ -3473,7 +3475,7 @@ class Util extends PDFObject { seconds: oDate.getSeconds() }; const patterns = /(mmmm|mmm|mm|m|dddd|ddd|dd|d|yyyy|yy|HH|H|hh|h|MM|M|ss|s|tt|t|\\.)/g; - return cFormat.replaceAll(patterns, function (match, pattern) { + return cFormat.replaceAll(patterns, function (_, pattern) { return pattern in handlers ? handlers[pattern](data) : pattern.charCodeAt(1); }); } @@ -3548,66 +3550,66 @@ class Util extends PDFObject { } return buf.join(""); } - #tryToGuessDate(cFormat, cDate) { - let actions = (this.#dateActionsCache ||= new Map()).get(cFormat); - if (!actions) { - actions = []; - this.#dateActionsCache.set(cFormat, actions); - cFormat.replaceAll(/(d+)|(m+)|(y+)|(H+)|(M+)|(s+)/g, function (_match, d, m, y, H, M, s) { - if (d) { - actions.push((n, data) => { - if (n >= 1 && n <= 31) { - data.day = n; - return true; - } - return false; - }); - } else if (m) { - actions.push((n, data) => { - if (n >= 1 && n <= 12) { - data.month = n - 1; - return true; - } - return false; - }); - } else if (y) { - actions.push((n, data) => { - if (n < 50) { - n += 2000; - } else if (n < 100) { - n += 1900; - } - data.year = n; + #createDateActions(cFormat) { + const actions = []; + cFormat.replaceAll(/(d+)|(m+)|(y+)|(H+)|(M+)|(s+)/g, function (_, d, m, y, H, M, s) { + if (d) { + actions.push((n, data) => { + if (n >= 1 && n <= 31) { + data.day = n; return true; - }); - } else if (H) { - actions.push((n, data) => { - if (n >= 0 && n <= 23) { - data.hours = n; - return true; - } - return false; - }); - } else if (M) { - actions.push((n, data) => { - if (n >= 0 && n <= 59) { - data.minutes = n; - return true; - } - return false; - }); - } else if (s) { - actions.push((n, data) => { - if (n >= 0 && n <= 59) { - data.seconds = n; - return true; - } - return false; - }); - } - return ""; - }); - } + } + return false; + }); + } else if (m) { + actions.push((n, data) => { + if (n >= 1 && n <= 12) { + data.month = n - 1; + return true; + } + return false; + }); + } else if (y) { + actions.push((n, data) => { + if (n < 50) { + n += 2000; + } else if (n < 100) { + n += 1900; + } + data.year = n; + return true; + }); + } else if (H) { + actions.push((n, data) => { + if (n >= 0 && n <= 23) { + data.hours = n; + return true; + } + return false; + }); + } else if (M) { + actions.push((n, data) => { + if (n >= 0 && n <= 59) { + data.minutes = n; + return true; + } + return false; + }); + } else if (s) { + actions.push((n, data) => { + if (n >= 0 && n <= 59) { + data.seconds = n; + return true; + } + return false; + }); + } + return ""; + }); + return actions; + } + #tryToGuessDate(cFormat, cDate) { + const actions = (this.#dateActionsCache ??= new Map()).getOrInsertComputed(cFormat, this.#createDateActionsBound); const number = /\d+/g; let i = 0; let array; @@ -3636,6 +3638,145 @@ class Util extends PDFObject { scand(cFormat, cDate) { return this._scand(cFormat, cDate); } + #createScandData(cFormat) { + const months = this.#months, + days = this.#days; + const handlers = { + mmmm: { + pattern: `(${months.join("|")})`, + action: (value, data) => { + data.month = months.indexOf(value); + } + }, + mmm: { + pattern: `(${months.map(month => month.substring(0, 3)).join("|")})`, + action: (value, data) => { + data.month = months.findIndex(month => month.substring(0, 3) === value); + } + }, + mm: { + pattern: `(\\d{2})`, + action: (value, data) => { + data.month = parseInt(value) - 1; + } + }, + m: { + pattern: `(\\d{1,2})`, + action: (value, data) => { + data.month = parseInt(value) - 1; + } + }, + dddd: { + pattern: `(${days.join("|")})`, + action: (value, data) => { + data.day = days.indexOf(value); + } + }, + ddd: { + pattern: `(${days.map(day => day.substring(0, 3)).join("|")})`, + action: (value, data) => { + data.day = days.findIndex(day => day.substring(0, 3) === value); + } + }, + dd: { + pattern: "(\\d{2})", + action: (value, data) => { + data.day = parseInt(value); + } + }, + d: { + pattern: "(\\d{1,2})", + action: (value, data) => { + data.day = parseInt(value); + } + }, + yyyy: { + pattern: "(\\d{4})", + action: (value, data) => { + data.year = parseInt(value); + } + }, + yy: { + pattern: "(\\d{2})", + action: (value, data) => { + data.year = 2000 + parseInt(value); + } + }, + HH: { + pattern: "(\\d{2})", + action: (value, data) => { + data.hours = parseInt(value); + } + }, + H: { + pattern: "(\\d{1,2})", + action: (value, data) => { + data.hours = parseInt(value); + } + }, + hh: { + pattern: "(\\d{2})", + action: (value, data) => { + data.hours = parseInt(value); + } + }, + h: { + pattern: "(\\d{1,2})", + action: (value, data) => { + data.hours = parseInt(value); + } + }, + MM: { + pattern: "(\\d{2})", + action: (value, data) => { + data.minutes = parseInt(value); + } + }, + M: { + pattern: "(\\d{1,2})", + action: (value, data) => { + data.minutes = parseInt(value); + } + }, + ss: { + pattern: "(\\d{2})", + action: (value, data) => { + data.seconds = parseInt(value); + } + }, + s: { + pattern: "(\\d{1,2})", + action: (value, data) => { + data.seconds = parseInt(value); + } + }, + tt: { + pattern: "([aApP][mM])", + action: (value, data) => { + const char = value.charAt(0); + data.am = char === "a" || char === "A"; + } + }, + t: { + pattern: "([aApP])", + action: (value, data) => { + data.am = value === "a" || value === "A"; + } + } + }; + const escapedFormat = cFormat.replaceAll(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); + const patterns = /(mmmm|mmm|mm|m|dddd|ddd|dd|d|yyyy|yy|HH|H|hh|h|MM|M|ss|s|tt|t)/g; + const actions = []; + const re = escapedFormat.replaceAll(patterns, function (_, patternElement) { + const { + pattern, + action + } = handlers[patternElement]; + actions.push(action); + return pattern.includes(",") ? `(?=${pattern})\\${actions.length}` : pattern; + }); + return [new RegExp(`^${re}$`, "g"), actions]; + } _scand(cFormat, cDate, strict = false) { if (typeof cDate !== "string") { return new Date(cDate); @@ -3651,147 +3792,8 @@ class Util extends PDFObject { case 2: return this.scand("m/d/yy h:MM:ss tt", cDate); } - if (!this._scandCache.has(cFormat)) { - const months = this._months; - const days = this._days; - const handlers = { - mmmm: { - pattern: `(${months.join("|")})`, - action: (value, data) => { - data.month = months.indexOf(value); - } - }, - mmm: { - pattern: `(${months.map(month => month.substring(0, 3)).join("|")})`, - action: (value, data) => { - data.month = months.findIndex(month => month.substring(0, 3) === value); - } - }, - mm: { - pattern: `(\\d{2})`, - action: (value, data) => { - data.month = parseInt(value) - 1; - } - }, - m: { - pattern: `(\\d{1,2})`, - action: (value, data) => { - data.month = parseInt(value) - 1; - } - }, - dddd: { - pattern: `(${days.join("|")})`, - action: (value, data) => { - data.day = days.indexOf(value); - } - }, - ddd: { - pattern: `(${days.map(day => day.substring(0, 3)).join("|")})`, - action: (value, data) => { - data.day = days.findIndex(day => day.substring(0, 3) === value); - } - }, - dd: { - pattern: "(\\d{2})", - action: (value, data) => { - data.day = parseInt(value); - } - }, - d: { - pattern: "(\\d{1,2})", - action: (value, data) => { - data.day = parseInt(value); - } - }, - yyyy: { - pattern: "(\\d{4})", - action: (value, data) => { - data.year = parseInt(value); - } - }, - yy: { - pattern: "(\\d{2})", - action: (value, data) => { - data.year = 2000 + parseInt(value); - } - }, - HH: { - pattern: "(\\d{2})", - action: (value, data) => { - data.hours = parseInt(value); - } - }, - H: { - pattern: "(\\d{1,2})", - action: (value, data) => { - data.hours = parseInt(value); - } - }, - hh: { - pattern: "(\\d{2})", - action: (value, data) => { - data.hours = parseInt(value); - } - }, - h: { - pattern: "(\\d{1,2})", - action: (value, data) => { - data.hours = parseInt(value); - } - }, - MM: { - pattern: "(\\d{2})", - action: (value, data) => { - data.minutes = parseInt(value); - } - }, - M: { - pattern: "(\\d{1,2})", - action: (value, data) => { - data.minutes = parseInt(value); - } - }, - ss: { - pattern: "(\\d{2})", - action: (value, data) => { - data.seconds = parseInt(value); - } - }, - s: { - pattern: "(\\d{1,2})", - action: (value, data) => { - data.seconds = parseInt(value); - } - }, - tt: { - pattern: "([aApP][mM])", - action: (value, data) => { - const char = value.charAt(0); - data.am = char === "a" || char === "A"; - } - }, - t: { - pattern: "([aApP])", - action: (value, data) => { - data.am = value === "a" || value === "A"; - } - } - }; - const escapedFormat = cFormat.replaceAll(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); - const patterns = /(mmmm|mmm|mm|m|dddd|ddd|dd|d|yyyy|yy|HH|H|hh|h|MM|M|ss|s|tt|t)/g; - const actions = []; - const re = escapedFormat.replaceAll(patterns, function (match, patternElement) { - const { - pattern, - action - } = handlers[patternElement]; - actions.push(action); - return pattern.includes(",") ? `(?=${pattern})\\${actions.length}` : pattern; - }); - this._scandCache.set(cFormat, [re, actions]); - } - const [re, actions] = this._scandCache.get(cFormat); - const matches = new RegExp(`^${re}$`, "g").exec(cDate); + const [regex, actions] = (this.#scandCache ??= new Map()).getOrInsertComputed(cFormat, this.#createScandDataBound); + const matches = regex.exec(cDate); if (!matches || matches.length !== actions.length + 1) { return strict ? null : this.#tryToGuessDate(cFormat, cDate); } diff --git a/toolkit/components/pdfjs/content/build/pdf.worker.mjs b/toolkit/components/pdfjs/content/build/pdf.worker.mjs index 3e7ff1d1b4dd..cc711b9f428a 100644 --- a/toolkit/components/pdfjs/content/build/pdf.worker.mjs +++ b/toolkit/components/pdfjs/content/build/pdf.worker.mjs @@ -21,8 +21,8 @@ */ /** - * pdfjsVersion = 6.3.72 - * pdfjsBuild = 71a3c6a89 + * pdfjsVersion = 6.3.183 + * pdfjsBuild = 48bb93b89 */ ;// ./src/shared/util.js @@ -529,7 +529,9 @@ class FeatureTest { } class Util { static get hexNums() { - return shadow(this, "hexNums", Array.from(Array(256).keys(), n => n.toString(16).padStart(2, "0"))); + return shadow(this, "hexNums", Array.from({ + length: 256 + }, (_, n) => n.toString(16).padStart(2, "0"))); } static makeHexColor(r, g, b) { return `#${this.hexNums[r]}${this.hexNums[g]}${this.hexNums[b]}`; @@ -1000,44 +1002,49 @@ class Ref { } } class RefSet { + #set = new Set(); constructor(parent = null) { - this._set = new Set(parent?._set); + if (parent) { + for (const refStr of parent) { + this.#set.add(refStr); + } + } } has(ref) { - return this._set.has(ref.toString()); + return this.#set.has(ref.toString()); } put(ref) { - this._set.add(ref.toString()); + this.#set.add(ref.toString()); } remove(ref) { - this._set.delete(ref.toString()); + this.#set.delete(ref.toString()); } [Symbol.iterator]() { - return this._set.values(); + return this.#set.keys(); } clear() { - this._set.clear(); + this.#set.clear(); } } -class RefSetCache { - _map = new Map(); +class RefMap { + #map = new Map(); get size() { - return this._map.size; + return this.#map.size; } get(ref) { - return this._map.get(ref.toString()); + return this.#map.get(ref.toString()); } has(ref) { - return this._map.has(ref.toString()); + return this.#map.has(ref.toString()); } put(ref, obj) { - this._map.set(ref.toString(), obj); + this.#map.set(ref.toString(), obj); } putAlias(ref, aliasRef) { - this._map.set(ref.toString(), this.get(aliasRef)); + this.#map.set(ref.toString(), this.get(aliasRef)); } getOrPutComputed(ref, callback) { - const map = this._map, + const map = this.#map, refStr = ref.toString(); if (!map.has(refStr)) { map.set(refStr, callback(ref)); @@ -1045,21 +1052,21 @@ class RefSetCache { return map.get(refStr); } [Symbol.iterator]() { - return this._map.values(); + return this.#map.values(); } clear() { - this._map.clear(); + this.#map.clear(); } *values() { - yield* this._map.values(); + yield* this.#map.values(); } *items() { - for (const [ref, value] of this._map) { + for (const [ref, value] of this.#map) { yield [Ref.fromString(ref), value]; } } *keys() { - for (const ref of this._map.keys()) { + for (const ref of this.#map.keys()) { yield Ref.fromString(ref); } } @@ -1285,11 +1292,11 @@ const MAX_INT_32 = 2 ** 31 - 1; const IDENTITY_MATRIX = [1, 0, 0, 1, 0, 0]; const RESOURCES_KEYS_OPERATOR_LIST = ["ColorSpace", "ExtGState", "Font", "Pattern", "Properties", "Shading", "XObject"]; const RESOURCES_KEYS_TEXT_CONTENT = ["ExtGState", "Font", "Properties", "XObject"]; -function getLookupTableFactory(initializer) { +function getLookupTableFactory(initializer, useArray = false) { let lookup; return function () { if (initializer) { - lookup = Object.create(null); + lookup = useArray ? [] : Object.create(null); initializer(lookup); initializer = null; } @@ -11253,6 +11260,8 @@ function getInlineImageCacheKey(bytes) { return ii + "_" + String.fromCharCode.apply(null, strBuf); } class Parser { + #imageCache = null; + #imageId = 0; constructor({ lexer, xref, @@ -11263,8 +11272,6 @@ class Parser { this.xref = xref; this.allowStreams = allowStreams; this.recoveryMode = recoveryMode; - this.imageCache = Object.create(null); - this._imageId = 0; this.refill(); } refill() { @@ -11593,7 +11600,7 @@ class Parser { makeInlineImage(cipherTransform) { const lexer = this.lexer; const stream = lexer.stream; - const dictMap = Object.create(null); + const dict = new Dict(this.xref); let dictLength; while (!isCmd(this.buf1, "ID") && this.buf1 !== EOF) { if (!(this.buf1 instanceof Name)) { @@ -11604,12 +11611,12 @@ class Parser { if (this.buf1 === EOF) { break; } - dictMap[key] = this.getObj(cipherTransform); + dict.set(key, this.getObj(cipherTransform)); } if (lexer.beginInlineImagePos !== -1) { dictLength = stream.pos - lexer.beginInlineImagePos; } - const filter = this.#fetchIfRef(dictMap.F || dictMap.Filter); + const filter = dict.get("F", "Filter"); let filterName; if (filter instanceof Name) { filterName = filter.name; @@ -11643,27 +11650,23 @@ class Parser { stream.pos = lexer.beginInlineImagePos; cacheKey = getInlineImageCacheKey(stream.getBytes(dictLength + length)); stream.pos = initialStreamPos; - const cacheEntry = this.imageCache[cacheKey]; - if (cacheEntry !== undefined) { + const cacheEntry = this.#imageCache?.get(cacheKey); + if (cacheEntry) { this.buf2 = Cmd.get("EI"); this.shift(); cacheEntry.reset(); return cacheEntry; } } - const dict = new Dict(this.xref); - for (const key in dictMap) { - dict.set(key, dictMap[key]); - } let imageStream = stream.makeSubStream(startPos, length, dict); if (cipherTransform && !this.#hasCryptFilter(filter)) { imageStream = cipherTransform.createStream(imageStream, length); } imageStream = this.filter(imageStream, dict, length, cipherTransform); imageStream.dict = dict; - if (cacheKey !== undefined) { - imageStream.cacheKey = `inline_img_${++this._imageId}`; - this.imageCache[cacheKey] = imageStream; + if (cacheKey) { + imageStream.cacheKey = `inline_img_${++this.#imageId}`; + (this.#imageCache ??= new Map()).set(cacheKey, imageStream); } this.buf2 = Cmd.get("EI"); this.shift(); @@ -17324,6 +17327,9 @@ const getSpecialPUASymbols = getLookupTableFactory(function (t) { t[63194] = 0x00ae; t[63722] = 0x2122; t[63195] = 0x2122; + t[63718] = 0x23d0; + t[63719] = 0x23af; + t[63733] = 0x23ae; t[63729] = 0x23a7; t[63730] = 0x23a8; t[63731] = 0x23a9; @@ -19254,6 +19260,7 @@ class CFFCompiler { ;// ./src/core/standard_fonts.js + const getStdFontMap = getLookupTableFactory(function (t) { t["Times-Roman"] = "Times-Roman"; t.Helvetica = "Helvetica"; @@ -19544,6 +19551,16 @@ const getSymbolsFonts = getLookupTableFactory(function (t) { t["Wingdings-Bold"] = true; t["Wingdings-Regular"] = true; }); +const getGlyphMapForMacOrderedFonts = getLookupTableFactory(function (t) { + const glyphsUnicode = getGlyphsUnicode(); + t[2] = 10; + for (let gid = 3; gid < MacStandardGlyphOrdering.length; gid++) { + const unicode = glyphsUnicode[MacStandardGlyphOrdering[gid]]; + if (unicode !== undefined) { + t[gid] = unicode; + } + } +}); const getGlyphMapForStandardFonts = getLookupTableFactory(function (t) { t[2] = 10; t[3] = 32; @@ -20042,6 +20059,83 @@ const getGlyphMapForStandardFonts = getLookupTableFactory(function (t) { t[3393] = 1159; t[3416] = 8377; }); +const getSupplementalGlyphMapForTrebuchetMS = getLookupTableFactory(function (t) { + t[151] = 956; + t[159] = 937; + t[168] = 916; + t[189] = 8364; + t[195] = 8729; + t[218] = 713; + t[236] = 222; + t[237] = 254; + t[238] = 8722; + t[239] = 185; + t[240] = 178; + t[241] = 179; + t[242] = 189; + t[243] = 188; + t[244] = 190; + t[245] = 181; + t[246] = 8486; + t[247] = 8710; + t[248] = 253; + t[249] = 215; + t[250] = 173; + t[253] = 8355; + t[254] = 286; + t[255] = 287; + t[256] = 304; + t[257] = 350; + t[258] = 351; + t[259] = 262; + t[260] = 263; + t[261] = 268; + t[262] = 269; + t[263] = 273; + t[264] = 175; + t[266] = 183; + t[267] = 258; + t[268] = 259; + t[269] = 260; + t[270] = 261; + t[271] = 270; + t[272] = 271; + t[273] = 272; + t[274] = 280; + t[275] = 281; + t[276] = 282; + t[277] = 283; + t[278] = 313; + t[279] = 314; + t[280] = 317; + t[281] = 318; + t[282] = 319; + t[283] = 320; + t[284] = 323; + t[285] = 324; + t[286] = 327; + t[287] = 328; + t[288] = 336; + t[289] = 337; + t[290] = 340; + t[291] = 341; + t[292] = 344; + t[293] = 345; + t[294] = 346; + t[295] = 347; + t[296] = 538; + t[297] = 539; + t[298] = 356; + t[299] = 357; + t[300] = 366; + t[301] = 367; + t[302] = 368; + t[303] = 369; + t[304] = 377; + t[305] = 378; + t[306] = 379; + t[307] = 380; +}); const getSupplementalGlyphMapForArialBlack = getLookupTableFactory(function (t) { t[227] = 322; t[264] = 261; @@ -26265,6 +26359,7 @@ class Type1Font { + const PRIVATE_USE_AREAS = [[0xe000, 0xf8ff], [0x100000, 0x10fffd]]; const PDF_GLYPH_SPACE_UNITS = 1000; const EXPORT_DATA_PROPERTIES = ["ascent", "bbox", "black", "bold", "cssFontInfo", "data", "defaultVMetrics", "defaultWidth", "descent", "disableFontFace", "fallbackName", "fontExtraProperties", "fontMatrix", "isInvalidPDFjsFont", "isType3Font", "italic", "loadedName", "mimetype", "missingFile", "name", "remeasure", "systemFontInfo", "vertical"]; @@ -26476,6 +26571,14 @@ function applyStandardFontGlyphMap(map, glyphMap) { map[+charCode] = glyphMap[charCode]; } } +const getSymbolGlyphIdEncoding = getLookupTableFactory(t => { + let glyphId = 3; + for (const [firstCharCode, lastCharCode] of [[0x20, 0x7e], [0xa1, 0xfe]]) { + for (let charCode = firstCharCode; charCode <= lastCharCode; charCode++) { + t[glyphId++] = SymbolSetEncoding[charCode]; + } + } +}, true); function buildToFontChar(encoding, glyphsUnicodeMap, differences) { const toFontChar = []; let unicode; @@ -26908,7 +27011,7 @@ function createPostscriptName(name) { function createNameTable(name, proto) { proto ||= [[], []]; const strings = [proto[0][0] || "Original licence", proto[0][1] || name, proto[0][2] || "Unknown", proto[0][3] || "uniqueID", proto[0][4] || name, proto[0][5] || "Version 0.11", proto[0][6] || createPostscriptName(name), proto[0][7] || "Unknown", proto[0][8] || "Unknown", proto[0][9] || "Unknown"]; - const stringsBytes = strings.map(s => stringToBytes(s)); + const stringsBytes = strings.map(stringToBytes); const stringsUnicodeBytes = new Array(strings.length); let i, ii, j, jj, str; for (i = 0, ii = strings.length; i < ii; i++) { @@ -27147,11 +27250,16 @@ class Font { if ((isStandardFont || isMappedToStandardFont) && type === "CIDFontType2" && this.cidEncoding.startsWith("Identity-")) { const cidToGidMap = properties.cidToGidMap; const map = []; - applyStandardFontGlyphMap(map, getGlyphMapForStandardFonts()); - if (/Arial-?Black/i.test(name)) { - applyStandardFontGlyphMap(map, getSupplementalGlyphMapForArialBlack()); - } else if (/Calibri/i.test(name)) { - applyStandardFontGlyphMap(map, getSupplementalGlyphMapForCalibri()); + if (/Trebuchet/i.test(name)) { + applyStandardFontGlyphMap(map, getGlyphMapForMacOrderedFonts()); + applyStandardFontGlyphMap(map, getSupplementalGlyphMapForTrebuchetMS()); + } else { + applyStandardFontGlyphMap(map, getGlyphMapForStandardFonts()); + if (/Arial-?Black/i.test(name)) { + applyStandardFontGlyphMap(map, getSupplementalGlyphMapForArialBlack()); + } else if (/Calibri/i.test(name)) { + applyStandardFontGlyphMap(map, getSupplementalGlyphMapForCalibri()); + } } if (cidToGidMap) { for (const charCode in map) { @@ -27177,7 +27285,8 @@ class Font { this.toFontChar = map; this.toUnicode = new ToUnicodeMap(map); } else if (/Symbol/i.test(fontName)) { - this.toFontChar = buildToFontChar(SymbolSetEncoding, getGlyphsUnicode(), this.differences); + const isCidKeyed = this.composite && this.cidEncoding.startsWith("Identity-"); + this.toFontChar = buildToFontChar(isCidKeyed ? getSymbolGlyphIdEncoding() : SymbolSetEncoding, getGlyphsUnicode(), this.differences); } else if (/Dingbats/i.test(fontName)) { this.toFontChar = buildToFontChar(ZapfDingbatsEncoding, getDingbatsGlyphsUnicode(), this.differences); } else if (isStandardFont || isMappedToStandardFont) { @@ -31001,7 +31110,7 @@ class PSStackBasedInterpreter { const base = this.#sp - nOut; for (let i = 0; i < nOut; i++) { const v = base + i >= 0 ? this.#stack[base + i] : 0; - dest[destOffset + i] = MathClamp(range[i * 2 + 1], range[i * 2], v); + dest[destOffset + i] = MathClamp(v, range[i * 2], range[i * 2 + 1]); } }; } @@ -31020,6 +31129,7 @@ function buildPostScriptJsFunction(source, domain, range, forceInterpreter = fal ;// ./src/core/postscript/wasm_compiler.js + const wasm_compiler_OP = { if: 0x04, else: 0x05, @@ -31085,7 +31195,7 @@ function unsignedLEB128(n) { return out; } function encodeASCIIString(s) { - return [...unsignedLEB128(s.length), ...Array.from(s, c => c.charCodeAt(0))]; + return [...unsignedLEB128(s.length), ...stringToBytes(s)]; } function section(id, data) { return [id, ...unsignedLEB128(data.length), ...data]; @@ -31816,7 +31926,7 @@ class BaseLocalCache { this._nameRefMap = new Map(); this._imageMap = new Map(); } - this._imageCache = new RefSetCache(); + this._imageCache = new RefMap(); } getByName(name) { if (this._onlyRefs) { @@ -31967,8 +32077,8 @@ class GlobalImageCache { static MAX_BYTE_SIZE = 5e7; #decodeFailedSet = new RefSet(); constructor() { - this._refCache = new RefSetCache(); - this._imageCache = new RefSetCache(); + this._refCache = new RefMap(); + this._imageCache = new RefMap(); } get #byteSize() { let byteSize = 0; @@ -34779,11 +34889,11 @@ class PartialEvaluator { } break; case "TR": + if (gState.has("TR2")) { + break; + } case "TR2": { - if (key === "TR" && gState.has("TR2")) { - break; - } const transferMaps = this.handleTransferFunction(value); gStateObj.push(["TR", transferMaps]); break; @@ -37410,70 +37520,69 @@ class TranslatedFont { this.font.disableFontFace = true; PartialEvaluator.buildFontPaths(this.font, this.font.glyphCacheValues, handler, evaluatorOptions); } - loadType3Data(evaluator, resources, task, seenRefs = null) { + async loadType3Data(evaluator, resources, task, seenRefs = null) { if (this.#type3Loaded) { return this.#type3Loaded; } const { + dict, font, type3Dependencies } = this; assert(font.isType3Font, "Must be a Type3 font."); + const { + promise, + resolve + } = Promise.withResolvers(); + this.#type3Loaded = promise; const type3Evaluator = evaluator.clone({ ignoreErrors: false }); const type3FontRefs = new RefSet(evaluator.type3FontRefs); - if (this.dict.objId && !type3FontRefs.has(this.dict.objId)) { - type3FontRefs.put(this.dict.objId); + if (dict.objId) { + type3FontRefs.put(dict.objId); } type3Evaluator.type3FontRefs = type3FontRefs; - let loadCharProcsPromise = Promise.resolve(); - const charProcs = this.dict.get("CharProcs"); - const fontResources = this.dict.get("Resources") || resources; - const charProcOperatorList = Object.create(null); - const [x0, y0, x1, y1] = font.bbox, - width = x1 - x0, - height = y1 - y0; - const fontBBoxSize = Math.hypot(width, height); + const charProcs = dict.get("CharProcs"); + const fontResources = dict.get("Resources") || resources; + const charProcOperatorList = new Map(); + const [x0, y0, x1, y1] = font.bbox; + const fontBBoxSize = Math.hypot(x1 - x0, y1 - y0); for (const key of charProcs.getKeys()) { - loadCharProcsPromise = loadCharProcsPromise.then(() => { - const glyphStream = charProcs.get(key); + try { const operatorList = new OperatorList(); - return type3Evaluator.getOperatorList({ - stream: glyphStream, + await type3Evaluator.getOperatorList({ + stream: charProcs.get(key), task, resources: fontResources, operatorList, prevRefs: seenRefs - }).then(() => { - switch (operatorList.fnArray[0]) { - case OPS.setCharWidthAndBounds: - this.#removeType3ColorOperators(operatorList, fontBBoxSize); - break; - case OPS.setCharWidth: - if (!fontBBoxSize) { - this.#guessType3FontBBox(operatorList); - } - break; - } - charProcOperatorList[key] = operatorList.getIR(); - for (const dependency of operatorList.dependencies) { - type3Dependencies.add(dependency); - } - }).catch(function (reason) { - warn(`Type3 font resource "${key}" is not available.`); - const dummyOperatorList = new OperatorList(); - charProcOperatorList[key] = dummyOperatorList.getIR(); }); - }); - } - this.#type3Loaded = loadCharProcsPromise.then(() => { - font.charProcOperatorList = charProcOperatorList; - if (this._bbox) { - font.isCharBBox = true; - font.bbox = this._bbox; + switch (operatorList.fnArray[0]) { + case OPS.setCharWidthAndBounds: + this.#removeType3ColorOperators(operatorList, fontBBoxSize); + break; + case OPS.setCharWidth: + if (!fontBBoxSize) { + this.#guessType3FontBBox(operatorList); + } + break; + } + charProcOperatorList.set(key, operatorList.getIR()); + for (const dependency of operatorList.dependencies) { + type3Dependencies.add(dependency); + } + } catch { + warn(`Type3 font resource "${key}" is not available.`); + charProcOperatorList.set(key, new OperatorList().getIR()); } - }); + } + font.charProcOperatorList = charProcOperatorList; + if (this._bbox) { + font.isCharBBox = true; + font.bbox = this._bbox; + } + resolve(); return this.#type3Loaded; } #removeType3ColorOperators(operatorList, fontBBoxSize = NaN) { @@ -39404,6 +39513,7 @@ function soundStreamToWav(stream, samples) { const MAX_DEPTH = 40; +const TABLE_SPAN_ATTRIBUTES = [["RowSpan", "rowSpan"], ["ColSpan", "colSpan"]]; const StructElementType = { PAGE_CONTENT: 1, STREAM_CONTENT: 2, @@ -39458,8 +39568,7 @@ class StructTreeRoot { if (!(pageRef instanceof Ref) || id < 0) { return; } - this.structParentIds ||= new RefSetCache(); - this.structParentIds.getOrPutComputed(pageRef, makeArr).push([id, type]); + (this.structParentIds ??= new RefMap()).getOrPutComputed(pageRef, makeArr).push([id, type]); } addAnnotationIdToPage(pageRef, id) { this.#addIdToPage(pageRef, id, StructElementType.ANNOTATION); @@ -39509,7 +39618,7 @@ class StructTreeRoot { changes }) { const root = await pdfManager.ensureCatalog("cloneDict"); - const cache = new RefSetCache(); + const cache = new RefMap(); cache.put(catalogRef, root); const structTreeRootRef = xref.getNewTemporaryRef(); root.set("StructTreeRoot", structTreeRootRef); @@ -39620,7 +39729,7 @@ class StructTreeRoot { xref } = this; const structTreeRoot = this.dict.clone(); - const cache = new RefSetCache(); + const cache = new RefMap(); cache.put(structTreeRootRef, structTreeRoot); let parentTreeRef = structTreeRoot.getRaw("ParentTree"); let parentTree; @@ -39905,16 +40014,120 @@ class StructElementNode { } return stringToUTF8String(fileStream.getString()); } - const A = this.dict.get("A"); - if (A instanceof Dict) { - const O = A.get("O"); - if (isName(O, "MSFT_Office")) { - const mathml = A.get("MSFT_MathML"); + for (const attributes of this.attributes) { + if (isName(attributes.get("O"), "MSFT_Office")) { + const mathml = attributes.get("MSFT_MathML"); return mathml ? stringToPDFString(mathml) : null; } } return null; } + #collectAttributes(value, attributes) { + const pending = [value]; + const visited = new RefSet(); + while (pending.length > 0) { + value = pending.pop(); + if (value instanceof Ref) { + if (visited.has(value)) { + continue; + } + visited.put(value); + value = this.xref.fetch(value); + } + if (value instanceof BaseStream) { + value = value.dict; + } + if (value instanceof Dict) { + attributes.push(value); + continue; + } + if (!Array.isArray(value)) { + continue; + } + for (let i = value.length - 1; i >= 0; i--) { + if (!Number.isInteger(value[i])) { + pending.push(value[i]); + } + } + } + } + get attributes() { + const attributes = []; + const classes = this.dict.getArray("C"); + if (classes !== undefined) { + const classMap = this.tree.rootDict?.get("ClassMap"); + if (classMap instanceof Dict) { + for (const className of Array.isArray(classes) ? classes : [classes]) { + if (className instanceof Name) { + this.#collectAttributes(classMap.getRaw(className.name), attributes); + } + } + } + } + this.#collectAttributes(this.dict.getRaw("A"), attributes); + return shadow(this, "attributes", attributes); + } + get tableAttributes() { + const { + role + } = this; + if (role !== "Table" && role !== "TH" && role !== "TD") { + return null; + } + const map = new Map(); + for (const attributes of this.attributes) { + if (!isName(attributes.get("O"), "Table")) { + continue; + } + if (role === "Table") { + if (attributes.has("Summary")) { + const summary = attributes.get("Summary"); + if (typeof summary === "string" && summary) { + map.set("summary", stringToPDFString(summary)); + } else { + map.delete("summary"); + } + } + continue; + } + for (const [key, name] of TABLE_SPAN_ATTRIBUTES) { + if (!attributes.has(key)) { + continue; + } + const value = attributes.get(key); + if (Number.isInteger(value) && value > 1) { + map.set(name, value); + } else { + map.delete(name); + } + } + if (attributes.has("Headers")) { + map.delete("headers"); + const headers = attributes.getArray("Headers"); + if (Array.isArray(headers)) { + const ids = headers.filter(header => typeof header === "string").map(stringToPDFString); + if (ids.length > 0) { + map.set("headers", ids); + } + } + } + if (role === "TH" && attributes.has("Scope")) { + map.delete("scope"); + const scope = attributes.get("Scope"); + if (scope instanceof Name && ["Row", "Column", "Both"].includes(scope.name)) { + map.set("scope", scope.name); + } + } + if (role === "TH" && attributes.has("Short")) { + map.delete("short"); + const short = attributes.get("Short"); + if (typeof short === "string" && short) { + map.set("short", stringToPDFString(short)); + } + } + } + return map.size ? map : null; + } parseKids() { let pageObjId = null; const objRef = this.dict.getRaw("Pg"); @@ -40149,6 +40362,13 @@ class StructTreePage { if (typeof alt === "string") { obj.alt = stringToPDFString(alt); } + const structId = node.dict.get("ID"); + if (obj.role === "TH" && typeof structId === "string" && structId) { + obj.structId = stringToPDFString(structId); + } + node.tableAttributes?.forEach((val, key) => { + obj[key] = val; + }); if (obj.role === "Formula") { try { const { @@ -40164,19 +40384,19 @@ class StructTreePage { warn(`Ignoring mathML: "${ex}".`); } } - const a = node.dict.get("A"); - if (a instanceof Dict) { - const bbox = lookupNormalRect(a.getArray("BBox"), null); - if (bbox) { - obj.bbox = bbox; - } else { - const width = a.get("Width"); - const height = a.get("Height"); - if (typeof width === "number" && width > 0 && typeof height === "number" && height > 0) { - obj.bbox = [0, 0, width, height]; - } + let bbox = null, + size = null; + for (const a of node.attributes) { + bbox = lookupNormalRect(a.getArray("BBox"), bbox); + const width = a.get("Width"); + const height = a.get("Height"); + if (typeof width === "number" && width > 0 && typeof height === "number" && height > 0) { + size = [0, 0, width, height]; } } + if (bbox || size) { + obj.bbox = bbox ?? size; + } const lang = node.dict.get("Lang"); if (typeof lang === "string") { obj.lang = stringToPDFString(lang); @@ -40255,18 +40475,18 @@ function fetchRemoteDest(action) { } class Catalog { #actualNumPages = null; - #annotationAttachmentIdByRef = new RefSetCache(); + #annotationAttachmentIdByRef = new RefMap(); #annotationAttachmentRefById = new Map(); #soundAttachmentIds = new Set(); #catDict = null; builtInCMapCache = new Map(); - fontCache = new RefSetCache(); + fontCache = new RefMap(); globalColorSpaceCache = new GlobalColorSpaceCache(); globalImageCache = new GlobalImageCache(); nonBlendModesSet = new RefSet(); - pageDictCache = new RefSetCache(); - pageIndexCache = new RefSetCache(); - pageKidsCountCache = new RefSetCache(); + pageDictCache = new RefMap(); + pageIndexCache = new RefMap(); + pageKidsCountCache = new RefMap(); standardFontDataCache = new Map(); systemFontCache = new Map(); constructor(pdfManager, xref) { @@ -40564,11 +40784,10 @@ class Catalog { return null; } flags += 2 ** 32; - const permissions = []; - for (const key in PermissionFlag) { - const value = PermissionFlag[key]; + const permissions = new Set(); + for (const value of Object.values(PermissionFlag)) { if (flags & value) { - permissions.push(value); + permissions.add(value); } } return permissions; @@ -40588,7 +40807,7 @@ class Catalog { if (!Array.isArray(groupsData)) { return shadow(this, "optionalContentConfig", null); } - const groupRefCache = new RefSetCache(); + const groupRefCache = new RefMap(); for (const groupRef of groupsData) { if (!(groupRef instanceof Ref) || groupRefCache.has(groupRef)) { continue; @@ -43207,9 +43426,6 @@ class XFAObject { [$getSubformParent]() { return this[$getParent](); } - [$getChildren](name = null) { - return !name ? this[_children] : this[name]; - } [$dump]() { const dumped = Object.create(null); if (this[$content]) { @@ -43655,9 +43871,6 @@ class XmlObject extends XFAObject { } return HTMLResult.EMPTY; } - [$getChildren](name = null) { - return !name ? this[_children] : this[_children].filter(c => c[$nodeName] === name); - } [$getAttributes]() { return this[_attributes]; } @@ -48746,7 +48959,7 @@ class Text extends ContentObject { } [$getExtra]() { if (typeof this[$content] === "string") { - return this[$content].split(/[\u2029\u2028\n]/).filter(line => !!line).join("\n"); + return this[$content].split(/[\u2029\u2028\n]/).filter(Boolean).join("\n"); } return this[$content][$text](); } @@ -50201,7 +50414,7 @@ class EquateRange extends XFAObject { const ranges = []; const unicodeRegex = /U\+([0-9a-fA-F]+)/; const unicodeRange = this._unicodeRange; - for (let range of unicodeRange.split(",").map(x => x.trim()).filter(x => !!x)) { + for (let range of unicodeRange.split(",").map(x => x.trim()).filter(Boolean)) { range = range.split("-", 2).map(x => { const found = x.match(unicodeRegex); if (!found) { @@ -51975,7 +52188,7 @@ class XhtmlObject extends XmlObject { xfaFont.letterSpacing = getMeasurement(value); break; case "margin": - const values = value.split(/ \t/).map(x => getMeasurement(x)); + const values = value.split(/ \t/).map(getMeasurement); switch (values.length) { case 1: margin.top = margin.bottom = margin.left = margin.right = values[0]; @@ -58300,7 +58513,9 @@ class XRef { tableState.parserBuf2 = parser.buf2; const entry = { offset: parser.getObj(), - gen: parser.getObj() + gen: parser.getObj(), + free: false, + uncompressed: false }; const type = parser.getObj(); if (type instanceof Cmd) { @@ -58397,7 +58612,9 @@ class XRef { } const entry = { offset, - gen: generation + gen: generation, + free: false, + uncompressed: false }; switch (type) { case 0: @@ -58519,6 +58736,7 @@ class XRef { this.#entries[num] = { offset: position - stream.start, gen, + free: false, uncompressed: true }; } @@ -59138,7 +59356,7 @@ class Page { throw new Error("XFA: Cannot save new annotations."); } const partialEvaluator = this.#createPartialEvaluator(handler); - const deletedAnnotations = new RefSetCache(); + const deletedAnnotations = new RefMap(); const existingAnnotations = new RefSet(); await this.#replaceIdByRef(annotations, deletedAnnotations, existingAnnotations); const pageDict = this.pageDict; @@ -60275,14 +60493,14 @@ class PDFDocument { const visitedRefs = new RefSet(); const allFields = new Map(); const fieldPromises = new Map(); - const orphanFields = new RefSetCache(); + const orphanFields = new RefMap(); for (const fieldRef of acroForm.get("Fields")) { await this.#collectFieldObjects("", null, fieldRef, fieldPromises, annotationGlobals, visitedRefs, orphanFields); } const allPromises = []; for (const [name, promises] of fieldPromises) { allPromises.push(Promise.all(promises).then(fields => { - fields = fields.filter(field => !!field); + fields = fields.filter(Boolean); if (fields.length > 0) { allFields.set(name, fields); } @@ -61519,11 +61737,11 @@ class DocumentData { this.document = document; this.destinations = null; this.pageLabels = null; - this.pagesMap = new RefSetCache(); - this.oldRefMapping = new RefSetCache(); + this.pagesMap = new RefMap(); + this.oldRefMapping = new RefMap(); this.dedupNamedDestinations = new Map(); this.usedNamedDestinations = new Set(); - this.postponedRefCopies = new RefSetCache(); + this.postponedRefCopies = new RefMap(); this.resourceStreamPromises = new Map(); this.usedStructParents = new Set(); this.oldStructParentMapping = new Map(); @@ -61540,7 +61758,7 @@ class DocumentData { this.acroFormDefaultResources = null; this.acroFormQ = 0; this.hasSignatureAnnotations = false; - this.fieldToParent = new RefSetCache(); + this.fieldToParent = new RefMap(); this.outline = null; this.embeddedFiles = null; } @@ -62151,8 +62369,6 @@ class PDFEditor { } this.oldPages[newPageIndex] = null; }; - const docPageInfos = pageInfos.filter(info => !!info.document); - this.isSingleFile = docPageInfos.length === 1 || docPageInfos.length > 0 && docPageInfos.every(info => info.document === docPageInfos[0].document); const allDocumentData = []; if (annotationStorage) { this.#newAnnotationsParams = { @@ -62230,7 +62446,11 @@ class PDFEditor { } } await Promise.all(promises); + if (this.oldPages.length === 0) { + throw new Error("extractPages: nothing to extract."); + } const copyCounts = new Map(); + const documents = new Set(); for (let i = 0, ii = this.oldPages.length; i < ii; i++) { const pageData = this.oldPages[i]; if (pageData === undefined) { @@ -62243,8 +62463,10 @@ class PDFEditor { const copyLevel = copyCounts.get(page) ?? 0; copyCounts.set(page, copyLevel + 1); pageData.copyLevel = copyLevel; + documents.add(pageData.documentData.document); } } + this.isSingleFile = documents.size === 1; promises.length = 0; this.#collectValidDestinations(allDocumentData); this.#collectOutlineDestinations(allDocumentData); @@ -62364,7 +62586,7 @@ class PDFEditor { })); } await Promise.all(promises); - newAnnotations = newAnnotations.filter(annot => !!annot); + newAnnotations = newAnnotations.filter(Boolean); pageData.annotations = newAnnotations.length > 0 ? newAnnotations : null; pageData.documentData.hasSignatureAnnotations ||= hasSignatureAnnotations; } @@ -63267,7 +63489,7 @@ class PDFEditor { } const numPages = document.numPages; const labelsByPageIndex = new Map(); - const oldPageIndices = new Set(this.oldPages.filter(p => !!p).map(({ + const oldPageIndices = new Set(this.oldPages.filter(Boolean).map(({ page: { pageIndex } @@ -63374,7 +63596,7 @@ class PDFEditor { task, imagesPromises } = this.#newAnnotationsParams; - const changes = new RefSetCache(); + const changes = new RefMap(); const newData = await AnnotationFactory.saveNewAnnotations(page.createAnnotationEvaluator(handler), this.xrefWrapper, task, newAnnotations, imagesPromises, changes); for (const [ref, { data @@ -63716,7 +63938,11 @@ class PDFEditor { const parentTree = this.xref[parentTreeRef.num]; parentTree.setIfName("Type", "ParentTree"); structTree.set("ParentTree", parentTreeRef); - structTree.set("ParentTreeNextKey", this.parentTree.size); + let nextKey = 0; + for (const key of this.parentTree.keys()) { + nextKey = Math.max(nextKey, key + 1); + } + structTree.set("ParentTreeNextKey", nextKey); } if (this.idTree.size > 0) { const idTreeRef = this.#makeNameNumTree(Array.from(this.idTree.entries()), true); @@ -63851,7 +64077,7 @@ class PDFEditor { return result; } async #createChanges() { - const changes = new RefSetCache(); + const changes = new RefMap(); changes.put(Ref.get(0, 0xffff), { data: null }); @@ -64152,7 +64378,7 @@ class WorkerMessageHandler { docId, apiVersion } = docParams; - const workerVersion = "6.3.72"; + const workerVersion = "6.3.183"; if (apiVersion !== workerVersion) { throw new Error(`The API version "${apiVersion}" does not match ` + `the Worker version "${workerVersion}".`); } @@ -64438,7 +64664,7 @@ class WorkerMessageHandler { } await Promise.all(pagePromises); const annotations = await Promise.all(annotationPromises); - return annotations.filter(a => !!a); + return annotations.filter(Boolean); } finally { if (task) { finishWorkerTask(task); @@ -64588,7 +64814,7 @@ class WorkerMessageHandler { filename }) { const globalPromises = [pdfManager.requestLoadedStream(), pdfManager.ensureCatalog("acroForm"), pdfManager.ensureCatalog("acroFormRef"), pdfManager.ensureDoc("startXRef"), pdfManager.ensureDoc("xref"), pdfManager.ensureCatalog("structTreeRoot")]; - const changes = new RefSetCache(); + const changes = new RefMap(); const promises = []; const newAnnotationsByPage = !isPureXfa ? getNewAnnotationsMap(annotationStorage) : null; const [stream, acroForm, acroFormRef, startXRef, xref, _structTreeRoot] = await Promise.all(globalPromises); diff --git a/toolkit/components/pdfjs/content/web/debugger.mjs b/toolkit/components/pdfjs/content/web/debugger.mjs index 16ebd141e425..cbaaf790dcf4 100644 --- a/toolkit/components/pdfjs/content/web/debugger.mjs +++ b/toolkit/components/pdfjs/content/web/debugger.mjs @@ -624,12 +624,7 @@ class Stepper { getNextBreakPoint() { this.breakPoints.sort((a, b) => a - b); - for (const breakPoint of this.breakPoints) { - if (breakPoint > this.currentIdx) { - return breakPoint; - } - } - return null; + return this.breakPoints.find(idx => idx > this.currentIdx) ?? null; } breakIt(idx, callback) { diff --git a/toolkit/components/pdfjs/content/web/images/toolbarButton-menuArrowNova.svg b/toolkit/components/pdfjs/content/web/images/toolbarButton-menuArrowNova.svg new file mode 100644 index 000000000000..f5c80208f990 --- /dev/null +++ b/toolkit/components/pdfjs/content/web/images/toolbarButton-menuArrowNova.svg @@ -0,0 +1,3 @@ + + + diff --git a/toolkit/components/pdfjs/content/web/standard_fonts/LICENSE_LIBERATION b/toolkit/components/pdfjs/content/web/standard_fonts/LICENSE_LIBERATION index aba73e8a4030..213c18265882 100644 --- a/toolkit/components/pdfjs/content/web/standard_fonts/LICENSE_LIBERATION +++ b/toolkit/components/pdfjs/content/web/standard_fonts/LICENSE_LIBERATION @@ -1,102 +1,363 @@ -Digitized data copyright (c) 2010 Google Corporation - with Reserved Font Arimo, Tinos and Cousine. -Copyright (c) 2012 Red Hat, Inc. - with Reserved Font Name Liberation. +LICENSE AGREEMENT AND LIMITED PRODUCT WARRANTY +LIBERATION FONT SOFTWARE -This Font Software is licensed under the SIL Open Font License, -Version 1.1. +This agreement governs the use of the Software and any updates to the Software, regardless of the delivery mechanism. Subject to the following terms, Red Hat, Inc. ("Red Hat") grants to the user ("Client") a license to this work pursuant to the GNU General Public License v.2 with the exceptions set forth below and such other terms as are set forth in this End User License Agreement. -This license is copied below, and is also available with a FAQ at: -http://scripts.sil.org/OFL + 1. The Software and License Exception. LIBERATION font software (the "Software") consists of TrueType-OpenType formatted font software for rendering LIBERATION typefaces in sans-serif, serif, and monospaced character styles. You are licensed to use, modify, copy, and distribute the Software pursuant to the GNU General Public License v.2 with the following exceptions: -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 + (a) As a special exception, if you create a document which uses this font, and embed this font or unaltered portions of this font into the document, this font does not by itself cause the resulting document to be covered by the GNU General Public License. This exception does not however invalidate any other reasons why the document might be covered by the GNU General Public License. If you modify this font, you may extend this exception to your version of the font, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. -PREAMBLE The goals of the Open Font License (OFL) are to stimulate -worldwide development of collaborative font projects, to support the font -creation efforts of academic and linguistic communities, and to provide -a free and open framework in which fonts may be shared and improved in -partnership with others. + (b) As a further exception, any distribution of the object code of the Software in a physical product must provide you the right to access and modify the source code for the Software and to reinstall that modified version of the Software in object code form on the same physical product on which you received it. -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. -The fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply to -any document created using the fonts or their derivatives. + 2. Intellectual Property Rights. The Software and each of its components, including the source code, documentation, appearance, structure and organization are owned by Red Hat and others and are protected under copyright and other laws. Title to the Software and any component, or to any copy, modification, or merged portion shall remain with the aforementioned, subject to the applicable license. The "LIBERATION" trademark is a trademark of Red Hat, Inc. in the U.S. and other countries. This agreement does not permit Client to distribute modified versions of the Software using Red Hat's trademarks. If Client makes a redistribution of a modified version of the Software, then Client must modify the files names to remove any reference to the Red Hat trademarks and must not use the Red Hat trademarks in any way to reference or promote the modified Software. - + 3. Limited Warranty. To the maximum extent permitted under applicable law, the Software is provided and licensed "as is" without warranty of any kind, expressed or implied, including the implied warranties of merchantability, non-infringement or fitness for a particular purpose. Red Hat does not warrant that the functions contained in the Software will meet Client's requirements or that the operation of the Software will be entirely error free or appear precisely as described in the accompanying documentation. -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. -This may include source files, build scripts and documentation. + 4. Limitation of Remedies and Liability. To the maximum extent permitted by applicable law, Red Hat or any Red Hat authorized dealer will not be liable to Client for any incidental or consequential damages, including lost profits or lost savings arising out of the use or inability to use the Software, even if Red Hat or such dealer has been advised of the possibility of such damages. -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). + 5. General. If any provision of this agreement is held to be unenforceable, that shall not affect the enforceability of the remaining provisions. This agreement shall be governed by the laws of the State of North Carolina and of the United States, without regard to any conflict of laws provisions, except that the United Nations Convention on the International Sale of Goods shall not apply. +Copyright © 2007-2011 Red Hat, Inc. All rights reserved. LIBERATION is a trademark of Red Hat, Inc. -"Original Version" refers to the collection of Font Software components -as distributed by the Copyright Holder(s). +------------------------------------------------------------------------------ -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting ? in part or in whole ? -any of the components of the Original Version, by changing formats or -by porting the Font Software to a new environment. +The text of the GNU General Public License, version 2, referenced above: -"Author" refers to any designer, engineer, programmer, technical writer -or other person who contributed to the Font Software. + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. -PERMISSION & CONDITIONS + Preamble -Permission is hereby granted, free of charge, to any person obtaining a -copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. -1) Neither the Font Software nor any of its individual components,in - Original or Modified Versions, may be sold by itself. + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. -2) Original or Modified Versions of the Font Software may be bundled, - redistributed and/or sold with any software, provided that each copy - contains the above copyright notice and this license. These can be - included either as stand-alone text files, human-readable headers or - in the appropriate machine-readable metadata fields within text or - binary files as long as those fields can be easily viewed by the user. + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. -3) No Modified Version of the Font Software may use the Reserved Font - Name(s) unless explicit written permission is granted by the - corresponding Copyright Holder. This restriction only applies to the - primary font name as presented to the users. + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font - Software shall not be used to promote, endorse or advertise any - Modified Version, except to acknowledge the contribution(s) of the - Copyright Holder(s) and the Author(s) or with their explicit written - permission. + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. -5) The Font Software, modified or unmodified, in part or in whole, must - be distributed entirely under this license, and must not be distributed - under any other license. The requirement for fonts to remain under - this license does not apply to any document created using the Font - Software. + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. - -TERMINATION -This license becomes null and void if any of the above conditions are not met. + The precise terms and conditions for copying, distribution and +modification follow. - + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER -DEALINGS IN THE FONT SOFTWARE. + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/toolkit/components/pdfjs/content/web/viewer-geckoview.css b/toolkit/components/pdfjs/content/web/viewer-geckoview.css index cceafe526eca..4d928d2ee0f7 100644 --- a/toolkit/components/pdfjs/content/web/viewer-geckoview.css +++ b/toolkit/components/pdfjs/content/web/viewer-geckoview.css @@ -1573,6 +1573,7 @@ border-radius:4px; border:1px solid var(--new-badge-border-color); padding-inline:4px; + margin-inline:4px; font:menu; font-size:12px; diff --git a/toolkit/components/pdfjs/content/web/viewer-geckoview.mjs b/toolkit/components/pdfjs/content/web/viewer-geckoview.mjs index e8a8c06fbeb6..e7b307ae17ba 100644 --- a/toolkit/components/pdfjs/content/web/viewer-geckoview.mjs +++ b/toolkit/components/pdfjs/content/web/viewer-geckoview.mjs @@ -21,8 +21,8 @@ */ /** - * pdfjsVersion = 6.3.72 - * pdfjsBuild = 71a3c6a89 + * pdfjsVersion = 6.3.183 + * pdfjsBuild = 48bb93b89 */ ;// ./web/ui_utils.js @@ -822,18 +822,16 @@ class AppOptions { continue; } if (this.eventBus && kind & OptionKind.EVENT_DISPATCH) { - (events ||= new Map()).set(name, userOpt); + (events ??= new Map()).set(name, userOpt); } this.#opts.set(name, userOpt); } - if (events) { - for (const [name, value] of events) { - this.eventBus.dispatch(name.toLowerCase(), { - source: this, - value - }); - } - } + events?.forEach((value, name) => { + this.eventBus.dispatch(name.toLowerCase(), { + source: this, + value + }); + }); } } @@ -904,7 +902,7 @@ const { } = globalThis.pdfjsLib; ;// ./web/internal_evt.js -const INTERNAL_EVT = "73d553f8-709f-4713-892b-c46926003d23"; +const INTERNAL_EVT = "df51a2ca-766d-4bd1-bd4e-9faff090d03c"; const internalOpt = Object.freeze({ internal: INTERNAL_EVT }); @@ -4207,7 +4205,7 @@ class PDFFindController { } return this._normalizedQuery; } - return (query || []).filter(q => !!q).map(q => normalize(q)[0]); + return (query || []).filter(Boolean).map(q => normalize(q)[0]); } #shouldDirtyMatch(state) { const newQuery = state.query, @@ -6010,6 +6008,15 @@ class AnnotationEditorLayerBuilder { await this.annotationEditorLayer.render(parameters); this.show(); } + update(viewport) { + if (this.div) { + this.annotationEditorLayer.update({ + viewport: viewport.clone({ + dontFlip: true + }) + }); + } + } cancel() { this._cancelled = true; if (!this.div) { @@ -6864,19 +6871,23 @@ const PDF_ROLE_TO_HTML_ROLE = { Document: null, DocumentFragment: null, Part: "group", + Art: "article", Sect: "group", Div: "group", + BlockQuote: "blockquote", Aside: "note", NonStruct: "none", - P: null, + P: "paragraph", H: "heading", Title: null, FENote: "note", Sub: "group", Lbl: null, Span: null, - Em: null, - Strong: null, + Em: "emphasis", + Strong: "strong", + Note: "note", + Code: "code", Link: "link", Annot: "note", Form: "form", @@ -6896,12 +6907,13 @@ const PDF_ROLE_TO_HTML_ROLE = { TD: "cell", THead: "rowgroup", TBody: "rowgroup", - TFoot: null, - Caption: null, + TFoot: "rowgroup", + Caption: "caption", Figure: "figure", Formula: null, Artifact: null }; +const ARIA_ROLES_WITH_PROHIBITED_NAMES = new Set(["caption", "code", "emphasis", "generic", "none", "paragraph", "strong"]); const MathMLElements = new Set(["math", "merror", "mfrac", "mi", "mmultiscripts", "mn", "mo", "mover", "mpadded", "mprescripts", "mroot", "mrow", "ms", "mspace", "msqrt", "mstyle", "msub", "msubsup", "msup", "mtable", "mtd", "mtext", "mtr", "munder", "munderover", "semantics"]); const MathMLNamespace = "http://www.w3.org/1998/Math/MathML"; class MathMLSanitizer { @@ -6926,6 +6938,9 @@ class StructTreeLayerBuilder { #treeDom = null; #treePromise; #elementAttributes = new Map(); + #structElementIdPrefix = `pdfjs_internal_struct_${getUuid()}_`; + #structElementIds = new Map(); + #structElements = new Map(); #rawDims; #elementsToAddToTextLayer = null; #elementsToHideInTextLayer = null; @@ -6945,7 +6960,9 @@ class StructTreeLayerBuilder { } = Promise.withResolvers(); this.#treePromise = promise; try { - this.#treeDom = this.#walk(await this.#promise); + const tree = await this.#promise; + this.#collectStructElements(tree); + this.#treeDom = this.#walk(tree); } catch (ex) { reject(ex); } @@ -6971,11 +6988,54 @@ class StructTreeLayerBuilder { this.#treeDom.hidden = false; } } + #collectStructElements(node) { + if (!node) { + return; + } + if (node.structId) { + this.#structElements.getOrInsert(node.structId, node); + } + for (const child of node.children || []) { + this.#collectStructElements(child); + } + } + #getStructElementId(structId) { + return this.#structElementIds.getOrInsertComputed(structId, () => `${this.#structElementIdPrefix}${this.#structElementIds.size}`); + } + #getHeaderIds(headers) { + const result = [], + visited = new Set(), + pending = headers.toReversed(); + while (pending.length > 0) { + const structId = pending.pop(); + if (visited.has(structId)) { + continue; + } + visited.add(structId); + const header = this.#structElements.get(structId); + if (header?.role !== "TH") { + continue; + } + result.push(this.#getStructElementId(structId)); + if (header.headers) { + for (let i = header.headers.length - 1; i >= 0; i--) { + pending.push(header.headers[i]); + } + } + } + return result; + } #setAttributes(structElement, htmlElement) { const { alt, + colSpan, + headers, id, - lang + lang, + rowSpan, + short, + structId, + summary } = structElement; if (alt !== undefined) { let added = false; @@ -6986,16 +7046,44 @@ class StructTreeLayerBuilder { added = true; } } - if (!added) { - htmlElement.setAttribute("aria-label", label); + const role = htmlElement.getAttribute("role") || (htmlElement.localName === "span" ? "generic" : null); + if (!added && role !== "none") { + htmlElement.setAttribute(ARIA_ROLES_WITH_PROHIBITED_NAMES.has(role) ? "aria-description" : "aria-label", label); } } if (id !== undefined) { htmlElement.setAttribute("aria-owns", id); } + if (structId !== undefined && this.#structElements.get(structId) === structElement) { + const elementId = this.#getStructElementId(structId); + if (short !== undefined) { + const abbreviation = document.createElement("span"); + abbreviation.setAttribute("id", elementId); + abbreviation.setAttribute("aria-hidden", "true"); + abbreviation.textContent = removeNullCharacters(short); + htmlElement.append(abbreviation); + } else { + htmlElement.setAttribute("id", elementId); + } + } if (lang !== undefined) { htmlElement.setAttribute("lang", removeNullCharacters(lang, true)); } + if (rowSpan !== undefined) { + htmlElement.setAttribute("aria-rowspan", rowSpan); + } + if (colSpan !== undefined) { + htmlElement.setAttribute("aria-colspan", colSpan); + } + if (headers?.length > 0) { + const headerIds = this.#getHeaderIds(headers); + if (headerIds.length > 0) { + htmlElement.setAttribute("aria-describedby", headerIds.join(" ")); + } + } + if (summary !== undefined) { + htmlElement.setAttribute("aria-description", removeNullCharacters(summary)); + } } #addImageInTextLayer(node, element) { const { @@ -7112,7 +7200,24 @@ class StructTreeLayerBuilder { element.setAttribute("role", "heading"); element.setAttribute("aria-level", match[1]); } else if (PDF_ROLE_TO_HTML_ROLE[role]) { - element.setAttribute("role", role === "TH" && parentNodes.at(-1)?.role === "TR" && parentNodes.at(-2)?.role === "TBody" ? "rowheader" : PDF_ROLE_TO_HTML_ROLE[role]); + let htmlRole = PDF_ROLE_TO_HTML_ROLE[role]; + if (role === "TH") { + if (node.scope === "Row") { + htmlRole = "rowheader"; + } else if (node.scope === "Column") { + htmlRole = "columnheader"; + } else if (parentNodes.at(-1)?.role === "TR" && parentNodes.at(-2)?.role === "TBody") { + htmlRole = "rowheader"; + } + } else if (role === "Caption") { + const parentRole = parentNodes.at(-1)?.role; + if (parentRole !== "Table" && parentRole !== "Figure") { + htmlRole = null; + } + } + if (htmlRole) { + element.setAttribute("role", htmlRole); + } } if (role === "Figure" && this.#addImageInTextLayer(node, element)) { return element; @@ -7135,7 +7240,7 @@ class StructTreeLayerBuilder { element ||= document.createElement("span"); this.#setAttributes(node, element); if (node.children) { - if (node.children.length === 1 && "id" in node.children[0]) { + if (node.children.length === 1 && !("role" in node.children[0]) && "id" in node.children[0] && element.getAttribute("role") !== "none") { this.#setAttributes(node.children[0], element); } else if (visitChildren) { parentNodes.push(node); @@ -8204,6 +8309,7 @@ class PDFPageView extends BasePDFPageView { } } this.cssTransform({}); + this.annotationEditorLayer?.update(this.viewport); this.reset({ keepAnnotationLayer: true, keepAnnotationEditorLayer: true, @@ -8625,8 +8731,10 @@ class PDFViewer { #eventAC = null; #minDurationToUpdateCanvas = 0; #mlManager = null; + #panPosition = [NaN, NaN]; #printingAllowed = true; #scrollTimeoutId = null; + #staleLocation = false; #switchAnnotationEditorModeAC = null; #switchAnnotationEditorModeTimeoutId = null; #copyAllInProgress = false; @@ -8643,7 +8751,7 @@ class PDFViewer { #savedPageViews = null; #deletedPageNumbers = null; constructor(options) { - const viewerVersion = "6.3.72"; + const viewerVersion = "6.3.183"; if (version !== viewerVersion) { throw new Error(`The API version "${version}" does not match the Viewer version "${viewerVersion}".`); } @@ -8927,14 +9035,14 @@ class PDFViewer { this.#setPrintingAllowed(true); return params; } - this.#setPrintingAllowed(permissions.includes(PermissionFlag.PRINT_HIGH_QUALITY) || permissions.includes(PermissionFlag.PRINT)); - if (!permissions.includes(PermissionFlag.COPY) && this.#textLayerMode === TextLayerMode.ENABLE) { + this.#setPrintingAllowed(permissions.has(PermissionFlag.PRINT_HIGH_QUALITY) || permissions.has(PermissionFlag.PRINT)); + if (!permissions.has(PermissionFlag.COPY) && this.#textLayerMode === TextLayerMode.ENABLE) { params.textLayerMode = TextLayerMode.ENABLE_PERMISSIONS; } - if (!permissions.includes(PermissionFlag.MODIFY_CONTENTS)) { + if (!permissions.has(PermissionFlag.MODIFY_CONTENTS)) { params.annotationEditorMode = AnnotationEditorType.DISABLE; } - if (!permissions.includes(PermissionFlag.MODIFY_ANNOTATIONS) && !permissions.includes(PermissionFlag.FILL_INTERACTIVE_FORMS) && this.#annotationMode === AnnotationMode.ENABLE_FORMS) { + if (!permissions.has(PermissionFlag.MODIFY_ANNOTATIONS) && !permissions.has(PermissionFlag.FILL_INTERACTIVE_FORMS) && this.#annotationMode === AnnotationMode.ENABLE_FORMS) { params.annotationMode = AnnotationMode.ENABLE; } return params; @@ -9476,15 +9584,46 @@ class PDFViewer { #isSameScale(newScale) { return newScale === this._currentScale || Math.abs(newScale - this._currentScale) < 1e-15; } + panBy(dx, dy) { + const { + container + } = this; + const position = this.#panPosition; + const { + scrollLeft, + scrollTop + } = container; + const left = (Math.abs(scrollLeft - position[0]) < 1 ? position[0] : scrollLeft) - dx; + const top = (Math.abs(scrollTop - position[1]) < 1 ? position[1] : scrollTop) - dy; + position[0] = left; + position[1] = top; + container.scrollLeft = left; + container.scrollTop = top; + this.#staleLocation = true; + } + #refreshLocation() { + if (!this.#staleLocation) { + return; + } + const { + first + } = this._getVisiblePages(); + if (first) { + this._updateLocation(first); + } + } #setScaleUpdatePages(newScale, newValue, { noScroll = false, preset = false, drawingDelay = -1, - origin = null + origin = null, + pan = null }) { - this.clearSelection(); this._currentScaleValue = newValue.toString(); if (this.#isSameScale(newScale)) { + if (pan && !noScroll) { + this.panBy(pan[0], pan[1]); + } if (preset) { this.eventBus.dispatch("scalechanging", { source: this, @@ -9494,6 +9633,7 @@ class PDFViewer { } return; } + this.clearSelection(); this.viewer.style.setProperty("--scale-factor", newScale * PixelsPerInch.PDF_TO_CSS_UNITS); const postponeDrawing = drawingDelay >= 0 && drawingDelay < 1000; this.refresh(true, { @@ -9509,6 +9649,7 @@ class PDFViewer { const previousScale = this._currentScale; this._currentScale = newScale; if (!noScroll) { + this.#refreshLocation(); let page = this._currentPageNumber, dest; if (this._location && !(this.isInPresentationMode || this.isChangingPresentationMode)) { @@ -9522,11 +9663,16 @@ class PDFViewer { destArray: dest, allowNegativeOffset: true }); + let dx = pan?.[0] ?? 0, + dy = pan?.[1] ?? 0; if (Array.isArray(origin)) { const scaleDiff = newScale / previousScale - 1; const [top, left] = this.containerTopLeft; - this.container.scrollLeft += (origin[0] - left) * scaleDiff; - this.container.scrollTop += (origin[1] - top) * scaleDiff; + dx -= (origin[0] - left) * scaleDiff; + dy -= (origin[1] - top) * scaleDiff; + } + if (dx || dy) { + this.panBy(dx, dy); } } this.eventBus.dispatch("scalechanging", { @@ -9712,6 +9858,7 @@ class PDFViewer { }); } _updateLocation(firstPage) { + this.#staleLocation = false; const currentScale = this._currentScale; const currentScaleValue = this._currentScaleValue; const normalizedScaleValue = parseFloat(currentScaleValue) === currentScale ? Math.round(currentScale * 10000) / 100 : currentScaleValue; @@ -10144,7 +10291,8 @@ class PDFViewer { drawingDelay, scaleFactor = null, steps = null, - origin + origin, + pan = null }) { if (steps === null && scaleFactor === null) { throw new Error("Invalid updateScale options: either `steps` or `scaleFactor` must be provided."); @@ -10167,7 +10315,8 @@ class PDFViewer { this.#setScale(newScale, { noScroll: false, drawingDelay, - origin + origin, + pan }); } increaseScale(options = {}) { @@ -10850,7 +10999,7 @@ const PDFViewerApplication = { this.toolbar = new Toolbar(appConfig.toolbar, eventBus, nimbusData); } if (appConfig.secondaryToolbar) { - if (AppOptions.get("enableAltText")) { + if (AppOptions.get("enableAltText") && this.imageAltTextSettings) { appConfig.secondaryToolbar.imageAltTextSettingsButton?.classList.remove("hidden"); appConfig.secondaryToolbar.imageAltTextSettingsSeparator?.classList.remove("hidden"); } @@ -10955,7 +11104,7 @@ const PDFViewerApplication = { get initializedPromise() { return this._initializedCapability.promise; }, - updateZoom(steps, scaleFactor, origin) { + updateZoom(steps, scaleFactor, origin, pan = null) { if (this.pdfViewer.isInPresentationMode) { return; } @@ -10963,7 +11112,8 @@ const PDFViewerApplication = { drawingDelay: AppOptions.get("defaultZoomDelay"), steps, scaleFactor, - origin + origin, + pan }); }, zoomIn() { @@ -10978,16 +11128,26 @@ const PDFViewerApplication = { } this.pdfViewer.currentScaleValue = (/* inlined export .DEFAULT_SCALE_VALUE */"auto"); }, - touchPinchCallback(origin, prevDistance, distance) { + touchPinchCallback(origin, prevDistance, distance, panX, panY) { + const pan = [panX, panY]; if (this.supportsPinchToZoom) { const newScaleFactor = this._accumulateFactor(this.pdfViewer.currentScale, distance / prevDistance, "_touchUnusedFactor"); - this.updateZoom(null, newScaleFactor, origin); + this.updateZoom(null, newScaleFactor, origin, pan); } else { const PIXELS_PER_LINE_SCALE = 30; const ticks = this._accumulateTicks((distance - prevDistance) / PIXELS_PER_LINE_SCALE, "_touchUnusedTicks"); - this.updateZoom(ticks, null, origin); + this.updateZoom(ticks, null, origin, pan); } }, + touchPanCallback(dx, dy) { + const { + pdfViewer + } = this; + if (!this.pdfDocument || pdfViewer.isInPresentationMode) { + return; + } + pdfViewer.panBy(dx, dy); + }, touchPinchEndCallback() { this._touchUnusedTicks = 0; this._touchUnusedFactor = 1; @@ -11800,6 +11960,7 @@ const PDFViewerApplication = { isPinchingStopped: () => this.overlayManager?.active, onPinching: this.touchPinchCallback.bind(this), onPinchEnd: this.touchPinchEndCallback.bind(this), + onPanning: this.touchPanCallback.bind(this), signal }); function addWindowResolutionChange(evt = null) { diff --git a/toolkit/components/pdfjs/content/web/viewer.css b/toolkit/components/pdfjs/content/web/viewer.css index e2ae210a51c0..dd5df89becdf 100644 --- a/toolkit/components/pdfjs/content/web/viewer.css +++ b/toolkit/components/pdfjs/content/web/viewer.css @@ -75,6 +75,12 @@ --button-disabled-opacity:0.6; --hover-filter:brightness(1.4); } + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + --hover-filter:none; + --button-disabled-opacity:var(--button-opacity-disabled); + --button-secondary-hover-border-color:var(--button-border-color-hover); + --button-secondary-active-border-color:var(--button-border-color-active); + } @media screen and (forced-colors: active){ --button-primary-bg-color:var( @@ -258,6 +264,9 @@ --button-background-color, light-dark(rgb(21 20 26 / 0.07), rgb(251 251 254 / 0.07)) ); + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + --message-bar-close-button-border-radius:var(--button-border-radius); + } @media screen and (forced-colors: active){ --message-bar-close-button-color:var(--button-text-color, ButtonText); @@ -376,6 +385,11 @@ rgb(0 0 0 / 0.08), rgb(255 255 255 / 0.08) ); + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + --message-bar-icon-color:var(--icon-color-information); + --message-bar-bg-color:var(--background-color-box); + --message-bar-border-color:var(--border-color-deemphasized); + } @media screen and (forced-colors: active){ --message-bar-icon-color:CanvasText; @@ -426,6 +440,11 @@ --input-text-bg-color:light-dark(white, #42414d); --input-text-fg-color:var(--text-primary-color); + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + --dialog-border-color:var(--border-color-deemphasized); + --dialog-shadow:var(--box-shadow-level-3); + --separator-color:var(--border-color-deemphasized); + } @media screen and (forced-colors: active){ --dialog-bg-color:var(--background-color-canvas, Canvas); @@ -1876,6 +1895,10 @@ --button-signature-border:none; --button-signature-hover-bg:light-dark(#e0e0e6, #52525e); --button-signature-hover-color:var(--button-signature-color); + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + --button-signature-hover-bg:var(--button-background-color-hover); + --button-signature-active-bg:var(--button-background-color-active); + } @media screen and (forced-colors: active){ --signature-bg:HighlightText; @@ -2214,6 +2237,9 @@ margin:0; background-color:var(--thickness-bg); border-radius:4px 4px 0 0; + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + border-radius:var(--border-radius-small) var(--border-radius-small) 0 0; + } border-inline:var(--thickness-border); border-top:var(--thickness-border); pointer-events:auto; @@ -2455,6 +2481,9 @@ button{ border:var(--button-signature-border); border-radius:4px; + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + border-radius:var(--border-radius-small); + } background-color:var(--button-signature-bg); color:var(--button-signature-color); @@ -2497,6 +2526,9 @@ justify-content:flex-start; outline:none; border-radius:4px; + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + border-radius:var(--border-radius-small); + } box-sizing:border-box; font:message-box; position:relative; @@ -2518,6 +2550,9 @@ box-sizing:border-box; border:none; border-radius:4px; + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + border-radius:var(--border-radius-small); + } > path{ stroke:var(--button-signature-color); @@ -2537,6 +2572,9 @@ &:is(:hover, :active) > svg{ border-radius:4px 0 0 4px; + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + border-radius:var(--border-radius-small) 0 0 var(--border-radius-small); + } background-color:var(--signature-hover-bg); } @@ -2593,6 +2631,9 @@ box-sizing:border-box; border-radius:8px; + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + border-radius:var(--border-radius-medium); + } } #commentManagerDialog{ @@ -2826,6 +2867,22 @@ --button-comment-border:none; --button-comment-hover-bg:light-dark(#e0e0e6, #52525e); --button-comment-hover-color:var(--button-comment-color); + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + --comment-date-fg-color:color-mix( + in srgb, + var(--comment-fg-color) 69%, + transparent + ); + --comment-bg-color:var(--background-color-box-info); + --comment-border-color:var(--border-color-deemphasized); + --comment-count-bg-color:var(--background-color-information); + --comment-indicator-active-fg-color:var(--color-accent-primary-active); + --comment-indicator-focus-fg-color:var(--icon-color); + + --button-comment-color:var(--toolbarbutton-icon-fill); + --button-comment-active-bg:var(--toolbarbutton-background-color-active); + --button-comment-hover-bg:var(--toolbarbutton-background-color-hover); + } @media screen and (forced-colors: active){ --comment-date-fg-color:CanvasText; @@ -2895,6 +2952,9 @@ font-style:normal; font-weight:400; line-height:normal; + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + border-radius:var(--border-radius-small); + } } } @@ -2945,6 +3005,14 @@ height:0; overflow:hidden; } + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + border-radius:var(--button-border-radius); + + &:focus-visible{ + outline:var(--focus-outline); + outline-offset:var(--focus-outline-offset); + } + } } } @@ -2973,6 +3041,10 @@ border-radius:8px; border:0.5px solid var(--comment-border-color); background-color:var(--comment-bg-color); + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + border-radius:var(--border-radius-medium); + border-width:1px; + } &:not(.noComments){ &:hover{ @@ -3487,6 +3559,17 @@ --alt-text-warning-color:light-dark(#0090ed, #80ebff); --alt-text-hover-done-color:var(--alt-text-done-color); --alt-text-hover-warning-color:var(--alt-text-warning-color); + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + --editor-toolbar-bg-color:var(--background-color-box); + --editor-toolbar-fg-color:var(--toolbarbutton-icon-fill); + --editor-toolbar-border-color:var(--border-color-deemphasized); + --editor-toolbar-hover-bg-color:var( + --toolbarbutton-background-color-hover + ); + --editor-toolbar-shadow:var(--box-shadow-level-2); + --editor-toolbar-height:var(--size-item-large); + --alt-text-warning-color:var(--icon-color-information); + } @media screen and (forced-colors: active){ --editor-toolbar-bg-color:var(--button-background-color, ButtonFace); @@ -3537,6 +3620,10 @@ border:1px solid var(--editor-toolbar-border-color); box-shadow:var(--editor-toolbar-shadow); + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + border-radius:var(--panel-border-radius); + } + &.hidden{ display:none; } @@ -3630,6 +3717,11 @@ border-radius:2px; outline:2px solid var(--editor-toolbar-focus-outline-color); } + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + &:is(:hover, :focus-visible){ + border-radius:var(--button-border-radius); + } + } } .altText{ @@ -3708,6 +3800,13 @@ ); --alt-text-tooltip-border:var(--border-color-interactive, #8f8f9d); --alt-text-tooltip-shadow:0 2px 6px 0 light-dark(rgb(58 57 68 / 0.2), #15141a); + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + --alt-text-tooltip-bg:var(--background-color-box); + --alt-text-tooltip-border:var(--border-color-deemphasized); + --alt-text-tooltip-shadow:var(--box-shadow-level-2); + + border-radius:var(--border-radius-xsmall); + } @media screen and (forced-colors: active){ --alt-text-tooltip-bg:Canvas; @@ -3834,6 +3933,11 @@ --no-alt-text-badge-border-color:light-dark(#f0f0f4, #52525e); --no-alt-text-badge-bg-color:light-dark(#cfcfd8, #fbfbfe); --no-alt-text-badge-fg-color:light-dark(#5b5b66, #15141a); + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + --no-alt-text-badge-border-color:var(--border-color-deemphasized); + --no-alt-text-badge-bg-color:var(--background-color-box); + --no-alt-text-badge-fg-color:var(--icon-color); + } @media screen and (forced-colors: active){ --no-alt-text-badge-border-color:ButtonText; @@ -3857,6 +3961,10 @@ border:1px solid var(--no-alt-text-badge-border-color); background:var(--no-alt-text-badge-bg-color); + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + border-radius:var(--border-radius-xsmall); + } + &::before{ content:""; display:inline-block; @@ -4118,6 +4226,9 @@ --new-alt-text-spinner-icon:url(images/altText_spinner.svg); --preview-image-bg-color:light-dark(#f0f0f4, #2b2a33); --preview-image-border:none; + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + --preview-image-bg-color:var(--background-color-box-info); + } @media screen and (forced-colors: active){ --preview-image-bg-color:ButtonFace; @@ -4301,6 +4412,10 @@ ); --selected-outline-color:light-dark(#0060df, #aaf2ff); --swatch-border-color:light-dark(#cfcfd8, #52525e); + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + --selected-outline-color:var(--color-accent-primary); + --swatch-border-color:var(--border-color-deemphasized); + } @media screen and (forced-colors: active){ --hover-outline-color:Highlight; @@ -4325,6 +4440,11 @@ .basicColorPicker{ width:28px; + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + width:auto; + aspect-ratio:1; + padding:var(--space-small); + } &::-moz-color-swatch{ border-radius:100%; @@ -4425,6 +4545,10 @@ &:has(.dropdown:not(.hidden)){ background-color:var(--editor-toolbar-hover-bg-color); + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + background-color:var(--toolbarbutton-background-color-active); + border-radius:var(--button-border-radius); + } &::after{ scale:-1; @@ -4472,6 +4596,25 @@ outline:2px solid var(--hover-outline-color); } } + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + flex-direction:row; + gap:var(--space-small); + padding:var(--space-small); + width:max-content; + inset-inline-start:50%; + translate:calc(-50% * var(--dir-factor)); + border-radius:var(--border-radius-medium); + + button{ + width:auto; + flex:0 0 auto; + + > .swatch{ + width:var(--size-item-medium); + height:var(--size-item-medium); + } + } + } } } } @@ -4703,6 +4846,18 @@ button.hasPopupMenu{ ); --menuitem-hover-background-blend-mode:normal; --disabled-opacity:0.62; + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + --menuitem-gap:var(--space-small); + --menuitem-border-radius:var(--panel-menuitem-border-radius); + --menu-box-shadow:var(--box-shadow-level-2); + --menu-border-color:var(--border-color-deemphasized); + --menuitem-hover-bg:var(--button-background-color-menu-hover); + --menuitem-text-hover-fg:var(--button-text-color-menu-hover); + --menuitem-active-bg:var(--button-background-color-menu-active); + --menuitem-text-active-fg:var(--button-text-color-menu-active); + --menuitem-focus-border-color:transparent; + --disabled-opacity:var(--button-opacity-disabled); + } @media screen and (forced-colors: active){ --menu-bg:var(--background-color-box, Canvas); @@ -4741,6 +4896,10 @@ button.hasPopupMenu{ border-radius:6px; border:1px solid var(--menu-border-color); backdrop-filter:var(--menu-backdrop-filter); + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + padding:var(--space-small) 0; + border-radius:var(--panel-border-radius); + } &.withMark{ --menu-mark-icon-size:16px; @@ -4822,6 +4981,10 @@ button.hasPopupMenu{ background-color:var(--menuitem-focus-bg); outline:2px solid var(--menuitem-focus-outline-color); outline-offset:2px; + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + outline:var(--focus-outline); + outline-offset:var(--focus-outline-inset); + } } } @@ -4852,6 +5015,30 @@ button.hasPopupMenu{ font-weight:510; line-height:normal; } + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + height:auto; + min-height:var(--size-item-large); + width:auto; + margin:var(--panel-menuitem-margin); + padding:var(--panel-menuitem-padding); + + &.selected::after{ + inset-inline-start:var(--panel-menuitem-padding-inline); + } + + > span{ + padding-inline-start:0; + } + } + } + + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + &.withMark > button{ + padding-inline-start:calc( + var(--panel-menuitem-padding-inline) + var(--menu-mark-icon-size) + + var(--menuitem-gap) + ); + } } } @@ -4869,6 +5056,15 @@ button.hasPopupMenu{ ); --treeitem-expanded-icon:url(images/treeitem-expanded.svg); --treeitem-collapsed-icon:url(images/treeitem-collapsed.svg); + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + --treeitem-color:var(--text-color); + --treeitem-bg-color:var(--toolbarbutton-background-color-hover); + --treeitem-hover-color:var(--text-color); + --treeitem-selected-color:var(--text-color); + --treeitem-selected-bg-color:var(--toolbarbutton-background-color-active); + + padding-inline:var(--panel-menuitem-margin-inline); + } &.withNesting{ .treeItemToggler{ @@ -4908,6 +5104,9 @@ button.hasPopupMenu{ background-clip:padding-box; border-radius:2px; color:var(--treeitem-hover-color); + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + border-radius:var(--panel-menuitem-border-radius); + } } } @@ -4948,12 +5147,23 @@ button.hasPopupMenu{ user-select:none; white-space:normal; cursor:default; + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + box-sizing:border-box; + min-width:100%; + padding-block:var(--space-xsmall); + padding-inline:var(--space-small); + border-radius:var(--panel-menuitem-border-radius); + } &:hover{ background-color:var(--treeitem-bg-color); background-clip:padding-box; border-radius:2px; color:var(--treeitem-hover-color); + + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + border-radius:var(--panel-menuitem-border-radius); + } } } @@ -5080,6 +5290,28 @@ button.hasPopupMenu{ --image-dragging-shadow:0 0 0 var(--image-border-width) var(--image-current-border-color); --multiple-dragging-indicator-bg:var(--indicator-color); --multiple-dragging-text-color:light-dark(#fbfbfe, #15141a); + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + --sidebar-bg-color:var(--background-color-box); + --sidebar-backdrop-filter:none; + --header-bg:var(--background-color-box); + --status-undo-bg:color-mix( + in srgb, + var(--color-accent-primary) 8%, + transparent + ); + --status-warning-bg:var(--background-color-box-info); + --indicator-warning-color:var(--icon-color-critical); + --image-border-color:var(--border-color-deemphasized); + --image-hover-border-color:var(--border-color-interactive); + --image-page-number-bg:var(--background-color-box-info); + --image-current-page-number-fg:var(--button-text-color-primary); + --image-dragging-placeholder-bg:color-mix( + in srgb, + var(--color-accent-primary) 8%, + transparent + ); + --multiple-dragging-bg:var(--background-color-box); + } @media screen and (forced-colors: active){ --views-text-color:var(--text-color, CanvasText); @@ -5192,6 +5424,14 @@ button.hasPopupMenu{ } } } + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + padding-block-start:0; + + #viewsManagerHeader{ + border-start-start-radius:var(--panel-border-radius); + border-start-end-radius:var(--panel-border-radius); + } + } #viewsManagerHeader{ display:flex; @@ -5611,6 +5851,10 @@ button.hasPopupMenu{ min-height:24px; padding:4px 16px; + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + border-radius:var(--button-border-radius); + } + font:menu; font-size:13px; font-style:normal; @@ -5653,6 +5897,9 @@ button.hasPopupMenu{ &:not(.isDragging) > .thumbnailImageContainer::after{ content:attr(page-number); border-radius:8px; + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + border-radius:var(--border-radius-circle); + } border:1px solid var(--image-page-number-border-color); background-color:var(--image-page-number-bg); color:var(--image-page-number-fg); @@ -5704,6 +5951,9 @@ button.hasPopupMenu{ outline:var(--image-outline); user-select:none; position:relative; + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + border-radius:var(--border-radius-medium); + } img{ width:100%; @@ -5713,6 +5963,10 @@ button.hasPopupMenu{ outline:none; user-select:none; pointer-events:none; + + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + border-radius:var(--border-radius-medium); + } } &.missingThumbnailImage{ @@ -5826,11 +6080,22 @@ button.hasPopupMenu{ box-sizing:content-box; outline:none; user-select:none; + + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + border-radius:var(--border-radius-medium); + } } &::after{ content:attr(data-multiple-count); border-radius:calc(8px * var(--thumbnail-dragging-scale)); + + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + border-radius:calc( + var(--border-radius-medium) * + var(--thumbnail-dragging-scale) + ); + } background-color:var(--multiple-dragging-indicator-bg); color:var(--multiple-dragging-text-color); position:absolute; @@ -5862,10 +6127,18 @@ button.hasPopupMenu{ rgb(0 0 0 / 0.9), rgb(255 255 255 / 0.9) ); + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + --attachment-color:var(--text-color); + --attachment-hover-color:var(--text-color); + --attachment-bg-color:var(--toolbarbutton-background-color-hover); + } > ul{ list-style-type:none; padding:0; + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + padding-inline:var(--panel-menuitem-margin-inline); + } > li > a{ text-decoration:none; @@ -5889,6 +6162,17 @@ button.hasPopupMenu{ border-radius:2px; color:var(--attachment-hover-color); } + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + box-sizing:border-box; + min-width:100%; + padding-block:var(--space-xsmall); + padding-inline:var(--space-small); + + &, + &:hover{ + border-radius:var(--panel-menuitem-border-radius); + } + } } } } @@ -5914,6 +6198,11 @@ button.hasPopupMenu{ --color-accent-primary, light-dark(#0062fa, #00cadb) ); + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + --sidebar-border-color:var(--border-color-deemphasized); + --sidebar-box-shadow:var(--box-shadow-level-2); + --sidebar-border-radius:var(--panel-border-radius); + } @media screen and (forced-colors: active){ --sidebar-bg-color:var(--background-color-box, Canvas); @@ -5957,6 +6246,10 @@ button.hasPopupMenu{ background-color:var(--resizer-hover-bg-color); outline:none; } + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + border-inline-width:0; + border-radius:var(--border-radius-large); + } } &.resizing{ @@ -5990,6 +6283,11 @@ button.hasPopupMenu{ --new-badge-bg:light-dark(#070, #37b847); --new-badge-color:light-dark(#fff, #15141a); --new-badge-border-color:light-dark(#fbfbfe / 40%, #15141a / 40%); + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + --new-badge-bg:var(--background-color-success); + --new-badge-color:var(--text-color); + --new-badge-border-color:transparent; + } @media screen and (forced-colors: active){ --pdfViewer-padding-bottom:9px; @@ -6012,6 +6310,11 @@ button.hasPopupMenu{ border-radius:4px; border:1px solid var(--new-badge-border-color); padding-inline:4px; + + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + border-radius:var(--border-radius-circle); + padding-inline:var(--space-small); + } margin-inline:4px; font:menu; font-size:12px; @@ -6218,6 +6521,22 @@ button.hasPopupMenu{ --icon-color-success, light-dark(rgb(29 142 61), rgb(106 210 126)) ); + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + --sig-card-border:var(--border-color-deemphasized); + --sig-card-nested-bg:var(--background-color-box-info); + --sig-row-color:var(--text-color); + --sig-detail-color:var(--text-color-deemphasized); + --sig-divider-color:var(--border-color-deemphasized); + --sig-summary-hover-color:var(--link-color-hover); + --sig-link-hover-bg:var(--button-background-color-ghost-hover); + --sig-banner-verified-bg:var(--background-color-box-info); + --sig-banner-warn-bg:var(--background-color-box-info); + --sig-banner-error-bg:var(--background-color-box-info); + --sig-banner-verified-color:var(--text-color); + --sig-banner-warn-color:var(--text-color); + --sig-banner-error-color:var(--text-color); + --sig-icon-default:var(--text-color-deemphasized); + } @media screen and (forced-colors: active){ --sig-card-border:ButtonBorder; @@ -6322,6 +6641,9 @@ button.hasPopupMenu{ font-size:12.5px; line-height:1.35; border-inline-start:3px solid currentcolor; + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + border-radius:var(--border-radius-medium); + } &.verified{ background:var(--sig-banner-verified-bg); @@ -6358,6 +6680,9 @@ button.hasPopupMenu{ flex-direction:column; gap:3px; background:var(--sig-card-bg); + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + border-radius:var(--border-radius-medium); + } .signer{ font-weight:600; @@ -6440,6 +6765,10 @@ button.hasPopupMenu{ padding:2px 4px; border-radius:4px; white-space:nowrap; + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + padding-inline:var(--space-small); + border-radius:var(--button-border-radius); + } &:hover{ background:var(--sig-link-hover-bg); @@ -6448,6 +6777,11 @@ button.hasPopupMenu{ &:focus-visible{ outline:2px solid var(--sig-link-color); outline-offset:1px; + + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + outline:var(--focus-outline); + outline-offset:var(--focus-outline-offset); + } } } @@ -6456,6 +6790,9 @@ button.hasPopupMenu{ border-top:1px dashed var(--sig-divider-color); padding-top:4px; font-size:12px; + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + border-top-style:solid; + } > summary{ cursor:pointer; @@ -6504,6 +6841,9 @@ button.hasPopupMenu{ padding:6px 8px; background:var(--sig-card-nested-bg); gap:2px; + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + border-radius:var(--border-radius-small); + } } .subSignatures .signer, @@ -6617,6 +6957,54 @@ button.hasPopupMenu{ --secondaryToolbarButton-documentProperties-icon:url(images/secondaryToolbarButton-documentProperties.svg); --editorParams-stampAddImage-icon:url(images/toolbarButton-zoomIn.svg); --comment-edit-button-icon:url(images/comment-editButton.svg); + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + font-size:var(--font-size-root); + + --toolbarButton-menuArrowNova-icon:url(images/toolbarButton-menuArrowNova.svg); + + --toolbar-height:40px; + --toolbar-horizontal-padding:6px; + --toolbar-vertical-padding:4px; + + --toolbar-icon-opacity:1; + --doorhanger-icon-opacity:1; + + --main-color:var(--text-color); + --body-bg-color:light-dark(var(--color-gray-15), var(--color-gray-85)); + --progressBar-color:var(--color-accent-primary); + --progressBar-bg-color:var(--background-color-box-info); + --progressBar-blend-color:color-mix( + in srgb, + var(--color-accent-primary) 40%, + var(--background-color-box) + ); + --toolbar-icon-bg-color:var(--toolbarbutton-icon-fill); + --toolbar-icon-hover-bg-color:var(--toolbarbutton-icon-fill); + + --sidebar-narrow-bg-color:color-mix( + in srgb, + var(--background-color-canvas) 90%, + transparent + ); + --sidebar-toolbar-bg-color:var(--toolbar-background-color); + --toolbar-bg-color:var(--toolbar-background-color); + --toolbar-border-color:var(--border-color-deemphasized); + --toggled-btn-color:var(--toolbarbutton-icon-fill); + --toggled-btn-bg-color:var(--toolbarbutton-background-color-active); + --toggled-hover-active-btn-color:var( + --toolbarbutton-background-color-active + ); + --dropdown-btn-bg-color:var(--button-background-color); + --dropdown-btn-border:1px solid var(--button-border-color); + --separator-color:var(--border-color-deemphasized); + --field-color:var(--text-color); + --field-bg-color:var(--background-color-box); + --field-border-color:var(--border-color-interactive); + --doorhanger-bg-color:var(--background-color-box); + --doorhanger-border-color:var(--border-color-deemphasized); + --doorhanger-hover-color:var(--text-color); + --doorhanger-separator-color:var(--border-color-deemphasized); + } } :root:dir(rtl){ @@ -6668,9 +7056,15 @@ button.hasPopupMenu{ html{ &[data-toolbar-density="compact"]{ --toolbar-height:30px; + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + --toolbar-height:32px; + } } &[data-toolbar-density="touch"]{ --toolbar-height:44px; + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + --toolbar-height:48px; + } } } @@ -6864,6 +7258,16 @@ body{ &::after{ border-width:var(--doorhanger-height); } + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + border-radius:var(--panel-border-radius); + border:1px solid var(--doorhanger-border-color); + box-shadow:var(--box-shadow-level-2); + + &::after, + &::before{ + display:none; + } + } } .doorHangerRight{ @@ -6877,6 +7281,9 @@ body{ border-bottom-color:var(--doorhanger-bg-color); inset-inline-end:1px; } + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + inset-inline-end:0; + } } .doorHanger{ @@ -6890,6 +7297,10 @@ body{ border-bottom-color:var(--toolbar-bg-color); inset-inline-start:1px; } + + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + inset-inline-start:0; + } } .splitToolbarButtonSeparator{ @@ -6978,6 +7389,10 @@ body{ height:9px; width:9px; border-radius:50%; + + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + background-color:var(--color-accent-attention); + } } .verticalToolbarSeparator{ @@ -7038,6 +7453,19 @@ body{ &:focus{ border-color:#0a84ff; } + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + border-radius:var(--border-radius-small); + padding:var(--space-xsmall) var(--space-small); + + &:focus{ + border-color:var(--field-border-color); + } + + &:focus-visible{ + outline:var(--focus-outline); + outline-offset:var(--focus-outline-inset); + } + } } #pageNumber{ @@ -7137,6 +7565,25 @@ dialog .buttonRow{ dialog :link{ color:rgb(255 255 255); } +@media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + dialog:not(.dialog){ + padding:var(--space-large); + font-size:var(--font-size-small); + line-height:150%; + border-spacing:var(--space-xsmall); + background-color:var(--background-color-canvas); + border:1px solid var(--border-color); + box-shadow:var(--box-shadow-level-3); + + .separator{ + margin-block:var(--space-small); + } + + :link{ + color:var(--link-color); + } + } +} #passwordDialog{ text-align:center; @@ -7291,6 +7738,24 @@ dialog :link{ height:auto; } } + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + border-radius:var(--button-border-radius); + + &:focus-visible{ + outline:var(--focus-outline); + outline-offset:var(--focus-outline-inset); + } + + &.toggled:hover:focus-visible{ + --toggled-hover-btn-outline:var(--focus-outline); + } + + &.labeled{ + border-radius:var(--panel-menuitem-border-radius); + gap:var(--space-small); + padding-inline-start:var(--space-small); + } + } } .toolbarButtonWithContainer{ @@ -7307,6 +7772,10 @@ dialog :link{ .menu{ padding-block:5px; + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + padding-block:var(--space-small); + padding-inline:var(--space-small); + } } .menuContainer{ @@ -7360,6 +7829,11 @@ dialog :link{ padding-inline:10px; padding-block:10px; + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + padding-inline:var(--space-medium); + padding-block:var(--space-medium); + } + > .editorParamsSetter{ min-height:26px; display:flex; @@ -7391,6 +7865,19 @@ dialog :link{ &::-moz-range-thumb{ background-color:white; } + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + &::-moz-range-progress{ + background-color:var(--color-accent-primary); + } + + &::-moz-range-track{ + background-color:var(--border-color-interactive); + } + + &::-moz-range-thumb{ + background-color:var(--color-accent-primary); + } + } } } } @@ -7502,6 +7989,12 @@ dialog :link{ box-sizing:border-box; flex-wrap:wrap; justify-content:flex-start; + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + --input-horizontal-padding:var(--space-medium); + + border-radius:var(--border-radius-medium); + padding-inline:var(--space-small); + } > *{ height:var(--toolbar-height); @@ -7535,6 +8028,13 @@ dialog :link{ &[data-status="notFound"]{ background-color:rgb(255 102 102); + + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + background-color:var(--background-color-critical); + } + } + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + border-radius:var(--input-search-border-radius); } } } @@ -7551,6 +8051,10 @@ dialog :link{ background-color:rgb(217 217 217); color:rgb(82 82 82); padding-block:4px; + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + background-color:var(--background-color-box-info); + color:var(--text-color-deemphasized); + } &:empty{ display:none; @@ -7662,6 +8166,11 @@ dialog :link{ padding-inline:4px; margin:2px; border-radius:2px; + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + padding-inline:var(--space-small); + border-radius:var(--button-border-radius); + } + color:var(--main-color); font-size:12px; line-height:14px; @@ -7688,6 +8197,10 @@ dialog :link{ justify-content:space-between; gap:1px; box-sizing:border-box; + + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + gap:var(--space-xxsmall); + } } .dropdownToolbarButton{ @@ -7759,10 +8272,73 @@ dialog :link{ &:is(:hover, :focus-visible, :active)::after{ background-color:var(--toolbar-icon-hover-bg-color); } + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + overflow:hidden; + border:var(--button-border); + border-radius:var(--button-border-radius); + background-color:var(--button-background-color); + color:var(--button-text-color); + + &:is(:hover, :has(> select:open)){ + border-color:var(--border-color-interactive-hover); + background-color:var(--button-background-color-hover); + color:var(--button-text-color-hover); + } + + &:has(> select:focus-visible){ + outline:var(--focus-outline); + outline-offset:var(--focus-outline-offset); + } + + > select{ + height:auto; + min-height:var(--button-min-height); + border-radius:inherit; + padding-block:var(--button-padding-block); + padding-inline:var(--button-padding-inline) calc( + var(--button-padding-inline) + var(--icon-size) + var(--space-small) + ); + font-size:var(--font-size-root); + font-weight:var(--button-font-weight); + color:inherit; + background-color:var(--background-color-box); + + > option{ + color:var(--text-color); + font-weight:var(--font-weight); + } + + &:is(:hover, :focus-visible){ + background-color:var(--background-color-box); + color:inherit; + } + + &:focus-visible{ + outline:none; + } + } + + &:is(:hover, :has(> select:open)) > select{ + background-image:linear-gradient( + var(--button-background-color-hover), + var(--button-background-color-hover) + ); + } + + &::after, + &:is(:hover, :focus-visible, :active)::after{ + inset-inline-end:var(--button-padding-inline); + background-color:currentColor; + mask-image:var(--toolbarButton-menuArrowNova-icon); + } + } } #toolbarContainer{ --menuitem-height:calc(var(--toolbar-height) - 6px); + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + --menuitem-height:var(--size-item-large); + } width:100%; height:var(--toolbar-height); @@ -7795,6 +8371,9 @@ dialog :link{ #toolbarViewerLeft{ margin-inline-start:8px; + @media -moz-pref("pdfjs.enableNova") and -moz-pref("browser.nova.enabled"){ + margin-inline-start:0; + } #numPages.toolbarLabel{ padding-inline-start:3px; diff --git a/toolkit/components/pdfjs/content/web/viewer.mjs b/toolkit/components/pdfjs/content/web/viewer.mjs index 2e22ed791ee1..1d57a31191ec 100644 --- a/toolkit/components/pdfjs/content/web/viewer.mjs +++ b/toolkit/components/pdfjs/content/web/viewer.mjs @@ -21,8 +21,8 @@ */ /** - * pdfjsVersion = 6.3.72 - * pdfjsBuild = 71a3c6a89 + * pdfjsVersion = 6.3.183 + * pdfjsBuild = 48bb93b89 */ ;// ./web/ui_utils.js @@ -825,18 +825,16 @@ class AppOptions { continue; } if (this.eventBus && kind & OptionKind.EVENT_DISPATCH) { - (events ||= new Map()).set(name, userOpt); + (events ??= new Map()).set(name, userOpt); } this.#opts.set(name, userOpt); } - if (events) { - for (const [name, value] of events) { - this.eventBus.dispatch(name.toLowerCase(), { - source: this, - value - }); - } - } + events?.forEach((value, name) => { + this.eventBus.dispatch(name.toLowerCase(), { + source: this, + value + }); + }); } } @@ -907,7 +905,7 @@ const { } = globalThis.pdfjsLib; ;// ./web/internal_evt.js -const INTERNAL_EVT = "73d553f8-709f-4713-892b-c46926003d23"; +const INTERNAL_EVT = "df51a2ca-766d-4bd1-bd4e-9faff090d03c"; const internalOpt = Object.freeze({ internal: INTERNAL_EVT }); @@ -2613,7 +2611,7 @@ class NewAltTextManager { this.#uiManager = null; } #extractWords(text) { - return new Set(text.toLowerCase().split(/[^\p{L}\p{N}]+/gu).filter(x => !!x)); + return new Set(text.toLowerCase().split(/[^\p{L}\p{N}]+/gu).filter(Boolean)); } #save() { const altText = this.#textarea.value.trim(); @@ -5751,7 +5749,7 @@ class PDFFindController { } return this._normalizedQuery; } - return (query || []).filter(q => !!q).map(q => normalize(q)[0]); + return (query || []).filter(Boolean).map(q => normalize(q)[0]); } #shouldDirtyMatch(state) { const newQuery = state.query, @@ -10533,6 +10531,15 @@ class AnnotationEditorLayerBuilder { await this.annotationEditorLayer.render(parameters); this.show(); } + update(viewport) { + if (this.div) { + this.annotationEditorLayer.update({ + viewport: viewport.clone({ + dontFlip: true + }) + }); + } + } cancel() { this._cancelled = true; if (!this.div) { @@ -11387,19 +11394,23 @@ const PDF_ROLE_TO_HTML_ROLE = { Document: null, DocumentFragment: null, Part: "group", + Art: "article", Sect: "group", Div: "group", + BlockQuote: "blockquote", Aside: "note", NonStruct: "none", - P: null, + P: "paragraph", H: "heading", Title: null, FENote: "note", Sub: "group", Lbl: null, Span: null, - Em: null, - Strong: null, + Em: "emphasis", + Strong: "strong", + Note: "note", + Code: "code", Link: "link", Annot: "note", Form: "form", @@ -11419,12 +11430,13 @@ const PDF_ROLE_TO_HTML_ROLE = { TD: "cell", THead: "rowgroup", TBody: "rowgroup", - TFoot: null, - Caption: null, + TFoot: "rowgroup", + Caption: "caption", Figure: "figure", Formula: null, Artifact: null }; +const ARIA_ROLES_WITH_PROHIBITED_NAMES = new Set(["caption", "code", "emphasis", "generic", "none", "paragraph", "strong"]); const MathMLElements = new Set(["math", "merror", "mfrac", "mi", "mmultiscripts", "mn", "mo", "mover", "mpadded", "mprescripts", "mroot", "mrow", "ms", "mspace", "msqrt", "mstyle", "msub", "msubsup", "msup", "mtable", "mtd", "mtext", "mtr", "munder", "munderover", "semantics"]); const MathMLNamespace = "http://www.w3.org/1998/Math/MathML"; class MathMLSanitizer { @@ -11449,6 +11461,9 @@ class StructTreeLayerBuilder { #treeDom = null; #treePromise; #elementAttributes = new Map(); + #structElementIdPrefix = `pdfjs_internal_struct_${getUuid()}_`; + #structElementIds = new Map(); + #structElements = new Map(); #rawDims; #elementsToAddToTextLayer = null; #elementsToHideInTextLayer = null; @@ -11468,7 +11483,9 @@ class StructTreeLayerBuilder { } = Promise.withResolvers(); this.#treePromise = promise; try { - this.#treeDom = this.#walk(await this.#promise); + const tree = await this.#promise; + this.#collectStructElements(tree); + this.#treeDom = this.#walk(tree); } catch (ex) { reject(ex); } @@ -11494,11 +11511,54 @@ class StructTreeLayerBuilder { this.#treeDom.hidden = false; } } + #collectStructElements(node) { + if (!node) { + return; + } + if (node.structId) { + this.#structElements.getOrInsert(node.structId, node); + } + for (const child of node.children || []) { + this.#collectStructElements(child); + } + } + #getStructElementId(structId) { + return this.#structElementIds.getOrInsertComputed(structId, () => `${this.#structElementIdPrefix}${this.#structElementIds.size}`); + } + #getHeaderIds(headers) { + const result = [], + visited = new Set(), + pending = headers.toReversed(); + while (pending.length > 0) { + const structId = pending.pop(); + if (visited.has(structId)) { + continue; + } + visited.add(structId); + const header = this.#structElements.get(structId); + if (header?.role !== "TH") { + continue; + } + result.push(this.#getStructElementId(structId)); + if (header.headers) { + for (let i = header.headers.length - 1; i >= 0; i--) { + pending.push(header.headers[i]); + } + } + } + return result; + } #setAttributes(structElement, htmlElement) { const { alt, + colSpan, + headers, id, - lang + lang, + rowSpan, + short, + structId, + summary } = structElement; if (alt !== undefined) { let added = false; @@ -11509,16 +11569,44 @@ class StructTreeLayerBuilder { added = true; } } - if (!added) { - htmlElement.setAttribute("aria-label", label); + const role = htmlElement.getAttribute("role") || (htmlElement.localName === "span" ? "generic" : null); + if (!added && role !== "none") { + htmlElement.setAttribute(ARIA_ROLES_WITH_PROHIBITED_NAMES.has(role) ? "aria-description" : "aria-label", label); } } if (id !== undefined) { htmlElement.setAttribute("aria-owns", id); } + if (structId !== undefined && this.#structElements.get(structId) === structElement) { + const elementId = this.#getStructElementId(structId); + if (short !== undefined) { + const abbreviation = document.createElement("span"); + abbreviation.setAttribute("id", elementId); + abbreviation.setAttribute("aria-hidden", "true"); + abbreviation.textContent = removeNullCharacters(short); + htmlElement.append(abbreviation); + } else { + htmlElement.setAttribute("id", elementId); + } + } if (lang !== undefined) { htmlElement.setAttribute("lang", removeNullCharacters(lang, true)); } + if (rowSpan !== undefined) { + htmlElement.setAttribute("aria-rowspan", rowSpan); + } + if (colSpan !== undefined) { + htmlElement.setAttribute("aria-colspan", colSpan); + } + if (headers?.length > 0) { + const headerIds = this.#getHeaderIds(headers); + if (headerIds.length > 0) { + htmlElement.setAttribute("aria-describedby", headerIds.join(" ")); + } + } + if (summary !== undefined) { + htmlElement.setAttribute("aria-description", removeNullCharacters(summary)); + } } #addImageInTextLayer(node, element) { const { @@ -11635,7 +11723,24 @@ class StructTreeLayerBuilder { element.setAttribute("role", "heading"); element.setAttribute("aria-level", match[1]); } else if (PDF_ROLE_TO_HTML_ROLE[role]) { - element.setAttribute("role", role === "TH" && parentNodes.at(-1)?.role === "TR" && parentNodes.at(-2)?.role === "TBody" ? "rowheader" : PDF_ROLE_TO_HTML_ROLE[role]); + let htmlRole = PDF_ROLE_TO_HTML_ROLE[role]; + if (role === "TH") { + if (node.scope === "Row") { + htmlRole = "rowheader"; + } else if (node.scope === "Column") { + htmlRole = "columnheader"; + } else if (parentNodes.at(-1)?.role === "TR" && parentNodes.at(-2)?.role === "TBody") { + htmlRole = "rowheader"; + } + } else if (role === "Caption") { + const parentRole = parentNodes.at(-1)?.role; + if (parentRole !== "Table" && parentRole !== "Figure") { + htmlRole = null; + } + } + if (htmlRole) { + element.setAttribute("role", htmlRole); + } } if (role === "Figure" && this.#addImageInTextLayer(node, element)) { return element; @@ -11658,7 +11763,7 @@ class StructTreeLayerBuilder { element ||= document.createElement("span"); this.#setAttributes(node, element); if (node.children) { - if (node.children.length === 1 && "id" in node.children[0]) { + if (node.children.length === 1 && !("role" in node.children[0]) && "id" in node.children[0] && element.getAttribute("role") !== "none") { this.#setAttributes(node.children[0], element); } else if (visitChildren) { parentNodes.push(node); @@ -12727,6 +12832,7 @@ class PDFPageView extends BasePDFPageView { } } this.cssTransform({}); + this.annotationEditorLayer?.update(this.viewport); this.reset({ keepAnnotationLayer: true, keepAnnotationEditorLayer: true, @@ -13148,8 +13254,10 @@ class PDFViewer { #eventAC = null; #minDurationToUpdateCanvas = 0; #mlManager = null; + #panPosition = [NaN, NaN]; #printingAllowed = true; #scrollTimeoutId = null; + #staleLocation = false; #switchAnnotationEditorModeAC = null; #switchAnnotationEditorModeTimeoutId = null; #copyAllInProgress = false; @@ -13166,7 +13274,7 @@ class PDFViewer { #savedPageViews = null; #deletedPageNumbers = null; constructor(options) { - const viewerVersion = "6.3.72"; + const viewerVersion = "6.3.183"; if (version !== viewerVersion) { throw new Error(`The API version "${version}" does not match the Viewer version "${viewerVersion}".`); } @@ -13450,14 +13558,14 @@ class PDFViewer { this.#setPrintingAllowed(true); return params; } - this.#setPrintingAllowed(permissions.includes(PermissionFlag.PRINT_HIGH_QUALITY) || permissions.includes(PermissionFlag.PRINT)); - if (!permissions.includes(PermissionFlag.COPY) && this.#textLayerMode === TextLayerMode.ENABLE) { + this.#setPrintingAllowed(permissions.has(PermissionFlag.PRINT_HIGH_QUALITY) || permissions.has(PermissionFlag.PRINT)); + if (!permissions.has(PermissionFlag.COPY) && this.#textLayerMode === TextLayerMode.ENABLE) { params.textLayerMode = TextLayerMode.ENABLE_PERMISSIONS; } - if (!permissions.includes(PermissionFlag.MODIFY_CONTENTS)) { + if (!permissions.has(PermissionFlag.MODIFY_CONTENTS)) { params.annotationEditorMode = AnnotationEditorType.DISABLE; } - if (!permissions.includes(PermissionFlag.MODIFY_ANNOTATIONS) && !permissions.includes(PermissionFlag.FILL_INTERACTIVE_FORMS) && this.#annotationMode === AnnotationMode.ENABLE_FORMS) { + if (!permissions.has(PermissionFlag.MODIFY_ANNOTATIONS) && !permissions.has(PermissionFlag.FILL_INTERACTIVE_FORMS) && this.#annotationMode === AnnotationMode.ENABLE_FORMS) { params.annotationMode = AnnotationMode.ENABLE; } return params; @@ -13999,15 +14107,46 @@ class PDFViewer { #isSameScale(newScale) { return newScale === this._currentScale || Math.abs(newScale - this._currentScale) < 1e-15; } + panBy(dx, dy) { + const { + container + } = this; + const position = this.#panPosition; + const { + scrollLeft, + scrollTop + } = container; + const left = (Math.abs(scrollLeft - position[0]) < 1 ? position[0] : scrollLeft) - dx; + const top = (Math.abs(scrollTop - position[1]) < 1 ? position[1] : scrollTop) - dy; + position[0] = left; + position[1] = top; + container.scrollLeft = left; + container.scrollTop = top; + this.#staleLocation = true; + } + #refreshLocation() { + if (!this.#staleLocation) { + return; + } + const { + first + } = this._getVisiblePages(); + if (first) { + this._updateLocation(first); + } + } #setScaleUpdatePages(newScale, newValue, { noScroll = false, preset = false, drawingDelay = -1, - origin = null + origin = null, + pan = null }) { - this.clearSelection(); this._currentScaleValue = newValue.toString(); if (this.#isSameScale(newScale)) { + if (pan && !noScroll) { + this.panBy(pan[0], pan[1]); + } if (preset) { this.eventBus.dispatch("scalechanging", { source: this, @@ -14017,6 +14156,7 @@ class PDFViewer { } return; } + this.clearSelection(); this.viewer.style.setProperty("--scale-factor", newScale * PixelsPerInch.PDF_TO_CSS_UNITS); const postponeDrawing = drawingDelay >= 0 && drawingDelay < 1000; this.refresh(true, { @@ -14032,6 +14172,7 @@ class PDFViewer { const previousScale = this._currentScale; this._currentScale = newScale; if (!noScroll) { + this.#refreshLocation(); let page = this._currentPageNumber, dest; if (this._location && !(this.isInPresentationMode || this.isChangingPresentationMode)) { @@ -14045,11 +14186,16 @@ class PDFViewer { destArray: dest, allowNegativeOffset: true }); + let dx = pan?.[0] ?? 0, + dy = pan?.[1] ?? 0; if (Array.isArray(origin)) { const scaleDiff = newScale / previousScale - 1; const [top, left] = this.containerTopLeft; - this.container.scrollLeft += (origin[0] - left) * scaleDiff; - this.container.scrollTop += (origin[1] - top) * scaleDiff; + dx -= (origin[0] - left) * scaleDiff; + dy -= (origin[1] - top) * scaleDiff; + } + if (dx || dy) { + this.panBy(dx, dy); } } this.eventBus.dispatch("scalechanging", { @@ -14235,6 +14381,7 @@ class PDFViewer { }); } _updateLocation(firstPage) { + this.#staleLocation = false; const currentScale = this._currentScale; const currentScaleValue = this._currentScaleValue; const normalizedScaleValue = parseFloat(currentScaleValue) === currentScale ? Math.round(currentScale * 10000) / 100 : currentScaleValue; @@ -14699,7 +14846,8 @@ class PDFViewer { drawingDelay, scaleFactor = null, steps = null, - origin + origin, + pan = null }) { if (steps === null && scaleFactor === null) { throw new Error("Invalid updateScale options: either `steps` or `scaleFactor` must be provided."); @@ -14722,7 +14870,8 @@ class PDFViewer { this.#setScale(newScale, { noScroll: false, drawingDelay, - origin + origin, + pan }); } increaseScale(options = {}) { @@ -16794,7 +16943,7 @@ class Toolbar { eventBus.on("mainhighlightcolorpickerupdatecolor", ({ value }) => { - this.#colorPicker?.updateColor(value); + this.#colorPicker?.update(value); }, internalOpt); } } @@ -17733,7 +17882,7 @@ const PDFViewerApplication = { this.toolbar = new Toolbar(appConfig.toolbar, eventBus, AppOptions.get("toolbarDensity")); } if (appConfig.secondaryToolbar) { - if (AppOptions.get("enableAltText")) { + if (AppOptions.get("enableAltText") && this.imageAltTextSettings) { appConfig.secondaryToolbar.imageAltTextSettingsButton?.classList.remove("hidden"); appConfig.secondaryToolbar.imageAltTextSettingsSeparator?.classList.remove("hidden"); } @@ -17838,7 +17987,7 @@ const PDFViewerApplication = { get initializedPromise() { return this._initializedCapability.promise; }, - updateZoom(steps, scaleFactor, origin) { + updateZoom(steps, scaleFactor, origin, pan = null) { if (this.pdfViewer.isInPresentationMode) { return; } @@ -17846,7 +17995,8 @@ const PDFViewerApplication = { drawingDelay: AppOptions.get("defaultZoomDelay"), steps, scaleFactor, - origin + origin, + pan }); }, zoomIn() { @@ -17861,16 +18011,26 @@ const PDFViewerApplication = { } this.pdfViewer.currentScaleValue = (/* inlined export .DEFAULT_SCALE_VALUE */"auto"); }, - touchPinchCallback(origin, prevDistance, distance) { + touchPinchCallback(origin, prevDistance, distance, panX, panY) { + const pan = [panX, panY]; if (this.supportsPinchToZoom) { const newScaleFactor = this._accumulateFactor(this.pdfViewer.currentScale, distance / prevDistance, "_touchUnusedFactor"); - this.updateZoom(null, newScaleFactor, origin); + this.updateZoom(null, newScaleFactor, origin, pan); } else { const PIXELS_PER_LINE_SCALE = 30; const ticks = this._accumulateTicks((distance - prevDistance) / PIXELS_PER_LINE_SCALE, "_touchUnusedTicks"); - this.updateZoom(ticks, null, origin); + this.updateZoom(ticks, null, origin, pan); } }, + touchPanCallback(dx, dy) { + const { + pdfViewer + } = this; + if (!this.pdfDocument || pdfViewer.isInPresentationMode) { + return; + } + pdfViewer.panBy(dx, dy); + }, touchPinchEndCallback() { this._touchUnusedTicks = 0; this._touchUnusedFactor = 1; @@ -18716,6 +18876,7 @@ const PDFViewerApplication = { isPinchingStopped: () => this.overlayManager?.active, onPinching: this.touchPinchCallback.bind(this), onPinchEnd: this.touchPinchEndCallback.bind(this), + onPanning: this.touchPanCallback.bind(this), signal }); function addWindowResolutionChange(evt = null) { diff --git a/toolkit/components/pdfjs/content/web/wasm/LICENSE_PDFJS_QCMS b/toolkit/components/pdfjs/content/web/wasm/LICENSE_PDFJS_QCMS index 7e1aeb34f917..9cf106272ac3 100644 --- a/toolkit/components/pdfjs/content/web/wasm/LICENSE_PDFJS_QCMS +++ b/toolkit/components/pdfjs/content/web/wasm/LICENSE_PDFJS_QCMS @@ -1,22 +1,19 @@ -Copyright (c) 2025, Mozilla Foundation +MIT License -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/toolkit/components/pdfjs/moz.yaml b/toolkit/components/pdfjs/moz.yaml index d605f0c2bf63..32387bc65d95 100644 --- a/toolkit/components/pdfjs/moz.yaml +++ b/toolkit/components/pdfjs/moz.yaml @@ -20,8 +20,8 @@ origin: # Human-readable identifier for this version/release # Generally "version NNN", "tag SSS", "bookmark SSS" - release: 71a3c6a896b1d297f1f48384a65df6645ad11c4b (2026-08-06T12:21:01Z). - revision: 71a3c6a896b1d297f1f48384a65df6645ad11c4b + release: 48bb93b89de51fe40521c2e52f8be51440283b6f (2026-08-15T10:58:32Z). + revision: 48bb93b89de51fe40521c2e52f8be51440283b6f # The package's license, where possible using the mnemonic from # https://spdx.org/licenses/ diff --git a/toolkit/components/protobuf/src/google/protobuf/cpp_file_options.proto b/toolkit/components/protobuf/src/google/protobuf/cpp_file_options.proto deleted file mode 100644 index eac608710e47..000000000000 --- a/toolkit/components/protobuf/src/google/protobuf/cpp_file_options.proto +++ /dev/null @@ -1,24 +0,0 @@ -// Protocol Buffers - Google's data interchange format -// Copyright 2026 Google LLC. All rights reserved. -// -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file or at -// https://developers.google.com/open-source/licenses/bsd - -edition = "2024"; - -package pb.file; - -import "google/protobuf/descriptor.proto"; - -extend google.protobuf.FileOptions { - CppFileOptions cpp = 990 - [feature_support = { edition_introduced: EDITION_UNSTABLE }]; -} - -local message CppFileOptions { - // Use this option to change the namespace of cpp generated classes. When this - // option is not set, the package name will be used for determining the cpp - // namespace. - string namespace = 1; -} diff --git a/toolkit/components/protobuf/src/google/protobuf/cpp_options.proto b/toolkit/components/protobuf/src/google/protobuf/cpp_options.proto deleted file mode 100644 index 0017e047e5c0..000000000000 --- a/toolkit/components/protobuf/src/google/protobuf/cpp_options.proto +++ /dev/null @@ -1,15 +0,0 @@ -// Protocol Buffers - Google's data interchange format -// Copyright 2026 Google LLC. All rights reserved. -// -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file or at -// https://developers.google.com/open-source/licenses/bsd - -edition = "2024"; - -package pb; - -// This file is the user facing cpp language proto that bundles all descriptor -// type cpp language options. - -import public "google/protobuf/cpp_file_options.proto"; diff --git a/toolkit/components/protobuf/src/google/protobuf/json_enumvalue_options.proto b/toolkit/components/protobuf/src/google/protobuf/json_enumvalue_options.proto deleted file mode 100644 index 56998877c48c..000000000000 --- a/toolkit/components/protobuf/src/google/protobuf/json_enumvalue_options.proto +++ /dev/null @@ -1,29 +0,0 @@ -// Protocol Buffers - Google's data interchange format -// Copyright 2026 Google LLC. All rights reserved. -// -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file or at -// https://developers.google.com/open-source/licenses/bsd - -edition = "2024"; - -package pb.enumvalue; - -import "google/protobuf/descriptor.proto"; - -local message JsonEnumValueOptions { - // Allows user to specify a custom string to use when serializing an enum - // value to JSON. - // - // Sample usage: - // enum Foo { - // ... - // FOO_BAR = 42 [(pb.enumvalue.json).string = "my_custom_name"]; - // } - string string = 1; -} - -extend google.protobuf.EnumValueOptions { - JsonEnumValueOptions json = 998 - [feature_support = { edition_introduced: EDITION_UNSTABLE }]; -} diff --git a/toolkit/components/protobuf/src/google/protobuf/json_options.proto b/toolkit/components/protobuf/src/google/protobuf/json_options.proto deleted file mode 100644 index 60b03e2d4c77..000000000000 --- a/toolkit/components/protobuf/src/google/protobuf/json_options.proto +++ /dev/null @@ -1,14 +0,0 @@ -// Protocol Buffers - Google's data interchange format -// Copyright 2026 Google LLC. All rights reserved. -// -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file or at -// https://developers.google.com/open-source/licenses/bsd - -// A convenience proto that houses all JSON-related options. - -edition = "2024"; - -package pb; - -import public "google/protobuf/json_enumvalue_options.proto"; diff --git a/toolkit/components/resistfingerprinting/nsRFPService.cpp b/toolkit/components/resistfingerprinting/nsRFPService.cpp index 138b0b08feb5..93a9efd0685f 100644 --- a/toolkit/components/resistfingerprinting/nsRFPService.cpp +++ b/toolkit/components/resistfingerprinting/nsRFPService.cpp @@ -3182,6 +3182,8 @@ void nsRFPService::GetExemptedDomainsLowercase(nsCString& aExemptedDomains) { #define EXEMPTED_DOMAINS_PREF_NAME \ "privacy.resistFingerprinting.exemptedDomains" + // Callers compare this against a lower-cased host, and + // nsContentUtils::IsURIInList() asserts that the list is lower-case. static bool sInited = false; if (!sInited) { sInited = true; @@ -3189,10 +3191,12 @@ void nsRFPService::GetExemptedDomainsLowercase(nsCString& aExemptedDomains) { ClearOnShutdown(sExemptedDomainsLowercase); Preferences::GetCString(EXEMPTED_DOMAINS_PREF_NAME, *sExemptedDomainsLowercase); + ToLowerCase(*sExemptedDomainsLowercase); Preferences::RegisterCallback( [](const char* aPref, void* aData) { Preferences::GetCString(EXEMPTED_DOMAINS_PREF_NAME, *sExemptedDomainsLowercase); + ToLowerCase(*sExemptedDomainsLowercase); }, EXEMPTED_DOMAINS_PREF_NAME); } diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js b/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js index e963cf71a9bf..87f15259a353 100644 --- a/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js +++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js @@ -1726,10 +1726,7 @@ add_task(async function test_abortedSession() { ); // Make sure the aborted sessions directory does not exist to test its creation. - await IOUtils.remove(DATAREPORTING_PATH, { - ignoreAbsent: true, - recursive: true, - }); + await IOUtils.remove(ABORTED_FILE, { ignoreAbsent: true }); let schedulerTickCallback = null; let now = new Date(2040, 1, 1, 0, 0, 0); @@ -1847,10 +1844,7 @@ add_task(async function test_abortedDailyCoalescing() { ); // Make sure the aborted sessions directory does not exist to test its creation. - await IOUtils.remove(DATAREPORTING_PATH, { - ignoreAbsent: true, - recursive: true, - }); + await IOUtils.remove(ABORTED_FILE, { ignoreAbsent: true }); let schedulerTickCallback = null; PingServer.clearRequests(); @@ -1923,10 +1917,7 @@ add_task(async function test_schedulerComputerSleep() { PingServer.clearRequests(); // Remove any aborted-session ping from the previous tests. - await IOUtils.remove(DATAREPORTING_PATH, { - ignoreAbsent: true, - recursive: true, - }); + await IOUtils.remove(ABORTED_FILE, { ignoreAbsent: true }); // Set a fake current date and start Telemetry. let nowDate = fakeNow(2009, 10, 18, 0, 0, 0); @@ -2065,10 +2056,7 @@ add_task(async function test_schedulerNothingDue() { ); // Remove any aborted-session ping from the previous tests. - await IOUtils.remove(DATAREPORTING_PATH, { - ignoreAbsent: true, - recursive: true, - }); + await IOUtils.remove(ABORTED_FILE, { ignoreAbsent: true }); await TelemetryStorage.testClearPendingPings(); await TelemetryController.testReset(); diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetrySession_abortedSessionQueued.js b/toolkit/components/telemetry/tests/unit/test_TelemetrySession_abortedSessionQueued.js index 80807469fc95..f1594a2e84e5 100644 --- a/toolkit/components/telemetry/tests/unit/test_TelemetrySession_abortedSessionQueued.js +++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySession_abortedSessionQueued.js @@ -55,10 +55,7 @@ add_task(async function test_abortedSessionQueued() { ); // Make sure the aborted sessions directory does not exist to test its creation. - await IOUtils.remove(DATAREPORTING_PATH, { - ignoreAbsent: true, - recursive: true, - }); + await IOUtils.remove(ABORTED_FILE, { ignoreAbsent: true }); let schedulerTickCallback = null; let now = new Date(2040, 1, 1, 0, 0, 0); @@ -128,10 +125,7 @@ add_task(async function test_abortedSession_canary_clientid() { ); // Make sure the aborted sessions directory does not exist to test its creation. - await IOUtils.remove(DATAREPORTING_PATH, { - ignoreAbsent: true, - recursive: true, - }); + await IOUtils.remove(ABORTED_FILE, { ignoreAbsent: true }); let schedulerTickCallback = null; let now = new Date(2040, 1, 1, 0, 0, 0); diff --git a/toolkit/components/thumbnails/test/test_thumbnails_interfaces.js b/toolkit/components/thumbnails/test/test_thumbnails_interfaces.js index eb70e8011b1e..65d56b1fe972 100644 --- a/toolkit/components/thumbnails/test/test_thumbnails_interfaces.js +++ b/toolkit/components/thumbnails/test/test_thumbnails_interfaces.js @@ -37,7 +37,7 @@ function run_test() { ); let badQuery = Services.io.newURI( - "moz-page-thumb://thumbnail/http%3A%2F%2Fwww.mozilla.org%2F" + "moz-page-thumb://thumbnails/http%3A%2F%2Fwww.mozilla.org%2F" ); Assert.throws( () => handler.newChannel(badQuery, dummyLoadInfo), @@ -45,7 +45,7 @@ function run_test() { "moz-page-thumb object with malformed query parameters must not resolve to a file path" ); - let noURL = Services.io.newURI("moz-page-thumb://thumbnail/?badStuff"); + let noURL = Services.io.newURI("moz-page-thumb://thumbnails/?badStuff"); Assert.throws( () => handler.newChannel(noURL, dummyLoadInfo), /NS_ERROR_NOT_AVAILABLE/i, diff --git a/toolkit/components/translations/content/about-translations.css b/toolkit/components/translations/content/about-translations.css index e4fa02e1a8eb..ad87a24ba447 100644 --- a/toolkit/components/translations/content/about-translations.css +++ b/toolkit/components/translations/content/about-translations.css @@ -30,7 +30,7 @@ --AT-square-button-large: calc(var(--size-item-large) + var(--space-small)); --AT-square-button-small: var(--size-item-large); - --AT-source-textarea-button-inline-padding: calc(var(--AT-square-button-small) + var(--space-small)); + --AT-source-textarea-button-inline-padding: calc(var(--AT-square-button-small) + var(--space-small) + var(--space-xsmall)); --AT-target-textarea-button-block-padding: calc(var(--AT-square-button-large) + var(--space-small)); } @@ -227,8 +227,8 @@ body { position: absolute; z-index: 1; position-anchor: --AT-source-textarea-anchor; - inset-block-start: calc(anchor(top) + var(--space-xsmall)); - inset-inline-end: calc(anchor(end) + var(--space-xsmall)); + inset-block-start: calc(anchor(top) + var(--space-small)); + inset-inline-end: calc(anchor(end) + var(--space-small)); &:focus-visible { outline: none; diff --git a/toolkit/library/moz.build b/toolkit/library/moz.build index 377b348dfd1b..33d6dc4b827a 100644 --- a/toolkit/library/moz.build +++ b/toolkit/library/moz.build @@ -25,6 +25,7 @@ def Libxul(name, output_category=None): DELAYLOAD_DLLS += [ "avrt.dll", + "bcrypt.dll", "comdlg32.dll", "credui.dll", "crypt32.dll", diff --git a/toolkit/xre/dllservices/mozglue/WindowsBCryptInitialization.cpp b/toolkit/xre/dllservices/mozglue/WindowsBCryptInitialization.cpp deleted file mode 100644 index 02b64913d012..000000000000 --- a/toolkit/xre/dllservices/mozglue/WindowsBCryptInitialization.cpp +++ /dev/null @@ -1,21 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#include "mozilla/WindowsBCryptInitialization.h" - -#include "nsWindowsDllInterceptor.h" - -#include -#pragma comment(lib, "bcrypt.lib") - -namespace mozilla { - -bool WindowsBCryptInitialization() { - UCHAR buffer[32]; - NTSTATUS status = ::BCryptGenRandom(nullptr, buffer, sizeof(buffer), - BCRYPT_USE_SYSTEM_PREFERRED_RNG); - return NT_SUCCESS(status); -} - -} // namespace mozilla diff --git a/toolkit/xre/dllservices/mozglue/WindowsBCryptInitialization.h b/toolkit/xre/dllservices/mozglue/WindowsBCryptInitialization.h deleted file mode 100644 index 2db2f3c82cca..000000000000 --- a/toolkit/xre/dllservices/mozglue/WindowsBCryptInitialization.h +++ /dev/null @@ -1,22 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#ifndef mozilla_WindowsBCryptInitialization_h -#define mozilla_WindowsBCryptInitialization_h - -#include "mozilla/Types.h" - -namespace mozilla { - -// This functions ensures that calling BCryptGenRandom will work later. It -// triggers a first call to BCryptGenRandom() to pre-load bcryptPrimitives.dll. -// In sandboxed processes, this must happen while the current thread still has -// an unrestricted impersonation token. We need to perform that operation to -// warmup the BCryptGenRandom() calls is used by others, especially Rust. See -// bug 1746524, bug 1751094, bug 1751177, bug 1788004. -MFBT_API bool WindowsBCryptInitialization(); - -} // namespace mozilla - -#endif // mozilla_WindowsBCryptInitialization_h diff --git a/toolkit/xre/dllservices/mozglue/moz.build b/toolkit/xre/dllservices/mozglue/moz.build index 9ef6f7a5b569..f7cd434a1c02 100644 --- a/toolkit/xre/dllservices/mozglue/moz.build +++ b/toolkit/xre/dllservices/mozglue/moz.build @@ -14,7 +14,6 @@ if CONFIG["MOZ_WIDGET_TOOLKIT"]: "Authenticode.cpp", "LoaderObserver.cpp", "ModuleLoadFrame.cpp", - "WindowsBCryptInitialization.cpp", "WindowsFallbackLoaderAPI.cpp", "WindowsMsctfInitialization.cpp", "WindowsOleAut32Initialization.cpp", @@ -42,7 +41,6 @@ EXPORTS.mozilla += [ "CacheNtDllThunk.h", "LoaderAPIInterfaces.h", "ModuleLoadInfo.h", - "WindowsBCryptInitialization.h", "WindowsDllBlocklist.h", "WindowsDllBlocklistCommon.h", "WindowsDllBlocklistInfo.h", diff --git a/toolkit/xre/dllservices/tests/TestDllInterceptor.cpp b/toolkit/xre/dllservices/tests/TestDllInterceptor.cpp index c23c48fd7ba2..51613b741729 100644 --- a/toolkit/xre/dllservices/tests/TestDllInterceptor.cpp +++ b/toolkit/xre/dllservices/tests/TestDllInterceptor.cpp @@ -12,9 +12,6 @@ #include #include -#include -#pragma comment(lib, "bcrypt.lib") - #include #pragma comment(lib, "oleaut32.lib") @@ -1559,8 +1556,6 @@ extern "C" int wmain(int argc, wchar_t* argv[]) { #if !defined(_M_ARM64) TEST_HOOK("user32.dll", SetCursorPos, NotEquals, FALSE) && #endif - TEST_HOOK("bcrypt.dll", BCryptGenRandom, Equals, - static_cast(STATUS_INVALID_HANDLE)) && TEST_HOOK("advapi32.dll", RtlGenRandom, Equals, TRUE) && TEST_HOOK_PARAMS("oleaut32.dll", VariantClear, Equals, S_OK, &var) && #if !defined(_M_ARM64) diff --git a/toolkit/xre/nsAppRunner.cpp b/toolkit/xre/nsAppRunner.cpp index cd9485c34497..83a21bb44664 100644 --- a/toolkit/xre/nsAppRunner.cpp +++ b/toolkit/xre/nsAppRunner.cpp @@ -117,7 +117,6 @@ # include "detect_win32k_conflicts.h" # include "mozilla/PreXULSkeletonUI.h" # include "mozilla/DllPrefetchExperimentRegistryInfo.h" -# include "mozilla/WindowsBCryptInitialization.h" # include "mozilla/WindowsDllBlocklist.h" # include "mozilla/WindowsMsctfInitialization.h" # include "mozilla/WindowsOleAut32Initialization.h" @@ -6509,11 +6508,6 @@ int XREMain::XRE_main(int argc, char* argv[], const BootstrapConfig& aConfig) { mAppData->sandboxBrokerServices = aConfig.sandboxBrokerServices; # endif // defined(MOZ_SANDBOX) - { - DebugOnly result = WindowsBCryptInitialization(); - MOZ_ASSERT(result); - } - # if defined(_M_IX86) || defined(_M_X64) { DebugOnly result = WindowsMsctfInitialization(); diff --git a/toolkit/xre/nsEmbedFunctions.cpp b/toolkit/xre/nsEmbedFunctions.cpp index 7689ccb829dd..22939d60120c 100644 --- a/toolkit/xre/nsEmbedFunctions.cpp +++ b/toolkit/xre/nsEmbedFunctions.cpp @@ -24,7 +24,6 @@ # endif # include "mozilla/ScopeExit.h" # include "mozilla/WinDllServices.h" -# include "mozilla/WindowsBCryptInitialization.h" # include "WinUtils.h" #endif @@ -487,13 +486,6 @@ nsresult XRE_InitChildProcess(int aArgc, char* aArgv[], break; } -#if defined(XP_WIN) - { - DebugOnly result = mozilla::WindowsBCryptInitialization(); - MOZ_ASSERT(result); - } -#endif // defined(XP_WIN) - { // This is a lexical scope for the MessageLoop below. We want it // to go out of scope before NS_LogTerm() so that we don't get diff --git a/tools/coverity/model_file.cpp b/tools/coverity/model_file.cpp index 8dd48f8675f5..c84ece75b34e 100644 --- a/tools/coverity/model_file.cpp +++ b/tools/coverity/model_file.cpp @@ -276,6 +276,8 @@ void* moz_xmemalign(size_t boundary, size_t size) { /// Model the copy operations of the string classes as trivial so that /// COPY_INSTEAD_OF_MOVE stops reporting copies where std::move() brings no /// benefit (CID 1697195 and friends). +/// The assignments across string classes (`nsCString = nsAutoCString`, as in +/// CID 1700142) go through the substring overloads, so model those too. template class nsTSubstring { public: @@ -287,14 +289,18 @@ template class nsTString : public nsTSubstring { public: nsTString(const nsTString& aStr) {} + nsTString(const nsTSubstring& aStr) {} nsTString& operator=(const nsTString& aStr) { return *this; } + nsTString& operator=(const nsTSubstring& aStr) { return *this; } }; template class nsTAutoStringN : public nsTString { public: nsTAutoStringN(const nsTAutoStringN& aStr) {} + nsTAutoStringN(const nsTSubstring& aStr) {} nsTAutoStringN& operator=(const nsTAutoStringN& aStr) { return *this; } + nsTAutoStringN& operator=(const nsTSubstring& aStr) { return *this; } }; /// usrsctp defines all its usrsctp_sysctl_set_* setters with this macro, whose diff --git a/tools/profiler/gecko/ProfilerParent.cpp b/tools/profiler/gecko/ProfilerParent.cpp index 163b07a11fb9..944c577131ce 100644 --- a/tools/profiler/gecko/ProfilerParent.cpp +++ b/tools/profiler/gecko/ProfilerParent.cpp @@ -21,6 +21,7 @@ #include "mozilla/RefPtr.h" #include "nsTArray.h" #include "nsThreadUtils.h" +#include "nsXULAppAPI.h" #include @@ -530,6 +531,10 @@ void ProfileBufferGlobalController::HandleChunkManagerNonFinalUpdate( ProfilerParentTracker* ProfilerParentTracker::GetInstance() { MOZ_RELEASE_ASSERT(NS_IsMainThread()); + if (!XRE_IsParentProcess()) { + return nullptr; + } + // The main instance pointer, it will be initialized at most once, before // XPCOMShutdownThreads. static StaticAutoPtr instance; diff --git a/widget/TextEvents.h b/widget/TextEvents.h index 7d73b5b43d42..595020b2812f 100644 --- a/widget/TextEvents.h +++ b/widget/TextEvents.h @@ -1027,7 +1027,8 @@ class WidgetCompositionEvent final : public WidgetGUIEvent { } bool IsFollowedByCompositionEnd() const { - return IsFollowedByCompositionEnd(mOriginalMessage); + return IsFollowedByCompositionEnd(mOriginalMessage ? mOriginalMessage + : mMessage); } static bool IsFollowedByCompositionEnd(EventMessage aEventMessage) { diff --git a/widget/gtk/nsWindow.cpp b/widget/gtk/nsWindow.cpp index 53cc0d90ca5c..24597f990b7e 100644 --- a/widget/gtk/nsWindow.cpp +++ b/widget/gtk/nsWindow.cpp @@ -3640,23 +3640,15 @@ void nsWindow::OnWindowStateEvent(GtkWidget* aWidget, ForceTitlebarRedraw(); } - // We don't care about anything but changes in the maximized/icon/fullscreen - // states but we need a workaround for bug in Wayland: + // We don't care about anything but changes in the + // maximized/icon/fullscreen/tiled/resizable states. + constexpr auto kInterestingStates = + GDK_WINDOW_STATE_ICONIFIED | GDK_WINDOW_STATE_MAXIMIZED | + GDK_WINDOW_STATE_FULLSCREEN | kTiledStates | kResizableStates; + + // states. Note that Wayland never gets iconified, see: // https://gitlab.gnome.org/GNOME/gtk/issues/67 - // Under wayland the gtk_window_iconify implementation does NOT synthetize - // window_state_event where the GDK_WINDOW_STATE_ICONIFIED is set. - // During restore we won't get aEvent->changed_mask with - // the GDK_WINDOW_STATE_ICONIFIED so to detect that change we use the stored - // mSizeMode and obtaining a focus. - bool waylandWasIconified = - (GdkIsWaylandDisplay() && - aEvent->changed_mask & GDK_WINDOW_STATE_FOCUSED && - aEvent->new_window_state & GDK_WINDOW_STATE_FOCUSED && - mSizeMode == nsSizeMode_Minimized); - if (!waylandWasIconified && - (aEvent->changed_mask & - (GDK_WINDOW_STATE_ICONIFIED | GDK_WINDOW_STATE_MAXIMIZED | kTiledStates | - kResizableStates | GDK_WINDOW_STATE_FULLSCREEN)) == 0) { + if (!(aEvent->changed_mask & kInterestingStates)) { LOG("\tearly return because no interesting bits changed\n"); return; }