Skip to content

Commit 9b21b39

Browse files
committed
ci playwright fixes
1 parent fa55c9c commit 9b21b39

File tree

5 files changed

+92
-179
lines changed

5 files changed

+92
-179
lines changed

.github/actions/setup-ci-compose/action.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,12 @@ runs:
2828
yq eval '.services.backend.environment += ["MONGO_ROOT_PASSWORD=rootpassword"]' -i docker-compose.ci.yaml
2929
yq eval '.services.backend.environment += ["OTEL_SDK_DISABLED=true"]' -i docker-compose.ci.yaml
3030
31-
# Remove hot-reload volume mounts (causes permission issues in CI)
31+
# Remove hot-reload volume mounts (causes permission issues and slow rebuilds in CI)
3232
yq eval '.services.backend.volumes = [.services.backend.volumes[] | select(. != "./backend:/app")]' -i docker-compose.ci.yaml
3333
yq eval '.services."k8s-worker".volumes = [.services."k8s-worker".volumes[] | select(. != "./backend:/app:ro")]' -i docker-compose.ci.yaml
3434
yq eval '.services."pod-monitor".volumes = [.services."pod-monitor".volumes[] | select(. != "./backend:/app:ro")]' -i docker-compose.ci.yaml
3535
yq eval '.services."result-processor".volumes = [.services."result-processor".volumes[] | select(. != "./backend:/app:ro")]' -i docker-compose.ci.yaml
36+
yq eval '.services.frontend.volumes = [.services.frontend.volumes[] | select(. != "./frontend:/app")]' -i docker-compose.ci.yaml
3637
3738
# Disable Kafka SASL authentication for CI
3839
yq eval 'del(.services.kafka.environment.KAFKA_OPTS)' -i docker-compose.ci.yaml

frontend/e2e/auth.spec.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ test.describe('Authentication', () => {
1616
// Wait for the login form to render
1717
await page.waitForSelector('#username');
1818

19-
await expect(page.locator('h2')).toContainText('Sign in to your account');
19+
await expect(page.getByRole('heading', { name: 'Sign in to your account' })).toBeVisible();
2020
await expect(page.locator('#username')).toBeVisible();
2121
await expect(page.locator('#password')).toBeVisible();
2222
await expect(page.locator('button[type="submit"]')).toBeVisible();
@@ -64,6 +64,8 @@ test.describe('Authentication', () => {
6464
await page.fill('#password', 'user123');
6565
await page.click('button[type="submit"]');
6666

67+
// Wait for Editor page content (router updates DOM before URL)
68+
await expect(page.getByRole('heading', { name: 'Code Editor' })).toBeVisible();
6769
await expect(page).toHaveURL(/\/editor/);
6870
});
6971

@@ -98,23 +100,26 @@ test.describe('Authentication', () => {
98100
await page.fill('#password', 'user123');
99101
await page.click('button[type="submit"]');
100102

103+
// Wait for Settings page content (redirect target)
104+
await expect(page.getByRole('heading', { name: 'Settings', level: 1 })).toBeVisible();
101105
await expect(page).toHaveURL(/\/settings/);
102106
});
103107

104108
test('has link to registration page', async ({ page }) => {
105109
await page.goto('/login');
106110
await page.waitForSelector('#username');
107111

108-
const registerLink = page.locator('a[href="/register"]');
112+
// Use specific text to avoid matching the Register button in header
113+
const registerLink = page.getByRole('link', { name: 'create a new account' });
109114
await expect(registerLink).toBeVisible();
110-
await expect(registerLink).toContainText('create a new account');
111115
});
112116

113117
test('can navigate to registration page', async ({ page }) => {
114118
await page.goto('/login');
115119
await page.waitForSelector('#username');
116120

117-
await page.click('a[href="/register"]');
121+
// Click the specific link in the form, not the header button
122+
await page.getByRole('link', { name: 'create a new account' }).click();
118123

119124
await expect(page).toHaveURL(/\/register/);
120125
});
@@ -135,13 +140,18 @@ test.describe('Logout', () => {
135140
await page.fill('#username', 'user');
136141
await page.fill('#password', 'user123');
137142
await page.click('button[type="submit"]');
138-
await expect(page).toHaveURL(/\/editor/);
143+
// Wait for Editor page content (router updates DOM before URL)
144+
await expect(page.getByRole('heading', { name: 'Code Editor' })).toBeVisible();
139145
});
140146

141147
test('can logout from authenticated state', async ({ page }) => {
142-
// Logout button is in the header - must exist when authenticated
143-
const logoutButton = page.locator('button:has-text("Logout")').first();
148+
// Open user dropdown (contains the logout button)
149+
const userDropdown = page.locator('.user-dropdown-container button').first();
150+
await expect(userDropdown).toBeVisible();
151+
await userDropdown.click();
144152

153+
// Click logout button inside the dropdown
154+
const logoutButton = page.locator('button:has-text("Logout")').first();
145155
await expect(logoutButton).toBeVisible();
146156
await logoutButton.click();
147157
await expect(page).toHaveURL(/\/login/);

frontend/rollup.config.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ function startServer() {
3838

3939
const proxyAgent = new https.Agent({
4040
ca: fs.readFileSync(caPath),
41-
rejectUnauthorized: false // Accept self-signed certificates in development
41+
rejectUnauthorized: false, // Accept self-signed certificates in development
42+
keepAlive: true, // Reuse connections to avoid TLS handshake per request
43+
keepAliveMsecs: 1000
4244
});
4345

4446
server = https.createServer(httpsOptions, (req, res) => {
@@ -201,6 +203,7 @@ export default {
201203
})
202204
],
203205
watch: {
204-
clearScreen: false
206+
clearScreen: false,
207+
exclude: ['node_modules/**', 'public/build/**', 'test-results/**', 'playwright-report/**', 'e2e/**']
205208
}
206209
};

frontend/src/App.svelte

Lines changed: 63 additions & 164 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,29 @@
11
<script lang="ts">
2-
import type { Snippet } from 'svelte';
32
import { onMount, onDestroy } from 'svelte';
4-
import { Router, type Route } from "@mateothegreat/svelte5-router";
5-
import Home from "./routes/Home.svelte";
6-
import Login from "./routes/Login.svelte";
7-
import Register from "./routes/Register.svelte";
8-
import Editor from "./routes/Editor.svelte";
9-
import AdminEvents from "./routes/admin/AdminEvents.svelte";
10-
import AdminUsers from "./routes/admin/AdminUsers.svelte";
11-
import AdminSettings from "./routes/admin/AdminSettings.svelte";
12-
import AdminSagas from "./routes/admin/AdminSagas.svelte";
13-
import Settings from "./routes/Settings.svelte";
14-
import NotificationsPage from "./routes/Notifications.svelte";
15-
import Privacy from "./routes/Privacy.svelte";
3+
import { Router, type Route, goto } from "@mateothegreat/svelte5-router";
164
import Header from "./components/Header.svelte";
175
import Footer from "./components/Footer.svelte";
186
import ToastContainer from "./components/ToastContainer.svelte";
19-
import ProtectedRoute from "./components/ProtectedRoute.svelte";
207
import Spinner from "./components/Spinner.svelte";
218
import ErrorDisplay from "./components/ErrorDisplay.svelte";
229
import { theme } from './stores/theme';
23-
import { initializeAuth } from './lib/auth-init';
10+
import { initializeAuth, AuthInitializer } from './lib/auth-init';
2411
import { appError } from './stores/errorStore';
12+
import { isAuthenticated } from './stores/auth';
13+
import { get } from 'svelte/store';
14+
15+
// Page components
16+
import Home from "./routes/Home.svelte";
17+
import Login from "./routes/Login.svelte";
18+
import Register from "./routes/Register.svelte";
19+
import Privacy from "./routes/Privacy.svelte";
20+
import Editor from "./routes/Editor.svelte";
21+
import Settings from "./routes/Settings.svelte";
22+
import Notifications from "./routes/Notifications.svelte";
23+
import AdminEvents from "./routes/admin/AdminEvents.svelte";
24+
import AdminSagas from "./routes/admin/AdminSagas.svelte";
25+
import AdminUsers from "./routes/admin/AdminUsers.svelte";
26+
import AdminSettings from "./routes/admin/AdminSettings.svelte";
2527
2628
// Theme value derived from store with proper cleanup
2729
let themeValue = $state('auto');
@@ -50,159 +52,56 @@
5052
}
5153
});
5254
53-
// Routes for public pages (no auth required)
54-
const publicRoutes: Route[] = [
55-
{ path: "/", component: homeSnippet },
56-
{ path: "/login", component: loginSnippet },
57-
{ path: "/register", component: registerSnippet },
58-
{ path: "/privacy", component: privacySnippet },
59-
];
60-
61-
// Routes for protected pages (auth required)
62-
const protectedRoutes: Route[] = [
63-
{ path: "/editor", component: editorSnippet },
64-
{ path: "/settings", component: settingsSnippet },
65-
{ path: "/notifications", component: notificationsSnippet },
66-
{ path: "/admin/events", component: adminEventsSnippet },
67-
{ path: "/admin/sagas", component: adminSagasSnippet },
68-
{ path: "/admin/users", component: adminUsersSnippet },
69-
{ path: "/admin/settings", component: adminSettingsSnippet },
70-
{ path: "/admin", component: adminEventsSnippet },
55+
// Auth hook for protected routes
56+
const requireAuth = async () => {
57+
await AuthInitializer.waitForInit();
58+
if (!get(isAuthenticated)) {
59+
const currentPath = window.location.pathname + window.location.search;
60+
if (currentPath !== '/login' && currentPath !== '/register') {
61+
sessionStorage.setItem('redirectAfterLogin', currentPath);
62+
}
63+
goto('/login');
64+
return false;
65+
}
66+
return true;
67+
};
68+
69+
// Routes configuration
70+
const routes: Route[] = [
71+
// Public routes
72+
{ path: "/", component: Home },
73+
{ path: "/login", component: Login },
74+
{ path: "/register", component: Register },
75+
{ path: "/privacy", component: Privacy },
76+
// Protected routes
77+
{ path: "/editor", component: Editor, hooks: { pre: requireAuth } },
78+
{ path: "/settings", component: Settings, hooks: { pre: requireAuth } },
79+
{ path: "/notifications", component: Notifications, hooks: { pre: requireAuth } },
80+
{ path: "/admin/events", component: AdminEvents, hooks: { pre: requireAuth } },
81+
{ path: "/admin/sagas", component: AdminSagas, hooks: { pre: requireAuth } },
82+
{ path: "/admin/users", component: AdminUsers, hooks: { pre: requireAuth } },
83+
{ path: "/admin/settings", component: AdminSettings, hooks: { pre: requireAuth } },
84+
{ path: "/admin", component: AdminEvents, hooks: { pre: requireAuth } },
7185
];
72-
73-
const allRoutes: Route[] = [...publicRoutes, ...protectedRoutes];
7486
</script>
7587

76-
<!-- Layout wrapper snippet -->
77-
{#snippet layoutWrapper(content: Snippet, isProtected: boolean = false, isFullWidth: boolean = false)}
78-
{#if isProtected}
79-
<ProtectedRoute>
80-
<div class="flex flex-col min-h-screen bg-bg-default dark:bg-dark-bg-default pt-16">
81-
<Header/>
82-
<div class="flex-grow flex flex-col">
83-
<ToastContainer/>
84-
<main class={isFullWidth ? "flex-grow" : "flex-grow w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"}>
85-
{@render content()}
86-
</main>
87-
</div>
88-
<Footer/>
89-
</div>
90-
</ProtectedRoute>
91-
{:else}
92-
<div class="flex flex-col min-h-screen bg-bg-default dark:bg-dark-bg-default pt-16">
93-
<Header/>
94-
<div class="flex-grow flex flex-col">
95-
<ToastContainer/>
96-
<main class={isFullWidth ? "flex-grow" : "flex-grow w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"}>
97-
{@render content()}
98-
</main>
99-
</div>
100-
<Footer/>
101-
</div>
102-
{/if}
103-
{/snippet}
104-
105-
<!-- Public route snippets -->
106-
{#snippet homeSnippet()}
107-
{@render layoutWrapper(homeContent, false)}
108-
{/snippet}
109-
110-
{#snippet homeContent()}
111-
<Home/>
112-
{/snippet}
113-
114-
{#snippet loginSnippet()}
115-
{@render layoutWrapper(loginContent, false)}
116-
{/snippet}
117-
118-
{#snippet loginContent()}
119-
<Login/>
120-
{/snippet}
121-
122-
{#snippet registerSnippet()}
123-
{@render layoutWrapper(registerContent, false)}
124-
{/snippet}
125-
126-
{#snippet registerContent()}
127-
<Register/>
128-
{/snippet}
129-
130-
{#snippet privacySnippet()}
131-
{@render layoutWrapper(privacyContent, false)}
132-
{/snippet}
133-
134-
{#snippet privacyContent()}
135-
<Privacy/>
136-
{/snippet}
137-
138-
<!-- Protected route snippets -->
139-
{#snippet editorSnippet()}
140-
{@render layoutWrapper(editorContent, true)}
141-
{/snippet}
142-
143-
{#snippet editorContent()}
144-
<Editor/>
145-
{/snippet}
146-
147-
{#snippet settingsSnippet()}
148-
{@render layoutWrapper(settingsContent, true)}
149-
{/snippet}
150-
151-
{#snippet settingsContent()}
152-
<Settings/>
153-
{/snippet}
154-
155-
{#snippet notificationsSnippet()}
156-
{@render layoutWrapper(notificationsContent, true)}
157-
{/snippet}
158-
159-
{#snippet notificationsContent()}
160-
<NotificationsPage/>
161-
{/snippet}
162-
163-
<!-- Admin route snippets (full width) -->
164-
{#snippet adminEventsSnippet()}
165-
{@render layoutWrapper(adminEventsContent, true, true)}
166-
{/snippet}
167-
168-
{#snippet adminEventsContent()}
169-
<AdminEvents/>
170-
{/snippet}
171-
172-
{#snippet adminSagasSnippet()}
173-
{@render layoutWrapper(adminSagasContent, true, true)}
174-
{/snippet}
175-
176-
{#snippet adminSagasContent()}
177-
<AdminSagas/>
178-
{/snippet}
179-
180-
{#snippet adminUsersSnippet()}
181-
{@render layoutWrapper(adminUsersContent, true, true)}
182-
{/snippet}
183-
184-
{#snippet adminUsersContent()}
185-
<AdminUsers/>
186-
{/snippet}
187-
188-
{#snippet adminSettingsSnippet()}
189-
{@render layoutWrapper(adminSettingsContent, true, true)}
190-
{/snippet}
191-
192-
{#snippet adminSettingsContent()}
193-
<AdminSettings/>
194-
{/snippet}
195-
19688
{#if globalError}
19789
<ErrorDisplay error={globalError.error} title={globalError.title} />
198-
{:else if !authInitialized}
199-
<div class="flex items-center justify-center min-h-screen bg-bg-default dark:bg-dark-bg-default">
200-
<Spinner size="large" />
201-
</div>
20290
{:else}
203-
<Router base="/" routes={allRoutes} />
91+
<div class="flex flex-col min-h-screen bg-bg-default dark:bg-dark-bg-default pt-16">
92+
<Header/>
93+
<div class="flex-grow flex flex-col">
94+
<ToastContainer/>
95+
<main class="flex-grow">
96+
{#if !authInitialized}
97+
<div class="flex items-center justify-center min-h-[50vh]">
98+
<Spinner size="large" />
99+
</div>
100+
{:else}
101+
<Router base="/" {routes} />
102+
{/if}
103+
</main>
104+
</div>
105+
<Footer/>
106+
</div>
204107
{/if}
205-
206-
<style>
207-
/* Styles moved to Tailwind classes */
208-
</style>

frontend/src/routes/Login.svelte

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,12 @@
2929
error = null; // Clear previous error
3030
try {
3131
await login(username, password);
32-
33-
// Load and apply user settings (theme, etc)
34-
await loadUserSettings();
35-
32+
33+
// Load user settings in background (non-blocking)
34+
loadUserSettings().catch(err => console.warn('Failed to load user settings:', err));
35+
3636
addToast("Login successful! Welcome back.", "success");
37-
37+
3838
// Check if there's a saved redirect path
3939
const redirectPath = sessionStorage.getItem('redirectAfterLogin');
4040
if (redirectPath) {

0 commit comments

Comments
 (0)