Merge pull request #717 from 4gray/e2e-tests

e2e smoke tests
This commit is contained in:
4gray authored and GitHub committed 2026-01-09 15:38:45 +01:00
commit 2c5fc7e1ab
12 files changed
+335 -166

No files matched your search

+4 -1
View File
@@ -8,7 +8,10 @@
"Bash(cat:*)",
"Bash(npx asar:*)",
"Bash(npm run package:app:*)",
"Bash(npm run build:backend:*)"
"Bash(npm run build:backend:*)",
"Bash(pnpm add:*)",
"WebFetch(domain:github.com)",
"WebFetch(domain:www.npmjs.com)"
],
"deny": [],
"ask": []
-89
View File
@@ -1,89 +0,0 @@
name: 'Build and Test'
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- run: git config --global url."https://${{ secrets.PAT }}@github.com/".insteadOf ssh://git@github.com/
- run: npm ci
- run: npm test -- --coverage
- run: bash <(curl -s https://codecov.io/bash)
- run: npm run build:prod
env:
CI: true
e2e-tests:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest]
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- run: git config --global url."https://${{ secrets.PAT }}@github.com/".insteadOf ssh://git@github.com/
- run: npm ci
# env:
# PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
- run: npm run build:prod
- run: npx playwright install --with-deps
#- run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run e2e:ci
# if: matrix.os == 'ubuntu-latest'
# env:
# e2e: true
- run: npm run e2e:ci
env:
e2e: true
# release:
# needs: [build-and-test]
# runs-on: ${{ matrix.os }}
# strategy:
# matrix:
# os: [macos-latest, ubuntu-latest, windows-latest]
# steps:
# - name: Check out Git repository
# uses: actions/checkout@v4
# - name: Install Node.js, NPM and Yarn
# uses: actions/setup-node@v4
# with:
# node-version: '22'
# - name: Build Electron app
# uses: samuelmeuli/action-electron-builder@v1
# with:
# # GitHub token, automatically provided to the action
# # (No need to define this secret in the repo settings)
# github_token: ${{ secrets.github_token }}
# # If the commit is tagged with a version (e.g. "v1.0.0"),
# # release the app after building
# release: ${{ startsWith(github.ref, 'refs/tags/v') }}
# #- name: Archive production artifacts
# # uses: actions/upload-artifact@v3
# # with:
# # name: dev-release
# # path: ./release/**
+63
View File
@@ -0,0 +1,63 @@
name: 'Cross-Platform E2E Tests'
on:
push:
branches:
- nx
pull_request:
branches:
- nx
jobs:
e2e-tests:
name: E2E on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
env:
NX_SKIP_NX_CACHE: true
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'
- name: Install Dependencies
run: pnpm install
- name: Build Backend
run: npx nx build electron-backend
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run E2E Tests (Linux)
if: runner.os == 'Linux'
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" npx nx run electron-backend-e2e:e2e
env:
CI: true
- name: Run E2E Tests (Windows/Mac)
if: runner.os != 'Linux'
run: npx nx run electron-backend-e2e:e2e
env:
CI: true
- name: Upload Test Results
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report-${{ matrix.os }}
path: dist/playwright-report/electron-backend-e2e/
retention-days: 7
@@ -0,0 +1,12 @@
import playwright from 'eslint-plugin-playwright';
import baseConfig from '../../eslint.config.mjs';
export default [
playwright.configs['flat/recommended'],
...baseConfig,
{
files: ['**/*.ts', '**/*.js'],
// Override or add rules here
rules: {},
},
];
@@ -0,0 +1,47 @@
import { defineConfig } from '@playwright/test';
/**
* Playwright configuration for Electron e2e tests.
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './src',
/* Match .e2e.ts files */
testMatch: '**/*.e2e.ts',
/* Run tests sequentially since we're testing an Electron app */
fullyParallel: false,
/* Fail the build on CI if you accidentally left test.only in the source code */
forbidOnly: !!process.env['CI'],
/* Retry on CI only */
retries: process.env['CI'] ? 2 : 0,
/* Single worker since we're testing an Electron app */
workers: 1,
/* Reporter to use */
reporter: [
['list'],
['html', { outputFolder: '../../dist/playwright-report/electron-backend-e2e' }],
],
/* Shared settings for all the projects below */
use: {
/* Collect trace when retrying the failed test */
trace: 'on-first-retry',
/* Screenshots on failure */
screenshot: 'on',
/* Video on failure */
video: 'on-first-retry',
},
/* Output folder for test artifacts */
outputDir: '../../dist/test-results/electron-backend-e2e',
/* Timeout for each test */
timeout: 60000,
/* Timeout for expect() assertions */
expect: {
timeout: 10000,
},
projects: [
{
name: 'electron',
use: {},
},
],
});
+12
View File
@@ -0,0 +1,12 @@
{
"name": "electron-backend-e2e",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"sourceRoot": "apps/electron-backend-e2e/src",
"implicitDependencies": ["electron-backend"],
"targets": {
"e2e": {
"dependsOn": ["electron-backend:build"]
}
}
}
+171
View File
@@ -0,0 +1,171 @@
import {
_electron as electron,
ElectronApplication,
expect,
Page,
test,
} from '@playwright/test';
import { existsSync, mkdirSync } from 'fs';
import { join, resolve } from 'path';
/**
* Smoke test for the Electron application.
* Verifies that the app starts correctly and renders the main window.
*/
let electronApp: ElectronApplication;
let mainWindow: Page;
// Workspace root (where nx.json is located)
const workspaceRoot = resolve(__dirname, '../../..');
// Path to the packaged Electron app main.js file
const electronMainPath = join(
workspaceRoot,
'dist/apps/electron-backend/main.js'
);
// Path for screenshots
const screenshotDir = join(
workspaceRoot,
'dist/test-results/electron-backend-e2e'
);
// Ensure screenshot directory exists
if (!existsSync(screenshotDir)) {
mkdirSync(screenshotDir, { recursive: true });
}
/**
* Helper to find the main application window (not DevTools)
*/
async function findMainWindow(app: ElectronApplication): Promise<Page> {
// Wait for windows to be available
await new Promise((resolve) => setTimeout(resolve, 2000));
const windows = app.windows();
console.log(`Found ${windows.length} window(s)`);
for (const window of windows) {
const title = await window.title();
console.log(`Window title: "${title}"`);
// Skip DevTools windows
if (!title.includes('DevTools')) {
return window;
}
}
// If no non-DevTools window found, wait for one
console.log('Waiting for main window...');
return await app.firstWindow();
}
test.describe('Electron App Smoke Test', () => {
test.beforeAll(async () => {
console.log(`Launching Electron app from: ${electronMainPath}`);
console.log(`Platform: ${process.platform}`);
console.log(`CI: ${process.env['CI']}`);
// Check if web build exists
const webIndexPath = join(workspaceRoot, 'dist/apps/web/index.html');
console.log(`Web index path: ${webIndexPath}`);
console.log(`Web index exists: ${existsSync(webIndexPath)}`);
// Launch the Electron app
// On Linux CI, we need to disable sandbox due to permission issues
const args = [electronMainPath];
if (process.platform === 'linux' && process.env['CI']) {
args.unshift('--no-sandbox', '--disable-gpu');
}
electronApp = await electron.launch({
args,
env: {
...process.env,
NODE_ENV: 'test',
ELECTRON_IS_DEV: '0',
},
});
// Find the main application window (not DevTools)
mainWindow = await findMainWindow(electronApp);
// Wait for the window to be fully loaded
await mainWindow.waitForLoadState('domcontentloaded');
// Debug: Log the current URL
const url = mainWindow.url();
console.log(`Page URL: ${url}`);
// Wait for Angular to bootstrap and render
await mainWindow.waitForSelector('app-root', { timeout: 30000 });
// Wait for Angular to fully initialize and set the title
await mainWindow.waitForFunction(
() => document.title.length > 0,
{ timeout: 30000 }
);
console.log('Electron app launched successfully');
});
test.afterAll(async () => {
if (electronApp) {
await electronApp.close();
}
});
test('app should start and display main window', async () => {
expect(mainWindow).toBeTruthy();
await mainWindow.screenshot({
path: join(screenshotDir, 'smoke-test-screenshot.png'),
fullPage: true,
});
const title = await mainWindow.title();
console.log(`Window title: ${title}`);
expect(title).toContain('IPTVnator');
});
test('app should have expected window properties', async () => {
const isVisible = await electronApp.evaluate(({ BrowserWindow }) => {
const mainWindow = BrowserWindow.getAllWindows()[0];
return mainWindow ? mainWindow.isVisible() : false;
});
expect(isVisible).toBe(true);
const bounds = await electronApp.evaluate(({ BrowserWindow }) => {
const mainWindow = BrowserWindow.getAllWindows()[0];
return mainWindow ? mainWindow.getBounds() : null;
});
expect(bounds).not.toBeNull();
expect(bounds?.width).toBeGreaterThan(800);
expect(bounds?.height).toBeGreaterThan(600);
await mainWindow.screenshot({
path: join(screenshotDir, 'window-properties-screenshot.png'),
});
});
test('app should render main content', async () => {
// Wait for app-root to have actual content
await mainWindow.waitForFunction(
() => {
const appRoot = document.querySelector('app-root');
return appRoot && appRoot.innerHTML.trim().length > 0;
},
{ timeout: 30000 }
);
const bodyContent = await mainWindow.locator('body').innerHTML();
expect(bodyContent.length).toBeGreaterThan(0);
await mainWindow.screenshot({
path: join(screenshotDir, 'rendered-content-screenshot.png'),
fullPage: true,
});
console.log('Smoke test completed successfully');
});
});
+25
View File
@@ -0,0 +1,25 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"allowJs": true,
"outDir": "../../dist/out-tsc",
"sourceMap": false,
"module": "commonjs",
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"include": [
"**/*.ts",
"**/*.js",
"playwright.config.ts",
"src/**/*.spec.ts",
"src/**/*.spec.js",
"src/**/*.test.ts",
"src/**/*.test.js",
"src/**/*.d.ts",
"src/**/*.e2e.ts"
]
}
+1 -1
View File
@@ -156,7 +156,7 @@ export default class App {
private static loadMainWindow() {
// load the index.html of the app.
if (!App.application.isPackaged) {
if (App.isDevelopmentMode()) {
App.mainWindow.loadURL(`http://localhost:${rendererAppPort}`);
App.mainWindow.webContents.openDevTools();
} else {
-49
View File
@@ -130,8 +130,6 @@
"ng-mocks": "14.14.0",
"nx": "22.3.3",
"nx-electron": "^21.0.1",
"playwright": "1.56.1",
"playwright-core": "1.56.1",
"prettier": "^3.6.2",
"semantic-release": "24.2.9",
"ts-jest": "^29.4.5",
@@ -32101,53 +32099,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/playwright": {
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz",
"integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.56.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz",
"integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/playwright/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/plist": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz",
-2
View File
@@ -154,8 +154,6 @@
"ng-mocks": "14.14.0",
"nx": "22.3.3",
"nx-electron": "^21.0.1",
"playwright": "1.56.1",
"playwright-core": "1.56.1",
"prettier": "^3.6.2",
"semantic-release": "24.2.9",
"ts-jest": "^29.4.5",
-24
View File
@@ -342,12 +342,6 @@ importers:
nx-electron:
specifier: ^21.0.1
version: 21.0.1(@nx/workspace@22.3.3(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.18))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.18)))(@swc/core@1.5.29(@swc/helpers@0.5.18))(electron@39.2.7)(esbuild@0.27.0)(rxjs@7.8.2)(typescript@5.9.3)
playwright:
specifier: 1.56.1
version: 1.56.1
playwright-core:
specifier: 1.56.1
version: 1.56.1
prettier:
specifier: ^3.6.2
version: 3.7.4
@@ -8296,21 +8290,11 @@ packages:
resolution: {integrity: sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==}
engines: {node: '>=14.16'}
playwright-core@1.56.1:
resolution: {integrity: sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==}
engines: {node: '>=18'}
hasBin: true
playwright-core@1.57.0:
resolution: {integrity: sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==}
engines: {node: '>=18'}
hasBin: true
playwright@1.56.1:
resolution: {integrity: sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==}
engines: {node: '>=18'}
hasBin: true
playwright@1.57.0:
resolution: {integrity: sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==}
engines: {node: '>=18'}
@@ -19755,16 +19739,8 @@ snapshots:
dependencies:
find-up: 6.3.0
playwright-core@1.56.1: {}
playwright-core@1.57.0: {}
playwright@1.56.1:
dependencies:
playwright-core: 1.56.1
optionalDependencies:
fsevents: 2.3.2
playwright@1.57.0:
dependencies:
playwright-core: 1.57.0