feat: W1 frontend — index.html, style.css, keybar, main.ts (esbuild entry)
- T8 public/index.html — #term/#keybar, loads ./build/main.js (type=module)
- T9 public/style.css — fullscreen term, keybar fixed bottom, hidden >768px
- T10 public/keybar.ts — key→byte map + mountKeybar (13 tests)
- T11 public/main.ts — xterm+fit, WS client (wss on https/M6), reconnect
backoff, resize debounce, mountKeybar wiring
Verified by orchestrator: backend+frontend tsc clean; full suite 141 tests;
npm run build:web bundles main.ts -> public/build/main.js (430kb).
Note: main.ts uses one @ts-ignore for the CSS import (minor; future: *.css d.ts).
This commit is contained in:
106
test/keybar.test.ts
Normal file
106
test/keybar.test.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* test/keybar.test.ts — Unit tests for keybar module (T10).
|
||||
*
|
||||
* Tests the pure KEY_MAP data structure and verifies each key
|
||||
* is mapped to the correct byte string (ARCHITECTURE §5 / §6.3).
|
||||
*
|
||||
* Per task spec: "对纯字节表单测,DOM 部分手动验" —
|
||||
* We test the byte mappings here. DOM behavior (button creation,
|
||||
* event binding, touchstart vs click) is left to manual E2E verification
|
||||
* since jsdom is not in the project dependencies.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { KEY_MAP } from '../public/keybar.js'
|
||||
|
||||
describe('keybar', () => {
|
||||
describe('KEY_MAP — pure byte mappings', () => {
|
||||
it('exports KEY_MAP as a constant object', () => {
|
||||
expect(KEY_MAP).toBeDefined()
|
||||
expect(typeof KEY_MAP).toBe('object')
|
||||
})
|
||||
|
||||
it('Esc maps to \\x1b (0x1B)', () => {
|
||||
expect(KEY_MAP.esc).toBe('\x1b')
|
||||
expect(KEY_MAP.esc.charCodeAt(0)).toBe(0x1b)
|
||||
})
|
||||
|
||||
it('Shift+Tab maps to \\x1b[Z', () => {
|
||||
expect(KEY_MAP.shiftTab).toBe('\x1b[Z')
|
||||
expect(KEY_MAP.shiftTab).toHaveLength(3)
|
||||
expect(KEY_MAP.shiftTab.charCodeAt(0)).toBe(0x1b)
|
||||
expect(KEY_MAP.shiftTab.charCodeAt(1)).toBe('['.charCodeAt(0))
|
||||
expect(KEY_MAP.shiftTab.charCodeAt(2)).toBe('Z'.charCodeAt(0))
|
||||
})
|
||||
|
||||
it('arrow up maps to \\x1b[A', () => {
|
||||
expect(KEY_MAP.arrowUp).toBe('\x1b[A')
|
||||
expect(KEY_MAP.arrowUp).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('arrow down maps to \\x1b[B', () => {
|
||||
expect(KEY_MAP.arrowDown).toBe('\x1b[B')
|
||||
expect(KEY_MAP.arrowDown).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('arrow left maps to \\x1b[D', () => {
|
||||
expect(KEY_MAP.arrowLeft).toBe('\x1b[D')
|
||||
expect(KEY_MAP.arrowLeft).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('arrow right maps to \\x1b[C', () => {
|
||||
expect(KEY_MAP.arrowRight).toBe('\x1b[C')
|
||||
expect(KEY_MAP.arrowRight).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('Enter maps to \\r (0x0D carriage return, not \\n)', () => {
|
||||
expect(KEY_MAP.enter).toBe('\r')
|
||||
expect(KEY_MAP.enter).toHaveLength(1)
|
||||
expect(KEY_MAP.enter.charCodeAt(0)).toBe(0x0d)
|
||||
expect(KEY_MAP.enter).not.toBe('\n')
|
||||
expect(KEY_MAP.enter.charCodeAt(0)).not.toBe(0x0a)
|
||||
})
|
||||
|
||||
it('Ctrl+C maps to \\x03 (0x03 ETX)', () => {
|
||||
expect(KEY_MAP.ctrlC).toBe('\x03')
|
||||
expect(KEY_MAP.ctrlC).toHaveLength(1)
|
||||
expect(KEY_MAP.ctrlC.charCodeAt(0)).toBe(0x03)
|
||||
})
|
||||
|
||||
it('Tab maps to \\t (0x09)', () => {
|
||||
expect(KEY_MAP.tab).toBe('\t')
|
||||
expect(KEY_MAP.tab).toHaveLength(1)
|
||||
expect(KEY_MAP.tab.charCodeAt(0)).toBe(0x09)
|
||||
})
|
||||
|
||||
it('all 9 keys are defined', () => {
|
||||
const keys = Object.keys(KEY_MAP)
|
||||
expect(keys).toEqual([
|
||||
'esc',
|
||||
'shiftTab',
|
||||
'arrowUp',
|
||||
'arrowDown',
|
||||
'arrowLeft',
|
||||
'arrowRight',
|
||||
'enter',
|
||||
'ctrlC',
|
||||
'tab',
|
||||
])
|
||||
})
|
||||
|
||||
it('all mapped bytes are unique strings', () => {
|
||||
const bytes = Object.values(KEY_MAP)
|
||||
expect(bytes).toHaveLength(9)
|
||||
expect(new Set(bytes).size).toBe(9) // all unique
|
||||
})
|
||||
|
||||
it('ANSI arrow sequences use correct format', () => {
|
||||
// All arrow keys follow \x1b[X pattern where X is A/B/C/D
|
||||
const arrowKeys = [KEY_MAP.arrowUp, KEY_MAP.arrowDown, KEY_MAP.arrowLeft, KEY_MAP.arrowRight]
|
||||
arrowKeys.forEach((seq) => {
|
||||
expect(seq).toMatch(/^\x1b\[./)
|
||||
expect(seq).toHaveLength(3)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user