|
| 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