blob: d967c3157b7dfb90de4ab94d77763eb33190ccb5 [file] [log] [blame]
/*
* 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");
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; }
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._result = new WI.AuditTestCaseResult(this.name, level, options);
this.dispatchEventToListeners(WI.AuditTestBase.Event.ResultChanged);
}
};
WI.AuditTestCase.TypeIdentifier = "test-case";