Add initial Playwright tests (#1191)

This commit is contained in:
Kalle
2022-12-11 17:18:33 +02:00
committed by GitHub
parent 9cd190df53
commit c3963223b6
11 changed files with 309 additions and 5 deletions

28
.github/workflows/playwright.yml vendored Normal file
View 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
View File

@@ -18,3 +18,6 @@ dump
!/scripts/output/.gitkeep
/scripts/dicts/**/*.json
/test-results/
/playwright-report/
/playwright/.cache/

View File

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

View File

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

View File

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

View File

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

View 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
View File

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

View File

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