62 lines
4.7 KiB
JavaScript
62 lines
4.7 KiB
JavaScript
import {test,expect} from '@playwright/test';
|
||
// Mock permission/subscription APIs, not the page UI or backend subscription API.
|
||
// Browser vendor push enrollment requires real user/device consent and credentials.
|
||
async function mockPush(page,{denied=false,failSave=false}={}){
|
||
await page.addInitScript(({denied})=>{
|
||
let current=null;
|
||
let permission=denied?'denied':'default';
|
||
const encode=bytes=>btoa(String.fromCharCode(...bytes)).replace(/\+/g,'-').replace(/\//g,'_').replace(/=+$/,'');
|
||
const key=new Uint8Array(65);key[0]=4;key.fill(1,1);
|
||
const subscription={endpoint:'https://fcm.googleapis.com/fcm/send/browser-test',keys:{p256dh:encode(key),auth:encode(new Uint8Array(16).fill(2))}};
|
||
const manager={getSubscription:async()=>current,subscribe:async()=>{
|
||
current={toJSON:()=>subscription,unsubscribe:async()=>{current=null;return true;}};return current;
|
||
}};
|
||
const registration={pushManager:manager};
|
||
Object.defineProperty(Notification,'permission',{get:()=>permission});
|
||
Notification.requestPermission=async()=>{window.permissionRequests=(window.permissionRequests||0)+1;permission=denied?'denied':'granted';return permission;};
|
||
Object.defineProperty(navigator.serviceWorker,'register',{value:async()=>registration});
|
||
Object.defineProperty(navigator.serviceWorker,'ready',{value:Promise.resolve(registration)});
|
||
window.PushManager ||= function(){};
|
||
},{denied});
|
||
if(failSave)await page.route('**/api/push/subscribe',route=>route.fulfill({status:500,body:'{}',contentType:'application/json'}));
|
||
}
|
||
test('invitation is dismissible, fits mobile, and never asks permission on page load',async({page})=>{
|
||
await page.setViewportSize({width:390,height:844});await mockPush(page);await page.goto('/');
|
||
await expect(page.locator('#push-prompt')).toBeVisible();
|
||
expect(await page.evaluate(()=>window.permissionRequests||0)).toBe(0);
|
||
const box=await page.locator('#push-prompt').boundingBox();expect(box.x).toBeGreaterThanOrEqual(0);expect(box.x+box.width).toBeLessThanOrEqual(390);
|
||
await page.getByRole('button',{name:'Not now'}).click();await expect(page.locator('#push-prompt')).toBeHidden();
|
||
await page.reload();await expect(page.locator('#push-toggle')).toBeVisible();await expect(page.locator('#push-prompt')).toBeHidden();
|
||
await page.locator('#push-toggle').click();await expect(page.locator('#push-prompt')).toBeVisible();
|
||
});
|
||
test('enable and disable save preferences through the real API',async({page})=>{
|
||
await mockPush(page);await page.goto('/');await page.getByRole('button',{name:'Enable notifications',exact:true}).click();
|
||
await expect(page.locator('#push-status')).toContainText('You’re subscribed');expect(await page.evaluate(()=>window.permissionRequests)).toBe(1);
|
||
await expect(page.locator('#push-toggle')).toHaveText('Notifications on');
|
||
await page.getByRole('button',{name:'Turn off notifications'}).click();await expect(page.locator('#push-status')).toContainText('turned off');
|
||
});
|
||
test('denied permission is not prompted again on load',async({page})=>{
|
||
await mockPush(page,{denied:true});await page.goto('/');await expect(page.locator('#push-toggle')).toBeVisible();
|
||
await expect(page.locator('#push-prompt')).toBeHidden();expect(await page.evaluate(()=>window.permissionRequests||0)).toBe(0);
|
||
await page.locator('#push-toggle').click();await page.getByRole('button',{name:'Enable notifications',exact:true}).click();
|
||
await expect(page.locator('#push-status')).toContainText('browser’s site settings');
|
||
});
|
||
test('failed subscription persistence never claims success',async({page})=>{
|
||
await mockPush(page,{failSave:true});await page.goto('/');await page.getByRole('button',{name:'Enable notifications',exact:true}).click();
|
||
await expect(page.locator('#push-status')).toContainText('Could not save');await expect(page.locator('#push-toggle')).toHaveText('Get new stories');
|
||
});
|
||
test('service worker and app manifest are served from the root',async({request})=>{
|
||
const worker=await request.get('/sw.js');expect(worker.status()).toBe(200);expect(worker.headers()['content-type']).toContain('javascript');expect(worker.headers()['cache-control']).toBe('no-cache');
|
||
const manifest=await request.get('/manifest.webmanifest');expect(manifest.status()).toBe(200);expect((await manifest.json()).display).toBe('standalone');
|
||
const privateData=await request.get('/.data/push/vapid.json');expect(privateData.status()).toBe(404);
|
||
});
|
||
test('a real service worker registers with site-wide scope',async({page})=>{
|
||
await page.goto('/');
|
||
const registration=await page.evaluate(async()=>{
|
||
const worker=await navigator.serviceWorker.ready;
|
||
return {scope:worker.scope,script:worker.active.scriptURL};
|
||
});
|
||
expect(new URL(registration.scope).pathname).toBe('/');
|
||
expect(new URL(registration.script).pathname).toBe('/sw.js');
|
||
});
|