feat(config): add shared browser cookie policy
Quality / quality (ubuntu-latest) (push) Failing after 9m46s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-12 21:53:07 +05:30
parent a0dab308f0
commit 5de792f359
15 changed files with 198 additions and 22 deletions
+4 -4
View File
@@ -268,7 +268,7 @@
}, },
"packages/cli": { "packages/cli": {
"name": "@wrnexus/cli", "name": "@wrnexus/cli",
"version": "0.8.24", "version": "0.8.29",
"bin": { "bin": {
"wrnexus": "src/index.ts", "wrnexus": "src/index.ts",
}, },
@@ -332,7 +332,7 @@
}, },
"packages/dev-server": { "packages/dev-server": {
"name": "@wrnexus/dev-server", "name": "@wrnexus/dev-server",
"version": "0.8.23", "version": "0.8.28",
"dependencies": { "dependencies": {
"@wrnexus/authz": "workspace:*", "@wrnexus/authz": "workspace:*",
"@wrnexus/cache": "workspace:*", "@wrnexus/cache": "workspace:*",
@@ -392,7 +392,7 @@
}, },
"packages/i18n": { "packages/i18n": {
"name": "@wrnexus/i18n", "name": "@wrnexus/i18n",
"version": "0.8.9", "version": "0.8.10",
"dependencies": { "dependencies": {
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
"@wrnexus/plugin": "workspace:*", "@wrnexus/plugin": "workspace:*",
@@ -588,7 +588,7 @@
}, },
"packages/styles": { "packages/styles": {
"name": "@wrnexus/styles", "name": "@wrnexus/styles",
"version": "0.8.14", "version": "0.8.15",
"dependencies": { "dependencies": {
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
"@wrnexus/plugin": "workspace:*", "@wrnexus/plugin": "workspace:*",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/cli", "name": "@wrnexus/cli",
"version": "0.8.28", "version": "0.8.29",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+12 -2
View File
@@ -44,6 +44,7 @@ import {
bundleCss, bundleCss,
renderStyles, renderStyles,
resolveThemeConfig, resolveThemeConfig,
resolveBrowserCookieOptions,
renderActiveThemeCss, renderActiveThemeCss,
renderThemeCss, renderThemeCss,
renderThemeRuntime, renderThemeRuntime,
@@ -493,7 +494,7 @@ export async function runBuild(appRoot: string): Promise<void> {
console.log(`✓ Controllers: ${controllersPath}`); console.log(`✓ Controllers: ${controllersPath}`);
// 1a) Theme tokens + client switcher (always emitted; built-in light/dark). // 1a) Theme tokens + client switcher (always emitted; built-in light/dark).
const theme = resolveThemeConfig(config.theme); const theme = resolveThemeConfig(config.theme, config.cookies);
const themeCss = renderThemeCss(theme); const themeCss = renderThemeCss(theme);
const themeJs = renderThemeRuntime(theme); const themeJs = renderThemeRuntime(theme);
writeFileSync(join(distDir, "theme.css"), themeCss, "utf8"); writeFileSync(join(distDir, "theme.css"), themeCss, "utf8");
@@ -541,8 +542,17 @@ export async function runBuild(appRoot: string): Promise<void> {
// 1a4) i18n: bake locale messages into the manifest (opt-in via app/locales). // 1a4) i18n: bake locale messages into the manifest (opt-in via app/locales).
const localeMessages = loadLocales(join(appDir, "locales")); const localeMessages = loadLocales(join(appDir, "locales"));
const i18nConfig = config.i18n
? {
...config.i18n,
cookie: {
...resolveBrowserCookieOptions(config.cookies, "language"),
...(config.i18n.cookie ?? {}),
},
}
: undefined;
const i18n = Object.keys(localeMessages).length const i18n = Object.keys(localeMessages).length
? resolveI18n(localeMessages, config.i18n) ? resolveI18n(localeMessages, i18nConfig)
: undefined; : undefined;
if (i18n) console.log(`✓ i18n: ${i18n.langs.length} locales (default: ${i18n.default})`); if (i18n) console.log(`✓ i18n: ${i18n.langs.length} locales (default: ${i18n.default})`);
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/dev-server", "name": "@wrnexus/dev-server",
"version": "0.8.27", "version": "0.8.28",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+15 -2
View File
@@ -12,6 +12,8 @@ import type { Middleware, Mode, SecurityConfig, SeoConfig } from "@wrnexus/core"
import { buildRouter, type Router } from "@wrnexus/router"; import { buildRouter, type Router } from "@wrnexus/router";
import { import {
resolveThemeConfig, resolveThemeConfig,
resolveBrowserCookieOptions,
type BrowserCookiesConfig,
type StylesConfig, type StylesConfig,
type ThemeConfig, type ThemeConfig,
type MobileConfig, type MobileConfig,
@@ -101,6 +103,8 @@ export interface ServeOptions {
security?: SecurityConfig; security?: SecurityConfig;
/** Design-token theme config (merged over the built-in light/dark). */ /** Design-token theme config (merged over the built-in light/dark). */
theme?: ThemeConfig; theme?: ThemeConfig;
/** Shared browser-cookie defaults and preference-specific overrides. */
cookies?: BrowserCookiesConfig;
/** i18n config (default language + supported locales). */ /** i18n config (default language + supported locales). */
i18n?: I18nConfig; i18n?: I18nConfig;
/** Default database connection (driver + url). Enables `getDb()` and dev auto-migrate. */ /** Default database connection (driver + url). Enables `getDb()` and dev auto-migrate. */
@@ -355,7 +359,7 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
// Compile every `.wrn` into ONE cache dir at the project root, instead of a // Compile every `.wrn` into ONE cache dir at the project root, instead of a
// `.wrnexus/` next to each source file (and inside node_modules UI dirs). // `.wrnexus/` next to each source file (and inside node_modules UI dirs).
setCompileCacheDir(cacheDir); setCompileCacheDir(cacheDir);
const theme = resolveThemeConfig(opts.theme); const theme = resolveThemeConfig(opts.theme, opts.cookies);
const uiStylesPath = uiCssPath(); const uiStylesPath = uiCssPath();
const schemasJs = await schemaRuntime(router); const schemasJs = await schemaRuntime(router);
@@ -375,8 +379,17 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
// i18n is opt-in by the presence of app/locales/*.json. // i18n is opt-in by the presence of app/locales/*.json.
const localeMessages = loadLocales(join(appDir, "locales"), { strict: opts.i18n?.strict }); const localeMessages = loadLocales(join(appDir, "locales"), { strict: opts.i18n?.strict });
const i18nConfig = opts.i18n
? {
...opts.i18n,
cookie: {
...resolveBrowserCookieOptions(opts.cookies, "language"),
...(opts.i18n.cookie ?? {}),
},
}
: undefined;
const i18n = Object.keys(localeMessages).length const i18n = Object.keys(localeMessages).length
? resolveI18n(localeMessages, opts.i18n) ? resolveI18n(localeMessages, i18nConfig)
: undefined; : undefined;
// Databases: configure the default (getDb()) + each named one (getDb("<name>")), // Databases: configure the default (getDb()) + each named one (getDb("<name>")),
+1
View File
@@ -54,6 +54,7 @@ const server = await startServer({
seo: config.seo, seo: config.seo,
security: config.security, security: config.security,
theme: config.theme, theme: config.theme,
cookies: config.cookies,
i18n: config.i18n, i18n: config.i18n,
db: config.db, db: config.db,
databases: config.databases, databases: config.databases,
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/i18n", "name": "@wrnexus/i18n",
"version": "0.8.9", "version": "0.8.10",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "./src/index.ts", "main": "./src/index.ts",
+12 -4
View File
@@ -30,6 +30,7 @@ export interface LocaleLoadOptions {
export interface I18nCookieConfig { export interface I18nCookieConfig {
name?: string; name?: string;
maxAge?: number; maxAge?: number;
domain?: string;
path?: string; path?: string;
sameSite?: "Strict" | "Lax" | "None"; sameSite?: "Strict" | "Lax" | "None";
secure?: boolean; secure?: boolean;
@@ -253,6 +254,7 @@ export function resolveI18n(
cookie: { cookie: {
name: config.cookie?.name ?? LANG_COOKIE, name: config.cookie?.name ?? LANG_COOKIE,
maxAge: config.cookie?.maxAge ?? 31_536_000, maxAge: config.cookie?.maxAge ?? 31_536_000,
domain: config.cookie?.domain ?? "",
path: config.cookie?.path ?? "/", path: config.cookie?.path ?? "/",
sameSite: config.cookie?.sameSite ?? "Lax", sameSite: config.cookie?.sameSite ?? "Lax",
secure: config.cookie?.sameSite === "None" ? true : (config.cookie?.secure ?? false), secure: config.cookie?.sameSite === "None" ? true : (config.cookie?.secure ?? false),
@@ -464,10 +466,16 @@ export const I18N_RUNTIME = String.raw`
var current = state(); var current = state();
if (current.langs && current.langs.indexOf(lang) === -1) return false; if (current.langs && current.langs.indexOf(lang) === -1) return false;
var cookie = current.cookie || {}; var cookie = current.cookie || {};
var secure = cookie.secure || location.protocol === "https:"; var cookieName = cookie.name || "${LANG_COOKIE}";
document.cookie = encodeURIComponent(cookie.name || "${LANG_COOKIE}") + "=" + encodeURIComponent(lang) + if (window.wrnCookies && typeof window.wrnCookies.set === "function") {
";path=" + (cookie.path || "/") + ";max-age=" + (cookie.maxAge || 31536000) + window.wrnCookies.set(cookieName, lang, "language", cookie);
";samesite=" + (cookie.sameSite || "Lax") + (secure ? ";secure" : ""); } else {
var secure = cookie.secure || location.protocol === "https:";
var domain = cookie.domain ? ";domain=" + cookie.domain : "";
document.cookie = encodeURIComponent(cookieName) + "=" + encodeURIComponent(lang) +
";path=" + (cookie.path || "/") + ";max-age=" + (cookie.maxAge || 31536000) +
";samesite=" + (cookie.sameSite || "Lax") + domain + (secure ? ";secure" : "");
}
document.documentElement.lang = lang; document.documentElement.lang = lang;
document.documentElement.dir = (current.directions && current.directions[lang]) || "ltr"; document.documentElement.dir = (current.directions && current.directions[lang]) || "ltr";
window.dispatchEvent(new CustomEvent("wrnexus:language-change", { detail: { locale: lang } })); window.dispatchEvent(new CustomEvent("wrnexus:language-change", { detail: { locale: lang } }));
+5 -1
View File
@@ -1,5 +1,5 @@
import { test, expect } from "bun:test"; import { test, expect } from "bun:test";
import { resolveI18n, makeT, resolveLang, translateHtml } from "../src/index.ts"; import { I18N_RUNTIME, resolveI18n, makeT, resolveLang, translateHtml } from "../src/index.ts";
const i18n = resolveI18n( const i18n = resolveI18n(
{ {
@@ -27,6 +27,10 @@ test("resolveLang: cookie → Accept-Language → default", () => {
expect(resolveLang(i18n, "xx", "de")).toBe("en"); // invalid cookie + unsupported header expect(resolveLang(i18n, "xx", "de")).toBe("en"); // invalid cookie + unsupported header
}); });
test("language changes use the shared browser cookie helper", () => {
expect(I18N_RUNTIME).toContain('window.wrnCookies.set(cookieName, lang, "language", cookie)');
});
test("translateHtml resolves data-t text and t:attr attributes", () => { test("translateHtml resolves data-t text and t:attr attributes", () => {
const t = makeT(i18n, "en"); const t = makeT(i18n, "en");
const out = translateHtml('<input t:placeholder="ph"><span data-t="nav.home"></span>', t); const out = translateHtml('<input t:placeholder="ph"><span data-t="nav.home"></span>', t);
+29
View File
@@ -166,6 +166,35 @@ theme: {
The client runtime (`renderThemeRuntime`) exposes `window.wrnTheme` with `{ get, set, toggle, bind, themes }`, connects up any `[data-wrn-theme-toggle]` and `[data-wrn-theme-set]` elements on load, and persists the choice to the `wrn-theme` cookie (`max-age` 1 year, `samesite=lax`). `toggle()` cycles through the configured theme names in order. The client runtime (`renderThemeRuntime`) exposes `window.wrnTheme` with `{ get, set, toggle, bind, themes }`, connects up any `[data-wrn-theme-toggle]` and `[data-wrn-theme-set]` elements on load, and persists the choice to the `wrn-theme` cookie (`max-age` 1 year, `samesite=lax`). `toggle()` cycles through the configured theme names in order.
### Shared browser cookies
Set one cookie policy for theme, accent, language, and application cookies:
```ts
export default {
cookies: {
defaults: {
domain: "example.com",
path: "/",
maxAge: 31_536_000,
sameSite: "Lax",
secure: true,
},
language: { maxAge: 2_592_000 },
},
} satisfies AppConfig;
```
The theme, accent, and language switchers use this policy automatically. Client
functions can use the same resolved policy for other values:
```ts
window.wrnCookies.set("dashboard-view", "compact");
window.wrnCookies.set("locale-preview", "hi", "language");
const value = window.wrnCookies.get("dashboard-view");
window.wrnCookies.remove("dashboard-view");
```
## Usage ## Usage
### `wrnexus.config.ts` ### `wrnexus.config.ts`
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/styles", "name": "@wrnexus/styles",
"version": "0.8.14", "version": "0.8.15",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+4 -1
View File
@@ -14,7 +14,7 @@ import { pathToFileURL } from "node:url";
import type { PerformanceBudgets, SecurityConfig, SeoConfig } from "@wrnexus/core"; import type { PerformanceBudgets, SecurityConfig, SeoConfig } from "@wrnexus/core";
import type { PluginInput, PluginPermission } from "@wrnexus/plugin"; import type { PluginInput, PluginPermission } from "@wrnexus/plugin";
import type { StorageConfig } from "@wrnexus/uploader"; import type { StorageConfig } from "@wrnexus/uploader";
import type { ThemeConfig } from "./theme.ts"; import type { BrowserCookiesConfig, ThemeConfig } from "./theme.ts";
import type { FontConfig } from "./fonts.ts"; import type { FontConfig } from "./fonts.ts";
import { fontCspSources } from "./fonts.ts"; import { fontCspSources } from "./fonts.ts";
import { import {
@@ -299,6 +299,8 @@ export interface AppConfig extends CompatibilityPolicy {
fonts?: FontConfig; fonts?: FontConfig;
/** Design-token themes (deep-merged over the built-in light/dark). */ /** Design-token themes (deep-merged over the built-in light/dark). */
theme?: ThemeConfig; theme?: ThemeConfig;
/** Shared client cookie policy used by theme, accent, language, and window.wrnCookies. */
cookies?: BrowserCookiesConfig;
/** i18n: default language + supported locales (strings live in app/locales/*.json). */ /** i18n: default language + supported locales (strings live in app/locales/*.json). */
i18n?: { i18n?: {
default?: string; default?: string;
@@ -309,6 +311,7 @@ export interface AppConfig extends CompatibilityPolicy {
cookie?: { cookie?: {
name?: string; name?: string;
maxAge?: number; maxAge?: number;
domain?: string;
path?: string; path?: string;
sameSite?: "Strict" | "Lax" | "None"; sameSite?: "Strict" | "Lax" | "None";
secure?: boolean; secure?: boolean;
+5
View File
@@ -54,6 +54,10 @@ export type {
CustomThemePalette, CustomThemePalette,
ThemeToken, ThemeToken,
ThemeSemanticColor, ThemeSemanticColor,
BrowserCookieOptions,
BrowserCookiesConfig,
BrowserCookiePreference,
BrowserCookieApi,
} from "./theme.ts"; } from "./theme.ts";
export { export {
DEFAULT_THEMES, DEFAULT_THEMES,
@@ -71,6 +75,7 @@ export {
renderThemeRuntime, renderThemeRuntime,
defineThemeTokens, defineThemeTokens,
themeVar, themeVar,
resolveBrowserCookieOptions,
} from "./theme.ts"; } from "./theme.ts";
import type { Mode, StyleProcessContext, StylesConfig } from "./config.ts"; import type { Mode, StyleProcessContext, StylesConfig } from "./config.ts";
+85 -4
View File
@@ -111,6 +111,54 @@ export interface ThemeCookieConfig {
sameSite?: "Strict" | "Lax" | "None"; sameSite?: "Strict" | "Lax" | "None";
/** Force or disable Secure. When omitted it follows the current protocol. */ /** Force or disable Secure. When omitted it follows the current protocol. */
secure?: boolean; secure?: boolean;
/** Cookie lifetime in seconds. Defaults to one year. */
maxAge?: number;
}
export type BrowserCookieOptions = ThemeCookieConfig;
/** Shared browser-cookie policy with optional per-preference overrides. */
export interface BrowserCookiesConfig {
defaults?: BrowserCookieOptions;
theme?: BrowserCookieOptions;
accent?: BrowserCookieOptions;
language?: BrowserCookieOptions;
}
export type BrowserCookiePreference = "theme" | "accent" | "language";
export interface BrowserCookieApi {
readonly config: BrowserCookiesConfig;
get(name: string): string | null;
options(
preference?: BrowserCookiePreference | BrowserCookieOptions,
override?: BrowserCookieOptions,
): BrowserCookieOptions;
set(
name: string,
value: string,
preference?: BrowserCookiePreference | BrowserCookieOptions,
override?: BrowserCookieOptions,
): void;
remove(
name: string,
preference?: BrowserCookiePreference | BrowserCookieOptions,
override?: BrowserCookieOptions,
): void;
}
declare global {
interface Window {
wrnCookies: BrowserCookieApi;
}
}
export function resolveBrowserCookieOptions(
config: BrowserCookiesConfig | undefined,
preference: BrowserCookiePreference,
override?: BrowserCookieOptions,
): BrowserCookieOptions {
return { ...(config?.defaults ?? {}), ...(config?.[preference] ?? {}), ...(override ?? {}) };
} }
export interface ThemeConfig { export interface ThemeConfig {
@@ -131,6 +179,8 @@ export interface ResolvedTheme {
defaultAccent?: ThemePaletteName; defaultAccent?: ThemePaletteName;
accentNames: ThemePaletteName[]; accentNames: ThemePaletteName[];
accentCookie?: ThemeCookieConfig; accentCookie?: ThemeCookieConfig;
themeCookie?: ThemeCookieConfig;
cookies?: BrowserCookiesConfig;
} }
/** Cookies used by the SSR renderer and client runtime. */ /** Cookies used by the SSR renderer and client runtime. */
@@ -477,7 +527,10 @@ function themeScheme(tokens: ThemeTokens): ColorScheme {
} }
/** Merge the user's theme config over the built-in defaults. */ /** Merge the user's theme config over the built-in defaults. */
export function resolveThemeConfig(config?: ThemeConfig): ResolvedTheme { export function resolveThemeConfig(
config?: ThemeConfig,
cookies?: BrowserCookiesConfig,
): ResolvedTheme {
const palette = resolvePalette(config?.palette); const palette = resolvePalette(config?.palette);
const themes: Record<string, ThemeTokens> = {}; const themes: Record<string, ThemeTokens> = {};
const names = new Set<string>([ const names = new Set<string>([
@@ -508,7 +561,9 @@ export function resolveThemeConfig(config?: ThemeConfig): ResolvedTheme {
themes, themes,
defaultAccent, defaultAccent,
accentNames, accentNames,
accentCookie: config?.accent?.cookie, themeCookie: resolveBrowserCookieOptions(cookies, "theme"),
accentCookie: resolveBrowserCookieOptions(cookies, "accent", config?.accent?.cookie),
cookies,
}; };
} }
@@ -637,6 +692,8 @@ export function renderThemeRuntime(theme: ResolvedTheme): string {
const accentNames = JSON.stringify(theme.accentNames); const accentNames = JSON.stringify(theme.accentNames);
const defaultAccent = JSON.stringify(theme.defaultAccent ?? null); const defaultAccent = JSON.stringify(theme.defaultAccent ?? null);
const cssPrefix = JSON.stringify(THEME_CSS_PREFIX); const cssPrefix = JSON.stringify(THEME_CSS_PREFIX);
const cookieConfig = JSON.stringify(theme.cookies ?? {});
const themeCookie = JSON.stringify(theme.themeCookie ?? {});
const accentCookie = JSON.stringify(theme.accentCookie ?? {}); const accentCookie = JSON.stringify(theme.accentCookie ?? {});
return `(function(){ return `(function(){
@@ -647,6 +704,8 @@ export function renderThemeRuntime(theme: ResolvedTheme): string {
var DEFAULT_THEME=${JSON.stringify(theme.default)}; var DEFAULT_THEME=${JSON.stringify(theme.default)};
var DEFAULT_ACCENT=${defaultAccent}; var DEFAULT_ACCENT=${defaultAccent};
var THEME_CSS_PREFIX=${cssPrefix}; var THEME_CSS_PREFIX=${cssPrefix};
var COOKIE_CONFIG=${cookieConfig};
var THEME_COOKIE_OPTIONS=${themeCookie};
var ACCENT_COOKIE_OPTIONS=${accentCookie}; var ACCENT_COOKIE_OPTIONS=${accentCookie};
var MAX_AGE=31536000; var MAX_AGE=31536000;
var el=document.documentElement; var el=document.documentElement;
@@ -678,14 +737,36 @@ export function renderThemeRuntime(theme: ResolvedTheme): string {
} }
function writeCookie(name,value,options){ function writeCookie(name,value,options){
options=options||{};
var maxAge=options.maxAge===undefined?MAX_AGE:Math.max(0,Math.floor(options.maxAge));
document.cookie=encodeURIComponent(name)+"="+encodeURIComponent(value)+ document.cookie=encodeURIComponent(name)+"="+encodeURIComponent(value)+
";max-age="+MAX_AGE+cookieOptions(options); ";max-age="+maxAge+cookieOptions(options);
} }
function deleteCookie(name,options){ function deleteCookie(name,options){
document.cookie=encodeURIComponent(name)+"=;max-age=0"+cookieOptions(options); document.cookie=encodeURIComponent(name)+"=;max-age=0"+cookieOptions(options);
} }
function configuredCookieOptions(preference,override){
var resolved=Object.assign({},COOKIE_CONFIG.defaults||{});
if(typeof preference==="string")Object.assign(resolved,COOKIE_CONFIG[preference]||{});
else if(preference)Object.assign(resolved,preference);
if(override)Object.assign(resolved,override);
return resolved;
}
window.wrnCookies={
config:COOKIE_CONFIG,
get:readCookie,
options:configuredCookieOptions,
set:function(name,value,preference,override){
writeCookie(name,value,configuredCookieOptions(preference,override));
},
remove:function(name,preference,override){
deleteCookie(name,configuredCookieOptions(preference,override));
}
};
function syncStylesheet(themeName,accentName){ function syncStylesheet(themeName,accentName){
var link=document.querySelector("link[data-wrnexus-theme]"); var link=document.querySelector("link[data-wrnexus-theme]");
if(!link)return; if(!link)return;
@@ -720,7 +801,7 @@ export function renderThemeRuntime(theme: ResolvedTheme): string {
function setTheme(name){ function setTheme(name){
if(THEMES.indexOf(name)<0)return; if(THEMES.indexOf(name)<0)return;
el.setAttribute("data-theme",name); el.setAttribute("data-theme",name);
writeCookie(THEME_COOKIE,name); writeCookie(THEME_COOKIE,name,THEME_COOKIE_OPTIONS);
syncStylesheet(name,getAccent()); syncStylesheet(name,getAccent());
syncThemeButtons(document); syncThemeButtons(document);
window.dispatchEvent(new CustomEvent("wrnexus:theme-changed",{detail:{theme:name}})); window.dispatchEvent(new CustomEvent("wrnexus:theme-changed",{detail:{theme:name}}));
+22
View File
@@ -47,6 +47,28 @@ describe("theme accents", () => {
expect(runtime).not.toContain("style.setProperty"); expect(runtime).not.toContain("style.setProperty");
}); });
test("shared cookie config drives theme, accent, and the public browser helper", () => {
const theme = resolveThemeConfig(
{ palette: "rose" },
{
defaults: { domain: "example.com", path: "/", maxAge: 600, secure: true },
accent: { sameSite: "Strict" },
},
);
const runtime = renderThemeRuntime(theme);
expect(theme.themeCookie).toEqual({
domain: "example.com",
path: "/",
maxAge: 600,
secure: true,
});
expect(theme.accentCookie?.sameSite).toBe("Strict");
expect(runtime).toContain("window.wrnCookies={");
expect(runtime).toContain("writeCookie(THEME_COOKIE,name,THEME_COOKIE_OPTIONS)");
expect(runtime).toContain("configuredCookieOptions(preference,override)");
});
test("runtime scopes accent cookies to a configured parent domain", () => { test("runtime scopes accent cookies to a configured parent domain", () => {
const theme = resolveThemeConfig({ const theme = resolveThemeConfig({
palette: "rose", palette: "rose",