diff --git a/e2e/playwright/.eslintignore b/e2e/playwright/.eslintignore index 44e9af97e7..169a2afd8a 100644 --- a/e2e/playwright/.eslintignore +++ b/e2e/playwright/.eslintignore @@ -1,2 +1,3 @@ -results node_modules +playwright-report +test-results diff --git a/e2e/playwright/global_setup.ts b/e2e/playwright/global_setup.ts index ed5c9d00ad..d54d6ded88 100644 --- a/e2e/playwright/global_setup.ts +++ b/e2e/playwright/global_setup.ts @@ -2,18 +2,10 @@ // See LICENSE.txt for license information. import {expect} from '@playwright/test'; -import {AdminConfig} from '@mattermost/types/config'; import {UserProfile} from '@mattermost/types/users'; -import {PluginsResponse} from '@mattermost/types/plugins'; -import { - Client, - createRandomTeam, - getAdminClient, - getDefaultAdminUser, - getOnPremServerConfig, - makeClient, -} from './support/server'; +import {Client, createRandomTeam, getAdminClient, getDefaultAdminUser, makeClient} from './support/server'; +import {boardsPluginId, callsPluginId} from './support/constant'; import {defaultTeam} from './support/util'; import testConfig from './test.config'; @@ -43,9 +35,6 @@ async function sysadminSetup(client: Client, user: UserProfile | null) { await client.verifyUserEmail(client.token); } - // Update default server config - const adminConfig = await client.updateConfig(getOnPremServerConfig()); - // Log license and config info await printLicenseInfo(client); await printClientInfo(client); @@ -75,9 +64,6 @@ async function sysadminSetup(client: Client, user: UserProfile | null) { ); } - // Log boards product status - printBoardsProductStatus(adminConfig); - // Ensure all products as plugin are installed and active. await ensurePluginsLoaded(client); @@ -110,9 +96,6 @@ async function printClientInfo(client: Client) { - BuildHash = ${config.BuildHash} - BuildHashEnterprise = ${config.BuildHashEnterprise} - BuildEnterpriseReady = ${config.BuildEnterpriseReady} - - BuildHashBoards = ${config.BuildHashBoards} - - BuildBoards = ${config.BuildBoards} - - BuildHashPlaybooks = ${config.BuildHashPlaybooks} - FeatureFlagAppsEnabled = ${config.FeatureFlagAppsEnabled} - FeatureFlagBoardsProduct = ${config.FeatureFlagBoardsProduct} - FeatureFlagCallsEnabled = ${config.FeatureFlagCallsEnabled} @@ -120,10 +103,10 @@ async function printClientInfo(client: Client) { } function getProductsAsPlugin() { - const productsAsPlugin = ['com.mattermost.calls', 'playbooks']; + const productsAsPlugin = [callsPluginId]; if (!testConfig.boardsProductEnabled) { - productsAsPlugin.push('focalboard'); + productsAsPlugin.push(boardsPluginId); } return productsAsPlugin; @@ -131,7 +114,7 @@ function getProductsAsPlugin() { async function ensurePluginsLoaded(client: Client) { const pluginStatus = await client.getPluginStatuses(); - const plugins = (await client.getPlugins()) as PluginsResponse; + const plugins = await client.getPlugins(); getProductsAsPlugin().forEach(async (pluginId) => { const isInstalled = pluginStatus.some((plugin) => plugin.plugin_id === pluginId); @@ -154,19 +137,8 @@ async function ensurePluginsLoaded(client: Client) { }); } -function printBoardsProductStatus(config: AdminConfig) { - // Ensure boards as product is enabled - if (!config.FeatureFlags.BoardsProduct) { - // eslint-disable-next-line no-console - console.log('FeatureFlags.BoardsProduct is disabled. Related visual test will fail.'); - } else { - // eslint-disable-next-line no-console - console.log('FeatureFlags.BoardsProduct is enabled.'); - } -} - async function printPluginDetails(client: Client) { - const plugins = (await client.getPlugins()) as PluginsResponse; + const plugins = await client.getPlugins(); if (plugins.active.length) { // eslint-disable-next-line no-console diff --git a/e2e/playwright/package-lock.json b/e2e/playwright/package-lock.json index da89c78da9..ef9a9a8578 100644 --- a/e2e/playwright/package-lock.json +++ b/e2e/playwright/package-lock.json @@ -9,6 +9,7 @@ "@percy/cli": "1.18.0", "@percy/playwright": "1.0.4", "@playwright/test": "1.30.0", + "async-wait-until": "2.0.12", "chalk": "4.1.2", "deepmerge": "4.3.0", "dotenv": "16.0.3", @@ -740,6 +741,15 @@ "node": ">=8" } }, + "node_modules/async-wait-until": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/async-wait-until/-/async-wait-until-2.0.12.tgz", + "integrity": "sha512-SXy/vDs6UPJMG6YeEYOQ4ilA/JnGxk187KPGqFx9O+qVxsjkSl+jH+3P50qSNyMpEmDgr8qOFGOKCJckWb1i7A==", + "engines": { + "node": ">= 0.12.0", + "npm": ">= 1.0.0" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2861,6 +2871,11 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, + "async-wait-until": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/async-wait-until/-/async-wait-until-2.0.12.tgz", + "integrity": "sha512-SXy/vDs6UPJMG6YeEYOQ4ilA/JnGxk187KPGqFx9O+qVxsjkSl+jH+3P50qSNyMpEmDgr8qOFGOKCJckWb1i7A==" + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", diff --git a/e2e/playwright/package.json b/e2e/playwright/package.json index 4ad65eaa19..8ee95ffa63 100644 --- a/e2e/playwright/package.json +++ b/e2e/playwright/package.json @@ -14,6 +14,7 @@ "@percy/cli": "1.18.0", "@percy/playwright": "1.0.4", "@playwright/test": "1.30.0", + "async-wait-until": "2.0.12", "chalk": "4.1.2", "deepmerge": "4.3.0", "dotenv": "16.0.3", diff --git a/e2e/playwright/playwright.config.ts b/e2e/playwright/playwright.config.ts index c2808a548b..a014c9cb3b 100644 --- a/e2e/playwright/playwright.config.ts +++ b/e2e/playwright/playwright.config.ts @@ -24,9 +24,16 @@ export default defineConfig({ }, use: { baseURL: testConfig.baseURL, + ignoreHTTPSErrors: true, headless: testConfig.headless, locale: 'en-US', launchOptions: { + args: ['--use-fake-device-for-media-stream', '--use-fake-ui-for-media-stream'], + firefoxUserPrefs: { + 'media.navigator.streams.fake': true, + 'permissions.default.microphone': 1, + 'permissions.default.camera': 1, + }, slowMo: testConfig.slowMo, }, screenshot: 'only-on-failure', diff --git a/e2e/playwright/sample.env b/e2e/playwright/sample.env new file mode 100644 index 0000000000..d474d199e6 --- /dev/null +++ b/e2e/playwright/sample.env @@ -0,0 +1,56 @@ +# 1. PW_BASE_URL +# - Default to "http://localhost:8065" if not set. +# - When running test inside the Playwright docker image +# PW_BASE_URL=http://host.docker.internal:8065 + +# 2. NODE_TLS_REJECT_UNAUTHORIZED +# This node environment variable disables the verification of local certificates. Set for local testing only. +# NODE_TLS_REJECT_UNAUTHORIZED=0 + +# 3. PW_ADMIN_USERNAME +# - Default to "sysadmin" if not set. + +# 4. PW_ADMIN_PASSWORD +# - Default to "Sys@dmin-sample1" if not set. + +# 5. PW_ADMIN_EMAIL +# - Default to "sysadmin@sample.mattermost.com" if not set. + +# 6. PW_BOARDS_PRODUCT_ENABLED +# - Default to "true" if not set. Used to correctly set server config. + +# 7. PW_HA_CLUSTER_ENABLED +# - Default to "false" if not set. Set to true if the test server is with HA enabled. + +# 8. PW_HA_CLUSTER_NODE_COUNT +# - Default to "2" if not set. + +# 9. PW_HA_CLUSTER_NAME +# - Default to "mm_dev_cluster" if not set. + +# 10. PW_RESET_BEFORE_TEST +# - Default to "false" if not set. If true, the setup deletes all teams and channels other than the default team which is "ad-1". + +# 11. CI +# - Default to "false" if not set. + +# 12. PW_HEADLESS +# - Default to "true" if not set. Set to false to run test in head mode. + +# 13. PW_SLOWMO +# - Default to "0" if not set which means normal test speed run. Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on. + +# 14. PW_WORKERS +# - Default to "1" if not set. The maximum number of concurrent worker processes to use for parallelizing tests. + +# 15. PW_SNAPSHOT_ENABLE +# - Default to "false" if not set. Set to true to enable snapshot testing. +# Note that, snapshot testing should be done in Playwright docker image only. +# This is to ensure that, there's a common base platform for all contributors +# regardless of each local development platform. + +# 16. PW_PERCY_ENABLE +# - Default to "false" if not set. Use to save and compare results via https://percy.io/. + +# 17. PERCY_TOKEN +# - A token required by https://percy.io/. diff --git a/e2e/playwright/support/browser_context.ts b/e2e/playwright/support/browser_context.ts index 654820608f..59badf9a9c 100644 --- a/e2e/playwright/support/browser_context.ts +++ b/e2e/playwright/support/browser_context.ts @@ -15,12 +15,16 @@ export class TestBrowser { this.browser = browser; } - async login(user: UserProfile) { - // Log in via API request and save user storage - const storagePath = await loginByAPI(user.username, user.password); + async login(user: UserProfile | null) { + const options = {storageState: ''}; + if (user) { + // Log in via API request and save user storage + const storagePath = await loginByAPI(user.username, user.password); + options.storageState = storagePath; + } // Sign in a user in new browser context - const context = await this.browser.newContext({storageState: storagePath}); + const context = await this.browser.newContext(options); const page = await context.newPage(); return {context, page}; diff --git a/e2e/playwright/support/constant.ts b/e2e/playwright/support/constant.ts new file mode 100644 index 0000000000..e5fd709f4e --- /dev/null +++ b/e2e/playwright/support/constant.ts @@ -0,0 +1,7 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +export const boardsPluginId = 'focalboard'; +export const boardsProductId = 'boards'; +export const callsPluginId = 'com.mattermost.calls'; +export const playbooksPluginId = 'playbooks'; diff --git a/e2e/playwright/support/flag.ts b/e2e/playwright/support/flag.ts index bfa5dd4ec9..dfa82775bc 100644 --- a/e2e/playwright/support/flag.ts +++ b/e2e/playwright/support/flag.ts @@ -5,6 +5,7 @@ import os from 'node:os'; import {expect, test} from '@playwright/test'; +import {boardsPluginId, callsPluginId} from './constant'; import {getAdminClient} from './server/init'; import {isSmallScreen} from './util'; @@ -14,12 +15,22 @@ export async function shouldHaveBoardsEnabled(enabled = true) { const boardsEnabled = (typeof config.FeatureFlags.BoardsProduct === 'boolean' && config.FeatureFlags.BoardsProduct) || - config.PluginSettings.PluginStates['focalboard'].Enable; + config.PluginSettings.PluginStates[boardsPluginId].Enable; const matched = boardsEnabled === enabled; expect(matched, matched ? '' : `Boards expect "${enabled}" but actual "${boardsEnabled}"`).toBeTruthy(); } +export async function shouldHaveCallsEnabled(enabled = true) { + const {adminClient} = await getAdminClient(); + const config = await adminClient.getConfig(); + + const callsEnabled = config.PluginSettings.PluginStates[callsPluginId].Enable; + + const matched = callsEnabled === enabled; + expect(matched, matched ? '' : `Calls expect "${enabled}" but actual "${callsEnabled}"`).toBeTruthy(); +} + export async function shouldHaveFeatureFlag(name: string, value: string | boolean) { const {adminClient} = await getAdminClient(); const config = await adminClient.getConfig(); diff --git a/e2e/playwright/support/server/default_config.ts b/e2e/playwright/support/server/default_config.ts index 06b5becd1b..63be678309 100644 --- a/e2e/playwright/support/server/default_config.ts +++ b/e2e/playwright/support/server/default_config.ts @@ -73,8 +73,8 @@ const onPremServerConfig = (): Partial => { }; }; -// Should be based only from the generated default config from mattermost-server via "make config-reset" -// Based on v7.9 server +// Should be based only from the generated default config from ./server via "make config-reset" +// Based on v7.10 server const defaultServerConfig: AdminConfig = { ServiceSettings: { SiteURL: '', @@ -180,9 +180,6 @@ const defaultServerConfig: AdminConfig = { EnableCustomGroups: true, SelfHostedPurchase: true, AllowSyncedDrafts: true, - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore SelfHostedExpansion: false, }, TeamSettings: { @@ -636,7 +633,7 @@ const defaultServerConfig: AdminConfig = { Enable: true, }, focalboard: { - Enable: false, + Enable: true, }, playbooks: { Enable: true, @@ -689,13 +686,12 @@ const defaultServerConfig: AdminConfig = { GraphQL: false, InsightsEnabled: true, CommandPalette: false, - BoardsProduct: true, + BoardsProduct: false, SendWelcomePost: true, WorkTemplate: false, PostPriority: true, WysiwygEditor: false, PeopleProduct: false, - AnnualSubscription: false, ReduceOnBoardingTaskList: false, OnboardingAutoShowLinkedBoard: true, ThreadsEverywhere: false, diff --git a/e2e/playwright/support/test_action.ts b/e2e/playwright/support/test_action.ts index 469ce8bb97..8cb9428c4e 100644 --- a/e2e/playwright/support/test_action.ts +++ b/e2e/playwright/support/test_action.ts @@ -3,6 +3,8 @@ import {Locator, Page} from '@playwright/test'; +export {waitUntil} from 'async-wait-until'; + const visibilityHidden = 'visibility: hidden !important;'; const hideTeamHeader = `.test-team-header {${visibilityHidden}} `; const hidePostHeaderTime = `.post__time {${visibilityHidden}} `; diff --git a/e2e/playwright/support/test_fixture.ts b/e2e/playwright/support/test_fixture.ts index 605e55721a..8b7949634b 100644 --- a/e2e/playwright/support/test_fixture.ts +++ b/e2e/playwright/support/test_fixture.ts @@ -1,9 +1,15 @@ import {test as base, Browser} from '@playwright/test'; import {TestBrowser} from './browser_context'; -import {shouldHaveBoardsEnabled, shouldHaveFeatureFlag, shouldSkipInSmallScreen, shouldRunInLinux} from './flag'; +import { + shouldHaveBoardsEnabled, + shouldHaveCallsEnabled, + shouldHaveFeatureFlag, + shouldSkipInSmallScreen, + shouldRunInLinux, +} from './flag'; import {initSetup, getAdminClient} from './server'; -import {hideDynamicChannelsContent, waitForAnimationEnd} from './test_action'; +import {hideDynamicChannelsContent, waitForAnimationEnd, waitUntil} from './test_action'; import {pages} from './ui/pages'; import {matchSnapshot} from './visual'; @@ -30,24 +36,26 @@ class PlaywrightExtended { readonly testBrowser: TestBrowser; // ./flag - readonly shouldHaveBoardsEnabled: typeof shouldHaveBoardsEnabled; - readonly shouldHaveFeatureFlag: typeof shouldHaveFeatureFlag; - readonly shouldSkipInSmallScreen: typeof shouldSkipInSmallScreen; - readonly shouldRunInLinux: typeof shouldRunInLinux; + readonly shouldHaveBoardsEnabled; + readonly shouldHaveCallsEnabled; + readonly shouldHaveFeatureFlag; + readonly shouldSkipInSmallScreen; + readonly shouldRunInLinux; // ./server - readonly getAdminClient: typeof getAdminClient; - readonly initSetup: typeof initSetup; + readonly getAdminClient; + readonly initSetup; // ./test_action - readonly hideDynamicChannelsContent: typeof hideDynamicChannelsContent; - readonly waitForAnimationEnd: typeof waitForAnimationEnd; + readonly hideDynamicChannelsContent; + readonly waitForAnimationEnd; + readonly waitUntil; // ./ui/pages - readonly pages: typeof pages; + readonly pages; // ./visual - readonly matchSnapshot: typeof matchSnapshot; + readonly matchSnapshot; constructor(browser: Browser) { // ./browser_context @@ -55,6 +63,7 @@ class PlaywrightExtended { // ./flag this.shouldHaveBoardsEnabled = shouldHaveBoardsEnabled; + this.shouldHaveCallsEnabled = shouldHaveCallsEnabled; this.shouldHaveFeatureFlag = shouldHaveFeatureFlag; this.shouldSkipInSmallScreen = shouldSkipInSmallScreen; this.shouldRunInLinux = shouldRunInLinux; @@ -66,6 +75,7 @@ class PlaywrightExtended { // ./test_action this.hideDynamicChannelsContent = hideDynamicChannelsContent; this.waitForAnimationEnd = waitForAnimationEnd; + this.waitUntil = waitUntil; // ./ui/pages this.pages = pages; diff --git a/e2e/playwright/support/ui/components/boards/create_modal.ts b/e2e/playwright/support/ui/components/boards/create_modal.ts index 2c82c4bef0..ed3f21a2a3 100644 --- a/e2e/playwright/support/ui/components/boards/create_modal.ts +++ b/e2e/playwright/support/ui/components/boards/create_modal.ts @@ -4,22 +4,23 @@ import {expect, Locator} from '@playwright/test'; export default class BoardsCreateModal { - readonly locator: Locator; - readonly productSwitchMenu: Locator; + readonly container: Locator; - constructor(locator: Locator) { - this.locator = locator; + readonly productSwitchMenu; - this.productSwitchMenu = locator.getByRole('button', {name: 'Product switch menu'}); + constructor(container: Locator) { + this.container = container; + + this.productSwitchMenu = container.getByRole('button', {name: 'Product switch menu'}); } async switchProduct(name: string) { await this.productSwitchMenu.click(); - await this.locator.getByRole('link', {name: ` ${name}`}).click(); + await this.container.getByRole('link', {name: ` ${name}`}).click(); } async toBeVisible(name: string) { - await expect(this.locator.getByRole('heading', {name})).toBeVisible(); + await expect(this.container.getByRole('heading', {name})).toBeVisible(); } } diff --git a/e2e/playwright/support/ui/components/boards/sidebar.ts b/e2e/playwright/support/ui/components/boards/sidebar.ts index 7a6e6f0c3c..372054524c 100644 --- a/e2e/playwright/support/ui/components/boards/sidebar.ts +++ b/e2e/playwright/support/ui/components/boards/sidebar.ts @@ -5,10 +5,11 @@ import {Locator} from '@playwright/test'; export default class BoardsSidebar { readonly container: Locator; - readonly plusButton: Locator; - readonly createNewBoardMenuItem: Locator; - readonly createNewCategoryMenuItem: Locator; - readonly titles: Locator; + + readonly plusButton; + readonly createNewBoardMenuItem; + readonly createNewCategoryMenuItem; + readonly titles; constructor(container: Locator) { this.container = container; diff --git a/e2e/playwright/support/ui/components/channels/app_bar.ts b/e2e/playwright/support/ui/components/channels/app_bar.ts index 2c776bfdb5..740856d53c 100644 --- a/e2e/playwright/support/ui/components/channels/app_bar.ts +++ b/e2e/playwright/support/ui/components/channels/app_bar.ts @@ -5,7 +5,8 @@ import {expect, Locator} from '@playwright/test'; export default class ChannelsAppBar { readonly container: Locator; - readonly playbooksIcon: Locator; + + readonly playbooksIcon; constructor(container: Locator) { this.container = container; diff --git a/e2e/playwright/support/ui/components/channels/header.ts b/e2e/playwright/support/ui/components/channels/header.ts new file mode 100644 index 0000000000..4bb69e90fb --- /dev/null +++ b/e2e/playwright/support/ui/components/channels/header.ts @@ -0,0 +1,18 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {expect, Locator} from '@playwright/test'; + +export default class ChannelsHeader { + readonly container: Locator; + + constructor(container: Locator) { + this.container = container; + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + } +} + +export {ChannelsHeader}; diff --git a/e2e/playwright/support/ui/components/channels/post.ts b/e2e/playwright/support/ui/components/channels/post.ts index 39388d339f..341571dd6d 100644 --- a/e2e/playwright/support/ui/components/channels/post.ts +++ b/e2e/playwright/support/ui/components/channels/post.ts @@ -5,12 +5,17 @@ import {expect, Locator} from '@playwright/test'; export default class ChannelsPost { readonly container: Locator; - readonly profileIcon: Locator; + + readonly body; + readonly profileIcon; + readonly replyButton; constructor(container: Locator) { this.container = container; + this.body = container.locator('.post__body'); this.profileIcon = container.locator('.profile-icon'); + this.replyButton = container.getByRole('button', {name: 'reply'}); } async toBeVisible() { @@ -26,6 +31,12 @@ export default class ChannelsPost { async getProfileImage(username: string) { return await this.profileIcon.getByAltText(`${username} profile image`); } + + async openRHS() { + await this.container.hover(); + await this.replyButton.waitFor(); + await this.replyButton.click(); + } } export {ChannelsPost}; diff --git a/e2e/playwright/support/ui/components/channels/post_create.ts b/e2e/playwright/support/ui/components/channels/post_create.ts index 60efa9703b..2a8be0ec6d 100644 --- a/e2e/playwright/support/ui/components/channels/post_create.ts +++ b/e2e/playwright/support/ui/components/channels/post_create.ts @@ -5,9 +5,10 @@ import {expect, Locator} from '@playwright/test'; export default class ChannelsPostCreate { readonly container: Locator; - readonly input: Locator; - readonly attachmentButton: Locator; - readonly emojiButton: Locator; + + readonly input; + readonly attachmentButton; + readonly emojiButton; constructor(container: Locator) { this.container = container; diff --git a/e2e/playwright/support/ui/components/channels/sidebar_right.ts b/e2e/playwright/support/ui/components/channels/sidebar_right.ts new file mode 100644 index 0000000000..76dd0cec54 --- /dev/null +++ b/e2e/playwright/support/ui/components/channels/sidebar_right.ts @@ -0,0 +1,18 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {expect, Locator} from '@playwright/test'; + +export default class ChannelsSidebarRight { + readonly container: Locator; + + constructor(container: Locator) { + this.container = container; + } + + async toBeVisible() { + await expect(this.container).toBeVisible(); + } +} + +export {ChannelsSidebarRight}; diff --git a/e2e/playwright/support/ui/components/global_header.ts b/e2e/playwright/support/ui/components/global_header.ts index 51f580b478..ccd1d48ee4 100644 --- a/e2e/playwright/support/ui/components/global_header.ts +++ b/e2e/playwright/support/ui/components/global_header.ts @@ -5,7 +5,8 @@ import {expect, Locator} from '@playwright/test'; export default class GlobalHeader { readonly container: Locator; - readonly productSwitchMenu: Locator; + + readonly productSwitchMenu; constructor(container: Locator) { this.container = container; diff --git a/e2e/playwright/support/ui/components/index.ts b/e2e/playwright/support/ui/components/index.ts index 20fe35256d..019e4be325 100644 --- a/e2e/playwright/support/ui/components/index.ts +++ b/e2e/playwright/support/ui/components/index.ts @@ -2,17 +2,30 @@ // See LICENSE.txt for license information. import {BoardsSidebar} from './boards/sidebar'; +import {ChannelsHeader} from './channels/header'; import {ChannelsAppBar} from './channels/app_bar'; import {ChannelsPostCreate} from './channels/post_create'; import {ChannelsPost} from './channels/post'; +import {ChannelsSidebarRight} from './channels/sidebar_right'; import {GlobalHeader} from './global_header'; const components = { BoardsSidebar, ChannelsAppBar, + ChannelsHeader, ChannelsPostCreate, ChannelsPost, + ChannelsSidebarRight, GlobalHeader, }; -export {components, BoardsSidebar, ChannelsAppBar, ChannelsPostCreate, ChannelsPost, GlobalHeader}; +export { + components, + BoardsSidebar, + ChannelsAppBar, + ChannelsHeader, + ChannelsPostCreate, + ChannelsPost, + ChannelsSidebarRight, + GlobalHeader, +}; diff --git a/e2e/playwright/support/ui/pages/boards_create.ts b/e2e/playwright/support/ui/pages/boards_create.ts index e572fcf337..f49aa163df 100644 --- a/e2e/playwright/support/ui/pages/boards_create.ts +++ b/e2e/playwright/support/ui/pages/boards_create.ts @@ -1,21 +1,23 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {expect, Locator, Page} from '@playwright/test'; +import {expect, Page} from '@playwright/test'; -import {GlobalHeader} from '@e2e-support/ui/components'; +import {components} from '@e2e-support/ui/components'; export default class BoardsCreatePage { readonly boards = 'Boards'; readonly page: Page; - readonly globalHeader: GlobalHeader; - readonly createBoardHeading: Locator; - readonly createEmptyBoardButton: Locator; - readonly useTemplateButton: Locator; + + readonly globalHeader; + + readonly createBoardHeading; + readonly createEmptyBoardButton; + readonly useTemplateButton; constructor(page: Page) { this.page = page; - this.globalHeader = new GlobalHeader(this.page.locator('#global-header')); + this.globalHeader = new components.GlobalHeader(this.page.locator('#global-header')); this.createBoardHeading = page.getByRole('heading', {name: 'Create a board'}); this.createEmptyBoardButton = page.getByRole('button', {name: ' Create an empty board'}); this.useTemplateButton = page.getByRole('button', {name: 'Use this template'}); diff --git a/e2e/playwright/support/ui/pages/boards_view.ts b/e2e/playwright/support/ui/pages/boards_view.ts index 882619589e..bc618e67f8 100644 --- a/e2e/playwright/support/ui/pages/boards_view.ts +++ b/e2e/playwright/support/ui/pages/boards_view.ts @@ -1,23 +1,25 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {expect, Locator, Page} from '@playwright/test'; +import {expect, Page} from '@playwright/test'; -import {BoardsSidebar, GlobalHeader} from '@e2e-support/ui/components'; +import {components} from '@e2e-support/ui/components'; export default class BoardsViewPage { readonly boards = 'Boards'; readonly page: Page; - readonly sidebar: BoardsSidebar; - readonly globalHeader: GlobalHeader; - readonly topHead: Locator; - readonly editableTitle: Locator; - readonly shareButton: Locator; + + readonly sidebar; + readonly globalHeader; + + readonly topHead; + readonly editableTitle; + readonly shareButton; constructor(page: Page) { this.page = page; - this.sidebar = new BoardsSidebar(page.locator('.octo-sidebar')); - this.globalHeader = new GlobalHeader(this.page.locator('#global-header')); + this.sidebar = new components.BoardsSidebar(page.locator('.octo-sidebar')); + this.globalHeader = new components.GlobalHeader(this.page.locator('#global-header')); this.topHead = page.locator('.top-head'); this.editableTitle = this.topHead.getByPlaceholder('Untitled board'); this.shareButton = page.getByRole('button', {name: '󰍁 Share'}); diff --git a/e2e/playwright/support/ui/pages/channels.ts b/e2e/playwright/support/ui/pages/channels.ts index 9187ee4270..948cb673c5 100644 --- a/e2e/playwright/support/ui/pages/channels.ts +++ b/e2e/playwright/support/ui/pages/channels.ts @@ -3,27 +3,32 @@ import {Page} from '@playwright/test'; -import {ChannelsAppBar, ChannelsPost, ChannelsPostCreate, GlobalHeader} from '@e2e-support/ui/components'; -import {isSmallScreen} from '@e2e-support/util'; +import {waitUntil} from '@e2e-support/test_action'; +import {components} from '@e2e-support/ui/components'; +import {duration, isSmallScreen} from '@e2e-support/util'; export default class ChannelsPage { readonly channels = 'Channels'; readonly page: Page; - readonly postCreate: ChannelsPostCreate; - readonly globalHeader: GlobalHeader; - readonly appBar: ChannelsAppBar; + readonly postCreate; + readonly globalHeader; + readonly header; + readonly appBar; + readonly sidebarRight; constructor(page: Page) { this.page = page; - this.postCreate = new ChannelsPostCreate(page.locator('#post-create')); - this.globalHeader = new GlobalHeader(this.page.locator('#global-header')); - this.appBar = new ChannelsAppBar(page.locator('.app-bar')); + this.postCreate = new components.ChannelsPostCreate(page.locator('#post-create')); + this.globalHeader = new components.GlobalHeader(page.locator('#global-header')); + this.header = new components.ChannelsHeader(page.locator('.channel-header')); + this.appBar = new components.ChannelsAppBar(page.locator('.app-bar')); + this.sidebarRight = new components.ChannelsSidebarRight(page.locator('#sidebar-right')); } async goto(teamName = '', channelName = '') { let channelsUrl = '/'; if (teamName) { - channelsUrl += `/${teamName}`; + channelsUrl += `${teamName}`; if (channelName) { channelsUrl += `/${channelName}`; } @@ -47,25 +52,54 @@ export default class ChannelsPage { async getFirstPost() { await this.page.getByTestId('postView').first().waitFor(); const post = await this.page.getByTestId('postView').first(); - return new ChannelsPost(post); + return new components.ChannelsPost(post); } async getLastPost() { await this.page.getByTestId('postView').last().waitFor(); const post = await this.page.getByTestId('postView').last(); - return new ChannelsPost(post); + return new components.ChannelsPost(post); } async getNthPost(index: number) { await this.page.getByTestId('postView').nth(index).waitFor(); const post = await this.page.getByTestId('postView').nth(index); - return new ChannelsPost(post); + return new components.ChannelsPost(post); } async getPostById(id: string) { await this.page.locator(`[id="post_${id}"]`).waitFor(); const post = await this.page.locator(`[id="post_${id}"]`); - return new ChannelsPost(post); + return new components.ChannelsPost(post); + } + + async getRHSPostById(id: string) { + await this.page.locator(`[id="rhsPost_${id}"]`).waitFor(); + const post = await this.page.locator(`[id="rhsPost_${id}"]`); + return new components.ChannelsPost(post); + } + + async waitUntilLastPostContains(text: string, timeout = duration.ten_sec) { + await waitUntil( + async () => { + const post = await this.getLastPost(); + const content = await post.container.textContent(); + return content?.includes(text); + }, + {timeout} + ); + } + + async waitUntilPostWithIdContains(id: string, text: string, timeout = duration.ten_sec) { + await waitUntil( + async () => { + const post = await this.getPostById(id); + const content = await post.container.textContent(); + + return content?.includes(text); + }, + {timeout} + ); } } diff --git a/e2e/playwright/support/ui/pages/landing_login.ts b/e2e/playwright/support/ui/pages/landing_login.ts index fad94e2b11..238d28bb89 100644 --- a/e2e/playwright/support/ui/pages/landing_login.ts +++ b/e2e/playwright/support/ui/pages/landing_login.ts @@ -1,14 +1,16 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {expect, Locator, Page} from '@playwright/test'; +import {expect, Page} from '@playwright/test'; export default class LandingLoginPage { readonly page: Page; + readonly isMobile?: boolean; - readonly viewInAppButton: Locator; - readonly viewInDesktopAppButton: Locator; - readonly viewInBrowserButton: Locator; + + readonly viewInAppButton; + readonly viewInDesktopAppButton; + readonly viewInBrowserButton; constructor(page: Page, isMobile?: boolean) { this.page = page; diff --git a/e2e/playwright/support/ui/pages/login.ts b/e2e/playwright/support/ui/pages/login.ts index ff4a7e9adc..69db9f59f3 100644 --- a/e2e/playwright/support/ui/pages/login.ts +++ b/e2e/playwright/support/ui/pages/login.ts @@ -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, Page} from '@playwright/test'; import {AdminConfig} from '@mattermost/types/config'; import {UserProfile} from '@mattermost/types/users'; @@ -10,18 +10,19 @@ export default class LoginPage { readonly adminConfig: AdminConfig; readonly page: Page; - readonly title: Locator; - readonly subtitle: Locator; - readonly bodyCard: Locator; - readonly loginInput: Locator; - readonly loginPlaceholder: Locator; - readonly passwordInput: Locator; - readonly signInButton: Locator; - readonly createAccountLink: Locator; - readonly forgotPasswordLink: Locator; - readonly userErrorLabel: Locator; - readonly fieldWithError: Locator; - readonly formContainer: Locator; + + readonly title; + readonly subtitle; + readonly bodyCard; + readonly loginInput; + readonly loginPlaceholder; + readonly passwordInput; + readonly signInButton; + readonly createAccountLink; + readonly forgotPasswordLink; + readonly userErrorLabel; + readonly fieldWithError; + readonly formContainer; constructor(page: Page, adminConfig: AdminConfig) { this.page = page; diff --git a/e2e/playwright/support/ui/pages/signup.ts b/e2e/playwright/support/ui/pages/signup.ts index b108e57cda..1fa8362e07 100644 --- a/e2e/playwright/support/ui/pages/signup.ts +++ b/e2e/playwright/support/ui/pages/signup.ts @@ -1,23 +1,24 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {expect, Locator, Page} from '@playwright/test'; +import {expect, Page} from '@playwright/test'; import {duration, wait} from '@e2e-support/util'; export default class SignupPage { readonly page: Page; - readonly title: Locator; - readonly subtitle: Locator; - readonly bodyCard: Locator; - readonly emailInput: Locator; - readonly usernameInput: Locator; - readonly passwordInput: Locator; - readonly createAccountButton: Locator; - readonly loginLink: Locator; - readonly emailError: Locator; - readonly usernameError: Locator; - readonly passwordError: Locator; + + readonly title; + readonly subtitle; + readonly bodyCard; + readonly emailInput; + readonly usernameInput; + readonly passwordInput; + readonly createAccountButton; + readonly loginLink; + readonly emailError; + readonly usernameError; + readonly passwordError; constructor(page: Page) { this.page = page; diff --git a/e2e/playwright/tsconfig.json b/e2e/playwright/tsconfig.json index acf6a4122b..3c136644ba 100644 --- a/e2e/playwright/tsconfig.json +++ b/e2e/playwright/tsconfig.json @@ -13,5 +13,6 @@ "@e2e-test.config": ["test.config.ts"] } }, - "include": ["./**/*"] + "include": ["./**/*"], + "exclude": ["playwright-report"] }