diff --git a/packages/fake-browser/src/apis/action.test.ts b/packages/fake-browser/src/apis/action.test.ts new file mode 100644 index 0000000..d991531 --- /dev/null +++ b/packages/fake-browser/src/apis/action.test.ts @@ -0,0 +1,92 @@ +import { beforeEach, describe, expect, it } from 'vitest'; + +// Import your fake browser implementation +import { fakeBrowser } from '..'; + +describe('Fake Action API', () => { + beforeEach(() => { + fakeBrowser.reset(); + }); + + describe('setTitle / getTitle', () => { + it('should set and get global title', async () => { + const title = 'Test Title'; + await fakeBrowser.action.setTitle({ title }); + const result = await fakeBrowser.action.getTitle({}); + expect(result).toBe(title); + }); + + it('should set and get tab-specific title', async () => { + const tabId = 123; + const title = 'Tab Title'; + await fakeBrowser.action.setTitle({ tabId, title }); + const result = await fakeBrowser.action.getTitle({ tabId }); + expect(result).toBe(title); + }); + }); + + describe('setBadgeText / getBadgeText', () => { + it('should set and get global badge text', async () => { + const text = '10'; + await fakeBrowser.action.setBadgeText({ text }); + const result = await fakeBrowser.action.getBadgeText({}); + expect(result).toBe(text); + }); + + it('should set and get tab-specific badge text', async () => { + const tabId = 123; + const text = '99+'; + await fakeBrowser.action.setBadgeText({ tabId, text }); + const result = await fakeBrowser.action.getBadgeText({ tabId }); + expect(result).toBe(text); + }); + + it('should return empty string if no badge text is set', async () => { + const result = await fakeBrowser.action.getBadgeText({}); + expect(result).toBe(''); + }); + }); + + describe('setBadgeBackgroundColor / getBadgeBackgroundColor', () => { + it('should set and get global badge background color', async () => { + const color = '#FF0000'; // red + await fakeBrowser.action.setBadgeBackgroundColor({ color }); + const result = await fakeBrowser.action.getBadgeBackgroundColor({}); + expect(result).toEqual([255, 0, 0, 255]); + }); + + it('should set and get tab-specific badge background color', async () => { + const tabId = 123; + const color = '#00FF00'; // green + await fakeBrowser.action.setBadgeBackgroundColor({ tabId, color }); + const result = await fakeBrowser.action.getBadgeBackgroundColor({ tabId }); + expect(result).toEqual([0, 255, 0, 255]); + }); + + it('should fallback to default black if color not set', async () => { + const result = await fakeBrowser.action.getBadgeBackgroundColor({}); + expect(result).toEqual([95, 93, 91, 255]); + }); + }); + + describe('setBadgeTextColor / getBadgeTextColor', () => { + it('should set and get global badge text color', async () => { + const color = '#0000FF'; + fakeBrowser.action.setBadgeTextColor({ color }); + //@ts-ignore + fakeBrowser.action.getBadgeTextColor({}, result => { + expect(result).toEqual(color); + }); + }); + + it('should set and get tab-specific badge text color', async () => { + const tabId = 123; + const color = '#00FFFF'; + fakeBrowser.action.setBadgeTextColor({ tabId, color }); + //@ts-ignore + fakeBrowser.action.getBadgeTextColor({ tabId }, result => { + expect(result).toBe(color); + }); + }); + }); +}); diff --git a/packages/fake-browser/src/apis/action.ts b/packages/fake-browser/src/apis/action.ts new file mode 100644 index 0000000..cd250be --- /dev/null +++ b/packages/fake-browser/src/apis/action.ts @@ -0,0 +1,182 @@ +import { Action, Tabs } from 'webextension-polyfill'; +import { BrowserOverrides } from '../types'; +import { defineEventWithTrigger } from '../utils/defineEventWithTrigger'; + +const onClicked = + defineEventWithTrigger<(tab: Tabs.Tab, info: Action.OnClickData | undefined) => void>(); + +let DEFAULT_BADGE_BACKGROUND_COLOR = '#5F5D5B'; +let DEFAULT_BADGE_TEXT_COLOR = '#FFFFFF'; +type ColorArray = [number, number, number, number]; +const badgeTextColorState: ScopedState = { + global: DEFAULT_BADGE_TEXT_COLOR, + tabs: new Map(), + windows: new Map(), +}; + +interface ScopedState { + global?: T; + tabs: Map; + windows: Map; +} + +const badgeTextState: ScopedState = { + global: '', + tabs: new Map(), + windows: new Map(), +}; + +const badgeBackgroundColorState: ScopedState = { + global: hexToRgbaArray(DEFAULT_BADGE_BACKGROUND_COLOR), + tabs: new Map(), + windows: new Map(), +}; + +const titleState: ScopedState = { + global: '', + tabs: new Map(), + windows: new Map(), +}; + +function hexToRgbaArray(hex: string): ColorArray { + hex = hex.replace('#', ''); + let hasAlpha = hex.length === 8; + let bigint = parseInt(hex, 16); + let r = (bigint >> (hasAlpha ? 24 : 16)) & 255; + let g = (bigint >> (hasAlpha ? 16 : 8)) & 255; + let b = (bigint >> (hasAlpha ? 8 : 0)) & 255; + let a = hasAlpha ? bigint & 255 : 255; + return [r, g, b, a]; +} + +function getScopedValue(state: ScopedState, details?: Action.Details): T | undefined { + if (!details) return state.global; + const { tabId, windowId } = details; + if (tabId !== undefined) return state.tabs.get(tabId); + if (windowId !== undefined) return state.windows.get(windowId); + return state.global; +} + +export const action: BrowserOverrides['action'] = { + resetState() { + onClicked.removeAllListeners(); + badgeTextState.global = ''; + badgeTextState.tabs.clear(); + badgeTextState.windows.clear(); + + badgeBackgroundColorState.global = hexToRgbaArray(DEFAULT_BADGE_BACKGROUND_COLOR); + badgeBackgroundColorState.tabs.clear(); + badgeBackgroundColorState.windows.clear(); + + titleState.global = ''; + titleState.tabs.clear(); + titleState.windows.clear(); + }, + + setTitle(details: Action.SetTitleDetailsType) { + const { title, tabId, windowId } = details; + if (tabId !== undefined) { + if (title === null || title === undefined) { + titleState.tabs.delete(tabId); + } else { + titleState.tabs.set(tabId, title); + } + } else if (windowId !== undefined) { + if (title === null || title === undefined) { + titleState.windows.delete(windowId); + } else { + titleState.windows.set(windowId, title); + } + } else { + titleState.global = title ?? ''; + } + return Promise.resolve(); + }, + + getTitle(details: Action.Details): Promise { + const value = getScopedValue(titleState, details); + return Promise.resolve(value ?? ''); + }, + + setBadgeText(details: Action.SetBadgeTextDetailsType) { + const { text, tabId, windowId } = details; + if (tabId !== undefined) { + if (text === null || text === undefined) { + badgeTextState.tabs.delete(tabId); + } else { + badgeTextState.tabs.set(tabId, text); + } + } else if (windowId !== undefined) { + if (text === null || text === undefined) { + badgeTextState.windows.delete(windowId); + } else { + badgeTextState.windows.set(windowId, text); + } + } else { + badgeTextState.global = text ?? ''; + } + return Promise.resolve(); + }, + + getBadgeText(details: Action.Details): Promise { + const value = getScopedValue(badgeTextState, details); + return Promise.resolve(value ?? ''); + }, + + setBadgeBackgroundColor(details: Action.SetBadgeBackgroundColorDetailsType) { + const { color, tabId, windowId } = details; + let rgbaColor: ColorArray; + + if (typeof color === 'string') { + rgbaColor = hexToRgbaArray(color); + } else if (Array.isArray(color)) { + rgbaColor = [...color] as ColorArray; + } else { + rgbaColor = hexToRgbaArray(DEFAULT_BADGE_BACKGROUND_COLOR); + } + + if (tabId !== undefined) { + badgeBackgroundColorState.tabs.set(tabId, rgbaColor); + } else if (windowId !== undefined) { + badgeBackgroundColorState.windows.set(windowId, rgbaColor); + } else { + badgeBackgroundColorState.global = rgbaColor; + } + + return Promise.resolve(); + }, + + getBadgeBackgroundColor(details: Action.Details): Promise { + const value = getScopedValue(badgeBackgroundColorState, details); + return Promise.resolve(value ?? hexToRgbaArray(DEFAULT_BADGE_BACKGROUND_COLOR)); + }, + + setBadgeTextColor(details: Action.SetBadgeTextColorDetailsType) { + const { color, tabId, windowId } = details; + + let normalizedColor = typeof color === 'string' ? color : DEFAULT_BADGE_TEXT_COLOR; + + if (tabId !== undefined) { + if (color === null || color === undefined) { + badgeTextColorState.tabs.delete(tabId); + } else { + badgeTextColorState.tabs.set(tabId, normalizedColor); + } + } else if (windowId !== undefined) { + if (color === null || color === undefined) { + badgeTextColorState.windows.delete(windowId); + } else { + badgeTextColorState.windows.set(windowId, normalizedColor); + } + } else { + badgeTextColorState.global = normalizedColor; + } + }, + + getBadgeTextColor(details: Action.Details, callback?: (value: string) => void): void { + const value = getScopedValue(badgeTextColorState, details); + callback?.(value ?? DEFAULT_BADGE_TEXT_COLOR); + }, + + onClicked, +}; diff --git a/packages/fake-browser/src/index.ts b/packages/fake-browser/src/index.ts index ae0b989..85f4e75 100644 --- a/packages/fake-browser/src/index.ts +++ b/packages/fake-browser/src/index.ts @@ -5,6 +5,7 @@ import { storage } from './apis/storage'; import { tabs } from './apis/tabs'; import { webNavigation } from './apis/webNavigation'; import { windows } from './apis/windows'; +import { action } from './apis/action'; import { BrowserOverrides, FakeBrowser } from './types'; import { GeneratedBrowser } from './base.gen'; import merge from 'lodash.merge'; @@ -26,6 +27,7 @@ const overrides: BrowserOverrides = { tabs, webNavigation, windows, + action, }; /** diff --git a/packages/fake-browser/src/types.ts b/packages/fake-browser/src/types.ts index d0e820a..c345e55 100644 --- a/packages/fake-browser/src/types.ts +++ b/packages/fake-browser/src/types.ts @@ -1,4 +1,5 @@ import type { + Action, Alarms, Browser, Notifications, @@ -107,6 +108,20 @@ export interface BrowserOverrides { onRemoved: EventForTesting<[windowId: number]>; onFocusChanged: EventForTesting<[windowId: number]>; }; + action: Pick< + Action.Static, + | 'setTitle' + | 'getTitle' + | 'getBadgeText' + | 'setBadgeText' + | 'setBadgeTextColor' + | 'getBadgeTextColor' + | 'getBadgeBackgroundColor' + | 'setBadgeBackgroundColor' + > & { + resetState(): void; + onClicked: EventForTesting<[tab: Tabs.Tab, info: Action.OnClickData | undefined]>; + }; } /**