Skip to content

Commit 2dfdf00

Browse files
committed
Fix Input Mapping V2 auto gamepad polling and selection flow - PR_26140_093-fix-input-mapping-v2-auto-gamepad-and-selection-flow
1 parent 8cef92b commit 2dfdf00

10 files changed

Lines changed: 248 additions & 45 deletions

File tree

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# PR_26140_093 Input Mapping V2 Auto Gamepad and Selection Flow Report
2+
3+
## Scope
4+
- Updated Input Mapping V2 only, plus focused Workspace V2 Playwright coverage.
5+
- Active `docs/pr/BUILD_PR.md` was for an unrelated Level 18 rebase, so the inline `PR_26140_093-fix-input-mapping-v2-auto-gamepad-and-selection-flow` BUILD request was used as the source of truth.
6+
- No schemas were changed.
7+
- No sample JSON was touched.
8+
9+
## Failing Tool Before
10+
- Input Mapping V2 had an exposed manual `Start Listening / Poll Gamepads` control, capture success messages were echoed in the Capture accordion, captured tiles did not select their action, and created action tiles could be duplicated from the action selector.
11+
12+
## Tool Fixed
13+
- Input Mapping V2.
14+
15+
## Changes
16+
- Kept tool-owned automatic `navigator.getGamepads()` refresh on load and interval polling while the tool is active.
17+
- Removed the `Start Listening / Poll Gamepads` control and handler.
18+
- Kept one full-width capture button per detected gamepad, labeled by device name and index.
19+
- Moved `Refresh Gamepads` to the bottom of the Capture accordion.
20+
- Added selected-state rendering for Captured Mapping tiles.
21+
- Clicking a Captured Mapping tile now selects that tile's action and updates Selected Action.
22+
- Mapping success messages now go to the main Status log only.
23+
- Added `Delete Action`.
24+
- Renamed `Clear Action Inputs` to `Clear Actions`.
25+
- Created action tiles are single-instance; their actions are disabled in the Selected Action dropdown until deleted.
26+
- Preserved combo input behavior, 175x175 mapping tiles, fullscreen layout, and the existing toolState payload contract.
27+
28+
## Remaining Failures After
29+
- None observed in requested validation.
30+
31+
## Playwright
32+
Playwright impacted: Yes.
33+
34+
Validated behavior:
35+
- gamepads auto-poll on load with mocked `navigator.getGamepads()`;
36+
- detected gamepads become assignable capture sources;
37+
- no `Start Listening / Poll Gamepads` control appears;
38+
- `Refresh Gamepads` appears at the bottom of the Capture accordion;
39+
- selected mapping tile is visibly indicated;
40+
- clicking a tile updates Selected Action;
41+
- mapping messages appear in the main Status log, not inside the Capture accordion;
42+
- `Delete Action` removes the selected action tile;
43+
- `Clear Actions` label is correct;
44+
- created actions are disabled/unselectable in the dropdown and cannot be duplicated;
45+
- combo input behavior remains intact by preserving multiple bindings on one tile.
46+
47+
Expected pass behavior:
48+
- Mocked gamepads appear as device-specific capture buttons automatically.
49+
- Created action tiles are highlighted when selected, set the dropdown value when clicked, and cannot be duplicated.
50+
- Capture success text is present in `#statusLog` and absent from `#captureInputContent`.
51+
52+
Expected fail behavior:
53+
- If polling, tile selection, or single-instance action locking regresses, the focused Workspace V2 Input Mapping tests fail before the full suite completes.
54+
55+
## Validation
56+
- PASS: targeted syntax validation for changed Input Mapping V2 JS files and the touched Workspace V2 Playwright spec.
57+
- PASS: focused Playwright Input Mapping V2 slice:
58+
`npx playwright test tests/playwright/tools/WorkspaceManagerV2.spec.mjs -g "auto-polls Input Mapping V2 gamepads on load|launches Input Mapping V2 and captures keyboard mappings" --project=playwright --workers=1 --reporter=list`
59+
- PASS: `npm run test:workspace-v2` with 61 passed.
60+
- PASS: Input Mapping V2 HTML external JS/CSS contract scan rerun with `rg --pcre2`; no inline scripts, inline styles, or inline event handlers found.
61+
- PASS: Playwright V8 coverage report was generated by the Workspace V2 suite at `docs/dev/reports/playwright_v8_coverage_report.txt`.
62+
- Full samples smoke test skipped because this PR is limited to Input Mapping V2 and Workspace V2 coverage; sample JSON is out of scope.
63+
64+
## Manual Validation
65+
1. Launch Input Mapping V2.
66+
2. Confirm the Capture accordion has no `Start Listening / Poll Gamepads` button and `Refresh Gamepads` is the last control in Capture.
67+
3. Connect or expose a gamepad to the browser and confirm a capture button appears for each device with its name and Gamepad index.
68+
4. Add an action tile and confirm its action is grayed out in Selected Action and cannot create a second tile.
69+
5. Add a keyboard input, confirm the mapping message appears in Status, and confirm it does not appear in Capture.
70+
6. Create a second action tile, click between tiles, and confirm the selected tile highlight and Selected Action value update.
71+
7. Click `Delete Action` and confirm the selected tile is removed and the action becomes selectable again.

tests/playwright/tools/WorkspaceManagerV2.spec.mjs

Lines changed: 85 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1390,6 +1390,57 @@ test.describe("Workspace Manager V2 bootstrap", () => {
13901390
}
13911391
});
13921392

1393+
test("auto-polls Input Mapping V2 gamepads on load", async ({ page }) => {
1394+
await page.addInitScript(() => {
1395+
window.__inputMappingV2MockGamepads = [
1396+
{
1397+
axes: [0, 0],
1398+
buttons: [{ pressed: false }, { pressed: false }],
1399+
connected: true,
1400+
id: "Load Pad Alpha",
1401+
index: 0,
1402+
mapping: "standard",
1403+
timestamp: 11
1404+
},
1405+
{
1406+
axes: [0, 0, 0, 0],
1407+
buttons: [{ pressed: false }, { pressed: false }, { pressed: false }],
1408+
connected: true,
1409+
id: "Load Pad Beta",
1410+
index: 1,
1411+
mapping: "standard",
1412+
timestamp: 12
1413+
}
1414+
];
1415+
Object.defineProperty(navigator, "getGamepads", {
1416+
configurable: true,
1417+
value: () => window.__inputMappingV2MockGamepads
1418+
});
1419+
});
1420+
const server = await openInputMappingV2(page);
1421+
const pageErrors = [];
1422+
1423+
page.on("pageerror", (error) => {
1424+
pageErrors.push(error.message);
1425+
});
1426+
1427+
try {
1428+
await expect(page.locator(".input-mapping-v2__gamepad-capture-button")).toHaveCount(2);
1429+
await expect(page.locator(".input-mapping-v2__gamepad-capture-button[data-input-mapping-gamepad-index='0']")).toContainText("Load Pad Alpha (Gamepad 0)");
1430+
await expect(page.locator(".input-mapping-v2__gamepad-capture-button[data-input-mapping-gamepad-index='1']")).toContainText("Load Pad Beta (Gamepad 1)");
1431+
await expect(page.locator("#inputMappingV2SourceList")).toContainText("2 connected gamepads");
1432+
await expect(page.locator("#statusLog")).toHaveValue(/OK 2 connected gamepads detected: Load Pad Alpha \(Gamepad 0\), Load Pad Beta \(Gamepad 1\)/);
1433+
await expect(page.locator("#inputMappingV2StartGamepadPollingButton")).toHaveCount(0);
1434+
await expect(page.locator("button", { hasText: /Start Listening|Poll Gamepads/ })).toHaveCount(0);
1435+
const captureBottomElementId = await page.locator("#captureInputContent").evaluate((content) => content.lastElementChild?.id);
1436+
expect(captureBottomElementId).toBe("inputMappingV2RefreshGamepadsButton");
1437+
expect(pageErrors).toEqual([]);
1438+
} finally {
1439+
await workspaceV2CoverageReporter.stop(page);
1440+
await server.close();
1441+
}
1442+
});
1443+
13931444
test("launches Input Mapping V2 and captures keyboard mappings", async ({ page }) => {
13941445
await page.setViewportSize({ width: 1920, height: 900 });
13951446
const server = await openInputMappingV2(page);
@@ -1433,15 +1484,15 @@ test.describe("Workspace Manager V2 bootstrap", () => {
14331484
expect(actionOptions).toEqual([...actionOptions].sort((left, right) => left.localeCompare(right)));
14341485
expect(actionOptions).toEqual(expect.arrayContaining(["Move Left", "Confirm", "Cancel", "Fire", "Thrust", "Rotate Left", "Rotate Right", "Pause", "Select", "Start"]));
14351486
await expect(page.locator("#inputMappingV2ResetActionsButton")).toHaveCount(0);
1487+
await expect(page.locator("#inputMappingV2ClearActionButton")).toHaveText("Clear Actions");
14361488
await expect(page.locator("#previewOutput")).toContainText("No inputs captured yet.");
14371489
await expect(page.locator(".input-mapping-v2__mapping-card")).toHaveCount(0);
14381490
expect(await page.locator("#previewOutput").evaluate((node) => getComputedStyle(node).overflowY)).toBe("auto");
14391491
const captureButtonLayout = await page.locator("#captureInputContent").evaluate((content) => {
14401492
const buttons = [
14411493
content.querySelector("#inputMappingV2CaptureKeyboardButton"),
14421494
content.querySelector("#inputMappingV2CaptureMouseButton"),
1443-
content.querySelector("#inputMappingV2RefreshGamepadsButton"),
1444-
content.querySelector("#inputMappingV2StartGamepadPollingButton")
1495+
content.querySelector("#inputMappingV2RefreshGamepadsButton")
14451496
];
14461497
return buttons.map((button) => {
14471498
const box = button.getBoundingClientRect();
@@ -1455,9 +1506,11 @@ test.describe("Workspace Manager V2 bootstrap", () => {
14551506
expect(captureButtonLayout.every((entry) => entry.width > 250)).toBe(true);
14561507
expect(new Set(captureButtonLayout.map((entry) => entry.left)).size).toBe(1);
14571508
expect(captureButtonLayout[1].top).toBeGreaterThan(captureButtonLayout[0].top);
1509+
const captureBottomElementId = await page.locator("#captureInputContent").evaluate((content) => content.lastElementChild?.id);
14581510
expect(captureButtonLayout[2].top).toBeGreaterThan(captureButtonLayout[1].top);
1459-
expect(captureButtonLayout[3].top).toBeGreaterThan(captureButtonLayout[2].top);
1460-
await expect(page.locator("#inputMappingV2StartGamepadPollingButton")).toBeVisible();
1511+
expect(captureBottomElementId).toBe("inputMappingV2RefreshGamepadsButton");
1512+
await expect(page.locator("#inputMappingV2StartGamepadPollingButton")).toHaveCount(0);
1513+
await expect(page.locator("button", { hasText: /Start Listening|Poll Gamepads/ })).toHaveCount(0);
14611514
await expect(page.locator("#inputMappingV2GamepadDiagnostics")).toContainText("Raw navigator.getGamepads()");
14621515
await expect(page.locator("#inputMappingV2GamepadDiagnostics")).toContainText("InputService gamepad state");
14631516
await expect(page.locator("#inputMappingV2GamepadDiagnostics")).toContainText("Sample 0104 engine/input path");
@@ -1470,6 +1523,12 @@ test.describe("Workspace Manager V2 bootstrap", () => {
14701523
await page.locator("#inputMappingV2AddActionButton").click();
14711524
await expect(page.locator(".input-mapping-v2__mapping-card")).toHaveCount(1);
14721525
await expect(page.locator(".input-mapping-v2__tile-action-label")).toHaveText("Move Left");
1526+
await expect(page.locator(".input-mapping-v2__mapping-card.is-selected")).toHaveCount(1);
1527+
await expect(page.locator(".input-mapping-v2__mapping-card.is-selected")).toContainText("Move Left");
1528+
await expect(page.locator("#inputMappingV2ActionSelect option[value='moveLeft']")).toHaveJSProperty("disabled", true);
1529+
await page.locator("#inputMappingV2AddActionButton").click();
1530+
await expect(page.locator(".input-mapping-v2__mapping-card")).toHaveCount(1);
1531+
await expect(page.locator("#statusLog")).toHaveValue(/WARN Move Left already has an action tile\. Delete it before creating another\./);
14731532
await expect(page.locator(".input-mapping-v2__tile-action-select")).toHaveCount(0);
14741533
await expect(page.locator(".input-mapping-v2__empty-token", { hasText: "No inputs captured." })).toHaveCount(1);
14751534
const emptyMappingTileBox = await page.locator(".input-mapping-v2__mapping-card").first().boundingBox();
@@ -1493,7 +1552,26 @@ test.describe("Workspace Manager V2 bootstrap", () => {
14931552
await page.locator(".input-mapping-v2__input-token", { hasText: "Keyboard KeyA" }).click();
14941553
await expect(page.locator(".input-mapping-v2__input-token", { hasText: "Keyboard KeyA" })).toHaveCount(0);
14951554
await expect(page.locator(".input-mapping-v2__input-token", { hasText: "Keyboard KeyD" })).toHaveCount(1);
1496-
const actionValues = await page.locator("#inputMappingV2ActionSelect option").evaluateAll((options) => options.map((option) => option.value));
1555+
await page.locator("#inputMappingV2ActionSelect").selectOption("confirm");
1556+
await page.locator("#inputMappingV2AddActionButton").click();
1557+
await expect(page.locator(".input-mapping-v2__mapping-card")).toHaveCount(2);
1558+
await expect(page.locator("#inputMappingV2ActionSelect option[value='confirm']")).toHaveJSProperty("disabled", true);
1559+
await page.locator(".input-mapping-v2__mapping-card[data-input-mapping-tile-action-id='moveLeft']").click();
1560+
await expect(page.locator("#inputMappingV2ActionSelect")).toHaveValue("moveLeft");
1561+
await expect(page.locator(".input-mapping-v2__mapping-card.is-selected")).toContainText("Move Left");
1562+
await page.locator(".input-mapping-v2__mapping-card[data-input-mapping-tile-action-id='confirm']").click();
1563+
await expect(page.locator("#inputMappingV2ActionSelect")).toHaveValue("confirm");
1564+
await expect(page.locator(".input-mapping-v2__mapping-card.is-selected")).toContainText("Confirm");
1565+
await page.locator("#inputMappingV2CaptureKeyboardButton").click();
1566+
await page.keyboard.press("KeyB");
1567+
await expect(page.locator("#statusLog")).toHaveValue(/OK Keyboard KeyB mapped to Confirm\./);
1568+
await expect(page.locator("#captureInputContent")).not.toContainText("Keyboard KeyB mapped to Confirm.");
1569+
await page.locator("#inputMappingV2DeleteActionButton").click();
1570+
await expect(page.locator(".input-mapping-v2__mapping-card", { hasText: "Confirm" })).toHaveCount(0);
1571+
await expect(page.locator("#inputMappingV2ActionSelect option[value='confirm']")).toHaveJSProperty("disabled", false);
1572+
const actionValues = await page.locator("#inputMappingV2ActionSelect option").evaluateAll((options) => (
1573+
options.filter((option) => !option.disabled).map((option) => option.value)
1574+
));
14971575
for (const actionValue of actionValues.slice(0, 16)) {
14981576
await page.locator("#inputMappingV2ActionSelect").selectOption(actionValue);
14991577
await page.locator("#inputMappingV2AddActionButton").click();
@@ -1540,8 +1618,6 @@ test.describe("Workspace Manager V2 bootstrap", () => {
15401618
await expect(page.locator("#inputMappingV2GamepadDiagnostics")).toContainText("button count");
15411619
await expect(page.locator("#inputMappingV2GamepadDiagnostics")).toContainText("axis count");
15421620
await expect(page.locator("#inputMappingV2GamepadDiagnostics")).toContainText(/navigator\.getGamepads\(\) count2/);
1543-
await page.locator("#inputMappingV2StartGamepadPollingButton").click();
1544-
await expect(page.locator("#statusLog")).toHaveValue(/OK Gamepad listening\/polling is active\./);
15451621
await expect(page.locator("#inputMappingV2SourceList")).toContainText("2 connected gamepads");
15461622
await expect(page.locator("#inputMappingV2SourceList")).toContainText("Mock Flight Stick");
15471623
await expect(page.locator("#inputMappingV2SourceList")).toContainText("Arcade Twin Stick");
@@ -1568,7 +1644,8 @@ test.describe("Workspace Manager V2 bootstrap", () => {
15681644
timestamp: 44
15691645
};
15701646
});
1571-
await page.locator("#inputMappingV2ActionSelect").selectOption("moveLeft");
1647+
await page.locator(".input-mapping-v2__mapping-card[data-input-mapping-tile-action-id='moveLeft']").click();
1648+
await expect(page.locator("#inputMappingV2ActionSelect")).toHaveValue("moveLeft");
15721649
await page.locator(".input-mapping-v2__gamepad-capture-button[data-input-mapping-gamepad-index='1']").click();
15731650
await expect(page.locator("#previewOutput")).toContainText("Keyboard KeyD");
15741651
await expect(page.locator("#previewOutput")).toContainText("Arcade Twin Stick (Gamepad 1) Button 2");

tools/input-mapping-v2/index.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ <h2 class="tools-platform-frame__eyebrow">Input Sources and Action Mapping</h2>
6868
</label>
6969
<div class="input-mapping-v2__button-row">
7070
<button id="inputMappingV2AddActionButton" type="button">Add Action</button>
71-
<button id="inputMappingV2ClearActionButton" type="button">Clear Action Inputs</button>
71+
<button id="inputMappingV2DeleteActionButton" type="button">Delete Action</button>
72+
<button id="inputMappingV2ClearActionButton" type="button">Clear Actions</button>
7273
</div>
7374
<p id="inputMappingV2ActionHint" class="tool-starter__hint">Default actions are engine-style action names sorted alphabetically.</p>
7475
</div>
@@ -85,10 +86,9 @@ <h2 class="tools-platform-frame__eyebrow">Input Sources and Action Mapping</h2>
8586
<button id="inputMappingV2CaptureKeyboardButton" type="button">Capture Keyboard</button>
8687
<button id="inputMappingV2CaptureMouseButton" type="button">Capture Mouse</button>
8788
</div>
88-
<button id="inputMappingV2RefreshGamepadsButton" type="button">Refresh Gamepads</button>
89-
<button id="inputMappingV2StartGamepadPollingButton" type="button">Start Listening / Poll Gamepads</button>
9089
<div id="inputMappingV2GamepadCaptureButtons" class="input-mapping-v2__gamepad-buttons" aria-label="Detected gamepad capture buttons"></div>
9190
<p id="inputMappingV2CaptureMessage" class="tool-starter__hint">Keyboard, mouse, and gamepad sources are provided by src/engine/input.</p>
91+
<button id="inputMappingV2RefreshGamepadsButton" type="button">Refresh Gamepads</button>
9292
</div>
9393
</section>
9494
</aside>

0 commit comments

Comments
 (0)