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
118 changes: 118 additions & 0 deletions packages/router/__tests__/data-context.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/**
* @openelement/router/data-context — render-scoped loader/action data stack.
*
* These are pure stack operations with no DOM dependency. We verify the
* push/pop/peek contract, action-data association to the top frame, nested
* render scoping, and the depth-warning guard.
*/

import { assert, assertEquals } from 'jsr:@std/assert@^1.0.0';
import {
__internal_popData,
__internal_pushActionData,
__internal_pushLoaderData,
useActionData,
useLoaderData,
} from '../src/data-context.ts';

// Reset the module-scoped stack between tests so ordering does not leak.
function drainStack(): void {
// Pop until empty — guarded by try since we cannot read length directly.
for (let i = 0; i < 16; i++) {
try {
__internal_popData();
} catch {
break;
}
}
}

Deno.test('data-context: useLoaderData reads the top frame loader data', () => {
drainStack();
__internal_pushLoaderData({ message: 'hello' });
try {
assertEquals(useLoaderData<{ message: string }>(), { message: 'hello' });
} finally {
__internal_popData();
}
});

Deno.test('data-context: useActionData is undefined before any action push', () => {
drainStack();
__internal_pushLoaderData({ id: 1 });
try {
assertEquals(useActionData(), undefined);
} finally {
__internal_popData();
}
});

Deno.test('data-context: pushActionData associates with the top frame', () => {
drainStack();
__internal_pushLoaderData({ id: 1 });
__internal_pushActionData({ ok: true });
try {
assertEquals(useLoaderData(), { id: 1 });
assertEquals(useActionData(), { ok: true });
} finally {
__internal_popData();
}
});

Deno.test('data-context: pop restores the previous frame', () => {
drainStack();
__internal_pushLoaderData('outer');
__internal_pushLoaderData('inner');
try {
assertEquals(useLoaderData(), 'inner');
} finally {
__internal_popData();
}
assertEquals(useLoaderData(), 'outer');
__internal_popData();
});

Deno.test('data-context: nested renders scope data independently', () => {
drainStack();
__internal_pushLoaderData('a');
__internal_pushLoaderData('b');
__internal_pushLoaderData('c');
try {
assertEquals(useLoaderData(), 'c');
} finally {
__internal_popData();
}
try {
assertEquals(useLoaderData(), 'b');
} finally {
__internal_popData();
}
assertEquals(useLoaderData(), 'a');
__internal_popData();
});

Deno.test('data-context: warns when stack depth exceeds 10', () => {
drainStack();
let warnCount = 0;
const originalWarn = console.warn;
console.warn = () => {
warnCount++;
};
try {
for (let i = 0; i < 12; i++) {
__internal_pushLoaderData(i);
}
// Depth reaches 11 and 12, each triggering a warning.
assert(warnCount >= 1, `expected a depth warning, got ${warnCount}`);
assertEquals(warnCount, 2);
} finally {
console.warn = originalWarn;
drainStack();
}
});

Deno.test('data-context: empty stack returns undefined for both hooks', () => {
drainStack();
assertEquals(useLoaderData(), undefined);
assertEquals(useActionData(), undefined);
});
83 changes: 83 additions & 0 deletions packages/router/__tests__/i18n.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* @openelement/router/i18n — locale-aware path normalization.
*
* Pure function, no DOM dependency. Verifies prefix detection, default-locale
* fallback, localized-path construction, and default-locale identity.
*/

import { assertEquals } from 'jsr:@std/assert@^1.0.0';
import { normalizeLocalePath } from '../src/i18n.ts';

Deno.test('i18n: strips a recognized locale prefix', () => {
const result = normalizeLocalePath('/en/about', {
locales: ['en', 'fr', 'de'],
defaultLocale: 'en',
});
assertEquals(result, {
locale: 'en',
path: '/about',
localizedPath: '/about',
isDefaultLocalePath: true,
});
});

Deno.test('i18n: prefixes a non-default locale path', () => {
const result = normalizeLocalePath('/about', {
locales: ['en', 'fr', 'de'],
defaultLocale: 'en',
});
assertEquals(result, {
locale: 'en',
path: '/about',
localizedPath: '/about',
isDefaultLocalePath: true,
});

const fr = normalizeLocalePath('/fr/about', {
locales: ['en', 'fr', 'de'],
defaultLocale: 'en',
});
assertEquals(fr.locale, 'fr');
assertEquals(fr.path, '/about');
assertEquals(fr.localizedPath, '/fr/about');
assertEquals(fr.isDefaultLocalePath, false);
});

Deno.test('i18n: falls back to default locale when no prefix', () => {
const result = normalizeLocalePath('products/widget', {
locales: ['en', 'ja'],
defaultLocale: 'ja',
});
assertEquals(result.locale, 'ja');
assertEquals(result.path, '/products/widget');
assertEquals(result.localizedPath, '/products/widget');
assertEquals(result.isDefaultLocalePath, true);
});

Deno.test('i18n: root path maps to locale + "/"', () => {
const result = normalizeLocalePath('/', {
locales: ['en', 'fr'],
defaultLocale: 'en',
});
assertEquals(result.locale, 'en');
assertEquals(result.path, '/');
assertEquals(result.localizedPath, '/');
});

Deno.test('i18n: uses first locale when defaultLocale not in list', () => {
const result = normalizeLocalePath('/', {
locales: ['fr', 'de'],
defaultLocale: 'en',
});
assertEquals(result.locale, 'fr');
assertEquals(result.isDefaultLocalePath, true);
});

Deno.test('i18n: empty locales list falls back to defaultLocale', () => {
const result = normalizeLocalePath('/de/foo', {
locales: [],
defaultLocale: 'en',
});
assertEquals(result.locale, 'en');
assertEquals(result.path, '/de/foo');
});
Loading