Skip to content
Merged
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
33 changes: 33 additions & 0 deletions packages/bruno-api-docs/e2e/pages/graphql-request.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { Locator } from '@playwright/test';
import { BasePage } from './base.page';
import { SidebarComponent } from '../components/sidebar.component';
import { BreadcrumbComponent } from '../components/breadcrumb.component';
import { RequestUrlBarComponent } from '../components/request/url-bar.component';
import { CodeSnippetComponent } from '../components/request/code-snippet.component';
import { ExecutionContextComponent } from '../components/request/execution-context.component';

export class GraphqlRequestPage extends BasePage {
readonly root = this.page.getByTestId('graphql-request-page');
readonly title = this.page.getByTestId('request-title');
readonly description = this.page.getByTestId('request-description');

readonly sidebar = new SidebarComponent(this.page);
readonly breadcrumb = new BreadcrumbComponent(this.page, 'request-breadcrumb');
readonly urlBar = new RequestUrlBarComponent(this.page);
Comment thread
sachin-bruno marked this conversation as resolved.
readonly codeSnippet = new CodeSnippetComponent(this.page);
readonly executionContext = new ExecutionContextComponent(this.page);

readonly query = this.page.getByTestId('request-graphql-query');
readonly variables = this.page.getByTestId('request-graphql-variables');

async open(path: string[]): Promise<void> {
await this.navigate('/');
await this.sidebar.open(path);
await this.root.waitFor({ state: 'visible' });
}

section(label: string): Locator {
const slug = label.toLowerCase().replace(/\s+/g, '-');
return this.page.getByTestId(`request-section-${slug}`);
}
}
5 changes: 5 additions & 0 deletions packages/bruno-api-docs/e2e/playwright/pages.fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { test as base } from '@playwright/test';
import { OverviewPage } from '../pages/overview.page';
import { EnvironmentsPage } from '../pages/environments.page';
import { RequestPage } from '../pages/request.page';
import { GraphqlRequestPage } from '../pages/graphql-request.page';
import { ScriptPage } from '../pages/script.page';
import { FolderPage } from '../pages/folder.page';
import { UnsupportedRequestPage } from '../pages/unsupported-request.page';
Expand All @@ -22,6 +23,7 @@ type Fixtures = {
overviewPage: OverviewPage;
environmentsPage: EnvironmentsPage;
requestPage: RequestPage;
graphqlRequestPage: GraphqlRequestPage;
scriptPage: ScriptPage;
folderPage: FolderPage;
unsupportedRequestPage: UnsupportedRequestPage;
Expand Down Expand Up @@ -49,6 +51,9 @@ export const test = base.extend<Fixtures>({
requestPage: async ({ page }, use) => {
await use(new RequestPage(page));
},
graphqlRequestPage: async ({ page }, use) => {
await use(new GraphqlRequestPage(page));
},
scriptPage: async ({ page }, use) => {
await use(new ScriptPage(page));
},
Expand Down
6 changes: 3 additions & 3 deletions packages/bruno-api-docs/e2e/tests/overview/overview.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ test.describe('Collection Overview', () => {
});
});

test('shows three stat cards with the request (48), folder (7) and environment (2) counts', async ({ overviewPage }) => {
test('shows three stat cards with the request (61), folder (14) and environment (2) counts', async ({ overviewPage }) => {
await expect(overviewPage.stats.cards).toHaveCount(3);
await expect(overviewPage.stats.valueFor('Requests')).toHaveText('48');
await expect(overviewPage.stats.valueFor('Folders')).toHaveText('7');
await expect(overviewPage.stats.valueFor('Requests')).toHaveText('61');
await expect(overviewPage.stats.valueFor('Folders')).toHaveText('14');
await expect(overviewPage.stats.valueFor('Environments')).toHaveText('2');
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const openAt = (dock: string): string => `/#/?pg=1&dock=${dock}`;

const UNSUPPORTED = [
{ paths: ['Realtime', 'Live Updates'], name: 'Live Updates', typeLabel: 'Websocket' },
{ paths: ['Realtime', 'GraphQL API'], name: 'GraphQL API', typeLabel: 'GraphQL' },
{ paths: ['Realtime', 'GraphQL Details'], name: 'GraphQL Details', typeLabel: 'GraphQL' },
{ paths: ['Realtime', 'Order Service'], name: 'Order Service', typeLabel: 'gRPC' }
];

Expand Down
60 changes: 60 additions & 0 deletions packages/bruno-api-docs/e2e/tests/request/graphql-request.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { test, expect } from '../../playwright';

const GRAPHQL_DETAILS = ['Realtime', 'GraphQL Details'];

test.describe('Request page — GraphQL', () => {
test.beforeEach(async ({ graphqlRequestPage }) => {
await graphqlRequestPage.open(GRAPHQL_DETAILS);
});

test('shows the POST method, the endpoint url and the request name', async ({ graphqlRequestPage }) => {
await expect(graphqlRequestPage.title).toHaveText('GraphQL Details');
await expect(graphqlRequestPage.urlBar.method).toHaveText('POST');
await expect(graphqlRequestPage.urlBar.url).toContainText('api.example.com/graphql');
});

test('shows the breadcrumb with the parent folder and the current request', async ({ graphqlRequestPage }) => {
await expect(graphqlRequestPage.breadcrumb.current).toHaveText('GraphQL Details');
await expect(graphqlRequestPage.breadcrumb.segment('Realtime')).toBeVisible();
});

test('does not offer a Try button (the interactive playground does not support graphql yet)', async ({ graphqlRequestPage }) => {
await expect(graphqlRequestPage.urlBar.tryButton).toHaveCount(0);
});

test('renders the GraphQL query and variables instead of a Body section', async ({ graphqlRequestPage, page }) => {
await expect(graphqlRequestPage.query).toBeVisible();
await expect(graphqlRequestPage.query).toContainText('country');
await expect(graphqlRequestPage.variables).toBeVisible();
await expect(graphqlRequestPage.variables).toContainText('countryCode');
await expect(page.getByTestId('request-section-body')).toHaveCount(0);
});

test('reuses the Headers and Auth sections from the request page', async ({ graphqlRequestPage }) => {
const headers = graphqlRequestPage.section('Headers');
await expect(headers).toBeVisible();
await expect(headers.getByText('x-api-key')).toBeVisible();

const auth = graphqlRequestPage.section('Auth');
await expect(auth).toBeVisible();
await expect(auth).toContainText('Basic');
await expect(auth).toContainText('user@example.com');
});

test('builds a GraphQL POST code snippet from the query and variables', async ({ graphqlRequestPage }) => {
await expect(graphqlRequestPage.codeSnippet.code).toContainText('graphql');
await expect(graphqlRequestPage.codeSnippet.code).toContainText('query');
});

test('shows the Execution Context variables, including set-variable actions', async ({ graphqlRequestPage }) => {
await graphqlRequestPage.executionContext.openTab('variables');
await expect(graphqlRequestPage.executionContext.variable('countryCode')).toBeVisible();
await expect(graphqlRequestPage.executionContext.variable('countryName')).toBeVisible();
});

test('renders the request description and no Params section for a request without params', async ({ graphqlRequestPage, page }) => {
await expect(graphqlRequestPage.description).toBeVisible();
await expect(graphqlRequestPage.description).toContainText('country');
await expect(page.getByTestId('request-section-params')).toHaveCount(0);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ test.describe('Request page — Examples', () => {
await expect(examples.statusCode(BAD_REQUEST_EXAMPLE)).toHaveText('400');
});

test('an example code snippet carries the request\'s resolved auth', async ({ requestPage }) => {
const { examples } = requestPage;
await examples.openSnippet(OK_EXAMPLE);
await expect(examples.snippetCode).toContainText('Bearer');
});

test.describe('Request pane', () => {
test('shows the query parameters by default', async ({ requestPage }) => {
const { examples } = requestPage;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { test, expect } from '../../playwright';

const UNSUPPORTED_REQUESTS = [
{ paths: ['Realtime', 'Live Updates'], name: 'Live Updates', typeLabel: 'Websocket', shortName: 'WS', url: '/ws/updates' },
{ paths: ['Realtime', 'GraphQL API'], name: 'GraphQL API', typeLabel: 'GraphQL', shortName: 'GQL', url: '/graphql' }
{ paths: ['Realtime', 'Live Updates'], name: 'Live Updates', typeLabel: 'Websocket', shortName: 'WS', url: '/ws/updates' }
];

test.describe('Request page — unsupported request types', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ describe('ScriptChain', () => {
});

it('numbers every row 1..N in display order, including the HTTP marker', () => {
const html = renderToStaticMarkup(<ScriptChain steps={postChain} flow="sandwich" url="http://x" method="POST" />);
const html = renderToStaticMarkup(<ScriptChain steps={postChain} flow="sandwich" url="http://x" />);
expect(html).toContain('HTTP');
// 3 post-response steps + 1 marker → positions 1..4 are present.
['>1<', '>2<', '>3<', '>4<'].forEach((n) => expect(html).toContain(n));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const StyledWrapper = styled.div`
}

.vars-field-label {
text-transform: uppercase;
text-transform: none;
}

&.vars-stacked {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export const FolderConfiguration: React.FC<FolderConfigurationProps> = ({
{hasHeaders && (
<div className="config-group" data-testid="folder-config-headers" data-nav-section="Headers" data-nav-level={2}>
<div className="config-group-head">
<SectionLabel className="config-group-label">Headers</SectionLabel>
<SectionLabel className="config-group-label section-label-lower">Headers</SectionLabel>
{hasInheritedHeaders && (
<ContentTypeBadge label={inheritedCountLabel(config.inheritedHeaders.length, 'header')} />
)}
Expand All @@ -66,7 +66,7 @@ export const FolderConfiguration: React.FC<FolderConfigurationProps> = ({
{hasAuth && (
<div className="config-group" data-testid="folder-config-auth" data-nav-section="Auth" data-nav-level={2}>
<div className="config-group-head">
<SectionLabel className="config-group-label">Auth</SectionLabel>
<SectionLabel className="config-group-label section-label-lower">Auth</SectionLabel>
{authBadge}
</div>
<AuthDetails auth={config.auth} authModeLabels={authModeLabels} testId="folder-config-auth-details" />
Expand All @@ -76,7 +76,7 @@ export const FolderConfiguration: React.FC<FolderConfigurationProps> = ({
{hasVariables && (
<div className="config-group" data-testid="folder-config-vars" data-nav-section="Vars" data-nav-level={2}>
<div className="config-group-head">
<SectionLabel className="config-group-label">Vars</SectionLabel>
<SectionLabel className="config-group-label section-label-lower">Vars</SectionLabel>
{inheritedVarCount > 0 && <ContentTypeBadge label={inheritedCountLabel(inheritedVarCount, 'var')} />}
</div>
<div className="config-columns">
Expand All @@ -99,7 +99,7 @@ export const FolderConfiguration: React.FC<FolderConfigurationProps> = ({
{hasScripts && (
<div className="config-group" data-testid="folder-config-script" data-nav-section="Script" data-nav-level={2}>
<div className="config-group-head">
<SectionLabel className="config-group-label">Script</SectionLabel>
<SectionLabel className="config-group-label section-label-lower">Script</SectionLabel>
</div>
<div className="config-columns">
{config.preRequest && (
Expand All @@ -121,7 +121,7 @@ export const FolderConfiguration: React.FC<FolderConfigurationProps> = ({
{hasTests && (
<div className="config-group" data-testid="folder-config-tests" data-nav-section="Tests" data-nav-level={2}>
<div className="config-group-head">
<SectionLabel className="config-group-label">Tests</SectionLabel>
<SectionLabel className="config-group-label section-label-lower">Tests</SectionLabel>
</div>
<Code code={config.tests as string} language="javascript" showLineNumbers testId="folder-config-tests-code" />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const StyledWrapper = styled.div`
font-size: 0.625rem;
line-height: 1;
letter-spacing: 0.0525rem;
text-transform: uppercase;
text-transform: none;
color: var(--text-tertiary);
}
`;
13 changes: 13 additions & 0 deletions packages/bruno-api-docs/src/components/PageRouter/PageRouter.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useMemo, useRef } from 'react';
import { Navigate } from 'react-router-dom';
import type { GraphQLRequest } from '@opencollection/types/requests/graphql';
import type { ScriptFile, Folder as FolderItem } from '@opencollection/types/collection/item';
import type { RequestItem } from '../../utils/schemaHelpers';
import { useActiveResolution, useNavModel } from '../../routing/hooks';
Expand All @@ -17,6 +18,7 @@ import { ErrorBoundary } from '../ErrorBoundary/ErrorBoundary';
import { StyledWrapper } from './StyledWrapper';
import { Overview } from '../../pages/Overview/Overview';
import Request from '../../pages/Request/Request';
import GraphqlRequest from '@/pages/GraphqlRequest/GraphqlRequest';
import Script from '../../pages/Script/Script';
import Folder from '../../pages/Folder/Folder';
import Environments from '../../pages/Environments/Environments';
Expand Down Expand Up @@ -88,6 +90,17 @@ const PageRouter: React.FC<PageRouterProps> = ({ onOpenPlayground, testId = 'pag
return item ? (
<Script item={item as ScriptFile} ancestry={ancestry} collection={collection} onBreadcrumbClick={goToUuid} />
) : null;
case 'graphql':
return item ? (
<ItemVariableResolverProvider collection={collection} ancestry={ancestry} item={item as Item}>
<GraphqlRequest
item={item as GraphQLRequest}
ancestry={ancestry}
collection={collection}
onBreadcrumbClick={goToUuid}
/>
</ItemVariableResolverProvider>
) : null;
case 'request':
default:
return item ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ describe('LargeResponseWarning', () => {
const response: RunRequestResponse = { data: { hello: 'world' }, base64Data: 'aGVsbG8=' };

it('renders the warning icon', () => {
const root = useRenderToDom(<LargeResponseWarning responseSize={responseSize} onReveal={() => {}} />);
const root = useRenderToDom(
<LargeResponseWarning responseSize={responseSize} response={response} onReveal={() => {}} />
);
query(root, '.warning-icon');
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const ENV_TABS = [

type EnvTabId = (typeof ENV_TABS)[number]['id'];

const SECRET_POINTER_FIELD: Record<SecretProviderType, 'path' | 'secretName' | 'vaultName' | 'projectId'> = {
const SECRET_POINTER_FIELD: Record<SecretProviderType | 'gcp-secrets-manager', 'path' | 'secretName' | 'vaultName' | 'projectId'> = {
'hashicorp-vault-cloud': 'path',
'hashicorp-vault-server': 'path',
'aws-secrets-manager': 'secretName',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import RequestPane from './RequestPane/RequestPane';
import ResponsePane from './ResponsePane/ResponsePane';
import { useAppDispatch, useAppSelector } from '../../../../../store/hooks';
import { updatePlaygroundItem, setPlaygroundResponse, selectPlaygroundResponse } from '../../../../../store/slices/playground';
import { getItemName, isUnsupportedRequestInPlayground } from '../../../../../utils/schemaHelpers';
import { getItemName, isPlaygroundUnsupported } from '@/utils/schemaHelpers';
import { getInheritedAuthSummary } from '../../../../../utils/request';
import UnsupportedRequest from '../../../../UnsupportedRequest/UnsupportedRequest';
import { FileNotFoundIcon } from '../../../../../assets/icons';
Expand Down Expand Up @@ -152,7 +152,7 @@ const HttpRequestPlaygroundView: React.FC<PlaygroundViewProps> = ({ item, collec
};

const PlaygroundView: React.FC<PlaygroundViewProps> = ({ item, ...otherProps }) => {
if (isUnsupportedRequestInPlayground(item)) {
if (isPlaygroundUnsupported(item)) {
return (
<UnsupportedRequest
className="px-5"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,21 +51,20 @@ describe('PropertyTable', () => {
expect(root.querySelector('.property-type')).toBeNull();
});

it('renders the inherited-source link pinned at the end of the value line, after the disabled badge', () => {
it('renders the inherited-source link as a row-level cell (sibling of the value line) so it can center across the whole row', () => {
const root = useRenderToDom(
<PropertyTable
rows={[{ label: 'X-Trace', value: 'abc', disabled: true, inheritedSource: { level: 'folder', name: 'Parent', uuid: 'p1' } }]}
onNavigate={() => {}}
/>
);
const line = query(root, '.property-value-line');
expect(line.querySelector('[data-testid="inherited-source"]')).not.toBeNull();
const html = line.innerHTML;
expect(html.indexOf('disabled-badge')).toBeLessThan(html.indexOf('data-testid="inherited-source"'));
expect(query(root, '.property-row').querySelector('[data-testid="inherited-source"]')).not.toBeNull();
expect(query(root, '.property-value-line').querySelector('[data-testid="inherited-source"]')).toBeNull();
expect(query(root, '.property-value-line').querySelector('.disabled-badge')).not.toBeNull();
});

it('omits the inherited-source link when a row is not inherited', () => {
const root = useRenderToDom(<PropertyTable rows={[{ label: 'Accept', value: 'application/json' }]} />);
expect(query(root, '.property-value-line').querySelector('[data-testid="inherited-source"]')).toBeNull();
expect(query(root, '.property-row').querySelector('[data-testid="inherited-source"]')).toBeNull();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,11 @@ export const PropertyTable: React.FC<PropertyTableProps> = ({ rows, emptyMessage
<div className="property-value-main" data-testid="property-value"><ValueCell row={row} testId={testId} /></div>
{row.type ? <span className="property-type">{row.type}</span> : null}
{row.disabled ? <DisabledBadge /> : null}
{row.inheritedSource ? (
<InheritedSourceLink source={row.inheritedSource} itemName={row.label} onNavigate={onNavigate} />
) : null}
</div>
</dd>
{row.inheritedSource ? (
<InheritedSourceLink source={row.inheritedSource} itemName={row.label} onNavigate={onNavigate} />
) : null}
<Description text={row.description} />
</div>
))}
Expand Down
Loading
Loading