Skip to content

Commit 5d0c75a

Browse files
committed
🔨 Fix default options mutation and unnecessary address save
1 parent 70897c2 commit 5d0c75a

2 files changed

Lines changed: 66 additions & 9 deletions

File tree

‎src/composables/useOptionsData.js‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@ export function useOptionsData() {
1212
// all valid option values for selfMessages
1313
const selfMessagesOptions = ['none', 'sameAccount', 'anyAccount'];
1414

15+
// deep clone so mutating `options` never touches the shared defaultOptions singleton
16+
const cloneDefaultOptions = () => JSON.parse(JSON.stringify(defaultOptions));
17+
1518
// create options object with default values
16-
const options = ref(defaultOptions);
19+
const options = ref(cloneDefaultOptions());
1720

1821
// get all saved add-on settings
1922
const getSettings = async () => {
@@ -66,23 +69,21 @@ export function useOptionsData() {
6669
cacheSize.value = allEntriesSize - optionsSize;
6770
};
6871

69-
// add configured email address to list of addresses and save it
70-
const addAddress = async () => {
72+
// add configured email address to list of addresses
73+
const addAddress = () => {
7174
if (input.value.address) {
7275
let addresses = options.value.addresses ? options.value.addresses + ',' : '';
7376
addresses += input.value.address;
74-
await messenger.storage.local.set({ options: { addresses: addresses } });
7577
options.value.addresses = addresses;
7678
input.value.address = '';
7779
}
7880
};
7981

80-
// remove given email address from list of addresses and delete it
81-
const removeAddress = async (address) => {
82+
// remove given email address from list of addresses
83+
const removeAddress = (address) => {
8284
let addresses = options.value.addresses.replace(address, '');
8385
addresses = addresses.replace(/,,/g, ',');
8486
addresses = addresses.replace(/^,+|,+$/g, '');
85-
await messenger.storage.local.set({ options: { addresses: addresses } });
8687
options.value.addresses = addresses;
8788
};
8889

@@ -143,7 +144,7 @@ export function useOptionsData() {
143144
const resetOptions = async () => {
144145
// save options default values
145146
await messenger.storage.local.set({ options: defaultOptions });
146-
options.value = defaultOptions;
147+
options.value = cloneDefaultOptions();
147148
await getAccounts();
148149
};
149150

‎test/composables/useOptionsData.spec.js‎

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,72 @@
11
import { describe, expect, it, vi } from 'vitest';
22
import { useOptionsData } from '@/composables/useOptionsData.js';
3+
import { defaultOptions } from '@/definitions.js';
34

45
// the composable's internal deep watch on `options` fires (asynchronously, via Vue's
56
// reactivity flush) on every mutation made below and calls messenger.storage.local.set()
67
// plus setTheme(..., document.body, ...) - stub both globally for the whole file so the
78
// watcher's side effects don't throw, regardless of exactly when its microtask fires
89
// relative to a given test's completion
910
const fakeElement = () => ({ classList: { add: () => {}, remove: () => {}, contains: () => false } });
10-
vi.stubGlobal('messenger', { storage: { local: { set: vi.fn() } } });
11+
vi.stubGlobal('messenger', {
12+
storage: { local: { set: vi.fn() } },
13+
// only needed by resetOptions -> getAccounts
14+
runtime: { getBackgroundPage: async () => ({ messenger: { accounts: { list: async () => [] } } }) },
15+
});
1116
vi.stubGlobal('document', { body: fakeElement(), documentElement: fakeElement() });
1217
vi.stubGlobal('window', { matchMedia: () => ({ matches: false }) });
1318

19+
describe('options initial state (defaultOptions aliasing regression)', () => {
20+
it('is not the same object reference as the shared defaultOptions singleton', () => {
21+
const { options } = useOptionsData();
22+
expect(options.value).not.toBe(defaultOptions);
23+
});
24+
25+
it('mutating options does not mutate the shared defaultOptions singleton', () => {
26+
const { options } = useOptionsData();
27+
options.value.theme = 'dark';
28+
options.value.accountColors.abc = '#123456';
29+
expect(defaultOptions.theme).toBe('system');
30+
expect(defaultOptions.accountColors).toEqual({});
31+
});
32+
33+
it('resetOptions() re-clones defaultOptions rather than re-aliasing it', async () => {
34+
const { options, resetOptions } = useOptionsData();
35+
options.value.theme = 'dark';
36+
await resetOptions();
37+
expect(options.value).not.toBe(defaultOptions);
38+
options.value.debug = true;
39+
expect(defaultOptions.debug).toBe(false);
40+
});
41+
});
42+
43+
describe('addAddress / removeAddress (no redundant partial storage write)', () => {
44+
it('addAddress appends the given address and clears the input', () => {
45+
const { input, options, addAddress, addressList } = useOptionsData();
46+
options.value.addresses = '';
47+
input.value.address = 'a@example.com';
48+
addAddress();
49+
expect(options.value.addresses).toBe('a@example.com');
50+
expect(addressList.value).toEqual(['a@example.com']);
51+
expect(input.value.address).toBe('');
52+
});
53+
54+
it('addAddress appends to existing addresses with a comma separator', () => {
55+
const { input, options, addAddress } = useOptionsData();
56+
options.value.addresses = 'a@example.com';
57+
input.value.address = 'b@example.com';
58+
addAddress();
59+
expect(options.value.addresses).toBe('a@example.com,b@example.com');
60+
});
61+
62+
it('removeAddress removes only the given address and normalizes separators', () => {
63+
const { options, removeAddress, addressList } = useOptionsData();
64+
options.value.addresses = 'a@example.com,b@example.com,c@example.com';
65+
removeAddress('b@example.com');
66+
expect(addressList.value).toEqual(['a@example.com', 'c@example.com']);
67+
});
68+
});
69+
1470
describe('addressList', () => {
1571
it('is empty when addresses is unset', () => {
1672
const { options, addressList } = useOptionsData();

0 commit comments

Comments
 (0)