Playwright/E2E: Add method to stub notification in Playwright and automate MM-T483 (#29203)

Этот коммит содержится в:
Saturnino Abril
2024-11-19 22:16:33 +08:00
коммит произвёл GitHub
родитель 05abb39019
Коммит 80289a0d8c
6 изменённых файлов: 176 добавлений и 2 удалений

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

@@ -0,0 +1,90 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Page} from '@playwright/test';
type NotificationData = {title: string} & NotificationOptions;
// Extend the Window interface to add custom properties
declare global {
interface Window {
_originalNotification: typeof Notification;
_notifications: NotificationData[];
getNotifications: () => NotificationData[];
}
}
/**
* `stubNotification` intercepts the Notification API to capture notifications.
*
* Note:
* - Works across browsers and devices, except in headless mode, where stubbing the Notification API is supported only in Firefox and WebKit.
* - An `Error: page.evaluate: window.getNotifications is not a function` may occur if the `stubNotification` function is called before the page has fully loaded.
*
* @param page Page object
* @param permission Permission setting for notifications, with possible values: "default" | "granted" | "denied". Note: A notification sound may still occur even when set to "denied", as the browser might attempt to trigger system notifications.
*/
export async function stubNotification(page: Page, permission: NotificationPermission) {
await page.evaluate((notificationPermission: NotificationPermission) => {
// Override the Notification.requestPermission method
window.Notification.requestPermission = () => Promise.resolve(permission);
// Copy the original Notification
if (!window._originalNotification) {
window._originalNotification = window.Notification;
}
// Initialize a list where to capture the notifications
window._notifications = [];
// Override the Notification constructor
class CustomNotification extends window._originalNotification {
constructor(title: string, options?: NotificationOptions) {
super(title, options);
const notification = {title, ...options};
window._notifications.push(notification);
}
}
// Set static properties and permission status
Object.defineProperties(CustomNotification, {
permission: {
get: () => notificationPermission,
},
requestPermission: {
value: () => Promise.resolve(notificationPermission),
},
});
// Replace the global Notification with the custom one
window.Notification = CustomNotification as unknown as typeof Notification;
// Method to get all notifications
window.getNotifications = () => window._notifications;
}, permission);
}
/**
* `waitForNotification` waits for a specified number of notifications to be received on the page within a given timeout.
* @param page Page object
* @param expectedCount Number of notifications to wait for before returning. (default: 1)
* @param timeout Wait time in milliseconds. (default: 5000ms)
* @returns An array of notifications received
*/
export async function waitForNotification(
page: Page,
expectedCount = 1,
timeout: number = 5000,
): Promise<NotificationData[]> {
const start = Date.now();
while (Date.now() - start < timeout) {
const notifications = await page.evaluate(() => window.getNotifications());
if (notifications.length >= expectedCount) {
return notifications;
}
await new Promise((resolve) => setTimeout(resolve, 100));
}
// eslint-disable-next-line no-console
console.error(`Notification not received within the timeout period of ${timeout}ms`);
return [];
}

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

@@ -8,6 +8,7 @@ import {initSetup, getAdminClient} from './server';
import {hideDynamicChannelsContent, waitForAnimationEnd, waitUntil} from './test_action';
import {pages} from './ui/pages';
import {matchSnapshot} from './visual';
import {stubNotification, waitForNotification} from './mock_browser_api';
export {expect} from '@playwright/test';
@@ -41,7 +42,7 @@ export const test = base.extend<ExtendedFixtures>({
class PlaywrightExtended {
// ./browser_context
readonly testBrowser: TestBrowser;
readonly testBrowser;
// ./flag
readonly shouldHaveCallsEnabled;
@@ -65,6 +66,10 @@ class PlaywrightExtended {
// ./visual
readonly matchSnapshot;
// ./mock_browser_api
readonly stubNotification;
readonly waitForNotification;
constructor(browser: Browser) {
// ./browser_context
this.testBrowser = new TestBrowser(browser);
@@ -90,6 +95,10 @@ class PlaywrightExtended {
// ./visual
this.matchSnapshot = matchSnapshot;
// ./mock_browser_api
this.stubNotification = stubNotification;
this.waitForNotification = waitForNotification;
}
}

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

@@ -63,6 +63,14 @@ export default class ChannelsPost {
await this.removePostButton.waitFor();
await this.removePostButton.click();
}
/**
* `toContainText` verifies if the post contains the specified text.
* @param text Text to be verified in the post
*/
async toContainText(text: string) {
await expect(this.container).toContainText(text);
}
}
export {ChannelsPost};

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

@@ -69,12 +69,20 @@ export default class ChannelsPage {
if (teamName) {
channelsUrl += `${teamName}`;
if (channelName) {
const prefix = channelName.startsWith('@') ? '/messages' : '';
const prefix = channelName.startsWith('@') ? '/messages' : '/channels';
channelsUrl += `${prefix}/${channelName}`;
}
}
await this.page.goto(channelsUrl);
}
/**
* `postMessage` posts a message in the current channel
* @param message Message to post
*/
async postMessage(message: string) {
await this.centerView.postCreate.postMessage(message);
}
}
export {ChannelsPage};