diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /widget/windows/tests/unit | |
parent | Initial commit. (diff) | |
download | firefox-upstream/124.0.1.tar.xz firefox-upstream/124.0.1.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'widget/windows/tests/unit')
-rw-r--r-- | widget/windows/tests/unit/test_windows_alert_service.js | 667 | ||||
-rw-r--r-- | widget/windows/tests/unit/xpcshell.toml | 3 |
2 files changed, 670 insertions, 0 deletions
diff --git a/widget/windows/tests/unit/test_windows_alert_service.js b/widget/windows/tests/unit/test_windows_alert_service.js new file mode 100644 index 0000000000..0ba0d2a4d4 --- /dev/null +++ b/widget/windows/tests/unit/test_windows_alert_service.js @@ -0,0 +1,667 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Test that Windows alert notifications generate expected XML. + */ + +var { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +let gProfD = do_get_profile(); + +// Setup that allows to use the profile service in xpcshell tests, +// lifted from `toolkit/profile/xpcshell/head.js`. +function setupProfileService() { + let gDataHome = gProfD.clone(); + gDataHome.append("data"); + gDataHome.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + let gDataHomeLocal = gProfD.clone(); + gDataHomeLocal.append("local"); + gDataHomeLocal.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + + let xreDirProvider = Cc["@mozilla.org/xre/directory-provider;1"].getService( + Ci.nsIXREDirProvider + ); + xreDirProvider.setUserDataDirectory(gDataHome, false); + xreDirProvider.setUserDataDirectory(gDataHomeLocal, true); +} + +add_setup(setupProfileService); + +function makeAlert(options) { + var alert = Cc["@mozilla.org/alert-notification;1"].createInstance( + Ci.nsIAlertNotification + ); + alert.init( + options.name, + options.imageURL, + options.title, + options.text, + options.textClickable, + options.cookie, + options.dir, + options.lang, + options.data, + options.principal, + options.inPrivateBrowsing, + options.requireInteraction, + options.silent, + options.vibrate || [] + ); + if (options.actions) { + alert.actions = options.actions; + } + if (options.opaqueRelaunchData) { + alert.opaqueRelaunchData = options.opaqueRelaunchData; + } + return alert; +} + +/** + * Take a `key1\nvalue1\n...` string encoding as used by the Windows native + * notification server DLL, and split it into an object, keeping `action\n...` + * intact. + * + * @param {string} t string encoding. + * @returns {object} an object with keys and values. + */ +function parseOneEncoded(t) { + var launch = {}; + + var lines = t.split("\n"); + while (lines.length) { + var key = lines.shift(); + var value; + if (key === "action") { + value = lines.join("\n"); + lines = []; + } else { + value = lines.shift(); + } + launch[key] = value; + } + + return launch; +} + +/** + * This complicated-looking function takes a (XML) string representation of a + * Windows alert (toast notification), parses it into XML, extracts and further + * parses internal data, and returns a simplified XML representation together + * with the parsed internals. + * + * Doing this lets us compare JSON objects rather than stringified-JSON further + * encoded as XML strings, which have lots of slashes and `"` characters to + * contend with. + * + * @param {string} s XML string for Windows alert. + + * @returns {Array} a pair of a simplified XML string and an object with + * `launch` and `actions` keys. + */ +function parseLaunchAndActions(s) { + var document = new DOMParser().parseFromString(s, "text/xml"); + var root = document.documentElement; + + var launchString = root.getAttribute("launch"); + root.setAttribute("launch", "launch"); + var launch = parseOneEncoded(launchString); + + // `actions` is keyed by "content" attribute. + let actions = {}; + for (var actionElement of root.querySelectorAll("action")) { + // `activationType="system"` is special. Leave them alone. + let systemActivationType = + actionElement.getAttribute("activationType") === "system"; + + let action = {}; + let names = [...actionElement.attributes].map(attribute => attribute.name); + + for (var name of names) { + let value = actionElement.getAttribute(name); + + // Here is where we parse stringified-JSON to simplify comparisons. + if (value.startsWith("{")) { + value = JSON.parse(value); + if ("opaqueRelaunchData" in value) { + value.opaqueRelaunchData = JSON.parse(value.opaqueRelaunchData); + } + } + + if (name == "arguments" && !systemActivationType) { + action[name] = parseOneEncoded(value); + } else { + action[name] = value; + } + + if (name != "content" && !systemActivationType) { + actionElement.removeAttribute(name); + } + } + + let actionName = actionElement.getAttribute("content"); + actions[actionName] = action; + } + + return [new XMLSerializer().serializeToString(document), { launch, actions }]; +} + +function escape(s) { + return s + .replace(/&/g, "&") + .replace(/"/g, """) + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/\n/g, "
"); +} + +function unescape(s) { + return s + .replace(/&/g, "&") + .replace(/"/g, '"') + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/
/g, "\n"); +} + +function testAlert(when, { serverEnabled, profD, isBackgroundTaskMode } = {}) { + let argumentString = action => { + // 
 is "\n". + let s = ``; + if (serverEnabled) { + s += `program
${AppConstants.MOZ_APP_NAME}`; + } else { + s += `invalid key
invalid value`; + } + if (serverEnabled && profD) { + s += `
profile
${profD.path}`; + } + if (serverEnabled) { + s += "
windowsTag
"; + } + if (action) { + s += `
action
${escape(JSON.stringify(action))}`; + } + + return s; + }; + + let parsedArgumentString = action => + parseOneEncoded(unescape(argumentString(action))); + + let settingsAction = isBackgroundTaskMode + ? "" + : `<action content="Notification settings"/>`; + + let parsedSettingsAction = hostport => { + if (isBackgroundTaskMode) { + return []; + } + let content = "Notification settings"; + return [ + content, + { + content, + arguments: parsedArgumentString( + Object.assign( + { + action: "settings", + }, + hostport && { + launchUrl: hostport, + } + ) + ), + placement: "contextmenu", + }, + ]; + }; + + let parsedSnoozeAction = hostport => { + let content = `Disable notifications from ${hostport}`; + return [ + content, + { + content, + arguments: parsedArgumentString( + Object.assign( + { + action: "snooze", + }, + hostport && { + launchUrl: hostport, + } + ) + ), + placement: "contextmenu", + }, + ]; + }; + + let alertsService = Cc["@mozilla.org/system-alerts-service;1"] + .getService(Ci.nsIAlertsService) + .QueryInterface(Ci.nsIWindowsAlertsService); + + let name = "name"; + let title = "title"; + let text = "text"; + let imageURL = "file:///image.png"; + let actions = [ + { action: "action1", title: "title1", iconURL: "file:///iconURL1.png" }, + { action: "action2", title: "title2", iconURL: "file:///iconURL2.png" }, + ]; + let opaqueRelaunchData = { foo: 1, bar: "two" }; + + let alert = makeAlert({ name, title, text }); + let expected = `<toast launch="launch"><visual><binding template="ToastText03"><text id="1">title</text><text id="2">text</text></binding></visual><actions>${settingsAction}</actions></toast>`; + Assert.deepEqual( + [ + expected.replace("<actions></actions>", "<actions/>"), + { + launch: parsedArgumentString({ action: "" }), + actions: Object.fromEntries( + [parsedSettingsAction()].filter(x => x.length) + ), + }, + ], + parseLaunchAndActions(alertsService.getXmlStringForWindowsAlert(alert)), + when + ); + + alert = makeAlert({ name, title, text, imageURL }); + expected = `<toast launch="launch"><visual><binding template="ToastImageAndText03"><image id="1" src="file:///image.png"/><text id="1">title</text><text id="2">text</text></binding></visual><actions>${settingsAction}</actions></toast>`; + Assert.deepEqual( + [ + expected.replace("<actions></actions>", "<actions/>"), + { + launch: parsedArgumentString({ action: "" }), + actions: Object.fromEntries( + [parsedSettingsAction()].filter(x => x.length) + ), + }, + ], + parseLaunchAndActions(alertsService.getXmlStringForWindowsAlert(alert)), + when + ); + + alert = makeAlert({ name, title, text, imageURL, requireInteraction: true }); + expected = `<toast scenario="reminder" launch="launch"><visual><binding template="ToastImageAndText03"><image id="1" src="file:///image.png"/><text id="1">title</text><text id="2">text</text></binding></visual><actions>${settingsAction}<action content="Dismiss" arguments="dismiss" activationType="system"/></actions></toast>`; + Assert.deepEqual( + [ + expected.replace("<actions></actions>", "<actions/>"), + { + launch: parsedArgumentString({ action: "" }), + actions: Object.fromEntries( + [ + parsedSettingsAction(), + [ + "Dismiss", + { + content: "Dismiss", + arguments: "dismiss", + activationType: "system", + }, + ], + ].filter(x => x.length) + ), + }, + ], + parseLaunchAndActions(alertsService.getXmlStringForWindowsAlert(alert)), + when + ); + + alert = makeAlert({ name, title, text, imageURL, actions }); + expected = `<toast launch="launch"><visual><binding template="ToastImageAndText03"><image id="1" src="file:///image.png"/><text id="1">title</text><text id="2">text</text></binding></visual><actions>${settingsAction}<action content="title1"/><action content="title2"/></actions></toast>`; + Assert.deepEqual( + [ + expected.replace("<actions></actions>", "<actions/>"), + { + launch: parsedArgumentString({ action: "" }), + actions: Object.fromEntries( + [ + parsedSettingsAction(), + [ + "title1", + { + content: "title1", + arguments: parsedArgumentString({ action: "action1" }), + }, + ], + [ + "title2", + { + content: "title2", + arguments: parsedArgumentString({ action: "action2" }), + }, + ], + ].filter(x => x.length) + ), + }, + ], + parseLaunchAndActions(alertsService.getXmlStringForWindowsAlert(alert)), + when + ); + + // Chrome privileged alerts can use `windowsSystemActivationType`. + let systemActions = [ + { + action: "dismiss", + title: "dismissTitle", + windowsSystemActivationType: true, + }, + { + action: "snooze", + title: "snoozeTitle", + windowsSystemActivationType: true, + }, + ]; + let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); + alert = makeAlert({ + name, + title, + text, + imageURL, + principal: systemPrincipal, + actions: systemActions, + }); + let parsedSettingsActionWithPrivilegedName = isBackgroundTaskMode + ? [] + : [ + "Notification settings", + { + content: "Notification settings", + arguments: parsedArgumentString({ + action: "settings", + privilegedName: name, + }), + placement: "contextmenu", + }, + ]; + + expected = `<toast launch="launch"><visual><binding template="ToastGeneric"><image id="1" src="file:///image.png"/><text id="1">title</text><text id="2">text</text></binding></visual><actions>${settingsAction}<action content="dismissTitle" arguments="dismiss" activationType="system"/><action content="snoozeTitle" arguments="snooze" activationType="system"/></actions></toast>`; + Assert.deepEqual( + [ + expected.replace("<actions></actions>", "<actions/>"), + { + launch: parsedArgumentString({ action: "", privilegedName: name }), + actions: Object.fromEntries( + [ + parsedSettingsActionWithPrivilegedName, + [ + "dismissTitle", + { + content: "dismissTitle", + arguments: "dismiss", + activationType: "system", + }, + ], + [ + "snoozeTitle", + { + content: "snoozeTitle", + arguments: "snooze", + activationType: "system", + }, + ], + ].filter(x => x.length) + ), + }, + ], + parseLaunchAndActions(alertsService.getXmlStringForWindowsAlert(alert)), + when + ); + + // But content unprivileged alerts can't use `windowsSystemActivationType`. + let launchUrl = "https://example.com/foo/bar.html"; + const principaluri = Services.io.newURI(launchUrl); + const principal = Services.scriptSecurityManager.createContentPrincipal( + principaluri, + {} + ); + + alert = makeAlert({ + name, + title, + text, + imageURL, + actions: systemActions, + principal, + }); + expected = `<toast launch="launch"><visual><binding template="ToastImageAndText04"><image id="1" src="file:///image.png"/><text id="1">title</text><text id="2">text</text><text id="3" placement="attribution">via example.com</text></binding></visual><actions><action content="Disable notifications from example.com"/>${settingsAction}<action content="dismissTitle"/><action content="snoozeTitle"/></actions></toast>`; + Assert.deepEqual( + [ + expected.replace("<actions></actions>", "<actions/>"), + { + launch: parsedArgumentString({ + action: "", + launchUrl: principaluri.hostPort, + }), + actions: Object.fromEntries( + [ + parsedSnoozeAction(principaluri.hostPort), + parsedSettingsAction(principaluri.hostPort), + [ + "dismissTitle", + { + content: "dismissTitle", + arguments: parsedArgumentString({ + action: "dismiss", + launchUrl: principaluri.hostPort, + }), + }, + ], + [ + "snoozeTitle", + { + content: "snoozeTitle", + arguments: parsedArgumentString({ + action: "snooze", + launchUrl: principaluri.hostPort, + }), + }, + ], + ].filter(x => x.length) + ), + }, + ], + parseLaunchAndActions(alertsService.getXmlStringForWindowsAlert(alert)), + when + ); + + // Chrome privileged alerts can set `opaqueRelaunchData`. + alert = makeAlert({ + name, + title, + text, + imageURL, + principal: systemPrincipal, + opaqueRelaunchData: JSON.stringify(opaqueRelaunchData), + }); + expected = `<toast launch="launch"><visual><binding template="ToastGeneric"><image id="1" src="file:///image.png"/><text id="1">title</text><text id="2">text</text></binding></visual><actions>${settingsAction}</actions></toast>`; + Assert.deepEqual( + [ + expected.replace("<actions></actions>", "<actions/>"), + { + launch: parsedArgumentString({ + action: "", + opaqueRelaunchData: JSON.stringify(opaqueRelaunchData), + privilegedName: name, + }), + actions: Object.fromEntries( + [parsedSettingsActionWithPrivilegedName].filter(x => x.length) + ), + }, + ], + parseLaunchAndActions(alertsService.getXmlStringForWindowsAlert(alert)), + when + ); + + // But content unprivileged alerts can't set `opaqueRelaunchData`. + alert = makeAlert({ + name, + title, + text, + imageURL, + principal, + opaqueRelaunchData: JSON.stringify(opaqueRelaunchData), + }); + expected = `<toast launch="launch"><visual><binding template="ToastImageAndText04"><image id="1" src="file:///image.png"/><text id="1">title</text><text id="2">text</text><text id="3" placement="attribution">via example.com</text></binding></visual><actions><action content="Disable notifications from example.com"/>${settingsAction}</actions></toast>`; + Assert.deepEqual( + [ + expected.replace("<actions></actions>", "<actions/>"), + { + launch: parsedArgumentString({ + action: "", + launchUrl: principaluri.hostPort, + }), + actions: Object.fromEntries( + [ + parsedSnoozeAction(principaluri.hostPort), + parsedSettingsAction(principaluri.hostPort), + ].filter(x => x.length) + ), + }, + ], + parseLaunchAndActions(alertsService.getXmlStringForWindowsAlert(alert)), + when + ); + + // Chrome privileged alerts can set action-specific relaunch parameters. + let systemRelaunchActions = [ + { + action: "action1", + title: "title1", + opaqueRelaunchData: JSON.stringify({ json: "data1" }), + }, + { + action: "action2", + title: "title2", + opaqueRelaunchData: JSON.stringify({ json: "data2" }), + }, + ]; + systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); + alert = makeAlert({ + name, + title, + text, + imageURL, + principal: systemPrincipal, + actions: systemRelaunchActions, + }); + expected = `<toast launch="launch"><visual><binding template="ToastGeneric"><image id="1" src="file:///image.png"/><text id="1">title</text><text id="2">text</text></binding></visual><actions>${settingsAction}<action content="title1"/><action content="title2"/></actions></toast>`; + Assert.deepEqual( + [ + expected.replace("<actions></actions>", "<actions/>"), + { + launch: parsedArgumentString({ action: "", privilegedName: name }), + actions: Object.fromEntries( + [ + parsedSettingsActionWithPrivilegedName, + [ + "title1", + { + content: "title1", + arguments: parsedArgumentString( + { + action: "action1", + opaqueRelaunchData: JSON.stringify({ json: "data1" }), + privilegedName: name, + }, + null, + name + ), + }, + ], + + [ + "title2", + { + content: "title2", + arguments: parsedArgumentString( + { + action: "action2", + opaqueRelaunchData: JSON.stringify({ json: "data2" }), + privilegedName: name, + }, + null, + name + ), + }, + ], + ].filter(x => x.length) + ), + }, + ], + parseLaunchAndActions(alertsService.getXmlStringForWindowsAlert(alert)), + when + ); +} + +add_task(async () => { + Services.prefs.deleteBranch( + "alerts.useSystemBackend.windows.notificationserver.enabled" + ); + testAlert("when notification server pref is unset", { + profD: gProfD, + }); + + Services.prefs.setBoolPref( + "alerts.useSystemBackend.windows.notificationserver.enabled", + false + ); + testAlert("when notification server pref is false", { profD: gProfD }); + + Services.prefs.setBoolPref( + "alerts.useSystemBackend.windows.notificationserver.enabled", + true + ); + testAlert("when notification server pref is true", { + serverEnabled: true, + profD: gProfD, + }); +}); + +let condition = { + skip_if: () => !AppConstants.MOZ_BACKGROUNDTASKS, +}; + +add_task(condition, async () => { + const bts = Cc["@mozilla.org/backgroundtasks;1"]?.getService( + Ci.nsIBackgroundTasks + ); + + // Pretend that this is a background task. + bts.overrideBackgroundTaskNameForTesting("taskname"); + + Services.prefs.setBoolPref( + "alerts.useSystemBackend.windows.notificationserver.enabled", + true + ); + testAlert( + "when notification server pref is true in background task, no default profile", + { serverEnabled: true, isBackgroundTaskMode: true } + ); + + let profileService = Cc["@mozilla.org/toolkit/profile-service;1"].getService( + Ci.nsIToolkitProfileService + ); + + let profilePath = do_get_profile(); + profilePath.append(`test_windows_alert_service`); + let profile = profileService.createUniqueProfile( + profilePath, + "test_windows_alert_service" + ); + + profileService.defaultProfile = profile; + + testAlert( + "when notification server pref is true in background task, default profile", + { serverEnabled: true, isBackgroundTaskMode: true, profD: profilePath } + ); + + // No longer a background task, + bts.overrideBackgroundTaskNameForTesting(""); +}); diff --git a/widget/windows/tests/unit/xpcshell.toml b/widget/windows/tests/unit/xpcshell.toml new file mode 100644 index 0000000000..9943d5510e --- /dev/null +++ b/widget/windows/tests/unit/xpcshell.toml @@ -0,0 +1,3 @@ +[DEFAULT] + +["test_windows_alert_service.js"] |