diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 483910551..c82d415d9 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -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": [] diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml deleted file mode 100644 index ad634d39d..000000000 --- a/.github/workflows/build-and-test.yaml +++ /dev/null @@ -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/** diff --git a/.github/workflows/e2e-tests.yaml b/.github/workflows/e2e-tests.yaml new file mode 100644 index 000000000..7a068e0ff --- /dev/null +++ b/.github/workflows/e2e-tests.yaml @@ -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 diff --git a/apps/electron-backend-e2e/eslint.config.mjs b/apps/electron-backend-e2e/eslint.config.mjs new file mode 100644 index 000000000..b2e9fac09 --- /dev/null +++ b/apps/electron-backend-e2e/eslint.config.mjs @@ -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: {}, + }, +]; diff --git a/apps/electron-backend-e2e/playwright.config.ts b/apps/electron-backend-e2e/playwright.config.ts new file mode 100644 index 000000000..47244390a --- /dev/null +++ b/apps/electron-backend-e2e/playwright.config.ts @@ -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: {}, + }, + ], +}); diff --git a/apps/electron-backend-e2e/project.json b/apps/electron-backend-e2e/project.json new file mode 100644 index 000000000..c239c4432 --- /dev/null +++ b/apps/electron-backend-e2e/project.json @@ -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"] + } + } +} diff --git a/apps/electron-backend-e2e/src/smoke.e2e.ts b/apps/electron-backend-e2e/src/smoke.e2e.ts new file mode 100644 index 000000000..7d5ea35a9 --- /dev/null +++ b/apps/electron-backend-e2e/src/smoke.e2e.ts @@ -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 { + // 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'); + }); +}); diff --git a/apps/electron-backend-e2e/tsconfig.json b/apps/electron-backend-e2e/tsconfig.json new file mode 100644 index 000000000..32bbdccb5 --- /dev/null +++ b/apps/electron-backend-e2e/tsconfig.json @@ -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" + ] +} diff --git a/apps/electron-backend/src/app/app.ts b/apps/electron-backend/src/app/app.ts index 07e9d479c..847f0e20d 100644 --- a/apps/electron-backend/src/app/app.ts +++ b/apps/electron-backend/src/app/app.ts @@ -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 { diff --git a/package-lock.json b/package-lock.json index f06c28052..7bb5ded7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 7cb3d4e89..90ec715bb 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ca93d154f..9ae73afa1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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