Skip to content

Commit c4509c0

Browse files
committed
Modify how errors are validated and provide a summary. Include missing toggle code
1 parent f0981c3 commit c4509c0

7 files changed

Lines changed: 966 additions & 47 deletions

File tree

e2e-tests/tests/newsletter-signup-loggedout.spec.js

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,4 +285,201 @@ test.describe('Newsletter Signup - Logged-Out User Flow', () => {
285285
expect(isYesVisible).toBe(false);
286286
expect(isNoVisible).toBe(false);
287287
});
288+
289+
// ========== MULTI-ERROR VALIDATION TESTS ==========
290+
291+
test('should show all validation errors at once when submitting empty form', async ({ page }) => {
292+
// Submit empty form
293+
const submitButton = page.locator('button:has-text("Submit")').first();
294+
await submitButton.click();
295+
296+
// Wait for validation to complete
297+
await page.waitForTimeout(500);
298+
299+
// Should see error summary at the top
300+
const errorSummary = page.locator('.newsletterErrorSummary');
301+
await expect(errorSummary).toBeVisible();
302+
303+
// Should list multiple errors in the summary
304+
const errorItems = page.locator('.errorSummaryList li');
305+
const errorCount = await errorItems.count();
306+
expect(errorCount).toBeGreaterThanOrEqual(3); // firstName, email, newsletters at minimum
307+
308+
// Should have inline errors above fields
309+
const inlineErrors = page.locator('.inlineFieldError');
310+
const inlineCount = await inlineErrors.count();
311+
expect(inlineCount).toBeGreaterThanOrEqual(3);
312+
313+
// Verify specific errors are shown
314+
const firstNameError = page.locator('#firstName-error');
315+
const emailError = page.locator('#email-error');
316+
const newslettersError = page.locator('#newsletters-error');
317+
318+
await expect(firstNameError).toBeVisible();
319+
await expect(emailError).toBeVisible();
320+
await expect(newslettersError).toBeVisible();
321+
});
322+
323+
test('should focus error summary on validation failure for accessibility', async ({ page }) => {
324+
// Submit empty form
325+
const submitButton = page.locator('button:has-text("Submit")').first();
326+
await submitButton.click();
327+
328+
// Wait for validation and focus management
329+
await page.waitForTimeout(500);
330+
331+
// Error summary should be focused (for screen reader users)
332+
const errorSummary = page.locator('.newsletterErrorSummary');
333+
await expect(errorSummary).toBeFocused();
334+
});
335+
336+
test('should clear inline error when field is fixed and loses focus', async ({ page }) => {
337+
// Submit empty form to trigger errors
338+
await page.locator('button:has-text("Submit")').first().click();
339+
await page.waitForTimeout(500);
340+
341+
// Verify firstName error exists
342+
const firstNameError = page.locator('#firstName-error');
343+
await expect(firstNameError).toBeVisible();
344+
345+
// Fill in first name
346+
const firstNameInput = page.locator('input#firstName');
347+
await firstNameInput.fill('John');
348+
349+
// Error should still be visible while typing (not cleared on change)
350+
await expect(firstNameError).toBeVisible();
351+
352+
// Blur the field (click somewhere else or tab away)
353+
await firstNameInput.blur();
354+
355+
// Wait for state update
356+
await page.waitForTimeout(100);
357+
358+
// Now error should be cleared
359+
await expect(firstNameError).not.toBeVisible();
360+
361+
// But other errors should still be visible
362+
const emailError = page.locator('#email-error');
363+
await expect(emailError).toBeVisible();
364+
});
365+
366+
test('should update error summary when errors are fixed', async ({ page }) => {
367+
// Submit empty form to trigger all errors
368+
await page.locator('button:has-text("Submit")').first().click();
369+
await page.waitForTimeout(500);
370+
371+
// Count initial errors
372+
const initialErrorCount = await page.locator('.errorSummaryList li').count();
373+
expect(initialErrorCount).toBeGreaterThanOrEqual(3);
374+
375+
// Fix first name
376+
await page.locator('input#firstName').fill('John');
377+
await page.locator('input#firstName').blur();
378+
await page.waitForTimeout(100);
379+
380+
// Error count should decrease
381+
const afterFirstFix = await page.locator('.errorSummaryList li').count();
382+
expect(afterFirstFix).toBe(initialErrorCount - 1);
383+
384+
// Fix email
385+
await page.locator('input#email').fill('john@example.com');
386+
await page.locator('input#email').blur();
387+
await page.waitForTimeout(100);
388+
389+
const afterEmailFix = await page.locator('.errorSummaryList li').count();
390+
expect(afterEmailFix).toBe(afterFirstFix - 1);
391+
});
392+
393+
test('should show error styling on invalid input fields', async ({ page }) => {
394+
// Submit empty form
395+
await page.locator('button:has-text("Submit")').first().click();
396+
await page.waitForTimeout(500);
397+
398+
// Input fields should have error styling
399+
const firstNameInput = page.locator('input#firstName');
400+
await expect(firstNameInput).toHaveAttribute('aria-invalid', 'true');
401+
await expect(firstNameInput).toHaveClass(/hasError/);
402+
403+
const emailInput = page.locator('input#email');
404+
await expect(emailInput).toHaveAttribute('aria-invalid', 'true');
405+
await expect(emailInput).toHaveClass(/hasError/);
406+
});
407+
408+
test('should allow clicking error summary links to navigate to fields', async ({ page }) => {
409+
// Submit empty form
410+
await page.locator('button:has-text("Submit")').first().click();
411+
await page.waitForTimeout(500);
412+
413+
// Click the email error link in the summary
414+
const emailErrorLink = page.locator('.errorSummaryLink[href="#email"]');
415+
await emailErrorLink.click();
416+
417+
// Email input should now be focused (due to href="#email" navigation)
418+
// Note: This works because the input has id="email"
419+
await page.waitForTimeout(100);
420+
421+
// The email input should be scrolled into view and potentially focused
422+
const emailInput = page.locator('input#email');
423+
await expect(emailInput).toBeInViewport();
424+
});
425+
426+
test('should validate email format and show appropriate error', async ({ page }) => {
427+
// Fill invalid email
428+
await page.locator('input#firstName').fill('John');
429+
await page.locator('input#email').fill('not-an-email');
430+
await page.locator('input#confirmEmail').fill('not-an-email');
431+
432+
// Select a newsletter
433+
const checkboxLabels = page.locator('label.newsletterCheckboxLabel');
434+
await checkboxLabels.nth(0).click();
435+
436+
// Submit
437+
await page.locator('button:has-text("Submit")').first().click();
438+
await page.waitForTimeout(500);
439+
440+
// Should show email format error
441+
const emailError = page.locator('#email-error');
442+
await expect(emailError).toBeVisible();
443+
await expect(emailError).toContainText('valid email');
444+
});
445+
446+
test('should show mismatched email error when emails do not match', async ({ page }) => {
447+
// Fill form with mismatched emails
448+
await page.locator('input#firstName').fill('John');
449+
await page.locator('input#email').fill('john@example.com');
450+
await page.locator('input#confirmEmail').fill('jane@example.com');
451+
452+
// Select a newsletter
453+
const checkboxLabels = page.locator('label.newsletterCheckboxLabel');
454+
await checkboxLabels.nth(0).click();
455+
456+
// Submit
457+
await page.locator('button:has-text("Submit")').first().click();
458+
await page.waitForTimeout(500);
459+
460+
// Should show email mismatch error
461+
const confirmEmailError = page.locator('#confirmEmail-error');
462+
await expect(confirmEmailError).toBeVisible();
463+
await expect(confirmEmailError).toContainText('do not match');
464+
});
465+
466+
test('should clear newsletter error when a newsletter is selected', async ({ page }) => {
467+
// Submit empty form to trigger errors
468+
await page.locator('button:has-text("Submit")').first().click();
469+
await page.waitForTimeout(500);
470+
471+
// Verify newsletters error exists
472+
const newslettersError = page.locator('#newsletters-error');
473+
await expect(newslettersError).toBeVisible();
474+
475+
// Select a newsletter
476+
const checkboxLabels = page.locator('label.newsletterCheckboxLabel');
477+
await checkboxLabels.nth(0).click();
478+
479+
// Wait for state update
480+
await page.waitForTimeout(200);
481+
482+
// Newsletter error should be cleared
483+
await expect(newslettersError).not.toBeVisible();
484+
});
288485
});

static/css/newsletter-signup-page.css

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,123 @@
244244
flex-shrink: 0;
245245
}
246246

247+
/* ============================================
248+
ERROR SUMMARY - Multi-error display at top of form
249+
============================================ */
250+
251+
.newsletterFormView .newsletterErrorSummary {
252+
background-color: #fef2f2;
253+
border: 2px solid #dc2626;
254+
border-radius: 8px;
255+
padding: 16px 20px;
256+
margin-bottom: 24px;
257+
}
258+
259+
.newsletterFormView .newsletterErrorSummary:focus {
260+
outline: 2px solid #dc2626;
261+
outline-offset: 2px;
262+
}
263+
264+
.newsletterFormView .errorSummaryTitle {
265+
color: #dc2626;
266+
font-size: 16px;
267+
font-weight: 600;
268+
margin: 0 0 12px 0;
269+
display: flex;
270+
align-items: center;
271+
gap: 8px;
272+
font-family: var(--english-sans-serif-font-family);
273+
}
274+
275+
.interface-hebrew .newsletterFormView .errorSummaryTitle {
276+
font-family: var(--hebrew-sans-serif-font-family);
277+
}
278+
279+
.newsletterFormView .errorSummaryList {
280+
margin: 0;
281+
padding-left: 24px;
282+
list-style-type: disc;
283+
}
284+
285+
.interface-hebrew .newsletterFormView .errorSummaryList {
286+
padding-left: 0;
287+
padding-right: 24px;
288+
}
289+
290+
.newsletterFormView .errorSummaryList li {
291+
color: #dc2626;
292+
margin-bottom: 4px;
293+
font-family: var(--english-sans-serif-font-family);
294+
}
295+
296+
.interface-hebrew .newsletterFormView .errorSummaryList li {
297+
font-family: var(--hebrew-sans-serif-font-family);
298+
}
299+
300+
.newsletterFormView .errorSummaryLink {
301+
color: #dc2626;
302+
text-decoration: underline;
303+
}
304+
305+
.newsletterFormView .errorSummaryLink:hover {
306+
text-decoration: none;
307+
}
308+
309+
/* ============================================
310+
INLINE FIELD ERRORS - Above each invalid field
311+
============================================ */
312+
313+
.newsletterFormView .inlineFieldError {
314+
color: #dc2626;
315+
font-size: 14px;
316+
margin-bottom: 6px;
317+
font-weight: 500;
318+
font-family: var(--english-sans-serif-font-family);
319+
}
320+
321+
.interface-hebrew .newsletterFormView .inlineFieldError {
322+
font-family: var(--hebrew-sans-serif-font-family);
323+
}
324+
325+
/* ============================================
326+
INPUT ERROR STATE - Visual feedback on invalid fields
327+
Note: Must use [type] selectors to match specificity of base input styles
328+
============================================ */
329+
330+
.newsletterFormView input[type="text"].hasError,
331+
.newsletterFormView input[type="email"].hasError,
332+
.newsletterFormView input[type="text"][aria-invalid="true"],
333+
.newsletterFormView input[type="email"][aria-invalid="true"] {
334+
border-color: #dc2626 !important;
335+
background-color: #fef2f2 !important;
336+
}
337+
338+
.newsletterFormView input[type="text"].hasError:focus,
339+
.newsletterFormView input[type="email"].hasError:focus,
340+
.newsletterFormView input[type="text"][aria-invalid="true"]:focus,
341+
.newsletterFormView input[type="email"][aria-invalid="true"]:focus {
342+
outline: none;
343+
border-color: #dc2626 !important;
344+
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.2) !important;
345+
}
346+
347+
/* Newsletter section error state */
348+
.newsletterFormView .newsletterCheckboxes.hasError {
349+
border: 2px solid #dc2626;
350+
border-radius: 8px;
351+
padding: 12px;
352+
margin-top: 0; /* Don't pull up into the error message */
353+
margin-left: -12px;
354+
margin-right: -12px;
355+
margin-bottom: -12px;
356+
background-color: #fef2f2;
357+
}
358+
359+
/* Extra spacing for newsletter inline error */
360+
.newsletterFormView .newsletterSelectionSection > .inlineFieldError {
361+
margin-bottom: 12px;
362+
}
363+
247364
/* ============================================
248365
NEWSLETTER SELECTION SECTION
249366
============================================ */

0 commit comments

Comments
 (0)