mirror of
https://git.zavage.net/Zavage-Software/search-with-chatgpt-powered-by-openai-extension.git
synced 2026-08-10 13:40:29 -06:00
feat: add settings defaults and validation with unit tests
This commit is contained in:
parent
b59648d900
commit
c49f6415af
13
lib/defaults.js
Normal file
13
lib/defaults.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// Single source of truth for the settings shape and their defaults.
|
||||||
|
// Defaults reproduce the extension's pre-1.1 behaviour exactly: an empty model
|
||||||
|
// and both toggles off mean the only URL parameter sent is `q`.
|
||||||
|
|
||||||
|
export const OPEN_IN_VALUES = ["new-tab", "background-tab", "new-window"];
|
||||||
|
|
||||||
|
export const DEFAULT_SETTINGS = {
|
||||||
|
model: "",
|
||||||
|
temporaryChat: false,
|
||||||
|
webSearch: false,
|
||||||
|
openIn: "new-tab",
|
||||||
|
promptTemplate: "{query}",
|
||||||
|
};
|
||||||
45
lib/settings.js
Normal file
45
lib/settings.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { DEFAULT_SETTINGS, OPEN_IN_VALUES } from "./defaults.js";
|
||||||
|
|
||||||
|
// Pure. Layers stored values over the defaults, dropping unknown keys and
|
||||||
|
// falling back on wrong types or out-of-range values. A fresh or signed-out
|
||||||
|
// profile returns {} from storage, which yields the defaults.
|
||||||
|
export function mergeSettings(stored) {
|
||||||
|
const merged = { ...DEFAULT_SETTINGS };
|
||||||
|
if (stored === null || typeof stored !== "object") {
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
if (typeof stored.model === "string") {
|
||||||
|
merged.model = stored.model;
|
||||||
|
}
|
||||||
|
if (typeof stored.temporaryChat === "boolean") {
|
||||||
|
merged.temporaryChat = stored.temporaryChat;
|
||||||
|
}
|
||||||
|
if (typeof stored.webSearch === "boolean") {
|
||||||
|
merged.webSearch = stored.webSearch;
|
||||||
|
}
|
||||||
|
if (OPEN_IN_VALUES.includes(stored.openIn)) {
|
||||||
|
merged.openIn = stored.openIn;
|
||||||
|
}
|
||||||
|
if (typeof stored.promptTemplate === "string") {
|
||||||
|
merged.promptTemplate = stored.promptTemplate;
|
||||||
|
}
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
// storage.sync works with no Mozilla account (it behaves as local storage and
|
||||||
|
// syncs later if the user signs in), so no capability probe is needed. Any
|
||||||
|
// rejection falls back to defaults rather than blocking the user's action.
|
||||||
|
// Firefox sanitises all non-quota backend errors to a single generic string, so
|
||||||
|
// there is nothing useful to branch on.
|
||||||
|
export async function load() {
|
||||||
|
try {
|
||||||
|
const stored = await browser.storage.sync.get(Object.keys(DEFAULT_SETTINGS));
|
||||||
|
return mergeSettings(stored);
|
||||||
|
} catch {
|
||||||
|
return mergeSettings(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function save(partial) {
|
||||||
|
await browser.storage.sync.set(partial);
|
||||||
|
}
|
||||||
@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "search-with-chatgpt-powered-by-openai-extension",
|
"name": "search-with-chatgpt-powered-by-openai-extension",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
"description": "Development tooling for the Search with ChatGPT Firefox extension.",
|
"description": "Development tooling for the Search with ChatGPT Firefox extension.",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22"
|
"node": ">=22"
|
||||||
@ -9,8 +10,9 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "web-ext run",
|
"dev": "web-ext run",
|
||||||
"lint": "web-ext lint",
|
"lint": "web-ext lint",
|
||||||
|
"test:unit": "node --test",
|
||||||
"build": "web-ext build",
|
"build": "web-ext build",
|
||||||
"test": "npm run lint && npm run build"
|
"test": "npm run lint && npm run test:unit && npm run build"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"web-ext": "10.5.0"
|
"web-ext": "10.5.0"
|
||||||
|
|||||||
80
test/settings.test.js
Normal file
80
test/settings.test.js
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import test from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
|
import { DEFAULT_SETTINGS } from "../lib/defaults.js";
|
||||||
|
import { mergeSettings } from "../lib/settings.js";
|
||||||
|
|
||||||
|
test("empty object yields the defaults", () => {
|
||||||
|
assert.deepEqual(mergeSettings({}), DEFAULT_SETTINGS);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("null and undefined yield the defaults", () => {
|
||||||
|
assert.deepEqual(mergeSettings(null), DEFAULT_SETTINGS);
|
||||||
|
assert.deepEqual(mergeSettings(undefined), DEFAULT_SETTINGS);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a non-object yields the defaults", () => {
|
||||||
|
assert.deepEqual(mergeSettings("nonsense"), DEFAULT_SETTINGS);
|
||||||
|
assert.deepEqual(mergeSettings(42), DEFAULT_SETTINGS);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("valid stored values are preserved", () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
mergeSettings({
|
||||||
|
model: "auto",
|
||||||
|
temporaryChat: true,
|
||||||
|
webSearch: true,
|
||||||
|
openIn: "new-window",
|
||||||
|
promptTemplate: "Explain: {query}",
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
model: "auto",
|
||||||
|
temporaryChat: true,
|
||||||
|
webSearch: true,
|
||||||
|
openIn: "new-window",
|
||||||
|
promptTemplate: "Explain: {query}",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("missing keys fall back individually", () => {
|
||||||
|
const merged = mergeSettings({ model: "auto" });
|
||||||
|
assert.equal(merged.model, "auto");
|
||||||
|
assert.equal(merged.temporaryChat, DEFAULT_SETTINGS.temporaryChat);
|
||||||
|
assert.equal(merged.promptTemplate, DEFAULT_SETTINGS.promptTemplate);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("unknown keys are dropped", () => {
|
||||||
|
const merged = mergeSettings({ model: "auto", nope: "gone", openInn: "typo" });
|
||||||
|
assert.deepEqual(Object.keys(merged).sort(), Object.keys(DEFAULT_SETTINGS).sort());
|
||||||
|
assert.equal("nope" in merged, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("wrong types fall back to the default", () => {
|
||||||
|
const merged = mergeSettings({
|
||||||
|
model: 123,
|
||||||
|
temporaryChat: "yes",
|
||||||
|
webSearch: 1,
|
||||||
|
promptTemplate: [],
|
||||||
|
});
|
||||||
|
assert.equal(merged.model, DEFAULT_SETTINGS.model);
|
||||||
|
assert.equal(merged.temporaryChat, DEFAULT_SETTINGS.temporaryChat);
|
||||||
|
assert.equal(merged.webSearch, DEFAULT_SETTINGS.webSearch);
|
||||||
|
assert.equal(merged.promptTemplate, DEFAULT_SETTINGS.promptTemplate);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("an out-of-range openIn falls back to the default", () => {
|
||||||
|
assert.equal(mergeSettings({ openIn: "teleport" }).openIn, DEFAULT_SETTINGS.openIn);
|
||||||
|
assert.equal(mergeSettings({ openIn: "" }).openIn, DEFAULT_SETTINGS.openIn);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("each documented openIn value is accepted", () => {
|
||||||
|
for (const value of ["new-tab", "background-tab", "new-window"]) {
|
||||||
|
assert.equal(mergeSettings({ openIn: value }).openIn, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("mergeSettings does not mutate DEFAULT_SETTINGS", () => {
|
||||||
|
mergeSettings({ model: "mutated" });
|
||||||
|
assert.equal(DEFAULT_SETTINGS.model, "");
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user