diff --git a/package.json b/package.json index 6ec1d3f..f9d9706 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "A simple CSV parser - BountyPay workflow demo", "main": "src/parser.js", "scripts": { - "test": "node test/parser.test.js" + "test": "node test/parser.test.js && node test/settings-theme-preference.test.js" }, "license": "MIT" } diff --git a/src/settings-theme-preference.js b/src/settings-theme-preference.js new file mode 100644 index 0000000..7e1e67e --- /dev/null +++ b/src/settings-theme-preference.js @@ -0,0 +1,85 @@ +const SETTINGS_THEME_KEY = 'ai-agent-pay-demo.settings.theme'; +const VALID_THEMES = ['light', 'dark']; + +function isValidTheme(theme) { + return VALID_THEMES.includes(theme); +} + +function getStoredTheme(storage) { + try { + const value = storage && typeof storage.getItem === 'function' + ? storage.getItem(SETTINGS_THEME_KEY) + : null; + return isValidTheme(value) ? value : null; + } catch (_error) { + return null; + } +} + +function saveTheme(storage, theme) { + if (!isValidTheme(theme)) { + throw new Error(`Unsupported theme: ${theme}`); + } + + try { + if (storage && typeof storage.setItem === 'function') { + storage.setItem(SETTINGS_THEME_KEY, theme); + } + } catch (_error) { + // Storage can fail in private browsing; the in-memory state still updates. + } + + return theme; +} + +function applySettingsTheme(root, theme) { + const normalized = isValidTheme(theme) ? theme : 'light'; + if (root) { + root.dataset.settingsTheme = normalized; + root.classList.remove('settings-theme-light', 'settings-theme-dark'); + root.classList.add(`settings-theme-${normalized}`); + } + return normalized; +} + +function createSettingsThemePreference({ storage, root, defaultTheme = 'light' } = {}) { + let currentTheme = getStoredTheme(storage) || (isValidTheme(defaultTheme) ? defaultTheme : 'light'); + + function setTheme(theme) { + currentTheme = saveTheme(storage, theme); + applySettingsTheme(root, currentTheme); + return getSettingsState(); + } + + function getSettingsState() { + return { + field: 'theme', + value: currentTheme, + options: VALID_THEMES.map((theme) => ({ + value: theme, + label: theme === 'dark' ? 'Dark mode' : 'Light mode', + selected: theme === currentTheme, + })), + }; + } + + return { + initialize() { + applySettingsTheme(root, currentTheme); + return getSettingsState(); + }, + getTheme() { + return currentTheme; + }, + getSettingsState, + setTheme, + }; +} + +module.exports = { + SETTINGS_THEME_KEY, + applySettingsTheme, + createSettingsThemePreference, + getStoredTheme, + saveTheme, +}; diff --git a/test/settings-theme-preference.test.js b/test/settings-theme-preference.test.js new file mode 100644 index 0000000..2753d9c --- /dev/null +++ b/test/settings-theme-preference.test.js @@ -0,0 +1,105 @@ +const { + SETTINGS_THEME_KEY, + applySettingsTheme, + createSettingsThemePreference, + getStoredTheme, + saveTheme, +} = require('../src/settings-theme-preference'); + +let passed = 0; +let failed = 0; + +function assert(name, actual, expected) { + const a = JSON.stringify(actual); + const e = JSON.stringify(expected); + if (a === e) { + console.log(` āœ… ${name}`); + passed++; + } else { + console.log(` āŒ ${name}`); + console.log(` Expected: ${e}`); + console.log(` Actual: ${a}`); + failed++; + } +} + +function createStorage(initial = {}) { + const data = { ...initial }; + return { + getItem(key) { + return Object.prototype.hasOwnProperty.call(data, key) ? data[key] : null; + }, + setItem(key, value) { + data[key] = value; + }, + snapshot() { + return { ...data }; + }, + }; +} + +function createRoot() { + const classes = new Set(); + return { + dataset: {}, + classList: { + add(name) { + classes.add(name); + }, + remove(...names) { + names.forEach((name) => classes.delete(name)); + }, + snapshot() { + return Array.from(classes).sort(); + }, + }, + }; +} + +console.log('\nāš™ļø Settings Theme Preference Tests\n'); + +const storage = createStorage({ [SETTINGS_THEME_KEY]: 'dark' }); +assert('reads stored dark theme', getStoredTheme(storage), 'dark'); +assert('ignores invalid stored theme', getStoredTheme(createStorage({ [SETTINGS_THEME_KEY]: 'sepia' })), null); + +const root = createRoot(); +assert('applies settings theme', applySettingsTheme(root, 'dark'), 'dark'); +assert('sets data attribute', root.dataset.settingsTheme, 'dark'); +assert('sets settings theme class', root.classList.snapshot(), ['settings-theme-dark']); + +const preferenceStorage = createStorage(); +const preference = createSettingsThemePreference({ + storage: preferenceStorage, + root: createRoot(), +}); + +assert('initializes light settings state', preference.initialize(), { + field: 'theme', + value: 'light', + options: [ + { value: 'light', label: 'Light mode', selected: true }, + { value: 'dark', label: 'Dark mode', selected: false }, + ], +}); + +assert('sets dark settings state', preference.setTheme('dark'), { + field: 'theme', + value: 'dark', + options: [ + { value: 'light', label: 'Light mode', selected: false }, + { value: 'dark', label: 'Dark mode', selected: true }, + ], +}); + +assert('persists settings theme', preferenceStorage.snapshot()[SETTINGS_THEME_KEY], 'dark'); + +let unsupportedThemeRejected = false; +try { + saveTheme(createStorage(), 'blue'); +} catch (_error) { + unsupportedThemeRejected = true; +} +assert('rejects unsupported theme values', unsupportedThemeRejected, true); + +console.log(`\nšŸ“Š Results: ${passed} passed, ${failed} failed\n`); +process.exit(failed > 0 ? 1 : 0);