-
Notifications
You must be signed in to change notification settings - Fork 0
chore: PlaywrightによるE2Eテストを導入 #73
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| name: E2E Tests | ||
|
|
||
| on: | ||
| pull_request: | ||
| branches: | ||
| - main | ||
| workflow_dispatch: | ||
|
|
||
| jobs: | ||
| e2e: | ||
| runs-on: ubuntu-latest | ||
| strategy: | ||
| matrix: | ||
| node-version: [22] | ||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| - name: Install pnpm | ||
| uses: pnpm/action-setup@v4 | ||
| with: | ||
| version: 10 | ||
| - name: Use Node.js ${{ matrix.node-version }} | ||
| uses: actions/setup-node@v6 | ||
| with: | ||
| node-version: ${{ matrix.node-version }} | ||
| cache: "pnpm" | ||
| - name: Install dependencies | ||
| run: pnpm install | ||
|
|
||
| - name: Install Playwright browsers | ||
| run: pnpm exec playwright install chromium --with-deps | ||
|
|
||
| - name: Build content | ||
| run: pnpm build:content | ||
|
|
||
| - name: Run E2E tests | ||
| run: pnpm test:e2e | ||
|
|
||
| - name: Upload test report | ||
| uses: actions/upload-artifact@v4 | ||
| if: failure() | ||
| with: | ||
| name: playwright-report | ||
| path: playwright-report/ | ||
| retention-days: 7 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,9 @@ | |
|
|
||
| # testing | ||
| /coverage | ||
| /test-results/ | ||
| /playwright-report/ | ||
| /playwright/.cache/ | ||
|
|
||
| # next.js | ||
| /.next/ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| import { test, expect } from "@playwright/test" | ||
|
|
||
| test.describe("記事ページ", () => { | ||
| // テスト用の記事slug(実在する記事を使用) | ||
| const testArticleSlug = "20230102_helloworld" | ||
|
|
||
| test("トップページから記事に遷移できる", async ({ page }) => { | ||
| await page.goto("/") | ||
|
|
||
| // 最初の記事をクリック | ||
| const firstArticle = page.locator("main ul li").first() | ||
| await firstArticle.click() | ||
|
|
||
| // 記事ページに遷移したことを確認 | ||
| await expect(page).toHaveURL(/\/articles\/.+/) | ||
| }) | ||
|
|
||
| test("記事タイトルが表示される", async ({ page }) => { | ||
| await page.goto(`/articles/${testArticleSlug}`) | ||
|
|
||
| // h2タグでタイトルが表示される | ||
| const title = page.locator("h2.font-bold") | ||
| await expect(title).toBeVisible() | ||
| await expect(title).toHaveText("ブログを始めました") | ||
| }) | ||
|
|
||
| test("公開日が表示される", async ({ page }) => { | ||
| await page.goto(`/articles/${testArticleSlug}`) | ||
|
|
||
| // 記事ヘッダー内に日付が表示される(main内のheader) | ||
| const articleHeader = page | ||
| .locator("main") | ||
| .locator("..") | ||
| .locator("header") | ||
| .first() | ||
| await expect(articleHeader).toContainText("2023-11-09") | ||
| }) | ||
|
|
||
| test("記事本文が表示される", async ({ page }) => { | ||
| await page.goto(`/articles/${testArticleSlug}`) | ||
|
|
||
| // proseクラスを持つ記事本文コンテナ | ||
| const articleContent = page.locator(".prose") | ||
| await expect(articleContent).toBeVisible() | ||
|
|
||
| // 記事内に特定のテキストが含まれることを確認 | ||
| await expect(articleContent).toContainText("ブログを始めました") | ||
| }) | ||
|
|
||
| test("いいねボタンが表示される", async ({ page }) => { | ||
| await page.goto(`/articles/${testArticleSlug}`) | ||
|
|
||
| // footer内のボタン | ||
| const likeButton = page.locator("footer button") | ||
| await expect(likeButton).toBeVisible() | ||
| }) | ||
|
|
||
| test("存在しない記事は404を表示する", async ({ page }) => { | ||
| await page.goto("/articles/non-existent-article-slug") | ||
|
|
||
| // 404表示を確認(div要素内の404テキスト) | ||
| await expect(page.locator("div").filter({ hasText: /^404$/ })).toBeVisible() | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| -- テスト用ツイートデータ(既存データがあれば削除して挿入) | ||
| DELETE FROM tweets; | ||
|
|
||
| INSERT INTO tweets (content, created_at) VALUES | ||
| ('テスト用つぶやき1', datetime('now', '-1 hour')), | ||
| ('テスト用つぶやき2', datetime('now', '-2 hours')), | ||
| ('テスト用つぶやき3', datetime('now', '-3 hours')), | ||
| ('テスト用つぶやき4', datetime('now', '-4 hours')), | ||
| ('テスト用つぶやき5', datetime('now', '-5 hours')); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import { test, expect } from "@playwright/test" | ||
|
|
||
| test.describe("トップページ", () => { | ||
| test.beforeEach(async ({ page }) => { | ||
| await page.goto("/") | ||
| }) | ||
|
|
||
| test("「最新の記事」セクションが表示される", async ({ page }) => { | ||
| const articlesHeading = page.locator("h2", { hasText: "最新の記事" }) | ||
| await expect(articlesHeading).toBeVisible() | ||
| }) | ||
|
|
||
| test("記事リストが表示される", async ({ page }) => { | ||
| // 記事リスト(ul内のli要素)が存在することを確認 | ||
| const articleList = page.locator("main ul li") | ||
|
|
||
| // 少なくとも1つ以上の記事が表示されている | ||
| await expect(articleList.first()).toBeVisible() | ||
|
|
||
| // 記事にはタイトル(h3)と日付(time)が含まれる | ||
| const firstArticle = articleList.first() | ||
| await expect(firstArticle.locator("h3")).toBeVisible() | ||
| await expect(firstArticle.locator("time")).toBeVisible() | ||
| }) | ||
|
|
||
| test("「最新のつぶやき」セクションが表示される", async ({ page }) => { | ||
| const tweetsHeading = page.locator("h2", { hasText: "最新のつぶやき" }) | ||
| await expect(tweetsHeading).toBeVisible() | ||
| }) | ||
|
|
||
| test("ツイートリストが5件表示される", async ({ page }) => { | ||
| // つぶやきセクション内のツイート(preタグを含むli) | ||
| const tweetItems = page | ||
| .locator("aside ul li") | ||
| .filter({ has: page.locator("pre") }) | ||
|
|
||
| // 5件のツイートが表示されている | ||
| await expect(tweetItems).toHaveCount(5) | ||
| }) | ||
|
|
||
| test("「もっと見る」リンクが存在する", async ({ page }) => { | ||
| const moreLink = page.locator("a", { hasText: "もっと見る" }) | ||
| await expect(moreLink).toBeVisible() | ||
| await expect(moreLink).toHaveAttribute("href", "/tweets") | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -15,7 +15,12 @@ | |||||||||
| "start": "next start", | ||||||||||
| "lint": "next lint", | ||||||||||
| "format": "prettier --write .", | ||||||||||
| "format:check": "prettier --check ." | ||||||||||
| "format:check": "prettier --check .", | ||||||||||
| "test:e2e": "playwright test", | ||||||||||
| "test:e2e:ui": "playwright test --ui", | ||||||||||
| "test:e2e:debug": "playwright test --debug", | ||||||||||
| "test:e2e:report": "playwright show-report", | ||||||||||
| "test:e2e:server": "pnpm wrangler d1 migrations apply blog-iine-counter --local && pnpm wrangler d1 execute blog-iine-counter --local --file=./e2e/fixtures/seed.sql && pnpm preview" | ||||||||||
|
||||||||||
| "test:e2e:server": "pnpm wrangler d1 migrations apply blog-iine-counter --local && pnpm wrangler d1 execute blog-iine-counter --local --file=./e2e/fixtures/seed.sql && pnpm preview" | |
| "test:e2e:db:migrate": "pnpm wrangler d1 migrations apply blog-iine-counter --local", | |
| "test:e2e:db:seed": "pnpm wrangler d1 execute blog-iine-counter --local --file=./e2e/fixtures/seed.sql", | |
| "test:e2e:server": "pnpm run test:e2e:db:migrate && pnpm run test:e2e:db:seed && pnpm preview" |
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,21 @@ | ||||||||
| import { defineConfig, devices } from "@playwright/test" | ||||||||
|
|
||||||||
| export default defineConfig({ | ||||||||
| testDir: "./e2e", | ||||||||
| timeout: 30 * 1000, | ||||||||
| retries: process.env.CI ? 2 : 0, | ||||||||
|
||||||||
| retries: process.env.CI ? 2 : 0, | |
| retries: process.env.CI ? 2 : 0, | |
| // NOTE: テスト並列実行によるデータベースや共有リソースの競合を避けるため、ワーカー数を 1 に固定している |
Copilot
AI
Jan 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
webServerのtimeoutが180秒(3分)に設定されていますが、この長いタイムアウトが必要な理由を検討してください。もし実際のサーバー起動時間が短い場合は、より短いタイムアウト値を設定することで、テストの失敗をより早く検出できます。
| timeout: 180 * 1000, | |
| timeout: 60 * 1000, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
テスト用シードデータで
DELETE FROM tweets;を実行していますが、これは既存のデータを削除します。テスト環境が適切に分離されていることを確認してください。また、テスト後のクリーンアップ処理が必要かどうかも検討してください。