mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-27 22:54:07 +00:00
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:
18
package-lock.json
generated
18
package-lock.json
generated
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -178,6 +178,7 @@ const ImportWorkspace = ({ onClose }) => {
|
||||
/>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
data-testid="import-workspace-file-input"
|
||||
type="file"
|
||||
className="hidden"
|
||||
onChange={handleFileInputChange}
|
||||
|
||||
15
tests/utils/page/title-bar.ts
Normal file
15
tests/utils/page/title-bar.ts
Normal 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();
|
||||
});
|
||||
};
|
||||
110
tests/utils/page/workspace/import-workspace.ts
Normal file
110
tests/utils/page/workspace/import-workspace.ts
Normal 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);
|
||||
};
|
||||
@@ -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');
|
||||
|
||||
|
||||
53
tests/workspace/import-workspace/import-workspace.spec.ts
Normal file
53
tests/workspace/import-workspace/import-workspace.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"preferences": {
|
||||
"general": {
|
||||
"defaultLocation": "{{wsLocation}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user