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
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ See [docs/ci/README.md](docs/ci/README.md). Manual fallback: `npm run deploy:pag

- Choose **Easy** or **Standard** pace (Easy keeps tickets/coach tips open for learning)
- Realistic rack chassis: panel, fiber tray, ToR switch, SFP, firewall, servers, console station, PDU, ISP peer
- Plug data / fiber / power / console cords (tap-tap or drag)
- Side config panel: IPv4/gateway, switchport VLAN/mode, ping, firewall ACLs, static NAT
- 32 missions through floating statics, PAT overload, and traceroute
- Sandbox save/load + ticket presets; progress export/import; first-run tips
- Custom ACL, PAT, route AD, traceroute; glossary, sound, Easy/Standard learning pace
- Plug data / fiber / power / console cords (tap-tap or drag); **Undo**, zoom/pan, cable history
- Side config panel + **CLI-lite** (`no shut`, `switchport`, `ip address`, `ping`, …)
- 32 missions + optional **transfer variants**; concept mastery map; incident-style challenge briefs
- Debrief path compare; sandbox **share URLs**; classroom codes (`PATCHLAB-LAB` / `PATCHLAB-SANDBOX`)
- PWA installable build; local analytics queue; progress export/import; glossary & sound

## Docs

Expand Down
31 changes: 31 additions & 0 deletions e2e/patchlab.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,37 @@ test.describe('visual & interaction polish', () => {
});
});

test.describe('feature wave', () => {
test('concept map, classroom unlock, and undo', async ({ page }) => {
await clearApp(page);
await expect(page.locator('.concept-map')).toBeVisible();
await expect(page.locator('.classroom-panel')).toBeVisible();

await page.locator('.classroom-code-input').fill('PATCHLAB-SANDBOX');
await page.getByRole('button', { name: /^Apply$/i }).click();
await expect(page.locator('.progress-notice.ok')).toContainText(/Sandbox unlocked/i);

await startMission(page, /First Lights On/i);
const before = await page.locator('.cable-path').count();
await connectPorts(page, /Panel-A A-01/, /ToR-SW-A Gi1\/0\/1 VLAN 10/);
await expect(page.locator('.cable-path')).toHaveCount(before + 1);
await expect(page.getByRole('button', { name: /^Undo$/i })).toBeEnabled();
await page.getByRole('button', { name: /^Undo$/i }).click();
await expect(page.locator('.cable-path')).toHaveCount(before);
});

test('transfer variant appears after clearing parent', async ({ page }) => {
await clearApp(page);
await unlockThrough(page, 2); // clears m1 in campaign order
await expect(
page.getByRole('button', { name: /^1 First Lights On/i }),
).toBeVisible();
await expect(
page.getByRole('button', { name: /^Transfer · First Lights On/i }),
).toBeVisible();
});
});

test.describe('home & shell', () => {
test('home, glossary, and sound toggle', async ({ page }) => {
await clearApp(page);
Expand Down
Loading
Loading