"use strict";
const assert = require("node:assert");
const { test } = require("node:test");
const {
attributeValueType,
parseComponentMetadata,
parseComponentTags,
validateComponentTags,
} = require("../src/component-metadata");
const source = `component Button {
props {
// @required
label = ""
variant = "primary"
disabled = false
count = 0
items = []
}
view {
}
}`;
test("extracts component prop types, required props, defaults, and options", () => {
const component = parseComponentMetadata(source);
assert.equal(component.name, "Button");
assert.deepEqual(
component.props.map(({ name, type, required, options }) => ({ name, type, required, options })),
[
{ name: "label", type: "string", required: true, options: [] },
{ name: "variant", type: "string", required: false, options: ["danger", "primary"] },
{ name: "disabled", type: "boolean", required: false, options: [] },
{ name: "count", type: "number", required: false, options: [] },
{ name: "items", type: "array", required: false, options: [] },
],
);
});
test("parses multiline component tags and their attribute locations", () => {
const page = ``;
const [tag] = parseComponentTags(page);
assert.equal(tag.name, "Button");
assert.deepEqual(
tag.attributes.map(({ name, value }) => ({ name, value })),
[
{ name: "label", value: "Save" },
{ name: "disabled", value: "{false}" },
],
);
});
test("reports missing, unknown, mismatched, and invalid-option component props", () => {
const catalog = new Map([["Button", parseComponentMetadata(source)]]);
const diagnostics = validateComponentTags(
``,
catalog,
);
assert.deepEqual(
diagnostics.map(({ code, severity }) => ({ code, severity })),
[
{ code: "wrn-missing-component-prop", severity: "error" },
{ code: "wrn-component-prop-option", severity: "warning" },
{ code: "wrn-component-prop-type", severity: "error" },
{ code: "wrn-unknown-component-prop", severity: "warning" },
],
);
});
test("recognizes literal attribute value types", () => {
assert.equal(attributeValueType("{false}"), "boolean");
assert.equal(attributeValueType("{42}"), "number");
assert.equal(attributeValueType("{[1, 2]}"), "array");
assert.equal(attributeValueType("plain"), "string");
});
test("extracts explicit prop types and resolves typed prop/state expressions", () => {
const input = `component DatePicker {
props {
required: boolean = false
value: string
}
state count: number = 0
view { }
}`;
const target = parseComponentMetadata(`component DateInput {
props {
required: boolean = false
value: string
count: number = 0
}
view { }
}`);
const own = parseComponentMetadata(input);
assert.deepEqual(
own.props.map(({ name, type, required }) => ({ name, type, required })),
[
{ name: "required", type: "boolean", required: false },
{ name: "value", type: "string", required: true },
],
);
assert.deepEqual(validateComponentTags(input, new Map([["DateInput", target]])), []);
});
test("extracts declared events and validates component event bindings", () => {
const target = parseComponentMetadata(`component PinInput {
props {
value = ""
@event complete = function
@event clear = function
}
view {
}
}`);
assert.deepEqual(target.events, ["complete", "clear"]);
assert.deepEqual(
validateComponentTags(
``,
new Map([["PinInput", target]]),
).map(({ code }) => code),
["wrn-unknown-component-event"],
);
});
test("extracts v0.6 optional props, declared union options, and outputs", () => {
const metadata = parseComponentMetadata(`component Dialog {
outputs { confirm(payload: string) cancel() }
props {
title: string
description?: string
size: "small" | "large" = "small"
}
view { }
}`);
assert.deepEqual(metadata.events, ["confirm", "cancel"]);
assert.deepEqual(
metadata.props.map(({ name, required, options }) => ({ name, required, options })),
[
{ name: "title", required: true, options: [] },
{ name: "description", required: false, options: [] },
{ name: "size", required: false, options: ["small", "large"] },
],
);
});
test("accepts unresolved dynamic expressions for typed component props", () => {
const featureCard = parseComponentMetadata(`component FeatureCard {
props {
external: boolean = false
disabled: boolean = false
showArrow: boolean = true
}
view { }
}`);
const source = `component FeatureGrid {
props { items: unknown[] = [] }
view {
{#each items as item}
{/each}
}
}`;
assert.equal(attributeValueType("{item.external || false}"), "unknown");
assert.equal(attributeValueType("{item.showArrow !== false}"), "boolean");
assert.deepEqual(validateComponentTags(source, new Map([["FeatureCard", featureCard]])), []);
});
test("continues to reject invalid literal boolean props", () => {
const target = parseComponentMetadata(`component Toggle {
props { enabled: boolean = false }
view { }
}`);
const diagnostics = validateComponentTags(
``,
new Map([["Toggle", target]]),
);
assert.deepEqual(
diagnostics.map(({ code }) => code),
["wrn-component-prop-type"],
);
});
test("accepts unresolved dynamic expressions for declared string options", () => {
const target = parseComponentMetadata(`component FeatureCard {
props { target: "_blank" | "_self" = "_self" }
view { }
}`);
const dynamic = ``;
assert.deepEqual(validateComponentTags(dynamic, new Map([["FeatureCard", target]])), []);
const invalidLiteral = validateComponentTags(
``,
new Map([["FeatureCard", target]]),
);
assert.deepEqual(
invalidLiteral.map(({ code }) => code),
["wrn-component-prop-option"],
);
});