From 0fb11686b4305ab437beabe0b84fd860ae8bec1c Mon Sep 17 00:00:00 2001 From: connectdotz Date: Sat, 12 Nov 2022 18:06:39 -0500 Subject: [PATCH 01/16] add snapshot-provider --- src/TestResults/snapshot-provider.ts | 57 ++++++++++++++++++++++++++++ src/test-provider/test-provider.ts | 8 ++++ 2 files changed, 65 insertions(+) create mode 100644 src/TestResults/snapshot-provider.ts diff --git a/src/TestResults/snapshot-provider.ts b/src/TestResults/snapshot-provider.ts new file mode 100644 index 00000000..4f94fbb5 --- /dev/null +++ b/src/TestResults/snapshot-provider.ts @@ -0,0 +1,57 @@ +import { Snapshot, SnapshotMetadata } from 'jest-editor-support'; + +export type SnapshotStatus = 'exists' | 'missing' | 'inline'; +export interface SnapshotInfo { + status: SnapshotStatus; + metadata: SnapshotMetadata; +} +export class SnapshotProvider { + private cache: Map; + private snapshots: Snapshot; + + constructor() { + this.snapshots = new Snapshot(undefined, [ + 'toMatchInlineSnapshot', + 'toThrowErrorMatchingInlineSnapshot', + ]); + this.cache = new Map(); + } + + private getSnapshotStatus(snapshot: SnapshotMetadata): SnapshotStatus { + if (snapshot.exists) { + return 'exists'; + } + if (snapshot.name.includes('inline')) { + return 'inline'; + } + return 'missing'; + } + public getSuiteSnapshots(testPath: string): Promise { + const infoList = this.cache.get(testPath); + if (infoList) { + return Promise.resolve(infoList); + } + return this.parse(testPath); + } + public removeSuiteSnapshots(testPath: string): void { + this.cache.delete(testPath); + } + private async parse(testPath: string): Promise { + try { + const metadataList = await this.snapshots.getMetadataAsync(testPath); + const infoList = metadataList.map((metadata) => ({ + status: this.getSnapshotStatus(metadata), + metadata, + })); + this.cache.set(testPath, infoList); + return infoList; + } catch (e) { + console.warn('[SnapshotProvider] getMetadataAsync failed:', e); + this.cache.delete(testPath); + return []; + } + } + public resetCache(): void { + this.cache.clear(); + } +} diff --git a/src/test-provider/test-provider.ts b/src/test-provider/test-provider.ts index 3f7d2f55..f1e9d425 100644 --- a/src/test-provider/test-provider.ts +++ b/src/test-provider/test-provider.ts @@ -65,6 +65,7 @@ export class JestTestProvider { }; private createProfiles = (controller: vscode.TestController): vscode.TestRunProfile[] => { const runTag = new vscode.TestTag('run'); + // const updateSnapshotTag = new vscode.TestTag('update-snapshot'); const debugTag = new vscode.TestTag('debug'); const profiles = [ controller.createRunProfile( @@ -74,6 +75,13 @@ export class JestTestProvider { true, runTag ), + controller.createRunProfile( + 'run-update-snapshot', + vscode.TestRunProfileKind.Run, + this.runTests, + false, + runTag + ), controller.createRunProfile( 'debug', vscode.TestRunProfileKind.Debug, From 4774cfadfeef178a2784240dd1f9d9fa6d68a8e7 Mon Sep 17 00:00:00 2001 From: connectdotz Date: Mon, 14 Nov 2022 15:34:32 -0500 Subject: [PATCH 02/16] added snapshot profile, menu and commands --- package.json | 11 ++ src/JestExt/core.ts | 4 + src/TestResults/TestResultProvider.ts | 20 +++ src/TestResults/snapshot-provider.ts | 59 +++----- src/TestResults/test-result-events.ts | 9 +- src/extensionManager.ts | 27 ++++ .../test-item-context-manager.ts | 143 +++++++++++++++--- src/test-provider/test-item-data.ts | 77 ++++++++-- src/test-provider/test-provider-helper.ts | 4 +- src/test-provider/test-provider.ts | 20 +-- src/test-provider/types.ts | 8 +- 11 files changed, 295 insertions(+), 87 deletions(-) diff --git a/package.json b/package.json index f25533e6..28da9f96 100644 --- a/package.json +++ b/package.json @@ -299,6 +299,11 @@ "command": "io.orta.jest.test-item.coverage.toggle-on", "title": "Toggle Coverage On", "icon": "$(color-mode)" + }, + { + "command": "io.orta.jest.test-item.view-snapshot", + "title": "View snapshot", + "icon": "$(camera)" } ], "menus": { @@ -364,6 +369,12 @@ "group": "inline", "when": "testId in jest.coverage.off" } + ], + "testing/item/gutter": [ + { + "command": "io.orta.jest.test-item.view-snapshot", + "when": "testId in jest.editor-view-snapshot" + } ] }, "keybindings": [ diff --git a/src/JestExt/core.ts b/src/JestExt/core.ts index 64f04baf..82e9d396 100644 --- a/src/JestExt/core.ts +++ b/src/JestExt/core.ts @@ -631,6 +631,10 @@ export class JestExt { // restart jest since coverage condition has changed this.triggerUpdateSettings(this.extContext.settings); } + viewSnapshot(_testItem: vscode.TestItem): void { + //TODO implement this + console.warn('viewSnapshot not yet implemented'); + } enableLoginShell(): void { if (this.extContext.settings.shell.useLoginShell) { return; diff --git a/src/TestResults/TestResultProvider.ts b/src/TestResults/TestResultProvider.ts index 5a7a4ea5..60670271 100644 --- a/src/TestResults/TestResultProvider.ts +++ b/src/TestResults/TestResultProvider.ts @@ -15,6 +15,7 @@ import { emptyTestStats } from '../helpers'; import { createTestResultEvents, TestResultEvents } from './test-result-events'; import { ContainerNode } from './match-node'; import { JestProcessInfo } from '../JestProcessManagement'; +import { SnapshotNode, SnapshotProvider } from './snapshot-provider'; export interface TestSuiteResult { status: TestReconciliationStateType; @@ -22,6 +23,7 @@ export interface TestSuiteResult { assertionContainer?: ContainerNode; results?: TestResult[]; sorted?: SortedTestResults; + snapshotNodes?: SnapshotNode[]; } export interface SortedTestResults { fail: TestResult[]; @@ -42,12 +44,14 @@ export class TestResultProvider { private reconciler: TestReconciler; private testSuites: Map; private testFiles?: string[]; + private snapshotProvider: SnapshotProvider; constructor(extEvents: JestSessionEvents, verbose = false) { this.reconciler = new TestReconciler(); this.verbose = verbose; this.events = createTestResultEvents(); this.testSuites = new Map(); + this.snapshotProvider = new SnapshotProvider(); extEvents.onTestSessionStarted.event(this.onSessionStart.bind(this)); } @@ -181,6 +185,7 @@ export class TestResultProvider { try { const parseResult = parse(filePath); this.testSuites.set(filePath, this.matchResults(filePath, parseResult)); + this.parseSnapshots(filePath); return this.testSuites.get(filePath)?.results; } catch (e) { const message = `failed to get test results for ${filePath}`; @@ -288,4 +293,19 @@ export class TestResultProvider { } return stats; } + + // snapshot support + private async parseSnapshots(testPath: string): Promise { + const snapshotSuite = await this.snapshotProvider.parse(testPath); + const suiteResult = this.testSuites.get(testPath); + if (suiteResult) { + suiteResult.snapshotNodes = snapshotSuite.nodes; + this.events.testSuiteChanged.fire({ + type: 'snapshot-suite-changed', + testPath, + }); + } else { + console.warn(`snapshots are ready but there is no test result record for ${testPath}:`); + } + } } diff --git a/src/TestResults/snapshot-provider.ts b/src/TestResults/snapshot-provider.ts index 4f94fbb5..f52b0d63 100644 --- a/src/TestResults/snapshot-provider.ts +++ b/src/TestResults/snapshot-provider.ts @@ -1,57 +1,42 @@ import { Snapshot, SnapshotMetadata } from 'jest-editor-support'; export type SnapshotStatus = 'exists' | 'missing' | 'inline'; -export interface SnapshotInfo { - status: SnapshotStatus; + +export interface SnapshotNode { + isInline: boolean; + //TODO refactor jest-e-ditor-support to split metadata and api metadata: SnapshotMetadata; } +export interface SnapshotSuite { + testPath: string; + nodes: SnapshotNode[]; +} +export interface SnapshotResult { + status: SnapshotStatus; + content?: string; +} + +const inlineKeys = ['toMatchInlineSnapshot', 'toThrowErrorMatchingInlineSnapshot']; export class SnapshotProvider { - private cache: Map; private snapshots: Snapshot; constructor() { - this.snapshots = new Snapshot(undefined, [ - 'toMatchInlineSnapshot', - 'toThrowErrorMatchingInlineSnapshot', - ]); - this.cache = new Map(); + this.snapshots = new Snapshot(undefined, inlineKeys); } - private getSnapshotStatus(snapshot: SnapshotMetadata): SnapshotStatus { - if (snapshot.exists) { - return 'exists'; - } - if (snapshot.name.includes('inline')) { - return 'inline'; - } - return 'missing'; - } - public getSuiteSnapshots(testPath: string): Promise { - const infoList = this.cache.get(testPath); - if (infoList) { - return Promise.resolve(infoList); - } - return this.parse(testPath); - } - public removeSuiteSnapshots(testPath: string): void { - this.cache.delete(testPath); - } - private async parse(testPath: string): Promise { + public async parse(testPath: string): Promise { try { const metadataList = await this.snapshots.getMetadataAsync(testPath); - const infoList = metadataList.map((metadata) => ({ - status: this.getSnapshotStatus(metadata), + const nodes = metadataList.map((metadata) => ({ + // TODO use the node.name instead + isInline: inlineKeys.find((key) => metadata.name.includes(key)) ? true : false, metadata, })); - this.cache.set(testPath, infoList); - return infoList; + const snapshotSuite = { testPath, nodes }; + return snapshotSuite; } catch (e) { console.warn('[SnapshotProvider] getMetadataAsync failed:', e); - this.cache.delete(testPath); - return []; + return { testPath, nodes: [] }; } } - public resetCache(): void { - this.cache.clear(); - } } diff --git a/src/TestResults/test-result-events.ts b/src/TestResults/test-result-events.ts index 6fb20635..d5e49d4c 100644 --- a/src/TestResults/test-result-events.ts +++ b/src/TestResults/test-result-events.ts @@ -3,13 +3,20 @@ import * as vscode from 'vscode'; import { JestProcessInfo } from '../JestProcessManagement'; import { ContainerNode } from './match-node'; -export type TestSuiteChangeReason = 'assertions-updated' | 'result-matched'; +export type TestSuiteChangeReason = + | 'assertions-updated' + | 'result-matched' + | 'snapshot-suite-changed'; export type TestSuitChangeEvent = | { type: 'assertions-updated'; process: JestProcessInfo; files: string[]; } + | { + type: 'snapshot-suite-changed'; + testPath: string; + } | { type: 'result-matched'; file: string; diff --git a/src/extensionManager.ts b/src/extensionManager.ts index 565a8238..eb164ec0 100644 --- a/src/extensionManager.ts +++ b/src/extensionManager.ts @@ -58,6 +58,12 @@ export type RegisterCommand = // eslint-disable-next-line @typescript-eslint/no-explicit-any callback: (extension: JestExt, ...args: any[]) => any; } + | { + type: 'workspace-test-item'; + name: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + callback: (extension: JestExt, testItem: vscode.TestItem, ...args: any[]) => any; + } | { type: 'active-text-editor' | 'active-text-editor-workspace'; name: string; @@ -69,6 +75,7 @@ const CommandPrefix: Record = { 'all-workspaces': `${extensionName}`, 'select-workspace': `${extensionName}.workspace`, workspace: `${extensionName}.with-workspace`, + 'workspace-test-item': `${extensionName}.with-workspace-test-item`, 'active-text-editor': `${extensionName}.editor`, 'active-text-editor-workspace': `${extensionName}.editor.workspace`, }; @@ -234,6 +241,17 @@ export class ExtensionManager { } ); } + case 'workspace-test-item': { + return vscode.commands.registerCommand( + commandName, + async (workspace: vscode.WorkspaceFolder, testItem: vscode.TestItem, ...args) => { + const extension = this.getByName(workspace.name); + if (extension) { + command.callback.call(thisArg, extension, testItem, ...args); + } + } + ); + } case 'active-text-editor': case 'active-text-editor-workspace': { return vscode.commands.registerTextEditorCommand( @@ -408,6 +426,15 @@ export class ExtensionManager { }, }), + // with-workspace-test-item commands + this.registerCommand({ + type: 'workspace-test-item', + name: 'view-snapshot', + callback: (extension, testItem) => { + extension.viewSnapshot(testItem); + }, + }), + // setup tool vscode.commands.registerCommand(`${extensionName}.setup-extension`, this.startWizard), diff --git a/src/test-provider/test-item-context-manager.ts b/src/test-provider/test-item-context-manager.ts index 37189f73..77a1e52b 100644 --- a/src/test-provider/test-item-context-manager.ts +++ b/src/test-provider/test-item-context-manager.ts @@ -6,15 +6,86 @@ import { extensionName } from '../appGlobals'; * TestExplorer menu when-condition */ -export type TEItemContextKey = 'jest.autoRun' | 'jest.coverage'; +// export type TEItemContextKey = +// | 'jest.autoRun' +// | 'jest.coverage' +// | 'jest.editor-view-snapshot'; -export interface ItemContext { - workspace: vscode.WorkspaceFolder; - key: TEItemContextKey; - /** the current value of the itemId */ - value: boolean; - itemIds: string[]; -} +// export interface ItemContext { +// key: TEItemContextKey; +// workspace: vscode.WorkspaceFolder; +// /** the current value of the itemId */ +// value: boolean; +// itemIds: string[]; +// } + +// interface DocumentTreeContext { +// key: 'jest.editor-view-snapshot' | 'jest.editor-update-snapshot'; +// workspace: vscode.WorkspaceFolder; +// value: boolean; +// // in top-down order, e.g. [root, folder, document] +// documentTreeItemIds: string[]; +// itemIds: Set; +// } +// type DocumentTree = Map>; +// interface DocumentTreeInfo { +// workspace: vscode.WorkspaceFolder; +// tree: DocumentTree; +// itemIds: Set; +// } +// class WorkspaceDocumentTree { +// private cache: Map; +// constructor() { +// this.cache = new Map(); +// } +// private addNode(tree: DocumentTree, context: DocumentTreeContext) { +// for (let i = 0; i < context.documentTreeItemIds.length - 2; i++) { +// const itemId = context.documentTreeItemIds[i]; +// const childId = context.documentTreeItemIds[i + 1]; +// const children = tree.get(itemId); +// if (!children) { +// tree.set(itemId, new Set([childId])); +// } else { +// children.add(childId); +// } +// } +// } +// private removeNode(tree: DocumentTree, context: DocumentTreeContext) {} + +// addContext(context: DocumentTreeContext): void { +// const wsList = this.cache.get(context.key); +// let tree: DocumentTree; +// if (!wsList) { +// tree = new Map(); +// this.cache.set(context.key, [{ workspace: context.workspace, tree, itemIds: new Set() }]); +// } else { +// const wsInfo = wsList.find( +// (info) => info.workspace.uri.fsPath === context.workspace.uri.fsPath +// ); +// tree = wsInfo?.tree ?? new Map(); +// if (!wsInfo?.tree) { +// wsList.push({ workspace: context.workspace, tree, itemIds: new Set() }); +// } else { +// } +// } +// this.addNode(tree, context); +// } +// } + +export type ItemContext = + | { + key: 'jest.autoRun' | 'jest.coverage'; + workspace: vscode.WorkspaceFolder; + /** the current value of the itemId */ + value: boolean; + itemIds: string[]; + } + | { + key: 'jest.editor-view-snapshot'; + workspace: vscode.WorkspaceFolder; + itemIds: string[]; + }; +export type TEItemContextKey = ItemContext['key']; export class TestItemContextManager { private cache = new Map(); @@ -24,20 +95,37 @@ export class TestItemContextManager { } public setItemContext(context: ItemContext): void { console.log(`setItemContext for context=`, context); - let list = this.cache.get(context.key); - if (!list) { - list = [context]; - } else { - list = list.filter((c) => c.workspace.name !== context.workspace.name).concat(context); - } - this.cache.set(context.key, list); - //set context for both on and off - let itemIds = list.filter((c) => c.value === true).flatMap((c) => c.itemIds); - vscode.commands.executeCommand('setContext', this.contextKey(context.key, true), itemIds); + switch (context.key) { + case 'jest.autoRun': + case 'jest.coverage': { + let list = this.cache.get(context.key); + if (!list) { + list = [context]; + } else { + // itemIds are not accumulated, but toggled + list = list.filter((c) => c.workspace.name !== context.workspace.name).concat(context); + } + this.cache.set(context.key, list); + + //set context for both on and off + let itemIds = list + .filter((c) => c.key === context.key && c.value === true) + .flatMap((c) => c.itemIds as string[]); + vscode.commands.executeCommand('setContext', this.contextKey(context.key, true), itemIds); - itemIds = list.filter((c) => c.value === false).flatMap((c) => c.itemIds); - vscode.commands.executeCommand('setContext', this.contextKey(context.key, false), itemIds); + itemIds = list + .filter((c) => c.key === context.key && c.value === false) + .flatMap((c) => c.itemIds as string[]); + vscode.commands.executeCommand('setContext', this.contextKey(context.key, false), itemIds); + break; + } + case 'jest.editor-view-snapshot': { + this.cache.set(context.key, [context]); + vscode.commands.executeCommand('setContext', context.key, context.itemIds); + break; + } + } } private getWorkspace( key: TEItemContextKey, @@ -71,7 +159,20 @@ export class TestItemContextManager { } }) ); - return [...autoRunCommands, ...coverageCommands]; + const viewSnapshotCommand = vscode.commands.registerCommand( + `${extensionName}.test-item.view-snapshot`, + (testItem: vscode.TestItem) => { + const workspace = this.getWorkspace('jest.editor-view-snapshot', testItem); + if (workspace) { + vscode.commands.executeCommand( + `${extensionName}.with-workspace-test-item.view-snapshot`, + workspace, + testItem + ); + } + } + ); + return [...autoRunCommands, ...coverageCommands, viewSnapshotCommand]; } } diff --git a/src/test-provider/test-item-data.ts b/src/test-provider/test-item-data.ts index e829827b..927d749c 100644 --- a/src/test-provider/test-item-data.ts +++ b/src/test-provider/test-item-data.ts @@ -8,11 +8,12 @@ import { ItBlock, TestAssertionStatus } from 'jest-editor-support'; import { ContainerNode, DataNode, NodeType, ROOT_NODE_NAME } from '../TestResults/match-node'; import { Logging } from '../logging'; import { TestSuitChangeEvent } from '../TestResults/test-result-events'; -import { Debuggable, TestItemData } from './types'; +import { Debuggable, TestItemData, TestTagId } from './types'; import { JestTestProviderContext, JestTestRun, JestTestRunOptions } from './test-provider-helper'; import { JestProcessInfo, JestProcessRequest } from '../JestProcessManagement'; import { GENERIC_ERROR, getExitErrorDef, LONG_RUNNING_TESTS } from '../errors'; import { JestExtOutput } from '../JestExt/output-terminal'; +import { tiContextManager } from './test-item-context-manager'; interface JestRunable { getJestRunRequest: () => JestExtRequestType; @@ -52,8 +53,8 @@ abstract class TestItemDataBase implements TestItemData, JestRunable, WithUri { item.children.forEach((child) => this.deepItemState(child, setState)); } - scheduleTest(run: JestTestRun): void { - const jestRequest = this.getJestRunRequest(); + scheduleTest(run: JestTestRun, runProfile?: vscode.TestRunProfile): void { + const jestRequest = this.getJestRunRequest(runProfile); run.item = this.item; this.deepItemState(this.item, run.enqueued); @@ -70,7 +71,7 @@ abstract class TestItemDataBase implements TestItemData, JestRunable, WithUri { } } - abstract getJestRunRequest(): JestExtRequestType; + abstract getJestRunRequest(runProfile?: vscode.TestRunProfile): JestExtRequestType; } /** @@ -98,7 +99,7 @@ export class WorkspaceRoot extends TestItemDataBase { this.context.ext.workspace.uri, this, undefined, - ['run'] + ['run', 'update-snapshot'] ); item.description = `(${this.context.ext.settings.autoRun.mode})`; @@ -106,12 +107,13 @@ export class WorkspaceRoot extends TestItemDataBase { return item; } - getJestRunRequest(): JestExtRequestType { + getJestRunRequest(runProfile?: vscode.TestRunProfile): JestExtRequestType { const transform = (request: JestProcessRequest) => { request.schedule.queue = 'blocking-2'; return request; }; - return { type: 'all-tests', transform }; + const updateSnapshot = runProfile?.tag?.id === TestTagId.UpdateSnapshot; + return { type: 'all-tests', updateSnapshot, transform }; } discoverTest(run: JestTestRun): void { const testList = this.context.ext.testResolveProvider.getTestList(); @@ -245,10 +247,36 @@ export class WorkspaceRoot extends TestItemDataBase { this.addTestFile(event.file, (testRoot) => testRoot.discoverTest(undefined, event.testContainer) ); + break; + } + case 'snapshot-suite-changed': { + this.onSnapshotResult(event.testPath); + break; } } }; + private onSnapshotResult(testPath: string) { + // const inlineSnapshotItems = []; + const snapshotItems: vscode.TestItem[] = []; + const docRoot = this.testDocuments.get(testPath); + const suiteResult = this.context.ext.testResolveProvider.getTestSuiteResult(testPath); + if (docRoot && suiteResult) { + suiteResult.snapshotNodes?.forEach((node) => { + if (node.isInline) { + return; + } + const testItems = findItemByLine(node.metadata.node.loc.start.line - 1, docRoot.item); + snapshotItems.push(...testItems); + }); + } + tiContextManager.setItemContext({ + workspace: this.context.ext.workspace, + key: 'jest.editor-view-snapshot', + itemIds: snapshotItems.map((item) => item.id), + }); + } + /** 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 @@ -398,14 +426,19 @@ export class FolderData extends TestItemDataBase { } private createTestItem(name: string, parent: vscode.TestItem) { const uri = FolderData.makeUri(parent, name); - const item = this.context.createTestItem(uri.fsPath, name, uri, this, parent, ['run']); + const item = this.context.createTestItem(uri.fsPath, name, uri, this, parent, [ + 'run', + 'update-snapshot', + ]); item.canResolveChildren = false; return item; } - getJestRunRequest(): JestExtRequestType { + getJestRunRequest(runProfile?: vscode.TestRunProfile): JestExtRequestType { + const updateSnapshot = runProfile?.tag?.id === TestTagId.UpdateSnapshot; return { type: 'by-file-pattern', + updateSnapshot, testFileNamePattern: this.uri.fsPath, }; } @@ -600,12 +633,14 @@ export class TestDocumentRoot extends TestResultData { ); } - getJestRunRequest = (): JestExtRequestType => { + getJestRunRequest(runProfile?: vscode.TestRunProfile): JestExtRequestType { + const updateSnapshot = runProfile?.tag?.id === TestTagId.UpdateSnapshot; return { type: 'by-file-pattern', + updateSnapshot, testFileNamePattern: this.uri.fsPath, }; - }; + } getDebugInfo(): ReturnType { return { fileName: this.uri.fsPath }; @@ -616,6 +651,22 @@ export class TestDocumentRoot extends TestResultData { ); }; } +const findItemByLine = (zeroBasedLine: number, item: vscode.TestItem): vscode.TestItem[] => { + const found: vscode.TestItem[] = []; + const range = item.range; + if (range && (range.start.line > zeroBasedLine || range.end.line < zeroBasedLine)) { + return []; + } + if (range && range.start.line <= zeroBasedLine && range.end.line >= zeroBasedLine) { + found.push(item); + } + + item.children.forEach((child) => { + found.push(...findItemByLine(zeroBasedLine, child)); + }); + + return found; +}; export class TestData extends TestResultData implements Debuggable { constructor( readonly context: JestTestProviderContext, @@ -642,9 +693,11 @@ export class TestData extends TestResultData implements Debuggable { return item; } - getJestRunRequest(): JestExtRequestType { + getJestRunRequest(runProfile?: vscode.TestRunProfile): JestExtRequestType { + const updateSnapshot = runProfile?.tag?.id === TestTagId.UpdateSnapshot; return { type: 'by-file-test-pattern', + updateSnapshot, testFileNamePattern: this.uri.fsPath, testNamePattern: this.node.fullName, }; diff --git a/src/test-provider/test-provider-helper.ts b/src/test-provider/test-provider-helper.ts index 3a8f5e83..64b81a00 100644 --- a/src/test-provider/test-provider-helper.ts +++ b/src/test-provider/test-provider-helper.ts @@ -9,7 +9,7 @@ import { JestExtExplorerContext, TestItemData } from './types'; * as well as factory functions to create TestItem and TestRun that could impact the state */ -export type TagIdType = 'run' | 'debug'; +export type TagIdType = 'run' | 'debug' | 'update-snapshot'; let RunSeq = 0; export class JestTestProviderContext { @@ -32,7 +32,7 @@ export class JestTestProviderContext { uri: vscode.Uri, data: TestItemData, parent?: vscode.TestItem, - tagIds: TagIdType[] = ['run', 'debug'] + tagIds: TagIdType[] = ['run', 'debug', 'update-snapshot'] ): vscode.TestItem => { const testItem = this.controller.createTestItem(id, label, uri); this.testItemData.set(testItem, data); diff --git a/src/test-provider/test-provider.ts b/src/test-provider/test-provider.ts index f1e9d425..be168149 100644 --- a/src/test-provider/test-provider.ts +++ b/src/test-provider/test-provider.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; import { JestTestProviderContext, JestTestRun } from './test-provider-helper'; import { WorkspaceRoot } from './test-item-data'; -import { Debuggable, JestExtExplorerContext, TestItemData } from './types'; +import { Debuggable, JestExtExplorerContext, TestItemData, TestTagId } from './types'; import { extensionId } from '../appGlobals'; import { Logging } from '../logging'; import { toErrorString } from '../helpers'; @@ -10,12 +10,6 @@ import { tiContextManager } from './test-item-context-manager'; // eslint-disable-next-line @typescript-eslint/no-explicit-any const isDebuggable = (arg: any): arg is Debuggable => arg && typeof arg.getDebugInfo === 'function'; -export const RunProfileInfo: Record = { - [vscode.TestRunProfileKind.Run]: 'run', - [vscode.TestRunProfileKind.Debug]: 'debug', - [vscode.TestRunProfileKind.Coverage]: 'run with coverage', -}; - export class JestTestProvider { private readonly controller: vscode.TestController; private context: JestTestProviderContext; @@ -64,9 +58,9 @@ export class JestTestProvider { return controller; }; private createProfiles = (controller: vscode.TestController): vscode.TestRunProfile[] => { - const runTag = new vscode.TestTag('run'); - // const updateSnapshotTag = new vscode.TestTag('update-snapshot'); - const debugTag = new vscode.TestTag('debug'); + const runTag = new vscode.TestTag(TestTagId.Run); + const updateSnapshotTag = new vscode.TestTag(TestTagId.UpdateSnapshot); + const debugTag = new vscode.TestTag(TestTagId.Debug); const profiles = [ controller.createRunProfile( 'run', @@ -76,11 +70,11 @@ export class JestTestProvider { runTag ), controller.createRunProfile( - 'run-update-snapshot', + 'update snapshot', vscode.TestRunProfileKind.Run, this.runTests, false, - runTag + updateSnapshotTag ), controller.createRunProfile( 'debug', @@ -179,7 +173,7 @@ export class JestTestProvider { item: test, end: resolve, }); - tData.scheduleTest(itemRun); + tData.scheduleTest(itemRun, request.profile); } catch (e) { const msg = `failed to schedule test for ${tData.item.id}: ${toErrorString(e)}`; this.log('error', msg, e); diff --git a/src/test-provider/types.ts b/src/test-provider/types.ts index a51d196c..8f0430de 100644 --- a/src/test-provider/types.ts +++ b/src/test-provider/types.ts @@ -18,9 +18,15 @@ export interface TestItemData { readonly uri: vscode.Uri; context: JestTestProviderContext; discoverTest?: (run: JestTestRun) => void; - scheduleTest: (run: JestTestRun) => void; + scheduleTest: (run: JestTestRun, runProfile?: vscode.TestRunProfile) => void; } export interface Debuggable { getDebugInfo: () => { fileName: string; testNamePattern?: string }; } + +export enum TestTagId { + Run = 'run', + UpdateSnapshot = 'update-snapshot', + Debug = 'debug', +} From 4aa3da72208bc6dc1c814e65a15d310068dd9ea4 Mon Sep 17 00:00:00 2001 From: connectdotz Date: Mon, 14 Nov 2022 15:49:37 -0500 Subject: [PATCH 03/16] remove listener update-snapshot request --- src/JestExt/process-listeners.ts | 44 ------------- src/JestExt/process-session.ts | 55 ++-------------- .../test-item-context-manager.ts | 66 ------------------- 3 files changed, 5 insertions(+), 160 deletions(-) diff --git a/src/JestExt/process-listeners.ts b/src/JestExt/process-listeners.ts index beb79c24..0bec35ca 100644 --- a/src/JestExt/process-listeners.ts +++ b/src/JestExt/process-listeners.ts @@ -175,7 +175,6 @@ export class ListTestFileListener extends AbstractProcessListener { } } -const SnapshotFailRegex = /(snapshots? failed)|(snapshot test failed)/i; const IS_OUTSIDE_REPOSITORY_REGEXP = /Test suite failed to run[\s\S]*fatal:[\s\S]*is outside repository/im; const WATCH_IS_NOT_SUPPORTED_REGEXP = @@ -271,47 +270,6 @@ export class RunTestListener extends AbstractProcessListener { return text.replace(CONTROL_MESSAGES, ''); } - // if snapshot error, offer update snapshot option and execute if user confirms - private handleSnapshotTestFailuer(process: JestProcess, data: string) { - // if already in the updateSnapshot run, do not prompt again - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if ((process.request as any).updateSnapshot) { - return; - } - - if ( - this.session.context.settings.enableSnapshotUpdateMessages && - SnapshotFailRegex.test(data) - ) { - const msg = - process.request.type === 'watch-all-tests' || process.request.type === 'watch-tests' - ? 'all files' - : 'files in this run'; - vscode.window - .showInformationMessage( - `[${this.session.context.workspace.name}] Would you like to update snapshots for ${msg}?`, - { - title: 'Replace them', - } - ) - .then((response) => { - // No response == cancel - if (response) { - this.session.scheduleProcess({ - type: 'update-snapshot', - baseRequest: process.request, - }); - this.onRunEvent.fire({ - type: 'data', - process, - text: 'Updating snapshots...', - newLine: true, - }); - } - }); - } - } - // restart the process with watch-all if it is due to "watch not supported" error private handleWatchNotSupportedError(process: JestProcess, data: string) { if (IS_OUTSIDE_REPOSITORY_REGEXP.test(data) || WATCH_IS_NOT_SUPPORTED_REGEXP.test(data)) { @@ -359,8 +317,6 @@ export class RunTestListener extends AbstractProcessListener { this.handleRunComplete(process, message); - this.handleSnapshotTestFailuer(process, message); - this.handleWatchNotSupportedError(process, message); } diff --git a/src/JestExt/process-session.ts b/src/JestExt/process-session.ts index 4869fb56..eb072b39 100644 --- a/src/JestExt/process-session.ts +++ b/src/JestExt/process-session.ts @@ -4,7 +4,6 @@ import { JestProcessRequestBase, ScheduleStrategy, requestString, - QueueType, JestProcessInfo, JestProcessRequestTransform, } from '../JestProcessManagement'; @@ -12,21 +11,16 @@ import { JestTestProcessType } from '../Settings'; import { RunTestListener, ListTestFileListener } from './process-listeners'; import { JestExtProcessContext } from './types'; -type InternalProcessType = 'list-test-files' | 'update-snapshot'; +type InternalProcessType = 'list-test-files'; export type ListTestFilesCallback = ( fileNames?: string[], error?: string, exitCode?: number ) => void; -export type InternalRequestBase = - | { - type: Extract; - onResult: ListTestFilesCallback; - } - | { - type: Extract; - baseRequest: JestProcessRequest; - }; +export type InternalRequestBase = { + type: Extract; + onResult: ListTestFilesCallback; +}; export type JestExtRequestType = JestProcessRequestBase | InternalRequestBase; const isJestProcessRequestBase = (request: JestExtRequestType): request is JestProcessRequestBase => @@ -122,32 +116,6 @@ export const createProcessSession = (context: JestExtProcessContext): ProcessSes }; const listenerSession: ListenerSession = { context, scheduleProcess }; - /** - * returns an update-snapshot process-request base on the current process - * @param process - * @returns undefined if the process already is updating snapshot - */ - const createSnapshotRequest = (baseRequest: JestProcessRequest): JestProcessRequestBase => { - switch (baseRequest.type) { - case 'watch-tests': - case 'watch-all-tests': - return { type: 'all-tests', updateSnapshot: true }; - case 'all-tests': - case 'by-file': - case 'by-file-pattern': - case 'by-file-test': - case 'by-file-test-pattern': - if (baseRequest.updateSnapshot) { - throw new Error( - 'schedule a update-snapshot run within an update-snapshot run is not supported' - ); - } - return { ...baseRequest, updateSnapshot: true }; - default: - throw new Error(`unexpeted baseRequest type for snapshot run: ${baseRequest.toString()}`); - } - }; - const createProcessRequest = (request: JestExtRequestType): JestProcessRequest => { const transform = (pRequest: JestProcessRequest): JestProcessRequest => { const t = getTransform(request); @@ -170,19 +138,6 @@ export const createProcessSession = (context: JestExtProcessContext): ProcessSes schedule, }); } - case 'update-snapshot': { - const snapshotRequest = createSnapshotRequest(request.baseRequest); - const schedule = { - ...ProcessScheduleStrategy[snapshotRequest.type], - queue: 'non-blocking' as QueueType, - }; - - return transform({ - ...snapshotRequest, - listener: new RunTestListener(lSession), - schedule, - }); - } case 'list-test-files': { const schedule = ProcessScheduleStrategy['not-test']; return transform({ diff --git a/src/test-provider/test-item-context-manager.ts b/src/test-provider/test-item-context-manager.ts index 77a1e52b..52569b21 100644 --- a/src/test-provider/test-item-context-manager.ts +++ b/src/test-provider/test-item-context-manager.ts @@ -6,72 +6,6 @@ import { extensionName } from '../appGlobals'; * TestExplorer menu when-condition */ -// export type TEItemContextKey = -// | 'jest.autoRun' -// | 'jest.coverage' -// | 'jest.editor-view-snapshot'; - -// export interface ItemContext { -// key: TEItemContextKey; -// workspace: vscode.WorkspaceFolder; -// /** the current value of the itemId */ -// value: boolean; -// itemIds: string[]; -// } - -// interface DocumentTreeContext { -// key: 'jest.editor-view-snapshot' | 'jest.editor-update-snapshot'; -// workspace: vscode.WorkspaceFolder; -// value: boolean; -// // in top-down order, e.g. [root, folder, document] -// documentTreeItemIds: string[]; -// itemIds: Set; -// } -// type DocumentTree = Map>; -// interface DocumentTreeInfo { -// workspace: vscode.WorkspaceFolder; -// tree: DocumentTree; -// itemIds: Set; -// } -// class WorkspaceDocumentTree { -// private cache: Map; -// constructor() { -// this.cache = new Map(); -// } -// private addNode(tree: DocumentTree, context: DocumentTreeContext) { -// for (let i = 0; i < context.documentTreeItemIds.length - 2; i++) { -// const itemId = context.documentTreeItemIds[i]; -// const childId = context.documentTreeItemIds[i + 1]; -// const children = tree.get(itemId); -// if (!children) { -// tree.set(itemId, new Set([childId])); -// } else { -// children.add(childId); -// } -// } -// } -// private removeNode(tree: DocumentTree, context: DocumentTreeContext) {} - -// addContext(context: DocumentTreeContext): void { -// const wsList = this.cache.get(context.key); -// let tree: DocumentTree; -// if (!wsList) { -// tree = new Map(); -// this.cache.set(context.key, [{ workspace: context.workspace, tree, itemIds: new Set() }]); -// } else { -// const wsInfo = wsList.find( -// (info) => info.workspace.uri.fsPath === context.workspace.uri.fsPath -// ); -// tree = wsInfo?.tree ?? new Map(); -// if (!wsInfo?.tree) { -// wsList.push({ workspace: context.workspace, tree, itemIds: new Set() }); -// } else { -// } -// } -// this.addNode(tree, context); -// } -// } - export type ItemContext = | { key: 'jest.autoRun' | 'jest.coverage'; From a4282ad6718e4df07124bdf6f7bd5f8ea5eac49d Mon Sep 17 00:00:00 2001 From: connectdotz Date: Thu, 17 Nov 2022 12:53:44 -0500 Subject: [PATCH 04/16] refactor TestResultProvider and removed enableSnapshotUpdateMessages --- README.md | 1 - src/JestExt/core.ts | 5 +- src/JestExt/helper.ts | 1 - src/JestExt/process-listeners.ts | 1 - src/Settings/index.ts | 1 - src/TestResults/TestResultProvider.ts | 271 ++++++++++++------ src/TestResults/match-by-context.ts | 5 +- src/TestResults/match-node.ts | 1 + src/TestResults/snapshot-provider.ts | 68 +++-- src/TestResults/test-result-events.ts | 11 +- src/extensionManager.ts | 27 -- .../test-item-context-manager.ts | 34 ++- src/test-provider/test-item-data.ts | 122 +++++--- tests/JestExt/helper.test.ts | 1 - tests/JestExt/process-listeners.test.ts | 82 ------ yarn.lock | 7 +- 16 files changed, 345 insertions(+), 293 deletions(-) diff --git a/README.md b/README.md index e79e1f8b..3e3d9313 100644 --- a/README.md +++ b/README.md @@ -258,7 +258,6 @@ Users can use the following settings to tailor the extension for their environme |enableInlineErrorMessages :x:| Whether errors should be reported inline on a file|--|This is now deprecated in favor of `jest.testExplorer` | |[testExplorer](#testexplorer) |Configure jest test explorer|null|`{"showInlineError": "true"}`| |**Snapshot**| -|enableSnapshotUpdateMessages|Whether snapshot update messages should show|true|`"jest.enableSnapshotUpdateMessages": false`| |enableSnapshotPreviews 💼|Whether snapshot previews should show|true|`"jest.enableSnapshotPreviews": false`| |restartJestOnSnapshotUpdate :x:| Restart Jest runner after updating the snapshots|false|This is no longer needed in v4| |**Coverage**| diff --git a/src/JestExt/core.ts b/src/JestExt/core.ts index 82e9d396..84a039e5 100644 --- a/src/JestExt/core.ts +++ b/src/JestExt/core.ts @@ -631,10 +631,7 @@ export class JestExt { // restart jest since coverage condition has changed this.triggerUpdateSettings(this.extContext.settings); } - viewSnapshot(_testItem: vscode.TestItem): void { - //TODO implement this - console.warn('viewSnapshot not yet implemented'); - } + enableLoginShell(): void { if (this.extContext.settings.shell.useLoginShell) { return; diff --git a/src/JestExt/helper.ts b/src/JestExt/helper.ts index fb600923..a33e3662 100644 --- a/src/JestExt/helper.ts +++ b/src/JestExt/helper.ts @@ -119,7 +119,6 @@ export const getExtensionResourceSettings = (uri: vscode.Uri): PluginResourceSet return { autoEnable, - enableSnapshotUpdateMessages: config.get('enableSnapshotUpdateMessages'), pathToConfig: config.get('pathToConfig'), jestCommandLine: config.get('jestCommandLine'), pathToJest: config.get('pathToJest'), diff --git a/src/JestExt/process-listeners.ts b/src/JestExt/process-listeners.ts index 0bec35ca..b88ed6f3 100644 --- a/src/JestExt/process-listeners.ts +++ b/src/JestExt/process-listeners.ts @@ -263,7 +263,6 @@ export class RunTestListener extends AbstractProcessListener { } //=== private methods === private shouldIgnoreOutput(text: string): boolean { - // this fails when snapshots change - to be revised - returning always false for now return text.length <= 0 || text.includes('Watch Usage'); } private cleanupOutput(text: string): string { diff --git a/src/Settings/index.ts b/src/Settings/index.ts index f324934e..09c98cf1 100644 --- a/src/Settings/index.ts +++ b/src/Settings/index.ts @@ -39,7 +39,6 @@ export type NodeEnv = ProjectWorkspace['nodeEnv']; export type MonitorLongRun = 'off' | number; export interface PluginResourceSettings { autoEnable?: boolean; - enableSnapshotUpdateMessages?: boolean; jestCommandLine?: string; pathToConfig?: string; pathToJest?: string; diff --git a/src/TestResults/TestResultProvider.ts b/src/TestResults/TestResultProvider.ts index 60670271..55e531d8 100644 --- a/src/TestResults/TestResultProvider.ts +++ b/src/TestResults/TestResultProvider.ts @@ -5,6 +5,8 @@ import { IParseResults, parse, TestAssertionStatus, + ParsedRange, + ItBlock, } from 'jest-editor-support'; import { TestReconciliationState, TestReconciliationStateType } from './TestReconciliationState'; import { TestResult, TestResultStatusInfo } from './TestResult'; @@ -13,18 +15,26 @@ import { JestSessionEvents } from '../JestExt'; import { TestStats } from '../types'; import { emptyTestStats } from '../helpers'; import { createTestResultEvents, TestResultEvents } from './test-result-events'; -import { ContainerNode } from './match-node'; +import { ContainerNode, ROOT_NODE_NAME } from './match-node'; import { JestProcessInfo } from '../JestProcessManagement'; -import { SnapshotNode, SnapshotProvider } from './snapshot-provider'; +import { ExtSnapshotBlock, SnapshotProvider } from './snapshot-provider'; -export interface TestSuiteResult { +type TestBlocks = IParseResults & { sourceContainer: ContainerNode }; +interface TestSuiteParseResultRaw { + testBlocks: TestBlocks | 'failed'; +} +interface TestSuiteResultRaw { + // test result status: TestReconciliationStateType; message: string; assertionContainer?: ContainerNode; results?: TestResult[]; sorted?: SortedTestResults; - snapshotNodes?: SnapshotNode[]; } + +export type TestSuiteResult = Readonly; +type TestSuiteUpdatable = Readonly; + export interface SortedTestResults { fail: TestResult[]; skip: TestResult[]; @@ -38,11 +48,122 @@ const sortByStatus = (a: TestResult, b: TestResult): number => { } return TestResultStatusInfo[a.status].precedence - TestResultStatusInfo[b.status].precedence; }; + +export class TestSuiteRecord implements TestSuiteUpdatable { + private _status: TestReconciliationStateType; + private _message: string; + private _results?: TestResult[]; + private _sorted?: SortedTestResults; + + private _testBlocks?: TestBlocks | 'failed'; + // private _snapshotBlocks?: ExtSnapshotBlock[] | 'failed'; + private _assertionContainer?: ContainerNode; + + constructor( + public testFile: string, + private snapshotProvider: SnapshotProvider, + private events: TestResultEvents, + private reconciler: TestReconciler, + private verbose = false + ) { + this._status = TestReconciliationState.Unknown; + this._message = ''; + } + public get status(): TestReconciliationStateType { + return this._status; + } + public get message(): string { + return this._message; + } + public get results(): TestResult[] | undefined { + return this._results; + } + public get sorted(): SortedTestResults | undefined { + return this._sorted; + } + + public get testBlocks(): TestBlocks | 'failed' { + if (!this._testBlocks) { + try { + const pResult = parse(this.testFile); + const sourceContainer = match.buildSourceContainer(pResult.root); + this._testBlocks = { ...pResult, sourceContainer }; + + const snapshotBlocks = this.snapshotProvider.parse(this.testFile).blocks; + if (snapshotBlocks.length > 0) { + this.updateSnapshotAttr(sourceContainer, snapshotBlocks); + } + + this.events.testSuiteChanged.fire({ + type: 'test-parsed', + file: this.testFile, + sourceContainer: sourceContainer, + }); + } catch (e) { + // normal to fail, for example when source file has syntax error + if (this.verbose) { + console.log(`parseTestBlocks failed for ${this.testFile}`, e); + } + this._testBlocks = 'failed'; + } + } + + return this._testBlocks; + } + + public get assertionContainer(): ContainerNode | undefined { + if (!this._assertionContainer) { + const assertions = this.reconciler.assertionsForTestFile(this.testFile); + if (assertions && assertions.length > 0) { + this._assertionContainer = match.buildAssertionContainer(assertions); + } + } + return this._assertionContainer; + } + + private updateSnapshotAttr( + container: ContainerNode, + snapshots: ExtSnapshotBlock[] + ): void { + const isWithin = (snapshot: ExtSnapshotBlock, range?: ParsedRange): boolean => { + const zeroBasedLine = snapshot.node.loc.start.line - 1; + return !!range && range.start.line <= zeroBasedLine && range.end.line >= zeroBasedLine; + }; + + if ( + container.name !== ROOT_NODE_NAME && + container.attrs.range && + !snapshots.find((s) => isWithin(s, container.attrs.range)) + ) { + return; + } + container.childData.forEach((block) => { + const snapshot = snapshots.find((s) => isWithin(s, block.attrs.range)); + if (snapshot) { + block.attrs.snapshot = snapshot.isInline ? 'inline' : 'external'; + } + }); + container.childContainers.forEach((childContainer) => + this.updateSnapshotAttr(childContainer, snapshots) + ); + } + + public update(change: Partial): void { + this._status = change.status ?? this.status; + this._message = change.message ?? this.message; + + this._results = 'results' in change ? change.results : this._results; + this._sorted = 'sorted' in change ? change.sorted : this._sorted; + this._testBlocks = 'testBlocks' in change ? change.testBlocks : this._testBlocks; + this._assertionContainer = + 'assertionContainer' in change ? change.assertionContainer : this._assertionContainer; + } +} export class TestResultProvider { verbose: boolean; events: TestResultEvents; private reconciler: TestReconciler; - private testSuites: Map; + private testSuites: Map; private testFiles?: string[]; private snapshotProvider: SnapshotProvider; @@ -60,6 +181,17 @@ export class TestResultProvider { this.events.testSuiteChanged.dispose(); } + private addTestSuiteRecord(testFile: string): TestSuiteRecord { + const record = new TestSuiteRecord( + testFile, + this.snapshotProvider, + this.events, + this.reconciler, + this.verbose + ); + this.testSuites.set(testFile, record); + return record; + } private onSessionStart(): void { this.testSuites.clear(); this.reconciler = new TestReconciler(); @@ -121,48 +253,49 @@ export class TestResultProvider { } public getTestSuiteResult(filePath: string): TestSuiteResult | undefined { - const cache = this.testSuites.get(filePath); - if (cache && !cache.assertionContainer) { - const assertions = this.reconciler.assertionsForTestFile(filePath); - if (assertions && assertions.length > 0) { - cache.assertionContainer = match.buildAssertionContainer(assertions); - this.testSuites.set(filePath, cache); - } - } - return cache; + return this.testSuites.get(filePath); } - private matchResults(filePath: string, { root, itBlocks }: IParseResults): TestSuiteResult { + + /** match assertions with source file, if successful, update cache.results and related. Will also fire testSuiteChanged event */ + private updateMatchedResults(filePath: string, record: TestSuiteRecord): void { let error: string | undefined; - try { - const cache = this.getTestSuiteResult(filePath); - if (cache?.assertionContainer) { - cache.results = this.groupByRange( - match.matchTestAssertions(filePath, root, cache.assertionContainer, this.verbose) + if (record.testBlocks === 'failed') { + record.update({ status: 'KnownFail', message: 'test file parse error', results: [] }); + return; + } + + const { itBlocks } = record.testBlocks; + if (record.assertionContainer) { + try { + const results = this.groupByRange( + match.matchTestAssertions( + filePath, + record.testBlocks.sourceContainer, + record.assertionContainer, + this.verbose + ) ); + record.update({ results }); + this.events.testSuiteChanged.fire({ type: 'result-matched', file: filePath, }); - return cache; + return; + } catch (e) { + console.warn(`failed to match test results for ${filePath}:`, e); + error = `encountered internal match error: ${e}`; } + } else { error = 'no assertion generated for file'; - } catch (e) { - console.warn(`failed to match test results for ${filePath}:`, e); - error = `encountered internal match error: ${e}`; } - this.events.testSuiteChanged.fire({ - type: 'test-parsed', - file: filePath, - testContainer: match.buildSourceContainer(root), - }); - // no need to do groupByRange as the source block will not have blocks under the same location - return { - status: 'Unknown', + record.update({ + status: 'KnownFail', message: error, results: itBlocks.map((t) => match.toMatchResult(t, 'no assertion found', 'match-failed')), - }; + }); } /** @@ -172,27 +305,18 @@ export class TestResultProvider { * In the case when file can not be parsed or match error, empty results will be returned. * @throws if parsing or matching internal error */ - getResults(filePath: string): TestResult[] | undefined { - const results = this.testSuites.get(filePath)?.results; - if (results) { - return results; + getResults(filePath: string, record?: TestSuiteRecord): TestResult[] | undefined { + const _record = record ?? this.testSuites.get(filePath) ?? this.addTestSuiteRecord(filePath); + if (_record.results) { + return _record.results; } if (this.isTestFile(filePath) === 'no') { return; } - try { - const parseResult = parse(filePath); - this.testSuites.set(filePath, this.matchResults(filePath, parseResult)); - this.parseSnapshots(filePath); - return this.testSuites.get(filePath)?.results; - } catch (e) { - const message = `failed to get test results for ${filePath}`; - console.warn(message, e); - this.testSuites.set(filePath, { status: 'KnownFail', message, results: [] }); - throw e; - } + this.updateMatchedResults(filePath, _record); + return _record.results; } /** @@ -203,16 +327,12 @@ export class TestResultProvider { */ getSortedResults(filePath: string): SortedTestResults | undefined { - const cached = this.testSuites.get(filePath)?.sorted; - if (cached) { - return cached; + const record = this.testSuites.get(filePath) ?? this.addTestSuiteRecord(filePath); + if (record.sorted) { + return record.sorted; } - if (this.isTestFile(filePath) === 'no') { - return; - } - - const result: SortedTestResults = { + const sorted: SortedTestResults = { fail: [], skip: [], success: [], @@ -220,38 +340,38 @@ export class TestResultProvider { }; try { - const testResults = this.getResults(filePath); + const testResults = this.getResults(filePath, record); if (!testResults) { return; } for (const test of testResults) { if (test.status === TestReconciliationState.KnownFail) { - result.fail.push(test); + sorted.fail.push(test); } else if (test.status === TestReconciliationState.KnownSkip) { - result.skip.push(test); + sorted.skip.push(test); } else if (test.status === TestReconciliationState.KnownSuccess) { - result.success.push(test); + sorted.success.push(test); } else { - result.unknown.push(test); + sorted.unknown.push(test); } } } finally { - const cached = this.testSuites.get(filePath); - if (cached) { - cached.sorted = result; - } + record.update({ sorted }); } - return result; + return sorted; } updateTestResults(data: JestTotalResults, process: JestProcessInfo): TestFileAssertionStatus[] { const results = this.reconciler.updateFileWithJestStatus(data); results?.forEach((r) => { - this.testSuites.set(r.file, { + const record = this.testSuites.get(r.file) ?? this.addTestSuiteRecord(r.file); + record.update({ status: r.status, message: r.message, - assertionContainer: r.assertions ? match.buildAssertionContainer(r.assertions) : undefined, + assertionContainer: undefined, + results: undefined, + sorted: undefined, }); }); this.events.testSuiteChanged.fire({ @@ -295,17 +415,8 @@ export class TestResultProvider { } // snapshot support - private async parseSnapshots(testPath: string): Promise { - const snapshotSuite = await this.snapshotProvider.parse(testPath); - const suiteResult = this.testSuites.get(testPath); - if (suiteResult) { - suiteResult.snapshotNodes = snapshotSuite.nodes; - this.events.testSuiteChanged.fire({ - type: 'snapshot-suite-changed', - testPath, - }); - } else { - console.warn(`snapshots are ready but there is no test result record for ${testPath}:`); - } + + public previewSnapshot(testPath: string, testFullName: string): Promise { + return this.snapshotProvider.previewSnapshot(testPath, testFullName); } } diff --git a/src/TestResults/match-by-context.ts b/src/TestResults/match-by-context.ts index 467c91fd..a6a2178e 100644 --- a/src/TestResults/match-by-context.ts +++ b/src/TestResults/match-by-context.ts @@ -197,6 +197,7 @@ const ContextMatch = (): ContextMatchAlgorithm => { tNode.addEvent(event); aNode.addEvent(event); aNode.attrs.range = tNode.attrs.range; + aNode.attrs.snapshot = tNode.attrs.snapshot; }; const onMatchResult = ( tNode: DataNode, @@ -455,11 +456,11 @@ const { match } = ContextMatch(); export const matchTestAssertions = ( fileName: string, - sourceRoot: ParsedNode, + source: ParsedNode | ContainerNode, assertions: TestAssertionStatus[] | ContainerNode, verbose = false ): TestResult[] => { - const tContainer = buildSourceContainer(sourceRoot); + const tContainer = source instanceof ParsedNode ? buildSourceContainer(source) : source; const aContainer = Array.isArray(assertions) ? buildAssertionContainer(assertions) : assertions; const messaging = createMessaging(fileName, verbose); diff --git a/src/TestResults/match-node.ts b/src/TestResults/match-node.ts index c8213ce7..297de994 100644 --- a/src/TestResults/match-node.ts +++ b/src/TestResults/match-node.ts @@ -57,6 +57,7 @@ export interface OptionalAttributes { nonLiteralName?: boolean; // zero-based location range range?: ParsedRange; + snapshot?: 'inline' | 'external'; } export class BaseNode { name: string; diff --git a/src/TestResults/snapshot-provider.ts b/src/TestResults/snapshot-provider.ts index f52b0d63..b7e74a32 100644 --- a/src/TestResults/snapshot-provider.ts +++ b/src/TestResults/snapshot-provider.ts @@ -1,42 +1,76 @@ -import { Snapshot, SnapshotMetadata } from 'jest-editor-support'; +import * as vscode from 'vscode'; +import { Snapshot, SnapshotBlock } from 'jest-editor-support'; export type SnapshotStatus = 'exists' | 'missing' | 'inline'; -export interface SnapshotNode { +export interface ExtSnapshotBlock extends SnapshotBlock { isInline: boolean; - //TODO refactor jest-e-ditor-support to split metadata and api - metadata: SnapshotMetadata; } export interface SnapshotSuite { testPath: string; - nodes: SnapshotNode[]; -} -export interface SnapshotResult { - status: SnapshotStatus; - content?: string; + blocks: ExtSnapshotBlock[]; } const inlineKeys = ['toMatchInlineSnapshot', 'toThrowErrorMatchingInlineSnapshot']; export class SnapshotProvider { private snapshots: Snapshot; + private panel?: vscode.WebviewPanel; constructor() { this.snapshots = new Snapshot(undefined, inlineKeys); } - public async parse(testPath: string): Promise { + public parse(testPath: string): SnapshotSuite { try { - const metadataList = await this.snapshots.getMetadataAsync(testPath); - const nodes = metadataList.map((metadata) => ({ - // TODO use the node.name instead - isInline: inlineKeys.find((key) => metadata.name.includes(key)) ? true : false, - metadata, + const sBlocks = this.snapshots.parse(testPath); + const blocks = sBlocks.map((block) => ({ + ...block, + isInline: inlineKeys.find((key) => block.node.name.includes(key)) ? true : false, })); - const snapshotSuite = { testPath, nodes }; + const snapshotSuite = { testPath, blocks }; return snapshotSuite; } catch (e) { console.warn('[SnapshotProvider] getMetadataAsync failed:', e); - return { testPath, nodes: [] }; + return { testPath, blocks: [] }; + } + } + public async getContent(testPath: string, testFullName: string): Promise { + return this.snapshots.getSnapshotContent(testPath, testFullName); + } + private escapeContent = (content: string) => { + if (content) { + const escaped = content + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(//g, '>'); + return `
${escaped}
`; + } + }; + public async previewSnapshot(testPath: string, testFullName: string): Promise { + const content = await this.getContent(testPath, testFullName); + if (!content) { + vscode.window.showErrorMessage('no snapshot is found, please run test to generate first'); + return; } + + if (this.panel) { + this.panel.reveal(); + } else { + this.panel = vscode.window.createWebviewPanel( + 'view_snapshot', + testFullName, + vscode.ViewColumn.Two, + {} + ); + + this.panel.onDidDispose(() => { + this.panel = undefined; + }); + } + + this.panel.webview.html = (content && this.escapeContent(content)) || ''; + this.panel.title = testFullName; } } diff --git a/src/TestResults/test-result-events.ts b/src/TestResults/test-result-events.ts index d5e49d4c..774a9867 100644 --- a/src/TestResults/test-result-events.ts +++ b/src/TestResults/test-result-events.ts @@ -3,20 +3,13 @@ import * as vscode from 'vscode'; import { JestProcessInfo } from '../JestProcessManagement'; import { ContainerNode } from './match-node'; -export type TestSuiteChangeReason = - | 'assertions-updated' - | 'result-matched' - | 'snapshot-suite-changed'; +export type TestSuiteChangeReason = 'assertions-updated' | 'result-matched'; export type TestSuitChangeEvent = | { type: 'assertions-updated'; process: JestProcessInfo; files: string[]; } - | { - type: 'snapshot-suite-changed'; - testPath: string; - } | { type: 'result-matched'; file: string; @@ -24,7 +17,7 @@ export type TestSuitChangeEvent = | { type: 'test-parsed'; file: string; - testContainer: ContainerNode; + sourceContainer: ContainerNode; }; // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types diff --git a/src/extensionManager.ts b/src/extensionManager.ts index eb164ec0..565a8238 100644 --- a/src/extensionManager.ts +++ b/src/extensionManager.ts @@ -58,12 +58,6 @@ export type RegisterCommand = // eslint-disable-next-line @typescript-eslint/no-explicit-any callback: (extension: JestExt, ...args: any[]) => any; } - | { - type: 'workspace-test-item'; - name: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - callback: (extension: JestExt, testItem: vscode.TestItem, ...args: any[]) => any; - } | { type: 'active-text-editor' | 'active-text-editor-workspace'; name: string; @@ -75,7 +69,6 @@ const CommandPrefix: Record = { 'all-workspaces': `${extensionName}`, 'select-workspace': `${extensionName}.workspace`, workspace: `${extensionName}.with-workspace`, - 'workspace-test-item': `${extensionName}.with-workspace-test-item`, 'active-text-editor': `${extensionName}.editor`, 'active-text-editor-workspace': `${extensionName}.editor.workspace`, }; @@ -241,17 +234,6 @@ export class ExtensionManager { } ); } - case 'workspace-test-item': { - return vscode.commands.registerCommand( - commandName, - async (workspace: vscode.WorkspaceFolder, testItem: vscode.TestItem, ...args) => { - const extension = this.getByName(workspace.name); - if (extension) { - command.callback.call(thisArg, extension, testItem, ...args); - } - } - ); - } case 'active-text-editor': case 'active-text-editor-workspace': { return vscode.commands.registerTextEditorCommand( @@ -426,15 +408,6 @@ export class ExtensionManager { }, }), - // with-workspace-test-item commands - this.registerCommand({ - type: 'workspace-test-item', - name: 'view-snapshot', - callback: (extension, testItem) => { - extension.viewSnapshot(testItem); - }, - }), - // setup tool vscode.commands.registerCommand(`${extensionName}.setup-extension`, this.startWizard), diff --git a/src/test-provider/test-item-context-manager.ts b/src/test-provider/test-item-context-manager.ts index 52569b21..4f45950f 100644 --- a/src/test-provider/test-item-context-manager.ts +++ b/src/test-provider/test-item-context-manager.ts @@ -6,6 +6,10 @@ import { extensionName } from '../appGlobals'; * TestExplorer menu when-condition */ +export interface SnapshotItem { + itemId: string; + testFullName: string; +} export type ItemContext = | { key: 'jest.autoRun' | 'jest.coverage'; @@ -18,6 +22,7 @@ export type ItemContext = key: 'jest.editor-view-snapshot'; workspace: vscode.WorkspaceFolder; itemIds: string[]; + onClick: (testItem: vscode.TestItem) => void; }; export type TEItemContextKey = ItemContext['key']; @@ -44,13 +49,13 @@ export class TestItemContextManager { //set context for both on and off let itemIds = list - .filter((c) => c.key === context.key && c.value === true) - .flatMap((c) => c.itemIds as string[]); + .flatMap((c) => (c.key === context.key && c.value === true ? c.itemIds : undefined)) + .filter((c) => c !== undefined); vscode.commands.executeCommand('setContext', this.contextKey(context.key, true), itemIds); itemIds = list - .filter((c) => c.key === context.key && c.value === false) - .flatMap((c) => c.itemIds as string[]); + .flatMap((c) => (c.key === context.key && c.value === false ? c.itemIds : undefined)) + .filter((c) => c !== undefined); vscode.commands.executeCommand('setContext', this.contextKey(context.key, false), itemIds); break; } @@ -61,18 +66,15 @@ export class TestItemContextManager { } } } - private getWorkspace( - key: TEItemContextKey, - item: vscode.TestItem - ): vscode.WorkspaceFolder | undefined { + private getItemContext(key: TEItemContextKey, item: vscode.TestItem): ItemContext | undefined { const list = this.cache.get(key); - return list?.find((c) => c.itemIds.includes(item.id))?.workspace; + return list?.find((c) => c.itemIds.includes(item.id)); } public registerCommands(): vscode.Disposable[] { const autoRunCommands = ['test-item.auto-run.toggle-off', 'test-item.auto-run.toggle-on'].map( (n) => vscode.commands.registerCommand(`${extensionName}.${n}`, (testItem: vscode.TestItem) => { - const workspace = this.getWorkspace('jest.autoRun', testItem); + const workspace = this.getItemContext('jest.autoRun', testItem)?.workspace; if (workspace) { vscode.commands.executeCommand( `${extensionName}.with-workspace.toggle-auto-run`, @@ -84,7 +86,7 @@ export class TestItemContextManager { const coverageCommands = ['test-item.coverage.toggle-off', 'test-item.coverage.toggle-on'].map( (n) => vscode.commands.registerCommand(`${extensionName}.${n}`, (testItem: vscode.TestItem) => { - const workspace = this.getWorkspace('jest.coverage', testItem); + const workspace = this.getItemContext('jest.coverage', testItem)?.workspace; if (workspace) { vscode.commands.executeCommand( `${extensionName}.with-workspace.toggle-coverage`, @@ -96,13 +98,9 @@ export class TestItemContextManager { const viewSnapshotCommand = vscode.commands.registerCommand( `${extensionName}.test-item.view-snapshot`, (testItem: vscode.TestItem) => { - const workspace = this.getWorkspace('jest.editor-view-snapshot', testItem); - if (workspace) { - vscode.commands.executeCommand( - `${extensionName}.with-workspace-test-item.view-snapshot`, - workspace, - testItem - ); + const context = this.getItemContext('jest.editor-view-snapshot', testItem); + if (context && context.key === 'jest.editor-view-snapshot') { + context.onClick(testItem); } } ); diff --git a/src/test-provider/test-item-data.ts b/src/test-provider/test-item-data.ts index 927d749c..51ce080f 100644 --- a/src/test-provider/test-item-data.ts +++ b/src/test-provider/test-item-data.ts @@ -240,42 +240,55 @@ export class WorkspaceRoot extends TestItemDataBase { break; } case 'result-matched': { - this.addTestFile(event.file, (testRoot) => testRoot.onTestMatched()); + const extSnapshotItems: vscode.TestItem[] = []; + this.addTestFile(event.file, (testRoot) => testRoot.onTestMatched(extSnapshotItems)); + tiContextManager.setItemContext({ + workspace: this.context.ext.workspace, + key: 'jest.editor-view-snapshot', + itemIds: extSnapshotItems.map((item) => item.id), + onClick: this.onPreviewSnapshot, + }); break; } case 'test-parsed': { this.addTestFile(event.file, (testRoot) => - testRoot.discoverTest(undefined, event.testContainer) + testRoot.discoverTest(undefined, event.sourceContainer) ); break; } - case 'snapshot-suite-changed': { - this.onSnapshotResult(event.testPath); - break; - } } }; - private onSnapshotResult(testPath: string) { - // const inlineSnapshotItems = []; - const snapshotItems: vscode.TestItem[] = []; - const docRoot = this.testDocuments.get(testPath); - const suiteResult = this.context.ext.testResolveProvider.getTestSuiteResult(testPath); - if (docRoot && suiteResult) { - suiteResult.snapshotNodes?.forEach((node) => { - if (node.isInline) { - return; - } - const testItems = findItemByLine(node.metadata.node.loc.start.line - 1, docRoot.item); - snapshotItems.push(...testItems); - }); + // private onSnapshotResult(testPath: string, snapshotBlocks: ExtSnapshotBlock[]) { + // // const inlineSnapshotItems = []; + // const snapshotItems: vscode.TestItem[] = []; + // const docRoot = this.testDocuments.get(testPath); + // if (docRoot) { + // snapshotBlocks?.forEach((block) => { + // if (block.isInline) { + // return; + // } + // const testItems = findItemByLine(block.node.loc.start.line - 1, docRoot.item); + // snapshotItems.push(...testItems); + // }); + // } + // tiContextManager.setItemContext({ + // workspace: this.context.ext.workspace, + // key: 'jest.editor-view-snapshot', + // itemIds: snapshotItems.map((item) => item.id), + // onClick: this.onPreviewSnapshot, + // }); + // } + private onPreviewSnapshot = (testItem: vscode.TestItem): Promise => { + const data = this.context.getData(testItem); + if (data instanceof TestData) { + return data.previewSnapshot(); } - tiContextManager.setItemContext({ - workspace: this.context.ext.workspace, - key: 'jest.editor-view-snapshot', - itemIds: snapshotItems.map((item) => item.id), - }); - } + vscode.window.showErrorMessage( + `Preview snapshot failed: unexpected test block: ${testItem.id}` + ); + return Promise.resolve(); + }; /** get test item from jest process. If running tests from source file, will return undefined */ private getItemFromProcess = (process: JestProcessInfo): vscode.TestItem | undefined => { @@ -645,28 +658,38 @@ export class TestDocumentRoot extends TestResultData { getDebugInfo(): ReturnType { return { fileName: this.uri.fsPath }; } - public onTestMatched = (): void => { + public onTestMatched = (extSnapshotItems: vscode.TestItem[]): void => { this.item.children.forEach((childItem) => - this.context.getData(childItem)?.onTestMatched() + this.context.getData(childItem)?.onTestMatched(extSnapshotItems) ); }; } -const findItemByLine = (zeroBasedLine: number, item: vscode.TestItem): vscode.TestItem[] => { - const found: vscode.TestItem[] = []; - const range = item.range; - if (range && (range.start.line > zeroBasedLine || range.end.line < zeroBasedLine)) { - return []; - } - if (range && range.start.line <= zeroBasedLine && range.end.line >= zeroBasedLine) { - found.push(item); - } - - item.children.forEach((child) => { - found.push(...findItemByLine(zeroBasedLine, child)); - }); - - return found; -}; +// const findItemByLine = ( +// zeroBasedLine: number, +// item: vscode.TestItem, +// onlyLeaf = true +// ): vscode.TestItem[] => { +// const found: vscode.TestItem[] = []; +// const range = item.range; + +// const label = item.label; +// console.log(`label = ${label}`); + +// if (range && (range.start.line > zeroBasedLine || range.end.line < zeroBasedLine)) { +// return []; +// } +// if (!onlyLeaf || item.children.size === 0) { +// if (range && range.start.line <= zeroBasedLine && range.end.line >= zeroBasedLine) { +// found.push(item); +// } +// } + +// item.children.forEach((child) => { +// found.push(...findItemByLine(zeroBasedLine, child)); +// }); + +// return found; +// }; export class TestData extends TestResultData implements Debuggable { constructor( readonly context: JestTestProviderContext, @@ -727,11 +750,14 @@ export class TestData extends TestResultData implements Debuggable { this.syncChildNodes(node); } - public onTestMatched(): void { + public onTestMatched(extSnapshotItems: vscode.TestItem[]): void { // assertion might have picked up source block location this.updateItemRange(); + if (this.node.attrs.snapshot === 'external') { + extSnapshotItems.push(this.item); + } this.item.children.forEach((childItem) => - this.context.getData(childItem)?.onTestMatched() + this.context.getData(childItem)?.onTestMatched(extSnapshotItems) ); } @@ -746,4 +772,10 @@ export class TestData extends TestResultData implements Debuggable { this.context.getData(childItem)?.updateResultState(run) ); } + public previewSnapshot(): Promise { + return this.context.ext.testResolveProvider.previewSnapshot( + this.uri.fsPath, + this.node.fullName + ); + } } diff --git a/tests/JestExt/helper.test.ts b/tests/JestExt/helper.test.ts index 9ee85173..9404c89d 100644 --- a/tests/JestExt/helper.test.ts +++ b/tests/JestExt/helper.test.ts @@ -172,7 +172,6 @@ describe('getExtensionResourceSettings()', () => { expect(getExtensionResourceSettings(uri)).toEqual({ autoEnable: true, coverageFormatter: 'DefaultFormatter', - enableSnapshotUpdateMessages: true, pathToConfig: '', pathToJest: null, jestCommandLine: undefined, diff --git a/tests/JestExt/process-listeners.test.ts b/tests/JestExt/process-listeners.test.ts index fbe2d38e..9d06750c 100644 --- a/tests/JestExt/process-listeners.test.ts +++ b/tests/JestExt/process-listeners.test.ts @@ -447,88 +447,6 @@ describe('jest process listeners', () => { expect(clearTimeout).toHaveBeenCalledTimes(1); }); }); - describe('when snapshot test failed', () => { - it.each` - seq | output | enableSnapshotUpdateMessages | expectUpdateSnapshot - ${1} | ${'Snapshot test failed'} | ${true} | ${true} - ${2} | ${'Snapshot test failed'} | ${false} | ${false} - ${3} | ${'Snapshot failed'} | ${true} | ${true} - ${4} | ${'Snapshots failed'} | ${true} | ${true} - ${5} | ${'Failed for some other reason'} | ${true} | ${false} - `( - 'can detect snapshot failure: #$seq', - async ({ output, enableSnapshotUpdateMessages, expectUpdateSnapshot }) => { - expect.hasAssertions(); - mockSession.context.settings.enableSnapshotUpdateMessages = enableSnapshotUpdateMessages; - (vscode.window.showInformationMessage as jest.Mocked).mockReturnValue( - Promise.resolve('something') - ); - - const listener = new RunTestListener(mockSession); - - await listener.onEvent(mockProcess, 'executableStdErr', Buffer.from(output)); - if (expectUpdateSnapshot) { - expect(vscode.window.showInformationMessage).toHaveBeenCalledTimes(1); - expect(mockSession.scheduleProcess).toHaveBeenCalledWith({ - type: 'update-snapshot', - baseRequest: mockProcess.request, - }); - } else { - expect(vscode.window.showInformationMessage).not.toHaveBeenCalled(); - expect(mockSession.scheduleProcess).not.toHaveBeenCalled(); - } - } - ); - it('will abort auto update snapshot if no user action is taken', async () => { - expect.hasAssertions(); - mockSession.context.settings.enableSnapshotUpdateMessages = true; - (vscode.window.showInformationMessage as jest.Mocked).mockReturnValue( - Promise.resolve(undefined) - ); - - const listener = new RunTestListener(mockSession); - - await listener.onEvent( - mockProcess, - 'executableStdErr', - Buffer.from('Snapshot test failed') - ); - expect(vscode.window.showInformationMessage).toHaveBeenCalledTimes(1); - expect(mockSession.scheduleProcess).not.toHaveBeenCalled(); - }); - it('auto update snapsot only apply for runs not already updating snapshots', async () => { - expect.hasAssertions(); - mockSession.context.settings.enableSnapshotUpdateMessages = true; - (vscode.window.showInformationMessage as jest.Mocked).mockReturnValue( - Promise.resolve('something') - ); - - const listener = new RunTestListener(mockSession); - - await listener.onEvent( - mockProcess, - 'executableStdErr', - Buffer.from('Snapshot test failed') - ); - expect(vscode.window.showInformationMessage).toHaveBeenCalledTimes(1); - expect(mockSession.scheduleProcess).toHaveBeenCalledWith({ - type: 'update-snapshot', - baseRequest: mockProcess.request, - }); - - // for a process already with updateSnapshot flag: do nothing - (vscode.window.showInformationMessage as jest.Mocked).mockClear(); - mockSession.scheduleProcess.mockClear(); - mockProcess.request.updateSnapshot = true; - await listener.onEvent( - mockProcess, - 'executableStdErr', - Buffer.from('Snapshot test failed') - ); - expect(vscode.window.showInformationMessage).not.toHaveBeenCalled(); - expect(mockSession.scheduleProcess).not.toHaveBeenCalled(); - }); - }); describe('when "--watch" is not supported', () => { const outOfRepositoryOutput = ` diff --git a/yarn.lock b/yarn.lock index cf04a1a2..c5f36ced 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2934,10 +2934,9 @@ jest-each@^29.2.1: jest-util "^29.2.1" pretty-format "^29.2.1" -jest-editor-support@^30.2.1: - version "30.2.1" - resolved "https://registry.yarnpkg.com/jest-editor-support/-/jest-editor-support-30.2.1.tgz#902911427fe46f052ec29320de8b8da41f832d97" - integrity sha512-zsWAv6Taoqvci1jSiEVqCEG/IS/+Lwhyu1VJ/skcdlrhjjFrrcV+W3PRPjjI6bgSWMVNi20yPU1CrA88+e56Tg== +"jest-editor-support@file:../jest-editor-support/jest-editor-support-30.3.0.tgz": + version "30.3.0" + resolved "file:../jest-editor-support/jest-editor-support-30.3.0.tgz#dae605e7b749be57512c14c8a9b5a1e6970ea74d" dependencies: "@babel/parser" "^7.15.7" "@babel/runtime" "^7.15.4" From 5914e67dde1d8293be8f39625ef36eff88f09976 Mon Sep 17 00:00:00 2001 From: connectdotz Date: Thu, 17 Nov 2022 14:03:20 -0500 Subject: [PATCH 05/16] removed SnapshotCodeLenses --- README.md | 8 +-- release-notes/release-note-v5.md | 4 ++ .../SnapshotCodeLensProvider.ts | 49 --------------- .../SnapshotPreviewProvider.ts | 45 ------------- src/SnapshotCodeLens/index.ts | 2 - src/extension.ts | 5 +- .../SnapshotCodeLensProvider.test.ts | 63 ------------------- 7 files changed, 9 insertions(+), 167 deletions(-) delete mode 100644 src/SnapshotCodeLens/SnapshotCodeLensProvider.ts delete mode 100644 src/SnapshotCodeLens/SnapshotPreviewProvider.ts delete mode 100644 src/SnapshotCodeLens/index.ts delete mode 100644 tests/SnapshotCodeLens/SnapshotCodeLensProvider.test.ts diff --git a/README.md b/README.md index 3e3d9313..d6199cbf 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ Content - [How to trigger the test run?](#how-to-trigger-the-test-run) - [How to debug tests?](#how-to-debug-tests) - [How to use code coverage?](#how-to-use-code-coverage) + - [How to update and view snpashots?](#how-to-update-and-view-snpashots) - [How to use the extension with monorepo projects?](#how-to-use-the-extension-with-monorepo-projects) - [How to read the StatusBar?](#how-to-read-the-statusbar) - [How to use the Test Explorer?](#how-to-use-the-test-explorer) @@ -83,7 +84,7 @@ Content * Show fails inside the problem inspector. * Highlights the errors next to the `expect` functions. * Adds syntax highlighting to snapshot files. -* A one button update for failed snapshots. +* Update and view snapshots at any level. * Show coverage information in files being tested. * Help debug jest tests in vscode. * Supports multiple test run modes (automated, manual, and hybrid onSave) to meet user's preferred development experience. @@ -170,6 +171,8 @@ You can customize coverage start up behavior, style and colors, see [customizati +### How to update and view snpashots? + ### How to use the extension with monorepo projects? The easiest way to setup the monorepo projects is to use the [Setup Tool](setup-wizard.md#setup-monorepo-project) and choose **Setup monorepo project** @@ -257,9 +260,6 @@ Users can use the following settings to tailor the extension for their environme |**Editor**| |enableInlineErrorMessages :x:| Whether errors should be reported inline on a file|--|This is now deprecated in favor of `jest.testExplorer` | |[testExplorer](#testexplorer) |Configure jest test explorer|null|`{"showInlineError": "true"}`| -|**Snapshot**| -|enableSnapshotPreviews 💼|Whether snapshot previews should show|true|`"jest.enableSnapshotPreviews": false`| -|restartJestOnSnapshotUpdate :x:| Restart Jest runner after updating the snapshots|false|This is no longer needed in v4| |**Coverage**| |showCoverageOnLoad|Show code coverage when extension starts|false|`"jest.showCoverageOnLoad": true`| |[coverageFormatter](#coverageFormatter)|Determine the coverage overlay style|"DefaultFormatter"|`"jest.coverageFormatter": "GutterFormatter"`| diff --git a/release-notes/release-note-v5.md b/release-notes/release-note-v5.md index c20844c8..7c99cbea 100644 --- a/release-notes/release-note-v5.md +++ b/release-notes/release-note-v5.md @@ -122,6 +122,10 @@ Users with jest coverageProvider `v8` should be able to see coverage like with t - can not be turned off any more. - removed the "enable" and "showClassicStatus" attributes. The only valid attribute is "showInlineError". - `"jest.autoRun` default value has changed. see detail above. +- Snapshot changes: + - Snapshot codeLenses are gone and replaced with commands in test status and test explorer tree menu. + - No more snapshot update messaging when running tests but users can update any snapshot any time they want. + - Retired the following snapshot related settings: `jest.enableSnapshotPreviews`, `jest.enableSnapshotUpdateMessages`, `restartJestOnSnapshotUpdate`. ### Change log - [v5.0.2 pre-release](https://github.com/jest-community/vscode-jest/releases/tag/v5.0.2) - [v5.0.1 pre-release](https://github.com/jest-community/vscode-jest/releases/tag/v5.0.1) diff --git a/src/SnapshotCodeLens/SnapshotCodeLensProvider.ts b/src/SnapshotCodeLens/SnapshotCodeLensProvider.ts deleted file mode 100644 index b226b39e..00000000 --- a/src/SnapshotCodeLens/SnapshotCodeLensProvider.ts +++ /dev/null @@ -1,49 +0,0 @@ -import * as vscode from 'vscode'; -import { Snapshot } from 'jest-editor-support'; - -import { extensionName } from '../appGlobals'; -import { previewCommand } from './SnapshotPreviewProvider'; - -const missingSnapshotCommand = `${extensionName}.snapshot.missing`; - -class SnapshotCodeLensProvider implements vscode.CodeLensProvider { - public provideCodeLenses(document: vscode.TextDocument, _token: vscode.CancellationToken) { - const snapshots = new Snapshot(); - return snapshots.getMetadataAsync(document.uri.fsPath).then((metadata) => - metadata.map((snapshot) => { - const { line } = snapshot.node.loc.start; - const range = new vscode.Range(line - 1, 0, line - 1, 0); - let command: vscode.Command; - if (snapshot.exists) { - command = { - title: 'view snapshot', - command: previewCommand, - arguments: [snapshot], - }; - } else { - command = { - title: 'snapshot missing', - command: missingSnapshotCommand, - }; - } - - return new vscode.CodeLens(range, command); - }) - ); - } -} - -export function registerSnapshotCodeLens(enableSnapshotPreviews: boolean): vscode.Disposable[] { - if (!enableSnapshotPreviews) { - return []; - } - return [ - vscode.languages.registerCodeLensProvider( - { pattern: '**/*.{ts,tsx,js,jsx}' }, - new SnapshotCodeLensProvider() - ), - vscode.commands.registerCommand(missingSnapshotCommand, () => { - vscode.window.showInformationMessage('Run test to generate snapshot.'); - }), - ]; -} diff --git a/src/SnapshotCodeLens/SnapshotPreviewProvider.ts b/src/SnapshotCodeLens/SnapshotPreviewProvider.ts deleted file mode 100644 index 91764ca4..00000000 --- a/src/SnapshotCodeLens/SnapshotPreviewProvider.ts +++ /dev/null @@ -1,45 +0,0 @@ -import * as vscode from 'vscode'; -import { SnapshotMetadata } from 'jest-editor-support'; - -import { extensionName } from '../appGlobals'; - -export const previewCommand = `${extensionName}.snapshot.preview`; - -export function registerSnapshotPreview(): vscode.Disposable[] { - let panel: vscode.WebviewPanel | null = null; - - const escaped = (snapshot: string) => { - if (snapshot) { - // tslint:disable-next-line no-shadowed-variable - const escaped = snapshot - .replace(/&/g, '&') - .replace(/"/g, '"') - .replace(/'/g, ''') - .replace(//g, '>'); - return `
${escaped}
`; - } - }; - - return [ - vscode.commands.registerCommand(previewCommand, (snapshot: SnapshotMetadata) => { - if (panel) { - panel.reveal(); - } else { - panel = vscode.window.createWebviewPanel( - 'view_snapshot', - snapshot.name, - vscode.ViewColumn.Two, - {} - ); - - panel.onDidDispose(() => { - panel = null; - }); - } - - panel.webview.html = (snapshot.content && escaped(snapshot.content)) || ''; - panel.title = snapshot.name; - }), - ]; -} diff --git a/src/SnapshotCodeLens/index.ts b/src/SnapshotCodeLens/index.ts deleted file mode 100644 index 4bc6a868..00000000 --- a/src/SnapshotCodeLens/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { registerSnapshotCodeLens } from './SnapshotCodeLensProvider'; -export { registerSnapshotPreview } from './SnapshotPreviewProvider'; diff --git a/src/extension.ts b/src/extension.ts index fbfed348..11093647 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,8 +1,7 @@ import * as vscode from 'vscode'; import { statusBar } from './StatusBar'; -import { ExtensionManager, getExtensionWindowSettings } from './extensionManager'; -import { registerSnapshotCodeLens, registerSnapshotPreview } from './SnapshotCodeLens'; +import { ExtensionManager } from './extensionManager'; import { tiContextManager } from './test-provider/test-item-context-manager'; let extensionManager: ExtensionManager; @@ -21,8 +20,6 @@ const addSubscriptions = (context: vscode.ExtensionContext): void => { context.subscriptions.push( ...statusBar.register((folder: string) => extensionManager.getByName(folder)), ...extensionManager.register(), - ...registerSnapshotCodeLens(getExtensionWindowSettings()?.enableSnapshotPreviews ?? false), - ...registerSnapshotPreview(), vscode.languages.registerCodeLensProvider(languages, extensionManager.coverageCodeLensProvider), vscode.languages.registerCodeLensProvider(languages, extensionManager.debugCodeLensProvider), ...tiContextManager.registerCommands() diff --git a/tests/SnapshotCodeLens/SnapshotCodeLensProvider.test.ts b/tests/SnapshotCodeLens/SnapshotCodeLensProvider.test.ts deleted file mode 100644 index c201fa0f..00000000 --- a/tests/SnapshotCodeLens/SnapshotCodeLensProvider.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -jest.unmock('../../src/SnapshotCodeLens/SnapshotCodeLensProvider'); - -import * as vscode from 'vscode'; -import { registerSnapshotCodeLens } from '../../src/SnapshotCodeLens/SnapshotCodeLensProvider'; -import { Snapshot } from 'jest-editor-support'; - -describe('SnapshotCodeLensProvider', () => { - const mockMetadataAsync = jest.fn(); - const mockCodeLens = jest.fn(); - beforeEach(() => { - jest.resetAllMocks(); - (Snapshot as jest.Mocked).mockReturnValue({ - getMetadataAsync: mockMetadataAsync, - }); - (vscode as jest.Mocked).CodeLens = mockCodeLens; - }); - describe('registerSnapshotCodeLens', () => { - it('register if enableSnapshotPreviews is not false', () => { - const registration = registerSnapshotCodeLens(true); - expect(registration.length > 0).toBeTruthy(); - expect(vscode.languages.registerCodeLensProvider).toHaveBeenCalled(); - expect(vscode.commands.registerCommand).toHaveBeenCalledWith( - expect.stringContaining('snapshot.missing'), - expect.anything() - ); - }); - it('not register if enableSnapshotPreviews is false', () => { - const registration = registerSnapshotCodeLens(false); - expect(registration).toHaveLength(0); - expect(vscode.languages.registerCodeLensProvider).not.toHaveBeenCalled(); - expect(vscode.commands.registerCommand).not.toHaveBeenCalled(); - }); - }); - describe('provideCodeLenses', () => { - let provider; - const snapshotMetadata = (line: number, exists = true): any => ({ - node: { loc: { start: { line } } }, - exists, - }); - beforeEach(() => { - registerSnapshotCodeLens(true); - provider = (vscode.languages.registerCodeLensProvider as jest.Mocked).mock.calls[0][1]; - }); - it('create codeLens for each snapshot', async () => { - mockMetadataAsync.mockReturnValue( - Promise.resolve([snapshotMetadata(10, true), snapshotMetadata(20, false)]) - ); - - await provider.provideCodeLenses({ uri: { fsPath: 'whatever' } }, {}); - expect(mockCodeLens).toHaveBeenCalledTimes(2); - - let [, command] = mockCodeLens.mock.calls[0]; - let range = (vscode.Range as jest.Mocked).mock.calls[0]; - expect(range).toEqual([9, 0, 9, 0]); - expect(command.title).toEqual('view snapshot'); - - [, command] = mockCodeLens.mock.calls[1]; - range = (vscode.Range as jest.Mocked).mock.calls[1]; - expect(range).toEqual([19, 0, 19, 0]); - expect(command.title).toEqual('snapshot missing'); - }); - }); -}); From 086de444c9e2a96c288763ac880d390120b7babc Mon Sep 17 00:00:00 2001 From: connectdotz Date: Thu, 17 Nov 2022 14:32:57 -0500 Subject: [PATCH 06/16] fix publish parsed source file to test explorer --- src/TestResults/TestResultProvider.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/TestResults/TestResultProvider.ts b/src/TestResults/TestResultProvider.ts index 55e531d8..223e4679 100644 --- a/src/TestResults/TestResultProvider.ts +++ b/src/TestResults/TestResultProvider.ts @@ -86,6 +86,10 @@ export class TestSuiteRecord implements TestSuiteUpdatable { if (!this._testBlocks) { try { const pResult = parse(this.testFile); + if (![pResult.describeBlocks, pResult.itBlocks].find((blocks) => blocks.length > 0)) { + // nothing in this file yet, skip. Otherwise we might accidentally publish a source file, for example + return 'failed'; + } const sourceContainer = match.buildSourceContainer(pResult.root); this._testBlocks = { ...pResult, sourceContainer }; @@ -242,12 +246,12 @@ export class TestResultProvider { return Array.from(this.testSuites.keys()); } - isTestFile(fileName: string): 'yes' | 'no' | 'unknown' { - if (this.testFiles?.includes(fileName) || this.testSuites.get(fileName) != null) { + isTestFile(fileName: string): 'yes' | 'no' | 'maybe' { + if (this.testFiles?.includes(fileName)) { return 'yes'; } - if (!this.testFiles) { - return 'unknown'; + if (!this.testFiles || this.testSuites.get(fileName) != null) { + return 'maybe'; } return 'no'; } From ed309b31bbdbeecbc04f822ca2162a734d208174 Mon Sep 17 00:00:00 2001 From: connectdotz Date: Thu, 17 Nov 2022 19:31:49 -0500 Subject: [PATCH 07/16] fixes broken tests --- src/TestResults/TestResultProvider.ts | 70 ++++++++------- src/TestResults/match-by-context.ts | 7 +- tests/JestExt/process-session.test.ts | 42 ++------- tests/TestResults/TestResultProvider.test.ts | 87 +++++++++++-------- tests/extension.test.ts | 5 -- .../test-item-context-manager.test.ts | 4 +- tests/test-provider/test-item-data.test.ts | 8 +- tests/test-provider/test-provider.test.ts | 31 ++----- 8 files changed, 115 insertions(+), 139 deletions(-) diff --git a/src/TestResults/TestResultProvider.ts b/src/TestResults/TestResultProvider.ts index 223e4679..87f76655 100644 --- a/src/TestResults/TestResultProvider.ts +++ b/src/TestResults/TestResultProvider.ts @@ -30,6 +30,9 @@ interface TestSuiteResultRaw { assertionContainer?: ContainerNode; results?: TestResult[]; sorted?: SortedTestResults; + // if we are certain the record is for a test file, set this flag to true + // otherwise isTestFile is determined by the testFileList + isTestFile?: boolean; } export type TestSuiteResult = Readonly; @@ -54,6 +57,7 @@ export class TestSuiteRecord implements TestSuiteUpdatable { private _message: string; private _results?: TestResult[]; private _sorted?: SortedTestResults; + private _isTestFile?: boolean; private _testBlocks?: TestBlocks | 'failed'; // private _snapshotBlocks?: ExtSnapshotBlock[] | 'failed'; @@ -81,6 +85,9 @@ export class TestSuiteRecord implements TestSuiteUpdatable { public get sorted(): SortedTestResults | undefined { return this._sorted; } + public get isTestFile(): boolean | undefined { + return this._isTestFile; + } public get testBlocks(): TestBlocks | 'failed' { if (!this._testBlocks) { @@ -156,6 +163,7 @@ export class TestSuiteRecord implements TestSuiteUpdatable { this._status = change.status ?? this.status; this._message = change.message ?? this.message; + this._isTestFile = 'isTestFile' in change ? change.isTestFile : this._isTestFile; this._results = 'results' in change ? change.results : this._results; this._sorted = 'sorted' in change ? change.sorted : this._sorted; this._testBlocks = 'testBlocks' in change ? change.testBlocks : this._testBlocks; @@ -247,10 +255,10 @@ export class TestResultProvider { } isTestFile(fileName: string): 'yes' | 'no' | 'maybe' { - if (this.testFiles?.includes(fileName)) { + if (this.testFiles?.includes(fileName) || this.testSuites.get(fileName)?.isTestFile) { return 'yes'; } - if (!this.testFiles || this.testSuites.get(fileName) != null) { + if (!this.testFiles) { return 'maybe'; } return 'no'; @@ -260,7 +268,13 @@ export class TestResultProvider { return this.testSuites.get(filePath); } - /** match assertions with source file, if successful, update cache.results and related. Will also fire testSuiteChanged event */ + /** + * match assertions with source file, if successful, update cache, results and related. + * Will also fire testSuiteChanged event + * + * if the file is not a test or can not be parsed, the results will be undefined. + * any other errors will result the source blocks to be returned as unmatched block. + **/ private updateMatchedResults(filePath: string, record: TestSuiteRecord): void { let error: string | undefined; if (record.testBlocks === 'failed') { @@ -305,20 +319,18 @@ export class TestResultProvider { /** * returns matched test results for the given file * @param filePath - * @returns valid test result list or undefined if the file is not a test. - * In the case when file can not be parsed or match error, empty results will be returned. - * @throws if parsing or matching internal error + * @returns valid test result list or an empty array if the source file is not a test or can not be parsed. */ getResults(filePath: string, record?: TestSuiteRecord): TestResult[] | undefined { + if (this.isTestFile(filePath) === 'no') { + return; + } + const _record = record ?? this.testSuites.get(filePath) ?? this.addTestSuiteRecord(filePath); if (_record.results) { return _record.results; } - if (this.isTestFile(filePath) === 'no') { - return; - } - this.updateMatchedResults(filePath, _record); return _record.results; } @@ -327,10 +339,13 @@ export class TestResultProvider { * returns sorted test results for the given file * @param filePath * @returns valid sorted test result or undefined if the file is not a test. - * @throws if encountered internal error for test files */ getSortedResults(filePath: string): SortedTestResults | undefined { + if (this.isTestFile(filePath) === 'no') { + return; + } + const record = this.testSuites.get(filePath) ?? this.addTestSuiteRecord(filePath); if (record.sorted) { return record.sorted; @@ -343,26 +358,22 @@ export class TestResultProvider { unknown: [], }; - try { - const testResults = this.getResults(filePath, record); - if (!testResults) { - return; - } - - for (const test of testResults) { - if (test.status === TestReconciliationState.KnownFail) { - sorted.fail.push(test); - } else if (test.status === TestReconciliationState.KnownSkip) { - sorted.skip.push(test); - } else if (test.status === TestReconciliationState.KnownSuccess) { - sorted.success.push(test); - } else { - sorted.unknown.push(test); - } + const testResults = this.getResults(filePath, record); + if (!testResults) { + return; + } + for (const test of testResults) { + if (test.status === TestReconciliationState.KnownFail) { + sorted.fail.push(test); + } else if (test.status === TestReconciliationState.KnownSkip) { + sorted.skip.push(test); + } else if (test.status === TestReconciliationState.KnownSuccess) { + sorted.success.push(test); + } else { + sorted.unknown.push(test); } - } finally { - record.update({ sorted }); } + record.update({ sorted }); return sorted; } @@ -373,6 +384,7 @@ export class TestResultProvider { record.update({ status: r.status, message: r.message, + isTestFile: true, assertionContainer: undefined, results: undefined, sorted: undefined, diff --git a/src/TestResults/match-by-context.ts b/src/TestResults/match-by-context.ts index a6a2178e..b239969a 100644 --- a/src/TestResults/match-by-context.ts +++ b/src/TestResults/match-by-context.ts @@ -16,6 +16,7 @@ import { DescribeBlock, Location, NamedBlock, + ParsedNodeTypes, } from 'jest-editor-support'; import { TestReconciliationState } from './TestReconciliationState'; import { TestResult } from './TestResult'; @@ -453,14 +454,16 @@ const ContextMatch = (): ContextMatchAlgorithm => { */ const { match } = ContextMatch(); - +const isParsedNode = (source: ParsedNode | ContainerNode): source is ParsedNode => + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (source as any).type in ParsedNodeTypes; export const matchTestAssertions = ( fileName: string, source: ParsedNode | ContainerNode, assertions: TestAssertionStatus[] | ContainerNode, verbose = false ): TestResult[] => { - const tContainer = source instanceof ParsedNode ? buildSourceContainer(source) : source; + const tContainer = isParsedNode(source) ? buildSourceContainer(source) : source; const aContainer = Array.isArray(assertions) ? buildAssertionContainer(assertions) : assertions; const messaging = createMessaging(fileName, verbose); diff --git a/tests/JestExt/process-session.test.ts b/tests/JestExt/process-session.test.ts index 029ed8a8..0d0efd34 100644 --- a/tests/JestExt/process-session.test.ts +++ b/tests/JestExt/process-session.test.ts @@ -80,35 +80,6 @@ describe('ProcessSession', () => { } } ); - it.each` - baseRequest | snapshotRequest - ${{ type: 'watch-tests' }} | ${{ type: 'all-tests', updateSnapshot: true }} - ${{ type: 'watch-all-tests' }} | ${{ type: 'all-tests', updateSnapshot: true }} - ${{ type: 'all-tests' }} | ${{ type: 'all-tests', updateSnapshot: true }} - ${{ type: 'by-file', testFileName: 'abc' }} | ${{ type: 'by-file', testFileName: 'abc', updateSnapshot: true }} - ${{ type: 'by-file', testFileName: 'abc', updateSnapshot: true }} | ${undefined} - ${{ type: 'by-file-pattern', testFileNamePattern: 'abc' }} | ${{ type: 'by-file-pattern', testFileNamePattern: 'abc', updateSnapshot: true }} - ${{ type: 'by-file-test', testFileName: 'abc', testNamePattern: 'a test' }} | ${{ type: 'by-file-test', testFileName: 'abc', testNamePattern: 'a test', updateSnapshot: true }} - ${{ type: 'by-file-test-pattern', testFileNamePattern: 'abc', testNamePattern: 'a test' }} | ${{ type: 'by-file-test-pattern', testFileNamePattern: 'abc', testNamePattern: 'a test', updateSnapshot: true }} - `( - 'can schedule update-snapshot request: $baseRequest', - async ({ baseRequest, snapshotRequest }) => { - expect.hasAssertions(); - const sm = createProcessSession(context); - expect(mockProcessManager).toHaveBeenCalledTimes(1); - - sm.scheduleProcess({ type: 'update-snapshot', baseRequest }); - - if (snapshotRequest) { - expect(processManagerMock.scheduleJestProcess).toHaveBeenCalledWith( - expect.objectContaining(snapshotRequest) - ); - } else { - expect(processManagerMock.scheduleJestProcess).not.toHaveBeenCalled(); - } - } - ); - it.each([['not-test']])('currently does not support "%s" request scheduling', (type) => { expect.hasAssertions(); const sm = createProcessSession(context); @@ -119,13 +90,12 @@ describe('ProcessSession', () => { expect(processManagerMock.scheduleJestProcess).not.toHaveBeenCalled(); }); describe.each` - type | inputProperty | defaultListener - ${'all-tests'} | ${undefined} | ${listeners.RunTestListener} - ${'watch-tests'} | ${undefined} | ${listeners.RunTestListener} - ${'watch-all-tests'} | ${undefined} | ${listeners.RunTestListener} - ${'by-file'} | ${{ testFileNamePattern: 'abc' }} | ${listeners.RunTestListener} - ${'list-test-files'} | ${undefined} | ${listeners.ListTestFileListener} - ${'update-snapshot'} | ${{ baseRequest: { type: 'all-tests' } }} | ${listeners.RunTestListener} + type | inputProperty | defaultListener + ${'all-tests'} | ${undefined} | ${listeners.RunTestListener} + ${'watch-tests'} | ${undefined} | ${listeners.RunTestListener} + ${'watch-all-tests'} | ${undefined} | ${listeners.RunTestListener} + ${'by-file'} | ${{ testFileNamePattern: 'abc' }} | ${listeners.RunTestListener} + ${'list-test-files'} | ${undefined} | ${listeners.ListTestFileListener} `('schedule $type', ({ type, inputProperty, defaultListener }) => { it('with default listener', () => { expect.hasAssertions(); diff --git a/tests/TestResults/TestResultProvider.test.ts b/tests/TestResults/TestResultProvider.test.ts index 7b9340ea..3418a6a8 100644 --- a/tests/TestResults/TestResultProvider.test.ts +++ b/tests/TestResults/TestResultProvider.test.ts @@ -17,8 +17,8 @@ const mockParse = jest.fn(); jest.mock('jest-editor-support', () => { const TestReconciler = mockTestReconciler; const parse = mockParse; - - return { TestReconciler, parse }; + const ParsedNodeTypes = []; + return { TestReconciler, parse, ParsedNodeTypes }; }); const pathProperties = { @@ -41,11 +41,13 @@ import * as helper from '../test-helper'; import { ItBlock, TestAssertionStatus, TestReconcilationState } from 'jest-editor-support'; import * as match from '../../src/TestResults/match-by-context'; import { mockJestExtEvents } from '../test-helper'; +import { SnapshotProvider } from '../../src/TestResults/snapshot-provider'; const setupMockParse = (itBlocks: ItBlock[]) => { mockParse.mockReturnValue({ root: helper.makeRoot(itBlocks), itBlocks, + describeBlocks: [], }); }; @@ -98,6 +100,7 @@ const newProviderWithData = (testData: TestData[]): TestResultProvider => { return { root: helper.makeRoot(data.itBlocks), itBlocks: data.itBlocks, + describeBlocks: [], }; } }); @@ -142,17 +145,18 @@ describe('TestResultProvider', () => { throw new Error('forced error'); }); }; - const forceMatchResultError = (sut: any) => { - sut.matchResults = jest.fn(() => { - throw new Error('forced error'); - }); - }; + + let mockSnapshotProvider; beforeEach(() => { jest.resetAllMocks(); jest.restoreAllMocks(); console.warn = jest.fn(); mockTestReconciler.mockReturnValue(mockReconciler); (vscode.EventEmitter as jest.Mocked) = jest.fn().mockImplementation(helper.mockEvent); + mockSnapshotProvider = { + parse: jest.fn().mockReturnValue({ blocks: [] }), + }; + (SnapshotProvider as jest.Mocked).mockReturnValue(mockSnapshotProvider); }); describe('getResults()', () => { @@ -220,12 +224,22 @@ describe('TestResultProvider', () => { expect(actual[0].terseMessage).toBeUndefined(); }); - it('fire testSuiteChanged event for newly matched result', () => { - const sut = newProviderWithData([makeData([testBlock], [], filePath)]); - sut.getResults(filePath); - expect(sut.events.testSuiteChanged.fire).toHaveBeenCalledWith({ - type: 'result-matched', - file: filePath, + describe('fire "result-matched" event', () => { + it('fire testSuiteChanged event for newly matched result', () => { + const sut = newProviderWithData([makeData([testBlock], [assertion], filePath)]); + sut.getResults(filePath); + expect(sut.events.testSuiteChanged.fire).toHaveBeenCalledWith({ + type: 'result-matched', + file: filePath, + }); + }); + it('will not fire if no assertion to match', () => { + const sut = newProviderWithData([makeData([testBlock], [], filePath)]); + sut.getResults(filePath); + expect(sut.events.testSuiteChanged.fire).not.toHaveBeenCalledWith({ + type: 'result-matched', + file: filePath, + }); }); }); it('unmatched test will file test-parsed event instead', () => { @@ -234,7 +248,7 @@ describe('TestResultProvider', () => { expect(sut.events.testSuiteChanged.fire).toHaveBeenCalledWith({ type: 'test-parsed', file: filePath, - testContainer: expect.anything(), + sourceContainer: expect.anything(), }); }); @@ -573,23 +587,23 @@ describe('TestResultProvider', () => { const setupForNonTest = (sut: any) => { sut.updateTestFileList(['test-file']); + itBlocks = []; }; it.each` - desc | setup | expectedResults | statsChange - ${'parse failed'} | ${forceParseError} | ${'throw error'} | ${'fail'} - ${'matchResult failed'} | ${forceMatchResultError} | ${'throw error'} | ${'fail'} - ${'match failed'} | ${forceMatchError} | ${'Unknown'} | ${'unknown'} - ${'file is not a test file'} | ${setupForNonTest} | ${undefined} | ${undefined} + desc | setup | itBlockOverride | expectedResults | statsChange + ${'parse failed'} | ${forceParseError} | ${undefined} | ${[]} | ${'fail'} + ${'match failed'} | ${forceMatchError} | ${undefined} | ${'Unknown'} | ${'fail'} + ${'file is not a test file'} | ${setupForNonTest} | ${[]} | ${undefined} | ${'no change'} `( 'when $desc => returns $expectedResults, stats changed: $statsChange', - ({ setup, expectedResults, statsChange }) => { - const sut = newProviderWithData([makeData(itBlocks, assertions, 'whatever')]); + ({ setup, itBlockOverride, expectedResults, statsChange }) => { + const sut = newProviderWithData([ + makeData(itBlockOverride ?? itBlocks, assertions, 'whatever'), + ]); setup(sut); const stats = sut.getTestSuiteStats(); - if (expectedResults === 'throw error') { - expect(() => sut.getResults('whatever')).toThrow(); - } else if (expectedResults === 'Unknown') { + if (expectedResults === 'Unknown') { expect( sut.getResults('whatever').every((r) => r.status === expectedResults) ).toBeTruthy(); @@ -610,6 +624,12 @@ describe('TestResultProvider', () => { describe('getSortedResults()', () => { const filePath = 'file.js'; + const emptyResult = { + fail: [], + skip: [], + success: [], + unknown: [], + }; let sut; beforeEach(() => { const [itBlocks, assertions] = createDataSet(); @@ -644,17 +664,9 @@ describe('TestResultProvider', () => { sut.updateTestFileList(['test-file']); expect(sut.getSortedResults('test-file')).toBeUndefined(); }); - it('can throw for internal error for once', () => { + it('internal error cause empty result', () => { forceParseError(); - expect(() => sut.getSortedResults(filePath)).toThrow(); - - //2nd time will just return empty result - expect(sut.getSortedResults(filePath)).toEqual({ - fail: [], - skip: [], - success: [], - unknown: [], - }); + expect(sut.getSortedResults(filePath)).toEqual(emptyResult); }); }); @@ -672,6 +684,7 @@ describe('TestResultProvider', () => { const results2 = sut.getResults('file 2'); expect(results1).toHaveLength(5); expect(results2).toHaveLength(5); + expect(mockReconciler.assertionsForTestFile).toHaveBeenCalledTimes(2); // now let's update "file 1" mockReconciler.updateFileWithJestStatus.mockReturnValueOnce([ @@ -679,6 +692,8 @@ describe('TestResultProvider', () => { ]); sut.updateTestResults({} as any, {} as any); + mockReconciler.assertionsForTestFile.mockClear(); + // to get result from "file 1" should trigger mockReconciler.assertionsForTestFile const r1 = sut.getResults('file 1'); expect(r1).toEqual(results1); @@ -896,8 +911,8 @@ describe('TestResultProvider', () => { }); it.each` testFiles | testResults | expected - ${undefined} | ${undefined} | ${'unknown'} - ${undefined} | ${['file-2']} | ${'unknown'} + ${undefined} | ${undefined} | ${'maybe'} + ${undefined} | ${['file-2']} | ${'maybe'} ${[]} | ${[]} | ${'no'} ${[]} | ${['file-1']} | ${'yes'} ${[]} | ${['file-2']} | ${'no'} diff --git a/tests/extension.test.ts b/tests/extension.test.ts index a58c9b67..17b32270 100644 --- a/tests/extension.test.ts +++ b/tests/extension.test.ts @@ -15,11 +15,6 @@ jest.mock('../src/Coverage', () => ({ CoverageCodeLensProvider: jest.fn().mockReturnValue({}), })); -jest.mock('../src/SnapshotCodeLens', () => ({ - registerSnapshotCodeLens: jest.fn(() => []), - registerSnapshotPreview: jest.fn(() => []), -})); - jest.mock('../src/test-provider/test-item-context-manager', () => ({ tiContextManager: { registerCommands: jest.fn(() => []) }, })); diff --git a/tests/test-provider/test-item-context-manager.test.ts b/tests/test-provider/test-item-context-manager.test.ts index 6c90ff2a..2c93a343 100644 --- a/tests/test-provider/test-item-context-manager.test.ts +++ b/tests/test-provider/test-item-context-manager.test.ts @@ -71,7 +71,7 @@ describe('TestItemContextManager', () => { it('toggle-autoRun menu commands', () => { const manager = new TestItemContextManager(); const disposableList = manager.registerCommands(); - expect(disposableList).toHaveLength(4); + expect(disposableList.length).toBeGreaterThanOrEqual(4); const commands = [ `${extensionName}.test-item.auto-run.toggle-on`, @@ -99,7 +99,7 @@ describe('TestItemContextManager', () => { it('toggle-coverage menu commands', () => { const manager = new TestItemContextManager(); const disposableList = manager.registerCommands(); - expect(disposableList).toHaveLength(4); + expect(disposableList.length).toBeGreaterThanOrEqual(4); const commands = [ `${extensionName}.test-item.coverage.toggle-on`, diff --git a/tests/test-provider/test-item-data.test.ts b/tests/test-provider/test-item-data.test.ts index 60c5df63..51508933 100644 --- a/tests/test-provider/test-item-data.test.ts +++ b/tests/test-provider/test-item-data.test.ts @@ -458,7 +458,7 @@ describe('test-item-data', () => { const t1 = helper.makeItBlock('test-1', [1, 1, 5, 1]); const t2 = helper.makeItBlock('test-2', [6, 1, 7, 1]); const sourceRoot = helper.makeRoot([t2, t1]); - const testContainer = buildSourceContainer(sourceRoot); + const sourceContainer = buildSourceContainer(sourceRoot); const wsRoot = new WorkspaceRoot(context); wsRoot.discoverTest(jestRun); @@ -473,7 +473,7 @@ describe('test-item-data', () => { context.ext.testResolveProvider.events.testSuiteChanged.event.mock.calls[0][0]({ type: 'test-parsed', file: '/ws-1/a.test.ts', - testContainer, + sourceContainer, }); expect(docItem.children.size).toEqual(2); let dItem = getChildItem(docItem, 'test-1'); @@ -493,11 +493,11 @@ describe('test-item-data', () => { const t1 = helper.makeItBlock('test-1', [1, 1, 5, 1]); const t2 = helper.makeItBlock('test-2', [6, 1, 7, 1]); const sourceRoot = helper.makeRoot([t2, t1]); - const testContainer = buildSourceContainer(sourceRoot); + const sourceContainer = buildSourceContainer(sourceRoot); context.ext.testResolveProvider.events.testSuiteChanged.event.mock.calls[0][0]({ type: 'test-parsed', file: '/ws-1/a.test.ts', - testContainer, + sourceContainer, }); expect(wsRoot.item.children.size).toBe(1); let docItem = getChildItem(wsRoot.item, 'a.test.ts'); diff --git a/tests/test-provider/test-provider.test.ts b/tests/test-provider/test-provider.test.ts index 105de3f1..c5fdb07e 100644 --- a/tests/test-provider/test-provider.test.ts +++ b/tests/test-provider/test-provider.test.ts @@ -78,42 +78,23 @@ describe('JestTestProvider', () => { `${extensionId}:TestProvider:ws-1`, expect.stringContaining('ws-1') ); - expect(controllerMock.createRunProfile).toHaveBeenCalledTimes(2); + expect(controllerMock.createRunProfile).toHaveBeenCalledTimes(3); [ - [vscode.TestRunProfileKind.Run, 'run'], - [vscode.TestRunProfileKind.Debug, 'debug'], - ].forEach(([kind, id]) => { + [vscode.TestRunProfileKind.Run, 'run', true], + [vscode.TestRunProfileKind.Run, 'update-snapshot', false], + [vscode.TestRunProfileKind.Debug, 'debug', true], + ].forEach(([kind, id, isDefault]) => { expect(controllerMock.createRunProfile).toHaveBeenCalledWith( expect.anything(), kind, expect.anything(), - true, + isDefault, expect.objectContaining({ id }) ); }); expect(WorkspaceRoot).toHaveBeenCalled(); }); - it.each` - isWatchMode - ${true} - ${false} - `('will create Profiles regardless isWatchMode=$isWatchMode', ({ isWatchMode }) => { - extExplorerContextMock.settings.autoRun.isWatch = isWatchMode; - new JestTestProvider(extExplorerContextMock); - const kinds = [vscode.TestRunProfileKind.Debug, vscode.TestRunProfileKind.Run]; - - expect(controllerMock.createRunProfile).toHaveBeenCalledTimes(kinds.length); - kinds.forEach((kind) => { - expect(controllerMock.createRunProfile).toHaveBeenCalledWith( - expect.anything(), - kind, - expect.anything(), - true, - expect.anything() - ); - }); - }); }); describe('can discover tests', () => { From 42885384d0f1793407837f6077b1bfbffd0f0d3b Mon Sep 17 00:00:00 2001 From: connectdotz Date: Thu, 17 Nov 2022 19:35:20 -0500 Subject: [PATCH 08/16] clean up console output messages --- src/JestProcessManagement/JestProcessManager.ts | 2 -- src/test-provider/test-item-context-manager.ts | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/JestProcessManagement/JestProcessManager.ts b/src/JestProcessManagement/JestProcessManager.ts index 7134c7db..b6639518 100644 --- a/src/JestProcessManagement/JestProcessManager.ts +++ b/src/JestProcessManagement/JestProcessManager.ts @@ -71,9 +71,7 @@ export class JestProcessManager implements TaskArrayFunctions { const process = task.data; try { - this.logging('debug', 'starting process:', process); await process.start(); - this.logging('debug', 'process ended:', process); } catch (e) { this.logging('error', `${queue.name}: process failed:`, process, e); } finally { diff --git a/src/test-provider/test-item-context-manager.ts b/src/test-provider/test-item-context-manager.ts index 4f45950f..2ac80a2a 100644 --- a/src/test-provider/test-item-context-manager.ts +++ b/src/test-provider/test-item-context-manager.ts @@ -33,8 +33,6 @@ export class TestItemContextManager { return `${key}.${value ? 'on' : 'off'}`; } public setItemContext(context: ItemContext): void { - console.log(`setItemContext for context=`, context); - switch (context.key) { case 'jest.autoRun': case 'jest.coverage': { From 91ce29c4dd0e9b38327fef32a85a439cf8b47c15 Mon Sep 17 00:00:00 2001 From: connectdotz Date: Thu, 17 Nov 2022 21:26:49 -0500 Subject: [PATCH 09/16] upgrade jest-editor-support --- package.json | 2 +- yarn.lock | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 28da9f96..07051313 100644 --- a/package.json +++ b/package.json @@ -512,7 +512,7 @@ "dependencies": { "istanbul-lib-coverage": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", - "jest-editor-support": "^30.2.1" + "jest-editor-support": "^30.3.0" }, "devDependencies": { "@types/istanbul-lib-coverage": "^2.0.2", diff --git a/yarn.lock b/yarn.lock index c5f36ced..7bac497f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2934,9 +2934,10 @@ jest-each@^29.2.1: jest-util "^29.2.1" pretty-format "^29.2.1" -"jest-editor-support@file:../jest-editor-support/jest-editor-support-30.3.0.tgz": +jest-editor-support@^30.3.0: version "30.3.0" - resolved "file:../jest-editor-support/jest-editor-support-30.3.0.tgz#dae605e7b749be57512c14c8a9b5a1e6970ea74d" + resolved "https://registry.yarnpkg.com/jest-editor-support/-/jest-editor-support-30.3.0.tgz#b248e20f97be0ab31ff26b3848536e67b3c22044" + integrity sha512-U0yAYzTRUqSomlMazCOPZXzyqBghSwEzc2vSAjxHA8rylB4uQ1ZyaqOfsffqm1En3jhsVuYSoh0wXnBevrETRw== dependencies: "@babel/parser" "^7.15.7" "@babel/runtime" "^7.15.4" From 351fbfbdf3061ab2551f7b30c04be1d35ef4b8db Mon Sep 17 00:00:00 2001 From: connectdotz Date: Fri, 18 Nov 2022 12:18:42 -0500 Subject: [PATCH 10/16] adding tests for changes --- src/JestExt/core.ts | 1 - src/test-provider/test-item-data.ts | 46 ----- tests/test-provider/test-helper.ts | 4 +- .../test-item-context-manager.test.ts | 185 +++++++++++++----- tests/test-provider/test-item-data.test.ts | 103 ++++++++-- 5 files changed, 227 insertions(+), 112 deletions(-) diff --git a/src/JestExt/core.ts b/src/JestExt/core.ts index 84a039e5..64f04baf 100644 --- a/src/JestExt/core.ts +++ b/src/JestExt/core.ts @@ -631,7 +631,6 @@ export class JestExt { // restart jest since coverage condition has changed this.triggerUpdateSettings(this.extContext.settings); } - enableLoginShell(): void { if (this.extContext.settings.shell.useLoginShell) { return; diff --git a/src/test-provider/test-item-data.ts b/src/test-provider/test-item-data.ts index 51ce080f..4797ae36 100644 --- a/src/test-provider/test-item-data.ts +++ b/src/test-provider/test-item-data.ts @@ -259,26 +259,6 @@ export class WorkspaceRoot extends TestItemDataBase { } }; - // private onSnapshotResult(testPath: string, snapshotBlocks: ExtSnapshotBlock[]) { - // // const inlineSnapshotItems = []; - // const snapshotItems: vscode.TestItem[] = []; - // const docRoot = this.testDocuments.get(testPath); - // if (docRoot) { - // snapshotBlocks?.forEach((block) => { - // if (block.isInline) { - // return; - // } - // const testItems = findItemByLine(block.node.loc.start.line - 1, docRoot.item); - // snapshotItems.push(...testItems); - // }); - // } - // tiContextManager.setItemContext({ - // workspace: this.context.ext.workspace, - // key: 'jest.editor-view-snapshot', - // itemIds: snapshotItems.map((item) => item.id), - // onClick: this.onPreviewSnapshot, - // }); - // } private onPreviewSnapshot = (testItem: vscode.TestItem): Promise => { const data = this.context.getData(testItem); if (data instanceof TestData) { @@ -664,32 +644,6 @@ export class TestDocumentRoot extends TestResultData { ); }; } -// const findItemByLine = ( -// zeroBasedLine: number, -// item: vscode.TestItem, -// onlyLeaf = true -// ): vscode.TestItem[] => { -// const found: vscode.TestItem[] = []; -// const range = item.range; - -// const label = item.label; -// console.log(`label = ${label}`); - -// if (range && (range.start.line > zeroBasedLine || range.end.line < zeroBasedLine)) { -// return []; -// } -// if (!onlyLeaf || item.children.size === 0) { -// if (range && range.start.line <= zeroBasedLine && range.end.line >= zeroBasedLine) { -// found.push(item); -// } -// } - -// item.children.forEach((child) => { -// found.push(...findItemByLine(zeroBasedLine, child)); -// }); - -// return found; -// }; export class TestData extends TestResultData implements Debuggable { constructor( readonly context: JestTestProviderContext, diff --git a/tests/test-provider/test-helper.ts b/tests/test-provider/test-helper.ts index 0f83b572..307b0e7b 100644 --- a/tests/test-provider/test-helper.ts +++ b/tests/test-provider/test-helper.ts @@ -70,12 +70,12 @@ export const mockController = (): any => { return run; }), dispose: jest.fn(), - createRunProfile: jest.fn().mockImplementation((label, kind, runHandler, isDefault, tags) => ({ + createRunProfile: jest.fn().mockImplementation((label, kind, runHandler, isDefault, tag) => ({ label, kind, runHandler, isDefault, - tags: tags ?? [], + tag, })), createTestItem: jest.fn().mockImplementation((id, label, uri) => { const item: any = { diff --git a/tests/test-provider/test-item-context-manager.test.ts b/tests/test-provider/test-item-context-manager.test.ts index 2c93a343..1821002f 100644 --- a/tests/test-provider/test-item-context-manager.test.ts +++ b/tests/test-provider/test-item-context-manager.test.ts @@ -10,61 +10,113 @@ describe('TestItemContextManager', () => { jest.resetAllMocks(); }); describe('can set itemContext', () => { - it.each` - case | context | withItemKey | withoutItemKey - ${1} | ${{ key: 'jest.autoRun', value: true, itemIds: ['a'] }} | ${'jest.autoRun.on'} | ${'jest.autoRun.off'} - ${2} | ${{ key: 'jest.autoRun', value: false, itemIds: ['a'] }} | ${'jest.autoRun.off'} | ${'jest.autoRun.on'} - ${2} | ${{ key: 'jest.coverage', value: true, itemIds: ['a'] }} | ${'jest.coverage.on'} | ${'jest.coverage.off'} - ${2} | ${{ key: 'jest.coverage', value: false, itemIds: ['a'] }} | ${'jest.coverage.off'} | ${'jest.coverage.on'} - `('case $case: setContext for $expectedKey', ({ context, withItemKey, withoutItemKey }) => { - const workspace: any = { name: 'ws' }; - const manager = new TestItemContextManager(); - manager.setItemContext({ workspace, ...context }); - expect(vscode.commands.executeCommand).toHaveBeenCalledWith( - 'setContext', - withItemKey, - context.itemIds - ); - expect(vscode.commands.executeCommand).toHaveBeenCalledWith('setContext', withoutItemKey, []); + describe('jest.autoRun and jest.coverage', () => { + it.each` + case | context | withItemKey | withoutItemKey + ${1} | ${{ key: 'jest.autoRun', value: true, itemIds: ['a'] }} | ${'jest.autoRun.on'} | ${'jest.autoRun.off'} + ${2} | ${{ key: 'jest.autoRun', value: false, itemIds: ['a'] }} | ${'jest.autoRun.off'} | ${'jest.autoRun.on'} + ${3} | ${{ key: 'jest.coverage', value: true, itemIds: ['a'] }} | ${'jest.coverage.on'} | ${'jest.coverage.off'} + ${4} | ${{ key: 'jest.coverage', value: false, itemIds: ['a'] }} | ${'jest.coverage.off'} | ${'jest.coverage.on'} + `('case $case: setContext for $expectedKey', ({ context, withItemKey, withoutItemKey }) => { + const workspace: any = { name: 'ws' }; + const manager = new TestItemContextManager(); + manager.setItemContext({ workspace, ...context }); + expect(vscode.commands.executeCommand).toHaveBeenCalledWith( + 'setContext', + withItemKey, + context.itemIds + ); + expect(vscode.commands.executeCommand).toHaveBeenCalledWith( + 'setContext', + withoutItemKey, + [] + ); + }); + it('can manage itemContext for multiple workspaces', () => { + const ws1: any = { name: 'ws1' }; + const ws2: any = { name: 'ws2' }; + const manager = new TestItemContextManager(); + manager.setItemContext({ + workspace: ws1, + key: 'jest.autoRun', + value: true, + itemIds: ['a', 'b'], + }); + manager.setItemContext({ + workspace: ws2, + key: 'jest.autoRun', + value: true, + itemIds: ['c'], + }); + manager.setItemContext({ + workspace: ws2, + key: 'jest.autoRun', + value: false, + itemIds: ['d'], + }); + manager.setItemContext({ + workspace: ws2, + key: 'jest.coverage', + value: true, + itemIds: ['c'], + }); + expect(vscode.commands.executeCommand).toHaveBeenCalledWith( + 'setContext', + 'jest.autoRun.on', + ['a', 'b', 'c'] + ); + expect(vscode.commands.executeCommand).toHaveBeenCalledWith( + 'setContext', + 'jest.autoRun.off', + ['d'] + ); + expect(vscode.commands.executeCommand).toHaveBeenCalledWith( + 'setContext', + 'jest.coverage.on', + ['c'] + ); + expect(vscode.commands.executeCommand).toHaveBeenCalledWith( + 'setContext', + 'jest.coverage.off', + [] + ); + }); }); - it('can manage itemContext for multiple workspaces', () => { - const ws1: any = { name: 'ws1' }; - const ws2: any = { name: 'ws2' }; - const manager = new TestItemContextManager(); - manager.setItemContext({ - workspace: ws1, - key: 'jest.autoRun', - value: true, - itemIds: ['a', 'b'], + describe('jest.editor-view-snapshot', () => { + it('can set context with itemId and onClick action', () => { + const workspace: any = { name: 'ws' }; + const manager = new TestItemContextManager(); + const context: any = { + workspace, + key: 'jest.editor-view-snapshot', + itemIds: ['a'], + onClick: jest.fn(), + }; + manager.setItemContext(context); + expect(vscode.commands.executeCommand).toHaveBeenCalledWith( + 'setContext', + 'jest.editor-view-snapshot', + context.itemIds + ); }); - manager.setItemContext({ workspace: ws2, key: 'jest.autoRun', value: true, itemIds: ['c'] }); - manager.setItemContext({ workspace: ws2, key: 'jest.autoRun', value: false, itemIds: ['d'] }); - manager.setItemContext({ - workspace: ws2, - key: 'jest.coverage', - value: true, - itemIds: ['c'], + it('new context will override the olde one', () => { + const workspace: any = { name: 'ws' }; + const manager = new TestItemContextManager(); + const context1: any = { + workspace, + key: 'jest.editor-view-snapshot', + itemIds: ['a'], + onClick: jest.fn(), + }; + const context2 = { ...context1, itemIds: ['b'] }; + manager.setItemContext(context1); + manager.setItemContext(context2); + expect(vscode.commands.executeCommand).toHaveBeenCalledWith( + 'setContext', + 'jest.editor-view-snapshot', + context2.itemIds + ); }); - expect(vscode.commands.executeCommand).toHaveBeenCalledWith('setContext', 'jest.autoRun.on', [ - 'a', - 'b', - 'c', - ]); - expect(vscode.commands.executeCommand).toHaveBeenCalledWith( - 'setContext', - 'jest.autoRun.off', - ['d'] - ); - expect(vscode.commands.executeCommand).toHaveBeenCalledWith( - 'setContext', - 'jest.coverage.on', - ['c'] - ); - expect(vscode.commands.executeCommand).toHaveBeenCalledWith( - 'setContext', - 'jest.coverage.off', - [] - ); }); }); describe('can register item menu commands', () => { @@ -124,5 +176,34 @@ describe('TestItemContextManager', () => { expect(vscode.commands.executeCommand).not.toHaveBeenCalledWith(extCmd, workspace); }); }); + it('view-snapshot menu commands', () => { + const manager = new TestItemContextManager(); + const disposableList = manager.registerCommands(); + expect(disposableList.length).toBeGreaterThanOrEqual(5); + + const calls = (vscode.commands.registerCommand as jest.Mocked).mock.calls.filter( + (call) => call[0] === `${extensionName}.test-item.view-snapshot` + ); + + expect(calls).toHaveLength(1); + + // set some itemContext then trigger the menu + const workspace: any = { name: 'ws' }; + const context: any = { + workspace, + key: 'jest.editor-view-snapshot', + itemIds: ['a'], + onClick: jest.fn(), + }; + manager.setItemContext(context); + const callBack = calls[0][1]; + callBack({ id: 'a' }); + expect(context.onClick).toHaveBeenCalled(); + + (context.onClick as jest.Mocked).mockClear(); + + callBack({ id: 'b' }); + expect(context.onClick).not.toHaveBeenCalled(); + }); }); }); diff --git a/tests/test-provider/test-item-data.test.ts b/tests/test-provider/test-item-data.test.ts index 51508933..4b25e360 100644 --- a/tests/test-provider/test-item-data.test.ts +++ b/tests/test-provider/test-item-data.test.ts @@ -142,9 +142,25 @@ describe('test-item-data', () => { return { wsRoot, folder, testFile, testBlock, onRunEvent, scheduleItem, file }; }; + const createAllTestItems = () => { + const wsRoot = new WorkspaceRoot(context); + const folder = new FolderData(context, 'dir', wsRoot.item); + const uri: any = { fsPath: 'whatever' }; + const doc = new TestDocumentRoot(context, uri, folder.item); + const node: any = { fullName: 'a test', attrs: {}, data: {} }; + const test = new TestData(context, uri, node, doc.item); + const snapshotProfile = { tag: { id: 'update-snapshot' } }; + const runProfile = { tag: { id: 'run' } }; + return { wsRoot, folder, doc, test, snapshotProfile, runProfile }; + }; + beforeEach(() => { controllerMock = mockController(); - const profiles: any = [{ tag: { id: 'run' } }, { tag: { id: 'debug' } }]; + const profiles: any = [ + { tag: { id: 'run' } }, + { tag: { id: 'debug' } }, + { tag: { id: 'update-snapshot' } }, + ]; context = new JestTestProviderContext(mockExtExplorerContext('ws-1'), controllerMock, profiles); context.output.write = jest.fn((t) => t); [jestRun, runEndSpy, runMock] = createTestRun(); @@ -634,6 +650,32 @@ describe('test-item-data', () => { expect(request.run).toBe(jestRun); expect(request.run.item).toBe(folderData.item); }); + describe('can update snapshot based on runProfile', () => { + let wsRoot, folder, doc, test, snapshotProfile, runProfile; + beforeEach(() => { + ({ wsRoot, folder, doc, test, snapshotProfile, runProfile } = createAllTestItems()); + }); + it('with snapshot profile', () => { + [wsRoot, folder, doc, test].forEach((testItem) => { + testItem.scheduleTest(jestRun, snapshotProfile); + expect(context.ext.session.scheduleProcess).toHaveBeenCalledWith( + expect.objectContaining({ + updateSnapshot: true, + }) + ); + }); + }); + it('no update snapshot with run profile', () => { + [wsRoot, folder, doc, test].forEach((testItem) => { + testItem.scheduleTest(jestRun, runProfile); + expect(context.ext.session.scheduleProcess).toHaveBeenCalledWith( + expect.not.objectContaining({ + updateSnapshot: true, + }) + ); + }); + }); + }); }); describe('when test result is ready', () => { @@ -999,17 +1041,13 @@ describe('test-item-data', () => { describe('tags', () => { let wsRoot, folder, doc, test; beforeEach(() => { - wsRoot = new WorkspaceRoot(context); - folder = new FolderData(context, 'dir', wsRoot.item); - const uri: any = { fsPath: 'whatever' }; - doc = new TestDocumentRoot(context, uri, folder.item); - const node: any = { fullName: 'a test', attrs: {}, data: {} }; - test = new TestData(context, uri, node, doc.item); + ({ wsRoot, folder, doc, test } = createAllTestItems()); }); - it('all TestItem supports run tag', () => { - [wsRoot, folder, doc, test].forEach((itemData) => - expect(itemData.item.tags.find((t) => t.id === 'run')).toBeTruthy() - ); + it('all TestItem supports run and update-snapshot tag', () => { + [wsRoot, folder, doc, test].forEach((itemData) => { + expect(itemData.item.tags.find((t) => t.id === 'run')).toBeTruthy(); + expect(itemData.item.tags.find((t) => t.id === 'update-snapshot')).toBeTruthy(); + }); }); it('only TestData and TestDocument supports debug tags', () => { [doc, test].forEach((itemData) => @@ -1458,4 +1496,47 @@ describe('test-item-data', () => { }); }); }); + // describe('snapshow preview', () => { + // let wsRoot, folder, doc, test; + // beforeEach(() => { + // ({ wsRoot, folder, doc, test } = createAllTestItems()); + // }); + // it('update menu context for eligibile test items', () => { + // const a1 = helper.makeAssertion('test-a', 'KnownFail', ['desc-1'], [1, 0]); + // const assertionContainer = buildAssertionContainer([a1]); + // context.ext.testResolveProvider.getTestSuiteResult.mockReturnValue({ + // status: 'KnownFail', + // assertionContainer, + // }); + + // // 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: { type: 'watch-tests' } }, + // files: ['/ws-1/a.test.ts'], + // }); + + // // change the node + // const descNode = assertionContainer.childContainers[0]; + // descNode.attrs.range = { + // start: { line: 1, column: 2 }, + // end: { line: 13, column: 4 }, + // }; + // const testNode = descNode.childData[0]; + // testNode.attrs.range = { + // start: { line: 2, column: 2 }, + // end: { line: 10, column: 4 }, + // }; + + // // triggers testSuiteChanged event + // context.ext.testResolveProvider.events.testSuiteChanged.event.mock.calls[0][0]({ + // type: 'result-matched', + // file: '/ws-1/a.test.ts', + // }); + + // // should update the context with snapshot items + // expect(true).toEqual(true); + // }); + // it.todo('trigger the test item to perform previewSnapshot upon menu click'); + // }); }); From bb34c2cd7988672013d1b2f901357e95914f6d95 Mon Sep 17 00:00:00 2001 From: connectdotz Date: Fri, 18 Nov 2022 17:51:15 -0500 Subject: [PATCH 11/16] use context menu for all snapshot operations --- package.json | 18 +- src/JestExt/core.ts | 5 +- src/extensionManager.ts | 8 + .../test-item-context-manager.ts | 34 ++- src/test-provider/test-item-data.ts | 193 ++++++++++++------ src/test-provider/test-provider-helper.ts | 11 +- src/test-provider/test-provider.ts | 17 +- src/test-provider/types.ts | 9 +- 8 files changed, 207 insertions(+), 88 deletions(-) diff --git a/package.json b/package.json index 07051313..37773ffd 100644 --- a/package.json +++ b/package.json @@ -302,8 +302,13 @@ }, { "command": "io.orta.jest.test-item.view-snapshot", - "title": "View snapshot", + "title": "View Snapshot", "icon": "$(camera)" + }, + { + "command": "io.orta.jest.test-item.update-snapshot", + "title": "Update Snapshot", + "icon": "$(export)" } ], "menus": { @@ -368,13 +373,20 @@ "command": "io.orta.jest.test-item.coverage.toggle-on", "group": "inline", "when": "testId in jest.coverage.off" - } + }, + { + "command": "io.orta.jest.test-item.update-snapshot" + } ], "testing/item/gutter": [ { "command": "io.orta.jest.test-item.view-snapshot", "when": "testId in jest.editor-view-snapshot" - } + }, + { + "command": "io.orta.jest.test-item.update-snapshot", + "when": "testId in jest.editor-update-snapshot" + } ] }, "keybindings": [ diff --git a/src/JestExt/core.ts b/src/JestExt/core.ts index 64f04baf..68b8ee18 100644 --- a/src/JestExt/core.ts +++ b/src/JestExt/core.ts @@ -24,7 +24,7 @@ import { extensionName, SupportedLanguageIds } from '../appGlobals'; import { createJestExtContext, getExtensionResourceSettings, prefixWorkspace } from './helper'; import { PluginResourceSettings } from '../Settings'; import { WizardTaskId } from '../setup-wizard'; -import { JestExtExplorerContext } from '../test-provider/types'; +import { ItemCommand, JestExtExplorerContext } from '../test-provider/types'; import { JestTestProvider } from '../test-provider'; import { JestProcessInfo } from '../JestProcessManagement'; import { addFolderToDisabledWorkspaceFolders } from '../extensionManager'; @@ -631,6 +631,9 @@ export class JestExt { // restart jest since coverage condition has changed this.triggerUpdateSettings(this.extContext.settings); } + runItemCommand(testItem: vscode.TestItem, itemCommand: ItemCommand): void { + this.testProvider?.runItemCommand(testItem, itemCommand); + } enableLoginShell(): void { if (this.extContext.settings.shell.useLoginShell) { return; diff --git a/src/extensionManager.ts b/src/extensionManager.ts index 565a8238..4ba6e47c 100644 --- a/src/extensionManager.ts +++ b/src/extensionManager.ts @@ -13,6 +13,7 @@ import { StartWizardOptions, WizardTaskId, } from './setup-wizard'; +import { ItemCommand } from './test-provider/types'; export type GetJestExtByURI = (uri: vscode.Uri) => JestExt | undefined; @@ -407,6 +408,13 @@ export class ExtensionManager { extension.enableLoginShell(); }, }), + this.registerCommand({ + type: 'workspace', + name: 'item-command', + callback: (extension, testItem: vscode.TestItem, itemCommand: ItemCommand) => { + extension.runItemCommand(testItem, itemCommand); + }, + }), // setup tool vscode.commands.registerCommand(`${extensionName}.setup-extension`, this.startWizard), diff --git a/src/test-provider/test-item-context-manager.ts b/src/test-provider/test-item-context-manager.ts index 2ac80a2a..93223d9c 100644 --- a/src/test-provider/test-item-context-manager.ts +++ b/src/test-provider/test-item-context-manager.ts @@ -1,5 +1,6 @@ import * as vscode from 'vscode'; import { extensionName } from '../appGlobals'; +import { ItemCommand } from './types'; /** * A cross-workspace Context Manager that manages the testId context used in @@ -19,10 +20,9 @@ export type ItemContext = itemIds: string[]; } | { - key: 'jest.editor-view-snapshot'; + key: 'jest.editor-view-snapshot' | 'jest.editor-update-snapshot'; workspace: vscode.WorkspaceFolder; itemIds: string[]; - onClick: (testItem: vscode.TestItem) => void; }; export type TEItemContextKey = ItemContext['key']; @@ -57,7 +57,8 @@ export class TestItemContextManager { vscode.commands.executeCommand('setContext', this.contextKey(context.key, false), itemIds); break; } - case 'jest.editor-view-snapshot': { + case 'jest.editor-view-snapshot': + case 'jest.editor-update-snapshot': { this.cache.set(context.key, [context]); vscode.commands.executeCommand('setContext', context.key, context.itemIds); break; @@ -96,13 +97,32 @@ export class TestItemContextManager { const viewSnapshotCommand = vscode.commands.registerCommand( `${extensionName}.test-item.view-snapshot`, (testItem: vscode.TestItem) => { - const context = this.getItemContext('jest.editor-view-snapshot', testItem); - if (context && context.key === 'jest.editor-view-snapshot') { - context.onClick(testItem); + const workspace = this.getItemContext('jest.editor-view-snapshot', testItem)?.workspace; + if (workspace) { + vscode.commands.executeCommand( + `${extensionName}.with-workspace.item-command`, + workspace, + testItem, + ItemCommand.viewSnapshot + ); } } ); - return [...autoRunCommands, ...coverageCommands, viewSnapshotCommand]; + const updateSnapshotCommand = vscode.commands.registerCommand( + `${extensionName}.test-item.update-snapshot`, + (testItem: vscode.TestItem) => { + const workspace = this.getItemContext('jest.editor-update-snapshot', testItem)?.workspace; + if (workspace) { + vscode.commands.executeCommand( + `${extensionName}.with-workspace.item-command`, + workspace, + testItem, + ItemCommand.updateSnapshot + ); + } + } + ); + return [...autoRunCommands, ...coverageCommands, viewSnapshotCommand, updateSnapshotCommand]; } } diff --git a/src/test-provider/test-item-data.ts b/src/test-provider/test-item-data.ts index 4797ae36..85dfb5e9 100644 --- a/src/test-provider/test-item-data.ts +++ b/src/test-provider/test-item-data.ts @@ -8,7 +8,7 @@ import { ItBlock, TestAssertionStatus } from 'jest-editor-support'; import { ContainerNode, DataNode, NodeType, ROOT_NODE_NAME } from '../TestResults/match-node'; import { Logging } from '../logging'; import { TestSuitChangeEvent } from '../TestResults/test-result-events'; -import { Debuggable, TestItemData, TestTagId } from './types'; +import { Debuggable, ItemCommand, TestItemData } from './types'; import { JestTestProviderContext, JestTestRun, JestTestRunOptions } from './test-provider-helper'; import { JestProcessInfo, JestProcessRequest } from '../JestProcessManagement'; import { GENERIC_ERROR, getExitErrorDef, LONG_RUNNING_TESTS } from '../errors'; @@ -53,8 +53,8 @@ abstract class TestItemDataBase implements TestItemData, JestRunable, WithUri { item.children.forEach((child) => this.deepItemState(child, setState)); } - scheduleTest(run: JestTestRun, runProfile?: vscode.TestRunProfile): void { - const jestRequest = this.getJestRunRequest(runProfile); + scheduleTest(run: JestTestRun, itemCommand?: ItemCommand): void { + const jestRequest = this.getJestRunRequest(itemCommand); run.item = this.item; this.deepItemState(this.item, run.enqueued); @@ -71,7 +71,33 @@ abstract class TestItemDataBase implements TestItemData, JestRunable, WithUri { } } - abstract getJestRunRequest(runProfile?: vscode.TestRunProfile): JestExtRequestType; + runItemCommand(command: ItemCommand): void { + switch (command) { + case ItemCommand.updateSnapshot: { + const request = new vscode.TestRunRequest([this.item]); + const run = this.context.createTestRun(request, { + name: `${command}-${this.item.id}`, + }); + this.scheduleTest(run, command); + break; + } + case ItemCommand.viewSnapshot: { + this.viewSnapshot(); + break; + } + } + } + viewSnapshot(): Promise { + const msg = `viewSnapshot is not supported for ${this.item.id}`; + this.log('warn', msg); + return Promise.reject(msg); + } + abstract getJestRunRequest(itemCommand?: ItemCommand): JestExtRequestType; +} + +interface SnapshotItemCollection { + viewable: vscode.TestItem[]; + updatable: vscode.TestItem[]; } /** @@ -99,7 +125,7 @@ export class WorkspaceRoot extends TestItemDataBase { this.context.ext.workspace.uri, this, undefined, - ['run', 'update-snapshot'] + ['run'] ); item.description = `(${this.context.ext.settings.autoRun.mode})`; @@ -107,12 +133,12 @@ export class WorkspaceRoot extends TestItemDataBase { return item; } - getJestRunRequest(runProfile?: vscode.TestRunProfile): JestExtRequestType { + getJestRunRequest(itemCommand?: ItemCommand): JestExtRequestType { const transform = (request: JestProcessRequest) => { request.schedule.queue = 'blocking-2'; return request; }; - const updateSnapshot = runProfile?.tag?.id === TestTagId.UpdateSnapshot; + const updateSnapshot = itemCommand === ItemCommand.updateSnapshot; return { type: 'all-tests', updateSnapshot, transform }; } discoverTest(run: JestTestRun): void { @@ -149,6 +175,15 @@ export class WorkspaceRoot extends TestItemDataBase { item, }); }; + private traverseDataTree(data: TestItemData, onItemData: (data: TestItemData) => void): void { + onItemData(data); + data.item.children.forEach((item) => { + const child = this.context.getData(item); + if (child) { + this.traverseDataTree(child, onItemData); + } + }); + } private addFolder = (parent: FolderData | undefined, folderName: string): FolderData => { const p = parent ?? this; @@ -240,35 +275,43 @@ export class WorkspaceRoot extends TestItemDataBase { break; } case 'result-matched': { - const extSnapshotItems: vscode.TestItem[] = []; - this.addTestFile(event.file, (testRoot) => testRoot.onTestMatched(extSnapshotItems)); - tiContextManager.setItemContext({ - workspace: this.context.ext.workspace, - key: 'jest.editor-view-snapshot', - itemIds: extSnapshotItems.map((item) => item.id), - onClick: this.onPreviewSnapshot, + const snapshotItems: SnapshotItemCollection = { + viewable: [], + updatable: [], + }; + this.addTestFile(event.file, (testRoot) => { + testRoot.onTestMatched(); + testRoot.gatherSnapshotItems(snapshotItems); }); + this.updateSnapshotContext(snapshotItems); break; } case 'test-parsed': { - this.addTestFile(event.file, (testRoot) => - testRoot.discoverTest(undefined, event.sourceContainer) - ); + const snapshotItems: SnapshotItemCollection = { + viewable: [], + updatable: [], + }; + this.addTestFile(event.file, (testRoot) => { + testRoot.discoverTest(undefined, event.sourceContainer); + testRoot.gatherSnapshotItems(snapshotItems); + }); + this.updateSnapshotContext(snapshotItems); break; } } }; - - private onPreviewSnapshot = (testItem: vscode.TestItem): Promise => { - const data = this.context.getData(testItem); - if (data instanceof TestData) { - return data.previewSnapshot(); - } - vscode.window.showErrorMessage( - `Preview snapshot failed: unexpected test block: ${testItem.id}` - ); - return Promise.resolve(); - }; + private updateSnapshotContext(snapshotItems: SnapshotItemCollection): void { + tiContextManager.setItemContext({ + workspace: this.context.ext.workspace, + key: 'jest.editor-view-snapshot', + itemIds: snapshotItems.viewable.map((item) => item.id), + }); + tiContextManager.setItemContext({ + workspace: this.context.ext.workspace, + key: 'jest.editor-update-snapshot', + itemIds: snapshotItems.updatable.map((item) => item.id), + }); + } /** get test item from jest process. If running tests from source file, will return undefined */ private getItemFromProcess = (process: JestProcessInfo): vscode.TestItem | undefined => { @@ -419,16 +462,13 @@ export class FolderData extends TestItemDataBase { } private createTestItem(name: string, parent: vscode.TestItem) { const uri = FolderData.makeUri(parent, name); - const item = this.context.createTestItem(uri.fsPath, name, uri, this, parent, [ - 'run', - 'update-snapshot', - ]); + const item = this.context.createTestItem(uri.fsPath, name, uri, this, parent, ['run']); item.canResolveChildren = false; return item; } - getJestRunRequest(runProfile?: vscode.TestRunProfile): JestExtRequestType { - const updateSnapshot = runProfile?.tag?.id === TestTagId.UpdateSnapshot; + getJestRunRequest(itemCommand?: ItemCommand): JestExtRequestType { + const updateSnapshot = itemCommand === ItemCommand.updateSnapshot; return { type: 'by-file-pattern', updateSnapshot, @@ -521,6 +561,7 @@ abstract class TestResultData extends TestItemDataBase { const truncateExtra = (id: string): string => id.replace(/(.*)(#[0-9]+$)/, '$1'); return truncateExtra(id1) === truncateExtra(id2); } + syncChildNodes(node: ItemNodeType): void { const testId = this.makeTestId(this.uri, node); if (!this.isSameId(testId, this.item.id)) { @@ -567,6 +608,15 @@ abstract class TestResultData extends TestItemDataBase { new vscode.Range(new vscode.Position(zeroBasedLine, 0), new vscode.Position(zeroBasedLine, 0)) ); } + + forEachChild(onTestData: (child: TestData) => void): void { + this.item.children.forEach((childItem) => { + const child = this.context.getData(childItem); + if (child) { + onTestData(child); + } + }); + } } export class TestDocumentRoot extends TestResultData { constructor( @@ -620,14 +670,11 @@ export class TestDocumentRoot extends TestResultData { if (isEmpty(suiteResult?.assertionContainer)) { this.updateItemState(run, suiteResult); } - - this.item.children.forEach((childItem) => - this.context.getData(childItem)?.updateResultState(run) - ); + this.forEachChild((child) => child.updateResultState(run)); } - getJestRunRequest(runProfile?: vscode.TestRunProfile): JestExtRequestType { - const updateSnapshot = runProfile?.tag?.id === TestTagId.UpdateSnapshot; + getJestRunRequest(itemCommand?: ItemCommand): JestExtRequestType { + const updateSnapshot = itemCommand === ItemCommand.updateSnapshot; return { type: 'by-file-pattern', updateSnapshot, @@ -638,11 +685,13 @@ export class TestDocumentRoot extends TestResultData { getDebugInfo(): ReturnType { return { fileName: this.uri.fsPath }; } - public onTestMatched = (extSnapshotItems: vscode.TestItem[]): void => { - this.item.children.forEach((childItem) => - this.context.getData(childItem)?.onTestMatched(extSnapshotItems) - ); - }; + + public onTestMatched(): void { + this.forEachChild((child) => child.onTestMatched()); + } + public gatherSnapshotItems(snapshotItems: SnapshotItemCollection): void { + this.forEachChild((child) => child.gatherSnapshotItems(snapshotItems)); + } } export class TestData extends TestResultData implements Debuggable { constructor( @@ -670,15 +719,15 @@ export class TestData extends TestResultData implements Debuggable { return item; } - getJestRunRequest(runProfile?: vscode.TestRunProfile): JestExtRequestType { - const updateSnapshot = runProfile?.tag?.id === TestTagId.UpdateSnapshot; + getJestRunRequest(itemCommand?: ItemCommand): JestExtRequestType { return { type: 'by-file-test-pattern', - updateSnapshot, + updateSnapshot: itemCommand === ItemCommand.updateSnapshot, testFileNamePattern: this.uri.fsPath, testNamePattern: this.node.fullName, }; } + getDebugInfo(): ReturnType { return { fileName: this.uri.fsPath, testNamePattern: this.node.fullName }; } @@ -704,17 +753,35 @@ export class TestData extends TestResultData implements Debuggable { this.syncChildNodes(node); } - public onTestMatched(extSnapshotItems: vscode.TestItem[]): void { + public onTestMatched(): void { // assertion might have picked up source block location this.updateItemRange(); + this.forEachChild((child) => child.onTestMatched()); + } + + /** + * determine if a test contains dynamic content, such as template-literal or "test.each" variables from the node info. + * Once the test is run, the node should reflect the resolved names. + */ + private isTestNameResolved(): boolean { + //isGroup = true means "test.each" + return !(this.node.attrs.isGroup === 'yes' || this.node.attrs.nonLiteralName === true); + } + public gatherSnapshotItems(snapshotItems: SnapshotItemCollection): void { + // only response if not a "dynamic named" test, which we can't update or view snapshot until the names are resolved + // after running the tests + if (!this.isTestNameResolved()) { + return; + } + if (this.node.attrs.snapshot === 'inline') { + snapshotItems.updatable.push(this.item); + } if (this.node.attrs.snapshot === 'external') { - extSnapshotItems.push(this.item); + snapshotItems.updatable.push(this.item); + snapshotItems.viewable.push(this.item); } - this.item.children.forEach((childItem) => - this.context.getData(childItem)?.onTestMatched(extSnapshotItems) - ); + this.forEachChild((child) => child.gatherSnapshotItems(snapshotItems)); } - public updateResultState(run: JestTestRun): void { if (this.node && isAssertDataNode(this.node)) { const assertion = this.node.data; @@ -722,14 +789,16 @@ export class TestData extends TestResultData implements Debuggable { assertion.line != null ? this.createLocation(this.uri, assertion.line - 1) : undefined; this.updateItemState(run, assertion, errorLine); } - this.item.children.forEach((childItem) => - this.context.getData(childItem)?.updateResultState(run) - ); + this.forEachChild((child) => child.updateResultState(run)); } - public previewSnapshot(): Promise { - return this.context.ext.testResolveProvider.previewSnapshot( - this.uri.fsPath, - this.node.fullName - ); + public viewSnapshot(): Promise { + if (this.node.attrs.snapshot === 'external') { + return this.context.ext.testResolveProvider.previewSnapshot( + this.uri.fsPath, + this.node.fullName + ); + } + this.log('error', `no external snapshot to be viewed: ${this.item.id}`); + return Promise.resolve(); } } diff --git a/src/test-provider/test-provider-helper.ts b/src/test-provider/test-provider-helper.ts index 64b81a00..4c2fda97 100644 --- a/src/test-provider/test-provider-helper.ts +++ b/src/test-provider/test-provider-helper.ts @@ -32,7 +32,7 @@ export class JestTestProviderContext { uri: vscode.Uri, data: TestItemData, parent?: vscode.TestItem, - tagIds: TagIdType[] = ['run', 'debug', 'update-snapshot'] + tagIds: TagIdType[] = ['run', 'debug'] ): vscode.TestItem => { const testItem = this.controller.createTestItem(id, label, uri); this.testItemData.set(testItem, data); @@ -84,8 +84,13 @@ export class JestTestProviderContext { }; // tags - getTag = (tagId: TagIdType): vscode.TestTag | undefined => - this.profiles.find((p) => p.tag?.id === tagId)?.tag; + getTag = (tagId: TagIdType): vscode.TestTag => { + const tag = this.profiles.find((p) => p.tag?.id === tagId)?.tag; + if (!tag) { + throw new Error(`unrecognized tag: ${tagId}`); + } + return tag; + }; } export interface JestTestRunOptions { diff --git a/src/test-provider/test-provider.ts b/src/test-provider/test-provider.ts index be168149..c07da3bf 100644 --- a/src/test-provider/test-provider.ts +++ b/src/test-provider/test-provider.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; import { JestTestProviderContext, JestTestRun } from './test-provider-helper'; import { WorkspaceRoot } from './test-item-data'; -import { Debuggable, JestExtExplorerContext, TestItemData, TestTagId } from './types'; +import { Debuggable, ItemCommand, JestExtExplorerContext, TestItemData, TestTagId } from './types'; import { extensionId } from '../appGlobals'; import { Logging } from '../logging'; import { toErrorString } from '../helpers'; @@ -59,7 +59,6 @@ export class JestTestProvider { }; private createProfiles = (controller: vscode.TestController): vscode.TestRunProfile[] => { const runTag = new vscode.TestTag(TestTagId.Run); - const updateSnapshotTag = new vscode.TestTag(TestTagId.UpdateSnapshot); const debugTag = new vscode.TestTag(TestTagId.Debug); const profiles = [ controller.createRunProfile( @@ -69,13 +68,6 @@ export class JestTestProvider { true, runTag ), - controller.createRunProfile( - 'update snapshot', - vscode.TestRunProfileKind.Run, - this.runTests, - false, - updateSnapshotTag - ), controller.createRunProfile( 'debug', vscode.TestRunProfileKind.Debug, @@ -173,7 +165,7 @@ export class JestTestProvider { item: test, end: resolve, }); - tData.scheduleTest(itemRun, request.profile); + tData.scheduleTest(itemRun); } catch (e) { const msg = `failed to schedule test for ${tData.item.id}: ${toErrorString(e)}`; this.log('error', msg, e); @@ -193,6 +185,11 @@ export class JestTestProvider { run.end(); }; + public runItemCommand(testItem: vscode.TestItem, command: ItemCommand): void { + const data = this.context.getData(testItem); + return data?.runItemCommand(command); + } + dispose(): void { this.workspaceRoot.dispose(); this.controller.dispose(); diff --git a/src/test-provider/types.ts b/src/test-provider/types.ts index 8f0430de..eeefd541 100644 --- a/src/test-provider/types.ts +++ b/src/test-provider/types.ts @@ -18,7 +18,8 @@ export interface TestItemData { readonly uri: vscode.Uri; context: JestTestProviderContext; discoverTest?: (run: JestTestRun) => void; - scheduleTest: (run: JestTestRun, runProfile?: vscode.TestRunProfile) => void; + scheduleTest: (run: JestTestRun, itemCommand?: ItemCommand) => void; + runItemCommand: (command: ItemCommand) => void; } export interface Debuggable { @@ -27,6 +28,10 @@ export interface Debuggable { export enum TestTagId { Run = 'run', - UpdateSnapshot = 'update-snapshot', Debug = 'debug', } + +export enum ItemCommand { + updateSnapshot = 'update-snapshot', + viewSnapshot = 'view-snapshot', +} From f013559c926374cda3d64ccd1a396818581e03c3 Mon Sep 17 00:00:00 2001 From: connectdotz Date: Sat, 19 Nov 2022 17:30:18 -0500 Subject: [PATCH 12/16] adding tests for changes --- __mocks__/vscode.ts | 6 + src/TestResults/snapshot-provider.ts | 10 +- src/test-provider/test-item-data.ts | 18 +- src/test-provider/test-provider.ts | 2 +- tests/JestExt/core.test.ts | 12 ++ tests/TestResults/TestResultProvider.test.ts | 64 +++++- tests/TestResults/snapshot-provider.test.ts | 116 +++++++++++ tests/extensionManager.test.ts | 2 + tests/test-helper.ts | 10 + tests/test-provider/test-helper.ts | 1 + .../test-item-context-manager.test.ts | 64 +++--- tests/test-provider/test-item-data.test.ts | 186 +++++++++++------- .../test-provider-helper.test.ts | 9 + tests/test-provider/test-provider.test.ts | 12 +- 14 files changed, 389 insertions(+), 123 deletions(-) create mode 100644 tests/TestResults/snapshot-provider.test.ts diff --git a/__mocks__/vscode.ts b/__mocks__/vscode.ts index 483ceef2..f0883d3f 100644 --- a/__mocks__/vscode.ts +++ b/__mocks__/vscode.ts @@ -17,6 +17,7 @@ const window = { showWorkspaceFolderPick: jest.fn(), onDidChangeActiveTextEditor: jest.fn(), showInformationMessage: jest.fn(), + createWebviewPanel: jest.fn(), }; const workspace = { @@ -79,6 +80,10 @@ const TestRunProfileKind = { Debug: 2, Coverage: 3, }; +const ViewColumn = { + One: 1, + Tow: 2, +}; const TestMessage = jest.fn(); const TestRunRequest = jest.fn(); @@ -112,4 +117,5 @@ export = { EventEmitter, TestMessage, TestRunRequest, + ViewColumn, }; diff --git a/src/TestResults/snapshot-provider.ts b/src/TestResults/snapshot-provider.ts index b7e74a32..a165f60a 100644 --- a/src/TestResults/snapshot-provider.ts +++ b/src/TestResults/snapshot-provider.ts @@ -13,16 +13,16 @@ export interface SnapshotSuite { const inlineKeys = ['toMatchInlineSnapshot', 'toThrowErrorMatchingInlineSnapshot']; export class SnapshotProvider { - private snapshots: Snapshot; + private snapshotSupport: Snapshot; private panel?: vscode.WebviewPanel; constructor() { - this.snapshots = new Snapshot(undefined, inlineKeys); + this.snapshotSupport = new Snapshot(undefined, inlineKeys); } public parse(testPath: string): SnapshotSuite { try { - const sBlocks = this.snapshots.parse(testPath); + const sBlocks = this.snapshotSupport.parse(testPath); const blocks = sBlocks.map((block) => ({ ...block, isInline: inlineKeys.find((key) => block.node.name.includes(key)) ? true : false, @@ -30,12 +30,12 @@ export class SnapshotProvider { const snapshotSuite = { testPath, blocks }; return snapshotSuite; } catch (e) { - console.warn('[SnapshotProvider] getMetadataAsync failed:', e); + console.warn('[SnapshotProvider] parse failed:', e); return { testPath, blocks: [] }; } } public async getContent(testPath: string, testFullName: string): Promise { - return this.snapshots.getSnapshotContent(testPath, testFullName); + return this.snapshotSupport.getSnapshotContent(testPath, testFullName); } private escapeContent = (content: string) => { if (content) { diff --git a/src/test-provider/test-item-data.ts b/src/test-provider/test-item-data.ts index 85dfb5e9..602c0f46 100644 --- a/src/test-provider/test-item-data.ts +++ b/src/test-provider/test-item-data.ts @@ -71,7 +71,7 @@ abstract class TestItemDataBase implements TestItemData, JestRunable, WithUri { } } - runItemCommand(command: ItemCommand): void { + runItemCommand(command: ItemCommand): void | Promise { switch (command) { case ItemCommand.updateSnapshot: { const request = new vscode.TestRunRequest([this.item]); @@ -82,15 +82,12 @@ abstract class TestItemDataBase implements TestItemData, JestRunable, WithUri { break; } case ItemCommand.viewSnapshot: { - this.viewSnapshot(); - break; + return this.viewSnapshot().catch((e) => this.log('error', e)); } } } viewSnapshot(): Promise { - const msg = `viewSnapshot is not supported for ${this.item.id}`; - this.log('warn', msg); - return Promise.reject(msg); + return Promise.reject(`viewSnapshot is not supported for ${this.item.id}`); } abstract getJestRunRequest(itemCommand?: ItemCommand): JestExtRequestType; } @@ -175,15 +172,6 @@ export class WorkspaceRoot extends TestItemDataBase { item, }); }; - private traverseDataTree(data: TestItemData, onItemData: (data: TestItemData) => void): void { - onItemData(data); - data.item.children.forEach((item) => { - const child = this.context.getData(item); - if (child) { - this.traverseDataTree(child, onItemData); - } - }); - } private addFolder = (parent: FolderData | undefined, folderName: string): FolderData => { const p = parent ?? this; diff --git a/src/test-provider/test-provider.ts b/src/test-provider/test-provider.ts index c07da3bf..cd509d9e 100644 --- a/src/test-provider/test-provider.ts +++ b/src/test-provider/test-provider.ts @@ -185,7 +185,7 @@ export class JestTestProvider { run.end(); }; - public runItemCommand(testItem: vscode.TestItem, command: ItemCommand): void { + public runItemCommand(testItem: vscode.TestItem, command: ItemCommand): void | Promise { const data = this.context.getData(testItem); return data?.runItemCommand(command); } diff --git a/tests/JestExt/core.test.ts b/tests/JestExt/core.test.ts index cc0d836b..9df82bd5 100644 --- a/tests/JestExt/core.test.ts +++ b/tests/JestExt/core.test.ts @@ -43,6 +43,7 @@ import { addFolderToDisabledWorkspaceFolders } from '../../src/extensionManager' import { JestOutputTerminal } from '../../src/JestExt/output-terminal'; import { RunShell } from '../../src/JestExt/run-shell'; import * as errors from '../../src/errors'; +import { ItemCommand } from '../../src/test-provider/types'; /* eslint jest/expect-expect: ["error", { "assertFunctionNames": ["expect", "expectItTakesNoAction"] }] */ const mockHelpers = helper as jest.Mocked; @@ -112,6 +113,7 @@ describe('JestExt', () => { const mockTestProvider: any = { dispose: jest.fn(), + runItemCommand: jest.fn(), }; beforeEach(() => { @@ -1245,4 +1247,14 @@ describe('JestExt', () => { ); }); }); + it('runItemCommand will delegate operation to testProvider', () => { + const jestExt = newJestExt(); + jestExt.startSession(); + const testItem: any = {}; + jestExt.runItemCommand(testItem, ItemCommand.updateSnapshot); + expect(mockTestProvider.runItemCommand).toHaveBeenCalledWith( + testItem, + ItemCommand.updateSnapshot + ); + }); }); diff --git a/tests/TestResults/TestResultProvider.test.ts b/tests/TestResults/TestResultProvider.test.ts index 3418a6a8..864375c0 100644 --- a/tests/TestResults/TestResultProvider.test.ts +++ b/tests/TestResults/TestResultProvider.test.ts @@ -41,7 +41,7 @@ import * as helper from '../test-helper'; import { ItBlock, TestAssertionStatus, TestReconcilationState } from 'jest-editor-support'; import * as match from '../../src/TestResults/match-by-context'; import { mockJestExtEvents } from '../test-helper'; -import { SnapshotProvider } from '../../src/TestResults/snapshot-provider'; +import { ExtSnapshotBlock, SnapshotProvider } from '../../src/TestResults/snapshot-provider'; const setupMockParse = (itBlocks: ItBlock[]) => { mockParse.mockReturnValue({ @@ -51,7 +51,7 @@ const setupMockParse = (itBlocks: ItBlock[]) => { }); }; -const createDataSet = (): [ItBlock[], TestAssertionStatus[]] => { +const createDataSet = (): [ItBlock[], TestAssertionStatus[], ExtSnapshotBlock[]] => { const testBlocks = [ helper.makeItBlock('test 1', [2, 3, 4, 5]), helper.makeItBlock('test 2', [12, 13, 14, 15]), @@ -66,7 +66,11 @@ const createDataSet = (): [ItBlock[], TestAssertionStatus[]] => { helper.makeAssertion('test 4', TestReconciliationState.Unknown, undefined, [32, 0]), helper.makeAssertion('test 5', TestReconciliationState.KnownSuccess, undefined, [42, 0]), ]; - return [testBlocks, assertions]; + const snapshots = [ + helper.makeSnapshotBlock('test 2', false, 13), + helper.makeSnapshotBlock('test 5', true, 43), + ]; + return [testBlocks, assertions, snapshots]; }; interface TestData { @@ -155,6 +159,7 @@ describe('TestResultProvider', () => { (vscode.EventEmitter as jest.Mocked) = jest.fn().mockImplementation(helper.mockEvent); mockSnapshotProvider = { parse: jest.fn().mockReturnValue({ blocks: [] }), + previewSnapshot: jest.fn().mockReturnValue(Promise.resolve()), }; (SnapshotProvider as jest.Mocked).mockReturnValue(mockSnapshotProvider); }); @@ -936,4 +941,57 @@ describe('TestResultProvider', () => { expect(sut.isTestFile(target)).toEqual(expected); }); }); + describe('snapshot', () => { + const testPath = 'test-file'; + let itBlocks, assertions, snapshotBlocks; + beforeEach(() => { + [itBlocks, assertions, snapshotBlocks] = createDataSet(); + const dBlock0 = helper.makeDescribeBlock('describe-test-1', [itBlocks[0]], { + start: itBlocks[0].start, + end: itBlocks[0].end, + }); + const dBlock4 = helper.makeDescribeBlock('describe-test-5', [itBlocks[4]], { + start: itBlocks[4].start, + end: itBlocks[4].end, + }); + itBlocks[0] = dBlock0; + itBlocks[4] = dBlock4; + + mockSnapshotProvider.parse.mockImplementation((testPath: string) => ({ + testPath, + blocks: snapshotBlocks, + })); + }); + it('parsing test file should fire event for testBlocks with snapshot info', () => { + const sut = newProviderWithData([makeData(itBlocks, assertions, testPath)]); + sut.getResults(testPath); + const testParsedCall = (sut.events.testSuiteChanged.fire as jest.Mocked).mock.calls.find( + (call) => call[0].type === 'test-parsed' + ); + expect(testParsedCall).not.toBeUndefined(); + const sourceContainer = testParsedCall[0].sourceContainer; + let matchCount = 0; + [ + ...sourceContainer.childContainers.flatMap((c) => c.childData), + ...sourceContainer.childData, + ].forEach((child) => { + const sBlock = snapshotBlocks.find((block) => block.marker === child.name); + if (sBlock) { + expect(child.attrs.snapshot).toEqual(sBlock.isInline ? 'inline' : 'external'); + matchCount += 1; + } else { + expect(child.attrs.snapshot).toBeUndefined(); + } + }); + expect(matchCount).toEqual(2); + }); + it('forward previewSnapshot to the snapshot provider', async () => { + const sut = newProviderWithData([makeData([], [], '')]); + await sut.previewSnapshot('whatever', 'full test name'); + expect(mockSnapshotProvider.previewSnapshot).toHaveBeenCalledWith( + 'whatever', + 'full test name' + ); + }); + }); }); diff --git a/tests/TestResults/snapshot-provider.test.ts b/tests/TestResults/snapshot-provider.test.ts new file mode 100644 index 00000000..2f533f5a --- /dev/null +++ b/tests/TestResults/snapshot-provider.test.ts @@ -0,0 +1,116 @@ +jest.unmock('../../src/TestResults/snapshot-provider'); + +import * as vscode from 'vscode'; +import { SnapshotProvider } from '../../src/TestResults/snapshot-provider'; + +const mockSnapshot = { + parse: jest.fn(), + getSnapshotContent: jest.fn(), +}; +jest.mock('jest-editor-support', () => { + const Snapshot = jest.fn(() => mockSnapshot); + return { Snapshot }; +}); + +const makeSnapshotNode = (name: string): any => ({ + node: { name }, +}); +describe('SnapshotProvider', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + describe('parse', () => { + it.each` + keyword | isInline + ${'toMatchSnapshot'} | ${false} + ${'toThrowErrorMatchingSnapshot'} | ${false} + ${'toMatchInlineSnapshot'} | ${true} + ${'toThrowErrorMatchingInlineSnapshot'} | ${true} + `('returns parsed result: $keyword', ({ keyword, isInline }) => { + const parseBlocks = [makeSnapshotNode(keyword)]; + mockSnapshot.parse.mockReturnValue(parseBlocks); + const provider = new SnapshotProvider(); + expect(provider.parse('a test file')).toEqual({ + testPath: 'a test file', + blocks: [{ ...parseBlocks[0], isInline }], + }); + }); + it('if parse failed, returns empty blocks', () => { + mockSnapshot.parse.mockImplementation(() => { + throw new Error('forced error'); + }); + const provider = new SnapshotProvider(); + expect(provider.parse('a test file')).toEqual({ + testPath: 'a test file', + blocks: [], + }); + }); + }); + describe('getSnapshotContent', () => { + it.each` + case | impl | expected + ${1} | ${() => Promise.resolve('something')} | ${'something'} + ${2} | ${() => Promise.resolve()} | ${undefined} + ${3} | ${() => Promise.reject('error')} | ${'throws'} + `('$case: forward call to Snapshot', async ({ impl, expected }) => { + mockSnapshot.getSnapshotContent.mockImplementation(impl); + const provider = new SnapshotProvider(); + if (expected === 'throws') { + await expect(provider.getContent('whatever', 'whatever')).rejects.toEqual('error'); + } else { + await expect(provider.getContent('whatever', 'whatever')).resolves.toEqual(expected); + } + }); + }); + describe('previewSnapshot', () => { + it('display content in a WebviewPanel', async () => { + const content1 = ' result'; + const content2 = ' "some quoted text"'; + const content3 = " 'single quote' & this"; + mockSnapshot.getSnapshotContent + .mockReturnValueOnce(Promise.resolve(content1)) + .mockReturnValueOnce(Promise.resolve(content2)) + .mockReturnValueOnce(Promise.resolve(content3)); + const mockPanel = { + reveal: jest.fn(), + onDidDispose: jest.fn(), + webview: { html: undefined }, + title: undefined, + }; + (vscode.window.createWebviewPanel as jest.Mocked).mockReturnValue(mockPanel); + + const provider = new SnapshotProvider(); + await provider.previewSnapshot('test-file', 'some test'); + expect(vscode.window.createWebviewPanel).toHaveBeenCalledTimes(1); + expect(mockPanel.onDidDispose).toHaveBeenCalled(); + expect(mockPanel.webview.html).toMatchInlineSnapshot(`"
<test 1> result
"`); + expect(mockPanel.title).toEqual(expect.stringContaining('some test')); + + //2nd time showing the content will reuse the panel + (vscode.window.createWebviewPanel as jest.Mocked).mockClear(); + await provider.previewSnapshot('test-file', 'some other test'); + expect(vscode.window.createWebviewPanel).toHaveBeenCalledTimes(0); + expect(mockPanel.webview.html).toMatchInlineSnapshot( + `"
<test 2> "some quoted text"
"` + ); + expect(mockPanel.title).toEqual(expect.stringContaining('some other test')); + + //if user close the panel, it will be recreated on the next request + const callback = mockPanel.onDidDispose.mock.calls[0][0]; + callback(); + await provider.previewSnapshot('test-file', '3rd test'); + expect(vscode.window.createWebviewPanel).toHaveBeenCalledTimes(1); + expect(mockPanel.webview.html).toMatchInlineSnapshot( + `"
<test 3> 'single quote' & this
"` + ); + expect(mockPanel.title).toEqual(expect.stringContaining('3rd test')); + }); + it('show warning if no content is found', async () => { + mockSnapshot.getSnapshotContent.mockReturnValueOnce(undefined); + const provider = new SnapshotProvider(); + await provider.previewSnapshot('test-file', 'some test'); + expect(vscode.window.showErrorMessage).toHaveBeenCalled(); + expect(vscode.window.createWebviewPanel).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/tests/extensionManager.test.ts b/tests/extensionManager.test.ts index 1d1ead8e..e24c0c41 100644 --- a/tests/extensionManager.test.ts +++ b/tests/extensionManager.test.ts @@ -73,6 +73,7 @@ const makeJestExt = (workspace: vscode.WorkspaceFolder): any => { toggleAutoRun: jest.fn(), toggleCoverageOverlay: jest.fn(), enableLoginShell: jest.fn(), + runItemCommand: jest.fn(), workspace, }; }; @@ -700,6 +701,7 @@ describe('ExtensionManager', () => { ${'with-workspace.toggle-auto-run'} | ${'toggleAutoRun'} ${'with-workspace.toggle-coverage'} | ${'toggleCoverageOverlay'} ${'with-workspace.enable-login-shell'} | ${'enableLoginShell'} + ${'with-workspace.item-command'} | ${'runItemCommand'} `('extension-based commands "$name"', async ({ name, extFunc }) => { extensionManager.register(); const expectedName = `${extensionName}.${name}`; diff --git a/tests/test-helper.ts b/tests/test-helper.ts index 7913f200..2d008a81 100644 --- a/tests/test-helper.ts +++ b/tests/test-helper.ts @@ -38,6 +38,16 @@ export const findResultForTest = (results: TestResult[], itBlock: ItBlock): Test }; // factory method +export const makeSnapshotBlock = (marker?: string, isInline?: boolean, line?: number): any => { + const loc = line ? makePositionRange([line, 0, line, 0]) : EmptyLocationRange; + return { + marker, + isInline: isInline ?? false, + node: { name: 'node', loc }, + parents: [], + }; +}; + export const makeItBlock = ( name?: string, pos?: [number, number, number, number], diff --git a/tests/test-provider/test-helper.ts b/tests/test-provider/test-helper.ts index 307b0e7b..c0cbd710 100644 --- a/tests/test-provider/test-helper.ts +++ b/tests/test-provider/test-helper.ts @@ -37,6 +37,7 @@ export const mockExtExplorerContext = (wsName = 'ws-1', override: any = {}): any getTestList: jest.fn().mockReturnValue([]), isTestFile: jest.fn().mockReturnValue('yes'), getTestSuiteResult: jest.fn().mockReturnValue({}), + previewSnapshot: jest.fn(), }, debugTests: jest.fn(), sessionEvents: mockJestExtEvents(), diff --git a/tests/test-provider/test-item-context-manager.test.ts b/tests/test-provider/test-item-context-manager.test.ts index 1821002f..9fd7e235 100644 --- a/tests/test-provider/test-item-context-manager.test.ts +++ b/tests/test-provider/test-item-context-manager.test.ts @@ -4,6 +4,7 @@ jest.unmock('../../src/appGlobals'); import * as vscode from 'vscode'; import { extensionName } from '../../src/appGlobals'; import { TestItemContextManager } from '../../src/test-provider/test-item-context-manager'; +import { ItemCommand } from '../../src/test-provider/types'; describe('TestItemContextManager', () => { beforeEach(() => { @@ -176,34 +177,51 @@ describe('TestItemContextManager', () => { expect(vscode.commands.executeCommand).not.toHaveBeenCalledWith(extCmd, workspace); }); }); - it('view-snapshot menu commands', () => { - const manager = new TestItemContextManager(); - const disposableList = manager.registerCommands(); - expect(disposableList.length).toBeGreaterThanOrEqual(5); + describe('snapshot menu commands', () => { + it.each` + contextId | contextCommand | itemCommand + ${'jest.editor-view-snapshot'} | ${'test-item.view-snapshot'} | ${ItemCommand.viewSnapshot} + ${'jest.editor-update-snapshot'} | ${'test-item.update-snapshot'} | ${ItemCommand.updateSnapshot} + `('$contextId', ({ contextId, contextCommand, itemCommand }) => { + const manager = new TestItemContextManager(); + const disposableList = manager.registerCommands(); + expect(disposableList.length).toBeGreaterThanOrEqual(6); - const calls = (vscode.commands.registerCommand as jest.Mocked).mock.calls.filter( - (call) => call[0] === `${extensionName}.test-item.view-snapshot` - ); + const calls = (vscode.commands.registerCommand as jest.Mocked).mock.calls.filter( + (call) => call[0] === `${extensionName}.${contextCommand}` + ); - expect(calls).toHaveLength(1); + expect(calls).toHaveLength(1); - // set some itemContext then trigger the menu - const workspace: any = { name: 'ws' }; - const context: any = { - workspace, - key: 'jest.editor-view-snapshot', - itemIds: ['a'], - onClick: jest.fn(), - }; - manager.setItemContext(context); - const callBack = calls[0][1]; - callBack({ id: 'a' }); - expect(context.onClick).toHaveBeenCalled(); + // set some itemContext then trigger the menu + const workspace: any = { name: 'ws' }; + const context: any = { + workspace, + key: contextId, + itemIds: ['a'], + }; + manager.setItemContext(context); + const callBack = calls[0][1]; + const testItem = { id: 'a' }; + callBack(testItem); + const extCmd = `${extensionName}.with-workspace.item-command`; + expect(vscode.commands.executeCommand).toHaveBeenCalledWith( + extCmd, + workspace, + testItem, + itemCommand + ); - (context.onClick as jest.Mocked).mockClear(); + (vscode.commands.executeCommand as jest.Mocked).mockClear(); - callBack({ id: 'b' }); - expect(context.onClick).not.toHaveBeenCalled(); + callBack({ id: 'b' }); + expect(vscode.commands.executeCommand).not.toHaveBeenCalledWith( + extCmd, + workspace, + testItem, + itemCommand + ); + }); }); }); }); diff --git a/tests/test-provider/test-item-data.test.ts b/tests/test-provider/test-item-data.test.ts index 4b25e360..22b6cc75 100644 --- a/tests/test-provider/test-item-data.test.ts +++ b/tests/test-provider/test-item-data.test.ts @@ -8,6 +8,7 @@ jest.unmock('./test-helper'); jest.unmock('../../src/errors'); import { JestTestRun } from '../../src/test-provider/test-provider-helper'; +import { tiContextManager } from '../../src/test-provider/test-item-context-manager'; jest.mock('path', () => { let sep = '/'; @@ -43,6 +44,7 @@ import { import * as path from 'path'; import { mockController, mockExtExplorerContext } from './test-helper'; import * as errors from '../../src/errors'; +import { ItemCommand } from '../../src/test-provider/types'; const mockPathSep = (newSep: string) => { (path as jest.Mocked).setSep(newSep); @@ -148,10 +150,8 @@ describe('test-item-data', () => { const uri: any = { fsPath: 'whatever' }; const doc = new TestDocumentRoot(context, uri, folder.item); const node: any = { fullName: 'a test', attrs: {}, data: {} }; - const test = new TestData(context, uri, node, doc.item); - const snapshotProfile = { tag: { id: 'update-snapshot' } }; - const runProfile = { tag: { id: 'run' } }; - return { wsRoot, folder, doc, test, snapshotProfile, runProfile }; + const testItem = new TestData(context, uri, node, doc.item); + return { wsRoot, folder, doc, testItem }; }; beforeEach(() => { @@ -169,6 +169,7 @@ describe('test-item-data', () => { .fn() .mockImplementation((uri, p) => ({ fsPath: `${uri.fsPath}/${p}` })); vscode.Uri.file = jest.fn().mockImplementation((f) => ({ fsPath: f })); + (tiContextManager.setItemContext as jest.Mocked).mockClear(); }); describe('discover children', () => { describe('WorkspaceRoot', () => { @@ -390,7 +391,7 @@ describe('test-item-data', () => { }); }); describe('when testSuiteChanged.result-matched event fired', () => { - it('test data range will be updated accordingly', () => { + it('test data range and snapshot context will be updated accordingly', () => { // assertion should be discovered prior context.ext.testResolveProvider.getTestList.mockReturnValueOnce(['/ws-1/a.test.ts']); @@ -436,6 +437,8 @@ describe('test-item-data', () => { start: { line: 2, column: 2 }, end: { line: 10, column: 4 }, }; + // add snapshot marker + testNode.attrs.snapshot = 'inline'; // triggers testSuiteChanged event listener context.ext.testResolveProvider.events.testSuiteChanged.event.mock.calls[0][0]({ @@ -464,10 +467,25 @@ describe('test-item-data', () => { testNode.attrs.range.end.column, ], }); + + // snapshot menu context is populated + expect(tiContextManager.setItemContext).toHaveBeenCalledTimes(2); + expect(tiContextManager.setItemContext).toHaveBeenCalledWith( + expect.objectContaining({ + key: 'jest.editor-update-snapshot', + itemIds: [tItem.id], + }) + ); + expect(tiContextManager.setItemContext).toHaveBeenCalledWith( + expect.objectContaining({ + key: 'jest.editor-view-snapshot', + itemIds: [], + }) + ); }); }); describe('when testSuiteChanged.test-parsed event filed', () => { - it('test items will be added based on parsed test files (test blocks)', () => { + it('test items will be added and snapshot context updated accordingly', () => { // assertion should be discovered prior context.ext.testResolveProvider.getTestList.mockReturnValueOnce(['/ws-1/a.test.ts']); @@ -475,6 +493,10 @@ describe('test-item-data', () => { const t2 = helper.makeItBlock('test-2', [6, 1, 7, 1]); const sourceRoot = helper.makeRoot([t2, t1]); const sourceContainer = buildSourceContainer(sourceRoot); + const node1 = sourceContainer.childData.find((child) => child.fullName === 'test-1'); + const node2 = sourceContainer.childData.find((child) => child.fullName === 'test-2'); + node1.attrs = { ...node1.attrs, snapshot: 'external' }; + node2.attrs = { ...node2.attrs, snapshot: 'external', nonLiteralName: true }; const wsRoot = new WorkspaceRoot(context); wsRoot.discoverTest(jestRun); @@ -492,13 +514,28 @@ describe('test-item-data', () => { sourceContainer, }); expect(docItem.children.size).toEqual(2); - let dItem = getChildItem(docItem, 'test-1'); - expect(dItem.range).toEqual({ args: [0, 0, 4, 0] }); - dItem = getChildItem(docItem, 'test-2'); - expect(dItem.range).toEqual({ args: [5, 0, 6, 0] }); + const dItem1 = getChildItem(docItem, 'test-1'); + expect(dItem1.range).toEqual({ args: [0, 0, 4, 0] }); + const dItem2 = getChildItem(docItem, 'test-2'); + expect(dItem2.range).toEqual({ args: [5, 0, 6, 0] }); expect(context.ext.testResolveProvider.getTestSuiteResult).not.toHaveBeenCalled(); expect(controllerMock.createTestRun).not.toHaveBeenCalled(); + + // snapshot menu context is populated for "test-1" only + expect(tiContextManager.setItemContext).toHaveBeenCalledTimes(2); + expect(tiContextManager.setItemContext).toHaveBeenCalledWith( + expect.objectContaining({ + key: 'jest.editor-view-snapshot', + itemIds: [dItem1.id], + }) + ); + expect(tiContextManager.setItemContext).toHaveBeenCalledWith( + expect.objectContaining({ + key: 'jest.editor-update-snapshot', + itemIds: [dItem1.id], + }) + ); }); }); }); @@ -651,13 +688,13 @@ describe('test-item-data', () => { expect(request.run.item).toBe(folderData.item); }); describe('can update snapshot based on runProfile', () => { - let wsRoot, folder, doc, test, snapshotProfile, runProfile; + let wsRoot, folder, doc, testItem; beforeEach(() => { - ({ wsRoot, folder, doc, test, snapshotProfile, runProfile } = createAllTestItems()); + ({ wsRoot, folder, doc, testItem } = createAllTestItems()); }); it('with snapshot profile', () => { - [wsRoot, folder, doc, test].forEach((testItem) => { - testItem.scheduleTest(jestRun, snapshotProfile); + [wsRoot, folder, doc, testItem].forEach((testItem) => { + testItem.scheduleTest(jestRun, ItemCommand.updateSnapshot); expect(context.ext.session.scheduleProcess).toHaveBeenCalledWith( expect.objectContaining({ updateSnapshot: true, @@ -665,16 +702,6 @@ describe('test-item-data', () => { ); }); }); - it('no update snapshot with run profile', () => { - [wsRoot, folder, doc, test].forEach((testItem) => { - testItem.scheduleTest(jestRun, runProfile); - expect(context.ext.session.scheduleProcess).toHaveBeenCalledWith( - expect.not.objectContaining({ - updateSnapshot: true, - }) - ); - }); - }); }); }); @@ -1039,18 +1066,17 @@ describe('test-item-data', () => { }); }); describe('tags', () => { - let wsRoot, folder, doc, test; + let wsRoot, folder, doc, testItem; beforeEach(() => { - ({ wsRoot, folder, doc, test } = createAllTestItems()); + ({ wsRoot, folder, doc, testItem } = createAllTestItems()); }); - it('all TestItem supports run and update-snapshot tag', () => { - [wsRoot, folder, doc, test].forEach((itemData) => { + it('all TestItem supports run tag', () => { + [wsRoot, folder, doc, testItem].forEach((itemData) => { expect(itemData.item.tags.find((t) => t.id === 'run')).toBeTruthy(); - expect(itemData.item.tags.find((t) => t.id === 'update-snapshot')).toBeTruthy(); }); }); it('only TestData and TestDocument supports debug tags', () => { - [doc, test].forEach((itemData) => + [doc, testItem].forEach((itemData) => expect(itemData.item.tags.find((t) => t.id === 'debug')).toBeTruthy() ); [wsRoot, folder].forEach((itemData) => @@ -1496,47 +1522,61 @@ describe('test-item-data', () => { }); }); }); - // describe('snapshow preview', () => { - // let wsRoot, folder, doc, test; - // beforeEach(() => { - // ({ wsRoot, folder, doc, test } = createAllTestItems()); - // }); - // it('update menu context for eligibile test items', () => { - // const a1 = helper.makeAssertion('test-a', 'KnownFail', ['desc-1'], [1, 0]); - // const assertionContainer = buildAssertionContainer([a1]); - // context.ext.testResolveProvider.getTestSuiteResult.mockReturnValue({ - // status: 'KnownFail', - // assertionContainer, - // }); - - // // 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: { type: 'watch-tests' } }, - // files: ['/ws-1/a.test.ts'], - // }); - - // // change the node - // const descNode = assertionContainer.childContainers[0]; - // descNode.attrs.range = { - // start: { line: 1, column: 2 }, - // end: { line: 13, column: 4 }, - // }; - // const testNode = descNode.childData[0]; - // testNode.attrs.range = { - // start: { line: 2, column: 2 }, - // end: { line: 10, column: 4 }, - // }; - - // // triggers testSuiteChanged event - // context.ext.testResolveProvider.events.testSuiteChanged.event.mock.calls[0][0]({ - // type: 'result-matched', - // file: '/ws-1/a.test.ts', - // }); - - // // should update the context with snapshot items - // expect(true).toEqual(true); - // }); - // it.todo('trigger the test item to perform previewSnapshot upon menu click'); - // }); + describe('runItemCommand', () => { + let wsRoot, folder, doc, testItem; + beforeEach(() => { + ({ wsRoot, folder, doc, testItem } = createAllTestItems()); + }); + it('can update-snapshot for every TestItemData', () => { + const createTestRunSpy = jest.spyOn(context, 'createTestRun'); + [wsRoot, folder, doc, testItem].forEach((itemData) => { + createTestRunSpy.mockClear(); + context.ext.session.scheduleProcess.mockClear(); + + itemData.runItemCommand(ItemCommand.updateSnapshot); + expect(createTestRunSpy).toHaveBeenCalledTimes(1); + expect(context.ext.session.scheduleProcess).toHaveBeenCalledWith( + expect.objectContaining({ updateSnapshot: true }) + ); + }); + }); + describe('view-snapshot', () => { + beforeEach(() => { + context.ext.testResolveProvider.previewSnapshot.mockReturnValue(Promise.resolve()); + }); + it.each` + case | index | canView + ${'wsRoot'} | ${0} | ${false} + ${'folder'} | ${1} | ${false} + ${'doc'} | ${2} | ${false} + ${'testItem'} | ${3} | ${true} + `('$case supports view-snapshot? $canView', async ({ index, canView }) => { + testItem.node.attrs = { ...testItem.node.attrs, snapshot: 'external' }; + const data = [wsRoot, folder, doc, testItem][index]; + await data.runItemCommand(ItemCommand.viewSnapshot); + if (canView) { + expect(context.ext.testResolveProvider.previewSnapshot).toHaveBeenCalled(); + } else { + expect(context.ext.testResolveProvider.previewSnapshot).not.toHaveBeenCalled(); + } + }); + it.each` + snapshotAttr | canView + ${'inline'} | ${false} + ${'external'} | ${true} + ${undefined} | ${false} + `( + 'testItem: snapshot = $snapshotAttr, canView? $canView', + async ({ snapshotAttr, canView }) => { + testItem.node.attrs = { ...testItem.node.attrs, snapshot: snapshotAttr }; + await testItem.runItemCommand(ItemCommand.viewSnapshot); + if (canView) { + expect(context.ext.testResolveProvider.previewSnapshot).toHaveBeenCalled(); + } else { + expect(context.ext.testResolveProvider.previewSnapshot).not.toHaveBeenCalled(); + } + } + ); + }); + }); }); diff --git a/tests/test-provider/test-provider-helper.test.ts b/tests/test-provider/test-provider-helper.test.ts index d20541d5..24f10065 100644 --- a/tests/test-provider/test-provider-helper.test.ts +++ b/tests/test-provider/test-provider-helper.test.ts @@ -63,3 +63,12 @@ describe('JestTestRun', () => { }); }); }); +describe('JestTestProviderContext', () => { + it('when try to getTag not in any profiles, throw error', () => { + const whatever: any = {}; + const profile: any = { tag: { id: 'run' } }; + const context = new JestTestProviderContext(whatever, whatever, [profile]); + expect(context.getTag('run')).toEqual(profile.tag); + expect(() => context.getTag('debug')).toThrow(); + }); +}); diff --git a/tests/test-provider/test-provider.test.ts b/tests/test-provider/test-provider.test.ts index c5fdb07e..4f8f2bd1 100644 --- a/tests/test-provider/test-provider.test.ts +++ b/tests/test-provider/test-provider.test.ts @@ -10,6 +10,7 @@ import { JestTestProviderContext } from '../../src/test-provider/test-provider-h import { extensionId } from '../../src/appGlobals'; import { mockController, mockExtExplorerContext } from './test-helper'; import { tiContextManager } from '../../src/test-provider/test-item-context-manager'; +import { ItemCommand } from '../../src/test-provider/types'; const throwError = () => { throw new Error('debug error'); @@ -20,6 +21,7 @@ describe('JestTestProvider', () => { const data: any = { discoverTest: jest.fn(), scheduleTest: jest.fn(), + runItemCommand: jest.fn(), dispose: jest.fn(), }; if (debuggable) { @@ -78,10 +80,9 @@ describe('JestTestProvider', () => { `${extensionId}:TestProvider:ws-1`, expect.stringContaining('ws-1') ); - expect(controllerMock.createRunProfile).toHaveBeenCalledTimes(3); + expect(controllerMock.createRunProfile).toHaveBeenCalledTimes(2); [ [vscode.TestRunProfileKind.Run, 'run', true], - [vscode.TestRunProfileKind.Run, 'update-snapshot', false], [vscode.TestRunProfileKind.Debug, 'debug', true], ].forEach(([kind, id, isDefault]) => { expect(controllerMock.createRunProfile).toHaveBeenCalledWith( @@ -97,7 +98,7 @@ describe('JestTestProvider', () => { }); }); - describe('can discover tests', () => { + describe('can discover tests', () => { it('should only discover items with canResolveChildren = true', () => { new JestTestProvider(extExplorerContextMock); const data = setupTestItemData('whatever', true, workspaceRootMock.context); @@ -592,4 +593,9 @@ describe('JestTestProvider', () => { ); }); }); + it('supports runItemCommand', () => { + const provider = new JestTestProvider(extExplorerContextMock); + provider.runItemCommand(workspaceRootMock.item, ItemCommand.updateSnapshot); + expect(workspaceRootMock.runItemCommand).toHaveBeenCalled(); + }); }); From 93dcfe08580865f69e15deff6f62c1a75fe52f97 Mon Sep 17 00:00:00 2001 From: connectdotz Date: Sat, 19 Nov 2022 22:12:21 -0500 Subject: [PATCH 13/16] handle multiple snapshots in a given test --- src/TestResults/snapshot-provider.ts | 37 ++++++++-- tests/TestResults/snapshot-provider.test.ts | 78 ++++++++++++++------- 2 files changed, 84 insertions(+), 31 deletions(-) diff --git a/src/TestResults/snapshot-provider.ts b/src/TestResults/snapshot-provider.ts index a165f60a..9e7e3239 100644 --- a/src/TestResults/snapshot-provider.ts +++ b/src/TestResults/snapshot-provider.ts @@ -1,5 +1,6 @@ import * as vscode from 'vscode'; import { Snapshot, SnapshotBlock } from 'jest-editor-support'; +import { escapeRegExp } from '../helpers'; export type SnapshotStatus = 'exists' | 'missing' | 'inline'; @@ -34,9 +35,7 @@ export class SnapshotProvider { return { testPath, blocks: [] }; } } - public async getContent(testPath: string, testFullName: string): Promise { - return this.snapshotSupport.getSnapshotContent(testPath, testFullName); - } + private escapeContent = (content: string) => { if (content) { const escaped = content @@ -49,10 +48,36 @@ export class SnapshotProvider { } }; public async previewSnapshot(testPath: string, testFullName: string): Promise { - const content = await this.getContent(testPath, testFullName); - if (!content) { + const content = await this.snapshotSupport.getSnapshotContent( + testPath, + new RegExp(`^${escapeRegExp(testFullName)} [0-9]+$`) + ); + const noSnapshotFound = (): void => { vscode.window.showErrorMessage('no snapshot is found, please run test to generate first'); return; + }; + if (!content) { + return noSnapshotFound(); + } + let contentString: string | undefined; + if (typeof content === 'string') { + contentString = this.escapeContent(content); + } else { + const entries = Object.entries(content); + switch (entries.length) { + case 0: + return noSnapshotFound(); + case 1: + contentString = this.escapeContent(entries[0][1]); + break; + default: { + const strings = entries.map( + ([key, value]) => `

${key}

${this.escapeContent(value)}` + ); + contentString = strings.join('
'); + break; + } + } } if (this.panel) { @@ -70,7 +95,7 @@ export class SnapshotProvider { }); } - this.panel.webview.html = (content && this.escapeContent(content)) || ''; + this.panel.webview.html = contentString ?? ''; this.panel.title = testFullName; } } diff --git a/tests/TestResults/snapshot-provider.test.ts b/tests/TestResults/snapshot-provider.test.ts index 2f533f5a..0546d678 100644 --- a/tests/TestResults/snapshot-provider.test.ts +++ b/tests/TestResults/snapshot-provider.test.ts @@ -1,4 +1,5 @@ jest.unmock('../../src/TestResults/snapshot-provider'); +jest.unmock('../../src/helpers'); import * as vscode from 'vscode'; import { SnapshotProvider } from '../../src/TestResults/snapshot-provider'; @@ -46,38 +47,43 @@ describe('SnapshotProvider', () => { }); }); }); - describe('getSnapshotContent', () => { - it.each` - case | impl | expected - ${1} | ${() => Promise.resolve('something')} | ${'something'} - ${2} | ${() => Promise.resolve()} | ${undefined} - ${3} | ${() => Promise.reject('error')} | ${'throws'} - `('$case: forward call to Snapshot', async ({ impl, expected }) => { - mockSnapshot.getSnapshotContent.mockImplementation(impl); - const provider = new SnapshotProvider(); - if (expected === 'throws') { - await expect(provider.getContent('whatever', 'whatever')).rejects.toEqual('error'); - } else { - await expect(provider.getContent('whatever', 'whatever')).resolves.toEqual(expected); - } - }); - }); + describe('previewSnapshot', () => { - it('display content in a WebviewPanel', async () => { - const content1 = ' result'; - const content2 = ' "some quoted text"'; - const content3 = " 'single quote' & this"; - mockSnapshot.getSnapshotContent - .mockReturnValueOnce(Promise.resolve(content1)) - .mockReturnValueOnce(Promise.resolve(content2)) - .mockReturnValueOnce(Promise.resolve(content3)); - const mockPanel = { + let mockPanel; + beforeEach(() => { + mockPanel = { reveal: jest.fn(), onDidDispose: jest.fn(), webview: { html: undefined }, title: undefined, }; (vscode.window.createWebviewPanel as jest.Mocked).mockReturnValue(mockPanel); + }); + describe('create a regexp from test names', () => { + it.each` + testName | regString + ${'simple name'} | ${'simple name'} + ${'with $name'} | ${'with \\$name'} + ${'a string.with.dots*'} | ${'a string\\.with\\.dots\\*'} + ${'

title

'} | ${'

title<\\/p>'} + `('$testName', async ({ testName, regString }) => { + mockSnapshot.getSnapshotContent.mockReturnValue(Promise.resolve(undefined)); + const provider = new SnapshotProvider(); + await provider.previewSnapshot('whatever', testName); + expect(mockSnapshot.getSnapshotContent).toHaveBeenCalledWith( + 'whatever', + new RegExp(`^${regString} [0-9]+$`) + ); + }); + }); + it('display content in a WebviewPanel', async () => { + const content1 = { 'some test': ' result' }; + const content2 = ' "some quoted text"'; + const content3 = { '3rd test': " 'single quote' & this" }; + mockSnapshot.getSnapshotContent + .mockReturnValueOnce(Promise.resolve(content1)) + .mockReturnValueOnce(Promise.resolve(content2)) + .mockReturnValueOnce(Promise.resolve(content3)); const provider = new SnapshotProvider(); await provider.previewSnapshot('test-file', 'some test'); @@ -111,6 +117,28 @@ describe('SnapshotProvider', () => { await provider.previewSnapshot('test-file', 'some test'); expect(vscode.window.showErrorMessage).toHaveBeenCalled(); expect(vscode.window.createWebviewPanel).not.toHaveBeenCalled(); + + (vscode.window.showErrorMessage as jest.Mocked).mockClear(); + + mockSnapshot.getSnapshotContent.mockReturnValueOnce({}); + await provider.previewSnapshot('test-file', 'some test'); + expect(vscode.window.showErrorMessage).toHaveBeenCalled(); + expect(vscode.window.createWebviewPanel).not.toHaveBeenCalled(); + }); + it('can show multiple snapshots within a test', async () => { + const content = { + 'test 1': 'test 1 content', + 'test 2': 'test 2 content', + }; + mockSnapshot.getSnapshotContent.mockReturnValueOnce(Promise.resolve(content)); + const provider = new SnapshotProvider(); + await provider.previewSnapshot('test-file', 'some test'); + expect(vscode.window.createWebviewPanel).toHaveBeenCalledTimes(1); + expect(mockPanel.onDidDispose).toHaveBeenCalled(); + expect(mockPanel.webview.html).toMatchInlineSnapshot( + `"

test 1

test 1 content

test 2

test 2 content
"` + ); + expect(mockPanel.title).toEqual(expect.stringContaining('some test')); }); }); }); From 29f0884ee5e8cbd2eada1aa88b50640db8de2051 Mon Sep 17 00:00:00 2001 From: connectdotz Date: Sat, 19 Nov 2022 22:25:43 -0500 Subject: [PATCH 14/16] rename testResolveProvider to testResultProvider --- src/JestExt/core.ts | 2 +- src/test-provider/test-item-data.ts | 12 +- src/test-provider/types.ts | 2 +- tests/test-provider/test-helper.ts | 2 +- tests/test-provider/test-item-data.test.ts | 124 ++++++++++----------- 5 files changed, 71 insertions(+), 71 deletions(-) diff --git a/src/JestExt/core.ts b/src/JestExt/core.ts index 68b8ee18..d6d5dd7b 100644 --- a/src/JestExt/core.ts +++ b/src/JestExt/core.ts @@ -124,7 +124,7 @@ export class JestExt { ...this.extContext, sessionEvents: this.events, session: this.processSession, - testResolveProvider: this.testResultProvider, + testResultProvider: this.testResultProvider, debugTests: this.debugTests, }; } diff --git a/src/test-provider/test-item-data.ts b/src/test-provider/test-item-data.ts index 602c0f46..8f7953bb 100644 --- a/src/test-provider/test-item-data.ts +++ b/src/test-provider/test-item-data.ts @@ -139,7 +139,7 @@ export class WorkspaceRoot extends TestItemDataBase { return { type: 'all-tests', updateSnapshot, transform }; } discoverTest(run: JestTestRun): void { - const testList = this.context.ext.testResolveProvider.getTestList(); + const testList = this.context.ext.testResultProvider.getTestList(); // only trigger update when testList is not empty because it's possible test-list is not available yet, // in such case we should just wait for the testListUpdated event to trigger the update if (testList.length > 0) { @@ -153,8 +153,8 @@ export class WorkspaceRoot extends TestItemDataBase { // test result event handling private registerEvents = (): void => { this.listeners = [ - this.context.ext.testResolveProvider.events.testListUpdated.event(this.onTestListUpdated), - this.context.ext.testResolveProvider.events.testSuiteChanged.event(this.onTestSuiteChanged), + this.context.ext.testResultProvider.events.testListUpdated.event(this.onTestListUpdated), + this.context.ext.testResultProvider.events.testSuiteChanged.event(this.onTestSuiteChanged), this.context.ext.sessionEvents.onRunEvent.event(this.onRunEvent), ]; }; @@ -638,7 +638,7 @@ export class TestDocumentRoot extends TestResultData { private createChildItems = (parsedRoot?: ContainerNode): void => { const container = parsedRoot ?? - this.context.ext.testResolveProvider.getTestSuiteResult(this.item.id)?.assertionContainer; + this.context.ext.testResultProvider.getTestSuiteResult(this.item.id)?.assertionContainer; if (!container) { this.item.children.replace([]); } else { @@ -649,7 +649,7 @@ export class TestDocumentRoot extends TestResultData { }; public updateResultState(run: JestTestRun): void { - const suiteResult = this.context.ext.testResolveProvider.getTestSuiteResult(this.item.id); + const suiteResult = this.context.ext.testResultProvider.getTestSuiteResult(this.item.id); // 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. @@ -781,7 +781,7 @@ export class TestData extends TestResultData implements Debuggable { } public viewSnapshot(): Promise { if (this.node.attrs.snapshot === 'external') { - return this.context.ext.testResolveProvider.previewSnapshot( + return this.context.ext.testResultProvider.previewSnapshot( this.uri.fsPath, this.node.fullName ); diff --git a/src/test-provider/types.ts b/src/test-provider/types.ts index eeefd541..2dc16db1 100644 --- a/src/test-provider/types.ts +++ b/src/test-provider/types.ts @@ -8,7 +8,7 @@ export type TestItemDataType = WorkspaceRoot | FolderData | TestDocumentRoot | T /** JestExt context exposed to the test explorer */ export interface JestExtExplorerContext extends JestExtSessionContext { - readonly testResolveProvider: TestResultProvider; + readonly testResultProvider: TestResultProvider; readonly sessionEvents: JestSessionEvents; debugTests: DebugFunction; } diff --git a/tests/test-provider/test-helper.ts b/tests/test-provider/test-helper.ts index c0cbd710..74720060 100644 --- a/tests/test-provider/test-helper.ts +++ b/tests/test-provider/test-helper.ts @@ -29,7 +29,7 @@ export const mockExtExplorerContext = (wsName = 'ws-1', override: any = {}): any loggingFactory: { create: jest.fn().mockReturnValue(jest.fn()) }, session: { scheduleProcess: jest.fn() }, workspace: { name: wsName, uri: { fsPath: `/${wsName}` } }, - testResolveProvider: { + testResultProvider: { events: { testListUpdated: { event: jest.fn().mockReturnValue({ dispose: jest.fn() }) }, testSuiteChanged: { event: jest.fn().mockReturnValue({ dispose: jest.fn() }) }, diff --git a/tests/test-provider/test-item-data.test.ts b/tests/test-provider/test-item-data.test.ts index 22b6cc75..dc4f2812 100644 --- a/tests/test-provider/test-item-data.test.ts +++ b/tests/test-provider/test-item-data.test.ts @@ -97,12 +97,12 @@ describe('test-item-data', () => { message: 'test file failed', assertionContainer, }; - context.ext.testResolveProvider.getTestSuiteResult.mockReturnValue(testSuiteResult); + context.ext.testResultProvider.getTestSuiteResult.mockReturnValue(testSuiteResult); }; const setupTestEnv = () => { const file = '/ws-1/tests/a.test.ts'; - context.ext.testResolveProvider.getTestList.mockReturnValueOnce([file]); + context.ext.testResultProvider.getTestList.mockReturnValueOnce([file]); const wsRoot = new WorkspaceRoot(context); const onRunEvent = context.ext.sessionEvents.onRunEvent.event.mock.calls[0][0]; @@ -110,7 +110,7 @@ describe('test-item-data', () => { prepareTestResult(); // triggers testSuiteChanged event listener - context.ext.testResolveProvider.events.testSuiteChanged.event.mock.calls[0][0]({ + context.ext.testResultProvider.events.testSuiteChanged.event.mock.calls[0][0]({ type: 'assertions-updated', process: { id: 'whatever', request: { type: 'watch-tests' } }, files: [file], @@ -179,7 +179,7 @@ describe('test-item-data', () => { '/ws-1/src/b.test.ts', '/ws-1/src/app/app.test.ts', ]; - context.ext.testResolveProvider.getTestList.mockReturnValue(testFiles); + context.ext.testResultProvider.getTestList.mockReturnValue(testFiles); const wsRoot = new WorkspaceRoot(context); wsRoot.discoverTest(jestRun); @@ -212,7 +212,7 @@ describe('test-item-data', () => { }); describe('when no testFiles yet', () => { it('if no testFiles yet, will still turn off canResolveChildren and close the run', () => { - context.ext.testResolveProvider.getTestList.mockReturnValue([]); + context.ext.testResultProvider.getTestList.mockReturnValue([]); const wsRoot = new WorkspaceRoot(context); wsRoot.discoverTest(jestRun); expect(wsRoot.item.children.size).toEqual(0); @@ -221,7 +221,7 @@ describe('test-item-data', () => { }); it('will not wipe out existing test items', () => { // first time discover 1 file - context.ext.testResolveProvider.getTestList.mockReturnValue(['/ws-1/a.test.ts']); + context.ext.testResultProvider.getTestList.mockReturnValue(['/ws-1/a.test.ts']); const wsRoot = new WorkspaceRoot(context); wsRoot.discoverTest(jestRun); expect(jestRun.isClosed()).toBeTruthy(); @@ -230,7 +230,7 @@ describe('test-item-data', () => { expect(runEndSpy).toHaveBeenCalledTimes(1); // 2nd time if no test-file: testItems will not change - context.ext.testResolveProvider.getTestList.mockReturnValue([]); + context.ext.testResultProvider.getTestList.mockReturnValue([]); [jestRun, runEndSpy] = createTestRun(); wsRoot.discoverTest(jestRun); expect(jestRun.isClosed()).toBeTruthy(); @@ -243,8 +243,8 @@ describe('test-item-data', () => { const a1 = helper.makeAssertion('test-1', 'KnownSuccess', [], [1, 0]); const assertionContainer = buildAssertionContainer([a1]); const testFiles = ['/ws-1/a.test.ts']; - context.ext.testResolveProvider.getTestList.mockReturnValue(testFiles); - context.ext.testResolveProvider.getTestSuiteResult.mockReturnValue({ + context.ext.testResultProvider.getTestList.mockReturnValue(testFiles); + context.ext.testResultProvider.getTestSuiteResult.mockReturnValue({ status: 'KnownSuccess', assertionContainer, }); @@ -252,14 +252,14 @@ describe('test-item-data', () => { wsRoot.discoverTest(jestRun); const docItem = wsRoot.item.children.get(testFiles[0]); expect(docItem.children.size).toEqual(0); - expect(context.ext.testResolveProvider.getTestSuiteResult).toHaveBeenCalled(); + expect(context.ext.testResultProvider.getTestSuiteResult).toHaveBeenCalled(); }); it('will remove folder item if no test file exist any more', () => { const a1 = helper.makeAssertion('test-1', 'KnownSuccess', [], [1, 0]); const assertionContainer = buildAssertionContainer([a1]); const testFiles = ['/ws-1/tests1/a.test.ts', '/ws-1/tests2/b.test.ts']; - context.ext.testResolveProvider.getTestList.mockReturnValue(testFiles); - context.ext.testResolveProvider.getTestSuiteResult.mockReturnValue({ + context.ext.testResultProvider.getTestList.mockReturnValue(testFiles); + context.ext.testResultProvider.getTestSuiteResult.mockReturnValue({ status: 'KnownSuccess', assertionContainer, }); @@ -302,15 +302,15 @@ describe('test-item-data', () => { }); it('register for test result events', () => { new WorkspaceRoot(context); - expect(context.ext.testResolveProvider.events.testListUpdated.event).toHaveBeenCalled(); - expect(context.ext.testResolveProvider.events.testSuiteChanged.event).toHaveBeenCalled(); + expect(context.ext.testResultProvider.events.testListUpdated.event).toHaveBeenCalled(); + expect(context.ext.testResultProvider.events.testSuiteChanged.event).toHaveBeenCalled(); }); it('unregister events upon dispose', () => { const wsRoot = new WorkspaceRoot(context); const listeners = [ - context.ext.testResolveProvider.events.testListUpdated.event.mock.results[0].value, - context.ext.testResolveProvider.events.testSuiteChanged.event.mock.results[0].value, + context.ext.testResultProvider.events.testListUpdated.event.mock.results[0].value, + context.ext.testResultProvider.events.testSuiteChanged.event.mock.results[0].value, context.ext.sessionEvents.onRunEvent.event.mock.results[0].value, ]; wsRoot.dispose(); @@ -319,12 +319,12 @@ describe('test-item-data', () => { describe('when testFile list is changed', () => { it('testListUpdated event will be fired', () => { const wsRoot = new WorkspaceRoot(context); - context.ext.testResolveProvider.getTestList.mockReturnValueOnce([]); + context.ext.testResultProvider.getTestList.mockReturnValueOnce([]); wsRoot.discoverTest(jestRun); expect(wsRoot.item.children.size).toBe(0); // invoke testListUpdated event listener - context.ext.testResolveProvider.events.testListUpdated.event.mock.calls[0][0]([ + context.ext.testResultProvider.events.testListUpdated.event.mock.calls[0][0]([ '/ws-1/a.test.ts', ]); // should have created a new run @@ -339,7 +339,7 @@ describe('test-item-data', () => { }); describe('when testSuiteChanged.assertions-updated event filed', () => { it('all item data will be updated accordingly', () => { - context.ext.testResolveProvider.getTestList.mockReturnValueOnce([]); + context.ext.testResultProvider.getTestList.mockReturnValueOnce([]); context.ext.settings = { testExplorer: { enabled: true, showInlineError: true }, autoRun: {}, @@ -360,10 +360,10 @@ describe('test-item-data', () => { message: 'test file failed', assertionContainer, }; - context.ext.testResolveProvider.getTestSuiteResult.mockReturnValue(testSuiteResult); + context.ext.testResultProvider.getTestSuiteResult.mockReturnValue(testSuiteResult); // triggers testSuiteChanged event listener - context.ext.testResolveProvider.events.testSuiteChanged.event.mock.calls[0][0]({ + context.ext.testResultProvider.events.testSuiteChanged.event.mock.calls[0][0]({ type: 'assertions-updated', process: { id: 'whatever', request: { type: 'watch-tests' } }, files: ['/ws-1/a.test.ts'], @@ -393,25 +393,25 @@ describe('test-item-data', () => { describe('when testSuiteChanged.result-matched event fired', () => { it('test data range and snapshot context will be updated accordingly', () => { // assertion should be discovered prior - context.ext.testResolveProvider.getTestList.mockReturnValueOnce(['/ws-1/a.test.ts']); + context.ext.testResultProvider.getTestList.mockReturnValueOnce(['/ws-1/a.test.ts']); const a1 = helper.makeAssertion('test-a', 'KnownFail', ['desc-1'], [1, 0]); const assertionContainer = buildAssertionContainer([a1]); - context.ext.testResolveProvider.getTestSuiteResult.mockReturnValue({ + context.ext.testResultProvider.getTestSuiteResult.mockReturnValue({ status: 'KnownFail', assertionContainer, }); const wsRoot = new WorkspaceRoot(context); wsRoot.discoverTest(jestRun); - expect(context.ext.testResolveProvider.getTestSuiteResult).toHaveBeenCalledTimes(1); + expect(context.ext.testResultProvider.getTestSuiteResult).toHaveBeenCalledTimes(1); expect(wsRoot.item.children.size).toBe(1); const docItem = getChildItem(wsRoot.item, 'a.test.ts'); expect(docItem.children.size).toEqual(0); // 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]({ + context.ext.testResultProvider.events.testSuiteChanged.event.mock.calls[0][0]({ type: 'assertions-updated', process: { id: 'whatever', request: { type: 'watch-tests' } }, files: ['/ws-1/a.test.ts'], @@ -422,9 +422,9 @@ describe('test-item-data', () => { const tItem = getChildItem(dItem, 'test-a'); expect(tItem.range).toEqual({ args: [1, 0, 1, 0] }); - expect(context.ext.testResolveProvider.getTestSuiteResult).toHaveBeenCalled(); + expect(context.ext.testResultProvider.getTestSuiteResult).toHaveBeenCalled(); controllerMock.createTestRun.mockClear(); - context.ext.testResolveProvider.getTestSuiteResult.mockClear(); + context.ext.testResultProvider.getTestSuiteResult.mockClear(); // after match, the assertion nodes would have updated range const descNode = assertionContainer.childContainers[0]; @@ -441,14 +441,14 @@ describe('test-item-data', () => { testNode.attrs.snapshot = 'inline'; // triggers testSuiteChanged event listener - context.ext.testResolveProvider.events.testSuiteChanged.event.mock.calls[0][0]({ + context.ext.testResultProvider.events.testSuiteChanged.event.mock.calls[0][0]({ type: 'result-matched', file: '/ws-1/a.test.ts', }); // no run should be created as we are not changing any test item tree expect(controllerMock.createTestRun).not.toHaveBeenCalled(); - expect(context.ext.testResolveProvider.getTestSuiteResult).not.toHaveBeenCalled(); + expect(context.ext.testResultProvider.getTestSuiteResult).not.toHaveBeenCalled(); // expect the item's range has picked up the updated nodes expect(dItem.range).toEqual({ @@ -487,7 +487,7 @@ describe('test-item-data', () => { describe('when testSuiteChanged.test-parsed event filed', () => { it('test items will be added and snapshot context updated accordingly', () => { // assertion should be discovered prior - context.ext.testResolveProvider.getTestList.mockReturnValueOnce(['/ws-1/a.test.ts']); + context.ext.testResultProvider.getTestList.mockReturnValueOnce(['/ws-1/a.test.ts']); const t1 = helper.makeItBlock('test-1', [1, 1, 5, 1]); const t2 = helper.makeItBlock('test-2', [6, 1, 7, 1]); @@ -500,15 +500,15 @@ describe('test-item-data', () => { const wsRoot = new WorkspaceRoot(context); wsRoot.discoverTest(jestRun); - expect(context.ext.testResolveProvider.getTestSuiteResult).toHaveBeenCalledTimes(1); + expect(context.ext.testResultProvider.getTestSuiteResult).toHaveBeenCalledTimes(1); expect(wsRoot.item.children.size).toBe(1); const docItem = getChildItem(wsRoot.item, 'a.test.ts'); expect(docItem.children.size).toEqual(0); controllerMock.createTestRun.mockClear(); - context.ext.testResolveProvider.getTestSuiteResult.mockClear(); + context.ext.testResultProvider.getTestSuiteResult.mockClear(); - context.ext.testResolveProvider.events.testSuiteChanged.event.mock.calls[0][0]({ + context.ext.testResultProvider.events.testSuiteChanged.event.mock.calls[0][0]({ type: 'test-parsed', file: '/ws-1/a.test.ts', sourceContainer, @@ -519,7 +519,7 @@ describe('test-item-data', () => { const dItem2 = getChildItem(docItem, 'test-2'); expect(dItem2.range).toEqual({ args: [5, 0, 6, 0] }); - expect(context.ext.testResolveProvider.getTestSuiteResult).not.toHaveBeenCalled(); + expect(context.ext.testResultProvider.getTestSuiteResult).not.toHaveBeenCalled(); expect(controllerMock.createTestRun).not.toHaveBeenCalled(); // snapshot menu context is populated for "test-1" only @@ -547,7 +547,7 @@ describe('test-item-data', () => { const t2 = helper.makeItBlock('test-2', [6, 1, 7, 1]); const sourceRoot = helper.makeRoot([t2, t1]); const sourceContainer = buildSourceContainer(sourceRoot); - context.ext.testResolveProvider.events.testSuiteChanged.event.mock.calls[0][0]({ + context.ext.testResultProvider.events.testSuiteChanged.event.mock.calls[0][0]({ type: 'test-parsed', file: '/ws-1/a.test.ts', sourceContainer, @@ -557,7 +557,7 @@ describe('test-item-data', () => { expect(docItem.children.size).toEqual(2); // now call discovery with additional files - context.ext.testResolveProvider.getTestList.mockReturnValueOnce([ + context.ext.testResultProvider.getTestList.mockReturnValueOnce([ '/ws-1/a.test.ts', '/ws-1/b.test.ts', ]); @@ -577,7 +577,7 @@ describe('test-item-data', () => { const a1 = helper.makeAssertion('test-1', 'KnownSuccess', [], [1, 0]); const assertionContainer = buildAssertionContainer([a1]); const uri: any = { fsPath: '/ws-1/a.test.ts' }; - context.ext.testResolveProvider.getTestSuiteResult.mockReturnValue({ + context.ext.testResultProvider.getTestSuiteResult.mockReturnValue({ status: 'KnownSuccess', assertionContainer, }); @@ -590,7 +590,7 @@ describe('test-item-data', () => { expect(runMock.passed).toHaveBeenCalledWith(tData.item, undefined); }); it('if no test suite result yet, children list is empty', () => { - context.ext.testResolveProvider.getTestSuiteResult.mockReturnValue(undefined); + context.ext.testResultProvider.getTestSuiteResult.mockReturnValue(undefined); const uri: any = { fsPath: '/ws-1/a.test.ts' }; const parentItem: any = controllerMock.createTestItem('ws-1', 'ws-1', uri); const docRoot = new TestDocumentRoot(context, uri, parentItem); @@ -711,14 +711,14 @@ describe('test-item-data', () => { let wsRoot; beforeEach(() => { jest.clearAllMocks(); - context.ext.testResolveProvider.getTestList.mockReturnValueOnce([file]); + context.ext.testResultProvider.getTestList.mockReturnValueOnce([file]); wsRoot = new WorkspaceRoot(context); // mocking test results const a1 = helper.makeAssertion('test-a', 'KnownSuccess', [], [1, 0]); const a2 = helper.makeAssertion('test-b', 'KnownFail', [], [10, 0], { line: 13 }); const assertionContainer = buildAssertionContainer([a1, a2]); - context.ext.testResolveProvider.getTestSuiteResult.mockReturnValue({ + context.ext.testResultProvider.getTestSuiteResult.mockReturnValue({ status: 'KnownFail', assertionContainer, }); @@ -732,7 +732,7 @@ describe('test-item-data', () => { expect(controllerMock.createTestRun).toHaveBeenCalledTimes(1); // triggers testSuiteChanged event listener - context.ext.testResolveProvider.events.testSuiteChanged.event.mock.calls[0][0]({ + context.ext.testResultProvider.events.testSuiteChanged.event.mock.calls[0][0]({ type: 'assertions-updated', process, files: [file], @@ -765,7 +765,7 @@ describe('test-item-data', () => { expect(controllerMock.createTestRun).toHaveBeenCalledTimes(0); // triggers testSuiteChanged event listener - context.ext.testResolveProvider.events.testSuiteChanged.event.mock.calls[0][0]({ + context.ext.testResultProvider.events.testSuiteChanged.event.mock.calls[0][0]({ type: 'assertions-updated', process, files: [file], @@ -798,7 +798,7 @@ describe('test-item-data', () => { wsRoot.scheduleTest(jestRun); // triggers testSuiteChanged event listener - context.ext.testResolveProvider.events.testSuiteChanged.event.mock.calls[0][0]({ + context.ext.testResultProvider.events.testSuiteChanged.event.mock.calls[0][0]({ type: 'assertions-updated', process, files: [file], @@ -840,7 +840,7 @@ describe('test-item-data', () => { 'c:\\ws-1\\src\\b.test.ts', 'c:\\ws-1\\src\\app\\app.test.ts', ]; - context.ext.testResolveProvider.getTestList.mockReturnValue(testFiles); + context.ext.testResultProvider.getTestList.mockReturnValue(testFiles); const wsRoot = new WorkspaceRoot(context); wsRoot.discoverTest(jestRun); @@ -875,7 +875,7 @@ describe('test-item-data', () => { beforeEach(() => { // establish baseline with 3 test files testFiles = ['/ws-1/src/a.test.ts', '/ws-1/src/b.test.ts', '/ws-1/src/app/app.test.ts']; - context.ext.testResolveProvider.getTestList.mockReturnValue(testFiles); + context.ext.testResultProvider.getTestList.mockReturnValue(testFiles); wsRoot = new WorkspaceRoot(context); wsRoot.discoverTest(jestRun); }); @@ -884,7 +884,7 @@ describe('test-item-data', () => { const withNewTestFiles = [...testFiles, '/ws-1/tests/d.test.ts', '/ws-1/src/c.test.ts']; // trigger event - context.ext.testResolveProvider.events.testListUpdated.event.mock.calls[0][0]( + context.ext.testResultProvider.events.testListUpdated.event.mock.calls[0][0]( withNewTestFiles ); @@ -903,7 +903,7 @@ describe('test-item-data', () => { const withoutAppFiles = [testFiles[0], testFiles[1]]; // trigger event - context.ext.testResolveProvider.events.testListUpdated.event.mock.calls[0][0]( + context.ext.testResultProvider.events.testListUpdated.event.mock.calls[0][0]( withoutAppFiles ); @@ -923,7 +923,7 @@ describe('test-item-data', () => { const withRenamed = ['/ws-1/c.test.ts', testFiles[1], testFiles[2]]; // trigger event - context.ext.testResolveProvider.events.testListUpdated.event.mock.calls[0][0](withRenamed); + context.ext.testResultProvider.events.testListUpdated.event.mock.calls[0][0](withRenamed); //should see the new files in the tree expect(wsRoot.item.children.size).toEqual(2); @@ -946,7 +946,7 @@ describe('test-item-data', () => { const parent: any = controllerMock.createTestItem('ws-1', 'ws-1', { fsPath: '/ws-1' }); a1 = helper.makeAssertion('test-1', 'KnownSuccess', ['desc-1'], [1, 0]); const assertionContainer = buildAssertionContainer([a1]); - context.ext.testResolveProvider.getTestSuiteResult.mockReturnValueOnce({ + context.ext.testResultProvider.getTestSuiteResult.mockReturnValueOnce({ status: 'KnownSuccess', assertionContainer, }); @@ -959,7 +959,7 @@ describe('test-item-data', () => { const a3 = helper.makeAssertion('test-3', 'KnownSuccess', ['desc-2'], [10, 0]); const a4 = helper.makeAssertion('test-4', 'KnownTodo', ['desc-2'], [15, 0]); const assertionContainer = buildAssertionContainer([a1, a2, a3, a4]); - context.ext.testResolveProvider.getTestSuiteResult.mockReturnValue({ + context.ext.testResultProvider.getTestSuiteResult.mockReturnValue({ status: 'KnownFail', assertionContainer, }); @@ -990,7 +990,7 @@ describe('test-item-data', () => { it('delete', () => { // delete the only test -1 const assertionContainer = buildAssertionContainer([]); - context.ext.testResolveProvider.getTestSuiteResult.mockReturnValue({ + context.ext.testResultProvider.getTestSuiteResult.mockReturnValue({ status: 'Unknown', assertionContainer, }); @@ -1000,7 +1000,7 @@ describe('test-item-data', () => { it('rename', () => { const a2 = helper.makeAssertion('test-2', 'KnownFail', [], [1, 0]); const assertionContainer = buildAssertionContainer([a2]); - context.ext.testResolveProvider.getTestSuiteResult.mockReturnValue({ + context.ext.testResultProvider.getTestSuiteResult.mockReturnValue({ status: 'KnownFail', assertionContainer, }); @@ -1014,7 +1014,7 @@ describe('test-item-data', () => { }); it('with syntax error', () => { const assertionContainer = buildAssertionContainer([]); - context.ext.testResolveProvider.getTestSuiteResult.mockReturnValue({ + context.ext.testResultProvider.getTestSuiteResult.mockReturnValue({ status: 'KnownFail', assertionContainer, }); @@ -1028,7 +1028,7 @@ describe('test-item-data', () => { runMock.failed.mockClear(); const assertionContainer = buildAssertionContainer(assertions); - context.ext.testResolveProvider.getTestSuiteResult.mockReturnValue({ + context.ext.testResultProvider.getTestSuiteResult.mockReturnValue({ status: 'KnownFail', assertionContainer, }); @@ -1405,7 +1405,7 @@ describe('test-item-data', () => { createTestRunSpy.mockClear(); // triggers testSuiteChanged event listener - context.ext.testResolveProvider.events.testSuiteChanged.event.mock.calls[0][0]({ + context.ext.testResultProvider.events.testSuiteChanged.event.mock.calls[0][0]({ type: 'assertions-updated', process, files: [env.file], @@ -1453,7 +1453,7 @@ describe('test-item-data', () => { createTestRunSpy.mockClear(); // triggers testSuiteChanged event listener - context.ext.testResolveProvider.events.testSuiteChanged.event.mock.calls[0][0]({ + context.ext.testResultProvider.events.testSuiteChanged.event.mock.calls[0][0]({ type: 'assertions-updated', process, files: [env.file], @@ -1505,7 +1505,7 @@ describe('test-item-data', () => { createTestRunSpy.mockClear(); // process test results - context.ext.testResolveProvider.events.testSuiteChanged.event.mock.calls[0][0]({ + context.ext.testResultProvider.events.testSuiteChanged.event.mock.calls[0][0]({ type: 'assertions-updated', process, files: [env.file], @@ -1542,7 +1542,7 @@ describe('test-item-data', () => { }); describe('view-snapshot', () => { beforeEach(() => { - context.ext.testResolveProvider.previewSnapshot.mockReturnValue(Promise.resolve()); + context.ext.testResultProvider.previewSnapshot.mockReturnValue(Promise.resolve()); }); it.each` case | index | canView @@ -1555,9 +1555,9 @@ describe('test-item-data', () => { const data = [wsRoot, folder, doc, testItem][index]; await data.runItemCommand(ItemCommand.viewSnapshot); if (canView) { - expect(context.ext.testResolveProvider.previewSnapshot).toHaveBeenCalled(); + expect(context.ext.testResultProvider.previewSnapshot).toHaveBeenCalled(); } else { - expect(context.ext.testResolveProvider.previewSnapshot).not.toHaveBeenCalled(); + expect(context.ext.testResultProvider.previewSnapshot).not.toHaveBeenCalled(); } }); it.each` @@ -1571,9 +1571,9 @@ describe('test-item-data', () => { testItem.node.attrs = { ...testItem.node.attrs, snapshot: snapshotAttr }; await testItem.runItemCommand(ItemCommand.viewSnapshot); if (canView) { - expect(context.ext.testResolveProvider.previewSnapshot).toHaveBeenCalled(); + expect(context.ext.testResultProvider.previewSnapshot).toHaveBeenCalled(); } else { - expect(context.ext.testResolveProvider.previewSnapshot).not.toHaveBeenCalled(); + expect(context.ext.testResultProvider.previewSnapshot).not.toHaveBeenCalled(); } } ); From 14ce301e464823b1cca2559747f7a2d3bc495fdb Mon Sep 17 00:00:00 2001 From: connectdotz Date: Sun, 20 Nov 2022 13:23:36 -0500 Subject: [PATCH 15/16] upgrade jest-editor-support --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 37773ffd..4bb569c6 100644 --- a/package.json +++ b/package.json @@ -524,7 +524,7 @@ "dependencies": { "istanbul-lib-coverage": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", - "jest-editor-support": "^30.3.0" + "jest-editor-support": "^30.3.1" }, "devDependencies": { "@types/istanbul-lib-coverage": "^2.0.2", diff --git a/yarn.lock b/yarn.lock index 7bac497f..df3dc420 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2934,10 +2934,10 @@ jest-each@^29.2.1: jest-util "^29.2.1" pretty-format "^29.2.1" -jest-editor-support@^30.3.0: - version "30.3.0" - resolved "https://registry.yarnpkg.com/jest-editor-support/-/jest-editor-support-30.3.0.tgz#b248e20f97be0ab31ff26b3848536e67b3c22044" - integrity sha512-U0yAYzTRUqSomlMazCOPZXzyqBghSwEzc2vSAjxHA8rylB4uQ1ZyaqOfsffqm1En3jhsVuYSoh0wXnBevrETRw== +jest-editor-support@^30.3.1: + version "30.3.1" + resolved "https://registry.yarnpkg.com/jest-editor-support/-/jest-editor-support-30.3.1.tgz#d7c3eb396c0c6d8a99bfb01e4f02dd416280d437" + integrity sha512-LcQubLda7j5CzSXkwxJqm4PELUw66e6JHtNMXR0erzz37aiEUOlGpOYgWKoVH7JnNsH7IgJ78jmhCqdB1LLdhw== dependencies: "@babel/parser" "^7.15.7" "@babel/runtime" "^7.15.4" From 38462fe0b4ba98baf58c306e0435dcc19933dba7 Mon Sep 17 00:00:00 2001 From: connectdotz Date: Sun, 20 Nov 2022 13:45:18 -0500 Subject: [PATCH 16/16] address lint error/warning --- package.json | 4 ++-- tests/JestExt/process-listeners.test.ts | 1 - tests/test-helper.ts | 4 ++-- tests/test-provider/test-provider.test.ts | 1 - 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 4bb569c6..ee046260 100644 --- a/package.json +++ b/package.json @@ -376,7 +376,7 @@ }, { "command": "io.orta.jest.test-item.update-snapshot" - } + } ], "testing/item/gutter": [ { @@ -386,7 +386,7 @@ { "command": "io.orta.jest.test-item.update-snapshot", "when": "testId in jest.editor-update-snapshot" - } + } ] }, "keybindings": [ diff --git a/tests/JestExt/process-listeners.test.ts b/tests/JestExt/process-listeners.test.ts index 9d06750c..565484cd 100644 --- a/tests/JestExt/process-listeners.test.ts +++ b/tests/JestExt/process-listeners.test.ts @@ -227,7 +227,6 @@ describe('jest process listeners', () => { }); }); describe('RunTestListener', () => { - /* eslint-disable jest/no-conditional-expect */ beforeEach(() => { mockSession.context.output = { appendLine: jest.fn(), diff --git a/tests/test-helper.ts b/tests/test-helper.ts index 2d008a81..057caa33 100644 --- a/tests/test-helper.ts +++ b/tests/test-helper.ts @@ -19,7 +19,7 @@ export const makeLocation = (pos: [number, number]): Location => ({ line: pos[0], column: pos[1], }); -export const makePositionRange = (pos: [number, number, number, number]) => ({ +export const makePositionRange = (pos: [number, number, number, number]): any => ({ start: makeLocation([pos[0], pos[1]]), end: makeLocation([pos[2], pos[3]]), }); @@ -146,7 +146,7 @@ export const mockProjectWorkspace = (...args: any[]): any => { export const mockWworkspaceLogging = (): any => ({ create: () => jest.fn() }); -export const mockEvent = () => ({ +export const mockEvent = (): any => ({ event: jest.fn().mockReturnValue({ dispose: jest.fn() }), fire: jest.fn(), dispose: jest.fn(), diff --git a/tests/test-provider/test-provider.test.ts b/tests/test-provider/test-provider.test.ts index 4f8f2bd1..1ea05af1 100644 --- a/tests/test-provider/test-provider.test.ts +++ b/tests/test-provider/test-provider.test.ts @@ -517,7 +517,6 @@ describe('JestTestProvider', () => { const [run] = d.scheduleTest.mock.calls[0]; expect(run).toEqual(expect.objectContaining({ vscodeRun: controllerMock.lastRunMock() })); - /* eslint-disable jest/no-conditional-expect */ if (idx === 1) { expect(run.vscodeRun.errored).toHaveBeenCalledWith( d.item,