- 投稿日:
以下のようなコードを書いたときに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');
});
- 投稿日:
書いてなさ過ぎて忘れるので備忘録として書き出しておく。必要最低限の設定なので要件が他にある場合は追加が必要。
React(SPA)
SPAなのでパスがなければ全部index.htmlに飛ばす。原理はどれも同じなのでVueやAngularなどのSPAもこれで行けると思う。ケツの=404
がないと無限リダイレクトを起こす。
server {
listen 80;
location / {
root /path/to;
index index.html;
try_files $uri /index.html =404;
}
}
Next.js(SSR)
SSRなのでNext.jsのサーバープロセスにリバースプロキシする。別に直結してもいいが手前にルーティング機構があるほうが便利である。
map
ステートメントがあるのは、これがないとiOS Safariでアクセスできないため
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
location / {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass http://localhost:3000;
}
}
Next.js(SSG)
SPAではないのでパスがなければ同名のhtmlファイルに飛ばす。ケツの=404
がないと無限リダイレクトを起こす。
server {
listen 80;
location / {
root /path/to;
index index.html;
try_files $uri $uri.html =404;
}
}
- 投稿日:
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());
});
});
});
参考記事
- 投稿日:
別にReactに限った話でもないのですが、実務で悩ましいコードにあたって頭を抱えてる内にハゲてきたので、ハゲがこれ以上進まない事を祈り書きました。いやいっそのこと全ハゲになりたいですが、それはさておき
とりあえず個人的には以下2点を意識すれば起きないとは思うので、Next.jsを使った簡単なサンプルを交えながら3例ほどケーススタディ形式で紹介していきます
- 密結合にしない
- シンプルに実装する
挙げている事例については例示用にフルスクラッチで書いています(主題と関係ないコードは端折ってます
ここはこうした方がより良いのでは?などのご意見があれば是非コメントいただけると嬉しいです
コンポーネントのOptional引数のオーバーライド
コンポーネント引数をコンポーネント側で書き換えるような実装はどうかなと思います
問題点
- 親の預かり知らぬところで値が設定されている
- 誰かが知らずに設定値をすり替えたりしたら不具合が起きそうです
- 型のコメントと設定値が異なる
- 型のコメントと実装が異なるので、誰かが直すかもしれません
- すると、コンポーネントを参照している全コンポーネントに影響が波及します
- そもそも型と実装は本質的に関係ないので、こういった運用はNG
- 型のコメントと実装が異なるので、誰かが直すかもしれません
一例
type AccountContainerProps = {
email: string;
// デフォルトtrue
isUpdate?: boolean;
// デフォルトfalse
gotNotify?: boolean;
};
export const AccountContainer = ({
isUpdate = true,
gotNotify = true,
...props
}: AccountContainerProps) => {
...
}
改善案
呼び元で明示的に指定するように変更しています
これによって親は子の実装を知る必要がなくなり、責務がそこで閉じるようになりました
改善点
- 親の預かり知らぬところで値が設定されなくなった
- これで子コンポーネントで値が変わることに怯える必要はなくなりました
- 型の初期値コメントを削除できた
- 実装と一致する保証がないコメントは削除するべきでしょう
type AccountContainerProps = {
email: string;
isUpdate: boolean;
gotNotify: boolean;
};
export const AccountContainer = (props: AccountContainerProps) => {
...
}
コンポーネントのOptional引数の多重オーバーライド
コンポーネントのOptional引数のオーバーライドが多重化されている上に、なんか途中で更に書き換えられているとかいう地獄
どうしてそんなことをするのか…
一例
type AccountTemplateProps = {
email: string;
// デフォルトtrue
isUpdate?: boolean;
// デフォルトfalse
gotNotify?: boolean;
from: 'register' | 'update';
};
export const AccountTemplate = ({
isUpdate = false,
gotNotify = true,
...props
}: AccountTemplateProps) => {
if (props.from === 'register') isUpdate = false;
return <AccountContainer {...props} />;
};
type AccountContainerProps = {
email: string;
// デフォルトtrue
isUpdate?: boolean;
// デフォルトfalse
gotNotify?: boolean;
};
export const AccountContainer = ({
isUpdate = true,
gotNotify = true,
...props
}: AccountContainerProps) => {
...
}
改善案
前項のように明示的に値を渡してあげるようにしましょう
直列に分散されたコンポーネント
コンポーネントの中にコンポーネントがネストされ続けてるパターンです
一見して何をしているのか分かりづらい上、StateやらHookやら色んな処理が各コンポーネントに分散配置されていることもあります
問題点
- 親からみると子が何をしているのかが分かりづらい
- コンポーネント名が意味をなしていない(責務が別れていない)
- 子コンポーネントが状態を持っているため、親コンポーネントでハンドリングができない
- 子コンポーネントのロジック変更が参照している全コンポーネントのロジックに波及する
一例
親コンポーネント
RegisterHeader
なる物が差し込まれていることだけがわかります
このコンポーネントが何をするのかはパッと見ではよくわかりません
const AccountUpdatePage = () => {
return <Register header={<RegisterHeader />} />;
};
ラッピングしているコンポーネント
ラッパーなのでこのコンポーネントそのものは何をしているのかわかりません
type RegisterProps = {
header: JSX.Element;
};
export const Register = (props: RegisterProps) => {
return <>{props.header}</>;
};
差し込まれているコンポーネントの中身
どうやらRegisterHeader
はRegisterContent
を含むようです
なんでヘッダーの中にコンテンツがあるのでしょうか…
export const RegisterHeader = () => {
return (
<>
<p>head</p>
<RegisterContent />
</>
);
};
差し込まれているコンポーネントの子
現在のパスに応じて叩くAPIを変えるような実装がされていますが、もしパスが変わったり増えたりしたらこのコンポーネントの実装を知らない限り面倒なことになります
export const RegisterContent = () => {
const rt = useRouter();
const currentPath = rt.pathname;
const url =
currentPath === 'register'
? 'https://example.com/kaiin/touroku'
: 'https://example.com/kaiin/koshin';
const [username, setUsername] = useState<string | undefined>(undefined);
const onSubmit = (ev: React.FormEvent<HTMLFormElement>) => {
ev.preventDefault();
axios.post(url, {
username,
});
};
return (
<>
<form onSubmit={onSubmit}>
<input
type="text"
value={username}
onChange={(ev) => setUsername(ev.target.value)}
/>
<button>送信</button>
</form>
<RegisterFooter />
</>
);
};
export const RegisterFooter = () => {
return <p>foot</p>;
};
改善案
改善点
- 親から子への見通しが改善された
- コンポーネント名が名前の通りの意味を持つようになった
- ヘッダーはヘッダー、フォームはフォーム、フッターはフッターの責務だけに集中できます
Register
とか言う謎コンポーネントも姿を消しました
- 状態をすべて親に集約した
- 子コンポーネントのロジック変更が起きる確率が減った
- 子コンポーネント側のロジックを減らし、親から渡されるコールバックで行うようにしたため、子コンポーネントのロジック変更が他に影響する確率が減りました
親コンポーネント
- ひとまずヘッダーと入力フォーム、フッターがあるんだなという事が解るようにはなったと思います
- 状態を親に集約したのでAPI叩く時のURIも態々パスから判断しなくて良くなったのでコードの複雑性が減っています
const usePageState = () => {
const [username, setUsername] = useState('');
return {
username,
setUsername,
};
};
const onSubmit = (username: string) => {
axios.post('https://example.com/kaiin/koshin', {
username,
});
};
const AccountUpdatePage = () => {
const ps = usePageState();
return (
<>
<RegisterHeader />
<RegisterForm
onChangeUsername={ps.setUsername}
username={ps.username}
onSubmit={() => onSubmit(ps.username)}
/>
<RegisterFooter />
</>
);
};
ヘッダー
export const RegisterHeader = () => {
return <p>head</p>;
};
入力フォーム
type RegisterFormProps = {
username: string;
onChangeUsername: (value: string) => void;
onSubmit: () => void;
};
const onChangeUsername = (
ev: React.ChangeEvent<HTMLInputElement>,
setState: (value: string) => void
) => {
const value = ev.target.value;
setState(value);
};
const onSubmit = (
ev: React.FormEvent<HTMLFormElement>,
onSubmit: () => void
) => {
ev.preventDefault();
onSubmit();
};
export const RegisterForm = (props: RegisterFormProps) => {
return (
<>
<form onSubmit={(ev) => onSubmit(ev, props.onSubmit)}>
<input
type="text"
value={props.username}
onChange={(ev) => onChangeUsername(ev, props.onChangeUsername)}
/>
<button>送信</button>
</form>
</>
);
};
フッター
export const RegisterFooter = () => {
return <p>foot</p>;
};