test(electron): add Playwright smoke E2E tests for app

Add a new Electron smoke test suite using Playwright to verify the
packaged application starts, renders the main window, and exposes
expected window properties. The tests launch the built Electron main
bundle, locate the primary (non-DevTools) window, wait for DOM load,
and assert the title contains "IPTVnator". They also check window
visibility and bounds, validate rendered body content, and capture
screenshots to dist/test-results/electron-backend-e2e for debugging.

Include helpers to resolve project paths, ensure the screenshot
directory exists, and robustly find the main application window. Set
NODE_ENV and ELECTRON_IS_DEV to ensure the app loads built artifacts.
This commit is contained in:
4gray committed 2026-01-09 12:53:53 +01:00
1 parent c7fff52bc9
commit e8a80fdb60
6 files changed
+301 -1

No files matched your search

@@ -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"]
}
}
}
+204
View File
@@ -0,0 +1,204 @@
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
// Set ELECTRON_IS_DEV=0 to load from built files instead of dev server
// 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 and page content
const url = mainWindow.url();
console.log(`Page URL: ${url}`);
// Wait a moment and check the HTML content for debugging
await new Promise((resolve) => setTimeout(resolve, 3000));
const htmlContent = await mainWindow.content();
console.log(`HTML content length: ${htmlContent.length}`);
console.log(`HTML snippet: ${htmlContent.substring(0, 500)}`);
// Check for any errors in the page
const errors: string[] = [];
mainWindow.on('pageerror', (error) => {
errors.push(error.message);
console.log(`Page error: ${error.message}`);
});
// Wait for Angular to bootstrap and render (app-root should have content)
try {
await mainWindow.waitForSelector('app-root', { timeout: 30000 });
} catch (e) {
console.log(`Failed to find app-root. Errors: ${errors.join(', ')}`);
console.log(`Final HTML: ${await mainWindow.content()}`);
throw e;
}
// Wait a bit more 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 () => {
// Close the Electron app
if (electronApp) {
await electronApp.close();
}
});
test('app should start and display main window', async () => {
// Check that we have a window
expect(mainWindow).toBeTruthy();
// Take a screenshot of the main window
await mainWindow.screenshot({
path: join(screenshotDir, 'smoke-test-screenshot.png'),
fullPage: true,
});
// Verify the window title contains expected text
const title = await mainWindow.title();
console.log(`Window title: ${title}`);
// IPTVnator should be in the title
expect(title).toContain('IPTVnator');
});
test('app should have expected window properties', async () => {
// Check that the window is visible
const isVisible = await electronApp.evaluate(({ BrowserWindow }) => {
const mainWindow = BrowserWindow.getAllWindows()[0];
return mainWindow ? mainWindow.isVisible() : false;
});
expect(isVisible).toBe(true);
// Check window dimensions are reasonable
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);
// Take another screenshot after property checks
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 (not just the element)
await mainWindow.waitForFunction(
() => {
const appRoot = document.querySelector('app-root');
return appRoot && appRoot.innerHTML.trim().length > 0;
},
{ timeout: 30000 }
);
// Check that the body has content
const bodyContent = await mainWindow.locator('body').innerHTML();
expect(bodyContent.length).toBeGreaterThan(0);
// Take a final screenshot showing the rendered content
await mainWindow.screenshot({
path: join(screenshotDir, 'rendered-content-screenshot.png'),
fullPage: true,
});
console.log(
'Smoke test completed successfully - app is starting and rendering'
);
});
});
+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 {