/* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; const RuleRewriter = require("resource://devtools/client/fronts/inspector/rule-rewriter.js"); const { isCssPropertyKnown, } = require("resource://devtools/server/actors/css-properties.js"); const TEST_DATA = [ { desc: "simple set", input: "p:v;", instruction: { type: "set", name: "p", value: "N", priority: "", index: 0 }, expected: "p:N;", }, { desc: "simple set clearing !important", input: "p:v !important;", instruction: { type: "set", name: "p", value: "N", priority: "", index: 0 }, expected: "p:N;", }, { desc: "simple set adding !important", input: "p:v;", instruction: { type: "set", name: "p", value: "N", priority: "important", index: 0, }, expected: "p:N !important;", }, { desc: "simple set between comments", input: "/*color:red;*/ p:v; /*color:green;*/", instruction: { type: "set", name: "p", value: "N", priority: "", index: 1 }, expected: "/*color:red;*/ p:N; /*color:green;*/", }, // The rule view can generate a "set" with a previously unknown // property index; which should work like "create". { desc: "set at unknown index", input: "a:b; e: f;", instruction: { type: "set", name: "c", value: "d", priority: "", index: 2 }, expected: "a:b; e: f;c: d;", }, { desc: "simple rename", input: "p:v;", instruction: { type: "rename", name: "p", newName: "q", index: 0 }, expected: "q:v;", }, // "rename" is passed the name that the user entered, and must do // any escaping necessary to ensure that this is an identifier. { desc: "rename requiring escape", input: "p:v;", instruction: { type: "rename", name: "p", newName: "a b", index: 0 }, expected: "a\\ b:v;", }, { desc: "simple create", input: "", instruction: { type: "create", name: "p", value: "v", priority: "important", index: 0, enabled: true, }, expected: "p: v !important;", }, { desc: "create between two properties", input: "a:b; e: f;", instruction: { type: "create", name: "c", value: "d", priority: "", index: 1, enabled: true, }, expected: "a:b; c: d;e: f;", }, // "create" is passed the name that the user entered, and must do // any escaping necessary to ensure that this is an identifier. { desc: "create requiring escape", input: "", instruction: { type: "create", name: "a b", value: "d", priority: "", index: 1, enabled: true, }, expected: "a\\ b: d;", }, { desc: "simple disable", input: "p:v;", instruction: { type: "enable", name: "p", value: false, index: 0 }, expected: "/*! p:v; */", }, { desc: "simple enable", input: "/* color:v; */", instruction: { type: "enable", name: "color", value: true, index: 0 }, expected: "color:v;", }, { desc: "enable with following property in comment", input: "/* color:red; color: blue; */", instruction: { type: "enable", name: "color", value: true, index: 0 }, expected: "color:red; /* color: blue; */", }, { desc: "enable with preceding property in comment", input: "/* color:red; color: blue; */", instruction: { type: "enable", name: "color", value: true, index: 1 }, expected: "/* color:red; */ color: blue;", }, { desc: "simple remove", input: "a:b;c:d;e:f;", instruction: { type: "remove", name: "c", index: 1 }, expected: "a:b;e:f;", }, { desc: "disable with comment ender in string", input: "content: '*/';", instruction: { type: "enable", name: "content", value: false, index: 0 }, expected: "/*! content: '*\\/'; */", }, { desc: "enable with comment ender in string", input: "/* content: '*\\/'; */", instruction: { type: "enable", name: "content", value: true, index: 0 }, expected: "content: '*/';", }, { desc: "enable requiring semicolon insertion", // Note the lack of a trailing semicolon in the comment. input: "/* color:red */ color: blue;", instruction: { type: "enable", name: "color", value: true, index: 0 }, expected: "color:red; color: blue;", }, { desc: "create requiring semicolon insertion", // Note the lack of a trailing semicolon. input: "color: red", instruction: { type: "create", name: "a", value: "b", priority: "", index: 1, enabled: true, }, expected: "color: red;a: b;", }, // Newline insertion. { desc: "simple newline insertion", input: "\ncolor: red;\n", instruction: { type: "create", name: "a", value: "b", priority: "", index: 1, enabled: true, }, expected: "\ncolor: red;\na: b;\n", }, // Newline insertion. { desc: "semicolon insertion before newline", // Note the lack of a trailing semicolon. input: "\ncolor: red\n", instruction: { type: "create", name: "a", value: "b", priority: "", index: 1, enabled: true, }, expected: "\ncolor: red;\na: b;\n", }, // Newline insertion. { desc: "newline and semicolon insertion", // Note the lack of a trailing semicolon and newline. input: "\ncolor: red", instruction: { type: "create", name: "a", value: "b", priority: "", index: 1, enabled: true, }, expected: "\ncolor: red;\na: b;\n", }, // Newline insertion and indentation. { desc: "indentation with create", input: "\n color: red;\n", instruction: { type: "create", name: "a", value: "b", priority: "", index: 1, enabled: true, }, expected: "\n color: red;\n a: b;\n", }, // Newline insertion and indentation. { desc: "indentation plus semicolon insertion before newline", // Note the lack of a trailing semicolon. input: "\n color: red\n", instruction: { type: "create", name: "a", value: "b", priority: "", index: 1, enabled: true, }, expected: "\n color: red;\n a: b;\n", }, { desc: "indentation inserted before trailing whitespace", // Note the trailing whitespace. This could come from a rule // like: // @supports (mumble) { // body { // color: red; // } // } // Here if we create a rule we don't want it to follow // the indentation of the "}". input: "\n color: red;\n ", instruction: { type: "create", name: "a", value: "b", priority: "", index: 1, enabled: true, }, expected: "\n color: red;\n a: b;\n ", }, // Newline insertion and indentation. { desc: "indentation comes from preceding comment", // Note how the comment comes before the declaration. input: "\n /* comment */ color: red\n", instruction: { type: "create", name: "a", value: "b", priority: "", index: 1, enabled: true, }, expected: "\n /* comment */ color: red;\n a: b;\n", }, // Default indentation. { desc: "use of default indentation", input: "\n", instruction: { type: "create", name: "a", value: "b", priority: "", index: 0, enabled: true, }, expected: "\n\ta: b;\n", }, // Deletion handles newlines properly. { desc: "deletion removes newline", input: "a:b;\nc:d;\ne:f;", instruction: { type: "remove", name: "c", index: 1 }, expected: "a:b;\ne:f;", }, // Deletion handles newlines properly. { desc: "deletion remove blank line", input: "\n a:b;\n c:d; \ne:f;", instruction: { type: "remove", name: "c", index: 1 }, expected: "\n a:b;\ne:f;", }, // Deletion handles newlines properly. { desc: "deletion leaves comment", input: "\n a:b;\n /* something */ c:d; \ne:f;", instruction: { type: "remove", name: "c", index: 1 }, expected: "\n a:b;\n /* something */ \ne:f;", }, // Deletion handles newlines properly. { desc: "deletion leaves previous newline", input: "\n a:b;\n c:d; \ne:f;", instruction: { type: "remove", name: "e", index: 2 }, expected: "\n a:b;\n c:d; \n", }, // Deletion handles newlines properly. { desc: "deletion removes trailing whitespace", input: "\n a:b;\n c:d; \n e:f;", instruction: { type: "remove", name: "e", index: 2 }, expected: "\n a:b;\n c:d; \n", }, // Deletion handles newlines properly. { desc: "deletion preserves indentation", input: " a:b;\n c:d; \n e:f;", instruction: { type: "remove", name: "a", index: 0 }, expected: " c:d; \n e:f;", }, // Termination insertion corner case. { desc: "enable single quote termination", input: "/* content: 'hi */ color: red;", instruction: { type: "enable", name: "content", value: true, index: 0 }, expected: "content: 'hi'; color: red;", changed: { 0: "'hi'" }, }, // Termination insertion corner case. { desc: "create single quote termination", input: "content: 'hi", instruction: { type: "create", name: "color", value: "red", priority: "", index: 1, enabled: true, }, expected: "content: 'hi';color: red;", changed: { 0: "'hi'" }, }, // Termination insertion corner case. { desc: "enable double quote termination", input: '/* content: "hi */ color: red;', instruction: { type: "enable", name: "content", value: true, index: 0 }, expected: 'content: "hi"; color: red;', changed: { 0: '"hi"' }, }, // Termination insertion corner case. { desc: "create double quote termination", input: 'content: "hi', instruction: { type: "create", name: "color", value: "red", priority: "", index: 1, enabled: true, }, expected: 'content: "hi";color: red;', changed: { 0: '"hi"' }, }, // Termination insertion corner case. { desc: "enable url termination", input: "/* background-image: url(something.jpg */ color: red;", instruction: { type: "enable", name: "background-image", value: true, index: 0, }, expected: "background-image: url(something.jpg); color: red;", changed: { 0: "url(something.jpg)" }, }, // Termination insertion corner case. { desc: "create url termination", input: "background-image: url(something.jpg", instruction: { type: "create", name: "color", value: "red", priority: "", index: 1, enabled: true, }, expected: "background-image: url(something.jpg);color: red;", changed: { 0: "url(something.jpg)" }, }, // Termination insertion corner case. { desc: "enable url single quote termination", input: "/* background-image: url('something.jpg */ color: red;", instruction: { type: "enable", name: "background-image", value: true, index: 0, }, expected: "background-image: url('something.jpg'); color: red;", changed: { 0: "url('something.jpg')" }, }, // Termination insertion corner case. { desc: "create url single quote termination", input: "background-image: url('something.jpg", instruction: { type: "create", name: "color", value: "red", priority: "", index: 1, enabled: true, }, expected: "background-image: url('something.jpg');color: red;", changed: { 0: "url('something.jpg')" }, }, // Termination insertion corner case. { desc: "create url double quote termination", input: '/* background-image: url("something.jpg */ color: red;', instruction: { type: "enable", name: "background-image", value: true, index: 0, }, expected: 'background-image: url("something.jpg"); color: red;', changed: { 0: 'url("something.jpg")' }, }, // Termination insertion corner case. { desc: "enable url double quote termination", input: 'background-image: url("something.jpg', instruction: { type: "create", name: "color", value: "red", priority: "", index: 1, enabled: true, }, expected: 'background-image: url("something.jpg");color: red;', changed: { 0: 'url("something.jpg")' }, }, // Termination insertion corner case. { desc: "create backslash termination", input: "something: \\", instruction: { type: "create", name: "color", value: "red", priority: "", index: 1, enabled: true, }, expected: "something: \\\\;color: red;", // The lexer rewrites the token before we see it. However this is // so obscure as to be inconsequential. changed: { 0: "\uFFFD\\" }, }, // Termination insertion corner case. { desc: "enable backslash single quote termination", input: "something: '\\", instruction: { type: "create", name: "color", value: "red", priority: "", index: 1, enabled: true, }, expected: "something: '\\\\';color: red;", changed: { 0: "'\\\\'" }, }, { desc: "enable backslash double quote termination", input: 'something: "\\', instruction: { type: "create", name: "color", value: "red", priority: "", index: 1, enabled: true, }, expected: 'something: "\\\\";color: red;', changed: { 0: '"\\\\"' }, }, // Termination insertion corner case. { desc: "enable comment termination", input: "something: blah /* comment ", instruction: { type: "create", name: "color", value: "red", priority: "", index: 1, enabled: true, }, expected: "something: blah /* comment*/; color: red;", }, // Rewrite a "heuristic override" comment. { desc: "enable with heuristic override comment", input: "/*! walrus: zebra; */", instruction: { type: "enable", name: "walrus", value: true, index: 0 }, expected: "walrus: zebra;", }, // Sanitize a bad value. { desc: "create sanitize unpaired brace", input: "", instruction: { type: "create", name: "p", value: "}", priority: "", index: 0, enabled: true, }, expected: "p: \\};", changed: { 0: "\\}" }, }, // Sanitize a bad value. { desc: "set sanitize unpaired brace", input: "walrus: zebra;", instruction: { type: "set", name: "walrus", value: "{{}}}", priority: "", index: 0, }, expected: "walrus: {{}}\\};", changed: { 0: "{{}}\\}" }, }, // Sanitize a bad value. { desc: "enable sanitize unpaired brace", input: "/*! walrus: }*/", instruction: { type: "enable", name: "walrus", value: true, index: 0 }, expected: "walrus: \\};", changed: { 0: "\\}" }, }, // Creating a new declaration does not require an attempt to // terminate a previous commented declaration. { desc: "disabled declaration does not need semicolon insertion", input: "/*! no: semicolon */\n", instruction: { type: "create", name: "walrus", value: "zebra", priority: "", index: 1, enabled: true, }, expected: "/*! no: semicolon */\nwalrus: zebra;\n", changed: {}, }, { desc: "create commented-out property", input: "p: v", instruction: { type: "create", name: "shoveler", value: "duck", priority: "", index: 1, enabled: false, }, expected: "p: v;/*! shoveler: duck; */", }, { desc: "disabled create with comment ender in string", input: "", instruction: { type: "create", name: "content", value: "'*/'", priority: "", index: 0, enabled: false, }, expected: "/*! content: '*\\/'; */", }, { desc: "delete disabled property", input: "\n a:b;\n /* color:#f06; */\n e:f;", instruction: { type: "remove", name: "color", index: 1 }, expected: "\n a:b;\n e:f;", }, { desc: "delete heuristic-disabled property", input: "\n a:b;\n /*! c:d; */\n e:f;", instruction: { type: "remove", name: "c", index: 1 }, expected: "\n a:b;\n e:f;", }, { desc: "delete disabled property leaving other disabled property", input: "\n a:b;\n /* color:#f06; background-color: seagreen; */\n e:f;", instruction: { type: "remove", name: "color", index: 1 }, expected: "\n a:b;\n /* background-color: seagreen; */\n e:f;", }, { desc: "regression test for bug 1328016", input: "position:absolute;top50px;height:50px;width:50px;", instruction: { type: "set", name: "width", value: "60px", priority: "", index: 2, }, expected: "position:absolute;top50px;height:50px;width:60px;", }, { desc: "url regression test for bug 1321970", input: "", instruction: { type: "create", name: "p", value: "url(", priority: "", index: 0, enabled: true, }, expected: "p: url();", changed: { 0: "url()" }, }, { desc: "url semicolon regression test for bug 1321970", input: "", instruction: { type: "create", name: "p", value: "url(;", priority: "", index: 0, enabled: true, }, expected: "p: url();", changed: { 0: "url()" }, }, { desc: "basic regression test for bug 1321970", input: "", instruction: { type: "create", name: "p", value: "(", priority: "", index: 0, enabled: true, }, expected: "p: \\(;", changed: { 0: "\\(" }, }, { desc: "unbalanced regression test for bug 1321970", input: "", instruction: { type: "create", name: "p", value: "({[})", priority: "", index: 0, enabled: true, }, expected: "p: ({\\[});", changed: { 0: "({\\[})" }, }, { desc: "function regression test for bug 1321970", input: "", instruction: { type: "create", name: "p", value: "func(1,2)", priority: "", index: 0, enabled: true, }, expected: "p: func(1,2);", }, { desc: "function regression test for bug 1355233", input: "", instruction: { type: "create", name: "p", value: "func(", priority: "", index: 0, enabled: true, }, expected: "p: func\\(;", changed: { 0: "func\\(" }, }, ]; function rewriteDeclarations(inputString, instruction, defaultIndentation) { const rewriter = new RuleRewriter(isCssPropertyKnown, null, inputString); rewriter.defaultIndentation = defaultIndentation; switch (instruction.type) { case "rename": rewriter.renameProperty( instruction.index, instruction.name, instruction.newName ); break; case "enable": rewriter.setPropertyEnabled( instruction.index, instruction.name, instruction.value ); break; case "create": rewriter.createProperty( instruction.index, instruction.name, instruction.value, instruction.priority, instruction.enabled ); break; case "set": rewriter.setProperty( instruction.index, instruction.name, instruction.value, instruction.priority ); break; case "remove": rewriter.removeProperty(instruction.index, instruction.name); break; default: throw new Error("unrecognized instruction"); } return rewriter.getResult(); } function run_test() { for (const test of TEST_DATA) { const { changed, text } = rewriteDeclarations( test.input, test.instruction, "\t" ); equal(text, test.expected, "output for " + test.desc); let expectChanged; if ("changed" in test) { expectChanged = test.changed; } else { expectChanged = {}; } deepEqual(changed, expectChanged, "changed result for " + test.desc); } }