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,