Bug 1781929 - Part 4: Pass through or launch Firefox from notification server. r=nrishel

When the notification server callback is executed by the Windows
notification system, it invokes Firefox with additional command line
parameters, most importantly the Windows-specific notification
"Windows tag".

If no appropriate Firefox is running, the command line will be
processed, the provided Windows tag will be inspected (and seen to not
be registered with this running Firefox instance) and a "launch URL"
stored as part of the Windows notification itself opened (if one is
provided).

If an appropriate Firefox is running, the remoting protocol will
forward this command line to the running instance.  If the instance
recognizes the provided `--notification-windowsTag`, the command line
will be ignored.  When the notification server exits, Windows will
fallback to the Windows 8.1 style notification callbacks registered
for this Windows tag and the existing (non notification server)
behaviour will occur.

In fact, the server therefore waits for a Windows tag-specific system
event to be signalled by the invoked Firefox (or a sibling process).
If we were to return `S_OK` from the notification server immediately,
and a running Firefox process would handle the notification via
Windows 8.1-style notification callbacks, then Windows would fall back
to those callbacks.  The invoked callbacks unregister themselves upon
completion, often before the launched Firefox has an opportunity to
process the command line.  By waiting for this system event, we allow
the invoked Firefox to process the command line while its own
notification callbacks are registered and therefore recognize that its
callbacks will handle the notification.

Differential Revision: https://phabricator.services.mozilla.com/D154468
This commit is contained in:
Nick Alexander 2022-09-02 20:22:51 +00:00
Родитель 7dff57f69c
Коммит 7ad3c38071
8 изменённых файлов: 346 добавлений и 31 удалений

Просмотреть файл

@ -1016,6 +1016,37 @@ nsDefaultCommandLineHandler.prototype = {
handle: function dch_handle(cmdLine) {
var urilist = [];
if (AppConstants.platform == "win") {
let windowsAlertsService = Cc["@mozilla.org/system-alerts-service;1"]
.getService(Ci.nsIAlertsService)
.QueryInterface(Ci.nsIWindowsAlertsService);
var tag;
while (
(tag = cmdLine.handleFlagWithParam("notification-windowsTag", false))
) {
let onUnknownWindowsTag = (unknownTag, launchUrl) => {
let uri = resolveURIInternal(cmdLine, launchUrl);
console.info(
`Opening ${uri.spec} to complete Windows notification with tag '${unknownTag}'`
);
urilist.push(uri);
};
try {
if (
windowsAlertsService?.handleWindowsTag(tag, onUnknownWindowsTag)
) {
// Don't pop open a new window.
cmdLine.preventDefault = true;
}
} catch (e) {
Cu.reportError(
`Error handling Windows notification with tag '${tag}': ${e}`
);
}
}
}
if (
cmdLine.state == Ci.nsICommandLine.STATE_INITIAL_LAUNCH &&
Services.startup.wasSilentlyStarted

Просмотреть файл

@ -7,16 +7,44 @@
#include "nsISupports.idl"
#include "nsIObserver.idl"
[scriptable, function, uuid(059f8305-4e2f-4d31-a9cb-5b918ee84773)]
interface nsIUnknownWindowsTagListener : nsISupports
{
/**
* Handle a launch URL associated to the given Windows-specific tag string.
* Usually, this will navigate to the launch URL in some manner.
*
* @param {AString} aWindowsTag the tag
* @param {AString} aLaunchURL associated launch URL.
*/
void handleUnknownWindowsTag(in AString aWindowsTag,
in AString aLaunchURL);
};
[scriptable, uuid(e01c8066-fb4b-4304-b9c9-ab6ed4a8322c)]
interface nsIWindowsAlertsService : nsIAlertsService
{
/**
* If callbacks for the given Windows-specific tag string will be handled by
* this Firefox process, set the associated event.
*
* @param {AString} aWindowsTag the tag
* @param {nsIUnhandledWindowsTagListener} aListener the listener to callback
* if the tag is unknown and has an associated launch URL.
* @return {boolean} `true` iff the tag is registered and an event was set.
*/
bool handleWindowsTag(in AString aWindowsTag,
in nsIUnknownWindowsTagListener aListener);
/**
* Get the Windows-specific XML generated for the given alert.
*
* @note This method is intended for testing purposes.
*
* @param {nsIAlertNotification} aAlert the alert
* @param {AString} an optional Windows tag; default is generated
* @return {string} generated XML
*/
AString getXmlStringForWindowsAlert(in nsIAlertNotification aAlert);
AString getXmlStringForWindowsAlert(in nsIAlertNotification aAlert,
[optional] in AString aWindowsTag);
};

Просмотреть файл

@ -9,6 +9,7 @@
// XUL notifications make no sense in background tasks. This is only applies to
// Windows for now.
pref("alerts.useSystemBackend", true);
pref("alerts.useSystemBackend.windows.notificationserver.enabled", true);
// Configure Messaging Experiments for background tasks, with
// background task-specific feature ID. The regular Firefox Desktop

Просмотреть файл

@ -15,6 +15,8 @@
#include "mozilla/CmdLineAndEnvUtils.h"
#include "mozilla/UniquePtr.h"
#define NOTIFICATION_SERVER_EVENT_TIMEOUT_MS (10 * 1000)
HRESULT STDMETHODCALLTYPE
NotificationCallback::QueryInterface(REFIID riid, void** ppvObject) {
if (!ppvObject) {
@ -39,6 +41,7 @@ HRESULT STDMETHODCALLTYPE NotificationCallback::Activate(
const NOTIFICATION_USER_INPUT_DATA* data, ULONG dataCount) {
std::wstring program;
std::wstring profile;
std::wstring windowsTag;
LOG_ERROR_MESSAGE((L"Invoked with arguments: '%s'"), invokedArgs);
@ -52,6 +55,8 @@ HRESULT STDMETHODCALLTYPE NotificationCallback::Activate(
}
} else if (key == L"profile") {
profile = value;
} else if (key == L"windowsTag") {
windowsTag = value;
} else if (key == L"action") {
// Remainder of args are from the Web Notification action, don't parse.
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1781929.
@ -77,9 +82,24 @@ HRESULT STDMETHODCALLTYPE NotificationCallback::Activate(
LOG_ERROR_MESSAGE((L"No profile; invocation will choose default profile"));
}
if (!windowsTag.empty()) {
childArgv.push_back(L"--notification-windowsTag");
childArgv.push_back(windowsTag.c_str());
} else {
LOG_ERROR_MESSAGE((L"No windowsTag; invoking anyway"));
}
mozilla::UniquePtr<wchar_t[]> cmdLine(
mozilla::MakeCommandLine(childArgv.size(), childArgv.data()));
// This event object will let Firefox notify us when it has handled the
// notification.
std::wstring eventName(windowsTag);
nsAutoHandle event;
if (!eventName.empty()) {
event.own(CreateEventW(nullptr, TRUE, FALSE, eventName.c_str()));
}
STARTUPINFOW si = {0};
si.cb = sizeof(STARTUPINFOW);
PROCESS_INFORMATION pi = {0};
@ -91,5 +111,32 @@ HRESULT STDMETHODCALLTYPE NotificationCallback::Activate(
LOG_ERROR_MESSAGE((L"Invoked %s"), cmdLine.get());
if (windowsTag.empty()) {
return S_OK;
}
if (event.get()) {
LOG_ERROR_MESSAGE((L"Waiting on event with name '%s'"), eventName.c_str());
DWORD result =
WaitForSingleObject(event, NOTIFICATION_SERVER_EVENT_TIMEOUT_MS);
if (result == WAIT_TIMEOUT) {
LOG_ERROR_MESSAGE(L"Wait timed out");
return S_OK;
} else if (result == WAIT_FAILED) {
LOG_ERROR_MESSAGE((L"Wait failed: %#X"), GetLastError());
return S_OK;
} else if (result == WAIT_ABANDONED) {
LOG_ERROR_MESSAGE((L"Wait abandoned"));
return S_OK;
} else {
LOG_ERROR_MESSAGE((L"Wait succeeded!"));
return S_OK;
}
} else {
LOG_ERROR_MESSAGE((L"Failed to create event with name '%s'"),
eventName.c_str());
}
return S_OK;
}

Просмотреть файл

@ -34,7 +34,7 @@
namespace mozilla {
namespace widget {
static LazyLogModule sWASLog("WindowsAlertsService");
LazyLogModule sWASLog("WindowsAlertsService");
NS_IMPL_ISUPPORTS(ToastNotification, nsIAlertsService, nsIWindowsAlertsService,
nsIAlertsDoNotDisturb, nsIObserver)
@ -431,8 +431,16 @@ ToastNotification::ShowAlert(nsIAlertNotification* aAlert,
textClickable, requireInteraction, actions, isSystemPrincipal, launchUrl);
mActiveHandlers.InsertOrUpdate(name, RefPtr{handler});
MOZ_LOG(sWASLog, LogLevel::Debug,
("Adding handler '%s': [%p] (now %d handlers)",
NS_ConvertUTF16toUTF8(name).get(), handler.get(),
mActiveHandlers.Count()));
nsresult rv = handler->InitAlertAsync(aAlert);
if (NS_WARN_IF(NS_FAILED(rv))) {
MOZ_LOG(sWASLog, LogLevel::Debug,
("Failed to init alert, removing '%s'",
NS_ConvertUTF16toUTF8(name).get()));
mActiveHandlers.Remove(name);
handler->UnregisterHandler();
return rv;
@ -448,6 +456,7 @@ ToastNotification::ShowAlert(nsIAlertNotification* aAlert,
NS_IMETHODIMP
ToastNotification::GetXmlStringForWindowsAlert(nsIAlertNotification* aAlert,
const nsAString& aWindowsTag,
nsAString& aString) {
NS_ENSURE_ARG(aAlert);
@ -488,12 +497,84 @@ ToastNotification::GetXmlStringForWindowsAlert(nsIAlertNotification* aAlert,
text, hostPort, textClickable, requireInteraction, actions,
isSystemPrincipal, launchUrl);
// Usually, this will be empty during testing, making test output
// deterministic.
MOZ_TRY(handler->SetWindowsTag(aWindowsTag));
nsAutoString imageURL;
MOZ_TRY(aAlert->GetImageURL(imageURL));
return handler->CreateToastXmlString(imageURL, aString);
}
NS_IMETHODIMP
ToastNotification::HandleWindowsTag(const nsAString& aWindowsTag,
nsIUnknownWindowsTagListener* aListener,
bool* aRetVal) {
*aRetVal = false;
NS_ENSURE_TRUE(mAumid.isSome(), NS_ERROR_UNEXPECTED);
MOZ_LOG(sWASLog, LogLevel::Debug,
("Iterating %d handlers", mActiveHandlers.Count()));
for (auto iter = mActiveHandlers.Iter(); !iter.Done(); iter.Next()) {
RefPtr<ToastNotificationHandler> handler = iter.UserData();
nsAutoString tag;
nsresult rv = handler->GetWindowsTag(tag);
if (NS_SUCCEEDED(rv)) {
MOZ_LOG(sWASLog, LogLevel::Debug,
("Comparing external windowsTag '%s' to handled windowsTag '%s'",
NS_ConvertUTF16toUTF8(aWindowsTag).get(),
NS_ConvertUTF16toUTF8(tag).get()));
if (aWindowsTag.Equals(tag)) {
MOZ_LOG(sWASLog, LogLevel::Debug,
("External windowsTag '%s' is handled by handler [%p]",
NS_ConvertUTF16toUTF8(aWindowsTag).get(), handler.get()));
*aRetVal = true;
nsString eventName(aWindowsTag);
nsAutoHandle event(
OpenEventW(EVENT_MODIFY_STATE, FALSE, eventName.get()));
if (event.get()) {
if (SetEvent(event)) {
MOZ_LOG(sWASLog, LogLevel::Info,
("Set event for event named '%s'",
NS_ConvertUTF16toUTF8(eventName).get()));
} else {
MOZ_LOG(
sWASLog, LogLevel::Error,
("Failed to set event for event named '%s' (GetLastError=%lu)",
NS_ConvertUTF16toUTF8(eventName).get(), GetLastError()));
}
} else {
MOZ_LOG(sWASLog, LogLevel::Error,
("Failed to open event named '%s' (GetLastError=%lu)",
NS_ConvertUTF16toUTF8(eventName).get(), GetLastError()));
}
return NS_OK;
}
} else {
MOZ_LOG(sWASLog, LogLevel::Debug,
("Failed to get windowsTag for handler [%p]", handler.get()));
}
}
MOZ_LOG(sWASLog, LogLevel::Debug, ("aListener [%p]", aListener));
if (aListener) {
nsAutoString launchUrl;
MOZ_TRY(ToastNotificationHandler::FindLaunchURLForWindowsTag(
aWindowsTag, mAumid.ref(), launchUrl));
MOZ_LOG(sWASLog, LogLevel::Debug,
("Found launchUrl [%s]", NS_ConvertUTF16toUTF8(launchUrl).get()));
aListener->HandleUnknownWindowsTag(aWindowsTag, launchUrl);
}
return NS_OK;
}
NS_IMETHODIMP
ToastNotification::CloseAlert(const nsAString& aAlertName) {
RefPtr<ToastNotificationHandler> handler;

Просмотреть файл

@ -16,6 +16,7 @@
#endif
#include "mozilla/HashFunctions.h"
#include "mozilla/Result.h"
#include "mozilla/Logging.h"
#include "mozilla/Tokenizer.h"
#include "mozilla/WindowsVersion.h"
#include "nsAppDirectoryServiceDefs.h"
@ -41,6 +42,8 @@
namespace mozilla {
namespace widget {
extern LazyLogModule sWASLog;
using namespace ABI::Windows::Data::Xml::Dom;
using namespace ABI::Windows::Foundation;
using namespace ABI::Windows::UI::Notifications;
@ -146,6 +149,16 @@ static bool AddActionNode(ComPtr<IXmlDocument>& toastXml,
return true;
}
nsresult ToastNotificationHandler::GetWindowsTag(nsAString& aWindowsTag) {
aWindowsTag.Assign(mWindowsTag);
return NS_OK;
}
nsresult ToastNotificationHandler::SetWindowsTag(const nsAString& aWindowsTag) {
mWindowsTag.Assign(aWindowsTag);
return NS_OK;
}
// clang - format off
/* Populate the launch argument so the COM server can reconstruct the toast
* origin.
@ -217,6 +230,9 @@ Result<nsString, nsresult> ToastNotificationHandler::GetLaunchArgument() {
launchArg += u"\nlaunchUrl\n"_ns + mLaunchUrl;
}
// `windowsTag` argument.
launchArg += u"\nwindowsTag\n"_ns + mWindowsTag;
return launchArg;
}
@ -414,6 +430,9 @@ ComPtr<IXmlDocument> ToastNotificationHandler::CreateToastXmlDocument() {
success = SetAttribute(toastElement, HStringReference(L"launch"), launchArg);
NS_ENSURE_TRUE(success, nullptr);
MOZ_LOG(sWASLog, LogLevel::Debug,
("launchArg: '%s'", NS_ConvertUTF16toUTF8(launchArg).get()));
ComPtr<IXmlElement> actions;
hr = toastXml->CreateElement(HStringReference(L"actions").Get(), &actions);
NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
@ -644,6 +663,8 @@ HRESULT
ToastNotificationHandler::OnActivate(
const ComPtr<IToastNotification>& notification,
const ComPtr<IInspectable>& inspectable) {
MOZ_LOG(sWASLog, LogLevel::Info, ("OnActivate"));
if (mAlertListener) {
// Extract the `action` value from the argument string.
nsAutoString actionString;
@ -657,6 +678,10 @@ ToastNotificationHandler::OnActivate(
uint32_t len = 0;
const char16_t* buffer = (char16_t*)arguments.GetRawBuffer(&len);
if (buffer) {
MOZ_LOG(sWASLog, LogLevel::Info,
("OnActivate: arguments: %s",
NS_ConvertUTF16toUTF8(buffer).get()));
// Toast arguments are a newline separated key/value combination of
// launch arguments and an optional action argument provided as an
// argument to the toast's constructor. After the `action` key is
@ -711,51 +736,43 @@ ToastNotificationHandler::OnActivate(
return S_OK;
}
// A single toast message can receive multiple dismiss events, at most one for
// the popup and at most one for the action center. We can't simply count
// dismiss events as the user may have disabled either popups or action center
// notifications, therefore we have to check if the toast remains in the history
// (action center) to determine if the toast is fully dismissed.
static bool NotificationStillPresent(
const ComPtr<IToastNotification>& current_notification, nsString& nsAumid) {
// Returns `nullptr` if no such toast exists.
/* static */ ComPtr<IToastNotification>
ToastNotificationHandler::FindNotificationByTag(const nsAString& aWindowsTag,
const nsAString& nsAumid) {
HRESULT hr = S_OK;
ComPtr<IToastNotification2> current_notification2;
hr = current_notification.As(&current_notification2);
NS_ENSURE_TRUE(SUCCEEDED(hr), false);
HString current_id;
hr = current_notification2->get_Tag(current_id.GetAddressOf());
NS_ENSURE_TRUE(SUCCEEDED(hr), false);
current_id.Set(PromiseFlatString(aWindowsTag).get());
ComPtr<IToastNotificationManagerStatics> manager =
GetToastNotificationManagerStatics();
if (!manager) {
return false;
return nullptr;
}
ComPtr<IToastNotificationManagerStatics2> manager2;
hr = manager.As(&manager2);
NS_ENSURE_TRUE(SUCCEEDED(hr), false);
NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
ComPtr<IToastNotificationHistory> history;
hr = manager2->get_History(&history);
NS_ENSURE_TRUE(SUCCEEDED(hr), false);
NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
ComPtr<IToastNotificationHistory2> history2;
hr = history.As(&history2);
NS_ENSURE_TRUE(SUCCEEDED(hr), false);
NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
HString aumid;
hr = aumid.Set(nsAumid.get());
NS_ENSURE_TRUE(SUCCEEDED(hr), false);
hr = aumid.Set(PromiseFlatString(nsAumid).get());
NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
ComPtr<IVectorView_ToastNotification> toasts;
hr = history2->GetHistoryWithId(aumid.Get(), &toasts);
NS_ENSURE_TRUE(SUCCEEDED(hr), false);
NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
unsigned int hist_size;
hr = toasts->get_Size(&hist_size);
NS_ENSURE_TRUE(SUCCEEDED(hr), false);
NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
for (unsigned int i = 0; i < hist_size; i++) {
ComPtr<IToastNotification> hist_toast;
hr = toasts->GetAt(i, &hist_toast);
@ -765,30 +782,120 @@ static bool NotificationStillPresent(
ComPtr<IToastNotification2> hist_toast2;
hr = hist_toast.As(&hist_toast2);
NS_ENSURE_TRUE(SUCCEEDED(hr), false);
NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
HString history_id;
hr = hist_toast2->get_Tag(history_id.GetAddressOf());
NS_ENSURE_TRUE(SUCCEEDED(hr), false);
NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
// We can not directly compare IToastNotification objects; their IUnknown
// pointers should be equivalent but under inspection were not. Therefore we
// use the notification's tag instead.
if (current_id == history_id) {
return true;
return hist_toast;
}
}
return false;
return nullptr;
}
/* static */ HRESULT ToastNotificationHandler::GetLaunchArgumentValueForKey(
const ComPtr<IToastNotification> toast, const nsAString& key,
nsAString& value) {
ComPtr<IXmlDocument> xml;
HRESULT hr = toast->get_Content(&xml);
NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
ComPtr<IXmlElement> root;
hr = xml->get_DocumentElement(&root);
NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
HString launchHString;
hr = root->GetAttribute(HStringReference(L"launch").Get(),
launchHString.GetAddressOf());
NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
unsigned int len;
const wchar_t* launchPtr = launchHString.GetRawBuffer(&len);
nsAutoString launch(launchPtr, len);
// Toast arguments are a newline separated key/value combination of launch
// arguments and an optional action argument provided as an argument to the
// toast's constructor. After the `action` key is found, the remainder of
// toast argument (including newlines) is the `action` value.
Tokenizer16 parse((char16_t*)launchPtr);
nsDependentSubstring token;
while (parse.ReadUntil(Tokenizer16::Token::NewLine(), token)) {
if (token == u"action"_ns) {
// As soon as we see an action, we're done: we don't want to take a "key"
// from the user-provided action string.
return E_FAIL;
} else if (token.Equals(key)) {
Unused << parse.ReadUntil(Tokenizer16::Token::NewLine(), value);
return S_OK;
} else {
// Next line is a value in a key/value pair, skip.
parse.SkipUntil(Tokenizer16::Token::NewLine());
// Skip newline.
Tokenizer16::Token unused;
Unused << parse.Next(unused);
}
}
return E_FAIL;
}
/* static */ nsresult ToastNotificationHandler::FindLaunchURLForWindowsTag(
const nsAString& aWindowsTag, const nsAString& aAumid,
nsAString& aLaunchUrl) {
aLaunchUrl.Truncate();
ComPtr<IToastNotification> toast =
ToastNotificationHandler::FindNotificationByTag(aWindowsTag, aAumid);
MOZ_LOG(sWASLog, LogLevel::Debug, ("Found toast [%p]", toast.Get()));
if (!toast) {
return NS_ERROR_FAILURE;
}
HRESULT hr = ToastNotificationHandler::GetLaunchArgumentValueForKey(
toast, u"launchUrl"_ns, aLaunchUrl);
if (!SUCCEEDED(hr)) {
MOZ_LOG(sWASLog, LogLevel::Debug,
("Did not find launchUrl [hr=0x%08lX]", hr));
return NS_ERROR_FAILURE;
}
MOZ_LOG(sWASLog, LogLevel::Debug,
("Found launchUrl [%s]", NS_ConvertUTF16toUTF8(aLaunchUrl).get()));
return NS_OK;
}
// A single toast message can receive multiple dismiss events, at most one for
// the popup and at most one for the action center. We can't simply count
// dismiss events as the user may have disabled either popups or action center
// notifications, therefore we have to check if the toast remains in the history
// (action center) to determine if the toast is fully dismissed.
HRESULT
ToastNotificationHandler::OnDismiss(
const ComPtr<IToastNotification>& notification,
const ComPtr<IToastDismissedEventArgs>& aArgs) {
// Don't dismiss notifications when they are still in the action center. We
// can receive multiple dismiss events.
if (NotificationStillPresent(notification, mAumid)) {
ComPtr<IToastNotification2> notification2;
HRESULT hr = notification.As(&notification2);
NS_ENSURE_TRUE(SUCCEEDED(hr), E_FAIL);
HString tagHString;
hr = notification2->get_Tag(tagHString.GetAddressOf());
NS_ENSURE_TRUE(SUCCEEDED(hr), E_FAIL);
unsigned int len;
const wchar_t* tagPtr = tagHString.GetRawBuffer(&len);
nsAutoString tag(tagPtr, len);
if (FindNotificationByTag(tag, mAumid)) {
return S_OK;
}

Просмотреть файл

@ -58,6 +58,14 @@ class ToastNotificationHandler final
nsresult CreateToastXmlString(const nsAString& aImageURL, nsAString& aString);
nsresult GetWindowsTag(nsAString& aWindowsTag);
nsresult SetWindowsTag(const nsAString& aWindowsTag);
// Exposed for consumption by `ToastNotification.cpp`.
static nsresult FindLaunchURLForWindowsTag(const nsAString& aWindowsTag,
const nsAString& aAumid,
nsAString& aLaunchUrl);
protected:
virtual ~ToastNotificationHandler();
@ -121,6 +129,12 @@ class ToastNotificationHandler final
const ComPtr<IToastDismissedEventArgs>& aArgs);
HRESULT OnFail(const ComPtr<IToastNotification>& notification,
const ComPtr<IToastFailedEventArgs>& aArgs);
static HRESULT GetLaunchArgumentValueForKey(
const ComPtr<IToastNotification> toast, const nsAString& key,
nsAString& value);
static ComPtr<IToastNotification> FindNotificationByTag(
const nsAString& aWindowsTag, const nsAString& nsAumid);
};
} // namespace widget

Просмотреть файл

@ -71,6 +71,9 @@ function testAlert(when, { serverEnabled, profD, isBackgroundTaskMode } = {}) {
if (serverEnabled && launchUrl) {
s += `&#xA;launchUrl&#xA;${launchUrl}`;
}
if (serverEnabled) {
s += "&#xA;windowsTag&#xA;";
}
if (argument) {
s += `&#xA;action&#xA;${argument}`;
}
@ -205,7 +208,10 @@ function testAlert(when, { serverEnabled, profD, isBackgroundTaskMode } = {}) {
"settings",
launchUrl
)}" placement="contextmenu"/>`;
expected = `<toast launch="${argumentString(null, launchUrl)}"><visual><binding template="ToastImageAndText03"><image id="1" src="file:///image.png"/><text id="1">title</text><text id="2">text</text></binding></visual><actions>${settingsActionWithLaunchUrl}</actions></toast>`;
expected = `<toast launch="${argumentString(
null,
launchUrl
)}"><visual><binding template="ToastImageAndText03"><image id="1" src="file:///image.png"/><text id="1">title</text><text id="2">text</text></binding></visual><actions>${settingsActionWithLaunchUrl}</actions></toast>`;
Assert.equal(
expected.replace("<actions></actions>", "<actions/>"),
alertsService.getXmlStringForWindowsAlert(alert),