PlaywrightでのMantineの要素取得まとめ
はじめに
PlaywrightでMantineの要素の取得方法についてまとめます。Playwrightは、ユーザー操作を記録してHTMLのコンポーネントに対するテストコードを生成できます。
最初に生成したテストコードでテストが動き続けるのが理想ですが、作成後のテストコードに修正が入ることは容易に想像できます。その際に、既存のテストコードを活かしつつ修正ができるように、またゼロからでもテストコードが記述できるようにコンポーネントの要素の取得の方法をまとめます。
上記のページの左タブのMANTINE CORE
に表示されているコンポーネントの取得方法を備忘録的にまとめます。Layout
・Compobox
・Overlays
・DataDisplay
は除いています。
実行環境
- 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
<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
<Chip defaultChecked aria-label={"Chip"}>
Awesome chip
</Chip>
テスト自動記録の画面(Chip)
await page.getByText('Awesome chip').click();
JsonInput
<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
<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
<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
<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
<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
<Radio label="I cannot be unchecked" aria-label={"Radio"} />
ユーザー操作記録の画面(Radio)
// 要素を押下したとき
await page.getByLabel('Radio').click();
await page.getByText('I cannot be unchecked').click();
Rating
<Rating defaultValue={2} aria-label={"Rating"} />
ユーザー操作記録の画面(Rating)
// 4番目の要素を押下したとき
await page.locator('div:nth-child(4) > .m-21342ee4 > .m-fae05d6a > .m-5662a89a').click();
SegmentedControl
<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
<Slider
color="blue"
marks={[
{ value: 20, label: "20%" },
{ value: 50, label: "50%" },
{ value: 80, label: "80%" },
]}
aria-label={"Slider"}
/>
ユーザー操作記録の画面(Slider)
Switch
<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
<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
<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
例外的にCopyButton
とFileButton
は、中のButton
要素にaria-label
が必要です。
ActionIcon
<ActionIcon variant="filled" aria-label="ActionIcon">
<IconAdjustments style={{ width: "70%", height: "70%" }} stroke={1.5} />
</ActionIcon>
ユーザー操作記録の画面(ActionIcon)
// 要素を押下したとき
await page.getByLabel('ActionIcon').click();
Button
<Button variant="filled" aria-label={"Button"}>Button</Button>
ユーザー操作記録の画面(Button)
// 要素を押下したとき
await page.getByLabel('Button', { exact: true }).click();
CloseButton
<CloseButton />
ユーザー操作記録の画面(CloseButton)
// 要素を押下したとき
await page.getByLabel('CloseButton').click();
CopyButton
<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
<>
<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
<UnstyledButton aria-label={"UnstyledButton"}>Button without styles</UnstyledButton>
ユーザー操作記録の画面(UnstyledButton)
// 要素を押下したとき
await page.getByLabel('UnstyledButton').click();
Navigation
Anchor
<Anchor href="https://mantine.dev/" target="_blank" aria-label={"Anchor"}>
Anchor
</Anchor>
ユーザー操作記録の画面(Anchor)
// 要素を押下したとき
await page.getByLabel('Anchor').click()
Breadcrumbs
<>
<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
<Burger opened={opened} onClick={toggle} aria-label="Burger" />
ユーザー操作記録の画面(Burger)
// 要素を押下したとき
await page.getByLabel('Burger').click();
NavLink
<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
<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
<>
<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
<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
<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
<Loader color="blue" aria-label={"Loader"} />
ユーザー操作記録の画面(Loader)
// 要素を押下したとき
await page.getByLabel('Loader').click();
Notification
<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
<Progress value={50} aria-label={"Progress"} />
ユーザー操作記録の画面(Progress)
// 要素を押下したとき
await page.getByLabel('Progress', { exact: true }).click();
RingProgress
<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
<Skeleton height={50} circle mb="xl" aria-label={"Skeleton"} />
ユーザー操作記録の画面(Skeleton)
// 要素を押下したとき
await page.getByLabel('Skeleton').click();
Typography
Blockquote
<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
<Code aria-label={"Code"}>React.createElement()</Code>
// 要素を押下したとき
await page.getByLabel('Code').click();
Highlight
<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
<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
<Text>
Highlight <Mark aria-label={"Mark"}>this chunk</Mark> of the text
</Text>
ユーザー操作記録の画面(Mark)
// 要素を押下したとき
await page.getByLabel('Mark').click();
Table
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
<Text size="xs" aria-label={"Text"}>
Extra small text
</Text>
ユーザー操作記録の画面(Text)
// 要素を押下したとき
await page.getByText('Extra small text').click();
Title
<Title order={1} aria-label={"Title"}>
This is h1 title
</Title>
ユーザー操作記録の画面(Title)
// 要素を押下したとき
await page.getByLabel('Title').click();
おわりに
とりあえず、備忘録的にまとめました。テストの内容として現時点では押下しかできていないのでそのほかの内容についても追記などしていきたいです。
参考文献
Discussion