Create your Link in Bio for Bluesky

e2eテストをブラウザごとに別のアカウントで実行

+83 -23
+6 -1
app/features/board/card/profile-card.tsx
··· 34 34 export function ProfileCard({ user, button }: ProfileCardProps) { 35 35 const buttons = { 36 36 edit: ( 37 - <Link className="btn btn-primary" to={`/edit?base=${user.handle}`}> 37 + <Link 38 + className="btn btn-primary" 39 + to={`/edit?base=${user.handle}`} 40 + data-testid="profile-card__edit" 41 + > 38 42 <PencilSquareIcon className="size-6" /> 39 43 編集 40 44 </Link> ··· 43 47 <Link 44 48 className="btn btn-primary animate-bounce repeat-0" 45 49 to={`/board/${user.handle}`} 50 + data-testid="profile-card__preview" 46 51 > 47 52 <EyeIcon className="size-6" /> 48 53 ページを見る
+4
app/root.tsx
··· 14 14 import { userAtom } from "./atoms/user/base"; 15 15 import { resumeSessionAtom } from "./atoms/user/write-only"; 16 16 import { Toaster } from "./features/toast/toaster"; 17 + import { createLogger } from "./utils/logger"; 17 18 18 19 export { ErrorBoundary } from "~/components/error-boundary"; 19 20 export { HydrateFallback } from "~/components/hydate-fallback"; 20 21 21 22 const LOGIN_REQUIRED_PATHS = ["/edit"]; 22 23 24 + const logger = createLogger("root.tsx"); 25 + 23 26 export const clientLoader: ClientLoaderFunction = async ({ request }) => { 24 27 const store = getDefaultStore(); 25 28 await store.set(resumeSessionAtom); 26 29 const user = store.get(userAtom); 27 30 const pathname = new URL(request.url).pathname; 28 31 if (!user && LOGIN_REQUIRED_PATHS.includes(pathname)) { 32 + logger.debug("未ログインのためリダイレクトしました"); 29 33 return redirect("/"); 30 34 } 31 35 return null;
+15 -1
app/routes/_index.tsx
··· 1 + import { Link } from "@remix-run/react"; 2 + 3 + import { useUser } from "~/atoms/user/hooks"; 1 4 import { LoginForm } from "~/features/login/login-form"; 2 5 3 6 export default function Index() { 7 + const user = useUser(); 4 8 return ( 5 9 <div className="utils--center"> 6 - <LoginForm /> 10 + {user ? ( 11 + <Link 12 + className="btn btn-primary" 13 + to={`/edit?base=${user.profile.handle}`} 14 + data-testid="index__edit-link" 15 + > 16 + 編集ページへ 17 + </Link> 18 + ) : ( 19 + <LoginForm /> 20 + )} 7 21 </div> 8 22 ); 9 23 }
+9
e2e/edit-redirect.spec.ts
··· 1 1 import { test } from "@playwright/test"; 2 2 3 + import { resetStorageState } from "./utils"; 4 + 3 5 const DUMMY_EXPIRED_USER = JSON.stringify({ 4 6 profile: {}, 5 7 session: { ··· 14 16 service: "http://localhost:2583/", 15 17 }); 16 18 19 + resetStorageState(); 20 + 17 21 test.describe("編集(リダイレクト)", () => { 18 22 test("非ログイン時はトップにリダイレクト", async ({ page }) => { 23 + await page.goto("/edit?base=alice.test"); 24 + await page.waitForURL((url) => url.pathname === "/"); 25 + await page.waitForTimeout(2000); 26 + }); 27 + test("baseパラメータが無いときはトップにリダイレクト", async ({ page }) => { 19 28 await page.goto("/edit"); 20 29 await page.waitForURL((url) => url.pathname === "/"); 21 30 await page.waitForTimeout(2000);
+8 -10
e2e/edit.spec.ts
··· 1 1 import { expect, test } from "@playwright/test"; 2 2 3 - import { restoreStorageState } from "./utils"; 4 - 5 - restoreStorageState(); 6 - 7 3 test.describe("編集", () => { 8 4 test("カードを追加して保存すると閲覧ページに反映される", async ({ page }) => { 5 + // セットアップ 9 6 const text1 = `1. ${crypto.randomUUID()}`; 10 7 const text2 = `2. ${crypto.randomUUID()}`; 11 8 const text1Edited = `1(edit). ${crypto.randomUUID()}`; ··· 18 15 const card1Edited = page.locator('[data-testid="sortable-card"]', { 19 16 hasText: text1Edited, 20 17 }); 21 - await page.goto("/edit?base=alice.test"); 18 + await page.goto("/"); 19 + await page.getByTestId("index__edit-link").click(); 22 20 23 21 // カードを追加 24 22 await page.getByTestId("card-form-modal__button").click(); ··· 39 37 await page.waitForTimeout(1000); 40 38 41 39 // 閲覧ページで順番を確認 42 - await page.goto("/board/alice.test"); 40 + await page.getByTestId("profile-card__preview").click(); 43 41 await expect(card1).toBeVisible(); 44 42 await expect(card2).toBeVisible(); 45 43 const allCards = await page.getByTestId("sortable-card").allTextContents(); 46 44 expect(allCards.indexOf(text1)).toBeLessThan(allCards.indexOf(text2)); 47 45 48 46 // カードを並べ替える 49 - await page.goto("/edit?base=alice.test"); 47 + await page.getByTestId("profile-card__edit").click(); 50 48 await card1.dragTo(card2); 51 49 52 50 // 保存ボタン押下、Firehose反映待ち ··· 54 52 await page.waitForTimeout(1000); 55 53 56 54 // 閲覧ページで順番を確認 57 - await page.goto("/board/alice.test"); 55 + await page.getByTestId("profile-card__preview").click(); 58 56 await expect(card1).toBeVisible(); 59 57 await expect(card2).toBeVisible(); 60 58 const sorted = await page.getByTestId("sortable-card").allTextContents(); 61 59 expect(sorted.indexOf(text1)).toBeGreaterThan(sorted.indexOf(text2)); 62 60 63 61 // カードを編集 64 - await page.goto("/edit?base=alice.test"); 62 + await page.getByTestId("profile-card__edit").click(); 65 63 await card1.getByTestId("sortable-card__edit").click(); 66 64 await page.getByTestId("card-form__text").fill(text1Edited); 67 65 await page.getByTestId("card-form__url").fill("https://example.com"); ··· 72 70 await page.waitForTimeout(1000); 73 71 74 72 // 閲覧ページで編集済みを確認 75 - await page.goto("/board/alice.test"); 73 + await page.getByTestId("profile-card__preview").click(); 76 74 await expect(card1).not.toBeVisible(); 77 75 await expect(card1Edited).toBeVisible(); 78 76 await expect(card2).toBeVisible();
+34 -8
e2e/global.setup.ts
··· 1 + import type { Page } from "@playwright/test"; 1 2 import { test } from "@playwright/test"; 3 + 4 + const login = async ({ 5 + page, 6 + identifier, 7 + password, 8 + }: { 9 + page: Page; 10 + identifier: string; 11 + password: string; 12 + }) => { 13 + await page.goto("/"); 14 + await page.getByTestId("login-form__service").fill("http://localhost:2583"); 15 + await page.getByTestId("login-form__identifier").fill(identifier); 16 + await page.getByTestId("login-form__password").fill(password); 17 + await page.getByTestId("login-form__submit").click(); 18 + await page.waitForURL((url) => url.pathname === "/edit"); 19 + await page 20 + .context() 21 + .storageState({ path: `./e2e/states/${identifier}.json` }); 22 + }; 2 23 3 24 test.describe("ログイン", () => { 4 - test("テストアカウントでログインできる", async ({ page }) => { 5 - await page.goto("/"); 6 - await page.getByTestId("login-form__service").fill("http://localhost:2583"); 7 - await page.getByTestId("login-form__identifier").fill("alice.test"); 8 - await page.getByTestId("login-form__password").fill("hunter2"); 9 - await page.getByTestId("login-form__submit").click(); 10 - await page.waitForURL((url) => url.pathname === "/edit"); 11 - await page.context().storageState({ path: "./e2e/state.json" }); 25 + test("テストアカウントでログインできる(chromium)", async ({ page }) => { 26 + await login({ 27 + page, 28 + identifier: "alice.test", 29 + password: "hunter2", 30 + }); 31 + }); 32 + test("テストアカウントでログインできる(safari)", async ({ page }) => { 33 + await login({ 34 + page, 35 + identifier: "bob.test", 36 + password: "hunter2", 37 + }); 12 38 }); 13 39 });
+2
e2e/states/.gitignore
··· 1 + * 2 + !.gitignore
+2 -2
e2e/utils.ts
··· 1 1 import { test } from "@playwright/test"; 2 2 3 - export const restoreStorageState = () => { 3 + export const resetStorageState = () => { 4 4 test.use({ 5 - storageState: "./e2e/state.json", 5 + storageState: { cookies: [], origins: [] }, 6 6 }); 7 7 };
+3 -1
playwright.config.ts
··· 26 26 use: { 27 27 ...devices["Desktop Chrome"], 28 28 locale: "ja-JP", 29 + storageState: "./e2e/states/alice.test.json", 29 30 }, 30 31 dependencies: ["setup"], 31 32 }, ··· 34 35 use: { 35 36 ...devices["iPhone 15 Pro"], 36 37 locale: "ja-JP", 38 + storageState: "./e2e/states/bob.test.json", 37 39 }, 38 - dependencies: ["chromium"], 40 + dependencies: ["setup"], 39 41 }, 40 42 ], 41 43 webServer: {