import { describe, expect, it } from "vitest";
import { findWidgetScriptSyntaxError } from "./widget-script-syntax.js";
describe("widget script syntax", () => {
it("maps a raw newline in a single-quoted string to the original widget line", () => {
const widgetCode = "\n";
expect(findWidgetScriptSyntaxError(widgetCode)).toEqual({
message: "Unterminated string constant",
line: 3,
column: 18,
snippet: "-->";',
'',
'";',
'',
'-->";',
'',
"",
"',
"const =",
"1 < 2 ",
"",
"",
'',
'',
'',
'',
])("accepts valid scripts or markup: %s", (widgetCode) => {
expect(findWidgetScriptSyntaxError(widgetCode)).toBeUndefined();
});
it.each([
"",
"type",
'type=""',
"type='text/javascript'",
"type=application/javascript",
'TYPE=" TEXT/ECMASCRIPT "',
"type=application/ecmascript",
"type=application/x-javascript",
"type=text/jscript",
"type=text/javascript1.5",
"type=text/livescript",
])("rejects top-level await in classic scripts: %s", (attributes) => {
expect(
findWidgetScriptSyntaxError(``),
).toMatchObject({ scriptIndex: 1 });
});
it("does not allow wrapped-function grammar", () => {
expect(findWidgetScriptSyntaxError("")).toBeDefined();
});
it.each([
'type="application/json"',
"type='application/ld+json'",
"type=importmap",
"type=text/template",
"type=text/x-handlebars-template",
'src="script.js"',
"src='script.js'",
"SRC=script.js",
"src",
])("skips non-JavaScript and external scripts: %s", (attributes) => {
expect(findWidgetScriptSyntaxError(``)).toBeUndefined();
});
it.each(["", ''])(
"maps the second script after %s and stops at a case-insensitive raw-text close",
(first) => {
const widgetCode = `${first}\r\n
Text
\r\n";`;
expect(findWidgetScriptSyntaxError(widgetCode)).toEqual({
message: "Unterminated string constant",
line: 3,
column: 18,
snippet: '";',
scriptIndex: 2,
});
},
);
it("reports only the first error across multiple scripts", () => {
expect(
findWidgetScriptSyntaxError(""),
).toMatchObject({ scriptIndex: 1, line: 1, column: 14 });
});
it.each([
"",
"",
"",
'',
" ",
].join("\n");
expect(findWidgetScriptSyntaxError(widgetCode)).toEqual({
message: "Unexpected token",
scriptIndex: 1,
line: 4,
column: 16,
snippet: "",
});
});
it.each(["", '', "", ""])(
"parses the script body through its terminator or EOF: %s",
(endTag) => {
expect(findWidgetScriptSyntaxError(``,
),
).toMatchObject({ scriptIndex: 2, line: 2, column: 14 });
}
},
);
it.each([
'";',
'";',
])("ends script data unless the exact script token double-escapes it: %s", (widgetCode) => {
expect(findWidgetScriptSyntaxError(widgetCode)?.message).toBe("Unterminated string constant");
});
it.each([
"",
"",
"",
'";]]>',
"",
" ]]>
",
'
',
"",
'',
])("parses CDATA-wrapped scripts in SVG: %s", (widgetCode) => {
expect(findWidgetScriptSyntaxError(widgetCode)).toBeUndefined();
});
it.each([
{
widgetCode: "",
line: 1,
column: 28,
snippet: "",
},
{
widgetCode: "\n \n",
line: 2,
column: 27,
snippet: "",
},
{
widgetCode: "\r\n",
line: 4,
column: 6,
snippet: "const =",
},
])(
"maps CDATA errors to the original widget: $widgetCode",
({ widgetCode, line, column, snippet }) => {
expect(findWidgetScriptSyntaxError(widgetCode)).toEqual({
message: "Unexpected token",
scriptIndex: 1,
line,
column,
snippet,
});
},
);
it.each([
"",
"",
'";]]>',
"",
"",
"",
])("still reaches scripts after self-closing or CDATA foreign content: %s", (widgetCode) => {
expect(findWidgetScriptSyntaxError(widgetCode)).toMatchObject({ line: 1 });
});
it.each(["", "", "", ""])(
"leaves CDATA markers outside SVG untouched after %s",
(prefix) => {
expect(
findWidgetScriptSyntaxError(`${prefix}`),
).toMatchObject({ message: "Unexpected token", scriptIndex: 1 });
},
);
it.each(["text/javascript", "text/javascript", "module"])(
"validates scripts whose type contains character references: %s",
(type) => {
expect(findWidgetScriptSyntaxError(``)).toMatchObject({
message: "Unexpected token",
scriptIndex: 1,
});
},
);
it("bounds the offending line snippet", () => {
const widgetCode = ``;
expect(findWidgetScriptSyntaxError(widgetCode)?.snippet).toBe(widgetCode.slice(0, 160));
});
});