Skip to content

Commit 1de5710

Browse files
committed
test: setup E2E infrastructure with Playwright and Docker
Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
1 parent fe06015 commit 1de5710

File tree

8 files changed

+288
-19
lines changed

8 files changed

+288
-19
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,6 @@ src/ui/src/core/pkg/
1515

1616
# Research and documentation files (generated, not part of source)
1717
*.md
18-
!README.md
18+
!README.mdsrc/ui/playwright-report/
19+
src/ui/test-results/
20+
temp/

scripts/run-e2e-tests.sh

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
#!/bin/bash
2+
set -e
3+
4+
# Internal script to run E2E tests inside the container
5+
6+
echo "🔧 Setting up test environment..."
7+
8+
# 0. Ensure bun is installed (needed for UI)
9+
if ! command -v bun &> /dev/null; then
10+
echo "⚠️ Bun not found, installing..."
11+
curl -fsSL https://bun.sh/install | bash
12+
export BUN_INSTALL="$HOME/.bun"
13+
export PATH="$BUN_INSTALL/bin:$PATH"
14+
fi
15+
16+
# 1. Ensure Centrifugo is available (using the tool script if needed)
17+
if ! command -v centrifugo &> /dev/null; then
18+
echo "⚠️ Centrifugo not found in PATH, checking tools directory..."
19+
if [ ! -f "tools/centrifugo" ]; then
20+
./tools/setup-centrifugo.sh
21+
fi
22+
export PATH=$PATH:$(pwd)/tools
23+
fi
24+
25+
# 2. Start Centrifugo directly (Backend is mocked, but we need real WS)
26+
echo "🚀 Starting Centrifugo..."
27+
# Using the config from backend/config/centrifugo_config.json
28+
CENTRIFUGO_CONFIG="src/backend/config/centrifugo_config.json"
29+
30+
# Generate self-signed certs for testing if missing
31+
mkdir -p temp/certs
32+
if [ ! -f "temp/certs/server.cert.pem" ]; then
33+
openssl req -newkey rsa:2048 -nodes -keyout temp/certs/server.key.pem -x509 -days 365 -out temp/certs/server.cert.pem -subj "/CN=localhost" 2>/dev/null
34+
fi
35+
36+
# Env vars for Centrifugo
37+
export CENTRIFUGO_HTTP_SERVER_TLS_CERT_PEM="temp/certs/server.cert.pem"
38+
export CENTRIFUGO_HTTP_SERVER_TLS_KEY_PEM="temp/certs/server.key.pem"
39+
export CENTRIFUGO_HTTP_SERVER_PORT="8000"
40+
export CENTRIFUGO_CLIENT_TOKEN_HMAC_SECRET_KEY="secret"
41+
export CENTRIFUGO_HTTP_API_KEY="api_key"
42+
export CENTRIFUGO_LOG_LEVEL="info"
43+
44+
centrifugo -c "$CENTRIFUGO_CONFIG" > /tmp/centrifugo.log 2>&1 &
45+
CENTRIFUGO_PID=$!
46+
47+
echo "⏳ Waiting for Centrifugo..."
48+
for i in {1..30}; do
49+
if curl -k -s https://localhost:8000/health > /dev/null; then
50+
echo "✅ Centrifugo is ready!"
51+
break
52+
fi
53+
if [ $i -eq 30 ]; then
54+
echo "❌ Centrifugo failed to start."
55+
cat /tmp/centrifugo.log
56+
kill $CENTRIFUGO_PID || true
57+
exit 1
58+
fi
59+
sleep 1
60+
done
61+
62+
# 3. Serve the Frontend
63+
echo "🌐 Starting Frontend Dev Server..."
64+
cd src/ui
65+
# Install dependencies if needed (container might not have node_modules)
66+
if [ ! -d "node_modules" ]; then
67+
echo "📦 Installing UI dependencies..."
68+
bun install
69+
fi
70+
71+
# Start vite dev server in background
72+
bun run dev --port 5173 > /tmp/vite.log 2>&1 &
73+
FRONTEND_PID=$!
74+
75+
# Wait for Frontend
76+
echo "⏳ Waiting for Frontend..."
77+
for i in {1..30}; do
78+
if curl -s http://localhost:5173 > /dev/null; then
79+
echo "✅ Frontend is ready!"
80+
break
81+
fi
82+
if [ $i -eq 30 ]; then
83+
echo "❌ Frontend failed to start."
84+
cat /tmp/vite.log
85+
kill $FRONTEND_PID || true
86+
kill $CENTRIFUGO_PID || true
87+
exit 1
88+
fi
89+
sleep 1
90+
done
91+
92+
# 4. Run Playwright Tests
93+
echo "🧪 Running Playwright Tests..."
94+
# BASE_URL is set for playwright.config.ts
95+
export BASE_URL="http://localhost:5173"
96+
97+
# Run tests
98+
npx playwright test
99+
100+
TEST_EXIT_CODE=$?
101+
102+
exit $TEST_EXIT_CODE

scripts/test-e2e-in-container.sh

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/bin/bash
2+
set -e
3+
4+
# Host script to run E2E tests inside the docker container
5+
6+
IMAGE="omnectshareddevacr.azurecr.io/rust:bookworm"
7+
8+
echo "🐳 Launching test container..."
9+
10+
# Check if we need to build the frontend first
11+
if [ ! -d "src/ui/dist" ]; then
12+
echo "📦 Building frontend..."
13+
./scripts/build-frontend.sh
14+
fi
15+
16+
# Run the test script inside the container
17+
# We mount the current directory to /workspace
18+
docker run --rm \
19+
-v $(pwd):/workspace \
20+
-w /workspace \
21+
--net=host \
22+
$IMAGE \
23+
/bin/bash -c "./scripts/run-e2e-tests.sh"

src/ui/bun.lock

Lines changed: 27 additions & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/ui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
},
2727
"devDependencies": {
2828
"@biomejs/biome": "2.2.4",
29+
"@playwright/test": "^1.57.0",
2930
"@types/bun": "^1.2.23",
3031
"@vitejs/plugin-vue": "^6.0.1",
3132
"@vue/tsconfig": "^0.8.1",

src/ui/playwright.config.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
2+
import { defineConfig, devices } from '@playwright/test';
3+
4+
/**
5+
* See https://playwright.dev/docs/test-configuration.
6+
*/
7+
export default defineConfig({
8+
testDir: './tests',
9+
/* Run tests in files in parallel */
10+
fullyParallel: true,
11+
/* Fail the build on CI if you accidentally left test.only in the source code. */
12+
forbidOnly: !!process.env.CI,
13+
/* Retry on CI only */
14+
retries: process.env.CI ? 2 : 0,
15+
/* opt out of parallel tests on CI. */
16+
workers: process.env.CI ? 1 : undefined,
17+
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
18+
reporter: 'html',
19+
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
20+
use: {
21+
/* Base URL to use in actions like `await page.goto('/')`. */
22+
baseURL: process.env.BASE_URL || 'http://localhost:5173',
23+
24+
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
25+
trace: 'on-first-retry',
26+
},
27+
28+
/* Configure projects for major browsers */
29+
projects: [
30+
{
31+
name: 'chromium',
32+
use: { ...devices['Desktop Chrome'] },
33+
},
34+
],
35+
36+
/* Run your local dev server before starting the tests */
37+
// webServer: {
38+
// command: 'npm run dev',
39+
// url: 'http://127.0.0.1:5173',
40+
// reuseExistingServer: !process.env.CI,
41+
// },
42+
});

src/ui/tests/fixtures/mock-api.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { Page } from '@playwright/test';
2+
3+
export async function mockConfig(page: Page) {
4+
const config = {
5+
KEYCLOAK_URL: 'http://localhost:8080',
6+
REALM: 'omnect',
7+
CLIENT_ID: 'omnect-ui',
8+
CENTRIFUGO_URL: 'wss://localhost:8000/connection/websocket'
9+
};
10+
11+
// Add as init script so it's available even before config.js loads
12+
await page.addInitScript((cfg) => {
13+
(window as any).__APP_CONFIG__ = cfg;
14+
}, config);
15+
16+
// Still mock the file request to avoid 404s
17+
await page.route('**/config.js', async (route) => {
18+
await route.fulfill({
19+
status: 200,
20+
contentType: 'application/javascript',
21+
body: `window.__APP_CONFIG__ = ${JSON.stringify(config)};`,
22+
});
23+
});
24+
}
25+
26+
export async function mockLoginSuccess(page: Page) {
27+
await page.route('**/token/login', async (route) => {
28+
await route.fulfill({
29+
status: 200,
30+
contentType: 'text/plain',
31+
body: 'mock_token_123',
32+
});
33+
});
34+
}
35+
36+
export async function mockRequireSetPassword(page: Page) {
37+
await page.route('**/require-set-password', async (route) => {
38+
await route.fulfill({
39+
status: 200,
40+
contentType: 'application/json',
41+
body: 'false',
42+
});
43+
});
44+
}
45+
46+
export async function mockNetworkConfig(page: Page) {
47+
await page.route('**/api/v1/network/config', async (route) => {
48+
await route.fulfill({
49+
status: 200,
50+
contentType: 'application/json',
51+
body: JSON.stringify({
52+
interfaces: [
53+
{
54+
name: 'eth0',
55+
dhcp: true,
56+
},
57+
],
58+
}),
59+
});
60+
});
61+
}

src/ui/tests/smoke.spec.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { test, expect } from '@playwright/test';
2+
import { mockLoginSuccess, mockRequireSetPassword, mockConfig } from './fixtures/mock-api';
3+
4+
test('has title', async ({ page }) => {
5+
await mockConfig(page);
6+
await page.goto('/');
7+
8+
// Expect a title "to contain" a substring.
9+
await expect(page).toHaveTitle(/omnect/i);
10+
});
11+
12+
test('login flow', async ({ page }) => {
13+
// Listen for console logs
14+
page.on('console', msg => console.log(`BROWSER LOG: ${msg.text()}`));
15+
page.on('pageerror', err => console.log(`BROWSER ERROR: ${err}`));
16+
17+
await mockConfig(page);
18+
await mockLoginSuccess(page);
19+
await mockRequireSetPassword(page);
20+
21+
await page.goto('/');
22+
23+
// Wait for the form to appear (it's hidden while checking password set requirement)
24+
// Using placeholder as Vuetify labels can be tricky with getByLabel
25+
await expect(page.getByPlaceholder(/enter your password/i)).toBeVisible({ timeout: 10000 });
26+
27+
// Check for the login button
28+
await expect(page.getByRole('button', { name: /log in/i })).toBeVisible();
29+
});

0 commit comments

Comments
 (0)