Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions packages/element/src/adapter-binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,18 +116,23 @@ const BROWSER_ENVIRONMENT: AvalAdapterBindingEnvironment = Object.freeze({
upgrade(node: AvalAdapterBindingNode): AvalAdapterBindingElementPort {
defineAvalElement();
const element = node as unknown as AvalAdapterBindingElementPort;
if (
typeof element.getSnapshot !== "function" ||
typeof element.subscribe !== "function"
) {
throw new TypeError(
"Registered aval-player does not implement the required snapshot API"
);
if (!hasSnapshotApi(element)) {
customElements.upgrade(node as unknown as Node);
if (!hasSnapshotApi(element)) {
throw new TypeError(
"Registered aval-player does not implement the required snapshot API"
);
}
}
return element;
}
});

function hasSnapshotApi(element: AvalAdapterBindingElementPort): boolean {
return typeof element.getSnapshot === "function" &&
typeof element.subscribe === "function";
}

export class AvalAdapterBindingImplementation implements AvalAdapterBinding {
public readonly commands: Readonly<AvalAdapterCommands>;
readonly #statusListeners = new Set<StoreListener>();
Expand Down
4 changes: 4 additions & 0 deletions packages/svelte/src/controller.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { defineAvalElement } from "@pixel-point/aval-element";
import {
createAvalAdapterBinding,
createAvalAdapterConfiguration,
Expand All @@ -22,6 +23,9 @@ export function createAval(
if (typeof readOptions !== "function") {
throw new TypeError("createAval requires an option getter");
}
// svelte clones hosts from templates, and webkit never upgrades template
// nodes created before the first customElements access
if (typeof globalThis.customElements !== "undefined") defineAvalElement();

const configuration = createAvalAdapterConfiguration(readOptions());
const binding = createAvalAdapterBinding(configuration);
Expand Down
12 changes: 12 additions & 0 deletions packages/svelte/test/browser/LateDefinition.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<script lang="ts">
import { AvalComponent, createAval } from "../../src/index.js";

const aval = createAval(() => ({
sources: { h264: "/missing-motion.avl" },
autoplay: false,
autoBind: false
}));
</script>

<AvalComponent {aval} data-testid="player" />
<output data-testid="controller-readiness">{$aval.readiness}</output>
11 changes: 11 additions & 0 deletions packages/svelte/test/browser/disconnected-host.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Svelte disconnected host regression</title>
</head>
<body>
<script type="module" src="/disconnected-host.ts"></script>
</body>
</html>
27 changes: 27 additions & 0 deletions packages/svelte/test/browser/disconnected-host.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { getControllerRecord } from "../../src/controller.js";
import { createAval } from "../../src/index.js";

// touch the registry first so webkit gives template nodes the global one;
// define() still skips this host because it is disconnected
customElements.get("aval-player");
const template = document.createElement("template");
template.innerHTML = "<aval-player></aval-player>";
const node = document.importNode(template.content, true).firstElementChild;
if (!(node instanceof HTMLElement)) throw new Error("Expected an imported host");

const aval = createAval(() => ({
sources: { h264: "/missing-motion.avl" },
autoplay: false,
autoBind: false
}));
const result = document.createElement("output");
result.dataset.testid = "disconnected-attach";
try {
getControllerRecord(aval).binding.attach(node);
result.textContent = typeof Reflect.get(node, "getSnapshot") === "function"
? "attached"
: "missing snapshot API";
} catch (error) {
result.textContent = error instanceof Error ? error.message : String(error);
}
document.body.append(result);
12 changes: 12 additions & 0 deletions packages/svelte/test/browser/late-component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Svelte late component definition regression</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/late-component.ts"></script>
</body>
</html>
7 changes: 7 additions & 0 deletions packages/svelte/test/browser/late-component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { mount } from "svelte";

import LateDefinition from "./LateDefinition.svelte";

const target = document.querySelector<HTMLElement>("#app");
if (target === null) throw new Error("Svelte browser test mount is missing");
mount(LateDefinition, { target });
12 changes: 12 additions & 0 deletions packages/svelte/test/browser/late-definition.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Svelte late definition regression</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/late-definition.ts"></script>
</body>
</html>
51 changes: 51 additions & 0 deletions packages/svelte/test/browser/late-definition.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { expect, test, type Locator } from "@playwright/test";

async function snapshotApi(player: Locator) {
return player.evaluate((node) => ({
getSnapshot: typeof Reflect.get(node, "getSnapshot"),
subscribe: typeof Reflect.get(node, "subscribe")
}));
}

test("attaches a host imported from a template before registration", async ({
page
}) => {
const errors: string[] = [];
page.on("pageerror", (error) => errors.push(error.message));
await page.goto("/late-definition.html");

await expect(page.getByTestId("imported-attach")).toHaveText("attached");
expect(await snapshotApi(page.getByTestId("imported-player"))).toEqual({
getSnapshot: "function",
subscribe: "function"
});
expect(errors).toEqual([]);
});

test("attaches a disconnected host defined after it was imported", async ({
page
}) => {
const errors: string[] = [];
page.on("pageerror", (error) => errors.push(error.message));
await page.goto("/disconnected-host.html");

await expect(page.getByTestId("disconnected-attach")).toHaveText("attached");
expect(errors).toEqual([]);
});

test("mounts AvalComponent without the page registering aval-player", async ({
page
}) => {
const errors: string[] = [];
page.on("pageerror", (error) => errors.push(error.message));
await page.goto("/late-component.html");

const player = page.getByTestId("player");
await expect(player).toHaveAttribute("autoplay", "manual");
expect(await snapshotApi(player)).toEqual({
getSnapshot: "function",
subscribe: "function"
});
await expect(page.getByTestId("controller-readiness")).not.toBeEmpty();
expect(errors).toEqual([]);
});
26 changes: 26 additions & 0 deletions packages/svelte/test/browser/late-definition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { getControllerRecord } from "../../src/controller.js";
import { createAval } from "../../src/index.js";

// webkit gives template nodes a null registry until window.customElements has
// been touched, and those nodes never upgrade. nothing here may touch it
// before createAval does.
const aval = createAval(() => ({
sources: { h264: "/missing-motion.avl" },
autoplay: false,
autoBind: false
}));
const template = document.createElement("template");
template.innerHTML = '<aval-player data-testid="imported-player"></aval-player>';
const node = document.importNode(template.content, true).firstElementChild;
if (!(node instanceof HTMLElement)) throw new Error("Expected an imported host");
document.body.append(node);

const result = document.createElement("output");
result.dataset.testid = "imported-attach";
try {
getControllerRecord(aval).binding.attach(node);
result.textContent = "attached";
} catch (error) {
result.textContent = error instanceof Error ? error.message : String(error);
}
document.body.append(result);
22 changes: 19 additions & 3 deletions packages/svelte/test/browser/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,43 @@ import { defineConfig, devices } from "@playwright/test";

const port = process.env.AVAL_SVELTE_BROWSER_PORT ?? "4188";
const baseURL = `http://127.0.0.1:${port}`;
const previewPort = process.env.AVAL_SVELTE_PREVIEW_PORT ?? "4189";
const previewURL = `http://127.0.0.1:${previewPort}`;

export default defineConfig({
testDir: ".",
testMatch: "optional-attribute-removal.spec.ts",
testMatch: "*.spec.ts",
workers: 1,
reporter: [["list"]],
use: {
baseURL,
trace: "retain-on-failure"
},
webServer: {
webServer: [{
command: `vite --config vite.config.ts --host 127.0.0.1 --port ${port} --strictPort`,
url: baseURL,
reuseExistingServer: !process.env.CI,
timeout: 30_000
},
}, {
// the dev client registers its own custom element first, which hides
// webkit's null-registry upgrade failure
command: `vite build --config vite.config.ts --logLevel error && vite preview --config vite.config.ts --host 127.0.0.1 --port ${previewPort} --strictPort`,
url: `${previewURL}/late-definition.html`,
reuseExistingServer: !process.env.CI,
timeout: 60_000
}],
projects: [{
name: "chromium",
use: {
...devices["Desktop Chrome"],
channel: "chromium"
}
}, {
name: "webkit",
testMatch: "late-definition.spec.ts",
use: {
...devices["Desktop Safari"],
baseURL: previewURL
}
}]
});
18 changes: 17 additions & 1 deletion packages/svelte/test/browser/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,24 @@ import { fileURLToPath } from "node:url";
import { svelte } from "@sveltejs/vite-plugin-svelte";
import { defineConfig } from "vite";

const root = fileURLToPath(new URL(".", import.meta.url));

export default defineConfig({
root: fileURLToPath(new URL(".", import.meta.url)),
root,
build: {
outDir: fileURLToPath(
new URL("../../../../.cache/svelte-browser", import.meta.url)
),
emptyOutDir: true,
rollupOptions: {
input: {
index: `${root}/index.html`,
lateDefinition: `${root}/late-definition.html`,
lateComponent: `${root}/late-component.html`,
disconnectedHost: `${root}/disconnected-host.html`
}
}
},
plugins: [svelte({
configFile: fileURLToPath(new URL("../../svelte.config.js", import.meta.url))
})],
Expand Down