Skip to content
Closed
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 @@ -11,17 +11,23 @@ export class CodeEditorComponent extends BaseComponent {
readonly suggestions: Locator;
private readonly surface: Locator;
private readonly lines: Locator;
private readonly ready: Locator;

constructor(page: Page, testId: string) {
super(page, page.getByTestId(testId));
this.copyButton = this.root.getByTestId(`${testId}-copy`);
this.surface = this.root.locator('.monaco-editor');
this.lines = this.root.locator('.view-lines');
this.suggestions = page.locator('.suggest-widget.visible');
this.ready = page.locator(`[data-testid="${testId}"][data-editor-ready="true"]`);
}

async focus(): Promise<void> {
await this.surface.waitFor({ state: 'visible', timeout: 20000 });
// Autocomplete needs the editor's model tagged with its Bruno API roots. Tagging can lag the
// visible surface when the editor first mounts inside a hidden tab panel, so wait for the
// editor to report ready before typing — otherwise the trigger fires against an untagged model.
await this.ready.waitFor({ state: 'attached', timeout: 20000 });
await this.lines.click();
}
}
11 changes: 5 additions & 6 deletions packages/oc-docs/e2e/components/playground.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ import type { Locator } from '@playwright/test';
import { BaseComponent } from './base.component';
import { KeyValueTableComponent } from './key-value-table/key-value-table.component';
import { CodeEditorComponent } from './code-editor/code-editor.component';
import { RequestAuthComponent } from './playground/auth.component';
import type { DockMode } from '../../src/utils/playgroundDock';

export class PlaygroundComponent extends BaseComponent {
readonly keyValueTable = new KeyValueTableComponent(this.page);
// The Auth tab lives inside the playground request pane; open it with selectTab('auth').
readonly auth = new RequestAuthComponent(this.page);
readonly preRequestScriptEditor = new CodeEditorComponent(this.page, 'scripts-editor-pre-request');
readonly postResponseScriptEditor = new CodeEditorComponent(this.page, 'scripts-editor-post-response');
readonly bodyEditor = new CodeEditorComponent(this.page, 'body-editor');
Expand Down Expand Up @@ -55,10 +58,6 @@ export class PlaygroundComponent extends BaseComponent {
return this.sidebarPanel.getByTestId('sidebar-example').filter({ hasText: exampleName });
}

async open(dock: DockMode = 'bottom'): Promise<void> {
await this.page.goto(`/#/?pg=1&dock=${dock}`);
}

sidebarItem(name: string): Locator {
return this.treeItems.filter({ hasText: name }).first();
}
Expand Down Expand Up @@ -89,8 +88,8 @@ export class PlaygroundComponent extends BaseComponent {
return this.page.getByTestId(`playground-dock-${mode}-panel`);
}

async open(mode: DockMode): Promise<void> {
await this.page.goto(`/#/?pg=1&dock=${mode}`);
async open(dock: DockMode = 'bottom'): Promise<void> {
await this.page.goto(`/#/?pg=1&dock=${dock}`);
await this.runner.waitFor({ state: 'visible' });
}

Expand Down
24 changes: 24 additions & 0 deletions packages/oc-docs/e2e/components/playground/auth.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { Locator } from '@playwright/test';
import { BaseComponent } from '../base.component';

// The Auth tab inside the playground request pane: the auth-mode dropdown, its options,
// and the notice shown when a request inherits its auth. Open the tab via
// playground.selectTab('auth').
export class RequestAuthComponent extends BaseComponent {
readonly modeSelect = this.page.getByTestId('auth-mode-select');
readonly inheritNotice = this.page.getByTestId('auth-inherit-notice');
readonly options = this.page.getByTestId(/^auth-mode-select-(?!dropdown$)/);

option(value: string): Locator {
return this.page.getByTestId(`auth-mode-select-${value}`);
}

async open(): Promise<void> {
await this.modeSelect.click();
}

async selectMode(value: string): Promise<void> {
await this.modeSelect.click();
await this.option(value).click();
}
}
46 changes: 46 additions & 0 deletions packages/oc-docs/e2e/tests/playground/playground-auth.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { test, expect } from '../../playwright';

const DESKTOP = { width: 1280, height: 900 };

test.describe('Playground request — inherited auth', () => {
test.use({ viewport: DESKTOP });

test('the auth dropdown offers Inherit alongside the four supported types, and nothing more', async ({
playground
}) => {
await playground.open('bottom');
await playground.openRequest('get users');
await playground.selectTab('auth');

await playground.auth.open();
await expect(playground.auth.option('none')).toBeVisible();
await expect(playground.auth.option('inherit')).toBeVisible();
await expect(playground.auth.option('basic')).toBeVisible();
await expect(playground.auth.option('bearer')).toBeVisible();
await expect(playground.auth.option('apikey')).toBeVisible();
// No Auth + Inherit + Basic + Bearer + API Key — no other auth types are offered.
await expect(playground.auth.options).toHaveCount(5);
});

test('choosing Inherit shows the inherited-auth notice and marks the mode selected', async ({
playground
}) => {
await playground.open('bottom');
await playground.openRequest('get users');
await playground.selectTab('auth');

await playground.auth.selectMode('inherit');
await expect(playground.auth.modeSelect).toContainText('Inherit');
await expect(playground.auth.inheritNotice).toBeVisible();
});

test('a request already set to inherit opens with Inherit preselected', async ({ playground }) => {
await playground.open('bottom');
// Get All Customers inherits its auth from the Bearer-authed collection.
await playground.openTreeItem(['billing', 'customers', 'Get All Customers']);
await playground.selectTab('auth');

await expect(playground.auth.modeSelect).toContainText('Inherit');
await expect(playground.auth.inheritNotice).toBeVisible();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,18 @@ describe('AuthTab', () => {
expect(getByTestId(root, 'no-content-text').text.trim()).toBe('No authentication configured.');
});

it('shows an inherit note for inherited auth', () => {
it('names the inherited source and its effective mode, matching the app', () => {
const { root, getByTestId } = renderAuth('inherit', {
showInherit: true,
inheritedAuth: { sourceName: 'Users', modeLabel: 'Bearer Token' }
});
expect(query(root, '.auth-empty').text.replace(/\s+/g, ' ').trim()).toBe('Auth inherited from Users: Bearer Token');
expect(getByTestId('inherited-auth-mode').text.trim()).toBe('Bearer Token');
});

it('falls back to a generic inherit note when the source is not resolved', () => {
const { root } = renderAuth('inherit', { showInherit: true });
expect(getByTestId(root, 'no-content-text').text.trim()).toBe('Inherits auth from parent collection.');
expect(query(root, '.auth-empty').text.trim()).toBe('Auth inherited from the parent folder or collection.');
});

it('renders basic auth with a plain username and a masked, revealable password', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import { Field, type SelectOption } from '../../../../../../ui/Field';
import { AUTH_DEFAULTS, AUTH_MODE_LABELS, PLACEMENT_OPTIONS } from '../../../../../../constants';
import { REQUEST_PROTOCOL_KEYS } from '../../../../../../utils/schemaHelpers';
import type { InheritedAuthSummary } from '../../../../../../utils/request';
import { StyledWrapper } from './StyledWrapper';
import NoContentText from '../../../../../../ui/NoContentText/NoContentText';

Expand All @@ -14,6 +15,7 @@ interface AuthTabProps {
description?: string;
showInherit?: boolean;
showFullAuth?: boolean;
inheritedAuth?: InheritedAuthSummary;
}

export const AuthTab: React.FC<AuthTabProps> = ({
Expand All @@ -24,7 +26,8 @@ export const AuthTab: React.FC<AuthTabProps> = ({
title,
description,
showInherit = false,
showFullAuth = false
showFullAuth = false,
inheritedAuth
}) => {
const authType = typeof auth === 'object' && auth !== null ? auth.type : auth === 'inherit' ? 'inherit' : 'none';

Expand Down Expand Up @@ -139,7 +142,20 @@ export const AuthTab: React.FC<AuthTabProps> = ({
return <NoContentText text='No authentication configured.' />
}
if (auth === 'inherit') {
return <NoContentText text='Inherits auth from parent collection.' />
return (
<p className="auth-empty" data-testid="auth-inherit-notice">
{inheritedAuth ? (
<>
Auth inherited from {inheritedAuth.sourceName}:{' '}
<span className="auth-inherited-mode" data-testid="inherited-auth-mode">
{inheritedAuth.modeLabel}
</span>
</>
) : (
'Auth inherited from the parent folder or collection.'
)}
</p>
);
}
if (showFullAuth) {
return <div className="auth-form">{renderForm()}</div>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@ export const StyledWrapper = styled.div`
gap: 1rem;
}

.auth-empty {
margin: 0;
font-style: italic;
font-size: 0.8125rem;
color: var(--text-muted);
}

.auth-inherited-mode {
color: var(--primary-text);
}

.auth-form {
display: flex;
flex-direction: column;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useMemo, useState } from 'react';
import type { Folder } from '@opencollection/types/collection/item';
import type { OpenCollection } from '@opencollection/types';
import Tabs from '../../../../../ui/Tabs/Tabs';
Expand All @@ -18,6 +18,9 @@ import {
scriptsArrayToObject,
scriptsObjectToArray
} from '../../../../../utils/schemaHelpers';
import { getAncestorsByUuid } from '../../../../../utils/fileUtils';
import { getItemUuid } from '../../../../../utils/itemUtils';
import { getInheritedAuthSummary } from '../../../../../utils/request';
import TestsTab from '../Common/TestsTab/TestsTab';
import OverviewTab from '../Common/OverviewTab/OverviewTab';

Expand All @@ -27,10 +30,17 @@ interface FolderSettingsProps {
onFolderChange: (updatedFolder: Folder) => void;
}

const FolderSettings: React.FC<FolderSettingsProps> = ({ folder, onFolderChange }) => {
const FolderSettings: React.FC<FolderSettingsProps> = ({ folder, collection, onFolderChange }) => {
const dispatch = useAppDispatch();
const [activeTab, setActiveTab] = useState('overview');

const inheritedAuth = useMemo(() => {
if (folder.request?.auth !== 'inherit') return undefined;
const uuid = getItemUuid(folder);
const ancestry = uuid ? getAncestorsByUuid(collection, uuid) : [];
return getInheritedAuthSummary(collection, ancestry, folder);
}, [folder, collection]);

const handleHeadersChange = (headers: KeyValueRow[]) => {
const originals = folder.request?.headers ?? [];
const originalByName = new Map(
Expand Down Expand Up @@ -160,6 +170,7 @@ const FolderSettings: React.FC<FolderSettingsProps> = ({ folder, onFolderChange
description="Configures authentication for this folder. This applies to all requests using the Inherit option in the Auth tab."
showInherit={true}
showFullAuth={true}
inheritedAuth={inheritedAuth}
/>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import ResponsePane from './ResponsePane/ResponsePane';
import { useAppDispatch, useAppSelector } from '../../../../../store/hooks';
import { updatePlaygroundItem, setPlaygroundResponse, selectPlaygroundResponse } from '../../../../../store/slices/playground';
import { getItemName, isUnsupportedRequest } from '../../../../../utils/schemaHelpers';
import { getInheritedAuthSummary } from '../../../../../utils/request';
import UnsupportedRequest from '../../../../UnsupportedRequest/UnsupportedRequest';
import { FileNotFoundIcon } from '../../../../../assets/icons';
import { useSplitPane } from '../../../../../hooks/useSplitPane';
Expand Down Expand Up @@ -39,6 +40,10 @@ const HttpRequestPlaygroundView: React.FC<PlaygroundViewProps> = ({ item, collec
() => (collection && itemUuid ? getAncestorsByUuid(collection, itemUuid) : []),
[collection, itemUuid]
);
const inheritedAuth = useMemo(
() => getInheritedAuthSummary(collection, ancestry, editableItem),
[collection, ancestry, editableItem]
);
const saveTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const pendingSaveRef = useRef<{ uuid: string; item: HttpRequest } | null>(null);

Expand Down Expand Up @@ -127,7 +132,7 @@ const HttpRequestPlaygroundView: React.FC<PlaygroundViewProps> = ({ item, collec
: { width: `${paneSize}%`, borderColor: 'var(--border-color)' }
}
>
<RequestPane item={editableItem} onItemChange={handleItemChange} />
<RequestPane item={editableItem} onItemChange={handleItemChange} inheritedAuth={inheritedAuth} />
</div>

<SplitDivider orientation={orientation} onPointerDown={startResize} active={isResizing} testId="playground-divider" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,15 @@ import {
getItemDocs
} from '../../../../../../utils/schemaHelpers';
import { setUrlQueryParams } from '../../../../../../utils/pathParams';
import { actionsToPostResponseVars, postResponseVarsToActions } from '../../../../../../utils/request';
import { actionsToPostResponseVars, postResponseVarsToActions, type InheritedAuthSummary } from '../../../../../../utils/request';

interface RequestPaneProps {
item: HttpRequest;
onItemChange: (item: HttpRequest) => void;
inheritedAuth?: InheritedAuthSummary;
}

const RequestPane: React.FC<RequestPaneProps> = ({ item, onItemChange }) => {
const RequestPane: React.FC<RequestPaneProps> = ({ item, onItemChange, inheritedAuth }) => {
const [activeTab, setActiveTab] = useState('overview');

const handleParamsChange = (params: KeyValueRow[]) => {
Expand Down Expand Up @@ -174,8 +175,10 @@ const RequestPane: React.FC<RequestPaneProps> = ({ item, onItemChange }) => {
onAuthChange={() => {}} // Not used for full auth
onItemChange={onItemChange}
item={item}
showFullAuth={true}
description="Configures authentication for this request."
showInherit={true}
showFullAuth={true}
inheritedAuth={inheritedAuth}
/>
);

Expand Down
Loading
Loading