diff --git a/src/settings.ts b/src/settings.ts index 8a9dbd11..0afda0a9 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -107,6 +107,7 @@ interface Refs { se: HTMLUListElement; sd: HTMLUListElement; reset: HTMLButtonElement; + backup: HTMLInputElement; } const meta = compile(` @@ -134,6 +135,12 @@ const meta = compile(` +
+ +
+
@@ -244,6 +251,28 @@ const Settings = () => { } }; + refs.backup.onchange = async () => { + if (refs.backup.checked) { + const bytesInUse = await chrome.storage.local.getBytesInUse(); + const quotaBytes = chrome.storage.local.QUOTA_BYTES; + if (bytesInUse > quotaBytes) { + refs.feedback.textContent = `Warning: Settings exceed local storage limit. Current size: ${bytesInUse} bytes, Allowed size: ${quotaBytes} bytes, Total size over: ${bytesInUse - quotaBytes} bytes.`; + refs.backup.checked = false; + } else { + void chrome.storage.local.set({ + backup: true, + }); + void chrome.storage.sync.set({ + backup: true, + }); + } + } else { + void chrome.storage.local.set({ + backup: false, + }); + } + }; + // eslint-disable-next-line no-multi-assign refs.se.ondragover = refs.sd.ondragover = (event) => { event.preventDefault(); @@ -267,6 +296,7 @@ const Settings = () => { void updateTheme(themeName); refs.b.checked = !storage.b; + refs.backup.checked = !!storage.backup; updateOrder([orderEnabled, orderDisabled], true); return root; diff --git a/src/sw.ts b/src/sw.ts index 1504971f..e39bc52f 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -23,6 +23,13 @@ chrome.runtime.onInstalled.addListener(async () => { t: themes[settings.tn ?? 'auto'], }); + // Include backup and sync setting during installation or update + if (settings.backupSync) { + void chrome.storage.local.set({ + backupSync: settings.backupSync, + }); + } + // TODO: Open settings page on install? // if (details.reason === ('install' as chrome.runtime.OnInstalledReason.INSTALL)) { // void chrome.tabs.create({ diff --git a/src/utils.ts b/src/utils.ts index 3869d9f2..eb2a79b0 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -50,4 +50,8 @@ export const handleClick = (event: MouseEvent): false | void => { } s.focus(); + + if (storage.backupSync) { + void chrome.storage.sync.set(storage); + } }; diff --git a/test/unit/settings.test.ts b/test/unit/settings.test.ts index f6967e87..e6b75ca1 100644 --- a/test/unit/settings.test.ts +++ b/test/unit/settings.test.ts @@ -14,7 +14,6 @@ async function load() { if (input === 'themes.json') { return Promise.resolve(new Response(themes)); } - // eslint-disable-next-line @typescript-eslint/no-base-to-string throw new Error(`Unexpected fetch call: ${String(input)}`); }); // eslint-disable-next-line no-multi-assign @@ -62,6 +61,59 @@ test('gets stored user settings once on load', async () => { expect(spy).toHaveBeenCalledTimes(1); }); +test('displays warning and unchecks backup box if settings exceed local storage', async () => { + expect.assertions(4); + const spy = spyOn(chrome.storage.local, 'getBytesInUse').mockResolvedValueOnce(11000000); + await load(); + const backupCheckbox = document.querySelector('input[type="checkbox"][class="box"]'); + expect(backupCheckbox).toBeInstanceOf(HTMLInputElement); + if (backupCheckbox instanceof HTMLInputElement) { + backupCheckbox.click(); + expect(spy).toHaveBeenCalledTimes(1); + expect(backupCheckbox.checked).toBeFalse(); + const feedback = document.querySelector('div[feedback]'); + expect(feedback?.textContent).toContain('Warning: Settings exceed local storage limit.'); + } +}); + +test('syncs backup setting choice when enabled', async () => { + expect.assertions(2); + const spy = spyOn(chrome.storage.local, 'set'); + await load(); + const backupCheckbox = document.querySelector('input[type="checkbox"][class="box"]'); + expect(backupCheckbox).toBeInstanceOf(HTMLInputElement); + if (backupCheckbox instanceof HTMLInputElement) { + backupCheckbox.click(); + expect(spy).toHaveBeenCalledWith({ backup: true }); + } +}); + +test('syncs settings to chrome.storage.sync when backup and sync is enabled', async () => { + expect.assertions(2); + const spy = spyOn(chrome.storage.sync, 'set'); + await load(); + const backupCheckbox = document.querySelector('input[type="checkbox"][class="box"]'); + expect(backupCheckbox).toBeInstanceOf(HTMLInputElement); + if (backupCheckbox instanceof HTMLInputElement) { + backupCheckbox.click(); + expect(spy).toHaveBeenCalledWith({ backup: true }); + } +}); + +test('does not remove backed-up settings when disabling sync', async () => { + expect.assertions(2); + const spy = spyOn(chrome.storage.local, 'set'); + await load(); + const backupCheckbox = document.querySelector('input[type="checkbox"][class="box"]'); + expect(backupCheckbox).toBeInstanceOf(HTMLInputElement); + if (backupCheckbox instanceof HTMLInputElement) { + backupCheckbox.click(); // Enable sync + backupCheckbox.click(); // Disable sync + expect(spy).toHaveBeenCalledWith({ backup: true }); + expect(spy).toHaveBeenCalledWith({ backup: false }); + } +}); + const css = await Bun.file('dist/settings.css').text(); describe('CSS', () => { @@ -92,12 +144,12 @@ describe('CSS', () => { test('does not contain ":root"', () => { expect.assertions(1); - expect(css).not.toInclude(':root'); + expect(css).not toInclude(':root'); }); test('compiled AST is not empty', () => { expect.assertions(1); - expect(ast).not.toBeEmpty(); + expect(ast).not toBeEmpty(); }); test('does not have any rules with a ":root" selector', () => {