From eaefe60938fa09ee3c21c5b705e3d1d3ec91ec15 Mon Sep 17 00:00:00 2001 From: "github-action[bot]" Date: Tue, 4 Mar 2025 19:52:54 +0100 Subject: [PATCH] Update On Tue Mar 4 19:52:53 CET 2025 --- CLOBBER | 2 +- accessible/base/NotificationController.cpp | 9 +- accessible/base/nsAccessibilityService.cpp | 42 +- accessible/generic/DocAccessible.cpp | 12 + accessible/ipc/RemoteAccessible.cpp | 30 +- .../browser/e10s/browser_caching_relations.js | 19 + .../browser/e10s/browser_caching_states.js | 40 + .../e10s/browser_treeupdate_whitespace.js | 41 + .../windows/uia/browser_textPatterns.js | 49 +- .../treeupdate/test_delayed_removal.html | 1 - accessible/windows/uia/UiaText.cpp | 7 +- browser/base/content/browser-init.js | 17 +- browser/base/content/browser.js | 10 +- browser/base/content/browser.js.globals | 1 + .../static/browser_all_files_referenced.js | 41 +- browser/components/BrowserComponents.manifest | 19 +- browser/components/BrowserGlue.sys.mjs | 927 +---- .../content/ContentAnalysis.sys.mjs | 7 +- .../enterprisepolicies/Policies.sys.mjs | 1 + .../components/extensions/ext-browser.json | 9 + .../browser_ext_tabs_autoDiscardable.js | 2 +- browser/components/moz.build | 1 + .../newtab/AboutHomeStartupCache.sys.mjs | 903 +++++ browser/components/newtab/moz.build | 1 + .../components/places/PlacesUIUtils.sys.mjs | 3 +- .../search/SERPCategorization.sys.mjs | 1750 ++++++++++ .../search/SearchSERPTelemetry.sys.mjs | 1742 +--------- browser/components/search/moz.build | 1 + ...elemetry_categorization_enabled_by_pref.js | 5 +- ...elemetry_categorization_experiment_info.js | 4 +- ...metry_categorization_missing_impression.js | 6 +- ...ry_domain_categorization_download_timer.js | 9 +- ...y_domain_categorization_ping_submission.js | 17 +- ...lemetry_domain_categorization_reporting.js | 9 +- ...y_domain_categorization_reporting_timer.js | 12 +- ...n_categorization_reporting_timer_wakeup.js | 16 +- ...r_search_telemetry_remote_settings_sync.js | 3 +- .../search/test/browser/telemetry/head.js | 9 +- .../telemetry/test_ping_submitted.py | 2 +- .../unit/test_domain_to_categories_store.js | 4 +- ...t_search_telemetry_categorization_logic.js | 47 +- ..._search_telemetry_categorization_region.js | 13 +- ...st_search_telemetry_categorization_sync.js | 123 +- ..._categorization_sync_could_modify_store.js | 13 +- .../sessionstore/SessionStore.sys.mjs | 12 +- .../shell/nsWindowsShellService.cpp | 7 +- .../shopping/content/shopping-container.css | 2 +- browser/components/sidebar/browser-sidebar.js | 1 + browser/components/sidebar/metrics.yaml | 20 + .../tests/browser/browser_glean_sidebar.js | 39 + .../syncedtabs/TabListComponent.sys.mjs | 3 +- .../tabbrowser/SmartTabGrouping.sys.mjs | 65 +- .../tabbrowser/content/browser-allTabsMenu.js | 4 +- .../tabbrowser/content/tabbrowser.js | 73 +- .../tabbrowser/content/tabgroup-menu.js | 2 +- browser/components/tabbrowser/moz.build | 2 +- .../tabunloader/content/aboutUnloads.js | 2 +- .../taskbartabs/TaskbarTabUI.sys.mjs | 29 + browser/components/taskbartabs/moz.build | 13 + .../taskbartabs/test/browser/browser.toml | 4 + .../browser/browser_taskbarTabs_chromeTest.js | 95 + browser/extensions/formautofill/api.js | 1 + .../formautofill/test/browser/browser.toml | 10 + ...ser_autofill_address_select_match_isoid.js | 63 + .../browser_dynamic_form_change_detection.js | 242 ++ ...r_fill_on_dynamic_form_change_detection.js | 271 ++ .../test/browser/browser_form_changes.js | 20 +- .../formautofill/test/browser/head.js | 36 + .../test/fixtures/dynamic_form_changes.html | 150 + ...mless_changes_element_visiblity_state.html | 53 + ...namic_formless_changes_node_mutations.html | 60 + .../form_change_on_user_interaction.html | 46 + .../third_party/NewEgg/BillingInfo.html | 2 +- .../test/unit/test_findLabelElements.js | 100 +- .../formautofill/test/unit/xpcshell.toml | 1 + .../content-src/components/Base/Base.jsx | 3 - .../data/content/activity-stream.bundle.js | 3 - browser/extensions/newtab/docs/index.rst | 15 +- .../about_home_startup_cache.md | 4 +- .../lib/ActivityStreamMessageChannel.sys.mjs | 2 +- .../test/browser/abouthomecache/head.js | 2 +- browser/installer/package-manifest.in | 1 + browser/locales/l10n-changesets.json | 244 +- .../test/browser/browser_TabUnloader.js | 2 +- browser/modules/test/unit/test_TabUnloader.js | 2 +- browser/themes/shared/urlbar-searchbar.css | 43 +- build.gradle | 116 +- build/moz.configure/bindgen.configure | 2 +- build/sparse-profiles/taskgraph | 1 + .../browser_accessibility_print_to_json.js | 18 +- ...owser_accessibility_relation_navigation.js | 10 +- .../browser/browser_accessibility_sidebar.js | 9 +- .../browser_accessibility_tree_navigation.js | 10 +- ...owser_accessibility_tree_navigation_oop.js | 10 +- .../debugger/src/actions/sources/symbols.js | 6 +- .../client/jsonview/components/JsonPanel.js | 2 +- .../test/browser_jsonview_object-type.js | 83 +- devtools/client/jsonview/test/head.js | 15 + devtools/client/netmonitor/test/browser.toml | 2 + .../netmonitor/test/browser_net_json-long.js | 2 +- .../test/browser_net_json-nogrip.js | 2 +- ...rowser_net_requests_with_empty_response.js | 57 + .../shared/components/reps/reps/object.js | 17 +- .../test/node/components/reps/object.test.js | 20 +- .../components/reps/string-with-url.test.js | 2 +- devtools/client/shared/sourceeditor/editor.js | 31 + .../client/shared/sourceeditor/lezer-utils.js | 3 + .../network-observer/NetworkUtils.sys.mjs | 4 + docs/performance/index.md | 2 +- docshell/base/BaseHistory.cpp | 3 +- docshell/base/BrowsingContextGroup.cpp | 115 +- docshell/base/BrowsingContextGroup.h | 44 +- docshell/base/CanonicalBrowsingContext.cpp | 20 +- docshell/base/CanonicalBrowsingContext.h | 6 +- docshell/base/nsDocShell.cpp | 7 + dom/base/CompressionStream.cpp | 23 +- ...ZLibHelper.h => CompressionStreamHelper.h} | 32 +- dom/base/DecompressionStream.cpp | 258 +- dom/base/DocGroup.cpp | 40 +- dom/base/DocGroup.h | 45 +- dom/base/Document.cpp | 134 +- dom/base/Document.h | 13 +- dom/base/ImageTracker.cpp | 14 +- dom/base/Selection.cpp | 20 +- dom/base/Selection.h | 8 +- dom/base/UseCounters.conf | 4 +- dom/base/nsContentUtils.cpp | 46 +- dom/base/nsDocumentWarningList.h | 2 +- dom/base/nsGlobalWindowInner.cpp | 7 + dom/base/nsGlobalWindowInner.h | 1 + dom/base/nsINode.cpp | 10 +- dom/base/use_counter_metrics.yaml | 79 +- dom/bindings/Bindings.conf | 4 - dom/ipc/BrowserHost.cpp | 7 + dom/ipc/ContentChild.cpp | 16 +- dom/ipc/ContentChild.h | 6 +- dom/ipc/DOMTypes.ipdlh | 5 + dom/ipc/PContent.ipdl | 15 +- dom/ipc/ProcessIsolation.cpp | 11 + dom/locales/en-US/chrome/dom/dom.properties | 7 +- dom/media/MediaDecoderStateMachine.cpp | 8 +- dom/media/MediaDecoderStateMachineBase.cpp | 3 + dom/media/MediaDecoderStateMachineBase.h | 8 + dom/media/webaudio/AudioContext.cpp | 4 +- dom/media/webaudio/AudioContext.h | 6 +- dom/media/webaudio/AudioWorklet.cpp | 36 + dom/media/webaudio/AudioWorklet.h | 34 + .../webaudio/AudioWorkletGlobalScope.cpp | 2 +- dom/media/webaudio/AudioWorkletGlobalScope.h | 6 + dom/media/webaudio/AudioWorkletImpl.cpp | 51 +- dom/media/webaudio/AudioWorkletImpl.h | 13 +- dom/media/webaudio/AudioWorkletNode.cpp | 1 + dom/media/webaudio/moz.build | 2 + dom/media/webrtc/jsapi/RTCRtpSender.cpp | 37 +- dom/media/webrtc/jsapi/RTCRtpSender.h | 6 + dom/media/webrtc/jsapi/RTCRtpTransceiver.cpp | 5 +- .../libwebrtcglue/MediaConduitControl.h | 3 + .../webrtc/libwebrtcglue/VideoConduit.cpp | 22 +- dom/media/webrtc/libwebrtcglue/VideoConduit.h | 3 + .../third_party_build/default_config_env | 20 +- dom/messagechannel/MessagePort.cpp | 3 +- dom/notification/Notification.cpp | 340 -- dom/notification/Notification.h | 25 - dom/notification/moz.build | 2 +- dom/security/nsCSPService.cpp | 2 +- dom/security/nsContentSecurityUtils.cpp | 215 +- .../PServiceWorkerRegistration.ipdl | 2 + .../ServiceWorkerRegistration.cpp | 81 +- .../ServiceWorkerRegistrationParent.cpp | 17 + .../ServiceWorkerRegistrationParent.h | 3 + .../ServiceWorkerRegistrationProxy.cpp | 85 + .../ServiceWorkerRegistrationProxy.h | 2 + dom/serviceworkers/ServiceWorkerUtils.h | 4 + .../mochitest/general/test_interfaces.js | 2 + dom/webgpu/ExternalTexture.h | 2 + dom/webgpu/ExternalTextureDMABuf.cpp | 43 +- dom/webgpu/ExternalTextureDMABuf.h | 9 + dom/webgpu/ipc/WebGPUParent.cpp | 24 +- dom/webgpu/ipc/WebGPUParent.h | 23 +- dom/webidl/AudioWorklet.webidl | 1 + dom/webidl/AudioWorkletGlobalScope.webidl | 1 + dom/webidl/CompressionStream.webidl | 1 + dom/webidl/RTCRtpParameters.webidl | 2 + dom/webidl/Selection.webidl | 9 +- dom/webidl/Window.webidl | 5 + dom/worklet/Worklet.h | 7 +- dom/worklet/WorkletImpl.cpp | 2 +- dom/worklet/WorkletImpl.h | 3 +- gfx/harfbuzz/NEWS | 25 +- gfx/harfbuzz/README.md | 5 +- gfx/harfbuzz/moz.yaml | 4 +- gfx/harfbuzz/src/OT/Color/CBDT/CBDT.hh | 4 +- gfx/harfbuzz/src/OT/Var/VARC/VARC.cc | 31 +- gfx/harfbuzz/src/OT/Var/VARC/VARC.hh | 77 +- gfx/harfbuzz/src/OT/Var/VARC/coord-setter.hh | 40 +- gfx/harfbuzz/src/OT/glyf/CompositeGlyph.hh | 2 +- gfx/harfbuzz/src/OT/glyf/Glyph.hh | 66 +- gfx/harfbuzz/src/OT/glyf/SimpleGlyph.hh | 12 +- gfx/harfbuzz/src/OT/glyf/glyf.hh | 92 +- gfx/harfbuzz/src/OT/glyf/path-builder.hh | 2 +- gfx/harfbuzz/src/hb-decycler.hh | 23 +- gfx/harfbuzz/src/hb-directwrite.cc | 25 +- gfx/harfbuzz/src/hb-directwrite.h | 10 +- gfx/harfbuzz/src/hb-face.cc | 10 +- gfx/harfbuzz/src/hb-ft.cc | 37 +- gfx/harfbuzz/src/hb-ft.h | 9 +- gfx/harfbuzz/src/hb-open-type.hh | 178 +- gfx/harfbuzz/src/hb-ot-face-table-list.hh | 5 +- gfx/harfbuzz/src/hb-ot-face.cc | 1 + gfx/harfbuzz/src/hb-ot-hdmx-table.hh | 2 +- gfx/harfbuzz/src/hb-ot-hmtx-table.hh | 2 +- gfx/harfbuzz/src/hb-ot-layout-base-table.hh | 2 +- gfx/harfbuzz/src/hb-ot-layout-common.hh | 32 +- gfx/harfbuzz/src/hb-ot-var-common.hh | 40 +- gfx/harfbuzz/src/hb-ot-var-cvar-table.hh | 26 +- gfx/harfbuzz/src/hb-ot-var-gvar-table.hh | 139 +- gfx/harfbuzz/src/hb-subset-plan.hh | 17 +- gfx/harfbuzz/src/hb-vector.hh | 85 +- gfx/harfbuzz/src/hb-version.h | 4 +- gfx/wgpu_bindings/src/server.rs | 115 +- gradle/libs.versions.toml | 232 ++ gradle/wrapper/gradle-wrapper.jar | Bin 43583 -> 43705 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 2 +- intl/strres/nsStringBundle.cpp | 16 +- ipc/docs/processes.rst | 1 + ipc/docs/utility_process.rst | 12 +- ipc/glue/DataPipe.h | 16 +- ipc/gtest/TestUnsafeSharedMemoryHandle.cpp | 8 +- js/loader/ScriptLoadRequest.cpp | 2 +- js/public/ErrorReport.h | 3 + js/public/ProtoKey.h | 1 + js/public/friend/ErrorNumbers.msg | 14 +- js/public/friend/UsageStatistics.h | 4 +- js/src/builtin/Array.cpp | 236 +- js/src/builtin/Array.h | 82 +- js/src/builtin/Array.js | 8 +- js/src/builtin/Promise.cpp | 33 +- js/src/builtin/TestingFunctions.cpp | 5 +- .../arrays/species-optimize-intrinsic.js | 36 + .../tests/debug/Debugger-onNativeCall-06.js | 12 +- js/src/jit-test/tests/fuses/species-fuse-1.js | 21 + js/src/jit-test/tests/fuses/species-fuse-2.js | 17 + js/src/jit-test/tests/fuses/species-fuse-3.js | 24 + .../tests/promise/thenable-counter.js | 2 +- .../js-promise-integration.new.js | 4 +- js/src/jit/BaselineCacheIRCompiler.cpp | 17 + js/src/jit/CacheIR.cpp | 34 + js/src/jit/CacheIRGenerator.h | 1 + js/src/jit/CacheIROps.yaml | 8 + js/src/jit/CodeGenerator.cpp | 24 +- js/src/jit/CompilationDependencyTracker.h | 8 +- js/src/jit/GenerateLIRFiles.py | 2 +- js/src/jit/InlinableNatives.cpp | 1 + js/src/jit/InlinableNatives.h | 1 + js/src/jit/IonCacheIRCompiler.cpp | 5 + js/src/jit/Lowering.cpp | 6 + js/src/jit/MIR.cpp | 14 + js/src/jit/MIROps.yaml | 11 + js/src/jit/MacroAssembler-inl.h | 4 + js/src/jit/MacroAssembler.h | 1 + js/src/jit/WarpCacheIRTranspiler.cpp | 56 +- .../x86-shared/MacroAssembler-x86-shared.h | 4 +- js/src/jsexn.h | 5 + js/src/tests/user.js | 1 - js/src/vm/ErrorObject.cpp | 15 +- js/src/vm/GlobalObject.cpp | 1 + js/src/vm/Realm.cpp | 1 - js/src/vm/Realm.h | 1 - js/src/vm/RealmFuses.cpp | 95 +- js/src/vm/RealmFuses.h | 33 +- js/src/vm/SelfHosting.cpp | 3 + js/src/vm/Watchtower.cpp | 61 +- js/src/wasm/WasmBCFrame.cpp | 2 +- js/src/wasm/WasmBuiltins.cpp | 5 +- js/src/wasm/WasmCode.cpp | 79 +- js/src/wasm/WasmCode.h | 18 +- js/src/wasm/WasmDebug.cpp | 11 +- js/src/wasm/WasmFrameIter.cpp | 2 +- js/src/wasm/WasmGC.cpp | 11 + js/src/wasm/WasmGC.h | 126 +- js/src/wasm/WasmGenerator.cpp | 41 +- js/src/wasm/WasmInstance.cpp | 22 +- js/src/wasm/WasmJS.cpp | 1 + js/src/wasm/WasmSerialize.cpp | 43 +- js/src/wasm/WasmStubs.cpp | 6 +- js/xpconnect/loader/mozJSModuleLoader.cpp | 3 +- js/xpconnect/src/XPCJSRuntime.cpp | 6 + js/xpconnect/tests/chrome/test_xrayToJS.xhtml | 2 + layout/base/AccessibleCaretManager.cpp | 3 +- layout/base/nsLayoutUtils.cpp | 5 +- layout/generic/ReflowInput.cpp | 34 +- layout/reftests/svg/as-image/reftest.list | 2 +- layout/style/PaintWorkletImpl.cpp | 2 +- layout/style/PaintWorkletImpl.h | 3 +- layout/xul/nsMenuPopupFrame.cpp | 51 +- layout/xul/nsMenuPopupFrame.h | 26 +- media/libdav1d/moz.yaml | 4 +- media/libdav1d/vcs_version.h | 2 +- .../libpng/loongarch/filter_lsx_intrinsics.c | 412 +++ media/libpng/loongarch/loongarch_lsx_init.c | 65 + media/libpng/moz.build | 7 +- media/libpng/moz.yaml | 1 + media/libpng/pnglibconf.h | 7 + media/libvpx/libvpx/build/make/rtcd.pl | 28 +- media/libvpx/libvpx/test/sad_test.cc | 20 + media/libvpx/libvpx/test/svc_datarate_test.cc | 302 +- .../libvpx/vp9/encoder/vp9_aq_cyclicrefresh.c | 6 +- media/libvpx/libvpx/vp9/encoder/vp9_encoder.c | 90 +- .../libvpx/libvpx/vp9/encoder/vp9_ratectrl.c | 16 +- media/libvpx/libvpx/vp9/vp9_cx_iface.c | 11 +- media/libvpx/libvpx/vpx_dsp/vpx_dsp.mk | 1 + .../libvpx/vpx_dsp/vpx_dsp_rtcd_defs.pl | 12 +- media/libvpx/libvpx/vpx_dsp/x86/sad_avx512.c | 88 + media/libvpx/moz.yaml | 4 +- media/webrtc/signaling/gtest/Canonicals.h | 10 +- media/webrtc/signaling/gtest/MockCall.cpp | 7 + media/webrtc/signaling/gtest/MockCall.h | 7 +- .../gtest/videoconduit_unittests.cpp | 128 + .../android/android-components/build.gradle | 24 +- .../components/browser/domains/build.gradle | 8 +- .../browser/engine-gecko/build.gradle | 34 +- .../engine/gecko/GeckoEngineSession.kt | 3 + .../engine/gecko/GeckoEngineSessionTest.kt | 14 + .../browser/engine-system/build.gradle | 18 +- .../browser/errorpages/build.gradle | 6 +- .../components/browser/icons/build.gradle | 38 +- .../components/browser/menu/build.gradle | 20 +- .../components/browser/menu2/build.gradle | 22 +- .../browser/session-storage/build.gradle | 36 +- .../components/browser/state/build.gradle | 24 +- .../browser/storage-sync/build.gradle | 20 +- .../components/browser/tabstray/build.gradle | 14 +- .../browser/thumbnails/build.gradle | 16 +- .../components/browser/toolbar/build.gradle | 18 +- .../compose/awesomebar/build.gradle | 26 +- .../components/compose/base/build.gradle | 22 +- .../compose/browser-toolbar/build.gradle | 24 +- .../components/compose/cfr/build.gradle | 32 +- .../components/compose/engine/build.gradle | 28 +- .../components/compose/tabstray/build.gradle | 20 +- .../concept/awesomebar/build.gradle | 2 +- .../components/concept/base/build.gradle | 10 +- .../components/concept/engine/build.gradle | 14 +- .../components/concept/fetch/build.gradle | 10 +- .../components/concept/menu/build.gradle | 8 +- .../components/concept/push/build.gradle | 2 +- .../components/concept/storage/build.gradle | 6 +- .../components/concept/sync/build.gradle | 2 +- .../components/concept/toolbar/build.gradle | 10 +- .../feature/accounts-push/build.gradle | 16 +- .../components/feature/accounts/build.gradle | 10 +- .../components/feature/addons/build.gradle | 30 +- .../addons/src/main/res/values-oc/strings.xml | 6 + .../components/feature/app-links/build.gradle | 10 +- .../src/main/res/values-oc/strings.xml | 4 +- .../components/feature/autofill/build.gradle | 30 +- .../feature/awesomebar/build.gradle | 12 +- .../feature/containers/build.gradle | 28 +- .../feature/contextmenu/build.gradle | 14 +- .../feature/customtabs/build.gradle | 14 +- .../components/feature/downloads/build.gradle | 38 +- .../downloads/AbstractFetchDownloadService.kt | 1 + .../downloads/src/main/res/values/strings.xml | 2 +- .../AbstractFetchDownloadServiceTest.kt | 17 + .../feature/findinpage/build.gradle | 12 +- .../components/feature/fxsuggest/build.gradle | 17 +- .../components/feature/intent/build.gradle | 10 +- .../components/feature/logins/build.gradle | 28 +- .../components/feature/media/build.gradle | 18 +- .../feature/privatemode/build.gradle | 12 +- .../components/feature/prompts/build.gradle | 32 +- .../components/feature/push/build.gradle | 14 +- .../components/feature/pwa/build.gradle | 30 +- .../components/feature/qr/build.gradle | 16 +- .../feature/readerview/build.gradle | 14 +- .../feature/recentlyclosed/build.gradle | 26 +- .../feature/screendetection/build.gradle | 2 +- .../components/feature/search/build.gradle | 12 +- .../assets/search/search_telemetry_v2.json | 587 ++-- .../feature/serviceworker/build.gradle | 10 +- .../components/feature/session/build.gradle | 16 +- .../components/feature/share/build.gradle | 24 +- .../feature/sitepermissions/build.gradle | 32 +- .../feature/syncedtabs/build.gradle | 16 +- .../feature/tab-collections/build.gradle | 32 +- .../components/feature/tabs/build.gradle | 12 +- .../components/feature/toolbar/build.gradle | 12 +- .../components/feature/top-sites/build.gradle | 34 +- .../components/feature/webauthn/build.gradle | 4 +- .../feature/webcompat-reporter/build.gradle | 12 +- .../components/feature/webcompat/build.gradle | 12 +- .../feature/webnotifications/build.gradle | 12 +- .../components/lib/auth/build.gradle | 10 +- .../components/lib/crash-sentry/build.gradle | 10 +- .../components/lib/crash/build.gradle | 38 +- .../components/lib/dataprotect/build.gradle | 6 +- .../lib/fetch-httpurlconnection/build.gradle | 10 +- .../components/lib/fetch-okhttp/build.gradle | 10 +- .../components/lib/jexl/build.gradle | 4 +- .../lib/publicsuffixlist/build.gradle | 12 +- .../components/lib/push-firebase/build.gradle | 12 +- .../components/lib/state/build.gradle | 32 +- .../service/digitalassetlinks/build.gradle | 10 +- .../service/firefox-accounts/build.gradle | 18 +- .../components/service/glean/build.gradle | 18 +- .../components/service/location/build.gradle | 10 +- .../components/service/mars/build.gradle | 20 +- .../components/service/nimbus/build.gradle | 32 +- .../components/service/pocket/build.gradle | 42 +- .../service/sync-autofill/build.gradle | 8 +- .../service/sync-logins/build.gradle | 4 +- .../support/android-test/build.gradle | 10 +- .../components/support/base/build.gradle | 46 +- .../components/support/images/build.gradle | 24 +- .../components/support/ktx/build.gradle | 24 +- .../components/support/license/build.gradle | 8 +- .../components/support/locale/build.gradle | 16 +- .../support/remotesettings/build.gradle | 12 +- .../support/rusterrors/build.gradle | 2 +- .../components/support/rustlog/build.gradle | 8 +- .../support/test-fakes/build.gradle | 4 +- .../support/test-libstate/build.gradle | 6 +- .../components/support/test/build.gradle | 22 +- .../components/support/utils/build.gradle | 20 +- .../support/webextensions/build.gradle | 10 +- .../components/tooling/detekt/build.gradle | 8 +- .../tooling/fetch-tests/build.gradle | 6 +- .../components/tooling/lint/build.gradle | 16 +- .../components/ui/autocomplete/build.gradle | 6 +- .../components/ui/colors/build.gradle | 6 +- .../components/ui/tabcounter/build.gradle | 8 +- .../src/main/res/values-oc/strings.xml | 3 + .../components/ui/widgets/build.gradle | 16 +- .../android-components/docs/changelog.md | 4 +- .../gradle/wrapper/gradle-wrapper.jar | Bin 43583 -> 43705 bytes .../gradle/wrapper/gradle-wrapper.properties | 2 +- mobile/android/android-components/gradlew | 2 +- .../plugins/config/settings.gradle | 8 + .../config/src/main/java/ConfigPlugin.kt | 68 +- .../plugins/dependencies/settings.gradle | 8 + .../src/main/java/DependenciesPlugin.kt | 250 -- .../plugins/publicsuffixlist/build.gradle | 4 +- .../plugins/publicsuffixlist/settings.gradle | 8 + .../android/android-components/publish.gradle | 24 +- .../samples/browser/build.gradle | 54 +- .../samples/compose-browser/build.gradle | 20 +- .../samples/crash/build.gradle | 12 +- .../samples/dataprotect/build.gradle | 6 +- .../samples/firefox-accounts/build.gradle | 10 +- .../samples/glean/build.gradle | 28 +- .../glean/samples-glean-library/build.gradle | 4 +- .../samples/sync-logins/build.gradle | 6 +- .../samples/sync/build.gradle | 6 +- .../samples/toolbar/build.gradle | 14 +- .../android-components/settings.gradle | 8 + mobile/android/annotations/build.gradle | 4 +- .../messaging_example/app/build.gradle | 8 +- .../port_messaging_example/app/build.gradle | 8 +- mobile/android/exoplayer2/build.gradle | 4 +- mobile/android/fenix/app/build.gradle | 158 +- mobile/android/fenix/app/metrics.yaml | 204 +- mobile/android/fenix/app/nimbus.fml.yaml | 9 + mobile/android/fenix/app/onboarding.fml.yaml | 222 +- mobile/android/fenix/app/pings.yaml | 24 + .../java/org/mozilla/fenix/ui/MainMenuTest.kt | 225 +- .../mozilla/fenix/ui/MainMenuTestCompose.kt | 205 +- .../mozilla/fenix/ui/robots/BrowserRobot.kt | 132 +- .../mozilla/fenix/ui/robots/DownloadRobot.kt | 5 + .../fenix/ui/robots/ThreeDotMenuMainRobot.kt | 14 +- .../ui/robots/ThreeDotMenuMainRobotCompose.kt | 9 + .../metrics/AdjustMetricsService.kt | 59 +- .../metrics/InstallReferrerMetricsService.kt | 13 + .../downloads/listscreen/DownloadFragment.kt | 65 +- .../listscreen/DownloadFragmentStore.kt | 178 - .../downloads/listscreen/DownloadsScreen.kt | 47 +- .../listscreen/FileItemToIconMapper.kt | 52 + .../DownloadUIMapperMiddleware.kt} | 44 +- .../listscreen/store/DownloadUIAction.kt | 62 + .../listscreen/store/DownloadUIState.kt | 68 + .../listscreen/store/DownloadUIStore.kt | 74 + .../{DownloadItem.kt => store/FileItem.kt} | 18 +- .../org/mozilla/fenix/ext/DownloadItem.kt | 49 - .../RecentlyClosedController.kt | 5 +- .../settings/SecretDebugSettingsFragment.kt | 40 +- .../fenix/settings/SecretSettingsFragment.kt | 2 +- .../webcompat/BrokenSiteReporterTestTags.kt | 10 + .../fenix/webcompat/ui/WebCompatReporter.kt | 19 +- .../main/res/layout/preference_divider.xml | 1 - .../main/res/layout/preference_no_widget.xml | 5 +- .../app/src/main/res/values-ast/strings.xml | 71 + .../app/src/main/res/values-cy/strings.xml | 8 +- .../src/main/res/values-es-rCL/strings.xml | 7 + .../app/src/main/res/values-is/strings.xml | 119 + .../app/src/main/res/values-ka/strings.xml | 6 +- .../app/src/main/res/values-lo/strings.xml | 5 + .../app/src/main/res/values-ml/strings.xml | 11 + .../app/src/main/res/values-oc/strings.xml | 132 +- .../src/main/res/values-pt-rBR/strings.xml | 61 + .../app/src/main/res/values-tr/strings.xml | 8 + .../src/main/res/values-zh-rCN/strings.xml | 9 + .../src/main/res/values/static_strings.xml | 1 + .../fenix/app/src/main/res/values/strings.xml | 22 +- .../metrics/AdjustMetricsServiceTest.kt | 46 + ...t.kt => DownloadUIMapperMiddlewareTest.kt} | 24 +- ...entStoreTest.kt => DownloadUIStoreTest.kt} | 56 +- .../listscreen/FileItemToIconMapperTest.kt | 36 + .../FileExistsTest.kt} | 21 +- .../mozilla/fenix/ext/DownloadItemKtTest.kt | 36 - .../RecentlyClosedFragmentInteractorTest.kt | 10 + mobile/android/fenix/benchmark/build.gradle | 8 +- mobile/android/fenix/build.gradle | 36 +- .../fenix/gradle/wrapper/gradle-wrapper.jar | Bin 43583 -> 43705 bytes .../gradle/wrapper/gradle-wrapper.properties | 2 +- mobile/android/fenix/gradlew | 2 +- .../fenix/mozilla-detekt-rules/build.gradle | 14 +- .../fenix/mozilla-lint-rules/build.gradle | 10 +- .../fenix/plugins/apksize/settings.gradle | 8 + .../apksize/src/main/java/ApkSizePlugin.kt | 6 +- mobile/android/fenix/settings.gradle | 2 - mobile/android/focus-android/app/build.gradle | 122 +- .../BiometricAuthenticationFragmentCompose.kt | 19 +- .../mozilla/focus/browser/LocalizedContent.kt | 38 - .../focus/engine/AppContentInterceptor.kt | 5 - .../OnboardingFirstScreenCompose.kt | 83 +- .../OnboardingSecondScreenCompose.kt | 115 +- .../org/mozilla/focus/ui/theme/Modifier.kt | 59 + .../app/src/main/res/raw/about.html | 60 - .../app/src/main/res/values-oc/strings.xml | 8 + .../app/src/main/res/values/strings.xml | 4 +- mobile/android/focus-android/build.gradle | 27 +- .../gradle/wrapper/gradle-wrapper.jar | Bin 43583 -> 43705 bytes .../gradle/wrapper/gradle-wrapper.properties | 2 +- mobile/android/focus-android/gradlew | 2 +- mobile/android/focus-android/settings.gradle | 9 +- mobile/android/geckoview/build.gradle | 26 +- mobile/android/geckoview_example/build.gradle | 12 +- mobile/android/gradle/mozconfig.gradle | 25 +- mobile/android/test_runner/build.gradle | 10 +- mobile/locales/l10n-changesets.json | 196 +- mobile/shared/chrome/geckoview/geckoview.js | 1 + modules/libpref/init/StaticPrefList.yaml | 26 +- modules/libpref/init/all.js | 7 + moz.build | 2 +- mozglue/baseprofiler/moz.build | 2 +- netwerk/base/nsIOService.cpp | 2 +- netwerk/base/nsNetUtil.cpp | 10 + netwerk/base/nsStandardURL.cpp | 1 + netwerk/build/components.conf | 17 + netwerk/dns/effective_tld_names.dat | 19 +- netwerk/ipc/DocumentLoadListener.cpp | 77 +- netwerk/ipc/DocumentLoadListener.h | 9 +- netwerk/protocol/http/HttpBaseChannel.cpp | 32 + netwerk/protocol/http/HttpBaseChannel.h | 1 + netwerk/protocol/http/nsHttpAtomList.h | 1 + netwerk/protocol/http/nsHttpChannel.cpp | 11 + .../protocol/http/nsIHttpChannelInternal.idl | 6 + .../protocol/res/MozSrcProtocolHandler.cpp | 69 + netwerk/protocol/res/MozSrcProtocolHandler.h | 49 + netwerk/protocol/res/moz.build | 2 + netwerk/test/mochitests/mochitest.toml | 5 +- .../test_stale-while-revalidate_positive.js | 15 +- python/mach/mach/command_util.py | 1 + python/mozbuild/mozbuild/frontend/context.py | 8 + python/mozbuild/mozbuild/frontend/data.py | 18 + python/mozbuild/mozbuild/frontend/emitter.py | 21 +- python/mozbuild/mozbuild/frontend/reader.py | 19 +- python/mozbuild/mozbuild/mach_commands.py | 10 + .../frontend/data/moz-src-files/dir/file1.txt | 0 .../frontend/data/moz-src-files/dir/file2.txt | 0 .../moz-src-files/dir/subdir/otherfile.txt | 0 .../test/frontend/data/moz-src-files/file.txt | 0 .../frontend/data/moz-src-files/moz.build | 10 + .../mozbuild/test/frontend/test_emitter.py | 18 + .../mozbuild/mozbuild/test/test_jarmaker.py | 3 +- python/mozbuild/mozpack/packager/formats.py | 1 + security/ct/CTKnownLogs.h | 2 +- security/manager/ssl/StaticHPKPins.h | 5 +- security/manager/ssl/nsSTSPreloadList.inc | 3066 ++++++++++------- security/manager/tools/log_list.json | 4 +- security/sandbox/linux/SandboxFilter.cpp | 13 + .../main/moz-essential-domain-fallbacks.json | 5 +- ...default-unknown-schemes-interventions.json | 8 +- .../dumps/security-state/intermediates.json | 38 +- settings.gradle | 2 - srcdir-resolver.js | 29 + startupcache/StartupCacheUtils.cpp | 13 +- taskcluster/kinds/fetch/toolchains.yml | 4 +- taskcluster/kinds/test/talos.yml | 15 +- taskcluster/kinds/test/test-platforms.yml | 8 +- taskcluster/kinds/test/test-sets.yml | 5 +- taskcluster/kinds/toolchain/android.yml | 14 +- taskcluster/kinds/toolchain/cbindgen.yml | 2 +- testing/mozbase/mozdevice/mozdevice/adb.py | 24 +- .../mozrunner/devices/android_device.py | 25 +- .../configs/unittests/win_unittest.py | 22 + testing/perfdocs/generated/talos.rst | 2078 ++++++++++- testing/talos/perfdocs/config.yml | 2 +- testing/talos/perfdocs/index.rst | 2 +- testing/talos/talos/talos-powers/api.js | 2 +- .../idbobjectstore-rename-store.any.js.ini | 4 +- ...nnect-src-json-import-allowed.sub.html.ini | 2 - ...nnect-src-json-import-blocked.sub.html.ini | 2 - .../import-style-allowed.sub.html.ini | 1 - .../import-style-blocked.sub.html.ini | 1 - .../multiple-content-values-001.xht.ini | 2 +- .../corner-shape-fill-any.html.ini | 26 + .../quotes-lang-dynamic-001.html.ini | 2 + .../css/css-flexbox/flex-basis-013.html.ini | 30 + .../gap-decorations-width-computed.html.ini | 24 + .../gap-decorations-width-valid.html.ini | 45 + .../text-box-trim/new-fc-001.html.ini | 2 + .../text-box-trim-multicol-013.html.ini | 15 + .../scroll-buttons-appearance.html.ini | 2 + .../scroll-marker-focus-visible.html.ini | 3 + ...nput-element-pseudo-open.optional.html.ini | 1 + .../css/css-values/if-conditionals.html.ini | 33 + .../css-values/if-supports-quirks.html.ini | 3 + .../empty-render-target-crash.html.ini | 3 +- ...me-transition-old-main-old-iframe.html.ini | 3 +- ...d-content-container-writing-modes.html.ini | 2 +- .../old-root-vertical-writing-mode.html.ini | 1 + ...nsformed-element-scroll-transform.html.ini | 7 +- .../update-callback-called-once.html.ini | 5 +- ...oot-init-customElements.tentative.html.ini | 3 - .../tentative/observable-take.any.js.ini | 6 + .../textencoder-utf16-surrogates.any.js.ini | 4 +- .../fetch-destination.https.html.ini | 1 - .../network-partition-key.html.ini | 5 +- ...on-module-import-static.https.sub.html.ini | 2 - ...ipt-json-module-import-static.sub.html.ini | 2 - .../window-history.https.sub.html.ini | 13 +- ...avigation-unload-same-origin.window.js.ini | 2 +- ...-no-child-bad-subdomain.sub.https.html.ini | 23 - ...arent-no-child-yes-port.sub.https.html.ini | 14 - ...arent-no-child-yes-same.sub.https.html.ini | 8 - ...subdomain-with-redirect.sub.https.html.ini | 14 - ...-no-child-yes-subdomain.sub.https.html.ini | 14 - ...yeswithparams-subdomain.sub.https.html.ini | 14 - ...arent-yes-child-no-port.sub.https.html.ini | 14 - ...arent-yes-child-no-same.sub.https.html.ini | 8 - ...-yes-child-no-subdomain.sub.https.html.ini | 14 - ...rent-yes-child-yes-port.sub.https.html.ini | 14 - ...rent-yes-child-yes-same.sub.https.html.ini | 8 - ...yes-child-yes-subdomain.sub.https.html.ini | 14 - ...in-child2-yes-subdomain.sub.https.html.ini | 11 - ...hild2-yes-subdomainport.sub.https.html.ini | 29 - ...1-child2-yes-subdomain2.sub.https.html.ini | 29 - ...ubdomain-child2-no-port.sub.https.html.ini | 29 - ...ain-child2-no-subdomain.sub.https.html.ini | 23 - ...ain-child2-no-subdomain.sub.https.html.ini | 23 - ...in-child2-no-subdomain2.sub.https.html.ini | 23 - ...in-child2-yes-subdomain.sub.https.html.ini | 23 - ...n-child2-yes-subdomain2.sub.https.html.ini | 35 - ...hild2-yes-subdomainport.sub.https.html.ini | 35 - ...ubdomain-child2-no-port.sub.https.html.ini | 35 - ...ain-child2-no-subdomain.sub.https.html.ini | 23 - ...in-child2-yes-subdomain.sub.https.html.ini | 23 - ...n-child2-yes-subdomain2.sub.https.html.ini | 35 - ...hild2-yes-subdomainport.sub.https.html.ini | 35 - .../about-blank.https.sub.html.ini | 27 - .../document-domain.sub.https.html.ini | 8 - .../cross-origin-isolated.sub.https.html.ini | 9 - .../csp-sandbox-no.https.html.ini | 5 - .../csp-sandbox-yes.https.html.ini | 5 - .../data-to-javascript-no.https.html.ini | 5 - .../data-to-javascript-yes.https.html.ini | 5 - .../data-url-no.https.html.ini | 5 - .../data-url-yes.https.html.ini | 5 - .../javascript-url-no.https.html.ini | 5 - .../javascript-url-yes.https.html.ini | 5 - .../removed-iframe.sub.https.html.ini | 5 - .../sandboxed-iframe-no.https.html.ini | 5 - .../sandboxed-iframe-yes.https.html.ini | 5 - ...boxed-same-origin-iframe-no.https.html.ini | 5 - ...oxed-same-origin-iframe-yes.https.html.ini | 5 - .../going-back.sub.https.html.ini | 26 - ...no-1-no-same-2-yes-port.sub.https.html.ini | 20 - ...no-same-2-yes-subdomain.sub.https.html.ini | 20 - ...bdomain-2-yes-subdomain.sub.https.html.ini | 14 - ...domain-2-yes-subdomain2.sub.https.html.ini | 20 - ...ain-yes-2-subdomain2-no.sub.https.html.ini | 20 - ...ubdomain-2-no-subdomain.sub.https.html.ini | 26 - ...yes-1-no-same-2-no-port.sub.https.html.ini | 20 - ...-no-same-2-no-subdomain.sub.https.html.ini | 20 - .../insecure-http.sub.html.ini | 8 - ...ener-no-openee-yes-port.sub.https.html.ini | 14 - ...ener-no-openee-yes-same.sub.https.html.ini | 8 - ...no-openee-yes-subdomain.sub.https.html.ini | 14 - ...ener-yes-openee-no-port.sub.https.html.ini | 14 - ...ener-yes-openee-no-same.sub.https.html.ini | 8 - ...yes-openee-no-subdomain.sub.https.html.ini | 14 - ...ner-yes-openee-yes-port.sub.https.html.ini | 14 - ...ner-yes-openee-yes-same.sub.https.html.ini | 8 - ...es-openee-yes-subdomain.sub.https.html.ini | 14 - .../removing-iframes.sub.https.html.ini | 32 - .../meta/html/dom/idlharness.https.html.ini | 6 - .../the-script-element/css-module/__dir__.ini | 1 - .../import-attributes/__dir__.ini | 1 - .../json-module/__dir__.ini | 1 - .../css-import-in-worker.any.js.ini | 5 - .../with-import-assertions.any.js.ini | 8 - .../promise-job-incumbent.html.ini | 1 + .../scroll-margin-no-intersect.html.ini | 7 +- testing/web-platform/meta/mozilla-sync | 2 +- ...dParameters-degradationPreference.html.ini | 2 - ...ary_event_handler_at_ua_shadowdom.html.ini | 6 +- .../meta/selection/idlharness.window.js.ini | 3 - .../tentative/property-reflection.html.ini | 31 +- ...low-storage-access.sub.https.window.js.ini | 13 +- ...-no-storage-access.sub.https.window.js.ini | 3 +- ...eAccess-web-socket.sub.https.window.js.ini | 2 +- .../signatures/tentative/status.window.js.ini | 18 +- .../wasm/jsapi/constructor/compile.any.js.ini | 4 +- .../webaudio/idlharness.https.window.js.ini | 6 - .../browsing_context/navigate/error.py.ini | 4 +- .../set_viewport/viewport.py.ini | 2 + .../invalid.py.ini | 4 +- .../web_extension/uninstall/invalid.py.ini | 3 + .../tests/classic/set_window_rect/set.py.ini | 2 +- .../meta/webrtc/RTCIceTransport.html.ini | 6 +- .../mozilla/meta/compression/zstd/__dir__.ini | 1 + .../compression-constructor.tentative.any.js | 93 + .../decompression-bad-chunks.tentative.any.js | 66 + ...ecompression-buffersource.tentative.any.js | 88 + ...decompression-constructor.tentative.any.js | 92 + ...compression-correct-input.tentative.any.js | 36 + ...compression-corrupt-input.tentative.any.js | 407 +++ ...decompression-empty-input.tentative.any.js | 31 + ...decompression-split-chunk.tentative.any.js | 66 + ...ression-uint8array-output.tentative.any.js | 28 + .../webdriver/classic/addon_install/addon.py | 10 +- .../webdriver/classic/addon_install/path.py | 16 +- .../addon_uninstall/addon_uninstall.py | 4 +- .../name/comp_host_language_label.html | 11 + .../corner-shape-fill-any-ref.html | 48 + .../corner-shape/corner-shape-fill-any.html | 31 + .../corner-shape/corner-shape-render-ref.html | 6 +- .../corner-shape/resources/corner-math.js | 51 + .../resources/resolve-corner-style.js | 90 + .../css-content/quotes-lang-dynamic-001.html | 13 + .../css-content/quotes-slot-scoping-ref.html | 9 +- .../quotes-lang-dynamic-001-ref.html | 7 + .../tests/css/css-flexbox/flex-basis-013.html | 80 + .../gap-decorations-width-computed.html | 23 +- .../gap-decorations-width-invalid.html | 24 +- .../parsing/gap-decorations-width-valid.html | 49 +- .../css-images/object-fit-none-png-001c.html | 1 + .../css-images/object-fit-none-png-002c.html | 1 + .../object-fit-scale-down-png-001c.html | 1 + .../object-fit-scale-down-png-002c.html | 1 + .../css-inline/text-box-trim/new-fc-001.html | 33 + .../text-box-trim-multicol-013.html | 48 + .../scroll-buttons-appearance-ref.html | 10 + .../scroll-buttons-appearance.html | 19 + .../scroll-buttons-disabled-ref.html | 3 - .../scroll-buttons-disabled-rtl-ref.html | 3 - ...oll-buttons-disabled-vertical-ltr-ref.html | 3 - .../scroll-buttons-enabled-ref.html | 3 - .../scroll-buttons-enabled-rtl-ref.html | 3 - ...roll-buttons-enabled-vertical-ltr-ref.html | 3 - .../scroll-marker-focus-visible.html | 60 + .../tests/css/css-scoping/svg-id-ref-001.html | 36 + .../tests/css/css-values/if-conditionals.html | 37 +- .../css/css-values/if-supports-quirks.html | 20 + .../orthogonal-child-with-border.html | 38 + ...dowRoot-init-customElements.tentative.html | 4 +- ...polymer-polyfill-regression.tentative.html | 22 + .../tentative/observable-take.any.js | 25 + ...nerate-bid-browser-signals.https.window.js | 5 + .../tentative/utf8-helpers.https.window.js | 209 ++ .../about-blank.https.sub.html | 5 +- .../dialog-open-pseudo-invalidation-ref.html | 11 + .../dialog-open-pseudo-invalidation.html | 22 + ...s-mode-no-height-is-still-bounded-ref.html | 17 + ...-no-height-is-still-bounded.tentative.html | 15 + ...target-plain-inline-element.tentative.html | 40 + .../testharness/full.stop/full-stop.html | 8 + .../after-transition-commit-helpers.js | 8 +- .../registration-association.https.window.js | 11 +- .../tests/resources/testharness.js | 3 +- .../detached-register-crash.https.html | 14 + .../reference-target/tentative/label-for.html | 53 + .../tentative/property-reflection.html | 49 +- ...e-allow-storage-access.sub.https.window.js | 66 +- ...rame-no-storage-access.sub.https.window.js | 50 +- .../resources/embedded_responder.js | 5 +- .../resources/permissions-iframe.https.html | 57 +- ...sandboxed-iframe-allow-storage-access.html | 57 + .../sandboxed-iframe-no-storage-access.html | 44 + ...rage-access-permission.sub.https.window.js | 189 +- .../signatures/tentative/status.window.js | 22 +- .../webdriver/bidi/modules/bluetooth.py | 5 +- .../wptrunner/wptrunner/browsers/chrome.py | 6 + .../wptrunner/executors/asyncactions.py | 4 +- .../wptrunner/executors/executorwebdriver.py | 5 +- .../wptrunner/wptrunner/executors/protocol.py | 3 +- .../audioworklet-messageport.https.html | 13 + .../processors/port-processor.js | 2 + .../bluetooth/simulate_adapter/__init__.py | 2 +- .../bluetooth/simulate_adapter/invalid.py | 8 +- .../__init__.py | 2 +- .../tests/bidi/web_extension/conftest.py | 23 +- .../bidi/web_extension/install/install.py | 24 +- .../bidi/web_extension/install/invalid.py | 10 +- .../bidi/web_extension/uninstall/invalid.py | 4 +- .../bidi/web_extension/uninstall/uninstall.py | 4 +- .../tests/webdriver/tests/support/helpers.py | 6 +- .../chrome/unpacked/manifest.json | 6 + .../tests/webrtc/RTCIceTransport.html | 16 + third_party/dav1d/include/common/attributes.h | 6 + third_party/dav1d/src/decode.c | 4 +- third_party/dav1d/src/tables.c | 7 + third_party/libwebrtc/AUTHORS | 3 + third_party/libwebrtc/BUILD.gn | 1 + third_party/libwebrtc/DEPS | 205 +- .../libwebrtc/README.mozilla.last-vendor | 6 +- third_party/libwebrtc/api/audio/BUILD.gn | 2 + .../libwebrtc/api/audio/audio_device.h | 6 +- .../api/audio/echo_canceller3_factory.cc | 7 +- .../api/audio/echo_canceller3_factory.h | 10 +- .../libwebrtc/api/audio/echo_control.h | 15 +- .../builtin_audio_decoder_factory.cc | 3 +- .../builtin_audio_encoder_factory.cc | 3 +- .../audio_encoder_multi_channel_opus_config.h | 1 + .../libwebrtc/api/data_channel_interface.h | 6 +- third_party/libwebrtc/api/fec_controller.h | 1 + third_party/libwebrtc/api/field_trials.cc | 44 +- third_party/libwebrtc/api/field_trials.h | 6 +- third_party/libwebrtc/api/rtc_error.h | 10 + .../libwebrtc/api/rtc_event_log_output.h | 1 - third_party/libwebrtc/api/stats/rtc_stats.h | 1 + .../test/create_network_emulation_manager.h | 2 +- .../libwebrtc/api/test/mock_rtpsender.h | 4 +- .../api/test/mock_transformable_audio_frame.h | 4 +- .../libwebrtc/api/test/mock_video_decoder.h | 3 +- .../libwebrtc/api/test/rtc_error_matchers.h | 36 +- .../libwebrtc/api/transport/rtp/BUILD.gn | 20 + .../rtp}/corruption_detection_message.h | 6 +- .../corruption_detection_message_gn/moz.build | 0 .../corruption_detection_message_unittest.cc | 2 +- third_party/libwebrtc/api/uma_metrics.h | 38 + .../api/video/video_stream_encoder_settings.h | 1 - .../api/video_codecs/video_decoder.cc | 5 +- .../api/video_codecs/video_encoder.cc | 4 +- .../api/voip/test/compile_all_headers.cc | 1 - third_party/libwebrtc/audio/BUILD.gn | 13 + .../libwebrtc/audio/audio_receive_stream.cc | 35 +- .../libwebrtc/audio/audio_receive_stream.h | 20 +- .../audio/audio_receive_stream_unittest.cc | 16 +- .../audio/audio_send_stream_tests.cc | 4 +- .../audio/audio_send_stream_unittest.cc | 31 +- .../libwebrtc/audio/channel_receive.cc | 36 - third_party/libwebrtc/audio/channel_receive.h | 15 +- third_party/libwebrtc/audio/channel_send.cc | 74 +- third_party/libwebrtc/audio/channel_send.h | 23 +- .../libwebrtc/audio/channel_send_unittest.cc | 22 +- .../libwebrtc/audio/mock_voe_channel_proxy.h | 28 +- .../audio/test/non_sender_rtt_test.cc | 20 +- .../audio/voip/test/audio_channel_unittest.cc | 2 +- third_party/libwebrtc/call/BUILD.gn | 1 + .../libwebrtc/call/adaptation/BUILD.gn | 7 + .../resource_adaptation_processor_unittest.cc | 24 +- third_party/libwebrtc/call/call.cc | 32 +- third_party/libwebrtc/call/call_config.cc | 3 +- third_party/libwebrtc/call/call_unittest.cc | 64 - third_party/libwebrtc/call/create_call.cc | 40 - .../call/flexfec_receive_stream_unittest.cc | 1 - .../libwebrtc/call/payload_type_picker.cc | 16 +- .../call/payload_type_picker_unittest.cc | 8 +- .../libwebrtc/call/rtp_payload_params.cc | 6 +- .../call/rtp_payload_params_unittest.cc | 1 + .../libwebrtc/call/rtp_video_sender.cc | 2 +- .../call/rtp_video_sender_unittest.cc | 46 +- .../call/rtx_receive_stream_unittest.cc | 1 - third_party/libwebrtc/call/version.cc | 2 +- .../libwebrtc/common_audio/ring_buffer.c | 43 +- .../auto_corr_to_refl_coef.c | 141 +- .../signal_processing/auto_correlation.c | 1 - .../signal_processing/complex_bit_reverse.c | 54 +- .../complex_bit_reverse_mips.c | 263 +- .../signal_processing/complex_fft.c | 438 ++- .../signal_processing/complex_fft_mips.c | 478 +-- .../signal_processing/copy_set_operations.c | 66 +- .../cross_correlation_mips.c | 160 +- .../cross_correlation_neon.c | 21 +- .../signal_processing/division_operations.c | 167 +- .../signal_processing/downsample_fast.c | 11 +- .../signal_processing/downsample_fast_mips.c | 240 +- .../signal_processing/downsample_fast_neon.c | 5 +- .../common_audio/signal_processing/energy.c | 29 +- .../signal_processing/filter_ar.c | 108 +- .../signal_processing/filter_ar_fast_q12.c | 6 +- .../filter_ar_fast_q12_mips.c | 197 +- .../signal_processing/filter_ma_fast_q12.c | 49 +- .../signal_processing/get_hanning_window.c | 91 +- .../signal_processing/get_scaling_square.c | 41 +- .../signal_processing/levinson_durbin.c | 406 ++- .../signal_processing/lpc_to_refl_coef.c | 56 +- .../signal_processing/min_max_operations.c | 10 +- .../min_max_operations_mips.c | 447 ++- .../min_max_operations_neon.c | 8 +- .../randomization_functions.c | 123 +- .../signal_processing/refl_coef_to_lpc.c | 67 +- .../common_audio/signal_processing/resample.c | 724 ++-- .../signal_processing/resample_48khz.c | 234 +- .../signal_processing/resample_by_2.c | 30 +- .../resample_by_2_internal.c | 1128 +++--- .../signal_processing/resample_by_2_mips.c | 171 +- .../signal_processing/resample_fractional.c | 327 +- .../common_audio/signal_processing/spl_inl.c | 4 +- .../common_audio/signal_processing/spl_sqrt.c | 260 +- .../signal_processing/splitting_filter.c | 294 +- .../sqrt_of_one_minus_x_squared.c | 29 +- .../signal_processing/vector_operations.c | 108 +- .../vector_scaling_operations.c | 164 +- .../vector_scaling_operations_mips.c | 49 +- .../common_audio/third_party/.clang-format | 1 + .../third_party/ooura/README.chromium | 2 +- .../libwebrtc/common_audio/vad/vad_core.c | 264 +- .../common_audio/vad/vad_filterbank.c | 82 +- .../libwebrtc/common_audio/vad/vad_gmm.c | 4 +- .../libwebrtc/common_audio/vad/vad_sp.c | 18 +- .../libwebrtc/common_audio/vad/webrtc_vad.c | 16 +- third_party/libwebrtc/common_video/BUILD.gn | 22 +- .../corruption_detection_converters.cc | 2 +- .../corruption_detection_converters.h | 2 +- ...orruption_detection_converters_unittest.cc | 2 +- .../appspot/apprtc/TCPChannelClientTest.java | 3 + .../objc/AppRTCMobile/ARDAppClient+Internal.h | 6 +- .../examples/objc/AppRTCMobile/ARDAppClient.h | 20 +- .../examples/objc/AppRTCMobile/ARDAppClient.m | 358 +- .../objc/AppRTCMobile/ARDAppEngineClient.m | 51 +- .../objc/AppRTCMobile/ARDCaptureController.h | 3 +- .../objc/AppRTCMobile/ARDCaptureController.m | 25 +- .../AppRTCMobile/ARDExternalSampleCapturer.m | 9 +- .../objc/AppRTCMobile/ARDRoomServerClient.h | 6 +- .../objc/AppRTCMobile/ARDSettingsModel.h | 16 +- .../objc/AppRTCMobile/ARDSettingsModel.m | 56 +- .../objc/AppRTCMobile/ARDSettingsStore.m | 3 +- .../objc/AppRTCMobile/ARDSignalingChannel.h | 6 +- .../objc/AppRTCMobile/ARDSignalingMessage.h | 12 +- .../objc/AppRTCMobile/ARDSignalingMessage.m | 29 +- .../objc/AppRTCMobile/ARDStatsBuilder.m | 1 - .../objc/AppRTCMobile/ARDTURNClient.h | 4 +- .../objc/AppRTCMobile/ARDTURNClient.m | 36 +- .../objc/AppRTCMobile/ARDWebSocketChannel.m | 48 +- .../objc/AppRTCMobile/RTCIceCandidate+JSON.h | 6 +- .../objc/AppRTCMobile/RTCIceCandidate+JSON.m | 12 +- .../objc/AppRTCMobile/RTCIceServer+JSON.h | 3 +- .../objc/AppRTCMobile/RTCIceServer+JSON.m | 3 +- .../AppRTCMobile/RTCSessionDescription+JSON.m | 3 +- .../objc/AppRTCMobile/common/ARDUtilities.h | 8 +- .../objc/AppRTCMobile/common/ARDUtilities.m | 74 +- .../objc/AppRTCMobile/ios/ARDAppDelegate.m | 6 +- .../ios/ARDFileCaptureController.h | 3 +- .../ios/ARDFileCaptureController.m | 3 +- .../objc/AppRTCMobile/ios/ARDMainView.h | 4 +- .../objc/AppRTCMobile/ios/ARDMainView.m | 72 +- .../AppRTCMobile/ios/ARDMainViewController.m | 92 +- .../ios/ARDSettingsViewController.m | 103 +- .../objc/AppRTCMobile/ios/ARDVideoCallView.h | 6 +- .../objc/AppRTCMobile/ios/ARDVideoCallView.m | 40 +- .../ios/ARDVideoCallViewController.m | 79 +- .../ios/RTCVideoCodecInfo+HumanReadable.m | 3 +- .../ARDBroadcastSampleHandler.h | 3 +- .../ARDBroadcastSampleHandler.m | 32 +- .../ARDBroadcastSetupViewController.h | 3 +- .../ARDBroadcastSetupViewController.m | 62 +- .../objc/AppRTCMobile/mac/APPRTCAppDelegate.m | 7 +- .../AppRTCMobile/mac/APPRTCViewController.m | 120 +- .../AppRTCMobile/tests/ARDAppClient_xctest.mm | 161 +- .../tests/ARDFileCaptureController_xctest.mm | 6 +- .../tests/ARDSettingsModel_xctest.mm | 9 +- .../AppRTCMobile/third_party/.clang-format | 1 + .../third_party/SocketRocket/SRWebSocket.h | 9 +- .../objcnativeapi/objc/NADAppDelegate.m | 31 +- .../objcnativeapi/objc/NADViewController.mm | 73 +- .../examples/objcnativeapi/objc/main.m | 3 +- .../objcnativeapi/objc/objc_call_client.h | 18 +- .../objcnativeapi/objc/objc_call_client.mm | 59 +- .../peerconnection/server/data_socket.h | 1 - .../libwebrtc/experiments/field_trials.py | 6 +- third_party/libwebrtc/g3doc/style-guide.md | 1 + .../libwebrtc/infra/specs/client.webrtc.json | 114 +- .../infra/specs/generate_buildbot_json.py | 84 +- .../libwebrtc/infra/specs/gn_isolate_map.pyl | 6 +- third_party/libwebrtc/infra/specs/mixins.pyl | 3 + .../libwebrtc/infra/specs/mixins_webrtc.pyl | 92 + .../infra/specs/tryserver.webrtc.json | 114 +- .../libwebrtc/infra/specs/waterfalls.pyl | 16 +- .../rtc_event_neteq_set_minimum_delay.cc | 2 - third_party/libwebrtc/media/base/codec.cc | 4 - .../libwebrtc/media/base/codec_comparators.cc | 13 +- .../media/base/codec_comparators_unittest.cc | 106 + .../libwebrtc/media/base/codec_unittest.cc | 1 - .../libwebrtc/media/base/fake_media_engine.cc | 6 + .../libwebrtc/media/base/fake_media_engine.h | 2 + .../media/base/media_channel_impl.cc | 46 +- .../libwebrtc/media/base/media_constants.cc | 1 + .../libwebrtc/media/base/media_constants.h | 1 + .../libwebrtc/media/base/media_engine.cc | 3 +- .../libwebrtc/media/base/stream_params.h | 2 +- .../libwebrtc/media/base/video_adapter.cc | 5 +- .../media/engine/fake_webrtc_video_engine.cc | 8 +- .../media/engine/fake_webrtc_video_engine.h | 1 + .../media/engine/webrtc_video_engine.cc | 3 +- .../engine/webrtc_video_engine_unittest.cc | 28 + .../media/engine/webrtc_voice_engine.cc | 12 +- .../libwebrtc/modules/audio_coding/BUILD.gn | 25 +- .../audio_coding/acm2/acm_send_test.cc | 2 +- .../builtin_audio_encoder_factory_unittest.cc | 23 +- .../audio_coding/codecs/g711/g711_interface.c | 3 +- .../audio_coding/codecs/g722/g722_interface.c | 120 +- .../isac/main/source/filter_functions.c | 126 +- .../codecs/isac/main/source/pitch_estimator.c | 356 +- .../codecs/isac/main/source/pitch_filter.c | 101 +- .../codecs/opus/audio_encoder_opus.cc | 6 +- .../audio_coding/neteq/buffer_level_filter.cc | 2 +- .../audio_coding/neteq/delay_manager.cc | 1 - .../neteq/packet_buffer_unittest.cc | 1 - .../neteq/test/delay_tool/parse_delay_file.m | 1 + .../neteq/test/delay_tool/plot_neteq_delay.m | 1 + .../audio_coding/neteq/tools/neteq_rtpplay.cc | 23 +- .../neteq/tools/neteq_test_factory.cc | 26 +- .../neteq/tools/neteq_test_factory.h | 6 + .../neteq/tools/packet_unittest.cc | 2 +- .../modules/audio_device/audio_device_impl.cc | 7 +- .../linux/audio_device_alsa_linux.cc | 4 +- .../modules/audio_processing/BUILD.gn | 1 - .../modules/audio_processing/aec3/BUILD.gn | 6 +- .../aec3/adaptive_fir_filter_unittest.cc | 4 +- .../audio_processing/aec3/aec_state.cc | 34 +- .../modules/audio_processing/aec3/aec_state.h | 5 +- .../aec3/aec_state_unittest.cc | 6 +- .../audio_processing/aec3/block_processor.cc | 31 +- .../audio_processing/aec3/block_processor.h | 16 +- .../aec3/block_processor_unittest.cc | 69 +- .../aec3/comfort_noise_generator_unittest.cc | 3 +- .../audio_processing/aec3/echo_canceller3.cc | 186 +- .../audio_processing/aec3/echo_canceller3.h | 9 +- .../aec3/echo_canceller3_unittest.cc | 73 +- .../audio_processing/aec3/echo_remover.cc | 28 +- .../audio_processing/aec3/echo_remover.h | 11 +- .../aec3/echo_remover_metrics_unittest.cc | 3 +- .../aec3/echo_remover_unittest.cc | 27 +- .../audio_processing/aec3/erle_estimator.cc | 5 +- .../audio_processing/aec3/erle_estimator.h | 3 +- .../aec3/erle_estimator_unittest.cc | 6 +- .../audio_processing/aec3/matched_filter.cc | 9 +- .../refined_filter_update_gain_unittest.cc | 29 +- .../aec3/render_delay_buffer.cc | 1 - .../aec3/residual_echo_estimator.cc | 24 +- .../aec3/residual_echo_estimator.h | 4 +- .../aec3/residual_echo_estimator_unittest.cc | 7 +- .../aec3/subband_erle_estimator.cc | 13 +- .../aec3/subband_erle_estimator.h | 4 +- .../audio_processing/aec3/subtractor.cc | 13 +- .../audio_processing/aec3/subtractor.h | 4 +- .../aec3/subtractor_unittest.cc | 39 +- .../audio_processing/aec3/suppression_gain.cc | 1 - .../aec3/suppression_gain_unittest.cc | 9 +- .../audio_processing/aec3/transparent_mode.cc | 17 +- .../audio_processing/aec3/transparent_mode.h | 2 + .../modules/audio_processing/agc/BUILD.gn | 6 +- .../agc/agc_manager_direct.cc | 18 +- .../audio_processing/agc/agc_manager_direct.h | 3 + .../agc/agc_manager_direct_unittest.cc | 339 +- .../audio_processing/audio_processing_impl.cc | 7 +- .../audio_processing/gain_control_impl.cc | 1 - .../modules/audio_processing/test/apmtest.m | 1 + .../test/echo_canceller3_config_json.h | 7 +- .../echo_canceller3_config_json_unittest.cc | 2 +- .../modules/congestion_controller/BUILD.gn | 1 + .../goog_cc/loss_based_bwe_v2.cc | 14 +- .../goog_cc/loss_based_bwe_v2_test.cc | 3 +- .../goog_cc/send_side_bandwidth_estimation.cc | 3 +- .../receive_side_congestion_controller.h | 12 +- .../receive_side_congestion_controller.cc | 21 +- ...ive_side_congestion_controller_unittest.cc | 26 +- .../modules/desktop_capture/BUILD.gn | 5 +- .../modules/desktop_capture/desktop_frame.cc | 2 + .../modules/desktop_capture/desktop_frame.h | 11 + .../desktop_capture/desktop_frame_unittest.cc | 14 +- .../mac/desktop_configuration.mm | 27 +- .../mac/desktop_frame_cgimage.mm | 47 +- .../mac/desktop_frame_iosurface.mm | 37 +- .../mac/desktop_frame_provider.mm | 5 +- .../desktop_capture/mac/screen_capturer_mac.h | 7 +- .../mac/screen_capturer_mac.mm | 146 +- .../mac/screen_capturer_sck.mm | 379 +- .../mouse_cursor_monitor_mac.mm | 46 +- .../desktop_capture/screen_capturer_darwin.mm | 9 +- .../screen_capturer_fuchsia.cc | 6 +- .../modules/desktop_capture/win/cursor.cc | 4 +- .../win/dxgi_adapter_duplicator.cc | 19 +- .../win/dxgi_adapter_duplicator.h | 7 +- .../win/dxgi_duplicator_controller.cc | 55 +- .../win/dxgi_duplicator_controller.h | 17 +- .../modules/desktop_capture/win/dxgi_frame.cc | 6 +- .../modules/desktop_capture/win/dxgi_frame.h | 7 +- .../win/dxgi_output_duplicator.cc | 10 + .../win/dxgi_output_duplicator.h | 6 + .../win/wgc_capture_session.cc | 19 +- .../desktop_capture/win/wgc_capture_session.h | 9 + .../desktop_capture/win/wgc_capturer_win.cc | 4 +- .../desktop_capture/window_capturer_mac.mm | 25 +- .../modules/pacing/rtp_packet_pacer.h | 1 - .../modules/remote_bitrate_estimator/BUILD.gn | 2 + .../congestion_control_feedback_generator.cc | 8 +- .../congestion_control_feedback_generator.h | 2 - ...ion_control_feedback_generator_unittest.cc | 5 +- .../rtp_transport_feedback_generator.h | 6 +- ...port_sequence_number_feedback_generator.cc | 79 +- ...sport_sequence_number_feedback_generator.h | 23 +- ...ence_number_feedback_generator_unittest.cc | 110 +- .../libwebrtc/modules/rtp_rtcp/BUILD.gn | 8 +- .../modules/rtp_rtcp/mocks/mock_rtp_rtcp.h | 4 +- .../source/corruption_detection_extension.cc | 2 +- .../source/corruption_detection_extension.h | 2 +- ...corruption_detection_extension_unittest.cc | 21 +- .../forward_error_correction_internal.cc | 2 +- .../modules/rtp_rtcp/source/rtp_format.h | 4 +- .../rtp_rtcp/source/rtp_header_extensions.h | 5 +- .../rtp_rtcp/source/rtp_packetizer_av1.h | 2 +- .../source/rtp_packetizer_av1_test_helper.cc | 4 +- .../rtp_rtcp/source/rtp_sender_video.cc | 7 +- .../source/rtp_sender_video_unittest.cc | 2 +- .../video_rtp_depacketizer_h264_unittest.cc | 5 +- .../modules/third_party/.clang-format | 1 + .../modules/utility/source/jvm_android.cc | 3 +- .../libwebrtc/modules/video_capture/BUILD.gn | 4 + .../test/video_capture_unittest.cc | 46 +- .../video_coding/codecs/av1/dav1d_decoder.cc | 4 +- .../codecs/test/objc_codec_factory_helper.mm | 6 +- .../modules/video_coding/deprecated/packet.cc | 3 +- .../video_coding/deprecated/receiver.cc | 3 +- .../video_coding/generic_decoder_unittest.cc | 3 +- .../modules/video_coding/nack_requester.cc | 3 +- .../modules/video_coding/packet_buffer.cc | 3 +- .../video_coding/packet_buffer_unittest.cc | 13 + .../modules/video_coding/timing/timing.cc | 10 +- .../modules/video_coding/timing/timing.h | 4 +- .../video_coding/timing/timing_unittest.cc | 19 +- .../utility/vp9_uncompressed_header_parser.cc | 3 +- .../libwebrtc/moz-patch-stack/0001.patch | 19 +- .../libwebrtc/moz-patch-stack/0008.patch | 29 +- .../libwebrtc/moz-patch-stack/0009.patch | 4 +- .../libwebrtc/moz-patch-stack/0010.patch | 2 +- .../libwebrtc/moz-patch-stack/0012.patch | 4 +- .../libwebrtc/moz-patch-stack/0013.patch | 2 +- .../libwebrtc/moz-patch-stack/0030.patch | 54 +- .../libwebrtc/moz-patch-stack/0032.patch | 20 +- .../libwebrtc/moz-patch-stack/0033.patch | 2 +- .../libwebrtc/moz-patch-stack/0039.patch | 16 +- .../libwebrtc/moz-patch-stack/0040.patch | 26 +- .../libwebrtc/moz-patch-stack/0042.patch | 4 +- .../libwebrtc/moz-patch-stack/0044.patch | 4 +- .../libwebrtc/moz-patch-stack/0047.patch | 9 +- .../libwebrtc/moz-patch-stack/0048.patch | 4 +- .../libwebrtc/moz-patch-stack/0051.patch | 6 +- .../libwebrtc/moz-patch-stack/0052.patch | 4 +- .../libwebrtc/moz-patch-stack/0053.patch | 4 +- .../libwebrtc/moz-patch-stack/0056.patch | 10 +- .../libwebrtc/moz-patch-stack/0061.patch | 2 +- .../libwebrtc/moz-patch-stack/0063.patch | 16 +- .../libwebrtc/moz-patch-stack/0065.patch | 14 +- .../libwebrtc/moz-patch-stack/0066.patch | 6 +- .../libwebrtc/moz-patch-stack/0067.patch | 12 +- .../libwebrtc/moz-patch-stack/0068.patch | 2 +- .../libwebrtc/moz-patch-stack/0072.patch | 2 +- .../libwebrtc/moz-patch-stack/0074.patch | 2 +- .../libwebrtc/moz-patch-stack/0077.patch | 2 +- .../libwebrtc/moz-patch-stack/0078.patch | 2 +- .../libwebrtc/moz-patch-stack/0079.patch | 4 +- .../libwebrtc/moz-patch-stack/0080.patch | 2 +- .../libwebrtc/moz-patch-stack/0081.patch | 4 +- .../libwebrtc/moz-patch-stack/0085.patch | 6 +- .../libwebrtc/moz-patch-stack/0093.patch | 26 +- .../libwebrtc/moz-patch-stack/0094.patch | 6 +- .../libwebrtc/moz-patch-stack/0096.patch | 2 +- .../libwebrtc/moz-patch-stack/0098.patch | 2 +- .../libwebrtc/moz-patch-stack/0105.patch | 4 +- .../libwebrtc/moz-patch-stack/0107.patch | 2 +- .../libwebrtc/moz-patch-stack/0108.patch | 6 +- .../libwebrtc/moz-patch-stack/0111.patch | 16 +- .../libwebrtc/moz-patch-stack/0112.patch | 6 +- .../libwebrtc/moz-patch-stack/0118.patch | 280 +- .../libwebrtc/moz-patch-stack/0119.patch | 45 +- .../libwebrtc/moz-patch-stack/0120.patch | 163 +- .../libwebrtc/moz-patch-stack/0121.patch | 21 +- .../libwebrtc/moz-patch-stack/0123.patch | 4 +- .../libwebrtc/moz-patch-stack/0124.patch | 8 +- .../libwebrtc/moz-patch-stack/0125.patch | 4 +- .../libwebrtc/moz-patch-stack/0126.patch | 2 +- .../libwebrtc/moz-patch-stack/0127.patch | 2 +- .../libwebrtc/moz-patch-stack/0128.patch | 30 +- .../libwebrtc/moz-patch-stack/0129.patch | 4 +- .../libwebrtc/moz-patch-stack/0134.patch | 4 +- .../libwebrtc/moz-patch-stack/0136.patch | 10 +- .../libwebrtc/moz-patch-stack/0139.patch | 2 +- .../libwebrtc/moz-patch-stack/0142.patch | 2 +- .../libwebrtc/moz-patch-stack/0143.patch | 2 +- .../libwebrtc/moz-patch-stack/0145.patch | 2 +- .../a705b2d73a.no-op-cherry-pick-msg | 1 + third_party/libwebrtc/moz.build | 2 +- .../net/dcsctp/socket/heartbeat_handler.cc | 6 +- .../net/dcsctp/timer/task_queue_timeout.cc | 57 +- third_party/libwebrtc/p2p/BUILD.gn | 186 +- third_party/libwebrtc/p2p/base/connection.cc | 90 +- third_party/libwebrtc/p2p/base/connection.h | 29 +- .../p2p/base/ice_transport_internal.h | 17 +- .../p2p/base/p2p_transport_channel.cc | 88 +- .../p2p/base/p2p_transport_channel.h | 18 + .../base/p2p_transport_channel_unittest.cc | 2088 ++++++++--- .../libwebrtc/p2p/base/port_unittest.cc | 574 ++- .../libwebrtc/p2p/base/pseudo_tcp_unittest.cc | 39 +- .../libwebrtc/p2p/base/stun_port_unittest.cc | 225 +- .../p2p/base/stun_request_unittest.cc | 21 +- .../libwebrtc/p2p/base/tcp_port_unittest.cc | 117 +- .../libwebrtc/p2p/base/turn_port_unittest.cc | 458 ++- .../client/basic_port_allocator_unittest.cc | 669 +++- .../p2p/dtls/dtls_ice_integrationtest.cc | 208 ++ .../dtls/dtls_stun_piggyback_controller.cc | 36 +- .../p2p/dtls/dtls_stun_piggyback_controller.h | 8 +- ...dtls_stun_piggyback_controller_unittest.cc | 27 +- .../libwebrtc/p2p/dtls/dtls_transport.cc | 163 +- .../libwebrtc/p2p/dtls/dtls_transport.h | 25 +- .../p2p/dtls/dtls_transport_unittest.cc | 128 +- third_party/libwebrtc/p2p/dtls/dtls_utils.cc | 44 +- .../libwebrtc/p2p/dtls/dtls_utils_unittest.cc | 105 +- third_party/libwebrtc/pc/BUILD.gn | 201 +- .../pc/audio_rtp_receiver_unittest.cc | 15 +- third_party/libwebrtc/pc/channel_unittest.cc | 1 - third_party/libwebrtc/pc/codec_vendor.cc | 902 +++++ third_party/libwebrtc/pc/codec_vendor.h | 123 + .../libwebrtc/pc/codec_vendor_unittest.cc | 105 + .../pc/congestion_control_integrationtest.cc | 62 +- .../pc/data_channel_integrationtest.cc | 708 ++-- .../libwebrtc/pc/data_channel_unittest.cc | 56 +- .../pc/dtls_srtp_transport_integrationtest.cc | 33 +- .../libwebrtc/pc/dtls_transport_unittest.cc | 54 +- .../libwebrtc/pc/dtmf_sender_unittest.cc | 82 +- .../libwebrtc/pc/jsep_session_description.cc | 6 +- .../libwebrtc/pc/jsep_transport_controller.cc | 49 +- .../pc/jsep_transport_controller_unittest.cc | 258 +- third_party/libwebrtc/pc/media_options.cc | 82 + third_party/libwebrtc/pc/media_options.h | 120 + third_party/libwebrtc/pc/media_session.cc | 914 +---- third_party/libwebrtc/pc/media_session.h | 152 +- .../libwebrtc/pc/media_session_unittest.cc | 455 +-- third_party/libwebrtc/pc/peer_connection.cc | 82 +- third_party/libwebrtc/pc/peer_connection.h | 7 +- ...r_connection_adaptation_integrationtest.cc | 30 +- .../pc/peer_connection_bundle_unittest.cc | 146 +- .../pc/peer_connection_crypto_unittest.cc | 34 +- .../peer_connection_data_channel_unittest.cc | 6 +- ...er_connection_encodings_integrationtest.cc | 430 +++ .../pc/peer_connection_end_to_end_unittest.cc | 93 +- .../pc/peer_connection_factory_unittest.cc | 4 +- .../pc/peer_connection_field_trial_tests.cc | 30 +- .../pc/peer_connection_histogram_unittest.cc | 65 +- .../pc/peer_connection_ice_unittest.cc | 127 +- .../pc/peer_connection_integrationtest.cc | 1132 ++++-- .../pc/peer_connection_interface_unittest.cc | 130 +- .../libwebrtc/pc/peer_connection_internal.h | 2 +- .../pc/peer_connection_jsep_unittest.cc | 88 +- .../pc/peer_connection_media_unittest.cc | 143 +- .../pc/peer_connection_rampup_tests.cc | 34 +- .../pc/peer_connection_rtp_unittest.cc | 21 +- .../pc/peer_connection_signaling_unittest.cc | 102 +- .../pc/peer_connection_svc_integrationtest.cc | 35 +- .../libwebrtc/pc/peer_connection_wrapper.cc | 55 +- .../libwebrtc/pc/peer_connection_wrapper.h | 3 + .../pc/rtc_stats_collector_unittest.cc | 69 +- .../libwebrtc/pc/rtc_stats_integrationtest.cc | 26 +- .../pc/rtp_sender_receiver_unittest.cc | 25 +- third_party/libwebrtc/pc/rtp_transceiver.cc | 205 +- third_party/libwebrtc/pc/rtp_transceiver.h | 24 +- .../libwebrtc/pc/rtp_transceiver_unittest.cc | 451 ++- .../libwebrtc/pc/rtp_transport_unittest.cc | 23 +- third_party/libwebrtc/pc/sctp_data_channel.cc | 4 + .../libwebrtc/pc/sctp_transport_unittest.cc | 41 +- .../libwebrtc/pc/sdp_munging_detector.cc | 349 ++ .../libwebrtc/pc/sdp_munging_detector.h | 25 + third_party/libwebrtc/pc/sdp_offer_answer.cc | 214 +- third_party/libwebrtc/pc/sdp_offer_answer.h | 7 + .../libwebrtc/pc/sdp_offer_answer_unittest.cc | 549 +++ third_party/libwebrtc/pc/sdp_utils.cc | 4 +- .../libwebrtc/pc/session_description.cc | 50 +- .../libwebrtc/pc/session_description.h | 39 +- .../slow_peer_connection_integration_test.cc | 144 +- third_party/libwebrtc/pc/srtp_session.cc | 198 +- third_party/libwebrtc/pc/srtp_session.h | 43 +- .../libwebrtc/pc/srtp_session_unittest.cc | 230 +- third_party/libwebrtc/pc/srtp_transport.cc | 101 +- third_party/libwebrtc/pc/srtp_transport.h | 19 +- .../fake_audio_capture_module_unittest.cc | 28 +- .../pc/test/fake_peer_connection_base.h | 2 +- .../pc/test/fake_rtc_certificate_generator.h | 12 +- .../pc/test/integration_test_helpers.h | 115 +- .../pc/test/mock_peer_connection_internal.h | 2 +- .../pc/test/peer_connection_test_wrapper.cc | 73 +- .../pc/test/peer_connection_test_wrapper.h | 9 + third_party/libwebrtc/pc/webrtc_sdp.cc | 4 +- .../libwebrtc/pc/webrtc_sdp_unittest.cc | 11 +- third_party/libwebrtc/rtc_base/BUILD.gn | 24 + .../rtc_base/async_dns_resolver_unittest.cc | 20 +- .../rtc_base/bounded_inline_vector_impl.h | 13 +- third_party/libwebrtc/rtc_base/byte_order.h | 2 +- third_party/libwebrtc/rtc_base/checks.h | 1 + third_party/libwebrtc/rtc_base/gunit.h | 106 - .../libwebrtc/rtc_base/nat_unittest.cc | 13 +- .../libwebrtc/rtc_base/network_unittest.cc | 29 +- .../rtc_base/null_socket_server_unittest.cc | 12 +- .../rtc_base/numerics/sequence_number_util.h | 1 - .../rtc_base/openssl_stream_adapter.cc | 10 +- .../rtc_base/openssl_stream_adapter.h | 3 - .../rtc_base/operations_chain_unittest.cc | 33 +- .../rtc_base/physical_socket_server.cc | 68 +- .../physical_socket_server_unittest.cc | 24 +- .../rtc_certificate_generator_unittest.cc | 26 +- .../libwebrtc/rtc_base/socket_unittest.cc | 198 +- .../libwebrtc/rtc_base/socket_unittest.h | 10 +- .../rtc_base/ssl_adapter_unittest.cc | 52 +- .../rtc_base/ssl_identity_unittest.cc | 6 +- .../rtc_base/ssl_stream_adapter_unittest.cc | 92 +- .../libwebrtc/rtc_base/string_encode.cc | 2 +- .../rtc_base/system/cocoa_threading.mm | 4 +- .../task_utils/repeating_task_unittest.cc | 6 +- third_party/libwebrtc/rtc_base/test_client.cc | 17 +- third_party/libwebrtc/rtc_base/thread.cc | 4 +- .../libwebrtc/rtc_base/thread_unittest.cc | 26 +- .../libwebrtc/rtc_base/untyped_function.h | 5 +- .../rtc_tools/network_tester/BUILD.gn | 2 + .../network_tester/network_tester_unittest.cc | 9 +- .../rtc_event_log_visualizer/analyzer.cc | 2 +- .../rtc_tools/video_encoder/video_encoder.cc | 2 +- .../libwebrtc/rtc_tools/video_replay.cc | 7 +- third_party/libwebrtc/sdk/BUILD.gn | 15 + .../java/org/webrtc/AndroidVideoDecoder.java | 2 +- .../webrtc/MediaCodecVideoDecoderFactory.java | 10 +- third_party/libwebrtc/sdk/media_constraints.h | 4 +- .../api/RTCVideoRendererAdapter+Private.h | 9 +- .../sdk/objc/api/RTCVideoRendererAdapter.mm | 18 +- .../sdk/objc/api/logging/RTCCallbackLogger.h | 4 +- .../sdk/objc/api/logging/RTCCallbackLogger.mm | 26 +- .../peerconnection/RTCAudioSource+Private.h | 18 +- .../objc/api/peerconnection/RTCAudioSource.mm | 19 +- .../peerconnection/RTCAudioTrack+Private.h | 6 +- .../objc/api/peerconnection/RTCAudioTrack.mm | 26 +- .../objc/api/peerconnection/RTCCertificate.h | 9 +- .../objc/api/peerconnection/RTCCertificate.mm | 25 +- .../peerconnection/RTCConfiguration+Private.h | 23 +- .../api/peerconnection/RTCConfiguration.h | 14 +- .../api/peerconnection/RTCConfiguration.mm | 142 +- .../api/peerconnection/RTCCryptoOptions.h | 11 +- .../api/peerconnection/RTCCryptoOptions.mm | 20 +- .../peerconnection/RTCDataChannel+Private.h | 7 +- .../objc/api/peerconnection/RTCDataChannel.h | 6 +- .../objc/api/peerconnection/RTCDataChannel.mm | 52 +- .../RTCDataChannelConfiguration+Private.h | 3 +- .../peerconnection/RTCDtmfSender+Private.h | 3 +- .../objc/api/peerconnection/RTCDtmfSender.h | 16 +- .../objc/api/peerconnection/RTCDtmfSender.mm | 25 +- .../peerconnection/RTCEncodedImage+Private.h | 3 +- .../peerconnection/RTCEncodedImage+Private.mm | 38 +- .../objc/api/peerconnection/RTCFieldTrials.h | 6 +- .../objc/api/peerconnection/RTCFieldTrials.mm | 26 +- .../objc/api/peerconnection/RTCFileLogger.h | 6 +- .../objc/api/peerconnection/RTCFileLogger.mm | 26 +- .../peerconnection/RTCIceCandidate+Private.h | 6 +- .../objc/api/peerconnection/RTCIceCandidate.h | 3 +- .../api/peerconnection/RTCIceCandidate.mm | 14 +- .../RTCIceCandidateErrorEvent+Private.h | 6 +- .../RTCIceCandidateErrorEvent.h | 15 +- .../api/peerconnection/RTCIceServer+Private.h | 6 +- .../objc/api/peerconnection/RTCIceServer.h | 15 +- .../objc/api/peerconnection/RTCIceServer.mm | 47 +- .../RTCLegacyStatsReport+Private.h | 3 +- .../peerconnection/RTCLegacyStatsReport.mm | 22 +- .../RTCMediaConstraints+Private.h | 4 +- .../api/peerconnection/RTCMediaConstraints.h | 9 +- .../api/peerconnection/RTCMediaConstraints.mm | 36 +- .../peerconnection/RTCMediaSource+Private.h | 18 +- .../objc/api/peerconnection/RTCMediaSource.mm | 8 +- .../peerconnection/RTCMediaStream+Private.h | 17 +- .../objc/api/peerconnection/RTCMediaStream.h | 3 +- .../objc/api/peerconnection/RTCMediaStream.mm | 31 +- .../RTCMediaStreamTrack+Private.h | 21 +- .../api/peerconnection/RTCMediaStreamTrack.mm | 65 +- .../sdk/objc/api/peerconnection/RTCMetrics.h | 3 +- .../sdk/objc/api/peerconnection/RTCMetrics.mm | 7 +- .../RTCMetricsSampleInfo+Private.h | 3 +- .../RTCPeerConnection+DataChannel.mm | 8 +- .../RTCPeerConnection+Private.h | 67 +- .../peerconnection/RTCPeerConnection+Stats.mm | 41 +- .../api/peerconnection/RTCPeerConnection.h | 89 +- .../api/peerconnection/RTCPeerConnection.mm | 562 ++- .../RTCPeerConnectionFactory+Native.h | 78 +- .../RTCPeerConnectionFactory+Private.h | 4 +- .../peerconnection/RTCPeerConnectionFactory.h | 67 +- .../RTCPeerConnectionFactory.mm | 251 +- ...nectionFactoryBuilder+DefaultComponents.mm | 3 +- .../RTCPeerConnectionFactoryBuilder.h | 21 +- .../RTCPeerConnectionFactoryBuilder.mm | 21 +- .../RTCPeerConnectionFactoryOptions+Private.h | 3 +- .../RTCPeerConnectionFactoryOptions.mm | 12 +- .../RTCRtcpParameters+Private.h | 4 +- .../api/peerconnection/RTCRtcpParameters.mm | 3 +- .../RTCRtpCapabilities+Private.h | 17 +- .../api/peerconnection/RTCRtpCapabilities.mm | 15 +- .../RTCRtpCodecCapability+Private.h | 15 +- .../peerconnection/RTCRtpCodecCapability.mm | 39 +- .../RTCRtpCodecParameters+Private.h | 3 +- .../peerconnection/RTCRtpCodecParameters.mm | 30 +- .../RTCRtpEncodingParameters+Private.h | 6 +- .../peerconnection/RTCRtpEncodingParameters.h | 3 +- .../RTCRtpEncodingParameters.mm | 17 +- .../RTCRtpHeaderExtension+Private.h | 4 +- .../peerconnection/RTCRtpHeaderExtension.mm | 3 +- .../RTCRtpHeaderExtensionCapability+Private.h | 21 +- .../RTCRtpHeaderExtensionCapability.h | 3 +- .../RTCRtpHeaderExtensionCapability.mm | 31 +- .../peerconnection/RTCRtpParameters+Private.h | 4 +- .../api/peerconnection/RTCRtpParameters.h | 6 +- .../api/peerconnection/RTCRtpParameters.mm | 32 +- .../peerconnection/RTCRtpReceiver+Native.h | 6 +- .../peerconnection/RTCRtpReceiver+Private.h | 13 +- .../objc/api/peerconnection/RTCRtpReceiver.h | 26 +- .../objc/api/peerconnection/RTCRtpReceiver.mm | 39 +- .../api/peerconnection/RTCRtpSender+Native.h | 10 +- .../api/peerconnection/RTCRtpSender+Private.h | 10 +- .../objc/api/peerconnection/RTCRtpSender.h | 3 +- .../objc/api/peerconnection/RTCRtpSender.mm | 39 +- .../objc/api/peerconnection/RTCRtpSource.h | 21 +- .../objc/api/peerconnection/RTCRtpSource.mm | 16 +- .../RTCRtpTransceiver+Private.h | 12 +- .../api/peerconnection/RTCRtpTransceiver.h | 46 +- .../api/peerconnection/RTCRtpTransceiver.mm | 116 +- .../RTCSessionDescription+Private.h | 8 +- .../peerconnection/RTCSessionDescription.h | 3 +- .../peerconnection/RTCSessionDescription.mm | 15 +- .../api/peerconnection/RTCStatisticsReport.h | 11 +- .../api/peerconnection/RTCStatisticsReport.mm | 62 +- .../RTCVideoCodecInfo+Private.h | 3 +- .../RTCVideoCodecInfo+Private.mm | 18 +- .../RTCVideoEncoderSettings+Private.h | 3 +- .../RTCVideoEncoderSettings+Private.mm | 3 +- .../peerconnection/RTCVideoSource+Private.h | 25 +- .../objc/api/peerconnection/RTCVideoSource.mm | 32 +- .../peerconnection/RTCVideoTrack+Private.h | 6 +- .../objc/api/peerconnection/RTCVideoTrack.mm | 40 +- .../RTCNativeVideoDecoderBuilder+Native.h | 3 +- .../api/video_codec/RTCNativeVideoEncoder.mm | 3 +- .../RTCNativeVideoEncoderBuilder+Native.h | 3 +- .../api/video_codec/RTCVideoDecoderAV1.mm | 3 +- .../api/video_codec/RTCVideoDecoderVP8.mm | 3 +- .../api/video_codec/RTCVideoDecoderVP9.mm | 3 +- .../api/video_codec/RTCVideoEncoderAV1.mm | 13 +- .../api/video_codec/RTCVideoEncoderVP8.mm | 13 +- .../api/video_codec/RTCVideoEncoderVP9.mm | 6 +- .../RTCNativeI420Buffer+Private.h | 3 +- .../video_frame_buffer/RTCNativeI420Buffer.mm | 25 +- .../libwebrtc/sdk/objc/base/RTCLogging.h | 12 +- .../libwebrtc/sdk/objc/base/RTCMacros.h | 3 +- .../sdk/objc/base/RTCMutableI420Buffer.h | 3 +- .../sdk/objc/base/RTCVideoCapturer.h | 6 +- .../sdk/objc/base/RTCVideoCapturer.m | 3 +- .../sdk/objc/base/RTCVideoCodecInfo.h | 9 +- .../sdk/objc/base/RTCVideoCodecInfo.m | 9 +- .../sdk/objc/base/RTCVideoDecoderFactory.h | 3 +- .../libwebrtc/sdk/objc/base/RTCVideoEncoder.h | 23 +- .../sdk/objc/base/RTCVideoEncoderFactory.h | 18 +- .../sdk/objc/base/RTCVideoEncoderFactory.mm | 3 +- .../objc/base/RTCVideoEncoderQpThresholds.h | 3 +- .../libwebrtc/sdk/objc/base/RTCVideoFrame.h | 3 +- .../sdk/objc/base/RTCVideoRenderer.h | 3 +- .../objc/components/audio/RTCAudioDevice.h | 155 +- .../audio/RTCAudioSession+Configuration.mm | 35 +- .../audio/RTCAudioSession+Private.h | 13 +- .../objc/components/audio/RTCAudioSession.h | 68 +- .../objc/components/audio/RTCAudioSession.mm | 143 +- .../audio/RTCAudioSessionConfiguration.h | 3 +- .../audio/RTCAudioSessionConfiguration.m | 6 +- .../RTCNativeAudioSessionDelegateAdapter.h | 6 +- .../RTCNativeAudioSessionDelegateAdapter.mm | 15 +- .../capturer/RTCCameraVideoCapturer.h | 18 +- .../capturer/RTCCameraVideoCapturer.m | 189 +- .../capturer/RTCFileVideoCapturer.m | 84 +- .../components/network/RTCNetworkMonitor.mm | 36 +- .../renderer/metal/RTCMTLI420Renderer.mm | 39 +- .../renderer/metal/RTCMTLNSVideoView.m | 3 +- .../renderer/metal/RTCMTLNV12Renderer.mm | 62 +- .../renderer/metal/RTCMTLRGBRenderer.mm | 54 +- .../renderer/metal/RTCMTLRenderer+Private.h | 3 +- .../renderer/metal/RTCMTLRenderer.h | 7 +- .../renderer/metal/RTCMTLRenderer.mm | 131 +- .../renderer/metal/RTCMTLVideoView.m | 28 +- .../renderer/opengl/RTCDefaultShader.h | 5 +- .../renderer/opengl/RTCDefaultShader.mm | 72 +- .../renderer/opengl/RTCEAGLVideoView.m | 35 +- .../renderer/opengl/RTCI420TextureCache.h | 3 +- .../renderer/opengl/RTCI420TextureCache.mm | 11 +- .../renderer/opengl/RTCNV12TextureCache.h | 3 +- .../renderer/opengl/RTCNV12TextureCache.m | 38 +- .../components/renderer/opengl/RTCShader.mm | 64 +- .../renderer/opengl/RTCVideoViewShading.h | 10 +- .../video_codec/RTCCodecSpecificInfoH264.h | 5 +- .../RTCDefaultVideoDecoderFactory.h | 6 +- .../RTCDefaultVideoDecoderFactory.m | 24 +- .../RTCDefaultVideoEncoderFactory.h | 6 +- .../RTCDefaultVideoEncoderFactory.m | 27 +- .../video_codec/RTCH264ProfileLevelId.h | 3 +- .../video_codec/RTCH264ProfileLevelId.mm | 26 +- .../video_codec/RTCVideoDecoderFactoryH264.m | 16 +- .../video_codec/RTCVideoDecoderH264.mm | 78 +- .../video_codec/RTCVideoEncoderFactoryH264.m | 16 +- .../video_codec/RTCVideoEncoderH264.mm | 256 +- .../video_codec/UIDevice+H264Profile.mm | 153 +- .../video_frame_buffer/RTCCVPixelBuffer.h | 6 +- .../video_frame_buffer/RTCCVPixelBuffer.mm | 97 +- .../helpers/AVCaptureSession+DevicePosition.h | 3 +- .../AVCaptureSession+DevicePosition.mm | 8 +- .../sdk/objc/helpers/NSString+StdString.mm | 2 +- .../sdk/objc/helpers/RTCCameraPreviewView.m | 32 +- .../sdk/objc/helpers/RTCDispatcher+Private.h | 3 +- .../sdk/objc/helpers/RTCDispatcher.h | 3 +- .../sdk/objc/helpers/RTCDispatcher.m | 16 +- .../sdk/objc/native/api/audio_device_module.h | 10 + .../objc/native/api/audio_device_module.mm | 32 +- .../api/audio_device_module_error_handler.h | 54 + .../native/api/network_monitor_factory.mm | 2 +- .../native/api/ssl_certificate_verifier.mm | 10 +- .../objc/native/src/audio/audio_device_ios.h | 15 +- .../objc/native/src/audio/audio_device_ios.mm | 199 +- .../src/audio/audio_device_module_ios.h | 6 +- .../src/audio/audio_device_module_ios.mm | 1308 +++---- .../sdk/objc/native/src/audio/helpers.mm | 8 +- .../src/audio/voice_processing_audio_unit.mm | 155 +- .../sdk/objc/native/src/objc_audio_device.h | 104 +- .../sdk/objc/native/src/objc_audio_device.mm | 174 +- .../native/src/objc_audio_device_delegate.h | 11 +- .../native/src/objc_audio_device_delegate.mm | 94 +- .../sdk/objc/native/src/objc_frame_buffer.mm | 52 +- .../objc/native/src/objc_network_monitor.mm | 10 +- .../native/src/objc_video_decoder_factory.mm | 45 +- .../native/src/objc_video_encoder_factory.h | 5 +- .../native/src/objc_video_encoder_factory.mm | 124 +- .../sdk/objc/native/src/objc_video_frame.mm | 9 +- .../objc/native/src/objc_video_renderer.mm | 3 +- .../objc/native/src/objc_video_track_source.h | 3 +- .../native/src/objc_video_track_source.mm | 39 +- .../unittests/ObjCVideoTrackSource_xctest.mm | 389 ++- .../unittests/RTCAudioDeviceModule_xctest.mm | 149 +- .../objc/unittests/RTCAudioDevice_xctest.mm | 73 +- .../sdk/objc/unittests/RTCAudioSessionTest.mm | 111 +- .../objc/unittests/RTCCVPixelBuffer_xctest.mm | 340 +- .../objc/unittests/RTCCallbackLogger_xctest.m | 113 +- .../unittests/RTCCameraVideoCapturerTests.mm | 308 +- .../sdk/objc/unittests/RTCCertificateTest.mm | 37 +- .../objc/unittests/RTCConfigurationTest.mm | 62 +- ...NotPutCPlusPlusInFrameworkHeaders_xctest.m | 6 +- .../objc/unittests/RTCEncodedImage_xctest.mm | 16 +- .../unittests/RTCFileVideoCapturer_xctest.mm | 56 +- .../unittests/RTCH264ProfileLevelId_xctest.m | 12 +- .../sdk/objc/unittests/RTCIceCandidateTest.mm | 7 +- .../sdk/objc/unittests/RTCIceServerTest.mm | 63 +- .../objc/unittests/RTCMTLVideoView_xctest.m | 72 +- .../objc/unittests/RTCMediaConstraintsTest.mm | 18 +- .../unittests/RTCNV12TextureCache_xctest.m | 3 +- .../RTCPeerConnectionFactoryBuilderTest.mm | 22 +- .../RTCPeerConnectionFactory_xctest.m | 363 +- .../objc/unittests/RTCPeerConnectionTest.mm | 96 +- .../unittests/RTCSessionDescriptionTest.mm | 26 +- .../sdk/objc/unittests/RTCTracingTest.mm | 5 +- .../sdk/objc/unittests/avformatmappertests.mm | 26 +- .../objc/unittests/frame_buffer_helpers.mm | 67 +- .../objc/unittests/nalu_rewriter_xctest.mm | 118 +- .../objc_video_decoder_factory_tests.mm | 33 +- .../objc_video_encoder_factory_tests.mm | 102 +- .../objc/unittests/scoped_cftyperef_tests.mm | 3 +- .../libwebrtc/stats/rtc_stats_unittest.cc | 6 + .../system_wrappers/include/metrics.h | 4 - third_party/libwebrtc/test/DEPS | 1 + third_party/libwebrtc/test/fake_decoder.cc | 3 +- third_party/libwebrtc/test/fake_decoder.h | 6 +- third_party/libwebrtc/test/fake_encoder.cc | 2 +- third_party/libwebrtc/test/fake_vp8_decoder.h | 3 +- third_party/libwebrtc/test/fuzzers/BUILD.gn | 5 +- .../libwebrtc/test/fuzzers/aec3_fuzzer.cc | 3 +- ...ceive_side_congestion_controller_fuzzer.cc | 3 +- .../test/fuzzers/rtp_packet_fuzzer.cc | 2 +- .../test/fuzzers/sdp_integration_fuzzer.cc | 10 +- .../test/fuzzers/stun_parser_fuzzer.cc | 1 - .../libwebrtc/test/ios/coverage_util_ios.mm | 10 +- .../libwebrtc/test/ios/google_test_runner.mm | 3 +- .../libwebrtc/test/ios/test_support.mm | 51 +- third_party/libwebrtc/test/mac/run_test.mm | 15 +- .../libwebrtc/test/mac/video_renderer_mac.mm | 29 +- third_party/libwebrtc/test/mac_capturer.mm | 21 +- third_party/libwebrtc/test/network/BUILD.gn | 25 + .../network/network_emulation_pc_unittest.cc | 59 +- .../network/network_emulation_unittest.cc | 56 +- .../video/default_video_quality_analyzer.cc | 6 +- ...t_video_quality_analyzer_frame_in_flight.h | 3 +- .../peer_scenario/peer_scenario_client.cc | 64 +- .../test/testsupport/ios_file_utils.mm | 8 +- .../time_controller/simulated_task_queue.cc | 2 +- .../libwebrtc/test/video_codec_tester.cc | 16 +- .../libwebrtc/test/wait_until_unittest.cc | 4 +- .../tools_webrtc/android/build_aar.py | 2 - .../libwebrtc/tools_webrtc/matlab/maxUnwrap.m | 1 + .../libwebrtc/tools_webrtc/matlab/parseLog.m | 1 + .../tools_webrtc/matlab/rtpAnalyze.m | 1 + third_party/libwebrtc/tools_webrtc/mb/mb.py | 28 +- .../libwebrtc/tools_webrtc/mb/mb_config.pyl | 15 - .../libwebrtc/tools_webrtc/mb/mb_unittest.py | 44 +- third_party/libwebrtc/video/BUILD.gn | 13 +- third_party/libwebrtc/video/config/BUILD.gn | 1 + .../video/config/encoder_stream_factory.cc | 3 +- .../libwebrtc/video/config/simulcast.cc | 9 +- .../video/decode_synchronizer_unittest.cc | 2 +- .../libwebrtc/video/frame_dumping_encoder.cc | 4 +- third_party/libwebrtc/video/g3doc/stats.md | 57 +- .../video/receive_statistics_proxy.cc | 39 +- .../video/rtp_video_stream_receiver2.cc | 2 +- .../libwebrtc/video/video_receive_stream2.cc | 69 +- .../video/video_receive_stream2_unittest.cc | 17 +- ...video_stream_buffer_controller_unittest.cc | 9 +- .../libwebrtc/video/video_stream_encoder.cc | 73 +- .../video/video_stream_encoder_unittest.cc | 123 +- third_party/libwebrtc/webrtc.gni | 34 +- .../contentanalysis/ContentAnalysis.cpp | 18 +- .../components/extensions/ext-toolkit.json | 9 - toolkit/components/extensions/jar.mn | 2 + .../extensions/parent/ext-webRequest.js | 41 +- toolkit/components/extensions/schemas/jar.mn | 2 + ...file_webRequest_handlerBehaviorChanged.css | 4 + ...ile_webRequest_handlerBehaviorChanged.html | 12 + .../file_webRequest_handlerBehaviorChanged.js | 3 + ...file_webRequest_handlerBehaviorChanged.png | Bin 0 -> 69 bytes .../test/xpcshell/test_ext_permissions.js | 42 +- ...t_ext_webRequest_handlerBehaviorChanged.js | 150 + .../test/xpcshell/xpcshell-common.toml | 6 +- .../extensions/test/xpcshell/xpcshell.toml | 2 + .../webrequest/WebNavigationContent.cpp | 6 +- .../components/formautofill/Constants.ios.mjs | 2 + .../formautofill/FormAutofill.sys.mjs | 30 + .../formautofill/FormAutofillChild.sys.mjs | 252 +- .../formautofill/FormAutofillParent.sys.mjs | 92 +- .../shared/FormAutofillHandler.sys.mjs | 256 +- .../shared/FormAutofillHeuristics.sys.mjs | 5 +- .../shared/FormAutofillUtils.sys.mjs | 22 +- .../shared/FormStateManager.sys.mjs | 17 +- .../formautofill/shared/LabelUtils.sys.mjs | 145 +- toolkit/components/glean/ipc/FOGIPC.cpp | 28 +- .../browser/browser_ml_smart_tab_perf.js | 8 +- .../ml/tests/browser/shared-head.js | 4 +- toolkit/components/pdfjs/PdfJsDefaultPrefs.js | 1 + .../components/pdfjs/content/build/pdf.mjs | 10 +- .../pdfjs/content/build/pdf.scripting.mjs | 4 +- .../pdfjs/content/build/pdf.worker.mjs | 261 +- .../pdfjs/content/web/viewer-geckoview.css | 19 +- .../pdfjs/content/web/viewer-geckoview.mjs | 63 +- .../components/pdfjs/content/web/viewer.css | 40 +- .../components/pdfjs/content/web/viewer.mjs | 63 +- .../pdfjs/content/web/wasm/openjpeg.wasm | Bin 243150 -> 254625 bytes toolkit/components/pdfjs/moz.yaml | 4 +- .../tests/unit/test_frecency_interactions.js | 56 + toolkit/components/reader/moz.yaml | 4 +- .../reader/readability/JSDOMParser.js | 763 ++-- .../readability/Readability-readerable.js | 30 +- .../reader/readability/Readability.js | 1466 +++++--- .../neterror/supportpages/time-errors.html | 2 +- toolkit/docs/index.rst | 1 + toolkit/docs/internal-urls.md | 138 + .../locales-preview/localModelManagement.ftl | 11 + .../contentanalysis/contentanalysis.ftl | 2 +- toolkit/locales/jar.mn | 1 + toolkit/moz.configure | 2 +- .../extensions/content/aboutaddons.css | 3 + .../extensions/content/aboutaddons.html | 10 + .../mozapps/extensions/content/aboutaddons.js | 10 +- .../mozapps/extensions/extensions.manifest | 1 + .../internal/ModelHubProvider.sys.mjs | 88 + toolkit/mozapps/extensions/internal/moz.build | 1 + .../extensions/test/browser/browser.toml | 2 + .../browser/browser_html_mlmodel_addons.js | 81 + .../test/xpcshell/test_ModelHubProvider.js | 45 + .../extensions/test/xpcshell/xpcshell.toml | 6 + .../browser_components_search_pings.d.ts | 10 + tools/@types/lib.gecko.glean.d.ts | 1 + tools/@types/lib.gecko.tweaks.d.ts | 8 + tools/@types/tsconfig.json | 6 +- .../lib/configs/browser-test.js | 1 + .../lib/configs/chrome-test.js | 1 + .../lib/configs/general-test.js | 1 + .../lib/configs/mochitest-test.js | 1 + .../lib/configs/recommended.js | 1 + .../lib/configs/require-jsdoc.js | 1 + .../lib/configs/valid-jsdoc.js | 1 + .../lib/configs/xpcshell-test.js | 1 + .../eslint-plugin-mozilla/lib/globals.js | 6 +- .../eslint-plugin-mozilla/lib/helpers.js | 34 - .../eslint/eslint-plugin-mozilla/lib/index.js | 9 + .../rules/import-browser-window-globals.js | 4 +- .../lib/rules/import-content-task-globals.js | 36 +- .../lib/rules/import-headjs-globals.js | 4 +- .../lib/rules/mark-test-function-used.js | 8 +- .../rules/no-redeclare-with-import-autofix.js | 2 +- .../lib/rules/no-throw-cr-literal.js | 2 +- .../lib/rules/no-useless-parameters.js | 2 +- .../lib/rules/no-useless-run-test.js | 2 +- .../lib/rules/prefer-boolean-length-check.js | 2 +- .../lib/rules/reject-addtask-only.js | 2 +- .../rules/reject-importGlobalProperties.js | 2 +- .../lib/rules/reject-mixing-eager-and-lazy.js | 2 +- .../lib/rules/reject-top-level-await.js | 6 +- .../lib/rules/use-chromeutils-generateqi.js | 3 +- .../lib/rules/use-isInstance.js | 9 +- .../lib/rules/use-services.js | 2 +- .../lib/rules/use-static-import.js | 6 +- .../lib/rules/valid-lazy.js | 2 +- .../lib/rules/var-only-at-top-level.js | 4 +- .../eslint-plugin-mozilla/package-lock.json | 1834 ++-------- .../eslint/eslint-plugin-mozilla/package.json | 15 +- tools/lint/pipelint | 5 +- tools/ts/build_glean.js | 43 +- tools/ts/mach_commands.py | 2 +- tools/use-moz-src/mach_commands.py | 270 ++ view/nsView.cpp | 23 +- widget/gtk/MozContainerWayland.cpp | 12 +- widget/gtk/MozContainerWayland.h | 3 - widget/gtk/WaylandSurface.cpp | 47 +- widget/gtk/WaylandSurface.h | 17 +- widget/gtk/WindowSurfaceCairo.cpp | 97 + widget/gtk/WindowSurfaceCairo.h | 38 + widget/gtk/WindowSurfaceProvider.cpp | 22 +- widget/gtk/WindowSurfaceX11SHM.h | 3 + widget/gtk/moz.build | 1 + widget/gtk/nsWindow.cpp | 240 +- widget/gtk/nsWindow.h | 9 + widget/nsBaseWidget.cpp | 17 + widget/nsIWidget.h | 22 +- 1745 files changed, 51573 insertions(+), 30355 deletions(-) create mode 100644 browser/components/newtab/AboutHomeStartupCache.sys.mjs create mode 100644 browser/components/search/SERPCategorization.sys.mjs create mode 100644 browser/components/taskbartabs/TaskbarTabUI.sys.mjs create mode 100644 browser/components/taskbartabs/moz.build create mode 100644 browser/components/taskbartabs/test/browser/browser.toml create mode 100644 browser/components/taskbartabs/test/browser/browser_taskbarTabs_chromeTest.js create mode 100644 browser/extensions/formautofill/test/browser/browser_autofill_address_select_match_isoid.js create mode 100644 browser/extensions/formautofill/test/browser/browser_dynamic_form_change_detection.js create mode 100644 browser/extensions/formautofill/test/browser/browser_fill_on_dynamic_form_change_detection.js create mode 100644 browser/extensions/formautofill/test/fixtures/dynamic_form_changes.html create mode 100644 browser/extensions/formautofill/test/fixtures/dynamic_formless_changes_element_visiblity_state.html create mode 100644 browser/extensions/formautofill/test/fixtures/dynamic_formless_changes_node_mutations.html create mode 100644 browser/extensions/formautofill/test/fixtures/form_change_on_user_interaction.html create mode 100644 devtools/client/netmonitor/test/browser_net_requests_with_empty_response.js rename dom/base/{ZLibHelper.h => CompressionStreamHelper.h} (68%) create mode 100644 dom/media/webaudio/AudioWorklet.cpp create mode 100644 dom/media/webaudio/AudioWorklet.h create mode 100644 js/src/jit-test/tests/arrays/species-optimize-intrinsic.js create mode 100644 js/src/jit-test/tests/fuses/species-fuse-1.js create mode 100644 js/src/jit-test/tests/fuses/species-fuse-2.js create mode 100644 js/src/jit-test/tests/fuses/species-fuse-3.js create mode 100644 media/libpng/loongarch/filter_lsx_intrinsics.c create mode 100644 media/libpng/loongarch/loongarch_lsx_init.c create mode 100644 media/libvpx/libvpx/vpx_dsp/x86/sad_avx512.c delete mode 100644 mobile/android/fenix/app/src/main/java/org/mozilla/fenix/downloads/listscreen/DownloadFragmentStore.kt create mode 100644 mobile/android/fenix/app/src/main/java/org/mozilla/fenix/downloads/listscreen/FileItemToIconMapper.kt rename mobile/android/fenix/app/src/main/java/org/mozilla/fenix/downloads/listscreen/{DownloadFragmentDataMiddleware.kt => middleware/DownloadUIMapperMiddleware.kt} (61%) create mode 100644 mobile/android/fenix/app/src/main/java/org/mozilla/fenix/downloads/listscreen/store/DownloadUIAction.kt create mode 100644 mobile/android/fenix/app/src/main/java/org/mozilla/fenix/downloads/listscreen/store/DownloadUIState.kt create mode 100644 mobile/android/fenix/app/src/main/java/org/mozilla/fenix/downloads/listscreen/store/DownloadUIStore.kt rename mobile/android/fenix/app/src/main/java/org/mozilla/fenix/downloads/listscreen/{DownloadItem.kt => store/FileItem.kt} (62%) delete mode 100644 mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/DownloadItem.kt create mode 100644 mobile/android/fenix/app/src/main/java/org/mozilla/fenix/webcompat/BrokenSiteReporterTestTags.kt create mode 100644 mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/metrics/AdjustMetricsServiceTest.kt rename mobile/android/fenix/app/src/test/java/org/mozilla/fenix/downloads/listscreen/{DownloadFragmentDataMiddlewareTest.kt => DownloadUIMapperMiddlewareTest.kt} (89%) rename mobile/android/fenix/app/src/test/java/org/mozilla/fenix/downloads/listscreen/{DownloadFragmentStoreTest.kt => DownloadUIStoreTest.kt} (55%) create mode 100644 mobile/android/fenix/app/src/test/java/org/mozilla/fenix/downloads/listscreen/FileItemToIconMapperTest.kt rename mobile/android/fenix/app/src/test/java/org/mozilla/fenix/downloads/listscreen/{DownloadItemTest.kt => middleware/FileExistsTest.kt} (87%) delete mode 100644 mobile/android/fenix/app/src/test/java/org/mozilla/fenix/ext/DownloadItemKtTest.kt create mode 100644 mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ui/theme/Modifier.kt delete mode 100644 mobile/android/focus-android/app/src/main/res/raw/about.html create mode 100644 netwerk/protocol/res/MozSrcProtocolHandler.cpp create mode 100644 netwerk/protocol/res/MozSrcProtocolHandler.h create mode 100644 python/mozbuild/mozbuild/test/frontend/data/moz-src-files/dir/file1.txt create mode 100644 python/mozbuild/mozbuild/test/frontend/data/moz-src-files/dir/file2.txt create mode 100644 python/mozbuild/mozbuild/test/frontend/data/moz-src-files/dir/subdir/otherfile.txt create mode 100644 python/mozbuild/mozbuild/test/frontend/data/moz-src-files/file.txt create mode 100644 python/mozbuild/mozbuild/test/frontend/data/moz-src-files/moz.build create mode 100644 srcdir-resolver.js delete mode 100644 testing/web-platform/meta/content-security-policy/connect-src/connect-src-json-import-allowed.sub.html.ini delete mode 100644 testing/web-platform/meta/content-security-policy/connect-src/connect-src-json-import-blocked.sub.html.ini create mode 100644 testing/web-platform/meta/css/css-borders/tentative/corner-shape/corner-shape-fill-any.html.ini create mode 100644 testing/web-platform/meta/css/css-content/quotes-lang-dynamic-001.html.ini create mode 100644 testing/web-platform/meta/css/css-flexbox/flex-basis-013.html.ini create mode 100644 testing/web-platform/meta/css/css-inline/text-box-trim/new-fc-001.html.ini create mode 100644 testing/web-platform/meta/css/css-inline/text-box-trim/text-box-trim-multicol-013.html.ini create mode 100644 testing/web-platform/meta/css/css-overflow/scroll-buttons-appearance.html.ini create mode 100644 testing/web-platform/meta/css/css-overflow/scroll-marker-focus-visible.html.ini create mode 100644 testing/web-platform/meta/css/css-values/if-supports-quirks.html.ini delete mode 100644 testing/web-platform/meta/fetch/metadata/generated/script-json-module-import-static.https.sub.html.ini delete mode 100644 testing/web-platform/meta/fetch/metadata/generated/script-json-module-import-static.sub.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-no-child-bad-subdomain.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-no-child-yes-port.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-no-child-yes-same.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-no-child-yes-subdomain-with-redirect.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-no-child-yes-subdomain.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-no-child-yeswithparams-subdomain.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-no-port.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-no-same.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-no-subdomain.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-yes-port.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-yes-same.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/1-iframe/parent-yes-child-yes-subdomain.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-no-child1-no-subdomain-child2-yes-subdomain.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-no-child1-no-subdomain-child2-yes-subdomainport.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-no-child1-no-subdomain1-child2-yes-subdomain2.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-no-child1-yes-subdomain-child2-no-port.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-no-child1-yes-subdomain-child2-no-subdomain.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-no-subdomain.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-no-subdomain2.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-yes-subdomain.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-yes-subdomain2.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-no-subdomain-child2-yes-subdomainport.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-no-port.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-no-subdomain.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-yes-subdomain.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-yes-subdomain2.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/2-iframes/parent-yes-child1-yes-subdomain-child2-yes-subdomainport.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/about-blank.https.sub.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/document-domain.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/cross-origin-isolated.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/csp-sandbox-no.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/csp-sandbox-yes.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/data-to-javascript-no.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/data-to-javascript-yes.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/data-url-no.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/data-url-yes.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/javascript-url-no.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/javascript-url-yes.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/removed-iframe.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/sandboxed-iframe-no.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/sandboxed-iframe-yes.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/sandboxed-same-origin-iframe-no.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/getter-special-cases/sandboxed-same-origin-iframe-yes.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/going-back.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-no-1-no-same-2-yes-port.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-no-1-no-same-2-yes-subdomain.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-no-1-no-subdomain-2-yes-subdomain.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-no-1-no-subdomain-2-yes-subdomain2.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-no-1-subdomain-yes-2-subdomain2-no.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-no-1-yes-subdomain-2-no-subdomain.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-yes-1-no-same-2-no-port.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/iframe-navigation/parent-yes-1-no-same-2-no-subdomain.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/insecure-http.sub.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-no-openee-yes-port.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-no-openee-yes-same.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-no-openee-yes-subdomain.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-no-port.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-no-same.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-no-subdomain.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-yes-port.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-yes-same.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/popups/opener-yes-openee-yes-subdomain.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/browsers/origin/origin-keyed-agent-clusters/removing-iframes.sub.https.html.ini delete mode 100644 testing/web-platform/meta/html/semantics/scripting-1/the-script-element/css-module/__dir__.ini delete mode 100644 testing/web-platform/meta/html/semantics/scripting-1/the-script-element/import-attributes/__dir__.ini delete mode 100644 testing/web-platform/meta/html/semantics/scripting-1/the-script-element/json-module/__dir__.ini delete mode 100644 testing/web-platform/meta/html/semantics/scripting-1/the-script-element/module/dynamic-import/microtasks/css-import-in-worker.any.js.ini delete mode 100644 testing/web-platform/meta/html/semantics/scripting-1/the-script-element/module/dynamic-import/microtasks/with-import-assertions.any.js.ini delete mode 100644 testing/web-platform/meta/mst-content-hint/RTCRtpSendParameters-degradationPreference.html.ini create mode 100644 testing/web-platform/meta/webdriver/tests/bidi/web_extension/uninstall/invalid.py.ini create mode 100644 testing/web-platform/mozilla/meta/compression/zstd/__dir__.ini create mode 100644 testing/web-platform/mozilla/tests/compression/zstd/compression-constructor.tentative.any.js create mode 100644 testing/web-platform/mozilla/tests/compression/zstd/decompression-bad-chunks.tentative.any.js create mode 100644 testing/web-platform/mozilla/tests/compression/zstd/decompression-buffersource.tentative.any.js create mode 100644 testing/web-platform/mozilla/tests/compression/zstd/decompression-constructor.tentative.any.js create mode 100644 testing/web-platform/mozilla/tests/compression/zstd/decompression-correct-input.tentative.any.js create mode 100644 testing/web-platform/mozilla/tests/compression/zstd/decompression-corrupt-input.tentative.any.js create mode 100644 testing/web-platform/mozilla/tests/compression/zstd/decompression-empty-input.tentative.any.js create mode 100644 testing/web-platform/mozilla/tests/compression/zstd/decompression-split-chunk.tentative.any.js create mode 100644 testing/web-platform/mozilla/tests/compression/zstd/decompression-uint8array-output.tentative.any.js create mode 100644 testing/web-platform/tests/css/css-borders/tentative/corner-shape/corner-shape-fill-any-ref.html create mode 100644 testing/web-platform/tests/css/css-borders/tentative/corner-shape/corner-shape-fill-any.html create mode 100644 testing/web-platform/tests/css/css-borders/tentative/corner-shape/resources/corner-math.js create mode 100644 testing/web-platform/tests/css/css-borders/tentative/corner-shape/resources/resolve-corner-style.js create mode 100644 testing/web-platform/tests/css/css-content/quotes-lang-dynamic-001.html create mode 100644 testing/web-platform/tests/css/css-content/reference/quotes-lang-dynamic-001-ref.html create mode 100644 testing/web-platform/tests/css/css-flexbox/flex-basis-013.html create mode 100644 testing/web-platform/tests/css/css-inline/text-box-trim/new-fc-001.html create mode 100644 testing/web-platform/tests/css/css-inline/text-box-trim/text-box-trim-multicol-013.html create mode 100644 testing/web-platform/tests/css/css-overflow/scroll-buttons-appearance-ref.html create mode 100644 testing/web-platform/tests/css/css-overflow/scroll-buttons-appearance.html create mode 100644 testing/web-platform/tests/css/css-overflow/scroll-marker-focus-visible.html create mode 100644 testing/web-platform/tests/css/css-scoping/svg-id-ref-001.html create mode 100644 testing/web-platform/tests/css/css-values/if-supports-quirks.html create mode 100644 testing/web-platform/tests/css/css-writing-modes/orthogonal-child-with-border.html create mode 100644 testing/web-platform/tests/custom-elements/revamped-scoped-registry/polymer-polyfill-regression.tentative.html create mode 100644 testing/web-platform/tests/fledge/tentative/utf8-helpers.https.window.js create mode 100644 testing/web-platform/tests/html/semantics/interactive-elements/the-dialog-element/dialog-open-pseudo-invalidation-ref.html create mode 100644 testing/web-platform/tests/html/semantics/interactive-elements/the-dialog-element/dialog-open-pseudo-invalidation.html create mode 100644 testing/web-platform/tests/html/semantics/permission-element/quirks-mode-no-height-is-still-bounded-ref.html create mode 100644 testing/web-platform/tests/html/semantics/permission-element/quirks-mode-no-height-is-still-bounded.tentative.html create mode 100644 testing/web-platform/tests/html/semantics/the-button-element/interest-target/interesttarget-plain-inline-element.tentative.html create mode 100644 testing/web-platform/tests/infrastructure/testharness/full.stop/full-stop.html create mode 100644 testing/web-platform/tests/service-workers/service-worker/detached-register-crash.https.html create mode 100644 testing/web-platform/tests/storage-access-api/resources/sandboxed-iframe-allow-storage-access.html create mode 100644 testing/web-platform/tests/storage-access-api/resources/sandboxed-iframe-no-storage-access.html create mode 100644 testing/web-platform/tests/webdriver/tests/support/webextensions/chrome/unpacked/manifest.json rename third_party/libwebrtc/{common_video => api/transport/rtp}/corruption_detection_message.h (96%) rename third_party/libwebrtc/{common_video => api/transport/rtp}/corruption_detection_message_gn/moz.build (100%) rename third_party/libwebrtc/{common_video => api/transport/rtp}/corruption_detection_message_unittest.cc (98%) delete mode 100644 third_party/libwebrtc/call/create_call.cc create mode 100644 third_party/libwebrtc/common_audio/third_party/.clang-format create mode 100644 third_party/libwebrtc/examples/objc/AppRTCMobile/third_party/.clang-format create mode 100644 third_party/libwebrtc/modules/third_party/.clang-format create mode 100644 third_party/libwebrtc/moz-patch-stack/a705b2d73a.no-op-cherry-pick-msg create mode 100644 third_party/libwebrtc/p2p/dtls/dtls_ice_integrationtest.cc create mode 100644 third_party/libwebrtc/pc/codec_vendor.cc create mode 100644 third_party/libwebrtc/pc/codec_vendor.h create mode 100644 third_party/libwebrtc/pc/codec_vendor_unittest.cc create mode 100644 third_party/libwebrtc/pc/media_options.cc create mode 100644 third_party/libwebrtc/pc/media_options.h create mode 100644 third_party/libwebrtc/pc/sdp_munging_detector.cc create mode 100644 third_party/libwebrtc/pc/sdp_munging_detector.h create mode 100644 third_party/libwebrtc/sdk/objc/native/api/audio_device_module_error_handler.h create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_webRequest_handlerBehaviorChanged.css create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_webRequest_handlerBehaviorChanged.html create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_webRequest_handlerBehaviorChanged.js create mode 100644 toolkit/components/extensions/test/xpcshell/data/file_webRequest_handlerBehaviorChanged.png create mode 100644 toolkit/components/extensions/test/xpcshell/test_ext_webRequest_handlerBehaviorChanged.js create mode 100644 toolkit/docs/internal-urls.md create mode 100644 toolkit/locales-preview/localModelManagement.ftl create mode 100644 toolkit/mozapps/extensions/internal/ModelHubProvider.sys.mjs create mode 100644 toolkit/mozapps/extensions/test/browser/browser_html_mlmodel_addons.js create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_ModelHubProvider.js create mode 100644 tools/@types/glean/browser_components_search_pings.d.ts create mode 100644 tools/use-moz-src/mach_commands.py create mode 100644 widget/gtk/WindowSurfaceCairo.cpp create mode 100644 widget/gtk/WindowSurfaceCairo.h diff --git a/CLOBBER b/CLOBBER index de426ee2dd1..73cd5a4e937 100644 --- a/CLOBBER +++ b/CLOBBER @@ -22,4 +22,4 @@ # changes to stick? As of bug 928195, this shouldn't be necessary! Please # don't change CLOBBER for WebIDL changes any more. -Merge day clobber 2025-03-03 \ No newline at end of file +Modified build files in third_party/libwebrtc - Bug 1948685 - Vendor libwebrtc from 39da6f3a75 diff --git a/accessible/base/NotificationController.cpp b/accessible/base/NotificationController.cpp index ee0a324f8bd..69c8f2e70d3 100644 --- a/accessible/base/NotificationController.cpp +++ b/accessible/base/NotificationController.cpp @@ -816,9 +816,14 @@ void NotificationController::WillRefresh(mozilla::TimeStamp aTime) { 0, UINT32_MAX, nsIFrame::TextOffsetType::OffsetsInContentText, nsIFrame::TrailingWhitespace::DontTrim); - // Remove text accessible if rendered text is empty. if (textAcc) { - if (text.mString.IsEmpty()) { + // Remove the TextLeafAccessible if: + // 1. The rendered text is empty; or + // 2. The text is just a space, but its layout frame has a width of 0, + // so it isn't visible. This can happen if there is whitespace before an + // invisible element at the end of a block. + if (text.mString.IsEmpty() || + (text.mString.EqualsLiteral(" ") && textFrame->GetRect().IsEmpty())) { #ifdef A11Y_LOG if (logging::IsEnabled(logging::eTree | logging::eText)) { logging::MsgBegin("TREE", "text node lost its content; doc: %p", diff --git a/accessible/base/nsAccessibilityService.cpp b/accessible/base/nsAccessibilityService.cpp index 27fb435c219..af00d0adadc 100644 --- a/accessible/base/nsAccessibilityService.cpp +++ b/accessible/base/nsAccessibilityService.cpp @@ -593,21 +593,34 @@ void nsAccessibilityService::FireAccessibleEvent(uint32_t aEvent, void nsAccessibilityService::NotifyOfPossibleBoundsChange( mozilla::PresShell* aPresShell, nsIContent* aContent) { + if (!aContent || (!IPCAccessibilityActive() && !aContent->IsText())) { + return; + } + DocAccessible* document = aPresShell->GetDocAccessible(); + if (!document) { + return; + } + LocalAccessible* accessible = document->GetAccessible(aContent); + if (!accessible && aContent == document->GetContent()) { + // DocAccessible::GetAccessible() won't return the document if a root + // element like body is passed. In that case we need the doc accessible + // itself. + accessible = document; + } + if (!accessible) { + return; + } if (IPCAccessibilityActive()) { - DocAccessible* document = aPresShell->GetDocAccessible(); - if (document) { - LocalAccessible* accessible = document->GetAccessible(aContent); - if (!accessible && aContent == document->GetContent()) { - // DocAccessible::GetAccessible() won't return the document if a root - // element like body is passed. In that case we need the doc accessible - // itself. - accessible = document; - } - - if (accessible) { - document->QueueCacheUpdate(accessible, CacheDomain::Bounds); - } - } + document->QueueCacheUpdate(accessible, CacheDomain::Bounds); + } + if (accessible->IsTextLeaf() && + accessible->AsTextLeaf()->Text().EqualsLiteral(" ")) { + // This space might be becoming invisible, even though it still has a frame. + // In this case, the frame will have 0 width. Unfortunately, we can't check + // the frame width here because layout isn't ready yet, so we need to defer + // this until the refresh driver tick. + MOZ_ASSERT(aContent->IsText()); + document->UpdateText(aContent); } } @@ -1293,6 +1306,7 @@ LocalAccessible* nsAccessibilityService::CreateAccessible( // Ignore not rendered text nodes and whitespace text nodes between table // cells. if (text.mString.IsEmpty() || + (text.mString.EqualsLiteral(" ") && frame->GetRect().IsEmpty()) || (aContext->IsTableRow() && nsCoreUtils::IsWhitespaceString(text.mString))) { if (aIsSubtreeHidden) *aIsSubtreeHidden = true; diff --git a/accessible/generic/DocAccessible.cpp b/accessible/generic/DocAccessible.cpp index cf157a988b1..008c67ae4df 100644 --- a/accessible/generic/DocAccessible.cpp +++ b/accessible/generic/DocAccessible.cpp @@ -240,6 +240,18 @@ uint64_t DocAccessible::NativeState() const { RefPtr editorBase = GetEditor(); state |= editorBase ? states::EDITABLE : states::READONLY; + // GetFrame() returns the root frame, which is normally what we want. However, + // user-select: none might be set on the body, in which case this won't be + // exposed on the root frame. Therefore, we explicitly use the body frame + // here (if any). + nsIFrame* bodyFrame = mContent ? mContent->GetPrimaryFrame() : nullptr; + if ((state & states::EDITABLE) || + (bodyFrame && bodyFrame->IsSelectable(nullptr))) { + // If the accessible is editable the layout selectable state only disables + // mouse selection, but keyboard (shift+arrow) selection is still possible. + state |= states::SELECTABLE_TEXT; + } + return state; } diff --git a/accessible/ipc/RemoteAccessible.cpp b/accessible/ipc/RemoteAccessible.cpp index 4e70762c1e2..e4e62597de7 100644 --- a/accessible/ipc/RemoteAccessible.cpp +++ b/accessible/ipc/RemoteAccessible.cpp @@ -1209,20 +1209,36 @@ Relation RemoteAccessible::RelationByType(RelationType aType) const { // the cached relations need to take precedence. For example, a
with // both aria-labelledby and a
must return two LABELLED_BY // targets: the aria-labelledby and then the
. - if (aType == RelationType::LABELLED_BY && TagName() == nsGkAtoms::figure) { + auto AddChildWithTag = [this, &rel](nsAtom* aTarget) { uint32_t count = ChildCount(); for (uint32_t c = 0; c < count; ++c) { RemoteAccessible* child = RemoteChildAt(c); MOZ_ASSERT(child); - if (child->TagName() == nsGkAtoms::figcaption) { + if (child->TagName() == aTarget) { rel.AppendTarget(child); } } - } else if (aType == RelationType::LABEL_FOR && - TagName() == nsGkAtoms::figcaption) { - if (RemoteAccessible* parent = RemoteParent()) { - if (parent->TagName() == nsGkAtoms::figure) { - rel.AppendTarget(parent); + }; + if (aType == RelationType::LABELLED_BY) { + auto tag = TagName(); + if (tag == nsGkAtoms::figure) { + AddChildWithTag(nsGkAtoms::figcaption); + } else if (tag == nsGkAtoms::fieldset) { + AddChildWithTag(nsGkAtoms::legend); + } + } else if (aType == RelationType::LABEL_FOR) { + auto tag = TagName(); + if (tag == nsGkAtoms::figcaption) { + if (RemoteAccessible* parent = RemoteParent()) { + if (parent->TagName() == nsGkAtoms::figure) { + rel.AppendTarget(parent); + } + } + } else if (tag == nsGkAtoms::legend) { + if (RemoteAccessible* parent = RemoteParent()) { + if (parent->TagName() == nsGkAtoms::fieldset) { + rel.AppendTarget(parent); + } } } } diff --git a/accessible/tests/browser/e10s/browser_caching_relations.js b/accessible/tests/browser/e10s/browser_caching_relations.js index cc839308304..11083bc4937 100644 --- a/accessible/tests/browser/e10s/browser_caching_relations.js +++ b/accessible/tests/browser/e10s/browser_caching_relations.js @@ -289,3 +289,22 @@ addAccessibleTask( }, { topLevel: true, chrome: true } ); + +/* + * Test relation caching for LABELLED_BY and LABEL_FOR with legend/fieldset. + */ +addAccessibleTask( + ` +
+ legend + inner content +
`, + async function testFieldsetLegendLabels(browser, accDoc) { + const fs = findAccessibleChildByID(accDoc, "fs"); + const leg = findAccessibleChildByID(accDoc, "leg"); + + await testCachedRelation(fs, RELATION_LABELLED_BY, leg); + await testCachedRelation(leg, RELATION_LABEL_FOR, fs); + }, + { chrome: true, iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_caching_states.js b/accessible/tests/browser/e10s/browser_caching_states.js index 2d7a8141670..37bd936c248 100644 --- a/accessible/tests/browser/e10s/browser_caching_states.js +++ b/accessible/tests/browser/e10s/browser_caching_states.js @@ -863,3 +863,43 @@ addAccessibleTask( }, { chrome: true, topLevel: true } ); + +/** + * Test the selectable text state. + */ +addAccessibleTask( + ` +

selectableP

+

unselectableP

+ `, + async function testSelectableText(browser, docAcc) { + testStates(docAcc, 0, EXT_STATE_SELECTABLE_TEXT); + const selectableP = findAccessibleChildByID(docAcc, "selectableP"); + testStates(selectableP, 0, EXT_STATE_SELECTABLE_TEXT); + const unselectableP = findAccessibleChildByID(docAcc, "unselectableP"); + testStates(unselectableP, 0, 0, 0, EXT_STATE_SELECTABLE_TEXT); + }, + { chrome: true, topLevel: true } +); + +/** + * Test the selectable text state on an unselectable body. + */ +addAccessibleTask( + ` + +

p

`, async function testTextSupportedTextSelection() { let result = await runPython(` @@ -1411,9 +1412,20 @@ body { SupportedTextSelection_Multiple, "input SupportedTextSelection correct" ); - // The IA2 -> UIA bridge doesn't understand that text isn't selectable in - // this document. if (gIsUiaEnabled) { + // The IA2 -> UIA proxy doesn't expose the Text pattern on this text leaf. + is( + await runPython(` + p = findUiaByDomId(doc, "p") + pLeaf = uiaClient.RawViewWalker.GetFirstChildElement(p) + text = getUiaPattern(pLeaf, "Text") + return text.SupportedTextSelection + `), + SupportedTextSelection_None, + "pLeaf SupportedTextSelection correct" + ); + // The IA2 -> UIA proxy doesn't understand that text isn't selectable in + // this document. is( await runPython(`getUiaPattern(doc, "Text").SupportedTextSelection`), SupportedTextSelection_None, @@ -1423,6 +1435,39 @@ body { } ); +/** + * Test the Text pattern's SupportedTextSelection property on a document with a + * selectable body. + */ +addUiaTask( + `

p

`, + async function testTextSupportedTextSelectionSelectableBody() { + is( + await runPython(` + global doc + doc = getDocUia() + text = getUiaPattern(doc, "Text") + return text.SupportedTextSelection + `), + SupportedTextSelection_Multiple, + "doc SupportedTextSelection correct" + ); + // The IA2 -> UIA proxy doesn't expose the Text pattern on this text leaf. + if (gIsUiaEnabled) { + is( + await runPython(` + p = findUiaByDomId(doc, "p") + pLeaf = uiaClient.RawViewWalker.GetFirstChildElement(p) + text = getUiaPattern(pLeaf, "Text") + return text.SupportedTextSelection + `), + SupportedTextSelection_Multiple, + "pLeaf SupportedTextSelection correct" + ); + } + } +); + /** * Test the Text pattern's GetSelection method with the caret. */ diff --git a/accessible/tests/mochitest/treeupdate/test_delayed_removal.html b/accessible/tests/mochitest/treeupdate/test_delayed_removal.html index 2ba1e31203d..f32172ba571 100644 --- a/accessible/tests/mochitest/treeupdate/test_delayed_removal.html +++ b/accessible/tests/mochitest/treeupdate/test_delayed_removal.html @@ -144,7 +144,6 @@ testAccessibleTree("c7",{ SECTION: [ { role: ROLE_PUSHBUTTON, name: "Hello" }, - { TEXT_LEAF: [] } ] }); let events = waitForOrderedEvents( diff --git a/accessible/windows/uia/UiaText.cpp b/accessible/windows/uia/UiaText.cpp index 16db2424baf..33292b13435 100644 --- a/accessible/windows/uia/UiaText.cpp +++ b/accessible/windows/uia/UiaText.cpp @@ -165,7 +165,12 @@ UiaText::get_SupportedTextSelection( if (!acc) { return CO_E_OBJNOTCONNECTED; } - if (acc->State() & states::SELECTABLE_TEXT) { + if (!acc->IsHyperText()) { + // Currently, the SELECTABLE_TEXT state is only exposed on HyperText + // Accessibles. + acc = acc->Parent(); + } + if (acc && acc->State() & states::SELECTABLE_TEXT) { *aRetVal = SupportedTextSelection_Multiple; } else { *aRetVal = SupportedTextSelection_None; diff --git a/browser/base/content/browser-init.js b/browser/base/content/browser-init.js index 3d517331581..b65d7da2502 100644 --- a/browser/base/content/browser-init.js +++ b/browser/base/content/browser-init.js @@ -101,6 +101,17 @@ var gBrowserInit = { ); toolbarMenubar.setAttribute("data-l10n-attrs", "toolbarname"); } + // If opening a Taskbar Tab window, add an attribute to the top-level element + // to inform window styling. + if (window.arguments && window.arguments[1]) { + let extraOptions = window.arguments[1]; + if ( + extraOptions instanceof Ci.nsIWritablePropertyBag2 && + extraOptions.hasKey("taskbartab") + ) { + window.document.documentElement.setAttribute("taskbartab", ""); + } + } // Run menubar initialization first, to avoid CustomTitlebar code picking // up mutations from it and causing a reflow. @@ -221,7 +232,10 @@ var gBrowserInit = { // have been initialized. Services.obs.notifyObservers(window, "browser-window-before-show"); - if (!window.toolbar.visible) { + if ( + !window.toolbar.visible || + window.document.documentElement.hasAttribute("taskbartab") + ) { // adjust browser UI for popups gURLBar.readOnly = true; } @@ -231,6 +245,7 @@ var gBrowserInit = { Win10TabletModeUpdater.init(); CombinedStopReload.ensureInitialized(); gPrivateBrowsingUI.init(); + TaskbarTabUI.init(window); BrowserPageActions.init(); if (gToolbarKeyNavEnabled) { ToolbarKeyboardNavigator.init(); diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index b89846a1103..3bca566e499 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -46,11 +46,13 @@ ChromeUtils.defineESModuleGetters(this, { LoginManagerParent: "resource://gre/modules/LoginManagerParent.sys.mjs", MigrationUtils: "resource:///modules/MigrationUtils.sys.mjs", NetUtil: "resource://gre/modules/NetUtil.sys.mjs", - NewTabPagePreloading: "resource:///modules/NewTabPagePreloading.sys.mjs", + NewTabPagePreloading: + "moz-src:///browser/components/tabbrowser/NewTabPagePreloading.sys.mjs", NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs", NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", nsContextMenu: "chrome://browser/content/nsContextMenu.sys.mjs", - OpenInTabsUtils: "resource:///modules/OpenInTabsUtils.sys.mjs", + OpenInTabsUtils: + "moz-src:///browser/components/tabbrowser/OpenInTabsUtils.sys.mjs", OpenSearchManager: "resource:///modules/OpenSearchManager.sys.mjs", PageActions: "resource:///modules/PageActions.sys.mjs", PageThumbs: "resource://gre/modules/PageThumbs.sys.mjs", @@ -84,6 +86,7 @@ ChromeUtils.defineESModuleGetters(this, { TabCrashHandler: "resource:///modules/ContentCrashHandlers.sys.mjs", TabsSetupFlowManager: "resource:///modules/firefox-view-tabs-setup-manager.sys.mjs", + TaskbarTabUI: "resource:///modules/TaskbarTabUI.sys.mjs", TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.sys.mjs", ToolbarContextMenu: "resource:///modules/ToolbarContextMenu.sys.mjs", TranslationsParent: "resource://gre/actors/TranslationsParent.sys.mjs", @@ -799,6 +802,7 @@ function updateFxaToolbarMenu(enable, isInitialUpdate = false) { const mainWindowEl = document.documentElement; const fxaPanelEl = PanelMultiView.getViewNode(document, "PanelUI-fxa"); + const taskbarTab = mainWindowEl.hasAttribute("taskbartab"); // To minimize the toolbar button flickering or appearing/disappearing during startup, // we use this pref to anticipate the likely FxA status. @@ -813,7 +817,7 @@ function updateFxaToolbarMenu(enable, isInitialUpdate = false) { fxaPanelEl.addEventListener("ViewShowing", gSync.updateSendToDeviceTitle); - if (enable && syncEnabled) { + if (enable && syncEnabled && !taskbarTab) { mainWindowEl.setAttribute("fxatoolbarmenu", "visible"); // We have to manually update the sync state UI when toggling the FxA toolbar diff --git a/browser/base/content/browser.js.globals b/browser/base/content/browser.js.globals index 9e6a0f1587a..7491fca5357 100644 --- a/browser/base/content/browser.js.globals +++ b/browser/base/content/browser.js.globals @@ -159,6 +159,7 @@ "SubDialogManager", "TabCrashHandler", "TabsSetupFlowManager", + "TaskbarTabUI", "TelemetryEnvironment", "TranslationsParent", "UITour", diff --git a/browser/base/content/test/static/browser_all_files_referenced.js b/browser/base/content/test/static/browser_all_files_referenced.js index f51ac363738..52f696eda55 100644 --- a/browser/base/content/test/static/browser_all_files_referenced.js +++ b/browser/base/content/test/static/browser_all_files_referenced.js @@ -430,7 +430,6 @@ var gChromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].getService( ); var gChromeMap = new Map(); var gOverrideMap = new Map(); -var gComponentsSet = new Set(); // In this map when the value is a Set of URLs, the file is referenced if any // of the files in the Set is referenced. @@ -505,8 +504,6 @@ function parseManifest(manifestUri) { } } else if (type == "resource") { trackResourcePrefix(argv[0]); - } else if (type == "component") { - gComponentsSet.add(argv[1]); } } }); @@ -647,6 +644,10 @@ function parseCodeFile(fileUri) { /["'`]chrome:\/\/[a-zA-Z0-9-]+\/(content|skin|locale)\/[^"'` ]*["'`]/g ); + if (!urls) { + urls = line.match(/["']moz-src:\/\/\/[^"']+["']/g); + } + if (!urls) { urls = line.match(/["']resource:\/\/[^"']+["']/g); if ( @@ -734,7 +735,10 @@ function parseCodeFile(fileUri) { if (!/\.(properties|js|jsm|mjs|json|css)$/.test(url)) { url += ".js"; } - if (url.startsWith("resource://")) { + if ( + url.startsWith("resource://") || + url.startsWith("moz-src:///") + ) { addCodeReference(url, fileUri); } else { // if we end up with a chrome:// url here, it's likely because @@ -784,13 +788,22 @@ function parseCodeFile(fileUri) { function convertToCodeURI(fileUri) { let baseUri = fileUri; let path = ""; - while (true) { + while (baseUri) { let slashPos = baseUri.lastIndexOf("/", baseUri.length - 2); if (slashPos <= 0) { // File not accessible from chrome protocol, try resource:// for (let res of gResourceMap) { if (fileUri.startsWith(res[1])) { - return fileUri.replace(res[1], "resource://" + res[0] + "/"); + let resourceUriString = fileUri.replace( + res[1], + `resource://${res[0]}/` + ); + // If inside moz-src, treat as moz-src url. + resourceUriString = resourceUriString.replace( + /^resource:\/\/gre\/moz-src\//, + "moz-src:///" + ); + return resourceUriString; } } // Give up and return the original URL. @@ -802,6 +815,7 @@ function convertToCodeURI(fileUri) { return gChromeMap.get(baseUri) + path; } } + throw new Error(`Unparsable URI: ${fileUri}`); } async function chromeFileExists(aURI) { @@ -849,6 +863,7 @@ function findChromeUrlsFromArray(array, prefix) { // Only keep strings that look like real chrome or resource urls. if ( /chrome:\/\/[a-zA-Z09-]+\/(content|skin|locale)\//.test(string) || + /moz-src:\/\/\/\w+/.test(string) || /resource:\/\/[a-zA-Z09-]*\/.*\.[a-z]+/.test(string) ) { gReferencesFromCode.set(string, null); @@ -862,10 +877,12 @@ add_task(async function checkAllTheFiles() { const libxul = await IOUtils.read(PathUtils.xulLibraryPath); findChromeUrlsFromArray(libxul, "chrome://"); findChromeUrlsFromArray(libxul, "resource://"); + findChromeUrlsFromArray(libxul, "moz-src:///"); // Handle NS_LITERAL_STRING. let uint16 = new Uint16Array(libxul.buffer); findChromeUrlsFromArray(uint16, "chrome://"); findChromeUrlsFromArray(uint16, "resource://"); + findChromeUrlsFromArray(uint16, "moz-src:///"); const kCodeExtensions = [ ".xml", @@ -954,6 +971,7 @@ add_task(async function checkAllTheFiles() { // the non-devtools paths: let devtoolsPrefixes = [ "chrome://devtools", + "moz-src:///devtools/", "resource://devtools/", "resource://devtools-shared-images/", "resource://devtools-highlighter-styles/", @@ -968,7 +986,9 @@ add_task(async function checkAllTheFiles() { for (let uri of uris) { uri = convertToCodeURI(uri.spec); if ( - (uri.startsWith("chrome://") || uri.startsWith("resource://")) && + (uri.startsWith("chrome://") || + uri.startsWith("resource://") || + uri.startsWith("moz-src:///")) && isDevtools == hasDevtoolsPrefix(uri) ) { chromeFiles.push(uri); @@ -1025,9 +1045,6 @@ add_task(async function checkAllTheFiles() { if (rv && f.startsWith("resource://app/")) { rv = isUnreferenced(f.replace("resource://app/", "resource:///")); } - if (rv && /^resource:\/\/(?:app|gre)\/components\/[^/]+\.js$/.test(f)) { - rv = !gComponentsSet.has(f.replace(/.*\//, "")); - } if (!rv) { foundReference = true; if (useAllowlist) { @@ -1108,7 +1125,9 @@ add_task(async function checkAllTheFiles() { } if ( - (file.startsWith("chrome://") || file.startsWith("resource://")) && + (file.startsWith("chrome://") || + file.startsWith("resource://") || + file.startsWith("moz-src:///")) && !(await chromeFileExists(file)) ) { // Ignore chrome prefixes that have been automatically expanded. diff --git a/browser/components/BrowserComponents.manifest b/browser/components/BrowserComponents.manifest index 8bdb12e99fe..805c196248d 100644 --- a/browser/components/BrowserComponents.manifest +++ b/browser/components/BrowserComponents.manifest @@ -9,6 +9,17 @@ category app-startup nsBrowserGlue @mozilla.org/browser/browserglue;1 application={ec8030f7-c20a-464f-9b0e-13a3a9e97384} application={aa3c5121-dab2-40e2-81ca-7ea25febc110} +# Browser global components initializing before UI startup +category browser-before-ui-startup resource:///modules/sessionstore/SessionStore.sys.mjs SessionStore.init +category browser-before-ui-startup resource:///modules/BuiltInThemes.sys.mjs BuiltInThemes.maybeInstallActiveBuiltInTheme +#ifdef MOZ_NORMANDY +category browser-before-ui-startup resource://normandy/Normandy.sys.mjs Normandy.init +#endif +category browser-before-ui-startup chrome://pocket/content/SaveToPocket.sys.mjs SaveToPocket.init +category browser-before-ui-startup resource:///modules/ResetPBMPanel.sys.mjs ResetPBMPanel.init +category browser-before-ui-startup resource:///modules/AboutHomeStartupCache.sys.mjs AboutHomeStartupCache.init +category browser-before-ui-startup resource:///modules/AccountsGlue.sys.mjs AccountsGlue.init + # Browser window lifecycle consumers category browser-window-domcontentloaded-before-tabbrowser resource:///modules/BrowserDOMWindow.sys.mjs BrowserDOMWindow.setupInWindow category browser-window-domcontentloaded resource:///modules/BrowserWindowTracker.sys.mjs BrowserWindowTracker.track @@ -20,19 +31,19 @@ category browser-window-delayed-startup resource:///modules/ReportBrokenSite.sys category browser-window-delayed-startup resource:///modules/SearchUIUtils.sys.mjs SearchUIUtils.init category browser-window-unload resource:///modules/BrowserDOMWindow.sys.mjs BrowserDOMWindow.teardownInWindow -category browser-window-unload resource:///modules/NewTabPagePreloading.sys.mjs NewTabPagePreloading.removePreloadedBrowser +category browser-window-unload moz-src:///browser/components/tabbrowser/NewTabPagePreloading.sys.mjs NewTabPagePreloading.removePreloadedBrowser # App startup consumers category browser-idle-startup resource:///modules/PlacesUIUtils.sys.mjs PlacesUIUtils.unblockToolbars category browser-idle-startup resource:///modules/BuiltInThemes.sys.mjs BuiltInThemes.ensureBuiltInThemes category browser-idle-startup resource://gre/modules/RFPHelper.sys.mjs RFPHelper.init category browser-idle-startup resource://gre/modules/Blocklist.sys.mjs Blocklist.loadBlocklistAsync -category browser-idle-startup resource:///modules/TabUnloader.sys.mjs TabUnloader.init +category browser-idle-startup moz-src:///browser/components/tabbrowser/TabUnloader.sys.mjs TabUnloader.init category browser-idle-startup resource:///modules/GenAI.sys.mjs GenAI.init category browser-idle-startup resource:///modules/QuickSuggest.sys.mjs QuickSuggest.init category browser-idle-startup resource:///modules/UrlbarSearchTermsPersistence.sys.mjs UrlbarSearchTermsPersistence.init category browser-idle-startup resource:///modules/ShoppingUtils.sys.mjs ShoppingUtils.init -category browser-idle-startup resource:///modules/SearchSERPTelemetry.sys.mjs SearchSERPCategorization.init +category browser-idle-startup resource:///modules/SERPCategorization.sys.mjs SERPCategorization.init category browser-idle-startup resource://gre/modules/ContentRelevancyManager.sys.mjs ContentRelevancyManager.init #ifdef MOZ_UPDATER category browser-idle-startup resource://gre/modules/UpdateListener.sys.mjs UpdateListener.maybeShowUnsupportedNotification @@ -51,6 +62,8 @@ category browser-quit-application-granted resource://normandy/Normandy.sys.mjs N category browser-quit-application-granted resource://gre/modules/RFPHelper.sys.mjs RFPHelper.uninit category browser-quit-application-granted resource:///modules/ShoppingUtils.sys.mjs ShoppingUtils.uninit category browser-quit-application-granted resource:///modules/asrouter/ASRouterNewTabHook.sys.mjs ASRouterNewTabHook.destroy +category browser-quit-application-granted resource:///modules/SERPCategorization.sys.mjs SERPCategorization.uninit +category browser-quit-application-granted resource:///modules/SearchSERPTelemetry.sys.mjs SearchSERPTelemetry.uninit #ifdef MOZ_UPDATER category browser-quit-application-granted resource://gre/modules/UpdateListener.sys.mjs UpdateListener.reset #endif diff --git a/browser/components/BrowserGlue.sys.mjs b/browser/components/BrowserGlue.sys.mjs index 0d7a65ffff6..042ea1576e0 100644 --- a/browser/components/BrowserGlue.sys.mjs +++ b/browser/components/BrowserGlue.sys.mjs @@ -8,8 +8,8 @@ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { + AboutHomeStartupCache: "resource:///modules/AboutHomeStartupCache.sys.mjs", AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs", - AccountsGlue: "resource:///modules/AccountsGlue.sys.mjs", AWToolbarButton: "resource:///modules/aboutwelcome/AWToolbarUtils.sys.mjs", ASRouter: "resource:///modules/asrouter/ASRouter.sys.mjs", ASRouterDefaultConfig: @@ -25,19 +25,16 @@ ChromeUtils.defineESModuleGetters(lazy, { BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.sys.mjs", BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", - BuiltInThemes: "resource:///modules/BuiltInThemes.sys.mjs", CaptchaDetectionPingUtils: "resource://gre/modules/CaptchaDetectionPingUtils.sys.mjs", CommonDialog: "resource://gre/modules/CommonDialog.sys.mjs", ContextualIdentityService: "resource://gre/modules/ContextualIdentityService.sys.mjs", DAPTelemetrySender: "resource://gre/modules/DAPTelemetrySender.sys.mjs", - DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs", Discovery: "resource:///modules/Discovery.sys.mjs", DoHController: "resource:///modules/DoHController.sys.mjs", DownloadsViewableInternally: "resource:///modules/DownloadsViewableInternally.sys.mjs", - E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs", ExtensionsUI: "resource:///modules/ExtensionsUI.sys.mjs", FeatureGate: "resource://featuregates/FeatureGate.sys.mjs", FirefoxBridgeExtensionUtils: @@ -46,16 +43,13 @@ ChromeUtils.defineESModuleGetters(lazy, { // eslint-disable-next-line mozilla/valid-lazy FilePickerCrashed: "resource:///modules/FilePickerCrashed.sys.mjs", FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs", - HomePage: "resource:///modules/HomePage.sys.mjs", Integration: "resource://gre/modules/Integration.sys.mjs", Interactions: "resource:///modules/Interactions.sys.mjs", LoginBreaches: "resource:///modules/LoginBreaches.sys.mjs", LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs", MigrationUtils: "resource:///modules/MigrationUtils.sys.mjs", - NetUtil: "resource://gre/modules/NetUtil.sys.mjs", NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs", NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", - Normandy: "resource://normandy/Normandy.sys.mjs", OnboardingMessageProvider: "resource:///modules/asrouter/OnboardingMessageProvider.sys.mjs", OsEnvironment: "resource://gre/modules/OsEnvironment.sys.mjs", @@ -76,13 +70,10 @@ ChromeUtils.defineESModuleGetters(lazy, { RemoteSecuritySettings: "resource://gre/modules/psm/RemoteSecuritySettings.sys.mjs", RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", - ResetPBMPanel: "resource:///modules/ResetPBMPanel.sys.mjs", SafeBrowsing: "resource://gre/modules/SafeBrowsing.sys.mjs", Sanitizer: "resource:///modules/Sanitizer.sys.mjs", SandboxUtils: "resource://gre/modules/SandboxUtils.sys.mjs", - SaveToPocket: "chrome://pocket/content/SaveToPocket.sys.mjs", ScreenshotsUtils: "resource:///modules/ScreenshotsUtils.sys.mjs", - SearchSERPCategorization: "resource:///modules/SearchSERPTelemetry.sys.mjs", SearchSERPTelemetry: "resource:///modules/SearchSERPTelemetry.sys.mjs", SelectableProfileService: "resource:///modules/profiles/SelectableProfileService.sys.mjs", @@ -104,7 +95,6 @@ ChromeUtils.defineESModuleGetters(lazy, { WindowsLaunchOnLogin: "resource://gre/modules/WindowsLaunchOnLogin.sys.mjs", WindowsRegistry: "resource://gre/modules/WindowsRegistry.sys.mjs", WindowsGPOParser: "resource://gre/modules/policies/WindowsGPOParser.sys.mjs", - clearTimeout: "resource://gre/modules/Timer.sys.mjs", setTimeout: "resource://gre/modules/Timer.sys.mjs", }); @@ -1389,7 +1379,7 @@ BrowserGlue.prototype = { // AboutHomeStartupCache might write to the cache during // quit-application-granted, so we defer uninitialization // until here. - AboutHomeStartupCache.uninit(); + lazy.AboutHomeStartupCache.uninit(); if (this._bookmarksBackupIdleTime) { this._userIdleService.removeIdleObserver( @@ -1503,21 +1493,9 @@ BrowserGlue.prototype = { listeners.init(); - lazy.SessionStore.init(); - - lazy.BuiltInThemes.maybeInstallActiveBuiltInTheme(); - - if (AppConstants.MOZ_NORMANDY) { - lazy.Normandy.init(); - } - - lazy.SaveToPocket.init(); - - lazy.ResetPBMPanel.init(); - - AboutHomeStartupCache.init(); - - lazy.AccountsGlue.init(); + lazy.BrowserUtils.callModulesFromCategory({ + categoryName: "browser-before-ui-startup", + }); Services.obs.notifyObservers(null, "browser-ui-startup-complete"); }, @@ -2219,11 +2197,6 @@ BrowserGlue.prototype = { } }, - // These should also be moved to use the category manager, but ran into - // leaking issues. Bug 1949294 tracks. - () => lazy.SearchSERPTelemetry.uninit(), - () => lazy.SearchSERPCategorization.uninit(), - () => { // bug 1839426 - The FOG service needs to be instantiated reliably so it // can perform at-shutdown tasks later in shutdown. @@ -5539,893 +5512,3 @@ export var DefaultBrowserCheck = { return willPrompt; }, }; - -/** - * AboutHomeStartupCache is responsible for reading and writing the - * initial about:home document from the HTTP cache as a startup - * performance optimization. It only works when the "privileged about - * content process" is enabled and when ENABLED_PREF is set to true. - * - * See https://firefox-source-docs.mozilla.org/browser/extensions/newtab/docs/v2-system-addon/about_home_startup_cache.html - * for further details. - */ -export var AboutHomeStartupCache = { - ABOUT_HOME_URI_STRING: "about:home", - SCRIPT_EXTENSION: "script", - ENABLED_PREF: "browser.startup.homepage.abouthome_cache.enabled", - PRELOADED_NEWTAB_PREF: "browser.newtab.preload", - LOG_LEVEL_PREF: "browser.startup.homepage.abouthome_cache.loglevel", - - // It's possible that the layout of about:home will change such that - // we want to invalidate any pre-existing caches. We do this by setting - // this meta key in the nsICacheEntry for the page. - // - // The version is currently set to the build ID, meaning that the cache - // is invalidated after every upgrade (like the main startup cache). - CACHE_VERSION_META_KEY: "version", - - LOG_NAME: "AboutHomeStartupCache", - - // These messages are used to request the "privileged about content process" - // to create the cached document, and then to receive that document. - CACHE_REQUEST_MESSAGE: "AboutHomeStartupCache:CacheRequest", - CACHE_RESPONSE_MESSAGE: "AboutHomeStartupCache:CacheResponse", - CACHE_USAGE_RESULT_MESSAGE: "AboutHomeStartupCache:UsageResult", - - // When a "privileged about content process" is launched, this message is - // sent to give it some nsIInputStream's for the about:home document they - // should load. - SEND_STREAMS_MESSAGE: "AboutHomeStartupCache:InputStreams", - - // This time in ms is used to debounce messages that are broadcast to - // all about:newtab's, or the preloaded about:newtab. We use those - // messages as a signal that it's likely time to refresh the cache. - CACHE_DEBOUNCE_RATE_MS: 5000, - - // This is how long we'll block the AsyncShutdown while waiting for - // the cache to write. If we fail to write within that time, we will - // allow the shutdown to proceed. - SHUTDOWN_CACHE_WRITE_TIMEOUT_MS: 1000, - - // The following values are as possible values for the - // browser.startup.abouthome_cache_result scalar. Keep these in sync with the - // scalar definition in Scalars.yaml and the matching Glean metric in - // browser/components/metrics.yaml. See setDeferredResult for more - // information. - CACHE_RESULT_SCALARS: { - UNSET: 0, - DOES_NOT_EXIST: 1, - CORRUPT_PAGE: 2, - CORRUPT_SCRIPT: 3, - INVALIDATED: 4, - LATE: 5, - VALID_AND_USED: 6, - DISABLED: 7, - NOT_LOADING_ABOUTHOME: 8, - PRELOADING_DISABLED: 9, - }, - - // This will be set to one of the values of CACHE_RESULT_SCALARS - // once it is determined which result best suits what occurred. - _cacheDeferredResultScalar: -1, - - // A reference to the nsICacheEntry to read from and write to. - _cacheEntry: null, - - // These nsIPipe's are sent down to the "privileged about content process" - // immediately after the process launches. This allows us to race the loading - // of the cache entry in the parent process with the load of the about:home - // page in the content process, since we'll connect the InputStream's to - // the pipes as soon as the nsICacheEntry is available. - // - // The page pipe is for the HTML markup for the page. - _pagePipe: null, - // The script pipe is for the JavaScript that the HTML markup loads - // to set its internal state. - _scriptPipe: null, - _cacheDeferred: null, - - _enabled: false, - _initted: false, - _hasWrittenThisSession: false, - _finalized: false, - _firstPrivilegedProcessCreated: false, - - init() { - if (this._initted) { - throw new Error("AboutHomeStartupCache already initted."); - } - - this.setDeferredResult(this.CACHE_RESULT_SCALARS.UNSET); - - this._enabled = Services.prefs.getBoolPref( - "browser.startup.homepage.abouthome_cache.enabled" - ); - - if (!this._enabled) { - this.recordResult(this.CACHE_RESULT_SCALARS.DISABLED); - return; - } - - this.log = console.createInstance({ - prefix: this.LOG_NAME, - maxLogLevelPref: this.LOG_LEVEL_PREF, - }); - - this.log.trace("Initting."); - - // If the user is not configured to load about:home at startup, then - // let's not bother with the cache - loading it needlessly is more likely - // to hinder what we're actually trying to load. - let willLoadAboutHome = - !lazy.HomePage.overridden && - Services.prefs.getIntPref("browser.startup.page") === 1; - - if (!willLoadAboutHome) { - this.log.trace("Not configured to load about:home by default."); - this.recordResult(this.CACHE_RESULT_SCALARS.NOT_LOADING_ABOUTHOME); - return; - } - - if (!Services.prefs.getBoolPref(this.PRELOADED_NEWTAB_PREF, false)) { - this.log.trace("Preloaded about:newtab disabled."); - this.recordResult(this.CACHE_RESULT_SCALARS.PRELOADING_DISABLED); - return; - } - - Services.obs.addObserver(this, "ipc:content-created"); - Services.obs.addObserver(this, "process-type-set"); - Services.obs.addObserver(this, "ipc:content-shutdown"); - Services.obs.addObserver(this, "intl:app-locales-changed"); - - this.log.trace("Constructing pipes."); - this._pagePipe = this.makePipe(); - this._scriptPipe = this.makePipe(); - - this._cacheEntryPromise = new Promise(resolve => { - this._cacheEntryResolver = resolve; - }); - - let lci = Services.loadContextInfo.default; - let storage = Services.cache2.diskCacheStorage(lci); - try { - storage.asyncOpenURI( - this.aboutHomeURI, - "", - Ci.nsICacheStorage.OPEN_PRIORITY, - this - ); - } catch (e) { - this.log.error("Failed to open about:home cache entry", e); - } - - this._cacheTask = new lazy.DeferredTask(async () => { - await this.cacheNow(); - }, this.CACHE_DEBOUNCE_RATE_MS); - - lazy.AsyncShutdown.quitApplicationGranted.addBlocker( - "AboutHomeStartupCache: Writing cache", - async () => { - await this.onShutdown(); - }, - () => this._cacheProgress - ); - - this._cacheDeferred = null; - this._initted = true; - this.log.trace("Initialized."); - }, - - get initted() { - return this._initted; - }, - - uninit() { - if (!this._enabled) { - return; - } - - try { - Services.obs.removeObserver(this, "ipc:content-created"); - Services.obs.removeObserver(this, "process-type-set"); - Services.obs.removeObserver(this, "ipc:content-shutdown"); - Services.obs.removeObserver(this, "intl:app-locales-changed"); - } catch (e) { - // If we failed to initialize and register for these observer - // notifications, then attempting to remove them will throw. - // It's fine to ignore that case on shutdown. - } - - if (this._cacheTask) { - this._cacheTask.disarm(); - this._cacheTask = null; - } - - this._pagePipe = null; - this._scriptPipe = null; - this._initted = false; - this._cacheEntry = null; - this._hasWrittenThisSession = false; - this._cacheEntryPromise = null; - this._cacheEntryResolver = null; - this._cacheDeferredResultScalar = -1; - - if (this.log) { - this.log.trace("Uninitialized."); - this.log = null; - } - - this._procManager = null; - this._procManagerID = null; - this._appender = null; - this._cacheDeferred = null; - this._finalized = false; - this._firstPrivilegedProcessCreated = false; - }, - - _aboutHomeURI: null, - - get aboutHomeURI() { - if (this._aboutHomeURI) { - return this._aboutHomeURI; - } - - this._aboutHomeURI = Services.io.newURI(this.ABOUT_HOME_URI_STRING); - return this._aboutHomeURI; - }, - - // For the AsyncShutdown blocker, this is used to populate the progress - // value. - _cacheProgress: "Not yet begun", - - /** - * Called by the AsyncShutdown blocker on quit-application-granted - * to potentially flush the most recent cache to disk. If one was - * never written during the session, one is generated and written - * before the async function resolves. - * - * @param withTimeout (boolean) - * Whether or not the timeout mechanism should be used. Defaults - * to true. - * @returns Promise - * @resolves boolean - * If a cache has never been written, or a cache write is in - * progress, resolves true when the cache has been written. Also - * resolves to true if a cache didn't need to be written. - * - * Resolves to false if a cache write unexpectedly timed out. - */ - async onShutdown(withTimeout = true) { - // If we never wrote this session, arm the task so that the next - // step can finalize. - if (!this._hasWrittenThisSession) { - this.log.trace("Never wrote a cache this session. Arming cache task."); - this._cacheTask.arm(); - } - - Glean.browserStartup.abouthomeCacheShutdownwrite.set( - this._cacheTask.isArmed - ); - - if (this._cacheTask.isArmed) { - this.log.trace("Finalizing cache task on shutdown"); - this._finalized = true; - - // To avoid hanging shutdowns, we'll ensure that we wait a maximum of - // SHUTDOWN_CACHE_WRITE_TIMEOUT_MS millseconds before giving up. - const TIMED_OUT = Symbol(); - let timeoutID = 0; - - let timeoutPromise = new Promise(resolve => { - timeoutID = lazy.setTimeout( - () => resolve(TIMED_OUT), - this.SHUTDOWN_CACHE_WRITE_TIMEOUT_MS - ); - }); - - let promises = [this._cacheTask.finalize()]; - if (withTimeout) { - this.log.trace("Using timeout mechanism."); - promises.push(timeoutPromise); - } else { - this.log.trace("Skipping timeout mechanism."); - } - - let result = await Promise.race(promises); - this.log.trace("Done blocking shutdown."); - lazy.clearTimeout(timeoutID); - if (result === TIMED_OUT) { - this.log.error("Timed out getting cache streams. Skipping cache task."); - return false; - } - } - this.log.trace("onShutdown is exiting"); - return true; - }, - - /** - * Called by the _cacheTask DeferredTask to actually do the work of - * caching the about:home document. - * - * @returns Promise - * @resolves undefined - * Resolves when a fresh version of the cache has been written. - */ - async cacheNow() { - this.log.trace("Caching now."); - this._cacheProgress = "Getting cache streams"; - - let { pageInputStream, scriptInputStream } = await this.requestCache(); - - if (!pageInputStream || !scriptInputStream) { - this.log.trace("Failed to get cache streams."); - this._cacheProgress = "Failed to get streams"; - return; - } - - this.log.trace("Got cache streams."); - - this._cacheProgress = "Writing to cache"; - - try { - this.log.trace("Populating cache."); - await this.populateCache(pageInputStream, scriptInputStream); - } catch (e) { - this._cacheProgress = "Failed to populate cache"; - this.log.error("Populating the cache failed: ", e); - return; - } - - this._cacheProgress = "Done"; - this.log.trace("Done writing to cache."); - this._hasWrittenThisSession = true; - }, - - /** - * Requests the cached document streams from the "privileged about content - * process". - * - * @returns Promise - * @resolves Object - * Resolves with an Object with the following properties: - * - * pageInputStream (nsIInputStream) - * The page content to write to the cache, or null if request the streams - * failed. - * - * scriptInputStream (nsIInputStream) - * The script content to write to the cache, or null if request the streams - * failed. - */ - requestCache() { - this.log.trace("Parent is requesting Activity Stream state object."); - if (!this._procManager) { - this.log.error("requestCache called with no _procManager!"); - return { pageInputStream: null, scriptInputStream: null }; - } - - if ( - this._procManager.remoteType != lazy.E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE - ) { - this.log.error("Somehow got the wrong process type."); - return { pageInputStream: null, scriptInputStream: null }; - } - - let state = lazy.AboutNewTab.activityStream.store.getState(); - return new Promise(resolve => { - this._cacheDeferred = resolve; - this.log.trace("Parent is requesting cache streams."); - this._procManager.sendAsyncMessage(this.CACHE_REQUEST_MESSAGE, { state }); - }); - }, - - /** - * Helper function that returns a newly constructed nsIPipe instance. - * - * @return nsIPipe - */ - makePipe() { - let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe); - pipe.init( - true /* non-blocking input */, - true /* non-blocking output */, - 0 /* segment size */, - 0 /* max segments */ - ); - return pipe; - }, - - get pagePipe() { - return this._pagePipe; - }, - - get scriptPipe() { - return this._scriptPipe; - }, - - /** - * Called when the nsICacheEntry has been accessed. If the nsICacheEntry - * has content that we want to send down to the "privileged about content - * process", then we connect that content to the nsIPipe's that may or - * may not have already been sent down to the process. - * - * In the event that the nsICacheEntry doesn't contain anything usable, - * the nsInputStreams on the nsIPipe's are closed. - */ - connectToPipes() { - this.log.trace(`Connecting nsICacheEntry to pipes.`); - - // If the cache doesn't yet exist, we'll know because the version metadata - // won't exist yet. - let version; - try { - this.log.trace(""); - version = this._cacheEntry.getMetaDataElement( - this.CACHE_VERSION_META_KEY - ); - } catch (e) { - if (e.result == Cr.NS_ERROR_NOT_AVAILABLE) { - this.log.debug("Cache meta data does not exist. Closing streams."); - this.pagePipe.outputStream.close(); - this.scriptPipe.outputStream.close(); - this.setDeferredResult(this.CACHE_RESULT_SCALARS.DOES_NOT_EXIST); - return; - } - - throw e; - } - - this.log.info("Version retrieved is", version); - - if (version != Services.appinfo.appBuildID) { - this.log.info("Version does not match! Dooming and closing streams.\n"); - // This cache is no good - doom it, and prepare for a new one. - this.clearCache(); - this.pagePipe.outputStream.close(); - this.scriptPipe.outputStream.close(); - this.setDeferredResult(this.CACHE_RESULT_SCALARS.INVALIDATED); - return; - } - - let cachePageInputStream; - - try { - cachePageInputStream = this._cacheEntry.openInputStream(0); - } catch (e) { - this.log.error("Failed to open main input stream for cache entry", e); - this.pagePipe.outputStream.close(); - this.scriptPipe.outputStream.close(); - this.setDeferredResult(this.CACHE_RESULT_SCALARS.CORRUPT_PAGE); - return; - } - - this.log.trace("Connecting page stream to pipe."); - lazy.NetUtil.asyncCopy( - cachePageInputStream, - this.pagePipe.outputStream, - () => { - this.log.info("Page stream connected to pipe."); - } - ); - - let cacheScriptInputStream; - try { - this.log.trace("Connecting script stream to pipe."); - cacheScriptInputStream = - this._cacheEntry.openAlternativeInputStream("script"); - lazy.NetUtil.asyncCopy( - cacheScriptInputStream, - this.scriptPipe.outputStream, - () => { - this.log.info("Script stream connected to pipe."); - } - ); - } catch (e) { - if (e.result == Cr.NS_ERROR_NOT_AVAILABLE) { - // For some reason, the script was not available. We'll close the pipe - // without sending anything into it. The privileged about content process - // will notice that there's nothing available in the pipe, and fall back - // to dynamically generating the page. - this.log.error("Script stream not available! Closing pipe."); - this.scriptPipe.outputStream.close(); - this.setDeferredResult(this.CACHE_RESULT_SCALARS.CORRUPT_SCRIPT); - } else { - throw e; - } - } - - this.setDeferredResult(this.CACHE_RESULT_SCALARS.VALID_AND_USED); - this.log.trace("Streams connected to pipes."); - }, - - /** - * Called when we have received a the cache values from the "privileged - * about content process". The page and script streams are written to - * the nsICacheEntry. - * - * This writing is asynchronous, and if a write happens to already be - * underway when this function is called, that latter call will be - * ignored. - * - * @param pageInputStream (nsIInputStream) - * A stream containing the HTML markup to be saved to the cache. - * @param scriptInputStream (nsIInputStream) - * A stream containing the JS hydration script to be saved to the cache. - * @returns Promise - * @resolves undefined - * When the cache has been successfully written to. - * @rejects Error - * Rejects with a JS Error if writing any part of the cache happens to - * fail. - */ - async populateCache(pageInputStream, scriptInputStream) { - await this.ensureCacheEntry(); - - await new Promise((resolve, reject) => { - // Doom the old cache entry, so we can start writing to a new one. - this.log.trace("Populating the cache. Dooming old entry."); - this.clearCache(); - - this.log.trace("Opening the page output stream."); - let pageOutputStream; - try { - pageOutputStream = this._cacheEntry.openOutputStream(0, -1); - } catch (e) { - reject(e); - return; - } - - this.log.info("Writing the page cache."); - lazy.NetUtil.asyncCopy(pageInputStream, pageOutputStream, pageResult => { - if (!Components.isSuccessCode(pageResult)) { - this.log.error("Failed to write page. Result: " + pageResult); - reject(new Error(pageResult)); - return; - } - - this.log.trace( - "Writing the page data is complete. Now opening the " + - "script output stream." - ); - - let scriptOutputStream; - try { - scriptOutputStream = this._cacheEntry.openAlternativeOutputStream( - "script", - -1 - ); - } catch (e) { - reject(e); - return; - } - - this.log.info("Writing the script cache."); - lazy.NetUtil.asyncCopy( - scriptInputStream, - scriptOutputStream, - scriptResult => { - if (!Components.isSuccessCode(scriptResult)) { - this.log.error("Failed to write script. Result: " + scriptResult); - reject(new Error(scriptResult)); - return; - } - - this.log.trace( - "Writing the script cache is done. Setting version." - ); - try { - this._cacheEntry.setMetaDataElement( - "version", - Services.appinfo.appBuildID - ); - } catch (e) { - this.log.error("Failed to write version."); - reject(e); - return; - } - this.log.trace(`Version is set to ${Services.appinfo.appBuildID}.`); - this.log.info("Caching of page and script is done."); - resolve(); - } - ); - }); - }); - - this.log.trace("populateCache has finished."); - }, - - /** - * Returns a Promise that resolves once the nsICacheEntry for the cache - * is available to write to and read from. - * - * @returns Promise - * @resolves nsICacheEntry - * Once the cache entry has become available. - * @rejects String - * Rejects with an error message if getting the cache entry is attempted - * before the AboutHomeStartupCache component has been initialized. - */ - ensureCacheEntry() { - if (!this._initted) { - return Promise.reject( - "Cannot ensureCacheEntry - AboutHomeStartupCache is not initted" - ); - } - - return this._cacheEntryPromise; - }, - - /** - * Clears the contents of the cache. - */ - clearCache() { - this.log.trace("Clearing the cache."); - this._cacheEntry = this._cacheEntry.recreate(); - this._cacheEntryPromise = new Promise(resolve => { - resolve(this._cacheEntry); - }); - this._hasWrittenThisSession = false; - }, - - /** - * Called when a content process is created. If this is the "privileged - * about content process", then the cache streams will be sent to it. - * - * @param childID (Number) - * The unique ID for the content process that was created, as passed by - * ipc:content-created. - * @param procManager (ProcessMessageManager) - * The ProcessMessageManager for the created content process. - * @param processParent - * The nsIDOMProcessParent for the tab. - */ - onContentProcessCreated(childID, procManager, processParent) { - if (procManager.remoteType == lazy.E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE) { - if (this._finalized) { - this.log.trace( - "Ignoring privileged about content process launch after finalization." - ); - return; - } - - if (this._firstPrivilegedProcessCreated) { - this.log.trace( - "Ignoring non-first privileged about content processes." - ); - return; - } - - this.log.trace( - `A privileged about content process is launching with ID ${childID}.` - ); - - this.log.info("Sending input streams down to content process."); - let actor = processParent.getActor("BrowserProcess"); - actor.sendAsyncMessage(this.SEND_STREAMS_MESSAGE, { - pageInputStream: this.pagePipe.inputStream, - scriptInputStream: this.scriptPipe.inputStream, - }); - - procManager.addMessageListener(this.CACHE_RESPONSE_MESSAGE, this); - procManager.addMessageListener(this.CACHE_USAGE_RESULT_MESSAGE, this); - this._procManager = procManager; - this._procManagerID = childID; - this._firstPrivilegedProcessCreated = true; - } - }, - - /** - * Called when a content process is destroyed. Either it shut down normally, - * or it crashed. If this is the "privileged about content process", then some - * internal state is cleared. - * - * @param childID (Number) - * The unique ID for the content process that was created, as passed by - * ipc:content-shutdown. - */ - onContentProcessShutdown(childID) { - this.log.info(`Content process shutdown: ${childID}`); - if (this._procManagerID == childID) { - this.log.info("It was the current privileged about process."); - if (this._cacheDeferred) { - this.log.error( - "A privileged about content process shut down while cache streams " + - "were still en route." - ); - // The crash occurred while we were waiting on cache input streams to - // be returned to us. Resolve with null streams instead. - this._cacheDeferred({ pageInputStream: null, scriptInputStream: null }); - this._cacheDeferred = null; - } - - this._procManager.removeMessageListener( - this.CACHE_RESPONSE_MESSAGE, - this - ); - this._procManager.removeMessageListener( - this.CACHE_USAGE_RESULT_MESSAGE, - this - ); - this._procManager = null; - this._procManagerID = null; - } - }, - - /** - * Called externally by ActivityStreamMessageChannel anytime - * a message is broadcast to all about:newtabs, or sent to the - * preloaded about:newtab. This is used to determine if we need - * to refresh the cache. - */ - onPreloadedNewTabMessage() { - if (!this._initted || !this._enabled) { - return; - } - - if (this._finalized) { - this.log.trace("Ignoring preloaded newtab update after finalization."); - return; - } - - this.log.trace("Preloaded about:newtab was updated."); - - this._cacheTask.disarm(); - this._cacheTask.arm(); - }, - - /** - * Stores the CACHE_RESULT_SCALARS value that most accurately represents - * the current notion of how the cache has operated so far. It is stored - * temporarily like this because we need to hear from the privileged - * about content process to hear whether or not retrieving the cache - * actually worked on that end. The success state reported back from - * the privileged about content process will be compared against the - * deferred result scalar to compute what will be recorded to - * Telemetry. - * - * Note that this value will only be recorded if its value is GREATER - * than the currently recorded value. This is because it's possible for - * certain functions that record results to re-enter - but we want to record - * the _first_ condition that caused the cache to not be read from. - * - * @param result (Number) - * One of the CACHE_RESULT_SCALARS values. If this value is less than - * the currently recorded value, it is ignored. - */ - setDeferredResult(result) { - if (this._cacheDeferredResultScalar < result) { - this._cacheDeferredResultScalar = result; - } - }, - - /** - * Records the final result of how the cache operated for the user - * during this session to Telemetry. - */ - recordResult(result) { - // Note: this can be called very early on in the lifetime of - // AboutHomeStartupCache, so things like this.log might not exist yet. - Glean.browserStartup.abouthomeCacheResult.set(result); - }, - - /** - * Called when the parent process receives a message from the privileged - * about content process saying whether or not reading from the cache - * was successful. - * - * @param success (boolean) - * True if reading from the cache succeeded. - */ - onUsageResult(success) { - this.log.trace(`Received usage result. Success = ${success}`); - if (success) { - if ( - this._cacheDeferredResultScalar != - this.CACHE_RESULT_SCALARS.VALID_AND_USED - ) { - this.log.error( - "Somehow got a success result despite having never " + - "successfully sent down the cache streams" - ); - this.recordResult(this._cacheDeferredResultScalar); - } else { - this.recordResult(this.CACHE_RESULT_SCALARS.VALID_AND_USED); - } - - return; - } - - if ( - this._cacheDeferredResultScalar == - this.CACHE_RESULT_SCALARS.VALID_AND_USED - ) { - // We failed to read from the cache despite having successfully - // sent it down to the content process. We presume then that the - // streams just didn't provide any bytes in time. - this.recordResult(this.CACHE_RESULT_SCALARS.LATE); - } else { - // We failed to read the cache, but already knew why. We can - // now record that value. - this.recordResult(this._cacheDeferredResultScalar); - } - }, - - QueryInterface: ChromeUtils.generateQI([ - "nsICacheEntryOpenallback", - "nsIObserver", - ]), - - /** MessageListener **/ - - receiveMessage(message) { - // Only the privileged about content process can write to the cache. - if ( - message.target.remoteType != lazy.E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE - ) { - this.log.error( - "Received a message from a non-privileged content process!" - ); - return; - } - - switch (message.name) { - case this.CACHE_RESPONSE_MESSAGE: { - this.log.trace("Parent received cache streams."); - if (!this._cacheDeferred) { - this.log.error("Parent doesn't have _cacheDeferred set up!"); - return; - } - - this._cacheDeferred(message.data); - this._cacheDeferred = null; - break; - } - case this.CACHE_USAGE_RESULT_MESSAGE: { - this.onUsageResult(message.data.success); - break; - } - } - }, - - /** nsIObserver **/ - - observe(aSubject, aTopic, aData) { - switch (aTopic) { - case "intl:app-locales-changed": { - this.clearCache(); - break; - } - case "process-type-set": - // Intentional fall-through - case "ipc:content-created": { - let childID = aData; - let procManager = aSubject - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIMessageSender); - let pp = aSubject.QueryInterface(Ci.nsIDOMProcessParent); - this.onContentProcessCreated(childID, procManager, pp); - break; - } - - case "ipc:content-shutdown": { - let childID = aData; - this.onContentProcessShutdown(childID); - break; - } - } - }, - - /** nsICacheEntryOpenCallback **/ - - onCacheEntryCheck() { - return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; - }, - - onCacheEntryAvailable(aEntry) { - this.log.trace("Cache entry is available."); - - this._cacheEntry = aEntry; - this.connectToPipes(); - this._cacheEntryResolver(this._cacheEntry); - }, -}; diff --git a/browser/components/contentanalysis/content/ContentAnalysis.sys.mjs b/browser/components/contentanalysis/content/ContentAnalysis.sys.mjs index e72a799a255..bcb3343c2bd 100644 --- a/browser/components/contentanalysis/content/ContentAnalysis.sys.mjs +++ b/browser/components/contentanalysis/content/ContentAnalysis.sys.mjs @@ -104,12 +104,15 @@ class RequestInfos { } /** - * Gets all requests across all browsing contexts + * Gets all requests. * * @returns {Array} all the requests */ getAllRequests() { - return this.#map.values(); + return this.#map + .values() + .map(entry => entry.request) + .toArray(); } } diff --git a/browser/components/enterprisepolicies/Policies.sys.mjs b/browser/components/enterprisepolicies/Policies.sys.mjs index 13192264356..648128264e0 100644 --- a/browser/components/enterprisepolicies/Policies.sys.mjs +++ b/browser/components/enterprisepolicies/Policies.sys.mjs @@ -1042,6 +1042,7 @@ export var Policies = { setAndLockPref("datareporting.healthreport.uploadEnabled", false); setAndLockPref("datareporting.policy.dataSubmissionEnabled", false); setAndLockPref("toolkit.telemetry.archive.enabled", false); + setAndLockPref("datareporting.usage.uploadEnabled", false); blockAboutPage(manager, "about:telemetry"); } }, diff --git a/browser/components/extensions/ext-browser.json b/browser/components/extensions/ext-browser.json index 5839278a201..a15893f078f 100644 --- a/browser/components/extensions/ext-browser.json +++ b/browser/components/extensions/ext-browser.json @@ -41,6 +41,15 @@ "manifest": ["commands"], "paths": [["commands"]] }, + "contextualIdentities": { + "url": "chrome://extensions/content/parent/ext-contextualIdentities.js", + "schema": "chrome://extensions/content/schemas/contextual_identities.json", + "scopes": ["addon_parent"], + "settings": true, + "events": ["startup"], + "permissions": ["contextualIdentities"], + "paths": [["contextualIdentities"]] + }, "devtools": { "url": "chrome://browser/content/parent/ext-devtools.js", "schema": "chrome://browser/content/schemas/devtools.json", diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_autoDiscardable.js b/browser/components/extensions/test/browser/browser_ext_tabs_autoDiscardable.js index ced8f80f071..052502ce256 100644 --- a/browser/components/extensions/test/browser/browser_ext_tabs_autoDiscardable.js +++ b/browser/components/extensions/test/browser/browser_ext_tabs_autoDiscardable.js @@ -19,7 +19,7 @@ add_task(async function test_autoDiscardable() { ]), "parent.js": () => { const { TabUnloader } = ChromeUtils.importESModule( - "resource:///modules/TabUnloader.sys.mjs" + "moz-src:///browser/components/tabbrowser/TabUnloader.sys.mjs" ); /* globals ExtensionAPI, ExtensionUtils */ const { ExtensionError } = ExtensionUtils; diff --git a/browser/components/moz.build b/browser/components/moz.build index cec19dd2497..72c51892f5b 100644 --- a/browser/components/moz.build +++ b/browser/components/moz.build @@ -65,6 +65,7 @@ DIRS += [ "syncedtabs", "tabbrowser", "tabunloader", + "taskbartabs", "textrecognition", "topsites", "translations", diff --git a/browser/components/newtab/AboutHomeStartupCache.sys.mjs b/browser/components/newtab/AboutHomeStartupCache.sys.mjs new file mode 100644 index 00000000000..df3cf10d2c2 --- /dev/null +++ b/browser/components/newtab/AboutHomeStartupCache.sys.mjs @@ -0,0 +1,903 @@ +/* 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/. */ + +let lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs", + AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs", + DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs", + E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs", + HomePage: "resource:///modules/HomePage.sys.mjs", + NetUtil: "resource://gre/modules/NetUtil.sys.mjs", + clearTimeout: "resource://gre/modules/Timer.sys.mjs", + setTimeout: "resource://gre/modules/Timer.sys.mjs", +}); + +/** + * AboutHomeStartupCache is responsible for reading and writing the + * initial about:home document from the HTTP cache as a startup + * performance optimization. It only works when the "privileged about + * content process" is enabled and when ENABLED_PREF is set to true. + * + * See https://firefox-source-docs.mozilla.org/browser/extensions/newtab/docs/v2-system-addon/about_home_startup_cache.html + * for further details. + */ +export var AboutHomeStartupCache = { + ABOUT_HOME_URI_STRING: "about:home", + SCRIPT_EXTENSION: "script", + ENABLED_PREF: "browser.startup.homepage.abouthome_cache.enabled", + PRELOADED_NEWTAB_PREF: "browser.newtab.preload", + LOG_LEVEL_PREF: "browser.startup.homepage.abouthome_cache.loglevel", + + // It's possible that the layout of about:home will change such that + // we want to invalidate any pre-existing caches. We do this by setting + // this meta key in the nsICacheEntry for the page. + // + // The version is currently set to the build ID, meaning that the cache + // is invalidated after every upgrade (like the main startup cache). + CACHE_VERSION_META_KEY: "version", + + LOG_NAME: "AboutHomeStartupCache", + + // These messages are used to request the "privileged about content process" + // to create the cached document, and then to receive that document. + CACHE_REQUEST_MESSAGE: "AboutHomeStartupCache:CacheRequest", + CACHE_RESPONSE_MESSAGE: "AboutHomeStartupCache:CacheResponse", + CACHE_USAGE_RESULT_MESSAGE: "AboutHomeStartupCache:UsageResult", + + // When a "privileged about content process" is launched, this message is + // sent to give it some nsIInputStream's for the about:home document they + // should load. + SEND_STREAMS_MESSAGE: "AboutHomeStartupCache:InputStreams", + + // This time in ms is used to debounce messages that are broadcast to + // all about:newtab's, or the preloaded about:newtab. We use those + // messages as a signal that it's likely time to refresh the cache. + CACHE_DEBOUNCE_RATE_MS: 5000, + + // This is how long we'll block the AsyncShutdown while waiting for + // the cache to write. If we fail to write within that time, we will + // allow the shutdown to proceed. + SHUTDOWN_CACHE_WRITE_TIMEOUT_MS: 1000, + + // The following values are as possible values for the + // browser.startup.abouthome_cache_result scalar. Keep these in sync with the + // scalar definition in Scalars.yaml and the matching Glean metric in + // browser/components/metrics.yaml. See setDeferredResult for more + // information. + CACHE_RESULT_SCALARS: { + UNSET: 0, + DOES_NOT_EXIST: 1, + CORRUPT_PAGE: 2, + CORRUPT_SCRIPT: 3, + INVALIDATED: 4, + LATE: 5, + VALID_AND_USED: 6, + DISABLED: 7, + NOT_LOADING_ABOUTHOME: 8, + PRELOADING_DISABLED: 9, + }, + + // This will be set to one of the values of CACHE_RESULT_SCALARS + // once it is determined which result best suits what occurred. + _cacheDeferredResultScalar: -1, + + // A reference to the nsICacheEntry to read from and write to. + _cacheEntry: null, + + // These nsIPipe's are sent down to the "privileged about content process" + // immediately after the process launches. This allows us to race the loading + // of the cache entry in the parent process with the load of the about:home + // page in the content process, since we'll connect the InputStream's to + // the pipes as soon as the nsICacheEntry is available. + // + // The page pipe is for the HTML markup for the page. + _pagePipe: null, + // The script pipe is for the JavaScript that the HTML markup loads + // to set its internal state. + _scriptPipe: null, + _cacheDeferred: null, + + _enabled: false, + _initted: false, + _hasWrittenThisSession: false, + _finalized: false, + _firstPrivilegedProcessCreated: false, + + init() { + if (this._initted) { + throw new Error("AboutHomeStartupCache already initted."); + } + + this.setDeferredResult(this.CACHE_RESULT_SCALARS.UNSET); + + this._enabled = Services.prefs.getBoolPref( + "browser.startup.homepage.abouthome_cache.enabled" + ); + + if (!this._enabled) { + this.recordResult(this.CACHE_RESULT_SCALARS.DISABLED); + return; + } + + this.log = console.createInstance({ + prefix: this.LOG_NAME, + maxLogLevelPref: this.LOG_LEVEL_PREF, + }); + + this.log.trace("Initting."); + + // If the user is not configured to load about:home at startup, then + // let's not bother with the cache - loading it needlessly is more likely + // to hinder what we're actually trying to load. + let willLoadAboutHome = + !lazy.HomePage.overridden && + Services.prefs.getIntPref("browser.startup.page") === 1; + + if (!willLoadAboutHome) { + this.log.trace("Not configured to load about:home by default."); + this.recordResult(this.CACHE_RESULT_SCALARS.NOT_LOADING_ABOUTHOME); + return; + } + + if (!Services.prefs.getBoolPref(this.PRELOADED_NEWTAB_PREF, false)) { + this.log.trace("Preloaded about:newtab disabled."); + this.recordResult(this.CACHE_RESULT_SCALARS.PRELOADING_DISABLED); + return; + } + + Services.obs.addObserver(this, "ipc:content-created"); + Services.obs.addObserver(this, "process-type-set"); + Services.obs.addObserver(this, "ipc:content-shutdown"); + Services.obs.addObserver(this, "intl:app-locales-changed"); + + this.log.trace("Constructing pipes."); + this._pagePipe = this.makePipe(); + this._scriptPipe = this.makePipe(); + + this._cacheEntryPromise = new Promise(resolve => { + this._cacheEntryResolver = resolve; + }); + + let lci = Services.loadContextInfo.default; + let storage = Services.cache2.diskCacheStorage(lci); + try { + storage.asyncOpenURI( + this.aboutHomeURI, + "", + Ci.nsICacheStorage.OPEN_PRIORITY, + this + ); + } catch (e) { + this.log.error("Failed to open about:home cache entry", e); + } + + this._cacheTask = new lazy.DeferredTask(async () => { + await this.cacheNow(); + }, this.CACHE_DEBOUNCE_RATE_MS); + + lazy.AsyncShutdown.quitApplicationGranted.addBlocker( + "AboutHomeStartupCache: Writing cache", + async () => { + await this.onShutdown(); + }, + () => this._cacheProgress + ); + + this._cacheDeferred = null; + this._initted = true; + this.log.trace("Initialized."); + }, + + get initted() { + return this._initted; + }, + + uninit() { + if (!this._enabled) { + return; + } + + try { + Services.obs.removeObserver(this, "ipc:content-created"); + Services.obs.removeObserver(this, "process-type-set"); + Services.obs.removeObserver(this, "ipc:content-shutdown"); + Services.obs.removeObserver(this, "intl:app-locales-changed"); + } catch (e) { + // If we failed to initialize and register for these observer + // notifications, then attempting to remove them will throw. + // It's fine to ignore that case on shutdown. + } + + if (this._cacheTask) { + this._cacheTask.disarm(); + this._cacheTask = null; + } + + this._pagePipe = null; + this._scriptPipe = null; + this._initted = false; + this._cacheEntry = null; + this._hasWrittenThisSession = false; + this._cacheEntryPromise = null; + this._cacheEntryResolver = null; + this._cacheDeferredResultScalar = -1; + + if (this.log) { + this.log.trace("Uninitialized."); + this.log = null; + } + + this._procManager = null; + this._procManagerID = null; + this._appender = null; + this._cacheDeferred = null; + this._finalized = false; + this._firstPrivilegedProcessCreated = false; + }, + + _aboutHomeURI: null, + + get aboutHomeURI() { + if (this._aboutHomeURI) { + return this._aboutHomeURI; + } + + this._aboutHomeURI = Services.io.newURI(this.ABOUT_HOME_URI_STRING); + return this._aboutHomeURI; + }, + + // For the AsyncShutdown blocker, this is used to populate the progress + // value. + _cacheProgress: "Not yet begun", + + /** + * Called by the AsyncShutdown blocker on quit-application-granted + * to potentially flush the most recent cache to disk. If one was + * never written during the session, one is generated and written + * before the async function resolves. + * + * @param {boolean} withTimeout + * Whether or not the timeout mechanism should be used. Defaults + * to true. + * @returns {Promise} + * If a cache has never been written, or a cache write is in + * progress, resolves true when the cache has been written. Also + * resolves to true if a cache didn't need to be written. + * + * Resolves to false if a cache write unexpectedly timed out. + */ + async onShutdown(withTimeout = true) { + // If we never wrote this session, arm the task so that the next + // step can finalize. + if (!this._hasWrittenThisSession) { + this.log.trace("Never wrote a cache this session. Arming cache task."); + this._cacheTask.arm(); + } + + Glean.browserStartup.abouthomeCacheShutdownwrite.set( + this._cacheTask.isArmed + ); + + if (this._cacheTask.isArmed) { + this.log.trace("Finalizing cache task on shutdown"); + this._finalized = true; + + // To avoid hanging shutdowns, we'll ensure that we wait a maximum of + // SHUTDOWN_CACHE_WRITE_TIMEOUT_MS millseconds before giving up. + const TIMED_OUT = Symbol(); + let timeoutID = 0; + + let timeoutPromise = new Promise(resolve => { + timeoutID = lazy.setTimeout( + () => resolve(TIMED_OUT), + this.SHUTDOWN_CACHE_WRITE_TIMEOUT_MS + ); + }); + + let promises = [this._cacheTask.finalize()]; + if (withTimeout) { + this.log.trace("Using timeout mechanism."); + promises.push(timeoutPromise); + } else { + this.log.trace("Skipping timeout mechanism."); + } + + let result = await Promise.race(promises); + this.log.trace("Done blocking shutdown."); + lazy.clearTimeout(timeoutID); + if (result === TIMED_OUT) { + this.log.error("Timed out getting cache streams. Skipping cache task."); + return false; + } + } + this.log.trace("onShutdown is exiting"); + return true; + }, + + /** + * Called by the _cacheTask DeferredTask to actually do the work of + * caching the about:home document. + * + * @returns {Promise} + * Resolves when a fresh version of the cache has been written. + */ + async cacheNow() { + this.log.trace("Caching now."); + this._cacheProgress = "Getting cache streams"; + + let { pageInputStream, scriptInputStream } = await this.requestCache(); + + if (!pageInputStream || !scriptInputStream) { + this.log.trace("Failed to get cache streams."); + this._cacheProgress = "Failed to get streams"; + return; + } + + this.log.trace("Got cache streams."); + + this._cacheProgress = "Writing to cache"; + + try { + this.log.trace("Populating cache."); + await this.populateCache(pageInputStream, scriptInputStream); + } catch (e) { + this._cacheProgress = "Failed to populate cache"; + this.log.error("Populating the cache failed: ", e); + return; + } + + this._cacheProgress = "Done"; + this.log.trace("Done writing to cache."); + this._hasWrittenThisSession = true; + }, + + /** + * Requests the cached document streams from the "privileged about content + * process". + * + * @returns {Promise} + * Resolves with an Object with the following properties: + * + * pageInputStream (nsIInputStream) + * The page content to write to the cache, or null if request the streams + * failed. + * + * scriptInputStream (nsIInputStream) + * The script content to write to the cache, or null if request the streams + * failed. + */ + requestCache() { + this.log.trace("Parent is requesting Activity Stream state object."); + if (!this._procManager) { + this.log.error("requestCache called with no _procManager!"); + return { pageInputStream: null, scriptInputStream: null }; + } + + if ( + this._procManager.remoteType != lazy.E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE + ) { + this.log.error("Somehow got the wrong process type."); + return { pageInputStream: null, scriptInputStream: null }; + } + + let state = lazy.AboutNewTab.activityStream.store.getState(); + return new Promise(resolve => { + this._cacheDeferred = resolve; + this.log.trace("Parent is requesting cache streams."); + this._procManager.sendAsyncMessage(this.CACHE_REQUEST_MESSAGE, { state }); + }); + }, + + /** + * Helper function that returns a newly constructed nsIPipe instance. + * + * @returns {nsIPipe} + */ + makePipe() { + let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe); + pipe.init( + true /* non-blocking input */, + true /* non-blocking output */, + 0 /* segment size */, + 0 /* max segments */ + ); + return pipe; + }, + + get pagePipe() { + return this._pagePipe; + }, + + get scriptPipe() { + return this._scriptPipe; + }, + + /** + * Called when the nsICacheEntry has been accessed. If the nsICacheEntry + * has content that we want to send down to the "privileged about content + * process", then we connect that content to the nsIPipe's that may or + * may not have already been sent down to the process. + * + * In the event that the nsICacheEntry doesn't contain anything usable, + * the nsInputStreams on the nsIPipe's are closed. + */ + connectToPipes() { + this.log.trace(`Connecting nsICacheEntry to pipes.`); + + // If the cache doesn't yet exist, we'll know because the version metadata + // won't exist yet. + let version; + try { + this.log.trace(""); + version = this._cacheEntry.getMetaDataElement( + this.CACHE_VERSION_META_KEY + ); + } catch (e) { + if (e.result == Cr.NS_ERROR_NOT_AVAILABLE) { + this.log.debug("Cache meta data does not exist. Closing streams."); + this.pagePipe.outputStream.close(); + this.scriptPipe.outputStream.close(); + this.setDeferredResult(this.CACHE_RESULT_SCALARS.DOES_NOT_EXIST); + return; + } + + throw e; + } + + this.log.info("Version retrieved is", version); + + if (version != Services.appinfo.appBuildID) { + this.log.info("Version does not match! Dooming and closing streams.\n"); + // This cache is no good - doom it, and prepare for a new one. + this.clearCache(); + this.pagePipe.outputStream.close(); + this.scriptPipe.outputStream.close(); + this.setDeferredResult(this.CACHE_RESULT_SCALARS.INVALIDATED); + return; + } + + let cachePageInputStream; + + try { + cachePageInputStream = this._cacheEntry.openInputStream(0); + } catch (e) { + this.log.error("Failed to open main input stream for cache entry", e); + this.pagePipe.outputStream.close(); + this.scriptPipe.outputStream.close(); + this.setDeferredResult(this.CACHE_RESULT_SCALARS.CORRUPT_PAGE); + return; + } + + this.log.trace("Connecting page stream to pipe."); + lazy.NetUtil.asyncCopy( + cachePageInputStream, + this.pagePipe.outputStream, + () => { + this.log.info("Page stream connected to pipe."); + } + ); + + let cacheScriptInputStream; + try { + this.log.trace("Connecting script stream to pipe."); + cacheScriptInputStream = + this._cacheEntry.openAlternativeInputStream("script"); + lazy.NetUtil.asyncCopy( + cacheScriptInputStream, + this.scriptPipe.outputStream, + () => { + this.log.info("Script stream connected to pipe."); + } + ); + } catch (e) { + if (e.result == Cr.NS_ERROR_NOT_AVAILABLE) { + // For some reason, the script was not available. We'll close the pipe + // without sending anything into it. The privileged about content process + // will notice that there's nothing available in the pipe, and fall back + // to dynamically generating the page. + this.log.error("Script stream not available! Closing pipe."); + this.scriptPipe.outputStream.close(); + this.setDeferredResult(this.CACHE_RESULT_SCALARS.CORRUPT_SCRIPT); + } else { + throw e; + } + } + + this.setDeferredResult(this.CACHE_RESULT_SCALARS.VALID_AND_USED); + this.log.trace("Streams connected to pipes."); + }, + + /** + * Called when we have received a the cache values from the "privileged + * about content process". The page and script streams are written to + * the nsICacheEntry. + * + * This writing is asynchronous, and if a write happens to already be + * underway when this function is called, that latter call will be + * ignored. + * + * @param {nsIInputStream} pageInputStream + * A stream containing the HTML markup to be saved to the cache. + * @param {nsIInputStream} scriptInputStream + * A stream containing the JS hydration script to be saved to the cache. + * @returns {Promise} + * When the cache has been successfully written to. + + * Rejects with a JS Error if writing any part of the cache happens to + * fail. + */ + async populateCache(pageInputStream, scriptInputStream) { + await this.ensureCacheEntry(); + + await new Promise((resolve, reject) => { + // Doom the old cache entry, so we can start writing to a new one. + this.log.trace("Populating the cache. Dooming old entry."); + this.clearCache(); + + this.log.trace("Opening the page output stream."); + let pageOutputStream; + try { + pageOutputStream = this._cacheEntry.openOutputStream(0, -1); + } catch (e) { + reject(e); + return; + } + + this.log.info("Writing the page cache."); + lazy.NetUtil.asyncCopy(pageInputStream, pageOutputStream, pageResult => { + if (!Components.isSuccessCode(pageResult)) { + this.log.error("Failed to write page. Result: " + pageResult); + reject(new Error(pageResult)); + return; + } + + this.log.trace( + "Writing the page data is complete. Now opening the " + + "script output stream." + ); + + let scriptOutputStream; + try { + scriptOutputStream = this._cacheEntry.openAlternativeOutputStream( + "script", + -1 + ); + } catch (e) { + reject(e); + return; + } + + this.log.info("Writing the script cache."); + lazy.NetUtil.asyncCopy( + scriptInputStream, + scriptOutputStream, + scriptResult => { + if (!Components.isSuccessCode(scriptResult)) { + this.log.error("Failed to write script. Result: " + scriptResult); + reject(new Error(scriptResult)); + return; + } + + this.log.trace( + "Writing the script cache is done. Setting version." + ); + try { + this._cacheEntry.setMetaDataElement( + "version", + Services.appinfo.appBuildID + ); + } catch (e) { + this.log.error("Failed to write version."); + reject(e); + return; + } + this.log.trace(`Version is set to ${Services.appinfo.appBuildID}.`); + this.log.info("Caching of page and script is done."); + resolve(); + } + ); + }); + }); + + this.log.trace("populateCache has finished."); + }, + + /** + * Returns a Promise that resolves once the nsICacheEntry for the cache + * is available to write to and read from. + * + * @returns {Promise} + * Resolves once the cache entry has become available. + * + * Rejects with an error message if getting the cache entry is attempted + * before the AboutHomeStartupCache component has been initialized. + */ + ensureCacheEntry() { + if (!this._initted) { + return Promise.reject( + "Cannot ensureCacheEntry - AboutHomeStartupCache is not initted" + ); + } + + return this._cacheEntryPromise; + }, + + /** + * Clears the contents of the cache. + */ + clearCache() { + this.log.trace("Clearing the cache."); + this._cacheEntry = this._cacheEntry.recreate(); + this._cacheEntryPromise = new Promise(resolve => { + resolve(this._cacheEntry); + }); + this._hasWrittenThisSession = false; + }, + + /** + * Called when a content process is created. If this is the "privileged + * about content process", then the cache streams will be sent to it. + * + * @param {number} childID + * The unique ID for the content process that was created, as passed by + * ipc:content-created. + * @param {ProcessMessageManager} procManager + * The ProcessMessageManager for the created content process. + * @param {nsIDOMProcessParent} processParent + * The nsIDOMProcessParent for the tab. + */ + onContentProcessCreated(childID, procManager, processParent) { + if (procManager.remoteType == lazy.E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE) { + if (this._finalized) { + this.log.trace( + "Ignoring privileged about content process launch after finalization." + ); + return; + } + + if (this._firstPrivilegedProcessCreated) { + this.log.trace( + "Ignoring non-first privileged about content processes." + ); + return; + } + + this.log.trace( + `A privileged about content process is launching with ID ${childID}.` + ); + + this.log.info("Sending input streams down to content process."); + let actor = processParent.getActor("BrowserProcess"); + actor.sendAsyncMessage(this.SEND_STREAMS_MESSAGE, { + pageInputStream: this.pagePipe.inputStream, + scriptInputStream: this.scriptPipe.inputStream, + }); + + procManager.addMessageListener(this.CACHE_RESPONSE_MESSAGE, this); + procManager.addMessageListener(this.CACHE_USAGE_RESULT_MESSAGE, this); + this._procManager = procManager; + this._procManagerID = childID; + this._firstPrivilegedProcessCreated = true; + } + }, + + /** + * Called when a content process is destroyed. Either it shut down normally, + * or it crashed. If this is the "privileged about content process", then some + * internal state is cleared. + * + * @param {number} childID + * The unique ID for the content process that was created, as passed by + * ipc:content-shutdown. + */ + onContentProcessShutdown(childID) { + this.log.info(`Content process shutdown: ${childID}`); + if (this._procManagerID == childID) { + this.log.info("It was the current privileged about process."); + if (this._cacheDeferred) { + this.log.error( + "A privileged about content process shut down while cache streams " + + "were still en route." + ); + // The crash occurred while we were waiting on cache input streams to + // be returned to us. Resolve with null streams instead. + this._cacheDeferred({ pageInputStream: null, scriptInputStream: null }); + this._cacheDeferred = null; + } + + this._procManager.removeMessageListener( + this.CACHE_RESPONSE_MESSAGE, + this + ); + this._procManager.removeMessageListener( + this.CACHE_USAGE_RESULT_MESSAGE, + this + ); + this._procManager = null; + this._procManagerID = null; + } + }, + + /** + * Called externally by ActivityStreamMessageChannel anytime + * a message is broadcast to all about:newtabs, or sent to the + * preloaded about:newtab. This is used to determine if we need + * to refresh the cache. + */ + onPreloadedNewTabMessage() { + if (!this._initted || !this._enabled) { + return; + } + + if (this._finalized) { + this.log.trace("Ignoring preloaded newtab update after finalization."); + return; + } + + this.log.trace("Preloaded about:newtab was updated."); + + this._cacheTask.disarm(); + this._cacheTask.arm(); + }, + + /** + * Stores the CACHE_RESULT_SCALARS value that most accurately represents + * the current notion of how the cache has operated so far. It is stored + * temporarily like this because we need to hear from the privileged + * about content process to hear whether or not retrieving the cache + * actually worked on that end. The success state reported back from + * the privileged about content process will be compared against the + * deferred result scalar to compute what will be recorded to + * Telemetry. + * + * Note that this value will only be recorded if its value is GREATER + * than the currently recorded value. This is because it's possible for + * certain functions that record results to re-enter - but we want to record + * the _first_ condition that caused the cache to not be read from. + * + * @param {number} result + * One of the CACHE_RESULT_SCALARS values. If this value is less than + * the currently recorded value, it is ignored. + */ + setDeferredResult(result) { + if (this._cacheDeferredResultScalar < result) { + this._cacheDeferredResultScalar = result; + } + }, + + /** + * Records the final result of how the cache operated for the user + * during this session to Telemetry. + * + * @param {number} result + * One of the result constants from CACHE_RESULT_SCALARS. + */ + recordResult(result) { + // Note: this can be called very early on in the lifetime of + // AboutHomeStartupCache, so things like this.log might not exist yet. + Glean.browserStartup.abouthomeCacheResult.set(result); + }, + + /** + * Called when the parent process receives a message from the privileged + * about content process saying whether or not reading from the cache + * was successful. + * + * @param {boolean} success + * True if reading from the cache succeeded. + */ + onUsageResult(success) { + this.log.trace(`Received usage result. Success = ${success}`); + if (success) { + if ( + this._cacheDeferredResultScalar != + this.CACHE_RESULT_SCALARS.VALID_AND_USED + ) { + this.log.error( + "Somehow got a success result despite having never " + + "successfully sent down the cache streams" + ); + this.recordResult(this._cacheDeferredResultScalar); + } else { + this.recordResult(this.CACHE_RESULT_SCALARS.VALID_AND_USED); + } + + return; + } + + if ( + this._cacheDeferredResultScalar == + this.CACHE_RESULT_SCALARS.VALID_AND_USED + ) { + // We failed to read from the cache despite having successfully + // sent it down to the content process. We presume then that the + // streams just didn't provide any bytes in time. + this.recordResult(this.CACHE_RESULT_SCALARS.LATE); + } else { + // We failed to read the cache, but already knew why. We can + // now record that value. + this.recordResult(this._cacheDeferredResultScalar); + } + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsICacheEntryOpenallback", + "nsIObserver", + ]), + + /* MessageListener */ + + receiveMessage(message) { + // Only the privileged about content process can write to the cache. + if ( + message.target.remoteType != lazy.E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE + ) { + this.log.error( + "Received a message from a non-privileged content process!" + ); + return; + } + + switch (message.name) { + case this.CACHE_RESPONSE_MESSAGE: { + this.log.trace("Parent received cache streams."); + if (!this._cacheDeferred) { + this.log.error("Parent doesn't have _cacheDeferred set up!"); + return; + } + + this._cacheDeferred(message.data); + this._cacheDeferred = null; + break; + } + case this.CACHE_USAGE_RESULT_MESSAGE: { + this.onUsageResult(message.data.success); + break; + } + } + }, + + /* nsIObserver */ + + observe(aSubject, aTopic, aData) { + switch (aTopic) { + case "intl:app-locales-changed": { + this.clearCache(); + break; + } + case "process-type-set": + // Intentional fall-through + case "ipc:content-created": { + let childID = aData; + let procManager = aSubject + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIMessageSender); + let pp = aSubject.QueryInterface(Ci.nsIDOMProcessParent); + this.onContentProcessCreated(childID, procManager, pp); + break; + } + + case "ipc:content-shutdown": { + let childID = aData; + this.onContentProcessShutdown(childID); + break; + } + } + }, + + /* nsICacheEntryOpenCallback */ + + onCacheEntryCheck() { + return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; + }, + + onCacheEntryAvailable(aEntry) { + this.log.trace("Cache entry is available."); + + this._cacheEntry = aEntry; + this.connectToPipes(); + this._cacheEntryResolver(this._cacheEntry); + }, +}; diff --git a/browser/components/newtab/moz.build b/browser/components/newtab/moz.build index 4f36aa82f12..a50df15b4f1 100644 --- a/browser/components/newtab/moz.build +++ b/browser/components/newtab/moz.build @@ -14,6 +14,7 @@ XPIDL_SOURCES += [ XPIDL_MODULE = "browser-newtab" EXTRA_JS_MODULES += [ + "AboutHomeStartupCache.sys.mjs", "AboutNewTabService.sys.mjs", ] diff --git a/browser/components/places/PlacesUIUtils.sys.mjs b/browser/components/places/PlacesUIUtils.sys.mjs index fbdd6a34b12..710f755fe98 100644 --- a/browser/components/places/PlacesUIUtils.sys.mjs +++ b/browser/components/places/PlacesUIUtils.sys.mjs @@ -15,7 +15,8 @@ ChromeUtils.defineESModuleGetters(lazy, { BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", CustomizableUI: "resource:///modules/CustomizableUI.sys.mjs", MigrationUtils: "resource:///modules/MigrationUtils.sys.mjs", - OpenInTabsUtils: "resource:///modules/OpenInTabsUtils.sys.mjs", + OpenInTabsUtils: + "moz-src:///browser/components/tabbrowser/OpenInTabsUtils.sys.mjs", PlacesTransactions: "resource://gre/modules/PlacesTransactions.sys.mjs", PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", diff --git a/browser/components/search/SERPCategorization.sys.mjs b/browser/components/search/SERPCategorization.sys.mjs new file mode 100644 index 00000000000..0c831209509 --- /dev/null +++ b/browser/components/search/SERPCategorization.sys.mjs @@ -0,0 +1,1750 @@ +/* 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/. */ + +/** + * Functionality related to categorizing SERPs. + */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs", + NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", + Region: "resource://gre/modules/Region.sys.mjs", + RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", + SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs", + Sqlite: "resource://gre/modules/Sqlite.sys.mjs", +}); + +ChromeUtils.defineLazyGetter(lazy, "gCryptoHash", () => { + return Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash); +}); + +const CATEGORIZATION_PREF = + "browser.search.serpEventTelemetryCategorization.enabled"; +const CATEGORIZATION_REGION_PREF = + "browser.search.serpEventTelemetryCategorization.regionEnabled"; + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "serpEventTelemetryCategorization", + CATEGORIZATION_PREF, + false, + (aPreference, previousValue, newValue) => { + if (newValue) { + SERPCategorization.init(); + } else { + SERPCategorization.uninit({ deleteMap: true }); + } + } +); + +ChromeUtils.defineLazyGetter(lazy, "logConsole", () => { + return console.createInstance({ + prefix: "SearchTelemetry", + maxLogLevel: lazy.SearchUtils.loggingEnabled ? "Debug" : "Warn", + }); +}); + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "activityLimit", + "telemetry.fog.test.activity_limit", + 120 +); + +export const TELEMETRY_CATEGORIZATION_KEY = "search-categorization"; +export const TELEMETRY_CATEGORIZATION_DOWNLOAD_SETTINGS = { + // Units are in milliseconds. + base: 3600000, + minAdjust: 60000, + maxAdjust: 600000, + maxTriesPerSession: 2, +}; + +export const CATEGORIZATION_SETTINGS = { + STORE_SCHEMA: 1, + STORE_FILE: "domain_to_categories.sqlite", + STORE_NAME: "domain_to_categories", + MAX_DOMAINS_TO_CATEGORIZE: 10, + MINIMUM_SCORE: 0, + STARTING_RANK: 2, + IDLE_TIMEOUT_SECONDS: 60 * 60, + WAKE_TIMEOUT_MS: 60 * 60 * 1000, + PING_SUBMISSION_THRESHOLD: 10, + HAS_MATCHING_REGION: "SearchTelemetry:HasMatchingRegion", + INCONCLUSIVE: 0, +}; + +/** + * @typedef {object} CategorizationResult + * @property {string} organic_category + * The category for the organic result. + * @property {number} organic_num_domains + * The number of domains examined to determine the organic category result. + * @property {number} organic_num_inconclusive + * The number of inconclusive domains when determining the organic result. + * @property {number} organic_num_unknown + * The number of unknown domains when determining the organic result. + * @property {string} sponsored_category + * The category for the organic result. + * @property {number} sponsored_num_domains + * The number of domains examined to determine the sponsored category. + * @property {number} sponsored_num_inconclusive + * The number of inconclusive domains when determining the sponsored category. + * @property {number} sponsored_num_unknown + * The category for the sponsored result. + * @property {string} mappings_version + * The category mapping version used to determine the categories. + */ + +/** + * @typedef {object} CategorizationExtraParams + * @property {number} num_ads_clicked + * The total number of ads clicked on a SERP. + * @property {number} num_ads_hidden + * The total number of ads hidden from the user when categorization occured. + * @property {number} num_ads_loaded + * The total number of ads loaded when categorization occured. + * @property {number} num_ads_visible + * The total number of ads visible to the user when categorization occured. + */ + +/* eslint-disable jsdoc/valid-types */ +/** + * @typedef {CategorizationResult & CategorizationExtraParams} RecordCategorizationParameters + */ +/* eslint-enable jsdoc/valid-types */ + +/** + * Categorizes SERPs. + */ +class Categorizer { + async init() { + if (this.enabled) { + lazy.logConsole.debug("Initialize SERP categorizer."); + await SERPDomainToCategoriesMap.init(); + SERPCategorizationEventScheduler.init(); + SERPCategorizationRecorder.init(); + } + } + + async uninit({ deleteMap = false } = {}) { + lazy.logConsole.debug("Uninit SERP categorizer."); + await SERPDomainToCategoriesMap.uninit(deleteMap); + SERPCategorizationEventScheduler.uninit(); + SERPCategorizationRecorder.uninit(); + } + + get enabled() { + return lazy.serpEventTelemetryCategorization; + } + + /** + * Categorizes domains extracted from SERPs. Note that we don't process + * domains if the domain-to-categories map is empty (if the client couldn't + * download Remote Settings attachments, for example). + * + * @param {Set} nonAdDomains + * Domains from organic results extracted from the page. + * @param {Set} adDomains + * Domains from ad results extracted from the page. + * @returns {CategorizationResult | null} + * The final categorization result. Returns null if the map was empty. + */ + async maybeCategorizeSERP(nonAdDomains, adDomains) { + // Per DS, if the map was empty (e.g. because of a technical issue + // downloading the data), we shouldn't report telemetry. + // Thus, there is no point attempting to categorize the SERP. + if (SERPDomainToCategoriesMap.empty) { + SERPCategorizationRecorder.recordMissingImpressionTelemetry(); + return null; + } + let resultsToReport = {}; + + let results = await this.applyCategorizationLogic(nonAdDomains); + resultsToReport.organic_category = results.category; + resultsToReport.organic_num_domains = results.num_domains; + resultsToReport.organic_num_unknown = results.num_unknown; + resultsToReport.organic_num_inconclusive = results.num_inconclusive; + + results = await this.applyCategorizationLogic(adDomains); + resultsToReport.sponsored_category = results.category; + resultsToReport.sponsored_num_domains = results.num_domains; + resultsToReport.sponsored_num_unknown = results.num_unknown; + resultsToReport.sponsored_num_inconclusive = results.num_inconclusive; + + resultsToReport.mappings_version = SERPDomainToCategoriesMap.version; + + return resultsToReport; + } + + /** + * Applies the logic for reducing extracted domains to a single category for + * the SERP. + * + * @param {Set} domains + * The domains extracted from the page. + * @returns {object} resultsToReport + * The final categorization results. Keys are: "category", "num_domains", + * "num_unknown" and "num_inconclusive". + */ + async applyCategorizationLogic(domains) { + let domainInfo = {}; + let domainsCount = 0; + let unknownsCount = 0; + let inconclusivesCount = 0; + + for (let domain of domains) { + domainsCount++; + + let categoryCandidates = await SERPDomainToCategoriesMap.get(domain); + + if (!categoryCandidates.length) { + unknownsCount++; + continue; + } + + // Inconclusive domains do not have more than one category candidate. + if ( + categoryCandidates[0].category == CATEGORIZATION_SETTINGS.INCONCLUSIVE + ) { + inconclusivesCount++; + continue; + } + + domainInfo[domain] = categoryCandidates; + } + + let finalCategory; + let topCategories = []; + // Determine if all domains were unknown or inconclusive. + if (unknownsCount + inconclusivesCount == domainsCount) { + finalCategory = CATEGORIZATION_SETTINGS.INCONCLUSIVE; + } else { + let maxScore = CATEGORIZATION_SETTINGS.MINIMUM_SCORE; + let rank = CATEGORIZATION_SETTINGS.STARTING_RANK; + for (let categoryCandidates of Object.values(domainInfo)) { + for (let { category, score } of categoryCandidates) { + let adjustedScore = score / Math.log2(rank); + if (adjustedScore > maxScore) { + maxScore = adjustedScore; + topCategories = [category]; + } else if (adjustedScore == maxScore) { + topCategories.push(Number(category)); + } + rank++; + } + } + finalCategory = + topCategories.length > 1 + ? this.#chooseRandomlyFrom(topCategories) + : topCategories[0]; + } + + return { + category: finalCategory, + num_domains: domainsCount, + num_unknown: unknownsCount, + num_inconclusive: inconclusivesCount, + }; + } + + #chooseRandomlyFrom(categories) { + let randIdx = Math.floor(Math.random() * categories.length); + return categories[randIdx]; + } +} + +/** + * Contains outstanding categorizations of browser objects that have yet to be + * scheduled to be reported into a Glean event. + * They are kept here until one of the conditions are met: + * 1. The browser that was tracked is no longer being tracked. + * 2. A user has been idle for IDLE_TIMEOUT_SECONDS + * 3. The user has awoken their computer and the time elapsed from the last + * categorization event exceeds WAKE_TIMEOUT_MS. + */ +class CategorizationEventScheduler { + /** + * A WeakMap containing browser objects mapped to a callback. + * + * @type {WeakMap | null} + */ + #browserToCallbackMap = null; + + /** + * An instance of user idle service. Cached for testing purposes. + * + * @type {nsIUserIdleService | null} + */ + #idleService = null; + + /** + * Whether it has been initialized. + * + * @type {boolean} + */ + #init = false; + + /** + * The last Date.now() of a callback insertion. + * + * @type {number | null} + */ + #mostRecentMs = null; + + init() { + if (this.#init) { + return; + } + + lazy.logConsole.debug("Initializing categorization event scheduler."); + + this.#browserToCallbackMap = new WeakMap(); + + // In tests, we simulate idleness as it is more reliable and easier than + // trying to replicate idleness. The way to do is so it by creating + // an mock idle service and having the component subscribe to it. If we + // used a lazy instantiation of idle service, the test could only ever be + // subscribed to the real one. + this.#idleService = Cc["@mozilla.org/widget/useridleservice;1"].getService( + Ci.nsIUserIdleService + ); + + this.#idleService.addIdleObserver( + this, + CATEGORIZATION_SETTINGS.IDLE_TIMEOUT_SECONDS + ); + + Services.obs.addObserver(this, "quit-application"); + Services.obs.addObserver(this, "wake_notification"); + + this.#init = true; + } + + uninit() { + if (!this.#init) { + return; + } + + this.#browserToCallbackMap = null; + + lazy.logConsole.debug("Un-initializing categorization event scheduler."); + this.#idleService.removeIdleObserver( + this, + CATEGORIZATION_SETTINGS.IDLE_TIMEOUT_SECONDS + ); + + Services.obs.removeObserver(this, "quit-application"); + Services.obs.removeObserver(this, "wake_notification"); + + this.#idleService = null; + this.#init = false; + } + + observe(subject, topic) { + switch (topic) { + case "idle": + lazy.logConsole.debug("Triggering all callbacks due to idle."); + this.#sendAllCallbacks(); + break; + case "quit-application": + this.uninit(); + break; + case "wake_notification": + if ( + this.#mostRecentMs && + Date.now() - this.#mostRecentMs >= + CATEGORIZATION_SETTINGS.WAKE_TIMEOUT_MS + ) { + lazy.logConsole.debug( + "Triggering all callbacks due to a wake notification." + ); + this.#sendAllCallbacks(); + } + break; + } + } + + addCallback(browser, callback) { + lazy.logConsole.debug("Adding callback to queue."); + this.#mostRecentMs = Date.now(); + this.#browserToCallbackMap?.set(browser, callback); + } + + sendCallback(browser) { + let callback = this.#browserToCallbackMap?.get(browser); + if (callback) { + lazy.logConsole.debug("Triggering callback."); + callback(); + Services.obs.notifyObservers( + null, + "recorded-single-categorization-event" + ); + this.#browserToCallbackMap.delete(browser); + } + } + + #sendAllCallbacks() { + let browsers = ChromeUtils.nondeterministicGetWeakMapKeys( + this.#browserToCallbackMap + ); + if (browsers) { + lazy.logConsole.debug("Triggering all callbacks."); + for (let browser of browsers) { + this.sendCallback(browser); + } + } + this.#mostRecentMs = null; + Services.obs.notifyObservers(null, "recorded-all-categorization-events"); + } +} + +/** + * Handles reporting SERP categorization telemetry to Glean. + */ +class CategorizationRecorder { + #init = false; + + // The number of SERP categorizations that have been recorded but not yet + // reported in a Glean ping. + #serpCategorizationsCount = 0; + + // When the user started interacting with the SERP. + #userInteractionStartTime = null; + + async init() { + if (this.#init) { + return; + } + + Services.obs.addObserver(this, "user-interaction-active"); + Services.obs.addObserver(this, "user-interaction-inactive"); + this.#init = true; + this.#serpCategorizationsCount = Services.prefs.getIntPref( + "browser.search.serpMetricsRecordedCounter", + 0 + ); + Services.prefs.setIntPref("browser.search.serpMetricsRecordedCounter", 0); + this.submitPing("startup"); + Services.obs.notifyObservers(null, "categorization-recorder-init"); + } + + uninit() { + if (this.#init) { + Services.obs.removeObserver(this, "user-interaction-active"); + Services.obs.removeObserver(this, "user-interaction-inactive"); + Services.prefs.setIntPref( + "browser.search.serpMetricsRecordedCounter", + this.#serpCategorizationsCount + ); + + this.#resetCategorizationRecorderData(); + this.#init = false; + } + } + + observe(subject, topic, _data) { + switch (topic) { + case "user-interaction-active": { + // If the user is already active, we don't want to overwrite the start + // time. + if (this.#userInteractionStartTime == null) { + this.#userInteractionStartTime = Date.now(); + } + break; + } + case "user-interaction-inactive": { + let currentTime = Date.now(); + let activityLimitInMs = lazy.activityLimit * 1000; + if ( + this.#userInteractionStartTime && + currentTime - this.#userInteractionStartTime >= activityLimitInMs + ) { + this.submitPing("inactivity"); + } + this.#userInteractionStartTime = null; + break; + } + } + } + + /** + * Helper function for recording the SERP categorization event. + * + * @param {RecordCategorizationParameters} resultToReport + * The object containing all the data required to report. + */ + recordCategorizationTelemetry(resultToReport) { + lazy.logConsole.debug( + "Reporting the following categorization result:", + resultToReport + ); + Glean.serp.categorization.record(resultToReport); + + this.#incrementCategorizationsCount(); + } + + /** + * Helper function for recording Glean telemetry when issues with the + * domain-to-categories map cause the categorization and impression not to be + * recorded. + */ + recordMissingImpressionTelemetry() { + lazy.logConsole.debug( + "Recording a missing impression due to an issue with the domain-to-categories map." + ); + Glean.serp.categorizationNoMapFound.add(); + this.#incrementCategorizationsCount(); + } + + /** + * Adds a Glean object metric to the custom SERP categorization ping if info + * about a single experiment has been requested via Nimbus config. + */ + maybeExtractAndRecordExperimentInfo() { + let targetExperiment = + lazy.NimbusFeatures.search.getVariable("targetExperiment"); + if (!targetExperiment) { + lazy.logConsole.debug("No targetExperiment found."); + return; + } + + lazy.logConsole.debug("Found targetExperiment:", targetExperiment); + + // Try checking if an Experiment exists, otherwise check for a Rollout. + let metadata = + lazy.ExperimentAPI.getExperimentMetaData({ + featureId: "search", + slug: targetExperiment, + }) ?? + lazy.ExperimentAPI.getRolloutMetaData({ + featureId: "search", + slug: targetExperiment, + }); + if (!metadata) { + lazy.logConsole.debug( + "No experiment or rollout found that matches targetExperiment." + ); + return; + } + + let experimentToRecord = { + slug: metadata.slug, + branch: metadata.branch?.slug, + }; + lazy.logConsole.debug("Experiment data:", experimentToRecord); + Glean.serp.experimentInfo.set(experimentToRecord); + } + + submitPing(reason) { + if (!this.#serpCategorizationsCount) { + return; + } + + // If experiment info has been requested via Nimbus config, we want to + // record it just before submitting the ping. + this.maybeExtractAndRecordExperimentInfo(); + lazy.logConsole.debug("Submitting SERP categorization ping:", reason); + GleanPings.serpCategorization.submit(reason); + + this.#serpCategorizationsCount = 0; + } + + /** + * Tests are able to clear telemetry on demand. When that happens, we need to + * ensure we're doing to the same here or else the internal count in tests + * will be inaccurate. + */ + testReset() { + if (Cu.isInAutomation) { + this.#resetCategorizationRecorderData(); + } + } + + #incrementCategorizationsCount() { + this.#serpCategorizationsCount++; + + if ( + this.#serpCategorizationsCount >= + CATEGORIZATION_SETTINGS.PING_SUBMISSION_THRESHOLD + ) { + this.submitPing("threshold_reached"); + } + } + + #resetCategorizationRecorderData() { + this.#serpCategorizationsCount = 0; + this.#userInteractionStartTime = null; + } +} + +/** + * @typedef {object} DomainToCategoriesRecord + * @property {boolean} isDefault + * Whether the record is a default if the user's region does not contain a + * more specific set of mappings. + * @property {Array} includeRegions + * The region codes to include. If left blank, it applies to all regions. + * @property {Array} excludeRegions + * The region codes to exclude. + * @property {number} version + * The version of the record. + */ + +/** + * @typedef {object} DomainCategoryScore + * @property {number} category + * The index of the category. + * @property {number} score + * The score associated with the category. + */ + +/** + * Maps domain to categories. Data is downloaded from Remote Settings and + * stored inside DomainToCategoriesStore. + */ +class DomainToCategoriesMap { + /** + * Latest version number of the attachments. + * + * @type {number | null} + */ + #version = null; + + /** + * The Remote Settings client. + * + * @type {object | null} + */ + #client = null; + + /** + * Whether this is synced with Remote Settings. + * + * @type {boolean} + */ + #init = false; + + /** + * Callback when Remote Settings syncs. + * + * @type {Function | null} + */ + #onSettingsSync = null; + + /** + * When downloading an attachment from Remote Settings fails, this will + * contain a timer which will eventually attempt to retry downloading + * attachments. + */ + #downloadTimer = null; + + /** + * Number of times this has attempted to try another download. Will reset + * if the categorization preference has been toggled, or a sync event has + * been detected. + * + * @type {number} + */ + #downloadRetries = 0; + + /** + * A reference to the data store. + * + * @type {DomainToCategoriesStore | null} + */ + #store = null; + + /** + * Runs at application startup with startup idle tasks. If the SERP + * categorization preference is enabled, it creates a Remote Settings + * client to listen to updates, and populates the store. + */ + async init() { + if (this.#init) { + return; + } + lazy.logConsole.debug("Initializing domain-to-categories map."); + + // Set early to allow un-init from an initialization. + this.#init = true; + + try { + await this.#setupClientAndStore(); + } catch (ex) { + lazy.logConsole.error(ex); + await this.uninit(); + return; + } + + // If we don't have a client and store, it likely means an un-init process + // started during the initialization process. + if (this.#client && this.#store) { + lazy.logConsole.debug("Initialized domain-to-categories map."); + Services.obs.notifyObservers(null, "domain-to-categories-map-init"); + } + } + + async uninit(shouldDeleteStore) { + if (this.#init) { + lazy.logConsole.debug("Un-initializing domain-to-categories map."); + this.#clearClient(); + this.#cancelAndNullifyTimer(); + + if (this.#store) { + if (shouldDeleteStore) { + try { + await this.#store.dropData(); + } catch (ex) { + lazy.logConsole.error(ex); + } + } + await this.#store.uninit(); + this.#store = null; + } + + lazy.logConsole.debug("Un-initialized domain-to-categories map."); + this.#init = false; + Services.obs.notifyObservers(null, "domain-to-categories-map-uninit"); + } + } + + /** + * Given a domain, find categories and relevant scores. + * + * @param {string} domain Domain to lookup. + * @returns {Array} + * An array containing categories and their respective score. If no record + * for the domain is available, return an empty array. + */ + async get(domain) { + if (!this.#store || this.#store.empty || !this.#store.ready) { + return []; + } + lazy.gCryptoHash.init(lazy.gCryptoHash.SHA256); + let bytes = new TextEncoder().encode(domain); + lazy.gCryptoHash.update(bytes, domain.length); + let hash = lazy.gCryptoHash.finish(true); + let rawValues = await this.#store.getCategories(hash); + if (rawValues?.length) { + let output = []; + // Transform data into a more readable format. + // [x, y] => { category: x, score: y } + for (let i = 0; i < rawValues.length; i += 2) { + output.push({ category: rawValues[i], score: rawValues[i + 1] }); + } + return output; + } + return []; + } + + /** + * If the map was initialized, returns the version number for the data. + * The version number is determined by the record with the highest version + * number. Even if the records have different versions, only records from the + * latest version should be available. Returns null if the map was not + * initialized. + * + * @returns {null | number} The version number. + */ + get version() { + return this.#version; + } + + /** + * Whether the store is empty of data. + * + * @returns {boolean} + */ + get empty() { + if (!this.#store) { + return true; + } + return this.#store.empty; + } + + /** + * Unit test-only function, used to override the domainToCategoriesMap so + * that tests can set it to easy to test values. + * + * @param {object} domainToCategoriesMap + * An object where the key is a hashed domain and the value is an array + * containing an arbitrary number of DomainCategoryScores. + * @param {number} version + * The version number for the store. + * @param {boolean} isDefault + * Whether the records should be considered default. + */ + async overrideMapForTests( + domainToCategoriesMap, + version = 1, + isDefault = false + ) { + if (Cu.isInAutomation || Services.env.exists("XPCSHELL_TEST_PROFILE_DIR")) { + await this.#store.init(); + await this.#store.dropData(); + await this.#store.insertObject(domainToCategoriesMap, version, isDefault); + } + } + + /** + * Given a list of records from Remote Settings, determine which ones should + * be matched based on the region. + * + * - If a set of records match the region, they should be derived from one + * source JSON file. The reason why it is split up is to make it less + * onerous to download and parse, though testing might find a single + * file to be sufficient. + * - If more than one set of records match the region, it would be from one + * set of records belonging to default mappings that apply to many regions. + * The more specific collection should override the default set. + * + * @param {Array} records + * The records from Remote Settings. + * @param {string|null} region + * The region to match. + * @returns {object|null} + */ + findRecordsForRegion(records, region) { + if (!region || !records?.length) { + return null; + } + + let regionSpecificRecords = []; + let defaultRecords = []; + for (let record of records) { + if (this.recordMatchesRegion(record, region)) { + if (record.isDefault) { + defaultRecords.push(record); + } else { + regionSpecificRecords.push(record); + } + } + } + + if (regionSpecificRecords.length) { + return { records: regionSpecificRecords, isDefault: false }; + } + + if (defaultRecords.length) { + return { records: defaultRecords, isDefault: true }; + } + + return null; + } + + /** + * Checks the record matches the region. + * + * @param {DomainToCategoriesRecord} record + * The record to check. + * @param {string|null} region + * The region the record to be matched against. + * @returns {boolean} + */ + recordMatchesRegion(record, region) { + if (!region || !record) { + return false; + } + + if (record.excludeRegions?.includes(region)) { + return false; + } + + if (record.isDefault) { + return true; + } + + if (!record.includeRegions?.includes(region)) { + return false; + } + + return true; + } + + async syncMayModifyStore(syncData, region) { + if (!syncData || !region) { + return false; + } + + let currentResult = this.findRecordsForRegion(syncData?.current, region); + if (this.#store.empty && !currentResult) { + lazy.logConsole.debug("Store was empty and there were no results."); + return false; + } + + if (!this.#store.empty && !currentResult) { + return true; + } + + let storeHasDefault = await this.#store.isDefault(); + if (storeHasDefault != currentResult.isDefault) { + return true; + } + + const recordsDifferFromStore = records => { + let result = this.findRecordsForRegion(records, region); + return result?.records.length && storeHasDefault == result.isDefault; + }; + + if ( + recordsDifferFromStore(syncData.created) || + recordsDifferFromStore(syncData.deleted) || + recordsDifferFromStore(syncData.updated.map(obj => obj.new)) + ) { + return true; + } + + return false; + } + + /** + * Connect with Remote Settings and retrieve the records associated with + * categorization. Then, check if the records match the store version. If + * no records exist, return early. If records exist but the version stored + * on the records differ from the store version, then attempt to + * empty the store and fill it with data from downloaded attachments. Only + * reuse the store if the version in each record matches the store. + */ + async #setupClientAndStore() { + if (this.#client && !this.empty) { + return; + } + lazy.logConsole.debug("Setting up domain-to-categories map."); + this.#client = lazy.RemoteSettings(TELEMETRY_CATEGORIZATION_KEY); + + this.#onSettingsSync = event => this.#sync(event.data); + this.#client.on("sync", this.#onSettingsSync); + + this.#store = new DomainToCategoriesStore(); + await this.#store.init(); + + let records = await this.#client.get(); + // Even though records don't exist, we still consider the store initialized + // since a sync event from Remote Settings could populate the store with + // records eligible for the client to download. + if (!records.length) { + lazy.logConsole.debug("No records found for domain-to-categories map."); + return; + } + + // At least one of the records must be eligible for the region. + let result = this.findRecordsForRegion(records, lazy.Region.home); + let matchingRecords = result?.records; + let matchingRecordsAreDefault = result?.isDefault; + let hasMatchingRecords = !!matchingRecords?.length; + Services.prefs.setBoolPref(CATEGORIZATION_REGION_PREF, hasMatchingRecords); + + if (!hasMatchingRecords) { + lazy.logConsole.debug( + "No domain-to-category records match the current region:", + lazy.Region.home + ); + // If no matching record was found but the store is not empty, + // the user changed their home region. + if (!this.#store.empty) { + lazy.logConsole.debug( + "Drop store because it no longer matches the home region." + ); + await this.#store.dropData(); + } + return; + } + + this.#version = this.#retrieveLatestVersion(matchingRecords); + let storeVersion = await this.#store.getVersion(); + let storeIsDefault = await this.#store.isDefault(); + if ( + storeVersion == this.#version && + !this.#store.empty && + storeIsDefault == matchingRecordsAreDefault + ) { + lazy.logConsole.debug("Reuse existing domain-to-categories map."); + Services.obs.notifyObservers( + null, + "domain-to-categories-map-update-complete" + ); + return; + } + + await this.#clearAndPopulateStore(records); + } + + #clearClient() { + if (this.#client) { + lazy.logConsole.debug("Removing Remote Settings client."); + this.#client.off("sync", this.#onSettingsSync); + this.#client = null; + this.#onSettingsSync = null; + this.#downloadRetries = 0; + } + } + + /** + * Inspects a list of records from the categorization domain bucket and finds + * the maximum version score from the set of records. Each record should have + * the same version number but if for any reason one entry has a lower + * version number, the latest version can be used to filter it out. + * + * @param {Array} records + * An array containing the records from a Remote Settings collection. + * @returns {number} + */ + #retrieveLatestVersion(records) { + return records.reduce((version, record) => { + if (record.version > version) { + return record.version; + } + return version; + }, 0); + } + + /** + * Callback when Remote Settings has indicated the collection has been + * synced. Determine if the records changed should result in updating the map, + * as some of the records changed might not affect the user's region. + * Additionally, delete any attachment for records that no longer exist. + * + * @param {object} data + * Object containing records that are current, deleted, created, or updated. + */ + async #sync(data) { + lazy.logConsole.debug("Syncing domain-to-categories with Remote Settings."); + + // Remove local files of deleted records. + let toDelete = data?.deleted.filter(d => d.attachment); + await Promise.all( + toDelete.map(record => this.#client.attachments.deleteDownloaded(record)) + ); + + let couldModify = await this.syncMayModifyStore(data, lazy.Region.home); + if (!couldModify) { + lazy.logConsole.debug( + "Domain-to-category records had no changes that matched the region." + ); + return; + } + + this.#downloadRetries = 0; + + try { + await this.#clearAndPopulateStore(data?.current); + } catch (ex) { + lazy.logConsole.error("Error populating map: ", ex); + await this.uninit(); + } + } + + /** + * Clear the existing store and populate it with attachments found in the + * records. If no attachments are found, or no record containing an + * attachment contained the latest version, then nothing will change. + * + * @param {Array} records + * The records containing attachments. + * @throws {Error} + * Will throw if it was not able to drop the store data, or it was unable + * to insert data into the store. + */ + async #clearAndPopulateStore(records) { + // If we don't have a handle to a store, it would mean that it was removed + // during an uninitialization process. + if (!this.#store) { + lazy.logConsole.debug( + "Could not populate store because no store was available." + ); + return; + } + + if (!this.#store.ready) { + lazy.logConsole.debug( + "Could not populate store because it was not ready." + ); + return; + } + + // Empty table so that if there are errors in the download process, callers + // querying the map won't use information we know is probably outdated. + await this.#store.dropData(); + + this.#version = null; + this.#cancelAndNullifyTimer(); + + let result = this.findRecordsForRegion(records, lazy.Region.home); + let recordsMatchingRegion = result?.records; + let isDefault = result?.isDefault; + let hasMatchingRecords = !!recordsMatchingRegion?.length; + Services.prefs.setBoolPref(CATEGORIZATION_REGION_PREF, hasMatchingRecords); + + // A collection with no records is still a valid init state. + if (!records?.length) { + lazy.logConsole.debug("No records found for domain-to-categories map."); + return; + } + + if (!hasMatchingRecords) { + lazy.logConsole.debug( + "No domain-to-category records match the current region:", + lazy.Region.home + ); + return; + } + + let fileContents = []; + let start = Cu.now(); + for (let record of recordsMatchingRegion) { + let fetchedAttachment; + // Downloading attachments can fail. + try { + fetchedAttachment = await this.#client.attachments.download(record); + } catch (ex) { + lazy.logConsole.error("Could not download file:", ex); + this.#createTimerToPopulateMap(); + return; + } + fileContents.push(fetchedAttachment.buffer); + } + ChromeUtils.addProfilerMarker( + "SERPCategorization.#clearAndPopulateStore", + start, + "Download attachments." + ); + + this.#version = this.#retrieveLatestVersion(recordsMatchingRegion); + if (!this.#version) { + lazy.logConsole.debug("Could not find a version number for any record."); + return; + } + + await this.#store.insertFileContents( + fileContents, + this.#version, + isDefault + ); + + lazy.logConsole.debug("Finished updating domain-to-categories store."); + Services.obs.notifyObservers( + null, + "domain-to-categories-map-update-complete" + ); + } + + #cancelAndNullifyTimer() { + if (this.#downloadTimer) { + lazy.logConsole.debug("Cancel and nullify download timer."); + this.#downloadTimer.cancel(); + this.#downloadTimer = null; + } + } + + #createTimerToPopulateMap() { + if ( + this.#downloadRetries >= + TELEMETRY_CATEGORIZATION_DOWNLOAD_SETTINGS.maxTriesPerSession || + !this.#client + ) { + return; + } + if (!this.#downloadTimer) { + this.#downloadTimer = Cc["@mozilla.org/timer;1"].createInstance( + Ci.nsITimer + ); + } + lazy.logConsole.debug("Create timer to retry downloading attachments."); + let delay = + TELEMETRY_CATEGORIZATION_DOWNLOAD_SETTINGS.base + + randomInteger( + TELEMETRY_CATEGORIZATION_DOWNLOAD_SETTINGS.minAdjust, + TELEMETRY_CATEGORIZATION_DOWNLOAD_SETTINGS.maxAdjust + ); + this.#downloadTimer.initWithCallback( + async () => { + this.#downloadRetries += 1; + let records = await this.#client.get(); + try { + await this.#clearAndPopulateStore(records); + } catch (ex) { + lazy.logConsole.error("Error populating store: ", ex); + await this.uninit(); + } + }, + delay, + Ci.nsITimer.TYPE_ONE_SHOT + ); + } +} + +/** + * Handles the storage of data containing domains to categories. + */ +export class DomainToCategoriesStore { + #init = false; + + /** + * The connection to the store. + * + * @type {object | null} + */ + #connection = null; + + /** + * Reference for the shutdown blocker in case we need to remove it before + * shutdown. + * + * @type {Function | null} + */ + #asyncShutdownBlocker = null; + + /** + * Whether the store is empty of data. + * + * @type {boolean} + */ + #empty = true; + + /** + * For a particular subset of errors, we'll attempt to rebuild the database + * from scratch. + */ + #rebuildableErrors = ["NS_ERROR_FILE_CORRUPTED"]; + + /** + * Initializes the store. If the store is initialized it should have cached + * a connection to the store and ensured the store exists. + */ + async init() { + if (this.#init) { + return; + } + lazy.logConsole.debug("Initializing domain-to-categories store."); + + // Attempts to cache a connection to the store. + // If a failure occured, try to re-build the store. + let rebuiltStore = false; + try { + await this.#initConnection(); + } catch (ex1) { + lazy.logConsole.error(`Error initializing a connection: ${ex1}`); + if (this.#rebuildableErrors.includes(ex1.name)) { + try { + await this.#rebuildStore(); + } catch (ex2) { + await this.#closeConnection(); + lazy.logConsole.error(`Could not rebuild store: ${ex2}`); + return; + } + rebuiltStore = true; + } + } + + // If we don't have a connection, bail because the browser could be + // shutting down ASAP, or re-creating the store is impossible. + if (!this.#connection) { + lazy.logConsole.debug( + "Bailing from DomainToCategoriesStore.init because connection doesn't exist." + ); + return; + } + + // If we weren't forced to re-build the store, we only have the connection. + // We want to ensure the store exists so calls to public methods can pass + // without throwing errors due to the absence of the store. + if (!rebuiltStore) { + try { + await this.#initSchema(); + } catch (ex) { + lazy.logConsole.error(`Error trying to create store: ${ex}`); + await this.#closeConnection(); + return; + } + } + + lazy.logConsole.debug("Initialized domain-to-categories store."); + this.#init = true; + } + + async uninit() { + if (this.#init) { + lazy.logConsole.debug("Un-initializing domain-to-categories store."); + await this.#closeConnection(); + this.#asyncShutdownBlocker = null; + lazy.logConsole.debug("Un-initialized domain-to-categories store."); + } + } + + /** + * Whether the store has an open connection to the physical store. + * + * @returns {boolean} + */ + get ready() { + return this.#init; + } + + /** + * Whether the store is devoid of data. + * + * @returns {boolean} + */ + get empty() { + return this.#empty; + } + + /** + * Clears information in the store. If dropping data encountered a failure, + * try to delete the file containing the store and re-create it. + * + * @throws {Error} Will throw if it was unable to clear information from the + * store. + */ + async dropData() { + if (!this.#connection) { + return; + } + let tableExists = await this.#connection.tableExists( + CATEGORIZATION_SETTINGS.STORE_NAME + ); + if (tableExists) { + lazy.logConsole.debug("Drop domain_to_categories."); + // This can fail if the permissions of the store are read-only. + await this.#connection.executeTransaction(async () => { + await this.#connection.execute(`DROP TABLE domain_to_categories`); + const createDomainToCategoriesTable = ` + CREATE TABLE IF NOT EXISTS + domain_to_categories ( + string_id + TEXT PRIMARY KEY NOT NULL, + categories + TEXT + ); + `; + await this.#connection.execute(createDomainToCategoriesTable); + await this.#connection.execute(`DELETE FROM moz_meta`); + await this.#connection.executeCached( + ` + INSERT INTO + moz_meta (key, value) + VALUES + (:key, :value) + ON CONFLICT DO UPDATE SET + value = :value + `, + { key: "version", value: 0 } + ); + }); + + this.#empty = true; + } + } + + /** + * Given file contents, try moving them into the store. If a failure occurs, + * it will attempt to drop existing data to ensure callers aren't accessing + * a partially filled store. + * + * @param {Array} fileContents + * Contents to convert. + * @param {number} version + * The version for the store. + * @param {boolean} isDefault + * Whether the file contents are from a default collection. + * @throws {Error} + * Will throw if the insertion failed and dropData was unable to run + * successfully. + */ + async insertFileContents(fileContents, version, isDefault = false) { + if (!this.#init || !fileContents?.length || !version) { + return; + } + + try { + await this.#insert(fileContents, version, isDefault); + } catch (ex) { + lazy.logConsole.error(`Could not insert file contents: ${ex}`); + await this.dropData(); + } + } + + /** + * Convenience function to make it trivial to insert Javascript objects into + * the store. This avoids having to set up the collection in Remote Settings. + * + * @param {object} domainToCategoriesMap + * An object whose keys should be hashed domains with values containing + * an array of integers. + * @param {number} version + * The version for the store. + * @param {boolean} isDefault + * Whether the mappings are from a default record. + * @returns {boolean} + * Whether the operation was successful. + */ + async insertObject(domainToCategoriesMap, version, isDefault) { + if (!Cu.isInAutomation || !this.#init) { + return false; + } + let buffer = new TextEncoder().encode( + JSON.stringify(domainToCategoriesMap) + ).buffer; + await this.insertFileContents([buffer], version, isDefault); + return true; + } + + /** + * Retrieves domains mapped to the key. + * + * @param {string} key + * The value to lookup in the store. + * @returns {Array} + * An array of numbers corresponding to the category and score. If the key + * does not exist in the store or the store is having issues retrieving the + * value, returns an empty array. + */ + async getCategories(key) { + if (!this.#init) { + return []; + } + + let rows; + try { + rows = await this.#connection.executeCached( + ` + SELECT + categories + FROM + domain_to_categories + WHERE + string_id = :key + `, + { + key, + } + ); + } catch (ex) { + lazy.logConsole.error(`Could not retrieve from the store: ${ex}`); + return []; + } + + if (!rows.length) { + return []; + } + return JSON.parse(rows[0].getResultByName("categories")) ?? []; + } + + /** + * Retrieves the version number of the store. + * + * @returns {number} + * The version number. Returns 0 if the version was never set or if there + * was an issue accessing the version number. + */ + async getVersion() { + if (this.#connection) { + let rows; + try { + rows = await this.#connection.executeCached( + ` + SELECT + value + FROM + moz_meta + WHERE + key = "version" + ` + ); + } catch (ex) { + lazy.logConsole.error(`Could not retrieve version of the store: ${ex}`); + return 0; + } + if (rows.length) { + return parseInt(rows[0].getResultByName("value")) ?? 0; + } + } + return 0; + } + + /** + * Whether the data inside the store was derived from a default set of + * records. + * + * @returns {boolean} + */ + async isDefault() { + if (this.#connection) { + let rows; + try { + rows = await this.#connection.executeCached( + ` + SELECT + value + FROM + moz_meta + WHERE + key = "is_default" + ` + ); + } catch (ex) { + lazy.logConsole.error( + `Could not retrieve if the store is using default records: ${ex}` + ); + return false; + } + if (rows.length && parseInt(rows[0].getResultByName("value")) == 1) { + return true; + } + } + return false; + } + + /** + * Test only function allowing tests to delete the store. + */ + async testDelete() { + if (Cu.isInAutomation) { + await this.#closeConnection(); + await this.#delete(); + } + } + + /** + * If a connection is available, close it and remove shutdown blockers. + */ + async #closeConnection() { + this.#init = false; + this.#empty = true; + if (this.#asyncShutdownBlocker) { + lazy.Sqlite.shutdown.removeBlocker(this.#asyncShutdownBlocker); + this.#asyncShutdownBlocker = null; + } + + if (this.#connection) { + lazy.logConsole.debug("Closing connection."); + // An error could occur while closing the connection. We suppress the + // error since it is not a critical part of the browser. + try { + await this.#connection.close(); + } catch (ex) { + lazy.logConsole.error(ex); + } + this.#connection = null; + } + } + + /** + * Initialize the schema for the store. + * + * @throws {Error} + * Will throw if a permissions error prevents creating the store. + */ + async #initSchema() { + if (!this.#connection) { + return; + } + lazy.logConsole.debug("Create store."); + // Creation can fail if the store is read only. + await this.#connection.executeTransaction(async () => { + // Let outer try block handle the exception. + const createDomainToCategoriesTable = ` + CREATE TABLE IF NOT EXISTS + domain_to_categories ( + string_id + TEXT PRIMARY KEY NOT NULL, + categories + TEXT + ) WITHOUT ROWID; + `; + await this.#connection.execute(createDomainToCategoriesTable); + const createMetaTable = ` + CREATE TABLE IF NOT EXISTS + moz_meta ( + key + TEXT PRIMARY KEY NOT NULL, + value + INTEGER + ) WITHOUT ROWID; + `; + await this.#connection.execute(createMetaTable); + await this.#connection.setSchemaVersion( + CATEGORIZATION_SETTINGS.STORE_SCHEMA + ); + }); + + let rows = await this.#connection.executeCached( + "SELECT count(*) = 0 FROM domain_to_categories" + ); + this.#empty = !!rows[0].getResultByIndex(0); + } + + /** + * Attempt to delete the store. + * + * @throws {Error} + * Will throw if the permissions for the file prevent its deletion. + */ + async #delete() { + lazy.logConsole.debug("Attempt to delete the store."); + try { + await IOUtils.remove( + PathUtils.join( + PathUtils.profileDir, + CATEGORIZATION_SETTINGS.STORE_FILE + ), + { ignoreAbsent: true } + ); + } catch (ex) { + lazy.logConsole.error(ex); + } + this.#empty = true; + lazy.logConsole.debug("Store was deleted."); + } + + /** + * Tries to establish a connection to the store. + * + * @throws {Error} + * Will throw if there was an issue establishing a connection or adding + * adding a shutdown blocker. + */ + async #initConnection() { + if (this.#connection) { + return; + } + + // This could fail if the store is corrupted. + this.#connection = await lazy.Sqlite.openConnection({ + path: PathUtils.join( + PathUtils.profileDir, + CATEGORIZATION_SETTINGS.STORE_FILE + ), + }); + + await this.#connection.execute("PRAGMA journal_mode = TRUNCATE"); + + this.#asyncShutdownBlocker = async () => { + await this.#connection.close(); + this.#connection = null; + }; + + // This could fail if we're adding it during shutdown. In this case, + // don't throw but close the connection. + try { + lazy.Sqlite.shutdown.addBlocker( + "SERPCategorization:DomainToCategoriesSqlite closing", + this.#asyncShutdownBlocker + ); + } catch (ex) { + lazy.logConsole.error(ex); + await this.#closeConnection(); + } + } + + /** + * Inserts into the store. + * + * @param {Array} fileContents + * The data that should be converted and inserted into the store. + * @param {number} version + * The version number that should be inserted into the store. + * @param {boolean} isDefault + * Whether the file contents are a default set of records. + * @throws {Error} + * Will throw if a connection is not present, if the store is not + * able to be updated (permissions error, corrupted file), or there is + * something wrong with the file contents. + */ + async #insert(fileContents, version, isDefault) { + let start = Cu.now(); + await this.#connection.executeTransaction(async () => { + lazy.logConsole.debug("Insert into domain_to_categories table."); + for (let fileContent of fileContents) { + await this.#connection.executeCached( + ` + INSERT INTO + domain_to_categories (string_id, categories) + SELECT + json_each.key AS string_id, + json_each.value AS categories + FROM + json_each(json(:obj)) + `, + { + obj: new TextDecoder().decode(fileContent), + } + ); + } + // Once the insertions have successfully completed, update the version. + await this.#connection.executeCached( + ` + INSERT INTO + moz_meta (key, value) + VALUES + (:key, :value) + ON CONFLICT DO UPDATE SET + value = :value + `, + { key: "version", value: version } + ); + if (isDefault) { + await this.#connection.executeCached( + ` + INSERT INTO + moz_meta (key, value) + VALUES + (:key, :value) + ON CONFLICT DO UPDATE SET + value = :value + `, + { key: "is_default", value: 1 } + ); + } + }); + ChromeUtils.addProfilerMarker( + "DomainToCategoriesSqlite.#insert", + start, + "Move file contents into table." + ); + + if (fileContents?.length) { + this.#empty = false; + } + } + + /** + * Deletes and re-build's the store. Used in cases where we encounter a + * failure and we want to try fixing the error by starting with an + * entirely fresh store. + * + * @throws {Error} + * Will throw if a connection could not be established, if it was + * unable to delete the store, or it was unable to build a new store. + */ + async #rebuildStore() { + lazy.logConsole.debug("Try rebuilding store."); + // Step 1. Close all connections. + await this.#closeConnection(); + + // Step 2. Delete the existing store. + await this.#delete(); + + // Step 3. Re-establish the connection. + await this.#initConnection(); + + // Step 4. If a connection exists, try creating the store. + await this.#initSchema(); + } +} + +function randomInteger(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +export var SERPDomainToCategoriesMap = new DomainToCategoriesMap(); +export var SERPCategorization = new Categorizer(); +export var SERPCategorizationRecorder = new CategorizationRecorder(); +export var SERPCategorizationEventScheduler = + new CategorizationEventScheduler(); diff --git a/browser/components/search/SearchSERPTelemetry.sys.mjs b/browser/components/search/SearchSERPTelemetry.sys.mjs index aa222894c92..edb7074f712 100644 --- a/browser/components/search/SearchSERPTelemetry.sys.mjs +++ b/browser/components/search/SearchSERPTelemetry.sys.mjs @@ -2,23 +2,18 @@ * 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/. */ -import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; - const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { BrowserSearchTelemetry: "resource:///modules/BrowserSearchTelemetry.sys.mjs", BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", - ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs", - NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", Region: "resource://gre/modules/Region.sys.mjs", RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs", - Sqlite: "resource://gre/modules/Sqlite.sys.mjs", -}); - -ChromeUtils.defineLazyGetter(lazy, "gCryptoHash", () => { - return Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash); + SERPCategorization: "resource:///modules/SERPCategorization.sys.mjs", + SERPCategorizationRecorder: "resource:///modules/SERPCategorization.sys.mjs", + SERPCategorizationEventScheduler: + "resource:///modules/SERPCategorization.sys.mjs", }); // Exported for tests. @@ -29,14 +24,6 @@ export const ADLINK_CHECK_TIMEOUT_MS = 1000; // slow to update the content on the page. export const SPA_ADLINK_CHECK_TIMEOUT_MS = 2500; export const TELEMETRY_SETTINGS_KEY = "search-telemetry-v2"; -export const TELEMETRY_CATEGORIZATION_KEY = "search-categorization"; -export const TELEMETRY_CATEGORIZATION_DOWNLOAD_SETTINGS = { - // Units are in milliseconds. - base: 3600000, - minAdjust: 60000, - maxAdjust: 600000, - maxTriesPerSession: 2, -}; export const SEARCH_TELEMETRY_SHARED = { PROVIDER_INFO: "SearchTelemetry:ProviderInfo", @@ -46,19 +33,6 @@ export const SEARCH_TELEMETRY_SHARED = { const impressionIdsWithoutEngagementsSet = new Set(); -export const CATEGORIZATION_SETTINGS = { - STORE_SCHEMA: 1, - STORE_FILE: "domain_to_categories.sqlite", - STORE_NAME: "domain_to_categories", - MAX_DOMAINS_TO_CATEGORIZE: 10, - MINIMUM_SCORE: 0, - STARTING_RANK: 2, - IDLE_TIMEOUT_SECONDS: 60 * 60, - WAKE_TIMEOUT_MS: 60 * 60 * 1000, - PING_SUBMISSION_THRESHOLD: 10, - HAS_MATCHING_REGION: "SearchTelemetry:HasMatchingRegion", -}; - ChromeUtils.defineLazyGetter(lazy, "logConsole", () => { return console.createInstance({ prefix: "SearchTelemetry", @@ -66,32 +40,6 @@ ChromeUtils.defineLazyGetter(lazy, "logConsole", () => { }); }); -const CATEGORIZATION_PREF = - "browser.search.serpEventTelemetryCategorization.enabled"; -const CATEGORIZATION_REGION_PREF = - "browser.search.serpEventTelemetryCategorization.regionEnabled"; - -XPCOMUtils.defineLazyPreferenceGetter( - lazy, - "serpEventTelemetryCategorization", - CATEGORIZATION_PREF, - false, - (aPreference, previousValue, newValue) => { - if (newValue) { - SearchSERPCategorization.init(); - } else { - SearchSERPCategorization.uninit({ deleteMap: true }); - } - } -); - -XPCOMUtils.defineLazyPreferenceGetter( - lazy, - "activityLimit", - "telemetry.fog.test.activity_limit", - 120 -); - export const SearchSERPTelemetryUtils = { ACTIONS: { CLICKED: "clicked", @@ -125,9 +73,6 @@ export const SearchSERPTelemetryUtils = { REFINE_ON_SERP: "follow_on_from_refine_on_SERP", SEARCHBOX: "follow_on_from_refine_on_incontent_search", }, - CATEGORIZATION: { - INCONCLUSIVE: 0, - }, }; const AD_COMPONENTS = [ @@ -688,10 +633,10 @@ class TelemetryHandler { } if ( - lazy.serpEventTelemetryCategorization && + lazy.SERPCategorization.enabled && telemetryState.categorizationInfo ) { - SearchSERPCategorizationEventScheduler.sendCallback(browser); + lazy.SERPCategorizationEventScheduler.sendCallback(browser); } item.browserTelemetryStateMap.delete(browser); @@ -1844,10 +1789,10 @@ class ContentHandler { async _reportPageDomains(info, browser) { let item = this._findItemForBrowser(browser); let telemetryState = item?.browserTelemetryStateMap.get(browser); - if (lazy.serpEventTelemetryCategorization && telemetryState) { + if (lazy.SERPCategorization.enabled && telemetryState) { lazy.logConsole.debug("Ad domains:", Array.from(info.adDomains)); lazy.logConsole.debug("Non ad domains:", Array.from(info.nonAdDomains)); - let result = await SearchSERPCategorization.maybeCategorizeSERP( + let result = await lazy.SERPCategorization.maybeCategorizeSERP( info.nonAdDomains, info.adDomains, item.info.provider @@ -1856,7 +1801,7 @@ class ContentHandler { telemetryState.categorizationInfo = result; let callback = () => { let impressionInfo = telemetryState.impressionInfo; - SERPCategorizationRecorder.recordCategorizationTelemetry({ + lazy.SERPCategorizationRecorder.recordCategorizationTelemetry({ ...telemetryState.categorizationInfo, app_version: item.majorVersion, channel: item.channel, @@ -1871,7 +1816,7 @@ class ContentHandler { num_ads_visible: telemetryState.adsVisible, }); }; - SearchSERPCategorizationEventScheduler.addCallback(browser, callback); + lazy.SERPCategorizationEventScheduler.addCallback(browser, callback); } } Services.obs.notifyObservers( @@ -1881,1671 +1826,4 @@ class ContentHandler { } } -/** - * @typedef {object} CategorizationResult - * @property {string} organic_category - * The category for the organic result. - * @property {number} organic_num_domains - * The number of domains examined to determine the organic category result. - * @property {number} organic_num_inconclusive - * The number of inconclusive domains when determining the organic result. - * @property {number} organic_num_unknown - * The number of unknown domains when determining the organic result. - * @property {string} sponsored_category - * The category for the organic result. - * @property {number} sponsored_num_domains - * The number of domains examined to determine the sponsored category. - * @property {number} sponsored_num_inconclusive - * The number of inconclusive domains when determining the sponsored category. - * @property {number} sponsored_num_unknown - * The category for the sponsored result. - * @property {string} mappings_version - * The category mapping version used to determine the categories. - */ - -/** - * @typedef {object} CategorizationExtraParams - * @property {number} num_ads_clicked - * The total number of ads clicked on a SERP. - * @property {number} num_ads_hidden - * The total number of ads hidden from the user when categorization occured. - * @property {number} num_ads_loaded - * The total number of ads loaded when categorization occured. - * @property {number} num_ads_visible - * The total number of ads visible to the user when categorization occured. - */ - -/* eslint-disable jsdoc/valid-types */ -/** - * @typedef {CategorizationResult & CategorizationExtraParams} RecordCategorizationParameters - */ -/* eslint-enable jsdoc/valid-types */ - -/** - * Categorizes SERPs. - */ -class SERPCategorizer { - async init() { - if (lazy.serpEventTelemetryCategorization) { - lazy.logConsole.debug("Initialize SERP categorizer."); - await SearchSERPDomainToCategoriesMap.init(); - SearchSERPCategorizationEventScheduler.init(); - SERPCategorizationRecorder.init(); - } - } - - async uninit({ deleteMap = false } = {}) { - lazy.logConsole.debug("Uninit SERP categorizer."); - await SearchSERPDomainToCategoriesMap.uninit(deleteMap); - SearchSERPCategorizationEventScheduler.uninit(); - SERPCategorizationRecorder.uninit(); - } - - /** - * Categorizes domains extracted from SERPs. Note that we don't process - * domains if the domain-to-categories map is empty (if the client couldn't - * download Remote Settings attachments, for example). - * - * @param {Set} nonAdDomains - * Domains from organic results extracted from the page. - * @param {Set} adDomains - * Domains from ad results extracted from the page. - * @returns {CategorizationResult | null} - * The final categorization result. Returns null if the map was empty. - */ - async maybeCategorizeSERP(nonAdDomains, adDomains) { - // Per DS, if the map was empty (e.g. because of a technical issue - // downloading the data), we shouldn't report telemetry. - // Thus, there is no point attempting to categorize the SERP. - if (SearchSERPDomainToCategoriesMap.empty) { - SERPCategorizationRecorder.recordMissingImpressionTelemetry(); - return null; - } - let resultsToReport = {}; - - let results = await this.applyCategorizationLogic(nonAdDomains); - resultsToReport.organic_category = results.category; - resultsToReport.organic_num_domains = results.num_domains; - resultsToReport.organic_num_unknown = results.num_unknown; - resultsToReport.organic_num_inconclusive = results.num_inconclusive; - - results = await this.applyCategorizationLogic(adDomains); - resultsToReport.sponsored_category = results.category; - resultsToReport.sponsored_num_domains = results.num_domains; - resultsToReport.sponsored_num_unknown = results.num_unknown; - resultsToReport.sponsored_num_inconclusive = results.num_inconclusive; - - resultsToReport.mappings_version = SearchSERPDomainToCategoriesMap.version; - - return resultsToReport; - } - - /** - * Applies the logic for reducing extracted domains to a single category for - * the SERP. - * - * @param {Set} domains - * The domains extracted from the page. - * @returns {object} resultsToReport - * The final categorization results. Keys are: "category", "num_domains", - * "num_unknown" and "num_inconclusive". - */ - async applyCategorizationLogic(domains) { - let domainInfo = {}; - let domainsCount = 0; - let unknownsCount = 0; - let inconclusivesCount = 0; - - for (let domain of domains) { - domainsCount++; - - let categoryCandidates = - await SearchSERPDomainToCategoriesMap.get(domain); - - if (!categoryCandidates.length) { - unknownsCount++; - continue; - } - - // Inconclusive domains do not have more than one category candidate. - if ( - categoryCandidates[0].category == - SearchSERPTelemetryUtils.CATEGORIZATION.INCONCLUSIVE - ) { - inconclusivesCount++; - continue; - } - - domainInfo[domain] = categoryCandidates; - } - - let finalCategory; - let topCategories = []; - // Determine if all domains were unknown or inconclusive. - if (unknownsCount + inconclusivesCount == domainsCount) { - finalCategory = SearchSERPTelemetryUtils.CATEGORIZATION.INCONCLUSIVE; - } else { - let maxScore = CATEGORIZATION_SETTINGS.MINIMUM_SCORE; - let rank = CATEGORIZATION_SETTINGS.STARTING_RANK; - for (let categoryCandidates of Object.values(domainInfo)) { - for (let { category, score } of categoryCandidates) { - let adjustedScore = score / Math.log2(rank); - if (adjustedScore > maxScore) { - maxScore = adjustedScore; - topCategories = [category]; - } else if (adjustedScore == maxScore) { - topCategories.push(Number(category)); - } - rank++; - } - } - finalCategory = - topCategories.length > 1 - ? this.#chooseRandomlyFrom(topCategories) - : topCategories[0]; - } - - return { - category: finalCategory, - num_domains: domainsCount, - num_unknown: unknownsCount, - num_inconclusive: inconclusivesCount, - }; - } - - #chooseRandomlyFrom(categories) { - let randIdx = Math.floor(Math.random() * categories.length); - return categories[randIdx]; - } -} - -/** - * Contains outstanding categorizations of browser objects that have yet to be - * scheduled to be reported into a Glean event. - * They are kept here until one of the conditions are met: - * 1. The browser that was tracked is no longer being tracked. - * 2. A user has been idle for IDLE_TIMEOUT_SECONDS - * 3. The user has awoken their computer and the time elapsed from the last - * categorization event exceeds WAKE_TIMEOUT_MS. - */ -class CategorizationEventScheduler { - /** - * A WeakMap containing browser objects mapped to a callback. - * - * @type {WeakMap | null} - */ - #browserToCallbackMap = null; - - /** - * An instance of user idle service. Cached for testing purposes. - * - * @type {nsIUserIdleService | null} - */ - #idleService = null; - - /** - * Whether it has been initialized. - * - * @type {boolean} - */ - #init = false; - - /** - * The last Date.now() of a callback insertion. - * - * @type {number | null} - */ - #mostRecentMs = null; - - init() { - if (this.#init) { - return; - } - - lazy.logConsole.debug("Initializing categorization event scheduler."); - - this.#browserToCallbackMap = new WeakMap(); - - // In tests, we simulate idleness as it is more reliable and easier than - // trying to replicate idleness. The way to do is so it by creating - // an mock idle service and having the component subscribe to it. If we - // used a lazy instantiation of idle service, the test could only ever be - // subscribed to the real one. - this.#idleService = Cc["@mozilla.org/widget/useridleservice;1"].getService( - Ci.nsIUserIdleService - ); - - this.#idleService.addIdleObserver( - this, - CATEGORIZATION_SETTINGS.IDLE_TIMEOUT_SECONDS - ); - - Services.obs.addObserver(this, "quit-application"); - Services.obs.addObserver(this, "wake_notification"); - - this.#init = true; - } - - uninit() { - if (!this.#init) { - return; - } - - this.#browserToCallbackMap = null; - - lazy.logConsole.debug("Un-initializing categorization event scheduler."); - this.#idleService.removeIdleObserver( - this, - CATEGORIZATION_SETTINGS.IDLE_TIMEOUT_SECONDS - ); - - Services.obs.removeObserver(this, "quit-application"); - Services.obs.removeObserver(this, "wake_notification"); - - this.#idleService = null; - this.#init = false; - } - - observe(subject, topic) { - switch (topic) { - case "idle": - lazy.logConsole.debug("Triggering all callbacks due to idle."); - this.#sendAllCallbacks(); - break; - case "quit-application": - this.uninit(); - break; - case "wake_notification": - if ( - this.#mostRecentMs && - Date.now() - this.#mostRecentMs >= - CATEGORIZATION_SETTINGS.WAKE_TIMEOUT_MS - ) { - lazy.logConsole.debug( - "Triggering all callbacks due to a wake notification." - ); - this.#sendAllCallbacks(); - } - break; - } - } - - addCallback(browser, callback) { - lazy.logConsole.debug("Adding callback to queue."); - this.#mostRecentMs = Date.now(); - this.#browserToCallbackMap?.set(browser, callback); - } - - sendCallback(browser) { - let callback = this.#browserToCallbackMap?.get(browser); - if (callback) { - lazy.logConsole.debug("Triggering callback."); - callback(); - Services.obs.notifyObservers( - null, - "recorded-single-categorization-event" - ); - this.#browserToCallbackMap.delete(browser); - } - } - - #sendAllCallbacks() { - let browsers = ChromeUtils.nondeterministicGetWeakMapKeys( - this.#browserToCallbackMap - ); - if (browsers) { - lazy.logConsole.debug("Triggering all callbacks."); - for (let browser of browsers) { - this.sendCallback(browser); - } - } - this.#mostRecentMs = null; - Services.obs.notifyObservers(null, "recorded-all-categorization-events"); - } -} - -/** - * Handles reporting SERP categorization telemetry to Glean. - */ -class CategorizationRecorder { - #init = false; - - // The number of SERP categorizations that have been recorded but not yet - // reported in a Glean ping. - #serpCategorizationsCount = 0; - - // When the user started interacting with the SERP. - #userInteractionStartTime = null; - - async init() { - if (this.#init) { - return; - } - - Services.obs.addObserver(this, "user-interaction-active"); - Services.obs.addObserver(this, "user-interaction-inactive"); - this.#init = true; - this.#serpCategorizationsCount = Services.prefs.getIntPref( - "browser.search.serpMetricsRecordedCounter", - 0 - ); - Services.prefs.setIntPref("browser.search.serpMetricsRecordedCounter", 0); - this.submitPing("startup"); - Services.obs.notifyObservers(null, "categorization-recorder-init"); - } - - uninit() { - if (this.#init) { - Services.obs.removeObserver(this, "user-interaction-active"); - Services.obs.removeObserver(this, "user-interaction-inactive"); - Services.prefs.setIntPref( - "browser.search.serpMetricsRecordedCounter", - this.#serpCategorizationsCount - ); - - this.#resetCategorizationRecorderData(); - this.#init = false; - } - } - - observe(subject, topic, _data) { - switch (topic) { - case "user-interaction-active": { - // If the user is already active, we don't want to overwrite the start - // time. - if (this.#userInteractionStartTime == null) { - this.#userInteractionStartTime = Date.now(); - } - break; - } - case "user-interaction-inactive": { - let currentTime = Date.now(); - let activityLimitInMs = lazy.activityLimit * 1000; - if ( - this.#userInteractionStartTime && - currentTime - this.#userInteractionStartTime >= activityLimitInMs - ) { - this.submitPing("inactivity"); - } - this.#userInteractionStartTime = null; - break; - } - } - } - - /** - * Helper function for recording the SERP categorization event. - * - * @param {RecordCategorizationParameters} resultToReport - * The object containing all the data required to report. - */ - recordCategorizationTelemetry(resultToReport) { - lazy.logConsole.debug( - "Reporting the following categorization result:", - resultToReport - ); - Glean.serp.categorization.record(resultToReport); - - this.#incrementCategorizationsCount(); - } - - /** - * Helper function for recording Glean telemetry when issues with the - * domain-to-categories map cause the categorization and impression not to be - * recorded. - */ - recordMissingImpressionTelemetry() { - lazy.logConsole.debug( - "Recording a missing impression due to an issue with the domain-to-categories map." - ); - Glean.serp.categorizationNoMapFound.add(); - this.#incrementCategorizationsCount(); - } - - /** - * Adds a Glean object metric to the custom SERP categorization ping if info - * about a single experiment has been requested via Nimbus config. - */ - maybeExtractAndRecordExperimentInfo() { - let targetExperiment = - lazy.NimbusFeatures.search.getVariable("targetExperiment"); - if (!targetExperiment) { - lazy.logConsole.debug("No targetExperiment found."); - return; - } - - lazy.logConsole.debug("Found targetExperiment:", targetExperiment); - - // Try checking if an Experiment exists, otherwise check for a Rollout. - let metadata = - lazy.ExperimentAPI.getExperimentMetaData({ - featureId: "search", - slug: targetExperiment, - }) ?? - lazy.ExperimentAPI.getRolloutMetaData({ - featureId: "search", - slug: targetExperiment, - }); - if (!metadata) { - lazy.logConsole.debug( - "No experiment or rollout found that matches targetExperiment." - ); - return; - } - - let experimentToRecord = { - slug: metadata.slug, - branch: metadata.branch?.slug, - }; - lazy.logConsole.debug("Experiment data:", experimentToRecord); - Glean.serp.experimentInfo.set(experimentToRecord); - } - - submitPing(reason) { - if (!this.#serpCategorizationsCount) { - return; - } - - // If experiment info has been requested via Nimbus config, we want to - // record it just before submitting the ping. - this.maybeExtractAndRecordExperimentInfo(); - lazy.logConsole.debug("Submitting SERP categorization ping:", reason); - GleanPings.serpCategorization.submit(reason); - - this.#serpCategorizationsCount = 0; - } - - /** - * Tests are able to clear telemetry on demand. When that happens, we need to - * ensure we're doing to the same here or else the internal count in tests - * will be inaccurate. - */ - testReset() { - if (Cu.isInAutomation) { - this.#resetCategorizationRecorderData(); - } - } - - #incrementCategorizationsCount() { - this.#serpCategorizationsCount++; - - if ( - this.#serpCategorizationsCount >= - CATEGORIZATION_SETTINGS.PING_SUBMISSION_THRESHOLD - ) { - this.submitPing("threshold_reached"); - } - } - - #resetCategorizationRecorderData() { - this.#serpCategorizationsCount = 0; - this.#userInteractionStartTime = null; - } -} - -/** - * @typedef {object} DomainToCategoriesRecord - * @property {boolean} isDefault - * Whether the record is a default if the user's region does not contain a - * more specific set of mappings. - * @property {Array} includeRegions - * The region codes to include. If left blank, it applies to all regions. - * @property {Array} excludeRegions - * The region codes to exclude. - * @property {number} version - * The version of the record. - */ - -/** - * @typedef {object} DomainCategoryScore - * @property {number} category - * The index of the category. - * @property {number} score - * The score associated with the category. - */ - -/** - * Maps domain to categories. Data is downloaded from Remote Settings and - * stored inside DomainToCategoriesStore. - */ -class DomainToCategoriesMap { - /** - * Latest version number of the attachments. - * - * @type {number | null} - */ - #version = null; - - /** - * The Remote Settings client. - * - * @type {object | null} - */ - #client = null; - - /** - * Whether this is synced with Remote Settings. - * - * @type {boolean} - */ - #init = false; - - /** - * Callback when Remote Settings syncs. - * - * @type {Function | null} - */ - #onSettingsSync = null; - - /** - * When downloading an attachment from Remote Settings fails, this will - * contain a timer which will eventually attempt to retry downloading - * attachments. - */ - #downloadTimer = null; - - /** - * Number of times this has attempted to try another download. Will reset - * if the categorization preference has been toggled, or a sync event has - * been detected. - * - * @type {number} - */ - #downloadRetries = 0; - - /** - * A reference to the data store. - * - * @type {DomainToCategoriesStore | null} - */ - #store = null; - - /** - * Runs at application startup with startup idle tasks. If the SERP - * categorization preference is enabled, it creates a Remote Settings - * client to listen to updates, and populates the store. - */ - async init() { - if (this.#init) { - return; - } - lazy.logConsole.debug("Initializing domain-to-categories map."); - - // Set early to allow un-init from an initialization. - this.#init = true; - - try { - await this.#setupClientAndStore(); - } catch (ex) { - lazy.logConsole.error(ex); - await this.uninit(); - return; - } - - // If we don't have a client and store, it likely means an un-init process - // started during the initialization process. - if (this.#client && this.#store) { - lazy.logConsole.debug("Initialized domain-to-categories map."); - Services.obs.notifyObservers(null, "domain-to-categories-map-init"); - } - } - - async uninit(shouldDeleteStore) { - if (this.#init) { - lazy.logConsole.debug("Un-initializing domain-to-categories map."); - this.#clearClient(); - this.#cancelAndNullifyTimer(); - - if (this.#store) { - if (shouldDeleteStore) { - try { - await this.#store.dropData(); - } catch (ex) { - lazy.logConsole.error(ex); - } - } - await this.#store.uninit(); - this.#store = null; - } - - lazy.logConsole.debug("Un-initialized domain-to-categories map."); - this.#init = false; - Services.obs.notifyObservers(null, "domain-to-categories-map-uninit"); - } - } - - /** - * Given a domain, find categories and relevant scores. - * - * @param {string} domain Domain to lookup. - * @returns {Array} - * An array containing categories and their respective score. If no record - * for the domain is available, return an empty array. - */ - async get(domain) { - if (!this.#store || this.#store.empty || !this.#store.ready) { - return []; - } - lazy.gCryptoHash.init(lazy.gCryptoHash.SHA256); - let bytes = new TextEncoder().encode(domain); - lazy.gCryptoHash.update(bytes, domain.length); - let hash = lazy.gCryptoHash.finish(true); - let rawValues = await this.#store.getCategories(hash); - if (rawValues?.length) { - let output = []; - // Transform data into a more readable format. - // [x, y] => { category: x, score: y } - for (let i = 0; i < rawValues.length; i += 2) { - output.push({ category: rawValues[i], score: rawValues[i + 1] }); - } - return output; - } - return []; - } - - /** - * If the map was initialized, returns the version number for the data. - * The version number is determined by the record with the highest version - * number. Even if the records have different versions, only records from the - * latest version should be available. Returns null if the map was not - * initialized. - * - * @returns {null | number} The version number. - */ - get version() { - return this.#version; - } - - /** - * Whether the store is empty of data. - * - * @returns {boolean} - */ - get empty() { - if (!this.#store) { - return true; - } - return this.#store.empty; - } - - /** - * Unit test-only function, used to override the domainToCategoriesMap so - * that tests can set it to easy to test values. - * - * @param {object} domainToCategoriesMap - * An object where the key is a hashed domain and the value is an array - * containing an arbitrary number of DomainCategoryScores. - * @param {number} version - * The version number for the store. - * @param {boolean} isDefault - * Whether the records should be considered default. - */ - async overrideMapForTests( - domainToCategoriesMap, - version = 1, - isDefault = false - ) { - if (Cu.isInAutomation || Services.env.exists("XPCSHELL_TEST_PROFILE_DIR")) { - await this.#store.init(); - await this.#store.dropData(); - await this.#store.insertObject(domainToCategoriesMap, version, isDefault); - } - } - - /** - * Given a list of records from Remote Settings, determine which ones should - * be matched based on the region. - * - * - If a set of records match the region, they should be derived from one - * source JSON file. The reason why it is split up is to make it less - * onerous to download and parse, though testing might find a single - * file to be sufficient. - * - If more than one set of records match the region, it would be from one - * set of records belonging to default mappings that apply to many regions. - * The more specific collection should override the default set. - * - * @param {Array} records - * The records from Remote Settings. - * @param {string|null} region - * The region to match. - * @returns {object|null} - */ - findRecordsForRegion(records, region) { - if (!region || !records?.length) { - return null; - } - - let regionSpecificRecords = []; - let defaultRecords = []; - for (let record of records) { - if (this.recordMatchesRegion(record, region)) { - if (record.isDefault) { - defaultRecords.push(record); - } else { - regionSpecificRecords.push(record); - } - } - } - - if (regionSpecificRecords.length) { - return { records: regionSpecificRecords, isDefault: false }; - } - - if (defaultRecords.length) { - return { records: defaultRecords, isDefault: true }; - } - - return null; - } - - /** - * Checks the record matches the region. - * - * @param {DomainToCategoriesRecord} record - * The record to check. - * @param {string|null} region - * The region the record to be matched against. - * @returns {boolean} - */ - recordMatchesRegion(record, region) { - if (!region || !record) { - return false; - } - - if (record.excludeRegions?.includes(region)) { - return false; - } - - if (record.isDefault) { - return true; - } - - if (!record.includeRegions?.includes(region)) { - return false; - } - - return true; - } - - async syncMayModifyStore(syncData, region) { - if (!syncData || !region) { - return false; - } - - let currentResult = this.findRecordsForRegion(syncData?.current, region); - if (this.#store.empty && !currentResult) { - lazy.logConsole.debug("Store was empty and there were no results."); - return false; - } - - if (!this.#store.empty && !currentResult) { - return true; - } - - let storeHasDefault = await this.#store.isDefault(); - if (storeHasDefault != currentResult.isDefault) { - return true; - } - - const recordsDifferFromStore = records => { - let result = this.findRecordsForRegion(records, region); - return result?.records.length && storeHasDefault == result.isDefault; - }; - - if ( - recordsDifferFromStore(syncData.created) || - recordsDifferFromStore(syncData.deleted) || - recordsDifferFromStore(syncData.updated.map(obj => obj.new)) - ) { - return true; - } - - return false; - } - - /** - * Connect with Remote Settings and retrieve the records associated with - * categorization. Then, check if the records match the store version. If - * no records exist, return early. If records exist but the version stored - * on the records differ from the store version, then attempt to - * empty the store and fill it with data from downloaded attachments. Only - * reuse the store if the version in each record matches the store. - */ - async #setupClientAndStore() { - if (this.#client && !this.empty) { - return; - } - lazy.logConsole.debug("Setting up domain-to-categories map."); - this.#client = lazy.RemoteSettings(TELEMETRY_CATEGORIZATION_KEY); - - this.#onSettingsSync = event => this.#sync(event.data); - this.#client.on("sync", this.#onSettingsSync); - - this.#store = new DomainToCategoriesStore(); - await this.#store.init(); - - let records = await this.#client.get(); - // Even though records don't exist, we still consider the store initialized - // since a sync event from Remote Settings could populate the store with - // records eligible for the client to download. - if (!records.length) { - lazy.logConsole.debug("No records found for domain-to-categories map."); - return; - } - - // At least one of the records must be eligible for the region. - let result = this.findRecordsForRegion(records, lazy.Region.home); - let matchingRecords = result?.records; - let matchingRecordsAreDefault = result?.isDefault; - let hasMatchingRecords = !!matchingRecords?.length; - Services.prefs.setBoolPref(CATEGORIZATION_REGION_PREF, hasMatchingRecords); - - if (!hasMatchingRecords) { - lazy.logConsole.debug( - "No domain-to-category records match the current region:", - lazy.Region.home - ); - // If no matching record was found but the store is not empty, - // the user changed their home region. - if (!this.#store.empty) { - lazy.logConsole.debug( - "Drop store because it no longer matches the home region." - ); - await this.#store.dropData(); - } - return; - } - - this.#version = this.#retrieveLatestVersion(matchingRecords); - let storeVersion = await this.#store.getVersion(); - let storeIsDefault = await this.#store.isDefault(); - if ( - storeVersion == this.#version && - !this.#store.empty && - storeIsDefault == matchingRecordsAreDefault - ) { - lazy.logConsole.debug("Reuse existing domain-to-categories map."); - Services.obs.notifyObservers( - null, - "domain-to-categories-map-update-complete" - ); - return; - } - - await this.#clearAndPopulateStore(records); - } - - #clearClient() { - if (this.#client) { - lazy.logConsole.debug("Removing Remote Settings client."); - this.#client.off("sync", this.#onSettingsSync); - this.#client = null; - this.#onSettingsSync = null; - this.#downloadRetries = 0; - } - } - - /** - * Inspects a list of records from the categorization domain bucket and finds - * the maximum version score from the set of records. Each record should have - * the same version number but if for any reason one entry has a lower - * version number, the latest version can be used to filter it out. - * - * @param {Array} records - * An array containing the records from a Remote Settings collection. - * @returns {number} - */ - #retrieveLatestVersion(records) { - return records.reduce((version, record) => { - if (record.version > version) { - return record.version; - } - return version; - }, 0); - } - - /** - * Callback when Remote Settings has indicated the collection has been - * synced. Determine if the records changed should result in updating the map, - * as some of the records changed might not affect the user's region. - * Additionally, delete any attachment for records that no longer exist. - * - * @param {object} data - * Object containing records that are current, deleted, created, or updated. - */ - async #sync(data) { - lazy.logConsole.debug("Syncing domain-to-categories with Remote Settings."); - - // Remove local files of deleted records. - let toDelete = data?.deleted.filter(d => d.attachment); - await Promise.all( - toDelete.map(record => this.#client.attachments.deleteDownloaded(record)) - ); - - let couldModify = await this.syncMayModifyStore(data, lazy.Region.home); - if (!couldModify) { - lazy.logConsole.debug( - "Domain-to-category records had no changes that matched the region." - ); - return; - } - - this.#downloadRetries = 0; - - try { - await this.#clearAndPopulateStore(data?.current); - } catch (ex) { - lazy.logConsole.error("Error populating map: ", ex); - await this.uninit(); - } - } - - /** - * Clear the existing store and populate it with attachments found in the - * records. If no attachments are found, or no record containing an - * attachment contained the latest version, then nothing will change. - * - * @param {Array} records - * The records containing attachments. - * @throws {Error} - * Will throw if it was not able to drop the store data, or it was unable - * to insert data into the store. - */ - async #clearAndPopulateStore(records) { - // If we don't have a handle to a store, it would mean that it was removed - // during an uninitialization process. - if (!this.#store) { - lazy.logConsole.debug( - "Could not populate store because no store was available." - ); - return; - } - - if (!this.#store.ready) { - lazy.logConsole.debug( - "Could not populate store because it was not ready." - ); - return; - } - - // Empty table so that if there are errors in the download process, callers - // querying the map won't use information we know is probably outdated. - await this.#store.dropData(); - - this.#version = null; - this.#cancelAndNullifyTimer(); - - let result = this.findRecordsForRegion(records, lazy.Region.home); - let recordsMatchingRegion = result?.records; - let isDefault = result?.isDefault; - let hasMatchingRecords = !!recordsMatchingRegion?.length; - Services.prefs.setBoolPref(CATEGORIZATION_REGION_PREF, hasMatchingRecords); - - // A collection with no records is still a valid init state. - if (!records?.length) { - lazy.logConsole.debug("No records found for domain-to-categories map."); - return; - } - - if (!hasMatchingRecords) { - lazy.logConsole.debug( - "No domain-to-category records match the current region:", - lazy.Region.home - ); - return; - } - - let fileContents = []; - let start = Cu.now(); - for (let record of recordsMatchingRegion) { - let fetchedAttachment; - // Downloading attachments can fail. - try { - fetchedAttachment = await this.#client.attachments.download(record); - } catch (ex) { - lazy.logConsole.error("Could not download file:", ex); - this.#createTimerToPopulateMap(); - return; - } - fileContents.push(fetchedAttachment.buffer); - } - ChromeUtils.addProfilerMarker( - "SearchSERPTelemetry.#clearAndPopulateStore", - start, - "Download attachments." - ); - - this.#version = this.#retrieveLatestVersion(recordsMatchingRegion); - if (!this.#version) { - lazy.logConsole.debug("Could not find a version number for any record."); - return; - } - - await this.#store.insertFileContents( - fileContents, - this.#version, - isDefault - ); - - lazy.logConsole.debug("Finished updating domain-to-categories store."); - Services.obs.notifyObservers( - null, - "domain-to-categories-map-update-complete" - ); - } - - #cancelAndNullifyTimer() { - if (this.#downloadTimer) { - lazy.logConsole.debug("Cancel and nullify download timer."); - this.#downloadTimer.cancel(); - this.#downloadTimer = null; - } - } - - #createTimerToPopulateMap() { - if ( - this.#downloadRetries >= - TELEMETRY_CATEGORIZATION_DOWNLOAD_SETTINGS.maxTriesPerSession || - !this.#client - ) { - return; - } - if (!this.#downloadTimer) { - this.#downloadTimer = Cc["@mozilla.org/timer;1"].createInstance( - Ci.nsITimer - ); - } - lazy.logConsole.debug("Create timer to retry downloading attachments."); - let delay = - TELEMETRY_CATEGORIZATION_DOWNLOAD_SETTINGS.base + - randomInteger( - TELEMETRY_CATEGORIZATION_DOWNLOAD_SETTINGS.minAdjust, - TELEMETRY_CATEGORIZATION_DOWNLOAD_SETTINGS.maxAdjust - ); - this.#downloadTimer.initWithCallback( - async () => { - this.#downloadRetries += 1; - let records = await this.#client.get(); - try { - await this.#clearAndPopulateStore(records); - } catch (ex) { - lazy.logConsole.error("Error populating store: ", ex); - await this.uninit(); - } - }, - delay, - Ci.nsITimer.TYPE_ONE_SHOT - ); - } -} - -/** - * Handles the storage of data containing domains to categories. - */ -export class DomainToCategoriesStore { - #init = false; - - /** - * The connection to the store. - * - * @type {object | null} - */ - #connection = null; - - /** - * Reference for the shutdown blocker in case we need to remove it before - * shutdown. - * - * @type {Function | null} - */ - #asyncShutdownBlocker = null; - - /** - * Whether the store is empty of data. - * - * @type {boolean} - */ - #empty = true; - - /** - * For a particular subset of errors, we'll attempt to rebuild the database - * from scratch. - */ - #rebuildableErrors = ["NS_ERROR_FILE_CORRUPTED"]; - - /** - * Initializes the store. If the store is initialized it should have cached - * a connection to the store and ensured the store exists. - */ - async init() { - if (this.#init) { - return; - } - lazy.logConsole.debug("Initializing domain-to-categories store."); - - // Attempts to cache a connection to the store. - // If a failure occured, try to re-build the store. - let rebuiltStore = false; - try { - await this.#initConnection(); - } catch (ex1) { - lazy.logConsole.error(`Error initializing a connection: ${ex1}`); - if (this.#rebuildableErrors.includes(ex1.name)) { - try { - await this.#rebuildStore(); - } catch (ex2) { - await this.#closeConnection(); - lazy.logConsole.error(`Could not rebuild store: ${ex2}`); - return; - } - rebuiltStore = true; - } - } - - // If we don't have a connection, bail because the browser could be - // shutting down ASAP, or re-creating the store is impossible. - if (!this.#connection) { - lazy.logConsole.debug( - "Bailing from DomainToCategoriesStore.init because connection doesn't exist." - ); - return; - } - - // If we weren't forced to re-build the store, we only have the connection. - // We want to ensure the store exists so calls to public methods can pass - // without throwing errors due to the absence of the store. - if (!rebuiltStore) { - try { - await this.#initSchema(); - } catch (ex) { - lazy.logConsole.error(`Error trying to create store: ${ex}`); - await this.#closeConnection(); - return; - } - } - - lazy.logConsole.debug("Initialized domain-to-categories store."); - this.#init = true; - } - - async uninit() { - if (this.#init) { - lazy.logConsole.debug("Un-initializing domain-to-categories store."); - await this.#closeConnection(); - this.#asyncShutdownBlocker = null; - lazy.logConsole.debug("Un-initialized domain-to-categories store."); - } - } - - /** - * Whether the store has an open connection to the physical store. - * - * @returns {boolean} - */ - get ready() { - return this.#init; - } - - /** - * Whether the store is devoid of data. - * - * @returns {boolean} - */ - get empty() { - return this.#empty; - } - - /** - * Clears information in the store. If dropping data encountered a failure, - * try to delete the file containing the store and re-create it. - * - * @throws {Error} Will throw if it was unable to clear information from the - * store. - */ - async dropData() { - if (!this.#connection) { - return; - } - let tableExists = await this.#connection.tableExists( - CATEGORIZATION_SETTINGS.STORE_NAME - ); - if (tableExists) { - lazy.logConsole.debug("Drop domain_to_categories."); - // This can fail if the permissions of the store are read-only. - await this.#connection.executeTransaction(async () => { - await this.#connection.execute(`DROP TABLE domain_to_categories`); - const createDomainToCategoriesTable = ` - CREATE TABLE IF NOT EXISTS - domain_to_categories ( - string_id - TEXT PRIMARY KEY NOT NULL, - categories - TEXT - ); - `; - await this.#connection.execute(createDomainToCategoriesTable); - await this.#connection.execute(`DELETE FROM moz_meta`); - await this.#connection.executeCached( - ` - INSERT INTO - moz_meta (key, value) - VALUES - (:key, :value) - ON CONFLICT DO UPDATE SET - value = :value - `, - { key: "version", value: 0 } - ); - }); - - this.#empty = true; - } - } - - /** - * Given file contents, try moving them into the store. If a failure occurs, - * it will attempt to drop existing data to ensure callers aren't accessing - * a partially filled store. - * - * @param {Array} fileContents - * Contents to convert. - * @param {number} version - * The version for the store. - * @param {boolean} isDefault - * Whether the file contents are from a default collection. - * @throws {Error} - * Will throw if the insertion failed and dropData was unable to run - * successfully. - */ - async insertFileContents(fileContents, version, isDefault = false) { - if (!this.#init || !fileContents?.length || !version) { - return; - } - - try { - await this.#insert(fileContents, version, isDefault); - } catch (ex) { - lazy.logConsole.error(`Could not insert file contents: ${ex}`); - await this.dropData(); - } - } - - /** - * Convenience function to make it trivial to insert Javascript objects into - * the store. This avoids having to set up the collection in Remote Settings. - * - * @param {object} domainToCategoriesMap - * An object whose keys should be hashed domains with values containing - * an array of integers. - * @param {number} version - * The version for the store. - * @param {boolean} isDefault - * Whether the mappings are from a default record. - * @returns {boolean} - * Whether the operation was successful. - */ - async insertObject(domainToCategoriesMap, version, isDefault) { - if (!Cu.isInAutomation || !this.#init) { - return false; - } - let buffer = new TextEncoder().encode( - JSON.stringify(domainToCategoriesMap) - ).buffer; - await this.insertFileContents([buffer], version, isDefault); - return true; - } - - /** - * Retrieves domains mapped to the key. - * - * @param {string} key - * The value to lookup in the store. - * @returns {Array} - * An array of numbers corresponding to the category and score. If the key - * does not exist in the store or the store is having issues retrieving the - * value, returns an empty array. - */ - async getCategories(key) { - if (!this.#init) { - return []; - } - - let rows; - try { - rows = await this.#connection.executeCached( - ` - SELECT - categories - FROM - domain_to_categories - WHERE - string_id = :key - `, - { - key, - } - ); - } catch (ex) { - lazy.logConsole.error(`Could not retrieve from the store: ${ex}`); - return []; - } - - if (!rows.length) { - return []; - } - return JSON.parse(rows[0].getResultByName("categories")) ?? []; - } - - /** - * Retrieves the version number of the store. - * - * @returns {number} - * The version number. Returns 0 if the version was never set or if there - * was an issue accessing the version number. - */ - async getVersion() { - if (this.#connection) { - let rows; - try { - rows = await this.#connection.executeCached( - ` - SELECT - value - FROM - moz_meta - WHERE - key = "version" - ` - ); - } catch (ex) { - lazy.logConsole.error(`Could not retrieve version of the store: ${ex}`); - return 0; - } - if (rows.length) { - return parseInt(rows[0].getResultByName("value")) ?? 0; - } - } - return 0; - } - - /** - * Whether the data inside the store was derived from a default set of - * records. - * - * @returns {boolean} - */ - async isDefault() { - if (this.#connection) { - let rows; - try { - rows = await this.#connection.executeCached( - ` - SELECT - value - FROM - moz_meta - WHERE - key = "is_default" - ` - ); - } catch (ex) { - lazy.logConsole.error( - `Could not retrieve if the store is using default records: ${ex}` - ); - return false; - } - if (rows.length && parseInt(rows[0].getResultByName("value")) == 1) { - return true; - } - } - return false; - } - - /** - * Test only function allowing tests to delete the store. - */ - async testDelete() { - if (Cu.isInAutomation) { - await this.#closeConnection(); - await this.#delete(); - } - } - - /** - * If a connection is available, close it and remove shutdown blockers. - */ - async #closeConnection() { - this.#init = false; - this.#empty = true; - if (this.#asyncShutdownBlocker) { - lazy.Sqlite.shutdown.removeBlocker(this.#asyncShutdownBlocker); - this.#asyncShutdownBlocker = null; - } - - if (this.#connection) { - lazy.logConsole.debug("Closing connection."); - // An error could occur while closing the connection. We suppress the - // error since it is not a critical part of the browser. - try { - await this.#connection.close(); - } catch (ex) { - lazy.logConsole.error(ex); - } - this.#connection = null; - } - } - - /** - * Initialize the schema for the store. - * - * @throws {Error} - * Will throw if a permissions error prevents creating the store. - */ - async #initSchema() { - if (!this.#connection) { - return; - } - lazy.logConsole.debug("Create store."); - // Creation can fail if the store is read only. - await this.#connection.executeTransaction(async () => { - // Let outer try block handle the exception. - const createDomainToCategoriesTable = ` - CREATE TABLE IF NOT EXISTS - domain_to_categories ( - string_id - TEXT PRIMARY KEY NOT NULL, - categories - TEXT - ) WITHOUT ROWID; - `; - await this.#connection.execute(createDomainToCategoriesTable); - const createMetaTable = ` - CREATE TABLE IF NOT EXISTS - moz_meta ( - key - TEXT PRIMARY KEY NOT NULL, - value - INTEGER - ) WITHOUT ROWID; - `; - await this.#connection.execute(createMetaTable); - await this.#connection.setSchemaVersion( - CATEGORIZATION_SETTINGS.STORE_SCHEMA - ); - }); - - let rows = await this.#connection.executeCached( - "SELECT count(*) = 0 FROM domain_to_categories" - ); - this.#empty = !!rows[0].getResultByIndex(0); - } - - /** - * Attempt to delete the store. - * - * @throws {Error} - * Will throw if the permissions for the file prevent its deletion. - */ - async #delete() { - lazy.logConsole.debug("Attempt to delete the store."); - try { - await IOUtils.remove( - PathUtils.join( - PathUtils.profileDir, - CATEGORIZATION_SETTINGS.STORE_FILE - ), - { ignoreAbsent: true } - ); - } catch (ex) { - lazy.logConsole.error(ex); - } - this.#empty = true; - lazy.logConsole.debug("Store was deleted."); - } - - /** - * Tries to establish a connection to the store. - * - * @throws {Error} - * Will throw if there was an issue establishing a connection or adding - * adding a shutdown blocker. - */ - async #initConnection() { - if (this.#connection) { - return; - } - - // This could fail if the store is corrupted. - this.#connection = await lazy.Sqlite.openConnection({ - path: PathUtils.join( - PathUtils.profileDir, - CATEGORIZATION_SETTINGS.STORE_FILE - ), - }); - - await this.#connection.execute("PRAGMA journal_mode = TRUNCATE"); - - this.#asyncShutdownBlocker = async () => { - await this.#connection.close(); - this.#connection = null; - }; - - // This could fail if we're adding it during shutdown. In this case, - // don't throw but close the connection. - try { - lazy.Sqlite.shutdown.addBlocker( - "SearchSERPTelemetry:DomainToCategoriesSqlite closing", - this.#asyncShutdownBlocker - ); - } catch (ex) { - lazy.logConsole.error(ex); - await this.#closeConnection(); - } - } - - /** - * Inserts into the store. - * - * @param {Array} fileContents - * The data that should be converted and inserted into the store. - * @param {number} version - * The version number that should be inserted into the store. - * @param {boolean} isDefault - * Whether the file contents are a default set of records. - * @throws {Error} - * Will throw if a connection is not present, if the store is not - * able to be updated (permissions error, corrupted file), or there is - * something wrong with the file contents. - */ - async #insert(fileContents, version, isDefault) { - let start = Cu.now(); - await this.#connection.executeTransaction(async () => { - lazy.logConsole.debug("Insert into domain_to_categories table."); - for (let fileContent of fileContents) { - await this.#connection.executeCached( - ` - INSERT INTO - domain_to_categories (string_id, categories) - SELECT - json_each.key AS string_id, - json_each.value AS categories - FROM - json_each(json(:obj)) - `, - { - obj: new TextDecoder().decode(fileContent), - } - ); - } - // Once the insertions have successfully completed, update the version. - await this.#connection.executeCached( - ` - INSERT INTO - moz_meta (key, value) - VALUES - (:key, :value) - ON CONFLICT DO UPDATE SET - value = :value - `, - { key: "version", value: version } - ); - if (isDefault) { - await this.#connection.executeCached( - ` - INSERT INTO - moz_meta (key, value) - VALUES - (:key, :value) - ON CONFLICT DO UPDATE SET - value = :value - `, - { key: "is_default", value: 1 } - ); - } - }); - ChromeUtils.addProfilerMarker( - "DomainToCategoriesSqlite.#insert", - start, - "Move file contents into table." - ); - - if (fileContents?.length) { - this.#empty = false; - } - } - - /** - * Deletes and re-build's the store. Used in cases where we encounter a - * failure and we want to try fixing the error by starting with an - * entirely fresh store. - * - * @throws {Error} - * Will throw if a connection could not be established, if it was - * unable to delete the store, or it was unable to build a new store. - */ - async #rebuildStore() { - lazy.logConsole.debug("Try rebuilding store."); - // Step 1. Close all connections. - await this.#closeConnection(); - - // Step 2. Delete the existing store. - await this.#delete(); - - // Step 3. Re-establish the connection. - await this.#initConnection(); - - // Step 4. If a connection exists, try creating the store. - await this.#initSchema(); - } -} - -function randomInteger(min, max) { - return Math.floor(Math.random() * (max - min + 1)) + min; -} - -export var SearchSERPDomainToCategoriesMap = new DomainToCategoriesMap(); export var SearchSERPTelemetry = new TelemetryHandler(); -export var SearchSERPCategorization = new SERPCategorizer(); -export var SERPCategorizationRecorder = new CategorizationRecorder(); -export var SearchSERPCategorizationEventScheduler = - new CategorizationEventScheduler(); diff --git a/browser/components/search/moz.build b/browser/components/search/moz.build index c535a2c4c46..d1c924d051a 100644 --- a/browser/components/search/moz.build +++ b/browser/components/search/moz.build @@ -10,6 +10,7 @@ EXTRA_JS_MODULES += [ "SearchOneOffs.sys.mjs", "SearchSERPTelemetry.sys.mjs", "SearchUIUtils.sys.mjs", + "SERPCategorization.sys.mjs", ] BROWSER_CHROME_MANIFESTS += [ diff --git a/browser/components/search/test/browser/telemetry/browser_search_glean_serp_event_telemetry_categorization_enabled_by_pref.js b/browser/components/search/test/browser/telemetry/browser_search_glean_serp_event_telemetry_categorization_enabled_by_pref.js index 425b1bc9f62..5c2c7b551a8 100644 --- a/browser/components/search/test/browser/telemetry/browser_search_glean_serp_event_telemetry_categorization_enabled_by_pref.js +++ b/browser/components/search/test/browser/telemetry/browser_search_glean_serp_event_telemetry_categorization_enabled_by_pref.js @@ -9,8 +9,7 @@ const TELEMETRY_PREF = "browser.search.serpEventTelemetryCategorization.enabled"; ChromeUtils.defineESModuleGetters(lazy, { - SearchSERPDomainToCategoriesMap: - "resource:///modules/SearchSERPTelemetry.sys.mjs", + SERPDomainToCategoriesMap: "resource:///modules/SERPCategorization.sys.mjs", }); XPCOMUtils.defineLazyPreferenceGetter( @@ -166,7 +165,7 @@ add_task(async function test_enable_experiment_when_pref_is_not_enabled() { ); Assert.ok( - lazy.SearchSERPDomainToCategoriesMap.empty, + lazy.SERPDomainToCategoriesMap.empty, "Domain to categories map should be empty." ); diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_categorization_experiment_info.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_categorization_experiment_info.js index 119ce61a744..df366b9eb9c 100644 --- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_categorization_experiment_info.js +++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_categorization_experiment_info.js @@ -10,11 +10,11 @@ */ ChromeUtils.defineESModuleGetters(this, { - CATEGORIZATION_SETTINGS: "resource:///modules/SearchSERPTelemetry.sys.mjs", + CATEGORIZATION_SETTINGS: "resource:///modules/SERPCategorization.sys.mjs", ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs", ExperimentFakes: "resource://testing-common/NimbusTestUtils.sys.mjs", SearchSERPTelemetry: "resource:///modules/SearchSERPTelemetry.sys.mjs", - SERPCategorizationRecorder: "resource:///modules/SearchSERPTelemetry.sys.mjs", + SERPCategorizationRecorder: "resource:///modules/SERPCategorization.sys.mjs", }); const TEST_PROVIDER_INFO = [ diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_categorization_missing_impression.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_categorization_missing_impression.js index 4eb96b68f8a..679bca9cf5c 100644 --- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_categorization_missing_impression.js +++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_categorization_missing_impression.js @@ -108,7 +108,7 @@ add_task(async function test_count_incremented_if_map_is_not_downloaded() { resetTelemetry(); // Clear the existing domain-to-categories map. - await SearchSERPDomainToCategoriesMap.uninit({ deleteMap: true }); + await SERPDomainToCategoriesMap.uninit({ deleteMap: true }); let sandbox = sinon.createSandbox(); sandbox @@ -120,7 +120,7 @@ add_task(async function test_count_incremented_if_map_is_not_downloaded() { msg.wrappedJSObject.arguments[0].includes("Could not download file:") ); }); - await SearchSERPDomainToCategoriesMap.init(); + await SERPDomainToCategoriesMap.init(); info("Wait for download error."); await downloadError; info("Domain-to-categories map unsuccessfully downloaded."); @@ -158,7 +158,7 @@ add_task(async function test_threshold_reached() { // Simulate a broken domain-to-categories map. let sandbox = sinon.createSandbox(); - sandbox.stub(SearchSERPDomainToCategoriesMap, "get").returns([]); + sandbox.stub(SERPDomainToCategoriesMap, "get").returns([]); let submitted = false; GleanPings.serpCategorization.testBeforeNextSubmit(reason => { diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_download_timer.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_download_timer.js index 694900912d4..edb51d0c53b 100644 --- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_download_timer.js +++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_download_timer.js @@ -11,9 +11,8 @@ ChromeUtils.defineESModuleGetters(this, { TELEMETRY_CATEGORIZATION_DOWNLOAD_SETTINGS: - "resource:///modules/SearchSERPTelemetry.sys.mjs", - SearchSERPDomainToCategoriesMap: - "resource:///modules/SearchSERPTelemetry.sys.mjs", + "resource:///modules/SERPCategorization.sys.mjs", + SERPDomainToCategoriesMap: "resource:///modules/SERPCategorization.sys.mjs", SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs", }); @@ -233,7 +232,7 @@ add_task(async function test_download_after_multiple_failures() { Assert.equal(consoleObserved, false, "Encountered download failure"); Assert.equal(timeout, true, "Timeout occured"); - Assert.ok(SearchSERPDomainToCategoriesMap.empty, "Map is empty"); + Assert.ok(SERPDomainToCategoriesMap.empty, "Map is empty"); // Clean up. await SpecialPowers.popPrefEnv(); @@ -285,7 +284,7 @@ add_task(async function test_cancel_download_timer() { await Promise.race([firstPromise, secondPromise]); Assert.equal(consoleObserved, false, "Encountered download failure"); Assert.equal(timeout, true, "Timeout occured"); - Assert.ok(SearchSERPDomainToCategoriesMap.empty, "Map is empty"); + Assert.ok(SERPDomainToCategoriesMap.empty, "Map is empty"); // Clean up. await resetCategorizationCollection(record); diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_ping_submission.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_ping_submission.js index 4cf1daf830b..ede53dd0f1f 100644 --- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_ping_submission.js +++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_ping_submission.js @@ -9,14 +9,17 @@ * a test of the ping's submission upon startup.) */ +// Can fail in TV mode. +requestLongerTimeout(2); + ChromeUtils.defineESModuleGetters(this, { - CATEGORIZATION_SETTINGS: "resource:///modules/SearchSERPTelemetry.sys.mjs", - DomainToCategoriesStore: "resource:///modules/SearchSERPTelemetry.sys.mjs", + CATEGORIZATION_SETTINGS: "resource:///modules/SERPCategorization.sys.mjs", + DomainToCategoriesStore: "resource:///modules/SERPCategorization.sys.mjs", RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", SearchSERPTelemetry: "resource:///modules/SearchSERPTelemetry.sys.mjs", - SERPCategorizationRecorder: "resource:///modules/SearchSERPTelemetry.sys.mjs", + SERPCategorizationRecorder: "resource:///modules/SERPCategorization.sys.mjs", TELEMETRY_CATEGORIZATION_KEY: - "resource:///modules/SearchSERPTelemetry.sys.mjs", + "resource:///modules/SERPCategorization.sys.mjs", }); const TEST_PROVIDER_INFO = [ @@ -306,7 +309,7 @@ add_task(async function test_count_incremented_if_store_is_not_created() { resetTelemetry(); // Clear the existing domain-to-categories map. - await SearchSERPDomainToCategoriesMap.uninit({ deleteMap: true }); + await SERPDomainToCategoriesMap.uninit({ deleteMap: true }); let sandbox = sinon.createSandbox(); sandbox @@ -314,7 +317,7 @@ add_task(async function test_count_incremented_if_store_is_not_created() { .throws(new Error()); // Initializing should fail and cause the component to un-initialize. let promise = waitForDomainToCategoriesUninit(); - await SearchSERPDomainToCategoriesMap.init(); + await SERPDomainToCategoriesMap.init(); await promise; info("Store for the domain-to-categories map not created successfully."); @@ -331,6 +334,6 @@ add_task(async function test_count_incremented_if_store_is_not_created() { ); sandbox.restore(); - await SearchSERPDomainToCategoriesMap.init(); + await SERPDomainToCategoriesMap.init(); await BrowserTestUtils.removeTab(tab); }); diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting.js index 18af7fffe8f..db3c1170783 100644 --- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting.js +++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting.js @@ -8,9 +8,8 @@ */ ChromeUtils.defineESModuleGetters(this, { - CATEGORIZATION_SETTINGS: "resource:///modules/SearchSERPTelemetry.sys.mjs", - SearchSERPDomainToCategoriesMap: - "resource:///modules/SearchSERPTelemetry.sys.mjs", + CATEGORIZATION_SETTINGS: "resource:///modules/SERPCategorization.sys.mjs", + SERPDomainToCategoriesMap: "resource:///modules/SERPCategorization.sys.mjs", }); const TEST_PROVIDER_INFO = [ @@ -180,8 +179,8 @@ add_task(async function test_no_reporting_if_download_failure() { // The map is going to attempt to redo a download. There are other tests that // do it, so instead reset the map so later tests don't get interrupted by // a sync event caused by this test. - await SearchSERPDomainToCategoriesMap.uninit(); - await SearchSERPDomainToCategoriesMap.init(); + await SERPDomainToCategoriesMap.uninit(); + await SERPDomainToCategoriesMap.init(); }); add_task(async function test_no_reporting_if_no_records() { diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting_timer.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting_timer.js index 59a3c15ef9b..330474a6271 100644 --- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting_timer.js +++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting_timer.js @@ -11,8 +11,8 @@ */ ChromeUtils.defineESModuleGetters(this, { - SearchSERPCategorizationEventScheduler: - "resource:///modules/SearchSERPTelemetry.sys.mjs", + SERPCategorizationEventScheduler: + "resource:///modules/SERPCategorization.sys.mjs", }); const TEST_PROVIDER_INFO = [ @@ -74,8 +74,8 @@ add_setup(async function () { "browser.search.serpEventTelemetryCategorization.enabled" ) ) { - SearchSERPCategorizationEventScheduler.uninit(); - SearchSERPCategorizationEventScheduler.init(); + SERPCategorizationEventScheduler.uninit(); + SERPCategorizationEventScheduler.init(); } await waitForIdle(); @@ -98,8 +98,8 @@ add_setup(async function () { await waitForDomainToCategoriesUninit(); } else { // The scheduler uses the mock idle service. - SearchSERPCategorizationEventScheduler.uninit(); - SearchSERPCategorizationEventScheduler.init(); + SERPCategorizationEventScheduler.uninit(); + SERPCategorizationEventScheduler.init(); } SearchSERPTelemetry.overrideSearchTelemetryForTests(); resetTelemetry(); diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting_timer_wakeup.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting_timer_wakeup.js index b824ec9817d..98670c44421 100644 --- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting_timer_wakeup.js +++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_domain_categorization_reporting_timer_wakeup.js @@ -9,11 +9,11 @@ */ ChromeUtils.defineESModuleGetters(this, { - CATEGORIZATION_SETTINGS: "resource:///modules/SearchSERPTelemetry.sys.mjs", - SearchSERPCategorizationEventScheduler: - "resource:///modules/SearchSERPTelemetry.sys.mjs", + CATEGORIZATION_SETTINGS: "resource:///modules/SERPCategorization.sys.mjs", + SERPCategorizationEventScheduler: + "resource:///modules/SERPCategorization.sys.mjs", TELEMETRY_CATEGORIZATION_KEY: - "resource:///modules/SearchSERPTelemetry.sys.mjs", + "resource:///modules/SERPCategorization.sys.mjs", }); const TEST_PROVIDER_INFO = [ @@ -79,8 +79,8 @@ add_setup(async function () { "browser.search.serpEventTelemetryCategorization.enabled" ) ) { - SearchSERPCategorizationEventScheduler.uninit(); - SearchSERPCategorizationEventScheduler.init(); + SERPCategorizationEventScheduler.uninit(); + SERPCategorizationEventScheduler.init(); } await waitForIdle(); @@ -103,8 +103,8 @@ add_setup(async function () { await waitForDomainToCategoriesUninit(); } else { // The scheduler uses the mock idle service. - SearchSERPCategorizationEventScheduler.uninit(); - SearchSERPCategorizationEventScheduler.init(); + SERPCategorizationEventScheduler.uninit(); + SERPCategorizationEventScheduler.init(); } CATEGORIZATION_SETTINGS.WAKE_TIMEOUT_MS = oldWakeTimeout; SearchSERPTelemetry.overrideSearchTelemetryForTests(); diff --git a/browser/components/search/test/browser/telemetry/browser_search_telemetry_remote_settings_sync.js b/browser/components/search/test/browser/telemetry/browser_search_telemetry_remote_settings_sync.js index a8a52ba7946..1cda0ddd27e 100644 --- a/browser/components/search/test/browser/telemetry/browser_search_telemetry_remote_settings_sync.js +++ b/browser/components/search/test/browser/telemetry/browser_search_telemetry_remote_settings_sync.js @@ -15,7 +15,8 @@ ChromeUtils.defineESModuleGetters(this, { ADLINK_CHECK_TIMEOUT_MS: "resource:///modules/SearchSERPTelemetry.sys.mjs", RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", SEARCH_TELEMETRY_SHARED: "resource:///modules/SearchSERPTelemetry.sys.mjs", - SearchSERPCategorization: "resource:///modules/SearchSERPTelemetry.sys.mjs", + SearchSERPCategorization: + "resource:///modules/SearchSERPCategorization.sys.mjs", SearchSERPDomainToCategoriesMap: "resource:///modules/SearchSERPTelemetry.sys.mjs", SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs", diff --git a/browser/components/search/test/browser/telemetry/head.js b/browser/components/search/test/browser/telemetry/head.js index cabf07e2969..232ac49a27f 100644 --- a/browser/components/search/test/browser/telemetry/head.js +++ b/browser/components/search/test/browser/telemetry/head.js @@ -5,24 +5,23 @@ ChromeUtils.defineESModuleGetters(this, { ADLINK_CHECK_TIMEOUT_MS: "resource:///actors/SearchSERPTelemetryChild.sys.mjs", BrowserSearchTelemetry: "resource:///modules/BrowserSearchTelemetry.sys.mjs", - CATEGORIZATION_SETTINGS: "resource:///modules/SearchSERPTelemetry.sys.mjs", + CATEGORIZATION_SETTINGS: "resource:///modules/SERPCategorization.sys.mjs", CustomizableUITestUtils: "resource://testing-common/CustomizableUITestUtils.sys.mjs", Region: "resource://gre/modules/Region.sys.mjs", RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", SEARCH_TELEMETRY_SHARED: "resource:///modules/SearchSERPTelemetry.sys.mjs", - SearchSERPDomainToCategoriesMap: - "resource:///modules/SearchSERPTelemetry.sys.mjs", SearchSERPTelemetry: "resource:///modules/SearchSERPTelemetry.sys.mjs", SearchSERPTelemetryUtils: "resource:///modules/SearchSERPTelemetry.sys.mjs", SearchTestUtils: "resource://testing-common/SearchTestUtils.sys.mjs", SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs", - SERPCategorizationRecorder: "resource:///modules/SearchSERPTelemetry.sys.mjs", + SERPCategorizationRecorder: "resource:///modules/SERPCategorization.sys.mjs", + SERPDomainToCategoriesMap: "resource:///modules/SERPCategorization.sys.mjs", sinon: "resource://testing-common/Sinon.sys.mjs", SPA_ADLINK_CHECK_TIMEOUT_MS: "resource:///modules/SearchSERPTelemetry.sys.mjs", TELEMETRY_CATEGORIZATION_KEY: - "resource:///modules/SearchSERPTelemetry.sys.mjs", + "resource:///modules/SERPCategorization.sys.mjs", TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs", VISIBILITY_THRESHOLD: "resource:///actors/SearchSERPTelemetryChild.sys.mjs", }); diff --git a/browser/components/search/test/marionette/telemetry/test_ping_submitted.py b/browser/components/search/test/marionette/telemetry/test_ping_submitted.py index e858a1bb622..2040c0a46c2 100644 --- a/browser/components/search/test/marionette/telemetry/test_ping_submitted.py +++ b/browser/components/search/test/marionette/telemetry/test_ping_submitted.py @@ -46,7 +46,7 @@ class TestPingSubmitted(MarionetteTestCase): # Record an event for the ping to eventually submit. self.marionette.execute_script( """ - const { SERPCategorizationRecorder } = ChromeUtils.importESModule("resource:///modules/SearchSERPTelemetry.sys.mjs"); + const { SERPCategorizationRecorder } = ChromeUtils.importESModule("resource:///modules/SERPCategorization.sys.mjs"); SERPCategorizationRecorder.recordCategorizationTelemetry({ organic_category: "3", organic_num_domains: "1", diff --git a/browser/components/search/test/unit/test_domain_to_categories_store.js b/browser/components/search/test/unit/test_domain_to_categories_store.js index 8f73bce20c3..bbcf4cb68a6 100644 --- a/browser/components/search/test/unit/test_domain_to_categories_store.js +++ b/browser/components/search/test/unit/test_domain_to_categories_store.js @@ -7,8 +7,8 @@ */ ChromeUtils.defineESModuleGetters(this, { - CATEGORIZATION_SETTINGS: "resource:///modules/SearchSERPTelemetry.sys.mjs", - DomainToCategoriesStore: "resource:///modules/SearchSERPTelemetry.sys.mjs", + CATEGORIZATION_SETTINGS: "resource:///modules/SERPCategorization.sys.mjs", + DomainToCategoriesStore: "resource:///modules/SERPCategorization.sys.mjs", sinon: "resource://testing-common/Sinon.sys.mjs", Sqlite: "resource://gre/modules/Sqlite.sys.mjs", }); diff --git a/browser/components/search/test/unit/test_search_telemetry_categorization_logic.js b/browser/components/search/test/unit/test_search_telemetry_categorization_logic.js index 38a3f43d468..4374b1bfec6 100644 --- a/browser/components/search/test/unit/test_search_telemetry_categorization_logic.js +++ b/browser/components/search/test/unit/test_search_telemetry_categorization_logic.js @@ -9,10 +9,9 @@ "use strict"; ChromeUtils.defineESModuleGetters(this, { - SearchSERPCategorization: "resource:///modules/SearchSERPTelemetry.sys.mjs", - SearchSERPDomainToCategoriesMap: - "resource:///modules/SearchSERPTelemetry.sys.mjs", - SearchSERPTelemetryUtils: "resource:///modules/SearchSERPTelemetry.sys.mjs", + SERPCategorization: "resource:///modules/SERPCategorization.sys.mjs", + SERPDomainToCategoriesMap: "resource:///modules/SERPCategorization.sys.mjs", + CATEGORIZATION_SETTINGS: "resource:///modules/SERPCategorization.sys.mjs", }); ChromeUtils.defineLazyGetter(this, "gCryptoHash", () => { @@ -115,11 +114,11 @@ add_setup(async () => { "browser.search.serpEventTelemetryCategorization.enabled", true ); - await SearchSERPDomainToCategoriesMap.init(); + await SERPDomainToCategoriesMap.init(); }); add_task(async function test_categorization_simple() { - await SearchSERPDomainToCategoriesMap.overrideMapForTests( + await SERPDomainToCategoriesMap.overrideMapForTests( TEST_DOMAIN_TO_CATEGORIES_MAP_SIMPLE ); @@ -137,7 +136,7 @@ add_task(async function test_categorization_simple() { ]); let resultsToReport = - await SearchSERPCategorization.applyCategorizationLogic(domains); + await SERPCategorization.applyCategorizationLogic(domains); Assert.deepEqual( resultsToReport, @@ -147,7 +146,7 @@ add_task(async function test_categorization_simple() { }); add_task(async function test_categorization_inconclusive() { - await SearchSERPDomainToCategoriesMap.overrideMapForTests( + await SERPDomainToCategoriesMap.overrideMapForTests( TEST_DOMAIN_TO_CATEGORIES_MAP_INCONCLUSIVE ); @@ -165,12 +164,12 @@ add_task(async function test_categorization_inconclusive() { ]); let resultsToReport = - await SearchSERPCategorization.applyCategorizationLogic(domains); + await SERPCategorization.applyCategorizationLogic(domains); Assert.deepEqual( resultsToReport, { - category: SearchSERPTelemetryUtils.CATEGORIZATION.INCONCLUSIVE, + category: CATEGORIZATION_SETTINGS.INCONCLUSIVE, num_domains: 10, num_inconclusive: 10, num_unknown: 0, @@ -182,7 +181,7 @@ add_task(async function test_categorization_inconclusive() { add_task(async function test_categorization_unknown() { // Reusing TEST_DOMAIN_TO_CATEGORIES_MAP_SIMPLE since none of this task's // domains will be keys within it. - await SearchSERPDomainToCategoriesMap.overrideMapForTests( + await SERPDomainToCategoriesMap.overrideMapForTests( TEST_DOMAIN_TO_CATEGORIES_MAP_SIMPLE ); @@ -200,12 +199,12 @@ add_task(async function test_categorization_unknown() { ]); let resultsToReport = - await SearchSERPCategorization.applyCategorizationLogic(domains); + await SERPCategorization.applyCategorizationLogic(domains); Assert.deepEqual( resultsToReport, { - category: SearchSERPTelemetryUtils.CATEGORIZATION.INCONCLUSIVE, + category: CATEGORIZATION_SETTINGS.INCONCLUSIVE, num_domains: 10, num_inconclusive: 0, num_unknown: 10, @@ -215,7 +214,7 @@ add_task(async function test_categorization_unknown() { }); add_task(async function test_categorization_unknown_and_inconclusive() { - await SearchSERPDomainToCategoriesMap.overrideMapForTests( + await SERPDomainToCategoriesMap.overrideMapForTests( TEST_DOMAIN_TO_CATEGORIES_MAP_UNKNOWN_AND_INCONCLUSIVE ); @@ -233,12 +232,12 @@ add_task(async function test_categorization_unknown_and_inconclusive() { ]); let resultsToReport = - await SearchSERPCategorization.applyCategorizationLogic(domains); + await SERPCategorization.applyCategorizationLogic(domains); Assert.deepEqual( resultsToReport, { - category: SearchSERPTelemetryUtils.CATEGORIZATION.INCONCLUSIVE, + category: CATEGORIZATION_SETTINGS.INCONCLUSIVE, num_domains: 10, num_inconclusive: 5, num_unknown: 5, @@ -249,7 +248,7 @@ add_task(async function test_categorization_unknown_and_inconclusive() { // Tests a mixture of categorized, inconclusive and unknown domains. add_task(async function test_categorization_all_types() { - await SearchSERPDomainToCategoriesMap.overrideMapForTests( + await SERPDomainToCategoriesMap.overrideMapForTests( TEST_DOMAIN_TO_CATEGORIES_MAP_ALL_TYPES ); @@ -269,7 +268,7 @@ add_task(async function test_categorization_all_types() { ]); let resultsToReport = - await SearchSERPCategorization.applyCategorizationLogic(domains); + await SERPCategorization.applyCategorizationLogic(domains); Assert.deepEqual( resultsToReport, @@ -284,7 +283,7 @@ add_task(async function test_categorization_all_types() { }); add_task(async function test_categorization_tie() { - await SearchSERPDomainToCategoriesMap.overrideMapForTests( + await SERPDomainToCategoriesMap.overrideMapForTests( TEST_DOMAIN_TO_CATEGORIES_MAP_TIE ); @@ -302,7 +301,7 @@ add_task(async function test_categorization_tie() { ]); let resultsToReport = - await SearchSERPCategorization.applyCategorizationLogic(domains); + await SERPCategorization.applyCategorizationLogic(domains); Assert.equal( [1, 2].includes(resultsToReport.category), @@ -322,7 +321,7 @@ add_task(async function test_categorization_tie() { }); add_task(async function test_rank_penalization_equal_scores() { - await SearchSERPDomainToCategoriesMap.overrideMapForTests( + await SERPDomainToCategoriesMap.overrideMapForTests( TEST_DOMAIN_TO_CATEGORIES_MAP_RANK_PENALIZATION_1 ); @@ -340,7 +339,7 @@ add_task(async function test_rank_penalization_equal_scores() { ]); let resultsToReport = - await SearchSERPCategorization.applyCategorizationLogic(domains); + await SERPCategorization.applyCategorizationLogic(domains); Assert.deepEqual( resultsToReport, @@ -350,14 +349,14 @@ add_task(async function test_rank_penalization_equal_scores() { }); add_task(async function test_rank_penalization_highest_score_lower_on_page() { - await SearchSERPDomainToCategoriesMap.overrideMapForTests( + await SERPDomainToCategoriesMap.overrideMapForTests( TEST_DOMAIN_TO_CATEGORIES_MAP_RANK_PENALIZATION_2 ); let domains = new Set(["test61.com", "test62.com"]); let resultsToReport = - await SearchSERPCategorization.applyCategorizationLogic(domains); + await SERPCategorization.applyCategorizationLogic(domains); Assert.deepEqual( resultsToReport, diff --git a/browser/components/search/test/unit/test_search_telemetry_categorization_region.js b/browser/components/search/test/unit/test_search_telemetry_categorization_region.js index 45130d0002b..c1eab1ae1aa 100644 --- a/browser/components/search/test/unit/test_search_telemetry_categorization_region.js +++ b/browser/components/search/test/unit/test_search_telemetry_categorization_region.js @@ -8,8 +8,7 @@ "use strict"; ChromeUtils.defineESModuleGetters(this, { - SearchSERPDomainToCategoriesMap: - "resource:///modules/SearchSERPTelemetry.sys.mjs", + SERPDomainToCategoriesMap: "resource:///modules/SERPCategorization.sys.mjs", }); add_task(async function record_matches_region() { @@ -108,10 +107,7 @@ add_task(async function record_matches_region() { for (let { title, record, expectedResult, region } of TESTS) { info(title); - let result = SearchSERPDomainToCategoriesMap.recordMatchesRegion( - record, - region - ); + let result = SERPDomainToCategoriesMap.recordMatchesRegion(record, region); Assert.equal(result, expectedResult, "Result should match."); } }); @@ -214,10 +210,7 @@ add_task(async function find_records_for_region() { for (let { title, record, expectedResult, region } of TESTS) { info(title); - let result = SearchSERPDomainToCategoriesMap.findRecordsForRegion( - record, - region - ); + let result = SERPDomainToCategoriesMap.findRecordsForRegion(record, region); Assert.deepEqual(result, expectedResult, "Result should match."); } }); diff --git a/browser/components/search/test/unit/test_search_telemetry_categorization_sync.js b/browser/components/search/test/unit/test_search_telemetry_categorization_sync.js index 24995654d39..a18862851b7 100644 --- a/browser/components/search/test/unit/test_search_telemetry_categorization_sync.js +++ b/browser/components/search/test/unit/test_search_telemetry_categorization_sync.js @@ -10,11 +10,10 @@ ChromeUtils.defineESModuleGetters(this, { RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", Region: "resource://gre/modules/Region.sys.mjs", - SearchSERPCategorization: "resource:///modules/SearchSERPTelemetry.sys.mjs", - SearchSERPDomainToCategoriesMap: - "resource:///modules/SearchSERPTelemetry.sys.mjs", + SERPCategorization: "resource:///modules/SERPCategorization.sys.mjs", + SERPDomainToCategoriesMap: "resource:///modules/SERPCategorization.sys.mjs", TELEMETRY_CATEGORIZATION_KEY: - "resource:///modules/SearchSERPTelemetry.sys.mjs", + "resource:///modules/SERPCategorization.sys.mjs", TestUtils: "resource://testing-common/TestUtils.sys.mjs", }); @@ -182,24 +181,24 @@ add_task(async function test_initial_import() { info("Initialize search categorization mappings."); let promise = waitForDomainToCategoriesUpdate(); - await SearchSERPDomainToCategoriesMap.init(); + await SERPDomainToCategoriesMap.init(); await promise; Assert.deepEqual( - await SearchSERPDomainToCategoriesMap.get("example.com"), + await SERPDomainToCategoriesMap.get("example.com"), [{ category: 1, score: 100 }], "Return value from lookup of example.com should be the same." ); Assert.deepEqual( - await SearchSERPDomainToCategoriesMap.get("example.org"), + await SERPDomainToCategoriesMap.get("example.org"), [{ category: 2, score: 90 }], "Return value from lookup of example.org should be the same." ); // Clean up. await db.clear(); - await SearchSERPDomainToCategoriesMap.uninit(true); + await SERPDomainToCategoriesMap.uninit(true); }); add_task(async function test_update_records() { @@ -216,7 +215,7 @@ add_task(async function test_update_records() { info("Initialize search categorization mappings."); let promise = waitForDomainToCategoriesUpdate(); - await SearchSERPDomainToCategoriesMap.init(); + await SERPDomainToCategoriesMap.init(); await promise; info("Send update from Remote Settings with updates to attachments."); @@ -238,13 +237,13 @@ add_task(async function test_update_records() { await promise; Assert.deepEqual( - await SearchSERPDomainToCategoriesMap.get("example.com"), + await SERPDomainToCategoriesMap.get("example.com"), [{ category: 1, score: 80 }], "Return value from lookup of example.com should have changed." ); Assert.deepEqual( - await SearchSERPDomainToCategoriesMap.get("example.org"), + await SERPDomainToCategoriesMap.get("example.org"), [ { category: 2, score: 50 }, { category: 4, score: 80 }, @@ -253,14 +252,14 @@ add_task(async function test_update_records() { ); Assert.equal( - SearchSERPDomainToCategoriesMap.version, + SERPDomainToCategoriesMap.version, 2, "Version should be correct." ); // Clean up. await db.clear(); - await SearchSERPDomainToCategoriesMap.uninit(true); + await SERPDomainToCategoriesMap.uninit(true); }); add_task(async function test_delayed_initial_import() { @@ -274,10 +273,10 @@ add_task(async function test_delayed_initial_import() { ); }); info("Initialize without records."); - await SearchSERPDomainToCategoriesMap.init(); + await SERPDomainToCategoriesMap.init(); await observeNoRecordsFound; - Assert.ok(SearchSERPDomainToCategoriesMap.empty, "Map is empty."); + Assert.ok(SERPDomainToCategoriesMap.empty, "Map is empty."); info("Send update from Remote Settings with updates to attachments."); let record1a = await mockRecordWithCachedAttachment(RECORDS.record1a); @@ -295,26 +294,26 @@ add_task(async function test_delayed_initial_import() { await promise; Assert.deepEqual( - await SearchSERPDomainToCategoriesMap.get("example.com"), + await SERPDomainToCategoriesMap.get("example.com"), [{ category: 1, score: 100 }], "Return value from lookup of example.com should be the same." ); Assert.deepEqual( - await SearchSERPDomainToCategoriesMap.get("example.org"), + await SERPDomainToCategoriesMap.get("example.org"), [{ category: 2, score: 90 }], "Return value from lookup of example.org should be the same." ); Assert.equal( - SearchSERPDomainToCategoriesMap.version, + SERPDomainToCategoriesMap.version, 1, "Version should be correct." ); // Clean up. await db.clear(); - await SearchSERPDomainToCategoriesMap.uninit(true); + await SERPDomainToCategoriesMap.uninit(true); }); add_task(async function test_remove_record() { @@ -331,11 +330,11 @@ add_task(async function test_remove_record() { info("Initialize search categorization mappings."); let promise = waitForDomainToCategoriesUpdate(); - await SearchSERPDomainToCategoriesMap.init(); + await SERPDomainToCategoriesMap.init(); await promise; Assert.deepEqual( - await SearchSERPDomainToCategoriesMap.get("example.com"), + await SERPDomainToCategoriesMap.get("example.com"), [{ category: 1, score: 80 }], "Initialized properly." ); @@ -354,26 +353,26 @@ add_task(async function test_remove_record() { await promise; Assert.deepEqual( - await SearchSERPDomainToCategoriesMap.get("example.com"), + await SERPDomainToCategoriesMap.get("example.com"), [{ category: 1, score: 80 }], "Return value from lookup of example.com should remain unchanged." ); Assert.deepEqual( - await SearchSERPDomainToCategoriesMap.get("example.org"), + await SERPDomainToCategoriesMap.get("example.org"), [], "Return value from lookup of example.org should be empty." ); Assert.equal( - SearchSERPDomainToCategoriesMap.version, + SERPDomainToCategoriesMap.version, 2, "Version should be correct." ); // Clean up. await db.clear(); - await SearchSERPDomainToCategoriesMap.uninit(true); + await SERPDomainToCategoriesMap.uninit(true); }); add_task(async function test_different_versions_coexisting() { @@ -390,11 +389,11 @@ add_task(async function test_different_versions_coexisting() { info("Initialize search categorization mappings."); let promise = waitForDomainToCategoriesUpdate(); - await SearchSERPDomainToCategoriesMap.init(); + await SERPDomainToCategoriesMap.init(); await promise; Assert.deepEqual( - await SearchSERPDomainToCategoriesMap.get("example.com"), + await SERPDomainToCategoriesMap.get("example.com"), [ { category: 1, @@ -405,7 +404,7 @@ add_task(async function test_different_versions_coexisting() { ); Assert.deepEqual( - await SearchSERPDomainToCategoriesMap.get("example.org"), + await SERPDomainToCategoriesMap.get("example.org"), [ { category: 2, score: 50 }, { category: 4, score: 80 }, @@ -414,14 +413,14 @@ add_task(async function test_different_versions_coexisting() { ); Assert.equal( - SearchSERPDomainToCategoriesMap.version, + SERPDomainToCategoriesMap.version, 2, "Version should be the latest." ); // Clean up. await db.clear(); - await SearchSERPDomainToCategoriesMap.uninit(true); + await SERPDomainToCategoriesMap.uninit(true); }); add_task(async function test_download_error() { @@ -434,11 +433,11 @@ add_task(async function test_download_error() { info("Initialize search categorization mappings."); let promise = waitForDomainToCategoriesUpdate(); - await SearchSERPDomainToCategoriesMap.init(); + await SERPDomainToCategoriesMap.init(); await promise; Assert.deepEqual( - await SearchSERPDomainToCategoriesMap.get("example.com"), + await SERPDomainToCategoriesMap.get("example.com"), [ { category: 1, @@ -449,7 +448,7 @@ add_task(async function test_download_error() { ); Assert.equal( - SearchSERPDomainToCategoriesMap.version, + SERPDomainToCategoriesMap.version, 1, "Version should be present." ); @@ -477,20 +476,20 @@ add_task(async function test_download_error() { await observeDownloadError; Assert.deepEqual( - await SearchSERPDomainToCategoriesMap.get("example.com"), + await SERPDomainToCategoriesMap.get("example.com"), [], "Domain should not exist in store." ); Assert.equal( - SearchSERPDomainToCategoriesMap.version, + SERPDomainToCategoriesMap.version, null, "Version should remain null." ); // Clean up. await db.clear(); - await SearchSERPDomainToCategoriesMap.uninit(true); + await SERPDomainToCategoriesMap.uninit(true); }); add_task(async function test_mock_restart() { @@ -507,11 +506,11 @@ add_task(async function test_mock_restart() { info("Initialize search categorization mappings."); let promise = waitForDomainToCategoriesUpdate(); - await SearchSERPCategorization.init(); + await SERPCategorization.init(); await promise; Assert.deepEqual( - await SearchSERPDomainToCategoriesMap.get("example.com"), + await SERPDomainToCategoriesMap.get("example.com"), [ { category: 1, @@ -522,19 +521,19 @@ add_task(async function test_mock_restart() { ); Assert.equal( - SearchSERPDomainToCategoriesMap.version, + SERPDomainToCategoriesMap.version, 2, "Version should be the latest." ); info("Mock a restart by un-initializing the map."); - await SearchSERPCategorization.uninit(); + await SERPCategorization.uninit(); promise = waitForDomainToCategoriesUpdate(); - await SearchSERPCategorization.init(); + await SERPCategorization.init(); await promise; Assert.deepEqual( - await SearchSERPDomainToCategoriesMap.get("example.com"), + await SERPDomainToCategoriesMap.get("example.com"), [ { category: 1, @@ -545,14 +544,14 @@ add_task(async function test_mock_restart() { ); Assert.equal( - SearchSERPDomainToCategoriesMap.version, + SERPDomainToCategoriesMap.version, 2, "Version should be the latest." ); // Clean up. await db.clear(); - await SearchSERPDomainToCategoriesMap.uninit(true); + await SERPDomainToCategoriesMap.uninit(true); }); add_task(async function update_record_from_non_matching_region() { @@ -565,11 +564,11 @@ add_task(async function update_record_from_non_matching_region() { info("Initialize search categorization mappings."); let promise = waitForDomainToCategoriesUpdate(); - await SearchSERPDomainToCategoriesMap.init(); + await SERPDomainToCategoriesMap.init(); await promise; Assert.deepEqual( - await SearchSERPDomainToCategoriesMap.get("example.com"), + await SERPDomainToCategoriesMap.get("example.com"), [{ category: 1, score: 100 }], "Return value from lookup of example.com should exist." ); @@ -597,26 +596,26 @@ add_task(async function update_record_from_non_matching_region() { await observeNoChange; Assert.deepEqual( - await SearchSERPDomainToCategoriesMap.get("example.com"), + await SERPDomainToCategoriesMap.get("example.com"), [{ category: 1, score: 100 }], "Return value from lookup of example.com should still exist." ); Assert.deepEqual( - await SearchSERPDomainToCategoriesMap.get("example.ca"), + await SERPDomainToCategoriesMap.get("example.ca"), [], "Domain from non-home region should not exist." ); Assert.equal( - SearchSERPDomainToCategoriesMap.version, + SERPDomainToCategoriesMap.version, 1, "Version should be remain the same." ); // Clean up. await db.clear(); - await SearchSERPDomainToCategoriesMap.uninit(true); + await SERPDomainToCategoriesMap.uninit(true); }); add_task(async function update_record_from_non_matching_region() { @@ -629,11 +628,11 @@ add_task(async function update_record_from_non_matching_region() { info("Initialize search categorization mappings."); let promise = waitForDomainToCategoriesUpdate(); - await SearchSERPDomainToCategoriesMap.init(); + await SERPDomainToCategoriesMap.init(); await promise; Assert.deepEqual( - await SearchSERPDomainToCategoriesMap.get("example.com"), + await SERPDomainToCategoriesMap.get("example.com"), [{ category: 1, score: 100 }], "Return value from lookup of example.com should exist." ); @@ -661,26 +660,26 @@ add_task(async function update_record_from_non_matching_region() { await observeNoChange; Assert.deepEqual( - await SearchSERPDomainToCategoriesMap.get("example.com"), + await SERPDomainToCategoriesMap.get("example.com"), [{ category: 1, score: 100 }], "Return value from lookup of example.com should still exist." ); Assert.deepEqual( - await SearchSERPDomainToCategoriesMap.get("example.ca"), + await SERPDomainToCategoriesMap.get("example.ca"), [], "Domain from non-home region should not exist." ); Assert.equal( - SearchSERPDomainToCategoriesMap.version, + SERPDomainToCategoriesMap.version, 1, "Version should be remain the same." ); // Clean up. await db.clear(); - await SearchSERPDomainToCategoriesMap.uninit(true); + await SERPDomainToCategoriesMap.uninit(true); }); add_task(async function update_() { @@ -693,17 +692,17 @@ add_task(async function update_() { info("Initialize search categorization mappings."); let promise = waitForDomainToCategoriesUpdate(); - await SearchSERPDomainToCategoriesMap.init(); + await SERPDomainToCategoriesMap.init(); await promise; Assert.deepEqual( - await SearchSERPDomainToCategoriesMap.get("example.com"), + await SERPDomainToCategoriesMap.get("example.com"), [{ category: 1, score: 100 }], "Return value from lookup of example.com should exist." ); // Re-init the Map to mimic a restart. - await SearchSERPDomainToCategoriesMap.uninit(); + await SERPDomainToCategoriesMap.uninit(); info("Change home region to one that doesn't match region of map."); let originalHomeRegion = Region.home; @@ -718,10 +717,10 @@ add_task(async function update_() { ); }); - await SearchSERPDomainToCategoriesMap.init(); + await SERPDomainToCategoriesMap.init(); await observeDropStore; Assert.deepEqual( - await SearchSERPDomainToCategoriesMap.get("example.com"), + await SERPDomainToCategoriesMap.get("example.com"), [], "Return value from lookup of example.com should be empty." ); @@ -729,5 +728,5 @@ add_task(async function update_() { // Clean up. await db.clear(); Region._setHomeRegion(originalHomeRegion); - await SearchSERPDomainToCategoriesMap.uninit(true); + await SERPDomainToCategoriesMap.uninit(true); }); diff --git a/browser/components/search/test/unit/test_search_telemetry_categorization_sync_could_modify_store.js b/browser/components/search/test/unit/test_search_telemetry_categorization_sync_could_modify_store.js index a5ecee956dc..68df4d5422c 100644 --- a/browser/components/search/test/unit/test_search_telemetry_categorization_sync_could_modify_store.js +++ b/browser/components/search/test/unit/test_search_telemetry_categorization_sync_could_modify_store.js @@ -9,8 +9,7 @@ ChromeUtils.defineESModuleGetters(this, { Region: "resource://gre/modules/Region.sys.mjs", - SearchSERPDomainToCategoriesMap: - "resource:///modules/SearchSERPTelemetry.sys.mjs", + SERPDomainToCategoriesMap: "resource:///modules/SERPCategorization.sys.mjs", }); // For the tests, domains aren't checked, but add at least one value so the @@ -30,7 +29,7 @@ add_setup(async () => { "browser.search.serpEventTelemetryCategorization.enabled", true ); - await SearchSERPDomainToCategoriesMap.init(); + await SERPDomainToCategoriesMap.init(); await Region.init(); let originalRegion = Region.home; Region._setHomeRegion(USER_REGION); @@ -203,9 +202,9 @@ const TESTS = [ add_task(async function test_sync_may_modify_store() { for (let test of TESTS) { if (test.emptyMap) { - await SearchSERPDomainToCategoriesMap.overrideMapForTests({}, 0, false); + await SERPDomainToCategoriesMap.overrideMapForTests({}, 0, false); } else { - await SearchSERPDomainToCategoriesMap.overrideMapForTests( + await SERPDomainToCategoriesMap.overrideMapForTests( DATA, VERSION, test.isDefault @@ -213,11 +212,11 @@ add_task(async function test_sync_may_modify_store() { } info( `Domain to Categories Map: ${ - SearchSERPDomainToCategoriesMap.empty ? "Empty" : "Has Existing Data" + SERPDomainToCategoriesMap.empty ? "Empty" : "Has Existing Data" }.` ); info(`${test.title}.`); - let result = await SearchSERPDomainToCategoriesMap.syncMayModifyStore( + let result = await SERPDomainToCategoriesMap.syncMayModifyStore( test.data, USER_REGION ); diff --git a/browser/components/sessionstore/SessionStore.sys.mjs b/browser/components/sessionstore/SessionStore.sys.mjs index 9391a70ec6b..aacb18002b1 100644 --- a/browser/components/sessionstore/SessionStore.sys.mjs +++ b/browser/components/sessionstore/SessionStore.sys.mjs @@ -6240,9 +6240,13 @@ var SessionStoreInternal = { */ restoreWindowFeatures: function ssi_restoreWindowFeatures(aWindow, aWinData) { var hidden = aWinData.hidden ? aWinData.hidden.split(",") : []; - WINDOW_HIDEABLE_FEATURES.forEach(function (aItem) { - aWindow[aItem].visible = !hidden.includes(aItem); - }); + var isTaskbarTab = + aWindow.document.documentElement.getAttribute("taskbartab"); + if (!isTaskbarTab) { + WINDOW_HIDEABLE_FEATURES.forEach(function (aItem) { + aWindow[aItem].visible = !hidden.includes(aItem); + }); + } if (aWinData.isPopup) { this._windows[aWindow.__SSi].isPopup = true; @@ -6251,7 +6255,7 @@ var SessionStoreInternal = { } } else { delete this._windows[aWindow.__SSi].isPopup; - if (aWindow.gURLBar) { + if (aWindow.gURLBar && !isTaskbarTab) { aWindow.gURLBar.readOnly = false; } } diff --git a/browser/components/shell/nsWindowsShellService.cpp b/browser/components/shell/nsWindowsShellService.cpp index 7b9f5111c83..29126a82873 100644 --- a/browser/components/shell/nsWindowsShellService.cpp +++ b/browser/components/shell/nsWindowsShellService.cpp @@ -1629,9 +1629,10 @@ static nsresult PinShortcutToTaskbarImpl(bool aCheckOnly, // during install or runtime - causes a race between it propagating to the // virtual `shell:appsfolder` and attempts to pin via `ITaskbarManager`, // resulting in pin failures when the latter occurs before the former. We can - // skip this when we're only checking whether we're pinned. - if (!aCheckOnly && !PollAppsFolderForShortcut( - aAppUserModelId, TimeDuration::FromSeconds(15))) { + // skip this when we're in a MSIX build or only checking whether we're pinned. + if (!widget::WinUtils::HasPackageIdentity() && !aCheckOnly && + !PollAppsFolderForShortcut(aAppUserModelId, + TimeDuration::FromSeconds(15))) { return NS_ERROR_FILE_NOT_FOUND; } diff --git a/browser/components/shopping/content/shopping-container.css b/browser/components/shopping/content/shopping-container.css index 969fa0230d3..69736ca4579 100644 --- a/browser/components/shopping/content/shopping-container.css +++ b/browser/components/shopping/content/shopping-container.css @@ -25,7 +25,7 @@ position: sticky; top: 0; width: 100%; - z-index: 2; + z-index: 3; &.header-wrapper-overflow { align-items: baseline; diff --git a/browser/components/sidebar/browser-sidebar.js b/browser/components/sidebar/browser-sidebar.js index 371190c058f..5d215bfdd94 100644 --- a/browser/components/sidebar/browser-sidebar.js +++ b/browser/components/sidebar/browser-sidebar.js @@ -173,6 +173,7 @@ var SidebarController = { revampL10nId: "sidebar-menu-review-checker-label", iconUrl: "chrome://browser/content/shopping/assets/shopping.svg", gleanEvent: Glean.shopping.sidebarToggle, + gleanClickEvent: Glean.sidebar.shoppingReviewCheckerIconClick, recordSidebarVersion: true, } ); diff --git a/browser/components/sidebar/metrics.yaml b/browser/components/sidebar/metrics.yaml index 0f1dc99b1e2..0e4afa14abc 100644 --- a/browser/components/sidebar/metrics.yaml +++ b/browser/components/sidebar/metrics.yaml @@ -243,6 +243,26 @@ sidebar: addon_id: type: string description: The extension's ID. + shopping_review_checker_icon_click: + type: event + description: > + The Review Checker icon was clicked. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1951175 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1951175 + data_sensitivity: + - interaction + expires: 147 + notification_emails: + - betling@mozilla.com + - fx-desktop-shopping-eng@mozilla.com + send_in_pings: + - events + extra_keys: + sidebar_open: + type: boolean + description: Whether the sidebar is expanded or collapsed. keyboard_shortcut: type: event description: > diff --git a/browser/components/sidebar/tests/browser/browser_glean_sidebar.js b/browser/components/sidebar/tests/browser/browser_glean_sidebar.js index ba3a866b0b3..38aed83f59e 100644 --- a/browser/components/sidebar/tests/browser/browser_glean_sidebar.js +++ b/browser/components/sidebar/tests/browser/browser_glean_sidebar.js @@ -610,6 +610,37 @@ async function testIconClick(expanded) { Services.fog.testResetFOG(); } +async function testIconClickReviewChecker(expanded) { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "sidebar.main.tools", + "aichat,syncedtabs,history,bookmarks,reviewchecker", + ], + ], + }); + + const { sidebarMain } = SidebarController; + + await SidebarController.initializeUIState({ launcherExpanded: expanded }); + + let reviewCheckerButton = sidebarMain.shadowRoot.querySelector( + "moz-button[view='viewReviewCheckerSidebar']" + ); + EventUtils.synthesizeMouseAtCenter(reviewCheckerButton, {}); + + let event = Glean.sidebar.shoppingReviewCheckerIconClick.testGetValue(); + Assert.equal(event?.length, 1, "One event was reported."); + Assert.deepEqual( + event?.[0].extra, + { sidebar_open: `${expanded}` }, + `Event indicates the sidebar was ${expanded ? "expanded" : "collapsed"}.` + ); + + await SpecialPowers.popPrefEnv(); + Services.fog.testResetFOG(); +} + add_task(async function test_icon_click_collapsed_sidebar() { await testIconClick(false); }); @@ -617,3 +648,11 @@ add_task(async function test_icon_click_collapsed_sidebar() { add_task(async function test_icon_click_expanded_sidebar() { await testIconClick(true); }); + +add_task(async function test_review_checker_icon_click_collapsed_sidebar() { + await testIconClickReviewChecker(false); +}); + +add_task(async function test_review_checker_icon_click_expanded_sidebar() { + await testIconClickReviewChecker(true); +}); diff --git a/browser/components/syncedtabs/TabListComponent.sys.mjs b/browser/components/syncedtabs/TabListComponent.sys.mjs index cda3376dc2a..e707715998f 100644 --- a/browser/components/syncedtabs/TabListComponent.sys.mjs +++ b/browser/components/syncedtabs/TabListComponent.sys.mjs @@ -9,7 +9,8 @@ let log = ChromeUtils.importESModule( const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { - OpenInTabsUtils: "resource:///modules/OpenInTabsUtils.sys.mjs", + OpenInTabsUtils: + "moz-src:///browser/components/tabbrowser/OpenInTabsUtils.sys.mjs", }); /** diff --git a/browser/components/tabbrowser/SmartTabGrouping.sys.mjs b/browser/components/tabbrowser/SmartTabGrouping.sys.mjs index 5a54211afbc..2a211176848 100644 --- a/browser/components/tabbrowser/SmartTabGrouping.sys.mjs +++ b/browser/components/tabbrowser/SmartTabGrouping.sys.mjs @@ -206,52 +206,63 @@ export class SmartTabGroupingManager { allTabs, groupedIndices, alreadyGroupedIndices, - threshold = NEAREST_NEIGHBOR_DEFAULT_THRESHOLD + threshold = NEAREST_NEIGHBOR_DEFAULT_THRESHOLD, + precomputedEmbeddings = [], + depth = 1 ) { - // get tabs in group first - const tabsInGroup = groupedIndices.map(i => allTabs[i]); - const tabsInGroupData = await this._prepareTabData(tabsInGroup); - const tabsInGroupEmbeddings = await this._generateEmbeddings( - tabsInGroupData.map(a => a[EMBED_TEXT_KEY]) - ); + // get embeddings for all the tabs + const tabData = await this._prepareTabData(allTabs); + let embeddings = precomputedEmbeddings; + if (precomputedEmbeddings.length === 0) { + embeddings = await this._generateEmbeddings( + tabData.map(a => a[EMBED_TEXT_KEY]) + ); + } - // get tabs that we need to assign + // get tabs that need to be assigned const groupedTabIndices = groupedIndices.concat(alreadyGroupedIndices); - const tabsToAssign = allTabs.filter( - (_, index) => !groupedTabIndices.includes(index) - ); - const tabsToAssignData = await this._prepareTabData(tabsToAssign); - const tabsToAssignEmbeddings = await this._generateEmbeddings( - tabsToAssignData.map(a => a[EMBED_TEXT_KEY]) - ); + const tabsToAssignIndices = allTabs + .map((_, index) => index) + .filter(i => !groupedTabIndices.includes(i)); - // find closest tabs - // if any tab is close to a tab in the existing group, add to list - const closestTabs = []; - - // select MAX_NN_GROUPED_TABS so too many tabs in same group won't cause performance issues - for (let i = 0; i < tabsToAssign.length; i++) { + let closestTabs = []; + const similarTabsIndices = []; + for (let i = 0; i < tabsToAssignIndices.length; i++) { let closestScore = null; for ( let j = 0; - j < Math.min(tabsInGroup.length, MAX_NN_GROUPED_TABS); + j < Math.min(groupedIndices.length, MAX_NN_GROUPED_TABS); j++ ) { const cosineSim = cosSim( - tabsToAssignEmbeddings[i], - tabsInGroupEmbeddings[j] + embeddings[tabsToAssignIndices[i]], + embeddings[groupedIndices[j]] ); if (!closestScore || cosineSim > closestScore) { closestScore = cosineSim; } } if (closestScore > threshold) { - closestTabs.push([tabsToAssign[i], closestScore]); + closestTabs.push([allTabs[tabsToAssignIndices[i]], closestScore]); + similarTabsIndices.push(tabsToAssignIndices[i]); } } - // sort and return by tabs that are most similar closestTabs.sort((a, b) => b[1] - a[1]); - return closestTabs.map(t => t[0]); + closestTabs = closestTabs.map(t => t[0]); + // recurse once if the initial call only had a single tab + // and we found at least 1 similar tab - this improves recall + if (groupedIndices.length === 1 && !!closestTabs.length && depth === 1) { + const recurseSimilarTabs = await this.findNearestNeighbors( + allTabs, + similarTabsIndices, + alreadyGroupedIndices.concat(groupedIndices), + threshold, + embeddings, + depth - 1 + ); + closestTabs = closestTabs.concat(recurseSimilarTabs); + } + return closestTabs; } /** diff --git a/browser/components/tabbrowser/content/browser-allTabsMenu.js b/browser/components/tabbrowser/content/browser-allTabsMenu.js index 996edb31d66..a5426b60da9 100644 --- a/browser/components/tabbrowser/content/browser-allTabsMenu.js +++ b/browser/components/tabbrowser/content/browser-allTabsMenu.js @@ -7,9 +7,9 @@ ChromeUtils.defineESModuleGetters(this, { BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.sys.mjs", - GroupsPanel: "resource:///modules/GroupsList.sys.mjs", + GroupsPanel: "moz-src:///browser/components/tabbrowser/GroupsList.sys.mjs", NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", - TabsPanel: "resource:///modules/TabsList.sys.mjs", + TabsPanel: "moz-src:///browser/components/tabbrowser/TabsList.sys.mjs", }); var gTabsPanel = { diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js index 06fec203dca..bfbc55097dd 100644 --- a/browser/components/tabbrowser/content/tabbrowser.js +++ b/browser/components/tabbrowser/content/tabbrowser.js @@ -106,9 +106,11 @@ ); ChromeUtils.defineESModuleGetters(this, { - AsyncTabSwitcher: "resource:///modules/AsyncTabSwitcher.sys.mjs", + AsyncTabSwitcher: + "moz-src:///browser/components/tabbrowser/AsyncTabSwitcher.sys.mjs", PictureInPicture: "resource://gre/modules/PictureInPicture.sys.mjs", - SmartTabGroupingManager: "resource:///modules/SmartTabGrouping.sys.mjs", + SmartTabGroupingManager: + "moz-src:///browser/components/tabbrowser/SmartTabGrouping.sys.mjs", UrlbarProviderOpenTabs: "resource:///modules/UrlbarProviderOpenTabs.sys.mjs", }); @@ -8295,62 +8297,67 @@ var TabBarVisibility = { _initialUpdateDone: false, update(force = false) { - let toolbar = document.getElementById("TabsToolbar"); - let navbar = document.getElementById("nav-bar"); - let hideTabstrip = false; let isPopup = !window.toolbar.visible; - let isVerticalTabs = Services.prefs.getBoolPref( - "sidebar.verticalTabs", - false - ); - let nonPopupWithVerticalTabs = !isPopup && isVerticalTabs; - if ( - !gBrowser /* gBrowser isn't initialized yet */ || - gBrowser.visibleTabs.length == 1 - ) { - hideTabstrip = isPopup; - } + let isTaskbarTab = document.documentElement.hasAttribute("taskbartab"); + let isSingleTabWindow = isPopup || isTaskbarTab; - if (nonPopupWithVerticalTabs) { - // CustomTitlebar decides if we can draw within the titlebar area. - // In vertical tabs mode, the toolbar with the horizontal tabstrip gets hidden - // and the navbar becomes a titlebar. - hideTabstrip = true; - CustomTitlebar.allowedBy("tabs-visible", true); - } else { - CustomTitlebar.allowedBy("tabs-visible", !hideTabstrip); - } + let hasVerticalTabs = + !isSingleTabWindow && + Services.prefs.getBoolPref("sidebar.verticalTabs", false); - gNavToolbox.toggleAttribute("tabs-hidden", hideTabstrip); + // 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 && hideTabstrip + CustomTitlebar.enabled && hideTabsToolbar ); document .getElementById("browser") .classList.toggle( "browser-toolbox-background", - CustomTitlebar.enabled && nonPopupWithVerticalTabs + CustomTitlebar.enabled && hasVerticalTabs ); if ( - hideTabstrip == toolbar.collapsed && + hideTabsToolbar == tabsToolbar.collapsed && !force && this._initialUpdateDone ) { - // no further updates needed, toolbar.collapsed already matches hideTabstrip + // No further updates needed, `TabsToolbar` already matches the expected + // visibilty. return; } this._initialUpdateDone = true; - toolbar.collapsed = hideTabstrip; + tabsToolbar.collapsed = hideTabsToolbar; - document.getElementById("menu_closeWindow").hidden = hideTabstrip; + // 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"), - hideTabstrip + hideTabsToolbar ? "tabbrowser-menuitem-close" : "tabbrowser-menuitem-close-tab" ); diff --git a/browser/components/tabbrowser/content/tabgroup-menu.js b/browser/components/tabbrowser/content/tabgroup-menu.js index f6d5fb2ab25..12d8929edf9 100644 --- a/browser/components/tabbrowser/content/tabgroup-menu.js +++ b/browser/components/tabbrowser/content/tabgroup-menu.js @@ -408,7 +408,7 @@ ? "chrome://global/skin/icons/highlights.svg" : ""; const { SmartTabGroupingManager } = ChromeUtils.importESModule( - "resource:///modules/SmartTabGrouping.sys.mjs" + "moz-src:///browser/components/tabbrowser/SmartTabGrouping.sys.mjs" ); this.#smartTabGroupingManager = new SmartTabGroupingManager(); diff --git a/browser/components/tabbrowser/moz.build b/browser/components/tabbrowser/moz.build index fceab2d69fa..7a84ed8be97 100644 --- a/browser/components/tabbrowser/moz.build +++ b/browser/components/tabbrowser/moz.build @@ -7,7 +7,7 @@ with Files("**"): JAR_MANIFESTS += ["jar.mn"] -EXTRA_JS_MODULES += [ +MOZ_SRC_FILES += [ "AsyncTabSwitcher.sys.mjs", "GroupsList.sys.mjs", "NewTabPagePreloading.sys.mjs", diff --git a/browser/components/tabunloader/content/aboutUnloads.js b/browser/components/tabunloader/content/aboutUnloads.js index 635f1243d33..db4b8531205 100644 --- a/browser/components/tabunloader/content/aboutUnloads.js +++ b/browser/components/tabunloader/content/aboutUnloads.js @@ -4,7 +4,7 @@ "use strict"; const { TabUnloader } = ChromeUtils.importESModule( - "resource:///modules/TabUnloader.sys.mjs" + "moz-src:///browser/components/tabbrowser/TabUnloader.sys.mjs" ); async function refreshData() { diff --git a/browser/components/taskbartabs/TaskbarTabUI.sys.mjs b/browser/components/taskbartabs/TaskbarTabUI.sys.mjs new file mode 100644 index 00000000000..1d54e1f287d --- /dev/null +++ b/browser/components/taskbartabs/TaskbarTabUI.sys.mjs @@ -0,0 +1,29 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +// Set up Taskbar Tabs Window UI +export var TaskbarTabUI = { + init(window) { + let document = window.document; + if (!document.documentElement.hasAttribute("taskbartab")) { + return; + } + + // Ensure tab strip is hidden + window.TabBarVisibility.update(); + + // Hide pocket button + const saveToPocketButton = document.getElementById("save-to-pocket-button"); + if (saveToPocketButton) { + saveToPocketButton.remove(); + document.documentElement.setAttribute("pocketdisabled", "true"); + } + + // Hide bookmark star + document.getElementById("star-button-box").style.display = "none"; + + // Hide fxa in the hamburger menu + document.documentElement.setAttribute("fxadisabled", true); + }, +}; diff --git a/browser/components/taskbartabs/moz.build b/browser/components/taskbartabs/moz.build new file mode 100644 index 00000000000..8b61d4d43e4 --- /dev/null +++ b/browser/components/taskbartabs/moz.build @@ -0,0 +1,13 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +EXTRA_JS_MODULES += [ + "TaskbarTabUI.sys.mjs", +] + +BROWSER_CHROME_MANIFESTS += [ + "test/browser/browser.toml", +] diff --git a/browser/components/taskbartabs/test/browser/browser.toml b/browser/components/taskbartabs/test/browser/browser.toml new file mode 100644 index 00000000000..3b034628039 --- /dev/null +++ b/browser/components/taskbartabs/test/browser/browser.toml @@ -0,0 +1,4 @@ +[DEFAULT] + +["browser_taskbarTabs_chromeTest.js"] +run-if = ["os == 'win'"] diff --git a/browser/components/taskbartabs/test/browser/browser_taskbarTabs_chromeTest.js b/browser/components/taskbartabs/test/browser/browser_taskbarTabs_chromeTest.js new file mode 100644 index 00000000000..1c5c2d04188 --- /dev/null +++ b/browser/components/taskbartabs/test/browser/browser_taskbarTabs_chromeTest.js @@ -0,0 +1,95 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + AppConstants: "resource://gre/modules/AppConstants.sys.mjs", +}); + +// Given a window, check if it meets all requirements +// of the taskbar tab chrome UI +function checkWindowChrome(win) { + let document = win.document.documentElement; + + ok( + document.hasAttribute("taskbartab"), + "The window HTML should have a taskbartab attribute" + ); + + ok(win.gURLBar.readOnly, "The URL bar should be read-only"); + + ok( + win.document.getElementById("TabsToolbar").collapsed, + "The tab bar should be collapsed" + ); + + is( + document.getAttribute("chromehidden"), + "menubar directories extrachrome ", + "The correct chrome hidden attributes should be populated" + ); + + ok(!win.menubar.visible, "menubar barprop should not be visible"); + ok(!win.personalbar.visible, "personalbar barprop should not be visible"); + + is( + document.getAttribute("pocketdisabled"), + "true", + "Pocket button should be disabled" + ); + + let starButton = win.document.querySelector("#star-button-box"); + is( + win.getComputedStyle(starButton).display, + "none", + "Bookmark button should not be visible" + ); + + ok( + !document.hasAttribute("fxatoolbarmenu"), + "Firefox accounts menu should not be displayed" + ); +} + +// Given a window, check if hamburger menu +// buttons that aren't relevant to taskbar tabs +// are hidden +function checkHamburgerMenu(win) { + let document = win.document.documentElement; + + is( + document.getAttribute("fxadisabled"), + "true", + "fxadisabled attribute should be true" + ); +} + +add_task(async function testWindowChrome() { + let extraOptions = Cc["@mozilla.org/hash-property-bag;1"].createInstance( + Ci.nsIWritablePropertyBag2 + ); + extraOptions.setPropertyAsBool("taskbartab", true); + + let args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray); + args.appendElement(null); + args.appendElement(extraOptions); + args.appendElement(null); + + // Simulate opening a taskbar tab window + let win = Services.ww.openWindow( + null, + AppConstants.BROWSER_CHROME_URL, + "_blank", + "chrome,dialog=no,titlebar,close,toolbar,location,personalbar=no,status,menubar=no,resizable,minimizable,scrollbars", + args + ); + await new Promise(resolve => { + win.addEventListener("load", resolve, { once: true }); + }); + await win.delayedStartupPromise; + + checkWindowChrome(win); + checkHamburgerMenu(win); + + await BrowserTestUtils.closeWindow(win); +}); diff --git a/browser/extensions/formautofill/api.js b/browser/extensions/formautofill/api.js index 0b32d1bc8a3..d1f6f7174f7 100644 --- a/browser/extensions/formautofill/api.js +++ b/browser/extensions/formautofill/api.js @@ -166,6 +166,7 @@ this.formautofill = class extends ExtensionAPI { esModuleURI: "resource://autofill/FormAutofillChild.sys.mjs", events: { focusin: {}, + "form-changed": { createActor: false }, "form-submission-detected": { createActor: false }, }, }, diff --git a/browser/extensions/formautofill/test/browser/browser.toml b/browser/extensions/formautofill/test/browser/browser.toml index b2ce0c93c32..3921bc36bdb 100644 --- a/browser/extensions/formautofill/test/browser/browser.toml +++ b/browser/extensions/formautofill/test/browser/browser.toml @@ -6,6 +6,10 @@ support-files = [ "../fixtures/autocomplete_iframe.html", "../fixtures/autocomplete_iframe_sandboxed.html", "../fixtures/autocomplete_simple_basic.html", + "../fixtures/dynamic_form_changes.html", + "../fixtures/form_change_on_user_interaction.html", + "../fixtures/dynamic_formless_changes_node_mutations.html", + "../fixtures/dynamic_formless_changes_element_visiblity_state.html", "../fixtures/page_navigation.html", "./empty.html", "../fixtures/**", @@ -30,6 +34,8 @@ skip-if = [ ["browser_autofill_address_select_inexact.js"] +["browser_autofill_address_select_match_isoid.js"] + ["browser_autofill_creditCard_name.js"] ["browser_autofill_creditCard_type.js"] @@ -55,6 +61,8 @@ skip-if = [ ["browser_dropdown_layout.js"] +["browser_dynamic_form_change_detection.js"] + ["browser_editAddressDialog.js"] skip-if = [ "verify", @@ -66,6 +74,8 @@ skip-if = [ ["browser_fillclear_events.js"] +["browser_fill_on_dynamic_form_change_detection.js"] + ["browser_form_changes.js"] ["browser_iframe_autofill_cc_number.js"] diff --git a/browser/extensions/formautofill/test/browser/browser_autofill_address_select_match_isoid.js b/browser/extensions/formautofill/test/browser/browser_autofill_address_select_match_isoid.js new file mode 100644 index 00000000000..3831df9c84a --- /dev/null +++ b/browser/extensions/formautofill/test/browser/browser_autofill_address_select_match_isoid.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// This test ensures that an inexact substring match on an option label is performed +// when autofilling dropdown values. + +const TEST_PROFILE = { + "given-name": "Joe", + "family-name": "Smith", + "street-address": "7 First St", + "address-level1": "BR", + country: "IN", +}; + +const MARKUP_SELECT_STATE = ` + +
+ + + + + +
+ +`; + +add_autofill_heuristic_tests([ + { + fixtureData: MARKUP_SELECT_STATE, + profile: TEST_PROFILE, + expectedResult: [ + { + default: { + reason: "autocomplete", + }, + fields: [ + { fieldName: "given-name", autofill: TEST_PROFILE["given-name"] }, + { fieldName: "family-name", autofill: TEST_PROFILE["family-name"] }, + { + fieldName: "street-address", + autofill: TEST_PROFILE["street-address"], + }, + { + fieldName: "address-level2", + autofill: TEST_PROFILE["address-level2"], + }, + { fieldName: "address-level1", autofill: 1479 }, + ], + }, + ], + }, +]); diff --git a/browser/extensions/formautofill/test/browser/browser_dynamic_form_change_detection.js b/browser/extensions/formautofill/test/browser/browser_dynamic_form_change_detection.js new file mode 100644 index 00000000000..1d420eb6eec --- /dev/null +++ b/browser/extensions/formautofill/test/browser/browser_dynamic_form_change_detection.js @@ -0,0 +1,242 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(async () => { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.formautofill.addresses.supported", "on"], + ["extensions.formautofill.creditCards.supported", "on"], + ["extensions.formautofill.heuristics.detectDynamicFormChanges", true], + ], + }); +}); + +const verifyCurrentIdentifiedFields = async (browser, expectedFields) => { + const actor = + browser.browsingContext.currentWindowGlobal.getActor("FormAutofill"); + let actualFields = Array.from(actor.sectionsByRootId.values()).flat(); + verifySectionFieldDetails(actualFields, expectedFields); +}; + +const expectedAddressFieldsExcludingAdditionalFields = [ + { + fields: [ + { fieldName: "name" }, + { fieldName: "email" }, + { fieldName: "tel" }, + { fieldName: "country" }, + ], + }, +]; + +const expectedAddressFieldsIncludingAdditionalFields = [ + { + fields: [ + { fieldName: "name" }, + { fieldName: "email" }, + { fieldName: "tel" }, + { fieldName: "country" }, + { fieldName: "street-address" }, + { fieldName: "address-level1" }, + { fieldName: "address-level2" }, + { fieldName: "postal-code" }, + ], + }, +]; + +const expectedCreditCardFieldsExcludingAdditionalFields = [ + { + fields: [{ fieldName: "cc-number" }], + }, +]; + +const expectedCreditCardFieldsIncludingAdditionalFields = [ + { + fields: [ + { fieldName: "cc-number" }, + { fieldName: "cc-name" }, + { fieldName: "cc-exp-month" }, + { fieldName: "cc-exp-year" }, + ], + }, +]; + +/** + * Tests that the identified fields are updated correctly during dynamic form changes + * + * The form changes can be of two types (see FormAutofillHeuristics.FORM_CHANGE_REASON): + * 1. An element changes its visibility state, + * e.g. visible element becomes invisible or vice versa + * 2. The form/document adds or removes nodes + * + * Both form changes should make FormAutofillChild consider triggering another identification process. + * This method tests that we identifiy the correct fields in three different scenarios: + * 1. Focusing on a field which triggers a first identification process + * 2. Filling a field that dispatches a change event which makes the site + * change the form, which triggers a second field identification process + * 3. Clearing the value from the field, which reverts the previous form change + * and triggers a third field identification process + * + * @param {string} documentPath + * @param {string} elementIdToFill + * @param {object[]} identifiedFieldsExcludingExtraFields + * @param {object[]} identifiedFieldsIncludingExtraFields + */ +const verifyIdentifiedFieldsDuringFormChange = async ( + documentPath, + elementIdToFill, + identifiedFieldsExcludingExtraFields, + identifiedFieldsIncludingExtraFields +) => { + await BrowserTestUtils.withNewTab(documentPath, async browser => { + const fieldDetectionCompletedBeforeFormChangePromise = + getFieldDetectionCompletedPromiseResolver(); + + info("Focusing on a form field to trigger the identification process"); + await SpecialPowers.spawn(browser, [elementIdToFill], elementId => { + const field = content.document.getElementById(elementId); + // Focus event invokes the first field identification process + field.focus(); + }); + + info("Waiting for initial fieldDetectionCompleted notification"); + await fieldDetectionCompletedBeforeFormChangePromise; + + info("Checking that additional fields are not identified yet"); + await verifyCurrentIdentifiedFields( + browser, + identifiedFieldsExcludingExtraFields + ); + + const fieldDetectionCompletedIncludingAdditionalFieldsPromise = + getFieldDetectionCompletedPromiseResolver(); + + info("Simulating user input so that additional field nodes are added."); + await SpecialPowers.spawn(browser, [elementIdToFill], elementId => { + let field = content.document.getElementById(elementId); + field.setUserInput("dummyValue"); + }); + + // A "form-changed" event was dispatched, triggering another field identification process + info("Waiting for another fieldDetectionCompleted notification"); + await fieldDetectionCompletedIncludingAdditionalFieldsPromise; + + info("Checking that additional address fields are also identied."); + await verifyCurrentIdentifiedFields( + browser, + identifiedFieldsIncludingExtraFields + ); + + const fieldDetectionCompletedExcludingAdditionalFieldsPromise = + getFieldDetectionCompletedPromiseResolver(); + + info("Clearing user input, so that additional fields are removed again."); + await SpecialPowers.spawn(browser, [elementIdToFill], elementId => { + let field = content.document.getElementById(elementId); + field.focus(); + field.setUserInput(""); + }); + + // A "form-changed" event was dispatched, triggering another field identification process + info("Waiting for another fieldIdentified notification"); + await fieldDetectionCompletedExcludingAdditionalFieldsPromise; + + info("Checking that additional fields are removed from identified fields"); + await verifyCurrentIdentifiedFields( + browser, + identifiedFieldsExcludingExtraFields + ); + }); +}; + +/** + * Tests that the correct address fields are identified in a form after "form-changed" + * events are dispatched with reasons "nodes-added" and "nodes-removed" + */ +add_task( + async function correct_address_fields_identified_in_form_during_form_changes_due_to_node_mutations() { + await verifyIdentifiedFieldsDuringFormChange( + FORMS_WITH_DYNAMIC_FORM_CHANGE, + "country-node-addition", + expectedAddressFieldsExcludingAdditionalFields, + expectedAddressFieldsIncludingAdditionalFields + ); + } +); + +/** + * Tests that the correct credit card fields are identified in a form after "form-changed" + * events are dispatched with reasons "nodes-added" and "nodes-removed" + */ +add_task( + async function correct_credit_card_fields_identified_in_form_during_form_changes_due_to_node_mutations() { + await verifyIdentifiedFieldsDuringFormChange( + FORMS_WITH_DYNAMIC_FORM_CHANGE, + "cc-number-node-addition", + expectedCreditCardFieldsExcludingAdditionalFields, + expectedCreditCardFieldsIncludingAdditionalFields + ); + } +); + +/** + * Tests that the correct address fields are identified in a form after "form-changed" + * events are dispatched with reasons "visible-element-became-invisible" and "invisible-element-became-visible" + */ +add_task( + async function correct_address_fields_identified_in_form_during_form_changes_due_to_element_visibility_change() { + await verifyIdentifiedFieldsDuringFormChange( + FORMS_WITH_DYNAMIC_FORM_CHANGE, + "country-visibility-change", + expectedAddressFieldsExcludingAdditionalFields, + expectedAddressFieldsIncludingAdditionalFields + ); + } +); + +/** + * Tests that the correct credit card fields are identified in a form after "form-changed" + * events are dispatched with reasons "visible-element-became-invisible" and "invisible-element-became-visible" + */ +add_task( + async function correct_credit_card_fields_identified_in_form_during_form_changes_due_to_element_visibility_change() { + await verifyIdentifiedFieldsDuringFormChange( + FORMS_WITH_DYNAMIC_FORM_CHANGE, + "cc-number-visibility-change", + expectedCreditCardFieldsExcludingAdditionalFields, + expectedCreditCardFieldsIncludingAdditionalFields + ); + } +); + +/** + * Tests that the correct fields are identified in a document (form-less) after "form-changed" + * events are dispatched with reasons "nodes-added" and "nodes-removed" + */ +add_task( + async function correct_fields_identified_in_formless_document_during_form_changes_due_to_node_mutations() { + await verifyIdentifiedFieldsDuringFormChange( + FORMLESS_FIELDS_WITH_DYNAMIC_FORM_CHANGE_AFTER_NODE_MUTATIONS, + "country-node-addition", + expectedAddressFieldsExcludingAdditionalFields, + expectedAddressFieldsIncludingAdditionalFields + ); + } +); + +/** + * Tests that the correct fields are identified in a document (form-less) after "form-changed" + * events are dispatched with reasons "visible-element-became-invisible" and "invisible-element-became-visible" + */ +add_task( + async function correct_fields_identified_in_formless_document_during_form_changes_due_to_element_visibility_change() { + await verifyIdentifiedFieldsDuringFormChange( + FORMLESS_FIELDS_WITH_DYNAMIC_FORM_CHANGE_AFTER_VISIBILITY_STATE_CHANGE, + "country-visibility-change", + expectedAddressFieldsExcludingAdditionalFields, + expectedAddressFieldsIncludingAdditionalFields + ); + } +); diff --git a/browser/extensions/formautofill/test/browser/browser_fill_on_dynamic_form_change_detection.js b/browser/extensions/formautofill/test/browser/browser_fill_on_dynamic_form_change_detection.js new file mode 100644 index 00000000000..fd0703fca69 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/browser_fill_on_dynamic_form_change_detection.js @@ -0,0 +1,271 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { FormAutofill } = ChromeUtils.importESModule( + "resource://autofill/FormAutofill.sys.mjs" +); + +add_setup(async () => { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.formautofill.addresses.supported", "on"], + ["extensions.formautofill.creditCards.supported", "on"], + ["extensions.formautofill.heuristics.detectDynamicFormChanges", true], + ["extensions.formautofill.heuristics.fillOnDynamicFormChanges", true], + [ + "extensions.formautofill.heuristics.fillOnDynamicFormChanges.timeout", + 1000, + ], + ], + }); + const oldValue = FormAutofillUtils.getOSAuthEnabled( + FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF + ); + FormAutofillUtils.setOSAuthEnabled( + FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF, + false + ); + + await setStorage(TEST_ADDRESS_1); + await setStorage(TEST_CREDIT_CARD_1); + + registerCleanupFunction(async () => { + await removeAllRecords(); + FormAutofillUtils.setOSAuthEnabled( + FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF, + oldValue + ); + }); +}); + +const expectedFilledCreditCardFields = { + fields: [ + { fieldName: "cc-number", autofill: TEST_CREDIT_CARD_1["cc-number"] }, + { fieldName: "cc-name", autofill: TEST_CREDIT_CARD_1["cc-name"] }, + { fieldName: "cc-exp-month", autofill: TEST_CREDIT_CARD_1["cc-exp-month"] }, + { fieldName: "cc-exp-year", autofill: TEST_CREDIT_CARD_1["cc-exp-year"] }, + ], +}; + +const expectedFilledAddressFields = { + fields: [ + { fieldName: "name", autofill: "John R. Smith" }, + { fieldName: "email", autofill: TEST_ADDRESS_1.email }, + { fieldName: "tel", autofill: TEST_ADDRESS_1.tel }, + { fieldName: "country", autofill: TEST_ADDRESS_1.country }, + { + fieldName: "street-address", + autofill: TEST_ADDRESS_1["street-address"].replace("\n", " "), + }, + { fieldName: "address-level1", autofill: TEST_ADDRESS_1["address-level1"] }, + { fieldName: "address-level2", autofill: TEST_ADDRESS_1["address-level2"] }, + { fieldName: "postal-code", autofill: TEST_ADDRESS_1["postal-code"] }, + ], +}; + +/** + * Verify that fields that are added/become visible immediately after + * an initial autocompletion get filled as well + * + * @param {string} documentPath + * @param {string} selectorToTriggerAutocompletion + */ +const verifyAutofilledFieldsDuringFormChange = async ( + documentPath, + selectorToTriggerAutocompletion, + elementValueToVerifyAutofill, + expectedSection +) => { + await BrowserTestUtils.withNewTab(documentPath, async browser => { + info("Triggering autocompletion."); + await openPopupOn(browser, selectorToTriggerAutocompletion); + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser); + await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser); + + const filledOnFormChangePromise = TestUtils.topicObserved( + "formautofill-fill-after-form-change-complete" + ); + + await waitForAutofill( + browser, + selectorToTriggerAutocompletion, + elementValueToVerifyAutofill + ); + info( + `Waiting for "formautofill-fill-after-form-change-complete" notification` + ); + await filledOnFormChangePromise; + + info("Verifying that all fields are filled correctly."); + const actor = + browser.browsingContext.currentWindowGlobal.getActor("FormAutofill"); + const section = Array.from(actor.sectionsByRootId.values()).flat()[0]; + await verifyAutofillResult(browser, section, expectedSection); + }); +}; + +/** + * Tests that all address fields are filled. + * Form adds autofillable input fields after country field is modified + */ +add_task( + async function address_fields_filled_in_form_during_form_changes_due_to_node_mutations() { + await verifyAutofilledFieldsDuringFormChange( + FORMS_WITH_DYNAMIC_FORM_CHANGE, + "#country-node-addition", + TEST_ADDRESS_1.country, + expectedFilledAddressFields + ); + } +); + +/** + * Tests that all credit card fields are filled. + * Form adds autofillable input fields after cc number field is modified. + */ +add_task( + async function credit_card_fields_filled_in_form_during_form_changes_due_to_node_mutations() { + await verifyAutofilledFieldsDuringFormChange( + FORMS_WITH_DYNAMIC_FORM_CHANGE, + "#cc-number-node-addition", + TEST_CREDIT_CARD_1["cc-number"], + expectedFilledCreditCardFields + ); + } +); + +/** + * Tests that all address fields are filled. + * Form makes invisible autofillable input fields become visible after country field is modified + */ +add_task( + async function address_fields_filled_in_form_during_form_changes_due_to_element_visibility_change() { + await verifyAutofilledFieldsDuringFormChange( + FORMS_WITH_DYNAMIC_FORM_CHANGE, + "#country-visibility-change", + TEST_ADDRESS_1.country, + expectedFilledAddressFields + ); + } +); + +/** + * Tests that all credit card fields are filled. + * Form makes invisible autofillable input fields become visible after cc number field is modified. + */ +add_task( + async function credit_card_fields_filled_in_form_during_form_changes_due_to_element_visibility_change() { + await verifyAutofilledFieldsDuringFormChange( + FORMS_WITH_DYNAMIC_FORM_CHANGE, + "#cc-number-visibility-change", + TEST_CREDIT_CARD_1["cc-number"], + expectedFilledCreditCardFields + ); + } +); + +/** + * Tests that all fields are filled. + * Formless document adds autofillable input fields after country field is modified. + */ +add_task( + async function address_fields_filled_in_formless_document_during_form_changes_due_to_node_mutations() { + await verifyAutofilledFieldsDuringFormChange( + FORMLESS_FIELDS_WITH_DYNAMIC_FORM_CHANGE_AFTER_NODE_MUTATIONS, + "#country-node-addition", + TEST_ADDRESS_1.country, + expectedFilledAddressFields + ); + } +); + +/** + * Tests that all fields are filled. + * Formless document makes invisible autofillable input fields become visible after country field is modified. + */ +add_task( + async function address_fields_filled_in_formless_document_during_form_changes_due_to_element_visibility_change() { + await verifyAutofilledFieldsDuringFormChange( + FORMLESS_FIELDS_WITH_DYNAMIC_FORM_CHANGE_AFTER_VISIBILITY_STATE_CHANGE, + "#country-visibility-change", + TEST_ADDRESS_1.country, + expectedFilledAddressFields + ); + } +); + +/** + * Tests that additional fields are not filled when the form change was initiated + * by a user interaction that triggered a "click" event on the form. + */ +add_task( + async function additional_fields_not_filled_on_user_initiated_form_change() { + await BrowserTestUtils.withNewTab( + FORM_WITH_USER_INITIATED_FORM_CHANGE, + async browser => { + const selectorToTriggerAutocompletion = "#country-visibility-change"; + const elementValueToVerifyAutofill = TEST_ADDRESS_1.country; + + info("Triggering autocompletion."); + await openPopupOn(browser, selectorToTriggerAutocompletion); + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser); + await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser); + await waitForAutofill( + browser, + selectorToTriggerAutocompletion, + elementValueToVerifyAutofill + ); + + info( + "Simulating user interaction to cancel any filling on dynamic form change actions" + ); + const showFieldButtonSelector = "#show-fields-btn"; + await SpecialPowers.spawn( + browser, + [showFieldButtonSelector], + async btnSelector => { + const showFieldsButton = + content.document.querySelector(btnSelector); + showFieldsButton.click(); + } + ); + + info( + "Waiting for any possible filling on dynamic form change to complete" + ); + /* eslint-disable mozilla/no-arbitrary-setTimeout */ + await new Promise(resolve => { + setTimeout(resolve, FormAutofill.fillOnDynamicFormChangeTimeout); + }); + + info( + "Verifying that all fields are detected, but additional fields are not filled" + ); + const expectedAdditionalFieldsNotFilled = { + fields: [ + { fieldName: "name", autofill: "John R. Smith" }, + { fieldName: "email", autofill: TEST_ADDRESS_1.email }, + { fieldName: "tel", autofill: TEST_ADDRESS_1.tel }, + { fieldName: "country", autofill: TEST_ADDRESS_1.country }, + { + fieldName: "street-address", + }, + { fieldName: "address-level1" }, + { fieldName: "address-level2" }, + { fieldName: "postal-code" }, + ], + }; + const actor = + browser.browsingContext.currentWindowGlobal.getActor("FormAutofill"); + const section = Array.from(actor.sectionsByRootId.values()).flat()[0]; + await verifyAutofillResult( + browser, + section, + expectedAdditionalFieldsNotFilled + ); + } + ); + } +); diff --git a/browser/extensions/formautofill/test/browser/browser_form_changes.js b/browser/extensions/formautofill/test/browser/browser_form_changes.js index a1d7ce51b02..f1fd9f82c4d 100644 --- a/browser/extensions/formautofill/test/browser/browser_form_changes.js +++ b/browser/extensions/formautofill/test/browser/browser_form_changes.js @@ -135,8 +135,13 @@ async function checkFormChangeHappened(formId) { // Click the first entry of the autocomplete popup and make sure all fields are autofilled await checkFieldsAutofilled(browser, formId, MOCK_STORAGE[0]); - // This is for checking the changes of element count. + const fieldsDetectedAfterFieldAdded = + getFieldDetectionCompletedPromiseResolver(); + addInputField(browser, formId, "address-level2"); + + await fieldsDetectedAfterFieldAdded; + await openPopupOn(browser, `#${formId} input[name=name]`); // Click on an autofilled field would show an autocomplete popup with "clear form" entry @@ -144,14 +149,20 @@ async function checkFormChangeHappened(formId) { browser, [ "Clear Autofill Form", // Clear Autofill Form - "Manage addresses", // FormAutofill Preferemce + "Manage addresses", // FormAutofill Preference ], 0 ); + const fieldDetectedAfterFieldMutations = + getFieldDetectionCompletedPromiseResolver(); + // This is for checking the changes of element removed and added then. removeInputField(browser, `#${formId} input[name=address-level2]`); addInputField(browser, formId, "address-level2"); + + await fieldDetectedAfterFieldMutations; + await openPopupOn(browser, `#${formId} input[name=address-level2]`); await checkMenuEntries( @@ -178,6 +189,11 @@ async function checkFormChangeHappened(formId) { add_setup(async function () { await setStorage(MOCK_STORAGE[0]); await setStorage(MOCK_STORAGE[1]); + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.formautofill.heuristics.detectDynamicFormChanges", true], + ], + }); }); add_task(async function check_change_happened_in_form() { diff --git a/browser/extensions/formautofill/test/browser/head.js b/browser/extensions/formautofill/test/browser/head.js index a82941ec4b1..308973abbbf 100644 --- a/browser/extensions/formautofill/test/browser/head.js +++ b/browser/extensions/formautofill/test/browser/head.js @@ -84,6 +84,20 @@ const ADDRESS_FORM_WITH_PAGE_NAVIGATION_BUTTONS = "address/capture_address_on_page_navigation.html"; const FORM_IFRAME_SANDBOXED_URL = "https://example.org" + HTTP_TEST_PATH + "autocomplete_iframe_sandboxed.html"; +const FORMS_WITH_DYNAMIC_FORM_CHANGE = + "https://example.org" + HTTP_TEST_PATH + "dynamic_form_changes.html"; +const FORM_WITH_USER_INITIATED_FORM_CHANGE = + "https://example.org" + + HTTP_TEST_PATH + + "form_change_on_user_interaction.html"; +const FORMLESS_FIELDS_WITH_DYNAMIC_FORM_CHANGE_AFTER_NODE_MUTATIONS = + "https://example.org" + + HTTP_TEST_PATH + + "dynamic_formless_changes_node_mutations.html"; +const FORMLESS_FIELDS_WITH_DYNAMIC_FORM_CHANGE_AFTER_VISIBILITY_STATE_CHANGE = + "https://example.org" + + HTTP_TEST_PATH + + "dynamic_formless_changes_element_visiblity_state.html"; const CREDITCARD_FORM_URL = "https://example.org" + HTTP_TEST_PATH + @@ -377,6 +391,28 @@ async function waitForStorageChangedEvents(...eventTypes) { ); } +/** + * Sets up a promise that resolves when the FormAutofillParent sends out a notification + * that the field detection processes have completed in all FormAutofill children. + * + * @returns {Promise} + */ +async function getFieldDetectionCompletedPromiseResolver() { + let fieldDetectionCompletedPromiseResolver; + const fieldDetectionCompletedObserver = { + fieldDetectionCompleted() { + info(`All fields detected.`); + fieldDetectionCompletedPromiseResolver(); + FormAutofillParent.removeMessageObserver(fieldDetectionCompletedObserver); + }, + }; + + return new Promise(resolve => { + fieldDetectionCompletedPromiseResolver = resolve; + FormAutofillParent.addMessageObserver(fieldDetectionCompletedObserver); + }); +} + /** * Wait until the element found matches the expected autofill value * diff --git a/browser/extensions/formautofill/test/fixtures/dynamic_form_changes.html b/browser/extensions/formautofill/test/fixtures/dynamic_form_changes.html new file mode 100644 index 00000000000..ca09baea2ba --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/dynamic_form_changes.html @@ -0,0 +1,150 @@ + + + + + + Document + + + +
+ + + + + + + + + + + + +
+
+ + +
+ + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + + + + + + +
+ + + + diff --git a/browser/extensions/formautofill/test/fixtures/dynamic_formless_changes_element_visiblity_state.html b/browser/extensions/formautofill/test/fixtures/dynamic_formless_changes_element_visiblity_state.html new file mode 100644 index 00000000000..9144bd44452 --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/dynamic_formless_changes_element_visiblity_state.html @@ -0,0 +1,53 @@ + + + + + + Document + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/browser/extensions/formautofill/test/fixtures/dynamic_formless_changes_node_mutations.html b/browser/extensions/formautofill/test/fixtures/dynamic_formless_changes_node_mutations.html new file mode 100644 index 00000000000..4f323a86242 --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/dynamic_formless_changes_node_mutations.html @@ -0,0 +1,60 @@ + + + + + + Document + + + +
+ + + + + + + + + + + + +
+
+ + + + + diff --git a/browser/extensions/formautofill/test/fixtures/form_change_on_user_interaction.html b/browser/extensions/formautofill/test/fixtures/form_change_on_user_interaction.html new file mode 100644 index 00000000000..e8f7aeeae22 --- /dev/null +++ b/browser/extensions/formautofill/test/fixtures/form_change_on_user_interaction.html @@ -0,0 +1,46 @@ + + + + + Form Autofill Address Demo Page + + +

Form Autofill Address Demo Page

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + diff --git a/browser/extensions/formautofill/test/fixtures/third_party/NewEgg/BillingInfo.html b/browser/extensions/formautofill/test/fixtures/third_party/NewEgg/BillingInfo.html index d44bc01b704..5ca0f3b4122 100644 --- a/browser/extensions/formautofill/test/fixtures/third_party/NewEgg/BillingInfo.html +++ b/browser/extensions/formautofill/test/fixtures/third_party/NewEgg/BillingInfo.html @@ -21,7 +21,7 @@
- + `, - inputId: "typeA", - expectedLabelIds: ["labelA"], + expectedLabelIds: [["labelA"]], }, { description: "Input contains in a label element.", @@ -23,14 +22,14 @@ const TESTCASES = [
`, inputId: "typeB", - expectedLabelIds: ["labelB"], + expectedLabelIds: [["labelB"]], }, { description: '"for" attribute used to indicate input by one label.', document: ` `, inputId: "typeC", - expectedLabelIds: ["labelC"], + expectedLabelIds: [["labelC"]], }, { description: '"for" attribute used to indicate input by multiple labels.', @@ -41,7 +40,7 @@ const TESTCASES = [ `, inputId: "typeD", - expectedLabelIds: ["labelD1", "labelD2", "labelD3"], + expectedLabelIds: [["labelD1", "labelD2", "labelD3"]], }, { description: @@ -52,7 +51,7 @@ const TESTCASES = [ `, inputId: " typeE ", - expectedLabelIds: [], + expectedLabelIds: [[]], }, { description: "Input contains in a label element.", @@ -63,7 +62,7 @@ const TESTCASES = [
`, inputId: "typeF", - expectedLabelIds: ["labelF"], + expectedLabelIds: [["labelF"], [""]], }, { description: @@ -75,7 +74,77 @@ const TESTCASES = [ `, inputId: "typeG", - expectedLabelIds: ["labelG1", "labelG2", "labelG3"], + expectedLabelIds: [["labelG1", "labelG2", "labelG3"]], + }, + { + description: + "labels with no for attribute or child with one input at a different level", + document: `
+ + + +
+
`, + inputId: "labelH1", + expectedLabelIds: [["labelH1"], ["labelH2"]], + }, + { + description: + "labels with no for attribute or child with an input and button", + document: `
+ + + +
diff --git a/testing/web-platform/tests/css/css-overflow/scroll-buttons-appearance.html b/testing/web-platform/tests/css/css-overflow/scroll-buttons-appearance.html new file mode 100644 index 00000000000..9a9cd10ee78 --- /dev/null +++ b/testing/web-platform/tests/css/css-overflow/scroll-buttons-appearance.html @@ -0,0 +1,19 @@ + + +CSS Test: ::scroll-button()s appearance + + + +

Test passes if there are two buttons, the first one using appearance auto +and the second using appearance: none.

+
diff --git a/testing/web-platform/tests/css/css-overflow/scroll-buttons-disabled-ref.html b/testing/web-platform/tests/css/css-overflow/scroll-buttons-disabled-ref.html index 19d94020568..be6b5a243a1 100644 --- a/testing/web-platform/tests/css/css-overflow/scroll-buttons-disabled-ref.html +++ b/testing/web-platform/tests/css/css-overflow/scroll-buttons-disabled-ref.html @@ -4,9 +4,6 @@ * { margin: 0; } - button { - appearance: none; - } #scroller { width: 600px; diff --git a/testing/web-platform/tests/css/css-overflow/scroll-buttons-disabled-rtl-ref.html b/testing/web-platform/tests/css/css-overflow/scroll-buttons-disabled-rtl-ref.html index 8caeea5c122..0f87226481e 100644 --- a/testing/web-platform/tests/css/css-overflow/scroll-buttons-disabled-rtl-ref.html +++ b/testing/web-platform/tests/css/css-overflow/scroll-buttons-disabled-rtl-ref.html @@ -4,9 +4,6 @@ * { margin: 0; } - button { - appearance: none; - } #scroller { width: 600px; diff --git a/testing/web-platform/tests/css/css-overflow/scroll-buttons-disabled-vertical-ltr-ref.html b/testing/web-platform/tests/css/css-overflow/scroll-buttons-disabled-vertical-ltr-ref.html index dbc5762ed4e..e05b9bed067 100644 --- a/testing/web-platform/tests/css/css-overflow/scroll-buttons-disabled-vertical-ltr-ref.html +++ b/testing/web-platform/tests/css/css-overflow/scroll-buttons-disabled-vertical-ltr-ref.html @@ -4,9 +4,6 @@ * { margin: 0; } - button { - appearance: none; - } #scroller { width: 600px; diff --git a/testing/web-platform/tests/css/css-overflow/scroll-buttons-enabled-ref.html b/testing/web-platform/tests/css/css-overflow/scroll-buttons-enabled-ref.html index 9a682eb668d..622f205d328 100644 --- a/testing/web-platform/tests/css/css-overflow/scroll-buttons-enabled-ref.html +++ b/testing/web-platform/tests/css/css-overflow/scroll-buttons-enabled-ref.html @@ -6,9 +6,6 @@ margin: 0; font-family: Ahem; } - button { - appearance: none; - } #scroller { width: 600px; diff --git a/testing/web-platform/tests/css/css-overflow/scroll-buttons-enabled-rtl-ref.html b/testing/web-platform/tests/css/css-overflow/scroll-buttons-enabled-rtl-ref.html index 33a66521f80..bba03e679ab 100644 --- a/testing/web-platform/tests/css/css-overflow/scroll-buttons-enabled-rtl-ref.html +++ b/testing/web-platform/tests/css/css-overflow/scroll-buttons-enabled-rtl-ref.html @@ -6,9 +6,6 @@ margin: 0; font-family: Ahem; } - button { - appearance: none; - } #scroller { width: 600px; diff --git a/testing/web-platform/tests/css/css-overflow/scroll-buttons-enabled-vertical-ltr-ref.html b/testing/web-platform/tests/css/css-overflow/scroll-buttons-enabled-vertical-ltr-ref.html index 2793ddbc9a4..76cd6a69846 100644 --- a/testing/web-platform/tests/css/css-overflow/scroll-buttons-enabled-vertical-ltr-ref.html +++ b/testing/web-platform/tests/css/css-overflow/scroll-buttons-enabled-vertical-ltr-ref.html @@ -6,9 +6,6 @@ margin: 0; font-family: Ahem; } - button { - appearance: none; - } #scroller { width: 600px; diff --git a/testing/web-platform/tests/css/css-overflow/scroll-marker-focus-visible.html b/testing/web-platform/tests/css/css-overflow/scroll-marker-focus-visible.html new file mode 100644 index 00000000000..cc9399ae166 --- /dev/null +++ b/testing/web-platform/tests/css/css-overflow/scroll-marker-focus-visible.html @@ -0,0 +1,60 @@ + + +CSS Overflow Test: ::scroll-marker supports :focus-visible + + + + + + + +
+
+
+
+ \ No newline at end of file diff --git a/testing/web-platform/tests/css/css-scoping/svg-id-ref-001.html b/testing/web-platform/tests/css/css-scoping/svg-id-ref-001.html new file mode 100644 index 00000000000..81deb379f8f --- /dev/null +++ b/testing/web-platform/tests/css/css-scoping/svg-id-ref-001.html @@ -0,0 +1,36 @@ + +Same clip-path id's in different tree scopes + + + + +

Test passes if you see a single 100px by 100px green box below.

+
+ + + + + + +
+
+ diff --git a/testing/web-platform/tests/css/css-values/if-conditionals.html b/testing/web-platform/tests/css/css-values/if-conditionals.html index 001c603a090..21ef0c58566 100644 --- a/testing/web-platform/tests/css/css-values/if-conditionals.html +++ b/testing/web-platform/tests/css/css-values/if-conditionals.html @@ -522,7 +522,39 @@ else: false_value)`, 'true_value'); - // media() and style() queries in the condition + // supports() queries in the condition + test_if(`if(supports((display: table-cell)): true_value; + else: false_value)`, + 'true_value'); + test_if(`if(supports(display: table-cell): true_value; + else: false_value)`, + 'true_value'); + test_if(`if(supports(display): true_value; + else: false_value)`, + 'false_value'); + test_if(`if(supports(display: invalid): true_value; + else: false_value)`, + 'false_value'); + test_if(`if(supports(not (transform-origin: 10em 10em 10em)): true_value; + else: false_value)`, + 'false_value') + test_if(`if(supports(selector(h2 > p)): true_value; + else: false_value)`, + 'true_value'); + test_if(`if(supports((selector(h2 > p))): true_value; + else: false_value)`, + 'true_value'); + test_if(`if(supports((display: table-cell) and (display: list-item) and (display: contents)): true_value; + else: false_value)`, + 'true_value'); + test_if(`if(supports((display: invalid) and (display: list-item) and (display: contents)): true_value; + else: false_value)`, + 'false_value'); + test_if(`if(supports((display: table-cell) and (invalid: list-item) and (display: contents)): true_value; + else: false_value)`, + 'false_value'); + + // media(), style() and supports() queries in the condition test_if(`if((media(screen and (min-width: 1px))) or (style(--x)): true_value; else: false_value)`, 'true_value'); @@ -532,6 +564,9 @@ test_if(`if((media((min-color: 1) and (height <= 999999px))) and (style(--x)): true_value; else: false_value)`, 'true_value'); + test_if(`if((media((min-color: 8) and (height <= 600px)) and style(--x: 3px)) or supports(display: table-cell): true_value; + else: false_value)`, + 'true_value'); // Invalid if() syntax test_if_with_custom_properties('if()', [['--x', '3']], ''); diff --git a/testing/web-platform/tests/css/css-values/if-supports-quirks.html b/testing/web-platform/tests/css/css-values/if-supports-quirks.html new file mode 100644 index 00000000000..f64eb8ea85f --- /dev/null +++ b/testing/web-platform/tests/css/css-values/if-supports-quirks.html @@ -0,0 +1,20 @@ + +CSS Values and Units Test: CSS inline if() function supports() in quirks mode + + + + + +
test
+ \ No newline at end of file diff --git a/testing/web-platform/tests/css/css-writing-modes/orthogonal-child-with-border.html b/testing/web-platform/tests/css/css-writing-modes/orthogonal-child-with-border.html new file mode 100644 index 00000000000..877cce5a710 --- /dev/null +++ b/testing/web-platform/tests/css/css-writing-modes/orthogonal-child-with-border.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + +

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

+ +
+
+
+
+ + diff --git a/testing/web-platform/tests/custom-elements/revamped-scoped-registry/ShadowRoot-init-customElements.tentative.html b/testing/web-platform/tests/custom-elements/revamped-scoped-registry/ShadowRoot-init-customElements.tentative.html index e8f13c6a5f2..6169bfff116 100644 --- a/testing/web-platform/tests/custom-elements/revamped-scoped-registry/ShadowRoot-init-customElements.tentative.html +++ b/testing/web-platform/tests/custom-elements/revamped-scoped-registry/ShadowRoot-init-customElements.tentative.html @@ -35,8 +35,8 @@ test(() => { test(() => { const host = document.body.appendChild(document.createElement('div')); - assert_throws_js(TypeError, () => host.attachShadow({mode: 'closed', customElements: null})); -}, 'attachShadow() should throw for a null customElements value'); + host.attachShadow({mode: 'closed', customElements: null}); +}, 'attachShadow() should not throw for a null customElements value'); diff --git a/testing/web-platform/tests/custom-elements/revamped-scoped-registry/polymer-polyfill-regression.tentative.html b/testing/web-platform/tests/custom-elements/revamped-scoped-registry/polymer-polyfill-regression.tentative.html new file mode 100644 index 00000000000..d3d56c8125e --- /dev/null +++ b/testing/web-platform/tests/custom-elements/revamped-scoped-registry/polymer-polyfill-regression.tentative.html @@ -0,0 +1,22 @@ + + + + + + + + + + + + diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-take.any.js b/testing/web-platform/tests/dom/observable/tentative/observable-take.any.js index 8350d0214cc..736a08abe31 100644 --- a/testing/web-platform/tests/dom/observable/tentative/observable-take.any.js +++ b/testing/web-platform/tests/dom/observable/tentative/observable-take.any.js @@ -106,3 +106,28 @@ test(() => { assert_array_equals(results, ["source subscribe", 1, 2, 3, "complete"]); }, "take(): Negative count is treated as maximum value"); + +// This tests a regression in Chromium's implementation. In ref-counted +// producers, when Subscriber#next() is called, the Subscriber iterates over all +// of its "internal observers" [1] and calls "next" on them. However, "next" can +// complete the subscription, and modify the "internal observers" list while +// Subscriber is iterating over it. This mutation-during-iteration caused a +// crash regression in Chromium, which this test covers. +// +// [1]: https://wicg.github.io/observable/#subscriber-internal-observers +promise_test(async () => { + async function* asyncNumbers() { + yield* [1,2,3,4]; + } + + const source = Observable.from(asyncNumbers()); + const results = []; + + source.take(1).toArray().then(result => results.push(result)); + await source.take(3).toArray().then(result => results.push(result)); + + assert_equals(results.length, 2); + assert_array_equals(results[0], [1]); + assert_array_equals(results[1], [1, 2, 3]); +}, "take(): No crash when take(1) unsubscribes from its source when next() " + + "is called, and the Subscriber iterates over the rest of the Observables"); diff --git a/testing/web-platform/tests/fledge/tentative/generate-bid-browser-signals.https.window.js b/testing/web-platform/tests/fledge/tentative/generate-bid-browser-signals.https.window.js index 00c4bfdcc31..e0cbe96fd94 100644 --- a/testing/web-platform/tests/fledge/tentative/generate-bid-browser-signals.https.window.js +++ b/testing/web-platform/tests/fledge/tentative/generate-bid-browser-signals.https.window.js @@ -51,6 +51,11 @@ subsetTest(promise_test, async test => { // Remove deprecated field, if present. delete browserSignals.prevWins; + // encode/decode utf-8 are tested separately, and aren't + // suitable to equality testing. + delete browserSignals.encodeUtf8; + delete browserSignals.decodeUtf8; + if (!deepEquals(browserSignals, expectedBrowserSignals)) throw "Unexpected browserSignals: " + JSON.stringify(browserSignals);` }); diff --git a/testing/web-platform/tests/fledge/tentative/utf8-helpers.https.window.js b/testing/web-platform/tests/fledge/tentative/utf8-helpers.https.window.js new file mode 100644 index 00000000000..7f5391f32c3 --- /dev/null +++ b/testing/web-platform/tests/fledge/tentative/utf8-helpers.https.window.js @@ -0,0 +1,209 @@ +// META: script=/resources/testdriver.js +// META: script=/resources/testdriver-vendor.js +// META: script=/common/utils.js +// META: script=resources/fledge-util.sub.js +// META: script=/common/subset-tests.js +// META: timeout=long +// META: variant=?1-5 +// META: variant=?6-10 +// META: variant=?11-15 + +'use strict;' + +// These tests cover encodeUtf8 and decodeUtf8. + +const helpers = ` + function assertEq(l, r, label) { + if (l !== r) + throw 'Mismatch ' + label; + } + + function assertByteArray(result, expect) { + if (!(result instanceof Uint8Array)) { + throw 'Not a Uint8Array!'; + } + assertEq(result.length, expect.length, 'length'); + for (var i = 0; i < result.length; ++i) { + assertEq(result[i], expect[i], i); + } + } + + function assertString(result, expect) { + if (typeof result !== 'string') { + throw 'Not a string'; + } + assertEq(result.length, expect.length, 'length'); + for (var i = 0; i < result.length; ++i) { + assertEq(result.charCodeAt(i), expect.charCodeAt(i), i); + } + } +` + +async function testConversion(test, conversionBody) { + const uuid = generateUuid(test); + let sellerReportURL = createSellerReportURL(uuid); + let bidderReportURL = createBidderReportURL(uuid); + + let fullBody = ` + ${helpers} + ${conversionBody} + `; + + let biddingLogicURL = createBiddingScriptURL({ + generateBid: fullBody, + reportWin: fullBody + `sendReportTo('${bidderReportURL}')` + }); + + let decisionLogicURL = createDecisionScriptURL(uuid, { + scoreAd: fullBody, + reportResult: fullBody + `sendReportTo('${sellerReportURL}')` + }); + + await joinInterestGroup(test, uuid, {biddingLogicURL: biddingLogicURL}); + await runBasicFledgeAuctionAndNavigate( + test, uuid, {decisionLogicURL: decisionLogicURL}); + await waitForObservedRequests(uuid, [sellerReportURL, bidderReportURL]); +} + +async function testConversionException(test, conversionBody) { + const uuid = generateUuid(test); + let sellerReportURL = createSellerReportURL(uuid); + let bidderReportURL = createBidderReportURL(uuid); + + let fullBody = ` + ${helpers} + try { + ${conversionBody}; + return -1; + } catch (e) { + } + `; + + let biddingLogicURL = createBiddingScriptURL({ + generateBid: fullBody, + reportWin: fullBody + `sendReportTo('${bidderReportURL}')` + }); + + let decisionLogicURL = createDecisionScriptURL(uuid, { + scoreAd: fullBody, + reportResult: fullBody + `sendReportTo('${sellerReportURL}')` + }); + + await joinInterestGroup(test, uuid, {biddingLogicURL: biddingLogicURL}); + await runBasicFledgeAuctionAndNavigate( + test, uuid, {decisionLogicURL: decisionLogicURL}); + await waitForObservedRequests(uuid, [sellerReportURL, bidderReportURL]); +} + +subsetTest(promise_test, async test => { + await testConversion( + test, `let result = browserSignals.encodeUtf8('ABC\u0490'); + assertByteArray(result, [65, 66, 67, 0xD2, 0x90])`); +}, 'encodeUtf8 - basic'); + +subsetTest(promise_test, async test => { + await testConversion( + test, `let result = browserSignals.encodeUtf8('A\uD800C'); + assertByteArray(result, [65, 0xEF, 0xBF, 0xBD, 67])`); +}, 'encodeUtf8 - mismatched surrogate gets replaced'); + +subsetTest(promise_test, async test => { + await testConversion( + test, `let result = browserSignals.encodeUtf8('A\uD83D\uDE02C'); + assertByteArray(result, [65, 0xF0, 0x9F, 0x98, 0x82, 67])`); +}, 'encodeUtf8 - surrogate pair combined'); + +subsetTest(promise_test, async test => { + const conversionBody = ` + let obj = { + toString: () => "ABC" + }; + let result = browserSignals.encodeUtf8(obj); + assertByteArray(result, [65, 66, 67]) + `; + await testConversion(test, conversionBody); +}, 'encodeUtf8 - custom string conversion'); + +subsetTest(promise_test, async test => { + const conversionBody = ` + let result = browserSignals.encodeUtf8(); + `; + await testConversionException(test, conversionBody); +}, 'encodeUtf8 - not enough arguments'); + +subsetTest(promise_test, async test => { + const conversionBody = ` + let obj = { + toString: () => { throw 'no go' } + }; + let result = browserSignals.encodeUtf8(obj); + `; + await testConversionException(test, conversionBody); +}, 'encodeUtf8 - custom string conversion failure'); + +subsetTest(promise_test, async test => { + const conversionBody = ` + let input = new Uint8Array([65, 66, 0xD2, 0x90, 67]); + let result = browserSignals.decodeUtf8(input); + assertString(result, 'AB\u0490C'); + `; + await testConversion(test, conversionBody); +}, 'decodeUtf8 - basic'); + +subsetTest(promise_test, async test => { + const conversionBody = ` + let input = new Uint8Array([65, 32, 0xD2]); + let result = browserSignals.decodeUtf8(input); + if (result.indexOf('\uFFFD') === -1) + throw 'Should have replacement character'; + `; + await testConversion(test, conversionBody); +}, 'decodeUtf8 - broken utf-8'); + +subsetTest(promise_test, async test => { + const conversionBody = ` + let input = new Uint8Array([65, 32, 0xED, 0xA0, 0x80, 66]); + let result = browserSignals.decodeUtf8(input); + if (result.indexOf('\uFFFD') === -1) + throw 'Should have replacement character'; + `; + await testConversion(test, conversionBody); +}, 'decodeUtf8 - mismatched surrogate'); + +subsetTest(promise_test, async test => { + const conversionBody = ` + let input = new Uint8Array([65, 0xF0, 0x9F, 0x98, 0x82, 67]); + let result = browserSignals.decodeUtf8(input); + assertString(result, 'A\uD83D\uDE02C'); + `; + await testConversion(test, conversionBody); +}, 'decodeUtf8 - non-BMP character'); + +subsetTest(promise_test, async test => { + const conversionBody = ` + let buffer = new ArrayBuffer(8); + let fullView = new Uint8Array(buffer); + for (let i = 0; i < fullView.length; ++i) + fullView[i] = 65 + i; + let partialView = new Uint8Array(buffer, 2, 3); + assertString(browserSignals.decodeUtf8(fullView), + 'ABCDEFGH'); + assertString(browserSignals.decodeUtf8(partialView), + 'CDE'); + `; + await testConversion(test, conversionBody); +}, 'decodeUtf8 - proper Uint8Array handling'); + +subsetTest(promise_test, async test => { + const conversionBody = ` + let result = browserSignals.decodeUtf8(); + `; + await testConversionException(test, conversionBody); +}, 'decodeUtf8 - not enough arguments'); + +subsetTest(promise_test, async test => { + const conversionBody = ` + let result = browserSignals.decodeUtf8([65, 32, 66]); + `; + await testConversionException(test, conversionBody); +}, 'decodeUtf8 - wrong type'); diff --git a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/about-blank.https.sub.html b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/about-blank.https.sub.html index 556d528aa05..e96a5b4dd0b 100644 --- a/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/about-blank.https.sub.html +++ b/testing/web-platform/tests/html/browsers/origin/origin-keyed-agent-clusters/about-blank.https.sub.html @@ -52,12 +52,13 @@ async function insertAboutBlankIframe() { await setBothDocumentDomains(iframe.contentWindow); } -function createBlankIframe() { +async function createBlankIframe() { const iframe = document.createElement("iframe"); const promise = new Promise(resolve => { iframe.addEventListener("load", resolve); }); document.body.append(iframe); - return promise; + await promise; + return iframe; } diff --git a/testing/web-platform/tests/html/semantics/interactive-elements/the-dialog-element/dialog-open-pseudo-invalidation-ref.html b/testing/web-platform/tests/html/semantics/interactive-elements/the-dialog-element/dialog-open-pseudo-invalidation-ref.html new file mode 100644 index 00000000000..97141e6c517 --- /dev/null +++ b/testing/web-platform/tests/html/semantics/interactive-elements/the-dialog-element/dialog-open-pseudo-invalidation-ref.html @@ -0,0 +1,11 @@ + + + +

The dialog should be open and green:

+Dialog + + diff --git a/testing/web-platform/tests/html/semantics/interactive-elements/the-dialog-element/dialog-open-pseudo-invalidation.html b/testing/web-platform/tests/html/semantics/interactive-elements/the-dialog-element/dialog-open-pseudo-invalidation.html new file mode 100644 index 00000000000..4a683c590db --- /dev/null +++ b/testing/web-platform/tests/html/semantics/interactive-elements/the-dialog-element/dialog-open-pseudo-invalidation.html @@ -0,0 +1,22 @@ + + + + + +

The dialog should be open and green:

+Dialog + + + + diff --git a/testing/web-platform/tests/html/semantics/permission-element/quirks-mode-no-height-is-still-bounded-ref.html b/testing/web-platform/tests/html/semantics/permission-element/quirks-mode-no-height-is-still-bounded-ref.html new file mode 100644 index 00000000000..a97003d82f0 --- /dev/null +++ b/testing/web-platform/tests/html/semantics/permission-element/quirks-mode-no-height-is-still-bounded-ref.html @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/testing/web-platform/tests/html/semantics/permission-element/quirks-mode-no-height-is-still-bounded.tentative.html b/testing/web-platform/tests/html/semantics/permission-element/quirks-mode-no-height-is-still-bounded.tentative.html new file mode 100644 index 00000000000..cca12c5fb72 --- /dev/null +++ b/testing/web-platform/tests/html/semantics/permission-element/quirks-mode-no-height-is-still-bounded.tentative.html @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/testing/web-platform/tests/html/semantics/the-button-element/interest-target/interesttarget-plain-inline-element.tentative.html b/testing/web-platform/tests/html/semantics/the-button-element/interest-target/interesttarget-plain-inline-element.tentative.html new file mode 100644 index 00000000000..68ce00193f7 --- /dev/null +++ b/testing/web-platform/tests/html/semantics/the-button-element/interest-target/interesttarget-plain-inline-element.tentative.html @@ -0,0 +1,40 @@ + + + + + + + + + + + + +

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

+
+ xxxxx +
+
+
+
+ + + diff --git a/testing/web-platform/tests/infrastructure/testharness/full.stop/full-stop.html b/testing/web-platform/tests/infrastructure/testharness/full.stop/full-stop.html new file mode 100644 index 00000000000..f04baddb627 --- /dev/null +++ b/testing/web-platform/tests/infrastructure/testharness/full.stop/full-stop.html @@ -0,0 +1,8 @@ + + + + diff --git a/testing/web-platform/tests/navigation-api/commit-behavior/resources/after-transition-commit-helpers.js b/testing/web-platform/tests/navigation-api/commit-behavior/resources/after-transition-commit-helpers.js index 664e8d7280e..2be6a03bac8 100644 --- a/testing/web-platform/tests/navigation-api/commit-behavior/resources/after-transition-commit-helpers.js +++ b/testing/web-platform/tests/navigation-api/commit-behavior/resources/after-transition-commit-helpers.js @@ -18,14 +18,14 @@ window.testAfterTransitionCommit = async (t, navigationType, mode, destinationIn rejectAfterCommit : async (e) => { e.commit(); assert_equals(location.hash, destinationHash, "hash after commit"); - assert_true(popstate_fired, "popstate fired after commit"); + assert_equals(navigationType == "traverse", popstate_fired, "popstate fired after commit"); await new Promise(resolve => t.step_timeout(resolve, 0)); return Promise.reject(err); }, successExplicitCommit : async (e) => { e.commit(); assert_equals(location.hash, destinationHash, "hash after commit"); - assert_true(popstate_fired, "popstate fired after commit"); + assert_equals(navigationType == "traverse", popstate_fired, "popstate fired after commit"); return new Promise(resolve => t.step_timeout(resolve, 0)); }, successNoExplicitCommit : async (e) => { @@ -76,14 +76,14 @@ window.testAfterTransitionCommit = async (t, navigationType, mode, destinationIn await promises.committed; await assertCommittedFulfillsFinishedRejectsExactly(t, promises, navigation.currentEntry, err); assert_equals(location.hash, destinationHash, "hash after promise resolution"); - assert_true(popstate_fired, "popstate fired after promise resolution"); + assert_equals(navigationType == "traverse", popstate_fired, "popstate fired after promise resolution"); assert_false(navigatesuccess_fired, "navigatesuccess fired"); assert_true(navigateerror_fired, "navigateerror fired"); } else { await promises.committed; await assertBothFulfill(t, promises, navigation.currentEntry); assert_equals(location.hash, destinationHash, "hash after promise resolution"); - assert_true(popstate_fired, "popstate fired after promise resolution"); + assert_equals(navigationType == "traverse", popstate_fired, "popstate fired after promise resolution"); assert_true(navigatesuccess_fired, "navigatesuccess fired"); assert_false(navigateerror_fired, "navigateerror fired"); } diff --git a/testing/web-platform/tests/notifications/registration-association.https.window.js b/testing/web-platform/tests/notifications/registration-association.https.window.js index 53aed8d5bc8..e838257faa8 100644 --- a/testing/web-platform/tests/notifications/registration-association.https.window.js +++ b/testing/web-platform/tests/notifications/registration-association.https.window.js @@ -17,8 +17,8 @@ promise_test(async (t) => { await registration.showNotification("foo"); await registration.unregister(); - const newRegistration = await prepareActiveServiceWorker("noop-sw.js"); - const notifications = await newRegistration.getNotifications(); + registration = await prepareActiveServiceWorker("noop-sw.js"); + const notifications = await registration.getNotifications(); // The spec says notifications should be associated with service worker registration // and thus unregistering should dissociate the notification. @@ -29,3 +29,10 @@ promise_test(async (t) => { // > is not the empty string, is tag. assert_equals(notifications.length, 0, "Should return zero notification"); }, "A new SW registration gets no previous notification"); + +promise_test(async (t) => { + await registration.showNotification("foo"); + await registration.unregister(); + const notifications = await registration.getNotifications(); + assert_equals(notifications.length, 0, "Should return zero notification"); +}, "An unregistered SW registration gets no previous notification"); diff --git a/testing/web-platform/tests/resources/testharness.js b/testing/web-platform/tests/resources/testharness.js index 81cf6175588..5b5410c8b21 100644 --- a/testing/web-platform/tests/resources/testharness.js +++ b/testing/web-platform/tests/resources/testharness.js @@ -4788,7 +4788,8 @@ return META_TITLE; } if ('location' in global_scope && 'pathname' in location) { - return location.pathname.substring(location.pathname.lastIndexOf('/') + 1, location.pathname.indexOf('.')); + var filename = location.pathname.substring(location.pathname.lastIndexOf('/') + 1); + return filename.substring(0, filename.indexOf('.')); } return "Untitled"; } diff --git a/testing/web-platform/tests/service-workers/service-worker/detached-register-crash.https.html b/testing/web-platform/tests/service-workers/service-worker/detached-register-crash.https.html new file mode 100644 index 00000000000..2785142a3cf --- /dev/null +++ b/testing/web-platform/tests/service-workers/service-worker/detached-register-crash.https.html @@ -0,0 +1,14 @@ + + +Assures navigator.serviceWorker.register() doesn't crash when rejecting being called in a detached frame + + + + + + diff --git a/testing/web-platform/tests/shadow-dom/reference-target/tentative/label-for.html b/testing/web-platform/tests/shadow-dom/reference-target/tentative/label-for.html index db94c4b9f52..05bb3511af4 100644 --- a/testing/web-platform/tests/shadow-dom/reference-target/tentative/label-for.html +++ b/testing/web-platform/tests/shadow-dom/reference-target/tentative/label-for.html @@ -2,6 +2,7 @@ + @@ -96,6 +97,58 @@ }, "Setting .htmlFor property to target a custom element using shadowrootreferencetarget"); +
+ + + + + diff --git a/testing/web-platform/tests/shadow-dom/reference-target/tentative/property-reflection.html b/testing/web-platform/tests/shadow-dom/reference-target/tentative/property-reflection.html index 2f1bf01a864..a44baeaf4a3 100644 --- a/testing/web-platform/tests/shadow-dom/reference-target/tentative/property-reflection.html +++ b/testing/web-platform/tests/shadow-dom/reference-target/tentative/property-reflection.html @@ -47,11 +47,10 @@ } referencing_element.remove(); host_container.setHTMLUnsafe(""); - }, `${referencing_element_type}.${reflected_property} has reflection behavior ${expected_behavior} when pointing to ${referenced_element_type} with reference target${element_creation_method.name}`); + }, `${referencing_element_type}.${reflected_property} has reflection behavior ${expected_behavior} when pointing to ${referenced_element_type} with reference target${element_creation_method.method_name}`); } - const element_creation_methods = [ - function appendTestDeclaratively(host_container, referenced_element_type) { + function appendTestDeclaratively(host_container, referenced_element_type) { host_container.setHTMLUnsafe(`