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
This commit is contained in:
mohit-bruno
2026-06-26 16:57:02 +05:30
committed by GitHub
parent 0a3ee95310
commit 33e8f5ca4a
8 changed files with 207 additions and 6 deletions

18
package-lock.json generated
View File

@@ -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"

View File

@@ -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",

View File

@@ -178,6 +178,7 @@ const ImportWorkspace = ({ onClose }) => {
/>
<input
ref={fileInputRef}
data-testid="import-workspace-file-input"
type="file"
className="hidden"
onChange={handleFileInputChange}

View File

@@ -0,0 +1,15 @@
import { Page, test } from '../../../playwright';
export const buildTitleBarLocators = (page: Page) => ({
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();
});
};

View File

@@ -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);
};

View File

@@ -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');

View File

@@ -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);
});
});

View File

@@ -0,0 +1,7 @@
{
"preferences": {
"general": {
"defaultLocation": "{{wsLocation}}"
}
}
}