// The browser-local preferences: the three trigger switches and the popup // lifetime. Storage is foreign input: it can be absent, hold another version's // shape, or refuse to write, and none of that may stop the plugin — nor may // writing one preference ever clear another. import { afterEach, test } from 'node:test' import assert from 'node:assert/strict' import { STORAGE_KEY, readStoredDismissMs, readStoredKinds, writeStoredDismissMs, writeStoredKinds, } from '../../src/client/core/storage.js' import { TOAST_DURATION_MS } from '../../src/client/constants.js' /** Install a storage double, optionally one that throws. */ function useStorage(behaviour) { const data = new Map() globalThis.localStorage = { getItem: (key) => { if (behaviour === 'read-throws') throw new Error('denied') return data.has(key) ? data.get(key) : null }, setItem: (key, value) => { if (behaviour === 'write-throws') throw new Error('quota') data.set(key, String(value)) }, } return data } afterEach(() => { delete globalThis.localStorage }) test('everything is on, with the shipped popup lifetime, when this browser has no stored choice', () => { useStorage() assert.deepEqual(readStoredKinds(), { completion: true, approval: true, question: true }) assert.equal(readStoredDismissMs(), TOAST_DURATION_MS) }) test('without storage at all the defaults still apply', () => { assert.deepEqual(readStoredKinds(), { completion: true, approval: true, question: true }) assert.equal(readStoredDismissMs(), TOAST_DURATION_MS) assert.doesNotThrow(() => writeStoredKinds({ completion: false, approval: true, question: true })) assert.doesNotThrow(() => writeStoredDismissMs(0)) }) test('a stored switch survives a round trip, and unknown or broken values do not', () => { const data = useStorage() writeStoredKinds({ completion: false, approval: true, question: false }) assert.deepEqual(readStoredKinds(), { completion: false, approval: true, question: false }) data.set(STORAGE_KEY, '{ not json') assert.deepEqual(readStoredKinds(), { completion: true, approval: true, question: true }) data.set(STORAGE_KEY, JSON.stringify({ completion: 'yes', approval: false })) assert.deepEqual(readStoredKinds(), { completion: true, approval: false, question: true }) data.set(STORAGE_KEY, JSON.stringify({ somethingElse: false })) assert.deepEqual(readStoredKinds(), { completion: true, approval: true, question: true }) }) test('the popup lifetime survives a round trip, including "never"', () => { const data = useStorage() writeStoredDismissMs(30000) assert.equal(readStoredDismissMs(), 30000) writeStoredDismissMs(0) assert.equal(readStoredDismissMs(), 0, 'zero means the card waits for the user') // A value this version does not offer is ignored rather than trusted. data.set(STORAGE_KEY, JSON.stringify({ dismissAfterMs: 1234 })) assert.equal(readStoredDismissMs(), TOAST_DURATION_MS) data.set(STORAGE_KEY, JSON.stringify({ dismissAfterMs: -1 })) assert.equal(readStoredDismissMs(), TOAST_DURATION_MS) data.set(STORAGE_KEY, JSON.stringify({ dismissAfterMs: '6000' })) assert.equal(readStoredDismissMs(), TOAST_DURATION_MS) }) test('writing one preference keeps every other one', () => { useStorage() writeStoredKinds({ completion: false, approval: false, question: false }) writeStoredDismissMs(0) assert.deepEqual(readStoredKinds(), { completion: false, approval: false, question: false }) assert.equal(readStoredDismissMs(), 0) // …and the other way round: the switches do not reset the lifetime. writeStoredKinds({ completion: true, approval: true, question: true }) assert.equal(readStoredDismissMs(), 0) }) test('storage that refuses to be read or written never throws', () => { useStorage('read-throws') assert.deepEqual(readStoredKinds(), { completion: true, approval: true, question: true }) assert.equal(readStoredDismissMs(), TOAST_DURATION_MS) useStorage('write-throws') assert.doesNotThrow(() => writeStoredKinds({ completion: false, approval: false, question: false })) assert.doesNotThrow(() => writeStoredDismissMs(3000)) })