Skip to content

Commit ccd4065

Browse files
committed
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
1 parent a3181de commit ccd4065

41 files changed

Lines changed: 183 additions & 1779 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: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -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)