diff --git a/.gitignore b/.gitignore index 780d8dd9321..75a79012578 100644 --- a/.gitignore +++ b/.gitignore @@ -157,3 +157,5 @@ bld/ tsdoc-metadata.json *.dmp +**/errorShots +**/appium.txt \ No newline at end of file diff --git a/change/react-native-windows-2020-04-23-16-03-11-treedumpRNTester.json b/change/react-native-windows-2020-04-23-16-03-11-treedumpRNTester.json new file mode 100644 index 00000000000..3361a6f6115 --- /dev/null +++ b/change/react-native-windows-2020-04-23-16-03-11-treedumpRNTester.json @@ -0,0 +1,8 @@ +{ + "type": "prerelease", + "comment": "RNTester visual tests", + "packageName": "react-native-windows", + "email": "asklar@winse.microsoft.com", + "dependentChangeType": "patch", + "date": "2020-04-23T23:03:11.029Z" +} diff --git a/packages/playground/dist/wdio/pages/BasePage.d.ts b/packages/playground/dist/wdio/pages/BasePage.d.ts new file mode 100644 index 00000000000..5284bab4318 --- /dev/null +++ b/packages/playground/dist/wdio/pages/BasePage.d.ts @@ -0,0 +1,16 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +/// +export declare function By(testId: string): WebdriverIO.Element; +export declare function wait(timeout: number): Promise; +export declare class BasePage { + isPageLoaded(): boolean; + waitForPageLoaded(timeout?: number): void; + protected timeoutForPageLoaded(currentTimeout?: number): number; + protected get loadButton(): WebdriverIO.Element; + private get reactControlErrorMessage(); + private isPagedLoadedOrLoadBundleError; + private waitforPageTimeout; +} diff --git a/packages/playground/dist/wdio/pages/BasePage.js b/packages/playground/dist/wdio/pages/BasePage.js new file mode 100644 index 00000000000..a3ad193b30e --- /dev/null +++ b/packages/playground/dist/wdio/pages/BasePage.js @@ -0,0 +1,79 @@ +"use strict"; +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +// import { +// REACT_CONTROL_ERROR_TEST_ID, +// HOME_BUTTON, +// TREE_DUMP_RESULT, +// } from '../../app/Consts'; +Object.defineProperty(exports, "__esModule", { value: true }); +const REACT_CONTROL_ERROR_TEST_ID = 'ReactControlErrorMessage'; +function By(testId) { + return $('~' + testId); +} +exports.By = By; +function wait(timeout) { + return new Promise(resolve => { + setTimeout(resolve, timeout); + }); +} +exports.wait = wait; +class BasePage { + constructor() { + // private get treeDumpResult() { + // return By(TREE_DUMP_RESULT); + // } + // Default timeout for waitForPageLoaded command in PageObject + this.waitforPageTimeout = 10000; + } + isPageLoaded() { + return this.loadButton.isDisplayed(); + } + waitForPageLoaded(timeout) { + browser.waitUntil(() => { + return this.isPagedLoadedOrLoadBundleError(); + }, this.timeoutForPageLoaded(timeout), 'Wait for page ' + this.constructor.name + ' timeout'); + } + // getTreeDumpResult() { + // var testResult = false; + // const maxWait = 20; + // var waitCount = 1; + // do { + // testResult = this.treeDumpResult.getText() == 'TreeDump:Passed'; + // if (!testResult) { + // console.log( + // '####Waiting for treedump comparison result ' + + // waitCount + + // '/' + + // maxWait + + // '...####' + // ); + // wait(100); + // waitCount += 1; + // } + // } while (waitCount <= maxWait && !testResult); + // return testResult; + // } + timeoutForPageLoaded(currentTimeout) { + if (currentTimeout) + return currentTimeout; + return this.waitforPageTimeout; + } + get loadButton() { + return By('x_LoadButton'); + } + get reactControlErrorMessage() { + return By(REACT_CONTROL_ERROR_TEST_ID); + } + isPagedLoadedOrLoadBundleError() { + if (this.reactControlErrorMessage.isDisplayed()) { + throw "ReactControl doesn't load bundle successfully: " + + this.reactControlErrorMessage.getText(); + } + return this.isPageLoaded(); + } +} +exports.BasePage = BasePage; +//# sourceMappingURL=BasePage.js.map \ No newline at end of file diff --git a/packages/playground/dist/wdio/pages/BasePage.js.map b/packages/playground/dist/wdio/pages/BasePage.js.map new file mode 100644 index 00000000000..485aa396cb0 --- /dev/null +++ b/packages/playground/dist/wdio/pages/BasePage.js.map @@ -0,0 +1 @@ +{"version":3,"file":"BasePage.js","sourceRoot":"","sources":["../../../wdio/pages/BasePage.ts"],"names":[],"mappings":";AAAA;;;GAGG;AACH,WAAW;AACX,iCAAiC;AACjC,iBAAiB;AACjB,sBAAsB;AACtB,6BAA6B;;AAE7B,MAAM,2BAA2B,GAAG,0BAA0B,CAAC;AAE/D,SAAgB,EAAE,CAAC,MAAc;IAC/B,OAAO,CAAC,CAAC,GAAG,GAAG,MAAM,CAAC,CAAC;AACzB,CAAC;AAFD,gBAEC;AAED,SAAgB,IAAI,CAAC,OAAe;IAClC,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE;QAC3B,UAAU,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC/B,CAAC,CAAC,CAAC;AACL,CAAC;AAJD,oBAIC;AAED,MAAa,QAAQ;IAArB;QA0DE,iCAAiC;QACjC,iCAAiC;QACjC,IAAI;QAEJ,8DAA8D;QACtD,uBAAkB,GAAW,KAAK,CAAC;IAC7C,CAAC;IA/DC,YAAY;QACV,OAAO,IAAI,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC;IACvC,CAAC;IAED,iBAAiB,CAAC,OAAgB;QAChC,OAAO,CAAC,SAAS,CACf,GAAG,EAAE;YACH,OAAO,IAAI,CAAC,8BAA8B,EAAE,CAAC;QAC/C,CAAC,EACD,IAAI,CAAC,oBAAoB,CAAC,OAAO,CAAC,EAClC,gBAAgB,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,GAAG,UAAU,CACtD,CAAC;IACJ,CAAC;IAED,wBAAwB;IACxB,4BAA4B;IAC5B,wBAAwB;IACxB,uBAAuB;IACvB,SAAS;IACT,uEAAuE;IACvE,yBAAyB;IACzB,qBAAqB;IACrB,0DAA0D;IAC1D,wBAAwB;IACxB,kBAAkB;IAClB,sBAAsB;IACtB,sBAAsB;IACtB,WAAW;IACX,mBAAmB;IACnB,wBAAwB;IACxB,QAAQ;IACR,mDAAmD;IAEnD,uBAAuB;IACvB,IAAI;IAEM,oBAAoB,CAAC,cAAuB;QACpD,IAAI,cAAc;YAAE,OAAO,cAAc,CAAC;QAC1C,OAAO,IAAI,CAAC,kBAAkB,CAAC;IACjC,CAAC;IAED,IAAc,UAAU;QACtB,OAAO,EAAE,CAAC,cAAc,CAAC,CAAC;IAC5B,CAAC;IAED,IAAY,wBAAwB;QAClC,OAAO,EAAE,CAAC,2BAA2B,CAAC,CAAC;IACzC,CAAC;IAEO,8BAA8B;QACpC,IAAI,IAAI,CAAC,wBAAwB,CAAC,WAAW,EAAE,EAAE;YAC/C,MAAM,iDAAiD;gBACrD,IAAI,CAAC,wBAAwB,CAAC,OAAO,EAAE,CAAC;SAC3C;QACD,OAAO,IAAI,CAAC,YAAY,EAAE,CAAC;IAC7B,CAAC;CAQF;AAhED,4BAgEC"} \ No newline at end of file diff --git a/packages/playground/dist/wdio/pages/HomePage.d.ts b/packages/playground/dist/wdio/pages/HomePage.d.ts new file mode 100644 index 00000000000..7289f301ab7 --- /dev/null +++ b/packages/playground/dist/wdio/pages/HomePage.d.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { BasePage } from './BasePage'; +declare class HomePage extends BasePage { + loadRNTester(): void; +} +declare const _default: HomePage; +export default _default; diff --git a/packages/playground/dist/wdio/pages/HomePage.js b/packages/playground/dist/wdio/pages/HomePage.js new file mode 100644 index 00000000000..8a3a4819dbb --- /dev/null +++ b/packages/playground/dist/wdio/pages/HomePage.js @@ -0,0 +1,28 @@ +"use strict"; +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const BasePage_1 = require("./BasePage"); +// import TextInputTestPage from './TextInputTestPage'; +// import { +// TEXTINPUT_TESTPAGE, +// LOGIN_TESTPAGE, +// DIRECT_MANIPULATION_TESTPAGE, +// IMAGE_TESTPAGE, +// CONTROL_STYLE_TESTPAGE, +// TRANSFORM_TESTPAGE, +// } from '../../app/Consts'; +// import LoginPage from './LoginPage'; +// import DirectManipulationPage from './DirectManipulationPage'; +// import ImageTestPage from './ImageTestPage'; +// import ControlStyleTestPage from './ControlStylePage'; +class HomePage extends BasePage_1.BasePage { + loadRNTester() { + this.loadButton.click(); + this.waitForPageLoaded(); + } +} +exports.default = new HomePage(); +//# sourceMappingURL=HomePage.js.map \ No newline at end of file diff --git a/packages/playground/dist/wdio/pages/HomePage.js.map b/packages/playground/dist/wdio/pages/HomePage.js.map new file mode 100644 index 00000000000..ee94c54d775 --- /dev/null +++ b/packages/playground/dist/wdio/pages/HomePage.js.map @@ -0,0 +1 @@ +{"version":3,"file":"HomePage.js","sourceRoot":"","sources":["../../../wdio/pages/HomePage.ts"],"names":[],"mappings":";AAAA;;;GAGG;;AAEH,yCAA8C;AAC9C,uDAAuD;AACvD,WAAW;AACX,wBAAwB;AACxB,oBAAoB;AACpB,kCAAkC;AAClC,oBAAoB;AACpB,4BAA4B;AAC5B,wBAAwB;AACxB,6BAA6B;AAC7B,uCAAuC;AACvC,iEAAiE;AACjE,+CAA+C;AAC/C,yDAAyD;AAEzD,MAAM,QAAS,SAAQ,mBAAQ;IAC7B,YAAY;QACV,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;QACxB,IAAI,CAAC,iBAAiB,EAAE,CAAC;IAC3B,CAAC;CA2DF;AAED,kBAAe,IAAI,QAAQ,EAAE,CAAC"} \ No newline at end of file diff --git a/packages/playground/dist/wdio/test/Image.spec.d.ts b/packages/playground/dist/wdio/test/Image.spec.d.ts new file mode 100644 index 00000000000..a8f94049193 --- /dev/null +++ b/packages/playground/dist/wdio/test/Image.spec.d.ts @@ -0,0 +1,5 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +export {}; diff --git a/packages/playground/dist/wdio/test/Image.spec.js b/packages/playground/dist/wdio/test/Image.spec.js new file mode 100644 index 00000000000..f538a31cc11 --- /dev/null +++ b/packages/playground/dist/wdio/test/Image.spec.js @@ -0,0 +1,50 @@ +"use strict"; +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const HomePage_1 = __importDefault(require("../pages/HomePage")); +const BasePage_1 = require("../pages/BasePage"); +// import ImageTestPage from '../pages/ImageTestPage'; +const assert_1 = __importDefault(require("assert")); +beforeAll(() => { + HomePage_1.default.loadRNTester(); + HomePage_1.default.waitForPageLoaded(); + // HomePage.clickAndGotoImagePage(); +}); +describe('basicTest', () => { + it('basicTest', () => { + const treedump = BasePage_1.By('x_TreeDump'); + const text = treedump.getValue(); + assert_1.default(text == 'fail', `text = ${text}`); + }); +}); +// describe('ImageWithoutBorderTest', () => { +// /* Test case #1: view and image displayed with no border and cornerRadius */ +// it('ImageWithoutBorderTest', () => { +// const result = ImageTestPage.getTreeDumpResult(); +// assert(result, '#1. Dump comparison for image without border!'); +// }); +// /* Test case #2: Click button once, update view and image with round border*/ +// it('ImageWithBorderTest', () => { +// ImageTestPage.toggleImageBorder(); +// const result = ImageTestPage.getTreeDumpResult(); +// assert(result, '#2. Dump comparison for image with border!'); +// }); +// /* Test case #3: Click button one more, remove border from view and image but tree sturcture is different from #1*/ +// it('ImageWithoutBorderTest', () => { +// ImageTestPage.toggleImageBorder(); +// const result = ImageTestPage.getTreeDumpResult(); +// assert(result, '#3. Second dump comparison for image without border!'); +// }); +// it('ImageRTLTest', () => { +// ImageTestPage.toggleRTLMode(); +// const result = ImageTestPage.getTreeDumpResult(); +// assert(result, '#4. Dump comparison for image RTL'); +// }); +// }); +//# sourceMappingURL=Image.spec.js.map \ No newline at end of file diff --git a/packages/playground/dist/wdio/test/Image.spec.js.map b/packages/playground/dist/wdio/test/Image.spec.js.map new file mode 100644 index 00000000000..8870a0b4c57 --- /dev/null +++ b/packages/playground/dist/wdio/test/Image.spec.js.map @@ -0,0 +1 @@ +{"version":3,"file":"Image.spec.js","sourceRoot":"","sources":["../../../wdio/test/Image.spec.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;;;AAEH,iEAAyC;AACzC,gDAAuC;AACvC,sDAAsD;AACtD,oDAA4B;AAE5B,SAAS,CAAC,GAAG,EAAE;IACb,kBAAQ,CAAC,YAAY,EAAE,CAAC;IACxB,kBAAQ,CAAC,iBAAiB,EAAE,CAAC;IAC7B,oCAAoC;AACtC,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,WAAW,EAAE,GAAG,EAAE;IACzB,EAAE,CAAC,WAAW,EAAE,GAAG,EAAE;QACnB,MAAM,QAAQ,GAAG,aAAE,CAAC,YAAY,CAAC,CAAC;QAClC,MAAM,IAAI,GAAG,QAAQ,CAAC,QAAQ,EAAE,CAAC;QACjC,gBAAM,CAAC,IAAI,IAAI,MAAM,EAAE,UAAU,IAAI,EAAE,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AACH,6CAA6C;AAC7C,iFAAiF;AACjF,yCAAyC;AACzC,wDAAwD;AACxD,uEAAuE;AACvE,QAAQ;AAER,kFAAkF;AAClF,sCAAsC;AACtC,yCAAyC;AACzC,wDAAwD;AACxD,oEAAoE;AACpE,QAAQ;AAER,wHAAwH;AACxH,yCAAyC;AACzC,yCAAyC;AACzC,wDAAwD;AACxD,8EAA8E;AAC9E,QAAQ;AAER,+BAA+B;AAC/B,qCAAqC;AACrC,wDAAwD;AACxD,2DAA2D;AAC3D,QAAQ;AACR,MAAM"} \ No newline at end of file diff --git a/packages/playground/just-task.js b/packages/playground/just-task.js index ad9f3c8a1b8..a72c1058850 100644 --- a/packages/playground/just-task.js +++ b/packages/playground/just-task.js @@ -5,7 +5,7 @@ * @ts-check */ -const {task, series, eslintTask} = require('just-scripts'); +const {task, series, eslintTask, argv, tscTask} = require('just-scripts'); task('eslint', () => { return eslintTask(); @@ -16,3 +16,16 @@ task('eslint:fix', () => { task('lint', series('eslint')); task('lint:fix', series('eslint:fix')); + +task('ts', () => { + return tscTask({ + pretty: true, + ...(argv().production && { + inlineSources: true, + }), + target: 'es6', + module: 'commonjs', + }); +}); + +task('build', series('ts')); \ No newline at end of file diff --git a/packages/playground/package.json b/packages/playground/package.json index 99dffded717..4c1f834ef76 100644 --- a/packages/playground/package.json +++ b/packages/playground/package.json @@ -3,12 +3,14 @@ "version": "0.0.54", "private": true, "scripts": { + "build": "just-scripts build", "start": "react-native start", "lint:fix": "just-scripts lint:fix", "lint": "just-scripts lint", "windows": "react-native run-windows" }, "dependencies": { + "chalk": "^4.0.0", "react": "16.9.0", "react-native": "0.62.2", "react-native-windows": "0.0.0-master.46" @@ -16,11 +18,28 @@ "devDependencies": { "@babel/core": "^7.8.4", "@babel/runtime": "^7.8.4", + "@types/jasmine": "2.8.7", + "@types/jquery": "^3.3.35", + "@types/node": "^10.14.8", "@types/react": "16.9.0", "@types/react-native": "^0.62.2", + "@wdio/appium-service": "5.12.1", + "@wdio/cli": "5.12.1", + "@wdio/dot-reporter": "5.12.1", + "@wdio/jasmine-framework": "5.12.1", + "@wdio/junit-reporter": "5.12.1", + "@wdio/local-runner": "5.12.1", + "@wdio/sync": "5.12.1", + "appium": "1.14.1", "just-scripts": "^0.36.1", "metro-react-native-babel-preset": "^0.56.0", + "prettier": "^1.18.2", "react-test-renderer": "16.9.0", - "@react-native-community/eslint-config": "^1.0.0" + "rimraf": "^3.0.0", + "ts-node": "^7.0.1", + "tsconfig-paths": "^3.8.0", + "typescript": "^3.8.3", + "webdriver": "git+https://github.com/react-native-windows/webdriver.git", + "webdriverio": "5.12.1" } } diff --git a/packages/playground/run_wdio.js b/packages/playground/run_wdio.js new file mode 100644 index 00000000000..8ab95e3d1a0 --- /dev/null +++ b/packages/playground/run_wdio.js @@ -0,0 +1,150 @@ +const path = require('path'); +const fs = require('fs'); +const xml2js = require('xml2js'); +const parser = new xml2js.Parser({ attrkey: 'ATTR' }); +const child_process = require('child_process'); +const prompt = require('prompt-sync')(); +const chalk = require('chalk'); + +const specFolder = 'wdio/test'; + +function GetMetadata(specPath) { + const contents = fs.readFileSync(specPath); + const metadataTag = '// @metadata '; + const metadataStart = contents.indexOf(metadataTag); + if (metadataStart != -1) { + let metadata = contents + .toString() + .substr(metadataStart + metadataTag.length) + .split(/[\r\n]/)[0]; + return metadata.split(' '); + } + return []; +} + +const filters = { + SkipCI: specPath => { + return process.env.BUILD_QUEUEDBY == 'GitHub'; + }, +}; + +// Returns true if the spec is to run. +// Specs marked SkipCI are excluded from CI (identified by environment variables in the ADO lab) +function FilterSpec(specPath) { + const metadata = GetMetadata(specPath); + for (let i = 0; i < metadata.length; i++) { + if (filters[metadata[i]](specPath)) { + return false; + } + } + return true; +} + +function SelectSpecs(folder) { + let specs = []; + if (process.argv.length > 2) { + specs = process.argv.splice(2).map(spec => spec + '.spec.ts'); + } else { + specs = fs.readdirSync(folder).filter(x => x.endsWith('.spec.ts')); + } + specs = specs.map(spec => path.join(folder, spec)).filter(FilterSpec); + return specs; +} + +let opts = SelectSpecs(specFolder); +console.log(`Selected tests: ${opts}`); + +function OverrideHyperV() { + const baseboardMfr = child_process + .execSync('powershell.exe (gwmi Win32_BaseBoard).Manufacturer') + .toString() + .replace(/[\r\n]/, ''); + if (!baseboardMfr.startsWith('Microsoft Corporation')) { + console.log(`Not running in HyperV. Mfr = ${baseboardMfr}`); + const answer = prompt( + 'E2ETest is meant to be run in a HyperV VM. Continue? (Y/N)' + ); + if (answer.toUpperCase() != 'Y') { + process.exit(0); + } + } +} + +// OverrideHyperV(); + +const Launcher = require('@wdio/cli').default; + +const wdio = new Launcher('wdio.conf.js', { specs: opts }); + +function parseLog(logfile) { + const xmlString = fs.readFileSync(logfile); + let failures = {}; + parser.parseString(xmlString, (err, res) => { + if (!res.testsuites) { + failures = 'something went wrong'; + } else { + for (let i = 0; i < res.testsuites.testsuite.length; i++) { + const attr = res.testsuites.testsuite[i].ATTR; + if (attr.errors > 0 || attr.failures > 0) { + let name = attr.name; + failures[name] = {}; + const testcases = res.testsuites.testsuite[i].testcase; + for (let j = 0; j < testcases.length; j++) { + if (testcases[j].error && testcases[j].error[0].ATTR) { + failures[name].testcase = testcases[j].ATTR.name; + failures[name].error = testcases[j].error[0].ATTR.message; + const systemErr = testcases[j]['system-err'][0]; + const stack = systemErr.substr(systemErr.indexOf('\n at ') + 1); + failures[name].stack = stack; + } + } + } + + } + } + }); + return failures; +} + +function parseLogs() { + const reportsDir = path.join(__dirname, 'reports'); + const logs = fs.readdirSync(reportsDir).filter(x => x.endsWith('.log')); + const names = logs + .map(x => parseLog(path.join(reportsDir, x))) + .filter(x => x != null); + return names; +} + +function PrintFailedTests(ft) { + for (let i = 0; i < Object.keys(ft).length; i++) { + const key = Object.keys(ft)[i]; + console.log(chalk.redBright(key)); + console.log(' ', chalk.underline('testcase'), chalk.bold(ft[key].testcase)); + console.log(' ', chalk.underline('error'), chalk.bold(ft[key].error)); + console.log(' ', chalk.underline('stack')); + console.log(ft[key].stack); + } +} + +function Process(code) { + const failedTests = parseLogs(); + for (let i = 0; i < failedTests.length; i++) { + console.log('Failed tests: '); + PrintFailedTests(failedTests[i]); + } + process.exit(code); +} + +function RunWdio() { + wdio.run().then( + code => { + Process(code); + }, + error => { + console.error('Launcher failed to start the test', error.stacktrace); + process.exit(1); + } + ); +} + +RunWdio(); diff --git a/packages/playground/tsconfig.json b/packages/playground/tsconfig.json index 16e778a02f9..54c59dd48cb 100644 --- a/packages/playground/tsconfig.json +++ b/packages/playground/tsconfig.json @@ -4,18 +4,29 @@ "target": "es6", "module": "commonjs", "jsx": "react", - "outDir": ".", + "outDir": "dist", "declaration": true, "sourceMap": true, - "experimentalDecorators": true, "noEmitOnError": true, "skipLibCheck": true, "moduleResolution": "node", + "noEmit": false, "noUnusedLocals": true, "strict": true, - "noEmit": true, - "rootDir": "." + "rootDir": ".", + "esModuleInterop": true, + "types": [ + "@wdio/sync", + "@wdio/jasmine-framework", + "@types/jasmine", + "node" + ] }, - "include": ["."], - "exclude": ["node_modules"] -} + "include": [ + "app", + "wdio" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/packages/playground/wdio.conf.js b/packages/playground/wdio.conf.js new file mode 100644 index 00000000000..0c6df2f66ce --- /dev/null +++ b/packages/playground/wdio.conf.js @@ -0,0 +1,291 @@ +const baseUrl = 'https://webdriver.io'; + +exports.config = { + // + // ==================== + // Runner Configuration + // ==================== + // + // WebdriverIO allows it to run your tests in arbitrary locations (e.g. locally or + // on a remote machine). + runner: 'local', + // + // ===================== + // Server Configurations + // ===================== + // Host address of the running Selenium server. This information is usually obsolete as + // WebdriverIO automatically connects to localhost. Also, if you are using one of the + // supported cloud services like Sauce Labs, Browserstack, or Testing Bot you don't + // need to define host and port information because WebdriverIO can figure that out + // according to your user and key information. However, if you are using a private Selenium + // backend you should define the host address, port, and path here. + // + // hostname: '172.18.5.185', + // port: 4444, + // path: '/wd/hub', + + // + // ================== + // Specify Test Files + // ================== + // Define which test specs should run. The pattern is relative to the directory + // from which `wdio` was called. Notice that, if you are calling `wdio` from an + // NPM script (see https://docs.npmjs.com/cli/run-script) then the current working + // directory is where your package.json resides, so `wdio` will be called from there. + // + specs: [ + 'wdio/test/**/*.ts', + ], + // Patterns to exclude. + exclude: [ + // 'path/to/excluded/files' + ], + // + // ============ + // Capabilities + // ============ + // Define your capabilities here. WebdriverIO can run multiple capabilities at the same + // time. Depending on the number of capabilities, WebdriverIO launches several test + // sessions. Within your capabilities you can overwrite the spec and exclude options in + // order to group specific specs to a specific capability. + // + // First, you can define how many instances should be started at the same time. Let's + // say you have 3 different capabilities (Chrome, Firefox, and Safari) and you have + // set maxInstances to 1; wdio will spawn 3 processes. Therefore, if you have 10 spec + // files and you set maxInstances to 10, all spec files will get tested at the same time + // and 30 processes will get spawned. The property handles how many capabilities + // from the same test should run tests. + // + maxInstances: 1, + // + // If you have trouble getting all important capabilities together, check out the + // Sauce Labs platform configurator - a great tool to configure your capabilities: + // https://docs.saucelabs.com/reference/platforms-configurator + // + capabilities: [ + { + // maxInstances can get overwritten per capability. So if you have an in-house Selenium + // grid with only 5 firefox instances available you can make sure that not more than + // 5 instances get started at a time. + maxInstances: 1, + // + platformName: 'windows', + // For W3C the appium capabilities need to have an extension prefix + // http://appium.io/docs/en/writing-running-appium/caps/ + // This is `appium:` for all Appium Capabilities which can be found here + 'appium:deviceName': 'WindowsPC', + 'appium:app': 'a752ebd1-71f2-4731-af80-c81e1782cc2b_gg9qsjdrgpss6!App', + 'deviceName': 'WindowsPC', + 'app': 'RNWPlayground_gg9qsjdrgpss6!App', + 'winAppDriver:experimental-w3c': true, + }, + // { + // // maxInstances can get overwritten per capability. So if you have an in house Selenium + // // grid with only 5 firefox instance available you can make sure that not more than + // // 5 instance gets started at a time. + // maxInstances: 5, + // browserName: 'firefox', + // // specs: [ + // // 'test/ffOnly/*' + // // ], + // "moz:firefoxOptions": { + // // flag to activate Firefox headless mode (see https://github.com/mozilla/geckodriver/blob/master/README.md#firefox-capabilities for more details about moz:firefoxOptions) + // // args: ['-headless'] + // } + // } + ], + // + // =================== + // Test Configurations + // =================== + // Define all options that are relevant for the WebdriverIO instance here + // + // Level of logging verbosity: trace | debug | info | warn | error + logLevel: 'trace', + // + // Warns when a deprecated command is used + deprecationWarnings: true, + // + // If you only want to run your tests until a specific amount of tests have failed use + // bail (default is 0 - don't bail, run all tests). + bail: 0, + // + // Set a base URL in order to shorten url command calls. If your `url` parameter starts + // with `/`, the base url gets prepended, not including the path portion of your baseUrl. + // If your `url` parameter starts without a scheme or `/` (like `some/path`), the base url + // gets prepended directly. + baseUrl: baseUrl, + // + // Default timeout for all waitFor* commands. + waitforTimeout: 10000, + // + // Default timeout in milliseconds for request + // if Selenium Grid doesn't send response + connectionRetryTimeout: 90000, + // + // Default request retries count + connectionRetryCount: 1, + + port: 4723, + // + // Test runner services + // Services take over a specific job you don't want to take care of. They enhance + // your test setup with almost no effort. Unlike plugins, they don't add new + // commands. Instead, they hook themselves up into the test process. + services: ['appium'], + appium: { + logPath: './reports/', + args: { + port: '4723', + }, + }, + + // + // Framework you want to run your specs with. + // The following are supported: Mocha, Jasmine, and Cucumber + // see also: https://webdriver.io/docs/frameworks.html + // + // Make sure you have the wdio adapter package for the specific framework installed + // before running any tests. + framework: 'jasmine', + // + // Test reporter for stdout. + // The only one supported by default is 'dot' + // see also: https://webdriver.io/docs/dot-reporter.html + reporters: ['dot', ['junit', { outputDir : '.\\reports' }]], + + // + // Options to be passed to Mocha. + // See the full list at http://mochajs.org/ + mochaOpts: { + ui: 'bdd', + timeout: 60000, + compilers: [ + // 'ts-node/register', + 'tsconfig-paths/register', + ], + }, + + jasmineNodeOpts: { + defaultTimeoutInterval: 60000, + }, + + // + // ===== + // Hooks + // ===== + // WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance + // it and to build services around it. You can either apply a single function or an array of + // methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got + // resolved to continue. + /** + * Gets executed once before all workers get launched. + * @param {Object} config wdio configuration object + * @param {Array.} capabilities list of capabilities details + */ + // onPrepare: function (config, capabilities) { + // }, + /** + * Gets executed just before initialising the webdriver session and test framework. It allows you + * to manipulate configurations depending on the capability or spec. + * @param {Object} config wdio configuration object + * @param {Array.} capabilities list of capabilities details + * @param {Array.} specs List of spec file paths that are to be run + */ + // beforeSession: function (config, capabilities, specs) { + // }, + /** + * Gets executed before test execution begins. At this point you can access to all global + * variables like `browser`. It is the perfect place to define custom commands. + * @param {Array.} capabilities list of capabilities details + * @param {Array.} specs List of spec file paths that are to be run + */ + before: function (capabilities, specs) { + // require('ts-node/register'); + require('ts-node').register({ files: true }); + }, + /** + * Runs before a WebdriverIO command gets executed. + * @param {String} commandName hook command name + * @param {Array} args arguments that command would receive + */ + // beforeCommand: function (commandName, args) { + // }, + + /** + * Hook that gets executed before the suite starts + * @param {Object} suite suite details + */ + // beforeSuite: function (suite) { + // }, + /** + * Function to be executed before a test (in Mocha/Jasmine) or a step (in Cucumber) starts. + * @param {Object} test test details + */ + // beforeTest: function (test) { + // }, + /** + * Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling + * beforeEach in Mocha) + */ + // beforeHook: function () { + // }, + /** + * Hook that gets executed _after_ a hook within the suite starts (e.g. runs after calling + * afterEach in Mocha) + */ + // afterHook: function () { + // }, + /** + * Function to be executed after a test (in Mocha/Jasmine) or a step (in Cucumber) starts. + * @param {Object} test test details + */ + afterTest: function (test) { + if (test.error !== undefined) { + let name = 'ERROR-' + Date.now(); + browser.saveScreenshot('./errorShots/' + name + '.png'); + } + }, + /** + * Hook that gets executed after the suite has ended + * @param {Object} suite suite details + */ + // afterSuite: function (suite) { + // }, + + /** + * Runs after a WebdriverIO command gets executed + * @param {String} commandName hook command name + * @param {Array} args arguments that command would receive + * @param {Number} result 0 - command success, 1 - command error + * @param {Object} error error object if any + */ + // afterCommand: function (commandName, args, result, error) { + // }, + /** + * Gets executed after all tests are done. You still have access to all global variables from + * the test. + * @param {Number} result 0 - test pass, 1 - test fail + * @param {Array.} capabilities list of capabilities details + * @param {Array.} specs List of spec file paths that ran + */ + // after: function (result, capabilities, specs) { + // }, + /** + * Gets executed right after terminating the webdriver session. + * @param {Object} config wdio configuration object + * @param {Array.} capabilities list of capabilities details + * @param {Array.} specs List of spec file paths that ran + */ + // afterSession: function (config, capabilities, specs) { + // }, + /** + * Gets executed after all workers got shut down and the process is about to exit. + * @param {Object} exitCode 0 - success, 1 - fail + * @param {Object} config wdio configuration object + * @param {Array.} capabilities list of capabilities details + * @param {} results object containing test results + */ + // onComplete: function(exitCode, config, capabilities, results) { + // } +}; diff --git a/packages/playground/wdio/pages/BasePage.ts b/packages/playground/wdio/pages/BasePage.ts new file mode 100644 index 00000000000..ed908348e95 --- /dev/null +++ b/packages/playground/wdio/pages/BasePage.ts @@ -0,0 +1,95 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +// import { +// REACT_CONTROL_ERROR_TEST_ID, +// HOME_BUTTON, +// TREE_DUMP_RESULT, +// } from '../../app/Consts'; + +const REACT_CONTROL_ERROR_TEST_ID = 'ReactControlErrorMessage'; + +export function By(testId: string): WebdriverIO.Element { + return $(`~${testId}`); +} + +export function wait(timeout: number) { + return new Promise(resolve => { + setTimeout(resolve, timeout); + }); +} + +export class BasePage { + isPageLoaded(): boolean { + return this.loadButton.isDisplayed(); + } + + waitForPageLoaded(timeout?: number) { + browser.waitUntil( + () => { + return this.isPagedLoadedOrLoadBundleError(); + }, + this.timeoutForPageLoaded(timeout), + `Wait for page ${this.constructor.name} timeout` + ); + } + + treeDump() { + return this.treeDumpButton.getText(); + } + + // getTreeDumpResult() { + // var testResult = false; + // const maxWait = 20; + // var waitCount = 1; + // do { + // testResult = this.treeDumpResult.getText() == 'TreeDump:Passed'; + // if (!testResult) { + // console.log( + // '####Waiting for treedump comparison result ' + + // waitCount + + // '/' + + // maxWait + + // '...####' + // ); + // wait(100); + // waitCount += 1; + // } + // } while (waitCount <= maxWait && !testResult); + + // return testResult; + // } + + protected timeoutForPageLoaded(currentTimeout?: number) { + if (currentTimeout) return currentTimeout; + return this.waitforPageTimeout; + } + + protected get loadButton() { + return By('x_LoadButton'); + } + + protected get treeDumpButton() { + return By('x_TreeDump'); + } + + private get reactControlErrorMessage() { + return By(REACT_CONTROL_ERROR_TEST_ID); + } + + private isPagedLoadedOrLoadBundleError(): boolean { + if (this.reactControlErrorMessage.isDisplayed()) { + throw "ReactControl doesn't load bundle successfully: " + + this.reactControlErrorMessage.getText(); + } + return this.isPageLoaded(); + } + + // private get treeDumpResult() { + // return By(TREE_DUMP_RESULT); + // } + + // Default timeout for waitForPageLoaded command in PageObject + private waitforPageTimeout: number = 10000; +} diff --git a/packages/playground/wdio/pages/HomePage.ts b/packages/playground/wdio/pages/HomePage.ts new file mode 100644 index 00000000000..1d7a441a375 --- /dev/null +++ b/packages/playground/wdio/pages/HomePage.ts @@ -0,0 +1,86 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { BasePage, /*By*/ } from './BasePage'; +// import TextInputTestPage from './TextInputTestPage'; +// import { +// TEXTINPUT_TESTPAGE, +// LOGIN_TESTPAGE, +// DIRECT_MANIPULATION_TESTPAGE, +// IMAGE_TESTPAGE, +// CONTROL_STYLE_TESTPAGE, +// TRANSFORM_TESTPAGE, +// } from '../../app/Consts'; +// import LoginPage from './LoginPage'; +// import DirectManipulationPage from './DirectManipulationPage'; +// import ImageTestPage from './ImageTestPage'; +// import ControlStyleTestPage from './ControlStylePage'; + +class HomePage extends BasePage { + loadRNTester() { + this.loadButton.click(); + this.waitForPageLoaded(); + } + + // isPageLoaded() { + // return super.isPageLoaded() && this.testInputTestPageButton.isDisplayed(); + // } + + // clickAndGoToTextInputPage() { + // this.testInputTestPageButton.click(); + // TextInputTestPage.waitForPageLoaded(); + // } + + // clickAndGotoLoginPage() { + // this.loginTestPageButton.click(); + // LoginPage.waitForPageLoaded(); + // } + + // clickAndGotoDirectManipulationPage() { + // this.directManipulationPageButton.click(); + // DirectManipulationPage.waitForPageLoaded(); + // } + + // clickAndGotoImagePage() { + // this.ImagePageButton.click(); + // ImageTestPage.waitForPageLoaded(); + // } + + // clickAndGotoControlStylePage() { + // this.ControlStylePageButton.click(); + // ControlStyleTestPage.waitForPageLoaded(); + // } + + // clickAndGotoTransformTestPage() { + // this.TransformTestPageButton.click(); + // ControlStyleTestPage.waitForPageLoaded(); + // } + + // private get testInputTestPageButton() { + // return By(TEXTINPUT_TESTPAGE); + // } + + // private get loginTestPageButton() { + // return By(LOGIN_TESTPAGE); + // } + + // private get directManipulationPageButton() { + // return By(DIRECT_MANIPULATION_TESTPAGE); + // } + + // private get ImagePageButton() { + // return By(IMAGE_TESTPAGE); + // } + + // private get ControlStylePageButton() { + // return By(CONTROL_STYLE_TESTPAGE); + // } + + // private get TransformTestPageButton() { + // return By(TRANSFORM_TESTPAGE); + // } +} + +export default new HomePage(); diff --git a/packages/playground/wdio/test/RNTesterVisualTest.spec.ts b/packages/playground/wdio/test/RNTesterVisualTest.spec.ts new file mode 100644 index 00000000000..e5e9517610d --- /dev/null +++ b/packages/playground/wdio/test/RNTesterVisualTest.spec.ts @@ -0,0 +1,92 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import assert from 'assert'; +import WebdriverIO from '@wdio/sync'; + +function VerifyTreeDumpOk(pageName : string) { + browser.waitUntil(() => $('~x_TreeDump').getText() == 'OK', 15000, `ERROR - treedump comparison failed for page ${pageName}`); + const treedump = $('~x_TreeDump').getText(); + assert(treedump == 'OK', `treedump = ${treedump} on page ${pageName}`); +} + +function DismissYellowBox() { + const dismissYellowBox = $('/Window/Window[2]/Custom/Text'); + if (dismissYellowBox && dismissYellowBox.isDisplayed()) { + dismissYellowBox.waitForEnabled(1000); + dismissYellowBox.click(); + } +} + +function VerifyPage(element : WebdriverIO.Element) { + const name = element.getText(); + element.click(); + browser.waitUntil(() => $('~PageHeader').getText() == name, 10000, `Timeout waiting for page ${name} to load, pageheader was ${$('~PageHeader').getText()}`); + browser.pause(2000); // wait 2 seconds for the page header to refresh post-navigation + + DismissYellowBox(); + VerifyTreeDumpOk(name); + const backButton = $('/Window/Window[2]/Button[2]'); + backButton.waitForEnabled(); + backButton.click(); +} + +beforeAll(() => { + browser.maximizeWindow(); +}); + +function findPage(name: String) { + const paneItems = $$('/Window/Window[2]/Pane[1]/Text'); + return paneItems.find(x => x.getText() == name); +} + +describe('RNTesterVisualTests', () => { + it('loadTest', async () => { + $('~x_LoadButton').waitForEnabled(30000, false, "TIMEOUT. NOT ENABLED"); + const load = $('~x_LoadButton'); + load.click(); + $('~PageHeader').waitForDisplayed(30000, false, 'No pageheader'); + const title = $('~PageHeader').getText(); + assert(title == 'RNTester'); + + VerifyTreeDumpOk('RNTester'); + DismissYellowBox(); + }); + it('basicSanity', async () => { + const activityIndicator = $('/Window/Window[2]/Pane[1]/Text[2]'); + activityIndicator.waitForEnabled(30000, false, 'No ActivityIndicator'); + assert(activityIndicator.getText() == '', `text = ${activityIndicator.getText()}`); + assert($('/Window/Window[2]/Pane[1]/Text[1]').getText() == 'COMPONENTS'); + }); + + const allowedPages = [ + '123', + '