Merge autoland to mozilla-central
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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"],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+3
-1
@@ -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"
|
||||
);
|
||||
|
||||
|
||||
+1
-1
@@ -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"]
|
||||
|
||||
+470
@@ -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
-39
@@ -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"
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
});
|
||||
|
||||
+36
@@ -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
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
|
||||
+10
-6
@@ -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);
|
||||
|
||||
|
||||
+57
@@ -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"]
|
||||
|
||||
|
||||
+1
@@ -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)
|
||||
|
||||
+10
-6
@@ -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();
|
||||
|
||||
|
||||
+1
@@ -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();
|
||||
|
||||
+2
-2
@@ -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");
|
||||
|
||||
+2
-2
@@ -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 =
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
+143
@@ -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();
|
||||
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -7,6 +7,7 @@ DIRS += [
|
||||
]
|
||||
|
||||
DevToolsModules(
|
||||
"compatibility-dataset.js",
|
||||
"compatibility-user-settings.js",
|
||||
"constants.js",
|
||||
"helpers.js",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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())) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user