474 lines
16 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
}); |