Skip to content

Commit 07834de

Browse files
ryan-williamsclaude
andcommitted
Add e2e tests for useActionTriplet, builtinGroup, and sortOrder
- Triplet display: slice shows as single combined row with `/` separators - Triplet functionality: X/Y/Z keys toggle clipping planes - Builtin group: actions appear under "Meta" group - Sort order: builtin actions display in correct order (Show shortcuts, Command palette, Key lookup) - Add slice-along-axis demo using `useActionTriplet` in `ThreeDDemo` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent b0781b6 commit 07834de

File tree

2 files changed

+116
-4
lines changed

2 files changed

+116
-4
lines changed

site/e2e/hotkeys.spec.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2526,4 +2526,98 @@ test.describe('3D Viewer Demo', () => {
25262526

25272527
await page.keyboard.press('Escape')
25282528
})
2529+
2530+
test('action triplet: slice shows as single combined row', async ({ page }) => {
2531+
await page.locator('body').click({ position: { x: 10, y: 10 } })
2532+
2533+
await page.keyboard.press('?')
2534+
await page.waitForSelector('.kbd-modal', { timeout: 5000 })
2535+
2536+
// Slice along X / Y / Z should be a single triplet row
2537+
const tripletRow = page.locator('.kbd-action-triplet-row', { hasText: 'Slice along X / Y / Z' })
2538+
await expect(tripletRow).toBeVisible()
2539+
2540+
// Should have two "/" separators between binding groups
2541+
const seps = tripletRow.locator('.kbd-action-pair-sep')
2542+
await expect(seps).toHaveCount(2)
2543+
2544+
// Should NOT have individual "Slice along X / Y / Z a/b/c" rows
2545+
const viewGroup = page.locator('.kbd-group', { hasText: '3D: VIEW' })
2546+
await expect(viewGroup.locator('.kbd-action', { hasText: /^Slice along X \/ Y \/ Z a$/ })).not.toBeVisible()
2547+
await expect(viewGroup.locator('.kbd-action', { hasText: /^Slice along X \/ Y \/ Z b$/ })).not.toBeVisible()
2548+
await expect(viewGroup.locator('.kbd-action', { hasText: /^Slice along X \/ Y \/ Z c$/ })).not.toBeVisible()
2549+
2550+
await page.keyboard.press('Escape')
2551+
})
2552+
2553+
test('action triplet: slice keys toggle clipping', async ({ page }) => {
2554+
await page.locator('body').click({ position: { x: 10, y: 10 } })
2555+
2556+
const sliceStatus = page.locator('[data-testid="slice-axis"]')
2557+
await expect(sliceStatus).toHaveText('Slice: off')
2558+
2559+
// Press X to slice along X
2560+
await page.keyboard.press('x')
2561+
await expect(sliceStatus).toHaveText('Slice: x')
2562+
2563+
// Press X again to toggle off
2564+
await page.keyboard.press('x')
2565+
await expect(sliceStatus).toHaveText('Slice: off')
2566+
2567+
// Press Y to slice along Y
2568+
await page.keyboard.press('y')
2569+
await expect(sliceStatus).toHaveText('Slice: y')
2570+
2571+
// Press Z to switch to Z
2572+
await page.keyboard.press('z')
2573+
await expect(sliceStatus).toHaveText('Slice: z')
2574+
2575+
// Press Z again to toggle off
2576+
await page.keyboard.press('z')
2577+
await expect(sliceStatus).toHaveText('Slice: off')
2578+
})
2579+
})
2580+
2581+
test.describe('Builtin Group and Sort Order', () => {
2582+
test.beforeEach(async ({ page }) => {
2583+
await page.addInitScript(() => {
2584+
localStorage.removeItem('use-kbd-demo')
2585+
localStorage.removeItem('use-kbd-demo-removed')
2586+
})
2587+
await page.goto('/')
2588+
await expect(page.locator('.kbd-speed-dial-primary')).toBeVisible()
2589+
})
2590+
2591+
test('builtin actions appear under "Meta" group (not "Global")', async ({ page }) => {
2592+
await page.locator('body').click({ position: { x: 10, y: 10 } })
2593+
2594+
await page.keyboard.press('?')
2595+
await page.waitForSelector('.kbd-modal', { timeout: 5000 })
2596+
2597+
// "Meta" group should exist
2598+
const metaGroup = page.locator('.kbd-group', { hasText: 'META' })
2599+
await expect(metaGroup).toBeVisible()
2600+
2601+
// Builtin actions should be in the Meta group, not the Global group
2602+
await expect(metaGroup.locator('.kbd-action', { hasText: 'Show shortcuts' })).toBeVisible()
2603+
await expect(metaGroup.locator('.kbd-action', { hasText: 'Command palette' })).toBeVisible()
2604+
await expect(metaGroup.locator('.kbd-action', { hasText: 'Key lookup' })).toBeVisible()
2605+
2606+
await page.keyboard.press('Escape')
2607+
})
2608+
2609+
test('builtin actions display in order: Show shortcuts, Command palette, Key lookup', async ({ page }) => {
2610+
await page.locator('body').click({ position: { x: 10, y: 10 } })
2611+
2612+
await page.keyboard.press('?')
2613+
await page.waitForSelector('.kbd-modal', { timeout: 5000 })
2614+
2615+
const metaGroup = page.locator('.kbd-group', { hasText: 'META' })
2616+
const labels = metaGroup.locator('.kbd-action-label')
2617+
const texts = await labels.allTextContents()
2618+
2619+
expect(texts).toEqual(['Show shortcuts', 'Command palette', 'Key lookup'])
2620+
2621+
await page.keyboard.press('Escape')
2622+
})
25292623
})

site/src/routes/ThreeDDemo.tsx

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useCallback, useEffect, useRef, useState } from 'react'
22
import * as THREE from 'three'
3-
import { KbdModal, ModeIndicator, ShortcutsModal, useAction, useActionPair, useArrowGroup, useMode } from 'use-kbd'
3+
import { KbdModal, ModeIndicator, ShortcutsModal, useAction, useActionPair, useActionTriplet, useArrowGroup, useMode } from 'use-kbd'
44
import 'use-kbd/styles.css'
55
import { useTheme } from '../contexts/ThemeContext'
66

@@ -54,6 +54,7 @@ function Viewer() {
5454
const [roll, setRoll] = useState(() => getStored('roll', DEFAULTS.roll))
5555
const [target, setTarget] = useState(() => getStored('target', DEFAULTS.target))
5656
const [wireframe, setWireframe] = useState(() => getStored('wireframe', false))
57+
const [sliceAxis, setSliceAxis] = useState<'x' | 'y' | 'z' | null>(() => getStored('sliceAxis', null))
5758

5859
// Persist to sessionStorage
5960
useEffect(() => { sessionStorage.setItem(`${STORAGE_KEY}-azimuth`, JSON.stringify(azimuth)) }, [azimuth])
@@ -62,13 +63,15 @@ function Viewer() {
6263
useEffect(() => { sessionStorage.setItem(`${STORAGE_KEY}-roll`, JSON.stringify(roll)) }, [roll])
6364
useEffect(() => { sessionStorage.setItem(`${STORAGE_KEY}-target`, JSON.stringify(target)) }, [target])
6465
useEffect(() => { sessionStorage.setItem(`${STORAGE_KEY}-wireframe`, JSON.stringify(wireframe)) }, [wireframe])
66+
useEffect(() => { sessionStorage.setItem(`${STORAGE_KEY}-sliceAxis`, JSON.stringify(sliceAxis)) }, [sliceAxis])
6567

6668
// Initialize Three.js scene
6769
useEffect(() => {
6870
const container = containerRef.current
6971
if (!container) return
7072

7173
const renderer = new THREE.WebGLRenderer({ antialias: true })
74+
renderer.localClippingEnabled = true
7275
renderer.setPixelRatio(window.devicePixelRatio)
7376
renderer.setSize(container.clientWidth, 500)
7477
container.appendChild(renderer.domElement)
@@ -140,11 +143,15 @@ function Viewer() {
140143
// Update background
141144
scene.background = new THREE.Color(bg)
142145

143-
// Update wireframe
146+
// Update wireframe and clipping
144147
const cube = cubeRef.current
145148
if (cube) {
149+
const clipPlane = sliceAxis === 'x' ? [new THREE.Plane(new THREE.Vector3(-1, 0, 0), 0)]
150+
: sliceAxis === 'y' ? [new THREE.Plane(new THREE.Vector3(0, -1, 0), 0.75)]
151+
: sliceAxis === 'z' ? [new THREE.Plane(new THREE.Vector3(0, 0, -1), 0)]
152+
: []
146153
const mats = cube.material as THREE.MeshStandardMaterial[]
147-
mats.forEach(m => { m.wireframe = wireframe })
154+
mats.forEach(m => { m.wireframe = wireframe; m.clippingPlanes = clipPlane; m.clipShadows = true })
148155
}
149156

150157
// Spherical to Cartesian
@@ -175,7 +182,7 @@ function Viewer() {
175182
}
176183
render()
177184
return () => cancelAnimationFrame(frameRef.current)
178-
}, [azimuth, elevation, distance, roll, target, wireframe, bg])
185+
}, [azimuth, elevation, distance, roll, target, wireframe, sliceAxis, bg])
179186

180187
// Modes
181188
const orbitMode = useMode('view:orbit', {
@@ -314,6 +321,16 @@ function Viewer() {
314321
handler: useCallback(() => setWireframe(w => !w), []),
315322
})
316323

324+
useActionTriplet('view:slice', {
325+
label: 'Slice along X / Y / Z',
326+
group: '3D: View',
327+
actions: [
328+
{ defaultBindings: ['x'], handler: useCallback(() => setSliceAxis(a => a === 'x' ? null : 'x'), []) },
329+
{ defaultBindings: ['y'], handler: useCallback(() => setSliceAxis(a => a === 'y' ? null : 'y'), []) },
330+
{ defaultBindings: ['z'], handler: useCallback(() => setSliceAxis(a => a === 'z' ? null : 'z'), []) },
331+
],
332+
})
333+
317334
// Mouse wheel: zoom (bare), pan (Shift), roll (Ctrl)
318335
useEffect(() => {
319336
const container = containerRef.current
@@ -352,6 +369,7 @@ function Viewer() {
352369
Target: ({target.x.toFixed(1)}, {target.y.toFixed(1)}, {target.z.toFixed(1)})
353370
</span>
354371
<span data-testid="wireframe">Wireframe: {wireframe ? 'on' : 'off'}</span>
372+
<span data-testid="slice-axis">Slice: {sliceAxis ?? 'off'}</span>
355373
</div>
356374

357375
<ModeIndicator position="bottom-left" />

0 commit comments

Comments
 (0)