import assert from 'node:assert/strict'; import { readFile, access } from 'node:fs/promises'; import { test } from 'node:test'; import { loadTypescript } from './support/load-typescript.mjs'; const moduleURL = new URL('../src/state/PetName.ts', import.meta.url); assert.equal(await access(moduleURL).then(() => true, () => false), true, 'application-wide pet name state must exist'); const { normalizePetName, petNameForms, createPetNameStore, PET_NAME_STORAGE_KEY } = await loadTypescript(moduleURL); test('friendly Korean name forms avoid duplicated 이 and match natural particles', () => { for (const [raw, display, object, subject] of [ ['', '멍멍이', '멍멍이를', '멍멍이가'], ['호롱', '호롱이', '호롱이를', '호롱이가'], ['초코', '초코', '초코를', '초코가'], ['보리', '보리', '보리를', '보리가'], ['호롱이', '호롱이', '호롱이를', '호롱이가'], ]) assert.deepEqual(petNameForms(raw), { display, object, subject, possessive: `${display}의` }); }); test('name normalization trims, composes Korean, bounds Unicode safely, preserves literal text', () => { assert.equal(normalizePetName(' 호롱 '), '호롱'); assert.equal(normalizePetName(' \n\t '), ''); assert.equal(normalizePetName('호롱'), '호롱'); assert.equal(normalizePetName('🐕'.repeat(25)), '🐕'.repeat(20)); assert.equal(normalizePetName(''), ''); }); test('one store publishes normalized state, persists reloads and unsubscribe stops updates', () => { const values = new Map(), storage = { getItem: key => values.get(key) ?? null, setItem: (key, value) => values.set(key, value) }; const store = createPetNameStore(() => storage), updates = [], signal = new AbortController(); store.subscribe(() => updates.push(store.name), signal.signal); store.setName(' 호롱 '); store.setName('호롱'); assert.deepEqual(updates, ['호롱']); assert.equal(values.get(PET_NAME_STORAGE_KEY), '호롱'); assert.equal(createPetNameStore(() => storage).name, '호롱'); store.setName(''); assert.equal(createPetNameStore(() => storage).name, ''); signal.abort(); store.setName('초코'); assert.deepEqual(updates, ['호롱', '']); }); test('storage getter, read and write failures never prevent session edits', () => { for (const getStorage of [() => undefined, () => { throw Error('blocked'); }, () => ({ getItem() { throw Error('blocked'); }, setItem() { throw Error('full'); } }), () => ({ getItem: () => '호롱', setItem() { throw Error('full'); } })]) { const store = createPetNameStore(getStorage); store.setName('보리'); assert.equal(store.name, '보리'); assert.equal(store.storageUnavailable, true); } }); test('removed settings surfaces are outside settings and inert, landing sample/retry stay available', async () => { const html = await readFile(new URL('../index.html', import.meta.url), 'utf8'); const settings = html.slice(html.indexOf('', html.indexOf(']*hidden[^>]*inert/); for (const id of ['entry-samples', 'sample-dialog', 'flow-retry', 'flow-flat']) assert.match(html, new RegExp(`id="${id}"`)); for (const id of ['entry-pet-name', 'settings-pet-name']) assert.match(html, new RegExp(`id="${id}"[^>]*autocomplete="off"`)); });