
Shadcn/uiで作るログイン画面
Shadcn/uiを利用してログインを画面作成。UIのほかZodによるバリデーションなどを実践
初回公開日
最終更新日
Shadcn/uiを利用してログイン画面のUIを作成していきます。Zodを利用したバリデーションについても実践します。
0. 事前準備
準備段階は下記の通りです。詳細はそれぞれ過去記事で詳しい実践記録があるので、参照してください。
Next.jsとshadcn/uiのインストール
/xxx/xxx/project
配下にインストールします。プロジェクトディレクトリはlogin-form
とします。Next.jsインストール:
zsh
1cd /xxx/xxx/project
2npx create-next-app@latest
Next.jsインストール:
zsh
1cd login-form
2npx shadcn@latest init
layout.tsxとglobals.cssを編集:
これは少し長くなってしまいますので、該当の過去記事:Next.js+shadcn/uiのインストールと基本動作のまとめ を参照してください。
(任意)Tailwind CSS用のプリティアのインストール
これは任意ですが、私はCursorエディタを利用していますので、Tailwind CSS用のプリティアをインストールします。
zsh
1npm install -D prettier prettier-plugin-tailwindcss
プロジェクトディレクトリ直下に
.prettierrc
を作成して、下記のように記述json : .prettierrc
1{
2 "plugins": ["prettier-plugin-tailwindcss"]
3}
(任意)Tailwind CSS用のプリティアのインストール
これもやはり任意ですが、E2Eテスト用にplaywrightを利用するので、これもインストールします。
zsh
1npm i -D @playwright/test
2npx playwright install
これで準備完了です。
1. ログイン画面のUI設計(Shadcn/ui使用)
本記事では、Shadcn/uiを使用して、法人向けアプリに適したログインフォームUIを構築します。必要な入力項目は以下の3つです:
- アカウントID
- メールアドレス
- パスワード
すべての項目に対して適切なバリデーションを実装し、視覚的にわかりやすいエラーメッセージ表示も行います。
下記のようなUIになります。

次のセクションでは、フォーム全体の構成と使用するShadcn/uiコンポーネント、そしてZodによるバリデーション定義について詳しく解説していきます。
2. Shadcn/ui コンポーネントの追加インストール
以下のコマンドで、今回使用するUIコンポーネントを追加インストールします:
zsh
1npx shadcn@latest add input card form
form
をインストールするとbutton
を同時にイントールされます。また、バリデーションスキーマのZod
も利用できるようになります。使用コンポーネントの構成
今回のログインフォームには以下のShadcn/uiコンポーネントを使用します:
<Form>
:React Hook Formのラッパー<FormField>
:各入力フィールドとバリデーションを繋ぐ<Input>
:テキスト入力<FormMessage>
:バリデーションエラー表示<Button>
:ログインボタン
これらを組み合わせることで、堅牢かつ見やすいUIを構築します。
3. ページ構成とuse clientの役割
Next.jsのApp Router構成では、
app/page.tsx
はデフォルトでサーバーコンポーネントです。そのため、useState
やuseEffect
などのクライアント側の状態管理を使用する場合は、別ファイルに分離して use client
を指定する必要があります。ディレクトリ構成
txt
1 login-form/
2├── app/
3│ ├── page.tsx ← サーバーコンポーネント(トップページ)
4│
5├── components/
6│ └── LoginForm.tsx ← クライアントコンポーネント(フォーム本体)
7│
8├── public/
9│ └── logo.svg ← ロゴ画像
10│
11├── lib/
12│ └── schema.ts ← Zodのバリデーションスキーマ
13│
14├── tests/
15│ └── login-form.spec.ts ← Playwright E2Eテスト設定
16│
17├── playwright.config.ts ← Playwright設定
Next.js の App Router 構成では、app/page.tsx は デフォルトでサーバーコンポーネント(use client が不要)です。これは以下のような処理に適しています:
- 初回のページ読み込み時にサーバーからHTMLを生成する(SEOや速度に有利)
- 状態管理やイベント処理を含まない表示専用のUIに向いている
当サイトでは可能な限りサーバーコンポーネントでサーバサイドレンダリング(SSR)できるようにしたいと思っています。
一方、フォームのように:
- useState や useForm など状態管理
- イベント処理(onSubmit など)
- DOMとのやり取り(フォーカス・バリデーション表示)
が必要な処理はクライアントコンポーネントとして分離する必要があります。そのため、components/LoginForm.tsx の冒頭に "use client"を記述し、page.tsx 側では表示として読み込むだけ、という設計にしています。
page.tsx
今回のログインフォームはユーザー入力を取り扱うため、
components/LoginForm.tsx
としてクライアントコンポーネント化し、page.tsx
から呼び出す構成にします。tsx
1// app/page.tsx(サーバーコンポーネント)
2import Image from "next/image";
3import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
4import LoginForm from "@/components/LoginForm"; // ログインフォームコンポーネント
5
6export default function Page() {
7 return (
8 <main className="flex min-h-svh w-full items-center justify-center bg-gray-800 p-6 md:p-10">
9 <Card className="w-full max-w-md">
10 <CardHeader>
11 <CardTitle className="flex justify-center">
12 <Image
13 src="/logo.svg"
14 alt="サイトロゴ"
15 width={160}
16 height={40}
17 priority={true}
18 className="h-[40px] w-[160px]"
19 />
20 </CardTitle>
21 </CardHeader>
22 <CardContent>
23 <LoginForm />
24 </CardContent>
25 </Card>
26 </main>
27 );
28}
この構成により、SSRのメリットを活かしつつ、フォームの動的な処理はクライアント側に切り分けることができます。
next/image
のポイント
- 自動で最適なサイズ・形式に変換してくれる(WebPなど)
- 遅延読み込み(Lazy Load)によりパフォーマンス向上
<img>
タグよりもSEOや速度面で有利
next/image
はせっかくNext.jsを使うのなら必須のものですが、最初、下記の2つのブラウザ警告に悩まされました
。Image with src "/logo.svg" was detected as the Largest Contentful Paint (LCP). Please add the "priority" property if this image is above the fold.
- 結論:
priority={true}
をつける - LCP (Largest Contentful Paint)とは?: 警告文にある LCP(Largest Contentful Paint)とは、Webページの表示速度を測る重要な指標の一つです。ユーザーがページを開いたときに、画面内で最も大きな画像またはテキストブロックが表示されるまでの時間を指します。LCPが速いほど、ユーザーは「このページは速い」と感じます。今回のケースでは、ログインフォームがページの主要コンテンツであるため、その中にあるロゴ画像がLCP要素だとNext.jsが自動で検知した、ということです。
- priorityプロパティの役割: priorityプロパティは、Next.jsに対して「この画像はLCPになる可能性が高い、非常に重要な画像です」と明示的に伝えるためのものです。このプロパティを指定すると、Next.jsは画像の読み込みを最適化するために、内部で以下の2つの処理を自動的に行います。
- 画像のプリロード(Preload): HTMLのタグ内にタグを挿入します。これにより、ブラウザは他のリソースの読み込みを待たずに、非常に早い段階で画像のダウンロードを開始します。
- 遅延読み込み(Lazy Loading)の無効化: 通常、画面外にある画像はスクロールされるまで読み込みが遅延されますが、priorityが指定された画像は遅延されず、即座に読み込まれます。
Image with src "http://localhost:3001/logo.svg" has either width or height modified, but not the other. If you use CSS to change the size of your image, also include the styles 'width: "auto"' or 'height: "auto"' to maintain the aspect ratio.
- 結論:
className="h-[40px] w-[160px]"
のようにwidth={160}
とheight={40}
と同じ値をCSSで指定 - 親要素がFlexbox(またはCSS Grid)であることが直接的な原因。簡単に言うと、レイアウトの主導権争いが起きている
- Next.js の主張:width={160} と height={40} のプロパティは、HTMLのタグに width="160" と height="40" という属性を設定します。これはブラウザに対して「私は本来このサイズです。レイアウトが崩れないように、このスペースを確保してください」と伝えています。
- 親要素(Flexbox)の主張:一方、className="flex" が指定された親要素は、「私の配下にあるアイテム(子要素)は、Flexboxのルールに従って配置・伸縮しなさい!」と、より強力な指示を出します。Flexboxのデフォルトの挙動では、アイテムはコンテナに合わせて引き伸ばされたり縮んだりします。
- ブラウザの判断:ブラウザは、HTML属性よりもCSSのレイアウト指示(Flexbox)を優先します。その結果、コンポーネントはFlexboxのルールに従ってリサイズされ、widthとheight属性が示す本来のサイズと食い違いが生じます。これが、Next.jsが警告を出す原因です。
- 解決策(className)の役割:そこで、className="w-[160px] h-[40px]" というCSSのクラスを指定します。これはブラウザに対して「Flexboxのルールの中にいるけれど、この要素だけは絶対に width: 160px, height: 40px にしてください!」という、より具体的で強い指示になります。これにより、主導権争いが解決し、Next.jsの要求(レイアウトシフト防止)とCSSの要求(表示サイズ)が一致するため、すべてが丸く収まります。
画像は
/public/logo.svg
に配置しておきましょう。4. Zodによるバリデーションスキーマ
Zodとは?─ 安全でわかりやすいスキーマバリデーションライブラリ
Zod(ゾッド)は、TypeScriptの型安全性とバリデーションを両立するための軽量なバリデーションライブラリです。
フォーム入力やAPIレスポンスのような「ユーザーが入力したデータ」や「外部から受け取ったデータ」は、正しい形式である保証がありません。
Zodを使えば、そのような「信用できないデータ」に対して、構造や型、制約を定義し、厳密に検証できるようになります。
Zodを使えば、そのような「信用できないデータ」に対して、構造や型、制約を定義し、厳密に検証できるようになります。
✅ Zodの特徴
特徴 | 説明 |
---|---|
型推論に強い | スキーマ定義からTypeScript型をそのまま生成できる(z.infer<> ) |
エラーメッセージのカスタマイズ | 各フィールドごとに個別のエラーメッセージを設定可能 |
シンプルな構文 | JavaScript初心者でも読みやすく、直感的に書ける |
バリデーションと型を1つに | TypeScript 型とバリデーションルール を同時に記述できる |
🔰 なぜZodを使うのか?
Next.js や React Hook Form と組み合わせることで、ユーザー入力のチェック → エラー表示 → データ送信までを一貫して安全に処理できます。
たとえば、以下のように:
ts
1const schema = z.object({
2 email: z.email({ message: "有効なメールアドレスを入力してください。" }),
3 password: z.string().min(15).regex(...),
4});
と書くだけで:
- メール形式の検証
- パスワードの文字数制限・形式制限
- エラーメッセージの指定
などを直感的に定義できます。
また、次のように
TypeScriptの型
も自動で得られるため、フロントエンド開発の型整合性にも役立ちます:ts
1type FormData = z.infer<typeof schema>;
バリデーション定義(lib/schema.ts)
Zodライブラリを用いて、次のようなバリデーションルールを定義します:
ts
1import { z } from "zod";
2
3export const loginSchema = z.object({
4 accountId: z
5 .string()
6 .min(15, { message: "アカウントIDは15文字以上で入力してください。" })
7 .regex(/[A-Z]/, "大文字を1文字以上含めてください。")
8 .regex(/[a-z]/, "小文字を1文字以上含めてください。")
9 .regex(/[0-9]/, "数字を1文字以上含めてください。"),
10 email: z.email({ message: "有効なメールアドレスを入力してください。" }),
11 password: z
12 .string()
13 .min(15, { message: "パスワードは15文字以上で入力してください。" })
14 .regex(/[A-Z]/, "大文字を1文字以上含めてください。")
15 .regex(/[a-z]/, "小文字を1文字以上含めてください。")
16 .regex(/[0-9]/, "数字を1文字以上含めてください。")
17});
.email()
は、Zodが内部的に「@記号を含み、適切なドメイン構造を持つメールアドレス形式か?」を検証する関数です。これは RFC 5322 の簡易準拠として設計されており、実用上のメールバリデーションとして広く使われている正規表現を基にしています。Gmailやyahooなどの一般的な形式は通過しますが、「@が無い」「ドメインが無い」といった典型的な誤りは弾かれます。このスキーマを使って、
react-hook-form
と連携させていきます。5. フォームの実装コード例(LoginForm.tsx)
以下は
src/components/LoginForm.tsx
に配置する仮実装のログインフォーム例です:tsx
1"use client";
2
3import { useState } from "react";
4import { useForm } from "react-hook-form";
5import { zodResolver } from "@hookform/resolvers/zod";
6import { loginSchema } from "@/lib/schema"; // ログインスキーマ
7import { z } from "zod";
8import {
9 Form,
10 FormField,
11 FormItem,
12 FormLabel,
13 FormControl,
14 FormMessage,
15} from "@/components/ui/form";
16import { Input } from "@/components/ui/input";
17import { Button } from "@/components/ui/button";
18import { Eye, EyeOff } from "lucide-react";
19
20export default function LoginForm() {
21 const form = useForm<z.infer<typeof loginSchema>>({
22 resolver: zodResolver(loginSchema),
23 defaultValues: {
24 accountId: "",
25 email: "",
26 password: "",
27 },
28 });
29
30 const [showPassword, setShowPassword] = useState(false);
31
32 function onSubmit(values: z.infer<typeof loginSchema>) {
33 console.log("送信データ:", values);
34 // ここにAPI連携を実装予定
35 }
36
37 return (
38 <Form {...form}>
39 <form
40 onSubmit={form.handleSubmit(onSubmit)}
41 className="mx-auto max-w-md space-y-4 pt-4 pb-10"
42 >
43 <FormField
44 control={form.control}
45 name="accountId"
46 render={({ field }) => (
47 <FormItem>
48 <FormLabel>アカウントID</FormLabel>
49 <FormControl>
50 <Input
51 data-testid="accountId"
52 placeholder="CORP000123456"
53 autoFocus
54 {...field}
55 />
56 </FormControl>
57 <FormMessage data-testid="accountId-error" />
58 </FormItem>
59 )}
60 />
61 <FormField
62 control={form.control}
63 name="email"
64 render={({ field }) => (
65 <FormItem>
66 <FormLabel>メールアドレス</FormLabel>
67 <FormControl>
68 <Input
69 data-testid="email"
70 type="email"
71 autoComplete="email"
72 placeholder="your@email.com"
73 {...field}
74 />
75 </FormControl>
76 <FormMessage data-testid="email-error" />
77 </FormItem>
78 )}
79 />
80 <FormField
81 control={form.control}
82 name="password"
83 render={({ field }) => (
84 <FormItem>
85 <FormLabel>パスワード</FormLabel>
86 <div className="flex items-start gap-2">
87 <FormControl>
88 <Input
89 {...field}
90 data-testid="password"
91 type={showPassword ? "text" : "password"}
92 autoComplete="current-password"
93 placeholder="半角英数字15文字以上"
94 />
95 </FormControl>
96 <Button
97 data-testid="password-toggle"
98 type="button"
99 size="icon"
100 variant="outline"
101 onClick={() => setShowPassword((prev) => !prev)}
102 aria-label={
103 showPassword
104 ? "パスワードを非表示にする"
105 : "パスワードを表示する"
106 }
107 className="shrink-0 cursor-pointer"
108 >
109 {showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
110 </Button>
111 </div>
112 <FormMessage data-testid="password-error" />
113 </FormItem>
114 )}
115 />
116 <Button
117 data-testid="submit"
118 type="submit"
119 className="mt-4 w-full cursor-pointer"
120 >
121 ログイン
122 </Button>
123 </form>
124 </Form>
125 );
126}
LoginForm.tsx の初心者向け構造解説
このセクションでは、
src/components/LoginForm.tsx
の構成を 初心者の方でも理解しやすいように丁寧に分解して解説します。🔰 ファイル全体の役割は?
このファイルは、**ログインフォームのUIとロジックを実装する「クライアントコンポーネント」**です。ユーザーが入力する情報(アカウントID・メールアドレス・パスワード)を受け取り、バリデーションして、ログイン処理(まだAPI未接続)につなげる準備をしています。
🔁 useForm とは?
ts
1const form = useForm<z.infer<typeof loginSchema>>({
2 resolver: zodResolver(loginSchema),
3 defaultValues: {
4 accountId: "",
5 email: "",
6 password: "",
7 },
8});
useForm
は React Hook Form のメイン関数で、フォームの状態やバリデーションの制御をまとめるものです。zodResolver(loginSchema)
によって、Zodで定義したバリデーションルールを適用しています。defaultValues
でフォームの初期値を定義します。
z.infer<typeof loginSchema>
は TypeScript の型推論機能で、バリデーション定義から自動で型を導き出します。🔐 パスワード表示切替(useState)
ts
1const [showPassword, setShowPassword] = useState(false);
showPassword
がtrue
ならパスワードを「表示」、false
なら「●●●」のマスク表示にします。- これをトグルで切り替えることで、パスワード表示/非表示切替ボタンを実装しています。
🧩 Form の構造全体
tsx
1<Form {...form}>
2 <form onSubmit={form.handleSubmit(onSubmit)}>
3 {/* 各FormFieldがここに入る */}
4 </form>
5</Form>
Form
は Shadcn/ui のラッパーで、React Hook Form を内包して動作します。onSubmit
でフォーム送信時の処理を受け取ります。- 中身には
<FormField>
を使ってそれぞれの入力欄を定義していきます。
🧱 FormField の使い方
tsx
1<FormField
2 control={form.control}
3 name="accountId"
4 render={({ field }) => (
5 <FormItem>
6 <FormLabel>アカウントID</FormLabel>
7 <FormControl>
8 <Input placeholder="CORP000123456" {...field} />
9 </FormControl>
10 <FormMessage />
11 </FormItem>
12 )}
13/>
それぞれの要素の役割は以下の通り:
コンポーネント | 役割説明 |
---|---|
FormField | 1フィールドのバリデーションや状態をまとめる枠組み |
FormItem | 全体を包むラッパーで、ラベル・入力欄・エラーメッセージを内包 |
FormLabel | 入力欄のラベル表示(<label> ) |
FormControl | 入力欄のラッパー。ここに Input や Textarea を入れる |
FormMessage | バリデーションエラー時のメッセージを自動表示 |
🧠 Password欄 + 表示切り替えボタンの構成
tsx
1<FormLabel>パスワード</FormLabel>
2<div className="flex items-start gap-2">
3 <FormControl>
4 <Input type={showPassword ? "text" : "password"} ... />
5 </FormControl>
6 <Button type="button"> {showPassword ? <EyeOff /> : <Eye />} </Button>
7</div>
- パスワード欄は
Input
の横に<Button>
を配置し、クリックで表示状態を切り替えます。 lucide-react
のEye
/EyeOff
アイコンを使って視覚的に明示しています。- この構成は、今後も DELOGsのログインフォーム標準仕様として扱います。
📤 onSubmit処理
tsx
1function onSubmit(values) {
2 console.log("送信データ:", values);
3 // TODO: API連携を実装予定
4}
values
には、Zodバリデーション済みの 安全な入力データ が入ります。- 今は
console.log
のみですが、今後ここにログインAPIとの通信処理を追加予定です。
🧪 テスト用のIDを付与
これはE2Eテスト用なので、そこはやらないという方には関係ないのですが、各パーツにテスト用のID:
data-testid
を付与しています。tsx
1<Input data-testid="accountId" placeholder="CORP000123456" autoFocus {...field} />
これがあるとE2Eのテストファイルを記述するときに
input
要素の日本語名称やエラーメッセージなどをターゲットにせずに、data-testid
をターゲットにできます。運用面を考慮すると、私としてはマストの対応となっています。これで、
LoginForm.tsx
の構成とその理由がしっかり理解できるようになったかと思います。 この知識は次の「API連携」や「テスト設計」のステップでも役立ちます!6. (任意)E2Eのテストファイルを作ろう
この章以降は、任意の作業です。毎度テストするのが面倒という方向けです。それ以外の方は読み飛ばしていただいても問題ないです。
テストファイルについては下記でも触れています。ただ今回は、より実践的にパワーアップした内容になっています。
Playwright の設定ファイルを作成:playwright.config.ts
Playwright の設定ファイルについては下記でも少し触れています。ただ今回は、より実践的にパワーアップした内容になっています。
Playwright を用いた E2E テストを行う際、
playwright.config.ts
はその「設計図」となる重要ファイルです。この記事では、DELOGsログインフォームに合わせて最適化された設定ファイルを作成します。🎯 playwright.config.ts
とは?
Playwright の動作全体を定義する設定ファイルで、次のような内容を記述できます:
- テストファイルの場所(どのフォルダを見るか)
- デフォルトのブラウザや画面サイズ
- 各種オプション(動画撮影、スクリーンショット、HTMLレポートなど)
- 実行時間やタイムアウトなどの制御
ファイル名は
playwright.config.ts
(または .js
)で、プロジェクトのルートに配置します。🏗 サンプル構成(DELOGs用)
ts : title
1import { defineConfig, devices } from '@playwright/test';
2
3const PORT = 3010;
4
5export default defineConfig({
6 testDir: './tests',
7 timeout: 30 * 1000,
8 expect: {
9 timeout: 5000,
10 },
11 fullyParallel: true,
12 reporter: [['html', { open: 'never' }]],
13 use: {
14 baseURL: `http://localhost:${PORT}`,
15 headless: true,
16 viewport: { width: 1280, height: 720 },
17 ignoreHTTPSErrors: true,
18 screenshot: 'only-on-failure',
19 video: 'retain-on-failure',
20 },
21 webServer: {
22 command: `npm run dev -- -p ${PORT}`,
23 port: PORT,
24 reuseExistingServer: !process.env.CI,
25 },
26 projects: [
27 {
28 name: 'Desktop Chrome',
29 use: { ...devices['Desktop Chrome'] },
30 },
31 {
32 name: 'Desktop Firefox',
33 use: { ...devices['Desktop Firefox'] },
34 },
35 {
36 name: 'Desktop Safari',
37 use: { ...devices['Desktop Safari'] },
38 },
39 {
40 name: 'Mobile Chrome',
41 use: { ...devices['Pixel 5'] },
42 },
43 {
44 name: 'Mobile Safari',
45 use: { ...devices['iPhone 13'] },
46 },
47 ],
48});
各項目の意味とポイント
🔍
testDir
: テストファイルがあるディレクトリの指定。今回は ./tests
にすべての .spec.ts
を置く想定です。⏱
timeout
: テスト1件あたりの実行タイムアウト時間(ミリ秒)。30 * 1000
は 30秒です。🧪
expect.timeout
:expect(...)
が toBeVisible()
などの判定を待つ時間。🚀
fullyParallel
:true
にすると、複数のテストファイルを並列に実行できます。CI高速化に有効。📊
reporter
: テストレポートの出力形式。ここでは HTML を指定しつつ open: 'never'
にして、勝手にブラウザ表示しないようにしています。◯
use
ブロックの詳細🌐
baseURL
: テストのアクセス先(page.goto('/')
が http://localhost:3000/
を意味するように)👻
headless
: GUIなしでブラウザを開くか。true
にすると非表示モード(CIで使う)。📐
viewport
: ブラウザの画面サイズ。🔒
ignoreHTTPSErrors
: 自己署名SSL証明書などでもエラーにしない。📸
screenshot
: スクリーンショットの自動撮影タイミング。only-on-failure
:テスト失敗時だけ保存(デフォルトにおすすめ)
🎥
video
: テスト実行中の「画面操作を録画」するオプション。retain-on-failure
:失敗したときだけ動画を保存(バグ再現の検証に便利)- ファイルは
test-results
フォルダに保存される on
にすると常時記録されますが容量増大に注意
✅ Playwright の録画機能は、UIテストにおいて非常に強力なデバッグ手段になります。特にCIでの不安定な動作を「見える化」できる点が優秀です。
◯
webServer
オプション(Next.js 開発サーバを自動起動)CI環境などで
npm run dev
を自動実行し、Next.jsアプリケーションを起動する設定です。ts
1webServer: {
2 command: `npm run dev -- -p ${PORT}`,
3 port: PORT,
4 reuseExistingServer: !process.env.CI,
5},
command
:テスト前に実行するコマンドport
:Playwrightがアクセスを試みるポートreuseExistingServer
:既にサーバーが起動していれば再利用(ローカルで便利)
◯
projects
ブロックPlaywright はマルチブラウザ対応なので、ここで
Chrome
や Firefox
などを定義できます。ts
1projects: [
2 {
3 name: 'Desktop Chrome',
4 use: { ...devices['Desktop Chrome'] },
5 },
6 {
7 name: 'Desktop Firefox',
8 use: { ...devices['Desktop Firefox'] },
9 },
10 {
11 name: 'Desktop Safari',
12 use: { ...devices['Desktop Safari'] },
13 },
14 {
15 name: 'Mobile Chrome',
16 use: { ...devices['Pixel 5'] },
17 },
18 {
19 name: 'Mobile Safari',
20 use: { ...devices['iPhone 13'] },
21 },
22]
これにより、あらゆる主要ブラウザ・主要端末でのUI検証が自動化可能になります。
Playwright設定のまとめ
Playwrightの設定ファイルは少し複雑に見えますが、1つ1つの役割を知ればとてもパワフルな仕組みです。特に動画やスクリーンショット、レポートなどの自動生成は、開発者だけでなく非エンジニアにとっても可視性の高い検証手段になります。
今回のようなログイン画面のUI検証や、今後のフォーム・ダッシュボード画面のE2Eにも活用していきましょう。
テストファイルの作成 :tests/login-form.spec.ts
Playwright設定が終わったら、次にテストファイルを作成します。
テストファイルについては下記でも触れています。ただ今回は、より実践的にパワーアップした内容になっています。
プロジェクトディレクトリ直下に
tests
ディレクトリを作成してtsファイルを作成します。今回はtests/login-form.spec.ts
を作成していきます。テストパターンの構成は以下の4パートにしています:
- 初期状態の確認:すべて空欄でボタンが有効であること
- 未入力時のバリデーション表示
- 形式エラー入力時のバリデーション表示
- 正しい入力での送信処理(仮想送信)
🏗 テストファイルのサンプル(DELOGs用)
ts
1// tests/login-form.spec.ts
2import { test, expect } from "@playwright/test";
3
4// ログインフォームの動作テスト
5test.describe("ログインフォーム", () => {
6 test.beforeEach(async ({ page }) => {
7 await page.goto("/");
8 });
9
10 test("フォームの初期状態", async ({ page }) => {
11 await expect(page.getByTestId("accountId")).toBeEmpty();
12 await expect(page.getByTestId("email")).toBeEmpty();
13 await expect(page.getByTestId("password")).toBeEmpty();
14 await expect(page.getByTestId("submit")).toBeEnabled();
15 });
16
17 test("バリデーションメッセージの表示(未入力)", async ({ page }) => {
18 await page.getByTestId("submit").click();
19
20 await Promise.all([
21 expect(page.getByTestId("accountId-error")).toBeVisible({
22 timeout: 7000,
23 }),
24 expect(page.getByTestId("email-error")).toBeVisible({ timeout: 7000 }),
25 expect(page.getByTestId("password-error")).toBeVisible({ timeout: 7000 }),
26 ]);
27 });
28
29 test("バリデーションメッセージの表示(形式エラー)", async ({ page }) => {
30 await page.getByTestId("accountId").fill("ABC123ABC456ABC"); // 小文字なし
31 await page.getByTestId("email").fill("test@example.com"); // OK
32 await page.getByTestId("password").fill("securepassword123"); // 大文字なし
33
34 await page.getByTestId("submit").click();
35
36 await Promise.all([
37 expect(page.getByTestId("accountId-error")).toBeVisible({
38 timeout: 7000,
39 }),
40 expect(page.getByTestId("password-error")).toBeVisible({ timeout: 7000 }),
41 ]);
42 });
43
44 test("有効なデータでフォーム送信", async ({ page }) => {
45 await page.getByTestId("accountId").fill("CorpUserAccount123");
46 await page.getByTestId("email").fill("test@example.com");
47 await page.getByTestId("password").fill("SecurePassword123");
48
49 await page.getByTestId("submit").click();
50 // Safari/WebKitでは、submit直後にDOM反映が遅れるケースがあるため200msの猶予を与える
51 await page.waitForTimeout(200); // Safariでバリデーションが遅延する場合の対策
52
53 await Promise.all([
54 expect(page.getByTestId("accountId-error")).not.toBeVisible({
55 timeout: 10000,
56 }),
57 expect(page.getByTestId("email-error")).not.toBeVisible({
58 timeout: 10000,
59 }),
60 expect(page.getByTestId("password-error")).not.toBeVisible({
61 timeout: 10000,
62 }),
63 ]);
64
65 // 実際のAPI連携が未実装なので、ここではconsoleの確認やモック処理を想定
66 await expect(page).not.toHaveURL(/error/);
67 });
68});
フォームで
data-testid
を付与しているので、getByTestId
で値を取得してチェックするように書けます。運用面を考えると今のところ、これがベストかなと思っています。テスト実施
準備ができたら、下記のコマンドでテストを実施してみます。
zsh
1npx playwright test --reporter=html
zsh
1Running 20 tests using 7 workers
2 20 passed (7.5s)
3
4To open last HTML report run:
5
6 npx playwright show-report
上記のようにテストを通過すればOKです。環境によってはDOM描画が遅くてエラーになることもあるかもしれません。その際はテストファイルの
await page.waitForTimeout(200); // Safariでバリデーションが遅延する場合の対策
やtimeout
の値を調整してみてください。7. (任意)GitHub ActionsによるE2E自動テスト設定
Playwrightを使ったE2Eテストを**CI(継続的インテグレーション)**環境で自動実行するには、GitHub Actionsの設定が必要です。ここでは、Next.jsアプリの開発サーバを起動し、E2Eテストを自動で走らせる最小構成の
.github/workflows/e2e.yml
を作成していきます。GitHub ActionsでPlaywrightを動かす目的
目的 | 説明 |
---|---|
自動テストの仕組み化 | プルリクエスト作成やpush時にテストを自動で実行 |
テスト失敗の早期検知 | UIの崩れやバリデーションの不備をCIで検出 |
チーム開発での安心材料 | 誰かがうっかり壊しても、CIが止めてくれる |
e2e.ymlファイルの内容
ワークフローファイルの作成については、下記でも実施しています。今回は微調整を加えています。
.github/workflows/e2e.yml
を下記の内容で作成します。yml
1name: Run Playwright E2E Tests
2
3on:
4 push:
5 branches: [main]
6 pull_request:
7 branches: [main]
8
9jobs:
10 test:
11 timeout-minutes: 10
12 runs-on: ubuntu-latest
13
14 steps:
15 # 1) ソース取得
16 - name: Checkout Repository
17 uses: actions/checkout@v4
18
19 # 2) Node 環境(バージョン固定)
20 - name: Setup Node.js
21 uses: actions/setup-node@v4
22 with:
23 node-version: 22.17.0
24
25 # 3) 依存インストール
26 - name: Install Dependencies
27 run: npm ci
28
29 # 4) Playwright のブラウザ環境をセットアップ
30 - name: Install Playwright Browsers
31 run: npx playwright install --with-deps
32
33 # 5) TypeScript型チェック
34 - name: Type Check
35 run: npx tsc --noEmit
36
37 # 6) Playwright E2Eテスト実行(HTMLレポート)
38 - name: Run Playwright Tests
39 run: npx playwright test --reporter=html
40
41 # 7) レポートのアーティファクト保存(常に実行)
42 - name: Upload Playwright Report
43 uses: actions/upload-artifact@v4
44 if: always()
45 with:
46 name: playwright-report
47 path: playwright-report
48 retention-days: 7
49
50 # 8) Next.jsの本番ビルド確認(任意・品質ゲート)
51 - name: Next.js Build
52 run: npm run build
設定ポイント(GitHub ActionsでPlaywrightをCI化)
このワークフローは、pushやpull_requestのたびにPlaywrightのE2Eテストを実行し、結果をHTMLレポートとして保存します。
以下は主な設定ポイントです:
ステップ | 内容 | 備考 |
---|---|---|
on.push / pull_request | mainブランチへのpushまたはPR作成時に動作 | |
uses: actions/checkout@v4 | リポジトリのソースコードをチェックアウト | GitHub Actionsの定番。必須 |
setup-node@v4 | Node.js 22.17.0 をインストールして固定化 | 再現性を確保。毎回必ず同じ環境でテスト |
npm ci | lockファイルをもとに依存を完全再構築 | npm install より厳密な依存管理 |
npx playwright install --with-deps | Playwright本体とブラウザのドライバをインストール | GUIテストに必要なChrome/Firefox/Safariなど |
npx tsc --noEmit | TypeScriptの型エラーがないかチェック | テストを始める前に型の整合性を担保 |
npx playwright test --reporter=html | E2Eテストを実行してHTMLレポートを生成 | GUIテストの結果をHTMLで残せる |
upload-artifact@v4 | PlaywrightのHTMLレポートを保存 | if: always() で失敗時もレポート取得可 |
retention-days: 7 | レポート保存期間を7日間に制限 | ディスク節約のための設定(CIコスト最適化) |
npm run build | Next.jsの本番ビルドが通るか確認 | テストとビルド両方が成功すれば安全にマージ可 |
これを設定することで、GitHubにpushした瞬間にテストが自動で走り、結果がPRやコミットに表示されるようになります。
参考文献・リンク集
本記事で紹介した内容に関連する、公式ドキュメントや参考になる記事を以下にまとめます。
項目 | リンク |
---|---|
Next.js App Router 公式ドキュメント | https://nextjs.org/docs/app |
shadcn/ui コンポーネント集 | https://ui.shadcn.dev |
React Hook Form | https://react-hook-form.com |
Zod(バリデーションライブラリ) | https://zod.dev |
Playwright 公式 | https://playwright.dev |
Next.jsの画像最適化(next/image ) | https://nextjs.org/docs/app/api-reference/components/image |
GitHub Actions ドキュメント | https://docs.github.com/actions |
以上で、Shadcn/uiとZodを用いたログイン画面のUI構築とE2Eテストの完全ガイドは完了です。
徐々に過去記事とも連動する内容になってきて、少しだけ成長してきた気がしています。
次のステップでは「認証APIの実装」や「保護ルートの設定」など、実運用を見据えた処理に進んでいきます!
この記事の執筆・編集担当
DE
松本 孝太郎
DELOGs編集部/中年新米プログラマー
ここ数年はReact&MUIのフロントエンドエンジニアって感じでしたが、Next.jsを学んで少しずつできることが広がりつつあります。その実践記録をできるだけ共有していければと思っています。