Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 146 additions & 0 deletions .tests/frontend/terminal-sexy.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import test from "node:test";
import assert from "node:assert/strict";

import {
clearTerminalSexyCatalogCache,
groupTerminalSexyCatalog,
importTerminalSexyTheme,
loadTerminalSexyCatalog,
normalizeTerminalSexyCatalog,
searchTerminalSexyThemes,
selectTerminalSexyFeaturedThemes,
} from "../../frontend/src/utils/terminalSexyThemes.js";

const originalFetch = globalThis.fetch;
const originalSetTimeout = globalThis.setTimeout;

const scheme = {
name: "Solarized Dark",
background: "#002b36",
foreground: "#839496",
color: [
"#073642", "#dc322f", "#859900", "#b58900", "#268bd2", "#d33682", "#2aa198", "#eee8d5",
"#002b36", "#cb4b16", "#586e75", "#657b83", "#839496", "#6c71c4", "#93a1a1", "#fdf6e3",
],
};

test.afterEach(() => {
globalThis.fetch = originalFetch;
globalThis.setTimeout = originalSetTimeout;
clearTerminalSexyCatalogCache();
});

test("terminal.sexy catalogs normalize and discard unsafe paths", () => {
const catalog = normalizeTerminalSexyCatalog(["base16/solarized.dark", "collection/Dawn", "../unsafe"]);
assert.deepEqual(catalog.map((entry) => entry.label), ["solarized", "Dawn"]);
assert.equal(catalog[0].appearance, "dark");
assert.equal(catalog[1].category, "collection");
});

test("terminal.sexy light and dark paths share one theme card", () => {
const catalog = normalizeTerminalSexyCatalog(["base16/solarized.dark", "base16/solarized.light"]);
const [group] = groupTerminalSexyCatalog(catalog);
assert.equal(group.label, "solarized");
assert.deepEqual(Object.keys(group.sources), ["dark", "light"]);
});

test("terminal.sexy featured themes choose five stable schemes", () => {
const catalog = normalizeTerminalSexyCatalog([
"base16/monokai.dark",
"base16/solarized.dark",
"base16/ocean.dark",
"collection/dawn",
"xcolors.net/zenburn",
"base16/3024.dark",
]);
assert.deepEqual(
selectTerminalSexyFeaturedThemes(catalog).map((entry) => entry.path),
["base16/monokai.dark", "base16/solarized.dark", "base16/ocean.dark", "collection/dawn", "xcolors.net/zenburn"],
);
});

test("terminal.sexy schemes can be searched and imported with variants", async () => {
const lightScheme = {
...scheme,
name: "Solarized Light",
background: "#fdf6e3",
foreground: "#657b83",
};
globalThis.fetch = async (input) => {
const url = String(input);
if (url.endsWith("/index.json")) return new Response(JSON.stringify(["base16/solarized.dark", "base16/solarized.light"]));
if (url.endsWith("/base16/solarized.dark.json")) return new Response(JSON.stringify(scheme));
if (url.endsWith("/base16/solarized.light.json")) return new Response(JSON.stringify(lightScheme));
throw new Error(`Unexpected URL: ${url}`);
};

const [result] = await searchTerminalSexyThemes("solarized");
assert.equal(result.label, "solarized");
assert.deepEqual(Object.keys(result.sources), ["dark", "light"]);
const theme = await importTerminalSexyTheme(result);
assert.equal(theme.label, "solarized");
assert.equal(theme.appearance, "light");
assert.equal(theme.colors.chrome, "#fdf6e3");
assert.equal(theme.variants.dark.chrome, "#002b36");
});

test("paired terminal.sexy sources keep their declared light and dark modes", async () => {
const measuredDark = { ...scheme, name: "Measured dark light source", background: "#101010" };
globalThis.fetch = async (input) => {
const url = String(input);
if (url.endsWith("/base16/solarized.dark.json")) return new Response(JSON.stringify(scheme));
if (url.endsWith("/base16/solarized.light.json")) return new Response(JSON.stringify(measuredDark));
throw new Error(`Unexpected URL: ${url}`);
};

const theme = await importTerminalSexyTheme({
id: "terminal-sexy-solarized",
label: "solarized",
sources: {
dark: { path: "base16/solarized.dark" },
light: { path: "base16/solarized.light" },
},
});

assert.equal(theme.appearance, "light");
assert.equal(theme.colors.chrome, "#101010");
assert.equal(theme.variants.dark.chrome, "#002b36");
});

test("terminal.sexy catalog failures do not poison later retries", async () => {
let attempts = 0;
globalThis.fetch = async () => {
attempts += 1;
if (attempts === 1) return new Response("unavailable", { status: 503 });
return new Response(JSON.stringify(["base16/solarized.dark"]));
};

await assert.rejects(loadTerminalSexyCatalog(), /unavailable/);
const catalog = await loadTerminalSexyCatalog();
assert.equal(catalog[0].path, "base16/solarized.dark");
assert.equal(attempts, 2);
});

test("terminal.sexy catalog rejects malformed and oversized responses", async () => {
globalThis.fetch = async () => new Response("not json");
await assert.rejects(loadTerminalSexyCatalog(), /unavailable/);

clearTerminalSexyCatalogCache();
globalThis.fetch = async () => new Response(JSON.stringify(["x".repeat(256 * 1024)]));
await assert.rejects(loadTerminalSexyCatalog(), /unavailable/);
});

test("terminal.sexy catalog requests abort when they exceed the timeout", async () => {
let requestSignal;
globalThis.setTimeout = (callback) => {
callback();
return 1;
};
globalThis.fetch = async (_input, options) => {
requestSignal = options.signal;
throw new Error("aborted");
};

await assert.rejects(loadTerminalSexyCatalog(), /unavailable/);
assert.equal(requestSignal.aborted, true);
});
20 changes: 20 additions & 0 deletions .tests/frontend/theme-settings.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import test from "node:test";
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";

test("main Themes cards do not expose modal-only scheme actions", async () => {
const source = await readFile(
new URL("../../frontend/src/pages/Settings/components/ThemeSettings.jsx", import.meta.url),
"utf8",
);
const sectionStart = source.indexOf('aria-labelledby="theme-themes-heading"');
const sectionEnd = source.indexOf("<SchemeSearchModal", sectionStart);
assert.ok(sectionStart >= 0);
assert.ok(sectionEnd > sectionStart);

const themesSection = source.slice(sectionStart, sectionEnd);
assert.equal(themesSection.includes("<SchemeCard"), false);
assert.equal(themesSection.includes("onPreview"), false);
assert.equal(themesSection.includes("onAdd"), false);
assert.equal(themesSection.includes("theme-settings__preview-note"), false);
});
192 changes: 180 additions & 12 deletions .tests/frontend/theme.test.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,73 @@
import test from "node:test";
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { runInNewContext } from "node:vm";

import {
applyThemePreview,
BUILT_IN_THEMES,
CUSTOM_THEMES_STORAGE_KEY,
getCustomThemes,
getThemeSettings,
installCustomTheme,
invalidateThemeCaches,
replaceCustomTheme,
THEME_STORAGE_KEY,
getThemePreference,
normalizeThemePreference,
setThemePreference,
setThemeSelection,
} from "../../frontend/src/utils/theme.js";
import { parseTerminalSexyTheme } from "../../frontend/src/utils/terminalSexyThemes.js";

const originalDocument = globalThis.document;
const originalLocalStorage = globalThis.localStorage;

test.afterEach(() => {
globalThis.document = originalDocument;
globalThis.localStorage = originalLocalStorage;
invalidateThemeCaches();
});

test("theme preferences default invalid stored values to system", () => {
assert.equal(normalizeThemePreference("light"), "light");
assert.equal(normalizeThemePreference("dark"), "dark");
assert.equal(normalizeThemePreference("sepia"), "system");

globalThis.localStorage = { getItem: () => "sepia" };
assert.equal(getThemePreference(), "system");
test("Aurral keeps the original light and dark palettes", () => {
const [aurral] = BUILT_IN_THEMES;
assert.deepEqual(
{
chrome: aurral.colors.chrome,
surface: aurral.colors.surface,
surfaceRaised: aurral.colors.surfaceRaised,
surfacePopover: aurral.colors.surfacePopover,
surfaceHover: aurral.colors.surfaceHover,
accent: aurral.colors.accent,
text: aurral.colors.text,
},
{
chrome: "#e5e7eb",
surface: "#ffffff",
surfaceRaised: "#f1f1f1",
surfacePopover: "#e4e4e4",
surfaceHover: "#d0d0d0",
accent: "#4d7c0f",
text: "#171717",
},
);
assert.deepEqual(
{
chrome: aurral.variants.dark.chrome,
surface: aurral.variants.dark.surface,
surfaceRaised: aurral.variants.dark.surfaceRaised,
surfacePopover: aurral.variants.dark.surfacePopover,
surfaceHover: aurral.variants.dark.surfaceHover,
accent: aurral.variants.dark.accent,
text: aurral.variants.dark.text,
},
{
chrome: "#000000",
surface: "#121212",
surfaceRaised: "#202020",
surfacePopover: "#2d2d2d",
surfaceHover: "#424242",
accent: "#84cc16",
text: "#ffffff",
},
);
});

test("theme preferences persist explicit themes and clear the system override", () => {
Expand All @@ -39,11 +84,134 @@ test("theme preferences persist explicit themes and clear the system override",
setItem: (key, value) => stored.set(key, value),
};

assert.equal(setThemePreference("light"), "light");
setThemeSelection("aurral", "light");
assert.equal(attributes.get("data-theme"), "light");
assert.equal(stored.get(THEME_STORAGE_KEY), "light");

assert.equal(setThemePreference("system"), "system");
setThemeSelection("aurral", "system");
assert.equal(attributes.has("data-theme"), false);
assert.equal(stored.get(THEME_STORAGE_KEY), "system");
});

test("temporary theme previews change the page without changing saved selection", () => {
const attributes = new Map();
const stored = new Map();
const styles = new Map();
globalThis.document = {
documentElement: {
style: {
setProperty: (name, value) => styles.set(name, value),
removeProperty: (name) => styles.delete(name),
},
removeAttribute: (name) => attributes.delete(name),
setAttribute: (name, value) => attributes.set(name, value),
},
querySelectorAll: () => [],
};
globalThis.localStorage = {
getItem: (key) => stored.get(key) ?? null,
setItem: (key, value) => stored.set(key, value),
};

const preview = {
id: "terminal-sexy-preview",
label: "Preview",
appearance: "dark",
colors: { ...BUILT_IN_THEMES[0].variants.dark, accent: "#ff00aa" },
};
applyThemePreview(preview, "dark");

assert.equal(styles.get("--aurral-accent"), "#ff00aa");
assert.equal(attributes.get("data-theme-id"), "terminal-sexy-preview");
assert.equal(stored.has(THEME_STORAGE_KEY), false);
assert.deepEqual(getThemeSettings(), { themeId: "aurral", appearance: "system" });
});

test("terminal.sexy ANSI colors become a complete Aurral palette", () => {
const source = {
name: "Example Dark",
background: "#1e1e1e",
foreground: "#d4d4d4",
color: [
"#000000", "#dc322f", "#859900", "#b58900", "#268bd2", "#d33682", "#2aa198", "#eee8d5",
"#073642", "#cb4b16", "#586e75", "#657b83", "#839496", "#6c71c4", "#93a1a1", "#fdf6e3",
],
};

const theme = parseTerminalSexyTheme(source);
assert.equal(theme.label, "Example Dark");
assert.equal(theme.appearance, "dark");
assert.equal(theme.colors.chrome, "#1e1e1e");
assert.equal(theme.colors.accent, "#268bd2");
assert.equal(theme.colors.danger, "#dc322f");
assert.equal(Object.keys(theme.colors).length, BUILT_IN_THEMES[0] && Object.keys(BUILT_IN_THEMES[0].colors).length);
});

test("custom themes persist and restore their selected mode", () => {
const stored = new Map();
globalThis.localStorage = {
getItem: (key) => stored.get(key) ?? null,
setItem: (key, value) => stored.set(key, value),
removeItem: (key) => stored.delete(key),
};
globalThis.document = {
documentElement: {
style: { setProperty() {}, removeProperty() {} },
setAttribute() {},
removeAttribute() {},
},
querySelectorAll: () => [],
};

const theme = installCustomTheme({
id: "midnight-garden",
label: "Midnight garden",
appearance: "dark",
colors: { accent: "#6cc5ff" },
});
assert.equal(stored.has(CUSTOM_THEMES_STORAGE_KEY), true);
assert.equal(getCustomThemes()[0].id, theme.id);

replaceCustomTheme({ ...theme, variants: { light: { accent: "#e5b567" } } });
assert.equal(getCustomThemes()[0].variants.light.accent, "#e5b567");

setThemeSelection(theme.id, "dark");
assert.deepEqual(getThemeSettings(), { themeId: theme.id, appearance: "dark" });
});

test("theme bootstrap restores a saved custom palette before app startup", async () => {
const source = await readFile(new URL("../../frontend/public/theme.js", import.meta.url), "utf8");
const values = new Map();
const dataset = {};
const storage = new Map([
[THEME_STORAGE_KEY, "terminal-sexy-custom"],
["aurralThemeAppearance:v1", "dark"],
[CUSTOM_THEMES_STORAGE_KEY, JSON.stringify([{
version: 1,
id: "terminal-sexy-custom",
name: "Custom",
appearance: "light",
colors: { chrome: "#eeeeee", surface: "#ffffff", accent: "#315fcb" },
variants: { dark: { chrome: "#101010", surface: "#181818", accent: "#6cc5ff" } },
}])],
]);

runInNewContext(source, {
localStorage: { getItem: (key) => storage.get(key) ?? null },
matchMedia: () => ({ matches: false }),
document: {
documentElement: {
dataset,
style: {
setProperty: (name, value) => values.set(name, value),
removeProperty: () => {},
},
},
},
});

assert.equal(dataset.theme, "dark");
assert.equal(dataset.themeId, "terminal-sexy-custom");
assert.equal(values.get("--aurral-chrome"), "#101010");
assert.equal(values.get("--aurral-accent"), "#6cc5ff");
});
Loading
Loading