讓包覆在 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 出來了,順利解決!

Ref
Introduction to Web AccessibilityUsing styled-components to build a corporate landing page