Skip to content

Commit f355d54

Browse files
committed
fix: login - E2E tests and visual regression
1 parent b7db7cc commit f355d54

17 files changed

+658
-43
lines changed

e2e/fixtures.ts

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,45 @@
11
/* eslint-disable react-hooks/rules-of-hooks */
2-
import { expect, test as base } from "@playwright/test";
2+
import { expect, test as base, Page } from "@playwright/test";
33

4-
import { ConnectionPage, DashboardPage, ProjectPage } from "./pages";
4+
import { ConnectionPage, DashboardPage, LoginPage, ProjectPage } from "./pages";
5+
import { VisualTestHelpers } from "./utils/visual-helpers";
56

6-
const test = base.extend<{ connectionPage: ConnectionPage; dashboardPage: DashboardPage; projectPage: ProjectPage }>({
7-
dashboardPage: async ({ page }, use) => {
8-
await use(new DashboardPage(page));
7+
const test = base.extend<{
8+
connectionPage: ConnectionPage;
9+
dashboardPage: DashboardPage;
10+
loginPage: LoginPage;
11+
projectPage: ProjectPage;
12+
visualHelpers: typeof VisualTestHelpers;
13+
}>({
14+
// Override the page fixture to disable auth for login tests
15+
page: async ({ page }, use, testInfo) => {
16+
// For login tests, clear auth headers to force login page
17+
// This ensures we're not logged in when we need to see the login page
18+
const isLoginRelatedTest =
19+
testInfo.title.toLowerCase().includes("login") ||
20+
testInfo.file.includes("login") ||
21+
testInfo.file.includes("auth/") ||
22+
testInfo.title.toLowerCase().includes("auth");
23+
24+
if (isLoginRelatedTest) {
25+
await page.setExtraHTTPHeaders({});
26+
}
27+
await use(page);
928
},
10-
connectionPage: async ({ page }, use) => {
29+
connectionPage: async ({ page }: { page: Page }, use: (r: ConnectionPage) => Promise<void>) => {
1130
await use(new ConnectionPage(page));
1231
},
13-
projectPage: async ({ page }, use) => {
32+
dashboardPage: async ({ page }: { page: Page }, use: (r: DashboardPage) => Promise<void>) => {
33+
await use(new DashboardPage(page));
34+
},
35+
loginPage: async ({ page }: { page: Page }, use: (r: LoginPage) => Promise<void>) => {
36+
await use(new LoginPage(page));
37+
},
38+
projectPage: async ({ page }: { page: Page }, use: (r: ProjectPage) => Promise<void>) => {
1439
await use(new ProjectPage(page));
1540
},
41+
visualHelpers: async ({}, use: (r: typeof VisualTestHelpers) => Promise<void>) => {
42+
await use(VisualTestHelpers);
43+
},
1644
});
1745
export { expect, test };

e2e/global-setup.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { chromium, FullConfig } from "@playwright/test";
2+
3+
async function globalSetup(config: FullConfig) {
4+
// Set NODE_ENV for development mode
5+
process.env.NODE_ENV = "development";
6+
7+
// Optional: Start dev server if not already running
8+
console.log("🚀 Setting up development environment for Playwright tests");
9+
10+
return undefined;
11+
}
12+
13+
export default globalSetup;

e2e/global-teardown.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { FullConfig } from "@playwright/test";
2+
3+
async function globalTeardown(config: FullConfig) {
4+
console.log("🧹 Cleaning up after Playwright tests");
5+
6+
// Optional cleanup logic here
7+
return undefined;
8+
}
9+
10+
export default globalTeardown;

e2e/pages/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { ConnectionPage } from "./connection";
22
export { DashboardPage } from "./dashboard";
3+
export { LoginPage } from "./login";
34
export { ProjectPage } from "./project";

e2e/pages/login.ts

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import { expect, type Locator, type Page } from "@playwright/test";
2+
3+
export class LoginPage {
4+
readonly page: Page;
5+
6+
// Main elements
7+
private readonly logoText: Locator;
8+
private readonly pageTitle: Locator;
9+
private readonly pageSubtitle: Locator;
10+
private readonly authSection: Locator;
11+
12+
// OAuth provider buttons
13+
private readonly githubButton: Locator;
14+
private readonly googleButton: Locator;
15+
private readonly microsoftButton: Locator;
16+
17+
// Other elements
18+
private readonly loader: Locator;
19+
20+
// Error boundary elements
21+
private readonly errorBoundary: Locator;
22+
private readonly errorMessage: Locator;
23+
private readonly retryButton: Locator;
24+
25+
constructor(page: Page) {
26+
this.page = page;
27+
28+
// Main elements
29+
this.logoText = this.page.getByTestId("header-logo-text");
30+
this.pageTitle = this.page.getByTestId("welcome-title");
31+
this.pageSubtitle = this.page.getByTestId("welcome-subtitle");
32+
this.authSection = this.page.getByTestId("auth-section");
33+
34+
// OAuth provider buttons
35+
this.githubButton = this.page.getByTestId("oauth-button-github");
36+
this.googleButton = this.page.getByTestId("oauth-button-google");
37+
this.microsoftButton = this.page.getByTestId("oauth-button-microsoft");
38+
39+
this.loader = this.page.getByTestId("auth-loader");
40+
41+
// Error boundary elements
42+
this.errorBoundary = this.page.getByTestId("oauth-error-boundary");
43+
this.errorMessage = this.page.getByTestId("error-message");
44+
this.retryButton = this.page.getByTestId("retry-button");
45+
}
46+
47+
getByRole(role: Parameters<Page["getByRole"]>[0], options?: Parameters<Page["getByRole"]>[1]) {
48+
return this.page.getByRole(role, options);
49+
}
50+
51+
getByTestId(testId: string) {
52+
return this.page.getByTestId(testId);
53+
}
54+
55+
getByText(text: string) {
56+
return this.page.getByText(text);
57+
}
58+
59+
/**
60+
* Navigate to the login page
61+
* NOTE: Auth headers are automatically cleared for tests with 'login' in title/filename via fixtures.ts
62+
*/
63+
async goto() {
64+
// Clear any existing auth cookies/tokens to ensure we see the login page
65+
await this.page.context().clearCookies();
66+
await this.page.context().clearPermissions();
67+
68+
await this.page.goto("/");
69+
await this.page.waitForLoadState("domcontentloaded");
70+
71+
// Wait a bit for any redirects or authentication checks and login page to load
72+
await this.page.waitForTimeout(2000);
73+
}
74+
75+
/**
76+
* Wait for the login page to be fully loaded
77+
*/
78+
async waitForLoad() {
79+
// Wait for either the login page elements or check if we're on a different page
80+
try {
81+
await expect(this.logoText).toBeVisible({ timeout: 10000 });
82+
await expect(this.pageTitle).toBeVisible();
83+
} catch (error) {
84+
// If login elements aren't found, check what page we're actually on
85+
const url = await this.page.url();
86+
const title = await this.page.title();
87+
console.log(`Expected login page, but found URL: ${url}, Title: ${title}`);
88+
throw error;
89+
}
90+
}
91+
92+
/**
93+
* Verify all OAuth buttons are present and visible
94+
*/
95+
async verifyOAuthButtons() {
96+
await expect(this.githubButton).toBeVisible();
97+
await expect(this.googleButton).toBeVisible();
98+
await expect(this.microsoftButton).toBeVisible();
99+
}
100+
101+
/**
102+
* Click on a specific OAuth provider button
103+
*/
104+
async clickOAuthProvider(provider: "github" | "google" | "microsoft") {
105+
const buttonMap = {
106+
github: this.githubButton,
107+
google: this.googleButton,
108+
microsoft: this.microsoftButton,
109+
};
110+
111+
const button = buttonMap[provider];
112+
await expect(button).toBeVisible();
113+
await expect(button).toBeEnabled();
114+
await button.click();
115+
}
116+
117+
/**
118+
* Verify the page is in loading state
119+
*/
120+
async verifyLoadingState() {
121+
await expect(this.loader).toBeVisible();
122+
}
123+
124+
/**
125+
* Verify error boundary is displayed with error message
126+
*/
127+
async verifyErrorBoundary(expectedError?: string) {
128+
await expect(this.errorBoundary).toBeVisible();
129+
await expect(this.errorMessage).toBeVisible();
130+
131+
if (expectedError) {
132+
await expect(this.errorMessage).toContainText(expectedError);
133+
}
134+
}
135+
136+
/**
137+
* Click the retry button in error boundary
138+
*/
139+
async clickRetry() {
140+
await expect(this.retryButton).toBeVisible();
141+
await expect(this.retryButton).toBeEnabled();
142+
await this.retryButton.click();
143+
}
144+
145+
/**
146+
* Verify page accessibility and basic structure
147+
*/
148+
async verifyPageStructure() {
149+
// Check branding
150+
await expect(this.logoText).toBeVisible();
151+
152+
// Check main content
153+
await expect(this.pageTitle).toBeVisible();
154+
await expect(this.pageSubtitle).toBeVisible();
155+
await expect(this.authSection).toBeVisible();
156+
157+
// Check OAuth buttons are keyboard accessible
158+
await expect(this.githubButton).toBeVisible();
159+
await expect(this.googleButton).toBeVisible();
160+
await expect(this.microsoftButton).toBeVisible();
161+
162+
// Verify buttons are focusable
163+
await this.githubButton.focus();
164+
await expect(this.githubButton).toBeFocused();
165+
}
166+
167+
/**
168+
* Mock OAuth provider redirect for testing
169+
*/
170+
async mockOAuthRedirect(_provider: string, success: boolean = true) {
171+
if (success) {
172+
// Mock successful OAuth callback
173+
await this.page.route(`**/auth/callback**`, (route) => {
174+
route.fulfill({
175+
status: 200,
176+
body: JSON.stringify({
177+
ok: true,
178+
sessionToken: "mock-jwt-token",
179+
}),
180+
});
181+
});
182+
} else {
183+
// Mock failed OAuth callback
184+
await this.page.route(`**/auth/callback**`, (route) => {
185+
route.fulfill({
186+
status: 400,
187+
body: JSON.stringify({
188+
ok: false,
189+
error: "OAuth authentication failed",
190+
}),
191+
});
192+
});
193+
}
194+
}
195+
196+
/**
197+
* Get current URL for redirect validation
198+
*/
199+
async getCurrentUrl(): Promise<string> {
200+
return this.page.url();
201+
}
202+
203+
/**
204+
* Wait for navigation after OAuth button click
205+
*/
206+
async waitForNavigation() {
207+
await this.page.waitForNavigation();
208+
}
209+
}

0 commit comments

Comments
 (0)