Files
sousa-gecko/devtools/shared/webconsole/test/xpcshell/test_js_property_provider.js
T
Nicolas Chevobbe 4f13eaf338 Bug 2004579 - [devtools] Compute string index from character length in analyzeInputString. r=devtools-reviewers,bomsy.
We were only increasing the index by 1, which wouldn't account for surrogate pairs (e.g. emojis).
We need to increase the index by the character length so the string can be manipulated accurately
after.
Test case is added to cover the fix.

Differential Revision: https://phabricator.services.mozilla.com/D275419
2025-12-09 08:39:36 +00:00

767 lines
23 KiB
JavaScript

// 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 testProxies = `
var testSelfPrototypeProxy = new Proxy({
hello: 1
}, {
getPrototypeOf: () => testProxy
});
var testArrayPrototypeProxy = new Proxy({
world: 2
}, {
getPrototypeOf: () => Array.prototype
})
`;
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);
Cu.evalInSandbox(testProxies, 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");
test_has_result(results, "Infinity");
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");
results = propertyProvider(`"🧑".c`);
test_has_result(results, "codePointAt");
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.strictEqual(Object.keys(results).includes("isUnsafeGetter"), false);
Assert.strictEqual(Object.keys(results).includes("getterPath"), false);
results = propertyProvider("testGetters.x.", {
authorizedEvaluations: [["testGetters", "x"], ["y"]],
});
test_has_exact_results(results, ["hello", "world"]);
Assert.strictEqual(Object.keys(results).includes("isUnsafeGetter"), false);
Assert.strictEqual(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.strictEqual(
results,
null,
"Does not complete on possible floating number"
);
results = propertyProvider("(1)..");
Assert.strictEqual(results, null, "Does not complete on invalid syntax");
results = propertyProvider("(1.1.).");
Assert.strictEqual(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.strictEqual(results, null, "Does not complete in inline comment");
results = propertyProvider(`// t`);
Assert.strictEqual(
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(`1/(t`);
test_has_result(results, "testObject");
results = propertyProvider(`/* t`);
Assert.strictEqual(results, null, "Does not complete in multiline comment");
results = propertyProvider(`/*I'm\nt`);
Assert.strictEqual(
results,
null,
"Does not complete in multiline comment after line break"
);
results = propertyProvider(`/*I'm a comment\n \t * /t`);
Assert.strictEqual(
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.strictEqual(results, null, "Does not complete a debugger keyword");
// Test autocompletion on Proxies
// proxy does not get autocompletion result from prototype defined in `getPrototypeOf`
test_has_no_results(propertyProvider(`testArrayPrototypeProxy.filte`));
results = propertyProvider(`testArrayPrototypeProxy.`);
// it does get the own property
test_has_result(results, `world`);
// as well as method from the actual proxy target prototype
test_has_result(results, `hasOwnProperty`);
results = propertyProvider(`testSelfPrototypeProxy.`);
test_has_result(results, `hello`);
test_has_result(results, `hasOwnProperty`);
info("Test suggestion for Infinity");
results = propertyProvider("Inf");
test_has_result(results, "Infinity");
}
/**
* 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.greater(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);
}