Files
2026-07-13 12:25:07 +08:00

251 lines
11 KiB
JavaScript

// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "@perspective-dev/client";
let server;
let port;
test.describe("WebSocketManager", function () {
test.beforeEach(() => {
server = new perspective.WebSocketServer({ port: 0 });
port = server._server.address().port;
});
test.afterEach(() => {
server.close();
});
test("sends initial data client on subscribe", async () => {
const data = [{ x: 1 }];
const table = await perspective.table(data, { name: "test" });
const client = await perspective.websocket(`ws://localhost:${port}`);
const client_table = await client.open_table("test");
const client_view = await client_table.view();
const client_data = await client_view.to_json();
expect(client_data).toEqual(data);
await client.terminate();
// // TODO Can't await delete here because `terminate()` doesn't
// // guarantee server-side cleanup happens immediately. So this test
// // leaks a small `Table` sometimes, as do several others in this
// // suite.
await new Promise((x) => setTimeout(x, 10));
await table.delete();
});
test("Throws an exception when the server is detached", async () => {
const data = [{ x: 1 }];
const _table = await perspective.table(data, { name: "test2" });
const client = await perspective.websocket(`ws://localhost:${port}`);
const client_table = await client.open_table("test2");
const client_view = await client_table.view();
await server.close();
const client_data = client_view.to_json();
await expect(client_data).rejects.toThrow();
});
test("passes back errors from server", async () => {
expect.assertions(2);
const data = [{ x: 1 }];
const table = await perspective.table(data, { name: "test" });
const client = await perspective.websocket(`ws://localhost:${port}`);
const client_table = await client.open_table("test");
client_table.view({ columns: ["z"] }).catch((error) => {
expect(error.message).toContain(
"Abort(): Invalid column 'z' found in View columns.\n",
);
});
const client_view = await client_table.view();
const client_data = await client_view.to_json();
expect(client_data).toEqual(data);
await client.terminate();
await new Promise((x) => setTimeout(x, 10));
await table.delete();
});
test("sends initial data multiple client on subscribe", async () => {
const data = [{ x: 1 }];
const table = await perspective.table(data, { name: "test" });
const client_1 = await perspective.websocket(`ws://localhost:${port}`);
const client_2 = await perspective.websocket(`ws://localhost:${port}`);
const client_1_table = await client_1.open_table("test");
const client_2_table = await client_2.open_table("test");
const client_1_view = await client_1_table.view();
const client_2_view = await client_2_table.view();
const client_1_data = await client_1_view.to_json();
const client_2_data = await client_2_view.to_json();
await client_1.terminate();
await client_2.terminate();
expect(client_1_data).toEqual(data);
expect(client_2_data).toEqual(data);
await new Promise((x) => setTimeout(x, 10));
await table.delete();
});
test("passes back errors with multiple client on subscribe", async () => {
expect.assertions(3);
const data = [{ x: 1 }];
const table = await perspective.table(data, { name: "test" });
const client_1 = await perspective.websocket(`ws://localhost:${port}`);
const client_2 = await perspective.websocket(`ws://localhost:${port}`);
const client_1_table = await client_1.open_table("test");
const client_2_table = await client_2.open_table("test");
client_1_table.view({ columns: ["z"] }).catch((error) => {
expect(error.message).toContain(
"Abort(): Invalid column 'z' found in View columns.\n",
);
});
const client_1_view = await client_1_table.view();
const client_2_view = await client_2_table.view();
const client_1_data = await client_1_view.to_json();
const client_2_data = await client_2_view.to_json();
await client_1.terminate();
await client_2.terminate();
expect(client_1_data).toEqual(data);
expect(client_2_data).toEqual(data);
await new Promise((x) => setTimeout(x, 10));
await table.delete();
});
test("sends updates to client on subscribe", async () => {
const data = [{ x: 1 }];
const table = await perspective.table(data, { name: "test" });
const client = await perspective.websocket(`ws://localhost:${port}`);
const client_table = await client.open_table("test");
const client_view = await client_table.view();
let done;
let result = new Promise((x) => {
done = x;
});
const on_update = () => {
client_view.to_json().then(async (updated_data) => {
await client_view.delete();
await table.delete();
expect(updated_data).toEqual([{ x: 1 }, { x: 2 }]);
await client.terminate();
setTimeout(done);
});
};
client_view.on_update(on_update);
client_view.to_json().then((client_data) => {
expect(client_data).toEqual(data);
return table.update([{ x: 2 }]);
});
await result;
});
test("Calls `update` and sends arraybuffers using `binary_length`", async () => {
const data = [{ x: 1 }];
const table = await perspective.table(data, { name: "test" });
const view = await table.view();
const arrow = await view.to_arrow();
const client = await perspective.websocket(`ws://localhost:${port}`);
const client_table = await client.open_table("test");
await client_table.update(arrow);
const client_view = await client_table.view();
const client_data = await client_view.to_json();
expect(client_data).toEqual([{ x: 1 }, { x: 1 }]);
await client_view.delete();
await client.terminate();
await view.delete();
await table.delete();
});
test("Calls `update` and sends `Uint8Array` using `binary_length` multiple times", async () => {
const data = [{ x: 1 }];
const table = await perspective.table(data, { name: "test" });
const view = await table.view();
const arrow = await view.to_arrow();
const client = await perspective.websocket(`ws://localhost:${port}`);
const client_table = await client.open_table("test");
await client_table.update(arrow);
await client_table.update(arrow);
await client_table.update(arrow);
const client_view = await client_table.view();
const client_data = await client_view.to_json();
expect(client_data).toEqual([{ x: 1 }, { x: 1 }, { x: 1 }, { x: 1 }]);
await client_view.delete();
await view.delete();
await table.delete();
});
test("Calls `update` and sends `Uint8Array` using `on_update`", async () => {
const data = [{ x: 1 }];
const table = await perspective.table(data, { name: "test" });
const view = await table.view();
const arrow = await view.to_arrow();
let update_port;
let done;
let result = new Promise((x) => {
done = x;
});
const updater = async (updated) => {
expect(updated.port_id).toEqual(update_port);
expect(updated.delta instanceof Uint8Array).toEqual(true);
expect(updated.delta.byteLength).toBeGreaterThan(0);
await view.delete();
await table.delete();
await client.terminate();
done();
};
view.on_update(updater, { mode: "row" });
const client = await perspective.websocket(`ws://localhost:${port}`);
const client_table = await client.open_table("test");
for (let i = 0; i < 5; i++) {
// take up some ports on the remote table
await client_table.make_port();
}
update_port = await client_table.make_port();
await client_table.update(arrow, { port_id: update_port });
await result;
});
// TODO ping loop is not yet implemented
test.skip("disables ping loop on disconnect", async () => {
const data = [{ x: 1 }];
const table = await perspective.table(data, { name: "test" });
const client = await perspective.websocket(`ws://localhost:${port}`);
const _client_table = await client.open_table("test");
expect(await _client_table.size()).toEqual(1);
expect(client._ping_loop).toBeDefined();
await server.close();
expect(client._ping_loop).toBeUndefined();
await table.delete();
});
test("Can get hosted table names", async () => {
const data = [{ x: 1 }];
const host_table_name = "test";
const table = await perspective.table(data, { name: host_table_name });
const client = await perspective.websocket(`ws://localhost:${port}`);
const table_names = await client.get_hosted_table_names();
expect(table_names).toContain(host_table_name);
await client.terminate();
await table.delete();
});
});