Skip to content

Commit d52e169

Browse files
committed
make animation type check tests run on multiple keyframe indecies
1 parent 2c70b43 commit d52e169

File tree

5 files changed

+91
-52
lines changed

5 files changed

+91
-52
lines changed

internal/animate/state.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,18 @@ export const statefulAnimationGenerator = <K extends CallbackKeyframe, T>(
4242
const transition = (...keyframes: K[]) => {
4343
// Make sure frame info is valid.
4444
for (let i = 0; i < keyframes.length; i++) {
45-
typeCheck(`keyframe[${i}]`, keyframes[i], ["object"]);
45+
typeCheck(`keyframes[${i}]`, keyframes[i], ["object"]);
4646
const {delay, duration, timingFunction, callback} = keyframes[i];
47-
typeCheck(`keyframe[${i}].delay`, delay, ["number", "undefined"]);
48-
if (delay && delay < 0) error(`keyframe[${i}].delay is invalid "${delay}".`);
49-
typeCheck(`keyframe[${i}].duration`, duration, ["number"]);
47+
typeCheck(`keyframes[${i}].delay`, delay, ["number", "undefined"]);
48+
if (delay && delay < 0) error(`keyframes[${i}].delay is invalid "${delay}".`);
49+
typeCheck(`keyframes[${i}].duration`, duration, ["number"]);
5050
if (duration && duration < 0)
51-
error(`keyframe[${i}].duration is invalid "${duration}".`);
52-
typeCheck(`keyframe[${i}].timingFunction`, timingFunction, ["string", "undefined"]);
51+
error(`keyframes[${i}].duration is invalid "${duration}".`);
52+
typeCheck(`keyframes[${i}].timingFunction`, timingFunction, ["string", "undefined"]);
5353
if (timingFunctions[timingFunction || "linear"] === undefined) {
54-
error(`"keyframe[${i}].timingFunction" is not recognized "${timingFunction}".`);
54+
error(`"keyframes[${i}].timingFunction" is not recognized "${timingFunction}".`);
5555
}
56-
typeCheck(`keyframe[${i}].callback`, callback, ["function", "undefined"]);
56+
typeCheck(`keyframes[${i}].callback`, callback, ["function", "undefined"]);
5757
checker(keyframes[i], i);
5858
}
5959

internal/errors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export const error = (message: string) => {
55
export const typeCheck = (name: string, val: any, expected: string[]) => {
66
let actual: string = typeof val;
77
if (actual === "number" && isNaN(val)) actual = "NaN";
8+
if (actual === "object" && val === null) actual = "null";
89
if (!expected.includes(actual)) {
910
error(`"${name}" should have type "${expected.join("|")}" but was "${actual}".`);
1011
}

public/animate.test.ts

Lines changed: 78 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -21,153 +21,191 @@ describe("animate", () => {
2121
describe("canvasPath", () => {
2222
describe("transition", () => {
2323
describe("keyframe", () => {
24-
// TODO test for errors in the nth keyframe.
2524
it("should accept minimal generated keyframe", () => {
2625
const animation = canvasPath();
2726
const keyframe = genKeyframe();
2827

2928
expect(() => animation.transition(keyframe)).not.toThrow();
3029
});
3130

31+
it("should indicate the rejected frame index", () => {
32+
const animation = canvasPath();
33+
const keyframes = [genKeyframe(), null as any, genKeyframe()];
34+
35+
expect(() => animation.transition(...keyframes)).toThrow(/keyframes.*1/g);
36+
});
37+
3238
interface TestCase {
33-
test: string;
39+
name: string;
3440
edit: (keyframe: CanvasKeyframe) => void;
3541
error?: RegExp;
3642
}
3743

3844
const testCases: Array<TestCase> = [
3945
{
40-
test: "should accept valid duration",
46+
name: "should accept valid duration",
4147
edit: (keyframe) => (keyframe.duration = 100),
4248
},
4349
{
44-
test: "should accept zero duration",
50+
name: "should accept zero duration",
4551
edit: (keyframe) => (keyframe.duration = 0),
4652
},
4753
{
48-
test: "should reject undefined duration",
54+
name: "should reject undefined duration",
4955
edit: (keyframe) => delete keyframe.duration,
5056
error: /duration.*number.*undefined/g,
5157
},
5258
{
53-
test: "should reject negative duration",
59+
name: "should reject negative duration",
5460
edit: (keyframe) => (keyframe.duration = -10),
5561
error: /duration.*invalid/g,
5662
},
5763
{
58-
test: "should reject broken duration",
64+
name: "should reject broken duration",
5965
edit: (keyframe) => (keyframe.duration = NaN),
6066
error: /duration.*number.*NaN/g,
6167
},
6268
{
63-
test: "should reject invalid duration",
69+
name: "should reject invalid duration",
6470
edit: (keyframe) => (keyframe.duration = "123" as any),
6571
error: /duration.*number.*string/g,
6672
},
6773
{
68-
test: "should accept valid delay",
74+
name: "should accept valid delay",
6975
edit: (keyframe) => (keyframe.delay = 200),
7076
},
7177
{
72-
test: "should accept zero delay",
78+
name: "should accept zero delay",
7379
edit: (keyframe) => (keyframe.delay = 0),
7480
},
7581
{
76-
test: "should accept undefined delay",
82+
name: "should accept undefined delay",
7783
edit: (keyframe) => delete keyframe.delay,
7884
},
7985
{
80-
test: "should reject negative delay",
86+
name: "should reject negative delay",
8187
edit: (keyframe) => (keyframe.delay = -10),
8288
error: /delay.*invalid/g,
8389
},
8490
{
85-
test: "should reject broken delay",
91+
name: "should reject broken delay",
8692
edit: (keyframe) => (keyframe.delay = NaN),
8793
error: /delay.*number.*NaN/g,
8894
},
8995
{
90-
test: "should reject invalid delay",
96+
name: "should reject invalid delay",
9197
edit: (keyframe) => (keyframe.delay = "123" as any),
9298
error: /delay.*number.*string/g,
9399
},
94100
{
95-
test: "should accept known timingFunction",
101+
name: "should accept known timingFunction",
96102
edit: (keyframe) => (keyframe.timingFunction = "ease"),
97103
},
98104
{
99-
test: "should accept undefined timingFunction",
105+
name: "should accept undefined timingFunction",
100106
edit: (keyframe) => delete keyframe.timingFunction,
101107
},
102108
{
103-
test: "should reject invalid timingFunction",
109+
name: "should reject invalid timingFunction",
104110
edit: (keyframe) => (keyframe.timingFunction = (() => 0) as any),
105111
error: /timingFunction.*string.*function/g,
106112
},
107113
{
108-
test: "should reject unknown timingFunction",
114+
name: "should reject unknown timingFunction",
109115
edit: (keyframe) => (keyframe.timingFunction = "unknown" as any),
110116
error: /timingFunction.*not recognized.*unknown/g,
111117
},
112118
{
113-
test: "should accept valid callback",
119+
name: "should accept valid callback",
114120
edit: (keyframe) => (keyframe.callback = () => console.log("test")),
115121
},
116122
{
117-
test: "should accept undefined callback",
123+
name: "should accept undefined callback",
118124
edit: (keyframe) => delete keyframe.callback,
119125
},
120126
{
121-
test: "should reject invalid callback",
127+
name: "should reject invalid callback",
122128
edit: (keyframe) => (keyframe.callback = {} as any),
123129
error: /callback.*function.*object/g,
124130
},
125131
// TODO complete blobOptions type tests, should be the same as non-animated.
126132
{
127-
test: "should reject undefined blobOptions",
133+
name: "should reject undefined blobOptions",
128134
edit: (keyframe) => delete keyframe.blobOptions,
129135
error: /blobOptions.*object.*undefined/g,
130136
},
131137
{
132-
test: "should accept empty canvasOptions",
138+
name: "should accept empty canvasOptions",
133139
edit: (keyframe) => (keyframe.canvasOptions = {}),
134140
},
135141
{
136-
test: "should accept undefined canvasOptions",
142+
name: "should accept undefined canvasOptions",
137143
edit: (keyframe) => delete keyframe.canvasOptions,
138144
},
139145
{
140-
test: "should accept undefined canvasOptions offsetX",
146+
name: "should reject invalid canvasOptions",
147+
edit: (keyframe) => keyframe.canvasOptions = null as any,
148+
error: /canvasOptions.*object.*null/g
149+
},
150+
{
151+
name: "should accept undefined canvasOptions offsetX",
141152
edit: (keyframe) => delete keyframe.canvasOptions?.offsetX,
142153
},
143154
{
144-
test: "should reject broken canvasOptions offsetX",
155+
name: "should reject broken canvasOptions offsetX",
145156
edit: (keyframe) => (keyframe.canvasOptions = {offsetX: NaN}),
146157
error: /canvasOptions.*offsetX.*number.*NaN/g,
147158
},
148159
{
149-
test: "should accept undefined canvasOptions offsetY",
160+
name: "should accept undefined canvasOptions offsetY",
150161
edit: (keyframe) => delete keyframe.canvasOptions?.offsetY,
151162
},
152163
{
153-
test: "should reject broken canvasOptions offsetY",
164+
name: "should reject broken canvasOptions offsetY",
154165
edit: (keyframe) => (keyframe.canvasOptions = {offsetY: NaN}),
155166
error: /canvasOptions.*offsetY.*number.*NaN/g,
156167
},
157168
];
158169

159-
for (const testCase of testCases) {
160-
it(testCase.test, () => {
161-
const animation = canvasPath();
162-
const keyframe = genKeyframe();
163-
testCase.edit(keyframe);
164-
if (testCase.error) {
165-
expect(() => animation.transition(keyframe)).toThrow(testCase.error);
166-
} else {
167-
expect(() => animation.transition(keyframe)).not.toThrow();
168-
}
169-
});
170-
}
170+
// Run all test cases with a configurable amount of keyframes
171+
// and index of the keyframe being edited for the tests.
172+
const runSuite = (keyframeCount: number, editIndex: number) => {
173+
for (const testCase of testCases) {
174+
it(testCase.name, () => {
175+
// Create blank animation.
176+
const animation = canvasPath();
177+
178+
// Create keyframes to call transition with.
179+
const keyframes: CanvasKeyframe[] = [];
180+
for (let i = 0; i < keyframeCount; i++) {
181+
keyframes.push(genKeyframe());
182+
}
183+
184+
// Modify selected keyframe.
185+
testCase.edit(keyframes[editIndex]);
186+
187+
if (testCase.error) {
188+
// Copy regexp because they are stateful.
189+
const pattern = new RegExp(testCase.error);
190+
expect(() => animation.transition(...keyframes)).toThrow(pattern);
191+
} else {
192+
expect(() => animation.transition(...keyframes)).not.toThrow();
193+
}
194+
});
195+
}
196+
};
197+
198+
// Run all cases when given a single test frame and asserting on it.
199+
describe("first", () => runSuite(1, 0));
200+
201+
// Run all cases when given more than one frame, asserting on last one.
202+
const lastLength = 2 + Math.floor(4 * Math.random());
203+
describe("last", () => runSuite(lastLength, lastLength - 1));
204+
205+
// Run all cases when given more than one frame, asserting on a random one.
206+
const nthLength = 2 + Math.floor(16 * Math.random());
207+
const nthIndex = Math.floor(nthLength * Math.random());
208+
describe("nth", () => runSuite(nthLength, nthIndex));
171209
});
172210
});
173211
});

public/animate.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,11 @@ const canvasBlobGenerator = (keyframe: CanvasKeyframe): Point[] => {
4040

4141
// Only need to check properties unique to the canvas animation generator.
4242
const canvasKeyframeChecker = (keyframe: CanvasKeyframe, index: number) => {
43-
typeCheck(`keyframe[${index}].canvasOptions`, keyframe.canvasOptions, ["object", "undefined"]);
43+
typeCheck(`keyframes[${index}].canvasOptions`, keyframe.canvasOptions, ["object", "undefined"]);
4444
if (keyframe.canvasOptions) {
4545
const {offsetX, offsetY} = keyframe.canvasOptions;
46-
typeCheck(`keyframe[${index}].canvasOptions.offsetX`, offsetX, ["number", "undefined"]);
47-
typeCheck(`keyframe[${index}].canvasOptions.offsetY`, offsetY, ["number", "undefined"]);
46+
typeCheck(`keyframes[${index}].canvasOptions.offsetX`, offsetX, ["number", "undefined"]);
47+
typeCheck(`keyframes[${index}].canvasOptions.offsetY`, offsetY, ["number", "undefined"]);
4848
}
4949
};
5050

public/legacy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ blobs.editable = (options: BlobOptions): XmlElement => {
5252
}
5353

5454
// Random number generator.
55-
const rgen = rand(options.seed || String(Date.now()));
55+
const rgen = rand(options.seed || String(Math.random()));
5656

5757
if (!options.size) {
5858
throw new Error("no size specified");

0 commit comments

Comments
 (0)