@@ -2542,7 +2542,7 @@ if (typeof module !== 'undefined' && module.exports) {
2542
2542
* the report.
2543
2543
*/
2544
2544
2545
- /* globals getFilenamePrefix Util ElementScreenshotRenderer */
2545
+ /* globals getFilenamePrefix Util TextEncoding ElementScreenshotRenderer */
2546
2546
2547
2547
/** @typedef {import('./dom') } DOM */
2548
2548
@@ -2672,8 +2672,7 @@ class ReportUIFeatures {
2672
2672
2673
2673
const showTreemapApp =
2674
2674
this . json . audits [ 'script-treemap-data' ] && this . json . audits [ 'script-treemap-data' ] . details ;
2675
- // TODO: need window.opener to work in DevTools.
2676
- if ( showTreemapApp && ! this . _dom . isDevTools ( ) ) {
2675
+ if ( showTreemapApp ) {
2677
2676
this . addButton ( {
2678
2677
text : Util . i18n . strings . viewTreemapLabel ,
2679
2678
icon : 'treemap' ,
@@ -2700,12 +2699,18 @@ class ReportUIFeatures {
2700
2699
}
2701
2700
2702
2701
/**
2703
- * @param {{text: string, icon?: string, onClick: () => void} } opts
2702
+ * @param {{container?: Element, text: string, icon?: string, onClick: () => void} } opts
2704
2703
*/
2705
2704
addButton ( opts ) {
2705
+ // report-ui-features doesn't have a reference to the root report el, and PSI has
2706
+ // 2 reports on the page (and not even attached to DOM when installFeatures is called..)
2707
+ // so we need a container option to specify where the element should go.
2706
2708
const metricsEl = this . _document . querySelector ( '.lh-audit-group--metrics' ) ;
2707
- // Not supported without metrics group.
2708
- if ( ! metricsEl ) return ;
2709
+ const containerEl = opts . container || metricsEl ;
2710
+ if ( ! containerEl ) return ;
2711
+
2712
+ let buttonsEl = containerEl . querySelector ( '.lh-buttons' ) ;
2713
+ if ( ! buttonsEl ) buttonsEl = this . _dom . createChildOf ( containerEl , 'div' , 'lh-buttons' ) ;
2709
2714
2710
2715
const classes = [
2711
2716
'lh-button' ,
@@ -2714,10 +2719,9 @@ class ReportUIFeatures {
2714
2719
classes . push ( 'report-icon' ) ;
2715
2720
classes . push ( `report-icon--${ opts . icon } ` ) ;
2716
2721
}
2717
- const buttonEl = this . _dom . createChildOf ( metricsEl , 'button' , classes . join ( ' ' ) ) ;
2718
- buttonEl . addEventListener ( 'click' , opts . onClick ) ;
2722
+ const buttonEl = this . _dom . createChildOf ( buttonsEl , 'button' , classes . join ( ' ' ) ) ;
2719
2723
buttonEl . textContent = opts . text ;
2720
- metricsEl . append ( buttonEl ) ;
2724
+ buttonEl . addEventListener ( 'click' , opts . onClick ) ;
2721
2725
return buttonEl ;
2722
2726
}
2723
2727
@@ -3049,23 +3053,31 @@ class ReportUIFeatures {
3049
3053
}
3050
3054
3051
3055
/**
3052
- * Opens a new tab to the online viewer and sends the local page's JSON results
3053
- * to the online viewer using postMessage.
3056
+ * The popup's window.name is keyed by version+url+fetchTime, so we reuse/select tabs correctly.
3054
3057
* @param {LH.Result } json
3055
3058
* @protected
3056
3059
*/
3057
- static openTabAndSendJsonReportToViewer ( json ) {
3058
- // The popup's window.name is keyed by version+url+fetchTime, so we reuse/select tabs correctly
3060
+ static computeWindowNameSuffix ( json ) {
3059
3061
// @ts -ignore - If this is a v2 LHR, use old `generatedTime`.
3060
3062
const fallbackFetchTime = /** @type {string } */ ( json . generatedTime ) ;
3061
3063
const fetchTime = json . fetchTime || fallbackFetchTime ;
3062
- const windowName = `${ json . lighthouseVersion } -${ json . requestedUrl } -${ fetchTime } ` ;
3064
+ return `${ json . lighthouseVersion } -${ json . requestedUrl } -${ fetchTime } ` ;
3065
+ }
3066
+
3067
+ /**
3068
+ * Opens a new tab to the online viewer and sends the local page's JSON results
3069
+ * to the online viewer using postMessage.
3070
+ * @param {LH.Result } json
3071
+ * @protected
3072
+ */
3073
+ static openTabAndSendJsonReportToViewer ( json ) {
3074
+ const windowName = 'viewer-' + this . computeWindowNameSuffix ( json ) ;
3063
3075
const url = getAppsOrigin ( ) + '/viewer/' ;
3064
3076
ReportUIFeatures . openTabAndSendData ( { lhr : json } , url , windowName ) ;
3065
3077
}
3066
3078
3067
3079
/**
3068
- * Opens a new tab to the treemap app and sends the JSON results using postMessage.
3080
+ * Opens a new tab to the treemap app and sends the JSON results using URL.fragment
3069
3081
* @param {LH.Result } json
3070
3082
*/
3071
3083
static openTreemap ( json ) {
@@ -3074,13 +3086,23 @@ class ReportUIFeatures {
3074
3086
throw new Error ( 'no script treemap data found' ) ;
3075
3087
}
3076
3088
3077
- const windowName = `treemap-${ json . requestedUrl } ` ;
3078
3089
/** @type {LH.Treemap.Options } */
3079
3090
const treemapOptions = {
3080
- lhr : json ,
3091
+ lhr : {
3092
+ requestedUrl : json . requestedUrl ,
3093
+ finalUrl : json . finalUrl ,
3094
+ audits : {
3095
+ 'script-treemap-data' : json . audits [ 'script-treemap-data' ] ,
3096
+ } ,
3097
+ configSettings : {
3098
+ locale : json . configSettings . locale ,
3099
+ } ,
3100
+ } ,
3081
3101
} ;
3082
3102
const url = getAppsOrigin ( ) + '/treemap/' ;
3083
- ReportUIFeatures . openTabAndSendData ( treemapOptions , url , windowName ) ;
3103
+ const windowName = 'treemap-' + this . computeWindowNameSuffix ( json ) ;
3104
+
3105
+ ReportUIFeatures . openTabWithUrlData ( treemapOptions , url , windowName ) ;
3084
3106
}
3085
3107
3086
3108
/**
@@ -3106,10 +3128,26 @@ class ReportUIFeatures {
3106
3128
}
3107
3129
} ) ;
3108
3130
3109
- // The popup's window.name is keyed by version+url+fetchTime, so we reuse/select tabs correctly
3110
3131
const popup = window . open ( url , windowName ) ;
3111
3132
}
3112
3133
3134
+ /**
3135
+ * Opens a new tab to an external page and sends data via base64 encoded url params.
3136
+ * @param {{lhr: LH.Result} | LH.Treemap.Options } data
3137
+ * @param {string } url_
3138
+ * @param {string } windowName
3139
+ * @protected
3140
+ */
3141
+ static async openTabWithUrlData ( data , url_ , windowName ) {
3142
+ const url = new URL ( url_ ) ;
3143
+ const gzip = Boolean ( window . CompressionStream ) ;
3144
+ url . hash = await TextEncoding . toBase64 ( JSON . stringify ( data ) , {
3145
+ gzip,
3146
+ } ) ;
3147
+ if ( gzip ) url . searchParams . set ( 'gzip' , '1' ) ;
3148
+ window . open ( url . toString ( ) , windowName ) ;
3149
+ }
3150
+
3113
3151
/**
3114
3152
* Expands all audit `<details>`.
3115
3153
* Ideally, a print stylesheet could take care of this, but CSS has no way to
@@ -4071,11 +4109,12 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
4071
4109
* @return {string }
4072
4110
*/
4073
4111
_getScoringCalculatorHref ( auditRefs ) {
4074
- const v5andv6metrics = auditRefs . filter ( audit => audit . group === 'metrics' ) ;
4112
+ // TODO: filter by !!acronym when dropping renderer support of v7 LHRs.
4113
+ const metrics = auditRefs . filter ( audit => audit . group === 'metrics' ) ;
4075
4114
const fci = auditRefs . find ( audit => audit . id === 'first-cpu-idle' ) ;
4076
4115
const fmp = auditRefs . find ( audit => audit . id === 'first-meaningful-paint' ) ;
4077
- if ( fci ) v5andv6metrics . push ( fci ) ;
4078
- if ( fmp ) v5andv6metrics . push ( fmp ) ;
4116
+ if ( fci ) metrics . push ( fci ) ;
4117
+ if ( fmp ) metrics . push ( fmp ) ;
4079
4118
4080
4119
/**
4081
4120
* Clamp figure to 2 decimal places
@@ -4084,7 +4123,7 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
4084
4123
*/
4085
4124
const clampTo2Decimals = val => Math . round ( val * 100 ) / 100 ;
4086
4125
4087
- const metricPairs = v5andv6metrics . map ( audit => {
4126
+ const metricPairs = metrics . map ( audit => {
4088
4127
let value ;
4089
4128
if ( typeof audit . result . numericValue === 'number' ) {
4090
4129
value = audit . id === 'cumulative-layout-shift' ?
@@ -4270,17 +4309,17 @@ class PerformanceCategoryRenderer extends CategoryRenderer {
4270
4309
] ) ;
4271
4310
for ( const metric of filterChoices ) {
4272
4311
const elemId = `metric-${ metric . acronym } ` ;
4312
+ const radioEl = this . dom . createChildOf ( metricFilterEl , 'input' , 'lh-metricfilter__radio' , {
4313
+ type : 'radio' ,
4314
+ name : 'metricsfilter' ,
4315
+ id : elemId ,
4316
+ } ) ;
4317
+
4273
4318
const labelEl = this . dom . createChildOf ( metricFilterEl , 'label' , 'lh-metricfilter__label' , {
4274
4319
for : elemId ,
4275
4320
title : metric . result && metric . result . title ,
4276
4321
} ) ;
4277
4322
labelEl . textContent = metric . acronym || metric . id ;
4278
- const radioEl = this . dom . createChildOf ( labelEl , 'input' , 'lh-metricfilter__radio' , {
4279
- type : 'radio' ,
4280
- name : 'metricsfilter' ,
4281
- id : elemId ,
4282
- hidden : 'true' ,
4283
- } ) ;
4284
4323
4285
4324
if ( metric . acronym === 'All' ) {
4286
4325
radioEl . checked = true ;
@@ -5010,3 +5049,82 @@ if (typeof module !== 'undefined' && module.exports) {
5010
5049
} else {
5011
5050
self . I18n = I18n ;
5012
5051
}
5052
+ ;
5053
+ /**
5054
+ * @license Copyright 2021 The Lighthouse Authors. All Rights Reserved.
5055
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
5056
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
5057
+ */
5058
+ 'use strict' ;
5059
+
5060
+ /* global self btoa atob window CompressionStream Response */
5061
+
5062
+ const btoa_ = typeof btoa !== 'undefined' ?
5063
+ btoa :
5064
+ /** @param {string } str */
5065
+ ( str ) => Buffer . from ( str ) . toString ( 'base64' ) ;
5066
+ const atob_ = typeof atob !== 'undefined' ?
5067
+ atob :
5068
+ /** @param {string } str */
5069
+ ( str ) => Buffer . from ( str , 'base64' ) . toString ( ) ;
5070
+
5071
+ /**
5072
+ * Takes an UTF-8 string and returns a base64 encoded string.
5073
+ * If gzip is true, the UTF-8 bytes are gzipped before base64'd, using
5074
+ * CompressionStream (currently only in Chrome), falling back to pako
5075
+ * (which is only used to encode in our Node tests).
5076
+ * @param {string } string
5077
+ * @param {{gzip: boolean} } options
5078
+ * @return {Promise<string> }
5079
+ */
5080
+ async function toBase64 ( string , options ) {
5081
+ let bytes = new TextEncoder ( ) . encode ( string ) ;
5082
+
5083
+ if ( options . gzip ) {
5084
+ if ( typeof CompressionStream !== 'undefined' ) {
5085
+ const cs = new CompressionStream ( 'gzip' ) ;
5086
+ const writer = cs . writable . getWriter ( ) ;
5087
+ writer . write ( bytes ) ;
5088
+ writer . close ( ) ;
5089
+ const compAb = await new Response ( cs . readable ) . arrayBuffer ( ) ;
5090
+ bytes = new Uint8Array ( compAb ) ;
5091
+ } else {
5092
+ /** @type {import('pako')= } */
5093
+ const pako = window . pako ;
5094
+ bytes = pako . gzip ( string ) ;
5095
+ }
5096
+ }
5097
+
5098
+ let binaryString = '' ;
5099
+ // This is ~25% faster than building the string one character at a time.
5100
+ // https://jsbench.me/2gkoxazvjl
5101
+ const chunkSize = 5000 ;
5102
+ for ( let i = 0 ; i < bytes . length ; i += chunkSize ) {
5103
+ binaryString += String . fromCharCode ( ...bytes . subarray ( i , i + chunkSize ) ) ;
5104
+ }
5105
+ return btoa_ ( binaryString ) ;
5106
+ }
5107
+
5108
+ /**
5109
+ * @param {string } encoded
5110
+ * @param {{gzip: boolean} } options
5111
+ * @return {string }
5112
+ */
5113
+ function fromBase64 ( encoded , options ) {
5114
+ const binaryString = atob_ ( encoded ) ;
5115
+ const bytes = Uint8Array . from ( binaryString , c => c . charCodeAt ( 0 ) ) ;
5116
+
5117
+ if ( options . gzip ) {
5118
+ /** @type {import('pako')= } */
5119
+ const pako = window . pako ;
5120
+ return pako . ungzip ( bytes , { to : 'string' } ) ;
5121
+ } else {
5122
+ return new TextDecoder ( ) . decode ( bytes ) ;
5123
+ }
5124
+ }
5125
+
5126
+ if ( typeof module !== 'undefined' && module . exports ) {
5127
+ module . exports = { toBase64, fromBase64} ;
5128
+ } else {
5129
+ self . TextEncoding = { toBase64, fromBase64} ;
5130
+ }
0 commit comments