import React from 'react';
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { render, cleanup } from '@testing-library/preact';
import { LML, LmlSanitizationRules } from '../types';
import { defaultSanitizationRules, lml2jsx, processJSX } from '../core';
import { h } from 'preact';
describe('LML Preact JSX', () => {
let errors: string[] = [];
let logs: string[];
beforeEach(() => {
errors = [];
logs = mockGlobalConsole();
});
afterEach(() => {
expect(errors).toMatchObject([]);
resetGlobalConsole();
})
const console1 = globalThis.console;
const sanitizationRules: LmlSanitizationRules = {
allowedElements: new Set(["input", "my-widget", ...defaultSanitizationRules.allowedElements]),
forbiddenElementAttributes: defaultSanitizationRules.forbiddenElementAttributes,
forbidEventHandlers: true,
allowedUrlPrefixes: defaultSanitizationRules.allowedUrlPrefixes,
urlAttributes: defaultSanitizationRules.urlAttributes
}
function mockGlobalConsole() {
const logs: string[] = [];
globalThis.console = Object.create(console1, {
error: {
writable: true,
configurable: true,
value: (...args: any[]) => {
logs.push(args[0]);
}
}
});
return logs;
}
function resetGlobalConsole() {
globalThis.console = console1;
}
function getJSX(v: LML) {
return lml2jsx(v, h, (name, ns) => {
if (name === "MyCpt" && ns === "") {
return MyCpt;
}
if (name === "MyCpt" && ns === "b") {
return MyCpt2;
}
if (name === "img" && ns === "c") {
return Img;
}
return null;
}, (msg) => {
errors.push(msg);
}, sanitizationRules);
}
function printJSX(jsxContent: JSX.Element | string | null | (JSX.Element | string)[]) {
cleanup();
let container = render(jsxContent);
return container.baseElement.innerHTML;
}
function print(v: LML) {
return printJSX((getJSX(v)));
}
// Text node
const ex1: LML =
// Hello World
"Hello World";
// Span, no attributes
const ex2: LML =
// Hello World!
["#span.hello", "Hello", ["#em", "World!"]]
// Span with attributes
const ex3: LML =
// Hello World!
["#span.hello", { "title": "Greetings" }, "Hello", ["#em", "World!"]]
// Fragment
const ex4: LML =
// <>Hello World!>
[["#em", "Hello"], "World!"]
// Component
const ex5: LML =
// Some content...
["*MyCpt.abc", { "title": "..." }, " Some ", ["#span.em", "content... "]]
// Node with type, id and empty attribute (here: checked - value will be ignored)
const ex6: LML =
//
["#input+checkbox.abc", { "id": "subscribeNews", "name": "subscribe", "value": "newsletter", "disabled": true }]
// Advanced component with bundle id + JSON and LML attributes
const ex7: LML =
["*b:MyCpt", { // b = bundle id
"logo": ["*c:img", { "height": 22, "width": 22, "src": "..." }],
"columnWidths": [1, 2, 3, 4]
},
["#span.hello", "Some ", ["#em", "content..."]]
]
const MyCpt = (props: { title: string, className?: string, children?: any }) => {
const { title, className, children } = props;
return
}
const MyCpt2 = (props: { columnWidths: number[], logo: any, children?: any }) => {
const { columnWidths, logo, children } = props;
let count = 0;
for (let nbr of columnWidths) {
count += nbr;
}
const jsxLogo = getJSX(logo);
return {jsxLogo} / {children}
}
const Img = (props: { height: number, width: number, src: string }) => {
const { height, width, src } = props;
return
}
it('should support text nodes', async () => {
expect(printJSX("Hello")).toBe("Hello
");
expect(printJSX(ABC )).toBe("ABC
");
expect(printJSX(h("div", { style: "color:red;font-weight:bold" }, "Hello"))).toBe('')
expect(print("")).toBe("
");
expect(print(undefined as any)).toBe("
");
expect(print(ex1)).toBe("Hello World
");
expect(print(["Hello ", "World"])).toBe("Hello World
");
expect(print(["Hello ", "", "World"])).toBe("Hello World
");
expect(print(["", "Hello"])).toBe("Hello
");
expect(print(["#", "Hello"])).toBe("#Hello
");
});
it('should support simple elements', async () => {
expect(print(["#span", "Hi"])).toBe("Hi
");
expect(print(["#span", "Hello", ["#em", "World!"]])).toBe('HelloWorld!
');
expect(print(["#span", "Hello", ["#b", "World", ["#i", "!"]], ["#br"]])).toBe('HelloWorld!
');
});
it('should support element with attributes', async () => {
expect(print(["#span", { "title": "abc" }, "Hi"])).toBe('Hi
');
expect(print(["#span.foo", { "title": "abc" }, "Hi"])).toBe('Hi
');
expect(print(["#span.foo.bar", { "title": "abc" }, "Hi"])).toBe('Hi
');
expect(print(["#span.foo.bar", "Hi"])).toBe('Hi
');
expect(print(["#my-widget.foo.bar", "Hi"])).toBe('Hi
');
expect(print(["#span.foo", { "class": "abc" }, "Hi"])).toBe('Hi
');
expect(print(["#span.foo.bar", { "class": "abc" }, "Hi"])).toBe('Hi
');
expect(print(["#span", { "misc": true, "baz": 123 }, "Hi"])).toBe('Hi
');
expect(print(["#span", { "misc": { a: "abc" } }, "Hi"])).toBe('Hi
');
expect(print(["#input+text"])).toBe('
');
expect(print(["#input+text.abc"])).toBe('
');
expect(print(["#input+text", { "placeholder": "xxx" }])).toBe('
');
expect(print(["#span.foo.p-12", "Hi"])).toBe('Hi
');
expect(print(ex2)).toBe('HelloWorld!
');
expect(print(ex3)).toBe('HelloWorld!
');
expect(print(ex6)).toBe('
');
});
it('should support element with key attribute', async () => {
// the key attribute is interpreted by JSX and won't show through the print function
expect(print(["#span", { "title": "abc", "key": "123" }, "Hi"])).toBe('Hi
');
expect(getJSX(["#span", { "title": "abc", "key": "123" }, "Hi"])).toMatchObject({
type: 'span',
props: { title: 'abc', children: 'Hi', keyValue: '123' },
key: '123',
});
expect(getJSX(["#span!123", { "title": "abc" }, "Hi"])).toMatchObject({
type: 'span',
props: { title: 'abc', children: 'Hi', keyValue: '123' },
key: '123',
});
expect(getJSX(["#span!123", "Hi"])).toMatchObject({
type: 'span',
props: { children: 'Hi', keyValue: '123' },
key: '123',
});
expect(getJSX(["#input+text.name.pt-4!123!#@$rweT$🕺#%", "Hi"])).toMatchObject({
type: 'input',
props: { "class": "name pt-4", className: "name pt-4", type: "text", children: 'Hi', keyValue: '123!#@$rweT$🕺#%' },
key: '123!#@$rweT$🕺#%',
});
});
it('should support fragments', async () => {
expect(print(ex4)).toBe('Hello World!
');
expect(print(["a", [[[["b", "c"]], ["#span"], [[[]]]]]])).toBe('abc
');
});
it('should support components', async () => {
expect(print(ex5)).toBe('');
});
it('should automatically transform properties that are valid LML values', async () => {
expect(print(ex7)).toBe(' /
Some content... ');
});
it('should not mutate the LML nodes twice', async () => {
// class & className
const v1 = ["#div.foo", { "title": ".." }, "Hello"]
expect(v1).toMatchObject(["#div.foo", { "title": ".." }, "Hello"]);
getJSX(v1);
expect(v1).toMatchObject(["#div", { "title": "..", "class": "foo", "className": "foo" }, "Hello"]);
getJSX(v1);
// no more changes
expect(v1).toMatchObject(["#div", { "title": "..", "class": "foo", "className": "foo" }, "Hello"]);
// type
const v2 = ["#input+text.foo.bar", { "title": "..", "class": "abc" }]
expect(v2).toMatchObject(["#input+text.foo.bar", { "title": ".." }]);
getJSX(v2);
expect(v2).toMatchObject(["#input", { "title": "..", "class": "foo bar abc", "className": "foo bar abc", "type": "text" }]);
getJSX(v2);
// no more chages
expect(v2).toMatchObject(["#input", { "title": "..", "class": "foo bar abc", "className": "foo bar abc", "type": "text" }]);
// key
const v3 = ["*b:MyCpt.xyz!ABCDE", { "title": ".." }];
expect(v3).toMatchObject(["*b:MyCpt.xyz!ABCDE", { "title": ".." }]);
getJSX(v3);
expect(v3).toMatchObject(["*b:MyCpt", { "title": "..", "class": "xyz", "className": "xyz", "key": "ABCDE", "keyValue": "ABCDE" }]);
getJSX(v3);
// no more changes
expect(v3).toMatchObject(["*b:MyCpt", { "title": "..", "class": "xyz", "className": "xyz", "key": "ABCDE", "keyValue": "ABCDE" }]);
});
describe('Errors', () => {
it('should be raised in case of invalid syntax', async () => {
getJSX({ foo: "bar" } as any);
expect(errors).toMatchObject([
'Invalid LML node: {"foo":"bar"}'
]);
errors = [];
});
it('should be raised in case of reserved node types', async () => {
getJSX(["#span", ["@somedecorator", { "foo": "bar" }], ["!somedecorator", { "foo": "bar" }]]);
expect(errors).toMatchObject([
'Invalid LML node: ["@somedecorator",{"foo":"bar"}]',
'Invalid LML node: ["!somedecorator",{"foo":"bar"}]',
]);
errors = [];
});
it('should be raised in case of invalid node', async () => {
getJSX({ "foo": "bar" } as any);
expect(errors).toMatchObject([
'Invalid LML node: {"foo":"bar"}'
]);
errors = [];
getJSX([{ "foo": "bar" }] as any);
expect(errors).toMatchObject([
'Invalid LML node: {"foo":"bar"}'
]);
errors = [];
});
it('should cut message if too long', async () => {
getJSX(["#span", "x", { "foo": "bar", "baz": 124321424234, "blah": "hello world hello world hello world hellow world" } as any]);
expect(errors).toMatchObject([
'Invalid LML node: {"foo":"bar","baz":124321424234,"blah":"hello world hello world hello world hellow...'
]);
errors = [];
});
it('should be raised in case of invalid component', async () => {
getJSX(["#span", ["*Foo", "bar"]]);
expect(errors).toMatchObject([
"Invalid component: Foo"
]);
errors = [];
});
it('should be logged on console when no error handler is provided to lm2JSX', async () => {
lml2jsx(["*x:foo"], h);
expect(logs).toMatchObject([
"[lm2JSX Error] Invalid component: x:foo"
]);
errors = [];
});
it('should be logged on console when no error handler is provided to processJSX', async () => {
processJSX(["!x:foo"], { format: () => "x" });
expect(logs).toMatchObject([
'[LML Scan Error] Invalid LML node: ["!x:foo"]'
]);
errors = [];
});
});
});