mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
refactor: update UI interactions and improve test stability (#6042)
This commit is contained in:
@@ -205,7 +205,11 @@ class SingleLineEditor extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div className={`flex flex-row justify-between w-full overflow-x-auto ${this.props.className}`}>
|
||||
<StyledWrapper ref={this.editorRef} className={`single-line-editor grow ${this.props.readOnly ? 'read-only' : ''}`} />
|
||||
<StyledWrapper
|
||||
ref={this.editorRef}
|
||||
className={`single-line-editor grow ${this.props.readOnly ? 'read-only' : ''}`}
|
||||
{...(this.props['data-testid'] ? { 'data-testid': this.props['data-testid'] } : {})}
|
||||
/>
|
||||
{this.secretEye(this.props.isSecret)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -23,13 +23,18 @@ const StyledWrapper = styled.div`
|
||||
max-width: 200px;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
cursor: default;
|
||||
|
||||
&:hover {
|
||||
&:has(.tag-remove:hover) {
|
||||
background-color: ${(props) => props.theme.requestTabs.active.bg};
|
||||
border-color: ${(props) => props.theme.requestTabs.active.border || props.theme.requestTabs.bottomBorder};
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.tag-remove {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.tag-icon {
|
||||
|
||||
@@ -49,6 +49,7 @@ const TagList = ({ tagsHintList = [], handleAddTag, tags, handleRemoveTag, onSav
|
||||
onChange={handleInputChange}
|
||||
onRun={handleKeyDown}
|
||||
onSave={onSave}
|
||||
data-testid="tag-input"
|
||||
/>
|
||||
{error && <span className='text-xs text-red-500'>{error}</span>}
|
||||
<ul className="flex flex-wrap gap-1">
|
||||
@@ -57,14 +58,15 @@ const TagList = ({ tagsHintList = [], handleAddTag, tags, handleRemoveTag, onSav
|
||||
<li key={_tag}>
|
||||
<button
|
||||
className="tag-item"
|
||||
onClick={() => handleRemoveTag(_tag)}
|
||||
type="button"
|
||||
>
|
||||
<IconTag size={12} className="tag-icon" aria-hidden="true" />
|
||||
<span className="tag-text" title={_tag}>
|
||||
{_tag}
|
||||
</span>
|
||||
<IconX size={12} strokeWidth={2} aria-hidden="true" />
|
||||
<span className="tag-remove" title="Remove tag" onClick={() => handleRemoveTag(_tag)}>
|
||||
<IconX size={12} strokeWidth={2} aria-hidden="true" />
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
))
|
||||
|
||||
@@ -11,12 +11,14 @@ test.describe('Create GraphQL Requests', () => {
|
||||
|
||||
test.afterAll(async ({ pageWithUserData: page }) => {
|
||||
// Clean up Root GraphQL Request
|
||||
await locators.sidebar.request('Root GraphQL Request').click({ button: 'right' });
|
||||
await locators.sidebar.request('Root GraphQL Request').hover();
|
||||
await locators.actions.collectionItemActions('Root GraphQL Request').click();
|
||||
await locators.dropdown.item('Delete').click();
|
||||
await locators.modal.button('Delete').click();
|
||||
|
||||
// Clean up Folder GraphQL Request
|
||||
await locators.sidebar.request('Folder GraphQL Request').click({ button: 'right' });
|
||||
await locators.sidebar.request('Folder GraphQL Request').hover();
|
||||
await locators.actions.collectionItemActions('Folder GraphQL Request').click();
|
||||
await locators.dropdown.item('Delete').click();
|
||||
await locators.modal.button('Delete').click();
|
||||
|
||||
@@ -58,8 +60,8 @@ test.describe('Create GraphQL Requests', () => {
|
||||
});
|
||||
|
||||
await test.step('Create GraphQL request via folder1 three dots menu', async () => {
|
||||
const folderItem = locators.sidebar.folder('folder1');
|
||||
await folderItem.click({ button: 'right' });
|
||||
await locators.sidebar.folder('folder1').hover();
|
||||
await locators.actions.collectionItemActions('folder1').click();
|
||||
await locators.dropdown.item('New Request').click();
|
||||
|
||||
await page.getByTestId('graphql-request').click();
|
||||
|
||||
@@ -13,12 +13,14 @@ test.describe('Create gRPC Requests', () => {
|
||||
const locators = buildCommonLocators(page);
|
||||
|
||||
// Clean up Root gRPC Request
|
||||
await locators.sidebar.request('Root gRPC Request').click({ button: 'right' });
|
||||
await locators.sidebar.request('Root gRPC Request').hover();
|
||||
await locators.actions.collectionItemActions('Root gRPC Request').click();
|
||||
await locators.dropdown.item('Delete').click();
|
||||
await locators.modal.button('Delete').click();
|
||||
|
||||
// Clean up Folder gRPC Request
|
||||
await locators.sidebar.request('Folder gRPC Request').click({ button: 'right' });
|
||||
await locators.sidebar.request('Folder gRPC Request').hover();
|
||||
await locators.actions.collectionItemActions('Folder gRPC Request').click();
|
||||
await locators.dropdown.item('Delete').click();
|
||||
await locators.modal.button('Delete').click();
|
||||
|
||||
@@ -60,8 +62,8 @@ test.describe('Create gRPC Requests', () => {
|
||||
});
|
||||
|
||||
await test.step('Create gRPC request via folder1 three dots menu', async () => {
|
||||
const folderItem = locators.sidebar.folder('folder1');
|
||||
await folderItem.click({ button: 'right' });
|
||||
await locators.sidebar.folder('folder1').hover();
|
||||
await locators.actions.collectionItemActions('folder1').click();
|
||||
await locators.dropdown.item('New Request').click();
|
||||
|
||||
await page.getByTestId('grpc-request').click();
|
||||
|
||||
@@ -13,12 +13,14 @@ test.describe('Create HTTP Requests', () => {
|
||||
const locators = buildCommonLocators(page);
|
||||
|
||||
// Clean up Root HTTP Request
|
||||
await locators.sidebar.request('Root HTTP Request').click({ button: 'right' });
|
||||
await locators.sidebar.request('Root HTTP Request').hover();
|
||||
await locators.actions.collectionItemActions('Root HTTP Request').click();
|
||||
await locators.dropdown.item('Delete').click();
|
||||
await locators.modal.button('Delete').click();
|
||||
|
||||
// Clean up Folder HTTP Request
|
||||
await locators.sidebar.request('Folder HTTP Request').click({ button: 'right' });
|
||||
await locators.sidebar.request('Folder HTTP Request').hover();
|
||||
await locators.actions.collectionItemActions('Folder HTTP Request').click();
|
||||
await locators.dropdown.item('Delete').click();
|
||||
await locators.modal.button('Delete').click();
|
||||
|
||||
@@ -59,8 +61,8 @@ test.describe('Create HTTP Requests', () => {
|
||||
});
|
||||
|
||||
await test.step('Create HTTP request via folder1 three dots menu', async () => {
|
||||
const folderItem = locators.sidebar.folder('folder1');
|
||||
await folderItem.click({ button: 'right' });
|
||||
await locators.sidebar.folder('folder1').hover();
|
||||
await locators.actions.collectionItemActions('folder1').click();
|
||||
await locators.dropdown.item('New Request').click();
|
||||
|
||||
await page.getByTestId('request-name').fill('Folder HTTP Request');
|
||||
|
||||
@@ -13,12 +13,14 @@ test.describe('Create WebSocket Requests', () => {
|
||||
const locators = buildCommonLocators(page);
|
||||
|
||||
// Clean up Folder WebSocket Request
|
||||
await locators.sidebar.request('Folder WebSocket Request').click({ button: 'right' });
|
||||
await locators.sidebar.request('Folder WebSocket Request').hover();
|
||||
await locators.actions.collectionItemActions('Folder WebSocket Request').click();
|
||||
await locators.dropdown.item('Delete').click();
|
||||
await locators.modal.button('Delete').click();
|
||||
|
||||
// Clean up Root WebSocket Request
|
||||
await locators.sidebar.request('Root WebSocket Request').click({ button: 'right' });
|
||||
await locators.sidebar.request('Root WebSocket Request').hover();
|
||||
await locators.actions.collectionItemActions('Root WebSocket Request').click();
|
||||
await locators.dropdown.item('Delete').click();
|
||||
await locators.modal.button('Delete').click();
|
||||
|
||||
@@ -60,8 +62,8 @@ test.describe('Create WebSocket Requests', () => {
|
||||
});
|
||||
|
||||
await test.step('Create WebSocket request via folder1 three dots menu', async () => {
|
||||
const folderItem = locators.sidebar.folder('folder1');
|
||||
await folderItem.click({ button: 'right' });
|
||||
await locators.sidebar.folder('folder1').hover();
|
||||
await locators.actions.collectionItemActions('folder1').click();
|
||||
await locators.dropdown.item('New Request').click();
|
||||
|
||||
await page.getByTestId('ws-request').click();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { test, expect } from '../../../playwright';
|
||||
import { closeAllCollections } from '../../utils/page';
|
||||
import { closeAllCollections, createCollection } from '../../utils/page';
|
||||
|
||||
test.describe('Cross-Collection Drag and Drop for folder', () => {
|
||||
test.afterEach(async ({ page }) => {
|
||||
@@ -8,18 +8,8 @@ test.describe('Cross-Collection Drag and Drop for folder', () => {
|
||||
});
|
||||
|
||||
test('Verify cross-collection folder drag and drop', async ({ page, createTmpDir }) => {
|
||||
// Create first collection - click dropdown menu first
|
||||
await page.locator('.dropdown-icon').click();
|
||||
await page.locator('.dropdown-item').filter({ hasText: 'Create Collection' }).click();
|
||||
await page.getByLabel('Name').fill('source-collection');
|
||||
await page.getByLabel('Location').fill(await createTmpDir('source-collection'));
|
||||
await page.getByRole('button', { name: 'Create', exact: true }).click();
|
||||
|
||||
// Wait for collection to appear and click on it
|
||||
await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' })).toBeVisible();
|
||||
await page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' }).click();
|
||||
await page.getByLabel('Safe Mode').check();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
// Create first collection - open with sandbox mode
|
||||
await createCollection(page, 'source-collection', await createTmpDir('source-collection'), { openWithSandboxMode: 'safe' });
|
||||
|
||||
// Create a folder in the first collection
|
||||
// Look for the collection menu button for the source collection specifically
|
||||
@@ -34,11 +24,12 @@ test.describe('Cross-Collection Drag and Drop for folder', () => {
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
// Wait for the folder to be created and appear in the sidebar
|
||||
await page.waitForTimeout(2000);
|
||||
await page.waitForTimeout(200);
|
||||
await expect(page.locator('.collection-item-name').filter({ hasText: 'test-folder' })).toBeVisible();
|
||||
|
||||
// Add a request to the folder to make it more realistic
|
||||
await page.locator('.collection-item-name').filter({ hasText: 'test-folder' }).click({ button: 'right' });
|
||||
await page.locator('.collection-item-name').filter({ hasText: 'test-folder' }).hover();
|
||||
await page.locator('.collection-item-name').filter({ hasText: 'test-folder' }).locator('.menu-icon').click();
|
||||
await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click();
|
||||
await page.getByPlaceholder('Request Name').fill('test-request-in-folder');
|
||||
await page.locator('#new-request-url .CodeMirror').click();
|
||||
@@ -46,25 +37,15 @@ test.describe('Cross-Collection Drag and Drop for folder', () => {
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
// Wait for the request to be created
|
||||
await page.waitForTimeout(1000);
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
// Expand the folder to see the request inside
|
||||
await page.locator('.collection-item-name').filter({ hasText: 'test-folder' }).click();
|
||||
await page.waitForTimeout(500);
|
||||
await page.waitForTimeout(200);
|
||||
await expect(page.locator('.collection-item-name').filter({ hasText: 'test-request-in-folder' })).toBeVisible();
|
||||
|
||||
// Create second collection - click dropdown menu first
|
||||
await page.locator('.dropdown-icon').click();
|
||||
await page.locator('.dropdown-item').filter({ hasText: 'Create Collection' }).click();
|
||||
await page.getByLabel('Name').fill('target-collection');
|
||||
await page.getByLabel('Location').fill(await createTmpDir('target-collection'));
|
||||
await page.getByRole('button', { name: 'Create', exact: true }).click();
|
||||
|
||||
// Wait for second collection to appear and click on it
|
||||
await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' })).toBeVisible();
|
||||
await page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' }).click();
|
||||
await page.getByLabel('Safe Mode').check();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
// Create second collection - open with sandbox mode
|
||||
await createCollection(page, 'target-collection', await createTmpDir('target-collection'), { openWithSandboxMode: 'safe' });
|
||||
|
||||
// Wait for both collections to be visible in sidebar
|
||||
await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' })).toBeVisible();
|
||||
@@ -82,12 +63,12 @@ test.describe('Cross-Collection Drag and Drop for folder', () => {
|
||||
await sourceFolder.dragTo(targetCollection);
|
||||
|
||||
// Wait for the operation to complete
|
||||
await page.waitForTimeout(3000);
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
// Verify the folder has been moved to the target collection
|
||||
// Click on target collection to expand it if needed
|
||||
await page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' }).click();
|
||||
await page.waitForTimeout(1000);
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
// Check that the folder now appears under target collection
|
||||
const targetCollectionContainer = page
|
||||
@@ -100,7 +81,7 @@ test.describe('Cross-Collection Drag and Drop for folder', () => {
|
||||
|
||||
// Expand the moved folder to verify the request inside is also moved
|
||||
await targetCollectionContainer.locator('.collection-item-name').filter({ hasText: 'test-folder' }).click();
|
||||
await page.waitForTimeout(500);
|
||||
await page.waitForTimeout(200);
|
||||
await expect(
|
||||
targetCollectionContainer.locator('.collection-item-name').filter({ hasText: 'test-request-in-folder' })
|
||||
).toBeVisible();
|
||||
@@ -125,17 +106,7 @@ test.describe('Cross-Collection Drag and Drop for folder', () => {
|
||||
createTmpDir
|
||||
}) => {
|
||||
// Create first collection (source) - use unique names for this test
|
||||
await page.locator('.dropdown-icon').click();
|
||||
await page.locator('.dropdown-item').filter({ hasText: 'Create Collection' }).click();
|
||||
await page.getByLabel('Name').fill('source-collection');
|
||||
await page.getByLabel('Location').fill(await createTmpDir('source-collection'));
|
||||
await page.getByRole('button', { name: 'Create', exact: true }).click();
|
||||
|
||||
// Wait for collection to appear and click on it
|
||||
await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' })).toBeVisible();
|
||||
await page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' }).click();
|
||||
await page.getByLabel('Safe Mode').check();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await createCollection(page, 'source-collection', await createTmpDir('source-collection'), { openWithSandboxMode: 'safe' });
|
||||
|
||||
// Create a folder in the first collection
|
||||
await page
|
||||
@@ -158,7 +129,8 @@ test.describe('Cross-Collection Drag and Drop for folder', () => {
|
||||
await expect(page.locator('.collection-item-name').filter({ hasText: 'folder-1' })).toBeVisible();
|
||||
|
||||
// Add a request to the folder to make it more realistic
|
||||
await page.locator('.collection-item-name').filter({ hasText: 'folder-1' }).click({ button: 'right' });
|
||||
await page.locator('.collection-item-name').filter({ hasText: 'folder-1' }).hover();
|
||||
await page.locator('.collection-item-name').filter({ hasText: 'folder-1' }).locator('.menu-icon').click();
|
||||
await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click();
|
||||
await page.getByPlaceholder('Request Name').fill('http-request');
|
||||
await page.locator('#new-request-url .CodeMirror').click();
|
||||
@@ -169,17 +141,7 @@ test.describe('Cross-Collection Drag and Drop for folder', () => {
|
||||
await expect(page.locator('.collection-item-name').filter({ hasText: 'http-request' })).toBeVisible();
|
||||
|
||||
// Create second collection (target)
|
||||
await page.locator('.dropdown-icon').click();
|
||||
await page.locator('.dropdown-item').filter({ hasText: 'Create Collection' }).click();
|
||||
await page.getByLabel('Name').fill('target-collection');
|
||||
await page.getByLabel('Location').fill(await createTmpDir('target-collection'));
|
||||
await page.getByRole('button', { name: 'Create', exact: true }).click();
|
||||
|
||||
// Wait for second collection to appear and click on it
|
||||
await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' })).toBeVisible();
|
||||
await page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' }).click();
|
||||
await page.getByLabel('Safe Mode').check();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await createCollection(page, 'target-collection', await createTmpDir('target-collection'), { openWithSandboxMode: 'safe' });
|
||||
|
||||
// Create a folder with the same name in the target collection
|
||||
await page
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { test, expect } from '../../../playwright';
|
||||
import { closeAllCollections } from '../../utils/page';
|
||||
import { closeAllCollections, createCollection } from '../../utils/page';
|
||||
|
||||
test.describe('Cross-Collection Drag and Drop', () => {
|
||||
test.afterEach(async ({ page }) => {
|
||||
@@ -8,17 +8,8 @@ test.describe('Cross-Collection Drag and Drop', () => {
|
||||
});
|
||||
|
||||
test('Verify request drag and drop', async ({ page, createTmpDir }) => {
|
||||
// Create first collection - click dropdown menu first
|
||||
await page.locator('.dropdown-icon').click();
|
||||
await page.locator('.dropdown-item').filter({ hasText: 'Create Collection' }).click();
|
||||
await page.getByLabel('Name').fill('source-collection');
|
||||
await page.getByLabel('Location').fill(await createTmpDir('source-collection'));
|
||||
await page.getByRole('button', { name: 'Create', exact: true }).click();
|
||||
|
||||
await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' })).toBeVisible();
|
||||
await page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' }).click();
|
||||
await page.getByLabel('Safe Mode').check();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
// Create first collection - open with sandbox mode
|
||||
await createCollection(page, 'source-collection', await createTmpDir('source-collection'), { openWithSandboxMode: 'safe' });
|
||||
|
||||
// Create a request in the first collection
|
||||
await page.locator('#create-new-tab').getByRole('img').click();
|
||||
@@ -29,17 +20,8 @@ test.describe('Cross-Collection Drag and Drop', () => {
|
||||
|
||||
await expect(page.locator('.collection-item-name').filter({ hasText: 'test-request' })).toBeVisible();
|
||||
|
||||
// Create second collection - click dropdown menu first
|
||||
await page.locator('.dropdown-icon').click();
|
||||
await page.locator('.dropdown-item').filter({ hasText: 'Create Collection' }).click();
|
||||
await page.getByLabel('Name').fill('target-collection');
|
||||
await page.getByLabel('Location').fill(await createTmpDir('target-collection'));
|
||||
await page.getByRole('button', { name: 'Create', exact: true }).click();
|
||||
|
||||
await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' })).toBeVisible();
|
||||
await page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' }).click();
|
||||
await page.getByLabel('Safe Mode').check();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
// Create second collection - open with sandbox mode
|
||||
await createCollection(page, 'target-collection', await createTmpDir('target-collection'), { openWithSandboxMode: 'safe' });
|
||||
|
||||
await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' })).toBeVisible();
|
||||
await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' })).toBeVisible();
|
||||
@@ -83,17 +65,7 @@ test.describe('Cross-Collection Drag and Drop', () => {
|
||||
createTmpDir
|
||||
}) => {
|
||||
// Create first collection (source-collection)
|
||||
await page.locator('.dropdown-icon').click();
|
||||
await page.locator('.dropdown-item').filter({ hasText: 'Create Collection' }).click();
|
||||
await page.getByLabel('Name').fill('source-collection');
|
||||
await page.getByLabel('Location').fill(await createTmpDir('source-collection'));
|
||||
await page.getByRole('button', { name: 'Create', exact: true }).click();
|
||||
|
||||
// Open collection
|
||||
await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' })).toBeVisible();
|
||||
await page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' }).click();
|
||||
await page.getByLabel('Safe Mode').check();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await createCollection(page, 'source-collection', await createTmpDir('source-collection'), { openWithSandboxMode: 'safe' });
|
||||
|
||||
// Create a request in the first collection (request-1)
|
||||
await page.locator('#create-new-tab').getByRole('img').click();
|
||||
@@ -106,17 +78,7 @@ test.describe('Cross-Collection Drag and Drop', () => {
|
||||
await expect(page.locator('.collection-item-name').filter({ hasText: 'request-1' })).toBeVisible();
|
||||
|
||||
// Create second collection (target-collection)
|
||||
await page.locator('.dropdown-icon').click();
|
||||
await page.locator('.dropdown-item').filter({ hasText: 'Create Collection' }).click();
|
||||
await page.getByLabel('Name').fill('target-collection');
|
||||
await page.getByLabel('Location').fill(await createTmpDir('target-collection'));
|
||||
await page.getByRole('button', { name: 'Create', exact: true }).click();
|
||||
|
||||
// Open collection
|
||||
await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' })).toBeVisible();
|
||||
await page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' }).click();
|
||||
await page.getByLabel('Safe Mode').check();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await createCollection(page, 'target-collection', await createTmpDir('target-collection'), { openWithSandboxMode: 'safe' });
|
||||
|
||||
// Create a request in the target collection with the same name (request-1)
|
||||
await page.locator('#create-new-tab').getByRole('img').click();
|
||||
|
||||
@@ -19,19 +19,19 @@ test.describe('Tag persistence', () => {
|
||||
|
||||
// Create a new request
|
||||
await page.locator('#create-new-tab').getByRole('img').click();
|
||||
await page.getByRole('textbox', { name: 'Request Name' }).fill('r1');
|
||||
await page.getByRole('textbox', { name: 'Request Name' }).fill('request-1');
|
||||
await page.locator('#new-request-url textarea').fill('https://httpfaker.org/api/echo');
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
// create another request
|
||||
await page.locator('#create-new-tab').getByRole('img').click();
|
||||
await page.getByRole('textbox', { name: 'Request Name' }).fill('r2');
|
||||
await page.getByRole('textbox', { name: 'Request Name' }).fill('request-2');
|
||||
await page.locator('#new-request-url textarea').fill('https://httpfaker.org/api/echo');
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
// create another request
|
||||
await page.locator('#create-new-tab').getByRole('img').click();
|
||||
await page.getByRole('textbox', { name: 'Request Name' }).fill('r3');
|
||||
await page.getByRole('textbox', { name: 'Request Name' }).fill('request-3');
|
||||
await page.locator('#new-request-url textarea').fill('https://httpfaker.org/api/echo');
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
@@ -39,37 +39,38 @@ test.describe('Tag persistence', () => {
|
||||
|
||||
// Add a tag to the request
|
||||
await page.getByRole('tab', { name: 'Settings' }).click();
|
||||
await page.getByText('Tagse.g., smoke, regression').click();
|
||||
await page.getByRole('textbox').nth(2).fill('smoke');
|
||||
await page.getByRole('textbox').nth(2).press('Enter');
|
||||
|
||||
await page.waitForTimeout(200);
|
||||
const tagInput = await page.getByTestId('tag-input').getByRole('textbox');
|
||||
await tagInput.fill('smoke');
|
||||
await tagInput.press('Enter');
|
||||
await page.waitForTimeout(200);
|
||||
// Verify the tag was added
|
||||
await expect(page.getByRole('button', { name: 'smoke' })).toBeVisible();
|
||||
await expect(page.locator('.tag-item', { hasText: 'smoke' })).toBeVisible();
|
||||
await page.keyboard.press('Meta+s');
|
||||
|
||||
// Move the r2 request to just above r1 within the same collection
|
||||
const r3Request = page.locator('.collection-item-name').filter({ hasText: 'r3' });
|
||||
const r1Request = page.locator('.collection-item-name').filter({ hasText: 'r1' });
|
||||
// Move the request-3 request to just above request-1 within the same collection
|
||||
const r3Request = page.locator('.collection-item-name').filter({ hasText: 'request-3' });
|
||||
const r1Request = page.locator('.collection-item-name').filter({ hasText: 'request-1' });
|
||||
|
||||
await expect(r3Request).toBeVisible();
|
||||
await expect(r1Request).toBeVisible();
|
||||
|
||||
// Perform drag and drop operation to move r3 below r1 using source position
|
||||
// Perform drag and drop operation to move request-3 below request-1 using source position
|
||||
await r3Request.dragTo(r1Request, {
|
||||
targetPosition: { x: 0, y: 1 }
|
||||
});
|
||||
|
||||
// Verify the requests are still in the collection and r3 is now above r1
|
||||
await expect(page.locator('.collection-item-name').filter({ hasText: 'r3' })).toBeVisible();
|
||||
await expect(page.locator('.collection-item-name').filter({ hasText: 'r1' })).toBeVisible();
|
||||
// Verify the requests are still in the collection and request-3 is now above request-1
|
||||
await expect(page.locator('.collection-item-name').filter({ hasText: 'request-3' })).toBeVisible();
|
||||
await expect(page.locator('.collection-item-name').filter({ hasText: 'request-1' })).toBeVisible();
|
||||
|
||||
// Click on r3 to verify the tag persisted after the move
|
||||
await page.locator('.collection-item-name').filter({ hasText: 'r3' }).click();
|
||||
await page.locator('.request-tab.active', { hasText: 'r3' }).waitFor({ state: 'visible' });
|
||||
// Click on request-3 to verify the tag persisted after the move
|
||||
await page.locator('.collection-item-name').filter({ hasText: 'request-3' }).click();
|
||||
await page.locator('.request-tab.active').filter({ hasText: 'request-3' }).waitFor({ state: 'visible' });
|
||||
await page.getByRole('tab', { name: 'Settings' }).click();
|
||||
|
||||
await page.waitForTimeout(200);
|
||||
// Verify the tag is still present after the move
|
||||
await expect(page.getByRole('button', { name: 'smoke' })).toBeVisible();
|
||||
await expect(page.locator('.tag-item', { hasText: 'smoke' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('verify tag persistence while moving requests between folders', async ({ page, createTmpDir }) => {
|
||||
@@ -83,75 +84,82 @@ test.describe('Tag persistence', () => {
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
// Create a new folder
|
||||
await page.getByTitle('test-collection').click({
|
||||
button: 'right'
|
||||
});
|
||||
await page.waitForTimeout(200);
|
||||
await page.locator('.collection-name')
|
||||
.filter({ hasText: 'test-collection' }).hover();
|
||||
await page.locator('.collection-name')
|
||||
.filter({ hasText: 'test-collection' }).locator('.collection-actions .icon').click();
|
||||
await page.waitForTimeout(1);
|
||||
await page.getByText('New Folder').click();
|
||||
await page.locator('#folder-name').fill('f1');
|
||||
await page.locator('#folder-name').fill('folder-1');
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
// Create a new request within folder-1 folder
|
||||
await page.getByText('folder-1').click();
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
// Create a new request within f1 folder
|
||||
await page.getByText('f1').click();
|
||||
await page.waitForTimeout(200);
|
||||
await page.getByTitle('f1', { exact: true }).click({
|
||||
button: 'right'
|
||||
});
|
||||
await page.locator('.collection-item-name').filter({ hasText: 'folder-1' }).hover();
|
||||
await page.locator('.collection-item-name').filter({ hasText: 'folder-1' }).locator('.menu-icon').click();
|
||||
await page.locator('.dropdown-item').getByText('New Request').click()
|
||||
await page.getByRole('textbox', { name: 'Request Name' }).fill('r1');
|
||||
await page.getByRole('textbox', { name: 'Request Name' }).fill('request-1');
|
||||
await page.locator('#new-request-url textarea').fill('https://httpfaker.org/api/echo');
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
// create another request within f1 folder
|
||||
await page.getByTitle('f1', { exact: true }).click({
|
||||
button: 'right'
|
||||
});
|
||||
// create another request within folder-1 folder
|
||||
await page.locator('.collection-item-name')
|
||||
.filter({ hasText: 'folder-1' }).hover();
|
||||
await page.locator('.collection-item-name')
|
||||
.filter({ hasText: 'folder-1' }).locator('.menu-icon').click();
|
||||
await page.locator('.dropdown-item').getByText('New Request').click()
|
||||
await page.getByRole('textbox', { name: 'Request Name' }).fill('r2');
|
||||
await page.getByRole('textbox', { name: 'Request Name' }).fill('request-2');
|
||||
await page.locator('#new-request-url textarea').fill('https://httpfaker.org/api/echo');
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
// Add a tag to the request
|
||||
await page.getByRole('tab', { name: 'Settings' }).click();
|
||||
await page.getByText('Tagse.g., smoke, regression').click();
|
||||
await page.getByRole('textbox').nth(2).fill('smoke');
|
||||
await page.getByRole('textbox').nth(2).press('Enter');
|
||||
await expect(page.getByRole('button', { name: 'smoke' })).toBeVisible();
|
||||
await page.waitForTimeout(200);
|
||||
const tagInput = await page.getByTestId('tag-input').getByRole('textbox');
|
||||
await tagInput.fill('smoke');
|
||||
await tagInput.press('Enter');
|
||||
await page.waitForTimeout(200);
|
||||
await expect(page.locator('.tag-item', { hasText: 'smoke' })).toBeVisible();
|
||||
await page.keyboard.press('Meta+s');
|
||||
|
||||
// Create another folder
|
||||
await page.getByTitle('test-collection').click({
|
||||
button: 'right'
|
||||
});
|
||||
await page.locator('.collection-name')
|
||||
.filter({ hasText: 'test-collection' }).hover();
|
||||
await page.locator('.collection-name')
|
||||
.filter({ hasText: 'test-collection' }).locator('.collection-actions .icon').click();
|
||||
await page.locator('.dropdown-item').getByText('New Folder').click();
|
||||
await page.locator('#folder-name').fill('f2');
|
||||
await page.locator('#folder-name').fill('folder-2');
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
// open f2 folder
|
||||
await page.getByText('f2').click();
|
||||
await page.getByTitle('f2', { exact: true }).click({
|
||||
button: 'right'
|
||||
});
|
||||
// open folder-2 folder
|
||||
await page.getByText('folder-2').click();
|
||||
await page.locator('.collection-item-name')
|
||||
.filter({ hasText: 'folder-2' }).hover();
|
||||
await page.locator('.collection-item-name')
|
||||
.filter({ hasText: 'folder-2' }).locator('.menu-icon').click();
|
||||
await page.locator('.dropdown-item').getByText('New Request').click();
|
||||
await page.getByRole('textbox', { name: 'Request Name' }).fill('r3');
|
||||
await page.getByRole('textbox', { name: 'Request Name' }).fill('request-3');
|
||||
await page.locator('#new-request-url textarea').fill('https://httpfaker.org/api/echo');
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
// Drag and drop r2 request to f2 folder
|
||||
const r2Request = page.locator('.collection-item-name').filter({ hasText: 'r2' });
|
||||
const f2Folder = page.locator('.collection-item-name').filter({ hasText: 'f2' });
|
||||
// Drag and drop request-2 request to folder-2 folder
|
||||
const r2Request = page.locator('.collection-item-name').filter({ hasText: 'request-2' });
|
||||
const f2Folder = page.locator('.collection-item-name').filter({ hasText: 'folder-2' });
|
||||
await r2Request.dragTo(f2Folder);
|
||||
|
||||
// Verify the requests are still in the collection and r2 is now in f2 folder
|
||||
await expect(page.locator('.collection-item-name').filter({ hasText: 'r2' })).toBeVisible();
|
||||
await expect(page.locator('.collection-item-name').filter({ hasText: 'f2' })).toBeVisible();
|
||||
// Verify the requests are still in the collection and request-2 is now in folder-2 folder
|
||||
await expect(page.locator('.collection-item-name').filter({ hasText: 'request-2' })).toBeVisible();
|
||||
await expect(page.locator('.collection-item-name').filter({ hasText: 'folder-2' })).toBeVisible();
|
||||
|
||||
// Click on r2 to verify the tag persisted after the move
|
||||
await page.locator('.collection-item-name').filter({ hasText: 'r2' }).click();
|
||||
await page.locator('.request-tab.active', { hasText: 'r2' }).waitFor({ state: 'visible' });
|
||||
// Click on request-2 to verify the tag persisted after the move
|
||||
await page.locator('.collection-item-name').filter({ hasText: 'request-2' }).click();
|
||||
await page.locator('.request-tab.active').filter({ hasText: 'request-2' }).waitFor({ state: 'visible' });
|
||||
await page.getByRole('tab', { name: 'Settings' }).click();
|
||||
await expect(page.getByRole('button', { name: 'smoke' })).toBeVisible();
|
||||
await page.waitForTimeout(200);
|
||||
await expect(page.locator('.tag-item', { hasText: 'smoke' })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,7 +10,9 @@ test.describe.serial('bru.setEnvVar multiple persistent variables', () => {
|
||||
if (page && !page.isClosed()) {
|
||||
await page.locator('#sidebar-collection-name').click();
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.waitForTimeout(200);
|
||||
await page.locator('#configure-env').click();
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
// Remove the test environment variables
|
||||
const key1Row = page.getByRole('row', { name: 'multiple-persist-vars-key1' });
|
||||
@@ -40,6 +42,7 @@ test.describe.serial('bru.setEnvVar multiple persistent variables', () => {
|
||||
|
||||
await test.step('Select stage environment', async () => {
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.waitForTimeout(200);
|
||||
await expect(page.locator('.environment-list .dropdown-item', { hasText: 'Stage' })).toBeVisible();
|
||||
await page.locator('.environment-list .dropdown-item', { hasText: 'Stage' }).click();
|
||||
await expect(page.locator('.current-environment', { hasText: 'Stage' })).toBeVisible();
|
||||
@@ -49,8 +52,9 @@ test.describe.serial('bru.setEnvVar multiple persistent variables', () => {
|
||||
// Ensure we're in the correct collection context before selecting the folder
|
||||
await expect(page.locator('#sidebar-collection-name', { hasText: 'collection' })).toBeVisible();
|
||||
|
||||
// Right-click on the folder to open context menu
|
||||
await page.getByText('multiple-persist-vars-folder', { exact: true }).click({ button: 'right' });
|
||||
// Hover on the folder and open context menu
|
||||
await page.getByText('multiple-persist-vars-folder', { exact: true }).hover();
|
||||
await page.locator('.collection-item-name').filter({ hasText: 'multiple-persist-vars-folder' }).locator('.menu-icon').click();
|
||||
|
||||
// Click on Run option
|
||||
await page.getByText('Run', { exact: true }).click();
|
||||
@@ -67,7 +71,9 @@ test.describe.serial('bru.setEnvVar multiple persistent variables', () => {
|
||||
await expect(page.locator('#sidebar-collection-name', { hasText: 'collection' })).toBeVisible();
|
||||
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.waitForTimeout(200);
|
||||
await page.locator('#configure-env').click();
|
||||
await page.waitForTimeout(200);
|
||||
await expect(page.getByRole('row', { name: 'multiple-persist-vars-key1' }).getByRole('cell').nth(1)).toBeVisible();
|
||||
await expect(page.getByRole('row', { name: 'value1' }).getByRole('cell').nth(2)).toBeVisible();
|
||||
await expect(page.getByRole('row', { name: 'multiple-persist-vars-key2' }).getByRole('cell').nth(1)).toBeVisible();
|
||||
|
||||
@@ -249,7 +249,7 @@ test.describe.serial('Global Environment Export Tests', () => {
|
||||
await page.getByRole('button', { name: 'Export 1 Environment' }).click();
|
||||
|
||||
// Verify success message
|
||||
await expect(page.getByText('Environment(s) exported successfully', { exact: false })).toBeVisible();
|
||||
await expect(page.getByText('Environment(s) exported successfully', { exact: false }).first()).toBeVisible();
|
||||
|
||||
await page.getByTestId('modal-close-button').click();
|
||||
});
|
||||
@@ -296,9 +296,9 @@ test.describe.serial('Global Environment Export Tests', () => {
|
||||
await test.step('Execute export and verify success', async () => {
|
||||
// Export the environments
|
||||
await page.getByRole('button', { name: 'Export 2 Environments' }).click();
|
||||
|
||||
await page.waitForTimeout(200);
|
||||
// Verify success message
|
||||
await expect(page.getByText('Environment(s) exported successfully', { exact: false })).toBeVisible();
|
||||
await expect(page.getByText('Environment(s) exported successfully', { exact: false }).first()).toBeVisible();
|
||||
|
||||
await page.getByTestId('modal-close-button').click();
|
||||
});
|
||||
|
||||
@@ -83,17 +83,5 @@ test.describe('Collection Environment Import Tests', () => {
|
||||
await page.locator('[data-testid="send-arrow-icon"]').click();
|
||||
await page.locator('[data-testid="response-status-code"]').waitFor({ state: 'visible' });
|
||||
await expect(page.locator('[data-testid="response-status-code"]')).toContainText('201');
|
||||
|
||||
// Cleanup
|
||||
await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Test Collection' }).click();
|
||||
await page
|
||||
.locator('.collection-name')
|
||||
.filter({ has: page.locator('#sidebar-collection-name:has-text("Environment Test Collection")') })
|
||||
.locator('.collection-actions')
|
||||
.click();
|
||||
// Wait for the close collection modal to be hidden
|
||||
await page.locator('.dropdown-item').filter({ hasText: 'Close' }).click();
|
||||
await page.locator('.dropdown-item').filter({ hasText: 'Close' }).waitFor({ state: 'detached' });
|
||||
await page.getByRole('button', { name: 'Close' }).click();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -87,11 +87,13 @@ test.describe('Onboarding', () => {
|
||||
const page = await app.firstWindow();
|
||||
|
||||
// First launch - sample collection should be created
|
||||
const sampleCollection = page.locator('#sidebar-collection-name').getByText('Sample API Collection');
|
||||
const sampleCollection = page.locator('.collection-name').filter({ hasText: 'Sample API Collection' });
|
||||
await expect(sampleCollection).toBeVisible();
|
||||
|
||||
// User closes the sample collection (right-click to open context menu)
|
||||
await sampleCollection.click({ button: 'right' });
|
||||
// User closes the sample collection (hover on the collection and open context menu)
|
||||
await sampleCollection.hover();
|
||||
await sampleCollection.locator('.collection-actions .icon').click();
|
||||
|
||||
|
||||
// Close the sample collection
|
||||
const closeOption = page.locator('.dropdown-item').getByText('Close');
|
||||
|
||||
@@ -28,7 +28,7 @@ test.describe('Default Collection Location Feature', () => {
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
// verify success message
|
||||
await expect(page.locator('text=Preferences saved successfully')).toBeVisible();
|
||||
await expect(page.locator('text=Preferences saved successfully').first()).toBeVisible();
|
||||
|
||||
// wait for 2 seconds
|
||||
await page.waitForTimeout(2000);
|
||||
@@ -48,7 +48,7 @@ test.describe('Default Collection Location Feature', () => {
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
// verify success message
|
||||
await expect(page.locator('text=Preferences saved successfully')).toBeVisible();
|
||||
await expect(page.locator('text=Preferences saved successfully').first()).toBeVisible();
|
||||
|
||||
// wait for 2 seconds
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { execSync } from 'child_process';
|
||||
import { test, expect } from '../../playwright';
|
||||
import { closeAllCollections } from '../utils/page';
|
||||
import path from 'path';
|
||||
|
||||
test.describe('manage protofile', () => {
|
||||
test.afterAll(async ({ pageWithUserData: page }) => {
|
||||
await closeAllCollections(page);
|
||||
// Reset the collection request file to the original state
|
||||
execSync(`git checkout -- ${path.join(__dirname, 'collection', 'bruno.json')}`);
|
||||
});
|
||||
|
||||
test('protofiles, import paths from bruno.json are visible in the protobuf settings', async ({ pageWithUserData: page }) => {
|
||||
|
||||
@@ -27,7 +27,8 @@ test.describe('Copy and Paste Requests', () => {
|
||||
await page.locator('.dropdown-item').filter({ hasText: 'Copy' }).click();
|
||||
|
||||
// Paste into the collection root
|
||||
await collection.click({ button: 'right' });
|
||||
await collection.hover();
|
||||
await collection.locator('.collection-actions .icon').click();
|
||||
await page.locator('.dropdown-item').filter({ hasText: 'Paste' }).click();
|
||||
|
||||
// Verify the pasted request appears with the same name
|
||||
@@ -45,7 +46,8 @@ test.describe('Copy and Paste Requests', () => {
|
||||
// Paste into the folder
|
||||
const folder = page.locator('.collection-item-name').filter({ hasText: 'test-folder' });
|
||||
await folder.click();
|
||||
await folder.click({ button: 'right' });
|
||||
await folder.hover();
|
||||
await folder.locator('.menu-icon').click();
|
||||
await page.locator('.dropdown-item').filter({ hasText: 'Paste' }).click();
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
@@ -57,7 +59,8 @@ test.describe('Copy and Paste Requests', () => {
|
||||
const collection = page.locator('.collection-name').filter({ hasText: 'test-collection-2' });
|
||||
|
||||
// Paste into the collection root
|
||||
await collection.click({ button: 'right' });
|
||||
await collection.hover();
|
||||
await collection.locator('.collection-actions .icon').click();
|
||||
await page.locator('.dropdown-item').filter({ hasText: 'Paste' }).click();
|
||||
|
||||
// Verify the pasted request appears with the same name
|
||||
|
||||
@@ -10,8 +10,8 @@ test.describe.serial('Create and Delete Response Examples', () => {
|
||||
|
||||
test('should create a response example from response bookmark', async ({ pageWithUserData: page }) => {
|
||||
await test.step('Open collection and request', async () => {
|
||||
await page.locator('#sidebar-collection-name').getByText('collection').click();
|
||||
await page.locator('.collection-item-name').getByText('create-example').click();
|
||||
await page.locator('#sidebar-collection-name').filter({ hasText: 'collection' }).click();
|
||||
await page.locator('.collection-item-name').filter({ hasText: 'create-example' }).click();
|
||||
});
|
||||
|
||||
await test.step('Send request and validate example creation', async () => {
|
||||
|
||||
@@ -27,7 +27,7 @@ test.describe.serial('Edit Response Examples', () => {
|
||||
|
||||
await test.step('Open existing example', async () => {
|
||||
await page.locator('.collection-item-name', { hasText: 'edit-example' }).getByTestId('request-item-chevron').click();
|
||||
const exampleItem = page.locator('.collection-item-name').getByText('Test Example', { exact: true });
|
||||
const exampleItem = page.locator('.collection-item-name').filter({ hasText: 'Test Example' });
|
||||
await expect(exampleItem).toBeVisible();
|
||||
await exampleItem.click();
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { execSync } from 'child_process';
|
||||
import path from 'path';
|
||||
|
||||
test.describe.serial('Response Example Menu Operations', () => {
|
||||
test.setTimeout(1 * 60 * 1000); // 1 minute for all tests in this describe block, default is 30 seconds.
|
||||
test.afterAll(async () => {
|
||||
// Reset the collection request file to the original state
|
||||
execSync(`git checkout -- ${path.join(__dirname, 'fixtures', 'collection', 'menu-operations.bru')}`);
|
||||
@@ -15,7 +16,7 @@ test.describe.serial('Response Example Menu Operations', () => {
|
||||
});
|
||||
|
||||
await test.step('Create example', async () => {
|
||||
await page.locator('#send-request').getByRole('img').nth(2).click();
|
||||
await page.getByTestId('send-arrow-icon').click();
|
||||
await page.getByTestId('response-bookmark-btn').click();
|
||||
await page.getByTestId('create-example-name-input').clear();
|
||||
await page.getByTestId('create-example-name-input').fill('Example to Clone');
|
||||
|
||||
@@ -21,7 +21,12 @@ export const buildCommonLocators = (page: Page) => ({
|
||||
collectionActions: (collectionName: string) =>
|
||||
page.locator('.collection-name')
|
||||
.filter({ hasText: collectionName })
|
||||
.locator('.collection-actions .icon')
|
||||
.locator('.collection-actions .icon'),
|
||||
collectionItemActions: (itemName: string) =>
|
||||
page.locator('.collection-item-name')
|
||||
.filter({ hasText: itemName })
|
||||
.locator('.menu-icon')
|
||||
|
||||
},
|
||||
dropdown: {
|
||||
item: (text: string) => page.locator('.dropdown-item').filter({ hasText: text })
|
||||
|
||||
Reference in New Issue
Block a user