実務1年目駆け出しエンジニアがLaravel&ReactでWebアプリケーション開発に挑戦してみた!(その52)
0. 初めに
Webアプリケーション開発シリーズ、E2Eテスト編です。
前回は、ゲストのテストシナリオを作っていました。
今日は、一般ユーザーのテストシナリオを作っていきましょう!
1. ブランチ運用
例によって、developブランチを最新化させて、新規にtest/e2e/general-userというブランチを切って作業をします。
前回使用した、test/e2e/guestブランチはもう必要ないので削除しましょう。
2. レビュー CRUD
e2e/helpers/fill-stars.js
\project-root\src\e2e\helpers\fill-stars.js
/**
* 研究室詳細ページで7項目すべてに星評価を入力するヘルパー
* @param {import('@playwright/test').Page} page
* @param {number} stars - 1〜5
*/
export async function fillAllStars(page, stars = 3) {
const labels = [
'指導スタイル',
'雰囲気・文化',
'成果・活動',
'拘束度',
'設備',
'働き方',
'人数バランス',
];
for (const label of labels) {
const radios = await page
.getByRole('radiogroup', { name: label })
.getByRole('radio')
.all();
await radios[stars - 1].click();
}
}
e2e/review.spec.js
\project-root\src\e2e\review.spec.js
import { test, expect } from '@playwright/test';
import { AUTH } from './helpers/auth-paths.js';
import { fillAllStars } from './helpers/fill-stars.js';
// このファイルは一般ユーザー(user@example.com)として実行されるレビュー CRUD テスト
// シードデータ前提:
// 研究室 id=1〜3: 機械工学科 テストA〜C研究室 (faculty_id=5)
// user@example.com はシード時に存在しないためレビューを持っていない
test.use({ storageState: AUTH.user });
test.describe('レビュー: 投稿・閲覧', () => {
test('未投稿の研究室では「まだ、レビューを投稿していません。」が表示される', async ({ page }) => {
await page.goto('/labs/1');
await expect(page.getByText('まだ、レビューを投稿していません。')).toBeVisible({
timeout: 10000,
});
});
test('7項目を入力してレビューを投稿すると「レビューを投稿済みです。」に変わる', async ({
page,
}) => {
await page.goto('/labs/1');
// 未投稿ボタンをクリックしてモーダルを開く
await page.getByText('まだ、レビューを投稿していません。').click();
await expect(page.getByText('レビューを作成する')).toBeVisible({ timeout: 10000 });
// 7項目すべてに3つ星を入力
await fillAllStars(page, 3);
// 投稿ボタンを押す
await page.getByRole('button', { name: 'レビューする' }).click();
// 投稿済み状態に変わること
await expect(page.getByText('レビューを投稿済みです。')).toBeVisible({ timeout: 10000 });
});
});
test.describe('レビュー: 編集', () => {
test('投稿済みレビューを編集して保存できる', async ({ page }) => {
// lab 2 で新規投稿してから編集する(lab 1 の状態に依存しない独立したテスト)
await page.goto('/labs/2');
// レビューを投稿
await page.getByText('まだ、レビューを投稿していません。').click();
await expect(page.getByText('レビューを作成する')).toBeVisible({ timeout: 10000 });
await fillAllStars(page, 3);
await page.getByRole('button', { name: 'レビューする' }).click();
await expect(page.getByText('レビューを投稿済みです。')).toBeVisible({ timeout: 10000 });
// 投稿済みボタンをクリックして編集モーダルを開く
await page.getByText('レビューを投稿済みです。').click();
await expect(page.getByText('あなたが投稿済したレビュー')).toBeVisible({ timeout: 10000 });
// 編集アイコンをクリック
await page.getByRole('button', { name: '編集', exact: true }).click();
await expect(page.getByText('レビューを編集する')).toBeVisible({ timeout: 10000 });
// 「指導スタイル」を5星に変更
const radios = await page
.getByRole('radiogroup', { name: '指導スタイル' })
.getByRole('radio')
.all();
await radios[4].click({ force: true });
// 保存
await page.getByRole('dialog').filter({ hasText: 'レビューを編集する' }).getByRole('button', { name: '編集する' }).click();
// モーダルが閉じて「投稿済みです」状態が維持されること
await expect(page.getByText('レビューを投稿済みです。')).toBeVisible({ timeout: 10000 });
});
});
test.describe('レビュー: 削除', () => {
test('投稿済みレビューを削除すると未投稿状態に戻る', async ({ page }) => {
// lab 3 で新規投稿してから削除する
await page.goto('/labs/3');
// レビューを投稿
await page.getByText('まだ、レビューを投稿していません。').click();
await expect(page.getByText('レビューを作成する')).toBeVisible({ timeout: 10000 });
await fillAllStars(page, 2);
await page.getByRole('button', { name: 'レビューする' }).click();
await expect(page.getByText('レビューを投稿済みです。')).toBeVisible({ timeout: 10000 });
// 投稿済みボタンをクリックして編集モーダルを開く
await page.getByText('レビューを投稿済みです。').click();
await expect(page.getByText('あなたが投稿済したレビュー')).toBeVisible({ timeout: 10000 });
// 削除アイコンをクリック
await page.getByRole('button', { name: '削除', exact: true }).click();
// 削除確認モーダルが表示されること
await expect(page.getByText('レビューの削除')).toBeVisible({ timeout: 10000 });
// 「削除する」ボタンを押す
await page.getByRole('dialog').filter({ hasText: 'レビューの削除' }).getByRole('button', { name: '削除する' }).click();
// 未投稿状態に戻ること
await expect(page.getByText('まだ、レビューを投稿していません。')).toBeVisible({
timeout: 10000,
});
});
});
- 実行コマンド
/project-root/src
$ npx playwright test e2e/auth.spec.js
できたら、コミット!
3. コメント CRUD
/e2e/helpers/comment-modal.js
\project-root\src\e2e\helpers\comment-modal.js
import { expect } from '@playwright/test';
/**
* コメント一覧モーダルを開くヘルパー
* コメント件数によってトリガーボタンのラベルが変わるため正規表現で対応
*/
export async function openCommentModal(page) {
const trigger = page.getByRole('button', {
name: 'もっと見る',
});
await expect(trigger).toBeVisible({ timeout: 10000 });
await trigger.click();
await expect(page.getByText(/コメント一覧/)).toBeVisible({ timeout: 10000 });
}
/**
* コメントを投稿するヘルパー(モーダルが開いている状態で呼ぶ)
*/
export async function postComment(page, text) {
const textarea = page.getByPlaceholder('コメントを入力...');
await textarea.click();
await textarea.fill(text);
await page.getByRole('button', { name: 'コメントする' }).click();
await expect(page.locator('p').filter({ hasText: text }).first()).toBeVisible({ timeout: 10000 });
}
/e2e/comment.spec.js
\project-root\src\e2e\comment.spec.js
import { test, expect } from '@playwright/test';
import { AUTH } from './helpers/auth-paths.js';
import { openCommentModal, postComment } from './helpers/comment-modal.js';
// このファイルはコメント CRUD をテストする
// シードデータ: CommentSeeder は DatabaseSeeder から呼ばれないため、
// コメント初期件数は 0 件
// user@example.com はシード後に tinker で作成されるため、他ユーザーのコメントは存在しない
test.describe('コメント: 投稿・編集・削除(一般ユーザー)', () => {
test.use({ storageState: AUTH.user });
test('コメントを投稿するとコメント一覧に表示される', async ({ page }) => {
await page.goto('/labs/1');
await openCommentModal(page);
await postComment(page, 'E2Eテスト: コメント投稿テスト');
});
test('自分のコメントを編集できる', async ({ page }) => {
await page.goto('/labs/2');
await openCommentModal(page);
// コメントを投稿
const original = 'E2Eテスト: 編集前のコメント';
await postComment(page, original);
// 編集アイコンをクリック(投稿したコメントの行に絞り込んでクリック)
const commentRow = page.locator('.border-b').filter({ hasText: original });
await commentRow.getByRole('button').filter({ has: page.getByAltText('編集') }).click();
// 編集モードに切り替わるとテキストは textarea の value になるため
// placeholder なしの textarea(編集用)で絞り込む
const editTextarea = page.locator('textarea:not([placeholder])');
await editTextarea.clear();
await editTextarea.fill('E2Eテスト: 編集後のコメント');
// 「編集する」ボタンは .border-b 内にのみ存在するため絞り込む
await page.locator('.border-b').getByRole('button', { name: '編集する' }).click();
// 編集後の内容が表示されること
await expect(page.getByText('E2Eテスト: 編集後のコメント')).toBeVisible({ timeout: 10000 });
// 編集前の内容が消えていること
await expect(page.getByText(original)).not.toBeVisible();
});
test('自分のコメントを削除できる', async ({ page }) => {
await page.goto('/labs/3');
await openCommentModal(page);
// コメントを投稿
const commentText = 'E2Eテスト: 削除するコメント';
await postComment(page, commentText);
// 削除アイコンをクリック(最後に投稿したコメントの行に絞り込む)
const commentRow = page.locator('.border-b').filter({ has: page.locator('p').filter({ hasText: commentText }) }).last();
await commentRow.getByRole('button').filter({ has: page.getByAltText('削除') }).click();
// 削除確認モーダルが表示されること
await expect(page.getByText('コメントの削除')).toBeVisible({ timeout: 10000 });
// 「削除する」ボタンを押す(AlertModal 内のボタンを強制クリック)
await page.getByRole('button', { name: '削除する' }).first().click({ force: true });
// コメント一覧モーダルが再度開き、削除したコメントが消えていること
await expect(page.getByText(/コメント一覧/)).toBeVisible({ timeout: 10000 });
await expect(page.getByText(commentText)).not.toBeVisible();
});
test('他ユーザーのコメントには編集・削除ボタンが表示されない', async ({ page }) => {
// lab 4 にコメントを投稿していない状態で開く
// → 表示されているコメントは他ユーザーのものがないか確認
// ただし初期状態でコメントが 0 件の場合はスキップ相当の検証
await page.goto('/labs/4');
await openCommentModal(page);
// 自分のコメントが一件もない状態で「編集」「削除」ボタンが存在しないこと
// モーダル内(role="dialog")に絞り込んで背景の編集ボタンを除外する
const modal = page.getByRole('dialog');
await expect(
modal.getByRole('button').filter({ has: modal.getByAltText('編集') })
).toHaveCount(0);
await expect(
modal.getByRole('button').filter({ has: modal.getByAltText('削除') })
).toHaveCount(0);
});
});
- 実行コマンド
/project-root/src
$ npx playwright test e2e/comment.spec.js
コミットしましょう!
4. ブックマーク
\project-root\src\e2e\bookmark.spec.js
import { test, expect } from '@playwright/test';
import { AUTH } from './helpers/auth-paths.js';
import { execSync } from 'child_process';
// このファイルはブックマーク機能をテストする
// シードデータ前提:
// 研究室 id=1: 機械工学科 テストA研究室
// user@example.com はブックマークを持っていない状態から開始
test.use({ storageState: AUTH.user });
/**
* ブックマークアイコン(SVG path)を含む親要素を返す
* aria-label がないため SVG の fill 属性で状態を判別する
*/
const bookmarkSvg = page => page.locator('svg').filter({ has: page.locator('path[stroke="#747D8C"]') });
test.describe('ブックマーク: 追加・解除', () => {
// 各テスト前に user@example.com のブックマークを削除して初期状態に戻す
test.beforeEach(() => {
execSync(
`docker exec php-lab php artisan tinker --execute="\\App\\Models\\User::where('email', 'user@example.com')->first()->bookmarks()->delete();"`,
{ stdio: 'inherit' }
);
});
test('研究室詳細のブックマークボタンを押すとブックマーク数が増える', async ({ page }) => {
await page.goto('/labs/1');
// 現在のブックマーク数を取得
const countLocator = page.locator('svg').filter({ has: page.locator('path[stroke="#747D8C"]') }).locator('~ span');
await expect(countLocator).toBeVisible({ timeout: 10000 });
const before = parseInt(await countLocator.textContent());
// ブックマークアイコンをクリック
await bookmarkSvg(page).click();
// カウントが 1 増えること
await expect(countLocator).toHaveText(String(before + 1), { timeout: 10000 });
});
test('ブックマーク済みの研究室でアイコンを再度押すと解除されてカウントが戻る', async ({ page }) => {
await page.goto('/labs/1');
const countLocator = page.locator('svg').filter({ has: page.locator('path[stroke="#747D8C"]') }).locator('~ span');
await expect(countLocator).toBeVisible({ timeout: 10000 });
const before = parseInt(await countLocator.textContent());
// 1回目: ブックマーク追加(カウントが増えるまで待ってから値を確定する)
await bookmarkSvg(page).click();
await expect(countLocator).toHaveText(String(before + 1), { timeout: 10000 });
const afterAdd = parseInt(await countLocator.textContent());
// 2回目: ブックマーク解除
await bookmarkSvg(page).click();
await expect(countLocator).toHaveText(String(afterAdd - 1), { timeout: 10000 });
});
});
test.describe('ブックマーク: マイページ反映', () => {
test('ブックマークした研究室がマイページに表示される', async ({ page }) => {
// ブックマーク追加
await page.goto('/labs/1');
await bookmarkSvg(page).click();
// カウントが増えるまで待つ(追加完了の確認)
await expect(
page.locator('svg').filter({ has: page.locator('path[stroke="#747D8C"]') }).locator('~ span')
).not.toHaveText('0', { timeout: 10000 });
// マイページに遷移
await page.goto('/mypage');
// ブックマーク済み研究室セクションに研究室名が表示されること
await expect(page.getByText('機械工学科 テストA研究室')).toBeVisible({ timeout: 10000 });
// 件数が1件以上と表示されること
await expect(page.getByText(/保存済み: [1-9]/)).toBeVisible();
});
test('マイページでブックマークがない場合は「ありません」メッセージが表示される', async ({ page }) => {
await page.goto('/mypage');
// ブックマーク件数が 0 ならメッセージが表示される
const hasNone = await page.getByText('ブックマーク済みの研究室はありません。').isVisible();
const hasCard = await page.getByText(/保存済み: [1-9]/).isVisible();
// どちらか一方が表示されていること(状態に依存しない検証)
expect(hasNone || hasCard).toBeTruthy();
});
});
- 実行コマンド
/project-root/src
$ npx playwright test e2e/bookmark.spec.js
コミットです!
5. コンテンツ作成・編集
コンテンツ(大学・学部・研究室)の作成・編集をテストします。
5.1 作成・実行
e2e/helpers/kebab-menu.js
\project-root\src\e2e\helpers\kebab-menu.js
/**
* ケバブメニューを開いて指定ラベルのメニューアイテムを押すヘルパー
* @param {import('@playwright/test').Page} page
* @param {string} label - メニューアイテムのラベル
*/
export async function openMenuAndClick(page, label) {
// KebabIcon は SVG circle 3つで構成されるボタン
const kebab = page.locator('button').filter({ has: page.locator('circle') }).first();
await kebab.click();
// メニューポップオーバー内のボタンに絞り込む(送信ボタンとの衝突を避ける)
await page.getByRole('listitem').filter({ hasText: label }).getByRole('button').click();
}
e2e/content.spec.js
\project-root\src\e2e\content.spec.js
import { test, expect } from '@playwright/test';
import { AUTH } from './helpers/auth-paths.js';
import { openMenuAndClick } from './helpers/kebab-menu.js';
// このファイルはコンテンツ作成・編集(大学・学部・研究室)をテストする
// シードデータ前提:
// 大学 id=1: テストA国立大学
// 学部 id=5: 工学部 (university_id=1)
// 研究室 id=1: 機械工学科 テストA研究室 (faculty_id=5)
// 全ログインユーザーが作成・編集可能(Policy: user.exists)
test.use({ storageState: AUTH.user });
// ─────────────────────────────────────────────
// 大学
// ─────────────────────────────────────────────
test.describe('コンテンツ作成: 大学', () => {
test('マイページの「追加」ボタンで大学を作成できる', async ({ page }) => {
await page.goto('/mypage');
// 「追加」ボタンをクリックして大学作成モーダルを開く
await page.getByRole('button', { name: '追加' }).click();
await expect(page.getByText('大学を作成する')).toBeVisible({ timeout: 10000 });
// 大学名を入力
const uniName = `E2Eテスト大学_${Date.now()}`;
await page.getByPlaceholder('大学名(正式名称)').fill(uniName);
// 「作成する」を押す
await page.getByRole('button', { name: '作成する' }).click();
// 大学作成後は学部一覧ページにリダイレクトされるため、サイドバーからマイページに戻る
await page.getByRole('button', { name: 'メニューを開く' }).click();
await page.locator('aside[role="dialog"]').getByRole('link', { name: 'マイページ' }).click();
// マイページに戻り、作成済み大学セクションに表示されること
await expect(page.getByRole('link', { name: uniName })).toBeVisible({ timeout: 10000 });
});
});
test.describe('コンテンツ編集: 大学', () => {
test('学部一覧ページのケバブメニューから大学を編集できる', async ({ page }) => {
await page.goto('/universities/1/faculties');
// ケバブメニュー →「編集する」
await openMenuAndClick(page, '編集する');
await expect(page.getByText('大学を編集する')).toBeVisible({ timeout: 10000 });
// 大学名を変更
const nameInput = page.getByPlaceholder('大学名(正式名称)');
await nameInput.clear();
await nameInput.fill('テストA国立大学(編集済み)');
// 編集理由を入力
await page.getByPlaceholder('編集理由を入力してください').fill('E2Eテスト編集');
// 「編集する」を押す
await page.getByRole('button', { name: '編集する' }).click();
// 変更が反映されること
await expect(page.getByRole('link', { name: 'テストA国立大学(編集済み)' })).toBeVisible({ timeout: 10000 });
// 元に戻す
await openMenuAndClick(page, '編集する');
const nameInput2 = page.getByPlaceholder('大学名(正式名称)');
await nameInput2.clear();
await nameInput2.fill('テストA国立大学');
await page.getByPlaceholder('編集理由を入力してください').fill('E2Eテスト 元に戻す');
await page.getByRole('button', { name: '編集する' }).click();
await expect(page.getByRole('link', { name: 'テストA国立大学' })).toBeVisible({ timeout: 10000 });
});
});
// ─────────────────────────────────────────────
// 学部
// ─────────────────────────────────────────────
test.describe('コンテンツ作成: 学部', () => {
test('学部一覧ページのケバブメニューから学部を作成できる', async ({ page }) => {
await page.goto('/universities/1/faculties');
// 学部数を記録
const beforeText = await page.getByText(/件の学部/).textContent();
const before = parseInt(beforeText);
// ケバブメニュー →「学部を追加する」
await openMenuAndClick(page, '学部を追加する');
await expect(page.getByText('学部を作成する')).toBeVisible({ timeout: 10000 });
// 学部名を入力して作成
const facName = `E2Eテスト学部_${Date.now()}`;
await page.getByPlaceholder('学部名(正式名称)').fill(facName);
await page.getByRole('button', { name: '作成する' }).click();
// 学部作成後は研究室一覧ページにリダイレクトされるため、リダイレクト完了を待ってから学部一覧ページに戻る
await page.waitForURL(/\/faculties\/\d+\/labs/, { timeout: 10000 });
await page.goto('/universities/1/faculties');
// 学部一覧に追加された学部が表示されること
await expect(page.getByText(facName)).toBeVisible({ timeout: 10000 });
// 件数が増えていること
await expect(page.getByText(new RegExp(`${before + 1}件の学部`))).toBeVisible();
});
});
test.describe('コンテンツ編集: 学部', () => {
test('研究室一覧ページのケバブメニューから学部を編集できる', async ({ page }) => {
await page.goto('/faculties/5/labs');
// ケバブメニュー →「編集する」
await openMenuAndClick(page, '編集する');
await expect(page.getByText('学部を編集する')).toBeVisible({ timeout: 10000 });
// 学部名を変更
const nameInput = page.getByPlaceholder('学部名(正式名称)');
await nameInput.clear();
await nameInput.fill('工学部(編集済み)');
await page.getByPlaceholder('編集理由を入力してください').fill('E2Eテスト編集');
await page.getByRole('button', { name: '編集する' }).click();
// 変更が反映されること(パンくずリストのリンクで確認)
await expect(page.getByRole('link', { name: '工学部(編集済み)', exact: true })).toBeVisible({ timeout: 10000 });
// 元に戻す
await openMenuAndClick(page, '編集する');
const nameInput2 = page.getByPlaceholder('学部名(正式名称)');
await nameInput2.clear();
await nameInput2.fill('工学部');
await page.getByPlaceholder('編集理由を入力してください').fill('E2Eテスト 元に戻す');
await page.getByRole('button', { name: '編集する' }).click();
await expect(page.getByRole('link', { name: '工学部', exact: true })).toBeVisible({ timeout: 10000 });
});
});
// ─────────────────────────────────────────────
// 研究室
// ─────────────────────────────────────────────
test.describe('コンテンツ作成: 研究室', () => {
test('研究室一覧ページのケバブメニューから研究室を作成できる', async ({ page }) => {
await page.goto('/faculties/5/labs');
// 研究室数を記録
const beforeText = await page.getByText(/件の研究室/).textContent();
const before = parseInt(beforeText);
// ケバブメニュー →「研究室を追加する」
await openMenuAndClick(page, '研究室を追加する');
await expect(page.getByText('研究室を作成する')).toBeVisible({ timeout: 10000 });
// 研究室名を入力して作成(必須項目のみ)
const labName = `E2Eテスト研究室_${Date.now()}`;
await page.getByPlaceholder('研究室名(正式名称)').fill(labName);
await page.getByRole('button', { name: '作成する' }).click();
// 研究室作成後は研究室詳細ページにリダイレクトされるため、リダイレクト完了を待ってから研究室一覧ページに戻る
await page.waitForURL(/\/labs\/\d+$/, { timeout: 10000 });
await page.goto('/faculties/5/labs');
// 研究室一覧に追加された研究室が表示されること
await expect(page.getByText(labName)).toBeVisible({ timeout: 10000 });
// 件数が増えていること
await expect(page.getByText(new RegExp(`${before + 1}件の研究室`))).toBeVisible();
});
});
test.describe('コンテンツ編集: 研究室', () => {
test('研究室詳細ページのケバブメニューから研究室を編集できる', async ({ page }) => {
await page.goto('/labs/1');
// ケバブメニュー →「編集する」
await openMenuAndClick(page, '編集する');
await expect(page.getByText('研究室を編集する')).toBeVisible({ timeout: 10000 });
// 研究室名を変更
const nameInput = page.getByPlaceholder('研究室名(正式名称)');
await nameInput.clear();
await nameInput.fill('機械工学科 テストA研究室(編集済み)');
await page.getByPlaceholder('編集理由を入力してください').fill('E2Eテスト編集');
await page.getByRole('button', { name: '編集する' }).click();
// 変更が反映されること
await expect(page.getByRole('link', { name: '機械工学科 テストA研究室(編集済み)', exact: true })).toBeVisible({ timeout: 10000 });
// 元に戻す
await openMenuAndClick(page, '編集する');
const nameInput2 = page.getByPlaceholder('研究室名(正式名称)');
await nameInput2.clear();
await nameInput2.fill('機械工学科 テストA研究室');
await page.getByPlaceholder('編集理由を入力してください').fill('E2Eテスト 元に戻す');
await page.getByRole('button', { name: '編集する' }).click();
await expect(page.getByRole('link', { name: '機械工学科 テストA研究室', exact: true })).toBeVisible({ timeout: 10000 });
});
});
実行コマンド
/project-root/src
$ npx playwright test e2e/content.spec.js
5.2 デバッグ
プレースホルダが間違っていましたので、修正しましょう。
\project-root\src\resources\js\Components\Faculty\EditFacultyModal.jsx
{/* 学部名(修正: 大学名 → 学部名) */}
<InputField
type="text"
value={data.name}
onChange={e => setData('name', e.target.value)}
placeholder="学部名(正式名称)"
size="sm"
className="mb-2 w-full"
/>
5.3 再テスト
実行コマンド
/var/www
$ npm run build
/project-root/src
$ npx playwright test e2e/content.spec.js
コミットしておきましょう。
6. マイページ
\project-root\src\e2e\mypage.spec.js
import { test, expect } from '@playwright/test';
import { AUTH } from './helpers/auth-paths.js';
// マイページ(一般ユーザー)のテスト
// シードデータ前提:
// user@example.com (global-setup で tinker 経由で作成、name="テストユーザー")
// admin が作成した大学・学部・研究室は存在するが、user は何も作成していない初期状態
test.use({ storageState: AUTH.user });
// ─────────────────────────────────────────────
// マイページ表示
// ─────────────────────────────────────────────
test.describe('マイページ: 表示', () => {
test('マイページにユーザー情報が表示される', async ({ page }) => {
await page.goto('/mypage');
// 基本情報セクション
await expect(page.getByText('基本情報')).toBeVisible();
// ニックネーム・メールアドレス・パスワードのラベル
await expect(page.getByText('ニックネーム')).toBeVisible();
await expect(page.getByText('e-Mailアドレス')).toBeVisible();
await expect(page.getByText('パスワード')).toBeVisible();
// 退会リンク
await expect(page.getByRole('button', { name: '退会' })).toBeVisible();
});
test('ブックマーク0件の場合は空状態メッセージが表示される', async ({ page }) => {
await page.goto('/mypage');
await expect(page.getByText('ブックマーク済み研究室')).toBeVisible();
await expect(page.getByText('ブックマーク済みの研究室はありません。')).toBeVisible();
});
test('作成済みコンテンツが0件の場合は空状態メッセージが表示される', async ({ page }) => {
await page.goto('/mypage');
await expect(page.getByText('作成済みの大学はありません。')).toBeVisible();
await expect(page.getByText('作成済みの学部はありません。')).toBeVisible();
await expect(page.getByText('作成済みの研究室はありません。')).toBeVisible();
});
});
// ─────────────────────────────────────────────
// ニックネーム編集
// ─────────────────────────────────────────────
test.describe('マイページ: ユーザー情報編集', () => {
test('ニックネームを変更して保存できる', async ({ page }) => {
await page.goto('/mypage');
// UserInfoBar の鉛筆アイコン(img alt="編集")をクリックして EditUserModal を開く
await page.getByAltText('編集').first().click();
await expect(page.getByText('ユーザー情報を編集する')).toBeVisible({ timeout: 10000 });
// ニックネームを変更
const nicknameInput = page.getByPlaceholder('ニックネーム');
await nicknameInput.clear();
await nicknameInput.fill('変更後ユーザー名');
// 保存
await page.getByRole('button', { name: '更新する' }).click();
// モーダルが閉じること(opacity: 0 で非表示になるため CSS で確認)
await expect(page.getByRole('dialog').filter({ hasText: 'ユーザー情報を編集する' })).toHaveCSS('opacity', '0', { timeout: 10000 });
// 元に戻す
await page.getByAltText('編集').first().click();
await expect(page.getByText('ユーザー情報を編集する')).toBeVisible({ timeout: 10000 });
const nicknameInput2 = page.getByPlaceholder('ニックネーム');
await nicknameInput2.clear();
await nicknameInput2.fill('テストユーザー');
await page.getByRole('button', { name: '更新する' }).click();
await expect(page.getByRole('dialog').filter({ hasText: 'ユーザー情報を編集する' })).toHaveCSS('opacity', '0', { timeout: 10000 });
});
});
// ─────────────────────────────────────────────
// 退会ページ
// ─────────────────────────────────────────────
test.describe('マイページ: 退会ページ', () => {
test('退会ページに遷移できる', async ({ page }) => {
await page.goto('/mypage');
await page.getByRole('button', { name: '退会' }).click();
await expect(page).toHaveURL(/\/mypage\/withdrawal/);
await expect(page.getByText('退会すると、以下のデータがすべて削除されます。')).toBeVisible();
await expect(page.getByRole('button', { name: '退会する' }).first()).toBeVisible();
await expect(page.getByText('マイページに戻る')).toBeVisible();
});
test('退会ページの「退会する」ボタンで確認モーダルが表示される', async ({ page }) => {
await page.goto('/mypage/withdrawal');
await page.getByRole('button', { name: '退会する' }).first().click();
// AlertModal「退会の確認」が表示される
await expect(page.getByText('退会の確認')).toBeVisible({ timeout: 10000 });
await expect(page.getByText('退会すると元に戻せません。本当に退会しますか?')).toBeVisible();
// キャンセルでモーダルが閉じる(opacity: 0 で非表示になるため CSS で確認)
await page.getByRole('button', { name: 'キャンセル' }).click();
await expect(page.getByRole('dialog').filter({ hasText: '退会の確認' })).toHaveCSS('opacity', '0', { timeout: 10000 });
});
test('退会ページから「マイページに戻る」でマイページに戻れる', async ({ page }) => {
await page.goto('/mypage/withdrawal');
await page.getByText('マイページに戻る').click();
await expect(page).toHaveURL(/\/mypage/);
await expect(page.getByText('基本情報')).toBeVisible({ timeout: 10000 });
});
});
// ─────────────────────────────────────────────
// ブックマーク・作成済みコンテンツが1件以上ある場合
// ─────────────────────────────────────────────
test.describe('マイページ: データあり状態の表示', () => {
test('ブックマーク済み研究室が1件表示される', async ({ page }) => {
// lab_id=1 の研究室をブックマーク
await page.goto('/labs/1');
await page.locator('svg').filter({ has: page.locator('path[stroke="#747D8C"]') }).click();
// マイページに移動して確認
await page.goto('/mypage');
await expect(page.getByText('ブックマーク済みの研究室はありません。')).not.toBeVisible({ timeout: 10000 });
await expect(page.getByText(/保存済み: [1-9]/)).toBeVisible();
await expect(page.getByText('1 / 1')).toBeVisible();
});
test('作成済み大学が1件表示される', async ({ page }) => {
// マイページの「追加」ボタンから大学を作成
await page.goto('/mypage');
await page.getByRole('button', { name: '追加' }).click();
await expect(page.getByText('大学を作成する')).toBeVisible({ timeout: 10000 });
await page.getByPlaceholder('大学名(正式名称)').fill('E2Eテスト大学');
await page.getByRole('button', { name: '作成する' }).click();
// 作成後は学部一覧にリダイレクトされるのでマイページに戻る
await page.goto('/mypage');
await expect(page.getByText('作成済みの大学はありません。')).not.toBeVisible({ timeout: 10000 });
await expect(page.getByText('E2Eテスト大学').first()).toBeVisible();
});
test('作成済み学部が1件表示される', async ({ page }) => {
// university_id=1 の学部一覧から学部を追加
await page.goto('/universities/1/faculties');
const kebab = page.locator('button').filter({ has: page.locator('circle') }).first();
await kebab.click();
await page.getByRole('button', { name: '学部を追加する' }).click();
await expect(page.getByText('学部を作成する')).toBeVisible({ timeout: 10000 });
await page.getByPlaceholder('学部名(正式名称)').fill('E2Eテスト学部');
await page.getByRole('button', { name: '作成する' }).click();
// 作成後は研究室一覧にリダイレクトされるのでマイページに移動
await page.goto('/mypage');
await expect(page.getByText('作成済みの学部はありません。')).not.toBeVisible({ timeout: 10000 });
await expect(page.getByText('E2Eテスト学部')).toBeVisible();
});
test('作成済み研究室が1件表示される', async ({ page }) => {
// faculty_id=5 の研究室一覧から研究室を追加
await page.goto('/faculties/5/labs');
const kebab = page.locator('button').filter({ has: page.locator('circle') }).first();
await kebab.click();
await page.getByRole('button', { name: '研究室を追加する' }).click();
await expect(page.getByText('研究室を作成する')).toBeVisible({ timeout: 10000 });
await page.getByPlaceholder('研究室名(正式名称)').fill('E2Eテスト研究室');
await page.getByRole('button', { name: '作成する' }).click();
// 作成後は研究室詳細にリダイレクトされるのでマイページに移動
await page.goto('/mypage');
await expect(page.getByText('作成済みの研究室はありません。')).not.toBeVisible({ timeout: 10000 });
await expect(page.getByText('E2Eテスト研究室')).toBeVisible();
});
});
実行コマンド
/project-root/src
$ npx playwright test e2e/mypage.spec.js
こみっと!
7. 削除依頼
\project-root\src\e2e\deletion-request.spec.js
import { test, expect } from '@playwright/test';
import { AUTH } from './helpers/auth-paths.js';
// 削除依頼(一般ユーザー)のテスト
// シードデータ前提:
// University id=1: テストA国立大学
// Faculty id=5: 工学部 (university_id=1)
// Lab id=1: 機械工学科 テストA研究室 (faculty_id=5)
test.use({ storageState: AUTH.user });
const kebabButton = (page) =>
page.locator('button').filter({ has: page.locator('circle') }).first();
// ─────────────────────────────────────────────
// フォームへの遷移
// ─────────────────────────────────────────────
test.describe('削除依頼: フォーム遷移', () => {
test('大学ページのメニューから削除依頼フォームに遷移できる', async ({ page }) => {
await page.goto('/universities/1/faculties');
await kebabButton(page).click();
await page.getByRole('button', { name: '削除依頼をする' }).click();
await expect(page).toHaveURL(/\/deletion-requests\/create\/university\/1/);
await expect(page.getByText('削除依頼フォーム - テストA国立大学')).toBeVisible();
await expect(page.getByText('掲載情報の削除をご希望の場合は')).toBeVisible();
});
test('学部ページのメニューから削除依頼フォームに遷移できる', async ({ page }) => {
await page.goto('/faculties/5/labs');
await kebabButton(page).click();
await page.getByRole('button', { name: '削除依頼をする' }).click();
await expect(page).toHaveURL(/\/deletion-requests\/create\/faculty\/5/);
await expect(page.getByText('削除依頼フォーム - 工学部')).toBeVisible();
});
test('研究室ページのメニューから削除依頼フォームに遷移できる', async ({ page }) => {
await page.goto('/labs/1');
await kebabButton(page).click();
await page.getByRole('button', { name: '削除依頼をする' }).click();
await expect(page).toHaveURL(/\/deletion-requests\/create\/lab\/1/);
await expect(page.getByText(/削除依頼フォーム - .+/)).toBeVisible();
});
});
// ─────────────────────────────────────────────
// フォームの操作
// ─────────────────────────────────────────────
test.describe('削除依頼: フォーム操作', () => {
test('削除依頼を送信すると完了ページが表示される', async ({ page }) => {
await page.goto('/deletion-requests/create/university/1');
await page.getByPlaceholder('削除を希望する理由をご記入ください。').fill('E2Eテストによる削除依頼です。');
await page.getByRole('button', { name: '送信' }).click();
await expect(page.getByText('削除依頼をお送りいただき、ありがとうございました。')).toBeVisible({ timeout: 10000 });
await expect(page.getByRole('button', { name: 'ホームへ戻る' })).toBeVisible();
});
test('理由が空の場合はバリデーションエラーが表示される', async ({ page }) => {
await page.goto('/deletion-requests/create/university/1');
// テキストエリアを空のまま送信
await page.getByRole('button', { name: '送信' }).click();
await expect(page.getByText('削除理由は必須項目です。')).toBeVisible({ timeout: 10000 });
});
test('「戻る」ボタンで元の大学ページに戻れる', async ({ page }) => {
await page.goto('/deletion-requests/create/university/1');
await page.getByRole('button', { name: /大学に戻る/ }).click();
await expect(page).toHaveURL(/\/universities\/1\/faculties/);
});
test('「戻る」ボタンで元の学部ページに戻れる', async ({ page }) => {
await page.goto('/deletion-requests/create/faculty/5');
await page.getByRole('button', { name: /学部に戻る/ }).click();
await expect(page).toHaveURL(/\/faculties\/5\/labs/);
});
test('「戻る」ボタンで元の研究室ページに戻れる', async ({ page }) => {
await page.goto('/deletion-requests/create/lab/1');
await page.getByRole('button', { name: /研究室に戻る/ }).click();
await expect(page).toHaveURL(/\/labs\/1/);
});
test('完了ページの「ホームへ戻る」でトップページに遷移できる', async ({ page }) => {
await page.goto('/deletion-requests/create/lab/1');
await page.getByPlaceholder('削除を希望する理由をご記入ください。').fill('E2Eテストによる削除依頼です。');
await page.getByRole('button', { name: '送信' }).click();
await expect(page.getByText('削除依頼をお送りいただき、ありがとうございました。')).toBeVisible({ timeout: 10000 });
await page.getByRole('button', { name: 'ホームへ戻る' }).click();
await expect(page).toHaveURL('/');
});
});
// ─────────────────────────────────────────────
// ゲスト(未ログイン)アクセス
// ─────────────────────────────────────────────
test.describe('削除依頼: ゲストアクセス', () => {
test.use({ storageState: { cookies: [], origins: [] } });
test('未ログインで削除依頼フォームにアクセスするとリダイレクトされる', async ({ page }) => {
await page.goto('/deletion-requests/create/university/1');
// 認証ページ or トップページにリダイレクトされること
await expect(page).not.toHaveURL(/\/deletion-requests\/create/);
});
});
実行コマンド
/project-root/src
$ npx playwright test e2e/deletion-request.spec.js
コミットです。
今日はこれで終わりなので、プッシュ、PR作成、マージ、ブランチの削除をしましょう。
9. まとめ・次回予告
お疲れ様でした!
今回は、一般ユーザーの目線に立ったテストシナリオを作成しました。
次回は、管理者の目線に立ったテストシナリオを作成したいと思います!
これまでの記事一覧
☆要件定義・設計編
☆環境構築編
☆バックエンド実装編
- その7: バックエンド実装編① ~認証機能作成~
- その8: バックエンド実装編②前編 ~レビュー投稿機能作成~
- その8.5: バックエンド実装編②後編 ~レビュー投稿機能作成~
- その9: バックエンド実装編③ ~レビューCRUD機能作成~
- その10: バックエンド実装編④ ~レビューCRUD機能作成その2~
- その11: バックエンド実装編⑤ ~新規大学・学部・研究室作成機能作成~
- その12: バックエンド実装編⑥ ~大学検索機能作成~
- その13: バックエンド実装編⑦ ~大学・学部・研究室編集機能作成~
- その14: バックエンド実装編⑧ ~コメント投稿機能~
- その15: バックエンド実装編⑨ ~コメント編集・削除機能~
- その16: バックエンド実装編⑩ ~ブックマーク機能~
- その17: バックエンド実装編⑪ ~排他制御・トランザクション処理~
- その18: バックエンド実装編⑫ ~マイページ機能作成~
- その19: バックエンド実装編⑬ ~管理者アカウント機能作成~
- その20: バックエンド実装編⑭ ~通知機能作成~
- その21: バックエンド実装編⑮ ~ソーシャルログイン機能作成~
☆フロントエンド実装編
- その22: フロントエンド実装編① ~メインコンテンツ領域作成~
- その23: フロントエンド実装編② ~ヘッダー作成~
- その24: フロントエンド実装編③ ~サイドバー作成~
- その25: フロントエンド実装編④ ~ホームページ作成~
- その26: フロントエンド実装編⑤ ~大学検索結果画面作成~
- その27: フロントエンド実装編⑥ ~大学詳細・学部一覧画面作成~
- その28: フロントエンド実装編⑦ ~学部詳細・研究室一覧画面作成~
- その29: フロントエンド実装編⑧前編 ~研究室詳細・レビュー画面作成前編~
- その29.5: フロントエンド実装編⑧後編 ~研究室詳細・レビュー画面作成後編~
- その30: フロントエンド実装編⑨ ~マイページ作成~
- その31: フロントエンド実装編⑩ ~パンくずリスト作成~
- その32: フロントエンド実装編⑪ ~ログイン・新規登録モーダル作成~
- その33: フロントエンド実装編⑫ ~大学作成・編集モーダル作成~
- その34: フロントエンド実装編⑬ ~学部作成・編集モーダル作成~
- その35: フロントエンド実装編⑭ ~研究室作成・編集モーダル作成~
- その36: フロントエンド実装編⑮ ~レビュー作成・編集モーダル作成~
- その37: フロントエンド実装編⑯ ~コメント一覧表示モーダル作成~
- その38: フロントエンド実装編⑰ ~編集履歴ページ・削除依頼ページ・通知ドロップダウン作成~
- その39: フロントエンド実装編⑱ ~トースト作成~
- その40: フロントエンド実装編⑲ ~退会ページ作成~
- その41: フロントエンド実装編⑳ ~ファビコン作成~
- その42: フロントエンド実装編㉑ ~レスポンシブデザイン対応~
☆テスト・デバッグ編
- その43: テスト・デバッグ編① ~Unitテスト1[テストデータ準備・DB設定]~
- その44: テスト・デバッグ編② ~Unitテスト2[ポリシー]~
- その45: テスト・デバッグ編③ ~Unitテスト3[モデル]~
- その46: テスト・デバッグ編④ ~Featureテスト1[コントローラー①/レビュー/コメント/ブックマーク]~
- [その47: テスト・デバッグ編⑤ ~Featureテスト2コントローラー②/大学/学部/研究室~
- その48: テスト・デバッグ編⑥ ~Featureテスト3[コントローラー③/マイページ/削除依頼/通知/ホーム/管理者]~
- その49: テスト・デバッグ編⑦ ~Featureテスト4[ポリシー・モデル残り]~
- その50: テスト・デバッグ編⑧)~E2Eテスト1[Playwrightセットアップ]~
- その51: テスト・デバッグ編⑨)~E2Eテスト2[ゲスト]~
軽く宣伝
YouTubeを始めました(というか始めてました)。
内容としては、Webエンジニアの生活や稼げるようになるまでの成長記録などを発信していく予定です。






