Open Mrtenz opened 3 years ago
I'm in the process of updating my projects to MV3, I'll put this on the todo list!
Thanks for mentioning it, @Mrtenz :1st_place_medal:
Hey @jacksteamdev - first of all, thank you for this awesome project. I was about to ask if you're planning on adding Manifest V3 support.
For those who are stuck right now and would like to mock chrome.action
(or other V3 APIs), I created a little helper to mock those. All it needs is the types from @types/chrome
.
Here is the helper:
// ./test-helper.ts
type Join<K, P> = K extends string | number
? P extends string | number
? `${K}${'' extends P ? '' : '.'}${P}`
: never
: never;
type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...0[]];
type Paths<T, D extends number = 10> = [D] extends [never]
? never
: T extends object
? {
[K in keyof T]-?: K extends string | number
? `${K}` | Join<K, Paths<T[K], Prev[D]>>
: never;
}[keyof T]
: '';
/* Utility function to mock currently unavailable methods in
'jest-chrome */
/**
* Takes a path to a method of the Chrome API. Properties are accessed
* via dot notation. Example:
* ```
* const scriptMock = mockForV3('scripting.executeScript')
* ```
* This will produce
* ```
* global.chrome.scripting.executeScript = jest.fn()
* ```
* The returned mock function above will mock
* `scripting.executeScript`. Each returned mock function has all the
* Jest methods available and you can add your custom implementations
* as usual.
* ```
* scriptMock.mockImplementation(() => true)
* ```
* @param args string
* @returns jest.Mock - Generic jest mock function
*
*/
export default function <T extends Paths<typeof chrome>>(path: T) {
const mockFn = jest.fn();
const keys = path.split('.');
function deepRecreate(): void {
const methods = keys.reduceRight((obj, next, idx) => {
if (idx === keys.length - 1) {
return { [next]: mockFn };
}
return { [next]: obj };
}, {});
Object.assign(global.chrome, methods);
}
deepRecreate();
return mockFn;
}
If you want to go the easy way, just put it somewhere and import the module when necessary. The function will recreate the global chrome object according to the path you provide as a string. With Typescript, you even get IntelliSense and linting.
The function will return a new mock function with all the Jest methods available, and you can add your custom mock implementation.
If you want to have type-safe mocks, just cast the returned mock to be whatever you want (as in the official docs). Otherwise, just use the returned mock, but be aware that, depending on what you test, your tests may fail because you're not providing the correct implementations.
Here is an example with a "loosely" typed mock (a mock implementation that does not conform to the actual API and a "strongly" typed mock:
// ./test/example.spec.ts
import mockForV3 from '../test-helper';
// Mocking a V3 API
async function getTabGroup() {
const { color } = await chrome.tabGroups.get(1);
if (!color) throw new Error('No color!');
return color;
}
describe('Sample test', () => {
it('fails with loose implementation', async () => {
// Example with "loose implementation"
const looseScriptMock = mockForV3('tabGroups.get');
// Custom response, does not have to follow API interface
looseScriptMock.mockImplementation(async () => ({
collapsed: true,
// color: 'blue', // Left out!
id: 1,
windowId: 1
}));
await expect(getTabGroup()).rejects.toThrow();
});
it('works with strong implementation', async () => {
// Example with mocking the actual implementation
const strongScriptMock = mockForV3('tabGroups.get') as jest.MockedFunction<
typeof chrome.tabGroups.get
>;
// TS complains if something is removed from the mocked response
strongScriptMock.mockImplementation(async () => ({
collapsed: true,
color: 'blue',
id: 1,
windowId: 1
}));
await expect(getTabGroup()).resolves.toBe('blue');
});
});
If you want to have the helper available globally (just like describe
, etc.), add this in jest.setup.ts
:
// ./jest.setup.ts
// Assuming the helper file is in the root dir
global.mockForV3 = require('./test-helper').default;
// From the package
Object.assign(global, require('jest-chrome'));
Create a global.d.ts file (or any other name) and add this:
// ./typings/global.d.ts
declare var mockForV3: typeof import('../test-helper').default;
Note that in this case, the global.d.ts
file is located in ./typings/
.
And, last but not least, make sure TS is aware of your definitions file.
{
"compilerOptions": {
...
"include": ["src/**/*", "test/**/*", "typings/**/*", "jest.setup.ts"],
}
Now mockForV3
is available globally and you don't need to import it.
To recap: This is my directory structure:
test/
├─ example.spec.ts
typings/
├─ globals.d.ts
tsconfig.json
jest.setup.ts
test-helper.ts
Maybe this is helpful to someone. Thank you for your work!
@eegli That's some real TypeScript magic! Love it! Thank you so much.
I ended up mocking action
with everything else still using jest-chrome
Object.assign(global, require('jest-chrome'));
global.chrome.action = {
setTitle: jest.fn()
}
Is your feature request related to a problem? Please describe.
When using Manifest V3,
chrome.browserAction
(andpageAction
) is replaced withchrome.action
. It looks like this is not supported byjest-chrome
currently.Describe the solution you'd like
Support for
chrome.action
as alternative tochrome.browserAction
.