Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit d1d17c8

Browse files
mattgodboltclaude
andcommittedApr 7, 2025··
Add unit tests for video.js with focus on Teletext mode
- Created comprehensive unit tests for Video class - Added tests for interaction with Teletext class - Exported display flag constants from video.js - Achieved 61% code coverage for video.js module 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 34e272f commit d1d17c8

File tree

2 files changed

+296
-8
lines changed

2 files changed

+296
-8
lines changed
 

‎src/video.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
import { Teletext } from "./teletext.js";
33
import * as utils from "./utils.js";
44

5-
const VDISPENABLE = 1 << 0,
6-
HDISPENABLE = 1 << 1,
7-
SKEWDISPENABLE = 1 << 2,
8-
SCANLINEDISPENABLE = 1 << 3,
9-
USERDISPENABLE = 1 << 4,
10-
FRAMESKIPENABLE = 1 << 5,
11-
EVERYTHINGENABLED =
12-
VDISPENABLE | HDISPENABLE | SKEWDISPENABLE | SCANLINEDISPENABLE | USERDISPENABLE | FRAMESKIPENABLE;
5+
export const VDISPENABLE = 1 << 0;
6+
export const HDISPENABLE = 1 << 1;
7+
export const SKEWDISPENABLE = 1 << 2;
8+
export const SCANLINEDISPENABLE = 1 << 3;
9+
export const USERDISPENABLE = 1 << 4;
10+
export const FRAMESKIPENABLE = 1 << 5;
11+
export const EVERYTHINGENABLED =
12+
VDISPENABLE | HDISPENABLE | SKEWDISPENABLE | SCANLINEDISPENABLE | USERDISPENABLE | FRAMESKIPENABLE;
1313

1414
////////////////////
1515
// ULA interface

‎tests/unit/test-video.js

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
2+
import { Video, HDISPENABLE, VDISPENABLE, USERDISPENABLE, EVERYTHINGENABLED } from "../../src/video.js";
3+
import * as utils from "../../src/utils.js";
4+
5+
// Setup the video with imported constants
6+
describe("Video", () => {
7+
let video;
8+
let mockCpu;
9+
let mockVia;
10+
let mockFb32;
11+
let mockPaintExt;
12+
let mockTeletext;
13+
14+
beforeEach(() => {
15+
// Reset all mocks
16+
vi.clearAllMocks();
17+
18+
// Mock frame buffer
19+
mockFb32 = new Uint32Array(1024 * 768);
20+
21+
// Mock CPU with videoRead method
22+
mockCpu = {
23+
videoRead: vi.fn().mockReturnValue(0),
24+
interrupt: 0,
25+
};
26+
27+
// Mock VIA with cb2changecallback property
28+
mockVia = {
29+
cb2changecallback: null,
30+
setVBlankInt: vi.fn(),
31+
};
32+
33+
// Mock paint_ext function
34+
mockPaintExt = vi.fn();
35+
36+
// Spy on utils.makeFast32
37+
vi.spyOn(utils, "makeFast32").mockImplementation((arr) => arr);
38+
39+
// Create a video instance (using Model B mode, not Master)
40+
video = new Video(false, mockFb32, mockPaintExt);
41+
42+
// Create the mock teletext manually and replace the one in the video object
43+
mockTeletext = {
44+
setDEW: vi.fn(),
45+
setDISPTMG: vi.fn(),
46+
setRA0: vi.fn(),
47+
fetchData: vi.fn(),
48+
render: vi.fn(),
49+
};
50+
51+
// Replace the teletext instance
52+
video.teletext = mockTeletext;
53+
54+
// Reset and connect CPU and VIA
55+
video.reset(mockCpu, mockVia);
56+
});
57+
58+
afterEach(() => {
59+
vi.restoreAllMocks();
60+
});
61+
62+
describe("ULA control register", () => {
63+
it("should set teletextMode when bit 1 is set", () => {
64+
// Initially teletext mode should be false
65+
expect(video.teletextMode).toBe(false);
66+
67+
// Write to ULA control register (address 0) with value 2 (bit 1 set)
68+
video.ula.write(0, 2);
69+
70+
// Verify teletext mode was set
71+
expect(video.teletextMode).toBe(true);
72+
73+
// Clear bit 1
74+
video.ula.write(0, 0);
75+
76+
// Verify teletext mode was cleared
77+
expect(video.teletextMode).toBe(false);
78+
});
79+
});
80+
81+
describe("Memory access in teletext mode", () => {
82+
beforeEach(() => {
83+
// Set teletext mode
84+
video.ula.write(0, 2);
85+
});
86+
87+
it("should use correct addressing for Mode 7 on Master", () => {
88+
// Set up MA13 set (addr bit 13 set) for Mode 7 addressing
89+
video.addr = 0x2000; // Bit 13 set
90+
video.isMaster = true; // Set to Master mode
91+
92+
// Set up CPU to return a specific value
93+
const expectedData = 0x7f;
94+
mockCpu.videoRead.mockReturnValue(expectedData);
95+
96+
// Call readVideoMem which should use chunky addressing mode
97+
const result = video.readVideoMem();
98+
99+
// Verify result
100+
expect(result).toBe(expectedData);
101+
102+
// Check correct address was used (should mask to 0x3ff and add 0x7c00 for Master)
103+
expect(mockCpu.videoRead).toHaveBeenCalledWith(0x7c00);
104+
});
105+
106+
it("should handle Model B quirk for reading 0x3c00", () => {
107+
// Set up addr with MA13 set but MA11 clear for Model B quirk
108+
video.addr = 0x2000; // Bit 13 set, bit 11 clear
109+
video.isMaster = false; // Set to Model B mode
110+
111+
// Call readVideoMem
112+
video.readVideoMem();
113+
114+
// For Model B, should use 0x3c00 instead of 0x7c00
115+
expect(mockCpu.videoRead).toHaveBeenCalledWith(0x3c00);
116+
});
117+
});
118+
119+
describe("Teletext integration", () => {
120+
beforeEach(() => {
121+
// Set teletext mode
122+
video.ula.write(0, 2);
123+
expect(video.teletextMode).toBe(true);
124+
});
125+
126+
it("should call teletext.setDISPTMG when display enable state changes", () => {
127+
// Clear the teletext mock history
128+
mockTeletext.setDISPTMG.mockClear();
129+
130+
// Test display enable set - all required display flags set
131+
video.dispEnabled = 0;
132+
video.dispEnableSet(HDISPENABLE | VDISPENABLE | USERDISPENABLE);
133+
134+
// The mask in dispEnableChanged is HDISPENABLE | VDISPENABLE | USERDISPENABLE
135+
// and it checks if all bits are set with (this.dispEnabled & mask) === mask
136+
expect(mockTeletext.setDISPTMG).toHaveBeenCalledWith(true);
137+
138+
// Clear the mock history
139+
mockTeletext.setDISPTMG.mockClear();
140+
141+
// Test display enable clear - clear a required flag
142+
video.dispEnableClear(HDISPENABLE);
143+
144+
// Now the mask check will fail, so setDISPTMG is called with false
145+
expect(mockTeletext.setDISPTMG).toHaveBeenCalledWith(false);
146+
});
147+
148+
it("should update teletext.setRA0 correctly based on scanlineCounter", () => {
149+
// Initialize scanlineCounter to 0
150+
video.scanlineCounter = 0;
151+
152+
// Clear the mock history
153+
mockTeletext.setRA0.mockClear();
154+
155+
// For non-interlaced mode, the RA0 value is just the lowest bit of scanlineCounter
156+
video.interlacedSyncAndVideo = false;
157+
158+
// We need to set up the registers to allow endOfScanline to work
159+
video.regs[9] = 10; // Max scanline number that triggers endOfCharacterLine
160+
161+
// Call endOfScanline to increment scanlineCounter to 1
162+
video.endOfScanline();
163+
164+
// Verify scanlineCounter was incremented
165+
expect(video.scanlineCounter).toBe(1);
166+
167+
// Verify setRA0 was called with the correct value (bit 0 is 1)
168+
expect(mockTeletext.setRA0).toHaveBeenCalledWith(true);
169+
170+
// Clear the mock history
171+
mockTeletext.setRA0.mockClear();
172+
173+
// Call endOfScanline again to increment scanlineCounter to 2
174+
video.endOfScanline();
175+
176+
// Verify scanlineCounter was incremented
177+
expect(video.scanlineCounter).toBe(2);
178+
179+
// Verify setRA0 was called with the correct value (bit 0 is 0)
180+
expect(mockTeletext.setRA0).toHaveBeenCalledWith(false);
181+
});
182+
183+
it("should handle interlaced RA0 correctly", () => {
184+
// Set up for interlaced mode
185+
video.interlacedSyncAndVideo = true;
186+
video.scanlineCounter = 0;
187+
video.frameCount = 1; // Odd frame number
188+
189+
// Initialize registers
190+
video.regs[9] = 10; // Max scanline number
191+
192+
// Clear the mock history
193+
mockTeletext.setRA0.mockClear();
194+
195+
// Call endOfScanline
196+
video.endOfScanline();
197+
198+
// In interlaced mode with odd frame count, externalScanline is scanlineCounter + 1
199+
// So even though scanlineCounter is now 2 (bit 0 = 0), externalScanline is 3 (bit 0 = 1)
200+
expect(mockTeletext.setRA0).toHaveBeenCalledWith(true);
201+
});
202+
203+
it("should call setDEW when vsync state changes", () => {
204+
// Setup necessary conditions for vsync
205+
video.regs[7] = 10; // Vertical sync position
206+
video.vertCounter = 10;
207+
video.inVSync = false;
208+
video.hadVSyncThisRow = false;
209+
video.horizCounter = 1; // Non-zero to avoid end-of-line logic
210+
211+
// Clear mock history
212+
mockTeletext.setDEW.mockClear();
213+
214+
// Triggering vsync is complex, we need to set up more state
215+
// Calling polltime with the right conditions
216+
video.polltime(1);
217+
218+
// Since we've set up the vertical counter to match R7, vsync should start
219+
expect(video.inVSync).toBe(true);
220+
221+
// Verify setDEW was called with the correct parameter
222+
expect(mockTeletext.setDEW).toHaveBeenCalledWith(true);
223+
});
224+
});
225+
226+
describe("Teletext rendering", () => {
227+
beforeEach(() => {
228+
// Set teletext mode
229+
video.ula.write(0, 2);
230+
231+
// Set up all display flags to make rendering active
232+
video.dispEnabled = EVERYTHINGENABLED;
233+
234+
// Set coords to visible area
235+
video.bitmapX = 100;
236+
video.bitmapY = 100;
237+
238+
// Set test data for video memory
239+
mockCpu.videoRead.mockReturnValue(0x42);
240+
});
241+
242+
it("should call fetchData in teletext mode", () => {
243+
// Clear mock history
244+
mockTeletext.fetchData.mockClear();
245+
246+
// Set up horizCounter to avoid vsync logic
247+
video.horizCounter = 10;
248+
249+
// Poll to trigger rendering
250+
video.polltime(1);
251+
252+
// Verify fetchData was called with the correct parameter
253+
expect(mockTeletext.fetchData).toHaveBeenCalledWith(0x42);
254+
});
255+
256+
it("should call render in teletext mode", () => {
257+
// Clear mock history
258+
mockTeletext.render.mockClear();
259+
260+
// Set up horizCounter to avoid vsync logic
261+
video.horizCounter = 10;
262+
263+
// Poll to trigger rendering
264+
video.polltime(1);
265+
266+
// Verify render was called with the expected parameters
267+
expect(mockTeletext.render).toHaveBeenCalledWith(expect.any(Uint32Array), expect.any(Number));
268+
});
269+
270+
it("should not render in non-teletext mode", () => {
271+
// Switch to non-teletext mode
272+
video.ula.write(0, 0);
273+
expect(video.teletextMode).toBe(false);
274+
275+
// Clear mock history
276+
mockTeletext.render.mockClear();
277+
278+
// Set up horizCounter to avoid vsync logic
279+
video.horizCounter = 10;
280+
281+
// Poll to trigger rendering
282+
video.polltime(1);
283+
284+
// Verify render was not called
285+
expect(mockTeletext.render).not.toHaveBeenCalled();
286+
});
287+
});
288+
});

0 commit comments

Comments
 (0)
Please sign in to comment.