Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/info.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
. + [{"vercel_project_name": env.DESIGN_PROJECT}]
else . end |
if ($p | contains(["@ageorgedev/game-tools"])) then
. + [{"vercel_project_name": env.GAME_TOOLS_PROJECT}]
. + [{"vercel_project_name": env.GAME_TOOLS_PROJECT, "e2e_project": "@ageorgedev/game-tools-e2e"}]
else . end
')
echo "production_deploy_matrix=$matrix" >> "$GITHUB_OUTPUT"
Expand All @@ -66,8 +66,8 @@ jobs:
if ($p | contains(["@ageorgedev/design-docs"])) then
. + [{"vercel_project_name": env.DESIGN_PROJECT, "label": "Design System"}]
else . end |
if ($p | contains(["@ageorgedev/game-tools"])) then
. + [{"vercel_project_name": env.GAME_TOOLS_PROJECT, "label": "Game Tools"}]
if (($p | contains(["@ageorgedev/game-tools"])) or ($p | contains(["@ageorgedev/game-tools-e2e"]))) then
. + [{"vercel_project_name": env.GAME_TOOLS_PROJECT, "e2e_project": "@ageorgedev/game-tools-e2e", "label": "Game Tools"}]
else . end
')
echo "pr_deploy_matrix=$matrix" >> "$GITHUB_OUTPUT"
Expand Down
1 change: 1 addition & 0 deletions apps/game-tools-e2e/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test-results
11 changes: 11 additions & 0 deletions apps/game-tools-e2e/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "@ageorgedev/game-tools-e2e",
"version": "0.0.1",
"scripts": {
"e2e": "playwright test"
},
"devDependencies": {
"@playwright/test": "1.60.0",
"concurrently": "10.0.3"
}
}
14 changes: 14 additions & 0 deletions apps/game-tools-e2e/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
testDir: './tests',
use: {
baseURL: process.env.BASE_URL ?? 'http://localhost:3001',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
});
18 changes: 18 additions & 0 deletions apps/game-tools-e2e/tests/smoke.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { expect, test } from '@playwright/test';

test('home page renders', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('heading', { name: 'Game Tools' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Characters' })).toBeVisible();
});

test('character list page renders', async ({ page }) => {
await page.goto('/dnd/characters');
await expect(page.getByRole('heading', { name: 'Characters' })).toBeVisible();
await expect(page.locator('body')).not.toBeEmpty();
});

test('claw character sheet renders', async ({ page }) => {
await page.goto('/dnd/characters/claw');
await expect(page.getByRole('heading', { name: 'Claw' })).toBeVisible();
});
9 changes: 9 additions & 0 deletions apps/game-tools-e2e/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "@ageorgedev/ts-config/base.json",
"include": ["tests", "playwright.config.ts"],
"exclude": ["node_modules", "test-results"],
"compilerOptions": {
"outDir": "dist",
"types": ["node"]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
## Context

The main site (`apps/ageorgedev`) has a dedicated Playwright e2e app at `apps/ageorgedev-e2e` that runs against Vercel preview deployments via the shared `run-e2e` composite action. The `game-tools` app has no equivalent — routes can silently break on deploy without any signal.

The existing e2e pattern is intentionally lightweight: a separate app, `chromium`-only, no `webServer` config (CI drives it against a deployed `BASE_URL`, developers start dev servers manually). The CI orchestration lives in `.github/workflows/info.yml`, which builds a matrix of `{ vercel_project_name, e2e_project }` entries; the deploy workflow consumes that matrix and, when `e2e_project` is set, invokes the `run-e2e` action against the deployed URL.

## Goals / Non-Goals

**Goals:**
- Provide fast smoke coverage that game-tools boots and its main routes render.
- Reuse the existing CI plumbing (composite action, matrix pattern, `BASE_URL` env) with zero new infrastructure.
- Detect regressions on PR previews before merge, matching the ageorgedev flow.

**Non-Goals:**
- Deep functional testing of individual character sheets (stats, spellbooks, print layouts).
- Visual regression testing.
- Cross-browser coverage — chromium-only mirrors the existing standard.
- Testing every character sheet; one representative sheet (Claw) suffices for smoke.

## Decisions

**Separate `apps/game-tools-e2e` app (not colocated in `apps/game-tools`).**
Matches the ageorgedev convention exactly. Colocation would require entangling Playwright deps with the app's runtime deps and complicate the CI matrix (the matrix keys off e2e project name). Alternative rejected: colocation adds inconsistency for no gain.

**Chromium-only, no `webServer` config.**
Mirrors `apps/ageorgedev-e2e/playwright.config.ts`. CI always targets a deployed `BASE_URL`; the composite action handles browser install. A `webServer` block would slow local runs and diverge from the existing pattern. Alternative rejected: adding `webServer` for developer convenience — the `yarn turbo dev` step is already the standard local workflow.

**Default `BASE_URL` to `http://localhost:3001`.**
Game-tools runs on port 3001 (per `CLAUDE.md` and app config). Matches the "default to local dev port" pattern from `apps/ageorgedev-e2e`.

**Content-based assertions, not `body not empty`.**
The ageorgedev smoke suite uses specific heading matches for the home page and looser `body not empty` for less-critical pages. For game-tools, the character sheets *are* the main feature — a blank render that returns HTTP 200 would falsely pass a `body not empty` check. Assert on visible headings/names (`"Game Tools"`, `"Characters"`, `"Claw"`) so a broken data pipeline still fails the test.

**Test one character (Claw), not all four.**
Smoke = "does it boot," not "is every character correct." Per-character validation, if wanted later, belongs in a separate spec file (mirroring how `talks.spec.ts` extends `smoke.spec.ts` for the talks section).

**CI wiring in `info.yml`: extend the game-tools matrix entries with `e2e_project`, and treat `@ageorgedev/game-tools-e2e` as an affected trigger for the game-tools preview.**
Direct parallel to the ageorgedev wiring — same shape, same guard. When only the e2e app changes, still deploy the game-tools preview so the tests have something to hit.

## Risks / Trade-offs

- **[Flaky character-name assertion if Claw's route is renamed]** → Mitigation: assert on the character name text (`"Claw"`) which is stable content, not the route path alone. If Claw is ever removed, swap to another character in one line.
- **[Local dev requires manually starting `game-tools` on port 3001]** → Mitigation: document in the new app's absent-by-design (matches the ageorgedev-e2e pattern developers already know). Not a regression.
- **[CI cost: extra chromium install + preview deploy on every game-tools PR]** → Mitigation: chromium install is already cached in the composite action; the game-tools preview already deploys on affected PRs — this only *adds* the e2e run, not a new deploy.
- **[Smoke test could pass while a specific character sheet is broken]** → Accepted trade-off: this is smoke, not functional. Broadening coverage later is a separate change.

## Migration Plan

No migration needed — additive change. Rollout:

1. Land `apps/game-tools-e2e/` with tests.
2. Update `.github/workflows/info.yml` in the same PR.
3. The PR's own CI run will exercise the new wiring end-to-end (game-tools preview deploy → e2e run against it).

Rollback: revert the PR. No data or runtime state involved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
## Why

The `game-tools` app has no automated verification that its routes render on deployed previews. The main site is covered by `@ageorgedev/ageorgedev-e2e` smoke tests wired into the PR + production deploy pipelines, but game-tools regressions (broken routes, blank pages, missing character data) currently go undetected until a manual click-through. As the character catalog grows, a smoke-level safety net is cheap insurance.

## What Changes

- Add a new `apps/game-tools-e2e` Playwright app mirroring the structure of `apps/ageorgedev-e2e`.
- Add smoke tests covering the game-tools home page (`/`), the character list (`/dnd/characters`), and a representative character sheet (`/dnd/characters/claw`).
- Default `BASE_URL` to `http://localhost:3001` (the game-tools dev port), overridable via env for CI runs against Vercel preview URLs.
- Wire the new e2e app into `.github/workflows/info.yml` so it runs against game-tools preview deploys on PRs and production deploys on `main`.

## Capabilities

### New Capabilities
- `game-tools-smoke-tests`: End-to-end smoke tests for the game-tools app that verify key routes render on a configurable base URL, executed against Vercel preview and production deployments via the shared CI matrix.

### Modified Capabilities
- `ci-matrix-deploy`: The PR and production deploy matrices SHALL associate `@ageorgedev/game-tools-e2e` with the game-tools Vercel project, and SHALL treat changes to `@ageorgedev/game-tools-e2e` itself as an affected trigger for that project (mirroring the ageorgedev-e2e wiring).

## Impact

- **New app**: `apps/game-tools-e2e/` (`package.json`, `playwright.config.ts`, `tsconfig.json`, `tests/smoke.spec.ts`).
- **CI**: `.github/workflows/info.yml` PR + production matrix blocks updated to include the new e2e project.
- **Dependencies**: Adds `@playwright/test@1.60.0` to the new workspace (already in use by ageorgedev-e2e — no version drift).
- **No runtime changes** to the game-tools app itself.
- **Local dev**: developers running the smoke tests locally must start `yarn turbo dev --filter=@ageorgedev/game-tools` first (matches existing pattern).
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
## MODIFIED Requirements

### Requirement: Info job outputs production deploy matrix
The Info job SHALL compute and output a `production_deploy_matrix` JSON array containing one entry per deployable app where the corresponding `should_deploy_*` condition is true. Each entry SHALL contain `vercel_project_name` (resolved from vars) and an optional `e2e_project` field. When no apps qualify, the output SHALL be an empty JSON array `[]`.

#### Scenario: Site changed on production push
- **WHEN** `@ageorgedev/ageorgedev` is in the affected projects list
- **THEN** `production_deploy_matrix` contains one entry with `vercel_project_name` set to the resolved site project name and `e2e_project` set to `@ageorgedev/ageorgedev-e2e`

#### Scenario: Design system changed on production push
- **WHEN** `@ageorgedev/design-docs` is in the affected projects list
- **THEN** `production_deploy_matrix` contains one entry with `vercel_project_name` set to the resolved design system project name and no `e2e_project` field

#### Scenario: Game-tools changed on production push
- **WHEN** `@ageorgedev/game-tools` is in the affected projects list
- **THEN** `production_deploy_matrix` contains one entry with `vercel_project_name` set to the resolved game-tools project name and `e2e_project` set to `@ageorgedev/game-tools-e2e`

#### Scenario: Nothing deployable changed
- **WHEN** none of site, design-system, or game-tools is in the affected projects list
- **THEN** `production_deploy_matrix` is `[]`

---

### Requirement: Info job outputs PR deploy matrix
The Info job SHALL compute and output a `pr_deploy_matrix` JSON array using the same structure as `production_deploy_matrix`, except an app entry SHALL be included when either the app itself OR its associated e2e project is in the affected projects list. This ensures e2e-only changes still trigger a preview deploy for the test target.

#### Scenario: Only ageorgedev e2e tests changed on PR
- **WHEN** `@ageorgedev/ageorgedev-e2e` is in the affected projects but `@ageorgedev/ageorgedev` is not
- **THEN** `pr_deploy_matrix` contains the site entry (to run e2e against a deployed preview)

#### Scenario: Only game-tools e2e tests changed on PR
- **WHEN** `@ageorgedev/game-tools-e2e` is in the affected projects but `@ageorgedev/game-tools` is not
- **THEN** `pr_deploy_matrix` contains the game-tools entry with `e2e_project` set to `@ageorgedev/game-tools-e2e`

#### Scenario: Game-tools app changed on PR
- **WHEN** `@ageorgedev/game-tools` is in the affected projects
- **THEN** `pr_deploy_matrix` contains the game-tools entry with `vercel_project_name` set to the resolved game-tools project name and `e2e_project` set to `@ageorgedev/game-tools-e2e`

#### Scenario: Neither game-tools nor its e2e changed on PR
- **WHEN** neither `@ageorgedev/game-tools` nor `@ageorgedev/game-tools-e2e` is in the affected projects
- **THEN** the game-tools entry is absent from `pr_deploy_matrix`
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
## ADDED Requirements

### Requirement: Game-tools home page renders
The system SHALL render the game-tools home page with identifiable content proving the app booted and top-level navigation is present.

#### Scenario: Home page loads successfully
- **WHEN** a user navigates to `/`
- **THEN** the page SHALL display a heading with the text "Game Tools"
- **THEN** the page SHALL display a link to the character list

---

### Requirement: Character list page renders
The system SHALL render the D&D character list with at least one character visible, proving the dynamic route-collection logic resolved successfully.

#### Scenario: Character list loads successfully
- **WHEN** a user navigates to `/dnd/characters`
- **THEN** the page SHALL display a heading with the text "Characters"
- **THEN** the page SHALL display at least one character entry with a visible character name

---

### Requirement: Individual character sheet renders
The system SHALL render an individual character sheet with the character's name visible, proving both routing and character-data hydration succeeded.

#### Scenario: Claw character sheet loads successfully
- **WHEN** a user navigates to `/dnd/characters/claw`
- **THEN** the page SHALL display the character name "Claw"

---

### Requirement: Configurable base URL
The system SHALL target the host defined by the `BASE_URL` environment variable, defaulting to the local game-tools dev port when unset.

#### Scenario: BASE_URL env var used when set
- **WHEN** `BASE_URL` is set to a deployed URL
- **THEN** all test requests SHALL be made against that URL

#### Scenario: Defaults to localhost when BASE_URL is unset
- **WHEN** `BASE_URL` is not set
- **THEN** all test requests SHALL default to `http://localhost:3001`

---

### Requirement: Chromium-only execution
The test suite SHALL run under a single `chromium` Playwright project, matching the convention established by `@ageorgedev/ageorgedev-e2e`.

#### Scenario: Default project configuration
- **WHEN** `playwright test` is invoked with no `--project` flag
- **THEN** tests SHALL execute against Desktop Chrome only
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
## 1. Scaffold the e2e app

- [x] 1.1 Create `apps/game-tools-e2e/package.json` mirroring `apps/ageorgedev-e2e/package.json` — name `@ageorgedev/game-tools-e2e`, `e2e` script running `playwright test`, `@playwright/test@1.60.0` and `concurrently@10.0.3` as devDependencies
- [x] 1.2 Create `apps/game-tools-e2e/playwright.config.ts` mirroring the ageorgedev-e2e config, with `baseURL` defaulting to `http://localhost:3001` and a single `chromium` project
- [x] 1.3 Create `apps/game-tools-e2e/tsconfig.json` matching the ageorgedev-e2e tsconfig
- [x] 1.4 Run `yarn install` at the repo root to register the new workspace

## 2. Write the smoke tests

- [x] 2.1 Create `apps/game-tools-e2e/tests/smoke.spec.ts`
- [x] 2.2 Add test: `/` renders — assert heading with name "Game Tools" is visible and a link to `/dnd/characters` is present
- [x] 2.3 Add test: `/dnd/characters` renders — assert heading "Characters" is visible and at least one character name is visible
- [x] 2.4 Add test: `/dnd/characters/claw` renders — assert the character name "Claw" is visible on the sheet

## 3. Verify tests locally

- [x] 3.1 Start game-tools dev server in a separate terminal: `yarn turbo dev --filter=@ageorgedev/game-tools`
- [x] 3.2 Run `yarn turbo e2e --filter=@ageorgedev/game-tools-e2e` and confirm all three tests pass
- [x] 3.3 Confirm `yarn format-and-lint:fix` reports clean

## 4. Wire into CI

- [x] 4.1 Update `.github/workflows/info.yml` production matrix: add `"e2e_project": "@ageorgedev/game-tools-e2e"` to the game-tools entry
- [x] 4.2 Update `.github/workflows/info.yml` PR matrix: add `"e2e_project": "@ageorgedev/game-tools-e2e"` to the game-tools entry AND extend the condition so `@ageorgedev/game-tools-e2e` in affected projects also triggers the game-tools preview deploy (mirroring the ageorgedev-e2e OR-condition pattern)
- [x] 4.3 Sanity-check the `jq` expressions by running them locally against a sample `affected` array or by staging the PR and inspecting the workflow's Debug output

## 5. Validate on PR

- [ ] 5.1 Open the PR and confirm the game-tools preview deploys
- [ ] 5.2 Confirm the e2e job runs against the deployed preview URL and passes
- [ ] 5.3 Confirm no regressions in the ageorgedev preview + e2e flow
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: my-workflow
created: 2026-07-03
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
## Context

`packages/dnd-character-sheet` has no tests. The package contains pure calculation functions (`abilityModifier`, `proficiencyBonus`, `calculateStats`) alongside formatting utilities and data extraction helpers. These functions encode D&D 5e rules directly and are the highest-value testing targets. The monorepo already has a shared Vitest/jsdom config in `packages/testing-config`.

## Goals / Non-Goals

**Goals:**
- Test all pure functions with meaningful assertions (no fluff)
- Cover all three `statMod` variant types in `calculateStats`
- Use minimal inline fixtures (not the example character) for stability
- Compress related assertions into single tests with multiple `expect()`s

**Non-Goals:**
- Testing React components
- Testing DOMPurify's sanitisation behaviour (third-party responsibility)
- 100% line coverage — coverage is a byproduct, not the goal

## Decisions

**Decision: Minimal inline fixtures over reusing `example-wizard.data.ts`**
The example character is narrative data maintained for UI demonstration. Coupling tests to it makes assertions brittle when the character's stats are updated for storytelling reasons. Minimal fixtures with controlled values make test intent explicit.

**Decision: Multiple `expect()`s per test for related assertions**
Functions like `abilityModifier` and `proficiencyBonus` have a small, enumerable input space. A single test with compressed assertions (e.g., all level tiers in one block) is more readable than 5 separate `it()` blocks testing the same function.

**Decision: Test `enrichCharacterData` selection logic, not EJS internals**
The value in testing `enrichCharacterData` is verifying *which fields* on the character get template-processed (feature descriptions) and which don't (ability scores, etc.). EJS interpolation correctness is tested as a side-effect of exercising a real template token.

**Decision: Test all three `statMod` types in `calculateStats`**
`'static-skill-additions'`, `'skill-function'`, and `'generic-derived'` each take a distinct code path. All three are exercised with a minimal character fixture that isolates each variant.

## Risks / Trade-offs

- [Risk] `calculateStats` signature changes → Tests will catch this at compile time (TypeScript) and fail fast
- [Risk] EJS version changes affect template syntax → Single `enrichCharacterData` test contains the blast radius
Loading