mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-10-02 08:07:40 -05:00
Add sidebar e2e tests, fix external stream links and offline casts showing as live
This commit is contained in:
@@ -45,7 +45,7 @@ export function Catcher() {
|
||||
|
||||
if (isNetworkError) {
|
||||
return (
|
||||
<Main>
|
||||
<ErrorMain>
|
||||
<ErrorGirlImage />
|
||||
<h2 className="text-center">Connection error</h2>
|
||||
<p className="text-center">
|
||||
@@ -55,7 +55,7 @@ export function Catcher() {
|
||||
<div className="mt-4 stack sm items-center">
|
||||
<RefreshPageButton />
|
||||
</div>
|
||||
</Main>
|
||||
</ErrorMain>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ export function Catcher() {
|
||||
})();
|
||||
|
||||
return (
|
||||
<Main>
|
||||
<ErrorMain>
|
||||
<ErrorGirlImage />
|
||||
<h2 className="text-center">Error happened</h2>
|
||||
<p className="text-center">
|
||||
@@ -84,7 +84,7 @@ export function Catcher() {
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</Main>
|
||||
</ErrorMain>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ export function Catcher() {
|
||||
case 401:
|
||||
if (!user) {
|
||||
return (
|
||||
<Main>
|
||||
<ErrorMain>
|
||||
<h2>Authentication required</h2>
|
||||
<p>This page requires you to be logged in.</p>
|
||||
<form action={LOG_IN_URL} method="post" className="mt-2">
|
||||
@@ -100,36 +100,36 @@ export function Catcher() {
|
||||
Log in via Discord
|
||||
</SendouButton>
|
||||
</form>
|
||||
</Main>
|
||||
</ErrorMain>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Main>
|
||||
<ErrorMain>
|
||||
<h2>Error 401 Unauthorized</h2>
|
||||
<GetHelp />
|
||||
</Main>
|
||||
</ErrorMain>
|
||||
);
|
||||
case 403:
|
||||
return (
|
||||
<Main>
|
||||
<ErrorMain>
|
||||
<h2>Error 403 Forbidden</h2>
|
||||
<p className="text-sm text-lighter font-semi-bold">
|
||||
Your account doesn't have the required permissions to perform this
|
||||
action.
|
||||
</p>
|
||||
<GetHelp />
|
||||
</Main>
|
||||
</ErrorMain>
|
||||
);
|
||||
case 404:
|
||||
return (
|
||||
<Main>
|
||||
<ErrorMain>
|
||||
<h2>Error {error.status} - Page not found</h2>
|
||||
<GetHelp />
|
||||
</Main>
|
||||
</ErrorMain>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Main>
|
||||
<ErrorMain>
|
||||
<h2>Error {error.status}</h2>
|
||||
<GetHelp />
|
||||
<div className="text-sm text-lighter font-semi-bold">
|
||||
@@ -142,11 +142,16 @@ export function Catcher() {
|
||||
? `\n${typeof error.data === "string" ? error.data : JSON.stringify(error.data, null, 2)}`
|
||||
: null}
|
||||
</pre>
|
||||
</Main>
|
||||
</ErrorMain>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** Every branch of the error page, marked so tests can assert one is not shown. */
|
||||
function ErrorMain({ children }: { children: React.ReactNode }) {
|
||||
return <Main testId="error-page">{children}</Main>;
|
||||
}
|
||||
|
||||
function GetHelp() {
|
||||
return (
|
||||
<p className="mt-2">
|
||||
|
||||
@@ -10,6 +10,7 @@ export const Main = ({
|
||||
bigger,
|
||||
breakoutContainer,
|
||||
style,
|
||||
testId,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
@@ -18,6 +19,7 @@ export const Main = ({
|
||||
bigger?: boolean;
|
||||
breakoutContainer?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
testId?: string;
|
||||
}) => {
|
||||
return (
|
||||
<main
|
||||
@@ -37,6 +39,7 @@ export const Main = ({
|
||||
)
|
||||
}
|
||||
data-main-breakout={breakoutContainer || undefined}
|
||||
data-testid={testId}
|
||||
style={style}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -226,6 +226,38 @@ export async function startBracket(
|
||||
return startedBracket.data.match.map((match) => ({ id: match.id }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a match as casted by a Twitch account, adding the account to the
|
||||
* tournament's cast accounts first, the way the organizer's stream page does.
|
||||
*/
|
||||
export async function castMatch({
|
||||
tournamentId,
|
||||
matchId,
|
||||
twitchAccount,
|
||||
}: {
|
||||
tournamentId: number;
|
||||
matchId: number;
|
||||
twitchAccount: string;
|
||||
}) {
|
||||
const tournament = await TournamentRepository.findById(tournamentId);
|
||||
invariant(tournament, `Tournament ${tournamentId} not found`);
|
||||
|
||||
await TournamentRepository.updateCastTwitchAccounts({
|
||||
tournamentId,
|
||||
castTwitchAccounts: [
|
||||
...(tournament.castTwitchAccounts ?? []),
|
||||
twitchAccount,
|
||||
],
|
||||
});
|
||||
await TournamentRepository.setMatchAsCasted({
|
||||
tournamentId,
|
||||
matchId,
|
||||
twitchAccount,
|
||||
});
|
||||
|
||||
clearTournamentDataCache(tournamentId);
|
||||
}
|
||||
|
||||
interface PlayedMatch {
|
||||
id: number;
|
||||
/** Index of the bracket the match belongs to in the progression. */
|
||||
|
||||
@@ -2,7 +2,10 @@ import { refreshApiTokensCache } from "~/features/api-public/api-public-utils.se
|
||||
import { refreshBannedCache } from "~/features/ban/core/banned.server";
|
||||
import { clearParticipationInfoMap } from "~/features/front-page/core/ShowcaseTournaments.server";
|
||||
import { refreshSendouQInstance } from "~/features/sendouq/core/SendouQ.server";
|
||||
import { clearAllTournamentDataCache } from "~/features/tournament-bracket/core/Tournament.server";
|
||||
import {
|
||||
clearAllTournamentDataCache,
|
||||
refreshRunningTournaments,
|
||||
} from "~/features/tournament-bracket/core/Tournament.server";
|
||||
import { refreshTentativeTiersCache } from "~/features/tournament-organization/core/tentativeTiers.server";
|
||||
import { cache } from "~/utils/cache.server";
|
||||
|
||||
@@ -19,4 +22,5 @@ export async function refreshCaches() {
|
||||
await refreshSendouQInstance();
|
||||
await refreshTentativeTiersCache();
|
||||
await refreshApiTokensCache();
|
||||
await refreshRunningTournaments();
|
||||
}
|
||||
|
||||
@@ -188,9 +188,7 @@ function tournamentStreamUrl({
|
||||
);
|
||||
if (teammateAccount) return twitchUrl(teammateAccount);
|
||||
|
||||
const castAccount = tournament.ctx.castedMatchesInfo?.castedMatches.find(
|
||||
(castedMatch) => castedMatch.matchId === matchId,
|
||||
)?.twitchAccount;
|
||||
const castAccount = liveCastAccount(tournament, matchId);
|
||||
if (castAccount) return twitchUrl(castAccount);
|
||||
|
||||
const opponentAccount = streamingTwitchAccount(
|
||||
@@ -201,6 +199,20 @@ function tournamentStreamUrl({
|
||||
return opponentAccount ? twitchUrl(opponentAccount) : null;
|
||||
}
|
||||
|
||||
/** Account casting the match, only when it is actually streaming right now. */
|
||||
function liveCastAccount(tournament: Tournament, matchId: number) {
|
||||
const castAccount = tournament.ctx.castedMatchesInfo?.castedMatches.find(
|
||||
(castedMatch) => castedMatch.matchId === matchId,
|
||||
)?.twitchAccount;
|
||||
if (!castAccount) return null;
|
||||
|
||||
const isLive = tournament.ctx.castStreams.some(
|
||||
(stream) => stream.twitch?.toLowerCase() === castAccount.toLowerCase(),
|
||||
);
|
||||
|
||||
return isLive ? castAccount : null;
|
||||
}
|
||||
|
||||
function streamingTwitchAccount(
|
||||
players: Array<{ userId: number; streamTwitch: string | null }>,
|
||||
streamingParticipantIds: ReadonlySet<number>,
|
||||
|
||||
@@ -188,6 +188,17 @@ function syncTournamentToRegistry(tournament: Tournament) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuilds the running tournaments registry from the database, forgetting the
|
||||
* tournaments it held. E2E workers call this (via `/refresh-caches`) after
|
||||
* writing tournaments straight into the database file.
|
||||
*/
|
||||
export async function refreshRunningTournaments() {
|
||||
RunningTournaments.clear();
|
||||
|
||||
await primeRunningTournamentsCache();
|
||||
}
|
||||
|
||||
async function primeRunningTournamentsCache() {
|
||||
const tournamentIds = await TournamentRepository.findRunningTournamentIds();
|
||||
|
||||
|
||||
15
app/root.tsx
15
app/root.tsx
@@ -235,7 +235,7 @@ function Document({
|
||||
<body>
|
||||
{IS_E2E_TEST_RUN && <HydrationTestIndicator />}
|
||||
<React.StrictMode>
|
||||
<RouterProvider navigate={navigate} useHref={useHref}>
|
||||
<RouterProvider navigate={navigate} useHref={useExternalAwareHref}>
|
||||
<I18nProvider locale={language}>
|
||||
<SendouToastRegion />
|
||||
<UnsavedChangesGuard />
|
||||
@@ -253,6 +253,19 @@ function Document({
|
||||
);
|
||||
}
|
||||
|
||||
const ABSOLUTE_URL_REGEX = /^[a-z][a-z\d+\-.]*:/i;
|
||||
|
||||
/**
|
||||
* Href every React Aria link (menu items, buttons, tabs) is rendered with.
|
||||
* `useHref` resolves its argument against the current route, which would turn an
|
||||
* absolute URL such as a Twitch link into a path of our own.
|
||||
*/
|
||||
function useExternalAwareHref(href: string) {
|
||||
const resolved = useHref(href);
|
||||
|
||||
return ABSOLUTE_URL_REGEX.test(href) ? href : resolved;
|
||||
}
|
||||
|
||||
function useTriggerToasts() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -40,12 +40,18 @@ export async function loadFactories(parallelIndex: number) {
|
||||
CalendarEventFactory: await import(
|
||||
"~/db/seed/factories/CalendarEventFactory"
|
||||
),
|
||||
FriendRequestFactory: await import(
|
||||
"~/db/seed/factories/FriendRequestFactory"
|
||||
),
|
||||
FriendshipFactory: await import("~/db/seed/factories/FriendshipFactory"),
|
||||
LFGPostFactory: await import("~/db/seed/factories/LFGPostFactory"),
|
||||
LiveStreamFactory: await import("~/db/seed/factories/LiveStreamFactory"),
|
||||
NotificationFactory: await import(
|
||||
"~/db/seed/factories/NotificationFactory"
|
||||
),
|
||||
SavedCalendarEventFactory: await import(
|
||||
"~/db/seed/factories/SavedCalendarEventFactory"
|
||||
),
|
||||
ScrimPostFactory: await import("~/db/seed/factories/ScrimPostFactory"),
|
||||
SQGroupFactory: await import("~/db/seed/factories/SQGroupFactory"),
|
||||
SQMatchFactory: await import("~/db/seed/factories/SQMatchFactory"),
|
||||
|
||||
@@ -21,6 +21,9 @@ try {
|
||||
}
|
||||
export const E2E_BASE_PORT = Number(process.env.PORT || 5173) + 500;
|
||||
|
||||
export const MOBILE_VIEWPORT = { width: 375, height: 667 };
|
||||
export const TABLET_VIEWPORT = { width: 768, height: 1024 };
|
||||
|
||||
type WorkerFixtures = {
|
||||
workerPort: number;
|
||||
workerBaseURL: string;
|
||||
@@ -266,6 +269,11 @@ async function expectRouterIdle(page: Page) {
|
||||
);
|
||||
}
|
||||
|
||||
/** Asserts the page rendered rather than the error boundary catching something. */
|
||||
export async function expectNoErrorPage(page: Page) {
|
||||
await expect(page.getByTestId("error-page")).toHaveCount(0);
|
||||
}
|
||||
|
||||
export function isNotVisible(locator: Locator) {
|
||||
return expect(locator).toHaveCount(0);
|
||||
}
|
||||
|
||||
73
e2e/helpers/sidebar.ts
Normal file
73
e2e/helpers/sidebar.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import type { Locator } from "@playwright/test";
|
||||
import { addDays, addHours } from "date-fns";
|
||||
import { NZAP_TEST_ID } from "~/db/seed/constants";
|
||||
import { dateToDatabaseTimestamp } from "~/utils/dates";
|
||||
import invariant from "~/utils/invariant";
|
||||
import type { Factories } from "./factories";
|
||||
import { expect } from "./playwright";
|
||||
|
||||
export const NO_EVENTS_TEXT = "No upcoming events";
|
||||
export const NO_STREAMS_TEXT = "No streams currently";
|
||||
export const NO_FRIENDS_TEXT = "Add friends to see their activity here";
|
||||
export const LOGGED_OUT_FRIENDS_TEXT =
|
||||
"Log in to follow your friends' activity";
|
||||
|
||||
export function inHours(hours: number) {
|
||||
return dateToDatabaseTimestamp(addHours(new Date(), hours));
|
||||
}
|
||||
|
||||
export function inDays(days: number) {
|
||||
return dateToDatabaseTimestamp(addDays(new Date(), days));
|
||||
}
|
||||
|
||||
/** The Twitch account `LiveStreamFactory` gives a user's stream. */
|
||||
export function streamAccountOf(userId: number) {
|
||||
return `stream_${userId}`;
|
||||
}
|
||||
|
||||
/** Users the sidebar rows can be found by, one per name. */
|
||||
export function createNamedUsers(
|
||||
factories: Factories,
|
||||
names: string[],
|
||||
overrides?: { twitch?: string },
|
||||
) {
|
||||
return factories.UserFactory.createMany(names.length, (index) => ({
|
||||
...overrides,
|
||||
discordName: names[index],
|
||||
}));
|
||||
}
|
||||
|
||||
export async function createUserIds(factories: Factories, count: number) {
|
||||
const users = await factories.UserFactory.createMany(count);
|
||||
|
||||
return users.map((user) => user.id);
|
||||
}
|
||||
|
||||
export function befriend(
|
||||
factories: Factories,
|
||||
friendIds: number[],
|
||||
userId = NZAP_TEST_ID,
|
||||
) {
|
||||
return Promise.all(
|
||||
friendIds.map((friendId) =>
|
||||
factories.FriendshipFactory.create({
|
||||
userOneId: userId,
|
||||
userTwoId: friendId,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/** Asserts the rows are rendered in the order given, top to bottom. */
|
||||
export async function expectTopToBottom(rows: Locator[]) {
|
||||
const tops: number[] = [];
|
||||
|
||||
for (const row of rows) {
|
||||
await expect(row).toBeVisible();
|
||||
const box = await row.boundingBox();
|
||||
invariant(box, "Visible row has no bounding box");
|
||||
tops.push(box.y);
|
||||
}
|
||||
|
||||
expect(tops).toEqual([...tops].sort((a, b) => a - b));
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
import { subMinutes } from "date-fns";
|
||||
import { addHours, subMinutes } from "date-fns";
|
||||
import type { TournamentSettings } from "~/db/tables-json";
|
||||
import { ADMIN_ID } from "~/features/admin/admin-constants";
|
||||
import type { TournamentTierNumber } from "~/features/tournament/core/tiering";
|
||||
import type { ModeShort, StageId } from "~/modules/in-game-lists/types";
|
||||
import { dateToDatabaseTimestamp } from "~/utils/dates";
|
||||
import invariant from "~/utils/invariant";
|
||||
import type { Factories } from "./factories";
|
||||
|
||||
export const ROSTER_SIZE = 4;
|
||||
@@ -166,3 +169,90 @@ export async function createTeams(
|
||||
export function startedTournamentTimes() {
|
||||
return [dateToDatabaseTimestamp(subMinutes(new Date(), 30))];
|
||||
}
|
||||
|
||||
/**
|
||||
* A tournament running an unfinished match: two checked-in teams and the bracket
|
||||
* started, which is all it takes for both teams to be "in a match". The friend and
|
||||
* their teammate play the opponent, and every user given is on the roster it is
|
||||
* named for.
|
||||
*/
|
||||
export async function createInProgressMatch(
|
||||
factories: Factories,
|
||||
{
|
||||
name,
|
||||
friendId,
|
||||
teammateId,
|
||||
opponentId,
|
||||
tier,
|
||||
}: {
|
||||
name: string;
|
||||
friendId: number;
|
||||
teammateId?: number;
|
||||
opponentId?: number;
|
||||
tier?: TournamentTierNumber;
|
||||
},
|
||||
) {
|
||||
const tournament = await factories.TournamentFactory.create(
|
||||
{
|
||||
authorId: ADMIN_ID,
|
||||
name,
|
||||
startTimes: startedTournamentTimes(),
|
||||
},
|
||||
{ tier },
|
||||
);
|
||||
|
||||
const teams = await createTeams(factories, tournament.id, [
|
||||
{ members: [friendId, ...(teammateId ? [teammateId] : [])] },
|
||||
{ members: opponentId ? [opponentId] : [] },
|
||||
]);
|
||||
|
||||
const [match] = await factories.TournamentFactory.startBracket(tournament.id);
|
||||
invariant(match, "Starting the bracket created no match");
|
||||
|
||||
return { tournament, teams, matchId: match.id };
|
||||
}
|
||||
|
||||
/** An upcoming tournament where the given user's team is looking for subs. */
|
||||
export async function createSubSeekingTournament(
|
||||
factories: Factories,
|
||||
{ name, subId }: { name: string; subId: number },
|
||||
) {
|
||||
const tournament = await factories.TournamentFactory.create({
|
||||
authorId: ADMIN_ID,
|
||||
name,
|
||||
startTimes: [dateToDatabaseTimestamp(addHours(new Date(), 2))],
|
||||
});
|
||||
|
||||
await factories.TournamentTeamFactory.create(
|
||||
{ tournamentId: tournament.id, memberUserIds: [subId] },
|
||||
{ isLooking: true },
|
||||
);
|
||||
|
||||
return { tournament };
|
||||
}
|
||||
|
||||
/**
|
||||
* A tournament where the given user is waiting for their next match: three teams
|
||||
* in a single elimination bracket leave the top seed with a bye into the final,
|
||||
* whose other side is still being played for.
|
||||
*/
|
||||
export async function createTournamentWithByeTeam(
|
||||
factories: Factories,
|
||||
{ name, waitingUserId }: { name: string; waitingUserId: number },
|
||||
) {
|
||||
const tournament = await factories.TournamentFactory.create({
|
||||
authorId: ADMIN_ID,
|
||||
name,
|
||||
startTimes: startedTournamentTimes(),
|
||||
});
|
||||
|
||||
await createTeams(factories, tournament.id, [
|
||||
{ members: [waitingUserId] },
|
||||
{},
|
||||
{},
|
||||
]);
|
||||
|
||||
await factories.TournamentFactory.startBracket(tournament.id);
|
||||
|
||||
return { tournament };
|
||||
}
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { NZAP_TEST_ID } from "~/db/seed/constants";
|
||||
import { expect, impersonate, navigate, test } from "./helpers/playwright";
|
||||
import { SENDOUQ_PAGE } from "~/utils/urls";
|
||||
import {
|
||||
expect,
|
||||
impersonate,
|
||||
MOBILE_VIEWPORT,
|
||||
navigate,
|
||||
TABLET_VIEWPORT,
|
||||
test,
|
||||
} from "./helpers/playwright";
|
||||
import { MobileNav } from "./pages/layout/mobile-nav";
|
||||
import { SideNav } from "./pages/layout/side-nav";
|
||||
import { TopNavMenus } from "./pages/layout/top-nav-menus";
|
||||
|
||||
test.describe("Navigation", () => {
|
||||
@@ -9,41 +16,26 @@ test.describe("Navigation", () => {
|
||||
await impersonate(page, NZAP_TEST_ID);
|
||||
await navigate({ page, url: "/" });
|
||||
|
||||
const sideNav = new SideNav(page);
|
||||
await expect(sideNav.sectionHeading("Events")).toBeVisible();
|
||||
await expect(sideNav.sectionHeading("Friends")).toBeVisible();
|
||||
await expect(sideNav.sectionHeading("Streams")).toBeVisible();
|
||||
await expect(sideNav.locators.viewAllLinks.first()).toBeVisible();
|
||||
|
||||
await sideNav.toggleCollapse();
|
||||
await expect(sideNav.sectionHeading("Events")).not.toBeVisible();
|
||||
|
||||
await sideNav.toggleCollapse();
|
||||
await expect(sideNav.sectionHeading("Events")).toBeVisible();
|
||||
|
||||
const topNav = new TopNavMenus(page);
|
||||
await topNav.open("Play");
|
||||
await expect(topNav.link("SendouQ")).toBeVisible();
|
||||
await expect(topNav.link("Scrims")).toBeVisible();
|
||||
|
||||
await topNav.link("SendouQ").click();
|
||||
await expect(page).toHaveURL(SENDOUQ_PAGE);
|
||||
await expect(topNav.link("SendouQ")).not.toBeVisible();
|
||||
|
||||
await navigate({ page, url: "/" });
|
||||
|
||||
await topNav.open("Tools");
|
||||
await expect(topNav.link("Analyzer")).toBeVisible();
|
||||
await topNav.close();
|
||||
await topNav.link("Analyzer").click();
|
||||
await expect(page).toHaveURL(/\/analyzer/);
|
||||
|
||||
await topNav.open("Community");
|
||||
await expect(topNav.link("Builds")).toBeVisible();
|
||||
await topNav.close();
|
||||
|
||||
await expect(sideNav.locators.notificationsButton).toBeVisible();
|
||||
await topNav.link("Builds").click();
|
||||
await expect(page).toHaveURL(/\/builds/);
|
||||
});
|
||||
|
||||
test("mobile navigation", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
await page.setViewportSize(MOBILE_VIEWPORT);
|
||||
await impersonate(page, NZAP_TEST_ID);
|
||||
await navigate({ page, url: "/" });
|
||||
|
||||
@@ -55,44 +47,26 @@ test.describe("Navigation", () => {
|
||||
await expect(mobileNav.tab("you")).toBeVisible();
|
||||
|
||||
await mobileNav.openPanel("menu");
|
||||
await expect(mobileNav.menuLink("SendouQ")).toBeVisible();
|
||||
await expect(mobileNav.menuLink("Analyzer")).toBeVisible();
|
||||
await expect(mobileNav.menuLink("Builds")).toBeVisible();
|
||||
await expect(mobileNav.locators.streamsHeading).toBeVisible();
|
||||
|
||||
await mobileNav.switchPanel("friends");
|
||||
await expect(mobileNav.menuLink("SendouQ")).not.toBeVisible();
|
||||
await expect(mobileNav.locators.viewAllLink).toBeVisible();
|
||||
|
||||
await mobileNav.switchPanel("you");
|
||||
await expect(mobileNav.locators.youPanelUsername).toBeVisible();
|
||||
|
||||
await mobileNav.switchPanel("tourneys");
|
||||
await expect(mobileNav.locators.viewAllLink).toBeVisible();
|
||||
|
||||
await mobileNav.closePanel();
|
||||
await expect(mobileNav.locators.viewAllLink).not.toBeVisible();
|
||||
await expect(mobileNav.locators.youPanelUsername).toHaveCount(0);
|
||||
|
||||
await mobileNav.openPanel("menu");
|
||||
await mobileNav.menuLink("SendouQ").click();
|
||||
await expect(page).toHaveURL(SENDOUQ_PAGE);
|
||||
await expect(mobileNav.menuLink("SendouQ")).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("tablet navigation", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 768, height: 1024 });
|
||||
await page.setViewportSize(TABLET_VIEWPORT);
|
||||
await impersonate(page, NZAP_TEST_ID);
|
||||
await navigate({ page, url: "/" });
|
||||
|
||||
const sideNav = new SideNav(page);
|
||||
await expect(sideNav.sectionHeading("Events")).not.toBeVisible();
|
||||
await expect(sideNav.sectionHeading("Friends")).not.toBeVisible();
|
||||
|
||||
await sideNav.openModal();
|
||||
|
||||
await expect(sideNav.sectionHeading("Events")).toBeVisible();
|
||||
await expect(sideNav.sectionHeading("Friends")).toBeVisible();
|
||||
await expect(sideNav.sectionHeading("Streams")).toBeVisible();
|
||||
await expect(sideNav.locators.viewAllLinks.first()).toBeVisible();
|
||||
|
||||
await sideNav.closeModal();
|
||||
await expect(sideNav.sectionHeading("Events")).not.toBeVisible();
|
||||
|
||||
const topNav = new TopNavMenus(page);
|
||||
await topNav.open("Play");
|
||||
await expect(topNav.link("SendouQ")).toBeVisible();
|
||||
|
||||
@@ -38,9 +38,10 @@ export class FriendsPage {
|
||||
await submit(this.page);
|
||||
}
|
||||
|
||||
/** Accepts the first pending request, the newest one. */
|
||||
async acceptRequest() {
|
||||
await waitForPOSTResponse(this.page, () =>
|
||||
this.locators.acceptButton.click(),
|
||||
this.locators.acceptButton.first().click(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
import type { Locator, Page } from "@playwright/test";
|
||||
import { FriendRow } from "./side-nav";
|
||||
|
||||
type Panel = "menu" | "friends" | "tourneys" | "chat" | "you";
|
||||
|
||||
@@ -12,18 +13,26 @@ const TAB_NAMES: Record<Panel, string> = {
|
||||
|
||||
const PANELS: Panel[] = ["menu", "friends", "tourneys", "chat", "you"];
|
||||
|
||||
/** The bottom tab bar and its panels, rendered in place of the side nav on mobile. */
|
||||
/**
|
||||
* The bottom tab bar and its panels, rendered in place of the side nav on mobile.
|
||||
*
|
||||
* The panels show the same rows as the side nav does, and its accessors find them
|
||||
* the same way — scoped to the panel that is open.
|
||||
*/
|
||||
export class MobileNav {
|
||||
private readonly page: Page;
|
||||
private readonly openPanelDialog: Locator;
|
||||
readonly locators;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.openPanelDialog = page.locator("[class*='panelDialog']:visible");
|
||||
this.locators = {
|
||||
menuPanel: page.getByLabel("Menu", { exact: true }),
|
||||
streamsHeading: page.locator("h3").filter({ hasText: "Streams" }),
|
||||
viewAllLink: page.getByRole("link", { name: "View all", exact: true }),
|
||||
youPanelUsername: page.locator("[class*='youPanelUsername']"),
|
||||
friendItems: this.openPanelDialog.locator("button[class*='listButton']"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -53,4 +62,16 @@ export class MobileNav {
|
||||
menuLink(name: string) {
|
||||
return this.locators.menuPanel.getByRole("link", { name });
|
||||
}
|
||||
|
||||
friend(name: string) {
|
||||
return new FriendRow(this.page, this.openPanelDialog, name);
|
||||
}
|
||||
|
||||
eventItem(name: string) {
|
||||
return this.openPanelDialog.getByRole("link", { name });
|
||||
}
|
||||
|
||||
streamItem(name: string) {
|
||||
return this.eventItem(name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,101 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
import type { Locator, Page } from "@playwright/test";
|
||||
import { waitForPOSTResponse } from "../../helpers/playwright";
|
||||
|
||||
type Section = "Events" | "Friends" | "Streams";
|
||||
|
||||
/** The sidebar of the site layout, a modal behind a hamburger on narrower viewports. */
|
||||
/**
|
||||
* The sidebar of the site layout, a modal behind a hamburger on narrower viewports.
|
||||
*
|
||||
* Its rows are looked up inside the sidebar the viewport shows — the rail, or the
|
||||
* copy of it the modal renders — so that the same row appearing on the page behind
|
||||
* it (a tournament also shown as a front page card) is not mistaken for one.
|
||||
*/
|
||||
export class SideNav {
|
||||
private readonly page: Page;
|
||||
private readonly root: Locator;
|
||||
readonly locators;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.root = page.locator("nav[class*='sideNav']:visible");
|
||||
this.locators = {
|
||||
collapseButton: page.getByTestId("sidenav-collapse-button"),
|
||||
modalTrigger: page.getByTestId("sidenav-modal-trigger"),
|
||||
notificationsButton: page.getByTestId("notifications-button"),
|
||||
viewAllLinks: page.getByRole("link", { name: /View all/ }),
|
||||
unseenRequestsBadge: page.getByRole("status", {
|
||||
name: /unseen friend request/,
|
||||
}),
|
||||
upcomingDivider: this.root.locator("[class*='upcomingDivider']"),
|
||||
/** Every row of the sidebar's link lists, in render order: events, then streams. */
|
||||
listItems: this.root.locator("a[class*='listLink']"),
|
||||
friendItems: this.root.locator("button[class*='listButton']"),
|
||||
};
|
||||
}
|
||||
|
||||
/** Where the unseen friend requests badge moves to while the sidebar is collapsed. */
|
||||
get collapsedRequestsBadge() {
|
||||
return this.locators.collapseButton.getByRole("status");
|
||||
}
|
||||
|
||||
sectionHeading(section: Section) {
|
||||
return this.page.getByRole("heading", { name: section });
|
||||
return this.root.getByRole("heading", { name: section });
|
||||
}
|
||||
|
||||
viewAllLink(section: "Events" | "Friends") {
|
||||
return this.sectionHeading(section)
|
||||
.locator("xpath=..")
|
||||
.getByRole("link", { name: /View all/ });
|
||||
}
|
||||
|
||||
/** Text shown in place of a section's rows when it has none. */
|
||||
emptyText(text: string) {
|
||||
return this.page.locator("[class*='sideNavEmpty']:visible", {
|
||||
hasText: text,
|
||||
});
|
||||
}
|
||||
|
||||
eventItem(name: string) {
|
||||
return this.root.getByRole("link", { name });
|
||||
}
|
||||
|
||||
streamItem(name: string) {
|
||||
return this.eventItem(name);
|
||||
}
|
||||
|
||||
itemSubtitle(name: string) {
|
||||
return this.eventItem(name).locator("span[class*='listLinkSubtitle']");
|
||||
}
|
||||
|
||||
itemBadge(name: string) {
|
||||
return this.eventItem(name).locator("span[class*='listLinkBadge']");
|
||||
}
|
||||
|
||||
/** The tier an upcoming tournament's stream row is pilled with. */
|
||||
itemTier(name: string) {
|
||||
return this.streamItem(name).getByTestId("confirmed-tier");
|
||||
}
|
||||
|
||||
saveStreamButton(streamName: string) {
|
||||
return this.streamItem(streamName).getByTitle("Save");
|
||||
}
|
||||
|
||||
savedStreamIcon(streamName: string) {
|
||||
return this.streamItem(streamName).locator("svg.lucide-bookmark-check");
|
||||
}
|
||||
|
||||
async saveStream(streamName: string) {
|
||||
await waitForPOSTResponse(this.page, () =>
|
||||
this.saveStreamButton(streamName).click(),
|
||||
);
|
||||
}
|
||||
|
||||
friend(name: string) {
|
||||
return new FriendRow(this.page, this.root, name);
|
||||
}
|
||||
|
||||
async toggleCollapse() {
|
||||
await this.locators.collapseButton.click();
|
||||
await waitForPOSTResponse(this.page, () =>
|
||||
this.locators.collapseButton.click(),
|
||||
);
|
||||
}
|
||||
|
||||
async openModal() {
|
||||
@@ -33,3 +106,32 @@ export class SideNav {
|
||||
await this.page.keyboard.press("Escape");
|
||||
}
|
||||
}
|
||||
|
||||
/** One friend of the friends list, and the menu opening from it. */
|
||||
export class FriendRow {
|
||||
private readonly page: Page;
|
||||
readonly trigger: Locator;
|
||||
readonly locators;
|
||||
|
||||
constructor(page: Page, root: Locator, name: string) {
|
||||
this.page = page;
|
||||
this.trigger = root.getByRole("button", { name });
|
||||
this.locators = {
|
||||
subtitle: this.trigger.locator("span[class*='listLinkSubtitle']"),
|
||||
badge: this.trigger.locator("span[class*='listLinkBadge']"),
|
||||
};
|
||||
}
|
||||
|
||||
async openMenu() {
|
||||
await this.trigger.click();
|
||||
}
|
||||
|
||||
/** Menu items render in a portal, so they are found on the page, not in the row. */
|
||||
menuItem(name: string) {
|
||||
return this.page.getByRole("menuitem", { name });
|
||||
}
|
||||
|
||||
watchStreamHref() {
|
||||
return this.menuItem("Watch stream").getAttribute("href");
|
||||
}
|
||||
}
|
||||
|
||||
176
e2e/sidebar-events.spec.ts
Normal file
176
e2e/sidebar-events.spec.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import { NZAP_TEST_ID } from "~/db/seed/constants";
|
||||
import { ADMIN_ID } from "~/features/admin/admin-constants";
|
||||
import { EVENTS_PAGE } from "~/utils/urls";
|
||||
import {
|
||||
expect,
|
||||
expectNoErrorPage,
|
||||
impersonate,
|
||||
test,
|
||||
} from "./helpers/playwright";
|
||||
import { expectTopToBottom, inHours, NO_EVENTS_TEXT } from "./helpers/sidebar";
|
||||
import { EventsPage } from "./pages/calendar/events-page";
|
||||
import { FrontPage } from "./pages/front-page/front-page";
|
||||
import { SideNav } from "./pages/layout/side-nav";
|
||||
|
||||
test.describe("Sidebar events", () => {
|
||||
test("shows my tournaments, my scrims and saved events, soonest first", async ({
|
||||
page,
|
||||
factories,
|
||||
}) => {
|
||||
const saved = await factories.TournamentFactory.create({
|
||||
authorId: ADMIN_ID,
|
||||
name: "Saved Tournament",
|
||||
startTimes: [inHours(1)],
|
||||
});
|
||||
await factories.SavedCalendarEventFactory.create({
|
||||
userId: NZAP_TEST_ID,
|
||||
tournamentId: saved.id,
|
||||
});
|
||||
|
||||
const played = await factories.TournamentFactory.create({
|
||||
authorId: ADMIN_ID,
|
||||
name: "Played Tournament",
|
||||
startTimes: [inHours(2)],
|
||||
});
|
||||
await factories.TournamentTeamFactory.create({
|
||||
tournamentId: played.id,
|
||||
memberUserIds: [NZAP_TEST_ID],
|
||||
});
|
||||
|
||||
await factories.ScrimPostFactory.create({
|
||||
startsAt: inHours(3),
|
||||
users: [{ userId: NZAP_TEST_ID, isOwner: 1 }],
|
||||
});
|
||||
|
||||
await factories.TournamentFactory.create({
|
||||
authorId: NZAP_TEST_ID,
|
||||
name: "Organized Tournament",
|
||||
startTimes: [inHours(4)],
|
||||
});
|
||||
|
||||
await impersonate(page, NZAP_TEST_ID);
|
||||
await new FrontPage(page).goto();
|
||||
|
||||
const sideNav = new SideNav(page);
|
||||
await expect(sideNav.locators.listItems).toHaveCount(4);
|
||||
await expectTopToBottom([
|
||||
sideNav.eventItem("Saved Tournament"),
|
||||
sideNav.eventItem("Played Tournament"),
|
||||
sideNav.eventItem("Looking for scrim"),
|
||||
sideNav.eventItem("Organized Tournament"),
|
||||
]);
|
||||
await expect(sideNav.itemSubtitle("Played Tournament")).toHaveText(
|
||||
/\d{1,2}:\d{2}/,
|
||||
);
|
||||
|
||||
await sideNav.eventItem("Played Tournament").click();
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`/to/${played.id}`));
|
||||
await expectNoErrorPage(page);
|
||||
});
|
||||
|
||||
test("scrim rows show their status and link to the right place", async ({
|
||||
page,
|
||||
factories,
|
||||
}) => {
|
||||
const [opponentOwner, otherPostOwner] =
|
||||
await factories.UserFactory.createMany(2);
|
||||
const opponentTeam = await factories.TeamFactory.create({
|
||||
name: "Squid Squad",
|
||||
memberUserIds: [opponentOwner.id],
|
||||
});
|
||||
|
||||
const booked = await factories.ScrimPostFactory.create(
|
||||
{
|
||||
startsAt: inHours(1),
|
||||
teamId: opponentTeam.id,
|
||||
users: [{ userId: opponentOwner.id, isOwner: 1 }],
|
||||
},
|
||||
{
|
||||
requests: [
|
||||
{ users: [{ userId: NZAP_TEST_ID, isOwner: 1 }], isAccepted: true },
|
||||
],
|
||||
},
|
||||
);
|
||||
await factories.ScrimPostFactory.create({
|
||||
startsAt: inHours(2),
|
||||
users: [{ userId: NZAP_TEST_ID, isOwner: 1 }],
|
||||
});
|
||||
const pending = await factories.ScrimPostFactory.create(
|
||||
{
|
||||
startsAt: inHours(3),
|
||||
users: [{ userId: otherPostOwner.id, isOwner: 1 }],
|
||||
},
|
||||
{ requests: [{ users: [{ userId: NZAP_TEST_ID, isOwner: 1 }] }] },
|
||||
);
|
||||
|
||||
await impersonate(page, NZAP_TEST_ID);
|
||||
const front = new FrontPage(page);
|
||||
await front.goto();
|
||||
|
||||
const sideNav = new SideNav(page);
|
||||
await expect(sideNav.eventItem("vs. Squid Squad")).toBeVisible();
|
||||
await expect(sideNav.eventItem("Looking for scrim")).toBeVisible();
|
||||
await expect(sideNav.eventItem("Request pending")).toBeVisible();
|
||||
|
||||
await sideNav.eventItem("vs. Squid Squad").click();
|
||||
await expect(page).toHaveURL(`/scrims/${booked.id}`);
|
||||
await expectNoErrorPage(page);
|
||||
|
||||
await front.goto();
|
||||
await sideNav.eventItem("Looking for scrim").click();
|
||||
await expect(page).toHaveURL("/scrims");
|
||||
|
||||
await front.goto();
|
||||
await expect(sideNav.eventItem("Request pending")).toHaveAttribute(
|
||||
"href",
|
||||
`/scrims?pendingRequestPostId=${pending.id}`,
|
||||
);
|
||||
|
||||
await sideNav.eventItem("Request pending").click();
|
||||
// the scrims page clears the param once it has scrolled to the post
|
||||
await expect(page).toHaveURL("/scrims");
|
||||
await expectNoErrorPage(page);
|
||||
});
|
||||
|
||||
test("caps at five events and View all opens the events page", async ({
|
||||
page,
|
||||
factories,
|
||||
}) => {
|
||||
for (const nth of [1, 2, 3, 4, 5, 6, 7]) {
|
||||
const tournament = await factories.TournamentFactory.create({
|
||||
authorId: ADMIN_ID,
|
||||
name: `Tournament ${nth}`,
|
||||
startTimes: [inHours(nth)],
|
||||
});
|
||||
await factories.TournamentTeamFactory.create({
|
||||
tournamentId: tournament.id,
|
||||
memberUserIds: [NZAP_TEST_ID],
|
||||
});
|
||||
}
|
||||
|
||||
await impersonate(page, NZAP_TEST_ID);
|
||||
await new FrontPage(page).goto();
|
||||
|
||||
const sideNav = new SideNav(page);
|
||||
await expect(sideNav.locators.listItems).toHaveCount(5);
|
||||
await expect(sideNav.eventItem("Tournament 5")).toBeVisible();
|
||||
await expect(sideNav.eventItem("Tournament 6")).toHaveCount(0);
|
||||
|
||||
await sideNav.viewAllLink("Events").click();
|
||||
await expect(page).toHaveURL(EVENTS_PAGE);
|
||||
|
||||
const events = new EventsPage(page);
|
||||
await expect(events.eventLink("Tournament 6")).toBeVisible();
|
||||
await expect(events.eventLink("Tournament 7")).toBeVisible();
|
||||
});
|
||||
|
||||
test("shows an empty state without upcoming events", async ({ page }) => {
|
||||
await impersonate(page, NZAP_TEST_ID);
|
||||
await new FrontPage(page).goto();
|
||||
|
||||
const sideNav = new SideNav(page);
|
||||
await expect(sideNav.emptyText(NO_EVENTS_TEXT)).toBeVisible();
|
||||
await expect(sideNav.viewAllLink("Events")).toBeVisible();
|
||||
});
|
||||
});
|
||||
625
e2e/sidebar-friends.spec.ts
Normal file
625
e2e/sidebar-friends.spec.ts
Normal file
@@ -0,0 +1,625 @@
|
||||
import { NZAP_TEST_ID } from "~/db/seed/constants";
|
||||
import { ADMIN_ID } from "~/features/admin/admin-constants";
|
||||
import {
|
||||
EVENTS_PAGE,
|
||||
FRIENDS_PAGE,
|
||||
SENDOUQ_LOOKING_PAGE,
|
||||
sendouQMatchPage,
|
||||
tournamentMatchPage,
|
||||
tournamentSubsPage,
|
||||
twitchUrl,
|
||||
} from "~/utils/urls";
|
||||
import {
|
||||
expect,
|
||||
expectNoErrorPage,
|
||||
impersonate,
|
||||
MOBILE_VIEWPORT,
|
||||
TABLET_VIEWPORT,
|
||||
test,
|
||||
waitForPOSTResponse,
|
||||
} from "./helpers/playwright";
|
||||
import {
|
||||
befriend,
|
||||
createNamedUsers,
|
||||
createUserIds,
|
||||
inHours,
|
||||
LOGGED_OUT_FRIENDS_TEXT,
|
||||
NO_FRIENDS_TEXT,
|
||||
streamAccountOf,
|
||||
} from "./helpers/sidebar";
|
||||
import {
|
||||
createInProgressMatch,
|
||||
createSubSeekingTournament,
|
||||
createTournamentWithByeTeam,
|
||||
} from "./helpers/tournament";
|
||||
import { FriendsPage } from "./pages/friends/friends-page";
|
||||
import { FrontPage } from "./pages/front-page/front-page";
|
||||
import { MobileNav } from "./pages/layout/mobile-nav";
|
||||
import { SideNav } from "./pages/layout/side-nav";
|
||||
|
||||
const CAST_ACCOUNT = "cast_account";
|
||||
|
||||
test.describe("Sidebar friends", () => {
|
||||
test("shows what each friend is doing right now", async ({
|
||||
page,
|
||||
factories,
|
||||
}) => {
|
||||
const [idle, queueing, sub, inMatch, groupMate] = await createNamedUsers(
|
||||
factories,
|
||||
["IdleFriend", "QueueFriend", "SubFriend", "MatchFriend", "GroupMate"],
|
||||
);
|
||||
await befriend(factories, [idle.id, queueing.id, sub.id, inMatch.id]);
|
||||
|
||||
await factories.SQGroupFactory.create({
|
||||
memberUserIds: [queueing.id, groupMate.id],
|
||||
});
|
||||
|
||||
await createSubSeekingTournament(factories, {
|
||||
name: "Sub Tournament",
|
||||
subId: sub.id,
|
||||
});
|
||||
|
||||
await createInProgressMatch(factories, {
|
||||
name: "Match Tournament",
|
||||
friendId: inMatch.id,
|
||||
});
|
||||
|
||||
await impersonate(page, NZAP_TEST_ID);
|
||||
await new FrontPage(page).goto();
|
||||
|
||||
const sideNav = new SideNav(page);
|
||||
|
||||
const idleRow = sideNav.friend("IdleFriend");
|
||||
await expect(idleRow.trigger).toBeVisible();
|
||||
await expect(idleRow.locators.subtitle).toHaveCount(0);
|
||||
await expect(idleRow.locators.badge).toHaveCount(0);
|
||||
|
||||
const queueingRow = sideNav.friend("QueueFriend");
|
||||
await expect(queueingRow.locators.subtitle).toHaveText("SendouQ");
|
||||
await expect(queueingRow.locators.badge).toHaveText("2/4");
|
||||
|
||||
const subRow = sideNav.friend("SubFriend");
|
||||
await expect(subRow.locators.subtitle).toHaveText("Sub Tournament");
|
||||
await expect(subRow.locators.badge).toHaveText("1/4");
|
||||
|
||||
const inMatchRow = sideNav.friend("MatchFriend");
|
||||
await expect(inMatchRow.locators.subtitle).toHaveText("Match Tournament");
|
||||
await expect(inMatchRow.locators.badge).toHaveText("Match");
|
||||
});
|
||||
|
||||
test("friend menu actions of SendouQ friends land on a real page", async ({
|
||||
page,
|
||||
factories,
|
||||
}) => {
|
||||
const [me, idle, queueing, inSendouQMatch] = await createNamedUsers(
|
||||
factories,
|
||||
["SidebarUser", "IdleFriend", "QueueFriend", "MatchFriend"],
|
||||
);
|
||||
await befriend(factories, [idle.id, queueing.id, inSendouQMatch.id], me.id);
|
||||
|
||||
await factories.SQGroupFactory.create({ memberUserIds: [queueing.id] });
|
||||
|
||||
const match = await factories.SQMatchFactory.create({
|
||||
alphaUserIds: [inSendouQMatch.id, ...(await createUserIds(factories, 3))],
|
||||
bravoUserIds: await createUserIds(factories, 4),
|
||||
});
|
||||
|
||||
await impersonate(page, me.id);
|
||||
const front = new FrontPage(page);
|
||||
await front.goto();
|
||||
|
||||
const sideNav = new SideNav(page);
|
||||
|
||||
const idleRow = sideNav.friend("IdleFriend");
|
||||
await idleRow.openMenu();
|
||||
await expect(idleRow.menuItem("Delete friend")).toHaveCount(0);
|
||||
await idleRow.menuItem("View user page").click();
|
||||
await expect(page).toHaveURL(/\/u\//);
|
||||
await expectNoErrorPage(page);
|
||||
|
||||
await front.goto();
|
||||
const matchRow = sideNav.friend("MatchFriend");
|
||||
await matchRow.openMenu();
|
||||
await matchRow.menuItem("View match").click();
|
||||
await expect(page).toHaveURL(sendouQMatchPage(match.id));
|
||||
await expectNoErrorPage(page);
|
||||
|
||||
await front.goto();
|
||||
const queueingRow = sideNav.friend("QueueFriend");
|
||||
await queueingRow.openMenu();
|
||||
await waitForPOSTResponse(page, () =>
|
||||
queueingRow.menuItem("Join SendouQ").click(),
|
||||
);
|
||||
await expect(page).toHaveURL(SENDOUQ_LOOKING_PAGE);
|
||||
await expectNoErrorPage(page);
|
||||
});
|
||||
|
||||
test("friend menu actions of tournament friends land on a real page", async ({
|
||||
page,
|
||||
factories,
|
||||
}) => {
|
||||
const [sub, inMatch, waiting] = await createNamedUsers(factories, [
|
||||
"SubFriend",
|
||||
"MatchFriend",
|
||||
"WaitingFriend",
|
||||
]);
|
||||
await befriend(factories, [sub.id, inMatch.id, waiting.id]);
|
||||
|
||||
const { tournament: subTournament } = await createSubSeekingTournament(
|
||||
factories,
|
||||
{ name: "Sub Tournament", subId: sub.id },
|
||||
);
|
||||
|
||||
const { tournament, matchId } = await createInProgressMatch(factories, {
|
||||
name: "Match Tournament",
|
||||
friendId: inMatch.id,
|
||||
});
|
||||
const { tournament: waitingTournament } = await createTournamentWithByeTeam(
|
||||
factories,
|
||||
{ name: "Waiting Tournament", waitingUserId: waiting.id },
|
||||
);
|
||||
|
||||
await impersonate(page, NZAP_TEST_ID);
|
||||
const front = new FrontPage(page);
|
||||
await front.goto();
|
||||
|
||||
const sideNav = new SideNav(page);
|
||||
|
||||
const subRow = sideNav.friend("SubFriend");
|
||||
await subRow.openMenu();
|
||||
await subRow.menuItem("View tournament").click();
|
||||
await expect(page).toHaveURL(tournamentSubsPage(subTournament.id));
|
||||
await expectNoErrorPage(page);
|
||||
|
||||
await front.goto();
|
||||
const waitingRow = sideNav.friend("WaitingFriend");
|
||||
await expect(waitingRow.locators.badge).toHaveText("Next");
|
||||
await waitingRow.openMenu();
|
||||
await waitingRow.menuItem("View tournament").click();
|
||||
await expect(page).toHaveURL(
|
||||
new RegExp(`/to/${waitingTournament.id}(/|$)`),
|
||||
);
|
||||
await expectNoErrorPage(page);
|
||||
|
||||
await front.goto();
|
||||
const inMatchRow = sideNav.friend("MatchFriend");
|
||||
await inMatchRow.openMenu();
|
||||
await inMatchRow.menuItem("View match").click();
|
||||
await expect(page).toHaveURL(
|
||||
tournamentMatchPage({ tournamentId: tournament.id, matchId }),
|
||||
);
|
||||
await expectNoErrorPage(page);
|
||||
});
|
||||
|
||||
test("shows four friends on desktop, eight in the mobile panel", async ({
|
||||
page,
|
||||
factories,
|
||||
}) => {
|
||||
const names = Array.from(
|
||||
{ length: 10 },
|
||||
(_, index) => `Friend${String(index + 1).padStart(2, "0")}`,
|
||||
);
|
||||
const friends = await createNamedUsers(factories, names);
|
||||
await befriend(
|
||||
factories,
|
||||
friends.map((friend) => friend.id),
|
||||
);
|
||||
|
||||
await impersonate(page, NZAP_TEST_ID);
|
||||
const front = new FrontPage(page);
|
||||
await front.goto();
|
||||
|
||||
const sideNav = new SideNav(page);
|
||||
await expect(sideNav.locators.friendItems).toHaveCount(4);
|
||||
|
||||
await page.setViewportSize(MOBILE_VIEWPORT);
|
||||
await front.goto();
|
||||
|
||||
const mobileNav = new MobileNav(page);
|
||||
await mobileNav.openPanel("friends");
|
||||
await expect(mobileNav.locators.friendItems).toHaveCount(8);
|
||||
|
||||
await mobileNav.locators.viewAllLink.click();
|
||||
await expect(page).toHaveURL(FRIENDS_PAGE);
|
||||
|
||||
const friendsPage = new FriendsPage(page);
|
||||
for (const name of names) {
|
||||
await expect(friendsPage.friendButton(name)).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test("unseen friend requests show as a badge", async ({
|
||||
page,
|
||||
factories,
|
||||
}) => {
|
||||
const requesters = await factories.UserFactory.createMany(2);
|
||||
for (const requester of requesters) {
|
||||
await factories.FriendRequestFactory.create({
|
||||
senderId: requester.id,
|
||||
receiverId: NZAP_TEST_ID,
|
||||
});
|
||||
}
|
||||
|
||||
await impersonate(page, NZAP_TEST_ID);
|
||||
const front = new FrontPage(page);
|
||||
await front.goto();
|
||||
|
||||
const sideNav = new SideNav(page);
|
||||
await expect(sideNav.locators.unseenRequestsBadge).toHaveText("2");
|
||||
|
||||
await sideNav.toggleCollapse();
|
||||
await expect(sideNav.collapsedRequestsBadge).toHaveText("2");
|
||||
|
||||
const friendsPage = new FriendsPage(page);
|
||||
await friendsPage.goto();
|
||||
await friendsPage.acceptRequest();
|
||||
|
||||
await front.goto();
|
||||
await expect(sideNav.locators.unseenRequestsBadge).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("shows an empty state for both logged out and friendless users", async ({
|
||||
page,
|
||||
}) => {
|
||||
const front = new FrontPage(page);
|
||||
await front.goto();
|
||||
|
||||
const sideNav = new SideNav(page);
|
||||
await expect(sideNav.emptyText(LOGGED_OUT_FRIENDS_TEXT)).toBeVisible();
|
||||
await expect(sideNav.viewAllLink("Friends")).toHaveCount(0);
|
||||
|
||||
await impersonate(page, NZAP_TEST_ID);
|
||||
await front.goto();
|
||||
|
||||
await expect(sideNav.emptyText(NO_FRIENDS_TEXT)).toBeVisible();
|
||||
await expect(sideNav.viewAllLink("Friends")).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Sidebar friend streams", () => {
|
||||
test("picks the best available stream for a friend's tournament match", async ({
|
||||
page,
|
||||
factories,
|
||||
}) => {
|
||||
const [friend, teammate, opponent] = await createNamedUsers(factories, [
|
||||
"StreamFriend",
|
||||
"StreamTeammate",
|
||||
"StreamOpponent",
|
||||
]);
|
||||
await befriend(factories, [friend.id]);
|
||||
|
||||
const { tournament, matchId } = await createInProgressMatch(factories, {
|
||||
name: "Match Tournament",
|
||||
friendId: friend.id,
|
||||
teammateId: teammate.id,
|
||||
opponentId: opponent.id,
|
||||
});
|
||||
await factories.TournamentFactory.castMatch({
|
||||
tournamentId: tournament.id,
|
||||
matchId,
|
||||
twitchAccount: CAST_ACCOUNT,
|
||||
});
|
||||
|
||||
await impersonate(page, NZAP_TEST_ID);
|
||||
const front = new FrontPage(page);
|
||||
const friendRow = new SideNav(page).friend("StreamFriend");
|
||||
|
||||
const expectWatchStream = async (account: string) => {
|
||||
await front.goto();
|
||||
await expect(friendRow.locators.badge).toHaveText("Live");
|
||||
await friendRow.openMenu();
|
||||
expect(await friendRow.watchStreamHref()).toBe(twitchUrl(account));
|
||||
};
|
||||
|
||||
await factories.LiveStreamFactory.replaceAll([{ userId: friend.id }]);
|
||||
await expectWatchStream(streamAccountOf(friend.id));
|
||||
|
||||
await factories.LiveStreamFactory.replaceAll([{ userId: teammate.id }]);
|
||||
await expectWatchStream(streamAccountOf(teammate.id));
|
||||
|
||||
await factories.LiveStreamFactory.replaceAll([
|
||||
{ userId: null, twitch: CAST_ACCOUNT },
|
||||
]);
|
||||
await expectWatchStream(CAST_ACCOUNT);
|
||||
|
||||
await factories.LiveStreamFactory.replaceAll([{ userId: opponent.id }]);
|
||||
await expectWatchStream(streamAccountOf(opponent.id));
|
||||
|
||||
await factories.LiveStreamFactory.replaceAll([
|
||||
{ userId: friend.id },
|
||||
{ userId: teammate.id },
|
||||
{ userId: opponent.id },
|
||||
{ userId: null, twitch: CAST_ACCOUNT },
|
||||
]);
|
||||
await expectWatchStream(streamAccountOf(friend.id));
|
||||
});
|
||||
|
||||
test("watch stream opens Twitch, not an in-app error page", async ({
|
||||
page,
|
||||
factories,
|
||||
}) => {
|
||||
const [friend] = await createNamedUsers(factories, ["StreamFriend"]);
|
||||
await befriend(factories, [friend.id]);
|
||||
|
||||
await createInProgressMatch(factories, {
|
||||
name: "Match Tournament",
|
||||
friendId: friend.id,
|
||||
});
|
||||
await factories.LiveStreamFactory.replaceAll([{ userId: friend.id }]);
|
||||
|
||||
// stubbed rather than aborted so the popup keeps the URL it was opened with
|
||||
await page.context().route("https://twitch.tv/**", (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: "text/html",
|
||||
body: "<html lang='en'><head><title>Twitch</title></head><body></body></html>",
|
||||
}),
|
||||
);
|
||||
|
||||
await impersonate(page, NZAP_TEST_ID);
|
||||
await new FrontPage(page).goto();
|
||||
|
||||
const friendRow = new SideNav(page).friend("StreamFriend");
|
||||
await friendRow.openMenu();
|
||||
|
||||
const watchStream = friendRow.menuItem("Watch stream");
|
||||
await expect(watchStream).toHaveAttribute(
|
||||
"href",
|
||||
twitchUrl(streamAccountOf(friend.id)),
|
||||
);
|
||||
await expect(watchStream).toHaveAttribute("target", "_blank");
|
||||
await expect(watchStream).toHaveAttribute("rel", "noreferrer");
|
||||
|
||||
const popupPromise = page
|
||||
.context()
|
||||
.waitForEvent("page", { timeout: 5_000 })
|
||||
.catch(() => null);
|
||||
await watchStream.click();
|
||||
const popup = await popupPromise;
|
||||
|
||||
if (popup) {
|
||||
expect(popup.url()).toContain("twitch.tv");
|
||||
await popup.close();
|
||||
}
|
||||
|
||||
await expect(page).toHaveURL("/");
|
||||
await expectNoErrorPage(page);
|
||||
});
|
||||
|
||||
test("shows no stream when nobody streams the friend's match", async ({
|
||||
page,
|
||||
factories,
|
||||
}) => {
|
||||
// the friend has a Twitch account on their profile throughout: only an
|
||||
// actually live stream may turn the row live
|
||||
const [friend, outsider] = await createNamedUsers(
|
||||
factories,
|
||||
["StreamFriend", "Outsider"],
|
||||
{ twitch: "friend_twitch" },
|
||||
);
|
||||
await befriend(factories, [friend.id]);
|
||||
|
||||
const { tournament, matchId } = await createInProgressMatch(factories, {
|
||||
name: "Match Tournament",
|
||||
friendId: friend.id,
|
||||
});
|
||||
|
||||
await impersonate(page, NZAP_TEST_ID);
|
||||
const front = new FrontPage(page);
|
||||
await front.goto();
|
||||
|
||||
const friendRow = new SideNav(page).friend("StreamFriend");
|
||||
await expect(friendRow.locators.badge).toHaveText("Match");
|
||||
await friendRow.openMenu();
|
||||
await expect(friendRow.menuItem("View user page")).toBeVisible();
|
||||
await expect(friendRow.menuItem("View match")).toHaveAttribute(
|
||||
"href",
|
||||
tournamentMatchPage({ tournamentId: tournament.id, matchId }),
|
||||
);
|
||||
await expect(friendRow.menuItem("Watch stream")).toHaveCount(0);
|
||||
|
||||
await factories.LiveStreamFactory.replaceAll([{ userId: outsider.id }]);
|
||||
await front.goto();
|
||||
|
||||
await expect(friendRow.locators.badge).toHaveText("Match");
|
||||
await friendRow.openMenu();
|
||||
await expect(friendRow.menuItem("Watch stream")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("a casted match whose caster is offline is not live", async ({
|
||||
page,
|
||||
factories,
|
||||
}) => {
|
||||
const [friend] = await createNamedUsers(factories, ["StreamFriend"]);
|
||||
await befriend(factories, [friend.id]);
|
||||
|
||||
const { tournament, matchId } = await createInProgressMatch(factories, {
|
||||
name: "Match Tournament",
|
||||
friendId: friend.id,
|
||||
});
|
||||
await factories.TournamentFactory.castMatch({
|
||||
tournamentId: tournament.id,
|
||||
matchId,
|
||||
twitchAccount: CAST_ACCOUNT,
|
||||
});
|
||||
|
||||
await impersonate(page, NZAP_TEST_ID);
|
||||
await new FrontPage(page).goto();
|
||||
|
||||
const friendRow = new SideNav(page).friend("StreamFriend");
|
||||
await expect(friendRow.locators.badge).toHaveText("Match");
|
||||
await friendRow.openMenu();
|
||||
await expect(friendRow.menuItem("Watch stream")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("a friend waiting for their next match is never live", async ({
|
||||
page,
|
||||
factories,
|
||||
}) => {
|
||||
const [friend] = await createNamedUsers(factories, ["WaitingFriend"]);
|
||||
await befriend(factories, [friend.id]);
|
||||
|
||||
await createTournamentWithByeTeam(factories, {
|
||||
name: "Waiting Tournament",
|
||||
waitingUserId: friend.id,
|
||||
});
|
||||
await factories.LiveStreamFactory.replaceAll([{ userId: friend.id }]);
|
||||
|
||||
await impersonate(page, NZAP_TEST_ID);
|
||||
await new FrontPage(page).goto();
|
||||
|
||||
const friendRow = new SideNav(page).friend("WaitingFriend");
|
||||
await expect(friendRow.locators.badge).toHaveText("Next");
|
||||
await friendRow.openMenu();
|
||||
await expect(friendRow.menuItem("View tournament")).toBeVisible();
|
||||
await expect(friendRow.menuItem("Watch stream")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("shows the stream of a friend's SendouQ match", async ({
|
||||
page,
|
||||
factories,
|
||||
}) => {
|
||||
const [friend, streamer] = await createNamedUsers(factories, [
|
||||
"MatchFriend",
|
||||
"MatchStreamer",
|
||||
]);
|
||||
await befriend(factories, [friend.id]);
|
||||
|
||||
const match = await factories.SQMatchFactory.create({
|
||||
alphaUserIds: [
|
||||
friend.id,
|
||||
streamer.id,
|
||||
...(await createUserIds(factories, 2)),
|
||||
],
|
||||
bravoUserIds: await createUserIds(factories, 4),
|
||||
});
|
||||
await factories.LiveStreamFactory.replaceAll([{ userId: streamer.id }]);
|
||||
|
||||
await impersonate(page, NZAP_TEST_ID);
|
||||
const front = new FrontPage(page);
|
||||
await front.goto();
|
||||
|
||||
const friendRow = new SideNav(page).friend("MatchFriend");
|
||||
await expect(friendRow.locators.badge).toHaveText("Live");
|
||||
await friendRow.openMenu();
|
||||
expect(await friendRow.watchStreamHref()).toBe(
|
||||
twitchUrl(streamAccountOf(streamer.id)),
|
||||
);
|
||||
|
||||
await factories.LiveStreamFactory.replaceAll([]);
|
||||
await front.goto();
|
||||
|
||||
await expect(friendRow.locators.badge).toHaveText("Match");
|
||||
await friendRow.openMenu();
|
||||
await expect(friendRow.menuItem("Watch stream")).toHaveCount(0);
|
||||
await friendRow.menuItem("View match").click();
|
||||
|
||||
await expect(page).toHaveURL(sendouQMatchPage(match.id));
|
||||
await expectNoErrorPage(page);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Sidebar form factors", () => {
|
||||
test("collapsing the sidebar sticks across reloads", async ({ page }) => {
|
||||
await impersonate(page, NZAP_TEST_ID);
|
||||
const front = new FrontPage(page);
|
||||
await front.goto();
|
||||
|
||||
const sideNav = new SideNav(page);
|
||||
await sideNav.toggleCollapse();
|
||||
await expect(sideNav.sectionHeading("Events")).toHaveCount(0);
|
||||
|
||||
await front.goto();
|
||||
await expect(sideNav.sectionHeading("Events")).toHaveCount(0);
|
||||
|
||||
await sideNav.toggleCollapse();
|
||||
await expect(sideNav.sectionHeading("Events")).toBeVisible();
|
||||
|
||||
await front.goto();
|
||||
await expect(sideNav.sectionHeading("Events")).toBeVisible();
|
||||
});
|
||||
|
||||
test("tablet shows the same sidebar in a modal", async ({
|
||||
page,
|
||||
factories,
|
||||
}) => {
|
||||
const [friend] = await createNamedUsers(factories, ["ModalFriend"]);
|
||||
await befriend(factories, [friend.id]);
|
||||
|
||||
await page.setViewportSize(TABLET_VIEWPORT);
|
||||
await impersonate(page, NZAP_TEST_ID);
|
||||
await new FrontPage(page).goto();
|
||||
|
||||
const sideNav = new SideNav(page);
|
||||
await expect(sideNav.sectionHeading("Events")).toHaveCount(0);
|
||||
|
||||
await sideNav.openModal();
|
||||
await expect(sideNav.sectionHeading("Events")).toBeVisible();
|
||||
await expect(sideNav.sectionHeading("Friends")).toBeVisible();
|
||||
await expect(sideNav.sectionHeading("Streams")).toBeVisible();
|
||||
await expect(sideNav.friend("ModalFriend").trigger).toBeVisible();
|
||||
|
||||
await sideNav.closeModal();
|
||||
await expect(sideNav.sectionHeading("Events")).toHaveCount(0);
|
||||
|
||||
await sideNav.openModal();
|
||||
const friendRow = sideNav.friend("ModalFriend");
|
||||
await friendRow.openMenu();
|
||||
await friendRow.menuItem("View user page").click();
|
||||
|
||||
await expect(page).toHaveURL(/\/u\//);
|
||||
await expect(sideNav.sectionHeading("Events")).toHaveCount(0);
|
||||
await expectNoErrorPage(page);
|
||||
});
|
||||
|
||||
test("mobile panels show the same data as the desktop sidebar", async ({
|
||||
page,
|
||||
factories,
|
||||
}) => {
|
||||
const [friend] = await createNamedUsers(factories, ["StreamFriend"]);
|
||||
await befriend(factories, [friend.id]);
|
||||
|
||||
await createInProgressMatch(factories, {
|
||||
name: "Streamed Tournament",
|
||||
friendId: friend.id,
|
||||
});
|
||||
await factories.LiveStreamFactory.replaceAll([{ userId: friend.id }]);
|
||||
|
||||
const tournament = await factories.TournamentFactory.create({
|
||||
authorId: ADMIN_ID,
|
||||
name: "My Tournament",
|
||||
startTimes: [inHours(2)],
|
||||
});
|
||||
await factories.TournamentTeamFactory.create({
|
||||
tournamentId: tournament.id,
|
||||
memberUserIds: [NZAP_TEST_ID],
|
||||
});
|
||||
|
||||
await page.setViewportSize(MOBILE_VIEWPORT);
|
||||
await impersonate(page, NZAP_TEST_ID);
|
||||
await new FrontPage(page).goto();
|
||||
|
||||
const mobileNav = new MobileNav(page);
|
||||
await mobileNav.openPanel("friends");
|
||||
|
||||
const friendRow = mobileNav.friend("StreamFriend");
|
||||
await expect(friendRow.locators.badge).toHaveText("Live");
|
||||
await friendRow.openMenu();
|
||||
expect(await friendRow.watchStreamHref()).toBe(
|
||||
twitchUrl(streamAccountOf(friend.id)),
|
||||
);
|
||||
await page.keyboard.press("Escape");
|
||||
|
||||
await mobileNav.switchPanel("tourneys");
|
||||
await expect(mobileNav.eventItem("My Tournament")).toBeVisible();
|
||||
|
||||
await mobileNav.switchPanel("menu");
|
||||
await expect(mobileNav.locators.streamsHeading).toBeVisible();
|
||||
await expect(mobileNav.streamItem("Streamed Tournament")).toBeVisible();
|
||||
|
||||
await mobileNav.switchPanel("tourneys");
|
||||
await mobileNav.locators.viewAllLink.click();
|
||||
await expect(page).toHaveURL(EVENTS_PAGE);
|
||||
await expect(mobileNav.locators.viewAllLink).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
212
e2e/sidebar-streams.spec.ts
Normal file
212
e2e/sidebar-streams.spec.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
import { NZAP_TEST_ID } from "~/db/seed/constants";
|
||||
import { ADMIN_ID } from "~/features/admin/admin-constants";
|
||||
import { tournamentStreamsPage, twitchUrl } from "~/utils/urls";
|
||||
import {
|
||||
expect,
|
||||
expectNoErrorPage,
|
||||
impersonate,
|
||||
test,
|
||||
} from "./helpers/playwright";
|
||||
import {
|
||||
createNamedUsers,
|
||||
createUserIds,
|
||||
expectTopToBottom,
|
||||
inDays,
|
||||
LOGGED_OUT_FRIENDS_TEXT,
|
||||
NO_EVENTS_TEXT,
|
||||
NO_STREAMS_TEXT,
|
||||
} from "./helpers/sidebar";
|
||||
import { createInProgressMatch } from "./helpers/tournament";
|
||||
import { FrontPage } from "./pages/front-page/front-page";
|
||||
import { SideNav } from "./pages/layout/side-nav";
|
||||
import { TournamentStreamsPage } from "./pages/tournament/tournament-streams-page";
|
||||
|
||||
const XRANK_TWITCH = "xrank_streamer";
|
||||
const TOP_PLAYER_XP = 4000;
|
||||
|
||||
test.describe("Sidebar streams", () => {
|
||||
test("live tournament streams link to the tournament's streams page", async ({
|
||||
page,
|
||||
factories,
|
||||
}) => {
|
||||
const [streamer] = await createNamedUsers(factories, [
|
||||
"TournamentStreamer",
|
||||
]);
|
||||
const { tournament } = await createInProgressMatch(factories, {
|
||||
name: "Streamed Tournament",
|
||||
friendId: streamer.id,
|
||||
});
|
||||
await factories.LiveStreamFactory.replaceAll([{ userId: streamer.id }]);
|
||||
|
||||
await impersonate(page, NZAP_TEST_ID);
|
||||
await new FrontPage(page).goto();
|
||||
|
||||
const sideNav = new SideNav(page);
|
||||
await expect(sideNav.itemBadge("Streamed Tournament")).toHaveText("LIVE");
|
||||
await expect(sideNav.itemSubtitle("Streamed Tournament")).not.toBeEmpty();
|
||||
|
||||
await sideNav.streamItem("Streamed Tournament").click();
|
||||
await expect(page).toHaveURL(tournamentStreamsPage(tournament.id));
|
||||
|
||||
await expect(
|
||||
new TournamentStreamsPage(page).locators.streams.first(),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("x rank streamers link straight to Twitch", async ({
|
||||
page,
|
||||
factories,
|
||||
}) => {
|
||||
const [topPlayer] = await createNamedUsers(factories, ["TopPlayer"], {
|
||||
twitch: XRANK_TWITCH,
|
||||
});
|
||||
await factories.XRankPlacementFactory.create(
|
||||
{ playerUserId: topPlayer.id, power: TOP_PLAYER_XP },
|
||||
{ refreshPeakXp: true },
|
||||
);
|
||||
await factories.LiveStreamFactory.replaceAll([
|
||||
{ userId: topPlayer.id, twitch: XRANK_TWITCH },
|
||||
]);
|
||||
|
||||
await impersonate(page, NZAP_TEST_ID);
|
||||
await new FrontPage(page).goto();
|
||||
|
||||
const sideNav = new SideNav(page);
|
||||
await expect(sideNav.itemSubtitle("TopPlayer")).toHaveText(
|
||||
String(TOP_PLAYER_XP),
|
||||
);
|
||||
await expect(sideNav.streamItem("TopPlayer")).toHaveAttribute(
|
||||
"href",
|
||||
twitchUrl(XRANK_TWITCH),
|
||||
);
|
||||
});
|
||||
|
||||
test("upcoming tournaments sit under the Upcoming divider and can be saved", async ({
|
||||
page,
|
||||
factories,
|
||||
}) => {
|
||||
const [topPlayer] = await createNamedUsers(factories, ["TopPlayer"], {
|
||||
twitch: XRANK_TWITCH,
|
||||
});
|
||||
await factories.XRankPlacementFactory.create(
|
||||
{ playerUserId: topPlayer.id, power: TOP_PLAYER_XP },
|
||||
{ refreshPeakXp: true },
|
||||
);
|
||||
await factories.LiveStreamFactory.replaceAll([
|
||||
{ userId: topPlayer.id, twitch: XRANK_TWITCH },
|
||||
]);
|
||||
|
||||
await factories.TournamentFactory.create(
|
||||
{
|
||||
authorId: ADMIN_ID,
|
||||
name: "Upcoming Tournament",
|
||||
startTimes: [inDays(2)],
|
||||
},
|
||||
{ tier: 1 },
|
||||
);
|
||||
|
||||
await impersonate(page, NZAP_TEST_ID);
|
||||
await new FrontPage(page).goto();
|
||||
|
||||
const sideNav = new SideNav(page);
|
||||
await expectTopToBottom([
|
||||
sideNav.streamItem("TopPlayer"),
|
||||
sideNav.locators.upcomingDivider,
|
||||
sideNav.streamItem("Upcoming Tournament"),
|
||||
]);
|
||||
await expect(sideNav.itemTier("Upcoming Tournament")).toBeVisible();
|
||||
|
||||
await sideNav.saveStream("Upcoming Tournament");
|
||||
await expect(sideNav.savedStreamIcon("Upcoming Tournament")).toBeVisible();
|
||||
await expect(sideNav.eventItem("Upcoming Tournament")).toHaveCount(2);
|
||||
|
||||
await sideNav.saveStream("Upcoming Tournament");
|
||||
await expect(sideNav.savedStreamIcon("Upcoming Tournament")).toHaveCount(0);
|
||||
await expect(sideNav.eventItem("Upcoming Tournament")).toHaveCount(1);
|
||||
});
|
||||
|
||||
test("shows at most five streams, best first", async ({
|
||||
page,
|
||||
factories,
|
||||
}) => {
|
||||
const [highTierStreamer, lowTierStreamer, sendouQStreamer] =
|
||||
await createNamedUsers(factories, [
|
||||
"HighTierStreamer",
|
||||
"LowTierStreamer",
|
||||
"SendouQStreamer",
|
||||
]);
|
||||
|
||||
await createInProgressMatch(factories, {
|
||||
name: "High Tier Tournament",
|
||||
friendId: highTierStreamer.id,
|
||||
tier: 1,
|
||||
});
|
||||
await createInProgressMatch(factories, {
|
||||
name: "Low Tier Tournament",
|
||||
friendId: lowTierStreamer.id,
|
||||
tier: 3,
|
||||
});
|
||||
await factories.SQMatchFactory.create({
|
||||
alphaUserIds: [
|
||||
sendouQStreamer.id,
|
||||
...(await createUserIds(factories, 3)),
|
||||
],
|
||||
bravoUserIds: await createUserIds(factories, 4),
|
||||
});
|
||||
await factories.LiveStreamFactory.replaceAll([
|
||||
{ userId: highTierStreamer.id },
|
||||
{ userId: lowTierStreamer.id },
|
||||
{ userId: sendouQStreamer.id },
|
||||
]);
|
||||
|
||||
for (const nth of [1, 2, 3]) {
|
||||
await factories.TournamentFactory.create(
|
||||
{
|
||||
authorId: ADMIN_ID,
|
||||
name: `Upcoming Tournament ${nth}`,
|
||||
startTimes: [inDays(1)],
|
||||
},
|
||||
{ tier: 1 },
|
||||
);
|
||||
}
|
||||
|
||||
await impersonate(page, NZAP_TEST_ID);
|
||||
await new FrontPage(page).goto();
|
||||
|
||||
const sideNav = new SideNav(page);
|
||||
await expect(sideNav.locators.listItems).toHaveCount(5);
|
||||
await expectTopToBottom([
|
||||
sideNav.streamItem("High Tier Tournament"),
|
||||
sideNav.streamItem("Low Tier Tournament"),
|
||||
sideNav.locators.upcomingDivider,
|
||||
]);
|
||||
});
|
||||
|
||||
test("shows an empty state without streams", async ({ page }) => {
|
||||
await impersonate(page, NZAP_TEST_ID);
|
||||
await new FrontPage(page).goto();
|
||||
|
||||
await expect(new SideNav(page).emptyText(NO_STREAMS_TEXT)).toBeVisible();
|
||||
});
|
||||
|
||||
test("shows streams to logged out visitors", async ({ page, factories }) => {
|
||||
const [streamer] = await createNamedUsers(factories, [
|
||||
"TournamentStreamer",
|
||||
]);
|
||||
const { tournament } = await createInProgressMatch(factories, {
|
||||
name: "Streamed Tournament",
|
||||
friendId: streamer.id,
|
||||
});
|
||||
await factories.LiveStreamFactory.replaceAll([{ userId: streamer.id }]);
|
||||
|
||||
await new FrontPage(page).goto();
|
||||
|
||||
const sideNav = new SideNav(page);
|
||||
await expect(sideNav.emptyText(NO_EVENTS_TEXT)).toBeVisible();
|
||||
await expect(sideNav.emptyText(LOGGED_OUT_FRIENDS_TEXT)).toBeVisible();
|
||||
|
||||
await sideNav.streamItem("Streamed Tournament").click();
|
||||
await expect(page).toHaveURL(tournamentStreamsPage(tournament.id));
|
||||
await expectNoErrorPage(page);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user