Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Let generating URL based on options in storage #4

Merged
merged 1 commit into from
Dec 14, 2024
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
5 changes: 5 additions & 0 deletions .dir-locals.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
((nil .
((eval .
(add-to-list 'exec-path
(concat (locate-dominating-file default-directory dir-locals-file)
"node_modules/.bin/"))))))
109 changes: 86 additions & 23 deletions src/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,46 +20,109 @@ import { SiteData, amazonComParam, builtinSiteData } from "./site_data";
import { generateActivatingUrl, generateDisablingUrl } from "./main";

describe("generate Url", () => {
const activatingResult = "https://example.com/?result=activated" as const;
const disablingResult = "https://example.com/?notactivated" as const;
const matchingUrl = "https://example.com";
const unmatchingUrl = "https://example.org";
const activating1Result = "https://example.com/?result=activated" as const;
const disabling1Result = "https://example.com/?notactivated" as const;
const matching1Url = "https://example.com";
const matching2Url = "https://example.org";
const activating2Result = "https://example.org/?result=activated" as const;
const disabling2Result = "https://example.org/?notactivated" as const;
const unmatchingUrl = "https://example.net";
const siteData = [
{
id: "Test",
name: "Test Example",
urlRegex: /https:\/\/example\.com/,
activatingFunc: (): string => activatingResult,
disablingFunc: (): string => disablingResult,
activatingFunc: (): string => activating1Result,
disablingFunc: (): string => disabling1Result,
},
{
id: "Test 2",
name: "Test 2 Example",
urlRegex: /https:\/\/example\.org/,
activatingFunc: (): string => activating2Result,
disablingFunc: (): string => disabling2Result,
},
] as const satisfies SiteData;

test("Returns activating function result if URL matches", () => {
expect(generateActivatingUrl(matchingUrl, siteData)).toBe(activatingResult);
beforeEach(async () => {
vi.restoreAllMocks();
resetBrowserStorage();
await chrome.storage.local.set({
onOff: { [siteData[0].id]: true, [siteData[1].id]: true },
});
});

test("Returns activating function result if URL matches the first", async () => {
expect(await generateActivatingUrl(matching1Url, siteData)).toBe(
activating1Result,
);
});

test("Returns activating function result if URL matches the second", async () => {
expect(await generateActivatingUrl(matching2Url, siteData)).toBe(
activating2Result,
);
});

test("Returns disabling function result if URL matches the first", () => {
expect(generateDisablingUrl(matching1Url, siteData)).toBe(disabling1Result);
});

test("Returns disabling function result if URL matches", () => {
expect(generateDisablingUrl(matchingUrl, siteData)).toBe(disablingResult);
test("Returns disabling function result if URL matches the second", () => {
expect(generateDisablingUrl(matching2Url, siteData)).toBe(disabling2Result);
});

for (const func of [generateActivatingUrl, generateDisablingUrl] as const) {
test("Returns null if no URL matches", () => {
expect(func(unmatchingUrl, siteData)).toBeNull();
test("Returns null if no URL matches", async () => {
expect(await func(unmatchingUrl, siteData)).toBeNull();
});
}

describe("builtin sitedata", () => {
test("Amazon.com activating matched", () => {
expect(
generateActivatingUrl("https://www.amazon.com/s?", builtinSiteData),
).toBe(
`https://www.amazon.com/s?${amazonComParam.key}=${encodeURIComponent(amazonComParam.value)}`,
);
for (const falsyValue of [false, ""]) {
test(`Activating function result is null if option is false ${falsyValue.toString()}`, async () => {
await chrome.storage.local.set({
onOff: { [siteData[0].id]: falsyValue, [siteData[1].id]: falsyValue },
});
expect(await generateActivatingUrl(matching1Url, siteData)).toBeNull();
expect(await generateActivatingUrl(matching2Url, siteData)).toBeNull();
});
}

for (const option of [
["non-object onOff", { onOff: "not an object" }],
["null onOff", { onOff: null }],
["Missing site ID", { onOff: { "random ID": true } }],
] as const) {
test(`Activating function throws if onOff is invalid: ${option[0]}`, async () => {
const localOption: object = option[1];
await chrome.storage.local.set(localOption);
await expect(
generateActivatingUrl(matching1Url, siteData),
).rejects.toThrow("Unexpected onOff options type");
});
}
});

test("Amazon.com disabling matched", () => {
expect(
generateDisablingUrl("https://www.amazon.com/s?rh=", builtinSiteData),
).toBe("https://www.amazon.com/s");
describe("builtin sitedata", () => {
beforeEach(async () => {
vi.restoreAllMocks();
resetBrowserStorage();
await chrome.storage.local.set({
onOff: { [builtinSiteData[0].id]: true },
});
});

test("Amazon.com activating matched", async () => {
expect(
await generateActivatingUrl("https://www.amazon.com/s?", builtinSiteData),
).toBe(
`https://www.amazon.com/s?${amazonComParam.key}=${encodeURIComponent(amazonComParam.value)}`,
);
});

test("Amazon.com disabling matched", () => {
expect(
generateDisablingUrl("https://www.amazon.com/s?rh=", builtinSiteData),
).toBe("https://www.amazon.com/s");
});
});
30 changes: 27 additions & 3 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@

import type { SiteData, SiteDataEntry } from "./site_data";

// TODO: Move this to a utility library.
function isIn<T extends object>(key: PropertyKey, obj: T): key is keyof T {
return key in obj;
}

function getMatchedSite(url: string, siteData: SiteData): SiteDataEntry | null {
const matchedSite = siteData.find((siteDataEntry) =>
siteDataEntry.urlRegex.exec(url),
Expand All @@ -27,13 +32,32 @@ function getMatchedSite(url: string, siteData: SiteData): SiteDataEntry | null {
}

/** Returns the updated URL of the functionality of this extension on a given current URL. */
export function generateActivatingUrl(
export async function generateActivatingUrl(
url: string,
siteData: SiteData,
): string | null {
): Promise<string | null> {
const matchedSite = getMatchedSite(url, siteData);

return matchedSite?.activatingFunc(url) ?? null;
if (matchedSite === null) {
return null;
}

// Don't generate URL if not activated.
const matchedSiteId = matchedSite.id;
const onOffOptions: unknown = (
await chrome.storage.local.get({ onOff: { [matchedSiteId]: true } })
).onOff;
if (
typeof onOffOptions !== "object" ||
onOffOptions === null ||
!isIn(matchedSiteId, onOffOptions)
) {
throw new Error("Unexpected onOff options type");
}

const activated = Boolean(onOffOptions[matchedSiteId]);

return activated ? matchedSite.activatingFunc(url) : null;
}

export function generateDisablingUrl(
Expand Down
5 changes: 4 additions & 1 deletion src/site_data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import { addUrlParam, removeUrlParam } from "./utils";

export interface SiteDataEntry {
id: string;
// Human readable name.
name: string;
// Regular expression to match the website.
urlRegex: RegExp;
Expand All @@ -33,10 +35,11 @@ export type SiteData = SiteDataEntry[];
export const amazonComParam = {
key: "rh",
value: "n:16310101,p_6:ATVPDKIKX0DER",
};
} as const;

export const builtinSiteData = [
{
id: "Amazon.com",
name: "Amazon.com",
urlRegex: /https:\/\/www\.amazon\.com\/s.*/,
disablingFunc: (url: string): string | null =>
Expand Down
10 changes: 5 additions & 5 deletions vitest-setup.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import "@testing-library/jest-dom/vitest";

let syncStorage = {};
let localBrowserStorage = {};

globalThis.chrome = {
storage: {
sync: {
local: {
set: async (items) => {
syncStorage = items;
localBrowserStorage = items;
},
get: async () => {
return syncStorage;
return localBrowserStorage;
},
},
},
Expand All @@ -20,5 +20,5 @@ globalThis.window.alert = (message) => {
};

globalThis.resetBrowserStorage = () => {
syncStorage = {};
localBrowserStorage = {};
};
Loading