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

jest-community / vscode-jest / 9394770034

06 Jun 2024 03:41AM UTC coverage: 98.013% (-0.06%) from 98.069%
9394770034

Pull #1153

github

web-flow
Merge a1b766982 into 6a3a7e7c6
Pull Request #1153: Support Jest v30

2261 of 2379 branches covered (95.04%)

Branch coverage included in aggregate %.

19 of 20 new or added lines in 3 files covered. (95.0%)

3 existing lines in 1 file now uncovered.

4005 of 4014 relevant lines covered (99.78%)

52.31 hits per line

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

96.79
/src/test-provider/test-item-data.ts
1
import * as vscode from 'vscode';
4✔
2
import { extensionId } from '../appGlobals';
4✔
3
import { JestRunEvent, RunEventBase } from '../JestExt';
4
import { TestSuiteResult } from '../TestResults';
5
import * as path from 'path';
4✔
6
import { JestExtRequestType } from '../JestExt/process-session';
7
import { ItBlock, TestAssertionStatus } from 'jest-editor-support';
8
import { ContainerNode, DataNode, NodeType, ROOT_NODE_NAME } from '../TestResults/match-node';
4✔
9
import { Logging } from '../logging';
10
import { TestSuitChangeEvent } from '../TestResults/test-result-events';
11
import { Debuggable, ItemCommand, TestItemData } from './types';
4✔
12
import { JestTestProviderContext } from './test-provider-context';
13
import { JestTestRun } from './jest-test-run';
14
import { JestProcessInfo, ProcessStatus } from '../JestProcessManagement';
4✔
15
import { GENERIC_ERROR, LONG_RUNNING_TESTS, getExitErrorDef } from '../errors';
4✔
16
import { tiContextManager } from './test-item-context-manager';
4✔
17
import { runModeDescription } from '../JestExt/run-mode';
4✔
18
import { isVirtualWorkspaceFolder } from '../virtual-workspace-folder';
4✔
19
import { outputManager } from '../output-manager';
4✔
20
import { TestNamePattern } from '../types';
21

22
interface JestRunnable {
23
  getJestRunRequest: () => JestExtRequestType;
24
}
25

26
interface WithUri {
27
  uri: vscode.Uri;
28
}
29

30
type TypedRunEvent = RunEventBase & { type: string };
31

32
abstract class TestItemDataBase implements TestItemData, JestRunnable, WithUri {
33
  item!: vscode.TestItem;
34
  log: Logging;
35

36
  constructor(
37
    public context: JestTestProviderContext,
792✔
38
    name: string
39
  ) {
40
    this.log = context.ext.loggingFactory.create(name);
792✔
41
  }
42
  get uri(): vscode.Uri {
43
    return this.item.uri!;
447✔
44
  }
45

46
  deepItemState(
47
    item: vscode.TestItem | undefined,
48
    setState: (item: vscode.TestItem) => void
49
  ): void {
50
    if (!item) {
600✔
51
      this.log('warn', '<deepItemState>: no item to set state');
4✔
52
      return;
4✔
53
    }
54
    setState(item);
596✔
55
    item.children.forEach((child) => this.deepItemState(child, setState));
596✔
56
  }
57

58
  isTestNameResolved() {
59
    return true;
54✔
60
  }
61

62
  scheduleTest(run: JestTestRun, itemCommand?: ItemCommand): void {
63
    if (!this.isTestNameResolved()) {
77✔
64
      const parent = this.item.parent && this.context.getData(this.item.parent);
3✔
65
      if (parent) {
3✔
66
        run.end({ reason: 'unresolved parameterized test' });
2✔
67
        run.updateRequest(new vscode.TestRunRequest([parent.item]));
2✔
68
        return parent.scheduleTest(run, itemCommand);
2✔
69
      }
70
      this.context.output.write(`running an unresolved parameterized test might fail`, 'warn');
1✔
71
    }
72

73
    const jestRequest = this.getJestRunRequest(itemCommand);
75✔
74

75
    this.deepItemState(this.item, run.enqueued);
75✔
76

77
    const process = this.context.ext.session.scheduleProcess(jestRequest, {
75✔
78
      run,
79
      testItem: this.item,
80
    });
81
    if (!process) {
75✔
82
      const msg = `failed to schedule test for ${this.item.id}`;
5✔
83
      run.errored(this.item, new vscode.TestMessage(msg));
5✔
84
      run.write(msg, 'error');
5✔
85
      run.end({ reason: 'failed to schedule test' });
5✔
86
    } else {
87
      run.addProcess(process);
70✔
88
    }
89
  }
90

91
  runItemCommand(command: ItemCommand): void | Promise<void> {
92
    switch (command) {
12✔
93
      case ItemCommand.updateSnapshot: {
94
        const request = new vscode.TestRunRequest([this.item]);
4✔
95
        const run = this.context.createTestRun(request, {
4✔
96
          name: `${command}-${this.item.id}`,
97
        });
98
        this.scheduleTest(run, command);
4✔
99
        break;
4✔
100
      }
101
      case ItemCommand.viewSnapshot: {
102
        return this.viewSnapshot().catch((e) => this.log('error', e));
7✔
103
      }
104
      case ItemCommand.revealOutput: {
105
        return this.context.output.show();
1✔
106
      }
107
    }
108
  }
109
  viewSnapshot(): Promise<void> {
110
    return Promise.reject(`viewSnapshot is not supported for ${this.item.id}`);
3✔
111
  }
112
  abstract getJestRunRequest(itemCommand?: ItemCommand): JestExtRequestType;
113
}
114

115
interface SnapshotItemCollection {
116
  viewable: vscode.TestItem[];
117
  updatable: vscode.TestItem[];
118
}
119

120
/**
121
 * Goal of this class is to manage the TestItem hierarchy reflects DocumentRoot path. It is responsible
122
 * to create DocumentRoot for each test file by listening to the TestResultEvents.
123
 */
124
export class WorkspaceRoot extends TestItemDataBase {
4✔
125
  private testDocuments: Map<string, TestDocumentRoot>;
126
  private listeners: vscode.Disposable[];
127

128
  constructor(context: JestTestProviderContext) {
129
    super(context, 'WorkspaceRoot');
192✔
130
    this.item = this.createTestItem();
192✔
131
    this.testDocuments = new Map();
192✔
132
    this.listeners = [];
192✔
133

134
    this.registerEvents();
192✔
135
  }
136
  createTestItem(): vscode.TestItem {
137
    const workspaceFolder = this.context.ext.workspace;
194✔
138
    const item = this.context.createTestItem(
194✔
139
      `${extensionId}:${workspaceFolder.name}`,
140
      workspaceFolder.name,
141
      isVirtualWorkspaceFolder(workspaceFolder)
194✔
142
        ? workspaceFolder.effectiveUri
143
        : workspaceFolder.uri,
144
      this,
145
      undefined,
146
      ['run']
147
    );
148
    const desc = runModeDescription(this.context.ext.settings.runMode.config);
194✔
149
    item.description = `(${desc.deferred?.label ?? desc.type.label})`;
194!
150

151
    item.canResolveChildren = true;
194✔
152
    return item;
194✔
153
  }
154

155
  getJestRunRequest(itemCommand?: ItemCommand): JestExtRequestType {
156
    const updateSnapshot = itemCommand === ItemCommand.updateSnapshot;
17✔
157
    return { type: 'all-tests', nonBlocking: true, updateSnapshot };
17✔
158
  }
159
  discoverTest(run: JestTestRun): void {
160
    const testList = this.context.ext.testResultProvider.getTestList();
16✔
161
    // only trigger update when testList is not empty because it's possible test-list is not available yet,
162
    // in such case we should just wait for the testListUpdated event to trigger the update
163
    if (testList.length > 0) {
16✔
164
      this.onTestListUpdated(testList, run);
12✔
165
    } else {
166
      run.end({ reason: 'no test found' });
4✔
167
      this.item.canResolveChildren = false;
4✔
168
    }
169
  }
170

171
  // test result event handling
172
  private registerEvents = (): void => {
192✔
173
    this.listeners = [
192✔
174
      this.context.ext.testResultProvider.events.testListUpdated.event(this.onTestListUpdated),
175
      this.context.ext.testResultProvider.events.testSuiteChanged.event(this.onTestSuiteChanged),
176
      this.context.ext.sessionEvents.onRunEvent.event(this.onRunEvent),
177
    ];
178
  };
179
  private unregisterEvents = (): void => {
192✔
180
    this.listeners.forEach((l) => l.dispose());
6✔
181
    this.listeners.length = 0;
2✔
182
  };
183

184
  private createRun = (name: string, testItem?: vscode.TestItem): JestTestRun => {
192✔
185
    const item = testItem ?? this.item;
247✔
186
    const request = new vscode.TestRunRequest([item]);
247✔
187
    return this.context.createTestRun(request, {
247✔
188
      name,
189
    });
190
  };
191

192
  private addFolder = (parent: FolderData | undefined, folderName: string): FolderData => {
192✔
193
    const p = parent ?? this;
191✔
194
    const uri = FolderData.makeUri(p.item, folderName);
191✔
195
    return (
191✔
196
      this.context.getChildData<FolderData>(p.item, uri.fsPath) ??
573✔
197
      new FolderData(this.context, folderName, p.item)
198
    );
199
  };
200
  private addPath = (absoluteFileName: string): FolderData | undefined => {
192✔
201
    const relativePath = path.relative(this.item.uri!.fsPath, absoluteFileName);
199✔
202
    const folders = relativePath.split(path.sep).slice(0, -1);
199✔
203

204
    return folders.reduce(this.addFolder, undefined);
199✔
205
  };
206
  /**
207
   * create a test item hierarchy for the given the test file based on its relative path. If the file is not
208
   * a test file, exception will be thrown.
209
   */
210
  private addTestFile = (
192✔
211
    absoluteFileName: string,
212
    onTestDocument: (doc: TestDocumentRoot) => void
213
  ): TestDocumentRoot => {
214
    const parent = this.addPath(absoluteFileName) ?? this;
199✔
215
    let docRoot = this.context.getChildData<TestDocumentRoot>(parent.item, absoluteFileName);
199✔
216
    if (!docRoot) {
199✔
217
      docRoot = this.testDocuments.get(absoluteFileName);
187✔
218
      if (docRoot) {
187✔
219
        parent.item.children.add(docRoot.item);
9✔
220
      } else {
221
        docRoot = new TestDocumentRoot(
178✔
222
          this.context,
223
          vscode.Uri.file(absoluteFileName),
224
          parent.item
225
        );
226
      }
227
    }
228
    this.testDocuments.set(absoluteFileName, docRoot);
199✔
229

230
    onTestDocument(docRoot);
199✔
231

232
    return docRoot;
199✔
233
  };
234

235
  /**
236
   * When test list updated, rebuild the whole testItem tree for all the test files (DocumentRoot).
237
   * But will reuse the existing test document from cache (testDocuments) to preserve info
238
   * such as parsed result that could be stored before test list update
239
   */
240
  private onTestListUpdated = (
192✔
241
    absoluteFileNames: string[] | undefined,
242
    run?: JestTestRun
243
  ): void => {
244
    this.item.children.replace([]);
16✔
245
    const testRoots: TestDocumentRoot[] = [];
16✔
246

247
    const aRun = run ?? this.createRun('onTestListUpdated');
16✔
248
    absoluteFileNames?.forEach((f) =>
16!
249
      this.addTestFile(f, (testRoot) => {
35✔
250
        testRoot.updateResultState(aRun);
35✔
251
        testRoots.push(testRoot);
35✔
252
      })
253
    );
254
    //sync testDocuments
255
    this.testDocuments.clear();
16✔
256
    testRoots.forEach((t) => this.testDocuments.set(t.item.id, t));
35✔
257
    aRun.end({ reason: 'onTestListUpdated' });
16✔
258
    this.item.canResolveChildren = false;
16✔
259
  };
260

261
  // prevent a jest non-watch mode runs failed to stop, which could block the process queue from running other tests.
262
  // by default it will wait 10 seconds before killing the process
263
  private preventZombieProcess = (process: JestProcessInfo, delay = 10000): void => {
192✔
264
    if (process.status === ProcessStatus.Running && !process.isWatchMode) {
167✔
265
      process.autoStop(delay, () => {
1✔
266
        this.context.output.write(
1✔
267
          `Zombie jest process "${process.id}" is killed. Please investigate the root cause or file an issue.`,
268
          'warn'
269
        );
270
      });
271
    }
272
  };
273

274
  /**
275
   * invoked when external test result changed, this could be caused by the watch-mode or on-demand test run, includes vscode's runTest.
276
   * We will use either existing run or creating a new one if none exist yet,
277
   * and ask all touched DocumentRoot to refresh both the test items and their states.
278
   *
279
   * @param event
280
   */
281
  private onTestSuiteChanged = (event: TestSuitChangeEvent): void => {
192✔
282
    switch (event.type) {
170✔
283
      case 'assertions-updated': {
284
        const run = this.getJestRun(event, true);
167✔
285

286
        this.log(
167✔
287
          'debug',
288
          `update status from run "${event.process.id}": ${event.files.length} files`
289
        );
290
        if (event.files.length === 0) {
167✔
291
          run.write(`No tests were run.`, `new-line`);
6✔
292
        } else {
293
          event.files.forEach((f) => this.addTestFile(f, (testRoot) => testRoot.discoverTest(run)));
161✔
294
        }
295
        run.end({ process: event.process, delay: 1000, reason: 'assertions-updated' });
167✔
296
        this.preventZombieProcess(event.process);
167✔
297

298
        break;
167✔
299
      }
300
      case 'result-matched': {
301
        const snapshotItems: SnapshotItemCollection = {
1✔
302
          viewable: [],
303
          updatable: [],
304
        };
305
        this.addTestFile(event.file, (testRoot) => {
1✔
306
          testRoot.onTestMatched();
1✔
307
          testRoot.gatherSnapshotItems(snapshotItems);
1✔
308
        });
309
        this.updateSnapshotContext(snapshotItems);
1✔
310
        break;
1✔
311
      }
312

313
      case 'result-match-failed': {
314
        const snapshotItems: SnapshotItemCollection = {
2✔
315
          viewable: [],
316
          updatable: [],
317
        };
318
        this.addTestFile(event.file, (testRoot) => {
2✔
319
          testRoot.discoverTest(undefined, event.sourceContainer);
2✔
320
          testRoot.gatherSnapshotItems(snapshotItems);
2✔
321
        });
322
        this.updateSnapshotContext(snapshotItems);
2✔
323
        break;
2✔
324
      }
325
    }
326
  };
327
  private updateSnapshotContext(snapshotItems: SnapshotItemCollection): void {
328
    tiContextManager.setItemContext({
3✔
329
      workspace: this.context.ext.workspace,
330
      key: 'jest.editor-view-snapshot',
331
      itemIds: snapshotItems.viewable.map((item) => item.id),
1✔
332
    });
333
    const getAllIds = (item: vscode.TestItem, allIds: Set<string>): void => {
3✔
334
      if (allIds.has(item.id)) {
9✔
335
        return;
1✔
336
      }
337
      allIds.add(item.id);
8✔
338
      if (item.parent) {
8✔
339
        getAllIds(item.parent, allIds);
6✔
340
      }
341
    };
342
    const allIds = new Set<string>();
3✔
343
    snapshotItems.updatable.forEach((item) => getAllIds(item, allIds));
3✔
344
    tiContextManager.setItemContext({
3✔
345
      workspace: this.context.ext.workspace,
346
      key: 'jest.editor-update-snapshot',
347
      itemIds: [...allIds],
348
    });
349
  }
350

351
  /** get test item from jest process. If running tests from source file, will return undefined */
352
  private getItemFromProcess = (process: JestProcessInfo): vscode.TestItem | undefined => {
192✔
353
    // the TestExplorer triggered run should already have item associated
354
    if (process.userData?.testItem) {
243✔
355
      return process.userData.testItem;
1✔
356
    }
357

358
    let fileName;
359
    switch (process.request.type) {
242!
360
      case 'watch-tests':
361
      case 'watch-all-tests':
362
      case 'all-tests':
363
        return this.item;
207✔
364
      case 'by-file':
365
      case 'by-file-test':
366
        fileName = process.request.testFileName;
21✔
367
        break;
21✔
368
      case 'by-file-pattern':
369
      case 'by-file-test-pattern':
370
        fileName = process.request.testFileNamePattern;
14✔
371
        break;
14✔
372
      default:
NEW
UNCOV
373
        throw new Error(`unsupported external process type ${process.request.type}`);
×
374
    }
375

376
    return this.testDocuments.get(fileName)?.item;
35✔
377
  };
378

379
  /** return a valid run from event. if createIfMissing is true, then create a new one if none exist in the event **/
380
  private getJestRun(event: TypedRunEvent, createIfMissing: true): JestTestRun;
381
  private getJestRun(event: TypedRunEvent, createIfMissing?: false): JestTestRun | undefined;
382
  // istanbul ignore next
383
  private getJestRun(event: TypedRunEvent, createIfMissing = false): JestTestRun | undefined {
384
    let run = event.process.userData?.run;
385

386
    if (!run && createIfMissing) {
387
      const name = (event.process.userData?.run?.name ?? event.process.id) + `:${event.type}`;
388
      const testItem = this.getItemFromProcess(event.process) ?? this.item;
389
      run = this.createRun(name, testItem);
390
      event.process.userData = { ...event.process.userData, run, testItem };
391
    }
392
    run?.addProcess(event.process);
393
    return run;
394
  }
395

396
  private runLog(type: string): void {
397
    const d = new Date();
200✔
398
    this.context.output.write(`> Test run ${type} at ${d.toLocaleString()} <\r\n`, [
200✔
399
      'bold',
400
      'new-line',
401
    ]);
402
  }
403
  private onRunEvent = (event: JestRunEvent) => {
192✔
404
    if (event.process.request.type === 'not-test') {
278✔
405
      return;
1✔
406
    }
407

408
    try {
277✔
409
      const run = this.getJestRun(event, true);
277✔
410
      switch (event.type) {
277✔
411
        case 'scheduled': {
412
          this.deepItemState(event.process.userData?.testItem, run.enqueued);
14!
413
          break;
14✔
414
        }
415
        case 'data': {
416
          const text = event.raw ?? event.text;
61✔
417
          if (text && text.length > 0) {
61✔
418
            const opt = event.isError ? 'error' : event.newLine ? 'new-line' : undefined;
61✔
419
            run.write(text, opt);
61✔
420
          }
421
          break;
61✔
422
        }
423
        case 'start': {
424
          this.deepItemState(event.process.userData?.testItem, run.started);
132!
425
          outputManager.clearOutputOnRun(this.context.ext.output);
132✔
426
          this.runLog('started');
132✔
427
          break;
132✔
428
        }
429
        case 'end': {
430
          if (event.error && !event.process.userData?.execError) {
35!
431
            run.write(event.error, 'error');
8✔
432
            event.process.userData = { ...(event.process.userData ?? {}), execError: true };
8!
433
          }
434
          this.runLog('finished');
35✔
435
          run.end({ process: event.process, delay: 30000, reason: 'process end' });
35✔
436
          break;
35✔
437
        }
438
        case 'exit': {
439
          if (event.error) {
33✔
440
            const testItem = event.process.userData?.testItem;
25!
441
            if (testItem) {
25✔
442
              run.errored(testItem, new vscode.TestMessage(event.error));
25✔
443
            }
444
            if (!event.process.userData?.execError) {
25!
445
              const type = getExitErrorDef(event.code) ?? GENERIC_ERROR;
25✔
446
              run.write(event.error, type);
25✔
447
              event.process.userData = { ...(event.process.userData ?? {}), execError: true };
25!
448
            }
449
          }
450
          this.runLog('exited');
33✔
451
          run.end({ process: event.process, delay: 1000, reason: 'process exit' });
33✔
452
          break;
33✔
453
        }
454
        case 'long-run': {
455
          run.write(
1✔
456
            `Long Running Tests Warning: Tests exceeds ${event.threshold}ms threshold. Please reference Troubleshooting if this is not expected`,
457
            LONG_RUNNING_TESTS
458
          );
459
          break;
1✔
460
        }
461
      }
462
    } catch (err) {
UNCOV
463
      this.log('error', `<onRunEvent> ${event.type} failed:`, err);
×
UNCOV
464
      this.context.output.write(`<onRunEvent> ${event.type} failed: ${err}`, 'error');
×
465
    }
466
  };
467

468
  dispose(): void {
469
    this.unregisterEvents();
2✔
470
  }
471
}
472

473
export class FolderData extends TestItemDataBase {
4✔
474
  static makeUri = (parent: vscode.TestItem, folderName: string): vscode.Uri => {
4✔
475
    return vscode.Uri.joinPath(parent.uri!, folderName);
377✔
476
  };
477
  constructor(
478
    readonly context: JestTestProviderContext,
186✔
479
    readonly name: string,
186✔
480
    parent: vscode.TestItem
481
  ) {
482
    super(context, 'FolderData');
186✔
483
    this.item = this.createTestItem(name, parent);
186✔
484
  }
485
  private createTestItem(name: string, parent: vscode.TestItem) {
486
    const uri = FolderData.makeUri(parent, name);
186✔
487
    const item = this.context.createTestItem(uri.fsPath, name, uri, this, parent, ['run']);
186✔
488

489
    item.canResolveChildren = false;
186✔
490
    return item;
186✔
491
  }
492
  getJestRunRequest(itemCommand?: ItemCommand): JestExtRequestType {
493
    const updateSnapshot = itemCommand === ItemCommand.updateSnapshot;
17✔
494
    return {
17✔
495
      type: 'by-file-pattern',
496
      updateSnapshot,
497
      testFileNamePattern: this.uri.fsPath,
498
    };
499
  }
500
}
501

502
type ItemNodeType = NodeType<ItBlock | TestAssertionStatus>;
503
type ItemDataNodeType = DataNode<ItBlock | TestAssertionStatus>;
504
const isDataNode = (arg: ItemNodeType): arg is ItemDataNodeType =>
4✔
505
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
506
  (arg as any).data != null;
611✔
507

508
const isAssertDataNode = (arg: ItemNodeType): arg is DataNode<TestAssertionStatus> =>
4✔
509
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
510
  isDataNode(arg) && (arg.data as any).fullName;
193✔
511

512
const isContainerEmpty = (node?: ContainerNode<TestAssertionStatus>): boolean => {
4✔
513
  if (!node) {
211✔
514
    return true;
37✔
515
  }
516
  if (
174✔
517
    (node.childData && node.childData.length > 0) ||
358✔
518
    (node.childContainers && node.childContainers.length > 0)
519
  ) {
520
    return false;
172✔
521
  }
522
  return true;
2✔
523
};
524

525
// type AssertNode = NodeType<TestAssertionStatus>;
526
abstract class TestResultData extends TestItemDataBase {
527
  constructor(
528
    readonly context: JestTestProviderContext,
414✔
529
    name: string
530
  ) {
531
    super(context, name);
414✔
532
  }
533

534
  updateItemState(
535
    run: JestTestRun,
536
    result?: TestSuiteResult | TestAssertionStatus,
537
    errorLocation?: vscode.Location
538
  ): void {
539
    if (!result) {
221✔
540
      return;
1✔
541
    }
542
    const status = result.status;
220✔
543
    switch (status) {
220!
544
      case 'KnownSuccess':
545
        run.passed(this.item);
16✔
546
        break;
16✔
547
      case 'KnownSkip':
548
      case 'KnownTodo':
549
        run.skipped(this.item);
1✔
550
        break;
1✔
551
      case 'KnownFail': {
552
        const message = new vscode.TestMessage(result.message);
166✔
553
        if (errorLocation) {
166✔
554
          message.location = errorLocation;
3✔
555
        }
556

557
        run.failed(this.item, message);
166✔
558
        break;
166✔
559
      }
560
    }
561
  }
562

563
  makeTestId(fileUri: vscode.Uri, target?: ItemNodeType, extra?: string): string {
564
    const parts = [fileUri.fsPath];
609✔
565
    if (target && target.name !== ROOT_NODE_NAME) {
609✔
566
      parts.push(target.fullName);
403✔
567
    }
568
    if (extra) {
609✔
569
      parts.push(extra);
6✔
570
    }
571
    return parts.join('#');
609✔
572
  }
573

574
  /**
575
   * Synchronizes the child nodes of the test item with the given ItemNodeType, recursively.
576
   * @param node - The ItemNodeType to synchronize the child nodes with.
577
   * @returns void
578
   */
579
  syncChildNodes(node: ItemNodeType): void {
580
    this.item.error = undefined;
396✔
581

582
    if (!isDataNode(node)) {
396✔
583
      const idMap = [...node.childContainers, ...node.childData]
188✔
584
        .flatMap((n) => n.getAll() as ItemDataNodeType[])
192✔
585
        .reduce((map, node) => {
586
          const id = this.makeTestId(this.uri, node);
195✔
587
          map.set(id, map.get(id)?.concat(node) ?? [node]);
195✔
588
          return map;
195✔
589
        }, new Map<string, ItemDataNodeType[]>());
590

591
      const newItems: vscode.TestItem[] = [];
188✔
592
      idMap.forEach((nodes, id) => {
188✔
593
        if (nodes.length > 1) {
192✔
594
          // duplicate names found, append index to make a unique id: re-create the item with new id
595
          nodes.forEach((n, idx) => {
3✔
596
            newItems.push(new TestData(this.context, this.uri, n, this.item, `${idx}`).item);
6✔
597
          });
598
          return;
3✔
599
        }
600
        let cItem = this.item.children.get(id);
189✔
601
        if (cItem) {
189✔
602
          this.context.getData<TestData>(cItem)?.updateNode(nodes[0]);
11!
603
        } else {
604
          cItem = new TestData(this.context, this.uri, nodes[0], this.item).item;
178✔
605
        }
606
        newItems.push(cItem);
189✔
607
      });
608
      this.item.children.replace(newItems);
188✔
609
    } else {
610
      this.item.children.replace([]);
208✔
611
    }
612
  }
613

614
  createLocation(uri: vscode.Uri, zeroBasedLine = 0): vscode.Location {
×
615
    return new vscode.Location(uri, new vscode.Range(zeroBasedLine, 0, zeroBasedLine, 0));
3✔
616
  }
617

618
  forEachChild(onTestData: (child: TestData) => void): void {
619
    this.item.children.forEach((childItem) => {
417✔
620
      const child = this.context.getData<TestData>(childItem);
203✔
621
      if (child) {
203✔
622
        onTestData(child);
203✔
623
      }
624
    });
625
  }
626
}
627
export class TestDocumentRoot extends TestResultData {
4✔
628
  constructor(
629
    readonly context: JestTestProviderContext,
206✔
630
    fileUri: vscode.Uri,
631
    parent: vscode.TestItem
632
  ) {
633
    super(context, 'TestDocumentRoot');
206✔
634
    this.item = this.createTestItem(fileUri, parent);
206✔
635
  }
636
  private createTestItem(fileUri: vscode.Uri, parent: vscode.TestItem): vscode.TestItem {
637
    const item = this.context.createTestItem(
206✔
638
      this.makeTestId(fileUri),
639
      path.basename(fileUri.fsPath),
640
      fileUri,
641
      this,
642
      parent
643
    );
644

645
    item.canResolveChildren = true;
206✔
646
    return item;
206✔
647
  }
648

649
  discoverTest = (run?: JestTestRun, parsedRoot?: ContainerNode<ItBlock>): void => {
206✔
650
    this.createChildItems(parsedRoot);
178✔
651
    if (run) {
178✔
652
      this.updateResultState(run);
176✔
653
    }
654
  };
655

656
  private createChildItems = (parsedRoot?: ContainerNode<ItBlock>): void => {
206✔
657
    const container =
658
      this.context.ext.testResultProvider.getTestSuiteResult(this.item.id)?.assertionContainer ??
178✔
659
      parsedRoot;
660
    if (!container) {
178✔
661
      this.item.children.replace([]);
1✔
662
    } else {
663
      this.syncChildNodes(container);
177✔
664
    }
665

666
    this.item.canResolveChildren = false;
178✔
667
  };
668

669
  public updateResultState(run: JestTestRun): void {
670
    const suiteResult = this.context.ext.testResultProvider.getTestSuiteResult(this.item.id);
211✔
671

672
    // only update suite status if the assertionContainer is empty, which can occur when
673
    // test file has syntax error or failed to run for whatever reason.
674
    // In this case we should mark the suite itself as TestExplorer won't be able to
675
    // aggregate from the children list
676
    if (isContainerEmpty(suiteResult?.assertionContainer)) {
211✔
677
      this.updateItemState(run, suiteResult);
39✔
678
    }
679
    this.forEachChild((child) => child.updateResultState(run));
211✔
680
  }
681

682
  getJestRunRequest(itemCommand?: ItemCommand): JestExtRequestType {
683
    const updateSnapshot = itemCommand === ItemCommand.updateSnapshot;
20✔
684
    return {
20✔
685
      type: 'by-file-pattern',
686
      updateSnapshot,
687
      testFileNamePattern: this.uri.fsPath,
688
    };
689
  }
690

691
  getDebugInfo(): ReturnType<Debuggable['getDebugInfo']> {
692
    return { fileName: this.uri.fsPath };
1✔
693
  }
694

695
  public onTestMatched(): void {
696
    this.forEachChild((child) => child.onTestMatched());
1✔
697
  }
698
  public gatherSnapshotItems(snapshotItems: SnapshotItemCollection): void {
699
    this.forEachChild((child) => child.gatherSnapshotItems(snapshotItems));
5✔
700
  }
701
}
702
export class TestData extends TestResultData implements Debuggable {
4✔
703
  constructor(
704
    readonly context: JestTestProviderContext,
208✔
705
    fileUri: vscode.Uri,
706
    private node: ItemNodeType,
208✔
707
    parent: vscode.TestItem,
708
    extraId?: string
709
  ) {
710
    super(context, 'TestData');
208✔
711
    this.item = this.createTestItem(fileUri, parent, extraId);
208✔
712
    this.updateNode(node);
208✔
713
  }
714

715
  private createTestItem(fileUri: vscode.Uri, parent: vscode.TestItem, extraId?: string) {
716
    const item = this.context.createTestItem(
208✔
717
      this.makeTestId(fileUri, this.node, extraId),
718
      this.node.name,
719
      fileUri,
720
      this,
721
      parent
722
    );
723

724
    item.canResolveChildren = false;
208✔
725
    return item;
208✔
726
  }
727

728
  private getTestNamePattern(): TestNamePattern {
729
    if (isDataNode(this.node)) {
22✔
730
      return { value: this.node.fullName, exactMatch: true };
21✔
731
    }
732
    return { value: this.node.fullName, exactMatch: false };
1✔
733
  }
734

735
  getJestRunRequest(itemCommand?: ItemCommand): JestExtRequestType {
736
    return {
21✔
737
      type: 'by-file-test-pattern',
738
      updateSnapshot: itemCommand === ItemCommand.updateSnapshot,
739
      testFileNamePattern: this.uri.fsPath,
740
      testNamePattern: this.getTestNamePattern(),
741
    };
742
  }
743

744
  getDebugInfo(): ReturnType<Debuggable['getDebugInfo']> {
745
    return { fileName: this.uri.fsPath, testNamePattern: this.getTestNamePattern() };
1✔
746
  }
747
  private updateItemRange(): void {
748
    if (this.node.attrs.range) {
222✔
749
      const pos = [
198✔
750
        this.node.attrs.range.start.line,
751
        this.node.attrs.range.start.column,
752
        this.node.attrs.range.end.line,
753
        this.node.attrs.range.end.column,
754
      ];
755
      if (pos.every((n) => n >= 0)) {
792✔
756
        this.item.range = new vscode.Range(pos[0], pos[1], pos[2], pos[3]);
198✔
757
        return;
198✔
758
      }
759
    }
760
    this.item.range = undefined;
24✔
761
  }
762

763
  updateNode(node: ItemNodeType): void {
764
    this.node = node;
219✔
765
    this.updateItemRange();
219✔
766
    this.syncChildNodes(node);
219✔
767
  }
768

769
  public onTestMatched(): void {
770
    // assertion might have picked up source block location
771
    this.updateItemRange();
3✔
772
    this.forEachChild((child) => child.onTestMatched());
3✔
773
  }
774

775
  /**
776
   * determine if a test contains dynamic content, such as template-literal or "test.each" variables from the node info.
777
   * Once the test is run, the node should reflect the resolved names.
778
   */
779
  isTestNameResolved(): boolean {
780
    //isGroup = true means "test.each"
781
    return !(this.node.attrs.isGroup === 'yes' || this.node.attrs.nonLiteralName === true);
30✔
782
  }
783
  public gatherSnapshotItems(snapshotItems: SnapshotItemCollection): void {
784
    // only response if not a "dynamic named" test, which we can't update or view snapshot until the names are resolved
785
    // after running the tests
786
    if (!this.isTestNameResolved()) {
7✔
787
      return;
1✔
788
    }
789
    if (this.node.attrs.snapshot === 'inline') {
6✔
790
      snapshotItems.updatable.push(this.item);
2✔
791
    }
792
    if (this.node.attrs.snapshot === 'external') {
6✔
793
      snapshotItems.updatable.push(this.item);
1✔
794
      snapshotItems.viewable.push(this.item);
1✔
795
    }
796
    this.forEachChild((child) => child.gatherSnapshotItems(snapshotItems));
6✔
797
  }
798
  public updateResultState(run: JestTestRun): void {
799
    if (this.node && isAssertDataNode(this.node)) {
193✔
800
      const assertion = this.node.data;
182✔
801
      const errorLine =
802
        assertion.line != null ? this.createLocation(this.uri, assertion.line - 1) : undefined;
182✔
803
      this.updateItemState(run, assertion, errorLine);
182✔
804
    }
805
    this.forEachChild((child) => child.updateResultState(run));
193✔
806
  }
807
  public viewSnapshot(): Promise<void> {
808
    if (this.node.attrs.snapshot === 'external') {
4✔
809
      return this.context.ext.testResultProvider.previewSnapshot(
2✔
810
        this.uri.fsPath,
811
        this.node.fullName
812
      );
813
    }
814
    this.log('error', `no external snapshot to be viewed: ${this.item.id}`);
2✔
815
    return Promise.resolve();
2✔
816
  }
817
}
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

© 2025 Coveralls, Inc