first commit
This commit is contained in:
434
packages/blocks/tests/validation.test.ts
Normal file
434
packages/blocks/tests/validation.test.ts
Normal file
@@ -0,0 +1,434 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { validateBlocks } from "../src/validation.js";
|
||||
|
||||
describe("validateBlocks", () => {
|
||||
// ── Valid blocks ─────────────────────────────────────────────────────────
|
||||
|
||||
describe("valid blocks", () => {
|
||||
it("header", () => {
|
||||
const result = validateBlocks([{ type: "header", text: "Hello" }]);
|
||||
expect(result).toEqual({ valid: true, errors: [] });
|
||||
});
|
||||
|
||||
it("section", () => {
|
||||
const result = validateBlocks([{ type: "section", text: "Body text" }]);
|
||||
expect(result).toEqual({ valid: true, errors: [] });
|
||||
});
|
||||
|
||||
it("divider", () => {
|
||||
const result = validateBlocks([{ type: "divider" }]);
|
||||
expect(result).toEqual({ valid: true, errors: [] });
|
||||
});
|
||||
|
||||
it("fields", () => {
|
||||
const result = validateBlocks([
|
||||
{
|
||||
type: "fields",
|
||||
fields: [{ label: "Status", value: "Active" }],
|
||||
},
|
||||
]);
|
||||
expect(result).toEqual({ valid: true, errors: [] });
|
||||
});
|
||||
|
||||
it("table", () => {
|
||||
const result = validateBlocks([
|
||||
{
|
||||
type: "table",
|
||||
columns: [{ key: "name", label: "Name" }],
|
||||
rows: [{ name: "Alice" }],
|
||||
page_action_id: "load_page",
|
||||
},
|
||||
]);
|
||||
expect(result).toEqual({ valid: true, errors: [] });
|
||||
});
|
||||
|
||||
it("actions", () => {
|
||||
const result = validateBlocks([
|
||||
{
|
||||
type: "actions",
|
||||
elements: [{ type: "button", action_id: "btn1", label: "Click me" }],
|
||||
},
|
||||
]);
|
||||
expect(result).toEqual({ valid: true, errors: [] });
|
||||
});
|
||||
|
||||
it("stats", () => {
|
||||
const result = validateBlocks([
|
||||
{
|
||||
type: "stats",
|
||||
items: [{ label: "Users", value: 42 }],
|
||||
},
|
||||
]);
|
||||
expect(result).toEqual({ valid: true, errors: [] });
|
||||
});
|
||||
|
||||
it("form", () => {
|
||||
const result = validateBlocks([
|
||||
{
|
||||
type: "form",
|
||||
fields: [{ type: "text_input", action_id: "name", label: "Name" }],
|
||||
submit: { label: "Save", action_id: "save" },
|
||||
},
|
||||
]);
|
||||
expect(result).toEqual({ valid: true, errors: [] });
|
||||
});
|
||||
|
||||
it("image", () => {
|
||||
const result = validateBlocks([
|
||||
{ type: "image", url: "https://example.com/img.png", alt: "Photo" },
|
||||
]);
|
||||
expect(result).toEqual({ valid: true, errors: [] });
|
||||
});
|
||||
|
||||
it("context", () => {
|
||||
const result = validateBlocks([{ type: "context", text: "Last updated 5m ago" }]);
|
||||
expect(result).toEqual({ valid: true, errors: [] });
|
||||
});
|
||||
|
||||
it("columns", () => {
|
||||
const result = validateBlocks([
|
||||
{
|
||||
type: "columns",
|
||||
columns: [[{ type: "header", text: "Left" }], [{ type: "header", text: "Right" }]],
|
||||
},
|
||||
]);
|
||||
expect(result).toEqual({ valid: true, errors: [] });
|
||||
});
|
||||
});
|
||||
|
||||
// ── Invalid blocks ───────────────────────────────────────────────────────
|
||||
|
||||
describe("invalid blocks", () => {
|
||||
it("not an array", () => {
|
||||
const result = validateBlocks("not an array");
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toEqual([{ path: "blocks", message: "Blocks must be an array" }]);
|
||||
});
|
||||
|
||||
it("block without type", () => {
|
||||
const result = validateBlocks([{ text: "hello" }]);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors[0]!.path).toBe("blocks[0].type");
|
||||
expect(result.errors[0]!.message).toContain("Unknown block type");
|
||||
});
|
||||
|
||||
it("block with unknown type", () => {
|
||||
const result = validateBlocks([{ type: "foobar" }]);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors[0]!.path).toBe("blocks[0].type");
|
||||
expect(result.errors[0]!.message).toContain("Unknown block type 'foobar'");
|
||||
});
|
||||
|
||||
it("header missing text", () => {
|
||||
const result = validateBlocks([{ type: "header" }]);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toEqual([
|
||||
{
|
||||
path: "blocks[0].text",
|
||||
message: "Required field 'text' must be a string",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("section missing text", () => {
|
||||
const result = validateBlocks([{ type: "section" }]);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors[0]!.path).toBe("blocks[0].text");
|
||||
});
|
||||
|
||||
it("table missing required fields", () => {
|
||||
const result = validateBlocks([{ type: "table" }]);
|
||||
expect(result.valid).toBe(false);
|
||||
const paths = result.errors.map((e) => e.path);
|
||||
expect(paths).toContain("blocks[0].columns");
|
||||
expect(paths).toContain("blocks[0].rows");
|
||||
expect(paths).toContain("blocks[0].page_action_id");
|
||||
});
|
||||
|
||||
it("table column missing key or label", () => {
|
||||
const result = validateBlocks([
|
||||
{
|
||||
type: "table",
|
||||
columns: [{ format: "text" }],
|
||||
rows: [],
|
||||
page_action_id: "p",
|
||||
},
|
||||
]);
|
||||
expect(result.valid).toBe(false);
|
||||
const paths = result.errors.map((e) => e.path);
|
||||
expect(paths).toContain("blocks[0].columns[0].key");
|
||||
expect(paths).toContain("blocks[0].columns[0].label");
|
||||
});
|
||||
|
||||
it("table column with invalid format", () => {
|
||||
const result = validateBlocks([
|
||||
{
|
||||
type: "table",
|
||||
columns: [{ key: "k", label: "K", format: "html" }],
|
||||
rows: [],
|
||||
page_action_id: "p",
|
||||
},
|
||||
]);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors[0]!.path).toBe("blocks[0].columns[0].format");
|
||||
expect(result.errors[0]!.message).toContain("format");
|
||||
});
|
||||
|
||||
it("form missing fields or submit", () => {
|
||||
const result = validateBlocks([{ type: "form" }]);
|
||||
expect(result.valid).toBe(false);
|
||||
const paths = result.errors.map((e) => e.path);
|
||||
expect(paths).toContain("blocks[0].fields");
|
||||
expect(paths).toContain("blocks[0].submit");
|
||||
});
|
||||
|
||||
it("form submit missing action_id", () => {
|
||||
const result = validateBlocks([
|
||||
{
|
||||
type: "form",
|
||||
fields: [],
|
||||
submit: { label: "Save" },
|
||||
},
|
||||
]);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors[0]!.path).toBe("blocks[0].submit.action_id");
|
||||
});
|
||||
|
||||
it("actions with invalid elements", () => {
|
||||
const result = validateBlocks([
|
||||
{
|
||||
type: "actions",
|
||||
elements: [{ type: "invalid_type" }],
|
||||
},
|
||||
]);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors[0]!.path).toBe("blocks[0].elements[0].type");
|
||||
expect(result.errors[0]!.message).toContain("Unknown element type");
|
||||
});
|
||||
|
||||
it("select with empty options array", () => {
|
||||
const result = validateBlocks([
|
||||
{
|
||||
type: "actions",
|
||||
elements: [
|
||||
{
|
||||
type: "select",
|
||||
action_id: "sel",
|
||||
label: "Pick",
|
||||
options: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors[0]!.path).toBe("blocks[0].elements[0].options");
|
||||
expect(result.errors[0]!.message).toContain("must not be empty");
|
||||
});
|
||||
|
||||
it("select option missing label/value", () => {
|
||||
const result = validateBlocks([
|
||||
{
|
||||
type: "actions",
|
||||
elements: [
|
||||
{
|
||||
type: "select",
|
||||
action_id: "sel",
|
||||
label: "Pick",
|
||||
options: [{ foo: "bar" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
expect(result.valid).toBe(false);
|
||||
const paths = result.errors.map((e) => e.path);
|
||||
expect(paths).toContain("blocks[0].elements[0].options[0].label");
|
||||
expect(paths).toContain("blocks[0].elements[0].options[0].value");
|
||||
});
|
||||
|
||||
it("button with invalid style", () => {
|
||||
const result = validateBlocks([
|
||||
{
|
||||
type: "actions",
|
||||
elements: [
|
||||
{
|
||||
type: "button",
|
||||
action_id: "btn",
|
||||
label: "Go",
|
||||
style: "bold",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors[0]!.path).toBe("blocks[0].elements[0].style");
|
||||
});
|
||||
|
||||
it("confirm dialog missing required fields", () => {
|
||||
const result = validateBlocks([
|
||||
{
|
||||
type: "actions",
|
||||
elements: [
|
||||
{
|
||||
type: "button",
|
||||
action_id: "btn",
|
||||
label: "Delete",
|
||||
confirm: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
expect(result.valid).toBe(false);
|
||||
const paths = result.errors.map((e) => e.path);
|
||||
expect(paths).toContain("blocks[0].elements[0].confirm.title");
|
||||
expect(paths).toContain("blocks[0].elements[0].confirm.text");
|
||||
expect(paths).toContain("blocks[0].elements[0].confirm.confirm");
|
||||
expect(paths).toContain("blocks[0].elements[0].confirm.deny");
|
||||
});
|
||||
|
||||
it("image missing url or alt", () => {
|
||||
const result = validateBlocks([{ type: "image" }]);
|
||||
expect(result.valid).toBe(false);
|
||||
const paths = result.errors.map((e) => e.path);
|
||||
expect(paths).toContain("blocks[0].url");
|
||||
expect(paths).toContain("blocks[0].alt");
|
||||
});
|
||||
|
||||
it("columns with less than 2 arrays", () => {
|
||||
const result = validateBlocks([
|
||||
{
|
||||
type: "columns",
|
||||
columns: [[{ type: "divider" }]],
|
||||
},
|
||||
]);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors[0]!.path).toBe("blocks[0].columns");
|
||||
expect(result.errors[0]!.message).toContain("2-3 column arrays");
|
||||
});
|
||||
|
||||
it("columns with more than 3 arrays", () => {
|
||||
const result = validateBlocks([
|
||||
{
|
||||
type: "columns",
|
||||
columns: [
|
||||
[{ type: "divider" }],
|
||||
[{ type: "divider" }],
|
||||
[{ type: "divider" }],
|
||||
[{ type: "divider" }],
|
||||
],
|
||||
},
|
||||
]);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors[0]!.message).toContain("2-3 column arrays");
|
||||
});
|
||||
|
||||
it("columns with invalid nested blocks reports correct path", () => {
|
||||
const result = validateBlocks([
|
||||
{
|
||||
type: "columns",
|
||||
columns: [
|
||||
[{ type: "header", text: "OK" }],
|
||||
[{ type: "header" }], // missing text
|
||||
],
|
||||
},
|
||||
]);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors[0]!.path).toBe("blocks[0].columns[1][0].text");
|
||||
});
|
||||
|
||||
it("stats item missing label or value", () => {
|
||||
const result = validateBlocks([
|
||||
{
|
||||
type: "stats",
|
||||
items: [{ description: "desc" }],
|
||||
},
|
||||
]);
|
||||
expect(result.valid).toBe(false);
|
||||
const paths = result.errors.map((e) => e.path);
|
||||
expect(paths).toContain("blocks[0].items[0].label");
|
||||
expect(paths).toContain("blocks[0].items[0].value");
|
||||
});
|
||||
|
||||
it("stats item with invalid trend", () => {
|
||||
const result = validateBlocks([
|
||||
{
|
||||
type: "stats",
|
||||
items: [{ label: "Users", value: 10, trend: "sideways" }],
|
||||
},
|
||||
]);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors[0]!.path).toBe("blocks[0].items[0].trend");
|
||||
});
|
||||
|
||||
it("form field with invalid condition (no eq/neq)", () => {
|
||||
const result = validateBlocks([
|
||||
{
|
||||
type: "form",
|
||||
fields: [
|
||||
{
|
||||
type: "text_input",
|
||||
action_id: "name",
|
||||
label: "Name",
|
||||
condition: { field: "toggle" },
|
||||
},
|
||||
],
|
||||
submit: { label: "Save", action_id: "save" },
|
||||
},
|
||||
]);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors[0]!.path).toBe("blocks[0].fields[0].condition");
|
||||
expect(result.errors[0]!.message).toContain("either 'eq' or 'neq'");
|
||||
});
|
||||
});
|
||||
|
||||
// ── Edge cases ───────────────────────────────────────────────────────────
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("empty blocks array is valid", () => {
|
||||
const result = validateBlocks([]);
|
||||
expect(result).toEqual({ valid: true, errors: [] });
|
||||
});
|
||||
|
||||
it("deeply nested columns validate recursively", () => {
|
||||
const result = validateBlocks([
|
||||
{
|
||||
type: "columns",
|
||||
columns: [
|
||||
[
|
||||
{
|
||||
type: "columns",
|
||||
columns: [
|
||||
[{ type: "header", text: "Deep left" }],
|
||||
[{ type: "header" }], // missing text
|
||||
],
|
||||
},
|
||||
],
|
||||
[{ type: "divider" }],
|
||||
],
|
||||
},
|
||||
]);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors[0]!.path).toBe("blocks[0].columns[0][0].columns[1][0].text");
|
||||
});
|
||||
|
||||
it("multiple errors in one block are all reported", () => {
|
||||
const result = validateBlocks([
|
||||
{
|
||||
type: "table",
|
||||
columns: [{ format: "invalid" }], // missing key, label, bad format
|
||||
rows: "not an array",
|
||||
// missing page_action_id
|
||||
},
|
||||
]);
|
||||
expect(result.valid).toBe(false);
|
||||
// Should have errors for key, label, format, rows, and page_action_id
|
||||
expect(result.errors.length).toBeGreaterThanOrEqual(4);
|
||||
const paths = result.errors.map((e) => e.path);
|
||||
expect(paths).toContain("blocks[0].columns[0].key");
|
||||
expect(paths).toContain("blocks[0].columns[0].label");
|
||||
expect(paths).toContain("blocks[0].columns[0].format");
|
||||
expect(paths).toContain("blocks[0].rows");
|
||||
expect(paths).toContain("blocks[0].page_action_id");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user