diff --git a/.gitignore b/.gitignore index 451ec5d..7161910 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ jspm_packages/ .wrangler/ .DS_Store .cursor +.idea diff --git a/package-lock.json b/package-lock.json index 0741b91..7fc343e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -644,9 +644,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.8.0.tgz", - "integrity": "sha512-MJQFqrZgcW0UNYLGOuQpey/oTN59vyWwplvCGZztn1cKz9agZPPYpJB7h2OMmuu7VLqkvEjN8feFZJmxNF9D+Q==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1421,9 +1421,9 @@ } }, "node_modules/@poppinss/colors": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.5.tgz", - "integrity": "sha512-FvdDqtcRCtz6hThExcFOgW0cWX+xwSMWcRuQe5ZEb2m7cVQOAVZOIMt+/v9RxGiD9/OY16qJBXK4CVKWAPalBw==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", + "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", "dev": true, "license": "MIT", "dependencies": { @@ -1456,9 +1456,9 @@ } }, "node_modules/@poppinss/exception": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.2.tgz", - "integrity": "sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", + "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", "dev": true, "license": "MIT" }, @@ -1470,9 +1470,9 @@ "license": "MIT" }, "node_modules/@sindresorhus/is": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.1.1.tgz", - "integrity": "sha512-rO92VvpgMc3kfiTjGT52LEtJ8Yc5kCWhZjLQ3LwlA4pSgPpQO7bVpYXParOD8Jwf+cVQECJo3yP/4I8aZtUQTQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", "dev": true, "license": "MIT", "engines": { @@ -1483,9 +1483,9 @@ } }, "node_modules/@speed-highlight/core": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.12.tgz", - "integrity": "sha512-uilwrK0Ygyri5dToHYdZSjcvpS2ZwX0w5aSt3GCEN9hrjxWCoeV4Z2DTXuxjwbntaLQIEEAlCeNQss5SoHvAEA==", + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.14.tgz", + "integrity": "sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==", "dev": true, "license": "CC0-1.0" }, @@ -1553,7 +1553,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2113,13 +2112,17 @@ "license": "MIT" }, "node_modules/cookie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", - "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", "dev": true, "license": "MIT", "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/cross-spawn": { @@ -2620,7 +2623,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2813,9 +2815,9 @@ } }, "node_modules/esmock": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esmock/-/esmock-2.7.3.tgz", - "integrity": "sha512-/M/YZOjgyLaVoY6K83pwCsGE1AJQnj4S4GyXLYgi/Y79KL8EeW6WU7Rmjc89UO7jv6ec8+j34rKeWOfiLeEu0A==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/esmock/-/esmock-2.7.1.tgz", + "integrity": "sha512-YgtZ6TSwRbdqFLkJwxVCYkt0rzKpjHb0tbDqSjWvbkm8Uy5Tm5W6ixICb3FVRkAd1uQlLOXiIn7OPY4F4f18cw==", "dev": true, "license": "ISC", "engines": { @@ -3906,16 +3908,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", @@ -4485,9 +4477,9 @@ } }, "node_modules/mocha": { - "version": "11.7.5", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", - "integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==", + "version": "11.7.1", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.1.tgz", + "integrity": "sha512-5EK+Cty6KheMS/YLPPMJC64g5V61gIR25KsRItHw6x4hEKT6Njp1n9LOlH4gpevuwMVS66SXaBBpg+RWZkza4A==", "dev": true, "license": "MIT", "dependencies": { @@ -4499,7 +4491,6 @@ "find-up": "^5.0.0", "glob": "^10.4.5", "he": "^1.2.0", - "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "log-symbols": "^4.1.0", "minimatch": "^9.0.5", @@ -5763,7 +5754,6 @@ "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "pathe": "^2.0.3" } diff --git a/src/ue/attributes.js b/src/ue/attributes.js index c8806e9..78017fb 100644 --- a/src/ue/attributes.js +++ b/src/ue/attributes.js @@ -10,9 +10,10 @@ * governing permissions and limitations under the License. */ import { select, selectAll } from 'hast-util-select'; -import { visit } from 'unist-util-visit'; import { isElement } from 'hast-util-is-element'; +import { toString } from 'hast-util-to-string'; import { h } from 'hastscript'; +import { visit } from 'unist-util-visit'; import { getBlockNameAndClasses, removeWhitespaceTextNodes, @@ -79,40 +80,59 @@ function wrapParagraphs(section) { * This function processes fields in a block component and adds appropriate UE attributes * based on the field type (richtext, reference, text). * - * @param {Object} ueConfig - Configuration object containing UE component models + * @param {Object} blockCmpFields - The defined UE fields for the block * @param {Object} block - The block element to process */ function addBlockFieldAttributes(ueConfig, block) { - const blockName = block.properties['data-aue-model']; - const modelDef = getModelDefinition(ueConfig, blockName); - if (modelDef) { - const fields = modelDef.fields || []; - const fieldsWithAttributes = fields.filter((field) => field.component === 'richtext' || field.component === 'reference' || (field.component === 'text' && field.name.indexOf('[') === -1)); - fieldsWithAttributes.forEach((field) => { - const blockFieldTag = select(field.name, block); - if (blockFieldTag) { + const blockName = block.properties['data-aue-component'] ?? block.properties['data-aue-model']; + const blockCmpDef = getComponentDefinition(ueConfig, blockName); + const blockModelDef = getModelDefinition(ueConfig, blockName); + if (!blockCmpDef || !blockModelDef) return; + + const cmpFieldsDef = blockCmpDef.plugins?.da?.fields || []; + const modelFields = blockModelDef.fields || []; + if (cmpFieldsDef.length === 0 || modelFields.length === 0) return; + + // extract available unique fields from cmpFieldsDef + const cmpFields = []; + const seenSelectors = new Set(); + cmpFieldsDef.forEach((fieldObj) => { + const { name, selector } = fieldObj; + if (selector) { + const cleanedSelector = selector.replace(/\[.*?\]/g, ''); + if (!seenSelectors.has(cleanedSelector)) { + cmpFields.push({ selector, name }); + seenSelectors.add(cleanedSelector); + } + } + }); + + // add attributes to block fields based on cmpFields mapping and modelFields + cmpFields.forEach((cmpField) => { + const blockFieldTag = select(cmpField.selector, block); + if (blockFieldTag) { + // find matching model field + const modelField = modelFields.find((mf) => mf.name === cmpField.name); + if (modelField) { addAttributes(blockFieldTag, { - 'data-aue-type': field.component === 'reference' ? 'media' : field.component, - 'data-aue-prop': field.name, - 'data-aue-label': field.label || field.name, + 'data-aue-type': modelField.component === 'reference' ? 'media' : modelField.component, + 'data-aue-prop': cmpField.name, + 'data-aue-label': modelField.label || cmpField.name, }); } - }); - } + } + }); } -function addColumnBehaviourInstrumentation(section, sIndex, block, bIndex, blockCmpDef, ueConfig) { +function addColumnBlockInstrumentation(sIndex, block, bIndex, ueConfig) { const { name: blockName } = getBlockNameAndClasses(block); + const blockCmpDef = getComponentDefinition(ueConfig, blockName); const cellFilterDef = getFilterDefinition(ueConfig, `${blockName}-cell`); - // handle columns + // add additional container attribute to the column block addAttributes(block, { 'data-aue-resource': `urn:ab:section-${sIndex}/columns-${bIndex}`, - 'data-aue-label': blockCmpDef.title, - 'data-aue-model': blockName, - 'data-aue-filter': blockName, 'data-aue-type': 'container', - 'data-aue-behavior': 'component', }); const rows = selectAll(':scope>div', block); @@ -120,8 +140,7 @@ function addColumnBehaviourInstrumentation(section, sIndex, block, bIndex, block addAttributes(row, { 'data-aue-resource': `urn:ab:section-${sIndex}/columns-${bIndex}/row-${rIndex}`, 'data-aue-label': `${blockCmpDef.title} Row`, - 'data-aue-model': `${blockName}-row`, - 'data-aue-filter': `${blockName}-row`, + 'data-aue-component': `${blockName}-row`, 'data-aue-type': 'container', 'data-aue-behavior': 'component', }); @@ -131,8 +150,7 @@ function addColumnBehaviourInstrumentation(section, sIndex, block, bIndex, block addAttributes(cell, { 'data-aue-resource': `urn:ab:section-${sIndex}/columns-${bIndex}/row-${rIndex}/cell-${cIndex}`, 'data-aue-label': `${blockCmpDef.title} Cell`, - 'data-aue-model': `${blockName}-cell`, - 'data-aue-filter': `${blockName}-cell`, + 'data-aue-component': `${blockName}-cell`, 'data-aue-type': 'container', 'data-aue-behavior': 'component', }); @@ -145,10 +163,9 @@ function addColumnBehaviourInstrumentation(section, sIndex, block, bIndex, block addAttributes(picture, { 'data-aue-resource': `urn:ab:section-${sIndex}/columns-${bIndex}/row-${rIndex}/cell-${cIndex}/image-${iIndex}`, 'data-aue-label': 'Image', - 'data-aue-behavior': 'component', + 'data-aue-component': 'image', 'data-aue-prop': 'image', - 'data-aue-type': 'container', - 'data-aue-model': 'image', + 'data-aue-type': 'media', }); }); @@ -157,6 +174,7 @@ function addColumnBehaviourInstrumentation(section, sIndex, block, bIndex, block richTextWrappers.forEach((wrapper, wIndex) => { addAttributes(wrapper, { 'data-aue-resource': `urn:ab:section-${sIndex}/columns-${bIndex}/row-${rIndex}/cell-${cIndex}/text-${wIndex}`, + 'data-aue-component': 'text', 'data-aue-type': 'richtext', 'data-aue-label': 'Text', 'data-aue-prop': 'root', @@ -168,6 +186,45 @@ function addColumnBehaviourInstrumentation(section, sIndex, block, bIndex, block }); } +/** + * Adds Universal Editor (UE) attributes to key-value block components. + * Key-value blocks are two-column structures where the first column contains field names (keys) + * and the second column contains the editable values. This function extracts keys from the first + * column, matches them to model field definitions, and adds appropriate UE attributes. + * + * @param {Object} ueConfig - The UE configuration object containing component and model definitions + * @param {Object} block - The block element to process + */ +function addKeyValueBlockInstrumentation(block, ueConfig) { + const { name: blockName } = getBlockNameAndClasses(block); + const blockCmpDef = getComponentDefinition(ueConfig, blockName); + const blockModelDef = getModelDefinition(ueConfig, blockName); + if (!blockCmpDef || !blockModelDef) return; + + const modelFields = blockModelDef.fields || []; + if (modelFields.length === 0) return; + + const rows = selectAll(':scope>div', block); + rows.forEach((row) => { + const columns = selectAll(':scope>div', row); + if (columns.length !== 2) return; // key-value blocks must have exactly 2 columns + const keyColumn = columns[0]; + const valueColumn = columns[1]; + const keyText = toString(keyColumn); + if (!keyText) return; + + // find matching model field + const modelField = modelFields.find((mf) => mf.name === keyText.trim()); + if (modelField) { + addAttributes(valueColumn, { + 'data-aue-type': modelField.component === 'reference' ? 'media' : modelField.component, + 'data-aue-prop': modelField.name, + 'data-aue-label': modelField.label || modelField.name, + }); + } + }); +} + /** * Injects Universal Editor (UE) attributes into the HTML body tree. * This function adds data attributes to various elements in the body to enable UE functionality: @@ -210,9 +267,8 @@ export function injectUEAttributes(bodyTree, ueConfig) { 'data-aue-resource': `urn:ab:section-${sIndex}`, 'data-aue-type': 'container', 'data-aue-label': componentDef ? componentDef.title : 'Section', - 'data-aue-model': 'section', + 'data-aue-component': 'section', 'data-aue-behavior': 'component', - 'data-aue-filter': 'section', }); // handle rich text @@ -235,10 +291,9 @@ export function injectUEAttributes(bodyTree, ueConfig) { addAttributes(picture, { 'data-aue-resource': `urn:ab:section-${sIndex}/asset-${iIndex}`, 'data-aue-label': 'Image', - 'data-aue-behavior': 'component', 'data-aue-prop': 'image', 'data-aue-type': 'media', - 'data-aue-model': 'image', + 'data-aue-component': 'image', }); // TODO wait for SITES-27973 // const img = select('img', picture); @@ -259,39 +314,46 @@ export function injectUEAttributes(bodyTree, ueConfig) { const blockCmpDef = getComponentDefinition(ueConfig, blockName); const filterDef = getFilterDefinition(ueConfig, blockName); - if (blockCmpDef?.plugins?.da?.behaviour === 'columns') { - addColumnBehaviourInstrumentation(section, sIndex, block, bIndex, blockCmpDef, ueConfig); - return; - } - addAttributes(block, { 'data-aue-resource': `urn:ab:section-${sIndex}/block-${bIndex}`, 'data-aue-type': 'component', 'data-aue-label': blockCmpDef ? blockCmpDef.title : `${blockName} (no definition)`, - 'data-aue-model': blockName, + 'data-aue-component': blockName, }); + + // special handling for column blocks + if (blockCmpDef?.plugins?.da?.behaviour === 'columns' || blockCmpDef?.plugins?.da?.type === 'columns-block') { + addColumnBlockInstrumentation(sIndex, block, bIndex, ueConfig); + return; + } + + // special handling for key-value blocks + if (blockCmpDef?.plugins?.da?.type === 'key-value-block') { + addKeyValueBlockInstrumentation(block, ueConfig); + return; + } + addBlockFieldAttributes(ueConfig, block); // apply block flter and child items if (filterDef) { addAttributes(block, { - 'data-aue-filter': blockName, + 'data-aue-component': blockName, 'data-aue-type': 'container', - 'data-aue-behavior': 'component', }); const itemId = filterDef.components[0]; - const itemCmpDef = getComponentDefinition(ueConfig, itemId); - if (itemCmpDef) { + const blockItemCmpDef = getComponentDefinition(ueConfig, itemId); + if (blockItemCmpDef) { const blockItems = selectAll(':scope>div', block); blockItems.forEach((blockItem, biIndex) => { addAttributes(blockItem, { 'data-aue-resource': `urn:ab:section-${sIndex}/block-${bIndex}/item-${biIndex}`, 'data-aue-type': 'component', - 'data-aue-label': itemCmpDef.title, - 'data-aue-model': itemCmpDef.id, + 'data-aue-label': blockItemCmpDef.title, + 'data-aue-component': blockItemCmpDef.id, }); addBlockFieldAttributes(ueConfig, blockItem); }); diff --git a/src/ue/scaffold.js b/src/ue/scaffold.js index e03ebd4..73bebc1 100644 --- a/src/ue/scaffold.js +++ b/src/ue/scaffold.js @@ -27,8 +27,10 @@ export function getUEHtmlHeadEntries(daCtx, aemCtx) { aemPathname, isLocal, orgSiteInPath, + ueService: ueServiceParam, } = daCtx; const { ueHostname, ueService } = aemCtx; + let finalUeService = ueService; const children = []; children.push(h('meta', { @@ -36,11 +38,15 @@ export function getUEHtmlHeadEntries(daCtx, aemCtx) { content: isLocal ? `da:https://${ueHostname}/${org}/${site}${path}` : `da:https://${ref}--${site}--${org}.${ueHostname}${path}`, })); - if (ueService) { + if (ueServiceParam && ueServiceParam === 'local') { + finalUeService = 'https://localhost:8000'; + } + + if (finalUeService) { children.push( h('meta', { name: 'urn:adobe:aue:config:service', - content: ueService, + content: finalUeService, }), ); } diff --git a/src/utils/daCtx.js b/src/utils/daCtx.js index 77ce57b..168e3a4 100644 --- a/src/utils/daCtx.js +++ b/src/utils/daCtx.js @@ -61,7 +61,7 @@ function getAuthToken(req) { * @returns {DaCtx} The Dark Alley Context. */ export function getDaCtx(req) { - const { pathname, hostname } = new URL(req.url); + const { pathname, hostname, searchParams } = new URL(req.url); // TODO this requires some improvements to be more robust const { @@ -110,6 +110,11 @@ export function getDaCtx(req) { daCtx.pathname = `/${daPathBase}.${daCtx.ext}`; } + const query = Object.fromEntries(searchParams.entries()); + if (typeof query['ue-service'] === 'string') { + daCtx.ueService = query['ue-service']; + } + daCtx.authToken = getAuthToken(req); return daCtx; } diff --git a/test/ue/attributes.test.js b/test/ue/attributes.test.js index 129a7a1..b8bae2c 100644 --- a/test/ue/attributes.test.js +++ b/test/ue/attributes.test.js @@ -17,6 +17,7 @@ import { describe, it, before } from 'mocha'; import esmock from 'esmock'; import { select, selectAll } from 'hast-util-select'; import { h } from 'hastscript'; +import { toString } from 'hast-util-to-string'; describe('UE attributes', () => { let attributes; @@ -26,563 +27,1006 @@ describe('UE attributes', () => { }); describe('injectUEAttributes', () => { - it('adds UE attributes to main content', () => { - const bodyTree = h('body', {}, [ - h('main', {}, []), - ]); - - const ueConfig = { - 'component-definition': { - groups: [ - { - components: [ - { id: 'section', title: 'Section' }, - ], - }, - ], - }, - }; - - attributes.injectUEAttributes(bodyTree, ueConfig); - - const main = select('main', bodyTree); - assert.equal(main.properties['data-aue-resource'], 'urn:ab:main'); - assert.equal(main.properties['data-aue-type'], 'container'); - assert.equal(main.properties['data-aue-label'], 'Main Content'); - assert.equal(main.properties['data-aue-filter'], 'main'); - }); - - it('adds UE attributes to sections', () => { - const bodyTree = h('body', {}, [ - h('main', {}, [ - h('div', {}, []), - ]), - ]); - - const ueConfig = { - 'component-definition': { - groups: [ - { - components: [ - { id: 'section', title: 'Section' }, - ], - }, - ], - }, - }; + describe('main content and sections', () => { + it('adds UE attributes to main content', () => { + const bodyTree = h('body', {}, [ + h('main', {}, []), + ]); + + const ueConfig = { + 'component-definition': { + groups: [ + { + components: [ + { id: 'section', title: 'Section' }, + ], + }, + ], + }, + }; - attributes.injectUEAttributes(bodyTree, ueConfig); + attributes.injectUEAttributes(bodyTree, ueConfig); - const section = select('main > div', bodyTree); - assert.equal(section.properties['data-aue-resource'], 'urn:ab:section-0'); - assert.equal(section.properties['data-aue-type'], 'container'); - assert.equal(section.properties['data-aue-label'], 'Section'); - assert.equal(section.properties['data-aue-model'], 'section'); - }); + const main = select('main', bodyTree); + assert.equal(main.properties['data-aue-resource'], 'urn:ab:main'); + assert.equal(main.properties['data-aue-type'], 'container'); + assert.equal(main.properties['data-aue-label'], 'Main Content'); + assert.equal(main.properties['data-aue-filter'], 'main'); + }); - it('adds UE attributes to blocks within sections', () => { - const bodyTree = h('body', {}, [ - h('main', {}, [ - h('div', {}, [ - h('div', { className: ['card-block'] }, []), + it('adds UE attributes to sections', () => { + const bodyTree = h('body', {}, [ + h('main', {}, [ + h('div', {}, []), ]), - ]), - ]); + ]); - const ueConfig = { - 'component-definition': { - groups: [ - { - components: [ - { id: 'section', title: 'Section' }, - { id: 'card-block', title: 'Card Block' }, - ], - }, - ], - }, - }; + const ueConfig = { + 'component-definition': { + groups: [ + { + components: [ + { id: 'section', title: 'Section' }, + ], + }, + ], + }, + }; + + attributes.injectUEAttributes(bodyTree, ueConfig); + + const section = select('main > div', bodyTree); + assert.equal(section.properties['data-aue-resource'], 'urn:ab:section-0'); + assert.equal(section.properties['data-aue-type'], 'container'); + assert.equal(section.properties['data-aue-label'], 'Section'); + assert.equal(section.properties['data-aue-component'], 'section'); + }); + + it('adds UE attributes to richtext within sections', () => { + const bodyTree = h('body', {}, [ + h('main', {}, [ + h('div', {}, [ + h('h1', {}, [{ type: 'text', value: 'Heading 1' }]), + h('p', {}, [{ type: 'text', value: 'Paragraph 1' }]), + ]), + ]), + ]); - attributes.injectUEAttributes(bodyTree, ueConfig); + const ueConfig = { + 'component-definition': { + groups: [ + { + components: [ + { id: 'section', title: 'Section' }, + { id: 'text', title: 'Text' }, + ], + }, + ], + }, + }; + + attributes.injectUEAttributes(bodyTree, ueConfig); + + const richTextDiv = select('main > div > div', bodyTree); + assert.equal(richTextDiv.properties['data-aue-resource'], 'urn:ab:section-0/text-0'); + assert.equal(richTextDiv.properties['data-aue-type'], 'richtext'); + assert.equal(richTextDiv.properties['data-aue-label'], 'Text'); + assert.equal(richTextDiv.properties['data-aue-prop'], 'root'); + assert.equal(richTextDiv.properties.className, 'richtext'); + }); + + it('adds UE attributes to pictures within sections', () => { + const bodyTree = h('body', {}, [ + h('main', {}, [ + h('div', {}, [ + h('picture', {}, [ + h('img', {}, []), + ]), + ]), + ]), + ]); - const block = select('main > div > div', bodyTree); - assert.equal(block.properties['data-aue-resource'], 'urn:ab:section-0/block-0'); - assert.equal(block.properties['data-aue-type'], 'component'); - assert.equal(block.properties['data-aue-label'], 'Card Block'); - assert.equal(block.properties['data-aue-model'], 'card-block'); - }); + const ueConfig = { + 'component-definition': { + groups: [ + { + components: [ + { id: 'section', title: 'Section' }, + { id: 'image', title: 'Image' }, + ], + }, + ], + }, + }; + + attributes.injectUEAttributes(bodyTree, ueConfig); + + const image = select('main > div > picture', bodyTree); + assert.equal(image.properties['data-aue-resource'], 'urn:ab:section-0/asset-0'); + assert.equal(image.properties['data-aue-type'], 'media'); + assert.equal(image.properties['data-aue-label'], 'Image'); + assert.equal(image.properties['data-aue-component'], 'image'); + }); + + it('handles whitespace text nodes in wrapParagraphs', () => { + const bodyTree = h('body', {}, [ + h('main', {}, [ + h('div', {}, [ + h('h1', {}, [{ type: 'text', value: 'Heading 1' }]), + h('p', {}, [{ type: 'text', value: 'Paragraph 1' }]), + ]), + ]), + ]); - it('adds UE attributes to block items', () => { - const bodyTree = h('body', {}, [ - h('main', {}, [ - h('div', {}, [ - h('div', { className: ['cards'] }, [ - h('div', {}, []), - h('div', {}, []), + const ueConfig = { + 'component-definition': { + groups: [ + { + components: [ + { id: 'section', title: 'Section' }, + { id: 'text', title: 'Text' }, + ], + }, + ], + }, + }; + + attributes.injectUEAttributes(bodyTree, ueConfig); + + const section = select('main > div', bodyTree); + assert.equal(section.children.length, 1); + assert.equal(section.children[0].tagName, 'div'); + assert.equal(section.children[0].properties['data-aue-resource'], 'urn:ab:section-0/text-0'); + assert.equal(section.children[0].properties['data-aue-type'], 'richtext'); + assert.equal(section.children[0].properties['data-aue-label'], 'Text'); + assert.equal(section.children[0].children.length, 2); + assert.equal(section.children[0].children[0].tagName, 'h1'); + assert.equal(section.children[0].children[1].tagName, 'p'); + }); + + it('handles multiple consecutive richtext wrappers', () => { + const bodyTree = h('body', {}, [ + h('main', {}, [ + h('div', {}, [ + h('h1', {}, 'Heading 1'), + h('img', {}), + h('p', {}, 'Paragraph 1'), ]), ]), - ]), - ]); + ]); - const ueConfig = { - 'component-definition': { - groups: [ - { - components: [ - { id: 'section', title: 'Section' }, - { id: 'cards', title: 'Cards' }, - { id: 'card', title: 'Card' }, - ], - }, - ], - }, - 'component-filter': [ - { - id: 'cards', - components: ['card'], + const ueConfig = { + 'component-definition': { + groups: [ + { + components: [ + { id: 'section', title: 'Section' }, + { id: 'text', title: 'Text' }, + { id: 'image', title: 'Image' }, + ], + }, + ], }, - ], - }; - - attributes.injectUEAttributes(bodyTree, ueConfig); - - const blockItems = selectAll('main > div > div > div', bodyTree); - assert.equal(blockItems[0].properties['data-aue-resource'], 'urn:ab:section-0/block-0/item-0'); - assert.equal(blockItems[0].properties['data-aue-type'], 'component'); - assert.equal(blockItems[0].properties['data-aue-label'], 'Card'); - assert.equal(blockItems[0].properties['data-aue-model'], 'card'); - assert.equal(blockItems[1].properties['data-aue-resource'], 'urn:ab:section-0/block-0/item-1'); - assert.equal(blockItems[1].properties['data-aue-type'], 'component'); - assert.equal(blockItems[1].properties['data-aue-label'], 'Card'); - assert.equal(blockItems[1].properties['data-aue-model'], 'card'); + }; + + attributes.injectUEAttributes(bodyTree, ueConfig); + + const section = select('main > div', bodyTree); + assert.equal(section.children.length, 3); + assert.equal(section.children[0].tagName, 'div'); + assert.equal(section.children[0].properties['data-aue-resource'], 'urn:ab:section-0/text-0'); + assert.equal(section.children[0].properties['data-aue-type'], 'richtext'); + assert.equal(section.children[0].properties['data-aue-label'], 'Text'); + assert.equal(section.children[0].children.length, 1); + assert.equal(section.children[0].children[0].tagName, 'h1'); + assert.equal(section.children[1].tagName, 'img'); + assert.equal(section.children[2].tagName, 'div'); + assert.equal(section.children[2].properties['data-aue-resource'], 'urn:ab:section-0/text-1'); + assert.equal(section.children[2].properties['data-aue-type'], 'richtext'); + assert.equal(section.children[2].properties['data-aue-label'], 'Text'); + assert.equal(section.children[2].children.length, 1); + assert.equal(section.children[2].children[0].tagName, 'p'); + }); }); - it('adds UE attributes to body for page metadata', () => { - const bodyTree = h('body', {}, [ - h('main', {}, []), - ]); + describe('page metadata', () => { + it('adds UE attributes to body for page metadata', () => { + const bodyTree = h('body', {}, [ + h('main', {}, []), + ]); - const ueConfig = { - 'component-model': [ - { id: 'page-metadata', title: 'Page Metadata' }, - ], - 'component-definition': { - groups: [ - { - components: [ - { id: 'section', title: 'Section' }, - ], - }, + const ueConfig = { + 'component-model': [ + { id: 'page-metadata', title: 'Page Metadata' }, ], - }, - }; + 'component-definition': { + groups: [ + { + components: [ + { id: 'section', title: 'Section' }, + ], + }, + ], + }, + }; - attributes.injectUEAttributes(bodyTree, ueConfig); + attributes.injectUEAttributes(bodyTree, ueConfig); - // Check body attributes for page metadata - assert.equal(bodyTree.properties['data-aue-resource'], 'urn:ab:page'); - assert.equal(bodyTree.properties['data-aue-label'], 'Page'); - assert.equal(bodyTree.properties['data-aue-type'], 'component'); - assert.equal(bodyTree.properties['data-aue-model'], 'page-metadata'); - }); + // Check body attributes for page metadata + assert.equal(bodyTree.properties['data-aue-resource'], 'urn:ab:page'); + assert.equal(bodyTree.properties['data-aue-label'], 'Page'); + assert.equal(bodyTree.properties['data-aue-type'], 'component'); + assert.equal(bodyTree.properties['data-aue-model'], 'page-metadata'); + }); - it('does not add page metadata attributes when not defined in config', () => { - const bodyTree = h('body', {}, [ - h('main', {}, []), - ]); + it('does not add page metadata attributes when not defined in config', () => { + const bodyTree = h('body', {}, [ + h('main', {}, []), + ]); - const ueConfig = { - 'component-definition': { - groups: [ - { - components: [ - { id: 'section', title: 'Section' }, - ], - }, - ], - }, - }; + const ueConfig = { + 'component-definition': { + groups: [ + { + components: [ + { id: 'section', title: 'Section' }, + ], + }, + ], + }, + }; - attributes.injectUEAttributes(bodyTree, ueConfig); + attributes.injectUEAttributes(bodyTree, ueConfig); - // Check that body doesn't have page metadata attributes - assert.equal(bodyTree.properties['data-aue-resource'], undefined); - assert.equal(bodyTree.properties['data-aue-label'], undefined); - assert.equal(bodyTree.properties['data-aue-type'], undefined); - assert.equal(bodyTree.properties['data-aue-model'], undefined); + // Check that body doesn't have page metadata attributes + assert.equal(bodyTree.properties['data-aue-resource'], undefined); + assert.equal(bodyTree.properties['data-aue-label'], undefined); + assert.equal(bodyTree.properties['data-aue-type'], undefined); + assert.equal(bodyTree.properties['data-aue-component'], undefined); + }); }); - it('adds UE attributes to richtext within sections', () => { - const bodyTree = h('body', {}, [ - h('main', {}, [ - h('div', {}, [ - h('h1', {}, [{ type: 'text', value: 'Heading 1' }]), - h('p', {}, [{ type: 'text', value: 'Paragraph 1' }]), + describe('blocks and block attributes', () => { + it('adds UE attributes to blocks within sections', () => { + const bodyTree = h('body', {}, [ + h('main', {}, [ + h('div', {}, [ + h('div', { className: ['card-block'] }, []), + ]), ]), - ]), - ]); + ]); - const ueConfig = { - 'component-definition': { - groups: [ + const ueConfig = { + 'component-definition': { + groups: [ + { + components: [ + { id: 'section', title: 'Section' }, + { id: 'card-block', title: 'Card Block' }, + ], + }, + ], + }, + }; + + attributes.injectUEAttributes(bodyTree, ueConfig); + + const block = select('main > div > div', bodyTree); + assert.equal(block.properties['data-aue-resource'], 'urn:ab:section-0/block-0'); + assert.equal(block.properties['data-aue-type'], 'component'); + assert.equal(block.properties['data-aue-label'], 'Card Block'); + assert.equal(block.properties['data-aue-component'], 'card-block'); + }); + + it('adds UE attributes to block items', () => { + const bodyTree = h('body', {}, [ + h('main', {}, [ + h('div', {}, [ + h('div', { className: ['cards'] }, [ + h('div', {}, []), + h('div', {}, []), + ]), + ]), + ]), + ]); + + const ueConfig = { + 'component-definition': { + groups: [ + { + components: [ + { id: 'section', title: 'Section' }, + { id: 'cards', title: 'Cards' }, + { id: 'card', title: 'Card' }, + ], + }, + ], + }, + 'component-filter': [ { - components: [ - { id: 'section', title: 'Section' }, - { id: 'text', title: 'Text' }, - ], + id: 'cards', + components: ['card'], }, ], - }, - }; - - attributes.injectUEAttributes(bodyTree, ueConfig); - - const richTextDiv = select('main > div > div', bodyTree); - assert.equal(richTextDiv.properties['data-aue-resource'], 'urn:ab:section-0/text-0'); - assert.equal(richTextDiv.properties['data-aue-type'], 'richtext'); - assert.equal(richTextDiv.properties['data-aue-label'], 'Text'); - assert.equal(richTextDiv.properties['data-aue-prop'], 'root'); - assert.equal(richTextDiv.properties.className, 'richtext'); - }); - - it('adds UE attributes to pictures within sections', () => { - const bodyTree = h('body', {}, [ - h('main', {}, [ - h('div', {}, [ - h('picture', {}, [ - h('img', {}, []), + }; + + attributes.injectUEAttributes(bodyTree, ueConfig); + + const blockItems = selectAll('main > div > div > div', bodyTree); + assert.equal(blockItems[0].properties['data-aue-resource'], 'urn:ab:section-0/block-0/item-0'); + assert.equal(blockItems[0].properties['data-aue-type'], 'component'); + assert.equal(blockItems[0].properties['data-aue-label'], 'Card'); + assert.equal(blockItems[0].properties['data-aue-component'], 'card'); + assert.equal(blockItems[1].properties['data-aue-resource'], 'urn:ab:section-0/block-0/item-1'); + assert.equal(blockItems[1].properties['data-aue-type'], 'component'); + assert.equal(blockItems[1].properties['data-aue-label'], 'Card'); + }); + + it('adds UE attributes to block fields based on fields and model definition', () => { + const bodyTree = h('body', {}, [ + h('main', {}, [ + h('div', {}, [ // the section + h('div', { className: ['hero-block'] }, [ // the heroblock + h('div', {}, [ + h('div', {}, [ + h('picture', {}, [ + h('img', { src: 'img.jpg', alt: 'Hero Image' }), + ]), + h('h1', {}, [{ type: 'text', value: 'Hero Text' }]), + ]), + ]), + ]), ]), ]), - ]), - ]); + ]); - const ueConfig = { - 'component-definition': { - groups: [ + const ueConfig = { + 'component-definition': { + groups: [ + { + components: [ + { id: 'section', title: 'Section' }, + { + id: 'hero-block', + title: 'Hero', + model: 'hero-block', + plugins: { + da: { + rows: 1, + columns: 1, + fields: [ + { + name: 'image', + selector: 'div>div>picture>img[src]', + }, + { + name: 'imageAlt', + selector: 'div>div>picture>img[alt]', + }, + { + name: 'text', + selector: 'div>div>h1', + }, + ], + }, + }, + }, + ], + }, + ], + }, + 'component-model': [ { - components: [ - { id: 'section', title: 'Section' }, - { id: 'image', title: 'Image' }, + id: 'hero-block', + fields: [ + { + name: 'text', + label: 'Hero Text', + component: 'richtext', + }, + { + name: 'image', + label: 'Hero Image', + component: 'reference', + }, + { + name: 'imageAlt', + label: 'Hero Image Alt', + component: 'text', + }, ], }, ], - }, - }; - - attributes.injectUEAttributes(bodyTree, ueConfig); - - const image = select('main > div > picture', bodyTree); - assert.equal(image.properties['data-aue-resource'], 'urn:ab:section-0/asset-0'); - assert.equal(image.properties['data-aue-type'], 'media'); - assert.equal(image.properties['data-aue-label'], 'Image'); - assert.equal(image.properties['data-aue-model'], 'image'); + }; + + attributes.injectUEAttributes(bodyTree, ueConfig); + + const textField = select('main div.hero-block > div > div > h1', bodyTree); + assert.equal(textField.properties['data-aue-type'], 'richtext'); + assert.equal(textField.properties['data-aue-prop'], 'text'); + assert.equal(textField.properties['data-aue-label'], 'Hero Text'); + + const imageField = select('main div.hero-block > div > div > picture > img', bodyTree); + assert.equal(imageField.properties['data-aue-type'], 'media'); + assert.equal(imageField.properties['data-aue-prop'], 'image'); + assert.equal(imageField.properties['data-aue-label'], 'Hero Image'); + assert.equal(imageField.properties.src, 'img.jpg'); + assert.equal(imageField.properties.alt, 'Hero Image'); + }); }); - it('adds UE attributes to block fields based on model definition', () => { - const bodyTree = h('body', {}, [ - h('main', {}, [ - h('div', {}, [ - h('div', { className: ['hero-block'] }, [ - h('div', {}, [{ type: 'text', value: 'Hero Text' }]), - h('picture', {}, [ - h('img', {}, []), + describe('column blocks', () => { + it('adds UE attributes to column structure with proper hierarchy', () => { + const bodyTree = h('body', {}, [ + h('main', {}, [ + h('div', {}, [ + h('div', { className: ['custom-columns'] }, [ + h('div', {}, [ + h('div', {}, [h('p', {}, 'Cell 1')]), + h('div', {}, [h('p', {}, 'Cell 2')]), + ]), + h('div', {}, [ + h('div', {}, [h('p', {}, 'Cell 3')]), + h('div', {}, [h('p', {}, 'Cell 4')]), + ]), ]), - h('div', {}, [{ type: 'text', value: 'Array Field' }]), ]), ]), - ]), - ]); + ]); - const ueConfig = { - 'component-definition': { - groups: [ + const ueConfig = { + 'component-definition': { + groups: [ + { + components: [ + { id: 'section', title: 'Section' }, + { + id: 'custom-columns', + title: 'Custom Columns', + plugins: { + da: { + behaviour: 'columns', + }, + }, + }, + ], + }, + ], + }, + 'component-filter': [ { - components: [ - { id: 'section', title: 'Section' }, - { id: 'hero-block', title: 'Hero Block' }, - ], + id: 'custom-columns-cell', + components: ['text', 'image'], }, ], - }, - 'component-model': [ - { - id: 'hero-block', - title: 'Hero Block', - fields: [ - { - name: 'div:first-child', - label: 'Hero Text', - component: 'richtext', - }, - { - name: 'picture', - label: 'Hero Image', - component: 'reference', - }, + }; + + attributes.injectUEAttributes(bodyTree, ueConfig); + + // Test column block attributes + const columnBlock = select('main > div > div', bodyTree); + assert.equal(columnBlock.properties['data-aue-resource'], 'urn:ab:section-0/columns-0'); + assert.equal(columnBlock.properties['data-aue-label'], 'Custom Columns'); + assert.equal(columnBlock.properties['data-aue-component'], 'custom-columns'); + assert.equal(columnBlock.properties['data-aue-type'], 'container'); + + // Test first row attributes + const firstRow = select('main > div > div > div:first-child', bodyTree); + assert.equal(firstRow.properties['data-aue-resource'], 'urn:ab:section-0/columns-0/row-0'); + assert.equal(firstRow.properties['data-aue-label'], 'Custom Columns Row'); + assert.equal(firstRow.properties['data-aue-component'], 'custom-columns-row'); + assert.equal(firstRow.properties['data-aue-type'], 'container'); + + // Test first cell attributes + const firstCell = select('main > div > div > div:first-child > div:first-child', bodyTree); + assert.equal(firstCell.properties['data-aue-resource'], 'urn:ab:section-0/columns-0/row-0/cell-0'); + assert.equal(firstCell.properties['data-aue-label'], 'Custom Columns Cell'); + assert.equal(firstCell.properties['data-aue-component'], 'custom-columns-cell'); + assert.equal(firstCell.properties['data-aue-type'], 'container'); + + // Test second row attributes + const secondRow = select('main > div > div > div:last-child', bodyTree); + assert.equal(secondRow.properties['data-aue-resource'], 'urn:ab:section-0/columns-0/row-1'); + assert.equal(secondRow.properties['data-aue-label'], 'Custom Columns Row'); + assert.equal(secondRow.properties['data-aue-component'], 'custom-columns-row'); + assert.equal(secondRow.properties['data-aue-type'], 'container'); + + // Test second row first cell attributes + const secondRowFirstCell = select('main > div > div > div:last-child > div:first-child', bodyTree); + assert.equal(secondRowFirstCell.properties['data-aue-resource'], 'urn:ab:section-0/columns-0/row-1/cell-0'); + assert.equal(secondRowFirstCell.properties['data-aue-label'], 'Custom Columns Cell'); + assert.equal(secondRowFirstCell.properties['data-aue-component'], 'custom-columns-cell'); + assert.equal(secondRowFirstCell.properties['data-aue-type'], 'container'); + }); + + it('instruments picture and richtext inside column cells', () => { + const bodyTree = h('body', {}, [ + h('main', {}, [ + h('div', {}, [ + h('div', { className: ['custom-columns'] }, [ + h('div', {}, [ + h('div', {}, [ + h('picture', {}, [h('img', { src: 'img.jpg' })]), + { type: 'text', value: 'Some text' }, + ]), + ]), + ]), + ]), + ]), + ]); + + const ueConfig = { + 'component-definition': { + groups: [ { - name: 'div:last-child', - label: 'Array Field', - component: 'text', + components: [ + { + id: 'custom-columns', + title: 'Custom Columns', + plugins: { + da: { + behaviour: 'columns', + }, + }, + }, + ], }, ], }, - ], - }; - - attributes.injectUEAttributes(bodyTree, ueConfig); - - const textField = select('main > div > div > div:first-child', bodyTree); - assert.equal(textField.properties['data-aue-type'], 'richtext'); - assert.equal(textField.properties['data-aue-prop'], 'div:first-child'); - assert.equal(textField.properties['data-aue-label'], 'Hero Text'); - - const imageField = select('main > div > div > picture', bodyTree); - assert.equal(imageField.properties['data-aue-type'], 'media'); - assert.equal(imageField.properties['data-aue-prop'], 'picture'); - assert.equal(imageField.properties['data-aue-label'], 'Hero Image'); - - const arrayField = select('main > div > div > div:last-child', bodyTree); - assert.equal(arrayField.properties['data-aue-type'], 'text'); - assert.equal(arrayField.properties['data-aue-prop'], 'div:last-child'); - assert.equal(arrayField.properties['data-aue-label'], 'Array Field'); + 'component-filter': [ + { + id: 'custom-columns-cell', + components: ['text', 'image'], // triggers instrumentation + }, + ], + }; + + attributes.injectUEAttributes(bodyTree, ueConfig); + + // Find the cell + const cell = select('main > div > div > div > div', bodyTree); + // Picture instrumentation + const picture = select('picture', cell); + assert.ok(picture, 'Picture element exists'); + assert.equal(picture.properties['data-aue-resource'], 'urn:ab:section-0/columns-0/row-0/cell-0/image-0'); + assert.equal(picture.properties['data-aue-label'], 'Image'); + assert.equal(picture.properties['data-aue-prop'], 'image'); + assert.equal(picture.properties['data-aue-type'], 'media'); + assert.equal(picture.properties['data-aue-component'], 'image'); + + // Richtext instrumentation + const richtext = select('div.richtext', cell); + assert.ok(richtext, 'Richtext wrapper exists'); + assert.equal(richtext.properties['data-aue-resource'], 'urn:ab:section-0/columns-0/row-0/cell-0/text-0'); + assert.equal(richtext.properties['data-aue-type'], 'richtext'); + assert.equal(richtext.properties['data-aue-label'], 'Text'); + assert.equal(richtext.properties['data-aue-prop'], 'root'); + assert.equal(richtext.properties['data-aue-behavior'], 'component'); + }); }); - it('handles whitespace text nodes in wrapParagraphs', () => { - const bodyTree = h('body', {}, [ - h('main', {}, [ - h('div', {}, [ - h('h1', {}, [{ type: 'text', value: 'Heading 1' }]), - h('p', {}, [{ type: 'text', value: 'Paragraph 1' }]), + describe('key-value blocks', () => { + it('adds UE attributes to key-value block fields full block instrumentation', () => { + const bodyTree = h('body', {}, [ + h('main', {}, [ + h('div', {}, [ // the section + h('div', { className: ['keyvalue-block'] }, [// the keyvalue block + h('div', {}, [ // the first row + h('div', {}, [h('p', {}, 'key1')]), + h('div', {}, [h('p', {}, 'text value 1')]), + ]), + h('div', {}, [ // the second row + h('div', {}, [h('p', {}, 'key2')]), + h('div', {}, [ + h('h1', {}, 'Section Title'), + h('p', {}, [ + 'Some paragraph text with ', + h('strong', {}, 'some text in bold'), + '.', + ]), + ]), + ]), + h('div', {}, [ // the third row + h('div', {}, [h('p', {}, 'key3')]), + h('div', {}, [h('img', { src: 'img3.jpg', alt: 'Image 3' })]), + ]), + ]), + ]), ]), - ]), - ]); + ]); - const ueConfig = { - 'component-definition': { - groups: [ + const ueConfig = { + 'component-definition': { + groups: [ + { + components: [ + { id: 'section', title: 'Section' }, + { + id: 'keyvalue-block', + title: 'Key Value Block Sample', + model: 'keyvalue-block', + plugins: { + da: { + type: 'key-value-block', + }, + }, + }, + ], + }, + ], + }, + 'component-model': [ { - components: [ - { id: 'section', title: 'Section' }, - { id: 'text', title: 'Text' }, + id: 'keyvalue-block', + fields: [ + { + name: 'key1', + label: 'Key 1', + component: 'text', + }, + { + name: 'key2', + label: 'Key 2', + component: 'richtext', + }, + { + name: 'key3', + component: 'reference', + }, ], }, ], - }, - }; - - attributes.injectUEAttributes(bodyTree, ueConfig); - - const section = select('main > div', bodyTree); - assert.equal(section.children.length, 1); - assert.equal(section.children[0].tagName, 'div'); - assert.equal(section.children[0].properties['data-aue-resource'], 'urn:ab:section-0/text-0'); - assert.equal(section.children[0].properties['data-aue-type'], 'richtext'); - assert.equal(section.children[0].properties['data-aue-label'], 'Text'); - assert.equal(section.children[0].children.length, 2); - assert.equal(section.children[0].children[0].tagName, 'h1'); - assert.equal(section.children[0].children[1].tagName, 'p'); - }); - - it('handles multiple consecutive richtext wrappers', () => { - const bodyTree = h('body', {}, [ - h('main', {}, [ - h('div', {}, [ - h('h1', {}, 'Heading 1'), - h('img', {}), - h('p', {}, 'Paragraph 1'), + }; + + attributes.injectUEAttributes(bodyTree, ueConfig); + + const block = select('main div.keyvalue-block', bodyTree); + assert.equal(block.properties['data-aue-resource'], 'urn:ab:section-0/block-0'); + assert.equal(block.properties['data-aue-type'], 'component'); + assert.equal(block.properties['data-aue-label'], 'Key Value Block Sample'); + assert.equal(block.properties['data-aue-component'], 'keyvalue-block'); + + const row1 = select('main div.keyvalue-block > div:first-child', bodyTree); + const key1cell = select('div:first-child', row1); + assert.equal(toString(key1cell), 'key1'); + const text1cell = select('div:last-child', row1); + assert.equal(text1cell.properties['data-aue-type'], 'text'); + assert.equal(text1cell.properties['data-aue-prop'], 'key1'); + assert.equal(text1cell.properties['data-aue-label'], 'Key 1'); + + const row2 = select('main div.keyvalue-block > div:nth-child(2)', bodyTree); + const key2cell = select('div:first-child', row2); + assert.equal(toString(key2cell), 'key2'); + const text2cell = select('div:last-child', row2); + assert.equal(text2cell.properties['data-aue-type'], 'richtext'); + assert.equal(text2cell.properties['data-aue-prop'], 'key2'); + assert.equal(text2cell.properties['data-aue-label'], 'Key 2'); + + const row3 = select('main div.keyvalue-block > div:last-child', bodyTree); + const key3cell = select('div:first-child', row3); + assert.equal(toString(key3cell), 'key3'); + const image3cell = select('div:last-child', row3); + assert.equal(image3cell.properties['data-aue-type'], 'media'); + assert.equal(image3cell.properties['data-aue-prop'], 'key3'); + assert.equal(image3cell.properties['data-aue-label'], 'key3'); // fallback to the key name + const img3Tag = select('img', image3cell); + assert.equal(img3Tag.properties.src, 'img3.jpg'); + assert.equal(img3Tag.properties.alt, 'Image 3'); + }); + + it('adds UE attributes to key-value block fields partially block instrumentation 1', () => { + const bodyTree = h('body', {}, [ + h('main', {}, [ + h('div', {}, [ // the section + h('div', { className: ['keyvalue-block'] }, [// the keyvalue block + h('div', {}, [ // the first row + h('div', {}, [h('p', {}, 'key1')]), + h('div', {}, [h('p', {}, 'text value 1')]), + ]), + h('div', {}, [ // the second row + h('div', {}, [h('p', {}, '')]), + h('div', {}, [ + h('h1', {}, 'Section Title'), + h('p', {}, [ + 'Some paragraph text with ', + h('strong', {}, 'some text in bold'), + '.', + ]), + ]), + ]), + h('div', {}, [ // the third row + h('div', {}, [h('p', {}, 'key3')]), + h('div', {}, [h('img', { src: 'img3.jpg', alt: 'Image 3' })]), + ]), + ]), + ]), ]), - ]), - ]); + ]); - const ueConfig = { - 'component-definition': { - groups: [ + const ueConfig = { + 'component-definition': { + groups: [ + { + components: [ + { id: 'section', title: 'Section' }, + { + id: 'keyvalue-block', + title: 'Key Value Block Sample', + model: 'keyvalue-block', + plugins: { + da: { + type: 'key-value-block', + }, + }, + }, + ], + }, + ], + }, + 'component-model': [ { - components: [ - { id: 'section', title: 'Section' }, - { id: 'text', title: 'Text' }, - { id: 'image', title: 'Image' }, + id: 'keyvalue-block', + fields: [ + { + name: 'key1', + label: 'Key 1', + component: 'text', + }, + { + name: 'key2', + label: 'Key 2', + component: 'richtext', + }, ], }, ], - }, - }; - - attributes.injectUEAttributes(bodyTree, ueConfig); - - const section = select('main > div', bodyTree); - assert.equal(section.children.length, 3); - assert.equal(section.children[0].tagName, 'div'); - assert.equal(section.children[0].properties['data-aue-resource'], 'urn:ab:section-0/text-0'); - assert.equal(section.children[0].properties['data-aue-type'], 'richtext'); - assert.equal(section.children[0].properties['data-aue-label'], 'Text'); - assert.equal(section.children[0].children.length, 1); - assert.equal(section.children[0].children[0].tagName, 'h1'); - assert.equal(section.children[1].tagName, 'img'); - assert.equal(section.children[2].tagName, 'div'); - assert.equal(section.children[2].properties['data-aue-resource'], 'urn:ab:section-0/text-1'); - assert.equal(section.children[2].properties['data-aue-type'], 'richtext'); - assert.equal(section.children[2].properties['data-aue-label'], 'Text'); - assert.equal(section.children[2].children.length, 1); - assert.equal(section.children[2].children[0].tagName, 'p'); - }); - - it('adds UE attributes to column structure with proper hierarchy', () => { - const bodyTree = h('body', {}, [ - h('main', {}, [ - h('div', {}, [ - h('div', { className: ['custom-columns'] }, [ - h('div', {}, [ - h('div', {}, [h('p', {}, 'Cell 1')]), - h('div', {}, [h('p', {}, 'Cell 2')]), - ]), - h('div', {}, [ - h('div', {}, [h('p', {}, 'Cell 3')]), - h('div', {}, [h('p', {}, 'Cell 4')]), + }; + + attributes.injectUEAttributes(bodyTree, ueConfig); + + const block = select('main div.keyvalue-block', bodyTree); + assert.equal(block.properties['data-aue-resource'], 'urn:ab:section-0/block-0'); + assert.equal(block.properties['data-aue-type'], 'component'); + assert.equal(block.properties['data-aue-label'], 'Key Value Block Sample'); + assert.equal(block.properties['data-aue-component'], 'keyvalue-block'); + + const row1 = select('main div.keyvalue-block > div:first-child', bodyTree); + const key1cell = select('div:first-child', row1); + assert.equal(toString(key1cell), 'key1'); + const text1cell = select('div:last-child', row1); + assert.equal(text1cell.properties['data-aue-type'], 'text'); + assert.equal(text1cell.properties['data-aue-prop'], 'key1'); + assert.equal(text1cell.properties['data-aue-label'], 'Key 1'); + + const row2 = select('main div.keyvalue-block > div:nth-child(2)', bodyTree); + const text2cell = select('div:last-child', row2); + // no key 2 in first column, so no UE attributes should be added + assert.deepEqual(text2cell.properties, {}); + }); + + it('adds UE attributes to key-value block fields partially block instrumentation 2', () => { + const bodyTree = h('body', {}, [ + h('main', {}, [ + h('div', {}, [ // the section + h('div', { className: ['keyvalue-block'] }, [// the keyvalue block + h('div', {}, [ // the first row + h('div', {}, [h('p', {}, 'key1')]), + h('div', {}, [h('p', {}, 'text value 1')]), + ]), + h('div', {}, [ // the second row + h('div', {}, [h('p', {}, 'key2')]), + h('div', {}, [ + h('h1', {}, 'Section Title'), + h('p', {}, [ + 'Some paragraph text with ', + h('strong', {}, 'some text in bold'), + '.', + ]), + ]), + ]), + h('div', {}, [ // the third row + h('div', {}, [h('p', {}, 'key3')]), + h('div', {}, [h('img', { src: 'img3.jpg', alt: 'Image 3' })]), + ]), ]), ]), ]), - ]), - ]); + ]); - const ueConfig = { - 'component-definition': { - groups: [ - { - components: [ - { id: 'section', title: 'Section' }, - { - id: 'custom-columns', - title: 'Custom Columns', - plugins: { - da: { - behaviour: 'columns', + const ueConfig = { + 'component-definition': { + groups: [ + { + components: [ + { id: 'section', title: 'Section' }, + { + id: 'keyvalue-block', + title: 'Key Value Block Sample', + model: 'keyvalue-block', + plugins: { + da: { + type: 'key-value-block', + }, }, }, + ], + }, + ], + }, + 'component-model': [ + { + id: 'keyvalue-block', + fields: [ + { + name: 'key1', + label: 'Key 1', + component: 'text', + }, + { + name: 'key3', + component: 'reference', }, ], }, ], - }, - 'component-filter': [ - { - id: 'custom-columns-cell', - components: ['text', 'image'], - }, - ], - }; - - attributes.injectUEAttributes(bodyTree, ueConfig); - - // Test column block attributes - const columnBlock = select('main > div > div', bodyTree); - assert.equal(columnBlock.properties['data-aue-resource'], 'urn:ab:section-0/columns-0'); - assert.equal(columnBlock.properties['data-aue-label'], 'Custom Columns'); - assert.equal(columnBlock.properties['data-aue-model'], 'custom-columns'); - assert.equal(columnBlock.properties['data-aue-filter'], 'custom-columns'); - assert.equal(columnBlock.properties['data-aue-type'], 'container'); - assert.equal(columnBlock.properties['data-aue-behavior'], 'component'); - - // Test first row attributes - const firstRow = select('main > div > div > div:first-child', bodyTree); - assert.equal(firstRow.properties['data-aue-resource'], 'urn:ab:section-0/columns-0/row-0'); - assert.equal(firstRow.properties['data-aue-label'], 'Custom Columns Row'); - assert.equal(firstRow.properties['data-aue-model'], 'custom-columns-row'); - assert.equal(firstRow.properties['data-aue-filter'], 'custom-columns-row'); - assert.equal(firstRow.properties['data-aue-type'], 'container'); - assert.equal(firstRow.properties['data-aue-behavior'], 'component'); - - // Test first cell attributes - const firstCell = select('main > div > div > div:first-child > div:first-child', bodyTree); - assert.equal(firstCell.properties['data-aue-resource'], 'urn:ab:section-0/columns-0/row-0/cell-0'); - assert.equal(firstCell.properties['data-aue-label'], 'Custom Columns Cell'); - assert.equal(firstCell.properties['data-aue-model'], 'custom-columns-cell'); - assert.equal(firstCell.properties['data-aue-filter'], 'custom-columns-cell'); - assert.equal(firstCell.properties['data-aue-type'], 'container'); - assert.equal(firstCell.properties['data-aue-behavior'], 'component'); - - // Test second row attributes - const secondRow = select('main > div > div > div:last-child', bodyTree); - assert.equal(secondRow.properties['data-aue-resource'], 'urn:ab:section-0/columns-0/row-1'); - assert.equal(secondRow.properties['data-aue-label'], 'Custom Columns Row'); - assert.equal(secondRow.properties['data-aue-model'], 'custom-columns-row'); - assert.equal(secondRow.properties['data-aue-filter'], 'custom-columns-row'); - assert.equal(secondRow.properties['data-aue-type'], 'container'); - assert.equal(secondRow.properties['data-aue-behavior'], 'component'); - - // Test second row first cell attributes - const secondRowFirstCell = select('main > div > div > div:last-child > div:first-child', bodyTree); - assert.equal(secondRowFirstCell.properties['data-aue-resource'], 'urn:ab:section-0/columns-0/row-1/cell-0'); - assert.equal(secondRowFirstCell.properties['data-aue-label'], 'Custom Columns Cell'); - assert.equal(secondRowFirstCell.properties['data-aue-model'], 'custom-columns-cell'); - assert.equal(secondRowFirstCell.properties['data-aue-filter'], 'custom-columns-cell'); - assert.equal(secondRowFirstCell.properties['data-aue-type'], 'container'); - assert.equal(secondRowFirstCell.properties['data-aue-behavior'], 'component'); - }); - - it('instruments picture and richtext inside column cells', () => { - const bodyTree = h('body', {}, [ - h('main', {}, [ - h('div', {}, [ - h('div', { className: ['custom-columns'] }, [ - h('div', {}, [ - h('div', {}, [ - h('picture', {}, [h('img', { src: 'img.jpg' })]), - { type: 'text', value: 'Some text' }, + }; + + attributes.injectUEAttributes(bodyTree, ueConfig); + + const block = select('main div.keyvalue-block', bodyTree); + assert.equal(block.properties['data-aue-resource'], 'urn:ab:section-0/block-0'); + assert.equal(block.properties['data-aue-type'], 'component'); + assert.equal(block.properties['data-aue-label'], 'Key Value Block Sample'); + assert.equal(block.properties['data-aue-component'], 'keyvalue-block'); + + const row1 = select('main div.keyvalue-block > div:first-child', bodyTree); + const key1cell = select('div:first-child', row1); + assert.equal(toString(key1cell), 'key1'); + const text1cell = select('div:last-child', row1); + assert.equal(text1cell.properties['data-aue-type'], 'text'); + assert.equal(text1cell.properties['data-aue-prop'], 'key1'); + assert.equal(text1cell.properties['data-aue-label'], 'Key 1'); + + const row2 = select('main div.keyvalue-block > div:nth-child(2)', bodyTree); + const key2cell = select('div:first-child', row2); + assert.equal(toString(key2cell), 'key2'); + const text2cell = select('div:last-child', row2); + // no field defined for this key, so no UE attributes should be added + assert.deepEqual(text2cell.properties, {}); + + const row3 = select('main div.keyvalue-block > div:last-child', bodyTree); + const key3cell = select('div:first-child', row3); + assert.equal(toString(key3cell), 'key3'); + const image3cell = select('div:last-child', row3); + assert.equal(image3cell.properties['data-aue-type'], 'media'); + assert.equal(image3cell.properties['data-aue-prop'], 'key3'); + assert.equal(image3cell.properties['data-aue-label'], 'key3'); // fallback to the key name + const img3Tag = select('img', image3cell); + assert.equal(img3Tag.properties.src, 'img3.jpg'); + assert.equal(img3Tag.properties.alt, 'Image 3'); + }); + + it('adds no UE attributes to key-value block fields with no defined fields', () => { + const bodyTree = h('body', {}, [ + h('main', {}, [ + h('div', {}, [ // the section + h('div', { className: ['keyvalue-block'] }, [// the keyvalue block + h('div', {}, [ // the first row + h('div', {}, [h('p', {}, 'key1')]), + h('div', {}, [h('p', {}, 'text value 1')]), ]), ]), ]), ]), - ]), - ]); + ]); - const ueConfig = { - 'component-definition': { - groups: [ + const ueConfig = { + 'component-definition': { + groups: [ + { + components: [ + { id: 'section', title: 'Section' }, + { + id: 'keyvalue-block', + title: 'Key Value Block Sample', + model: 'keyvalue-block', + plugins: { + da: { + type: 'keyvalue-block', + }, + }, + }, + ], + }, + ], + }, + 'component-model': [ { - components: [ - { - id: 'custom-columns', - title: 'Custom Columns', - plugins: { - da: { - behaviour: 'columns', + id: 'keyvalue-block', + }, + ], + }; + + attributes.injectUEAttributes(bodyTree, ueConfig); + + const block = select('main div.keyvalue-block', bodyTree); + assert.equal(block.properties['data-aue-resource'], 'urn:ab:section-0/block-0'); + assert.equal(block.properties['data-aue-type'], 'component'); + assert.equal(block.properties['data-aue-label'], 'Key Value Block Sample'); + assert.equal(block.properties['data-aue-component'], 'keyvalue-block'); + + const row1 = select('main div.keyvalue-block > div:first-child', bodyTree); + const key1cell = select('div:first-child', row1); + assert.equal(toString(key1cell), 'key1'); + const text1cell = select('div:last-child', row1); + // no field defined for this key, so no UE attributes should be added + assert.deepEqual(text1cell.properties, {}); + }); + + it('adds no UE attributes to key-value block with invalid column count', () => { + const bodyTree = h('body', {}, [ + h('main', {}, [ + h('div', {}, [ // the section + h('div', { className: ['keyvalue-block'] }, [// the keyvalue block + h('div', {}, [ // the first row + h('div', {}, [h('p', {}, 'key1')]), + h('div', {}, [h('p', {}, 'text value 1')]), + h('div', {}, [h('p', {}, 'text value 2')]), + ]), + ]), + ]), + ]), + ]); + + const ueConfig = { + 'component-definition': { + groups: [ + { + components: [ + { id: 'section', title: 'Section' }, + { + id: 'keyvalue-block', + title: 'Key Value Block Sample', + model: 'keyvalue-block', + plugins: { + da: { + type: 'key-value-block', + }, }, }, + ], + }, + ], + }, + 'component-model': [ + { + id: 'keyvalue-block', + fields: [ + { + name: 'key1', + label: 'Key 1', + component: 'text', }, ], }, ], - }, - 'component-filter': [ - { - id: 'custom-columns-cell', - components: ['text', 'image'], // triggers instrumentation - }, - ], - }; - - attributes.injectUEAttributes(bodyTree, ueConfig); - - // Find the cell - const cell = select('main > div > div > div > div', bodyTree); - // Picture instrumentation - const picture = select('picture', cell); - assert.ok(picture, 'Picture element exists'); - assert.equal(picture.properties['data-aue-resource'], 'urn:ab:section-0/columns-0/row-0/cell-0/image-0'); - assert.equal(picture.properties['data-aue-label'], 'Image'); - assert.equal(picture.properties['data-aue-behavior'], 'component'); - assert.equal(picture.properties['data-aue-prop'], 'image'); - assert.equal(picture.properties['data-aue-type'], 'container'); - assert.equal(picture.properties['data-aue-model'], 'image'); - - // Richtext instrumentation - const richtext = select('div.richtext', cell); - assert.ok(richtext, 'Richtext wrapper exists'); - assert.equal(richtext.properties['data-aue-resource'], 'urn:ab:section-0/columns-0/row-0/cell-0/text-0'); - assert.equal(richtext.properties['data-aue-type'], 'richtext'); - assert.equal(richtext.properties['data-aue-label'], 'Text'); - assert.equal(richtext.properties['data-aue-prop'], 'root'); - assert.equal(richtext.properties['data-aue-behavior'], 'component'); + }; + + attributes.injectUEAttributes(bodyTree, ueConfig); + + const block = select('main div.keyvalue-block', bodyTree); + assert.equal(block.properties['data-aue-resource'], 'urn:ab:section-0/block-0'); + assert.equal(block.properties['data-aue-type'], 'component'); + assert.equal(block.properties['data-aue-label'], 'Key Value Block Sample'); + assert.equal(block.properties['data-aue-component'], 'keyvalue-block'); + + const row1 = select('main div.keyvalue-block > div:first-child', bodyTree); + const key1cell = select('div:first-child', row1); + assert.equal(toString(key1cell), 'key1'); + const text1cell = select('div:last-child', row1); + // no field defined for this key, so no UE attributes should be added + assert.deepEqual(text1cell.properties, {}); + }); }); }); @@ -706,7 +1150,7 @@ describe('UE attributes', () => { 'data-aue-resource': 'urn:ab:section-0/block-0', 'data-aue-type': 'component', 'data-aue-label': 'Hero', - 'data-aue-model': 'hero', + 'data-aue-component': 'hero', }, [ h('div', {}, [ h('div', { diff --git a/test/ue/scaffold.test.js b/test/ue/scaffold.test.js index 9f89d01..828ba56 100644 --- a/test/ue/scaffold.test.js +++ b/test/ue/scaffold.test.js @@ -235,6 +235,20 @@ describe('UE scaffold', () => { '/component-filters.json', ); }); + + it('overrides ueService to localhost:8000 when ueService parameter is "local"', () => { + daCtx.ueService = 'local'; + + const entries = scaffold.getUEHtmlHeadEntries(daCtx, aemCtx); + + // Check service meta tag is overridden to localhost:8000 + const serviceTag = entries.find( + (entry) => entry.tagName === 'meta' + && entry.properties.name === 'urn:adobe:aue:config:service', + ); + assert.ok(serviceTag); + assert.strictEqual(serviceTag.properties.content, 'https://localhost:8000'); + }); }); describe('getUEConfig', () => { diff --git a/test/utils/daCtx.test.js b/test/utils/daCtx.test.js index 4677907..23656d9 100644 --- a/test/utils/daCtx.test.js +++ b/test/utils/daCtx.test.js @@ -146,4 +146,18 @@ describe('DA context', () => { assert.ifError(daCtx.site); }); }); + + describe('ue-service query parameter', () => { + it('should set ueService when ue-service query parameter is present as string', () => { + const req = new Request('https://main--site--org.ue.da.live/folder/content?ue-service=local'); + daCtx = getDaCtx(req); + assert.strictEqual(daCtx.ueService, 'local'); + }); + + it('should set ueService with empty string when ue-service is empty', () => { + const req = new Request('https://main--site--org.ue.da.live/folder/content?ue-service='); + daCtx = getDaCtx(req); + assert.strictEqual(daCtx.ueService, ''); + }); + }); }); diff --git a/wrangler.toml b/wrangler.toml index 2562cb7..79e0516 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -10,11 +10,11 @@ port = 4712 [env.dev] vars = { ENVIRONMENT = "dev", UE_HOST = "localhost:4712", UE_SERVICE = "https://localhost:8000", DA_ADMIN = "https://admin.da.live" } -services = [{ binding = "daadmin", service = "da-admin" }] +services = [{ binding = "daadmin", service = "da-admin-dev" }] [env.stage] vars = { ENVIRONMENT = "stage", UE_HOST = "stage-ue.da.live", UE_SERVICE = "https://universal-editor-service-dev.adobe.io", DA_ADMIN = "https://admin.da.live" } services = [{ binding = "daadmin", service = "da-admin" }] [env.stage.observability] -enabled = true \ No newline at end of file +enabled = true