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