Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: E2E tests

on:
# Можно выбрать любую ветку из выпадающего списка
workflow_dispatch: {}

jobs:
e2e:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'

- name: Install dependencies
run: pnpm install

- name: Install Playwright browsers
run: pnpm --filter demo-wallet e2e:deps

- name: Build project
run: pnpm build

- name: Run web e2e tests
env:
CI: "1"
ENABLE_HEADLESS: "true"
E2E_DEBUG_LOGS: "true"
DEBUG: "pw:webserver"
run: pnpm demo-wallet e2e

- name: Build extension (chrome)
run: pnpm --filter demo-wallet build:extension:chrome

- name: Run extension e2e tests
env:
CI: "1"
ENABLE_HEADLESS: "false"
E2E_DEBUG_LOGS: "true"
E2E_WALLET_SOURCE_EXTENSION: "../../dist-extension-chrome"
E2E_JS_BRIDGE: "true"
run: pnpm demo-wallet e2e

- name: Upload Playwright artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-artifacts
path: |
apps/demo-wallet/test-results/
apps/demo-wallet/playwright-report/
retention-days: 3


8 changes: 6 additions & 2 deletions .github/workflows/e2e_extension.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,11 @@ jobs:
run: |
pnpm build
cd apps/demo-wallet
pnpm e2e
xvfb-run pnpm e2e
env:
WALLET_MNEMONIC: ${{ secrets.WALLET_MNEMONIC }}
ENABLE_HEADLESS: "false"
E2E_DEBUG_LOGS: "true"
E2E_WALLET_SOURCE_EXTENSION: "${{ github.workspace }}/apps/demo-wallet/dist-extension-chrome"
E2E_JS_BRIDGE: true
ALLURE_BASE_URL: ${{ secrets.ALLURE_BASE_URL }}
Expand All @@ -84,5 +86,7 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: playwright-report-extension
path: apps/demo-wallet/test-results/
path: |
apps/demo-wallet/test-results/
apps/demo-wallet/playwright-report/
retention-days: 1
6 changes: 5 additions & 1 deletion .github/workflows/e2e_web.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ jobs:
xvfb-run pnpm e2e
env:
WALLET_MNEMONIC: ${{ secrets.WALLET_MNEMONIC }}
E2E_DEBUG_LOGS: "true"
DEBUG: "pw:webserver"
# VITE_BRIDGE_URL: 'http://localhost:8081/bridge'
VITE_BRIDGE_URL: 'https://connect.ton.org/bridge'
ALLURE_BASE_URL: ${{ secrets.ALLURE_BASE_URL }}
Expand All @@ -116,5 +118,7 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: playwright-report-web
path: apps/demo-wallet/test-results/
path: |
apps/demo-wallet/test-results/
apps/demo-wallet/playwright-report/
retention-days: 1
130 changes: 127 additions & 3 deletions apps/demo-wallet/e2e/demo-wallet/demoWalletFixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import path from 'path';
import { fileURLToPath } from 'url';

import type { Page, TestInfo } from '@playwright/test';
import { test } from '@playwright/test';

import type { ConfigFixture } from '../qa';
Expand All @@ -17,6 +18,104 @@ import type { TestFixture } from '../qa';
import { DemoWallet } from './DemoWallet';
import { isExtensionWalletSource } from '../qa/WalletApp';

const enableDebugLogs = process.env.E2E_DEBUG_LOGS === 'true' || !!process.env.CI;
const enableVerboseDebugLogs = process.env.E2E_DEBUG_LOGS_VERBOSE === 'true';

type PageDebugState = {
lastUrl?: string;
lastTitle?: string;
};

const pageDebugState = new WeakMap<Page, PageDebugState>();
let signalHandlersInstalled = false;

function prefix(testInfo: TestInfo): string {
const project = testInfo.project?.name ?? 'unknown-project';
return `[E2E][${project}][${testInfo.title}]`;
}

function safeText(value: unknown, maxLen = 700): string {
const str = typeof value === 'string' ? value : String(value);
return str.length > maxLen ? `${str.slice(0, maxLen)}…(truncated)` : str;
}

function installCancelHandlers(testInfo: TestInfo, getPages: () => Page[]): void {
if (signalHandlersInstalled) return;
signalHandlersInstalled = true;

const dump = (signal: string) => {
// eslint-disable-next-line no-console
console.error(`${prefix(testInfo)} received ${signal}. Dumping open pages state:`);
try {
for (const p of getPages()) {
const state = pageDebugState.get(p);
// eslint-disable-next-line no-console
console.error(
`${prefix(testInfo)} page url=${safeText(state?.lastUrl ?? p.url())} title=${safeText(
state?.lastTitle ?? '',
)}`,
);
}
} catch (e) {
// eslint-disable-next-line no-console
console.error(`${prefix(testInfo)} failed to dump pages: ${String(e)}`);
}
};

process.once('SIGTERM', () => dump('SIGTERM'));
process.once('SIGINT', () => dump('SIGINT'));
}

function attachDebugLogging(page: Page, testInfo: TestInfo): void {
if (!enableDebugLogs) return;
if (pageDebugState.has(page)) return;

pageDebugState.set(page, {});

const updateState = async () => {
const state = pageDebugState.get(page);
if (!state) return;
try {
state.lastUrl = page.url();
state.lastTitle = await page.title().catch(() => state.lastTitle);
} catch {
// ignore
}
};

page.on('framenavigated', (frame) => {
if (frame === page.mainFrame()) void updateState();
});
page.on('load', () => void updateState());
void updateState();

page.on('pageerror', (err) => {
// eslint-disable-next-line no-console
console.error(`${prefix(testInfo)} [pageerror] ${safeText(err?.stack ?? err)}`);
});

page.on('console', (msg) => {
const type = msg.type();
if (!enableVerboseDebugLogs && type !== 'error' && type !== 'warning') return;
// eslint-disable-next-line no-console
console.log(`${prefix(testInfo)} [console:${type}] ${safeText(msg.text())}`);
});

page.on('requestfailed', (req) => {
// eslint-disable-next-line no-console
console.error(
`${prefix(testInfo)} [requestfailed] ${req.method()} ${req.url()} ${safeText(req.failure()?.errorText ?? '')}`,
);
});

page.on('response', (res) => {
const status = res.status();
if (status < 400) return;
// eslint-disable-next-line no-console
console.error(`${prefix(testInfo)} [http ${status}] ${res.request().method()} ${res.url()}`);
});
}

export function detectWalletSource() {
const source = process.env.E2E_WALLET_SOURCE ?? 'http://localhost:5173/';
const extensionPath = process.env.E2E_WALLET_SOURCE_EXTENSION;
Expand All @@ -35,22 +134,39 @@ export function demoWalletFixture(config: ConfigFixture, slowMo = 0) {
const mnemonic = config.mnemonic ?? process.env.WALLET_MNEMONIC;

return test.extend<TestFixture>({
context: async ({ context: _ }, use) => {
context: async ({ context: _ }, use, testInfo) => {
const extensionPath = isExtension ? walletSource : '';
const context = await launchPersistentContext(extensionPath, slowMo);
if (enableDebugLogs) {
// eslint-disable-next-line no-console
console.log(
`${prefix(testInfo)} starting fixtures: appUrl=${safeText(config.appUrl)} walletSource=${safeText(
walletSource,
)} extension=${String(isExtension)} headless=${safeText(process.env.ENABLE_HEADLESS ?? '(default)')}`,
);
installCancelHandlers(testInfo, () => context.pages());
context.on('page', (p) => attachDebugLogging(p, testInfo));
for (const p of context.pages()) attachDebugLogging(p, testInfo);
}
await use(context);
await context.close();
},
app: async ({ context }, use) => {
app: async ({ context }, use, testInfo) => {
// @ts-expect-error - custom property on context
let app = context._app;
if (!app) {
app = await context.newPage();
// @ts-expect-error - custom property on context
context._app = app;
attachDebugLogging(app, testInfo);
const startedAt = Date.now();
// eslint-disable-next-line no-console
if (enableDebugLogs) console.log(`${prefix(testInfo)} opening dapp: ${safeText(config.appUrl)}`);
app.onReady = await app.goto(config.appUrl, {
waitUntil: 'load',
});
// eslint-disable-next-line no-console
if (enableDebugLogs) console.log(`${prefix(testInfo)} dapp loaded in ${Date.now() - startedAt}ms`);
}
// const pages = context.pages();
// context.get
Expand All @@ -66,10 +182,18 @@ export function demoWalletFixture(config: ConfigFixture, slowMo = 0) {
const widget = new TonConnectWidget(app);
await use(widget);
},
wallet: async ({ context }, use) => {
wallet: async ({ context }, use, testInfo) => {
const source = isExtension ? await getExtensionId(context) : walletSource;
const app = new DemoWallet(context, source);

if (enableDebugLogs) {
// eslint-disable-next-line no-console
console.log(
`${prefix(testInfo)} importing wallet. mnemonic=${mnemonic ? '(set)' : '(empty)'} source=${safeText(
source,
)}`,
);
}
const importPromise = app.importWallet(mnemonic ?? '');
// await _app.onReady;
await importPromise;
Expand Down
14 changes: 11 additions & 3 deletions apps/demo-wallet/e2e/qa/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,17 @@ export async function launchPersistentContext(extensionPath: string, slowMo = 0)
args.push(`--load-extension=${extensionPath}`);
}

const isHeadlessEnv = process.env.ENABLE_HEADLESS === 'true';
const isCi = !!process.env.CI;
const headless = isHeadlessEnv || isCi;
const headless =
process.env.ENABLE_HEADLESS === 'true' ? true : process.env.ENABLE_HEADLESS === 'false' ? false : isCi;

if (extensionPath !== '' && headless) {
// eslint-disable-next-line no-console
console.warn(
`[E2E] Extension mode detected but headless=true. Chrome extensions usually won't load in headless mode. ` +
`Set ENABLE_HEADLESS=false in CI for extension e2e (run under xvfb).`,
);
}

if (headless) {
args.push('--headless=new');
Expand All @@ -38,7 +46,7 @@ export async function launchPersistentContext(extensionPath: string, slowMo = 0)
slowMo = isCi ? 0 : (parseInt(process.env.E2E_SLOW_MO || '0') ?? slowMo);
const browserContext = await chromium.launchPersistentContext('', {
args,
headless: false,
headless,
slowMo,
});

Expand Down
Loading
Loading