Skip to content

Commit

Permalink
feat: report Individual Test Cases (#10227)
Browse files Browse the repository at this point in the history
Co-authored-by: Kunal Kushwaha <[email protected]>
Co-authored-by: Rogelio Guzman <[email protected]>
  • Loading branch information
3 people authored Jul 30, 2020
1 parent 93cde55 commit eb23ac1
Show file tree
Hide file tree
Showing 25 changed files with 521 additions and 126 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

### Features

- `[jest-core, jest-circus, jest-reporter, jest-runner]` Added support for reporting individual test cases using jest-circus ([#10227](https://github.com/facebook/jest/pull/10227))
- `[jest-config, jest-reporter, jest-runner, jest-test-sequencer]` Add `slowTestThreshold` configuration option ([#9366](https://github.com/facebook/jest/pull/9366))
- `[jest-worker]` Added support for workers to send custom messages to parent in jest-worker ([#10293](https://github.com/facebook/jest/pull/10293))
- `[pretty-format]` Added support for serializing custom elements (web components) ([#10217](https://github.com/facebook/jest/pull/10237))
Expand Down
1 change: 1 addition & 0 deletions packages/jest-circus/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"jest-each": "^26.1.0",
"jest-matcher-utils": "^26.1.0",
"jest-message-util": "^26.1.0",
"jest-runner": "^26.1.0",
"jest-runtime": "^26.1.0",
"jest-snapshot": "^26.1.0",
"jest-util": "^26.1.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import * as path from 'path';
import type {Config} from '@jest/types';
import type {JestEnvironment} from '@jest/environment';
import type {TestResult} from '@jest/test-result';
import type {TestFileEvent} from 'jest-runner';
import type {RuntimeType as Runtime} from 'jest-runtime';
import type {SnapshotStateType} from 'jest-snapshot';
import {deepCyclicCopy} from 'jest-util';
Expand All @@ -22,6 +23,7 @@ const jestAdapter = async (
environment: JestEnvironment,
runtime: Runtime,
testPath: string,
sendMessageToJest?: TestFileEvent,
): Promise<TestResult> => {
const {
initialize,
Expand All @@ -46,6 +48,7 @@ const jestAdapter = async (
globalConfig,
localRequire: runtime.requireModule.bind(runtime),
parentProcess: process,
sendMessageToJest,
testPath,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from '@jest/test-result';
import {extractExpectedAssertionsErrors, getState, setState} from 'expect';
import {formatExecError, formatResultsErrors} from 'jest-message-util';
import type {TestFileEvent} from 'jest-runner';
import {
SnapshotState,
SnapshotStateType,
Expand All @@ -30,6 +31,7 @@ import {
} from '../state';
import {getTestID} from '../utils';
import run from '../run';
import testCaseReportHandler from '../testCaseReportHandler';
import globals from '..';

type Process = NodeJS.Process;
Expand All @@ -45,6 +47,7 @@ export const initialize = async ({
localRequire,
parentProcess,
testPath,
sendMessageToJest,
}: {
config: Config.ProjectConfig;
environment: JestEnvironment;
Expand All @@ -54,6 +57,7 @@ export const initialize = async ({
localRequire: (path: Config.Path) => any;
testPath: Config.Path;
parentProcess: Process;
sendMessageToJest?: TestFileEvent;
}) => {
if (globalConfig.testTimeout) {
getRunnerState().testTimeout = globalConfig.testTimeout;
Expand Down Expand Up @@ -140,6 +144,9 @@ export const initialize = async ({
setState({snapshotState, testPath});

addEventHandler(handleSnapshotStateAfterRetry(snapshotState));
if (sendMessageToJest) {
addEventHandler(testCaseReportHandler(testPath, sendMessageToJest));
}

// Return it back to the outer scope (test runner outside the VM).
return {globals, snapshotState};
Expand Down
26 changes: 26 additions & 0 deletions packages/jest-circus/src/testCaseReportHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import type {Circus} from '@jest/types';
import type {TestFileEvent} from 'jest-runner';
import {makeSingleTestResult, parseSingleTestResult} from './utils';

const testCaseReportHandler = (
testPath: string,
sendMessageToJest: TestFileEvent,
) => (event: Circus.Event) => {
switch (event.name) {
case 'test_done': {
const testResult = makeSingleTestResult(event.test);
const testCaseResult = parseSingleTestResult(testResult);
sendMessageToJest('test-case-result', [testPath, testCaseResult]);
break;
}
}
};

export default testCaseReportHandler;
124 changes: 80 additions & 44 deletions packages/jest-circus/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import co from 'co';
import dedent = require('dedent');
import StackUtils = require('stack-utils');
import prettyFormat = require('pretty-format');
import {getState} from './state';
import type {AssertionResult, Status} from '@jest/test-result';
import {ROOT_DESCRIBE_BLOCK_NAME, getState} from './state';

const stackUtils = new StackUtils({cwd: 'A path that does not exist'});

Expand Down Expand Up @@ -282,60 +283,61 @@ export const makeRunResult = (
unhandledErrors: unhandledErrors.map(_formatError),
});

export const makeSingleTestResult = (
test: Circus.TestEntry,
): Circus.TestResult => {
const {includeTestLocationInResult} = getState();
const testPath = [];
let parent: Circus.TestEntry | Circus.DescribeBlock | undefined = test;

const {status} = test;
invariant(status, 'Status should be present after tests are run.');

do {
testPath.unshift(parent.name);
} while ((parent = parent.parent));

let location = null;
if (includeTestLocationInResult) {
const stackLine = test.asyncError.stack.split('\n')[1];
const parsedLine = stackUtils.parseLine(stackLine);
if (
parsedLine &&
typeof parsedLine.column === 'number' &&
typeof parsedLine.line === 'number'
) {
location = {
column: parsedLine.column,
line: parsedLine.line,
};
}
}

return {
duration: test.duration,
errors: test.errors.map(_formatError),
invocations: test.invocations,
location,
status,
testPath: Array.from(testPath),
};
};

const makeTestResults = (
describeBlock: Circus.DescribeBlock,
): Circus.TestResults => {
const {includeTestLocationInResult} = getState();
const testResults: Circus.TestResults = [];

for (const child of describeBlock.children) {
switch (child.type) {
case 'describeBlock': {
testResults.push(...makeTestResults(child));
break;
}
case 'test':
{
const testPath = [];
let parent:
| Circus.TestEntry
| Circus.DescribeBlock
| undefined = child;
do {
testPath.unshift(parent.name);
} while ((parent = parent.parent));

const {status} = child;

if (!status) {
throw new Error('Status should be present after tests are run.');
}

let location = null;
if (includeTestLocationInResult) {
const stackLine = child.asyncError.stack.split('\n')[1];
const parsedLine = stackUtils.parseLine(stackLine);
if (
parsedLine &&
typeof parsedLine.column === 'number' &&
typeof parsedLine.line === 'number'
) {
location = {
column: parsedLine.column,
line: parsedLine.line,
};
}
}

testResults.push({
duration: child.duration,
errors: child.errors.map(_formatError),
invocations: child.invocations,
location,
status,
testPath,
});
}
case 'test': {
testResults.push(makeSingleTestResult(child));
break;
}
}
}

Expand Down Expand Up @@ -408,3 +410,37 @@ export function invariant(
throw new Error(message);
}
}

export const parseSingleTestResult = (
testResult: Circus.TestResult,
): AssertionResult => {
let status: Status;
if (testResult.status === 'skip') {
status = 'pending';
} else if (testResult.status === 'todo') {
status = 'todo';
} else if (testResult.errors.length > 0) {
status = 'failed';
} else {
status = 'passed';
}

const ancestorTitles = testResult.testPath.filter(
name => name !== ROOT_DESCRIBE_BLOCK_NAME,
);
const title = ancestorTitles.pop();

return {
ancestorTitles,
duration: testResult.duration,
failureMessages: Array.from(testResult.errors),
fullName: title
? ancestorTitles.concat(title).join(' ')
: ancestorTitles.join(' '),
invocations: testResult.invocations,
location: testResult.location,
numPassingAsserts: 0,
status,
title: testResult.testPath[testResult.testPath.length - 1],
};
};
1 change: 1 addition & 0 deletions packages/jest-circus/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
{"path": "../jest-environment"},
{"path": "../jest-matcher-utils"},
{"path": "../jest-message-util"},
{"path": "../jest-runner"},
{"path": "../jest-runtime"},
{"path": "../jest-snapshot"},
{"path": "../jest-test-result"},
Expand Down
39 changes: 31 additions & 8 deletions packages/jest-core/src/ReporterDispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
* LICENSE file in the root directory of this source tree.
*/

import type {AggregatedResult, TestResult} from '@jest/test-result';
import type {
AggregatedResult,
TestCaseResult,
TestResult,
} from '@jest/test-result';
import type {Test} from 'jest-runner';
import type {Context} from 'jest-runtime';
import type {Reporter, ReporterOnStartOptions} from '@jest/reporters';
Expand All @@ -27,14 +31,17 @@ export default class ReporterDispatcher {
);
}

async onTestResult(
async onTestFileResult(
test: Test,
testResult: TestResult,
results: AggregatedResult,
): Promise<void> {
for (const reporter of this._reporters) {
reporter.onTestResult &&
(await reporter.onTestResult(test, testResult, results));
if (reporter.onTestFileResult) {
await reporter.onTestFileResult(test, testResult, results);
} else if (reporter.onTestResult) {
await reporter.onTestResult(test, testResult, results);
}
}

// Release memory if unused later.
Expand All @@ -43,9 +50,13 @@ export default class ReporterDispatcher {
testResult.console = undefined;
}

async onTestStart(test: Test): Promise<void> {
async onTestFileStart(test: Test): Promise<void> {
for (const reporter of this._reporters) {
reporter.onTestStart && (await reporter.onTestStart(test));
if (reporter.onTestFileStart) {
await reporter.onTestFileStart(test);
} else if (reporter.onTestStart) {
await reporter.onTestStart(test);
}
}
}

Expand All @@ -58,13 +69,25 @@ export default class ReporterDispatcher {
}
}

async onTestCaseResult(
test: Test,
testCaseResult: TestCaseResult,
): Promise<void> {
for (const reporter of this._reporters) {
if (reporter.onTestCaseResult) {
await reporter.onTestCaseResult(test, testCaseResult);
}
}
}

async onRunComplete(
contexts: Set<Context>,
results: AggregatedResult,
): Promise<void> {
for (const reporter of this._reporters) {
reporter.onRunComplete &&
(await reporter.onRunComplete(contexts, results));
if (reporter.onRunComplete) {
await reporter.onRunComplete(contexts, results);
}
}
}

Expand Down
Loading

0 comments on commit eb23ac1

Please sign in to comment.