diff --git a/src/elements/stream_source_element.js b/src/elements/stream_source_element.js index 1be9a3afc..6fe4bdb79 100644 --- a/src/elements/stream_source_element.js +++ b/src/elements/stream_source_element.js @@ -1,15 +1,19 @@ import { connectStreamSource, disconnectStreamSource } from "../core/index" export class StreamSourceElement extends HTMLElement { + static observedAttributes = ["src"] + streamSource = null connectedCallback() { this.streamSource = this.src.match(/^ws{1,2}:/) ? new WebSocket(this.src) : new EventSource(this.src) connectStreamSource(this.streamSource) + this.setAttribute("connected", "") } disconnectedCallback() { + this.removeAttribute("connected") if (this.streamSource) { this.streamSource.close() @@ -17,6 +21,13 @@ export class StreamSourceElement extends HTMLElement { } } + attributeChangedCallback() { + if (this.streamSource) { + this.disconnectedCallback() + this.connectedCallback() + } + } + get src() { return this.getAttribute("src") || "" } diff --git a/src/tests/functional/stream_tests.js b/src/tests/functional/stream_tests.js index 5c77016d8..2bb1a5b02 100644 --- a/src/tests/functional/stream_tests.js +++ b/src/tests/functional/stream_tests.js @@ -2,6 +2,7 @@ import { expect, test } from "@playwright/test" import { assert } from "chai" import { hasSelector, + nextAttributeMutationNamed, nextBeat, nextEventNamed, nextEventOnTarget, @@ -113,35 +114,39 @@ test("receiving a stream message over SSE", async ({ page }) => { `` ) }) - await nextBeat() - assert.equal(await getReadyState(page, "stream-source"), await page.evaluate(() => EventSource.OPEN)) - const messages = await page.locator("#messages .message") + const messages = page.locator("#messages .message") + const streamSource = page.locator("#stream-source") + const submit = page.locator("#async button") - assert.deepEqual(await messages.allTextContents(), ["First"]) + await expectReadyState(streamSource, "OPEN") + await expect(messages).toHaveText(["First"]) - await page.click("#async button") + await submit.click() - await waitUntilText(page, "Hello world!") - assert.deepEqual(await messages.allTextContents(), ["First", "Hello world!"]) + await expect(messages).toHaveText(["First", "Hello world!"]) - const readyState = await page.evaluate((id) => { - const element = document.getElementById(id) + await expectReadyState(streamSource, "CLOSED", { removeBeforeCheck: true }) - if (element && element.streamSource) { - element.remove() + await submit.click() - return element.streamSource.readyState - } else { - return -1 - } - }, "stream-source") - assert.equal(readyState, await page.evaluate(() => EventSource.CLOSED)) + await expect(messages).toHaveText(["First", "Hello world!"]) +}) - await page.click("#async button") - await nextBeat() +test("changes to the attributes triggers a reconnection", async ({ page }) => { + await page.evaluate(() => { + document.body.insertAdjacentHTML( + "afterbegin", + `` + ) + }) - assert.deepEqual(await messages.allTextContents(), ["First", "Hello world!"]) + const streamSource = page.locator("#stream-source") + await expectReadyState(streamSource, "OPEN") + + await streamSource.evaluate((element) => element.setAttribute("src", "/__turbo/changed")) + + await expect(await nextAttributeMutationNamed(page, "stream-source", "connected")).toEqual("") }) test("receiving an update stream message preserves focus if the activeElement has an [id]", async ({ page }) => { @@ -226,14 +231,14 @@ test("preventing a turbo:before-morph-element prevents the morph", async ({ page await expect(page.locator("#message_1")).toHaveText("Morph me") }) -async function getReadyState(page, id) { - return page.evaluate((id) => { - const element = document.getElementById(id) +async function expectReadyState(streamSource, name, { removeBeforeCheck } = { removeBeforeCheck: false }) { + const expected = await streamSource.evaluate((_, name) => EventSource[name], name) + const [actual, connected] = await streamSource.evaluate((element, removeBeforeCheck) => { + if (removeBeforeCheck) element.remove() - if (element?.streamSource) { - return element.streamSource.readyState - } else { - return -1 - } - }, id) + return [element.streamSource.readyState, element.getAttribute("connected")] + }, removeBeforeCheck) + + await expect(connected).toEqual(name === "OPEN" ? "" : null) + await expect(actual).toEqual(expected) }