diff --git a/README.md b/README.md index 17eea24ba..7d1ae5931 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Zen is a firefox-based browser with the aim of pushing your productivity to a ne ### Firefox Versions -- [`Release`](https://zen-browser.app/download) - Is currently built using Firefox version `153.0`! 🚀 +- [`Release`](https://zen-browser.app/download) - Is currently built using Firefox version `153.0.1`! 🚀 - [`Twilight`](https://zen-browser.app/download?twilight) - Is currently built using Firefox version `RC 153.0`! ### Contributing diff --git a/build/firefox-cache/l10n-last-commit-hash b/build/firefox-cache/l10n-last-commit-hash index 4e1b64816..c712cf24e 100644 --- a/build/firefox-cache/l10n-last-commit-hash +++ b/build/firefox-cache/l10n-last-commit-hash @@ -1 +1 @@ -b1d3fe8f65751e6d695fa9ef94b11ad81e70dac1 \ No newline at end of file +30959a45eaaf0bc499f3054793c76c6d7f0c8bc2 \ No newline at end of file diff --git a/src/zen/tests/mochitests/shell/browser.toml b/src/zen/tests/mochitests/shell/browser.toml index 79e0e5b60..b3c5bb1ca 100644 --- a/src/zen/tests/mochitests/shell/browser.toml +++ b/src/zen/tests/mochitests/shell/browser.toml @@ -118,7 +118,19 @@ run-if = [ ] tags = "os_integration" +["browser_setDefaultProtocolHandler.js"] +run-if = [ + "os == 'win'", +] +tags = "os_integration" + ["browser_setDesktopBackgroundPreview.js"] disabled="Disabled by import_external_tests.py" -support-files = ["large.png"] +support-files = ["large.png", "canvas.html"] +tags = "os_integration" + +["browser_windowsSetDefaultAppCmdHandler.js"] +run-if = [ + "os == 'win'", +] tags = "os_integration" diff --git a/src/zen/tests/mochitests/shell/browser_headless_screenshot_1.js b/src/zen/tests/mochitests/shell/browser_headless_screenshot_1.js index b5f84f0f6..798c37d45 100644 --- a/src/zen/tests/mochitests/shell/browser_headless_screenshot_1.js +++ b/src/zen/tests/mochitests/shell/browser_headless_screenshot_1.js @@ -71,4 +71,10 @@ add_task(async function () { ], screenshotPath ); + + // Regression test for bug 2003265, likely to become flaky if it regresses. + await testFileCreationPositive( + ["-url", "about:blank", "-screenshot", screenshotPath], + screenshotPath + ); }); diff --git a/src/zen/tests/mochitests/shell/browser_setDefaultPDFHandler.js b/src/zen/tests/mochitests/shell/browser_setDefaultPDFHandler.js index 04cc1f069..3ffa6cb57 100644 --- a/src/zen/tests/mochitests/shell/browser_setDefaultPDFHandler.js +++ b/src/zen/tests/mochitests/shell/browser_setDefaultPDFHandler.js @@ -8,6 +8,9 @@ ChromeUtils.defineESModuleGetters(this, { sinon: "resource://testing-common/Sinon.sys.mjs", }); +const confusedFoxPath = ShellService.getBundledPdfFile("confused_fox.pdf").path; +const SET_HANDLER_WIN11 = Ci.nsIWindowsShellService.OPEN_WITH_SET_HANDLER; + const setDefaultBrowserUserChoiceStub = sinon.stub(); const setDefaultExtensionHandlersUserChoiceStub = sinon .stub() @@ -34,17 +37,15 @@ const _userChoiceImpossibleTelemetryResultStub = sinon const setDefaultStub = sinon.stub(); // We'll dynamically update this as needed during the tests. const queryCurrentDefaultHandlerForStub = sinon.stub(); -const launchOpenWithDefaultPickerForFileTypeStub = sinon.stub(); +const launchSetDefaultAppPickerStub = sinon.stub(); const launchModernSettingsDialogDefaultAppsStub = sinon.stub(); const shellStub = sinon.stub(ShellService, "shellService").value({ setDefaultBrowser: setDefaultStub, queryCurrentDefaultHandlerFor: queryCurrentDefaultHandlerForStub, - QueryInterface: () => ({ - launchOpenWithDefaultPickerForFileType: - launchOpenWithDefaultPickerForFileTypeStub, - launchModernSettingsDialogDefaultApps: - launchModernSettingsDialogDefaultAppsStub, - }), + launchSetDefaultAppPicker: launchSetDefaultAppPickerStub, + launchModernSettingsDialogDefaultApps: + launchModernSettingsDialogDefaultAppsStub, + QueryInterface: ChromeUtils.generateQI([]), }); registerCleanupFunction(() => { @@ -228,7 +229,7 @@ add_task(async function test_setAsDefaultPDFHandler_knownBrowser() { const expectedArguments = [aumi, [".pdf", "FirefoxPDF"]]; const resetStubs = () => { setDefaultExtensionHandlersUserChoiceStub.resetHistory(); - launchOpenWithDefaultPickerForFileTypeStub.resetHistory(); + launchSetDefaultAppPickerStub.resetHistory(); launchModernSettingsDialogDefaultAppsStub.resetHistory(); }; @@ -250,7 +251,7 @@ add_task(async function test_setAsDefaultPDFHandler_knownBrowser() { "Called default browser agent with expected arguments" ); Assert.ok( - launchOpenWithDefaultPickerForFileTypeStub.notCalled, + launchSetDefaultAppPickerStub.notCalled, "Did not fall back to open-with picker" ); Assert.ok( @@ -271,7 +272,7 @@ add_task(async function test_setAsDefaultPDFHandler_knownBrowser() { "Called default browser agent with expected arguments" ); Assert.ok( - launchOpenWithDefaultPickerForFileTypeStub.notCalled, + launchSetDefaultAppPickerStub.notCalled, "Did not fall back to open-with picker" ); Assert.ok( @@ -289,7 +290,7 @@ add_task(async function test_setAsDefaultPDFHandler_knownBrowser() { "Did not use userChoice" ); Assert.ok( - launchOpenWithDefaultPickerForFileTypeStub.notCalled, + launchSetDefaultAppPickerStub.notCalled, "Did not fall back to open-with picker" ); Assert.ok( @@ -310,7 +311,7 @@ add_task(async function test_setAsDefaultPDFHandler_knownBrowser() { "Called default browser agent with expected arguments" ); Assert.ok( - launchOpenWithDefaultPickerForFileTypeStub.notCalled, + launchSetDefaultAppPickerStub.notCalled, "Did not fall back to open-with picker" ); Assert.ok( @@ -365,8 +366,11 @@ add_task(async function test_setAsDefaultPDFHandler_fallback() { Assert.ok(userChoiceStub.called, "Attempted userChoice"); Assert.ok( - launchOpenWithDefaultPickerForFileTypeStub.calledWith(".pdf"), - "Fell back to open-with picker for .pdf" + launchSetDefaultAppPickerStub.calledWith( + confusedFoxPath, + SET_HANDLER_WIN11 + ), + "Fell back to open-with picker with bundled PDF path and Win11 flag" ); Assert.ok( launchModernSettingsDialogDefaultAppsStub.notCalled, @@ -392,7 +396,7 @@ add_task(async function test_setAsDefaultPDFHandler_fallback() { ); userChoiceStub.resetHistory(); isDefaultHandlerForStub.resetHistory(); - launchOpenWithDefaultPickerForFileTypeStub.resetHistory(); + launchSetDefaultAppPickerStub.resetHistory(); launchModernSettingsDialogDefaultAppsStub.resetHistory(); info( @@ -413,22 +417,25 @@ add_task(async function test_setAsDefaultPDFHandler_fallback() { isDefaultHandlerForStub.returns(true); userChoiceStub.resetHistory(); isDefaultHandlerForStub.resetHistory(); - launchOpenWithDefaultPickerForFileTypeStub.resetHistory(); + launchSetDefaultAppPickerStub.resetHistory(); launchModernSettingsDialogDefaultAppsStub.resetHistory(); info( "When userChoice fails and open-with picker fails, should fall back to settings dialog" ); Services.fog.testResetFOG(); - launchOpenWithDefaultPickerForFileTypeStub.throws( + launchSetDefaultAppPickerStub.throws( new Error("mock IOpenWithLauncher failure") ); await ShellService.setAsDefaultPDFHandler(false); Assert.ok(userChoiceStub.called, "Attempted userChoice"); Assert.ok( - launchOpenWithDefaultPickerForFileTypeStub.calledWith(".pdf"), - "Attempted open-with picker for .pdf" + launchSetDefaultAppPickerStub.calledWith( + confusedFoxPath, + SET_HANDLER_WIN11 + ), + "Attempted open-with picker with bundled PDF path and Win11 flag" ); Assert.ok( launchModernSettingsDialogDefaultAppsStub.called, @@ -463,7 +470,7 @@ add_task(async function test_setAsDefaultPDFHandler_fallback() { ); userChoiceStub.resetHistory(); isDefaultHandlerForStub.resetHistory(); - launchOpenWithDefaultPickerForFileTypeStub.resetHistory(); + launchSetDefaultAppPickerStub.resetHistory(); launchModernSettingsDialogDefaultAppsStub.resetHistory(); info( @@ -499,7 +506,7 @@ add_task(async function test_setAsDefaultPDFHandler_fallback() { "Event result_is_default is false when no method set the default" ); } finally { - launchOpenWithDefaultPickerForFileTypeStub.reset(); + launchSetDefaultAppPickerStub.reset(); launchModernSettingsDialogDefaultAppsStub.reset(); sandbox.restore(); await SpecialPowers.popPrefEnv(); @@ -529,7 +536,7 @@ add_task(async function test_setAsDefaultPDFHandler_useOpenWithDisabled() { await ShellService.setAsDefaultPDFHandler(false); Assert.ok( - launchOpenWithDefaultPickerForFileTypeStub.notCalled, + launchSetDefaultAppPickerStub.notCalled, "Did not invoke open-with picker when pref is disabled" ); Assert.ok( @@ -550,7 +557,7 @@ add_task(async function test_setAsDefaultPDFHandler_useOpenWithDisabled() { "Event result_is_default reflects isDefaultHandlerFor" ); } finally { - launchOpenWithDefaultPickerForFileTypeStub.reset(); + launchSetDefaultAppPickerStub.reset(); launchModernSettingsDialogDefaultAppsStub.reset(); sandbox.restore(); await SpecialPowers.popPrefEnv(); diff --git a/src/zen/tests/mochitests/shell/browser_setDefaultProtocolHandler.js b/src/zen/tests/mochitests/shell/browser_setDefaultProtocolHandler.js new file mode 100644 index 000000000..cc4c5275c --- /dev/null +++ b/src/zen/tests/mochitests/shell/browser_setDefaultProtocolHandler.js @@ -0,0 +1,321 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +ChromeUtils.defineESModuleGetters(this, { + DEFAULT_PROTOCOL_URLS: + "moz-src:///browser/components/shell/ShellService.sys.mjs", + SET_DEFAULT_REDIRECT_PREF: + "moz-src:///browser/components/shell/WindowsSetDefaultRedirect.sys.mjs", + WindowsSetDefaultRedirect: + "moz-src:///browser/components/shell/WindowsSetDefaultRedirect.sys.mjs", + sinon: "resource://testing-common/Sinon.sys.mjs", +}); + +// Everything here is Windows. +Assert.equal(AppConstants.platform, "win", "Platform is Windows"); + +// Protocol defaults carry OPEN_WITH_PROTOCOL_MESSAGING on top of the +// set-handler flag, unlike the file-type flags in browser_setDefaultPDFHandler. +const SET_PROTOCOL_WIN11 = + Ci.nsIWindowsShellService.OPEN_WITH_SET_HANDLER | + Ci.nsIWindowsShellService.OPEN_WITH_PROTOCOL_MESSAGING; +const SET_PROTOCOL_WIN10 = + Ci.nsIWindowsShellService.OPEN_WITH_SET_HANDLER_WIN10 | + Ci.nsIWindowsShellService.OPEN_WITH_PROTOCOL_MESSAGING; + +const launchSetDefaultAppPickerStub = sinon.stub(); +const launchModernSettingsDialogDefaultAppsStub = sinon.stub(); +const shellStub = sinon.stub(ShellService, "shellService").value({ + launchSetDefaultAppPicker: launchSetDefaultAppPickerStub, + launchModernSettingsDialogDefaultApps: + launchModernSettingsDialogDefaultAppsStub, + QueryInterface: ChromeUtils.generateQI([]), +}); + +registerCleanupFunction(() => { + shellStub.restore(); + Services.prefs.clearUserPref(SET_DEFAULT_REDIRECT_PREF); +}); + +function resetState() { + launchSetDefaultAppPickerStub.reset(); + launchModernSettingsDialogDefaultAppsStub.reset(); + Services.prefs.clearUserPref(SET_DEFAULT_REDIRECT_PREF); +} + +function readPrefObject() { + return JSON.parse( + Services.prefs.getStringPref(SET_DEFAULT_REDIRECT_PREF, "") + ); +} + +add_task(async function test_default_https_uses_url_and_win11_flag() { + resetState(); + const sandbox = sinon.createSandbox(); + sandbox.stub(ShellService, "_isWindows11").returns(true); + + try { + await ShellService.setAsDefaultProtocolHandler("https"); + + Assert.ok( + launchSetDefaultAppPickerStub.calledOnce, + "Picker invoked once for the https default" + ); + Assert.deepEqual( + launchSetDefaultAppPickerStub.firstCall.args, + [DEFAULT_PROTOCOL_URLS.https, SET_PROTOCOL_WIN11], + "Picker called with https URL and SET_HANDLER | PROTOCOL_MESSAGING" + ); + Assert.ok( + launchModernSettingsDialogDefaultAppsStub.notCalled, + "Modern settings not invoked when launcher succeeds" + ); + Assert.deepEqual( + readPrefObject(), + { + openWithArg: DEFAULT_PROTOCOL_URLS.https, + overrideUri: null, + type: WindowsSetDefaultRedirect.TYPE.PROTOCOL, + }, + "Pref records the URL; overrideUri null so the round-trip is suppressed" + ); + } finally { + sandbox.restore(); + } +}); + +add_task(async function test_custom_url_overrides_protocol_default() { + resetState(); + const sandbox = sinon.createSandbox(); + sandbox.stub(ShellService, "_isWindows11").returns(true); + + try { + const customUrl = "https://custom.example.com/handler-check"; + await ShellService.setAsDefaultProtocolHandler("https", customUrl, true); + + Assert.deepEqual( + launchSetDefaultAppPickerStub.firstCall.args, + [customUrl, SET_PROTOCOL_WIN11], + "Picker called with caller-provided URL, not the protocol default" + ); + Assert.deepEqual( + readPrefObject(), + { + openWithArg: customUrl, + overrideUri: DEFAULT_PROTOCOL_URLS.https, + type: WindowsSetDefaultRedirect.TYPE.PROTOCOL, + }, + "Pref stashes the custom URL + protocol-default overrideUri for the round-trip" + ); + } finally { + sandbox.restore(); + } +}); + +add_task(async function test_win10_uses_set_handler_win10_flag() { + resetState(); + const sandbox = sinon.createSandbox(); + sandbox.stub(ShellService, "_isWindows11").returns(false); + + try { + await ShellService.setAsDefaultProtocolHandler("https"); + Assert.deepEqual( + launchSetDefaultAppPickerStub.firstCall.args, + [DEFAULT_PROTOCOL_URLS.https, SET_PROTOCOL_WIN10], + "Picker called with SET_HANDLER_WIN10 | PROTOCOL_MESSAGING when not on Win11" + ); + } finally { + sandbox.restore(); + } +}); + +add_task(async function test_falls_back_to_settings_when_picker_throws() { + resetState(); + const sandbox = sinon.createSandbox(); + sandbox.stub(ShellService, "_isWindows11").returns(true); + launchSetDefaultAppPickerStub.throws(new Error("mock launcher failure")); + + try { + await ShellService.setAsDefaultProtocolHandler("https", undefined, true); + + Assert.ok(launchSetDefaultAppPickerStub.called, "Tried picker first"); + Assert.ok( + launchModernSettingsDialogDefaultAppsStub.called, + "Fell through to modern settings when launcher threw" + ); + Assert.ok( + !Services.prefs.prefHasUserValue(SET_DEFAULT_REDIRECT_PREF), + "Pending redirect cleared on launcher failure so a later round-trip can't pick up stale intent" + ); + } finally { + sandbox.restore(); + } +}); + +add_task(async function test_unknown_protocol_with_no_url_throws() { + resetState(); + + await Assert.rejects( + ShellService.setAsDefaultProtocolHandler("notaprotocol"), + /No URL provided and no DEFAULT_PROTOCOL_URLS fallback/, + "Throws when neither URL nor protocol fallback resolves to a URL" + ); + Assert.ok( + launchSetDefaultAppPickerStub.notCalled, + "Picker not invoked when args don't resolve to a URL" + ); + Assert.ok( + !Services.prefs.prefHasUserValue(SET_DEFAULT_REDIRECT_PREF), + "Pref not touched when validation fails before any side effects" + ); +}); + +// Wait for the deferred set_default_protocol_handler_attempt event to be +// recorded, then return the single event emitted by the most recent call. +async function awaitAttemptEvent() { + await TestUtils.waitForCondition(() => { + const events = + Glean.browser.setDefaultProtocolHandlerAttempt.testGetValue(); + return events && events.length; + }, "Recorded set_default_protocol_handler_attempt event"); + const events = Glean.browser.setDefaultProtocolHandlerAttempt.testGetValue(); + Assert.equal(events.length, 1, "Recorded exactly one attempt event"); + return events[0]; +} + +add_task(async function test_telemetry_records_open_with_success() { + resetState(); + const sandbox = sinon.createSandbox(); + sandbox.stub(ShellService, "_isWindows11").returns(true); + const isDefaultHandlerForStub = sandbox + .stub(ShellService, "isDefaultHandlerFor") + .returns(true); + await SpecialPowers.pushPrefEnv({ + set: [["browser.shell.setDefaultProtocolHandler.attemptWaitTimeMs", 0]], + }); + + try { + Services.fog.testResetFOG(); + await ShellService.setAsDefaultProtocolHandler("https"); + + const event = await awaitAttemptEvent(); + Assert.equal(event.extra.method, "open_with", "Event method is open_with"); + Assert.equal(event.extra.success, "true", "Event success is true"); + Assert.equal(event.extra.protocol, "https", "Event protocol is https"); + Assert.equal( + event.extra.result_is_default, + "true", + "Event result_is_default reflects isDefaultHandlerFor" + ); + Assert.ok( + isDefaultHandlerForStub.calledWith("https"), + "Sampled isDefaultHandlerFor with the protocol" + ); + } finally { + sandbox.restore(); + await SpecialPowers.popPrefEnv(); + } +}); + +add_task(async function test_telemetry_records_settings_fallback() { + resetState(); + const sandbox = sinon.createSandbox(); + sandbox.stub(ShellService, "_isWindows11").returns(true); + sandbox.stub(ShellService, "isDefaultHandlerFor").returns(true); + launchSetDefaultAppPickerStub.throws(new Error("mock launcher failure")); + await SpecialPowers.pushPrefEnv({ + set: [["browser.shell.setDefaultProtocolHandler.attemptWaitTimeMs", 0]], + }); + + try { + Services.fog.testResetFOG(); + await ShellService.setAsDefaultProtocolHandler("mailto"); + + Assert.equal( + Glean.browser.setDefaultProtocolHandlerModernSettingsResult.Success.testGetValue(), + 1, + "Recorded modern settings success" + ); + + const event = await awaitAttemptEvent(); + Assert.equal( + event.extra.method, + "settings", + "Event method is settings (last attempted)" + ); + Assert.equal( + event.extra.success, + "true", + "Event success reflects modern settings launch" + ); + Assert.equal(event.extra.protocol, "mailto", "Event protocol is mailto"); + } finally { + sandbox.restore(); + await SpecialPowers.popPrefEnv(); + } +}); + +add_task(async function test_telemetry_records_complete_failure() { + resetState(); + const sandbox = sinon.createSandbox(); + sandbox.stub(ShellService, "_isWindows11").returns(true); + sandbox.stub(ShellService, "isDefaultHandlerFor").returns(false); + launchSetDefaultAppPickerStub.throws(new Error("mock launcher failure")); + launchModernSettingsDialogDefaultAppsStub.throws( + new Error("mock modern settings failure") + ); + await SpecialPowers.pushPrefEnv({ + set: [["browser.shell.setDefaultProtocolHandler.attemptWaitTimeMs", 0]], + }); + + try { + Services.fog.testResetFOG(); + await ShellService.setAsDefaultProtocolHandler("https"); + + Assert.equal( + Glean.browser.setDefaultProtocolHandlerModernSettingsResult.Failure.testGetValue(), + 1, + "Recorded modern settings failure" + ); + + const event = await awaitAttemptEvent(); + Assert.equal(event.extra.method, "settings", "Event method is settings"); + Assert.equal( + event.extra.success, + "false", + "Event success is false when every method failed" + ); + Assert.equal( + event.extra.result_is_default, + "false", + "Event result_is_default is false when no method set the default" + ); + } finally { + sandbox.restore(); + await SpecialPowers.popPrefEnv(); + } +}); + +add_task(async function test_overwrites_existing_pending_redirect() { + resetState(); + const sandbox = sinon.createSandbox(); + sandbox.stub(ShellService, "_isWindows11").returns(true); + + try { + // A differently-typed leftover value on the pref must not trip the + // setStringPref in WindowsSetDefaultRedirect.arm (it clears first). + Services.prefs.setBoolPref(SET_DEFAULT_REDIRECT_PREF, true); + await ShellService.setAsDefaultProtocolHandler("https", undefined, false); + Assert.deepEqual( + readPrefObject(), + { + openWithArg: DEFAULT_PROTOCOL_URLS.https, + overrideUri: null, + type: WindowsSetDefaultRedirect.TYPE.PROTOCOL, + }, + "Stale value replaced with the structured redirect on the next call" + ); + } finally { + sandbox.restore(); + } +}); diff --git a/src/zen/tests/mochitests/shell/browser_setDesktopBackgroundPreview.js b/src/zen/tests/mochitests/shell/browser_setDesktopBackgroundPreview.js index 733123e04..037d76623 100644 --- a/src/zen/tests/mochitests/shell/browser_setDesktopBackgroundPreview.js +++ b/src/zen/tests/mochitests/shell/browser_setDesktopBackgroundPreview.js @@ -3,7 +3,7 @@ /** * Check whether the preview image for setDesktopBackground is rendered - * correctly, without stretching + * correctly, without stretching, for both and targets. */ add_setup(async function () { @@ -12,82 +12,85 @@ add_setup(async function () { }); }); -add_task(async function () { - await BrowserTestUtils.withNewTab( - { - gBrowser, - url: getRootDirectory(gTestPath) + "large.png", - }, - async () => { - const dialogLoad = BrowserTestUtils.domWindowOpened(null, async win => { - await BrowserTestUtils.waitForEvent(win, "load"); - Assert.equal( - win.document.documentElement.getAttribute("windowtype"), - "Shell:SetDesktopBackground", - "Opened correct window" - ); - return true; - }); +async function testPreview(url, targetSelector) { + await BrowserTestUtils.withNewTab({ gBrowser, url }, async browser => { + const dialogLoad = BrowserTestUtils.domWindowOpened(null, async win => { + await BrowserTestUtils.waitForEvent(win, "load"); + Assert.equal( + win.document.documentElement.getAttribute("windowtype"), + "Shell:SetDesktopBackground", + "Opened correct window" + ); + return true; + }); - const image = content.document.images[0]; - EventUtils.synthesizeMouseAtCenter(image, { type: "contextmenu" }); + await BrowserTestUtils.synthesizeMouseAtCenter( + targetSelector, + { type: "contextmenu" }, + browser + ); - const menu = document.getElementById("contentAreaContextMenu"); - await BrowserTestUtils.waitForPopupEvent(menu, "shown"); - const menuClosed = BrowserTestUtils.waitForPopupEvent(menu, "hidden"); + const menu = document.getElementById("contentAreaContextMenu"); + await BrowserTestUtils.waitForPopupEvent(menu, "shown"); + const menuClosed = BrowserTestUtils.waitForPopupEvent(menu, "hidden"); - const menuItem = document.getElementById("context-setDesktopBackground"); - try { - menu.activateItem(menuItem); - } catch (ex) { - ok( - menuItem.hidden, - "should only fail to activate when menu item is hidden" - ); - ok( - !ShellService.canSetDesktopBackground, - "Should only hide when not able to set the desktop background" - ); - is( - AppConstants.platform, - "linux", - "Should always be able to set desktop background on non-linux platforms" - ); - todo(false, "Skipping test on this configuration"); - - menu.hidePopup(); - await menuClosed; - return; - } + const menuItem = document.getElementById("context-setDesktopBackground"); + try { + menu.activateItem(menuItem); + } catch (ex) { + ok( + menuItem.hidden, + "should only fail to activate when menu item is hidden" + ); + ok( + !ShellService.canSetDesktopBackground, + "Should only hide when not able to set the desktop background" + ); + is( + AppConstants.platform, + "linux", + "Should always be able to set desktop background on non-linux platforms" + ); + todo(false, "Skipping test on this configuration"); + menu.hidePopup(); await menuClosed; + return; + } - const win = await dialogLoad; + await menuClosed; - /* setDesktopBackground.js does a setTimeout to wait for correct + const win = await dialogLoad; + + /* setDesktopBackground.js does a setTimeout to wait for correct dimensions. If we don't wait here we could read the preview dimensions before they're changed to match the screen */ - await TestUtils.waitForTick(); + await TestUtils.waitForTick(); - const canvas = win.document.getElementById("screen"); - const screenRatio = screen.width / screen.height; - const previewRatio = canvas.clientWidth / canvas.clientHeight; + const canvas = win.document.getElementById("screen"); + const screenRatio = screen.width / screen.height; + const previewRatio = canvas.clientWidth / canvas.clientHeight; - info(`Screen dimensions are ${screen.width}x${screen.height}`); - info(`Screen's raw ratio is ${screenRatio}`); - info( - `Preview dimensions are ${canvas.clientWidth}x${canvas.clientHeight}` - ); - info(`Preview's raw ratio is ${previewRatio}`); + info(`Screen dimensions are ${screen.width}x${screen.height}`); + info(`Screen's raw ratio is ${screenRatio}`); + info(`Preview dimensions are ${canvas.clientWidth}x${canvas.clientHeight}`); + info(`Preview's raw ratio is ${previewRatio}`); - Assert.ok( - previewRatio < screenRatio + 0.01 && previewRatio > screenRatio - 0.01, - "Preview's aspect ratio is within ±.01 of screen's" - ); + Assert.ok( + previewRatio < screenRatio + 0.01 && previewRatio > screenRatio - 0.01, + "Preview's aspect ratio is within ±.01 of screen's" + ); - win.close(); + win.close(); - await menuClosed; - } - ); + await menuClosed; + }); +} + +add_task(async function test_image() { + await testPreview(getRootDirectory(gTestPath) + "large.png", "img"); +}); + +add_task(async function test_canvas() { + await testPreview(getRootDirectory(gTestPath) + "canvas.html", "canvas"); }); diff --git a/src/zen/tests/mochitests/shell/browser_windowsSetDefaultAppCmdHandler.js b/src/zen/tests/mochitests/shell/browser_windowsSetDefaultAppCmdHandler.js new file mode 100644 index 000000000..62c31979f --- /dev/null +++ b/src/zen/tests/mochitests/shell/browser_windowsSetDefaultAppCmdHandler.js @@ -0,0 +1,293 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { CommandLineHandler } = ChromeUtils.importESModule( + "moz-src:///browser/components/shell/WindowsSetDefaultAppCmdHandler.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", + SET_DEFAULT_REDIRECT_PREF: + "moz-src:///browser/components/shell/WindowsSetDefaultRedirect.sys.mjs", + ShellService: "moz-src:///browser/components/shell/ShellService.sys.mjs", + WindowsSetDefaultRedirect: + "moz-src:///browser/components/shell/WindowsSetDefaultRedirect.sys.mjs", + sinon: "resource://testing-common/Sinon.sys.mjs", +}); + +Assert.equal(AppConstants.platform, "win", "Platform is Windows"); + +const confusedFoxPath = ShellService.getBundledPdfFile("confused_fox.pdf").path; +const blankURISpec = Services.io.newFileURI( + ShellService.getBundledPdfFile("blank.pdf") +).spec; + +const workingDir = Services.dirsvc.get("GreD", Ci.nsIFile); + +// Build a real nsICommandLine from an args array, so we exercise the same +// flag-parsing the OS-initiated launch goes through +function makeCmdLine(args, state = Ci.nsICommandLine.STATE_INITIAL_LAUNCH) { + return Cu.createCommandLine(args, workingDir, state); +} + +// Arm the one-shot redirect via the real ShellService helper, the way +// setAsDefault{*}Handler does right before launching the OS picker. These +// tests all use file openWithArgs, so default the type. +function armRedirect( + openWithArg, + overrideUri, + type = WindowsSetDefaultRedirect.TYPE.FILE +) { + WindowsSetDefaultRedirect.arm(openWithArg, overrideUri, type); +} + +let fakeWin; +let getTopWindowStub; +let openWindowStub; + +add_setup(function () { + fakeWin = { openTrustedLinkIn: sinon.stub() }; + // Stubbed rather than spied: getTopWindow must return our fake window (and + // null on demand), and openWindow must not actually open a window in the + // test harness. We only inspect the calls. + getTopWindowStub = sinon.stub(BrowserWindowTracker, "getTopWindow"); + openWindowStub = sinon.stub(BrowserWindowTracker, "openWindow"); +}); + +registerCleanupFunction(() => { + sinon.restore(); + Services.prefs.clearUserPref(SET_DEFAULT_REDIRECT_PREF); +}); + +function resetState() { + fakeWin.openTrustedLinkIn.resetHistory(); + getTopWindowStub.reset(); + getTopWindowStub.returns(fakeWin); + openWindowStub.reset(); + Services.prefs.clearUserPref(SET_DEFAULT_REDIRECT_PREF); +} + +add_task(async function test_no_osint_returns_early() { + resetState(); + armRedirect(confusedFoxPath, blankURISpec); + const cmdLine = makeCmdLine(["-url", confusedFoxPath]); + new CommandLineHandler().handle(cmdLine); + + Assert.equal( + cmdLine.preventDefault, + false, + "preventDefault left untouched without -osint" + ); + Assert.greaterOrEqual( + cmdLine.findFlag("url", false), + 0, + "-url remains for the next handler" + ); + Assert.ok( + fakeWin.openTrustedLinkIn.notCalled, + "No tab opened without -osint" + ); + Assert.ok(openWindowStub.notCalled, "No window opened without -osint"); +}); + +add_task(async function test_osint_without_url_returns_early() { + resetState(); + armRedirect(confusedFoxPath, blankURISpec); + const cmdLine = makeCmdLine(["-osint"]); + new CommandLineHandler().handle(cmdLine); + + Assert.equal( + cmdLine.preventDefault, + false, + "preventDefault left untouched without -url" + ); + Assert.ok(fakeWin.openTrustedLinkIn.notCalled, "No tab opened without -url"); + Assert.ok(openWindowStub.notCalled, "No window opened without -url"); +}); + +add_task(async function test_no_pending_redirect_leaves_arg() { + // No armed redirect: even a -url that looks like our stub PDF is a real, + // user-initiated open (e.g. they double-clicked the file), so we must leave + // it for BrowserContentHandler. + resetState(); + + const cmdLine = makeCmdLine(["-osint", "-url", confusedFoxPath]); + new CommandLineHandler().handle(cmdLine); + + Assert.equal( + cmdLine.preventDefault, + false, + "preventDefault not set when no redirect is pending" + ); + Assert.greaterOrEqual( + cmdLine.findFlag("url", false), + 0, + "-url preserved for BrowserContentHandler" + ); + Assert.ok( + fakeWin.openTrustedLinkIn.notCalled, + "No tab opened when no redirect is pending" + ); + Assert.ok( + openWindowStub.notCalled, + "No window opened when no redirect is pending" + ); +}); + +add_task(async function test_unrelated_url_arg_is_ignored() { + resetState(); + armRedirect(confusedFoxPath, blankURISpec); + + const cmdLine = makeCmdLine([ + "-osint", + "-url", + "https://example.com/some-page", + ]); + new CommandLineHandler().handle(cmdLine); + + Assert.equal( + cmdLine.preventDefault, + false, + "preventDefault not set for a -url that isn't the pending openWithArg" + ); + Assert.greaterOrEqual( + cmdLine.findFlag("url", false), + 0, + "-url preserved for subsequent handler" + ); + Assert.ok( + fakeWin.openTrustedLinkIn.notCalled, + "No tab opened for an unrelated -url" + ); + Assert.ok( + Services.prefs.prefHasUserValue(SET_DEFAULT_REDIRECT_PREF), + "Pending redirect untouched when the openWithArg doesn't match" + ); +}); + +add_task(async function test_suppress_only_when_target_null() { + resetState(); + armRedirect(confusedFoxPath, null); + + const cmdLine = makeCmdLine(["-osint", "-url", confusedFoxPath]); + new CommandLineHandler().handle(cmdLine); + + Assert.equal( + cmdLine.preventDefault, + true, + "openWithArg suppressed so BrowserContentHandler skips it" + ); + Assert.equal( + cmdLine.findFlag("url", false), + -1, + "-url consumed even with no redirect target" + ); + Assert.ok( + fakeWin.openTrustedLinkIn.notCalled, + "No redirect when target is null" + ); + Assert.ok(openWindowStub.notCalled, "No fallback window when target is null"); + Assert.ok( + !Services.prefs.prefHasUserValue(SET_DEFAULT_REDIRECT_PREF), + "Redirect intent consumed" + ); +}); + +add_task(async function test_redirects_to_top_window() { + resetState(); + armRedirect(confusedFoxPath, blankURISpec); + + const cmdLine = makeCmdLine(["-osint", "-url", confusedFoxPath]); + new CommandLineHandler().handle(cmdLine); + + Assert.equal( + cmdLine.preventDefault, + true, + "Default open suppressed for the pending openWithArg" + ); + Assert.equal(cmdLine.findFlag("url", false), -1, "-url consumed"); + Assert.ok( + fakeWin.openTrustedLinkIn.calledOnce, + "Redirected into the top window" + ); + Assert.deepEqual(fakeWin.openTrustedLinkIn.firstCall.args, [ + blankURISpec, + "tab", + ]); + Assert.ok(openWindowStub.notCalled, "No new window when one exists"); + Assert.ok( + !Services.prefs.prefHasUserValue(SET_DEFAULT_REDIRECT_PREF), + "Redirect intent is one-shot and cleared after use" + ); +}); + +add_task(async function test_opens_new_window_when_no_top() { + resetState(); + getTopWindowStub.returns(null); + armRedirect(confusedFoxPath, blankURISpec); + + const cmdLine = makeCmdLine(["-osint", "-url", confusedFoxPath]); + new CommandLineHandler().handle(cmdLine); + + Assert.equal( + cmdLine.preventDefault, + true, + "Default open suppressed even when no top window exists" + ); + Assert.ok( + fakeWin.openTrustedLinkIn.notCalled, + "Top-window path skipped when getTopWindow returns null" + ); + Assert.ok(openWindowStub.calledOnce, "Falls back to openWindow"); + + const opts = openWindowStub.firstCall.args[0]; + Assert.ok( + opts && opts.args, + "openWindow called with a {args} options object" + ); + Assert.ok( + opts.args instanceof Ci.nsISupportsString, + "args is an nsISupportsString" + ); + Assert.equal( + opts.args.data, + blankURISpec, + "nsISupportsString carries the redirect URI" + ); + Assert.ok( + !Services.prefs.prefHasUserValue(SET_DEFAULT_REDIRECT_PREF), + "Redirect intent is one-shot and cleared after use" + ); +}); + +add_task(async function test_intent_is_one_shot() { + resetState(); + armRedirect(confusedFoxPath, blankURISpec); + + new CommandLineHandler().handle( + makeCmdLine(["-osint", "-url", confusedFoxPath]) + ); + Assert.ok( + fakeWin.openTrustedLinkIn.calledOnce, + "First call honors the pending redirect" + ); + + fakeWin.openTrustedLinkIn.resetHistory(); + const second = makeCmdLine(["-osint", "-url", confusedFoxPath]); + new CommandLineHandler().handle(second); + + Assert.equal( + second.preventDefault, + false, + "Second call leaves the openWithArg alone because the intent was consumed" + ); + Assert.greaterOrEqual( + second.findFlag("url", false), + 0, + "-url preserved on the second call (no pending redirect)" + ); + Assert.ok( + fakeWin.openTrustedLinkIn.notCalled, + "Second call does not redirect" + ); +}); diff --git a/src/zen/tests/mochitests/shell/canvas.html b/src/zen/tests/mochitests/shell/canvas.html new file mode 100644 index 000000000..cedb7b537 --- /dev/null +++ b/src/zen/tests/mochitests/shell/canvas.html @@ -0,0 +1,11 @@ + + + + + + + diff --git a/src/zen/tests/mochitests/shell/unit/test_dynamicLauncher.js b/src/zen/tests/mochitests/shell/unit/test_dynamicLauncher.js new file mode 100644 index 000000000..f68a15359 --- /dev/null +++ b/src/zen/tests/mochitests/shell/unit/test_dynamicLauncher.js @@ -0,0 +1,657 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + ShellService: "moz-src:///browser/components/shell/ShellService.sys.mjs", + Subprocess: "resource://gre/modules/Subprocess.sys.mjs", + TestUtils: "resource://testing-common/TestUtils.sys.mjs", + XPCOMUtils: "resource://gre/modules/XPCOMUtils.sys.mjs", +}); + +// This tests the browser's interaction with the Dynamic Launcher portal, which +// is documented at +// +// https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.DynamicLauncher + +XPCOMUtils.defineLazyServiceGetter( + this, + "IniParserFactory", + "@mozilla.org/xpcom/ini-parser-factory;1", + Ci.nsIINIParserFactory +); + +function desktopEntryFromObject(object) { + let ini = IniParserFactory.createINIParser().QueryInterface( + Ci.nsIINIParserWriter + ); + + for (const property of Object.getOwnPropertyNames(object)) { + ini.setString("Desktop Entry", property, "" + object[property]); + } + + return ini; +} + +function objectFromDesktopEntry(aINI) { + let ini = IniParserFactory.createINIParser(); + ini.initFromString(aINI); + + let sections = ini.getSections(); + ok(sections.hasMore(), "There is at least one section"); + Assert.equal(sections.getNext(), "Desktop Entry", "First key is correct"); + Assert.equal(sections.hasMore(), false, "...and there aren't others"); + + let keys = ini.getKeys("Desktop Entry"); + let result = Object.create(null); + while (keys.hasMore()) { + let key = keys.getNext(); + result[key] = ini.getString("Desktop Entry", key); + } + + return result; +} + +const portalBusName = "org.freedesktop.portal.Desktop"; +const portalObjectPath = "/org/freedesktop/portal/desktop"; +const portalInterfaceName = "org.freedesktop.portal.DynamicLauncher"; +const dbusMockInterface = "org.freedesktop.DBus.Mock"; +const addObjectMethod = `${dbusMockInterface}.AddObject`; +const addMethodMethod = `${dbusMockInterface}.AddMethod`; +const emitSignalDetailedMethod = `${dbusMockInterface}.EmitSignalDetailed`; +const getCallsMethod = `${dbusMockInterface}.GetCalls`; +const clearCallsMethod = `${dbusMockInterface}.ClearCalls`; +const resetMethod = `${dbusMockInterface}.Reset`; +const mockRequestObjectPath = "/org/freedesktop/portal/desktop/request"; + +var DBUS_SESSION_BUS_ADDRESS = ""; +var DBUS_SESSION_BUS_PID = 0; // eslint-disable-line no-unused-vars +var DBUS_MOCK = null; +var gdbusCmd = null; + +async function callDbusMethod(objectPath, methodName, args) { + let mockProcess = await Subprocess.call({ + command: gdbusCmd, + arguments: [ + "call", + "--session", + "-d", + portalBusName, + "-o", + objectPath, + "-m", + methodName, + ...args, + ], + }); + return mockProcess.wait(); +} + +// Launches a D-Bus daemon that the tests will run under, and sets it (via +// environment variables) to be used as the D-Bus connection. Additionally, sets +// up dbusmock to allow the test to create fake D-Bus objects; this avoids +// needing to display UI to the user, or depending on the details of the +// specific portal they have installed. +add_setup(async function setup() { + // Start and use a separate message bus for the tests, to not interfere with + // the current's session message bus. + let dbus = await Subprocess.call({ + command: await Subprocess.pathSearch("dbus-launch"), + }); + await dbus.wait(); + let stdout = await dbus.stdout.readString(); + let lines = stdout.split("\n"); + for (let i in lines) { + let tokens = lines[i].split("="); + switch (tokens.shift()) { + case "DBUS_SESSION_BUS_ADDRESS": + DBUS_SESSION_BUS_ADDRESS = tokens.join("="); + break; + case "DBUS_SESSION_BUS_PID": + DBUS_SESSION_BUS_PID = tokens.join(); + break; + default: + } + } + + gdbusCmd = await Subprocess.pathSearch("gdbus"); + + Services.env.set("DBUS_SESSION_BUS_ADDRESS", DBUS_SESSION_BUS_ADDRESS); + Services.env.set("GTK_USE_PORTAL", "1"); + + // dbusmock is used to mock the native messaging portal's D-Bus API. + DBUS_MOCK = await Subprocess.call({ + command: await Subprocess.pathSearch("python3"), + arguments: [ + "-m", + "dbusmock", + portalBusName, + portalObjectPath, + portalInterfaceName, + // vvvvvvvvvvvvv why is this needed?! + "-l", + "/dev/null", + ], + }); + + // Wait until dbusmock is ready + await TestUtils.waitForCondition(async () => { + let res = await callDbusMethod( + portalObjectPath, + "org.freedesktop.DBus.Mock.GetCalls", + [] + ); + return res.exitCode == 0; + }, "waiting for dbusmock"); + + registerCleanupFunction(async function () { + await DBUS_MOCK.kill(); + // XXX: While this works locally, it consistently fails when tests are run + // in CI, with "xpcshell return code: -15". This needs to be investigated + // further. This leaves a stray dbus-daemon process behind, + // which isn't ideal, but is harmless. + /*await lazy.Subprocess.call({ + command: await lazy.Subprocess.pathSearch("kill"), + arguments: ["-SIGQUIT", DBUS_SESSION_BUS_PID], + });*/ + }); +}); + +/** + * Determines who called the corresponding D-Bus method. dbusmock doesn't + * provide this information, but we need it to create a portal Request path, so + * this uses a one-off dbus-monitor instead. + * + * This function has two phases, determined by the promises in its return value: + * + * - monitorReady will resolve when dbus-monitor appears to be awake. At this + * point, the method call should happen. + * + * - senderPromise will resolve when the call is detected. This will resolve + * to the sender of the method call. + * + * @param {string} method + * The method call to look for. (Currently you can't key based on the object + * path.) + * @returns {{monitorReady:Promise,senderPromise:Promise}} + * A set of promises corresponding to the current 'phase' of the check. + */ +function sniffDbusMethodCaller(method) { + let readyResolvers = Promise.withResolvers(); + let doneResolvers = Promise.withResolvers(); + + // Use .then here so this can be a synchronous function, so you don't + // additionally have to worry about the promise returned. + Subprocess.pathSearch("dbus-monitor") + .then(command => + Subprocess.call({ + command, + arguments: ["--session"], + }) + ) + .then(async process => { + while (true) { + for (const line of (await process.stdout.readString()).split("\n")) { + readyResolvers.resolve(); + + // Extract the sender name from the dbus-monitor process's output. + if ( + line.startsWith("method call") && + line.includes(`member=${method}`) + ) { + let match = line.match(/sender=(\S*)/); + ok(match, "dbus-monitor output informs us of the sender"); + doneResolvers.resolve(match[1]); + await process.kill(); + return; + } + } + } + }) + .finally(err => { + readyResolvers.reject(err); + doneResolvers.reject(err); + }); + + return { + monitorReady: readyResolvers.promise, + senderPromise: doneResolvers.promise, + }; +} + +/** + * Waits for a D-Bus method call with the given object path, method, and + * position in time (in case multiple calls happened). + * + * @param {string} objectPath + * The object path the call should be for. This must be a dbusmock-based mock + * object. + * @param {string} method + * The method to get information for. + * @param {number} offset + * Indicates that information should be returned for the nth (zero-based) + * call. + * @returns {{offset:number,params:string}} + * The offset after this call as a number (zero if it wasn't called), and the + * string representation of the arguments; e.g. "'abc', 'def'". + */ +async function expectDbusMockCall(objectPath, method, offset) { + let getCalls = await Subprocess.call({ + command: gdbusCmd, + arguments: [ + "call", + "--session", + "-d", + portalBusName, + "-o", + objectPath, + "-m", + getCallsMethod, + ], + }); + let out = ""; + while (!out.endsWith("\n")) { + out += await getCalls.stdout.readString(); + } + // The other regexes are fragile, so remove the byte arrays that break them. + out = out.replaceAll(/<\('bytes', <\[byte 0x[0-9a-fx, ]+\]>\)>/g, ""); + out = out.match(/\((@a\(tsav\) )?\[(.*)\],\)/)[2]; + let calls = out.matchAll(/\(.*?\),?/g); + let methodCalled = false; + let params = {}; + let i = 0; + for (let call of calls) { + if (i++ < offset) { + continue; + } + let matches = call[0].match( + /\((uint64 )?(?\d+), '(?\w+)', (@av )?\[(?.*)\]\),?/ + ); + ok(parseFloat(matches.groups.timestamp), "timestamp is valid"); + if (matches.groups.method == method) { + methodCalled = true; + params = matches.groups.params; + break; + } + } + if (method) { + ok(methodCalled, `The ${method} mock was called`); + } else { + equal(i, 0, "No method mock was called"); + } + + await getCalls.wait(); + return { offset: i, params }; +} + +add_task(async function test_successful_creation() { + await callDbusMethod(portalObjectPath, resetMethod, []); + await callDbusMethod(portalObjectPath, clearCallsMethod, []); + + await callDbusMethod(portalObjectPath, addMethodMethod, [ + portalInterfaceName, + "PrepareInstall", + "ssva{sv}", + "o", + `ret = "/should/not/be/used"`, + ]); + + let object = { + Version: "1.5", + Type: "Application", + Name: "Example Launcher Name", + Icon: + Services.dirsvc.get("CurWorkD", Ci.nsIFile).path + + "/favicon-normal16.png", + Exec: "browser command", + }; + let ini = desktopEntryFromObject(object); + + let { monitorReady, senderPromise } = sniffDbusMethodCaller("PrepareInstall"); + await monitorReady; + + let promise = ShellService.requestInstallDynamicLauncher("a.b.c", ini, null); + + let senderName = await senderPromise; + let result = await expectDbusMockCall(portalObjectPath, "PrepareInstall", 0); + let [parentWindow, name, icon] = result.params.split(", "); + Assert.equal(parentWindow, "<''>", "No parent window was provided"); + Assert.equal(name, "<'Example Launcher Name'>", "Launcher name was provided"); + Assert.equal(icon, "", "Icon was some sequence of bytes"); + + let match = result.params.match(/'handle_token': <'(?.*)'>/); + ok(match, "Start arguments contain a handle token"); + let handleToken = match.groups.token; + + await callDbusMethod(portalObjectPath, addMethodMethod, [ + portalInterfaceName, + "Install", + "sssa{sv}", + "", + "", + ]); + + // Mock the Request object that is expected to be created in response to + // calling the Start method on the native messaging portal, wait for it to be + // available, and emit its Response signal. + let requestPath = `${mockRequestObjectPath}/${senderName + .slice(1) + .replace(".", "_")}/${handleToken}`; + await callDbusMethod(portalObjectPath, addObjectMethod, [ + requestPath, + "org.freedesktop.portal.Request", + "@a{sv} {}", + "@a(ssss) []", + ]); + await callDbusMethod(requestPath, emitSignalDetailedMethod, [ + "org.freedesktop.portal.Request", + "Response", + "ua{sv}", + `[, <@a{sv} {'token': <'qwerty'>}>]`, + `{'destination': <'${senderName}'>}`, + ]); + + // Verify that the Install method was called as expected after the Start + // request completed. + result = await expectDbusMockCall(portalObjectPath, "Install", result.offset); + let [token, desktopFileId, desktopEntry] = result.params.split(", "); + Assert.equal(token, "<'qwerty'>", "Correct token is provided"); + Assert.equal( + desktopFileId, + "<'a.b.c.desktop'>", + "Correct desktop file ID is provided" + ); + + desktopEntry = desktopEntry + .replace(/^<'/, "") + .replace(/'>$/, "") + .replaceAll(/\\n/g, "\n"); + let entry = objectFromDesktopEntry(desktopEntry); + Assert.deepEqual(entry, object, "Desktop entry contains expected values"); + + // Check for exceptions etc. + await promise; +}); + +add_task(async function test_prepareinstall_error() { + await callDbusMethod(portalObjectPath, resetMethod, []); + await callDbusMethod(portalObjectPath, clearCallsMethod, []); + + await callDbusMethod(portalObjectPath, addMethodMethod, [ + portalInterfaceName, + "PrepareInstall", + "ssva{sv}", + "o", + `raise dbus.exceptions.DBusException("prepareinstall failed", name="org.mozilla.Error.Mocked")`, + ]); + + let object = { + Version: "1.5", + Type: "Application", + Name: "Example Launcher Name", + Icon: + Services.dirsvc.get("CurWorkD", Ci.nsIFile).path + + "/favicon-normal16.png", + Exec: "browser command", + }; + let ini = desktopEntryFromObject(object); + + await Assert.rejects( + (async () => + ShellService.requestInstallDynamicLauncher("a.b.c", ini, null))(), + /.*prepareinstall failed.*/, + "Failure was propagated from D-Bus" + ); +}); + +add_task(async function test_install_error() { + await callDbusMethod(portalObjectPath, resetMethod, []); + await callDbusMethod(portalObjectPath, clearCallsMethod, []); + + await callDbusMethod(portalObjectPath, addMethodMethod, [ + portalInterfaceName, + "PrepareInstall", + "ssva{sv}", + "o", + `ret = "/should/not/be/used"`, + ]); + + let object = { + Version: "1.5", + Type: "Application", + Name: "Example Launcher Name", + Icon: + Services.dirsvc.get("CurWorkD", Ci.nsIFile).path + + "/favicon-normal16.png", + Exec: "browser command", + }; + let ini = desktopEntryFromObject(object); + + let { monitorReady, senderPromise } = sniffDbusMethodCaller("PrepareInstall"); + await monitorReady; + + let promise = ShellService.requestInstallDynamicLauncher("a.b.c", ini, null); + + let senderName = await senderPromise; + let result = await expectDbusMockCall(portalObjectPath, "PrepareInstall", 0); + + let match = result.params.match(/'handle_token': <'(?.*)'>/); + ok(match, "Start arguments contain a handle token"); + let handleToken = match.groups.token; + + await callDbusMethod(portalObjectPath, addMethodMethod, [ + portalInterfaceName, + "Install", + "sssa{sv}", + "", + `raise dbus.exceptions.DBusException("plain install failed", name="org.mozilla.Error.Mocked")`, + ]); + + // Mock the Request object that is expected to be created in response to + // calling the Start method on the native messaging portal, wait for it to be + // available, and emit its Response signal. + let requestPath = `${mockRequestObjectPath}/${senderName + .slice(1) + .replace(".", "_")}/${handleToken}`; + await callDbusMethod(portalObjectPath, addObjectMethod, [ + requestPath, + "org.freedesktop.portal.Request", + "@a{sv} {}", + "@a(ssss) []", + ]); + await callDbusMethod(requestPath, emitSignalDetailedMethod, [ + "org.freedesktop.portal.Request", + "Response", + "ua{sv}", + `[, <@a{sv} {'token': <'qwerty'>}>]`, + `{'destination': <'${senderName}'>}`, + ]); + + result = await expectDbusMockCall(portalObjectPath, "Install", result.offset); + await Assert.rejects( + promise, + /.*plain install failed.*/, + "Promise rejected with expected content." + ); +}); + +add_task(async function test_negativeResponseFromPrepareInstall() { + await callDbusMethod(portalObjectPath, resetMethod, []); + await callDbusMethod(portalObjectPath, clearCallsMethod, []); + + await callDbusMethod(portalObjectPath, addMethodMethod, [ + portalInterfaceName, + "PrepareInstall", + "ssva{sv}", + "o", + `ret = "/should/not/be/used"`, + ]); + + let object = { + Version: "1.5", + Type: "Application", + Name: "Example Launcher Name", + Icon: + Services.dirsvc.get("CurWorkD", Ci.nsIFile).path + + "/favicon-normal16.png", + Exec: "browser command", + }; + let ini = desktopEntryFromObject(object); + + let { monitorReady, senderPromise } = sniffDbusMethodCaller("PrepareInstall"); + await monitorReady; + + let promise = ShellService.requestInstallDynamicLauncher("a.b.c", ini, null); + + let senderName = await senderPromise; + let result = await expectDbusMockCall(portalObjectPath, "PrepareInstall", 0); + + let match = result.params.match(/'handle_token': <'(?.*)'>/); + ok(match, "Start arguments contain a handle token"); + let handleToken = match.groups.token; + + let requestPath = `${mockRequestObjectPath}/${senderName + .slice(1) + .replace(".", "_")}/${handleToken}`; + await callDbusMethod(portalObjectPath, addObjectMethod, [ + requestPath, + "org.freedesktop.portal.Request", + "@a{sv} {}", + "@a(ssss) []", + ]); + await callDbusMethod(requestPath, emitSignalDetailedMethod, [ + "org.freedesktop.portal.Request", + "Response", + "ua{sv}", + `[, <@a{sv} {}>]`, + `{'destination': <'${senderName}'>}`, + ]); + + await Assert.rejects( + promise, + /.*Response was non-zero.*/, + "Promise rejected with expected content." + ); +}); + +add_task(async function test_missingTokenFromPrepareInstall() { + await callDbusMethod(portalObjectPath, resetMethod, []); + await callDbusMethod(portalObjectPath, clearCallsMethod, []); + + await callDbusMethod(portalObjectPath, addMethodMethod, [ + portalInterfaceName, + "PrepareInstall", + "ssva{sv}", + "o", + `ret = "/should/not/be/used"`, + ]); + + let object = { + Version: "1.5", + Type: "Application", + Name: "Example Launcher Name", + Icon: + Services.dirsvc.get("CurWorkD", Ci.nsIFile).path + + "/favicon-normal16.png", + Exec: "browser command", + }; + let ini = desktopEntryFromObject(object); + + let { monitorReady, senderPromise } = sniffDbusMethodCaller("PrepareInstall"); + await monitorReady; + + let promise = ShellService.requestInstallDynamicLauncher("a.b.c", ini, null); + + let senderName = await senderPromise; + let result = await expectDbusMockCall(portalObjectPath, "PrepareInstall", 0); + + let match = result.params.match(/'handle_token': <'(?.*)'>/); + ok(match, "Start arguments contain a handle token"); + let handleToken = match.groups.token; + + let requestPath = `${mockRequestObjectPath}/${senderName + .slice(1) + .replace(".", "_")}/${handleToken}`; + await callDbusMethod(portalObjectPath, addObjectMethod, [ + requestPath, + "org.freedesktop.portal.Request", + "@a{sv} {}", + "@a(ssss) []", + ]); + await callDbusMethod(requestPath, emitSignalDetailedMethod, [ + "org.freedesktop.portal.Request", + "Response", + "ua{sv}", + `[, <@a{sv} {}>]`, + `{'destination': <'${senderName}'>}`, + ]); + + await Assert.rejects( + promise, + /.*No token was provided from the portal.*/, + "Promise rejected with expected content." + ); +}); + +add_task(async function test_bad_icon() { + await callDbusMethod(portalObjectPath, resetMethod, []); + await callDbusMethod(portalObjectPath, clearCallsMethod, []); + + let object = { + Version: "1.5", + Type: "Application", + Name: "Example Launcher Name", + Icon: + Services.dirsvc.get("CurWorkD", Ci.nsIFile).path + "/does-not-exist.png", + Exec: "browser command", + }; + let ini = desktopEntryFromObject(object); + + await Assert.rejects( + (async () => + ShellService.requestInstallDynamicLauncher("a.b.c", ini, null))(), + /.*Failed to open .*png.*/, + "The resulting error should indicate that the file wasn't found." + ); +}); + +add_task(async function test_uninstall_success() { + await callDbusMethod(portalObjectPath, resetMethod, []); + await callDbusMethod(portalObjectPath, clearCallsMethod, []); + + await callDbusMethod(portalObjectPath, addMethodMethod, [ + portalInterfaceName, + "Uninstall", + "sa{sv}", + "", + ``, + ]); + + await ShellService.requestUninstallDynamicLauncher("a.b.c"); + let result = await expectDbusMockCall(portalObjectPath, "Uninstall", 0); + + Assert.equal( + result.params, + "<'a.b.c.desktop'>, <@a{sv} {}>", + "Correct desktop entry ID was provided" + ); +}); + +add_task(async function test_uninstall_failure() { + await callDbusMethod(portalObjectPath, resetMethod, []); + await callDbusMethod(portalObjectPath, clearCallsMethod, []); + + await callDbusMethod(portalObjectPath, addMethodMethod, [ + portalInterfaceName, + "Uninstall", + "sa{sv}", + "", + `raise dbus.exceptions.DBusException("uninstall dbus failed", name="org.mozilla.Error.Mocked")`, + ]); + + await Assert.rejects( + (async () => ShellService.requestUninstallDynamicLauncher("a.b.c"))(), + /.*uninstall dbus failed.*/, + "The uninstall request should fail." + ); +}); diff --git a/src/zen/tests/mochitests/shell/unit/test_maybeCreateLaunchOnLoginOnFirstRun.js b/src/zen/tests/mochitests/shell/unit/test_maybeCreateLaunchOnLoginOnFirstRun.js deleted file mode 100644 index 16106de4d..000000000 --- a/src/zen/tests/mochitests/shell/unit/test_maybeCreateLaunchOnLoginOnFirstRun.js +++ /dev/null @@ -1,109 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * https://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -ChromeUtils.defineESModuleGetters(this, { - StartupOSIntegration: - "moz-src:///browser/components/shell/StartupOSIntegration.sys.mjs", - WindowsLaunchOnLogin: "resource://gre/modules/WindowsLaunchOnLogin.sys.mjs", - sinon: "resource://testing-common/Sinon.sys.mjs", -}); - -const PREF = "browser.startup.windowsLaunchOnLogin.defaultEnabled"; - -async function runWith({ isFirstRun, prefValue, approved }) { - let sandbox = sinon.createSandbox(); - let approvedStub = sandbox - .stub(WindowsLaunchOnLogin, "getLaunchOnLoginApproved") - .resolves(approved); - let createStub = sandbox - .stub(WindowsLaunchOnLogin, "createLaunchOnLogin") - .resolves(); - - if (prefValue === null) { - Services.prefs.clearUserPref(PREF); - } else { - Services.prefs.setBoolPref(PREF, prefValue); - } - - try { - await StartupOSIntegration.maybeCreateLaunchOnLoginOnFirstRun(isFirstRun); - return { approvedStub, createStub }; - } finally { - sandbox.restore(); - Services.prefs.clearUserPref(PREF); - } -} - -add_task(async function test_creates_when_all_conditions_true() { - let { createStub } = await runWith({ - isFirstRun: true, - prefValue: true, - approved: true, - }); - Assert.ok( - createStub.calledOnce, - "createLaunchOnLogin should be called when isFirstRun, pref, and approval are all true" - ); -}); - -add_task(async function test_skips_when_not_first_run() { - let { createStub, approvedStub } = await runWith({ - isFirstRun: false, - prefValue: true, - approved: true, - }); - Assert.ok( - !createStub.called, - "createLaunchOnLogin should not be called when isFirstRun is false" - ); - Assert.ok( - !approvedStub.called, - "getLaunchOnLoginApproved should be short-circuited when isFirstRun is false" - ); -}); - -add_task(async function test_skips_when_pref_disabled() { - let { createStub, approvedStub } = await runWith({ - isFirstRun: true, - prefValue: false, - approved: true, - }); - Assert.ok( - !createStub.called, - "createLaunchOnLogin should not be called when pref is false" - ); - Assert.ok( - !approvedStub.called, - "getLaunchOnLoginApproved should be short-circuited when pref is false" - ); -}); - -add_task(async function test_skips_when_windows_policy_denies() { - let { createStub, approvedStub } = await runWith({ - isFirstRun: true, - prefValue: true, - approved: false, - }); - Assert.ok( - approvedStub.calledOnce, - "getLaunchOnLoginApproved should be consulted when pref and isFirstRun are true" - ); - Assert.ok( - !createStub.called, - "createLaunchOnLogin should not be called when Windows policy denies" - ); -}); - -add_task(async function test_uses_pref_default_when_unset() { - let { createStub } = await runWith({ - isFirstRun: true, - prefValue: null, - approved: true, - }); - Assert.ok( - createStub.calledOnce, - "createLaunchOnLogin should be called when pref is at its built-in default of true" - ); -}); diff --git a/src/zen/tests/mochitests/shell/unit/xpcshell.toml b/src/zen/tests/mochitests/shell/unit/xpcshell.toml index bfd6f01ea..39bb48115 100644 --- a/src/zen/tests/mochitests/shell/unit/xpcshell.toml +++ b/src/zen/tests/mochitests/shell/unit/xpcshell.toml @@ -10,6 +10,21 @@ run-if = [ "os == 'linux'", ] +["test_dynamicLauncher.js"] +environment = [ + "XPCSHELL_TEST=1", + # The following prevents gsettings from initializing dbus before our mock is + # ready. + "GSETTINGS_BACKEND=memory", +] +run-if = [ + "os == 'linux'", +] +support-files = [ + "../../../places/tests/browser/favicon-normal16.png", +] +tags = ["portal"] + ["test_linuxDesktopEntry.js"] run-if = [ "os == 'linux'", @@ -20,11 +35,6 @@ run-if = [ "os == 'mac'", ] -["test_maybeCreateLaunchOnLoginOnFirstRun.js"] -run-if = [ - "os == 'win'" -] - ["test_secondaryTileJs.js"] run-if = [ "os == 'win'" diff --git a/surfer.json b/surfer.json index e916ace81..992d65c26 100644 --- a/surfer.json +++ b/surfer.json @@ -5,7 +5,7 @@ "binaryName": "zen", "version": { "product": "firefox", - "version": "153.0", + "version": "153.0.1", "candidate": "153.0", "candidateBuild": 1 },