Skip to content

Commit

Permalink
first-commit
Browse files Browse the repository at this point in the history
  • Loading branch information
SoraKumo001 committed Mar 8, 2023
0 parents commit f30061b
Show file tree
Hide file tree
Showing 16 changed files with 10,384 additions and 0 deletions.
25 changes: 25 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"env": {
"node": true,
"es6": true,
"browser": true
},
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"modules": true
}
},
"plugins": ["@typescript-eslint"],
"rules": {
"no-empty": 0,
"@typescript-eslint/explicit-module-boundary-types": 0,
"@typescript-eslint/no-non-null-assertion": 0,
"@typescript-eslint/no-var-requires": 0,
"@typescript-eslint/ban-types": 0,
"@next/next/no-html-link-for-pages": 0
}
}
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules
*.log
/dist
/coverage

12 changes: 12 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
node_modules
/src
.gitignore
yarn.lock
tsconfig.json
jest.config.js
.eslintrc.json
.github
*.log
/test
/coverage
.prettierrc
6 changes: 6 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"trailingComma": "es5",
"semi": true,
"singleQuote": true,
"printWidth": 100
}
120 changes: 120 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# storybook-addon-module-mock

Provides module mocking functionality like `jest.mock` on Storybook.

## usage

Added 'storybook-addon-module-mock' to Storybook addons.

### .storybook/main.js

```js
// @ts-check
/**
* @type { import("@storybook/react/types").StorybookConfig}
*/
module.exports = {
addons: ['storybook-addon-module-mock'],
};
```

### Sample

#### message.ts

Mock target file.

```tsx
export const getMessage = () => {
return 'Before';
};
```

#### MockTest.tsx

```tsx
import React, { FC, useState } from 'react';
import { getMessage } from './message';

interface Props {}

/**
* MockTest
*
* @param {Props} { }
*/
export const MockTest: FC<Props> = ({}) => {
const [, reload] = useState({});
return (
<div>
<button onClick={() => reload({})}>{getMessage()}</button>
</div>
);
};
```

#### MockTest.stories.tsx

`createMock` replaces the target module function with the return value of `jest.fn()`.
The `mockRestore()` is automatically performed after the Story display is finished.

```tsx
import { expect } from '@storybook/jest';
import { userEvent, waitFor, within } from '@storybook/testing-library';
import { ComponentMeta, ComponentStoryObj } from '@storybook/react';
import { MockTest } from './MockTest';
import { createMock, getMock } from 'storybook-addon-module-mock';

import * as message from './message';

const meta: ComponentMeta<typeof MockTest> = {
title: 'Components/MockTest',
component: MockTest,
};
export default meta;

export const Primary: ComponentStoryObj<typeof MockTest> = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvas.getByText('Before')).toBeInTheDocument();
},
};

export const Mock: ComponentStoryObj<typeof MockTest> = {
parameters: {
moduleMock: {
mock: () => {
const mock = createMock(message, 'getMessage');
mock.mockReturnValue('After');
return [mock];
},
},
},
play: async ({ canvasElement, parameters }) => {
const canvas = within(canvasElement);
expect(canvas.getByText('After')).toBeInTheDocument();
const mock = getMock(parameters, message, 'getMessage');
expect(mock).toBeCalled();
},
};

export const Action: ComponentStoryObj<typeof MockTest> = {
parameters: {
moduleMock: {
mock: () => {
const mock = createMock(message, 'getMessage');
return [mock];
},
},
},
play: async ({ canvasElement, parameters }) => {
const canvas = within(canvasElement);
const mock = getMock(parameters, message, 'getMessage');
mock.mockReturnValue('Action');
userEvent.click(await canvas.findByRole('button'));
await waitFor(() => {
expect(canvas.getByText('Action')).toBeInTheDocument();
});
},
};
```
28 changes: 28 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "storybook-addon-module-mock",
"version": "0.0.1",
"main": "dist/index.js",
"license": "MIT",
"scripts": {
"build": "tsc -b",
"lint": "eslint --fix ./src",
"lint:fix": "eslint --fix ./src"
},
"devDependencies": {
"@babel/core": "^7.21.0",
"@storybook/addons": "^6.5.16",
"@storybook/jest": "^0.0.10",
"@storybook/react": "^6.5.16",
"@types/babel__core": "^7.20.0",
"@types/react": "^18.0.28",
"@typescript-eslint/eslint-plugin": "^5.54.0",
"@typescript-eslint/parser": "^5.54.0",
"eslint": "8.35.0",
"eslint-config-prettier": "^8.6.0",
"eslint-import-resolver-typescript": "^3.5.3",
"eslint-plugin-import": "^2.27.5",
"typescript": "^4.9.5"
},
"repository": "https://github.com/ReactLibraries/storybook-addon-module-mock",
"author": "SoraKumo <[email protected]>"
}
1 change: 1 addition & 0 deletions preset.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require("./dist/preset.js");
73 changes: 73 additions & 0 deletions src/babel-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { types as t, PluginObj, template } from '@babel/core';

const buildMocks = template(
`
const MOCKS={};
export const $$mock$$=(name,value)=>MOCKS[name](value);`
);

const buildMock = template(
`
MOCKS[NAME] =
function ($$value$$) {
const $$temp$$ = LOCAL;
LOCAL = $$value$$;
return $$temp$$;
}`
);

const plugin = (): PluginObj<{
moduleExports: [string, string][];
}> => {
return {
name: 'mocks',
visitor: {
Program: {
enter(_, state) {
state.moduleExports = [];
},
exit(path, { moduleExports }) {
const mocks = path.scope.generateDeclaredUidIdentifier('mocks');
path.pushContainer('body', buildMocks({ MOCKS: mocks }));
moduleExports.forEach((name) => {
const mock = buildMock({
NAME: t.stringLiteral(name[0]),
LOCAL: t.identifier(name[1]),
MOCKS: mocks,
});
path.pushContainer('body', mock);
});
},
},
ExportNamedDeclaration(path, { moduleExports }) {
const identifiers = path.getOuterBindingIdentifiers();
moduleExports.push(
...Object.keys(identifiers).map<[string, string]>((name) => [name, name])
);
},
ExportDefaultDeclaration(path, { moduleExports }) {
const declaration = path.node.declaration;
const name = t.isIdentifier(declaration) && declaration.name;
if (!name) {
if (t.isArrowFunctionExpression(declaration)) {
const id = path.scope.generateUidIdentifier('default');
const variableDeclaration = t.variableDeclaration('const', [
t.variableDeclarator(id, declaration),
]);
path.replaceWith(t.exportDefaultDeclaration(id));
path.insertBefore(variableDeclaration);
moduleExports.push(['default', id.name]);
}
} else {
const decl = t.exportNamedDeclaration(null, [
t.exportSpecifier(t.identifier(name), t.identifier('default')),
]);
path.replaceWith(decl);
moduleExports.push(['default', name]);
}
},
},
};
};

export default plugin;
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './types';
export * from './mocks';
43 changes: 43 additions & 0 deletions src/mocks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { jest } from '@storybook/jest';
import { Mock, moduleMockParameter } from '../types';
import type { Parameters as P } from '@storybook/react';

export const createMock = <
T extends { [key: string | number]: () => unknown },
N extends keyof T = 'default'
>(
module: T,
name: N = 'default' as N
): Mock<T, N> => {
const fn = jest.fn<ReturnType<T[N]>, Parameters<T[N]>>();
const mock = (module as unknown as { $$mock$$: (name: N, value: unknown) => unknown }).$$mock$$;
const f = mock(name, fn);
fn.mockRestore = () => {
mock(name, f);
};
return Object.assign(fn, { __module: { module, name } });
};

export const getMock = <T extends { [key: string | number]: () => unknown }, N extends keyof T>(
parameters: P,
module: T,
name: N
): Mock<T, N> => {
const mock = (parameters as moduleMockParameter).moduleMock.mocks?.find((mock) => {
return mock.__module?.module === module && mock.__module?.name === name;
});
if (!mock) throw new Error("Can't find mock");
return mock as unknown as Mock<T, N>;
};

export const resetMock = (parameters: P) => {
(parameters as moduleMockParameter).moduleMock.mocks?.forEach((mock) => {
return mock.mockReset();
});
};

export const clearMock = (parameters: P) => {
(parameters as moduleMockParameter).moduleMock.mocks?.forEach((mock) => {
return mock.mockClear();
});
};
21 changes: 21 additions & 0 deletions src/preset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { StorybookConfig } from "@storybook/core-common";
import { TransformOptions } from "@babel/core";

export const managerEntries = (entry: string[] = []): string[] => [
...entry,
require.resolve("./register"),
];

export const babel = async (
config: TransformOptions
): Promise<TransformOptions> => {
return {
...config,
plugins: [...(config.plugins ?? []), require.resolve("./babel-plugin")],
};
};

export const config: StorybookConfig["previewAnnotations"] = (entry = []) => [
...entry,
require.resolve("./preview"),
];
26 changes: 26 additions & 0 deletions src/preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { DecoratorFn } from '@storybook/react';
import React, { useEffect } from 'react';
import { moduleMockParameter } from './types';

const MockDecorator: DecoratorFn = (Story, { parameters }) => {
const { moduleMock } = parameters as moduleMockParameter;
if (!moduleMock?.mocks) {
const mocks = moduleMock?.mock?.();
moduleMock.mocks = !mocks ? undefined : Array.isArray(mocks) ? mocks : [mocks];
}
useEffect(() => {
return () => {
if (moduleMock) {
moduleMock.mocks?.forEach((mock) => mock.mockRestore());
moduleMock.mocks = undefined;
}
};
}, []);
return <Story />;
};

export const parameters: moduleMockParameter = {
moduleMock: {},
};

export const decorators = [MockDecorator];
5 changes: 5 additions & 0 deletions src/register.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { addons } from '@storybook/addons';
const ADDON_ID = 'storybook-addon-module-mock';
addons.register(ADDON_ID, () => {
//
});
12 changes: 12 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { jest } from "@storybook/jest";

export type ModuleType<T,N> = { __module: { module: T; name: N } }
export type Mocks = (ReturnType<typeof jest.fn> & ModuleType<unknown,unknown>)[];
export type Mock<T extends { [key: string | number]: unknown }, N extends keyof T> =
ReturnType<typeof jest.fn<ReturnType<T[N],>, Parameters<T[N]>>> & ModuleType<T,N>
export type moduleMockParameter = {
moduleMock: {
mock?: () => Mocks;
mocks?: Mocks;
};
};
Loading

0 comments on commit f30061b

Please sign in to comment.