|
| 1 | +import { describe, it, expect, beforeEach, vi, afterEach } from "vitest"; |
| 2 | +import { TeletextAdaptor } from "../../src/teletext_adaptor.js"; |
| 3 | + |
| 4 | +describe("TeletextAdaptor", () => { |
| 5 | + // Constants |
| 6 | + const TELETEXT_IRQ = 5; |
| 7 | + |
| 8 | + // Mock CPU |
| 9 | + const mockCpu = { |
| 10 | + interrupt: 0, |
| 11 | + resetLine: true, |
| 12 | + }; |
| 13 | + |
| 14 | + let teletext; |
| 15 | + |
| 16 | + beforeEach(() => { |
| 17 | + // Reset mocks |
| 18 | + vi.clearAllMocks(); |
| 19 | + mockCpu.interrupt = 0; |
| 20 | + mockCpu.resetLine = true; |
| 21 | + |
| 22 | + // Create fresh teletext adaptor |
| 23 | + teletext = new TeletextAdaptor(mockCpu); |
| 24 | + |
| 25 | + // Silence console logs during tests |
| 26 | + vi.spyOn(console, "log").mockImplementation(() => {}); |
| 27 | + |
| 28 | + // Override loadChannelStream to avoid actual network calls |
| 29 | + teletext.loadChannelStream = vi.fn(); |
| 30 | + }); |
| 31 | + |
| 32 | + afterEach(() => { |
| 33 | + vi.restoreAllMocks(); |
| 34 | + }); |
| 35 | + |
| 36 | + describe("Initialization", () => { |
| 37 | + it("should initialize with default state", () => { |
| 38 | + expect(teletext.teletextStatus).toBe(0x0f); |
| 39 | + expect(teletext.teletextInts).toBe(false); |
| 40 | + expect(teletext.teletextEnable).toBe(false); |
| 41 | + expect(teletext.channel).toBe(0); |
| 42 | + expect(teletext.currentFrame).toBe(0); |
| 43 | + expect(teletext.totalFrames).toBe(0); |
| 44 | + expect(teletext.rowPtr).toBe(0); |
| 45 | + expect(teletext.colPtr).toBe(0); |
| 46 | + expect(teletext.frameBuffer.length).toBe(16); |
| 47 | + expect(teletext.frameBuffer[0].length).toBe(64); |
| 48 | + expect(teletext.streamData).toBe(null); |
| 49 | + expect(teletext.pollCount).toBe(0); |
| 50 | + }); |
| 51 | + |
| 52 | + it("should call loadChannelStream on hard reset", () => { |
| 53 | + // Hard reset |
| 54 | + teletext.reset(true); |
| 55 | + |
| 56 | + // Check if loadChannelStream was called with channel 0 |
| 57 | + expect(teletext.loadChannelStream).toHaveBeenCalledWith(0); |
| 58 | + }); |
| 59 | + |
| 60 | + it("should not call loadChannelStream on soft reset", () => { |
| 61 | + // Soft reset |
| 62 | + teletext.reset(false); |
| 63 | + |
| 64 | + // Check if loadChannelStream was not called |
| 65 | + expect(teletext.loadChannelStream).not.toHaveBeenCalled(); |
| 66 | + }); |
| 67 | + }); |
| 68 | + |
| 69 | + describe("Register operations", () => { |
| 70 | + describe("Read operations", () => { |
| 71 | + it("should read status register (addr 0)", () => { |
| 72 | + teletext.teletextStatus = 0x42; |
| 73 | + expect(teletext.read(0)).toBe(0x42); |
| 74 | + }); |
| 75 | + |
| 76 | + it("should read row register (addr 1)", () => { |
| 77 | + // Row register reads always return 0 |
| 78 | + expect(teletext.read(1)).toBe(0); |
| 79 | + }); |
| 80 | + |
| 81 | + it("should read from frame buffer and increment column pointer (addr 2)", () => { |
| 82 | + // Set up known values in frame buffer |
| 83 | + teletext.rowPtr = 5; |
| 84 | + teletext.colPtr = 10; |
| 85 | + teletext.frameBuffer[5][10] = 0xaa; |
| 86 | + teletext.frameBuffer[5][11] = 0xbb; |
| 87 | + |
| 88 | + // First read should return value at current position and increment column |
| 89 | + expect(teletext.read(2)).toBe(0xaa); |
| 90 | + expect(teletext.colPtr).toBe(11); |
| 91 | + |
| 92 | + // Second read should return next value |
| 93 | + expect(teletext.read(2)).toBe(0xbb); |
| 94 | + expect(teletext.colPtr).toBe(12); |
| 95 | + }); |
| 96 | + |
| 97 | + it("should clear status and interrupt on addr 3 read", () => { |
| 98 | + // Set status and interrupt |
| 99 | + teletext.teletextStatus = 0xff; |
| 100 | + mockCpu.interrupt = 1 << TELETEXT_IRQ; |
| 101 | + |
| 102 | + // Read from addr 3 |
| 103 | + teletext.read(3); |
| 104 | + |
| 105 | + // Status should be cleared (INT, DOR, and FSYN latches) |
| 106 | + expect(teletext.teletextStatus & 0xd0).toBe(0); |
| 107 | + |
| 108 | + // Interrupt should be cleared |
| 109 | + expect(mockCpu.interrupt & (1 << TELETEXT_IRQ)).toBe(0); |
| 110 | + }); |
| 111 | + }); |
| 112 | + |
| 113 | + describe("Write operations", () => { |
| 114 | + it("should update control bits on status register write (addr 0)", () => { |
| 115 | + // Write with teletext enabled, interrupts enabled, channel 2 |
| 116 | + teletext.write(0, 0x0c | 2); // 0x0E |
| 117 | + |
| 118 | + expect(teletext.teletextEnable).toBe(true); |
| 119 | + expect(teletext.teletextInts).toBe(true); |
| 120 | + expect(teletext.channel).toBe(2); |
| 121 | + |
| 122 | + // Check if loadChannelStream was called |
| 123 | + expect(teletext.loadChannelStream).toHaveBeenCalledWith(2); |
| 124 | + }); |
| 125 | + |
| 126 | + it("should not reload channel if channel doesn't change", () => { |
| 127 | + // Set initial state |
| 128 | + teletext.channel = 1; |
| 129 | + teletext.teletextEnable = true; |
| 130 | + |
| 131 | + // Write same channel |
| 132 | + teletext.write(0, 0x0c | 1); // 0x0D |
| 133 | + |
| 134 | + // Check loadChannelStream wasn't called |
| 135 | + expect(teletext.loadChannelStream).not.toHaveBeenCalled(); |
| 136 | + }); |
| 137 | + |
| 138 | + it("should not change channel or load channel if teletext not enabled", () => { |
| 139 | + // Set initial values |
| 140 | + teletext.channel = 1; |
| 141 | + teletext.loadChannelStream.mockClear(); |
| 142 | + |
| 143 | + // Write with teletext disabled, channel 2 |
| 144 | + teletext.write(0, 2); // 0x02 |
| 145 | + |
| 146 | + // Teletext should be disabled |
| 147 | + expect(teletext.teletextEnable).toBe(false); |
| 148 | + |
| 149 | + // Channel should remain unchanged since teletext is disabled |
| 150 | + // According to the implementation in write method, channel is only updated |
| 151 | + // if teletext is enabled: if ((value & 0x03) !== this.channel && this.teletextEnable) |
| 152 | + expect(teletext.channel).toBe(1); |
| 153 | + |
| 154 | + // Check loadChannelStream wasn't called |
| 155 | + expect(teletext.loadChannelStream).not.toHaveBeenCalled(); |
| 156 | + }); |
| 157 | + |
| 158 | + it("should set interrupt flag if INT and interrupts enabled", () => { |
| 159 | + // Set INT latch |
| 160 | + teletext.teletextStatus = 0x80; |
| 161 | + |
| 162 | + // Enable interrupts |
| 163 | + teletext.write(0, 0x08); |
| 164 | + |
| 165 | + // Check if interrupt was set |
| 166 | + expect(mockCpu.interrupt & (1 << TELETEXT_IRQ)).toBe(1 << TELETEXT_IRQ); |
| 167 | + }); |
| 168 | + |
| 169 | + it("should clear interrupt flag if interrupts disabled", () => { |
| 170 | + // Set interrupt |
| 171 | + mockCpu.interrupt = 1 << TELETEXT_IRQ; |
| 172 | + |
| 173 | + // Disable interrupts |
| 174 | + teletext.write(0, 0x00); |
| 175 | + |
| 176 | + // Check if interrupt was cleared |
| 177 | + expect(mockCpu.interrupt & (1 << TELETEXT_IRQ)).toBe(0); |
| 178 | + }); |
| 179 | + |
| 180 | + it("should update row pointer and reset column pointer (addr 1)", () => { |
| 181 | + // Set initial state |
| 182 | + teletext.rowPtr = 0; |
| 183 | + teletext.colPtr = 10; |
| 184 | + |
| 185 | + // Write to row register |
| 186 | + teletext.write(1, 5); |
| 187 | + |
| 188 | + expect(teletext.rowPtr).toBe(5); |
| 189 | + expect(teletext.colPtr).toBe(0); |
| 190 | + }); |
| 191 | + |
| 192 | + it("should write to frame buffer and increment column (addr 2)", () => { |
| 193 | + // Set initial position |
| 194 | + teletext.rowPtr = 3; |
| 195 | + teletext.colPtr = 7; |
| 196 | + |
| 197 | + // Write to data register |
| 198 | + teletext.write(2, 0xaa); |
| 199 | + |
| 200 | + // Check if value was written and column incremented |
| 201 | + expect(teletext.frameBuffer[3][7]).toBe(0xaa); |
| 202 | + expect(teletext.colPtr).toBe(8); |
| 203 | + }); |
| 204 | + |
| 205 | + it("should clear status and interrupt on addr 3 write", () => { |
| 206 | + // Set status and interrupt |
| 207 | + teletext.teletextStatus = 0xff; |
| 208 | + mockCpu.interrupt = 1 << TELETEXT_IRQ; |
| 209 | + |
| 210 | + // Write to addr 3 |
| 211 | + teletext.write(3, 0); |
| 212 | + |
| 213 | + // Status should be cleared (INT, DOR, and FSYN latches) |
| 214 | + expect(teletext.teletextStatus & 0xd0).toBe(0); |
| 215 | + |
| 216 | + // Interrupt should be cleared |
| 217 | + expect(mockCpu.interrupt & (1 << TELETEXT_IRQ)).toBe(0); |
| 218 | + }); |
| 219 | + }); |
| 220 | + }); |
| 221 | + |
| 222 | + describe("Update and polling", () => { |
| 223 | + // Create a mock implementation of update() that doesn't access streamData |
| 224 | + // This avoids trying to access the null streamData property |
| 225 | + let originalUpdate; |
| 226 | + |
| 227 | + beforeEach(() => { |
| 228 | + // Save original update method |
| 229 | + originalUpdate = teletext.update; |
| 230 | + |
| 231 | + // Replace with mock that only updates the status and frame counter |
| 232 | + teletext.update = function () { |
| 233 | + // Set status latches |
| 234 | + this.teletextStatus &= 0x0f; |
| 235 | + this.teletextStatus |= 0xd0; |
| 236 | + |
| 237 | + // Increment frame counter |
| 238 | + if (this.currentFrame >= this.totalFrames - 1) { |
| 239 | + this.currentFrame = 0; |
| 240 | + } else { |
| 241 | + this.currentFrame++; |
| 242 | + } |
| 243 | + |
| 244 | + // Reset pointers |
| 245 | + this.rowPtr = 0; |
| 246 | + this.colPtr = 0; |
| 247 | + |
| 248 | + // Set interrupt if enabled |
| 249 | + if (this.teletextInts) { |
| 250 | + this.cpu.interrupt |= 1 << TELETEXT_IRQ; |
| 251 | + } |
| 252 | + }; |
| 253 | + }); |
| 254 | + |
| 255 | + afterEach(() => { |
| 256 | + // Restore original update method |
| 257 | + teletext.update = originalUpdate; |
| 258 | + }); |
| 259 | + |
| 260 | + it("should update status and frame counter on update()", () => { |
| 261 | + // Set initial state |
| 262 | + teletext.teletextEnable = true; |
| 263 | + teletext.currentFrame = 2; |
| 264 | + teletext.totalFrames = 5; |
| 265 | + |
| 266 | + // Call update |
| 267 | + teletext.update(); |
| 268 | + |
| 269 | + // Check status latches were set |
| 270 | + expect(teletext.teletextStatus & 0xd0).toBe(0xd0); |
| 271 | + |
| 272 | + // Check frame was incremented |
| 273 | + expect(teletext.currentFrame).toBe(3); |
| 274 | + |
| 275 | + // Check pointers were reset |
| 276 | + expect(teletext.rowPtr).toBe(0); |
| 277 | + expect(teletext.colPtr).toBe(0); |
| 278 | + }); |
| 279 | + |
| 280 | + it("should wrap to frame 0 when reaching end of frames", () => { |
| 281 | + // Set to last frame |
| 282 | + teletext.currentFrame = 4; |
| 283 | + teletext.totalFrames = 5; |
| 284 | + |
| 285 | + // Call update |
| 286 | + teletext.update(); |
| 287 | + |
| 288 | + // Check frame wrapped to 0 |
| 289 | + expect(teletext.currentFrame).toBe(0); |
| 290 | + }); |
| 291 | + |
| 292 | + it("should generate interrupt if interrupts enabled", () => { |
| 293 | + // Enable interrupts |
| 294 | + teletext.teletextInts = true; |
| 295 | + |
| 296 | + // Clear interrupt |
| 297 | + mockCpu.interrupt = 0; |
| 298 | + |
| 299 | + // Call update |
| 300 | + teletext.update(); |
| 301 | + |
| 302 | + // Check interrupt was generated |
| 303 | + expect(mockCpu.interrupt & (1 << TELETEXT_IRQ)).toBe(1 << TELETEXT_IRQ); |
| 304 | + }); |
| 305 | + |
| 306 | + it("should not generate interrupt if interrupts disabled", () => { |
| 307 | + // Disable interrupts |
| 308 | + teletext.teletextInts = false; |
| 309 | + |
| 310 | + // Clear interrupt |
| 311 | + mockCpu.interrupt = 0; |
| 312 | + |
| 313 | + // Call update |
| 314 | + teletext.update(); |
| 315 | + |
| 316 | + // Check interrupt wasn't generated |
| 317 | + expect(mockCpu.interrupt & (1 << TELETEXT_IRQ)).toBe(0); |
| 318 | + }); |
| 319 | + |
| 320 | + it("should trigger update when poll cycles exceed threshold", () => { |
| 321 | + // Mock update method |
| 322 | + const updateSpy = vi.spyOn(teletext, "update"); |
| 323 | + |
| 324 | + // Poll with cycles just below threshold (50000) |
| 325 | + teletext.pollCount = 0; |
| 326 | + teletext.polltime(49999); |
| 327 | + |
| 328 | + // Check update wasn't called |
| 329 | + expect(updateSpy).not.toHaveBeenCalled(); |
| 330 | + |
| 331 | + // Poll with cycles to exceed threshold |
| 332 | + teletext.polltime(2); |
| 333 | + |
| 334 | + // Check update was called |
| 335 | + expect(updateSpy).toHaveBeenCalled(); |
| 336 | + |
| 337 | + // Check poll count was reset |
| 338 | + expect(teletext.pollCount).toBe(0); |
| 339 | + }); |
| 340 | + |
| 341 | + it("should not update when CPU reset line is low", () => { |
| 342 | + // Set CPU reset line low |
| 343 | + mockCpu.resetLine = false; |
| 344 | + |
| 345 | + // Mock update method |
| 346 | + const updateSpy = vi.spyOn(teletext, "update"); |
| 347 | + |
| 348 | + // Poll with cycles to exceed threshold |
| 349 | + teletext.pollCount = 0; |
| 350 | + teletext.polltime(50001); |
| 351 | + |
| 352 | + // Check update wasn't called |
| 353 | + expect(updateSpy).not.toHaveBeenCalled(); |
| 354 | + |
| 355 | + // Check poll count was set to negative value |
| 356 | + expect(teletext.pollCount).toBeLessThan(0); |
| 357 | + }); |
| 358 | + }); |
| 359 | +}); |
0 commit comments