🎭

PlaywrightでのMantineの要素取得まとめ

2024/03/07に公開

はじめに

PlaywrightでMantineの要素の取得方法についてまとめます。Playwrightは、ユーザー操作を記録してHTMLのコンポーネントに対するテストコードを生成できます。

最初に生成したテストコードでテストが動き続けるのが理想ですが、作成後のテストコードに修正が入ることは容易に想像できます。その際に、既存のテストコードを活かしつつ修正ができるように、またゼロからでもテストコードが記述できるようにコンポーネントの要素の取得の方法をまとめます。

https://mantine.dev/getting-started/

上記のページの左タブのMANTINE COREに表示されているコンポーネントの取得方法を備忘録的にまとめます。LayoutCompoboxOverlaysDataDisplayは除いています。

実行環境

  • macOS: 14.1
  • Next.js: 14.0
  • Node.js: 20.1
  • Mantine: 7.2.2
  • Playwright: 7.2.2

準備

以下のコマンドでPlaywrightをインストールします。

$ npm init playwright@latest

Initializing project in '.'
✔ Where to put your end-to-end tests? · tests
✔ Add a GitHub Actions workflow? (y/N) · true
✔ Install Playwright browsers (can be done manually via 'npx playwright install')? (Y/n) · true
Installing Playwright Test (npm install --save-dev @playwright/test)

以下のコマンドでヘッドレスブラウザでのテストが実行できます。

$ npx playwright test

以下のコマンドで、画面遷移を実際に見つつテストを確認できます。

$ npx playwright test --ui

以下のコマンドで、ユーザー操作を記録してテストコードを生成できます。本記事では以下のコマンドで生成したコードを記録していきます。

$ npx playwright codegen

テスト項目

それぞれのコンポーネントごとにテストコードを列挙していきます。

各種コンポーネントにおいて、aria-labelを指定するとpage.getByLabel(aria-label)で要素を取得できます。ただ要素を指定できないコンポーネントなどもあるため適宜列挙していきます。

  • ユーザー操作記録: $ npx playwright codegenで起動するブラウザにてユーザー操作を記録して作成されたコード
  • テスト直接記述:直接書いたコード

aria-labelについて

WAI-ARIAで策定されているアクセシビリティに関する要素です。ウェブの読み上げなどにも用いられており、正しく設定することが求められています。テスト用に設定しつつアクセシビリティにも配慮できるようになるのはいいなと思いました。ただ多用しすぎるのもかえってわかりづらくなるので注意して記述するようにしてください。

Inputs

Checkbox

test/mantine-inputs.tsx
<Checkbox defaultChecked label="I agree to sell my privacy" aria-label={"Checkbox"} />


テスト自動記録の画面(Checkbox)

テストコード
// 要素を押下したとき
await page.getByLabel('Checkbox').check();
await page.getByLabel('Checkbox').uncheck();

// テキストを押下したとき
await page.getByText('I agree to sell my privacy').first().click();

Chip

test/mantine-inputs.tsx
<Chip defaultChecked aria-label={"Chip"}>
  Awesome chip
</Chip>


テスト自動記録の画面(Chip)

テストコード
await page.getByText('Awesome chip').click();

JsonInput

test/mantine-inputs.tsx
<JsonInput
  label="Your package.json"
  placeholder="Textarea will autosize to fit the content"
  validationError="Invalid JSON"
  formatOnBlur
  autosize
  minRows={4}
  aria-label={"JsonInput"}
/>


ユーザー操作記録の画面(JsonInput)

テストコード
// 選択した場合
await page.getByPlaceholder('Textarea will autosize to fit').click();
// 文字を入力した場合
await page.getByPlaceholder('Textarea will autosize to fit').fill('hello');

NativeSelect

test/mantine-inputs.tsx
<NativeSelect
  label="Input label"
  description="Input description"
  data={["React", "Angular", "Vue"]}
  aria-label={"NativeSelect"}
/>


ユーザー操作記録の画面(NativeSelect)

テストコード
// 各要素を選択した場合
await page.getByLabel('NativeSelect').selectOption('React');
await page.getByLabel('NativeSelect').selectOption('Angular');
await page.getByLabel('NativeSelect').selectOption('Vue');

NumberInput

test/mantine-inputs.tsx
<NumberInput
  label="NumberInput label"
  description="Input description"
  placeholder="Input placeholder"
  aria-label={"NumberInput"}
/>


ユーザー操作記録の画面(NumberInput)

テストコード
// 要素を押下したとき
await page.getByLabel('NumberInput').click();
// 要素に値を入力したとき
await page.getByLabel('NumberInput').fill('111');
// 要素の右端の上ボタンを押下したとき
await page.locator('button').first().click();
// 要素の右端の下ボタンを押下したとき
await page.locator('button').nth(1).click();

PasswordInput

test/mantine-inputs.tsx
<PasswordInput
  label="PasswordInput label"
  description="Input description"
  placeholder="Input placeholder"
  aria-label={"PasswordInput"}
/>


ユーザー操作記録の画面(PasswordInput)

テストコード
// 要素を押下したとき
await page.getByLabel('PasswordInput').click();
// 要素に値を入力したとき
await page.getByLabel('PasswordInput').fill('hello');
// 要素の右端のパスワード表示・非表示ボタンを押下したとき
await page.locator('div').filter({ hasText: /^PasswordInput labelInput description$/ }).locator('button').click();

PinInput

test/mantine-inputs.tsx
<PinInput aria-label={"PinInput"} />


ユーザー操作記録の画面(PinInput)

テストコード
// 要素を押下したとき
await page.getByLabel('PinInput').nth(1).click();
// 要素に値を順次入力して行ったとき
await page.getByLabel('PinInput').nth(1).fill('1');
await page.getByLabel('PinInput').nth(2).fill('2');
await page.getByLabel('PinInput').nth(3).fill('3');
await page.getByLabel('PinInput').nth(4).fill('4');

Radio

test/mantine-inputs.tsx
<Radio label="I cannot be unchecked" aria-label={"Radio"} />


ユーザー操作記録の画面(Radio)

テストコード
// 要素を押下したとき
await page.getByLabel('Radio').click();
await page.getByText('I cannot be unchecked').click();

Rating

test/mantine-inputs.tsx
<Rating defaultValue={2} aria-label={"Rating"} />


ユーザー操作記録の画面(Rating)

テストコード
// 4番目の要素を押下したとき
await page.locator('div:nth-child(4) > .m-21342ee4 > .m-fae05d6a > .m-5662a89a').click();

SegmentedControl

test/mantine-inputs.tsx
<SegmentedControl data={["React", "Angular", "Vue"]} aria-label={"SegmentedControl"} />


ユーザー操作記録の画面(SegmentedControl)

テストコード
// 各要素を押下したとき
await page.getByLabel('SegmentedControl').getByText('React').click();
await page.getByLabel('SegmentedControl').getByText('Angular').click();
await page.getByLabel('SegmentedControl').getByText('Vue').click();

Slider

test/mantine-inputs.tsx
<Slider
  color="blue"
  marks={[
    { value: 20, label: "20%" },
    { value: 50, label: "50%" },
    { value: 80, label: "80%" },
  ]}
  aria-label={"Slider"}
/>


ユーザー操作記録の画面(Slider)

Switch

test/mantine-inputs.tsx
<Switch defaultChecked label="I agree to sell my privacy" aria-label={"Switch"} />


ユーザー操作記録の画面(Switch)

テストコード
// 要素を押下する処理
await page.getByText('I agree to sell my privacy').nth(1).click();

Textarea

test/mantine-inputs.tsx
<Textarea
  label="Input label"
  description="Input description"
  placeholder="Input placeholder"
  aria-label={"Textarea"}
/>


ユーザー操作記録の画面(Textarea)

テストコード
// 要素を押下したとき
await page.getByLabel('Textarea').click();
// 要素に入力したとき
await page.getByLabel('Textarea').fill('hello');

TextInput

test/mantine-inputs.tsx
<TextInput
  label="Input label"
  description="Input description"
  placeholder="Input placeholder"
  aria-label={"TextInput"}
/>


ユーザー操作記録の画面(TextInput)

テストコード
// 要素を押下したとき
await page.getByLabel('TextInput').click();
// 要素に入力したとき
await page.getByLabel('TextInput').fill('hello');

Buttons

例外的にCopyButtonFileButtonは、中のButton要素にaria-labelが必要です。

ActionIcon

test/mantine-buttons.tsx
<ActionIcon variant="filled" aria-label="ActionIcon">
  <IconAdjustments style={{ width: "70%", height: "70%" }} stroke={1.5} />
</ActionIcon>


ユーザー操作記録の画面(ActionIcon)

テストコード
// 要素を押下したとき
await page.getByLabel('ActionIcon').click();

Button

test/mantine-buttons.tsx
<Button variant="filled" aria-label={"Button"}>Button</Button>


ユーザー操作記録の画面(Button)

テストコード
// 要素を押下したとき
await page.getByLabel('Button', { exact: true }).click();

CloseButton

test/mantine-buttons.tsx
<CloseButton />


ユーザー操作記録の画面(CloseButton)

テストコード
// 要素を押下したとき
await page.getByLabel('CloseButton').click();

CopyButton

test/mantine-buttons.tsx
<CopyButton value="https://mantine.dev">
  {({ copied, copy }) => (
    <Button aria-label={"CopyButton"} color={copied ? "teal" : "blue"} onClick={copy}>
      {copied ? "Copied url" : "Copy url"}
    </Button>
  )}
</CopyButton>


ユーザー操作記録の画面(CopyButton)

テストコード
// 要素を押下したとき
await page.getByLabel('CopyButton').click();

FileButton

test/mantine-buttons.tsx
<>
  <Group justify="center">
    <FileButton onChange={setFile} accept="image/png,image/jpeg">
      {(props) => <Button {...props} aria-label={"FileButton"}>Upload image</Button>}
    </FileButton>
  </Group>

  {file && (
    <Text size="sm" ta="center" mt="sm">
      Picked file: {file.name}
    </Text>
  )}
</>


ユーザー操作記録の画面(FileButton)

テストコード
// 要素を押下したとき
await page.getByLabel('FileButton').click();

UnstyledButton

test/mantine-buttons.tsx
<UnstyledButton aria-label={"UnstyledButton"}>Button without styles</UnstyledButton>


ユーザー操作記録の画面(UnstyledButton)

テストコード
// 要素を押下したとき
await page.getByLabel('UnstyledButton').click();

Anchor

test/mantine-navigation.tsx
<Anchor href="https://mantine.dev/" target="_blank" aria-label={"Anchor"}>
  Anchor
</Anchor>


ユーザー操作記録の画面(Anchor)

テストコード
// 要素を押下したとき
await page.getByLabel('Anchor').click()
test/mantine-navigation.tsx
<>
  <Breadcrumbs>{items}</Breadcrumbs>
  <Breadcrumbs separator="→" separatorMargin="md" mt="xs">
    {items}
  </Breadcrumbs>
</>


ユーザー操作記録の画面(Breadcrumbs1)


ユーザー操作記録の画面(Breadcrumbs2)


ユーザー操作記録の画面(Breadcrumbs3)

テストコード
// 各要素を押下したとき
await page.getByLabel('Mantine', { exact: true }).click();
await page.getByLabel('Mantine hooks').click();
await page.getByLabel('use-id').click();

Burger

test/mantine-navigation.tsx
<Burger opened={opened} onClick={toggle} aria-label="Burger" />


ユーザー操作記録の画面(Burger)

テストコード
// 要素を押下したとき
await page.getByLabel('Burger').click();
test/mantine-navigation.tsx
<NavLink
  href="#required-for-focus"
  label="With icon"
  leftSection={<IconHome2 size="1rem" stroke={1.5} />}
  aria-label={"NavLink"}
/>


ユーザー操作記録の画面(NavLink)

テストコード
// 要素を押下したとき
await page.getByLabel('NavLink').click();

Pagination

test/mantine-navigation.tsx
<Pagination total={10} aria-label={"Pagination"}/>


ユーザー操作記録の画面(Pagination1)


ユーザー操作記録の画面(Pagination5)

テストコード
// 「1」の要素を押下したとき
await page.getByRole('button', { name: '1', exact: true }).click();
// 「5」の要素を押下したとき
await page.getByRole('button', { name: '5' }).click();
// 「6」の要素を押下したとき
await page.getByRole('button', { name: '6' }).click();
// 「7」の要素を押下したとき
await page.getByRole('button', { name: '7' }).click();
// 「10」の要素を押下したとき
await page.getByRole('button', { name: '10' }).click();

Stepper

test/mantine-navigation.tsx
<>
  <Stepper active={active} onStepClick={setActive}>
    <Stepper.Step label="First step" description="Create an account" aria-label={"Step1"}>
      Step 1 content: Create an account
    </Stepper.Step>
    <Stepper.Step label="Second step" description="Verify email" aria-label={"Step2"}>
      Step 2 content: Verify email
    </Stepper.Step>
    <Stepper.Step label="Final step" description="Get full access" aria-label={"Step3"}>
      Step 3 content: Get full access
    </Stepper.Step>
    <Stepper.Completed>Completed, click back button to get to previous step</Stepper.Completed>
  </Stepper>

  <Group justify="center" mt="xl">
    <Button variant="default" onClick={prevStep}>
      Back
    </Button>
    <Button onClick={nextStep}>Next step</Button>
  </Group>
</>


ユーザー操作記録の画面(Stepper1)

ユーザー操作記録の画面(Stepper2)

ユーザー操作記録の画面(Stepper3)

テストコード
// 要素を押下したとき
await page.getByLabel('Step1').click();
await page.getByLabel('Step2').click();
await page.getByLabel('Step3').click();

Tabs

test/mantine-navigation.tsx
<Tabs defaultValue="gallery">
  <Tabs.List>
    <Tabs.Tab value="gallery" leftSection={<IconPhoto style={iconStyle} />} aria-label={"Tab1"}>
      Gallery
    </Tabs.Tab>
    <Tabs.Tab value="messages" leftSection={<IconMessageCircle style={iconStyle} />} aria-label={"Tab2"}>
      Messages
    </Tabs.Tab>
    <Tabs.Tab value="settings" leftSection={<IconSettings style={iconStyle} />} aria-label={"Tab3"}>
      Settings
    </Tabs.Tab>
  </Tabs.List>

  <Tabs.Panel value="gallery">Gallery tab content</Tabs.Panel>

  <Tabs.Panel value="messages">Messages tab content</Tabs.Panel>

  <Tabs.Panel value="settings">Settings tab content</Tabs.Panel>
</Tabs>


ユーザー操作記録の画面(Tab1)

ユーザー操作記録の画面(Tab2)

ユーザー操作記録の画面(Tab3)

テストコード
// 要素を押下したとき
await page.getByLabel('Tab1').click();
await page.getByLabel('Tab2').click();
await page.getByLabel('Tab3').click();

Feedback

Alert

test/mantine-feedback.tsx
<Alert variant="light" color="blue" title="Alert title" icon={<IconInfoCircle />} aria-label={"Alert"}>
  Lorem ipsum dolor sit, amet consectetur adipisicing elit. At officiis, quae tempore necessitatibus placeat
  saepe.
</Alert>


ユーザー操作記録の画面(Alert)

テストコード
// 要素を押下したとき(自動選択)
await page.getByLabel('Alert title').click();
// 要素を押下したとき(直接記述)
await page.getByLabel('Alert').click();

Loader

test/mantine-feedback.tsx
<Loader color="blue" aria-label={"Loader"} />


ユーザー操作記録の画面(Loader)

テストコード
// 要素を押下したとき
await page.getByLabel('Loader').click();

Notification

test/mantine-feedback.tsx
<Notification title="We notify you that" aria-label={"Notification"}>
  You are now obligated to give a star to Mantine project on GitHub
</Notification>


ユーザー操作記録の画面(Notification)

テストコード
// 要素を押下したとき
await page.getByLabel('Notification').click();

Progress

test/mantine-feedback.tsx
<Progress value={50} aria-label={"Progress"} />


ユーザー操作記録の画面(Progress)

テストコード
// 要素を押下したとき
await page.getByLabel('Progress', { exact: true }).click();

RingProgress

test/mantine-feedback.tsx
<RingProgress
  aria-label={"RingProgress"}
  label={
    <Text size="xs" ta="center">
      Application data usage
    </Text>
  }
  sections={[
    { value: 40, color: "cyan" },
    { value: 15, color: "orange" },
    { value: 15, color: "grape" },
  ]}
/>


ユーザー操作記録の画面(RingProgress)

テストコード
// 要素を押下したとき(直接記述)
await page.getByLabel('RingProgress').click();

Skeleton

test/mantine-feedback.tsx
<Skeleton height={50} circle mb="xl" aria-label={"Skeleton"} />


ユーザー操作記録の画面(Skeleton)

テストコード
// 要素を押下したとき
await page.getByLabel('Skeleton').click();

Typography

Blockquote

test/mantine-typography.tsx
<Blockquote color="blue" cite="– Forrest Gump" icon={<IconInfoCircle />} mt="xl" aria-label={"Blockquote"}>
  Life is like an npm install – you never know what you are going to get.
</Blockquote>


ユーザー操作記録の画面(Blockquote)

テストコード
// 要素を押下したとき
await page.getByLabel('Blockquote').click();

Code

test/mantine-typography.tsx
<Code aria-label={"Code"}>React.createElement()</Code>
テストコード
// 要素を押下したとき
await page.getByLabel('Code').click();

Highlight

test/mantine-typography.tsx
<Highlight highlight="this" aria-label={"Highlight"}>
  Highlight This, definitely THIS and also this!
</Highlight>


ユーザー操作記録の画面(Highlight1)

ユーザー操作記録の画面(Highlight2)

ユーザー操作記録の画面(Highlight3)

ユーザー操作記録の画面(Highlight4)

テストコード
// 要素を押下したとき
await page.getByText('This', { exact: true }).click();
await page.getByText('THIS', { exact: true }).click();
await page.getByText('this', { exact: true }).click();
// ハイライトされた箇所ではなく、要素全体を取得する
await page.getByLabel('Highlight').click();

List

test/mantine-typography.tsx
<List>
  <List.Item aria-label={"List.Item1"}>Clone or download repository from GitHub</List.Item>
  <List.Item aria-label={"List.Item2"}>Install dependencies with yarn</List.Item>
  <List.Item aria-label={"List.Item3"}>To start development server run npm start command</List.Item>
  <List.Item aria-label={"List.Item4"}>Run tests to make sure your changes do not break the build</List.Item>
  <List.Item aria-label={"List.Item5"}>Submit a pull request once you are done</List.Item>
</List>


ユーザー操作記録の画面(List)

テストコード
// 要素を押下したとき
await page.getByLabel('List.Item1').click();

Mark

test/mantine-typography.tsx
<Text>
  Highlight <Mark aria-label={"Mark"}>this chunk</Mark> of the text
</Text>


ユーザー操作記録の画面(Mark)

テストコード
// 要素を押下したとき
await page.getByLabel('Mark').click();

Table

test/mantine-typography.tsx
const elements = [
  { position: 6, mass: 12.011, symbol: "C", name: "Carbon" },
  { position: 7, mass: 14.007, symbol: "N", name: "Nitrogen" },
  { position: 39, mass: 88.906, symbol: "Y", name: "Yttrium" },
  { position: 56, mass: 137.33, symbol: "Ba", name: "Barium" },
  { position: 58, mass: 140.12, symbol: "Ce", name: "Cerium" },
];

const rows = elements.map((element, idx) => (
  <Table.Tr key={element.name}>
    <Table.Td aria-label={`Table.Tr${idx}:position`}>{element.position}</Table.Td>
    <Table.Td aria-label={`Table.Tr${idx}:name`}>{element.name}</Table.Td>
    <Table.Td aria-label={`Table.Tr${idx}:symbol`}>{element.symbol}</Table.Td>
    <Table.Td aria-label={`Table.Tr${idx}:mass`}>{element.mass}</Table.Td>
  </Table.Tr>
));

(中略)

<Table>
  <Table.Thead>
    <Table.Tr>
      <Table.Th aria-label={`Table.Th:position`}>Element position</Table.Th>
      <Table.Th aria-label={`Table.Th:name`}>Element name</Table.Th>
      <Table.Th aria-label={`Table.Th:symbol`}>Symbol</Table.Th>
      <Table.Th aria-label={`Table.Th:mass`}>Atomic mass</Table.Th>
    </Table.Tr>
  </Table.Thead>
  <Table.Tbody>{rows}</Table.Tbody>
</Table>


ユーザー操作記録の画面(Table)

ユーザー操作記録の画面(Table)

ユーザー操作記録の画面(Table)

テストコード
// 要素を押下したとき
await page.getByLabel('Table.Th:position').click();
await page.getByLabel('Table.Tr0:position').click();

Text

test/mantine-typography.tsx
<Text size="xs" aria-label={"Text"}>
  Extra small text
</Text>


ユーザー操作記録の画面(Text)

テストコード
// 要素を押下したとき
await page.getByText('Extra small text').click();

Title

test/mantine-typography.tsx
<Title order={1} aria-label={"Title"}>
  This is h1 title
</Title>


ユーザー操作記録の画面(Title)

テストコード
// 要素を押下したとき
await page.getByLabel('Title').click();

おわりに

とりあえず、備忘録的にまとめました。テストの内容として現時点では押下しかできていないのでそのほかの内容についても追記などしていきたいです。

参考文献

https://zenn.dev/tnyo43/articles/39e4caa321d0aa

https://zenn.dev/takky94/articles/4daa73dd516bf3

GitHubで編集を提案

Discussion