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 = // <>HelloWorld! [["#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
{title}{null}
{children}
} 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('
Hello
') 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('
HelloWorld!
'); expect(print(["a", [[[["b", "c"]], ["#span"], [[[]]]]]])).toBe('
abc
'); }); it('should support components', async () => { expect(print(ex5)).toBe('
...
Some content...
'); }); 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 = []; }); }); });