This commit is contained in:
2025-12-12 21:39:48 +01:00
parent ddbd99b747
commit cae81b1088
49 changed files with 777 additions and 269 deletions

View File

@@ -1,6 +1,8 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { describe, it, expect, beforeEach, afterEach, beforeAll, afterAll } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import type { LoggerPort } from '@gridpilot/automation/application/ports/LoggerPort';
import type { LogContext } from '@gridpilot/automation/application/ports/LoggerContext';
/**
* Integration tests for Browser Mode in PlaywrightAutomationAdapter - GREEN PHASE
@@ -27,7 +29,12 @@ describe('Browser Mode Integration - GREEN Phase', () => {
beforeEach(() => {
process.env = { ...originalEnv };
delete process.env.NODE_ENV;
Object.defineProperty(process.env, 'NODE_ENV', {
value: undefined,
writable: true,
enumerable: true,
configurable: true
});
});
beforeAll(() => {
@@ -53,7 +60,7 @@ describe('Browser Mode Integration - GREEN Phase', () => {
afterAll(() => {
if (unhandledRejectionHandler) {
process.removeListener('unhandledRejection', unhandledRejectionHandler);
(process as any).removeListener('unhandledRejection', unhandledRejectionHandler);
unhandledRejectionHandler = null;
}
});
@@ -72,7 +79,12 @@ describe('Browser Mode Integration - GREEN Phase', () => {
describe('Headless Mode Launch (NODE_ENV=production/test)', () => {
it('should launch browser with headless: true when NODE_ENV=production', async () => {
process.env.NODE_ENV = 'production';
Object.defineProperty(process.env, 'NODE_ENV', {
value: 'production',
writable: true,
enumerable: true,
configurable: true
});
const { PlaywrightAutomationAdapter } = await import(
'packages/automation/infrastructure/adapters/automation'
@@ -80,8 +92,8 @@ describe('Browser Mode Integration - GREEN Phase', () => {
adapter = new PlaywrightAutomationAdapter({
mode: 'mock',
});
}, undefined, undefined);
const result = await adapter.connect();
expect(result.success).toBe(true);
@@ -91,7 +103,12 @@ describe('Browser Mode Integration - GREEN Phase', () => {
});
it('should launch browser with headless: true when NODE_ENV=test', async () => {
process.env.NODE_ENV = 'test';
Object.defineProperty(process.env, 'NODE_ENV', {
value: 'test',
writable: true,
enumerable: true,
configurable: true
});
const { PlaywrightAutomationAdapter } = await import(
'packages/automation/infrastructure/adapters/automation'
@@ -99,8 +116,8 @@ describe('Browser Mode Integration - GREEN Phase', () => {
adapter = new PlaywrightAutomationAdapter({
mode: 'mock',
});
}, undefined, undefined);
const result = await adapter.connect();
expect(result.success).toBe(true);
@@ -110,7 +127,12 @@ describe('Browser Mode Integration - GREEN Phase', () => {
});
it('should default to headless when NODE_ENV is not set', async () => {
delete process.env.NODE_ENV;
Object.defineProperty(process.env, 'NODE_ENV', {
value: undefined,
writable: true,
enumerable: true,
configurable: true
});
const { PlaywrightAutomationAdapter } = await import(
'packages/automation/infrastructure/adapters/automation'
@@ -118,8 +140,8 @@ describe('Browser Mode Integration - GREEN Phase', () => {
adapter = new PlaywrightAutomationAdapter({
mode: 'mock',
});
}, undefined, undefined);
await adapter.connect();
expect(adapter.getBrowserMode()).toBe('headless');
@@ -134,7 +156,12 @@ describe('Browser Mode Integration - GREEN Phase', () => {
});
it('should report NODE_ENV as source in production mode', async () => {
process.env.NODE_ENV = 'production';
Object.defineProperty(process.env, 'NODE_ENV', {
value: 'production',
writable: true,
enumerable: true,
configurable: true
});
const { PlaywrightAutomationAdapter } = await import(
'packages/automation/infrastructure/adapters/automation'
@@ -142,7 +169,7 @@ describe('Browser Mode Integration - GREEN Phase', () => {
adapter = new PlaywrightAutomationAdapter({
mode: 'mock',
});
}, undefined, undefined);
await adapter.connect();
@@ -150,7 +177,12 @@ describe('Browser Mode Integration - GREEN Phase', () => {
});
it('should report NODE_ENV as source in test mode', async () => {
process.env.NODE_ENV = 'test';
Object.defineProperty(process.env, 'NODE_ENV', {
value: 'test',
writable: true,
enumerable: true,
configurable: true
});
const { PlaywrightAutomationAdapter } = await import(
'packages/automation/infrastructure/adapters/automation'
@@ -158,7 +190,7 @@ describe('Browser Mode Integration - GREEN Phase', () => {
adapter = new PlaywrightAutomationAdapter({
mode: 'mock',
});
}, undefined);
await adapter.connect();
@@ -173,22 +205,26 @@ describe('Browser Mode Integration - GREEN Phase', () => {
});
it('should log browser mode configuration with NODE_ENV source in production', async () => {
process.env.NODE_ENV = 'production';
(process.env as any).NODE_ENV = 'production';
const logSpy: Array<{ level: string; message: string; context?: Record<string, unknown> }> = [];
type LoggerLike = {
debug: (msg: string, ctx?: Record<string, unknown>) => void;
info: (msg: string, ctx?: Record<string, unknown>) => void;
warn: (msg: string, ctx?: Record<string, unknown>) => void;
error: (msg: string, ctx?: Record<string, unknown>) => void;
child: () => LoggerLike;
debug: (message: string, context?: Record<string, unknown>) => void;
info: (message: string, context?: Record<string, unknown>) => void;
warn: (message: string, context?: Record<string, unknown>) => void;
error: (message: string, error?: Error, context?: Record<string, unknown>) => void;
fatal: (message: string, error?: Error, context?: Record<string, unknown>) => void;
child: (context: Record<string, unknown>) => LoggerLike;
flush: () => Promise<void>;
};
const mockLogger: LoggerLike = {
debug: (msg: string, ctx?: Record<string, unknown>) => logSpy.push({ level: 'debug', message: msg, context: ctx }),
info: (msg: string, ctx?: Record<string, unknown>) => logSpy.push({ level: 'info', message: msg, context: ctx }),
warn: (msg: string, ctx?: Record<string, unknown>) => logSpy.push({ level: 'warn', message: msg, context: ctx }),
error: (msg: string, ctx?: Record<string, unknown>) => logSpy.push({ level: 'error', message: msg, context: ctx }),
child: () => mockLogger,
debug: (message: string, context?: Record<string, unknown>) => logSpy.push({ level: 'debug', message, ...(context ? { context } : {}) }),
info: (message: string, context?: Record<string, unknown>) => logSpy.push({ level: 'info', message, ...(context ? { context } : {}) }),
warn: (message: string, context?: Record<string, unknown>) => logSpy.push({ level: 'warn', message, ...(context ? { context } : {}) }),
error: (message: string, error?: Error, context?: Record<string, unknown>) => logSpy.push({ level: 'error', message, ...(context ? { context } : {}) }),
fatal: (message: string, error?: Error, context?: Record<string, unknown>) => logSpy.push({ level: 'fatal', message, ...(context ? { context } : {}) }),
child: (context: Record<string, unknown>) => mockLogger,
flush: () => Promise.resolve(),
};
const { PlaywrightAutomationAdapter } = await import(
@@ -215,7 +251,12 @@ describe('Browser Mode Integration - GREEN Phase', () => {
describe('Persistent Context', () => {
it('should apply browser mode to persistent browser context', async () => {
process.env.NODE_ENV = 'production';
Object.defineProperty(process.env, 'NODE_ENV', {
value: 'production',
writable: true,
enumerable: true,
configurable: true
});
const { PlaywrightAutomationAdapter } = await import(
'packages/automation/infrastructure/adapters/automation'
@@ -226,8 +267,8 @@ describe('Browser Mode Integration - GREEN Phase', () => {
adapter = new PlaywrightAutomationAdapter({
mode: 'real',
userDataDir,
});
}, undefined, undefined);
await adapter.connect();
expect(adapter.getBrowserMode()).toBe('headless');
@@ -242,7 +283,12 @@ describe('Browser Mode Integration - GREEN Phase', () => {
describe('Runtime loader re-read instrumentation (test-only)', () => {
it('reads mode from injected loader and passes headless flag to launcher accordingly', async () => {
process.env.NODE_ENV = 'development';
Object.defineProperty(process.env, 'NODE_ENV', {
value: 'development',
writable: true,
enumerable: true,
configurable: true
});
const { PlaywrightAutomationAdapter } = await import(
'packages/automation/infrastructure/adapters/automation'
);
@@ -293,7 +339,7 @@ describe('Browser Mode Integration - GREEN Phase', () => {
const r1 = await adapter.connect();
expect(r1.success).toBe(true);
expect(launches.length).toBeGreaterThan(0);
expect(launches[0].opts.headless).toBe(false);
expect((launches[0] as any).opts.headless).toBe(false);
// Disconnect and change loader to headless
await adapter.disconnect();
@@ -305,10 +351,10 @@ describe('Browser Mode Integration - GREEN Phase', () => {
// The second recorded launch may be at index 1 if both calls used the same launcher path
const secondLaunch = launches.slice(1).find(l => l.type === 'launch' || l.type === 'launchPersistent');
expect(secondLaunch).toBeDefined();
expect(secondLaunch!.opts.headless).toBe(true);
expect(secondLaunch!.opts?.headless).toBe(true);
// Cleanup test hook
AdapterWithTestLauncher.testLauncher = undefined;
(AdapterWithTestLauncher as any).testLauncher = undefined;
await adapter.disconnect();
});
});

View File

@@ -8,6 +8,8 @@ import { FixtureServer, getAllStepFixtureMappings, PlaywrightAutomationAdapter }
declare const getComputedStyle: any;
declare const document: any;
const logger = console as any;
describe('FixtureServer integration', () => {
let server: FixtureServer;
let adapter: PlaywrightAutomationAdapter;
@@ -22,7 +24,7 @@ describe('FixtureServer integration', () => {
headless: true,
timeout: 5000,
baseUrl,
});
}, logger);
const connectResult = await adapter.connect();
expect(connectResult.success).toBe(true);
@@ -86,7 +88,7 @@ describe('FixtureServer integration', () => {
const disconnectedAdapter = new PlaywrightAutomationAdapter({
headless: true,
timeout: 1000,
});
}, logger);
const navResult = await disconnectedAdapter.navigateToPage('http://localhost:9999');
expect(navResult.success).toBe(false);
@@ -105,7 +107,7 @@ describe('FixtureServer integration', () => {
it('reports connected state correctly', async () => {
expect(adapter.isConnected()).toBe(true);
const newAdapter = new PlaywrightAutomationAdapter({ headless: true });
const newAdapter = new PlaywrightAutomationAdapter({ headless: true }, logger);
expect(newAdapter.isConnected()).toBe(false);
await newAdapter.connect();

View File

@@ -287,7 +287,7 @@ describe('InMemorySessionRepository Integration Tests', () => {
const inProgressSessions = await repository.findByState('IN_PROGRESS');
expect(inProgressSessions).toHaveLength(1);
expect(inProgressSessions[0].id).toBe(session1.id);
expect(inProgressSessions[0]!.id).toBe(session1.id);
});
it('should return empty array when no sessions match state', async () => {

View File

@@ -1,14 +1,15 @@
import { describe, test, expect } from 'vitest'
import type { Page } from 'playwright'
import { PlaywrightAutomationAdapter } from 'packages/automation/infrastructure/adapters/automation'
describe('CarsFlow integration', () => {
test('adapter emits panel-attached then action-started then action-complete for performAddCar', async () => {
const adapter = new PlaywrightAutomationAdapter({})
const adapter = new PlaywrightAutomationAdapter({}, undefined, undefined)
const received: Array<{ type: string }> = []
adapter.onLifecycle?.((e) => {
received.push({ type: (e as { type: string }).type })
adapter.onLifecycle?.((e: { type: string; actionId?: string; timestamp: number; payload?: any }) => {
received.push({ type: e.type })
})
// Use mock page fixture: minimal object with required methods
const mockPage = {
waitForSelector: async () => {},
@@ -16,7 +17,7 @@ describe('CarsFlow integration', () => {
waitForTimeout: async () => {},
click: async () => {},
setDefaultTimeout: () => {},
}
} as unknown as Page
// call attachPanel which emits panel-attached and then action-started
await adapter.attachPanel(mockPage, 'add-car')

View File

@@ -45,6 +45,9 @@ describe('Overlay lifecycle (integration)', () => {
info: (...args: unknown[]) => void;
warn: (...args: unknown[]) => void;
error: (...args: unknown[]) => void;
fatal: (...args: unknown[]) => void;
child: (...args: unknown[]) => LoggerLike;
flush: (...args: unknown[]) => Promise<void>;
};
const logger = console as unknown as LoggerLike;
@@ -63,7 +66,7 @@ describe('Overlay lifecycle (integration)', () => {
const ackPromise: Promise<ActionAck> = service.startAction(action);
expect(publisher.events.length).toBe(1);
const first = publisher.events[0];
const first = publisher.events[0]!;
expect(first.type).toBe('modal-opened');
expect(first.actionId).toBe('hosted-session');
@@ -84,8 +87,8 @@ describe('Overlay lifecycle (integration)', () => {
expect(ack.id).toBe('hosted-session');
expect(ack.status).toBe('confirmed');
expect(publisher.events[0].type).toBe('modal-opened');
expect(publisher.events[0].actionId).toBe('hosted-session');
expect(publisher.events[0]!.type).toBe('modal-opened');
expect(publisher.events[0]!.actionId).toBe('hosted-session');
});
it('emits panel-missing when cancelAction is called', async () => {
@@ -96,6 +99,9 @@ describe('Overlay lifecycle (integration)', () => {
info: (...args: unknown[]) => void;
warn: (...args: unknown[]) => void;
error: (...args: unknown[]) => void;
fatal: (...args: unknown[]) => void;
child: (...args: unknown[]) => LoggerLike;
flush: (...args: unknown[]) => Promise<void>;
};
const logger = console as unknown as LoggerLike;
@@ -108,7 +114,7 @@ describe('Overlay lifecycle (integration)', () => {
await service.cancelAction('hosted-session-cancel');
expect(publisher.events.length).toBe(1);
const ev = publisher.events[0];
const ev = publisher.events[0]!;
expect(ev.type).toBe('panel-missing');
expect(ev.actionId).toBe('hosted-session-cancel');
});