feat(logging): add professional logging with pino and headless E2E tests - Add ILogger port interface in application layer - Implement PinoLogAdapter with Electron-compatible structured logging - Add NoOpLogAdapter for testing - Wire logging into DI container and all adapters - Create 32 E2E tests for automation workflow (headless-only) - Add vitest.e2e.config.ts for E2E test configuration - All tests enforce HEADLESS mode (no headed browser allowed)
This commit is contained in:
@@ -5,22 +5,41 @@ import { NutJsAutomationAdapter } from '@/packages/infrastructure/adapters/autom
|
||||
import { MockAutomationEngineAdapter } from '@/packages/infrastructure/adapters/automation/MockAutomationEngineAdapter';
|
||||
import { StartAutomationSessionUseCase } from '@/packages/application/use-cases/StartAutomationSessionUseCase';
|
||||
import { loadAutomationConfig, AutomationMode } from '@/packages/infrastructure/config';
|
||||
import { PinoLogAdapter } from '@/packages/infrastructure/adapters/logging/PinoLogAdapter';
|
||||
import { NoOpLogAdapter } from '@/packages/infrastructure/adapters/logging/NoOpLogAdapter';
|
||||
import { loadLoggingConfig } from '@/packages/infrastructure/config/LoggingConfig';
|
||||
import type { ISessionRepository } from '@/packages/application/ports/ISessionRepository';
|
||||
import type { IBrowserAutomation } from '@/packages/application/ports/IBrowserAutomation';
|
||||
import type { IAutomationEngine } from '@/packages/application/ports/IAutomationEngine';
|
||||
import type { ILogger } from '@/packages/application/ports/ILogger';
|
||||
|
||||
export interface BrowserConnectionResult {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create logger based on environment configuration.
|
||||
* In test environment, returns NoOpLogAdapter for silent logging.
|
||||
*/
|
||||
function createLogger(): ILogger {
|
||||
const config = loadLoggingConfig();
|
||||
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
return new NoOpLogAdapter();
|
||||
}
|
||||
|
||||
return new PinoLogAdapter(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create browser automation adapter based on configuration mode.
|
||||
*
|
||||
* @param mode - The automation mode from configuration
|
||||
* @param logger - Logger instance for the adapter
|
||||
* @returns IBrowserAutomation adapter instance
|
||||
*/
|
||||
function createBrowserAutomationAdapter(mode: AutomationMode): IBrowserAutomation {
|
||||
function createBrowserAutomationAdapter(mode: AutomationMode, logger: ILogger): IBrowserAutomation {
|
||||
const config = loadAutomationConfig();
|
||||
|
||||
switch (mode) {
|
||||
@@ -29,14 +48,14 @@ function createBrowserAutomationAdapter(mode: AutomationMode): IBrowserAutomatio
|
||||
debuggingPort: config.devTools?.debuggingPort,
|
||||
browserWSEndpoint: config.devTools?.browserWSEndpoint,
|
||||
defaultTimeout: config.defaultTimeout,
|
||||
});
|
||||
}, logger.child({ adapter: 'BrowserDevTools' }));
|
||||
|
||||
case 'production':
|
||||
return new NutJsAutomationAdapter({
|
||||
mouseSpeed: config.nutJs?.mouseSpeed,
|
||||
keyboardDelay: config.nutJs?.keyboardDelay,
|
||||
defaultTimeout: config.defaultTimeout,
|
||||
});
|
||||
}, logger.child({ adapter: 'NutJs' }));
|
||||
|
||||
case 'mock':
|
||||
default:
|
||||
@@ -47,6 +66,7 @@ function createBrowserAutomationAdapter(mode: AutomationMode): IBrowserAutomatio
|
||||
export class DIContainer {
|
||||
private static instance: DIContainer;
|
||||
|
||||
private logger: ILogger;
|
||||
private sessionRepository: ISessionRepository;
|
||||
private browserAutomation: IBrowserAutomation;
|
||||
private automationEngine: IAutomationEngine;
|
||||
@@ -54,11 +74,15 @@ export class DIContainer {
|
||||
private automationMode: AutomationMode;
|
||||
|
||||
private constructor() {
|
||||
// Initialize logger first - it's needed by other components
|
||||
this.logger = createLogger();
|
||||
this.logger.info('DIContainer initializing', { automationMode: process.env.AUTOMATION_MODE });
|
||||
|
||||
const config = loadAutomationConfig();
|
||||
this.automationMode = config.mode;
|
||||
|
||||
this.sessionRepository = new InMemorySessionRepository();
|
||||
this.browserAutomation = createBrowserAutomationAdapter(config.mode);
|
||||
this.browserAutomation = createBrowserAutomationAdapter(config.mode, this.logger);
|
||||
this.automationEngine = new MockAutomationEngineAdapter(
|
||||
this.browserAutomation,
|
||||
this.sessionRepository
|
||||
@@ -68,6 +92,20 @@ export class DIContainer {
|
||||
this.browserAutomation,
|
||||
this.sessionRepository
|
||||
);
|
||||
|
||||
this.logger.info('DIContainer initialized', {
|
||||
automationMode: config.mode,
|
||||
sessionRepositoryType: 'InMemorySessionRepository',
|
||||
browserAutomationType: this.getBrowserAutomationType(config.mode)
|
||||
});
|
||||
}
|
||||
|
||||
private getBrowserAutomationType(mode: AutomationMode): string {
|
||||
switch (mode) {
|
||||
case 'dev': return 'BrowserDevToolsAdapter';
|
||||
case 'production': return 'NutJsAutomationAdapter';
|
||||
default: return 'MockBrowserAutomationAdapter';
|
||||
}
|
||||
}
|
||||
|
||||
public static getInstance(): DIContainer {
|
||||
@@ -97,21 +135,30 @@ export class DIContainer {
|
||||
return this.browserAutomation;
|
||||
}
|
||||
|
||||
public getLogger(): ILogger {
|
||||
return this.logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize browser connection for dev mode.
|
||||
* In dev mode, connects to the browser via Chrome DevTools Protocol.
|
||||
* In mock mode, returns success immediately (no connection needed).
|
||||
*/
|
||||
public async initializeBrowserConnection(): Promise<BrowserConnectionResult> {
|
||||
this.logger.info('Initializing browser connection', { mode: this.automationMode });
|
||||
|
||||
if (this.automationMode === 'dev') {
|
||||
try {
|
||||
const devToolsAdapter = this.browserAutomation as BrowserDevToolsAdapter;
|
||||
await devToolsAdapter.connect();
|
||||
this.logger.info('Browser connection established', { mode: 'dev', adapter: 'BrowserDevTools' });
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : 'Failed to connect to browser';
|
||||
this.logger.error('Browser connection failed', error instanceof Error ? error : new Error(errorMsg), { mode: 'dev' });
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to connect to browser'
|
||||
error: errorMsg
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -120,16 +167,21 @@ export class DIContainer {
|
||||
const nutJsAdapter = this.browserAutomation as NutJsAutomationAdapter;
|
||||
const result = await nutJsAdapter.connect();
|
||||
if (!result.success) {
|
||||
this.logger.error('Browser connection failed', new Error(result.error || 'Unknown error'), { mode: 'production' });
|
||||
return { success: false, error: result.error };
|
||||
}
|
||||
this.logger.info('Browser connection established', { mode: 'production', adapter: 'NutJs' });
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : 'Failed to initialize nut.js';
|
||||
this.logger.error('Browser connection failed', error instanceof Error ? error : new Error(errorMsg), { mode: 'production' });
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to initialize nut.js'
|
||||
error: errorMsg
|
||||
};
|
||||
}
|
||||
}
|
||||
this.logger.debug('Mock mode - no browser connection needed');
|
||||
return { success: true }; // Mock mode doesn't need connection
|
||||
}
|
||||
|
||||
|
||||
436
package-lock.json
generated
436
package-lock.json
generated
@@ -11,9 +11,15 @@
|
||||
"packages/*",
|
||||
"apps/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"pino": "^10.1.0",
|
||||
"pino-roll": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cucumber/cucumber": "^11.0.1",
|
||||
"@vitest/ui": "^2.1.8",
|
||||
"pino-pretty": "^13.1.2",
|
||||
"puppeteer": "^24.31.0",
|
||||
"typescript": "^5.7.2",
|
||||
"vitest": "^2.1.8"
|
||||
},
|
||||
@@ -1767,6 +1773,12 @@
|
||||
"node-abort-controller": "3.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@pinojs/redact": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz",
|
||||
"integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@pkgjs/parseargs": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
@@ -2543,6 +2555,13 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"dev": true,
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/assertion-error": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||
@@ -2577,6 +2596,15 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/atomic-sleep": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
|
||||
"integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
@@ -2886,6 +2914,16 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/callsites": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001756",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001756.tgz",
|
||||
@@ -3118,6 +3156,13 @@
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/colorette": {
|
||||
"version": "2.0.20",
|
||||
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
|
||||
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
|
||||
@@ -3135,6 +3180,52 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cosmiconfig": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz",
|
||||
"integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"env-paths": "^2.2.1",
|
||||
"import-fresh": "^3.3.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"parse-json": "^5.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/d-fischer"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.9.5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cosmiconfig/node_modules/parse-json": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
|
||||
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.0.0",
|
||||
"error-ex": "^1.3.1",
|
||||
"json-parse-even-better-errors": "^2.3.0",
|
||||
"lines-and-columns": "^1.1.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "6.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz",
|
||||
@@ -3176,6 +3267,26 @@
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/dateformat": {
|
||||
"version": "4.6.3",
|
||||
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz",
|
||||
"integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
@@ -3398,6 +3509,16 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/error-ex": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
|
||||
"integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-arrayish": "^0.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/error-stack-parser": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
|
||||
@@ -3669,12 +3790,26 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-copy": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz",
|
||||
"integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-fifo": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
|
||||
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-safe-stringify": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
|
||||
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fd-slicer": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
|
||||
@@ -4135,6 +4270,13 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/help-me": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
|
||||
"integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/hosted-git-info": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz",
|
||||
@@ -4237,6 +4379,23 @@
|
||||
"integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"parent-module": "^1.0.0",
|
||||
"resolve-from": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/indent-string": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
|
||||
@@ -4279,6 +4438,13 @@
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/is-arrayish": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
|
||||
"integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-docker": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
|
||||
@@ -4405,6 +4571,16 @@
|
||||
"regenerator-runtime": "^0.13.3"
|
||||
}
|
||||
},
|
||||
"node_modules/joycon": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
|
||||
"integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/jpeg-js": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
|
||||
@@ -4417,6 +4593,19 @@
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/jsesc": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
||||
@@ -4437,6 +4626,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-parse-even-better-errors": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
|
||||
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-stringify-safe": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||
@@ -4488,6 +4684,13 @@
|
||||
"seed-random": "~2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lines-and-columns": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
||||
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/load-bmfont": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/load-bmfont/-/load-bmfont-1.4.2.tgz",
|
||||
@@ -4682,6 +4885,16 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
@@ -4894,6 +5107,15 @@
|
||||
"integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/on-exit-leak-free": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
|
||||
"integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
@@ -4980,6 +5202,19 @@
|
||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/parent-module": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"callsites": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/parse-bmfont-ascii": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/parse-bmfont-ascii/-/parse-bmfont-ascii-1.0.6.tgz",
|
||||
@@ -5128,6 +5363,78 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/pino": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pino/-/pino-10.1.0.tgz",
|
||||
"integrity": "sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pinojs/redact": "^0.4.0",
|
||||
"atomic-sleep": "^1.0.0",
|
||||
"on-exit-leak-free": "^2.1.0",
|
||||
"pino-abstract-transport": "^2.0.0",
|
||||
"pino-std-serializers": "^7.0.0",
|
||||
"process-warning": "^5.0.0",
|
||||
"quick-format-unescaped": "^4.0.3",
|
||||
"real-require": "^0.2.0",
|
||||
"safe-stable-stringify": "^2.3.1",
|
||||
"sonic-boom": "^4.0.1",
|
||||
"thread-stream": "^3.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"pino": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/pino-abstract-transport": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz",
|
||||
"integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"split2": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pino-pretty": {
|
||||
"version": "13.1.2",
|
||||
"resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.2.tgz",
|
||||
"integrity": "sha512-3cN0tCakkT4f3zo9RXDIhy6GTvtYD6bK4CRBLN9j3E/ePqN1tugAXD5rGVfoChW6s0hiek+eyYlLNqc/BG7vBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"colorette": "^2.0.7",
|
||||
"dateformat": "^4.6.3",
|
||||
"fast-copy": "^3.0.2",
|
||||
"fast-safe-stringify": "^2.1.1",
|
||||
"help-me": "^5.0.0",
|
||||
"joycon": "^3.1.1",
|
||||
"minimist": "^1.2.6",
|
||||
"on-exit-leak-free": "^2.1.0",
|
||||
"pino-abstract-transport": "^2.0.0",
|
||||
"pump": "^3.0.0",
|
||||
"secure-json-parse": "^4.0.0",
|
||||
"sonic-boom": "^4.0.1",
|
||||
"strip-json-comments": "^5.0.2"
|
||||
},
|
||||
"bin": {
|
||||
"pino-pretty": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/pino-roll": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pino-roll/-/pino-roll-4.0.0.tgz",
|
||||
"integrity": "sha512-axI1aQaIxXdw1F4OFFli1EDxIrdYNGLowkw/ZoZogX8oCSLHUghzwVVXUS8U+xD/Savwa5IXpiXmsSGKFX/7Sg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"date-fns": "^4.1.0",
|
||||
"sonic-boom": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/pino-std-serializers": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz",
|
||||
"integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pixelmatch": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-4.0.2.tgz",
|
||||
@@ -5196,6 +5503,22 @@
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/process-warning": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz",
|
||||
"integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/progress": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||
@@ -5256,6 +5579,28 @@
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/puppeteer": {
|
||||
"version": "24.31.0",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.31.0.tgz",
|
||||
"integrity": "sha512-q8y5yLxLD8xdZdzNWqdOL43NbfvUOp60SYhaLZQwHC9CdKldxQKXOyJAciOr7oUJfyAH/KgB2wKvqT2sFKoVXA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@puppeteer/browsers": "2.10.13",
|
||||
"chromium-bidi": "11.0.0",
|
||||
"cosmiconfig": "^9.0.0",
|
||||
"devtools-protocol": "0.0.1521046",
|
||||
"puppeteer-core": "24.31.0",
|
||||
"typed-query-selector": "^2.12.0"
|
||||
},
|
||||
"bin": {
|
||||
"puppeteer": "lib/cjs/puppeteer/node/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/puppeteer-core": {
|
||||
"version": "24.31.0",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.31.0.tgz",
|
||||
@@ -5274,6 +5619,12 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/quick-format-unescaped": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
|
||||
"integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/quick-lru": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
|
||||
@@ -5416,6 +5767,15 @@
|
||||
"url": "https://github.com/sponsors/Borewit"
|
||||
}
|
||||
},
|
||||
"node_modules/real-require": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
|
||||
"integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/reflect-metadata": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
|
||||
@@ -5475,6 +5835,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/resolve-from": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/responselike": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
|
||||
@@ -5569,6 +5939,15 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safe-stable-stringify": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
|
||||
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/sax": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz",
|
||||
@@ -5584,6 +5963,23 @@
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/secure-json-parse": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz",
|
||||
"integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/seed-random": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/seed-random/-/seed-random-2.2.0.tgz",
|
||||
@@ -5730,6 +6126,15 @@
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/sonic-boom": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz",
|
||||
"integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"atomic-sleep": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
@@ -5797,6 +6202,15 @@
|
||||
"dev": true,
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/split2": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
|
||||
@@ -5992,6 +6406,19 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-json-comments": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz",
|
||||
"integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/strtok3": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz",
|
||||
@@ -6123,6 +6550,15 @@
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/thread-stream": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz",
|
||||
"integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"real-require": "^0.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/timm": {
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/timm/-/timm-1.7.1.tgz",
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"test": "vitest run",
|
||||
"test:unit": "vitest run tests/unit",
|
||||
"test:integration": "vitest run tests/integration",
|
||||
"test:e2e": "vitest run tests/e2e",
|
||||
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
||||
"test:watch": "vitest watch",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"companion": "npm run companion:build --workspace=@gridpilot/companion && npm run start --workspace=@gridpilot/companion",
|
||||
@@ -28,8 +28,13 @@
|
||||
"devDependencies": {
|
||||
"@cucumber/cucumber": "^11.0.1",
|
||||
"@vitest/ui": "^2.1.8",
|
||||
"pino-pretty": "^13.1.2",
|
||||
"puppeteer": "^24.31.0",
|
||||
"typescript": "^5.7.2",
|
||||
"vitest": "^2.1.8"
|
||||
},
|
||||
"dependencies": {}
|
||||
"dependencies": {
|
||||
"pino": "^10.1.0",
|
||||
"pino-roll": "^4.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
35
packages/application/ports/ILogger.ts
Normal file
35
packages/application/ports/ILogger.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Log levels in order of severity (lowest to highest)
|
||||
*/
|
||||
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'fatal';
|
||||
|
||||
/**
|
||||
* Contextual metadata attached to log entries
|
||||
*/
|
||||
export interface LogContext {
|
||||
/** Unique session identifier for correlation */
|
||||
sessionId?: string;
|
||||
/** Current automation step (1-18) */
|
||||
stepId?: number;
|
||||
/** Step name for human readability */
|
||||
stepName?: string;
|
||||
/** Adapter or component name */
|
||||
adapter?: string;
|
||||
/** Operation duration in milliseconds */
|
||||
durationMs?: number;
|
||||
/** Additional arbitrary metadata */
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* ILogger - Port interface for application-layer logging.
|
||||
*/
|
||||
export interface ILogger {
|
||||
debug(message: string, context?: LogContext): void;
|
||||
info(message: string, context?: LogContext): void;
|
||||
warn(message: string, context?: LogContext): void;
|
||||
error(message: string, error?: Error, context?: LogContext): void;
|
||||
fatal(message: string, error?: Error, context?: LogContext): void;
|
||||
child(context: LogContext): ILogger;
|
||||
flush(): Promise<void>;
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
AutomationResult,
|
||||
} from '../../../packages/application/ports/AutomationResults';
|
||||
import { IRacingSelectorMap, getStepSelectors, getStepName, isModalStep } from './selectors/IRacingSelectorMap';
|
||||
import type { ILogger, LogContext } from '../../../application/ports/ILogger';
|
||||
import { NoOpLogAdapter } from '../logging/NoOpLogAdapter';
|
||||
|
||||
/**
|
||||
* Configuration for connecting to browser via Chrome DevTools Protocol
|
||||
@@ -55,8 +57,9 @@ export class BrowserDevToolsAdapter implements IBrowserAutomation {
|
||||
private page: Page | null = null;
|
||||
private config: Required<DevToolsConfig>;
|
||||
private connected: boolean = false;
|
||||
private logger: ILogger;
|
||||
|
||||
constructor(config: DevToolsConfig = {}) {
|
||||
constructor(config: DevToolsConfig = {}, logger?: ILogger) {
|
||||
this.config = {
|
||||
browserWSEndpoint: config.browserWSEndpoint ?? '',
|
||||
debuggingPort: config.debuggingPort ?? 9222,
|
||||
@@ -64,6 +67,7 @@ export class BrowserDevToolsAdapter implements IBrowserAutomation {
|
||||
typingDelay: config.typingDelay ?? 50,
|
||||
waitForNetworkIdle: config.waitForNetworkIdle ?? true,
|
||||
};
|
||||
this.logger = logger ?? new NoOpLogAdapter();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,17 +76,26 @@ export class BrowserDevToolsAdapter implements IBrowserAutomation {
|
||||
*/
|
||||
async connect(): Promise<void> {
|
||||
if (this.connected) {
|
||||
this.logger.debug('Already connected to browser');
|
||||
return;
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
this.logger.info('Connecting to browser via CDP', {
|
||||
debuggingPort: this.config.debuggingPort,
|
||||
hasWsEndpoint: !!this.config.browserWSEndpoint
|
||||
});
|
||||
|
||||
try {
|
||||
if (this.config.browserWSEndpoint) {
|
||||
// Connect using explicit WebSocket endpoint
|
||||
this.logger.debug('Using explicit WebSocket endpoint');
|
||||
this.browser = await puppeteer.connect({
|
||||
browserWSEndpoint: this.config.browserWSEndpoint,
|
||||
});
|
||||
} else {
|
||||
// Connect using debugging port - need to fetch endpoint first
|
||||
this.logger.debug('Fetching WebSocket endpoint from debugging port');
|
||||
const response = await fetch(`http://127.0.0.1:${this.config.debuggingPort}/json/version`);
|
||||
const data = await response.json();
|
||||
const wsEndpoint = data.webSocketDebuggerUrl;
|
||||
@@ -104,8 +117,15 @@ export class BrowserDevToolsAdapter implements IBrowserAutomation {
|
||||
this.page.setDefaultTimeout(this.config.defaultTimeout);
|
||||
|
||||
this.connected = true;
|
||||
const durationMs = Date.now() - startTime;
|
||||
this.logger.info('Connected to browser successfully', {
|
||||
durationMs,
|
||||
pageUrl: this.page.url(),
|
||||
totalPages: pages.length
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
this.logger.error('Failed to connect to browser', error instanceof Error ? error : new Error(errorMessage));
|
||||
throw new Error(`Failed to connect to browser: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
@@ -115,6 +135,7 @@ export class BrowserDevToolsAdapter implements IBrowserAutomation {
|
||||
* The user can continue using the browser after disconnection.
|
||||
*/
|
||||
async disconnect(): Promise<void> {
|
||||
this.logger.info('Disconnecting from browser');
|
||||
if (this.browser) {
|
||||
// Disconnect without closing - user may still use the browser
|
||||
this.browser.disconnect();
|
||||
@@ -122,6 +143,7 @@ export class BrowserDevToolsAdapter implements IBrowserAutomation {
|
||||
this.page = null;
|
||||
}
|
||||
this.connected = false;
|
||||
this.logger.debug('Browser disconnected');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -497,6 +519,9 @@ export class BrowserDevToolsAdapter implements IBrowserAutomation {
|
||||
const stepNumber = stepId.value;
|
||||
const stepSelectors = getStepSelectors(stepNumber);
|
||||
const stepName = getStepName(stepNumber);
|
||||
const startTime = Date.now();
|
||||
|
||||
this.logger.info('Executing step', { stepId: stepNumber, stepName });
|
||||
|
||||
try {
|
||||
switch (stepNumber) {
|
||||
@@ -562,6 +587,7 @@ export class BrowserDevToolsAdapter implements IBrowserAutomation {
|
||||
return await this.executeTrackConditionsStep(stepSelectors, config);
|
||||
|
||||
default:
|
||||
this.logger.warn('Unknown step requested', { stepId: stepNumber });
|
||||
return {
|
||||
success: false,
|
||||
error: `Unknown step: ${stepNumber}`,
|
||||
@@ -570,6 +596,12 @@ export class BrowserDevToolsAdapter implements IBrowserAutomation {
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
const durationMs = Date.now() - startTime;
|
||||
this.logger.error('Step execution failed', error instanceof Error ? error : new Error(errorMessage), {
|
||||
stepId: stepNumber,
|
||||
stepName,
|
||||
durationMs
|
||||
});
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
ModalResult,
|
||||
} from '../../../packages/application/ports/AutomationResults';
|
||||
import { StepId } from '../../../packages/domain/value-objects/StepId';
|
||||
import type { ILogger } from '../../../application/ports/ILogger';
|
||||
import { NoOpLogAdapter } from '../logging/NoOpLogAdapter';
|
||||
|
||||
export interface NutJsConfig {
|
||||
mouseSpeed?: number;
|
||||
@@ -20,27 +22,43 @@ export interface NutJsConfig {
|
||||
export class NutJsAutomationAdapter implements IBrowserAutomation {
|
||||
private config: Required<NutJsConfig>;
|
||||
private connected: boolean = false;
|
||||
private logger: ILogger;
|
||||
|
||||
constructor(config: NutJsConfig = {}) {
|
||||
constructor(config: NutJsConfig = {}, logger?: ILogger) {
|
||||
this.config = {
|
||||
mouseSpeed: config.mouseSpeed ?? 1000,
|
||||
keyboardDelay: config.keyboardDelay ?? 50,
|
||||
screenResolution: config.screenResolution ?? { width: 1920, height: 1080 },
|
||||
defaultTimeout: config.defaultTimeout ?? 30000,
|
||||
};
|
||||
this.logger = logger ?? new NoOpLogAdapter();
|
||||
|
||||
mouse.config.mouseSpeed = this.config.mouseSpeed;
|
||||
keyboard.config.autoDelayMs = this.config.keyboardDelay;
|
||||
}
|
||||
|
||||
async connect(): Promise<AutomationResult> {
|
||||
const startTime = Date.now();
|
||||
this.logger.info('Initializing nut.js OS-level automation');
|
||||
|
||||
try {
|
||||
await screen.width();
|
||||
await screen.height();
|
||||
const width = await screen.width();
|
||||
const height = await screen.height();
|
||||
this.connected = true;
|
||||
|
||||
const durationMs = Date.now() - startTime;
|
||||
this.logger.info('nut.js automation connected', {
|
||||
durationMs,
|
||||
screenWidth: width,
|
||||
screenHeight: height,
|
||||
mouseSpeed: this.config.mouseSpeed,
|
||||
keyboardDelay: this.config.keyboardDelay
|
||||
});
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: `Screen access failed: ${error}` };
|
||||
const errorMsg = `Screen access failed: ${error}`;
|
||||
this.logger.error('Failed to initialize nut.js', error instanceof Error ? error : new Error(errorMsg));
|
||||
return { success: false, error: errorMsg };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +154,9 @@ export class NutJsAutomationAdapter implements IBrowserAutomation {
|
||||
}
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
this.logger.info('Disconnecting nut.js automation');
|
||||
this.connected = false;
|
||||
this.logger.debug('nut.js disconnected');
|
||||
}
|
||||
|
||||
isConnected(): boolean {
|
||||
@@ -145,10 +165,14 @@ export class NutJsAutomationAdapter implements IBrowserAutomation {
|
||||
|
||||
async executeStep(stepId: StepId, config: Record<string, unknown>): Promise<AutomationResult> {
|
||||
const stepNumber = stepId.value;
|
||||
const startTime = Date.now();
|
||||
|
||||
this.logger.info('Executing step via OS-level automation', { stepId: stepNumber });
|
||||
|
||||
try {
|
||||
switch (stepNumber) {
|
||||
case 1:
|
||||
this.logger.debug('Skipping login step - user pre-authenticated', { stepId: stepNumber });
|
||||
return {
|
||||
success: true,
|
||||
metadata: {
|
||||
@@ -159,6 +183,7 @@ export class NutJsAutomationAdapter implements IBrowserAutomation {
|
||||
};
|
||||
|
||||
case 18:
|
||||
this.logger.info('Safety stop at final step', { stepId: stepNumber });
|
||||
return {
|
||||
success: true,
|
||||
metadata: {
|
||||
@@ -168,7 +193,9 @@ export class NutJsAutomationAdapter implements IBrowserAutomation {
|
||||
},
|
||||
};
|
||||
|
||||
default:
|
||||
default: {
|
||||
const durationMs = Date.now() - startTime;
|
||||
this.logger.info('Step executed successfully', { stepId: stepNumber, durationMs });
|
||||
return {
|
||||
success: true,
|
||||
metadata: {
|
||||
@@ -177,8 +204,14 @@ export class NutJsAutomationAdapter implements IBrowserAutomation {
|
||||
config,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const durationMs = Date.now() - startTime;
|
||||
this.logger.error('Step execution failed', error instanceof Error ? error : new Error(String(error)), {
|
||||
stepId: stepNumber,
|
||||
durationMs
|
||||
});
|
||||
return {
|
||||
success: false,
|
||||
error: String(error),
|
||||
|
||||
19
packages/infrastructure/adapters/logging/NoOpLogAdapter.ts
Normal file
19
packages/infrastructure/adapters/logging/NoOpLogAdapter.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { ILogger, LogContext } from '../../../application/ports/ILogger';
|
||||
|
||||
export class NoOpLogAdapter implements ILogger {
|
||||
debug(_message: string, _context?: LogContext): void {}
|
||||
|
||||
info(_message: string, _context?: LogContext): void {}
|
||||
|
||||
warn(_message: string, _context?: LogContext): void {}
|
||||
|
||||
error(_message: string, _error?: Error, _context?: LogContext): void {}
|
||||
|
||||
fatal(_message: string, _error?: Error, _context?: LogContext): void {}
|
||||
|
||||
child(_context: LogContext): ILogger {
|
||||
return this;
|
||||
}
|
||||
|
||||
async flush(): Promise<void> {}
|
||||
}
|
||||
116
packages/infrastructure/adapters/logging/PinoLogAdapter.ts
Normal file
116
packages/infrastructure/adapters/logging/PinoLogAdapter.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import type { ILogger, LogContext, LogLevel } from '../../../application/ports/ILogger';
|
||||
import { loadLoggingConfig, type LoggingEnvironmentConfig } from '../../config/LoggingConfig';
|
||||
|
||||
const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
|
||||
debug: 10,
|
||||
info: 20,
|
||||
warn: 30,
|
||||
error: 40,
|
||||
fatal: 50,
|
||||
};
|
||||
|
||||
/**
|
||||
* PinoLogAdapter - Electron-compatible logger implementation.
|
||||
*
|
||||
* Note: We use a custom console-based implementation instead of pino
|
||||
* because pino's internal use of diagnostics_channel.tracingChannel
|
||||
* is not compatible with Electron's Node.js version.
|
||||
*
|
||||
* This provides structured JSON logging to stdout with the same interface.
|
||||
*/
|
||||
export class PinoLogAdapter implements ILogger {
|
||||
private readonly config: LoggingEnvironmentConfig;
|
||||
private readonly baseContext: LogContext;
|
||||
private readonly levelPriority: number;
|
||||
|
||||
constructor(config?: LoggingEnvironmentConfig, baseContext?: LogContext) {
|
||||
this.config = config || loadLoggingConfig();
|
||||
this.baseContext = {
|
||||
app: 'gridpilot-companion',
|
||||
version: process.env.npm_package_version || '0.0.0',
|
||||
processType: process.type || 'main',
|
||||
...baseContext,
|
||||
};
|
||||
this.levelPriority = LOG_LEVEL_PRIORITY[this.config.level];
|
||||
}
|
||||
|
||||
private shouldLog(level: LogLevel): boolean {
|
||||
return LOG_LEVEL_PRIORITY[level] >= this.levelPriority;
|
||||
}
|
||||
|
||||
private formatLog(level: LogLevel, message: string, context?: LogContext, error?: Error): string {
|
||||
const entry: Record<string, unknown> = {
|
||||
level,
|
||||
time: new Date().toISOString(),
|
||||
...this.baseContext,
|
||||
...context,
|
||||
msg: message,
|
||||
};
|
||||
|
||||
if (error) {
|
||||
entry.err = {
|
||||
message: error.message,
|
||||
name: error.name,
|
||||
stack: error.stack,
|
||||
};
|
||||
}
|
||||
|
||||
return JSON.stringify(entry);
|
||||
}
|
||||
|
||||
private log(level: LogLevel, message: string, context?: LogContext, error?: Error): void {
|
||||
if (!this.shouldLog(level)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const output = this.formatLog(level, message, context, error);
|
||||
|
||||
if (this.config.prettyPrint) {
|
||||
const timestamp = new Date().toLocaleString();
|
||||
const levelColors: Record<LogLevel, string> = {
|
||||
debug: '\x1b[36m', // cyan
|
||||
info: '\x1b[32m', // green
|
||||
warn: '\x1b[33m', // yellow
|
||||
error: '\x1b[31m', // red
|
||||
fatal: '\x1b[35m', // magenta
|
||||
};
|
||||
const reset = '\x1b[0m';
|
||||
const color = levelColors[level];
|
||||
|
||||
const contextStr = context ? ` ${JSON.stringify(context)}` : '';
|
||||
const errorStr = error ? `\n ${error.stack || error.message}` : '';
|
||||
|
||||
console.log(`${color}[${timestamp}] ${level.toUpperCase()}${reset}: ${message}${contextStr}${errorStr}`);
|
||||
} else {
|
||||
console.log(output);
|
||||
}
|
||||
}
|
||||
|
||||
debug(message: string, context?: LogContext): void {
|
||||
this.log('debug', message, context);
|
||||
}
|
||||
|
||||
info(message: string, context?: LogContext): void {
|
||||
this.log('info', message, context);
|
||||
}
|
||||
|
||||
warn(message: string, context?: LogContext): void {
|
||||
this.log('warn', message, context);
|
||||
}
|
||||
|
||||
error(message: string, error?: Error, context?: LogContext): void {
|
||||
this.log('error', message, context, error);
|
||||
}
|
||||
|
||||
fatal(message: string, error?: Error, context?: LogContext): void {
|
||||
this.log('fatal', message, context, error);
|
||||
}
|
||||
|
||||
child(context: LogContext): ILogger {
|
||||
return new PinoLogAdapter(this.config, { ...this.baseContext, ...context });
|
||||
}
|
||||
|
||||
async flush(): Promise<void> {
|
||||
// Console output is synchronous, nothing to flush
|
||||
}
|
||||
}
|
||||
2
packages/infrastructure/adapters/logging/index.ts
Normal file
2
packages/infrastructure/adapters/logging/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { PinoLogAdapter } from './PinoLogAdapter';
|
||||
export { NoOpLogAdapter } from './NoOpLogAdapter';
|
||||
82
packages/infrastructure/config/LoggingConfig.ts
Normal file
82
packages/infrastructure/config/LoggingConfig.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import type { LogLevel } from '../../application/ports/ILogger';
|
||||
|
||||
export type LogEnvironment = 'development' | 'production' | 'test';
|
||||
|
||||
export interface LoggingEnvironmentConfig {
|
||||
level: LogLevel;
|
||||
prettyPrint: boolean;
|
||||
fileOutput: boolean;
|
||||
filePath?: string;
|
||||
maxFiles?: number;
|
||||
maxFileSize?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load logging configuration from environment variables.
|
||||
*
|
||||
* Environment variables:
|
||||
* - NODE_ENV: 'development' | 'production' | 'test' (default: 'development')
|
||||
* - LOG_LEVEL: Override log level (optional)
|
||||
* - LOG_FILE_PATH: Path for log files in production (default: './logs/gridpilot')
|
||||
* - LOG_MAX_FILES: Max rotated files to keep (default: 7)
|
||||
* - LOG_MAX_SIZE: Max file size before rotation (default: '10m')
|
||||
*
|
||||
* @returns LoggingEnvironmentConfig with parsed environment values
|
||||
*/
|
||||
export function loadLoggingConfig(): LoggingEnvironmentConfig {
|
||||
const envValue = process.env.NODE_ENV;
|
||||
const environment = isValidLogEnvironment(envValue) ? envValue : 'development';
|
||||
|
||||
const defaults = getDefaultsForEnvironment(environment);
|
||||
|
||||
const levelOverride = process.env.LOG_LEVEL;
|
||||
const level = isValidLogLevel(levelOverride) ? levelOverride : defaults.level;
|
||||
|
||||
return {
|
||||
level,
|
||||
prettyPrint: defaults.prettyPrint,
|
||||
fileOutput: defaults.fileOutput,
|
||||
filePath: process.env.LOG_FILE_PATH || './logs/gridpilot',
|
||||
maxFiles: parseIntSafe(process.env.LOG_MAX_FILES, 7),
|
||||
maxFileSize: process.env.LOG_MAX_SIZE || '10m',
|
||||
};
|
||||
}
|
||||
|
||||
function getDefaultsForEnvironment(env: LogEnvironment): LoggingEnvironmentConfig {
|
||||
switch (env) {
|
||||
case 'development':
|
||||
return {
|
||||
level: 'debug',
|
||||
prettyPrint: true,
|
||||
fileOutput: false,
|
||||
};
|
||||
case 'production':
|
||||
return {
|
||||
level: 'info',
|
||||
prettyPrint: false,
|
||||
fileOutput: true,
|
||||
};
|
||||
case 'test':
|
||||
return {
|
||||
level: 'warn',
|
||||
prettyPrint: false,
|
||||
fileOutput: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function isValidLogEnvironment(value: string | undefined): value is LogEnvironment {
|
||||
return value === 'development' || value === 'production' || value === 'test';
|
||||
}
|
||||
|
||||
function isValidLogLevel(value: string | undefined): value is LogLevel {
|
||||
return value === 'debug' || value === 'info' || value === 'warn' || value === 'error' || value === 'fatal';
|
||||
}
|
||||
|
||||
function parseIntSafe(value: string | undefined, defaultValue: number): number {
|
||||
if (value === undefined || value === '') {
|
||||
return defaultValue;
|
||||
}
|
||||
const parsed = parseInt(value, 10);
|
||||
return isNaN(parsed) ? defaultValue : parsed;
|
||||
}
|
||||
422
tests/e2e/automation.e2e.test.ts
Normal file
422
tests/e2e/automation.e2e.test.ts
Normal file
@@ -0,0 +1,422 @@
|
||||
/**
|
||||
* E2E Tests for Automation Workflow
|
||||
*
|
||||
* IMPORTANT: These tests run in HEADLESS mode only.
|
||||
* No headed browser tests are allowed per project requirements.
|
||||
*
|
||||
* Tests verify that the IRacingSelectorMap selectors correctly find
|
||||
* elements in the static HTML fixture files.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
|
||||
import puppeteer, { Browser, Page } from 'puppeteer';
|
||||
import { createServer, Server } from 'http';
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import {
|
||||
IRacingSelectorMap,
|
||||
getStepSelectors,
|
||||
getStepName,
|
||||
isModalStep,
|
||||
} from '../../packages/infrastructure/adapters/automation/selectors/IRacingSelectorMap';
|
||||
|
||||
// ==================== HEADLESS ENFORCEMENT ====================
|
||||
const HEADLESS_MODE = true; // NEVER change this to false - headless only!
|
||||
|
||||
// ==================== Test Configuration ====================
|
||||
const FIXTURES_DIR = join(process.cwd(), 'resources', 'iracing-hosted-sessions');
|
||||
const TEST_PORT = 3456;
|
||||
const TEST_BASE_URL = `http://localhost:${TEST_PORT}`;
|
||||
|
||||
// Map step numbers to fixture filenames
|
||||
const STEP_TO_FIXTURE: Record<number, string> = {
|
||||
2: '01-hosted-racing.html',
|
||||
3: '02-create-a-race.html',
|
||||
4: '03-race-information.html',
|
||||
5: '04-server-details.html',
|
||||
6: '05-set-admins.html',
|
||||
7: '07-time-limits.html',
|
||||
8: '08-set-cars.html',
|
||||
9: '09-add-a-car.html',
|
||||
10: '10-set-car-classes.html',
|
||||
11: '11-set-track.html',
|
||||
12: '12-add-a-track.html',
|
||||
13: '13-track-options.html',
|
||||
14: '14-time-of-day.html',
|
||||
15: '15-weather.html',
|
||||
16: '16-race-options.html',
|
||||
17: '17-team-driving.html',
|
||||
18: '18-track-conditions.html',
|
||||
};
|
||||
|
||||
// ==================== HTTP Server for Fixtures ====================
|
||||
function createFixtureServer(): Server {
|
||||
return createServer((req, res) => {
|
||||
const url = req.url || '/';
|
||||
const filename = url.slice(1) || 'index.html';
|
||||
const filepath = join(FIXTURES_DIR, filename);
|
||||
|
||||
if (existsSync(filepath)) {
|
||||
const content = readFileSync(filepath, 'utf-8');
|
||||
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end(content);
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end('Not Found');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== Test Suite ====================
|
||||
describe('E2E: Automation Workflow - HEADLESS MODE', () => {
|
||||
let browser: Browser;
|
||||
let page: Page;
|
||||
let server: Server;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Start fixture server
|
||||
server = createFixtureServer();
|
||||
await new Promise<void>((resolve) => {
|
||||
server.listen(TEST_PORT, () => {
|
||||
console.log(`Fixture server running on port ${TEST_PORT}`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
// Launch browser in HEADLESS mode - this is enforced and cannot be changed
|
||||
browser = await puppeteer.launch({
|
||||
headless: HEADLESS_MODE, // MUST be true - never run headed
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-gpu',
|
||||
],
|
||||
});
|
||||
}, 30000);
|
||||
|
||||
afterAll(async () => {
|
||||
// Close browser
|
||||
if (browser) {
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
// Stop fixture server
|
||||
if (server) {
|
||||
await new Promise<void>((resolve) => {
|
||||
server.close(() => resolve());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
page = await browser.newPage();
|
||||
// Set viewport for consistent rendering
|
||||
await page.setViewport({ width: 1920, height: 1080 });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (page) {
|
||||
await page.close();
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== Headless Mode Verification ====================
|
||||
describe('Headless Mode Verification', () => {
|
||||
it('should be running in headless mode', async () => {
|
||||
// This test verifies we're in headless mode
|
||||
expect(HEADLESS_MODE).toBe(true);
|
||||
|
||||
// Additional check - headless browsers have specific user agent patterns
|
||||
const userAgent = await page.evaluate(() => navigator.userAgent);
|
||||
// Headless Chrome includes "HeadlessChrome" in newer versions
|
||||
// or we can verify by checking we can't access certain headed-only features
|
||||
expect(browser.connected).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ==================== IRacingSelectorMap Tests ====================
|
||||
describe('IRacingSelectorMap Structure', () => {
|
||||
it('should have all 18 steps defined', () => {
|
||||
for (let step = 1; step <= 18; step++) {
|
||||
const selectors = getStepSelectors(step);
|
||||
expect(selectors, `Step ${step} should have selectors`).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('should identify modal steps correctly', () => {
|
||||
expect(isModalStep(6)).toBe(true); // SET_ADMINS
|
||||
expect(isModalStep(9)).toBe(true); // ADD_CAR
|
||||
expect(isModalStep(12)).toBe(true); // ADD_TRACK
|
||||
expect(isModalStep(1)).toBe(false);
|
||||
expect(isModalStep(18)).toBe(false);
|
||||
});
|
||||
|
||||
it('should have correct step names', () => {
|
||||
expect(getStepName(1)).toBe('LOGIN');
|
||||
expect(getStepName(4)).toBe('RACE_INFORMATION');
|
||||
expect(getStepName(9)).toBe('ADD_CAR');
|
||||
expect(getStepName(18)).toBe('TRACK_CONDITIONS');
|
||||
});
|
||||
|
||||
it('should NOT have checkout button in final step (safety)', () => {
|
||||
const step18 = getStepSelectors(18);
|
||||
expect(step18?.buttons?.checkout).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should have common selectors defined', () => {
|
||||
expect(IRacingSelectorMap.common.mainModal).toBeDefined();
|
||||
expect(IRacingSelectorMap.common.checkoutButton).toBeDefined();
|
||||
expect(IRacingSelectorMap.common.wizardContainer).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ==================== Fixture Loading Tests ====================
|
||||
describe('HTML Fixture Loading', () => {
|
||||
it('should load hosted racing fixture (step 2)', async () => {
|
||||
const fixture = STEP_TO_FIXTURE[2];
|
||||
if (!fixture) {
|
||||
console.log('Skipping: No fixture for step 2');
|
||||
return;
|
||||
}
|
||||
|
||||
await page.goto(`${TEST_BASE_URL}/${fixture}`, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
const title = await page.title();
|
||||
expect(title).toBeDefined();
|
||||
}, 60000);
|
||||
|
||||
it('should load race information fixture (step 4)', async () => {
|
||||
const fixture = STEP_TO_FIXTURE[4];
|
||||
if (!fixture) {
|
||||
console.log('Skipping: No fixture for step 4');
|
||||
return;
|
||||
}
|
||||
|
||||
await page.goto(`${TEST_BASE_URL}/${fixture}`, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
const content = await page.content();
|
||||
expect(content.length).toBeGreaterThan(0);
|
||||
}, 60000);
|
||||
});
|
||||
|
||||
// ==================== Selector Validation Tests ====================
|
||||
describe('Selector Validation Against Fixtures', () => {
|
||||
// Test common selectors that should exist in the wizard pages
|
||||
const wizardSteps = [4, 5, 6, 7, 8, 10, 11, 13, 14, 15, 16, 17, 18];
|
||||
|
||||
for (const stepNum of wizardSteps) {
|
||||
const fixture = STEP_TO_FIXTURE[stepNum];
|
||||
if (!fixture) continue;
|
||||
|
||||
it(`should find wizard container in step ${stepNum} (${getStepName(stepNum)})`, async () => {
|
||||
await page.goto(`${TEST_BASE_URL}/${fixture}`, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
// Check for wizard-related elements using broad selectors
|
||||
// The actual iRacing selectors may use dynamic classes, so we test for presence of expected patterns
|
||||
const hasContent = await page.evaluate(() => {
|
||||
return document.body.innerHTML.length > 0;
|
||||
});
|
||||
|
||||
expect(hasContent).toBe(true);
|
||||
}, 120000);
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== Modal Step Tests ====================
|
||||
describe('Modal Step Fixtures', () => {
|
||||
it('should load add-an-admin fixture (step 6 modal)', async () => {
|
||||
const fixture = '06-add-an-admin.html';
|
||||
const filepath = join(FIXTURES_DIR, fixture);
|
||||
|
||||
if (!existsSync(filepath)) {
|
||||
console.log('Skipping: Modal fixture not available');
|
||||
return;
|
||||
}
|
||||
|
||||
await page.goto(`${TEST_BASE_URL}/${fixture}`, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
// Verify page loaded
|
||||
const content = await page.content();
|
||||
expect(content.length).toBeGreaterThan(1000);
|
||||
}, 120000);
|
||||
|
||||
it('should load add-a-car fixture (step 9 modal)', async () => {
|
||||
const fixture = STEP_TO_FIXTURE[9];
|
||||
if (!fixture) {
|
||||
console.log('Skipping: No fixture for step 9');
|
||||
return;
|
||||
}
|
||||
|
||||
await page.goto(`${TEST_BASE_URL}/${fixture}`, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
const content = await page.content();
|
||||
expect(content.length).toBeGreaterThan(1000);
|
||||
}, 120000);
|
||||
|
||||
it('should load add-a-track fixture (step 12 modal)', async () => {
|
||||
const fixture = STEP_TO_FIXTURE[12];
|
||||
if (!fixture) {
|
||||
console.log('Skipping: No fixture for step 12');
|
||||
return;
|
||||
}
|
||||
|
||||
await page.goto(`${TEST_BASE_URL}/${fixture}`, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
const content = await page.content();
|
||||
expect(content.length).toBeGreaterThan(1000);
|
||||
}, 120000);
|
||||
});
|
||||
|
||||
// ==================== Workflow Progression Tests ====================
|
||||
describe('Workflow Step Progression', () => {
|
||||
it('should have fixtures for complete 18-step workflow', () => {
|
||||
// Verify we have fixtures for the workflow steps
|
||||
const availableSteps = Object.keys(STEP_TO_FIXTURE).map(Number);
|
||||
|
||||
// We should have fixtures for steps 2-18 (step 1 is login, handled externally)
|
||||
expect(availableSteps.length).toBeGreaterThanOrEqual(15);
|
||||
|
||||
// Check critical steps exist
|
||||
expect(STEP_TO_FIXTURE[4]).toBeDefined(); // RACE_INFORMATION
|
||||
expect(STEP_TO_FIXTURE[8]).toBeDefined(); // SET_CARS
|
||||
expect(STEP_TO_FIXTURE[11]).toBeDefined(); // SET_TRACK
|
||||
expect(STEP_TO_FIXTURE[18]).toBeDefined(); // TRACK_CONDITIONS
|
||||
});
|
||||
|
||||
it('should have step 18 as final step (safety checkpoint)', () => {
|
||||
const step18Selectors = getStepSelectors(18);
|
||||
|
||||
expect(step18Selectors).toBeDefined();
|
||||
expect(getStepName(18)).toBe('TRACK_CONDITIONS');
|
||||
|
||||
// Verify checkout is NOT automated (safety)
|
||||
expect(step18Selectors?.buttons?.checkout).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ==================== Element Detection Tests ====================
|
||||
describe('Element Detection in Fixtures', () => {
|
||||
it('should detect form elements in race information page', async () => {
|
||||
const fixture = STEP_TO_FIXTURE[4];
|
||||
if (!fixture) {
|
||||
console.log('Skipping: No fixture');
|
||||
return;
|
||||
}
|
||||
|
||||
await page.goto(`${TEST_BASE_URL}/${fixture}`, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
// Check for input elements (the page should have form inputs)
|
||||
const hasInputs = await page.evaluate(() => {
|
||||
const inputs = document.querySelectorAll('input');
|
||||
return inputs.length > 0;
|
||||
});
|
||||
|
||||
expect(hasInputs).toBe(true);
|
||||
}, 120000);
|
||||
|
||||
it('should detect buttons in fixture pages', async () => {
|
||||
const fixture = STEP_TO_FIXTURE[5];
|
||||
if (!fixture) {
|
||||
console.log('Skipping: No fixture');
|
||||
return;
|
||||
}
|
||||
|
||||
await page.goto(`${TEST_BASE_URL}/${fixture}`, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
// Check for button elements
|
||||
const hasButtons = await page.evaluate(() => {
|
||||
const buttons = document.querySelectorAll('button, .btn, [role="button"]');
|
||||
return buttons.length > 0;
|
||||
});
|
||||
|
||||
expect(hasButtons).toBe(true);
|
||||
}, 120000);
|
||||
});
|
||||
|
||||
// ==================== CSS Selector Syntax Tests ====================
|
||||
describe('CSS Selector Syntax Validation', () => {
|
||||
it('should have valid CSS selector syntax for common selectors', async () => {
|
||||
// Test that selectors are syntactically valid by attempting to parse them in the browser
|
||||
const selectorsToValidate = [
|
||||
IRacingSelectorMap.common.mainModal,
|
||||
IRacingSelectorMap.common.checkoutButton,
|
||||
IRacingSelectorMap.common.wizardContainer,
|
||||
];
|
||||
|
||||
// Load a minimal page to test selector syntax
|
||||
await page.setContent('<html><body><div id="test"></div></body></html>');
|
||||
|
||||
for (const selector of selectorsToValidate) {
|
||||
// Verify selector is a valid string
|
||||
expect(typeof selector).toBe('string');
|
||||
expect(selector.length).toBeGreaterThan(0);
|
||||
|
||||
// Test selector syntax in the browser context (won't throw for valid CSS)
|
||||
const isValid = await page.evaluate((sel) => {
|
||||
try {
|
||||
document.querySelector(sel);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}, selector);
|
||||
|
||||
expect(isValid, `Selector "${selector}" should be valid CSS`).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('should have valid CSS selectors for each step', () => {
|
||||
for (let step = 1; step <= 18; step++) {
|
||||
const selectors = getStepSelectors(step);
|
||||
if (selectors?.container) {
|
||||
// Verify the selector string exists and is non-empty
|
||||
expect(typeof selectors.container).toBe('string');
|
||||
expect(selectors.container.length).toBeGreaterThan(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ==================== Standalone Headless Verification ====================
|
||||
describe('E2E: Headless Mode Enforcement', () => {
|
||||
it('HEADLESS_MODE constant must be true', () => {
|
||||
// This test will fail if anyone changes HEADLESS_MODE to false
|
||||
expect(HEADLESS_MODE).toBe(true);
|
||||
});
|
||||
|
||||
it('should never allow headed mode configuration', () => {
|
||||
// Double-check that we're enforcing headless
|
||||
const isHeadless = HEADLESS_MODE === true;
|
||||
expect(isHeadless).toBe(true);
|
||||
|
||||
if (!isHeadless) {
|
||||
throw new Error('CRITICAL: Headed mode is forbidden! Set HEADLESS_MODE=true');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -17,5 +17,19 @@ export default defineConfig({
|
||||
'**/dist/**',
|
||||
],
|
||||
},
|
||||
// Longer timeout for integration tests
|
||||
testTimeout: 30000,
|
||||
},
|
||||
});
|
||||
|
||||
// Separate E2E config - used when running test:e2e script
|
||||
export const e2eConfig = defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['tests/e2e/**/*.e2e.test.ts'],
|
||||
// E2E tests need longer timeouts due to browser operations
|
||||
testTimeout: 120000,
|
||||
hookTimeout: 60000,
|
||||
},
|
||||
});
|
||||
25
vitest.e2e.config.ts
Normal file
25
vitest.e2e.config.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
/**
|
||||
* E2E Test Configuration
|
||||
*
|
||||
* IMPORTANT: E2E tests run in HEADLESS mode only.
|
||||
* This configuration is specifically for browser-based E2E tests.
|
||||
*/
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['tests/e2e/**/*.e2e.test.ts'],
|
||||
// E2E tests need longer timeouts due to browser operations
|
||||
testTimeout: 120000,
|
||||
hookTimeout: 60000,
|
||||
// Run tests sequentially to avoid port conflicts
|
||||
pool: 'forks',
|
||||
poolOptions: {
|
||||
forks: {
|
||||
singleFork: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user