import { expect, test } from "vitest";
import { renderResultToHtml } from "./render_lua_html.ts";
import { LuaTable } from "./runtime.ts";
import { makeLuaFloat } from "./numeric.ts";
import { LIQ_NULL } from "./liq_null.ts";
// ── Nil / empty values ──────────────────────────────────────────────
test("null renders as empty span with dataType nil", async () => {
const r = await renderResultToHtml(null);
expect(r).toEqual({ html: ``, dataType: "nil" });
});
test("undefined renders as empty span with dataType nil", async () => {
const r = await renderResultToHtml(undefined);
expect(r).toEqual({ html: ``, dataType: "nil" });
});
test("LIQ_NULL (SQL NULL) renders as empty span with dataType nil", async () => {
const r = await renderResultToHtml(LIQ_NULL);
expect(r).toEqual({ html: ``, dataType: "nil" });
});
// ── Strings ─────────────────────────────────────────────────────────
test("string renders with default escaping", async () => {
const r = await renderResultToHtml("hello world");
expect(r).toEqual({ html: "hello world", dataType: "string" });
});
test("empty string is rendered (not treated as nil)", async () => {
const r = await renderResultToHtml("");
expect(r).toEqual({ html: "", dataType: "string" });
});
test("string with HTML special chars is escaped", async () => {
const r = await renderResultToHtml('"Tom & Jerry"');
expect(r).toEqual({
html: '<b>"Tom & Jerry"</b>',
dataType: "string",
});
});
// ── Numbers ─────────────────────────────────────────────────────────
test("integer number", async () => {
const r = await renderResultToHtml(42);
expect(r).toEqual({ html: "42", dataType: "number" });
});
test("zero", async () => {
const r = await renderResultToHtml(0);
expect(r).toEqual({ html: "0", dataType: "number" });
});
test("non-integer float", async () => {
const r = await renderResultToHtml(1.5);
expect(r).toEqual({ html: "1.5", dataType: "number" });
});
test("tagged float (integer-valued)", async () => {
const r = await renderResultToHtml(makeLuaFloat(2));
expect(r).toEqual({ html: "2.0", dataType: "number" });
});
// ── Booleans ────────────────────────────────────────────────────────
test("boolean true", async () => {
const r = await renderResultToHtml(true);
expect(r).toEqual({ html: "true", dataType: "boolean" });
});
test("boolean false", async () => {
const r = await renderResultToHtml(false);
expect(r).toEqual({ html: "false", dataType: "boolean" });
});
// ── Fallback (unknown object type) ──────────────────────────────────
test("non-matching object is stringified via renderInline", async () => {
const d = new Date("2024-01-15T00:00:00.000Z");
const r = await renderResultToHtml(d);
expect(r.dataType).toBe("string");
expect(r.html).toContain("2024"); // toString includes year
});
// ── Custom renderInline ─────────────────────────────────────────────
test("custom renderInline is used for strings", async () => {
const custom = (s: string) => `${s}`;
const r = await renderResultToHtml("hello", custom);
expect(r).toEqual({ html: "hello", dataType: "string" });
});
// ── LuaTable: empty ─────────────────────────────────────────────────
test("empty LuaTable", async () => {
const r = await renderResultToHtml(new LuaTable());
expect(r).toEqual({
html: `
`,
dataType: "table",
});
});
// ── LuaTable: pure array of scalars ─────────────────────────────────
test("LuaTable array of scalars renders as ", async () => {
const tbl = new LuaTable();
tbl.rawSet(1, "alpha");
tbl.rawSet(2, "beta");
tbl.rawSet(3, "gamma");
const r = await renderResultToHtml(tbl);
expect(r.dataType).toBe("list");
expect(r.html).toBe(
'',
);
});
// ── LuaTable: array of LuaTables → multi-row table ─────────────────
test("LuaTable array of LuaTables renders as multi-row table", async () => {
const row1 = new LuaTable();
row1.rawSet("name", "Alice");
row1.rawSet("age", 30);
const row2 = new LuaTable();
row2.rawSet("name", "Bob");
row2.rawSet("age", 25);
const tbl = new LuaTable();
tbl.rawSet(1, row1);
tbl.rawSet(2, row2);
const r = await renderResultToHtml(tbl);
expect(r.dataType).toBe("list");
expect(r.html).toBe(
"" +
"| name | age |
" +
"" +
'| Alice | 30 |
' +
'| Bob | 25 |
' +
"" +
"
",
);
});
// ── LuaTable: record (string keys) ─────────────────────────────────
test("LuaTable with string keys renders as single-row table", async () => {
const tbl = new LuaTable();
tbl.rawSet("x", 10);
tbl.rawSet("y", 20);
const r = await renderResultToHtml(tbl);
expect(r.dataType).toBe("table");
expect(r.html).toBe(
"" +
"| x | y |
" +
"" +
'| 10 | ' +
'20 | ' +
"
" +
"
",
);
});
// ── LuaTable: mixed keys ────────────────────────────────────────────
test("LuaTable with mixed keys uses keys order in header", async () => {
const tbl = new LuaTable();
tbl.rawSet(1, "first");
tbl.rawSet("label", "test");
const r = await renderResultToHtml(tbl);
expect(r.dataType).toBe("table");
// hasStringKeys → record path; keys() returns both 1 and "label"
expect(r.html).toContain("1 | ");
expect(r.html).toContain("label | ");
expect(r.html).toContain("first");
expect(r.html).toContain("test");
});
// ── JS arrays ───────────────────────────────────────────────────────
test("empty JS array", async () => {
const r = await renderResultToHtml([]);
expect(r).toEqual({
html: ``,
dataType: "table",
});
});
test("JS array of plain objects renders as multi-row table", async () => {
const r = await renderResultToHtml([
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
]);
expect(r.dataType).toBe("table");
expect(r.html).toBe(
"" +
"| id | name |
" +
"" +
'| 1 | Alice |
' +
'| 2 | Bob |
' +
"" +
"
",
);
});
test("JS array of scalars renders as ", async () => {
const r = await renderResultToHtml([10, 20, 30]);
expect(r.dataType).toBe("list");
expect(r.html).toBe(
'',
);
});
// ── JS objects ──────────────────────────────────────────────────────
test("empty JS object", async () => {
const r = await renderResultToHtml({});
expect(r).toEqual({
html: ``,
dataType: "table",
});
});
test("single JS plain object renders as single-row table", async () => {
const r = await renderResultToHtml({ color: "red", count: 5 });
expect(r.dataType).toBe("table");
expect(r.html).toBe(
"" +
"| color | count |
" +
"" +
'| red | ' +
'5 | ' +
"
" +
"
",
);
});
// ── Null / empty values inside cells ────────────────────────────────
test("null value in a table cell gets data-table-cell-empty", async () => {
const r = await renderResultToHtml([{ a: 1, b: null }]);
expect(r.html).toContain('data-table-cell-empty');
expect(r.html).toContain("b | ");
});
test("LIQ_NULL in a table cell gets data-table-cell-empty", async () => {
const r = await renderResultToHtml([{ a: 1, b: LIQ_NULL }]);
expect(r.html).toContain('data-table-cell-empty');
});
test("null item in a list gets data-list-item-empty", async () => {
const r = await renderResultToHtml([1, null, 3]);
expect(r.dataType).toBe("list");
expect(r.html).toContain("data-list-item-empty");
});
test("LIQ_NULL in LuaTable cell gets data-table-cell-empty", async () => {
const row = new LuaTable();
row.rawSet("val", LIQ_NULL);
const tbl = new LuaTable();
tbl.rawSet(1, row);
const r = await renderResultToHtml(tbl);
expect(r.html).toContain("data-table-cell-empty");
});
// ── Sparse / missing key handling ───────────────────────────────────
test("JS object array with different keys shows union of headers", async () => {
const r = await renderResultToHtml([
{ a: 1, b: 2 },
{ b: 3, c: 4 },
]);
expect(r.dataType).toBe("table");
expect(r.html).toContain("a | ");
expect(r.html).toContain("b | ");
expect(r.html).toContain("c | ");
// Row 1 has no "c", row 2 has no "a" → both get empty cells
// Count occurrences of data-table-cell-empty
const empties = r.html.match(/data-table-cell-empty/g);
expect(empties).toHaveLength(2);
});
test("LuaTable array with different keys shows union of headers", async () => {
const row1 = new LuaTable();
row1.rawSet("x", 1);
const row2 = new LuaTable();
row2.rawSet("y", 2);
const tbl = new LuaTable();
tbl.rawSet(1, row1);
tbl.rawSet(2, row2);
const r = await renderResultToHtml(tbl);
expect(r.html).toContain("x | ");
expect(r.html).toContain("y | ");
const empties = r.html.match(/data-table-cell-empty/g);
expect(empties).toHaveLength(2);
});
// ── Nested rendering ────────────────────────────────────────────────
test("nested LuaTable in a cell renders recursively", async () => {
const inner = new LuaTable();
inner.rawSet("nested", "value");
const tbl = new LuaTable();
tbl.rawSet("data", inner);
const r = await renderResultToHtml(tbl);
expect(r.dataType).toBe("table");
// The outer td should contain a nested
expect(r.html).toContain('| ');
expect(r.html).toContain(" | nested | ");
expect(r.html).toContain("value");
});
test("nested JS array in a plain object cell", async () => {
const r = await renderResultToHtml({ items: [1, 2, 3] });
expect(r.dataType).toBe("table");
expect(r.html).toContain('');
expect(r.html).toContain("");
expect(r.html).toContain("- {
const tbl = new LuaTable();
tbl.rawSet("child", new LuaTable());
const r = await renderResultToHtml(tbl);
expect(r.html).toContain("data-table-empty");
});
|