remove companion tests
This commit is contained in:
@@ -1,40 +0,0 @@
|
||||
import { test, expect } from 'vitest';
|
||||
import { DIContainer } from '@apps/companion/main/di-container';
|
||||
|
||||
test('renderer -> preload -> main: set/get updates BrowserModeConfigLoader (reproduces headless-toggle bug)', () => {
|
||||
// Ensure environment is development so toggle is available
|
||||
(process.env as any).NODE_ENV = 'development';
|
||||
|
||||
// Provide a minimal electron.app mock so DIContainer can resolve paths in node test environment
|
||||
// This avoids calling the real Electron runtime during unit/runner tests.
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const electron = require('electron');
|
||||
electron.app = electron.app || {};
|
||||
electron.app.getAppPath = electron.app.getAppPath || (() => process.cwd());
|
||||
electron.app.isPackaged = electron.app.isPackaged || false;
|
||||
electron.app.getPath = electron.app.getPath || ((p: string) => process.cwd());
|
||||
} catch {
|
||||
// If require('electron') fails, ignore; DIContainer will still attempt to access app and may error.
|
||||
}
|
||||
|
||||
// Reset and get fresh DI container for test isolation
|
||||
DIContainer.resetInstance();
|
||||
const container = DIContainer.getInstance();
|
||||
const loader = container.getBrowserModeConfigLoader();
|
||||
|
||||
// Sanity: toggle visible and default is 'headed' in development
|
||||
expect(process.env.NODE_ENV).toBe('development');
|
||||
expect(loader.getDevelopmentMode()).toBe('headed');
|
||||
|
||||
// Simulate renderer setting to 'headless' via IPC (which should call loader.setDevelopmentMode)
|
||||
loader.setDevelopmentMode('headless');
|
||||
|
||||
// After setting, the loader must reflect new value
|
||||
expect(loader.getDevelopmentMode()).toBe('headless');
|
||||
|
||||
// loader.load() should report the GUI source in development and the updated mode
|
||||
const config = loader.load();
|
||||
expect(config.mode).toBe('headless');
|
||||
expect(config.source).toBe('GUI');
|
||||
});
|
||||
@@ -1,23 +0,0 @@
|
||||
/**
|
||||
* Experimental Playwright+Electron companion boot smoke test (retired).
|
||||
*
|
||||
* This suite attempted to launch the Electron-based companion app via
|
||||
* Playwright's Electron driver, but it cannot run in this environment because
|
||||
* Electron embeds Node.js 16.17.1 while the installed Playwright version
|
||||
* requires Node.js 18 or higher.
|
||||
*
|
||||
* Companion behavior is instead covered by:
|
||||
* - Playwright-based automation E2Es and integrations against fixtures.
|
||||
* - Electron build/init/DI smoke tests.
|
||||
* - Domain and application unit/integration tests.
|
||||
*
|
||||
* This file now contains a minimal Vitest suite to keep the historical
|
||||
* entrypoint discoverable without failing the test runner.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('companion-boot smoke (retired)', () => {
|
||||
it('is documented as retired and covered elsewhere', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,21 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { DIContainer, resolveTemplatePath, resolveSessionDataPath } from '@apps/companion/main/di-container';
|
||||
|
||||
describe('DIContainer (smoke) - test-tolerance', () => {
|
||||
it('constructs without electron.app and exposes path resolvers', () => {
|
||||
// Constructing DIContainer should not throw in plain Node (vitest) environment.
|
||||
expect(() => {
|
||||
// Note: getInstance lazily constructs the container
|
||||
DIContainer.resetInstance();
|
||||
DIContainer.getInstance();
|
||||
}).not.toThrow();
|
||||
|
||||
// Path resolvers should return strings
|
||||
const tpl = resolveTemplatePath();
|
||||
const sess = resolveSessionDataPath();
|
||||
expect(typeof tpl).toBe('string');
|
||||
expect(tpl.length).toBeGreaterThan(0);
|
||||
expect(typeof sess).toBe('string');
|
||||
expect(sess.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
/**
|
||||
* Legacy Electron app smoke suite (superseded).
|
||||
*
|
||||
* Canonical boot coverage now lives in
|
||||
* [companion-boot.smoke.test.ts](tests/smoke/companion-boot.smoke.test.ts).
|
||||
*
|
||||
* This file now contains a minimal Vitest suite to keep the historical
|
||||
* entrypoint discoverable without failing the test runner.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('electron-app smoke (superseded)', () => {
|
||||
it('is documented as superseded and covered elsewhere', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,113 +0,0 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
/**
|
||||
* Electron Build Smoke Test
|
||||
*
|
||||
* Purpose: Detect browser context errors during Electron build.
|
||||
*
|
||||
* This test catches bundling issues where Node.js modules are imported
|
||||
* in the renderer process, causing runtime errors.
|
||||
*
|
||||
* It now runs under the Playwright test runner used by the smoke suite.
|
||||
*/
|
||||
|
||||
test.describe('Electron Build Smoke Tests', () => {
|
||||
test('should build Electron app without browser context errors', () => {
|
||||
// When: Building the Electron companion app
|
||||
let buildOutput: string;
|
||||
|
||||
try {
|
||||
buildOutput = execSync('npm run companion:build', {
|
||||
cwd: process.cwd(),
|
||||
encoding: 'utf-8',
|
||||
stdio: 'pipe',
|
||||
});
|
||||
} catch (error: any) {
|
||||
buildOutput = error.stdout + error.stderr;
|
||||
}
|
||||
|
||||
// Then: Build should not contain externalized module warnings
|
||||
const foundErrors: string[] = [];
|
||||
|
||||
// Split output into lines and check each line
|
||||
const lines = buildOutput.split('\n');
|
||||
lines.forEach((line: string) => {
|
||||
if (line.includes('has been externalized for browser compatibility')) {
|
||||
foundErrors.push(line.trim());
|
||||
}
|
||||
if (line.includes('Cannot access') && line.includes('in client code')) {
|
||||
foundErrors.push(line.trim());
|
||||
}
|
||||
});
|
||||
|
||||
// This WILL FAIL in RED phase due to electron/fs/path being externalized
|
||||
expect(
|
||||
foundErrors.length,
|
||||
`Browser context errors detected during build:\n\n${foundErrors.map((e, i) => `${i + 1}. ${e}`).join('\n')}\n\n` +
|
||||
`These indicate Node.js modules (electron, fs, path) are being imported in renderer code.\n` +
|
||||
`This will cause runtime errors when the app launches.`
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
test('should not import Node.js modules in renderer source code', () => {
|
||||
// Given: Renderer source code
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const rendererPath = path.join(
|
||||
process.cwd(),
|
||||
'apps/companion/renderer'
|
||||
);
|
||||
|
||||
// When: Checking renderer source for forbidden imports
|
||||
const forbiddenPatterns = [
|
||||
{ pattern: /from\s+['"]electron['"]/, name: 'electron' },
|
||||
{ pattern: /require\(['"]electron['"]\)/, name: 'electron' },
|
||||
{ pattern: /from\s+['"]fs['"]/, name: 'fs' },
|
||||
{ pattern: /require\(['"]fs['"]\)/, name: 'fs' },
|
||||
{ pattern: /from\s+['"]path['"]/, name: 'path' },
|
||||
{ pattern: /require\(['"]path['"]\)/, name: 'path' },
|
||||
];
|
||||
|
||||
const violations: Array<{ file: string; line: number; import: string; module: string }> = [];
|
||||
|
||||
function scanDirectory(dir: string) {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
|
||||
entries.forEach((entry: any) => {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
scanDirectory(fullPath);
|
||||
} else if (entry.name.endsWith('.tsx') || entry.name.endsWith('.ts')) {
|
||||
const content = fs.readFileSync(fullPath, 'utf-8');
|
||||
const lines = content.split('\n');
|
||||
|
||||
lines.forEach((line: string, index: number) => {
|
||||
forbiddenPatterns.forEach(({ pattern, name }) => {
|
||||
if (pattern.test(line)) {
|
||||
violations.push({
|
||||
file: path.relative(process.cwd(), fullPath),
|
||||
line: index + 1,
|
||||
import: line.trim(),
|
||||
module: name,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
scanDirectory(rendererPath);
|
||||
|
||||
// Then: No Node.js modules should be imported in renderer
|
||||
expect(
|
||||
violations.length,
|
||||
`Found Node.js module imports in renderer source code:\n\n${
|
||||
violations.map(v => `${v.file}:${v.line}\n Module: ${v.module}\n Code: ${v.import}`).join('\n\n')
|
||||
}\n\nRenderer code must use the preload script or IPC to access Node.js APIs.`
|
||||
).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -1,72 +0,0 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { DIContainer } from '@apps/companion/main/di-container';
|
||||
import { StartAutomationSessionUseCase } from 'apps/companion/main/automation/application/use-cases/StartAutomationSessionUseCase';
|
||||
import { CheckAuthenticationUseCase } from 'apps/companion/main/automation/application/use-cases/CheckAuthenticationUseCase';
|
||||
import { InitiateLoginUseCase } from 'apps/companion/main/automation/application/use-cases/InitiateLoginUseCase';
|
||||
import { ClearSessionUseCase } from 'apps/companion/main/automation/application/use-cases/ClearSessionUseCase';
|
||||
import { ConfirmCheckoutUseCase } from 'apps/companion/main/automation/application/use-cases/ConfirmCheckoutUseCase';
|
||||
import { PlaywrightAutomationAdapter } from 'core/automation/infrastructure//automation';
|
||||
import { InMemorySessionRepository } from 'apps/companion/main/automation/infrastructure/repositories/InMemorySessionRepository';
|
||||
import { NoOpLogAdapter } from '@core/automation/infrastructure//logging/NoOpLogAdapter';
|
||||
|
||||
// Mock Electron's app module
|
||||
vi.mock('electron', () => ({
|
||||
app: {
|
||||
getPath: vi.fn((name: string) => {
|
||||
if (name === 'userData') return '/tmp/test-user-data';
|
||||
return '/tmp/test';
|
||||
}),
|
||||
getAppPath: vi.fn(() => '/tmp/test-app'),
|
||||
isPackaged: false,
|
||||
},
|
||||
}));
|
||||
|
||||
describe('Electron DIContainer Smoke Tests', () => {
|
||||
beforeEach(() => {
|
||||
(DIContainer as unknown as { instance?: unknown }).instance = undefined;
|
||||
});
|
||||
|
||||
it('DIContainer initializes without errors', () => {
|
||||
expect(() => DIContainer.getInstance()).not.toThrow();
|
||||
});
|
||||
|
||||
it('All use cases are accessible', () => {
|
||||
const container = DIContainer.getInstance();
|
||||
|
||||
expect(() => container.getStartAutomationUseCase()).not.toThrow();
|
||||
expect(() => container.getCheckAuthenticationUseCase()).not.toThrow();
|
||||
expect(() => container.getInitiateLoginUseCase()).not.toThrow();
|
||||
expect(() => container.getClearSessionUseCase()).not.toThrow();
|
||||
expect(() => container.getConfirmCheckoutUseCase()).not.toThrow();
|
||||
});
|
||||
|
||||
it('Use case instances are available after initialization', () => {
|
||||
const container = DIContainer.getInstance();
|
||||
|
||||
// Verify all core use cases are available
|
||||
expect(container.getStartAutomationUseCase()).not.toBeNull();
|
||||
expect(container.getStartAutomationUseCase()).toBeDefined();
|
||||
|
||||
// These may be null in test mode, but should not throw
|
||||
expect(() => container.getCheckAuthenticationUseCase()).not.toThrow();
|
||||
expect(() => container.getInitiateLoginUseCase()).not.toThrow();
|
||||
expect(() => container.getClearSessionUseCase()).not.toThrow();
|
||||
});
|
||||
|
||||
it('Container provides access to dependencies', () => {
|
||||
const container = DIContainer.getInstance();
|
||||
|
||||
// Verify core dependencies are accessible
|
||||
expect(container.getSessionRepository()).toBeDefined();
|
||||
expect(container.getAutomationEngine()).toBeDefined();
|
||||
expect(container.getBrowserAutomation()).toBeDefined();
|
||||
expect(container.getLogger()).toBeDefined();
|
||||
});
|
||||
|
||||
it('ConfirmCheckoutUseCase can be verified without errors', () => {
|
||||
const container = DIContainer.getInstance();
|
||||
|
||||
// This getter should not throw even if null (verifies the import)
|
||||
expect(() => container.getConfirmCheckoutUseCase()).not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
/**
|
||||
* Experimental Playwright+Electron companion boot harness (retired).
|
||||
*
|
||||
* This harness attempted to launch the Electron-based companion app via
|
||||
* Playwright's Electron driver, but it cannot run in this environment because
|
||||
* Electron embeds Node.js 16.17.1 while the installed Playwright version
|
||||
* requires Node.js 18 or higher.
|
||||
*
|
||||
* Companion behavior is instead covered by:
|
||||
* - Playwright-based automation E2Es and integrations against fixtures.
|
||||
* - Electron build/init/DI smoke tests.
|
||||
* - Domain and application unit/integration tests.
|
||||
*
|
||||
* This file is intentionally implementation-empty to avoid misleading
|
||||
* Playwright+Electron coverage while keeping the historical entrypoint
|
||||
* discoverable.
|
||||
*/
|
||||
@@ -1,134 +0,0 @@
|
||||
import { Page, ConsoleMessage } from '@playwright/test';
|
||||
|
||||
export interface ConsoleError {
|
||||
type: 'error' | 'warning' | 'pageerror';
|
||||
message: string;
|
||||
location?: string;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* ConsoleMonitor - Aggregates and tracks all console output
|
||||
*
|
||||
* Purpose: Catch ANY runtime errors during Electron app lifecycle
|
||||
*
|
||||
* Critical Detections:
|
||||
* - "Module has been externalized for browser compatibility"
|
||||
* - "__dirname is not defined"
|
||||
* - "require is not defined"
|
||||
* - Any uncaught exceptions
|
||||
*/
|
||||
export class ConsoleMonitor {
|
||||
private errors: ConsoleError[] = [];
|
||||
private warnings: ConsoleError[] = [];
|
||||
private isMonitoring = false;
|
||||
|
||||
/**
|
||||
* Start monitoring console output on the page
|
||||
*/
|
||||
startMonitoring(page: Page): void {
|
||||
if (this.isMonitoring) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Monitor console.error calls
|
||||
page.on('console', (msg: ConsoleMessage) => {
|
||||
if (msg.type() === 'error') {
|
||||
this.errors.push({
|
||||
type: 'error',
|
||||
message: msg.text(),
|
||||
location: msg.location()?.url,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
} else if (msg.type() === 'warning') {
|
||||
this.warnings.push({
|
||||
type: 'warning',
|
||||
message: msg.text(),
|
||||
location: msg.location()?.url,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Monitor uncaught exceptions
|
||||
page.on('pageerror', (error: Error) => {
|
||||
const errorObj: ConsoleError = {
|
||||
type: 'pageerror',
|
||||
message: error.message,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
if (error.stack) {
|
||||
errorObj.location = error.stack;
|
||||
}
|
||||
this.errors.push(errorObj);
|
||||
});
|
||||
|
||||
this.isMonitoring = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any errors were detected
|
||||
*/
|
||||
hasErrors(): boolean {
|
||||
return this.errors.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all detected errors
|
||||
*/
|
||||
getErrors(): ConsoleError[] {
|
||||
return [...this.errors];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all detected warnings
|
||||
*/
|
||||
getWarnings(): ConsoleError[] {
|
||||
return [...this.warnings];
|
||||
}
|
||||
|
||||
/**
|
||||
* Format errors for test output
|
||||
*/
|
||||
formatErrors(): string {
|
||||
if (this.errors.length === 0) {
|
||||
return 'No errors detected';
|
||||
}
|
||||
|
||||
const lines = ['Console errors detected during test:', ''];
|
||||
|
||||
this.errors.forEach((error, index) => {
|
||||
lines.push(`${index + 1}. [${error.type}] ${error.message}`);
|
||||
if (error.location) {
|
||||
lines.push(` Location: ${error.location}`);
|
||||
}
|
||||
lines.push('');
|
||||
});
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for specific browser context errors
|
||||
*/
|
||||
hasBrowserContextErrors(): boolean {
|
||||
const contextErrorPatterns = [
|
||||
/has been externalized for browser compatibility/i,
|
||||
/__dirname is not defined/i,
|
||||
/require is not defined/i,
|
||||
/Cannot access .* in client code/i,
|
||||
];
|
||||
|
||||
return this.errors.some(error =>
|
||||
contextErrorPatterns.some(pattern => pattern.test(error.message))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset monitoring state
|
||||
*/
|
||||
reset(): void {
|
||||
this.errors = [];
|
||||
this.warnings = [];
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
import { _electron as electron, ElectronApplication, Page } from '@playwright/test';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* ElectronTestHarness - Manages Electron app lifecycle for smoke tests
|
||||
*
|
||||
* Responsibilities:
|
||||
* - Launch actual compiled Electron app
|
||||
* - Wait for renderer window to open
|
||||
* - Provide access to main process and renderer page
|
||||
* - Clean shutdown
|
||||
*/
|
||||
export class ElectronTestHarness {
|
||||
private app: ElectronApplication | null = null;
|
||||
private mainWindow: Page | null = null;
|
||||
|
||||
/**
|
||||
* Launch Electron app and wait for main window
|
||||
*
|
||||
* @throws Error if app fails to launch or window doesn't open
|
||||
*/
|
||||
async launch(): Promise<void> {
|
||||
// Path to the built Electron app entry point
|
||||
const electronEntryPath = path.join(__dirname, '../../../apps/companion/dist/main/main.cjs');
|
||||
|
||||
// Launch Electron app with the compiled entry file
|
||||
// Note: Playwright may have compatibility issues with certain Electron versions
|
||||
// regarding --remote-debugging-port flag
|
||||
const launchOptions: any = {
|
||||
args: [electronEntryPath],
|
||||
env: {
|
||||
...Object.fromEntries(Object.entries(process.env).filter(([_, v]) => v !== undefined)),
|
||||
NODE_ENV: 'test',
|
||||
},
|
||||
};
|
||||
if (process.env.ELECTRON_EXECUTABLE_PATH) {
|
||||
launchOptions.executablePath = process.env.ELECTRON_EXECUTABLE_PATH;
|
||||
}
|
||||
this.app = await electron.launch(launchOptions);
|
||||
|
||||
// Wait for first window (renderer process)
|
||||
this.mainWindow = await this.app.firstWindow({
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
// Wait for React to render
|
||||
await this.mainWindow.waitForLoadState('domcontentloaded');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the main renderer window
|
||||
*/
|
||||
getMainWindow(): Page {
|
||||
if (!this.mainWindow) {
|
||||
throw new Error('Main window not available. Did you call launch()?');
|
||||
}
|
||||
return this.mainWindow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Electron app instance for IPC testing
|
||||
*/
|
||||
getApp(): ElectronApplication {
|
||||
if (!this.app) {
|
||||
throw new Error('Electron app not available. Did you call launch()?');
|
||||
}
|
||||
return this.app;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean shutdown of Electron app
|
||||
*/
|
||||
async close(): Promise<void> {
|
||||
if (this.app) {
|
||||
await this.app.close();
|
||||
this.app = null;
|
||||
this.mainWindow = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,228 +0,0 @@
|
||||
import { ElectronApplication } from '@playwright/test';
|
||||
|
||||
type IpcHandlerResult = {
|
||||
error?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export interface IPCTestResult {
|
||||
channel: string;
|
||||
success: boolean;
|
||||
error?: string;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* IPCVerifier - Tests IPC channel contracts
|
||||
*
|
||||
* Purpose: Verify main <-> renderer communication works
|
||||
* Scope: Core IPC channels required for app functionality
|
||||
*/
|
||||
export class IPCVerifier {
|
||||
constructor(private app: ElectronApplication) {}
|
||||
|
||||
/**
|
||||
* Test checkAuth IPC channel
|
||||
*/
|
||||
async testCheckAuth(): Promise<IPCTestResult> {
|
||||
const start = Date.now();
|
||||
const channel = 'auth:check';
|
||||
|
||||
try {
|
||||
const result = await this.app.evaluate(
|
||||
async ({ ipcMain }: { ipcMain: { listeners: (channel: string) => unknown[] } }) => {
|
||||
return new Promise((resolve) => {
|
||||
// Simulate IPC invoke handler by calling the first registered handler for the channel
|
||||
const handlers = ipcMain.listeners('auth:check') || [];
|
||||
const handler = handlers[0] as
|
||||
| ((event: unknown, ...args: unknown[]) => unknown | Promise<unknown>)
|
||||
| undefined;
|
||||
|
||||
if (!handler) {
|
||||
resolve({ error: 'Handler not registered' });
|
||||
} else {
|
||||
// Invoke the handler similar to ipcMain.handle invocation signature
|
||||
// (event, ...args) => Promise
|
||||
const mockEvent: unknown = {};
|
||||
Promise.resolve(handler(mockEvent))
|
||||
.then((res: unknown) => resolve(res))
|
||||
.catch((err: unknown) =>
|
||||
resolve({
|
||||
error:
|
||||
err && err instanceof Error && err.message
|
||||
? err.message
|
||||
: String(err),
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const typed: IpcHandlerResult = result as IpcHandlerResult;
|
||||
|
||||
const resultObj: IPCTestResult = {
|
||||
channel,
|
||||
success: !typed.error,
|
||||
duration: Date.now() - start,
|
||||
};
|
||||
if (typed.error) {
|
||||
resultObj.error = typed.error;
|
||||
}
|
||||
return resultObj;
|
||||
} catch (error) {
|
||||
return {
|
||||
channel,
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
duration: Date.now() - start,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test getBrowserMode IPC channel
|
||||
*/
|
||||
async testGetBrowserMode(): Promise<IPCTestResult> {
|
||||
const start = Date.now();
|
||||
const channel = 'browser-mode:get';
|
||||
|
||||
try {
|
||||
const result = await this.app.evaluate(
|
||||
async ({ ipcMain }: { ipcMain: { listeners: (channel: string) => unknown[] } }) => {
|
||||
return new Promise((resolve) => {
|
||||
const handlers = ipcMain.listeners('browser-mode:get') || [];
|
||||
const handler = handlers[0] as
|
||||
| ((event: unknown, ...args: unknown[]) => unknown | Promise<unknown>)
|
||||
| undefined;
|
||||
|
||||
if (!handler) {
|
||||
resolve({ error: 'Handler not registered' });
|
||||
} else {
|
||||
const mockEvent: unknown = {};
|
||||
Promise.resolve(handler(mockEvent))
|
||||
.then((res: unknown) => resolve(res))
|
||||
.catch((err: unknown) =>
|
||||
resolve({
|
||||
error:
|
||||
err && err instanceof Error && err.message
|
||||
? err.message
|
||||
: String(err),
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const typed: IpcHandlerResult = result as IpcHandlerResult;
|
||||
|
||||
const resultObj: IPCTestResult = {
|
||||
channel,
|
||||
success: !typed.error,
|
||||
duration: Date.now() - start,
|
||||
};
|
||||
if (typed.error) {
|
||||
resultObj.error = typed.error;
|
||||
}
|
||||
return resultObj;
|
||||
} catch (error) {
|
||||
return {
|
||||
channel,
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
duration: Date.now() - start,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test startAutomationSession IPC channel contract
|
||||
*/
|
||||
async testStartAutomationSession(): Promise<IPCTestResult> {
|
||||
const start = Date.now();
|
||||
const channel = 'start-automation';
|
||||
|
||||
try {
|
||||
const result = await this.app.evaluate(
|
||||
async ({ ipcMain }: { ipcMain: { listeners: (channel: string) => unknown[] } }) => {
|
||||
return new Promise((resolve) => {
|
||||
const handlers = ipcMain.listeners('start-automation') || [];
|
||||
const handler = handlers[0] as
|
||||
| ((
|
||||
event: unknown,
|
||||
payload: { sessionName: string; mode: string },
|
||||
) => unknown | Promise<unknown>)
|
||||
| undefined;
|
||||
|
||||
if (!handler) {
|
||||
resolve({ error: 'Handler not registered' });
|
||||
} else {
|
||||
// Test with mock data
|
||||
const mockEvent: unknown = {};
|
||||
Promise.resolve(
|
||||
handler(mockEvent, { sessionName: 'test', mode: 'test' }),
|
||||
)
|
||||
.then((res: unknown) => resolve(res))
|
||||
.catch((err: unknown) =>
|
||||
resolve({
|
||||
error:
|
||||
err && err instanceof Error && err.message
|
||||
? err.message
|
||||
: String(err),
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const typed: IpcHandlerResult = result as IpcHandlerResult;
|
||||
|
||||
const resultObj: IPCTestResult = {
|
||||
channel,
|
||||
success: !typed.error,
|
||||
duration: Date.now() - start,
|
||||
};
|
||||
if (typed.error) {
|
||||
resultObj.error = typed.error;
|
||||
}
|
||||
return resultObj;
|
||||
} catch (error) {
|
||||
return {
|
||||
channel,
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
duration: Date.now() - start,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run all IPC tests and return results
|
||||
*/
|
||||
async verifyAllChannels(): Promise<IPCTestResult[]> {
|
||||
return Promise.all([
|
||||
this.testCheckAuth(),
|
||||
this.testGetBrowserMode(),
|
||||
this.testStartAutomationSession(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format IPC test results for output
|
||||
*/
|
||||
static formatResults(results: IPCTestResult[]): string {
|
||||
const lines = ['IPC Channel Verification:', ''];
|
||||
|
||||
results.forEach(result => {
|
||||
const status = result.success ? '✓' : '✗';
|
||||
lines.push(`${status} ${result.channel} (${result.duration}ms)`);
|
||||
if (result.error) {
|
||||
lines.push(` Error: ${result.error}`);
|
||||
}
|
||||
});
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
import { describe, it, expect, afterEach, beforeAll, afterAll } from 'vitest';
|
||||
import { PlaywrightAutomationAdapter, FixtureServer } from 'core/automation/infrastructure//automation';
|
||||
import { NoOpLogAdapter } from '@core/automation/infrastructure//logging/NoOpLogAdapter';
|
||||
|
||||
describe('Playwright Adapter Smoke Tests', () => {
|
||||
let adapter: PlaywrightAutomationAdapter | undefined;
|
||||
let server: FixtureServer | undefined;
|
||||
let unhandledRejectionHandler: ((reason: unknown) => void) | null = null;
|
||||
const logger = new NoOpLogAdapter();
|
||||
|
||||
beforeAll(() => {
|
||||
unhandledRejectionHandler = (reason: unknown) => {
|
||||
const message =
|
||||
reason instanceof Error ? reason.message : String(reason ?? '');
|
||||
if (message.includes('cdpSession.send: Target page, context or browser has been closed')) {
|
||||
return;
|
||||
}
|
||||
throw reason;
|
||||
};
|
||||
(process as any).on('unhandledRejection', unhandledRejectionHandler);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (adapter) {
|
||||
try {
|
||||
await adapter.disconnect();
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
adapter = undefined;
|
||||
}
|
||||
if (server) {
|
||||
try {
|
||||
await server.stop();
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
server = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (unhandledRejectionHandler) {
|
||||
(process as any).removeListener('unhandledRejection', unhandledRejectionHandler);
|
||||
unhandledRejectionHandler = null;
|
||||
}
|
||||
});
|
||||
|
||||
it('Adapter instantiates without errors', () => {
|
||||
expect(() => {
|
||||
adapter = new PlaywrightAutomationAdapter({
|
||||
headless: true,
|
||||
mode: 'mock',
|
||||
timeout: 5000,
|
||||
}, logger);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('Browser connects successfully', async () => {
|
||||
adapter = new PlaywrightAutomationAdapter({
|
||||
headless: true,
|
||||
mode: 'mock',
|
||||
timeout: 5000,
|
||||
}, logger);
|
||||
|
||||
const result = await adapter.connect();
|
||||
expect(result.success).toBe(true);
|
||||
expect(adapter.isConnected()).toBe(true);
|
||||
});
|
||||
|
||||
it('Basic navigation works with mock fixtures', async () => {
|
||||
server = new FixtureServer();
|
||||
await server.start();
|
||||
|
||||
adapter = new PlaywrightAutomationAdapter({
|
||||
headless: true,
|
||||
mode: 'mock',
|
||||
timeout: 5000,
|
||||
}, logger);
|
||||
|
||||
await adapter.connect();
|
||||
const navResult = await adapter.navigateToPage(server.getFixtureUrl(2));
|
||||
expect(navResult.success).toBe(true);
|
||||
});
|
||||
|
||||
it('Adapter can be instantiated multiple times', () => {
|
||||
expect(() => {
|
||||
const adapter1 = new PlaywrightAutomationAdapter({
|
||||
headless: true,
|
||||
mode: 'mock',
|
||||
timeout: 5000,
|
||||
}, logger);
|
||||
const adapter2 = new PlaywrightAutomationAdapter({
|
||||
headless: true,
|
||||
mode: 'mock',
|
||||
timeout: 5000,
|
||||
}, logger);
|
||||
expect(adapter1).not.toBe(adapter2);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('FixtureServer starts and stops cleanly', async () => {
|
||||
server = new FixtureServer();
|
||||
|
||||
await expect(server.start()).resolves.not.toThrow();
|
||||
expect(server.getFixtureUrl(2)).toContain('http://localhost:');
|
||||
await expect(server.stop()).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user