refactor: migrate legacy wire namespace to wrn
This commit is contained in:
+13
-13
@@ -21,7 +21,7 @@ other arrays intentionally replace earlier values. Cycles and missing/invalid
|
||||
entries fail with stable `WRN-CONFIG-LAYER-*` diagnostics. `wrnexus config
|
||||
--explain` lists every resolved layer source.
|
||||
|
||||
> Global CSS bundling, the `--wire-*` design-token theme system, and the `wrnexus.config.ts` app-config loader for WrNexus apps.
|
||||
> Global CSS bundling, the `--wrn-*` design-token theme system, and the `wrnexus.config.ts` app-config loader for WrNexus apps.
|
||||
|
||||
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
|
||||
|
||||
@@ -30,7 +30,7 @@ Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web fr
|
||||
This package owns three server-side concerns that shape every page a WrNexus app renders:
|
||||
|
||||
1. **Global stylesheet pipeline** — finds `app/styles/global.css` (or aggregates `app/styles/*.css`), bundles it with Bun's CSS bundler (which resolves `@import`, including from `node_modules`), and produces one stylesheet that is `<link>`ed into every page's `<head>`. Because it is a plain global sheet, it styles server-rendered markup and hydrated client islands identically. A custom `process` hook lets you swap in Tailwind / PostCSS / Sass.
|
||||
2. **Theme system** — design tokens exposed as CSS custom properties (`--wire-<key>`), with built-in `light`/`dark` sets, deep-merged user overrides, an SSR `<html data-theme>` render (no flash), and a tiny client runtime to toggle/persist the choice.
|
||||
2. **Theme system** — design tokens exposed as CSS custom properties (`--wrn-<key>`), with built-in `light`/`dark` sets, deep-merged user overrides, an SSR `<html data-theme>` render (no flash), and a tiny client runtime to toggle/persist the choice.
|
||||
3. **App config** — loads `wrnexus.config.ts` (the `AppConfig` type), applies named profile overrides, and loads the `.env` cascade.
|
||||
|
||||
It runs server-side / at build time. Reach for it when configuring an app, defining themes, or customising how global CSS is produced.
|
||||
@@ -111,7 +111,7 @@ interface StyleProcessContext {
|
||||
| Export | Type / Signature | Purpose |
|
||||
| -------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `DEFAULT_THEMES` | `Record<string, ThemeTokens>` | Built-in `light` and `dark` token maps. |
|
||||
| `THEME_COOKIE` | `"wire-theme"` | Cookie the resolved theme is read from / persisted to. |
|
||||
| `THEME_COOKIE` | `"wrn-theme"` | Cookie the resolved theme is read from / persisted to. |
|
||||
| `THEME_CSS_HREF` | `"/__wrnexus/theme.css"` | URL the generated theme stylesheet is served at. |
|
||||
| `THEME_JS_HREF` | `"/__wrnexus/theme.js"` | URL the client theme runtime is served at. |
|
||||
| `resolveThemeConfig` | `(config?: ThemeConfig) => ResolvedTheme` | Deep-merge the user's `theme` config over the defaults; pick the default theme (config's `default` if valid, else `dark`, else the first). |
|
||||
@@ -119,7 +119,7 @@ interface StyleProcessContext {
|
||||
| `renderThemeCss` | `(theme: ResolvedTheme) => string` | Generate the theme stylesheet: a `:root{…}` default plus one `[data-theme="<name>"]{…}` block per theme. |
|
||||
| `renderThemeRuntime` | `(theme: ResolvedTheme) => string` | Generate the client runtime (see below). |
|
||||
|
||||
Tokens are emitted as `--wire-<key>` custom properties, **except** the reserved key `color-scheme`, which is emitted as the native `color-scheme` CSS property so form controls and scrollbars match the theme.
|
||||
Tokens are emitted as `--wrn-<key>` custom properties, **except** the reserved key `color-scheme`, which is emitted as the native `color-scheme` CSS property so form controls and scrollbars match the theme.
|
||||
|
||||
`ThemeConfig` / `ThemeTokens` / `ResolvedTheme`:
|
||||
|
||||
@@ -164,7 +164,7 @@ theme: {
|
||||
}
|
||||
```
|
||||
|
||||
The client runtime (`renderThemeRuntime`) exposes `window.wireTheme` with `{ get, set, toggle, bind, themes }`, wires up any `[data-wire-theme-toggle]` and `[data-wire-theme-set]` elements on load, and persists the choice to the `wire-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.
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -252,17 +252,17 @@ In templates, consume tokens via the custom properties:
|
||||
|
||||
```css
|
||||
.card {
|
||||
background: var(--wire-color-surface);
|
||||
color: var(--wire-color-text);
|
||||
border: 1px solid var(--wire-color-border);
|
||||
border-radius: var(--wire-radius);
|
||||
box-shadow: var(--wire-shadow-1);
|
||||
background: var(--wrn-color-surface);
|
||||
color: var(--wrn-color-text);
|
||||
border: 1px solid var(--wrn-color-border);
|
||||
border-radius: var(--wrn-radius);
|
||||
box-shadow: var(--wrn-shadow-1);
|
||||
}
|
||||
```
|
||||
|
||||
```html
|
||||
<button data-wire-theme-toggle>Toggle theme</button>
|
||||
<button data-wire-theme-set="brand">Brand theme</button>
|
||||
<button data-wrn-theme-toggle>Toggle theme</button>
|
||||
<button data-wrn-theme-set="brand">Brand theme</button>
|
||||
```
|
||||
|
||||
## Requirements / Notes
|
||||
@@ -270,4 +270,4 @@ In templates, consume tokens via the custom properties:
|
||||
- **Bun-only.** `bundleCss` uses `Bun.build`'s CSS bundler for `@import` resolution, nesting, and minification. Node is not supported.
|
||||
- Config and env loading use `node:fs` / `node:path` / `node:url` and read from `process.env`.
|
||||
- Peer package: `@wrnexus/core` supplies the `SeoConfig` and `SecurityConfig` types referenced by `AppConfig`.
|
||||
- The bundled global stylesheet, the theme stylesheet (`THEME_CSS_HREF`), and the theme runtime (`THEME_JS_HREF`) are wired into pages by the framework's server; this package only produces their contents.
|
||||
- The bundled global stylesheet, the theme stylesheet (`THEME_CSS_HREF`), and the theme runtime (`THEME_JS_HREF`) are connected into pages by the framework's server; this package only produces their contents.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/styles",
|
||||
"version": "0.8.9",
|
||||
"version": "0.8.10",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -6,11 +6,11 @@ export interface CssTokenAudit {
|
||||
}
|
||||
|
||||
/** Audit framework design-token declarations and var() references. */
|
||||
export function auditWireTokens(css: string): CssTokenAudit {
|
||||
export function auditWrnTokens(css: string): CssTokenAudit {
|
||||
const declared = new Set<string>();
|
||||
const used = new Set<string>();
|
||||
for (const match of css.matchAll(/(--wire-[A-Za-z0-9_-]+)\s*:/g)) declared.add(match[1]!);
|
||||
for (const match of css.matchAll(/var\(\s*(--wire-[A-Za-z0-9_-]+)/g)) used.add(match[1]!);
|
||||
for (const match of css.matchAll(/(--wrn-[A-Za-z0-9_-]+)\s*:/g)) declared.add(match[1]!);
|
||||
for (const match of css.matchAll(/var\(\s*(--wrn-[A-Za-z0-9_-]+)/g)) used.add(match[1]!);
|
||||
return {
|
||||
declared: [...declared].sort(),
|
||||
used: [...used].sort(),
|
||||
|
||||
@@ -164,7 +164,7 @@ async function packageAwareStyleContext(ctx: StyleProcessContext): Promise<Style
|
||||
export type { Mode as StylesMode };
|
||||
|
||||
export {
|
||||
auditWireTokens,
|
||||
auditWrnTokens,
|
||||
normalizeStyleSources,
|
||||
tailwindSourceDirectives,
|
||||
contrast,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Theme system - design tokens that work SSR and client-side.
|
||||
*
|
||||
* Tokens are plain CSS custom properties (`--wire-<key>`) so they cascade and
|
||||
* Tokens are plain CSS custom properties (`--wrn-<key>`) so they cascade and
|
||||
* can be overridden by user CSS. Each theme is a flat token map; the framework
|
||||
* ships default `light`/`dark` sets and the user's config deep-merges over them.
|
||||
*
|
||||
@@ -58,7 +58,7 @@ export function defineThemeTokens<T extends ThemeTokens>(tokens: T): T {
|
||||
}
|
||||
|
||||
export function themeVar(token: ThemeToken, fallback?: string): string {
|
||||
return `var(--wire-${token}${fallback ? `, ${fallback}` : ""})`;
|
||||
return `var(--wrn-${token}${fallback ? `, ${fallback}` : ""})`;
|
||||
}
|
||||
|
||||
export const THEME_PALETTE_NAMES = [
|
||||
@@ -90,7 +90,7 @@ export interface CustomThemePalette {
|
||||
|
||||
export interface ThemeAccentConfig {
|
||||
/**
|
||||
* Accent used when no `wire-accent` cookie is present.
|
||||
* Accent used when no `wrn-accent` cookie is present.
|
||||
*
|
||||
* - Omitted: use the named `palette`, or `blue` when no palette is configured.
|
||||
* - `false`: keep the configured base palette until the user explicitly picks an accent.
|
||||
@@ -105,7 +105,7 @@ export interface ThemeConfig {
|
||||
palette?: ThemePaletteName | CustomThemePalette;
|
||||
/** Runtime accent/palette switcher configuration. */
|
||||
accent?: ThemeAccentConfig;
|
||||
/** Name of the theme used when no `wire-theme` cookie is present. */
|
||||
/** Name of the theme used when no `wrn-theme` cookie is present. */
|
||||
default?: string;
|
||||
/** Named token maps. Deep-merged over the framework's built-in light/dark. */
|
||||
themes?: Record<string, ThemeTokens>;
|
||||
@@ -120,8 +120,8 @@ export interface ResolvedTheme {
|
||||
}
|
||||
|
||||
/** Cookies used by the SSR renderer and client runtime. */
|
||||
export const THEME_COOKIE = "wire-theme";
|
||||
export const ACCENT_COOKIE = "wire-accent";
|
||||
export const THEME_COOKIE = "wrn-theme";
|
||||
export const ACCENT_COOKIE = "wrn-accent";
|
||||
export const THEME_CSS_HREF = "/__wrnexus/theme.css";
|
||||
export const THEME_CSS_PREFIX = "/__wrnexus/theme/";
|
||||
export const THEME_JS_HREF = "/__wrnexus/theme.js";
|
||||
@@ -248,7 +248,7 @@ function semanticColorTokens(
|
||||
[`color-${name}-text`]: dark ? colorMix(color, 64, "white") : colorMix(color, 82, "black"),
|
||||
/*
|
||||
* hover and contrast are generated for every semantic colour, not just
|
||||
* primary and secondary. Components referenced --wire-color-danger-hover
|
||||
* primary and secondary. Components referenced --wrn-color-danger-hover
|
||||
* and the rest for a long time with nothing defining them, so those states
|
||||
* simply did not paint.
|
||||
*
|
||||
@@ -517,7 +517,7 @@ export function resolveAccentName(
|
||||
function tokensToDeclarations(tokens: ThemeTokens): string {
|
||||
return Object.entries(tokens)
|
||||
.map(([key, value]) =>
|
||||
key === "color-scheme" ? `color-scheme:${value};` : `--wire-${key}:${value};`,
|
||||
key === "color-scheme" ? `color-scheme:${value};` : `--wrn-${key}:${value};`,
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
@@ -528,12 +528,12 @@ function selectorValue(value: string): string {
|
||||
|
||||
function globalThemeCss(): string {
|
||||
return (
|
||||
'\nhtml,body{font-family:var(--wire-font-sans,"Plus Jakarta Sans",ui-sans-serif,system-ui,sans-serif);}\n' +
|
||||
":root{--wire-radius-sm:0.55rem;--wire-radius-md:0.9rem;--wire-radius-lg:1.35rem;" +
|
||||
"--wire-shadow-1:0 1px 2px color-mix(in srgb, black 22%, transparent)," +
|
||||
'\nhtml,body{font-family:var(--wrn-font-sans,"Plus Jakarta Sans",ui-sans-serif,system-ui,sans-serif);}\n' +
|
||||
":root{--wrn-radius-sm:0.55rem;--wrn-radius-md:0.9rem;--wrn-radius-lg:1.35rem;" +
|
||||
"--wrn-shadow-1:0 1px 2px color-mix(in srgb, black 22%, transparent)," +
|
||||
"0 10px 30px color-mix(in srgb, black 14%, transparent);" +
|
||||
"--wire-shadow-2:0 10px 30px color-mix(in srgb, black 18%, transparent);" +
|
||||
"--wire-shadow-3:0 24px 64px color-mix(in srgb, black 24%, transparent);}\n" +
|
||||
"--wrn-shadow-2:0 10px 30px color-mix(in srgb, black 18%, transparent);" +
|
||||
"--wrn-shadow-3:0 24px 64px color-mix(in srgb, black 24%, transparent);}\n" +
|
||||
"wrn-slot{display:contents;}\n" +
|
||||
"[data-for]{display:none !important;}\n"
|
||||
);
|
||||
@@ -610,8 +610,8 @@ export function renderThemeCss(theme: ResolvedTheme): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the client theme runtime. It exposes `window.wireTheme` and
|
||||
* `window.wireAccent`, and binds theme/accent controls.
|
||||
* Generate the client theme runtime. It exposes `window.wrnTheme` and
|
||||
* `window.wrnAccent`, and binds theme/accent controls.
|
||||
*
|
||||
* The runtime changes only data attributes and cookies. It never writes inline
|
||||
* CSS variables and never uses localStorage, so CSS and SSR remain the single
|
||||
@@ -676,15 +676,15 @@ export function renderThemeRuntime(theme: ResolvedTheme): string {
|
||||
|
||||
function syncThemeButtons(root){
|
||||
var current=getTheme();
|
||||
(root||document).querySelectorAll("[data-wire-theme-set]").forEach(function(node){
|
||||
node.setAttribute("aria-pressed",String(node.getAttribute("data-wire-theme-set")===current));
|
||||
(root||document).querySelectorAll("[data-wrn-theme-set]").forEach(function(node){
|
||||
node.setAttribute("aria-pressed",String(node.getAttribute("data-wrn-theme-set")===current));
|
||||
});
|
||||
}
|
||||
|
||||
function syncAccentButtons(root){
|
||||
var current=getAccent();
|
||||
(root||document).querySelectorAll("[data-wire-accent-set]").forEach(function(node){
|
||||
node.setAttribute("aria-pressed",String(node.getAttribute("data-wire-accent-set")===current));
|
||||
(root||document).querySelectorAll("[data-wrn-accent-set]").forEach(function(node){
|
||||
node.setAttribute("aria-pressed",String(node.getAttribute("data-wrn-accent-set")===current));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -722,31 +722,31 @@ export function renderThemeRuntime(theme: ResolvedTheme): string {
|
||||
function bind(root){
|
||||
var target=root||document;
|
||||
|
||||
target.querySelectorAll("[data-wire-theme-toggle]").forEach(function(node){
|
||||
if(node.__wireThemeBound)return;
|
||||
node.__wireThemeBound=1;
|
||||
target.querySelectorAll("[data-wrn-theme-toggle]").forEach(function(node){
|
||||
if(node.__wrnThemeBound)return;
|
||||
node.__wrnThemeBound=1;
|
||||
node.addEventListener("click",toggleTheme);
|
||||
});
|
||||
|
||||
target.querySelectorAll("[data-wire-theme-set]").forEach(function(node){
|
||||
if(node.__wireThemeBound)return;
|
||||
node.__wireThemeBound=1;
|
||||
target.querySelectorAll("[data-wrn-theme-set]").forEach(function(node){
|
||||
if(node.__wrnThemeBound)return;
|
||||
node.__wrnThemeBound=1;
|
||||
node.addEventListener("click",function(){
|
||||
setTheme(node.getAttribute("data-wire-theme-set"));
|
||||
setTheme(node.getAttribute("data-wrn-theme-set"));
|
||||
});
|
||||
});
|
||||
|
||||
target.querySelectorAll("[data-wire-accent-set]").forEach(function(node){
|
||||
if(node.__wireAccentBound)return;
|
||||
node.__wireAccentBound=1;
|
||||
target.querySelectorAll("[data-wrn-accent-set]").forEach(function(node){
|
||||
if(node.__wrnAccentBound)return;
|
||||
node.__wrnAccentBound=1;
|
||||
node.addEventListener("click",function(){
|
||||
setAccent(node.getAttribute("data-wire-accent-set"));
|
||||
setAccent(node.getAttribute("data-wrn-accent-set"));
|
||||
});
|
||||
});
|
||||
|
||||
target.querySelectorAll("[data-wire-accent-clear]").forEach(function(node){
|
||||
if(node.__wireAccentBound)return;
|
||||
node.__wireAccentBound=1;
|
||||
target.querySelectorAll("[data-wrn-accent-clear]").forEach(function(node){
|
||||
if(node.__wrnAccentBound)return;
|
||||
node.__wrnAccentBound=1;
|
||||
node.addEventListener("click",clearAccent);
|
||||
});
|
||||
|
||||
@@ -761,7 +761,7 @@ export function renderThemeRuntime(theme: ResolvedTheme): string {
|
||||
var initialAccent=getAccent();
|
||||
if(initialAccent&&!el.hasAttribute("data-accent"))el.setAttribute("data-accent",initialAccent);
|
||||
|
||||
window.wireTheme={
|
||||
window.wrnTheme={
|
||||
get:getTheme,
|
||||
set:setTheme,
|
||||
toggle:toggleTheme,
|
||||
@@ -769,7 +769,7 @@ export function renderThemeRuntime(theme: ResolvedTheme): string {
|
||||
themes:THEMES.slice()
|
||||
};
|
||||
|
||||
window.wireAccent={
|
||||
window.wrnAccent={
|
||||
get:getAccent,
|
||||
set:setAccent,
|
||||
clear:clearAccent,
|
||||
|
||||
@@ -57,7 +57,7 @@ test("resolveProfile: explicit > WRNEXUS_PROFILE > mode default", () => {
|
||||
});
|
||||
|
||||
test("loadAppConfig deep-merges the active profile and strips `profiles`", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "wire-cfg-"));
|
||||
const dir = mkdtempSync(join(tmpdir(), "wrn-cfg-"));
|
||||
writeFileSync(
|
||||
join(dir, "wrnexus.config.mjs"),
|
||||
`export default {
|
||||
@@ -157,7 +157,7 @@ test("config layers resolve packages and reject dependency cycles", async () =>
|
||||
});
|
||||
|
||||
test("loadEnv layers .env files by precedence; real env always wins", () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "wire-env-"));
|
||||
const dir = mkdtempSync(join(tmpdir(), "wrn-env-"));
|
||||
writeFileSync(join(dir, ".env"), 'BASE=1\nSHARED=base\n# a comment\nQUOTED="hi there"\n');
|
||||
writeFileSync(join(dir, ".env.prod"), "SHARED=prod\nPROD_ONLY=yes\nREALVAR=fromfile\n");
|
||||
writeFileSync(join(dir, ".env.prod.local"), "SHARED=local\n");
|
||||
@@ -177,7 +177,7 @@ test("loadEnv layers .env files by precedence; real env always wins", () => {
|
||||
});
|
||||
|
||||
test("loadAppConfig loads env before evaluating the config module", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "wire-config-env-"));
|
||||
const dir = mkdtempSync(join(tmpdir(), "wrn-config-env-"));
|
||||
writeFileSync(join(dir, ".env"), "DATABASE_URL=postgres://pms-from-env\n");
|
||||
writeFileSync(
|
||||
join(dir, "wrnexus.config.mjs"),
|
||||
|
||||
@@ -31,11 +31,11 @@ describe("theme accents", () => {
|
||||
|
||||
expect(css).toContain('[data-theme="light"][data-accent="emerald"]');
|
||||
expect(css).toContain('[data-theme="dark"][data-accent="emerald"]');
|
||||
expect(css).toContain("--wire-color-primary-soft:");
|
||||
expect(css).toContain("--wire-color-primary-muted:");
|
||||
expect(css).toContain("--wire-color-primary-text:");
|
||||
expect(css).toContain("--wire-color-on-primary:");
|
||||
expect(css).toContain("--wire-color-secondary-soft:");
|
||||
expect(css).toContain("--wrn-color-primary-soft:");
|
||||
expect(css).toContain("--wrn-color-primary-muted:");
|
||||
expect(css).toContain("--wrn-color-primary-text:");
|
||||
expect(css).toContain("--wrn-color-on-primary:");
|
||||
expect(css).toContain("--wrn-color-secondary-soft:");
|
||||
});
|
||||
|
||||
test("runtime persists accents with cookies and never writes inline tokens", () => {
|
||||
|
||||
@@ -7,6 +7,6 @@ test("typed theme token helpers preserve custom tokens and emit CSS variables",
|
||||
"space-product-card": "1rem",
|
||||
});
|
||||
expect(tokens["space-product-card"]).toBe("1rem");
|
||||
expect(themeVar("color-primary")).toBe("var(--wire-color-primary)");
|
||||
expect(themeVar("color-text", "#111")).toBe("var(--wire-color-text, #111)");
|
||||
expect(themeVar("color-primary")).toBe("var(--wrn-color-primary)");
|
||||
expect(themeVar("color-text", "#111")).toBe("var(--wrn-color-text, #111)");
|
||||
});
|
||||
|
||||
@@ -74,7 +74,7 @@ test("resolveThemeName validates against configured names", () => {
|
||||
expect(resolveThemeName(undefined, t)).toBe(t.default);
|
||||
});
|
||||
|
||||
test("renderThemeCss emits :root + per-theme blocks and --wire-* vars", () => {
|
||||
test("renderThemeCss emits :root + per-theme blocks and --wrn-* vars", () => {
|
||||
const t = resolveThemeConfig({
|
||||
default: "dark",
|
||||
themes: { dark: { "color-primary": "#6c8cff" } },
|
||||
@@ -82,9 +82,9 @@ test("renderThemeCss emits :root + per-theme blocks and --wire-* vars", () => {
|
||||
const css = renderThemeCss(t);
|
||||
expect(css).toContain(":root{");
|
||||
expect(css).toContain('[data-theme="dark"]{');
|
||||
expect(css).toContain("--wire-color-primary:#6c8cff");
|
||||
expect(css).toContain("--wrn-color-primary:#6c8cff");
|
||||
expect(css).toContain("color-scheme:dark"); // reserved token → native property
|
||||
expect(css).toContain("font-family:var(--wire-font-sans");
|
||||
expect(css).toContain("font-family:var(--wrn-font-sans");
|
||||
});
|
||||
|
||||
test("active theme CSS contains only the selected theme and accent", () => {
|
||||
@@ -101,7 +101,7 @@ test("active theme CSS contains only the selected theme and accent", () => {
|
||||
test("renderThemeRuntime bakes the theme names for cycling", () => {
|
||||
const t = resolveThemeConfig();
|
||||
const js = renderThemeRuntime(t);
|
||||
expect(js).toContain("data-wire-theme-toggle");
|
||||
expect(js).toContain("data-wrn-theme-toggle");
|
||||
expect(js).toContain(JSON.stringify(t.names));
|
||||
expect(js).toContain('addEventListener("wrnexus:navigated"');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user