@@ -12,7 +12,7 @@ import { describe, it, before, after } from 'node:test';
1212import assert from 'node:assert' ;
1313import fs from 'fs-extra' ;
1414import { createServer } from '../src/server.js' ;
15- import { renderServerRoot } from '../src/ui/server-root.js' ;
15+ import { renderServerRoot , decideRevealForRegisterStatus } from '../src/ui/server-root.js' ;
1616import { startTestServer , stopTestServer , request , assertStatus } from './helpers.js' ;
1717
1818describe ( 'Server-root landing page' , ( ) => {
@@ -105,22 +105,29 @@ describe('Server-root landing — operator override', () => {
105105
106106describe ( 'renderServerRoot' , ( ) => {
107107 // Mode-agnostic copy: the same page is served regardless of single-user
108- // vs multi-user. The status pill carries the mode label; the buttons
109- // adapt at load time via the HEAD probe (verified separately below).
110- it ( 'renders the same mode-agnostic copy regardless of singleUser flag' , ( ) => {
111- const single = renderServerRoot ( { version : '1.0.0' , singleUser : true } ) ;
112- const multi = renderServerRoot ( { version : '1.0.0' , singleUser : false } ) ;
113-
114- // Same welcome copy, same primary CTA, same explainer.
115- for ( const html of [ single , multi ] ) {
116- assert . match ( html , / < h 1 > W e l c o m e < \/ h 1 > / ) ;
117- assert . match ( html , / Y o u r J S S S o l i d p o d i s r u n n i n g / ) ;
118- assert . match ( html , / o p e n s t a n d a r d f o r p e r s o n a l d a t a / ) ;
119- }
120-
121- // Mode pill differs.
122- assert . match ( single , / < c o d e > s i n g l e - u s e r < \/ c o d e > / ) ;
123- assert . match ( multi , / < c o d e > m u l t i - u s e r < \/ c o d e > / ) ;
108+ // vs multi-user. There's no mode pill or features list in the seeded
109+ // HTML — those would go stale on the next mode change because of
110+ // skip-if-exists. Mode/feature differences land in the buttons, which
111+ // adapt at load time via the HEAD probe (covered below).
112+ it ( 'renders the same copy regardless of any context flags' , ( ) => {
113+ const a = renderServerRoot ( { version : '1.0.0' , singleUser : true } ) ;
114+ const b = renderServerRoot ( { version : '1.0.0' , singleUser : false } ) ;
115+ // Drop the only varying value (the version, identical here) and
116+ // assert byte-equality across the two renders.
117+ assert . strictEqual ( a , b ) ;
118+ assert . match ( a , / < h 1 > W e l c o m e < \/ h 1 > / ) ;
119+ assert . match ( a , / Y o u r J S S S o l i d p o d i s r u n n i n g / ) ;
120+ assert . match ( a , / o p e n s t a n d a r d f o r p e r s o n a l d a t a / ) ;
121+ } ) ;
122+
123+ it ( 'does not bake mode or feature pills into the seeded HTML' , ( ) => {
124+ // These would go stale: the seed is skip-if-exists, so a mode
125+ // change after first start wouldn't re-render them. Excluded
126+ // from the seed; the CLI banner already lists them at startup.
127+ const html = renderServerRoot ( { version : '1.0.0' , singleUser : true , enabled : { idp : true , nostr : true } } ) ;
128+ assert . doesNotMatch ( html , / < c o d e > s i n g l e - u s e r < \/ c o d e > / ) ;
129+ assert . doesNotMatch ( html , / < s p a n > i d p < \/ s p a n > / ) ;
130+ assert . doesNotMatch ( html , / < s p a n > n o s t r < \/ s p a n > / ) ;
124131 } ) ;
125132
126133 it ( 'always emits the Get started button pointing at the docs introduction' , ( ) => {
@@ -134,20 +141,25 @@ describe('renderServerRoot', () => {
134141
135142 it ( 'emits Sign up + Sign in buttons hidden for the HEAD probe to reveal' , ( ) => {
136143 const html = renderServerRoot ( { version : '1.0.0' } ) ;
137- // Both anchors are present in every mode; the inline script reveals
138- // them based on what /idp/register actually returns.
139144 assert . match ( html , / < a h r e f = " \/ i d p \/ r e g i s t e r " [ ^ > ] * d a t a - c o n d = " r e g i s t e r " [ ^ > ] * h i d d e n / ) ;
140145 assert . match ( html , / < a h r e f = " \/ i d p " [ ^ > ] * d a t a - c o n d = " l o g i n " [ ^ > ] * h i d d e n / ) ;
141146 assert . match ( html , / S i g n u p / ) ;
142147 assert . match ( html , / S i g n i n / ) ;
143148 } ) ;
144149
150+ it ( 'overrides the .btn display rule for the [hidden] attribute so the buttons actually start hidden' , ( ) => {
151+ // Without an explicit !important [hidden] rule, the .btn class's
152+ // display:inline-block beats the UA stylesheet's [hidden]{display:none}
153+ // and the Sign up / Sign in anchors flash visible before the HEAD probe
154+ // finishes. The CSS rule is the load-bearing piece; assert it's there.
155+ const html = renderServerRoot ( { version : '1.0.0' } ) ;
156+ assert . match ( html , / \[ h i d d e n \] \s * \{ [ ^ } ] * d i s p l a y : \s * n o n e \s * ! i m p o r t a n t / ) ;
157+ } ) ;
158+
145159 it ( 'includes the HEAD-adaptive script targeting /idp/register' , ( ) => {
146160 const html = renderServerRoot ( { version : '1.0.0' } ) ;
147161 assert . match ( html , / f e t c h \( [ ' " ] \/ i d p \/ r e g i s t e r [ ' " ] / ) ;
148162 assert . match ( html , / m e t h o d : \s * [ ' " ] H E A D [ ' " ] / ) ;
149- // The three documented branches: 200 → both, 403 → login only,
150- // anything else → neither. Assert the magic numbers are present.
151163 assert . match ( html , / r e s \. s t a t u s = = = 2 0 0 / ) ;
152164 assert . match ( html , / r e s \. s t a t u s = = = 4 0 3 / ) ;
153165 } ) ;
@@ -158,17 +170,6 @@ describe('renderServerRoot', () => {
158170 assert . match ( html , / w i n d o w \. l o c a t i o n \. o r i g i n / ) ;
159171 } ) ;
160172
161- it ( 'lists enabled features as pills' , ( ) => {
162- const html = renderServerRoot ( {
163- version : '1.0.0' ,
164- enabled : { idp : true , nostr : true , webrtc : true , terminal : true }
165- } ) ;
166- assert . match ( html , / < s p a n > i d p < \/ s p a n > / ) ;
167- assert . match ( html , / < s p a n > n o s t r < \/ s p a n > / ) ;
168- assert . match ( html , / < s p a n > w e b r t c < \/ s p a n > / ) ;
169- assert . match ( html , / < s p a n > t e r m i n a l < \/ s p a n > / ) ;
170- } ) ;
171-
172173 it ( 'interpolates version into the info box' , ( ) => {
173174 const html = renderServerRoot ( { version : '9.9.9' } ) ;
174175 assert . match ( html , / < c o d e > 9 \. 9 \. 9 < \/ c o d e > / ) ;
@@ -187,3 +188,45 @@ describe('renderServerRoot', () => {
187188 assert . match ( html , / < c o d e > \/ i n d e x \. h t m l < \/ c o d e > / ) ;
188189 } ) ;
189190} ) ;
191+
192+ // Pure-function unit tests for the HEAD response → button-reveal matrix.
193+ // The inline script in server-root.html implements the same matrix by
194+ // hand; a regex check on the script text (above) catches outright drops
195+ // of the literals, but only this helper test pins down the *behaviour*
196+ // of the matrix without needing a DOM.
197+ describe ( 'decideRevealForRegisterStatus' , ( ) => {
198+ it ( 'reveals both Sign up and Sign in for HTTP 200 (registration open)' , ( ) => {
199+ assert . deepStrictEqual (
200+ decideRevealForRegisterStatus ( 200 ) ,
201+ { register : true , login : true }
202+ ) ;
203+ } ) ;
204+
205+ it ( 'reveals only Sign in for HTTP 403 (single-user — registration disabled)' , ( ) => {
206+ assert . deepStrictEqual (
207+ decideRevealForRegisterStatus ( 403 ) ,
208+ { register : false , login : true }
209+ ) ;
210+ } ) ;
211+
212+ it ( 'reveals neither for HTTP 404 (no IDP)' , ( ) => {
213+ assert . deepStrictEqual (
214+ decideRevealForRegisterStatus ( 404 ) ,
215+ { register : false , login : false }
216+ ) ;
217+ } ) ;
218+
219+ it ( 'reveals neither for any other status (e.g. 500)' , ( ) => {
220+ assert . deepStrictEqual (
221+ decideRevealForRegisterStatus ( 500 ) ,
222+ { register : false , login : false }
223+ ) ;
224+ } ) ;
225+
226+ it ( 'reveals neither when status is undefined (network error)' , ( ) => {
227+ assert . deepStrictEqual (
228+ decideRevealForRegisterStatus ( undefined ) ,
229+ { register : false , login : false }
230+ ) ;
231+ } ) ;
232+ } ) ;
0 commit comments