upgrade playwright dependencies, screenshots and tests (#29466)

Этот коммит содержится в:
Saturnino Abril
2024-12-04 19:52:18 +08:00
коммит произвёл GitHub
родитель 43d589be79
Коммит 90c0235c4b
37 изменённых файлов: 1333 добавлений и 1260 удалений

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

@@ -275,7 +275,7 @@ $(if mme2e_is_token_in_list "webhook-interactions" "$ENABLED_DOCKER_SERVICES"; t
$(if mme2e_is_token_in_list "playwright" "$ENABLED_DOCKER_SERVICES"; then
echo '
playwright:
image: mcr.microsoft.com/playwright:v1.46.1
image: mcr.microsoft.com/playwright:v1.49.0-noble
entrypoint: ["/bin/bash", "-c"]
command: ["until [ -f /var/run/mm_terminate ]; do sleep 5; done"]
env_file:

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

@@ -43,8 +43,8 @@ ${MME2E_DC_SERVER} exec -i -u "$MME2E_UID" -- playwright bash -c "cd e2e-tests/p
# Documentation on the results.json file: https://playwright.dev/docs/api/class-testcase#test-case-expected-status
# NB: the following line is needed only for compatibility reasons, to support RollingRelease tests for versions prior to v10.1.0
# It can be removed after releases <=v10.0.x are phased out
mv -v ../playwright/playwright-report/results.json ../playwright/results/results.json 2>/dev/null || true
jq -f /dev/stdin ../playwright/results/results.json > ../playwright/results/summary.json <<EOF
mv -v ../playwright/playwright-report/results.json ../playwright/results/reporter/results.json 2>/dev/null || true
jq -f /dev/stdin ../playwright/results/reporter/results.json > ../playwright/results/summary.json <<EOF
{
passed: .stats.expected,
failed: .stats.unexpected,

3
e2e-tests/.gitignore поставляемый
Просмотреть файл

@@ -9,3 +9,6 @@
# node
*.lock
# build
*.tsbuildinfo

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

@@ -1,4 +0,0 @@
node_modules
playwright-report
test-results
results

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

@@ -1,18 +0,0 @@
{
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
"plugins": ["@typescript-eslint"],
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"sourceType": "module"
},
"rules": {
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-var-requires": "off",
"no-console": "error"
},
"env": {
"node": true
}
}

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

@@ -40,10 +40,10 @@ npm run test
#### 1. Run docker container using latest focal version
Change to the root directory, then run the docker container.
Change to the root directory, then run the docker container. (See https://playwright.dev/docs/docker for reference.)
```
docker run -it --rm -v "$(pwd):/mattermost/" --ipc=host mcr.microsoft.com/playwright:v1.46.1 /bin/bash
docker run -it --rm -v "$(pwd):/mattermost/" --ipc=host mcr.microsoft.com/playwright:v1.49.0-noble /bin/bash
```
#### 2. Inside the docker container

45
e2e-tests/playwright/eslint.config.mjs Обычный файл
Просмотреть файл

@@ -0,0 +1,45 @@
import typescriptEslint from '@typescript-eslint/eslint-plugin';
import globals from 'globals';
import tsParser from '@typescript-eslint/parser';
import path from 'node:path';
import {fileURLToPath} from 'node:url';
import js from '@eslint/js';
import {FlatCompat} from '@eslint/eslintrc';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
});
export default [
{
ignores: ['**/node_modules', '**/playwright-report', '**/test-results', '**/results'],
},
...compat.extends('eslint:recommended', 'plugin:@typescript-eslint/recommended').map((config) => ({
...config,
files: ['**/*.ts', '**/*.s'],
})),
{
files: ['**/*.ts', '**/*.s'],
plugins: {
'@typescript-eslint': typescriptEslint,
},
languageOptions: {
globals: {
...globals.node,
},
parser: tsParser,
ecmaVersion: 5,
sourceType: 'module',
},
rules: {
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-var-requires': 'off',
'no-console': 'error',
},
},
];

2340
e2e-tests/playwright/package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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

@@ -4,7 +4,7 @@
"test:update-snapshots": "cross-env PW_SNAPSHOT_ENABLE=true playwright test --update-snapshots",
"percy": "cross-env PERCY_TOKEN=$PERCY_TOKEN PW_PERCY_ENABLE=true percy exec -- playwright test --project=chrome --project=ipad",
"tsc": "tsc -b",
"lint": "eslint . --ext .js,.ts",
"lint": "eslint .",
"prettier": "prettier . --check",
"prettier:fix": "prettier --write .",
"check": "npm run tsc && npm run lint && npm run prettier",
@@ -15,27 +15,27 @@
"postinstall": "script/post_install.sh"
},
"dependencies": {
"@axe-core/playwright": "4.10.0",
"@percy/cli": "1.29.3",
"@percy/playwright": "1.0.6",
"@playwright/test": "1.46.1",
"@axe-core/playwright": "4.10.1",
"@percy/cli": "1.30.3",
"@percy/playwright": "1.0.7",
"@playwright/test": "1.49.0",
"async-wait-until": "2.0.12",
"axe-core": "4.10.0",
"axe-core": "4.10.2",
"chalk": "4.1.2",
"dayjs": "1.11.13",
"deepmerge": "4.3.1",
"dotenv": "16.4.5",
"form-data-encoder": "4.0.2",
"formdata-node": "6.0.3",
"uuid": "10.0.0"
"uuid": "11.0.3"
},
"devDependencies": {
"@types/uuid": "9.0.8",
"@typescript-eslint/eslint-plugin": "7.2.0",
"@typescript-eslint/parser": "7.2.0",
"@types/uuid": "10.0.0",
"@typescript-eslint/eslint-plugin": "8.16.0",
"@typescript-eslint/parser": "8.16.0",
"cross-env": "7.0.3",
"eslint": "8.57.0",
"prettier": "3.2.5",
"typescript": "5.4.5"
"eslint": "9.16.0",
"prettier": "3.4.1",
"typescript": "5.7.2"
}
}

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

@@ -6,12 +6,10 @@ import {defineConfig, devices} from '@playwright/test';
import {duration} from '@e2e-support/util';
import testConfig from '@e2e-test.config';
const defaultOutputFolder = './results';
export default defineConfig({
globalSetup: require.resolve('./global_setup'),
forbidOnly: testConfig.isCI,
outputDir: './results/tests',
outputDir: './results/output',
retries: testConfig.isCI ? 2 : 0,
testDir: 'tests',
timeout: duration.one_min,
@@ -80,9 +78,9 @@ export default defineConfig({
},
],
reporter: [
['html', {open: 'never', outputFolder: defaultOutputFolder}],
['json', {outputFile: `${defaultOutputFolder}/results.json`}],
['junit', {outputFile: `${defaultOutputFolder}/results.xml`}],
['html', {open: 'never', outputFolder: './results/reporter'}],
['json', {outputFile: './results/reporter/results.json'}],
['junit', {outputFile: './results/reporter/results.xml'}],
['list'],
],
});

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

@@ -8,14 +8,14 @@ dayjs.extend(duration);
const {TYPE, SERVER_TYPE, BRANCH, PULL_REQUEST, BUILD_ID, MM_ENV, MM_DOCKER_IMAGE, MM_DOCKER_TAG, RELEASE_DATE} =
process.env;
const resultsFile = 'results/results.json';
const resultsFile = 'results/reporter/results.json';
const summaryFile = 'results/summary.json';
const results = JSON.parse(fs.readFileSync(resultsFile, 'utf8'));
const summary = JSON.parse(fs.readFileSync(summaryFile, 'utf8'));
const passRate = summary.passed / (summary.passed + summary.failed);
const passRate = (summary.passed * 100) / (summary.passed + summary.failed);
const totalSpecs = summary.passed + summary.failed;
const playwrightVersion = results.config.version;
const playwrightDuration = dayjs.duration(results.stats.duration, 'seconds').format('HH:mm:ss');
const playwrightDuration = dayjs.duration(results.stats.duration, 'millisecond').format('HH:mm:ss');
function generateTitle() {
let dockerImageLink = '';
@@ -68,7 +68,7 @@ function generateWebhookBody() {
}
}
const summaryField = `${(passRate * 100).toFixed(2)}% (${summary.passed}/${totalSpecs}) | ${playwrightDuration} | playwright@${playwrightVersion}`;
const summaryField = `${passRate.toFixed(2)}% (${summary.passed}/${totalSpecs}) | ${playwrightDuration} | playwright@${playwrightVersion}`;
const serverTypeField = SERVER_TYPE ? '\nTest server: ' + SERVER_TYPE : '';
const mmEnvField = MM_ENV ? '\nTest server override: ' + MM_ENV : '';
const rollingReleaseMatchRegex = BUILD_ID?.match(/-rolling(?<version>[^-]+)-/);

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

@@ -64,7 +64,6 @@ export async function requestTrialLicense() {
users: 100,
});
} catch (error) {
// eslint-disable-next-line no-console
expect(error, 'Failed to request trial license').toBeFalsy();
throw error;
}

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

@@ -81,7 +81,7 @@ const onPremServerConfig = (): Partial<TestAdminConfig> => {
};
// Should be based only from the generated default config from ./server via "make config-reset"
// Based on v10.2 server
// Based on v10.3 server
const defaultServerConfig: AdminConfig = {
ServiceSettings: {
SiteURL: '',
@@ -198,6 +198,7 @@ const defaultServerConfig: AdminConfig = {
RefreshPostStatsRunTime: '00:00',
MaximumPayloadSizeBytes: 300000,
MaximumURLLength: 2048,
ScheduledPosts: true,
},
TeamSettings: {
SiteName: 'Mattermost',
@@ -685,6 +686,9 @@ const defaultServerConfig: AdminConfig = {
'com.mattermost.nps': {
Enable: true,
},
'mattermost-ai': {
Enable: true,
},
playbooks: {
Enable: true,
},
@@ -724,6 +728,7 @@ const defaultServerConfig: AdminConfig = {
TestFeature: 'off',
TestBoolFeature: false,
EnableRemoteClusterService: false,
EnableSharedChannelsDMs: false,
AppsEnabled: false,
PermalinkPreviews: false,
NormalizeLdapDNs: false,
@@ -741,6 +746,7 @@ const defaultServerConfig: AdminConfig = {
WebSocketEventScope: true,
NotificationMonitoring: true,
ExperimentalAuditSettingsSystemConsoleUI: false,
ExperimentalCrossTeamSearch: false,
},
ImportSettings: {
Directory: './import',

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

@@ -0,0 +1,36 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {expect, Locator} from '@playwright/test';
export default class SearchPopover {
readonly container: Locator;
readonly messagesButton;
readonly filesButton;
readonly searchInput;
readonly searchBoxClose;
readonly selectedSuggestion;
readonly searchHints;
constructor(container: Locator) {
this.container = container;
this.messagesButton = container.getByRole('button', {name: 'Messages'});
this.filesButton = container.getByRole('button', {name: 'Files'});
this.searchInput = container.getByLabel('Search messages');
this.searchBoxClose = container.getByTestId('searchBoxClose');
this.selectedSuggestion = container.locator('.suggestion--selected').locator('.suggestion-list__main');
this.searchHints = container.locator('#searchHints');
}
async toBeVisible() {
await expect(this.container).toBeVisible();
}
getSelectedSuggestion() {
return this.searchHints.locator('.suggestion--selected');
}
}
export {SearchPopover};

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

@@ -43,6 +43,11 @@ export default class GlobalHeader {
await expect(this.searchBox).toBeVisible();
await this.searchBox.click();
}
async closeSearch() {
await expect(this.searchBox).toBeVisible();
await this.searchBox.getByTestId('input-clear').click();
}
}
export {GlobalHeader};

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

@@ -13,6 +13,7 @@ import {FindChannelsModal} from './channels/find_channels_modal';
import {SettingsModal} from './channels/settings/settings_modal';
import {Footer} from './footer';
import {GlobalHeader} from './global_header';
import {SearchPopover} from './channels/search_popover';
import {MainHeader} from './main_header';
import {PostDotMenu} from './channels/post_dot_menu';
import {PostReminderMenu} from './channels/post_reminder_menu';
@@ -33,6 +34,7 @@ import {SystemUsersColumnToggleMenu} from './system_console/sections/system_user
const components = {
GlobalHeader,
SearchPopover,
ChannelsCenterView,
ChannelsSidebarLeft,
ChannelsSidebarRight,

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

@@ -11,6 +11,7 @@ export default class ChannelsPage {
readonly page: Page;
readonly globalHeader;
readonly searchPopover;
readonly centerView;
readonly scheduledDraftDropdown;
readonly scheduledDraftModal;
@@ -33,6 +34,7 @@ export default class ChannelsPage {
// The main areas of the app
this.globalHeader = new components.GlobalHeader(page.locator('#global-header'));
this.searchPopover = new components.SearchPopover(page.locator('#searchPopover'));
this.centerView = new components.ChannelsCenterView(page.getByTestId('channel_view'));
this.sidebarLeft = new components.ChannelsSidebarLeft(page.locator('#SidebarContainer'));
this.sidebarRight = new components.ChannelsSidebarRight(page.locator('#sidebar-right'));

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

@@ -31,7 +31,7 @@ export async function matchSnapshot(testInfo: TestInfo, testArgs: TestArgs, opti
if (testConfig.snapshotEnabled) {
// Visual test with built-in snapshot
const filename = testInfo.title.replace(illegalRe, '').replace(/\s/g, '-').trim().toLowerCase();
const filename = testInfo.title.trim().replace(illegalRe, '').replace(/\s/g, '-').trim().toLowerCase();
await expect(testArgs.page).toHaveScreenshot(`${filename}.png`, {fullPage: true, ...options});
}

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

@@ -60,7 +60,7 @@ test('MM-T5435_1 Global Drafts link in sidebar should be hidden when another use
try {
await adminClient.deletePost(adminPost.id);
} catch (error) {
throw new Error('Failed to delete post by admin');
throw new Error(`Failed to delete post by admin: ${error}`);
}
// # Open the last post in the channel sent by admin again

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

@@ -17,16 +17,42 @@ test('Search box suggestion must be case insensitive', async ({pw, pages}) => {
// # Open the search UI
await channelsPage.globalHeader.openSearch();
const searchInput = await page.getByPlaceholder('Search messages');
const searchWord = 'off';
const searchOutput = 'In:off-topic';
const channelName = 'Off-Topic';
// * it's working when using lowercase
await searchInput.fill('In:off');
await searchInput.press('Enter');
await expect(searchInput).toHaveValue('In:off-topic ');
// Should work as expected when using lowercase
// # Type in lowercase "off" to search for the "Off-Topic" channel
const {searchInput} = await channelsPage.searchPopover;
await searchInput.pressSequentially(`In:${searchWord}`);
// * it's working when using uppercase
await searchInput.clear();
await searchInput.fill('In:Off');
// * The suggestion should be visible
await expect(channelsPage.searchPopover.selectedSuggestion).toBeVisible();
await expect(channelsPage.searchPopover.selectedSuggestion).toHaveText(channelName);
// # Press Enter to select the suggestion and another Enter to search
await searchInput.press('Enter');
await expect(searchInput).toHaveValue('In:off-topic ');
await searchInput.press('Enter');
// * The search box should contain the selected suggestion
await expect(channelsPage.globalHeader.searchBox.getByText(searchOutput, {exact: true})).toBeVisible();
// Should work as expected when using uppercase
// # Close then open the search UI
await channelsPage.globalHeader.closeSearch();
await channelsPage.globalHeader.openSearch();
// # Type in uppercase "OFF" to search for the "Off-Topic" channel
await searchInput.pressSequentially(`In:${searchWord.toUpperCase()}`);
// * The suggestion should be visible
await expect(channelsPage.searchPopover.selectedSuggestion).toBeVisible();
await expect(channelsPage.searchPopover.selectedSuggestion).toHaveText(channelName);
// # Press Enter to select the suggestion and another Enter to search
await searchInput.press('Enter');
await searchInput.press('Enter');
// * The search box should contain the selected suggestion
await expect(channelsPage.globalHeader.searchBox.getByText(searchOutput, {exact: true})).toBeVisible();
});

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

@@ -8,15 +8,8 @@ async function interceptConfigWithLandingPage(page: Page, enabled: boolean) {
route.fulfill({
status: 200,
body: JSON.stringify({
EmailLoginButtonBorderColor: '#2389D7',
EmailLoginButtonColor: '#0000',
EmailLoginButtonTextColor: '#2389D7',
EnableDesktopLandingPage: enabled,
EnableSignInWithEmail: 'true',
EnableSignInWithUsername: 'true',
EnableSignUpWithEmail: 'true',
SiteName: 'Mattermost',
SiteURL: 'http://localhost:8065',
}),
headers: {'Content-Type': 'application/json'},
});
@@ -29,7 +22,7 @@ test('MM-T5640_1 should not see landing page ', async ({pw, pages, page}) => {
await interceptConfigWithLandingPage(page, false);
// Navigate to your starting URL
await page.goto('http://localhost:8065');
await page.goto('/');
// Wait until the URL contains '/login'
await page.waitForURL(/.*\/login.*/);
@@ -44,11 +37,11 @@ test('MM-T5640_1 should not see landing page ', async ({pw, pages, page}) => {
test('MM-T5640_2 should see landing page', async ({pages, isMobile, page}) => {
// Navigate to your starting URL
await page.goto('http://localhost:8065');
await page.goto('/');
await page.evaluate(() => localStorage.clear());
await page.goto('http://localhost:8065');
await page.goto('/');
// Wait until the URL contains '/landing'
await page.waitForURL(/.*\/landing.*/);

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

@@ -7,6 +7,7 @@ test('/login', async ({pw, pages, page, browserName, viewport}, testInfo) => {
// Go to login page
const {adminClient} = await pw.getAdminClient();
const adminConfig = await adminClient.getConfig();
const license = await adminClient.getClientLicenseOld();
const loginPage = new pages.LoginPage(page, adminConfig);
await loginPage.goto();
await loginPage.toBeVisible();
@@ -16,7 +17,8 @@ test('/login', async ({pw, pages, page, browserName, viewport}, testInfo) => {
// Match snapshot of login page
const testArgs = {page, browserName, viewport};
await pw.matchSnapshot(testInfo, testArgs);
const editionSuffix = license.IsLicensed === 'true' ? '' : 'free edition';
await pw.matchSnapshot({...testInfo, title: `${testInfo.title} ${editionSuffix}`}, testArgs);
// Click sign in button without entering user credential
await loginPage.signInButton.click();
@@ -24,5 +26,5 @@ test('/login', async ({pw, pages, page, browserName, viewport}, testInfo) => {
await pw.waitForAnimationEnd(loginPage.bodyCard);
// Match snapshot of login page with error
await pw.matchSnapshot({...testInfo, title: `${testInfo.title} error`}, testArgs);
await pw.matchSnapshot({...testInfo, title: `${testInfo.title} error ${editionSuffix}`}, testArgs);
});

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 138 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 275 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 125 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 139 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 280 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 120 KiB

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

@@ -7,6 +7,7 @@ test('/signup_email', async ({pw, pages, page, browserName, viewport}, testInfo)
// Go to login page
const {adminClient} = await pw.getAdminClient();
const adminConfig = await adminClient.getConfig();
const license = await adminClient.getClientLicenseOld();
const loginPage = new pages.LoginPage(page, adminConfig);
await loginPage.goto();
await loginPage.toBeVisible();
@@ -23,7 +24,8 @@ test('/signup_email', async ({pw, pages, page, browserName, viewport}, testInfo)
// Match snapshot of signup_email page
const testArgs = {page, browserName, viewport};
await pw.matchSnapshot(testInfo, testArgs);
const editionSuffix = license.IsLicensed === 'true' ? '' : 'free edition';
await pw.matchSnapshot({...testInfo, title: `${testInfo.title} ${editionSuffix}`}, testArgs);
// Click sign in button without entering user credential
const invalidUser = {email: 'invalid', username: 'a', password: 'b'};
@@ -34,5 +36,5 @@ test('/signup_email', async ({pw, pages, page, browserName, viewport}, testInfo)
await pw.waitForAnimationEnd(signupPage.bodyCard);
// Match snapshot of signup_email page
await pw.matchSnapshot({...testInfo, title: `${testInfo.title} error`}, testArgs);
await pw.matchSnapshot({...testInfo, title: `${testInfo.title} error ${editionSuffix}`}, testArgs);
});

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 159 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 301 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 161 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 153 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 296 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 155 KiB

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

@@ -225,6 +225,7 @@ const SearchBox = forwardRef(
role='searchbox'
>
<CloseIcon
data-testid='searchBoxClose'
className='btn btn-icon btn-m'
onClick={closeHandler}
>

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

@@ -400,6 +400,7 @@ export type ServiceSettings = {
EnableAPIPostDeletion: boolean;
EnableDesktopLandingPage: boolean;
MaximumURLLength: number;
ScheduledPosts: boolean;
};
export type TeamSettings = {