Web Inspector: Audit: allow audits to be enabled/disabled
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Models / AuditTestCase.js
1 /*
2  * Copyright (C) 2018 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WI.AuditTestCase = class AuditTestCase extends WI.AuditTestBase
27 {
28     constructor(name, test, options = {})
29     {
30         console.assert(typeof test === "string");
31
32         super(name, options);
33
34         this._test = test;
35     }
36
37     // Static
38
39     static async fromPayload(payload)
40     {
41         if (typeof payload !== "object" || payload === null)
42             return null;
43
44         let {type, name, test, description, disabled} = payload;
45
46         if (type !== WI.AuditTestCase.TypeIdentifier)
47             return null;
48
49         if (typeof name !== "string")
50             return null;
51
52         if (typeof test !== "string")
53             return null;
54
55         let options = {};
56         if (typeof description === "string")
57             options.description = description;
58         if (typeof disabled === "boolean")
59             options.disabled = disabled;
60
61         return new WI.AuditTestCase(name, test, options);
62     }
63
64     // Public
65
66     get test() { return this._test; }
67
68     toJSON(key)
69     {
70         let json = super.toJSON(key);
71         json.test = this._test;
72         return json;
73     }
74
75     // Protected
76
77     async run()
78     {
79         const levelStrings = Object.values(WI.AuditTestCaseResult.Level);
80         let level = null;
81         let data = {};
82         let metadata = {
83             url: WI.networkManager.mainFrame.url,
84             startTimestamp: null,
85             endTimestamp: null,
86         };
87         let resolvedDOMNodes = null;
88
89         function setLevel(newLevel) {
90             let newLevelIndex = levelStrings.indexOf(newLevel);
91             if (newLevelIndex < 0) {
92                 addError(WI.UIString("Return string must be one of %s").format(JSON.stringify(levelStrings)));
93                 return;
94             }
95
96             if (newLevelIndex <= levelStrings.indexOf(level))
97                 return;
98
99             level = newLevel;
100         }
101
102         function addError(value) {
103             setLevel(WI.AuditTestCaseResult.Level.Error);
104
105             if (!data.errors)
106                 data.errors = [];
107
108             data.errors.push(value);
109         }
110
111         let evaluateArguments = {
112             expression: `(function() { "use strict"; return eval(${this._test})(); })()`,
113             objectGroup: "audit",
114             doNotPauseOnExceptionsAndMuteConsole: true,
115         };
116
117         async function parseResponse(response) {
118             let remoteObject = WI.RemoteObject.fromPayload(response.result, WI.mainTarget);
119             if (response.wasThrown || (remoteObject.type === "object" && remoteObject.subtype === "error"))
120                 addError(remoteObject.description);
121             else if (remoteObject.type === "boolean")
122                 setLevel(remoteObject.value ? WI.AuditTestCaseResult.Level.Pass : WI.AuditTestCaseResult.Level.Fail);
123             else if (remoteObject.type === "string")
124                 setLevel(remoteObject.value.trim().toLowerCase());
125             else if (remoteObject.type === "object" && !remoteObject.subtype) {
126                 const options = {
127                     ownProperties: true,
128                 };
129
130                 let properties = await new Promise((resolve, reject) => remoteObject.getPropertyDescriptorsAsObject(resolve, options));
131
132                 function checkResultProperty(key, type, subtype) {
133                     if (!(key in properties))
134                         return null;
135
136                     let property = properties[key].value;
137                     if (!property)
138                         return null;
139
140                     function addErrorForValueType(valueType) {
141                         let value = null;
142                         if (valueType === "object" || valueType === "array")
143                             value = WI.UIString("\u0022%s\u0022 must be an %s");
144                         else
145                             value = WI.UIString("\u0022%s\u0022 must be a %s");
146                         addError(value.format(key, valueType));
147                     }
148
149                     if (property.subtype !== subtype) {
150                         addErrorForValueType(subtype);
151                         return null;
152                     }
153
154                     if (property.type !== type) {
155                         addErrorForValueType(type);
156                         return null;
157                     }
158
159                     if (type === "boolean" || type === "string")
160                         return property.value;
161
162                     return property;
163                 }
164
165                 async function resultArrayForEach(key, callback) {
166                     let array = checkResultProperty(key, "object", "array");
167                     if (!array)
168                         return;
169
170                     // `getPropertyDescriptorsAsObject` returns an object, meaning that if we
171                     // want to iterate over `array` by index, we have to count.
172                     let asObject = await new Promise((resolve, reject) => array.getPropertyDescriptorsAsObject(resolve, options));
173                     for (let i = 0; i < array.size; ++i) {
174                         if (i in asObject)
175                             await callback(asObject[i]);
176                     }
177                 }
178
179                 let levelString = checkResultProperty("level", "string");
180                 if (levelString)
181                     setLevel(levelString.trim().toLowerCase());
182
183                 if (checkResultProperty("pass", "boolean"))
184                     setLevel(WI.AuditTestCaseResult.Level.Pass);
185                 if (checkResultProperty("warn", "boolean"))
186                     setLevel(WI.AuditTestCaseResult.Level.Warn);
187                 if (checkResultProperty("fail", "boolean"))
188                     setLevel(WI.AuditTestCaseResult.Level.Fail);
189                 if (checkResultProperty("error", "boolean"))
190                     setLevel(WI.AuditTestCaseResult.Level.Error);
191                 if (checkResultProperty("unsupported", "boolean"))
192                     setLevel(WI.AuditTestCaseResult.Level.Unsupported);
193
194                 await resultArrayForEach("domNodes", async (item) => {
195                     if (!item || !item.value || item.value.type !== "object" || item.value.subtype !== "node") {
196                         addError(WI.UIString("All items in \u0022%s\u0022 must be valid DOM nodes").format(WI.unlocalizedString("domNodes")));
197                         return;
198                     }
199
200                     let domNodeId = await new Promise((resolve, reject) => item.value.pushNodeToFrontend(resolve));
201                     let domNode = WI.domManager.nodeForId(domNodeId);
202                     if (!domNode)
203                         return;
204
205                     if (!data.domNodes)
206                         data.domNodes = [];
207                     data.domNodes.push(WI.cssPath(domNode, {full: true}));
208
209                     if (!resolvedDOMNodes)
210                         resolvedDOMNodes = [];
211                     resolvedDOMNodes.push(domNode);
212                 });
213
214                 await resultArrayForEach("domAttributes", (item) => {
215                     if (!item || !item.value || item.value.type !== "string" || !item.value.value.length) {
216                         addError(WI.UIString("All items in \u0022%s\u0022 must be non-empty strings").format(WI.unlocalizedString("domAttributes")));
217                         return;
218                     }
219
220                     if (!data.domAttributes)
221                         data.domAttributes = [];
222                     data.domAttributes.push(item.value.value);
223                 });
224
225                 await resultArrayForEach("errors", (item) => {
226                     if (!item || !item.value || item.value.type !== "object" || item.value.subtype !== "error") {
227                         addError(WI.UIString("All items in \u0022%s\u0022 must be error objects").format(WI.unlocalizedString("errors")));
228                         return;
229                     }
230
231                     addError(item.value.description);
232                 });
233             } else
234                 addError(WI.UIString("Return value is not an object, string, or boolean"));
235         }
236
237         try {
238             metadata.startTimestamp = new Date;
239             let response = await RuntimeAgent.evaluate.invoke(evaluateArguments);
240             metadata.endTimestamp = new Date;
241
242             if (response.result.type === "object" && response.result.className === "Promise") {
243                 if (WI.RuntimeManager.supportsAwaitPromise()) {
244                     metadata.asyncTimestamp = metadata.endTimestamp;
245                     response = await RuntimeAgent.awaitPromise(response.result.objectId);
246                     metadata.endTimestamp = new Date;
247                 } else {
248                     response = null;
249                     addError(WI.UIString("Async audits are not supported."));
250                     setLevel(WI.AuditTestCaseResult.Level.Unsupported);
251                 }
252             }
253
254             if (response)
255                 await parseResponse(response);
256         } catch (error) {
257             metadata.endTimestamp = new Date;
258             addError(error.message);
259         }
260
261         if (!level)
262             addError(WI.UIString("Missing result level"));
263
264         let options = {
265             description: this.description,
266             metadata,
267         };
268         if (!isEmptyObject(data))
269             options.data = data;
270         if (resolvedDOMNodes)
271             options.resolvedDOMNodes = resolvedDOMNodes;
272         this._result = new WI.AuditTestCaseResult(this.name, level, options);
273     }
274 };
275
276 WI.AuditTestCase.TypeIdentifier = "test-case";