Files
gridpilot.gg/tests/integration/playwright-automation.test.ts
2025-11-30 02:07:08 +01:00

474 lines
16 KiB
TypeScript

/**
* Integration tests for PlaywrightAutomationAdapter using mock HTML fixtures.
*
* These tests verify that the browser automation adapter correctly interacts
* with the mock fixtures served by FixtureServer.
*/
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import { FixtureServer, getAllStepFixtureMappings, PlaywrightAutomationAdapter } from 'packages/infrastructure/adapters/automation';
import { StepId } from '../../packages/domain/value-objects/StepId';
describe('Playwright Browser Automation', () => {
let server: FixtureServer;
let adapter: PlaywrightAutomationAdapter;
let baseUrl: string;
beforeAll(async () => {
server = new FixtureServer();
const serverInfo = await server.start();
baseUrl = serverInfo.url;
adapter = new PlaywrightAutomationAdapter({
headless: true,
timeout: 5000,
baseUrl,
});
const connectResult = await adapter.connect();
expect(connectResult.success).toBe(true);
});
afterAll(async () => {
await adapter.disconnect();
await server.stop();
});
describe('FixtureServer Tests', () => {
it('should start and report running state', () => {
expect(server.isRunning()).toBe(true);
});
it('should serve the root URL with step 2 fixture', async () => {
const result = await adapter.navigateToPage(`${baseUrl}/`);
expect(result.success).toBe(true);
const step = await adapter.getCurrentStep();
expect(step).toBe(2);
});
it('should serve all 16 step fixtures (steps 2-17)', async () => {
const mappings = getAllStepFixtureMappings();
const stepNumbers = Object.keys(mappings).map(Number);
expect(stepNumbers).toHaveLength(16);
expect(stepNumbers).toContain(2);
expect(stepNumbers).toContain(17);
for (const stepNum of stepNumbers) {
const url = server.getFixtureUrl(stepNum);
const result = await adapter.navigateToPage(url);
expect(result.success).toBe(true);
}
});
it('should serve CSS file correctly', async () => {
const page = adapter.getPage();
expect(page).not.toBeNull();
await adapter.navigateToPage(server.getFixtureUrl(2));
const cssLoaded = await page!.evaluate(() => {
const styles = getComputedStyle(document.body);
return styles.backgroundColor !== '';
});
expect(cssLoaded).toBe(true);
});
it('should return 404 for non-existent files', async () => {
const page = adapter.getPage();
expect(page).not.toBeNull();
const response = await page!.goto(`${baseUrl}/non-existent-file.html`);
expect(response?.status()).toBe(404);
});
});
describe('Step Detection Tests', () => {
beforeEach(async () => {
await adapter.navigateToPage(server.getFixtureUrl(2));
});
it('should detect current step via data-step attribute', async () => {
const step = await adapter.getCurrentStep();
expect(step).toBe(2);
});
it('should correctly identify step 3', async () => {
await adapter.navigateToPage(server.getFixtureUrl(3));
const step = await adapter.getCurrentStep();
expect(step).toBe(3);
});
it('should correctly identify step 17 (final step)', async () => {
await adapter.navigateToPage(server.getFixtureUrl(17));
const step = await adapter.getCurrentStep();
expect(step).toBe(17);
});
it('should detect step from each fixture file correctly', async () => {
// Note: Some fixture files have mismatched names vs data-step attributes
// This test verifies we can detect whatever step is in each file
const mappings = getAllStepFixtureMappings();
for (const stepNum of Object.keys(mappings).map(Number)) {
await adapter.navigateToPage(server.getFixtureUrl(stepNum));
const detectedStep = await adapter.getCurrentStep();
expect(detectedStep).toBeGreaterThanOrEqual(2);
expect(detectedStep).toBeLessThanOrEqual(17);
}
});
it('should wait for specific step to be visible', async () => {
await adapter.navigateToPage(server.getFixtureUrl(4));
await expect(adapter.waitForStep(4)).resolves.not.toThrow();
});
});
describe('Navigation Tests', () => {
beforeEach(async () => {
await adapter.navigateToPage(server.getFixtureUrl(2));
});
it('should click data-action="create" on step 2 to navigate to step 3', async () => {
const result = await adapter.clickAction('create');
expect(result.success).toBe(true);
await adapter.waitForStep(3);
const step = await adapter.getCurrentStep();
expect(step).toBe(3);
});
it('should click data-action="next" to navigate forward', async () => {
await adapter.navigateToPage(server.getFixtureUrl(3));
const result = await adapter.clickAction('next');
expect(result.success).toBe(true);
await adapter.waitForStep(4);
const step = await adapter.getCurrentStep();
expect(step).toBe(4);
});
it('should click data-action="back" to navigate backward', async () => {
await adapter.navigateToPage(server.getFixtureUrl(4));
const result = await adapter.clickAction('back');
expect(result.success).toBe(true);
await adapter.waitForStep(3);
const step = await adapter.getCurrentStep();
expect(step).toBe(3);
});
it('should fail gracefully when clicking non-existent action', async () => {
const result = await adapter.clickElement('[data-action="nonexistent"]');
expect(result.success).toBe(false);
expect(result.error).toBeDefined();
});
});
describe('Form Field Tests', () => {
beforeEach(async () => {
await adapter.navigateToPage(server.getFixtureUrl(3));
});
it('should fill data-field text inputs', async () => {
const result = await adapter.fillField('sessionName', 'Test Session');
expect(result.success).toBe(true);
expect(result.fieldName).toBe('sessionName');
expect(result.value).toBe('Test Session');
const page = adapter.getPage()!;
const value = await page.inputValue('[data-field="sessionName"]');
expect(value).toBe('Test Session');
});
it('should fill password field', async () => {
const result = await adapter.fillField('password', 'secret123');
expect(result.success).toBe(true);
const page = adapter.getPage()!;
const value = await page.inputValue('[data-field="password"]');
expect(value).toBe('secret123');
});
it('should fill textarea field', async () => {
const result = await adapter.fillField('description', 'This is a test description');
expect(result.success).toBe(true);
const page = adapter.getPage()!;
const value = await page.inputValue('[data-field="description"]');
expect(value).toBe('This is a test description');
});
it('should select from data-dropdown elements', async () => {
await adapter.navigateToPage(server.getFixtureUrl(4));
await adapter.selectDropdown('region', 'eu-central');
const page = adapter.getPage()!;
const value = await page.inputValue('[data-dropdown="region"]');
expect(value).toBe('eu-central');
});
it('should toggle data-toggle checkboxes', async () => {
await adapter.navigateToPage(server.getFixtureUrl(4));
const page = adapter.getPage()!;
const initialState = await page.isChecked('[data-toggle="startNow"]');
await adapter.setToggle('startNow', !initialState);
const newState = await page.isChecked('[data-toggle="startNow"]');
expect(newState).toBe(!initialState);
});
it('should set data-slider range inputs', async () => {
await adapter.navigateToPage(server.getFixtureUrl(17));
await adapter.setSlider('rubberLevel', 75);
const page = adapter.getPage()!;
const value = await page.inputValue('[data-slider="rubberLevel"]');
expect(value).toBe('75');
});
it('should fail gracefully when filling non-existent field', async () => {
const result = await adapter.fillFormField('nonexistent', 'value');
expect(result.success).toBe(false);
expect(result.error).toBeDefined();
});
});
describe('Modal Tests', () => {
beforeEach(async () => {
await adapter.navigateToPage(server.getFixtureUrl(7));
});
it('should detect modal presence via data-modal="true"', async () => {
const page = adapter.getPage()!;
const modalExists = await page.$('[data-modal="true"]');
expect(modalExists).not.toBeNull();
});
it('should wait for modal to be visible', async () => {
await expect(adapter.waitForModal()).resolves.not.toThrow();
});
it('should interact with modal content fields', async () => {
const result = await adapter.fillField('adminSearch', 'John');
expect(result.success).toBe(true);
const page = adapter.getPage()!;
const value = await page.inputValue('[data-field="adminSearch"]');
expect(value).toBe('John');
});
it('should close modal via data-action="confirm"', async () => {
const result = await adapter.clickAction('confirm');
expect(result.success).toBe(true);
await adapter.waitForStep(5);
const step = await adapter.getCurrentStep();
expect(step).toBe(5);
});
it('should close modal via data-action="cancel"', async () => {
await adapter.navigateToPage(server.getFixtureUrl(7));
const result = await adapter.clickAction('cancel');
expect(result.success).toBe(true);
await adapter.waitForStep(5);
const step = await adapter.getCurrentStep();
expect(step).toBe(5);
});
it('should handle modal via handleModal method with confirm action', async () => {
const stepId = StepId.create(6);
const result = await adapter.handleModal(stepId, 'confirm');
expect(result.success).toBe(true);
expect(result.action).toBe('confirm');
});
it('should select list items in modal via data-item', async () => {
await expect(adapter.selectListItem('admin-001')).resolves.not.toThrow();
});
});
describe('Full Flow Tests', () => {
beforeEach(async () => {
await adapter.navigateToPage(server.getFixtureUrl(2));
});
it('should navigate through steps 2 → 3 → 4', async () => {
expect(await adapter.getCurrentStep()).toBe(2);
await adapter.clickAction('create');
await adapter.waitForStep(3);
expect(await adapter.getCurrentStep()).toBe(3);
await adapter.clickAction('next');
await adapter.waitForStep(4);
expect(await adapter.getCurrentStep()).toBe(4);
});
it('should fill form fields and navigate through steps 3 → 4 → 5', async () => {
await adapter.navigateToPage(server.getFixtureUrl(3));
await adapter.fillField('sessionName', 'My Race Session');
await adapter.fillField('password', 'pass123');
await adapter.fillField('description', 'A test racing session');
await adapter.clickAction('next');
await adapter.waitForStep(4);
await adapter.selectDropdown('region', 'us-west');
await adapter.setToggle('startNow', true);
await adapter.clickAction('next');
await adapter.waitForStep(5);
expect(await adapter.getCurrentStep()).toBe(5);
});
it('should navigate backward through multiple steps', async () => {
await adapter.navigateToPage(server.getFixtureUrl(5));
expect(await adapter.getCurrentStep()).toBe(5);
await adapter.clickAction('back');
await adapter.waitForStep(4);
expect(await adapter.getCurrentStep()).toBe(4);
await adapter.clickAction('back');
await adapter.waitForStep(3);
expect(await adapter.getCurrentStep()).toBe(3);
});
it('should execute step 2 via executeStep method', async () => {
const stepId = StepId.create(2);
const result = await adapter.executeStep(stepId, {});
expect(result.success).toBe(true);
await adapter.waitForStep(3);
expect(await adapter.getCurrentStep()).toBe(3);
});
it('should execute step 3 with config via executeStep method', async () => {
await adapter.navigateToPage(server.getFixtureUrl(3));
const stepId = StepId.create(3);
const result = await adapter.executeStep(stepId, {
sessionName: 'Automated Session',
password: 'auto123',
description: 'Created by automation',
});
expect(result.success).toBe(true);
await adapter.waitForStep(4);
expect(await adapter.getCurrentStep()).toBe(4);
});
it('should execute step 4 with dropdown and toggle config', async () => {
await adapter.navigateToPage(server.getFixtureUrl(4));
const stepId = StepId.create(4);
const result = await adapter.executeStep(stepId, {
region: 'asia',
startNow: true,
});
expect(result.success).toBe(true);
await adapter.waitForStep(5);
expect(await adapter.getCurrentStep()).toBe(5);
});
});
describe('Error Handling Tests', () => {
it('should return error when browser not connected', async () => {
const disconnectedAdapter = new PlaywrightAutomationAdapter({
headless: true,
timeout: 1000,
});
const navResult = await disconnectedAdapter.navigateToPage('http://localhost:9999');
expect(navResult.success).toBe(false);
expect(navResult.error).toBe('Browser not connected');
const fillResult = await disconnectedAdapter.fillFormField('test', 'value');
expect(fillResult.success).toBe(false);
expect(fillResult.error).toBe('Browser not connected');
const clickResult = await disconnectedAdapter.clickElement('test');
expect(clickResult.success).toBe(false);
expect(clickResult.error).toBe('Browser not connected');
});
it('should handle timeout when waiting for non-existent element', async () => {
const shortTimeoutAdapter = new PlaywrightAutomationAdapter({
headless: true,
timeout: 100,
});
await shortTimeoutAdapter.connect();
await shortTimeoutAdapter.navigateToPage(server.getFixtureUrl(2));
const result = await shortTimeoutAdapter.waitForElement('[data-step="99"]', 100);
expect(result.success).toBe(false);
expect(result.error).toContain('Timeout');
await shortTimeoutAdapter.disconnect();
});
it('should report connected state correctly', async () => {
expect(adapter.isConnected()).toBe(true);
const newAdapter = new PlaywrightAutomationAdapter({ headless: true });
expect(newAdapter.isConnected()).toBe(false);
await newAdapter.connect();
expect(newAdapter.isConnected()).toBe(true);
await newAdapter.disconnect();
expect(newAdapter.isConnected()).toBe(false);
});
});
describe('Indicator and List Tests', () => {
it('should detect step indicator element', async () => {
await adapter.navigateToPage(server.getFixtureUrl(3));
const page = adapter.getPage()!;
const indicator = await page.$('[data-indicator="race-information"]');
expect(indicator).not.toBeNull();
});
it('should detect list container', async () => {
await adapter.navigateToPage(server.getFixtureUrl(8));
const page = adapter.getPage()!;
const list = await page.$('[data-list="cars"]');
expect(list).not.toBeNull();
});
it('should detect modal trigger button', async () => {
await adapter.navigateToPage(server.getFixtureUrl(8));
const page = adapter.getPage()!;
const trigger = await page.$('[data-modal-trigger="car"]');
expect(trigger).not.toBeNull();
});
it('should click modal trigger and navigate to modal step', async () => {
await adapter.navigateToPage(server.getFixtureUrl(8));
await adapter.openModalTrigger('car');
// The modal trigger navigates to step-10-add-car.html which has data-step="9"
await adapter.waitForStep(9);
expect(await adapter.getCurrentStep()).toBe(9);
});
});
});