a6daef2582bc4e9463c2eb453b15f93a3c7048c8
[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, {description} = {})
29     {
30         console.assert(typeof test === "string");
31
32         super(name, {description});
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} = 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
59         return new WI.AuditTestCase(name, test, options);
60     }
61
62     // Public
63
64     get test() { return this._test; }
65
66     toJSON()
67     {
68         let json = super.toJSON();
69         json.test = this._test;
70         return json;
71     }
72
73     // Protected
74
75     async run()
76     {
77         const levelStrings = Object.values(WI.AuditTestCaseResult.Level);
78         let level = null;
79         let data = {};
80         let metadata = {
81             url: WI.networkManager.mainFrame.url,
82             startTimestamp: null,
83             endTimestamp: null,
84         };
85         let resolvedDOMNodes = null;
86
87         function setLevel(newLevel) {
88             let newLevelIndex = levelStrings.indexOf(newLevel);
89             if (newLevelIndex < 0) {
90                 addError(WI.UIString("Return string must be one of %s").format(JSON.stringify(levelStrings)));
91                 return;
92             }
93
94             if (newLevelIndex <= levelStrings.indexOf(level))
95                 return;
96
97             level = newLevel;
98         }
99
100         function addError(value) {
101             setLevel(WI.AuditTestCaseResult.Level.Error);
102
103             if (!data.errors)
104                 data.errors = [];
105
106             data.errors.push(value);
107         }
108
109         let evaluateArguments = {
110             expression: `(function() { "use strict"; return eval(${this._test})(); })()`,
111             objectGroup: "audit",
112             doNotPauseOnExceptionsAndMuteConsole: true,
113         };
114
115         async function parseResponse(response) {
116             let remoteObject = WI.RemoteObject.fromPayload(response.result, WI.mainTarget);
117             if (response.wasThrown || (remoteObject.type === "object" && remoteObject.subtype === "error"))
118                 addError(remoteObject.description);
119             else if (remoteObject.type === "boolean")
120                 setLevel(remoteObject.value ? WI.AuditTestCaseResult.Level.Pass : WI.AuditTestCaseResult.Level.Fail);
121             else if (remoteObject.type === "string")
122                 setLevel(remoteObject.value.trim().toLowerCase());
123             else if (remoteObject.type === "object" && !remoteObject.subtype) {
124                 const options = {
125                     ownProperties: true,
126                 };
127
128                 let properties = await new Promise((resolve, reject) => remoteObject.getPropertyDescriptorsAsObject(resolve, options));
129
130                 function checkResultProperty(key, type, subtype) {
131                     if (!(key in properties))
132                         return null;
133
134                     let property = properties[key].value;
135                     if (!property)
136                         return null;
137
138                     function addErrorForValueType(valueType) {
139                         let value = null;
140                         if (valueType === "object" || valueType === "array")
141                             value = WI.UIString("\u0022%s\u0022 must be an %s");
142                         else
143                             value = WI.UIString("\u0022%s\u0022 must be a %s");
144                         addError(value.format(key, valueType));
145                     }
146
147                     if (property.subtype !== subtype) {
148                         addErrorForValueType(subtype);
149                         return null;
150                     }
151
152                     if (property.type !== type) {
153                         addErrorForValueType(type);
154                         return null;
155                     }
156
157                     if (type === "boolean" || type === "string")
158                         return property.value;
159
160                     return property;
161                 }
162
163                 async function resultArrayForEach(key, callback) {
164                     let array = checkResultProperty(key, "object", "array");
165                     if (!array)
166                         return;
167
168                     // `getPropertyDescriptorsAsObject` returns an object, meaning that if we
169                     // want to iterate over `array` by index, we have to count.
170                     let asObject = await new Promise((resolve, reject) => array.getPropertyDescriptorsAsObject(resolve, options));
171                     for (let i = 0; i < array.size; ++i) {
172                         if (i in asObject)
173                             await callback(asObject[i]);
174                     }
175                 }
176
177                 let levelString = checkResultProperty("level", "string");
178                 if (levelString)
179                     setLevel(levelString.trim().toLowerCase());
180
181                 if (checkResultProperty("pass", "boolean"))
182                     setLevel(WI.AuditTestCaseResult.Level.Pass);
183                 if (checkResultProperty("warn", "boolean"))
184                     setLevel(WI.AuditTestCaseResult.Level.Warn);
185                 if (checkResultProperty("fail", "boolean"))
186                     setLevel(WI.AuditTestCaseResult.Level.Fail);
187                 if (checkResultProperty("error", "boolean"))
188                     setLevel(WI.AuditTestCaseResult.Level.Error);
189                 if (checkResultProperty("unsupported", "boolean"))
190                     setLevel(WI.AuditTestCaseResult.Level.Unsupported);
191
192                 await resultArrayForEach("domNodes", async (item) => {
193                     if (!item || !item.value || item.value.type !== "object" || item.value.subtype !== "node") {
194                         addError(WI.UIString("All items in \u0022%s\u0022 must be valid DOM nodes").format(WI.unlocalizedString("domNodes")));
195                         return;
196                     }
197
198                     let domNodeId = await new Promise((resolve, reject) => item.value.pushNodeToFrontend(resolve));
199                     let domNode = WI.domManager.nodeForId(domNodeId);
200                     if (!domNode)
201                         return;
202
203                     if (!data.domNodes)
204                         data.domNodes = [];
205                     data.domNodes.push(WI.cssPath(domNode, {full: true}));
206
207                     if (!resolvedDOMNodes)
208                         resolvedDOMNodes = [];
209                     resolvedDOMNodes.push(domNode);
210                 });
211
212                 await resultArrayForEach("domAttributes", (item) => {
213                     if (!item || !item.value || item.value.type !== "string" || !item.value.value.length) {
214                         addError(WI.UIString("All items in \u0022%s\u0022 must be non-empty strings").format(WI.unlocalizedString("domAttributes")));
215                         return;
216                     }
217
218                     if (!data.domAttributes)
219                         data.domAttributes = [];
220                     data.domAttributes.push(item.value.value);
221                 });
222
223                 await resultArrayForEach("errors", (item) => {
224                     if (!item || !item.value || item.value.type !== "object" || item.value.subtype !== "error") {
225                         addError(WI.UIString("All items in \u0022%s\u0022 must be error objects").format(WI.unlocalizedString("errors")));
226                         return;
227                     }
228
229                     addError(item.value.description);
230                 });
231             } else
232                 addError(WI.UIString("Return value is not an object, string, or boolean"));
233         }
234
235         try {
236             metadata.startTimestamp = new Date;
237             let response = await RuntimeAgent.evaluate.invoke(evaluateArguments);
238             metadata.endTimestamp = new Date;
239
240             if (response.result.type === "object" && response.result.className === "Promise") {
241                 if (WI.RuntimeManager.supportsAwaitPromise()) {
242                     metadata.asyncTimestamp = metadata.endTimestamp;
243                     response = await RuntimeAgent.awaitPromise(response.result.objectId);
244                     metadata.endTimestamp = new Date;
245                 } else {
246                     response = null;
247                     addError(WI.UIString("Async audits are not supported."));
248                     setLevel(WI.AuditTestCaseResult.Level.Unsupported);
249                 }
250             }
251
252             if (response)
253                 await parseResponse(response);
254         } catch (error) {
255             metadata.endTimestamp = new Date;
256             addError(error.message);
257         }
258
259         if (!level)
260             addError(WI.UIString("Missing result level"));
261
262         let options = {
263             description: this.description,
264             metadata,
265         };
266         if (!isEmptyObject(data))
267             options.data = data;
268         if (resolvedDOMNodes)
269             options.resolvedDOMNodes = resolvedDOMNodes;
270         this._result = new WI.AuditTestCaseResult(this.name, level, options);
271     }
272 };
273
274 WI.AuditTestCase.TypeIdentifier = "test-case";