From 8418a6dea51d757a11b377020533a7717ae5f2f6 Mon Sep 17 00:00:00 2001 From: Nemo Godebski-Pedersen Date: Wed, 26 Mar 2025 12:59:39 +0000 Subject: [PATCH] move integration testing to full playwright framework --- .gitignore | 8 +- README.md | 11 +- package-lock.json | 16 +++ package.json | 1 + playwright.config.ts | 43 +++++++ src/test/integration/auth.setup.ts | 37 ++++++ src/test/integration/auto-imports.d.ts | 10 ++ src/test/integration/global.setup.ts | 73 ++++++++++++ src/test/integration/opey.integration.test.ts | 9 ++ .../integration/playwright/.auth/user.json | 25 ++++ src/test/integration/setup.ts | 110 ------------------ .../integration/simple.integration.test.ts | 67 ++++------- tsconfig.app.json | 2 +- 13 files changed, 250 insertions(+), 162 deletions(-) create mode 100644 playwright.config.ts create mode 100644 src/test/integration/auth.setup.ts create mode 100644 src/test/integration/auto-imports.d.ts create mode 100644 src/test/integration/global.setup.ts create mode 100644 src/test/integration/opey.integration.test.ts create mode 100644 src/test/integration/playwright/.auth/user.json delete mode 100644 src/test/integration/setup.ts diff --git a/.gitignore b/.gitignore index 51a8798..a8912a1 100644 --- a/.gitignore +++ b/.gitignore @@ -47,4 +47,10 @@ public_key.pem .vite/deps -__snapshots__/ \ No newline at end of file +__snapshots__/ + +# Playwright auth +playwright/.auth +test-results/ +playwright-report/ +playwright-coverage/ \ No newline at end of file diff --git a/README.md b/README.md index dc8fa6a..00a04c7 100644 --- a/README.md +++ b/README.md @@ -60,17 +60,18 @@ npm test ``` -##### Run Integration Tests with vitest and [Playwright](https://playwright.dev/) +##### Run Integration Tests with [Playwright](https://playwright.dev/) - - ```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 ``` - ## Compile and Minify for Production diff --git a/package-lock.json b/package-lock.json index ed76bc7..b5aae60 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 3247ea5..e9fa094 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..f9dbf25 --- /dev/null +++ b/playwright.config.ts @@ -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, + }, +}); \ No newline at end of file diff --git a/src/test/integration/auth.setup.ts b/src/test/integration/auth.setup.ts new file mode 100644 index 0000000..718d27a --- /dev/null +++ b/src/test/integration/auth.setup.ts @@ -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 }); +}); \ No newline at end of file diff --git a/src/test/integration/auto-imports.d.ts b/src/test/integration/auto-imports.d.ts new file mode 100644 index 0000000..9d24007 --- /dev/null +++ b/src/test/integration/auto-imports.d.ts @@ -0,0 +1,10 @@ +/* eslint-disable */ +/* prettier-ignore */ +// @ts-nocheck +// noinspection JSUnusedGlobalSymbols +// Generated by unplugin-auto-import +// biome-ignore lint: disable +export {} +declare global { + +} diff --git a/src/test/integration/global.setup.ts b/src/test/integration/global.setup.ts new file mode 100644 index 0000000..8f7c41e --- /dev/null +++ b/src/test/integration/global.setup.ts @@ -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 { + 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`); +} diff --git a/src/test/integration/opey.integration.test.ts b/src/test/integration/opey.integration.test.ts new file mode 100644 index 0000000..c74330b --- /dev/null +++ b/src/test/integration/opey.integration.test.ts @@ -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', () => { + + +}) \ No newline at end of file diff --git a/src/test/integration/playwright/.auth/user.json b/src/test/integration/playwright/.auth/user.json new file mode 100644 index 0000000..ba8976b --- /dev/null +++ b/src/test/integration/playwright/.auth/user.json @@ -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": [] +} \ No newline at end of file diff --git a/src/test/integration/setup.ts b/src/test/integration/setup.ts deleted file mode 100644 index b37b4be..0000000 --- a/src/test/integration/setup.ts +++ /dev/null @@ -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 { - 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; -} diff --git a/src/test/integration/simple.integration.test.ts b/src/test/integration/simple.integration.test.ts index d60ffc6..c0deb36 100644 --- a/src/test/integration/simple.integration.test.ts +++ b/src/test/integration/simple.integration.test.ts @@ -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'); diff --git a/tsconfig.app.json b/tsconfig.app.json index 745113c..1a5f035 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -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,