diff --git a/packages/happy-dom/src/config/HTMLElementConfig.ts b/packages/happy-dom/src/config/HTMLElementConfig.ts
index 41a9f0208..348b99fbb 100644
--- a/packages/happy-dom/src/config/HTMLElementConfig.ts
+++ b/packages/happy-dom/src/config/HTMLElementConfig.ts
@@ -126,7 +126,9 @@ export default <
},
caption: {
className: 'HTMLTableCaptionElement',
- contentModel: HTMLElementConfigContentModelEnum.textOrComments
+ contentModel: HTMLElementConfigContentModelEnum.noForbiddenFirstLevelDescendants,
+ forbiddenDescendants: ['table', 'tbody', 'thead', 'tfoot', 'tr', 'td', 'th', 'col', 'colgroup'],
+ permittedParents: ['table']
},
cite: {
className: 'HTMLElement',
diff --git a/packages/happy-dom/test/html-parser/HTMLParser.malformedHTML.test.ts b/packages/happy-dom/test/html-parser/HTMLParser.malformedHTML.test.ts
index 63da3eea8..d2fa23bbd 100644
--- a/packages/happy-dom/test/html-parser/HTMLParser.malformedHTML.test.ts
+++ b/packages/happy-dom/test/html-parser/HTMLParser.malformedHTML.test.ts
@@ -103,4 +103,162 @@ describe('HTMLParser - Malformed HTML handling (Issue #1949)', () => {
expect(new HTMLSerializer().serializeToString(result)).toBe('
first
second
');
});
});
+
+ /**
+ * Test cases for GitHub issue #2052:
+ * Incorrect DOM structure with elements
+ *
+ * Per the HTML spec, elements should contain flow content
+ * (including inline and block elements), except table elements.
+ *
+ * @see https://github.com/capricorn86/happy-dom/issues/2052
+ * @see https://html.spec.whatwg.org/multipage/tables.html#the-caption-element
+ */
+ describe('Table caption element content model (Issue #2052)', () => {
+ it('Should preserve inline elements inside - original issue with ', () => {
+ const doc = new window.DOMParser().parseFromString(
+ `
+
+ This is a caption.
+
+
+
+
`,
+ 'text/html'
+ );
+ const caption = doc.querySelector('caption');
+ const b = doc.querySelector('b');
+
+ // The element should be inside the caption
+ expect(caption?.contains(b)).toBe(true);
+ expect(caption?.innerHTML.trim()).toBe('This is a caption.');
+ });
+
+ it('Should preserve nested inline elements inside ', () => {
+ const doc = new window.DOMParser().parseFromString(
+ `
+
+ This is a caption.
+
+
+
`,
+ 'text/html'
+ );
+ const caption = doc.querySelector('caption');
+ const small = doc.querySelector('small');
+ const b = doc.querySelector('b');
+
+ // Both elements should be inside the caption
+ expect(caption?.contains(small)).toBe(true);
+ expect(caption?.contains(b)).toBe(true);
+ expect(caption?.innerHTML.trim()).toBe('This is a caption.');
+ });
+
+ it('Should allow various inline elements in ', () => {
+ const testCases = [
+ {
+ input: '',
+ selector: 'strong'
+ },
+ {
+ input: '',
+ selector: 'em'
+ },
+ {
+ input: '',
+ selector: 'span'
+ },
+ {
+ input: '',
+ selector: 'a'
+ }
+ ];
+
+ for (const { input, selector } of testCases) {
+ const doc = new window.DOMParser().parseFromString(input, 'text/html');
+ const caption = doc.querySelector('caption');
+ const element = doc.querySelector(selector);
+ expect(caption?.contains(element)).toBe(true);
+ }
+ });
+
+ it('Should allow block-level elements in (flow content)', () => {
+ const doc = new window.DOMParser().parseFromString(
+ `
+
+ Paragraph in caption
+ Div in caption
+
+
+
`,
+ 'text/html'
+ );
+ const caption = doc.querySelector('caption');
+ const p = doc.querySelector('p');
+ const div = doc.querySelector('div');
+
+ // Block elements should be allowed inside caption
+ expect(caption?.contains(p)).toBe(true);
+ expect(caption?.contains(div)).toBe(true);
+ });
+
+ it('Should NOT allow as direct child of ', () => {
+ const doc = new window.DOMParser().parseFromString(
+ ``,
+ 'text/html'
+ );
+ const caption = doc.querySelector('caption');
+ const nestedTable = doc.querySelectorAll('table')[1];
+
+ // The nested table should NOT be inside the caption
+ expect(caption?.contains(nestedTable)).toBe(false);
+ });
+
+ it('Should preserve caption content when serializing', () => {
+ const html =
+ '';
+ const doc = new window.DOMParser().parseFromString(html, 'text/html');
+ const table = doc.querySelector('table');
+ const serialized = table?.outerHTML;
+
+ expect(serialized).toContain('This is a test.');
+ });
+
+ it('Should remove tag when parent is not ', () => {
+ const doc = new window.DOMParser().parseFromString(
+ 'Wrong parent',
+ 'text/html'
+ );
+ // The caption tag should be removed, leaving only the text content
+ expect(doc.body.innerHTML).toBe('Wrong parent
');
+ });
+
+ it('Should remove tag when used standalone', () => {
+ const doc = new window.DOMParser().parseFromString(
+ 'Standalone caption',
+ 'text/html'
+ );
+ // The caption tag should be removed, leaving only the text content
+ expect(doc.body.innerHTML).toBe('Standalone caption');
+ });
+
+ it('Should preserve tag only when parent is ', () => {
+ const doc = new window.DOMParser().parseFromString(
+ '',
+ 'text/html'
+ );
+ const caption = doc.querySelector('caption');
+ const table = doc.querySelector('table');
+
+ // The caption should exist and be inside the table
+ expect(caption).not.toBeNull();
+ expect(table?.contains(caption)).toBe(true);
+ expect(doc.body.innerHTML).toContain('Correct parent');
+ });
+ });
});