feat(ui): auth-gate the landing, add Help page and Home nav

- Landing shows the tool tiles only when signed in and a sign-in card
  otherwise; the login callback now returns to /.
- New public /help page: intro plus a per-tool Carbon accordion.
- Nav restructured to Home, {services}, divider, Help, Sign In/Out in
  both the header and the side nav; the header right keeps only the
  identity chip. Side-nav logo enlarged, border removed, spacing
  equalised. Sign-out modal no longer flashes before it is defined.
- Per-deploy asset versioning (?v=) with immutable asset caching and
  no-store HTML; help step lists show their numbers again.
- Tests updated and extended (48 total).
This commit is contained in:
2026-10-01 14:39:50 +01:00
parent 134edb6ebd
commit a218835a1c
5 changed files with 289 additions and 79 deletions
+84 -21
View File
@@ -112,7 +112,7 @@ async function login(base) {
assert.equal(loginRes.status, 302);
const callbackRes = await fetch(`${base}/auth/callback?sessionId=s1`, manual);
assert.equal(callbackRes.status, 302);
assert.equal(callbackRes.headers.get('location'), '/fingerprint');
assert.equal(callbackRes.headers.get('location'), '/');
const session = cookieValue(callbackRes, 'session');
const profile = cookieValue(callbackRes, 'user_profile');
assert.ok(session, 'session cookie set');
@@ -121,14 +121,19 @@ async function login(base) {
return `session=${session}; user_profile=${profile}`;
}
test('landing page links both tools', async () => {
test('signed-out landing prompts sign-in; signed-in landing links both tools', async () => {
const { base, stop } = await bootApp();
try {
const res = await fetch(`${base}/`);
const html = await res.text();
assert.equal(res.status, 200);
assert.match(html, /href="\/cropper"/);
assert.match(html, /href="\/fingerprint"/);
const anon = await (await fetch(`${base}/`)).text();
// no tools until authenticated — a call to action instead
assert.match(anon, /href="\/auth\/login">Sign in with YOUreka</);
assert.ok(!anon.includes('href="/cropper"'), 'no cropper tile when signed out');
assert.ok(!anon.includes('href="/fingerprint"'), 'no fingerprint tile when signed out');
const cookies = await login(base);
const authed = await (await fetch(`${base}/`, { headers: { cookie: cookies } })).text();
assert.match(authed, /href="\/cropper"/);
assert.match(authed, /href="\/fingerprint"/);
} finally {
await stop();
}
@@ -350,28 +355,86 @@ test('landing header is server-rendered for the session', async () => {
const anon = await (await fetch(`${base}/`)).text();
assert.match(anon, /href="\/auth\/login">Sign in</);
assert.ok(!anon.includes('<!--app-header-->'), 'header marker is replaced');
// tools live in the header nav and in the mobile side nav
assert.match(anon, /cds-header-nav-item href="\/cropper"/);
assert.match(anon, /cds-header-nav-item href="\/fingerprint"/);
assert.match(anon, /cds-side-nav-link href="\/cropper"/);
assert.match(anon, /cds-side-nav-divider/);
// side nav items carry Carbon icons in the title-icon slot
assert.match(anon, /cds-side-nav-link href="\/cropper"[^>]*><svg slot="title-icon"/);
assert.match(anon, /cds-side-nav-link href="\/fingerprint"[^>]*><svg slot="title-icon"/);
assert.match(anon, /cds-side-nav-link href="\/auth\/logout"[^>]*><svg slot="title-icon"/);
assert.match(anon, /cds-side-nav-link href="\/auth\/logout"/);
// signed out: no service links, but Help and Sign in are always there
assert.ok(!anon.includes('cds-header-nav-item href="/cropper"'), 'no cropper in header nav when signed out');
assert.ok(!anon.includes('cds-header-nav-item href="/fingerprint"'), 'no fingerprint in header nav when signed out');
assert.match(anon, /cds-header-nav-item href="\/help"/);
assert.match(anon, /cds-side-nav-link href="\/help"/);
// the divider only groups the services, so it is absent when signed out
assert.ok(!anon.includes('cds-side-nav-divider'), 'no side nav divider without services');
assert.ok(!anon.includes('header-nav-divider'), 'no header divider without services');
// no identity or sign-out when signed out
assert.ok(!anon.includes('user-chip'), 'no user chip when signed out');
assert.ok(!anon.includes('data-signout'), 'no sign-out when signed out');
assert.ok(!anon.includes('signout-modal'), 'no sign-out modal when signed out');
assert.match(anon, /youreka-logo\.png/);
assert.match(anon, /header-menu-button/);
// favicon is the Youreka logo; sign-out is confirmed via a Carbon modal
// the side nav brand is just the logo — no repeated "YOUreka Tools" text
assert.match(anon, /<div class="side-nav-brand">\s*<img[^>]*>\s*<\/div>/);
// favicon is the Youreka logo
assert.match(anon, /rel="icon"[^>]*youreka-favicon\.png/);
assert.match(anon, /data-signout/);
assert.match(anon, /cds-modal id="signout-modal"/);
assert.match(anon, /id="signout-confirm"/);
const cookies = await login(base);
const authed = await (await fetch(`${base}/`, { headers: { cookie: cookies } })).text();
assert.match(authed, /Ada/);
assert.match(authed, /user-chip/);
assert.match(authed, /href="\/auth\/logout"[^>]*>Sign out</);
// signed in: services live in the header nav and in the mobile side nav
assert.match(authed, /cds-header-nav-item href="\/cropper"/);
assert.match(authed, /cds-header-nav-item href="\/fingerprint"/);
assert.match(authed, /cds-header-nav-item href="\/help"/);
assert.match(authed, /cds-header-nav-item href="\/auth\/logout" data-signout/);
assert.match(authed, /cds-side-nav-link href="\/cropper"/);
assert.match(authed, /cds-side-nav-link href="\/help"/);
assert.match(authed, /cds-side-nav-divider/);
assert.match(authed, /header-nav-divider/);
// side nav items carry Carbon icons in the title-icon slot
assert.match(authed, /cds-side-nav-link href="\/cropper"[^>]*><svg slot="title-icon"/);
assert.match(authed, /cds-side-nav-link href="\/fingerprint"[^>]*><svg slot="title-icon"/);
assert.match(authed, /cds-side-nav-link href="\/help"[^>]*><svg slot="title-icon"/);
assert.match(authed, /cds-side-nav-link href="\/auth\/logout"[^>]*><svg slot="title-icon"/);
// sign-out is confirmed via a Carbon modal
assert.match(authed, /data-signout/);
assert.match(authed, /cds-modal id="signout-modal"/);
assert.match(authed, /id="signout-confirm"/);
} finally {
await stop();
}
});
test('help page is public and documents each service', async () => {
const { base, stop } = await bootApp();
try {
const res = await fetch(`${base}/help`);
assert.equal(res.status, 200);
const html = await res.text();
assert.ok(!html.includes('<!--app-header-->'), 'header marker is replaced');
assert.match(html, /cds-accordion-item title="Image Cropper"/);
assert.match(html, /cds-accordion-item title="Fingerprint Agent"/);
assert.match(html, /accordion\.min\.js/);
assert.match(html, /cds-header-nav-item href="\/help" aria-current="page"/);
assert.match(html, /cds-side-nav-link href="\/help" aria-current="page"/);
} finally {
await stop();
}
});
test('asset URLs are versioned per deployment; HTML is never cached', async () => {
const { base, stop } = await bootApp();
try {
const res = await fetch(`${base}/`);
const html = await res.text();
assert.equal(res.headers.get('cache-control'), 'no-store');
assert.match(html, /\/assets\/style\.css\?v=[a-z0-9]+/);
assert.match(html, /\/assets\/youreka-favicon\.png\?v=[a-z0-9]+/);
// versioned assets may be cached hard...
const css = await fetch(`${base}/assets/style.css?v=x`);
assert.equal(css.status, 200);
assert.match(css.headers.get('cache-control'), /immutable/);
// ...but the raw .html templates must not be
const tpl = await fetch(`${base}/assets/help.html`);
assert.equal(tpl.headers.get('cache-control'), 'no-store');
} finally {
await stop();
}