讓包覆在 Material-UI Hidden 的內容順利產生 snapshot
2021-08-14 · 6 min read
前言
最近開始花心思研究前端測試,而手邊可以拿來練習的好對象就是我的個人網站,於是開始拿它來動手寫測試。
關於寫測試,我自己選擇的工具主要是:
- Jest
- React Testing Library
我這邊就不深入介紹這些工具了,直接進入正題吧。
Snapshot Testing
這次文章描述的狀況是關於 Snapshot Testing:
Snapshot tests are a very useful tool whenever you want to make sure your UI does not change unexpectedly.
簡單來說,我們可以透過相關工具來產生 UI 的 snapshot,而這些 snapshot 代表的意義就是當下你的 UI 在 DOM tree 上呈現的樣子,所以他們也應該要被 git 給記錄版本。
如果之後你的 UI 變動了,就可以透過這些 snapshot 來察覺兩者差異,以避免開發者在開發過程中不小心動到 UI 卻沒注意到。
測試對象
我這次要用 Jest 來產生 Header 的 snapshot,你可以在我的Github 連結看這個 component 完整的內容,而它的部分內容如下:
<Container>
<StyledToolbar component="nav" disableGutters>
<SiteName color="textPrimary" variant="h5" href="/" underline="none">
{siteTitle}
</SiteName>
...
<Hidden xsDown>
<Toolbar disableGutters>
{tabLinks &&
tabLinks.map((link) => (
<NavLink
$active={isTabActive(link)}
key={link.title}
href={link.url}
>
{link.title}
</NavLink>
))}
</Toolbar>
</Hidden>
</StyledToolbar>
</Container>
其中 <Hidden>
是 Material-UI 提供 components,我們可以透過它讓內容在某些 screen width 下隱藏,而這邊 <Hidden xsDown>
的意思就是當螢幕寬度在 xs 以下的時候,其中的內容會隱藏。
動手寫測試
這邊的寫法有參考 Kent C. Dodds 的文章,先寫一個 render component 的函數,然後在每個 test 裡面 call function 就不用每次都用 beforeEach 產生 render,這兩種作法都行只是我覺得 call function 比較好懂。
__tests__/header
import React from "react";
import { render } from "@testing-library/react";
import Header from "../Header";
import { theme } from "../../constants/theme";
import config from "../../../data/SiteConfig";
function renderHeader(props) {
const utils = render(<Header config={config} theme={theme} />);
const header = utils.getByRole("banner");
return { ...utils, header };
}
test("Header snapshot", () => {
const { header } = renderHeader();
expect(header).toMatchSnapshot();
});
就這樣直接跑測試,產生的 snapshot 會像這樣:
__tests__/__snapshots__/header
exports[`Header snapshot 1`] = `
<header
class="MuiPaper-root MuiAppBar-root MuiAppBar-positionFixed MuiAppBar-colorPrimary sc-gsTCUz lbsyTM mui-fixed MuiPaper-elevation4"
style="webkit-transform: none; transform: none; webkit-transition: -webkit-transform 225ms cubic-bezier(0.0, 0, 0.2, 1) 0ms; transition: transform 225ms cubic-bezier(0.0, 0, 0.2, 1) 0ms;"
>
<div
class="MuiContainer-root MuiContainer-maxWidthLg"
>
<nav
class="MuiToolbar-root MuiToolbar-regular sc-dlfnbm cZnpQH"
>
<a
class="MuiTypography-root MuiLink-root MuiLink-underlineNone sc-bdfBwQ ckOduL MuiTypography-h5 MuiTypography-colorTextPrimary"
href="/"
>
Arsene's Alibi
</a>
</nav>
</div>
</header>
`;
從 snapshot 來看是沒有 header 右邊的 nav 連結,也就是上面我提到用 <Hidden>
包覆的內容。
這樣的話 snapshot 就不能發揮它的用處,因為之後要是更改包在 <Hidden>
的內容的話,snapshot 一樣察覺不到差異。
我猜測是因為跑測試時不會真的有 window.innerWidth
因此沒有 render。
改寫
所幸 Material UI 有提供設定 initialWidth
的方法,另外我再參考 React Testing Library 的 Custom Render 寫法,為測試寫了一個 utils:
test-utils
import React from "react";
import { ThemeProvider, createGlobalStyle } from "styled-components";
import { createMuiTheme, MuiThemeProvider } from "@material-ui/core/styles";
import { render } from "@testing-library/react";
import { theme } from "../constants/theme";
const SizeWrapper = ({ children }) => {
const initTheme = createMuiTheme({
props: { MuiWithWidth: { initialWidth: "lg" } },
});
return (
<MuiThemeProvider theme={initTheme}>
<ThemeProvider theme={theme}>{children}</ThemeProvider>
</MuiThemeProvider>
);
};
const customRender = (ui, options) =>
render(ui, { wrapper: SizeWrapper, ...options });
// re-export everything
export * from "@testing-library/react";
// override render method
export { customRender as render };
這個 utils 預設 width 是 lg,並把 ThemeProvider 也一起當 Wrapper,省去每次寫測試時都要另外傳 theme 進去。
再來改寫我的測試檔:
__tests__/header
import React from "react";
import Header from "../Header";
import config from "../../../data/SiteConfig";
import { render } from "../../utils/test-utils";
function renderHeader(props) {
const utils = render(<Header config={config} />);
const header = utils.getByRole("banner");
return { ...utils, header };
}
test("Header snapshot at lg screen width", () => {
const { header } = renderHeader();
expect(header).toMatchSnapshot();
});
改完之後,產生的 snapshot 會像這樣:
exports[`Header snapshot at lg screen width 1`] = `
<header
class="MuiPaper-root MuiAppBar-root MuiAppBar-positionFixed MuiAppBar-colorPrimary sc-gsTCUz lbsyTM mui-fixed MuiPaper-elevation4"
style="webkit-transform: none; transform: none; webkit-transition: -webkit-transform 225ms cubic-bezier(0.0, 0, 0.2, 1) 0ms; transition: transform 225ms cubic-bezier(0.0, 0, 0.2, 1) 0ms;"
>
<div
class="MuiContainer-root MuiContainer-maxWidthLg"
>
<nav
class="MuiToolbar-root MuiToolbar-regular sc-dlfnbm cZnpQH"
>
<a
class="MuiTypography-root MuiLink-root MuiLink-underlineNone sc-bdfBwQ HrXuG MuiTypography-h5 MuiTypography-colorTextPrimary"
href="/"
>
Arsene's Alibi
</a>
<div
class="MuiToolbar-root MuiToolbar-regular"
>
<a
aria-disabled="false"
class="MuiButtonBase-root MuiButton-root MuiButton-text sc-hKgILt xTIhp"
href="/"
tabindex="0"
>
<span
class="MuiButton-label"
>
Home
</span>
<span
class="MuiTouchRipple-root"
/>
</a>
<a
aria-disabled="false"
class="MuiButtonBase-root MuiButton-root MuiButton-text sc-hKgILt blgYtI"
href="/about"
tabindex="0"
>
<span
class="MuiButton-label"
>
About
</span>
<span
class="MuiTouchRipple-root"
/>
</a>
<a
aria-disabled="false"
class="MuiButtonBase-root MuiButton-root MuiButton-text sc-hKgILt blgYtI"
href="/blog"
tabindex="0"
>
<span
class="MuiButton-label"
>
Blog
</span>
<span
class="MuiTouchRipple-root"
/>
</a>
</div>
</nav>
</div>
</header>
`;
很明顯地,這次有順利把 <Hidden>
的內容給 render 出來了,順利解決!