Files
bruno/tests/request/multipart-form/multipart-form-file-chips.spec.ts
Sid 0bba66a590 feat: variable descriptions (#8269)
* feat: add description column to various tables and enhance UI interactions
- Introduced a description column in Headers, VarsTable, and other components to allow multi-line descriptions.
- Added toggle buttons to show/hide the description column in relevant tables.
- Updated EditableTable component to support dynamic row addition with customizable labels.
- Enhanced UI for better user experience with consistent button styles and spacing adjustments.

* chore: resize fix handles

* fix: restrict column check for last index

* refactor: remove unwanted style and fix bottom padding of the last child in the table row

* tests: improve testability and fix impacted locators

* tests: fix locators for value fields

* fix: improve newline handling in description prefix

* fix: ts changes

* chore: remove redundant code and fix description roundtrip

* chore: remove redundant check

* tests(bru-lang): tests for description and annotation

* test: improve locators and tests

* tests: descriptions

* chore: re-add description setup

* fix: re-add description cols

* chore: fix the double click reset

* fix: account for hidden sidebar in request pane width calculation

* refactor: ui/ux fixes for data type toggle

* chore: inline common deps into bruno-lang and bruno-schema

* chore: tests

* chore: fix ux for descriptions

* fix: layout for environment vars table

* fix: ensure correct row selection for multipart file upload in tests

* feat: enhance UID assignment for request headers and variables in collections

* chore: simplify

* chore: update pkg

* chore: abstract the e2e utils

* chore: fix exports

* tests: fix locators for datatype vars

* fix: ux issue with secrets in environment table

* tests: update locators for collection headers and vars descriptions

* tests: update locators to use data attributes for datatype selectors

* chore: update default css width for actions column

* chore: remove tooltip for string type warnings

* fix: reduce name widths

* refactor: update annotation serialization to use single-quote delimiters for descriptions

* refactor(tests): simplify description formatting in jsonToEnv tests

* feat: add multiline description escaping and unescaping functions with tests

* refactor: clean up imports and improve multiline text block handling

* refactor: update locators to use new test IDs for environment variable editors

* refactor: streamline header input handling and improve environment variable row access

* refactor: update e2e-test job to support self-hosted runners

* refactor: update E2E test runner configuration to include e2e and macOS environments

---------

Co-authored-by: Pragadesh-45 <temporaryg7904@gmail.com>
2026-07-07 21:43:39 +05:30

164 lines
6.4 KiB
TypeScript

import { test, expect } from '../../../playwright';
import {
closeAllCollections,
createCollection,
createRequest,
openCollection,
openRequest,
saveRequest,
selectRequestPaneTab
} from '../../utils/page';
import { buildCommonLocators } from '../../utils/page/locators';
import type { ElectronApplication, Page } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
test.describe('Multipart Form - Multiple File Upload', () => {
let tmpDir: string;
let fileA: string;
let fileB: string;
let fileC: string;
test.beforeAll(async ({ page, electronApp, createTmpDir }) => {
tmpDir = await createTmpDir('multipart-multi-upload');
fileA = path.join(tmpDir, 'alpha.txt');
fileB = path.join(tmpDir, 'beta.txt');
fileC = path.join(tmpDir, 'gamma.txt');
await fs.promises.writeFile(fileA, 'a');
await fs.promises.writeFile(fileB, 'b');
await fs.promises.writeFile(fileC, 'c');
// Maximize the window so the value column is wide enough to render chips
await electronApp.evaluate(({ BrowserWindow }) => {
BrowserWindow.getAllWindows()[0]?.maximize();
});
await electronApp.evaluate(({ dialog }) => {
(dialog as any).__originalShowOpenDialog = dialog.showOpenDialog;
(global as any).__mockFilePaths = [];
dialog.showOpenDialog = async () => ({
canceled: false,
filePaths: (global as any).__mockFilePaths || []
});
});
await createCollection(page, 'multipart-multi-upload', tmpDir);
await createRequest(page, 'test-multi-upload', '', {
url: 'https://testbench-sanity.usebruno.com/api/echo/json',
method: 'POST',
inFolder: false
});
await openCollection(page, 'multipart-multi-upload');
await openRequest(page, 'multipart-multi-upload', 'test-multi-upload', { persist: true });
await selectRequestPaneTab(page, 'Body');
await buildCommonLocators(page).request.bodyModeSelector().click();
await page.locator('.dropdown-item').filter({ hasText: 'Multipart Form' }).click();
});
test.afterAll(async ({ page, electronApp }) => {
await electronApp.evaluate(({ dialog }) => {
if ((dialog as any).__originalShowOpenDialog) {
dialog.showOpenDialog = (dialog as any).__originalShowOpenDialog;
delete (dialog as any).__originalShowOpenDialog;
}
});
await closeAllCollections(page);
});
// Reset the form to a single empty row before each test.
test.beforeEach(async ({ page }) => {
const table = buildCommonLocators(page).table('multipart-form-table');
await expect(table.container()).toBeVisible();
let rowCount = await table.allRows().count();
while (rowCount > 1) {
await table.rowDeleteButton(rowCount - 2).click();
await expect(table.allRows()).toHaveCount(rowCount - 1);
rowCount = await table.allRows().count();
}
await saveRequest(page);
});
// Tell the mocked dialog what to return, then click the upload button on
// the empty last row.
const uploadFiles = async (page: Page, electronApp: ElectronApplication, files: string[]) => {
await electronApp.evaluate((_, paths) => {
(global as any).__mockFilePaths = paths;
}, files);
const table = buildCommonLocators(page).table('multipart-form-table');
await table.allRows().last().getByTestId('multipart-file-upload').click();
};
// Reads all file names currently associated with the row, regardless of
// whether they render as inline chips, in a `+N more` overflow dropdown, or
// as a collapsed `N files` summary. The CI Linux runner has a small display,
// so the value column often collapses into one of the overflow modes.
const readFileNames = async (page: Page): Promise<string[]> => {
const inlineChips = page.getByTestId('multipart-file-chip');
const summary = page.getByTestId('multipart-file-summary');
const more = page.getByTestId('multipart-file-more');
const inlineNames = await inlineChips.allTextContents();
const overflowTrigger = (await summary.count()) > 0 ? summary : (await more.count()) > 0 ? more : null;
if (!overflowTrigger) {
return inlineNames;
}
await overflowTrigger.click();
const overflowRows = page.getByTestId('multipart-file-overflow-row');
await expect(overflowRows.first()).toBeVisible();
const overflowNames = await overflowRows.allTextContents();
// Close the popover by clicking the trigger again (Tippy click-toggle).
await overflowTrigger.click();
await expect(overflowRows.first()).toBeHidden();
// In summary mode all files are in the dropdown; in `+N more` mode the
// inline chips plus the dropdown rows together cover the full list.
return (await summary.count()) > 0 ? overflowNames : [...inlineNames, ...overflowNames];
};
// Removes a single file by name, handling inline-chip and overflow-row paths.
const removeFileByName = async (page: Page, fileName: string) => {
const inlineChip = page.getByTestId('multipart-file-chip').filter({ hasText: fileName });
if ((await inlineChip.count()) > 0) {
await inlineChip.getByTestId('multipart-file-chip-remove').click();
await expect(inlineChip).toHaveCount(0);
return;
}
const summary = page.getByTestId('multipart-file-summary');
const more = page.getByTestId('multipart-file-more');
const trigger = (await summary.count()) > 0 ? summary : more;
await trigger.click();
const row = page.getByTestId('multipart-file-overflow-row').filter({ hasText: fileName });
await expect(row).toBeVisible();
await row.getByTestId('multipart-file-overflow-remove').click();
await expect(row).toHaveCount(0);
// Close the popover if it's still open (it may have auto-closed when its
// last row disappeared).
if ((await trigger.count()) > 0 && (await page.getByTestId('multipart-file-overflow-row').first().isVisible().catch(() => false))) {
await trigger.click();
}
};
test('uploading multiple files registers one entry per file', async ({ page, electronApp }) => {
await uploadFiles(page, electronApp, [fileA, fileB, fileC]);
const names = await readFileNames(page);
expect(names).toEqual(['alpha.txt', 'beta.txt', 'gamma.txt']);
});
test('each file can be removed individually', async ({ page, electronApp }) => {
await uploadFiles(page, electronApp, [fileA, fileB, fileC]);
await removeFileByName(page, 'beta.txt');
const names = await readFileNames(page);
expect(names).toEqual(['alpha.txt', 'gamma.txt']);
});
});