forked from mirrors/gecko-dev
567 lines
18 KiB
JavaScript
567 lines
18 KiB
JavaScript
/* Any copyright is dedicated to the Public Domain.
|
|
http://creativecommons.org/publicdomain/zero/1.0/ */
|
|
|
|
"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]],
|
|
});
|
|
});
|
|
|
|
/**
|
|
* 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.
|
|
* See: https://searchfox.org/mozilla-central/rev/f32d5f3949a3f4f185122142b29f2e3ab776836e/toolkit/modules/PopupNotifications.sys.mjs#1870-1872
|
|
*
|
|
* 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.
|
|
*/
|
|
async function ensureSecurityDelayReady() {
|
|
await TestUtils.waitForCondition(
|
|
() => performance.now() > TEST_SECURITY_DELAY,
|
|
"Wait for performance.now() > SECURITY_DELAY",
|
|
500,
|
|
50
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Test helper for security delay tests which performs the following steps:
|
|
* 1. Shows a test notification.
|
|
* 2. Waits for the notification panel to be shown and checks that the initial
|
|
* security delay blocks clicks.
|
|
* 3. Waits for the security delay to expire. Clicks should now be allowed.
|
|
* 4. Executes the provided onSecurityDelayExpired function. This function
|
|
* should renew the security delay.
|
|
* 5. Tests that the security delay was renewed.
|
|
* 6. Ensures that after the security delay the notification can be closed.
|
|
*
|
|
* @param {*} options
|
|
* @param {function} options.onSecurityDelayExpired - Function to run after the
|
|
* security delay has expired. This function should trigger a renew of the
|
|
* security delay.
|
|
* @param {function} options.cleanupFn - Optional cleanup function to run after
|
|
* the test has completed.
|
|
* @returns {Promise<void>} - Resolves when the test has completed.
|
|
*/
|
|
async function runPopupNotificationSecurityDelayTest({
|
|
onSecurityDelayExpired,
|
|
cleanupFn = () => {},
|
|
}) {
|
|
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 initial 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) {
|
|
await cleanupFn();
|
|
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("Run test specific actions which restarts the security delay.");
|
|
await onSecurityDelayExpired();
|
|
|
|
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) {
|
|
await cleanupFn();
|
|
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 no longer see the notification."
|
|
);
|
|
|
|
info("Cleanup.");
|
|
await cleanupFn();
|
|
}
|
|
|
|
/**
|
|
* 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 no 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 runPopupNotificationSecurityDelayTest({
|
|
onSecurityDelayExpired: async () => {
|
|
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;
|
|
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."
|
|
);
|
|
let notification = PopupNotifications.getNotification(
|
|
"foo",
|
|
gBrowser.selectedBrowser
|
|
);
|
|
is(
|
|
notification?.id,
|
|
"foo",
|
|
"There should still be a notification with id foo"
|
|
);
|
|
|
|
info(
|
|
"Because we re-show the panel after tab close / switch the security delay should have reset."
|
|
);
|
|
},
|
|
});
|
|
});
|
|
|
|
/**
|
|
* 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() {
|
|
let screenX, screenY;
|
|
|
|
await runPopupNotificationSecurityDelayTest({
|
|
onSecurityDelayExpired: async () => {
|
|
info("Reposition the window");
|
|
// Remember original window position.
|
|
screenX = window.screenX;
|
|
screenY = window.screenY;
|
|
|
|
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));
|
|
},
|
|
cleanupFn: async () => {
|
|
// 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");
|
|
|
|
await SpecialPowers.popPrefEnv();
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Tests that the security delay gets extended when pointer lock is entered.
|
|
*/
|
|
add_task(async function test_notificationPointerLock() {
|
|
// We need a tab to enter pointer lock.
|
|
let tab = await BrowserTestUtils.openNewForegroundTab(
|
|
gBrowser,
|
|
"https://example.com"
|
|
);
|
|
|
|
await runPopupNotificationSecurityDelayTest({
|
|
onSecurityDelayExpired: async () => {
|
|
info("Enter pointer lock");
|
|
// Move focus to the browser to ensure pointer lock request succeeds.
|
|
gBrowser.selectedBrowser.focus();
|
|
let pointerLockEnterPromise = TestUtils.topicObserved(
|
|
"pointer-lock-entered"
|
|
);
|
|
await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
|
|
SpecialPowers.wrap(content.document).notifyUserGestureActivation();
|
|
await content.document.body.requestPointerLock();
|
|
});
|
|
|
|
// Wait for pointer lock to be entered and the PopupNotifications listener to run.
|
|
await pointerLockEnterPromise;
|
|
await new Promise(resolve => setTimeout(resolve, 0));
|
|
},
|
|
cleanupFn: async () => {
|
|
// Cleanup.
|
|
await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
|
|
SpecialPowers.wrap(content.document).notifyUserGestureActivation();
|
|
await content.document.exitPointerLock();
|
|
});
|
|
await TestUtils.waitForCondition(
|
|
() => !window.PointerLock.isActive,
|
|
"Wait for pointer lock exit."
|
|
);
|
|
BrowserTestUtils.removeTab(tab);
|
|
},
|
|
});
|
|
});
|