mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-14 15:16:09 -05:00
Add initial Playwright tests (#1191)
This commit is contained in:
28
.github/workflows/playwright.yml
vendored
Normal file
28
.github/workflows/playwright.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: Playwright Tests
|
||||
on:
|
||||
push:
|
||||
branches: [rewrite]
|
||||
pull_request:
|
||||
branches: [rewrite]
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
# TODO: cache deps
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps
|
||||
- name: Run Playwright tests
|
||||
run: npx playwright test
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -18,3 +18,6 @@ dump
|
||||
!/scripts/output/.gitkeep
|
||||
|
||||
/scripts/dicts/**/*.json
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
|
||||
@@ -121,7 +121,7 @@ export function Combobox<T extends Record<string, string | null | number>>({
|
||||
// To make SSR prefill work in an uncontrolled component
|
||||
defaultValue={initialValue ? displayValue(initialValue) : undefined}
|
||||
displayValue={displayValue}
|
||||
data-cy={`${inputName}-combobox-input`}
|
||||
data-testid={`${inputName}-combobox-input`}
|
||||
id={id}
|
||||
required={required}
|
||||
/>
|
||||
|
||||
@@ -21,6 +21,7 @@ export function Toggle({
|
||||
className={clsx("toggle", { checked, tiny })}
|
||||
id={id}
|
||||
name={name}
|
||||
data-testid={id ? `toggle-${id}` : null}
|
||||
>
|
||||
<span className={clsx("toggle-dot", { checked, tiny })} />
|
||||
</Switch>
|
||||
|
||||
10
app/root.tsx
10
app/root.tsx
@@ -39,6 +39,7 @@ import * as gtag from "~/utils/gtags.client";
|
||||
import { Theme, ThemeHead, useTheme, ThemeProvider } from "./modules/theme";
|
||||
import { getThemeSession } from "./modules/theme/session.server";
|
||||
import { isTheme } from "./modules/theme/provider";
|
||||
import { useIsMounted } from "./hooks/useIsMounted";
|
||||
|
||||
export const unstable_shouldReload: ShouldReloadFunction = ({ url }) => {
|
||||
// reload on language change so the selected language gets set into the cookie
|
||||
@@ -145,6 +146,7 @@ function Document({
|
||||
<Fonts />
|
||||
</head>
|
||||
<body>
|
||||
{process.env.NODE_ENV === "development" && <HydrationTestIndicator />}
|
||||
{data?.gtmId ? <GTM id={data.gtmId} /> : null}
|
||||
<React.StrictMode>
|
||||
<Layout patrons={data?.patrons} isCatchBoundary={isCatchBoundary}>
|
||||
@@ -235,6 +237,14 @@ export const ErrorBoundary: ErrorBoundaryComponent = ({ error }) => {
|
||||
);
|
||||
};
|
||||
|
||||
function HydrationTestIndicator() {
|
||||
const isMounted = useIsMounted();
|
||||
|
||||
if (!isMounted) return null;
|
||||
|
||||
return <div style={{ display: "none" }} data-testid="hydrated" />;
|
||||
}
|
||||
|
||||
function GTM({ id }: { id: string }) {
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -102,6 +102,7 @@ export default function ObjectDamagePage() {
|
||||
×{multiShotCount}
|
||||
</label>
|
||||
<Toggle
|
||||
id="multi"
|
||||
name="multi"
|
||||
checked={isMultiShot}
|
||||
setChecked={(checked) =>
|
||||
@@ -288,7 +289,9 @@ function DamageReceiversGrid({
|
||||
/>
|
||||
</div>
|
||||
<div className="object-damage__hp">
|
||||
{damageToReceiver.hitPoints}
|
||||
<span data-testid={`hp-${damageToReceiver.receiver}`}>
|
||||
{damageToReceiver.hitPoints}
|
||||
</span>
|
||||
{t("analyzer:suffix.hp")}
|
||||
</div>
|
||||
{damageToReceiver.damages.map((damage) => {
|
||||
@@ -301,14 +304,26 @@ function DamageReceiversGrid({
|
||||
>
|
||||
{t("analyzer:damageShort")}
|
||||
</abbr>
|
||||
<div>{damage.value}</div>
|
||||
<div
|
||||
data-testid={`dmg${damage.objectShredder ? "-os" : ""}-${
|
||||
damageToReceiver.receiver
|
||||
}`}
|
||||
>
|
||||
{damage.value}
|
||||
</div>
|
||||
<abbr
|
||||
className="object-damage__abbr"
|
||||
title={t("analyzer:hitsToDestroyLong")}
|
||||
>
|
||||
{t("analyzer:hitsToDestroyShort")}
|
||||
</abbr>
|
||||
<div>{damage.hitsToDestroy}</div>
|
||||
<div
|
||||
data-testid={`htd${damage.objectShredder ? "-os" : ""}-${
|
||||
damageToReceiver.receiver
|
||||
}`}
|
||||
>
|
||||
{damage.hitsToDestroy}
|
||||
</div>
|
||||
</div>
|
||||
<div className="object-damage__multiplier">
|
||||
×{damage.multiplier}
|
||||
|
||||
20
app/utils/playwright.ts
Normal file
20
app/utils/playwright.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
|
||||
export async function selectWeapon({
|
||||
page,
|
||||
name,
|
||||
}: {
|
||||
page: Page;
|
||||
name: string;
|
||||
}) {
|
||||
const weaponCombobox = page.getByTestId("weapon-combobox-input");
|
||||
await weaponCombobox.clear();
|
||||
await weaponCombobox.fill(name);
|
||||
await weaponCombobox.press("Enter");
|
||||
}
|
||||
|
||||
/** page.goto that waits for the page to be hydrated before proceeding */
|
||||
export async function navigate({ page, url }: { page: Page; url: string }) {
|
||||
await page.goto(url);
|
||||
page.getByTestId("hydrated");
|
||||
}
|
||||
71
e2e/object-damage-calculator.spec.ts
Normal file
71
e2e/object-damage-calculator.spec.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { navigate, selectWeapon } from "~/utils/playwright";
|
||||
import { OBJECT_DAMAGE_CALCULATOR_URL } from "~/utils/urls";
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await navigate({ page, url: OBJECT_DAMAGE_CALCULATOR_URL });
|
||||
});
|
||||
|
||||
const cellId = (id: string, damageReceiver = "Chariot") =>
|
||||
`${id}-${damageReceiver}`;
|
||||
|
||||
test("operates damage type select, max damage > min damage", async ({
|
||||
page,
|
||||
}) => {
|
||||
const hp = page.getByTestId(cellId("hp"));
|
||||
const dmg = page.getByTestId(cellId("dmg"));
|
||||
const htd = page.getByTestId(cellId("htd"));
|
||||
|
||||
const hpBefore = (await hp.textContent())!;
|
||||
const dmgBefore = (await dmg.textContent())!;
|
||||
const htdBefore = (await htd.textContent())!;
|
||||
|
||||
// test hits to destroy calculation
|
||||
expect(Number(htdBefore)).toBe(
|
||||
Math.ceil(Number(hpBefore) / Number(dmgBefore))
|
||||
);
|
||||
|
||||
await page.locator("text=Damage type").selectOption("NORMAL_MIN");
|
||||
|
||||
// select did what we expect it to do
|
||||
await expect(hp).toHaveText(hpBefore);
|
||||
await expect(dmg).not.toHaveText(dmgBefore);
|
||||
await expect(htd).not.toHaveText(htdBefore);
|
||||
});
|
||||
|
||||
test("changes weapon and saves it to url", async ({ page }) => {
|
||||
const dmg = page.getByTestId(cellId("dmg"));
|
||||
const dmgBefore = (await dmg.textContent())!;
|
||||
|
||||
await selectWeapon({ page, name: "Luna Blaster" });
|
||||
|
||||
await expect(dmg).not.toHaveText(dmgBefore);
|
||||
await page.reload();
|
||||
await expect(dmg).not.toHaveText(dmgBefore);
|
||||
});
|
||||
|
||||
test("multiplier switch increases damage", async ({ page }) => {
|
||||
await selectWeapon({ page, name: "Tri-Stringer" });
|
||||
|
||||
const dmg = page.getByTestId(cellId("dmg"));
|
||||
const dmgBefore = (await dmg.textContent())!;
|
||||
await page.getByTestId("toggle-multi").click();
|
||||
|
||||
// Multiplier is on by default
|
||||
await expect(dmg).not.toHaveText(dmgBefore);
|
||||
});
|
||||
|
||||
test("object hp increases when ability points added", async ({ page }) => {
|
||||
const crabTankHp = page.getByTestId(cellId("hp"));
|
||||
const crabTankHpBefore = (await crabTankHp.textContent())!;
|
||||
|
||||
const splashWallHp = page.getByTestId(cellId("hp", "Wsb_Shield"));
|
||||
const splashWallHpBefore = (await splashWallHp.textContent())!;
|
||||
|
||||
await page.locator("text=Amount of").selectOption("10");
|
||||
|
||||
// Crab Tank doesn't gain HP from ability points
|
||||
await expect(crabTankHp).toHaveText(crabTankHpBefore);
|
||||
// ... but Splash Wall does
|
||||
await expect(splashWallHp).not.toHaveText(splashWallHpBefore);
|
||||
});
|
||||
45
package-lock.json
generated
45
package-lock.json
generated
@@ -45,6 +45,7 @@
|
||||
"zod": "^3.19.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.28.1",
|
||||
"@remix-run/dev": "^1.8.1",
|
||||
"@remix-run/eslint-config": "^1.8.1",
|
||||
"@types/better-sqlite3": "^7.6.2",
|
||||
@@ -2561,6 +2562,22 @@
|
||||
"url": "https://opencollective.com/unts"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.1.tgz",
|
||||
"integrity": "sha512-xN6spdqrNlwSn9KabIhqfZR7IWjPpFK1835tFNgjrlysaSezuX8PYUwaz38V/yI8TJLG9PkAMEXoHRXYXlpTPQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"playwright-core": "1.28.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"version": "2.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz",
|
||||
@@ -13197,6 +13214,18 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.28.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.1.tgz",
|
||||
"integrity": "sha512-3PixLnGPno0E8rSBJjtwqTwJe3Yw72QwBBBxNoukIj3lEeBNXwbNiKrNuB1oyQgTBw5QHUhNO3SteEtHaMK6ag==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/posix-character-classes": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz",
|
||||
@@ -19118,6 +19147,16 @@
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"@playwright/test": {
|
||||
"version": "1.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.1.tgz",
|
||||
"integrity": "sha512-xN6spdqrNlwSn9KabIhqfZR7IWjPpFK1835tFNgjrlysaSezuX8PYUwaz38V/yI8TJLG9PkAMEXoHRXYXlpTPQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
"playwright-core": "1.28.1"
|
||||
}
|
||||
},
|
||||
"@popperjs/core": {
|
||||
"version": "2.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz",
|
||||
@@ -26867,6 +26906,12 @@
|
||||
"find-up": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"playwright-core": {
|
||||
"version": "1.28.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.1.tgz",
|
||||
"integrity": "sha512-3PixLnGPno0E8rSBJjtwqTwJe3Yw72QwBBBxNoukIj3lEeBNXwbNiKrNuB1oyQgTBw5QHUhNO3SteEtHaMK6ag==",
|
||||
"dev": true
|
||||
},
|
||||
"posix-character-classes": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"deploy": "npm ci && npm run build",
|
||||
"build": "remix build",
|
||||
"dev": "cross-env NODE_ENV=development remix dev",
|
||||
"dev:ci": "cp .env.example .env && npm run migrate up && npm run dev",
|
||||
"start": "npm run migrate up && remix-serve build",
|
||||
"migrate": "ley",
|
||||
"migrate:reset": "node scripts/delete-db-files.mjs && npm run migrate && npm run seed",
|
||||
@@ -25,7 +26,8 @@
|
||||
"prettier:check": "prettier --check . --loglevel warn",
|
||||
"prettier:write": "prettier --write . --loglevel warn",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test:unit": "uvu -r tsm -r tsconfig-paths/register",
|
||||
"test:unit": "uvu -r tsm -r tsconfig-paths/register -i e2e",
|
||||
"test:e2e": "npx playwright test",
|
||||
"checks": "npm run test:unit && npm run lint:styles && npm run lint:ts && npm run prettier:check && npm run typecheck",
|
||||
"cf": "npm run test:unit && npm run check-translation-jsons && npm run lint:styles -- --fix && npm run lint:ts -- --fix && npm run prettier:write && npm run typecheck"
|
||||
},
|
||||
@@ -69,6 +71,7 @@
|
||||
"zod": "^3.19.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.28.1",
|
||||
"@remix-run/dev": "^1.8.1",
|
||||
"@remix-run/eslint-config": "^1.8.1",
|
||||
"@types/better-sqlite3": "^7.6.2",
|
||||
|
||||
108
playwright.config.ts
Normal file
108
playwright.config.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import type { PlaywrightTestConfig } from "@playwright/test";
|
||||
import { devices } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
const config: PlaywrightTestConfig = {
|
||||
testDir: "./e2e",
|
||||
/* Maximum time one test can run for. */
|
||||
timeout: 30 * 1000,
|
||||
expect: {
|
||||
/**
|
||||
* Maximum time expect() should wait for the condition to be met.
|
||||
* For example in `await expect(locator).toHaveText();`
|
||||
*/
|
||||
timeout: 5000,
|
||||
},
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env["CI"],
|
||||
/* Retry on CI only */
|
||||
retries: process.env["CI"] ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env["CI"] ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: "list",
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
|
||||
actionTimeout: 0,
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: "http://localhost:5800",
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "retain-on-failure",
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: {
|
||||
...devices["Desktop Chrome"],
|
||||
},
|
||||
},
|
||||
|
||||
// {
|
||||
// name: "firefox",
|
||||
// use: {
|
||||
// ...devices["Desktop Firefox"],
|
||||
// },
|
||||
// },
|
||||
|
||||
// {
|
||||
// name: "webkit",
|
||||
// use: {
|
||||
// ...devices["Desktop Safari"],
|
||||
// },
|
||||
// },
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: {
|
||||
// ...devices['Pixel 5'],
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: {
|
||||
// ...devices['iPhone 12'],
|
||||
// },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: {
|
||||
// channel: 'msedge',
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: {
|
||||
// channel: 'chrome',
|
||||
// },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
|
||||
// outputDir: 'test-results/',
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
command: "npm run dev:ci",
|
||||
port: 5800,
|
||||
reuseExistingServer: !process.env["CI"],
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
Reference in New Issue
Block a user