Skip to content
Open
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
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.43",
"version": "4.2.44",
"license": "Apache-2.0",
"main": "index.js",
"module": "index.js",
Expand Down
26 changes: 24 additions & 2 deletions src/ApiTypeDocument.js
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,8 @@ export class ApiTypeDocument extends PropertyDocumentMixin(LitElement) {
this.requestUpdate('noMainExample', old);
this._renderMainExample = this._computeRenderMainExample(
value,
this._hasExamples
this._hasExamples,
this._isScalarLikeType(this._resolvedType)
);
}

Expand All @@ -307,7 +308,7 @@ export class ApiTypeDocument extends PropertyDocumentMixin(LitElement) {
}
this.__hasExamples = value;
this.requestUpdate('_hasExamples', old);
const scalarType = this._hasType(this.type, this.ns.aml.vocabularies.shapes.ScalarShape);
const scalarType = this._isScalarLikeType(this._resolvedType);
this._renderMainExample = this._computeRenderMainExample(
this.noMainExample,
value,
Expand Down Expand Up @@ -365,6 +366,27 @@ export class ApiTypeDocument extends PropertyDocumentMixin(LitElement) {
return isScalar ? false : !!(!noMainExample && hasExamples);
}

/**
* Whether the type renders as a scalar for the purpose of the main example.
* Scalar, Nil, and nullable unions (`type | null`) are scalar-like here,
* matching `_typeChanged`: all suppress the standalone main-example section
* (the value is shown inline). Nullable unions must agree with `_typeChanged`
* or the empty `.examples` section reappears for `type: [T, null]` shapes.
* @param {Array|Object} type AMF type shape (pass the resolved type)
* @returns {boolean}
*/
_isScalarLikeType(type) {
if (!type) {
return false;
}
const { shapes } = this.ns.aml.vocabularies;
if (this._hasType(type, shapes.ScalarShape) || this._hasType(type, shapes.NilShape)) {
return true;
}
const nullableCheck = this._checkNullableUnion(type);
return !!(nullableCheck && nullableCheck.isNullable);
}

/**
* Called when properties change
* @param {Map} changedProperties Changed properties
Expand Down
13 changes: 9 additions & 4 deletions test/api-type-document.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -608,10 +608,9 @@ describe('<api-type-document>', () => {
});

it('Does not render main example', () => {
const examples = element.shadowRoot.querySelector(
'examples'
);
assert.notOk(examples);
const examples = element.shadowRoot.querySelector('.examples');
assert.exists(examples);
assert.isTrue(examples.hasAttribute('hidden'));
});
});

Expand All @@ -629,6 +628,12 @@ describe('<api-type-document>', () => {
it('isScalar is true', () => {
assert.isTrue(element.isScalar);
});

it('Does not render main example', () => {
const examples = element.shadowRoot.querySelector('.examples');
assert.exists(examples);
assert.isTrue(examples.hasAttribute('hidden'));
});
});

describe('Union type', () => {
Expand Down
136 changes: 136 additions & 0 deletions test/nil-main-example.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/* eslint-disable prefer-destructuring */
import { fixture, assert, aTimeout } from '@open-wc/testing';
import '../api-type-document.js';

/** @typedef {import('..').ApiTypeDocument} ApiTypeDocument */

/**
* Regression tests for the "empty main-example section for Nil types" bug.
*
* AMF 5.11.x materializes OAS `example: null` as a concrete Example node, which
* flips `_hasExamples` to `true` on Nil-typed props and made the standalone
* `.examples` section render empty. The fix suppresses that section for
* scalar-like types (Scalar + Nil), mirroring `_typeChanged`.
*
* Nodes are built inline (no api-model-generator) so the shapes are
* deterministic and independent of the pinned generator version. With `amf`
* unset, `_getAmfKey` returns the raw expanded URIs, so `@type` uses the full
* namespace strings from `element.ns`.
*/
describe('Nil main-example suppression', () => {
/** @returns {Promise<ApiTypeDocument>} */
async function basicFixture() {
return fixture(`<api-type-document></api-type-document>`);
}

/** @type ApiTypeDocument */
let element;

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

function shapes() {
return element.ns.aml.vocabularies.shapes;
}

function nilNode() {
return { '@type': [shapes().NilShape] };
}

function scalarNode() {
return { '@type': [shapes().ScalarShape] };
}

function objectNode() {
return { '@type': [element.ns.w3.shacl.NodeShape] };
}

function arrayNode() {
return { '@type': [shapes().ArrayShape] };
}

/**
* Builds a nullable union `[Scalar, Nil]` whose members match what
* `_checkNullableUnion` recognizes: a UnionShape with exactly two `anyOf`
* members, one NilShape and one non-nil.
*/
function nullableUnionNode() {
const anyOfKey = element._getAmfKey(shapes().anyOf);
return {
'@type': [shapes().UnionShape],
[anyOfKey]: [scalarNode(), nilNode()],
};
}

/**
* Sets the type, forces the "has examples" signal, and flushes both the
* Lit render and the `_typeChanged` debouncer (scheduled via setTimeout).
* @param {object} type
*/
async function applyTypeWithExamples(type) {
element.type = type;
element._hasExamples = true;
await element.updateComplete;
await aTimeout(0);
await element.updateComplete;
}

it('A - pure Nil with examples keeps the main-example section hidden', async () => {
await applyTypeWithExamples(nilNode());

assert.isFalse(element._renderMainExample, '_renderMainExample is false for Nil');
const section = element.shadowRoot.querySelector('.examples');
assert.isTrue(section.hasAttribute('hidden'), '.examples section is hidden');
});

it('B - nullable union [Scalar, Nil] with examples keeps the section hidden', async () => {
const type = nullableUnionNode();
// Sanity: the union is the nullable shape `_typeChanged` treats as scalar.
assert.isTrue(
element._checkNullableUnion(type).isNullable,
'node is a nullable union'
);

await applyTypeWithExamples(type);

assert.isTrue(element.isScalar, '_typeChanged classifies nullable union as scalar');
assert.isFalse(element._renderMainExample, '_renderMainExample is false for nullable union');
const section = element.shadowRoot.querySelector('.examples');
assert.isTrue(section.hasAttribute('hidden'), '.examples section is hidden');
});

it('C - object with examples still renders the main-example section', async () => {
await applyTypeWithExamples(objectNode());

assert.isTrue(element._renderMainExample, '_renderMainExample is true for object');
const section = element.shadowRoot.querySelector('.examples');
assert.isFalse(section.hasAttribute('hidden'), '.examples section is visible');
});

describe('_isScalarLikeType()', () => {
it('is true for a NilShape node', () => {
assert.isTrue(element._isScalarLikeType(nilNode()));
});

it('is true for a ScalarShape node', () => {
assert.isTrue(element._isScalarLikeType(scalarNode()));
});

it('is false for undefined', () => {
assert.isFalse(element._isScalarLikeType(undefined));
});

it('is false for an array input', () => {
assert.isFalse(element._isScalarLikeType([nilNode()]));
});

it('is false for a NodeShape node', () => {
assert.isFalse(element._isScalarLikeType(objectNode()));
});

it('is false for an ArrayShape node', () => {
assert.isFalse(element._isScalarLikeType(arrayNode()));
});
});
});
Loading