diff --git a/packages/suite-desktop-core/e2e/support/common.ts b/packages/suite-desktop-core/e2e/support/common.ts index ce54bf41418..5da7dd3a296 100644 --- a/packages/suite-desktop-core/e2e/support/common.ts +++ b/packages/suite-desktop-core/e2e/support/common.ts @@ -1,113 +1,11 @@ -/* eslint-disable no-console */ - -import test, { TestInfo, _electron as electron } from '@playwright/test'; -import { readdirSync, removeSync } from 'fs-extra'; +import test, { TestInfo } from '@playwright/test'; import { isEqual, omit } from 'lodash'; -import path from 'path'; import { TrezorUserEnvLink } from '@trezor/trezor-user-env-link'; import { splitStringEveryNCharacters } from '@trezor/utils'; import { PlaywrightProjects } from '../playwright.config'; -// specific version of legacy bridge is requested & expected -export const LEGACY_BRIDGE_VERSION = '2.0.33'; -const disableHashCheckArgument = '--state.suite.settings.isFirmwareHashCheckDisabled=true'; -const showDebugMenuArgument = `--state.suite.settings.debug.showDebugMenu=true`; - -type LaunchSuiteParams = { - rmUserData?: boolean; - bridgeLegacyTest?: boolean; - bridgeDaemon?: boolean; - locale?: string; - colorScheme?: 'light' | 'dark' | 'no-preference' | null | undefined; - videoFolder: string; - viewport: { width: number; height: number }; -}; - -const formatErrorLogMessage = (data: string) => { - const red = '\x1b[31m'; - const reset = '\x1b[0m'; - - return `${red}${data}${reset}`; -}; - -export const launchSuiteElectronApp = async (params: LaunchSuiteParams) => { - const defaultParams = { - rmUserData: true, - bridgeLegacyTest: true, - bridgeDaemon: false, - }; - const options = Object.assign(defaultParams, params); - - const appDir = path.join(__dirname, '../../../suite-desktop'); - const logLevelArgument = `--log-level=${process.env.LOGLEVEL ?? 'error'}`; - const viewportArgument = `--width=${options.viewport.width} --height=${options.viewport.height}`; - if (!options.bridgeDaemon) { - // TODO: #15646 Find out why currently pw fails to see node-bridge so we default to legacy bridge. - await TrezorUserEnvLink.startBridge(LEGACY_BRIDGE_VERSION); - } - const electronApp = await electron.launch({ - cwd: appDir, - args: [ - path.join(appDir, './dist/app.js'), - disableHashCheckArgument, - showDebugMenuArgument, - viewportArgument, - logLevelArgument, - ...(options.bridgeLegacyTest ? ['--bridge-legacy', '--bridge-test'] : []), - ...(options.bridgeDaemon ? ['--bridge-daemon', '--skip-new-bridge-rollout'] : []), - ], - colorScheme: params.colorScheme, - locale: params.locale, - recordVideo: { dir: options.videoFolder, size: options.viewport }, - }); - - const localDataDir = await electronApp.evaluate(({ app }) => app.getPath('userData')); - - if (options.rmUserData) { - 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 { - removeSync(`${localDataDir}/${file}`); - } catch { - // If files does not exist do nothing. - } - } - }); - } - - // #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 - .process() - .stderr?.on('data', data => console.error(formatErrorLogMessage(data.toString()))); - - await electronApp.evaluate( - (_, [resourcesPath]) => { - // This runs in the main Electron process. - // override global variable defined in app.ts - global.resourcesPath = resourcesPath; - - return global.resourcesPath; - }, - [path.join(appDir, 'build/static')], - ); - - return electronApp; -}; - -export const launchSuite = async (params: LaunchSuiteParams) => { - const electronApp = await launchSuiteElectronApp(params); - const window = await electronApp.firstWindow(); - - return { electronApp, window }; -}; - export const isDesktopProject = (testInfo: TestInfo) => testInfo.project.name === PlaywrightProjects.Desktop; @@ -124,19 +22,6 @@ export const getUrl = (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]); -}; - // Wraps whole page object methods with test.step export function step(stepName?: string) { /* eslint-disable @typescript-eslint/no-unsafe-function-type */ diff --git a/packages/suite-desktop-core/e2e/support/electron.ts b/packages/suite-desktop-core/e2e/support/electron.ts new file mode 100644 index 00000000000..928442829ca --- /dev/null +++ b/packages/suite-desktop-core/e2e/support/electron.ts @@ -0,0 +1,123 @@ +/* eslint-disable no-console */ + +import { ElectronApplication, Page, _electron as electron } from '@playwright/test'; +import { readdirSync, removeSync } from 'fs-extra'; +import path from 'path'; + +import { TrezorUserEnvLink } from '@trezor/trezor-user-env-link'; + +// specific version of legacy bridge is requested & expected +export const LEGACY_BRIDGE_VERSION = '2.0.33'; +const disableHashCheckArgument = '--state.suite.settings.isFirmwareHashCheckDisabled=true'; +const showDebugMenuArgument = `--state.suite.settings.debug.showDebugMenu=true`; + +type LaunchSuiteParams = { + rmUserData?: boolean; + bridgeLegacyTest?: boolean; + bridgeDaemon?: boolean; + locale?: string; + colorScheme?: 'light' | 'dark' | 'no-preference' | null | undefined; + videoFolder: string; + viewport: { width: number; height: number }; +}; + +export type Suite = { + electronApp: ElectronApplication; + window: Page; +}; + +const formatErrorLogMessage = (data: string) => { + const red = '\x1b[31m'; + const reset = '\x1b[0m'; + + return `${red}${data}${reset}`; +}; + +export const launchSuiteElectronApp = async (params: LaunchSuiteParams) => { + const defaultParams = { + rmUserData: true, + bridgeLegacyTest: true, + bridgeDaemon: false, + }; + const options = Object.assign(defaultParams, params); + + const appDir = path.join(__dirname, '../../../suite-desktop'); + const logLevelArgument = `--log-level=${process.env.LOGLEVEL ?? 'error'}`; + const viewportArgument = `--width=${options.viewport.width} --height=${options.viewport.height}`; + if (!options.bridgeDaemon) { + // TODO: #15646 Find out why currently pw fails to see node-bridge so we default to legacy bridge. + await TrezorUserEnvLink.startBridge(LEGACY_BRIDGE_VERSION); + } + const electronApp = await electron.launch({ + cwd: appDir, + args: [ + path.join(appDir, './dist/app.js'), + disableHashCheckArgument, + showDebugMenuArgument, + viewportArgument, + logLevelArgument, + ...(options.bridgeLegacyTest ? ['--bridge-legacy', '--bridge-test'] : []), + ...(options.bridgeDaemon ? ['--bridge-daemon', '--skip-new-bridge-rollout'] : []), + ], + colorScheme: params.colorScheme, + locale: params.locale, + recordVideo: { dir: options.videoFolder, size: options.viewport }, + }); + + const localDataDir = await electronApp.evaluate(({ app }) => app.getPath('userData')); + + if (options.rmUserData) { + 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 { + removeSync(`${localDataDir}/${file}`); + } catch { + // If files does not exist do nothing. + } + } + }); + } + + // #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 + .process() + .stderr?.on('data', data => console.error(formatErrorLogMessage(data.toString()))); + + await electronApp.evaluate( + (_, [resourcesPath]) => { + // This runs in the main Electron process. + // override global variable defined in app.ts + global.resourcesPath = resourcesPath; + + return global.resourcesPath; + }, + [path.join(appDir, 'build/static')], + ); + + return electronApp; +}; + +export const launchSuite = async (params: LaunchSuiteParams): Promise => { + const electronApp = await launchSuiteElectronApp(params); + const window = await electronApp.firstWindow(); + + return { electronApp, window }; +}; + +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]); +}; diff --git a/packages/suite-desktop-core/e2e/support/fixtures.ts b/packages/suite-desktop-core/e2e/support/fixtures.ts index ad210b9310d..feed004d006 100644 --- a/packages/suite-desktop-core/e2e/support/fixtures.ts +++ b/packages/suite-desktop-core/e2e/support/fixtures.ts @@ -1,21 +1,5 @@ /* eslint-disable react-hooks/rules-of-hooks */ -import { Page, test as base } from '@playwright/test'; - -import { - type Model, - type SetupEmu, - type StartEmu, - type TrezorUserEnvLinkClass, -} from '@trezor/trezor-user-env-link'; - import { AnalyticsFixture } from './analytics'; -import { - TrezorUserEnvLinkProxy, - getElectronVideoPath, - getUrl, - isDesktopProject, - launchSuite, -} from './common'; import { IndexedDbFixture } from './indexedDb'; import { BlockbookMock } from './mocks/blockBookMock'; import { MetadataProviderMock } from './mocks/metadataProviderMock'; @@ -32,18 +16,9 @@ import { SettingsActions } from './pageActions/settings/settingsActions'; import { SuiteGuide } from './pageActions/suiteGuideActions'; import { TrezorInputActions } from './pageActions/trezorInputActions'; import { WalletActions } from './pageActions/walletActions'; - -type StartEmuModelRequired = StartEmu & { model: Model }; +import { suiteBaseTest } from './testExtends/suiteBaseFixture'; type Fixtures = { - startEmulator: boolean; - setupEmulator: boolean; - emulatorStartConf: StartEmuModelRequired; - emulatorSetupConf: SetupEmu; - url: string; - trezorUserEnvLink: TrezorUserEnvLinkClass; - electronWindow: Page | undefined; - page: Page; dashboardPage: DashboardActions; settingsPage: SettingsActions; suiteGuidePage: SuiteGuide; @@ -61,164 +36,67 @@ type Fixtures = { metadataProviderMock: MetadataProviderMock; blockbookMock: BlockbookMock; tradingMock: TradingMock; - exceptionLogger: void; }; -const test = base.extend({ - startEmulator: true, - setupEmulator: true, - emulatorStartConf: { model: 'T3T1', wipe: true }, - emulatorSetupConf: {}, - /* eslint-disable-next-line no-empty-pattern */ - url: async ({}, use, testInfo) => { - await use(getUrl(testInfo)); - }, - /* eslint-disable-next-line no-empty-pattern */ - trezorUserEnvLink: async ({}, use) => { - await use(TrezorUserEnvLinkProxy); - }, - electronWindow: async ( - { - trezorUserEnvLink, - startEmulator, - setupEmulator, - emulatorStartConf, - emulatorSetupConf, - locale, - colorScheme, - }, - use, - testInfo, - ) => { - // We need to ensure emulator is running before launching the suite - await trezorUserEnvLink.logTestDetails( - ` - - - STARTING TEST ${testInfo.titlePath.join(' - ')}`, - ); - await trezorUserEnvLink.stopBridge(); - await trezorUserEnvLink.stopEmu(); - await trezorUserEnvLink.connect(); - if (startEmulator) { - await trezorUserEnvLink.startEmu(emulatorStartConf); - } - - if (startEmulator && setupEmulator) { - await trezorUserEnvLink.setupEmu(emulatorSetupConf); - } - - if (isDesktopProject(testInfo)) { - const suite = await launchSuite({ - locale, - colorScheme, - videoFolder: testInfo.outputDir, - viewport: testInfo.project.use.viewport!, - }); - await use(suite.window); - await suite.electronApp.close(); // Ensure cleanup after tests - } else { - if (startEmulator) { - await trezorUserEnvLink.startBridge(); - } - await use(undefined); - } - await trezorUserEnvLink.logTestDetails( - ` - - - FINISHING TEST ${testInfo.titlePath.join(' - ')}`, - ); - }, - page: async ({ electronWindow, page: webPage }, use, testInfo) => { - if (electronWindow) { - await webPage.close(); // Close the default chromium page - await electronWindow.context().tracing.start({ screenshots: true, snapshots: true }); - await use(electronWindow); - const tracePath = `${testInfo.outputDir}/trace.electron.zip`; - await electronWindow.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 webPage.context().addInitScript(() => { - // Tells the app to attach Redux Store to window object. packages/suite-web/src/support/usePlaywright.ts - window.Playwright = true; - }); - await webPage.goto('./'); - await use(webPage); - } - }, +const test = suiteBaseTest.extend({ dashboardPage: async ({ page, devicePrompt }, use) => { - const dashboardPage = new DashboardActions(page, devicePrompt); - await use(dashboardPage); + await use(new DashboardActions(page, devicePrompt)); }, settingsPage: async ({ page, url }, use) => { - const settingsPage = new SettingsActions(page, url); - await use(settingsPage); + await use(new SettingsActions(page, url)); }, suiteGuidePage: async ({ page }, use) => { - const suiteGuidePage = new SuiteGuide(page); - await use(suiteGuidePage); + await use(new SuiteGuide(page)); }, walletPage: async ({ page }, use) => { - const walletPage = new WalletActions(page); - await use(walletPage); + await use(new WalletActions(page)); }, onboardingPage: async ( - { page, analyticsPage, devicePrompt, emulatorStartConf }, + { page, emulatorStartConf, devicePrompt, analyticsPage }, use, testInfo, ) => { - const onboardingPage = new OnboardingActions( - page, - analyticsPage, - devicePrompt, - emulatorStartConf.model, - testInfo, + await use( + new OnboardingActions( + page, + emulatorStartConf.model, + testInfo, + devicePrompt, + analyticsPage, + ), ); - await use(onboardingPage); }, analyticsPage: async ({ page }, use) => { - const analyticsPage = new AnalyticsActions(page); - await use(analyticsPage); + await use(new AnalyticsActions(page)); }, devicePrompt: async ({ page }, use) => { - const devicePromptActions = new DevicePromptActions(page); - await use(devicePromptActions); + await use(new DevicePromptActions(page)); }, recoveryPage: async ({ page }, use) => { - const recoveryPage = new RecoveryActions(page); - await use(recoveryPage); + await use(new RecoveryActions(page)); }, - marketPage: async ({ page }, use) => { - const marketPage = new MarketActions(page); - await use(marketPage); + marketPage: async ({ page, devicePrompt }, use) => { + await use(new MarketActions(page, devicePrompt)); }, assetsPage: async ({ page }, use) => { - const assetPage = new AssetsActions(page); - await use(assetPage); + await use(new AssetsActions(page)); }, metadataPage: async ({ page, devicePrompt }, use) => { - const metadataPage = new MetadataActions(page, devicePrompt); - await use(metadataPage); + await use(new MetadataActions(page, devicePrompt)); }, trezorInput: async ({ page }, use) => { - const trezorInput = new TrezorInputActions(page); - await use(trezorInput); + await use(new TrezorInputActions(page)); }, analytics: async ({ page }, use) => { - const analytics = new AnalyticsFixture(page); - await use(analytics); + await use(new AnalyticsFixture(page)); }, indexedDb: async ({ page }, use) => { - const indexedDb = new IndexedDbFixture(page); - await use(indexedDb); + await use(new IndexedDbFixture(page)); }, metadataProviderMock: async ({ page }, use) => { const metadataProviderMock = new MetadataProviderMock(page); await use(metadataProviderMock); + await metadataProviderMock.stop(); }, /* eslint-disable-next-line no-empty-pattern */ blockbookMock: async ({}, use) => { @@ -227,28 +105,9 @@ const test = base.extend({ blockbookMock.stop(); }, tradingMock: async ({ page }, use) => { - const tradingMock = new TradingMock(page); - await use(tradingMock); - }, - exceptionLogger: [ - async ({ page }, use) => { - const errors: Error[] = []; - page.on('pageerror', error => { - errors.push(error); - }); - - await use(); - - if (errors.length > 0) { - throw new Error( - `There was a JS exception during test run. - \n${errors.map(error => `${error.message}\n${error.stack}`).join('\n-----\n')}`, - ); - } - }, - { auto: true }, - ], + await use(new TradingMock(page)); + }, }); export { test }; -export { expect } from './customMatchers'; +export { expect } from './testExtends/customMatchers'; diff --git a/packages/suite-desktop-core/e2e/support/pageActions/marketActions.ts b/packages/suite-desktop-core/e2e/support/pageActions/marketActions.ts index 5d3191cf100..255666a1eab 100644 --- a/packages/suite-desktop-core/e2e/support/pageActions/marketActions.ts +++ b/packages/suite-desktop-core/e2e/support/pageActions/marketActions.ts @@ -6,9 +6,9 @@ import { NetworkSymbol } from '@suite-common/wallet-config'; import { buyQuotesBTC, invityEndpoint } from '../../fixtures/invity'; import { TrezorUserEnvLinkProxy, step } from '../common'; -import { expect } from '../customMatchers'; import { DevicePromptActions } from './devicePromptActions'; import { solanaUrlPattern } from '../mocks/tradingMock'; +import { expect } from '../testExtends/customMatchers'; const quoteProviderLocator = '@trading/offers/quote/provider'; const quoteAmountLocator = '@trading/offers/quote/crypto-amount'; @@ -53,8 +53,6 @@ function isAccountTabFilter(network: string): network is AccountTabFilter { } export class MarketActions { - devicePrompt: DevicePromptActions; - // Input and general readonly offerSpinner: Locator; readonly section: Locator; @@ -134,8 +132,10 @@ export class MarketActions { // Sell readonly sellBestOfferButton: Locator; - constructor(private page: Page) { - this.devicePrompt = new DevicePromptActions(page); + constructor( + private page: Page, + private readonly devicePrompt: DevicePromptActions, + ) { this.offerSpinner = this.page.getByTestId('@trading/offers/loading-spinner'); this.section = this.page.getByTestId('@trading'); this.form = this.page.getByTestId('@trading/form'); diff --git a/packages/suite-desktop-core/e2e/support/pageActions/onboarding/backupActions.ts b/packages/suite-desktop-core/e2e/support/pageActions/onboarding/backupActions.ts index 7b94a6eeb81..bbee6ace920 100644 --- a/packages/suite-desktop-core/e2e/support/pageActions/onboarding/backupActions.ts +++ b/packages/suite-desktop-core/e2e/support/pageActions/onboarding/backupActions.ts @@ -5,7 +5,7 @@ import { DevicePromptActions } from '../devicePromptActions'; export class BackupActions { readonly startButton: Locator; - readonly undertandWhatSeedIsCheckbox: Locator; + readonly understandWhatSeedIsCheckbox: Locator; readonly hasEnoughTimeCheckbox: Locator; readonly isInPrivateCheckbox: Locator; readonly wroteSeedProperlyCheckbox: Locator; @@ -18,7 +18,7 @@ export class BackupActions { private devicePrompt: DevicePromptActions, ) { this.startButton = page.getByTestId('@backup/start-button'); - this.undertandWhatSeedIsCheckbox = page.getByTestId( + this.understandWhatSeedIsCheckbox = page.getByTestId( '@backup/check-item/understands-what-seed-is', ); this.hasEnoughTimeCheckbox = page.getByTestId('@backup/check-item/has-enough-time'); diff --git a/packages/suite-desktop-core/e2e/support/pageActions/onboarding/onboardingActions.ts b/packages/suite-desktop-core/e2e/support/pageActions/onboarding/onboardingActions.ts index b2dc7461171..941a9e0d98b 100644 --- a/packages/suite-desktop-core/e2e/support/pageActions/onboarding/onboardingActions.ts +++ b/packages/suite-desktop-core/e2e/support/pageActions/onboarding/onboardingActions.ts @@ -42,10 +42,10 @@ export class OnboardingActions { constructor( public page: Page, - private analyticsPage: AnalyticsActions, - private readonly devicePrompt: DevicePromptActions, - private readonly model: Model, + readonly model: Model, private readonly testInfo: TestInfo, + private readonly devicePrompt: DevicePromptActions, + private readonly analyticsPage: AnalyticsActions, ) { this.backup = new BackupActions(page, devicePrompt); this.firmware = new FirmwareActions(page); diff --git a/packages/suite-desktop-core/e2e/support/pageActions/settings/coinActions.ts b/packages/suite-desktop-core/e2e/support/pageActions/settings/coinActions.ts index 34a3e6c9e2c..c4bb2b34c11 100644 --- a/packages/suite-desktop-core/e2e/support/pageActions/settings/coinActions.ts +++ b/packages/suite-desktop-core/e2e/support/pageActions/settings/coinActions.ts @@ -3,7 +3,7 @@ import { Locator, Page } from '@playwright/test'; import { BackendType, NetworkSymbol } from '@suite-common/wallet-config'; import { step } from '../../common'; -import { expect } from '../../customMatchers'; +import { expect } from '../../testExtends/customMatchers'; export class CoinsActions { readonly networkButton = (symbol: NetworkSymbol) => diff --git a/packages/suite-desktop-core/e2e/support/pageActions/settings/settingsActions.ts b/packages/suite-desktop-core/e2e/support/pageActions/settings/settingsActions.ts index 99e519082b7..0bcc6122e36 100644 --- a/packages/suite-desktop-core/e2e/support/pageActions/settings/settingsActions.ts +++ b/packages/suite-desktop-core/e2e/support/pageActions/settings/settingsActions.ts @@ -5,7 +5,7 @@ import { capitalizeFirstLetter } from '@trezor/utils'; import { CoinsActions } from './coinActions'; import { DeviceActions } from './deviceActions'; import { TrezorUserEnvLinkProxy, step } from '../../common'; -import { expect } from '../../customMatchers'; +import { expect } from '../../testExtends/customMatchers'; export enum Theme { System = 'system', diff --git a/packages/suite-desktop-core/e2e/support/pageActions/trezorInputActions.ts b/packages/suite-desktop-core/e2e/support/pageActions/trezorInputActions.ts index 8b52d1d5cba..3b7d444b2a4 100644 --- a/packages/suite-desktop-core/e2e/support/pageActions/trezorInputActions.ts +++ b/packages/suite-desktop-core/e2e/support/pageActions/trezorInputActions.ts @@ -1,7 +1,7 @@ import { Locator, Page, test } from '@playwright/test'; import { TrezorUserEnvLinkProxy, step } from '../common'; -import { expect } from '../customMatchers'; +import { expect } from '../testExtends/customMatchers'; export class TrezorInputActions { readonly wordSelectInput: Locator; diff --git a/packages/suite-desktop-core/e2e/support/customMatchers.ts b/packages/suite-desktop-core/e2e/support/testExtends/customMatchers.ts similarity index 98% rename from packages/suite-desktop-core/e2e/support/customMatchers.ts rename to packages/suite-desktop-core/e2e/support/testExtends/customMatchers.ts index 84259668908..a556e1a81f3 100644 --- a/packages/suite-desktop-core/e2e/support/customMatchers.ts +++ b/packages/suite-desktop-core/e2e/support/testExtends/customMatchers.ts @@ -1,7 +1,7 @@ import { Locator, Request, expect as baseExpect } from '@playwright/test'; import { diff } from 'jest-diff'; -import { isEqualWithOmit } from './common'; +import { isEqualWithOmit } from '../common'; const compareTextAndNumber = async ( locator: Locator, diff --git a/packages/suite-desktop-core/e2e/support/testExtends/suiteBaseFixture.ts b/packages/suite-desktop-core/e2e/support/testExtends/suiteBaseFixture.ts new file mode 100644 index 00000000000..a81801ca31d --- /dev/null +++ b/packages/suite-desktop-core/e2e/support/testExtends/suiteBaseFixture.ts @@ -0,0 +1,160 @@ +/* eslint-disable react-hooks/rules-of-hooks */ +import { BrowserContext, Page, TestInfo, test as base } from '@playwright/test'; + +import { Model, SetupEmu, StartEmu, TrezorUserEnvLinkClass } from '@trezor/trezor-user-env-link'; + +import { TrezorUserEnvLinkProxy, getUrl, isDesktopProject } from '../common'; +import { Suite, getElectronVideoPath, launchSuite } from '../electron'; + +type StartEmuModelRequired = StartEmu & { model: Model }; + +type suiteBaseFixture = { + startEmulator: boolean; + setupEmulator: boolean; + emulatorStartConf: StartEmuModelRequired; + emulatorSetupConf: SetupEmu; + url: string; + trezorUserEnvLink: TrezorUserEnvLinkClass; + page: Page; + exceptionLogger: void; +}; + +const electronSetup = async (testInfo: TestInfo, locale: string | undefined, colorScheme: any) => { + const suite = await launchSuite({ + locale, + colorScheme, + videoFolder: testInfo.outputDir, + viewport: testInfo.project.use.viewport!, + }); + + await suite.window.context().tracing.start({ screenshots: true, snapshots: true }); + + return suite; +}; + +const electronTeardown = async (suite: Suite, testInfo: TestInfo) => { + const tracePath = `${testInfo.outputDir}/trace.electron.zip`; + await suite.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', + }); + await suite.electronApp.close(); +}; + +const webSetup = async (browserContext: BrowserContext) => { + await TrezorUserEnvLinkProxy.startBridge(); + const page = await browserContext.newPage(); + // Tells the app to attach Redux Store to window object. packages/suite-web/src/support/usePlaywright.ts + // Which is needed for methods manupalating Redux store like onboardingActions.disableFirmwareHashCheck + await page.context().addInitScript(() => { + window.Playwright = true; + }); + await page.goto('./'); + + return page; +}; + +const trezorEnvSetup = async ( + testInfo: TestInfo, + startEmulator: boolean, + setupEmulator: boolean, + emulatorStartConf: StartEmu, + emulatorSetupConf: SetupEmu, +) => { + await TrezorUserEnvLinkProxy.logTestDetails( + ` - - - STARTING TEST ${testInfo.titlePath.join(' - ')}`, + ); + // We cannot rely on that previous teardown was done correctly + await TrezorUserEnvLinkProxy.stopBridge(); + await TrezorUserEnvLinkProxy.stopEmu(); + await TrezorUserEnvLinkProxy.connect(); + if (startEmulator) { + await TrezorUserEnvLinkProxy.startEmu(emulatorStartConf); + } + if (startEmulator && setupEmulator) { + await TrezorUserEnvLinkProxy.setupEmu(emulatorSetupConf); + } +}; + +// This is the base Suite text fixture containing all the necessary setup and core page object +// Depending on the project type (desktop or web) it will launch the appropriate environment +// and provide the necessary page object which is either electron window or web page +const suiteBaseTest = base.extend({ + startEmulator: true, + setupEmulator: true, + emulatorStartConf: { model: 'T3T1', wipe: true }, + emulatorSetupConf: {}, + /* eslint-disable-next-line no-empty-pattern */ + url: async ({}, use, testInfo) => { + await use(getUrl(testInfo)); + }, + /* eslint-disable-next-line no-empty-pattern */ + trezorUserEnvLink: async ({}, use) => { + await use(TrezorUserEnvLinkProxy); + }, + page: async ( + { + locale, + colorScheme, + browser, + startEmulator, + setupEmulator, + emulatorStartConf, + emulatorSetupConf, + }, + use, + testInfo, + ) => { + // This Trezor env setup needs to happen before electron or web page are launched + await trezorEnvSetup( + testInfo, + startEmulator, + setupEmulator, + emulatorStartConf, + emulatorSetupConf, + ); + + if (isDesktopProject(testInfo)) { + const suite = await electronSetup(testInfo, locale, colorScheme); + await use(suite.window); + await electronTeardown(suite, testInfo); + } else { + const browserContext = await browser.newContext(); + const page = await webSetup(browserContext); + await use(page); + await page.close(); + await browserContext.close(); + } + + await TrezorUserEnvLinkProxy.logTestDetails( + ` - - - FINISHING TEST ${testInfo.titlePath.join(' - ')}`, + ); + }, + exceptionLogger: [ + async ({ page }, use) => { + const errors: Error[] = []; + page.on('pageerror', error => { + errors.push(error); + }); + + await use(); + + if (errors.length > 0) { + throw new Error( + `There was a JS exception during test run. + \n${errors.map(error => `${error.message}\n${error.stack}`).join('\n-----\n')}`, + ); + } + }, + { auto: true }, + ], +}); + +export { suiteBaseTest }; diff --git a/packages/suite-desktop-core/e2e/tests/backup/t2t1-fail.test.ts b/packages/suite-desktop-core/e2e/tests/backup/t2t1-fail.test.ts index 5d7d54642ad..281d1b91b2d 100644 --- a/packages/suite-desktop-core/e2e/tests/backup/t2t1-fail.test.ts +++ b/packages/suite-desktop-core/e2e/tests/backup/t2t1-fail.test.ts @@ -24,7 +24,7 @@ test.describe('Backup fail', { tag: ['@group=device-management'] }, () => { trezorUserEnvLink, }) => { await dashboardPage.notificationNoBackupButton.click(); - await onboardingPage.backup.undertandWhatSeedIsCheckbox.click(); + await onboardingPage.backup.understandWhatSeedIsCheckbox.click(); await onboardingPage.backup.hasEnoughTimeCheckbox.click(); await onboardingPage.backup.isInPrivateCheckbox.click(); await onboardingPage.backup.startButton.click(); diff --git a/packages/suite-desktop-core/e2e/tests/backup/t2t1-misc.test.ts b/packages/suite-desktop-core/e2e/tests/backup/t2t1-misc.test.ts index 2f6dfe159f3..b0d1f4a6ea8 100644 --- a/packages/suite-desktop-core/e2e/tests/backup/t2t1-misc.test.ts +++ b/packages/suite-desktop-core/e2e/tests/backup/t2t1-misc.test.ts @@ -13,11 +13,11 @@ test.describe('Backup misc', { tag: ['@group=device-management'] }, () => { test('Backup should reset if modal is closed', async ({ onboardingPage, dashboardPage }) => { await dashboardPage.notificationNoBackupButton.click(); - await onboardingPage.backup.undertandWhatSeedIsCheckbox.click(); + await onboardingPage.backup.understandWhatSeedIsCheckbox.click(); await onboardingPage.backup.hasEnoughTimeCheckbox.click(); await onboardingPage.backup.isInPrivateCheckbox.click(); await expect( - onboardingPage.backup.undertandWhatSeedIsCheckbox.locator('input'), + onboardingPage.backup.understandWhatSeedIsCheckbox.locator('input'), ).toBeChecked(); await expect(onboardingPage.backup.hasEnoughTimeCheckbox.locator('input')).toBeChecked(); await expect(onboardingPage.backup.isInPrivateCheckbox.locator('input')).toBeChecked(); @@ -26,7 +26,7 @@ test.describe('Backup misc', { tag: ['@group=device-management'] }, () => { //at this moment, after modal was closed and opened again, no checkbox should be checked await expect( - onboardingPage.backup.undertandWhatSeedIsCheckbox.locator('input'), + onboardingPage.backup.understandWhatSeedIsCheckbox.locator('input'), ).not.toBeChecked(); await expect( onboardingPage.backup.hasEnoughTimeCheckbox.locator('input'), @@ -44,7 +44,7 @@ test.describe('Backup misc', { tag: ['@group=device-management'] }, () => { await dashboardPage.openDeviceSwitcher(); await dashboardPage.walletAtIndex(0).click(); await dashboardPage.notificationNoBackupButton.click(); - await onboardingPage.backup.undertandWhatSeedIsCheckbox.click(); + await onboardingPage.backup.understandWhatSeedIsCheckbox.click(); await onboardingPage.backup.hasEnoughTimeCheckbox.click(); await onboardingPage.backup.isInPrivateCheckbox.click(); diff --git a/packages/suite-desktop-core/e2e/tests/backup/t2t1-success.test.ts b/packages/suite-desktop-core/e2e/tests/backup/t2t1-success.test.ts index ed8c9e790aa..a04f57d2966 100644 --- a/packages/suite-desktop-core/e2e/tests/backup/t2t1-success.test.ts +++ b/packages/suite-desktop-core/e2e/tests/backup/t2t1-success.test.ts @@ -24,7 +24,7 @@ test.describe('Backup success', { tag: ['@group=device-management'] }, () => { // access from notification await dashboardPage.notificationNoBackupButton.click(); - await onboardingPage.backup.undertandWhatSeedIsCheckbox.click(); + await onboardingPage.backup.understandWhatSeedIsCheckbox.click(); await onboardingPage.backup.hasEnoughTimeCheckbox.click(); await onboardingPage.backup.isInPrivateCheckbox.click(); diff --git a/packages/suite-desktop-core/e2e/tests/bridge-tor/spawn-bridge-daemon.test.ts b/packages/suite-desktop-core/e2e/tests/bridge-tor/spawn-bridge-daemon.test.ts index 1d5b7d3b8cf..7e98e69a8a3 100644 --- a/packages/suite-desktop-core/e2e/tests/bridge-tor/spawn-bridge-daemon.test.ts +++ b/packages/suite-desktop-core/e2e/tests/bridge-tor/spawn-bridge-daemon.test.ts @@ -3,7 +3,8 @@ import { expectBridgeToBeStopped, waitForAppToBeInitialized, } from '../../support/bridge'; -import { launchSuite, launchSuiteElectronApp, skipFixture } from '../../support/common'; +import { skipFixture } from '../../support/common'; +import { launchSuite, launchSuiteElectronApp } from '../../support/electron'; import { expect, test } from '../../support/fixtures'; test.use({ exceptionLogger: skipFixture }); diff --git a/packages/suite-desktop-core/e2e/tests/bridge-tor/spawn-bridge.test.ts b/packages/suite-desktop-core/e2e/tests/bridge-tor/spawn-bridge.test.ts index 839e307a27f..203e67c01e6 100644 --- a/packages/suite-desktop-core/e2e/tests/bridge-tor/spawn-bridge.test.ts +++ b/packages/suite-desktop-core/e2e/tests/bridge-tor/spawn-bridge.test.ts @@ -4,7 +4,8 @@ import { expectBridgeToBeStopped, waitForAppToBeInitialized, } from '../../support/bridge'; -import { LEGACY_BRIDGE_VERSION, launchSuite, skipFixture } from '../../support/common'; +import { skipFixture } from '../../support/common'; +import { LEGACY_BRIDGE_VERSION, launchSuite } from '../../support/electron'; import { expect, test } from '../../support/fixtures'; import { AnalyticsActions } from '../../support/pageActions/analyticsActions'; import { DevicePromptActions } from '../../support/pageActions/devicePromptActions'; @@ -67,10 +68,10 @@ test.describe.serial('Bridge', { tag: ['@group=suite', '@desktopOnly'] }, () => const onboardingPage = new OnboardingActions( suite.window, - new AnalyticsActions(suite.window), - devicePrompt, trezorUserEnvLink.defaultModel, testInfo, + devicePrompt, + new AnalyticsActions(suite.window), ); await onboardingPage.completeOnboarding(); diff --git a/packages/suite-desktop-core/e2e/tests/bridge-tor/spawn-tor.test.ts b/packages/suite-desktop-core/e2e/tests/bridge-tor/spawn-tor.test.ts index 57bbb9dd35a..4ae705cf754 100644 --- a/packages/suite-desktop-core/e2e/tests/bridge-tor/spawn-tor.test.ts +++ b/packages/suite-desktop-core/e2e/tests/bridge-tor/spawn-tor.test.ts @@ -1,6 +1,7 @@ import { Page } from '@playwright/test'; -import { launchSuite, skipFixture } from '../../support/common'; +import { skipFixture } from '../../support/common'; +import { launchSuite } from '../../support/electron'; import { expect, test } from '../../support/fixtures'; import { NetworkAnalyzer } from '../../support/networkAnalyzer'; diff --git a/packages/suite-desktop-core/e2e/tests/metadata/account-metadata.test.ts b/packages/suite-desktop-core/e2e/tests/metadata/account-metadata.test.ts index 9c8e2cf5fb4..45999d9c54d 100644 --- a/packages/suite-desktop-core/e2e/tests/metadata/account-metadata.test.ts +++ b/packages/suite-desktop-core/e2e/tests/metadata/account-metadata.test.ts @@ -112,8 +112,4 @@ test.describe('Account metadata', { tag: ['@group=metadata1', '@webOnly'] }, () 'adding label to a newly added account. does it work?', ); }); - - test.afterEach(async ({ metadataProviderMock }) => { - await metadataProviderMock.stop(); - }); }); diff --git a/packages/suite-desktop-core/e2e/tests/metadata/address-metadata.test.ts b/packages/suite-desktop-core/e2e/tests/metadata/address-metadata.test.ts index e4229c929c6..10c03924df1 100644 --- a/packages/suite-desktop-core/e2e/tests/metadata/address-metadata.test.ts +++ b/packages/suite-desktop-core/e2e/tests/metadata/address-metadata.test.ts @@ -39,8 +39,4 @@ test.describe('Metadata - address labeling', { tag: ['@group=metadata1', '@webOn await page.keyboard.press('Enter'); await expect(page.getByTestId(metadataEl)).toHaveText('meow meow'); }); - - test.afterEach(async ({ metadataProviderMock }) => { - await metadataProviderMock.stop(); - }); }); diff --git a/packages/suite-desktop-core/e2e/tests/metadata/dropbox-api-errors.test.ts b/packages/suite-desktop-core/e2e/tests/metadata/dropbox-api-errors.test.ts index 145a9640fed..12b6e5beb51 100644 --- a/packages/suite-desktop-core/e2e/tests/metadata/dropbox-api-errors.test.ts +++ b/packages/suite-desktop-core/e2e/tests/metadata/dropbox-api-errors.test.ts @@ -152,9 +152,5 @@ test.describe('Dropbox API errors', { tag: ['@group=metadata1', '@webOnly'] }, ( await metadataPage.account.editLabel(AccountLabelId.BitcoinDefault1, 'Kvooo'); }); - test.afterEach(async ({ metadataProviderMock }) => { - await metadataProviderMock.stop(); - }); - // TODO: Add tests for more possible errors }); diff --git a/packages/suite-desktop-core/e2e/tests/metadata/google-api-errors.test.ts b/packages/suite-desktop-core/e2e/tests/metadata/google-api-errors.test.ts index b9dd267aeac..8f22ce068a9 100644 --- a/packages/suite-desktop-core/e2e/tests/metadata/google-api-errors.test.ts +++ b/packages/suite-desktop-core/e2e/tests/metadata/google-api-errors.test.ts @@ -58,10 +58,6 @@ test.describe('Google API errors', { tag: ['@group=metadata1', '@webOnly'] }, () ); }); - test.afterEach(async ({ metadataProviderMock }) => { - await metadataProviderMock.stop(); - }); - // TODO: Add tests for more possible errors // Reference: https://developers.google.com/drive/api/v3/handle-errors }); diff --git a/packages/suite-desktop-core/e2e/tests/metadata/interval-fetching.test.ts b/packages/suite-desktop-core/e2e/tests/metadata/interval-fetching.test.ts index 43d50b26e4f..ebea16223c5 100644 --- a/packages/suite-desktop-core/e2e/tests/metadata/interval-fetching.test.ts +++ b/packages/suite-desktop-core/e2e/tests/metadata/interval-fetching.test.ts @@ -63,8 +63,4 @@ test.describe('Account metadata', { tag: ['@group=metadata1', '@webOnly'] }, () ); }); }); - - test.afterEach(async ({ metadataProviderMock }) => { - await metadataProviderMock.stop(); - }); }); diff --git a/packages/suite-desktop-core/e2e/tests/metadata/output-labeling.test.ts b/packages/suite-desktop-core/e2e/tests/metadata/output-labeling.test.ts index f40f01a2b7c..2158a72de76 100644 --- a/packages/suite-desktop-core/e2e/tests/metadata/output-labeling.test.ts +++ b/packages/suite-desktop-core/e2e/tests/metadata/output-labeling.test.ts @@ -88,8 +88,4 @@ test.describe('Metadata - Output labeling', { tag: ['@group=metadata1', '@webOnl expect(fileContent).toContain(expectedSubstr); expect(typeof fileContent).toBe('string'); }); - - test.afterEach(async ({ metadataProviderMock }) => { - await metadataProviderMock.stop(); - }); }); diff --git a/packages/suite-desktop-core/e2e/tests/metadata/remembered-device.test.ts b/packages/suite-desktop-core/e2e/tests/metadata/remembered-device.test.ts index b700f850c7c..3f23c5dfafa 100644 --- a/packages/suite-desktop-core/e2e/tests/metadata/remembered-device.test.ts +++ b/packages/suite-desktop-core/e2e/tests/metadata/remembered-device.test.ts @@ -116,8 +116,4 @@ test.describe('Remembered device', { tag: ['@group=metadata2', '@webOnly'] }, () metadataPage.account.addLabelButton(AccountLabelId.BitcoinDefault1), ).not.toBeVisible(); }); - - test.afterEach(async ({ metadataProviderMock }) => { - await metadataProviderMock.stop(); - }); }); diff --git a/packages/suite-desktop-core/e2e/tests/metadata/wallet-metadata.test.ts b/packages/suite-desktop-core/e2e/tests/metadata/wallet-metadata.test.ts index 8097f226616..f7dddcac340 100644 --- a/packages/suite-desktop-core/e2e/tests/metadata/wallet-metadata.test.ts +++ b/packages/suite-desktop-core/e2e/tests/metadata/wallet-metadata.test.ts @@ -116,8 +116,4 @@ test.describe('Metadata - wallet labeling', { tag: ['@group=metadata2', '@webOnl 'still works, metadata enabled for currently not selected device', ); }); - - test.afterEach(async ({ metadataProviderMock }) => { - await metadataProviderMock.stop(); - }); }); diff --git a/packages/suite-desktop-core/e2e/tests/suite/multiple-sessions.test.ts b/packages/suite-desktop-core/e2e/tests/suite/multiple-sessions.test.ts index e9c91d2e6e1..beec1f8b4f5 100644 --- a/packages/suite-desktop-core/e2e/tests/suite/multiple-sessions.test.ts +++ b/packages/suite-desktop-core/e2e/tests/suite/multiple-sessions.test.ts @@ -3,6 +3,7 @@ import { BridgeTransport } from '@trezor/transport'; import { expect, test } from '../../support/fixtures'; import { DashboardActions } from '../../support/pageActions/dashboardActions'; +import { OnboardingActions } from '../../support/pageActions/onboarding/onboardingActions'; const stealBridgeSession = async () => { await test.step('Steal Bridge session', async () => { @@ -84,16 +85,32 @@ test.describe('Multiple sessions', { tag: ['@group=suite'] }, () => { test( 'Overtake session by opening suite new tab', { tag: ['@webOnly'] }, - async ({ context, onboardingPage, dashboardPage, devicePrompt }) => { + async ( + { context, onboardingPage, dashboardPage, devicePrompt, analyticsPage }, + testInfo, + ) => { await onboardingPage.completeOnboarding(); await dashboardPage.discoveryShouldFinish(); const pageTwo = await context.newPage(); - await pageTwo.goto(''); + await pageTwo.context().addInitScript(() => { + window.Playwright = true; + }); + await pageTwo.goto('./'); + const onboardingPageTwo = new OnboardingActions( + pageTwo, + onboardingPage.model, + testInfo, + devicePrompt, + analyticsPage, + ); + await onboardingPageTwo.completeOnboarding(); const dashboardPageTwo = new DashboardActions(pageTwo, devicePrompt); await dashboardPageTwo.discoveryShouldFinish(); await expect(dashboardPageTwo.deviceStatus).toHaveText('Connected'); await expect(dashboardPage.deviceStatus).toHaveText('Refresh'); + + await pageTwo.close(); }, );