mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-07 11:46:09 -05:00
Initial builds E2E tests
This commit is contained in:
@@ -84,7 +84,10 @@ export function BuildCard({ build, owner, canEdit = false }: BuildProps) {
|
||||
} = build;
|
||||
|
||||
return (
|
||||
<div className={clsx("build", { build__private: build.private })}>
|
||||
<div
|
||||
className={clsx("build", { build__private: build.private })}
|
||||
data-testid="build-card"
|
||||
>
|
||||
<div>
|
||||
<div className="build__top-row">
|
||||
{modes && modes.length > 0 && (
|
||||
@@ -101,7 +104,9 @@ export function BuildCard({ build, owner, canEdit = false }: BuildProps) {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<h2 className="build__title">{title}</h2>
|
||||
<h2 className="build__title" data-testid="build-title">
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="build__date-author-row">
|
||||
{owner && (
|
||||
@@ -193,6 +198,7 @@ export function BuildCard({ build, owner, canEdit = false }: BuildProps) {
|
||||
variant="minimal"
|
||||
size="tiny"
|
||||
to={`new?buildId=${id}&userId=${user!.id}`}
|
||||
testId="edit-build"
|
||||
>
|
||||
<EditIcon className="build__icon" />
|
||||
</LinkButton>
|
||||
|
||||
@@ -221,6 +221,7 @@ export function UserCombobox({
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: [object Object] flickers when server rendered with initialValue
|
||||
export function WeaponCombobox({
|
||||
id,
|
||||
required,
|
||||
|
||||
@@ -3,4 +3,8 @@ select
|
||||
from
|
||||
"Build"
|
||||
where
|
||||
"ownerId" = @userId;
|
||||
"Build"."ownerId" = @userId
|
||||
and (
|
||||
"Build"."private" = 0
|
||||
or "Build"."ownerId" = @loggedInUserId
|
||||
)
|
||||
|
||||
@@ -90,8 +90,15 @@ export const updateByReplacing = sql.transaction(
|
||||
}
|
||||
);
|
||||
|
||||
export function countByUserId(userId: Build["ownerId"]) {
|
||||
return (countByUserIdStm.get({ userId })?.count ?? 0) as number;
|
||||
export function countByUserId({
|
||||
userId,
|
||||
loggedInUserId,
|
||||
}: {
|
||||
userId: Build["ownerId"];
|
||||
loggedInUserId?: UserWithPlusTier["id"];
|
||||
}) {
|
||||
return (countByUserIdStm.get({ userId, loggedInUserId })?.count ??
|
||||
0) as number;
|
||||
}
|
||||
|
||||
export interface BuildWeaponWithTop500Info {
|
||||
|
||||
7
app/db/seed/constants.ts
Normal file
7
app/db/seed/constants.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const ADMIN_TEST_AVATAR = "f34d6169979e60dfe63de0f96c8050f3";
|
||||
|
||||
export const NZAP_TEST_DISCORD_ID = "455039198672453645";
|
||||
export const NZAP_TEST_AVATAR = "f809176af93132c3db5f0a5019e96339"; // https://cdn.discordapp.com/avatars/455039198672453645/f809176af93132c3db5f0a5019e96339.webp?size=160
|
||||
export const NZAP_TEST_ID = 2;
|
||||
|
||||
export const AMOUNT_OF_CALENDAR_EVENTS = 200;
|
||||
@@ -30,14 +30,13 @@ import { mySlugify } from "~/utils/urls";
|
||||
import { createVod } from "~/features/vods/queries/createVod.server";
|
||||
|
||||
import placements from "./placements.json";
|
||||
|
||||
const ADMIN_TEST_AVATAR = "f34d6169979e60dfe63de0f96c8050f3";
|
||||
|
||||
const NZAP_TEST_DISCORD_ID = "455039198672453645";
|
||||
const NZAP_TEST_AVATAR = "f809176af93132c3db5f0a5019e96339"; // https://cdn.discordapp.com/avatars/455039198672453645/f809176af93132c3db5f0a5019e96339.webp?size=160
|
||||
const NZAP_TEST_ID = 2;
|
||||
|
||||
const AMOUNT_OF_CALENDAR_EVENTS = 200;
|
||||
import {
|
||||
NZAP_TEST_DISCORD_ID,
|
||||
ADMIN_TEST_AVATAR,
|
||||
NZAP_TEST_AVATAR,
|
||||
NZAP_TEST_ID,
|
||||
AMOUNT_OF_CALENDAR_EVENTS,
|
||||
} from "./constants";
|
||||
|
||||
const calendarEventWithToToolsSz = () => calendarEventWithToTools(true);
|
||||
const calendarEventWithToToolsTeamsSz = () =>
|
||||
@@ -875,7 +874,9 @@ function adminBuilds() {
|
||||
function manySplattershotBuilds() {
|
||||
// ensure 500 has at least one splattershot build for x placement test
|
||||
const users = [
|
||||
...userIdsInRandomOrder().filter((id) => id !== 500 && id !== ADMIN_ID),
|
||||
...userIdsInRandomOrder().filter(
|
||||
(id) => id !== 500 && id !== ADMIN_ID && id !== NZAP_TEST_ID
|
||||
),
|
||||
500,
|
||||
];
|
||||
|
||||
|
||||
@@ -162,7 +162,7 @@ export const action: ActionFunction = async ({ request }) => {
|
||||
};
|
||||
|
||||
// TODO: messing with these and "one mode selection" can cause problems when teams
|
||||
// have already chosend maps for their pools
|
||||
// have already chosen maps for their pools
|
||||
const deserializedMaps = (() => {
|
||||
if (!data.pool) return;
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import { userTopPlacements } from "~/features/top-search";
|
||||
import { findVods } from "~/features/vods";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
import { useUser } from "~/modules/auth";
|
||||
import { getUserId } from "~/modules/auth/user.server";
|
||||
import { canAddCustomizedColorsToUserProfile } from "~/permissions";
|
||||
import styles from "~/styles/u.css";
|
||||
import { notFoundIfFalsy, type SendouRouteHandle } from "~/utils/remix";
|
||||
@@ -69,7 +70,8 @@ export const userParamsSchema = z.object({ identifier: z.string() });
|
||||
|
||||
export type UserPageLoaderData = SerializeFrom<typeof loader>;
|
||||
|
||||
export const loader = ({ params }: LoaderArgs) => {
|
||||
export const loader = async ({ params, request }: LoaderArgs) => {
|
||||
const loggedInUser = await getUserId(request);
|
||||
const { identifier } = userParamsSchema.parse(params);
|
||||
const user = notFoundIfFalsy(db.users.findByIdentifier(identifier));
|
||||
|
||||
@@ -94,7 +96,10 @@ export const loader = ({ params }: LoaderArgs) => {
|
||||
css: canAddCustomizedColorsToUserProfile(user) ? user.css : undefined,
|
||||
badges: db.badges.findByOwnerId(user.id),
|
||||
results: db.calendarEvents.findResultsByUserId(user.id),
|
||||
buildsCount: db.builds.countByUserId(user.id),
|
||||
buildsCount: db.builds.countByUserId({
|
||||
userId: user.id,
|
||||
loggedInUserId: loggedInUser?.id,
|
||||
}),
|
||||
vods: findVods({ userId: user.id }),
|
||||
playerId,
|
||||
topPlacements,
|
||||
@@ -125,7 +130,11 @@ export default function UserPageLayout() {
|
||||
</SubNavLink>
|
||||
)}
|
||||
{(isOwnPage || data.buildsCount > 0) && (
|
||||
<SubNavLink to={userBuildsPage(data)} prefetch="intent">
|
||||
<SubNavLink
|
||||
to={userBuildsPage(data)}
|
||||
prefetch="intent"
|
||||
data-testid="builds-tab"
|
||||
>
|
||||
{t("pages.builds")} ({data.buildsCount})
|
||||
</SubNavLink>
|
||||
)}
|
||||
|
||||
@@ -77,7 +77,11 @@ export default function UserBuildsPage() {
|
||||
{isOwnPage && (
|
||||
<div className="stack sm horizontal items-center justify-end">
|
||||
{data.builds.length < BUILD.MAX_COUNT ? (
|
||||
<LinkButton to={userNewBuildPage(parentPageData)} size="tiny">
|
||||
<LinkButton
|
||||
to={userNewBuildPage(parentPageData)}
|
||||
size="tiny"
|
||||
testId="new-build-button"
|
||||
>
|
||||
{t("addBuild")}
|
||||
</LinkButton>
|
||||
) : (
|
||||
|
||||
@@ -228,6 +228,7 @@ function TitleInput() {
|
||||
{t("forms.title")}
|
||||
</Label>
|
||||
<input
|
||||
id="title"
|
||||
name="title"
|
||||
required
|
||||
minLength={BUILD.TITLE_MIN_LENGTH}
|
||||
@@ -285,7 +286,7 @@ function ModeCheckboxes() {
|
||||
name={mode}
|
||||
type="checkbox"
|
||||
defaultChecked={modes.includes(mode)}
|
||||
data-cy={`${mode}-checkbox`}
|
||||
data-testid={`${mode}-checkbox`}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
@@ -357,6 +358,7 @@ function WeaponsSelector() {
|
||||
disabled={weapons.length === BUILD.MAX_WEAPONS_COUNT}
|
||||
onClick={() => setWeapons((weapons) => [...weapons, 0])}
|
||||
icon={<PlusIcon />}
|
||||
testId="add-weapon-button"
|
||||
/>
|
||||
{weapons.length > 1 && (
|
||||
<Button
|
||||
|
||||
@@ -9,10 +9,27 @@ export async function selectWeapon({
|
||||
name: string;
|
||||
inputName?: string;
|
||||
}) {
|
||||
const weaponCombobox = page.getByTestId(`${inputName}-combobox-input`);
|
||||
await weaponCombobox.clear();
|
||||
await weaponCombobox.fill(name);
|
||||
await weaponCombobox.press("Enter");
|
||||
return selectComboboxValue({ page, value: name, inputName });
|
||||
}
|
||||
|
||||
export async function selectComboboxValue({
|
||||
page,
|
||||
value,
|
||||
inputName,
|
||||
locator,
|
||||
}: {
|
||||
page: Page;
|
||||
value: string;
|
||||
inputName?: string;
|
||||
locator?: Locator;
|
||||
}) {
|
||||
if (!locator && !inputName) {
|
||||
throw new Error("Must provide either locator or inputName");
|
||||
}
|
||||
const combobox = locator ?? page.getByTestId(`${inputName!}-combobox-input`);
|
||||
await combobox.clear();
|
||||
await combobox.fill(value);
|
||||
await combobox.press("Enter");
|
||||
}
|
||||
|
||||
/** page.goto that waits for the page to be hydrated before proceeding */
|
||||
|
||||
99
e2e/builds.spec.ts
Normal file
99
e2e/builds.spec.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { ADMIN_DISCORD_ID } from "~/constants";
|
||||
import { NZAP_TEST_DISCORD_ID, NZAP_TEST_ID } from "~/db/seed/constants";
|
||||
import {
|
||||
impersonate,
|
||||
navigate,
|
||||
seed,
|
||||
selectComboboxValue,
|
||||
} from "~/utils/playwright";
|
||||
import { userBuildsPage, userNewBuildPage } from "~/utils/urls";
|
||||
|
||||
test.describe("Builds", () => {
|
||||
test("adds a build", async ({ page }) => {
|
||||
await seed(page);
|
||||
await impersonate(page, NZAP_TEST_ID);
|
||||
await navigate({
|
||||
page,
|
||||
url: userNewBuildPage({ discordId: NZAP_TEST_DISCORD_ID }),
|
||||
});
|
||||
|
||||
await selectComboboxValue({
|
||||
locator: page.getByTestId("weapon-combobox-input").first(),
|
||||
value: "Tenta Brella",
|
||||
page,
|
||||
});
|
||||
await page.getByTestId("add-weapon-button").click();
|
||||
await selectComboboxValue({
|
||||
locator: page.getByTestId("weapon-combobox-input").nth(1),
|
||||
value: "Splat Brella",
|
||||
page,
|
||||
});
|
||||
|
||||
await selectComboboxValue({
|
||||
inputName: "HEAD",
|
||||
value: "White Headband",
|
||||
page,
|
||||
});
|
||||
await selectComboboxValue({
|
||||
inputName: "CLOTHES",
|
||||
value: "Basic Tee",
|
||||
page,
|
||||
});
|
||||
await selectComboboxValue({
|
||||
inputName: "SHOES",
|
||||
value: "Blue Lo-Tops",
|
||||
page,
|
||||
});
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
await page.getByTestId("ISM-ability-button").click();
|
||||
}
|
||||
|
||||
await page.getByLabel("Title").fill("Test Build");
|
||||
await page.getByLabel("Description").fill("Test Description");
|
||||
await page.getByTestId("SZ-checkbox").click();
|
||||
|
||||
await page.getByTestId("submit-button").click();
|
||||
|
||||
await expect(page.getByTestId("new-build-button")).toBeVisible();
|
||||
|
||||
await expect(page.getByAltText("Tenta Brella")).toBeVisible();
|
||||
await expect(page.getByAltText("Splat Brella")).toBeVisible();
|
||||
|
||||
await expect(page.getByAltText("Tower Control")).toBeVisible();
|
||||
await expect(page.getByAltText("Splat Zones")).not.toBeVisible();
|
||||
|
||||
await expect(page.getByTestId("build-title")).toContainText("Test Build");
|
||||
});
|
||||
|
||||
test("makes build private", async ({ page }) => {
|
||||
await seed(page);
|
||||
await impersonate(page);
|
||||
await navigate({
|
||||
page,
|
||||
url: userBuildsPage({ discordId: ADMIN_DISCORD_ID }),
|
||||
});
|
||||
|
||||
await page.getByTestId("edit-build").first().click();
|
||||
|
||||
await page.getByLabel("Private").click();
|
||||
|
||||
await page.getByTestId("submit-button").click();
|
||||
|
||||
await expect(page.getByTestId("builds-tab")).toContainText("Builds (50)");
|
||||
await expect(page.getByTestId("build-card").first()).toContainText(
|
||||
"Private"
|
||||
);
|
||||
|
||||
await impersonate(page, NZAP_TEST_ID);
|
||||
await navigate({
|
||||
page,
|
||||
url: userBuildsPage({ discordId: ADMIN_DISCORD_ID }),
|
||||
});
|
||||
await expect(page.getByTestId("builds-tab")).toContainText("Builds (49)");
|
||||
await expect(page.getByTestId("build-card").first()).not.toContainText(
|
||||
"Private"
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user