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
3 changes: 2 additions & 1 deletion demo/apis.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,6 @@
"shopper-products/shopper-products.yaml": { "type": "OAS 3.0", "mime": "application/yaml" },
"W-12142859/W-12142859.yaml": "OAS 2.0",
"nulleable/nulleable.yaml": "OAS 3.0",
"nullable-test/nullable-test.yaml": { "type": "OAS 3.0", "mime": "application/yaml" }
"nullable-test/nullable-test.yaml": { "type": "OAS 3.0", "mime": "application/yaml" },
"nested-examples/nested-examples-oas3.json": { "type": "OAS 3.0", "mime": "application/json" }
}
1 change: 1 addition & 0 deletions demo/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ class ApiDemo extends ApiDemoPage {

_apiListTemplate() {
return [
['nested-examples-oas3', 'Nested Examples'],
['nullable-test', 'Nullable Test (Comprehensive)'],
['nulleable', 'Nulleable test'],
['grpc-test', 'GRPC test'],
Expand Down
472 changes: 472 additions & 0 deletions demo/nested-examples/nested-examples-oas3.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@api-components/api-type-document",
"description": "A documentation table for type (resource) properties. Works with AMF data model",
"version": "4.2.38",
"version": "4.2.39",
"license": "Apache-2.0",
"main": "index.js",
"module": "index.js",
Expand Down
10 changes: 6 additions & 4 deletions src/ApiTypeDocument.js
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,7 @@ export class ApiTypeDocument extends PropertyDocumentMixin(LitElement) {
this._showExamples = !this.noMainExample && (
this.renderMediaSelector ||
this.isObject ||
this.isArray ||
this._renderMainExample
);
}
Expand Down Expand Up @@ -530,15 +531,16 @@ export class ApiTypeDocument extends PropertyDocumentMixin(LitElement) {
}

// Determine if we should show the examples section
// Priority: noMainExample (hide) > renderMediaSelector (show) > isObject (show) > _renderMainExample
// Priority: noMainExample (hide) > renderMediaSelector (show) > isObject (show) > isArray (show) > _renderMainExample
this._showExamples = !this.noMainExample && (
this.renderMediaSelector || // Need to show the section for the media type selector
isObject || // Objects can generate examples automatically
isArray || // Arrays can generate examples automatically
this._renderMainExample // Has explicit examples
);

// Effective media type - use 'application/json' as default for objects without mediaType
this._exampleMediaType = this.mediaType || (isObject ? 'application/json' : undefined);
// Effective media type - use 'application/json' as default for objects and arrays without mediaType
this._exampleMediaType = this.mediaType || (isObject || isArray ? 'application/json' : undefined);
}

/**
Expand Down Expand Up @@ -1141,7 +1143,7 @@ export class ApiTypeDocument extends PropertyDocumentMixin(LitElement) {
: this._renderMainExample;
const exampleMediaType = this._exampleMediaType !== undefined
? this._exampleMediaType
: (this.mediaType || (this.isObject ? 'application/json' : undefined));
: (this.mediaType || (this.isObject || this.isArray ? 'application/json' : undefined));

return html`<style>${this.styles}</style>
${shouldRenderExamples ? html`<section class="examples">
Expand Down
200 changes: 200 additions & 0 deletions test/nested-array-examples.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/* eslint-disable prefer-destructuring */
import { fixture, assert, aTimeout, nextFrame } from '@open-wc/testing'
import { AmfLoader } from './amf-loader.js';
import '../api-type-document.js';

/** @typedef {import('..').ApiTypeDocument} ApiTypeDocument */
/** @typedef {import('@api-components/amf-helper-mixin').AmfDocument} AmfDocument */

/**
* Test for array response examples in OAS 3.0.
*
* This test reproduces the bug where example payloads were not displaying
* on the Exchange main page for APIs using OpenAPI Specification (OAS) 3.0.0
* when the response schema is an array type.
*
* The issue affected partner automation for order placement and was caused by
* the component not recognizing array types as eligible for example rendering,
* only checking for object types.
*
* Bug details:
* - Started: January 30, 2026
* - Impact: All OAS 3.0.0 specifications with array response schemas
* - Examples visible in Design Center but failed to render in Exchange
* - RAML specifications were unaffected
*
* Fix: Added isArray checks alongside isObject checks in:
* - _showExamples computation
* - _exampleMediaType defaulting (both should default to 'application/json')
*/
describe('Array Examples - OAS 3.0 (nested-examples)', () => {
const file = 'nested-examples-oas3';

/**
* @returns {Promise<ApiTypeDocument>}
*/
async function basicFixture() {
return fixture(`<api-type-document></api-type-document>`);
}

/**
* Get response payload schema for a specific endpoint and operation
* @param {AmfDocument} model
* @param {string} endpoint
* @param {string} operation
* @returns {any}
*/
function getResponsePayloadSchema(model, endpoint, operation) {
const op = AmfLoader.lookupOperation(model, endpoint, operation);

// Find the response with status 200
const returnsKey = Object.keys(op).find(k => k.includes('returns'));
if (!returnsKey) return null;

const responses = op[returnsKey];
let response200;

for (const resp of responses) {
const statusKey = Object.keys(resp).find(k => k.includes('statusCode'));
if (statusKey && resp[statusKey]) {
const statusValue = Array.isArray(resp[statusKey]) ? resp[statusKey][0] : resp[statusKey];
const status = typeof statusValue === 'object' ? statusValue['@value'] : statusValue;
if (status === '200') {
response200 = resp;
break;
}
}
}

if (!response200) return null;

// Get payload
const payloadKey = Object.keys(response200).find(k => k.includes('payload'));
if (!payloadKey) return null;

const payloads = response200[payloadKey];
const payload = Array.isArray(payloads) ? payloads[0] : payloads;

// Get schema
const schemaKey = Object.keys(payload).find(k => k.includes('schema'));
if (!schemaKey) return null;

const schemas = payload[schemaKey];
return Array.isArray(schemas) ? schemas[0] : schemas;
}

[
['Regular model', false],
['Compact model', true],
].forEach((item) => {
describe(String(item[0]), () => {
let element = /** @type ApiTypeDocument */ (null);
let amf;

before(async () => {
amf = await AmfLoader.load(item[1], file);
});

beforeEach(async () => {
element = await basicFixture();
});

it('loads the nested-examples-oas3 model', async () => {
assert.exists(amf, 'AMF model should be loaded');
});

it('identifies array response schema correctly', async () => {
const responseSchema = getResponsePayloadSchema(amf, '/productOrderItems', 'get');
assert.exists(responseSchema, 'Response schema should exist');

element.amf = amf;
element.type = responseSchema;
await aTimeout(0);

// Verify it's recognized as an array type
assert.isTrue(element.isArray, 'Response schema should be identified as array');
});

it('renders examples section for array responses', async () => {
const responseSchema = getResponsePayloadSchema(amf, '/productOrderItems', 'get');

element.amf = amf;
element.type = responseSchema;
await aTimeout(0);

// Verify the examples section is rendered
const examplesSection = element.shadowRoot.querySelector('.examples');
assert.exists(examplesSection, 'Examples section should be rendered for array responses');
});

it('passes resolved type to api-resource-example-document for arrays', async () => {
const responseSchema = getResponsePayloadSchema(amf, '/productOrderItems', 'get');

element.amf = amf;
element.type = responseSchema;
await aTimeout(0);

const exampleDoc = element.shadowRoot.querySelector('api-resource-example-document');
assert.exists(exampleDoc, 'api-resource-example-document should be rendered');
assert.isDefined(exampleDoc.examples, 'Examples should be passed to the component');
});

it('uses default mediaType for arrays without explicit mediaType', async () => {
const responseSchema = getResponsePayloadSchema(amf, '/productOrderItems', 'get');

element.amf = amf;
element.type = responseSchema;
// Don't set mediaType explicitly
await aTimeout(0);

const exampleDoc = element.shadowRoot.querySelector('api-resource-example-document');
assert.exists(exampleDoc, 'api-resource-example-document should exist');
assert.equal(
exampleDoc.mediaType,
'application/json',
'Should default to application/json for arrays'
);
});

it('sets _showExamples to true for array types', async () => {
const responseSchema = getResponsePayloadSchema(amf, '/productOrderItems', 'get');

element.amf = amf;
element.type = responseSchema;
await aTimeout(0);

assert.isTrue(element._showExamples, '_showExamples should be true for arrays');
});

it('respects noMainExample flag for arrays', async () => {
const responseSchema = getResponsePayloadSchema(amf, '/productOrderItems', 'get');

element.amf = amf;
element.noMainExample = true;
element.type = responseSchema;
await element.updateComplete;
await nextFrame();

const examplesSection = element.shadowRoot.querySelector('.examples');
assert.notExists(examplesSection, 'Examples should not render when noMainExample is true');
});

it('renders complex nested array examples correctly', async () => {
const responseSchema = getResponsePayloadSchema(amf, '/productOrderItems', 'get');

element.amf = amf;
element.type = responseSchema;
element.mediaType = 'application/json';
await aTimeout(0);

const exampleDoc = element.shadowRoot.querySelector('api-resource-example-document');
assert.exists(exampleDoc, 'Example document should be rendered');

// Verify the component received the correct props
assert.equal(exampleDoc.mediaType, 'application/json');
assert.isDefined(exampleDoc.examples, 'Examples should be defined');
assert.exists(element.shadowRoot.querySelector('.examples'), 'Examples section should exist');
});
});
});
});