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 удалений

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

@@ -59,6 +59,7 @@ export default defineConfig({
use: {
browserName: 'chromium',
...devices['iPad Pro 11'],
permissions: ['notifications'],
},
},
{

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

@@ -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};

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

@@ -0,0 +1,58 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, test} from '@e2e-support/test_fixture';
test('MM-T483 Channel-wide mentions with uppercase letters', async ({pw, pages, headless, browserName}) => {
test.skip(
headless && browserName !== 'firefox',
'Works across browsers and devices, except in headless mode, where stubbing the Notification API is supported only in Firefox and WebKit.',
);
// Initialize setup and get the required users and team
const {team, adminUser, user} = await pw.initSetup();
// Log in as the admin in one browser session and navigate to the "town-square" channel
const {page: adminPage} = await pw.testBrowser.login(adminUser);
const adminChannelPage = new pages.ChannelsPage(adminPage);
await adminChannelPage.goto(team.name, 'town-square');
await adminChannelPage.toBeVisible();
// Stub the Notification in the admin's browser to capture notifications
await pw.stubNotification(adminPage, 'granted');
// Log in as the regular user in a separate browser and navigate to the "off-topic" channel
const {page: otherPage} = await pw.testBrowser.login(user);
const otherChannelPage = new pages.ChannelsPage(otherPage);
await otherChannelPage.goto(team.name, 'off-topic');
await otherChannelPage.toBeVisible();
// Post a channel-wide mention message "@ALL" in uppercase from the user's browser
const message = `@ALL good morning, ${team.name}!`;
await otherChannelPage.postMessage(message);
// Wait for a notification to be received in the admin's browser and verify its content
const notifications = await pw.waitForNotification(adminPage);
expect(notifications.length).toBe(1);
const notification = notifications[0];
expect(notification.title).toBe('Off-Topic');
expect(notification.body).toBe(`@${user.username}: ${message}`);
expect(notification.tag).toBe(`@${user.username}: ${message}`);
expect(notification.icon).toContain('.png');
expect(notification.requireInteraction).toBe(false);
expect(notification.silent).toBe(false);
// Verify the last post as viewed by the regular user in the "off-topic" channel contains the message and is highlighted
const otherLastPost = await otherChannelPage.centerView.getLastPost();
await otherLastPost.toContainText(message);
await expect(otherLastPost.container.locator('.mention--highlight')).toBeVisible();
await expect(otherLastPost.container.locator('.mention--highlight').getByText('@ALL')).toBeVisible();
// Admin navigates to the "off-topic" channel and verifies the message is posted and highlighted correctly
await adminChannelPage.goto(team.name, 'off-topic');
const adminLastPost = await adminChannelPage.centerView.getLastPost();
await adminLastPost.toContainText(message);
await expect(adminLastPost.container.locator('.mention--highlight')).toBeVisible();
await expect(adminLastPost.container.locator('.mention--highlight').getByText('@ALL')).toBeVisible();
});