From 33e8f5ca4af2579eca0e1a278ed260f8052383d2 Mon Sep 17 00:00:00 2001 From: mohit-bruno Date: Fri, 26 Jun 2026 16:57:02 +0530 Subject: [PATCH] test: workspace import and validation testcase (TC-969) (#8349) * test cases for workspace import and validation TC-969, jira: https://usebruno.atlassian.net/browse/BRU-3575 * incorporated comments, moved findWorkspaceDirByName function to helpers.ts, fixed all the comments * modified as per comment provided , removed css locators , used playwright inbuilt methods , handled timeout * Created file structure as per comment provided, added tc-id , resolved code-rabbit review * incorporated comments removed commented line, removed timeouts, modified package.json added package in dev dependencies * changed const l to locators for better readbility * - Reorganized test helpers: split title-bar locators into title-bar.ts and import-workspace flow into workspace/import-workspace.ts for reuse - Replaced brittle .bruno-modal-card/CSS locators with stable role/testid/label based locators - Added a data-testid for the Import Workspace modal and removed the redundant one - Cleaned up unnecessary comments - Updated package-lock.json * minor changes * minor changes * addressed comments * addressed comments for variable naming * minor changes --- package-lock.json | 18 ++- package.json | 3 + .../WorkspaceSidebar/ImportWorkspace/index.js | 1 + tests/utils/page/title-bar.ts | 15 +++ .../utils/page/workspace/import-workspace.ts | 110 ++++++++++++++++++ .../create-workspace/create-workspace.spec.ts | 6 +- .../import-workspace/import-workspace.spec.ts | 53 +++++++++ .../init-user-data/preferences.json | 7 ++ 8 files changed, 207 insertions(+), 6 deletions(-) create mode 100644 tests/utils/page/title-bar.ts create mode 100644 tests/utils/page/workspace/import-workspace.ts create mode 100644 tests/workspace/import-workspace/import-workspace.spec.ts create mode 100644 tests/workspace/import-workspace/init-user-data/preferences.json diff --git a/package-lock.json b/package-lock.json index 587ed027f..05375668a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,11 +37,13 @@ "@storybook/react": "^10.1.10", "@storybook/react-webpack5": "^10.1.10", "@stylistic/eslint-plugin": "^5.3.1", + "@types/adm-zip": "^0.5.8", "@types/jest": "^29.5.11", "@types/js-yaml": "^4.0.9", "@types/lodash-es": "^4.17.12", "@types/node": "^22.14.1", "@typescript-eslint/parser": "^8.39.0", + "adm-zip": "^0.5.17", "concurrently": "^8.2.2", "cross-env": "10.1.0", "eslint": "^9.39.4", @@ -12678,6 +12680,16 @@ "node": ">=10.13.0" } }, + "node_modules/@types/adm-zip": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.8.tgz", + "integrity": "sha512-RVVH7QvZYbN+ihqZ4kX/dMiowf6o+Jk1fNwiSdx0NahBJLU787zkULhGhJM8mf/obmLGmgdMM0bXsQTmyfbR7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -14176,9 +14188,9 @@ } }, "node_modules/adm-zip": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", - "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.17.tgz", + "integrity": "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==", "license": "MIT", "engines": { "node": ">=12.0" diff --git a/package.json b/package.json index 8f7279f28..4100ee1a7 100644 --- a/package.json +++ b/package.json @@ -30,11 +30,13 @@ "@storybook/react": "^10.1.10", "@storybook/react-webpack5": "^10.1.10", "@stylistic/eslint-plugin": "^5.3.1", + "@types/adm-zip": "^0.5.8", "@types/jest": "^29.5.11", "@types/js-yaml": "^4.0.9", "@types/lodash-es": "^4.17.12", "@types/node": "^22.14.1", "@typescript-eslint/parser": "^8.39.0", + "adm-zip": "^0.5.17", "concurrently": "^8.2.2", "cross-env": "10.1.0", "eslint": "^9.39.4", @@ -83,6 +85,7 @@ "test:e2e": "playwright test --project=default --project=system-pac", "test:e2e:ssl": "playwright test --project=ssl", "test:e2e:auth": "playwright test --project=auth", + "test:e2e:sanity": "playwright test --project=default --project=system-pac --grep @sanity", "test:benchmark": "playwright test --config=playwright.benchmark.config.ts", "lint": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" npx eslint", "lint:fix": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" npx eslint --fix", diff --git a/packages/bruno-app/src/components/WorkspaceSidebar/ImportWorkspace/index.js b/packages/bruno-app/src/components/WorkspaceSidebar/ImportWorkspace/index.js index b7d006062..576e1b472 100644 --- a/packages/bruno-app/src/components/WorkspaceSidebar/ImportWorkspace/index.js +++ b/packages/bruno-app/src/components/WorkspaceSidebar/ImportWorkspace/index.js @@ -178,6 +178,7 @@ const ImportWorkspace = ({ onClose }) => { /> ({ + workspaceMenuTrigger: () => page.getByTestId('workspace-menu'), + activeWorkspaceName: () => page.getByTestId('workspace-name'), + importWorkspaceOption: () => page.getByTestId('workspace-menu-import-workspace') +}); + +export const clickImportWorkspace = async (page: Page) => { + const titleBar = buildTitleBarLocators(page); + await test.step('Open workspace menu and click "Import workspace"', async () => { + await titleBar.workspaceMenuTrigger().click(); + await titleBar.importWorkspaceOption().click(); + }); +}; diff --git a/tests/utils/page/workspace/import-workspace.ts b/tests/utils/page/workspace/import-workspace.ts new file mode 100644 index 000000000..c7c59d986 --- /dev/null +++ b/tests/utils/page/workspace/import-workspace.ts @@ -0,0 +1,110 @@ +import AdmZip from 'adm-zip'; +import * as path from 'path'; +import { clickImportWorkspace } from '../title-bar'; +import { test, expect, Page, Locator, ElectronApplication, waitForReadyPage } from '../../../../playwright'; + +/** + * Import Workspace modal locators. + */ +export const buildImportWorkspaceModalLocators = (page: Page) => { + // Scope every modal query to the dialog so we avoid the brittle + const modal = () => page.getByRole('dialog').filter({ hasText: 'Import Workspace' }); + + return { + // Import Workspace modal + modal, + fileInput: () => modal().getByTestId('import-workspace-file-input'), + selectedFileName: (name: string) => modal().getByText(name), + removeFileButton: () => modal().getByText('Remove'), + locationInput: () => modal().getByLabel('Extract Location'), + browseLink: () => modal().getByText('Browse', { exact: true }), + importButton: () => modal().getByTestId('modal-submit-btn') + }; +}; + +/** + * Build a valid Bruno workspace zip on disk that the importer will accept. + * The zip contains a single `workspace.yml` (info.name + info.type: workspace). + * + * @param zipDir - directory in which to write the zip + * @param workspaceName - the workspace name embedded in workspace.yml + * @returns absolute path to the created zip file + */ +export const createWorkspaceZip = (zipDir: string, workspaceName: string): string => { + const workspaceYml = [ + 'opencollection: 1.0.0', + 'info:', + ` name: "${workspaceName}"`, + ' type: workspace', + '', + 'collections: []', + 'specs: []', + 'docs: \'\'', + '' + ].join('\n'); + + const zip = new AdmZip(); + zip.addFile('workspace.yml', Buffer.from(workspaceYml, 'utf8')); + + const zipPath = path.join(zipDir, `${workspaceName}.zip`); + zip.writeZip(zipPath); + return zipPath; +}; + +/** + * Open the workspace dropdown and launch the Import Workspace modal. + */ +export const openImportWorkspaceModal = async (page: Page) => { + const locators = buildImportWorkspaceModalLocators(page); + await clickImportWorkspace(page); + await locators.modal().waitFor({ state: 'visible' }); +}; + +type ImportWorkspaceOptions = { + zipPath: string; + /** + * Where to extract the workspace. When omitted, the modal's pre-filled + * default location (from preferences.general.defaultLocation) is used as-is. + */ + extractLocation?: string; + app?: ElectronApplication; +}; + +/** + * select the zip, ensure an extract location is set, and click Import. + */ +export const submitWorkspaceImport = async (page: Page, opts: ImportWorkspaceOptions) => { + const locators = buildImportWorkspaceModalLocators(page); + + await test.step('Select the workspace zip file', async () => { + await locators.fileInput().setInputFiles(opts.zipPath); + await expect(locators.selectedFileName(path.basename(opts.zipPath))).toBeVisible(); + }); + + await test.step('Ensure an extract location is set', async () => { + if (opts.extractLocation && opts.app) { + // Stub the directory picker so Browse resolves to the desired location. + await opts.app.evaluate(({ dialog }, target: string) => { + (dialog as { showOpenDialog: typeof dialog.showOpenDialog }).showOpenDialog = () => + Promise.resolve({ canceled: false, filePaths: [target] }); + }, opts.extractLocation); + await locators.locationInput().click(); + await expect(locators.locationInput()).toHaveValue(opts.extractLocation); + } else { + // Rely on the pre-filled default location. + await expect(locators.locationInput()).not.toHaveValue(''); + } + }); + + await test.step('Submit the import', async () => { + await locators.importButton().click(); + }); +}; + +/** + * open the modal and import a zip in one call. + */ +export const importWorkspaceFromZip = async (page: Page, opts: ImportWorkspaceOptions) => { + await openImportWorkspaceModal(page); + await submitWorkspaceImport(page, opts); +}; diff --git a/tests/workspace/create-workspace/create-workspace.spec.ts b/tests/workspace/create-workspace/create-workspace.spec.ts index 1b6eddeb1..42a5222dc 100644 --- a/tests/workspace/create-workspace/create-workspace.spec.ts +++ b/tests/workspace/create-workspace/create-workspace.spec.ts @@ -24,8 +24,8 @@ function findCreatedWorkspaceDirs(location: string): string[] { } test.describe('Create Workspace', () => { - test.describe('Inline Creation Flow', () => { - test('should create workspace via inline rename and press Enter', async ({ launchElectronApp, createTmpDir }) => { + test.describe('inline workspace creation flow', () => { + test('TC-957: Verify create a workspace directly from the title bar by typing a name', { tag: '@sanity' }, async ({ launchElectronApp, createTmpDir }) => { const wsLocation = await createTmpDir('ws-location-enter'); const app = await launchElectronApp({ initUserDataPath, templateVars: { wsLocation } }); @@ -454,7 +454,7 @@ test.describe('Create Workspace', () => { await closeElectronApp(app); }); - test('should persist workspace name after app restart', async ({ launchElectronApp, createTmpDir }) => { + test('TC-959: Verify created Workspace persists even after bruno app restart.', { tag: '@sanity' }, async ({ launchElectronApp, createTmpDir }) => { const userDataPath = await createTmpDir('create-ws-name-persist'); const wsLocation = await createTmpDir('ws-location-persist'); diff --git a/tests/workspace/import-workspace/import-workspace.spec.ts b/tests/workspace/import-workspace/import-workspace.spec.ts new file mode 100644 index 000000000..2c6ce4fe0 --- /dev/null +++ b/tests/workspace/import-workspace/import-workspace.spec.ts @@ -0,0 +1,53 @@ +import path from 'path'; +import fs from 'fs'; +import yaml from 'js-yaml'; +import { test, expect, closeElectronApp, waitForReadyPage } from '../../../playwright'; +import { + createWorkspaceZip, + importWorkspaceFromZip +} from '../../utils/page/workspace/import-workspace'; +import { buildTitleBarLocators } from '../../utils/page/title-bar'; + +type WorkspaceConfig = { + info?: { name: string; type: string }; +}; + +const initUserDataPath = path.join(__dirname, 'init-user-data'); + +test.describe('Import Workspace', () => { + test('TC-969: Verify Import workspace from local directory containing valid workspace.zip file', { tag: '@sanity' }, async ({ launchElectronApp, createTmpDir }) => { + const wsLocation = await createTmpDir('import-ws-location'); + const zipDir = await createTmpDir('import-ws-zip'); + const workspaceName = 'Imported WS'; + const zipPath = createWorkspaceZip(zipDir, workspaceName); + + const app = await launchElectronApp({ initUserDataPath, templateVars: { wsLocation } }); + const page = await waitForReadyPage(app); + const titleBar = buildTitleBarLocators(page); + + await test.step('Import the workspace zip via the Import Workspace modal', async () => { + // extractLocation isn't passed as a parameter: the modal pre-fills the seeded default location. + await importWorkspaceFromZip(page, { zipPath }); + }); + + await test.step('Verify success toast is shown', async () => { + await expect(page.getByText('Workspace imported successfully!')).toBeVisible(); + }); + + await test.step('Verify the imported workspace becomes the active workspace', async () => { + await expect(titleBar.activeWorkspaceName()).toHaveText(workspaceName); + }); + + await test.step('Verify the workspace was extracted to the filesystem', async () => { + const wsDir = path.join(wsLocation, workspaceName); + const ymlPath = path.join(wsDir, 'workspace.yml'); + expect(fs.existsSync(ymlPath)).toBe(true); + + const wsConfig = yaml.load(fs.readFileSync(ymlPath, 'utf8')) as WorkspaceConfig; + expect(wsConfig?.info?.name).toBe(workspaceName); + expect(wsConfig?.info?.type).toBe('workspace'); + }); + + await closeElectronApp(app); + }); +}); diff --git a/tests/workspace/import-workspace/init-user-data/preferences.json b/tests/workspace/import-workspace/init-user-data/preferences.json new file mode 100644 index 000000000..67997e5ab --- /dev/null +++ b/tests/workspace/import-workspace/init-user-data/preferences.json @@ -0,0 +1,7 @@ +{ + "preferences": { + "general": { + "defaultLocation": "{{wsLocation}}" + } + } +} \ No newline at end of file