const assert = require("assert"); const fs = require("fs"); const path = require("path"); const ejs = require("ejs"); const root = path.join(__dirname, ".."); function listFiles(directory, extension, output = []) { for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { const target = path.join(directory, entry.name); if (entry.isDirectory()) listFiles(target, extension, output); else if (target.endsWith(extension)) output.push(target); } return output; } function verifyViews() { const viewRoots = [path.join(root, "src", "web", "views"), path.join(root, "plugins")]; const files = viewRoots.flatMap((directory) => listFiles(directory, ".ejs")); for (const file of files) { ejs.compile(fs.readFileSync(file, "utf8"), { filename: file }); } return files.length; } function verifyThemeService() { const sandbox = fs.mkdtempSync(path.join(root, ".tmp-lumi-theme-test-")); const serviceDir = path.join(sandbox, "src", "services"); let database = null; fs.mkdirSync(serviceDir, { recursive: true }); for (const file of ["config.js", "db.js", "settings.js", "themes.js"]) { fs.copyFileSync( path.join(root, "src", "services", file), path.join(serviceDir, file) ); } try { database = require(path.join(serviceDir, "db.js")); database.migrate(); require(path.join(serviceDir, "settings.js")).ensureDefaults(); const themes = require(path.join(serviceDir, "themes.js")); assert.strictEqual(themes.BUILTIN_THEMES.length, 6); for (const theme of themes.BUILTIN_THEMES) { assert.deepStrictEqual(themes.validateThemeValues(theme), []); } const library = themes.listThemes(); assert(library.every((theme) => theme.builtin)); assert.throws( () => themes.saveCustomTheme("builtin:lumi-default", library[0]), /read-only/ ); const copy = themes.duplicateTheme("builtin:midnight", "Verification Theme"); assert.strictEqual(copy.builtin, false); assert.strictEqual(copy.typography.bodyFont, "lumi"); themes.setActiveTheme(copy.id); assert.strictEqual(themes.getActiveTheme().id, copy.id); const invalid = JSON.parse(JSON.stringify(copy)); invalid.light.text = "not-a-color"; assert.throws(() => themes.saveCustomTheme(copy.id, invalid), /hex color/); invalid.light.text = copy.light.text; invalid.typography.bodyFont = "external-css"; assert.throws(() => themes.saveCustomTheme(copy.id, invalid), /font preset/); const renamed = themes.renameCustomTheme(copy.id, "Verified Theme"); assert.strictEqual(renamed.name, "Verified Theme"); const repaired = themes.normalizeThemeValues({ ...renamed, light: { ...renamed.light, text: "#ffffff", surface: "#ffffff" } }); assert.notStrictEqual(repaired.light.text, repaired.light.surface); const themeView = path.join(root, "src", "web", "views", "admin-theme.ejs"); const rendered = ejs.render(fs.readFileSync(themeView, "utf8"), { title: "Theming", siteTitle: "Lumi Bot", assetVersion: "verify", theme: renamed, activeTheme: renamed, themes: themes.listThemes(), editingTheme: renamed, editingBaseTheme: themes.getThemeById(renamed.baseThemeId), fontStacks: themes.FONT_STACKS, botAvatar: null, navSections: [], user: { username: "Admin" }, userAvatar: null, userInitial: "A", platformLogins: [], flash: null, softError: null }, { filename: themeView }); assert(rendered.includes("data-theme-editor")); assert(rendered.includes("data-theme-font")); assert(rendered.includes("data-theme-popout")); assert(rendered.includes("data-lumi-state-button")); assert(rendered.includes("Built-in · read-only")); const loginView = path.join(root, "src", "web", "views", "localhost-login.ejs"); const loginRendered = ejs.render(fs.readFileSync(loginView, "utf8"), { title: "Localhost Login", username: "admin", siteTitle: "Lumi Bot", assetVersion: "verify", theme: renamed, botAvatar: null, navSections: [], user: null, userAvatar: null, userInitial: "", platformLogins: [{ id: "localhost", label: "Localhost Login", configured: true, loginPath: "/auth/localhost" }], flash: null, softError: null }, { filename: loginView }); assert(loginRendered.includes("admin / admin")); const statusView = path.join(root, "plugins", "moderation", "views", "status.ejs"); const statusRendered = ejs.render(fs.readFileSync(statusView, "utf8"), { title: "Access restricted", assetVersion: "verify", theme: renamed, sanction: { action_type: "timeout", status: "active", created_at: Date.now(), expires_at: Date.now() + 60000, reason_short: "Verification", reason_detail: "Standalone themed view verification.", created_by_name: "Admin" } }, { filename: statusView }); assert(statusRendered.includes("/lumi-components.css")); assert(statusRendered.includes('data-theme-id="custom:')); themes.deleteCustomTheme(copy.id); assert.strictEqual(themes.getActiveTheme().id, themes.DEFAULT_THEME_ID); } finally { database?.db.close(); fs.rmSync(sandbox, { recursive: true, force: true }); } } const viewCount = verifyViews(); verifyThemeService(); console.log(`WebUI verification passed: ${viewCount} EJS views and theme CRUD.`);