fix: unable to add assertions to a request (#6435)

* fix: add assertion

* rm: unnecessary wait fn

* fix: test

* fix: tests

* fix: review comments

* fix: review

* fix: review comments

* fix: review comments

* fix: test failure

* review fixes

* fix: rm sandbox accept

* fix: indentation
This commit is contained in:
sanish chirayath
2025-12-18 19:37:33 +05:30
committed by GitHub
parent 6ab8fcb710
commit b188a9e9a9
11 changed files with 546 additions and 10 deletions

View File

@@ -16,7 +16,8 @@ const EditableTable = ({
checkboxKey = 'enabled',
reorderable = false,
onReorder,
showAddRow = true
showAddRow = true,
testId = 'editable-table'
}) => {
const tableRef = useRef(null);
const emptyRowUidRef = useRef(null);
@@ -224,7 +225,7 @@ const EditableTable = ({
return (
<StyledWrapper className={showCheckbox ? 'has-checkbox' : 'no-checkbox'}>
<div className="table-container" ref={tableRef}>
<div className="table-container" ref={tableRef} data-testid={testId}>
<table>
<thead>
<tr>
@@ -285,6 +286,7 @@ const EditableTable = ({
<input
type="checkbox"
className="mousetrap"
data-testid="column-checkbox"
checked={row[checkboxKey] ?? true}
onChange={(e) => handleCheckboxChange(row.uid, e.target.checked)}
/>
@@ -292,14 +294,17 @@ const EditableTable = ({
</td>
)}
{columns.map((column) => (
<td key={column.key}>
<td key={column.key} data-testid={`column-${column.key}`}>
{renderCell(column, row, rowIndex)}
</td>
))}
{showDelete && (
<td>
{!isEmpty && (
<button onClick={() => handleRemoveRow(row.uid)}>
<button
data-testid="column-delete"
onClick={() => handleRemoveRow(row.uid)}
>
<IconTrash strokeWidth={1.5} size={18} />
</button>
)}

View File

@@ -81,7 +81,7 @@ const AssertionOperator = ({ operator, onChange }) => {
};
return (
<select value={operator} onChange={handleChange} className="mousetrap">
<select value={operator} onChange={handleChange} className="mousetrap" data-testid="assertion-operator-select">
{operators.map((operator) => (
<option key={operator} value={operator}>
{getLabel(operator)}

View File

@@ -163,6 +163,7 @@ const Assertions = ({ item, collection }) => {
defaultRow={defaultRow}
reorderable={true}
onReorder={handleAssertionDrag}
testId="assertions-table"
/>
</StyledWrapper>
);

View File

@@ -33,6 +33,46 @@ const keyValueSchema = Yup.object({
.noUnknown(true)
.strict();
const assertionOperators = [
'eq',
'neq',
'gt',
'gte',
'lt',
'lte',
'in',
'notIn',
'contains',
'notContains',
'length',
'matches',
'notMatches',
'startsWith',
'endsWith',
'between',
'isEmpty',
'isNotEmpty',
'isNull',
'isUndefined',
'isDefined',
'isTruthy',
'isFalsy',
'isJson',
'isNumber',
'isString',
'isBoolean',
'isArray'
];
const assertionSchema = keyValueSchema.shape({
operator: Yup.string()
.oneOf(assertionOperators)
.nullable()
.optional()
})
.noUnknown(true)
.strict();
const varsSchema = Yup.object({
uid: uidSchema,
name: Yup.string().nullable(),
@@ -372,7 +412,7 @@ const requestSchema = Yup.object({
.noUnknown(true)
.strict()
.nullable(),
assertions: Yup.array().of(keyValueSchema).nullable(),
assertions: Yup.array().of(assertionSchema).nullable(),
tests: Yup.string().nullable(),
docs: Yup.string().nullable()
})
@@ -408,7 +448,7 @@ const grpcRequestSchema = Yup.object({
.noUnknown(true)
.strict()
.nullable(),
assertions: Yup.array().of(keyValueSchema).nullable(),
assertions: Yup.array().of(assertionSchema).nullable(),
tests: Yup.string().nullable(),
docs: Yup.string().nullable(),
})
@@ -446,7 +486,7 @@ const wsRequestSchema = Yup.object({
.noUnknown(true)
.strict()
.nullable(),
assertions: Yup.array().of(keyValueSchema).nullable(),
assertions: Yup.array().of(assertionSchema).nullable(),
tests: Yup.string().nullable(),
docs: Yup.string().nullable()
})

View File

@@ -0,0 +1,279 @@
import { test, expect } from '../../playwright';
import {
closeAllCollections,
openCollection,
openRequest,
selectRequestPaneTab,
sendRequest,
selectEnvironment,
addAssertion,
editAssertion,
deleteAssertion,
saveRequest
} from '../utils/page';
import { buildCommonLocators } from '../utils/page/locators';
test.describe('Assertions - BRU Collection', () => {
test.beforeAll(async ({ pageWithUserData: page }) => {
await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 });
await test.step('Navigate to assertions tab', async () => {
await openCollection(page, 'test-assertions-bru');
await selectEnvironment(page, 'Local', 'collection');
await openRequest(page, 'test-assertions-bru', 'ping');
await selectRequestPaneTab(page, 'Assert');
await page.waitForTimeout(1000);
});
});
test.afterEach(async ({ pageWithUserData: page }) => {
const locators = buildCommonLocators(page);
const table = locators.assertionsTable();
// Ensure we're on the Assertions tab
await selectRequestPaneTab(page, 'Assert');
// Wait for table to be visible
await expect(table.container()).toBeVisible();
// Get all rows and delete assertions (skip the empty row at the end)
let rowCount = await table.allRows().count();
// Keep deleting assertions until only the empty row remains
// We delete from the end to avoid index shifting issues
while (rowCount > 1) {
const deleteButton = table.rowDeleteButton(rowCount - 2); // Second to last (skip empty row)
await expect(deleteButton).toBeVisible({ timeout: 1000 });
await deleteButton.click();
// Wait for row count to decrease after deletion
await expect(table.allRows()).toHaveCount(rowCount - 1);
rowCount = await table.allRows().count(); // Re-count rows
}
// Save the request to persist the clean state
// saveRequest already waits for the "Request saved successfully" toast internally
await saveRequest(page);
});
test.afterAll(async ({ pageWithUserData: page }) => {
await closeAllCollections(page);
});
test('should add assertion to request, verify toast, and run request successfully', async ({ pageWithUserData: page }) => {
const locators = buildCommonLocators(page);
await test.step('Add assertion to the request', async () => {
await addAssertion(page, {
expr: 'res.body',
value: 'pong'
});
});
await test.step('Save request and verify success toast', async () => {
await saveRequest(page);
});
await test.step('Send request and verify response', async () => {
await sendRequest(page, 200);
// Verify response status
await expect(locators.response.statusCode()).toContainText('200');
// Verify response body contains "pong"
await expect(locators.response.body()).toContainText('pong', { timeout: 5000 });
});
await test.step('Delete assertion and save', async () => {
// Navigate back to Assertions tab
await selectRequestPaneTab(page, 'Assert');
// Delete the assertion at row 0 (first data row)
await deleteAssertion(page, 0);
// Save the request
await saveRequest(page);
});
});
test('should add multiple assertions', async ({ pageWithUserData: page }) => {
const locators = buildCommonLocators(page);
const table = locators.assertionsTable();
await test.step('Add first assertion', async () => {
await addAssertion(page, {
expr: 'res.status',
value: '200'
});
});
await test.step('Add second assertion', async () => {
await addAssertion(page, {
expr: 'res.body',
value: 'pong'
});
});
await test.step('Add third assertion', async () => {
await addAssertion(page, {
expr: 'res.responseTime',
value: '1000'
});
});
await test.step('Verify all assertions are present', async () => {
// Check input values instead of cell text content
await expect(table.rowExprInput(0)).toHaveValue('res.status');
await expect(table.rowExprInput(1)).toHaveValue('res.body');
await expect(table.rowExprInput(2)).toHaveValue('res.responseTime');
});
await test.step('Save request', async () => {
await saveRequest(page);
});
});
test('should edit an existing assertion', async ({ pageWithUserData: page }) => {
const locators = buildCommonLocators(page);
const table = locators.assertionsTable();
await test.step('Add initial assertion', async () => {
await addAssertion(page, {
expr: 'res.body',
value: 'ping'
});
});
await test.step('Edit the assertion', async () => {
await editAssertion(page, 0, {
expr: 'res.status',
value: '200'
});
});
await test.step('Verify assertion was updated', async () => {
await expect(table.rowExprInput(0)).toHaveValue('res.status');
// The value cell might contain the operator, so we check it contains our value
const valueCell = table.rowCell('value', 0);
await expect(valueCell).toContainText('200');
});
await test.step('Save request', async () => {
await saveRequest(page);
});
});
test('should toggle assertion checkbox (enable/disable)', async ({ pageWithUserData: page }) => {
const locators = buildCommonLocators(page);
const table = locators.assertionsTable();
await test.step('Add assertion', async () => {
await addAssertion(page, {
expr: 'res.status',
value: '200'
});
});
await test.step('Verify checkbox is checked by default', async () => {
const checkbox = table.rowCheckbox(0);
await expect(checkbox).toBeChecked();
});
await test.step('Uncheck the assertion', async () => {
const checkbox = table.rowCheckbox(0);
await checkbox.uncheck();
});
await test.step('Verify checkbox is unchecked', async () => {
const checkbox = table.rowCheckbox(0);
await expect(checkbox).not.toBeChecked();
});
await test.step('Re-check the assertion', async () => {
const checkbox = table.rowCheckbox(0);
await checkbox.check();
});
await test.step('Verify checkbox is checked again', async () => {
const checkbox = table.rowCheckbox(0);
await expect(checkbox).toBeChecked();
});
await test.step('Save request', async () => {
await saveRequest(page);
});
});
test('should delete multiple assertions', async ({ pageWithUserData: page }) => {
const locators = buildCommonLocators(page);
const table = locators.assertionsTable();
await test.step('Add multiple assertions', async () => {
await addAssertion(page, { expr: 'res.status', value: '200' });
await addAssertion(page, { expr: 'res.body', value: 'pong' });
await addAssertion(page, { expr: 'res.responseTime', value: '1000' });
});
await test.step('Verify three assertions exist', async () => {
const rowCount = await table.allRows().count();
expect(rowCount).toBeGreaterThanOrEqual(3);
});
await test.step('Delete first assertion', async () => {
await deleteAssertion(page, 0);
});
await test.step('Delete second assertion (now at index 0 after first deletion)', async () => {
await deleteAssertion(page, 0);
});
await test.step('Verify only one assertion remains', async () => {
const rowCount = await table.allRows().count();
// Should have at least 1 assertion row + 1 empty row
expect(rowCount).toBeGreaterThanOrEqual(1);
});
await test.step('Save request', async () => {
await saveRequest(page);
});
});
test('should add assertion with different operators', async ({ pageWithUserData: page }) => {
const locators = buildCommonLocators(page);
const table = locators.assertionsTable();
await test.step('Add assertion with contains operator', async () => {
await addAssertion(page, {
expr: 'res.body',
value: 'pong',
operator: 'contains'
});
});
await test.step('Add assertion with greater than operator', async () => {
await addAssertion(page, {
expr: 'res.status',
value: '199',
operator: 'gt'
});
});
await test.step('Add assertion with length operator', async () => {
await addAssertion(page, {
expr: 'res.body',
value: '4',
operator: 'length'
});
});
await test.step('Verify assertions with different operators exist', async () => {
await expect(table.rowExprInput(0)).toHaveValue('res.body');
await expect(table.rowExprInput(1)).toHaveValue('res.status');
await expect(table.rowExprInput(2)).toHaveValue('res.body');
});
await test.step('Save request', async () => {
await saveRequest(page);
});
});
});

View File

@@ -0,0 +1,6 @@
{
"version": "1",
"name": "test-assertions-bru",
"type": "collection",
"uid": "test-assertions-bru-uid"
}

View File

@@ -0,0 +1,3 @@
vars {
host: https://testbench-sanity.usebruno.com
}

View File

@@ -0,0 +1,11 @@
meta {
name: ping
type: http
seq: 1
}
get {
url: {{host}}/ping
body: none
auth: none
}

View File

@@ -0,0 +1,5 @@
{
"lastOpenedCollections": [
"{{projectRoot}}/tests/asserts/fixtures/collection"
]
}

View File

@@ -705,6 +705,143 @@ const clickResponseAction = async (page: Page, actionTestId: string) => {
}
};
type AssertionInput = {
expr: string;
value: string;
operator?: string;
};
/**
* Add an assertion to the current request (adds to the last empty row)
* @param page - The page object
* @param assertion - The assertion to add (expr, value, optional operator)
* @returns The row index where the assertion was added
*/
const addAssertion = async (page: Page, assertion: AssertionInput): Promise<number> => {
const operator = assertion.operator || 'eq';
return await test.step(`Add assertion: ${assertion.expr} ${operator} ${assertion.value}`, async () => {
const locators = buildCommonLocators(page);
const table = locators.assertionsTable();
// Ensure assertions table is visible
await expect(table.container()).toBeVisible();
// Find the last row (which is the empty row for adding new assertions)
const rowCount = await table.allRows().count();
const targetRowIndex = rowCount - 1; // Last row is the empty row
// Wait for the row to exist
await expect(table.row(targetRowIndex)).toBeVisible();
// Fill in the expression
const exprInput = table.rowExprInput(targetRowIndex);
await expect(exprInput).toBeVisible({ timeout: 2000 });
await exprInput.click();
await page.keyboard.type(assertion.expr);
// The component creates a new empty row when the key field is filled
await expect(table.allRows()).toHaveCount(rowCount + 1);
// Fill in the value first (defaults to 'eq value')
const valueInput = table.rowValueInput(targetRowIndex);
await valueInput.click();
await page.keyboard.type(assertion.value);
// Select the operator from dropdown (if provided and not default 'eq')
// This will update the value field to combine operator + value
if (assertion.operator && assertion.operator !== 'eq') {
const operatorSelect = table.rowOperatorSelect(targetRowIndex);
await operatorSelect.selectOption(assertion.operator);
}
// Wait for the assertion to be fully processed
// Verify the expression was actually saved by checking the input value
const exprInputAfter = table.rowExprInput(targetRowIndex);
await expect(exprInputAfter).toHaveValue(assertion.expr, { timeout: 2000 });
return targetRowIndex;
});
};
/**
* Edit an assertion at a specific row index
* @param page - The page object
* @param rowIndex - The row index of the assertion to edit
* @param assertion - The assertion data to update (expr, value, optional operator)
* @returns void
*/
const editAssertion = async (page: Page, rowIndex: number, assertion: AssertionInput) => {
const operator = assertion.operator || 'eq';
await test.step(`Edit assertion at row ${rowIndex}: ${assertion.expr} ${operator} ${assertion.value}`, async () => {
const locators = buildCommonLocators(page);
const table = locators.assertionsTable();
// Ensure assertions table is visible
await expect(table.container()).toBeVisible();
// Wait for the row to exist
await expect(table.row(rowIndex)).toBeVisible();
// Update the expression
const exprInput = table.rowExprInput(rowIndex);
await expect(exprInput).toBeVisible({ timeout: 2000 });
await exprInput.click();
// Clear the input and type new value - use triple-click to select all (works cross-platform)
await exprInput.click({ clickCount: 3 });
await page.keyboard.press('Backspace'); // Clear selection
await page.keyboard.type(assertion.expr);
// Update the operator from dropdown (if provided)
if (assertion.operator) {
const operatorSelect = table.rowOperatorSelect(rowIndex);
await operatorSelect.selectOption(assertion.operator);
}
// Update the value (just the value, operator is already selected)
// The value cell contains a SingleLineEditor, so we need to click and type
const valueInput = table.rowValueInput(rowIndex);
await valueInput.click({ clickCount: 3 });
await page.keyboard.press('Backspace'); // Clear selection
await page.keyboard.type(assertion.value);
});
};
/**
* Delete an assertion from the current request by row index
* @param page - The page object
* @param rowIndex - The row index of the assertion to delete
* @returns void
*/
const deleteAssertion = async (page: Page, rowIndex: number) => {
await test.step(`Delete assertion at row ${rowIndex}`, async () => {
const locators = buildCommonLocators(page);
const table = locators.assertionsTable();
await expect(table.container()).toBeVisible();
const initialRowCount = await table.allRows().count();
const deleteButton = table.rowDeleteButton(rowIndex);
await deleteButton.click();
await expect(table.allRows()).toHaveCount(initialRowCount - 1);
});
};
/**
* Save the current request and verify success toast
* @param page - The page object
* @returns void
*/
const saveRequest = async (page: Page) => {
await test.step('Save request', async () => {
await page.keyboard.press('Meta+s');
await expect(page.getByText('Request saved successfully').last()).toBeVisible({ timeout: 3000 });
await page.waitForTimeout(200);
});
};
export {
closeAllCollections,
openCollection,
@@ -732,7 +869,11 @@ export {
switchResponseFormat,
switchToPreviewTab,
switchToEditorTab,
clickResponseAction
clickResponseAction,
addAssertion,
editAssertion,
deleteAssertion,
saveRequest
};
export type { SandboxMode, EnvironmentType, EnvironmentVariable, ImportCollectionOptions, CreateRequestOptions, CreateUntitledRequestOptions };
export type { SandboxMode, EnvironmentType, EnvironmentVariable, ImportCollectionOptions, CreateRequestOptions, CreateUntitledRequestOptions, AssertionInput };

View File

@@ -102,6 +102,51 @@ export const buildCommonLocators = (page: Page) => ({
locationInput: () => page.locator('#collection-location'),
fileInput: () => page.locator('input[type="file"]'),
envOption: (name: string) => page.locator('.dropdown-item').getByText(name, { exact: true })
},
/**
* Build generic table locators for any table with a testId
* @param testId - The testId of the table
* @returns Table locators object
*/
table: (testId: string) => {
const container = () => page.getByTestId(testId);
const getBodyRow = (index?: number) => {
const locator = container().locator('tbody tr');
return index !== undefined ? locator.nth(index) : locator;
};
return {
container,
row: (index?: number) => getBodyRow(index),
rowCell: (columnKey: string, rowIndex?: number) => {
const row = getBodyRow(rowIndex);
return row.getByTestId(`column-${columnKey}`);
},
rowCheckbox: (rowIndex: number) => getBodyRow(rowIndex).getByTestId('column-checkbox'),
rowDeleteButton: (rowIndex: number) => getBodyRow(rowIndex).getByTestId('column-delete'),
allRows: () => container().locator('tbody tr')
};
},
/**
* Assertions table locators (extends generic table with assertion-specific helpers)
* @returns Assertions table locators object
*/
assertionsTable: () => {
const baseTable = buildCommonLocators(page).table('assertions-table');
return {
...baseTable,
// Assertion-specific helpers
rowExprInput: (rowIndex: number) => {
const cell = baseTable.rowCell('name', rowIndex);
// Wait for the cell to be visible, then find the textbox
return cell.getByRole('textbox').or(cell.locator('input[type="text"]'));
},
rowOperatorSelect: (rowIndex: number) => {
const cell = baseTable.rowCell('operator', rowIndex);
return cell.getByTestId('assertion-operator-select').or(cell.locator('select'));
},
rowValueInput: (rowIndex: number) => baseTable.rowCell('value', rowIndex)
};
}
});