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
72 changes: 72 additions & 0 deletions src/adapters/openapi/__tests__/operation-heading-level.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { describe, it, expect } from 'vitest';

import type { ApiItem } from '../../../types/store.js';
import type { OpenAPIDefinition, OpenAPITag } from '../../../types/openapi.js';

import { addTagItems } from '../group/item.js';
import { openApiContext } from '../buildContext.js';
import { createBuildContext } from '../build/context.js';
import { normalizeOptions } from '../../../options/normalizeOptions.js';
import { createStoreContext, toRecord } from '../../helpers.js';

/** Returns the level of the first HEADER node found in an item's content tree. */
function findHeaderLevel(node: unknown): number | undefined {
if (!node || typeof node !== 'object') return undefined;
const record = node as { nodeType?: string; level?: number; children?: unknown[] };
if (record.nodeType === 'header' && typeof record.level === 'number') return record.level;
for (const child of record.children ?? []) {
const found = findHeaderLevel(child);
if (found !== undefined) return found;
}
return undefined;
}

describe('operation title heading level', () => {
const document: OpenAPIDefinition = {
openapi: '3.0.0',
info: { title: 'Test', version: '1.0' },
tags: [{ name: 'Pets' }] as OpenAPITag[],
paths: {
'/pets': {
get: { operationId: 'listPets', tags: ['Pets'], responses: {} },
},
'/health': {
get: { operationId: 'health', responses: {} },
},
},
};

const options = normalizeOptions({
specType: 'openapi',
downloadUrls: [],
metadata: {},
basePath: '/api',
});

function buildItems(): ApiItem[] {
const context = createBuildContext({
document,
options,
basePath: '/api',
storeCtx: createStoreContext(toRecord(document)),
});
const items: ApiItem[] = [];
openApiContext.run(context, () => addTagItems(document, items));
return items;
}

it('renders operations nested under a tag with an h3 title (level 3), not h2', () => {
const items = buildItems();
const tagGroup = items.find((i) => i.type === 'group' && i.label === 'Pets');
const operation = (tagGroup?.items as ApiItem[]).find((i) => i.type === 'link');

expect(findHeaderLevel(operation?.content)).toBe(3);
});

it('keeps the operation title at level 2 when it is not under a tag', () => {
const items = buildItems();
const untagged = items.find((i) => i.type === 'link' && i.label === 'health');

expect(findHeaderLevel(untagged?.content)).toBe(2);
});
});
1 change: 1 addition & 0 deletions src/adapters/openapi/group/item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ function addSingleTagItem(
result: tagItems,
basePath: tagSlug,
parentRbac: effectiveTagRbac,
headingLevel: 3,
});

tagItems.push(...childItems);
Expand Down
8 changes: 6 additions & 2 deletions src/adapters/openapi/items/operation/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,11 @@ export function createCallbackIdFactory(): (name: string, httpVerb: string) => s
};
}

function _buildItemContent(operationInfo: OperationInfo, pageSlug: string): ApiItemContent {
function _buildItemContent(
operationInfo: OperationInfo,
pageSlug: string,
headingLevel: number = 2,
): ApiItemContent {
const { options, storeCtx, basePath } = openApiContext.get();
const operationName = getOperationLabel(operationInfo);
const children: ContentNode[] = [];
Expand All @@ -101,7 +105,7 @@ function _buildItemContent(operationInfo: OperationInfo, pageSlug: string): ApiI
const headerChildren: ContentNode[] = [
{
nodeType: nodeTypes.HEADER,
level: 2,
level: headingLevel,
label: operationName,
badges: operationInfo['x-badges'],
isWebhook,
Expand Down
13 changes: 9 additions & 4 deletions src/adapters/openapi/items/operation/item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,22 @@ export function addOperationItems({
result,
basePath: basePathOverride,
parentRbac,
headingLevel,
}: {
operations: OperationInfo[];
result: ApiItem[];
basePath?: string;
parentRbac?: RbacScope;
/** Heading level of the operation title: 3 when nested under a tag, 2 otherwise. */
headingLevel?: number;
}): void {
const { basePath } = openApiContext.get();
const effectiveBasePath = basePathOverride ?? basePath;
const { regular, regularDeprecated, webhooks, webhooksDeprecated } =
categorizeOperations(operations);

for (const operation of [...regular, ...regularDeprecated]) {
addOperationItem(operation, effectiveBasePath, result, parentRbac);
addOperationItem(operation, effectiveBasePath, result, parentRbac, headingLevel);
}

const hasWebhooks = webhooks.length > 0 || webhooksDeprecated.length > 0;
Expand All @@ -41,7 +44,7 @@ export function addOperationItems({
}

for (const operation of [...webhooks, ...webhooksDeprecated]) {
addOperationItem(operation, effectiveBasePath, result, parentRbac);
addOperationItem(operation, effectiveBasePath, result, parentRbac, headingLevel);
}
}

Expand Down Expand Up @@ -73,9 +76,10 @@ function addOperationItem(
basePath: string,
result: ApiItem[],
parentRbac?: RbacScope,
headingLevel?: number,
): void {
try {
result.push(buildOperationItem(operation, basePath, parentRbac));
result.push(buildOperationItem(operation, basePath, parentRbac, headingLevel));
} catch (error) {
reportError(
'buildItem',
Expand All @@ -89,6 +93,7 @@ function buildOperationItem(
operation: OperationInfo,
basePath: string,
parentRbac?: RbacScope,
headingLevel?: number,
): ApiItem {
const { processContent } = openApiContext.get();

Expand All @@ -105,7 +110,7 @@ function buildOperationItem(
link: slug,
routeSlug: slug,
...rbacProp(readRbacScope(operation) ?? parentRbac),
content: processContent ? buildItemContent(operation, slug) : null,
content: processContent ? buildItemContent(operation, slug, headingLevel) : null,
};

if (badges) {
Expand Down