diff --git a/e2e/constants/e2e.constants.ts b/e2e/constants/e2e.constants.ts deleted file mode 100644 index 2a14089d..00000000 --- a/e2e/constants/e2e.constants.ts +++ /dev/null @@ -1,5 +0,0 @@ -const TIMEOUT_BASE = 15000; - -export const TIMEOUT_SHORT = {timeout: TIMEOUT_BASE}; -export const TIMEOUT_AVERAGE = {timeout: 2 * TIMEOUT_BASE}; -export const TIMEOUT_LONG = {timeout: 4 * TIMEOUT_BASE}; diff --git a/e2e/constants/test-ids.constants.ts b/e2e/constants/test-ids.constants.ts deleted file mode 100644 index 23e1df7c..00000000 --- a/e2e/constants/test-ids.constants.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type {TestIds} from '../types/test-id'; - -export const testIds = { - auth: { - signInDev: 'btn-sign-in-dev', - switchDevAccount: 'btn-switch-account-dev', - inputDevIdentifier: 'input-dev-identifier', - continueDevAccount: 'btn-continue-dev' - }, - launchpad: { - launch: 'btn-launch-first-satellite', - launchExtraSatellite: 'btn-launch-extra-satellite', - actions: 'btn-open-actions' - }, - createSatellite: { - create: 'btn-create-satellite', - input: 'input-satellite-name', - website: 'input-radio-satellite-website', - application: 'input-radio-satellite-application', - continue: 'btn-continue-overview' - }, - satelliteOverview: { - visit: 'link-visit-satellite', - copySatelliteId: 'btn-copy-satellite-id' - }, - navbar: { - openWallet: 'btn-open-wallet', - getIcp: 'btn-get-icp', - getCycles: 'btn-get-cycles' - }, - createAnalytics: { - navLink: 'link-analytics-dashboard', - launch: 'btn-launch-analytics', - create: 'btn-create-analytics', - close: 'btn-close-analytics-wizard' - }, - wizard: { - closeInsufficientFunds: 'btn-close-insufficient-funds' - } -} as const satisfies TestIds; diff --git a/e2e/page-objects/_page.ts b/e2e/page-objects/_page.ts deleted file mode 100644 index 5522e0e5..00000000 --- a/e2e/page-objects/_page.ts +++ /dev/null @@ -1,3 +0,0 @@ -export abstract class TestPage { - abstract close(): Promise; -} diff --git a/e2e/page-objects/cli.page.ts b/e2e/page-objects/cli.page.ts deleted file mode 100644 index 4ce962db..00000000 --- a/e2e/page-objects/cli.page.ts +++ /dev/null @@ -1,220 +0,0 @@ -import {assertNonNullish, notEmptyString} from '@dfinity/utils'; -import type {PrincipalText} from '@dfinity/zod-schemas'; -import {execute, spawn} from '@junobuild/cli-tools'; -import {statSync} from 'node:fs'; -import {readdir, readFile, writeFile} from 'node:fs/promises'; -import {join} from 'node:path'; -import {TestPage} from './_page'; - -const DEV = (process.env.NODE_ENV ?? 'production') === 'development'; - -const JUNO_CONFIG = join(process.cwd(), 'juno.config.ts'); - -const JUNO_TEST_ARGS = ['--mode', 'development', '--headless']; - -const {command: JUNO_CMD, args: JUNO_CDM_ARGS} = DEV - ? {command: 'node', args: ['dist/index.js']} - : {command: 'juno', args: []}; - -const buildArgs = (args: string[]): string[] => [...JUNO_CDM_ARGS, ...args, ...JUNO_TEST_ARGS]; - -export interface CliPageParams { - satelliteId: PrincipalText; -} - -export class CliPage extends TestPage { - #satelliteId: PrincipalText; - - private constructor({satelliteId}: CliPageParams) { - super(); - - this.#satelliteId = satelliteId; - } - - static async initWithEmulatorLogin(params: CliPageParams): Promise { - const cliPage = new CliPage(params); - - await cliPage.initConfig(); - - await cliPage.loginWithEmulator(); - - await cliPage.applyConfig(); - - return cliPage; - } - - protected async initConfig(): Promise { - let content = await readFile(JUNO_CONFIG, 'utf-8'); - content = content.replace('', this.#satelliteId); - await writeFile(JUNO_CONFIG, content, 'utf-8'); - } - - private async revertConfig(): Promise { - let content = await readFile(JUNO_CONFIG, 'utf-8'); - content = content.replace(this.#satelliteId, ''); - await writeFile(JUNO_CONFIG, content, 'utf-8'); - } - - async toggleSatelliteId({satelliteId}: {satelliteId: PrincipalText}): Promise { - await this.revertConfig(); - this.#satelliteId = satelliteId; - await this.initConfig(); - } - - protected async loginWithEmulator(): Promise { - await execute({ - command: JUNO_CMD, - args: buildArgs(['login', '--emulator']) - }); - } - - async applyConfig(): Promise { - await execute({ - command: JUNO_CMD, - args: buildArgs(['config', 'apply', '--force']) - }); - } - - private async logout(): Promise { - await execute({ - command: JUNO_CMD, - args: buildArgs(['logout']) - }); - } - - async clearHosting(): Promise { - await execute({ - command: JUNO_CMD, - args: buildArgs(['hosting', 'clear']) - }); - } - - async deployHosting({clear}: {clear: boolean}): Promise { - await execute({ - command: JUNO_CMD, - args: buildArgs(['hosting', 'deploy', ...(clear ? ['--clear'] : [])]) - }); - } - - async createSnapshot({ - target - }: { - target: 'satellite' | 'orbiter' | 'mission-control'; - }): Promise { - await execute({ - command: JUNO_CMD, - args: buildArgs(['snapshot', 'create', '--target', target]) - }); - } - - async restoreSnapshot({ - target - }: { - target: 'satellite' | 'orbiter' | 'mission-control'; - }): Promise { - await execute({ - command: JUNO_CMD, - args: buildArgs(['snapshot', 'restore', '--target', target]) - }); - } - - async deleteSnapshot({ - target - }: { - target: 'satellite' | 'orbiter' | 'mission-control'; - }): Promise { - await execute({ - command: JUNO_CMD, - args: buildArgs(['snapshot', 'delete', '--target', target]) - }); - } - - async downloadSnapshot({ - target - }: { - target: 'satellite' | 'orbiter' | 'mission-control'; - }): Promise<{snapshotFolder: string}> { - await execute({ - command: JUNO_CMD, - args: buildArgs(['snapshot', 'download', '--target', target]) - }); - - return await this.getSnapshotFsFolder(); - } - - // Retrieve where the snapshot was created - async getSnapshotFsFolder(): Promise<{snapshotFolder: string}> { - const snapshotsFolder = join(process.cwd(), '.snapshots'); - - const folders = await readdir(snapshotsFolder, {withFileTypes: true}); - - const [snapshotFolder] = folders - .filter((d) => d.isDirectory()) - .map(({name}) => { - const path = join(snapshotsFolder, name); - const {birthtimeMs: time} = statSync(path); - return {path, time}; - }) - .sort((a, b) => b.time - a.time); - - assertNonNullish(snapshotFolder); - - return {snapshotFolder: snapshotFolder.path}; - } - - async uploadSnapshot({ - target, - folder - }: { - target: 'satellite' | 'orbiter' | 'mission-control'; - folder: string; - }): Promise { - await execute({ - command: JUNO_CMD, - args: buildArgs(['snapshot', 'upload', '--target', target, '--dir', folder]) - }); - } - - async listSnapshot({ - target - }: { - target: 'satellite' | 'orbiter' | 'mission-control'; - }): Promise<{snapshotId: string | undefined}> { - let output = ''; - - await spawn({ - command: JUNO_CMD, - args: buildArgs(['snapshot', 'list', '--target', target]), - stdout: (o) => (output += o), - silentErrors: true - }); - - const [_, snapshotId] = output.split('Snapshot found:'); - return {snapshotId: notEmptyString(snapshotId) ? snapshotId.trim() : undefined}; - } - - async whoami(): Promise<{accessKey: string}> { - let output = ''; - - await spawn({ - command: JUNO_CMD, - args: buildArgs(['whoami']), - stdout: (o) => (output += o), - silentErrors: true - }); - - const [_, __, ___, text] = output.split(' '); - const [value] = text.split('\n'); - const accessKey = value.replace('\x1B[32m', '').replace('\x1B[39m', ''); - - return {accessKey: accessKey.trim()}; - } - - /** - * @override - */ - async close(): Promise { - await this.revertConfig(); - await this.logout(); - } -} diff --git a/e2e/page-objects/console.page.ts b/e2e/page-objects/console.page.ts deleted file mode 100644 index 374e51c0..00000000 --- a/e2e/page-objects/console.page.ts +++ /dev/null @@ -1,161 +0,0 @@ -import {notEmptyString} from '@dfinity/utils'; -import {PrincipalText, PrincipalTextSchema} from '@dfinity/zod-schemas'; -import {expect} from '@playwright/test'; -import {TIMEOUT_AVERAGE, TIMEOUT_SHORT} from '../constants/e2e.constants'; -import {testIds} from '../constants/test-ids.constants'; -import {IdentityPage, type IdentityPageParams} from './identity.page'; -import {SatellitePage} from './satellite.page'; - -export class ConsolePage extends IdentityPage { - private constructor(params: IdentityPageParams) { - super(params); - } - - static async initWithSignIn(params: IdentityPageParams): Promise { - const consolePage = new ConsolePage(params); - - await consolePage.goto(); - - await consolePage.signIn(); - - return consolePage; - } - - async goto({path}: {path: string} = {path: '/'}): Promise { - await this.page.goto(path); - } - - private async signIn(): Promise { - await expect(this.page.getByTestId(testIds.auth.switchDevAccount)).toBeVisible(TIMEOUT_AVERAGE); - - await this.page.getByTestId(testIds.auth.switchDevAccount).click(); - - await expect(this.page.getByTestId(testIds.auth.inputDevIdentifier)).toBeVisible(); - - await this.page - .getByTestId(testIds.auth.inputDevIdentifier) - .fill(crypto.randomUUID().replaceAll('-', '')); - - await expect(this.page.getByTestId(testIds.auth.continueDevAccount)).toBeVisible(); - - await this.page.getByTestId(testIds.auth.continueDevAccount).click(); - } - - async createSatellite(params: {kind: 'website' | 'application'}): Promise { - await expect(this.page.getByTestId(testIds.launchpad.launch)).toBeVisible(TIMEOUT_AVERAGE); - - await this.page.getByTestId(testIds.launchpad.launch).click(); - - await this.createSatelliteWizard(params); - } - - async openCreateAdditionalSatelliteWizard(params: { - kind: 'website' | 'application'; - }): Promise { - await expect(this.page.getByTestId(testIds.launchpad.actions)).toBeVisible(TIMEOUT_AVERAGE); - - await this.page.getByTestId(testIds.launchpad.actions).click(); - - await expect(this.page.getByTestId(testIds.launchpad.launchExtraSatellite)).toBeVisible( - TIMEOUT_AVERAGE - ); - - await this.page.getByTestId(testIds.launchpad.launchExtraSatellite).click(); - - await this.createSatelliteWizard(params); - } - - private async createSatelliteWizard({kind}: {kind: 'website' | 'application'}): Promise { - await expect(this.page.getByTestId(testIds.createSatellite.create)).toBeVisible({ - timeout: 15000 - }); - - await this.page.getByTestId(testIds.createSatellite.input).fill('Test'); - await this.page.getByTestId(testIds.createSatellite[kind]).click(); - - await this.page.getByTestId(testIds.createSatellite.create).click(); - - await expect(this.page.getByTestId(testIds.createSatellite.continue)).toBeVisible( - TIMEOUT_AVERAGE - ); - - await this.page.getByTestId(testIds.createSatellite.continue).click(); - } - - async visitSatelliteSite( - {title}: {title: string} = {title: 'Juno / Satellite'} - ): Promise { - await expect(this.page.getByTestId(testIds.satelliteOverview.visit)).toBeVisible( - TIMEOUT_AVERAGE - ); - - const satellitePagePromise = this.context.waitForEvent('page'); - - await this.page.getByTestId(testIds.satelliteOverview.visit).click(); - - const satellitePage = await satellitePagePromise; - - await expect(satellitePage).toHaveTitle(title); - - return new SatellitePage({ - page: satellitePage, - browser: this.browser, - context: this.context - }); - } - - async getCycles(): Promise { - await expect(this.page.getByTestId(testIds.navbar.openWallet)).toBeVisible(); - - await this.page.getByTestId(testIds.navbar.openWallet).click(); - - await expect(this.page.getByTestId(testIds.navbar.getCycles)).toBeVisible(); - - await this.page.getByTestId(testIds.navbar.getCycles).click(); - - await expect(this.page.getByText('330.010 TCycles')).toBeVisible({timeout: 65000}); - } - - async copySatelliteID(): Promise { - await expect(this.page.getByTestId(testIds.satelliteOverview.copySatelliteId)).toBeVisible(); - - await this.page.getByTestId(testIds.satelliteOverview.copySatelliteId).click(); - - const satelliteId = await this.page.evaluate(() => navigator.clipboard.readText()); - - expect(notEmptyString(satelliteId)).toBeTruthy(); - expect(PrincipalTextSchema.safeParse(satelliteId).success).toBeTruthy(); - - return satelliteId; - } - - async addSatelliteAdminAccessKey({ - satelliteId, - accessKey - }: { - satelliteId: PrincipalText; - accessKey: string; - }): Promise { - await this.goto({path: `/satellite/?s=${satelliteId}&tab=setup`}); - - const btnLocator = this.page.locator('button', {hasText: 'Add an access key'}); - await expect(btnLocator).toBeVisible(TIMEOUT_SHORT); - await btnLocator.click(); - - const form = this.page.locator('form'); - - await form.getByRole('radio', {name: /enter one manually/i}).check(); - - const keyField = form.getByLabel('Access Key ID'); - await expect(keyField).toBeEnabled(); - await keyField.fill(accessKey); - - await form.locator('select[name="scope"]').selectOption('admin'); - - const submitLocator = form.getByRole('button', {name: /^submit$/i}); - await expect(submitLocator).toBeEnabled(); - await submitLocator.click(); - - await expect(this.page.getByText('Access Key Added')).toBeVisible(TIMEOUT_SHORT); - } -} diff --git a/e2e/page-objects/identity.page.ts b/e2e/page-objects/identity.page.ts deleted file mode 100644 index 43599f01..00000000 --- a/e2e/page-objects/identity.page.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type {Browser, BrowserContext, Page} from '@playwright/test'; -import {TestPage} from './_page'; - -export interface IdentityPageParams { - page: Page; - context: BrowserContext; - browser: Browser; -} - -export abstract class IdentityPage extends TestPage { - protected readonly page: Page; - protected readonly context: BrowserContext; - protected readonly browser: Browser; - - protected constructor({page, context, browser}: IdentityPageParams) { - super(); - - this.page = page; - this.context = context; - this.browser = browser; - } - - /** - * @override - */ - async close(): Promise { - await this.page.close(); - } -} diff --git a/e2e/page-objects/satellite.page.ts b/e2e/page-objects/satellite.page.ts deleted file mode 100644 index a2cfd08c..00000000 --- a/e2e/page-objects/satellite.page.ts +++ /dev/null @@ -1,24 +0,0 @@ -import {expect} from '@playwright/test'; -import {TIMEOUT_AVERAGE} from '../constants/e2e.constants'; -import {IdentityPage} from './identity.page'; - -export class SatellitePage extends IdentityPage { - async reload({title}: {title: string} = {title: 'Juno / Satellite'}): Promise { - await expect - .poll( - async () => { - await this.page.reload({waitUntil: 'load'}); - return await this.page.title(); - }, - { - ...TIMEOUT_AVERAGE, - intervals: [1_000, 2_000, 10_000] - } - ) - .toBe(title); - } - - async assertScreenshot(): Promise { - await expect(this.page).toHaveScreenshot({fullPage: true}); - } -} diff --git a/e2e/snapshots.spec.ts b/e2e/snapshots.spec.ts index cf9fe76d..0b140517 100644 --- a/e2e/snapshots.spec.ts +++ b/e2e/snapshots.spec.ts @@ -1,11 +1,14 @@ +import {initEmulatorSuite} from '@junobuild/emulator-playwright'; import {expect, test} from '@playwright/test'; -import {initTestSuite} from './utils/init.utils'; + +const DEV = (process.env.NODE_ENV ?? 'production') === 'development'; +const COMMAND = DEV ? {command: 'node', args: ['dist/index.js']} : {command: 'juno', args: []}; test.describe.configure({mode: 'serial'}); const snapshotTests = ({satelliteKind}: {satelliteKind: 'website' | 'application'}) => { test.describe(`satellite ${satelliteKind}`, () => { - const getTestPages = initTestSuite({satelliteKind}); + const getTestPages = initEmulatorSuite({satelliteKind, cli: {command: COMMAND}}); const SNAPSHOT_TARGET = {target: 'satellite' as const}; diff --git a/e2e/types/test-id.ts b/e2e/types/test-id.ts deleted file mode 100644 index b0998cde..00000000 --- a/e2e/types/test-id.ts +++ /dev/null @@ -1,9 +0,0 @@ -type TestCTAType = 'btn' | 'link' | 'input'; - -type TestAction = string; - -export type TestId = `${TestCTAType}-${TestAction}`; - -type TestSuite = string; - -export type TestIds = Record>; diff --git a/e2e/utils/init.utils.ts b/e2e/utils/init.utils.ts deleted file mode 100644 index 51ccaf1d..00000000 --- a/e2e/utils/init.utils.ts +++ /dev/null @@ -1,52 +0,0 @@ -import {test} from '@playwright/test'; -import {CliPage} from '../page-objects/cli.page'; -import {ConsolePage} from '../page-objects/console.page'; - -interface TestSuitePages { - consolePage: ConsolePage; - cliPage: CliPage; -} - -export const initTestSuite = ({ - satelliteKind -}: { - satelliteKind: 'website' | 'application'; -}): (() => TestSuitePages) => { - let consolePage: ConsolePage; - let cliPage: CliPage; - - test.beforeAll(async ({playwright}) => { - test.setTimeout(120000); - - const browser = await playwright.chromium.launch(); - - const context = await browser.newContext(); - const page = await context.newPage(); - - consolePage = await ConsolePage.initWithSignIn({ - page, - context, - browser - }); - - await consolePage.createSatellite({kind: satelliteKind}); - - const satelliteId = await consolePage.copySatelliteID(); - - cliPage = await CliPage.initWithEmulatorLogin({satelliteId}); - }); - - test.afterAll(async () => { - const results = await Promise.allSettled([consolePage.close(), cliPage.close()]); - - if (results.find(({status}) => status === 'rejected')) { - console.error(results); - throw new Error('Failed to close test suite!'); - } - }); - - return (): TestSuitePages => ({ - consolePage, - cliPage - }); -}; diff --git a/package-lock.json b/package-lock.json index f295b20e..2ef853d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "devDependencies": { "@eslint/eslintrc": "^3.3.3", "@eslint/js": "^9.39.2", + "@junobuild/emulator-playwright": "^0.0.3", "@junobuild/functions": "^0.5.6", "@playwright/test": "^1.58.1", "@types/node": "24.10.9", @@ -1847,6 +1848,24 @@ "@dfinity/utils": "^4.1" } }, + "node_modules/@junobuild/emulator-playwright": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@junobuild/emulator-playwright/-/emulator-playwright-0.0.3.tgz", + "integrity": "sha512-+W49nOvGcuN22bANa8nha1/lKSbPil7dtEIMNgBqC9MwT6b3IFRXJVWQsW6uxYep6hL0W4+egUsd8judUfZMNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=24", + "npm": ">=11.5.1 <12.0.0" + }, + "peerDependencies": { + "@dfinity/utils": "^4.1.0", + "@dfinity/zod-schemas": "^3.0.2", + "@junobuild/cli-tools": "^0.10.2", + "@junobuild/config-loader": "^0.4.8", + "@playwright/test": "^1.52.0" + } + }, "node_modules/@junobuild/errors": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@junobuild/errors/-/errors-0.2.3.tgz", @@ -7873,6 +7892,13 @@ "integrity": "sha512-7N0NWiN0v7oWdTOSVhvH18US/MgMj/ENho7Nr2SGz+O2+4f/PC8TVpSmF5k9GsWCiG3jdC31y/IZDFxVBhmzog==", "requires": {} }, + "@junobuild/emulator-playwright": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@junobuild/emulator-playwright/-/emulator-playwright-0.0.3.tgz", + "integrity": "sha512-+W49nOvGcuN22bANa8nha1/lKSbPil7dtEIMNgBqC9MwT6b3IFRXJVWQsW6uxYep6hL0W4+egUsd8judUfZMNQ==", + "dev": true, + "requires": {} + }, "@junobuild/errors": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@junobuild/errors/-/errors-0.2.3.tgz", diff --git a/package.json b/package.json index 0dd1ed83..cc9dca43 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "devDependencies": { "@eslint/eslintrc": "^3.3.3", "@eslint/js": "^9.39.2", + "@junobuild/emulator-playwright": "^0.0.3", "@junobuild/functions": "^0.5.6", "@playwright/test": "^1.58.1", "@types/node": "24.10.9",