• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

visgl / luma.gl / 23355601514

20 Mar 2026 05:50PM UTC coverage: 52.166% (-25.8%) from 77.934%
23355601514

push

github

web-flow
chore: Migrate to vitest (#2554)

4188 of 11561 branches covered (36.23%)

Branch coverage included in aggregate %.

613 of 632 new or added lines in 22 files covered. (96.99%)

343 existing lines in 28 files now uncovered.

7757 of 11337 relevant lines covered (68.42%)

394.31 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

4.05
/modules/test-utils/src/test-runner.ts
1
// luma.gl
2
// SPDX-License-Identifier: MIT
3
// Copyright (c) vis.gl contributors
4

5
// @ts-nocheck
6
/* eslint-disable */
7

8
import {AnimationProps} from '@luma.gl/engine';
9
import {getWebGLTestDevice} from './create-test-device';
10
import {createTestDevice} from './deprecated/sync-test-device';
11

12
// TODO - Replace with new AnimationLoop from `@luma.gl/engine`
13
import {ClassicAnimationLoop as AnimationLoop} from './deprecated/classic-animation-loop';
14

15
/** Describes a test case */
16
export type TestRunnerTestCase = {
17
  /** Name of the test case (for logging) */
18
  name: string;
19
  /** Initialize the test case. Can return a promise */
20
  onInitialize: (props: AnimationProps) => Promise<void | {}>;
21
  /** Perform rendering */
22
  onRender: (props: AnimationProps & {done}) => void;
23
  /** Clean up after the test case */
24
  onFinalize: (props: AnimationProps) => void;
25
  animationLoop?: AnimationLoop;
26
};
27

28
const DEFAULT_TEST_CASE: TestRunnerTestCase = {
55✔
29
  name: 'Unnamed test',
30
  onInitialize: async () => {},
31
  onRender: ({done}) => done(),
54✔
32
  onFinalize: () => {}
33
};
34

35
/** Options for a TestRunner */
36
export type TestRunnerProps = {
37
  width?: number;
38
  height?: number;
39
  // test lifecycle callback
40
  onTestStart?: (testCase: TestRunnerTestCase) => void;
41
  onTestPass?: (testCase: TestRunnerTestCase, result?: unknown) => void;
42
  onTestFail?: (testCase: TestRunnerTestCase, error?: unknown) => void;
43
  /** milliseconds to wait for each test case before aborting */
44
  timeout?: number;
45
  maxFramesToRender?: number;
46
  // HACK - this is for the snapshot test runner
47
  imageDiffOptions?: any;
48
};
49

50
const DEFAULT_TEST_PROPS: Required<TestRunnerProps> = {
55✔
51
  width: undefined,
52
  height: undefined,
53

54
  // test lifecycle callback
55
  onTestStart: (testCase: TestRunnerTestCase) => console.log(`# ${testCase.name}`),
54✔
56
  onTestPass: (testCase: TestRunnerTestCase, result?: unknown) =>
57
    console.log(`ok ${testCase.name} passed`),
54✔
58
  onTestFail: (testCase: TestRunnerTestCase, error?: unknown) =>
59
    console.log(`not ok ${testCase.name} failed`),
54✔
60

61
  // milliseconds to wait for each test case before aborting
62
  timeout: 2000,
63
  maxFramesToRender: undefined,
64
  imageDiffOptions: undefined
65
};
66

67
/** Runs an array of test cases */
68
export class TestRunner {
NEW
69
  device = createTestDevice();
×
70
  props: Record<string, any>;
71
  isRunning: boolean = false;
×
72
  testOptions: Required<TestRunnerProps> = {...DEFAULT_TEST_PROPS};
×
73
  readonly _animationProps?: AnimationProps;
74
  private _animationLoop: AnimationLoop | null;
75
  private _testCases: TestRunnerTestCase[] = [];
×
76
  private _testCaseData: any = null;
×
77
  private _currentTestCase: TestRunnerTestCase | null;
78
  private _currentTestCaseStartTime: number;
79
  private _currentTestCaseStartTick: number;
80

81
  // should be defined in snapshot-test-runner
82
  isDiffing: boolean = false;
×
83

84
  // @ts-expect-error
85
  isHeadless: boolean = Boolean(window.browserTestDriver_isHeadless);
×
86

87
  /**
88
   * props
89
   *   AnimationLoop props
90
   */
91
  constructor(props: Record<string, any> = {}) {
×
92
    this.props = props;
×
93
  }
94

95
  /**
96
   * Add testCase(s)
97
   */
98
  add(testCases: TestRunnerTestCase[]): this {
99
    if (!Array.isArray(testCases)) {
×
100
      testCases = [testCases];
×
101
    }
102
    for (const testCase of testCases) {
×
103
      this._testCases.push(testCase);
×
104
    }
105
    return this;
×
106
  }
107

108
  /**
109
   * Returns a promise that resolves when all the test cases are done
110
   */
111
  async run(options: object = {}): Promise<void> {
×
112
    this.testOptions = {...this.testOptions, ...options};
×
113

114
    const device = await getWebGLTestDevice();
×
115

116
    return new Promise<void>((resolve, reject) => {
×
117
      this._animationLoop = new AnimationLoop({
×
118
        ...this.props,
119
        device: this.device,
120
        onRender: this._onRender.bind(this),
121
        onFinalize: () => {
122
          this.isRunning = false;
×
123
          resolve();
×
124
        }
125
      });
126
      this._animationLoop.start(this.props);
×
127

128
      this.isRunning = true;
×
129
      this.isDiffing = false;
×
130
      this._currentTestCase = null;
×
131
    }).catch(error => {
132
      this._fail({error: error.message});
×
133
      // reject(error);
134
    });
135
  }
136

137
  /* Lifecycle methods for subclassing */
138

139
  initTestCase(testCase: TestRunnerTestCase) {
140
    const {animationLoop} = testCase;
×
141
    if (animationLoop) {
×
142
      testCase.onInitialize = animationLoop.props.onInitialize.bind(animationLoop);
×
143
      testCase.onRender = animationLoop.props.onRender.bind(animationLoop);
×
144
      testCase.onFinalize = animationLoop.props.onFinalize.bind(animationLoop);
×
145
    }
146
    for (const key in DEFAULT_TEST_CASE) {
×
147
      testCase[key] = testCase[key] || DEFAULT_TEST_CASE[key];
×
148
    }
149
  }
150

151
  shouldRender(animationProps): boolean {
152
    return true;
×
153
  }
154

155
  assert(testCase: TestRunnerTestCase): void {
156
    this._pass(testCase);
×
157
    this._next();
×
158
  }
159

160
  /* Utilities */
161

162
  _pass(result: unknown): void {
163
    this.testOptions.onTestPass(this._currentTestCase, result);
×
164
    // this._animationLoop?.stop();
165
  }
166

167
  _fail(result: unknown): void {
168
    this.testOptions.onTestFail(this._currentTestCase, result);
×
169
    // this._animationLoop?.stop();
170
  }
171

172
  _next(): void {
173
    this._nextTestCase();
×
174
  }
175

176
  /* Private methods */
177

178
  _onRender(animationProps): void {
179
    this._animationProps = animationProps;
×
180

181
    const testCase = this._currentTestCase || this._nextTestCase();
×
182
    if (!testCase) {
×
183
      // all test cases are done
184
      this._animationLoop.stop();
×
185
      return;
×
186
    }
187

188
    let isDone = false;
×
189
    const testCaseAnimationProps: AnimationProps = {
×
190
      ...animationProps,
191
      ...this._testCaseData,
192
      // tick/time starts from 0 for each test case
193
      startTime: this._currentTestCaseStartTime,
194
      time: animationProps.time - this._currentTestCaseStartTime,
195
      tick: animationProps.tick - this._currentTestCaseStartTick,
196
      // called by the test case when it is done rendering and ready for capture and diff
197
      done: () => {
198
        isDone = true;
×
199
      }
200
    };
201

202
    if (this._testCaseData && this.shouldRender(testCaseAnimationProps)) {
×
203
      // try {
204
      // test case is initialized, render frame
205
      testCase.onRender(testCaseAnimationProps);
×
206
      // } catch {
207
      //   isDone = true;
208
      // }
209
    }
210

211
    const timeout = testCase.timeout || this.testOptions.timeout;
×
212
    if (timeout && testCaseAnimationProps.time > timeout) {
×
213
      isDone = true;
×
214
    }
215

216
    if (isDone) {
×
217
      this.assert(testCase);
×
218
    }
219
  }
220

221
  _nextTestCase(): Promise<TestRunnerTestCase> {
222
    const animationProps = this._animationProps;
×
223

224
    // finalize the current test case
225
    if (this._testCaseData) {
×
226
      for (const key in this._testCaseData) {
×
227
        const value = this._testCaseData[key];
×
228
        if (value && value.delete) {
×
229
          value.destroy();
×
230
        }
231
      }
232
      this._currentTestCase.onFinalize(Object.assign({}, animationProps, this._testCaseData));
×
233

234
      // reset WebGL context
235
      this.device.popState();
×
236

237
      this._currentTestCase = null;
×
238
      this._testCaseData = null;
×
239
    }
240

241
    // get the next test case
242
    const testCase = this._testCases.shift();
×
243
    if (testCase) {
×
244
      // start new test case
245
      this._currentTestCase = testCase;
×
246
      this._currentTestCaseStartTime = animationProps.time;
×
247
      this._currentTestCaseStartTick = animationProps.tick;
×
248
      this.initTestCase(testCase);
×
249

250
      // initialize test case
251

252
      // save WebGL context
253
      this.device.pushState();
×
254

255
      // aligned with the behavior of AnimationLoop.onInitialized
256
      // onInitialized could return a plain object or a promise
257
      const initProps = {
×
258
        ...animationProps,
259
        // tick/time starts from 0 for each test case
260
        startTime: animationProps.time,
261
        time: 0,
262
        tick: 0
263
      };
264

265
      // aligned with the behavior of AnimationLoop.onInitialized
266
      // onInitialized could return a plain object or a promise
267
      Promise.resolve(testCase.onInitialize(initProps)).then(userData => {
×
268
        this._testCaseData = userData || {};
×
269
      });
270

271
      // invoke user callback
272
      this.testOptions.onTestStart(testCase);
×
273
    }
274
    return testCase;
×
275
  }
276
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc