Skip to content

Commit

Permalink
add more tests
Browse files Browse the repository at this point in the history
  • Loading branch information
madeindjs committed Jan 6, 2025
1 parent 0624f9c commit 86b780e
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 22 deletions.
1 change: 0 additions & 1 deletion src/ui/src/components/core/content/CoreLink.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ export default {
type: FieldType.Text,
default: "https://writer.com",
desc: "Specify a URL or choose a page. Keep in mind that you can only link to pages for which a key has been specified.",
// TODO: build dynamic schema
options: (wf: Core) => {
return Object.fromEntries(
wf
Expand Down
48 changes: 48 additions & 0 deletions src/ui/src/constants/validator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { describe, expect, it } from "vitest";
import {
getJsonSchemaValidator,
validatorCssClassname,
validatorCssSize,
} from "./validators";

describe("validators", () => {
describe("CSS Classname", () => {
const validator = getJsonSchemaValidator(validatorCssClassname);

it.each([
"",
"foo bar",
"foo",
"foo bar baz",
" foo bar ",
"foo123 bar_baz",
"class1 class2",
"foo-bar",
"foo bar-baz",
])("should be valid class names: %s", (value) => {
expect(validator(value)).toBe(true);
});

it.each(["123", "-abc"])(
"should be invalid class names: %s",
(value) => {
expect(validator(value)).toBe(false);
},
);
});

describe("CSS size", () => {
const validator = getJsonSchemaValidator(validatorCssSize);

it.each(["", "12px", "1rem", "2vh"])(
"should be valid size: %s",
(value) => {
expect(validator(value)).toBe(true);
},
);

it.each(["px", "vh"])("should be invalid class names: %s", (value) => {
expect(validator(value)).toBe(false);
});
});
});
35 changes: 22 additions & 13 deletions src/ui/src/constants/validators.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import injectionKeys from "@/injectionKeys";
import { Ajv, Format, type SchemaObject } from "ajv";
import { inject, provide } from "vue";
import { inject } from "vue";

export enum ValidatorCustomFormat {
/**
* Check that the workflow key is existing
*/
WriterWorkflowKey = "writer#workflowKey",
CssClassnames = "cssClassnames",
CssSize = "cssSize",
Uri = "uri",
Uuid = "uuid",
}

/**
* We use an URL to define the `$id` of the schema. The URL doesn't have to exist, it's only used for caching.
Expand Down Expand Up @@ -33,14 +44,13 @@ export function buildJsonSchemaForNumberBetween(
export const validatorCssClassname: SchemaObject = {
$id: generateSchemaId("cssClassname"),
type: "string",
pattern: "(^[a-zA-Z_][a-zA-Z0-9_-]*$)|(^$)",
format: ValidatorCustomFormat.CssClassnames,
};

export const validatorCssSize: SchemaObject = {
$id: generateSchemaId("cssSize"),
type: "string",
pattern:
"(^([+-]?\\d*\\.?\\d+)(px|em|%|vh|vw|rem|pt|pc|in|cm|mm|ex|ch|vmin|vmax|fr)$)|(^$)",
format: ValidatorCustomFormat.CssSize,
};

export const validatorEnumYesNo: SchemaObject = buildJsonSchemaForEnum([
Expand Down Expand Up @@ -157,15 +167,6 @@ export function getJsonSchemaValidator(schema: SchemaObject) {

// custom formats

export enum ValidatorCustomFormat {
/**
* Check that the workflow key is existing
*/
WriterWorkflowKey = "writer#workflowKey",
Uri = "uri",
Uuid = "uuid",
}

export const validatorCustomSchemas: Record<
ValidatorCustomFormat,
{ format: Format; errorMessage: string }
Expand All @@ -178,6 +179,14 @@ export const validatorCustomSchemas: Record<
format: /^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i,
errorMessage: "must be a valid UUID",
},
[ValidatorCustomFormat.CssClassnames]: {
format: /(^\s*[a-zA-Z_][-\w]*(\s+[a-zA-Z_][-\w]*)*\s*$)|(^$)/,
errorMessage: "must be a valid list of CSS classes separated by spaces",
},
[ValidatorCustomFormat.CssSize]: {
format: /(^([+-]?\d*\.?\d+)(px|em|%|vh|vw|rem|pt|pc|in|cm|mm|ex|ch|vmin|vmax|fr)$)|(^$)/,
errorMessage: "must be a valid CSS size",
},
[ValidatorCustomFormat.WriterWorkflowKey]: {
format: {
type: "string",
Expand Down
50 changes: 44 additions & 6 deletions src/ui/src/renderer/useFieldsErrors.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import { describe, vi, it, expect, beforeAll } from "vitest";
import { useFieldsErrors } from "./useFieldsErrors";
import { computed, ref } from "vue";
import { generateCore } from "@/core";
import { Core, FieldType, InstancePath } from "@/writerTypes";
import {
Core,
FieldType,
InstancePath,
WriterComponentDefinition,
} from "@/writerTypes";
import { validatorCustomSchemas } from "@/constants/validators";

const getEvaluatedFields = vi.fn();
Expand All @@ -20,15 +25,20 @@ describe(useFieldsErrors.name, () => {
]);
let core: Core;

const dummyComponent: WriterComponentDefinition = {
name: "dummmy component",
description: "",
};

beforeAll(() => {
core = generateCore();
vi.spyOn(core, "getComponentById").mockReturnValue({} as any);
// @ts-expect-error return a dummy mock
vi.spyOn(core, "getComponentById").mockReturnValue({});
});

it("should validate a field as number", () => {
vi.spyOn(core, "getComponentDefinition").mockReturnValue({
name: "dummmy component",
description: "",
...dummyComponent,
fields: {
value: {
name: "value",
Expand All @@ -54,8 +64,7 @@ describe(useFieldsErrors.name, () => {

it("should validate a field as string", () => {
vi.spyOn(core, "getComponentDefinition").mockReturnValue({
name: "dummmy component",
description: "",
...dummyComponent,
fields: {
value: {
name: "value",
Expand All @@ -80,4 +89,33 @@ describe(useFieldsErrors.name, () => {

expect(errors.value).toStrictEqual({ value: undefined });
});

it("should validate a field as options", () => {
vi.spyOn(core, "getComponentDefinition").mockReturnValue({
...dummyComponent,
fields: {
value: {
name: "value",
type: FieldType.Text,
options: {
a: "A",
b: "B",
c: "C",
},
},
},
});

const value = ref("test");
getEvaluatedFields.mockReturnValue({ value });

const errors = useFieldsErrors(core, instancePath);
expect(errors.value).toStrictEqual({
value: "must be equal to one of the allowed values: a,b,c",
});

value.value = "a";

expect(errors.value).toStrictEqual({ value: undefined });
});
});
5 changes: 3 additions & 2 deletions src/ui/src/renderer/useFieldsErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,9 @@ function computeFieldErrors(

if (
schema === undefined &&
Array.isArray(field.options) &&
field.options.length > 0
typeof field.options === "object" &&
field.options !== null &&
Object.keys(field.options).length > 0
) {
// set an automatic enum schema for options fields
schema = buildJsonSchemaForEnum(Object.keys(field.options));
Expand Down
86 changes: 86 additions & 0 deletions tests/e2e/tests/builderFieldValidation.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { test, expect } from "@playwright/test";

test.describe("Builder field validation", () => {
let url: string;

test.beforeAll(async ({ request }) => {
const response = await request.post(`/preset/section`);
expect(response.ok()).toBeTruthy();
({ url } = await response.json());
});

test.afterAll(async ({ request }) => {
await request.delete(url);
});

test.beforeEach(async ({ page }) => {
await page.goto(url, { waitUntil: "domcontentloaded" });
test.setTimeout(5000);
});

test("should display error for invalid button fields", async ({ page }) => {
await page
.locator(`.BuilderSidebarToolkit [data-component-type="button"]`)
.dragTo(page.locator(".CoreSection"));
await page.locator(`button.CoreButton.component`).click();

// is disabled

const isDisabledInput = page.locator(
'.BuilderFieldsText[data-automation-key="isDisabled"] input',
);

await isDisabledInput.fill("maybe");
expect(await isDisabledInput.getAttribute("aria-invalid")).toBe("true");

await isDisabledInput.fill("yes");
expect(await isDisabledInput.getAttribute("aria-invalid")).toBe("false");

// css classes

const cssClasses = page.locator(
'.BuilderFieldsText[data-automation-key="cssClasses"] input',
);
await cssClasses.fill("1234");
expect(await cssClasses.getAttribute("aria-invalid")).toBe("true");

await cssClasses.fill("class1 class2");
expect(await cssClasses.getAttribute("aria-invalid")).toBe("false");
});

test("should display error for invalid multiselectinput fields", async ({
page,
}) => {
await page
.locator(
`.BuilderSidebarToolkit [data-component-type="multiselectinput"]`,
)
.dragTo(page.locator(".CoreSection"));
await page.locator(`.CoreMultiselectInput.component`).click();

// maximum count

const maximunCountInput = page.locator(
'.BuilderFieldsText[data-automation-key="maximumCount"] input',
);

await maximunCountInput.fill("-1");
expect(await maximunCountInput.getAttribute("aria-invalid")).toBe("true");

await maximunCountInput.fill("2");
expect(await maximunCountInput.getAttribute("aria-invalid")).toBe("false");

// options

await page.locator(".BuilderFieldsOptions button").nth(1).click();

const optionsTextarea = page.locator(
'.BuilderFieldsObject[data-automation-key="options"] textarea',
);
await optionsTextarea.fill(JSON.stringify(true));
expect(await optionsTextarea.getAttribute("aria-invalid")).toBe("true");

await optionsTextarea.fill(JSON.stringify({ a: "A", b: "B" }));
expect(await optionsTextarea.getAttribute("aria-invalid")).toBe("false");
});
});

0 comments on commit 86b780e

Please sign in to comment.