browsertime 14.16.0 → 14.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # Browsertime changelog (we do [semantic versioning](https://semver.org))
2
2
 
3
+ ## 14.19.0 - 2022-02-02
4
+
5
+ ### Added
6
+ * Chrome and Chromedriver 98 [#1704](https://github.com/sitespeedio/browsertime/pull/1704).
7
+
8
+ ### Fixed
9
+ * Fix so we get the Firefox version on Android [#1704](https://github.com/sitespeedio/browsertime/pull/1704).
10
+
11
+ ## 14.18.1 - 2022-01-24
12
+ ### Fixed
13
+ * If loading a URL failed and we retry and we logged that as an info message, but you as a user only need to know if it fails after X retries. The log message now logs at debug level [#1701](https://github.com/sitespeedio/browsertime/pull/1701).
14
+ * The summary log message `[2022-01-24 16:12:38] INFO: https://www.sitespeed.io 27 requests, TTFB: 962ms (σ917.00ms), firstPaint: 2.92s (σ3.07s), firstVisualChange: 2.94s (σ3.07s), FCP: 2.92s (σ3.07s), DOMContentLoaded: 3.22s (σ2.98s), LCP: 2.92s (σ3.07s), CLS: 0.0389 (σ0.03), TBT: 769ms (σ53.00ms), Load: 3.77s (σ3.12s), speedIndex: 2.96s (σ3.07s), visualComplete85: 2.95s (σ3.06s), lastVisualChange: 4.54s (σ3.30s) (21 runs)` that summaries all the runs used mean instead of median metric. That sucks when you do many runs and want to compare them. That is now fixed to show median number instead [#1700](https://github.com/sitespeedio/browsertime/pull/1700).
15
+
16
+ ## 14.18.0 - 2022-01-24
17
+ ### Added
18
+ * Updated to Edge stable in the Docker container.
19
+
20
+ ### Fixed
21
+ * A more safe way to get dockumentURI for Firefox. See PR [#1699](https://github.com/sitespeedio/browsertime/pull/1699) and bug [#1698](https://github.com/sitespeedio/browsertime/issues/1698).
22
+
23
+ ## 14.17.0 - 2022-01-23
24
+ ### Added
25
+ * New Select command [#1696](https://github.com/sitespeedio/browsertime/pull/1696):
26
+ * `select.selectByIdValue(selectId, value)`
27
+ * `select.selectByNameAndValue(selectName, value)`
28
+ * `select.selectByIdAndIndex(selectId, index)`
29
+ * `select.selectByNameAndIndex(selectName, index)`
30
+ * `select.deselectById(selectId)`
31
+ * `select.getValuesById(selectId)`
32
+ * `select.getSelectedValueById(selectId)`
33
+
34
+ * New click by name command `click.byName(name)` [#1697](https://github.com/sitespeedio/browsertime/pull/1697).
35
+
36
+ ### Fixed
37
+ * Remove the top 10 rows of the image to handle progress bars on some mobile browser recordings. Thank you [Gregory Mierzwinski](https://github.com/gmierz) for PR [#1687](https://github.com/sitespeedio/browsertime/pull/1687).
38
+
3
39
  ## 14.16.0 - 2022-01-14
4
40
  ### Added
5
41
  * Upgraded Geckodriver downloader that downloads a pre-built Geckodriver on Raspberry Pis.
@@ -89,6 +89,7 @@ def video_to_frames(
89
89
  if os.path.isfile(video):
90
90
  video = os.path.realpath(video)
91
91
  logging.info("Processing frames from video " + video + " to " + directory)
92
+ is_mobile = find_recording_platform(video)
92
93
  if os.path.isdir(directory):
93
94
  shutil.rmtree(directory, True)
94
95
  if not os.path.isdir(directory):
@@ -103,13 +104,15 @@ def video_to_frames(
103
104
  viewport_retries,
104
105
  viewport_min_height,
105
106
  viewport_min_width,
107
+ is_mobile,
106
108
  )
107
109
  gc.collect()
108
110
  if extract_frames(video, directory, full_resolution, viewport):
109
111
  client_viewport = None
110
112
  if find_viewport and options.notification:
111
113
  client_viewport = find_image_viewport(
112
- os.path.join(directory, "video-000000.png")
114
+ os.path.join(directory, "video-000000.png"),
115
+ is_mobile
113
116
  )
114
117
  if multiple and orange_file is not None:
115
118
  directories = split_videos(directory, orange_file)
@@ -122,12 +125,12 @@ def video_to_frames(
122
125
  remove_orange_frames(dir, orange_file)
123
126
  find_first_frame(dir, white_file)
124
127
  blank_first_frame(dir)
125
- find_render_start(dir, orange_file, gray_file, cropped)
128
+ find_render_start(dir, orange_file, gray_file, cropped, is_mobile)
126
129
  find_last_frame(dir, white_file)
127
130
  adjust_frame_times(dir)
128
131
  if timeline_file is not None and not multiple:
129
132
  synchronize_to_timeline(dir, timeline_file)
130
- eliminate_duplicate_frames(dir, cropped)
133
+ eliminate_duplicate_frames(dir, cropped, is_mobile)
131
134
  eliminate_similar_frames(dir)
132
135
  # See if we are limiting the number of frames to keep
133
136
  # (before processing them to save processing time)
@@ -202,6 +205,33 @@ def extract_frames(video, directory, full_resolution, viewport):
202
205
  return ret
203
206
 
204
207
 
208
+ def find_recording_platform(video):
209
+ """Find the platform that this video was recorded on.
210
+
211
+ We can make use of a field called `com.android.version` to
212
+ determine if we've recorded on mobile or not.
213
+ """
214
+ command = ["ffprobe", video]
215
+ logging.debug(command)
216
+
217
+ lines = []
218
+ if sys.version_info > (3, 0):
219
+ proc = subprocess.Popen(command, stderr=subprocess.PIPE, encoding="UTF-8")
220
+ else:
221
+ proc = subprocess.Popen(command, stderr=subprocess.PIPE)
222
+
223
+ while proc.poll() is None:
224
+ lines.extend(iter(proc.stderr.readline, ""))
225
+
226
+ is_mobile = False
227
+ matcher = re.compile(".*com\.android\.version.*")
228
+ for line in lines:
229
+ if matcher.search(line):
230
+ is_mobile = True
231
+
232
+ return is_mobile
233
+
234
+
205
235
  def split_videos(directory, orange_file):
206
236
  """Split multiple videos on orange frame separators"""
207
237
  logging.debug("Splitting video on orange frames (this may take a while)...")
@@ -291,7 +321,7 @@ def remove_orange_frames(directory, orange_file):
291
321
  break
292
322
 
293
323
 
294
- def find_image_viewport(file):
324
+ def find_image_viewport(file, is_mobile):
295
325
  logging.debug("Finding the viewport for %s", file)
296
326
  try:
297
327
  from PIL import Image
@@ -357,6 +387,12 @@ def find_image_viewport(file):
357
387
  "height": (bottom - top),
358
388
  }
359
389
 
390
+ if is_mobile:
391
+ # On mobile we need to ignore the top ~10 pixels because
392
+ # there is a visible progress bar there on some browsers.
393
+ viewport["y"] += 10
394
+ viewport["height"] -= 10
395
+
360
396
  except Exception:
361
397
  viewport = None
362
398
 
@@ -371,6 +407,7 @@ def find_video_viewport(
371
407
  viewport_retries,
372
408
  viewport_min_height,
373
409
  viewport_min_width,
410
+ is_mobile,
374
411
  ):
375
412
  logging.debug("Finding Video Viewport...")
376
413
  viewport = None
@@ -463,7 +500,7 @@ def find_video_viewport(
463
500
  }
464
501
 
465
502
  elif find_viewport:
466
- viewport = find_image_viewport(frame)
503
+ viewport = find_image_viewport(frame, is_mobile)
467
504
  else:
468
505
  viewport = {"x": 0, "y": 0, "width": width, "height": height}
469
506
 
@@ -629,7 +666,7 @@ def find_last_frame(directory, white_file):
629
666
  logging.exception("Error finding last frame")
630
667
 
631
668
 
632
- def find_render_start(directory, orange_file, gray_file, cropped):
669
+ def find_render_start(directory, orange_file, gray_file, cropped, is_mobile):
633
670
  logging.debug("Finding Render Start...")
634
671
  try:
635
672
  if (
@@ -680,13 +717,19 @@ def find_render_start(directory, orange_file, gray_file, cropped):
680
717
  top += client_viewport["y"]
681
718
  elif cropped:
682
719
  # The image was already cropped, so only cutout the bottom
683
- # to get rid of the network request/etc. information
720
+ # to get rid of the network request/etc. information for
721
+ # desktop videos, and nothing extra on mobile.
684
722
  top = 0
685
723
  left = 0
686
724
  width = im_width
687
- height = im_height - bottom_margin
725
+
726
+ if is_mobile:
727
+ height = im_height
728
+ else:
729
+ height = im_height - bottom_margin
688
730
 
689
731
  crop = "{0:d}x{1:d}+{2:d}+{3:d}".format(width, height, left, top)
732
+
690
733
  for i in range(1, count):
691
734
  if frames_match(first, files[i], 10, 0, crop, mask):
692
735
  logging.debug("Removing pre-render frame %s", files[i])
@@ -705,7 +748,7 @@ def find_render_start(directory, orange_file, gray_file, cropped):
705
748
  logging.exception("Error getting render start")
706
749
 
707
750
 
708
- def eliminate_duplicate_frames(directory, cropped):
751
+ def eliminate_duplicate_frames(directory, cropped, is_mobile):
709
752
  logging.debug("Eliminating Duplicate Frames...")
710
753
  global client_viewport
711
754
  try:
@@ -745,11 +788,16 @@ def eliminate_duplicate_frames(directory, cropped):
745
788
  top += client_viewport["y"]
746
789
  elif cropped:
747
790
  # The image was already cropped, so only cutout the bottom
748
- # to get rid of the network request/etc. information
791
+ # to get rid of the network request/etc. information for
792
+ # desktop videos, and nothing extra on mobile.
749
793
  top = 0
750
794
  left = 0
751
795
  width = im_width
752
- height = im_height - bottom_margin
796
+
797
+ if is_mobile:
798
+ height = im_height
799
+ else:
800
+ height = im_height - bottom_margin
753
801
 
754
802
  crop = "{0:d}x{1:d}+{2:d}+{3:d}".format(width, height, left, top)
755
803
  logging.debug("Viewport cropping set to " + crop)
@@ -212,6 +212,23 @@ class Click {
212
212
  }
213
213
  }
214
214
 
215
+ /**
216
+ * Click on element located by the name, Uses document.querySelector.
217
+ * @param {string} name the name of the element
218
+ * @returns {Promise} Promise object represents when the element has been clicked
219
+ * @throws Will throw an error if the element is not found
220
+ */
221
+ async byName(name) {
222
+ try {
223
+ const script = `document.querySelector("[name='${name}']").click()`;
224
+ await this.browser.runScript(script, 'CUSTOM');
225
+ } catch (e) {
226
+ log.error('Could not find element by name %s', name);
227
+ log.verbose(e);
228
+ throw Error('Could not find element by name ' + name);
229
+ }
230
+ }
231
+
215
232
  /**
216
233
  * Click on link located by the ID attribute. Uses document.getElementById() to find the element. And wait for page complete check to finish.
217
234
  * @param {string} id
@@ -0,0 +1,155 @@
1
+ 'use strict';
2
+
3
+ const log = require('intel').getLogger('browsertime.command.select');
4
+
5
+ class Select {
6
+ constructor(browser) {
7
+ this.browser = browser;
8
+ }
9
+
10
+ /**
11
+ * Select value of a select by the selects id
12
+ * @param {string} selectId The id of the select
13
+ * @param {string} value The value of the option you want to set
14
+ * @returns {Promise} Promise object represents when the option has been
15
+ * set to the element
16
+ * @throws Will throw an error if the select is not found
17
+ */
18
+ async selectByIdAndValue(selectId, value) {
19
+ try {
20
+ const script = `const select = document.getElementById('${selectId}'); select.value = '${value}'; select.dispatchEvent(new Event('change'));`;
21
+ await this.browser.runScript(script, 'CUSTOM');
22
+ } catch (e) {
23
+ log.error('Could not select value for select with id %s', selectId);
24
+ log.verbose(e);
25
+ throw Error(`Could not select value for select with id ${selectId}`);
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Select value of a select by the selects name
31
+ * @param {string} selectName The name of the select
32
+ * @param {string} value The value of the option you want to set
33
+ * @returns {Promise} Promise object represents when the option has been
34
+ * set to the element
35
+ * @throws Will throw an error if the select is not found
36
+ */
37
+ async selectByNameAndValue(selectName, value) {
38
+ try {
39
+ const script = `const select = document.querySelector("select[name='${selectName}']"); select.value = '${value}'; select.dispatchEvent(new Event('change'));`;
40
+ await this.browser.runScript(script, 'CUSTOM');
41
+ } catch (e) {
42
+ log.error('Could not select value for select with name %s', selectName);
43
+ log.verbose(e);
44
+ throw Error(`Could not select value for select with name ${selectName}`);
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Select value of a select index and by the selects id
50
+ * @param {string} selectId The id of the select
51
+ * @param {number} index the index of the option you want to set
52
+ * @returns {Promise} Promise object represents when the option has been
53
+ * set to the element
54
+ * @throws Will throw an error if the select is not found
55
+ */
56
+ async selectByIdAndIndex(selectId, index) {
57
+ try {
58
+ const script = `const select = document.getElementById('${selectId}'); select.selectedIndex = ${index};select.dispatchEvent(new Event('change'));`;
59
+ const value = await this.browser.runScript(script, 'CUSTOM');
60
+ return value;
61
+ } catch (e) {
62
+ log.error(
63
+ 'Could not select value for select with id %s by index',
64
+ selectId
65
+ );
66
+ log.verbose(e);
67
+ throw Error(`Could not select value for select with id ${selectId}`);
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Select value of a select index and by the selects name
73
+ * @param {string} selectName - the name of the select
74
+ * @param {number} index - the index of the option you want to set
75
+ * @returns {Promise} Promise object represents when the option has been
76
+ * set to the element
77
+ * @throws Will throw an error if the select is not found
78
+ */
79
+ async selectByNameAndIndex(selectName, index) {
80
+ try {
81
+ const script = `const select = document.querySelector("select[name='${selectName}']"); select.selectedIndex = ${index};select.dispatchEvent(new Event('change'));`;
82
+ const value = await this.browser.runScript(script, 'CUSTOM');
83
+ return value;
84
+ } catch (e) {
85
+ log.error(
86
+ 'Could not select value for select with name %s by index',
87
+ selectName
88
+ );
89
+ log.verbose(e);
90
+ throw Error(`Could not select value for select name ${selectName}`);
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Deselect all options in a select.
96
+ * @param {string} selectId
97
+ * @returns {Promise} Promise object represents when options been deselected
98
+ * @throws Will throw an error if the select is not found
99
+ */
100
+ async deselectById(selectId) {
101
+ try {
102
+ const script = `const select = document.getElementById('${selectId}'); select.selectedIndex = -1;select.dispatchEvent(new Event('change'));`;
103
+ const value = await this.browser.runScript(script, 'CUSTOM');
104
+ return value;
105
+ } catch (e) {
106
+ log.error('Could not deleselect by select id %s', selectId);
107
+ log.verbose(e);
108
+ throw Error(`Could not deleselect by select id ${selectId}`);
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Get all option values in a select.
114
+ * @param {string} selectId - the id of the select.
115
+ * @returns {Promise} Promise object tha will return an array with the values of the select
116
+ * @throws Will throw an error if the select is not found
117
+ */
118
+ async getValuesById(selectId) {
119
+ const script = `const select = document.getElementById('${selectId}');
120
+ const values = [];
121
+ for (let option of select.options) {
122
+ values.push(option.value);
123
+ }
124
+ return values;
125
+ `;
126
+
127
+ try {
128
+ const value = await this.browser.runScript(script, 'CUSTOM');
129
+ return value;
130
+ } catch (e) {
131
+ log.error('Could not get select options for id %s', selectId);
132
+ log.verbose(e);
133
+ throw Error(`Could not get select options for id ${selectId}`);
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Get the selected option value in a select.
139
+ * @param {select} selectId the id of the select.
140
+ * @returns {Promise} Promise object tha will return the value of the selected option.
141
+ * @throws Will throw an error if the select is not found.
142
+ */
143
+ async getSelectedValueById(selectId) {
144
+ try {
145
+ const script = `const select = document.getElementById('${selectId}'); return select.options[select.selectedIndex].value;`;
146
+ const value = await this.browser.runScript(script, 'CUSTOM');
147
+ return value;
148
+ } catch (e) {
149
+ log.error('Could not select value for select with id %s', selectId);
150
+ log.verbose(e);
151
+ throw Error(`Could not select value for select with id ${selectId}`);
152
+ }
153
+ }
154
+ }
155
+ module.exports = Select;
@@ -345,7 +345,19 @@ class Engine {
345
345
  // Just swallow
346
346
  }
347
347
  }
348
+ } // We don't have a HAR for Firefox on Android
349
+ else if (options.browser === 'firefox' && options.android) {
350
+ for (let result of totalResult) {
351
+ result.info.browser.name = 'Firefox';
352
+ try {
353
+ const vString = result.info.browser.userAgent.split('Firefox/')[1];
354
+ result.info.browser.version = vString;
355
+ } catch (e) {
356
+ // Just swallow
357
+ }
358
+ }
348
359
  }
360
+
349
361
  util.logResultLogLine(totalResult);
350
362
 
351
363
  if (failures.length > 0) {
@@ -21,6 +21,7 @@ const Set = require('./command/set.js');
21
21
  const Cache = require('./command/cache.js');
22
22
  const Meta = require('./command/meta.js');
23
23
  const StopWatch = require('./command/stopWatch.js');
24
+ const Select = require('./command/select.js');
24
25
  const AndroidCommand = require('./command/android.js');
25
26
  const ChromeDevToolsProtocol = require('./command/chromeDevToolsProtocol.js');
26
27
  const { addConnectivity, removeConnectivity } = require('../../connectivity');
@@ -172,7 +173,8 @@ class Iteration {
172
173
  singleClick: new SingleClick(browser, this.pageCompleteCheck),
173
174
  doubleClick: new DoubleClick(browser, this.pageCompleteCheck),
174
175
  clickAndHold: new ClickAndHold(browser)
175
- }
176
+ },
177
+ select: new Select(browser)
176
178
  };
177
179
 
178
180
  // Safari can't load data:text URLs at startup, so you can either choose
@@ -222,9 +222,23 @@ class SeleniumRunner {
222
222
  // query parameters, order fragments, etc. We don't want to change url itself 'cuz it can be
223
223
  // used for indexing collected data and it is possible that normalization could change a key.
224
224
  const normalizedURI = new URL(url).toString();
225
- const startURI = new URL(
226
- await driver.executeScript('return document.documentURI;')
227
- ).toString();
225
+
226
+ // See https://github.com/sitespeedio/browsertime/issues/1698
227
+ const retries = 5;
228
+ let documentURI;
229
+ for (var i = 0; i < retries; i++) {
230
+ documentURI = await driver.executeScript('return document.documentURI;');
231
+ if (documentURI != null) {
232
+ break;
233
+ }
234
+ await delay(1000);
235
+ }
236
+ let startURI;
237
+ try {
238
+ startURI = new URL(documentURI).toString();
239
+ } catch (e) {
240
+ log.error(`Failed to get documentURI ${documentURI}`, e);
241
+ }
228
242
  if (this.options.webdriverPageload) {
229
243
  const clearOrange = `(function() {
230
244
  const orange = document.getElementById('browsertime-orange');
@@ -297,7 +311,7 @@ class SeleniumRunner {
297
311
  } else {
298
312
  const waitTime = (this.options.retryWaitTime || 10000) * (i + 1);
299
313
  totalWaitTime += waitTime;
300
- log.info(
314
+ log.debug(
301
315
  `URL ${url} failed to load, the ${
302
316
  this.options.browser
303
317
  } are still on ${startURI} , trying ${
@@ -982,7 +982,7 @@ module.exports.parseCommandLine = function parseCommandLine() {
982
982
  type: 'array',
983
983
  default: [0, 10, 90, 99, 100],
984
984
  describe:
985
- 'The percentile values within the data browsertime will calculate and report.'
985
+ 'The percentile values within the data browsertime will calculate and report. This argument uses Yargs arrays and you you to set them correctly it is recommended to use a configuraration file instead.'
986
986
  })
987
987
  .option('decimals', {
988
988
  type: 'number',
@@ -16,7 +16,7 @@ module.exports = {
16
16
  } else return value;
17
17
  }
18
18
 
19
- let formatted = `${name}: ${fmt(multiple ? metric.mean : metric)}`;
19
+ let formatted = `${name}: ${fmt(multiple ? metric.median : metric)}`;
20
20
  if (extras) {
21
21
  formatted += ` (σ${fmt(metric.stddev.toFixed(2))})`;
22
22
  }
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "description": "Browsertime",
3
- "version": "14.16.0",
3
+ "version": "14.19.0",
4
4
  "bin": "./bin/browsertime.js",
5
5
  "dependencies": {
6
6
  "@sitespeed.io/throttle": "3.0.0",
7
7
  "@devicefarmer/adbkit": "2.11.3",
8
8
  "btoa": "1.2.1",
9
- "@sitespeed.io/chromedriver": "97.0.4692-7b",
9
+ "@sitespeed.io/chromedriver": "98.0.4758-48",
10
10
  "chrome-har": "0.12.0",
11
11
  "chrome-remote-interface": "0.31.0",
12
12
  "dayjs": "1.10.7",