All files collect-test-profile.ts

94.73% Statements 72/76
80.48% Branches 33/41
100% Functions 5/5
98.5% Lines 66/67

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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            4x 4x 4x       4x 4x 4x 4x     19x 19x 19x   19x 11x 11x       8x   25x     7x             18x     1x       3x 3x       43x 43x 43x 43x 43x 43x 43x 43x       152x     2x 2x       41x 41x 41x 41x 41x     72x 72x   441x   41x 41x 123x 41x     608x 19x 19x 19x   8x                     3x 3x       2x 2x       1x 1x             3x 3x                   10x 10x       57x 19x         4x   2x 2x       935x     88x   43x                      
import * as ts from 'typescript';
 
import { getLineAndCharacter } from './utils.js';
 
import type { CodeLocation, FileEntry, MockControlCall, TestBlock, TestProfile } from './types.js';
 
const ASSERTION_PATTERNS = new Set(['expect', 'assert', 'should']);
const MOCK_PATTERNS = ['jest.mock', 'vi.mock', 'sinon.stub', 'jest.spyOn', 'vi.spyOn', 'sinon.mock'];
const RESTORE_PATTERNS = new Set([
  'jest.restoreAllMocks',
  'vi.restoreAllMocks',
]);
const SETUP_PATTERNS = new Set(['beforeAll', 'beforeEach', 'afterAll', 'afterEach']);
const FOCUSED_PATTERNS = new Set(['it.only', 'test.only', 'describe.only', 'it.skip', 'test.skip', 'describe.skip', 'it.todo', 'test.todo']);
const USE_FAKE_TIMER_PATTERNS = new Set(['jest.useFakeTimers', 'vi.useFakeTimers']);
const USE_REAL_TIMER_PATTERNS = new Set(['jest.useRealTimers', 'vi.useRealTimers']);
 
function getSpyOrStubKind(call: ts.CallExpression, sourceFile: ts.SourceFile): MockControlCall['kind'] | undefined {
  Iif (!ts.isPropertyAccessExpression(call.expression)) return undefined;
  const methodName = call.expression.name.getText(sourceFile);
  const receiver = call.expression.expression.getText(sourceFile);
 
  if ((receiver === 'jest' || receiver === 'vi') && methodName === 'spyOn') return 'spy';
  Iif (receiver === 'sinon' && (methodName === 'stub' || methodName === 'mock')) return 'stub';
  return undefined;
}
 
function getMockControlTarget(node: ts.Node, sourceFile: ts.SourceFile): string | undefined {
  let current = node;
  while (current.parent) {
    const parent = current.parent;
 
    if (ts.isVariableDeclaration(parent) && parent.initializer === current && ts.isIdentifier(parent.name)) {
      return parent.name.getText(sourceFile);
    }
 
    if (ts.isBinaryExpression(parent) && parent.operatorToken.kind === ts.SyntaxKind.EqualsToken && parent.right === current) {
      return parent.left.getText(sourceFile).trim();
    }
 
    current = parent;
  }
 
  return undefined;
}
 
function getMockRestoreTarget(node: ts.CallExpression, sourceFile: ts.SourceFile): string | undefined {
  Iif (!ts.isPropertyAccessExpression(node.expression)) return undefined;
  return node.expression.expression.getText(sourceFile).trim();
}
 
export function collectTestProfile(sourceFile: ts.SourceFile, fileRelative: string, fileEntry: FileEntry): void {
  const testBlocks: TestBlock[] = [];
  const mockCalls: CodeLocation[] = [];
  const setupCalls: TestProfile['setupCalls'] = [];
  const mutableStateDecls: CodeLocation[] = [];
  const focusedCalls: TestProfile['focusedCalls'] = [];
  const timerControls: TestProfile['timerControls'] = [];
  const mockRestores: TestProfile['mockRestores'] = [];
  const spyOrStubCalls: TestProfile['spyOrStubCalls'] = [];
 
  const visit = (node: ts.Node, insideDescribe: boolean, insideTest: boolean): void => {
    if (ts.isCallExpression(node)) {
      const callText = node.expression.getText(sourceFile);
 
      if (FOCUSED_PATTERNS.has(callText)) {
        const loc = getLineAndCharacter(sourceFile, node);
        focusedCalls.push({ kind: callText as TestProfile['focusedCalls'][0]['kind'], lineStart: loc.lineStart, lineEnd: loc.lineEnd });
      }
 
      if ((callText === 'it' || callText === 'test' || callText === 'it.only' || callText === 'test.only') && node.arguments.length >= 2) {
        const nameArg = node.arguments[0];
        const name = ts.isStringLiteral(nameArg) ? nameArg.text : callText;
        const body = node.arguments[1];
        const loc = getLineAndCharacter(sourceFile, node);
        let assertionCount = 0;
        const countAssertions = (n: ts.Node): void => {
          if (ts.isCallExpression(n)) {
            const t = n.expression.getText(sourceFile);
            if (ASSERTION_PATTERNS.has(t.split('.')[0]) || t.includes('.to.') || t.includes('.should')) assertionCount++;
          }
          ts.forEachChild(n, countAssertions);
        };
        ts.forEachChild(body, countAssertions);
        testBlocks.push({ name, lineStart: loc.lineStart, lineEnd: loc.lineEnd, assertionCount });
        ts.forEachChild(node, (child) => visit(child, insideDescribe, true));
        return;
      }
 
      if (MOCK_PATTERNS.some((p) => callText === p || callText.startsWith(p + '('))) {
        const loc = getLineAndCharacter(sourceFile, node);
        mockCalls.push({ file: fileRelative, lineStart: loc.lineStart, lineEnd: loc.lineEnd });
        const spyOrStubKind = getSpyOrStubKind(node, sourceFile);
        if (spyOrStubKind) {
          spyOrStubCalls.push({
            kind: spyOrStubKind,
            file: fileRelative,
            lineStart: loc.lineStart,
            lineEnd: loc.lineEnd,
            target: getMockControlTarget(node, sourceFile),
          });
        }
      }
 
      if (USE_FAKE_TIMER_PATTERNS.has(callText)) {
        const loc = getLineAndCharacter(sourceFile, node);
        timerControls.push({ kind: callText as TestProfile['timerControls'][0]['kind'], lineStart: loc.lineStart, lineEnd: loc.lineEnd });
      }
 
      if (USE_REAL_TIMER_PATTERNS.has(callText)) {
        const loc = getLineAndCharacter(sourceFile, node);
        timerControls.push({ kind: callText as TestProfile['timerControls'][0]['kind'], lineStart: loc.lineStart, lineEnd: loc.lineEnd });
      }
 
      if (RESTORE_PATTERNS.has(callText)) {
        const loc = getLineAndCharacter(sourceFile, node);
        mockRestores.push({
          kind: 'restoreAll',
          file: fileRelative,
          lineStart: loc.lineStart,
          lineEnd: loc.lineEnd,
        });
      } else if (callText.endsWith('.mockRestore')) {
        const loc = getLineAndCharacter(sourceFile, node);
        mockRestores.push({
          kind: 'restore',
          file: fileRelative,
          lineStart: loc.lineStart,
          lineEnd: loc.lineEnd,
          target: getMockRestoreTarget(node, sourceFile),
        });
      }
 
      if (SETUP_PATTERNS.has(callText)) {
        const loc = getLineAndCharacter(sourceFile, node);
        setupCalls.push({ kind: callText as TestProfile['setupCalls'][0]['kind'], lineStart: loc.lineStart });
      }
 
      if (callText === 'describe' || callText === 'describe.only') {
        ts.forEachChild(node, (child) => visit(child, true, insideTest));
        return;
      }
    }
 
    if (insideDescribe && !insideTest && ts.isVariableStatement(node)) {
      const decl = node.declarationList;
      if (decl.flags & ts.NodeFlags.Let || !(decl.flags & ts.NodeFlags.Const)) {
        const loc = getLineAndCharacter(sourceFile, node);
        mutableStateDecls.push({ file: fileRelative, lineStart: loc.lineStart, lineEnd: loc.lineEnd });
      }
    }
 
    ts.forEachChild(node, (child) => visit(child, insideDescribe, insideTest));
  };
 
  ts.forEachChild(sourceFile, (child) => visit(child, false, false));
 
  fileEntry.testProfile = {
    testBlocks,
    mockCalls,
    setupCalls,
    mutableStateDecls,
    focusedCalls,
    timerControls,
    mockRestores,
    spyOrStubCalls,
  };
}