Team: Manage roster tests + fixes

This commit is contained in:
Kalle 2023-01-12 20:57:22 +02:00
parent e83943abb1
commit 3634b85422
13 changed files with 176 additions and 52 deletions

View File

@ -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>

View File

@ -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"),

View File

@ -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>

View File

@ -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}

View File

@ -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" />

View File

@ -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 {

View File

@ -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"),

View File

@ -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 [
{

View File

@ -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"),

View File

@ -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 [
{

View File

@ -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();
}

View File

@ -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";

View File

@ -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
});