Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Errors

/* Any copyright is dedicated to the Public Domain.
"use strict";
const TEST_SECURITY_DELAY = 5000;
SimpleTest.requestCompleteLog();
/**
* Shows a test PopupNotification.
*/
function showNotification() {
PopupNotifications.show(
gBrowser.selectedBrowser,
"foo",
"Hello, World!",
"default-notification-icon",
{
label: "ok",
accessKey: "o",
callback: () => {},
},
[
{
label: "cancel",
accessKey: "c",
callback: () => {},
},
],
{
// Make test notifications persistent to ensure they are only closed
// explicitly by test actions and survive tab switches.
persistent: true,
}
);
}
add_setup(async function () {
// Set a longer security delay for PopupNotification actions so we can test
// the delay even if the test runs slowly.
await SpecialPowers.pushPrefEnv({
set: [["security.notification_enable_delay", TEST_SECURITY_DELAY]],
});
});
async function ensureSecurityDelayReady() {
/**
* The security delay calculation in PopupNotification.sys.mjs is dependent on
* the monotonically increasing value of performance.now. This timestamp is
* not relative to a fixed date, but to runtime.
* We need to wait for the value performance.now() to be larger than the
* security delay in order to observe the bug. Only then does the
* timeSinceShown check in PopupNotifications.sys.mjs lead to a timeSinceShown
* value that is unconditionally greater than lazy.buttonDelay for
* notification.timeShown = null = 0.
*
* When running in automation as part of a larger test suite performance.now()
* should usually be already sufficiently high in which case this check should
* directly resolve.
*/
await TestUtils.waitForCondition(
() => performance.now() > TEST_SECURITY_DELAY,
"Wait for performance.now() > SECURITY_DELAY",
500,
50
);
}
/**
* Tests that when we show a second notification while the panel is open the
* timeShown attribute is correctly set and the security delay is enforced
* properly.
*/
add_task(async function test_timeShownMultipleNotifications() {
await ensureSecurityDelayReady();
ok(
!PopupNotifications.isPanelOpen,
"PopupNotification panel should not be open initially."
);
info("Open the first notification.");
let popupShownPromise = waitForNotificationPanel();
showNotification();
await popupShownPromise;
ok(
PopupNotifications.isPanelOpen,
"PopupNotification should be open after first show call."
);
is(
PopupNotifications._currentNotifications.length,
1,
"There should only be one notification"
);
let notification = PopupNotifications.getNotification(
"foo",
gBrowser.selectedBrowser
);
is(notification?.id, "foo", "There should be a notification with id foo");
ok(notification.timeShown, "The notification should have timeShown set");
info(
"Call show again with the same notification id while the PopupNotification panel is still open."
);
showNotification();
ok(
PopupNotifications.isPanelOpen,
"PopupNotification should still open after second show call."
);
notification = PopupNotifications.getNotification(
"foo",
gBrowser.selectedBrowser
);
is(
PopupNotifications._currentNotifications.length,
1,
"There should still only be one notification"
);
is(
notification?.id,
"foo",
"There should still be a notification with id foo"
);
ok(notification.timeShown, "The notification should have timeShown set");
let notificationHiddenPromise = waitForNotificationPanelHidden();
info("Trigger main action via button click during security delay");
// Wait for a tick of the event loop to ensure the button we're clicking
// has been slotted into moz-button-group
await new Promise(resolve => setTimeout(resolve, 0));
triggerMainCommand(PopupNotifications.panel);
await new Promise(resolve => setTimeout(resolve, 0));
ok(PopupNotifications.isPanelOpen, "PopupNotification should still be open.");
notification = PopupNotifications.getNotification(
"foo",
gBrowser.selectedBrowser
);
ok(
notification,
"Notification should still be open because we clicked during the security delay."
);
// If the notification is no longer shown (test failure) skip the remaining
// checks.
if (!notification) {
return;
}
// Ensure that once the security delay has passed the notification can be
// closed again.
let fakeTimeShown = TEST_SECURITY_DELAY + 500;
info(`Manually set timeShown to ${fakeTimeShown}ms in the past.`);
notification.timeShown = performance.now() - fakeTimeShown;
info("Trigger main action via button click outside security delay");
triggerMainCommand(PopupNotifications.panel);
info("Wait for panel to be hidden.");
await notificationHiddenPromise;
ok(
!PopupNotifications.getNotification("foo", gBrowser.selectedBrowser),
"Should not longer see the notification."
);
});
/**
* Tests that when we reshow a notification after a tab switch the timeShown
* attribute is correctly reset and the security delay is enforced.
*/
add_task(async function test_notificationReshowTabSwitch() {
await ensureSecurityDelayReady();
ok(
!PopupNotifications.isPanelOpen,
"PopupNotification panel should not be open initially."
);
info("Open the first notification.");
let popupShownPromise = waitForNotificationPanel();
showNotification();
await popupShownPromise;
ok(
PopupNotifications.isPanelOpen,
"PopupNotification should be open after first show call."
);
let notification = PopupNotifications.getNotification(
"foo",
gBrowser.selectedBrowser
);
is(notification?.id, "foo", "There should be a notification with id foo");
ok(notification.timeShown, "The notification should have timeShown set");
info("Trigger main action via button click during security delay");
triggerMainCommand(PopupNotifications.panel);
await new Promise(resolve => setTimeout(resolve, 0));
ok(PopupNotifications.isPanelOpen, "PopupNotification should still be open.");
notification = PopupNotifications.getNotification(
"foo",
gBrowser.selectedBrowser
);
ok(
notification,
"Notification should still be open because we clicked during the security delay."
);
// If the notification is no longer shown (test failure) skip the remaining
// checks.
if (!notification) {
return;
}
let panelHiddenPromise = waitForNotificationPanelHidden();
let panelShownPromise;
info("Open a new tab which hides the notification panel.");
await BrowserTestUtils.withNewTab("https://example.com", async () => {
info("Wait for panel to be hidden by tab switch.");
await panelHiddenPromise;
info(
"Keep the tab open until the security delay for the original notification show has expired."
);
await new Promise(resolve =>
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
setTimeout(resolve, TEST_SECURITY_DELAY + 500)
);
panelShownPromise = waitForNotificationPanel();
});
info(
"Wait for the panel to show again after the tab close. We're showing the original tab again."
);
await panelShownPromise;
ok(
PopupNotifications.isPanelOpen,
"PopupNotification should be shown after tab close."
);
notification = PopupNotifications.getNotification(
"foo",
gBrowser.selectedBrowser
);
is(
notification?.id,
"foo",
"There should still be a notification with id foo"
);
let notificationHiddenPromise = waitForNotificationPanelHidden();
info(
"Because we re-show the panel after tab close / switch the security delay should have reset."
);
info("Trigger main action via button click during the new security delay.");
triggerMainCommand(PopupNotifications.panel);
await new Promise(resolve => setTimeout(resolve, 0));
ok(PopupNotifications.isPanelOpen, "PopupNotification should still be open.");
notification = PopupNotifications.getNotification(
"foo",
gBrowser.selectedBrowser
);
ok(
notification,
"Notification should still be open because we clicked during the security delay."
);
// If the notification is no longer shown (test failure) skip the remaining
// checks.
if (!notification) {
return;
}
// Ensure that once the security delay has passed the notification can be
// closed again.
let fakeTimeShown = TEST_SECURITY_DELAY + 500;
info(`Manually set timeShown to ${fakeTimeShown}ms in the past.`);
notification.timeShown = performance.now() - fakeTimeShown;
info("Trigger main action via button click outside security delay");
triggerMainCommand(PopupNotifications.panel);
info("Wait for panel to be hidden.");
await notificationHiddenPromise;
ok(
!PopupNotifications.getNotification("foo", gBrowser.selectedBrowser),
"Should not longer see the notification."
);
});
/**
* Tests that the security delay gets reset when a window is repositioned and
* the PopupNotifications panel position is updated.
*/
add_task(async function test_notificationWindowMove() {
await ensureSecurityDelayReady();
info("Open a notification.");
let popupShownPromise = waitForNotificationPanel();
showNotification();
await popupShownPromise;
ok(
PopupNotifications.isPanelOpen,
"PopupNotification should be open after show call."
);
// Test that the initial security delay works.
info("Trigger main action via button click during the new security delay.");
triggerMainCommand(PopupNotifications.panel);
await new Promise(resolve => setTimeout(resolve, 0));
ok(PopupNotifications.isPanelOpen, "PopupNotification should still be open.");
let notification = PopupNotifications.getNotification(
"foo",
gBrowser.selectedBrowser
);
ok(
notification,
"Notification should still be open because we clicked during the security delay."
);
// If the notification is no longer shown (test failure) skip the remaining
// checks.
if (!notification) {
return;
}
info("Wait for security delay to expire.");
await new Promise(resolve =>
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
setTimeout(resolve, TEST_SECURITY_DELAY + 500)
);
info("Reposition the window");
// Remember original window position.
let { screenX, screenY } = window;
let promisePopupPositioned = BrowserTestUtils.waitForEvent(
PopupNotifications.panel,
"popuppositioned"
);
// Move the window.
window.moveTo(200, 200);
// Wait for the panel to reposition and the PopupNotifications listener to run.
await promisePopupPositioned;
await new Promise(resolve => setTimeout(resolve, 0));
info("Trigger main action via button click during the new security delay.");
triggerMainCommand(PopupNotifications.panel);
await new Promise(resolve => setTimeout(resolve, 0));
ok(PopupNotifications.isPanelOpen, "PopupNotification should still be open.");
notification = PopupNotifications.getNotification(
"foo",
gBrowser.selectedBrowser
);
ok(
notification,
"Notification should still be open because we clicked during the security delay."
);
// If the notification is no longer shown (test failure) skip the remaining
// checks.
if (!notification) {
return;
}
// Ensure that once the security delay has passed the notification can be
// closed again.
let fakeTimeShown = TEST_SECURITY_DELAY + 500;
info(`Manually set timeShown to ${fakeTimeShown}ms in the past.`);
notification.timeShown = performance.now() - fakeTimeShown;
info("Trigger main action via button click outside security delay");
let notificationHiddenPromise = waitForNotificationPanelHidden();
triggerMainCommand(PopupNotifications.panel);
info("Wait for panel to be hidden.");
await notificationHiddenPromise;
ok(
!PopupNotifications.getNotification("foo", gBrowser.selectedBrowser),
"Should not longer see the notification."
);
// Reset window position
window.moveTo(screenX, screenY);
});
/**
* Tests that the security delay gets extended if a notification is shown during
* a full screen transition.
*/
add_task(async function test_notificationDuringFullScreenTransition() {
// Log full screen transition messages.
let loggingObserver = {
observe(subject, topic) {
info("Observed topic: " + topic);
},
};
Services.obs.addObserver(loggingObserver, "fullscreen-transition-start");
Services.obs.addObserver(loggingObserver, "fullscreen-transition-end");
// Unregister observers when the test ends:
registerCleanupFunction(() => {
Services.obs.removeObserver(loggingObserver, "fullscreen-transition-start");
Services.obs.removeObserver(loggingObserver, "fullscreen-transition-end");
});
if (Services.appinfo.OS == "Linux") {
ok(
"Skipping test on Linux because of disabled full screen transition in CI."
);
return;
}
// Bug 1882527: Intermittent failures on macOS.
if (Services.appinfo.OS == "Darwin") {
ok("Skipping test on macOS because of intermittent failures.");
return;
}
await BrowserTestUtils.withNewTab("https://example.com", async browser => {
await SpecialPowers.pushPrefEnv({
set: [
// Set a short security delay so we can observe it being extended.
["security.notification_enable_delay", 1],
// Set a longer full screen exit transition so the test works on slow builds.
["full-screen-api.transition-duration.leave", "1000 1000"],
// Waive the user activation requirement for full screen requests.
// The PoC this test is based on relies on spam clicking which grants
// user activation in the popup that requests full screen.
// This isn't reliable in automation.
["full-screen-api.allow-trusted-requests-only", false],
// macOS native full screen is not affected by the full screen
// transition overlap. Test with the old full screen implementation.
["full-screen-api.macos-native-full-screen", false],
],
});
await ensureSecurityDelayReady();
ok(
!PopupNotifications.isPanelOpen,
"PopupNotification panel should not be open initially."
);
info("Open a notification.");
let popupShownPromise = waitForNotificationPanel();
showNotification();
await popupShownPromise;
ok(
PopupNotifications.isPanelOpen,
"PopupNotification should be open after show call."
);
let notification = PopupNotifications.getNotification("foo", browser);
is(notification?.id, "foo", "There should be a notification with id foo");
info(
"Open a new tab via window.open, enter full screen and remove the tab."
);
// There are two transitions, one for full screen entry and one for full screen exit.
let transitionStartCount = 0;
let transitionEndCount = 0;
let promiseFullScreenTransitionStart = TestUtils.topicObserved(
"fullscreen-transition-start",
() => {
transitionStartCount++;
return transitionStartCount == 2;
}
);
let promiseFullScreenTransitionEnd = TestUtils.topicObserved(
"fullscreen-transition-end",
() => {
transitionEndCount++;
return transitionEndCount == 2;
}
);
let notificationShownPromise = waitForNotificationPanel();
await SpecialPowers.spawn(browser, [], () => {
// Use eval to execute in the privilege context of the website.
content.eval(`
let button = document.createElement("button");
button.id = "triggerBtn";
button.innerText = "Open Popup";
button.addEventListener("click", () => {
let popup = window.open("about:blank");
popup.document.write(
"<script>setTimeout(() => document.documentElement.requestFullscreen(), 500)</script>"
);
popup.document.write(
"<script>setTimeout(() => window.close(), 1500)</script>"
);
});
// Insert button at the top so the synthesized click works. Otherwise
// the button may be outside of the viewport.
document.body.prepend(button);
`);
});
let timeClick = performance.now();
await BrowserTestUtils.synthesizeMouseAtCenter("#triggerBtn", {}, browser);
info("Wait for the exit transition to start. It's the second transition.");
await promiseFullScreenTransitionStart;
info("Full screen transition start");
ok(true, "Full screen transition started");
ok(
window.isInFullScreenTransition,
"Full screen transition is still running."
);
info(
"Wait for notification to re-show on tab switch, after the popup has been closed"
);
await notificationShownPromise;
ok(
window.isInFullScreenTransition,
"Full screen transition is still running."
);
info(
"about to trigger notification. time between btn click and notification show: " +
(performance.now() - timeClick)
);
info(
"Trigger main action via button click during the extended security delay."
);
triggerMainCommand(PopupNotifications.panel);
await new Promise(resolve => setTimeout(resolve, 0));
ok(
PopupNotifications.isPanelOpen,
"PopupNotification should still be open."
);
notification = PopupNotifications.getNotification(
"foo",
gBrowser.selectedBrowser
);
ok(
notification,
"Notification should still be open because we clicked during the security delay."
);
info("Wait for full screen transition end.");
await promiseFullScreenTransitionEnd;
info("Full screen transition end");
});
});