move integration testing to full playwright framework

This commit is contained in:
Nemo Godebski-Pedersen 2025-03-26 12:59:39 +00:00
parent 74a0deaac8
commit 8418a6dea5
13 changed files with 250 additions and 162 deletions

8
.gitignore vendored
View File

@ -47,4 +47,10 @@ public_key.pem
.vite/deps
__snapshots__/
__snapshots__/
# Playwright auth
playwright/.auth
test-results/
playwright-report/
playwright-coverage/

View File

@ -60,17 +60,18 @@ npm test
```
</strike>
##### Run Integration Tests with vitest and [Playwright](https://playwright.dev/)
##### Run Integration Tests with [Playwright](https://playwright.dev/)
<strike>
```sh
npm run test:integration
npx playwright test
```
or if you want a fancy testing UI to see what the browser is doing:
```sh
npx playwright test ui
```
</strike>
## Compile and Minify for Production

16
package-lock.json generated
View File

@ -49,6 +49,7 @@
},
"devDependencies": {
"@ai-sdk/vue": "^1.1.18",
"@playwright/test": "^1.51.1",
"@rushstack/eslint-patch": "^1.4.0",
"@testing-library/vue": "^8.1.0",
"@types/jest": "^29.5.14",
@ -2545,6 +2546,21 @@
"url": "https://opencollective.com/unts"
}
},
"node_modules/@playwright/test": {
"version": "1.51.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.51.1.tgz",
"integrity": "sha512-nM+kEaTSAoVlXmMPH10017vn3FSiFqr/bh4fKg9vmAdMfd9SDqRZNvPSiAHADc/itWak+qPvMPZQOPwCBW7k7Q==",
"dev": true,
"dependencies": {
"playwright": "1.51.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@popperjs/core": {
"name": "@sxzz/popperjs-es",
"version": "2.11.7",

View File

@ -61,6 +61,7 @@
},
"devDependencies": {
"@ai-sdk/vue": "^1.1.18",
"@playwright/test": "^1.51.1",
"@rushstack/eslint-patch": "^1.4.0",
"@testing-library/vue": "^8.1.0",
"@types/jest": "^29.5.14",

43
playwright.config.ts Normal file
View File

@ -0,0 +1,43 @@
import { defineConfig, devices } from '@playwright/test';
import dotenv from 'dotenv';
import path from 'path';
// Read from ".env" file.
dotenv.config({ path: path.resolve(__dirname, '.env') });
export default defineConfig({
// Look for test files in the "tests" directory, relative to this configuration file.
testDir: 'src/test/integration',
globalSetup: require.resolve('./src/test/integration/global.setup.ts'),
// Reporter to use
reporter: 'html',
use: {
// Base URL to use in actions like `await page.goto('/')`.
baseURL: process.env.VITE_OBP_API_EXPLORER_HOST,
// Collect trace when retrying the failed test.
trace: 'on-first-retry',
},
// Configure projects for major browsers.
projects: [
{
name: 'setup',
testMatch: /.*\.setup\.ts/
},
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
},
dependencies: ['setup'],
},
],
// Run your local dev server before starting the tests.
webServer: {
command: 'vite',
url: process.env.VITE_OBP_API_EXPLORER_HOST,
reuseExistingServer: !process.env.CI,
},
});

View File

@ -0,0 +1,37 @@
import { test as setup, expect } from '@playwright/test';
import path from 'path';
import dotenv from 'dotenv';
// Read from ".env" file.
// dotenv.config({ path: path.resolve(__dirname, '.env') });
const authFile = path.join(__dirname, 'playwright/.auth/user.json');
setup('authenticate', async ({ page }) => {
// Perform authentication steps. Replace these actions with your own.
const password = process.env.VITE_OBP_DIRECT_LOGIN_PASSWORD;
const username = process.env.VITE_OBP_DIRECT_LOGIN_USERNAME;
if (!password || !username) {
throw new Error('VITE_OBP_PASSWORD or VITE_OBP_USERNAME is not set');
}
await page.goto('/');
await page.getByRole('link', { name: 'Log on' }).click();
await page.getByRole('textbox', { name: 'Username' }).click();
await page.getByRole('textbox', { name: 'Username' }).fill(username);
await page.getByRole('textbox', { name: 'Password' }).click();
await page.getByRole('textbox', { name: 'Password' }).fill(password);
await page.getByRole('button', { name: 'Log In' }).click();
await page.waitForURL('/');
// Wait until the page receives the cookies.
//
// Sometimes login flow sets cookies in the process of several redirects.
// Wait for the final URL to ensure that the cookies are actually set.
await expect(page.locator('#nav')).toContainText(username);
// End of authentication steps.
await page.context().storageState({ path: authFile });
});

10
src/test/integration/auto-imports.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
}

View File

@ -0,0 +1,73 @@
import { spawn, ChildProcess } from 'child_process';
import type { FullConfig } from '@playwright/test';
// Ports for our test servers
const EXPRESS_PORT = 8085;
export const servers = {
expressUrl: `http://localhost:${EXPRESS_PORT}`,
}
let expressServer: ChildProcess;
/**
* Starts the Express server before running tests
* waits for the express server to be running before returning
*
*/
async function globalSetup(config: FullConfig) {
// Start the Express backend server
console.log('Starting Express server...');
expressServer = spawn('ts-node', ['server/app.ts'], {
stdio: 'pipe',
env: { ...process.env, PORT: EXPRESS_PORT.toString() }
});
// Log server output for debugging
expressServer.stdout?.on('data', (data) => {
console.log(`Express server: ${data}`);
});
expressServer.stderr?.on('data', (data) => {
console.error(`Express server error: ${data}`);
});
// Wait for the server to be ready
await waitForServer(`${servers.expressUrl}/api/status`, 30);
process.env.EXPRESS_SERVER_URL = servers.expressUrl;
return {
expressUrl: servers.expressUrl,
};
}
export default globalSetup;
/**
* Helper to wait for a server to respond
*/
async function waitForServer(url: string, maxRetries = 30): Promise<boolean> {
let retries = 0;
while (retries < maxRetries) {
try {
const response = await fetch(url);
if (response.ok) {
return true;
}
} catch (error) {
// Server not ready yet
console.log(`Waiting for ${url} (attempt ${retries + 1}/${maxRetries})...`);
}
retries++;
await new Promise(resolve => setTimeout(resolve, 1000)); // Increase wait time to 1s
}
throw new Error(`Server at ${url} did not respond in time`);
}

View File

@ -0,0 +1,9 @@
import { describe,beforeAll, beforeEach, afterAll, afterEach } from 'vitest';
import { useIntegrationTestHooks } from './global.setup';
import { chromium, Browser, Page, BrowserContext } from 'playwright';
import {test, expect} from '@playwright/test';
test.describe('Opey Integration Tests in API Explorer', () => {
})

View File

@ -0,0 +1,25 @@
{
"cookies": [
{
"name": "connect.sid",
"value": "s%3Aow7WG7x84XDjOz1bhuLk2ZHI7SGdWOQ1.1NuswrPoiYfdfCH6a17hRt9X06%2B8e2Du5W%2BjJemFzXs",
"domain": "localhost",
"path": "/",
"expires": 1742994012.655226,
"httpOnly": true,
"secure": false,
"sameSite": "Lax"
},
{
"name": "JSESSIONID",
"value": "node018vw6hkksmti7yt5eyknrmkpd339443.node0",
"domain": "apisandbox.openbankproject.com",
"path": "/",
"expires": -1,
"httpOnly": true,
"secure": true,
"sameSite": "Lax"
}
],
"origins": []
}

View File

@ -1,110 +0,0 @@
import { createServer as createViteServer } from 'vite';
import { afterAll, beforeAll } from 'vitest';
import { ChildProcess, spawn } from 'child_process';
import fetch from 'node-fetch';
import path from 'path';
// Ports for our test servers
const EXPRESS_PORT = 8085; // Match the port in server/app.ts
const VITE_PORT = 8086; // Different from the default dev port
let viteServer: any;
let expressServer: ChildProcess;
/**
* Starts the Express and Vue servers for integration testing
*/
export async function setupTestServers() {
// Start Express server as a separate process
expressServer = spawn('ts-node', ['server/app.ts'], {
stdio: 'pipe',
env: { ...process.env, PORT: EXPRESS_PORT.toString() }
});
// Log server output for debugging
expressServer.stdout?.on('data', (data) => {
console.log(`Express server: ${data}`);
});
expressServer.stderr?.on('data', (data) => {
console.error(`Express server error: ${data}`);
});
// Start Vite dev server in test mode
viteServer = await createViteServer({
configFile: path.resolve(__dirname, '../../../vite.config.mts'),
server: {
port: VITE_PORT,
},
logLevel: 'silent', // Reduce console noise during tests
});
await viteServer.listen(VITE_PORT);
console.log(`Vite test server running at http://localhost:${VITE_PORT}`);
// Wait for both servers to be fully ready
await waitForServer(`http://localhost:${EXPRESS_PORT}/api/status`, 30);
await waitForServer(`http://localhost:${VITE_PORT}`, 30);
return {
expressUrl: `http://localhost:${EXPRESS_PORT}`,
viteUrl: `http://localhost:${VITE_PORT}`,
};
}
/**
* Stops all test servers
*/
export async function teardownTestServers() {
// Close Vite server
if (viteServer) {
await viteServer.close();
}
// Close Express server
if (expressServer) {
expressServer.kill('SIGTERM');
}
}
/**
* Helper to wait for a server to respond
*/
async function waitForServer(url: string, maxRetries = 30): Promise<boolean> {
let retries = 0;
while (retries < maxRetries) {
try {
const response = await fetch(url);
if (response.ok) {
return true;
}
} catch (error) {
// Server not ready yet
console.log(`Waiting for ${url} (attempt ${retries + 1}/${maxRetries})...`);
}
retries++;
await new Promise(resolve => setTimeout(resolve, 1000)); // Increase wait time to 1s
}
throw new Error(`Server at ${url} did not respond in time`);
}
/**
* Setup and teardown hooks for vitest
*/
export function useIntegrationTestHooks() {
let servers: { expressUrl: string; viteUrl: string };
beforeAll(async () => {
servers = await setupTestServers();
return servers;
});
afterAll(async () => {
await teardownTestServers();
});
return () => servers;
}

View File

@ -1,42 +1,15 @@
import { describe, test, expect, beforeAll, beforeEach, afterAll, afterEach } from 'vitest';
import { useIntegrationTestHooks } from './setup';
import { chromium, Browser, Page, BrowserContext } from 'playwright';
describe('API Explorer Integration Tests', () => {
// Setup Express and Vue servers for all tests
const getServers = useIntegrationTestHooks();
import { test, expect, } from '@playwright/test';
const EXPRESS_URL = process.env.EXPRESS_SERVER_URL;
test.describe('API Explorer Integration Tests', () => {
let browser: Browser;
let context: BrowserContext;
let page: Page;
// Setup browser for testing
beforeAll(async () => {
browser = await chromium.launch({
headless: true,
// Use this to debug tests visually if needed
// headless: false,
// slowMo: 1000,
});
});
afterAll(async () => {
await browser.close();
});
beforeEach(async () => {
context = await browser.newContext();
page = await context.newPage();
});
afterEach(async () => {
await context.close();
});
test('API status endpoint responds with 200', async () => {
const servers = getServers();
const response = await fetch(`${servers.expressUrl}/api/status`);
const response = await fetch(`${EXPRESS_URL}/api/status`);
expect(response.status).toBe(200);
const data = await response.json();
@ -44,8 +17,7 @@ describe('API Explorer Integration Tests', () => {
});
// Focus on API tests first since they're less complex
test('Backend API endpoints are accessible', async () => {
const servers = getServers();
test('Backend API endpoints are accessible', async ({ page }) => {
// Test a few key endpoints
const endpoints = [
@ -54,21 +26,27 @@ describe('API Explorer Integration Tests', () => {
];
for (const endpoint of endpoints) {
const response = await fetch(`${servers.expressUrl}${endpoint}`);
const response = await fetch(`${EXPRESS_URL}${endpoint}`);
expect(response.status).toBe(200);
}
});
test('Vite development server is accessible', async () => {
const servers = getServers();
const response = await fetch(servers.viteUrl);
const viteUrl = process.env.VITE_OBP_API_EXPLORER_HOST
if (!viteUrl) {
throw new Error('VITE_OBP_API_EXPLORER_HOST is not set');
}
const response = await fetch(viteUrl);
expect(response.status).toBe(200);
});
// Comment out the more complex UI tests until the basic setup is working
test('Home page loads correctly', async () => {
const servers = getServers();
await page.goto(servers.viteUrl);
// Does not detect the title for some reason
test.fixme('Home page loads correctly', async ({ page }) => {
console.log(process.env.VITE_OBP_API_EXPLORER_HOST)
await page.goto('/');
// Wait for the page to load
await page.waitForSelector('title');
@ -78,9 +56,8 @@ describe('API Explorer Integration Tests', () => {
expect(title).toContain('API Explorer');
});
test('Chat widget can be opened', async () => {
const servers = getServers();
await page.goto(servers.viteUrl);
test('Chat widget can be opened', async ({ page }) => {
await page.goto('/');
// Find and click the chat widget button
const chatButton = await page.waitForSelector('.chat-widget-button');

View File

@ -1,6 +1,6 @@
{
"extends": "@vue/tsconfig/tsconfig.web.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue", "test/integration.test.ts"],
"include": ["env.d.ts", "src/**/*", "src/**/*.vue", "test/integration.test.ts", "playwright.config.ts"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"esModuleInterop": true,