feat(browser-extension): add background script tests
This commit is contained in:
parent
edf5eda7dc
commit
aa99a4eaf3
@ -9,15 +9,16 @@ const tsconfig = require('./tsconfig.json');
|
|||||||
*/
|
*/
|
||||||
const config = {
|
const config = {
|
||||||
extensionsToTreatAsEsm: ['.ts', '.tsx'],
|
extensionsToTreatAsEsm: ['.ts', '.tsx'],
|
||||||
moduleNameMapper: pathsToModuleNameMapper(tsconfig.compilerOptions.paths, {
|
moduleNameMapper: {
|
||||||
prefix: '<rootDir>/',
|
...pathsToModuleNameMapper(tsconfig.compilerOptions.paths, { prefix: '<rootDir>/' }),
|
||||||
}),
|
'^url:~assets/(.+).png$': '<rootDir>/mocks/assets/$1.mock.ts',
|
||||||
|
},
|
||||||
preset: 'ts-jest/presets/default-esm',
|
preset: 'ts-jest/presets/default-esm',
|
||||||
setupFiles: ['jest-webextension-mock'],
|
setupFiles: ['./jest.setup.ts'],
|
||||||
testEnvironment: 'jsdom',
|
testEnvironment: 'jsdom',
|
||||||
testRegex: ['^.+\\.test.tsx?$'],
|
testRegex: ['^.+\\.test.tsx?$'],
|
||||||
transform: {
|
transform: {
|
||||||
'^.+.tsx?$': ['ts-jest', { isolatedModules: true }],
|
'^.+.tsx?$': ['ts-jest', { isolatedModules: true, useESM: true }],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
4
packages/browser-extension/jest.setup.ts
Normal file
4
packages/browser-extension/jest.setup.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import 'jest-webextension-mock';
|
||||||
|
import './mocks/chrome.mock';
|
||||||
|
import './mocks/utils/domain.mock';
|
||||||
|
import './mocks/utils/storage.mock';
|
1
packages/browser-extension/mocks/assets/off.mock.ts
Normal file
1
packages/browser-extension/mocks/assets/off.mock.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default 'off-icon';
|
1
packages/browser-extension/mocks/assets/on.mock.ts
Normal file
1
packages/browser-extension/mocks/assets/on.mock.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default 'on-icon';
|
1
packages/browser-extension/mocks/assets/warn.mock.ts
Normal file
1
packages/browser-extension/mocks/assets/warn.mock.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default 'warn-icon';
|
32
packages/browser-extension/mocks/chrome.mock.ts
Normal file
32
packages/browser-extension/mocks/chrome.mock.ts
Normal file
@ -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;
|
6
packages/browser-extension/mocks/utils/domain.mock.ts
Normal file
6
packages/browser-extension/mocks/utils/domain.mock.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { jest } from '@jest/globals';
|
||||||
|
|
||||||
|
jest.mock('~utils/domain', () => ({
|
||||||
|
formatDomainFromURL: jest.fn(() => 'example.com'),
|
||||||
|
validateSupport: jest.fn(),
|
||||||
|
}));
|
9
packages/browser-extension/mocks/utils/storage.mock.ts
Normal file
9
packages/browser-extension/mocks/utils/storage.mock.ts
Normal file
@ -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 }),
|
||||||
|
},
|
||||||
|
}));
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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 });
|
||||||
|
}
|
@ -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 });
|
||||||
|
}
|
@ -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<ExtensionData>('data')) || DEFAULT_EXTENSION_DATA;
|
||||||
|
|
||||||
|
if (!validateSupport(location.hostname, data.exclusions.domains)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = (await storage.get<DomainConfig>(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),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -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 {
|
chrome.contextMenus.onClicked.addListener(onClicked);
|
||||||
DEFAULT_DOMAIN_CONFIG,
|
chrome.runtime.onInstalled.addListener(onInstalled);
|
||||||
DEFAULT_EXTENSION_DATA,
|
chrome.runtime.onStartup.addListener(onStartup);
|
||||||
EXTENSION_MENU_ITEM_ID,
|
chrome.webRequest.onBeforeRequest.addListener(onBeforeRequest as any, { urls: ['<all_urls>'] });
|
||||||
REPORT_MENU_ITEM_ID,
|
chrome.webRequest.onErrorOccurred.addListener(onErrorOccurred, { urls: ['<all_urls>'] });
|
||||||
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<void>}
|
|
||||||
*/
|
|
||||||
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<ExtensionData>('data').then(({ exclusions, rules } = DEFAULT_EXTENSION_DATA) => {
|
|
||||||
if (!validateSupport(location.hostname, exclusions.domains)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
storage.get<DomainConfig>(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: ['<all_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: ['<all_urls>'] }
|
|
||||||
);
|
|
||||||
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
@ -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) }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
@ -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) }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
@ -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 });
|
||||||
|
});
|
||||||
|
});
|
@ -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 });
|
||||||
|
});
|
||||||
|
});
|
@ -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 });
|
||||||
|
});
|
||||||
|
});
|
@ -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 });
|
||||||
|
});
|
||||||
|
});
|
@ -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 });
|
||||||
|
});
|
||||||
|
});
|
@ -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 });
|
||||||
|
});
|
||||||
|
});
|
@ -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', () => {});
|
||||||
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user