diff --git a/playground/components/global/drupal-ce-container.ts b/playground/components/global/drupal-ce-container.ts new file mode 100644 index 0000000..a2da3fd --- /dev/null +++ b/playground/components/global/drupal-ce-container.ts @@ -0,0 +1,111 @@ +/** + * DrupalCeContainer Component + * + * A component that serves as a container for rendering Drupal custom elements. + * Supports two modes of operation: + * + * 1. Slot Mode - Using Vue slots for content + * 2. JSON Mode - Using content prop with array of elements + * + * @example Slot Mode Usage: + * ```vue + * + * Cell content before embed + * Some example embedded element. + * Cell content after embed + * + * ``` + * + * @example JSON Mode Usage: + * ```vue + * + * ``` + */ + +import { h, defineComponent } from 'vue'; +import type { VNode } from 'vue'; +// useDrupalCe is auto-imported by Nuxt + +export default defineComponent({ + name: 'DrupalCeContainer', + + props: { + /** + * HTML tag to use for the container + * @default 'div' + */ + tag: { + type: String, + default: 'div' + }, + + /** + * Array of custom elements to render (for JSON mode) + * Each element should have an 'element' property defining the component type + * @optional + */ + content: { + type: Array, + default: null + } + }, + + // @todo: Remove once issue #332 is fixed. + // We manually inherit attrs below. + inheritAttrs: false, + + /** + * @slot default - Content slot for custom elements in Slot mode + */ + + render(): VNode { + // useDrupalCe is auto-imported by Nuxt + const { renderCustomElements } = useDrupalCe(); + + // Determine which mode we're in - slot mode or JSON mode + const isJsonMode = Array.isArray(this.content); + + // Get all props except 'tag', 'content', and 'element' to pass to the container + const containerProps = { ...this.$attrs }; + + // Do not render the "element" prop passed by renderCustomElements() + // @todo: Remove once issue #332 is fixed. + if ('element' in containerProps) { + delete containerProps.element; + } + + let children; + + if (isJsonMode && this.content) { + // JSON mode - render each element in the content array + children = this.content.map(item => renderCustomElements(item)); + } else if (this.$slots.default) { + // Slot mode - use the default slot content + children = this.$slots.default(); + } else { + // No content + children = []; + } + + // Render the container with the appropriate tag and children. + return h(this.tag, containerProps, children); + } +}); diff --git a/test/unit/components/drupal-ce-container.test.ts b/test/unit/components/drupal-ce-container.test.ts new file mode 100644 index 0000000..30535dd --- /dev/null +++ b/test/unit/components/drupal-ce-container.test.ts @@ -0,0 +1,251 @@ +// @vitest-environment nuxt +import { describe, it, expect } from 'vitest' +import { mountSuspended } from '@nuxt/test-utils/runtime' +import { defineComponent } from 'vue' +import { useNuxtApp } from "#imports" + +describe('DrupalCeContainer', () => { + // Register test components + const DrupalMedia = defineComponent({ + name: 'DrupalMedia', + inheritAttrs: false, + props: { + id: String, + content: String + }, + template: '
{{ content }}
' + }) + + // Register components globally + const app = useNuxtApp() + app.vueApp.component('DrupalMedia', DrupalMedia) + // Note: DrupalMarkup is already available in the playground + + // Helper function to normalize HTML by removing whitespace between elements + const normalizeHtml = (html) => { + return html + .replace(/>\s+<') // Remove whitespace between tags + .replace(/\s+/g, ' ') // Replace multiple spaces with single space + .trim(); // Trim leading/trailing whitespace + } + + it('renders with slot-based content', async () => { + const wrapper = await mountSuspended(defineComponent({ + template: ` + + + + ` + })) + + const html = normalizeHtml(wrapper.html()) + + // Create a well-formatted expected HTML string for easy reading in the IDE + const expectedHtml = normalizeHtml(` +
Cell content before embed
Some example embedded element.
Cell content after embed
+ `) + + // Assert the exact HTML structure + expect(html).toEqual(expectedHtml) + }) + + it('renders with JSON-based content', async () => { + const { renderCustomElements } = useDrupalCe() + + const wrapper = await mountSuspended(defineComponent({ + setup() { + return { + component: renderCustomElements({ + element: 'drupal-ce-container', + tag: 'section', + class: 'test-container', + content: [ + { + 'element': 'drupal-markup', + 'content': 'Cell content before embed' + }, + { + 'element': 'drupal-media', + 'id': "123", + 'content': 'Some example embedded element.' + }, + { + 'element': 'drupal-markup', + 'content': 'Cell content after embed' + } + ] + }) + } + }, + template: '' + })) + + const html = normalizeHtml(wrapper.html()) + + // Create a well-formatted expected HTML string for easy reading in the IDE + // In JSON mode, DrupalMarkup components do add a wrapper div + const expectedHtml = normalizeHtml(` +
+
Cell content before embed
+
Some example embedded element.
+
Cell content after embed
+
+ `) + + // Assert the exact HTML structure + expect(html).toEqual(expectedHtml) + }) + + it('respects the default tag prop value', async () => { + const wrapper = await mountSuspended(defineComponent({ + template: ` + + + + ` + })) + + const html = normalizeHtml(wrapper.html()) + + // Create a well-formatted expected HTML string for easy reading in the IDE + const expectedHtml = normalizeHtml(` +
Test content
+ `) + + // Assert the exact HTML structure + expect(html).toEqual(expectedHtml) + }) + + it('works with empty content', async () => { + const wrapper = await mountSuspended(defineComponent({ + template: ` + + + ` + })) + + const html = normalizeHtml(wrapper.html()) + + // Create a well-formatted expected HTML string for easy reading in the IDE + const expectedHtml = normalizeHtml(` +
+ `) + + // Assert the exact HTML structure + expect(html).toEqual(expectedHtml) + }) + + it('renders table with embedded content using slot syntax', async () => { + const wrapper = await mountSuspended(defineComponent({ + template: ` + + + + + First column + + +

Media heading

+ +
+
+
+
+ ` + })) + + const html = normalizeHtml(wrapper.html()) + + // Create a well-formatted expected HTML string for easy reading in the IDE + const expectedHtml = normalizeHtml(` +
First column

Media heading

Media content
+ `) + + // Assert the exact HTML structure + expect(html).toEqual(expectedHtml) + }) + + it('renders table with embedded content using JSON syntax', async () => { + const { renderCustomElements } = useDrupalCe() + + const wrapper = await mountSuspended(defineComponent({ + setup() { + return { + component: renderCustomElements({ + element: 'drupal-ce-container', + tag: 'table', + content: [ + { + element: 'drupal-ce-container', + tag: 'tbody', + content: [ + { + element: 'drupal-ce-container', + tag: 'tr', + content: [ + { + element: 'drupal-ce-container', + tag: 'td', + content: [ + { + element: 'drupal-markup', + content: 'First column' + } + ] + }, + { + element: 'drupal-ce-container', + tag: 'td', + content: [ + { + element: 'drupal-markup', + content: '

Media heading

' + }, + { + element: 'drupal-media', + id: '123', + content: 'Media content' + } + ] + } + ] + } + ] + } + ] + }) + } + }, + template: '' + })) + + const html = normalizeHtml(wrapper.html()) + + // Create a well-formatted expected HTML string for easy reading in the IDE + const expectedHtml = normalizeHtml(` + + + + + + + +
+
First column
+
+

Media heading

+
Media content
+
+ `) + + // Assert the exact HTML structure + expect(html).toEqual(expectedHtml) + }) + +})