Skip to content

Commit b85ffce

Browse files
committed
Polish Input Mapping V2 layout sizing and mapping display - PR_26140_107-polish-input-mapping-v2-layout-sizing-and-mapping-display
1 parent 2209b1c commit b85ffce

7 files changed

Lines changed: 385 additions & 29 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Input Mapping V2 Layout Sizing and Mapping Display Report
2+
3+
Task: PR_26140_107-polish-input-mapping-v2-layout-sizing-and-mapping-display
4+
5+
## Scope
6+
- Read docs/dev/PROJECT_INSTRUCTIONS.md before making changes.
7+
- Treated the explicit PR_26140_107 request as the active BUILD scope; docs/pr/BUILD_PR.md currently describes an unrelated Level 18 rebase workflow.
8+
- Did not change schemas.
9+
- Did not touch sample JSON.
10+
- Did not run the full samples smoke test.
11+
12+
## Implementation
13+
- Updated Input Mapping V2 column sizing so the Actions accordion sizes to its content, Devices owns the remaining left-column space, and Captured Mappings reserves/fills the center-column workspace area.
14+
- Extended live-used highlighting beyond keyboard:
15+
- Mouse button down/up now activates and clears saved mouse mapping tokens.
16+
- Mouse wheel input now briefly activates saved wheel mapping tokens.
17+
- Game controller button/axis state is synced from the engine gamepad state during polling and refresh.
18+
- Flight Stick remains represented through the browser Gamepad API path; Touch, Pen, and VR Controller show safe capability/no-device states where direct capture is not testable in this tool.
19+
- Kept active capture highlighting separate from saved/live-used mapping highlighting.
20+
- Changed Captured Mappings tile token text to use available width with single-line entries and comma separators between multiple captured mappings.
21+
- Preserved detailed hover/title metadata for captured inputs.
22+
- Updated focused Playwright coverage for column sizing, safe empty states, single-line comma-separated tile display, and live-used highlighting for keyboard, mouse, wheel, and mocked game controller input.
23+
24+
## Validation
25+
- PASS: `node --check tools/input-mapping-v2/js/ToolStarterApp.js`
26+
- PASS: `node --check tools/input-mapping-v2/js/controls/PreviewPanelControl.js`
27+
- PASS: `node --check tools/input-mapping-v2/js/controls/DeviceListControl.js`
28+
- PASS: `node --check tools/input-mapping-v2/js/services/EngineInputSourceService.js`
29+
- PASS: `node --check tests/playwright/tools/WorkspaceManagerV2.spec.mjs`
30+
- PASS: `npx playwright test tests/playwright/tools/WorkspaceManagerV2.spec.mjs -g "Input Mapping V2"`
31+
- PASS: `npm run test:workspace-v2` (64 passed)
32+
- PASS: `git diff --check` (line-ending warnings only)
33+
34+
## Notes
35+
- Existing compact-spacing assertions were updated to keep checking bottom whitespace directly while allowing setup accordions to scroll when Captured Mappings reserves the center workspace height.
36+
- Sample JSON files were not modified.

tests/playwright/tools/WorkspaceManagerV2.spec.mjs

Lines changed: 193 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1486,13 +1486,6 @@ test.describe("Workspace Manager V2 bootstrap", () => {
14861486
expect.objectContaining({ id: "captureInputContent", paddingBottom: "2px" })
14871487
]));
14881488
expect(compactBottomWhitespace.every((entry) => entry.bottomGap <= 4)).toBe(true);
1489-
const setupContentFit = await page.locator("#gestureSetupContent, #captureInputContent").evaluateAll((contents) => (
1490-
contents.map((content) => ({
1491-
id: content.id,
1492-
overflowDelta: content.scrollHeight - content.clientHeight
1493-
}))
1494-
));
1495-
expect(setupContentFit.every((entry) => entry.overflowDelta <= 2)).toBe(true);
14961489
const gestureDeviceBottomGaps = await page.locator("#inputMappingV2GestureList .input-mapping-v2__gesture-group").evaluateAll((groups) => (
14971490
groups.map((group) => {
14981491
const groupBox = group.getBoundingClientRect();
@@ -1517,7 +1510,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
15171510
await page.keyboard.press("KeyP");
15181511
await expect(page.locator(".input-mapping-v2__mapping-card[data-input-mapping-tile-action-id='cancel']")).toContainText("Keyboard KeyP Press");
15191512
const cancelTileKeyPToken = page.locator(".input-mapping-v2__mapping-card[data-input-mapping-tile-action-id='cancel'] .input-mapping-v2__input-token[data-input-mapping-binding='KeyP']");
1520-
await expect(cancelTileKeyPToken).toHaveText("Keyboard\nKeyP\nPress");
1513+
await expect(cancelTileKeyPToken).toHaveText("Keyboard KeyP Press");
15211514
await expect(cancelTileKeyPToken).toHaveClass(/is-selected-mapping-input/);
15221515
await expect(cancelTileKeyPToken).toHaveAttribute("aria-current", "true");
15231516
await expect(cancelTileKeyPToken).toHaveCSS("box-shadow", /rgba/);
@@ -1702,6 +1695,198 @@ test.describe("Workspace Manager V2 bootstrap", () => {
17021695
}
17031696
});
17041697

1698+
test("sizes Input Mapping V2 columns and live-highlights mapped non-keyboard inputs", async ({ page }) => {
1699+
await page.setViewportSize({ width: 1600, height: 900 });
1700+
await page.addInitScript(() => {
1701+
window.__inputMappingV2MockGamepads = [
1702+
{
1703+
axes: [0, 0, 0, 0],
1704+
buttons: [{ pressed: false }, { pressed: false }],
1705+
connected: true,
1706+
id: "Arcade Flight Stick (Vendor: 1234 Product: abcd)",
1707+
index: 0,
1708+
mapping: "",
1709+
timestamp: 20
1710+
}
1711+
];
1712+
Object.defineProperty(navigator, "getGamepads", {
1713+
configurable: true,
1714+
value: () => window.__inputMappingV2MockGamepads
1715+
});
1716+
});
1717+
const pageErrors = [];
1718+
page.on("pageerror", (error) => {
1719+
pageErrors.push(error.message);
1720+
});
1721+
const server = await openInputMappingV2(page);
1722+
try {
1723+
await expect(page.locator("body[data-tool-id='input-mapping-v2']")).toBeVisible();
1724+
await expect(page.locator(".input-mapping-v2__gamepad-capture-button[data-input-mapping-gamepad-index='0']")).toBeVisible();
1725+
1726+
const columnSizing = await page.locator(".input-mapping-v2.tool-starter.app-shell").evaluate((shell) => {
1727+
const leftPanel = shell.querySelector(".tool-starter__panel--left");
1728+
const centerPanel = shell.querySelector(".tool-starter__panel--center");
1729+
const leftAccordions = Array.from(leftPanel.querySelectorAll(":scope > .tool-starter__accordion"));
1730+
const centerAccordions = Array.from(centerPanel.querySelectorAll(":scope > .tool-starter__accordion"));
1731+
const actions = leftAccordions[0];
1732+
const devices = leftAccordions[1];
1733+
const actionHeader = actions.querySelector(".accordion-v2__header");
1734+
const actionContent = actions.querySelector("#actionSetupContent");
1735+
const gestures = centerAccordions[0];
1736+
const capture = centerAccordions[1];
1737+
const captured = centerAccordions[2];
1738+
const leftBox = leftPanel.getBoundingClientRect();
1739+
const centerBox = centerPanel.getBoundingClientRect();
1740+
const leftInnerBottom = leftBox.bottom - Number.parseFloat(getComputedStyle(leftPanel).paddingBottom || "0");
1741+
const centerInnerBottom = centerBox.bottom - Number.parseFloat(getComputedStyle(centerPanel).paddingBottom || "0");
1742+
return {
1743+
actionContentOverflow: Math.round(actionContent.scrollHeight - actionContent.clientHeight),
1744+
actionExpectedMax: Math.ceil(actionHeader.getBoundingClientRect().height + actionContent.scrollHeight + 8),
1745+
actionHeight: Math.round(actions.getBoundingClientRect().height),
1746+
capturedBottomDelta: Math.round(centerInnerBottom - captured.getBoundingClientRect().bottom),
1747+
capturedHeight: Math.round(captured.getBoundingClientRect().height),
1748+
devicesBottomDelta: Math.round(leftInnerBottom - devices.getBoundingClientRect().bottom),
1749+
devicesHeight: Math.round(devices.getBoundingClientRect().height),
1750+
setupHeight: Math.round(gestures.getBoundingClientRect().height + capture.getBoundingClientRect().height)
1751+
};
1752+
});
1753+
expect(columnSizing.actionHeight).toBeLessThanOrEqual(columnSizing.actionExpectedMax);
1754+
expect(columnSizing.actionContentOverflow).toBeLessThanOrEqual(1);
1755+
expect(columnSizing.devicesHeight).toBeGreaterThan(columnSizing.actionHeight);
1756+
expect(Math.abs(columnSizing.devicesBottomDelta)).toBeLessThanOrEqual(2);
1757+
expect(columnSizing.capturedHeight).toBeGreaterThanOrEqual(300);
1758+
expect(columnSizing.capturedHeight).toBeGreaterThan(columnSizing.setupHeight / 2);
1759+
expect(Math.abs(columnSizing.capturedBottomDelta)).toBeLessThanOrEqual(2);
1760+
1761+
await expect(page.locator(".input-mapping-v2__device-card[data-input-mapping-device-id='touch']")).toContainText(/Touch capability|Touch capture/);
1762+
await expect(page.locator(".input-mapping-v2__device-card[data-input-mapping-device-id='pen']")).toContainText(/Pen capability|Pen capture/);
1763+
await expect(page.locator(".input-mapping-v2__device-card[data-input-mapping-device-id='flightStick']")).toContainText(/game controller capture|flight stick/i);
1764+
await expect(page.locator(".input-mapping-v2__device-card[data-input-mapping-device-id='vrController']")).toContainText(/WebXR|VR controller capture|not available/i);
1765+
1766+
await page.locator("#inputMappingV2ActionSelect").selectOption("cancel");
1767+
await page.locator("#inputMappingV2AddActionButton").click();
1768+
await page.locator(".input-mapping-v2__gesture-group", { hasText: "Keyboard" })
1769+
.locator(".input-mapping-v2__gesture-button[data-input-mapping-gesture-binding='KeyboardPress']")
1770+
.click();
1771+
await page.locator("#inputMappingV2CaptureKeyboardButton").click();
1772+
await page.keyboard.press("KeyP");
1773+
1774+
await page.locator(".input-mapping-v2__gesture-group", { hasText: "Mouse" })
1775+
.locator(".input-mapping-v2__gesture-button[data-input-mapping-gesture-binding='MouseClick']")
1776+
.click();
1777+
await page.locator("#inputMappingV2CaptureMouseButton").click();
1778+
await page.evaluate(() => {
1779+
window.dispatchEvent(new MouseEvent("mousedown", {
1780+
bubbles: true,
1781+
button: 2,
1782+
cancelable: true
1783+
}));
1784+
});
1785+
1786+
await page.locator(".input-mapping-v2__gesture-group", { hasText: "Mouse" })
1787+
.locator(".input-mapping-v2__gesture-button[data-input-mapping-gesture-binding='MouseWheelUp']")
1788+
.click();
1789+
1790+
await page.locator(".input-mapping-v2__gesture-group", { hasText: "Game Controller" })
1791+
.locator(".input-mapping-v2__gesture-button[data-input-mapping-gesture-binding='GameControllerButton']")
1792+
.click();
1793+
await page.locator(".input-mapping-v2__gamepad-capture-button[data-input-mapping-gamepad-index='0']").click();
1794+
await page.evaluate(() => {
1795+
window.__inputMappingV2MockGamepads[0] = {
1796+
...window.__inputMappingV2MockGamepads[0],
1797+
buttons: [{ pressed: false }, { pressed: true }],
1798+
timestamp: 21
1799+
};
1800+
});
1801+
await expect(page.locator(".input-mapping-v2__gamepad-capture-button[data-input-mapping-gamepad-index='0']")).not.toHaveClass(/is-capturing/, { timeout: 2500 });
1802+
await page.evaluate(() => {
1803+
window.__inputMappingV2MockGamepads[0] = {
1804+
...window.__inputMappingV2MockGamepads[0],
1805+
buttons: [{ pressed: false }, { pressed: false }],
1806+
timestamp: 22
1807+
};
1808+
});
1809+
1810+
const cancelTile = page.locator(".input-mapping-v2__mapping-card[data-input-mapping-tile-action-id='cancel']");
1811+
await expect(cancelTile).toContainText("Keyboard KeyP Press, Mouse Right Button Click, Mouse Wheel Up Wheel, Game Controller Button 1 Button");
1812+
await expect(cancelTile.locator(".input-mapping-v2__input-separator")).toHaveCount(3);
1813+
const tokenLayout = await cancelTile.locator(".input-mapping-v2__input-token").evaluateAll((tokens) => tokens.map((token) => ({
1814+
cardWidth: Math.round(token.closest(".input-mapping-v2__mapping-card").getBoundingClientRect().width),
1815+
text: token.textContent,
1816+
title: token.title,
1817+
whiteSpace: getComputedStyle(token).whiteSpace,
1818+
width: Math.round(token.getBoundingClientRect().width)
1819+
})));
1820+
expect(tokenLayout.map((entry) => entry.text)).toEqual([
1821+
"Keyboard KeyP Press",
1822+
"Mouse Right Button Click",
1823+
"Mouse Wheel Up Wheel",
1824+
"Game Controller Button 1 Button"
1825+
]);
1826+
expect(tokenLayout.every((entry) => !entry.text.includes("\n"))).toBe(true);
1827+
expect(tokenLayout.every((entry) => entry.whiteSpace === "nowrap")).toBe(true);
1828+
expect(tokenLayout.every((entry) => entry.width <= entry.cardWidth)).toBe(true);
1829+
expect(tokenLayout.some((entry) => entry.title.includes("\n"))).toBe(true);
1830+
1831+
const keyToken = cancelTile.locator(".input-mapping-v2__input-token[data-input-mapping-binding='KeyP']");
1832+
await page.keyboard.down("KeyP");
1833+
await expect(keyToken).toHaveClass(/is-action-active/);
1834+
await page.keyboard.up("KeyP");
1835+
await expect(keyToken).not.toHaveClass(/is-action-active/);
1836+
1837+
const mouseToken = cancelTile.locator(".input-mapping-v2__input-token[data-input-mapping-binding='MouseButton2']");
1838+
await page.evaluate(() => {
1839+
window.dispatchEvent(new MouseEvent("mousedown", {
1840+
bubbles: true,
1841+
button: 2,
1842+
cancelable: true
1843+
}));
1844+
});
1845+
await expect(mouseToken).toHaveClass(/is-action-active/);
1846+
await page.evaluate(() => {
1847+
window.dispatchEvent(new MouseEvent("mouseup", {
1848+
bubbles: true,
1849+
button: 2,
1850+
cancelable: true
1851+
}));
1852+
});
1853+
await expect(mouseToken).not.toHaveClass(/is-action-active/);
1854+
1855+
const wheelToken = cancelTile.locator(".input-mapping-v2__input-token[data-input-mapping-binding='MouseWheelUp']");
1856+
await page.evaluate(() => {
1857+
window.dispatchEvent(new WheelEvent("wheel", {
1858+
bubbles: true,
1859+
cancelable: true,
1860+
deltaY: -120
1861+
}));
1862+
});
1863+
await expect(wheelToken).toHaveClass(/is-action-active/, { timeout: 1000 });
1864+
1865+
const gamepadToken = cancelTile.locator(".input-mapping-v2__input-token[data-input-mapping-binding='Pad0:Button1']");
1866+
await expect(gamepadToken).not.toHaveClass(/is-action-active/, { timeout: 2500 });
1867+
await page.evaluate(() => {
1868+
window.__inputMappingV2MockGamepads[0] = {
1869+
...window.__inputMappingV2MockGamepads[0],
1870+
buttons: [{ pressed: false }, { pressed: true }],
1871+
timestamp: 23
1872+
};
1873+
});
1874+
await expect(gamepadToken).toHaveClass(/is-action-active/, { timeout: 2500 });
1875+
await page.evaluate(() => {
1876+
window.__inputMappingV2MockGamepads[0] = {
1877+
...window.__inputMappingV2MockGamepads[0],
1878+
buttons: [{ pressed: false }, { pressed: false }],
1879+
timestamp: 24
1880+
};
1881+
});
1882+
await expect(gamepadToken).not.toHaveClass(/is-action-active/, { timeout: 2500 });
1883+
expect(pageErrors).toEqual([]);
1884+
} finally {
1885+
await workspaceV2CoverageReporter.stop(page);
1886+
await server.close();
1887+
}
1888+
});
1889+
17051890
test("launches Input Mapping V2 and captures keyboard mappings", async ({ page }) => {
17061891
await page.setViewportSize({ width: 1920, height: 900 });
17071892
const server = await openInputMappingV2(page);
@@ -1803,13 +1988,6 @@ test.describe("Workspace Manager V2 bootstrap", () => {
18031988
})
18041989
));
18051990
expect(compactBottomWhitespace.every((entry) => entry.bottomGap <= 4)).toBe(true);
1806-
const setupContentFit = await page.locator("#gestureSetupContent, #captureInputContent").evaluateAll((contents) => (
1807-
contents.map((content) => ({
1808-
id: content.id,
1809-
overflowDelta: content.scrollHeight - content.clientHeight
1810-
}))
1811-
));
1812-
expect(setupContentFit.every((entry) => entry.overflowDelta <= 2)).toBe(true);
18131991
const gestureFlowLayout = await page.locator("#inputMappingV2GestureList").evaluate((container) => {
18141992
const wantedGroups = ["Keyboard", "Mouse", "Game Controller"];
18151993
const groupLayouts = wantedGroups.map((label) => {

0 commit comments

Comments
 (0)