From aa99a4eaf3ddfe9fe9836550e3edb05f242b489c Mon Sep 17 00:00:00 2001 From: wanhose Date: Wed, 20 Nov 2024 12:56:44 +0100 Subject: [PATCH] feat(browser-extension): add background script tests --- packages/browser-extension/jest.config.mjs | 11 +- packages/browser-extension/jest.setup.ts | 4 + .../mocks/assets/off.mock.ts | 1 + .../browser-extension/mocks/assets/on.mock.ts | 1 + .../mocks/assets/warn.mock.ts | 1 + .../browser-extension/mocks/chrome.mock.ts | 32 ++++ .../mocks/utils/domain.mock.ts | 6 + .../mocks/utils/storage.mock.ts | 9 + .../callbacks/contextMenus/onClicked.ts | 15 ++ .../callbacks/runtime/onInstalled.ts | 38 ++++ .../background/callbacks/runtime/onStartup.ts | 10 + .../callbacks/webRequest/onBeforeRequest.ts | 32 ++++ .../callbacks/webRequest/onErrorOccurred.ts | 15 ++ .../browser-extension/src/background/index.ts | 138 +------------- .../callbacks/contextMenus/onClicked.test.ts | 31 +++ .../callbacks/runtime/onInstalled.test.ts | 50 +++++ .../callbacks/runtime/onStartup.test.ts | 28 +++ .../webRequest/onBeforeRequest.test.ts | 163 ++++++++++++++++ .../webRequest/onErrorOcurred.test.ts | 52 ++++++ .../background/messages/database/get.test.ts | 73 ++++++++ .../messages/database/refresh.test.ts | 91 +++++++++ .../background/messages/domain/config.test.ts | 176 ++++++++++++++++++ .../extension/updateAvailable.test.ts | 96 ++++++++++ .../messages/extension/updateBadge.test.ts | 56 ++++++ .../messages/extension/updateIcon.test.ts | 156 ++++++++++++++++ .../tests/contents/index.test.ts | 6 +- 26 files changed, 1156 insertions(+), 135 deletions(-) create mode 100644 packages/browser-extension/jest.setup.ts create mode 100644 packages/browser-extension/mocks/assets/off.mock.ts create mode 100644 packages/browser-extension/mocks/assets/on.mock.ts create mode 100644 packages/browser-extension/mocks/assets/warn.mock.ts create mode 100644 packages/browser-extension/mocks/chrome.mock.ts create mode 100644 packages/browser-extension/mocks/utils/domain.mock.ts create mode 100644 packages/browser-extension/mocks/utils/storage.mock.ts create mode 100644 packages/browser-extension/src/background/callbacks/contextMenus/onClicked.ts create mode 100644 packages/browser-extension/src/background/callbacks/runtime/onInstalled.ts create mode 100644 packages/browser-extension/src/background/callbacks/runtime/onStartup.ts create mode 100644 packages/browser-extension/src/background/callbacks/webRequest/onBeforeRequest.ts create mode 100644 packages/browser-extension/src/background/callbacks/webRequest/onErrorOccurred.ts create mode 100644 packages/browser-extension/tests/background/callbacks/contextMenus/onClicked.test.ts create mode 100644 packages/browser-extension/tests/background/callbacks/runtime/onInstalled.test.ts create mode 100644 packages/browser-extension/tests/background/callbacks/runtime/onStartup.test.ts create mode 100644 packages/browser-extension/tests/background/callbacks/webRequest/onBeforeRequest.test.ts create mode 100644 packages/browser-extension/tests/background/callbacks/webRequest/onErrorOcurred.test.ts create mode 100644 packages/browser-extension/tests/background/messages/database/get.test.ts create mode 100644 packages/browser-extension/tests/background/messages/database/refresh.test.ts create mode 100644 packages/browser-extension/tests/background/messages/domain/config.test.ts create mode 100644 packages/browser-extension/tests/background/messages/extension/updateAvailable.test.ts create mode 100644 packages/browser-extension/tests/background/messages/extension/updateBadge.test.ts create mode 100644 packages/browser-extension/tests/background/messages/extension/updateIcon.test.ts diff --git a/packages/browser-extension/jest.config.mjs b/packages/browser-extension/jest.config.mjs index cd8feae..c70f556 100644 --- a/packages/browser-extension/jest.config.mjs +++ b/packages/browser-extension/jest.config.mjs @@ -9,15 +9,16 @@ const tsconfig = require('./tsconfig.json'); */ const config = { extensionsToTreatAsEsm: ['.ts', '.tsx'], - moduleNameMapper: pathsToModuleNameMapper(tsconfig.compilerOptions.paths, { - prefix: '/', - }), + moduleNameMapper: { + ...pathsToModuleNameMapper(tsconfig.compilerOptions.paths, { prefix: '/' }), + '^url:~assets/(.+).png$': '/mocks/assets/$1.mock.ts', + }, preset: 'ts-jest/presets/default-esm', - setupFiles: ['jest-webextension-mock'], + setupFiles: ['./jest.setup.ts'], testEnvironment: 'jsdom', testRegex: ['^.+\\.test.tsx?$'], transform: { - '^.+.tsx?$': ['ts-jest', { isolatedModules: true }], + '^.+.tsx?$': ['ts-jest', { isolatedModules: true, useESM: true }], }, }; diff --git a/packages/browser-extension/jest.setup.ts b/packages/browser-extension/jest.setup.ts new file mode 100644 index 0000000..ff7c1bb --- /dev/null +++ b/packages/browser-extension/jest.setup.ts @@ -0,0 +1,4 @@ +import 'jest-webextension-mock'; +import './mocks/chrome.mock'; +import './mocks/utils/domain.mock'; +import './mocks/utils/storage.mock'; diff --git a/packages/browser-extension/mocks/assets/off.mock.ts b/packages/browser-extension/mocks/assets/off.mock.ts new file mode 100644 index 0000000..52b3a3f --- /dev/null +++ b/packages/browser-extension/mocks/assets/off.mock.ts @@ -0,0 +1 @@ +export default 'off-icon'; diff --git a/packages/browser-extension/mocks/assets/on.mock.ts b/packages/browser-extension/mocks/assets/on.mock.ts new file mode 100644 index 0000000..2eb3107 --- /dev/null +++ b/packages/browser-extension/mocks/assets/on.mock.ts @@ -0,0 +1 @@ +export default 'on-icon'; diff --git a/packages/browser-extension/mocks/assets/warn.mock.ts b/packages/browser-extension/mocks/assets/warn.mock.ts new file mode 100644 index 0000000..38beaef --- /dev/null +++ b/packages/browser-extension/mocks/assets/warn.mock.ts @@ -0,0 +1 @@ +export default 'warn-icon'; diff --git a/packages/browser-extension/mocks/chrome.mock.ts b/packages/browser-extension/mocks/chrome.mock.ts new file mode 100644 index 0000000..cdcde62 --- /dev/null +++ b/packages/browser-extension/mocks/chrome.mock.ts @@ -0,0 +1,32 @@ +import { jest } from '@jest/globals'; + +chrome.action = { + ...chrome.action, + openPopup: jest.fn(), + setBadgeBackgroundColor: jest.fn(), + setBadgeText: jest.fn(), + setIcon: jest.fn(), +} as typeof chrome.action; + +chrome.contextMenus = { + ...chrome.contextMenus, + create: jest.fn(), + removeAll: jest.fn(), +} as typeof chrome.contextMenus; + +chrome.declarativeNetRequest = { + ...chrome.declarativeNetRequest, + updateSessionRules: jest.fn(), +} as typeof chrome.declarativeNetRequest; + +chrome.runtime = { + ...chrome.runtime, + getManifest: jest.fn( + () => + ({ + content_scripts: [{ matches: ['https://example.com/*'] }], + version: '1.0.0', + }) as chrome.runtime.ManifestV3 + ), + openOptionsPage: jest.fn(), +} as typeof chrome.runtime; diff --git a/packages/browser-extension/mocks/utils/domain.mock.ts b/packages/browser-extension/mocks/utils/domain.mock.ts new file mode 100644 index 0000000..4d64f3b --- /dev/null +++ b/packages/browser-extension/mocks/utils/domain.mock.ts @@ -0,0 +1,6 @@ +import { jest } from '@jest/globals'; + +jest.mock('~utils/domain', () => ({ + formatDomainFromURL: jest.fn(() => 'example.com'), + validateSupport: jest.fn(), +})); diff --git a/packages/browser-extension/mocks/utils/storage.mock.ts b/packages/browser-extension/mocks/utils/storage.mock.ts new file mode 100644 index 0000000..526904b --- /dev/null +++ b/packages/browser-extension/mocks/utils/storage.mock.ts @@ -0,0 +1,9 @@ +import { jest } from '@jest/globals'; + +jest.mock('~utils/storage', () => ({ + storage: { + get: async (key: string) => (await chrome.storage.local.get(key))?.[key], + remove: (key: string) => chrome.storage.local.remove(key), + set: (key: string, value: any) => chrome.storage.local.set({ [key]: value }), + }, +})); diff --git a/packages/browser-extension/src/background/callbacks/contextMenus/onClicked.ts b/packages/browser-extension/src/background/callbacks/contextMenus/onClicked.ts new file mode 100644 index 0000000..0e7cad4 --- /dev/null +++ b/packages/browser-extension/src/background/callbacks/contextMenus/onClicked.ts @@ -0,0 +1,15 @@ +import { REPORT_MENU_ITEM_ID, SETTINGS_MENU_ITEM_ID } from '~utils/constants'; +import { suppressLastError } from '~utils/error'; + +export default function onClicked(data: chrome.contextMenus.OnClickData) { + switch (data.menuItemId) { + case REPORT_MENU_ITEM_ID: + chrome.action.openPopup(suppressLastError); + break; + case SETTINGS_MENU_ITEM_ID: + chrome.runtime.openOptionsPage(suppressLastError); + break; + default: + break; + } +} diff --git a/packages/browser-extension/src/background/callbacks/runtime/onInstalled.ts b/packages/browser-extension/src/background/callbacks/runtime/onInstalled.ts new file mode 100644 index 0000000..2fe01fe --- /dev/null +++ b/packages/browser-extension/src/background/callbacks/runtime/onInstalled.ts @@ -0,0 +1,38 @@ +import databaseRefreshHandler from '~background/messages/database/refresh'; +import { + EXTENSION_MENU_ITEM_ID, + REPORT_MENU_ITEM_ID, + SETTINGS_MENU_ITEM_ID, +} from '~utils/constants'; +import { noop } from '~utils/error'; +import { storage } from '~utils/storage'; + +export default async function onInstalled() { + await chrome.contextMenus.removeAll(); + + const documentUrlPatterns = chrome.runtime.getManifest().content_scripts?.[0].matches; + + await chrome.contextMenus.create({ + contexts: ['all'], + documentUrlPatterns, + id: EXTENSION_MENU_ITEM_ID, + title: 'Cookie Dialog Monster', + }); + await chrome.contextMenus.create({ + contexts: ['all'], + documentUrlPatterns, + id: SETTINGS_MENU_ITEM_ID, + parentId: EXTENSION_MENU_ITEM_ID, + title: chrome.i18n.getMessage('contextMenu_settingsOption'), + }); + await chrome.contextMenus.create({ + contexts: ['all'], + documentUrlPatterns, + id: REPORT_MENU_ITEM_ID, + parentId: EXTENSION_MENU_ITEM_ID, + title: chrome.i18n.getMessage('contextMenu_reportOption'), + }); + + await storage.remove('updateAvailable'); + await databaseRefreshHandler({ name: 'database/refresh' }, { send: noop }); +} diff --git a/packages/browser-extension/src/background/callbacks/runtime/onStartup.ts b/packages/browser-extension/src/background/callbacks/runtime/onStartup.ts new file mode 100644 index 0000000..11960a0 --- /dev/null +++ b/packages/browser-extension/src/background/callbacks/runtime/onStartup.ts @@ -0,0 +1,10 @@ +import databaseRefreshHandler from '~background/messages/database/refresh'; +import extensionUpdateAvailableHandler from '~background/messages/extension/updateAvailable'; +import { noop } from '~utils/error'; +import { storage } from '~utils/storage'; + +export default async function onStartup() { + await storage.remove('updateAvailable'); + await databaseRefreshHandler({ name: 'database/refresh' }, { send: noop }); + await extensionUpdateAvailableHandler({ name: 'extension/updateAvailable' }, { send: noop }); +} diff --git a/packages/browser-extension/src/background/callbacks/webRequest/onBeforeRequest.ts b/packages/browser-extension/src/background/callbacks/webRequest/onBeforeRequest.ts new file mode 100644 index 0000000..651d168 --- /dev/null +++ b/packages/browser-extension/src/background/callbacks/webRequest/onBeforeRequest.ts @@ -0,0 +1,32 @@ +import { DEFAULT_DOMAIN_CONFIG, DEFAULT_EXTENSION_DATA } from '~utils/constants'; +import { formatDomainFromURL, validateSupport } from '~utils/domain'; +import { storage } from '~utils/storage'; +import type { DomainConfig, ExtensionData } from '~utils/types'; + +export default async function onBeforeRequest(details: chrome.webRequest.WebRequestBodyDetails) { + const { tabId, type, url } = details; + const location = new URL(url); + const domain = formatDomainFromURL(location); + + if (tabId > -1 && type === 'main_frame') { + const data = (await storage.get('data')) || DEFAULT_EXTENSION_DATA; + + if (!validateSupport(location.hostname, data.exclusions.domains)) { + return; + } + + const config = (await storage.get(domain)) || DEFAULT_DOMAIN_CONFIG; + + if (data.rules.length) { + const rulesWithTabId = data.rules.map((rule) => ({ + ...rule, + condition: { ...rule.condition, tabIds: [tabId] }, + })); + + chrome.declarativeNetRequest.updateSessionRules({ + addRules: config.on ? rulesWithTabId : undefined, + removeRuleIds: data.rules.map((rule) => rule.id), + }); + } + } +} diff --git a/packages/browser-extension/src/background/callbacks/webRequest/onErrorOccurred.ts b/packages/browser-extension/src/background/callbacks/webRequest/onErrorOccurred.ts new file mode 100644 index 0000000..cebe908 --- /dev/null +++ b/packages/browser-extension/src/background/callbacks/webRequest/onErrorOccurred.ts @@ -0,0 +1,15 @@ +import { sendToContentScript } from '@plasmohq/messaging'; + +export default async function onErrorOccurred(details: chrome.webRequest.WebResponseErrorDetails) { + const { error, tabId } = details; + + if (error === 'net::ERR_BLOCKED_BY_CLIENT' && tabId > -1) { + await sendToContentScript({ + body: { + value: error, + }, + name: 'INCREASE_LOG_COUNT', + tabId, + }); + } +} diff --git a/packages/browser-extension/src/background/index.ts b/packages/browser-extension/src/background/index.ts index 1acf49e..f6e5496 100644 --- a/packages/browser-extension/src/background/index.ts +++ b/packages/browser-extension/src/background/index.ts @@ -1,129 +1,11 @@ -import { sendToContentScript } from '@plasmohq/messaging'; +import onClicked from './callbacks/contextMenus/onClicked'; +import onInstalled from './callbacks/runtime/onInstalled'; +import onStartup from './callbacks/runtime/onStartup'; +import onBeforeRequest from './callbacks/webRequest/onBeforeRequest'; +import onErrorOccurred from './callbacks/webRequest/onErrorOccurred'; -import { - DEFAULT_DOMAIN_CONFIG, - DEFAULT_EXTENSION_DATA, - EXTENSION_MENU_ITEM_ID, - REPORT_MENU_ITEM_ID, - SETTINGS_MENU_ITEM_ID, -} from '~utils/constants'; -import { formatDomainFromURL, validateSupport } from '~utils/domain'; -import { noop, suppressLastError } from '~utils/error'; -import { storage } from '~utils/storage'; -import type { DomainConfig, ExtensionData } from '~utils/types'; - -import databaseRefreshHandler from './messages/database/refresh'; -import extensionUpdateAvailableHandler from './messages/extension/updateAvailable'; - -chrome.contextMenus.onClicked.addListener((info) => { - switch (info.menuItemId) { - case REPORT_MENU_ITEM_ID: - chrome.action.openPopup(); - break; - case SETTINGS_MENU_ITEM_ID: - chrome.runtime.openOptionsPage(); - break; - default: - break; - } -}); - -chrome.runtime.onInstalled.addListener(async () => { - chrome.contextMenus.removeAll(() => { - const documentUrlPatterns = chrome.runtime.getManifest().content_scripts?.[0].matches; - - chrome.contextMenus.create( - { - contexts: ['all'], - documentUrlPatterns, - id: EXTENSION_MENU_ITEM_ID, - title: 'Cookie Dialog Monster', - }, - suppressLastError - ); - chrome.contextMenus.create( - { - contexts: ['all'], - documentUrlPatterns, - id: SETTINGS_MENU_ITEM_ID, - parentId: EXTENSION_MENU_ITEM_ID, - title: chrome.i18n.getMessage('contextMenu_settingsOption'), - }, - suppressLastError - ); - chrome.contextMenus.create( - { - contexts: ['all'], - documentUrlPatterns, - id: REPORT_MENU_ITEM_ID, - parentId: EXTENSION_MENU_ITEM_ID, - title: chrome.i18n.getMessage('contextMenu_reportOption'), - }, - suppressLastError - ); - }); - - await storage.remove('updateAvailable'); - await databaseRefreshHandler({ name: 'database/refresh' }, { send: noop }); -}); - -chrome.runtime.onStartup.addListener(async () => { - await storage.remove('updateAvailable'); - await databaseRefreshHandler({ name: 'database/refresh' }, { send: noop }); - await extensionUpdateAvailableHandler({ name: 'extension/updateAvailable' }, { send: noop }); -}); - -/** - * @description Listen to the moment before a request is made to apply the rules - * @returns {Promise} - */ -chrome.webRequest.onBeforeRequest.addListener( - (details) => { - const { tabId, type, url } = details; - const location = new URL(url); - const domain = formatDomainFromURL(location); - - if (tabId > -1 && type === 'main_frame') { - storage.get('data').then(({ exclusions, rules } = DEFAULT_EXTENSION_DATA) => { - if (!validateSupport(location.hostname, exclusions.domains)) { - return; - } - - storage.get(domain).then((config = DEFAULT_DOMAIN_CONFIG) => { - if (rules.length) { - const rulesWithTabId = rules.map((rule) => ({ - ...rule, - condition: { ...rule.condition, tabIds: [tabId] }, - })); - - chrome.declarativeNetRequest.updateSessionRules({ - addRules: config.on ? rulesWithTabId : undefined, - removeRuleIds: rules.map((rule) => rule.id), - }); - } - }); - }); - } - }, - { urls: [''] } -); - -/** - * @description Listen for errors on network requests - */ -chrome.webRequest.onErrorOccurred.addListener( - async (details) => { - const { error, tabId } = details; - - if (error === 'net::ERR_BLOCKED_BY_CLIENT' && tabId > -1) { - await sendToContentScript({ - body: { - value: error, - }, - name: 'INCREASE_LOG_COUNT', - tabId, - }); - } - }, - { urls: [''] } -); +chrome.contextMenus.onClicked.addListener(onClicked); +chrome.runtime.onInstalled.addListener(onInstalled); +chrome.runtime.onStartup.addListener(onStartup); +chrome.webRequest.onBeforeRequest.addListener(onBeforeRequest as any, { urls: [''] }); +chrome.webRequest.onErrorOccurred.addListener(onErrorOccurred, { urls: [''] }); diff --git a/packages/browser-extension/tests/background/callbacks/contextMenus/onClicked.test.ts b/packages/browser-extension/tests/background/callbacks/contextMenus/onClicked.test.ts new file mode 100644 index 0000000..ffc8f52 --- /dev/null +++ b/packages/browser-extension/tests/background/callbacks/contextMenus/onClicked.test.ts @@ -0,0 +1,31 @@ +import { beforeEach, describe, expect, it, jest } from '@jest/globals'; + +import onClicked from '~background/callbacks/contextMenus/onClicked'; +import { REPORT_MENU_ITEM_ID, SETTINGS_MENU_ITEM_ID } from '~utils/constants'; + +describe('background/callbacks/runtime/onClicked.ts', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should call chrome.action.openPopup when REPORT_MENU_ITEM_ID is clicked', () => { + const data = { menuItemId: REPORT_MENU_ITEM_ID } as chrome.contextMenus.OnClickData; + onClicked(data); + expect(chrome.runtime.openOptionsPage).not.toHaveBeenCalled(); + expect(chrome.action.openPopup).toHaveBeenCalled(); + }); + + it('should call chrome.runtime.openOptionsPage when SETTINGS_MENU_ITEM_ID is clicked', () => { + const data = { menuItemId: SETTINGS_MENU_ITEM_ID } as chrome.contextMenus.OnClickData; + onClicked(data); + expect(chrome.runtime.openOptionsPage).toHaveBeenCalled(); + expect(chrome.action.openPopup).not.toHaveBeenCalled(); + }); + + it('should do nothing when an unrecognized menu item ID is clicked', () => { + const data = { menuItemId: 'UNKNOWN_MENU_ITEM_ID' } as chrome.contextMenus.OnClickData; + onClicked(data); + expect(chrome.runtime.openOptionsPage).not.toHaveBeenCalled(); + expect(chrome.action.openPopup).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/browser-extension/tests/background/callbacks/runtime/onInstalled.test.ts b/packages/browser-extension/tests/background/callbacks/runtime/onInstalled.test.ts new file mode 100644 index 0000000..0fb8b25 --- /dev/null +++ b/packages/browser-extension/tests/background/callbacks/runtime/onInstalled.test.ts @@ -0,0 +1,50 @@ +import { beforeEach, describe, expect, it, jest } from '@jest/globals'; + +import onInstalled from '~background/callbacks/runtime/onInstalled'; +import databaseRefreshHandler from '~background/messages/database/refresh'; +import { + EXTENSION_MENU_ITEM_ID, + REPORT_MENU_ITEM_ID, + SETTINGS_MENU_ITEM_ID, +} from '~utils/constants'; + +jest.mock('~background/messages/database/refresh'); + +describe('background/callbacks/runtime/onInstalled.ts', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should remove existing context menus and create new ones', async () => { + const documentUrlPatterns = ['https://example.com/*']; + + await onInstalled(); + + expect(chrome.contextMenus.removeAll).toHaveBeenCalled(); + expect(chrome.contextMenus.create).toHaveBeenNthCalledWith(1, { + contexts: ['all'], + documentUrlPatterns, + id: EXTENSION_MENU_ITEM_ID, + title: 'Cookie Dialog Monster', + }); + expect(chrome.contextMenus.create).toHaveBeenNthCalledWith(2, { + contexts: ['all'], + documentUrlPatterns, + id: SETTINGS_MENU_ITEM_ID, + parentId: EXTENSION_MENU_ITEM_ID, + title: chrome.i18n.getMessage('contextMenu_settingsOption'), + }); + expect(chrome.contextMenus.create).toHaveBeenNthCalledWith(3, { + contexts: ['all'], + documentUrlPatterns, + id: REPORT_MENU_ITEM_ID, + parentId: EXTENSION_MENU_ITEM_ID, + title: chrome.i18n.getMessage('contextMenu_reportOption'), + }); + expect(chrome.storage.local.remove).toHaveBeenCalledWith('updateAvailable'); + expect(databaseRefreshHandler).toHaveBeenCalledWith( + { name: 'database/refresh' }, + { send: expect.any(Function) } + ); + }); +}); diff --git a/packages/browser-extension/tests/background/callbacks/runtime/onStartup.test.ts b/packages/browser-extension/tests/background/callbacks/runtime/onStartup.test.ts new file mode 100644 index 0000000..842a964 --- /dev/null +++ b/packages/browser-extension/tests/background/callbacks/runtime/onStartup.test.ts @@ -0,0 +1,28 @@ +import { beforeEach, describe, expect, it, jest } from '@jest/globals'; + +import onStartup from '~background/callbacks/runtime/onStartup'; +import databaseRefreshHandler from '~background/messages/database/refresh'; +import updateAvailableHandler from '~background/messages/extension/updateAvailable'; + +jest.mock('~background/messages/database/refresh'); +jest.mock('~background/messages/extension/updateAvailable'); + +describe('background/callbacks/runtime/onStartup.ts', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should execute calls in sequence', async () => { + await onStartup(); + + expect(chrome.storage.local.remove).toHaveBeenCalledWith('updateAvailable'); + expect(databaseRefreshHandler).toHaveBeenCalledWith( + { name: 'database/refresh' }, + { send: expect.any(Function) } + ); + expect(updateAvailableHandler).toHaveBeenCalledWith( + { name: 'extension/updateAvailable' }, + { send: expect.any(Function) } + ); + }); +}); diff --git a/packages/browser-extension/tests/background/callbacks/webRequest/onBeforeRequest.test.ts b/packages/browser-extension/tests/background/callbacks/webRequest/onBeforeRequest.test.ts new file mode 100644 index 0000000..48de5d9 --- /dev/null +++ b/packages/browser-extension/tests/background/callbacks/webRequest/onBeforeRequest.test.ts @@ -0,0 +1,163 @@ +import { beforeEach, describe, expect, it, jest } from '@jest/globals'; + +import onBeforeRequest from '~background/callbacks/webRequest/onBeforeRequest'; +import { DEFAULT_DOMAIN_CONFIG } from '~utils/constants'; +import { formatDomainFromURL, validateSupport } from '~utils/domain'; +import type { ExtensionData } from '~utils/types'; + +describe('background/callbacks/webRequest/onBeforeRequest.ts', () => { + beforeEach(() => { + jest.clearAllMocks(); + (validateSupport as jest.Mock).mockReturnValue(true); + }); + + it('should update session rules if conditions are met', async () => { + const data: ExtensionData = { + actions: [ + { + domain: 'jestjs.io', + name: 'click', + selector: '#jest', + }, + ], + exclusions: { + domains: ['*.jestjs.io'], + overflows: ['*.jestjs.io'], + tags: ['JEST'], + }, + keywords: ['jest'], + rules: [ + { + action: { + type: 'block' as chrome.declarativeNetRequest.RuleActionType, + }, + condition: { + resourceTypes: [ + 'font' as chrome.declarativeNetRequest.ResourceType, + 'image' as chrome.declarativeNetRequest.ResourceType, + 'media' as chrome.declarativeNetRequest.ResourceType, + 'object' as chrome.declarativeNetRequest.ResourceType, + 'script' as chrome.declarativeNetRequest.ResourceType, + 'stylesheet' as chrome.declarativeNetRequest.ResourceType, + 'xmlhttprequest' as chrome.declarativeNetRequest.ResourceType, + ], + urlFilter: '||jestjs.io^', + tabIds: [123], + }, + id: 1, + priority: 1, + }, + ], + tokens: { + backdrops: ['#backdrop'], + classes: ['jest'], + containers: ['#container'], + selectors: ['#element'], + }, + version: '0.0.0', + }; + const details: chrome.webRequest.WebRequestBodyDetails = { + tabId: 123, + type: 'main_frame', + url: 'https://example.com/path', + } as chrome.webRequest.WebRequestBodyDetails; + + await chrome.storage.local.set({ data }); + await chrome.storage.local.set({ 'example.com': DEFAULT_DOMAIN_CONFIG }); + await onBeforeRequest(details); + + expect(chrome.storage.local.get).toHaveBeenNthCalledWith(1, 'data'); + expect(chrome.storage.local.get).toHaveBeenNthCalledWith(2, 'example.com'); + expect(formatDomainFromURL).toHaveBeenCalledWith(new URL(details.url)); + expect(validateSupport).toHaveBeenCalledWith('example.com', ['*.jestjs.io']); + expect(chrome.declarativeNetRequest.updateSessionRules).toHaveBeenCalledWith({ + addRules: [data.rules[0]], + removeRuleIds: [data.rules[0].id], + }); + }); + + it('should not update session rules if config.on is false', async () => { + const details: chrome.webRequest.WebRequestBodyDetails = { + tabId: 123, + type: 'main_frame', + url: 'https://example.com/path', + } as chrome.webRequest.WebRequestBodyDetails; + + (validateSupport as jest.Mock).mockReturnValue(false); + + await onBeforeRequest(details); + + expect(chrome.declarativeNetRequest.updateSessionRules).not.toHaveBeenCalled(); + }); + + it('should not update session rules if validateSupport returns false', async () => { + const data: ExtensionData = { + actions: [ + { + domain: 'jestjs.io', + name: 'click', + selector: '#jest', + }, + ], + exclusions: { + domains: ['*.jestjs.io'], + overflows: ['*.jestjs.io'], + tags: ['JEST'], + }, + keywords: ['jest'], + rules: [ + { + action: { + type: 'block' as chrome.declarativeNetRequest.RuleActionType, + }, + condition: { + resourceTypes: [ + 'font' as chrome.declarativeNetRequest.ResourceType, + 'image' as chrome.declarativeNetRequest.ResourceType, + 'media' as chrome.declarativeNetRequest.ResourceType, + 'object' as chrome.declarativeNetRequest.ResourceType, + 'script' as chrome.declarativeNetRequest.ResourceType, + 'stylesheet' as chrome.declarativeNetRequest.ResourceType, + 'xmlhttprequest' as chrome.declarativeNetRequest.ResourceType, + ], + urlFilter: '||jestjs.io^', + tabIds: [123], + }, + id: 1, + priority: 1, + }, + ], + tokens: { + backdrops: ['#backdrop'], + classes: ['jest'], + containers: ['#container'], + selectors: ['#element'], + }, + version: '0.0.0', + }; + const details: chrome.webRequest.WebRequestBodyDetails = { + tabId: 123, + type: 'main_frame', + url: 'https://example.com/path', + } as chrome.webRequest.WebRequestBodyDetails; + + await chrome.storage.local.set({ 'example.com': { ...DEFAULT_DOMAIN_CONFIG, on: false } }); + await onBeforeRequest(details); + + expect(chrome.declarativeNetRequest.updateSessionRules).toHaveBeenCalledWith({ + addRules: undefined, + removeRuleIds: [data.rules[0].id], + }); + }); + + it('should not process if tabId is invalid or type is not "main_frame"', async () => { + let details = {} as chrome.webRequest.WebRequestBodyDetails; + + details = { ...details, tabId: -1, type: 'main_frame', url: 'https://example.com' }; + await onBeforeRequest(details); + details = { ...details, tabId: 123, type: 'sub_frame', url: 'https://example.com' }; + await onBeforeRequest(details); + + expect(chrome.declarativeNetRequest.updateSessionRules).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/browser-extension/tests/background/callbacks/webRequest/onErrorOcurred.test.ts b/packages/browser-extension/tests/background/callbacks/webRequest/onErrorOcurred.test.ts new file mode 100644 index 0000000..447f7b2 --- /dev/null +++ b/packages/browser-extension/tests/background/callbacks/webRequest/onErrorOcurred.test.ts @@ -0,0 +1,52 @@ +import { beforeEach, describe, expect, it, jest } from '@jest/globals'; +import { sendToContentScript } from '@plasmohq/messaging'; + +import onErrorOccurred from '~background/callbacks/webRequest/onErrorOccurred'; + +jest.mock('@plasmohq/messaging', () => ({ sendToContentScript: jest.fn() })); + +describe('background/callbacks/webRequest/onErrorOcurred.ts', () => { + beforeEach(() => { + jest.clearAllMocks(); // Clear previous mocks before each test + }); + + it('should call sendToContentScript when error is "net::ERR_BLOCKED_BY_CLIENT" and tabId is valid', async () => { + const details = { + error: 'net::ERR_BLOCKED_BY_CLIENT', + tabId: 1, + } as chrome.webRequest.WebResponseErrorDetails; + + await onErrorOccurred(details); + + expect(sendToContentScript).toHaveBeenCalledTimes(1); + expect(sendToContentScript).toHaveBeenCalledWith({ + body: { + value: 'net::ERR_BLOCKED_BY_CLIENT', + }, + name: 'INCREASE_LOG_COUNT', + tabId: 1, + }); + }); + + it('should NOT call sendToContentScript when error is not "net::ERR_BLOCKED_BY_CLIENT"', async () => { + const details = { + error: 'net::ERR_FAILED', + tabId: 1, + } as chrome.webRequest.WebResponseErrorDetails; + + await onErrorOccurred(details); + + expect(sendToContentScript).not.toHaveBeenCalled(); + }); + + it('should NOT call sendToContentScript when tabId is invalid (less than 0)', async () => { + const details = { + error: 'net::ERR_BLOCKED_BY_CLIENT', + tabId: -1, + } as chrome.webRequest.WebResponseErrorDetails; + + await onErrorOccurred(details); + + expect(sendToContentScript).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/browser-extension/tests/background/messages/database/get.test.ts b/packages/browser-extension/tests/background/messages/database/get.test.ts new file mode 100644 index 0000000..29225e6 --- /dev/null +++ b/packages/browser-extension/tests/background/messages/database/get.test.ts @@ -0,0 +1,73 @@ +import { beforeEach, describe, expect, it, jest } from '@jest/globals'; + +import handler from '~background/messages/database/get'; +import { DEFAULT_EXTENSION_DATA } from '~utils/constants'; +import type { ExtensionData } from '~utils/types'; + +describe('background/messages/database/get.ts', () => { + const req = { name: 'database/get' as const }; + const res = { send: jest.fn() }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return default data if no data is in storage', async () => { + await handler(req, res); + + expect(chrome.storage.local.get).toHaveBeenCalledWith('data'); + expect(res.send).toHaveBeenCalledWith({ data: DEFAULT_EXTENSION_DATA, success: true }); + }); + + it('should return stored data if it exists in storage', async () => { + const data: ExtensionData = { + actions: [ + { + domain: 'jestjs.io', + name: 'click', + selector: '#jest', + }, + ], + exclusions: { + domains: ['*.jestjs.io'], + overflows: ['*.jestjs.io'], + tags: ['JEST'], + }, + keywords: ['jest'], + rules: [ + { + action: { + type: 'block' as chrome.declarativeNetRequest.RuleActionType, + }, + condition: { + resourceTypes: [ + 'font' as chrome.declarativeNetRequest.ResourceType, + 'image' as chrome.declarativeNetRequest.ResourceType, + 'media' as chrome.declarativeNetRequest.ResourceType, + 'object' as chrome.declarativeNetRequest.ResourceType, + 'script' as chrome.declarativeNetRequest.ResourceType, + 'stylesheet' as chrome.declarativeNetRequest.ResourceType, + 'xmlhttprequest' as chrome.declarativeNetRequest.ResourceType, + ], + urlFilter: '||jestjs.io^', + }, + id: 1, + priority: 1, + }, + ], + tokens: { + backdrops: ['#backdrop'], + classes: ['jest'], + containers: ['#container'], + selectors: ['#element'], + }, + version: '0.0.0', + }; + + await chrome.storage.local.set({ data }); + await handler(req, res); + + expect(chrome.storage.local.get).toHaveBeenCalledWith('data'); + expect(res.send).toHaveBeenCalledWith({ data, success: true }); + }); +}); diff --git a/packages/browser-extension/tests/background/messages/database/refresh.test.ts b/packages/browser-extension/tests/background/messages/database/refresh.test.ts new file mode 100644 index 0000000..1895901 --- /dev/null +++ b/packages/browser-extension/tests/background/messages/database/refresh.test.ts @@ -0,0 +1,91 @@ +import { beforeEach, describe, expect, it, jest } from '@jest/globals'; + +import handler from '~background/messages/database/refresh'; +import { API_URL } from '~utils/constants'; +import type { ExtensionData } from '~utils/types'; + +describe('background/messages/database/refresh.ts', () => { + const req = { name: 'database/refresh' as const }; + const res = { send: jest.fn() }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should fetch data from the API, store it, and return the data when successful', async () => { + const data: ExtensionData = { + actions: [ + { + domain: 'jestjs.io', + name: 'click', + selector: '#jest', + }, + ], + exclusions: { + domains: ['*.jestjs.io'], + overflows: ['*.jestjs.io'], + tags: ['JEST'], + }, + keywords: ['jest'], + rules: [ + { + action: { + type: 'block' as chrome.declarativeNetRequest.RuleActionType, + }, + condition: { + resourceTypes: [ + 'font' as chrome.declarativeNetRequest.ResourceType, + 'image' as chrome.declarativeNetRequest.ResourceType, + 'media' as chrome.declarativeNetRequest.ResourceType, + 'object' as chrome.declarativeNetRequest.ResourceType, + 'script' as chrome.declarativeNetRequest.ResourceType, + 'stylesheet' as chrome.declarativeNetRequest.ResourceType, + 'xmlhttprequest' as chrome.declarativeNetRequest.ResourceType, + ], + urlFilter: '||jestjs.io^', + }, + id: 1, + priority: 1, + }, + ], + tokens: { + backdrops: ['#backdrop'], + classes: ['jest'], + containers: ['#container'], + selectors: ['#element'], + }, + version: '0.0.0', + }; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + global.fetch = jest.fn(() => + Promise.resolve({ + json: () => Promise.resolve({ data, success: true }), + status: 200, + }) + ); + + await handler(req, res); + + expect(global.fetch).toHaveBeenCalledWith(`${API_URL}/data/`); + expect(chrome.storage.local.set).toHaveBeenCalledWith({ data }); + expect(res.send).toHaveBeenCalledWith({ data, success: true }); + }); + + it('should return success: false if the API call fails', async () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + global.fetch = jest.fn(() => + Promise.resolve({ + json: () => Promise.resolve({ success: false }), + status: 200, + }) + ); + await handler(req, res); + + expect(global.fetch).toHaveBeenCalledWith(`${API_URL}/data/`); + expect(chrome.storage.local.set).not.toHaveBeenCalled(); + expect(res.send).toHaveBeenCalledWith({ success: false }); + }); +}); diff --git a/packages/browser-extension/tests/background/messages/domain/config.test.ts b/packages/browser-extension/tests/background/messages/domain/config.test.ts new file mode 100644 index 0000000..96f55a3 --- /dev/null +++ b/packages/browser-extension/tests/background/messages/domain/config.test.ts @@ -0,0 +1,176 @@ +import { beforeEach, describe, expect, it, jest } from '@jest/globals'; + +import handler from '~background/messages/domain/config'; +import { API_URL } from '~utils/constants'; +import type { DomainConfig } from '~utils/types'; + +describe('background/messages/domain/config.ts', () => { + const body = { domain: 'example.com' }; + const req = { name: 'domain/config' as const }; + const res = { send: jest.fn() }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return success: false when domain is missing in request', async () => { + await handler(req, res); + + expect(res.send).toHaveBeenCalledWith({ success: false }); + }); + + it('should return cached data when issue is still valid', async () => { + const config: DomainConfig = { + issue: { + expiresAt: Date.now() + 8 * 60 * 60 * 1000, + flags: ['jest'], + url: 'https://jestjs.io/', + }, + on: false, + }; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + global.fetch = jest.fn(() => + Promise.resolve({ + json: () => + Promise.resolve({ + data: { + flags: config.issue?.flags, + url: config.issue?.url, + }, + success: true, + }), + status: 200, + }) + ); + + await chrome.storage.local.set({ 'example.com': config }); + await handler({ ...req, body }, res); + + expect(chrome.storage.local.get).toHaveBeenCalledWith('example.com'); + expect(res.send).toHaveBeenCalledWith({ data: config, success: true }); + expect(global.fetch).not.toHaveBeenCalled(); + }); + + it('should fetch new issue data if issue is expired', async () => { + const previous: DomainConfig = { + issue: { + expiresAt: Date.now() - 8 * 60 * 60 * 1000, + flags: ['jest'], + url: 'https://jestjs.io/', + }, + on: false, + }; + const next: DomainConfig = { + issue: { + expiresAt: Date.now() + 8 * 60 * 60 * 1000, + flags: ['jest'], + url: 'https://jestjs.io/', + }, + on: false, + }; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + global.fetch = jest.fn(() => + Promise.resolve({ + json: () => + Promise.resolve({ + data: { + flags: previous.issue?.flags, + url: previous.issue?.url, + }, + success: true, + }), + status: 200, + }) + ); + + await chrome.storage.local.set({ 'example.com': previous }); + await handler({ ...req, body }, res); + + expect(chrome.storage.local.get).toHaveBeenCalledWith('example.com'); + expect(global.fetch).toHaveBeenCalledWith(`${API_URL}/issues/example.com/`); + expect(chrome.storage.local.set).toHaveBeenCalledWith({ 'example.com': next }); + expect(res.send).toHaveBeenCalledWith({ data: next, success: true }); + }); + + it('should handle rate-limiting gracefully (HTTP 429)', async () => { + const previous: DomainConfig = { + issue: { + expiresAt: Date.now() - 8 * 60 * 60 * 1000, + flags: ['jest'], + url: 'https://jestjs.io/', + }, + on: false, + }; + const next: DomainConfig = { + issue: { + expiresAt: Date.now() + 8 * 60 * 60 * 1000, + flags: ['jest'], + url: 'https://jestjs.io/', + }, + on: false, + }; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + global.fetch = jest.fn(() => + Promise.resolve({ + json: () => + Promise.resolve({ + errors: [], + success: false, + }), + status: 429, + }) + ); + + await chrome.storage.local.set({ 'example.com': previous }); + await handler({ ...req, body }, res); + + expect(chrome.storage.local.get).toHaveBeenCalledWith('example.com'); + expect(global.fetch).toHaveBeenCalledWith(`${API_URL}/issues/example.com/`); + expect(chrome.storage.local.set).not.toHaveBeenCalledWith({ 'example.com': next }); + expect(res.send).toHaveBeenCalledWith({ data: previous, success: true }); + }); + + it('should fallback to a 24-hour expiration if API returns non-success response', async () => { + const previous: DomainConfig = { + issue: { + expiresAt: Date.now() - 8 * 60 * 60 * 1000, + flags: ['jest'], + url: 'https://jestjs.io/', + }, + on: false, + }; + const next: DomainConfig = { + issue: { + expiresAt: Date.now() + 24 * 60 * 60 * 1000, + }, + on: false, + }; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + global.fetch = jest.fn(() => + Promise.resolve({ + json: () => + Promise.resolve({ + errors: [], + success: false, + }), + status: 200, + }) + ); + + await chrome.storage.local.set({ 'example.com': previous }); + await handler({ ...req, body }, res); + + expect(chrome.storage.local.get).toHaveBeenCalledWith('example.com'); + expect(global.fetch).toHaveBeenCalledWith(`${API_URL}/issues/example.com/`); + expect(chrome.storage.local.set).toHaveBeenCalledWith({ 'example.com': next }); + expect(res.send).toHaveBeenCalledWith({ data: next, success: true }); + }); +}); diff --git a/packages/browser-extension/tests/background/messages/extension/updateAvailable.test.ts b/packages/browser-extension/tests/background/messages/extension/updateAvailable.test.ts new file mode 100644 index 0000000..6c6eacb --- /dev/null +++ b/packages/browser-extension/tests/background/messages/extension/updateAvailable.test.ts @@ -0,0 +1,96 @@ +import { beforeEach, describe, expect, it, jest } from '@jest/globals'; + +import handler from '~background/messages/extension/updateAvailable'; +import { API_URL } from '~utils/constants'; + +describe('background/messages/extension/updateAvailable.ts', () => { + const req = { name: 'extension/updateAvailable' as const }; + const res = { send: jest.fn() }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should set updateAvailable if next version differs from current version', async () => { + // const current = '1.0.0'; + const next = '2.0.0'; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + global.fetch = jest.fn(() => + Promise.resolve({ + json: () => + Promise.resolve({ + data: next, + success: true, + }), + status: 200, + }) + ); + + await handler(req, res); + + expect(global.fetch).toHaveBeenCalledWith(`${API_URL}/version/`); + expect(chrome.storage.local.set).toHaveBeenCalledWith({ updateAvailable: next }); + expect(res.send).toHaveBeenCalledWith({ success: true }); + }); + + it('should remove updateAvailable if next version matches current version', async () => { + // const current = '1.0.0'; + const next = '1.0.0'; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + global.fetch = jest.fn(() => + Promise.resolve({ + json: () => + Promise.resolve({ + data: next, + success: true, + }), + status: 200, + }) + ); + + await handler(req, res); + + expect(global.fetch).toHaveBeenCalledWith(`${API_URL}/version/`); + expect(chrome.storage.local.remove).toHaveBeenCalledWith('updateAvailable'); + expect(res.send).toHaveBeenCalledWith({ success: false }); + }); + + it('should return success: false if server responds with HTTP 429', async () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + global.fetch = jest.fn(() => + Promise.resolve({ + json: () => + Promise.resolve({ + errors: [], + success: false, + }), + status: 429, + }) + ); + + await handler(req, res); + + expect(global.fetch).toHaveBeenCalledWith(`${API_URL}/version/`); + expect(chrome.storage.local.set).not.toHaveBeenCalled(); + expect(chrome.storage.local.remove).not.toHaveBeenCalled(); + expect(res.send).toHaveBeenCalledWith({ success: false }); + }); + + it('should return success: false if an error occurs during fetch', async () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + global.fetch = jest.fn(() => Promise.resolve(new Error())); + + await handler(req, res); + + expect(global.fetch).toHaveBeenCalledWith(`${API_URL}/version/`); + expect(chrome.storage.local.set).not.toHaveBeenCalled(); + expect(chrome.storage.local.remove).not.toHaveBeenCalled(); + expect(res.send).toHaveBeenCalledWith({ success: false }); + }); +}); diff --git a/packages/browser-extension/tests/background/messages/extension/updateBadge.test.ts b/packages/browser-extension/tests/background/messages/extension/updateBadge.test.ts new file mode 100644 index 0000000..dedd0f7 --- /dev/null +++ b/packages/browser-extension/tests/background/messages/extension/updateBadge.test.ts @@ -0,0 +1,56 @@ +import { beforeEach, describe, expect, it, jest } from '@jest/globals'; + +import handler from '~background/messages/extension/updateBadge'; + +describe('background/messages/extension/updateBadge.ts', () => { + const req = { name: 'extension/updateBadge' as const }; + const res = { send: jest.fn() }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should set badge text and color when frameId is 0 and tabId exists', async () => { + const body = { value: 50 }; + const sender = { frameId: 0, tab: { id: 123 } } as chrome.runtime.MessageSender; + + await handler({ ...req, body, sender }, res); + + expect(chrome.action.setBadgeBackgroundColor).toHaveBeenCalledWith({ color: '#6b7280' }); + expect(chrome.action.setBadgeText).toHaveBeenCalledWith({ tabId: 123, text: '50' }); + expect(res.send).toHaveBeenCalledWith({ success: true }); + }); + + it('should clear badge text if value is zero', async () => { + const body = { value: 0 }; + const sender = { frameId: 0, tab: { id: 456 } } as chrome.runtime.MessageSender; + + await handler({ ...req, body, sender }, res); + + expect(chrome.action.setBadgeBackgroundColor).toHaveBeenCalledWith({ color: '#6b7280' }); + expect(chrome.action.setBadgeText).toHaveBeenCalledWith({ tabId: 456, text: '' }); + expect(res.send).toHaveBeenCalledWith({ success: true }); + }); + + it('should return success: false if frameId is not 0', async () => { + const body = { value: 50 }; + const sender = { frameId: 1, tab: { id: 123 } } as chrome.runtime.MessageSender; + + await handler({ ...req, body, sender }, res); + + expect(chrome.action.setBadgeBackgroundColor).not.toHaveBeenCalledWith(); + expect(chrome.action.setBadgeText).not.toHaveBeenCalledWith(); + expect(res.send).toHaveBeenCalledWith({ success: false }); + }); + + it('should return success: false if tabId is undefined', async () => { + const body = { value: 50 }; + const sender = { frameId: 1, tab: undefined } as chrome.runtime.MessageSender; + + await handler({ ...req, body, sender }, res); + + expect(chrome.action.setBadgeBackgroundColor).not.toHaveBeenCalledWith(); + expect(chrome.action.setBadgeText).not.toHaveBeenCalledWith(); + expect(res.send).toHaveBeenCalledWith({ success: false }); + }); +}); diff --git a/packages/browser-extension/tests/background/messages/extension/updateIcon.test.ts b/packages/browser-extension/tests/background/messages/extension/updateIcon.test.ts new file mode 100644 index 0000000..33e9385 --- /dev/null +++ b/packages/browser-extension/tests/background/messages/extension/updateIcon.test.ts @@ -0,0 +1,156 @@ +import { beforeEach, describe, expect, it, jest } from '@jest/globals'; + +import handler from '~background/messages/extension/updateIcon'; +import { validateSupport } from '~utils/domain'; + +describe('background/messages/extension/updateIcon.ts', () => { + const req = { name: 'extension/updateIcon' as const }; + const res = { send: jest.fn() }; + + beforeEach(() => { + jest.clearAllMocks(); + (validateSupport as jest.Mock).mockReturnValue(true); + }); + + it('should set warnIcon when config is on, tab.url is valid, and issue URL exists', async () => { + const body = { domain: 'example.com' }; + const sender = { + frameId: 0, + tab: { + id: 123, + url: 'https://example.com/page', + }, + } as chrome.runtime.MessageSender; + + await chrome.storage.local.set({ + 'example.com': { + on: true, + issue: { + expiresAt: Date.now() + 8 * 60 * 60 * 1000, + flags: ['jest'], + url: 'https://example.com/issue', + }, + }, + }); + await handler({ ...req, body, sender }, res); + + expect(chrome.action.setIcon).toHaveBeenCalledWith({ path: 'warn-icon', tabId: 123 }); + expect(res.send).toHaveBeenCalledWith({ success: true }); + }); + + it('should set onIcon when config is on, tab.url is valid, and no issue URL exists', async () => { + const body = { domain: 'example.com' }; + const sender = { + frameId: 0, + tab: { + id: 123, + url: 'https://example.com/page', + }, + } as chrome.runtime.MessageSender; + + await chrome.storage.local.set({ + 'example.com': { + on: true, + issue: { + expiresAt: Date.now() + 24 * 60 * 60 * 1000, + }, + }, + }); + await handler({ ...req, body, sender }, res); + + expect(chrome.action.setIcon).toHaveBeenCalledWith({ path: 'on-icon', tabId: 123 }); + expect(res.send).toHaveBeenCalledWith({ success: true }); + }); + + it('should set offIcon when config is off or tab.url is not valid', async () => { + const body = { domain: 'example.com' }; + const sender = { + frameId: 0, + tab: { + id: 123, + url: 'https://example.com/page', + }, + } as chrome.runtime.MessageSender; + + await chrome.storage.local.set({ + 'example.com': { + on: false, + issue: { + expiresAt: Date.now() + 24 * 60 * 60 * 1000, + }, + }, + }); + await handler({ ...req, body, sender }, res); + + expect(chrome.action.setIcon).toHaveBeenCalledWith({ path: 'off-icon', tabId: 123 }); + expect(res.send).toHaveBeenCalledWith({ success: true }); + }); + + it('should set offIcon when config is on but validateSupport is false', async () => { + const body = { domain: 'example.com' }; + const sender = { + frameId: 0, + tab: { + id: 123, + url: 'https://example.com/page', + }, + } as chrome.runtime.MessageSender; + + (validateSupport as jest.Mock).mockReturnValue(false); + + await chrome.storage.local.set({ + 'example.com': { + on: true, + issue: { + expiresAt: Date.now() + 24 * 60 * 60 * 1000, + }, + }, + }); + await handler({ ...req, body, sender }, res); + + expect(chrome.action.setIcon).toHaveBeenCalledWith({ path: 'off-icon', tabId: 123 }); + expect(res.send).toHaveBeenCalledWith({ success: true }); + }); + + it('should return success: false when frameId is not 0', async () => { + const body = { domain: 'example.com' }; + const sender = { + frameId: 1, + tab: { + id: 123, + url: 'https://example.com/page', + }, + } as chrome.runtime.MessageSender; + + await chrome.storage.local.set({ + 'example.com': { + on: false, + issue: { + expiresAt: Date.now() + 24 * 60 * 60 * 1000, + }, + }, + }); + await handler({ ...req, body, sender }, res); + + expect(chrome.action.setIcon).not.toHaveBeenCalled(); + expect(res.send).toHaveBeenCalledWith({ success: false }); + }); + + it('should return success: false when tab.id is undefined', async () => { + const body = { domain: 'example.com' }; + const sender = { frameId: 0, tab: undefined } as chrome.runtime.MessageSender; + + await chrome.storage.local.set({ + 'example.com': { + on: false, + issue: { + expiresAt: Date.now() + 24 * 60 * 60 * 1000, + }, + }, + }); + await handler({ ...req, body, sender }, res); + + expect(chrome.action.setIcon).not.toHaveBeenCalled(); + expect(res.send).toHaveBeenCalledWith({ success: false }); + }); +}); diff --git a/packages/browser-extension/tests/contents/index.test.ts b/packages/browser-extension/tests/contents/index.test.ts index 5e3f783..9937cae 100644 --- a/packages/browser-extension/tests/contents/index.test.ts +++ b/packages/browser-extension/tests/contents/index.test.ts @@ -1,3 +1,5 @@ -import { describe } from '@jest/globals'; +import { describe, it } from '@jest/globals'; -describe('contents/index.ts', () => {}); +describe('contents/index.ts', () => { + it.skip('temp', () => {}); +});