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
\n \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([ "", "", "", '', "", "
Example: > <script>const =</script>
", '
', "", '', ])("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([ "", "", '', "text", "", "", ])("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)); }); });