summaryrefslogtreecommitdiffstats
path: root/devtools/shared/webconsole/test/xpcshell
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/shared/webconsole/test/xpcshell')
-rw-r--r--devtools/shared/webconsole/test/xpcshell/.eslintrc.js6
-rw-r--r--devtools/shared/webconsole/test/xpcshell/head.js10
-rw-r--r--devtools/shared/webconsole/test/xpcshell/test_analyze_input_string.js225
-rw-r--r--devtools/shared/webconsole/test/xpcshell/test_js_property_provider.js719
-rw-r--r--devtools/shared/webconsole/test/xpcshell/xpcshell.ini9
5 files changed, 969 insertions, 0 deletions
diff --git a/devtools/shared/webconsole/test/xpcshell/.eslintrc.js b/devtools/shared/webconsole/test/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..8611c174f5
--- /dev/null
+++ b/devtools/shared/webconsole/test/xpcshell/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the common devtools xpcshell eslintrc config.
+ extends: "../../../../.eslintrc.xpcshell.js",
+};
diff --git a/devtools/shared/webconsole/test/xpcshell/head.js b/devtools/shared/webconsole/test/xpcshell/head.js
new file mode 100644
index 0000000000..e65552771e
--- /dev/null
+++ b/devtools/shared/webconsole/test/xpcshell/head.js
@@ -0,0 +1,10 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* exported require */
+
+"use strict";
+
+var { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+);
diff --git a/devtools/shared/webconsole/test/xpcshell/test_analyze_input_string.js b/devtools/shared/webconsole/test/xpcshell/test_analyze_input_string.js
new file mode 100644
index 0000000000..3df015056f
--- /dev/null
+++ b/devtools/shared/webconsole/test/xpcshell/test_analyze_input_string.js
@@ -0,0 +1,225 @@
+// Any copyright is dedicated to the Public Domain.
+// http://creativecommons.org/publicdomain/zero/1.0/
+
+"use strict";
+const {
+ analyzeInputString,
+} = require("resource://devtools/shared/webconsole/analyze-input-string.js");
+
+add_task(() => {
+ const tests = [
+ {
+ desc: "simple property access",
+ input: `var a = {b: 1};a.b`,
+ expected: {
+ isElementAccess: false,
+ isPropertyAccess: true,
+ expressionBeforePropertyAccess: `var a = {b: 1};a`,
+ lastStatement: "a.b",
+ mainExpression: `a`,
+ matchProp: `b`,
+ },
+ },
+ {
+ desc: "deep property access",
+ input: `a.b.c`,
+ expected: {
+ isElementAccess: false,
+ isPropertyAccess: true,
+ expressionBeforePropertyAccess: `a.b`,
+ lastStatement: "a.b.c",
+ mainExpression: `a.b`,
+ matchProp: `c`,
+ },
+ },
+ {
+ desc: "element access",
+ input: `a["b`,
+ expected: {
+ isElementAccess: true,
+ isPropertyAccess: true,
+ expressionBeforePropertyAccess: `a`,
+ lastStatement: `a["b`,
+ mainExpression: `a`,
+ matchProp: `"b`,
+ },
+ },
+ {
+ desc: "element access without quotes",
+ input: `a[b`,
+ expected: {
+ isElementAccess: true,
+ isPropertyAccess: true,
+ expressionBeforePropertyAccess: `a`,
+ lastStatement: `a[b`,
+ mainExpression: `a`,
+ matchProp: `b`,
+ },
+ },
+ {
+ desc: "simple optional chaining access",
+ input: `a?.b`,
+ expected: {
+ isElementAccess: false,
+ isPropertyAccess: true,
+ expressionBeforePropertyAccess: `a`,
+ lastStatement: `a?.b`,
+ mainExpression: `a`,
+ matchProp: `b`,
+ },
+ },
+ {
+ desc: "deep optional chaining access",
+ input: `a?.b?.c`,
+ expected: {
+ isElementAccess: false,
+ isPropertyAccess: true,
+ expressionBeforePropertyAccess: `a?.b`,
+ lastStatement: `a?.b?.c`,
+ mainExpression: `a?.b`,
+ matchProp: `c`,
+ },
+ },
+ {
+ desc: "optional chaining element access",
+ input: `a?.["b`,
+ expected: {
+ isElementAccess: true,
+ isPropertyAccess: true,
+ expressionBeforePropertyAccess: `a`,
+ lastStatement: `a?.["b`,
+ mainExpression: `a`,
+ matchProp: `"b`,
+ },
+ },
+ {
+ desc: "optional chaining element access without quotes",
+ input: `a?.[b`,
+ expected: {
+ isElementAccess: true,
+ isPropertyAccess: true,
+ expressionBeforePropertyAccess: `a`,
+ lastStatement: `a?.[b`,
+ mainExpression: `a`,
+ matchProp: `b`,
+ },
+ },
+ {
+ desc: "deep optional chaining element access with quotes",
+ input: `var a = {b: 1, c: ["."]}; a?.["b"]?.c?.["d[.`,
+ expected: {
+ isElementAccess: true,
+ isPropertyAccess: true,
+ expressionBeforePropertyAccess: `var a = {b: 1, c: ["."]}; a?.["b"]?.c`,
+ lastStatement: `a?.["b"]?.c?.["d[.`,
+ mainExpression: `a?.["b"]?.c`,
+ matchProp: `"d[.`,
+ },
+ },
+ {
+ desc: "literal arrays with newline",
+ input: `[1,2,3,\n4\n].`,
+ expected: {
+ isElementAccess: false,
+ isPropertyAccess: true,
+ expressionBeforePropertyAccess: `[1,2,3,\n4\n]`,
+ lastStatement: `[1,2,3,4].`,
+ mainExpression: `[1,2,3,4]`,
+ matchProp: ``,
+ },
+ },
+ {
+ desc: "number literal with newline",
+ input: `1\n.`,
+ expected: {
+ isElementAccess: false,
+ isPropertyAccess: true,
+ expressionBeforePropertyAccess: `1\n`,
+ lastStatement: `1\n.`,
+ mainExpression: `1`,
+ matchProp: ``,
+ },
+ },
+ {
+ desc: "string literal",
+ input: `"abc".`,
+ expected: {
+ isElementAccess: false,
+ isPropertyAccess: true,
+ expressionBeforePropertyAccess: `"abc"`,
+ lastStatement: `"abc".`,
+ mainExpression: `"abc"`,
+ matchProp: ``,
+ },
+ },
+ {
+ desc: "string literal containing backslash",
+ input: `"\\n".`,
+ expected: {
+ isElementAccess: false,
+ isPropertyAccess: true,
+ expressionBeforePropertyAccess: `"\\n"`,
+ lastStatement: `"\\n".`,
+ mainExpression: `"\\n"`,
+ matchProp: ``,
+ },
+ },
+ {
+ desc: "single quote string literal containing backslash",
+ input: `'\\r'.`,
+ expected: {
+ isElementAccess: false,
+ isPropertyAccess: true,
+ expressionBeforePropertyAccess: `'\\r'`,
+ lastStatement: `'\\r'.`,
+ mainExpression: `'\\r'`,
+ matchProp: ``,
+ },
+ },
+ {
+ desc: "template string literal containing backslash",
+ input: "`\\\\`.",
+ expected: {
+ isElementAccess: false,
+ isPropertyAccess: true,
+ expressionBeforePropertyAccess: "`\\\\`",
+ lastStatement: "`\\\\`.",
+ mainExpression: "`\\\\`",
+ matchProp: ``,
+ },
+ },
+ {
+ desc: "unterminated double quote string literal",
+ input: `"\n`,
+ expected: {
+ err: "unterminated string literal",
+ },
+ },
+ {
+ desc: "unterminated single quote string literal",
+ input: `'\n`,
+ expected: {
+ err: "unterminated string literal",
+ },
+ },
+ {
+ desc: "optional chaining operator with spaces",
+ input: `test ?. ["propA"] ?. [0] ?. ["propB"] ?. ['to`,
+ expected: {
+ isElementAccess: true,
+ isPropertyAccess: true,
+ expressionBeforePropertyAccess: `test ?. ["propA"] ?. [0] ?. ["propB"] `,
+ lastStatement: `test ?. ["propA"] ?. [0] ?. ["propB"] ?. ['to`,
+ mainExpression: `test ?. ["propA"] ?. [0] ?. ["propB"]`,
+ matchProp: `'to`,
+ },
+ },
+ ];
+
+ for (const { input, desc, expected } of tests) {
+ const result = analyzeInputString(input);
+ for (const [key, value] of Object.entries(expected)) {
+ Assert.equal(value, result[key], `${desc} | ${key} has expected value`);
+ }
+ }
+});
diff --git a/devtools/shared/webconsole/test/xpcshell/test_js_property_provider.js b/devtools/shared/webconsole/test/xpcshell/test_js_property_provider.js
new file mode 100644
index 0000000000..984bbd5747
--- /dev/null
+++ b/devtools/shared/webconsole/test/xpcshell/test_js_property_provider.js
@@ -0,0 +1,719 @@
+// Any copyright is dedicated to the Public Domain.
+// http://creativecommons.org/publicdomain/zero/1.0/
+
+"use strict";
+const {
+ FallibleJSPropertyProvider: JSPropertyProvider,
+} = require("resource://devtools/shared/webconsole/js-property-provider.js");
+
+const { addDebuggerToGlobal } = ChromeUtils.importESModule(
+ "resource://gre/modules/jsdebugger.sys.mjs"
+);
+addDebuggerToGlobal(globalThis);
+
+function run_test() {
+ Services.prefs.setBoolPref(
+ "security.allow_parent_unrestricted_js_loads",
+ true
+ );
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("security.allow_parent_unrestricted_js_loads");
+ });
+
+ const testArray = `var testArray = [
+ {propA: "A"},
+ {
+ propB: "B",
+ propC: [
+ "D"
+ ]
+ },
+ [
+ {propE: "E"}
+ ]
+ ]`;
+
+ const testObject = 'var testObject = {"propA": [{"propB": "B"}]}';
+ const testHyphenated = 'var testHyphenated = {"prop-A": "res-A"}';
+ const testLet = "let foobar = {a: ''}; const blargh = {a: 1};";
+
+ const testGenerators = `
+ // Test with generator using a named function.
+ function* genFunc() {
+ for (let i = 0; i < 10; i++) {
+ yield i;
+ }
+ }
+ let gen1 = genFunc();
+ gen1.next();
+
+ // Test with generator using an anonymous function.
+ let gen2 = (function* () {
+ for (let i = 0; i < 10; i++) {
+ yield i;
+ }
+ })();`;
+
+ const testGetters = `
+ var testGetters = {
+ get x() {
+ return Object.create(null, Object.getOwnPropertyDescriptors({
+ hello: "",
+ world: "",
+ }));
+ },
+ get y() {
+ return Object.create(null, Object.getOwnPropertyDescriptors({
+ get y() {
+ return "plop";
+ },
+ }));
+ }
+ };
+ `;
+
+ const sandbox = Cu.Sandbox("http://example.com");
+ const dbg = new Debugger();
+ const dbgObject = dbg.addDebuggee(sandbox);
+ const dbgEnv = dbgObject.asEnvironment();
+ Cu.evalInSandbox(
+ `
+ const hello = Object.create(null, Object.getOwnPropertyDescriptors({world: 1}));
+ String.prototype.hello = hello;
+ Number.prototype.hello = hello;
+ Array.prototype.hello = hello;
+ `,
+ sandbox
+ );
+ Cu.evalInSandbox(testArray, sandbox);
+ Cu.evalInSandbox(testObject, sandbox);
+ Cu.evalInSandbox(testHyphenated, sandbox);
+ Cu.evalInSandbox(testLet, sandbox);
+ Cu.evalInSandbox(testGenerators, sandbox);
+ Cu.evalInSandbox(testGetters, sandbox);
+
+ info("Running tests with dbgObject");
+ runChecks(dbgObject, null, sandbox);
+
+ info("Running tests with dbgEnv");
+ runChecks(null, dbgEnv, sandbox);
+}
+
+function runChecks(dbgObject, environment, sandbox) {
+ const propertyProvider = (inputValue, options) =>
+ JSPropertyProvider({
+ dbgObject,
+ environment,
+ inputValue,
+ ...options,
+ });
+
+ info("Test that suggestions are given for 'this'");
+ let results = propertyProvider("t");
+ test_has_result(results, "this");
+
+ if (dbgObject != null) {
+ info("Test that suggestions are given for 'this.'");
+ results = propertyProvider("this.");
+ test_has_result(results, "testObject");
+
+ info("Test that suggestions are given for '(this).'");
+ results = propertyProvider("(this).");
+ test_has_result(results, "testObject");
+
+ info("Test that suggestions are given for deep 'this' properties access");
+ results = propertyProvider("(this).testObject.propA.");
+ test_has_result(results, "shift");
+
+ results = propertyProvider("(this).testObject.propA[");
+ test_has_result(results, `"shift"`);
+
+ results = propertyProvider("(this)['testObject']['propA'][");
+ test_has_result(results, `"shift"`);
+
+ results = propertyProvider("(this).testObject['propA'].");
+ test_has_result(results, "shift");
+
+ info("Test that no suggestions are given for 'this.this'");
+ results = propertyProvider("this.this");
+ test_has_no_results(results);
+ }
+
+ info("Test that suggestions are given for 'globalThis'");
+ results = propertyProvider("g");
+ test_has_result(results, "globalThis");
+
+ info("Test that suggestions are given for 'globalThis.'");
+ results = propertyProvider("globalThis.");
+ test_has_result(results, "testObject");
+
+ info("Test that suggestions are given for '(globalThis).'");
+ results = propertyProvider("(globalThis).");
+ test_has_result(results, "testObject");
+
+ info(
+ "Test that suggestions are given for deep 'globalThis' properties access"
+ );
+ results = propertyProvider("(globalThis).testObject.propA.");
+ test_has_result(results, "shift");
+
+ results = propertyProvider("(globalThis).testObject.propA[");
+ test_has_result(results, `"shift"`);
+
+ results = propertyProvider("(globalThis)['testObject']['propA'][");
+ test_has_result(results, `"shift"`);
+
+ results = propertyProvider("(globalThis).testObject['propA'].");
+ test_has_result(results, "shift");
+
+ info("Testing lexical scope issues (Bug 1207868)");
+ results = propertyProvider("foobar");
+ test_has_result(results, "foobar");
+
+ results = propertyProvider("foobar.");
+ test_has_result(results, "a");
+
+ results = propertyProvider("blargh");
+ test_has_result(results, "blargh");
+
+ results = propertyProvider("blargh.");
+ test_has_result(results, "a");
+
+ info("Test that suggestions are given for 'foo[n]' where n is an integer.");
+ results = propertyProvider("testArray[0].");
+ test_has_result(results, "propA");
+
+ info("Test that suggestions are given for multidimensional arrays.");
+ results = propertyProvider("testArray[2][0].");
+ test_has_result(results, "propE");
+
+ info("Test that suggestions are given for nested arrays.");
+ results = propertyProvider("testArray[1].propC[0].");
+ test_has_result(results, "indexOf");
+
+ info("Test that suggestions are given for literal arrays.");
+ results = propertyProvider("[1,2,3].");
+ test_has_result(results, "indexOf");
+
+ results = propertyProvider("[1,2,3].h");
+ test_has_result(results, "hello");
+
+ results = propertyProvider("[1,2,3].hello.w");
+ test_has_result(results, "world");
+
+ info("Test that suggestions are given for literal arrays with newlines.");
+ results = propertyProvider("[1,2,3,\n4\n].");
+ test_has_result(results, "indexOf");
+
+ info("Test that suggestions are given for literal strings.");
+ results = propertyProvider("'foo'.");
+ test_has_result(results, "charAt");
+ results = propertyProvider('"foo".');
+ test_has_result(results, "charAt");
+ results = propertyProvider("`foo`.");
+ test_has_result(results, "charAt");
+ results = propertyProvider("`foo doc`.");
+ test_has_result(results, "charAt");
+ results = propertyProvider('`foo " doc`.');
+ test_has_result(results, "charAt");
+ results = propertyProvider("`foo ' doc`.");
+ test_has_result(results, "charAt");
+ results = propertyProvider("'[1,2,3]'.");
+ test_has_result(results, "charAt");
+ results = propertyProvider("'foo'.h");
+ test_has_result(results, "hello");
+ results = propertyProvider("'foo'.hello.w");
+ test_has_result(results, "world");
+ results = propertyProvider(`"\\n".`);
+ test_has_result(results, "charAt");
+ results = propertyProvider(`'\\r'.`);
+ test_has_result(results, "charAt");
+ results = propertyProvider("`\\\\`.");
+ test_has_result(results, "charAt");
+
+ info("Test that suggestions are not given for syntax errors.");
+ results = propertyProvider("'foo\"");
+ Assert.equal(null, results);
+ results = propertyProvider("'foo d");
+ Assert.equal(null, results);
+ results = propertyProvider(`"foo d`);
+ Assert.equal(null, results);
+ results = propertyProvider("`foo d");
+ Assert.equal(null, results);
+ results = propertyProvider("[1,',2]");
+ Assert.equal(null, results);
+ results = propertyProvider("'[1,2].");
+ Assert.equal(null, results);
+ results = propertyProvider("'foo'..");
+ Assert.equal(null, results);
+
+ info("Test that suggestions are not given without a dot.");
+ results = propertyProvider("'foo'");
+ test_has_no_results(results);
+ results = propertyProvider("`foo`");
+ test_has_no_results(results);
+ results = propertyProvider("[1,2,3]");
+ test_has_no_results(results);
+ results = propertyProvider("[1,2,3].\n'foo'");
+ test_has_no_results(results);
+
+ info("Test that suggestions are not given for index that's out of bounds.");
+ results = propertyProvider("testArray[10].");
+ Assert.equal(null, results);
+
+ info("Test that invalid element access syntax does not return anything");
+ results = propertyProvider("testArray[][1].");
+ Assert.equal(null, results);
+
+ info("Test that deep element access works.");
+ results = propertyProvider("testObject['propA'][0].");
+ test_has_result(results, "propB");
+
+ results = propertyProvider("testArray[1]['propC'].");
+ test_has_result(results, "shift");
+
+ results = propertyProvider("testArray[1].propC[0][");
+ test_has_result(results, `"trim"`);
+
+ results = propertyProvider("testArray[1].propC[0].");
+ test_has_result(results, "trim");
+
+ info(
+ "Test that suggestions are displayed when variable is wrapped in parens"
+ );
+ results = propertyProvider("(testObject)['propA'][0].");
+ test_has_result(results, "propB");
+
+ results = propertyProvider("(testArray)[1]['propC'].");
+ test_has_result(results, "shift");
+
+ results = propertyProvider("(testArray)[1].propC[0][");
+ test_has_result(results, `"trim"`);
+
+ results = propertyProvider("(testArray)[1].propC[0].");
+ test_has_result(results, "trim");
+
+ info("Test that suggestions are given if there is an hyphen in the chain.");
+ results = propertyProvider("testHyphenated['prop-A'].");
+ test_has_result(results, "trim");
+
+ info("Test that we have suggestions for generators.");
+ const gen1Result = Cu.evalInSandbox("gen1.next().value", sandbox);
+ results = propertyProvider("gen1.");
+ test_has_result(results, "next");
+ info("Test that the generator next() was not executed");
+ const gen1NextResult = Cu.evalInSandbox("gen1.next().value", sandbox);
+ Assert.equal(gen1Result + 1, gen1NextResult);
+
+ info("Test with an anonymous generator.");
+ const gen2Result = Cu.evalInSandbox("gen2.next().value", sandbox);
+ results = propertyProvider("gen2.");
+ test_has_result(results, "next");
+ const gen2NextResult = Cu.evalInSandbox("gen2.next().value", sandbox);
+ Assert.equal(gen2Result + 1, gen2NextResult);
+
+ info(
+ "Test that getters are not executed if authorizedEvaluations is undefined"
+ );
+ results = propertyProvider("testGetters.x.");
+ Assert.deepEqual(results, {
+ isUnsafeGetter: true,
+ getterPath: ["testGetters", "x"],
+ });
+
+ results = propertyProvider("testGetters.x[");
+ Assert.deepEqual(results, {
+ isUnsafeGetter: true,
+ getterPath: ["testGetters", "x"],
+ });
+
+ results = propertyProvider("testGetters.x.hell");
+ Assert.deepEqual(results, {
+ isUnsafeGetter: true,
+ getterPath: ["testGetters", "x"],
+ });
+
+ results = propertyProvider("testGetters.x['hell");
+ Assert.deepEqual(results, {
+ isUnsafeGetter: true,
+ getterPath: ["testGetters", "x"],
+ });
+
+ info(
+ "Test that getters are not executed if authorizedEvaluations does not match"
+ );
+ results = propertyProvider("testGetters.x.", { authorizedEvaluations: [] });
+ Assert.deepEqual(results, {
+ isUnsafeGetter: true,
+ getterPath: ["testGetters", "x"],
+ });
+
+ results = propertyProvider("testGetters.x.", {
+ authorizedEvaluations: [["testGetters"]],
+ });
+ Assert.deepEqual(results, {
+ isUnsafeGetter: true,
+ getterPath: ["testGetters", "x"],
+ });
+
+ results = propertyProvider("testGetters.x.", {
+ authorizedEvaluations: [["testGtrs", "x"]],
+ });
+ Assert.deepEqual(results, {
+ isUnsafeGetter: true,
+ getterPath: ["testGetters", "x"],
+ });
+
+ results = propertyProvider("testGetters.x.", {
+ authorizedEvaluations: [["x"]],
+ });
+ Assert.deepEqual(results, {
+ isUnsafeGetter: true,
+ getterPath: ["testGetters", "x"],
+ });
+
+ info("Test that deep getter property access returns intermediate getters");
+ results = propertyProvider("testGetters.y.y.");
+ Assert.deepEqual(results, {
+ isUnsafeGetter: true,
+ getterPath: ["testGetters", "y"],
+ });
+
+ results = propertyProvider("testGetters['y'].y.");
+ Assert.deepEqual(results, {
+ isUnsafeGetter: true,
+ getterPath: ["testGetters", "y"],
+ });
+
+ results = propertyProvider("testGetters['y']['y'].");
+ Assert.deepEqual(results, {
+ isUnsafeGetter: true,
+ getterPath: ["testGetters", "y"],
+ });
+
+ results = propertyProvider("testGetters.y['y'].");
+ Assert.deepEqual(results, {
+ isUnsafeGetter: true,
+ getterPath: ["testGetters", "y"],
+ });
+
+ info("Test that deep getter property access invoke intermediate getters");
+ results = propertyProvider("testGetters.y.y.", {
+ authorizedEvaluations: [["testGetters", "y"]],
+ });
+ Assert.deepEqual(results, {
+ isUnsafeGetter: true,
+ getterPath: ["testGetters", "y", "y"],
+ });
+
+ results = propertyProvider("testGetters['y'].y.", {
+ authorizedEvaluations: [["testGetters", "y"]],
+ });
+ Assert.deepEqual(results, {
+ isUnsafeGetter: true,
+ getterPath: ["testGetters", "y", "y"],
+ });
+
+ results = propertyProvider("testGetters['y']['y'].", {
+ authorizedEvaluations: [["testGetters", "y"]],
+ });
+ Assert.deepEqual(results, {
+ isUnsafeGetter: true,
+ getterPath: ["testGetters", "y", "y"],
+ });
+
+ results = propertyProvider("testGetters.y['y'].", {
+ authorizedEvaluations: [["testGetters", "y"]],
+ });
+ Assert.deepEqual(results, {
+ isUnsafeGetter: true,
+ getterPath: ["testGetters", "y", "y"],
+ });
+
+ info(
+ "Test that getters are executed if matching an authorizedEvaluation element"
+ );
+ results = propertyProvider("testGetters.x.", {
+ authorizedEvaluations: [["testGetters", "x"]],
+ });
+ test_has_exact_results(results, ["hello", "world"]);
+ Assert.ok(Object.keys(results).includes("isUnsafeGetter") === false);
+ Assert.ok(Object.keys(results).includes("getterPath") === false);
+
+ results = propertyProvider("testGetters.x.", {
+ authorizedEvaluations: [["testGetters", "x"], ["y"]],
+ });
+ test_has_exact_results(results, ["hello", "world"]);
+ Assert.ok(Object.keys(results).includes("isUnsafeGetter") === false);
+ Assert.ok(Object.keys(results).includes("getterPath") === false);
+
+ info("Test that executing getters filters with provided string");
+ results = propertyProvider("testGetters.x.hell", {
+ authorizedEvaluations: [["testGetters", "x"]],
+ });
+ test_has_exact_results(results, ["hello"]);
+
+ results = propertyProvider("testGetters.x['hell", {
+ authorizedEvaluations: [["testGetters", "x"]],
+ });
+ test_has_exact_results(results, ["'hello'"]);
+
+ info(
+ "Test children getters are not executed if not included in authorizedEvaluation"
+ );
+ results = propertyProvider("testGetters.y.y.", {
+ authorizedEvaluations: [["testGetters", "y", "y"]],
+ });
+ Assert.deepEqual(results, {
+ isUnsafeGetter: true,
+ getterPath: ["testGetters", "y"],
+ });
+
+ info(
+ "Test children getters are executed if matching an authorizedEvaluation element"
+ );
+ results = propertyProvider("testGetters.y.y.", {
+ authorizedEvaluations: [
+ ["testGetters", "y"],
+ ["testGetters", "y", "y"],
+ ],
+ });
+ test_has_result(results, "trim");
+
+ info("Test with number literals");
+ results = propertyProvider("1.");
+ Assert.ok(results === null, "Does not complete on possible floating number");
+
+ results = propertyProvider("(1)..");
+ Assert.ok(results === null, "Does not complete on invalid syntax");
+
+ results = propertyProvider("(1.1.).");
+ Assert.ok(results === null, "Does not complete on invalid syntax");
+
+ results = propertyProvider("1..");
+ test_has_result(results, "toFixed");
+
+ results = propertyProvider("1 .");
+ test_has_result(results, "toFixed");
+
+ results = propertyProvider("1\n.");
+ test_has_result(results, "toFixed");
+
+ results = propertyProvider(".1.");
+ test_has_result(results, "toFixed");
+
+ results = propertyProvider("1[");
+ test_has_result(results, `"toFixed"`);
+
+ results = propertyProvider("1[toFixed");
+ test_has_exact_results(results, [`"toFixed"`]);
+
+ results = propertyProvider("1['toFixed");
+ test_has_exact_results(results, ["'toFixed'"]);
+
+ results = propertyProvider("1.1[");
+ test_has_result(results, `"toFixed"`);
+
+ results = propertyProvider("(1).");
+ test_has_result(results, "toFixed");
+
+ results = propertyProvider("(.1).");
+ test_has_result(results, "toFixed");
+
+ results = propertyProvider("(1.1).");
+ test_has_result(results, "toFixed");
+
+ results = propertyProvider("(1).toFixed");
+ test_has_exact_results(results, ["toFixed"]);
+
+ results = propertyProvider("(1)[");
+ test_has_result(results, `"toFixed"`);
+
+ results = propertyProvider("(1.1)[");
+ test_has_result(results, `"toFixed"`);
+
+ results = propertyProvider("(1)[toFixed");
+ test_has_exact_results(results, [`"toFixed"`]);
+
+ results = propertyProvider("(1)['toFixed");
+ test_has_exact_results(results, ["'toFixed'"]);
+
+ results = propertyProvider("(1).h");
+ test_has_result(results, "hello");
+
+ results = propertyProvider("(1).hello.w");
+ test_has_result(results, "world");
+
+ info("Test access on dot-notation invalid property name");
+ results = propertyProvider("testHyphenated.prop");
+ Assert.ok(
+ !results.matches.has("prop-A"),
+ "Does not return invalid property name on dot access"
+ );
+
+ results = propertyProvider("testHyphenated['prop");
+ test_has_result(results, `'prop-A'`);
+
+ results = propertyProvider(`//t`);
+ Assert.ok(results === null, "Does not complete in inline comment");
+
+ results = propertyProvider(`// t`);
+ Assert.ok(
+ results === null,
+ "Does not complete in inline comment after space"
+ );
+
+ results = propertyProvider(`//I'm a comment\nt`);
+ test_has_result(results, "testObject");
+
+ results = propertyProvider(`1/t`);
+ test_has_result(results, "testObject");
+
+ results = propertyProvider(`/* t`);
+ Assert.ok(results === null, "Does not complete in multiline comment");
+
+ results = propertyProvider(`/*I'm\nt`);
+ Assert.ok(
+ results === null,
+ "Does not complete in multiline comment after line break"
+ );
+
+ results = propertyProvider(`/*I'm a comment\n \t * /t`);
+ Assert.ok(
+ results === null,
+ "Does not complete in multiline comment after line break and invalid comment end"
+ );
+
+ results = propertyProvider(`/*I'm a comment\n \t */t`);
+ test_has_result(results, "testObject");
+
+ results = propertyProvider(`/*I'm a comment\n \t */\n\nt`);
+ test_has_result(results, "testObject");
+
+ info("Test local expression variables");
+ results = propertyProvider("b", { expressionVars: ["a", "b", "c"] });
+ test_has_result(results, "b");
+ Assert.equal(results.matches.has("a"), false);
+ Assert.equal(results.matches.has("c"), false);
+
+ info(
+ "Test that local expression variables are not included when accessing an object properties"
+ );
+ results = propertyProvider("testObject.prop", {
+ expressionVars: ["propLocal"],
+ });
+ Assert.equal(results.matches.has("propLocal"), false);
+ test_has_result(results, "propA");
+
+ results = propertyProvider("testObject['prop", {
+ expressionVars: ["propLocal"],
+ });
+ test_has_result(results, "'propA'");
+ Assert.equal(results.matches.has("propLocal"), false);
+
+ info("Test that expression with optional chaining operator are completed");
+ results = propertyProvider("testObject?.prop");
+ test_has_result(results, "propA");
+
+ results = propertyProvider("testObject?.propA[0]?.propB?.to");
+ test_has_result(results, "toString");
+
+ results = propertyProvider("testObject?.propA?.[0]?.propB?.to");
+ test_has_result(results, "toString");
+
+ results = propertyProvider(
+ "testObject ?. propA[0] ?. propB ?. to"
+ );
+ test_has_result(results, "toString");
+
+ results = propertyProvider("testObject?.[prop");
+ test_has_result(results, '"propA"');
+
+ results = propertyProvider(`testObject?.["prop`);
+ test_has_result(results, '"propA"');
+
+ results = propertyProvider(`testObject?.['prop`);
+ test_has_result(results, `'propA'`);
+
+ results = propertyProvider(`testObject?.["propA"]?.[0]?.["propB"]?.["to`);
+ test_has_result(results, `"toString"`);
+
+ results = propertyProvider(
+ `testObject ?. ["propA"] ?. [0] ?. ["propB"] ?. ['to`
+ );
+ test_has_result(results, "'toString'");
+
+ results = propertyProvider("[1,2,3]?.");
+ test_has_result(results, "indexOf");
+
+ results = propertyProvider("'foo'?.");
+ test_has_result(results, "charAt");
+
+ results = propertyProvider("1?.");
+ test_has_result(results, "toFixed");
+
+ // check this doesn't throw since `propC` is not defined.
+ results = propertyProvider("testObject?.propC?.this?.does?.not?.exist?.d");
+
+ // check that ternary operator isn't mistaken for optional chaining
+ results = propertyProvider(`true?.3.to`);
+ test_has_result(results, `toExponential`);
+
+ results = propertyProvider(`true?.3?.to`);
+ test_has_result(results, `toExponential`);
+
+ // Test more ternary
+ results = propertyProvider(`true?t`);
+ test_has_result(results, `testObject`);
+
+ results = propertyProvider(`true??t`);
+ test_has_result(results, `testObject`);
+
+ results = propertyProvider(`true?/* comment */t`);
+ test_has_result(results, `testObject`);
+
+ results = propertyProvider(`true?<t`);
+ test_has_no_results(results);
+
+ // Test autocompletion on debugger statement does not throw
+ results = propertyProvider(`debugger.`);
+ Assert.ok(results === null, "Does not complete a debugger keyword");
+}
+
+/**
+ * A helper that ensures an empty array of results were found.
+ * @param Object results
+ * The results returned by JSPropertyProvider.
+ */
+function test_has_no_results(results) {
+ Assert.notEqual(results, null);
+ Assert.equal(results.matches.size, 0);
+}
+/**
+ * A helper that ensures (required) results were found.
+ * @param Object results
+ * The results returned by JSPropertyProvider.
+ * @param String requiredSuggestion
+ * A suggestion that must be found from the results.
+ */
+function test_has_result(results, requiredSuggestion) {
+ Assert.notEqual(results, null);
+ Assert.ok(results.matches.size > 0);
+ Assert.ok(
+ results.matches.has(requiredSuggestion),
+ `<${requiredSuggestion}> found in ${[...results.matches.values()].join(
+ " - "
+ )}`
+ );
+}
+
+/**
+ * A helper that ensures results are the expected ones.
+ * @param Object results
+ * The results returned by JSPropertyProvider.
+ * @param Array expectedMatches
+ * An array of the properties that should be returned by JsPropertyProvider.
+ */
+function test_has_exact_results(results, expectedMatches) {
+ Assert.deepEqual([...results.matches], expectedMatches);
+}
diff --git a/devtools/shared/webconsole/test/xpcshell/xpcshell.ini b/devtools/shared/webconsole/test/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..a0d7e75ad1
--- /dev/null
+++ b/devtools/shared/webconsole/test/xpcshell/xpcshell.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+tags = devtools
+head = head.js
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+support-files =
+
+[test_analyze_input_string.js]
+[test_js_property_provider.js]