mirror of
https://github.com/OpenBankProject/API-Explorer-II.git
synced 2026-02-06 10:47:04 +00:00
move integration testing to full playwright framework
This commit is contained in:
parent
74a0deaac8
commit
8418a6dea5
6
.gitignore
vendored
6
.gitignore
vendored
@ -48,3 +48,9 @@ public_key.pem
|
||||
|
||||
.vite/deps
|
||||
__snapshots__/
|
||||
|
||||
# Playwright auth
|
||||
playwright/.auth
|
||||
test-results/
|
||||
playwright-report/
|
||||
playwright-coverage/
|
||||
11
README.md
11
README.md
@ -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
16
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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
43
playwright.config.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
37
src/test/integration/auth.setup.ts
Normal file
37
src/test/integration/auth.setup.ts
Normal 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
10
src/test/integration/auto-imports.d.ts
vendored
Normal 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 {
|
||||
|
||||
}
|
||||
73
src/test/integration/global.setup.ts
Normal file
73
src/test/integration/global.setup.ts
Normal 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`);
|
||||
}
|
||||
9
src/test/integration/opey.integration.test.ts
Normal file
9
src/test/integration/opey.integration.test.ts
Normal 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', () => {
|
||||
|
||||
|
||||
})
|
||||
25
src/test/integration/playwright/.auth/user.json
Normal file
25
src/test/integration/playwright/.auth/user.json
Normal 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": []
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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';
|
||||
|
||||
let browser: Browser;
|
||||
let context: BrowserContext;
|
||||
let page: Page;
|
||||
const EXPRESS_URL = process.env.EXPRESS_SERVER_URL;
|
||||
|
||||
// Setup browser for testing
|
||||
beforeAll(async () => {
|
||||
browser = await chromium.launch({
|
||||
headless: true,
|
||||
// Use this to debug tests visually if needed
|
||||
// headless: false,
|
||||
// slowMo: 1000,
|
||||
});
|
||||
});
|
||||
test.describe('API Explorer Integration Tests', () => {
|
||||
|
||||
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');
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user