Skip to content

Commit

Permalink
feat(e2e): Converting cy to pw general.test.ts (#15882)
Browse files Browse the repository at this point in the history
* feat(e2e): Converting cy to pw general.test.ts

* fix(e2e): Adds retry mechanism for dropdown options in setting tests

* feat(e2e): Start attaching electron traces to the report

* feat(e2e): Electron test videos are not added to reports
  • Loading branch information
Vere-Grey authored Dec 11, 2024
1 parent 5efac5a commit 631f150
Show file tree
Hide file tree
Showing 9 changed files with 237 additions and 152 deletions.
4 changes: 4 additions & 0 deletions docs/tests/e2e-playwright-suite.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,14 @@ Steps:

1. **To run one group** you can do: `yarn workspace @trezor/suite-desktop-core test:e2e:web --grep=@group=wallet`

1. **To check for flakiness** you can specify test/suite and how many time it should run: `yarn workspace @trezor/suite-desktop-core test:e2e:web general/wallet-discovery.test.ts --repeat-each=10`

1. **To debug test** add `await window.pause();` to place where you want test to stop. Debugger window will open.

1. **To enable Debug Tools in the browser** press `Ctrl+Shift+I`

1. **To enable Electron verbose logging** add env variable LOGLEVEL=debug or any other level

## Contribution

Please follow our general [Playwright contribution guide](e2e-playwright-contribution-guide.md)
Expand Down
35 changes: 35 additions & 0 deletions packages/suite-desktop-core/e2e/support/analytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Page } from '@playwright/test';

import { SuiteAnalyticsEvent } from '@trezor/suite-analytics';
import { Requests, EventPayload } from '@trezor/suite-web/e2e/support/types';
import { urlSearchParams } from '@trezor/suite/src/utils/suite/metadata';

export class AnalyticsFixture {
private window: Page;
requests: Requests = [];

constructor(window: Page) {
this.window = window;
}

//TODO: #15811 To be refactored
findAnalyticsEventByType = <T extends SuiteAnalyticsEvent>(eventType: T['type']) => {
const event = this.requests.find(req => req.c_type === eventType) as EventPayload<T>;

if (!event) {
throw new Error(`Event with type ${eventType} not found.`);
}

return event;
};

//TODO: #15811 To be refactored
async interceptAnalytics() {
await this.window.route('**://data.trezor.io/suite/log/**', route => {
const url = route.request().url();
const params = urlSearchParams(url);
this.requests.push(params);
route.continue();
});
}
}
29 changes: 22 additions & 7 deletions packages/suite-desktop-core/e2e/support/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { _electron as electron, TestInfo } from '@playwright/test';
import path from 'path';
import fse from 'fs-extra';
import { readdirSync, removeSync } from 'fs-extra';

import { TrezorUserEnvLink } from '@trezor/trezor-user-env-link';

Expand All @@ -19,6 +19,7 @@ type LaunchSuiteParams = {
bridgeDaemon?: boolean;
locale?: string;
colorScheme?: 'light' | 'dark' | 'no-preference' | null | undefined;
videoFolder?: string;
};

const formatErrorLogMessage = (data: string) => {
Expand Down Expand Up @@ -54,28 +55,29 @@ export const launchSuiteElectronApp = async (params: LaunchSuiteParams = {}) =>
],
colorScheme: params.colorScheme,
locale: params.locale,
// when testing electron, video needs to be setup like this. it works locally but not in docker
// recordVideo: { dir: 'test-results' },
...(params.videoFolder && {
recordVideo: { dir: params.videoFolder, size: { width: 1280, height: 720 } },
}),
});

const localDataDir = await electronApp.evaluate(({ app }) => app.getPath('userData'));

if (options.rmUserData) {
const filesToDelete = fse.readdirSync(localDataDir);
const filesToDelete = readdirSync(localDataDir);
filesToDelete.forEach(file => {
// omitting Cache folder it sometimes prevents the deletion and is not necessary to delete for test idempotency
if (file !== 'Cache') {
try {
fse.removeSync(`${localDataDir}/${file}`);
removeSync(`${localDataDir}/${file}`);
} catch {
// If files does not exist do nothing.
}
}
});
}

// #15670 Bug in desktop app that loglevel is ignored
if (process.env.LOGLEVEL || process.env.GITHUB_ACTION) {
// #15670 Bug in desktop app that loglevel is ignored so we conditionally don't log to stdout
if (process.env.LOGLEVEL) {
electronApp.process().stdout?.on('data', data => console.log(data.toString()));
}
electronApp
Expand Down Expand Up @@ -113,3 +115,16 @@ export const getApiUrl = (webBaseUrl: string | undefined, testInfo: TestInfo) =>

return apiURL;
};

export const getElectronVideoPath = (videoFolder: string) => {
const videoFilenames = readdirSync(videoFolder).filter(file => file.endsWith('.webm'));
if (videoFilenames.length > 1) {
console.error(
formatErrorLogMessage(
`Warning: More than one electron video file found in the output directory: ${videoFolder}\nAttaching only the first one: ${videoFilenames[0]}`,
),
);
}

return path.join(videoFolder, videoFilenames[0]);
};
26 changes: 23 additions & 3 deletions packages/suite-desktop-core/e2e/support/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,19 @@ import {
} from '@trezor/trezor-user-env-link';

import { DashboardActions } from './pageActions/dashboardActions';
import { getApiUrl, launchSuite } from './common';
import { getApiUrl, getElectronVideoPath, launchSuite } from './common';
import { SettingsActions } from './pageActions/settingsActions';
import { SuiteGuide } from './pageActions/suiteGuideActions';
import { WalletActions } from './pageActions/walletActions';
import { OnboardingActions } from './pageActions/onboardingActions';
import { PlaywrightProjects } from '../playwright.config';
import { AnalyticsFixture } from './analytics';

type Fixtures = {
apiURL: string;
startEmulator: boolean;
emulatorStartConf: StartEmu;
emulatorSetupConf: SetupEmu;
apiURL: string;
trezorUserEnvLink: TrezorUserEnvLinkClass;
appContext: ElectronApplication | undefined;
window: Page;
Expand All @@ -29,6 +30,7 @@ type Fixtures = {
suiteGuidePage: SuiteGuide;
walletPage: WalletActions;
onboardingPage: OnboardingActions;
analytics: AnalyticsFixture;
};

const test = base.extend<Fixtures>({
Expand Down Expand Up @@ -64,7 +66,11 @@ const test = base.extend<Fixtures>({
}

if (testInfo.project.name === PlaywrightProjects.Desktop) {
const suite = await launchSuite({ locale, colorScheme });
const suite = await launchSuite({
locale,
colorScheme,
videoFolder: testInfo.outputDir,
});
await use(suite.electronApp);
await suite.electronApp.close(); // Ensure cleanup after tests
} else {
Expand All @@ -82,6 +88,16 @@ const test = base.extend<Fixtures>({
await use(window);
const tracePath = `${testInfo.outputDir}/trace.electron.zip`;
await window.context().tracing.stop({ path: tracePath });
testInfo.attachments.push({
name: 'trace',
path: tracePath,
contentType: 'application/zip',
});
testInfo.attachments.push({
name: 'video',
path: getElectronVideoPath(testInfo.outputDir),
contentType: 'video/webm',
});
} else {
await page.context().addInitScript(() => {
// Tells the app to attach Redux Store to window object. packages/suite-web/src/support/useCypress.ts
Expand Down Expand Up @@ -115,6 +131,10 @@ const test = base.extend<Fixtures>({
);
await use(onboardingPage);
},
analytics: async ({ window }, use) => {
const analytics = new AnalyticsFixture(window);
await use(analytics);
},
});

export { test };
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
import { Locator, Page } from '@playwright/test';
import { Locator, Page, test } from '@playwright/test';

import { BackendType, NetworkSymbol } from '@suite-common/wallet-config';
import { capitalizeFirstLetter } from '@trezor/utils';

import { expect } from '../customMatchers';

export enum Theme {
System = 'system',
Dark = 'dark',
Light = 'light',
}

export enum Language {
Spanish = 'es',
}

const languageMap = {
es: 'Español',
};

export class SettingsActions {
private readonly window: Page;
private readonly TIMES_CLICK_TO_SET_DEBUG_MODE = 5;
Expand All @@ -29,6 +44,12 @@ export class SettingsActions {
this.window.getByTestId(`@settings/advance/${backend}`);
readonly coinAddressInput: Locator;
readonly coinAdvanceSettingSaveButton: Locator;
readonly themeInput: Locator;
readonly themeInputOption = (theme: Theme) =>
this.window.getByTestId(`@theme/color-scheme-select/option/${theme}`);
readonly languageInput: Locator;
readonly languageInputOption = (language: Language) =>
this.window.getByTestId(`@settings/language-select/option/${language}`);

constructor(window: Page) {
this.window = window;
Expand All @@ -54,6 +75,8 @@ export class SettingsActions {
this.coinAdvanceSettingSaveButton = this.window.getByTestId(
'@settings/advance/button/save',
);
this.themeInput = this.window.getByTestId('@theme/color-scheme-select/input');
this.languageInput = this.window.getByTestId('@settings/language-select/input');
}

async navigateTo() {
Expand Down Expand Up @@ -109,4 +132,31 @@ export class SettingsActions {
await this.settingsCloseButton.click();
await this.settingsHeader.waitFor({ state: 'detached' });
}

async changeTheme(theme: Theme) {
await this.selectDropdownOptionWithRetry(this.themeInput, this.themeInputOption(theme));
await expect(this.themeInput).toHaveText(capitalizeFirstLetter(theme));
}

async changeLanguage(language: Language) {
await this.selectDropdownOptionWithRetry(
this.languageInput,
this.languageInputOption(language),
);
await expect(this.languageInput).toHaveText(languageMap[language]);
}

//Retry mechanism for settings dropdowns which tend to be flaky in automation
async selectDropdownOptionWithRetry(dropdown: Locator, option: Locator) {
await test.step('Select dropdown option with RETRY', async () => {
await dropdown.scrollIntoViewIfNeeded();
await expect(async () => {
if (!(await option.isVisible())) {
await dropdown.click({ timeout: 2000 });
}
await expect(option).toBeVisible({ timeout: 2000 });
await option.click({ timeout: 2000 });
}).toPass({ timeout: 10_000 });
});
}
}
38 changes: 5 additions & 33 deletions packages/suite-desktop-core/e2e/tests/settings/coins.test.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,15 @@
import { SuiteAnalyticsEvent } from '@trezor/suite-analytics';
import { Requests, EventPayload } from '@trezor/suite-web/e2e/support/types';
import { urlSearchParams } from '@trezor/suite/src/utils/suite/metadata';
import { NetworkSymbol } from '@suite-common/wallet-config';

import { test, expect } from '../../support/fixtures';

//TODO: #15811 To be refactored
export const findAnalyticsEventByType = <T extends SuiteAnalyticsEvent>(
requests: Requests,
eventType: T['type'],
) => {
const event = requests.find(req => req.c_type === eventType) as EventPayload<T>;

if (!event) {
throw new Error(`Event with type ${eventType} not found.`);
}

return event;
};

let requests: Requests;

test.describe('Coin Settings', { tag: ['@group=settings'] }, () => {
test.use({ emulatorStartConf: { wipe: true } });
test.beforeEach(async ({ onboardingPage, dashboardPage, settingsPage }) => {
test.beforeEach(async ({ analytics, onboardingPage, dashboardPage, settingsPage }) => {
await onboardingPage.completeOnboarding();
await dashboardPage.discoveryShouldFinish();
await settingsPage.navigateTo();
await settingsPage.coinsTabButton.click();

requests = [];

//TODO: #15811 To be refactored
await onboardingPage.window.route('**://data.trezor.io/suite/log/**', route => {
const url = route.request().url();
const params = urlSearchParams(url);
requests.push(params);
route.continue();
});
await analytics.interceptAnalytics();
});

test('go to wallet settings page, check BTC, activate all coins, deactivate all coins, set custom backend', async ({
Expand Down Expand Up @@ -89,9 +61,9 @@ test.describe('Coin Settings', { tag: ['@group=settings'] }, () => {
}

//TODO: #15811 this is just not useful validation. To be refactored
// const settingsCoinsEvent = findAnalyticsEventByType<
// const settingsCoinsEvent = analytics.findAnalyticsEventByType<
// ExtractByEventType<EventType.SettingsCoins>
// >(requests, EventType.SettingsCoins);
// >(EventType.SettingsCoins);
// expect(settingsCoinsEvent.symbol).to.be.oneOf(['btc', ...defaultUnchecked]);
// expect(settingsCoinsEvent.value).to.be.oneOf(['true', 'false']);

Expand All @@ -104,7 +76,7 @@ test.describe('Coin Settings', { tag: ['@group=settings'] }, () => {
await page.getByTestId('@settings/advance/button/save').click();

//TODO: #15811 this is just not useful validation. To be refactored
// const settingsCoinsBackendEvent = findAnalyticsEventByType<ExtractByEventType<EventType.SettingsCoinsBackend>>(requests, EventType.SettingsCoinsBackend)
// const settingsCoinsBackendEvent = analytics.findAnalyticsEventByType<ExtractByEventType<EventType.SettingsCoinsBackend>>(EventType.SettingsCoinsBackend)
// expect(settingsCoinsBackendEvent.type).to.equal('blockbook');
// expect(settingsCoinsBackendEvent.totalRegular).to.equal('1');
// expect(settingsCoinsBackendEvent.totalOnion).to.equal('0');
Expand Down
Loading

0 comments on commit 631f150

Please sign in to comment.