- 投稿日:
以下のようなコードを書いたときにmockFnにイベント引数が渡らないが、これをどうにかして取る方法。結論から言うとまともに取れないが、試行錯誤した時のログとして残しておく
const mockFn = jest.fn();
render(<input onChange={mockFn} />);
fireEvent.change(inputElement, { target: { value: 'aaa' } });
確認環境
Env | Ver |
---|---|
@swc/core | 1.3.66 |
@swc/jest | 0.2.26 |
@testing-library/jest-dom | 5.16.5 |
@testing-library/react | 14.0.0 |
jest | 29.5.0 |
react | 18.2.0 |
サンプルコード
これは以下のように書くとfireEvent
の第二引数で指定したtarget.value
の部分だけは一応取れる。
理由としてはfireEvent
がelement.dispatchEvent
を読んでいると思われるためだ。余り深くは追っていないが、react-testing-libraryの実装上は多分そうなっていると思われる。
import { fireEvent, render } from '@testing-library/react';
it('test', () => {
const mockFn = jest.fn((ev) => {
console.log(ev);
});
const { container } = render(<input id="hoge" onChange={mockFn} value={'a'} />);
const element = container.querySelector('input');
if (element === null) throw new Error();
element.dispatchEvent = jest.fn();
fireEvent.change(element, {
target: {
value: 'bbb'
}
});
expect(element.dispatchEvent.mock.instances[0].value).toBe('bbb');
});
- 投稿日:
SCSS Moduleを使ったNext.jsと連携させてるStorybookを6.5から7.0に上げたときの手順とトラブルシューティング
環境移行内容
moduleのバージョンをbeforeからafterにアップデート
module | before | after |
---|---|---|
@storybook/addon-actions | 6.5.13 | 7.0.24 |
@storybook/addon-essentials | 6.5.13 | 7.0.24 |
@storybook/addon-links | 6.5.13 | 7.0.24 |
@storybook/builder-webpack5 | 6.5.13 | 7.0.24 |
@storybook/manager-webpack5 | 6.5.13 | 6.5.16 |
@storybook/preset-scss | 1.0.3 | 1.0.3 |
@storybook/react | 6.5.13 | 7.0.24 |
やったこと
- storybook周りの最新化
npm i @storybook/xxx@latest && @storybook/yyy@latest ...
sb dev
する- エラーが出るので言われたとおりに
npx storybook@next automigrate
を流す - 適当に説明を読みながら進める
- 私のケースでは全部
Y
で問題ありませんでした
- 私のケースでは全部
- storyファイルを
s/storyName/name/
で置換する- なんか書式が変わったらしい
.storybook/main.js
のaddons
にある'@storybook/preset-scss'
を削除ComponentStoryObj
をStoryObj
に置換storybook dev
で起動確認
トラブルシューティング
start-storybook
が動かない
@storybook/cliを導入してstart-storybook
をsb dev
に置き換え
https://github.com/storybookjs/storybook/issues/18923#issuecomment-1214280920
sb dev
がコケる
書かれてる通りにnpx storybook@next automigrate
を流す
何か色々変更点を教えてくれるので適宜読んで対処する
終わったらnpx storybook dev
を流せば起動します
Unexpected usage of "storyName" in "Example". Please use "name" instead.
storyName
をname
に変える。
export const Example: Story = {
- storyName: 'ほげほげ',
+ name: 'ほげほげ',
}
sass-loader が動かない
ModuleBuildError: Module build failed (from ./node_modules/sass-loader/dist/cjs.js):
SassError: expected "{".
╷
2 │ import API from "!../../../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js";
.storybook/main.js
のaddons
に'@storybook/preset-scss'
がいたら消す
module.exports = {
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
- '@storybook/preset-scss',
],
framework: {
name: '@storybook/nextjs',
options: {},
},
docs: {
autodocs: true,
},
};
ゴミなので@storybook/preset-scss
を消しておく
npm un @storybook/preset-scss
- 投稿日:
条件分岐でコンポーネントの出し分けをしている時に正しくコンポーネントが出ているかどうかに使えるやつ
UIロジックのリグレッションテストで使える
分岐結果の出力を見てるだけなのでテストとして壊れづらく、運用しやすいと考えている
確認環境
Next.jsで確認してるけど素のReactでも同じだと思う
Env | Ver |
---|---|
@swc/core | 1.2.133 |
@swc/jest | 0.2.17 |
jest | 27.4.7 |
next | 12.0.8 |
react | 17.0.2 |
react-dom | 17.0.2 |
react-test-renderer | 17.0.2 |
typescript | 4.5.4 |
テスト対象
テストのためにコンポーネントを細かくexport
すると名前空間が汚染されるのが悩み…
type BaseProps = {
id: string;
};
type SwitchExampleProps = BaseProps & {
display: 'Foo' | 'Bar';
};
export const Foo = (props: BaseProps) => {
return (
<div id={props.id}>
<p>Foo</p>
</div>
);
};
export const Bar = (props: BaseProps) => {
return (
<div id={props.id}>
<p>Bar</p>
</div>
);
};
export const SwitchExample = (props: SwitchExampleProps) => {
if (props.display === 'Foo') {
return <Foo id={props.id} />;
} else {
return <Bar id={props.id} />;
}
};
テストコード
react-testing-libraryの.toHaveAttribute()
や.toHaveDisplayValue()
を書き連ねるより圧倒的に楽で保守性も良いと思う
import TestRenderer from 'react-test-renderer';
import { Bar, Foo, SwitchExample } from './SwitchExample';
type TestCase = {
name: string;
param: Parameters<typeof SwitchExample>[0];
actual: JSX.Element;
};
describe('SwitchExample', () => {
const testCaseItems: TestCase[] = [
{
name: 'Foo',
param: {
id: 'hoge',
display: 'Foo',
},
actual: <Foo id={'hoge'} />,
},
{
name: 'Bar',
param: {
id: 'piyo',
display: 'Bar',
},
actual: <Bar id={'piyo'} />,
},
];
testCaseItems.forEach((item) => {
// eslint-disable-next-line jest/valid-title
it(`switched condition ${item.name}`, () => {
const result = TestRenderer.create(
<SwitchExample id={item.param.id} display={item.param.display} />
);
const actual = TestRenderer.create(<>{item.actual}</>);
expect(result.toJSON()).toStrictEqual(actual.toJSON());
});
});
});
参考記事
- 投稿日:
確認環境
Env | Ver |
---|---|
next | 11.1.2 |
typescript | 4.3.4 |
サンプルコード
__mocks__/next/router.ts
export const useRouter = () => {
return {
route: '/',
pathname: '',
query: '',
asPath: '',
push: jest.fn(),
replace: jest.fn(),
events: {
on: jest.fn(),
off: jest.fn(),
},
beforePopState: jest.fn(() => null),
prefetch: jest.fn(() => null),
};
};
- 投稿日:
Create Next Appで生成されるNext.jsのプロジェクトはCRAで生成される内容と比べると不親切なので、快適に開発できるようにしようという内容です
Zero Config?そんなものは知りません。TypeScriptもESLintもPrettierもJestも全部欲しい!Next.jsのexamplesでは物足りない!
VSCodeで開発しTypeScriptでSSGとして組む前提で書いてます
確認環境
- CRAで吐き出されるTypeScript向けのプロジェクトに対し
npm run eject
をかけたものをベースにしてます - SCSSを使いたいのでsassを入れています
Env | Ver |
---|---|
VSCode | 1.57.1 |
next | 11.0.1 |
react | 17.0.2 |
react-dom | 17.0.2 |
@testing-library/jest-dom | 5.14.1 |
@testing-library/react | 11.2.7 |
@testing-library/react-hooks | 7.0.1 |
@types/jest | 26.0.23 |
@types/node | 15.0.2 |
@types/react | 17.0.5 |
@types/react-dom | 17.0.3 |
@typescript-eslint/eslint-plugin | 4.23.0 |
@typescript-eslint/parser | 4.23.0 |
eslint | 7.26.0 |
eslint-config-next | 11.0.1 |
eslint-config-prettier | 8.3.0 |
eslint-config-react-app | 6.0.0 |
eslint-plugin-flowtype | 5.7.2 |
eslint-plugin-import | 2.23.2 |
eslint-plugin-jest | 24.3.6 |
eslint-plugin-jsx-a11y | 6.4.1 |
eslint-plugin-react | 7.23.2 |
eslint-plugin-react-hooks | 4.2.0 |
eslint-plugin-testing-library | 3.9.0 |
identity-obj-proxy | 3.0.0 |
jest | 27.0.5 |
sass | 1.38.2 |
jest-watch-typeahead | 0.6.4 |
prettier | 2.2.1 |
serve | 12.0.0 |
ts-jest | 27.0.3 |
typescript | 4.3.4 |
npm packages の導入
- 次のコマンドを流し必要なものを一式入れる
- バージョンの整合性が上手く合わないときは気合で調整する
npm i next react react-dom
npm i -D @testing-library/jest-dom @testing-library/react @testing-library/react-hooks @types/jest @types/node @types/react @types/react-dom @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint eslint-config-next eslint-config-prettier eslint-config-react-app eslint-plugin-flowtype eslint-plugin-import eslint-plugin-jest eslint-plugin-jsx-a11y eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-testing-library identity-obj-proxy jest jest-watch-typeahead prettier sass serve ts-jest typescript
プロジェクトの構成例
- 例なので、実際の使用に際しては個々の事情に応じていい感じにしてください
|- .vscode/
|- extensions.json
|- launch.json
|- settings.json
|- public/
|- src/
|- components/
|- pages/
|- .eslintignore
|- .eslintrc
|- .gitignore
|- .prettierrc
|- jest-setup.ts
|- jest.config.js
|- next-env.d.ts
|- next.config.js
|- package.json
|- tsconfig.jest.json
|- tsconfig.json
.vscode/extensions.json
- 使う拡張機能を書いておくと拡張機能インストールの提案が走るのであると便利なやつです
{
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"msjsdiag.debugger-for-chrome"
]
}
.vscode/launch.json
- ブレークポイントを設定してデバッグするときの設定です
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "attach",
"name": "Next: Node",
"skipFiles": ["<node_internals>/**", ".next/**"],
"port": 9229
},
{
"type": "chrome",
"request": "launch",
"name": "Next: Chrome",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}",
"sourceMapPathOverrides": {
"webpack://_N_E/*": "${webRoot}/*"
},
"runtimeArgs": ["--profile-directory=debug-profile"]
}
],
"compounds": [
{
"name": "Next: Full",
"configurations": ["Next: Node", "Next: Chrome"]
}
]
}
.vscode/settings.json
- VSCodeの設定をワークスペースで上書くやつです
- 次の2つでワークスペースのTypeScriptを使うことを宣言しています。グローバルとは切り離しておきたいことってあると思います
"typescript.enablePromptUseWorkspaceTsdk": true,
"typescript.tsdk": "node_modules/typescript/lib",
// Place your settings in this file to overwrite default and user settings.
{
"search.exclude": {
"**/node_modules": true,
"**/.next": true,
"**/out": true,
"**/coverage": true
},
"files.eol": "\n",
"files.insertFinalNewline": true,
"git.suggestSmartCommit": false,
"git.autofetch": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"eslint.alwaysShowStatus": true,
"eslint.validate": ["javascript", "typescript"],
"editor.formatOnSave": true,
"[javascript]": {
"editor.tabSize": 2
},
"javascript.suggest.paths": true,
"javascript.updateImportsOnFileMove.enabled": "always",
"[typescript]": {
"editor.tabSize": 2
},
"typescript.suggest.paths": true,
"typescript.updateImportsOnFileMove.enabled": "always",
"typescript.enablePromptUseWorkspaceTsdk": true,
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.referencesCodeLens.enabled": true,
"typescript.validate.enable": true,
"typescript.preferences.importModuleSpecifier": "relative"
}
public/
- こいつはルートに配置されている必要があり、src/ の下には配置できない
src/
- Nextの公式テンプレでは基本的に存在しないが、ルートがカオスになるのであったほうがいいと思う
.eslintignore
- とりあえずこんくらいあればいいでしょう
# /node_modules/* in the project root is ignored by default
# build artefacts
.next/*
out/*
coverage/*
# data definition files
**/*.d.ts
.eslintrc
- TSとReactの推奨を設定し、ビルドエラーに繋がりな部分や、型を厳格にする設定、楽な記法を全力採用
- Jest周りが弱い気がしてるので、もっといい書き方があると思います
"extends": "next"
は意図的に入れていませんpackage.json
に書いてあるeslint-config-next
はないと怒られるので入れてるだけです
{
"parser": "@typescript-eslint/parser",
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"react-app",
"react-app/jest",
"plugin:react/recommended",
"prettier"
],
"plugins": ["react", "@typescript-eslint"],
"parserOptions": {
"project": "./tsconfig.json",
"ecmaFeatures": {
"jsx": true
}
},
"settings": {
"react": {
"version": "detect"
}
},
"rules": {
// Enforce the consistent use of either backticks, double, or single quotes
"quotes": ["warn", "single"],
// Enforce the consistent use of either function declarations or expressions
"func-style": ["warn", "expression", { "allowArrowFunctions": true }],
// Detect missing key prop
"react/jsx-key": [
"warn",
{ "checkFragmentShorthand": true, "checkKeyMustBeforeSpread": true }
],
// Enforce shorthand or standard form for React fragments
"react/jsx-fragments": ["warn", "syntax"],
// Prevent missing React when using JSX
"react/react-in-jsx-scope": "off",
// Validate whitespace in and around the JSX opening and closing brackets
"react/jsx-tag-spacing": [
"warn",
{
"closingSlash": "never",
"beforeSelfClosing": "always",
"afterOpening": "never",
"beforeClosing": "never"
}
],
// Prevent extra closing tags for components without children
"react/self-closing-comp": [
"warn",
{
"component": true,
"html": true
}
],
// Allow non-explicit return types on functions and class methods
"@typescript-eslint/explicit-function-return-type": "off",
// Allow non-explicit return types on exported functions
"@typescript-eslint/explicit-module-boundary-types": "off",
// Disallow usage of the any type
"@typescript-eslint/no-explicit-any": 1,
// Disallows explicit type declarations for variables or parameters initialized to a number, string, or boolean
"@typescript-eslint/no-inferrable-types": [
"error",
{
"ignoreParameters": true
}
],
// Error on declared but not used variable
"@typescript-eslint/no-unused-vars": "error"
}
}
.gitignore
- これはテンプレそのままでいい気がします
# /node_modules/* in the project root is ignored by default
# build artefacts
.next/*
out/*
coverage/*
# data definition files
**/*.d.ts
# config files
**/*.config.js
.prettierrc
- こんくらいあればいいでしょう
{
"trailingComma": "es5",
"printWidth": 80,
"tabWidth": 2,
"semi": true,
"singleQuote": true
}
jest-setup.ts
- CRAでReactをセットアップすると当たり前のように入ってるmatcher を標準で入れるためのやつです
- 具体的には
toHaveAttribute()
みたいなやつを追加できます
import '@testing-library/jest-dom';
import '@testing-library/jest-dom/extend-expect';
jest.config.js
- CRAでReactをセットアップすると勝手に設定されますが、Next.jsはそこまで親切ではないので自分で書きます
- 一応examplesにそれっぽいものはありますが、物足りないと思います
- tsconfig.jsonがNext.jsによって勝手に書き換えられる関係で、jest用のtsconfigを用意しています
- CRAで作ったプロジェクトみたいにテストをWatch出来るようにしています
.scss
とかをimport
していてもコケないようにしています- 但し以下の設定ではjsdomとaxiosの相性が良くなく、axiosを使ったテストが上手く通らないケースがあります
- これは基本的にテストがSSRとして実行される関係でnodeでないと上手く機能しないことが関係しているのではないかと考えていますが、DOMのテストにはjsdomが必要なので悩ましいところ…
module.exports = {
globals: {
'ts-jest': {
tsconfig: '<rootDir>/tsconfig.jest.json',
},
},
preset: 'ts-jest',
clearMocks: true,
setupFilesAfterEnv: ['<rootDir>/jest-setup.ts'],
roots: ['<rootDir>'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'json', 'jsx'],
collectCoverageFrom: ['src/**/*.{ts,tsx,js,jsx}'],
testEnvironment: 'jsdom',
testPathIgnorePatterns: ['<rootDir>[/\\\\](node_modules|.next)[/\\\\]'],
transformIgnorePatterns: ['[/\\\\]node_modules[/\\\\].+\\.(ts|tsx)$'],
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest',
},
watchPlugins: [
'jest-watch-typeahead/filename',
'jest-watch-typeahead/testname',
],
moduleNameMapper: {
'\\.(css|less|sass|scss)$': 'identity-obj-proxy',
'\\.(gif|ttf|eot|svg|png)$': '<rootDir>/test/__mocks__/fileMock.js',
},
};
next-env.d.ts
.scss
とかをimport
しようとしたときにエラーにならなくなるやつです
/// <reference types="next" />
/// <reference types="next/types/global" />
/// <reference types="next/image-types/global" />
next.config.js
basePath
: SSGをビルドしたときの基準パスを設定できます- 以下の例だと
https://example.com/deploy-path-here
が基準パスになります - 開発時は不便なので
http://localhost:3000
になるようにしてます
- 以下の例だと
assetPrefix
:_next/
の位置を指定できます- 以下の例だと
https://example.com/assets-path-here/_next
が_next/
の位置になります- 既に
_next/
が存在する場所に設置するような場合に役立つと思われます
- 既に
- 開発時にはあっても意味がないので除外する設定にしています
- 以下の例だと
module.exports = (phase, { defaultConfig }) => {
return {
basePath: isProd ? '/deploy-path-here' : '',
assetPrefix: isProd ? '/assets-path-here' : '',
};
};
package.json
scripts
にこんくらい書いとくと便利かな?とは思いますdev
デバッグサーバーが起動します。ブレークポイントを設定して確認するときに使いますstart
開発サーバーが起動します。CRAで作ったReactと同じですbuild
out/
にプロダクションビルドを吐きますserve
out/
の中身をlocalhostで上げますtype-check
ESLintとTSCのチェックを走らせてエラーを検知しますtest
CRAで作ったReact同様にWatchモードでテストを走らせますcoverage
テストカバレッジが出ます。但しDOM周りのチェックはしてくれないので、ラッパーコンポーネントでPropsが意図した通り刺さっているかみたいなのは計測対象外です
"scripts": {
"dev": "NODE_OPTIONS='--inspect' next",
"start": "next",
"build": "next build --profile && next export",
"serve": "serve -p 3000 ./out",
"test": "jest --watch",
"type-check": "tsc --noEmit && eslint .",
"headless-test": "npm run type-check && jest",
"coverage": "jest --coverage"
},
tsconfig.jest.json
jsx
をreact-jsx
にしてるのが味噌ですnext
を蹴るとtsconfig.json
を勝手にpreserve
に書き換えてくるのでその対策です
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"lib": ["dom", "es2017"],
"allowJs": true,
"checkJs": true,
"jsx": "react-jsx",
"sourceMap": true,
"noEmit": true,
"isolatedModules": true,
"strict": true,
"noImplicitAny": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"moduleResolution": "node",
"baseUrl": "./",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
tsconfig.json
- CRAでTSのReactプロジェクトを生やした時のものがベースですが、より厳格に設定したような気がします
isolatedModules: true
にしておくとソースファイルにexport
やimport
が存在しないときに怒ってくれて便利ですresolveJsonModule: true
もJSONをimport
可能になるので地味に便利です
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"lib": ["dom", "es2017"],
"allowJs": true,
"checkJs": true,
"jsx": "preserve",
"sourceMap": true,
"noEmit": true,
"isolatedModules": true,
"strict": true,
"noImplicitAny": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"moduleResolution": "node",
"baseUrl": "./",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
おまけ
tsconfig.jsonが開発サーバー起動時に書き換えられて困る
"jsx": "preserve",
にされるとVSCodeでは入力補完がイマイチ効かなかったり、import React from 'react';
を挿入しようとしてきて面倒です- 解決策としては
next
起動後にtsconfig.json
の"jsx": "preserve",
を"jsx": "react-jsx",
に置換してあげればよいですが、nextの動作に何かしらの悪影響があるかどうかまでは調べてないです- 自動化も単純なものならconcurrentlyとシェルスクリプトの組み合わせで簡単に実現できます
- 根本的にはTypeScriptの問題である可能性があるので、解決されるのを待つしかない気がします(Next.js側で対応すべきだとは思うが…
- Next.js側にもIssueが立ちました
今回説明した一式が欲しい人向け
以下のリポジトリにある内容は随時更新されるため、本記事投稿時点の内容と差異がある可能性があります
- Lycolia / ts-next-boilerplate に転がしてるのでどうぞ
- @lycolia/ts-boilerplate-generator-cli を使って生やすことも出来ます