diff --git a/packages/@glimmer-workspace/integration-tests/lib/suites.ts b/packages/@glimmer-workspace/integration-tests/lib/suites.ts index a3ecce2e83c..b3d576dad90 100644 --- a/packages/@glimmer-workspace/integration-tests/lib/suites.ts +++ b/packages/@glimmer-workspace/integration-tests/lib/suites.ts @@ -7,8 +7,11 @@ export * from './suites/entry-point'; export * from './suites/has-block'; export * from './suites/has-block-params'; export * from './suites/in-element'; +export * from './suites/in-element-document-fragment'; +export * from './suites/in-element-shadow-root'; export * from './suites/initial-render'; export * from './suites/scope'; +export * from './suites/shadow-dom'; export * from './suites/shadowing'; export * from './suites/ssr'; export * from './suites/with-dynamic-vars'; diff --git a/packages/@glimmer-workspace/integration-tests/lib/suites/in-element-document-fragment.ts b/packages/@glimmer-workspace/integration-tests/lib/suites/in-element-document-fragment.ts new file mode 100644 index 00000000000..99e6a5d892a --- /dev/null +++ b/packages/@glimmer-workspace/integration-tests/lib/suites/in-element-document-fragment.ts @@ -0,0 +1,164 @@ +import { RenderTest } from '../render-test'; +import { test } from '../test-decorator'; + +export class InElementDocumentFragmentSuite extends RenderTest { + static suiteName = '#in-element (DocumentFragment)'; + + @test + 'Renders curlies into a detached DocumentFragment'() { + const fragment = document.createDocumentFragment(); + + this.render('{{#in-element this.fragment}}[{{this.foo}}]{{/in-element}}', { + fragment, + foo: 'Hello Fragment!', + }); + + this.assert.strictEqual( + fragment.textContent, + '[Hello Fragment!]', + 'content rendered in document fragment' + ); + this.assertHTML(''); + this.assertStableRerender(); + + this.rerender({ foo: 'Updated!' }); + this.assert.strictEqual( + fragment.textContent, + '[Updated!]', + 'content updated in document fragment' + ); + this.assertHTML(''); + + this.rerender({ foo: 'Hello Fragment!' }); + this.assert.strictEqual( + fragment.textContent, + '[Hello Fragment!]', + 'content reverted in document fragment' + ); + this.assertHTML(''); + } + + @test + 'Renders curlies into a template.content fragment'() { + const templateEl = document.createElement('template'); + const fragment = templateEl.content; + + this.render('{{#in-element this.fragment}}[{{this.foo}}]{{/in-element}}', { + fragment, + foo: 'Hello Template Content!', + }); + + this.assert.strictEqual( + fragment.textContent, + '[Hello Template Content!]', + 'content rendered in template.content fragment' + ); + this.assertHTML(''); + this.assertStableRerender(); + + this.rerender({ foo: 'Updated!' }); + this.assert.strictEqual( + fragment.textContent, + '[Updated!]', + 'content updated in template.content fragment' + ); + this.assertHTML(''); + + this.rerender({ foo: 'Hello Template Content!' }); + this.assert.strictEqual( + fragment.textContent, + '[Hello Template Content!]', + 'content reverted in template.content fragment' + ); + this.assertHTML(''); + } + + @test + 'Renders elements into a fragment that is later attached to the DOM'() { + const fragment = document.createDocumentFragment(); + const container = document.createElement('div'); + + this.render('{{#in-element this.fragment}}
{{this.message}}
{{/in-element}}', { + fragment, + message: 'in fragment', + }); + + this.assert.strictEqual( + fragment.querySelector('#frag-p')?.textContent, + 'in fragment', + 'content rendered in detached fragment' + ); + this.assertHTML(''); + + // Attach fragment's children to the DOM + container.appendChild(fragment); + this.assert.strictEqual( + container.querySelector('#frag-p')?.textContent, + 'in fragment', + 'content is in the DOM after fragment is appended' + ); + // Fragment itself is now empty (children moved to container) + this.assert.strictEqual(fragment.childNodes.length, 0, 'fragment is empty after append'); + } + + @test + 'Multiple in-element calls to the same DocumentFragment'() { + const fragment = document.createDocumentFragment(); + + this.render( + '{{#in-element this.fragment}}[{{this.foo}}]{{/in-element}}' + + '{{#in-element this.fragment insertBefore=null}}[{{this.bar}}]{{/in-element}}', + { + fragment, + foo: 'first', + bar: 'second', + } + ); + + this.assert.ok(fragment.textContent?.includes('[first]'), 'first block present in fragment'); + this.assert.ok(fragment.textContent?.includes('[second]'), 'second block present in fragment'); + this.assertHTML(''); + this.assertStableRerender(); + + this.rerender({ foo: 'updated-first', bar: 'updated-second' }); + this.assert.ok( + fragment.textContent?.includes('[updated-first]'), + 'first block updated in fragment' + ); + this.assert.ok( + fragment.textContent?.includes('[updated-second]'), + 'second block updated in fragment' + ); + this.assertHTML(''); + } + + @test + 'Multiple in-element calls to the same DocumentFragment with insertBefore=null'() { + const fragment = document.createDocumentFragment(); + + this.render( + '{{#in-element this.fragment insertBefore=null}}{{this.foo}}
{{/in-element}}' + + '{{#in-element this.fragment insertBefore=null}}{{this.bar}}
{{/in-element}}', + { + fragment, + foo: 'first', + bar: 'second', + } + ); + + // Use childNodes to query into the fragment since querySelector doesn't work on detached fragment nodes in all browsers + const nodes = Array.from(fragment.childNodes); + const pA = nodes.find((n) => (n as Element).id === 'a') as HTMLElement | undefined; + const pB = nodes.find((n) => (n as Element).id === 'b') as HTMLElement | undefined; + + this.assert.strictEqual(pA?.textContent, 'first', 'first block appended to fragment'); + this.assert.strictEqual(pB?.textContent, 'second', 'second block appended to fragment'); + this.assertHTML(''); + this.assertStableRerender(); + + this.rerender({ foo: 'updated-first', bar: 'updated-second' }); + this.assert.strictEqual(pA?.textContent, 'updated-first', 'first block updated in fragment'); + this.assert.strictEqual(pB?.textContent, 'updated-second', 'second block updated in fragment'); + this.assertHTML(''); + } +} diff --git a/packages/@glimmer-workspace/integration-tests/lib/suites/in-element-shadow-root.ts b/packages/@glimmer-workspace/integration-tests/lib/suites/in-element-shadow-root.ts new file mode 100644 index 00000000000..0d8678ac12a --- /dev/null +++ b/packages/@glimmer-workspace/integration-tests/lib/suites/in-element-shadow-root.ts @@ -0,0 +1,256 @@ +import { GlimmerishComponent } from '../components/emberish-glimmer'; +import { RenderTest } from '../render-test'; +import { test } from '../test-decorator'; + +export class InElementShadowRootSuite extends RenderTest { + static suiteName = '#in-element (ShadowRoot)'; + + @test + 'Renders curlies into a ShadowRoot'() { + const hostElement = document.createElement('div'); + const shadowRoot = hostElement.attachShadow({ mode: 'open' }); + + this.render('{{#in-element this.shadowRoot}}[{{this.foo}}]{{/in-element}}', { + shadowRoot, + foo: 'Hello Shadow!', + }); + + this.assert.strictEqual( + shadowRoot.textContent, + '[Hello Shadow!]', + 'content rendered in shadow root' + ); + this.assertHTML(''); + this.assertStableRerender(); + + this.rerender({ foo: 'Updated!' }); + this.assert.strictEqual(shadowRoot.textContent, '[Updated!]', 'content updated in shadow root'); + this.assertHTML(''); + + this.rerender({ foo: 'Hello Shadow!' }); + this.assert.strictEqual( + shadowRoot.textContent, + '[Hello Shadow!]', + 'content reverted in shadow root' + ); + this.assertHTML(''); + } + + @test + 'Class-based component with tracked property renders into shadow root without full DOM replacement on update'() { + const hostElement = document.createElement('div'); + const shadowRoot = hostElement.attachShadow({ mode: 'open' }); + + class Counter extends GlimmerishComponent {} + + this.registerComponent('Glimmer', 'Counter', 'Count: {{@count}}
', Counter as any); + + this.render('{{#in-element this.shadowRoot}}element reused (no full DOM replacement)' + ); + this.assertHTML(''); + + this.rerender({ count: 42 }); + this.assert.strictEqual( + shadowRoot.querySelector('p')?.textContent, + 'Count: 42', + 'count updated to 42' + ); + this.assert.strictEqual( + shadowRoot.querySelector('p'), + p, + 'same
element still reused after second update' + ); + this.assertHTML(''); + } + + @test + 'Sibling components rendered into the same shadow root'() { + const hostElement = document.createElement('div'); + const shadowRoot = hostElement.attachShadow({ mode: 'open' }); + + this.registerComponent('TemplateOnly', 'Header', '
{{@content}}
'); + + this.render( + '{{#in-element this.shadowRoot insertBefore=null}}