diff --git a/packages/youtube-video-element/test/test.js b/packages/youtube-video-element/test/test.js index c0fc1e0..59e9a4b 100644 --- a/packages/youtube-video-element/test/test.js +++ b/packages/youtube-video-element/test/test.js @@ -250,6 +250,67 @@ test('t parameter - case insensitive', async function (t) { t.equal(startParam, '171', 'start parameter is set from uppercase T parameter'); }); +test('destroys the player when disconnected', async function (t) { + const video = await createVideoElement(); + await video.loadComplete; + + t.ok(video.api, 'has a player once loaded'); + t.ok(video.shadowRoot.querySelector('iframe'), 'has an iframe once loaded'); + + video.remove(); + + t.equal(video.api, null, 'the player reference is released on disconnect'); + t.equal(video.isLoaded, false, 'the element is no longer marked loaded'); + t.equal( + video.shadowRoot.querySelector('iframe'), + null, + 'destroy() removed the iframe from the shadow root' + ); +}); + +test('creates a new player when reconnected', async function (t) { + const video = await createVideoElement(); + await video.loadComplete; + + const firstApi = video.api; + video.remove(); + document.body.append(video); + + await video.loadComplete; + + t.ok(video.api, 'has a player again after reconnecting'); + t.ok(video.api !== firstApi, 'a new player was created, not the destroyed one'); + t.ok(video.shadowRoot.querySelector('iframe'), 'the iframe was rebuilt'); +}); + +test('disconnecting mid-load does not build a player afterwards', async function (t) { + const video = await createVideoElement(); + // Disconnect while load() is still awaiting the API script. + video.remove(); + + // Let that in-flight continuation actually resume before asserting; + // immediately after remove() api === null is true by construction. + for (let i = 0; i < 500 && !globalThis.YT?.Player; i++) await delay(10); + await delay(100); + + t.equal(video.api, null, 'the superseded load did not construct a player'); +}); + +test('disconnecting before ready settles the pending loadComplete', async function (t) { + const video = await createVideoElement(); + const pending = video.loadComplete; + + // Disconnect after the player is constructed but before onReady fires. + for (let i = 0; i < 500 && !video.api; i++) await delay(10); + video.remove(); + + const outcome = await Promise.race([ + pending.then(() => 'resolved', (err) => err.name), + delay(3000).then(() => 'pending'), + ]); + t.ok(outcome !== 'pending', `loadComplete settles after disconnect (${outcome})`); +}); + function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } diff --git a/packages/youtube-video-element/youtube-video-element.js b/packages/youtube-video-element/youtube-video-element.js index 75958db..7fadea9 100644 --- a/packages/youtube-video-element/youtube-video-element.js +++ b/packages/youtube-video-element/youtube-video-element.js @@ -147,6 +147,9 @@ class YoutubeVideoElement extends MediaPlayedRangesMixin(globalThis.HTMLElement loadComplete = new PublicPromise(); #loadRequested; #hasLoaded; + #wasDisconnected = false; + #loadId = 0; + #timers = []; #readyState = 0; #seeking = false; #seekComplete; @@ -172,6 +175,12 @@ class YoutubeVideoElement extends MediaPlayedRangesMixin(globalThis.HTMLElement async load() { if (this.#loadRequested) return; + // Identifies this load attempt. `load()` awaits the API script, so the + // element can be disconnected (or asked to load again) while this one is + // still in flight; those bump #loadId and this attempt bails out below + // instead of attaching a player nothing will ever destroy. + const loadId = ++this.#loadId; + if (!this.shadowRoot) { this.attachShadow({ mode: 'open' }); } @@ -193,12 +202,13 @@ class YoutubeVideoElement extends MediaPlayedRangesMixin(globalThis.HTMLElement let oldApi = this.api; this.api = null; + // A new player is always constructed below, so the previous one is never + // reused. Release it rather than just dropping the reference, otherwise it + // stays in the API's registries holding its (now discarded) iframe. + // Removes the