browsertime 14.14.0 → 14.17.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,29 @@
1
1
  # Browsertime changelog (we do [semantic versioning](https://semver.org))
2
2
 
3
+ ## 14.17.0 - 2022-01-23
4
+ ### Added
5
+ * New Select command [#1696](https://github.com/sitespeedio/browsertime/pull/1696):
6
+ * `select.selectByIdValue(selectId, value)`
7
+ * `select.selectByNameAndValue(selectName, value)`
8
+ * `select.selectByIdAndIndex(selectId, index)`
9
+ * `select.selectByNameAndIndex(selectName, index)`
10
+ * `select.deselectById(selectId)`
11
+ * `select.getValuesById(selectId)`
12
+ * `select.getSelectedValueById(selectId)`
13
+
14
+ * New click by name command `click.byName(name)` [#1697](https://github.com/sitespeedio/browsertime/pull/1697).
15
+
16
+
17
+ ## 14.16.0 - 2022-01-14
18
+ ### Added
19
+ * Upgraded Geckodriver downloader that downloads a pre-built Geckodriver on Raspberry Pis.
20
+
21
+ ## 14.15.0 - 2022-01-12
22
+ ### Added
23
+ * Added so you can append a String to Firefox User Agent [#1694](https://github.com/sitespeedio/browsertime/pull/1694), thank you [Henrik Skupin](https://github.com/whimboo) for pointing me in the right direction. With [#1695](https://github.com/sitespeedio/browsertime/pull/1695) we now have `--appendToUserAgent` that works in Chrome/Edge and Firefox.
24
+ ## 14.14.1 - 2022-01-12
25
+ ### Fixed
26
+ * Updated Chromedriver library that pickup already installed driver on Raspberry Pis. This makes it easier to run tests on an Android device from a Raspberry Pi.
3
27
  ## 14.14.0 - 2022-01-10
4
28
  ### Added
5
29
  * Add support for [Humble](https://calendar.perfplanet.com/2021/introducing-humble-the-raspberry-pi-wifi-network-link-conditioner/) as connectivity engine for mobile phone testing. Make sure to setup Humble on a Raspberry Pi 4 and the choose engine with `--connectivity.engine humble` and set the URL to your instance `--connectivity.humble.url http://raspberrypi.local:3000`. Added in [#1691](https://github.com/sitespeedio/browsertime/pull/1691).
@@ -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)
@@ -105,13 +105,15 @@ class Chromium {
105
105
  await this.cdpClient.setupCPUThrottling(this.chrome.CPUThrottlingRate);
106
106
  }
107
107
 
108
- if (this.chrome.appendToUserAgent) {
108
+ if (this.chrome.appendToUserAgent || this.options.appendToUserAgent) {
109
109
  const currentUserAgent = await runner.runScript(
110
110
  'return navigator.userAgent;',
111
111
  'GET_USER_AGENT'
112
112
  );
113
113
  await this.cdpClient.setUserAgent(
114
- currentUserAgent + ' ' + this.chrome.appendToUserAgent
114
+ currentUserAgent +
115
+ ' ' +
116
+ (this.chrome.appendToUserAgent || this.options.appendToUserAgent)
115
117
  );
116
118
  } else if (this.chrome.mobileEmulation && this.options.userAgent) {
117
119
  // Hack to set user agent for mobile emulation
@@ -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;
@@ -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
@@ -55,7 +55,21 @@ class Firefox {
55
55
  /**
56
56
  * Before the first iteration of your tests start.
57
57
  */
58
- async beforeStartIteration() {}
58
+ async beforeStartIteration(runner) {
59
+ if (
60
+ this.firefoxConfig.appendToUserAgent ||
61
+ this.options.appendToUserAgent
62
+ ) {
63
+ const currentUserAgent = await runner.runScript(
64
+ 'return navigator.userAgent;',
65
+ 'GET_USER_AGENT'
66
+ );
67
+ let script = `Services.prefs.setStringPref('general.useragent.override', '${currentUserAgent} ${
68
+ this.firefoxConfig.appendToUserAgent || this.options.appendToUserAgent
69
+ }');`;
70
+ return runner.runPrivilegedScript(script, 'SET_USER_AGENT');
71
+ }
72
+ }
59
73
 
60
74
  /**
61
75
  * Before each URL/test runs.
@@ -361,6 +361,11 @@ module.exports.parseCommandLine = function parseCommandLine() {
361
361
  group: 'firefox'
362
362
  })
363
363
 
364
+ .option('firefox.appendToUserAgent', {
365
+ type: 'string',
366
+ describe: 'Append to the user agent.',
367
+ group: 'firefox'
368
+ })
364
369
  .option('firefox.nightly', {
365
370
  describe:
366
371
  'Use Firefox Nightly. Works on OS X. For Linux you need to set the binary path.',
@@ -977,7 +982,7 @@ module.exports.parseCommandLine = function parseCommandLine() {
977
982
  type: 'array',
978
983
  default: [0, 10, 90, 99, 100],
979
984
  describe:
980
- '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.'
981
986
  })
982
987
  .option('decimals', {
983
988
  type: 'number',
@@ -1019,6 +1024,10 @@ module.exports.parseCommandLine = function parseCommandLine() {
1019
1024
  .option('userAgent', {
1020
1025
  describe: 'Override user agent'
1021
1026
  })
1027
+ .option('appendToUserAgent', {
1028
+ describe:
1029
+ 'Append a String to the user agent. Works in Chrome/Edge and Firefox.'
1030
+ })
1022
1031
  .option('silent', {
1023
1032
  alias: 'q',
1024
1033
  type: 'count',
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "description": "Browsertime",
3
- "version": "14.14.0",
3
+ "version": "14.17.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-71",
9
+ "@sitespeed.io/chromedriver": "97.0.4692-7b",
10
10
  "chrome-har": "0.12.0",
11
11
  "chrome-remote-interface": "0.31.0",
12
12
  "dayjs": "1.10.7",
@@ -24,7 +24,7 @@
24
24
  "lodash.set": "4.3.2",
25
25
  "selenium-webdriver": "4.1.0",
26
26
  "@sitespeed.io/edgedriver": "95.0.1020-30",
27
- "@sitespeed.io/geckodriver": "0.29.1-2",
27
+ "@sitespeed.io/geckodriver": "0.29.1-3",
28
28
  "@sitespeed.io/tracium": "0.3.3",
29
29
  "speedline-core": "1.4.3",
30
30
  "@cypress/xvfb": "1.2.4",