Core/tests/model.test.ts
Madeorsk a1691708ae
All checks were successful
/ test (push) Successful in 38s
Apply prettier.
2025-06-23 20:31:34 +02:00

589 lines
14 KiB
TypeScript

import {describe, expect, it} from "vitest";
import {circular, defineModel, s} from "../src/library";
/**
* Test class of an account.
*/
class Account {
static model = s.defineModel({
Class: Account,
identifier: "id",
properties: {
id: s.property.numeric(),
createdAt: s.property.date(),
name: s.property.string(),
email: s.property.string(),
active: s.property.boolean(),
},
});
id: number;
createdAt: Date;
name: string;
email: string;
active: boolean;
}
/**
* Test class of an article.
*/
class Article {
static model = s.defineModel({
Class: Article,
identifier: "id",
properties: {
id: s.property.numeric(),
title: s.property.string(),
authors: s.property.array(s.property.model(() => Account.model)),
text: s.property.string(),
evaluation: s.property.decimal(),
tags: s.property.array(s.property.object({name: s.property.string()})),
comments: s.property.array(s.property.model(() => ArticleComment.model)),
},
});
id: number;
title: string;
authors: Account[];
text: string;
evaluation: number;
tags: {name: string}[];
comments: ArticleComment[];
}
/**
* Test class of a comment on an article.
*/
class ArticleComment {
static model = s.defineModel({
Class: ArticleComment,
identifier: "id",
properties: {
id: s.property.numeric(),
article: s.property.model(circular<Article>(() => Article.model)),
author: s.property.model(() => Account.model),
message: s.property.string(),
},
});
id: number;
article?: Article;
author: Account;
message: string;
}
/**
* Get a test account instance.
*/
function getTestAccount(): Account {
const account = new Account();
account.id = 52;
account.createdAt = new Date();
account.name = "John Doe";
account.email = "john@doe.test";
account.active = true;
return account;
}
function getTestArticle(): Article {
const article = new Article();
article.id = 1;
article.title = "this is a test";
article.text = "this is a long test.";
article.evaluation = 25.23;
article.tags = [{name: "test"}, {name: "foo"}];
article.authors = [getTestAccount()];
article.comments = [];
return article;
}
describe("model", () => {
it("defines a new model, extending an existing one", () => {
class ExtendedAccount extends Account {
static extendedModel = s.extend(Account.model, {
Class: ExtendedAccount,
properties: {
extendedProperty: s.property.string(),
},
});
extendedProperty: string;
}
expect(ExtendedAccount.extendedModel.definition).toEqual({
Class: ExtendedAccount,
identifier: "id",
properties: {
id: s.property.numeric(),
createdAt: s.property.date(),
name: s.property.string(),
email: s.property.string(),
active: s.property.boolean(),
extendedProperty: s.property.string(),
},
});
});
it("initializes a new model", () => {
const article = getTestArticle();
const newModel = Article.model.model(article);
expect(newModel.instance).toBe(article);
});
it("gets a model state from its instance", () => {
const article = getTestArticle();
expect(Article.model.model(article).isNew()).toBeTruthy();
expect(Article.model.model(article).isDirty()).toBeFalsy();
});
it("gets a model identifier value", () => {
const article = getTestArticle();
expect(Article.model.model(article).getIdentifier()).toBe(1);
});
it("gets a model composite identifier value", () => {
class CompositeModel {
static model = s.defineModel({
Class: CompositeModel,
properties: {
firstId: s.property.numeric(),
secondId: s.property.numeric(),
label: s.property.string(),
},
identifier: ["firstId", "secondId"],
});
firstId: number;
secondId: number;
label: string;
}
expect(
CompositeModel.model
.model(
Object.assign(new CompositeModel(), {
firstId: 5,
secondId: 6,
label: "test",
}),
)
.getIdentifier(),
).toStrictEqual([5, 6]);
});
it("checks model dirtiness when altered, then reset diff", () => {
const article = getTestArticle();
expect(Article.model.model(article).isDirty()).toBeFalsy();
article.title = "new title";
expect(Article.model.model(article).isDirty()).toBeTruthy();
Article.model.model(article).resetDiff();
expect(Article.model.model(article).isDirty()).toBeFalsy();
});
it("deserializes a model from a serialized form", () => {
const expectedArticle = Object.assign(new Article(), {
id: 1,
title: "this is a test",
authors: [
Object.assign(new Account(), {
id: 52,
name: "John Doe",
email: "test@test.test",
createdAt: new Date("2022-08-07T08:47:01.000Z"),
active: true,
}),
Object.assign(new Account(), {
id: 4,
name: "Tester",
email: "another@test.test",
createdAt: new Date("2022-09-07T18:32:55.000Z"),
active: false,
}),
],
text: "this is a long test.",
evaluation: 8.52,
tags: [{name: "test"}, {name: "foo"}],
comments: [
Object.assign(new ArticleComment(), {
id: 542,
author: Object.assign(new Account(), {
id: 52,
name: "John Doe",
email: "test@test.test",
createdAt: new Date("2022-08-07T08:47:01.000Z"),
active: true,
}),
message: "comment content",
}),
],
});
const deserializedArticle = Article.model.parse({
id: 1,
title: "this is a test",
authors: [
{
id: 52,
name: "John Doe",
email: "test@test.test",
createdAt: "2022-08-07T08:47:01.000Z",
active: true,
},
{
id: 4,
name: "Tester",
email: "another@test.test",
createdAt: "2022-09-07T18:32:55.000Z",
active: false,
},
],
text: "this is a long test.",
evaluation: "8.52",
tags: [{name: "test"}, {name: "foo"}],
comments: [
{
id: 542,
author: {
id: 52,
name: "John Doe",
email: "test@test.test",
createdAt: "2022-08-07T08:47:01.000Z",
active: true,
},
message: "comment content",
},
],
});
const deserializedArticleProperties = Article.model
.model(deserializedArticle)
.getInstanceProperties();
delete deserializedArticleProperties.authors[0]._sharkitek;
delete deserializedArticleProperties.authors[1]._sharkitek;
delete deserializedArticleProperties.comments[0]._sharkitek;
delete (deserializedArticleProperties.comments[0].author as any)._sharkitek;
const expectedArticleProperties = Article.model
.model(expectedArticle)
.getInstanceProperties();
delete expectedArticleProperties.authors[0]._sharkitek;
delete expectedArticleProperties.authors[1]._sharkitek;
delete expectedArticleProperties.comments[0]._sharkitek;
delete (expectedArticleProperties.comments[0].author as any)._sharkitek;
expect(deserializedArticleProperties).toEqual(expectedArticleProperties);
});
it("serializes an initialized model", () => {
const article = getTestArticle();
expect(Article.model.model(article).serialize()).toEqual({
id: 1,
title: "this is a test",
text: "this is a long test.",
evaluation: "25.23",
tags: [{name: "test"}, {name: "foo"}],
authors: [
{
id: 52,
createdAt: article.authors[0].createdAt.toISOString(),
name: "John Doe",
email: "john@doe.test",
active: true,
},
],
comments: [],
});
});
it("deserializes, changes and patches", () => {
const deserializedArticle = Article.model.parse({
id: 1,
title: "this is a test",
authors: [
{
id: 52,
name: "John Doe",
email: "test@test.test",
createdAt: "2022-08-07T08:47:01.000Z",
active: true,
},
{
id: 4,
name: "Tester",
email: "another@test.test",
createdAt: "2022-09-07T18:32:55.000Z",
active: false,
},
],
text: "this is a long test.",
evaluation: "8.52",
tags: [{name: "test"}, {name: "foo"}],
comments: [
{
id: 542,
author: {
id: 52,
name: "John Doe",
email: "test@test.test",
createdAt: "2022-08-07T08:47:01.000Z",
active: true,
},
message: "comment content",
},
],
});
deserializedArticle.text = "A new text for a new life!";
expect(Article.model.model(deserializedArticle).patch()).toStrictEqual({
id: 1,
text: "A new text for a new life!",
});
deserializedArticle.evaluation = 5.24;
expect(Article.model.model(deserializedArticle).patch()).toStrictEqual({
id: 1,
evaluation: "5.24",
});
});
it("patches with modified submodels", () => {
const deserializedArticle = Article.model.parse({
id: 1,
title: "this is a test",
authors: [
{
id: 52,
name: "John Doe",
email: "test@test.test",
createdAt: "2022-08-07T08:47:01.000Z",
active: true,
},
{
id: 4,
name: "Tester",
email: "another@test.test",
createdAt: "2022-09-07T18:32:55.000Z",
active: false,
},
],
text: "this is a long test.",
evaluation: "8.52",
tags: [{name: "test"}, {name: "foo"}],
comments: [
{
id: 542,
author: {
id: 52,
name: "John Doe",
email: "test@test.test",
createdAt: "2022-08-07T08:47:01.000Z",
active: true,
},
message: "comment content",
},
],
});
deserializedArticle.authors[1].active = true;
expect(Article.model.model(deserializedArticle).patch()).toStrictEqual({
id: 1,
authors: [{id: 52}, {id: 4, active: true}],
});
deserializedArticle.comments[0].author.name = "Johnny";
expect(Article.model.model(deserializedArticle).patch()).toStrictEqual({
id: 1,
comments: [
{
id: 542,
author: {
id: 52,
name: "Johnny",
},
},
],
});
});
it("deserializes and patches with fields that are not properties", () => {
class TestModel {
static model = defineModel({
Class: TestModel,
properties: {
id: s.property.numeric(),
label: s.property.string(),
},
identifier: "id",
});
id: number;
label: string;
notAProperty: {hello: string} = {hello: "world"};
}
const deserializedModel = TestModel.model.parse({
id: 5,
label: "testing",
});
expect(deserializedModel.id).toBe(5);
expect(deserializedModel.label).toBe("testing");
expect(deserializedModel.notAProperty?.hello).toBe("world");
const clonedDeserializedModel = TestModel.model
.model(deserializedModel)
.clone();
deserializedModel.label = "new!";
expect(TestModel.model.model(deserializedModel).patch()).toStrictEqual({
id: 5,
label: "new!",
});
deserializedModel.notAProperty.hello = "monster";
expect(TestModel.model.model(deserializedModel).patch()).toStrictEqual({
id: 5,
});
expect(TestModel.model.model(deserializedModel).serialize()).toStrictEqual({
id: 5,
label: "new!",
});
expect(
TestModel.model.model(clonedDeserializedModel).serialize(),
).toStrictEqual({id: 5, label: "testing"});
expect(clonedDeserializedModel.notAProperty.hello).toEqual("world");
});
it("assigns properties, ignoring fields which are not properties", () => {
const deserializedArticle = Article.model.parse({
id: 1,
title: "this is a test",
authors: [
{
id: 52,
name: "John Doe",
email: "test@test.test",
createdAt: "2022-08-07T08:47:01.000Z",
active: true,
},
{
id: 4,
name: "Tester",
email: "another@test.test",
createdAt: "2022-09-07T18:32:55.000Z",
active: false,
},
],
text: "this is a long test.",
evaluation: "8.52",
tags: [{name: "test"}, {name: "foo"}],
comments: [
{
id: 542,
author: {
id: 52,
name: "John Doe",
email: "test@test.test",
createdAt: "2022-08-07T08:47:01.000Z",
active: true,
},
message: "comment content",
},
],
});
// Assign title and text, html is silently ignored.
Article.model.model(deserializedArticle).assign({
title: "something else",
text: "fully new text! yes!",
html: "<p>fully new text! yes!</p>",
});
expect((deserializedArticle as any)?.html).toBeUndefined();
expect(Article.model.model(deserializedArticle).patch()).toStrictEqual({
id: 1,
title: "something else",
text: "fully new text! yes!",
});
});
it("initializes a model from properties values", () => {
const testArticle = Article.model.from({
title: "this is a test",
authors: [
Account.model.from({
name: "John Doe",
email: "test@test.test",
createdAt: new Date(),
active: true,
}),
],
text: "this is a long text",
evaluation: 8.52,
tags: [{name: "test"}, {name: "foo"}],
unknownField: true,
anotherOne: "test",
});
expect(testArticle.title).toBe("this is a test");
expect(testArticle.text).toBe("this is a long text");
expect(testArticle.evaluation).toBe(8.52);
expect(testArticle.authors).toHaveLength(1);
expect(testArticle.authors[0]?.name).toBe("John Doe");
expect((testArticle as any).unknownField).toBeUndefined();
expect((testArticle as any).anotherOne).toBeUndefined();
});
it("applies patches to an existing model", () => {
const testArticle = Article.model.from({
id: 1,
title: "this is a test",
authors: [
Account.model.from({
id: 55,
name: "John Doe",
email: "test@test.test",
createdAt: new Date(),
active: true,
}),
],
text: "this is a long text",
evaluation: 8.52,
tags: [{name: "test"}, {name: "foo"}],
unknownField: true,
anotherOne: "test",
});
Article.model.model(testArticle).resetDiff();
// Test simple patch.
Article.model.model(testArticle).applyPatch({
title: "new title",
});
expect(testArticle.title).toBe("new title");
expect(Article.model.model(testArticle).serializeDiff()).toStrictEqual({
id: 1,
});
// Test originals update propagation.
Article.model.model(testArticle).applyPatch({
authors: [{email: "john@test.test"}],
});
expect(testArticle.authors[0].email).toBe("john@test.test");
expect(Article.model.model(testArticle).serializeDiff()).toStrictEqual({
id: 1,
});
// Test without originals update.
Article.model.model(testArticle).applyPatch(
{
authors: [{name: "Johnny"}],
},
false,
);
expect(testArticle.authors[0].name).toBe("Johnny");
expect(Article.model.model(testArticle).serializeDiff()).toStrictEqual({
id: 1,
authors: [{id: 55, name: "Johnny"}],
});
});
});