243 lines
9.8 KiB
JavaScript
243 lines
9.8 KiB
JavaScript
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 homeView = path.join(root, "src", "web", "views", "home.ejs");
|
|
const homeRendered = ejs.render(fs.readFileSync(homeView, "utf8"), {
|
|
title: "Home",
|
|
siteTitle: "Lumi Bot",
|
|
assetVersion: "verify",
|
|
theme: renamed,
|
|
botAvatar: null,
|
|
navSections: [],
|
|
user: { username: "Admin" },
|
|
userAvatar: null,
|
|
userInitial: "A",
|
|
platformLogins: [],
|
|
flash: null,
|
|
softError: null,
|
|
homepageLinks: [{
|
|
label: "Twitch",
|
|
description: "Watch live",
|
|
url: "https://example.com",
|
|
fallback_icon: "T"
|
|
}],
|
|
homepageHero: {
|
|
type: "static_image",
|
|
title: "Featured",
|
|
description: "Featured content",
|
|
image_url: "https://example.com/hero.png",
|
|
source_url: "https://example.com"
|
|
}
|
|
}, { filename: homeView });
|
|
assert(homeRendered.includes("homepage-link-button"));
|
|
assert(homeRendered.includes("homepage-dynamic-hero"));
|
|
|
|
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 });
|
|
}
|
|
}
|
|
|
|
function verifyUpdateCachePreserved() {
|
|
const updateManagerPath = path.join(root, "src", "services", "update-manager.js");
|
|
const source = fs.readFileSync(updateManagerPath, "utf8");
|
|
const generatedPaths = source.match(/const GENERATED_RELATIVE_PATHS = new Set\(\[([\s\S]*?)\]\);/);
|
|
assert(generatedPaths, "Could not find generated path cleanup list.");
|
|
assert(
|
|
!generatedPaths[1].includes('"data/update-cache"') &&
|
|
!generatedPaths[1].includes("'data/update-cache'"),
|
|
"Core update cleanup must not delete data/update-cache because repo updates apply from data/update-cache/repo."
|
|
);
|
|
}
|
|
|
|
function verifySharedUpdateActions() {
|
|
const appSource = fs.readFileSync(path.join(root, "src", "web", "public", "app.js"), "utf8");
|
|
const serverSource = fs.readFileSync(path.join(root, "src", "web", "server.js"), "utf8");
|
|
const dashboard = fs.readFileSync(path.join(root, "src", "web", "views", "admin-dashboard.ejs"), "utf8");
|
|
const settings = fs.readFileSync(path.join(root, "src", "web", "views", "admin-settings.ejs"), "utf8");
|
|
assert(appSource.includes('button[data-update-action]'));
|
|
assert(appSource.includes("submitter?.formAction || form.action"));
|
|
assert(appSource.includes('actionPath.endsWith("/check")'));
|
|
assert(dashboard.includes('action="/admin/check-update" class="inline-form" data-update-action'));
|
|
assert(settings.includes('formaction=\\"/admin/check-update\\" formmethod=\\"post\\" data-update-action'));
|
|
assert(serverSource.includes('sendUpdateResult(req, res, {\n status,'));
|
|
assert(serverSource.includes('message: "Core update applied. Lumi will restart in a few seconds."'));
|
|
}
|
|
|
|
function verifyHomepageEmbeds() {
|
|
const server = fs.readFileSync(path.join(root, "src", "web", "server.js"), "utf8");
|
|
const builder = fs.readFileSync(path.join(root, "src", "web", "public", "homepage-builder.js"), "utf8");
|
|
const home = fs.readFileSync(path.join(root, "src", "web", "views", "home.ejs"), "utf8");
|
|
const youtube = fs.readFileSync(path.join(root, "src", "services", "youtube.js"), "utf8");
|
|
|
|
assert(server.includes("function twitchParentDomains"));
|
|
assert(server.includes("function twitchChatEmbedUrl"));
|
|
assert(server.includes("function youtubeChatEmbedUrl"));
|
|
assert(server.includes('item.availability_mode === "live_only" && item.live_now !== true'));
|
|
assert(builder.includes('twitch_layout: "stream_only"'));
|
|
assert(builder.includes('youtube_show_chat: false'));
|
|
assert(builder.includes('additional_parent_domains: ""'));
|
|
assert(home.includes("homepageHero.secondary_embed_url"));
|
|
assert(home.includes("homepage-hero-embed-grid"));
|
|
assert(youtube.includes("liveStateUpdatedAt"));
|
|
assert(youtube.includes("activeVideoId"));
|
|
}
|
|
|
|
function verifyCustomPlaceholderSettings() {
|
|
const view = fs.readFileSync(path.join(root, "src", "web", "views", "admin-settings.ejs"), "utf8");
|
|
const client = fs.readFileSync(path.join(root, "src", "web", "public", "custom-placeholders.js"), "utf8");
|
|
const server = fs.readFileSync(path.join(root, "src", "web", "server.js"), "utf8");
|
|
assert(view.includes("Reusable text values"));
|
|
assert(view.includes("custom_placeholder_visibility"));
|
|
assert(view.includes("/custom-placeholders.js"));
|
|
assert(client.includes("data-custom-placeholder-row"));
|
|
assert(server.includes("validateCustomPlaceholders(customPlaceholdersFromBody(req.body))"));
|
|
assert(server.includes('setSetting("custom_placeholders"'));
|
|
}
|
|
|
|
const viewCount = verifyViews();
|
|
verifyThemeService();
|
|
verifyUpdateCachePreserved();
|
|
verifySharedUpdateActions();
|
|
verifyHomepageEmbeds();
|
|
verifyCustomPlaceholderSettings();
|
|
console.log(`WebUI verification passed: ${viewCount} EJS views, theme CRUD, and update-cache preservation.`);
|