369 lines
14 KiB
JavaScript
369 lines
14 KiB
JavaScript
/*
|
|
* Copyright (C) 2018 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.AuditTestCase = class AuditTestCase extends WI.AuditTestBase
|
|
{
|
|
constructor(name, test, options = {})
|
|
{
|
|
console.assert(typeof test === "string", test);
|
|
|
|
super(name, options);
|
|
|
|
this._test = test;
|
|
}
|
|
|
|
// Static
|
|
|
|
static async fromPayload(payload)
|
|
{
|
|
if (typeof payload !== "object" || payload === null)
|
|
return null;
|
|
|
|
if (payload.type !== WI.AuditTestCase.TypeIdentifier)
|
|
return null;
|
|
|
|
if (typeof payload.name !== "string") {
|
|
WI.AuditManager.synthesizeError(WI.UIString("\u0022%s\u0022 has a non-string \u0022%s\u0022 value").format(payload.name, WI.unlocalizedString("name")));
|
|
return null;
|
|
}
|
|
|
|
if (typeof payload.test !== "string") {
|
|
WI.AuditManager.synthesizeError(WI.UIString("\u0022%s\u0022 has a non-string \u0022%s\u0022 value").format(payload.name, WI.unlocalizedString("test")));
|
|
return null;
|
|
}
|
|
|
|
let options = {};
|
|
|
|
if (typeof payload.description === "string")
|
|
options.description = payload.description;
|
|
else if ("description" in payload)
|
|
WI.AuditManager.synthesizeWarning(WI.UIString("\u0022%s\u0022 has a non-string \u0022%s\u0022 value").format(payload.name, WI.unlocalizedString("description")));
|
|
|
|
if (typeof payload.supports === "number")
|
|
options.supports = payload.supports;
|
|
else if ("supports" in payload)
|
|
WI.AuditManager.synthesizeWarning(WI.UIString("\u0022%s\u0022 has a non-number \u0022%s\u0022 value").format(payload.name, WI.unlocalizedString("supports")));
|
|
|
|
if (typeof payload.setup === "string")
|
|
options.setup = payload.setup;
|
|
else if ("setup" in payload)
|
|
WI.AuditManager.synthesizeWarning(WI.UIString("\u0022%s\u0022 has a non-string \u0022%s\u0022 value").format(payload.name, WI.unlocalizedString("setup")));
|
|
|
|
if (typeof payload.disabled === "boolean")
|
|
options.disabled = payload.disabled;
|
|
|
|
return new WI.AuditTestCase(payload.name, payload.test, options);
|
|
}
|
|
|
|
// Public
|
|
|
|
get test()
|
|
{
|
|
return this._test;
|
|
}
|
|
|
|
set test(test)
|
|
{
|
|
console.assert(this.editable);
|
|
console.assert(typeof test === "string", test);
|
|
|
|
if (test === this._test)
|
|
return;
|
|
|
|
this._test = test;
|
|
|
|
this.clearResult();
|
|
|
|
this.dispatchEventToListeners(WI.AuditTestBase.Event.TestChanged);
|
|
}
|
|
|
|
toJSON(key)
|
|
{
|
|
let json = super.toJSON(key);
|
|
json.test = this._test;
|
|
return json;
|
|
}
|
|
|
|
// Protected
|
|
|
|
async run()
|
|
{
|
|
const levelStrings = Object.values(WI.AuditTestCaseResult.Level);
|
|
let level = null;
|
|
let data = {};
|
|
let metadata = {
|
|
url: WI.networkManager.mainFrame.url,
|
|
startTimestamp: null,
|
|
endTimestamp: null,
|
|
};
|
|
let resolvedDOMNodes = null;
|
|
|
|
function setLevel(newLevel) {
|
|
let newLevelIndex = levelStrings.indexOf(newLevel);
|
|
if (newLevelIndex < 0) {
|
|
addError(WI.UIString("Return string must be one of %s").format(JSON.stringify(levelStrings)));
|
|
return;
|
|
}
|
|
|
|
if (newLevelIndex <= levelStrings.indexOf(level))
|
|
return;
|
|
|
|
level = newLevel;
|
|
}
|
|
|
|
function addError(value) {
|
|
setLevel(WI.AuditTestCaseResult.Level.Error);
|
|
|
|
if (!data.errors)
|
|
data.errors = [];
|
|
|
|
data.errors.push(value);
|
|
}
|
|
|
|
async function parseResponse(response) {
|
|
let remoteObject = WI.RemoteObject.fromPayload(response.result, WI.mainTarget);
|
|
if (response.wasThrown || (remoteObject.type === "object" && remoteObject.subtype === "error")) {
|
|
addError(remoteObject.description);
|
|
return;
|
|
}
|
|
|
|
if (remoteObject.type === "boolean") {
|
|
setLevel(remoteObject.value ? WI.AuditTestCaseResult.Level.Pass : WI.AuditTestCaseResult.Level.Fail);
|
|
return;
|
|
}
|
|
|
|
if (remoteObject.type === "string") {
|
|
setLevel(remoteObject.value.trim().toLowerCase());
|
|
return;
|
|
}
|
|
|
|
if (remoteObject.type !== "object" || remoteObject.subtype) {
|
|
addError(WI.UIString("Return value is not an object, string, or boolean"));
|
|
return;
|
|
}
|
|
|
|
const options = {
|
|
ownProperties: true,
|
|
};
|
|
|
|
function checkResultProperty(key, value, type, subtype) {
|
|
function addErrorForValueType(valueType) {
|
|
let errorString = null;
|
|
if (valueType === "object" || valueType === "array")
|
|
errorString = WI.UIString("\u0022%s\u0022 must be an %s");
|
|
else
|
|
errorString = WI.UIString("\u0022%s\u0022 must be a %s");
|
|
addError(errorString.format(key, valueType));
|
|
}
|
|
|
|
if (value.subtype !== subtype) {
|
|
addErrorForValueType(subtype);
|
|
return null;
|
|
}
|
|
|
|
if (value.type !== type) {
|
|
addErrorForValueType(type);
|
|
return null;
|
|
}
|
|
|
|
if (type === "boolean" || type === "string")
|
|
return value.value;
|
|
|
|
return value;
|
|
}
|
|
|
|
async function resultArrayForEach(key, value, callback) {
|
|
let array = checkResultProperty(key, value, "object", "array");
|
|
if (!array)
|
|
return;
|
|
|
|
let arrayProperties = await new Promise((resolve, reject) => array.getPropertyDescriptors(resolve, options));
|
|
for (let i = 0; i < array.size; ++i) {
|
|
let arrayPropertyForIndex = arrayProperties.find((arrayProperty) => arrayProperty.name === String(i));
|
|
if (arrayPropertyForIndex)
|
|
await callback(arrayPropertyForIndex);
|
|
}
|
|
}
|
|
|
|
let properties = await new Promise((resolve, reject) => remoteObject.getPropertyDescriptors(resolve, options));
|
|
for (let property of properties) {
|
|
let key = property.name;
|
|
if (key === "__proto__")
|
|
continue;
|
|
|
|
let value = property.value;
|
|
|
|
switch (key) {
|
|
case "level": {
|
|
let levelString = checkResultProperty(key, value, "string");
|
|
if (levelString)
|
|
setLevel(levelString.trim().toLowerCase());
|
|
break;
|
|
}
|
|
|
|
case "pass":
|
|
if (checkResultProperty(key, value, "boolean"))
|
|
setLevel(WI.AuditTestCaseResult.Level.Pass);
|
|
break;
|
|
|
|
case "warn":
|
|
if (checkResultProperty(key, value, "boolean"))
|
|
setLevel(WI.AuditTestCaseResult.Level.Warn);
|
|
break;
|
|
|
|
case "fail":
|
|
if (checkResultProperty(key, value, "boolean"))
|
|
setLevel(WI.AuditTestCaseResult.Level.Fail);
|
|
break;
|
|
|
|
case "error":
|
|
if (checkResultProperty(key, value, "boolean"))
|
|
setLevel(WI.AuditTestCaseResult.Level.Error);
|
|
break;
|
|
|
|
case "unsupported":
|
|
if (checkResultProperty(key, value, "boolean"))
|
|
setLevel(WI.AuditTestCaseResult.Level.Unsupported);
|
|
break;
|
|
|
|
case "domNodes":
|
|
await resultArrayForEach(key, value, async (item) => {
|
|
if (!item || !item.value || item.value.type !== "object" || item.value.subtype !== "node") {
|
|
addError(WI.UIString("All items in \u0022%s\u0022 must be valid DOM nodes").format(WI.unlocalizedString("domNodes")));
|
|
return;
|
|
}
|
|
|
|
let domNodeId = await new Promise((resolve, reject) => item.value.pushNodeToFrontend(resolve));
|
|
let domNode = WI.domManager.nodeForId(domNodeId);
|
|
if (!domNode)
|
|
return;
|
|
|
|
if (!data.domNodes)
|
|
data.domNodes = [];
|
|
data.domNodes.push(WI.cssPath(domNode, {full: true}));
|
|
|
|
if (!resolvedDOMNodes)
|
|
resolvedDOMNodes = [];
|
|
resolvedDOMNodes.push(domNode);
|
|
});
|
|
break;
|
|
|
|
case "domAttributes":
|
|
await resultArrayForEach(key, value, (item) => {
|
|
if (!item || !item.value || item.value.type !== "string" || !item.value.value.length) {
|
|
addError(WI.UIString("All items in \u0022%s\u0022 must be non-empty strings").format(WI.unlocalizedString("domAttributes")));
|
|
return;
|
|
}
|
|
|
|
if (!data.domAttributes)
|
|
data.domAttributes = [];
|
|
data.domAttributes.push(item.value.value);
|
|
});
|
|
break;
|
|
|
|
case "errors":
|
|
await resultArrayForEach(key, value, (item) => {
|
|
if (!item || !item.value || item.value.type !== "object" || item.value.subtype !== "error") {
|
|
addError(WI.UIString("All items in \u0022%s\u0022 must be error objects").format(WI.unlocalizedString("errors")));
|
|
return;
|
|
}
|
|
|
|
addError(item.value.description);
|
|
});
|
|
break;
|
|
|
|
default:
|
|
if (value.objectId) {
|
|
try {
|
|
function inspectedPage_stringify() {
|
|
return JSON.stringify(this);
|
|
}
|
|
let stringifiedValue = await value.callFunction(inspectedPage_stringify);
|
|
data[key] = JSON.parse(stringifiedValue.value);
|
|
} catch {
|
|
addError(WI.UIString("\u0022%s\u0022 is not JSON serializable").format(key));
|
|
}
|
|
} else
|
|
data[key] = value.value;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
let target = WI.assumingMainTarget();
|
|
|
|
let agentCommandFunction = null;
|
|
let agentCommandArguments = {};
|
|
if (target.hasDomain("Audit")) {
|
|
agentCommandFunction = target.AuditAgent.run;
|
|
agentCommandArguments.test = this._test;
|
|
} else {
|
|
agentCommandFunction = target.RuntimeAgent.evaluate;
|
|
agentCommandArguments.expression = `(function() { "use strict"; return eval(\`(${this._test.replace(/`/g, "\\`")})\`)(); })()`;
|
|
agentCommandArguments.objectGroup = WI.AuditTestCase.ObjectGroup;
|
|
agentCommandArguments.doNotPauseOnExceptionsAndMuteConsole = true;
|
|
}
|
|
|
|
try {
|
|
metadata.startTimestamp = new Date;
|
|
let response = await agentCommandFunction.invoke(agentCommandArguments);
|
|
metadata.endTimestamp = new Date;
|
|
|
|
if (response.result.type === "object" && response.result.className === "Promise") {
|
|
if (WI.RuntimeManager.supportsAwaitPromise()) {
|
|
metadata.asyncTimestamp = metadata.endTimestamp;
|
|
response = await target.RuntimeAgent.awaitPromise(response.result.objectId);
|
|
metadata.endTimestamp = new Date;
|
|
} else {
|
|
response = null;
|
|
addError(WI.UIString("Async audits are not supported."));
|
|
setLevel(WI.AuditTestCaseResult.Level.Unsupported);
|
|
}
|
|
}
|
|
|
|
if (response)
|
|
await parseResponse(response);
|
|
} catch (error) {
|
|
metadata.endTimestamp = new Date;
|
|
addError(error.message);
|
|
}
|
|
|
|
if (!level)
|
|
addError(WI.UIString("Missing result level"));
|
|
|
|
let options = {
|
|
description: this.description,
|
|
metadata,
|
|
};
|
|
if (!isEmptyObject(data))
|
|
options.data = data;
|
|
if (resolvedDOMNodes)
|
|
options.resolvedDOMNodes = resolvedDOMNodes;
|
|
this.updateResult(new WI.AuditTestCaseResult(this.name, level, options));
|
|
}
|
|
};
|
|
|
|
WI.AuditTestCase.TypeIdentifier = "test-case";
|