improved component composition and added tests.

This commit is contained in:
Moon.eth
2026-07-29 10:30:28 +09:00
parent 95f9d73681
commit 264ae17a8d
39 changed files with 1811 additions and 147 deletions
+84
View File
@@ -0,0 +1,84 @@
/**
* Runtime dependencies used by Svelte components.
*
* Components read these through context instead of importing backend functions
* and global stores directly. Production gets the real implementations by
* default; tests can replace only the pieces they exercise.
*/
import { getContext } from 'svelte'
import type { ApiClient } from './api/client'
import type { CredentialAccount, InstanceInfo } from './api/types'
import * as endpointImplementations from './api/endpoints'
import { router as defaultRouter, type RouteMatch } from './router.svelte'
import { session as defaultSession } from './stores/session.svelte'
import { theme as defaultTheme } from './stores/theme.svelte'
export interface SessionService {
host: string
token: string | null
me: CredentialAccount | null
instance: InstanceInfo | null
loading: boolean
error: string | null
readonly api: ApiClient
readonly signedIn: boolean
readonly connected: boolean
restore(): Promise<string | null>
connect(host: string): Promise<void>
login(host: string, returnTo?: string): Promise<void>
logout(): Promise<void>
disconnect(): void
}
export interface RouterService {
current: RouteMatch
go(to: string): void
replace(to: string): void
}
export interface ThemeService {
viewerCss: string
allowProfileCss: boolean
setViewerCss(css: string): void
setAllowProfileCss(allow: boolean): void
applyProfileCss(css: string | null | undefined): void
clearProfileCss(): void
}
export interface AppServices {
session: SessionService
router: RouterService
theme: ThemeService
endpoints: typeof endpointImplementations
}
export const APP_SERVICES = Symbol('plspace.app-services')
export const defaultAppServices: AppServices = {
session: defaultSession,
router: defaultRouter,
theme: defaultTheme,
endpoints: endpointImplementations,
}
/** Read dependencies supplied by a parent/test, falling back to production. */
export function useAppServices(): AppServices {
return getContext<AppServices | undefined>(APP_SERVICES) ?? defaultAppServices
}
export interface AppServiceOverrides {
session?: SessionService
router?: RouterService
theme?: ThemeService
endpoints?: Partial<typeof endpointImplementations>
}
/** Build a complete service object from small test doubles. */
export function createAppServices(overrides: AppServiceOverrides = {}): AppServices {
return {
session: overrides.session ?? defaultAppServices.session,
router: overrides.router ?? defaultAppServices.router,
theme: overrides.theme ?? defaultAppServices.theme,
endpoints: { ...defaultAppServices.endpoints, ...overrides.endpoints },
}
}
+6 -1
View File
@@ -56,7 +56,12 @@ function matchPattern(pattern: string, path: string): Record<string, string> | n
if (prefixed) {
const [, prefix, name] = prefixed
if (prefix && !actual.startsWith(prefix)) return null
const value = decodeURIComponent(actual.slice(prefix.length))
let value: string
try {
value = decodeURIComponent(actual.slice(prefix.length))
} catch {
return null
}
if (!value) return null
params[name.slice(1)] = value
continue
+13
View File
@@ -0,0 +1,13 @@
import { describe, expect, it } from 'vitest'
import { parseHash } from './router.svelte'
describe('parseHash', () => {
it('decodes valid route parameters', () => {
expect(parseHash('#/@alice%40remote.test').params.acct).toBe('alice@remote.test')
})
it('treats malformed percent escapes as not found', () => {
expect(() => parseHash('#/@broken%ZZ')).not.toThrow()
expect(parseHash('#/@broken%ZZ').name).toBe('notfound')
})
})