diff --git a/CHANGELOG.md b/CHANGELOG.md index 210d4e13..19ecbe3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -319,6 +319,13 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Under the hood (tidying) +- **Parts switched off, proven in a browser.** The third gap in e2e/COVERAGE.md is closed: a new + browser test runs a custom system with every part on and checks what each shows, the player's + BANK and HIT_POINTS buttons, their MAX HEALTH, ARMOR and SAVE_STATS, IMPORT on their sheet, and the + GM's initiative tracker, PAY_PLAYERS, GIVE_ITEM and buy-back rate. It then publishes a version with + those parts off and checks each one is gone for both, then turns them back on and checks they all + return. The parts only the built-in games have (vehicles, cyberware, death saves, luck, XP) have + nothing to hide at a custom table; NPC tiers waits for the NPC family. - **Lists inside lists, the server's half (7a5a1).** A list can now say how it is picked: on its own (as every list was), only through the features that open it (a SOLDIER's SPECIALTY at level 3), or both (FEATS: one at creation, more from features). A feature opening a list says how many picks it diff --git a/README.md b/README.md index f390bd38..75a73ae6 100644 --- a/README.md +++ b/README.md @@ -1272,6 +1272,7 @@ CITY_NET/ │ ├── builder-journey.spec.ts # The GM builds a system from + NEW through the builder's screens, publishes it, publishes v2 through the version preview, and switches the game to it; from Phase 6 a save with a target whose players see only the verdict, rolled by a player from their sheet, the GM told more │ ├── player-journey.spec.ts # A custom system played in two browsers: a player makes a character in its words and numbers; the GM types in a catalogue, gives them an item and calls a rest; the player sees both │ ├── health-models.spec.ts # Every health model at the table: a player hurts and heals their token from HIT_POINTS under each of the seven, the GM sets a second track's maximum, another player watches from GLOBAL_CHAT and is sent no number or note (coverage family 2) +│ ├── parts-off.spec.ts # Parts switched off at the table: every part a custom table shows (bank, shops, initiative, combat, token health, sheet import, GIVE ITEM), hidden for the player and the GM in a version with it off, back when on (coverage family 3) │ ├── sheet-choices.spec.ts # A custom system's CHOICES block at the table (7a3b1): a player's class not met off with what it needs, SOLDIER picked with its features and its bonus on Defense, raised to level 2 unlocking the next feature, a use ticked off and given back by a short rest the GM calls; as the server keeps it; the GM on their own sheet adding an option not met, the bonus named on Defense, and a pick taken away, asked first (7a3b2) │ ├── sheet-nested-choices.spec.ts # Lists inside lists (7a5a2): in the builder SPECIALTIES (OPENED BY the Soldier's Specialty) set THROUGH FEATURES and COMMONS EVERYONE HAS IT; at the table a specialty picked under the feature at level 3, kept waiting at 2, back at 3; COMMONS every character's, its Improvement at level 2 │ ├── sheet-visibility.spec.ts # Who sees what on a customized sheet: the owner (the GM's field read-only), another player looking them up from GLOBAL_CHAT (public lines only, nothing else sent), the GM opening the whole sheet the same way and setting the GM's field (coverage family 1) diff --git a/e2e/COVERAGE.md b/e2e/COVERAGE.md index 215ae55d..69790777 100644 --- a/e2e/COVERAGE.md +++ b/e2e/COVERAGE.md @@ -28,7 +28,9 @@ panel), so a control that looks fine but does nothing is caught for all of them. | Option | Variants | Server tests | Browser test | Status | |---|---|---|---|---| | Words renamed | HP, the GM, any term; one / many / short | `words`, `initiative_words` | `builder-journey` (renamed on WORDS), `player-journey` (WOUNDS, "THE KEEPER GAVE YOU") | **AT THE TABLE** | -| Parts switched off | bank, shops, vehicles, cyberware, initiative, combat, token health, death, luck, XP, NPC tiers, sheet import, GIVE ITEM | `parts`, `parts_*` | `builder-journey` (VEHICLES off, in the builder) | **GAP**: never checked hidden at the table | +| Parts switched off | bank, shops, initiative, combat, token health, sheet import, GIVE ITEM | `parts`, `parts_*` | `parts-off` (each shown, hidden in a version with it off, back when on: the player's BANK and HIT_POINTS, MAX HEALTH, ARMOR and SAVE_STATS, IMPORT on the sheet; the GM's INITIATIVE_TRACKER, PAY_PLAYERS, GIVE_ITEM, the buy-back rate), `builder-journey` (VEHICLES off, in the builder) | **AT THE TABLE** | +| Parts only built-in games have | vehicles, cyberware, death saves, luck, XP awards | `parts` | | Nothing to hide at a custom table: shown off and greyed in PARTS | +| NPC tiers off | | `parts_tiers` | | **GAP**, with family 8 (NPC tiers): the tier picker is on an NPC token in the 3D map | | GIVE ITEM and shop catalogues | a catalogue typed in, an item given with a note | `shops_give`, `shop_catalogue_*` | `player-journey` | **AT THE TABLE** | | Building types and catalogues renamed or off | | `buildings` | | **GAP** | | Currencies | several, decimals, a catalogue's currency, the cart per currency, PAYROLL | `currencies`, `currency_icons`, `catalogue_currency`, `bank_currencies`, `shop_checkout_currencies` | | **GAP** | @@ -134,7 +136,7 @@ Fitted into Phase 7, one family between each of its pieces, riskiest first: 1. ~~**Sheet visibility**~~: done, `sheet-visibility.spec.ts`. It also gave the journeys a way to reach another player's token without the 3D map: GLOBAL_CHAT, their name, VIEW_PLAYER_INFO. 2. ~~**Health models**~~: done, `health-models.spec.ts`. It gave the journeys a token on the map without the 3D map too: the GM posts one to /api/locations, and HIT_POINTS opens it. -3. **Parts switched off**: each part hidden at the table while off, and back when on. +3. ~~**Parts switched off**~~: done, `parts-off.spec.ts`, but for NPC tiers, which waits for family 8. A new version published through the API (the draft, then PUBLISH) is how a journey changes a running system. 4. **House rules at the table**: switched in the GAME tab, and what waits on them shown and hidden. 5. **Conditions on tokens**: put on, a modifier reaching a stat, a formula and every roll, ending after rounds and at a rest. 6. **Rest variants**: every kind of refill, a rest counting as another, one switched off, the system's own. diff --git a/e2e/README.md b/e2e/README.md index 1c86489c..422ca9e9 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -147,6 +147,7 @@ frontend suites. | `tests/sheet-rolls.spec.ts` | A custom system's own roll at the table (6b2a): a published system running as the game with a roll rolled by a stats group; a player's sheet showing a die button by each stat in it, and a click rolled by the server from the stored sheet (read from what it broadcasts, the line the log shows); a stat with two rolls asking first, GRIT SAVE with ADVANTAGE picked in the chooser, two d20 the higher kept; PUSH adding 2 Stress (6b3a), the chooser showing 1 OF 4, the roll paying it and the sheet showing 3, then off at 3 OF 4; and a running system's own dice in the dice menu (6c4a): listed under THIS GAME'S DICE, two Good and a Bad rolled by the server, each face as the log writes it | | `tests/sheet-choices.spec.ts` | A custom system's CHOICES block at the table (7a3b1): a player picks a class on their sheet (one they don't meet is off, saying what it needs), its bonus reaches Defense, a level up unlocks the next feature, a use ticked off comes back with a short rest the GM calls; and the GM on their own sheet adds an option they don't meet (ADD ANYWAY), sees Guard Stance's bonus named on Defense, and takes a pick away, asked first (7a3b2) | | `tests/health-models.spec.ts` | Every health model at the table (coverage family 2): for each, a system running it and a player's token; the player hurts and heals it from HIT_POINTS (a pool's own name; two tracks spilling over, the GM setting the second's maximum; damage types light and heavy; harm levels moving up, refused without a sheet; a wound count to INCAPACITATED; hit locations with notes; none), and another player watches from GLOBAL_CHAT (VIEW_PLAYER_INFO, HEALTH): the heartbeat's band, a bar, the worst harm's name, which place is hurt, and the frames they are sent hold no number or note | +| `tests/parts-off.spec.ts` | Parts switched off at the table (coverage family 3): a custom system with every part on shows the player BANK and HIT_POINTS on the rail, MAX HEALTH, ARMOR and SAVE_STATS in their character controls and IMPORT on their sheet, and the GM INITIATIVE_TRACKER, PAY_PLAYERS, GIVE_ITEM and the buy-back rate; a version with bank, shops, initiative, combat, token health, sheet import and GIVE ITEM off hides every one; back on, they return | | `tests/sheet-nested-choices.spec.ts` | Lists inside lists, and a list everyone has (7a5a2): in the builder SPECIALTIES shows OPENED BY · SOLDIER: SPECIALTY (LV 3) and is set THROUGH FEATURES, COMMONS EVERYONE HAS IT, saved with no problems; published, a player's sheet has no SPECIALTIES block, COMMONS every character's with Improvement next at level 2 and then unlocked; SOLDIER at 3 offers PICK A SPECIALTY under its feature, DUELIST picked there with its own feature; lowered to 2 it waits, kept; back at 3 it counts again; as the server keeps it | | `tests/sheet-visibility.spec.ts` | Who sees what on a customized sheet (coverage family 1): its owner fills in a public field, a private one and a combat value and can't change the GM's field; another player, looking them up from GLOBAL_CHAT's user list (VIEW_PLAYER_INFO), sees only the public lines, and the server sends nothing else; the GM opens the whole sheet the same way (OPEN_SHEET) and sets the GM's field, which the owner then sees | | `tests/player-journey.spec.ts` | A custom system running (set up through the API), played in two browsers at once: a player makes a character and sees the system's words and its formula worked out; the GM types in a catalogue, gives the player something from it with a note, and calls a rest from the GAME tab; the player sees the gift arrive and the rest change their sheet | diff --git a/e2e/tests/parts-off.spec.ts b/e2e/tests/parts-off.spec.ts new file mode 100644 index 00000000..2784e6be --- /dev/null +++ b/e2e/tests/parts-off.spec.ts @@ -0,0 +1,93 @@ +import { test, expect, type Page } from '@playwright/test'; +import { signInAsGm, signInAsPlayer, gmToken, asGm, runningSystem, approvedPlayer, openGameSystem } from './helpers'; + +// Parts switched off, at the table (coverage family 3, e2e/COVERAGE.md; the parts from 3b, approved +// mockup docs/mockups/builder-words-features.html): a custom system running with every part on shows +// each one's controls to its player and its GM; a new version with them off hides every one; switched +// back on, they all return (off only hides, nothing is deleted). The parts that only built-in games +// have (vehicles, cyberware, death saves, luck, XP awards) have nothing to hide at a custom table. + +const PARTS_OFF = ['bank', 'shops', 'initiative', 'combat', 'token_health', 'sheet_import', 'give_items']; +const definition = (name: string, off: string[] = []) => ({ + format: 1, + name, + core: { health: { model: 'pool' } }, + ...(off.length ? { parts: Object.fromEntries(off.map((p) => [p, { on: false }])) } : {}), +}); + +/** What a part shows the player: the rail, their character's controls, their sheet. */ +const playerSees = async (page: Page, on: boolean) => { + const shown = (locator: ReturnType) => (on ? expect(locator).toBeVisible() : expect(locator).toHaveCount(0)); + await shown(page.getByRole('button', { name: 'CITY_NET // BANK', exact: true })); + await shown(page.getByRole('button', { name: 'HIT_POINTS', exact: true })); + await page.getByRole('button', { name: 'CHARACTER_CONTROLS', exact: true }).click(); + await shown(page.getByText('MAX HEALTH', { exact: true })); + await shown(page.getByText(/^ARMOR(_CLASS)?$/)); + await shown(page.getByRole('button', { name: 'SAVE_STATS', exact: true })); + await page.getByRole('button', { name: 'CHARACTER_SHEET', exact: true }).click(); + await expect(page.getByLabel('Name', { exact: true })).toBeVisible(); + await shown(page.getByRole('button', { name: 'IMPORT', exact: true })); +}; + +/** What a part shows the GM: the initiative tracker on the rail, and the GAME tab's money and items. */ +const gmSees = async (page: Page, on: boolean) => { + const shown = (locator: ReturnType) => (on ? expect(locator).toBeVisible() : expect(locator).toHaveCount(0)); + await shown(page.getByRole('button', { name: 'INITIATIVE_TRACKER', exact: true })); + await openGameSystem(page); + // The tab itself, so a hidden part is told apart from a tab that never opened. + await expect(page.getByRole('button', { name: /TTRPG_SYSTEM$/ })).toBeVisible(); + await shown(page.getByRole('button', { name: 'PAY_PLAYERS', exact: true })); + await shown(page.getByRole('button', { name: 'GIVE_ITEM', exact: true })); + // SHOP_BUY_BACK (in the system's word for a shop): its box, as its label holds a hint too. + await shown(page.locator('#buyback-pct')); +}; + +test('every part a custom table shows, hidden while off and back when on', async ({ page, browser, request }, info) => { + test.setTimeout(180_000); + const again = info.retry ? `_${info.retry + 1}` : ''; + const name = `Parted Keep${again}`; + const gm = await gmToken(request); + const id = await runningSystem(request, gm, definition(name)); + const player = { username: `e2e_parted${again}`, password: 'e2e-player-password' }; + await approvedPlayer(request, gm, player); + const context = await browser.newContext({ baseURL: info.project.use.baseURL }); + const playerPage = await context.newPage(); + + /** A new version of the system, published, and both screens opened fresh on it. */ + const publish = async (off: string[]) => { + await asGm(request, gm, 'put', `/api/systems/${id}/draft`, { definition: definition(name, off) }); + await asGm(request, gm, 'post', `/api/systems/${id}/publish`); + }; + const fresh = async () => { + await page.goto('/'); + await signInAsGm(page); + await playerPage.goto('/'); + await signInAsPlayer(playerPage, player); + }; + + // Every part on: each shows. + await fresh(); + await playerSees(playerPage, true); + await gmSees(page, true); + + // Off: each hidden, for the player and the GM. + await publish(PARTS_OFF); + await fresh(); + await playerSees(playerPage, false); + await gmSees(page, false); + + // Shops alone off: the buy-back rate goes, the money stays. + await publish(['shops']); + await page.goto('/'); + await signInAsGm(page); + await openGameSystem(page); + await expect(page.getByRole('button', { name: 'PAY_PLAYERS', exact: true })).toBeVisible(); + await expect(page.locator('#buyback-pct')).toHaveCount(0); + + // Back on: each returns. + await publish([]); + await fresh(); + await playerSees(playerPage, true); + await gmSees(page, true); + await context.close(); +});