diff --git a/libs/server/src/lib/utils/utils.ts b/libs/server/src/lib/utils/utils.ts index 16d57a6cf6..0f5a3a3142 100644 --- a/libs/server/src/lib/utils/utils.ts +++ b/libs/server/src/lib/utils/utils.ts @@ -350,6 +350,11 @@ export function toWorkspaceFormat(w: any): WorkspaceJsonConfiguration { }); }); + const sortedProjects = Object.entries(w.projects || {}).sort( + (projectA, projectB) => projectA[0].localeCompare(projectB[0]) + ); + w.projects = Object.fromEntries(sortedProjects); + if (w.schematics) { renameProperty(w, 'schematics', 'generators'); } diff --git a/libs/vscode/nx-workspace/__mocks__/@nx-console/server.ts b/libs/vscode/nx-workspace/__mocks__/@nx-console/server.ts new file mode 100644 index 0000000000..1e24fb9a0c --- /dev/null +++ b/libs/vscode/nx-workspace/__mocks__/@nx-console/server.ts @@ -0,0 +1,22 @@ +const cacheJson = jest.fn(); + +const fileExistsSync = jest.fn(); + +const getOutputChannel = jest.fn(() => ({ + appendLine: jest.fn(), + show: jest.fn(), +})); + +const getTelemetry = jest.fn(() => ({ + exception: jest.fn(), +})); + +const toWorkspaceFormat = jest.fn(); + +export { + cacheJson, + fileExistsSync, + getOutputChannel, + getTelemetry, + toWorkspaceFormat, +}; diff --git a/libs/vscode/nx-workspace/__mocks__/@nx-console/vscode/configuration.ts b/libs/vscode/nx-workspace/__mocks__/@nx-console/vscode/configuration.ts new file mode 100644 index 0000000000..538b37efb2 --- /dev/null +++ b/libs/vscode/nx-workspace/__mocks__/@nx-console/vscode/configuration.ts @@ -0,0 +1,7 @@ +const WorkspaceConfigurationStore = { + instance: { + get: jest.fn(), + }, +}; + +export { WorkspaceConfigurationStore }; diff --git a/libs/vscode/nx-workspace/__mocks__/vscode.ts b/libs/vscode/nx-workspace/__mocks__/vscode.ts new file mode 100644 index 0000000000..5a3ef84cd8 --- /dev/null +++ b/libs/vscode/nx-workspace/__mocks__/vscode.ts @@ -0,0 +1,9 @@ +/// + +const window = { + showErrorMessage: jest.fn(), +}; + +export = { + window, +}; diff --git a/libs/vscode/nx-workspace/jest.config.js b/libs/vscode/nx-workspace/jest.config.js index 71049c6a76..3be58c1950 100644 --- a/libs/vscode/nx-workspace/jest.config.js +++ b/libs/vscode/nx-workspace/jest.config.js @@ -8,6 +8,9 @@ module.exports = { '^.+\\.[tj]sx?$': 'ts-jest', }, moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + moduleNameMapper: { + '^@nx-console/vscode/(.+)$': '/libs/vscode/$1/src/index.ts', + }, coverageDirectory: '../../../coverage/libs/vscode/nx-workspace', testEnvironment: 'node', }; diff --git a/libs/vscode/nx-workspace/src/lib/verify-workspace.spec.ts b/libs/vscode/nx-workspace/src/lib/verify-workspace.spec.ts new file mode 100644 index 0000000000..0680882c97 --- /dev/null +++ b/libs/vscode/nx-workspace/src/lib/verify-workspace.spec.ts @@ -0,0 +1,178 @@ +import { verifyWorkspace } from './verify-workspace'; +import { WorkspaceConfigurationStore } from '@nx-console/vscode/configuration'; +import * as server from '@nx-console/server'; +import { + cacheJson, + fileExistsSync, + getOutputChannel, + getTelemetry, +} from '@nx-console/server'; +import type { WorkspaceJsonConfiguration } from '@nrwl/devkit'; +import * as vscode from 'vscode'; + +const mockCacheJsonFn = cacheJson as jest.MockedFunction; +mockCacheJsonFn.mockImplementation((filePath) => ({ + json: mockWorkspace, + path: filePath, +})); + +const mockStoreInstanceGetFn = WorkspaceConfigurationStore.instance + .get as jest.MockedFunction; +mockStoreInstanceGetFn.mockImplementation(() => workspacePath); + +const mockFileExistsSyncFn = fileExistsSync as jest.MockedFunction< + typeof fileExistsSync +>; + +const originalNxConsoleServerModule = jest.requireActual('@nx-console/server'); +(server.toWorkspaceFormat as unknown) = + originalNxConsoleServerModule.toWorkspaceFormat; + +const mockWorkspace: WorkspaceJsonConfiguration = { + version: 2, + projects: { + Project3: { + root: 'project-three', + }, + Project1: { + root: 'project-one', + }, + Project2: { + root: 'project-two', + }, + }, + defaultProject: undefined, + generators: undefined, + cli: undefined, +}; + +const DefaultWorkspaceInformation = { + validWorkspaceJson: false, + workspaceType: 'nx', + json: { + projects: {}, + version: 2, + }, + configurationFilePath: '', +}; + +const workspacePath = './test/fixtures/workspace/'; + +describe(verifyWorkspace.name, () => { + afterEach(() => { + mockFileExistsSyncFn.mockClear(); + }); + + describe('when Nx workspace exists', () => { + it('returns information about Nx workspace', async () => { + // arrange + mockFileExistsSyncFn.mockImplementation((filePath) => + /workspace.json$/i.test(filePath) + ); + + // act + const { validWorkspaceJson, json, workspaceType, configurationFilePath } = + await verifyWorkspace(); + + // assert + expect(mockFileExistsSyncFn).toHaveBeenCalledTimes(1); + expect(mockFileExistsSyncFn).toHaveLastReturnedWith(true); + expect(mockStoreInstanceGetFn).toHaveBeenCalledWith( + 'nxWorkspaceJsonPath', + '' + ); + expect(mockCacheJsonFn).toHaveBeenCalled(); + expect(validWorkspaceJson).toBe(true); + expect(json).toBeTruthy(); + expect(json).toEqual(mockWorkspace); + expect(workspaceType).toBe('nx'); + expect(configurationFilePath).toMatch(/workspace.json$/i); + }); + }); + + describe('when Ng workspace exists', () => { + it('returns information about Ng workspace', async () => { + // arrange + mockFileExistsSyncFn.mockImplementation((filePath) => + /angular.json$/i.test(filePath) + ); + + // act + const { validWorkspaceJson, json, workspaceType, configurationFilePath } = + await verifyWorkspace(); + + // assert + expect(mockFileExistsSyncFn).toHaveBeenCalledTimes(2); + expect(mockFileExistsSyncFn).toHaveNthReturnedWith(1, false); + expect(mockFileExistsSyncFn).toHaveNthReturnedWith(2, true); + expect(mockStoreInstanceGetFn).toHaveBeenCalledWith( + 'nxWorkspaceJsonPath', + '' + ); + expect(mockCacheJsonFn).toHaveBeenCalled(); + expect(validWorkspaceJson).toBe(true); + expect(json).toBeTruthy(); + expect(json).toEqual(mockWorkspace); + expect(workspaceType).toBe('ng'); + expect(configurationFilePath).toMatch(/angular.json$/i); + }); + }); + + describe('when workspace json does not exist', () => { + it('it shows error dialog and returns default workspace information', async () => { + // arrange + mockFileExistsSyncFn.mockImplementation( + (filePath) => !/(angular|workspace).json$/i.test(filePath) + ); + + (vscode.window.showErrorMessage as unknown) = jest + .fn() + .mockResolvedValue('Show Error'); + + // act + const result = await verifyWorkspace(); + + // assert + expect(mockFileExistsSyncFn).toHaveBeenCalledTimes(2); + expect(mockFileExistsSyncFn).toHaveNthReturnedWith(1, false); + expect(mockFileExistsSyncFn).toHaveNthReturnedWith(2, false); + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( + expect.any(String), + 'Show Error' + ); + expect(getOutputChannel).toHaveBeenCalledTimes(3); + expect(getTelemetry).toHaveBeenCalledTimes(1); + expect(result).toEqual(DefaultWorkspaceInformation); + }); + }); + + describe('when workspace information is found', () => { + it('projects entries are sorted by entry key', async () => { + // arrange + mockFileExistsSyncFn.mockImplementationOnce(() => true); + const sortedProject = { + Project1: { + root: 'project-one', + }, + Project2: { + root: 'project-two', + }, + Project3: { + root: 'project-three', + }, + }; + + // act + const { + json: { projects }, + } = await verifyWorkspace(); + const [project1, project2, project3] = Object.keys(projects); + const [sorted1, sorted2, sorted3] = Object.keys(sortedProject); + + // assert + expect(project1).toBe(sorted1); // should be 'Project1' + expect(project2).toBe(sorted2); // should be 'Project2' + expect(project3).toBe(sorted3); // should be 'Project3' + }); + }); +});