diff --git a/src/tree/appMapTreeDataProvider.ts b/src/tree/appMapTreeDataProvider.ts index db1751d9..01c0ec60 100644 --- a/src/tree/appMapTreeDataProvider.ts +++ b/src/tree/appMapTreeDataProvider.ts @@ -14,9 +14,9 @@ const LABEL_NO_NAME = 'Untitled AppMap'; export interface IAppMapTreeItem { appmap: AppMapLoader | undefined; - parent: IAppMapTreeItem | undefined; + parent: (vscode.TreeItem & IAppMapTreeItem) | undefined; - children: IAppMapTreeItem[]; + children: (vscode.TreeItem & IAppMapTreeItem)[]; } class AppMapFolder { @@ -139,7 +139,6 @@ class FolderTreeItem extends vscode.TreeItem implements IAppMapTreeItem { let { recorderType } = folderContent.folder; if (!recorderType) recorderType = 'unknown recorder type'; const sortFunction = FolderTreeItem.SortMethod[recorderType] || FolderTreeItem.sortByName; - warn(`Sorting ${appmaps.length} appmaps by ${recorderType}`); // TODO: Remove folderContent.appmaps.sort(sortFunction); return new FolderTreeItem(workspaceTreeItem, folderContent.folder, folderContent.appmaps); diff --git a/test/unit/tree/appMapTreeDataProvider.test.ts b/test/unit/tree/appMapTreeDataProvider.test.ts index 01fde563..04c8c12e 100644 --- a/test/unit/tree/appMapTreeDataProvider.test.ts +++ b/test/unit/tree/appMapTreeDataProvider.test.ts @@ -5,78 +5,301 @@ import { expect } from 'chai'; import { AppMapTreeDataProvider, IAppMapTreeItem } from '../../../src/tree/appMapTreeDataProvider'; import AppMapCollection from '../../../src/services/appmapCollection'; -import { AppmapUptodateService } from '../../../src/services/appmapUptodateService'; import AppMapLoader from '../../../src/services/appmapLoader'; +import { build } from 'tsup'; describe('AppMapTreeDataProvider', () => { let sinon: SinonSandbox; - let appmaps: AppMapCollection; let provider: AppMapTreeDataProvider; - const APPMAPS: AppMapLoader[] = [ - { - descriptor: { - metadata: { - language: { name: 'ruby', version: '2.7.2' }, - recorder: { - type: 'tests', - name: 'rspec', + beforeEach(() => (sinon = Sinon.createSandbox())); + afterEach(() => sinon.restore()); + + function findProject(label: string) { + return provider + .getChildren(undefined) + .find((treeItem) => treeItem.label === label) as IAppMapTreeItem & vscode.TreeItem; + } + + function getProjectA() { + return findProject('project-a'); + } + + function getProjectB() { + return findProject('project-b'); + } + + function buildSingleProjectWorkspace(appmaps: AppMapLoader[]): () => void { + return function () { + sinon.stub(vscode.workspace, 'workspaceFolders').value([ + { + index: 0, + name: 'project-a', + uri: vscode.Uri.file('/path/to/project-a'), + }, + ]); + + const appmapCollection = { + appMaps: Sinon.stub().returns(appmaps), + onUpdated: Sinon.stub(), + } as unknown as AppMapCollection; + provider = new AppMapTreeDataProvider(appmapCollection); + }; + } + + describe('in a single project workspace', () => { + const appmaps: AppMapLoader[] = [ + { + descriptor: { + metadata: { + name: 'appmap_1', + language: { name: 'ruby', version: '2.7.2' }, + recorder: { + type: 'tests', + name: 'rspec', + }, }, + resourceUri: vscode.Uri.file('/path/to/project-a/tmp/appmap/appmap_1.json'), }, - resourceUri: vscode.Uri.file('/path/to/project-a/tmp/appmap/appmap_1.json'), - }, - } as AppMapLoader, - { - descriptor: { - metadata: { - language: { name: 'ruby', version: '2.7.2' }, - recorder: { - type: 'requests', + } as AppMapLoader, + { + descriptor: { + metadata: { + name: 'appmap_2', + language: { name: 'ruby', version: '2.7.2' }, + recorder: { + type: 'requests', + }, }, + resourceUri: vscode.Uri.file('/path/to/project-a/tmp/appmap/appmap_2.json'), }, - resourceUri: vscode.Uri.file('/path/to/project-a/tmp/appmap/appmap_2.json'), - }, - } as AppMapLoader, - ]; + } as AppMapLoader, + ]; - beforeEach(() => (sinon = Sinon.createSandbox())); - afterEach(() => sinon.restore()); + beforeEach(buildSingleProjectWorkspace(appmaps)); - beforeEach(() => { - sinon.stub(vscode.workspace, 'workspaceFolders').value([ - { - index: 0, - name: 'project-a', - uri: vscode.Uri.file('/path/to/project-a'), - }, - ]); - - appmaps = { - appMaps: Sinon.stub().returns(APPMAPS), - onUpdated: Sinon.stub(), - } as unknown as AppMapCollection; - provider = new AppMapTreeDataProvider(appmaps); + describe('getRootElements', () => { + it('returns the single workspace', async () => { + const roots = provider.getChildren(undefined); + expect(roots.map((item) => item.label)).to.deep.equal(['project-a']); + }); + }); + + describe('appmap folders', () => { + it('exist for each recording type', () => { + const project = getProjectA(); + const folderItems = provider.getChildren(project); + expect(folderItems.map((item) => item.label)).to.deep.equal([ + 'Requests (ruby)', + 'Tests (ruby + rspec)', + ]); + }); + }); + + describe('AppMap items', () => { + function getFolderItem(label: string) { + return getProjectA().children.find((folder) => folder.label === label); + } + + it('exist for each AppMap', () => { + { + const folderItem = getFolderItem('Tests (ruby + rspec)'); + const appmapItems = provider.getChildren(folderItem); + expect(appmapItems.map((item) => item.label)).to.deep.equal(['appmap_1']); + } + { + const folderItem = getFolderItem('Requests (ruby)'); + const appmapItems = provider.getChildren(folderItem); + expect(appmapItems.map((item) => item.label)).to.deep.equal(['appmap_2']); + } + }); + }); }); - describe('getRootElement', () => { - it('returns the root element', async () => { - const roots = provider.getChildren(undefined); - expect(roots.map((item) => item.label)).to.deep.equal(['project-a']); + describe('requests recordings', () => { + function requestRecording(name: string, timestamp: number): AppMapLoader { + return { + descriptor: { + metadata: { + name, + timestamp, + language: { name: 'ruby', version: '2.7.2' }, + recorder: { + type: 'requests', + }, + }, + timestamp, + resourceUri: vscode.Uri.file(`/path/to/project-a/tmp/appmap/${name}.json`), + }, + } as unknown as AppMapLoader; + } + + const appmaps: AppMapLoader[] = [ + requestRecording('appmap_1', 1), + requestRecording('appmap_2', 3), + requestRecording('appmap_3', 5), + requestRecording('appmap_4', 6), + requestRecording('appmap_5', 4), + requestRecording('appmap_6', 2), + ]; + + beforeEach(buildSingleProjectWorkspace(appmaps)); + + it('are sorted by timestamp with most recent first', () => { + const project = getProjectA(); + const folderItems = provider.getChildren(project); + const appmapItems = provider.getChildren(folderItems[0]); + expect(appmapItems.map((item) => item.label)).to.deep.equal([ + 'appmap_4', + 'appmap_3', + 'appmap_5', + 'appmap_2', + 'appmap_6', + 'appmap_1', + ]); }); }); - describe('Project folder items', () => { - function getProject() { - return provider.getChildren(undefined)[0] as IAppMapTreeItem & vscode.TreeItem; + describe('tests recordings', () => { + function testRecording(name: string, timestamp: number): AppMapLoader { + return { + descriptor: { + metadata: { + name, + timestamp, + language: { name: 'ruby', version: '2.7.2' }, + recorder: { + type: 'tests', + name: 'rspec', + }, + }, + timestamp, + resourceUri: vscode.Uri.file(`/path/to/project-a/tmp/appmap/${name}.json`), + }, + } as unknown as AppMapLoader; } - it('exist for each recording type', () => { - const project = getProject(); + const appmaps: AppMapLoader[] = [ + testRecording('appmap_4', 6), + testRecording('appmap_1', 1), + testRecording('appmap_5', 4), + testRecording('appmap_2', 3), + testRecording('appmap_3', 5), + testRecording('appmap_6', 2), + ]; + + beforeEach(buildSingleProjectWorkspace(appmaps)); + + it('are sorted alphabetically by label', () => { + const project = getProjectA(); const folderItems = provider.getChildren(project); - expect(folderItems.map((item) => item.label)).to.deep.equal([ - 'Requests (ruby)', - 'Tests (ruby + rspec)', + const appmapItems = provider.getChildren(folderItems[0]); + expect(appmapItems.map((item) => item.label)).to.deep.equal([ + 'appmap_1', + 'appmap_2', + 'appmap_3', + 'appmap_4', + 'appmap_5', + 'appmap_6', + ]); + }); + }); + + describe('missing appmap metadata', () => { + const appmaps: AppMapLoader[] = [ + { + descriptor: { + resourceUri: vscode.Uri.file('/path/to/project-a/tmp/appmap/appmap_1.json'), + }, + } as AppMapLoader, + { + descriptor: { + resourceUri: vscode.Uri.file('/path/to/project-a/tmp/appmap/appmap_2.json'), + }, + } as AppMapLoader, + ]; + + beforeEach(buildSingleProjectWorkspace(appmaps)); + + it('is replaced with suitable defaults', () => { + const project = getProjectA(); + const folderItems = provider.getChildren(project); + expect(folderItems.map((item) => item.label)).to.deep.equal(['unspecified language']); + const appmapItems = provider.getChildren(folderItems[0]); + expect(appmapItems.map((item) => item.label)).to.deep.equal([ + 'Untitled AppMap', + 'Untitled AppMap', + ]); + }); + }); + + describe('in a multi-project workspace', () => { + const appmaps: AppMapLoader[] = [ + { + descriptor: { + metadata: { + name: 'appmap_1', + language: { name: 'ruby', version: '2.7.2' }, + recorder: { + type: 'tests', + name: 'rspec', + }, + }, + resourceUri: vscode.Uri.file('/path/to/project-a/tmp/appmap/appmap_1.json'), + }, + } as AppMapLoader, + { + descriptor: { + metadata: { + name: 'appmap_2', + language: { name: 'ruby', version: '2.7.2' }, + recorder: { + type: 'requests', + }, + }, + resourceUri: vscode.Uri.file('/path/to/project-b/tmp/appmap/appmap_2.json'), + }, + } as AppMapLoader, + ]; + + beforeEach(() => { + sinon.stub(vscode.workspace, 'workspaceFolders').value([ + { + index: 0, + name: 'project-a', + uri: vscode.Uri.file('/path/to/project-a'), + }, + { + index: 1, + name: 'project-b', + uri: vscode.Uri.file('/path/to/project-b'), + }, ]); + + const appmapCollection = { + appMaps: Sinon.stub().returns(appmaps), + onUpdated: Sinon.stub(), + } as unknown as AppMapCollection; + provider = new AppMapTreeDataProvider(appmapCollection); + }); + + describe('appmap folders', () => { + it('are placed in the proper projects', () => { + { + const project = getProjectA(); + const folderItems = provider.getChildren(project); + expect(folderItems.map((item) => item.label)).to.deep.equal(['Tests (ruby + rspec)']); + const appmapItems = provider.getChildren(folderItems[0]); + expect(appmapItems.map((item) => item.label)).to.deep.equal(['appmap_1']); + } + { + const project = getProjectB(); + const folderItems = provider.getChildren(project); + expect(folderItems.map((item) => item.label)).to.deep.equal(['Requests (ruby)']); + const appmapItems = provider.getChildren(folderItems[0]); + expect(appmapItems.map((item) => item.label)).to.deep.equal(['appmap_2']); + } + }); }); }); });