Playwright/E2E: Add method to stub notification in Playwright and automate MM-T483 (#29203)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
05abb39019
Коммит
80289a0d8c
@@ -59,6 +59,7 @@ export default defineConfig({
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
...devices['iPad Pro 11'],
|
||||
permissions: ['notifications'],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
90
e2e-tests/playwright/support/mock_browser_api.ts
Обычный файл
90
e2e-tests/playwright/support/mock_browser_api.ts
Обычный файл
@@ -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();
|
||||
});
|
||||
Ссылка в новой задаче
Block a user