Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .github/workflows/e2e.yaml
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

# testing
/coverage
/test-results/
/playwright-report/
/playwright/.cache/

# next.js
/.next/
Expand Down
11 changes: 11 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ Conventional Commitに準じた形式:

例: `feat/add-dark-mode`, `fix/like-button-error`, `refactor/api-structure`

## PR作成前のルール

**PRを作成する前に、必ず以下のコマンドを実行し、問題があれば修正すること。**

```bash
pnpm lint
pnpm format:check
```

問題がある場合は `pnpm format` でフォーマットを修正し、lintエラーは手動で修正する。

## コマンド

```bash
Expand Down
64 changes: 64 additions & 0 deletions e2e/article.spec.ts
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()
})
})
9 changes: 9 additions & 0 deletions e2e/fixtures/seed.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-- テスト用ツイートデータ(既存データがあれば削除して挿入)
DELETE FROM tweets;
Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

テスト用シードデータでDELETE FROM tweets;を実行していますが、これは既存のデータを削除します。テスト環境が適切に分離されていることを確認してください。また、テスト後のクリーンアップ処理が必要かどうかも検討してください。

Copilot uses AI. Check for mistakes.

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'));
46 changes: 46 additions & 0 deletions e2e/index.spec.ts
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")
})
})
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test:e2e:serverスクリプトが長く複雑です。このスクリプトを複数のステップに分割するか、専用のシェルスクリプトファイルに移動することで、可読性と保守性が向上します。また、各コマンドのエラーハンドリングが不明確です。

Suggested change
"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"

Copilot uses AI. Check for mistakes.
},
"dependencies": {
"@hono/zod-validator": "^0.7.6",
Expand Down Expand Up @@ -49,6 +54,7 @@
"@eslint/js": "^9.39.2",
"@next/bundle-analyzer": "15.5.9",
"@next/eslint-plugin-next": "^15.5.9",
"@playwright/test": "^1.57.0",
"@types/node": "^24.0.0",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
Expand Down
21 changes: 21 additions & 0 deletions playwright.config.ts
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,
Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

現在のテスト設定では1つのワーカー(workers: 1)のみが使用されていますが、この制限が必要な理由をコメントで説明することをお勧めします。もしデータベースの競合を避けるためであれば、その旨を明記すると他の開発者の理解が深まります。

Suggested change
retries: process.env.CI ? 2 : 0,
retries: process.env.CI ? 2 : 0,
// NOTE: テスト並列実行によるデータベースや共有リソースの競合を避けるため、ワーカー数を 1 に固定している

Copilot uses AI. Check for mistakes.
workers: 1,
reporter: [["html", { open: "never" }], ["list"]],
use: {
baseURL: "http://localhost:8787",
screenshot: "only-on-failure",
trace: "on-first-retry",
},
projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }],
webServer: {
command: "pnpm test:e2e:server",
url: "http://localhost:8787",
reuseExistingServer: !process.env.CI,
timeout: 180 * 1000,
Copy link

Copilot AI Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

webServerのtimeoutが180秒(3分)に設定されていますが、この長いタイムアウトが必要な理由を検討してください。もし実際のサーバー起動時間が短い場合は、より短いタイムアウト値を設定することで、テストの失敗をより早く検出できます。

Suggested change
timeout: 180 * 1000,
timeout: 60 * 1000,

Copilot uses AI. Check for mistakes.
},
})
Loading