Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions packages/fake-browser/src/apis/action.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
});
182 changes: 182 additions & 0 deletions packages/fake-browser/src/apis/action.ts
Original file line number Diff line number Diff line change
@@ -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<string> = {
global: DEFAULT_BADGE_TEXT_COLOR,
tabs: new Map(),
windows: new Map(),
};

interface ScopedState<T> {
global?: T;
tabs: Map<number, T>;
windows: Map<number, T>;
}

const badgeTextState: ScopedState<string> = {
global: '',
tabs: new Map(),
windows: new Map(),
};

const badgeBackgroundColorState: ScopedState<ColorArray> = {
global: hexToRgbaArray(DEFAULT_BADGE_BACKGROUND_COLOR),
tabs: new Map(),
windows: new Map(),
};

const titleState: ScopedState<string> = {
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<T>(state: ScopedState<T>, 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<string> {
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<string> {
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<ColorArray> {
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,
};
2 changes: 2 additions & 0 deletions packages/fake-browser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -26,6 +27,7 @@ const overrides: BrowserOverrides = {
tabs,
webNavigation,
windows,
action,
};

/**
Expand Down
15 changes: 15 additions & 0 deletions packages/fake-browser/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {
Action,
Alarms,
Browser,
Notifications,
Expand Down Expand Up @@ -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]>;
};
}

/**
Expand Down