Skip to content

Commit 18610cb

Browse files
authored
Migrate remaining Selenide UI tests to Playwright (#70)
* feat: migrate remaining Selenide UI tests to Playwright (#67) Remove all Selenide test infrastructure (18 Java files, 1,664 lines) and consolidate on Playwright as the single E2E framework. All Selenide test scenarios were already covered by existing Playwright specs; two new validation tests (empty fields, long names) were added to fill a gap. Key changes: - Delete Selenide test classes, page objects, utilities, and JDBC helpers - Remove selenide and webdrivermanager dependencies from build.gradle - Remove uiTest Gradle task and excludeTags 'ui' filter - Replace all networkidle waits with domcontentloaded across 14 Playwright spec files (fixes timeout failures caused by WebAuthn endpoints returning 500 on pages that load webauthn JS) - Add profile update validation tests to update-profile.spec.ts - Fix pre-existing Instant/Date type mismatch in TestDataController - Update EmailVerificationEdgeCaseTest to use userRepository instead of deleted DatabaseStateValidator - Update CLAUDE.md and README.md to reflect Playwright as E2E framework Closes #67 * chore: upgrade Spring User Framework to 4.3.1 Fixes WebAuthn 500 errors caused by missing user_credentials table (see devondragon/SpringUserFramework#286). Also removes unused DSUserDetails constant from ApiTestData that caused NPE with the new constructor signature. * fix: wait for async error element in duplicate registration test The registration form uses async fetch (no page navigation), so waitForLoadState returns immediately. Wait for the error element to become visible instead of checking synchronously. * fix: address Copilot review feedback on validation test assertions - Empty field submission: assert HTML5 checkValidity() instead of just URL - Long name submission: use updateProfileAndWait() and assert message visible - Weak password: wait for globalMessage with sufficient timeout before asserting error - Mismatched passwords: assert confirmPasswordError element is visible
1 parent a3181de commit 18610cb

42 files changed

Lines changed: 194 additions & 1796 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CLAUDE.md

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,9 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
1818

1919
### Testing
2020
```bash
21-
# Run all tests except UI tests
21+
# Run all tests
2222
./gradlew test
2323

24-
# Run UI tests only
25-
./gradlew uiTest
26-
2724
# Run a specific test class
2825
./gradlew test --tests TestClassName
2926

@@ -58,7 +55,7 @@ This is a Spring Boot demo application showcasing the [Spring User Framework](ht
5855
4. **Testing Strategy**:
5956
- Unit tests for individual components
6057
- Integration tests using `@IntegrationTest` annotation (combines Spring Boot test setup)
61-
- UI tests with Selenide for end-to-end testing
58+
- E2E tests with Playwright (in `playwright/` directory)
6259
- API tests using MockMvc for REST endpoints
6360

6461
### Important Conventions

README.md

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -193,12 +193,9 @@ This project includes comprehensive testing with multiple approaches:
193193
### Running Tests
194194

195195
```bash
196-
# Run all tests except UI tests
196+
# Run all tests
197197
./gradlew test
198198
199-
# Run UI tests only (requires running application)
200-
./gradlew uiTest
201-
202199
# Run specific test class
203200
./gradlew test --tests UserApiTest
204201
@@ -211,7 +208,7 @@ This project includes comprehensive testing with multiple approaches:
211208
- **Unit Tests**: Fast tests for individual components
212209
- **Integration Tests**: Tests using `@IntegrationTest` with Spring context
213210
- **API Tests**: REST endpoint testing with MockMvc
214-
- **UI Tests**: End-to-end testing with Selenide
211+
- **UI Tests**: End-to-end testing with Playwright
215212
- **Security Tests**: Authentication and authorization testing
216213

217214
### Test Data
@@ -641,7 +638,7 @@ This project supports **Spring Boot DevTools** for live reload and auto-restart.
641638
| **Security** | Spring Security 7 | Authentication, authorization, CSRF protection |
642639
| **Data** | Spring Data JPA + Hibernate | Object-relational mapping and data access |
643640
| **Database** | MariaDB/MySQL | Primary data persistence |
644-
| **Testing** | JUnit 5 + Selenide | Unit, integration, and UI testing |
641+
| **Testing** | JUnit 5 + Playwright | Unit, integration, and UI testing |
645642
| **Build** | Gradle | Dependency management and build automation |
646643
| **Containers** | Docker + Docker Compose | Development and deployment |
647644

build.gradle

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ repositories {
3939

4040
dependencies {
4141
// DigitalSanctuary Spring User Framework
42-
implementation 'com.digitalsanctuary:ds-spring-user-framework:4.3.0'
42+
implementation 'com.digitalsanctuary:ds-spring-user-framework:4.3.1'
4343

4444
// WebAuthn support (Passkey authentication)
4545
implementation 'org.springframework.security:spring-security-webauthn'
@@ -98,8 +98,6 @@ dependencies {
9898
testImplementation 'org.springframework.boot:spring-boot-starter-security-test'
9999
testImplementation 'org.springframework.security:spring-security-test'
100100
testImplementation 'com.h2database:h2:2.4.240'
101-
testImplementation 'com.codeborne:selenide:7.15.0'
102-
testImplementation 'io.github.bonigarcia:webdrivermanager:6.3.3'
103101

104102
// OAuth2 Testing dependencies
105103
testImplementation 'org.wiremock:wiremock-standalone:3.13.2'
@@ -110,7 +108,6 @@ dependencies {
110108

111109
test {
112110
useJUnitPlatform {
113-
excludeTags 'ui'
114111
}
115112
testLogging {
116113
events "PASSED", "FAILED", "SKIPPED"
@@ -119,15 +116,6 @@ test {
119116
}
120117
}
121118

122-
tasks.register('uiTest', Test) {
123-
useJUnitPlatform {
124-
includeTags 'ui'
125-
}
126-
testClassesDirs = sourceSets.test.output.classesDirs
127-
classpath = sourceSets.test.runtimeClasspath
128-
shouldRunAfter test
129-
}
130-
131119
bootRun {
132120
// Use Spring Boot DevTool only when we run Gradle bootRun task
133121
classpath = sourceSets.main.runtimeClasspath + configurations.developmentOnly

playwright/src/pages/BasePage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ export abstract class BasePage {
108108
* Wait for navigation to complete.
109109
*/
110110
async waitForNavigation(): Promise<void> {
111-
await this.page.waitForLoadState('networkidle');
111+
await this.page.waitForLoadState('domcontentloaded');
112112
}
113113

114114
/**

playwright/src/pages/EventDetailsPage.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export class EventDetailsPage extends BasePage {
8888
this.page.waitForEvent('load'),
8989
dialog.accept(),
9090
]);
91-
await this.page.waitForLoadState('networkidle');
91+
await this.page.waitForLoadState('domcontentloaded');
9292
return succeeded;
9393
}
9494

@@ -108,7 +108,7 @@ export class EventDetailsPage extends BasePage {
108108
this.page.waitForEvent('load'),
109109
dialog.accept(),
110110
]);
111-
await this.page.waitForLoadState('networkidle');
111+
await this.page.waitForLoadState('domcontentloaded');
112112
return succeeded;
113113
}
114114

playwright/tests/access-control/protected-pages.spec.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ test.describe('Access Control', () => {
88
}) => {
99
// Try to access protected page without logging in
1010
await protectedPage.goto();
11-
await page.waitForLoadState('networkidle');
11+
await page.waitForLoadState('domcontentloaded');
1212

1313
// Should be redirected to login
1414
expect(page.url()).toContain('login');
@@ -28,7 +28,7 @@ test.describe('Access Control', () => {
2828

2929
// Access protected page
3030
await protectedPage.goto();
31-
await page.waitForLoadState('networkidle');
31+
await page.waitForLoadState('domcontentloaded');
3232

3333
// Should be on protected page
3434
expect(page.url()).toContain('protected');
@@ -40,7 +40,7 @@ test.describe('Access Control', () => {
4040
}) => {
4141
// Try to access user profile without logging in
4242
await updateUserPage.goto();
43-
await page.waitForLoadState('networkidle');
43+
await page.waitForLoadState('domcontentloaded');
4444

4545
// Should be redirected to login
4646
expect(page.url()).toContain('login');
@@ -52,7 +52,7 @@ test.describe('Access Control', () => {
5252
}) => {
5353
// Try to access password change without logging in
5454
await updatePasswordPage.goto();
55-
await page.waitForLoadState('networkidle');
55+
await page.waitForLoadState('domcontentloaded');
5656

5757
// Should be redirected to login
5858
expect(page.url()).toContain('login');
@@ -64,7 +64,7 @@ test.describe('Access Control', () => {
6464
}) => {
6565
// Try to access delete account without logging in
6666
await deleteAccountPage.goto();
67-
await page.waitForLoadState('networkidle');
67+
await page.waitForLoadState('domcontentloaded');
6868

6969
// Should be redirected to login
7070
expect(page.url()).toContain('login');
@@ -76,7 +76,7 @@ test.describe('Access Control', () => {
7676
page,
7777
}) => {
7878
await page.goto('/');
79-
await page.waitForLoadState('networkidle');
79+
await page.waitForLoadState('domcontentloaded');
8080

8181
// Should stay on home page
8282
expect(page.url()).not.toContain('login');
@@ -87,7 +87,7 @@ test.describe('Access Control', () => {
8787
loginPage,
8888
}) => {
8989
await loginPage.goto();
90-
await page.waitForLoadState('networkidle');
90+
await page.waitForLoadState('domcontentloaded');
9191

9292
// Should be on login page
9393
expect(page.url()).toContain('login');
@@ -98,7 +98,7 @@ test.describe('Access Control', () => {
9898
registerPage,
9999
}) => {
100100
await registerPage.goto();
101-
await page.waitForLoadState('networkidle');
101+
await page.waitForLoadState('domcontentloaded');
102102

103103
// Should be on registration page
104104
expect(page.url()).toContain('register');
@@ -109,7 +109,7 @@ test.describe('Access Control', () => {
109109
forgotPasswordPage,
110110
}) => {
111111
await forgotPasswordPage.goto();
112-
await page.waitForLoadState('networkidle');
112+
await page.waitForLoadState('domcontentloaded');
113113

114114
// Should be on forgot password page
115115
expect(page.url()).toContain('forgot-password');
@@ -120,7 +120,7 @@ test.describe('Access Control', () => {
120120
eventListPage,
121121
}) => {
122122
await eventListPage.goto();
123-
await page.waitForLoadState('networkidle');
123+
await page.waitForLoadState('domcontentloaded');
124124

125125
// Should be on events page
126126
expect(page.url()).toContain('event');
@@ -130,7 +130,7 @@ test.describe('Access Control', () => {
130130
page,
131131
}) => {
132132
await page.goto('/about.html');
133-
await page.waitForLoadState('networkidle');
133+
await page.waitForLoadState('domcontentloaded');
134134

135135
// Should be on about page (not redirected)
136136
expect(page.url()).toContain('about');
@@ -152,7 +152,7 @@ test.describe('Access Control', () => {
152152

153153
// Try to access admin page
154154
await adminActionsPage.goto();
155-
await page.waitForLoadState('networkidle');
155+
await page.waitForLoadState('domcontentloaded');
156156

157157
// Should be denied (403 or error page)
158158
// With @PreAuthorize, the URL stays the same but shows error page
@@ -184,11 +184,11 @@ test.describe('Access Control', () => {
184184

185185
// Navigate to multiple protected pages
186186
await updateUserPage.goto();
187-
await page.waitForLoadState('networkidle');
187+
await page.waitForLoadState('domcontentloaded');
188188
expect(page.url()).toContain('update-user');
189189

190190
await page.goto('/event/my-events.html');
191-
await page.waitForLoadState('networkidle');
191+
await page.waitForLoadState('domcontentloaded');
192192
expect(page.url()).toContain('my-events');
193193

194194
// Should still be logged in
@@ -213,11 +213,11 @@ test.describe('Access Control', () => {
213213

214214
// Logout
215215
await loginPage.logout();
216-
await page.waitForLoadState('networkidle');
216+
await page.waitForLoadState('domcontentloaded');
217217

218218
// Try to access protected page
219219
await updateUserPage.goto();
220-
await page.waitForLoadState('networkidle');
220+
await page.waitForLoadState('domcontentloaded');
221221

222222
// Should be redirected to login
223223
expect(page.url()).toContain('login');

playwright/tests/auth/email-verification.spec.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ test.describe('Email Verification', () => {
3333
expect(verificationUrl).not.toBeNull();
3434

3535
await page.goto(verificationUrl!);
36-
await page.waitForLoadState('networkidle');
36+
await page.waitForLoadState('domcontentloaded');
3737

3838
// Should redirect to registration complete page
3939
expect(page.url()).toContain('registration-complete');
@@ -83,7 +83,7 @@ test.describe('Email Verification', () => {
8383
}) => {
8484
// Navigate to verification URL with invalid token
8585
await page.goto('/user/registrationConfirm?token=invalid-token-12345');
86-
await page.waitForLoadState('networkidle');
86+
await page.waitForLoadState('domcontentloaded');
8787

8888
// Should show error or redirect to error page
8989
const url = page.url();
@@ -121,7 +121,7 @@ test.describe('Email Verification', () => {
121121

122122
// Use a fake expired token (any invalid UUID)
123123
await page.goto('/user/registrationConfirm?token=expired-invalid-token-12345');
124-
await page.waitForLoadState('networkidle');
124+
await page.waitForLoadState('domcontentloaded');
125125

126126
// Should show error (same handling as invalid token)
127127
const url = page.url();
@@ -168,7 +168,7 @@ test.describe('Email Verification', () => {
168168

169169
// Try to use the same token again
170170
await page.goto(verificationUrl!);
171-
await page.waitForLoadState('networkidle');
171+
await page.waitForLoadState('domcontentloaded');
172172

173173
// Should either show error or redirect to registration-complete (idempotent behavior)
174174
// Both are acceptable - key thing is user stays verified
@@ -197,7 +197,7 @@ test.describe('Email Verification', () => {
197197

198198
// Navigate to resend verification page
199199
await page.goto('/user/request-new-verification-email.html');
200-
await page.waitForLoadState('networkidle');
200+
await page.waitForLoadState('domcontentloaded');
201201

202202
// Page should load (specific implementation may vary)
203203
expect(page.url()).toContain('verification');

playwright/tests/auth/login.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ test.describe('Login', () => {
6060
await loginPage.submit();
6161

6262
// Should be redirected to originally requested page or success page
63-
await page.waitForLoadState('networkidle');
63+
await page.waitForLoadState('domcontentloaded');
6464
});
6565
});
6666

@@ -180,7 +180,7 @@ test.describe('Login', () => {
180180
await loginPage.submit();
181181

182182
// Should show error or redirect to verification page
183-
await page.waitForLoadState('networkidle');
183+
await page.waitForLoadState('domcontentloaded');
184184
const url = page.url();
185185
const hasError = await loginPage.hasError();
186186

playwright/tests/auth/password-reset.spec.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ test.describe('Password Reset', () => {
3939
await forgotPasswordPage.requestReset('nonexistent-user-12345@example.com');
4040

4141
// Wait for response
42-
await page.waitForLoadState('networkidle');
42+
await page.waitForLoadState('domcontentloaded');
4343

4444
// Should either show generic message (for security) or redirect to pending page
4545
// Most secure implementations show success even for non-existent emails
@@ -81,7 +81,7 @@ test.describe('Password Reset', () => {
8181

8282
// Navigate to reset page
8383
await page.goto(resetUrl!);
84-
await page.waitForLoadState('networkidle');
84+
await page.waitForLoadState('domcontentloaded');
8585

8686
// Fill in new password
8787
const newPassword = 'NewTest@Pass456!';
@@ -123,7 +123,7 @@ test.describe('Password Reset', () => {
123123
await forgotPasswordPage.requestResetAndWait(user.email);
124124
const resetUrl = await testApiClient.getPasswordResetUrl(user.email);
125125
await page.goto(resetUrl!);
126-
await page.waitForLoadState('networkidle');
126+
await page.waitForLoadState('domcontentloaded');
127127

128128
const newPassword = 'NewTest@Pass789!';
129129
await forgotPasswordChangePage.fillForm(newPassword);
@@ -136,7 +136,7 @@ test.describe('Password Reset', () => {
136136
await loginPage.goto();
137137
await loginPage.fillCredentials(user.email, originalPassword);
138138
await loginPage.submit();
139-
await page.waitForLoadState('networkidle');
139+
await page.waitForLoadState('domcontentloaded');
140140

141141
// Should NOT be logged in (old password should fail)
142142
// The login page redirects back to itself on failure
@@ -150,7 +150,7 @@ test.describe('Password Reset', () => {
150150
}) => {
151151
// Navigate to reset page with invalid token
152152
await page.goto('/user/changePassword?token=invalid-reset-token-12345');
153-
await page.waitForLoadState('networkidle');
153+
await page.waitForLoadState('domcontentloaded');
154154

155155
// Should show error
156156
const url = page.url();
@@ -189,7 +189,7 @@ test.describe('Password Reset', () => {
189189
// Get reset token URL
190190
const resetUrl = await testApiClient.getPasswordResetUrl(user.email);
191191
await page.goto(resetUrl!);
192-
await page.waitForLoadState('networkidle');
192+
await page.waitForLoadState('domcontentloaded');
193193

194194
// Try to set a weak password
195195
await forgotPasswordChangePage.fillForm('weak');
@@ -229,12 +229,12 @@ test.describe('Password Reset', () => {
229229
// Get reset token URL
230230
const resetUrl = await testApiClient.getPasswordResetUrl(user.email);
231231
await page.goto(resetUrl!);
232-
await page.waitForLoadState('networkidle');
232+
await page.waitForLoadState('domcontentloaded');
233233

234234
// Try to set mismatched passwords
235235
await forgotPasswordChangePage.fillForm('NewTest@Pass123!', 'DifferentPass@456!');
236236
await forgotPasswordChangePage.submit();
237-
await page.waitForLoadState('networkidle');
237+
await page.waitForLoadState('domcontentloaded');
238238

239239
// Should show error or stay on page (client-side validation)
240240
});

0 commit comments

Comments
 (0)