import * as path from 'path'; import * as fs from 'fs'; import * as os from 'os'; import { Test } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { validateEnv, SETTINGS_KEYS } from '../../backend/src/config/env.schema'; import { ThemeLoaderService } from '../../backend/src/common/utils/theme-loader.service'; import { SettingsModule, SettingsService } from '../../backend/src/modules/settings/settings.module'; import { CommonModule } from '../../backend/src/common/common.module'; import { DatabaseModule } from '../../backend/src/database/database.module'; import { DatabaseInitService } from '../../backend/src/database/database-init.service'; import { THEME_IDS } from '../../backend/src/common/types/theme-ids'; describe('ThemeLoaderService - require 10 + validate configured key', () => { let app: INestApplication; let svc: ThemeLoaderService; async function buildApp(themeDir: string, themeKey: string): Promise { process.env.THEMES_DIR = themeDir; process.env.DATABASE_PATH = ':memory:'; process.env.FRONTEND_DIST = './frontend/dist'; const moduleRef = await Test.createTestingModule({ imports: [ ConfigModule.forRoot({ isGlobal: true, validate: validateEnv }), DatabaseModule, SettingsModule, CommonModule, ], }).compile(); app = moduleRef.createNestApplication(); await app.get(DatabaseInitService).init(); const settings = app.get(SettingsService); await settings.set(SETTINGS_KEYS.THEME_KEY, themeKey); svc = app.get(ThemeLoaderService); await svc.onModuleInit(); } afterEach(async () => { if (app) await app.close(); }); it('exposes all 10 canonical theme ids even when THEMES_DIR is empty', async () => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'themes-empty-')); await buildApp(tmp, 'classic'); const ids = svc.listThemes().map((t) => t.id).sort(); expect(ids).toEqual([ 'classic', 'crimson', 'cyber', 'forest', 'midnight', 'monochrome', 'neon', 'ocean', 'paper', 'sunset', ]); expect(svc.listThemes().length).toBe(THEME_IDS.length); }); it('backfills missing disk themes with built-ins (per-id warn)', async () => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'themes-partial-')); // Only write 3 of the 10 themes to disk. for (const id of ['classic', 'midnight', 'cyber']) { fs.writeFileSync(path.join(tmp, `${id}.json`), JSON.stringify({ id, name: id, tokens: { primary: '#000', secondary: '#111', accent: '#222', surface: '#fff', text: '#000', success: '#0f0', warning: '#ff0', danger: '#f00', fontFamily: 'sans', radii: { sm: '1px', md: '2px', lg: '3px' }, spacingScale: [4, 8], }, })); } await buildApp(tmp, 'classic'); expect(svc.listThemes().length).toBe(THEME_IDS.length); }); it('uses the configured themeKey as default when valid', async () => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'themes-')); await buildApp(tmp, 'ocean'); expect(svc.getDefaultId()).toBe('ocean'); const theme = svc.getTheme('ocean'); expect(theme.id).toBe('ocean'); }); it('falls back to classic and warns when configured themeKey is invalid', async () => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'themes-')); await buildApp(tmp, 'this-theme-does-not-exist'); expect(svc.getDefaultId()).toBe('classic'); }); it('falls back to classic and warns when configured themeKey is empty', async () => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'themes-')); await buildApp(tmp, ''); expect(svc.getDefaultId()).toBe('classic'); }); it('getTheme() returns a real theme for any canonical id', async () => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'themes-')); await buildApp(tmp, 'classic'); for (const id of THEME_IDS) { expect(svc.getTheme(id).id).toBe(id); } }); });