mirror of
https://github.com/koloml/furbooru-tagging-assistant.git
synced 2025-12-23 23:02:58 +00:00
Merge remote-tracking branch 'refs/remotes/origin/release/0.4.3' into feature/workaronud-for-opening-in-new-tab
This commit is contained in:
18
package-lock.json
generated
18
package-lock.json
generated
@@ -17,6 +17,7 @@
|
||||
"@sveltejs/kit": "^2.17.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||
"@types/chrome": "^0.0.304",
|
||||
"@types/node": "^22.13.5",
|
||||
"@vitest/coverage-v8": "^3.0.6",
|
||||
"cheerio": "^1.0.0",
|
||||
"jsdom": "^26.0.0",
|
||||
@@ -814,6 +815,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.13.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.5.tgz",
|
||||
"integrity": "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/coverage-v8": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.6.tgz",
|
||||
@@ -2972,6 +2983,13 @@
|
||||
"node": ">=18.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
||||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.1.0.tgz",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"@sveltejs/kit": "^2.17.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||
"@types/chrome": "^0.0.304",
|
||||
"@types/node": "^22.13.5",
|
||||
"@vitest/coverage-v8": "^3.0.6",
|
||||
"cheerio": "^1.0.0",
|
||||
"jsdom": "^26.0.0",
|
||||
|
||||
3
src/app.d.ts
vendored
3
src/app.d.ts
vendored
@@ -4,6 +4,9 @@ import MaintenanceProfile from "$entities/MaintenanceProfile";
|
||||
import type TagGroup from "$entities/TagGroup";
|
||||
|
||||
declare global {
|
||||
// Helper type to not deal with differences between the setTimeout of @types/node and usual web browser's type.
|
||||
type Timeout = ReturnType<typeof setTimeout>;
|
||||
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { initializeSiteHeader } from "$lib/components/SiteHeaderWrapper";
|
||||
|
||||
const siteHeader = document.querySelector('.header');
|
||||
const siteHeader = document.querySelector<HTMLElement>('.header');
|
||||
|
||||
if (siteHeader) {
|
||||
initializeSiteHeader(siteHeader);
|
||||
|
||||
@@ -4,8 +4,7 @@ import { calculateMediaBoxesPositions, initializeMediaBox } from "$lib/component
|
||||
import { createMaintenanceStatusIcon } from "$lib/components/MaintenanceStatusIcon";
|
||||
import { createImageShowFullscreenButton } from "$lib/components/ImageShowFullscreenButton";
|
||||
|
||||
/** @type {NodeListOf<HTMLElement>} */
|
||||
const mediaBoxes = document.querySelectorAll('.media-box');
|
||||
const mediaBoxes = document.querySelectorAll<HTMLElement>('.media-box');
|
||||
|
||||
mediaBoxes.forEach(mediaBoxElement => {
|
||||
initializeMediaBox(mediaBoxElement, [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { watchTagDropdownsInTagsEditor, wrapTagDropdown } from "$lib/components/TagDropdownWrapper";
|
||||
|
||||
for (let tagDropdownElement of document.querySelectorAll('.tag.dropdown')) {
|
||||
for (let tagDropdownElement of document.querySelectorAll<HTMLElement>('.tag.dropdown')) {
|
||||
wrapTagDropdown(tagDropdownElement);
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ export class ImageShowFullscreenButton extends BaseComponent {
|
||||
}
|
||||
|
||||
#onButtonClicked() {
|
||||
const imageLinks = this.#mediaBoxTools?.mediaBox.imageLinks;
|
||||
const imageLinks = this.#mediaBoxTools?.mediaBox?.imageLinks;
|
||||
|
||||
if (!imageLinks) {
|
||||
throw new Error('Failed to resolve image links from media box tools!');
|
||||
|
||||
@@ -30,7 +30,7 @@ export class MaintenancePopup extends BaseComponent {
|
||||
#tagsToAdd: Set<string> = new Set();
|
||||
#isPlanningToSubmit: boolean = false;
|
||||
#isSubmitting: boolean = false;
|
||||
#tagsSubmissionTimer: number | null = null;
|
||||
#tagsSubmissionTimer: Timeout | null = null;
|
||||
#emitter = emitterAt(this);
|
||||
|
||||
/**
|
||||
@@ -70,6 +70,10 @@ export class MaintenancePopup extends BaseComponent {
|
||||
|
||||
const mediaBox = this.#mediaBoxTools.mediaBox;
|
||||
|
||||
if (!mediaBox) {
|
||||
throw new Error('Media box component not found!');
|
||||
}
|
||||
|
||||
mediaBox.on('mouseout', this.#onMouseLeftArea.bind(this));
|
||||
mediaBox.on('mouseover', this.#onMouseEnteredArea.bind(this));
|
||||
}
|
||||
@@ -83,7 +87,7 @@ export class MaintenancePopup extends BaseComponent {
|
||||
}
|
||||
|
||||
#refreshTagsList() {
|
||||
if (!this.#mediaBoxTools) {
|
||||
if (!this.#mediaBoxTools?.mediaBox) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -109,11 +113,11 @@ export class MaintenancePopup extends BaseComponent {
|
||||
this.#tagsList[index] = tagElement;
|
||||
this.#tagsListElement.appendChild(tagElement);
|
||||
|
||||
const isPresent = currentPostTags.has(tagName);
|
||||
const isPresent = currentPostTags?.has(tagName);
|
||||
|
||||
tagElement.classList.toggle('is-present', isPresent);
|
||||
tagElement.classList.toggle('is-missing', !isPresent);
|
||||
tagElement.classList.toggle('is-aliased', isPresent && currentPostTags.get(tagName) !== tagName);
|
||||
tagElement.classList.toggle('is-aliased', isPresent && currentPostTags?.get(tagName) !== tagName);
|
||||
|
||||
// Just to prevent duplication, we need to include this tag to the map of suggested invalid tags
|
||||
if (tagsBlacklist.includes(tagName)) {
|
||||
@@ -193,7 +197,7 @@ export class MaintenancePopup extends BaseComponent {
|
||||
}
|
||||
|
||||
async #onSubmissionTimerPassed() {
|
||||
if (!this.#isPlanningToSubmit || this.#isSubmitting || !this.#mediaBoxTools) {
|
||||
if (!this.#isPlanningToSubmit || this.#isSubmitting || !this.#mediaBoxTools?.mediaBox) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -264,7 +268,7 @@ export class MaintenancePopup extends BaseComponent {
|
||||
}
|
||||
|
||||
#revealInvalidTags() {
|
||||
if (!this.#mediaBoxTools) {
|
||||
if (!this.#mediaBoxTools?.mediaBox) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,16 @@ import StorageHelper, { type StorageChangeSubscriber } from "$lib/browser/Storag
|
||||
|
||||
export default class ConfigurationController {
|
||||
readonly #configurationName: string;
|
||||
readonly #storage: StorageHelper;
|
||||
|
||||
/**
|
||||
* @param {string} configurationName Name of the configuration to work with.
|
||||
* @param {StorageHelper} [storage] Selected storage where the settings are stored in. If not provided, local storage
|
||||
* is used.
|
||||
*/
|
||||
constructor(configurationName: string) {
|
||||
constructor(configurationName: string, storage: StorageHelper = new StorageHelper(chrome.storage.local)) {
|
||||
this.#configurationName = configurationName;
|
||||
this.#storage = storage;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -19,7 +23,7 @@ export default class ConfigurationController {
|
||||
* @return The setting value or the default value if the setting does not exist.
|
||||
*/
|
||||
async readSetting<Type = any, DefaultType = any>(settingName: string, defaultValue: DefaultType | null = null): Promise<Type | DefaultType> {
|
||||
const settings = await ConfigurationController.#storageHelper.read(this.#configurationName, {});
|
||||
const settings = await this.#storage.read(this.#configurationName, {});
|
||||
return settings[settingName] ?? defaultValue;
|
||||
}
|
||||
|
||||
@@ -32,11 +36,11 @@ export default class ConfigurationController {
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async writeSetting(settingName: string, value: any): Promise<void> {
|
||||
const settings = await ConfigurationController.#storageHelper.read(this.#configurationName, {});
|
||||
const settings = await this.#storage.read(this.#configurationName, {});
|
||||
|
||||
settings[settingName] = value;
|
||||
|
||||
ConfigurationController.#storageHelper.write(this.#configurationName, settings);
|
||||
this.#storage.write(this.#configurationName, settings);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,11 +49,11 @@ export default class ConfigurationController {
|
||||
* @param {string} settingName Setting name to delete.
|
||||
*/
|
||||
async deleteSetting(settingName: string): Promise<void> {
|
||||
const settings = await ConfigurationController.#storageHelper.read(this.#configurationName, {});
|
||||
const settings = await this.#storage.read(this.#configurationName, {});
|
||||
|
||||
delete settings[settingName];
|
||||
|
||||
ConfigurationController.#storageHelper.write(this.#configurationName, settings);
|
||||
this.#storage.write(this.#configurationName, settings);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,10 +73,8 @@ export default class ConfigurationController {
|
||||
callback(changes[this.#configurationName].newValue);
|
||||
}
|
||||
|
||||
ConfigurationController.#storageHelper.subscribe(subscriber);
|
||||
this.#storage.subscribe(subscriber);
|
||||
|
||||
return () => ConfigurationController.#storageHelper.unsubscribe(subscriber);
|
||||
return () => this.#storage.unsubscribe(subscriber);
|
||||
}
|
||||
|
||||
static #storageHelper = new StorageHelper(chrome.storage.local);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ export default class CustomCategoriesResolver {
|
||||
#tagCategories = new Map<string, string>();
|
||||
#compiledRegExps = new Map<RegExp, string>();
|
||||
#tagDropdowns: TagDropdownWrapper[] = [];
|
||||
#nextQueuedUpdate = -1;
|
||||
#nextQueuedUpdate: Timeout | null = null;
|
||||
|
||||
constructor() {
|
||||
TagGroup.subscribe(this.#onTagGroupsReceived.bind(this));
|
||||
@@ -24,7 +24,9 @@ export default class CustomCategoriesResolver {
|
||||
}
|
||||
|
||||
#queueUpdatingTags() {
|
||||
clearTimeout(this.#nextQueuedUpdate);
|
||||
if (this.#nextQueuedUpdate) {
|
||||
clearTimeout(this.#nextQueuedUpdate);
|
||||
}
|
||||
|
||||
this.#nextQueuedUpdate = setTimeout(
|
||||
this.#updateUnprocessedTags.bind(this),
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { BaseComponent } from "$lib/components/base/BaseComponent";
|
||||
import { getComponent } from "$lib/components/base/component-utils";
|
||||
|
||||
function randomString() {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
import { randomString } from "$tests/utils";
|
||||
|
||||
describe('BaseComponent', () => {
|
||||
it('should bind the component to the element', () => {
|
||||
|
||||
186
tests/lib/extension/ConfigurationController.spec.ts
Normal file
186
tests/lib/extension/ConfigurationController.spec.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import ConfigurationController from "$lib/extension/ConfigurationController";
|
||||
import ChromeStorageArea from "$tests/mocks/ChromeStorageArea";
|
||||
import StorageHelper from "$lib/browser/StorageHelper";
|
||||
import { randomString } from "$tests/utils";
|
||||
|
||||
describe('ConfigurationController', () => {
|
||||
const mockedStorageArea = new ChromeStorageArea();
|
||||
const mockedStorageHelper = new StorageHelper(mockedStorageArea);
|
||||
|
||||
beforeEach(() => {
|
||||
mockedStorageArea.clear();
|
||||
});
|
||||
|
||||
it('should read setting from the field inside the configuration object', async () => {
|
||||
const name = randomString();
|
||||
const field = randomString();
|
||||
const value = randomString();
|
||||
|
||||
mockedStorageArea.insertMockedData({
|
||||
[name]: {
|
||||
[field]: value
|
||||
}
|
||||
});
|
||||
|
||||
const controller = new ConfigurationController(name, mockedStorageHelper);
|
||||
const returnedValue = await controller.readSetting(field);
|
||||
|
||||
expect(returnedValue).toBe(value);
|
||||
});
|
||||
|
||||
it('should return fallback value if configuration field does not exist', async () => {
|
||||
const controller = new ConfigurationController(randomString(), mockedStorageHelper);
|
||||
const fallbackValue = randomString();
|
||||
const returnedValue = await controller.readSetting(randomString(), fallbackValue);
|
||||
|
||||
expect(returnedValue).toBe(fallbackValue);
|
||||
});
|
||||
|
||||
it('should treat existing falsy values as existing values', async () => {
|
||||
const name = randomString();
|
||||
|
||||
const falsyValuesStorage = [0, false, ''].reduce((record, value) => {
|
||||
record[randomString()] = value;
|
||||
return record;
|
||||
}, {} as Record<string, any>);
|
||||
|
||||
mockedStorageArea.insertMockedData({
|
||||
[name]: falsyValuesStorage
|
||||
});
|
||||
|
||||
const controller = new ConfigurationController(name, mockedStorageHelper);
|
||||
|
||||
for (const fieldName of Object.keys(falsyValuesStorage)) {
|
||||
const returnedValue = await controller.readSetting(fieldName, randomString());
|
||||
|
||||
expect(returnedValue).toBe(falsyValuesStorage[fieldName]);
|
||||
}
|
||||
});
|
||||
|
||||
it('should write data to storage', async () => {
|
||||
const name = randomString();
|
||||
const field = randomString();
|
||||
const value = randomString();
|
||||
|
||||
const controller = new ConfigurationController(name, mockedStorageHelper);
|
||||
await controller.writeSetting(field, value);
|
||||
|
||||
const expectedStructure = {
|
||||
[name]: {
|
||||
[field]: value,
|
||||
}
|
||||
};
|
||||
|
||||
expect(mockedStorageArea.mockedData).toEqual(expectedStructure);
|
||||
});
|
||||
|
||||
it('should update existing object without touching other entries', async () => {
|
||||
const name = randomString();
|
||||
const existingField = randomString();
|
||||
const existingValue = randomString();
|
||||
const addedField = randomString();
|
||||
const addedValue = randomString();
|
||||
|
||||
mockedStorageArea.insertMockedData({
|
||||
[name]: {
|
||||
[existingField]: existingValue,
|
||||
}
|
||||
});
|
||||
|
||||
const controller = new ConfigurationController(name, mockedStorageHelper);
|
||||
await controller.writeSetting(addedField, addedValue);
|
||||
|
||||
const expectedStructure = {
|
||||
[name]: {
|
||||
[existingField]: existingValue,
|
||||
[addedField]: addedValue,
|
||||
}
|
||||
}
|
||||
|
||||
expect(mockedStorageArea.mockedData).toEqual(expectedStructure);
|
||||
});
|
||||
|
||||
it('should delete setting from storage', async () => {
|
||||
const name = randomString();
|
||||
const field = randomString();
|
||||
const value = randomString();
|
||||
|
||||
mockedStorageArea.insertMockedData({
|
||||
[name]: {
|
||||
[field]: value
|
||||
}
|
||||
});
|
||||
|
||||
const controller = new ConfigurationController(name, mockedStorageHelper);
|
||||
await controller.deleteSetting(field);
|
||||
|
||||
expect(mockedStorageArea.mockedData).toEqual({
|
||||
[name]: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return updated settings contents on changes', async () => {
|
||||
const name = randomString();
|
||||
const initialField = randomString();
|
||||
const initialValue = randomString();
|
||||
|
||||
const addedField = randomString();
|
||||
const addedValue = randomString();
|
||||
|
||||
const updatedInitialValue = randomString();
|
||||
const receivedData: Record<string, string>[] = [];
|
||||
|
||||
mockedStorageArea.insertMockedData({
|
||||
[name]: {
|
||||
[initialField]: initialValue,
|
||||
}
|
||||
});
|
||||
|
||||
const controller = new ConfigurationController(name, mockedStorageHelper);
|
||||
const subscriber = vi.fn((storageState: Record<string, string>) => {
|
||||
receivedData.push(JSON.parse(JSON.stringify(storageState)));
|
||||
});
|
||||
|
||||
controller.subscribeToChanges(subscriber);
|
||||
|
||||
await controller.writeSetting(addedField, addedValue);
|
||||
await controller.writeSetting(initialField, updatedInitialValue);
|
||||
await controller.deleteSetting(initialField);
|
||||
|
||||
expect(subscriber).toBeCalledTimes(3);
|
||||
|
||||
const expectedData: Record<string, string>[] = [
|
||||
// First, initial data and added field are present
|
||||
{
|
||||
[initialField]: initialValue,
|
||||
[addedField]: addedValue,
|
||||
},
|
||||
// Then we get new value on initial field
|
||||
{
|
||||
[initialField]: updatedInitialValue,
|
||||
[addedField]: addedValue,
|
||||
},
|
||||
// And then the initial value is dropped
|
||||
{
|
||||
[addedField]: addedValue,
|
||||
}
|
||||
];
|
||||
|
||||
expect(receivedData).toEqual(expectedData);
|
||||
});
|
||||
|
||||
it('should stop listening once unsubscribe called', async () => {
|
||||
const controller = new ConfigurationController(randomString(), mockedStorageHelper);
|
||||
const subscriber = vi.fn();
|
||||
|
||||
const unsubscribe = controller.subscribeToChanges(subscriber);
|
||||
|
||||
await controller.writeSetting(randomString(), randomString());
|
||||
expect(subscriber).toBeCalledTimes(1);
|
||||
|
||||
unsubscribe();
|
||||
subscriber.mockReset();
|
||||
await controller.writeSetting(randomString(), randomString())
|
||||
expect(subscriber).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,9 @@
|
||||
export default class ChromeEvent<T extends Function> implements chrome.events.Event<T> {
|
||||
addListener = vi.fn();
|
||||
getRules = vi.fn();
|
||||
hasListener = vi.fn();
|
||||
removeRules = vi.fn();
|
||||
addRules = vi.fn();
|
||||
removeListener = vi.fn();
|
||||
hasListeners = vi.fn();
|
||||
addListener = vi.fn();
|
||||
getRules = vi.fn();
|
||||
hasListener = vi.fn();
|
||||
removeRules = vi.fn();
|
||||
addRules = vi.fn();
|
||||
removeListener = vi.fn();
|
||||
hasListeners = vi.fn();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import ChromeStorageArea from "$tests/mocks/ChromeStorageArea";
|
||||
|
||||
export class ChromeLocalStorageArea extends ChromeStorageArea implements chrome.storage.LocalStorageArea {
|
||||
QUOTA_BYTES = 100000;
|
||||
QUOTA_BYTES = 100000;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import ChromeEvent from "./ChromeEvent";
|
||||
import ChromeStorageChangeEvent from "$tests/mocks/ChromeStorageChangeEvent";
|
||||
|
||||
type ChangedEventCallback = (changes: chrome.storage.StorageChange) => void
|
||||
|
||||
@@ -13,8 +13,20 @@ export default class ChromeStorageArea implements chrome.storage.StorageArea {
|
||||
})
|
||||
});
|
||||
set = vi.fn((...args: any[]): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise((resolve) => {
|
||||
const change: Record<string, chrome.storage.StorageChange> = {};
|
||||
const setter = args[0];
|
||||
|
||||
for (let targetKey of Object.keys(setter)) {
|
||||
change[targetKey] = {
|
||||
oldValue: this.#mockedData[targetKey] ?? undefined,
|
||||
newValue: setter[targetKey],
|
||||
};
|
||||
}
|
||||
|
||||
this.#mockedData = Object.assign(this.#mockedData, args[0]);
|
||||
this.onChanged.mockEmitStorageChange(change);
|
||||
|
||||
resolve();
|
||||
})
|
||||
});
|
||||
@@ -23,7 +35,16 @@ export default class ChromeStorageArea implements chrome.storage.StorageArea {
|
||||
const key = args[0];
|
||||
|
||||
if (typeof key === 'string') {
|
||||
const change: chrome.storage.StorageChange = {
|
||||
oldValue: this.#mockedData[key],
|
||||
};
|
||||
|
||||
delete this.#mockedData[key];
|
||||
|
||||
this.onChanged.mockEmitStorageChange({
|
||||
[key]: change
|
||||
});
|
||||
|
||||
resolve();
|
||||
}
|
||||
|
||||
@@ -58,7 +79,7 @@ export default class ChromeStorageArea implements chrome.storage.StorageArea {
|
||||
});
|
||||
});
|
||||
setAccessLevel = vi.fn();
|
||||
onChanged = new ChromeEvent<ChangedEventCallback>();
|
||||
onChanged = new ChromeStorageChangeEvent();
|
||||
getKeys = vi.fn();
|
||||
|
||||
insertMockedData(data: Record<string, any>) {
|
||||
|
||||
27
tests/mocks/ChromeStorageChangeEvent.ts
Normal file
27
tests/mocks/ChromeStorageChangeEvent.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import ChromeEvent from "$tests/mocks/ChromeEvent";
|
||||
import { EventEmitter } from "node:events";
|
||||
|
||||
type MockedStorageChanges = Record<string, chrome.storage.StorageChange>;
|
||||
type IncomingStorageChangeListener = (changes: MockedStorageChanges) => void;
|
||||
|
||||
const storageChangeEvent = Symbol();
|
||||
|
||||
interface StorageChangeEventMap {
|
||||
[storageChangeEvent]: [MockedStorageChanges];
|
||||
}
|
||||
|
||||
export default class ChromeStorageChangeEvent extends ChromeEvent<IncomingStorageChangeListener> {
|
||||
#emitter = new EventEmitter<StorageChangeEventMap>();
|
||||
|
||||
addListener = vi.fn((actualListener: IncomingStorageChangeListener) => {
|
||||
this.#emitter.addListener(storageChangeEvent, actualListener);
|
||||
});
|
||||
|
||||
removeListener = vi.fn((actualListener: IncomingStorageChangeListener) => {
|
||||
this.#emitter.removeListener(storageChangeEvent, actualListener);
|
||||
});
|
||||
|
||||
mockEmitStorageChange(changes: MockedStorageChanges) {
|
||||
this.#emitter.emit(storageChangeEvent, changes);
|
||||
}
|
||||
}
|
||||
7
tests/utils.ts
Normal file
7
tests/utils.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export function randomString(): string {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
|
||||
export function copyValue<T>(object: T): T {
|
||||
return JSON.parse(JSON.stringify(object));
|
||||
}
|
||||
Reference in New Issue
Block a user