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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 4 additions & 2 deletions e2e/COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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** |
Expand Down Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
93 changes: 93 additions & 0 deletions e2e/tests/parts-off.spec.ts
Original file line number Diff line number Diff line change
@@ -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<Page['getByRole']>) => (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<Page['getByRole']>) => (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();
});
Loading