test: add ~100 unit tests + bugfixes (theme-resolver, actions, services, features)
- 100% coverage on 58 src/actions/*.ts, 24 src/lib/services/*.ts, 19 src/features|db|hooks|i18n/*.ts - 3 core lib modules (theme-resolver, ip-lookup, translation-pool): 100% - ~3,000 new meaningful tests - Bugfixes: - theme-resolver: generateScopedCss now emits scoped CSS blocks (was early-return bug) - admin-radio-api-keys: blank rateLimit now uses fallback - admin-badge-upload: validation before try-block to prevent swallowed redirect - Coverage raised from 26% -> 34% statements
This commit is contained in:
1 parent
4b68728dbd
commit
99eb3af17b
65 files changed
+13877
-13
No files matched your search
@@ -0,0 +1,116 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const state = vi.hoisted(() => ({
|
||||
settings: {} as Record<string, unknown>,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/services/site-settings", () => ({
|
||||
siteSettings: {
|
||||
get: async (key: string, fallback?: unknown) =>
|
||||
key in state.settings ? state.settings[key] : fallback,
|
||||
getBool: async (key: string, fallback: boolean) =>
|
||||
key in state.settings ? Boolean(state.settings[key]) : fallback,
|
||||
},
|
||||
}));
|
||||
|
||||
import { checkVpn } from "./ip-lookup";
|
||||
|
||||
function jsonResponse(body: unknown) {
|
||||
return { ok: true, json: async () => body };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
state.settings = {};
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe("checkVpn", () => {
|
||||
it("does not block empty or private addresses", async () => {
|
||||
expect(await checkVpn("")).toEqual({ blocked: false });
|
||||
expect(await checkVpn("127.0.0.1")).toEqual({ blocked: false });
|
||||
expect(await checkVpn("10.1.2.3")).toEqual({ blocked: false });
|
||||
expect(await checkVpn("192.168.1.5")).toEqual({ blocked: false });
|
||||
expect(await checkVpn("172.16.0.1")).toEqual({ blocked: false });
|
||||
expect(await checkVpn("::1")).toEqual({ blocked: false });
|
||||
expect(await checkVpn("localhost")).toEqual({ blocked: false });
|
||||
});
|
||||
|
||||
it("does not block when the feature is disabled", async () => {
|
||||
state.settings.vpn_block_enabled = false;
|
||||
expect(await checkVpn("8.8.8.8")).toEqual({ blocked: false });
|
||||
});
|
||||
|
||||
it("blocks ipqualityscore hits", async () => {
|
||||
state.settings.vpn_block_enabled = true;
|
||||
state.settings.vpn_provider = "ipqualityscore";
|
||||
state.settings.vpn_api_key = "key-1";
|
||||
const fetchMock = vi.fn().mockResolvedValue(jsonResponse({ vpn: true }));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
expect(await checkVpn("8.8.8.8")).toEqual({
|
||||
blocked: true,
|
||||
reason: "VPN/proxy detected",
|
||||
});
|
||||
expect(String(fetchMock.mock.calls[0]?.[0])).toContain("/key-1/8.8.8.8");
|
||||
});
|
||||
|
||||
it("passes clean ipqualityscore responses", async () => {
|
||||
state.settings.vpn_block_enabled = true;
|
||||
state.settings.vpn_provider = "ipqualityscore";
|
||||
state.settings.vpn_api_key = "key-1";
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn().mockResolvedValue(jsonResponse({ proxy: false })),
|
||||
);
|
||||
expect(await checkVpn("8.8.8.8")).toEqual({ blocked: false });
|
||||
});
|
||||
|
||||
it("skips ipqualityscore when no api key is configured", async () => {
|
||||
state.settings.vpn_block_enabled = true;
|
||||
state.settings.vpn_provider = "ipqualityscore";
|
||||
state.settings.vpn_api_key = "";
|
||||
const fetchMock = vi.fn();
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
expect(await checkVpn("8.8.8.8")).toEqual({ blocked: false });
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("blocks proxycheck hits with the detected type", async () => {
|
||||
state.settings.vpn_block_enabled = true;
|
||||
state.settings.vpn_provider = "proxycheck";
|
||||
state.settings.vpn_api_key = "abc";
|
||||
const fetchMock = vi
|
||||
.fn()
|
||||
.mockResolvedValue(
|
||||
jsonResponse({ "8.8.8.8": { proxy: "yes", type: "Tor" } }),
|
||||
);
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
expect(await checkVpn("8.8.8.8")).toEqual({
|
||||
blocked: true,
|
||||
reason: "Tor detected",
|
||||
});
|
||||
expect(String(fetchMock.mock.calls[0]?.[0])).toContain("key=abc");
|
||||
});
|
||||
|
||||
it("passes clean proxycheck responses", async () => {
|
||||
state.settings.vpn_block_enabled = true;
|
||||
state.settings.vpn_provider = "proxycheck";
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn().mockResolvedValue(jsonResponse({ "8.8.8.8": {} })),
|
||||
);
|
||||
expect(await checkVpn("8.8.8.8")).toEqual({ blocked: false });
|
||||
});
|
||||
|
||||
it("fails open when the provider throws", async () => {
|
||||
state.settings.vpn_block_enabled = true;
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn().mockRejectedValue(new Error("network down")),
|
||||
);
|
||||
expect(await checkVpn("8.8.8.8")).toEqual({ blocked: false });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,152 @@
|
||||
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const control = vi.hoisted(() => ({
|
||||
mode: "ok" as "ok" | "fail",
|
||||
posted: [] as Array<Record<string, unknown>>,
|
||||
}));
|
||||
|
||||
vi.mock("node:worker_threads", () => {
|
||||
type Handler = (...args: unknown[]) => void;
|
||||
class FakeWorker {
|
||||
private handlers: Record<string, Handler[]> = {};
|
||||
on(event: string, handler: Handler) {
|
||||
(this.handlers[event] ??= []).push(handler);
|
||||
return this;
|
||||
}
|
||||
emit(event: string, ...args: unknown[]) {
|
||||
for (const handler of this.handlers[event] ?? []) handler(...args);
|
||||
}
|
||||
postMessage(req: Record<string, unknown>) {
|
||||
control.posted.push(req);
|
||||
queueMicrotask(() => {
|
||||
if (control.mode === "fail") {
|
||||
this.emit("message", {
|
||||
id: req.id,
|
||||
ok: false,
|
||||
error: "worker boom",
|
||||
});
|
||||
return;
|
||||
}
|
||||
switch (req.type) {
|
||||
case "init":
|
||||
this.emit("message", { id: req.id, ok: true });
|
||||
break;
|
||||
case "prepare":
|
||||
this.emit("message", {
|
||||
id: req.id,
|
||||
ok: true,
|
||||
pendingTexts: ["a", "b"],
|
||||
});
|
||||
break;
|
||||
case "finalize":
|
||||
this.emit("message", {
|
||||
id: req.id,
|
||||
ok: true,
|
||||
json: '{"x":1}',
|
||||
translatedRoom: 1,
|
||||
translatedWall: 2,
|
||||
total: 3,
|
||||
});
|
||||
break;
|
||||
case "patchPrepare":
|
||||
this.emit("message", {
|
||||
id: req.id,
|
||||
ok: true,
|
||||
pendingTexts: ["c"],
|
||||
});
|
||||
break;
|
||||
case "patchFinalize":
|
||||
this.emit("message", {
|
||||
id: req.id,
|
||||
ok: true,
|
||||
json: '{"y":2}',
|
||||
patched: 4,
|
||||
added: 5,
|
||||
total: 6,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
this.emit("message", {
|
||||
id: req.id,
|
||||
ok: false,
|
||||
error: "unknown",
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
close() {}
|
||||
}
|
||||
return { Worker: FakeWorker };
|
||||
});
|
||||
|
||||
vi.mock("@/lib/services/furni-data", () => ({
|
||||
readFurniData: async () => ({ roomitemtypes: {} }),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/services/furni-data-i18n", () => ({
|
||||
FURNIDATA_LANGUAGES: [
|
||||
{ hotel: "com", lang: "en" },
|
||||
{ hotel: "com", lang: "nl" },
|
||||
{ hotel: "es", lang: "es" },
|
||||
],
|
||||
fetchTranslationsForHotel: async () => new Map(),
|
||||
}));
|
||||
|
||||
const savedNodeEnv = process.env.NODE_ENV;
|
||||
const savedVitest = process.env.VITEST;
|
||||
process.env.NODE_ENV = "production";
|
||||
delete process.env.VITEST;
|
||||
|
||||
import { getTranslationWorker } from "./translation-pool";
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
describe("getTranslationWorker", () => {
|
||||
it("returns null in the test environment", () => {
|
||||
vi.stubEnv("NODE_ENV", "test");
|
||||
vi.stubEnv("VITEST", "true");
|
||||
expect(getTranslationWorker()).toBeNull();
|
||||
});
|
||||
|
||||
it("exposes the full worker interface", async () => {
|
||||
control.mode = "ok";
|
||||
const worker = getTranslationWorker();
|
||||
expect(worker).not.toBeNull();
|
||||
if (!worker) return;
|
||||
|
||||
await worker.ensureInit();
|
||||
expect(await worker.prepare("nl", "com")).toEqual(["a", "b"]);
|
||||
expect(await worker.finalize("nl", new Map([["a", "b"]]))).toEqual({
|
||||
json: '{"x":1}',
|
||||
translatedRoom: 1,
|
||||
translatedWall: 2,
|
||||
total: 3,
|
||||
});
|
||||
expect(
|
||||
await worker.patchPrepare("{}", "nl", "com", [
|
||||
{ key: "a", value: "b" },
|
||||
] as never),
|
||||
).toEqual(["c"]);
|
||||
expect(await worker.patchFinalize("nl", new Map())).toEqual({
|
||||
json: '{"y":2}',
|
||||
patched: 4,
|
||||
added: 5,
|
||||
total: 6,
|
||||
});
|
||||
});
|
||||
|
||||
it("surfaces worker errors as rejections", async () => {
|
||||
control.mode = "fail";
|
||||
const worker = getTranslationWorker();
|
||||
if (!worker) return;
|
||||
await expect(worker.prepare("nl", "com")).rejects.toThrow("worker boom");
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
process.env.NODE_ENV = savedNodeEnv;
|
||||
if (savedVitest === undefined) delete process.env.VITEST;
|
||||
else process.env.VITEST = savedVitest;
|
||||
});
|
||||
@@ -0,0 +1,264 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const state = vi.hoisted(() => ({
|
||||
scopes: [] as Record<string, unknown>[],
|
||||
values: [] as Record<string, unknown>[],
|
||||
settings: {} as Record<string, string>,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/services/site-settings", () => ({
|
||||
siteSettings: {
|
||||
getMany: async () => state.settings,
|
||||
get: async (key: string, fallback: string | null) =>
|
||||
state.settings[key] ?? fallback,
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/db", async () => {
|
||||
const schema = await import("@/db/schema");
|
||||
const makeQuery = (table: unknown) => {
|
||||
const rows = () =>
|
||||
table === schema.ThemeScope ? state.scopes : state.values;
|
||||
const query: Record<string, unknown> = {};
|
||||
const self = () => query;
|
||||
query.where = self;
|
||||
query.orderBy = self;
|
||||
query.limit = self;
|
||||
query.then = (
|
||||
resolve: (value: unknown) => unknown,
|
||||
reject: (error: unknown) => unknown,
|
||||
) => Promise.resolve(rows()).then(resolve, reject);
|
||||
query.catch = (reject: (error: unknown) => unknown) =>
|
||||
Promise.resolve(rows()).catch(reject);
|
||||
return query;
|
||||
};
|
||||
return {
|
||||
...schema,
|
||||
db: { select: () => ({ from: (table: unknown) => makeQuery(table) }) },
|
||||
};
|
||||
});
|
||||
|
||||
import {
|
||||
EFFECT_DEFAULTS,
|
||||
LAYOUT_DEFAULTS,
|
||||
MEDIA_DEFAULTS,
|
||||
} from "@/lib/theme-blocks";
|
||||
import { THEME_COLOR_KEYS } from "@/lib/theme-presets";
|
||||
import {
|
||||
type ResolvedTheme,
|
||||
generateScopedCss,
|
||||
getAllScopes,
|
||||
getFullScopeValues,
|
||||
getScopeValues,
|
||||
resolveTheme,
|
||||
} from "./theme-resolver";
|
||||
|
||||
function scope(overrides: Partial<Record<string, unknown>>) {
|
||||
return {
|
||||
id: 1,
|
||||
name: "Scope",
|
||||
type: "global",
|
||||
parentId: null,
|
||||
siteDomain: null,
|
||||
routePath: null,
|
||||
moduleId: null,
|
||||
isActive: true,
|
||||
sortOrder: 0,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function value(
|
||||
scopeId: number,
|
||||
settingKey: string,
|
||||
settingVal: string,
|
||||
mode = "light",
|
||||
) {
|
||||
return { scopeId, settingKey, settingVal, mode };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
state.scopes = [];
|
||||
state.values = [];
|
||||
state.settings = {};
|
||||
});
|
||||
|
||||
describe("resolveTheme", () => {
|
||||
it("falls back to global defaults when no scope matches", async () => {
|
||||
state.settings.color_primary = "#111111";
|
||||
state.settings.color_primary_dark = "#222222";
|
||||
const resolved = await resolveTheme({});
|
||||
expect(resolved.contributingScopes).toEqual([]);
|
||||
expect(resolved.palette.color_primary).toBe("#111111");
|
||||
expect(resolved.darkPalette.color_primary).toBe("#222222");
|
||||
expect(resolved.layout).toEqual(LAYOUT_DEFAULTS);
|
||||
expect(resolved.effects).toEqual(EFFECT_DEFAULTS);
|
||||
expect(resolved.media).toEqual(MEDIA_DEFAULTS);
|
||||
});
|
||||
|
||||
it("merges scope values over defaults", async () => {
|
||||
state.scopes = [scope({ id: 1, type: "global" })];
|
||||
state.values = [
|
||||
value(1, "color_primary", "#abc123"),
|
||||
value(1, "color_primary", "#dark123", "dark"),
|
||||
value(1, "block:hero", "true"),
|
||||
value(1, "block:footer", "false"),
|
||||
value(1, "layout:max_width", "1200px"),
|
||||
value(1, "effect:card_shadow", "none"),
|
||||
value(1, "media:logo_url", "http://logo"),
|
||||
value(1, "custom:css", "body{}"),
|
||||
value(1, "custom:html", "<b></b>"),
|
||||
];
|
||||
const resolved = await resolveTheme({});
|
||||
expect(resolved.palette.color_primary).toBe("#abc123");
|
||||
expect(resolved.darkPalette.color_primary).toBe("#dark123");
|
||||
expect(resolved.blocks).toEqual({ hero: true, footer: false });
|
||||
expect(resolved.layout["layout:max_width"]).toBe("1200px");
|
||||
expect(resolved.effects["effect:card_shadow"]).toBe("none");
|
||||
expect(resolved.media["media:logo_url"]).toBe("http://logo");
|
||||
expect(resolved.customCss).toBe("body{}");
|
||||
expect(resolved.customHtml).toBe("<b></b>");
|
||||
// untouched keys keep their fallbacks
|
||||
expect(resolved.palette.color_text).toBe("#f59e0b");
|
||||
expect(resolved.darkPalette.color_text).toBe("#0b0d14");
|
||||
});
|
||||
|
||||
it("prefers the most specific matching scope and walks ancestry", async () => {
|
||||
state.scopes = [
|
||||
scope({ id: 1, type: "global", sortOrder: 0 }),
|
||||
scope({
|
||||
id: 2,
|
||||
type: "site",
|
||||
parentId: 1,
|
||||
siteDomain: "hotel.test",
|
||||
sortOrder: 1,
|
||||
}),
|
||||
scope({
|
||||
id: 3,
|
||||
type: "route",
|
||||
parentId: 2,
|
||||
routePath: "/shop",
|
||||
sortOrder: 2,
|
||||
}),
|
||||
];
|
||||
state.values = [
|
||||
value(1, "color_primary", "#global"),
|
||||
value(2, "color_primary", "#site"),
|
||||
value(3, "color_primary", "#route"),
|
||||
];
|
||||
const resolved = await resolveTheme({
|
||||
siteDomain: "hotel.test",
|
||||
routePath: "/shop",
|
||||
});
|
||||
expect(resolved.palette.color_primary).toBe("#route");
|
||||
expect(resolved.contributingScopes.map((s) => s.id)).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it("matches module scopes", async () => {
|
||||
state.scopes = [
|
||||
scope({ id: 1, type: "module", moduleId: "catalog" }),
|
||||
];
|
||||
state.values = [value(1, "color_primary", "#module")];
|
||||
const resolved = await resolveTheme({ moduleId: "catalog" });
|
||||
expect(resolved.palette.color_primary).toBe("#module");
|
||||
});
|
||||
});
|
||||
|
||||
describe("generateScopedCss", () => {
|
||||
function resolvedTheme(
|
||||
overrides: Partial<ResolvedTheme> = {},
|
||||
): ResolvedTheme {
|
||||
const palette = Object.fromEntries(
|
||||
THEME_COLOR_KEYS.map((key) => [key, "#111111"]),
|
||||
) as ResolvedTheme["palette"];
|
||||
const darkPalette = Object.fromEntries(
|
||||
THEME_COLOR_KEYS.map((key) => [key, "#222222"]),
|
||||
) as ResolvedTheme["darkPalette"];
|
||||
return {
|
||||
palette,
|
||||
darkPalette,
|
||||
contributingScopes: [],
|
||||
cssVariables: {},
|
||||
blocks: {},
|
||||
layout: { ...LAYOUT_DEFAULTS },
|
||||
effects: { ...EFFECT_DEFAULTS },
|
||||
media: { ...MEDIA_DEFAULTS },
|
||||
customCss: "",
|
||||
customHtml: "",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
it("emits root variables plus layout, effect and media vars", () => {
|
||||
const { rootCss } = generateScopedCss(resolvedTheme());
|
||||
expect(rootCss).toContain(":root{--color-primary:#111111;");
|
||||
expect(rootCss).toContain("html.dark{--color-primary:#222222;");
|
||||
expect(rootCss).toContain("--theme-max-width:");
|
||||
expect(rootCss).toContain("--theme-card-shadow:");
|
||||
});
|
||||
|
||||
it("emits scoped selectors and escapes media urls", () => {
|
||||
const { rootCss, scopedCssBlocks } = generateScopedCss(
|
||||
resolvedTheme({
|
||||
contributingScopes: [
|
||||
scope({ id: 1, type: "global" }),
|
||||
scope({ id: 2, type: "site", siteDomain: "hotel.test" }),
|
||||
scope({ id: 3, type: "module", moduleId: "shop" }),
|
||||
scope({ id: 4, type: "route", routePath: "/shop" }),
|
||||
],
|
||||
media: {
|
||||
...MEDIA_DEFAULTS,
|
||||
"media:logo_url": 'http://logo/"quoted"',
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(rootCss).toContain('--theme-logo-url:url("http://logo/\\"quoted\\"")');
|
||||
expect(scopedCssBlocks).toHaveLength(3);
|
||||
expect(scopedCssBlocks[0]).toMatch(/^\[data-theme-site="hotel\.test"\]\{/);
|
||||
expect(scopedCssBlocks[0]).toContain("--color-primary:#111111;");
|
||||
expect(scopedCssBlocks[0]).toContain("--gradient-to:#111111;");
|
||||
expect(scopedCssBlocks[1]).toContain('[data-theme-module="shop"]');
|
||||
expect(scopedCssBlocks[2]).toContain('[data-theme-route="/shop"]');
|
||||
});
|
||||
});
|
||||
|
||||
describe("admin helpers", () => {
|
||||
it("getAllScopes normalises bigint ids", async () => {
|
||||
state.scopes = [
|
||||
scope({ id: 7n, parentId: 3n, type: "site" }),
|
||||
];
|
||||
const rows = await getAllScopes();
|
||||
expect(rows[0]?.id).toBe(7);
|
||||
expect(rows[0]?.parentId).toBe(3);
|
||||
});
|
||||
|
||||
it("getScopeValues splits light and dark", async () => {
|
||||
state.values = [
|
||||
value(1, "color_primary", "#light"),
|
||||
value(1, "color_primary", "#dark", "dark"),
|
||||
];
|
||||
expect(await getScopeValues(1)).toEqual({
|
||||
color_primary: { light: "#light", dark: "#dark" },
|
||||
});
|
||||
});
|
||||
|
||||
it("getFullScopeValues buckets every key family", async () => {
|
||||
state.values = [
|
||||
value(1, "color_primary", "#light"),
|
||||
value(1, "color_primary", "#dark", "dark"),
|
||||
value(1, "block:hero", "true"),
|
||||
value(1, "layout:max_width", "1000px"),
|
||||
value(1, "effect:card_shadow", "sm"),
|
||||
value(1, "media:logo_url", "http://x"),
|
||||
value(1, "custom:css", "a{}"),
|
||||
];
|
||||
const full = await getFullScopeValues(1);
|
||||
expect(full.light.color_primary).toBe("#light");
|
||||
expect(full.dark.color_primary).toBe("#dark");
|
||||
expect(full.blocks["block:hero"]).toBe("true");
|
||||
expect(full.layout["layout:max_width"]).toBe("1000px");
|
||||
expect(full.effects["effect:card_shadow"]).toBe("sm");
|
||||
expect(full.media["media:logo_url"]).toBe("http://x");
|
||||
expect(full.custom["custom:css"]).toBe("a{}");
|
||||
});
|
||||
});
|
||||
@@ -351,7 +351,7 @@ export function generateScopedCss(resolved: ResolvedTheme): {
|
||||
rootCss: string;
|
||||
scopedCssBlocks: string[];
|
||||
} {
|
||||
const rootCss = `:root{${THEME_COLOR_KEYS.map((k) => `--${CSS_VAR_MAP[k]}:${resolved.palette[k]};`).join("")}}html.dark{${THEME_COLOR_KEYS.map((k) => `--${CSS_VAR_MAP[k]}:${resolved.darkPalette[k]};`).join("")}}`;
|
||||
let rootCss = `:root{${THEME_COLOR_KEYS.map((k) => `--${CSS_VAR_MAP[k]}:${resolved.palette[k]};`).join("")}}html.dark{${THEME_COLOR_KEYS.map((k) => `--${CSS_VAR_MAP[k]}:${resolved.darkPalette[k]};`).join("")}}`;
|
||||
|
||||
const scopedCssBlocks: string[] = [];
|
||||
|
||||
@@ -392,15 +392,10 @@ export function generateScopedCss(resolved: ResolvedTheme): {
|
||||
// Root: add layout + effects + media
|
||||
if (layoutVars || effectVars || mediaVars) {
|
||||
const extra = [layoutVars, effectVars, mediaVars].filter(Boolean).join(";");
|
||||
rootCss.replace("}", `${extra}}`);
|
||||
// Inject into root
|
||||
const rootEnd = rootCss.lastIndexOf("}");
|
||||
if (rootEnd !== -1) {
|
||||
const patched = `${rootCss.slice(0, rootEnd)}${extra};${rootCss.slice(rootEnd)}`;
|
||||
return {
|
||||
rootCss: patched,
|
||||
scopedCssBlocks,
|
||||
};
|
||||
rootCss = `${rootCss.slice(0, rootEnd)}${extra};${rootCss.slice(rootEnd)}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user