diff --git a/package.json b/package.json index 263ac85..a4a45f2 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "package:chrome": "npm run build:app && web-ext build -s ./dist/webext/ -i app/manifest.json -i app/index.html -i manifest.firefox.json -n chrome.zip", "package:firefox": "npm run build:app && cp src/webext/manifest.firefox.json dist/webext/manifest.json && web-ext build -s ./dist/webext/ -i app/manifest.json -i manifest.firefox.json -i app/index.html -n firefox.zip", "lint": "eslint 'src/**/*.{js,ts,tsx}' --fix", - "test": "NODE_PATH=src mocha -r ts-node/register tests/**/*.test.ts", + "test": "TS_NODE_FILES=1 NODE_PATH=src mocha -r ts-node/register tests/**/*.test.ts", "coverage": "nyc -r lcov -e .ts -x \"*.test.ts\" npm run test", "trans:extract": "formatjs extract 'src/app/**/*.ts*' --out-file src/app/locale/en.json --id-interpolation-pattern '[sha512:contenthash:base64:6]' --format utils/translation-format.js && node utils/update_translations.js", "trans:compile": "node utils/compile_translations.js" diff --git a/src/app/Storage.tsx b/src/app/Storage.tsx index 922a1f3..405014e 100644 --- a/src/app/Storage.tsx +++ b/src/app/Storage.tsx @@ -119,7 +119,7 @@ class DelegateStorage implements IStorage { } -if (typeof browser === 'undefined' && +if (typeof browser === 'undefined' && typeof navigator !== "undefined" && navigator.storage && navigator.storage.persist) { navigator.storage.persist() .then((isPersisted) => diff --git a/src/app/WidgetManager.tsx b/src/app/WidgetManager.tsx index 24ae482..88c116d 100644 --- a/src/app/WidgetManager.tsx +++ b/src/app/WidgetManager.tsx @@ -1,4 +1,4 @@ -import { storage } from "./Storage"; +import { IStorage } from "./Storage"; import { Vector2 } from "./utils/Vector2"; import { WidgetTypes } from "./widgets"; import { getInitialTheme, Widget } from "./Widget"; @@ -13,10 +13,10 @@ export class WidgetManager { widgets: (Widget)[] = []; - constructor() {} + constructor(private storage: IStorage) {} async load() { - const json = await storage.get[]>("widgets"); + const json = await this.storage.get[]>("widgets"); if (!json) { this.resetToDefault(); return; @@ -43,7 +43,7 @@ export class WidgetManager { } save() { - storage.set("widgets", this.widgets); + this.storage.set("widgets", this.widgets); } resetToDefault() { @@ -52,11 +52,11 @@ export class WidgetManager { "HelpAbout", "Weather", "Feed", "Notes"].forEach(this.createWidget.bind(this)); } - createWidget(type: string) { + createWidget(type: string): Widget { this.id_counter++; const widget_type = WidgetTypes[type]; - const widget = { + const widget: Widget = { id: this.id_counter, type: type, position: undefined, @@ -71,6 +71,8 @@ export class WidgetManager { } this.save(); + + return widget; } removeWidget(id: number) { diff --git a/src/app/components/App.tsx b/src/app/components/App.tsx index d129da9..fc62b09 100644 --- a/src/app/components/App.tsx +++ b/src/app/components/App.tsx @@ -10,9 +10,10 @@ import { FormattedMessage, IntlProvider } from "react-intl"; import { getTranslation, getUserLocale } from "app/locale"; import { applyTheme, ThemeConfig } from "./settings/ThemeSettings"; import ReviewRequester from "./ReviewRequester"; +import { storage } from "app/Storage"; -const widgetManager = new WidgetManager(); +const widgetManager = new WidgetManager(storage); export default function App() { const [loadingRes,] = usePromise(async () => { diff --git a/src/app/custom.d.ts b/src/app/custom.d.ts new file mode 100644 index 0000000..cd3c976 --- /dev/null +++ b/src/app/custom.d.ts @@ -0,0 +1,7 @@ +interface Config { + API_URL: string; + PROXY_URL: string; +} + +declare var config: Config; +declare const app_version: string; diff --git a/src/app/hooks/http.tsx b/src/app/hooks/http.tsx index 6835000..a00e859 100644 --- a/src/app/hooks/http.tsx +++ b/src/app/hooks/http.tsx @@ -3,14 +3,13 @@ import { miscMessages } from "app/locale/common"; import { IntlShape, useIntl } from "react-intl"; import { usePromise } from "./promises"; -const proxy_url = config.PROXY_URL; function makeProxy(url: string) { if (typeof browser !== 'undefined') { console.log("Detected running as webext"); return url; } else { - const ret = new URL(proxy_url); + const ret = new URL(config.PROXY_URL); ret.searchParams.set("url", url); return ret.toString(); } diff --git a/src/app/index.tsx b/src/app/index.tsx index b811f8a..5fa47a7 100644 --- a/src/app/index.tsx +++ b/src/app/index.tsx @@ -1,14 +1,3 @@ -interface Config { - API_URL: string; - PROXY_URL: string; - PROXY_ALLOWED_HOSTS: string[]; -} - -declare global { - const config: Config; - const app_version: string; -} - import React from "react"; import { render } from "react-dom"; import App from "./components/App"; diff --git a/tests/app/DummyStorage.ts b/tests/app/DummyStorage.ts new file mode 100644 index 0000000..596f812 --- /dev/null +++ b/tests/app/DummyStorage.ts @@ -0,0 +1,25 @@ +import { IStorage } from "app/Storage"; + +export default class DummyStorage implements IStorage { + private values: { [key: string]: any } = {}; + + async getAll(): Promise<{ [key: string]: any; }> { + return { ...this.values }; + } + + async get(key: string): Promise { + return this.values[key] ?? null; + } + + async set(key: string, value: T): Promise { + this.values[key] = value; + } + + async remove(key: string): Promise { + delete this.values[key]; + } + + async clear(): Promise { + this.values = {} + } +} diff --git a/tests/WidgetLayouter.test.ts b/tests/app/WidgetLayouter.test.ts similarity index 89% rename from tests/WidgetLayouter.test.ts rename to tests/app/WidgetLayouter.test.ts index 55deab9..eac1da5 100644 --- a/tests/WidgetLayouter.test.ts +++ b/tests/app/WidgetLayouter.test.ts @@ -36,7 +36,10 @@ describe("WidgetLayouter", function() { type: "Type", position: V(1, 1), size: V(3, 3), - props: {} + props: {}, + theme: { + showPanelBG: false + } }; layouter.resolveAll([widget]); @@ -52,21 +55,30 @@ describe("WidgetLayouter", function() { id: 1, type: "Type", size: V(3, 3), - props: {} + props: {}, + theme: { + showPanelBG: false + } }; const widget2 : Widget = { id: 1, type: "Type", size: V(15, 2), - props: {} + props: {}, + theme: { + showPanelBG: false + } }; const widget3 : Widget = { id: 1, type: "Type", size: V(3, 3), - props: {} + props: {}, + theme: { + showPanelBG: false + } }; layouter.resolveAll([widget1, widget2, widget3]); @@ -86,7 +98,10 @@ describe("WidgetLayouter", function() { id: 1, type: "Type", size: V(3, 3), - props: {} + props: {}, + theme: { + showPanelBG: false + } }; const widget2 : Widget = { @@ -94,7 +109,10 @@ describe("WidgetLayouter", function() { type: "Type", position: V(0, 0), size: V(15, 2), - props: {} + props: {}, + theme: { + showPanelBG: false + } }; const widget3 : Widget = { @@ -102,7 +120,10 @@ describe("WidgetLayouter", function() { type: "Type", position: V(3, 0), size: V(3, 3), - props: {} + props: {}, + theme: { + showPanelBG: false + } }; layouter.resolveAll([widget1, widget2, widget3]); diff --git a/tests/app/WidgetManager.test.ts b/tests/app/WidgetManager.test.ts new file mode 100644 index 0000000..e60cbcb --- /dev/null +++ b/tests/app/WidgetManager.test.ts @@ -0,0 +1,123 @@ +import { LinkBoxProps } from "app/components/LinkBox"; +import { Vector2 } from "app/utils/Vector2"; +import { WidgetManager } from "app/WidgetManager"; +import { expect } from "chai"; +import DummyStorage from "./DummyStorage"; + +describe("WidgetManager::create", () => { + it("createsDefaultWidgets", async () => { + const storage = new DummyStorage(); + const wm = new WidgetManager(storage); + await wm.load(); + expect(wm.widgets.length).to.equal(9); + }); + + it("copiesProps", async () => { + const storage = new DummyStorage(); + storage.set("widgets", [ + { + id: "123", + type: "Notes", + props: {}, + size: new Vector2(3, 5), + }, + ]); + + const wm = new WidgetManager(storage); + expect(wm.widgets.length).to.equal(0); + await wm.load(); + expect(wm.widgets.length).to.equal(1); + + const widget1 = wm.createWidget("Links"); + const widget2 = wm.createWidget("Links"); + expect(widget1.props.links.length).to.equal(6); + expect(widget2.props.links.length).to.equal(6); + + widget1.props.links.push({ + id: "123", + title: "Title", + url: "url", + }); + + expect(widget1.props.links.length).to.equal(7); + expect(widget2.props.links.length).to.equal(6); + }); + + it("copiesTheme", async () => { + const storage = new DummyStorage(); + storage.set("widgets", []); + const wm = new WidgetManager(storage); + await wm.load(); + expect(wm.widgets.length).to.equal(0); + + const widget1 = wm.createWidget("Links"); + const widget2 = wm.createWidget("Links"); + expect(widget1.theme.useIconBar).is.false; + widget1.theme.useIconBar = true; + expect(widget1.theme.useIconBar).is.true; + expect(widget2.theme.useIconBar).is.false; + }); +}); + + +describe("WidgetManager::load", () => { + it("adds theme", async () => { + const storage = new DummyStorage(); + storage.set("widgets", [ + { + id: "123", + type: "Notes", + props: {}, + size: new Vector2(3, 5), + }, + { + id: "124", + type: "Clock", + props: {}, + size: new Vector2(3, 5), + }, + ]); + + const wm = new WidgetManager(storage); + expect(wm.widgets.length).to.equal(0); + await wm.load(); + expect(wm.widgets.length).to.equal(2); + + { + const notesWidget = wm.widgets[0]; + expect(notesWidget.theme).not.null; + expect(notesWidget.theme.showPanelBG).to.be.true; + } + + { + const clockWidget = wm.widgets[1]; + expect(clockWidget.theme).not.null; + expect(clockWidget.theme.showPanelBG).to.be.false; + } + }); + + it("runs load hook", async () => { + const storage = new DummyStorage(); + storage.set("widgets", [ + { + id: "123", + type: "TopSites", + props: { + useIconBar: false, + }, + size: new Vector2(3, 5), + }, + ]); + + const wm = new WidgetManager(storage); + expect(wm.widgets.length).to.equal(0); + await wm.load(); + expect(wm.widgets.length).to.equal(1); + + const widget = wm.widgets[0]; + expect(widget.props).not.to.haveOwnProperty("useIconBar"); + expect(widget.theme).not.null; + expect(widget.theme.showPanelBG).to.be.true; + expect(widget.theme.useIconBar).to.be.false; + }); +}); diff --git a/tests/utils/Color.test.ts b/tests/app/utils/Color.test.ts similarity index 100% rename from tests/utils/Color.test.ts rename to tests/app/utils/Color.test.ts diff --git a/tsconfig.app.json b/tsconfig.app.json index 2d1ef16..596e164 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -4,6 +4,7 @@ "module": "ES2015" }, "exclude": [ - "src/server" + "src/server", + "tests" ] } diff --git a/tsconfig.json b/tsconfig.json index 7c06b13..b93d4f2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,8 @@ "sourceMap": true, "typeRoots": [ "./node_modules/@types", - "./node_modules/web-ext-types" + "./node_modules/web-ext-types", + "src/app/custom.d.ts" ], "baseUrl": "src", "esModuleInterop": true, @@ -15,6 +16,6 @@ "resolveJsonModule": true }, "include": [ - "src" + "src", "tests" ] } diff --git a/tsconfig.server.json b/tsconfig.server.json index 12d8293..ba4ad3e 100644 --- a/tsconfig.server.json +++ b/tsconfig.server.json @@ -2,7 +2,8 @@ "extends": "./tsconfig.json", "exclude": [ "src/app", - "src/webext" + "src/webext", + "tests" ] } //