diff --git a/.claude/skills/e2e/SKILL.md b/.claude/skills/e2e/SKILL.md index ed87ec4c6..4afe22829 100644 --- a/.claude/skills/e2e/SKILL.md +++ b/.claude/skills/e2e/SKILL.md @@ -82,10 +82,10 @@ pnpm exec playwright show-trace test-results//trace.zip ## Test pattern reference -Every test follows this pattern — use these imports from `~/utils/playwright`, NOT raw Playwright APIs: +Every test follows this pattern — use these imports from `./helpers/playwright`, NOT raw Playwright APIs: ```typescript -import { expect, impersonate, navigate, seed, test } from "~/utils/playwright"; +import { expect, impersonate, navigate, seed, test } from "./helpers/playwright"; test.describe("Feature", () => { test("does something", async ({ page }) => { @@ -104,7 +104,7 @@ Key rules: - Use `seed(page, variation?)` to reset the database. Available variations: DEFAULT, NO_TOURNAMENT_TEAMS, REG_OPEN, SMALL_SOS, NZAP_IN_TEAM, NO_SCRIMS, NO_SQ_GROUPS - Use `impersonate(page, userId?)` to authenticate. Default is admin (ADMIN_ID) - Avoid `page.waitForTimeout` — use assertions or `waitFor` patterns instead -- Import `test` from `~/utils/playwright` (not from `@playwright/test`) — it includes worker port fixtures +- Import `test` from `./helpers/playwright` (not from `@playwright/test`) — it includes worker port fixtures ## Environment variables diff --git a/app/components/elements/Button.tsx b/app/components/elements/Button.tsx index 56fbdb9a1..1744c06bd 100644 --- a/app/components/elements/Button.tsx +++ b/app/components/elements/Button.tsx @@ -28,6 +28,7 @@ export interface SendouButtonProps shape?: "circle" | "square"; icon?: JSX.Element; children?: React.ReactNode; + testId?: string; } export function SendouButton({ @@ -37,10 +38,12 @@ export function SendouButton({ shape, className, icon, + testId, ...rest }: SendouButtonProps) { return ( diff --git a/app/components/match-page/MatchActionPickBanTab.tsx b/app/components/match-page/MatchActionPickBanTab.tsx index 32f1116ce..8ac69a0c7 100644 --- a/app/components/match-page/MatchActionPickBanTab.tsx +++ b/app/components/match-page/MatchActionPickBanTab.tsx @@ -148,6 +148,7 @@ export function MatchActionPickBanTab({ if (!selected) return; onSubmit?.({ type, map: selected }); }} + testId="pick-ban-submit-button" > {t("common:actions.submit")} @@ -294,6 +295,7 @@ function StageTile({ }} onClick={onSelect} disabled={disabled} + data-testid="pick-ban-button" /> {isSelected ? ( type === "PICK" ? ( @@ -353,6 +355,7 @@ function ModeTile({ })} onClick={onSelect} disabled={disabled} + data-testid="pick-ban-button" > diff --git a/app/components/match-page/MatchActionTab.tsx b/app/components/match-page/MatchActionTab.tsx index 1930e88f9..60719b9a5 100644 --- a/app/components/match-page/MatchActionTab.tsx +++ b/app/components/match-page/MatchActionTab.tsx @@ -134,6 +134,7 @@ export function MatchActionTab({ isOwnTeam={teams[0].id === ownTeamId} hideLabel={ownTeamId == null} className={styles.alpha} + testId="winner-radio-1" /> @@ -160,6 +162,7 @@ export function MatchActionTab({ type="checkbox" checked={isKo} onChange={(e) => setIsKo(e.target.checked)} + data-testid="ko-checkbox" /> {t("q:match.action.ko")} @@ -178,6 +181,7 @@ export function MatchActionTab({ } }} className={styles.submit} + testId="report-score-button" > {t("common:actions.submit")} @@ -248,6 +252,7 @@ function SetEndingConfirmation({ variant="primary" isDisabled={isSubmitting} onPress={onConfirm} + testId="confirm-set-end-button" > {t("common:actions.confirm")} @@ -264,11 +269,13 @@ function TeamRadioOption({ isOwnTeam, hideLabel, className, + testId, }: { team: ActionTabTeam; isOwnTeam: boolean; hideLabel?: boolean; className?: string; + testId?: string; }) { const { t } = useTranslation(["q"]); @@ -279,6 +286,7 @@ function TeamRadioOption({ value={String(team.id)} aria-label={team.name} className={clsx(styles.teamRadioContainer, className)} + data-testid={testId} > {({ isSelected, isFocusVisible }) => (
@@ -73,6 +74,7 @@ interface IconBannerProps { subtitle?: string; screenLegal?: boolean; topRight?: React.ReactNode; + testId?: string; } export function IconBanner({ @@ -81,9 +83,10 @@ export function IconBanner({ subtitle, screenLegal, topRight, + testId, }: IconBannerProps) { return ( -
+
{icon}
{header}
{subtitle ? ( @@ -109,7 +112,11 @@ function ScreenNotice({ screenLegal }: { screenLegal: boolean }) { return ( + ) { if (allSameMode) { return (
-
+
×{games.length}
@@ -49,11 +52,19 @@ function ModeProgress({ games }: Pick) {
{games.map((game, i) => game.mode ? ( -
+
) : ( -
+
), diff --git a/app/components/match-page/MatchBannerTopRow.tsx b/app/components/match-page/MatchBannerTopRow.tsx index 3d33921b9..ac7c4db43 100644 --- a/app/components/match-page/MatchBannerTopRow.tsx +++ b/app/components/match-page/MatchBannerTopRow.tsx @@ -33,7 +33,10 @@ function Score({ score }: { score: MatchBannerTopRowProps["score"] }) {
{score.alpha}-{score.bravo}
-
+
{score.isFinal ? t("q:match.banner.final") : score.bestOf @@ -73,7 +76,7 @@ function Timer({ : minuteFormatter.format(minutes); return ( -
+
diff --git a/app/components/match-page/MatchJoinTab.tsx b/app/components/match-page/MatchJoinTab.tsx index 4d0d984bf..c6c1ad01e 100644 --- a/app/components/match-page/MatchJoinTab.tsx +++ b/app/components/match-page/MatchJoinTab.tsx @@ -86,7 +86,11 @@ export function MatchJoinTab({ ) : null} - +
@@ -122,11 +126,21 @@ function StaleRoomPrompt({ ); } -function InfoWithHeader({ header, value }: { header: string; value: string }) { +function InfoWithHeader({ + header, + value, + testId, +}: { + header: string; + value: string; + testId?: string; +}) { return (
{header}
-
{value}
+
+ {value} +
); } diff --git a/app/components/match-page/MatchRosterTab.tsx b/app/components/match-page/MatchRosterTab.tsx index bd47d9d59..956f55fb9 100644 --- a/app/components/match-page/MatchRosterTab.tsx +++ b/app/components/match-page/MatchRosterTab.tsx @@ -138,13 +138,14 @@ function TeamRoster({ {team.members.length > 0 ? (
    {isEditing - ? team.members.map((member) => ( + ? team.members.map((member, index) => (