UAT-3: browser gate (hard stop) + iPad Safari fix
Spec CAPTURE.md §Browsers: Phone iOS Safari / Android Chrome, PC Chrome / Edge / Firefox, others hard stop. - browserGate.js: UA detection + allowlist (pure, DOM-free) - +layout.svelte: renders hard-stop panel when unsupported - iPadOS 13+ reports as Macintosh — added Safari rule (Version/ + Safari/ without Chrome/) so iPad Safari is not blocked - 27 unit tests incl. iPad Safari regression - 107/107 full suite green
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* UAT-3: Browser gate (hard stop) — unit tests for the pure logic in
|
||||
* src/lib/core/browserGate.js.
|
||||
*
|
||||
* CAPTURE.md §Browsers:
|
||||
* Phone: iOS Safari, Android Chrome.
|
||||
* PC: Chrome, Edge, Firefox.
|
||||
* Other clients get a hard stop.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { detectBrowser, isAllowed, checkBrowserGate } from '$lib/core/browserGate.js';
|
||||
|
||||
// --- representative user agents ------------------------------------
|
||||
const UAs = {
|
||||
iosSafari:
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/605.1.15',
|
||||
ipadSafari:
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15',
|
||||
iosChrome:
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/124.0.6336.69 Mobile/15E148 Safari/605.1.15',
|
||||
androidChrome:
|
||||
'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6331.115 Mobile Safari/537.36',
|
||||
desktopChrome:
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
||||
desktopEdge:
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0',
|
||||
desktopFirefox:
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0',
|
||||
desktopOpera:
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 OPR/110.0.0.0',
|
||||
androidOpera:
|
||||
'Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36 OPR/110.0.0.0',
|
||||
androidSamsung:
|
||||
'Mozilla/5.0 (Linux; Android 14; SM-S918B) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/24.0 Chrome/124.0.0.0 Mobile Safari/537.36',
|
||||
iosSamsung:
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/124.0.0.0 Mobile/15E148 SamsungBrowser/124.0.0.0',
|
||||
androidBrave:
|
||||
'Mozilla/5.0 (Linux; Android 14; SM-A546E) AppleWebKit/537.36 (KHTML, like Gecko) Version/14.0.0.5336 Mobile Safari/537.36',
|
||||
unknown:
|
||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) CustomBrowser/9.9.0 Safari/537.36',
|
||||
blank: '',
|
||||
};
|
||||
|
||||
describe('detectBrowser', () => {
|
||||
it('identifies iOS Safari on iPhone', () => {
|
||||
const b = detectBrowser(UAs.iosSafari);
|
||||
expect(b.name).toBe('iOS Safari');
|
||||
expect(b.platform).toBe('phone');
|
||||
});
|
||||
|
||||
it('identifies iPad Safari (iPadOS 13+ reports as Macintosh)', () => {
|
||||
const b = detectBrowser(UAs.ipadSafari);
|
||||
expect(b.name).toBe('Safari');
|
||||
expect(b.platform).toBe('pc');
|
||||
});
|
||||
|
||||
it('identifies Android Chrome', () => {
|
||||
const b = detectBrowser(UAs.androidChrome);
|
||||
expect(b.name).toBe('Android Chrome');
|
||||
expect(b.platform).toBe('phone');
|
||||
});
|
||||
|
||||
it('identifies desktop Chrome', () => {
|
||||
const b = detectBrowser(UAs.desktopChrome);
|
||||
expect(b.name).toBe('Chrome');
|
||||
expect(b.platform).toBe('pc');
|
||||
});
|
||||
|
||||
it('identifies desktop Edge (not Chrome)', () => {
|
||||
const b = detectBrowser(UAs.desktopEdge);
|
||||
expect(b.name).toBe('Microsoft Edge');
|
||||
expect(b.platform).toBe('pc');
|
||||
});
|
||||
|
||||
it('identifies desktop Firefox', () => {
|
||||
const b = detectBrowser(UAs.desktopFirefox);
|
||||
expect(b.name).toBe('Firefox');
|
||||
expect(b.platform).toBe('pc');
|
||||
});
|
||||
|
||||
it('identifies desktop Opera as its own browser (not Chrome)', () => {
|
||||
const b = detectBrowser(UAs.desktopOpera);
|
||||
expect(b.name).toBe('Opera');
|
||||
expect(b.platform).toBe('pc');
|
||||
});
|
||||
|
||||
it('identifies Android Opera (OPR beats Chrome)', () => {
|
||||
const b = detectBrowser(UAs.androidOpera);
|
||||
expect(b.name).toBe('Opera');
|
||||
expect(b.platform).toBe('phone');
|
||||
});
|
||||
|
||||
it('identifies Samsung Internet (SamsungBrowser beats Chrome)', () => {
|
||||
const b = detectBrowser(UAs.androidSamsung);
|
||||
expect(b.name).toBe('Samsung Internet');
|
||||
expect(b.platform).toBe('phone');
|
||||
});
|
||||
|
||||
it('identifies iOS Samsung Internet as unsupported', () => {
|
||||
const b = detectBrowser(UAs.iosSamsung);
|
||||
expect(b.name).toBe('Samsung Internet');
|
||||
expect(b.platform).toBe('phone');
|
||||
});
|
||||
|
||||
it('treats an unrecognized UA as Unknown', () => {
|
||||
const b = detectBrowser(UAs.unknown);
|
||||
expect(b.name).toBe('Unknown');
|
||||
expect(b.platform).toBe('pc');
|
||||
});
|
||||
|
||||
it('handles blank / undefined user agents without throwing', () => {
|
||||
expect(detectBrowser(UAs.blank).name).toBe('Unknown');
|
||||
expect(detectBrowser(undefined).name).toBe('Unknown');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAllowed', () => {
|
||||
it('allows iOS Safari on phone', () => {
|
||||
expect(isAllowed({ name: 'iOS Safari', platform: 'phone' })).toBe(true);
|
||||
});
|
||||
|
||||
it('allows Android Chrome on phone', () => {
|
||||
expect(isAllowed({ name: 'Android Chrome', platform: 'phone' })).toBe(true);
|
||||
});
|
||||
|
||||
it('allows Chrome on PC', () => {
|
||||
expect(isAllowed({ name: 'Chrome', platform: 'pc' })).toBe(true);
|
||||
});
|
||||
|
||||
it('allows Edge on PC', () => {
|
||||
expect(isAllowed({ name: 'Microsoft Edge', platform: 'pc' })).toBe(true);
|
||||
});
|
||||
|
||||
it('allows Firefox on PC', () => {
|
||||
expect(isAllowed({ name: 'Firefox', platform: 'pc' })).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects Opera', () => {
|
||||
expect(isAllowed({ name: 'Opera', platform: 'pc' })).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects Android Opera', () => {
|
||||
expect(isAllowed({ name: 'Opera', platform: 'phone' })).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects Samsung Internet', () => {
|
||||
expect(isAllowed({ name: 'Samsung Internet', platform: 'phone' })).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects Samsung Internet on iOS', () => {
|
||||
expect(isAllowed({ name: 'Samsung Internet', platform: 'phone' })).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects Brave', () => {
|
||||
expect(isAllowed({ name: 'Brave', platform: 'phone' })).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects Unknown', () => {
|
||||
expect(isAllowed({ name: 'Unknown', platform: 'pc' })).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects phone browsers on PC (impossible but rejected)', () => {
|
||||
expect(isAllowed({ name: 'iOS Safari', platform: 'pc' })).toBe(false);
|
||||
expect(isAllowed({ name: 'Android Chrome', platform: 'pc' })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkBrowserGate', () => {
|
||||
it('supports exactly the five spec browsers + Safari (iPad)', () => {
|
||||
for (const ua of [
|
||||
UAs.iosSafari,
|
||||
UAs.ipadSafari,
|
||||
UAs.androidChrome,
|
||||
UAs.desktopChrome,
|
||||
UAs.desktopEdge,
|
||||
UAs.desktopFirefox,
|
||||
]) {
|
||||
expect(checkBrowserGate(ua).supported).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('hard-stops Opera, Samsung Internet, unknown, blank', () => {
|
||||
for (const ua of [
|
||||
UAs.desktopOpera,
|
||||
UAs.androidOpera,
|
||||
UAs.androidSamsung,
|
||||
UAs.iosSamsung,
|
||||
UAs.androidBrave,
|
||||
UAs.unknown,
|
||||
UAs.blank,
|
||||
]) {
|
||||
expect(checkBrowserGate(ua).supported).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('returns the detected browser for the UI', () => {
|
||||
const r = checkBrowserGate(UAs.desktopOpera);
|
||||
expect(r.browser.name).toBe('Opera');
|
||||
expect(r.browser.platform).toBe('pc');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user