Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for chrome.action #11

Open
Mrtenz opened this issue Jul 1, 2021 · 4 comments
Open

Support for chrome.action #11

Mrtenz opened this issue Jul 1, 2021 · 4 comments
Assignees
Labels
MV3 Manifest Version 3
Projects

Comments

@Mrtenz
Copy link

Mrtenz commented Jul 1, 2021

Is your feature request related to a problem? Please describe.

When using Manifest V3, chrome.browserAction (and pageAction) is replaced with chrome.action. It looks like this is not supported by jest-chrome currently.

Describe the solution you'd like

Support for chrome.action as alternative to chrome.browserAction.

@jacksteamdev
Copy link
Contributor

I'm in the process of updating my projects to MV3, I'll put this on the todo list!

Thanks for mentioning it, @Mrtenz 🥇

@jacksteamdev jacksteamdev added the MV3 Manifest Version 3 label Aug 3, 2021
@jacksteamdev jacksteamdev added this to To do in MV3 Aug 3, 2021
@eegli
Copy link

eegli commented Aug 3, 2021

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');
  });
});

Bonus

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!

@jacksteamdev
Copy link
Contributor

@eegli That's some real TypeScript magic! Love it! Thank you so much.

@prli
Copy link

prli commented May 19, 2022

I ended up mocking action with everything else still using jest-chrome

Object.assign(global, require('jest-chrome'));
global.chrome.action = {
    setTitle: jest.fn()
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
MV3 Manifest Version 3
Projects
MV3
To do
Development

No branches or pull requests

4 participants