mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
fix: relative path getting stored as absolute on windows (#7895)
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import path from 'utils/common/path';
|
||||
import { getRelativePathWithinBasePath } from 'utils/common/path';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { browseFiles } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { IconX, IconUpload, IconFile } from '@tabler/icons';
|
||||
@@ -48,13 +48,7 @@ const FilePickerEditor = ({
|
||||
// If file is in the collection's directory, then we use relative path
|
||||
// Otherwise, we use the absolute path
|
||||
filePaths = filePaths.map((filePath) => {
|
||||
const collectionDir = collection.pathname;
|
||||
|
||||
if (filePath.startsWith(collectionDir)) {
|
||||
return path.relative(collectionDir, filePath);
|
||||
}
|
||||
|
||||
return filePath;
|
||||
return getRelativePathWithinBasePath(collection.pathname, filePath);
|
||||
});
|
||||
|
||||
onChange(isSingleFilePicker ? filePaths[0] : filePaths);
|
||||
|
||||
@@ -14,7 +14,7 @@ import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collection
|
||||
import { updateTableColumnWidths } from 'providers/ReduxStore/slices/tabs';
|
||||
import EditableTable from 'components/EditableTable';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import path from 'utils/common/path';
|
||||
import { getRelativePathWithinBasePath } from 'utils/common/path';
|
||||
import { usePersistedState } from 'hooks/usePersistedState';
|
||||
import { useTrackScroll } from 'hooks/useTrackScroll';
|
||||
import { isWindowsOS } from 'utils/common/platform';
|
||||
@@ -60,11 +60,7 @@ const MultipartFormParams = ({ item, collection }) => {
|
||||
dispatch(browseFiles())
|
||||
.then((filePaths) => {
|
||||
const processedPaths = filePaths.map((filePath) => {
|
||||
const collectionDir = collection.pathname;
|
||||
if (filePath.startsWith(collectionDir)) {
|
||||
return path.relative(collectionDir, filePath);
|
||||
}
|
||||
return filePath;
|
||||
return getRelativePathWithinBasePath(collection.pathname, filePath);
|
||||
});
|
||||
|
||||
const currentParams = item.draft
|
||||
|
||||
@@ -7,7 +7,7 @@ import { updateResponseExampleMultipartFormParams } from 'providers/ReduxStore/s
|
||||
import { browseFiles } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { updateTableColumnWidths } from 'providers/ReduxStore/slices/tabs';
|
||||
import mime from 'mime-types';
|
||||
import path from 'utils/common/path';
|
||||
import path, { getRelativePathWithinBasePath } from 'utils/common/path';
|
||||
import EditableTable from 'components/EditableTable';
|
||||
import MultiLineEditor from 'components/MultiLineEditor';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
@@ -51,11 +51,7 @@ const ResponseExampleMultipartFormParams = ({ item, collection, exampleUid, edit
|
||||
dispatch(browseFiles())
|
||||
.then((filePaths) => {
|
||||
const processedPaths = filePaths.map((filePath) => {
|
||||
const collectionDir = collection.pathname;
|
||||
if (filePath.startsWith(collectionDir)) {
|
||||
return path.relative(collectionDir, filePath);
|
||||
}
|
||||
return filePath;
|
||||
return getRelativePathWithinBasePath(collection.pathname, filePath);
|
||||
});
|
||||
|
||||
const currentParams = params || [];
|
||||
|
||||
@@ -163,10 +163,60 @@ const getAbsoluteFilePath = (basePath, relativePath, shouldPosixify = false) =>
|
||||
return shouldPosixify ? posixify(result) : result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a relative path when filePath is contained within basePath.
|
||||
* For paths outside basePath (or same path), returns the original filePath unchanged.
|
||||
*
|
||||
* @param {string} basePath - The base path to check containment against (e.g., collection pathname).
|
||||
* @param {string} filePath - The absolute file path to compute a relative path for.
|
||||
* @param {boolean} [shouldPosixify=false] - When true, output uses '/' separators for
|
||||
* cross-platform safety. Callers storing to version-controlled config files should opt in
|
||||
* by passing true. Default false preserves legacy platform-native separators for
|
||||
* backwards compatibility.
|
||||
* @returns {string} Relative path if filePath is inside basePath, otherwise filePath itself.
|
||||
*
|
||||
* @example
|
||||
* getRelativePathWithinBasePath('/users/john/collections/api', '/users/john/collections/api/files/payload.txt');
|
||||
* → "files/payload.txt"
|
||||
*
|
||||
* @example
|
||||
* getRelativePathWithinBasePath('/users/john/collections/api', '/users/john/downloads/payload.txt');
|
||||
* → "/users/john/downloads/payload.txt"
|
||||
*
|
||||
* @example
|
||||
* On Windows with posixify enabled
|
||||
* getRelativePathWithinBasePath('C:\\Users\\John\\Collections\\Api', 'C:\\Users\\John\\Collections\\Api\\files\\payload.txt', true);
|
||||
* → "files/payload.txt"
|
||||
*/
|
||||
const getRelativePathWithinBasePath = (basePath, filePath, shouldPosixify = false) => {
|
||||
if (!basePath || !filePath) {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
try {
|
||||
const relativePath = getRelativePath(basePath, filePath, shouldPosixify);
|
||||
const sep = shouldPosixify ? '/' : brunoPath.sep;
|
||||
|
||||
if (
|
||||
!relativePath
|
||||
|| relativePath === '.'
|
||||
|| relativePath === '..'
|
||||
|| relativePath.startsWith(`..${sep}`)
|
||||
|| brunoPath.isAbsolute(relativePath)
|
||||
) {
|
||||
return shouldPosixify ? posixify(filePath) : filePath;
|
||||
}
|
||||
|
||||
return relativePath;
|
||||
} catch (error) {
|
||||
return shouldPosixify ? posixify(filePath) : filePath;
|
||||
}
|
||||
};
|
||||
|
||||
const normalizePath = (p) => {
|
||||
if (!p) return '';
|
||||
return p.replace(/\\/g, '/').replace(/\/+$/, '');
|
||||
};
|
||||
|
||||
export default brunoPath;
|
||||
export { getRelativePath, getBasename, getAbsoluteFilePath, normalizePath };
|
||||
export { getRelativePath, getBasename, getAbsoluteFilePath, getRelativePathWithinBasePath, normalizePath };
|
||||
|
||||
@@ -5,7 +5,8 @@ jest.mock('platform', () => ({
|
||||
}
|
||||
}));
|
||||
|
||||
import { getRelativePath, getBasename, getAbsoluteFilePath } from './path';
|
||||
import path from 'path';
|
||||
import { getRelativePath, getBasename, getAbsoluteFilePath, getRelativePathWithinBasePath } from './path';
|
||||
|
||||
describe('Path Utilities - Unix Platform', () => {
|
||||
describe('getRelativePath', () => {
|
||||
@@ -25,6 +26,10 @@ describe('Path Utilities - Unix Platform', () => {
|
||||
expect(getRelativePath('/users/john/projects', '/users/john/projects/src/components')).toBe('src/components');
|
||||
});
|
||||
|
||||
it('should return ".." for direct parent directory', () => {
|
||||
expect(getRelativePath('/users/john/projects', '/users/john')).toBe('..');
|
||||
});
|
||||
|
||||
it('should handle null/undefined inputs', () => {
|
||||
expect(getRelativePath(null, '/users/john/projects')).toBe('/users/john/projects');
|
||||
expect(getRelativePath(undefined, '/users/john/projects')).toBe('/users/john/projects');
|
||||
@@ -113,6 +118,78 @@ describe('Path Utilities - Unix Platform', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRelativePathWithinBasePath', () => {
|
||||
it('should store in-collection files as relative paths', () => {
|
||||
const result = getRelativePathWithinBasePath('/users/john/collections/api', '/users/john/collections/api/files/payload.txt');
|
||||
expect(result).toBe('files/payload.txt');
|
||||
});
|
||||
|
||||
it('should handle collection paths with trailing separators', () => {
|
||||
const result = getRelativePathWithinBasePath('/users/john/collections/api/', '/users/john/collections/api/files/payload.txt');
|
||||
expect(result).toBe('files/payload.txt');
|
||||
});
|
||||
|
||||
it('should resolve dot segments before deciding whether a file is inside the collection', () => {
|
||||
const result = getRelativePathWithinBasePath('/users/john/collections/api', '/users/john/collections/api/files/../payload.txt');
|
||||
expect(result).toBe('payload.txt');
|
||||
});
|
||||
|
||||
it('should keep paths that resolve outside the collection absolute', () => {
|
||||
const filePath = '/users/john/collections/api/../payload.txt';
|
||||
const result = getRelativePathWithinBasePath('/users/john/collections/api', filePath);
|
||||
expect(result).toBe(filePath);
|
||||
});
|
||||
|
||||
it('should keep outside collection paths absolute', () => {
|
||||
const filePath = '/users/john/downloads/payload.txt';
|
||||
const result = getRelativePathWithinBasePath('/users/john/collections/api', filePath);
|
||||
expect(result).toBe(filePath);
|
||||
});
|
||||
|
||||
it('should keep sibling prefix paths absolute', () => {
|
||||
const filePath = '/users/john/collections/api-other/payload.txt';
|
||||
const result = getRelativePathWithinBasePath('/users/john/collections/api', filePath);
|
||||
expect(result).toBe(filePath);
|
||||
});
|
||||
|
||||
it('should keep same-path values unchanged', () => {
|
||||
const filePath = '/users/john/collections/api';
|
||||
const result = getRelativePathWithinBasePath('/users/john/collections/api', filePath);
|
||||
expect(result).toBe(filePath);
|
||||
});
|
||||
|
||||
it('should store in-collection paths whose names begin with two dots as relative paths', () => {
|
||||
const result = getRelativePathWithinBasePath('/users/john/collections/api', '/users/john/collections/api/..payload.txt');
|
||||
expect(result).toBe('..payload.txt');
|
||||
});
|
||||
|
||||
it('should keep the original file path when inputs are missing', () => {
|
||||
expect(getRelativePathWithinBasePath('', '/users/john/downloads/payload.txt')).toBe('/users/john/downloads/payload.txt');
|
||||
expect(getRelativePathWithinBasePath('/users/john/collections/api', '')).toBe('');
|
||||
});
|
||||
|
||||
it('should treat relative collection path as cwd-relative when file path is absolute', () => {
|
||||
const collectionPath = 'collections/api';
|
||||
const filePath = path.resolve(collectionPath, 'files/payload.txt');
|
||||
const result = getRelativePathWithinBasePath(collectionPath, filePath);
|
||||
expect(result).toBe('files/payload.txt');
|
||||
});
|
||||
|
||||
it('should treat relative file path as cwd-relative when collection path is absolute', () => {
|
||||
const collectionPath = path.resolve('collections/api');
|
||||
const filePath = 'collections/api/files/payload.txt';
|
||||
const result = getRelativePathWithinBasePath(collectionPath, filePath);
|
||||
expect(result).toBe('files/payload.txt');
|
||||
});
|
||||
|
||||
it('should treat both relative paths as cwd-relative for containment checks', () => {
|
||||
const collectionPath = 'collections/api';
|
||||
const filePath = 'collections/api/files/payload.txt';
|
||||
const result = getRelativePathWithinBasePath(collectionPath, filePath);
|
||||
expect(result).toBe('files/payload.txt');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should handle very long paths', () => {
|
||||
const longPath = '/users/john/projects/' + 'a'.repeat(100);
|
||||
|
||||
@@ -5,7 +5,7 @@ jest.mock('platform', () => ({
|
||||
}
|
||||
}));
|
||||
|
||||
import { getRelativePath, getBasename, getAbsoluteFilePath } from './path';
|
||||
import { getRelativePath, getBasename, getAbsoluteFilePath, getRelativePathWithinBasePath } from './path';
|
||||
|
||||
describe('Path Utilities - Windows Platform', () => {
|
||||
describe('getRelativePath', () => {
|
||||
@@ -25,6 +25,14 @@ describe('Path Utilities - Windows Platform', () => {
|
||||
expect(getRelativePath('C:\\Users\\John\\Projects', 'C:\\Users\\John\\Projects\\src\\components', false)).toBe('src\\components');
|
||||
});
|
||||
|
||||
it('should return ".." for direct parent directory', () => {
|
||||
expect(getRelativePath('C:\\Users\\John\\Projects', 'C:\\Users\\John', false)).toBe('..');
|
||||
});
|
||||
|
||||
it('should return an absolute path for cross-drive targets', () => {
|
||||
expect(getRelativePath('C:\\Users\\John\\Projects', 'D:\\payload.txt', false)).toBe('D:\\payload.txt');
|
||||
});
|
||||
|
||||
describe('with posixify enabled', () => {
|
||||
it('should convert backslashes to forward slashes', () => {
|
||||
expect(getRelativePath('C:\\Users\\John\\Projects', 'C:\\Users\\John\\Projects\\App')).toBe('App');
|
||||
@@ -181,6 +189,113 @@ describe('Path Utilities - Windows Platform', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRelativePathWithinBasePath', () => {
|
||||
it('should store in-collection files as Windows relative paths with mixed separators', () => {
|
||||
const result = getRelativePathWithinBasePath('C:/Users/John/Collections/Api', 'C:\\Users\\John\\Collections\\Api\\files\\payload.txt');
|
||||
expect(result).toBe('files\\payload.txt');
|
||||
});
|
||||
|
||||
it('should store nested in-collection files as Windows relative paths', () => {
|
||||
const result = getRelativePathWithinBasePath('C:\\Users\\John\\Collections\\Api', 'C:\\Users\\John\\Collections\\Api\\folder\\payload.txt');
|
||||
expect(result).toBe('folder\\payload.txt');
|
||||
});
|
||||
|
||||
it('should handle collection paths with trailing separators', () => {
|
||||
const result = getRelativePathWithinBasePath('C:\\Users\\John\\Collections\\Api\\', 'C:\\Users\\John\\Collections\\Api\\folder\\payload.txt');
|
||||
expect(result).toBe('folder\\payload.txt');
|
||||
});
|
||||
|
||||
it('should handle case differences in Windows drive paths', () => {
|
||||
const result = getRelativePathWithinBasePath('c:\\users\\john\\collections\\api', 'C:\\Users\\John\\Collections\\Api\\folder\\payload.txt');
|
||||
expect(result).toBe('folder\\payload.txt');
|
||||
});
|
||||
|
||||
it('should resolve dot segments before deciding whether a file is inside the collection', () => {
|
||||
const result = getRelativePathWithinBasePath('C:\\Users\\John\\Collections\\Api', 'C:\\Users\\John\\Collections\\Api\\folder\\..\\payload.txt');
|
||||
expect(result).toBe('payload.txt');
|
||||
});
|
||||
|
||||
it('should keep paths that resolve outside the collection absolute', () => {
|
||||
const filePath = 'C:\\Users\\John\\Collections\\Api\\..\\payload.txt';
|
||||
const result = getRelativePathWithinBasePath('C:\\Users\\John\\Collections\\Api', filePath);
|
||||
expect(result).toBe(filePath);
|
||||
});
|
||||
|
||||
it('should keep sibling prefix paths absolute', () => {
|
||||
const filePath = 'C:\\Users\\John\\Collections\\ApiOther\\payload.txt';
|
||||
const result = getRelativePathWithinBasePath('C:\\Users\\John\\Collections\\Api', filePath);
|
||||
expect(result).toBe(filePath);
|
||||
});
|
||||
|
||||
it('should keep outside collection paths absolute', () => {
|
||||
const filePath = 'C:\\Users\\John\\Downloads\\payload.txt';
|
||||
const result = getRelativePathWithinBasePath('C:\\Users\\John\\Collections\\Api', filePath);
|
||||
expect(result).toBe(filePath);
|
||||
});
|
||||
|
||||
it('should keep cross-drive paths absolute', () => {
|
||||
const filePath = 'D:\\payload.txt';
|
||||
const result = getRelativePathWithinBasePath('C:\\Users\\John\\Collections\\Api', filePath);
|
||||
expect(result).toBe(filePath);
|
||||
});
|
||||
|
||||
it('should keep same-path values unchanged', () => {
|
||||
const filePath = 'C:\\Users\\John\\Collections\\Api';
|
||||
const result = getRelativePathWithinBasePath('C:\\Users\\John\\Collections\\Api', filePath);
|
||||
expect(result).toBe(filePath);
|
||||
});
|
||||
|
||||
it('should keep the original file path when inputs are missing', () => {
|
||||
expect(getRelativePathWithinBasePath('', 'C:\\Users\\John\\Downloads\\payload.txt')).toBe('C:\\Users\\John\\Downloads\\payload.txt');
|
||||
expect(getRelativePathWithinBasePath('C:\\Users\\John\\Collections\\Api', '')).toBe('');
|
||||
});
|
||||
|
||||
describe('mixed separators (posix base / win file)', () => {
|
||||
it('inside → relative path with native separators (default)', () => {
|
||||
const r = getRelativePathWithinBasePath('C:/Users/John/Collections/Api', 'C:\\Users\\John\\Collections\\Api\\files\\payload.txt');
|
||||
expect(r).toBe('files\\payload.txt');
|
||||
});
|
||||
|
||||
it('outside → returns original filePath unchanged (default)', () => {
|
||||
const r = getRelativePathWithinBasePath('C:/Users/John/Collections/Api', 'C:\\Users\\John\\Downloads\\payload.txt');
|
||||
expect(r).toBe('C:\\Users\\John\\Downloads\\payload.txt');
|
||||
});
|
||||
|
||||
it('outside → posixified absolute fallback when posixify=true', () => {
|
||||
const r = getRelativePathWithinBasePath('C:/Users/John/Collections/Api', 'C:\\Users\\John\\Downloads\\payload.txt', true);
|
||||
expect(r).toBe('C:/Users/John/Downloads/payload.txt');
|
||||
});
|
||||
|
||||
it('inside → posixified relative path when posixify=true', () => {
|
||||
const r = getRelativePathWithinBasePath('C:/Users/John/Collections/Api', 'C:\\Users\\John\\Collections\\Api\\files\\payload.txt', true);
|
||||
expect(r).toBe('files/payload.txt');
|
||||
});
|
||||
});
|
||||
|
||||
describe('mixed separators (win base / posix file)', () => {
|
||||
it('inside → relative path with native separators (default)', () => {
|
||||
const r = getRelativePathWithinBasePath('C:\\Users\\John\\Collections\\Api', 'C:/Users/John/Collections/Api/files/payload.txt');
|
||||
expect(r).toBe('files\\payload.txt');
|
||||
});
|
||||
|
||||
it('outside → returns original filePath as-is (default)', () => {
|
||||
const r = getRelativePathWithinBasePath('C:\\Users\\John\\Collections\\Api', 'C:/Users/John/Downloads/payload.txt');
|
||||
// filePath uses '/', returned as-is since shouldPosixify=false
|
||||
expect(r).toBe('C:/Users/John/Downloads/payload.txt');
|
||||
});
|
||||
|
||||
it('outside → posixified fallback when posixify=true', () => {
|
||||
const r = getRelativePathWithinBasePath('C:\\Users\\John\\Collections\\Api', 'C:/Users/John/Downloads/payload.txt', true);
|
||||
expect(r).toBe('C:/Users/John/Downloads/payload.txt');
|
||||
});
|
||||
|
||||
it('inside → posixified relative path when posixify=true', () => {
|
||||
const r = getRelativePathWithinBasePath('C:\\Users\\John\\Collections\\Api', 'C:/Users/John/Collections/Api/files/payload.txt', true);
|
||||
expect(r).toBe('files/payload.txt');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Cross-platform path handling', () => {
|
||||
describe('Windows fromPath with POSIX toPath', () => {
|
||||
it('should handle Windows fromPath with POSIX toPath in getAbsoluteFilePath', () => {
|
||||
|
||||
146
tests/collection/opencollection/multipart-file-path.spec.ts
Normal file
146
tests/collection/opencollection/multipart-file-path.spec.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { test, expect, closeElectronApp } from '../../../playwright';
|
||||
import {
|
||||
addMultipartFileToLastRow,
|
||||
openRequest,
|
||||
removeFirstMultipartFile,
|
||||
saveRequest,
|
||||
selectRequestBodyMode,
|
||||
selectRequestPaneTab
|
||||
} from '../../utils/page';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const collectionName = 'RelativePathBug';
|
||||
const requestName = 'upload-payload';
|
||||
const relativePayloadPath = path.join('files', 'payload.json');
|
||||
|
||||
const writeJson = async (filePath: string, value: unknown) => {
|
||||
await fs.promises.writeFile(filePath, JSON.stringify(value, null, 2), 'utf-8');
|
||||
};
|
||||
|
||||
const setupOpenCollection = async (collectionDir: string, userDataDir: string) => {
|
||||
await fs.promises.mkdir(path.join(collectionDir, 'files'), { recursive: true });
|
||||
await fs.promises.mkdir(userDataDir, { recursive: true });
|
||||
|
||||
await fs.promises.writeFile(
|
||||
path.join(collectionDir, 'opencollection.yml'),
|
||||
[
|
||||
'opencollection: "1.0.0"',
|
||||
'info:',
|
||||
` name: ${collectionName}`,
|
||||
' type: collection',
|
||||
''
|
||||
].join('\n'),
|
||||
'utf-8'
|
||||
);
|
||||
|
||||
await fs.promises.writeFile(
|
||||
path.join(collectionDir, relativePayloadPath),
|
||||
'{"ok":true}\n',
|
||||
'utf-8'
|
||||
);
|
||||
|
||||
await fs.promises.writeFile(
|
||||
path.join(collectionDir, `${requestName}.yml`),
|
||||
[
|
||||
'info:',
|
||||
` name: ${requestName}`,
|
||||
' type: http',
|
||||
' seq: 1',
|
||||
'',
|
||||
'http:',
|
||||
' method: POST',
|
||||
' url: https://example.com/upload',
|
||||
'',
|
||||
'settings:',
|
||||
' encodeUrl: true',
|
||||
' timeout: 0',
|
||||
' followRedirects: true',
|
||||
' maxRedirects: 5',
|
||||
''
|
||||
].join('\n'),
|
||||
'utf-8'
|
||||
);
|
||||
|
||||
await writeJson(path.join(userDataDir, 'preferences.json'), {
|
||||
lastOpenedCollections: [collectionDir],
|
||||
preferences: {
|
||||
onboarding: {
|
||||
hasLaunchedBefore: true,
|
||||
hasSeenWelcomeModal: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await writeJson(path.join(userDataDir, 'collection-security.json'), {
|
||||
collections: [
|
||||
{
|
||||
path: collectionDir,
|
||||
securityConfig: {
|
||||
jsSandboxMode: 'safe'
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
const expectRequestFileToContainRelativePayload = async (requestFilePath: string, payloadPath: string) => {
|
||||
await expect.poll(async () => fs.existsSync(requestFilePath)).toBe(true);
|
||||
await expect.poll(async () => fs.promises.readFile(requestFilePath, 'utf-8')).toContain(` ${relativePayloadPath}\n`);
|
||||
await expect.poll(async () => fs.promises.readFile(requestFilePath, 'utf-8')).not.toContain(payloadPath);
|
||||
};
|
||||
|
||||
const expectRequestFileNotToContainPayload = async (requestFilePath: string, payloadPath: string) => {
|
||||
await expect.poll(async () => fs.promises.readFile(requestFilePath, 'utf-8')).not.toContain(` ${relativePayloadPath}\n`);
|
||||
await expect.poll(async () => fs.promises.readFile(requestFilePath, 'utf-8')).not.toContain(payloadPath);
|
||||
};
|
||||
|
||||
test.describe('OpenCollection multipart file paths', () => {
|
||||
test('keeps an in-collection multipart file relative after restart, OpenCollection edit, remove, and re-add', async ({
|
||||
launchElectronApp,
|
||||
createTmpDir
|
||||
}) => {
|
||||
const collectionDir = path.join(await createTmpDir('opencollection-multipart'), collectionName);
|
||||
const userDataDir = await createTmpDir('opencollection-multipart-userdata');
|
||||
const payloadPath = path.join(collectionDir, relativePayloadPath);
|
||||
const requestFilePath = path.join(collectionDir, `${requestName}.yml`);
|
||||
|
||||
await setupOpenCollection(collectionDir, userDataDir);
|
||||
|
||||
let electronApp = await launchElectronApp({ userDataPath: userDataDir });
|
||||
let page = await electronApp.firstWindow();
|
||||
await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
|
||||
|
||||
await expect(page.locator('#sidebar-collection-name').filter({ hasText: collectionName })).toBeVisible();
|
||||
await expect.poll(async () => fs.existsSync(requestFilePath), {
|
||||
timeout: 15000
|
||||
}).toBe(true);
|
||||
|
||||
await openRequest(page, collectionName, requestName, { persist: true });
|
||||
await selectRequestBodyMode(page, 'Multipart Form');
|
||||
|
||||
await addMultipartFileToLastRow(page, electronApp, payloadPath);
|
||||
await saveRequest(page);
|
||||
await expectRequestFileToContainRelativePayload(requestFilePath, payloadPath);
|
||||
|
||||
await closeElectronApp(electronApp);
|
||||
await fs.promises.appendFile(path.join(collectionDir, 'opencollection.yml'), '\n\n', 'utf-8');
|
||||
|
||||
electronApp = await launchElectronApp({ userDataPath: userDataDir });
|
||||
page = await electronApp.firstWindow();
|
||||
await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
|
||||
await expect(page.locator('#sidebar-collection-name').filter({ hasText: collectionName })).toBeVisible();
|
||||
|
||||
await openRequest(page, collectionName, requestName, { persist: true });
|
||||
await selectRequestPaneTab(page, 'Body');
|
||||
await removeFirstMultipartFile(page);
|
||||
await saveRequest(page);
|
||||
await expectRequestFileNotToContainPayload(requestFilePath, payloadPath);
|
||||
|
||||
await addMultipartFileToLastRow(page, electronApp, payloadPath);
|
||||
await saveRequest(page);
|
||||
|
||||
await expectRequestFileToContainRelativePayload(requestFilePath, payloadPath);
|
||||
await closeElectronApp(electronApp);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { test, expect, Page, ElectronApplication, waitForReadyPage as waitForReadyPageImpl } from '../../../playwright';
|
||||
import process from 'node:process';
|
||||
import * as path from 'path';
|
||||
import { buildCommonLocators, buildScriptErrorLocators } from './locators';
|
||||
|
||||
type SandboxMode = 'safe' | 'developer';
|
||||
@@ -1009,6 +1010,50 @@ const selectRequestPaneTab = async (page: Page, tabName: string) => {
|
||||
await selectPaneTab(page, '[data-testid="request-pane"] > .px-4', tabName);
|
||||
};
|
||||
|
||||
const selectRequestBodyMode = async (page: Page, mode: string) => {
|
||||
await test.step(`Select request body mode "${mode}"`, async () => {
|
||||
await selectRequestPaneTab(page, 'Body');
|
||||
const locators = buildCommonLocators(page);
|
||||
await locators.request.bodyModeSelector().click();
|
||||
await locators.dropdown.item(mode).click();
|
||||
});
|
||||
};
|
||||
|
||||
const mockBrowseFiles = async (electronApp: ElectronApplication, filePaths: string[]) => {
|
||||
await electronApp.evaluate(({ dialog }, selectedPaths: string[]) => {
|
||||
const originalShowOpenDialog = dialog.showOpenDialog;
|
||||
dialog.showOpenDialog = async (...args) => {
|
||||
dialog.showOpenDialog = originalShowOpenDialog;
|
||||
return {
|
||||
canceled: false,
|
||||
filePaths: selectedPaths
|
||||
};
|
||||
};
|
||||
}, filePaths);
|
||||
};
|
||||
|
||||
const addMultipartFileToLastRow = async (page: Page, electronApp: ElectronApplication, filePath: string) => {
|
||||
await test.step(`Add multipart file "${path.basename(filePath)}"`, async () => {
|
||||
await mockBrowseFiles(electronApp, [filePath]);
|
||||
|
||||
const table = buildCommonLocators(page).table('editable-table');
|
||||
const lastRow = table.allRows().last();
|
||||
|
||||
await expect(lastRow.locator('.upload-btn')).toBeVisible();
|
||||
await lastRow.locator('.upload-btn').click();
|
||||
await expect(lastRow.locator('.file-value-cell')).toContainText(path.basename(filePath));
|
||||
});
|
||||
};
|
||||
|
||||
const removeFirstMultipartFile = async (page: Page) => {
|
||||
await test.step('Remove first multipart file', async () => {
|
||||
const table = buildCommonLocators(page).table('editable-table');
|
||||
await expect(table.allRows().locator('.file-value-cell').first()).toBeVisible();
|
||||
await table.allRows().first().locator('.clear-file-btn').click();
|
||||
await expect(table.allRows().first().locator('.upload-btn')).toBeVisible();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Verify response contains specific text
|
||||
* @param page - The page object
|
||||
@@ -1472,7 +1517,11 @@ export {
|
||||
getResponseBody,
|
||||
expectResponseContains,
|
||||
selectRequestPaneTab,
|
||||
selectRequestBodyMode,
|
||||
selectResponsePaneTab,
|
||||
mockBrowseFiles,
|
||||
addMultipartFileToLastRow,
|
||||
removeFirstMultipartFile,
|
||||
sendRequestAndWaitForResponse,
|
||||
switchResponseFormat,
|
||||
switchToPreviewTab,
|
||||
|
||||
Reference in New Issue
Block a user