Browser Support Test — TypeScript source
Audit your real browser against 30 modern web capabilities - WebGPU, WASM, WebAuthn, Service Workers and more. Get a weighted readiness score, copy-paste detection snippets, and a shareable link.
This is the TypeScript implementation — the same logic the interactive tool runs, in a shareable, citable form.
/**
* Browser Support Test - pure logic (feature probes, weighted score, share codec).
*
* Language: TypeScript
* CosmoDev polyglot showcase port of the `browser-support-test` tool.
* Ported from src/tools/BrowserSupportTest.tsx (+ browser-support/* modules) -
* display source, part of CosmoDev's polyglot tool pages.
*
* Probes a normalized {nav, win, doc} env for web capabilities, reduces the
* verdicts to a weighted readiness score (0-100) + tier, and packs them into a
* compact base64url share token. Detection is pure API-presence probing
* (`'gpu' in navigator`, `canvas.getContext('webgl')`, `typeof WebAssembly`…);
* a throwing probe degrades to 'unknown'. This is browser-only logic - the
* Go/Rust/PHP/Python ports in this set show the analogous pattern for their
* own runtimes. Catalog below is a representative subset (live tool ships ~30).
*/
export type CheckStatus = 'supported' | 'limited' | 'needs-https' | 'unsupported' | 'unknown';
export interface FeatureResult { id: string; status: CheckStatus; detail?: string; }
export interface BrowserEnv { nav: any; win: any; doc: any; }
export interface FeatureCheck {
id: string; title: string; category: string; weight: number;
explainer: string; codeSnippet: string;
detect: (env: BrowserEnv) => CheckStatus | { status: CheckStatus; detail?: string };
}
export interface ResultReport { v: 1; s: string; score: number; browser?: string; }
// Status → contribution (0..1) × weight. needs-https scores below "limited":
// the capability exists but is unusable in the current context.
const CONTRIBUTION: Record<CheckStatus, number> = {
supported: 1, limited: 0.5, 'needs-https': 0.3, unsupported: 0, unknown: 0,
};
/** Weighted readiness 0-100; ids absent from `checks` are ignored. */
export function computeScore(results: FeatureResult[], checks: FeatureCheck[]): number {
const wm = new Map(checks.map((c) => [c.id, c.weight]));
let earned = 0, possible = 0;
for (const r of results) {
const w = wm.get(r.id);
if (w === undefined) continue;
earned += CONTRIBUTION[r.status] * w;
possible += w;
}
return possible === 0 ? 0 : Math.round((earned / possible) * 100);
}
// One char per check, in catalog order → fits a full audit in a URL.
const TO_CHAR: Record<CheckStatus, string> = {
supported: 's', limited: 'l', 'needs-https': 'h', unsupported: 'x', unknown: '?',
};
const FROM_CHAR: Record<string, CheckStatus> = {
s: 'supported', l: 'limited', h: 'needs-https', x: 'unsupported', '?': 'unknown',
};
export const packStatuses = (r: FeatureResult[]): string => r.map((x) => TO_CHAR[x.status] ?? '?').join('');
export const unpackStatuses = (s: string): CheckStatus[] => [...s].map((c) => FROM_CHAR[c] ?? 'unknown');
// base64url of UTF-8 bytes (btoa/atob + TextEncoder are global in browsers + Node ≥ 20).
function toBase64Url(str: string): string {
const bytes = new TextEncoder().encode(str);
const bin = Array.from(bytes, (b) => String.fromCharCode(b)).join('');
return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
function fromBase64Url(b64url: string): string {
const bin = atob(b64url.replace(/-/g, '+').replace(/_/g, '/'));
return new TextDecoder().decode(Uint8Array.from(bin, (c) => c.charCodeAt(0)));
}
export function encodeResults(r: ResultReport): string { return toBase64Url(JSON.stringify(r)); }
export function decodeResults(enc: string): ResultReport | null {
if (!enc) return null;
try {
const p = JSON.parse(fromBase64Url(enc));
if (p && p.v === 1 && typeof p.s === 'string' && typeof p.score === 'number') return p;
return null;
} catch { return null; }
}
/** Readiness tier from a 0-100 score. */
export function tierFor(score: number): { label: string; tone: string } {
if (score >= 90) return { label: 'Fully modern', tone: 'text-success' };
if (score >= 75) return { label: 'Very capable', tone: 'text-success' };
if (score >= 55) return { label: 'Capable, minor gaps', tone: 'text-accent' };
if (score >= 35) return { label: 'Showing its age', tone: 'text-fg-muted' };
return { label: 'Legacy browser', tone: 'text-danger' };
}
/** Parse a UA string into "Chrome 130 · macOS" (display label only). */
export function browserLabel(ua: string): string {
if (!ua) return 'Your browser';
let name = '', ver = '';
const pick = (re: RegExp, n: string): boolean => {
const m = re.exec(ua);
if (m) { name = n; ver = m[1]; return true; }
return false;
};
pick(/(?:firefox|fxios)\/(\d+)/i, 'Firefox') ||
pick(/(?:edg|edge)\/(\d+)/i, 'Edge') ||
pick(/(?:opr|opera)\/(\d+)/i, 'Opera') ||
pick(/(?:chrome|crios)\/(\d+)/i, 'Chrome') ||
pick(/version\/(\d+)[.\d]* safari/i, 'Safari') ||
pick(/safari\/(\d+)/i, 'Safari');
const os =
/mac os x/i.test(ua) ? 'macOS' : /windows nt/i.test(ua) ? 'Windows'
: /android/i.test(ua) ? 'Android' : /(iphone|ipad|cpu os)/i.test(ua) ? 'iOS'
: /linux/i.test(ua) ? 'Linux' : '';
const main = name ? `${name}${ver ? ' ' + ver : ''}` : 'Your browser';
return os ? `${main} · ${os}` : main;
}
export const createEnv = (): BrowserEnv => {
const g = globalThis as any;
return { nav: g.navigator, win: g, doc: g.document };
};
const S = 'supported' as CheckStatus, X = 'unsupported' as CheckStatus;
function canvasCtx(env: BrowserEnv, type: string): boolean {
try {
const c = env.doc.createElement('canvas');
return !!c && typeof c.getContext === 'function' && !!c.getContext(type);
} catch { return false; }
}
export const FEATURE_CHECKS: FeatureCheck[] = [
{ id: 'canvas-2d', title: 'Canvas 2D', category: 'Graphics', weight: 2, explainer: '2D drawing surface.', codeSnippet: `!!document.createElement('canvas').getContext('2d')`, detect: (e) => (canvasCtx(e, '2d') ? S : X) },
{ id: 'webgpu', title: 'WebGPU', category: 'Graphics', weight: 3, explainer: 'Next-gen GPU + compute API.', codeSnippet: `'gpu' in navigator`, detect: (e) => ('gpu' in e.nav ? S : X) },
{ id: 'webassembly', title: 'WebAssembly', category: 'Compute', weight: 3, explainer: 'Native-speed compiled code.', codeSnippet: `typeof WebAssembly === 'object'`, detect: (e) => (typeof e.win.WebAssembly === 'object' ? S : X) },
{ id: 'indexeddb', title: 'IndexedDB', category: 'Storage', weight: 3, explainer: 'Client-side database.', codeSnippet: `'indexedDB' in window`, detect: (e) => ('indexedDB' in e.win ? S : X) },
{ id: 'service-worker', title: 'Service Worker', category: 'Connectivity', weight: 3, explainer: 'Offline background scripts.', codeSnippet: `'serviceWorker' in navigator`, detect: (e) => ('serviceWorker' in e.nav ? S : X) },
{ id: 'crypto-subtle', title: 'WebCrypto (subtle)', category: 'Security', weight: 3, explainer: 'Native hashing/signing.', codeSnippet: `!!window.crypto?.subtle`, detect: (e) => (e.win.crypto?.subtle ? S : X) },
{ id: 'secure-context', title: 'Secure Context (HTTPS)', category: 'Security', weight: 2, explainer: 'Unlocks camera, SW, crypto.', codeSnippet: `window.isSecureContext`, detect: (e) => (e.win.isSecureContext === true ? S : { status: 'needs-https' }) },
{ id: 'css-has', title: ':has() Selector', category: 'Platform', weight: 2, explainer: 'Parent selector.', codeSnippet: `CSS.supports('selector', ':has(*)')`, detect: (e) => (typeof e.win.CSS?.supports === 'function' && e.win.CSS.supports('selector', ':has(*)') ? S : X) },
];
/** Run probes; a throw degrades to 'unknown' so one failure can't abort the audit. */
function resolve(id: string, out: CheckStatus | { status: CheckStatus; detail?: string }): FeatureResult {
return typeof out === 'string' ? { id, status: out } : { id, status: out.status, ...(out.detail ? { detail: out.detail } : {}) };
}
export function runChecks(checks: FeatureCheck[], env: BrowserEnv): FeatureResult[] {
return checks.map((c) => {
try { return resolve(c.id, c.detect(env)); } catch { return { id: c.id, status: 'unknown' }; }
});
}
export const runAllChecks = (env: BrowserEnv): FeatureResult[] => runChecks(FEATURE_CHECKS, env);
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →