Skip to content

feat(testing): Standardize Electron desktop UI test fixtures and native Playwright environment #525

Description

@khaira777

Context & Problem Statement

FloCafe is an offline-first hybrid Electron desktop POS with capability detection (window.electronAPI), platform title bar overlays (native-overlay on macOS, fallback controls on Windows/Linux), window focus/blur dimming, and custom layout rails.

Currently, automated E2E tests run primarily inside standard headless Chromium browsers. Because standard browser sessions lack window.electronAPI and native desktop OS window lifecycle events:

  1. Ad-Hoc Mock Fragility: Tests that need to test desktop chrome must invent inline mocks with page.addInitScript(...). These ad-hoc mocks often miss subtle platform flags, window event bindings (data-flo-window-focused), or IPC subscriptions.
  2. Lack of Native OS Verification: Native desktop presentation (macOS traffic lights clearance, window focus state transitions, titlebar dragging) cannot be tested in a pure headless browser without real Electron windowing.
  3. Execution Overhead & Timeouts: Re-building the frontend and re-inventing mocks in separate tests creates test setup drift, flakiness, and long test execution times.

Goals & Target Architecture

Implement the Dual-Layer Testing Architecture (the modern industry standard for Electron + Playwright apps):

  1. Layer 1: Typed Capability Fixtures (Fast Browser/LAN Tests)
    • Provide a shared, type-safe Playwright fixture (frontend/e2e/helpers/electron-fixture.ts) that standardizes window.electronAPI injection across browser tests in 1 line.
  2. Layer 2: Native Desktop E2E Project (True Electron Window Tests)
    • Configure a dedicated Playwright Electron test project using playwright._electron.launch() to run end-to-end tests against real Electron desktop binaries without mocks.

Step-by-Step Implementation Guide

Step 1: Create Centralized Browser Fixture (frontend/e2e/helpers/electron-fixture.ts)

Create a helper that extends Playwright's test or provides an injectElectronMock function matching frontend/src/types/electron.d.ts:

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

export interface ElectronMockOptions {
  platform?: 'darwin' | 'win32' | 'linux';
  status?: 'ready' | 'loading' | 'error';
  focused?: boolean;
}

/**
 * Injects a fully compliant, typed window.electronAPI mock into the page
 * before application scripts execute.
 */
export async function injectElectronMock(page: Page, options: ElectronMockOptions = {}) {
  const { platform = 'darwin', status = 'ready', focused = true } = options;

  await page.addInitScript(({ platform, status, focused }) => {
    (window as any).electronAPI = {
      platform,
      getStatus: () => Promise.resolve({ status }),
      onStatusChange: (callback: (status: any) => void) => {
        return () => {};
      },
      windowAction: (action: string) => Promise.resolve(),
    };

    // Simulate initial window focus state
    if (focused) {
      document.documentElement.setAttribute('data-flo-window-focused', 'true');
    }
  }, { platform, status, focused });
}

Step 2: Configure Native Electron Project in Playwright (frontend/playwright.config.ts)

Add a dedicated Electron project configuration that points to the compiled Electron main process:

// Add project definition in frontend/playwright.config.ts
{
  name: 'electron-desktop',
  testMatch: /.*\.electron\.spec\.ts/,
  use: {
    // Desktop-specific viewport and settings
    viewport: { width: 1280, height: 800 },
  },
}

Step 3: Create Native Electron Test Suite (frontend/e2e/desktop/title-bar.electron.spec.ts)

Create a real Electron E2E test utilizing _electron.launch:

import { test, expect, _electron as electron } from '@playwright/test';
import path from 'path';

test.describe('Electron Native Window Chrome', () => {
  test('launches desktop app with native titlebar and window controls', async () => {
    const electronApp = await electron.launch({
      args: [path.join(__dirname, '../../../dist/main/main.js')],
    });

    const window = await electronApp.firstWindow();
    await window.waitForLoadState('domcontentloaded');

    // Verify true native runtime flags
    const platform = await window.evaluate(() => window.electronAPI?.platform);
    expect(platform).toBeDefined();

    // Verify titlebar rendering in real Electron
    await expect(window.locator('[data-testid="desktop-title-bar"]')).toBeVisible();

    await electronApp.close();
  });
});

Step 4: Add NPM Script Shortcuts (package.json)

Add dedicated scripts for developer convenience:

"test:e2e:browser": "playwright test --project=chromium",
"test:e2e:electron": "playwright test --project=electron-desktop",
"test:e2e": "playwright test"

Verification Gates & Exit Criteria

To mark this issue complete, the implementation must meet the following criteria:

  • Gate 1: Unit & Type Safety: npm run lint passes with 0 type errors or any violations in the new fixture helper.
  • Gate 2: Browser Fixture Usability: Existing E2E tests (title-bar-platform.spec.ts, etc.) successfully refactored to use injectElectronMock() with 100% pass rate.
  • Gate 3: Native Electron Suite Execution: Running npm run test:e2e:electron launches the Electron window, tests the titlebar, and cleanly exits without leaving zombie background processes or port locks (lsof -i :3001 returns empty).
  • Gate 4: CI Compatibility: Tests pass in both local development and GitHub Actions CI matrix without exceeding execution timeouts.

Exit Strategy & Rollback Plan

  • The fixture helper is strictly additive in frontend/e2e/helpers/ and does not mutate application runtime logic in src/ or main/.
  • If native Electron tests encounter CI runner platform limitations (e.g. headless Linux xvfb requirements), the native Electron project can be gated to local runs while browser fixture tests run on all CI workflows.

References & Documentation

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions