Merge autoland to mozilla-central

This commit is contained in:
Lando
2026-08-17 21:06:12 +00:00
committed by csabou@mozilla.com
731 changed files with 18141 additions and 7390 deletions
+205
View File
@@ -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 <ID>` 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
+205
View File
@@ -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 <ID>` 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
+5 -1
View File
@@ -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<Accessible*> accHeaders;
cell->ColHeaderCells(&accHeaders);
cell->RowHeaderCells(&accHeaders);
nsTArray<Accessible*> accRowHeaders;
cell->RowHeaderCells(&accRowHeaders);
accHeaders.AppendElements(std::move(accRowHeaders));
std::vector<int> pdfHeaders;
pdfHeaders.reserve(accHeaders.Length());
for (Accessible* accHeader : accHeaders) {
@@ -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",
`
<table>
<tr><th>c1</th><th id="ch">c2</th></tr>
<tr><th>r1</th><td headers="ch">d</td></tr>
</table>
`,
[
{
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"],
},
],
},
+48 -27
View File
@@ -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],
+9
View File
@@ -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);
+2
View File
@@ -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);
@@ -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
@@ -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();
@@ -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();
@@ -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();
@@ -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,
@@ -5,7 +5,7 @@
<img id="page-icon" src="page-icon:https://mochi.test/">
<img id="moz-remote-image" src="moz-remote-image://?url=https://mochi.test/">
<img id="moz-newtab-wallpaper" src="moz-newtab-wallpaper://wallpaper.jpg">
<img id="moz-page-thumb" src="moz-page-thumb://thumbnail?url=http%3A%2F%2Ffoo.com%2F">
<img id="moz-page-thumb" src="moz-page-thumb://thumbnails/?url=http%3A%2F%2Ffoo.com%2F">
<img id="cached-favicon" src="cached-favicon:http://mozilla.org/made-up-favicon">
</body>
</html>
@@ -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() {
@@ -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
@@ -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"
);
@@ -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"
);
@@ -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 {
@@ -3464,6 +3464,10 @@ export var Policies = {
features.http = !policies.HttpsOnly;
}
if ("DisableServiceWorkers" in policies) {
features.serviceworkers = !policies.DisableServiceWorkers;
}
return features;
},
@@ -3657,6 +3657,9 @@
},
"HttpsOnly": {
"type": "boolean"
},
"DisableServiceWorkers": {
"type": "boolean"
}
}
}
@@ -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"]
@@ -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();
});
@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<div id="api-status">pending</div>
<div id="intercept-status">pending</div>
<script>
async function run() {
let apiEl = document.getElementById("api-status");
let interceptEl = document.getElementById("intercept-status");
try {
if ("serviceWorker" in navigator) {
apiEl.textContent = "sw-available";
await navigator.serviceWorker.register("sw_fetch_intercept.js");
await navigator.serviceWorker.ready;
if (!navigator.serviceWorker.controller) {
await new Promise(resolve => {
navigator.serviceWorker.addEventListener("controllerchange", resolve, { once: true });
});
}
} else {
apiEl.textContent = "sw-not-supported";
}
let response = await fetch("?sw-intercepted-resource");
let text = await response.text();
interceptEl.textContent = text === "intercepted" ? "sw-intercepted" : "sw-not-intercepted";
} catch (e) {
interceptEl.textContent = "sw-error";
}
}
run();
</script>
</body>
</html>
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<iframe src="https://example.com/browser/browser/components/enterprisepolicies/tests/browser/sitepolicies_sw_fetch.html"></iframe>
<iframe src="https://example.org/browser/browser/components/enterprisepolicies/tests/browser/sitepolicies_sw_fetch.html"></iframe>
</body>
</html>
@@ -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));
}
});
@@ -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);
});
@@ -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;
}
}
@@ -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",
@@ -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"] },
@@ -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({
@@ -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}
></moz-checkbox>
>
${when(
tool.view === "viewOpenTabsSidebar" && !tool.disabled,
() => html`
<moz-checkbox
slot="nested"
type="checkbox"
id="hover-preview"
name="hover-preview"
data-l10n-id="sidebar-show-preview-on-hover"
@change=${this.#toggleHoverPreview}
?checked=${this.hoverPreviewEnabled}
></moz-checkbox>
`
)}
</moz-checkbox>
`;
}
@@ -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);
@@ -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();
});
@@ -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";
@@ -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");
}
},
};
@@ -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"
);
},
};
@@ -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);
@@ -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"
);
},
};
+2
View File
@@ -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)
@@ -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"]
@@ -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();
@@ -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<object>}
* `{ 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,
@@ -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],
+47 -21
View File
@@ -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,
};
},
@@ -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.
@@ -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<typeof UrlbarShared.RESULT_TYPE>} [type]
* Details of the result type, if any.
* @property {string} [searchTerm]
* Search term of the result source, if any.
* @property {Values<typeof UrlbarShared.RESULT_SOURCE>} [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);
}
@@ -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 };
}
}
@@ -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") &&
@@ -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<typeof UrlbarShared.RESULT_TYPE>} [type]
* Details of the result type, if any.
* @property {string} [searchTerm]
* Search term of the result source, if any.
* @property {Values<typeof UrlbarShared.RESULT_SOURCE>} [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);
}
@@ -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];
};
@@ -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"
) {
@@ -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 {};
});
@@ -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 {};
})
);
@@ -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(
@@ -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");
}
});
@@ -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();
@@ -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: {
@@ -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
+7 -7
View File
@@ -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
@@ -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
@@ -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
@@ -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) {
@@ -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"))
+18 -61
View File
@@ -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,
};
@@ -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");
}
+1 -8
View File
@@ -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;
}
-37
View File
@@ -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;
@@ -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"]
@@ -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);
@@ -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 = `
<style>
body {
${PROPERTY}: auto;
}
</style>
<body></body>
`;
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}`);
});
@@ -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`);
}
}
}
@@ -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"]
@@ -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();
@@ -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)
@@ -11,6 +11,7 @@ const TEST_URI = `
body {
user-select: none;
stroke-color: red;
-moz-orient: horizontal;
}
</style>
<body>
@@ -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);
});
@@ -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();
@@ -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();
@@ -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");
@@ -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 =
+4 -24
View File
@@ -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);
}
/**
@@ -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
);
@@ -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;
}
@@ -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,
@@ -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"]
@@ -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);
}
@@ -130,7 +130,6 @@ skip-if = [
["browser_canvasframe_helper_06.js"]
["browser_compatibility_cssIssues.js"]
tags = "devtools-compat-data"
["browser_connectToFrame.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();
+12
View File
@@ -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 .
@@ -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,
};
@@ -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" }
}
@@ -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",
]
+1
View File
@@ -7,6 +7,7 @@ DIRS += [
]
DevToolsModules(
"compatibility-dataset.js",
"compatibility-user-settings.js",
"constants.js",
"helpers.js",
+10
View File
@@ -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<IDX_ServiceWorkersDisabledByPolicy>, bool,
ContentParent*) {
return IsTop();
}
bool CanSet(FieldIndex<IDX_LanguageOverride>, const nsCString&,
ContentParent*) {
return IsTop();
+2 -3
View File
@@ -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<Document> doc = parentInner->GetExtantDoc();
if (doc && StoragePartitioningEnabled(storage, doc->CookieJarSettings())) {
+13 -2
View File
@@ -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.
+31 -6
View File
@@ -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<nsIURI> 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<nsIURI> uri;
if (!baseDomain.IsEmpty()) {
rv = NS_NewURI(getter_AddRefs(uri), u"https://"_ns + baseDomain);
}
if (!NS_FAILED(rv)) {
+15 -19
View File
@@ -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<nsIPrincipal> 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();
}
+8 -4
View File
@@ -1006,12 +1006,16 @@ class nsGlobalWindowInner final : public mozilla::dom::EventTarget,
JS::Handle<JS::Value> 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,
+12 -15
View File
@@ -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,
+6 -7
View File
@@ -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();
+4 -5
View File
@@ -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<nsDOMCSSDeclaration> 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;

Some files were not shown because too many files have changed in this diff Show More