- {#if errorCode === 'NavigationError'}
-
- Signed in.
Continue when ready.
-
- {:else if errorCode === 'CredentialsSignin'}
- Invalid username or password
- {:else if errorCode === 'RateLimited'}
- Too many sign-in attempts. Please wait and try again.
- {:else if errorCode === 'NetworkError'}
- Unable to connect. Please try again.
- {:else}
- Unable to sign in. Please try again.
- {/if}
+
+
+
+ {#if errorCode === 'NavigationError'}
+
+ Signed in.
Continue when ready.
+
+ {:else if errorCode === 'CredentialsSignin'}
+ Invalid username or password. Please try again.
+ {:else if errorCode === 'RateLimited'}
+ Too many sign-in attempts. Please wait and try again.
+ {:else if errorCode === 'NetworkError'}
+ Unable to connect. Please try again.
+ {:else}
+ Unable to sign in. Please try again.
+ {/if}
+
{/if}
-{/snippet}
-
Login
-
-
- {#if data.authType === 'oidc'}
-
- {@render errorBanner()}
-
login via {data.oidcName}
-
-
- {:else if data.authType === 'credentials'}
-
- {/if}
+ {:else if data.authType === 'oidc'}
+ Use {data.oidcName} to access your archive.
+ {/if}
+
+
+
+
+
diff --git a/apps/web/src/routes/signin/fixtures/SigninTree.svelte b/apps/web/src/routes/signin/fixtures/SigninTree.svelte
new file mode 100644
index 00000000..f550986b
--- /dev/null
+++ b/apps/web/src/routes/signin/fixtures/SigninTree.svelte
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/apps/web/src/routes/signin/page.test.ts b/apps/web/src/routes/signin/page.test.ts
index 362e12ad..88f9be28 100644
--- a/apps/web/src/routes/signin/page.test.ts
+++ b/apps/web/src/routes/signin/page.test.ts
@@ -1,58 +1,98 @@
-import { readFileSync } from 'node:fs'
-import { parse } from 'svelte/compiler'
import { afterAll, expect, test, vi } from 'vitest'
import { render } from 'svelte/server'
+import type { ComponentProps } from 'svelte'
+import type { ConfigPublic } from '@luzzle/web.config'
+import { makePublicConfig } from '#lib/server/config.fixture.js'
import Signin from './+page.svelte'
+import SigninTree from './fixtures/SigninTree.svelte'
+const { page } = vi.hoisted(() => ({
+ page: {
+ url: new URL('https://app.test/signin'),
+ data: {
+ config: undefined as ConfigPublic | undefined,
+ meta: { title: 'Test Garden', description: 'An archive' }
+ }
+ }
+}))
+vi.mock('$app/state', () => ({ page }))
+vi.mock('$app/env', () => ({ browser: false }))
+vi.mock('$app/paths', () => ({ resolve: (path: string) => (path === '' ? '/' : path) }))
vi.mock('$app/navigation', () => ({ goto: vi.fn() }))
-// Import the real client during SSR without consulting ambient auth URL settings.
+// Client authentication must remain lazy during SSR, even with invalid ambient settings.
vi.hoisted(() => vi.stubEnv('BETTER_AUTH_URL', 'not-a-url'))
afterAll(() => vi.unstubAllEnvs())
-const source = readFileSync(new URL('./+page.svelte', import.meta.url), 'utf8')
+function signinData(): ComponentProps
['data'] {
+ const config = makePublicConfig()
+ config.content.text.title = 'Test Garden'
+ config.content.images.favicon = '/assets/favicon/garden.svg'
+ return {
+ pieceTypes: [],
+ config,
+ meta: { title: 'Test Garden', description: 'An archive' },
+ redirectTo: '/admin',
+ authType: 'credentials',
+ oidcName: undefined,
+ errorCode: null
+ }
+}
-// Structural guards: SSR cannot exercise clicks or client-side navigation.
-test('the client destination reacts to validated page data, not raw query parameters', () => {
- const statements = parse(source, { modern: true }).instance?.content.body ?? []
- const declaration = statements
- .flatMap((statement) =>
- statement.type === 'VariableDeclaration' ? statement.declarations : []
- )
- .find(
- (declaration) => declaration.id.type === 'Identifier' && declaration.id.name === 'redirectTo'
- )
+test('credentials use configured branding and named POST controls with password-manager hints', () => {
+ const data = signinData()
- expect(declaration).toMatchObject({
- init: {
- type: 'CallExpression',
- callee: { type: 'Identifier', name: '$derived' },
- arguments: [
- {
- type: 'MemberExpression',
- object: { type: 'Identifier', name: 'data' },
- property: { type: 'Identifier', name: 'redirectTo' }
- }
- ]
- }
- })
- expect(source).not.toContain('searchParams')
+ const result = render(Signin, { props: { data, params: {}, form: null } })
+ const html = result.body.replace(//g, '')
+
+ expect(html).toContain('Sign in to Test Garden')
+ expect(html).toContain('src="/assets/favicon/garden.svg"')
+ expect(html).toMatch(/