Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const StyledWrapper = styled.div`
background: transparent;
border: none;
cursor: pointer;
border-radius: 4px;
border-radius: ${(props) => props.theme.border.radius.sm};
transition: color 0.15s ease;
font-size: 12px;
white-space: nowrap;
Expand Down Expand Up @@ -76,6 +76,29 @@ const StyledWrapper = styled.div`
margin-right: 4px;
}

&.has-warning {
background-color: ${(props) => props.theme.status.warning.background};
font-weight: 500;
border-radius: ${(props) => props.theme.border.radius.sm};
padding: 4px;
}

.warning-icon {
color: ${(props) => props.theme.status.warning.text};
margin-right: 4px;
}

.warning-tooltip {
display: flex;
align-items: center;
gap: 6px;

svg {
color: ${(props) => props.theme.status.warning.text};
flex-shrink: 0;
}
}

.file-name {
flex: 1;
font-size: 12px;
Expand All @@ -96,7 +119,7 @@ const StyledWrapper = styled.div`
background: transparent;
border: none;
cursor: pointer;
border-radius: 4px;
border-radius: ${(props) => props.theme.border.radius.sm};
transition: color 0.15s ease;
flex-shrink: 0;

Expand Down
51 changes: 37 additions & 14 deletions packages/bruno-app/src/components/FilePickerEditor/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import React from 'react';
import { getRelativePathWithinBasePath } from 'utils/common/path';
import React, { useId } from 'react';
import { getRelativePathWithinBasePath, getBasename } from 'utils/common/path';
import { useDispatch } from 'react-redux';
import { browseFiles } from 'providers/ReduxStore/slices/collections/actions';
import { IconX, IconUpload, IconFile } from '@tabler/icons';
import { isWindowsOS } from 'utils/common/platform';
import { Tooltip } from 'react-tooltip';
import IconAlertTriangleFilled from 'components/Icons/IconAlertTriangleFilled';
import useMissingFileCheck from 'hooks/useMissingFileCheck';
import StyledWrapper from './StyledWrapper';

/**
Expand All @@ -30,15 +32,17 @@ const FilePickerEditor = ({
icon: CustomIcon
}) => {
const dispatch = useDispatch();
const filenames = (isSingleFilePicker ? [value] : value || [])
.filter((v) => v != null && v != '')
.map((v) => {
const separator = isWindowsOS() ? '\\' : '/';
return v.split(separator).pop();
});
const warningTooltipId = `file-picker-warning-${useId().replace(/:/g, '')}`;

const filePaths = (isSingleFilePicker ? [value] : value || []).filter((v) => v != null && v != '');
const filenames = filePaths.map((v) => getBasename(collection.pathname, v));
const { status, missingPaths } = useMissingFileCheck(filePaths, collection?.pathname);
const hasMissingFiles = status === 'ready' && missingPaths.length > 0;

// title is shown when hovering over the button
const title = filenames.map((v) => `- ${v}`).join('\n');
const title = filenames.length > 1
? filenames.map((v) => `- ${v}`).join('\n')
: filenames[0] ?? '';

const browse = () => {
if (readOnly) return;
Expand Down Expand Up @@ -97,12 +101,19 @@ const FilePickerEditor = ({
return (
<StyledWrapper>
<div
className={`file-picker-selected ${readOnly ? 'read-only' : ''}`}
title={title}
className={`file-picker-selected ${readOnly ? 'read-only' : ''} ${hasMissingFiles ? 'has-warning' : ''}`}
onClick={!readOnly ? browse : undefined}
>
<IconFile size={16} className="file-icon" />
<span className="file-name">
{hasMissingFiles ? (
<IconAlertTriangleFilled
data-tooltip-id={warningTooltipId}
className="warning-icon"
size={16}
/>
Comment on lines +107 to +112

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make the warning discoverable without hover.

The warning is attached to a plain, non-focusable SVG with a mouse-triggered tooltip, so keyboard and screen-reader users cannot reliably discover why the file is highlighted. Use a focusable semantic trigger or provide equivalent accessible text/ARIA.

Also applies to: 128-138

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/bruno-app/src/components/FilePickerEditor/index.js` around lines 106
- 111, Update the warning icon rendering in FilePickerEditor so the missing-file
warning is discoverable without hover: wrap the icon in a focusable semantic
trigger or add equivalent accessible text/ARIA, while preserving the existing
warningTooltipId tooltip behavior. Apply the same accessibility treatment to the
related warning rendering at the additional location.

) : (
<IconFile size={16} className="file-icon" />
)}
<span className="file-name" title={title}>
{renderButtonText(filenames)}
</span>
{!readOnly && (
Expand All @@ -115,6 +126,18 @@ const FilePickerEditor = ({
<IconX size={16} />
</button>
)}
{hasMissingFiles && (
<Tooltip
id={warningTooltipId}
className="tooltip-mod max-w-lg"
place="bottom-end"
>
<div className="warning-tooltip" data-testid="file-picker-warning-tooltip">
<IconAlertTriangleFilled size={14} />
<span>The file above is not in the given directory, please upload it again.</span>
</div>
</Tooltip>
)}
</div>
</StyledWrapper>
);
Expand Down
64 changes: 64 additions & 0 deletions packages/bruno-app/src/hooks/useMissingFileCheck/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useEffect, useRef, useState } from 'react';
import { getAbsoluteFilePath } from 'utils/common/path';
import { existsSync } from 'utils/filesystem';

/**
* Checks whether the given file paths (resolved against `basePath`) exist on disk.
*
* The hook only reports what it observed — callers own the "should I warn" policy.
* A sequence counter discards results from superseded runs so rapid input changes
* can't leave stale results on screen.
*
* @param {string[]} paths - Relative or absolute file paths to check.
* @param {string} basePath - Base directory that relative paths are resolved against.
* When falsy, the hook stays in the 'idle' state without issuing any check.
* @returns {{ status: 'idle' | 'checking' | 'ready' | 'error', missingPaths: string[] }}
* - `'idle'` — nothing to check (empty input or no `basePath`)
* - `'checking'` — a check is in flight; `missingPaths` is empty
* - `'ready'` — check completed; `missingPaths` lists paths that don't exist
* - `'error'` — the check itself failed (e.g. IPC threw); `missingPaths` is empty
*/
const useMissingFileCheck = (paths, basePath) => {
const [state, setState] = useState({ status: 'idle', missingPaths: [] });
const seqRef = useRef(0);
const pathsRef = useRef(paths);
pathsRef.current = paths;

// Content-based key so re-renders that pass a fresh array with the same
// contents don't retrigger the check.
const pathsKey = paths.join('\0');

useEffect(() => {
const currentPaths = pathsRef.current;
if (!currentPaths.length || !basePath) {
setState({ status: 'idle', missingPaths: [] });
return;
}

const seq = ++seqRef.current;
setState({ status: 'checking', missingPaths: [] });
Comment on lines +31 to +39

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Invalidate in-flight checks when entering idle.

When paths becomes empty or basePath becomes falsy, the effect sets idle without advancing seqRef. A prior Promise.all can then resolve with the same sequence and overwrite the idle state with stale ready/missingPaths, allowing an obsolete warning to reappear. Increment the sequence before the early return.

Proposed fix
 useEffect(() => {
+  const seq = ++seqRef.current;
   const currentPaths = pathsRef.current;
   if (!currentPaths.length || !basePath) {
     setState({ status: 'idle', missingPaths: [] });
     return;
   }

-  const seq = ++seqRef.current;
   setState({ status: 'checking', missingPaths: [] });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
const currentPaths = pathsRef.current;
if (!currentPaths.length || !basePath) {
setState({ status: 'idle', missingPaths: [] });
return;
}
const seq = ++seqRef.current;
setState({ status: 'checking', missingPaths: [] });
useEffect(() => {
const seq = ++seqRef.current;
const currentPaths = pathsRef.current;
if (!currentPaths.length || !basePath) {
setState({ status: 'idle', missingPaths: [] });
return;
}
setState({ status: 'checking', missingPaths: [] });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/bruno-app/src/hooks/useMissingFileCheck/index.js` around lines 31 -
39, Update the early idle-return branch in the useEffect of useMissingFileCheck
to increment seqRef.current before returning when paths are empty or basePath is
falsy. Keep the existing idle state reset unchanged so any in-flight check is
invalidated and cannot overwrite it with stale results.


Promise.all(
currentPaths.map(async (filePath) => {
const exists = await existsSync(getAbsoluteFilePath(basePath, filePath));
return { filePath, exists };
})
).then(
(results) => {
if (seq !== seqRef.current) return;
setState({
status: 'ready',
missingPaths: results.filter((r) => !r.exists).map((r) => r.filePath)
});
},
() => {
if (seq !== seqRef.current) return;
setState({ status: 'error', missingPaths: [] });
}
);
}, [pathsKey, basePath]);

return state;
};

export default useMissingFileCheck;
57 changes: 57 additions & 0 deletions tests/request/binary-file/binary-file-missing-warning.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import * as path from 'path';
import { test, expect } from '../../../playwright';
import { closeAllCollections, mockBrowseFiles, openCollection, selectRequestPaneTab } from '../../utils/page';
import { buildCommonLocators } from '../../utils/page/locators';

const MISSING_FILE_WARNING = 'The file above is not in the given directory, please upload it again.';

test.describe('File / Binary body missing-file warning', () => {
test.afterAll(async ({ page }) => {
await closeAllCollections(page);
});

test('should show warning for missing binary file', async ({ page, electronApp, createTmpDir }) => {
const collectionFile = path.resolve(__dirname, 'fixtures', 'binary-file-missing-warning.yml');
const locators = buildCommonLocators(page);

const importDir = await createTmpDir('imported-collection');

await mockBrowseFiles(electronApp, [importDir]);

await test.step('Import collection', async () => {
await locators.plusMenu.button().click();
await locators.plusMenu.importCollection().click();
const importModal = locators.import.modal();
await importModal.waitFor({ state: 'visible' });
await locators.import.fileInput().setInputFiles(collectionFile);
await locators.import.locationModal().waitFor({ state: 'visible', timeout: 5000 });
const locationModal = locators.import.locationModal();
await expect(locationModal.getByText('Binary body type')).toBeVisible();
await locators.import.browseLink(locationModal).click();
await locators.import.importButton(locationModal).click();
await locationModal.waitFor({ state: 'hidden' });
await expect(locators.sidebar.collection('Binary body type')).toBeVisible();
await openCollection(page, 'Binary body type');
});

await test.step('shows a warning when the binary file does not exist on disk', async () => {
await locators.sidebar.request('Binary body - missing file').click();
await expect(locators.request.pane()).toBeVisible();
await selectRequestPaneTab(page, 'Body');

const filePicker = page.locator('.file-picker-selected').first();
await expect(filePicker).toBeVisible({ timeout: 5000 });
await expect(filePicker.locator('.file-name')).toHaveText('missing-payload.bin');

// Warning: the missing-file styling, icon and tooltip must be present.
await expect(filePicker).toHaveClass(/has-warning/);
const warningIcon = filePicker.locator('.warning-icon');
await expect(warningIcon).toBeVisible();

await warningIcon.hover();
const tooltip = locators.filePicker.warningTooltip();
await expect(tooltip).toBeVisible();
await expect(tooltip).toContainText(MISSING_FILE_WARNING);
Comment on lines +42 to +54

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Move these selectors into the page locator module.

This spec directly uses page.locator(...) for .file-picker-selected, .file-name, and .warning-icon. Add selected, fileName, and warningIcon helpers under locators.filePicker and consume them here.

As per path instructions, specs must not inline raw selectors; selectors belong in tests/utils/page/* page modules.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/request/binary-file/binary-file-missing-warning.spec.ts` around lines
42 - 54, The binary-file warning spec should stop using inline page.locator
selectors. Add selected, fileName, and warningIcon helpers under
locators.filePicker in the page locator module, then update the spec to use
those helpers while preserving the existing visibility, text, class, hover, and
tooltip assertions.

Source: Path instructions

});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
opencollection: 1.0.0
info:
name: Binary body type
config:
proxy:
inherit: true
config:
protocol: http
hostname: ''
port: ''
auth:
username: ''
password: ''
bypassProxy: ''
items:
- info:
name: Binary body - missing file
type: http
seq: 2
http:
method: POST
url: https://api.com/users
body:
type: file
data:
- filePath: ./missing-payload.bin
contentType: ''
selected: true
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5
bundled: true
extensions:
bruno:
ignore:
- node_modules
- .git
exportedUsing: Bruno
3 changes: 3 additions & 0 deletions tests/utils/page/locators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ export const buildCommonLocators = (page: Page) => ({
},
pane: () => page.getByTestId('request-pane')
},
filePicker: {
warningTooltip: () => page.getByTestId('file-picker-warning-tooltip')
},
// The variable-info popup shown when hovering a `{{var}}` token in an editor.
varInfoPopup: {
all: () => page.getByTestId('var-info-popup'),
Expand Down
Loading