Skip to content
Open
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
76 changes: 38 additions & 38 deletions packages/anywidget/__tests__/widget.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,34 @@ import * as baseManager from "@jupyter-widgets/base-manager";
import { afterEach, expect, it } from "vitest";
import { page, userEvent } from "vitest/browser";

import create_anywidget from "../src/widget.js";
import createAnywidget from "../src/widget.js";

let anywidget = create_anywidget(widgets);
let num_comms = 0;
let anywidget = createAnywidget(widgets);
let numComms = 0;

class MockComm implements widgets.IClassicComm {
comm_id = `mock-${num_comms++}`;
comm_id = `mock-${numComms++}`;
target_name = "dummy";
#on_open: ((x?: unknown) => void) | null = null;
#on_close: ((x?: unknown) => void) | null = null;
#onOpen: ((x?: unknown) => void) | null = null;
#onClose: ((x?: unknown) => void) | null = null;

on_open(fn: () => void): void {
this.#on_open = fn;
this.#onOpen = fn;
}

on_close(fn: (x: unknown) => void): void {
this.#on_close = fn;
this.#onClose = fn;
}

on_msg(): void {}

open(): string {
this.#on_open?.();
this.#onOpen?.();
return "";
}

close(): string {
this.#on_close?.();
this.#onClose?.();
return "";
}

Expand Down Expand Up @@ -84,32 +84,32 @@ export default { render };
`;

async function createWidget(options: {
widget_manager: Manager;
widgetManager: Manager;
esm: string;
css?: string;
state?: Record<string, unknown>;
}): Promise<InstanceType<typeof anywidget.AnyModel>> {
let { widget_manager, esm, css, state = {} } = options;
let model_options = {
let { widgetManager, esm, css, state = {} } = options;
let modelOptions = {
model_name: "AnyModel",
model_module: "anywidget",
model_module_version: "0.1.0",
view_name: "AnyView",
view_module: "anywidget",
view_module_version: "0.1.0",
} as const;
let model = await widget_manager.new_widget(
let model = await widgetManager.new_widget(
{
...model_options,
...modelOptions,
model_id: widgets.uuid(),
},
{
_model_name: model_options.model_name,
_model_module: model_options.model_module,
_model_module_version: model_options.model_module_version,
_view_name: model_options.view_name,
_view_module: model_options.view_module,
_view_module_version: model_options.view_module_version,
_model_name: modelOptions.model_name,
_model_module: modelOptions.model_module,
_model_module_version: modelOptions.model_module_version,
_view_name: modelOptions.view_name,
_view_module: modelOptions.view_module,
_view_module_version: modelOptions.view_module_version,
_esm: esm,
_anywidget_id: "anywidget-test",
...(css ? { _css: css } : {}),
Expand All @@ -125,9 +125,9 @@ afterEach(() => {
});

it("creates an anywidget", async () => {
let widget_manager = new Manager();
let widgetManager = new Manager();
let model = await createWidget({
widget_manager,
widgetManager,
esm: _esm,
});
expect(model).toBeInstanceOf(anywidget.AnyModel);
Expand All @@ -136,24 +136,24 @@ it("creates an anywidget", async () => {
});

it("renders view", async () => {
let widget_manager = new Manager();
let widgetManager = new Manager();
let esm = `\
function render({ model, el }) {
el.innerText = "Hello, world";
}
export default { render };
`;
let model = await createWidget({
widget_manager,
widgetManager,
esm: esm,
});
let view = await widget_manager.create_view(model);
let view = await widgetManager.create_view(model);
document.body.appendChild(view.el);
await expect.element(page.getByText("Hello, world")).toBeInTheDocument();
});

it("renders view with styles", async () => {
let widget_manager = new Manager();
let widgetManager = new Manager();
let esm = `\
function render({ model, el }) {
el.classList.add("basic-test");
Expand All @@ -167,11 +167,11 @@ export default { render };
}
`;
let model = await createWidget({
widget_manager,
widgetManager,
esm: esm,
css: css,
});
let view = await widget_manager.create_view(model);
let view = await widgetManager.create_view(model);
document.body.appendChild(view.el);
await expect.element(page.getByText("Hello, world")).toBeInTheDocument();
expect(
Expand All @@ -180,7 +180,7 @@ export default { render };
});

it("updates view on model changes", async () => {
let widget_manager = new Manager();
let widgetManager = new Manager();
let esm = `\
function render({ model, el }) {
let button = document.createElement("button");
Expand All @@ -197,11 +197,11 @@ function render({ model, el }) {
export default { render };
`;
let model = await createWidget({
widget_manager,
widgetManager,
esm: esm,
state: { value: 0 },
});
let view = await widget_manager.create_view(model);
let view = await widgetManager.create_view(model);
document.body.appendChild(view.el);
await expect.element(page.getByText("count is 0")).toBeInTheDocument();
model.set("value", 10);
Expand All @@ -212,7 +212,7 @@ export default { render };
});

it("performs HMR update for _esm", async () => {
let widget_manager = new Manager();
let widgetManager = new Manager();
let esm = `\
function render({ model, el }) {
el.innerText = "hello. " + model.get("value");
Expand All @@ -223,11 +223,11 @@ function render({ model, el }) {
export default { render };
`;
let model = await createWidget({
widget_manager,
widgetManager,
esm: esm,
state: { value: 0 },
});
let view = await widget_manager.create_view(model);
let view = await widgetManager.create_view(model);
document.body.appendChild(view.el);
await expect.element(page.getByText("hello. 0")).toBeInTheDocument();
model.set("value", 10);
Expand All @@ -245,7 +245,7 @@ export default { render };
});

it("performs HMR update for _css", async () => {
let widget_manager = new Manager();
let widgetManager = new Manager();
let esm = `\
function render({ model, el }) {
el.classList.add("anywidget-hmr-test")
Expand All @@ -254,11 +254,11 @@ function render({ model, el }) {
export default { render };
`;
let model = await createWidget({
widget_manager,
widgetManager,
esm: esm,
css: `.anywidget-hmr-test { background-color: lightgreen }`,
});
let view = await widget_manager.create_view(model);
let view = await widgetManager.create_view(model);
document.body.appendChild(view.el);
await expect.element(page.getByText("hi")).toBeInTheDocument();
expect(
Expand Down
46 changes: 23 additions & 23 deletions packages/anywidget/src/binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,59 +2,59 @@ import type { Experimental, Host } from "@anywidget/types";
import type { DOMWidgetModel, DOMWidgetView } from "@jupyter-widgets/base";

import type { AnyWidget } from "./load.ts";
import { INITIALIZE_MARKER, model_proxy } from "./model-proxy.ts";
import { type Awaitable, promise_with_resolvers, safe_cleanup } from "./util.ts";
import { INITIALIZE_MARKER, modelProxy } from "./model-proxy.ts";
import { type Awaitable, promiseWithResolvers, safeCleanup } from "./util.ts";

function is_safe_cleanup_function(x: unknown): x is () => Awaitable<void> {
function isSafeCleanupFunction(x: unknown): x is () => Awaitable<void> {
return typeof x === "function";
}

export class WidgetBinding {
#controller: AbortController | undefined;
#widget_def: AnyWidget | undefined;
#widgetDef: AnyWidget | undefined;
#exports: unknown;
#model: DOMWidgetModel;
ready: Promise<unknown>;
#resolvers: PromiseWithResolvers<unknown>;

constructor(model: DOMWidgetModel) {
this.#model = model;
this.#resolvers = promise_with_resolvers();
this.#resolvers = promiseWithResolvers();
this.ready = this.#resolvers.promise;
}

async bind(
widget_def: AnyWidget,
widgetDef: AnyWidget,
{ experimental }: { experimental: Experimental },
): Promise<void> {
if (this.#widget_def === widget_def) return;
if (this.#widgetDef === widgetDef) return;

if (this.#widget_def && this.#widget_def !== widget_def) {
if (this.#widgetDef && this.#widgetDef !== widgetDef) {
this.#controller?.abort();
this.#resolvers = promise_with_resolvers();
this.#resolvers = promiseWithResolvers();
this.ready = this.#resolvers.promise;
}

this.#widget_def = widget_def;
this.#widgetDef = widgetDef;
this.#controller = new AbortController();
let signal = this.#controller.signal;
let model = this.#model;

model.off(null, null, INITIALIZE_MARKER);

let result = await widget_def.initialize?.({
model: model_proxy(model, INITIALIZE_MARKER),
let result = await widgetDef.initialize?.({
model: modelProxy(model, INITIALIZE_MARKER),
signal,
experimental,
});

if (signal.aborted) {
await safe_cleanup(is_safe_cleanup_function(result) ? result : undefined, "esm update");
await safeCleanup(isSafeCleanupFunction(result) ? result : undefined, "esm update");
return;
}

if (is_safe_cleanup_function(result)) {
signal.addEventListener("abort", () => safe_cleanup(result, "esm update"));
if (isSafeCleanupFunction(result)) {
signal.addEventListener("abort", () => safeCleanup(result, "esm update"));
this.#exports = undefined;
} else if (typeof result === "object" && result !== null) {
this.#exports = result;
Expand All @@ -65,26 +65,26 @@ export class WidgetBinding {
this.#resolvers.resolve(this.#exports);
}

async create_view(
async createView(
view: DOMWidgetView,
{ signal, experimental, host }: { signal: AbortSignal; experimental: Experimental; host: Host },
): Promise<(() => void) | undefined> {
await this.ready;
if (!this.#widget_def?.render) return;
if (!this.#widgetDef?.render) return;
let controller = new AbortController();
let combined = AbortSignal.any([signal, controller.signal]);
let cleanup = await this.#widget_def.render({
model: model_proxy(this.#model, view),
let cleanup = await this.#widgetDef.render({
model: modelProxy(this.#model, view),
el: view.el,
signal: combined,
host,
experimental,
});
if (combined.aborted) {
await safe_cleanup(cleanup, "dispose view - already aborted");
await safeCleanup(cleanup, "dispose view - already aborted");
return;
}
combined.addEventListener("abort", () => safe_cleanup(cleanup, "dispose view - aborted"));
combined.addEventListener("abort", () => safeCleanup(cleanup, "dispose view - aborted"));
return () => controller.abort();
}

Expand All @@ -95,14 +95,14 @@ export class WidgetBinding {
destroy(): void {
this.#controller?.abort();
this.#controller = undefined;
this.#widget_def = undefined;
this.#widgetDef = undefined;
}
}

class BindingManager {
#bindings = new Map<DOMWidgetModel, WidgetBinding>();

get_or_create(model: DOMWidgetModel): WidgetBinding {
getOrCreate(model: DOMWidgetModel): WidgetBinding {
let binding = this.#bindings.get(model);
if (!binding) {
binding = new WidgetBinding(model);
Expand Down
Loading