Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -14,23 +14,20 @@ const formatVersion = (version) => {
return coerced ? `v${coerced.version}` : DEFAULT_VERSION;
};

/**
* Read-only display of the collection's current version and a summary of its
* contents (folder + request counts). Presentational and prop-driven so it can be
* reused wherever the collection version needs to be shown.
*/
const CollectionVersionInfo = ({ version, folderCount = 0, requestCount = 0 }) => {
const CollectionVersionInfo = ({ name, version, folderCount = 0, requestCount = 0 }) => {
const folderLabel = folderCount === 1 ? 'Folder' : 'Folders';
const requestLabel = requestCount === 1 ? 'request' : 'requests';

return (
<div className="version-info" data-testid="version-info">
<div className="version-line">
<span className="version-label">Collection Version:</span>{' '}
<span className="collection-name" data-testid="collection-name">{name}</span>
<span className="version-value" data-testid="version-value">{formatVersion(version)}</span>
</div>
<p className="version-summary" data-testid="version-summary">
{`${folderCount} ${folderLabel} • ${requestCount} ${requestLabel}`}
<span>{`${folderCount} ${folderLabel}`}</span>
<span className="version-dot" aria-hidden="true" />
<span>{`${requestCount} ${requestLabel}`}</span>
</p>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import '@testing-library/jest-dom';
import React from 'react';
import { render, screen } from '@testing-library/react';
import CollectionVersionInfo from './index';

describe('CollectionVersionInfo', () => {
describe('collection name', () => {
it('renders the collection name in the collection-name slot', () => {
render(<CollectionVersionInfo name="Hotel Booking API" version="1.2.0" />);
expect(screen.getByTestId('collection-name')).toHaveTextContent('Hotel Booking API');
});

it('keeps the name and the version in separate slots', () => {
render(<CollectionVersionInfo name="Hotel Booking API" version="1.2.0" />);

const nameSlot = screen.getByTestId('collection-name');
const versionSlot = screen.getByTestId('version-value');

// Name lives only in the name slot, version only in the version slot.
expect(nameSlot).toHaveTextContent('Hotel Booking API');
expect(nameSlot).not.toHaveTextContent('v1.2.0');
expect(versionSlot).toHaveTextContent('v1.2.0');
expect(versionSlot).not.toHaveTextContent('Hotel Booking API');
});
});

describe('version formatting', () => {
it('renders a full semver with a v prefix', () => {
render(<CollectionVersionInfo name="API" version="1.2.0" />);
expect(screen.getByTestId('version-value')).toHaveTextContent('v1.2.0');
});

it('coerces a bare major version to major.minor.patch', () => {
render(<CollectionVersionInfo name="API" version="1" />);
expect(screen.getByTestId('version-value')).toHaveTextContent('v1.0.0');
});

it('coerces a partial major.minor version to major.minor.patch', () => {
render(<CollectionVersionInfo name="API" version="2.1" />);
expect(screen.getByTestId('version-value')).toHaveTextContent('v2.1.0');
});

it('does not double-prefix an already v-prefixed version', () => {
render(<CollectionVersionInfo name="API" version="v1.2.0" />);
const versionSlot = screen.getByTestId('version-value');
expect(versionSlot).toHaveTextContent('v1.2.0');
expect(versionSlot).not.toHaveTextContent('vv');
});

it('preserves a pre-release suffix', () => {
render(<CollectionVersionInfo name="API" version="1.0.0-beta" />);
expect(screen.getByTestId('version-value')).toHaveTextContent('v1.0.0-beta');
});

it('falls back to the default version when unset', () => {
render(<CollectionVersionInfo name="API" />);
expect(screen.getByTestId('version-value')).toHaveTextContent('v1.0.0');
});

it('falls back to the default version when unparseable', () => {
render(<CollectionVersionInfo name="API" version="not-a-version" />);
expect(screen.getByTestId('version-value')).toHaveTextContent('v1.0.0');
});
});

describe('content summary', () => {
it('pluralises folder and request counts', () => {
render(<CollectionVersionInfo name="API" version="1.0.0" folderCount={2} requestCount={5} />);
// Exact-match each count span so a singular/plural regression is caught.
expect(screen.getByText('2 Folders')).toBeInTheDocument();
expect(screen.getByText('5 requests')).toBeInTheDocument();
});

it('uses singular labels for a single folder and request', () => {
render(<CollectionVersionInfo name="API" version="1.0.0" folderCount={1} requestCount={1} />);
expect(screen.getByText('1 Folder')).toBeInTheDocument();
expect(screen.getByText('1 request')).toBeInTheDocument();
});

it('defaults the counts to zero', () => {
render(<CollectionVersionInfo name="API" version="1.0.0" />);
expect(screen.getByText('0 Folders')).toBeInTheDocument();
expect(screen.getByText('0 requests')).toBeInTheDocument();
});

it('renders the separator as an aria-hidden decorative dot', () => {
const { container } = render(
<CollectionVersionInfo name="API" version="1.0.0" folderCount={2} requestCount={5} />
);
const dot = container.querySelector('.version-dot');
expect(dot).toBeInTheDocument();
expect(dot).toHaveAttribute('aria-hidden', 'true');
// Decorative only — carries no text.
expect(dot).toHaveTextContent('');
});
});
Comment thread
sachin-bruno marked this conversation as resolved.
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import React, { useCallback, useEffect, useMemo, useRef, memo } from 'react';
import { Virtuoso } from 'react-virtuoso';
import ColorBadge from 'components/ColorBadge';

// Show at most 5 environments at a glance; the list virtualises and scrolls beyond
// Show at most 6 environments at a glance; the list virtualises and scrolls beyond
// that, so it stays performant even for collections with hundreds of environments
// (only the visible rows are ever in the DOM).
const MAX_VISIBLE_ROWS = 5;
const MAX_VISIBLE_ROWS = 6;

// Fixed row height (px). MUST stay in sync with the `.env-row` height in StyledWrapper.js,
// since it is passed to Virtuoso as `fixedItemHeight`.
const ENV_ROW_HEIGHT = 34;
const ENV_ROW_HEIGHT = 28;

/**
* A selectable, virtualised list of collection environments (checkbox + color dot + name)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,33 +14,57 @@ const StyledWrapper = styled.div`
}

.config-card {
border: 1px solid ${(props) => props.theme.border.border1};
border: 1px solid ${(props) => props.theme.table.border};
border-radius: ${(props) => props.theme.border.radius.md};
overflow: hidden;
width: 100%;

.version-info {
padding: 0.75rem 1rem;
background-color: ${(props) => props.theme.background.mantle};

.version-line {
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.text};
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
}

.version-label {
.collection-name {
font-weight: 500;
font-size: ${(props) => props.theme.font.size.base};
color: ${(props) => props.theme.text};
min-width: 0;
}

.version-value {
font-weight: 400;
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.colors.text.subtext2};
white-space: nowrap;
}

.version-summary {
display: flex;
align-items: center;
gap: 0.375rem;
margin: 0.25rem 0 0;
font-size: ${(props) => props.theme.font.size.xs};
color: ${(props) => props.theme.colors.text.muted};
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.colors.text.subtext2};
}

.version-dot {
width: 5px;
height: 5px;
border-radius: 50%;
background-color: ${(props) => props.theme.colors.text.subtext0};
flex-shrink: 0;
}
}

.card-divider {
height: 1px;
background-color: ${(props) => props.theme.border.border1};
background-color: ${(props) => props.theme.table.border};
}

.env-section {
Expand All @@ -59,7 +83,7 @@ const StyledWrapper = styled.div`
align-items: center;
justify-content: space-between;
gap: 0.5rem;
margin-bottom: 0.75rem;
margin-bottom: 0.15rem;
}

.env-section-heading {
Expand All @@ -70,18 +94,18 @@ const StyledWrapper = styled.div`
}

.env-section-count {
font-size: ${(props) => props.theme.font.size.xs};
color: ${(props) => props.theme.colors.text.muted};
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.colors.text.subtext2};
white-space: nowrap;
}

.env-section-title {
margin: 0;
font-size: ${(props) => props.theme.font.size.xs};
font-size: ${(props) => props.theme.font.size.sm};
font-weight: 600;
letter-spacing: 0.05em;
text-transform: uppercase;
color: ${(props) => props.theme.colors.text.muted};
color: ${(props) => props.theme.colors.text.subtext2};
}

.env-select-all {
Expand All @@ -94,7 +118,7 @@ const StyledWrapper = styled.div`

.env-select-all-label {
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.colors.text.muted};
color: ${(props) => props.theme.colors.text.subtext2};
white-space: nowrap;
}
}
Expand All @@ -105,12 +129,12 @@ const StyledWrapper = styled.div`
gap: 0.5rem;
/* Fixed row height — MUST match ENV_ROW_HEIGHT (Virtuoso fixedItemHeight)
in EnvironmentSelectionList. The inter-row spacing is baked in here. */
height: 34px;
height: 28px;
cursor: pointer;
margin: 0;

.env-name {
font-size: ${(props) => props.theme.font.size.sm};
font-size: ${(props) => props.theme.font.size.base};
color: ${(props) => props.theme.text};
min-width: 0;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ const GenerateDocumentation = ({ onClose, collectionUid }) => {
handleCancel={onClose}
confirmDisabled={isLoading}
>
<StyledWrapper className="w-[500px]">
<StyledWrapper>
{isLoading ? (
<div className="flex items-center justify-center gap-3 py-8">
<IconLoader2 size={20} className="animate-spin" />
Expand All @@ -215,7 +215,7 @@ const GenerateDocumentation = ({ onClose, collectionUid }) => {
</ul>

<div className="config-card mb-4">
<CollectionVersionInfo version={currentVersion} folderCount={folderCount} requestCount={requestCount} />
<CollectionVersionInfo name={collection.name} version={currentVersion} folderCount={folderCount} requestCount={requestCount} />
{environments.length > 0 && (
<Fragment>
<div className="card-divider" />
Expand Down
8 changes: 5 additions & 3 deletions tests/collection/generate-docs/generate-docs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ test.describe('Generate Documentation', () => {
await expect(modal).toBeHidden();
});

test('shows the current collection version formatted as a v-prefixed semver', async ({
test('shows the collection name alongside the current version formatted as a v-prefixed semver', async ({
pageWithUserData: page
}) => {
const locators = buildCommonLocators(page);
Expand All @@ -176,13 +176,15 @@ test.describe('Generate Documentation', () => {
const modal = locators.generateDocs.modal();
await expect(modal).toBeVisible();

await expect(locators.generateDocs.collectionName()).toHaveText(COLLECTION_NAME);

// The fixture's bruno.json version ("1") is normalised for display to "v1.0.0".
await expect(locators.generateDocs.versionInfo()).toContainText('Collection Version:');
await expect(locators.generateDocs.versionValue()).toHaveText('v1.0.0');

// The fixture has 2 folders (Zoo, Aviary) and 5 requests (Lion, Bear, Parrot,
// ReqAlpha, ReqBeta), counted recursively across the whole tree.
await expect(locators.generateDocs.versionCounts()).toHaveText('2 Folders • 5 requests');
await expect(locators.generateDocs.versionCounts()).toContainText('2 Folders');
await expect(locators.generateDocs.versionCounts()).toContainText('5 requests');

await locators.generateDocs.cancelButton().click();
await expect(modal).toBeHidden();
Expand Down
3 changes: 2 additions & 1 deletion tests/utils/page/locators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,9 @@ export const buildCommonLocators = (page: Page) => ({
heading: () => page.locator('.bruno-modal').getByText('Interactive API Documentation'),
generateButton: () => page.locator('.bruno-modal').getByRole('button', { name: 'Generate', exact: true }),
cancelButton: () => page.locator('.bruno-modal').getByRole('button', { name: 'Cancel', exact: true }),
// Collection version (read-only) display
// Collection name + version (read-only) display
versionInfo: () => page.locator('.bruno-modal').getByTestId('version-info'),
collectionName: () => page.locator('.bruno-modal').getByTestId('collection-name'),
versionValue: () => page.locator('.bruno-modal').getByTestId('version-value'),
versionCounts: () => page.locator('.bruno-modal').getByTestId('version-summary'),
// Environment selection list
Expand Down
Loading