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: '
Text with strong
', + selector: 'strong' + }, + { + input: '
Text with emphasis
', + selector: 'em' + }, + { + input: '
Text with span
', + selector: 'span' + }, + { + input: '
Text with link
', + 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 '); + }); + + it('Should remove
', () => { + const doc = new window.DOMParser().parseFromString( + ` + + +
+
Nested table
+
`, + '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 = + '
This is a test.
'; + 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. tag when parent is not ', () => { + const doc = new window.DOMParser().parseFromString( + '
', + 'text/html' + ); + // The caption tag should be removed, leaving only the text content + expect(doc.body.innerHTML).toBe('
Wrong parent
'); + }); + + it('Should remove ', + 'text/html' + ); + // The caption tag should be removed, leaving only the text content + expect(doc.body.innerHTML).toBe('Standalone caption'); + }); + + it('Should preserve '); + }); + }); });
Wrong parent tag when used standalone', () => { + const doc = new window.DOMParser().parseFromString( + 'Standalone caption tag only when parent is ', () => { + const doc = new window.DOMParser().parseFromString( + '
Correct parent
', + '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