Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions _locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,10 @@
"message": "Error opening tab.",
"description": "Message shown when an error occurs when opening a tab."
},
"errorRefreshingHistoryManager": {
"message": "Error refreshing history manager.",
"description": "Message shown when an error occurs when refreshing the history manager."
},
"openSourceAttributions": {
"message": "Open Source Attributions",
"description": "Text for link to open source attributions."
Expand Down
9 changes: 4 additions & 5 deletions src/treetop/Bookmark.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<script lang="ts">
import { getContext } from 'svelte';
import type { SvelteDate } from 'svelte/reactivity';
import type { Writable } from 'svelte/store';
import lodashTruncate from 'lodash-es/truncate';

import type * as Treetop from './types';
Expand All @@ -17,8 +16,8 @@

const lastVisitTimeMap: Treetop.LastVisitTimeMap =
getContext('lastVisitTimeMap');
const truncate = getContext<Writable<boolean>>('truncate');
const tooltips = getContext<Writable<boolean>>('tooltips');
const truncate = getContext<() => boolean>('truncate');
const tooltips = getContext<() => boolean>('tooltips');
const clock = getContext<SvelteDate>('clock');

const lastVisitTime = $derived(lastVisitTimeMap.get(nodeId)!);
Expand All @@ -29,7 +28,7 @@
// Set name, truncating based on preference setting.
// Fall back to URL if title is blank.
const name = $derived(
$truncate
truncate()
? lodashTruncate(title || url, {
length: maxLength,
separator: ' ',
Expand All @@ -40,7 +39,7 @@
// Set tooltip if preference is enabled.
// Display title and URL on separate lines, truncating long URLs in the middle.
const tooltip = $derived(
$tooltips ? `${title}\n${truncateMiddle(url, maxLength)}` : undefined,
tooltips() ? `${title}\n${truncateMiddle(url, maxLength)}` : undefined,
);

// Number of milliseconds in a day
Expand Down
15 changes: 8 additions & 7 deletions src/treetop/Bookmark.svelte.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { SvelteDate, SvelteMap } from 'svelte/reactivity';
import { type Writable, writable } from 'svelte/store';
import { faker } from '@faker-js/faker';
import { render, screen } from '@testing-library/svelte';
import escapeRegExp from 'lodash-es/escapeRegExp';
Expand All @@ -11,8 +10,10 @@ import type * as Treetop from '@Treetop/treetop/types';
import ContextWrapper from '../../test/utils/ContextWrapper.svelte';

let lastVisitTimeMap: Treetop.LastVisitTimeMap;
let truncate: Writable<boolean>;
let tooltips: Writable<boolean>;
let currentTruncate: boolean;
const truncate = () => currentTruncate;
let currentTooltips: boolean;
const tooltips = () => currentTooltips;
let clock: SvelteDate;
let nodeId: string;
let title: string;
Expand Down Expand Up @@ -48,8 +49,8 @@ beforeEach(() => {
lastVisitTimeMap = new SvelteMap();
lastVisitTimeMap.set(nodeId, 0);

truncate = writable(false);
tooltips = writable(false);
currentTruncate = false;
currentTooltips = false;

clock = new SvelteDate();
});
Expand Down Expand Up @@ -81,7 +82,7 @@ it('uses URL as content when title is empty', () => {

describe('truncate option', () => {
beforeEach(() => {
truncate.set(true);
currentTruncate = true;
});

it('truncates long title', () => {
Expand Down Expand Up @@ -133,7 +134,7 @@ describe('truncate option', () => {

describe('tooltips option', () => {
beforeEach(() => {
tooltips.set(true);
currentTooltips = true;
});

it('title attribute contains title and URL', () => {
Expand Down
7 changes: 2 additions & 5 deletions src/treetop/Folder.svelte.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/* eslint no-irregular-whitespace: ["error", { "skipComments": true }] */

import { SvelteDate, SvelteMap, SvelteSet } from 'svelte/reactivity';
import { type Writable, writable } from 'svelte/store';
import { render, screen } from '@testing-library/svelte';
import type { MockInstance } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';
Expand All @@ -25,8 +24,8 @@ let nodeId: string;

// Bookmark component requirements
let lastVisitTimeMap: Treetop.LastVisitTimeMap;
let truncate: Writable<boolean>;
let tooltips: Writable<boolean>;
const truncate = () => false;
const tooltips = () => false;
let clock: SvelteDate;

let rootNode: Treetop.FolderNode;
Expand Down Expand Up @@ -115,8 +114,6 @@ beforeEach(() => {
lastVisitTimeMap = new SvelteMap();
currentFilterActive = false;
filterSet = new SvelteSet();
truncate = writable(false);
tooltips = writable(false);
clock = new SvelteDate();
});

Expand Down
113 changes: 63 additions & 50 deletions src/treetop/PreferencesManager.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { get, type Writable } from 'svelte/store';
import { faker } from '@faker-js/faker';
import EventEmitter from 'node:events';
import { beforeEach, describe, expect, it, vi } from 'vitest';
Expand All @@ -21,7 +20,7 @@ describe('constructor', () => {
});
});

describe('createStore', () => {
describe('createPreference', () => {
beforeEach(() => {
const addListener = vi.fn();
vi.spyOn(chrome.storage.onChanged, 'addListener').mockImplementation(
Expand All @@ -32,23 +31,23 @@ describe('createStore', () => {
});

it.each(['value', 2, false, true, ['value'], [2], [false], [true]])(
'creates and returns a store for value: %p',
'creates and returns a preference for value: %p',
(value: Treetop.PreferenceValue) => {
const name = faker.word.sample();
const store = preferencesManager.createStore(name, value);
const preference = preferencesManager.createPreference(name, value);

expect(get(store)).toBe(value);
expect(preference()).toBe(value);
},
);
});

describe('loadPreferences', () => {
let stringStore: Writable<Treetop.PreferenceValue>;
let numberStore: Writable<Treetop.PreferenceValue>;
let booleanStore: Writable<Treetop.PreferenceValue>;
let stringArrayStore: Writable<Treetop.PreferenceValue>;
let numberArrayStore: Writable<Treetop.PreferenceValue>;
let booleanArrayStore: Writable<Treetop.PreferenceValue>;
let stringPreference: () => Treetop.PreferenceValue;
let numberPreference: () => Treetop.PreferenceValue;
let booleanPreference: () => Treetop.PreferenceValue;
let stringArrayPreference: () => Treetop.PreferenceValue;
let numberArrayPreference: () => Treetop.PreferenceValue;
let booleanArrayPreference: () => Treetop.PreferenceValue;

beforeEach(() => {
const addListener = vi.fn();
Expand All @@ -57,17 +56,24 @@ describe('loadPreferences', () => {
);

preferencesManager = new PreferencesManager();
stringStore = preferencesManager.createStore('string', 'value');
numberStore = preferencesManager.createStore('number', 2);
booleanStore = preferencesManager.createStore('boolean', true);
stringArrayStore = preferencesManager.createStore('string_array', [
'value',
]);
numberArrayStore = preferencesManager.createStore('number_array', [2]);
booleanArrayStore = preferencesManager.createStore('boolean_array', [true]);
stringPreference = preferencesManager.createPreference('string', 'value');
numberPreference = preferencesManager.createPreference('number', 2);
booleanPreference = preferencesManager.createPreference('boolean', true);
stringArrayPreference = preferencesManager.createPreference(
'string_array',
['value'],
);
numberArrayPreference = preferencesManager.createPreference(
'number_array',
[2],
);
booleanArrayPreference = preferencesManager.createPreference(
'boolean_array',
[true],
);
});

it('loads preferences from storage and initializes stores', async () => {
it('loads preferences from storage and initializes preference state', async () => {
const values = {
string: 'value2',
number: 3,
Expand All @@ -83,15 +89,15 @@ describe('loadPreferences', () => {

await preferencesManager.loadPreferences();

expect(get(stringStore)).toBe('value2');
expect(get(numberStore)).toBe(3);
expect(get(booleanStore)).toBe(false);
expect(get(stringArrayStore)).toStrictEqual(['value2']);
expect(get(numberArrayStore)).toStrictEqual([3]);
expect(get(booleanArrayStore)).toStrictEqual([false]);
expect(stringPreference()).toBe('value2');
expect(numberPreference()).toBe(3);
expect(booleanPreference()).toBe(false);
expect(stringArrayPreference()).toStrictEqual(['value2']);
expect(numberArrayPreference()).toStrictEqual([3]);
expect(booleanArrayPreference()).toStrictEqual([false]);
});

it('ignores preferences from storage without a corresponding store', async () => {
it('ignores preferences from storage without a corresponding preference', async () => {
const values = { other: true };

vi.spyOn(chrome.storage.local, 'get').mockImplementation(
Expand All @@ -102,14 +108,14 @@ describe('loadPreferences', () => {
});
});

describe('handleStoreChanged', () => {
describe('handleStorageChanged', () => {
let emitter: EventEmitter;
let stringStore: Writable<Treetop.PreferenceValue>;
let numberStore: Writable<Treetop.PreferenceValue>;
let booleanStore: Writable<Treetop.PreferenceValue>;
let stringArrayStore: Writable<Treetop.PreferenceValue>;
let numberArrayStore: Writable<Treetop.PreferenceValue>;
let booleanArrayStore: Writable<Treetop.PreferenceValue>;
let stringPreference: () => Treetop.PreferenceValue;
let numberPreference: () => Treetop.PreferenceValue;
let booleanPreference: () => Treetop.PreferenceValue;
let stringArrayPreference: () => Treetop.PreferenceValue;
let numberArrayPreference: () => Treetop.PreferenceValue;
let booleanArrayPreference: () => Treetop.PreferenceValue;

beforeEach(() => {
emitter = new EventEmitter();
Expand All @@ -120,17 +126,24 @@ describe('handleStoreChanged', () => {
);

preferencesManager = new PreferencesManager();
stringStore = preferencesManager.createStore('string', 'value');
numberStore = preferencesManager.createStore('number', 3);
booleanStore = preferencesManager.createStore('boolean', true);
stringArrayStore = preferencesManager.createStore('string_array', [
'value',
]);
numberArrayStore = preferencesManager.createStore('number_array', [2]);
booleanArrayStore = preferencesManager.createStore('boolean_array', [true]);
stringPreference = preferencesManager.createPreference('string', 'value');
numberPreference = preferencesManager.createPreference('number', 3);
booleanPreference = preferencesManager.createPreference('boolean', true);
stringArrayPreference = preferencesManager.createPreference(
'string_array',
['value'],
);
numberArrayPreference = preferencesManager.createPreference(
'number_array',
[2],
);
booleanArrayPreference = preferencesManager.createPreference(
'boolean_array',
[true],
);
});

it('updates stores when store values change', () => {
it('updates preferences when storage values change', () => {
const changes = {
string: {
newValue: 'value2',
Expand All @@ -154,15 +167,15 @@ describe('handleStoreChanged', () => {

emitter.emit('onChanged', changes, 'local');

expect(get(stringStore)).toBe('value2');
expect(get(numberStore)).toBe(4);
expect(get(booleanStore)).toBe(false);
expect(get(stringArrayStore)).toStrictEqual(['value2']);
expect(get(numberArrayStore)).toStrictEqual([3]);
expect(get(booleanArrayStore)).toStrictEqual([false]);
expect(stringPreference()).toBe('value2');
expect(numberPreference()).toBe(4);
expect(booleanPreference()).toBe(false);
expect(stringArrayPreference()).toStrictEqual(['value2']);
expect(numberArrayPreference()).toStrictEqual([3]);
expect(booleanArrayPreference()).toStrictEqual([false]);
});

it('ignores store changes without a corresponding store', () => {
it('ignores storage changes without a corresponding preference', () => {
const changes = { other: { newValue: true } };

emitter.emit('onChanged', changes, 'local');
Expand Down
36 changes: 17 additions & 19 deletions src/treetop/PreferencesManager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type Writable, writable } from 'svelte/store';
import { SvelteMap } from 'svelte/reactivity';

import type * as Treetop from './types';

Expand All @@ -7,12 +7,12 @@ type StorageChangedCallback = Parameters<
>[0];

/**
* Class to create and manage updating preference stores.
* Class to create and manage updating preferences.
*/
export class PreferencesManager {
private readonly stores = new Map<
private readonly preferences = new SvelteMap<
string,
Writable<Treetop.PreferenceValue>
Treetop.PreferenceValue
>();

// Bound event handler
Expand All @@ -26,37 +26,36 @@ export class PreferencesManager {
}

/**
* Create and register a store.
* Create and register a preference.
*/
createStore(
createPreference(
name: string,
value: Treetop.PreferenceValue,
): Writable<Treetop.PreferenceValue> {
const store = writable(value);
this.stores.set(name, store);
return store;
): () => Treetop.PreferenceValue {
this.preferences.set(name, value);

return () => this.preferences.get(name)!;
}

/**
* Load preferences from storage and initialize stores.
* Load preferences from storage and initialize preference state.
*/
async loadPreferences(): Promise<void> {
// Get preferences from storage
const results = await chrome.storage.local.get();

// Initialize stores
// Initialize preference state
for (const key of Object.keys(results)) {
const value = results[key] as Treetop.PreferenceValue;

const store = this.stores.get(key);
if (store !== undefined) {
store.set(value);
if (this.preferences.has(key)) {
this.preferences.set(key, value);
}
}
}

/**
* Update preferences stores when storage values change.
* Update preference state when storage values change.
*/
private handleStorageChanged(
changes: Record<string, chrome.storage.StorageChange>,
Expand All @@ -65,9 +64,8 @@ export class PreferencesManager {
for (const key of Object.keys(changes)) {
const value = changes[key].newValue as Treetop.PreferenceValue;

const store = this.stores.get(key);
if (store !== undefined) {
store.set(value);
if (this.preferences.has(key)) {
this.preferences.set(key, value);
}
}
}
Expand Down
Loading