diff --git a/web/packages/core/src/internal/register-element.ts b/web/packages/core/src/internal/register-element.ts index 2321b2a86015..b73aac13f5f4 100644 --- a/web/packages/core/src/internal/register-element.ts +++ b/web/packages/core/src/internal/register-element.ts @@ -86,6 +86,126 @@ export function registerElement( continue; } else { window.customElements.define(externalName, elementClass); + + if (elementName === "ruffle-embed") { + const orig = Object.getOwnPropertyDescriptor( + Document.prototype, + "embeds", + ); + if (orig?.get) { + const CACHE_SYM: unique symbol = Symbol( + "ruffle_embeds_cache", + ); + interface CachedCollection extends HTMLCollection { + [CACHE_SYM]?: true; + } + Object.defineProperty(Document.prototype, "embeds", { + get(this: Document): CachedCollection { + const existing = ( + this as unknown as Record< + symbol, + HTMLCollection + > + )[CACHE_SYM]; + if (existing) { + return existing; + } + + const nodes = (): NodeListOf => + this.querySelectorAll( + "embed, ruffle-embed", + ); + + const base = Object.create( + HTMLCollection.prototype, + ) as HTMLCollection; + + Object.defineProperty(base, "length", { + enumerable: true, + configurable: true, + get() { + return nodes().length; + }, + }); + + base.item = function ( + index: number, + ): Element | null { + return nodes()[index] ?? null; + }; + + base.namedItem = function ( + name: string, + ): Element | null { + const list = nodes(); + for (const el of list) { + const htmlEl = el as HTMLElement; + if ( + name && + (htmlEl.getAttribute("name") === + name || + htmlEl.id === name) + ) { + return htmlEl; + } + } + return null; + }; + + (base as Iterable)[Symbol.iterator] = + function* (): Iterator { + for (const el of nodes()) { + yield el; + } + }; + + const proxy = new Proxy(base, { + get(target, prop, receiver) { + if (typeof prop === "string") { + const index = Number(prop); + if ( + !Number.isNaN(index) && + index >= 0 + ) { + return nodes()[index]; + } + } + return Reflect.get( + target, + prop, + receiver, + ); + }, + has(target, prop) { + if (typeof prop === "string") { + const index = Number(prop); + if ( + !Number.isNaN(index) && + index >= 0 + ) { + return index < nodes().length; + } + } + return Reflect.has(target, prop); + }, + }) as CachedCollection; + + proxy[CACHE_SYM] = true; + + ( + this as unknown as Record< + symbol, + CachedCollection + > + )[CACHE_SYM] = proxy; + + return proxy; + }, + configurable: true, + enumerable: true, + }); + } + } } privateRegistry[elementName] = { diff --git a/web/packages/selfhosted/test/polyfill/document_embeds/index.html b/web/packages/selfhosted/test/polyfill/document_embeds/index.html new file mode 100644 index 000000000000..d2834d4e84c8 --- /dev/null +++ b/web/packages/selfhosted/test/polyfill/document_embeds/index.html @@ -0,0 +1,16 @@ + + + + + DOCUMENT EMBEDS + + + +
+ + + +
+
    + + diff --git a/web/packages/selfhosted/test/polyfill/document_embeds/test.ts b/web/packages/selfhosted/test/polyfill/document_embeds/test.ts new file mode 100644 index 000000000000..8f7f40af9c5d --- /dev/null +++ b/web/packages/selfhosted/test/polyfill/document_embeds/test.ts @@ -0,0 +1,50 @@ +import { injectRuffleAndWait, openTest, playAndMonitor } from "../../utils.js"; +import { expect } from "chai"; + +describe("Document embeds", () => { + it("loads the test", async () => { + await openTest(browser, `polyfill/document_embeds`); + }); + + it("Accesses the right number of elements with ruffle", async () => { + async function countDocumentEmbeds() { + return await browser.execute(() => { + return document.embeds?.length ?? 0; + }); + } + + async function documentEmbedsSelfIdentity() { + return await browser.execute(() => { + return document.embeds === document.embeds; + }); + } + + async function removeEl(selector: string) { + const el = await $(selector); + await browser.execute((element) => { + element.remove(); + }, el); + } + + await injectRuffleAndWait(browser); + await playAndMonitor( + browser, + await browser.$("#test-container").$("ruffle-embed#emb1"), + ); + await playAndMonitor( + browser, + await browser.$("#test-container").$("ruffle-embed#emb2"), + ); + await playAndMonitor( + browser, + await browser.$("#test-container").$("ruffle-embed#emb3"), + ); + const documentEmbedsIdentity = await documentEmbedsSelfIdentity(); + expect(documentEmbedsIdentity).to.equal(true); + const embeds1 = await countDocumentEmbeds(); + expect(embeds1).to.equal(3); + await removeEl("#emb1"); + const embeds2 = await countDocumentEmbeds(); + expect(embeds2).to.equal(2); + }); +});