1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 | const {helper, debugError, assert} = require('./helper');
|
18 |
|
19 | const {EVALUATION_SCRIPT_URL} = require('./ExecutionContext');
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 | class Coverage {
|
29 | |
30 |
|
31 |
|
32 | constructor(client) {
|
33 | this._jsCoverage = new JSCoverage(client);
|
34 | this._cssCoverage = new CSSCoverage(client);
|
35 | }
|
36 |
|
37 | |
38 |
|
39 |
|
40 | async startJSCoverage(options) {
|
41 | return await this._jsCoverage.start(options);
|
42 | }
|
43 |
|
44 | |
45 |
|
46 |
|
47 | async stopJSCoverage() {
|
48 | return await this._jsCoverage.stop();
|
49 | }
|
50 |
|
51 | |
52 |
|
53 |
|
54 | async startCSSCoverage(options) {
|
55 | return await this._cssCoverage.start(options);
|
56 | }
|
57 |
|
58 | |
59 |
|
60 |
|
61 | async stopCSSCoverage() {
|
62 | return await this._cssCoverage.stop();
|
63 | }
|
64 | }
|
65 |
|
66 | module.exports = {Coverage};
|
67 | helper.tracePublicAPI(Coverage);
|
68 |
|
69 | class JSCoverage {
|
70 | |
71 |
|
72 |
|
73 | constructor(client) {
|
74 | this._client = client;
|
75 | this._enabled = false;
|
76 | this._scriptURLs = new Map();
|
77 | this._scriptSources = new Map();
|
78 | this._eventListeners = [];
|
79 | this._resetOnNavigation = false;
|
80 | }
|
81 |
|
82 | |
83 |
|
84 |
|
85 | async start(options = {}) {
|
86 | assert(!this._enabled, 'JSCoverage is already enabled');
|
87 | this._resetOnNavigation = options.resetOnNavigation === undefined ? true : !!options.resetOnNavigation;
|
88 | this._reportAnonymousScripts = !!options.reportAnonymousScripts;
|
89 | this._enabled = true;
|
90 | this._scriptURLs.clear();
|
91 | this._scriptSources.clear();
|
92 | this._eventListeners = [
|
93 | helper.addEventListener(this._client, 'Debugger.scriptParsed', this._onScriptParsed.bind(this)),
|
94 | helper.addEventListener(this._client, 'Runtime.executionContextsCleared', this._onExecutionContextsCleared.bind(this)),
|
95 | ];
|
96 | await Promise.all([
|
97 | this._client.send('Profiler.enable'),
|
98 | this._client.send('Profiler.startPreciseCoverage', {callCount: false, detailed: true}),
|
99 | this._client.send('Debugger.enable'),
|
100 | this._client.send('Debugger.setSkipAllPauses', {skip: true})
|
101 | ]);
|
102 | }
|
103 |
|
104 | _onExecutionContextsCleared() {
|
105 | if (!this._resetOnNavigation)
|
106 | return;
|
107 | this._scriptURLs.clear();
|
108 | this._scriptSources.clear();
|
109 | }
|
110 |
|
111 | |
112 |
|
113 |
|
114 | async _onScriptParsed(event) {
|
115 |
|
116 | if (event.url === EVALUATION_SCRIPT_URL)
|
117 | return;
|
118 |
|
119 | if (!event.url && !this._reportAnonymousScripts)
|
120 | return;
|
121 | try {
|
122 | const response = await this._client.send('Debugger.getScriptSource', {scriptId: event.scriptId});
|
123 | this._scriptURLs.set(event.scriptId, event.url);
|
124 | this._scriptSources.set(event.scriptId, response.scriptSource);
|
125 | } catch (e) {
|
126 |
|
127 | debugError(e);
|
128 | }
|
129 | }
|
130 |
|
131 | |
132 |
|
133 |
|
134 | async stop() {
|
135 | assert(this._enabled, 'JSCoverage is not enabled');
|
136 | this._enabled = false;
|
137 | const [profileResponse] = await Promise.all([
|
138 | this._client.send('Profiler.takePreciseCoverage'),
|
139 | this._client.send('Profiler.stopPreciseCoverage'),
|
140 | this._client.send('Profiler.disable'),
|
141 | this._client.send('Debugger.disable'),
|
142 | ]);
|
143 | helper.removeEventListeners(this._eventListeners);
|
144 |
|
145 | const coverage = [];
|
146 | for (const entry of profileResponse.result) {
|
147 | let url = this._scriptURLs.get(entry.scriptId);
|
148 | if (!url && this._reportAnonymousScripts)
|
149 | url = 'debugger://VM' + entry.scriptId;
|
150 | const text = this._scriptSources.get(entry.scriptId);
|
151 | if (text === undefined || url === undefined)
|
152 | continue;
|
153 | const flattenRanges = [];
|
154 | for (const func of entry.functions)
|
155 | flattenRanges.push(...func.ranges);
|
156 | const ranges = convertToDisjointRanges(flattenRanges);
|
157 | coverage.push({url, ranges, text});
|
158 | }
|
159 | return coverage;
|
160 | }
|
161 | }
|
162 |
|
163 | class CSSCoverage {
|
164 | |
165 |
|
166 |
|
167 | constructor(client) {
|
168 | this._client = client;
|
169 | this._enabled = false;
|
170 | this._stylesheetURLs = new Map();
|
171 | this._stylesheetSources = new Map();
|
172 | this._eventListeners = [];
|
173 | this._resetOnNavigation = false;
|
174 | }
|
175 |
|
176 | |
177 |
|
178 |
|
179 | async start(options = {}) {
|
180 | assert(!this._enabled, 'CSSCoverage is already enabled');
|
181 | this._resetOnNavigation = options.resetOnNavigation === undefined ? true : !!options.resetOnNavigation;
|
182 | this._enabled = true;
|
183 | this._stylesheetURLs.clear();
|
184 | this._stylesheetSources.clear();
|
185 | this._eventListeners = [
|
186 | helper.addEventListener(this._client, 'CSS.styleSheetAdded', this._onStyleSheet.bind(this)),
|
187 | helper.addEventListener(this._client, 'Runtime.executionContextsCleared', this._onExecutionContextsCleared.bind(this)),
|
188 | ];
|
189 | await Promise.all([
|
190 | this._client.send('DOM.enable'),
|
191 | this._client.send('CSS.enable'),
|
192 | this._client.send('CSS.startRuleUsageTracking'),
|
193 | ]);
|
194 | }
|
195 |
|
196 | _onExecutionContextsCleared() {
|
197 | if (!this._resetOnNavigation)
|
198 | return;
|
199 | this._stylesheetURLs.clear();
|
200 | this._stylesheetSources.clear();
|
201 | }
|
202 |
|
203 | |
204 |
|
205 |
|
206 | async _onStyleSheet(event) {
|
207 | const header = event.header;
|
208 |
|
209 | if (!header.sourceURL)
|
210 | return;
|
211 | try {
|
212 | const response = await this._client.send('CSS.getStyleSheetText', {styleSheetId: header.styleSheetId});
|
213 | this._stylesheetURLs.set(header.styleSheetId, header.sourceURL);
|
214 | this._stylesheetSources.set(header.styleSheetId, response.text);
|
215 | } catch (e) {
|
216 |
|
217 | debugError(e);
|
218 | }
|
219 | }
|
220 |
|
221 | |
222 |
|
223 |
|
224 | async stop() {
|
225 | assert(this._enabled, 'CSSCoverage is not enabled');
|
226 | this._enabled = false;
|
227 | const [ruleTrackingResponse] = await Promise.all([
|
228 | this._client.send('CSS.stopRuleUsageTracking'),
|
229 | this._client.send('CSS.disable'),
|
230 | this._client.send('DOM.disable'),
|
231 | ]);
|
232 | helper.removeEventListeners(this._eventListeners);
|
233 |
|
234 |
|
235 | const styleSheetIdToCoverage = new Map();
|
236 | for (const entry of ruleTrackingResponse.ruleUsage) {
|
237 | let ranges = styleSheetIdToCoverage.get(entry.styleSheetId);
|
238 | if (!ranges) {
|
239 | ranges = [];
|
240 | styleSheetIdToCoverage.set(entry.styleSheetId, ranges);
|
241 | }
|
242 | ranges.push({
|
243 | startOffset: entry.startOffset,
|
244 | endOffset: entry.endOffset,
|
245 | count: entry.used ? 1 : 0,
|
246 | });
|
247 | }
|
248 |
|
249 | const coverage = [];
|
250 | for (const styleSheetId of this._stylesheetURLs.keys()) {
|
251 | const url = this._stylesheetURLs.get(styleSheetId);
|
252 | const text = this._stylesheetSources.get(styleSheetId);
|
253 | const ranges = convertToDisjointRanges(styleSheetIdToCoverage.get(styleSheetId) || []);
|
254 | coverage.push({url, ranges, text});
|
255 | }
|
256 |
|
257 | return coverage;
|
258 | }
|
259 | }
|
260 |
|
261 |
|
262 |
|
263 |
|
264 |
|
265 | function convertToDisjointRanges(nestedRanges) {
|
266 | const points = [];
|
267 | for (const range of nestedRanges) {
|
268 | points.push({ offset: range.startOffset, type: 0, range });
|
269 | points.push({ offset: range.endOffset, type: 1, range });
|
270 | }
|
271 |
|
272 | points.sort((a, b) => {
|
273 |
|
274 | if (a.offset !== b.offset)
|
275 | return a.offset - b.offset;
|
276 |
|
277 | if (a.type !== b.type)
|
278 | return b.type - a.type;
|
279 | const aLength = a.range.endOffset - a.range.startOffset;
|
280 | const bLength = b.range.endOffset - b.range.startOffset;
|
281 |
|
282 | if (a.type === 0)
|
283 | return bLength - aLength;
|
284 |
|
285 | return aLength - bLength;
|
286 | });
|
287 |
|
288 | const hitCountStack = [];
|
289 | const results = [];
|
290 | let lastOffset = 0;
|
291 |
|
292 | for (const point of points) {
|
293 | if (hitCountStack.length && lastOffset < point.offset && hitCountStack[hitCountStack.length - 1] > 0) {
|
294 | const lastResult = results.length ? results[results.length - 1] : null;
|
295 | if (lastResult && lastResult.end === lastOffset)
|
296 | lastResult.end = point.offset;
|
297 | else
|
298 | results.push({start: lastOffset, end: point.offset});
|
299 | }
|
300 | lastOffset = point.offset;
|
301 | if (point.type === 0)
|
302 | hitCountStack.push(point.range.count);
|
303 | else
|
304 | hitCountStack.pop();
|
305 | }
|
306 |
|
307 | return results.filter(range => range.end - range.start > 1);
|
308 | }
|
309 |
|