@player-ui/player
Version:
499 lines (399 loc) • 13 kB
text/typescript
import { describe, it, beforeEach, vitest, test, expect } from "vitest";
import { BindingParser } from "../binding";
import { LocalModel } from "../data";
import { ValidationMiddleware } from "../validator";
import type { Logger } from "..";
import { DataController } from "..";
import type { ReadOnlyDataController } from "../controllers/data/utils";
test("works with basic data", () => {
const model = {
foo: {
bar: "baz",
},
bar: "foo",
baz: [{ foo: "1" }],
};
const localData = new LocalModel(model);
const parser = new BindingParser({ get: localData.get, set: localData.set });
const controller = new DataController({}, { pathResolver: parser });
controller.hooks.resolveDataStages.tap("basic", () => [localData]);
expect(controller.get("foo")).toStrictEqual(model.foo);
expect(controller.get("foo.bar")).toStrictEqual(model.foo.bar);
expect(controller.get("baz.0.foo")).toStrictEqual(model.baz[0].foo);
controller.set({ "foo.baz": "bar" });
expect(controller.get("foo.baz")).toStrictEqual("bar");
});
test("works with path segments starting with numbers", () => {
const model = {
foo: {
"5f4704fd-adab-49df-bcbc-5aedb04194f9": {
bar: "baz",
},
},
};
const localData = new LocalModel(model);
const parser = new BindingParser({ get: localData.get, set: localData.set });
const controller = new DataController({}, { pathResolver: parser });
controller.hooks.resolveDataStages.tap("basic", () => [localData]);
expect(controller.get("foo.5f4704fd-adab-49df-bcbc-5aedb04194f9.bar")).toBe(
"baz",
);
});
test("works with nested model refs", () => {
const model = {
foo: {
bar: "baz",
},
other: "bar",
};
const localData = new LocalModel(model);
const parser = new BindingParser({ get: localData.get, set: localData.set });
const controller = new DataController({}, { pathResolver: parser });
controller.hooks.resolveDataStages.tap("basic", () => [localData]);
expect(controller.get("foo.{{other}}")).toStrictEqual(model.foo.bar);
});
test("works with variable indexes", () => {
const model = {
foo: [{ bar: "AAA" }, { bar: "BBB" }],
baz: 1,
};
const localData = new LocalModel(model);
const parser = new BindingParser({ get: localData.get, set: localData.set });
const controller = new DataController({}, { pathResolver: parser });
controller.hooks.resolveDataStages.tap("basic", () => [localData]);
expect(controller.get("foo[{{baz}}].bar")).toStrictEqual("BBB");
controller.set([["baz", 0]]);
expect(controller.get("foo[{{baz}}].bar")).toStrictEqual("AAA");
});
test("works with updates", () => {
const model = {
foo: [{ UUID: "not baz" }],
};
const localData = new LocalModel(model);
const parser = new BindingParser({ get: localData.get, set: localData.set });
const controller = new DataController({}, { pathResolver: parser });
controller.hooks.resolveDataStages.tap("basic", () => [localData]);
controller.set({
"foo[UUID='baz'].blah": "blah",
});
expect(controller.get("foo.0.UUID")).toStrictEqual(model.foo[0].UUID);
expect(controller.get("foo.1.UUID")).toStrictEqual("baz");
expect(controller.get("foo.1.blah")).toStrictEqual("blah");
});
describe("delete", () => {
test("requires binding", () => {
const model = {
foo: {
bar: "Some Data",
},
};
const localData = new LocalModel(model);
const parser = new BindingParser({
get: localData.get,
set: localData.set,
});
const controller = new DataController({}, { pathResolver: parser });
controller.hooks.resolveDataStages.tap("Local", () => [localData]);
expect(() => controller.delete(undefined as any)).toThrow(
"Invalid arguments: delete expects a data path (string)",
);
});
test("does nothing if not in dataModel", () => {
const model = {
foo: {
bar: "Some Data",
},
};
const localData = new LocalModel(model);
const parser = new BindingParser({
get: localData.get,
set: localData.set,
});
const controller = new DataController({}, { pathResolver: parser });
controller.hooks.resolveDataStages.tap("Local", () => [localData]);
controller.delete("foo.baz");
expect(controller.get("")).toStrictEqual({
foo: { bar: "Some Data" },
});
});
test("deletes property", () => {
const model = {
foo: {
bar: "Some Data",
},
};
const localData = new LocalModel(model);
const parser = new BindingParser({
get: localData.get,
set: localData.set,
});
const controller = new DataController({}, { pathResolver: parser });
controller.hooks.resolveDataStages.tap("Local", () => [localData]);
controller.delete("foo.bar");
expect(controller.get("")).toStrictEqual({ foo: {} });
});
test("deletes array item", () => {
const model = {
foo: ["Some Data"],
};
const localData = new LocalModel(model);
const parser = new BindingParser({
get: localData.get,
set: localData.set,
});
const controller = new DataController({}, { pathResolver: parser });
controller.hooks.resolveDataStages.tap("Local", () => [localData]);
controller.delete("foo.0");
expect(controller.get("")).toStrictEqual({ foo: [] });
});
test("doesn't delete data that is out of range", () => {
const model = {
foo: ["Some Data"],
};
const localData = new LocalModel(model);
const parser = new BindingParser({
get: localData.get,
set: localData.set,
});
const controller = new DataController({}, { pathResolver: parser });
controller.hooks.resolveDataStages.tap("Local", () => [localData]);
controller.delete("foo.1");
expect(controller.get("")).toStrictEqual({ foo: ["Some Data"] });
});
test("deletes root property", () => {
const model = {
foo: {
bar: "Some Data",
},
baz: "Other data",
};
const localData = new LocalModel(model);
const parser = new BindingParser({
get: localData.get,
set: localData.set,
});
const controller = new DataController({}, { pathResolver: parser });
controller.hooks.resolveDataStages.tap("Local", () => [localData]);
controller.delete("foo");
expect(controller.get("")).toStrictEqual({ baz: "Other data" });
});
test("deletes nothing for a blank binding", () => {
const model = {
foo: {
bar: "Some Data",
},
};
const localData = new LocalModel(model);
const parser = new BindingParser({
get: localData.get,
set: localData.set,
});
const controller = new DataController({}, { pathResolver: parser });
controller.hooks.resolveDataStages.tap("Local", () => [localData]);
controller.delete("");
expect(controller.get("")).toStrictEqual({
foo: {
bar: "Some Data",
},
});
});
});
describe("formatting", () => {
it("formats data", () => {
const localData = new LocalModel({});
const parser = new BindingParser({
get: localData.get,
set: localData.set,
});
const controller = new DataController({}, { pathResolver: parser });
controller.hooks.format.tap("test", (val) => {
if (val === "should-format") {
return "formatted!";
}
return val;
});
controller.hooks.deformat.tap("test", (val) => {
if (val === "should-deformat") {
return "deformatted!";
}
return val;
});
controller.set([["foo.bar", "should-format"]], { formatted: true });
expect(controller.get("foo.bar")).toBe("should-format");
expect(controller.get("foo.bar", { formatted: true })).toBe("formatted!");
controller.set([["foo.baz", "should-deformat"]]);
expect(controller.get("foo.baz")).toBe("should-deformat");
controller.set([["foo.baz", "should-deformat"]], { formatted: true });
expect(controller.get("foo.baz")).toBe("deformatted!");
expect(controller.get("foo.baz", { formatted: false })).toBe(
"deformatted!",
);
});
});
describe("serialization", () => {
it("can hook into serializing", () => {
const localData = new LocalModel();
const parser = new BindingParser({
get: localData.get,
set: localData.set,
});
const controller = new DataController(
{ testData: 0 },
{ pathResolver: parser },
);
controller.hooks.serialize.tap("test", (dataModel) => {
return {
...dataModel,
keys: Object.keys(dataModel),
};
});
expect(controller.serialize()).toStrictEqual({
testData: 0,
keys: ["testData"],
});
});
it("doesnt include invalid data", () => {
const localData = new LocalModel();
const parser = new BindingParser({
get: localData.get,
set: localData.set,
});
const dc = new DataController(
{ valid: true, invalid: false },
{
pathResolver: parser,
middleware: [
new ValidationMiddleware((binding, model) => {
if (
binding.asString() === "invalid" &&
model.get(binding) !== false
) {
return {
severity: "error",
message: "Nope",
};
}
}),
],
},
);
expect(dc?.serialize()).toStrictEqual({
valid: true,
invalid: false,
});
dc.set([["invalid", true]]);
expect(dc?.serialize()).toStrictEqual({
valid: true,
invalid: false,
});
expect(dc.get("", { includeInvalid: true })).toStrictEqual({
valid: true,
invalid: true,
});
});
});
describe("default value", () => {
it("gets/sets with default", () => {
const model = {
foo: "foo",
};
const localData = new LocalModel(model);
const parser = new BindingParser({
get: localData.get,
set: localData.set,
});
const controller = new DataController({}, { pathResolver: parser });
controller.hooks.resolveDefaultValue.tap("test", (b) => {
if (b.asString() === "foo") {
return "FOO";
}
if (b.asString() === "bar") {
return "BAR";
}
});
controller.hooks.resolveDataStages.tap("basic", () => [localData]);
expect(controller.get("bar")).toBe("BAR");
controller.set([["foo", undefined]]);
expect(controller.get("foo")).toBe("FOO");
// The data isn't actually set though
expect(controller.get("")).toStrictEqual({ foo: undefined });
});
});
it("should not send update for deeply equal data", () => {
const model = {
user: {
name: "frodo",
age: 3,
},
};
const localData = new LocalModel(model);
const parser = new BindingParser({
get: localData.get,
set: localData.set,
});
const controller = new DataController({}, { pathResolver: parser });
controller.hooks.resolveDataStages.tap("basic", () => [localData]);
const onUpdateCallback = vitest.fn();
controller.hooks.onUpdate.tap("test", onUpdateCallback);
controller.set([["user", { name: "frodo", age: 3 }]]);
expect(onUpdateCallback).not.toBeCalled();
});
it("should handle deleting non-existent value + parent value", () => {
const model = {
user: {
name: "frodo",
age: 3,
},
};
const localData = new LocalModel(model);
const parser = new BindingParser({
get: localData.get,
set: localData.set,
});
const controller = new DataController({}, { pathResolver: parser });
controller.hooks.resolveDataStages.tap("basic", () => [localData]);
controller.delete("user.email");
expect(controller.get("user")).toStrictEqual({
name: "frodo",
age: 3,
});
controller.delete("foo.bar");
});
describe("Read Only Data Controller", () => {
let readOnlyController: ReadOnlyDataController;
let logger: Logger;
beforeEach(() => {
const localData = new LocalModel();
const parser = new BindingParser({
get: localData.get,
set: localData.set,
});
logger = {
trace: vitest.fn(),
debug: vitest.fn(),
info: vitest.fn(),
warn: vitest.fn(),
error: vitest.fn(),
};
const controller = new DataController(
{ some: { data: true } },
{ pathResolver: parser, logger },
);
readOnlyController = controller.makeReadOnly();
});
it("Reads data", () => {
expect(readOnlyController.get("some.data")).toStrictEqual(true);
});
it("Logs error on set", () => {
expect(readOnlyController.set([["some.data", false]])).toStrictEqual([]);
expect(logger.error).toBeCalledWith(
"Error: Tried to set in a read only instance of the DataController",
);
});
it("Logs error on delete", () => {
readOnlyController.delete("some.data");
expect(logger.error).toBeCalledWith(
"Error: Tried to delete in a read only instance of the DataController",
);
});
});