diff --git a/packages/bruno-app/src/components/ApiSpecPanel/SpecViewer.js b/packages/bruno-app/src/components/ApiSpecPanel/SpecViewer.js
index ae1a9cdeb..cf4e1dc33 100644
--- a/packages/bruno-app/src/components/ApiSpecPanel/SpecViewer.js
+++ b/packages/bruno-app/src/components/ApiSpecPanel/SpecViewer.js
@@ -1,11 +1,34 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import get from 'lodash/get';
+import jsyaml from 'js-yaml';
import { useTheme } from 'providers/Theme';
import { useSelector } from 'react-redux';
-import { IconDeviceFloppy, IconLoader2 } from '@tabler/icons';
+import { IconAlertCircle, IconDeviceFloppy, IconLoader2 } from '@tabler/icons';
+import { isOpenApiSpec } from 'utils/importers/openapi-collection';
import CodeEditor from './FileEditor/CodeEditor/index';
import Swagger from './Renderers/Swagger';
import { useDragResize } from 'hooks/useDragResize';
+import { SPEC_PREVIEW_ERRORS } from './constants';
+
+const PREVIEW_TIMEOUT_MS = 15000;
+
+const getPreviewParseError = (content) => {
+ if (!content || typeof content !== 'string') return null;
+ let parsed;
+ try {
+ parsed = JSON.parse(content);
+ } catch {
+ try {
+ parsed = jsyaml.load(content);
+ } catch {
+ return SPEC_PREVIEW_ERRORS.INVALID_YAML_JSON;
+ }
+ }
+ if (!isOpenApiSpec(parsed)) {
+ return SPEC_PREVIEW_ERRORS.INVALID_OPENAPI;
+ }
+ return null;
+};
const MIN_LEFT_PANE_WIDTH = 300;
const MIN_RIGHT_PANE_WIDTH = 450;
@@ -51,16 +74,40 @@ const SpecViewer = ({ content, readOnly, onSave, leftPaneWidth, onLeftPaneWidthC
: { flex: '1 1 50%', minWidth: 0 };
const [swaggerReady, setSwaggerReady] = useState(false);
+ const [previewError, setPreviewError] = useState(null);
+ const previewTimeoutRef = useRef(null);
useEffect(() => {
setSwaggerReady(false);
+ clearTimeout(previewTimeoutRef.current);
+
+ if (!content || !content.trim()) {
+ setPreviewError(SPEC_PREVIEW_ERRORS.EMPTY);
+ return;
+ }
+
+ const parseErr = getPreviewParseError(content);
+ if (parseErr) {
+ setPreviewError(parseErr);
+ return;
+ }
+ setPreviewError(null);
+
+ previewTimeoutRef.current = setTimeout(() => {
+ setPreviewError(SPEC_PREVIEW_ERRORS.TIMEOUT);
+ }, PREVIEW_TIMEOUT_MS);
+
+ return () => clearTimeout(previewTimeoutRef.current);
}, [content]);
const handleSwaggerComplete = useCallback(() => {
// Double rAF: wait for one full paint cycle so Swagger is actually on screen
// before hiding the loader — avoids a flash of unrendered content.
requestAnimationFrame(() => {
- requestAnimationFrame(() => setSwaggerReady(true));
+ requestAnimationFrame(() => {
+ clearTimeout(previewTimeoutRef.current);
+ setSwaggerReady(true);
+ });
});
}, []);
@@ -101,19 +148,33 @@ const SpecViewer = ({ content, readOnly, onSave, leftPaneWidth, onLeftPaneWidthC
className="api-spec-right-pane relative"
style={{ flex: '1 1 50%', minWidth: 0 }}
>
-
-
-
- {!swaggerReady && (
+ {previewError ? (
-
-
-
Generating preview…
+
+
+ {previewError}
+ ) : (
+ <>
+
+
+
+ {!swaggerReady && (
+
+
+
+ Generating preview…
+
+
+ )}
+ >
)}
diff --git a/packages/bruno-app/src/components/ApiSpecPanel/constants.ts b/packages/bruno-app/src/components/ApiSpecPanel/constants.ts
new file mode 100644
index 000000000..a48288643
--- /dev/null
+++ b/packages/bruno-app/src/components/ApiSpecPanel/constants.ts
@@ -0,0 +1,6 @@
+export const SPEC_PREVIEW_ERRORS = {
+ EMPTY: 'Unable to render preview: No API definition provided.',
+ INVALID_YAML_JSON: 'Unable to render preview: content is not a valid YAML or JSON.',
+ INVALID_OPENAPI: 'Unable to render preview: content is not a valid OpenAPI specification.',
+ TIMEOUT: 'Preview timed out. The spec may be too large or contain unsupported content.'
+} as const;
diff --git a/packages/bruno-app/src/utils/importers/openapi-collection.js b/packages/bruno-app/src/utils/importers/openapi-collection.js
index c28b6f926..afbbd7efd 100644
--- a/packages/bruno-app/src/utils/importers/openapi-collection.js
+++ b/packages/bruno-app/src/utils/importers/openapi-collection.js
@@ -11,6 +11,10 @@ export const convertOpenapiToBruno = (data, options = {}) => {
};
export const isOpenApiSpec = (data) => {
+ if (!data || typeof data !== 'object') {
+ return false;
+ }
+
if (typeof data.info !== 'object' || data.info === null) {
return false;
}
diff --git a/packages/bruno-electron/src/app/apiSpecs.js b/packages/bruno-electron/src/app/apiSpecs.js
index cb4350dc3..6b0cb1827 100644
--- a/packages/bruno-electron/src/app/apiSpecs.js
+++ b/packages/bruno-electron/src/app/apiSpecs.js
@@ -1,8 +1,9 @@
-const fs = require('fs');
-const path = require('path');
+const fs = require('node:fs');
+const path = require('node:path');
const { dialog, ipcMain } = require('electron');
const { normalizeAndResolvePath } = require('../utils/filesystem');
const { generateUidBasedOnHash } = require('../utils/common');
+const { parseApiSpecContent } = require('../utils/apiSpecs');
const {
addApiSpecToWorkspace,
readWorkspaceConfig,
@@ -11,6 +12,18 @@ const {
const DEFAULT_WORKSPACE_NAME = 'My Workspace';
+const INVALID_EXTENSION_MESSAGE
+ = 'Invalid file format. Please select a valid OpenAPI spec in YAML or JSON format.';
+
+const VALID_API_SPEC_EXTENSIONS = ['.yaml', '.yml', '.json'];
+
+const validateApiSpec = (filePath) => {
+ const ext = path.extname(filePath).toLowerCase();
+ if (!VALID_API_SPEC_EXTENSIONS.includes(ext)) {
+ throw new Error(INVALID_EXTENSION_MESSAGE);
+ }
+};
+
const prepareWorkspaceConfigForClient = (workspaceConfig, isDefault) => {
if (isDefault) {
return {
@@ -24,7 +37,8 @@ const prepareWorkspaceConfigForClient = (workspaceConfig, isDefault) => {
const openApiSpecDialog = async (win, watcher, options = {}) => {
const { filePaths } = await dialog.showOpenDialog(win, {
- properties: ['openFile', 'createFile']
+ properties: ['openFile', 'createFile'],
+ filters: [{ name: 'OpenAPI Spec', extensions: ['yaml', 'yml', 'json'] }]
});
if (filePaths && filePaths[0]) {
@@ -39,6 +53,8 @@ const openApiSpecDialog = async (win, watcher, options = {}) => {
const openApiSpec = async (win, watcher, apiSpecPath, options = {}) => {
try {
+ validateApiSpec(apiSpecPath);
+
const uid = generateUidBasedOnHash(apiSpecPath);
if (options.workspacePath) {
@@ -76,28 +92,22 @@ const openApiSpec = async (win, watcher, apiSpecPath, options = {}) => {
if (!watcher.hasWatcher(apiSpecPath)) {
ipcMain.emit('main:apispec-opened', win, apiSpecPath, uid, options.workspacePath);
} else {
+ const rawContent = fs.readFileSync(apiSpecPath, 'utf8');
+ const extension = path.extname(apiSpecPath);
+
win.webContents.send('main:apispec-tree-updated', 'addFile', {
pathname: apiSpecPath,
uid: uid,
- raw: require('fs').readFileSync(apiSpecPath, 'utf8'),
- name: require('path').basename(apiSpecPath, require('path').extname(apiSpecPath)),
- filename: require('path').basename(apiSpecPath),
- json: (() => {
- const ext = require('path').extname(apiSpecPath).toLowerCase();
- const content = require('fs').readFileSync(apiSpecPath, 'utf8');
- if (ext === '.yaml' || ext === '.yml') {
- return require('js-yaml').load(content);
- } else if (ext === '.json') {
- return JSON.parse(content);
- }
- return null;
- })()
+ raw: rawContent,
+ name: path.basename(apiSpecPath, path.extname(apiSpecPath)),
+ filename: path.basename(apiSpecPath),
+ json: parseApiSpecContent(rawContent, extension)
});
}
} catch (err) {
if (!options.dontSendDisplayErrors) {
win.webContents.send('main:display-error', {
- error: err.message || 'An error occurred while opening the apiSpec'
+ message: err.message || 'An error occurred while opening the apiSpec'
});
}
}
@@ -105,5 +115,6 @@ const openApiSpec = async (win, watcher, apiSpecPath, options = {}) => {
module.exports = {
openApiSpec,
- openApiSpecDialog
+ openApiSpecDialog,
+ INVALID_EXTENSION_MESSAGE
};
diff --git a/packages/bruno-electron/src/app/apiSpecsWatcher.js b/packages/bruno-electron/src/app/apiSpecsWatcher.js
index 1aee5c35b..dc07d3f0f 100644
--- a/packages/bruno-electron/src/app/apiSpecsWatcher.js
+++ b/packages/bruno-electron/src/app/apiSpecsWatcher.js
@@ -1,29 +1,16 @@
const _ = require('lodash');
-const fs = require('fs');
-const path = require('path');
+const fs = require('node:fs');
+const path = require('node:path');
const chokidar = require('chokidar');
const { getApiSpecUid } = require('../cache/apiSpecUids');
-const yaml = require('js-yaml');
const { isDirectory } = require('../utils/filesystem');
-const { safeParseJSON } = require('../utils/common');
+const { parseApiSpecContent } = require('../utils/apiSpecs');
const hasApiSpecExtension = (filename) => {
if (!filename || typeof filename !== 'string') return false;
return ['yaml', 'yml', 'json'].some((ext) => filename.toLowerCase().endsWith(`.${ext}`));
};
-const parseApiSpecContent = (pathname) => {
- const extension = path.extname(pathname).toLowerCase();
- let content = fs.readFileSync(pathname, 'utf8');
-
- if (extension === '.yaml' || extension === '.yml') {
- return yaml.load(content);
- } else if (extension === '.json') {
- return safeParseJSON(content);
- }
- return null;
-};
-
const hydrateApiSpecWithUuid = (apiSpec, pathname) => {
apiSpec.uid = getApiSpecUid(pathname);
return apiSpec;
@@ -34,9 +21,11 @@ const add = async (win, pathname) => {
try {
const basename = path.basename(pathname);
const file = {};
- const apiSpecContent = parseApiSpecContent(pathname);
+ const raw = fs.readFileSync(pathname, 'utf8');
+ const extension = path.extname(pathname);
+ const apiSpecContent = parseApiSpecContent(raw, extension);
- file.raw = fs.readFileSync(pathname, 'utf8');
+ file.raw = raw;
file.name = apiSpecContent?.info?.title || basename.split('.')[0];
file.filename = basename;
file.pathname = pathname;
@@ -53,9 +42,11 @@ const change = async (win, pathname) => {
try {
const basename = path.basename(pathname);
const file = {};
- const apiSpecContent = parseApiSpecContent(pathname);
+ const raw = fs.readFileSync(pathname, 'utf8');
+ const extension = path.extname(pathname);
+ const apiSpecContent = parseApiSpecContent(raw, extension);
- file.raw = fs.readFileSync(pathname, 'utf8');
+ file.raw = raw;
file.name = apiSpecContent?.info?.title || basename.split('.')[0];
file.filename = basename;
file.pathname = pathname;
diff --git a/packages/bruno-electron/src/utils/apiSpecs.js b/packages/bruno-electron/src/utils/apiSpecs.js
new file mode 100644
index 000000000..a57b2f110
--- /dev/null
+++ b/packages/bruno-electron/src/utils/apiSpecs.js
@@ -0,0 +1,19 @@
+const yaml = require('js-yaml');
+
+const parseApiSpecContent = (content, extension) => {
+ const ext = (extension || '').toLowerCase();
+
+ try {
+ if (ext === '.yaml' || ext === '.yml') {
+ return yaml.load(content);
+ } else if (ext === '.json') {
+ return JSON.parse(content);
+ }
+ } catch {
+ return null;
+ }
+
+ return null;
+};
+
+module.exports = { parseApiSpecContent };
diff --git a/packages/bruno-electron/tests/app/apiSpecs.spec.js b/packages/bruno-electron/tests/app/apiSpecs.spec.js
new file mode 100644
index 000000000..63a38b03f
--- /dev/null
+++ b/packages/bruno-electron/tests/app/apiSpecs.spec.js
@@ -0,0 +1,107 @@
+const path = require('path');
+const fs = require('fs');
+const os = require('os');
+const { INVALID_EXTENSION_MESSAGE } = require('../../src/app/apiSpecs');
+
+jest.mock('electron', () => ({
+ dialog: { showOpenDialog: jest.fn() },
+ ipcMain: { emit: jest.fn() }
+}));
+
+const { ipcMain } = require('electron');
+const { openApiSpec } = require('../../src/app/apiSpecs');
+
+describe('openApiSpec', () => {
+ let tmpDir;
+ let win;
+ let watcher;
+
+ const writeSpecFile = (filename, content) => {
+ const filePath = path.join(tmpDir, filename);
+ fs.writeFileSync(filePath, content);
+ return filePath;
+ };
+
+ beforeEach(() => {
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bruno-apispec-'));
+ win = { webContents: { send: jest.fn() } };
+ watcher = { hasWatcher: jest.fn() };
+ });
+
+ afterEach(() => {
+ fs.rmSync(tmpDir, { recursive: true, force: true });
+ });
+
+ test.each(['spec.txt', 'spec.xml', 'spec', 'spec.yamlx'])(
+ 'rejects a file with an unsupported extension (%s)',
+ async (filename) => {
+ const specPath = writeSpecFile(filename, 'This is a plain text file, not an OpenAPI spec.');
+
+ await openApiSpec(win, watcher, specPath);
+
+ expect(win.webContents.send).toHaveBeenCalledWith('main:display-error', {
+ message: INVALID_EXTENSION_MESSAGE
+ });
+ expect(ipcMain.emit).not.toHaveBeenCalled();
+ }
+ );
+
+ test.each(['openapi.yaml', 'openapi.yml', 'openapi.json', 'openapi.YAML', 'openapi.Json'])(
+ 'accepts a file with a supported extension (%s)',
+ async (filename) => {
+ const specPath = writeSpecFile(filename, '{}');
+ watcher.hasWatcher.mockReturnValue(false);
+
+ await openApiSpec(win, watcher, specPath);
+
+ expect(win.webContents.send).not.toHaveBeenCalledWith('main:display-error', expect.anything());
+ expect(ipcMain.emit).toHaveBeenCalledWith('main:apispec-opened', win, specPath, expect.any(String), undefined);
+ }
+ );
+
+ test('does not send a display error when dontSendDisplayErrors is set', async () => {
+ const specPath = writeSpecFile('spec.txt', 'not a spec');
+
+ await openApiSpec(win, watcher, specPath, { dontSendDisplayErrors: true });
+
+ expect(win.webContents.send).not.toHaveBeenCalled();
+ });
+
+ test('opens a valid spec by emitting main:apispec-opened', async () => {
+ const specPath = writeSpecFile('openapi.yaml', 'openapi: 3.0.0\ninfo:\n title: Test\n version: 1.0.0\npaths: {}\n');
+ watcher.hasWatcher.mockReturnValue(false);
+
+ await openApiSpec(win, watcher, specPath);
+
+ expect(ipcMain.emit).toHaveBeenCalledWith('main:apispec-opened', win, specPath, expect.any(String), undefined);
+ expect(win.webContents.send).not.toHaveBeenCalledWith('main:display-error', expect.anything());
+ });
+
+ test('opens a malformed file with a valid extension without throwing, resolving json to null', async () => {
+ const specPath = writeSpecFile('malformed.yaml', 'openapi: 3.0.0\ninfo:\n title: Test\n version: : :\npaths: [');
+ watcher.hasWatcher.mockReturnValue(true);
+
+ await openApiSpec(win, watcher, specPath);
+
+ expect(win.webContents.send).toHaveBeenCalledWith(
+ 'main:apispec-tree-updated',
+ 'addFile',
+ expect.objectContaining({ pathname: specPath, json: null })
+ );
+ expect(win.webContents.send).not.toHaveBeenCalledWith('main:display-error', expect.anything());
+ });
+
+ test('opens a broken JSON file with a valid extension without throwing, resolving json to null', async () => {
+ const specPath = writeSpecFile('broken.json', '{\n "openapi": "3.0.0",\n "info": {\n "title": "Test"\n "version": "1.0.0"\n },\n "paths": {\n');
+ watcher.hasWatcher.mockReturnValue(true);
+
+ await openApiSpec(win, watcher, specPath);
+
+ expect(win.webContents.send).toHaveBeenCalledWith(
+ 'main:apispec-tree-updated',
+ 'addFile',
+ expect.objectContaining({ pathname: specPath, json: null })
+ );
+ expect(win.webContents.send).not.toHaveBeenCalledWith('main:display-error', expect.anything());
+ });
+});
diff --git a/tests/import/openapi/api-spec-panel-validation.spec.ts b/tests/import/openapi/api-spec-panel-validation.spec.ts
new file mode 100644
index 000000000..b956ae21a
--- /dev/null
+++ b/tests/import/openapi/api-spec-panel-validation.spec.ts
@@ -0,0 +1,74 @@
+import { test, expect } from '../../../playwright';
+import * as path from 'path';
+import { openApiSpecFromDialog, openApiSpecSidebarItem } from '../../utils/page/openapi/render-spec';
+import { SPEC_PREVIEW_ERRORS } from '../../../packages/bruno-app/src/components/ApiSpecPanel/constants';
+import { INVALID_EXTENSION_MESSAGE } from '../../../packages/bruno-electron/src/app/apiSpecs';
+
+test.describe('API Spec Panel - open & preview validation', () => {
+ test.beforeAll(async ({ electronApp }) => {
+ await electronApp.evaluate(({ dialog }) => {
+ (dialog as any).__savedShowOpenDialog = dialog.showOpenDialog;
+ });
+ });
+
+ test.afterAll(async ({ electronApp }) => {
+ await electronApp.evaluate(({ dialog }) => {
+ dialog.showOpenDialog = (dialog as any).__savedShowOpenDialog;
+ delete (dialog as any).__savedShowOpenDialog;
+ });
+ });
+
+ test('Reject a file with an unsupported extension', async ({ page, electronApp }) => {
+ const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-invalid-extension.txt');
+ await openApiSpecFromDialog(page, electronApp, openApiFile);
+ await expect(
+ page.getByText(INVALID_EXTENSION_MESSAGE)
+ ).toBeVisible();
+ });
+
+ test('Show the empty-spec message when opening an empty file', async ({ page, electronApp }) => {
+ const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-empty.yaml');
+ await openApiSpecFromDialog(page, electronApp, openApiFile);
+ await openApiSpecSidebarItem(page, 'openapi-empty');
+ await expect(page.getByText(SPEC_PREVIEW_ERRORS.EMPTY)).toBeVisible();
+ });
+
+ test('Show a parse error when opening malformed YAML', async ({ page, electronApp }) => {
+ const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-malformed.yaml');
+ await openApiSpecFromDialog(page, electronApp, openApiFile);
+ await openApiSpecSidebarItem(page, 'openapi-malformed');
+ await expect(
+ page.getByText(SPEC_PREVIEW_ERRORS.INVALID_YAML_JSON)
+ ).toBeVisible();
+ });
+
+ test('Show a parse error when opening malformed JSON', async ({ page, electronApp }) => {
+ const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-broken.json');
+ await openApiSpecFromDialog(page, electronApp, openApiFile);
+ await openApiSpecSidebarItem(page, 'openapi-broken');
+ await expect(
+ page.getByText(SPEC_PREVIEW_ERRORS.INVALID_YAML_JSON)
+ ).toBeVisible();
+ });
+
+ test('Show a spec error when the file parses but is not a valid OpenAPI document', async ({ page, electronApp }) => {
+ const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-missing-info.yaml');
+
+ await openApiSpecFromDialog(page, electronApp, openApiFile);
+ await openApiSpecSidebarItem(page, 'openapi-missing-info');
+ // Parses as an object but lacks the openapi/swagger + info contract, so it must fail
+ // fast with a precise message rather than falling through to the preview timeout.
+ await expect(
+ page.getByText(SPEC_PREVIEW_ERRORS.INVALID_OPENAPI)
+ ).toBeVisible();
+ });
+
+ test('Render a valid spec without any preview error', async ({ page, electronApp }) => {
+ const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-simple.json');
+ await openApiSpecFromDialog(page, electronApp, openApiFile);
+ await openApiSpecSidebarItem(page, 'Simple Test API');
+ // Panel opens for the valid spec (filename shown in the header) and no preview error appears
+ await expect(page.getByText('openapi-simple.json')).toBeVisible();
+ await expect(page.getByText(/Unable to render preview/i)).toHaveCount(0);
+ });
+});
diff --git a/tests/import/openapi/fixtures/openapi-broken.json b/tests/import/openapi/fixtures/openapi-broken.json
new file mode 100644
index 000000000..9e75d94cf
--- /dev/null
+++ b/tests/import/openapi/fixtures/openapi-broken.json
@@ -0,0 +1,19 @@
+{
+ "openapi": "3.0.0",
+ "info": {
+ "title": "Malformed OpenAPI"
+ "version": "1.0.0"
+ },
+ "paths": {
+ "/test": {
+ "get": {
+ "summary": "Test endpoint",
+ "responses": {
+ "200": {
+ "description": "Success"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/tests/import/openapi/fixtures/openapi-empty.yaml b/tests/import/openapi/fixtures/openapi-empty.yaml
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/import/openapi/fixtures/openapi-invalid-extension.txt b/tests/import/openapi/fixtures/openapi-invalid-extension.txt
new file mode 100644
index 000000000..a1681035a
--- /dev/null
+++ b/tests/import/openapi/fixtures/openapi-invalid-extension.txt
@@ -0,0 +1 @@
+This is a plain text file, not an OpenAPI spec.
\ No newline at end of file
diff --git a/tests/utils/page/locators.ts b/tests/utils/page/locators.ts
index 25f106c36..2655e0b2d 100644
--- a/tests/utils/page/locators.ts
+++ b/tests/utils/page/locators.ts
@@ -1,7 +1,11 @@
import { Page, Locator } from '../../../playwright';
+import { buildApiSpecPanelLocators } from './openapi/render-spec';
export const buildCommonLocators = (page: Page) => ({
runner: () => page.getByTestId('run-button'),
+ openApi: {
+ render: buildApiSpecPanelLocators(page)
+ },
saveButton: () => page
.locator('.infotip')
.filter({ hasText: /^Save/ }),
diff --git a/tests/utils/page/openapi/render-spec.ts b/tests/utils/page/openapi/render-spec.ts
new file mode 100644
index 000000000..dae854b76
--- /dev/null
+++ b/tests/utils/page/openapi/render-spec.ts
@@ -0,0 +1,30 @@
+import { test, Page, ElectronApplication } from '../../../../playwright';
+
+export const buildApiSpecPanelLocators = (page: Page) => ({
+ addMenuButton: () => page.getByTestId('api-specs-header-add-menu'),
+ openApiSpecMenuItem: () => page.getByTestId('api-specs-header-add-menu-open-api-spec'),
+ sidebarItem: (name: string) => page.locator('.api-spec-item').filter({ hasText: name })
+});
+
+export const openApiSpecFromDialog = async (
+ page: Page,
+ electronApp: ElectronApplication,
+ filePath: string
+): Promise => {
+ await test.step(`Open API spec from path: ${filePath}`, async () => {
+ await electronApp.evaluate(({ dialog }, filePath) => {
+ dialog.showOpenDialog = async () => ({ canceled: false, filePaths: [filePath] });
+ }, filePath);
+
+ const { addMenuButton, openApiSpecMenuItem } = buildApiSpecPanelLocators(page);
+ await addMenuButton().click();
+ await openApiSpecMenuItem().click();
+ });
+};
+
+export const openApiSpecSidebarItem = async (page: Page, name: string): Promise => {
+ await test.step(`Open API spec sidebar item "${name}"`, async () => {
+ const { sidebarItem } = buildApiSpecPanelLocators(page);
+ await sidebarItem(name).click();
+ });
+};