ac6502 1.9.3 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/components/CPU.d.ts +4 -0
  2. package/dist/components/CPU.js +87 -30
  3. package/dist/components/CPU.js.map +1 -1
  4. package/dist/components/IO/ACIA.d.ts +13 -21
  5. package/dist/components/IO/ACIA.js +53 -151
  6. package/dist/components/IO/ACIA.js.map +1 -1
  7. package/dist/components/IO/Attachments/KeyboardEncoderAttachment.d.ts +5 -10
  8. package/dist/components/IO/Attachments/KeyboardEncoderAttachment.js +43 -266
  9. package/dist/components/IO/Attachments/KeyboardEncoderAttachment.js.map +1 -1
  10. package/dist/components/IO/Empty.d.ts +1 -3
  11. package/dist/components/IO/Empty.js +1 -5
  12. package/dist/components/IO/Empty.js.map +1 -1
  13. package/dist/components/IO/RAMBank.d.ts +3 -4
  14. package/dist/components/IO/RAMBank.js +4 -13
  15. package/dist/components/IO/RAMBank.js.map +1 -1
  16. package/dist/components/IO/RTC.d.ts +2 -3
  17. package/dist/components/IO/RTC.js +17 -7
  18. package/dist/components/IO/RTC.js.map +1 -1
  19. package/dist/components/IO/Sound.d.ts +1 -3
  20. package/dist/components/IO/Sound.js +13 -23
  21. package/dist/components/IO/Sound.js.map +1 -1
  22. package/dist/components/IO/Storage.d.ts +1 -3
  23. package/dist/components/IO/Storage.js +1 -3
  24. package/dist/components/IO/Storage.js.map +1 -1
  25. package/dist/components/IO/VIA.d.ts +1 -3
  26. package/dist/components/IO/VIA.js +6 -7
  27. package/dist/components/IO/VIA.js.map +1 -1
  28. package/dist/components/IO/Video.d.ts +1 -3
  29. package/dist/components/IO/Video.js +3 -5
  30. package/dist/components/IO/Video.js.map +1 -1
  31. package/dist/components/IO.d.ts +1 -3
  32. package/dist/components/Machine.d.ts +1 -2
  33. package/dist/components/Machine.js +21 -74
  34. package/dist/components/Machine.js.map +1 -1
  35. package/dist/index.js +1 -1
  36. package/dist/index.js.map +1 -1
  37. package/dist/tests/IO/ACIA.test.js +57 -108
  38. package/dist/tests/IO/ACIA.test.js.map +1 -1
  39. package/dist/tests/IO/Attachments/KeyboardEncoderAttachment.test.js +334 -574
  40. package/dist/tests/IO/Attachments/KeyboardEncoderAttachment.test.js.map +1 -1
  41. package/dist/tests/IO/Empty.test.js +2 -14
  42. package/dist/tests/IO/Empty.test.js.map +1 -1
  43. package/dist/tests/IO/RAMBank.test.js +5 -12
  44. package/dist/tests/IO/RAMBank.test.js.map +1 -1
  45. package/dist/tests/IO/RTC.test.js +7 -16
  46. package/dist/tests/IO/RTC.test.js.map +1 -1
  47. package/dist/tests/IO/Sound.test.js +6 -8
  48. package/dist/tests/IO/Sound.test.js.map +1 -1
  49. package/dist/tests/IO/Storage.test.js +0 -6
  50. package/dist/tests/IO/Storage.test.js.map +1 -1
  51. package/dist/tests/IO/VIA.test.js +6 -10
  52. package/dist/tests/IO/VIA.test.js.map +1 -1
  53. package/dist/tests/IO/Video.test.js +7 -7
  54. package/dist/tests/IO/Video.test.js.map +1 -1
  55. package/package.json +1 -1
  56. package/src/components/CPU.ts +94 -31
  57. package/src/components/IO/ACIA.ts +57 -176
  58. package/src/components/IO/Attachments/KeyboardEncoderAttachment.ts +45 -217
  59. package/src/components/IO/Empty.ts +1 -4
  60. package/src/components/IO/RAMBank.ts +4 -15
  61. package/src/components/IO/RTC.ts +18 -7
  62. package/src/components/IO/Sound.ts +14 -27
  63. package/src/components/IO/Storage.ts +2 -5
  64. package/src/components/IO/VIA.ts +6 -8
  65. package/src/components/IO/Video.ts +5 -7
  66. package/src/components/IO.ts +1 -4
  67. package/src/components/Machine.ts +22 -90
  68. package/src/index.ts +1 -1
  69. package/src/tests/IO/ACIA.test.ts +60 -122
  70. package/src/tests/IO/Attachments/KeyboardEncoderAttachment.test.ts +342 -676
  71. package/src/tests/IO/Empty.test.ts +2 -17
  72. package/src/tests/IO/RAMBank.test.ts +5 -14
  73. package/src/tests/IO/RTC.test.ts +7 -20
  74. package/src/tests/IO/Sound.test.ts +6 -8
  75. package/src/tests/IO/Storage.test.ts +0 -7
  76. package/src/tests/IO/VIA.test.ts +6 -12
  77. package/src/tests/IO/Video.test.ts +7 -8
@@ -5,9 +5,15 @@ describe('KeyboardEncoderAttachment', () => {
5
5
 
6
6
  beforeEach(() => {
7
7
  encoder = new KeyboardEncoderAttachment(5)
8
+ encoder.activePort = 'both' // Enable both ports for testing
8
9
  })
9
10
 
10
11
  describe('Initialization', () => {
12
+ it('should default to Port B only', () => {
13
+ const fresh = new KeyboardEncoderAttachment(5)
14
+ expect(fresh.activePort).toBe('B')
15
+ })
16
+
11
17
  it('should initialize with no data ready', () => {
12
18
  expect(encoder.hasDataReadyA()).toBe(false)
13
19
  expect(encoder.hasDataReadyB()).toBe(false)
@@ -20,7 +26,7 @@ describe('KeyboardEncoderAttachment', () => {
20
26
 
21
27
  it('should be disabled by default (CA2/CB2 high)', () => {
22
28
  encoder.updateControlLines(false, true, false, true)
23
- encoder.updateKey(0x04, true) // 'a'
29
+ encoder.updateKey(0x04, true) // 'A'
24
30
  expect(encoder.hasCA1Interrupt()).toBe(false)
25
31
  expect(encoder.hasCB1Interrupt()).toBe(false)
26
32
  })
@@ -32,51 +38,60 @@ describe('KeyboardEncoderAttachment', () => {
32
38
 
33
39
  describe('Reset', () => {
34
40
  it('should clear all data and states', () => {
35
- // Enable and generate some data
36
41
  encoder.updateControlLines(false, false, false, false)
37
- encoder.updateKey(0x04, true) // 'a'
42
+ encoder.updateKey(0x04, true) // 'A'
38
43
  expect(encoder.hasDataReadyA()).toBe(true)
39
44
 
40
- // Reset
41
45
  encoder.reset()
42
46
  expect(encoder.hasDataReadyA()).toBe(false)
43
47
  expect(encoder.hasDataReadyB()).toBe(false)
44
48
  expect(encoder.hasCA1Interrupt()).toBe(false)
45
49
  expect(encoder.hasCB1Interrupt()).toBe(false)
46
50
  })
51
+
52
+ it('should clear modifier states on reset', () => {
53
+ encoder.updateControlLines(false, false, false, false)
54
+ // Press Shift, then reset
55
+ encoder.updateKey(0xE1, true) // Left Shift down
56
+ encoder.reset()
57
+ encoder.updateControlLines(false, false, false, false)
58
+ // After reset, Shift should be released - numbers produce numbers, not symbols
59
+ encoder.updateKey(0x1E, true) // '1'
60
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(0x31) // '1', not '!'
61
+ })
47
62
  })
48
63
 
49
64
  describe('Enable/Disable Control', () => {
50
65
  it('should enable Port A when CA2 is LOW', () => {
51
66
  encoder.updateControlLines(false, false, false, true)
52
- encoder.updateKey(0x04, true) // 'a'
67
+ encoder.updateKey(0x04, true) // 'A'
53
68
  expect(encoder.hasCA1Interrupt()).toBe(true)
54
69
  expect(encoder.hasCB1Interrupt()).toBe(false)
55
70
  })
56
71
 
57
72
  it('should enable Port B when CB2 is LOW', () => {
58
73
  encoder.updateControlLines(false, true, false, false)
59
- encoder.updateKey(0x04, true) // 'a'
74
+ encoder.updateKey(0x04, true) // 'A'
60
75
  expect(encoder.hasCA1Interrupt()).toBe(false)
61
76
  expect(encoder.hasCB1Interrupt()).toBe(true)
62
77
  })
63
78
 
64
79
  it('should enable both ports when both CA2 and CB2 are LOW', () => {
65
80
  encoder.updateControlLines(false, false, false, false)
66
- encoder.updateKey(0x04, true) // 'a'
81
+ encoder.updateKey(0x04, true) // 'A'
67
82
  expect(encoder.hasCA1Interrupt()).toBe(true)
68
83
  expect(encoder.hasCB1Interrupt()).toBe(true)
69
84
  })
70
85
 
71
86
  it('should disable Port A when CA2 is HIGH', () => {
72
87
  encoder.updateControlLines(false, true, false, false)
73
- encoder.updateKey(0x04, true) // 'a'
88
+ encoder.updateKey(0x04, true) // 'A'
74
89
  expect(encoder.hasCA1Interrupt()).toBe(false)
75
90
  })
76
91
 
77
92
  it('should disable Port B when CB2 is HIGH', () => {
78
93
  encoder.updateControlLines(false, false, false, true)
79
- encoder.updateKey(0x04, true) // 'a'
94
+ encoder.updateKey(0x04, true) // 'A'
80
95
  expect(encoder.hasCB1Interrupt()).toBe(false)
81
96
  })
82
97
  })
@@ -94,32 +109,32 @@ describe('KeyboardEncoderAttachment', () => {
94
109
 
95
110
  it('should return ASCII data when data ready on Port A', () => {
96
111
  encoder.updateControlLines(false, false, false, false)
97
- encoder.updateKey(0x04, true) // 'a' = 0x61
112
+ encoder.updateKey(0x04, true) // 'A' = 0x41
98
113
  const value = encoder.readPortA(0xFF, 0x00)
99
- expect(value).toBe(0x61)
114
+ expect(value).toBe(0x41)
100
115
  })
101
116
 
102
117
  it('should return ASCII data when data ready on Port B', () => {
103
118
  encoder.updateControlLines(false, false, false, false)
104
- encoder.updateKey(0x04, true) // 'a' = 0x61
119
+ encoder.updateKey(0x04, true) // 'A' = 0x41
105
120
  const value = encoder.readPortB(0xFF, 0x00)
106
- expect(value).toBe(0x61)
121
+ expect(value).toBe(0x41)
107
122
  })
108
123
 
109
124
  it('should return 0xFF on disabled port even with data ready', () => {
110
125
  encoder.updateControlLines(false, true, false, true) // Both disabled
111
- encoder.updateKey(0x04, true) // 'a'
126
+ encoder.updateKey(0x04, true) // 'A'
112
127
  expect(encoder.readPortA(0xFF, 0x00)).toBe(0xFF)
113
128
  expect(encoder.readPortB(0xFF, 0x00)).toBe(0xFF)
114
129
  })
115
130
 
116
131
  it('should provide same data on both ports', () => {
117
132
  encoder.updateControlLines(false, false, false, false)
118
- encoder.updateKey(0x04, true) // 'a'
133
+ encoder.updateKey(0x04, true) // 'A'
119
134
  const valueA = encoder.readPortA(0xFF, 0x00)
120
135
  const valueB = encoder.readPortB(0xFF, 0x00)
121
136
  expect(valueA).toBe(valueB)
122
- expect(valueA).toBe(0x61)
137
+ expect(valueA).toBe(0x41)
123
138
  })
124
139
  })
125
140
 
@@ -127,20 +142,20 @@ describe('KeyboardEncoderAttachment', () => {
127
142
  it('should trigger CA1 interrupt when Port A enabled and key pressed', () => {
128
143
  encoder.updateControlLines(false, false, false, true)
129
144
  expect(encoder.hasCA1Interrupt()).toBe(false)
130
- encoder.updateKey(0x04, true) // 'a'
145
+ encoder.updateKey(0x04, true) // 'A'
131
146
  expect(encoder.hasCA1Interrupt()).toBe(true)
132
147
  })
133
148
 
134
149
  it('should trigger CB1 interrupt when Port B enabled and key pressed', () => {
135
150
  encoder.updateControlLines(false, true, false, false)
136
151
  expect(encoder.hasCB1Interrupt()).toBe(false)
137
- encoder.updateKey(0x04, true) // 'a'
152
+ encoder.updateKey(0x04, true) // 'A'
138
153
  expect(encoder.hasCB1Interrupt()).toBe(true)
139
154
  })
140
155
 
141
156
  it('should clear CA1 interrupt and data ready when cleared', () => {
142
157
  encoder.updateControlLines(false, false, false, false)
143
- encoder.updateKey(0x04, true) // 'a'
158
+ encoder.updateKey(0x04, true) // 'A'
144
159
  expect(encoder.hasCA1Interrupt()).toBe(true)
145
160
  expect(encoder.hasDataReadyA()).toBe(true)
146
161
 
@@ -151,7 +166,7 @@ describe('KeyboardEncoderAttachment', () => {
151
166
 
152
167
  it('should clear CB1 interrupt and data ready when cleared', () => {
153
168
  encoder.updateControlLines(false, false, false, false)
154
- encoder.updateKey(0x04, true) // 'a'
169
+ encoder.updateKey(0x04, true) // 'A'
155
170
  expect(encoder.hasCB1Interrupt()).toBe(true)
156
171
  expect(encoder.hasDataReadyB()).toBe(true)
157
172
 
@@ -162,380 +177,356 @@ describe('KeyboardEncoderAttachment', () => {
162
177
 
163
178
  it('should not trigger interrupt when port is disabled', () => {
164
179
  encoder.updateControlLines(false, true, false, true) // Both disabled
165
- encoder.updateKey(0x04, true) // 'a'
180
+ encoder.updateKey(0x04, true) // 'A'
166
181
  expect(encoder.hasCA1Interrupt()).toBe(false)
167
182
  expect(encoder.hasCB1Interrupt()).toBe(false)
168
183
  })
169
184
  })
170
185
 
171
- describe('Basic Key Mapping', () => {
186
+ describe('Letter Key Mapping', () => {
172
187
  beforeEach(() => {
173
188
  encoder.updateControlLines(false, false, false, false)
174
189
  })
175
190
 
176
- it('should map lowercase letters correctly', () => {
177
- encoder.updateKey(0x04, true) // 'a'
178
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x61)
179
-
180
- encoder.clearInterrupts(true, false, true, false)
181
- encoder.updateKey(0x1D, true) // 'z'
182
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x7A)
183
- })
184
-
185
- it('should map numbers correctly', () => {
186
- encoder.updateKey(0x1E, true) // '1'
187
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x31)
188
-
189
- encoder.clearInterrupts(true, false, true, false)
190
- encoder.updateKey(0x27, true) // '0'
191
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x30)
192
- })
193
-
194
- it('should map special keys correctly', () => {
195
- encoder.updateKey(0x28, true) // Enter
196
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x0D)
197
-
198
- encoder.clearInterrupts(true, false, true, false)
199
- encoder.updateKey(0x29, true) // Escape
200
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x1B)
191
+ it('should output uppercase letters A-Z', () => {
192
+ // 'A' (HID 0x04)
193
+ encoder.updateKey(0x04, true)
194
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(0x41)
201
195
 
202
196
  encoder.clearInterrupts(true, false, true, false)
203
- encoder.updateKey(0x2C, true) // Space
204
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x20)
197
+ // 'Z' (HID 0x1D)
198
+ encoder.updateKey(0x1D, true)
199
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(0x5A)
205
200
  })
206
201
 
207
- it('should ignore key releases', () => {
208
- encoder.updateKey(0x04, true) // Press 'a'
209
- expect(encoder.hasDataReadyA()).toBe(true)
202
+ it('should output uppercase letters even with Shift held', () => {
203
+ encoder.updateKey(0xE1, true) // Left Shift down
204
+ encoder.updateKey(0x04, true) // 'A'
205
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(0x41) // Still uppercase A
210
206
 
211
207
  encoder.clearInterrupts(true, false, true, false)
212
- encoder.updateKey(0x04, false) // Release 'a'
213
- expect(encoder.hasDataReadyA()).toBe(false) // No new data
214
- })
215
-
216
- it('should ignore unknown keycodes', () => {
217
- encoder.updateKey(0xFF, true) // Invalid keycode
218
- expect(encoder.hasDataReadyA()).toBe(false)
208
+ encoder.updateKey(0x10, true) // 'M'
209
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(0x4D) // Still uppercase M
210
+ })
211
+
212
+ it('should map all letters correctly', () => {
213
+ const letterMap: [number, number][] = [
214
+ [0x04, 0x41], [0x05, 0x42], [0x06, 0x43], [0x07, 0x44],
215
+ [0x08, 0x45], [0x09, 0x46], [0x0A, 0x47], [0x0B, 0x48],
216
+ [0x0C, 0x49], [0x0D, 0x4A], [0x0E, 0x4B], [0x0F, 0x4C],
217
+ [0x10, 0x4D], [0x11, 0x4E], [0x12, 0x4F], [0x13, 0x50],
218
+ [0x14, 0x51], [0x15, 0x52], [0x16, 0x53], [0x17, 0x54],
219
+ [0x18, 0x55], [0x19, 0x56], [0x1A, 0x57], [0x1B, 0x58],
220
+ [0x1C, 0x59], [0x1D, 0x5A],
221
+ ]
222
+ for (const [hid, ascii] of letterMap) {
223
+ encoder.clearInterrupts(true, false, true, false)
224
+ encoder.updateKey(hid, true)
225
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(ascii)
226
+ }
219
227
  })
220
228
  })
221
229
 
222
- describe('Modifier Keys', () => {
230
+ describe('Number Key Mapping', () => {
223
231
  beforeEach(() => {
224
232
  encoder.updateControlLines(false, false, false, false)
225
233
  })
226
234
 
227
- it('should not generate output for modifier keys alone', () => {
228
- encoder.updateKey(0xE0, true) // Left Ctrl
229
- expect(encoder.hasDataReadyA()).toBe(false)
230
-
231
- encoder.updateKey(0xE1, true) // Left Shift
232
- expect(encoder.hasDataReadyA()).toBe(false)
233
-
234
- encoder.updateKey(0xE2, true) // Left Alt
235
- expect(encoder.hasDataReadyA()).toBe(false)
236
- })
237
-
238
- it('should track modifier key state across presses', () => {
239
- // Press Shift
240
- encoder.updateKey(0xE1, true)
241
- encoder.updateKey(0x04, true) // 'a' -> 'A'
242
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x41)
235
+ it('should map numbers correctly', () => {
236
+ encoder.updateKey(0x1E, true) // '1'
237
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(0x31)
243
238
 
244
- // Release Shift
245
239
  encoder.clearInterrupts(true, false, true, false)
246
- encoder.updateKey(0xE1, false)
247
- encoder.updateKey(0x04, true) // 'a'
248
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x61)
240
+ encoder.updateKey(0x27, true) // '0'
241
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(0x30)
249
242
  })
250
243
 
251
- it('should handle both left and right modifiers', () => {
252
- // Left Ctrl
253
- encoder.updateKey(0xE0, true)
254
- encoder.updateKey(0x04, true) // Ctrl+a
255
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x01)
256
-
257
- encoder.clearInterrupts(true, false, true, false)
258
- encoder.updateKey(0xE0, false)
259
-
260
- // Right Ctrl
261
- encoder.updateKey(0xE4, true)
262
- encoder.updateKey(0x04, true) // Ctrl+a
263
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x01)
244
+ it('should map all number keys 0-9', () => {
245
+ const numberMap: [number, number][] = [
246
+ [0x1E, 0x31], [0x1F, 0x32], [0x20, 0x33], [0x21, 0x34],
247
+ [0x22, 0x35], [0x23, 0x36], [0x24, 0x37], [0x25, 0x38],
248
+ [0x26, 0x39], [0x27, 0x30],
249
+ ]
250
+ for (const [hid, ascii] of numberMap) {
251
+ encoder.clearInterrupts(true, false, true, false)
252
+ encoder.updateKey(hid, true)
253
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(ascii)
254
+ }
264
255
  })
265
256
  })
266
257
 
267
- describe('Shift Key Mapping', () => {
258
+ describe('Special Key Mapping', () => {
268
259
  beforeEach(() => {
269
260
  encoder.updateControlLines(false, false, false, false)
270
261
  })
271
262
 
272
- it('should map Shift+letter to uppercase', () => {
273
- encoder.updateKey(0xE1, true) // Press Shift
274
- encoder.updateKey(0x04, true) // 'a' -> 'A'
275
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x41)
263
+ it('should map Enter to CR (0x0D)', () => {
264
+ encoder.updateKey(0x28, true)
265
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(0x0D)
276
266
  })
277
267
 
278
- it('should map Shift+number to symbols', () => {
279
- encoder.updateKey(0xE1, true) // Press Shift
280
- encoder.updateKey(0x1E, true) // '1' -> '!'
281
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x21)
282
-
283
- encoder.clearInterrupts(true, false, true, false)
284
- encoder.updateKey(0x25, true) // '8' -> '*'
285
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x2A)
268
+ it('should map Escape to ESC (0x1B)', () => {
269
+ encoder.updateKey(0x29, true)
270
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(0x1B)
286
271
  })
287
272
 
288
- it('should map Shift+special keys to shifted symbols', () => {
289
- encoder.updateKey(0xE1, true) // Press Shift
290
-
291
- encoder.updateKey(0x2D, true) // '-' -> '_'
292
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x5F)
293
-
294
- encoder.clearInterrupts(true, false, true, false)
295
- encoder.updateKey(0x2E, true) // '=' -> '+'
296
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x2B)
273
+ it('should map Backspace to BS (0x08)', () => {
274
+ encoder.updateKey(0x2A, true)
275
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(0x08)
276
+ })
297
277
 
298
- encoder.clearInterrupts(true, false, true, false)
299
- encoder.updateKey(0x2F, true) // '[' -> '{'
300
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x7B)
278
+ it('should map Tab to HT (0x09)', () => {
279
+ encoder.updateKey(0x2B, true)
280
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(0x09)
301
281
  })
302
- })
303
282
 
304
- describe('Ctrl Key Mapping', () => {
305
- beforeEach(() => {
306
- encoder.updateControlLines(false, false, false, false)
283
+ it('should map Space to SP (0x20)', () => {
284
+ encoder.updateKey(0x2C, true)
285
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(0x20)
307
286
  })
308
287
 
309
- it('should map Ctrl+letter to control codes', () => {
310
- encoder.updateKey(0xE0, true) // Press Ctrl
311
- encoder.updateKey(0x04, true) // Ctrl+a -> 0x01
312
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x01)
288
+ it('should map Delete to DEL (0x7F)', () => {
289
+ encoder.updateKey(0x4C, true)
290
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(0x7F)
291
+ })
313
292
 
314
- encoder.clearInterrupts(true, false, true, false)
315
- encoder.updateKey(0x1D, true) // Ctrl+z -> 0x1A
293
+ it('should map Insert to SUB (0x1A)', () => {
294
+ encoder.updateKey(0x49, true)
316
295
  expect(encoder.readPortA(0xFF, 0x00)).toBe(0x1A)
317
296
  })
318
297
 
319
- it('should map Ctrl+special keys to control codes', () => {
320
- encoder.updateKey(0xE0, true) // Press Ctrl
321
-
322
- encoder.updateKey(0x2F, true) // Ctrl+[ -> ESC (0x1B)
323
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x1B)
298
+ it('should map arrow keys correctly', () => {
299
+ encoder.updateKey(0x4F, true) // Right Arrow
300
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(0x1D)
324
301
 
325
302
  encoder.clearInterrupts(true, false, true, false)
326
- encoder.updateKey(0x31, true) // Ctrl+\ -> FS (0x1C)
303
+ encoder.updateKey(0x50, true) // Left Arrow
327
304
  expect(encoder.readPortA(0xFF, 0x00)).toBe(0x1C)
328
305
 
329
306
  encoder.clearInterrupts(true, false, true, false)
330
- encoder.updateKey(0x30, true) // Ctrl+] -> GS (0x1D)
331
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x1D)
332
- })
333
-
334
- it('should map Ctrl+2 to NUL', () => {
335
- encoder.updateKey(0xE0, true) // Press Ctrl
336
- encoder.updateKey(0x1F, true) // Ctrl+2 -> 0x00
337
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x00)
338
- })
307
+ encoder.updateKey(0x51, true) // Down Arrow
308
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(0x1F)
339
309
 
340
- it('should map Ctrl+6 to RS (UP arrow)', () => {
341
- encoder.updateKey(0xE0, true) // Press Ctrl
342
- encoder.updateKey(0x23, true) // Ctrl+6 -> 0x1E
310
+ encoder.clearInterrupts(true, false, true, false)
311
+ encoder.updateKey(0x52, true) // Up Arrow
343
312
  expect(encoder.readPortA(0xFF, 0x00)).toBe(0x1E)
344
313
  })
345
314
 
346
- it('should map Ctrl+- to US (DOWN arrow)', () => {
347
- encoder.updateKey(0xE0, true) // Press Ctrl
348
- encoder.updateKey(0x2D, true) // Ctrl+- -> 0x1F
349
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x1F)
315
+ it('should map symbol keys correctly', () => {
316
+ const symbolMap: [number, number][] = [
317
+ [0x2D, 0x2D], // -
318
+ [0x2E, 0x3D], // =
319
+ [0x2F, 0x5B], // [
320
+ [0x30, 0x5D], // ]
321
+ [0x31, 0x5C], // backslash
322
+ [0x33, 0x3B], // ;
323
+ [0x34, 0x27], // '
324
+ [0x35, 0x60], // `
325
+ [0x36, 0x2C], // ,
326
+ [0x37, 0x2E], // .
327
+ [0x38, 0x2F], // /
328
+ ]
329
+ for (const [hid, ascii] of symbolMap) {
330
+ encoder.clearInterrupts(true, false, true, false)
331
+ encoder.updateKey(hid, true)
332
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(ascii)
333
+ }
350
334
  })
351
335
  })
352
336
 
353
- describe('MENU Key Mapping', () => {
337
+ describe('Shift Key Mapping', () => {
354
338
  beforeEach(() => {
355
339
  encoder.updateControlLines(false, false, false, false)
356
- })
357
-
358
- it('should map MENU key to 0x80', () => {
359
- encoder.updateKey(0xE3, true) // Left GUI (MENU)
360
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x80)
361
- })
362
-
363
- it('should map Alt+MENU to 0x90', () => {
364
- encoder.updateKey(0xE2, true) // Press Alt
365
- encoder.updateKey(0xE3, true) // MENU
366
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x90)
367
- })
368
-
369
- it('should handle Right GUI as MENU', () => {
370
- encoder.updateKey(0xE7, true) // Right GUI (MENU)
371
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x80)
340
+ encoder.updateKey(0xE1, true) // Left Shift down
341
+ })
342
+
343
+ it('should produce shifted number symbols', () => {
344
+ const shiftNumberMap: [number, number][] = [
345
+ [0x1E, 0x21], // '1' -> '!'
346
+ [0x1F, 0x40], // '2' -> '@'
347
+ [0x20, 0x23], // '3' -> '#'
348
+ [0x21, 0x24], // '4' -> '$'
349
+ [0x22, 0x25], // '5' -> '%'
350
+ [0x23, 0x5E], // '6' -> '^'
351
+ [0x24, 0x26], // '7' -> '&'
352
+ [0x25, 0x2A], // '8' -> '*'
353
+ [0x26, 0x28], // '9' -> '('
354
+ [0x27, 0x29], // '0' -> ')'
355
+ ]
356
+ for (const [hid, ascii] of shiftNumberMap) {
357
+ encoder.clearInterrupts(true, false, true, false)
358
+ encoder.updateKey(hid, true)
359
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(ascii)
360
+ }
361
+ })
362
+
363
+ it('should produce shifted symbol keys', () => {
364
+ const shiftSymbolMap: [number, number][] = [
365
+ [0x2D, 0x5F], // '-' -> '_'
366
+ [0x2E, 0x2B], // '=' -> '+'
367
+ [0x2F, 0x7B], // '[' -> '{'
368
+ [0x30, 0x7D], // ']' -> '}'
369
+ [0x31, 0x7C], // '\\' -> '|'
370
+ [0x33, 0x3A], // ';' -> ':'
371
+ [0x34, 0x22], // '\'' -> '"'
372
+ [0x36, 0x3C], // ',' -> '<'
373
+ [0x37, 0x3E], // '.' -> '>'
374
+ [0x38, 0x3F], // '/' -> '?'
375
+ [0x35, 0x7E], // '`' -> '~'
376
+ ]
377
+ for (const [hid, ascii] of shiftSymbolMap) {
378
+ encoder.clearInterrupts(true, false, true, false)
379
+ encoder.updateKey(hid, true)
380
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(ascii)
381
+ }
382
+ })
383
+
384
+ it('should not change letter output when Shift is held (already uppercase)', () => {
385
+ encoder.updateKey(0x04, true) // 'A'
386
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(0x41) // Still 'A'
387
+ })
388
+
389
+ it('should track right Shift the same as left Shift', () => {
390
+ encoder.updateKey(0xE1, false) // Release left Shift
391
+ encoder.updateKey(0xE5, true) // Right Shift down
392
+ encoder.updateKey(0x1E, true) // '1'
393
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(0x21) // '!'
372
394
  })
373
395
  })
374
396
 
375
- describe('Function Key Mapping', () => {
397
+ describe('Ctrl Key Mapping', () => {
376
398
  beforeEach(() => {
377
399
  encoder.updateControlLines(false, false, false, false)
400
+ encoder.updateKey(0xE0, true) // Left Ctrl down
401
+ })
402
+
403
+ it('should produce control codes for Ctrl+A through Ctrl+Z', () => {
404
+ const ctrlLetterMap: [number, number][] = [
405
+ [0x04, 0x01], // Ctrl+A
406
+ [0x05, 0x02], // Ctrl+B
407
+ [0x06, 0x03], // Ctrl+C
408
+ [0x07, 0x04], // Ctrl+D
409
+ [0x08, 0x05], // Ctrl+E
410
+ [0x09, 0x06], // Ctrl+F
411
+ [0x0A, 0x07], // Ctrl+G
412
+ [0x0B, 0x08], // Ctrl+H
413
+ [0x0C, 0x09], // Ctrl+I
414
+ [0x0D, 0x0A], // Ctrl+J
415
+ [0x0E, 0x0B], // Ctrl+K
416
+ [0x0F, 0x0C], // Ctrl+L
417
+ [0x10, 0x0D], // Ctrl+M
418
+ [0x11, 0x0E], // Ctrl+N
419
+ [0x12, 0x0F], // Ctrl+O
420
+ [0x13, 0x10], // Ctrl+P
421
+ [0x14, 0x11], // Ctrl+Q
422
+ [0x15, 0x12], // Ctrl+R
423
+ [0x16, 0x13], // Ctrl+S
424
+ [0x17, 0x14], // Ctrl+T
425
+ [0x18, 0x15], // Ctrl+U
426
+ [0x19, 0x16], // Ctrl+V
427
+ [0x1A, 0x17], // Ctrl+W
428
+ [0x1B, 0x18], // Ctrl+X
429
+ [0x1C, 0x19], // Ctrl+Y
430
+ [0x1D, 0x1A], // Ctrl+Z
431
+ ]
432
+ for (const [hid, ascii] of ctrlLetterMap) {
433
+ encoder.clearInterrupts(true, false, true, false)
434
+ encoder.updateKey(hid, true)
435
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(ascii)
436
+ }
437
+ })
438
+
439
+ it('should produce Ctrl+C = 0x03 (BASIC break)', () => {
440
+ encoder.updateKey(0x06, true) // 'C'
441
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(0x03)
442
+ })
443
+
444
+ it('should produce Ctrl+2 = NUL (0x00)', () => {
445
+ encoder.updateKey(0x1F, true) // '2'
446
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(0x00)
378
447
  })
379
448
 
380
- it('should map F1-F12 to 0x81-0x8C', () => {
381
- encoder.updateKey(0x3A, true) // F1
382
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x81)
383
-
384
- encoder.clearInterrupts(true, false, true, false)
385
- encoder.updateKey(0x3B, true) // F2
386
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x82)
387
-
388
- encoder.clearInterrupts(true, false, true, false)
389
- encoder.updateKey(0x45, true) // F12
390
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x8C)
391
- })
392
-
393
- it('should map F13-F15 to 0x8D-0x8F', () => {
394
- encoder.updateKey(0x68, true) // F13
395
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x8D)
396
-
397
- encoder.clearInterrupts(true, false, true, false)
398
- encoder.updateKey(0x6A, true) // F15
399
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x8F)
400
- })
401
-
402
- it('should map Alt+F1-F12 to 0x91-0x9C', () => {
403
- encoder.updateKey(0xE2, true) // Press Alt
404
-
405
- encoder.updateKey(0x3A, true) // Alt+F1
406
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x91)
407
-
408
- encoder.clearInterrupts(true, false, true, false)
409
- encoder.updateKey(0x45, true) // Alt+F12
410
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x9C)
411
- })
412
-
413
- it('should map Alt+F13-F15 to 0x9D-0x9F', () => {
414
- encoder.updateKey(0xE2, true) // Press Alt
415
-
416
- encoder.updateKey(0x68, true) // Alt+F13
417
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x9D)
418
-
419
- encoder.clearInterrupts(true, false, true, false)
420
- encoder.updateKey(0x6A, true) // Alt+F15
421
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x9F)
449
+ it('should produce Ctrl+6 = RS (0x1E)', () => {
450
+ encoder.updateKey(0x23, true) // '6'
451
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(0x1E)
422
452
  })
423
- })
424
453
 
425
- describe('Alt Key Mapping', () => {
426
- beforeEach(() => {
427
- encoder.updateControlLines(false, false, false, false)
454
+ it('should produce Ctrl+- = US (0x1F)', () => {
455
+ encoder.updateKey(0x2D, true) // '-'
456
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(0x1F)
428
457
  })
429
458
 
430
- it('should map Alt+letter to extended character set', () => {
431
- encoder.updateKey(0xE2, true) // Press Alt
432
-
433
- encoder.updateKey(0x04, true) // Alt+a -> 0xE1
434
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xE1)
435
-
436
- encoder.clearInterrupts(true, false, true, false)
437
- encoder.updateKey(0x1D, true) // Alt+z -> 0xFA
438
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xFA)
459
+ it('should produce Ctrl+[ = ESC (0x1B)', () => {
460
+ encoder.updateKey(0x2F, true) // '['
461
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(0x1B)
439
462
  })
440
463
 
441
- it('should map Alt+number to extended character set', () => {
442
- encoder.updateKey(0xE2, true) // Press Alt
443
-
444
- encoder.updateKey(0x1E, true) // Alt+1 -> 0xB1
445
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xB1)
446
-
447
- encoder.clearInterrupts(true, false, true, false)
448
- encoder.updateKey(0x27, true) // Alt+0 -> 0xB0
449
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xB0)
464
+ it('should produce Ctrl+\\ = FS (0x1C)', () => {
465
+ encoder.updateKey(0x31, true) // '\\'
466
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(0x1C)
450
467
  })
451
468
 
452
- it('should map Alt+Space to 0xA0', () => {
453
- encoder.updateKey(0xE2, true) // Press Alt
454
- encoder.updateKey(0x2C, true) // Alt+Space -> 0xA0
455
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xA0)
469
+ it('should produce Ctrl+] = GS (0x1D)', () => {
470
+ encoder.updateKey(0x30, true) // ']'
471
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(0x1D)
456
472
  })
457
473
 
458
- it('should map Alt+DEL to 0xFF', () => {
459
- encoder.updateKey(0xE2, true) // Press Alt
460
- encoder.updateKey(0x4C, true) // Alt+DEL -> 0xFF
461
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xFF)
474
+ it('should track right Ctrl the same as left Ctrl', () => {
475
+ encoder.updateKey(0xE0, false) // Release left Ctrl
476
+ encoder.updateKey(0xE4, true) // Right Ctrl down
477
+ encoder.updateKey(0x04, true) // 'A'
478
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(0x01) // Ctrl+A
462
479
  })
463
480
  })
464
481
 
465
- describe('Alt+Shift Key Mapping', () => {
482
+ describe('Modifier Key Behavior', () => {
466
483
  beforeEach(() => {
467
484
  encoder.updateControlLines(false, false, false, false)
468
485
  })
469
486
 
470
- it('should map Alt+Shift+letter to extended character set', () => {
471
- encoder.updateKey(0xE2, true) // Press Alt
472
- encoder.updateKey(0xE1, true) // Press Shift
473
-
474
- encoder.updateKey(0x04, true) // Alt+Shift+a -> 0xC1
475
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xC1)
476
-
477
- encoder.clearInterrupts(true, false, true, false)
478
- encoder.updateKey(0x1D, true) // Alt+Shift+z -> 0xDA
479
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xDA)
487
+ it('should not generate output for Ctrl press/release alone', () => {
488
+ encoder.updateKey(0xE0, true) // Left Ctrl down
489
+ expect(encoder.hasDataReadyA()).toBe(false)
490
+ encoder.updateKey(0xE0, false) // Left Ctrl up
491
+ expect(encoder.hasDataReadyA()).toBe(false)
480
492
  })
481
493
 
482
- it('should map Alt+Shift+number to extended character set', () => {
483
- encoder.updateKey(0xE2, true) // Press Alt
484
- encoder.updateKey(0xE1, true) // Press Shift
485
-
486
- encoder.updateKey(0x1E, true) // Alt+Shift+1 -> 0xA1
487
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xA1)
488
-
489
- encoder.clearInterrupts(true, false, true, false)
490
- encoder.updateKey(0x1F, true) // Alt+Shift+2 -> 0xC0
491
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xC0)
494
+ it('should not generate output for Shift press/release alone', () => {
495
+ encoder.updateKey(0xE1, true) // Left Shift down
496
+ expect(encoder.hasDataReadyA()).toBe(false)
497
+ encoder.updateKey(0xE1, false) // Left Shift up
498
+ expect(encoder.hasDataReadyA()).toBe(false)
492
499
  })
493
500
 
494
- it('should map Alt+Shift+symbols to extended character set', () => {
495
- encoder.updateKey(0xE2, true) // Press Alt
496
- encoder.updateKey(0xE1, true) // Press Shift
497
-
498
- encoder.updateKey(0x2D, true) // Alt+Shift+- -> 0xDF
499
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xDF)
500
-
501
+ it('should not generate output for key release events', () => {
502
+ encoder.updateKey(0x04, true) // 'A' pressed
503
+ expect(encoder.hasDataReadyA()).toBe(true)
501
504
  encoder.clearInterrupts(true, false, true, false)
502
- encoder.updateKey(0x2F, true) // Alt+Shift+[ -> 0xFB
503
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xFB)
504
- })
505
- })
506
505
 
507
- describe('Complex Key Combinations', () => {
508
- beforeEach(() => {
509
- encoder.updateControlLines(false, false, false, false)
506
+ encoder.updateKey(0x04, false) // 'A' released
507
+ expect(encoder.hasDataReadyA()).toBe(false) // No new data from release
510
508
  })
511
509
 
512
- it('should handle Ctrl+C combination', () => {
513
- encoder.updateKey(0xE0, true) // Press Ctrl
514
- encoder.updateKey(0x06, true) // c
515
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x03) // ETX
510
+ it('should ignore Alt key (no output, no modifier effect)', () => {
511
+ encoder.updateKey(0xE2, true) // Left Alt down - ignored
512
+ expect(encoder.hasDataReadyA()).toBe(false)
513
+ encoder.updateKey(0x04, true) // 'A'
514
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(0x41) // Normal 'A', no alt mapping
516
515
  })
517
516
 
518
- it('should prioritize Ctrl over Shift', () => {
519
- encoder.updateKey(0xE0, true) // Press Ctrl
520
- encoder.updateKey(0xE1, true) // Press Shift
521
- encoder.updateKey(0x04, true) // a
522
- // When both Ctrl and Shift are pressed, shift is ignored for Ctrl combinations
523
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x01) // Ctrl+a
517
+ it('should ignore Caps Lock key', () => {
518
+ encoder.updateKey(0x39, true) // Caps Lock - ignored
519
+ expect(encoder.hasDataReadyA()).toBe(false)
524
520
  })
525
521
 
526
- it('should prioritize Alt+Shift over Alt alone', () => {
527
- encoder.updateKey(0xE2, true) // Press Alt
528
- encoder.updateKey(0xE1, true) // Press Shift
529
- encoder.updateKey(0x04, true) // a
530
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xC1) // Alt+Shift+a, not Alt+a (0xE1)
522
+ it('should ignore GUI/MENU keys', () => {
523
+ encoder.updateKey(0xE3, true) // Left GUI - ignored (no mapping in table)
524
+ expect(encoder.hasDataReadyA()).toBe(false)
531
525
  })
532
526
 
533
- it('should apply Alt when both Ctrl and Alt are active', () => {
534
- encoder.updateKey(0xE0, true) // Press Ctrl
535
- encoder.updateKey(0xE2, true) // Press Alt
536
- encoder.updateKey(0x04, true) // a
537
- // Alt takes effect when both Ctrl and Alt are pressed (per C++ implementation)
538
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xE1) // Alt+a
527
+ it('should ignore unrecognized keycodes', () => {
528
+ encoder.updateKey(0x3A, true) // F1 - no mapping
529
+ expect(encoder.hasDataReadyA()).toBe(false)
539
530
  })
540
531
  })
541
532
 
@@ -544,418 +535,93 @@ describe('KeyboardEncoderAttachment', () => {
544
535
  encoder.updateControlLines(false, false, false, false)
545
536
  })
546
537
 
547
- it('should handle multiple sequential key presses', () => {
548
- encoder.updateKey(0x04, true) // 'a'
549
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x61)
550
-
551
- encoder.clearInterrupts(true, false, true, false)
552
- encoder.updateKey(0x05, true) // 'b'
553
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x62)
554
-
555
- encoder.clearInterrupts(true, false, true, false)
556
- encoder.updateKey(0x06, true) // 'c'
557
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x63)
558
- })
559
-
560
- it('should overwrite previous data with new key press', () => {
561
- encoder.updateKey(0x04, true) // 'a'
562
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x61)
563
-
564
- // New key press without clearing interrupts
565
- encoder.updateKey(0x05, true) // 'b'
566
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x62) // Overwritten
567
- })
568
- })
569
-
570
- describe('Edge Cases', () => {
571
- beforeEach(() => {
572
- encoder.updateControlLines(false, false, false, false)
573
- })
574
-
575
- it('should handle rapid modifier changes', () => {
576
- encoder.updateKey(0xE1, true) // Press Shift
577
- encoder.updateKey(0xE1, false) // Release Shift
578
- encoder.updateKey(0xE1, true) // Press Shift again
579
- encoder.updateKey(0x04, true) // 'a' -> 'A'
538
+ it('should overwrite data with new key press', () => {
539
+ encoder.updateKey(0x04, true) // 'A'
580
540
  expect(encoder.readPortA(0xFF, 0x00)).toBe(0x41)
581
- })
582
-
583
- it('should handle all modifiers released', () => {
584
- encoder.updateKey(0xE0, true) // Press Ctrl
585
- encoder.updateKey(0xE1, true) // Press Shift
586
- encoder.updateKey(0xE2, true) // Press Alt
587
- encoder.updateKey(0xE0, false) // Release Ctrl
588
- encoder.updateKey(0xE1, false) // Release Shift
589
- encoder.updateKey(0xE2, false) // Release Alt
590
- encoder.updateKey(0x04, true) // 'a'
591
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x61) // Plain 'a'
592
- })
593
541
 
594
- it('should handle port disabled mid-operation', () => {
595
- encoder.updateKey(0x04, true) // 'a'
596
- expect(encoder.hasCA1Interrupt()).toBe(true)
597
-
598
- // Disable port
599
- encoder.updateControlLines(false, true, false, true)
600
- expect(encoder.hasCA1Interrupt()).toBe(false) // Interrupt not visible when disabled
542
+ encoder.updateKey(0x05, true) // 'B' overwrites without clearing
543
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(0x42)
601
544
  })
602
545
 
603
- it('should handle re-enabling port with data still present', () => {
604
- encoder.updateKey(0x04, true) // 'a'
605
- encoder.updateControlLines(false, true, false, true) // Disable
606
-
607
- // Re-enable
608
- encoder.updateControlLines(false, false, false, false)
609
- // Data should still be there
610
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x61)
611
- })
612
- })
613
-
614
- describe('Alt+Shift Symbol Mapping (Extended Character Set)', () => {
615
- beforeEach(() => {
616
- encoder.updateControlLines(false, false, false, false)
617
- encoder.updateKey(0xE2, true) // Press Alt
618
- encoder.updateKey(0xE1, true) // Press Shift
619
- })
620
-
621
- afterEach(() => {
546
+ it('should handle read-clear-press cycle', () => {
547
+ encoder.updateKey(0x04, true) // 'A'
548
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(0x41)
622
549
  encoder.clearInterrupts(true, false, true, false)
623
- encoder.updateKey(0xE2, false) // Release Alt
624
- encoder.updateKey(0xE1, false) // Release Shift
625
- })
626
-
627
- it('should map Alt+Shift+1 to ¡ (0xA1)', () => {
628
- encoder.updateKey(0x1E, true) // '1'
629
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xA1)
630
- })
631
-
632
- it('should map Alt+Shift+\' to ¢ (0xA2)', () => {
633
- encoder.updateKey(0x34, true) // '\'' (apostrophe, USB HID 0x34)
634
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xA2)
635
- })
636
-
637
- it('should map Alt+Shift+3 to £ (0xA3)', () => {
638
- encoder.updateKey(0x20, true) // '3'
639
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xA3)
640
- })
641
-
642
- it('should map Alt+Shift+4 to ¤ (0xA4)', () => {
643
- encoder.updateKey(0x21, true) // '4'
644
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xA4)
645
- })
646
-
647
- it('should map Alt+Shift+5 to ¥ (0xA5)', () => {
648
- encoder.updateKey(0x22, true) // '5'
649
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xA5)
650
- })
651
-
652
- it('should map Alt+Shift+7 to ¦ (0xA6)', () => {
653
- encoder.updateKey(0x24, true) // '7'
654
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xA6)
655
- })
656
-
657
- it('should map Alt+Shift+9 to ¨ (0xA8)', () => {
658
- encoder.updateKey(0x26, true) // '9'
659
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xA8)
660
- })
661
-
662
- it('should map Alt+Shift+0 to © (0xA9)', () => {
663
- encoder.updateKey(0x27, true) // '0'
664
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xA9)
665
- })
666
-
667
- it('should map Alt+Shift+8 to ª (0xAA)', () => {
668
- encoder.updateKey(0x25, true) // '8'
669
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xAA)
670
- })
671
-
672
- it('should map Alt+Shift+= to « (0xAB)', () => {
673
- encoder.updateKey(0x2E, true) // '='
674
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xAB)
675
- })
676
-
677
- it('should map Alt+Shift+; to º (0xBA)', () => {
678
- encoder.updateKey(0x33, true) // ';'
679
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xBA)
680
- })
681
-
682
- it('should map Alt+Shift+, to ¼ (0xBC)', () => {
683
- encoder.updateKey(0x36, true) // ','
684
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xBC)
685
- })
686
-
687
- it('should map Alt+Shift+. to ¾ (0xBE)', () => {
688
- encoder.updateKey(0x37, true) // '.'
689
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xBE)
690
- })
691
-
692
- it('should map Alt+Shift+/ to ¿ (0xBF)', () => {
693
- encoder.updateKey(0x38, true) // '/'
694
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xBF)
695
- })
696
-
697
- it('should map Alt+Shift+2 to À (0xC0)', () => {
698
- encoder.updateKey(0x1F, true) // '2'
699
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xC0)
700
- })
701
-
702
- it('should map Alt+Shift+b to  (0xC2)', () => {
703
- encoder.updateKey(0x05, true) // 'b'
704
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xC2)
705
- })
706
-
707
- it('should map Alt+Shift+c to à (0xC3)', () => {
708
- encoder.updateKey(0x06, true) // 'c'
709
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xC3)
710
- })
711
-
712
- it('should map Alt+Shift+d to Ä (0xC4)', () => {
713
- encoder.updateKey(0x07, true) // 'd'
714
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xC4)
715
- })
716
-
717
- it('should map Alt+Shift+e to Å (0xC5)', () => {
718
- encoder.updateKey(0x08, true) // 'e'
719
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xC5)
720
- })
721
-
722
- it('should map Alt+Shift+f to Æ (0xC6)', () => {
723
- encoder.updateKey(0x09, true) // 'f'
724
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xC6)
725
- })
726
-
727
- it('should map Alt+Shift+g to Ç (0xC7)', () => {
728
- encoder.updateKey(0x0A, true) // 'g'
729
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xC7)
730
- })
731
-
732
- it('should map Alt+Shift+h to È (0xC8)', () => {
733
- encoder.updateKey(0x0B, true) // 'h'
734
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xC8)
735
- })
736
-
737
- it('should map Alt+Shift+6 to Þ (0xDE)', () => {
738
- encoder.updateKey(0x23, true) // '6'
739
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xDE)
740
- })
741
-
742
- it('should map Alt+Shift+- to ß (0xDF)', () => {
743
- encoder.updateKey(0x2D, true) // '-'
744
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xDF)
745
- })
746
-
747
- it('should map Alt+Shift+[ to û (0xFB)', () => {
748
- encoder.updateKey(0x2F, true) // '['
749
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xFB)
750
- })
550
+ expect(encoder.hasDataReadyA()).toBe(false)
751
551
 
752
- it('should map Alt+Shift+\\ to ü (0xFC)', () => {
753
- encoder.updateKey(0x31, true) // '\\'
754
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xFC)
552
+ encoder.updateKey(0x05, true) // 'B'
553
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(0x42)
554
+ expect(encoder.hasDataReadyA()).toBe(true)
755
555
  })
756
556
 
757
- it('should map Alt+Shift+] to ý (0xFD)', () => {
758
- encoder.updateKey(0x30, true) // ']'
759
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xFD)
760
- })
557
+ it('should handle modifier press then key press', () => {
558
+ encoder.updateKey(0xE0, true) // Ctrl down
559
+ encoder.updateKey(0x06, true) // 'C' -> Ctrl+C = 0x03
560
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(0x03)
761
561
 
762
- it('should map Alt+Shift+` to þ (0xFE)', () => {
763
- encoder.updateKey(0x35, true) // '`'
764
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xFE)
562
+ encoder.clearInterrupts(true, false, true, false)
563
+ encoder.updateKey(0xE0, false) // Ctrl up
564
+ encoder.updateKey(0x06, true) // 'C' -> normal 'C' = 0x43
565
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(0x43)
765
566
  })
766
567
  })
767
568
 
768
- describe('Alt Symbol Mapping (Without Shift)', () => {
569
+ describe('Active Port Selection', () => {
769
570
  beforeEach(() => {
770
571
  encoder.updateControlLines(false, false, false, false)
771
- encoder.updateKey(0xE2, true) // Press Alt
772
572
  })
773
573
 
774
- afterEach(() => {
775
- encoder.clearInterrupts(true, false, true, false)
776
- encoder.updateKey(0xE2, false) // Release Alt
777
- })
778
-
779
- it('should map Alt+\' to § (0xA7)', () => {
780
- encoder.updateKey(0x34, true) // '\''
781
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xA7)
782
- })
783
-
784
- it('should map Alt+, to ¬ (0xAC)', () => {
785
- encoder.updateKey(0x36, true) // ','
786
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xAC)
787
- })
788
-
789
- it('should map Alt+- to soft hyphen (0xAD)', () => {
790
- encoder.updateKey(0x2D, true) // '-'
791
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xAD)
792
- })
793
-
794
- it('should map Alt+. to ® (0xAE)', () => {
795
- encoder.updateKey(0x37, true) // '.'
796
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xAE)
797
- })
798
-
799
- it('should map Alt+/ to ¯ (0xAF)', () => {
800
- encoder.updateKey(0x38, true) // '/'
801
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xAF)
802
- })
803
-
804
- it('should map Alt+; to » (0xBB)', () => {
805
- encoder.updateKey(0x33, true) // ';'
806
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xBB)
807
- })
808
-
809
- it('should map Alt+= to ½ (0xBD)', () => {
810
- encoder.updateKey(0x2E, true) // '='
811
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xBD)
812
- })
813
-
814
- it('should map Alt+[ to Û (0xDB)', () => {
815
- encoder.updateKey(0x2F, true) // '['
816
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xDB)
817
- })
818
-
819
- it('should map Alt+\\ to Ü (0xDC)', () => {
820
- encoder.updateKey(0x31, true) // '\\'
821
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xDC)
574
+ it('should default to both ports active', () => {
575
+ expect(encoder.activePort).toBe('both')
576
+ encoder.updateKey(0x04, true) // 'A'
577
+ expect(encoder.hasDataReadyA()).toBe(true)
578
+ expect(encoder.hasDataReadyB()).toBe(true)
822
579
  })
823
580
 
824
- it('should map Alt+] to Ý (0xDD)', () => {
825
- encoder.updateKey(0x30, true) // ']'
826
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xDD)
581
+ it('should only update Port A when activePort is A', () => {
582
+ encoder.activePort = 'A'
583
+ encoder.updateKey(0x04, true) // 'A'
584
+ expect(encoder.hasDataReadyA()).toBe(true)
585
+ expect(encoder.hasDataReadyB()).toBe(false)
827
586
  })
828
587
 
829
- it('should map Alt+` to à (0xE0)', () => {
830
- encoder.updateKey(0x35, true) // '`'
831
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0xE0)
588
+ it('should only update Port B when activePort is B', () => {
589
+ encoder.activePort = 'B'
590
+ encoder.updateKey(0x04, true) // 'A'
591
+ expect(encoder.hasDataReadyA()).toBe(false)
592
+ expect(encoder.hasDataReadyB()).toBe(true)
832
593
  })
833
594
  })
834
595
 
835
- describe('Ctrl Combinations Coverage', () => {
596
+ describe('Edge Cases', () => {
836
597
  beforeEach(() => {
837
598
  encoder.updateControlLines(false, false, false, false)
838
- encoder.updateKey(0xE0, true) // Press Ctrl
839
- })
840
-
841
- afterEach(() => {
842
- encoder.clearInterrupts(true, false, true, false)
843
- encoder.updateKey(0xE0, false) // Release Ctrl
844
- })
845
-
846
- it('should map Ctrl+b to 0x02', () => {
847
- encoder.updateKey(0x05, true) // 'b'
848
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x02)
849
- })
850
-
851
- it('should map Ctrl+d to 0x04', () => {
852
- encoder.updateKey(0x07, true) // 'd'
853
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x04)
854
- })
855
-
856
- it('should map Ctrl+e to 0x05', () => {
857
- encoder.updateKey(0x08, true) // 'e'
858
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x05)
859
- })
860
-
861
- it('should map Ctrl+f to 0x06', () => {
862
- encoder.updateKey(0x09, true) // 'f'
863
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x06)
864
- })
865
-
866
- it('should map Ctrl+g to 0x07', () => {
867
- encoder.updateKey(0x0A, true) // 'g'
868
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x07)
869
- })
870
-
871
- it('should map Ctrl+h to 0x08', () => {
872
- encoder.updateKey(0x0B, true) // 'h'
873
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x08)
874
- })
875
-
876
- it('should map Ctrl+i to 0x09', () => {
877
- encoder.updateKey(0x0C, true) // 'i'
878
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x09)
879
- })
880
-
881
- it('should map Ctrl+j to 0x0A', () => {
882
- encoder.updateKey(0x0D, true) // 'j'
883
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x0A)
884
- })
885
-
886
- it('should map Ctrl+k to 0x0B', () => {
887
- encoder.updateKey(0x0E, true) // 'k'
888
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x0B)
889
- })
890
-
891
- it('should map Ctrl+l to 0x0C', () => {
892
- encoder.updateKey(0x0F, true) // 'l'
893
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x0C)
894
- })
895
-
896
- it('should map Ctrl+m to 0x0D', () => {
897
- encoder.updateKey(0x10, true) // 'm'
898
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x0D)
899
- })
900
-
901
- it('should map Ctrl+n to 0x0E', () => {
902
- encoder.updateKey(0x11, true) // 'n'
903
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x0E)
904
- })
905
-
906
- it('should map Ctrl+o to 0x0F', () => {
907
- encoder.updateKey(0x12, true) // 'o'
908
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x0F)
909
599
  })
910
600
 
911
- it('should map Ctrl+p to 0x10', () => {
912
- encoder.updateKey(0x13, true) // 'p'
913
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x10)
914
- })
915
-
916
- it('should map Ctrl+q to 0x11', () => {
917
- encoder.updateKey(0x14, true) // 'q'
918
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x11)
919
- })
920
-
921
- it('should map Ctrl+r to 0x12', () => {
922
- encoder.updateKey(0x15, true) // 'r'
923
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x12)
924
- })
925
-
926
- it('should map Ctrl+s to 0x13', () => {
927
- encoder.updateKey(0x16, true) // 's'
928
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x13)
929
- })
930
-
931
- it('should map Ctrl+t to 0x14', () => {
932
- encoder.updateKey(0x17, true) // 't'
933
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x14)
934
- })
935
-
936
- it('should map Ctrl+u to 0x15', () => {
937
- encoder.updateKey(0x18, true) // 'u'
938
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x15)
939
- })
940
-
941
- it('should map Ctrl+v to 0x16', () => {
942
- encoder.updateKey(0x19, true) // 'v'
943
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x16)
944
- })
601
+ it('should handle port disable mid-operation', () => {
602
+ encoder.updateKey(0x04, true) // 'A'
603
+ expect(encoder.hasCA1Interrupt()).toBe(true)
945
604
 
946
- it('should map Ctrl+w to 0x17', () => {
947
- encoder.updateKey(0x1A, true) // 'w'
948
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x17)
605
+ // Disable Port A
606
+ encoder.updateControlLines(false, true, false, false)
607
+ expect(encoder.hasCA1Interrupt()).toBe(false)
608
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(0xFF) // Disabled, returns 0xFF
949
609
  })
950
610
 
951
- it('should map Ctrl+x to 0x18', () => {
952
- encoder.updateKey(0x1B, true) // 'x'
953
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x18)
611
+ it('should allow re-enabling port and reading existing data', () => {
612
+ encoder.updateKey(0x04, true) // 'A'
613
+ encoder.updateControlLines(false, true, false, true) // Disable both
614
+ encoder.updateControlLines(false, false, false, false) // Re-enable both
615
+ // Data should still be readable (dataReady is still true, just interrupt was gated)
616
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(0x41)
954
617
  })
955
618
 
956
- it('should map Ctrl+y to 0x19', () => {
957
- encoder.updateKey(0x1C, true) // 'y'
958
- expect(encoder.readPortA(0xFF, 0x00)).toBe(0x19)
619
+ it('should handle Ctrl+2 producing NUL (0x00) correctly', () => {
620
+ encoder.updateKey(0xE0, true) // Ctrl down
621
+ encoder.updateKey(0x1F, true) // '2'
622
+ // Should produce 0x00 and data should be ready
623
+ expect(encoder.hasDataReadyA()).toBe(true)
624
+ expect(encoder.readPortA(0xFF, 0x00)).toBe(0x00)
959
625
  })
960
626
  })
961
627
  })