Skip to content

Commit e91967a

Browse files
committed
presets, status banner, script progress highlighting, Playwright smoke test, and README demo guidance
1 parent 0c1fcc7 commit e91967a

9 files changed

Lines changed: 254 additions & 2 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
/node_modules
22
/dist
3+
/test-results
4+
/playwright-report

README.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Toy Robot simulator with two modes:
55
- CLI simulator (`npm start`)
66
- Browser game for GitHub Pages (`npm run web:start`)
77

8-
Both modes use the same simulator core and command rules.
8+
The browser experience is an interactive game powered by the same command engine and rules as the CLI.
99

1010
## Instructions
1111

@@ -143,10 +143,23 @@ Then open:
143143
Features:
144144

145145
- Visual 6x6 board with robot direction icon
146+
- Demo preset buttons for Example A/B/C
146147
- Single command input
147148
- Multiline script mode (Step / Run All)
149+
- Script progress preview with active-line highlight during stepping
150+
- Latest command status banner (`SUCCESS` / `FAILED`)
148151
- Command log with success/fail messages
149152

153+
## Demo Walkthrough
154+
155+
For a clean live demo:
156+
157+
1. Open the app and show the `Board (0..5)` grid.
158+
2. Click `Example C` in `Demo Presets`.
159+
3. Click `Step` repeatedly and point out the highlighted active script line.
160+
4. Show `Current State`, `Latest Status`, and `Command Log` updating together.
161+
5. Click `Run All` for `Example A` to quickly show expected `REPORT` output.
162+
150163
## GitHub Pages
151164

152165
Deployment is automated by GitHub Actions via `.github/workflows/deploy-pages.yml`.
@@ -156,3 +169,17 @@ Deployment is automated by GitHub Actions via `.github/workflows/deploy-pages.ym
156169
- [https://leo0331.github.io/ToyRobot/](https://leo0331.github.io/ToyRobot/)
157170

158171
If your repo name or owner changes, update the URL accordingly.
172+
173+
## E2E Smoke Test (Playwright)
174+
175+
Install Playwright browsers once:
176+
177+
```bash
178+
npx playwright install
179+
```
180+
181+
Run smoke test:
182+
183+
```bash
184+
npm run test:e2e
185+
```

index.html

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,21 @@
1010
<main class="app">
1111
<section class="panel">
1212
<h1>Toy Robot</h1>
13-
<p class="subhead">Web game mode with the same command rules as CLI.</p>
13+
<p class="subhead">
14+
Interactive browser game powered by the same command engine as the CLI.
15+
</p>
16+
<p id="latest-status" class="latest-status neutral">
17+
Waiting for command...
18+
</p>
19+
20+
<div class="controls">
21+
<h2>Demo Presets</h2>
22+
<div class="row">
23+
<button class="preset" data-preset="A" type="button">Example A</button>
24+
<button class="preset" data-preset="B" type="button">Example B</button>
25+
<button class="preset" data-preset="C" type="button">Example C</button>
26+
</div>
27+
</div>
1428

1529
<div class="controls">
1630
<h2>Single Command</h2>
@@ -39,6 +53,7 @@ <h2>Script Mode</h2>
3953
<button id="reset-script" type="button">Reset Script Cursor</button>
4054
<button id="reset-state" type="button">Reset Robot</button>
4155
</div>
56+
<ol id="script-preview" class="script-preview"></ol>
4257
</div>
4358

4459
<div class="status">

package-lock.json

Lines changed: 64 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,22 @@
66
"scripts": {
77
"start": "babel-node ./src/index.js",
88
"test": "jest --runInBand",
9+
"test:e2e": "playwright test",
910
"web:build": "node ./scripts/build-site.js",
1011
"web:start": "npm run web:build && node ./scripts/serve-static.js dist"
1112
},
1213
"author": "Leo",
1314
"license": "ISC",
15+
"jest": {
16+
"testPathIgnorePatterns": ["/node_modules/", "/test/e2e/"]
17+
},
1418
"devDependencies": {
1519
"@babel/cli": "^7.23.9",
1620
"@babel/core": "^7.23.9",
1721
"@babel/node": "^7.23.9",
1822
"@babel/preset-env": "^7.23.9",
1923
"@babel/register": "^7.23.7",
24+
"@playwright/test": "^1.54.2",
2025
"jest": "^29.7.0",
2126
"prettier": "3.2.5"
2227
}

playwright.config.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
const { defineConfig } = require("@playwright/test");
2+
3+
module.exports = defineConfig({
4+
testDir: "./test/e2e",
5+
retries: 1,
6+
use: {
7+
baseURL: "http://127.0.0.1:8080",
8+
headless: true,
9+
},
10+
webServer: {
11+
command: "npm run web:start",
12+
port: 8080,
13+
reuseExistingServer: true,
14+
timeout: 120000,
15+
},
16+
});

test/e2e/web-game.spec.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
const { test, expect } = require("@playwright/test");
2+
3+
test("web game loads and runs a simple script", async ({ page }) => {
4+
await page.goto("/");
5+
6+
await expect(page.getByRole("heading", { name: "Toy Robot" })).toBeVisible();
7+
await expect(page.locator("#board .cell")).toHaveCount(36);
8+
9+
await page.getByRole("button", { name: "Example A" }).click();
10+
await page.getByRole("button", { name: "Run All" }).click();
11+
12+
await expect(page.locator("#state-line")).toHaveText("0,1,NORTH");
13+
await expect(page.locator("#latest-status")).toContainText("0,1,NORTH");
14+
await expect(page.locator("#log")).toContainText("REPORT");
15+
});

web/app.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,55 @@ const resetStateButton = document.getElementById("reset-state");
1515
const board = document.getElementById("board");
1616
const log = document.getElementById("log");
1717
const stateLine = document.getElementById("state-line");
18+
const latestStatus = document.getElementById("latest-status");
19+
const scriptPreview = document.getElementById("script-preview");
20+
const presetButtons = document.querySelectorAll(".preset");
1821

1922
let state = { ...initialState };
2023
let scriptCommands = [];
2124
let scriptCursor = 0;
2225

26+
const demoPresets = {
27+
A: "PLACE 0,0,NORTH\nMOVE\nREPORT",
28+
B: "PLACE 0,0,NORTH\nLEFT\nREPORT",
29+
C: "PLACE 1,2,EAST\nMOVE\nMOVE\nLEFT\nMOVE\nREPORT",
30+
};
31+
2332
function escapeHtml(text) {
2433
return String(text)
2534
.replaceAll("&", "&amp;")
2635
.replaceAll("<", "&lt;")
2736
.replaceAll(">", "&gt;");
2837
}
2938

39+
function setStatusBanner(status, message) {
40+
latestStatus.classList.remove("success", "fail", "neutral");
41+
42+
if (status === "success") {
43+
latestStatus.classList.add("success");
44+
} else if (status === "fail") {
45+
latestStatus.classList.add("fail");
46+
} else {
47+
latestStatus.classList.add("neutral");
48+
}
49+
50+
latestStatus.textContent = message;
51+
}
52+
53+
function renderScriptPreview() {
54+
if (scriptCommands.length === 0) {
55+
scriptPreview.innerHTML = "<li>No parsed script commands yet.</li>";
56+
return;
57+
}
58+
59+
scriptPreview.innerHTML = scriptCommands
60+
.map((line, index) => {
61+
const activeClass = index === scriptCursor ? "active-line" : "";
62+
return `<li class="${activeClass}">${escapeHtml(line)}</li>`;
63+
})
64+
.join("");
65+
}
66+
3067
function renderState() {
3168
if (!state.Placed) {
3269
stateLine.textContent = "Not placed";
@@ -65,6 +102,7 @@ function executeCommand(commandText) {
65102
state = result.state;
66103
renderState();
67104
renderBoard();
105+
setStatusBanner(result.status, result.message);
68106
appendLog({
69107
commandText,
70108
status: result.status,
@@ -76,6 +114,7 @@ function executeCommand(commandText) {
76114
function reloadScriptCommands() {
77115
scriptCommands = parseScript(scriptInput.value);
78116
scriptCursor = 0;
117+
renderScriptPreview();
79118
}
80119

81120
runCommandButton.addEventListener("click", () => {
@@ -98,6 +137,7 @@ stepScriptButton.addEventListener("click", () => {
98137
}
99138

100139
if (scriptCursor >= scriptCommands.length) {
140+
setStatusBanner("fail", "Command failed: Script has no remaining commands");
101141
appendLog({
102142
commandText: "SCRIPT",
103143
status: "fail",
@@ -109,16 +149,20 @@ stepScriptButton.addEventListener("click", () => {
109149

110150
executeCommand(scriptCommands[scriptCursor]);
111151
scriptCursor += 1;
152+
renderScriptPreview();
112153
});
113154

114155
runScriptButton.addEventListener("click", () => {
115156
reloadScriptCommands();
116157
scriptCommands.forEach((command) => executeCommand(command));
117158
scriptCursor = scriptCommands.length;
159+
renderScriptPreview();
118160
});
119161

120162
resetScriptButton.addEventListener("click", () => {
121163
scriptCursor = 0;
164+
renderScriptPreview();
165+
setStatusBanner("success", "Command success: Script cursor reset");
122166
appendLog({
123167
commandText: "SCRIPT",
124168
status: "success",
@@ -131,6 +175,7 @@ resetStateButton.addEventListener("click", () => {
131175
state = { ...initialState };
132176
renderState();
133177
renderBoard();
178+
setStatusBanner("success", "Command success: Robot state reset");
134179
appendLog({
135180
commandText: "RESET",
136181
status: "success",
@@ -139,5 +184,19 @@ resetStateButton.addEventListener("click", () => {
139184
});
140185
});
141186

187+
scriptInput.addEventListener("input", () => {
188+
reloadScriptCommands();
189+
});
190+
191+
presetButtons.forEach((button) => {
192+
button.addEventListener("click", () => {
193+
const presetKey = button.dataset.preset;
194+
scriptInput.value = demoPresets[presetKey] || "";
195+
reloadScriptCommands();
196+
setStatusBanner("neutral", `Preset loaded: Example ${presetKey}`);
197+
});
198+
});
199+
142200
renderState();
143201
renderBoard();
202+
reloadScriptCommands();

0 commit comments

Comments
 (0)