Skip to content

Commit 20ff455

Browse files
committed
Polish Input Mapping V2 capture device labels - PR_26140_094-polish-input-mapping-v2-capture-device-labels
1 parent 2dfdf00 commit 20ff455

9 files changed

Lines changed: 383 additions & 53 deletions

File tree

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# PR_26140_094 Input Mapping V2 Capture Device Labels Report
2+
3+
## Scope
4+
- Updated Input Mapping V2 Capture UI, device labels, capture highlighting, and mapping tile rendering.
5+
- Added focused Workspace V2 Playwright assertions for the requested capture-device label behavior.
6+
- Active `docs/pr/BUILD_PR.md` was for an unrelated Level 18 rebase, so the inline `PR_26140_094-polish-input-mapping-v2-capture-device-labels` BUILD request was used as the source of truth.
7+
- No schemas were changed.
8+
- No sample JSON was touched.
9+
10+
## Failing Tool Before
11+
- Input Mapping V2 capture buttons exposed raw gamepad IDs in one line.
12+
- Captured gamepad mapping tiles included `(Gamepad N)` clutter in visible tile text.
13+
- Capture buttons did not visibly stay active while waiting for keyboard, mouse, or gamepad input.
14+
- Mapping tiles were still 175px wide.
15+
16+
## Tool Fixed
17+
- Input Mapping V2.
18+
19+
## Changes
20+
- Gamepad capture buttons now use multiline labels:
21+
- generic USB gamepad: `Capture (Gamepad N)`, vendor/product, display name;
22+
- named USB gamepad: `Capture <device name>`, vendor/product, display name.
23+
- Vendor/product parsing normalizes browser gamepad IDs and removes dangling punctuation from the visible line.
24+
- Keyboard, mouse, and gamepad capture buttons get a consistent `is-capturing` highlight while waiting.
25+
- Capture highlight clears after capture completion, timeout, failed device capture, or action-selection cancellation.
26+
- Gamepad capture is now armed and completed by auto polling when a matching device input becomes active.
27+
- Captured mapping tiles are 225x175, increasing width by 50px while preserving height.
28+
- Tile tokens render concise source/input text, such as `Logitech RumblePad 2` and `Button 2`, without visible `(Gamepad N)` clutter.
29+
- Token hover/title preserves detailed device information, including USB transport, mapping type, vendor/product, and input name.
30+
- Persisted payloads still emit only the existing schema fields: `source`, `binding`, `label`, and `engine`.
31+
- Preserved combo input behavior, selected tile indication, and automatic gamepad polling.
32+
33+
## Remaining Failures After
34+
- None observed in requested validation.
35+
36+
## Playwright
37+
Playwright impacted: Yes.
38+
39+
Validated behavior:
40+
- generic USB gamepad capture button multiline label;
41+
- named gamepad capture button multiline label;
42+
- capture button highlight while waiting;
43+
- capture button unhighlight after capture;
44+
- mapping tile width increased to 225px while height remains 175px;
45+
- tile text omits `(Gamepad 1)` USB ID clutter;
46+
- hover/title includes full device/vendor/product/input details;
47+
- keyboard and mouse capture still work;
48+
- existing auto gamepad polling and selected mapping indication remain covered.
49+
50+
Expected pass behavior:
51+
- Browser-exposed gamepads appear with readable multiline capture labels.
52+
- Capture buttons show a selected/waiting highlight until capture completes.
53+
- Captured gamepad tiles show concise device/input lines and detailed title text.
54+
- Tool JSON remains schema-compatible.
55+
56+
Expected fail behavior:
57+
- If raw USB IDs, dangling punctuation, missing capture highlight, tile-size regression, or schema-drifted display metadata returns, the focused Input Mapping V2 Playwright tests fail.
58+
59+
## Validation
60+
- PASS: targeted syntax validation for changed Input Mapping V2 JS files and the touched Workspace V2 Playwright spec.
61+
- PASS: focused Playwright Input Mapping V2 slice:
62+
`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`
63+
- PASS: `npm run test:workspace-v2` with 61 passed.
64+
- PASS: Input Mapping V2 HTML external JS/CSS contract scan with `rg --pcre2`; no inline scripts, inline styles, or inline event handlers found.
65+
- PASS: Playwright V8 coverage report generated by the Workspace V2 suite at `docs/dev/reports/playwright_v8_coverage_report.txt`.
66+
- Full samples smoke test skipped because this PR is limited to Input Mapping V2 capture UI and Workspace V2 coverage; sample JSON is out of scope.
67+
68+
## Manual Validation
69+
1. Launch Input Mapping V2.
70+
2. Connect or expose a generic USB gamepad and confirm its capture button shows `Capture (Gamepad N)`, a clean vendor/product line, and `USB gamepad`.
71+
3. Connect or expose a named USB gamepad and confirm its capture button shows `Capture <device name>`, a clean vendor/product line, and the device name.
72+
4. Click Capture Keyboard, Capture Mouse, and a gamepad capture button; confirm each highlights while waiting and clears after capture.
73+
5. Capture a gamepad button and confirm the mapping tile shows only the concise device name and button/axis, not `(Gamepad N)`.
74+
6. Hover the captured gamepad token and confirm the title includes device USB detail, mapping type, vendor/product, and input.
75+
7. Confirm combo captures still accumulate on the selected tile and selected tile indication still updates when clicked.

tests/playwright/tools/WorkspaceManagerV2.spec.mjs

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1397,7 +1397,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
13971397
axes: [0, 0],
13981398
buttons: [{ pressed: false }, { pressed: false }],
13991399
connected: true,
1400-
id: "Load Pad Alpha",
1400+
id: "USB gamepad (Vendor: 081f Product: e401)",
14011401
index: 0,
14021402
mapping: "standard",
14031403
timestamp: 11
@@ -1406,7 +1406,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
14061406
axes: [0, 0, 0, 0],
14071407
buttons: [{ pressed: false }, { pressed: false }, { pressed: false }],
14081408
connected: true,
1409-
id: "Load Pad Beta",
1409+
id: "Logitech RumblePad 2 USB (Vendor: 046d Product: c218)",
14101410
index: 1,
14111411
mapping: "standard",
14121412
timestamp: 12
@@ -1426,10 +1426,17 @@ test.describe("Workspace Manager V2 bootstrap", () => {
14261426

14271427
try {
14281428
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)");
1429+
const gamepadCaptureLabels = await page.locator(".input-mapping-v2__gamepad-capture-button").evaluateAll((buttons) => (
1430+
buttons.map((button) => button.textContent.split("\n").map((line) => line.trim()).filter(Boolean))
1431+
));
1432+
expect(gamepadCaptureLabels).toEqual([
1433+
["Capture (Gamepad 0)", "Vendor: 081f Product: e401", "USB gamepad"],
1434+
["Capture Logitech RumblePad 2", "Vendor: 046d Product: c218", "Logitech RumblePad 2"]
1435+
]);
1436+
await expect(page.locator(".input-mapping-v2__gamepad-capture-button[data-input-mapping-gamepad-index='0']")).not.toContainText("Product: e401)");
1437+
await expect(page.locator(".input-mapping-v2__gamepad-capture-button[data-input-mapping-gamepad-index='1']")).not.toContainText("Product: c218)");
14311438
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\)/);
1439+
await expect(page.locator("#statusLog")).toHaveValue(/OK 2 connected gamepads detected: USB gamepad \(Gamepad 0\), Logitech RumblePad 2 \(Gamepad 1\)/);
14331440
await expect(page.locator("#inputMappingV2StartGamepadPollingButton")).toHaveCount(0);
14341441
await expect(page.locator("button", { hasText: /Start Listening|Poll Gamepads/ })).toHaveCount(0);
14351442
const captureBottomElementId = await page.locator("#captureInputContent").evaluate((content) => content.lastElementChild?.id);
@@ -1532,23 +1539,37 @@ test.describe("Workspace Manager V2 bootstrap", () => {
15321539
await expect(page.locator(".input-mapping-v2__tile-action-select")).toHaveCount(0);
15331540
await expect(page.locator(".input-mapping-v2__empty-token", { hasText: "No inputs captured." })).toHaveCount(1);
15341541
const emptyMappingTileBox = await page.locator(".input-mapping-v2__mapping-card").first().boundingBox();
1535-
expect(Math.round(emptyMappingTileBox.width)).toBe(175);
1542+
expect(Math.round(emptyMappingTileBox.width)).toBe(225);
15361543
expect(Math.round(emptyMappingTileBox.height)).toBe(175);
15371544
await page.locator("#inputMappingV2CaptureKeyboardButton").click();
1545+
await expect(page.locator("#inputMappingV2CaptureKeyboardButton")).toHaveClass(/is-capturing/);
15381546
await expect(page.locator("#inputMappingV2CaptureMessage")).toContainText("Press a keyboard key");
15391547
await page.keyboard.press("KeyA");
1548+
await expect(page.locator("#inputMappingV2CaptureKeyboardButton")).not.toHaveClass(/is-capturing/);
15401549
await page.locator("#inputMappingV2CaptureKeyboardButton").click();
15411550
await page.keyboard.press("KeyD");
1551+
await page.locator("#inputMappingV2CaptureMouseButton").click();
1552+
await expect(page.locator("#inputMappingV2CaptureMouseButton")).toHaveClass(/is-capturing/);
1553+
await page.evaluate(() => {
1554+
window.dispatchEvent(new MouseEvent("mousedown", {
1555+
bubbles: true,
1556+
button: 1,
1557+
cancelable: true
1558+
}));
1559+
});
1560+
await expect(page.locator("#inputMappingV2CaptureMouseButton")).not.toHaveClass(/is-capturing/);
15421561
await expect(page.locator(".input-mapping-v2__mapping-card")).toHaveCount(1);
15431562
const mappingTileBox = await page.locator(".input-mapping-v2__mapping-card").first().boundingBox();
1544-
expect(Math.round(mappingTileBox.width)).toBe(175);
1563+
expect(Math.round(mappingTileBox.width)).toBe(225);
15451564
expect(Math.round(mappingTileBox.height)).toBe(175);
15461565
await expect(page.locator("#previewOutput")).toContainText("Move Left");
15471566
await expect(page.locator("#previewOutput")).toContainText("Keyboard KeyA");
15481567
await expect(page.locator("#previewOutput")).toContainText("Keyboard KeyD");
1568+
await expect(page.locator("#previewOutput")).toContainText("Mouse Button 1");
15491569
await expect(page.locator("#inspectorOutput")).toContainText('"action": "moveLeft"');
15501570
await expect(page.locator("#inspectorOutput")).toContainText('"binding": "KeyA"');
15511571
await expect(page.locator("#inspectorOutput")).toContainText('"binding": "KeyD"');
1572+
await expect(page.locator("#inspectorOutput")).toContainText('"binding": "MouseButton1"');
15521573
await page.locator(".input-mapping-v2__input-token", { hasText: "Keyboard KeyA" }).click();
15531574
await expect(page.locator(".input-mapping-v2__input-token", { hasText: "Keyboard KeyA" })).toHaveCount(0);
15541575
await expect(page.locator(".input-mapping-v2__input-token", { hasText: "Keyboard KeyD" })).toHaveCount(1);
@@ -1598,7 +1619,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
15981619
axes: [0, 0, 0, 0],
15991620
buttons: [{ pressed: false }, { pressed: false }, { pressed: false }],
16001621
connected: true,
1601-
id: "Arcade Twin Stick",
1622+
id: "Logitech RumblePad 2 USB (Vendor: 046d Product: c218)",
16021623
index: 1,
16031624
mapping: "standard",
16041625
timestamp: 43
@@ -1611,19 +1632,19 @@ test.describe("Workspace Manager V2 bootstrap", () => {
16111632
});
16121633
await expect(page.locator(".input-mapping-v2__gamepad-capture-button")).toHaveCount(2);
16131634
await page.locator("#inputMappingV2RefreshGamepadsButton").click();
1614-
await expect(page.locator("#statusLog")).toHaveValue(/OK 2 connected gamepads detected: Mock Flight Stick \(Gamepad 0\), Arcade Twin Stick \(Gamepad 1\)/);
1635+
await expect(page.locator("#statusLog")).toHaveValue(/OK 2 connected gamepads detected: Mock Flight Stick \(Gamepad 0\), Logitech RumblePad 2 \(Gamepad 1\)/);
16151636
await expect(page.locator("#inputMappingV2GamepadDiagnostics")).toContainText("Mock Flight Stick");
1616-
await expect(page.locator("#inputMappingV2GamepadDiagnostics")).toContainText("Arcade Twin Stick");
1637+
await expect(page.locator("#inputMappingV2GamepadDiagnostics")).toContainText("Logitech RumblePad 2");
16171638
await expect(page.locator("#inputMappingV2GamepadDiagnostics")).toContainText("index");
16181639
await expect(page.locator("#inputMappingV2GamepadDiagnostics")).toContainText("button count");
16191640
await expect(page.locator("#inputMappingV2GamepadDiagnostics")).toContainText("axis count");
16201641
await expect(page.locator("#inputMappingV2GamepadDiagnostics")).toContainText(/navigator\.getGamepads\(\) count2/);
16211642
await expect(page.locator("#inputMappingV2SourceList")).toContainText("2 connected gamepads");
16221643
await expect(page.locator("#inputMappingV2SourceList")).toContainText("Mock Flight Stick");
1623-
await expect(page.locator("#inputMappingV2SourceList")).toContainText("Arcade Twin Stick");
1644+
await expect(page.locator("#inputMappingV2SourceList")).toContainText("Logitech RumblePad 2");
16241645
await expect(page.locator(".input-mapping-v2__gamepad-capture-button")).toHaveCount(2);
16251646
await expect(page.locator(".input-mapping-v2__gamepad-capture-button").nth(0)).toContainText("Mock Flight Stick");
1626-
await expect(page.locator(".input-mapping-v2__gamepad-capture-button").nth(1)).toContainText("Arcade Twin Stick");
1647+
await expect(page.locator(".input-mapping-v2__gamepad-capture-button").nth(1)).toContainText("Logitech RumblePad 2");
16271648
const gamepadButtonLayout = await page.locator(".input-mapping-v2__gamepad-capture-button").evaluateAll((buttons) => buttons.map((button) => {
16281649
const box = button.getBoundingClientRect();
16291650
return {
@@ -1635,25 +1656,27 @@ test.describe("Workspace Manager V2 bootstrap", () => {
16351656
expect(gamepadButtonLayout.every((entry) => entry.width > 250)).toBe(true);
16361657
expect(new Set(gamepadButtonLayout.map((entry) => entry.left)).size).toBe(1);
16371658
expect(gamepadButtonLayout[1].top).toBeGreaterThan(gamepadButtonLayout[0].top);
1659+
await page.locator(".input-mapping-v2__mapping-card[data-input-mapping-tile-action-id='moveLeft']").click();
1660+
await expect(page.locator("#inputMappingV2ActionSelect")).toHaveValue("moveLeft");
16381661
await page.locator(".input-mapping-v2__gamepad-capture-button[data-input-mapping-gamepad-index='1']").click();
1639-
await expect(page.locator("#statusLog")).toHaveValue(/WARN Gamepad capture unavailable: Arcade Twin Stick \(Gamepad 1\) is detected, but no live button or axis value is active/);
1662+
await expect(page.locator(".input-mapping-v2__gamepad-capture-button[data-input-mapping-gamepad-index='1']")).toHaveClass(/is-capturing/);
16401663
await page.evaluate(() => {
16411664
window.__inputMappingV2MockGamepads[1] = {
16421665
...window.__inputMappingV2MockGamepads[1],
16431666
buttons: [{ pressed: false }, { pressed: false }, { pressed: true }],
16441667
timestamp: 44
16451668
};
16461669
});
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");
1649-
await page.locator(".input-mapping-v2__gamepad-capture-button[data-input-mapping-gamepad-index='1']").click();
1670+
await expect(page.locator(".input-mapping-v2__gamepad-capture-button[data-input-mapping-gamepad-index='1']")).not.toHaveClass(/is-capturing/);
16501671
await expect(page.locator("#previewOutput")).toContainText("Keyboard KeyD");
1651-
await expect(page.locator("#previewOutput")).toContainText("Arcade Twin Stick (Gamepad 1) Button 2");
1672+
await expect(page.locator("#previewOutput")).toContainText("Logitech RumblePad 2 Button 2");
1673+
await expect(page.locator("#previewOutput")).not.toContainText("(Gamepad 1)");
1674+
await expect(page.locator(".input-mapping-v2__input-token", { hasText: "Logitech RumblePad 2" })).toHaveAttribute("title", "Logitech RumblePad 2 USB\nSTANDARD GAMEPAD\nVendor: 046d Product: c218\nButton 2");
16521675
await expect(page.locator("#inspectorOutput")).toContainText('"source": "gamepad"');
16531676
await expect(page.locator("#inspectorOutput")).toContainText('"binding": "Pad1:Button2"');
1654-
await expect(page.locator("#inspectorOutput")).toContainText('"label": "Arcade Twin Stick (Gamepad 1) Button 2"');
1655-
await page.locator(".input-mapping-v2__input-token", { hasText: "Arcade Twin Stick (Gamepad 1) Button 2" }).click();
1656-
await expect(page.locator(".input-mapping-v2__input-token", { hasText: "Arcade Twin Stick (Gamepad 1) Button 2" })).toHaveCount(0);
1677+
await expect(page.locator("#inspectorOutput")).toContainText('"label": "Logitech RumblePad 2 Button 2"');
1678+
await page.locator(".input-mapping-v2__input-token", { hasText: "Logitech RumblePad 2" }).click();
1679+
await expect(page.locator(".input-mapping-v2__input-token", { hasText: "Logitech RumblePad 2" })).toHaveCount(0);
16571680
await expect(page.locator(".input-mapping-v2__input-token", { hasText: "Keyboard KeyD" })).toHaveCount(1);
16581681
const asteroidsManifestInputMapping = await page.evaluate(async () => {
16591682
const response = await fetch("/games/Asteroids/game.manifest.json");

tools/input-mapping-v2/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,8 @@ <h2 class="tools-platform-frame__eyebrow">Input Sources and Action Mapping</h2>
8383
<div id="captureInputContent" class="accordion-v2__content">
8484
<p id="inputMappingV2SelectedActionLabel" class="tool-starter__hint">Selected action: Move Up</p>
8585
<div class="input-mapping-v2__button-row">
86-
<button id="inputMappingV2CaptureKeyboardButton" type="button">Capture Keyboard</button>
87-
<button id="inputMappingV2CaptureMouseButton" type="button">Capture Mouse</button>
86+
<button id="inputMappingV2CaptureKeyboardButton" class="input-mapping-v2__capture-button" type="button">Capture Keyboard</button>
87+
<button id="inputMappingV2CaptureMouseButton" class="input-mapping-v2__capture-button" type="button">Capture Mouse</button>
8888
</div>
8989
<div id="inputMappingV2GamepadCaptureButtons" class="input-mapping-v2__gamepad-buttons" aria-label="Detected gamepad capture buttons"></div>
9090
<p id="inputMappingV2CaptureMessage" class="tool-starter__hint">Keyboard, mouse, and gamepad sources are provided by src/engine/input.</p>

0 commit comments

Comments
 (0)