test: save E2E test progress - data-testid attributes added, 1/16 login tests passing
|
After Width: | Height: | Size: 33 KiB |
@@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<string | null> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<boolean> {
|
||||
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<void> {
|
||||
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;
|
||||
```
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<string | null> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<boolean> {
|
||||
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<void> {
|
||||
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;
|
||||
```
|
||||
@@ -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();
|
||||
```
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<string | null> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<boolean> {
|
||||
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<void> {
|
||||
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;
|
||||
```
|
||||
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 33 KiB |
@@ -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);
|
||||
```
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<string | null> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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
|
||||
```
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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 | /**
|
||||
```
|
||||
@@ -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 |
|
||||
```
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<string | null> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<boolean> {
|
||||
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<void> {
|
||||
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;
|
||||
```
|
||||
@@ -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 |
|
||||
```
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<string | null> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<boolean> {
|
||||
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<void> {
|
||||
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;
|
||||
```
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<string | null> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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
|
||||
```
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<string | null> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<boolean> {
|
||||
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<void> {
|
||||
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;
|
||||
```
|
||||
@@ -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 |
|
||||
```
|
||||
@@ -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 |
|
||||
```
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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 |
|
||||
```
|
||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
@@ -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 |
|
||||
```
|
||||
|
After Width: | Height: | Size: 25 KiB |
@@ -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 |
|
||||
```
|
||||
|
After Width: | Height: | Size: 33 KiB |
@@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<string | null> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<boolean> {
|
||||
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<void> {
|
||||
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;
|
||||
```
|
||||
|
After Width: | Height: | Size: 25 KiB |
@@ -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);
|
||||
```
|
||||
|
After Width: | Height: | Size: 33 KiB |
@@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<string | null> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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
|
||||
```
|
||||
|
After Width: | Height: | Size: 25 KiB |
@@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<string | null> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<boolean> {
|
||||
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<void> {
|
||||
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;
|
||||
```
|
||||
|
After Width: | Height: | Size: 25 KiB |
@@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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 | /**
|
||||
```
|
||||
|
After Width: | Height: | Size: 25 KiB |
@@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<string | null> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<boolean> {
|
||||
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<void> {
|
||||
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;
|
||||
```
|
||||
|
After Width: | Height: | Size: 25 KiB |
@@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<string | null> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<boolean> {
|
||||
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<void> {
|
||||
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;
|
||||
```
|
||||
|
After Width: | Height: | Size: 25 KiB |
@@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<string | null> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<boolean> {
|
||||
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<void> {
|
||||
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;
|
||||
```
|
||||
|
After Width: | Height: | Size: 25 KiB |
@@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<string | null> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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
|
||||
```
|
||||
|
After Width: | Height: | Size: 25 KiB |
@@ -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 |
|
||||
```
|
||||
|
After Width: | Height: | Size: 33 KiB |
@@ -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();
|
||||
```
|
||||
|
After Width: | Height: | Size: 25 KiB |
@@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<string | null> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<boolean> {
|
||||
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<void> {
|
||||
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;
|
||||
```
|
||||
|
After Width: | Height: | Size: 25 KiB |