@enact/ui-test-utils 0.5.4 → 0.8.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.
@@ -1,4 +1,5 @@
1
1
  const parseArgs = require('minimist');
2
+ const {execSync} = require('child_process');
2
3
 
3
4
  const args = parseArgs(process.argv);
4
5
 
@@ -14,6 +15,40 @@ module.exports.configure = (options) => {
14
15
  delete opts.before;
15
16
  delete opts.services;
16
17
 
18
+ if (!process.env.CHROME_DRIVER) {
19
+ if (process.env.TV_IP && process.argv.find(arg => arg.includes('tv.conf'))) {
20
+ process.env.CHROME_DRIVER = 2.44; // Currently, TV supports 83 and lower, but keep the previous version for safety.
21
+ } else {
22
+ let chromeVersionMajorNumber;
23
+ try {
24
+ if (process.platform === 'win32') {
25
+ // Windows
26
+ const chromeVersion = /\d+/.exec(execSync('wmic datafile where "name=\'C:\\\\Program Files\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe\'" get Version /value').toString());
27
+ chromeVersionMajorNumber = (chromeVersion && chromeVersion[0]);
28
+ } else if (process.platform === 'darwin') {
29
+ // Mac
30
+ const chromeVersion = /Chrome (\d+)/.exec(execSync('/Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome --version'));
31
+ chromeVersionMajorNumber = (chromeVersion && chromeVersion[1]);
32
+ } else {
33
+ const chromeVersion = /Chrome (\d+)/.exec(execSync('google-chrome -version'));
34
+ chromeVersionMajorNumber = (chromeVersion && chromeVersion[1]);
35
+ }
36
+ const chromeDriverVersion = execSync('curl https://chromedriver.storage.googleapis.com/LATEST_RELEASE' + (chromeVersionMajorNumber ? ('_' + chromeVersionMajorNumber) : ''));
37
+
38
+ if (chromeDriverVersion.includes('Error') || !/\d+.\d+.\d+.\d+/.exec(chromeDriverVersion)) {
39
+ throw new Error();
40
+ } else {
41
+ process.env.CHROME_DRIVER = chromeDriverVersion;
42
+ }
43
+ } catch (error) {
44
+ console.log('ERROR: Cannnot find Chrome driver from Chrome ' + chromeVersionMajorNumber);
45
+ process.env.CHROME_DRIVER = 2.44;
46
+ }
47
+ }
48
+
49
+ console.log('Chrome Driver Version : ' + process.env.CHROME_DRIVER);
50
+ }
51
+
17
52
  return Object.assign(
18
53
  opts,
19
54
  {
@@ -72,7 +107,7 @@ module.exports.configure = (options) => {
72
107
  //
73
108
  browserName: 'chrome',
74
109
  'goog:chromeOptions': visibleBrowser ? {} : {
75
- args: ['--headless', '--window-size=1920,1280']
110
+ args: ['--headless', '--window-size=1920,1080']
76
111
  }
77
112
  }],
78
113
  //
@@ -138,7 +173,7 @@ module.exports.configure = (options) => {
138
173
  args: {
139
174
  drivers : {
140
175
  chrome : {
141
- version : '2.44',
176
+ version : process.env.CHROME_DRIVER,
142
177
  arch : process.arch
143
178
  }
144
179
  }
@@ -146,7 +181,7 @@ module.exports.configure = (options) => {
146
181
  installArgs: {
147
182
  drivers : {
148
183
  chrome : {
149
- version : '2.44',
184
+ version : process.env.CHROME_DRIVER,
150
185
  arch : process.arch,
151
186
  baseURL : 'https://chromedriver.storage.googleapis.com'
152
187
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enact/ui-test-utils",
3
- "version": "0.5.4",
3
+ "version": "0.8.0",
4
4
  "description": "UI Testing for the Enact framework",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -16,40 +16,38 @@
16
16
  "access": "public"
17
17
  },
18
18
  "dependencies": {
19
- "@wdio/cli": "^6.1.17",
20
- "@wdio/dot-reporter": "^6.1.14",
21
- "@wdio/local-runner": "^6.1.17",
22
- "@wdio/mocha-framework": "^6.1.17",
23
- "@wdio/selenium-standalone-service": "^6.1.14",
24
- "@wdio/spec-reporter": "^6.1.14",
25
- "@wdio/static-server-service": "^6.1.14",
26
- "@wdio/sync": "^6.1.14",
27
- "chai": "4.2.0",
28
- "chalk": "^4.1.0",
19
+ "@wdio/cli": "^7.9.0",
20
+ "@wdio/dot-reporter": "^7.9.0",
21
+ "@wdio/local-runner": "^7.9.0",
22
+ "@wdio/mocha-framework": "^7.9.0",
23
+ "@wdio/selenium-standalone-service": "^7.8.0",
24
+ "@wdio/spec-reporter": "^7.9.0",
25
+ "@wdio/static-server-service": "^7.8.0",
26
+ "chai": "4.3.4",
27
+ "chalk": "^4.1.2",
29
28
  "cross-spawn": "^7.0.3",
30
29
  "dirty-chai": "2.0.1",
31
- "expect-webdriverio": "^1.2.0",
32
- "fs-extra": "^9.0.1",
30
+ "expect-webdriverio": "^3.1.0",
31
+ "fs-extra": "^10.0.0",
33
32
  "minimist": "^1.2.5",
34
- "query-string": "^6.13.1",
35
- "ramda": "^0.27.0",
36
- "readdirp": "^3.4.0",
37
- "wdio-docker-service": "^2.3.0",
38
- "wdio-novus-visual-regression-service": "^1.2.0",
39
- "webdriverio": "^6.1.7"
33
+ "query-string": "^7.0.1",
34
+ "ramda": "^0.27.1",
35
+ "readdirp": "^3.6.0",
36
+ "wdio-docker-service": "^3.1.2",
37
+ "wdio-image-comparison-service": "^3.0.0",
38
+ "webdriverio": "^7.9.0"
40
39
  },
41
40
  "devDependencies": {
42
41
  "babel-eslint": "^10.1.0",
43
- "eslint": "^7.4.0",
44
- "eslint-config-enact": "^2.0.0",
42
+ "eslint": "^7.11.0",
43
+ "eslint-config-enact": "^3.0.0",
45
44
  "eslint-config-prettier": "^6.11.0",
46
45
  "eslint-plugin-babel": "^5.3.1",
47
46
  "eslint-plugin-enact": "^0.2.0",
48
47
  "eslint-plugin-import": "^2.22.0",
49
48
  "eslint-plugin-jsx-a11y": "^6.3.1",
50
49
  "eslint-plugin-prettier": "^3.1.4",
51
- "eslint-plugin-react": "^7.20.3",
52
- "eslint-plugin-react-hooks": "^4.0.8",
53
- "react": "^16.8.6"
50
+ "eslint-plugin-react": "^7.21.5",
51
+ "eslint-plugin-react-hooks": "^4.2.0"
54
52
  }
55
53
  }
@@ -1,5 +1,3 @@
1
- import React from 'react';
2
-
3
1
  import css from './TestChooser.module.css';
4
2
 
5
3
  // eslint-disable-next-line enact/prop-types
@@ -1,4 +1,3 @@
1
- import React from 'react';
2
1
  import {render} from 'react-dom';
3
2
  import App, {testMetadata} from 'UI_TEST_APP_ENTRY';
4
3
  import TestChooser from './TestChooser';
@@ -2,7 +2,6 @@ const crypto = require('crypto'),
2
2
  path = require('path'),
3
3
  fs = require('fs');
4
4
 
5
- const VisualRegressionCompare = require('wdio-novus-visual-regression-service/compare');
6
5
  const buildApps = require('../../src/build-apps');
7
6
  const makeHeader = require('./headerTemplate');
8
7
 
@@ -23,21 +22,18 @@ function getScreenshotName (basePath) {
23
22
 
24
23
  // shorten the name with a little bit of leading context to help find the file manually if necessary
25
24
  testName = testName.substring(0, 128) + '-' + crypto.createHash('md5').update(testName).digest('hex');
26
-
27
- return path.join(basePath, ...testNameParts, `${testName}.png`);
25
+ let screenshotFileName = path.join(basePath, ...testNameParts, `${testName}.png`);
26
+ return screenshotFileName.replace(/ /g, '_');
28
27
  };
29
28
  }
30
29
 
31
- const generateReferenceName = getScreenshotName(path.join(process.cwd(), 'tests/screenshot/dist/screenshots/reference')),
32
- generateScreenshotName = getScreenshotName(path.join(process.cwd(), 'tests/screenshot/dist/screenshots/screen')),
33
- generateDiffName = getScreenshotName(path.join(process.cwd(), 'tests/screenshot/dist/screenshots/diff'));
30
+ const distPath = path.join(process.cwd(), 'tests', 'screenshot', 'dist');
31
+ const baselineRelativePath = 'screenshots/reference';
32
+ const screenshotRelativePath = 'screenshots/screen';
33
+ const baselineFolder = path.join(distPath, baselineRelativePath);
34
+ const screenshotFolder = path.join(distPath, screenshotRelativePath);
34
35
 
35
- const comparator = new VisualRegressionCompare.LocalCompare({
36
- referenceName: generateReferenceName,
37
- screenshotName: generateScreenshotName,
38
- diffName: generateDiffName,
39
- misMatchTolerance: 0.005
40
- });
36
+ const generateReferenceName = getScreenshotName(baselineFolder);
41
37
 
42
38
  function initFile (name, content) {
43
39
  const dir = path.dirname(name);
@@ -77,16 +73,16 @@ function beforeTest (testData) {
77
73
  function afterTest (testData, _context, {passed}) {
78
74
  // If this doesn't include context data, not a screenshot test
79
75
  if (testData && testData.title && testData.context && testData.context.params) {
76
+ const fileName = testData.context.fileName.replace(/ /g, '_') + '.png';
77
+ const referencePath = path.join(baselineRelativePath, fileName);
78
+
80
79
  if (_context.isNewScreenshot) {
81
- const filename = generateReferenceName({test: testData});
82
80
  fs.open(newScreenshotFilename, 'a', (err, fd) => {
83
- const distPath = path.join(process.cwd(), 'tests', 'screenshot', 'dist'),
84
- relativeName = path.relative(distPath, filename);
85
81
  if (err) {
86
82
  console.error('Unable to create log file!');
87
83
  } else {
88
84
  const {params, url} = testData.context;
89
- const output = {title: testData.title.replace(/~\//g, '/'), path: relativeName, params, url};
85
+ const output = {title: testData.title.replace(/~\//g, '/'), path: referencePath, params, url};
90
86
  fs.appendFile(fd, `${JSON.stringify(output)},`, 'utf8', () => {
91
87
  fs.close(fd);
92
88
  });
@@ -95,11 +91,9 @@ function afterTest (testData, _context, {passed}) {
95
91
  }
96
92
 
97
93
  if (!passed) {
94
+ const screenPath = path.join(screenshotRelativePath, 'actual', fileName);
95
+ const diffPath = path.join(screenshotRelativePath, 'diff', fileName);
98
96
  fs.open(failedScreenshotFilename, 'a', (err, fd) => {
99
- const distPath = path.join(process.cwd(), 'tests', 'screenshot', 'dist'),
100
- diffPath = path.relative(distPath, generateDiffName({test: testData})),
101
- referencePath = path.relative(distPath, generateReferenceName({test: testData})),
102
- screenPath = path.relative(distPath, generateScreenshotName({test: testData}));
103
97
  if (err) {
104
98
  console.error('Unable to create failed test log file!');
105
99
  } else {
@@ -115,7 +109,6 @@ function afterTest (testData, _context, {passed}) {
115
109
  }
116
110
  }
117
111
 
118
-
119
112
  function onComplete () {
120
113
  const {size: newSize} = fs.statSync(newScreenshotFilename),
121
114
  {size: failedSize} = fs.statSync(failedScreenshotFilename);
@@ -141,8 +134,9 @@ function onComplete () {
141
134
 
142
135
  module.exports = {
143
136
  afterTest,
137
+ baselineFolder,
144
138
  beforeTest,
145
- comparator,
146
139
  onComplete,
147
- onPrepare
140
+ onPrepare,
141
+ screenshotFolder
148
142
  };
@@ -1,16 +1,15 @@
1
- // const visualRegression = require('wdio-novus-visual-regression-service');
2
1
  const {configure} = require('../config/wdio.conf.js');
3
- const {afterTest, beforeTest, comparator, onComplete, onPrepare} = require('./utils/confHelpers.js');
2
+ const {afterTest, baselineFolder, beforeTest, onComplete, onPrepare, screenshotFolder} = require('./utils/confHelpers.js');
4
3
 
5
4
  exports.config = configure({
6
5
  base: 'screenshot',
7
6
  services: [[
8
- 'novus-visual-regression',
7
+ 'image-comparison',
9
8
  {
10
- compare: comparator,
11
- viewportChangePause: 300,
12
- viewports: [{width: 1920, height: 1080}],
13
- orientations: ['landscape']
9
+ baselineFolder: baselineFolder,
10
+ formatImageName: '{tag}',
11
+ screenshotPath: screenshotFolder,
12
+ autoSaveBaseline: true
14
13
  }
15
14
  ]],
16
15
  //
@@ -1,10 +1,19 @@
1
1
  const ipAddress = require('../utils/ipAddress.js');
2
2
  const {config} = require('./wdio.conf.js');
3
3
 
4
+ const services = config.services.map(service => {
5
+ if (service[0] === 'novus-visual-regression') {
6
+ delete service[1].viewports;
7
+ }
8
+ return service;
9
+ });
10
+
4
11
  exports.config = Object.assign(
5
12
  {},
6
13
  config,
7
14
  {
15
+ services,
16
+
8
17
  // ============
9
18
  // Capabilities
10
19
  // ============
@@ -25,6 +34,7 @@ exports.config = Object.assign(
25
34
  //
26
35
  browserName: 'chrome',
27
36
  'goog:chromeOptions': {
37
+ w3c: false,
28
38
  debuggerAddress: `${process.env.TV_IP}:9998`
29
39
  }
30
40
  }],
@@ -44,6 +54,9 @@ exports.config = Object.assign(
44
54
  if (config.before) {
45
55
  config.before();
46
56
  }
57
+
58
+ browser._options = {remote: true};
59
+
47
60
  // Have to stub out these methods to prevent exceptions when running against
48
61
  // remote chrome session
49
62
  browser.setViewportSize = () => Promise.resolve({height: 1080, width: 1920});
package/ui/index.js CHANGED
@@ -1,4 +1,3 @@
1
- import React from 'react';
2
1
  import {render} from 'react-dom';
3
2
  import App from 'UI_TEST_APP_ENTRY';
4
3
 
@@ -3,7 +3,7 @@ const {config} = require('./wdio.conf.js');
3
3
 
4
4
  // Remove selenium-standalone and replace with docker service
5
5
  const services = config.services
6
- .filter(service => service !== 'selenium-standalone')
6
+ .filter(service => service[0] !== 'selenium-standalone')
7
7
  .concat(['docker']);
8
8
 
9
9
  exports.config = Object.assign(
@@ -10,6 +10,7 @@ exports.config = Object.assign(
10
10
 
11
11
  browserName: 'chrome',
12
12
  'goog:chromeOptions': {
13
+ w3c: false,
13
14
  debuggerAddress: `${process.env.TV_IP}:9998`
14
15
  }
15
16
  }],
@@ -17,7 +18,10 @@ exports.config = Object.assign(
17
18
  baseUrl: `http://${ipAddress()}:4567`,
18
19
 
19
20
  before: function () {
20
- if (config.before) config.before();
21
+ if (config.before) {
22
+ config.before();
23
+ }
24
+
21
25
  browser._options = {remote: true};
22
26
  }
23
27
  }
package/utils/Page.js CHANGED
@@ -12,13 +12,17 @@ class Page {
12
12
  return this._url;
13
13
  }
14
14
 
15
- open (appPath, urlExtra = '?locale=en-US') {
15
+ async open (appPath, urlExtra = '?locale=en-US') {
16
16
  this._url = `/${appPath}/${urlExtra}`;
17
17
  // Can't resize browser window when connected to remote debugger!
18
18
  if (!browser._options || !browser._options.remote) {
19
- browser.setWindowSize(1920, 1080);
19
+ await browser.setWindowSize(1920, 1080);
20
20
  }
21
- browser.url(this.url);
21
+
22
+ await browser.url(this.url);
23
+
24
+ const body = await $('body');
25
+ await body.waitForExist({timeout: 1000});
22
26
  }
23
27
 
24
28
  serializeParams (params) {
@@ -27,14 +31,14 @@ class Page {
27
31
  return queryObject;
28
32
  }
29
33
 
30
- delay (delay = 1000) {
31
- browser.pause(delay);
34
+ async delay (delay = 1000) {
35
+ await browser.pause(delay);
32
36
  return browser;
33
37
  }
34
- keyDelay (key, delay = 50) {
35
- browser.keys(key);
36
- browser.pause(delay);
37
- return browser;
38
+ async keyDelay (key, delay = 50) {
39
+ await browser.keys(key);
40
+ await browser.pause(delay);
41
+ return await browser;
38
42
  }
39
43
  spotlightLeft () {
40
44
  return this.keyDelay('Left arrow');
@@ -64,25 +68,25 @@ class Page {
64
68
  }
65
69
 
66
70
  // For testing "pointer off" by timeout.
67
- hidePointerByKeycode () {
71
+ async hidePointerByKeycode () {
68
72
  browser.execute(function () {
69
73
  const event = document.createEvent('Events');
70
74
  event.initEvent('keydown', true, true);
71
75
  event.keyCode = 1537;
72
76
  document.getElementById('root').dispatchEvent(event);
73
77
  });
74
- this.delay();
78
+ await this.delay();
75
79
  return browser;
76
80
  }
77
81
 
78
- showPointerByKeycode () {
79
- browser.execute(function () {
82
+ async showPointerByKeycode () {
83
+ await browser.execute(function () {
80
84
  const event = document.createEvent('Events');
81
85
  event.initEvent('keydown', true, true);
82
86
  event.keyCode = 1536;
83
87
  document.getElementById('root').dispatchEvent(event);
84
88
  });
85
- this.delay();
89
+ await this.delay();
86
90
  return browser;
87
91
  }
88
92
 
@@ -128,12 +132,12 @@ class Page {
128
132
  * @param {Number} [config.timeout=1200] Time to wait for focus condition
129
133
  * @param {Number} [config.interval=200] Time between checks
130
134
  */
131
- waitForFocused (target, {targetName = 'item', timeoutMsg = `timed out waiting for ${targetName} focused`, timeout = 1200, interval = 200} = {}) {
132
- browser.waitUntil(() => target.isExisting() && target.isFocused(), {timeout, timeoutMsg, interval});
135
+ async waitForFocused (target, {targetName = 'item', timeoutMsg = `timed out waiting for ${targetName} focused`, timeout = 1200, interval = 200} = {}) {
136
+ await browser.waitUntil(() => target.isExisting() && target.isFocused(), {timeout, timeoutMsg, interval});
133
137
  }
134
138
 
135
- waitTransitionEnd (delay = 3000, msg = 'timed out waiting for transitionend', callback, ignore = ['opacity', 'filter']) {
136
- browser.execute(
139
+ async waitTransitionEnd (delay = 3000, msg = 'timed out waiting for transitionend', callback, ignore = ['opacity', 'filter']) {
140
+ await browser.execute(
137
141
  // eslint-disable-next-line no-shadow
138
142
  function (ignore) {
139
143
  window.ontransitionend = function (evt) {
@@ -148,7 +152,7 @@ class Page {
148
152
  if (callback) {
149
153
  callback();
150
154
  }
151
- browser.waitUntil(
155
+ await browser.waitUntil(
152
156
  function () {
153
157
  return browser.execute(
154
158
  function () {
package/utils/runTest.js CHANGED
@@ -1,4 +1,5 @@
1
1
  'use strict';
2
+ const crypto = require('crypto');
2
3
  const parseArgs = require('minimist');
3
4
 
4
5
  const args = parseArgs(process.argv);
@@ -14,13 +15,14 @@ const runTest = ({concurrency, filter, Page, testName, ...rest}) => {
14
15
  }
15
16
 
16
17
  describe(testName, function () {
17
- it('should fetch test cases', function () {
18
- Page.open('?request');
19
- let testCases = browser.execute(function () {
20
- return window.__TEST_DATA; // eslint-disable-line no-undef
18
+ it('should fetch test cases', async function () {
19
+ await Page.open('?request');
20
+
21
+ let testCases = await browser.execute(async function () {
22
+ return await window.__TEST_DATA; // eslint-disable-line no-undef
21
23
  });
22
24
 
23
- expect(testCases).to.be.an('object', 'Test data failed to load');
25
+ await expect(testCases).to.be.an('object', 'Test data failed to load');
24
26
 
25
27
  describe(testName, function () {
26
28
  for (const component in testCases) {
@@ -42,18 +44,30 @@ const runTest = ({concurrency, filter, Page, testName, ...rest}) => {
42
44
  if (titlePattern && !testCase.title.match(titlePattern)) {
43
45
  return;
44
46
  }
45
- it(`${component}~/${testName}~/${testCase.title}`, function () {
47
+ it(`${component}~/${testName}~/${testCase.title}`, async function () {
46
48
  const params = Page.serializeParams(Object.assign({
47
49
  component,
48
50
  testId
49
51
  }, rest));
50
52
 
51
- Page.open(`?${params}`);
53
+ const testNameParts = testCase.title.split('~/');
54
+ let testCaseName = testNameParts.pop();
55
+ // Replace problematic filenames. Windows is much more restrictive.
56
+ testCaseName = testCaseName.replace(/[/\\:?*"|<>]/g, '_');
57
+ // shorten the name with a little bit of leading context to help find the file manually if necessary
58
+ testCaseName = testCaseName.substring(0, 128) + '-' + crypto.createHash('md5').update(testCaseName).digest('hex');
59
+ const screenshotFileName = (component + '/' + testName + '/' + testCaseName);
52
60
 
53
- const context = {params, component, testName, url: Page.url};
61
+ const context = {params, component, testName, url: Page.url, fileName: screenshotFileName};
54
62
  this.test.context = context;
55
- const result = browser.checkDocument();
56
- expect(result[0].isWithinMisMatchTolerance).to.be.true();
63
+
64
+ await Page.open(`?${params}`);
65
+
66
+ expect(await browser.checkScreen(screenshotFileName, {
67
+ disableCSSAnimation: true,
68
+ ignoreNothing: true,
69
+ rawMisMatchPercentage: true
70
+ })).to.equal(0);
57
71
  });
58
72
  });
59
73
  });
package/utils/selector.js CHANGED
@@ -42,13 +42,16 @@ const getSubComponent = curry((opts, el) => element(
42
42
 
43
43
  // Given two elements, determine if the first element is to the left of the second element
44
44
  // element, element => Boolean
45
- function expectOrdering (firstElement, secondElement) {
46
- expect(firstElement.getLocation().x < secondElement.getLocation().x).to.be.true();
45
+ async function expectOrdering (firstElement, secondElement) {
46
+ expect(await firstElement.getLocation().x < secondElement.getLocation().x).to.be.true();
47
47
  }
48
48
 
49
- const hasClass = curry((className, el) => {
50
- if (className[0] === '.') className = className.slice(1);
51
- return el.getAttribute('className').includes(className);
49
+ const hasClass = curry(async (className, el) => {
50
+ if (className[0] === '.') {
51
+ className = className.slice(1);
52
+ }
53
+ const elementClass = await el.getAttribute('className') || await el.getAttribute('class');
54
+ return elementClass.includes(className);
52
55
  });
53
56
 
54
57
  module.exports = {