667 lines
20 KiB
JavaScript
667 lines
20 KiB
JavaScript
/* 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 = origin => {
|
|
if (isBackgroundTaskMode) {
|
|
return [];
|
|
}
|
|
let content = "Notification settings";
|
|
return [
|
|
content,
|
|
{
|
|
content,
|
|
arguments: parsedArgumentString(
|
|
Object.assign(
|
|
{
|
|
action: "settings",
|
|
},
|
|
origin && {
|
|
origin,
|
|
}
|
|
)
|
|
),
|
|
placement: "contextmenu",
|
|
},
|
|
];
|
|
};
|
|
|
|
let parsedSnoozeAction = (hostport, origin) => {
|
|
let content = `Disable notifications from ${hostport}`;
|
|
return [
|
|
content,
|
|
{
|
|
content,
|
|
arguments: parsedArgumentString(
|
|
Object.assign(
|
|
{
|
|
action: "snooze",
|
|
},
|
|
origin && {
|
|
origin,
|
|
}
|
|
)
|
|
),
|
|
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 path = "https://example.com/foo/bar.html";
|
|
const principaluri = Services.io.newURI(path);
|
|
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: "",
|
|
origin: principal.origin,
|
|
}),
|
|
actions: Object.fromEntries(
|
|
[
|
|
parsedSnoozeAction(principal.hostPort, principal.origin),
|
|
parsedSettingsAction(principal.origin),
|
|
[
|
|
"dismissTitle",
|
|
{
|
|
content: "dismissTitle",
|
|
arguments: parsedArgumentString({
|
|
action: "dismiss",
|
|
origin: principal.origin,
|
|
}),
|
|
},
|
|
],
|
|
[
|
|
"snoozeTitle",
|
|
{
|
|
content: "snoozeTitle",
|
|
arguments: parsedArgumentString({
|
|
action: "snooze",
|
|
origin: principal.origin,
|
|
}),
|
|
},
|
|
],
|
|
].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: "",
|
|
origin: principal.origin,
|
|
}),
|
|
actions: Object.fromEntries(
|
|
[
|
|
parsedSnoozeAction(principal.hostPort, principal.origin),
|
|
parsedSettingsAction(principal.origin),
|
|
].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("");
|
|
});
|