diff --git a/package-lock.json b/package-lock.json index cc1fb7e..022d171 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@api-components/api-type-document", - "version": "4.2.43", + "version": "4.2.44", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@api-components/api-type-document", - "version": "4.2.43", + "version": "4.2.44", "license": "Apache-2.0", "dependencies": { "@advanced-rest-client/arc-marked": "^1.1.0", diff --git a/package.json b/package.json index 7d340c7..8c9e90b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/ApiTypeDocument.js b/src/ApiTypeDocument.js index c68b036..0c6adf5 100644 --- a/src/ApiTypeDocument.js +++ b/src/ApiTypeDocument.js @@ -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) ); } @@ -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, @@ -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 diff --git a/test/api-type-document.test.js b/test/api-type-document.test.js index 28572a1..632cf02 100644 --- a/test/api-type-document.test.js +++ b/test/api-type-document.test.js @@ -608,10 +608,9 @@ describe('', () => { }); 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')); }); }); @@ -629,6 +628,12 @@ describe('', () => { 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', () => { diff --git a/test/nil-main-example.test.js b/test/nil-main-example.test.js new file mode 100644 index 0000000..4699458 --- /dev/null +++ b/test/nil-main-example.test.js @@ -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} */ + async function basicFixture() { + return fixture(``); + } + + /** @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())); + }); + }); +});