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 | startJSCoverage(options) {return (fn => {
|
41 | const gen = fn.call(this);
|
42 | return new Promise((resolve, reject) => {
|
43 | function step(key, arg) {
|
44 | let info, value;
|
45 | try {
|
46 | info = gen[key](arg);
|
47 | value = info.value;
|
48 | } catch (error) {
|
49 | reject(error);
|
50 | return;
|
51 | }
|
52 | if (info.done) {
|
53 | resolve(value);
|
54 | } else {
|
55 | return Promise.resolve(value).then(
|
56 | value => {
|
57 | step('next', value);
|
58 | },
|
59 | err => {
|
60 | step('throw', err);
|
61 | });
|
62 | }
|
63 | }
|
64 | return step('next');
|
65 | });
|
66 | })(function*(){
|
67 | return (yield this._jsCoverage.start(options));
|
68 | });}
|
69 |
|
70 | |
71 |
|
72 |
|
73 | stopJSCoverage() {return (fn => {
|
74 | const gen = fn.call(this);
|
75 | return new Promise((resolve, reject) => {
|
76 | function step(key, arg) {
|
77 | let info, value;
|
78 | try {
|
79 | info = gen[key](arg);
|
80 | value = info.value;
|
81 | } catch (error) {
|
82 | reject(error);
|
83 | return;
|
84 | }
|
85 | if (info.done) {
|
86 | resolve(value);
|
87 | } else {
|
88 | return Promise.resolve(value).then(
|
89 | value => {
|
90 | step('next', value);
|
91 | },
|
92 | err => {
|
93 | step('throw', err);
|
94 | });
|
95 | }
|
96 | }
|
97 | return step('next');
|
98 | });
|
99 | })(function*(){
|
100 | return (yield this._jsCoverage.stop());
|
101 | });}
|
102 |
|
103 | |
104 |
|
105 |
|
106 | startCSSCoverage(options) {return (fn => {
|
107 | const gen = fn.call(this);
|
108 | return new Promise((resolve, reject) => {
|
109 | function step(key, arg) {
|
110 | let info, value;
|
111 | try {
|
112 | info = gen[key](arg);
|
113 | value = info.value;
|
114 | } catch (error) {
|
115 | reject(error);
|
116 | return;
|
117 | }
|
118 | if (info.done) {
|
119 | resolve(value);
|
120 | } else {
|
121 | return Promise.resolve(value).then(
|
122 | value => {
|
123 | step('next', value);
|
124 | },
|
125 | err => {
|
126 | step('throw', err);
|
127 | });
|
128 | }
|
129 | }
|
130 | return step('next');
|
131 | });
|
132 | })(function*(){
|
133 | return (yield this._cssCoverage.start(options));
|
134 | });}
|
135 |
|
136 | |
137 |
|
138 |
|
139 | stopCSSCoverage() {return (fn => {
|
140 | const gen = fn.call(this);
|
141 | return new Promise((resolve, reject) => {
|
142 | function step(key, arg) {
|
143 | let info, value;
|
144 | try {
|
145 | info = gen[key](arg);
|
146 | value = info.value;
|
147 | } catch (error) {
|
148 | reject(error);
|
149 | return;
|
150 | }
|
151 | if (info.done) {
|
152 | resolve(value);
|
153 | } else {
|
154 | return Promise.resolve(value).then(
|
155 | value => {
|
156 | step('next', value);
|
157 | },
|
158 | err => {
|
159 | step('throw', err);
|
160 | });
|
161 | }
|
162 | }
|
163 | return step('next');
|
164 | });
|
165 | })(function*(){
|
166 | return (yield this._cssCoverage.stop());
|
167 | });}
|
168 | }
|
169 |
|
170 | module.exports = {Coverage};
|
171 | helper.tracePublicAPI(Coverage);
|
172 |
|
173 | class JSCoverage {
|
174 | |
175 |
|
176 |
|
177 | constructor(client) {
|
178 | this._client = client;
|
179 | this._enabled = false;
|
180 | this._scriptURLs = new Map();
|
181 | this._scriptSources = new Map();
|
182 | this._eventListeners = [];
|
183 | this._resetOnNavigation = false;
|
184 | }
|
185 |
|
186 | |
187 |
|
188 |
|
189 | start(options = {}) {return (fn => {
|
190 | const gen = fn.call(this);
|
191 | return new Promise((resolve, reject) => {
|
192 | function step(key, arg) {
|
193 | let info, value;
|
194 | try {
|
195 | info = gen[key](arg);
|
196 | value = info.value;
|
197 | } catch (error) {
|
198 | reject(error);
|
199 | return;
|
200 | }
|
201 | if (info.done) {
|
202 | resolve(value);
|
203 | } else {
|
204 | return Promise.resolve(value).then(
|
205 | value => {
|
206 | step('next', value);
|
207 | },
|
208 | err => {
|
209 | step('throw', err);
|
210 | });
|
211 | }
|
212 | }
|
213 | return step('next');
|
214 | });
|
215 | })(function*(){
|
216 | assert(!this._enabled, 'JSCoverage is already enabled');
|
217 | this._resetOnNavigation = options.resetOnNavigation === undefined ? true : !!options.resetOnNavigation;
|
218 | this._reportAnonymousScripts = !!options.reportAnonymousScripts;
|
219 | this._enabled = true;
|
220 | this._scriptURLs.clear();
|
221 | this._scriptSources.clear();
|
222 | this._eventListeners = [
|
223 | helper.addEventListener(this._client, 'Debugger.scriptParsed', this._onScriptParsed.bind(this)),
|
224 | helper.addEventListener(this._client, 'Runtime.executionContextsCleared', this._onExecutionContextsCleared.bind(this)),
|
225 | ];
|
226 | (yield Promise.all([
|
227 | this._client.send('Profiler.enable'),
|
228 | this._client.send('Profiler.startPreciseCoverage', {callCount: false, detailed: true}),
|
229 | this._client.send('Debugger.enable'),
|
230 | this._client.send('Debugger.setSkipAllPauses', {skip: true})
|
231 | ]));
|
232 | });}
|
233 |
|
234 | _onExecutionContextsCleared() {
|
235 | if (!this._resetOnNavigation)
|
236 | return;
|
237 | this._scriptURLs.clear();
|
238 | this._scriptSources.clear();
|
239 | }
|
240 |
|
241 | |
242 |
|
243 |
|
244 | _onScriptParsed(event) {return (fn => {
|
245 | const gen = fn.call(this);
|
246 | return new Promise((resolve, reject) => {
|
247 | function step(key, arg) {
|
248 | let info, value;
|
249 | try {
|
250 | info = gen[key](arg);
|
251 | value = info.value;
|
252 | } catch (error) {
|
253 | reject(error);
|
254 | return;
|
255 | }
|
256 | if (info.done) {
|
257 | resolve(value);
|
258 | } else {
|
259 | return Promise.resolve(value).then(
|
260 | value => {
|
261 | step('next', value);
|
262 | },
|
263 | err => {
|
264 | step('throw', err);
|
265 | });
|
266 | }
|
267 | }
|
268 | return step('next');
|
269 | });
|
270 | })(function*(){
|
271 |
|
272 | if (event.url === EVALUATION_SCRIPT_URL)
|
273 | return;
|
274 |
|
275 | if (!event.url && !this._reportAnonymousScripts)
|
276 | return;
|
277 | try {
|
278 | const response = (yield this._client.send('Debugger.getScriptSource', {scriptId: event.scriptId}));
|
279 | this._scriptURLs.set(event.scriptId, event.url);
|
280 | this._scriptSources.set(event.scriptId, response.scriptSource);
|
281 | } catch (e) {
|
282 |
|
283 | debugError(e);
|
284 | }
|
285 | });}
|
286 |
|
287 | |
288 |
|
289 |
|
290 | stop() {return (fn => {
|
291 | const gen = fn.call(this);
|
292 | return new Promise((resolve, reject) => {
|
293 | function step(key, arg) {
|
294 | let info, value;
|
295 | try {
|
296 | info = gen[key](arg);
|
297 | value = info.value;
|
298 | } catch (error) {
|
299 | reject(error);
|
300 | return;
|
301 | }
|
302 | if (info.done) {
|
303 | resolve(value);
|
304 | } else {
|
305 | return Promise.resolve(value).then(
|
306 | value => {
|
307 | step('next', value);
|
308 | },
|
309 | err => {
|
310 | step('throw', err);
|
311 | });
|
312 | }
|
313 | }
|
314 | return step('next');
|
315 | });
|
316 | })(function*(){
|
317 | assert(this._enabled, 'JSCoverage is not enabled');
|
318 | this._enabled = false;
|
319 | const [profileResponse] = (yield Promise.all([
|
320 | this._client.send('Profiler.takePreciseCoverage'),
|
321 | this._client.send('Profiler.stopPreciseCoverage'),
|
322 | this._client.send('Profiler.disable'),
|
323 | this._client.send('Debugger.disable'),
|
324 | ]));
|
325 | helper.removeEventListeners(this._eventListeners);
|
326 |
|
327 | const coverage = [];
|
328 | for (const entry of profileResponse.result) {
|
329 | let url = this._scriptURLs.get(entry.scriptId);
|
330 | if (!url && this._reportAnonymousScripts)
|
331 | url = 'debugger://VM' + entry.scriptId;
|
332 | const text = this._scriptSources.get(entry.scriptId);
|
333 | if (text === undefined || url === undefined)
|
334 | continue;
|
335 | const flattenRanges = [];
|
336 | for (const func of entry.functions)
|
337 | flattenRanges.push(...func.ranges);
|
338 | const ranges = convertToDisjointRanges(flattenRanges);
|
339 | coverage.push({url, ranges, text});
|
340 | }
|
341 | return coverage;
|
342 | });}
|
343 | }
|
344 |
|
345 | class CSSCoverage {
|
346 | |
347 |
|
348 |
|
349 | constructor(client) {
|
350 | this._client = client;
|
351 | this._enabled = false;
|
352 | this._stylesheetURLs = new Map();
|
353 | this._stylesheetSources = new Map();
|
354 | this._eventListeners = [];
|
355 | this._resetOnNavigation = false;
|
356 | }
|
357 |
|
358 | |
359 |
|
360 |
|
361 | start(options = {}) {return (fn => {
|
362 | const gen = fn.call(this);
|
363 | return new Promise((resolve, reject) => {
|
364 | function step(key, arg) {
|
365 | let info, value;
|
366 | try {
|
367 | info = gen[key](arg);
|
368 | value = info.value;
|
369 | } catch (error) {
|
370 | reject(error);
|
371 | return;
|
372 | }
|
373 | if (info.done) {
|
374 | resolve(value);
|
375 | } else {
|
376 | return Promise.resolve(value).then(
|
377 | value => {
|
378 | step('next', value);
|
379 | },
|
380 | err => {
|
381 | step('throw', err);
|
382 | });
|
383 | }
|
384 | }
|
385 | return step('next');
|
386 | });
|
387 | })(function*(){
|
388 | assert(!this._enabled, 'CSSCoverage is already enabled');
|
389 | this._resetOnNavigation = options.resetOnNavigation === undefined ? true : !!options.resetOnNavigation;
|
390 | this._enabled = true;
|
391 | this._stylesheetURLs.clear();
|
392 | this._stylesheetSources.clear();
|
393 | this._eventListeners = [
|
394 | helper.addEventListener(this._client, 'CSS.styleSheetAdded', this._onStyleSheet.bind(this)),
|
395 | helper.addEventListener(this._client, 'Runtime.executionContextsCleared', this._onExecutionContextsCleared.bind(this)),
|
396 | ];
|
397 | (yield Promise.all([
|
398 | this._client.send('DOM.enable'),
|
399 | this._client.send('CSS.enable'),
|
400 | this._client.send('CSS.startRuleUsageTracking'),
|
401 | ]));
|
402 | });}
|
403 |
|
404 | _onExecutionContextsCleared() {
|
405 | if (!this._resetOnNavigation)
|
406 | return;
|
407 | this._stylesheetURLs.clear();
|
408 | this._stylesheetSources.clear();
|
409 | }
|
410 |
|
411 | |
412 |
|
413 |
|
414 | _onStyleSheet(event) {return (fn => {
|
415 | const gen = fn.call(this);
|
416 | return new Promise((resolve, reject) => {
|
417 | function step(key, arg) {
|
418 | let info, value;
|
419 | try {
|
420 | info = gen[key](arg);
|
421 | value = info.value;
|
422 | } catch (error) {
|
423 | reject(error);
|
424 | return;
|
425 | }
|
426 | if (info.done) {
|
427 | resolve(value);
|
428 | } else {
|
429 | return Promise.resolve(value).then(
|
430 | value => {
|
431 | step('next', value);
|
432 | },
|
433 | err => {
|
434 | step('throw', err);
|
435 | });
|
436 | }
|
437 | }
|
438 | return step('next');
|
439 | });
|
440 | })(function*(){
|
441 | const header = event.header;
|
442 |
|
443 | if (!header.sourceURL)
|
444 | return;
|
445 | try {
|
446 | const response = (yield this._client.send('CSS.getStyleSheetText', {styleSheetId: header.styleSheetId}));
|
447 | this._stylesheetURLs.set(header.styleSheetId, header.sourceURL);
|
448 | this._stylesheetSources.set(header.styleSheetId, response.text);
|
449 | } catch (e) {
|
450 |
|
451 | debugError(e);
|
452 | }
|
453 | });}
|
454 |
|
455 | |
456 |
|
457 |
|
458 | stop() {return (fn => {
|
459 | const gen = fn.call(this);
|
460 | return new Promise((resolve, reject) => {
|
461 | function step(key, arg) {
|
462 | let info, value;
|
463 | try {
|
464 | info = gen[key](arg);
|
465 | value = info.value;
|
466 | } catch (error) {
|
467 | reject(error);
|
468 | return;
|
469 | }
|
470 | if (info.done) {
|
471 | resolve(value);
|
472 | } else {
|
473 | return Promise.resolve(value).then(
|
474 | value => {
|
475 | step('next', value);
|
476 | },
|
477 | err => {
|
478 | step('throw', err);
|
479 | });
|
480 | }
|
481 | }
|
482 | return step('next');
|
483 | });
|
484 | })(function*(){
|
485 | assert(this._enabled, 'CSSCoverage is not enabled');
|
486 | this._enabled = false;
|
487 | const [ruleTrackingResponse] = (yield Promise.all([
|
488 | this._client.send('CSS.stopRuleUsageTracking'),
|
489 | this._client.send('CSS.disable'),
|
490 | this._client.send('DOM.disable'),
|
491 | ]));
|
492 | helper.removeEventListeners(this._eventListeners);
|
493 |
|
494 |
|
495 | const styleSheetIdToCoverage = new Map();
|
496 | for (const entry of ruleTrackingResponse.ruleUsage) {
|
497 | let ranges = styleSheetIdToCoverage.get(entry.styleSheetId);
|
498 | if (!ranges) {
|
499 | ranges = [];
|
500 | styleSheetIdToCoverage.set(entry.styleSheetId, ranges);
|
501 | }
|
502 | ranges.push({
|
503 | startOffset: entry.startOffset,
|
504 | endOffset: entry.endOffset,
|
505 | count: entry.used ? 1 : 0,
|
506 | });
|
507 | }
|
508 |
|
509 | const coverage = [];
|
510 | for (const styleSheetId of this._stylesheetURLs.keys()) {
|
511 | const url = this._stylesheetURLs.get(styleSheetId);
|
512 | const text = this._stylesheetSources.get(styleSheetId);
|
513 | const ranges = convertToDisjointRanges(styleSheetIdToCoverage.get(styleSheetId) || []);
|
514 | coverage.push({url, ranges, text});
|
515 | }
|
516 |
|
517 | return coverage;
|
518 | });}
|
519 | }
|
520 |
|
521 |
|
522 |
|
523 |
|
524 |
|
525 | function convertToDisjointRanges(nestedRanges) {
|
526 | const points = [];
|
527 | for (const range of nestedRanges) {
|
528 | points.push({ offset: range.startOffset, type: 0, range });
|
529 | points.push({ offset: range.endOffset, type: 1, range });
|
530 | }
|
531 |
|
532 | points.sort((a, b) => {
|
533 |
|
534 | if (a.offset !== b.offset)
|
535 | return a.offset - b.offset;
|
536 |
|
537 | if (a.type !== b.type)
|
538 | return b.type - a.type;
|
539 | const aLength = a.range.endOffset - a.range.startOffset;
|
540 | const bLength = b.range.endOffset - b.range.startOffset;
|
541 |
|
542 | if (a.type === 0)
|
543 | return bLength - aLength;
|
544 |
|
545 | return aLength - bLength;
|
546 | });
|
547 |
|
548 | const hitCountStack = [];
|
549 | const results = [];
|
550 | let lastOffset = 0;
|
551 |
|
552 | for (const point of points) {
|
553 | if (hitCountStack.length && lastOffset < point.offset && hitCountStack[hitCountStack.length - 1] > 0) {
|
554 | const lastResult = results.length ? results[results.length - 1] : null;
|
555 | if (lastResult && lastResult.end === lastOffset)
|
556 | lastResult.end = point.offset;
|
557 | else
|
558 | results.push({start: lastOffset, end: point.offset});
|
559 | }
|
560 | lastOffset = point.offset;
|
561 | if (point.type === 0)
|
562 | hitCountStack.push(point.range.count);
|
563 | else
|
564 | hitCountStack.pop();
|
565 | }
|
566 |
|
567 | return results.filter(range => range.end - range.start > 1);
|
568 | }
|
569 |
|