mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-03-21 18:04:39 -05:00
Tournament: Add subs to roster mid-tournament
This commit is contained in:
parent
ad1b1fdf84
commit
337f97e6c6
|
|
@ -4,7 +4,7 @@ export function Divider({
|
|||
children,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return <div className={clsx("divider", className)}>{children}</div>;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Popover as HeadlessPopover } from "@headlessui/react";
|
||||
import type { Placement } from "@popperjs/core";
|
||||
import clsx from "clsx";
|
||||
import * as React from "react";
|
||||
import { usePopper } from "react-popper";
|
||||
|
||||
|
|
@ -8,13 +9,17 @@ export function Popover({
|
|||
children,
|
||||
buttonChildren,
|
||||
triggerClassName,
|
||||
triggerTestId,
|
||||
containerClassName,
|
||||
contentClassName,
|
||||
placement,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
buttonChildren: React.ReactNode;
|
||||
triggerClassName?: string;
|
||||
triggerTestId?: string;
|
||||
containerClassName?: string;
|
||||
contentClassName?: string;
|
||||
placement?: Placement;
|
||||
}) {
|
||||
const [referenceElement, setReferenceElement] = React.useState();
|
||||
|
|
@ -37,6 +42,7 @@ export function Popover({
|
|||
// @ts-expect-error Popper docs: https://popper.js.org/react-popper/v2/
|
||||
ref={setReferenceElement}
|
||||
className={triggerClassName ?? "minimal tiny"}
|
||||
data-testid={triggerTestId}
|
||||
>
|
||||
{buttonChildren}
|
||||
</HeadlessPopover.Button>
|
||||
|
|
@ -44,7 +50,7 @@ export function Popover({
|
|||
<HeadlessPopover.Panel
|
||||
// @ts-expect-error Popper docs: https://popper.js.org/react-popper/v2/
|
||||
ref={setPopperElement}
|
||||
className="popover-content"
|
||||
className={clsx("popover-content", contentClassName)}
|
||||
style={styles["popper"]}
|
||||
{...attributes["popper"]}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,9 @@ import hasTournamentStarted from "../../tournament/queries/hasTournamentStarted.
|
|||
import { findByIdentifier } from "../../tournament/queries/findByIdentifier.server";
|
||||
import { notFoundIfFalsy, validate } from "~/utils/remix";
|
||||
import {
|
||||
SENDOU_INK_BASE_URL,
|
||||
tournamentBracketsSubscribePage,
|
||||
tournamentJoinPage,
|
||||
tournamentMatchPage,
|
||||
tournamentTeamPage,
|
||||
userPage,
|
||||
|
|
@ -60,6 +62,8 @@ import { removeDuplicates } from "~/utils/arrays";
|
|||
import { Flag } from "~/components/Flag";
|
||||
import { databaseTimestampToDate } from "~/utils/dates";
|
||||
import { Popover } from "~/components/Popover";
|
||||
import { useCopyToClipboard } from "react-use";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
|
||||
export const links: LinksFunction = () => {
|
||||
return [
|
||||
|
|
@ -324,6 +328,16 @@ export default function TournamentBracketsPage() {
|
|||
{parentRouteData.hasStarted && myTeam ? (
|
||||
<TournamentProgressPrompt ownedTeamId={myTeam.id} />
|
||||
) : null}
|
||||
{/* TODO: also hide this if out of the tournament */}
|
||||
{!data.finalStandings &&
|
||||
myTeam &&
|
||||
parentRouteData.hasStarted &&
|
||||
parentRouteData.ownTeam ? (
|
||||
<AddSubsPopOver
|
||||
members={myTeam.members}
|
||||
inviteCode={parentRouteData.ownTeam.inviteCode}
|
||||
/>
|
||||
) : null}
|
||||
{data.finalStandings ? (
|
||||
<FinalStandings standings={data.finalStandings} />
|
||||
) : null}
|
||||
|
|
@ -473,6 +487,55 @@ function TournamentProgressPrompt({ ownedTeamId }: { ownedTeamId: number }) {
|
|||
);
|
||||
}
|
||||
|
||||
function AddSubsPopOver({
|
||||
members,
|
||||
inviteCode,
|
||||
}: {
|
||||
members: unknown[];
|
||||
inviteCode: string;
|
||||
}) {
|
||||
const parentRouteData = useOutletContext<TournamentLoaderData>();
|
||||
const { t } = useTranslation(["common", "tournament"]);
|
||||
const [, copyToClipboard] = useCopyToClipboard();
|
||||
|
||||
const subsAvailableToAdd =
|
||||
TOURNAMENT.TEAM_MAX_MEMBERS_BEFORE_START + 1 - members.length;
|
||||
|
||||
const inviteLink = `${SENDOU_INK_BASE_URL}${tournamentJoinPage({
|
||||
eventId: parentRouteData.event.id,
|
||||
inviteCode,
|
||||
})}`;
|
||||
|
||||
return (
|
||||
<Popover
|
||||
buttonChildren={<>{t("tournament:actions.addSub")}</>}
|
||||
triggerClassName="tiny outlined ml-auto"
|
||||
triggerTestId="add-sub-button"
|
||||
containerClassName="mt-4"
|
||||
contentClassName="text-xs"
|
||||
>
|
||||
{t("tournament:actions.sub.prompt", { count: subsAvailableToAdd })}
|
||||
{subsAvailableToAdd > 0 ? (
|
||||
<>
|
||||
<Divider className="my-2" />
|
||||
<div>{t("tournament:actions.shareLink", { inviteLink })}</div>
|
||||
<div className="my-2 flex justify-center">
|
||||
<Button
|
||||
size="tiny"
|
||||
onClick={() => copyToClipboard(inviteLink)}
|
||||
variant="minimal"
|
||||
className="tiny"
|
||||
testId="copy-invite-link-button"
|
||||
>
|
||||
{t("common:actions.copyToClipboard")}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
function FinalStandings({ standings }: { standings: FinalStanding[] }) {
|
||||
const parentRouteData = useOutletContext<TournamentLoaderData>();
|
||||
const [viewAll, setViewAll] = React.useState(false);
|
||||
|
|
|
|||
|
|
@ -429,7 +429,7 @@ function MapListSection({ teams }: { teams: [id: number, id: number] }) {
|
|||
canReportTournamentScore({
|
||||
event: parentRouteData.event,
|
||||
match: data.match,
|
||||
ownedTeamId: parentRouteData.ownedTeamId,
|
||||
ownedTeamId: parentRouteData.ownTeam?.id,
|
||||
user,
|
||||
})
|
||||
? "EDIT"
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export interface FindTeamsByTournamentIdItem {
|
|||
id: TournamentTeam["id"];
|
||||
name: TournamentTeam["name"];
|
||||
seed: TournamentTeam["seed"];
|
||||
checkedInAt: TournamentTeamCheckIn["checkedInAt"];
|
||||
checkedInAt: TournamentTeamCheckIn["checkedInAt"] | null;
|
||||
prefersNotToHost: TournamentTeam["prefersNotToHost"];
|
||||
members: Array<
|
||||
Pick<TournamentTeamMember, "userId" | "isOwner"> &
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ const deleteMemberStm = sql.prepare(/*sql*/ `
|
|||
and "userId" = @userId
|
||||
`);
|
||||
|
||||
// TODO: divide this to different queries and compose in route
|
||||
// TODO: if captain leaves don't delete but give captain to someone else
|
||||
export const joinTeam = sql.transaction(
|
||||
({
|
||||
|
|
|
|||
|
|
@ -111,19 +111,31 @@ export const action: ActionFunction = async ({ request, params }) => {
|
|||
});
|
||||
break;
|
||||
}
|
||||
// TODO: could also handle the case of admin trying
|
||||
// to add members from a checked in team
|
||||
case "ADD_MEMBER": {
|
||||
const team = teams.find((t) => t.id === data.teamId);
|
||||
validate(team, "Invalid team id");
|
||||
validate(
|
||||
!teams.some((t) =>
|
||||
t.members.some((m) => m.userId === data["user[value]"])
|
||||
),
|
||||
"User is already on a team"
|
||||
|
||||
const previousTeam = teams.find((t) =>
|
||||
t.members.some((m) => m.userId === data["user[value]"])
|
||||
);
|
||||
|
||||
if (hasTournamentStarted(event.id)) {
|
||||
validate(
|
||||
!previousTeam || !previousTeam.checkedInAt,
|
||||
"User is already on a checked in team"
|
||||
);
|
||||
} else {
|
||||
validate(!previousTeam, "User is already on a team");
|
||||
}
|
||||
|
||||
joinTeam({
|
||||
userId: data["user[value]"],
|
||||
newTeamId: team.id,
|
||||
previousTeamId: previousTeam?.id,
|
||||
// this team is not checked in so we can simply delete it
|
||||
whatToDoWithPreviousTeam: previousTeam ? "DELETE" : undefined,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,8 +19,13 @@ import React from "react";
|
|||
import { discordFullName } from "~/utils/strings";
|
||||
import { joinSchema } from "../tournament-schemas.server";
|
||||
import { giveTrust } from "../queries/giveTrust.server";
|
||||
import {
|
||||
tournamentIdFromParams,
|
||||
tournamentTeamMaxSize,
|
||||
} from "../tournament-utils";
|
||||
|
||||
export const action: ActionFunction = async ({ request }) => {
|
||||
export const action: ActionFunction = async ({ request, params }) => {
|
||||
const tournamentId = tournamentIdFromParams(params);
|
||||
const user = await requireUserId(request);
|
||||
const url = new URL(request.url);
|
||||
const inviteCode = url.searchParams.get("code");
|
||||
|
|
@ -30,20 +35,28 @@ export const action: ActionFunction = async ({ request }) => {
|
|||
const leanTeam = notFoundIfFalsy(findByInviteCode(inviteCode));
|
||||
const teams = findTeamsByTournamentId(leanTeam.tournamentId);
|
||||
|
||||
validate(
|
||||
!hasTournamentStarted(leanTeam.tournamentId),
|
||||
"Tournament has started"
|
||||
);
|
||||
|
||||
const teamToJoin = teams.find((team) => team.id === leanTeam.id);
|
||||
const previousTeam = teams.find((team) =>
|
||||
team.members.some((member) => member.userId === user.id)
|
||||
);
|
||||
|
||||
const tournamentHasStarted = hasTournamentStarted(tournamentId);
|
||||
|
||||
if (tournamentHasStarted) {
|
||||
validate(
|
||||
!previousTeam || !previousTeam.checkedInAt,
|
||||
"Can't leave checked in team mid tournament"
|
||||
);
|
||||
}
|
||||
validate(teamToJoin, "Not team of this tournament");
|
||||
validate(
|
||||
validateCanJoin({ inviteCode, teamToJoin, userId: user.id }) === "VALID",
|
||||
"Invite code is invalid"
|
||||
validateCanJoin({
|
||||
inviteCode,
|
||||
teamToJoin,
|
||||
userId: user.id,
|
||||
tournamentHasStarted,
|
||||
}) === "VALID",
|
||||
"Cannot join this team or invite code is invalid"
|
||||
);
|
||||
|
||||
const whatToDoWithPreviousTeam = !previousTeam
|
||||
|
|
@ -80,13 +93,15 @@ export const action: ActionFunction = async ({ request }) => {
|
|||
return redirect(tournamentPage(leanTeam.tournamentId));
|
||||
};
|
||||
|
||||
export const loader = ({ request }: LoaderArgs) => {
|
||||
export const loader = ({ request, params }: LoaderArgs) => {
|
||||
const tournamentId = tournamentIdFromParams(params);
|
||||
const url = new URL(request.url);
|
||||
const inviteCode = url.searchParams.get("code");
|
||||
|
||||
return {
|
||||
teamId: inviteCode ? findByInviteCode(inviteCode)?.id : null,
|
||||
inviteCode,
|
||||
tournamentHasStarted: hasTournamentStarted(tournamentId),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -104,6 +119,7 @@ export default function JoinTeamPage() {
|
|||
inviteCode: data.inviteCode,
|
||||
teamToJoin,
|
||||
userId: user?.id,
|
||||
tournamentHasStarted: data.tournamentHasStarted,
|
||||
});
|
||||
|
||||
const textPrompt = () => {
|
||||
|
|
@ -162,10 +178,12 @@ function validateCanJoin({
|
|||
inviteCode,
|
||||
teamToJoin,
|
||||
userId,
|
||||
tournamentHasStarted,
|
||||
}: {
|
||||
inviteCode?: string | null;
|
||||
teamToJoin?: TournamentLoaderTeam;
|
||||
userId?: number;
|
||||
tournamentHasStarted: boolean;
|
||||
}) {
|
||||
if (typeof inviteCode !== "string") {
|
||||
return "MISSING_CODE";
|
||||
|
|
@ -179,7 +197,9 @@ function validateCanJoin({
|
|||
if (!teamToJoin) {
|
||||
return "NO_TEAM_MATCHING_CODE";
|
||||
}
|
||||
if (teamToJoin.members.length >= TOURNAMENT.TEAM_MAX_MEMBERS) {
|
||||
if (
|
||||
teamToJoin.members.length >= tournamentTeamMaxSize(tournamentHasStarted)
|
||||
) {
|
||||
return "TEAM_FULL";
|
||||
}
|
||||
if (teamToJoin.members.some((member) => member.userId === userId)) {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import {
|
|||
redirect,
|
||||
type ActionFunction,
|
||||
type LoaderArgs,
|
||||
type SerializeFrom,
|
||||
} from "@remix-run/node";
|
||||
import { useFetcher, useLoaderData, useOutletContext } from "@remix-run/react";
|
||||
import clsx from "clsx";
|
||||
|
|
@ -232,7 +231,6 @@ export const loader = async ({ request, params }: LoaderArgs) => {
|
|||
if (!ownTeam) return null;
|
||||
|
||||
return {
|
||||
ownTeam,
|
||||
mapPool: findMapPoolByTeamId(ownTeam.id),
|
||||
trustedPlayers: findTrustedPlayers({
|
||||
userId: user.id,
|
||||
|
|
@ -245,7 +243,6 @@ export default function TournamentRegisterPage() {
|
|||
const isMounted = useIsMounted();
|
||||
const { i18n } = useTranslation();
|
||||
const user = useUser();
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const parentRouteData = useOutletContext<TournamentLoaderData>();
|
||||
|
||||
const teamRegularMemberOf = parentRouteData.teams.find((team) =>
|
||||
|
|
@ -297,7 +294,7 @@ export default function TournamentRegisterPage() {
|
|||
{teamRegularMemberOf ? (
|
||||
<Alert>You are in a team for this event</Alert>
|
||||
) : (
|
||||
<RegistrationForms ownTeam={data?.ownTeam} />
|
||||
<RegistrationForms ownTeam={parentRouteData?.ownTeam} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -316,7 +313,7 @@ function PleaseLogIn() {
|
|||
function RegistrationForms({
|
||||
ownTeam,
|
||||
}: {
|
||||
ownTeam?: NonNullable<SerializeFrom<typeof loader>>["ownTeam"];
|
||||
ownTeam?: TournamentLoaderData["ownTeam"];
|
||||
}) {
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const user = useUser();
|
||||
|
|
@ -600,13 +597,13 @@ function TeamInfo({
|
|||
function FillRoster({
|
||||
ownTeam,
|
||||
}: {
|
||||
ownTeam: NonNullable<SerializeFrom<typeof loader>>["ownTeam"];
|
||||
ownTeam: NonNullable<TournamentLoaderData["ownTeam"]>;
|
||||
}) {
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const user = useUser();
|
||||
const parentRouteData = useOutletContext<TournamentLoaderData>();
|
||||
const [, copyToClipboard] = useCopyToClipboard();
|
||||
const { t } = useTranslation(["common"]);
|
||||
const { t } = useTranslation(["common", "tournament"]);
|
||||
|
||||
const inviteLink = `${SENDOU_INK_BASE_URL}${tournamentJoinPage({
|
||||
eventId: parentRouteData.event.id,
|
||||
|
|
@ -626,7 +623,9 @@ function FillRoster({
|
|||
);
|
||||
|
||||
const optionalMembers = Math.max(
|
||||
TOURNAMENT.TEAM_MAX_MEMBERS - ownTeamMembers.length - missingMembers,
|
||||
TOURNAMENT.TEAM_MAX_MEMBERS_BEFORE_START -
|
||||
ownTeamMembers.length -
|
||||
missingMembers,
|
||||
0
|
||||
);
|
||||
|
||||
|
|
@ -643,7 +642,8 @@ function FillRoster({
|
|||
});
|
||||
})();
|
||||
|
||||
const teamIsFull = ownTeamMembers.length >= TOURNAMENT.TEAM_MAX_MEMBERS;
|
||||
const teamIsFull =
|
||||
ownTeamMembers.length >= TOURNAMENT.TEAM_MAX_MEMBERS_BEFORE_START;
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
|
@ -658,7 +658,7 @@ function FillRoster({
|
|||
{!teamIsFull ? (
|
||||
<div className="stack md items-center">
|
||||
<div className="text-center text-sm">
|
||||
Share your invite link to add members: {inviteLink}
|
||||
{t("tournament:actions.shareLink", { inviteLink })}
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
|
|
@ -708,7 +708,8 @@ function FillRoster({
|
|||
</section>
|
||||
<div className="tournament__section__warning">
|
||||
At least {TOURNAMENT.TEAM_MIN_MEMBERS_FOR_FULL} members are required to
|
||||
participate. Max roster size is {TOURNAMENT.TEAM_MAX_MEMBERS}.
|
||||
participate. Max roster size is{" "}
|
||||
{TOURNAMENT.TEAM_MAX_MEMBERS_BEFORE_START}.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { findTeamsByTournamentId } from "../queries/findTeamsByTournamentId.serv
|
|||
import hasTournamentStarted from "../queries/hasTournamentStarted.server";
|
||||
import { teamHasCheckedIn, tournamentIdFromParams } from "../tournament-utils";
|
||||
import styles from "../tournament.css";
|
||||
import { findOwnTeam } from "../queries/findOwnTeam.server";
|
||||
|
||||
export const shouldRevalidate: ShouldRevalidateFunction = (args) => {
|
||||
const wasMutation = args.formMethod === "post";
|
||||
|
|
@ -74,9 +75,6 @@ export const loader = async ({ params, request }: LoaderArgs) => {
|
|||
teams = teams.filter(teamHasCheckedIn);
|
||||
}
|
||||
|
||||
const ownedTeamId = teams.find((team) =>
|
||||
team.members.some((member) => member.userId === user?.id && member.isOwner)
|
||||
)?.id;
|
||||
const teamMemberOfName = teams.find((team) =>
|
||||
team.members.some((member) => member.userId === user?.id)
|
||||
)?.name;
|
||||
|
|
@ -86,7 +84,12 @@ export const loader = async ({ params, request }: LoaderArgs) => {
|
|||
tieBreakerMapPool: db.calendarEvents.findTieBreakerMapPoolByEventId(
|
||||
event.eventId
|
||||
),
|
||||
ownedTeamId,
|
||||
ownTeam: user
|
||||
? findOwnTeam({
|
||||
tournamentId,
|
||||
userId: user.id,
|
||||
})
|
||||
: null,
|
||||
teamMemberOfName,
|
||||
teams,
|
||||
hasStarted,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ export const TOURNAMENT = {
|
|||
COUNTERPICK_MAX_STAGE_REPEAT: 2,
|
||||
COUNTERPICK_ONE_MODE_TOURNAMENT_MAPS_PER_MODE: 6,
|
||||
TEAM_MIN_MEMBERS_FOR_FULL: 4,
|
||||
TEAM_MAX_MEMBERS: 6,
|
||||
TEAM_MAX_MEMBERS_BEFORE_START: 6,
|
||||
AVAILABLE_BEST_OF: [3, 5, 7] as const,
|
||||
ENOUGH_TEAMS_TO_START: 2,
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -138,3 +138,10 @@ export function tournamentRoundI18nKey(round: PlayedSet["round"]) {
|
|||
|
||||
return `bracket.${round.type}` as const;
|
||||
}
|
||||
|
||||
export function tournamentTeamMaxSize(tournamentHasStarted: boolean) {
|
||||
// ensuring every team can add at least one sub while the tournament is ongoing
|
||||
return (
|
||||
TOURNAMENT.TEAM_MAX_MEMBERS_BEFORE_START + Number(tournamentHasStarted)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -170,6 +170,10 @@
|
|||
margin-block-end: var(--s-1);
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-block-end: var(--s-4);
|
||||
}
|
||||
|
||||
.ml-auto {
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
|
@ -190,6 +194,10 @@
|
|||
margin-inline: var(--s-2);
|
||||
}
|
||||
|
||||
.my-2 {
|
||||
margin-block: var(--s-2);
|
||||
}
|
||||
|
||||
.my-4 {
|
||||
margin-block: var(--s-4);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,15 @@
|
|||
import { type Page, test, expect } from "@playwright/test";
|
||||
import { NZAP_TEST_ID } from "~/db/seed/constants";
|
||||
import { impersonate, navigate, seed } from "~/utils/playwright";
|
||||
import { impersonate, navigate, seed, submit } from "~/utils/playwright";
|
||||
import { tournamentBracketsPage } from "~/utils/urls";
|
||||
|
||||
const TOURNAMENT_ID = 2;
|
||||
|
||||
const startBracket = async (page: Page) => {
|
||||
const startBracket = async (page: Page, tournamentId = 2) => {
|
||||
await seed(page);
|
||||
await impersonate(page);
|
||||
|
||||
await navigate({
|
||||
page,
|
||||
url: tournamentBracketsPage(TOURNAMENT_ID),
|
||||
url: tournamentBracketsPage(tournamentId),
|
||||
});
|
||||
|
||||
await page.getByTestId("finalize-bracket-button").click();
|
||||
|
|
@ -63,24 +61,25 @@ const expectScore = (page: Page, score: [number, number]) =>
|
|||
// 7) As N-ZAP, undo all scores and switch to different team sweeping
|
||||
test.describe("Tournament bracket", () => {
|
||||
test("reports score and sees bracket update", async ({ page }) => {
|
||||
const tournamentId = 2;
|
||||
await startBracket(page);
|
||||
|
||||
await impersonate(page, NZAP_TEST_ID);
|
||||
await navigate({
|
||||
page,
|
||||
url: tournamentBracketsPage(TOURNAMENT_ID),
|
||||
url: tournamentBracketsPage(tournamentId),
|
||||
});
|
||||
|
||||
// 1)
|
||||
await page.locator('[data-match-id="5"]').click();
|
||||
await reportResult(page, 2);
|
||||
await reportResult(page, tournamentId);
|
||||
await backToBracket(page);
|
||||
|
||||
// 2)
|
||||
await impersonate(page);
|
||||
await navigate({
|
||||
page,
|
||||
url: tournamentBracketsPage(TOURNAMENT_ID),
|
||||
url: tournamentBracketsPage(tournamentId),
|
||||
});
|
||||
await page.locator('[data-match-id="6"]').click();
|
||||
await reportResult(page, 2);
|
||||
|
|
@ -112,7 +111,7 @@ test.describe("Tournament bracket", () => {
|
|||
await impersonate(page, 2);
|
||||
await navigate({
|
||||
page,
|
||||
url: tournamentBracketsPage(TOURNAMENT_ID),
|
||||
url: tournamentBracketsPage(tournamentId),
|
||||
});
|
||||
await page.locator('[data-match-id="5"]').click();
|
||||
await page.getByTestId("undo-score-button").click();
|
||||
|
|
@ -123,4 +122,38 @@ test.describe("Tournament bracket", () => {
|
|||
page.locator("[data-round-id='5'] [data-participant-id='102']")
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("adds a sub mid tournament (from non checked in team)", async ({
|
||||
page,
|
||||
}) => {
|
||||
const tournamentId = 1;
|
||||
await startBracket(page, tournamentId);
|
||||
|
||||
// captain of the first team
|
||||
await impersonate(page, 5);
|
||||
await navigate({
|
||||
page,
|
||||
url: tournamentBracketsPage(tournamentId),
|
||||
});
|
||||
|
||||
await page.getByTestId("add-sub-button").click();
|
||||
await page.getByTestId("copy-invite-link-button").click();
|
||||
|
||||
const inviteLinkProd: string = await page.evaluate(
|
||||
"navigator.clipboard.readText()"
|
||||
);
|
||||
const inviteLink = inviteLinkProd.replace(
|
||||
"https://sendou.ink",
|
||||
"http://localhost:5800"
|
||||
);
|
||||
|
||||
await impersonate(page, NZAP_TEST_ID);
|
||||
await navigate({
|
||||
page,
|
||||
url: inviteLink,
|
||||
});
|
||||
|
||||
await submit(page);
|
||||
await expect(page).toHaveURL(/brackets/);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ const config: PlaywrightTestConfig = {
|
|||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "retain-on-failure",
|
||||
|
||||
permissions: ["clipboard-read"],
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
|
|
|
|||
|
|
@ -59,5 +59,11 @@
|
|||
"bracket.grand_finals": "Grand Finals",
|
||||
"bracket.grand_finals.bracket_reset": "Bracket Reset",
|
||||
"bracket.main": "Main Bracket",
|
||||
"bracket.underground": "Underground Bracket"
|
||||
"bracket.underground": "Underground Bracket",
|
||||
|
||||
"actions.addSub": "Add sub",
|
||||
"actions.shareLink": "Share your invite link to add members: {{inviteLink}}",
|
||||
"actions.sub.prompt_other": "You can still add {{count}} subs to your roster",
|
||||
"actions.sub.prompt_one": "You can still add {{count}} sub to your roster",
|
||||
"actions.sub.prompt_zero": "Your roster is full and more subs can't be added"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user