E2E/Playwright: Instantiate page in pw (#29765)
* instantiate page in pw * update spec * fix storage key and require user when logging in via API --------- Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
20e9c6aa3e
Коммит
90e3d2833e
@@ -7,6 +7,7 @@ import {request, Browser, BrowserContext} from '@playwright/test';
|
||||
|
||||
import {UserProfile} from '@mattermost/types/users';
|
||||
import testConfig from '@e2e-test.config';
|
||||
import pages from '@e2e-support/ui/pages';
|
||||
|
||||
export class TestBrowser {
|
||||
readonly browser: Browser;
|
||||
@@ -17,7 +18,7 @@ export class TestBrowser {
|
||||
this.context = null;
|
||||
}
|
||||
|
||||
async login(user: UserProfile | null) {
|
||||
async login(user: UserProfile) {
|
||||
const options = {storageState: ''};
|
||||
if (user) {
|
||||
// Log in via API request and save user storage
|
||||
@@ -29,9 +30,14 @@ export class TestBrowser {
|
||||
const context = await this.browser.newContext(options);
|
||||
const page = await context.newPage();
|
||||
|
||||
const channelsPage = new pages.ChannelsPage(page);
|
||||
const systemConsolePage = new pages.SystemConsolePage(page);
|
||||
const scheduledDraftPage = new pages.ScheduledDraftPage(page);
|
||||
const draftPage = new pages.DraftPage(page);
|
||||
|
||||
this.context = context;
|
||||
|
||||
return {context, page};
|
||||
return {context, page, channelsPage, systemConsolePage, scheduledDraftPage, draftPage};
|
||||
}
|
||||
|
||||
async close() {
|
||||
|
||||
@@ -20,6 +20,9 @@ export async function initSetup({
|
||||
try {
|
||||
// Login the admin user via API
|
||||
const {adminClient, adminUser} = await getAdminClient();
|
||||
if (!adminUser) {
|
||||
throw new Error('Failed to setup admin: Admin user not found.');
|
||||
}
|
||||
if (!adminClient) {
|
||||
throw new Error(
|
||||
"Failed to setup admin: Check that you're able to access the server using the same admin credential.",
|
||||
|
||||
@@ -6,16 +6,16 @@ import {TestBrowser} from './browser_context';
|
||||
import {shouldHaveCallsEnabled, shouldHaveFeatureFlag, shouldRunInLinux, ensureLicense, skipIfNoLicense} from './flag';
|
||||
import {initSetup, getAdminClient} from './server';
|
||||
import {hideDynamicChannelsContent, waitForAnimationEnd, waitUntil} from './test_action';
|
||||
import {pages} from './ui/pages';
|
||||
import pages from './ui/pages';
|
||||
import {matchSnapshot} from './visual';
|
||||
import {stubNotification, waitForNotification} from './mock_browser_api';
|
||||
import {duration} from './util';
|
||||
|
||||
export {expect} from '@playwright/test';
|
||||
|
||||
type ExtendedFixtures = {
|
||||
axe: AxeBuilderExtended;
|
||||
pw: PlaywrightExtended;
|
||||
pages: typeof pages;
|
||||
};
|
||||
|
||||
type AxeBuilderOptions = {
|
||||
@@ -29,15 +29,11 @@ export const test = base.extend<ExtendedFixtures>({
|
||||
const ab = new AxeBuilderExtended();
|
||||
await use(ab);
|
||||
},
|
||||
pw: async ({browser}, use) => {
|
||||
const pw = new PlaywrightExtended(browser);
|
||||
pw: async ({browser, page, isMobile}, use) => {
|
||||
const pw = new PlaywrightExtended(browser, page, isMobile);
|
||||
await use(pw);
|
||||
await pw.testBrowser.close();
|
||||
},
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
pages: async ({}, use) => {
|
||||
await use(pages);
|
||||
},
|
||||
});
|
||||
|
||||
class PlaywrightExtended {
|
||||
@@ -60,8 +56,11 @@ class PlaywrightExtended {
|
||||
readonly waitForAnimationEnd;
|
||||
readonly waitUntil;
|
||||
|
||||
// ./ui/pages
|
||||
readonly pages;
|
||||
// unauthenticated page
|
||||
readonly loginPage;
|
||||
readonly landingLoginPage;
|
||||
readonly signupPage;
|
||||
readonly resetPasswordPage;
|
||||
|
||||
// ./visual
|
||||
readonly matchSnapshot;
|
||||
@@ -70,7 +69,9 @@ class PlaywrightExtended {
|
||||
readonly stubNotification;
|
||||
readonly waitForNotification;
|
||||
|
||||
constructor(browser: Browser) {
|
||||
readonly hasSeenLandingPage;
|
||||
|
||||
constructor(browser: Browser, page: Page, isMobile: boolean) {
|
||||
// ./browser_context
|
||||
this.testBrowser = new TestBrowser(browser);
|
||||
|
||||
@@ -90,8 +91,11 @@ class PlaywrightExtended {
|
||||
this.waitForAnimationEnd = waitForAnimationEnd;
|
||||
this.waitUntil = waitUntil;
|
||||
|
||||
// ./ui/pages
|
||||
this.pages = pages;
|
||||
// unauthenticated page
|
||||
this.loginPage = new pages.LoginPage(page);
|
||||
this.landingLoginPage = new pages.LandingLoginPage(page, isMobile);
|
||||
this.signupPage = new pages.SignupPage(page);
|
||||
this.resetPasswordPage = new pages.ResetPasswordPage(page);
|
||||
|
||||
// ./visual
|
||||
this.matchSnapshot = matchSnapshot;
|
||||
@@ -99,6 +103,12 @@ class PlaywrightExtended {
|
||||
// ./mock_browser_api
|
||||
this.stubNotification = stubNotification;
|
||||
this.waitForNotification = waitForNotification;
|
||||
|
||||
this.hasSeenLandingPage = async () => {
|
||||
// Visit the base URL to be able to set the localStorage
|
||||
await page.goto('/');
|
||||
return await waitUntilLocalStorageIsSet(page, '__landingPageSeen__', 'true');
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,3 +152,20 @@ class AxeBuilderExtended {
|
||||
return JSON.stringify(fingerprints, null, 2);
|
||||
}
|
||||
}
|
||||
|
||||
async function waitUntilLocalStorageIsSet(page: Page, key: string, value: string, timeout = duration.ten_sec) {
|
||||
await waitUntil(
|
||||
() =>
|
||||
page.evaluate(
|
||||
({key, value}) => {
|
||||
if (localStorage.getItem(key) === value) {
|
||||
return true;
|
||||
}
|
||||
localStorage.setItem(key, value);
|
||||
return false;
|
||||
},
|
||||
{key, value},
|
||||
),
|
||||
{timeout},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export default class ChannelsCenterView {
|
||||
readonly header;
|
||||
readonly postCreate;
|
||||
readonly scheduledDraftOptions;
|
||||
readonly scheduledDraftChannelInfo;
|
||||
readonly postBoxIndicator;
|
||||
readonly scheduledDraftChannelIcon;
|
||||
readonly scheduledDraftChannelInfoMessage;
|
||||
readonly scheduledDraftChannelInfoMessageText;
|
||||
@@ -27,7 +27,7 @@ export default class ChannelsCenterView {
|
||||
this.scheduledDraftOptions = new components.ChannelsPostCreate(
|
||||
container.locator('#dropdown_send_post_options'),
|
||||
);
|
||||
this.scheduledDraftChannelInfo = container.locator('div.postBoxIndicator');
|
||||
this.postBoxIndicator = container.locator('div.postBoxIndicator');
|
||||
this.scheduledDraftChannelIcon = container.locator('#create_post i.icon-draft-indicator');
|
||||
this.scheduledDraftChannelInfoMessage = container.locator('div.ScheduledPostIndicator span');
|
||||
this.scheduledDraftChannelInfoMessageText = container.locator('span:has-text("Message scheduled for")');
|
||||
@@ -110,7 +110,7 @@ export default class ChannelsCenterView {
|
||||
}
|
||||
|
||||
async verifyscheduledDraftChannelInfo() {
|
||||
await this.scheduledDraftChannelInfo.isVisible();
|
||||
await this.postBoxIndicator.isVisible();
|
||||
await this.scheduledDraftChannelIcon.isVisible();
|
||||
const messageLocator = this.scheduledDraftChannelInfoMessage.first();
|
||||
await expect(messageLocator).toContainText('Message scheduled for');
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, Locator, Page} from '@playwright/test';
|
||||
import {expect, Locator} from '@playwright/test';
|
||||
|
||||
/**
|
||||
* This is the generic confirm modal that is used in the app.
|
||||
@@ -15,11 +15,10 @@ export default class GenericConfirmModal {
|
||||
readonly confirmButton: Locator;
|
||||
readonly cancelButton: Locator;
|
||||
|
||||
constructor(page: Page, id?: string) {
|
||||
const modalId = `#confirmModal ${id}`.trim();
|
||||
this.container = page.locator(modalId);
|
||||
this.confirmButton = page.locator('#confirmModalButton');
|
||||
this.cancelButton = page.locator('#cancelModalButton');
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
this.confirmButton = container.locator('#confirmModalButton');
|
||||
this.cancelButton = container.locator('#cancelModalButton');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
|
||||
@@ -11,7 +11,7 @@ export default class ChannelsSidebarRight {
|
||||
readonly closeButton;
|
||||
readonly postCreate;
|
||||
readonly rhsPostBody;
|
||||
readonly scheduledDraftChannelInfo;
|
||||
readonly postBoxIndicator;
|
||||
readonly scheduledDraftChannelInfoMessage;
|
||||
readonly scheduledDraftSeeAllLink;
|
||||
readonly scheduledDraftChannelInfoMessageText;
|
||||
@@ -19,7 +19,7 @@ export default class ChannelsSidebarRight {
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
this.scheduledDraftChannelInfo = container.locator('div.postBoxIndicator');
|
||||
this.postBoxIndicator = container.locator('div.postBoxIndicator');
|
||||
this.scheduledDraftChannelInfoMessage = container.locator('div.ScheduledPostIndicator span');
|
||||
this.scheduledDraftSeeAllLink = container.locator('a:has-text("See all scheduled messages")');
|
||||
this.scheduledDraftChannelInfoMessageText = container.locator('span:has-text("Message scheduled for")');
|
||||
|
||||
@@ -21,4 +21,5 @@ const pages = {
|
||||
DraftPage,
|
||||
};
|
||||
|
||||
export {pages, ChannelsPage, LandingLoginPage, LoginPage, SignupPage, ScheduledDraftPage, DraftPage};
|
||||
export {ChannelsPage, LandingLoginPage, LoginPage, SignupPage, ScheduledDraftPage, DraftPage};
|
||||
export default pages;
|
||||
|
||||
@@ -3,14 +3,11 @@
|
||||
|
||||
import {expect, Page} from '@playwright/test';
|
||||
|
||||
import {AdminConfig} from '@mattermost/types/config';
|
||||
import {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {components} from '@e2e-support/ui/components';
|
||||
|
||||
export default class LoginPage {
|
||||
readonly adminConfig: AdminConfig;
|
||||
|
||||
readonly page: Page;
|
||||
|
||||
readonly title;
|
||||
@@ -18,6 +15,7 @@ export default class LoginPage {
|
||||
readonly bodyCard;
|
||||
readonly loginInput;
|
||||
readonly loginPlaceholder;
|
||||
readonly loginWithAdLdapPlaceholder;
|
||||
readonly passwordInput;
|
||||
readonly passwordToggleButton;
|
||||
readonly signInButton;
|
||||
@@ -30,19 +28,15 @@ export default class LoginPage {
|
||||
readonly header;
|
||||
readonly footer;
|
||||
|
||||
constructor(page: Page, adminConfig: AdminConfig) {
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.adminConfig = adminConfig;
|
||||
|
||||
const loginInputPlaceholder = adminConfig.LdapSettings.Enable
|
||||
? 'Email, Username or AD/LDAP Username'
|
||||
: 'Email or Username';
|
||||
|
||||
this.title = page.locator('h1:has-text("Log in to your account")');
|
||||
this.subtitle = page.locator('text=Collaborate with your team in real-time');
|
||||
this.bodyCard = page.locator('.login-body-card-content');
|
||||
this.loginInput = page.locator('#input_loginId');
|
||||
this.loginPlaceholder = page.locator(`[placeholder="${loginInputPlaceholder}"]`);
|
||||
this.loginPlaceholder = page.locator(`[placeholder="Email or Username"]`);
|
||||
this.loginWithAdLdapPlaceholder = page.locator(`[placeholder="Email, Username or AD/LDAP Username"]`);
|
||||
this.passwordInput = page.locator('#input_password-input');
|
||||
this.passwordToggleButton = page.getByRole('button', {name: 'Show or hide password'});
|
||||
this.signInButton = page.locator('button:has-text("Log in")');
|
||||
|
||||
@@ -20,6 +20,10 @@ class SystemConsolePage {
|
||||
readonly systemUsersDateRangeMenu;
|
||||
readonly systemUsersColumnToggleMenu;
|
||||
readonly systemUsersActionMenus;
|
||||
|
||||
// modal
|
||||
readonly confirmModal;
|
||||
readonly exportModal;
|
||||
readonly saveChangesModal;
|
||||
|
||||
constructor(page: Page) {
|
||||
@@ -47,6 +51,9 @@ class SystemConsolePage {
|
||||
this.systemUsersActionMenus = Array.from(Array(10).keys()).map(
|
||||
(index) => new components.SystemUsersFilterMenu(page.locator(`#actionMenu-systemUsersTable-${index}`)),
|
||||
);
|
||||
|
||||
this.confirmModal = new components.GenericConfirmModal(page.locator('#confirmModal'));
|
||||
this.exportModal = new components.GenericConfirmModal(page.getByRole('dialog', {name: 'Export user data'}));
|
||||
this.saveChangesModal = new components.SystemUsers(page.locator('div.modal-content'));
|
||||
}
|
||||
|
||||
|
||||
Ссылка в новой задаче
Block a user