How to Test Web Push Notification Preferences
Short answer
Web push spans permission prompts, preference persistence, service worker registration, and GDPR opt-in proof—Playwright cannot deliver real OS notifications in every CI run. Test permission grant/deny flows, preference toggles with probe Assert, stub push delivery in test env, and document manual/device-farm coverage for payload deep links—not notification tray appearance in headless Linux.
Part of Testing Guides by integrations.
Who this is for
PWAs and mobile web apps using Web Push API (FCM web, OneSignal, custom service workers) for transactional or marketing notifications—with in-app preference centers.
Why testing push preferences matters
- GDPR/CCPA — marketing push without documented opt-in
- Broken unsubscribe — users still receive after toggle off
- Permission UX — app crashes when
Notification.permission === 'denied' - Stale subscriptions — expired push endpoint not cleaned on logout
Complexity map
| Scenario | Edge case | Why tests break | Approach |
|---|---|---|---|
| Permission denied | Feature crash | Uncaught throw | Assert graceful degrade |
| Permission granted | SW register fail | Silent no push | Probe subscription row |
| Unsubscribe toggle | DB not updated | Still receiving | Probe preference false |
| Logout | Endpoint orphaned | Ghost pushes | Probe endpoint deleted |
| Marketing vs transactional | Wrong category | Compliance | Separate toggles + probe |
| Double opt-in | Confirm step skipped | Illegal send | Two-step UI + probe |
| iOS Safari | Limited push | Untested platform | Document manual matrix |
| VAPID rotation | Old endpoints invalid | Mass failure | Probe re-subscribe flow |
| Deep link payload | URL routing | Manual only | Stub push handler in test |
| Service worker update | Two SW versions | Duplicate handlers | Probe single registration |
Grant notification permission (Playwright)
test.use({ permissions: ['notifications'] });
test('opt-in saves preference', async ({ page, request }) => {
await page.goto('/settings/notifications');
await page.getByRole('switch', { name: /product updates/i }).check();
await expect.poll(async () => {
const res = await request.get(`/api/test/probe-push-prefs?runId=${runId}`);
return (await res.json()).productUpdates;
}).toBe(true);
});
From Playwright permissions.
Denied permission path
test('denied permission degrades gracefully', async ({ context, page }) => {
await context.clearPermissions();
// Some browsers default deny in headless
await page.goto('/settings/notifications');
await expect(page.getByText(/notifications blocked|enable in browser/i)).toBeVisible();
await expect(page.getByRole('switch', { name: /product updates/i })).toBeDisabled();
});
No uncaught errors in console when permission denied.
Unsubscribe / opt-out
await page.getByRole('switch', { name: /marketing/i }).uncheck();
await expect.poll(() => probePushPref(runId, 'marketing')).toBe(false);
// Stub push sender should not target user—probe last_send log empty
await triggerMarketingCampaignStub(runId);
await expect.poll(() => probePushSent(runId, 'marketing')).toBe(0);
Service worker stub in test env
Full push delivery to OS is unreliable in CI. Stub:
// Test build registers mock SW that writes subscription to /api/test/push-subscribe
await page.evaluate(() => navigator.serviceWorker.register('/test-sw.js'));
await page.getByRole('button', { name: /enable notifications/i }).click();
await expect.poll(() => probePushSubscription(runId)).toMatchObject({ endpoint: expect.any(String) });
Nightly or manual: real FCM test message to physical device.
Logout cleans subscription
await page.getByRole('button', { name: /log out/i }).click();
await expect.poll(() => probePushSubscription(runId)).toBeNull();
Anti-patterns
| Anti-pattern | Why it fails | Better approach |
|---|---|---|
| Assert OS notification tray | Not available headless | Probe prefs + stub send |
| Skip denied permission | Prod crash | Negative spec |
| UI toggle only | DB stale | Probe preference row |
| Marketing on by default | GDPR risk | Opt-in scenario + probe |
| No logout cleanup test | Ghost endpoints | Probe null after logout |
Example scenario
Situation: User opts out of marketing push after previously opting in.
Expected outcome: Preference persisted; stub campaign sends zero messages to user.
Why UI-only automation breaks: Toggle off in UI but probe marketing=true—user still gets pushes.
- Arrange: Seed user with marketing push enabled and subscription endpoint.
- Act: Uncheck marketing switch in settings.
- Assert: Probe marketing false; probe push sent count 0 after campaign stub.
TestChimp workflow: Track notification_type when marketing opt-in path untested in prod slice.
Same Arrange/Act/Assert pattern as expired-coupon checkout.
Connect scenarios to your QA workflow
Capture business rules in markdown test plans and enforce them with seed routes and probe Assert. Link SmartTests with // @Scenario: for requirement traceability. Use /testchimp test on PRs; /testchimp explore on SmartTest paths for non-functional gaps (ExploreChimp).
Related scenarios
- GDPR privacy — consent records
- SaaS onboarding — early opt-in prompts
- Transactional email — parallel channel
External references
Frequently asked questions
Can Playwright test web push notifications?
Test permission UI, preference toggles, and service worker registration with probes. Stub push delivery in test env—full OS notification display is manual or device farm.
How do I grant notification permission in tests?
test.use({ permissions: ['notifications'] }) or context.grantPermissions before goto. Assert probe subscription row created.
How do I test permission denied?
clearPermissions, load settings, assert graceful message and disabled controls—no console throw.
How do I verify unsubscribe works?
Uncheck preference, probe false, run stub campaign, probe zero sends to user endpoint.
Marketing vs transactional push?
Separate toggles and probes. Marketing requires explicit opt-in scenario for GDPR—probe consent timestamp.
Logout and push endpoints?
Probe subscription deleted or invalidated on logout—prevent ghost pushes to prior user on shared device.
Marketing opt-in popular in prod but untested?
TrueCoverage notification_type slice shows gap. Add opt-in/out SmartTests via /testchimp evolve.
Apply these patterns in your repo
Run `/testchimp init` to connect TestChimp to your repo, then `/testchimp test` on PRs to turn these patterns into maintained SmartTests. Use `/testchimp evolve` when you want to expand coverage as your app grows.