diff --git a/src/test-provider/test-item-data.ts b/src/test-provider/test-item-data.ts index 93e4ae503..5d7a3a01f 100644 --- a/src/test-provider/test-item-data.ts +++ b/src/test-provider/test-item-data.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode'; import { extensionId } from '../appGlobals'; -import { JestRunEvent } from '../JestExt'; +import { JestRunEvent, RunEventBase } from '../JestExt'; import { TestSuiteResult } from '../TestResults'; import * as path from 'path'; import { JestExtRequestType } from '../JestExt/process-session'; @@ -21,6 +21,7 @@ interface WithUri { } type JestTestRunRequest = JestExtRequestType & { run: JestTestRun }; +type TypedRunEvent = RunEventBase & { type: string }; // eslint-disable-next-line @typescript-eslint/no-explicit-any const isJestTestRunRequest = (arg: any): arg is JestTestRunRequest => @@ -54,7 +55,7 @@ abstract class TestItemDataBase implements TestItemData, JestRunable, WithUri { const jestRequest = this.getJestRunRequest(); run.item = this.item; - this.deepItemState(this.item, run.vscodeRun.enqueued); + this.deepItemState(this.item, run.enqueued); const process = this.context.ext.session.scheduleProcess({ ...jestRequest, @@ -62,7 +63,7 @@ abstract class TestItemDataBase implements TestItemData, JestRunable, WithUri { }); if (!process) { const msg = `failed to schedule test for ${this.item.id}`; - run.vscodeRun.errored(this.item, new vscode.TestMessage(msg)); + run.errored(this.item, new vscode.TestMessage(msg)); run.write(msg, 'error'); run.end(); } @@ -137,11 +138,12 @@ export class WorkspaceRoot extends TestItemDataBase { }; private createRun = (options?: JestTestRunOptions): JestTestRun => { - const target = options?.item ?? this.item; - return this.context.createTestRun(new vscode.TestRunRequest([target]), { + const item = options?.item ?? this.item; + const request = options?.request ?? new vscode.TestRunRequest([item]); + return this.context.createTestRun(request, { ...options, - name: options?.name ?? target.id, - item: target, + name: options?.name ?? item.id, + item, }); }; @@ -224,7 +226,7 @@ export class WorkspaceRoot extends TestItemDataBase { private onTestSuiteChanged = (event: TestSuitChangeEvent): void => { switch (event.type) { case 'assertions-updated': { - const run = this.getJestRun(event.process) ?? this.createRun({ name: event.process.id }); + const run = this.getJestRun(event.process) ?? this.createRunForEvent(event); this.log( 'debug', @@ -248,6 +250,12 @@ export class WorkspaceRoot extends TestItemDataBase { /** get test item from jest process. If running tests from source file, will return undefined */ private getItemFromProcess = (process: JestProcessInfo): vscode.TestItem | undefined => { + // the TestExplorer triggered run should already have item associated + if (isJestTestRunRequest(process.request) && process.request.run.item) { + return process.request.run.item; + } + + // should only come here for autoRun processes let fileName; switch (process.request.type) { case 'watch-tests': @@ -267,19 +275,29 @@ export class WorkspaceRoot extends TestItemDataBase { return this.testDocuments.get(fileName)?.item; }; - private createJestTestRun = (event: JestRunEvent): JestTestRun => { + private createRunForEvent = (event: TypedRunEvent): JestTestRun => { const item = this.getItemFromProcess(event.process) ?? this.item; + const [request, name] = isJestTestRunRequest(event.process.request) + ? [event.process.request.run.request, event.process.request.run.name] + : []; const run = this.createRun({ - name: `${event.type}:${event.process.id}`, + name: name ?? `${event.type}:${event.process.id}`, item, onEnd: () => this.cachedRun.delete(event.process.id), + request, }); this.cachedRun.set(event.process.id, run); return run; }; - private getJestRun = (process: JestProcessInfo): JestTestRun | undefined => - isJestTestRunRequest(process.request) ? process.request.run : this.cachedRun.get(process.id); + /** return a valid run from process or process-run-cache. return undefined if run is closed. */ + private getJestRun = (process: JestProcessInfo): JestTestRun | undefined => { + const run = isJestTestRunRequest(process.request) + ? process.request.run + : this.cachedRun.get(process.id); + + return run?.isClosed() ? undefined : run; + }; private runLog(type: string): void { const d = new Date(); @@ -299,22 +317,24 @@ export class WorkspaceRoot extends TestItemDataBase { switch (event.type) { case 'scheduled': { if (!run) { - run = this.createJestTestRun(event); - this.deepItemState(run.item, run.vscodeRun.enqueued); + run = this.createRunForEvent(event); + this.deepItemState(run.item, run.enqueued); } break; } case 'data': { - run = run ?? this.createJestTestRun(event); const text = event.raw ?? event.text; - const opt = event.isError ? 'error' : event.newLine ? 'new-line' : undefined; - run.write(text, opt); + if (text && text.length > 0) { + run = run ?? this.createRunForEvent(event); + const opt = event.isError ? 'error' : event.newLine ? 'new-line' : undefined; + run.write(text, opt); + } break; } case 'start': { - run = run ?? this.createJestTestRun(event); - this.deepItemState(run.item, run.vscodeRun.started); + run = run ?? this.createRunForEvent(event); + this.deepItemState(run.item, run.started); this.runLog('started'); break; } @@ -325,13 +345,11 @@ export class WorkspaceRoot extends TestItemDataBase { } case 'exit': { if (event.error) { - if (!run || run.vscodeRun.token.isCancellationRequested) { - run = this.createJestTestRun(event); - } + run = run ?? this.createRunForEvent(event); const type = getExitErrorDef(event.code) ?? GENERIC_ERROR; run.write(event.error, type); if (run.item) { - run.vscodeRun.errored(run.item, new vscode.TestMessage(event.error)); + run.errored(run.item, new vscode.TestMessage(event.error)); } } this.runLog('exited'); @@ -397,6 +415,22 @@ const isAssertDataNode = (arg: ItemNodeType): arg is DataNode { + if (!node) { + return true; + } + if (isDataNode(node)) { + return false; + } + if ( + (node.childData && node.childData.length > 0) || + (node.childContainers && node.childContainers.length > 0) + ) { + return false; + } + return true; +}; + // type AssertNode = NodeType; abstract class TestResultData extends TestItemDataBase { constructor(readonly context: JestTestProviderContext, name: string) { @@ -414,11 +448,11 @@ abstract class TestResultData extends TestItemDataBase { const status = result.status; switch (status) { case 'KnownSuccess': - run.vscodeRun.passed(this.item); + run.passed(this.item); break; case 'KnownSkip': case 'KnownTodo': - run.vscodeRun.skipped(this.item); + run.skipped(this.item); break; case 'KnownFail': { if (this.context.ext.settings.testExplorer.showInlineError) { @@ -427,9 +461,9 @@ abstract class TestResultData extends TestItemDataBase { message.location = errorLocation; } - run.vscodeRun.failed(this.item, message); + run.failed(this.item, message); } else { - run.vscodeRun.failed(this.item, []); + run.failed(this.item, []); } break; } @@ -546,7 +580,14 @@ export class TestDocumentRoot extends TestResultData { public updateResultState(run: JestTestRun): void { const suiteResult = this.context.ext.testResolveProvider.getTestSuiteResult(this.item.id); - this.updateItemState(run, suiteResult); + + // only update suite status if the assertionContainer is empty, which can occur when + // test file has syntax error or failed to run for whatever reason. + // In this case we should mark the suite itself as TestExplorer won't be able to + // aggregate from the children list + if (isEmpty(suiteResult?.assertionContainer)) { + this.updateItemState(run, suiteResult); + } this.item.children.forEach((childItem) => this.context.getData(childItem)?.updateResultState(run) diff --git a/src/test-provider/test-provider-helper.ts b/src/test-provider/test-provider-helper.ts index 4437ed331..3a8f5e839 100644 --- a/src/test-provider/test-provider-helper.ts +++ b/src/test-provider/test-provider-helper.ts @@ -11,6 +11,7 @@ import { JestExtExplorerContext, TestItemData } from './types'; export type TagIdType = 'run' | 'debug'; +let RunSeq = 0; export class JestTestProviderContext { private testItemData: WeakMap; @@ -76,8 +77,10 @@ export class JestTestProviderContext { }; createTestRun = (request: vscode.TestRunRequest, options?: JestTestRunOptions): JestTestRun => { - const vscodeRun = this.controller.createTestRun(request, options?.name ?? 'unknown'); - return new JestTestRun(this, vscodeRun, options); + const name = options?.name ?? `run-${RunSeq++}`; + const opt = { ...(options ?? {}), request, name }; + const vscodeRun = this.controller.createTestRun(request, name); + return new JestTestRun(this, vscodeRun, opt); }; // tags @@ -88,36 +91,115 @@ export class JestTestProviderContext { export interface JestTestRunOptions { name?: string; item?: vscode.TestItem; + request?: vscode.TestRunRequest; + // in addition to the regular end() method onEnd?: () => void; - // if true, when the run ends, we will not end the vscodeRun, this is used when multiple test items - // in a single request, that the run should be closed when all items are done. - disableVscodeRunEnd?: boolean; + + // replace the end function + end?: () => void; } -export class JestTestRun implements JestExtOutput { +export type TestRunProtocol = Pick< + vscode.TestRun, + 'name' | 'enqueued' | 'started' | 'errored' | 'failed' | 'passed' | 'skipped' | 'end' +>; +export type ParentRun = vscode.TestRun | JestTestRun; +const isVscodeRun = (arg: ParentRun | undefined): arg is vscode.TestRun => + // eslint-disable-next-line @typescript-eslint/no-explicit-any + arg != null && typeof (arg as any).appendOutput === 'function'; +const isJestTestRun = (arg: ParentRun | undefined): arg is JestTestRun => !isVscodeRun(arg); + +/** a wrapper for vscode.TestRun or another JestTestRun */ +export class JestTestRun implements JestExtOutput, TestRunProtocol { private output: JestOutputTerminal; public item?: vscode.TestItem; + private parentRun?: ParentRun; constructor( context: JestTestProviderContext, - public vscodeRun: vscode.TestRun, + parentRun: ParentRun, private options?: JestTestRunOptions ) { + this.parentRun = parentRun; this.output = context.output; this.item = options?.item; } - end(): void { - if (this.options?.disableVscodeRunEnd !== true) { - this.vscodeRun.end(); + get vscodeRun(): vscode.TestRun | undefined { + if (!this.parentRun) { + return; } - this.options?.onEnd?.(); + if (isVscodeRun(this.parentRun)) { + return this.parentRun; + } + return this.parentRun.vscodeRun; } write(msg: string, opt?: OutputOptions): string { const text = this.output.write(msg, opt); - this.vscodeRun.appendOutput(text); + this.vscodeRun?.appendOutput(text); return text; } + + isClosed(): boolean { + return this.vscodeRun === undefined; + } + get request(): vscode.TestRunRequest | undefined { + return ( + this.options?.request ?? (isJestTestRun(this.parentRun) ? this.parentRun.request : undefined) + ); + } + + private updateState = (f: (pRun: ParentRun) => void): void => { + if (!this.parentRun || !this.vscodeRun) { + throw new Error(`run "${this.name}" has already closed`); + } + f(this.parentRun); + }; + + // TestRunProtocol + public get name(): string | undefined { + return this.options?.name; + } + public enqueued = (test: vscode.TestItem): void => { + this.updateState((pRun) => pRun.enqueued(test)); + }; + public started = (test: vscode.TestItem): void => { + this.updateState((pRun) => pRun.started(test)); + }; + public errored = ( + test: vscode.TestItem, + message: vscode.TestMessage | readonly vscode.TestMessage[], + duration?: number | undefined + ): void => { + this.updateState((pRun) => pRun.errored(test, message, duration)); + }; + public failed = ( + test: vscode.TestItem, + message: vscode.TestMessage | readonly vscode.TestMessage[], + duration?: number | undefined + ): void => { + this.updateState((pRun) => pRun.failed(test, message, duration)); + }; + public passed = (test: vscode.TestItem, duration?: number | undefined): void => { + this.updateState((pRun) => pRun.passed(test, duration)); + }; + public skipped = (test: vscode.TestItem): void => { + this.updateState((pRun) => pRun.skipped(test)); + }; + public end = (): void => { + if (this.options?.end) { + return this.options.end(); + } + + if (this.parentRun) { + this.parentRun.end(); + if (isVscodeRun(this.parentRun)) { + this.parentRun = undefined; + } + } + + this.options?.onEnd?.(); + }; } diff --git a/src/test-provider/test-provider.ts b/src/test-provider/test-provider.ts index b62e5194c..8cfee368d 100644 --- a/src/test-provider/test-provider.ts +++ b/src/test-provider/test-provider.ts @@ -115,7 +115,7 @@ export class JestTestProvider { } } error = error ?? `item ${tData.item.id} is not debuggable`; - run.vscodeRun.errored(tData.item, new vscode.TestMessage(error)); + run.errored(tData.item, new vscode.TestMessage(error)); run.write(error, 'error'); return Promise.resolve(); }; @@ -138,7 +138,7 @@ export class JestTestProvider { for (const test of tests) { const tData = this.context.getData(test); if (!tData || cancelToken.isCancellationRequested) { - run.vscodeRun.skipped(test); + run.skipped(test); continue; } this.log('debug', `executing profile: "${request.profile.label}" for ${test.id}...`); @@ -148,16 +148,15 @@ export class JestTestProvider { promises.push( new Promise((resolve, reject) => { try { - const itemRun = new JestTestRun(this.context, run.vscodeRun, { + const itemRun = new JestTestRun(this.context, run, { item: test, - onEnd: resolve, - disableVscodeRunEnd: true, + end: resolve, }); tData.scheduleTest(itemRun); } catch (e) { const msg = `failed to schedule test for ${tData.item.id}: ${toErrorString(e)}`; this.log('error', msg, e); - run.vscodeRun.errored(test, new vscode.TestMessage(msg)); + run.errored(test, new vscode.TestMessage(msg)); reject(msg); } }) diff --git a/tests/test-provider/test-item-data.test.ts b/tests/test-provider/test-item-data.test.ts index e9deb3763..99cf488ec 100644 --- a/tests/test-provider/test-item-data.test.ts +++ b/tests/test-provider/test-item-data.test.ts @@ -7,6 +7,8 @@ jest.unmock('../test-helper'); jest.unmock('./test-helper'); jest.unmock('../../src/errors'); +import { JestTestRun } from '../../src/test-provider/test-provider-helper'; + jest.mock('path', () => { let sep = '/'; return { @@ -58,26 +60,94 @@ const getChildItem = (item: vscode.TestItem, partialId: string): vscode.TestItem }; const mockScheduleProcess = (context) => { - const process = { id: 'whatever', request: { type: 'all-tests' } }; + const process: any = { id: 'whatever', request: { type: 'watch-tests' } }; context.ext.session.scheduleProcess.mockImplementation((request) => { process.request = request; return process; }); return process; }; + describe('test-item-data', () => { let context; let jestRun; let runEndSpy; let controllerMock; + let runMock; + + const createTestRun = (opt?: any): [any, jest.SpyInstance, any] => { + const run = context.createTestRun(opt?.request ?? {}, opt); + const endSpy = jest.spyOn(run, 'end'); + const runMock = controllerMock.lastRunMock(); + return [run, endSpy, runMock]; + }; + const prepareTestResult = (): void => { + const assertions = []; + assertions.push( + helper.makeAssertion('test-a', 'KnownFail', [], [1, 0], { + message: 'test error', + }) + ); + + const assertionContainer = buildAssertionContainer(assertions); + const testSuiteResult: any = { + status: 'KnownFail', + message: 'test file failed', + assertionContainer, + }; + context.ext.testResolveProvider.getTestSuiteResult.mockReturnValue(testSuiteResult); + }; + + const setupTestEnv = () => { + const file = '/ws-1/tests/a.test.ts'; + context.ext.testResolveProvider.getTestList.mockReturnValueOnce([file]); + const wsRoot = new WorkspaceRoot(context); + const onRunEvent = context.ext.sessionEvents.onRunEvent.event.mock.calls[0][0]; + + // build out the test item tree + prepareTestResult(); + + // triggers testSuiteChanged event listener + context.ext.testResolveProvider.events.testSuiteChanged.event.mock.calls[0][0]({ + type: 'assertions-updated', + process: { id: 'whatever', request: { type: 'watch-tests' } }, + files: [file], + }); + + const folder = getChildItem(wsRoot.item, 'tests'); + const testFile = getChildItem(folder, 'a.test.ts'); + const testBlock = getChildItem(testFile, 'test-a'); + + const scheduleItem = (type: string) => { + const getItem = () => { + switch (type) { + case 'workspace': + return wsRoot.item; + case 'folder': + return folder; + case 'testFile': + return testFile; + case 'testBlock': + return testBlock; + } + }; + const item = getItem(); + const data = context.getData(item); + data.scheduleTest(jestRun); + controllerMock.createTestRun.mockClear(); + + return item; + }; + + return { wsRoot, folder, testFile, testBlock, onRunEvent, scheduleItem, file }; + }; beforeEach(() => { controllerMock = mockController(); const profiles: any = [{ tag: { id: 'run' } }, { tag: { id: 'debug' } }]; context = new JestTestProviderContext(mockExtExplorerContext('ws-1'), controllerMock, profiles); context.output.write = jest.fn((t) => t); - jestRun = context.createTestRun({}, { disableVscodeRunEnd: true }); - runEndSpy = jest.spyOn(jestRun, 'end'); + [jestRun, runEndSpy, runMock] = createTestRun(); vscode.Uri.joinPath = jest .fn() @@ -137,16 +207,19 @@ describe('test-item-data', () => { context.ext.testResolveProvider.getTestList.mockReturnValue(['/ws-1/a.test.ts']); const wsRoot = new WorkspaceRoot(context); wsRoot.discoverTest(jestRun); + expect(jestRun.isClosed()).toBeTruthy(); expect(wsRoot.item.children.size).toEqual(1); expect(wsRoot.item.canResolveChildren).toBe(false); expect(runEndSpy).toBeCalledTimes(1); // 2nd time if no test-file: testItems will not change context.ext.testResolveProvider.getTestList.mockReturnValue([]); + [jestRun, runEndSpy] = createTestRun(); wsRoot.discoverTest(jestRun); + expect(jestRun.isClosed()).toBeTruthy(); expect(wsRoot.item.children.size).toEqual(1); expect(wsRoot.item.canResolveChildren).toBe(false); - expect(runEndSpy).toBeCalledTimes(2); + expect(runEndSpy).toBeCalledTimes(1); }); }); it('will only discover up to the test file level', () => { @@ -187,6 +260,7 @@ describe('test-item-data', () => { // now remove '/ws-1/tests2/b.test.ts' and rediscover testFiles.length = 1; + [jestRun, runEndSpy] = createTestRun(); wsRoot.discoverTest(jestRun); expect(wsRoot.item.children.size).toEqual(1); folderItem = wsRoot.item.children.get('/ws-1/tests2'); @@ -271,21 +345,26 @@ describe('test-item-data', () => { // triggers testSuiteChanged event listener context.ext.testResolveProvider.events.testSuiteChanged.event.mock.calls[0][0]({ type: 'assertions-updated', - process: { id: 'whatever', request: {} }, + process: { id: 'whatever', request: { type: 'watch-tests' } }, files: ['/ws-1/a.test.ts'], }); const runMock2 = controllerMock.lastRunMock(); + expect(runMock2).not.toBe(runMock); expect(wsRoot.item.children.size).toBe(1); const docItem = getChildItem(wsRoot.item, 'a.test.ts'); expect(docItem).not.toBeUndefined(); - expect(runMock2.failed).toHaveBeenCalledWith(docItem, { - message: testSuiteResult.message, - }); + expect(runMock2.failed).not.toHaveBeenCalledWith( + docItem, + { + message: testSuiteResult.message, + }, + undefined + ); expect(docItem.children.size).toEqual(1); const tItem = getChildItem(docItem, 'test-a'); expect(tItem).not.toBeUndefined(); - expect(runMock2.failed).toHaveBeenCalledWith(tItem, { message: a1.message }); + expect(runMock2.failed).toHaveBeenCalledWith(tItem, { message: a1.message }, undefined); expect(tItem.range).toEqual({ args: [1, 0, 1, 0] }); expect(runMock2.end).toBeCalled(); @@ -314,7 +393,7 @@ describe('test-item-data', () => { // after jest test run, result suite should be updated and test block should be populated context.ext.testResolveProvider.events.testSuiteChanged.event.mock.calls[0][0]({ type: 'assertions-updated', - process: { id: 'whatever', request: {} }, + process: { id: 'whatever', request: { type: 'watch-tests' } }, files: ['/ws-1/a.test.ts'], }); expect(docItem.children.size).toEqual(1); @@ -452,7 +531,7 @@ describe('test-item-data', () => { expect(docRoot.item.children.size).toEqual(1); const tData = context.getData(getChildItem(docRoot.item, 'test-1')); expect(tData instanceof TestData).toBeTruthy(); - expect(jestRun.vscodeRun.passed).toBeCalledWith(tData.item); + expect(runMock.passed).toBeCalledWith(tData.item, undefined); }); it('if no test suite result yet, children list is empty', () => { context.ext.testResolveProvider.getTestSuiteResult.mockReturnValue(undefined); @@ -539,8 +618,9 @@ describe('test-item-data', () => { const parent: any = controllerMock.createTestItem('ws-1', 'ws-1', { fsPath: '/ws-1' }); const docRoot = new TestDocumentRoot(context, { fsPath: '/ws-1/a.test.ts' } as any, parent); expect(docRoot.scheduleTest(jestRun)).toBeUndefined(); - expect(jestRun.vscodeRun.errored).toBeCalledWith(docRoot.item, expect.anything()); - expect(runEndSpy).toBeCalled(); + expect(runMock.errored).toBeCalledWith(docRoot.item, expect.anything(), undefined); + expect(runMock.end).toBeCalled(); + expect(jestRun.isClosed()).toBeTruthy(); }); it('schedule request will contain itemRun info', () => { const parent: any = controllerMock.createTestItem('ws-1', 'ws-1', { fsPath: '/ws-1' }); @@ -593,15 +673,15 @@ describe('test-item-data', () => { const dItem = getChildItem(wsRoot.item, 'a.test.ts'); expect(dItem.children.size).toBe(2); const tItem = getChildItem(dItem, 'test-a'); - expect(runMock.passed).toBeCalledWith(tItem); + expect(runMock.passed).toBeCalledWith(tItem, undefined); expect(runMock.end).toBeCalledTimes(1); }); it('for exporer-triggered runs, only the resolve function will be invoked', () => { // simulate an internal run has been scheduled const process = mockScheduleProcess(context); - const jestRun = context.createTestRun({}, { disableVscodeRunEnd: true }); - const runEndSpy = jest.spyOn(jestRun, 'end'); + const customEnd = jest.fn(); + [jestRun, runEndSpy, runMock] = createTestRun({ end: customEnd }); controllerMock.createTestRun.mockClear(); wsRoot.scheduleTest(jestRun); @@ -625,8 +705,8 @@ describe('test-item-data', () => { const dItem = getChildItem(wsRoot.item, 'a.test.ts'); expect(dItem.children.size).toBe(2); const tItem = getChildItem(dItem, 'test-a'); - expect(jestRun.vscodeRun.passed).toBeCalledWith(tItem); - expect(jestRun.vscodeRun.end).not.toBeCalled(); + expect(runMock.passed).toBeCalledWith(tItem, undefined); + expect(runMock.end).not.toBeCalled(); expect(runEndSpy).toBeCalled(); }); it.each` @@ -657,7 +737,7 @@ describe('test-item-data', () => { const dItem = getChildItem(wsRoot.item, 'a.test.ts'); const tItem = getChildItem(dItem, 'test-b'); - expect(jestRun.vscodeRun.failed).toBeCalledWith(tItem, expect.anything()); + expect(runMock.failed).toBeCalledWith(tItem, expect.anything(), undefined); if (hasLocation) { expect(vscode.TestMessage).toBeCalled(); } else { @@ -813,32 +893,32 @@ describe('test-item-data', () => { }); docRoot.discoverTest(jestRun); expect(docRoot.item.children.size).toEqual(2); - expect(jestRun.vscodeRun.failed).toBeCalledWith(docRoot.item, expect.anything()); + expect(runMock.failed).not.toBeCalledWith(docRoot.item, expect.anything(), undefined); const desc1 = getChildItem(docRoot.item, 'desc-1'); expect(desc1.children.size).toEqual(2); const t1 = getChildItem(desc1, 'desc-1 test-1'); expect(t1).not.toBeUndefined(); - expect(jestRun.vscodeRun.passed).toBeCalledWith(t1); + expect(runMock.passed).toBeCalledWith(t1, undefined); const t2 = getChildItem(desc1, 'desc-1 test-2'); expect(t2).not.toBeUndefined(); - expect(jestRun.vscodeRun.failed).toBeCalledWith(t2, expect.anything()); + expect(runMock.failed).toBeCalledWith(t2, expect.anything(), undefined); const desc2 = getChildItem(docRoot.item, 'desc-2'); const t3 = getChildItem(desc2, 'desc-2 test-3'); expect(t3).not.toBeUndefined(); - expect(jestRun.vscodeRun.passed).toBeCalledWith(t3); + expect(runMock.passed).toBeCalledWith(t3, undefined); const t4 = getChildItem(desc2, 'desc-2 test-4'); expect(t4).not.toBeUndefined(); - expect(jestRun.vscodeRun.skipped).toBeCalledWith(t4); + expect(runMock.skipped).toBeCalledWith(t4); }); it('delete', () => { // delete the only test -1 const assertionContainer = buildAssertionContainer([]); - context.ext.testResolveProvider.getTestSuiteResult.mockReturnValueOnce({ + context.ext.testResolveProvider.getTestSuiteResult.mockReturnValue({ status: 'Unknown', assertionContainer, }); @@ -855,15 +935,25 @@ describe('test-item-data', () => { docRoot.discoverTest(jestRun); expect(docRoot.item.children.size).toEqual(1); - expect(jestRun.vscodeRun.failed).toBeCalledWith(docRoot.item, expect.anything()); + expect(runMock.failed).not.toBeCalledWith(docRoot.item, expect.anything(), undefined); const t2 = getChildItem(docRoot.item, 'test-2'); expect(t2).not.toBeUndefined(); - expect(jestRun.vscodeRun.failed).toBeCalledWith(t2, expect.anything()); + expect(runMock.failed).toBeCalledWith(t2, expect.anything(), undefined); + }); + it('with syntax error', () => { + const assertionContainer = buildAssertionContainer([]); + context.ext.testResolveProvider.getTestSuiteResult.mockReturnValue({ + status: 'KnownFail', + assertionContainer, + }); + docRoot.discoverTest(jestRun); + expect(docRoot.item.children.size).toEqual(0); + expect(runMock.failed).toBeCalledWith(docRoot.item, expect.anything(), undefined); }); describe('duplicate test names', () => { const setup = (assertions) => { - jestRun.vscodeRun.passed.mockClear(); - jestRun.vscodeRun.failed.mockClear(); + runMock.passed.mockClear(); + runMock.failed.mockClear(); const assertionContainer = buildAssertionContainer(assertions); context.ext.testResolveProvider.getTestSuiteResult.mockReturnValue({ @@ -877,29 +967,29 @@ describe('test-item-data', () => { setup([a2, a3]); docRoot.discoverTest(jestRun); expect(docRoot.item.children.size).toEqual(2); - expect(jestRun.vscodeRun.failed).toBeCalledWith(docRoot.item, expect.anything()); + expect(runMock.failed).not.toBeCalledWith(docRoot.item, expect.anything(), undefined); const items = []; docRoot.item.children.forEach((item) => items.push(item)); expect(items[0].id).not.toEqual(items[1].id); items.forEach((item) => expect(item.id).toEqual(expect.stringContaining('test-1'))); - expect(jestRun.vscodeRun.failed).toBeCalledTimes(2); - expect(jestRun.vscodeRun.passed).toBeCalledTimes(1); + expect(runMock.failed).toBeCalledTimes(1); + expect(runMock.passed).toBeCalledTimes(1); }); it('can still sync with test results', () => { const a2 = helper.makeAssertion('test-1', 'KnownFail', [], [1, 0]); const a3 = helper.makeAssertion('test-1', 'KnownSuccess', [], [1, 0]); setup([a2, a3]); docRoot.discoverTest(jestRun); - expect(jestRun.vscodeRun.failed).toBeCalledTimes(2); - expect(jestRun.vscodeRun.passed).toBeCalledTimes(1); + expect(runMock.failed).toBeCalledTimes(1); + expect(runMock.passed).toBeCalledTimes(1); //update a2 status a2.status = 'KnownSuccess'; setup([a2, a3]); docRoot.discoverTest(jestRun); - expect(jestRun.vscodeRun.failed).toBeCalledTimes(1); - expect(jestRun.vscodeRun.passed).toBeCalledTimes(2); + expect(runMock.failed).toBeCalledTimes(0); + expect(runMock.passed).toBeCalledTimes(2); }); }); }); @@ -961,60 +1051,15 @@ describe('test-item-data', () => { expect(jestRun.vscodeRun.appendOutput).toBeCalledWith(expect.stringContaining(coloredText)); }); describe('handle run event to set item status and show output', () => { - const file = '/ws-1/tests/a.test.ts'; - let wsRoot, folder, testFile, testBlock, onRunEvent; + let env; beforeEach(() => { - context.ext.testResolveProvider.getTestList.mockReturnValueOnce([file]); - wsRoot = new WorkspaceRoot(context); - onRunEvent = context.ext.sessionEvents.onRunEvent.event.mock.calls[0][0]; - - // build out the test item tree - const a1 = helper.makeAssertion('test-a', 'KnownFail', [], [1, 0], { - message: 'test error', - }); - const assertionContainer = buildAssertionContainer([a1]); - const testSuiteResult: any = { - status: 'KnownFail', - message: 'test file failed', - assertionContainer, - }; - context.ext.testResolveProvider.getTestSuiteResult.mockReturnValue(testSuiteResult); - - // triggers testSuiteChanged event listener - context.ext.testResolveProvider.events.testSuiteChanged.event.mock.calls[0][0]({ - type: 'assertions-updated', - process: { id: 'whatever', request: {} }, - files: [file], - }); - - folder = getChildItem(wsRoot.item, 'tests'); - testFile = getChildItem(folder, 'a.test.ts'); - testBlock = getChildItem(testFile, 'test-a'); + env = setupTestEnv(); }); describe('explorer-triggered runs', () => { - const setup = (type: string) => { - const getItem = () => { - switch (type) { - case 'workspace': - return wsRoot.item; - case 'folder': - return folder; - case 'testFile': - return testFile; - case 'testBlock': - return testBlock; - } - }; - const item = getItem(); - const data = context.getData(item); - data.scheduleTest(jestRun); - controllerMock.createTestRun.mockClear(); - - return item; - }; let process; beforeEach(() => { process = mockScheduleProcess(context); + [jestRun, runEndSpy, runMock] = createTestRun({ end: jest.fn() }); }); describe.each` itemType @@ -1024,20 +1069,20 @@ describe('test-item-data', () => { ${'testBlock'} `('will use run passed from explorer throughout for $targetItem item', ({ itemType }) => { it('item will be enqueued after schedule', () => { - const item = setup(itemType); + const item = env.scheduleItem(itemType); expect(process.request.run.vscodeRun.enqueued).toBeCalledWith(item); }); it('item will show started when jest run started', () => { - const item = setup(itemType); + const item = env.scheduleItem(itemType); process.request.run.vscodeRun.enqueued.mockClear(); // scheduled event has no effect - onRunEvent({ type: 'scheduled', process }); + env.onRunEvent({ type: 'scheduled', process }); expect(process.request.run.vscodeRun.enqueued).not.toBeCalled(); // starting the process - onRunEvent({ type: 'start', process }); + env.onRunEvent({ type: 'start', process }); expect(process.request.run.item).toBe(item); expect(process.request.run.vscodeRun.started).toBeCalledWith(item); @@ -1054,10 +1099,10 @@ describe('test-item-data', () => { `( 'can output process data: case $case', ({ text, raw, newLine, isError, outputText, outputOptions }) => { - setup(itemType); + env.scheduleItem(itemType); - onRunEvent({ type: 'start', process }); - onRunEvent({ type: 'data', process, text, raw, newLine, isError }); + env.onRunEvent({ type: 'start', process }); + env.onRunEvent({ type: 'data', process, text, raw, newLine, isError }); expect(controllerMock.createTestRun).not.toBeCalled(); expect(context.output.write).toBeCalledWith(outputText, outputOptions); @@ -1069,28 +1114,28 @@ describe('test-item-data', () => { { type: 'exit', error: 'something is wrong', code: 127 }, { type: 'exit', error: 'something is wrong', code: 1 }, ])("will only resolve the promise and not close the run for event '%s'", (event) => { - setup(itemType); - onRunEvent({ type: 'start', process }); + env.scheduleItem(itemType); + env.onRunEvent({ type: 'start', process }); expect(controllerMock.createTestRun).not.toBeCalled(); expect(process.request.run.vscodeRun.started).toBeCalled(); - onRunEvent({ ...event, process }); + env.onRunEvent({ ...event, process }); expect(process.request.run.vscodeRun.end).not.toBeCalled(); expect(runEndSpy).toBeCalled(); }); it('can report exit error even if run is ended', () => { - setup(itemType); + env.scheduleItem(itemType); - onRunEvent({ type: 'start', process }); - onRunEvent({ type: 'end', process }); + env.onRunEvent({ type: 'start', process }); + env.onRunEvent({ type: 'end', process }); expect(controllerMock.createTestRun).not.toBeCalled(); expect(process.request.run.vscodeRun.end).not.toBeCalled(); expect(runEndSpy).toBeCalled(); const error = 'something is wrong'; - onRunEvent({ type: 'exit', error, process }); + env.onRunEvent({ type: 'exit', error, process }); // no new run need to be created expect(controllerMock.createTestRun).not.toBeCalled(); @@ -1101,6 +1146,7 @@ describe('test-item-data', () => { }); }); describe('extension-managed runs', () => { + const file = '/ws-1/tests/a.test.ts'; beforeEach(() => { controllerMock.createTestRun.mockClear(); }); @@ -1115,15 +1161,15 @@ describe('test-item-data', () => { `('will create a new run and use it throughout: $request', ({ request, withFile }) => { it('if run starts before schedule returns: no enqueue', () => { const process = { id: 'whatever', request }; - const item = withFile ? testFile : wsRoot.item; + const item = withFile ? env.testFile : env.wsRoot.item; // starting the process - onRunEvent({ type: 'start', process }); + env.onRunEvent({ type: 'start', process }); const runMock = controllerMock.lastRunMock(); expect(runMock.started).toBeCalledWith(item); //followed by scheduled - onRunEvent({ type: 'scheduled', process }); + env.onRunEvent({ type: 'scheduled', process }); // run has already started, do nothing, expect(runMock.enqueued).not.toBeCalled(); @@ -1132,16 +1178,16 @@ describe('test-item-data', () => { }); it('if run starts after schedule: show enqueue then start', () => { const process = { id: 'whatever', request }; - const item = withFile ? testFile : wsRoot.item; + const item = withFile ? env.testFile : env.wsRoot.item; //scheduled - onRunEvent({ type: 'scheduled', process }); + env.onRunEvent({ type: 'scheduled', process }); expect(controllerMock.createTestRun).toBeCalledTimes(1); const runMock = controllerMock.lastRunMock(); expect(runMock.enqueued).toBeCalledWith(item); // followed by starting process - onRunEvent({ type: 'start', process }); + env.onRunEvent({ type: 'start', process }); expect(runMock.started).toBeCalledWith(item); //will create 1 new run @@ -1159,8 +1205,8 @@ describe('test-item-data', () => { ({ text, raw, newLine, isError, outputText, outputOptions }) => { const process = { id: 'whatever', request }; - onRunEvent({ type: 'start', process }); - onRunEvent({ type: 'data', process, text, raw, newLine, isError }); + env.onRunEvent({ type: 'start', process }); + env.onRunEvent({ type: 'data', process, text, raw, newLine, isError }); expect(controllerMock.createTestRun).toBeCalledTimes(1); expect(context.output.write).toBeCalledWith(outputText, outputOptions); @@ -1168,26 +1214,26 @@ describe('test-item-data', () => { ); it.each([['end'], ['exit']])("close the run on event '%s'", (eventType) => { const process = { id: 'whatever', request: { type: 'all-tests' } }; - onRunEvent({ type: 'start', process }); + env.onRunEvent({ type: 'start', process }); expect(controllerMock.createTestRun).toBeCalledTimes(1); const runMock = controllerMock.lastRunMock(); expect(runMock.started).toBeCalled(); expect(runMock.end).not.toBeCalled(); - onRunEvent({ type: eventType, process }); + env.onRunEvent({ type: eventType, process }); expect(runMock.end).toBeCalled(); }); it('can report exit error even if run is ended', () => { const process = { id: 'whatever', request: { type: 'all-tests' } }; - onRunEvent({ type: 'start', process }); - onRunEvent({ type: 'end', process }); + env.onRunEvent({ type: 'start', process }); + env.onRunEvent({ type: 'end', process }); expect(controllerMock.createTestRun).toBeCalledTimes(1); const runMock = controllerMock.lastRunMock(); expect(runMock.end).toBeCalled(); const error = 'something is wrong'; - onRunEvent({ type: 'exit', error, process }); + env.onRunEvent({ type: 'exit', error, process }); expect(controllerMock.createTestRun).toBeCalledTimes(2); const runMock2 = controllerMock.lastRunMock(); @@ -1198,12 +1244,12 @@ describe('test-item-data', () => { }); it('if WorkspaceRoot is disposed before process end, all pending run will be closed', () => { const process = { id: 'whatever', request: { type: 'all-tests' } }; - onRunEvent({ type: 'start', process }); + env.onRunEvent({ type: 'start', process }); expect(controllerMock.createTestRun).toBeCalledTimes(1); const runMock = controllerMock.lastRunMock(); - wsRoot.dispose(); + env.wsRoot.dispose(); expect(runMock.end).toBeCalled(); }); }); @@ -1217,7 +1263,7 @@ describe('test-item-data', () => { const process = { id: 'whatever', request }; // starting the process - onRunEvent({ type: 'start', process }); + env.onRunEvent({ type: 'start', process }); const runMock = controllerMock.lastRunMock(); expect(runMock.started).not.toBeCalled(); @@ -1228,20 +1274,24 @@ describe('test-item-data', () => { }); it('scheduled and start events will do deep item status update', () => { const process = mockScheduleProcess(context); - const testFileData = context.getData(testFile); + const testFileData = context.getData(env.testFile); testFileData.scheduleTest(jestRun); expect(jestRun.vscodeRun.enqueued).toBeCalledTimes(2); - [testFile, testBlock].forEach((t) => expect(jestRun.vscodeRun.enqueued).toBeCalledWith(t)); + [env.testFile, env.testBlock].forEach((t) => + expect(jestRun.vscodeRun.enqueued).toBeCalledWith(t) + ); - onRunEvent({ type: 'start', process }); + env.onRunEvent({ type: 'start', process }); expect(jestRun.vscodeRun.started).toBeCalledTimes(2); - [testFile, testBlock].forEach((t) => expect(jestRun.vscodeRun.started).toBeCalledWith(t)); + [env.testFile, env.testBlock].forEach((t) => + expect(jestRun.vscodeRun.started).toBeCalledWith(t) + ); }); it('log long-run event', () => { const process = mockScheduleProcess(context); - onRunEvent({ type: 'long-run', threshold: 60000, process }); + env.onRunEvent({ type: 'long-run', threshold: 60000, process }); expect(context.output.write).toBeCalledTimes(1); expect(context.output.write).toBeCalledWith( expect.stringContaining('60000'), @@ -1250,4 +1300,117 @@ describe('test-item-data', () => { }); }); }); + describe('simulate complete run flow', () => { + let env; + beforeEach(() => { + env = setupTestEnv(); + }); + describe('testExplorer managed run', () => { + let pRun, runRequest, notifyProvider, createTestRunSpy; + beforeEach(() => { + notifyProvider = jest.fn(); + runRequest = {}; + [pRun] = createTestRun({ request: runRequest }); + jestRun = new JestTestRun(context, pRun, { end: notifyProvider }); + createTestRunSpy = jest.spyOn(context, 'createTestRun'); + }); + it('run explicit test block', () => { + const process: any = mockScheduleProcess(context); + const item = env.scheduleItem('testBlock'); + expect(process.request.run).toBe(jestRun); + expect(process.request.run.vscodeRun.enqueued).toBeCalledWith(item); + expect(process.request.run.item).toBe(item); + + //end the process: will not actually end the run but to only notify the provider + env.onRunEvent({ type: 'end', process }); + expect(process.request.run.isClosed()).toBeFalsy(); + expect(notifyProvider).toBeCalled(); + + //the run ends before results come in, the process's run should reflect it + pRun.end(); + expect(jestRun.isClosed()).toBeTruthy(); + expect(process.request.run.isClosed()).toBeTruthy(); + + // prepare for result processing + controllerMock.createTestRun.mockClear(); + createTestRunSpy.mockClear(); + + // triggers testSuiteChanged event listener + context.ext.testResolveProvider.events.testSuiteChanged.event.mock.calls[0][0]({ + type: 'assertions-updated', + process, + files: [env.file], + }); + + // expect the item status to be updated in a new run since the previous one is closed + expect(controllerMock.createTestRun).toBeCalledTimes(1); + expect(controllerMock.createTestRun).toBeCalledWith(runRequest, expect.anything()); + runMock = controllerMock.lastRunMock(); + expect(runMock.failed).toBeCalledWith(item, expect.anything(), undefined); + expect(runMock.failed).not.toBeCalledWith(env.testFile, expect.anything(), undefined); + + // the new testRun should still have the same item and request + expect(createTestRunSpy).toBeCalledTimes(1); + const aRequest = createTestRunSpy.mock.calls[0][0]; + const option = createTestRunSpy.mock.calls[0][1]; + expect(aRequest).toBe(runRequest); + expect(option.item.id).toEqual(item.id); + }); + }); + describe('extension managed autoRun', () => { + let createTestRunSpy; + beforeEach(() => { + createTestRunSpy = jest.spyOn(context, 'createTestRun'); + }); + it('wawtch-test run', () => { + const request: any = { type: 'watch-tests' }; + const process = { id: 'whatever', request }; + const item = env.wsRoot.item; + + // starting the process + env.onRunEvent({ type: 'start', process }); + + expect(createTestRunSpy).toBeCalledTimes(1); + let opt = createTestRunSpy.mock.calls[0][1]; + expect(opt.item.id).toEqual(item.id); + + runMock = controllerMock.lastRunMock(); + expect(runMock.started).toBeCalledWith(item); + expect(process.request.run).toBeUndefined(); + + createTestRunSpy.mockClear(); + + //received output: no new run should be created + const text = 'some data'; + env.onRunEvent({ type: 'data', process, text, raw: text }); + expect(createTestRunSpy).not.toBeCalled(); + expect(context.output.write).toBeCalledWith(text, undefined); + + createTestRunSpy.mockClear(); + + //end the run + env.onRunEvent({ type: 'end', process }); + expect(runMock.end).toBeCalled(); + + // prepare for result processing + createTestRunSpy.mockClear(); + + // process test results + context.ext.testResolveProvider.events.testSuiteChanged.event.mock.calls[0][0]({ + type: 'assertions-updated', + process, + files: [env.file], + }); + + // expect the item status to be updated in a new run since the previous one is closed + expect(createTestRunSpy).toBeCalledTimes(1); + opt = createTestRunSpy.mock.calls[0][1]; + expect(opt.item.id).toEqual(item.id); + + runMock = controllerMock.lastRunMock(); + expect(runMock.failed).toBeCalledWith(env.testBlock, expect.anything(), undefined); + expect(runMock.failed).not.toBeCalledWith(env.testFile, expect.anything(), undefined); + }); + }); + }); }); diff --git a/tests/test-provider/test-provider-helper.test.ts b/tests/test-provider/test-provider-helper.test.ts new file mode 100644 index 000000000..bd2b954ba --- /dev/null +++ b/tests/test-provider/test-provider-helper.test.ts @@ -0,0 +1,65 @@ +jest.unmock('../../src/test-provider/test-provider-helper'); +jest.unmock('../../src/test-provider/test-provider-helper'); +jest.unmock('./test-helper'); + +// import * as vscode from 'vscode'; +import { JestTestRun } from '../../src/test-provider/test-provider-helper'; +import { JestTestProviderContext } from '../../src/test-provider/test-provider-helper'; +import { mockController, mockExtExplorerContext } from './test-helper'; + +describe('JestTestRun', () => { + let context, controllerMock, vRun, item; + beforeEach(() => { + jest.resetAllMocks(); + controllerMock = mockController(); + const profiles: any = [{ tag: { id: 'run' } }, { tag: { id: 'debug' } }]; + context = new JestTestProviderContext(mockExtExplorerContext('ws-1'), controllerMock, profiles); + vRun = controllerMock.createTestRun({}, 'whatever'); + item = {}; + }); + it('can deetect status update after run is closed', () => { + const jestRun = new JestTestRun(context, vRun); + jestRun.enqueued(item); + expect(vRun.enqueued).toBeCalled(); + + jestRun.passed(item); + expect(vRun.passed).toBeCalled(); + + // end the run + jestRun.end(); + expect(jestRun.isClosed()).toBeTruthy(); + expect(vRun.end).toBeCalled(); + + //update state now should throw exception + expect(() => jestRun.passed(item)).toThrow(); + }); + describe('can chain JestTestRun backed by a single vscode Run', () => { + let jestRun1, jestRun2, request; + beforeEach(() => { + request = {}; + jestRun1 = new JestTestRun(context, vRun, { request }); + jestRun2 = new JestTestRun(context, jestRun1); + }); + + it('both are backed by the same vscode.TestRun', () => { + expect(jestRun1.vscodeRun).toBe(jestRun2.vscodeRun); + }); + it('request attribute is a deep attribute', () => { + expect(jestRun1.request).toBe(request); + expect(jestRun2.request).toBe(request); + }); + it('close the top of the chain will close the underlying vscodeRun and mark isClose() state', () => { + jestRun2.end(); + + expect(jestRun2.isClosed()).toBeTruthy(); + expect(jestRun1.isClosed()).toBeTruthy(); + expect(jestRun2.vscodeRun).toBeUndefined(); + expect(jestRun1.vscodeRun).toBeUndefined(); + }); + it('after close other attributes are still accessible', () => { + jestRun2.end(); + expect(jestRun1.request).toBe(request); + expect(jestRun2.request).toBe(request); + }); + }); +}); diff --git a/tests/test-provider/test-provider.test.ts b/tests/test-provider/test-provider.test.ts index 7d14e0c33..bdcd3e627 100644 --- a/tests/test-provider/test-provider.test.ts +++ b/tests/test-provider/test-provider.test.ts @@ -135,9 +135,7 @@ describe('JestTestProvider', () => { controllerMock.resolveHandler(); expect(controllerMock.createTestRun).toBeCalled(); expect(workspaceRootMock.discoverTest).toBeCalledTimes(1); - expect(workspaceRootMock.discoverTest).toBeCalledWith( - expect.objectContaining({ vscodeRun: controllerMock.lastRunMock() }) - ); + // run will be created with the controller's id expect(controllerMock.lastRunMock().name).toEqual( expect.stringContaining(controllerMock.id) @@ -153,9 +151,7 @@ describe('JestTestProvider', () => { data.item.canResolveChildren = true; controllerMock.resolveHandler(data.item); expect(controllerMock.createTestRun).toBeCalled(); - expect(data.discoverTest).toBeCalledWith( - expect.objectContaining({ vscodeRun: controllerMock.lastRunMock() }) - ); + // run will be created with the controller's id expect(controllerMock.lastRunMock().name).toEqual( expect.stringContaining(controllerMock.id) @@ -269,7 +265,8 @@ describe('JestTestProvider', () => { if (hasError) { expect(controllerMock.lastRunMock().errored).toBeCalledWith( itemDataList[0].item, - expect.anything() + expect.anything(), + undefined ); expect(vscode.TestMessage).toBeCalledTimes(1); } else { @@ -369,8 +366,8 @@ describe('JestTestProvider', () => { expect(extExplorerContextMock.debugTests).toBeCalledTimes(3); expect(runMock.errored).toBeCalledTimes(2); - expect(runMock.errored).toBeCalledWith(request.include[1], expect.anything()); - expect(runMock.errored).toBeCalledWith(request.include[2], expect.anything()); + expect(runMock.errored).toBeCalledWith(request.include[1], expect.anything(), undefined); + expect(runMock.errored).toBeCalledWith(request.include[2], expect.anything(), undefined); expect(runMock.end).toBeCalled(); }); }); @@ -414,7 +411,7 @@ describe('JestTestProvider', () => { switch (state) { case 'errored': - expect(runMock.errored).toBeCalledWith(tData.item, expect.anything()); + expect(runMock.errored).toBeCalledWith(tData.item, expect.anything(), undefined); expect(vscode.TestMessage).toBeCalledTimes(1); break; case 'skipped': @@ -524,9 +521,9 @@ describe('JestTestProvider', () => { /* eslint-disable jest/no-conditional-expect */ if (idx === 1) { - expect(run.vscodeRun.errored).toBeCalledWith(d.item, expect.anything()); + expect(run.vscodeRun.errored).toBeCalledWith(d.item, expect.anything(), undefined); } else { - expect(run.vscodeRun.errored).not.toBeCalledWith(d.item, expect.anything()); + expect(run.vscodeRun.errored).not.toBeCalledWith(d.item, expect.anything(), undefined); // close the schedule run.end(); }