| /* |
| * 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"; |