diff --git a/frontend/playwright-report/data/0b9e37cd5ecf83154fb9b300824d32ab6910d760.png b/frontend/playwright-report/data/0b9e37cd5ecf83154fb9b300824d32ab6910d760.png new file mode 100644 index 00000000..98a7abd7 Binary files /dev/null and b/frontend/playwright-report/data/0b9e37cd5ecf83154fb9b300824d32ab6910d760.png differ diff --git a/frontend/playwright-report/data/11afb044f69e204fbe127fa1dbadf98b251de8bc.md b/frontend/playwright-report/data/11afb044f69e204fbe127fa1dbadf98b251de8bc.md new file mode 100644 index 00000000..b1518080 --- /dev/null +++ b/frontend/playwright-report/data/11afb044f69e204fbe127fa1dbadf98b251de8bc.md @@ -0,0 +1,224 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: 1-login.spec.ts >> Login Workflow >> should remember login preference on revisit +- Location: e2e/workflows/1-login.spec.ts:165:7 + +# Error details + +``` +Test timeout of 30000ms exceeded. +``` + +``` +Error: page.fill: Test timeout of 30000ms exceeded. +Call log: + - waiting for locator('[data-testid="local-username-input"]') + +``` + +# Page snapshot + +```yaml +- generic [active] [ref=e1]: + - generic [ref=e3]: + - generic [ref=e4]: + - img [ref=e6] + - heading "Identity Check" [level=2] [ref=e9] + - paragraph [ref=e10]: Select operator profile or use direct login + - generic [ref=e11]: + - button "newuser user" [ref=e12] [cursor=pointer]: + - generic [ref=e13]: + - img [ref=e15] + - generic [ref=e18]: + - paragraph [ref=e19]: newuser + - paragraph [ref=e20]: user + - img [ref=e21] + - generic [ref=e23]: + - button "Enterprise" [ref=e24] [cursor=pointer]: + - img [ref=e25] + - text: Enterprise + - button "Manual Login" [ref=e28] [cursor=pointer]: + - img [ref=e29] + - text: Manual Login + - button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]: + - img [ref=e38] + - alert [ref=e41] +``` + +# Test source + +```ts + 1 | import { Page, BrowserContext } from '@playwright/test'; + 2 | + 3 | /** + 4 | * Authentication fixture for E2E tests + 5 | * Handles login flows and session management + 6 | */ + 7 | + 8 | export interface LoginCredentials { + 9 | username: string; + 10 | password: string; + 11 | } + 12 | + 13 | export interface AuthSession { + 14 | token: string; + 15 | userId: string; + 16 | email: string; + 17 | isAdmin: boolean; + 18 | } + 19 | + 20 | /** + 21 | * Perform LDAP authentication + 22 | */ + 23 | export async function loginWithLdap( + 24 | page: Page, + 25 | credentials: LoginCredentials, + 26 | baseUrl: string = 'http://localhost' + 27 | ): Promise { + 28 | await page.goto(`${baseUrl}`); + 29 | + 30 | // Wait for login form to appear + 31 | await page.waitForSelector('[data-testid="login-form"]', { timeout: 5000 }); + 32 | + 33 | // Fill in username + 34 | await page.fill('[data-testid="username-input"]', credentials.username); + 35 | + 36 | // Fill in password + 37 | await page.fill('[data-testid="password-input"]', credentials.password); + 38 | + 39 | // Click login button + 40 | await page.click('[data-testid="login-submit"]'); + 41 | + 42 | // Wait for navigation to inventory page + 43 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' }); + 44 | } + 45 | + 46 | /** + 47 | * Perform local user authentication + 48 | */ + 49 | export async function loginWithLocalUser( + 50 | page: Page, + 51 | credentials: LoginCredentials, + 52 | baseUrl: string = 'http://localhost' + 53 | ): Promise { + 54 | await page.goto(`${baseUrl}`); + 55 | + 56 | // Wait for identity check overlay + 57 | await page.waitForSelector('[data-testid="identity-check-overlay"]', { timeout: 5000 }); + 58 | + 59 | // Click "Local User" tab if it exists + 60 | const localUserTab = page.locator('[data-testid="local-user-tab"]'); + 61 | if (await localUserTab.isVisible()) { + 62 | await localUserTab.click(); + 63 | } + 64 | + 65 | // Fill in username +> 66 | await page.fill('[data-testid="local-username-input"]', credentials.username); + | ^ Error: page.fill: Test timeout of 30000ms exceeded. + 67 | + 68 | // Fill in password + 69 | await page.fill('[data-testid="local-password-input"]', credentials.password); + 70 | + 71 | // Click login button + 72 | await page.click('[data-testid="local-login-submit"]'); + 73 | + 74 | // Wait for navigation to inventory page + 75 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' }); + 76 | } + 77 | + 78 | /** + 79 | * Logout from the application + 80 | */ + 81 | export async function logout(page: Page, baseUrl: string = 'http://localhost'): Promise { + 82 | // Click logout button or menu + 83 | const logoutButton = page.locator('[data-testid="logout-button"]'); + 84 | if (await logoutButton.isVisible()) { + 85 | await logoutButton.click(); + 86 | } else { + 87 | // Try finding it in menu + 88 | const menu = page.locator('[data-testid="user-menu"]'); + 89 | if (await menu.isVisible()) { + 90 | await menu.click(); + 91 | await page.click('[data-testid="logout-menu-item"]'); + 92 | } + 93 | } + 94 | + 95 | // Wait for redirect to login + 96 | await page.waitForURL(`${baseUrl}/*`, { timeout: 5000 }); + 97 | } + 98 | + 99 | /** + 100 | * Get authentication token from local storage + 101 | */ + 102 | export async function getAuthToken(page: Page): Promise { + 103 | const token = await page.evaluate(() => { + 104 | return localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token'); + 105 | }); + 106 | return token; + 107 | } + 108 | + 109 | /** + 110 | * Set authentication token in local storage + 111 | */ + 112 | export async function setAuthToken(page: Page, token: string): Promise { + 113 | await page.evaluate((t) => { + 114 | localStorage.setItem('auth_token', t); + 115 | }, token); + 116 | } + 117 | + 118 | /** + 119 | * Clear authentication data + 120 | */ + 121 | export async function clearAuth(page: Page): Promise { + 122 | await page.evaluate(() => { + 123 | localStorage.removeItem('auth_token'); + 124 | sessionStorage.removeItem('auth_token'); + 125 | localStorage.removeItem('user_data'); + 126 | sessionStorage.removeItem('user_data'); + 127 | }); + 128 | } + 129 | + 130 | /** + 131 | * Check if user is authenticated + 132 | */ + 133 | export async function isAuthenticated(page: Page): Promise { + 134 | const token = await getAuthToken(page); + 135 | return !!token; + 136 | } + 137 | + 138 | /** + 139 | * Get current user info from page + 140 | */ + 141 | export async function getCurrentUser(page: Page): Promise<{ username: string; email: string } | null> { + 142 | try { + 143 | const userData = await page.evaluate(() => { + 144 | const stored = localStorage.getItem('user_data') || sessionStorage.getItem('user_data'); + 145 | return stored ? JSON.parse(stored) : null; + 146 | }); + 147 | return userData; + 148 | } catch { + 149 | return null; + 150 | } + 151 | } + 152 | + 153 | /** + 154 | * Wait for authentication to complete + 155 | */ + 156 | export async function waitForAuth(page: Page, timeout: number = 5000): Promise { + 157 | // Wait for either: + 158 | // 1. Token to be in storage + 159 | // 2. Navigation to happen + 160 | // 3. Auth overlay to disappear + 161 | await page.waitForFunction( + 162 | () => { + 163 | const token = + 164 | typeof window !== 'undefined' + 165 | ? localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token') + 166 | : null; +``` \ No newline at end of file diff --git a/frontend/playwright-report/data/18de4149d79572f42f059e1c21d2a1b03b0e0a62.md b/frontend/playwright-report/data/18de4149d79572f42f059e1c21d2a1b03b0e0a62.md new file mode 100644 index 00000000..2a30e17a --- /dev/null +++ b/frontend/playwright-report/data/18de4149d79572f42f059e1c21d2a1b03b0e0a62.md @@ -0,0 +1,224 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: 1-login.spec.ts >> Login Workflow >> should support logout functionality +- Location: e2e/workflows/1-login.spec.ts:120:7 + +# Error details + +``` +Test timeout of 30000ms exceeded. +``` + +``` +Error: page.fill: Test timeout of 30000ms exceeded. +Call log: + - waiting for locator('[data-testid="local-username-input"]') + +``` + +# Page snapshot + +```yaml +- generic [active] [ref=e1]: + - generic [ref=e3]: + - generic [ref=e4]: + - img [ref=e6] + - heading "Identity Check" [level=2] [ref=e9] + - paragraph [ref=e10]: Select operator profile or use direct login + - generic [ref=e11]: + - button "newuser user" [ref=e12] [cursor=pointer]: + - generic [ref=e13]: + - img [ref=e15] + - generic [ref=e18]: + - paragraph [ref=e19]: newuser + - paragraph [ref=e20]: user + - img [ref=e21] + - generic [ref=e23]: + - button "Enterprise" [ref=e24] [cursor=pointer]: + - img [ref=e25] + - text: Enterprise + - button "Manual Login" [ref=e28] [cursor=pointer]: + - img [ref=e29] + - text: Manual Login + - button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]: + - img [ref=e38] + - alert [ref=e41] +``` + +# Test source + +```ts + 1 | import { Page, BrowserContext } from '@playwright/test'; + 2 | + 3 | /** + 4 | * Authentication fixture for E2E tests + 5 | * Handles login flows and session management + 6 | */ + 7 | + 8 | export interface LoginCredentials { + 9 | username: string; + 10 | password: string; + 11 | } + 12 | + 13 | export interface AuthSession { + 14 | token: string; + 15 | userId: string; + 16 | email: string; + 17 | isAdmin: boolean; + 18 | } + 19 | + 20 | /** + 21 | * Perform LDAP authentication + 22 | */ + 23 | export async function loginWithLdap( + 24 | page: Page, + 25 | credentials: LoginCredentials, + 26 | baseUrl: string = 'http://localhost' + 27 | ): Promise { + 28 | await page.goto(`${baseUrl}`); + 29 | + 30 | // Wait for login form to appear + 31 | await page.waitForSelector('[data-testid="login-form"]', { timeout: 5000 }); + 32 | + 33 | // Fill in username + 34 | await page.fill('[data-testid="username-input"]', credentials.username); + 35 | + 36 | // Fill in password + 37 | await page.fill('[data-testid="password-input"]', credentials.password); + 38 | + 39 | // Click login button + 40 | await page.click('[data-testid="login-submit"]'); + 41 | + 42 | // Wait for navigation to inventory page + 43 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' }); + 44 | } + 45 | + 46 | /** + 47 | * Perform local user authentication + 48 | */ + 49 | export async function loginWithLocalUser( + 50 | page: Page, + 51 | credentials: LoginCredentials, + 52 | baseUrl: string = 'http://localhost' + 53 | ): Promise { + 54 | await page.goto(`${baseUrl}`); + 55 | + 56 | // Wait for identity check overlay + 57 | await page.waitForSelector('[data-testid="identity-check-overlay"]', { timeout: 5000 }); + 58 | + 59 | // Click "Local User" tab if it exists + 60 | const localUserTab = page.locator('[data-testid="local-user-tab"]'); + 61 | if (await localUserTab.isVisible()) { + 62 | await localUserTab.click(); + 63 | } + 64 | + 65 | // Fill in username +> 66 | await page.fill('[data-testid="local-username-input"]', credentials.username); + | ^ Error: page.fill: Test timeout of 30000ms exceeded. + 67 | + 68 | // Fill in password + 69 | await page.fill('[data-testid="local-password-input"]', credentials.password); + 70 | + 71 | // Click login button + 72 | await page.click('[data-testid="local-login-submit"]'); + 73 | + 74 | // Wait for navigation to inventory page + 75 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' }); + 76 | } + 77 | + 78 | /** + 79 | * Logout from the application + 80 | */ + 81 | export async function logout(page: Page, baseUrl: string = 'http://localhost'): Promise { + 82 | // Click logout button or menu + 83 | const logoutButton = page.locator('[data-testid="logout-button"]'); + 84 | if (await logoutButton.isVisible()) { + 85 | await logoutButton.click(); + 86 | } else { + 87 | // Try finding it in menu + 88 | const menu = page.locator('[data-testid="user-menu"]'); + 89 | if (await menu.isVisible()) { + 90 | await menu.click(); + 91 | await page.click('[data-testid="logout-menu-item"]'); + 92 | } + 93 | } + 94 | + 95 | // Wait for redirect to login + 96 | await page.waitForURL(`${baseUrl}/*`, { timeout: 5000 }); + 97 | } + 98 | + 99 | /** + 100 | * Get authentication token from local storage + 101 | */ + 102 | export async function getAuthToken(page: Page): Promise { + 103 | const token = await page.evaluate(() => { + 104 | return localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token'); + 105 | }); + 106 | return token; + 107 | } + 108 | + 109 | /** + 110 | * Set authentication token in local storage + 111 | */ + 112 | export async function setAuthToken(page: Page, token: string): Promise { + 113 | await page.evaluate((t) => { + 114 | localStorage.setItem('auth_token', t); + 115 | }, token); + 116 | } + 117 | + 118 | /** + 119 | * Clear authentication data + 120 | */ + 121 | export async function clearAuth(page: Page): Promise { + 122 | await page.evaluate(() => { + 123 | localStorage.removeItem('auth_token'); + 124 | sessionStorage.removeItem('auth_token'); + 125 | localStorage.removeItem('user_data'); + 126 | sessionStorage.removeItem('user_data'); + 127 | }); + 128 | } + 129 | + 130 | /** + 131 | * Check if user is authenticated + 132 | */ + 133 | export async function isAuthenticated(page: Page): Promise { + 134 | const token = await getAuthToken(page); + 135 | return !!token; + 136 | } + 137 | + 138 | /** + 139 | * Get current user info from page + 140 | */ + 141 | export async function getCurrentUser(page: Page): Promise<{ username: string; email: string } | null> { + 142 | try { + 143 | const userData = await page.evaluate(() => { + 144 | const stored = localStorage.getItem('user_data') || sessionStorage.getItem('user_data'); + 145 | return stored ? JSON.parse(stored) : null; + 146 | }); + 147 | return userData; + 148 | } catch { + 149 | return null; + 150 | } + 151 | } + 152 | + 153 | /** + 154 | * Wait for authentication to complete + 155 | */ + 156 | export async function waitForAuth(page: Page, timeout: number = 5000): Promise { + 157 | // Wait for either: + 158 | // 1. Token to be in storage + 159 | // 2. Navigation to happen + 160 | // 3. Auth overlay to disappear + 161 | await page.waitForFunction( + 162 | () => { + 163 | const token = + 164 | typeof window !== 'undefined' + 165 | ? localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token') + 166 | : null; +``` \ No newline at end of file diff --git a/frontend/playwright-report/data/d85d52a987bf605d34ced01f09c8c6347b7c781b.md b/frontend/playwright-report/data/25c086a4cde2673fed46f9d88609959f3059fc36.md similarity index 88% rename from frontend/playwright-report/data/d85d52a987bf605d34ced01f09c8c6347b7c781b.md rename to frontend/playwright-report/data/25c086a4cde2673fed46f9d88609959f3059fc36.md index d3bd717f..6b2076cd 100644 --- a/frontend/playwright-report/data/d85d52a987bf605d34ced01f09c8c6347b7c781b.md +++ b/frontend/playwright-report/data/25c086a4cde2673fed46f9d88609959f3059fc36.md @@ -6,22 +6,19 @@ # Test info -- Name: 1-login.spec.ts >> Login Workflow >> should display identity check overlay on app load -- Location: e2e/workflows/1-login.spec.ts:16:7 +- Name: 1-login.spec.ts >> Login Workflow >> should reject empty login credentials +- Location: e2e/workflows/1-login.spec.ts:32:7 # Error details ``` -Error: expect(locator).toBeVisible() failed - -Locator: locator('[data-testid="ldap-login-tab"]') -Expected: visible -Timeout: 5000ms -Error: element(s) not found +Test timeout of 30000ms exceeded. +``` +``` +Error: locator.click: Test timeout of 30000ms exceeded. Call log: - - Expect "toBeVisible" with timeout 5000ms - - waiting for locator('[data-testid="ldap-login-tab"]') + - waiting for locator('[data-testid="login-submit"]') ``` @@ -80,8 +77,7 @@ Call log: 21 | const ldapTab = page.locator('[data-testid="ldap-login-tab"]'); 22 | const localTab = page.locator('[data-testid="local-login-tab"]'); 23 | -> 24 | await expect(ldapTab).toBeVisible(); - | ^ Error: expect(locator).toBeVisible() failed + 24 | await expect(ldapTab).toBeVisible(); 25 | await expect(localTab).toBeVisible(); 26 | }); 27 | @@ -91,7 +87,8 @@ Call log: 31 | 32 | test('should reject empty login credentials', async ({ page }) => { 33 | const submitButton = page.locator('[data-testid="login-submit"]'); - 34 | await submitButton.click(); +> 34 | await submitButton.click(); + | ^ Error: locator.click: Test timeout of 30000ms exceeded. 35 | 36 | // Should show validation error 37 | const errorMessage = page.locator('[data-testid="toast-error"]'); @@ -182,4 +179,14 @@ Call log: 122 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); 123 | await assertions.assertUserAuthenticated(page, BASE_URL); 124 | + 125 | // Logout + 126 | await auth.logout(page, BASE_URL); + 127 | + 128 | // Should be back on login page + 129 | const identityOverlay = page.locator('[data-testid="identity-check-overlay"]'); + 130 | await expect(identityOverlay).toBeVisible({ timeout: 5000 }); + 131 | + 132 | // Token should be cleared + 133 | const token = await auth.getAuthToken(page); + 134 | expect(token).toBeFalsy(); ``` \ No newline at end of file diff --git a/frontend/playwright-report/data/328efe50859839024d2ccf522575379c83e61131.md b/frontend/playwright-report/data/328efe50859839024d2ccf522575379c83e61131.md new file mode 100644 index 00000000..952ef37c --- /dev/null +++ b/frontend/playwright-report/data/328efe50859839024d2ccf522575379c83e61131.md @@ -0,0 +1,224 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: 1-login.spec.ts >> Login Workflow >> should successfully login with valid local user credentials +- Location: e2e/workflows/1-login.spec.ts:81:7 + +# Error details + +``` +Test timeout of 30000ms exceeded. +``` + +``` +Error: page.fill: Test timeout of 30000ms exceeded. +Call log: + - waiting for locator('[data-testid="local-username-input"]') + +``` + +# Page snapshot + +```yaml +- generic [active] [ref=e1]: + - generic [ref=e3]: + - generic [ref=e4]: + - img [ref=e6] + - heading "Identity Check" [level=2] [ref=e9] + - paragraph [ref=e10]: Select operator profile or use direct login + - generic [ref=e11]: + - button "newuser user" [ref=e12] [cursor=pointer]: + - generic [ref=e13]: + - img [ref=e15] + - generic [ref=e18]: + - paragraph [ref=e19]: newuser + - paragraph [ref=e20]: user + - img [ref=e21] + - generic [ref=e23]: + - button "Enterprise" [ref=e24] [cursor=pointer]: + - img [ref=e25] + - text: Enterprise + - button "Manual Login" [ref=e28] [cursor=pointer]: + - img [ref=e29] + - text: Manual Login + - button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]: + - img [ref=e38] + - alert [ref=e41] +``` + +# Test source + +```ts + 1 | import { Page, BrowserContext } from '@playwright/test'; + 2 | + 3 | /** + 4 | * Authentication fixture for E2E tests + 5 | * Handles login flows and session management + 6 | */ + 7 | + 8 | export interface LoginCredentials { + 9 | username: string; + 10 | password: string; + 11 | } + 12 | + 13 | export interface AuthSession { + 14 | token: string; + 15 | userId: string; + 16 | email: string; + 17 | isAdmin: boolean; + 18 | } + 19 | + 20 | /** + 21 | * Perform LDAP authentication + 22 | */ + 23 | export async function loginWithLdap( + 24 | page: Page, + 25 | credentials: LoginCredentials, + 26 | baseUrl: string = 'http://localhost' + 27 | ): Promise { + 28 | await page.goto(`${baseUrl}`); + 29 | + 30 | // Wait for login form to appear + 31 | await page.waitForSelector('[data-testid="login-form"]', { timeout: 5000 }); + 32 | + 33 | // Fill in username + 34 | await page.fill('[data-testid="username-input"]', credentials.username); + 35 | + 36 | // Fill in password + 37 | await page.fill('[data-testid="password-input"]', credentials.password); + 38 | + 39 | // Click login button + 40 | await page.click('[data-testid="login-submit"]'); + 41 | + 42 | // Wait for navigation to inventory page + 43 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' }); + 44 | } + 45 | + 46 | /** + 47 | * Perform local user authentication + 48 | */ + 49 | export async function loginWithLocalUser( + 50 | page: Page, + 51 | credentials: LoginCredentials, + 52 | baseUrl: string = 'http://localhost' + 53 | ): Promise { + 54 | await page.goto(`${baseUrl}`); + 55 | + 56 | // Wait for identity check overlay + 57 | await page.waitForSelector('[data-testid="identity-check-overlay"]', { timeout: 5000 }); + 58 | + 59 | // Click "Local User" tab if it exists + 60 | const localUserTab = page.locator('[data-testid="local-user-tab"]'); + 61 | if (await localUserTab.isVisible()) { + 62 | await localUserTab.click(); + 63 | } + 64 | + 65 | // Fill in username +> 66 | await page.fill('[data-testid="local-username-input"]', credentials.username); + | ^ Error: page.fill: Test timeout of 30000ms exceeded. + 67 | + 68 | // Fill in password + 69 | await page.fill('[data-testid="local-password-input"]', credentials.password); + 70 | + 71 | // Click login button + 72 | await page.click('[data-testid="local-login-submit"]'); + 73 | + 74 | // Wait for navigation to inventory page + 75 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' }); + 76 | } + 77 | + 78 | /** + 79 | * Logout from the application + 80 | */ + 81 | export async function logout(page: Page, baseUrl: string = 'http://localhost'): Promise { + 82 | // Click logout button or menu + 83 | const logoutButton = page.locator('[data-testid="logout-button"]'); + 84 | if (await logoutButton.isVisible()) { + 85 | await logoutButton.click(); + 86 | } else { + 87 | // Try finding it in menu + 88 | const menu = page.locator('[data-testid="user-menu"]'); + 89 | if (await menu.isVisible()) { + 90 | await menu.click(); + 91 | await page.click('[data-testid="logout-menu-item"]'); + 92 | } + 93 | } + 94 | + 95 | // Wait for redirect to login + 96 | await page.waitForURL(`${baseUrl}/*`, { timeout: 5000 }); + 97 | } + 98 | + 99 | /** + 100 | * Get authentication token from local storage + 101 | */ + 102 | export async function getAuthToken(page: Page): Promise { + 103 | const token = await page.evaluate(() => { + 104 | return localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token'); + 105 | }); + 106 | return token; + 107 | } + 108 | + 109 | /** + 110 | * Set authentication token in local storage + 111 | */ + 112 | export async function setAuthToken(page: Page, token: string): Promise { + 113 | await page.evaluate((t) => { + 114 | localStorage.setItem('auth_token', t); + 115 | }, token); + 116 | } + 117 | + 118 | /** + 119 | * Clear authentication data + 120 | */ + 121 | export async function clearAuth(page: Page): Promise { + 122 | await page.evaluate(() => { + 123 | localStorage.removeItem('auth_token'); + 124 | sessionStorage.removeItem('auth_token'); + 125 | localStorage.removeItem('user_data'); + 126 | sessionStorage.removeItem('user_data'); + 127 | }); + 128 | } + 129 | + 130 | /** + 131 | * Check if user is authenticated + 132 | */ + 133 | export async function isAuthenticated(page: Page): Promise { + 134 | const token = await getAuthToken(page); + 135 | return !!token; + 136 | } + 137 | + 138 | /** + 139 | * Get current user info from page + 140 | */ + 141 | export async function getCurrentUser(page: Page): Promise<{ username: string; email: string } | null> { + 142 | try { + 143 | const userData = await page.evaluate(() => { + 144 | const stored = localStorage.getItem('user_data') || sessionStorage.getItem('user_data'); + 145 | return stored ? JSON.parse(stored) : null; + 146 | }); + 147 | return userData; + 148 | } catch { + 149 | return null; + 150 | } + 151 | } + 152 | + 153 | /** + 154 | * Wait for authentication to complete + 155 | */ + 156 | export async function waitForAuth(page: Page, timeout: number = 5000): Promise { + 157 | // Wait for either: + 158 | // 1. Token to be in storage + 159 | // 2. Navigation to happen + 160 | // 3. Auth overlay to disappear + 161 | await page.waitForFunction( + 162 | () => { + 163 | const token = + 164 | typeof window !== 'undefined' + 165 | ? localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token') + 166 | : null; +``` \ No newline at end of file diff --git a/frontend/playwright-report/data/3bd536c2f96e6887e8dcdf903496bf540c20b16c.png b/frontend/playwright-report/data/3bd536c2f96e6887e8dcdf903496bf540c20b16c.png new file mode 100644 index 00000000..a0f367fa Binary files /dev/null and b/frontend/playwright-report/data/3bd536c2f96e6887e8dcdf903496bf540c20b16c.png differ diff --git a/frontend/playwright-report/data/acd4ce422581ae27636b69d2084b159f0becc1c5.png b/frontend/playwright-report/data/acd4ce422581ae27636b69d2084b159f0becc1c5.png new file mode 100644 index 00000000..9449d71f Binary files /dev/null and b/frontend/playwright-report/data/acd4ce422581ae27636b69d2084b159f0becc1c5.png differ diff --git a/frontend/playwright-report/data/b55707639177d53bca85afb8b9268b02973aeb9c.md b/frontend/playwright-report/data/b55707639177d53bca85afb8b9268b02973aeb9c.md new file mode 100644 index 00000000..d14eef79 --- /dev/null +++ b/frontend/playwright-report/data/b55707639177d53bca85afb8b9268b02973aeb9c.md @@ -0,0 +1,233 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: 1-login.spec.ts >> Login Workflow >> should reject invalid local user credentials +- Location: e2e/workflows/1-login.spec.ts:66:7 + +# Error details + +``` +Test timeout of 30000ms exceeded. +``` + +``` +Error: page.fill: Test timeout of 30000ms exceeded. +Call log: + - waiting for locator('[data-testid="local-username-input"]') + +``` + +# Page snapshot + +```yaml +- generic [ref=e1]: + - generic [ref=e3]: + - generic [ref=e4]: + - img [ref=e6] + - heading "Identity Check" [level=2] [ref=e9] + - paragraph [ref=e10]: Select operator profile or use direct login + - generic [ref=e12]: + - generic [ref=e13]: + - button [ref=e14] [cursor=pointer]: + - img [ref=e15] + - generic [ref=e18]: + - paragraph [ref=e19]: Logging in as + - paragraph [ref=e20]: Manual Input + - generic [ref=e21]: + - text: Username + - generic [ref=e22]: + - img [ref=e23] + - textbox "Admin" [ref=e26] + - generic [ref=e27]: + - text: Password + - generic [ref=e28]: + - img [ref=e29] + - textbox "Enter password" [active] [ref=e32] + - button "Verify Identity" [ref=e33] [cursor=pointer] + - button "Open Next.js Dev Tools" [ref=e39] [cursor=pointer]: + - img [ref=e40] + - alert [ref=e43] +``` + +# Test source + +```ts + 1 | import { test, expect } from '@playwright/test'; + 2 | import * as auth from '../fixtures/auth'; + 3 | import * as assertions from '../utils/assertions'; + 4 | import { LDAP_USERS, LOCAL_USERS } from '../fixtures/test-data'; + 5 | + 6 | test.describe('Login Workflow', () => { + 7 | const BASE_URL = process.env.BASE_URL || 'http://localhost:8917'; + 8 | + 9 | test.beforeEach(async ({ page }) => { + 10 | // Navigate to login page + 11 | await page.goto(BASE_URL); + 12 | // Wait for identity check overlay + 13 | await page.waitForSelector('[data-testid="identity-check-overlay"]', { timeout: 10000 }); + 14 | }); + 15 | + 16 | test('should display identity check overlay on app load', async ({ page }) => { + 17 | const overlay = page.locator('[data-testid="identity-check-overlay"]'); + 18 | await expect(overlay).toBeVisible(); + 19 | + 20 | // Verify tabs are available + 21 | const ldapTab = page.locator('[data-testid="ldap-login-tab"]'); + 22 | const localTab = page.locator('[data-testid="local-login-tab"]'); + 23 | + 24 | await expect(ldapTab).toBeVisible(); + 25 | await expect(localTab).toBeVisible(); + 26 | }); + 27 | + 28 | test('should display login form with username and password fields', async ({ page }) => { + 29 | await assertions.assertLoginFormVisible(page); + 30 | }); + 31 | + 32 | test('should reject empty login credentials', async ({ page }) => { + 33 | const submitButton = page.locator('[data-testid="login-submit"]'); + 34 | await submitButton.click(); + 35 | + 36 | // Should show validation error + 37 | const errorMessage = page.locator('[data-testid="toast-error"]'); + 38 | await expect(errorMessage).toBeVisible({ timeout: 3000 }); + 39 | }); + 40 | + 41 | test('should reject invalid LDAP credentials', async ({ page }) => { + 42 | await auth.loginWithLdap(page, LDAP_USERS.invalid, BASE_URL); + 43 | + 44 | // Should show error message + 45 | const errorToast = page.locator('[data-testid="toast-error"]'); + 46 | await expect(errorToast).toBeVisible({ timeout: 5000 }); + 47 | + 48 | // Should still be on login page + 49 | await expect(page).toHaveURL(BASE_URL); + 50 | }); + 51 | + 52 | test('should successfully login with valid LDAP credentials', async ({ page }) => { + 53 | // Skip if LDAP is not configured in test environment + 54 | test.skip(process.env.SKIP_LDAP === 'true', 'LDAP not available in test environment'); + 55 | + 56 | await auth.loginWithLdap(page, LDAP_USERS.valid, BASE_URL); + 57 | + 58 | // Verify we're authenticated + 59 | await assertions.assertUserAuthenticated(page, BASE_URL); + 60 | + 61 | // Verify token is stored + 62 | const token = await auth.getAuthToken(page); + 63 | expect(token).toBeTruthy(); + 64 | }); + 65 | + 66 | test('should reject invalid local user credentials', async ({ page }) => { + 67 | // Switch to local user tab + 68 | const localTab = page.locator('[data-testid="local-login-tab"]'); + 69 | await localTab.click(); + 70 | + 71 | // Try to login with invalid credentials +> 72 | await page.fill('[data-testid="local-username-input"]', 'invalid_user'); + | ^ Error: page.fill: Test timeout of 30000ms exceeded. + 73 | await page.fill('[data-testid="local-password-input"]', 'wrong_password'); + 74 | await page.click('[data-testid="local-login-submit"]'); + 75 | + 76 | // Should show error + 77 | const errorToast = page.locator('[data-testid="toast-error"]'); + 78 | await expect(errorToast).toBeVisible({ timeout: 5000 }); + 79 | }); + 80 | + 81 | test('should successfully login with valid local user credentials', async ({ page }) => { + 82 | // Switch to local user tab + 83 | const localTab = page.locator('[data-testid="local-login-tab"]'); + 84 | await localTab.click(); + 85 | + 86 | // Login with valid credentials + 87 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 88 | + 89 | // Verify authenticated + 90 | await assertions.assertUserAuthenticated(page, BASE_URL); + 91 | }); + 92 | + 93 | test('should show user identity after successful login', async ({ page }) => { + 94 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 95 | + 96 | // Check that user info is displayed + 97 | const userDisplay = page.locator('[data-testid="user-display"]'); + 98 | await expect(userDisplay).toBeVisible(); + 99 | + 100 | // Verify username is shown + 101 | const username = await userDisplay.textContent(); + 102 | expect(username).toContain(LOCAL_USERS.admin.username); + 103 | }); + 104 | + 105 | test('should maintain session across page reloads', async ({ page }) => { + 106 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 107 | await assertions.assertUserAuthenticated(page, BASE_URL); + 108 | + 109 | // Reload page + 110 | await page.reload(); + 111 | + 112 | // Should still be authenticated + 113 | const token = await auth.getAuthToken(page); + 114 | expect(token).toBeTruthy(); + 115 | + 116 | // Should not be on login page + 117 | await expect(page).not.toHaveURL(BASE_URL); + 118 | }); + 119 | + 120 | test('should support logout functionality', async ({ page }) => { + 121 | // Login first + 122 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 123 | await assertions.assertUserAuthenticated(page, BASE_URL); + 124 | + 125 | // Logout + 126 | await auth.logout(page, BASE_URL); + 127 | + 128 | // Should be back on login page + 129 | const identityOverlay = page.locator('[data-testid="identity-check-overlay"]'); + 130 | await expect(identityOverlay).toBeVisible({ timeout: 5000 }); + 131 | + 132 | // Token should be cleared + 133 | const token = await auth.getAuthToken(page); + 134 | expect(token).toBeFalsy(); + 135 | }); + 136 | + 137 | test('should show admin button for admin users', async ({ page }) => { + 138 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 139 | await assertions.assertUserAuthenticated(page, BASE_URL); + 140 | + 141 | // Admin button should be visible + 142 | const adminButton = page.locator('[data-testid="admin-button"]'); + 143 | await expect(adminButton).toBeVisible(); + 144 | }); + 145 | + 146 | test('should allow switching between LDAP and local login tabs', async ({ page }) => { + 147 | const ldapTab = page.locator('[data-testid="ldap-login-tab"]'); + 148 | const localTab = page.locator('[data-testid="local-login-tab"]'); + 149 | + 150 | // Start on LDAP tab + 151 | let ldapForm = page.locator('[data-testid="ldap-login-form"]'); + 152 | await expect(ldapForm).toBeVisible(); + 153 | + 154 | // Switch to local + 155 | await localTab.click(); + 156 | ldapForm = page.locator('[data-testid="ldap-login-form"]'); + 157 | await expect(ldapForm).not.toBeVisible(); + 158 | + 159 | // Switch back to LDAP + 160 | await ldapTab.click(); + 161 | ldapForm = page.locator('[data-testid="ldap-login-form"]'); + 162 | await expect(ldapForm).toBeVisible(); + 163 | }); + 164 | + 165 | test('should remember login preference on revisit', async ({ page, context }) => { + 166 | // Login with local user + 167 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 168 | + 169 | // Close and reopen browser + 170 | await page.close(); + 171 | const newPage = await context.newPage(); + 172 | await newPage.goto(BASE_URL); +``` \ No newline at end of file diff --git a/frontend/playwright-report/data/bd8f14932ab704c3c314dba27be5ca5a190c6e1e.md b/frontend/playwright-report/data/bd8f14932ab704c3c314dba27be5ca5a190c6e1e.md new file mode 100644 index 00000000..fe107665 --- /dev/null +++ b/frontend/playwright-report/data/bd8f14932ab704c3c314dba27be5ca5a190c6e1e.md @@ -0,0 +1,187 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: 1-login.spec.ts >> Login Workflow >> should reject invalid LDAP credentials +- Location: e2e/workflows/1-login.spec.ts:41:7 + +# Error details + +``` +TimeoutError: page.waitForSelector: Timeout 5000ms exceeded. +Call log: + - waiting for locator('[data-testid="login-form"]') to be visible + - waiting for" http://localhost:8917/login" navigation to finish... + - navigated to "http://localhost:8917/login" + +``` + +# Page snapshot + +```yaml +- generic [active] [ref=e1]: + - generic [ref=e3]: + - generic [ref=e4]: + - img [ref=e6] + - heading "Identity Check" [level=2] [ref=e9] + - paragraph [ref=e10]: Select operator profile or use direct login + - generic [ref=e11]: + - button "newuser user" [ref=e12] [cursor=pointer]: + - generic [ref=e13]: + - img [ref=e15] + - generic [ref=e18]: + - paragraph [ref=e19]: newuser + - paragraph [ref=e20]: user + - img [ref=e21] + - generic [ref=e23]: + - button "Enterprise" [ref=e24] [cursor=pointer]: + - img [ref=e25] + - text: Enterprise + - button "Manual Login" [ref=e28] [cursor=pointer]: + - img [ref=e29] + - text: Manual Login + - button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]: + - img [ref=e38] + - alert [ref=e41] +``` + +# Test source + +```ts + 1 | import { Page, BrowserContext } from '@playwright/test'; + 2 | + 3 | /** + 4 | * Authentication fixture for E2E tests + 5 | * Handles login flows and session management + 6 | */ + 7 | + 8 | export interface LoginCredentials { + 9 | username: string; + 10 | password: string; + 11 | } + 12 | + 13 | export interface AuthSession { + 14 | token: string; + 15 | userId: string; + 16 | email: string; + 17 | isAdmin: boolean; + 18 | } + 19 | + 20 | /** + 21 | * Perform LDAP authentication + 22 | */ + 23 | export async function loginWithLdap( + 24 | page: Page, + 25 | credentials: LoginCredentials, + 26 | baseUrl: string = 'http://localhost' + 27 | ): Promise { + 28 | await page.goto(`${baseUrl}`); + 29 | + 30 | // Wait for login form to appear +> 31 | await page.waitForSelector('[data-testid="login-form"]', { timeout: 5000 }); + | ^ TimeoutError: page.waitForSelector: Timeout 5000ms exceeded. + 32 | + 33 | // Fill in username + 34 | await page.fill('[data-testid="username-input"]', credentials.username); + 35 | + 36 | // Fill in password + 37 | await page.fill('[data-testid="password-input"]', credentials.password); + 38 | + 39 | // Click login button + 40 | await page.click('[data-testid="login-submit"]'); + 41 | + 42 | // Wait for navigation to inventory page + 43 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' }); + 44 | } + 45 | + 46 | /** + 47 | * Perform local user authentication + 48 | */ + 49 | export async function loginWithLocalUser( + 50 | page: Page, + 51 | credentials: LoginCredentials, + 52 | baseUrl: string = 'http://localhost' + 53 | ): Promise { + 54 | await page.goto(`${baseUrl}`); + 55 | + 56 | // Wait for identity check overlay + 57 | await page.waitForSelector('[data-testid="identity-check-overlay"]', { timeout: 5000 }); + 58 | + 59 | // Click "Local User" tab if it exists + 60 | const localUserTab = page.locator('[data-testid="local-user-tab"]'); + 61 | if (await localUserTab.isVisible()) { + 62 | await localUserTab.click(); + 63 | } + 64 | + 65 | // Fill in username + 66 | await page.fill('[data-testid="local-username-input"]', credentials.username); + 67 | + 68 | // Fill in password + 69 | await page.fill('[data-testid="local-password-input"]', credentials.password); + 70 | + 71 | // Click login button + 72 | await page.click('[data-testid="local-login-submit"]'); + 73 | + 74 | // Wait for navigation to inventory page + 75 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' }); + 76 | } + 77 | + 78 | /** + 79 | * Logout from the application + 80 | */ + 81 | export async function logout(page: Page, baseUrl: string = 'http://localhost'): Promise { + 82 | // Click logout button or menu + 83 | const logoutButton = page.locator('[data-testid="logout-button"]'); + 84 | if (await logoutButton.isVisible()) { + 85 | await logoutButton.click(); + 86 | } else { + 87 | // Try finding it in menu + 88 | const menu = page.locator('[data-testid="user-menu"]'); + 89 | if (await menu.isVisible()) { + 90 | await menu.click(); + 91 | await page.click('[data-testid="logout-menu-item"]'); + 92 | } + 93 | } + 94 | + 95 | // Wait for redirect to login + 96 | await page.waitForURL(`${baseUrl}/*`, { timeout: 5000 }); + 97 | } + 98 | + 99 | /** + 100 | * Get authentication token from local storage + 101 | */ + 102 | export async function getAuthToken(page: Page): Promise { + 103 | const token = await page.evaluate(() => { + 104 | return localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token'); + 105 | }); + 106 | return token; + 107 | } + 108 | + 109 | /** + 110 | * Set authentication token in local storage + 111 | */ + 112 | export async function setAuthToken(page: Page, token: string): Promise { + 113 | await page.evaluate((t) => { + 114 | localStorage.setItem('auth_token', t); + 115 | }, token); + 116 | } + 117 | + 118 | /** + 119 | * Clear authentication data + 120 | */ + 121 | export async function clearAuth(page: Page): Promise { + 122 | await page.evaluate(() => { + 123 | localStorage.removeItem('auth_token'); + 124 | sessionStorage.removeItem('auth_token'); + 125 | localStorage.removeItem('user_data'); + 126 | sessionStorage.removeItem('user_data'); + 127 | }); + 128 | } + 129 | + 130 | /** + 131 | * Check if user is authenticated +``` \ No newline at end of file diff --git a/frontend/playwright-report/data/c49d3316f329d7283ba0d5b666781aac30b68caa.md b/frontend/playwright-report/data/c49d3316f329d7283ba0d5b666781aac30b68caa.md new file mode 100644 index 00000000..abff9b1c --- /dev/null +++ b/frontend/playwright-report/data/c49d3316f329d7283ba0d5b666781aac30b68caa.md @@ -0,0 +1,213 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: 1-login.spec.ts >> Login Workflow >> should display login form with username and password fields +- Location: e2e/workflows/1-login.spec.ts:28:7 + +# Error details + +``` +Error: expect(locator).toBeVisible() failed + +Locator: locator('[data-testid="login-form"]') +Expected: visible +Timeout: 5000ms +Error: element(s) not found + +Call log: + - Expect "toBeVisible" with timeout 5000ms + - waiting for locator('[data-testid="login-form"]') + +``` + +# Page snapshot + +```yaml +- generic [active] [ref=e1]: + - generic [ref=e3]: + - generic [ref=e4]: + - img [ref=e6] + - heading "Identity Check" [level=2] [ref=e9] + - paragraph [ref=e10]: Select operator profile or use direct login + - generic [ref=e11]: + - button "newuser user" [ref=e12] [cursor=pointer]: + - generic [ref=e13]: + - img [ref=e15] + - generic [ref=e18]: + - paragraph [ref=e19]: newuser + - paragraph [ref=e20]: user + - img [ref=e21] + - generic [ref=e23]: + - button "Enterprise" [ref=e24] [cursor=pointer]: + - img [ref=e25] + - text: Enterprise + - button "Manual Login" [ref=e28] [cursor=pointer]: + - img [ref=e29] + - text: Manual Login + - button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]: + - img [ref=e38] + - alert [ref=e41] +``` + +# Test source + +```ts + 1 | import { expect, Page, Locator } from '@playwright/test'; + 2 | + 3 | /** + 4 | * Custom Playwright assertions for E2E tests + 5 | * Provides domain-specific matchers for inventory operations + 6 | */ + 7 | + 8 | /** + 9 | * Assert that an inventory item is visible with expected data + 10 | */ + 11 | export async function assertItemVisible( + 12 | page: Page, + 13 | itemName: string, + 14 | expectedData?: { quantity?: number; category?: string } + 15 | ): Promise { + 16 | const itemRow = page.locator(`[data-testid="item-row"][data-item-name="${itemName}"]`); + 17 | await expect(itemRow).toBeVisible(); + 18 | + 19 | if (expectedData?.quantity !== undefined) { + 20 | const quantityCell = itemRow.locator('[data-testid="quantity-cell"]'); + 21 | await expect(quantityCell).toContainText(expectedData.quantity.toString()); + 22 | } + 23 | + 24 | if (expectedData?.category !== undefined) { + 25 | const categoryCell = itemRow.locator('[data-testid="category-cell"]'); + 26 | await expect(categoryCell).toContainText(expectedData.category); + 27 | } + 28 | } + 29 | + 30 | /** + 31 | * Assert that a barcode scan was successful + 32 | */ + 33 | export async function assertScanSuccess( + 34 | page: Page, + 35 | expectedItemName: string, + 36 | expectedQuantityChange: number + 37 | ): Promise { + 38 | // Check for success toast notification + 39 | const successToast = page.locator('[data-testid="toast-success"]'); + 40 | await expect(successToast).toBeVisible({ timeout: 3000 }); + 41 | + 42 | // Verify item quantity was updated + 43 | const itemRow = page.locator(`[data-testid="item-row"][data-item-name="${expectedItemName}"]`); + 44 | await expect(itemRow).toBeVisible(); + 45 | } + 46 | + 47 | /** + 48 | * Assert that login form is displayed + 49 | */ + 50 | export async function assertLoginFormVisible(page: Page): Promise { + 51 | const loginForm = page.locator('[data-testid="login-form"]'); +> 52 | await expect(loginForm).toBeVisible(); + | ^ Error: expect(locator).toBeVisible() failed + 53 | + 54 | const usernameInput = page.locator('[data-testid="username-input"]'); + 55 | const passwordInput = page.locator('[data-testid="password-input"]'); + 56 | const submitButton = page.locator('[data-testid="login-submit"]'); + 57 | + 58 | await expect(usernameInput).toBeVisible(); + 59 | await expect(passwordInput).toBeVisible(); + 60 | await expect(submitButton).toBeVisible(); + 61 | } + 62 | + 63 | /** + 64 | * Assert that user is authenticated (on inventory page) + 65 | */ + 66 | export async function assertUserAuthenticated(page: Page, baseUrl: string = 'http://localhost'): Promise { + 67 | // Check URL is on an authenticated page + 68 | await expect(page).toHaveURL(/inventory|dashboard|scanner/); + 69 | + 70 | // Verify logout button is visible + 71 | const logoutButton = page.locator('[data-testid="logout-button"]'); + 72 | await expect(logoutButton).toBeVisible(); + 73 | } + 74 | + 75 | /** + 76 | * Assert that admin dashboard is visible + 77 | */ + 78 | export async function assertAdminDashboardVisible(page: Page): Promise { + 79 | const adminOverlay = page.locator('[data-testid="admin-overlay"]'); + 80 | await expect(adminOverlay).toBeVisible(); + 81 | + 82 | // Check for tabs + 83 | const identityTab = page.locator('[data-testid="admin-tab-identity"]'); + 84 | const databaseTab = page.locator('[data-testid="admin-tab-database"]'); + 85 | + 86 | await expect(identityTab).toBeVisible(); + 87 | await expect(databaseTab).toBeVisible(); + 88 | } + 89 | + 90 | /** + 91 | * Assert that stock adjustment form is visible + 92 | */ + 93 | export async function assertStockAdjustmentVisible(page: Page, itemName: string): Promise { + 94 | const adjustmentForm = page.locator('[data-testid="stock-adjustment-form"]'); + 95 | await expect(adjustmentForm).toBeVisible(); + 96 | + 97 | const itemNameDisplay = page.locator(`[data-testid="adjustment-item-name"]`); + 98 | await expect(itemNameDisplay).toContainText(itemName); + 99 | + 100 | const quantityInput = page.locator('[data-testid="adjustment-quantity-input"]'); + 101 | await expect(quantityInput).toBeVisible(); + 102 | } + 103 | + 104 | /** + 105 | * Assert that AI extraction is in progress + 106 | */ + 107 | export async function assertAiExtractionInProgress(page: Page): Promise { + 108 | const extractionOverlay = page.locator('[data-testid="ai-extraction-overlay"]'); + 109 | await expect(extractionOverlay).toBeVisible(); + 110 | + 111 | const loadingSpinner = page.locator('[data-testid="extraction-loading"]'); + 112 | await expect(loadingSpinner).toBeVisible(); + 113 | } + 114 | + 115 | /** + 116 | * Assert that AI extraction results are displayed + 117 | */ + 118 | export async function assertAiExtractionResults( + 119 | page: Page, + 120 | expectedFields: { name?: string; partNumber?: string; quantity?: string } + 121 | ): Promise { + 122 | const resultsForm = page.locator('[data-testid="extraction-results-form"]'); + 123 | await expect(resultsForm).toBeVisible(); + 124 | + 125 | if (expectedFields.name) { + 126 | const nameInput = resultsForm.locator('[data-testid="extracted-name"]'); + 127 | await expect(nameInput).toHaveValue(expectedFields.name); + 128 | } + 129 | + 130 | if (expectedFields.partNumber) { + 131 | const pnInput = resultsForm.locator('[data-testid="extracted-part-number"]'); + 132 | await expect(pnInput).toHaveValue(expectedFields.partNumber); + 133 | } + 134 | + 135 | if (expectedFields.quantity) { + 136 | const quantityInput = resultsForm.locator('[data-testid="extracted-quantity"]'); + 137 | await expect(quantityInput).toHaveValue(expectedFields.quantity); + 138 | } + 139 | } + 140 | + 141 | /** + 142 | * Assert that offline sync is pending + 143 | */ + 144 | export async function assertOfflineSyncPending(page: Page): Promise { + 145 | const syncIndicator = page.locator('[data-testid="offline-sync-indicator"]'); + 146 | await expect(syncIndicator).toBeVisible(); + 147 | + 148 | const pendingBadge = page.locator('[data-testid="sync-pending-badge"]'); + 149 | await expect(pendingBadge).toBeVisible(); + 150 | } + 151 | + 152 | /** +``` \ No newline at end of file diff --git a/frontend/playwright-report/data/c598dfc727318850f1aaaa533df9f623f04844a4.md b/frontend/playwright-report/data/c598dfc727318850f1aaaa533df9f623f04844a4.md new file mode 100644 index 00000000..654868e4 --- /dev/null +++ b/frontend/playwright-report/data/c598dfc727318850f1aaaa533df9f623f04844a4.md @@ -0,0 +1,238 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: 1-login.spec.ts >> Login Workflow >> should allow switching between LDAP and local login tabs +- Location: e2e/workflows/1-login.spec.ts:146:7 + +# Error details + +``` +Error: expect(locator).toBeVisible() failed + +Locator: locator('[data-testid="ldap-login-form"]') +Expected: visible +Timeout: 5000ms +Error: element(s) not found + +Call log: + - Expect "toBeVisible" with timeout 5000ms + - waiting for locator('[data-testid="ldap-login-form"]') + +``` + +# Page snapshot + +```yaml +- generic [active] [ref=e1]: + - generic [ref=e3]: + - generic [ref=e4]: + - img [ref=e6] + - heading "Identity Check" [level=2] [ref=e9] + - paragraph [ref=e10]: Select operator profile or use direct login + - generic [ref=e11]: + - button "newuser user" [ref=e12] [cursor=pointer]: + - generic [ref=e13]: + - img [ref=e15] + - generic [ref=e18]: + - paragraph [ref=e19]: newuser + - paragraph [ref=e20]: user + - img [ref=e21] + - generic [ref=e23]: + - button "Enterprise" [ref=e24] [cursor=pointer]: + - img [ref=e25] + - text: Enterprise + - button "Manual Login" [ref=e28] [cursor=pointer]: + - img [ref=e29] + - text: Manual Login + - button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]: + - img [ref=e38] + - alert [ref=e41] +``` + +# Test source + +```ts + 52 | test('should successfully login with valid LDAP credentials', async ({ page }) => { + 53 | // Skip if LDAP is not configured in test environment + 54 | test.skip(process.env.SKIP_LDAP === 'true', 'LDAP not available in test environment'); + 55 | + 56 | await auth.loginWithLdap(page, LDAP_USERS.valid, BASE_URL); + 57 | + 58 | // Verify we're authenticated + 59 | await assertions.assertUserAuthenticated(page, BASE_URL); + 60 | + 61 | // Verify token is stored + 62 | const token = await auth.getAuthToken(page); + 63 | expect(token).toBeTruthy(); + 64 | }); + 65 | + 66 | test('should reject invalid local user credentials', async ({ page }) => { + 67 | // Switch to local user tab + 68 | const localTab = page.locator('[data-testid="local-login-tab"]'); + 69 | await localTab.click(); + 70 | + 71 | // Try to login with invalid credentials + 72 | await page.fill('[data-testid="local-username-input"]', 'invalid_user'); + 73 | await page.fill('[data-testid="local-password-input"]', 'wrong_password'); + 74 | await page.click('[data-testid="local-login-submit"]'); + 75 | + 76 | // Should show error + 77 | const errorToast = page.locator('[data-testid="toast-error"]'); + 78 | await expect(errorToast).toBeVisible({ timeout: 5000 }); + 79 | }); + 80 | + 81 | test('should successfully login with valid local user credentials', async ({ page }) => { + 82 | // Switch to local user tab + 83 | const localTab = page.locator('[data-testid="local-login-tab"]'); + 84 | await localTab.click(); + 85 | + 86 | // Login with valid credentials + 87 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 88 | + 89 | // Verify authenticated + 90 | await assertions.assertUserAuthenticated(page, BASE_URL); + 91 | }); + 92 | + 93 | test('should show user identity after successful login', async ({ page }) => { + 94 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 95 | + 96 | // Check that user info is displayed + 97 | const userDisplay = page.locator('[data-testid="user-display"]'); + 98 | await expect(userDisplay).toBeVisible(); + 99 | + 100 | // Verify username is shown + 101 | const username = await userDisplay.textContent(); + 102 | expect(username).toContain(LOCAL_USERS.admin.username); + 103 | }); + 104 | + 105 | test('should maintain session across page reloads', async ({ page }) => { + 106 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 107 | await assertions.assertUserAuthenticated(page, BASE_URL); + 108 | + 109 | // Reload page + 110 | await page.reload(); + 111 | + 112 | // Should still be authenticated + 113 | const token = await auth.getAuthToken(page); + 114 | expect(token).toBeTruthy(); + 115 | + 116 | // Should not be on login page + 117 | await expect(page).not.toHaveURL(BASE_URL); + 118 | }); + 119 | + 120 | test('should support logout functionality', async ({ page }) => { + 121 | // Login first + 122 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 123 | await assertions.assertUserAuthenticated(page, BASE_URL); + 124 | + 125 | // Logout + 126 | await auth.logout(page, BASE_URL); + 127 | + 128 | // Should be back on login page + 129 | const identityOverlay = page.locator('[data-testid="identity-check-overlay"]'); + 130 | await expect(identityOverlay).toBeVisible({ timeout: 5000 }); + 131 | + 132 | // Token should be cleared + 133 | const token = await auth.getAuthToken(page); + 134 | expect(token).toBeFalsy(); + 135 | }); + 136 | + 137 | test('should show admin button for admin users', async ({ page }) => { + 138 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 139 | await assertions.assertUserAuthenticated(page, BASE_URL); + 140 | + 141 | // Admin button should be visible + 142 | const adminButton = page.locator('[data-testid="admin-button"]'); + 143 | await expect(adminButton).toBeVisible(); + 144 | }); + 145 | + 146 | test('should allow switching between LDAP and local login tabs', async ({ page }) => { + 147 | const ldapTab = page.locator('[data-testid="ldap-login-tab"]'); + 148 | const localTab = page.locator('[data-testid="local-login-tab"]'); + 149 | + 150 | // Start on LDAP tab + 151 | let ldapForm = page.locator('[data-testid="ldap-login-form"]'); +> 152 | await expect(ldapForm).toBeVisible(); + | ^ Error: expect(locator).toBeVisible() failed + 153 | + 154 | // Switch to local + 155 | await localTab.click(); + 156 | ldapForm = page.locator('[data-testid="ldap-login-form"]'); + 157 | await expect(ldapForm).not.toBeVisible(); + 158 | + 159 | // Switch back to LDAP + 160 | await ldapTab.click(); + 161 | ldapForm = page.locator('[data-testid="ldap-login-form"]'); + 162 | await expect(ldapForm).toBeVisible(); + 163 | }); + 164 | + 165 | test('should remember login preference on revisit', async ({ page, context }) => { + 166 | // Login with local user + 167 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 168 | + 169 | // Close and reopen browser + 170 | await page.close(); + 171 | const newPage = await context.newPage(); + 172 | await newPage.goto(BASE_URL); + 173 | + 174 | // Should still be authenticated + 175 | const authenticated = await auth.isAuthenticated(newPage); + 176 | expect(authenticated).toBe(true); + 177 | + 178 | await newPage.close(); + 179 | }); + 180 | + 181 | test('should handle network errors gracefully', async ({ page }) => { + 182 | // Enable offline mode + 183 | await page.context().setOffline(true); + 184 | + 185 | // Try to login + 186 | const usernameInput = page.locator('[data-testid="username-input"]'); + 187 | const passwordInput = page.locator('[data-testid="password-input"]'); + 188 | const submitButton = page.locator('[data-testid="login-submit"]'); + 189 | + 190 | await usernameInput.fill(LOCAL_USERS.admin.username); + 191 | await passwordInput.fill(LOCAL_USERS.admin.password); + 192 | await submitButton.click(); + 193 | + 194 | // Should show network error + 195 | const errorToast = page.locator('[data-testid="toast-error"]'); + 196 | await expect(errorToast).toBeVisible({ timeout: 5000 }); + 197 | + 198 | // Re-enable network + 199 | await page.context().setOffline(false); + 200 | }); + 201 | + 202 | test('should display user list in local login tab', async ({ page }) => { + 203 | const localTab = page.locator('[data-testid="local-login-tab"]'); + 204 | await localTab.click(); + 205 | + 206 | // User list should be visible + 207 | const userList = page.locator('[data-testid="user-list"]'); + 208 | await expect(userList).toBeVisible(); + 209 | + 210 | // Should have at least one user + 211 | const userItems = page.locator('[data-testid="user-list-item"]'); + 212 | expect(await userItems.count()).toBeGreaterThan(0); + 213 | }); + 214 | + 215 | test('should auto-login when clicking user from list', async ({ page }) => { + 216 | const localTab = page.locator('[data-testid="local-login-tab"]'); + 217 | await localTab.click(); + 218 | + 219 | // Click first user in list + 220 | const firstUser = page.locator('[data-testid="user-list-item"]').first(); + 221 | await firstUser.click(); + 222 | + 223 | // Password input should be focused + 224 | const passwordInput = page.locator('[data-testid="local-password-input"]'); + 225 | await expect(passwordInput).toBeFocused(); + 226 | }); + 227 | }); + 228 | +``` \ No newline at end of file diff --git a/frontend/playwright-report/data/ce82bdf77eafd95f45c92adac93d33895723c22a.md b/frontend/playwright-report/data/ce82bdf77eafd95f45c92adac93d33895723c22a.md new file mode 100644 index 00000000..ec531fc2 --- /dev/null +++ b/frontend/playwright-report/data/ce82bdf77eafd95f45c92adac93d33895723c22a.md @@ -0,0 +1,224 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: 1-login.spec.ts >> Login Workflow >> should show user identity after successful login +- Location: e2e/workflows/1-login.spec.ts:93:7 + +# Error details + +``` +Test timeout of 30000ms exceeded. +``` + +``` +Error: page.fill: Test timeout of 30000ms exceeded. +Call log: + - waiting for locator('[data-testid="local-username-input"]') + +``` + +# Page snapshot + +```yaml +- generic [active] [ref=e1]: + - generic [ref=e3]: + - generic [ref=e4]: + - img [ref=e6] + - heading "Identity Check" [level=2] [ref=e9] + - paragraph [ref=e10]: Select operator profile or use direct login + - generic [ref=e11]: + - button "newuser user" [ref=e12] [cursor=pointer]: + - generic [ref=e13]: + - img [ref=e15] + - generic [ref=e18]: + - paragraph [ref=e19]: newuser + - paragraph [ref=e20]: user + - img [ref=e21] + - generic [ref=e23]: + - button "Enterprise" [ref=e24] [cursor=pointer]: + - img [ref=e25] + - text: Enterprise + - button "Manual Login" [ref=e28] [cursor=pointer]: + - img [ref=e29] + - text: Manual Login + - button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]: + - img [ref=e38] + - alert [ref=e41] +``` + +# Test source + +```ts + 1 | import { Page, BrowserContext } from '@playwright/test'; + 2 | + 3 | /** + 4 | * Authentication fixture for E2E tests + 5 | * Handles login flows and session management + 6 | */ + 7 | + 8 | export interface LoginCredentials { + 9 | username: string; + 10 | password: string; + 11 | } + 12 | + 13 | export interface AuthSession { + 14 | token: string; + 15 | userId: string; + 16 | email: string; + 17 | isAdmin: boolean; + 18 | } + 19 | + 20 | /** + 21 | * Perform LDAP authentication + 22 | */ + 23 | export async function loginWithLdap( + 24 | page: Page, + 25 | credentials: LoginCredentials, + 26 | baseUrl: string = 'http://localhost' + 27 | ): Promise { + 28 | await page.goto(`${baseUrl}`); + 29 | + 30 | // Wait for login form to appear + 31 | await page.waitForSelector('[data-testid="login-form"]', { timeout: 5000 }); + 32 | + 33 | // Fill in username + 34 | await page.fill('[data-testid="username-input"]', credentials.username); + 35 | + 36 | // Fill in password + 37 | await page.fill('[data-testid="password-input"]', credentials.password); + 38 | + 39 | // Click login button + 40 | await page.click('[data-testid="login-submit"]'); + 41 | + 42 | // Wait for navigation to inventory page + 43 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' }); + 44 | } + 45 | + 46 | /** + 47 | * Perform local user authentication + 48 | */ + 49 | export async function loginWithLocalUser( + 50 | page: Page, + 51 | credentials: LoginCredentials, + 52 | baseUrl: string = 'http://localhost' + 53 | ): Promise { + 54 | await page.goto(`${baseUrl}`); + 55 | + 56 | // Wait for identity check overlay + 57 | await page.waitForSelector('[data-testid="identity-check-overlay"]', { timeout: 5000 }); + 58 | + 59 | // Click "Local User" tab if it exists + 60 | const localUserTab = page.locator('[data-testid="local-user-tab"]'); + 61 | if (await localUserTab.isVisible()) { + 62 | await localUserTab.click(); + 63 | } + 64 | + 65 | // Fill in username +> 66 | await page.fill('[data-testid="local-username-input"]', credentials.username); + | ^ Error: page.fill: Test timeout of 30000ms exceeded. + 67 | + 68 | // Fill in password + 69 | await page.fill('[data-testid="local-password-input"]', credentials.password); + 70 | + 71 | // Click login button + 72 | await page.click('[data-testid="local-login-submit"]'); + 73 | + 74 | // Wait for navigation to inventory page + 75 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' }); + 76 | } + 77 | + 78 | /** + 79 | * Logout from the application + 80 | */ + 81 | export async function logout(page: Page, baseUrl: string = 'http://localhost'): Promise { + 82 | // Click logout button or menu + 83 | const logoutButton = page.locator('[data-testid="logout-button"]'); + 84 | if (await logoutButton.isVisible()) { + 85 | await logoutButton.click(); + 86 | } else { + 87 | // Try finding it in menu + 88 | const menu = page.locator('[data-testid="user-menu"]'); + 89 | if (await menu.isVisible()) { + 90 | await menu.click(); + 91 | await page.click('[data-testid="logout-menu-item"]'); + 92 | } + 93 | } + 94 | + 95 | // Wait for redirect to login + 96 | await page.waitForURL(`${baseUrl}/*`, { timeout: 5000 }); + 97 | } + 98 | + 99 | /** + 100 | * Get authentication token from local storage + 101 | */ + 102 | export async function getAuthToken(page: Page): Promise { + 103 | const token = await page.evaluate(() => { + 104 | return localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token'); + 105 | }); + 106 | return token; + 107 | } + 108 | + 109 | /** + 110 | * Set authentication token in local storage + 111 | */ + 112 | export async function setAuthToken(page: Page, token: string): Promise { + 113 | await page.evaluate((t) => { + 114 | localStorage.setItem('auth_token', t); + 115 | }, token); + 116 | } + 117 | + 118 | /** + 119 | * Clear authentication data + 120 | */ + 121 | export async function clearAuth(page: Page): Promise { + 122 | await page.evaluate(() => { + 123 | localStorage.removeItem('auth_token'); + 124 | sessionStorage.removeItem('auth_token'); + 125 | localStorage.removeItem('user_data'); + 126 | sessionStorage.removeItem('user_data'); + 127 | }); + 128 | } + 129 | + 130 | /** + 131 | * Check if user is authenticated + 132 | */ + 133 | export async function isAuthenticated(page: Page): Promise { + 134 | const token = await getAuthToken(page); + 135 | return !!token; + 136 | } + 137 | + 138 | /** + 139 | * Get current user info from page + 140 | */ + 141 | export async function getCurrentUser(page: Page): Promise<{ username: string; email: string } | null> { + 142 | try { + 143 | const userData = await page.evaluate(() => { + 144 | const stored = localStorage.getItem('user_data') || sessionStorage.getItem('user_data'); + 145 | return stored ? JSON.parse(stored) : null; + 146 | }); + 147 | return userData; + 148 | } catch { + 149 | return null; + 150 | } + 151 | } + 152 | + 153 | /** + 154 | * Wait for authentication to complete + 155 | */ + 156 | export async function waitForAuth(page: Page, timeout: number = 5000): Promise { + 157 | // Wait for either: + 158 | // 1. Token to be in storage + 159 | // 2. Navigation to happen + 160 | // 3. Auth overlay to disappear + 161 | await page.waitForFunction( + 162 | () => { + 163 | const token = + 164 | typeof window !== 'undefined' + 165 | ? localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token') + 166 | : null; +``` \ No newline at end of file diff --git a/frontend/playwright-report/data/df63ed0aadc766e5052f30f39c06940999f006f7.md b/frontend/playwright-report/data/df63ed0aadc766e5052f30f39c06940999f006f7.md new file mode 100644 index 00000000..b629c616 --- /dev/null +++ b/frontend/playwright-report/data/df63ed0aadc766e5052f30f39c06940999f006f7.md @@ -0,0 +1,197 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: 1-login.spec.ts >> Login Workflow >> should handle network errors gracefully +- Location: e2e/workflows/1-login.spec.ts:181:7 + +# Error details + +``` +Test timeout of 30000ms exceeded. +``` + +``` +Error: locator.fill: Test timeout of 30000ms exceeded. +Call log: + - waiting for locator('[data-testid="username-input"]') + +``` + +# Page snapshot + +```yaml +- generic [active] [ref=e1]: + - generic [ref=e3]: + - generic [ref=e4]: + - img [ref=e6] + - heading "Identity Check" [level=2] [ref=e9] + - paragraph [ref=e10]: Select operator profile or use direct login + - generic [ref=e11]: + - button "newuser user" [ref=e12] [cursor=pointer]: + - generic [ref=e13]: + - img [ref=e15] + - generic [ref=e18]: + - paragraph [ref=e19]: newuser + - paragraph [ref=e20]: user + - img [ref=e21] + - generic [ref=e23]: + - button "Enterprise" [ref=e24] [cursor=pointer]: + - img [ref=e25] + - text: Enterprise + - button "Manual Login" [ref=e28] [cursor=pointer]: + - img [ref=e29] + - text: Manual Login + - button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]: + - img [ref=e38] + - alert [ref=e41] +``` + +# Test source + +```ts + 90 | await assertions.assertUserAuthenticated(page, BASE_URL); + 91 | }); + 92 | + 93 | test('should show user identity after successful login', async ({ page }) => { + 94 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 95 | + 96 | // Check that user info is displayed + 97 | const userDisplay = page.locator('[data-testid="user-display"]'); + 98 | await expect(userDisplay).toBeVisible(); + 99 | + 100 | // Verify username is shown + 101 | const username = await userDisplay.textContent(); + 102 | expect(username).toContain(LOCAL_USERS.admin.username); + 103 | }); + 104 | + 105 | test('should maintain session across page reloads', async ({ page }) => { + 106 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 107 | await assertions.assertUserAuthenticated(page, BASE_URL); + 108 | + 109 | // Reload page + 110 | await page.reload(); + 111 | + 112 | // Should still be authenticated + 113 | const token = await auth.getAuthToken(page); + 114 | expect(token).toBeTruthy(); + 115 | + 116 | // Should not be on login page + 117 | await expect(page).not.toHaveURL(BASE_URL); + 118 | }); + 119 | + 120 | test('should support logout functionality', async ({ page }) => { + 121 | // Login first + 122 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 123 | await assertions.assertUserAuthenticated(page, BASE_URL); + 124 | + 125 | // Logout + 126 | await auth.logout(page, BASE_URL); + 127 | + 128 | // Should be back on login page + 129 | const identityOverlay = page.locator('[data-testid="identity-check-overlay"]'); + 130 | await expect(identityOverlay).toBeVisible({ timeout: 5000 }); + 131 | + 132 | // Token should be cleared + 133 | const token = await auth.getAuthToken(page); + 134 | expect(token).toBeFalsy(); + 135 | }); + 136 | + 137 | test('should show admin button for admin users', async ({ page }) => { + 138 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 139 | await assertions.assertUserAuthenticated(page, BASE_URL); + 140 | + 141 | // Admin button should be visible + 142 | const adminButton = page.locator('[data-testid="admin-button"]'); + 143 | await expect(adminButton).toBeVisible(); + 144 | }); + 145 | + 146 | test('should allow switching between LDAP and local login tabs', async ({ page }) => { + 147 | const ldapTab = page.locator('[data-testid="ldap-login-tab"]'); + 148 | const localTab = page.locator('[data-testid="local-login-tab"]'); + 149 | + 150 | // Start on LDAP tab + 151 | let ldapForm = page.locator('[data-testid="ldap-login-form"]'); + 152 | await expect(ldapForm).toBeVisible(); + 153 | + 154 | // Switch to local + 155 | await localTab.click(); + 156 | ldapForm = page.locator('[data-testid="ldap-login-form"]'); + 157 | await expect(ldapForm).not.toBeVisible(); + 158 | + 159 | // Switch back to LDAP + 160 | await ldapTab.click(); + 161 | ldapForm = page.locator('[data-testid="ldap-login-form"]'); + 162 | await expect(ldapForm).toBeVisible(); + 163 | }); + 164 | + 165 | test('should remember login preference on revisit', async ({ page, context }) => { + 166 | // Login with local user + 167 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 168 | + 169 | // Close and reopen browser + 170 | await page.close(); + 171 | const newPage = await context.newPage(); + 172 | await newPage.goto(BASE_URL); + 173 | + 174 | // Should still be authenticated + 175 | const authenticated = await auth.isAuthenticated(newPage); + 176 | expect(authenticated).toBe(true); + 177 | + 178 | await newPage.close(); + 179 | }); + 180 | + 181 | test('should handle network errors gracefully', async ({ page }) => { + 182 | // Enable offline mode + 183 | await page.context().setOffline(true); + 184 | + 185 | // Try to login + 186 | const usernameInput = page.locator('[data-testid="username-input"]'); + 187 | const passwordInput = page.locator('[data-testid="password-input"]'); + 188 | const submitButton = page.locator('[data-testid="login-submit"]'); + 189 | +> 190 | await usernameInput.fill(LOCAL_USERS.admin.username); + | ^ Error: locator.fill: Test timeout of 30000ms exceeded. + 191 | await passwordInput.fill(LOCAL_USERS.admin.password); + 192 | await submitButton.click(); + 193 | + 194 | // Should show network error + 195 | const errorToast = page.locator('[data-testid="toast-error"]'); + 196 | await expect(errorToast).toBeVisible({ timeout: 5000 }); + 197 | + 198 | // Re-enable network + 199 | await page.context().setOffline(false); + 200 | }); + 201 | + 202 | test('should display user list in local login tab', async ({ page }) => { + 203 | const localTab = page.locator('[data-testid="local-login-tab"]'); + 204 | await localTab.click(); + 205 | + 206 | // User list should be visible + 207 | const userList = page.locator('[data-testid="user-list"]'); + 208 | await expect(userList).toBeVisible(); + 209 | + 210 | // Should have at least one user + 211 | const userItems = page.locator('[data-testid="user-list-item"]'); + 212 | expect(await userItems.count()).toBeGreaterThan(0); + 213 | }); + 214 | + 215 | test('should auto-login when clicking user from list', async ({ page }) => { + 216 | const localTab = page.locator('[data-testid="local-login-tab"]'); + 217 | await localTab.click(); + 218 | + 219 | // Click first user in list + 220 | const firstUser = page.locator('[data-testid="user-list-item"]').first(); + 221 | await firstUser.click(); + 222 | + 223 | // Password input should be focused + 224 | const passwordInput = page.locator('[data-testid="local-password-input"]'); + 225 | await expect(passwordInput).toBeFocused(); + 226 | }); + 227 | }); + 228 | +``` \ No newline at end of file diff --git a/frontend/playwright-report/data/e2fc9fb7628901bc2b220788e3abb82ab1709e5f.md b/frontend/playwright-report/data/e2fc9fb7628901bc2b220788e3abb82ab1709e5f.md new file mode 100644 index 00000000..e970807f --- /dev/null +++ b/frontend/playwright-report/data/e2fc9fb7628901bc2b220788e3abb82ab1709e5f.md @@ -0,0 +1,224 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: 1-login.spec.ts >> Login Workflow >> should maintain session across page reloads +- Location: e2e/workflows/1-login.spec.ts:105:7 + +# Error details + +``` +Test timeout of 30000ms exceeded. +``` + +``` +Error: page.fill: Test timeout of 30000ms exceeded. +Call log: + - waiting for locator('[data-testid="local-username-input"]') + +``` + +# Page snapshot + +```yaml +- generic [active] [ref=e1]: + - generic [ref=e3]: + - generic [ref=e4]: + - img [ref=e6] + - heading "Identity Check" [level=2] [ref=e9] + - paragraph [ref=e10]: Select operator profile or use direct login + - generic [ref=e11]: + - button "newuser user" [ref=e12] [cursor=pointer]: + - generic [ref=e13]: + - img [ref=e15] + - generic [ref=e18]: + - paragraph [ref=e19]: newuser + - paragraph [ref=e20]: user + - img [ref=e21] + - generic [ref=e23]: + - button "Enterprise" [ref=e24] [cursor=pointer]: + - img [ref=e25] + - text: Enterprise + - button "Manual Login" [ref=e28] [cursor=pointer]: + - img [ref=e29] + - text: Manual Login + - button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]: + - img [ref=e38] + - alert [ref=e41] +``` + +# Test source + +```ts + 1 | import { Page, BrowserContext } from '@playwright/test'; + 2 | + 3 | /** + 4 | * Authentication fixture for E2E tests + 5 | * Handles login flows and session management + 6 | */ + 7 | + 8 | export interface LoginCredentials { + 9 | username: string; + 10 | password: string; + 11 | } + 12 | + 13 | export interface AuthSession { + 14 | token: string; + 15 | userId: string; + 16 | email: string; + 17 | isAdmin: boolean; + 18 | } + 19 | + 20 | /** + 21 | * Perform LDAP authentication + 22 | */ + 23 | export async function loginWithLdap( + 24 | page: Page, + 25 | credentials: LoginCredentials, + 26 | baseUrl: string = 'http://localhost' + 27 | ): Promise { + 28 | await page.goto(`${baseUrl}`); + 29 | + 30 | // Wait for login form to appear + 31 | await page.waitForSelector('[data-testid="login-form"]', { timeout: 5000 }); + 32 | + 33 | // Fill in username + 34 | await page.fill('[data-testid="username-input"]', credentials.username); + 35 | + 36 | // Fill in password + 37 | await page.fill('[data-testid="password-input"]', credentials.password); + 38 | + 39 | // Click login button + 40 | await page.click('[data-testid="login-submit"]'); + 41 | + 42 | // Wait for navigation to inventory page + 43 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' }); + 44 | } + 45 | + 46 | /** + 47 | * Perform local user authentication + 48 | */ + 49 | export async function loginWithLocalUser( + 50 | page: Page, + 51 | credentials: LoginCredentials, + 52 | baseUrl: string = 'http://localhost' + 53 | ): Promise { + 54 | await page.goto(`${baseUrl}`); + 55 | + 56 | // Wait for identity check overlay + 57 | await page.waitForSelector('[data-testid="identity-check-overlay"]', { timeout: 5000 }); + 58 | + 59 | // Click "Local User" tab if it exists + 60 | const localUserTab = page.locator('[data-testid="local-user-tab"]'); + 61 | if (await localUserTab.isVisible()) { + 62 | await localUserTab.click(); + 63 | } + 64 | + 65 | // Fill in username +> 66 | await page.fill('[data-testid="local-username-input"]', credentials.username); + | ^ Error: page.fill: Test timeout of 30000ms exceeded. + 67 | + 68 | // Fill in password + 69 | await page.fill('[data-testid="local-password-input"]', credentials.password); + 70 | + 71 | // Click login button + 72 | await page.click('[data-testid="local-login-submit"]'); + 73 | + 74 | // Wait for navigation to inventory page + 75 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' }); + 76 | } + 77 | + 78 | /** + 79 | * Logout from the application + 80 | */ + 81 | export async function logout(page: Page, baseUrl: string = 'http://localhost'): Promise { + 82 | // Click logout button or menu + 83 | const logoutButton = page.locator('[data-testid="logout-button"]'); + 84 | if (await logoutButton.isVisible()) { + 85 | await logoutButton.click(); + 86 | } else { + 87 | // Try finding it in menu + 88 | const menu = page.locator('[data-testid="user-menu"]'); + 89 | if (await menu.isVisible()) { + 90 | await menu.click(); + 91 | await page.click('[data-testid="logout-menu-item"]'); + 92 | } + 93 | } + 94 | + 95 | // Wait for redirect to login + 96 | await page.waitForURL(`${baseUrl}/*`, { timeout: 5000 }); + 97 | } + 98 | + 99 | /** + 100 | * Get authentication token from local storage + 101 | */ + 102 | export async function getAuthToken(page: Page): Promise { + 103 | const token = await page.evaluate(() => { + 104 | return localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token'); + 105 | }); + 106 | return token; + 107 | } + 108 | + 109 | /** + 110 | * Set authentication token in local storage + 111 | */ + 112 | export async function setAuthToken(page: Page, token: string): Promise { + 113 | await page.evaluate((t) => { + 114 | localStorage.setItem('auth_token', t); + 115 | }, token); + 116 | } + 117 | + 118 | /** + 119 | * Clear authentication data + 120 | */ + 121 | export async function clearAuth(page: Page): Promise { + 122 | await page.evaluate(() => { + 123 | localStorage.removeItem('auth_token'); + 124 | sessionStorage.removeItem('auth_token'); + 125 | localStorage.removeItem('user_data'); + 126 | sessionStorage.removeItem('user_data'); + 127 | }); + 128 | } + 129 | + 130 | /** + 131 | * Check if user is authenticated + 132 | */ + 133 | export async function isAuthenticated(page: Page): Promise { + 134 | const token = await getAuthToken(page); + 135 | return !!token; + 136 | } + 137 | + 138 | /** + 139 | * Get current user info from page + 140 | */ + 141 | export async function getCurrentUser(page: Page): Promise<{ username: string; email: string } | null> { + 142 | try { + 143 | const userData = await page.evaluate(() => { + 144 | const stored = localStorage.getItem('user_data') || sessionStorage.getItem('user_data'); + 145 | return stored ? JSON.parse(stored) : null; + 146 | }); + 147 | return userData; + 148 | } catch { + 149 | return null; + 150 | } + 151 | } + 152 | + 153 | /** + 154 | * Wait for authentication to complete + 155 | */ + 156 | export async function waitForAuth(page: Page, timeout: number = 5000): Promise { + 157 | // Wait for either: + 158 | // 1. Token to be in storage + 159 | // 2. Navigation to happen + 160 | // 3. Auth overlay to disappear + 161 | await page.waitForFunction( + 162 | () => { + 163 | const token = + 164 | typeof window !== 'undefined' + 165 | ? localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token') + 166 | : null; +``` \ No newline at end of file diff --git a/frontend/playwright-report/data/e5e30d817d86bfb02dc036758e13ff2f70ee0d1b.md b/frontend/playwright-report/data/e5e30d817d86bfb02dc036758e13ff2f70ee0d1b.md new file mode 100644 index 00000000..128321ad --- /dev/null +++ b/frontend/playwright-report/data/e5e30d817d86bfb02dc036758e13ff2f70ee0d1b.md @@ -0,0 +1,187 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: 1-login.spec.ts >> Login Workflow >> should successfully login with valid LDAP credentials +- Location: e2e/workflows/1-login.spec.ts:52:7 + +# Error details + +``` +TimeoutError: page.waitForSelector: Timeout 5000ms exceeded. +Call log: + - waiting for locator('[data-testid="login-form"]') to be visible + - waiting for" http://localhost:8917/login" navigation to finish... + - navigated to "http://localhost:8917/login" + +``` + +# Page snapshot + +```yaml +- generic [active] [ref=e1]: + - generic [ref=e3]: + - generic [ref=e4]: + - img [ref=e6] + - heading "Identity Check" [level=2] [ref=e9] + - paragraph [ref=e10]: Select operator profile or use direct login + - generic [ref=e11]: + - button "newuser user" [ref=e12] [cursor=pointer]: + - generic [ref=e13]: + - img [ref=e15] + - generic [ref=e18]: + - paragraph [ref=e19]: newuser + - paragraph [ref=e20]: user + - img [ref=e21] + - generic [ref=e23]: + - button "Enterprise" [ref=e24] [cursor=pointer]: + - img [ref=e25] + - text: Enterprise + - button "Manual Login" [ref=e28] [cursor=pointer]: + - img [ref=e29] + - text: Manual Login + - button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]: + - img [ref=e38] + - alert [ref=e41] +``` + +# Test source + +```ts + 1 | import { Page, BrowserContext } from '@playwright/test'; + 2 | + 3 | /** + 4 | * Authentication fixture for E2E tests + 5 | * Handles login flows and session management + 6 | */ + 7 | + 8 | export interface LoginCredentials { + 9 | username: string; + 10 | password: string; + 11 | } + 12 | + 13 | export interface AuthSession { + 14 | token: string; + 15 | userId: string; + 16 | email: string; + 17 | isAdmin: boolean; + 18 | } + 19 | + 20 | /** + 21 | * Perform LDAP authentication + 22 | */ + 23 | export async function loginWithLdap( + 24 | page: Page, + 25 | credentials: LoginCredentials, + 26 | baseUrl: string = 'http://localhost' + 27 | ): Promise { + 28 | await page.goto(`${baseUrl}`); + 29 | + 30 | // Wait for login form to appear +> 31 | await page.waitForSelector('[data-testid="login-form"]', { timeout: 5000 }); + | ^ TimeoutError: page.waitForSelector: Timeout 5000ms exceeded. + 32 | + 33 | // Fill in username + 34 | await page.fill('[data-testid="username-input"]', credentials.username); + 35 | + 36 | // Fill in password + 37 | await page.fill('[data-testid="password-input"]', credentials.password); + 38 | + 39 | // Click login button + 40 | await page.click('[data-testid="login-submit"]'); + 41 | + 42 | // Wait for navigation to inventory page + 43 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' }); + 44 | } + 45 | + 46 | /** + 47 | * Perform local user authentication + 48 | */ + 49 | export async function loginWithLocalUser( + 50 | page: Page, + 51 | credentials: LoginCredentials, + 52 | baseUrl: string = 'http://localhost' + 53 | ): Promise { + 54 | await page.goto(`${baseUrl}`); + 55 | + 56 | // Wait for identity check overlay + 57 | await page.waitForSelector('[data-testid="identity-check-overlay"]', { timeout: 5000 }); + 58 | + 59 | // Click "Local User" tab if it exists + 60 | const localUserTab = page.locator('[data-testid="local-user-tab"]'); + 61 | if (await localUserTab.isVisible()) { + 62 | await localUserTab.click(); + 63 | } + 64 | + 65 | // Fill in username + 66 | await page.fill('[data-testid="local-username-input"]', credentials.username); + 67 | + 68 | // Fill in password + 69 | await page.fill('[data-testid="local-password-input"]', credentials.password); + 70 | + 71 | // Click login button + 72 | await page.click('[data-testid="local-login-submit"]'); + 73 | + 74 | // Wait for navigation to inventory page + 75 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' }); + 76 | } + 77 | + 78 | /** + 79 | * Logout from the application + 80 | */ + 81 | export async function logout(page: Page, baseUrl: string = 'http://localhost'): Promise { + 82 | // Click logout button or menu + 83 | const logoutButton = page.locator('[data-testid="logout-button"]'); + 84 | if (await logoutButton.isVisible()) { + 85 | await logoutButton.click(); + 86 | } else { + 87 | // Try finding it in menu + 88 | const menu = page.locator('[data-testid="user-menu"]'); + 89 | if (await menu.isVisible()) { + 90 | await menu.click(); + 91 | await page.click('[data-testid="logout-menu-item"]'); + 92 | } + 93 | } + 94 | + 95 | // Wait for redirect to login + 96 | await page.waitForURL(`${baseUrl}/*`, { timeout: 5000 }); + 97 | } + 98 | + 99 | /** + 100 | * Get authentication token from local storage + 101 | */ + 102 | export async function getAuthToken(page: Page): Promise { + 103 | const token = await page.evaluate(() => { + 104 | return localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token'); + 105 | }); + 106 | return token; + 107 | } + 108 | + 109 | /** + 110 | * Set authentication token in local storage + 111 | */ + 112 | export async function setAuthToken(page: Page, token: string): Promise { + 113 | await page.evaluate((t) => { + 114 | localStorage.setItem('auth_token', t); + 115 | }, token); + 116 | } + 117 | + 118 | /** + 119 | * Clear authentication data + 120 | */ + 121 | export async function clearAuth(page: Page): Promise { + 122 | await page.evaluate(() => { + 123 | localStorage.removeItem('auth_token'); + 124 | sessionStorage.removeItem('auth_token'); + 125 | localStorage.removeItem('user_data'); + 126 | sessionStorage.removeItem('user_data'); + 127 | }); + 128 | } + 129 | + 130 | /** + 131 | * Check if user is authenticated +``` \ No newline at end of file diff --git a/frontend/playwright-report/data/e9f5853c90bbe234f26f48d5ffd6b063536c41e3.md b/frontend/playwright-report/data/e9f5853c90bbe234f26f48d5ffd6b063536c41e3.md new file mode 100644 index 00000000..888f9e98 --- /dev/null +++ b/frontend/playwright-report/data/e9f5853c90bbe234f26f48d5ffd6b063536c41e3.md @@ -0,0 +1,224 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: 1-login.spec.ts >> Login Workflow >> should show admin button for admin users +- Location: e2e/workflows/1-login.spec.ts:137:7 + +# Error details + +``` +Test timeout of 30000ms exceeded. +``` + +``` +Error: page.fill: Test timeout of 30000ms exceeded. +Call log: + - waiting for locator('[data-testid="local-username-input"]') + +``` + +# Page snapshot + +```yaml +- generic [active] [ref=e1]: + - generic [ref=e3]: + - generic [ref=e4]: + - img [ref=e6] + - heading "Identity Check" [level=2] [ref=e9] + - paragraph [ref=e10]: Select operator profile or use direct login + - generic [ref=e11]: + - button "newuser user" [ref=e12] [cursor=pointer]: + - generic [ref=e13]: + - img [ref=e15] + - generic [ref=e18]: + - paragraph [ref=e19]: newuser + - paragraph [ref=e20]: user + - img [ref=e21] + - generic [ref=e23]: + - button "Enterprise" [ref=e24] [cursor=pointer]: + - img [ref=e25] + - text: Enterprise + - button "Manual Login" [ref=e28] [cursor=pointer]: + - img [ref=e29] + - text: Manual Login + - button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]: + - img [ref=e38] + - alert [ref=e41] +``` + +# Test source + +```ts + 1 | import { Page, BrowserContext } from '@playwright/test'; + 2 | + 3 | /** + 4 | * Authentication fixture for E2E tests + 5 | * Handles login flows and session management + 6 | */ + 7 | + 8 | export interface LoginCredentials { + 9 | username: string; + 10 | password: string; + 11 | } + 12 | + 13 | export interface AuthSession { + 14 | token: string; + 15 | userId: string; + 16 | email: string; + 17 | isAdmin: boolean; + 18 | } + 19 | + 20 | /** + 21 | * Perform LDAP authentication + 22 | */ + 23 | export async function loginWithLdap( + 24 | page: Page, + 25 | credentials: LoginCredentials, + 26 | baseUrl: string = 'http://localhost' + 27 | ): Promise { + 28 | await page.goto(`${baseUrl}`); + 29 | + 30 | // Wait for login form to appear + 31 | await page.waitForSelector('[data-testid="login-form"]', { timeout: 5000 }); + 32 | + 33 | // Fill in username + 34 | await page.fill('[data-testid="username-input"]', credentials.username); + 35 | + 36 | // Fill in password + 37 | await page.fill('[data-testid="password-input"]', credentials.password); + 38 | + 39 | // Click login button + 40 | await page.click('[data-testid="login-submit"]'); + 41 | + 42 | // Wait for navigation to inventory page + 43 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' }); + 44 | } + 45 | + 46 | /** + 47 | * Perform local user authentication + 48 | */ + 49 | export async function loginWithLocalUser( + 50 | page: Page, + 51 | credentials: LoginCredentials, + 52 | baseUrl: string = 'http://localhost' + 53 | ): Promise { + 54 | await page.goto(`${baseUrl}`); + 55 | + 56 | // Wait for identity check overlay + 57 | await page.waitForSelector('[data-testid="identity-check-overlay"]', { timeout: 5000 }); + 58 | + 59 | // Click "Local User" tab if it exists + 60 | const localUserTab = page.locator('[data-testid="local-user-tab"]'); + 61 | if (await localUserTab.isVisible()) { + 62 | await localUserTab.click(); + 63 | } + 64 | + 65 | // Fill in username +> 66 | await page.fill('[data-testid="local-username-input"]', credentials.username); + | ^ Error: page.fill: Test timeout of 30000ms exceeded. + 67 | + 68 | // Fill in password + 69 | await page.fill('[data-testid="local-password-input"]', credentials.password); + 70 | + 71 | // Click login button + 72 | await page.click('[data-testid="local-login-submit"]'); + 73 | + 74 | // Wait for navigation to inventory page + 75 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' }); + 76 | } + 77 | + 78 | /** + 79 | * Logout from the application + 80 | */ + 81 | export async function logout(page: Page, baseUrl: string = 'http://localhost'): Promise { + 82 | // Click logout button or menu + 83 | const logoutButton = page.locator('[data-testid="logout-button"]'); + 84 | if (await logoutButton.isVisible()) { + 85 | await logoutButton.click(); + 86 | } else { + 87 | // Try finding it in menu + 88 | const menu = page.locator('[data-testid="user-menu"]'); + 89 | if (await menu.isVisible()) { + 90 | await menu.click(); + 91 | await page.click('[data-testid="logout-menu-item"]'); + 92 | } + 93 | } + 94 | + 95 | // Wait for redirect to login + 96 | await page.waitForURL(`${baseUrl}/*`, { timeout: 5000 }); + 97 | } + 98 | + 99 | /** + 100 | * Get authentication token from local storage + 101 | */ + 102 | export async function getAuthToken(page: Page): Promise { + 103 | const token = await page.evaluate(() => { + 104 | return localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token'); + 105 | }); + 106 | return token; + 107 | } + 108 | + 109 | /** + 110 | * Set authentication token in local storage + 111 | */ + 112 | export async function setAuthToken(page: Page, token: string): Promise { + 113 | await page.evaluate((t) => { + 114 | localStorage.setItem('auth_token', t); + 115 | }, token); + 116 | } + 117 | + 118 | /** + 119 | * Clear authentication data + 120 | */ + 121 | export async function clearAuth(page: Page): Promise { + 122 | await page.evaluate(() => { + 123 | localStorage.removeItem('auth_token'); + 124 | sessionStorage.removeItem('auth_token'); + 125 | localStorage.removeItem('user_data'); + 126 | sessionStorage.removeItem('user_data'); + 127 | }); + 128 | } + 129 | + 130 | /** + 131 | * Check if user is authenticated + 132 | */ + 133 | export async function isAuthenticated(page: Page): Promise { + 134 | const token = await getAuthToken(page); + 135 | return !!token; + 136 | } + 137 | + 138 | /** + 139 | * Get current user info from page + 140 | */ + 141 | export async function getCurrentUser(page: Page): Promise<{ username: string; email: string } | null> { + 142 | try { + 143 | const userData = await page.evaluate(() => { + 144 | const stored = localStorage.getItem('user_data') || sessionStorage.getItem('user_data'); + 145 | return stored ? JSON.parse(stored) : null; + 146 | }); + 147 | return userData; + 148 | } catch { + 149 | return null; + 150 | } + 151 | } + 152 | + 153 | /** + 154 | * Wait for authentication to complete + 155 | */ + 156 | export async function waitForAuth(page: Page, timeout: number = 5000): Promise { + 157 | // Wait for either: + 158 | // 1. Token to be in storage + 159 | // 2. Navigation to happen + 160 | // 3. Auth overlay to disappear + 161 | await page.waitForFunction( + 162 | () => { + 163 | const token = + 164 | typeof window !== 'undefined' + 165 | ? localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token') + 166 | : null; +``` \ No newline at end of file diff --git a/frontend/playwright-report/data/f29622b7ac30dae4a89cd725099a3fdfddd92bab.md b/frontend/playwright-report/data/f29622b7ac30dae4a89cd725099a3fdfddd92bab.md new file mode 100644 index 00000000..31d38f19 --- /dev/null +++ b/frontend/playwright-report/data/f29622b7ac30dae4a89cd725099a3fdfddd92bab.md @@ -0,0 +1,174 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: 1-login.spec.ts >> Login Workflow >> should display user list in local login tab +- Location: e2e/workflows/1-login.spec.ts:202:7 + +# Error details + +``` +Error: expect(received).toBeGreaterThan(expected) + +Expected: > 0 +Received: 0 +``` + +# Page snapshot + +```yaml +- generic [ref=e1]: + - generic [ref=e3]: + - generic [ref=e4]: + - img [ref=e6] + - heading "Identity Check" [level=2] [ref=e9] + - paragraph [ref=e10]: Select operator profile or use direct login + - generic [ref=e12]: + - generic [ref=e13]: + - button [ref=e14] [cursor=pointer]: + - img [ref=e15] + - generic [ref=e18]: + - paragraph [ref=e19]: Logging in as + - paragraph [ref=e20]: Manual Input + - generic [ref=e21]: + - text: Username + - generic [ref=e22]: + - img [ref=e23] + - textbox "Admin" [ref=e26] + - generic [ref=e27]: + - text: Password + - generic [ref=e28]: + - img [ref=e29] + - textbox "Enter password" [active] [ref=e32] + - button "Verify Identity" [ref=e33] [cursor=pointer] + - button "Open Next.js Dev Tools" [ref=e39] [cursor=pointer]: + - img [ref=e40] + - alert [ref=e43] +``` + +# Test source + +```ts + 112 | // Should still be authenticated + 113 | const token = await auth.getAuthToken(page); + 114 | expect(token).toBeTruthy(); + 115 | + 116 | // Should not be on login page + 117 | await expect(page).not.toHaveURL(BASE_URL); + 118 | }); + 119 | + 120 | test('should support logout functionality', async ({ page }) => { + 121 | // Login first + 122 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 123 | await assertions.assertUserAuthenticated(page, BASE_URL); + 124 | + 125 | // Logout + 126 | await auth.logout(page, BASE_URL); + 127 | + 128 | // Should be back on login page + 129 | const identityOverlay = page.locator('[data-testid="identity-check-overlay"]'); + 130 | await expect(identityOverlay).toBeVisible({ timeout: 5000 }); + 131 | + 132 | // Token should be cleared + 133 | const token = await auth.getAuthToken(page); + 134 | expect(token).toBeFalsy(); + 135 | }); + 136 | + 137 | test('should show admin button for admin users', async ({ page }) => { + 138 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 139 | await assertions.assertUserAuthenticated(page, BASE_URL); + 140 | + 141 | // Admin button should be visible + 142 | const adminButton = page.locator('[data-testid="admin-button"]'); + 143 | await expect(adminButton).toBeVisible(); + 144 | }); + 145 | + 146 | test('should allow switching between LDAP and local login tabs', async ({ page }) => { + 147 | const ldapTab = page.locator('[data-testid="ldap-login-tab"]'); + 148 | const localTab = page.locator('[data-testid="local-login-tab"]'); + 149 | + 150 | // Start on LDAP tab + 151 | let ldapForm = page.locator('[data-testid="ldap-login-form"]'); + 152 | await expect(ldapForm).toBeVisible(); + 153 | + 154 | // Switch to local + 155 | await localTab.click(); + 156 | ldapForm = page.locator('[data-testid="ldap-login-form"]'); + 157 | await expect(ldapForm).not.toBeVisible(); + 158 | + 159 | // Switch back to LDAP + 160 | await ldapTab.click(); + 161 | ldapForm = page.locator('[data-testid="ldap-login-form"]'); + 162 | await expect(ldapForm).toBeVisible(); + 163 | }); + 164 | + 165 | test('should remember login preference on revisit', async ({ page, context }) => { + 166 | // Login with local user + 167 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 168 | + 169 | // Close and reopen browser + 170 | await page.close(); + 171 | const newPage = await context.newPage(); + 172 | await newPage.goto(BASE_URL); + 173 | + 174 | // Should still be authenticated + 175 | const authenticated = await auth.isAuthenticated(newPage); + 176 | expect(authenticated).toBe(true); + 177 | + 178 | await newPage.close(); + 179 | }); + 180 | + 181 | test('should handle network errors gracefully', async ({ page }) => { + 182 | // Enable offline mode + 183 | await page.context().setOffline(true); + 184 | + 185 | // Try to login + 186 | const usernameInput = page.locator('[data-testid="username-input"]'); + 187 | const passwordInput = page.locator('[data-testid="password-input"]'); + 188 | const submitButton = page.locator('[data-testid="login-submit"]'); + 189 | + 190 | await usernameInput.fill(LOCAL_USERS.admin.username); + 191 | await passwordInput.fill(LOCAL_USERS.admin.password); + 192 | await submitButton.click(); + 193 | + 194 | // Should show network error + 195 | const errorToast = page.locator('[data-testid="toast-error"]'); + 196 | await expect(errorToast).toBeVisible({ timeout: 5000 }); + 197 | + 198 | // Re-enable network + 199 | await page.context().setOffline(false); + 200 | }); + 201 | + 202 | test('should display user list in local login tab', async ({ page }) => { + 203 | const localTab = page.locator('[data-testid="local-login-tab"]'); + 204 | await localTab.click(); + 205 | + 206 | // User list should be visible + 207 | const userList = page.locator('[data-testid="user-list"]'); + 208 | await expect(userList).toBeVisible(); + 209 | + 210 | // Should have at least one user + 211 | const userItems = page.locator('[data-testid="user-list-item"]'); +> 212 | expect(await userItems.count()).toBeGreaterThan(0); + | ^ Error: expect(received).toBeGreaterThan(expected) + 213 | }); + 214 | + 215 | test('should auto-login when clicking user from list', async ({ page }) => { + 216 | const localTab = page.locator('[data-testid="local-login-tab"]'); + 217 | await localTab.click(); + 218 | + 219 | // Click first user in list + 220 | const firstUser = page.locator('[data-testid="user-list-item"]').first(); + 221 | await firstUser.click(); + 222 | + 223 | // Password input should be focused + 224 | const passwordInput = page.locator('[data-testid="local-password-input"]'); + 225 | await expect(passwordInput).toBeFocused(); + 226 | }); + 227 | }); + 228 | +``` \ No newline at end of file diff --git a/frontend/playwright-report/data/fa35d529e1ccc0f83b98a04aad0b74476e30d06d.md b/frontend/playwright-report/data/fa35d529e1ccc0f83b98a04aad0b74476e30d06d.md new file mode 100644 index 00000000..69cac167 --- /dev/null +++ b/frontend/playwright-report/data/fa35d529e1ccc0f83b98a04aad0b74476e30d06d.md @@ -0,0 +1,169 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: 1-login.spec.ts >> Login Workflow >> should auto-login when clicking user from list +- Location: e2e/workflows/1-login.spec.ts:215:7 + +# Error details + +``` +Test timeout of 30000ms exceeded. +``` + +``` +Error: locator.click: Test timeout of 30000ms exceeded. +Call log: + - waiting for locator('[data-testid="user-list-item"]').first() + +``` + +# Page snapshot + +```yaml +- generic [ref=e1]: + - generic [ref=e3]: + - generic [ref=e4]: + - img [ref=e6] + - heading "Identity Check" [level=2] [ref=e9] + - paragraph [ref=e10]: Select operator profile or use direct login + - generic [ref=e12]: + - generic [ref=e13]: + - button [ref=e14] [cursor=pointer]: + - img [ref=e15] + - generic [ref=e18]: + - paragraph [ref=e19]: Logging in as + - paragraph [ref=e20]: Manual Input + - generic [ref=e21]: + - text: Username + - generic [ref=e22]: + - img [ref=e23] + - textbox "Admin" [ref=e26] + - generic [ref=e27]: + - text: Password + - generic [ref=e28]: + - img [ref=e29] + - textbox "Enter password" [active] [ref=e32] + - button "Verify Identity" [ref=e33] [cursor=pointer] + - button "Open Next.js Dev Tools" [ref=e39] [cursor=pointer]: + - img [ref=e40] + - alert [ref=e43] +``` + +# Test source + +```ts + 121 | // Login first + 122 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 123 | await assertions.assertUserAuthenticated(page, BASE_URL); + 124 | + 125 | // Logout + 126 | await auth.logout(page, BASE_URL); + 127 | + 128 | // Should be back on login page + 129 | const identityOverlay = page.locator('[data-testid="identity-check-overlay"]'); + 130 | await expect(identityOverlay).toBeVisible({ timeout: 5000 }); + 131 | + 132 | // Token should be cleared + 133 | const token = await auth.getAuthToken(page); + 134 | expect(token).toBeFalsy(); + 135 | }); + 136 | + 137 | test('should show admin button for admin users', async ({ page }) => { + 138 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 139 | await assertions.assertUserAuthenticated(page, BASE_URL); + 140 | + 141 | // Admin button should be visible + 142 | const adminButton = page.locator('[data-testid="admin-button"]'); + 143 | await expect(adminButton).toBeVisible(); + 144 | }); + 145 | + 146 | test('should allow switching between LDAP and local login tabs', async ({ page }) => { + 147 | const ldapTab = page.locator('[data-testid="ldap-login-tab"]'); + 148 | const localTab = page.locator('[data-testid="local-login-tab"]'); + 149 | + 150 | // Start on LDAP tab + 151 | let ldapForm = page.locator('[data-testid="ldap-login-form"]'); + 152 | await expect(ldapForm).toBeVisible(); + 153 | + 154 | // Switch to local + 155 | await localTab.click(); + 156 | ldapForm = page.locator('[data-testid="ldap-login-form"]'); + 157 | await expect(ldapForm).not.toBeVisible(); + 158 | + 159 | // Switch back to LDAP + 160 | await ldapTab.click(); + 161 | ldapForm = page.locator('[data-testid="ldap-login-form"]'); + 162 | await expect(ldapForm).toBeVisible(); + 163 | }); + 164 | + 165 | test('should remember login preference on revisit', async ({ page, context }) => { + 166 | // Login with local user + 167 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 168 | + 169 | // Close and reopen browser + 170 | await page.close(); + 171 | const newPage = await context.newPage(); + 172 | await newPage.goto(BASE_URL); + 173 | + 174 | // Should still be authenticated + 175 | const authenticated = await auth.isAuthenticated(newPage); + 176 | expect(authenticated).toBe(true); + 177 | + 178 | await newPage.close(); + 179 | }); + 180 | + 181 | test('should handle network errors gracefully', async ({ page }) => { + 182 | // Enable offline mode + 183 | await page.context().setOffline(true); + 184 | + 185 | // Try to login + 186 | const usernameInput = page.locator('[data-testid="username-input"]'); + 187 | const passwordInput = page.locator('[data-testid="password-input"]'); + 188 | const submitButton = page.locator('[data-testid="login-submit"]'); + 189 | + 190 | await usernameInput.fill(LOCAL_USERS.admin.username); + 191 | await passwordInput.fill(LOCAL_USERS.admin.password); + 192 | await submitButton.click(); + 193 | + 194 | // Should show network error + 195 | const errorToast = page.locator('[data-testid="toast-error"]'); + 196 | await expect(errorToast).toBeVisible({ timeout: 5000 }); + 197 | + 198 | // Re-enable network + 199 | await page.context().setOffline(false); + 200 | }); + 201 | + 202 | test('should display user list in local login tab', async ({ page }) => { + 203 | const localTab = page.locator('[data-testid="local-login-tab"]'); + 204 | await localTab.click(); + 205 | + 206 | // User list should be visible + 207 | const userList = page.locator('[data-testid="user-list"]'); + 208 | await expect(userList).toBeVisible(); + 209 | + 210 | // Should have at least one user + 211 | const userItems = page.locator('[data-testid="user-list-item"]'); + 212 | expect(await userItems.count()).toBeGreaterThan(0); + 213 | }); + 214 | + 215 | test('should auto-login when clicking user from list', async ({ page }) => { + 216 | const localTab = page.locator('[data-testid="local-login-tab"]'); + 217 | await localTab.click(); + 218 | + 219 | // Click first user in list + 220 | const firstUser = page.locator('[data-testid="user-list-item"]').first(); +> 221 | await firstUser.click(); + | ^ Error: locator.click: Test timeout of 30000ms exceeded. + 222 | + 223 | // Password input should be focused + 224 | const passwordInput = page.locator('[data-testid="local-password-input"]'); + 225 | await expect(passwordInput).toBeFocused(); + 226 | }); + 227 | }); + 228 | +``` \ No newline at end of file diff --git a/frontend/playwright-report/index.html b/frontend/playwright-report/index.html index 0d10fa8b..aea9e1a5 100644 --- a/frontend/playwright-report/index.html +++ b/frontend/playwright-report/index.html @@ -87,4 +87,4 @@ Error generating stack: `+l.message+`
- \ No newline at end of file + \ No newline at end of file diff --git a/frontend/test-results/.last-run.json b/frontend/test-results/.last-run.json index 8f99b4a4..5564eed8 100644 --- a/frontend/test-results/.last-run.json +++ b/frontend/test-results/.last-run.json @@ -1,6 +1,20 @@ { "status": "failed", "failedTests": [ - "c505212a9f19102e22e9-48f621cdb716ff0f50ce" + "c505212a9f19102e22e9-b8f769e1bec2e9dccfc0", + "c505212a9f19102e22e9-6a9f032de2702afdb378", + "c505212a9f19102e22e9-787cf157cb53bc071666", + "c505212a9f19102e22e9-84c2788f57def6af5ddb", + "c505212a9f19102e22e9-e95d7ac175608d44215d", + "c505212a9f19102e22e9-a0fc187a3d252408623f", + "c505212a9f19102e22e9-84172ac693ecaace19da", + "c505212a9f19102e22e9-61cc541ea57322557a4f", + "c505212a9f19102e22e9-1d3ef84183762c93c2f1", + "c505212a9f19102e22e9-c50a33e8ac1194b1cf3f", + "c505212a9f19102e22e9-416328d87be35eb14c6f", + "c505212a9f19102e22e9-f6e73b7d908d39f8af63", + "c505212a9f19102e22e9-2933a06b064cdb673ab5", + "c505212a9f19102e22e9-e264b575fec6c229ca47", + "c505212a9f19102e22e9-e34a858da752dac8c229" ] } \ No newline at end of file diff --git a/frontend/test-results/1-login-Login-Workflow-sho-0bb23-e-network-errors-gracefully-chromium/error-context.md b/frontend/test-results/1-login-Login-Workflow-sho-0bb23-e-network-errors-gracefully-chromium/error-context.md new file mode 100644 index 00000000..b629c616 --- /dev/null +++ b/frontend/test-results/1-login-Login-Workflow-sho-0bb23-e-network-errors-gracefully-chromium/error-context.md @@ -0,0 +1,197 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: 1-login.spec.ts >> Login Workflow >> should handle network errors gracefully +- Location: e2e/workflows/1-login.spec.ts:181:7 + +# Error details + +``` +Test timeout of 30000ms exceeded. +``` + +``` +Error: locator.fill: Test timeout of 30000ms exceeded. +Call log: + - waiting for locator('[data-testid="username-input"]') + +``` + +# Page snapshot + +```yaml +- generic [active] [ref=e1]: + - generic [ref=e3]: + - generic [ref=e4]: + - img [ref=e6] + - heading "Identity Check" [level=2] [ref=e9] + - paragraph [ref=e10]: Select operator profile or use direct login + - generic [ref=e11]: + - button "newuser user" [ref=e12] [cursor=pointer]: + - generic [ref=e13]: + - img [ref=e15] + - generic [ref=e18]: + - paragraph [ref=e19]: newuser + - paragraph [ref=e20]: user + - img [ref=e21] + - generic [ref=e23]: + - button "Enterprise" [ref=e24] [cursor=pointer]: + - img [ref=e25] + - text: Enterprise + - button "Manual Login" [ref=e28] [cursor=pointer]: + - img [ref=e29] + - text: Manual Login + - button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]: + - img [ref=e38] + - alert [ref=e41] +``` + +# Test source + +```ts + 90 | await assertions.assertUserAuthenticated(page, BASE_URL); + 91 | }); + 92 | + 93 | test('should show user identity after successful login', async ({ page }) => { + 94 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 95 | + 96 | // Check that user info is displayed + 97 | const userDisplay = page.locator('[data-testid="user-display"]'); + 98 | await expect(userDisplay).toBeVisible(); + 99 | + 100 | // Verify username is shown + 101 | const username = await userDisplay.textContent(); + 102 | expect(username).toContain(LOCAL_USERS.admin.username); + 103 | }); + 104 | + 105 | test('should maintain session across page reloads', async ({ page }) => { + 106 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 107 | await assertions.assertUserAuthenticated(page, BASE_URL); + 108 | + 109 | // Reload page + 110 | await page.reload(); + 111 | + 112 | // Should still be authenticated + 113 | const token = await auth.getAuthToken(page); + 114 | expect(token).toBeTruthy(); + 115 | + 116 | // Should not be on login page + 117 | await expect(page).not.toHaveURL(BASE_URL); + 118 | }); + 119 | + 120 | test('should support logout functionality', async ({ page }) => { + 121 | // Login first + 122 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 123 | await assertions.assertUserAuthenticated(page, BASE_URL); + 124 | + 125 | // Logout + 126 | await auth.logout(page, BASE_URL); + 127 | + 128 | // Should be back on login page + 129 | const identityOverlay = page.locator('[data-testid="identity-check-overlay"]'); + 130 | await expect(identityOverlay).toBeVisible({ timeout: 5000 }); + 131 | + 132 | // Token should be cleared + 133 | const token = await auth.getAuthToken(page); + 134 | expect(token).toBeFalsy(); + 135 | }); + 136 | + 137 | test('should show admin button for admin users', async ({ page }) => { + 138 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 139 | await assertions.assertUserAuthenticated(page, BASE_URL); + 140 | + 141 | // Admin button should be visible + 142 | const adminButton = page.locator('[data-testid="admin-button"]'); + 143 | await expect(adminButton).toBeVisible(); + 144 | }); + 145 | + 146 | test('should allow switching between LDAP and local login tabs', async ({ page }) => { + 147 | const ldapTab = page.locator('[data-testid="ldap-login-tab"]'); + 148 | const localTab = page.locator('[data-testid="local-login-tab"]'); + 149 | + 150 | // Start on LDAP tab + 151 | let ldapForm = page.locator('[data-testid="ldap-login-form"]'); + 152 | await expect(ldapForm).toBeVisible(); + 153 | + 154 | // Switch to local + 155 | await localTab.click(); + 156 | ldapForm = page.locator('[data-testid="ldap-login-form"]'); + 157 | await expect(ldapForm).not.toBeVisible(); + 158 | + 159 | // Switch back to LDAP + 160 | await ldapTab.click(); + 161 | ldapForm = page.locator('[data-testid="ldap-login-form"]'); + 162 | await expect(ldapForm).toBeVisible(); + 163 | }); + 164 | + 165 | test('should remember login preference on revisit', async ({ page, context }) => { + 166 | // Login with local user + 167 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 168 | + 169 | // Close and reopen browser + 170 | await page.close(); + 171 | const newPage = await context.newPage(); + 172 | await newPage.goto(BASE_URL); + 173 | + 174 | // Should still be authenticated + 175 | const authenticated = await auth.isAuthenticated(newPage); + 176 | expect(authenticated).toBe(true); + 177 | + 178 | await newPage.close(); + 179 | }); + 180 | + 181 | test('should handle network errors gracefully', async ({ page }) => { + 182 | // Enable offline mode + 183 | await page.context().setOffline(true); + 184 | + 185 | // Try to login + 186 | const usernameInput = page.locator('[data-testid="username-input"]'); + 187 | const passwordInput = page.locator('[data-testid="password-input"]'); + 188 | const submitButton = page.locator('[data-testid="login-submit"]'); + 189 | +> 190 | await usernameInput.fill(LOCAL_USERS.admin.username); + | ^ Error: locator.fill: Test timeout of 30000ms exceeded. + 191 | await passwordInput.fill(LOCAL_USERS.admin.password); + 192 | await submitButton.click(); + 193 | + 194 | // Should show network error + 195 | const errorToast = page.locator('[data-testid="toast-error"]'); + 196 | await expect(errorToast).toBeVisible({ timeout: 5000 }); + 197 | + 198 | // Re-enable network + 199 | await page.context().setOffline(false); + 200 | }); + 201 | + 202 | test('should display user list in local login tab', async ({ page }) => { + 203 | const localTab = page.locator('[data-testid="local-login-tab"]'); + 204 | await localTab.click(); + 205 | + 206 | // User list should be visible + 207 | const userList = page.locator('[data-testid="user-list"]'); + 208 | await expect(userList).toBeVisible(); + 209 | + 210 | // Should have at least one user + 211 | const userItems = page.locator('[data-testid="user-list-item"]'); + 212 | expect(await userItems.count()).toBeGreaterThan(0); + 213 | }); + 214 | + 215 | test('should auto-login when clicking user from list', async ({ page }) => { + 216 | const localTab = page.locator('[data-testid="local-login-tab"]'); + 217 | await localTab.click(); + 218 | + 219 | // Click first user in list + 220 | const firstUser = page.locator('[data-testid="user-list-item"]').first(); + 221 | await firstUser.click(); + 222 | + 223 | // Password input should be focused + 224 | const passwordInput = page.locator('[data-testid="local-password-input"]'); + 225 | await expect(passwordInput).toBeFocused(); + 226 | }); + 227 | }); + 228 | +``` \ No newline at end of file diff --git a/frontend/test-results/1-login-Login-Workflow-sho-ba9e9-y-check-overlay-on-app-load-chromium/test-failed-1.png b/frontend/test-results/1-login-Login-Workflow-sho-0bb23-e-network-errors-gracefully-chromium/test-failed-1.png similarity index 100% rename from frontend/test-results/1-login-Login-Workflow-sho-ba9e9-y-check-overlay-on-app-load-chromium/test-failed-1.png rename to frontend/test-results/1-login-Login-Workflow-sho-0bb23-e-network-errors-gracefully-chromium/test-failed-1.png diff --git a/frontend/test-results/1-login-Login-Workflow-sho-1065a-n-LDAP-and-local-login-tabs-chromium/error-context.md b/frontend/test-results/1-login-Login-Workflow-sho-1065a-n-LDAP-and-local-login-tabs-chromium/error-context.md new file mode 100644 index 00000000..654868e4 --- /dev/null +++ b/frontend/test-results/1-login-Login-Workflow-sho-1065a-n-LDAP-and-local-login-tabs-chromium/error-context.md @@ -0,0 +1,238 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: 1-login.spec.ts >> Login Workflow >> should allow switching between LDAP and local login tabs +- Location: e2e/workflows/1-login.spec.ts:146:7 + +# Error details + +``` +Error: expect(locator).toBeVisible() failed + +Locator: locator('[data-testid="ldap-login-form"]') +Expected: visible +Timeout: 5000ms +Error: element(s) not found + +Call log: + - Expect "toBeVisible" with timeout 5000ms + - waiting for locator('[data-testid="ldap-login-form"]') + +``` + +# Page snapshot + +```yaml +- generic [active] [ref=e1]: + - generic [ref=e3]: + - generic [ref=e4]: + - img [ref=e6] + - heading "Identity Check" [level=2] [ref=e9] + - paragraph [ref=e10]: Select operator profile or use direct login + - generic [ref=e11]: + - button "newuser user" [ref=e12] [cursor=pointer]: + - generic [ref=e13]: + - img [ref=e15] + - generic [ref=e18]: + - paragraph [ref=e19]: newuser + - paragraph [ref=e20]: user + - img [ref=e21] + - generic [ref=e23]: + - button "Enterprise" [ref=e24] [cursor=pointer]: + - img [ref=e25] + - text: Enterprise + - button "Manual Login" [ref=e28] [cursor=pointer]: + - img [ref=e29] + - text: Manual Login + - button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]: + - img [ref=e38] + - alert [ref=e41] +``` + +# Test source + +```ts + 52 | test('should successfully login with valid LDAP credentials', async ({ page }) => { + 53 | // Skip if LDAP is not configured in test environment + 54 | test.skip(process.env.SKIP_LDAP === 'true', 'LDAP not available in test environment'); + 55 | + 56 | await auth.loginWithLdap(page, LDAP_USERS.valid, BASE_URL); + 57 | + 58 | // Verify we're authenticated + 59 | await assertions.assertUserAuthenticated(page, BASE_URL); + 60 | + 61 | // Verify token is stored + 62 | const token = await auth.getAuthToken(page); + 63 | expect(token).toBeTruthy(); + 64 | }); + 65 | + 66 | test('should reject invalid local user credentials', async ({ page }) => { + 67 | // Switch to local user tab + 68 | const localTab = page.locator('[data-testid="local-login-tab"]'); + 69 | await localTab.click(); + 70 | + 71 | // Try to login with invalid credentials + 72 | await page.fill('[data-testid="local-username-input"]', 'invalid_user'); + 73 | await page.fill('[data-testid="local-password-input"]', 'wrong_password'); + 74 | await page.click('[data-testid="local-login-submit"]'); + 75 | + 76 | // Should show error + 77 | const errorToast = page.locator('[data-testid="toast-error"]'); + 78 | await expect(errorToast).toBeVisible({ timeout: 5000 }); + 79 | }); + 80 | + 81 | test('should successfully login with valid local user credentials', async ({ page }) => { + 82 | // Switch to local user tab + 83 | const localTab = page.locator('[data-testid="local-login-tab"]'); + 84 | await localTab.click(); + 85 | + 86 | // Login with valid credentials + 87 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 88 | + 89 | // Verify authenticated + 90 | await assertions.assertUserAuthenticated(page, BASE_URL); + 91 | }); + 92 | + 93 | test('should show user identity after successful login', async ({ page }) => { + 94 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 95 | + 96 | // Check that user info is displayed + 97 | const userDisplay = page.locator('[data-testid="user-display"]'); + 98 | await expect(userDisplay).toBeVisible(); + 99 | + 100 | // Verify username is shown + 101 | const username = await userDisplay.textContent(); + 102 | expect(username).toContain(LOCAL_USERS.admin.username); + 103 | }); + 104 | + 105 | test('should maintain session across page reloads', async ({ page }) => { + 106 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 107 | await assertions.assertUserAuthenticated(page, BASE_URL); + 108 | + 109 | // Reload page + 110 | await page.reload(); + 111 | + 112 | // Should still be authenticated + 113 | const token = await auth.getAuthToken(page); + 114 | expect(token).toBeTruthy(); + 115 | + 116 | // Should not be on login page + 117 | await expect(page).not.toHaveURL(BASE_URL); + 118 | }); + 119 | + 120 | test('should support logout functionality', async ({ page }) => { + 121 | // Login first + 122 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 123 | await assertions.assertUserAuthenticated(page, BASE_URL); + 124 | + 125 | // Logout + 126 | await auth.logout(page, BASE_URL); + 127 | + 128 | // Should be back on login page + 129 | const identityOverlay = page.locator('[data-testid="identity-check-overlay"]'); + 130 | await expect(identityOverlay).toBeVisible({ timeout: 5000 }); + 131 | + 132 | // Token should be cleared + 133 | const token = await auth.getAuthToken(page); + 134 | expect(token).toBeFalsy(); + 135 | }); + 136 | + 137 | test('should show admin button for admin users', async ({ page }) => { + 138 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 139 | await assertions.assertUserAuthenticated(page, BASE_URL); + 140 | + 141 | // Admin button should be visible + 142 | const adminButton = page.locator('[data-testid="admin-button"]'); + 143 | await expect(adminButton).toBeVisible(); + 144 | }); + 145 | + 146 | test('should allow switching between LDAP and local login tabs', async ({ page }) => { + 147 | const ldapTab = page.locator('[data-testid="ldap-login-tab"]'); + 148 | const localTab = page.locator('[data-testid="local-login-tab"]'); + 149 | + 150 | // Start on LDAP tab + 151 | let ldapForm = page.locator('[data-testid="ldap-login-form"]'); +> 152 | await expect(ldapForm).toBeVisible(); + | ^ Error: expect(locator).toBeVisible() failed + 153 | + 154 | // Switch to local + 155 | await localTab.click(); + 156 | ldapForm = page.locator('[data-testid="ldap-login-form"]'); + 157 | await expect(ldapForm).not.toBeVisible(); + 158 | + 159 | // Switch back to LDAP + 160 | await ldapTab.click(); + 161 | ldapForm = page.locator('[data-testid="ldap-login-form"]'); + 162 | await expect(ldapForm).toBeVisible(); + 163 | }); + 164 | + 165 | test('should remember login preference on revisit', async ({ page, context }) => { + 166 | // Login with local user + 167 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 168 | + 169 | // Close and reopen browser + 170 | await page.close(); + 171 | const newPage = await context.newPage(); + 172 | await newPage.goto(BASE_URL); + 173 | + 174 | // Should still be authenticated + 175 | const authenticated = await auth.isAuthenticated(newPage); + 176 | expect(authenticated).toBe(true); + 177 | + 178 | await newPage.close(); + 179 | }); + 180 | + 181 | test('should handle network errors gracefully', async ({ page }) => { + 182 | // Enable offline mode + 183 | await page.context().setOffline(true); + 184 | + 185 | // Try to login + 186 | const usernameInput = page.locator('[data-testid="username-input"]'); + 187 | const passwordInput = page.locator('[data-testid="password-input"]'); + 188 | const submitButton = page.locator('[data-testid="login-submit"]'); + 189 | + 190 | await usernameInput.fill(LOCAL_USERS.admin.username); + 191 | await passwordInput.fill(LOCAL_USERS.admin.password); + 192 | await submitButton.click(); + 193 | + 194 | // Should show network error + 195 | const errorToast = page.locator('[data-testid="toast-error"]'); + 196 | await expect(errorToast).toBeVisible({ timeout: 5000 }); + 197 | + 198 | // Re-enable network + 199 | await page.context().setOffline(false); + 200 | }); + 201 | + 202 | test('should display user list in local login tab', async ({ page }) => { + 203 | const localTab = page.locator('[data-testid="local-login-tab"]'); + 204 | await localTab.click(); + 205 | + 206 | // User list should be visible + 207 | const userList = page.locator('[data-testid="user-list"]'); + 208 | await expect(userList).toBeVisible(); + 209 | + 210 | // Should have at least one user + 211 | const userItems = page.locator('[data-testid="user-list-item"]'); + 212 | expect(await userItems.count()).toBeGreaterThan(0); + 213 | }); + 214 | + 215 | test('should auto-login when clicking user from list', async ({ page }) => { + 216 | const localTab = page.locator('[data-testid="local-login-tab"]'); + 217 | await localTab.click(); + 218 | + 219 | // Click first user in list + 220 | const firstUser = page.locator('[data-testid="user-list-item"]').first(); + 221 | await firstUser.click(); + 222 | + 223 | // Password input should be focused + 224 | const passwordInput = page.locator('[data-testid="local-password-input"]'); + 225 | await expect(passwordInput).toBeFocused(); + 226 | }); + 227 | }); + 228 | +``` \ No newline at end of file diff --git a/frontend/test-results/1-login-Login-Workflow-sho-1065a-n-LDAP-and-local-login-tabs-chromium/test-failed-1.png b/frontend/test-results/1-login-Login-Workflow-sho-1065a-n-LDAP-and-local-login-tabs-chromium/test-failed-1.png new file mode 100644 index 00000000..b0b1cec4 Binary files /dev/null and b/frontend/test-results/1-login-Login-Workflow-sho-1065a-n-LDAP-and-local-login-tabs-chromium/test-failed-1.png differ diff --git a/frontend/test-results/1-login-Login-Workflow-sho-19d2a-hen-clicking-user-from-list-chromium/error-context.md b/frontend/test-results/1-login-Login-Workflow-sho-19d2a-hen-clicking-user-from-list-chromium/error-context.md new file mode 100644 index 00000000..69cac167 --- /dev/null +++ b/frontend/test-results/1-login-Login-Workflow-sho-19d2a-hen-clicking-user-from-list-chromium/error-context.md @@ -0,0 +1,169 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: 1-login.spec.ts >> Login Workflow >> should auto-login when clicking user from list +- Location: e2e/workflows/1-login.spec.ts:215:7 + +# Error details + +``` +Test timeout of 30000ms exceeded. +``` + +``` +Error: locator.click: Test timeout of 30000ms exceeded. +Call log: + - waiting for locator('[data-testid="user-list-item"]').first() + +``` + +# Page snapshot + +```yaml +- generic [ref=e1]: + - generic [ref=e3]: + - generic [ref=e4]: + - img [ref=e6] + - heading "Identity Check" [level=2] [ref=e9] + - paragraph [ref=e10]: Select operator profile or use direct login + - generic [ref=e12]: + - generic [ref=e13]: + - button [ref=e14] [cursor=pointer]: + - img [ref=e15] + - generic [ref=e18]: + - paragraph [ref=e19]: Logging in as + - paragraph [ref=e20]: Manual Input + - generic [ref=e21]: + - text: Username + - generic [ref=e22]: + - img [ref=e23] + - textbox "Admin" [ref=e26] + - generic [ref=e27]: + - text: Password + - generic [ref=e28]: + - img [ref=e29] + - textbox "Enter password" [active] [ref=e32] + - button "Verify Identity" [ref=e33] [cursor=pointer] + - button "Open Next.js Dev Tools" [ref=e39] [cursor=pointer]: + - img [ref=e40] + - alert [ref=e43] +``` + +# Test source + +```ts + 121 | // Login first + 122 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 123 | await assertions.assertUserAuthenticated(page, BASE_URL); + 124 | + 125 | // Logout + 126 | await auth.logout(page, BASE_URL); + 127 | + 128 | // Should be back on login page + 129 | const identityOverlay = page.locator('[data-testid="identity-check-overlay"]'); + 130 | await expect(identityOverlay).toBeVisible({ timeout: 5000 }); + 131 | + 132 | // Token should be cleared + 133 | const token = await auth.getAuthToken(page); + 134 | expect(token).toBeFalsy(); + 135 | }); + 136 | + 137 | test('should show admin button for admin users', async ({ page }) => { + 138 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 139 | await assertions.assertUserAuthenticated(page, BASE_URL); + 140 | + 141 | // Admin button should be visible + 142 | const adminButton = page.locator('[data-testid="admin-button"]'); + 143 | await expect(adminButton).toBeVisible(); + 144 | }); + 145 | + 146 | test('should allow switching between LDAP and local login tabs', async ({ page }) => { + 147 | const ldapTab = page.locator('[data-testid="ldap-login-tab"]'); + 148 | const localTab = page.locator('[data-testid="local-login-tab"]'); + 149 | + 150 | // Start on LDAP tab + 151 | let ldapForm = page.locator('[data-testid="ldap-login-form"]'); + 152 | await expect(ldapForm).toBeVisible(); + 153 | + 154 | // Switch to local + 155 | await localTab.click(); + 156 | ldapForm = page.locator('[data-testid="ldap-login-form"]'); + 157 | await expect(ldapForm).not.toBeVisible(); + 158 | + 159 | // Switch back to LDAP + 160 | await ldapTab.click(); + 161 | ldapForm = page.locator('[data-testid="ldap-login-form"]'); + 162 | await expect(ldapForm).toBeVisible(); + 163 | }); + 164 | + 165 | test('should remember login preference on revisit', async ({ page, context }) => { + 166 | // Login with local user + 167 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 168 | + 169 | // Close and reopen browser + 170 | await page.close(); + 171 | const newPage = await context.newPage(); + 172 | await newPage.goto(BASE_URL); + 173 | + 174 | // Should still be authenticated + 175 | const authenticated = await auth.isAuthenticated(newPage); + 176 | expect(authenticated).toBe(true); + 177 | + 178 | await newPage.close(); + 179 | }); + 180 | + 181 | test('should handle network errors gracefully', async ({ page }) => { + 182 | // Enable offline mode + 183 | await page.context().setOffline(true); + 184 | + 185 | // Try to login + 186 | const usernameInput = page.locator('[data-testid="username-input"]'); + 187 | const passwordInput = page.locator('[data-testid="password-input"]'); + 188 | const submitButton = page.locator('[data-testid="login-submit"]'); + 189 | + 190 | await usernameInput.fill(LOCAL_USERS.admin.username); + 191 | await passwordInput.fill(LOCAL_USERS.admin.password); + 192 | await submitButton.click(); + 193 | + 194 | // Should show network error + 195 | const errorToast = page.locator('[data-testid="toast-error"]'); + 196 | await expect(errorToast).toBeVisible({ timeout: 5000 }); + 197 | + 198 | // Re-enable network + 199 | await page.context().setOffline(false); + 200 | }); + 201 | + 202 | test('should display user list in local login tab', async ({ page }) => { + 203 | const localTab = page.locator('[data-testid="local-login-tab"]'); + 204 | await localTab.click(); + 205 | + 206 | // User list should be visible + 207 | const userList = page.locator('[data-testid="user-list"]'); + 208 | await expect(userList).toBeVisible(); + 209 | + 210 | // Should have at least one user + 211 | const userItems = page.locator('[data-testid="user-list-item"]'); + 212 | expect(await userItems.count()).toBeGreaterThan(0); + 213 | }); + 214 | + 215 | test('should auto-login when clicking user from list', async ({ page }) => { + 216 | const localTab = page.locator('[data-testid="local-login-tab"]'); + 217 | await localTab.click(); + 218 | + 219 | // Click first user in list + 220 | const firstUser = page.locator('[data-testid="user-list-item"]').first(); +> 221 | await firstUser.click(); + | ^ Error: locator.click: Test timeout of 30000ms exceeded. + 222 | + 223 | // Password input should be focused + 224 | const passwordInput = page.locator('[data-testid="local-password-input"]'); + 225 | await expect(passwordInput).toBeFocused(); + 226 | }); + 227 | }); + 228 | +``` \ No newline at end of file diff --git a/frontend/test-results/1-login-Login-Workflow-sho-19d2a-hen-clicking-user-from-list-chromium/test-failed-1.png b/frontend/test-results/1-login-Login-Workflow-sho-19d2a-hen-clicking-user-from-list-chromium/test-failed-1.png new file mode 100644 index 00000000..98a7abd7 Binary files /dev/null and b/frontend/test-results/1-login-Login-Workflow-sho-19d2a-hen-clicking-user-from-list-chromium/test-failed-1.png differ diff --git a/frontend/test-results/1-login-Login-Workflow-sho-1b0d7-alid-local-user-credentials-chromium/error-context.md b/frontend/test-results/1-login-Login-Workflow-sho-1b0d7-alid-local-user-credentials-chromium/error-context.md new file mode 100644 index 00000000..952ef37c --- /dev/null +++ b/frontend/test-results/1-login-Login-Workflow-sho-1b0d7-alid-local-user-credentials-chromium/error-context.md @@ -0,0 +1,224 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: 1-login.spec.ts >> Login Workflow >> should successfully login with valid local user credentials +- Location: e2e/workflows/1-login.spec.ts:81:7 + +# Error details + +``` +Test timeout of 30000ms exceeded. +``` + +``` +Error: page.fill: Test timeout of 30000ms exceeded. +Call log: + - waiting for locator('[data-testid="local-username-input"]') + +``` + +# Page snapshot + +```yaml +- generic [active] [ref=e1]: + - generic [ref=e3]: + - generic [ref=e4]: + - img [ref=e6] + - heading "Identity Check" [level=2] [ref=e9] + - paragraph [ref=e10]: Select operator profile or use direct login + - generic [ref=e11]: + - button "newuser user" [ref=e12] [cursor=pointer]: + - generic [ref=e13]: + - img [ref=e15] + - generic [ref=e18]: + - paragraph [ref=e19]: newuser + - paragraph [ref=e20]: user + - img [ref=e21] + - generic [ref=e23]: + - button "Enterprise" [ref=e24] [cursor=pointer]: + - img [ref=e25] + - text: Enterprise + - button "Manual Login" [ref=e28] [cursor=pointer]: + - img [ref=e29] + - text: Manual Login + - button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]: + - img [ref=e38] + - alert [ref=e41] +``` + +# Test source + +```ts + 1 | import { Page, BrowserContext } from '@playwright/test'; + 2 | + 3 | /** + 4 | * Authentication fixture for E2E tests + 5 | * Handles login flows and session management + 6 | */ + 7 | + 8 | export interface LoginCredentials { + 9 | username: string; + 10 | password: string; + 11 | } + 12 | + 13 | export interface AuthSession { + 14 | token: string; + 15 | userId: string; + 16 | email: string; + 17 | isAdmin: boolean; + 18 | } + 19 | + 20 | /** + 21 | * Perform LDAP authentication + 22 | */ + 23 | export async function loginWithLdap( + 24 | page: Page, + 25 | credentials: LoginCredentials, + 26 | baseUrl: string = 'http://localhost' + 27 | ): Promise { + 28 | await page.goto(`${baseUrl}`); + 29 | + 30 | // Wait for login form to appear + 31 | await page.waitForSelector('[data-testid="login-form"]', { timeout: 5000 }); + 32 | + 33 | // Fill in username + 34 | await page.fill('[data-testid="username-input"]', credentials.username); + 35 | + 36 | // Fill in password + 37 | await page.fill('[data-testid="password-input"]', credentials.password); + 38 | + 39 | // Click login button + 40 | await page.click('[data-testid="login-submit"]'); + 41 | + 42 | // Wait for navigation to inventory page + 43 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' }); + 44 | } + 45 | + 46 | /** + 47 | * Perform local user authentication + 48 | */ + 49 | export async function loginWithLocalUser( + 50 | page: Page, + 51 | credentials: LoginCredentials, + 52 | baseUrl: string = 'http://localhost' + 53 | ): Promise { + 54 | await page.goto(`${baseUrl}`); + 55 | + 56 | // Wait for identity check overlay + 57 | await page.waitForSelector('[data-testid="identity-check-overlay"]', { timeout: 5000 }); + 58 | + 59 | // Click "Local User" tab if it exists + 60 | const localUserTab = page.locator('[data-testid="local-user-tab"]'); + 61 | if (await localUserTab.isVisible()) { + 62 | await localUserTab.click(); + 63 | } + 64 | + 65 | // Fill in username +> 66 | await page.fill('[data-testid="local-username-input"]', credentials.username); + | ^ Error: page.fill: Test timeout of 30000ms exceeded. + 67 | + 68 | // Fill in password + 69 | await page.fill('[data-testid="local-password-input"]', credentials.password); + 70 | + 71 | // Click login button + 72 | await page.click('[data-testid="local-login-submit"]'); + 73 | + 74 | // Wait for navigation to inventory page + 75 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' }); + 76 | } + 77 | + 78 | /** + 79 | * Logout from the application + 80 | */ + 81 | export async function logout(page: Page, baseUrl: string = 'http://localhost'): Promise { + 82 | // Click logout button or menu + 83 | const logoutButton = page.locator('[data-testid="logout-button"]'); + 84 | if (await logoutButton.isVisible()) { + 85 | await logoutButton.click(); + 86 | } else { + 87 | // Try finding it in menu + 88 | const menu = page.locator('[data-testid="user-menu"]'); + 89 | if (await menu.isVisible()) { + 90 | await menu.click(); + 91 | await page.click('[data-testid="logout-menu-item"]'); + 92 | } + 93 | } + 94 | + 95 | // Wait for redirect to login + 96 | await page.waitForURL(`${baseUrl}/*`, { timeout: 5000 }); + 97 | } + 98 | + 99 | /** + 100 | * Get authentication token from local storage + 101 | */ + 102 | export async function getAuthToken(page: Page): Promise { + 103 | const token = await page.evaluate(() => { + 104 | return localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token'); + 105 | }); + 106 | return token; + 107 | } + 108 | + 109 | /** + 110 | * Set authentication token in local storage + 111 | */ + 112 | export async function setAuthToken(page: Page, token: string): Promise { + 113 | await page.evaluate((t) => { + 114 | localStorage.setItem('auth_token', t); + 115 | }, token); + 116 | } + 117 | + 118 | /** + 119 | * Clear authentication data + 120 | */ + 121 | export async function clearAuth(page: Page): Promise { + 122 | await page.evaluate(() => { + 123 | localStorage.removeItem('auth_token'); + 124 | sessionStorage.removeItem('auth_token'); + 125 | localStorage.removeItem('user_data'); + 126 | sessionStorage.removeItem('user_data'); + 127 | }); + 128 | } + 129 | + 130 | /** + 131 | * Check if user is authenticated + 132 | */ + 133 | export async function isAuthenticated(page: Page): Promise { + 134 | const token = await getAuthToken(page); + 135 | return !!token; + 136 | } + 137 | + 138 | /** + 139 | * Get current user info from page + 140 | */ + 141 | export async function getCurrentUser(page: Page): Promise<{ username: string; email: string } | null> { + 142 | try { + 143 | const userData = await page.evaluate(() => { + 144 | const stored = localStorage.getItem('user_data') || sessionStorage.getItem('user_data'); + 145 | return stored ? JSON.parse(stored) : null; + 146 | }); + 147 | return userData; + 148 | } catch { + 149 | return null; + 150 | } + 151 | } + 152 | + 153 | /** + 154 | * Wait for authentication to complete + 155 | */ + 156 | export async function waitForAuth(page: Page, timeout: number = 5000): Promise { + 157 | // Wait for either: + 158 | // 1. Token to be in storage + 159 | // 2. Navigation to happen + 160 | // 3. Auth overlay to disappear + 161 | await page.waitForFunction( + 162 | () => { + 163 | const token = + 164 | typeof window !== 'undefined' + 165 | ? localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token') + 166 | : null; +``` \ No newline at end of file diff --git a/frontend/test-results/1-login-Login-Workflow-sho-1b0d7-alid-local-user-credentials-chromium/test-failed-1.png b/frontend/test-results/1-login-Login-Workflow-sho-1b0d7-alid-local-user-credentials-chromium/test-failed-1.png new file mode 100644 index 00000000..a0f367fa Binary files /dev/null and b/frontend/test-results/1-login-Login-Workflow-sho-1b0d7-alid-local-user-credentials-chromium/test-failed-1.png differ diff --git a/frontend/test-results/1-login-Login-Workflow-sho-1def4-alid-local-user-credentials-chromium/error-context.md b/frontend/test-results/1-login-Login-Workflow-sho-1def4-alid-local-user-credentials-chromium/error-context.md new file mode 100644 index 00000000..d14eef79 --- /dev/null +++ b/frontend/test-results/1-login-Login-Workflow-sho-1def4-alid-local-user-credentials-chromium/error-context.md @@ -0,0 +1,233 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: 1-login.spec.ts >> Login Workflow >> should reject invalid local user credentials +- Location: e2e/workflows/1-login.spec.ts:66:7 + +# Error details + +``` +Test timeout of 30000ms exceeded. +``` + +``` +Error: page.fill: Test timeout of 30000ms exceeded. +Call log: + - waiting for locator('[data-testid="local-username-input"]') + +``` + +# Page snapshot + +```yaml +- generic [ref=e1]: + - generic [ref=e3]: + - generic [ref=e4]: + - img [ref=e6] + - heading "Identity Check" [level=2] [ref=e9] + - paragraph [ref=e10]: Select operator profile or use direct login + - generic [ref=e12]: + - generic [ref=e13]: + - button [ref=e14] [cursor=pointer]: + - img [ref=e15] + - generic [ref=e18]: + - paragraph [ref=e19]: Logging in as + - paragraph [ref=e20]: Manual Input + - generic [ref=e21]: + - text: Username + - generic [ref=e22]: + - img [ref=e23] + - textbox "Admin" [ref=e26] + - generic [ref=e27]: + - text: Password + - generic [ref=e28]: + - img [ref=e29] + - textbox "Enter password" [active] [ref=e32] + - button "Verify Identity" [ref=e33] [cursor=pointer] + - button "Open Next.js Dev Tools" [ref=e39] [cursor=pointer]: + - img [ref=e40] + - alert [ref=e43] +``` + +# Test source + +```ts + 1 | import { test, expect } from '@playwright/test'; + 2 | import * as auth from '../fixtures/auth'; + 3 | import * as assertions from '../utils/assertions'; + 4 | import { LDAP_USERS, LOCAL_USERS } from '../fixtures/test-data'; + 5 | + 6 | test.describe('Login Workflow', () => { + 7 | const BASE_URL = process.env.BASE_URL || 'http://localhost:8917'; + 8 | + 9 | test.beforeEach(async ({ page }) => { + 10 | // Navigate to login page + 11 | await page.goto(BASE_URL); + 12 | // Wait for identity check overlay + 13 | await page.waitForSelector('[data-testid="identity-check-overlay"]', { timeout: 10000 }); + 14 | }); + 15 | + 16 | test('should display identity check overlay on app load', async ({ page }) => { + 17 | const overlay = page.locator('[data-testid="identity-check-overlay"]'); + 18 | await expect(overlay).toBeVisible(); + 19 | + 20 | // Verify tabs are available + 21 | const ldapTab = page.locator('[data-testid="ldap-login-tab"]'); + 22 | const localTab = page.locator('[data-testid="local-login-tab"]'); + 23 | + 24 | await expect(ldapTab).toBeVisible(); + 25 | await expect(localTab).toBeVisible(); + 26 | }); + 27 | + 28 | test('should display login form with username and password fields', async ({ page }) => { + 29 | await assertions.assertLoginFormVisible(page); + 30 | }); + 31 | + 32 | test('should reject empty login credentials', async ({ page }) => { + 33 | const submitButton = page.locator('[data-testid="login-submit"]'); + 34 | await submitButton.click(); + 35 | + 36 | // Should show validation error + 37 | const errorMessage = page.locator('[data-testid="toast-error"]'); + 38 | await expect(errorMessage).toBeVisible({ timeout: 3000 }); + 39 | }); + 40 | + 41 | test('should reject invalid LDAP credentials', async ({ page }) => { + 42 | await auth.loginWithLdap(page, LDAP_USERS.invalid, BASE_URL); + 43 | + 44 | // Should show error message + 45 | const errorToast = page.locator('[data-testid="toast-error"]'); + 46 | await expect(errorToast).toBeVisible({ timeout: 5000 }); + 47 | + 48 | // Should still be on login page + 49 | await expect(page).toHaveURL(BASE_URL); + 50 | }); + 51 | + 52 | test('should successfully login with valid LDAP credentials', async ({ page }) => { + 53 | // Skip if LDAP is not configured in test environment + 54 | test.skip(process.env.SKIP_LDAP === 'true', 'LDAP not available in test environment'); + 55 | + 56 | await auth.loginWithLdap(page, LDAP_USERS.valid, BASE_URL); + 57 | + 58 | // Verify we're authenticated + 59 | await assertions.assertUserAuthenticated(page, BASE_URL); + 60 | + 61 | // Verify token is stored + 62 | const token = await auth.getAuthToken(page); + 63 | expect(token).toBeTruthy(); + 64 | }); + 65 | + 66 | test('should reject invalid local user credentials', async ({ page }) => { + 67 | // Switch to local user tab + 68 | const localTab = page.locator('[data-testid="local-login-tab"]'); + 69 | await localTab.click(); + 70 | + 71 | // Try to login with invalid credentials +> 72 | await page.fill('[data-testid="local-username-input"]', 'invalid_user'); + | ^ Error: page.fill: Test timeout of 30000ms exceeded. + 73 | await page.fill('[data-testid="local-password-input"]', 'wrong_password'); + 74 | await page.click('[data-testid="local-login-submit"]'); + 75 | + 76 | // Should show error + 77 | const errorToast = page.locator('[data-testid="toast-error"]'); + 78 | await expect(errorToast).toBeVisible({ timeout: 5000 }); + 79 | }); + 80 | + 81 | test('should successfully login with valid local user credentials', async ({ page }) => { + 82 | // Switch to local user tab + 83 | const localTab = page.locator('[data-testid="local-login-tab"]'); + 84 | await localTab.click(); + 85 | + 86 | // Login with valid credentials + 87 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 88 | + 89 | // Verify authenticated + 90 | await assertions.assertUserAuthenticated(page, BASE_URL); + 91 | }); + 92 | + 93 | test('should show user identity after successful login', async ({ page }) => { + 94 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 95 | + 96 | // Check that user info is displayed + 97 | const userDisplay = page.locator('[data-testid="user-display"]'); + 98 | await expect(userDisplay).toBeVisible(); + 99 | + 100 | // Verify username is shown + 101 | const username = await userDisplay.textContent(); + 102 | expect(username).toContain(LOCAL_USERS.admin.username); + 103 | }); + 104 | + 105 | test('should maintain session across page reloads', async ({ page }) => { + 106 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 107 | await assertions.assertUserAuthenticated(page, BASE_URL); + 108 | + 109 | // Reload page + 110 | await page.reload(); + 111 | + 112 | // Should still be authenticated + 113 | const token = await auth.getAuthToken(page); + 114 | expect(token).toBeTruthy(); + 115 | + 116 | // Should not be on login page + 117 | await expect(page).not.toHaveURL(BASE_URL); + 118 | }); + 119 | + 120 | test('should support logout functionality', async ({ page }) => { + 121 | // Login first + 122 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 123 | await assertions.assertUserAuthenticated(page, BASE_URL); + 124 | + 125 | // Logout + 126 | await auth.logout(page, BASE_URL); + 127 | + 128 | // Should be back on login page + 129 | const identityOverlay = page.locator('[data-testid="identity-check-overlay"]'); + 130 | await expect(identityOverlay).toBeVisible({ timeout: 5000 }); + 131 | + 132 | // Token should be cleared + 133 | const token = await auth.getAuthToken(page); + 134 | expect(token).toBeFalsy(); + 135 | }); + 136 | + 137 | test('should show admin button for admin users', async ({ page }) => { + 138 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 139 | await assertions.assertUserAuthenticated(page, BASE_URL); + 140 | + 141 | // Admin button should be visible + 142 | const adminButton = page.locator('[data-testid="admin-button"]'); + 143 | await expect(adminButton).toBeVisible(); + 144 | }); + 145 | + 146 | test('should allow switching between LDAP and local login tabs', async ({ page }) => { + 147 | const ldapTab = page.locator('[data-testid="ldap-login-tab"]'); + 148 | const localTab = page.locator('[data-testid="local-login-tab"]'); + 149 | + 150 | // Start on LDAP tab + 151 | let ldapForm = page.locator('[data-testid="ldap-login-form"]'); + 152 | await expect(ldapForm).toBeVisible(); + 153 | + 154 | // Switch to local + 155 | await localTab.click(); + 156 | ldapForm = page.locator('[data-testid="ldap-login-form"]'); + 157 | await expect(ldapForm).not.toBeVisible(); + 158 | + 159 | // Switch back to LDAP + 160 | await ldapTab.click(); + 161 | ldapForm = page.locator('[data-testid="ldap-login-form"]'); + 162 | await expect(ldapForm).toBeVisible(); + 163 | }); + 164 | + 165 | test('should remember login preference on revisit', async ({ page, context }) => { + 166 | // Login with local user + 167 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 168 | + 169 | // Close and reopen browser + 170 | await page.close(); + 171 | const newPage = await context.newPage(); + 172 | await newPage.goto(BASE_URL); +``` \ No newline at end of file diff --git a/frontend/test-results/1-login-Login-Workflow-sho-1def4-alid-local-user-credentials-chromium/test-failed-1.png b/frontend/test-results/1-login-Login-Workflow-sho-1def4-alid-local-user-credentials-chromium/test-failed-1.png new file mode 100644 index 00000000..98a7abd7 Binary files /dev/null and b/frontend/test-results/1-login-Login-Workflow-sho-1def4-alid-local-user-credentials-chromium/test-failed-1.png differ diff --git a/frontend/test-results/1-login-Login-Workflow-sho-24db1-with-valid-LDAP-credentials-chromium/error-context.md b/frontend/test-results/1-login-Login-Workflow-sho-24db1-with-valid-LDAP-credentials-chromium/error-context.md new file mode 100644 index 00000000..128321ad --- /dev/null +++ b/frontend/test-results/1-login-Login-Workflow-sho-24db1-with-valid-LDAP-credentials-chromium/error-context.md @@ -0,0 +1,187 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: 1-login.spec.ts >> Login Workflow >> should successfully login with valid LDAP credentials +- Location: e2e/workflows/1-login.spec.ts:52:7 + +# Error details + +``` +TimeoutError: page.waitForSelector: Timeout 5000ms exceeded. +Call log: + - waiting for locator('[data-testid="login-form"]') to be visible + - waiting for" http://localhost:8917/login" navigation to finish... + - navigated to "http://localhost:8917/login" + +``` + +# Page snapshot + +```yaml +- generic [active] [ref=e1]: + - generic [ref=e3]: + - generic [ref=e4]: + - img [ref=e6] + - heading "Identity Check" [level=2] [ref=e9] + - paragraph [ref=e10]: Select operator profile or use direct login + - generic [ref=e11]: + - button "newuser user" [ref=e12] [cursor=pointer]: + - generic [ref=e13]: + - img [ref=e15] + - generic [ref=e18]: + - paragraph [ref=e19]: newuser + - paragraph [ref=e20]: user + - img [ref=e21] + - generic [ref=e23]: + - button "Enterprise" [ref=e24] [cursor=pointer]: + - img [ref=e25] + - text: Enterprise + - button "Manual Login" [ref=e28] [cursor=pointer]: + - img [ref=e29] + - text: Manual Login + - button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]: + - img [ref=e38] + - alert [ref=e41] +``` + +# Test source + +```ts + 1 | import { Page, BrowserContext } from '@playwright/test'; + 2 | + 3 | /** + 4 | * Authentication fixture for E2E tests + 5 | * Handles login flows and session management + 6 | */ + 7 | + 8 | export interface LoginCredentials { + 9 | username: string; + 10 | password: string; + 11 | } + 12 | + 13 | export interface AuthSession { + 14 | token: string; + 15 | userId: string; + 16 | email: string; + 17 | isAdmin: boolean; + 18 | } + 19 | + 20 | /** + 21 | * Perform LDAP authentication + 22 | */ + 23 | export async function loginWithLdap( + 24 | page: Page, + 25 | credentials: LoginCredentials, + 26 | baseUrl: string = 'http://localhost' + 27 | ): Promise { + 28 | await page.goto(`${baseUrl}`); + 29 | + 30 | // Wait for login form to appear +> 31 | await page.waitForSelector('[data-testid="login-form"]', { timeout: 5000 }); + | ^ TimeoutError: page.waitForSelector: Timeout 5000ms exceeded. + 32 | + 33 | // Fill in username + 34 | await page.fill('[data-testid="username-input"]', credentials.username); + 35 | + 36 | // Fill in password + 37 | await page.fill('[data-testid="password-input"]', credentials.password); + 38 | + 39 | // Click login button + 40 | await page.click('[data-testid="login-submit"]'); + 41 | + 42 | // Wait for navigation to inventory page + 43 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' }); + 44 | } + 45 | + 46 | /** + 47 | * Perform local user authentication + 48 | */ + 49 | export async function loginWithLocalUser( + 50 | page: Page, + 51 | credentials: LoginCredentials, + 52 | baseUrl: string = 'http://localhost' + 53 | ): Promise { + 54 | await page.goto(`${baseUrl}`); + 55 | + 56 | // Wait for identity check overlay + 57 | await page.waitForSelector('[data-testid="identity-check-overlay"]', { timeout: 5000 }); + 58 | + 59 | // Click "Local User" tab if it exists + 60 | const localUserTab = page.locator('[data-testid="local-user-tab"]'); + 61 | if (await localUserTab.isVisible()) { + 62 | await localUserTab.click(); + 63 | } + 64 | + 65 | // Fill in username + 66 | await page.fill('[data-testid="local-username-input"]', credentials.username); + 67 | + 68 | // Fill in password + 69 | await page.fill('[data-testid="local-password-input"]', credentials.password); + 70 | + 71 | // Click login button + 72 | await page.click('[data-testid="local-login-submit"]'); + 73 | + 74 | // Wait for navigation to inventory page + 75 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' }); + 76 | } + 77 | + 78 | /** + 79 | * Logout from the application + 80 | */ + 81 | export async function logout(page: Page, baseUrl: string = 'http://localhost'): Promise { + 82 | // Click logout button or menu + 83 | const logoutButton = page.locator('[data-testid="logout-button"]'); + 84 | if (await logoutButton.isVisible()) { + 85 | await logoutButton.click(); + 86 | } else { + 87 | // Try finding it in menu + 88 | const menu = page.locator('[data-testid="user-menu"]'); + 89 | if (await menu.isVisible()) { + 90 | await menu.click(); + 91 | await page.click('[data-testid="logout-menu-item"]'); + 92 | } + 93 | } + 94 | + 95 | // Wait for redirect to login + 96 | await page.waitForURL(`${baseUrl}/*`, { timeout: 5000 }); + 97 | } + 98 | + 99 | /** + 100 | * Get authentication token from local storage + 101 | */ + 102 | export async function getAuthToken(page: Page): Promise { + 103 | const token = await page.evaluate(() => { + 104 | return localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token'); + 105 | }); + 106 | return token; + 107 | } + 108 | + 109 | /** + 110 | * Set authentication token in local storage + 111 | */ + 112 | export async function setAuthToken(page: Page, token: string): Promise { + 113 | await page.evaluate((t) => { + 114 | localStorage.setItem('auth_token', t); + 115 | }, token); + 116 | } + 117 | + 118 | /** + 119 | * Clear authentication data + 120 | */ + 121 | export async function clearAuth(page: Page): Promise { + 122 | await page.evaluate(() => { + 123 | localStorage.removeItem('auth_token'); + 124 | sessionStorage.removeItem('auth_token'); + 125 | localStorage.removeItem('user_data'); + 126 | sessionStorage.removeItem('user_data'); + 127 | }); + 128 | } + 129 | + 130 | /** + 131 | * Check if user is authenticated +``` \ No newline at end of file diff --git a/frontend/test-results/1-login-Login-Workflow-sho-24db1-with-valid-LDAP-credentials-chromium/test-failed-1.png b/frontend/test-results/1-login-Login-Workflow-sho-24db1-with-valid-LDAP-credentials-chromium/test-failed-1.png new file mode 100644 index 00000000..b0b1cec4 Binary files /dev/null and b/frontend/test-results/1-login-Login-Workflow-sho-24db1-with-valid-LDAP-credentials-chromium/test-failed-1.png differ diff --git a/frontend/test-results/1-login-Login-Workflow-sho-2cf04-session-across-page-reloads-chromium/error-context.md b/frontend/test-results/1-login-Login-Workflow-sho-2cf04-session-across-page-reloads-chromium/error-context.md new file mode 100644 index 00000000..e970807f --- /dev/null +++ b/frontend/test-results/1-login-Login-Workflow-sho-2cf04-session-across-page-reloads-chromium/error-context.md @@ -0,0 +1,224 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: 1-login.spec.ts >> Login Workflow >> should maintain session across page reloads +- Location: e2e/workflows/1-login.spec.ts:105:7 + +# Error details + +``` +Test timeout of 30000ms exceeded. +``` + +``` +Error: page.fill: Test timeout of 30000ms exceeded. +Call log: + - waiting for locator('[data-testid="local-username-input"]') + +``` + +# Page snapshot + +```yaml +- generic [active] [ref=e1]: + - generic [ref=e3]: + - generic [ref=e4]: + - img [ref=e6] + - heading "Identity Check" [level=2] [ref=e9] + - paragraph [ref=e10]: Select operator profile or use direct login + - generic [ref=e11]: + - button "newuser user" [ref=e12] [cursor=pointer]: + - generic [ref=e13]: + - img [ref=e15] + - generic [ref=e18]: + - paragraph [ref=e19]: newuser + - paragraph [ref=e20]: user + - img [ref=e21] + - generic [ref=e23]: + - button "Enterprise" [ref=e24] [cursor=pointer]: + - img [ref=e25] + - text: Enterprise + - button "Manual Login" [ref=e28] [cursor=pointer]: + - img [ref=e29] + - text: Manual Login + - button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]: + - img [ref=e38] + - alert [ref=e41] +``` + +# Test source + +```ts + 1 | import { Page, BrowserContext } from '@playwright/test'; + 2 | + 3 | /** + 4 | * Authentication fixture for E2E tests + 5 | * Handles login flows and session management + 6 | */ + 7 | + 8 | export interface LoginCredentials { + 9 | username: string; + 10 | password: string; + 11 | } + 12 | + 13 | export interface AuthSession { + 14 | token: string; + 15 | userId: string; + 16 | email: string; + 17 | isAdmin: boolean; + 18 | } + 19 | + 20 | /** + 21 | * Perform LDAP authentication + 22 | */ + 23 | export async function loginWithLdap( + 24 | page: Page, + 25 | credentials: LoginCredentials, + 26 | baseUrl: string = 'http://localhost' + 27 | ): Promise { + 28 | await page.goto(`${baseUrl}`); + 29 | + 30 | // Wait for login form to appear + 31 | await page.waitForSelector('[data-testid="login-form"]', { timeout: 5000 }); + 32 | + 33 | // Fill in username + 34 | await page.fill('[data-testid="username-input"]', credentials.username); + 35 | + 36 | // Fill in password + 37 | await page.fill('[data-testid="password-input"]', credentials.password); + 38 | + 39 | // Click login button + 40 | await page.click('[data-testid="login-submit"]'); + 41 | + 42 | // Wait for navigation to inventory page + 43 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' }); + 44 | } + 45 | + 46 | /** + 47 | * Perform local user authentication + 48 | */ + 49 | export async function loginWithLocalUser( + 50 | page: Page, + 51 | credentials: LoginCredentials, + 52 | baseUrl: string = 'http://localhost' + 53 | ): Promise { + 54 | await page.goto(`${baseUrl}`); + 55 | + 56 | // Wait for identity check overlay + 57 | await page.waitForSelector('[data-testid="identity-check-overlay"]', { timeout: 5000 }); + 58 | + 59 | // Click "Local User" tab if it exists + 60 | const localUserTab = page.locator('[data-testid="local-user-tab"]'); + 61 | if (await localUserTab.isVisible()) { + 62 | await localUserTab.click(); + 63 | } + 64 | + 65 | // Fill in username +> 66 | await page.fill('[data-testid="local-username-input"]', credentials.username); + | ^ Error: page.fill: Test timeout of 30000ms exceeded. + 67 | + 68 | // Fill in password + 69 | await page.fill('[data-testid="local-password-input"]', credentials.password); + 70 | + 71 | // Click login button + 72 | await page.click('[data-testid="local-login-submit"]'); + 73 | + 74 | // Wait for navigation to inventory page + 75 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' }); + 76 | } + 77 | + 78 | /** + 79 | * Logout from the application + 80 | */ + 81 | export async function logout(page: Page, baseUrl: string = 'http://localhost'): Promise { + 82 | // Click logout button or menu + 83 | const logoutButton = page.locator('[data-testid="logout-button"]'); + 84 | if (await logoutButton.isVisible()) { + 85 | await logoutButton.click(); + 86 | } else { + 87 | // Try finding it in menu + 88 | const menu = page.locator('[data-testid="user-menu"]'); + 89 | if (await menu.isVisible()) { + 90 | await menu.click(); + 91 | await page.click('[data-testid="logout-menu-item"]'); + 92 | } + 93 | } + 94 | + 95 | // Wait for redirect to login + 96 | await page.waitForURL(`${baseUrl}/*`, { timeout: 5000 }); + 97 | } + 98 | + 99 | /** + 100 | * Get authentication token from local storage + 101 | */ + 102 | export async function getAuthToken(page: Page): Promise { + 103 | const token = await page.evaluate(() => { + 104 | return localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token'); + 105 | }); + 106 | return token; + 107 | } + 108 | + 109 | /** + 110 | * Set authentication token in local storage + 111 | */ + 112 | export async function setAuthToken(page: Page, token: string): Promise { + 113 | await page.evaluate((t) => { + 114 | localStorage.setItem('auth_token', t); + 115 | }, token); + 116 | } + 117 | + 118 | /** + 119 | * Clear authentication data + 120 | */ + 121 | export async function clearAuth(page: Page): Promise { + 122 | await page.evaluate(() => { + 123 | localStorage.removeItem('auth_token'); + 124 | sessionStorage.removeItem('auth_token'); + 125 | localStorage.removeItem('user_data'); + 126 | sessionStorage.removeItem('user_data'); + 127 | }); + 128 | } + 129 | + 130 | /** + 131 | * Check if user is authenticated + 132 | */ + 133 | export async function isAuthenticated(page: Page): Promise { + 134 | const token = await getAuthToken(page); + 135 | return !!token; + 136 | } + 137 | + 138 | /** + 139 | * Get current user info from page + 140 | */ + 141 | export async function getCurrentUser(page: Page): Promise<{ username: string; email: string } | null> { + 142 | try { + 143 | const userData = await page.evaluate(() => { + 144 | const stored = localStorage.getItem('user_data') || sessionStorage.getItem('user_data'); + 145 | return stored ? JSON.parse(stored) : null; + 146 | }); + 147 | return userData; + 148 | } catch { + 149 | return null; + 150 | } + 151 | } + 152 | + 153 | /** + 154 | * Wait for authentication to complete + 155 | */ + 156 | export async function waitForAuth(page: Page, timeout: number = 5000): Promise { + 157 | // Wait for either: + 158 | // 1. Token to be in storage + 159 | // 2. Navigation to happen + 160 | // 3. Auth overlay to disappear + 161 | await page.waitForFunction( + 162 | () => { + 163 | const token = + 164 | typeof window !== 'undefined' + 165 | ? localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token') + 166 | : null; +``` \ No newline at end of file diff --git a/frontend/test-results/1-login-Login-Workflow-sho-2cf04-session-across-page-reloads-chromium/test-failed-1.png b/frontend/test-results/1-login-Login-Workflow-sho-2cf04-session-across-page-reloads-chromium/test-failed-1.png new file mode 100644 index 00000000..b0b1cec4 Binary files /dev/null and b/frontend/test-results/1-login-Login-Workflow-sho-2cf04-session-across-page-reloads-chromium/test-failed-1.png differ diff --git a/frontend/test-results/1-login-Login-Workflow-sho-6d596-sername-and-password-fields-chromium/error-context.md b/frontend/test-results/1-login-Login-Workflow-sho-6d596-sername-and-password-fields-chromium/error-context.md new file mode 100644 index 00000000..abff9b1c --- /dev/null +++ b/frontend/test-results/1-login-Login-Workflow-sho-6d596-sername-and-password-fields-chromium/error-context.md @@ -0,0 +1,213 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: 1-login.spec.ts >> Login Workflow >> should display login form with username and password fields +- Location: e2e/workflows/1-login.spec.ts:28:7 + +# Error details + +``` +Error: expect(locator).toBeVisible() failed + +Locator: locator('[data-testid="login-form"]') +Expected: visible +Timeout: 5000ms +Error: element(s) not found + +Call log: + - Expect "toBeVisible" with timeout 5000ms + - waiting for locator('[data-testid="login-form"]') + +``` + +# Page snapshot + +```yaml +- generic [active] [ref=e1]: + - generic [ref=e3]: + - generic [ref=e4]: + - img [ref=e6] + - heading "Identity Check" [level=2] [ref=e9] + - paragraph [ref=e10]: Select operator profile or use direct login + - generic [ref=e11]: + - button "newuser user" [ref=e12] [cursor=pointer]: + - generic [ref=e13]: + - img [ref=e15] + - generic [ref=e18]: + - paragraph [ref=e19]: newuser + - paragraph [ref=e20]: user + - img [ref=e21] + - generic [ref=e23]: + - button "Enterprise" [ref=e24] [cursor=pointer]: + - img [ref=e25] + - text: Enterprise + - button "Manual Login" [ref=e28] [cursor=pointer]: + - img [ref=e29] + - text: Manual Login + - button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]: + - img [ref=e38] + - alert [ref=e41] +``` + +# Test source + +```ts + 1 | import { expect, Page, Locator } from '@playwright/test'; + 2 | + 3 | /** + 4 | * Custom Playwright assertions for E2E tests + 5 | * Provides domain-specific matchers for inventory operations + 6 | */ + 7 | + 8 | /** + 9 | * Assert that an inventory item is visible with expected data + 10 | */ + 11 | export async function assertItemVisible( + 12 | page: Page, + 13 | itemName: string, + 14 | expectedData?: { quantity?: number; category?: string } + 15 | ): Promise { + 16 | const itemRow = page.locator(`[data-testid="item-row"][data-item-name="${itemName}"]`); + 17 | await expect(itemRow).toBeVisible(); + 18 | + 19 | if (expectedData?.quantity !== undefined) { + 20 | const quantityCell = itemRow.locator('[data-testid="quantity-cell"]'); + 21 | await expect(quantityCell).toContainText(expectedData.quantity.toString()); + 22 | } + 23 | + 24 | if (expectedData?.category !== undefined) { + 25 | const categoryCell = itemRow.locator('[data-testid="category-cell"]'); + 26 | await expect(categoryCell).toContainText(expectedData.category); + 27 | } + 28 | } + 29 | + 30 | /** + 31 | * Assert that a barcode scan was successful + 32 | */ + 33 | export async function assertScanSuccess( + 34 | page: Page, + 35 | expectedItemName: string, + 36 | expectedQuantityChange: number + 37 | ): Promise { + 38 | // Check for success toast notification + 39 | const successToast = page.locator('[data-testid="toast-success"]'); + 40 | await expect(successToast).toBeVisible({ timeout: 3000 }); + 41 | + 42 | // Verify item quantity was updated + 43 | const itemRow = page.locator(`[data-testid="item-row"][data-item-name="${expectedItemName}"]`); + 44 | await expect(itemRow).toBeVisible(); + 45 | } + 46 | + 47 | /** + 48 | * Assert that login form is displayed + 49 | */ + 50 | export async function assertLoginFormVisible(page: Page): Promise { + 51 | const loginForm = page.locator('[data-testid="login-form"]'); +> 52 | await expect(loginForm).toBeVisible(); + | ^ Error: expect(locator).toBeVisible() failed + 53 | + 54 | const usernameInput = page.locator('[data-testid="username-input"]'); + 55 | const passwordInput = page.locator('[data-testid="password-input"]'); + 56 | const submitButton = page.locator('[data-testid="login-submit"]'); + 57 | + 58 | await expect(usernameInput).toBeVisible(); + 59 | await expect(passwordInput).toBeVisible(); + 60 | await expect(submitButton).toBeVisible(); + 61 | } + 62 | + 63 | /** + 64 | * Assert that user is authenticated (on inventory page) + 65 | */ + 66 | export async function assertUserAuthenticated(page: Page, baseUrl: string = 'http://localhost'): Promise { + 67 | // Check URL is on an authenticated page + 68 | await expect(page).toHaveURL(/inventory|dashboard|scanner/); + 69 | + 70 | // Verify logout button is visible + 71 | const logoutButton = page.locator('[data-testid="logout-button"]'); + 72 | await expect(logoutButton).toBeVisible(); + 73 | } + 74 | + 75 | /** + 76 | * Assert that admin dashboard is visible + 77 | */ + 78 | export async function assertAdminDashboardVisible(page: Page): Promise { + 79 | const adminOverlay = page.locator('[data-testid="admin-overlay"]'); + 80 | await expect(adminOverlay).toBeVisible(); + 81 | + 82 | // Check for tabs + 83 | const identityTab = page.locator('[data-testid="admin-tab-identity"]'); + 84 | const databaseTab = page.locator('[data-testid="admin-tab-database"]'); + 85 | + 86 | await expect(identityTab).toBeVisible(); + 87 | await expect(databaseTab).toBeVisible(); + 88 | } + 89 | + 90 | /** + 91 | * Assert that stock adjustment form is visible + 92 | */ + 93 | export async function assertStockAdjustmentVisible(page: Page, itemName: string): Promise { + 94 | const adjustmentForm = page.locator('[data-testid="stock-adjustment-form"]'); + 95 | await expect(adjustmentForm).toBeVisible(); + 96 | + 97 | const itemNameDisplay = page.locator(`[data-testid="adjustment-item-name"]`); + 98 | await expect(itemNameDisplay).toContainText(itemName); + 99 | + 100 | const quantityInput = page.locator('[data-testid="adjustment-quantity-input"]'); + 101 | await expect(quantityInput).toBeVisible(); + 102 | } + 103 | + 104 | /** + 105 | * Assert that AI extraction is in progress + 106 | */ + 107 | export async function assertAiExtractionInProgress(page: Page): Promise { + 108 | const extractionOverlay = page.locator('[data-testid="ai-extraction-overlay"]'); + 109 | await expect(extractionOverlay).toBeVisible(); + 110 | + 111 | const loadingSpinner = page.locator('[data-testid="extraction-loading"]'); + 112 | await expect(loadingSpinner).toBeVisible(); + 113 | } + 114 | + 115 | /** + 116 | * Assert that AI extraction results are displayed + 117 | */ + 118 | export async function assertAiExtractionResults( + 119 | page: Page, + 120 | expectedFields: { name?: string; partNumber?: string; quantity?: string } + 121 | ): Promise { + 122 | const resultsForm = page.locator('[data-testid="extraction-results-form"]'); + 123 | await expect(resultsForm).toBeVisible(); + 124 | + 125 | if (expectedFields.name) { + 126 | const nameInput = resultsForm.locator('[data-testid="extracted-name"]'); + 127 | await expect(nameInput).toHaveValue(expectedFields.name); + 128 | } + 129 | + 130 | if (expectedFields.partNumber) { + 131 | const pnInput = resultsForm.locator('[data-testid="extracted-part-number"]'); + 132 | await expect(pnInput).toHaveValue(expectedFields.partNumber); + 133 | } + 134 | + 135 | if (expectedFields.quantity) { + 136 | const quantityInput = resultsForm.locator('[data-testid="extracted-quantity"]'); + 137 | await expect(quantityInput).toHaveValue(expectedFields.quantity); + 138 | } + 139 | } + 140 | + 141 | /** + 142 | * Assert that offline sync is pending + 143 | */ + 144 | export async function assertOfflineSyncPending(page: Page): Promise { + 145 | const syncIndicator = page.locator('[data-testid="offline-sync-indicator"]'); + 146 | await expect(syncIndicator).toBeVisible(); + 147 | + 148 | const pendingBadge = page.locator('[data-testid="sync-pending-badge"]'); + 149 | await expect(pendingBadge).toBeVisible(); + 150 | } + 151 | + 152 | /** +``` \ No newline at end of file diff --git a/frontend/test-results/1-login-Login-Workflow-sho-6d596-sername-and-password-fields-chromium/test-failed-1.png b/frontend/test-results/1-login-Login-Workflow-sho-6d596-sername-and-password-fields-chromium/test-failed-1.png new file mode 100644 index 00000000..b0b1cec4 Binary files /dev/null and b/frontend/test-results/1-login-Login-Workflow-sho-6d596-sername-and-password-fields-chromium/test-failed-1.png differ diff --git a/frontend/test-results/1-login-Login-Workflow-sho-77e03-dmin-button-for-admin-users-chromium/error-context.md b/frontend/test-results/1-login-Login-Workflow-sho-77e03-dmin-button-for-admin-users-chromium/error-context.md new file mode 100644 index 00000000..888f9e98 --- /dev/null +++ b/frontend/test-results/1-login-Login-Workflow-sho-77e03-dmin-button-for-admin-users-chromium/error-context.md @@ -0,0 +1,224 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: 1-login.spec.ts >> Login Workflow >> should show admin button for admin users +- Location: e2e/workflows/1-login.spec.ts:137:7 + +# Error details + +``` +Test timeout of 30000ms exceeded. +``` + +``` +Error: page.fill: Test timeout of 30000ms exceeded. +Call log: + - waiting for locator('[data-testid="local-username-input"]') + +``` + +# Page snapshot + +```yaml +- generic [active] [ref=e1]: + - generic [ref=e3]: + - generic [ref=e4]: + - img [ref=e6] + - heading "Identity Check" [level=2] [ref=e9] + - paragraph [ref=e10]: Select operator profile or use direct login + - generic [ref=e11]: + - button "newuser user" [ref=e12] [cursor=pointer]: + - generic [ref=e13]: + - img [ref=e15] + - generic [ref=e18]: + - paragraph [ref=e19]: newuser + - paragraph [ref=e20]: user + - img [ref=e21] + - generic [ref=e23]: + - button "Enterprise" [ref=e24] [cursor=pointer]: + - img [ref=e25] + - text: Enterprise + - button "Manual Login" [ref=e28] [cursor=pointer]: + - img [ref=e29] + - text: Manual Login + - button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]: + - img [ref=e38] + - alert [ref=e41] +``` + +# Test source + +```ts + 1 | import { Page, BrowserContext } from '@playwright/test'; + 2 | + 3 | /** + 4 | * Authentication fixture for E2E tests + 5 | * Handles login flows and session management + 6 | */ + 7 | + 8 | export interface LoginCredentials { + 9 | username: string; + 10 | password: string; + 11 | } + 12 | + 13 | export interface AuthSession { + 14 | token: string; + 15 | userId: string; + 16 | email: string; + 17 | isAdmin: boolean; + 18 | } + 19 | + 20 | /** + 21 | * Perform LDAP authentication + 22 | */ + 23 | export async function loginWithLdap( + 24 | page: Page, + 25 | credentials: LoginCredentials, + 26 | baseUrl: string = 'http://localhost' + 27 | ): Promise { + 28 | await page.goto(`${baseUrl}`); + 29 | + 30 | // Wait for login form to appear + 31 | await page.waitForSelector('[data-testid="login-form"]', { timeout: 5000 }); + 32 | + 33 | // Fill in username + 34 | await page.fill('[data-testid="username-input"]', credentials.username); + 35 | + 36 | // Fill in password + 37 | await page.fill('[data-testid="password-input"]', credentials.password); + 38 | + 39 | // Click login button + 40 | await page.click('[data-testid="login-submit"]'); + 41 | + 42 | // Wait for navigation to inventory page + 43 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' }); + 44 | } + 45 | + 46 | /** + 47 | * Perform local user authentication + 48 | */ + 49 | export async function loginWithLocalUser( + 50 | page: Page, + 51 | credentials: LoginCredentials, + 52 | baseUrl: string = 'http://localhost' + 53 | ): Promise { + 54 | await page.goto(`${baseUrl}`); + 55 | + 56 | // Wait for identity check overlay + 57 | await page.waitForSelector('[data-testid="identity-check-overlay"]', { timeout: 5000 }); + 58 | + 59 | // Click "Local User" tab if it exists + 60 | const localUserTab = page.locator('[data-testid="local-user-tab"]'); + 61 | if (await localUserTab.isVisible()) { + 62 | await localUserTab.click(); + 63 | } + 64 | + 65 | // Fill in username +> 66 | await page.fill('[data-testid="local-username-input"]', credentials.username); + | ^ Error: page.fill: Test timeout of 30000ms exceeded. + 67 | + 68 | // Fill in password + 69 | await page.fill('[data-testid="local-password-input"]', credentials.password); + 70 | + 71 | // Click login button + 72 | await page.click('[data-testid="local-login-submit"]'); + 73 | + 74 | // Wait for navigation to inventory page + 75 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' }); + 76 | } + 77 | + 78 | /** + 79 | * Logout from the application + 80 | */ + 81 | export async function logout(page: Page, baseUrl: string = 'http://localhost'): Promise { + 82 | // Click logout button or menu + 83 | const logoutButton = page.locator('[data-testid="logout-button"]'); + 84 | if (await logoutButton.isVisible()) { + 85 | await logoutButton.click(); + 86 | } else { + 87 | // Try finding it in menu + 88 | const menu = page.locator('[data-testid="user-menu"]'); + 89 | if (await menu.isVisible()) { + 90 | await menu.click(); + 91 | await page.click('[data-testid="logout-menu-item"]'); + 92 | } + 93 | } + 94 | + 95 | // Wait for redirect to login + 96 | await page.waitForURL(`${baseUrl}/*`, { timeout: 5000 }); + 97 | } + 98 | + 99 | /** + 100 | * Get authentication token from local storage + 101 | */ + 102 | export async function getAuthToken(page: Page): Promise { + 103 | const token = await page.evaluate(() => { + 104 | return localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token'); + 105 | }); + 106 | return token; + 107 | } + 108 | + 109 | /** + 110 | * Set authentication token in local storage + 111 | */ + 112 | export async function setAuthToken(page: Page, token: string): Promise { + 113 | await page.evaluate((t) => { + 114 | localStorage.setItem('auth_token', t); + 115 | }, token); + 116 | } + 117 | + 118 | /** + 119 | * Clear authentication data + 120 | */ + 121 | export async function clearAuth(page: Page): Promise { + 122 | await page.evaluate(() => { + 123 | localStorage.removeItem('auth_token'); + 124 | sessionStorage.removeItem('auth_token'); + 125 | localStorage.removeItem('user_data'); + 126 | sessionStorage.removeItem('user_data'); + 127 | }); + 128 | } + 129 | + 130 | /** + 131 | * Check if user is authenticated + 132 | */ + 133 | export async function isAuthenticated(page: Page): Promise { + 134 | const token = await getAuthToken(page); + 135 | return !!token; + 136 | } + 137 | + 138 | /** + 139 | * Get current user info from page + 140 | */ + 141 | export async function getCurrentUser(page: Page): Promise<{ username: string; email: string } | null> { + 142 | try { + 143 | const userData = await page.evaluate(() => { + 144 | const stored = localStorage.getItem('user_data') || sessionStorage.getItem('user_data'); + 145 | return stored ? JSON.parse(stored) : null; + 146 | }); + 147 | return userData; + 148 | } catch { + 149 | return null; + 150 | } + 151 | } + 152 | + 153 | /** + 154 | * Wait for authentication to complete + 155 | */ + 156 | export async function waitForAuth(page: Page, timeout: number = 5000): Promise { + 157 | // Wait for either: + 158 | // 1. Token to be in storage + 159 | // 2. Navigation to happen + 160 | // 3. Auth overlay to disappear + 161 | await page.waitForFunction( + 162 | () => { + 163 | const token = + 164 | typeof window !== 'undefined' + 165 | ? localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token') + 166 | : null; +``` \ No newline at end of file diff --git a/frontend/test-results/1-login-Login-Workflow-sho-77e03-dmin-button-for-admin-users-chromium/test-failed-1.png b/frontend/test-results/1-login-Login-Workflow-sho-77e03-dmin-button-for-admin-users-chromium/test-failed-1.png new file mode 100644 index 00000000..b0b1cec4 Binary files /dev/null and b/frontend/test-results/1-login-Login-Workflow-sho-77e03-dmin-button-for-admin-users-chromium/test-failed-1.png differ diff --git a/frontend/test-results/1-login-Login-Workflow-sho-8b7ad-tity-after-successful-login-chromium/error-context.md b/frontend/test-results/1-login-Login-Workflow-sho-8b7ad-tity-after-successful-login-chromium/error-context.md new file mode 100644 index 00000000..ec531fc2 --- /dev/null +++ b/frontend/test-results/1-login-Login-Workflow-sho-8b7ad-tity-after-successful-login-chromium/error-context.md @@ -0,0 +1,224 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: 1-login.spec.ts >> Login Workflow >> should show user identity after successful login +- Location: e2e/workflows/1-login.spec.ts:93:7 + +# Error details + +``` +Test timeout of 30000ms exceeded. +``` + +``` +Error: page.fill: Test timeout of 30000ms exceeded. +Call log: + - waiting for locator('[data-testid="local-username-input"]') + +``` + +# Page snapshot + +```yaml +- generic [active] [ref=e1]: + - generic [ref=e3]: + - generic [ref=e4]: + - img [ref=e6] + - heading "Identity Check" [level=2] [ref=e9] + - paragraph [ref=e10]: Select operator profile or use direct login + - generic [ref=e11]: + - button "newuser user" [ref=e12] [cursor=pointer]: + - generic [ref=e13]: + - img [ref=e15] + - generic [ref=e18]: + - paragraph [ref=e19]: newuser + - paragraph [ref=e20]: user + - img [ref=e21] + - generic [ref=e23]: + - button "Enterprise" [ref=e24] [cursor=pointer]: + - img [ref=e25] + - text: Enterprise + - button "Manual Login" [ref=e28] [cursor=pointer]: + - img [ref=e29] + - text: Manual Login + - button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]: + - img [ref=e38] + - alert [ref=e41] +``` + +# Test source + +```ts + 1 | import { Page, BrowserContext } from '@playwright/test'; + 2 | + 3 | /** + 4 | * Authentication fixture for E2E tests + 5 | * Handles login flows and session management + 6 | */ + 7 | + 8 | export interface LoginCredentials { + 9 | username: string; + 10 | password: string; + 11 | } + 12 | + 13 | export interface AuthSession { + 14 | token: string; + 15 | userId: string; + 16 | email: string; + 17 | isAdmin: boolean; + 18 | } + 19 | + 20 | /** + 21 | * Perform LDAP authentication + 22 | */ + 23 | export async function loginWithLdap( + 24 | page: Page, + 25 | credentials: LoginCredentials, + 26 | baseUrl: string = 'http://localhost' + 27 | ): Promise { + 28 | await page.goto(`${baseUrl}`); + 29 | + 30 | // Wait for login form to appear + 31 | await page.waitForSelector('[data-testid="login-form"]', { timeout: 5000 }); + 32 | + 33 | // Fill in username + 34 | await page.fill('[data-testid="username-input"]', credentials.username); + 35 | + 36 | // Fill in password + 37 | await page.fill('[data-testid="password-input"]', credentials.password); + 38 | + 39 | // Click login button + 40 | await page.click('[data-testid="login-submit"]'); + 41 | + 42 | // Wait for navigation to inventory page + 43 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' }); + 44 | } + 45 | + 46 | /** + 47 | * Perform local user authentication + 48 | */ + 49 | export async function loginWithLocalUser( + 50 | page: Page, + 51 | credentials: LoginCredentials, + 52 | baseUrl: string = 'http://localhost' + 53 | ): Promise { + 54 | await page.goto(`${baseUrl}`); + 55 | + 56 | // Wait for identity check overlay + 57 | await page.waitForSelector('[data-testid="identity-check-overlay"]', { timeout: 5000 }); + 58 | + 59 | // Click "Local User" tab if it exists + 60 | const localUserTab = page.locator('[data-testid="local-user-tab"]'); + 61 | if (await localUserTab.isVisible()) { + 62 | await localUserTab.click(); + 63 | } + 64 | + 65 | // Fill in username +> 66 | await page.fill('[data-testid="local-username-input"]', credentials.username); + | ^ Error: page.fill: Test timeout of 30000ms exceeded. + 67 | + 68 | // Fill in password + 69 | await page.fill('[data-testid="local-password-input"]', credentials.password); + 70 | + 71 | // Click login button + 72 | await page.click('[data-testid="local-login-submit"]'); + 73 | + 74 | // Wait for navigation to inventory page + 75 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' }); + 76 | } + 77 | + 78 | /** + 79 | * Logout from the application + 80 | */ + 81 | export async function logout(page: Page, baseUrl: string = 'http://localhost'): Promise { + 82 | // Click logout button or menu + 83 | const logoutButton = page.locator('[data-testid="logout-button"]'); + 84 | if (await logoutButton.isVisible()) { + 85 | await logoutButton.click(); + 86 | } else { + 87 | // Try finding it in menu + 88 | const menu = page.locator('[data-testid="user-menu"]'); + 89 | if (await menu.isVisible()) { + 90 | await menu.click(); + 91 | await page.click('[data-testid="logout-menu-item"]'); + 92 | } + 93 | } + 94 | + 95 | // Wait for redirect to login + 96 | await page.waitForURL(`${baseUrl}/*`, { timeout: 5000 }); + 97 | } + 98 | + 99 | /** + 100 | * Get authentication token from local storage + 101 | */ + 102 | export async function getAuthToken(page: Page): Promise { + 103 | const token = await page.evaluate(() => { + 104 | return localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token'); + 105 | }); + 106 | return token; + 107 | } + 108 | + 109 | /** + 110 | * Set authentication token in local storage + 111 | */ + 112 | export async function setAuthToken(page: Page, token: string): Promise { + 113 | await page.evaluate((t) => { + 114 | localStorage.setItem('auth_token', t); + 115 | }, token); + 116 | } + 117 | + 118 | /** + 119 | * Clear authentication data + 120 | */ + 121 | export async function clearAuth(page: Page): Promise { + 122 | await page.evaluate(() => { + 123 | localStorage.removeItem('auth_token'); + 124 | sessionStorage.removeItem('auth_token'); + 125 | localStorage.removeItem('user_data'); + 126 | sessionStorage.removeItem('user_data'); + 127 | }); + 128 | } + 129 | + 130 | /** + 131 | * Check if user is authenticated + 132 | */ + 133 | export async function isAuthenticated(page: Page): Promise { + 134 | const token = await getAuthToken(page); + 135 | return !!token; + 136 | } + 137 | + 138 | /** + 139 | * Get current user info from page + 140 | */ + 141 | export async function getCurrentUser(page: Page): Promise<{ username: string; email: string } | null> { + 142 | try { + 143 | const userData = await page.evaluate(() => { + 144 | const stored = localStorage.getItem('user_data') || sessionStorage.getItem('user_data'); + 145 | return stored ? JSON.parse(stored) : null; + 146 | }); + 147 | return userData; + 148 | } catch { + 149 | return null; + 150 | } + 151 | } + 152 | + 153 | /** + 154 | * Wait for authentication to complete + 155 | */ + 156 | export async function waitForAuth(page: Page, timeout: number = 5000): Promise { + 157 | // Wait for either: + 158 | // 1. Token to be in storage + 159 | // 2. Navigation to happen + 160 | // 3. Auth overlay to disappear + 161 | await page.waitForFunction( + 162 | () => { + 163 | const token = + 164 | typeof window !== 'undefined' + 165 | ? localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token') + 166 | : null; +``` \ No newline at end of file diff --git a/frontend/test-results/1-login-Login-Workflow-sho-8b7ad-tity-after-successful-login-chromium/test-failed-1.png b/frontend/test-results/1-login-Login-Workflow-sho-8b7ad-tity-after-successful-login-chromium/test-failed-1.png new file mode 100644 index 00000000..b0b1cec4 Binary files /dev/null and b/frontend/test-results/1-login-Login-Workflow-sho-8b7ad-tity-after-successful-login-chromium/test-failed-1.png differ diff --git a/frontend/test-results/1-login-Login-Workflow-sho-8fef9-login-preference-on-revisit-chromium/error-context.md b/frontend/test-results/1-login-Login-Workflow-sho-8fef9-login-preference-on-revisit-chromium/error-context.md new file mode 100644 index 00000000..b1518080 --- /dev/null +++ b/frontend/test-results/1-login-Login-Workflow-sho-8fef9-login-preference-on-revisit-chromium/error-context.md @@ -0,0 +1,224 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: 1-login.spec.ts >> Login Workflow >> should remember login preference on revisit +- Location: e2e/workflows/1-login.spec.ts:165:7 + +# Error details + +``` +Test timeout of 30000ms exceeded. +``` + +``` +Error: page.fill: Test timeout of 30000ms exceeded. +Call log: + - waiting for locator('[data-testid="local-username-input"]') + +``` + +# Page snapshot + +```yaml +- generic [active] [ref=e1]: + - generic [ref=e3]: + - generic [ref=e4]: + - img [ref=e6] + - heading "Identity Check" [level=2] [ref=e9] + - paragraph [ref=e10]: Select operator profile or use direct login + - generic [ref=e11]: + - button "newuser user" [ref=e12] [cursor=pointer]: + - generic [ref=e13]: + - img [ref=e15] + - generic [ref=e18]: + - paragraph [ref=e19]: newuser + - paragraph [ref=e20]: user + - img [ref=e21] + - generic [ref=e23]: + - button "Enterprise" [ref=e24] [cursor=pointer]: + - img [ref=e25] + - text: Enterprise + - button "Manual Login" [ref=e28] [cursor=pointer]: + - img [ref=e29] + - text: Manual Login + - button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]: + - img [ref=e38] + - alert [ref=e41] +``` + +# Test source + +```ts + 1 | import { Page, BrowserContext } from '@playwright/test'; + 2 | + 3 | /** + 4 | * Authentication fixture for E2E tests + 5 | * Handles login flows and session management + 6 | */ + 7 | + 8 | export interface LoginCredentials { + 9 | username: string; + 10 | password: string; + 11 | } + 12 | + 13 | export interface AuthSession { + 14 | token: string; + 15 | userId: string; + 16 | email: string; + 17 | isAdmin: boolean; + 18 | } + 19 | + 20 | /** + 21 | * Perform LDAP authentication + 22 | */ + 23 | export async function loginWithLdap( + 24 | page: Page, + 25 | credentials: LoginCredentials, + 26 | baseUrl: string = 'http://localhost' + 27 | ): Promise { + 28 | await page.goto(`${baseUrl}`); + 29 | + 30 | // Wait for login form to appear + 31 | await page.waitForSelector('[data-testid="login-form"]', { timeout: 5000 }); + 32 | + 33 | // Fill in username + 34 | await page.fill('[data-testid="username-input"]', credentials.username); + 35 | + 36 | // Fill in password + 37 | await page.fill('[data-testid="password-input"]', credentials.password); + 38 | + 39 | // Click login button + 40 | await page.click('[data-testid="login-submit"]'); + 41 | + 42 | // Wait for navigation to inventory page + 43 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' }); + 44 | } + 45 | + 46 | /** + 47 | * Perform local user authentication + 48 | */ + 49 | export async function loginWithLocalUser( + 50 | page: Page, + 51 | credentials: LoginCredentials, + 52 | baseUrl: string = 'http://localhost' + 53 | ): Promise { + 54 | await page.goto(`${baseUrl}`); + 55 | + 56 | // Wait for identity check overlay + 57 | await page.waitForSelector('[data-testid="identity-check-overlay"]', { timeout: 5000 }); + 58 | + 59 | // Click "Local User" tab if it exists + 60 | const localUserTab = page.locator('[data-testid="local-user-tab"]'); + 61 | if (await localUserTab.isVisible()) { + 62 | await localUserTab.click(); + 63 | } + 64 | + 65 | // Fill in username +> 66 | await page.fill('[data-testid="local-username-input"]', credentials.username); + | ^ Error: page.fill: Test timeout of 30000ms exceeded. + 67 | + 68 | // Fill in password + 69 | await page.fill('[data-testid="local-password-input"]', credentials.password); + 70 | + 71 | // Click login button + 72 | await page.click('[data-testid="local-login-submit"]'); + 73 | + 74 | // Wait for navigation to inventory page + 75 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' }); + 76 | } + 77 | + 78 | /** + 79 | * Logout from the application + 80 | */ + 81 | export async function logout(page: Page, baseUrl: string = 'http://localhost'): Promise { + 82 | // Click logout button or menu + 83 | const logoutButton = page.locator('[data-testid="logout-button"]'); + 84 | if (await logoutButton.isVisible()) { + 85 | await logoutButton.click(); + 86 | } else { + 87 | // Try finding it in menu + 88 | const menu = page.locator('[data-testid="user-menu"]'); + 89 | if (await menu.isVisible()) { + 90 | await menu.click(); + 91 | await page.click('[data-testid="logout-menu-item"]'); + 92 | } + 93 | } + 94 | + 95 | // Wait for redirect to login + 96 | await page.waitForURL(`${baseUrl}/*`, { timeout: 5000 }); + 97 | } + 98 | + 99 | /** + 100 | * Get authentication token from local storage + 101 | */ + 102 | export async function getAuthToken(page: Page): Promise { + 103 | const token = await page.evaluate(() => { + 104 | return localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token'); + 105 | }); + 106 | return token; + 107 | } + 108 | + 109 | /** + 110 | * Set authentication token in local storage + 111 | */ + 112 | export async function setAuthToken(page: Page, token: string): Promise { + 113 | await page.evaluate((t) => { + 114 | localStorage.setItem('auth_token', t); + 115 | }, token); + 116 | } + 117 | + 118 | /** + 119 | * Clear authentication data + 120 | */ + 121 | export async function clearAuth(page: Page): Promise { + 122 | await page.evaluate(() => { + 123 | localStorage.removeItem('auth_token'); + 124 | sessionStorage.removeItem('auth_token'); + 125 | localStorage.removeItem('user_data'); + 126 | sessionStorage.removeItem('user_data'); + 127 | }); + 128 | } + 129 | + 130 | /** + 131 | * Check if user is authenticated + 132 | */ + 133 | export async function isAuthenticated(page: Page): Promise { + 134 | const token = await getAuthToken(page); + 135 | return !!token; + 136 | } + 137 | + 138 | /** + 139 | * Get current user info from page + 140 | */ + 141 | export async function getCurrentUser(page: Page): Promise<{ username: string; email: string } | null> { + 142 | try { + 143 | const userData = await page.evaluate(() => { + 144 | const stored = localStorage.getItem('user_data') || sessionStorage.getItem('user_data'); + 145 | return stored ? JSON.parse(stored) : null; + 146 | }); + 147 | return userData; + 148 | } catch { + 149 | return null; + 150 | } + 151 | } + 152 | + 153 | /** + 154 | * Wait for authentication to complete + 155 | */ + 156 | export async function waitForAuth(page: Page, timeout: number = 5000): Promise { + 157 | // Wait for either: + 158 | // 1. Token to be in storage + 159 | // 2. Navigation to happen + 160 | // 3. Auth overlay to disappear + 161 | await page.waitForFunction( + 162 | () => { + 163 | const token = + 164 | typeof window !== 'undefined' + 165 | ? localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token') + 166 | : null; +``` \ No newline at end of file diff --git a/frontend/test-results/1-login-Login-Workflow-sho-8fef9-login-preference-on-revisit-chromium/test-failed-1.png b/frontend/test-results/1-login-Login-Workflow-sho-8fef9-login-preference-on-revisit-chromium/test-failed-1.png new file mode 100644 index 00000000..b0b1cec4 Binary files /dev/null and b/frontend/test-results/1-login-Login-Workflow-sho-8fef9-login-preference-on-revisit-chromium/test-failed-1.png differ diff --git a/frontend/test-results/1-login-Login-Workflow-sho-a6e06-ct-invalid-LDAP-credentials-chromium/error-context.md b/frontend/test-results/1-login-Login-Workflow-sho-a6e06-ct-invalid-LDAP-credentials-chromium/error-context.md new file mode 100644 index 00000000..fe107665 --- /dev/null +++ b/frontend/test-results/1-login-Login-Workflow-sho-a6e06-ct-invalid-LDAP-credentials-chromium/error-context.md @@ -0,0 +1,187 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: 1-login.spec.ts >> Login Workflow >> should reject invalid LDAP credentials +- Location: e2e/workflows/1-login.spec.ts:41:7 + +# Error details + +``` +TimeoutError: page.waitForSelector: Timeout 5000ms exceeded. +Call log: + - waiting for locator('[data-testid="login-form"]') to be visible + - waiting for" http://localhost:8917/login" navigation to finish... + - navigated to "http://localhost:8917/login" + +``` + +# Page snapshot + +```yaml +- generic [active] [ref=e1]: + - generic [ref=e3]: + - generic [ref=e4]: + - img [ref=e6] + - heading "Identity Check" [level=2] [ref=e9] + - paragraph [ref=e10]: Select operator profile or use direct login + - generic [ref=e11]: + - button "newuser user" [ref=e12] [cursor=pointer]: + - generic [ref=e13]: + - img [ref=e15] + - generic [ref=e18]: + - paragraph [ref=e19]: newuser + - paragraph [ref=e20]: user + - img [ref=e21] + - generic [ref=e23]: + - button "Enterprise" [ref=e24] [cursor=pointer]: + - img [ref=e25] + - text: Enterprise + - button "Manual Login" [ref=e28] [cursor=pointer]: + - img [ref=e29] + - text: Manual Login + - button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]: + - img [ref=e38] + - alert [ref=e41] +``` + +# Test source + +```ts + 1 | import { Page, BrowserContext } from '@playwright/test'; + 2 | + 3 | /** + 4 | * Authentication fixture for E2E tests + 5 | * Handles login flows and session management + 6 | */ + 7 | + 8 | export interface LoginCredentials { + 9 | username: string; + 10 | password: string; + 11 | } + 12 | + 13 | export interface AuthSession { + 14 | token: string; + 15 | userId: string; + 16 | email: string; + 17 | isAdmin: boolean; + 18 | } + 19 | + 20 | /** + 21 | * Perform LDAP authentication + 22 | */ + 23 | export async function loginWithLdap( + 24 | page: Page, + 25 | credentials: LoginCredentials, + 26 | baseUrl: string = 'http://localhost' + 27 | ): Promise { + 28 | await page.goto(`${baseUrl}`); + 29 | + 30 | // Wait for login form to appear +> 31 | await page.waitForSelector('[data-testid="login-form"]', { timeout: 5000 }); + | ^ TimeoutError: page.waitForSelector: Timeout 5000ms exceeded. + 32 | + 33 | // Fill in username + 34 | await page.fill('[data-testid="username-input"]', credentials.username); + 35 | + 36 | // Fill in password + 37 | await page.fill('[data-testid="password-input"]', credentials.password); + 38 | + 39 | // Click login button + 40 | await page.click('[data-testid="login-submit"]'); + 41 | + 42 | // Wait for navigation to inventory page + 43 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' }); + 44 | } + 45 | + 46 | /** + 47 | * Perform local user authentication + 48 | */ + 49 | export async function loginWithLocalUser( + 50 | page: Page, + 51 | credentials: LoginCredentials, + 52 | baseUrl: string = 'http://localhost' + 53 | ): Promise { + 54 | await page.goto(`${baseUrl}`); + 55 | + 56 | // Wait for identity check overlay + 57 | await page.waitForSelector('[data-testid="identity-check-overlay"]', { timeout: 5000 }); + 58 | + 59 | // Click "Local User" tab if it exists + 60 | const localUserTab = page.locator('[data-testid="local-user-tab"]'); + 61 | if (await localUserTab.isVisible()) { + 62 | await localUserTab.click(); + 63 | } + 64 | + 65 | // Fill in username + 66 | await page.fill('[data-testid="local-username-input"]', credentials.username); + 67 | + 68 | // Fill in password + 69 | await page.fill('[data-testid="local-password-input"]', credentials.password); + 70 | + 71 | // Click login button + 72 | await page.click('[data-testid="local-login-submit"]'); + 73 | + 74 | // Wait for navigation to inventory page + 75 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' }); + 76 | } + 77 | + 78 | /** + 79 | * Logout from the application + 80 | */ + 81 | export async function logout(page: Page, baseUrl: string = 'http://localhost'): Promise { + 82 | // Click logout button or menu + 83 | const logoutButton = page.locator('[data-testid="logout-button"]'); + 84 | if (await logoutButton.isVisible()) { + 85 | await logoutButton.click(); + 86 | } else { + 87 | // Try finding it in menu + 88 | const menu = page.locator('[data-testid="user-menu"]'); + 89 | if (await menu.isVisible()) { + 90 | await menu.click(); + 91 | await page.click('[data-testid="logout-menu-item"]'); + 92 | } + 93 | } + 94 | + 95 | // Wait for redirect to login + 96 | await page.waitForURL(`${baseUrl}/*`, { timeout: 5000 }); + 97 | } + 98 | + 99 | /** + 100 | * Get authentication token from local storage + 101 | */ + 102 | export async function getAuthToken(page: Page): Promise { + 103 | const token = await page.evaluate(() => { + 104 | return localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token'); + 105 | }); + 106 | return token; + 107 | } + 108 | + 109 | /** + 110 | * Set authentication token in local storage + 111 | */ + 112 | export async function setAuthToken(page: Page, token: string): Promise { + 113 | await page.evaluate((t) => { + 114 | localStorage.setItem('auth_token', t); + 115 | }, token); + 116 | } + 117 | + 118 | /** + 119 | * Clear authentication data + 120 | */ + 121 | export async function clearAuth(page: Page): Promise { + 122 | await page.evaluate(() => { + 123 | localStorage.removeItem('auth_token'); + 124 | sessionStorage.removeItem('auth_token'); + 125 | localStorage.removeItem('user_data'); + 126 | sessionStorage.removeItem('user_data'); + 127 | }); + 128 | } + 129 | + 130 | /** + 131 | * Check if user is authenticated +``` \ No newline at end of file diff --git a/frontend/test-results/1-login-Login-Workflow-sho-a6e06-ct-invalid-LDAP-credentials-chromium/test-failed-1.png b/frontend/test-results/1-login-Login-Workflow-sho-a6e06-ct-invalid-LDAP-credentials-chromium/test-failed-1.png new file mode 100644 index 00000000..b0b1cec4 Binary files /dev/null and b/frontend/test-results/1-login-Login-Workflow-sho-a6e06-ct-invalid-LDAP-credentials-chromium/test-failed-1.png differ diff --git a/frontend/test-results/1-login-Login-Workflow-sho-d35b6-ser-list-in-local-login-tab-chromium/error-context.md b/frontend/test-results/1-login-Login-Workflow-sho-d35b6-ser-list-in-local-login-tab-chromium/error-context.md new file mode 100644 index 00000000..31d38f19 --- /dev/null +++ b/frontend/test-results/1-login-Login-Workflow-sho-d35b6-ser-list-in-local-login-tab-chromium/error-context.md @@ -0,0 +1,174 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: 1-login.spec.ts >> Login Workflow >> should display user list in local login tab +- Location: e2e/workflows/1-login.spec.ts:202:7 + +# Error details + +``` +Error: expect(received).toBeGreaterThan(expected) + +Expected: > 0 +Received: 0 +``` + +# Page snapshot + +```yaml +- generic [ref=e1]: + - generic [ref=e3]: + - generic [ref=e4]: + - img [ref=e6] + - heading "Identity Check" [level=2] [ref=e9] + - paragraph [ref=e10]: Select operator profile or use direct login + - generic [ref=e12]: + - generic [ref=e13]: + - button [ref=e14] [cursor=pointer]: + - img [ref=e15] + - generic [ref=e18]: + - paragraph [ref=e19]: Logging in as + - paragraph [ref=e20]: Manual Input + - generic [ref=e21]: + - text: Username + - generic [ref=e22]: + - img [ref=e23] + - textbox "Admin" [ref=e26] + - generic [ref=e27]: + - text: Password + - generic [ref=e28]: + - img [ref=e29] + - textbox "Enter password" [active] [ref=e32] + - button "Verify Identity" [ref=e33] [cursor=pointer] + - button "Open Next.js Dev Tools" [ref=e39] [cursor=pointer]: + - img [ref=e40] + - alert [ref=e43] +``` + +# Test source + +```ts + 112 | // Should still be authenticated + 113 | const token = await auth.getAuthToken(page); + 114 | expect(token).toBeTruthy(); + 115 | + 116 | // Should not be on login page + 117 | await expect(page).not.toHaveURL(BASE_URL); + 118 | }); + 119 | + 120 | test('should support logout functionality', async ({ page }) => { + 121 | // Login first + 122 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 123 | await assertions.assertUserAuthenticated(page, BASE_URL); + 124 | + 125 | // Logout + 126 | await auth.logout(page, BASE_URL); + 127 | + 128 | // Should be back on login page + 129 | const identityOverlay = page.locator('[data-testid="identity-check-overlay"]'); + 130 | await expect(identityOverlay).toBeVisible({ timeout: 5000 }); + 131 | + 132 | // Token should be cleared + 133 | const token = await auth.getAuthToken(page); + 134 | expect(token).toBeFalsy(); + 135 | }); + 136 | + 137 | test('should show admin button for admin users', async ({ page }) => { + 138 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 139 | await assertions.assertUserAuthenticated(page, BASE_URL); + 140 | + 141 | // Admin button should be visible + 142 | const adminButton = page.locator('[data-testid="admin-button"]'); + 143 | await expect(adminButton).toBeVisible(); + 144 | }); + 145 | + 146 | test('should allow switching between LDAP and local login tabs', async ({ page }) => { + 147 | const ldapTab = page.locator('[data-testid="ldap-login-tab"]'); + 148 | const localTab = page.locator('[data-testid="local-login-tab"]'); + 149 | + 150 | // Start on LDAP tab + 151 | let ldapForm = page.locator('[data-testid="ldap-login-form"]'); + 152 | await expect(ldapForm).toBeVisible(); + 153 | + 154 | // Switch to local + 155 | await localTab.click(); + 156 | ldapForm = page.locator('[data-testid="ldap-login-form"]'); + 157 | await expect(ldapForm).not.toBeVisible(); + 158 | + 159 | // Switch back to LDAP + 160 | await ldapTab.click(); + 161 | ldapForm = page.locator('[data-testid="ldap-login-form"]'); + 162 | await expect(ldapForm).toBeVisible(); + 163 | }); + 164 | + 165 | test('should remember login preference on revisit', async ({ page, context }) => { + 166 | // Login with local user + 167 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); + 168 | + 169 | // Close and reopen browser + 170 | await page.close(); + 171 | const newPage = await context.newPage(); + 172 | await newPage.goto(BASE_URL); + 173 | + 174 | // Should still be authenticated + 175 | const authenticated = await auth.isAuthenticated(newPage); + 176 | expect(authenticated).toBe(true); + 177 | + 178 | await newPage.close(); + 179 | }); + 180 | + 181 | test('should handle network errors gracefully', async ({ page }) => { + 182 | // Enable offline mode + 183 | await page.context().setOffline(true); + 184 | + 185 | // Try to login + 186 | const usernameInput = page.locator('[data-testid="username-input"]'); + 187 | const passwordInput = page.locator('[data-testid="password-input"]'); + 188 | const submitButton = page.locator('[data-testid="login-submit"]'); + 189 | + 190 | await usernameInput.fill(LOCAL_USERS.admin.username); + 191 | await passwordInput.fill(LOCAL_USERS.admin.password); + 192 | await submitButton.click(); + 193 | + 194 | // Should show network error + 195 | const errorToast = page.locator('[data-testid="toast-error"]'); + 196 | await expect(errorToast).toBeVisible({ timeout: 5000 }); + 197 | + 198 | // Re-enable network + 199 | await page.context().setOffline(false); + 200 | }); + 201 | + 202 | test('should display user list in local login tab', async ({ page }) => { + 203 | const localTab = page.locator('[data-testid="local-login-tab"]'); + 204 | await localTab.click(); + 205 | + 206 | // User list should be visible + 207 | const userList = page.locator('[data-testid="user-list"]'); + 208 | await expect(userList).toBeVisible(); + 209 | + 210 | // Should have at least one user + 211 | const userItems = page.locator('[data-testid="user-list-item"]'); +> 212 | expect(await userItems.count()).toBeGreaterThan(0); + | ^ Error: expect(received).toBeGreaterThan(expected) + 213 | }); + 214 | + 215 | test('should auto-login when clicking user from list', async ({ page }) => { + 216 | const localTab = page.locator('[data-testid="local-login-tab"]'); + 217 | await localTab.click(); + 218 | + 219 | // Click first user in list + 220 | const firstUser = page.locator('[data-testid="user-list-item"]').first(); + 221 | await firstUser.click(); + 222 | + 223 | // Password input should be focused + 224 | const passwordInput = page.locator('[data-testid="local-password-input"]'); + 225 | await expect(passwordInput).toBeFocused(); + 226 | }); + 227 | }); + 228 | +``` \ No newline at end of file diff --git a/frontend/test-results/1-login-Login-Workflow-sho-d35b6-ser-list-in-local-login-tab-chromium/test-failed-1.png b/frontend/test-results/1-login-Login-Workflow-sho-d35b6-ser-list-in-local-login-tab-chromium/test-failed-1.png new file mode 100644 index 00000000..9449d71f Binary files /dev/null and b/frontend/test-results/1-login-Login-Workflow-sho-d35b6-ser-list-in-local-login-tab-chromium/test-failed-1.png differ diff --git a/frontend/test-results/1-login-Login-Workflow-sho-ba9e9-y-check-overlay-on-app-load-chromium/error-context.md b/frontend/test-results/1-login-Login-Workflow-should-reject-empty-login-credentials-chromium/error-context.md similarity index 88% rename from frontend/test-results/1-login-Login-Workflow-sho-ba9e9-y-check-overlay-on-app-load-chromium/error-context.md rename to frontend/test-results/1-login-Login-Workflow-should-reject-empty-login-credentials-chromium/error-context.md index d3bd717f..6b2076cd 100644 --- a/frontend/test-results/1-login-Login-Workflow-sho-ba9e9-y-check-overlay-on-app-load-chromium/error-context.md +++ b/frontend/test-results/1-login-Login-Workflow-should-reject-empty-login-credentials-chromium/error-context.md @@ -6,22 +6,19 @@ # Test info -- Name: 1-login.spec.ts >> Login Workflow >> should display identity check overlay on app load -- Location: e2e/workflows/1-login.spec.ts:16:7 +- Name: 1-login.spec.ts >> Login Workflow >> should reject empty login credentials +- Location: e2e/workflows/1-login.spec.ts:32:7 # Error details ``` -Error: expect(locator).toBeVisible() failed - -Locator: locator('[data-testid="ldap-login-tab"]') -Expected: visible -Timeout: 5000ms -Error: element(s) not found +Test timeout of 30000ms exceeded. +``` +``` +Error: locator.click: Test timeout of 30000ms exceeded. Call log: - - Expect "toBeVisible" with timeout 5000ms - - waiting for locator('[data-testid="ldap-login-tab"]') + - waiting for locator('[data-testid="login-submit"]') ``` @@ -80,8 +77,7 @@ Call log: 21 | const ldapTab = page.locator('[data-testid="ldap-login-tab"]'); 22 | const localTab = page.locator('[data-testid="local-login-tab"]'); 23 | -> 24 | await expect(ldapTab).toBeVisible(); - | ^ Error: expect(locator).toBeVisible() failed + 24 | await expect(ldapTab).toBeVisible(); 25 | await expect(localTab).toBeVisible(); 26 | }); 27 | @@ -91,7 +87,8 @@ Call log: 31 | 32 | test('should reject empty login credentials', async ({ page }) => { 33 | const submitButton = page.locator('[data-testid="login-submit"]'); - 34 | await submitButton.click(); +> 34 | await submitButton.click(); + | ^ Error: locator.click: Test timeout of 30000ms exceeded. 35 | 36 | // Should show validation error 37 | const errorMessage = page.locator('[data-testid="toast-error"]'); @@ -182,4 +179,14 @@ Call log: 122 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL); 123 | await assertions.assertUserAuthenticated(page, BASE_URL); 124 | + 125 | // Logout + 126 | await auth.logout(page, BASE_URL); + 127 | + 128 | // Should be back on login page + 129 | const identityOverlay = page.locator('[data-testid="identity-check-overlay"]'); + 130 | await expect(identityOverlay).toBeVisible({ timeout: 5000 }); + 131 | + 132 | // Token should be cleared + 133 | const token = await auth.getAuthToken(page); + 134 | expect(token).toBeFalsy(); ``` \ No newline at end of file diff --git a/frontend/test-results/1-login-Login-Workflow-should-reject-empty-login-credentials-chromium/test-failed-1.png b/frontend/test-results/1-login-Login-Workflow-should-reject-empty-login-credentials-chromium/test-failed-1.png new file mode 100644 index 00000000..b0b1cec4 Binary files /dev/null and b/frontend/test-results/1-login-Login-Workflow-should-reject-empty-login-credentials-chromium/test-failed-1.png differ diff --git a/frontend/test-results/1-login-Login-Workflow-should-support-logout-functionality-chromium/error-context.md b/frontend/test-results/1-login-Login-Workflow-should-support-logout-functionality-chromium/error-context.md new file mode 100644 index 00000000..2a30e17a --- /dev/null +++ b/frontend/test-results/1-login-Login-Workflow-should-support-logout-functionality-chromium/error-context.md @@ -0,0 +1,224 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: 1-login.spec.ts >> Login Workflow >> should support logout functionality +- Location: e2e/workflows/1-login.spec.ts:120:7 + +# Error details + +``` +Test timeout of 30000ms exceeded. +``` + +``` +Error: page.fill: Test timeout of 30000ms exceeded. +Call log: + - waiting for locator('[data-testid="local-username-input"]') + +``` + +# Page snapshot + +```yaml +- generic [active] [ref=e1]: + - generic [ref=e3]: + - generic [ref=e4]: + - img [ref=e6] + - heading "Identity Check" [level=2] [ref=e9] + - paragraph [ref=e10]: Select operator profile or use direct login + - generic [ref=e11]: + - button "newuser user" [ref=e12] [cursor=pointer]: + - generic [ref=e13]: + - img [ref=e15] + - generic [ref=e18]: + - paragraph [ref=e19]: newuser + - paragraph [ref=e20]: user + - img [ref=e21] + - generic [ref=e23]: + - button "Enterprise" [ref=e24] [cursor=pointer]: + - img [ref=e25] + - text: Enterprise + - button "Manual Login" [ref=e28] [cursor=pointer]: + - img [ref=e29] + - text: Manual Login + - button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]: + - img [ref=e38] + - alert [ref=e41] +``` + +# Test source + +```ts + 1 | import { Page, BrowserContext } from '@playwright/test'; + 2 | + 3 | /** + 4 | * Authentication fixture for E2E tests + 5 | * Handles login flows and session management + 6 | */ + 7 | + 8 | export interface LoginCredentials { + 9 | username: string; + 10 | password: string; + 11 | } + 12 | + 13 | export interface AuthSession { + 14 | token: string; + 15 | userId: string; + 16 | email: string; + 17 | isAdmin: boolean; + 18 | } + 19 | + 20 | /** + 21 | * Perform LDAP authentication + 22 | */ + 23 | export async function loginWithLdap( + 24 | page: Page, + 25 | credentials: LoginCredentials, + 26 | baseUrl: string = 'http://localhost' + 27 | ): Promise { + 28 | await page.goto(`${baseUrl}`); + 29 | + 30 | // Wait for login form to appear + 31 | await page.waitForSelector('[data-testid="login-form"]', { timeout: 5000 }); + 32 | + 33 | // Fill in username + 34 | await page.fill('[data-testid="username-input"]', credentials.username); + 35 | + 36 | // Fill in password + 37 | await page.fill('[data-testid="password-input"]', credentials.password); + 38 | + 39 | // Click login button + 40 | await page.click('[data-testid="login-submit"]'); + 41 | + 42 | // Wait for navigation to inventory page + 43 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' }); + 44 | } + 45 | + 46 | /** + 47 | * Perform local user authentication + 48 | */ + 49 | export async function loginWithLocalUser( + 50 | page: Page, + 51 | credentials: LoginCredentials, + 52 | baseUrl: string = 'http://localhost' + 53 | ): Promise { + 54 | await page.goto(`${baseUrl}`); + 55 | + 56 | // Wait for identity check overlay + 57 | await page.waitForSelector('[data-testid="identity-check-overlay"]', { timeout: 5000 }); + 58 | + 59 | // Click "Local User" tab if it exists + 60 | const localUserTab = page.locator('[data-testid="local-user-tab"]'); + 61 | if (await localUserTab.isVisible()) { + 62 | await localUserTab.click(); + 63 | } + 64 | + 65 | // Fill in username +> 66 | await page.fill('[data-testid="local-username-input"]', credentials.username); + | ^ Error: page.fill: Test timeout of 30000ms exceeded. + 67 | + 68 | // Fill in password + 69 | await page.fill('[data-testid="local-password-input"]', credentials.password); + 70 | + 71 | // Click login button + 72 | await page.click('[data-testid="local-login-submit"]'); + 73 | + 74 | // Wait for navigation to inventory page + 75 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' }); + 76 | } + 77 | + 78 | /** + 79 | * Logout from the application + 80 | */ + 81 | export async function logout(page: Page, baseUrl: string = 'http://localhost'): Promise { + 82 | // Click logout button or menu + 83 | const logoutButton = page.locator('[data-testid="logout-button"]'); + 84 | if (await logoutButton.isVisible()) { + 85 | await logoutButton.click(); + 86 | } else { + 87 | // Try finding it in menu + 88 | const menu = page.locator('[data-testid="user-menu"]'); + 89 | if (await menu.isVisible()) { + 90 | await menu.click(); + 91 | await page.click('[data-testid="logout-menu-item"]'); + 92 | } + 93 | } + 94 | + 95 | // Wait for redirect to login + 96 | await page.waitForURL(`${baseUrl}/*`, { timeout: 5000 }); + 97 | } + 98 | + 99 | /** + 100 | * Get authentication token from local storage + 101 | */ + 102 | export async function getAuthToken(page: Page): Promise { + 103 | const token = await page.evaluate(() => { + 104 | return localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token'); + 105 | }); + 106 | return token; + 107 | } + 108 | + 109 | /** + 110 | * Set authentication token in local storage + 111 | */ + 112 | export async function setAuthToken(page: Page, token: string): Promise { + 113 | await page.evaluate((t) => { + 114 | localStorage.setItem('auth_token', t); + 115 | }, token); + 116 | } + 117 | + 118 | /** + 119 | * Clear authentication data + 120 | */ + 121 | export async function clearAuth(page: Page): Promise { + 122 | await page.evaluate(() => { + 123 | localStorage.removeItem('auth_token'); + 124 | sessionStorage.removeItem('auth_token'); + 125 | localStorage.removeItem('user_data'); + 126 | sessionStorage.removeItem('user_data'); + 127 | }); + 128 | } + 129 | + 130 | /** + 131 | * Check if user is authenticated + 132 | */ + 133 | export async function isAuthenticated(page: Page): Promise { + 134 | const token = await getAuthToken(page); + 135 | return !!token; + 136 | } + 137 | + 138 | /** + 139 | * Get current user info from page + 140 | */ + 141 | export async function getCurrentUser(page: Page): Promise<{ username: string; email: string } | null> { + 142 | try { + 143 | const userData = await page.evaluate(() => { + 144 | const stored = localStorage.getItem('user_data') || sessionStorage.getItem('user_data'); + 145 | return stored ? JSON.parse(stored) : null; + 146 | }); + 147 | return userData; + 148 | } catch { + 149 | return null; + 150 | } + 151 | } + 152 | + 153 | /** + 154 | * Wait for authentication to complete + 155 | */ + 156 | export async function waitForAuth(page: Page, timeout: number = 5000): Promise { + 157 | // Wait for either: + 158 | // 1. Token to be in storage + 159 | // 2. Navigation to happen + 160 | // 3. Auth overlay to disappear + 161 | await page.waitForFunction( + 162 | () => { + 163 | const token = + 164 | typeof window !== 'undefined' + 165 | ? localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token') + 166 | : null; +``` \ No newline at end of file diff --git a/frontend/test-results/1-login-Login-Workflow-should-support-logout-functionality-chromium/test-failed-1.png b/frontend/test-results/1-login-Login-Workflow-should-support-logout-functionality-chromium/test-failed-1.png new file mode 100644 index 00000000..b0b1cec4 Binary files /dev/null and b/frontend/test-results/1-login-Login-Workflow-should-support-logout-functionality-chromium/test-failed-1.png differ