-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathgif-encoder.test.js
More file actions
198 lines (170 loc) · 6.03 KB
/
Copy pathgif-encoder.test.js
File metadata and controls
198 lines (170 loc) · 6.03 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
import { describe, it, expect, vi, beforeEach } from 'vitest';
// Mock Chrome APIs
const mockChrome = {
runtime: {
lastError: null,
sendMessage: vi.fn(),
onMessage: { addListener: vi.fn() },
},
action: {
setBadgeText: vi.fn(),
setBadgeBackgroundColor: vi.fn(),
},
};
global.chrome = mockChrome;
import { GifEncoder } from './gif-encoder.js';
describe('GifEncoder', () => {
describe('initialization', () => {
it('should create an encoder with specified dimensions', () => {
const encoder = new GifEncoder(320, 240);
expect(encoder.width).toBe(320);
expect(encoder.height).toBe(240);
});
it('should default to 100ms frame delay', () => {
const encoder = new GifEncoder(320, 240);
expect(encoder.delay).toBe(100);
});
it('should accept custom frame delay', () => {
const encoder = new GifEncoder(320, 240, { delay: 50 });
expect(encoder.delay).toBe(50);
});
it('should preserve an explicit zero frame delay', () => {
const encoder = new GifEncoder(320, 240, { delay: 0 });
expect(encoder.delay).toBe(0);
});
it('should reject invalid dimensions', () => {
expect(() => new GifEncoder(0, 240)).toThrow('positive integers');
expect(() => new GifEncoder(320, -1)).toThrow('positive integers');
});
});
describe('GIF header', () => {
it('should produce valid GIF89a header bytes', () => {
const encoder = new GifEncoder(320, 240);
encoder.finish();
const output = encoder.getOutput();
// GIF89a magic bytes
expect(output[0]).toBe(0x47); // G
expect(output[1]).toBe(0x49); // I
expect(output[2]).toBe(0x46); // F
expect(output[3]).toBe(0x38); // 8
expect(output[4]).toBe(0x39); // 9
expect(output[5]).toBe(0x61); // a
});
it('should encode width and height in logical screen descriptor', () => {
const encoder = new GifEncoder(320, 240);
encoder.finish();
const output = encoder.getOutput();
// Width (little-endian) at offset 6-7
expect(output[6] | (output[7] << 8)).toBe(320);
// Height (little-endian) at offset 8-9
expect(output[8] | (output[9] << 8)).toBe(240);
});
it('should end with GIF trailer byte 0x3B', () => {
const encoder = new GifEncoder(320, 240);
encoder.finish();
const output = encoder.getOutput();
expect(output[output.length - 1]).toBe(0x3B);
});
});
describe('frame encoding', () => {
it('should add a frame from RGBA pixel data', () => {
const encoder = new GifEncoder(2, 2);
// 2x2 image, all red: RGBA
const pixels = new Uint8Array([
255, 0, 0, 255,
255, 0, 0, 255,
255, 0, 0, 255,
255, 0, 0, 255,
]);
encoder.addFrame(pixels);
encoder.finish();
const output = encoder.getOutput();
// Should have content beyond just header + trailer
expect(output.length).toBeGreaterThan(13);
// Should still have valid header
expect(output[0]).toBe(0x47);
// Should still end with trailer
expect(output[output.length - 1]).toBe(0x3B);
});
it('should reject frames with the wrong pixel buffer length', () => {
const encoder = new GifEncoder(2, 2);
expect(() => encoder.addFrame(new Uint8Array([255, 0, 0, 255]))).toThrow(
'Expected 16 RGBA values'
);
});
it('should handle multiple frames', () => {
const encoder = new GifEncoder(2, 2);
const red = new Uint8Array([
255, 0, 0, 255, 255, 0, 0, 255,
255, 0, 0, 255, 255, 0, 0, 255,
]);
const blue = new Uint8Array([
0, 0, 255, 255, 0, 0, 255, 255,
0, 0, 255, 255, 0, 0, 255, 255,
]);
encoder.addFrame(red);
encoder.addFrame(blue);
encoder.finish();
const output = encoder.getOutput();
// Multiple frames should produce larger output than single frame
const singleEncoder = new GifEncoder(2, 2);
singleEncoder.addFrame(red);
singleEncoder.finish();
const singleOutput = singleEncoder.getOutput();
expect(output.length).toBeGreaterThan(singleOutput.length);
});
it('should include Netscape looping extension for animation', () => {
const encoder = new GifEncoder(2, 2);
const pixels = new Uint8Array([
255, 0, 0, 255, 255, 0, 0, 255,
255, 0, 0, 255, 255, 0, 0, 255,
]);
encoder.addFrame(pixels);
encoder.addFrame(pixels);
encoder.finish();
const output = encoder.getOutput();
// Look for NETSCAPE2.0 application extension
const outputStr = Array.from(output.slice(0, 100))
.map(b => String.fromCharCode(b))
.join('');
expect(outputStr).toContain('NETSCAPE');
});
it('should not allow frames after finish', () => {
const encoder = new GifEncoder(2, 2);
encoder.finish();
expect(() => encoder.addFrame(new Uint8Array([
255, 0, 0, 255, 255, 0, 0, 255,
255, 0, 0, 255, 255, 0, 0, 255,
]))).toThrow('Cannot add a frame after finish');
});
});
describe('output', () => {
it('should return a Uint8Array', () => {
const encoder = new GifEncoder(2, 2);
encoder.finish();
const output = encoder.getOutput();
expect(output).toBeInstanceOf(Uint8Array);
});
it('should produce a valid Blob', () => {
const encoder = new GifEncoder(2, 2);
const pixels = new Uint8Array([
255, 0, 0, 255, 255, 0, 0, 255,
255, 0, 0, 255, 255, 0, 0, 255,
]);
encoder.addFrame(pixels);
encoder.finish();
const blob = encoder.getBlob();
expect(blob).toBeInstanceOf(Blob);
expect(blob.type).toBe('image/gif');
expect(blob.size).toBeGreaterThan(0);
});
it('should make finish idempotent', () => {
const encoder = new GifEncoder(2, 2);
encoder.finish();
const once = encoder.getOutput().length;
encoder.finish();
expect(encoder.getOutput().length).toBe(once);
expect(Array.from(encoder.getOutput()).filter(byte => byte === 0x3B)).toHaveLength(1);
});
});
});