diff --git a/about.css b/about.css
new file mode 100644
index 0000000..4d12bf3
--- /dev/null
+++ b/about.css
@@ -0,0 +1,90 @@
+:root {
+ color-scheme:light;
+ --page:#fff4d6;
+ --ink:#192541;
+ --muted:#58647a;
+ --panel:#fff9e8;
+ --line:#192541;
+ --accent:#765cff;
+ --pink:#ff4fa3;
+ --pink-heading:#e13a87;
+ --pink-text:#a31558;
+ --mint:#6ef2d0;
+ --yellow:#ffd84d;
+ --sky:#56a8ff;
+ --focus:#4b2fc9;
+ --link-accent:#4b2fc9;
+ --shadow:#192541;
+}
+:root[data-color-mode="dark"] { color-scheme:dark;--page:#0a1020;--ink:#fff4d6;--muted:#b7c2dc;--panel:#111a33;--line:#fff4d6;--pink-heading:#ff70b7;--pink-text:#ff70b7;--focus:#6ef2d0;--link-accent:#ffd84d;--shadow:#030712; }
+* { box-sizing:border-box; }
+body { min-height:100vh;margin:0;color:var(--ink);background:linear-gradient(90deg,#ffffff06 1px,transparent 1px),linear-gradient(#ffffff06 1px,transparent 1px),radial-gradient(circle at 8% 0,#765cff38,transparent 34rem),var(--page);background-size:32px 32px,32px 32px,auto,auto;font-family:var(--pixel-body,ui-monospace,"SFMono-Regular",Consolas,monospace);overflow-x:hidden; }
+a { color:inherit; }
+.about-shell { width:min(1120px,calc(100% - 40px));margin:0 auto;padding:clamp(38px,6vw,78px) 0 72px; }
+.about-hero { display:grid;grid-template-columns:minmax(0,1.45fr) minmax(280px,.55fr);gap:clamp(32px,6vw,76px);align-items:center;margin-bottom:52px; }
+.eyebrow,.panel-kicker { margin:0 0 18px;color:var(--pink-text);font:700 12px/1.2 var(--pixel-font,ui-monospace,monospace);letter-spacing:.1em;text-transform:uppercase; }
+.eyebrow { display:inline-block;padding:8px 12px;color:#192541;background:var(--yellow);border:2px solid var(--line);box-shadow:4px 4px 0 var(--line); }
+h1,h2,.player-card strong,.button,.resource-links span,.social-grid strong { font-family:var(--pixel-font,ui-monospace,monospace); }
+h1 { max-width:740px;margin:0;font-size:clamp(40px,7vw,76px);font-weight:700;letter-spacing:-.065em;line-height:1.08;text-wrap:balance;text-shadow:5px 5px 0 var(--accent); }
+h1 span { color:var(--pink-heading); }
+.lede { max-width:720px;margin:28px 0 0;color:var(--muted);font-size:clamp(17px,2vw,20px);line-height:1.65; }
+.hero-actions { display:flex;flex-wrap:wrap;gap:14px;margin-top:30px; }
+.button { display:inline-flex;align-items:center;justify-content:center;gap:9px;min-height:48px;padding:10px 16px;border:2px solid var(--line);font-size:11px;font-weight:700;text-decoration:none;box-shadow:4px 4px 0 var(--line);transition:transform 140ms steps(2,end),box-shadow 140ms steps(2,end); }
+.button-primary { color:#192541;background:var(--mint); }
+.button-secondary { background:var(--panel); }
+.button:hover { transform:translate(4px,4px);box-shadow:0 0 0 var(--line); }
+.button:focus-visible,.resource-links a:focus-visible,.patent-list a:focus-visible,.social-grid a:focus-visible { outline:3px solid var(--focus);outline-offset:4px; }
+.player-card { position:relative;padding:28px;background:var(--panel);border:3px solid var(--line);box-shadow:8px 8px 0 var(--pink),12px 12px 0 var(--line);transform:rotate(1.5deg); }
+.player-card::after { position:absolute;top:14px;right:14px;width:42px;height:42px;background:linear-gradient(90deg,var(--yellow) 0 30%,transparent 30% 70%,var(--yellow) 70%),linear-gradient(var(--yellow) 0 30%,transparent 30% 70%,var(--yellow) 70%);content:"";opacity:.8; }
+.player-label { margin:0 0 10px;color:var(--pink-text);font-size:11px;font-weight:800;letter-spacing:.12em;text-transform:uppercase; }
+.player-card>strong { display:block;padding-right:46px;font-size:clamp(19px,3vw,27px); }
+.player-card dl { display:grid;gap:0;margin:24px 0 0; }
+.player-card dl div { display:grid;grid-template-columns:90px 1fr;gap:12px;padding:12px 0;border-top:1px solid color-mix(in srgb,var(--line) 22%,transparent); }
+.player-card dt { color:var(--muted);font-size:12px;text-transform:uppercase; }
+.player-card dd { margin:0;font-size:14px;font-weight:800; }
+.about-grid { display:grid;grid-template-columns:1.25fr .75fr;gap:24px; }
+.panel { position:relative;padding:clamp(25px,4vw,40px);background:var(--panel);border:3px solid var(--line);box-shadow:7px 7px 0 var(--accent),11px 11px 0 var(--line); }
+.panel::before { position:absolute;top:0;right:0;left:0;height:8px;background:repeating-linear-gradient(90deg,var(--accent) 0 20px,color-mix(in srgb,var(--accent) 42%,transparent) 20px 28px);content:""; }
+.panel h2,.social-section h2 { margin:0;font-size:clamp(26px,4vw,38px);letter-spacing:-.045em;line-height:1.2; }
+.panel>p:not(.panel-kicker) { margin:20px 0 0;color:var(--muted);font-size:16px;line-height:1.7; }
+.career-path { display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:8px;margin:30px 0 0;padding:0;list-style:none; }
+.career-path li { position:relative;padding-top:19px;text-align:center; }
+.career-path li::before { position:absolute;top:3px;right:50%;left:-58%;height:3px;background:var(--accent);content:""; }
+.career-path li:first-child::before { left:50%; }
+.career-path li::after { position:absolute;top:0;left:calc(50% - 5px);width:10px;height:10px;background:var(--mint);border:2px solid var(--line);content:""; }
+.career-path span { font-size:12px;font-weight:800; }
+.community-panel { --accent:var(--mint); }
+.resource-links { display:grid;gap:12px;margin-top:28px; }
+.resource-links a { display:grid;grid-template-columns:1fr auto;gap:5px 12px;padding:15px;color:inherit;background:color-mix(in srgb,var(--accent) 9%,transparent);border:1px solid color-mix(in srgb,var(--line) 28%,transparent);text-decoration:none; }
+.resource-links span { font-size:13px; }
+.resource-links small { grid-column:1;color:var(--muted);font-size:12px;line-height:1.45; }
+.resource-links b { grid-area:1/2/3;font-size:20px;align-self:center; }
+.patent-panel { --accent:var(--yellow);display:grid;grid-template-columns:.4fr 1.6fr;gap:28px;margin-top:30px; }
+.patent-list { display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px; }
+.patent-list a { display:flex;flex-direction:column;gap:10px;min-height:170px;padding:18px;background:color-mix(in srgb,var(--yellow) 12%,transparent);border:2px solid var(--line);text-decoration:none; }
+.patent-list small { color:var(--muted);font-size:11px;font-weight:800;text-transform:uppercase; }
+.patent-list span { flex:1;font-size:15px;font-weight:800;line-height:1.45; }
+.patent-list b { color:var(--link-accent);font-size:12px; }
+.patent-list i { font-style:normal; }
+.social-section { margin-top:58px; }
+.social-grid { display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px;margin-top:24px; }
+.social-grid a { display:grid;grid-template-columns:38px 1fr;gap:3px 11px;align-items:center;min-height:92px;padding:16px;background:var(--panel);border:2px solid var(--line);box-shadow:4px 4px 0 var(--line);text-decoration:none;transition:transform 140ms steps(2,end),box-shadow 140ms steps(2,end); }
+.social-grid a:hover { transform:translate(3px,3px);box-shadow:1px 1px 0 var(--line); }
+.social-grid a>span { grid-row:1/3;display:grid;width:38px;height:38px;place-items:center;color:#192541;background:var(--sky);border:2px solid var(--line);font-size:13px;font-weight:950; }
+.social-grid a:nth-child(2n)>span { background:var(--mint); }
+.social-grid a:nth-child(3n)>span { background:var(--pink); }
+.social-grid strong { font-size:12px; }
+.social-grid small { color:var(--muted);font-size:11px; }
+:root[data-arcade-theme="cabinet"] .about-shell { width:min(1240px,calc(100% - 32px)); }
+:root[data-arcade-theme="cabinet"] :is(.player-card,.panel) { box-shadow:5px 5px 0 var(--yellow); }
+:root[data-arcade-theme="calm"] { --page:#eef4f1;--ink:#223a33;--muted:#61746d;--panel:#f8fbf9;--line:#527068;--accent:#356f61;--pink:#9b5548;--pink-heading:#9b5548;--pink-text:#854238;--mint:#789d90;--yellow:#d9b968;--sky:#668e9b;--focus:#315f54;--link-accent:#315f54;--shadow:#263c34; }
+:root[data-arcade-theme="calm"][data-color-mode="dark"] { --page:#111917;--ink:#e5efeb;--muted:#9fb0a9;--panel:#18231f;--line:#91a59d;--pink-heading:#e8a092;--pink-text:#e8a092;--focus:#d9b968;--link-accent:#d9b968;--shadow:#000; }
+:root[data-arcade-theme="calm"] body { background:var(--page);font-family:Georgia,"Times New Roman",serif; }
+:root[data-arcade-theme="calm"] .about-shell { width:min(1020px,calc(100% - 48px)); }
+:root[data-arcade-theme="calm"] :is(h1,h2,.player-card strong,.button,.resource-links span,.social-grid strong) { font-family:Georgia,"Times New Roman",serif; }
+:root[data-arcade-theme="calm"] h1 { text-shadow:none; }
+:root[data-arcade-theme="calm"] :is(.player-card,.panel,.social-grid a) { border-width:1px;box-shadow:none;transform:none; }
+:root[data-arcade-theme="calm"] .panel::before { height:3px; }
+@media (max-width:800px) { .about-hero,.about-grid,.patent-panel { grid-template-columns:1fr; } .player-card { width:min(100%,520px);transform:none; } .social-grid { grid-template-columns:repeat(2,minmax(0,1fr)); } }
+@media (max-width:520px) { .about-shell,:root[data-arcade-theme="calm"] .about-shell { width:min(100% - 28px,1120px);padding-top:32px; } .about-hero { margin-bottom:38px; } .hero-actions { flex-direction:column; } .panel { padding:26px 20px;box-shadow:5px 5px 0 var(--accent),8px 8px 0 var(--line); } .career-path { grid-template-columns:1fr;gap:0; } .career-path li { padding:0 0 14px 25px;text-align:left; } .career-path li::before { top:0;bottom:0;right:auto;left:5px;width:3px;height:auto; } .career-path li:first-child::before { left:5px;top:5px; } .career-path li:last-child::before { bottom:calc(100% - 5px); } .career-path li::after { top:3px;left:0; } .patent-list,.social-grid { grid-template-columns:1fr; } .patent-list a { min-height:0; } }
+@media (prefers-reduced-motion:reduce) { .button,.social-grid a { transition:none; } }
diff --git a/about.html b/about.html
new file mode 100644
index 0000000..adb201c
--- /dev/null
+++ b/about.html
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+ About Mohamed Elsherif · JavaScript Playground
+
+
+
+
+
+
+
+
+
+
Meet the maker
+
Built by Mohamed Elsherif.
+
I’m a software architect with more than 20 years in software engineering. I build systems, share what I learn, and still make time for a good JavaScript game.
+
+
+
+
+
+
+
+ Career map
+ Building services that scale
+ I work on Apple’s Services Architecture and Efficiency team, helping online services scale reliably and run efficiently. Earlier chapters of my career include Azure identity at Microsoft, integrations at Okta, enterprise mobility at VMware, and Metrics and Reporting infrastructure at Hulu.
+
+ Hulu
+ VMware
+ Okta
+ Microsoft
+ Apple
+
+
+
+
+
+
+
+
+
Granted achievements
+
Patents
+
+
+
+
+
+ Continue the conversation
+ Find me online
+
+
+
+
+
+
+
diff --git a/arcade.css b/arcade.css
index 25f1219..02dbdbc 100644
--- a/arcade.css
+++ b/arcade.css
@@ -10,6 +10,7 @@ body:not(.arcade-has-topbar)::before { display:block; height:env(safe-area-inset
body.arena-fullscreen .arcade-topbar { display:none; }
body.arena-fullscreen .arcade-account { display:none; }
.arcade-account a, .arcade-account button { padding: 8px 11px; color: inherit; background: transparent; border: 0; border-radius: 9px; font: inherit; text-decoration: none; cursor: pointer; }
+.arcade-account a[aria-current="page"] { text-decoration:underline; text-decoration-thickness:2px; text-underline-offset:5px; }
.arcade-account button { color: #10271f; background: #83e6c3; }
.arcade-appearance-button::before { margin-right:6px; content:'◐'; }
.arcade-audio-button::before { margin-right:6px; content:'♪'; }.arcade-audio-button[data-audio-muted="true"]::before{content:'×';}.arcade-audio-button:disabled{cursor:not-allowed;opacity:.55}
@@ -67,7 +68,7 @@ body.arena-fullscreen .arcade-account { display:none; }
.result-share-cancel { color: #f7f8fc; background: rgba(255,255,255,.09); }
.result-share-confirm { color: #10271f; background: #83e6c3; }
@media (max-width: 760px) { .arcade-topbar-inner { align-items:flex-start; flex-direction:column; gap:11px; } .arcade-account { width:100%; justify-content:flex-start; overflow-x:auto; padding:2px 3px 5px; scrollbar-width:none; } .arcade-account::-webkit-scrollbar { display:none; } }
-@media (max-width: 520px) { .arcade-topbar { padding-inline:12px; } .arcade-home > span:last-child { font-size:14px; } .arcade-account > a:first-of-type { display:none; } .arcade-account a,.arcade-account button { white-space:nowrap; } }
+@media (max-width: 520px) { .arcade-topbar { padding-inline:12px; } .arcade-home > span:last-child { font-size:14px; } .arcade-account > .arcade-scores-link { display:none; } .arcade-account a,.arcade-account button { white-space:nowrap; } }
@media (max-width: 520px) { .arcade-install-hint { right:max(12px,env(safe-area-inset-right)); bottom:max(12px,env(safe-area-inset-bottom)); left:auto; } .arcade-install-dialog form { padding:19px; } }
@media (max-width: 480px) { .result-share-content { padding: 17px; } .result-share-actions button { flex: 1; } }
@media (prefers-reduced-motion: reduce) { .arcade-install-hint { animation:none; } }
diff --git a/arcade.js b/arcade.js
index 6dc0eed..7c269a7 100644
--- a/arcade.js
+++ b/arcade.js
@@ -149,7 +149,7 @@
home.setAttribute('aria-label', 'JavaScript Playground home');
home.innerHTML = 'JSPG JavaScript Playground ';
const account = document.createElement('nav');
- account.className = 'arcade-account'; account.setAttribute('aria-label', 'Arcade account and appearance');
+ account.className = 'arcade-account'; account.setAttribute('aria-label', 'Arcade navigation and account');
topbarInner.append(home, account); topbar.append(topbarInner);
const appearanceButton = document.createElement('button'); appearanceButton.type = 'button'; appearanceButton.className = 'arcade-appearance-button'; appearanceButton.textContent = 'Appearance';
const audioButton = document.createElement('button'); audioButton.type = 'button'; audioButton.className = 'arcade-audio-button'; audioButton.textContent = 'Sound'; audioButton.setAttribute('aria-haspopup', 'dialog');
@@ -316,7 +316,9 @@
if (game && audio) { updateAudioControls(); account.append(audioButton); }
if (saveManager) account.append(saveManager.button);
if (game) { const achievements = document.createElement('button'); achievements.type = 'button'; achievements.className = 'achievement-nav'; achievements.textContent = 'Achievements'; achievements.addEventListener('click', () => { achievementDialog.showModal(); loadAchievements().catch(() => {}); }); account.append(achievements); }
- const scores = document.createElement('a'); scores.href = `${rootPath}profile.html#leaderboards`; scores.textContent = 'Top scores'; account.append(scores);
+ const about = document.createElement('a'); about.href = `${rootPath}about.html`; about.className = 'arcade-about-link'; about.textContent = 'About';
+ if (location.pathname.endsWith('/about.html')) about.setAttribute('aria-current', 'page');
+ const scores = document.createElement('a'); scores.href = `${rootPath}profile.html#leaderboards`; scores.className = 'arcade-scores-link'; scores.textContent = 'Top scores'; account.append(about, scores);
if (currentUser) {
const profile = document.createElement('a'); profile.href = `${rootPath}profile.html`; profile.textContent = currentUser.gamertag;
const logout = document.createElement('button'); logout.type = 'button'; logout.textContent = 'Sign out'; logout.addEventListener('click', async () => { await api('/api/auth/logout', { method: 'POST', body: '{}' }); currentUser = null; render(); });
diff --git a/docs/architecture.md b/docs/architecture.md
index d8bb5d8..f9e9ff0 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -21,7 +21,7 @@ flowchart LR
```mermaid
flowchart TB
subgraph Browser
- Pages[Home, profile, game pages]
+ Pages[Home, profile, about, game pages]
Arcade[arcade.js: account, appearance, results, achievements]
Events[game-events.js: browser domain events]
Shared[room UI, colors, sharing]
diff --git a/package-lock.json b/package-lock.json
index 30f053d..bb7986c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "javascript-playground",
- "version": "1.2.0",
+ "version": "1.2.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "javascript-playground",
- "version": "1.2.0",
+ "version": "1.2.5",
"dependencies": {
"ws": "8.21.3"
},
diff --git a/package.json b/package.json
index aaeea2e..45d23d0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "javascript-playground",
- "version": "1.2.0",
+ "version": "1.2.5",
"private": true,
"description": "Browser games with authoritative online multiplayer",
"main": "server/index.js",
diff --git a/releases.json b/releases.json
index a5c807a..e2dea69 100644
--- a/releases.json
+++ b/releases.json
@@ -1,6 +1,21 @@
{
"schemaVersion": 1,
"releases": [
+ {
+ "version": "1.2.5",
+ "title": "JavaScript Playground 1.2.5",
+ "summary": "A new About page introduces Mohamed Elsherif and connects the arcade with his work across software architecture and the developer community.",
+ "highlights": [
+ "Meet Mohamed Elsherif through a game-native profile of his career, community work, granted patents, websites, and social profiles.",
+ "Open the About page from the shared arcade navigation on phones, tablets, and desktops."
+ ],
+ "fixes": [
+ "Keeps the About link available in compact phone navigation while moving the less essential Top scores shortcut out of the narrow layout."
+ ],
+ "technical": [
+ "Adds the responsive About page to every experience theme and the offline application shell, with regression coverage for content, links, navigation, and safe-area behavior."
+ ]
+ },
{
"version": "1.2.0",
"title": "JavaScript Playground 1.2.0",
diff --git a/service-worker.js b/service-worker.js
index b308f35..ae1092f 100644
--- a/service-worker.js
+++ b/service-worker.js
@@ -1,9 +1,11 @@
'use strict';
-const CACHE_NAME = 'js-playground-v14';
+const CACHE_NAME = 'js-playground-v15';
const APP_SHELL = [
'./',
'./index.html',
+ './about.html',
+ './about.css',
'./arcade.css',
'./arcade.js',
'./fonts/Silkscreen-Regular.ttf',
diff --git a/tests/about-page.test.js b/tests/about-page.test.js
new file mode 100644
index 0000000..3fbf006
--- /dev/null
+++ b/tests/about-page.test.js
@@ -0,0 +1,71 @@
+'use strict';
+
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const test = require('node:test');
+
+const page = fs.readFileSync('about.html', 'utf8');
+const styles = fs.readFileSync('about.css', 'utf8');
+const shell = fs.readFileSync('arcade.js', 'utf8');
+const worker = fs.readFileSync('service-worker.js', 'utf8');
+
+function contrast(left, right) {
+ const luminance = color => {
+ const channels = color.slice(1).match(/../g).map(value => parseInt(value, 16) / 255).map(value => value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4);
+ return 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2];
+ };
+ const values = [luminance(left), luminance(right)];
+ return (Math.max(...values) + 0.05) / (Math.min(...values) + 0.05);
+}
+
+test('shared arcade navigation exposes the about page', () => {
+ assert.match(shell, /about\.href = `\$\{rootPath\}about\.html`/);
+ assert.match(shell, /about\.setAttribute\('aria-current', 'page'\)/);
+});
+
+test('about page presents the owner, work, community projects, and patents', () => {
+ assert.match(page, /Mohamed Elsherif/);
+ assert.match(page, /Apple’s Services Architecture and Efficiency team/);
+ assert.match(page, /AskDeveloper Podcast/);
+ assert.match(page, /US11531532B2/);
+ assert.match(page, /US11956232B2/);
+});
+
+test('about page links to the public profile and social destinations safely', () => {
+ for (const destination of ['bashmohandes.com', 'askdeveloper.com', 'linkedin.com', 'github.com', 'youtube.com', 'soundcloud.com', 'tiktok.com', 'x.com', 'facebook.com']) {
+ assert.match(page, new RegExp(`href="https://(?:www\\.)?${destination.replace('.', '\\.')}`));
+ }
+ const externalLinks = page.match(/ {
+ assert.match(page, /src="theme-init\.js"[\s\S]*href="arcade\.css"[\s\S]*href="about\.css"/);
+ assert.match(styles, /data-arcade-theme="cabinet"/);
+ assert.match(styles, /data-arcade-theme="calm"/);
+ assert.match(styles, /@media \(max-width:520px\)/);
+});
+
+test('about page uses the current color-mode contract and contrast-safe text and focus colors', () => {
+ assert.match(styles, /:root\[data-color-mode="dark"\]/);
+ assert.doesNotMatch(styles, /data-theme=/);
+ assert.match(styles, /h1 span \{ color:var\(--pink-heading\)/);
+ assert.match(styles, /color:var\(--pink-text\)/);
+ assert.match(styles, /outline:3px solid var\(--focus\)/);
+ assert.match(styles, /\.patent-list b \{ color:var\(--link-accent\)/);
+ for (const [foreground, background, minimum] of [
+ ['#e13a87', '#fff4d6', 3],
+ ['#a31558', '#fff9e8', 4.5],
+ ['#4b2fc9', '#fff9e8', 3],
+ ['#315f54', '#f8fbf9', 4.5],
+ ['#ff70b7', '#111a33', 4.5],
+ ['#6ef2d0', '#111a33', 3],
+ ['#d9b968', '#18231f', 4.5]
+ ]) assert.ok(contrast(foreground, background) >= minimum, `${foreground} should meet ${minimum}:1 against ${background}`);
+});
+
+test('about page is available from the offline app shell', () => {
+ assert.match(worker, /'\.\/about\.html'/);
+ assert.match(worker, /'\.\/about\.css'/);
+});
diff --git a/tests/game-events.test.js b/tests/game-events.test.js
index 5abc292..6676a94 100644
--- a/tests/game-events.test.js
+++ b/tests/game-events.test.js
@@ -62,7 +62,7 @@ test('modern controllers publish domain facts without importing audio', () => {
test('shared shell notifications and extension boundaries use the event contract', () => {
const shell = read('arcade.js'), guide = read('docs/game-events.md'), adr = read('docs/adr/0014-browser-domain-event-bus.md');
- for (const page of ['index.html','profile.html']) assert.match(read(page), /scripts\/game-events\.js[\s\S]*arcade\.js/);
+ for (const page of ['index.html','profile.html','about.html']) assert.match(read(page), /scripts\/game-events\.js[\s\S]*arcade\.js/);
for (const type of ['system:theme-changed','account:user-changed','achievement:unlocked','score:top']) assert.match(shell, new RegExp(type));
assert.match(read('profile.js'), /account:user-changed/);
assert.match(guide, /not a command bus, durable event log, WebSocket protocol, or trust boundary/);
diff --git a/tests/online-rooms.test.js b/tests/online-rooms.test.js
index 4daa980..386497e 100644
--- a/tests/online-rooms.test.js
+++ b/tests/online-rooms.test.js
@@ -63,7 +63,7 @@ test('online pages cache-bust shared contract changes',()=>{
assert.match(page,new RegExp(`scripts/online-rooms\\.js\\?v=${rooms.CONTRACT_VERSION}`),`${file} must request the current shared contract`);
}
const worker=fs.readFileSync('service-worker.js','utf8');
- assert.match(worker,/CACHE_NAME = 'js-playground-v14'/);
+ assert.match(worker,/CACHE_NAME = 'js-playground-v15'/);
});
test('Battle Tanks ignores obsolete sockets and owns one reconnect timer',()=>{
const app=fs.readFileSync('battle-tanks/scripts/app.js','utf8');
diff --git a/tests/release-notes.test.js b/tests/release-notes.test.js
index 19ad5fb..3f919cb 100644
--- a/tests/release-notes.test.js
+++ b/tests/release-notes.test.js
@@ -29,6 +29,14 @@ test('release notes render tailored public and GitHub views', () => {
assert.match(markdown, /^## Technical notes$/m);
});
+test('current release notes introduce the new About page', () => {
+ const release = releaseForVersion(manifest, packageVersion);
+ assert.equal(release.version, '1.2.5');
+ assert.match(release.summary, /new About page/);
+ assert.ok(release.highlights.some(item => /Mohamed Elsherif[\s\S]*career[\s\S]*social profiles/.test(item)));
+ assert.doesNotMatch(releaseForVersion(manifest, '1.2.0').summary, /About page/);
+});
+
test('build information exposes release notes only for an exact stable version', () => {
assert.deepEqual(createBuildInformation({}, manifest), { version: 'dev', channel: 'dev', release: null });
assert.deepEqual(createBuildInformation({ BUILD_VERSION: 'sha-1234567', BUILD_CHANNEL: 'alpha' }, manifest), { version: 'sha-1234567', channel: 'alpha', release: null });
diff --git a/tests/shared-game-assets.test.js b/tests/shared-game-assets.test.js
index a7b8b3d..edd6bdc 100644
--- a/tests/shared-game-assets.test.js
+++ b/tests/shared-game-assets.test.js
@@ -162,6 +162,7 @@ test('result sharing preserves user activation when the preview is confirmed', (
test('every arcade page opts into shared iPhone safe-area handling', () => {
const pages = [
'index.html',
+ 'about.html',
'profile.html',
'pong/index.html',
'pong/classic/index.html',