0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

実務1年目駆け出しエンジニアがLaravel&ReactでWebアプリケーション開発に挑戦してみた!(テスト・デバッグ編⑩)~E2Eテスト4[管理者]~

0
Posted at

実務1年目駆け出しエンジニアがLaravel&ReactでWebアプリケーション開発に挑戦してみた!(その53)

0. 初めに

こんにちは!

1年目の見習いエンジニアが、Webアプリケーションをゼロから開発する様子をお届けしているシリーズ、テスト編です!

ついに今日で、E2Eテスト、ひいてはテストの最後です!

よろしくお願いいたします。

1. ブランチ運用

developブランチを最新化させて、新規にtest/e2e/adminというブランチを切って作業をしましょう。

前回使用したtest/e2e/general-userブランチは削除しておきましょう。

2. 管理者

\project-root\src\e2e\admin.spec.js
import { test, expect } from '@playwright/test';
import { AUTH } from './helpers/auth-paths.js';

// 管理者機能のテスト
// シードデータ前提:
//   admin@example.com (is_admin=true)
//   user@example.com (一般ユーザー)
//   University id=1: テストA国立大学
//   Faculty id=5: 工学部 (university_id=1)
//   Lab id=1: 機械工学科 テストA研究室 (faculty_id=5)

test.use({ storageState: AUTH.admin });

// ─────────────────────────────────────────────
// 削除依頼一覧
// ─────────────────────────────────────────────
test.describe('管理者: 削除依頼一覧', () => {
  test('削除依頼一覧ページにアクセスできる', async ({ page }) => {
    await page.goto('/admin/deletion-requests');

    await expect(page.getByText('削除依頼一覧')).toBeVisible();
    await expect(page.getByRole('button', { name: 'マイページに戻る' })).toBeVisible();
  });

  test('削除依頼がない場合は空状態メッセージが表示される', async ({ page }) => {
    await page.goto('/admin/deletion-requests');

    await expect(page.getByText('削除依頼はありません。')).toBeVisible();
  });

  test('ユーザーが削除依頼を提出すると一覧に表示される', async ({ page, browser }) => {
    // user コンテキストで削除依頼を提出
    const userContext = await browser.newContext({ storageState: AUTH.user });
    const userPage = await userContext.newPage();
    await userPage.goto('/deletion-requests/create/university/1');
    await userPage.getByPlaceholder('削除を希望する理由をご記入ください。').fill('E2Eテスト: 管理者向け削除依頼');
    await userPage.getByRole('button', { name: '送信' }).click();
    await expect(userPage.getByText('削除依頼をお送りいただき、ありがとうございました。')).toBeVisible({ timeout: 10000 });
    await userContext.close();

    // 管理者で削除依頼一覧を確認
    await page.goto('/admin/deletion-requests');
    await expect(page.getByText('テストA国立大学').first()).toBeVisible();
    await expect(page.getByText('(大学)への削除依頼')).toBeVisible();
    await expect(page.getByText('E2Eテスト: 管理者向け削除依頼')).toBeVisible();
    await expect(page.getByText(/依頼者:/)).toBeVisible();
  });

  test('削除依頼の対象名リンクから対象ページに遷移できる', async ({ page, browser }) => {
    // user コンテキストで削除依頼を提出(前提データ)
    const userContext = await browser.newContext({ storageState: AUTH.user });
    const userPage = await userContext.newPage();
    await userPage.goto('/deletion-requests/create/university/1');
    await userPage.getByPlaceholder('削除を希望する理由をご記入ください。').fill('E2Eテスト: 遷移確認');
    await userPage.getByRole('button', { name: '送信' }).click();
    await expect(userPage.getByText('削除依頼をお送りいただき、ありがとうございました。')).toBeVisible({ timeout: 10000 });
    await userContext.close();

    // 管理者でリストの対象名をクリック
    await page.goto('/admin/deletion-requests');
    await page.getByText('テストA国立大学').first().click();

    await expect(page).toHaveURL(/\/universities\/1\/faculties/);
  });

  test('「マイページに戻る」ボタンでマイページに遷移できる', async ({ page }) => {
    await page.goto('/admin/deletion-requests');
    await page.getByRole('button', { name: 'マイページに戻る' }).click();

    await expect(page).toHaveURL(/\/mypage/);
  });
});

// ─────────────────────────────────────────────
// アクセス権限
// ─────────────────────────────────────────────
test.describe('管理者: アクセス権限', () => {
  test('一般ユーザーは削除依頼一覧にアクセスできない', async ({ browser }) => {
    const userContext = await browser.newContext({ storageState: AUTH.user });
    const userPage = await userContext.newPage();

    const response = await userPage.goto('/admin/deletion-requests');

    // abort(403) によりアクセスが拒否されること
    expect(response.status()).toBe(403);

    await userContext.close();
  });

  test('未ログインユーザーは削除依頼一覧にアクセスするとリダイレクトされる', async ({ browser }) => {
    const guestContext = await browser.newContext({ storageState: { cookies: [], origins: [] } });
    const guestPage = await guestContext.newPage();

    await guestPage.goto('/admin/deletion-requests');

    await expect(guestPage).not.toHaveURL(/\/admin\/deletion-requests/);

    await guestContext.close();
  });
});

実行コマンド

/project-root/src
$ npx playwright test e2e/admin.spec.js

実行結果
image.png

コミットしておきましょう!

3. 通知

\project-root\src\e2e\notification.spec.js
import { test, expect } from '@playwright/test';
import { AUTH } from './helpers/auth-paths.js';
import { openMenuAndClick } from './helpers/kebab-menu.js';

// 通知機能のテスト
// シードデータ前提:
//   admin@example.com (is_admin=true, University id=1「テストA国立大学」の作成者)
//   user@example.com (一般ユーザー)
//
// 通知の種類:
//   DeletionRequestNotification  : 削除依頼時に管理者に通知
//   ModelChangedNotification     : コンテンツ編集時に作成者に通知
//   DeletionCompletedNotification: 削除完了時に依頼者に通知(削除UIが未実装のためテスト対象外)

// ─────────────────────────────────────────────
// 通知: 一覧ページ (ユーザー)
// ─────────────────────────────────────────────
test.describe('通知: 一覧ページ', () => {
  test.use({ storageState: AUTH.user });

  test('/notifications にアクセスできる', async ({ page }) => {
    await page.goto('/notifications');

    await expect(page.getByText('通知一覧')).toBeVisible();
  });

  test('通知がない場合は「通知はありません。」が表示される', async ({ page }) => {
    await page.goto('/notifications');

    await expect(page.getByText('通知はありません。')).toBeVisible();
  });

  test('「< マイページに戻る」でマイページに遷移できる', async ({ page }) => {
    await page.goto('/notifications');
    await page.getByRole('button', { name: '< マイページに戻る' }).click();

    await expect(page).toHaveURL(/\/mypage/);
  });
});

// ─────────────────────────────────────────────
// 通知: 削除依頼通知 (管理者)
// ─────────────────────────────────────────────
test.describe('通知: 削除依頼通知', () => {
  test.use({ storageState: AUTH.admin });

  test('ユーザーが削除依頼を提出すると管理者に通知が届く', async ({ page, browser }) => {
    // user コンテキストで削除依頼を提出
    const userContext = await browser.newContext({ storageState: AUTH.user });
    const userPage = await userContext.newPage();
    await userPage.goto('/deletion-requests/create/university/1');
    await userPage.getByPlaceholder('削除を希望する理由をご記入ください。').fill('E2Eテスト: 通知確認');
    await userPage.getByRole('button', { name: '送信' }).click();
    await expect(userPage.getByText('削除依頼をお送りいただき、ありがとうございました。')).toBeVisible({ timeout: 10000 });
    await userContext.close();

    // 管理者の通知一覧を確認
    await page.goto('/notifications');
    await expect(page.getByText('「テストA国立大学」に削除依頼が届きました。').first()).toBeVisible({ timeout: 10000 });
  });

  test('削除依頼通知をクリックすると削除依頼一覧ページに遷移できる', async ({ page, browser }) => {
    // user コンテキストで削除依頼を提出
    const userContext = await browser.newContext({ storageState: AUTH.user });
    const userPage = await userContext.newPage();
    await userPage.goto('/deletion-requests/create/university/1');
    await userPage.getByPlaceholder('削除を希望する理由をご記入ください。').fill('E2Eテスト: 通知クリック遷移確認');
    await userPage.getByRole('button', { name: '送信' }).click();
    await expect(userPage.getByText('削除依頼をお送りいただき、ありがとうございました。')).toBeVisible({ timeout: 10000 });
    await userContext.close();

    // 管理者の通知一覧から通知をクリックして遷移を確認(最初の通知をクリック)
    await page.goto('/notifications');
    await page.getByText('「テストA国立大学」に削除依頼が届きました。').first().click();
    await expect(page).toHaveURL(/\/admin\/deletion-requests/);
  });
});

// ─────────────────────────────────────────────
// 通知: モデル変更通知 (管理者)
// ─────────────────────────────────────────────
test.describe('通知: モデル変更通知', () => {
  test.use({ storageState: AUTH.admin });

  test('ユーザーがコンテンツを編集すると作成者(管理者)に通知が届く', async ({ page, browser }) => {
    // user コンテキストで大学(admin 作成)を編集 → admin に通知が届く
    const userContext = await browser.newContext({ storageState: AUTH.user });
    const userPage = await userContext.newPage();
    await userPage.goto('/universities/1/faculties');
    await openMenuAndClick(userPage, '編集する');
    await expect(userPage.getByText('大学を編集する')).toBeVisible({ timeout: 10000 });

    // 大学名はそのまま、編集理由だけ入力して送信(大学名を変えないことで他テストへの影響を避ける)
    await userPage.getByPlaceholder('編集理由を入力してください').fill('E2Eテスト: 通知確認のための編集');
    await userPage.getByRole('button', { name: '編集する' }).click();
    await expect(userPage.getByRole('link', { name: 'テストA国立大学' })).toBeVisible({ timeout: 10000 });
    await userContext.close();

    // 管理者の通知一覧に変更通知が表示されることを確認
    await page.goto('/notifications');
    await expect(
      page.getByText('あなたが作成した「テストA国立大学(大学)」が編集されました。').first()
    ).toBeVisible({ timeout: 10000 });
  });
});

// ─────────────────────────────────────────────
// 通知: ドロップダウン (管理者)
// ─────────────────────────────────────────────
test.describe('通知: ドロップダウン', () => {
  test.use({ storageState: AUTH.admin });

  test('未読通知がある場合、ベルアイコンに未読バッジが表示される', async ({ page, browser }) => {
    // user コンテキストで削除依頼を提出して通知を生成
    const userContext = await browser.newContext({ storageState: AUTH.user });
    const userPage = await userContext.newPage();
    await userPage.goto('/deletion-requests/create/university/1');
    await userPage.getByPlaceholder('削除を希望する理由をご記入ください。').fill('E2Eテスト: バッジ確認');
    await userPage.getByRole('button', { name: '送信' }).click();
    await expect(userPage.getByText('削除依頼をお送りいただき、ありがとうございました。')).toBeVisible({ timeout: 10000 });
    await userContext.close();

    // 管理者のマイページでベルアイコンの未読バッジを確認
    await page.goto('/mypage');
    const bellButton = page.getByRole('button', { name: '通知' });
    await expect(bellButton.locator('span.bg-red-500')).toBeVisible({ timeout: 10000 });
  });

  test('ベルアイコンをクリックするとドロップダウンに通知が表示される', async ({ page, browser }) => {
    // user コンテキストで削除依頼を提出して通知を生成
    const userContext = await browser.newContext({ storageState: AUTH.user });
    const userPage = await userContext.newPage();
    await userPage.goto('/deletion-requests/create/university/1');
    await userPage.getByPlaceholder('削除を希望する理由をご記入ください。').fill('E2Eテスト: ドロップダウン表示確認');
    await userPage.getByRole('button', { name: '送信' }).click();
    await expect(userPage.getByText('削除依頼をお送りいただき、ありがとうございました。')).toBeVisible({ timeout: 10000 });
    await userContext.close();

    // 管理者がベルアイコンをクリックしてドロップダウンを開く
    await page.goto('/mypage');
    await page.getByRole('button', { name: '通知' }).click();

    // ドロップダウンに通知メッセージが表示される
    await expect(page.getByText('「テストA国立大学」に削除依頼が届きました。').first()).toBeVisible({ timeout: 10000 });
    // フッターの「すべての通知を見る」リンクが表示される
    await expect(page.getByRole('link', { name: 'すべての通知を見る' })).toBeVisible();
  });
});

// ─────────────────────────────────────────────
// 通知: アクセス権限
// ─────────────────────────────────────────────
test.describe('通知: アクセス権限', () => {
  test('未ログインは /notifications にアクセスするとリダイレクトされる', async ({ browser }) => {
    const guestContext = await browser.newContext({ storageState: { cookies: [], origins: [] } });
    const guestPage = await guestContext.newPage();

    await guestPage.goto('/notifications');

    await expect(guestPage).not.toHaveURL(/\/notifications/);

    await guestContext.close();
  });
});

実行コマンド

/project-root/src
$ npx playwright test e2e/notification.spec.js

実行結果
image.png

コミット・プッシュ、PR作成・マージ、ブランチ削除を行いましょう!

4. まとめ

これにて、テスト・デバッグ編完了です!!

5. 感謝とこれからについて

2025年の5月から初めて、もう2026年の9月になり、長いもので1年と4か月ほどもこのシリーズを続けてきました。

もともとは、「早く実務経験を積みたい」という気持ちがある一方で、入社後は研修期間が長くそのギャップへの抵抗の気持ちから始めました。

しかし、今となっては間もなく実務開始から1年が経過しようとしています。

このシリーズでたくさんことを学ぶことができた一方で、ポートフォリオとしては機能数が多く実際の実装も2026年の5月まで1年近く続いてしまいました。

今振り返れば、その間もっといろんな別のことに挑戦できたかもしれなかったと感じています。

ですが、普段の本業の業務では経験できないような「環境構築・設計・バックエンド・フロントエンド・テスト・デプロイ」をすべて自分一人で行うことで幅広い知識を身に着けることができましたし、本業に少なからず良い影響を与えたと感じています。

今後は、このシリーズを通じて学んだことを発信していければと思います。

また、デプロイ編についてですが、AWSを使って5月にとっくにデプロイ済みですが、シリーズとしての投稿はしない予定です。

コードを書くことが少なく、AWSのUIも頻繁に変わりますし、何より僕のモチベーションがもうないので...

ただし、AWSやクラウドに関連する知識(「サブネット」や「EC2」、「RDS」などの基本)を学ぶことはできました。

それについては、どこかで発信できればと思っています。

今後は、学んだことを生かして、副業などに挑戦したいと思います。

もし、ご縁があれば、声をかけてくださるとうれしいです!

最後に、ここまで読んでくださって本当にありがとうございました。

これまでの記事一覧

☆要件定義・設計編

☆環境構築編

☆バックエンド実装編

☆フロントエンド実装編

☆テスト・デバッグ編

軽く宣伝

YouTubeを始めました(というか始めてました)。
内容としては、Webエンジニアの生活や稼げるようになるまでの成長記録などを発信していく予定です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?