From 30eed04cbb5c505b5aec4b25938e3afa421c767e Mon Sep 17 00:00:00 2001 From: Nishant Mittal Date: Fri, 3 Nov 2023 02:21:54 +0530 Subject: [PATCH] feat: add unit tests for attribute parser, case, repeat, math --- src/helpers/case.ts | 2 +- src/helpers/utils/attributes.ts | 12 +- tests/helpers/utils/attributes.spec.ts | 163 +++++++++++++++ tests/parser.spec.ts | 269 +++++++++++++++++++++++++ 4 files changed, 439 insertions(+), 7 deletions(-) create mode 100644 tests/helpers/utils/attributes.spec.ts diff --git a/src/helpers/case.ts b/src/helpers/case.ts index 8d31942..af8160c 100644 --- a/src/helpers/case.ts +++ b/src/helpers/case.ts @@ -2,7 +2,7 @@ import { HandlebarsHelper, HelperConstructorBlock } from "./helper"; export const caseHelper: HelperConstructorBlock = ctx => { return new HandlebarsHelper("case", (type, rawV1): string => { - const v1 = new String(rawV1); + const v1 = new String(rawV1).toString(); switch (type) { case "upper": diff --git a/src/helpers/utils/attributes.ts b/src/helpers/utils/attributes.ts index 0d5f729..0b8676e 100644 --- a/src/helpers/utils/attributes.ts +++ b/src/helpers/utils/attributes.ts @@ -11,7 +11,7 @@ export interface AttributeDefinition { } interface RawAttributes { - [attr: string]: string + [attr: string]: unknown } export interface ParsedAttributes { @@ -21,19 +21,19 @@ export interface ParsedAttributes { export class AttributeParser { constructor(private schema: AttributeDefinition[]) {} - private parseAttribute(attr: AttributeDefinition, rawValue: string) { + private parseAttribute(attr: AttributeDefinition, rawValue: unknown) { switch (attr.valueType) { case AttributeValueType.Boolean: - return new Boolean(rawValue); + return !!rawValue; case AttributeValueType.Number: { - const v = Number.parseFloat(rawValue); - if (Number.isNaN(v)) { + const v = typeof rawValue === "string" ? Number.parseFloat(rawValue) : rawValue; + if (typeof v !== "number" || Number.isNaN(v)) { throw new Error(`Can't convert "${rawValue}" to number while parsing ${attr.name}.`); } return v; } case AttributeValueType.String: - return new String(rawValue); + return new String(rawValue).toString(); } } diff --git a/tests/helpers/utils/attributes.spec.ts b/tests/helpers/utils/attributes.spec.ts new file mode 100644 index 0000000..69281fb --- /dev/null +++ b/tests/helpers/utils/attributes.spec.ts @@ -0,0 +1,163 @@ +import { AttributeParser, AttributeDefinition, AttributeValueType } from "@templates/helpers/utils/attributes"; + +describe("Attribute parser", () => { + test("should be able to set default values", () => { + const schema: AttributeDefinition[] = [ + { + name: "v1", + valueType: AttributeValueType.String, + defaultValue: "s1" + }, + { + name: "v2", + valueType: AttributeValueType.String, + defaultValue: "s2" + }, + { + name: "v3", + valueType: AttributeValueType.Number, + defaultValue: 3 + }, + { + name: "v4", + valueType: AttributeValueType.Boolean, + defaultValue: false + } + ]; + + const attrs = new AttributeParser(schema).parse({ + v5: "test1", + v2: "test2" + }); + + expect(attrs.v1).toEqual("s1"); + expect(attrs.v2).toEqual("test2"); + expect(attrs.v3).toEqual(3); + expect(attrs.v4).toEqual(false); + }); + + test("should be able to parse string values", () => { + const schema: AttributeDefinition[] = [ + { + name: "v1", + valueType: AttributeValueType.String, + defaultValue: "s1" + }, + { + name: "v2", + valueType: AttributeValueType.String, + defaultValue: "s2" + }, + { + name: "v3", + valueType: AttributeValueType.String, + defaultValue: "s3" + }, + { + name: "v4", + valueType: AttributeValueType.String, + defaultValue: "s4" + } + ]; + + const attrs = new AttributeParser(schema).parse({ + v1: "test1", + v2: 123456789, + v3: false, + }); + + expect(attrs.v1).toEqual("test1"); + expect(attrs.v2).toEqual("123456789"); + expect(attrs.v3).toEqual("false"); + expect(attrs.v4).toEqual("s4"); + }); + + test("should be able to parse number values", () => { + const schema: AttributeDefinition[] = [ + { + name: "v1", + valueType: AttributeValueType.Number, + defaultValue: 1 + }, + { + name: "v2", + valueType: AttributeValueType.Number, + defaultValue: 2 + }, + { + name: "v3", + valueType: AttributeValueType.Number, + defaultValue: 3 + }, + ]; + + const attrs = new AttributeParser(schema).parse({ + v1: "123456789", + v2: 987654321, + }); + + expect(attrs.v1).toEqual(123456789); + expect(attrs.v2).toEqual(987654321); + expect(attrs.v3).toEqual(3); + }); + + test("should throw error when can't parse number values", () => { + const schema: AttributeDefinition[] = [ + { + name: "v1", + valueType: AttributeValueType.Number, + defaultValue: 1 + }, + ]; + + const parser = new AttributeParser(schema); + + expect(() => parser.parse({ v1: "abcd" })).toThrow(); + expect(() => parser.parse({ v1: false })).toThrow(); + expect(() => parser.parse({ v1: "123" })).not.toThrow(); + expect(() => parser.parse({ v1: 123 })).not.toThrow(); + }); + + test("should be able to parse boolean values", () => { + const schema: AttributeDefinition[] = [ + { + name: "v1", + valueType: AttributeValueType.Boolean, + defaultValue: true + }, + { + name: "v2", + valueType: AttributeValueType.Boolean, + defaultValue: true + }, + { + name: "v3", + valueType: AttributeValueType.Boolean, + defaultValue: false + }, + { + name: "v4", + valueType: AttributeValueType.Boolean, + defaultValue: false + }, + { + name: "v5", + valueType: AttributeValueType.Boolean, + defaultValue: false + }, + ]; + + const attrs = new AttributeParser(schema).parse({ + v1: "", + v2: 0, + v3: 5, + v4: false, + }); + + expect(attrs.v1).toEqual(false); + expect(attrs.v2).toEqual(false); + expect(attrs.v3).toEqual(true); + expect(attrs.v4).toEqual(false); + expect(attrs.v5).toEqual(false); + }); +}); diff --git a/tests/parser.spec.ts b/tests/parser.spec.ts index 0c7c7ac..4ee556c 100644 --- a/tests/parser.spec.ts +++ b/tests/parser.spec.ts @@ -478,4 +478,273 @@ describe("Template parser", () => { some_time: 17.25 `); }); + + // Math helper. + test("should support math helper", async () => { + const template = { + id: "note-id", + title: "Some Template", + body: dedent` + --- + num1: text + num2: number + + --- + + {{ math num1 "+" num2 }} + {{ math num2 "**" 2 }} + {{ math 2 "-" num1 }} + {{ math num1 "/" 2 }} + {{ math num2 "*" num1 }} + {{ math (math num1 "+" num2) "%" 3 }} + {{ math num2 "/" (math num1 "-" num1) }} + ` + }; + testVariableTypes({ + num1: TextCustomVariable, + num2: NumberCustomVariable, + }); + + handleVariableDialog("ok", { + num1: "11", + num2: "4" + }); + const parsedTemplate = await parser.parseTemplate(template); + expect(parsedTemplate.folder).toBeNull(); + expect(parsedTemplate.tags.length).toEqual(0); + expect(parsedTemplate.title).toEqual("Some Template"); + expect(parsedTemplate.body).toEqual(dedent` + 15 + 16 + -9 + 5.5 + 44 + 0 + Infinity + `); + }); + + test("should show error with invalid usage of math helpers", async () => { + const invalidTemplates = []; + invalidTemplates.push(dedent` + --- + num1: text + + --- + + {{ math num1 "+" num1 }} + `); + + invalidTemplates.push(dedent` + --- + num1: boolean + + --- + + {{ math num1 "+" num1 }} + `); + + invalidTemplates.push(dedent` + {{ math 2 "%" 0 }} + `); + testVariableTypes({ + num1: CustomVariable, + }); + + handleVariableDialog("ok", { + num1: "true", + }); + + let errorMessagesShown = 0; + jest.spyOn(joplin.views.dialogs, "showMessageBox").mockImplementation(async () => { + errorMessagesShown++; + return 0; + }); + + for (const body of invalidTemplates) { + await parser.parseTemplate({ + id: "some-id", + title: "some template", + body, + }); + } + + expect(errorMessagesShown).toEqual(invalidTemplates.length); + }); + + // Repeat helper. + test("should support repeat helper", async () => { + const template = { + id: "note-id", + title: "Some Template", + body: dedent` + --- + num1: number + var1: text + + --- + + {{#repeat num1 }} + {{ var1 }} + + {{#if (compare repeat_index "==" 0)}} + Test + {{else}} + {{#repeat 2}} + Hi + {{/repeat}} + {{/if}} + + {{/repeat}}eof + ` + }; + testVariableTypes({ + num1: NumberCustomVariable, + var1: TextCustomVariable, + }); + + handleVariableDialog("ok", { + num1: "3", + var1: "v" + }); + const parsedTemplate = await parser.parseTemplate(template); + expect(parsedTemplate.folder).toBeNull(); + expect(parsedTemplate.tags.length).toEqual(0); + expect(parsedTemplate.title).toEqual("Some Template"); + expect(parsedTemplate.body).toEqual(dedent` + v + + Test + + v + + Hi + Hi + + v + + Hi + Hi + + eof + `); + }); + + test("should show error with invalid usage of repeat helper", async () => { + const invalidTemplates = []; + invalidTemplates.push(dedent` + --- + var1: text + + --- + + {{#repeat var1 }} + Hi + {{/repeat}} + `); + + invalidTemplates.push(dedent` + --- + var1: text + + --- + + {{#repeat (compare var1 "==" var1) }} + Hi + {{/repeat}} + `); + testVariableTypes({ + var1: TextCustomVariable, + }); + + handleVariableDialog("ok", { + var1: "abc", + }); + + let errorMessagesShown = 0; + jest.spyOn(joplin.views.dialogs, "showMessageBox").mockImplementation(async () => { + errorMessagesShown++; + return 0; + }); + + for (const body of invalidTemplates) { + await parser.parseTemplate({ + id: "some-id", + title: "some template", + body, + }); + } + + expect(errorMessagesShown).toEqual(invalidTemplates.length); + }); + + // Case helper. + test("should support case helper", async () => { + const template = { + id: "note-id", + title: "Some Template", + body: dedent` + --- + var1: text + + --- + + {{ case "upper" var1 }} + {{ case "lower" var1 }} + {{ case "upper" (condition false "!") }} + ` + }; + testVariableTypes({ + var1: TextCustomVariable, + }); + + handleVariableDialog("ok", { + var1: "Variable" + }); + const parsedTemplate = await parser.parseTemplate(template); + expect(parsedTemplate.folder).toBeNull(); + expect(parsedTemplate.tags.length).toEqual(0); + expect(parsedTemplate.title).toEqual("Some Template"); + expect(parsedTemplate.body).toEqual(dedent` + VARIABLE + variable + TRUE + `); + }); + + test("should show error with invalid usage of case helper", async () => { + const invalidTemplates = []; + invalidTemplates.push(dedent` + --- + var1: text + + --- + + {{ case "random" var1 }} + `); + + testVariableTypes({ + var1: TextCustomVariable, + }); + + handleVariableDialog("ok", { + var1: "abc", + }); + + let errorMessagesShown = 0; + jest.spyOn(joplin.views.dialogs, "showMessageBox").mockImplementation(async () => { + errorMessagesShown++; + return 0; + }); + + for (const body of invalidTemplates) { + await parser.parseTemplate({ + id: "some-id", + title: "some template", + body, + }); + } + + expect(errorMessagesShown).toEqual(invalidTemplates.length); + }); });