wip
This commit is contained in:
131
tests/smoke/helpers/console-monitor.ts
Normal file
131
tests/smoke/helpers/console-monitor.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
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) => {
|
||||
this.errors.push({
|
||||
type: 'pageerror',
|
||||
message: error.message,
|
||||
location: error.stack,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
});
|
||||
|
||||
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 = [];
|
||||
}
|
||||
}
|
||||
78
tests/smoke/helpers/electron-test-harness.ts
Normal file
78
tests/smoke/helpers/electron-test-harness.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
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
|
||||
this.app = await electron.launch({
|
||||
args: [electronEntryPath],
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_ENV: 'test',
|
||||
},
|
||||
// Try to disable Chrome DevTools Protocol features that might conflict
|
||||
executablePath: process.env.ELECTRON_EXECUTABLE_PATH,
|
||||
});
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
159
tests/smoke/helpers/ipc-verifier.ts
Normal file
159
tests/smoke/helpers/ipc-verifier.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import { ElectronApplication } from '@playwright/test';
|
||||
|
||||
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 = 'checkAuth';
|
||||
|
||||
try {
|
||||
const result = await this.app.evaluate(async ({ ipcMain }) => {
|
||||
return new Promise((resolve) => {
|
||||
// Simulate IPC call
|
||||
const mockEvent = { reply: (ch: string, data: any) => resolve(data) } as any;
|
||||
const handler = (ipcMain as any).listeners('checkAuth')[0];
|
||||
|
||||
if (!handler) {
|
||||
resolve({ error: 'Handler not registered' });
|
||||
} else {
|
||||
handler(mockEvent);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
channel,
|
||||
success: !result.error,
|
||||
error: result.error,
|
||||
duration: Date.now() - start,
|
||||
};
|
||||
} 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 = 'getBrowserMode';
|
||||
|
||||
try {
|
||||
const result = await this.app.evaluate(async ({ ipcMain }) => {
|
||||
return new Promise((resolve) => {
|
||||
const mockEvent = { reply: (ch: string, data: any) => resolve(data) } as any;
|
||||
const handler = (ipcMain as any).listeners('getBrowserMode')[0];
|
||||
|
||||
if (!handler) {
|
||||
resolve({ error: 'Handler not registered' });
|
||||
} else {
|
||||
handler(mockEvent);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
channel,
|
||||
success: typeof result === 'boolean' || !result.error,
|
||||
error: result.error,
|
||||
duration: Date.now() - start,
|
||||
};
|
||||
} 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 = 'startAutomationSession';
|
||||
|
||||
try {
|
||||
const result = await this.app.evaluate(async ({ ipcMain }) => {
|
||||
return new Promise((resolve) => {
|
||||
const mockEvent = { reply: (ch: string, data: any) => resolve(data) } as any;
|
||||
const handler = (ipcMain as any).listeners('startAutomationSession')[0];
|
||||
|
||||
if (!handler) {
|
||||
resolve({ error: 'Handler not registered' });
|
||||
} else {
|
||||
// Test with mock data
|
||||
handler(mockEvent, { mode: 'test' });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
channel,
|
||||
success: !result.error,
|
||||
error: result.error,
|
||||
duration: Date.now() - start,
|
||||
};
|
||||
} 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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user