From 408291e229379bf79961c5d59cab3f37cae4dc6b Mon Sep 17 00:00:00 2001 From: Bauke Date: Mon, 26 Dec 2022 12:57:57 +0100 Subject: [PATCH] Add source code. --- source/index.ts | 1 + source/value.ts | 73 ++++++++++++++++++++++++++++++++++ tests/background.ts | 9 +++++ tests/example.ts | 33 ++++++++++++++++ tests/tests.ts | 78 +++++++++++++++++++++++++++++++++++++ tests/web-ext/index.html | 16 ++++++++ tests/web-ext/manifest.json | 24 ++++++++++++ 7 files changed, 234 insertions(+) create mode 100644 source/index.ts create mode 100644 source/value.ts create mode 100644 tests/background.ts create mode 100644 tests/example.ts create mode 100644 tests/tests.ts create mode 100644 tests/web-ext/index.html create mode 100644 tests/web-ext/manifest.json diff --git a/source/index.ts b/source/index.ts new file mode 100644 index 0000000..3b2d916 --- /dev/null +++ b/source/index.ts @@ -0,0 +1 @@ +export * from "./value.js"; diff --git a/source/value.ts b/source/value.ts new file mode 100644 index 0000000..c923a20 --- /dev/null +++ b/source/value.ts @@ -0,0 +1,73 @@ +import browser from "webextension-polyfill"; + +export type StorageArea = browser.Storage.StorageArea; + +type ValueOptions = { + /** A function to convert a string to the type `T`. */ + deserialize: (input: string) => T; + /** The key to use for storage. */ + key: string; + /** A function convert the type `T` to a string, defaults to `JSON.stringify`. */ + serialize?: (input: T) => string; + /** The storage area to use, defaults to local. */ + storage?: StorageArea; + /** The default value to use if none exists in storage. */ + value: T; +}; + +export async function createValue( + options: ValueOptions, +): Promise> { + const storage = options.storage ?? browser.storage.local; + + const value = await storage.get(options.key); + const stored = value[options.key] as string | undefined; + + return new Value({ + key: options.key, + deserialize: options.deserialize, + serialize: options.serialize ?? JSON.stringify, + storage, + value: stored === undefined ? options.value : options.deserialize(stored), + }); +} + +type Props = Required>; + +export class Value implements Props { + public readonly deserialize: Props["deserialize"]; + public readonly key: Props["key"]; + public readonly serialize: Props["serialize"]; + public readonly storage: Props["storage"]; + + private inner: Props["value"]; + + constructor(options: Required>) { + this.deserialize = options.deserialize; + this.key = options.key; + this.serialize = options.serialize; + this.storage = options.storage; + + this.inner = options.value; + } + + get value(): T { + return this.inner; + } + + set value(value: T) { + this.inner = value; + } + + /** Remove the value from storage. */ + public async remove(): Promise { + await this.storage.remove(this.key); + } + + /** Save the value to storage. */ + public async save(): Promise { + await this.storage.set({ + [this.key]: this.serialize(this.inner), + }); + } +} diff --git a/tests/background.ts b/tests/background.ts new file mode 100644 index 0000000..a74e35f --- /dev/null +++ b/tests/background.ts @@ -0,0 +1,9 @@ +import browser from "webextension-polyfill"; + +browser.browserAction.onClicked.addListener(async () => { + await browser.runtime.openOptionsPage(); +}); + +browser.runtime.onInstalled.addListener(async () => { + await browser.runtime.openOptionsPage(); +}); diff --git a/tests/example.ts b/tests/example.ts new file mode 100644 index 0000000..d11a554 --- /dev/null +++ b/tests/example.ts @@ -0,0 +1,33 @@ +import browser from "webextension-polyfill"; + +import {createValue} from "../build/index.js"; + +const updatedDate = await createValue({ + // A function that deserializes a string from storage to convert to the wanted + // type. + deserialize: (value) => new Date(value), + + // A function that serializes the type to a string to be set in storage. + serialize: (date) => date.toISOString(), + + // The key to get from storage. + key: "updatedDate", + + // The StorageArea to use, defaults to local. + storage: browser.storage.sync, + + // The value to use if there is none in storage. + value: new Date(), +}); + +// Get the inner value. +console.log(updatedDate.value); + +// Set the inner value. +updatedDate.value = new Date(); + +// Save the value to storage. +await updatedDate.save(); + +// Remove the value from storage. +await updatedDate.remove(); diff --git a/tests/tests.ts b/tests/tests.ts new file mode 100644 index 0000000..57dc8f0 --- /dev/null +++ b/tests/tests.ts @@ -0,0 +1,78 @@ +import {setup, type TestContext} from "@holllo/test"; +import browser from "webextension-polyfill"; + +import {createValue, type Value} from "../source/index.js"; + +const create = async ( + key: string, + expected: T, +): Promise<[string, T, Value]> => { + return [ + key, + expected, + await createValue({ + deserialize: JSON.parse, + key, + serialize: JSON.stringify, + value: expected, + }), + ]; +}; + +const isStored = async (test: TestContext, key: string, exist: boolean) => { + const stored = await browser.storage.local.get(key); + test.equals(typeof stored[key], exist ? "string" : "undefined"); +}; + +type SampleObject = { + name: string; + status: "failed" | "passed"; +}; + +const sampleObject: SampleObject = { + name: "Sample Object", + status: "passed", +}; + +const group = await setup("Value", async (group) => { + const samples = [ + ["number", "testNumber", Math.PI], + ["string", "testString", "A string to test with!" as string], + ["SampleObject", "testSampleObject", sampleObject], + ] as const; + + for (const sample of samples) { + group.test(`T = ${sample[0]}`, async (test) => { + const [key, expected, value] = await create(sample[1], sample[2]); + if (sample[0] === "SampleObject") { + const _expected = expected as SampleObject; + const _value = value.value as SampleObject; + test.equals(_value.name, _expected.name); + test.equals(_value.status, _expected.status); + } else { + test.equals(value.value, expected); + } + + await isStored(test, key, false); + await value.save(); + await isStored(test, key, true); + await value.remove(); + await isStored(test, key, false); + }); + } + + group.test(`T = Date`, async (test) => { + const expectedString = "2022-12-31T12:34:56.789Z"; + const expected = new Date(expectedString); + const value = await createValue({ + deserialize: (input) => new Date(input), + key: "testDate", + serialize: (input) => input.toISOString(), + value: expected, + }); + test.equals(value.value instanceof Date, true); + await value.save(); + const stored = await browser.storage.local.get(value.key); + test.equals(stored[value.key], expectedString); + }); +}); diff --git a/tests/web-ext/index.html b/tests/web-ext/index.html new file mode 100644 index 0000000..841b708 --- /dev/null +++ b/tests/web-ext/index.html @@ -0,0 +1,16 @@ + + + + + + + + WebExtension Storage Tests + + + + + + + + diff --git a/tests/web-ext/manifest.json b/tests/web-ext/manifest.json new file mode 100644 index 0000000..7e5961a --- /dev/null +++ b/tests/web-ext/manifest.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json.schemastore.org/webextension", + "manifest_version": 2, + "name": "WebExtension Storage Tests", + "version": "0.1.0", + "applications": { + "gecko": { + "id": "webextension-storage-tests@holllo.org" + } + }, + "background": { + "scripts": [ + "background.js" + ] + }, + "browser_action": {}, + "options_ui": { + "page": "index.html", + "open_in_tab": true + }, + "permissions": [ + "storage" + ] +}