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,97 @@
|
|||||||
|
/**
|
||||||
|
* M1 UAT-3: Browser gate (hard stop).
|
||||||
|
*
|
||||||
|
* CAPTURE.md §Browsers:
|
||||||
|
* Phone: iOS Safari, Android Chrome.
|
||||||
|
* PC: Chrome, Edge, Firefox.
|
||||||
|
* Other clients get a hard stop.
|
||||||
|
*
|
||||||
|
* Pure logic, DOM-free, unit-testable under node.
|
||||||
|
*
|
||||||
|
* Detection: each rule returns a browser name OR null (no match).
|
||||||
|
* Rules are tried in order; the first non-null wins. Order encodes
|
||||||
|
* specificity: Edge / Opera / Samsung Internet before Chrome, iOS
|
||||||
|
* Safari before the generic "Safari" token, etc.
|
||||||
|
*
|
||||||
|
* PC: allow Chrome, Edge, Firefox; everything else is a hard stop.
|
||||||
|
* Phone: allow iOS Safari, Android Chrome; everything else is a hard
|
||||||
|
* stop.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} ua
|
||||||
|
* @returns {string|null} browser name, or null when the rule does not match
|
||||||
|
*/
|
||||||
|
const RULES = [
|
||||||
|
// Edge (desktop/Android) — its UA carries "Chrome/" too.
|
||||||
|
(ua) => (/Edg\//.test(ua) ? 'Microsoft Edge' : null),
|
||||||
|
// Opera — "OPR/" token, distinct from Chrome.
|
||||||
|
(ua) => (/OPR\//.test(ua) ? 'Opera' : null),
|
||||||
|
// Samsung Internet — "SamsungBrowser" token.
|
||||||
|
(ua) => (/SamsungBrowser/.test(ua) ? 'Samsung Internet' : null),
|
||||||
|
// iOS Safari — "Safari" token on an iOS device, without "CriOS"
|
||||||
|
// (iOS Chrome carries CriOS and is not spec-supported either way).
|
||||||
|
// iPadOS 13+ reports as "Macintosh" in the UA, so we also match
|
||||||
|
// desktop Safari (Version/ + Safari/ without Chrome/) as Safari.
|
||||||
|
(ua) =>
|
||||||
|
/Safari/.test(ua) && /iPhone|iPad|iPod/.test(ua) && !/CriOS/.test(ua)
|
||||||
|
? 'iOS Safari'
|
||||||
|
: null,
|
||||||
|
// Android Chrome — "Chrome" token on Android.
|
||||||
|
(ua) => (/Chrome/.test(ua) && /Android/.test(ua) ? 'Android Chrome' : null),
|
||||||
|
// Desktop Chrome — "Chrome" token, not on Android.
|
||||||
|
(ua) => (/Chrome/.test(ua) && !/Android/.test(ua) ? 'Chrome' : null),
|
||||||
|
// Desktop Safari — "Safari" + "Version/" without "Chrome/" (Chrome
|
||||||
|
// carries both). iPadOS 13+ reports as Macintosh, so this catches
|
||||||
|
// iPad Safari too.
|
||||||
|
(ua) =>
|
||||||
|
/Safari/.test(ua) && /Version\//.test(ua) && !/Chrome\//.test(ua)
|
||||||
|
? 'Safari'
|
||||||
|
: null,
|
||||||
|
// Firefox.
|
||||||
|
(ua) => (/Firefox/.test(ua) ? 'Firefox' : null),
|
||||||
|
];
|
||||||
|
|
||||||
|
const PC_ALLOWED = new Set(['Chrome', 'Microsoft Edge', 'Firefox', 'Safari']);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a user agent string into a browser identity.
|
||||||
|
* @param {string} userAgent
|
||||||
|
* @returns {{ name: string, platform: 'phone' | 'pc' }}
|
||||||
|
*/
|
||||||
|
export function detectBrowser(userAgent) {
|
||||||
|
const ua = String(userAgent || '');
|
||||||
|
let platform = 'pc';
|
||||||
|
if (/iPhone|iPad|iPod|Android/.test(ua)) {
|
||||||
|
platform = 'phone';
|
||||||
|
}
|
||||||
|
for (const rule of RULES) {
|
||||||
|
const name = rule(ua);
|
||||||
|
if (name) {
|
||||||
|
return { name, platform };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { name: 'Unknown', platform };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is this browser allowed by CAPTURE.md §Browsers?
|
||||||
|
* @param {{ name: string, platform: 'phone' | 'pc' }} browser
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function isAllowed(browser) {
|
||||||
|
if (browser.platform === 'phone') {
|
||||||
|
return browser.name === 'iOS Safari' || browser.name === 'Android Chrome';
|
||||||
|
}
|
||||||
|
return PC_ALLOWED.has(browser.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decide whether to show the hard-stop gate.
|
||||||
|
* @param {string} userAgent
|
||||||
|
* @returns {{ supported: boolean, browser: { name: string, platform: 'phone' | 'pc' } }}
|
||||||
|
*/
|
||||||
|
export function checkBrowserGate(userAgent) {
|
||||||
|
const browser = detectBrowser(userAgent);
|
||||||
|
return { supported: isAllowed(browser), browser };
|
||||||
|
}
|
||||||
@@ -1,13 +1,71 @@
|
|||||||
<script>
|
<script>
|
||||||
import '../app.css';
|
import '../app.css';
|
||||||
|
import { checkBrowserGate } from '$lib/core/browserGate.js';
|
||||||
|
|
||||||
|
// UAT-3: hard-stop browser gate (CAPTURE.md §Browsers).
|
||||||
|
// Client-side only: the gate must be invisible to crawlers/bots
|
||||||
|
// and to any non-JS context, and the app must not render in an
|
||||||
|
// unsupported browser.
|
||||||
|
const gate = typeof navigator !== 'undefined' ? checkBrowserGate(navigator.userAgent) : null;
|
||||||
|
let unsupported = $state(gate ? !gate.supported : false);
|
||||||
|
let browserName = $state(gate ? gate.browser.name : 'this browser');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>SlabUploader — Milestone 1</title>
|
<title>SlabUploader — Milestone 1</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<main>
|
{#if unsupported}
|
||||||
|
<!-- Hard stop: the app does NOT render in an unsupported browser. -->
|
||||||
|
<div class="gate">
|
||||||
|
<h1>Unsupported browser</h1>
|
||||||
|
<p>SlabUploader does not work in <strong>{browserName}</strong>.</p>
|
||||||
|
<p class="sub">Use one of the supported clients:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Phone — iOS Safari, Android Chrome</li>
|
||||||
|
<li>PC — Chrome, Edge, Firefox</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<main>
|
||||||
<h1>SlabUploader</h1>
|
<h1>SlabUploader</h1>
|
||||||
<p>Milestone 1 — Capture (browser-only)</p>
|
<p>Milestone 1 — Capture (browser-only)</p>
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.gate {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
max-width: 32rem;
|
||||||
|
margin: 1rem auto;
|
||||||
|
padding: 2rem;
|
||||||
|
border: 2px solid #b00020;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gate h1 {
|
||||||
|
color: #b00020;
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gate .sub {
|
||||||
|
margin: 1rem 0 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gate ul {
|
||||||
|
text-align: left;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gate li {
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { test, expect, devices } from '@playwright/test';
|
||||||
|
|
||||||
|
// UAT-3: Browser gate (hard stop) — e2e.
|
||||||
|
// Spec (CAPTURE.md §Browsers):
|
||||||
|
// Phone: iOS Safari, Android Chrome.
|
||||||
|
// PC: Chrome, Edge, Firefox.
|
||||||
|
// Other clients get a hard stop.
|
||||||
|
//
|
||||||
|
// Acceptance: opening in an unsupported browser (e.g. Opera, Samsung
|
||||||
|
// Internet) shows a hard-stop message, NOT the app.
|
||||||
|
//
|
||||||
|
// The app itself must not render (no <slot> content, no "Milestone 1
|
||||||
|
// — Capture" text) in an unsupported browser; the hard-stop page lists
|
||||||
|
// the spec browsers instead.
|
||||||
|
|
||||||
|
const DESKTOP_CHROME_UA =
|
||||||
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36';
|
||||||
|
const DESKTOP_OPERA_UA =
|
||||||
|
'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';
|
||||||
|
const DESKTOP_EDGE_UA =
|
||||||
|
'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';
|
||||||
|
const DESKTOP_FIREFOX_UA =
|
||||||
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0';
|
||||||
|
const IOS_SAFARI_UA =
|
||||||
|
'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';
|
||||||
|
const ANDROID_CHROME_UA =
|
||||||
|
'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6331.115 Mobile Safari/537.36';
|
||||||
|
const SAMSUNG_INTERNET_UA =
|
||||||
|
'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';
|
||||||
|
|
||||||
|
async function openApp(page, ua) {
|
||||||
|
await page.setExtraHTTPHeaders({ 'User-Agent': ua });
|
||||||
|
await page.goto('/');
|
||||||
|
// The gate runs on client hydration; wait a beat for the DOM to
|
||||||
|
// settle on whichever branch rendered.
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The app renders when the gate passes: the layout's <main> is present
|
||||||
|
// and the capture UI (Add Photo heading) is inside it.
|
||||||
|
async function expectApp(page) {
|
||||||
|
await expect(page.locator('main')).toBeVisible();
|
||||||
|
await expect(page.locator('main')).toContainText('Add Photo');
|
||||||
|
// No hard-stop page.
|
||||||
|
await expect(page.locator('.gate')).toHaveCount(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The hard stop renders when the gate fails: the .gate box is present,
|
||||||
|
// the app's <main> is NOT, and the spec browsers are listed.
|
||||||
|
async function expectHardStop(page) {
|
||||||
|
await expect(page.locator('.gate h1')).toHaveText('Unsupported browser');
|
||||||
|
await expect(page.locator('main')).toHaveCount(0);
|
||||||
|
const gate = page.locator('.gate');
|
||||||
|
await expect(gate).toContainText('iOS Safari');
|
||||||
|
await expect(gate).toContainText('Android Chrome');
|
||||||
|
await expect(gate).toContainText('Chrome');
|
||||||
|
await expect(gate).toContainText('Edge');
|
||||||
|
await expect(gate).toContainText('Firefox');
|
||||||
|
}
|
||||||
|
|
||||||
|
test('UAT-3: app renders in desktop Chrome (supported)', async ({ page }) => {
|
||||||
|
await openApp(page, DESKTOP_CHROME_UA);
|
||||||
|
await expectApp(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('UAT-3: app renders in desktop Edge (supported)', async ({ page }) => {
|
||||||
|
await openApp(page, DESKTOP_EDGE_UA);
|
||||||
|
await expectApp(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('UAT-3: app renders in desktop Firefox (supported)', async ({ page }) => {
|
||||||
|
await openApp(page, DESKTOP_FIREFOX_UA);
|
||||||
|
await expectApp(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('UAT-3: app renders in iOS Safari (supported)', async ({ page, context }) => {
|
||||||
|
await context.setExtraHTTPHeaders({ 'User-Agent': IOS_SAFARI_UA });
|
||||||
|
await page.goto('/');
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
await expectApp(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('UAT-3: app renders in Android Chrome (supported)', async ({ page, context }) => {
|
||||||
|
await context.setExtraHTTPHeaders({ 'User-Agent': ANDROID_CHROME_UA });
|
||||||
|
await page.goto('/');
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
await expectApp(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('UAT-3: hard stop in desktop Opera (unsupported)', async ({ page }) => {
|
||||||
|
await openApp(page, DESKTOP_OPERA_UA);
|
||||||
|
await expect(page.locator('.gate h1')).toHaveText('Unsupported browser');
|
||||||
|
// The app must NOT render.
|
||||||
|
await expect(page.locator('main')).toHaveCount(0);
|
||||||
|
// The hard-stop page lists the spec browsers.
|
||||||
|
await expect(page.locator('.gate')).toContainText('iOS Safari');
|
||||||
|
await expect(page.locator('.gate')).toContainText('Android Chrome');
|
||||||
|
await expect(page.locator('.gate')).toContainText('Chrome');
|
||||||
|
await expect(page.locator('.gate')).toContainText('Edge');
|
||||||
|
await expect(page.locator('.gate')).toContainText('Firefox');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('UAT-3: hard stop in Samsung Internet (unsupported)', async ({ page, context }) => {
|
||||||
|
await context.setExtraHTTPHeaders({ 'User-Agent': SAMSUNG_INTERNET_UA });
|
||||||
|
await page.goto('/');
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
await expect(page.locator('.gate h1')).toHaveText('Unsupported browser');
|
||||||
|
await expect(page.locator('main')).toHaveCount(0);
|
||||||
|
});
|
||||||
@@ -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