diff --git a/.eslintrc.json b/.eslintrc.json index 9ae6ea2..4a38d5d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -62,7 +62,8 @@ // devDependenciesのimportを禁止 "optionalDependencies": false } - ] + ], + "no-unused-vars": "off" }, "env": { "jest": true diff --git a/__e2e__/index.test.ts b/__e2e__/index.test.ts new file mode 100644 index 0000000..43503a1 --- /dev/null +++ b/__e2e__/index.test.ts @@ -0,0 +1,8 @@ +import { test } from "@playwright/test"; + +import IndexPage from "./pages/Index"; + +test("画面項目が表示されること", async ({ page }) => { + await IndexPage.goto(page); + await IndexPage.validate(page); +}); diff --git a/__e2e__/navbar.test.ts b/__e2e__/navbar.test.ts new file mode 100644 index 0000000..b34fbbc --- /dev/null +++ b/__e2e__/navbar.test.ts @@ -0,0 +1,156 @@ +import { expect, test } from "@playwright/test"; + +import IndexPage from "./pages/Index"; +import LoginPage from "./pages/login"; +import ICTSCNavbar from "./pages/navbar"; +import ProblemsPage from "./pages/problems"; +import ProfilePage from "./pages/profile"; +import RankingPage from "./pages/ranking"; +import ScoringPage from "./pages/scoring"; +import TeamInfoPage from "./pages/teamInfo"; +import UsersPage from "./pages/users"; + +test.describe("未ログイン状態", () => { + test("ログインページに遷移できる", async ({ page }) => { + // setup + await IndexPage.goto(page); + await IndexPage.validate(page); + + // when + await ICTSCNavbar.LoginLink(page).click(); + + // then + await LoginPage.waitFormSelector(page); + expect(page.url().split("/").pop()).toBe("login"); + await LoginPage.validate(page); + }); +}); + +test.describe("ログイン状態", () => { + test.beforeEach(async ({ page }) => { + await page.goto("/login"); + + await page.fill("#username", "admin"); + await page.fill("#password", "password"); + await page.click("#loginBtn"); + + // ログインに成功しましたというアラートが出るまで待つ + await page.waitForURL("/"); + // await page.waitForSelector(".alert-success"); + }); + + test("ルールページに遷移できる", async ({ page }) => { + // setup + await IndexPage.goto(page); + await IndexPage.validate(page); + + // when + await ICTSCNavbar.RuleLink(page).click(); + + // then + await IndexPage.waitFormSelector(page); + expect(page.url().split("/").pop()).toBe(""); + await IndexPage.validate(page); + }); + + test("チーム情報ページに遷移できる", async ({ page }) => { + // setup + await IndexPage.goto(page); + await IndexPage.validate(page); + + // when + await ICTSCNavbar.TeamInfoLink(page).click(); + + // then + await TeamInfoPage.waitFormSelector(page); + expect(page.url().split("/").pop()).toBe("team_info"); + await TeamInfoPage.validate(page); + }); + + test("問題ページに遷移できる", async ({ page }) => { + // setup + await IndexPage.goto(page); + await IndexPage.validate(page); + + // when + await ICTSCNavbar.ProblemsLink(page).click(); + + // then + await ProblemsPage.waitFormSelector(page); + expect(page.url().split("/").pop()).toBe("problems"); + await ProblemsPage.validate(page); + }); + + test("順位ページに遷移できる", async ({ page }) => { + // setup + await IndexPage.goto(page); + await IndexPage.validate(page); + + // when + await ICTSCNavbar.RankingLink(page).click(); + + // then + await RankingPage.waitFormSelector(page); + expect(page.url().split("/").pop()).toBe("ranking"); + await RankingPage.validate(page); + }); + + test("参加者ページに遷移できる", async ({ page }) => { + // setup + await IndexPage.goto(page); + await IndexPage.validate(page); + + // when + await ICTSCNavbar.UsersLink(page).click(); + + // then + await UsersPage.waitFormSelector(page); + expect(page.url().split("/").pop()).toBe("users"); + await UsersPage.validate(page); + }); + + test("採点ページに遷移できる", async ({ page }) => { + // setup + await IndexPage.goto(page); + await IndexPage.validate(page); + + // when + await ICTSCNavbar.ScoringLink(page).click(); + + // then + await ScoringPage.waitFormSelector(page); + expect(page.url().split("/").pop()).toBe("scoring"); + await ScoringPage.validate(page); + }); + + test("プロフィールページに遷移できる", async ({ page }) => { + // setup + await IndexPage.goto(page); + await IndexPage.validate(page); + await ICTSCNavbar.DropdownMenu(page).click(); + + // when + await ICTSCNavbar.ProfileLink(page).click(); + + // then + await ProfilePage.waitFormSelector(page); + expect(page.url().split("/").pop()).toBe("profile"); + await ProfilePage.validate(page); + }); + + test("ログアウトできる", async ({ page }) => { + // setup + // 初期は problems ページに遷移しておく indexページに設定してしまうと、同じページに遷移するためテストがうまくできない + await ProblemsPage.goto(page); + await ProblemsPage.validate(page); + await ICTSCNavbar.DropdownMenu(page).click(); + + // when + await ICTSCNavbar.LogoutButton(page).click(); + + // then + await IndexPage.waitFormSelector(page); + expect(page.url().split("/").pop()).toBe(""); + await expect(ICTSCNavbar.LoginLink(page)).toBeVisible(); + }); +}); diff --git a/__e2e__/pages/Index.ts b/__e2e__/pages/Index.ts new file mode 100644 index 0000000..8bb59ee --- /dev/null +++ b/__e2e__/pages/Index.ts @@ -0,0 +1,17 @@ +import { expect, Page } from "@playwright/test"; + +import BasePage from "./page"; + +const IndexPage: BasePage = { + goto: async (page: Page) => { + await page.goto("/"); + }, + validate: async (page: Page) => { + await expect(page.locator(".title-ictsc")).toHaveText("ルール"); + }, + waitFormSelector: async (page: Page) => { + await page.waitForSelector(".title-ictsc >> text=ルール"); + }, +}; + +export default IndexPage; diff --git a/__e2e__/pages/login.ts b/__e2e__/pages/login.ts new file mode 100644 index 0000000..5ab795c --- /dev/null +++ b/__e2e__/pages/login.ts @@ -0,0 +1,17 @@ +import { expect } from "@playwright/test"; + +import BasePage from "./page"; + +const LoginPage: BasePage = { + goto: async (page) => { + await page.goto("/login"); + }, + validate: async (page) => { + await expect(page.locator(".title-ictsc")).toHaveText("ログイン"); + }, + waitFormSelector: async (page) => { + await page.waitForSelector(".title-ictsc >> text=ログイン"); + }, +}; + +export default LoginPage; diff --git a/__e2e__/pages/navbar.ts b/__e2e__/pages/navbar.ts new file mode 100644 index 0000000..2c17663 --- /dev/null +++ b/__e2e__/pages/navbar.ts @@ -0,0 +1,16 @@ +import { Page } from "@playwright/test"; + +const ICTSCNavbar = { + RuleLink: (page: Page) => page.locator("a >> text=ルール"), + TeamInfoLink: (page: Page) => page.locator("a >> text=チーム情報"), + ProblemsLink: (page: Page) => page.locator("a >> text=問題"), + RankingLink: (page: Page) => page.locator("a >> text=順位"), + UsersLink: (page: Page) => page.locator("a >> text=参加者"), + ScoringLink: (page: Page) => page.locator("a >> text=採点"), + LoginLink: (page: Page) => page.locator("a >> text=ログイン"), + DropdownMenu: (page: Page) => page.getByText("admin", { exact: true }), + ProfileLink: (page: Page) => page.locator("a >> text=プロフィール"), + LogoutButton: (page: Page) => page.locator("button >> text=ログアウト"), +}; + +export default ICTSCNavbar; diff --git a/__e2e__/pages/page.ts b/__e2e__/pages/page.ts new file mode 100644 index 0000000..b310fad --- /dev/null +++ b/__e2e__/pages/page.ts @@ -0,0 +1,11 @@ +import { Page } from "@playwright/test"; + +type BasePage = { + goto(page: Page): Promise; + + validate(page: Page): Promise; + + waitFormSelector(page: Page): Promise; +}; + +export default BasePage; diff --git a/__e2e__/pages/problems.ts b/__e2e__/pages/problems.ts new file mode 100644 index 0000000..cf75358 --- /dev/null +++ b/__e2e__/pages/problems.ts @@ -0,0 +1,17 @@ +import { expect } from "@playwright/test"; + +import BasePage from "./page"; + +const ProblemsPage: BasePage = { + goto: async (page) => { + await page.goto("/problems"); + }, + validate: async (page) => { + await expect(page.locator(".title-ictsc")).toHaveText("問題一覧"); + }, + waitFormSelector: async (page) => { + await page.waitForSelector(".title-ictsc >> text=問題"); + }, +}; + +export default ProblemsPage; diff --git a/__e2e__/pages/profile.ts b/__e2e__/pages/profile.ts new file mode 100644 index 0000000..d5805da --- /dev/null +++ b/__e2e__/pages/profile.ts @@ -0,0 +1,17 @@ +import { expect } from "@playwright/test"; + +import BasePage from "./page"; + +const ProfilePage: BasePage = { + goto: async (page) => { + await page.goto("/profile"); + }, + validate: async (page) => { + await expect(page.locator(".title-ictsc")).toHaveText("プロフィール"); + }, + waitFormSelector: async (page) => { + await page.waitForSelector(".title-ictsc >> text=プロフィール"); + }, +}; + +export default ProfilePage; diff --git a/__e2e__/pages/ranking.ts b/__e2e__/pages/ranking.ts new file mode 100644 index 0000000..d8b4012 --- /dev/null +++ b/__e2e__/pages/ranking.ts @@ -0,0 +1,17 @@ +import { expect } from "@playwright/test"; + +import BasePage from "./page"; + +const RankingPage: BasePage = { + goto: async (page) => { + await page.goto("/ranking"); + }, + validate: async (page) => { + await expect(page.locator(".title-ictsc")).toHaveText("ランキング"); + }, + waitFormSelector: async (page) => { + await page.waitForSelector(".title-ictsc >> text=ランキング"); + }, +}; + +export default RankingPage; diff --git a/__e2e__/pages/scoring.ts b/__e2e__/pages/scoring.ts new file mode 100644 index 0000000..047285f --- /dev/null +++ b/__e2e__/pages/scoring.ts @@ -0,0 +1,17 @@ +import { expect } from "@playwright/test"; + +import BasePage from "./page"; + +const ScoringPage: BasePage = { + goto: async (page) => { + await page.goto("/scoring"); + }, + validate: async (page) => { + await expect(page.locator("th:nth-child(1)")).toHaveText("採点"); + }, + waitFormSelector: async (page) => { + await page.waitForSelector("th >> text=採点"); + }, +}; + +export default ScoringPage; diff --git a/__e2e__/pages/teamInfo.ts b/__e2e__/pages/teamInfo.ts new file mode 100644 index 0000000..7ce9c40 --- /dev/null +++ b/__e2e__/pages/teamInfo.ts @@ -0,0 +1,17 @@ +import { expect } from "@playwright/test"; + +import BasePage from "./page"; + +const TeamInfoPage: BasePage = { + goto: async (page) => { + await page.goto("/team_info"); + }, + validate: async (page) => { + await expect(page.locator(".title-ictsc")).toHaveText("チーム情報"); + }, + waitFormSelector: async (page) => { + await page.waitForSelector(".title-ictsc >> text=チーム情報"); + }, +}; + +export default TeamInfoPage; diff --git a/__e2e__/pages/users.ts b/__e2e__/pages/users.ts new file mode 100644 index 0000000..b67801a --- /dev/null +++ b/__e2e__/pages/users.ts @@ -0,0 +1,17 @@ +import { expect } from "@playwright/test"; + +import BasePage from "./page"; + +const UsersPage: BasePage = { + goto: async (page) => { + await page.goto("/users"); + }, + validate: async (page) => { + await expect(page.locator(".title-ictsc")).toHaveText("参加者一覧"); + }, + waitFormSelector: async (page) => { + await page.waitForSelector(".title-ictsc >> text=参加者"); + }, +}; + +export default UsersPage; diff --git a/__test__/components/Navbar.test.tsx b/__test__/components/Navbar.test.tsx index b6cf144..e9a3a28 100644 --- a/__test__/components/Navbar.test.tsx +++ b/__test__/components/Navbar.test.tsx @@ -106,7 +106,7 @@ describe("参加者ログイン状態 ICTSCNavBar", () => { }); // verify - expect(useAuth).toHaveBeenCalledTimes(2); + expect(useAuth).toHaveBeenCalledTimes(1); expect(logout).toHaveBeenCalledTimes(1); }); }); diff --git a/components/Navbar.tsx b/components/Navbar.tsx index 7256cf9..edabf3f 100644 --- a/components/Navbar.tsx +++ b/components/Navbar.tsx @@ -1,18 +1,20 @@ import Link from "next/link"; import { useRouter } from "next/router"; +import { mutate } from "swr"; + import useAuth from "@/hooks/auth"; function ICTSCNavBar() { const router = useRouter(); - const { user, logout, mutate } = useAuth(); + const { user, logout } = useAuth(); const handleLogout = async () => { const response = await logout(); - if (response.status === 200) { - await mutate(); + if (response.code === 200) { + await mutate(() => true, undefined, { revalidate: true }); await router.push("/"); } }; diff --git a/hooks/api.ts b/hooks/api.ts index fd99174..56f50e2 100644 --- a/hooks/api.ts +++ b/hooks/api.ts @@ -18,13 +18,13 @@ const useApi = () => { get: (url: string) => apiClient.get>(url).then((response) => response.data), post: (url: string, data?: any) => - apiClient.post(url, data).then((response) => response.data), + apiClient.post>(url, data).then((response) => response.data), put: (url: string, data?: any) => - apiClient.put(url, data).then((response) => response.data), + apiClient.put>(url, data).then((response) => response.data), patch: (url: string, data?: any) => apiClient.patch>(url, data).then((response) => response.data), delete: (url: string) => - apiClient.delete(url).then((response) => response.data), + apiClient.delete>(url).then((response) => response.data), }; return { client }; diff --git a/hooks/auth.ts b/hooks/auth.ts index ce2aea3..9a1c6de 100644 --- a/hooks/auth.ts +++ b/hooks/auth.ts @@ -9,7 +9,7 @@ const useAuth = () => { const fetcher = (url: string) => client.get(url); const { data, mutate, isLoading } = useSWR("auth/self", fetcher); - const logout = () => client.delete("auth/logout"); + const logout = async () => client.delete("auth/signout"); return { user: data?.data?.user ?? null, diff --git a/layouts/CommonLayout.tsx b/layouts/CommonLayout.tsx index da99fed..c071afd 100644 --- a/layouts/CommonLayout.tsx +++ b/layouts/CommonLayout.tsx @@ -7,13 +7,15 @@ interface Props { children: React.ReactNode; } -const CommonLayout = ({ title, children }: Props) => { +function CommonLayout({ title, children }: Props) { return ( -

{title}

+

+ {title} +

{children}
); -}; +} export default CommonLayout; diff --git a/package.json b/package.json index 0c908ae..a85a31c 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,8 @@ "build": "next build", "start": "next start", "test": "vitest --coverage", + "test:e2e": "playwright test", + "test:e2e-ui": "playwright test --ui", "lint": "next lint", "format": "prettier --check --ignore-path .gitignore .", "format:fix": "prettier --write --ignore-path .gitignore .", @@ -39,6 +41,7 @@ "zenn-markdown-html": "^0.1.81" }, "devDependencies": { + "@playwright/test": "^1.35.1", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^14.0.0", "@types/luxon": "^3.2.0", diff --git a/pages/login.tsx b/pages/login.tsx index 88db8b1..f484e69 100644 --- a/pages/login.tsx +++ b/pages/login.tsx @@ -36,9 +36,9 @@ function Login() { const response = await client.post("auth/signin", data); setSubmitting(false); - setStatus(response.status); + setStatus(response.code); - if (response.status === 200) { + if (response.code === 200) { await mutate(); await router.push("/"); } @@ -63,6 +63,7 @@ function Login() { {...register("name", { required: true })} type="text" placeholder="ユーザー名" + id="username" className="input input-bordered max-w-xs min-w-[312px]" />
@@ -76,6 +77,7 @@ function Login() { {...register("password", { required: true })} type="password" placeholder="パスワード" + id="password" className="input input-bordered max-w-xs min-w-[312px] mt-4" />
@@ -87,6 +89,7 @@ function Login() {