mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-03-21 18:04:39 -05:00
Add Vitest browser testing setup with GroupCard tests (#2694)
This commit is contained in:
parent
73e13f7446
commit
ee4f3ef4b9
5
.github/workflows/main.yml
vendored
5
.github/workflows/main.yml
vendored
|
|
@ -27,12 +27,15 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: npx playwright install chromium
|
||||
|
||||
- name: Formatter/Linter
|
||||
run: npm run biome:check
|
||||
- name: Typecheck
|
||||
run: npm run typecheck
|
||||
- name: Unit tests
|
||||
run: npm run test:unit
|
||||
run: npm run test:unit:browser
|
||||
- name: Knip unused check
|
||||
run: npm run knip
|
||||
- name: Check translations jsons
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -6,6 +6,7 @@ node_modules
|
|||
.react-router/
|
||||
.env
|
||||
translation-progress.md
|
||||
**/*/__screenshots__
|
||||
|
||||
notes.md
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
- `npm run typecheck` runs TypeScript type checking
|
||||
- `npm run biome:fix` runs Biome code formatter and linter
|
||||
- `npm run test:unit` runs all unit tests
|
||||
- `npm run test:unit:browser` runs all unit tests and browser tests
|
||||
- `npm run test:e2e` runs all e2e tests
|
||||
- `npm run test:e2e:flaky-detect` runs all e2e tests and repeats each 10 times
|
||||
- `npm run i18n:sync` syncs translation jsons with English and should always be run after adding new text to an English translation file
|
||||
|
|
|
|||
18
app/browser-test-setup.ts
Normal file
18
app/browser-test-setup.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import i18next from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import { config } from "~/modules/i18n/config";
|
||||
import { resources } from "~/modules/i18n/resources.browser";
|
||||
|
||||
import "~/styles/common.css";
|
||||
import "~/styles/elements.css";
|
||||
import "~/styles/flags.css";
|
||||
import "~/styles/layout.css";
|
||||
import "~/styles/reset.css";
|
||||
import "~/styles/utils.css";
|
||||
import "~/styles/vars.css";
|
||||
|
||||
i18next.use(initReactI18next).init({
|
||||
...config,
|
||||
lng: "en",
|
||||
resources,
|
||||
});
|
||||
|
|
@ -7,6 +7,7 @@ export function MicrophoneIcon({ className }: { className?: string }) {
|
|||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className={className}
|
||||
data-testid="microphone-icon"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ export function SpeakerIcon({ className }: { className?: string }) {
|
|||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className={className}
|
||||
data-testid="speaker-icon"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ export function SpeakerXIcon({ className }: { className?: string }) {
|
|||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className={className}
|
||||
data-testid="speaker-x-icon"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
|
|
|
|||
438
app/features/sendouq/components/GroupCard.browser.test.tsx
Normal file
438
app/features/sendouq/components/GroupCard.browser.test.tsx
Normal file
|
|
@ -0,0 +1,438 @@
|
|||
import type { ComponentProps } from "react";
|
||||
import { createMemoryRouter, RouterProvider } from "react-router";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { render } from "vitest-browser-react";
|
||||
import type { GroupSkillDifference } from "~/db/tables";
|
||||
import type { TieredSkill } from "~/features/mmr/tiered.server";
|
||||
import type {
|
||||
SQGroup,
|
||||
SQGroupMember,
|
||||
SQOwnGroup,
|
||||
} from "../core/SendouQ.server";
|
||||
import type { TierRange } from "../q-types";
|
||||
import { GroupCard } from "./GroupCard";
|
||||
|
||||
vi.mock("~/features/auth/core/user", () => ({
|
||||
useUser: () => null,
|
||||
}));
|
||||
|
||||
function createMember(overrides: Partial<SQGroupMember> = {}): SQGroupMember {
|
||||
return {
|
||||
id: 1,
|
||||
discordId: "123456789",
|
||||
username: "TestUser",
|
||||
discordAvatar: null,
|
||||
customUrl: null,
|
||||
role: "OWNER",
|
||||
vc: "NO",
|
||||
languages: [],
|
||||
skill: "CALCULATING",
|
||||
weapons: [],
|
||||
plusTier: null,
|
||||
friendCode: null,
|
||||
inGameName: null,
|
||||
note: null,
|
||||
privateNote: null,
|
||||
pronouns: null,
|
||||
skillDifference: undefined,
|
||||
noScreen: undefined,
|
||||
chatNameColor: null,
|
||||
mapModePreferences: undefined,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createGroup(
|
||||
overrides: Partial<Omit<SQGroup, "members">> & {
|
||||
members?: SQGroupMember[];
|
||||
} = {},
|
||||
): SQGroup {
|
||||
const { members, ...rest } = overrides;
|
||||
return {
|
||||
id: 1,
|
||||
tier: null,
|
||||
tierRange: null,
|
||||
skillDifference: undefined,
|
||||
isReplay: false,
|
||||
usersRole: null,
|
||||
noScreen: false,
|
||||
modePreferences: [],
|
||||
status: "ACTIVE",
|
||||
matchId: null,
|
||||
latestActionAt: Date.now(),
|
||||
members: members ?? [createMember()],
|
||||
...rest,
|
||||
};
|
||||
}
|
||||
|
||||
type OwnGroupMember = SQOwnGroup["members"][number];
|
||||
|
||||
function createOwnGroupMember(
|
||||
overrides: Partial<OwnGroupMember> = {},
|
||||
): OwnGroupMember {
|
||||
return {
|
||||
id: 1,
|
||||
discordId: "123456789",
|
||||
username: "TestUser",
|
||||
discordAvatar: null,
|
||||
customUrl: null,
|
||||
role: "OWNER",
|
||||
vc: "NO",
|
||||
languages: [],
|
||||
skill: "CALCULATING",
|
||||
weapons: [],
|
||||
plusTier: null,
|
||||
friendCode: null,
|
||||
inGameName: null,
|
||||
note: null,
|
||||
privateNote: null,
|
||||
pronouns: null,
|
||||
skillDifference: undefined,
|
||||
noScreen: undefined,
|
||||
chatNameColor: null,
|
||||
mapModePreferences: undefined,
|
||||
...overrides,
|
||||
} satisfies OwnGroupMember;
|
||||
}
|
||||
|
||||
function createOwnGroup(
|
||||
overrides: Partial<Omit<SQOwnGroup, "members">> & {
|
||||
members?: OwnGroupMember[];
|
||||
} = {},
|
||||
): SQOwnGroup {
|
||||
const { members, ...rest } = overrides;
|
||||
return {
|
||||
id: 1,
|
||||
tier: null,
|
||||
tierRange: null,
|
||||
skillDifference: undefined,
|
||||
isReplay: false,
|
||||
usersRole: "OWNER",
|
||||
noScreen: false,
|
||||
modePreferences: [],
|
||||
chatCode: null,
|
||||
status: "ACTIVE",
|
||||
matchId: null,
|
||||
inviteCode: "test123",
|
||||
latestActionAt: Date.now(),
|
||||
members: members ?? [createOwnGroupMember()],
|
||||
...rest,
|
||||
};
|
||||
}
|
||||
|
||||
function renderGroupCard(
|
||||
props: Partial<ComponentProps<typeof GroupCard>> = {},
|
||||
) {
|
||||
const group = props.group ?? createGroup();
|
||||
const { displayOnly = true, ...restProps } = props;
|
||||
const router = createMemoryRouter(
|
||||
[
|
||||
{
|
||||
path: "/",
|
||||
element: (
|
||||
<GroupCard group={group} displayOnly={displayOnly} {...restProps} />
|
||||
),
|
||||
},
|
||||
],
|
||||
{ initialEntries: ["/"] },
|
||||
);
|
||||
|
||||
return render(<RouterProvider router={router} />);
|
||||
}
|
||||
|
||||
describe("GroupCard", () => {
|
||||
describe("member display", () => {
|
||||
test("renders single member with username", async () => {
|
||||
const screen = await renderGroupCard({
|
||||
group: createGroup({
|
||||
members: [createMember({ username: "Player1" })],
|
||||
}),
|
||||
});
|
||||
|
||||
await expect
|
||||
.element(screen.getByTestId("sendouq-group-card"))
|
||||
.toBeVisible();
|
||||
await expect
|
||||
.element(screen.getByTestId("sendouq-group-card-member"))
|
||||
.toBeVisible();
|
||||
await expect.element(screen.getByText("Player1")).toBeVisible();
|
||||
});
|
||||
|
||||
test("renders multiple members", async () => {
|
||||
const screen = await renderGroupCard({
|
||||
group: createGroup({
|
||||
members: [
|
||||
createMember({ id: 1, username: "Player1" }),
|
||||
createMember({ id: 2, username: "Player2", role: "MANAGER" }),
|
||||
createMember({ id: 3, username: "Player3", role: "REGULAR" }),
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
await expect.element(screen.getByText("Player1")).toBeVisible();
|
||||
await expect.element(screen.getByText("Player2")).toBeVisible();
|
||||
await expect.element(screen.getByText("Player3")).toBeVisible();
|
||||
});
|
||||
|
||||
test("displays in-game name when present", async () => {
|
||||
const screen = await renderGroupCard({
|
||||
group: createGroup({
|
||||
members: [createMember({ inGameName: "IGN#1234" })],
|
||||
}),
|
||||
});
|
||||
|
||||
await expect.element(screen.getByText("IGN")).toBeVisible();
|
||||
});
|
||||
|
||||
test("displays calculated tier", async () => {
|
||||
const skill: TieredSkill = {
|
||||
ordinal: 2100,
|
||||
tier: { name: "GOLD", isPlus: false },
|
||||
approximate: false,
|
||||
};
|
||||
|
||||
const screen = await renderGroupCard({
|
||||
group: createGroup({
|
||||
members: [createMember({ skill })],
|
||||
}),
|
||||
});
|
||||
|
||||
// Tier info is shown in a popover button, check it renders
|
||||
await expect
|
||||
.element(screen.getByTestId("sendouq-group-card-member"))
|
||||
.toBeVisible();
|
||||
});
|
||||
|
||||
test("displays weapons", async () => {
|
||||
const screen = await renderGroupCard({
|
||||
group: createGroup({
|
||||
members: [
|
||||
createMember({
|
||||
weapons: [
|
||||
{ weaponSplId: 40, isFavorite: 1 },
|
||||
{ weaponSplId: 50, isFavorite: 0 },
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
// Weapons are rendered as picture elements
|
||||
const pictures = screen.container.querySelectorAll("picture");
|
||||
expect(pictures.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("voice chat", () => {
|
||||
test("shows microphone icon for YES", async () => {
|
||||
const screen = await renderGroupCard({
|
||||
group: createGroup({
|
||||
members: [createMember({ vc: "YES", languages: ["en"] })],
|
||||
}),
|
||||
});
|
||||
|
||||
await expect.element(screen.getByTestId("microphone-icon")).toBeVisible();
|
||||
});
|
||||
|
||||
test("shows speaker icon for LISTEN_ONLY", async () => {
|
||||
const screen = await renderGroupCard({
|
||||
group: createGroup({
|
||||
members: [createMember({ vc: "LISTEN_ONLY", languages: ["en"] })],
|
||||
}),
|
||||
});
|
||||
|
||||
await expect.element(screen.getByTestId("speaker-icon")).toBeVisible();
|
||||
});
|
||||
|
||||
test("shows speaker-x icon for NO", async () => {
|
||||
const screen = await renderGroupCard({
|
||||
group: createGroup({
|
||||
members: [createMember({ vc: "NO", languages: ["en"] })],
|
||||
}),
|
||||
});
|
||||
|
||||
await expect.element(screen.getByTestId("speaker-x-icon")).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
describe("group states", () => {
|
||||
test("shows tier for full group", async () => {
|
||||
const tier: TieredSkill["tier"] = { name: "PLATINUM", isPlus: true };
|
||||
|
||||
const screen = await renderGroupCard({
|
||||
group: createGroup({
|
||||
tier,
|
||||
members: [
|
||||
createMember({ id: 1 }),
|
||||
createMember({ id: 2, role: "REGULAR" }),
|
||||
createMember({ id: 3, role: "REGULAR" }),
|
||||
createMember({ id: 4, role: "REGULAR" }),
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
await expect.element(screen.getByText(/PLATINUM\+/)).toBeVisible();
|
||||
});
|
||||
|
||||
test("shows tier range", async () => {
|
||||
const tierRange: TierRange = {
|
||||
diff: [0, 50],
|
||||
range: [
|
||||
{ name: "GOLD", isPlus: false },
|
||||
{ name: "PLATINUM", isPlus: true },
|
||||
],
|
||||
};
|
||||
|
||||
const screen = await renderGroupCard({
|
||||
group: createGroup({
|
||||
tierRange,
|
||||
tier: null,
|
||||
members: undefined,
|
||||
}),
|
||||
});
|
||||
|
||||
await expect
|
||||
.element(screen.getByTestId("sendouq-group-card"))
|
||||
.toBeVisible();
|
||||
});
|
||||
|
||||
test("shows REPLAY label when isReplay", async () => {
|
||||
const tier: TieredSkill["tier"] = { name: "GOLD", isPlus: false };
|
||||
|
||||
const screen = await renderGroupCard({
|
||||
group: createGroup({
|
||||
tier,
|
||||
isReplay: true,
|
||||
members: [
|
||||
createMember({ id: 1 }),
|
||||
createMember({ id: 2, role: "REGULAR" }),
|
||||
createMember({ id: 3, role: "REGULAR" }),
|
||||
createMember({ id: 4, role: "REGULAR" }),
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
// REPLAY text is rendered, translations are loaded
|
||||
await expect.element(screen.getByText(/REPLAY/i)).toBeVisible();
|
||||
});
|
||||
|
||||
test("shows group skill difference", async () => {
|
||||
const skillDifference: GroupSkillDifference = {
|
||||
calculated: true,
|
||||
oldSp: 2100,
|
||||
newSp: 2150,
|
||||
};
|
||||
|
||||
const screen = await renderGroupCard({
|
||||
group: createGroup({
|
||||
skillDifference,
|
||||
members: undefined,
|
||||
}),
|
||||
});
|
||||
|
||||
await expect.element(screen.getByText(/2100/)).toBeVisible();
|
||||
await expect.element(screen.getByText(/2150/)).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
describe("action buttons", () => {
|
||||
test("shows Invite for LIKE with members", async () => {
|
||||
const ownGroup = createOwnGroup({ id: 2 });
|
||||
|
||||
const screen = await renderGroupCard({
|
||||
group: createGroup({ members: [createMember()] }),
|
||||
action: "LIKE",
|
||||
ownGroup,
|
||||
displayOnly: false,
|
||||
});
|
||||
|
||||
// Actual translated text is "Invite"
|
||||
await expect.element(screen.getByText("Invite")).toBeVisible();
|
||||
});
|
||||
|
||||
test("shows Challenge for LIKE with full group (no visible members)", async () => {
|
||||
const ownGroup = createOwnGroup({ id: 2 });
|
||||
|
||||
// Create a group with members explicitly set to undefined (censored/full group)
|
||||
const fullGroup = createGroup({});
|
||||
fullGroup.members = undefined;
|
||||
|
||||
const screen = await renderGroupCard({
|
||||
group: fullGroup,
|
||||
action: "LIKE",
|
||||
ownGroup,
|
||||
displayOnly: false,
|
||||
});
|
||||
|
||||
// Actual translated text is "Challenge"
|
||||
await expect.element(screen.getByText("Challenge")).toBeVisible();
|
||||
});
|
||||
|
||||
test("shows Start Match for MATCH_UP", async () => {
|
||||
const ownGroup = createOwnGroup({ id: 2 });
|
||||
|
||||
const screen = await renderGroupCard({
|
||||
group: createGroup({ members: undefined }),
|
||||
action: "MATCH_UP",
|
||||
ownGroup,
|
||||
displayOnly: false,
|
||||
});
|
||||
|
||||
// Actual translated text is "Start match"
|
||||
await expect.element(screen.getByText("Start match")).toBeVisible();
|
||||
});
|
||||
|
||||
test("hides actions when user is not owner or manager", async () => {
|
||||
// ownGroup with REGULAR role shouldn't show action buttons
|
||||
const ownGroup = createOwnGroup({ id: 2, usersRole: "REGULAR" });
|
||||
|
||||
const screen = await renderGroupCard({
|
||||
group: createGroup({ members: [createMember()] }),
|
||||
action: "LIKE",
|
||||
ownGroup,
|
||||
displayOnly: false,
|
||||
});
|
||||
|
||||
// Action button should not be rendered when user is not OWNER or MANAGER
|
||||
const actionButton = screen.container.querySelector(
|
||||
'[data-testid="group-card-action-button"]',
|
||||
);
|
||||
expect(actionButton).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("props", () => {
|
||||
test("hides VC when hideVc=1", async () => {
|
||||
const screen = await renderGroupCard({
|
||||
group: createGroup({
|
||||
members: [createMember({ vc: "YES", languages: ["en"] })],
|
||||
}),
|
||||
hideVc: 1,
|
||||
});
|
||||
|
||||
await expect
|
||||
.element(screen.getByTestId("sendouq-group-card-member"))
|
||||
.toBeVisible();
|
||||
// VC button should not be visible, only member info
|
||||
});
|
||||
|
||||
test("hides weapons when hideWeapons=1", async () => {
|
||||
const screen = await renderGroupCard({
|
||||
group: createGroup({
|
||||
members: [
|
||||
createMember({
|
||||
weapons: [{ weaponSplId: 40, isFavorite: 1 }],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
hideWeapons: 1,
|
||||
});
|
||||
|
||||
await expect
|
||||
.element(screen.getByTestId("sendouq-group-card-member"))
|
||||
.toBeVisible();
|
||||
const weaponImages = screen.getByRole("img", { name: /weapons:MAIN/ });
|
||||
await expect.element(weaponImages.first()).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
47
app/modules/i18n/resources.browser.ts
Normal file
47
app/modules/i18n/resources.browser.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import analyzer from "../../../locales/en/analyzer.json";
|
||||
import art from "../../../locales/en/art.json";
|
||||
import badges from "../../../locales/en/badges.json";
|
||||
import builds from "../../../locales/en/builds.json";
|
||||
import calendar from "../../../locales/en/calendar.json";
|
||||
import common from "../../../locales/en/common.json";
|
||||
import contributions from "../../../locales/en/contributions.json";
|
||||
import faq from "../../../locales/en/faq.json";
|
||||
import front from "../../../locales/en/front.json";
|
||||
import gameMisc from "../../../locales/en/game-misc.json";
|
||||
import gear from "../../../locales/en/gear.json";
|
||||
import lfg from "../../../locales/en/lfg.json";
|
||||
import org from "../../../locales/en/org.json";
|
||||
import q from "../../../locales/en/q.json";
|
||||
import scrims from "../../../locales/en/scrims.json";
|
||||
import team from "../../../locales/en/team.json";
|
||||
import tierListMaker from "../../../locales/en/tier-list-maker.json";
|
||||
import tournament from "../../../locales/en/tournament.json";
|
||||
import user from "../../../locales/en/user.json";
|
||||
import vods from "../../../locales/en/vods.json";
|
||||
import weapons from "../../../locales/en/weapons.json";
|
||||
|
||||
export const resources = {
|
||||
en: {
|
||||
analyzer,
|
||||
art,
|
||||
badges,
|
||||
builds,
|
||||
calendar,
|
||||
common,
|
||||
contributions,
|
||||
faq,
|
||||
front,
|
||||
"game-misc": gameMisc,
|
||||
gear,
|
||||
lfg,
|
||||
org,
|
||||
q,
|
||||
scrims,
|
||||
team,
|
||||
"tier-list-maker": tierListMaker,
|
||||
tournament,
|
||||
user,
|
||||
vods,
|
||||
weapons,
|
||||
},
|
||||
};
|
||||
|
|
@ -12,6 +12,8 @@
|
|||
"scripts/**/*.{js,cjs,mjs,jsx,ts,cts,mts,tsx}",
|
||||
"public/sw-2.js",
|
||||
"ley.config.cjs",
|
||||
"ley-driver.cjs"
|
||||
"ley-driver.cjs",
|
||||
"vitest.browser.config.ts",
|
||||
"app/browser-test-setup.ts"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
207
package-lock.json
generated
207
package-lock.json
generated
|
|
@ -79,6 +79,8 @@
|
|||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/web-push": "^3.6.4",
|
||||
"@vitest/browser-playwright": "^4.0.16",
|
||||
"@vitest/ui": "^4.0.16",
|
||||
"babel-plugin-react-compiler": "^19.1.0-rc.2",
|
||||
"cross-env": "^10.1.0",
|
||||
"dotenv": "^17.2.3",
|
||||
|
|
@ -92,7 +94,8 @@
|
|||
"vite-node": "^5.2.0",
|
||||
"vite-plugin-babel": "^1.3.2",
|
||||
"vite-tsconfig-paths": "^6.0.3",
|
||||
"vitest": "^4.0.16"
|
||||
"vitest": "^4.0.16",
|
||||
"vitest-browser-react": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-crypto/crc32": {
|
||||
|
|
@ -2814,6 +2817,13 @@
|
|||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@polka/url": {
|
||||
"version": "1.0.0-next.29",
|
||||
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
|
||||
"integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"version": "2.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||
|
|
@ -8164,6 +8174,53 @@
|
|||
"react": ">= 16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/browser": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.0.16.tgz",
|
||||
"integrity": "sha512-t4toy8X/YTnjYEPoY0pbDBg3EvDPg1elCDrfc+VupPHwoN/5/FNQ8Z+xBYIaEnOE2vVEyKwqYBzZ9h9rJtZVcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/mocker": "4.0.16",
|
||||
"@vitest/utils": "4.0.16",
|
||||
"magic-string": "^0.30.21",
|
||||
"pixelmatch": "7.1.0",
|
||||
"pngjs": "^7.0.0",
|
||||
"sirv": "^3.0.2",
|
||||
"tinyrainbow": "^3.0.3",
|
||||
"ws": "^8.18.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vitest": "4.0.16"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/browser-playwright": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.0.16.tgz",
|
||||
"integrity": "sha512-I2Fy/ANdphi1yI46d15o0M1M4M0UJrUiVKkH5oKeRZZCdPg0fw/cfTKZzv9Ge9eobtJYp4BGblMzXdXH0vcl5g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/browser": "4.0.16",
|
||||
"@vitest/mocker": "4.0.16",
|
||||
"tinyrainbow": "^3.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"playwright": "*",
|
||||
"vitest": "4.0.16"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"playwright": {
|
||||
"optional": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.16.tgz",
|
||||
|
|
@ -8275,6 +8332,35 @@
|
|||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/ui": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.0.16.tgz",
|
||||
"integrity": "sha512-rkoPH+RqWopVxDnCBE/ysIdfQ2A7j1eDmW8tCxxrR9nnFBa9jKf86VgsSAzxBd1x+ny0GC4JgiD3SNfRHv3pOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/utils": "4.0.16",
|
||||
"fflate": "^0.8.2",
|
||||
"flatted": "^3.3.3",
|
||||
"pathe": "^2.0.3",
|
||||
"sirv": "^3.0.2",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"tinyrainbow": "^3.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vitest": "4.0.16"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/ui/node_modules/pathe": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vitest/utils": {
|
||||
"version": "4.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.16.tgz",
|
||||
|
|
@ -9818,6 +9904,13 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/file-uri-to-path": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||
|
|
@ -9870,6 +9963,13 @@
|
|||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/flatted": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
|
||||
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/flip-toolkit": {
|
||||
"version": "7.2.4",
|
||||
"resolved": "https://registry.npmjs.org/flip-toolkit/-/flip-toolkit-7.2.4.tgz",
|
||||
|
|
@ -11211,6 +11311,16 @@
|
|||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/mrmime": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
|
||||
"integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
|
|
@ -11576,6 +11686,19 @@
|
|||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/pixelmatch": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-7.1.0.tgz",
|
||||
"integrity": "sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"pngjs": "^7.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"pixelmatch": "bin/pixelmatch"
|
||||
}
|
||||
},
|
||||
"node_modules/pkg-types": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
|
||||
|
|
@ -11627,6 +11750,16 @@
|
|||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/pngjs": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
|
||||
"integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/possible-typed-array-names": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
||||
|
|
@ -13246,6 +13379,31 @@
|
|||
"simple-concat": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sirv": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
|
||||
"integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@polka/url": "^1.0.0-next.24",
|
||||
"mrmime": "^2.0.0",
|
||||
"totalist": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/sirv/node_modules/totalist": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
|
||||
"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/slugify": {
|
||||
"version": "1.6.6",
|
||||
"resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz",
|
||||
|
|
@ -14235,6 +14393,31 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vitest-browser-react": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/vitest-browser-react/-/vitest-browser-react-2.0.2.tgz",
|
||||
"integrity": "sha512-zuSgTe/CKODU3ip+w4ls6Qm4xZ9+A4OHmDf0obt/mwAqavpOtqtq2YcioZt8nfDQE50EWmhdnRfDmpS1jCsbTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.0.0 || ^19.0.0",
|
||||
"@types/react-dom": "^18.0.0 || ^19.0.0",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0",
|
||||
"vitest": "^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vitest/node_modules/pathe": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||
|
|
@ -14380,6 +14563,28 @@
|
|||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xml2js": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
|
||||
|
|
|
|||
11
package.json
11
package.json
|
|
@ -21,11 +21,13 @@
|
|||
"biome:fix": "npx @biomejs/biome check --write .",
|
||||
"biome:fix:unsafe": "npx @biomejs/biome check --write --unsafe .",
|
||||
"typecheck": "react-router typegen && tsc --noEmit",
|
||||
"test:unit": "cross-env VITE_SITE_DOMAIN=http://localhost:5173 vitest --silent=passed-only run",
|
||||
"test:unit:browser": "cross-env VITE_SITE_DOMAIN=http://localhost:5173 BROWSER_HEADLESS=true vitest --silent=passed-only run",
|
||||
"test:browser:ui": "cross-env VITE_SITE_DOMAIN=http://localhost:5173 vitest --silent=passed-only --project browser",
|
||||
"test:unit:browser:ui": "cross-env VITE_SITE_DOMAIN=http://localhost:5173 vitest --silent=passed-only",
|
||||
"test:e2e": "npx playwright test",
|
||||
"test:e2e:flaky-detect": "npx playwright test --repeat-each=10 --max-failures=1",
|
||||
"test:e2e:generate-seeds": "cross-env DB_PATH=db-test.sqlite3 npx vite-node scripts/generate-e2e-seed-dbs.ts",
|
||||
"checks": "npm run biome:fix && npm run test:unit && npm run check-translation-jsons && npm run typecheck && npm run knip",
|
||||
"checks": "npm run biome:fix && npm run test:unit:browser && npm run check-translation-jsons && npm run typecheck && npm run knip",
|
||||
"setup": "cross-env DB_PATH=db.sqlite3 vite-node ./scripts/setup.ts",
|
||||
"i18n:sync": "i18next-locales-sync -e true -p en -s da de es-ES es-US fr-CA fr-EU he it ja ko nl pl pt-BR ru zh -l locales && npm run biome:fix",
|
||||
"knip": "knip"
|
||||
|
|
@ -102,6 +104,8 @@
|
|||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/web-push": "^3.6.4",
|
||||
"@vitest/browser-playwright": "^4.0.16",
|
||||
"@vitest/ui": "^4.0.16",
|
||||
"babel-plugin-react-compiler": "^19.1.0-rc.2",
|
||||
"cross-env": "^10.1.0",
|
||||
"dotenv": "^17.2.3",
|
||||
|
|
@ -115,6 +119,7 @@
|
|||
"vite-node": "^5.2.0",
|
||||
"vite-plugin-babel": "^1.3.2",
|
||||
"vite-tsconfig-paths": "^6.0.3",
|
||||
"vitest": "^4.0.16"
|
||||
"vitest": "^4.0.16",
|
||||
"vitest-browser-react": "^2.0.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,8 +30,24 @@ export default defineConfig(({ mode }) => {
|
|||
tsconfigPaths(),
|
||||
],
|
||||
test: {
|
||||
exclude: [...configDefaults.exclude, "e2e/**"],
|
||||
setupFiles: ["./app/test-setup.ts"],
|
||||
projects: [
|
||||
{
|
||||
extends: true,
|
||||
test: {
|
||||
name: "unit",
|
||||
include: ["**/*.test.{ts,tsx}"],
|
||||
exclude: [
|
||||
...configDefaults.exclude,
|
||||
"e2e/**",
|
||||
"**/*.browser.test.{ts,tsx}",
|
||||
],
|
||||
setupFiles: ["./app/test-setup.ts"],
|
||||
},
|
||||
},
|
||||
{
|
||||
extends: "./vitest.browser.config.ts",
|
||||
},
|
||||
],
|
||||
},
|
||||
build: {
|
||||
// this is mostly done so that i18n jsons as defined in ./app/modules/i18n/loader.ts
|
||||
|
|
|
|||
23
vitest.browser.config.ts
Normal file
23
vitest.browser.config.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { playwright } from "@vitest/browser-playwright";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
const headless = process.env.BROWSER_HEADLESS === "true";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tsconfigPaths()],
|
||||
test: {
|
||||
name: "browser",
|
||||
include: ["**/*.browser.test.{ts,tsx}"],
|
||||
browser: {
|
||||
provider: playwright(),
|
||||
enabled: true,
|
||||
headless,
|
||||
instances: [{ browser: "chromium" }],
|
||||
},
|
||||
css: {
|
||||
include: /.+/,
|
||||
},
|
||||
setupFiles: ["./app/browser-test-setup.ts"],
|
||||
},
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user