mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-04-25 07:32:19 -05:00
Team: Manage roster tests + fixes
This commit is contained in:
parent
e83943abb1
commit
3634b85422
|
|
@ -37,7 +37,12 @@ export function FormWithConfirm({
|
|||
<div className="stack md">
|
||||
<h2 className="text-sm">{dialogHeading}</h2>
|
||||
<div className="stack horizontal md justify-center">
|
||||
<Button form={id} variant="destructive" type="submit">
|
||||
<Button
|
||||
form={id}
|
||||
variant="destructive"
|
||||
type="submit"
|
||||
testId={dialogOpen ? "confirm-button" : undefined}
|
||||
>
|
||||
{deleteButtonText ?? t("common:actions.delete")}
|
||||
</Button>
|
||||
<Button onClick={closeDialog}>{t("common:actions.cancel")}</Button>
|
||||
|
|
|
|||
|
|
@ -61,7 +61,10 @@ export const meta: MetaFunction = ({
|
|||
export const handle: SendouRouteHandle = {
|
||||
i18n: ["team"],
|
||||
breadcrumb: ({ match }) => {
|
||||
const data = match.data as SerializeFrom<typeof loader>;
|
||||
const data = match.data as SerializeFrom<typeof loader> | undefined;
|
||||
|
||||
if (!data) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
imgPath: navIconUrl("t"),
|
||||
|
|
|
|||
|
|
@ -108,7 +108,10 @@ export const action: ActionFunction = async ({ request, params }) => {
|
|||
export const handle: SendouRouteHandle = {
|
||||
i18n: ["team"],
|
||||
breadcrumb: ({ match }) => {
|
||||
const data = match.data as SerializeFrom<typeof loader>;
|
||||
const data = match.data as SerializeFrom<typeof loader> | undefined;
|
||||
|
||||
if (!data) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
imgPath: navIconUrl("t"),
|
||||
|
|
@ -198,8 +201,8 @@ function MemberActions() {
|
|||
<h2 className="text-lg">{t("team:roster.members.header")}</h2>
|
||||
|
||||
<div className="team__roster__members">
|
||||
{team.members.map((member) => (
|
||||
<MemberRow key={member.id} member={member} />
|
||||
{team.members.map((member, i) => (
|
||||
<MemberRow key={member.id} member={member} number={i} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -207,7 +210,13 @@ function MemberActions() {
|
|||
}
|
||||
|
||||
const NO_ROLE = "NO_ROLE";
|
||||
function MemberRow({ member }: { member: DetailedTeamMember }) {
|
||||
function MemberRow({
|
||||
member,
|
||||
number,
|
||||
}: {
|
||||
member: DetailedTeamMember;
|
||||
number: number;
|
||||
}) {
|
||||
const { team } = useLoaderData<typeof loader>();
|
||||
const { t } = useTranslation(["team"]);
|
||||
const user = useUser();
|
||||
|
|
@ -218,7 +227,10 @@ function MemberRow({ member }: { member: DetailedTeamMember }) {
|
|||
|
||||
return (
|
||||
<React.Fragment key={member.id}>
|
||||
<div className="team__roster__members__member">
|
||||
<div
|
||||
className="team__roster__members__member"
|
||||
data-testid={`member-row-${number}`}
|
||||
>
|
||||
{discordFullName(member)}
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -235,6 +247,7 @@ function MemberRow({ member }: { member: DetailedTeamMember }) {
|
|||
)
|
||||
}
|
||||
disabled={roleFetcher.state !== "idle"}
|
||||
data-testid={`role-select-${number}`}
|
||||
>
|
||||
<option value={NO_ROLE}>No role</option>
|
||||
{TEAM_MEMBER_ROLES.map((role) => {
|
||||
|
|
@ -258,7 +271,11 @@ function MemberRow({ member }: { member: DetailedTeamMember }) {
|
|||
["userId", member.id],
|
||||
]}
|
||||
>
|
||||
<Button size="tiny" variant="minimal-destructive">
|
||||
<Button
|
||||
size="tiny"
|
||||
variant="minimal-destructive"
|
||||
testId={`kick-button-${number}`}
|
||||
>
|
||||
{t("team:actionButtons.kick")}
|
||||
</Button>
|
||||
</FormWithConfirm>
|
||||
|
|
@ -275,7 +292,11 @@ function MemberRow({ member }: { member: DetailedTeamMember }) {
|
|||
["newOwnerId", member.id],
|
||||
]}
|
||||
>
|
||||
<Button size="tiny" variant="minimal-destructive">
|
||||
<Button
|
||||
size="tiny"
|
||||
variant="minimal-destructive"
|
||||
testId={`transfer-ownership-button-${number}`}
|
||||
>
|
||||
{t("team:actionButtons.transferOwnership")}
|
||||
</Button>
|
||||
</FormWithConfirm>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { Avatar } from "~/components/Avatar";
|
|||
import { Button, LinkButton } from "~/components/Button";
|
||||
import { Flag } from "~/components/Flag";
|
||||
import { FormWithConfirm } from "~/components/FormWithConfirm";
|
||||
import { TwitterIcon } from "~/components/icons/Twitter";
|
||||
import { WeaponImage } from "~/components/Image";
|
||||
import { Main } from "~/components/Main";
|
||||
import { Placement } from "~/components/Placement";
|
||||
|
|
@ -26,6 +27,7 @@ import {
|
|||
navIconUrl,
|
||||
teamPage,
|
||||
TEAM_SEARCH_PAGE,
|
||||
twitterUrl,
|
||||
userPage,
|
||||
userSubmittedImage,
|
||||
} from "~/utils/urls";
|
||||
|
|
@ -69,7 +71,10 @@ export const action: ActionFunction = async ({ request, params }) => {
|
|||
export const handle: SendouRouteHandle = {
|
||||
i18n: ["team"],
|
||||
breadcrumb: ({ match }) => {
|
||||
const data = match.data as SerializeFrom<typeof loader>;
|
||||
const data = match.data as SerializeFrom<typeof loader> | undefined;
|
||||
|
||||
if (!data) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
imgPath: navIconUrl("t"),
|
||||
|
|
@ -107,9 +112,9 @@ export default function TeamPage() {
|
|||
{team.results ? <ResultsBanner results={team.results} /> : null}
|
||||
{team.bio ? <article data-testid="team-bio">{team.bio}</article> : null}
|
||||
<div className="stack lg">
|
||||
{team.members.map((member) => (
|
||||
{team.members.map((member, i) => (
|
||||
<React.Fragment key={member.discordId}>
|
||||
<MemberRow member={member} />
|
||||
<MemberRow member={member} number={i} />
|
||||
<MobileMemberCard member={member} />
|
||||
</React.Fragment>
|
||||
))}
|
||||
|
|
@ -147,34 +152,15 @@ function TeamBanner() {
|
|||
return <Flag key={country} countryCode={country} />;
|
||||
})}
|
||||
</div>
|
||||
<div className="team__banner__name">{team.name}</div>
|
||||
<div className="team__banner__name">
|
||||
{team.name} <TwitterLink testId="twitter-link" />
|
||||
</div>
|
||||
</div>
|
||||
{team.avatarSrc ? <div className="team__banner__avatar__spacer" /> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// function InfoBadges() {
|
||||
// const { team } = useLoaderData<typeof loader>();
|
||||
|
||||
// return (
|
||||
// <div className="team__badges">
|
||||
// {team.teamXp ? (
|
||||
// <div>
|
||||
// <Image
|
||||
// path={navIconUrl("xsearch")}
|
||||
// width={26}
|
||||
// alt="Team XP"
|
||||
// title="Team XP"
|
||||
// />
|
||||
// {team.teamXp}
|
||||
// </div>
|
||||
// ) : null}
|
||||
// {team.lutiDiv ? <div>LUTI Div {team.lutiDiv}</div> : null}
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
function MobileTeamNameCountry() {
|
||||
const { team } = useLoaderData<typeof loader>();
|
||||
|
||||
|
|
@ -185,11 +171,32 @@ function MobileTeamNameCountry() {
|
|||
return <Flag key={country} countryCode={country} tiny />;
|
||||
})}
|
||||
</div>
|
||||
{team.name}
|
||||
<div className="team__mobile-team-name">
|
||||
{team.name}
|
||||
<TwitterLink />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TwitterLink({ testId }: { testId?: string }) {
|
||||
const { team } = useLoaderData<typeof loader>();
|
||||
|
||||
if (!team.twitter) return null;
|
||||
|
||||
return (
|
||||
<a
|
||||
className="team__twitter-link"
|
||||
href={twitterUrl(team.twitter)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
data-testid={testId}
|
||||
>
|
||||
<TwitterIcon />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
function ActionButtons() {
|
||||
const { t } = useTranslation(["team"]);
|
||||
const user = useUser();
|
||||
|
|
@ -217,6 +224,7 @@ function ActionButtons() {
|
|||
to={manageTeamRosterPage(team.customUrl)}
|
||||
variant="outlined"
|
||||
prefetch="intent"
|
||||
testId="manage-roster-button"
|
||||
>
|
||||
{t("team:actionButtons.manageRoster")}
|
||||
</LinkButton>
|
||||
|
|
@ -253,13 +261,22 @@ function ResultsBanner({ results }: { results: TeamResultPeek }) {
|
|||
);
|
||||
}
|
||||
|
||||
function MemberRow({ member }: { member: DetailedTeamMember }) {
|
||||
function MemberRow({
|
||||
member,
|
||||
number,
|
||||
}: {
|
||||
member: DetailedTeamMember;
|
||||
number: number;
|
||||
}) {
|
||||
const { t } = useTranslation(["team"]);
|
||||
|
||||
return (
|
||||
<div className="team__member">
|
||||
{member.role ? (
|
||||
<span className="team__member__role">
|
||||
<span
|
||||
className="team__member__role"
|
||||
data-testid={`member-row-role-${number}`}
|
||||
>
|
||||
{t(`team:roles.${member.role}`)}
|
||||
</span>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -257,7 +257,7 @@ function NewTeamDialog() {
|
|||
minLength={TEAM.NAME_MIN_LENGTH}
|
||||
maxLength={TEAM.NAME_MAX_LENGTH}
|
||||
required
|
||||
data-testid="new-team-name-input"
|
||||
data-testid={isOpen ? "new-team-name-input" : undefined}
|
||||
/>
|
||||
</div>
|
||||
<FormErrors namespace="team" />
|
||||
|
|
|
|||
|
|
@ -80,6 +80,21 @@
|
|||
font-weight: var(--bold);
|
||||
color: #fff;
|
||||
display: none;
|
||||
align-items: center;
|
||||
gap: var(--s-3);
|
||||
}
|
||||
|
||||
.team__twitter-link {
|
||||
padding: var(--s-1);
|
||||
border: 1px solid;
|
||||
border-radius: 50%;
|
||||
border-color: #1da1f2;
|
||||
background-color: #1da0f22f;
|
||||
}
|
||||
|
||||
.team__twitter-link > svg {
|
||||
width: 0.9rem;
|
||||
fill: #1da1f2;
|
||||
}
|
||||
|
||||
.team__banner__avatar {
|
||||
|
|
@ -111,6 +126,12 @@
|
|||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.team__mobile-team-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s-2);
|
||||
}
|
||||
|
||||
.team__banner__avatar img {
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
|
@ -275,7 +296,7 @@
|
|||
display: flex;
|
||||
}
|
||||
.team__banner__name {
|
||||
display: block;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.team__banner__avatar > div {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,10 @@ import { notFoundIfFalsy } from "~/utils/remix";
|
|||
|
||||
export const handle: SendouRouteHandle = {
|
||||
breadcrumb: ({ match }) => {
|
||||
const data = match.data as SerializeFrom<typeof loader>;
|
||||
const data = match.data as SerializeFrom<typeof loader> | undefined;
|
||||
|
||||
if (!data) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
imgPath: navIconUrl("articles"),
|
||||
|
|
|
|||
|
|
@ -36,7 +36,9 @@ export const meta: MetaFunction = (args) => {
|
|||
export const handle: SendouRouteHandle = {
|
||||
i18n: ["weapons", "builds", "gear"],
|
||||
breadcrumb: ({ match }) => {
|
||||
const data = match.data as SerializeFrom<typeof loader>;
|
||||
const data = match.data as SerializeFrom<typeof loader> | undefined;
|
||||
|
||||
if (!data) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -94,7 +94,10 @@ export const meta: MetaFunction = (args) => {
|
|||
export const handle: SendouRouteHandle = {
|
||||
i18n: ["calendar", "game-misc"],
|
||||
breadcrumb: ({ match }) => {
|
||||
const data = match.data as SerializeFrom<typeof loader>;
|
||||
const data = match.data as SerializeFrom<typeof loader> | undefined;
|
||||
|
||||
if (!data) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
imgPath: navIconUrl("calendar"),
|
||||
|
|
|
|||
|
|
@ -42,9 +42,9 @@ export const meta: MetaFunction = ({ data }: { data: UserPageLoaderData }) => {
|
|||
export const handle: SendouRouteHandle = {
|
||||
i18n: "user",
|
||||
breadcrumb: ({ match }) => {
|
||||
const data = match.data as UserPageLoaderData;
|
||||
const data = match.data as UserPageLoaderData | undefined;
|
||||
|
||||
if (!data) return;
|
||||
if (!data) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Page } from "@playwright/test";
|
||||
import { expect, type Locator, type Page } from "@playwright/test";
|
||||
|
||||
export async function selectWeapon({
|
||||
page,
|
||||
|
|
@ -16,17 +16,25 @@ export async function selectWeapon({
|
|||
/** page.goto that waits for the page to be hydrated before proceeding */
|
||||
export async function navigate({ page, url }: { page: Page; url: string }) {
|
||||
await page.goto(url);
|
||||
page.getByTestId("hydrated");
|
||||
await expect(page.getByTestId("hydrated")).toHaveCount(1);
|
||||
}
|
||||
|
||||
export async function seed(page: Page) {
|
||||
export function seed(page: Page) {
|
||||
return page.request.post("/seed");
|
||||
}
|
||||
|
||||
export async function impersonate(page: Page, userId = 1) {
|
||||
export function impersonate(page: Page, userId = 1) {
|
||||
return page.request.post(`/auth/impersonate?id=${userId}`);
|
||||
}
|
||||
|
||||
export async function submit(page: Page) {
|
||||
export function submit(page: Page) {
|
||||
return page.getByTestId("submit-button").click();
|
||||
}
|
||||
|
||||
export function isNotVisible(locator: Locator) {
|
||||
return expect(locator).toHaveCount(0);
|
||||
}
|
||||
|
||||
export function modalClickConfirmButton(page: Page) {
|
||||
return page.getByTestId("confirm-button").click();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,9 @@ export const ANTARISKA_TWITTER = "https://twitter.com/antariska_spl";
|
|||
export const ipLabsMaps = (pool: string) =>
|
||||
`https://maps.iplabs.ink/?3&pool=${pool}`;
|
||||
|
||||
export const twitterUrl = (accountName: string) =>
|
||||
`https://twitter.com/${accountName}`;
|
||||
|
||||
export const LOG_IN_URL = "/auth";
|
||||
export const LOG_OUT_URL = "/auth/logout";
|
||||
export const ADMIN_PAGE = "/admin";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
import { expect, test } from "@playwright/test";
|
||||
import { navigate, seed, impersonate, submit } from "~/utils/playwright";
|
||||
import {
|
||||
navigate,
|
||||
seed,
|
||||
impersonate,
|
||||
submit,
|
||||
isNotVisible,
|
||||
modalClickConfirmButton,
|
||||
} from "~/utils/playwright";
|
||||
import { teamPage, TEAM_SEARCH_PAGE } from "~/utils/urls";
|
||||
|
||||
test.describe("Team search page", () => {
|
||||
|
|
@ -55,7 +62,38 @@ test.describe("Team page", () => {
|
|||
await submit(page);
|
||||
|
||||
await expect(page).toHaveURL(/better-alliance-rogue/);
|
||||
await page.getByText("getByText").isVisible();
|
||||
// xxx: check twitter
|
||||
await page.getByText("shorter bio").isVisible();
|
||||
await expect(page.getByTestId("twitter-link")).toHaveAttribute(
|
||||
"href",
|
||||
"https://twitter.com/BetterAllianceRogue"
|
||||
);
|
||||
});
|
||||
|
||||
test("manages roster", async ({ page }) => {
|
||||
await seed(page);
|
||||
await impersonate(page, 1);
|
||||
await navigate({ page, url: teamPage("alliance-rogue") });
|
||||
|
||||
await page.getByTestId("manage-roster-button").click();
|
||||
|
||||
await page.getByTestId("role-select-0").selectOption("SUPPORT");
|
||||
|
||||
await page.getByTestId("member-row-3").isVisible();
|
||||
// kick-button-0 is self
|
||||
await page.getByTestId("kick-button-1").click();
|
||||
await modalClickConfirmButton(page);
|
||||
await isNotVisible(page.getByTestId("member-row-3"));
|
||||
|
||||
await page.getByTestId("transfer-ownership-button-1").click();
|
||||
await modalClickConfirmButton(page);
|
||||
|
||||
await expect(page.getByTestId("member-row-role-0")).toHaveText("Support");
|
||||
|
||||
await expect(page).not.toHaveURL(/roster/);
|
||||
await isNotVisible(page.getByTestId("manage-roster-button"));
|
||||
});
|
||||
|
||||
// resets inviteCode, copies it to clipboard, joins team, leaves, joins again
|
||||
|
||||
// deletes team
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user