From bd732aae74e3647ebf02917a843b0c671da5e77c Mon Sep 17 00:00:00 2001 From: Alexander Sklar Date: Sat, 18 Apr 2020 01:40:23 -0700 Subject: [PATCH 01/15] initial commit to do treedump on rntester --- .../playground/dist/wdio/pages/BasePage.d.ts | 16 + .../playground/dist/wdio/pages/BasePage.js | 79 ++++ .../dist/wdio/pages/BasePage.js.map | 1 + .../playground/dist/wdio/pages/HomePage.d.ts | 10 + .../playground/dist/wdio/pages/HomePage.js | 28 ++ .../dist/wdio/pages/HomePage.js.map | 1 + .../playground/dist/wdio/test/Image.spec.d.ts | 5 + .../playground/dist/wdio/test/Image.spec.js | 50 ++ .../dist/wdio/test/Image.spec.js.map | 1 + packages/playground/just-task.js | 15 +- packages/playground/package.json | 20 +- packages/playground/run_wdio.js | 119 +++++ packages/playground/tsconfig.json | 25 +- packages/playground/wdio.conf.js | 291 ++++++++++++ packages/playground/wdio/pages/BasePage.ts | 87 ++++ packages/playground/wdio/pages/HomePage.ts | 86 ++++ packages/playground/wdio/test/Image.spec.ts | 50 ++ .../DefaultVisualTreeLogger.cs | 44 ++ .../IPropertyValueTranslator.cs | 10 + .../TreeDumpLibrary/IVisualTreeLogger.cs | 17 + .../TreeDumpLibrary/JsonVisualTreeLogger.cs | 53 +++ .../TreeDumpLibrary/ReactPackageProvider.cs | 16 + .../TreeDumpControlViewManager.cs | 429 ++++++++++++++++++ .../TreeDumpLibrary/TreeDumpLibrary.csproj | 151 ++++++ .../TreeDumpLibrary/VisualTreeDumper.cs | 264 +++++++++++ packages/playground/windows/playground.sln | 19 + .../windows/playground/MainPage.cpp | 10 + .../playground/windows/playground/MainPage.h | 5 + .../windows/playground/MainPage.xaml | 5 + .../windows/playground/Playground.vcxproj | 52 ++- .../playground/Playground.vcxproj.filters | 6 + 31 files changed, 1955 insertions(+), 10 deletions(-) create mode 100644 packages/playground/dist/wdio/pages/BasePage.d.ts create mode 100644 packages/playground/dist/wdio/pages/BasePage.js create mode 100644 packages/playground/dist/wdio/pages/BasePage.js.map create mode 100644 packages/playground/dist/wdio/pages/HomePage.d.ts create mode 100644 packages/playground/dist/wdio/pages/HomePage.js create mode 100644 packages/playground/dist/wdio/pages/HomePage.js.map create mode 100644 packages/playground/dist/wdio/test/Image.spec.d.ts create mode 100644 packages/playground/dist/wdio/test/Image.spec.js create mode 100644 packages/playground/dist/wdio/test/Image.spec.js.map create mode 100644 packages/playground/run_wdio.js create mode 100644 packages/playground/wdio.conf.js create mode 100644 packages/playground/wdio/pages/BasePage.ts create mode 100644 packages/playground/wdio/pages/HomePage.ts create mode 100644 packages/playground/wdio/test/Image.spec.ts create mode 100644 packages/playground/windows/TreeDumpLibrary/DefaultVisualTreeLogger.cs create mode 100644 packages/playground/windows/TreeDumpLibrary/IPropertyValueTranslator.cs create mode 100644 packages/playground/windows/TreeDumpLibrary/IVisualTreeLogger.cs create mode 100644 packages/playground/windows/TreeDumpLibrary/JsonVisualTreeLogger.cs create mode 100644 packages/playground/windows/TreeDumpLibrary/ReactPackageProvider.cs create mode 100644 packages/playground/windows/TreeDumpLibrary/TreeDumpControlViewManager.cs create mode 100644 packages/playground/windows/TreeDumpLibrary/TreeDumpLibrary.csproj create mode 100644 packages/playground/windows/TreeDumpLibrary/VisualTreeDumper.cs 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..4164d545cce 100644 --- a/packages/playground/package.json +++ b/packages/playground/package.json @@ -3,6 +3,7 @@ "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", @@ -16,11 +17,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": "^6.0.18" } } diff --git a/packages/playground/run_wdio.js b/packages/playground/run_wdio.js new file mode 100644 index 00000000000..56a33f269f5 --- /dev/null +++ b/packages/playground/run_wdio.js @@ -0,0 +1,119 @@ +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 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 name; + parser.parseString(xmlString, (err, res) => { + if (!res.testsuites) { + name = 'something went wrong'; + } else { + const attr = res.testsuites.testsuite[0].ATTR; + if (attr.errors > 0 || attr.failures > 0) { + name = attr.name; + } + } + }); + return name; +} + +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 Process(code) { + const failedTests = parseLogs(); + for (let i = 0; i < failedTests.length; i++) { + console.log(`Failed test: ${failedTests[i]}`); + } + process.exit(code); +} + +wdio.run().then( + code => { + Process(code); + }, + error => { + console.error('Launcher failed to start the test', error.stacktrace); + process.exit(1); + } +); 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..65ec23d14c5 --- /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': 'a752ebd1-71f2-4731-af80-c81e1782cc2b_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..c931b9731a5 --- /dev/null +++ b/packages/playground/wdio/pages/BasePage.ts @@ -0,0 +1,87 @@ +/** + * 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' + ); + } + + // 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'); + } + + 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/Image.spec.ts b/packages/playground/wdio/test/Image.spec.ts new file mode 100644 index 00000000000..60dd34dd7b9 --- /dev/null +++ b/packages/playground/wdio/test/Image.spec.ts @@ -0,0 +1,50 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import HomePage from '../pages/HomePage'; +import { By } from '../pages/BasePage'; +// import ImageTestPage from '../pages/ImageTestPage'; +import assert from 'assert'; + +beforeAll(() => { + HomePage.loadRNTester(); + HomePage.waitForPageLoaded(); + // HomePage.clickAndGotoImagePage(); +}); + +describe('basicTest', () => { + it('basicTest', () => { + const treedump = By('x_TreeDump'); + const size = treedump.getSize(); + assert(size.width == 20, `size = ${size}`); + }); +}); +// 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'); +// }); +// }); diff --git a/packages/playground/windows/TreeDumpLibrary/DefaultVisualTreeLogger.cs b/packages/playground/windows/TreeDumpLibrary/DefaultVisualTreeLogger.cs new file mode 100644 index 00000000000..ab558d3f0ee --- /dev/null +++ b/packages/playground/windows/TreeDumpLibrary/DefaultVisualTreeLogger.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Text; +using Windows.UI.Xaml; + +namespace TreeDumpLibrary +{ + public sealed class DefaultVisualTreeLogger : IVisualTreeLogger + { + public void BeginNode(int indent, string nodeName, DependencyObject obj, bool hasProperties) + { + AppendLogger(indent, $"[{nodeName}]"); + } + + public void EndNode(int indent, string nodeName, DependencyObject obj, bool isLast) + { // no-op + } + + public void LogProperty(int indent, string propertyName, object propertyValue, bool isLast) + { + AppendLogger(indent, $"{propertyName}={propertyValue}"); + } + + public override string ToString() + { + return _logger.ToString(); + } + + private readonly StringBuilder _logger = new StringBuilder(); + private void AppendLogger(int indent, string s) + { + _logger.AppendLine(s.PadLeft(2 * indent + s.Length)); + } + + public void BeginArray(int indent, string propertyName) + { // no-op + } + + public void EndArray(int indent, string propertyName) + { + } + } +} diff --git a/packages/playground/windows/TreeDumpLibrary/IPropertyValueTranslator.cs b/packages/playground/windows/TreeDumpLibrary/IPropertyValueTranslator.cs new file mode 100644 index 00000000000..5bcc26823f6 --- /dev/null +++ b/packages/playground/windows/TreeDumpLibrary/IPropertyValueTranslator.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace TreeDumpLibrary +{ + public interface IPropertyValueTranslator + { + string PropertyValueToString(string propertyName, object propertyObject); + } +} \ No newline at end of file diff --git a/packages/playground/windows/TreeDumpLibrary/IVisualTreeLogger.cs b/packages/playground/windows/TreeDumpLibrary/IVisualTreeLogger.cs new file mode 100644 index 00000000000..4cddddc9cb2 --- /dev/null +++ b/packages/playground/windows/TreeDumpLibrary/IVisualTreeLogger.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Windows.UI.Xaml; + +namespace TreeDumpLibrary +{ + public interface IVisualTreeLogger + { + void BeginNode(int indent, string nodeName, DependencyObject obj, bool hasProperties); + void EndNode(int indent, string nodeName, DependencyObject obj, bool isLast); + void LogProperty(int indent, string propertyName, object propertyValue, bool isLast); + string ToString(); + void BeginArray(int indent, string propertyName); + void EndArray(int indent, string propertyName); + } +} diff --git a/packages/playground/windows/TreeDumpLibrary/JsonVisualTreeLogger.cs b/packages/playground/windows/TreeDumpLibrary/JsonVisualTreeLogger.cs new file mode 100644 index 00000000000..7cf52ca97ab --- /dev/null +++ b/packages/playground/windows/TreeDumpLibrary/JsonVisualTreeLogger.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Text; +using Windows.UI.Xaml; + +namespace TreeDumpLibrary +{ + public sealed class JsonVisualTreeLogger : IVisualTreeLogger + { + private readonly StringBuilder _logger = new StringBuilder(); + public void BeginArray(int indent, string propertyName) + { + AppendLogger(indent, $"\"{propertyName}\": ["); + } + + public void BeginNode(int indent, string nodeName, DependencyObject obj, bool hasProperties) + { + AppendLogger(indent, "{"); + LogProperty(indent + 2, "XamlType", JsonPropertyValueTranslator.Quote(nodeName), !hasProperties); + } + + public void EndArray(int indent, string propertyName) + { + AppendLogger(indent, "]"); + } + + public void EndNode(int indent, string nodeName, DependencyObject obj, bool isLast) + { + AppendLogger(indent, $"}}{GetDelimiter(isLast)}"); + } + + private string GetDelimiter(bool isLast) + { + return isLast ? "" : ","; + } + + public void LogProperty(int indent, string propertyName, object propertyValue, bool isLast) + { + AppendLogger(indent, $"\"{propertyName}\": {propertyValue}{GetDelimiter(isLast)}"); + } + + public override string ToString() + { + return _logger.ToString(); + } + + private void AppendLogger(int indent, string s) + { + _logger.AppendLine(s.PadLeft(2 * indent + s.Length)); + } + } +} diff --git a/packages/playground/windows/TreeDumpLibrary/ReactPackageProvider.cs b/packages/playground/windows/TreeDumpLibrary/ReactPackageProvider.cs new file mode 100644 index 00000000000..30d9991f556 --- /dev/null +++ b/packages/playground/windows/TreeDumpLibrary/ReactPackageProvider.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.ReactNative; +using Microsoft.ReactNative.Managed; + +namespace TreeDumpLibrary +{ + public sealed class ReactPackageProvider : IReactPackageProvider + { + public void CreatePackage(IReactPackageBuilder packageBuilder) + { + packageBuilder.AddViewManagers(); + } + } +} diff --git a/packages/playground/windows/TreeDumpLibrary/TreeDumpControlViewManager.cs b/packages/playground/windows/TreeDumpLibrary/TreeDumpControlViewManager.cs new file mode 100644 index 00000000000..1e12a33264b --- /dev/null +++ b/packages/playground/windows/TreeDumpLibrary/TreeDumpControlViewManager.cs @@ -0,0 +1,429 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.ReactNative; +using Microsoft.ReactNative.Managed; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Threading.Tasks; +using Windows.ApplicationModel.DataTransfer; +using Windows.Data.Json; +using Windows.Storage; +using Windows.UI.ViewManagement; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Documents; +using Windows.UI.Xaml.Media; + +namespace TreeDumpLibrary +{ + internal class TreeDumpControlViewManager : IViewManager, IViewManagerWithNativeProperties + { + public string Name => "TreeDumpControl"; + + public FrameworkElement CreateView() + { + m_textBlock = new TextBlock(); + m_textBlock.TextWrapping = TextWrapping.Wrap; + m_textBlock.IsTextSelectionEnabled = false; + m_textBlock.LayoutUpdated += (source, e) => + { + var bounds = ApplicationView.GetForCurrentView().VisibleBounds; + if (bounds.Width != 800 || bounds.Height != 600) + { + // Dump disabled when window size is not 800x600! + UpdateResult(false /*matchDump*/ , "Window has been resized, dump comparison is only valid at default launch size: 800x600!, current size:" + bounds.ToString()); + } + else + { + // delay dumping tree by 100ms for layout to stabilize + if (m_timer != null) + { + m_timer.Stop(); + m_timer.Start(); + } + } + }; + + m_textBlock.PointerPressed += (source, e) => + { + if (!m_dumpMatchExpected) + { + if (m_timer != null) + { + m_timer.Stop(); + } + m_errStringShowing = true; + errors.Apply(m_textBlock); + m_textBlock.IsTextSelectionEnabled = true; + } + }; + + return m_textBlock; + } + + public void UpdateProperties(FrameworkElement view, IJSValueReader propertyMapReader) + { + var propertyMap = JSValue.ReadObjectFrom(propertyMapReader); + foreach (KeyValuePair kvp in propertyMap) + { + if (kvp.Key == "dumpID") + { + SetDumpID((TextBlock)view, kvp.Value.AsString()); + } + else if (kvp.Key == "uiaID") + { + SetUIAID((TextBlock)view, kvp.Value.AsString()); + } + else if (kvp.Key == "additionalProperties") + { + SetAdditionalProperties(kvp.Value.AsArray()); + } + } + } + + IReadOnlyDictionary IViewManagerWithNativeProperties.NativeProps => new Dictionary + { { "dumpID", ViewManagerPropertyType.String }, + { "uiaID", ViewManagerPropertyType.String }, + { "additionalProperties", ViewManagerPropertyType.Array } + }; + + public void SetDumpID(TextBlock view, string value) + { + m_dumpID = value; + m_dumpMatchExpected = false; + m_dumpExpectedText = null; + m_errString = ""; + errors = new TreeDumpErrors(); + m_errStringShowing = false; + if (m_textBlock != null) + { + m_textBlock.IsTextSelectionEnabled = false; + UpdateTextBlockText(""); + } + m_timer = new DispatcherTimer(); + m_timer.Tick += dispatcherTimer_Tick; + m_timer.Interval = new TimeSpan(0, 0, 0, 0, 200); + } + + public void SetUIAID(TextBlock view, string value) + { + m_uiaID = value; + } + + public void SetAdditionalProperties(IReadOnlyList additionalProperties) + { + foreach (var property in additionalProperties) + { + m_additionalProperties.Add(property.AsString()); + } + } + + private async void dispatcherTimer_Tick(object sender, object e) + { + m_timer.Stop(); + if (VisualTreeHelper.GetParent(m_textBlock) != null) + { + await MatchTreeDumpFromLayoutUpdateAsync(); + } + } + private DependencyObject FindChildWithMatchingUIAID(DependencyObject element) + { + string automationId = (string)element.GetValue(Windows.UI.Xaml.Automation.AutomationProperties.AutomationIdProperty); + if (automationId == m_uiaID) + { + return element; + } + int childrenCount = VisualTreeHelper.GetChildrenCount(element); + for (int i = 0; i < childrenCount; i++) + { + var result = FindChildWithMatchingUIAID(VisualTreeHelper.GetChild(element, i)); + if (result != null) + { + return result; + } + } + + return null; + } + + private async Task MatchTreeDumpFromLayoutUpdateAsync() + { + // First find root + DependencyObject current = m_textBlock; + DependencyObject parent = VisualTreeHelper.GetParent(current); + while (parent != null) + { + current = parent; + parent = VisualTreeHelper.GetParent(current); + } + + DependencyObject dumpRoot = current; + // if UIAID is passed in from test, find the matching child as the root to dump + if (m_uiaID != null) + { + var matchingNode = FindChildWithMatchingUIAID(current); + if (matchingNode != null) + { + dumpRoot = matchingNode; + } + } + + string dumpText = VisualTreeDumper.DumpTree(dumpRoot, m_textBlock /* exclude */ , m_additionalProperties, mode); + if (dumpText != m_dumpExpectedText) + { + await MatchDump(dumpText); + } + } + + private readonly DumpTreeMode mode = DumpTreeMode.Json; + + private async Task MatchDump(string dumpText) + { + StorageFile masterFile = await Windows.ApplicationModel.Package.Current.InstalledLocation.GetFileAsync($@"Assets\{GetMasterFile()}"); + if (m_dumpExpectedText == null) + { + try + { + m_dumpExpectedText = await FileIO.ReadTextAsync(masterFile); + + StorageFolder storageFolder = ApplicationData.Current.LocalFolder; + string copyFileName = GetMasterFile(); + var copyDumpFile = await storageFolder.CreateFileAsync(copyFileName, CreationCollisionOption.ReplaceExisting); + await FileIO.WriteTextAsync(copyDumpFile, m_dumpExpectedText); + } + catch (IOException) + { + UpdateResult(false /*matchDump*/ , "Tree dump master file not found in testapp package!"); + } + } + + if (!DumpsAreEqual(m_dumpExpectedText, dumpText)) + { + StorageFolder storageFolder = ApplicationData.Current.LocalFolder; + string fileName = GetOutputFile(); + try + { + StorageFile outFile = await storageFolder.CreateFileAsync(fileName, CreationCollisionOption.ReplaceExisting); + await FileIO.WriteTextAsync(outFile, dumpText); + UpdateResult(false /*matchDump*/ , + $"Tree dump file does not match master at {masterFile.Path} - See output at {outFile.Path}", + GetInlines(masterFile, outFile, m_textBlock)); + } + catch (IOException) + { + UpdateResult(false /*matchDump*/ , "Can't write dump output file:" + fileName); + } + } + else + { + UpdateResult(true /*matchDump*/ , ""); + } + } + + private bool DumpsAreEqual(string dumpExpectedText, string dumpText) + { + if (mode == DumpTreeMode.Default) + { + return dumpExpectedText == dumpText; + } + else + { + JsonValue expected = JsonValue.Parse(dumpExpectedText); + JsonValue actual = JsonValue.Parse(dumpText); + return JsonComparesEqual(expected, actual); + } + } + + private bool JsonComparesEqual(IJsonValue expected, IJsonValue actual) + { + if (expected.ValueType != actual.ValueType) + { + return false; + } + switch (expected.ValueType) + { + case JsonValueType.String: + if (expected.GetString() == actual.GetString()) + { + return true; + } + else { Debug.WriteLine($"Expected {expected.GetString()} got {actual.GetString()}"); return false; } + case JsonValueType.Number: + return expected.GetNumber() == actual.GetNumber(); + case JsonValueType.Boolean: + return expected.GetBoolean() == actual.GetBoolean(); + case JsonValueType.Null: + return true; + case JsonValueType.Array: + { + var ea = expected.GetArray(); + var aa = actual.GetArray(); + if (ea.Count != aa.Count) { return false; } + for (uint i = 0; i < ea.Count; i++) + { + if (!JsonComparesEqual(ea[(int)i], aa[(int)i])) + { + return false; + } + } + return true; + } + case JsonValueType.Object: + { + var eo = expected.GetObject(); + var ao = actual.GetObject(); + if (eo.Keys.Count != ao.Keys.Count) { return false; } + foreach (var key in eo.Keys) + { + if (!JsonComparesEqual(eo.GetNamedValue(key), ao.GetNamedValue(key))) + { + return false; + } + } + return true; + } + default: + throw new ArgumentException(); + } + } + + private string GetOutputFile() + { + return "TreeDump\\" + m_dumpID + (mode == DumpTreeMode.Json ? ".json" : ".out"); + } + + private string GetMasterFile() + { + return "TreeDump\\masters\\" + m_dumpID + (mode == DumpTreeMode.Json ? ".json" : ".txt"); + } + + private static IList GetInlines(StorageFile masterFile, StorageFile outFile, UIElement anchor) + { + Hyperlink masterLink = new Hyperlink(); + masterLink.Click += (_1, _2) => { Windows.System.Launcher.LaunchFileAsync(masterFile); }; + masterLink.Inlines.Add(new Run() { Text = "master" }); + Hyperlink outLink = new Hyperlink(); + outLink.Click += (_1, _2) => { Windows.System.Launcher.LaunchFileAsync(outFile); }; + outLink.Inlines.Add(new Run() { Text = "output" }); + List inlines = new List() + { + new Run () { Text = "Tree dump " }, + outLink, + new Run () { Text = " does not match " }, + masterLink + }; + + #region Diff support - Replace with LaunchUriAsync when we find the VSCode protocol handler Uri for diffing + string code_cmd = Environment.ExpandEnvironmentVariables(@"%UserProfile%\AppData\Local\Programs\Microsoft VS Code\bin\code.cmd"); + Hyperlink diffLink = new Hyperlink(); + diffLink.Click += (_1, _2) => + { + string commandLine = $"code.cmd --diff \"{masterFile.Path}\" \"{outFile.Path}\""; + DataPackage dataPackage = new DataPackage(); + dataPackage.SetText(commandLine); + Clipboard.SetContent(dataPackage); + ToolTip toolTip = new ToolTip() { Content = "Copied to clipboard" }; + ToolTipService.SetToolTip(anchor, toolTip); + toolTip.Opened += (_3, _4) => + { + var timer = Windows.System.DispatcherQueue.GetForCurrentThread().CreateTimer(); + timer.IsRepeating = false; + timer.Interval = TimeSpan.FromSeconds(1); + timer.Tick += (_5, _6) => + { + toolTip.IsOpen = false; + ToolTipService.SetToolTip(anchor, null); + }; + timer.Start(); + }; + toolTip.IsOpen = true; + }; + diffLink.Inlines.Add(new Run() { Text = "copy diff command to clipboard" }); + inlines.Add(new Run() { Text = " - " }); + inlines.Add(diffLink); + #endregion + return inlines; + } + + private async void UpdateResult(bool matchDump, string helpText, IList inlines = null) + { + if (matchDump) + { + UpdateTextBlockText("TreeDump:Passed"); + } + else + { + UpdateTextBlockText("TreeDump:Failed, click to see more!"); + m_errString += "\r\n" + helpText; + errors.Inlines.Clear(); + if (inlines != null) + { + errors.Inlines.AddRange(inlines); + } + else + { + errors.Inlines.Add(new Run() { Text = helpText }); + } + errors.Inlines.Add(new LineBreak()); + await WriteErrorFile(); + } + + m_dumpMatchExpected = matchDump; + } + + private async Task WriteErrorFile() + { + StorageFolder storageFolder = ApplicationData.Current.LocalFolder; + string fileNameError = "TreeDump\\" + m_dumpID + ".err"; + try + { + StorageFile errFile = await storageFolder.CreateFileAsync(fileNameError, CreationCollisionOption.GenerateUniqueName); + await FileIO.WriteTextAsync(errFile, m_errString); + } + catch (Exception e) + { + UpdateTextBlockText("Create err file failed: " + e.ToString()); + } + } + + private void UpdateTextBlockText(string text) + { + if (!m_errStringShowing && m_textBlock.Text != text) + { + m_textBlock.Text = text; + } + } + private TextBlock m_textBlock = null; + private string m_dumpID = "UnknownTest"; + private string m_dumpExpectedText; + private bool m_dumpMatchExpected = false; + private bool m_errStringShowing = false; + private string m_errString = ""; + private TreeDumpErrors errors = new TreeDumpErrors(); + private string m_uiaID = null; + private readonly List m_additionalProperties = new List(); + + private DispatcherTimer m_timer = null; + } + + internal class TreeDumpErrors + { + readonly List inlines = new List(); + + public List Inlines => inlines; + + public void Apply(TextBlock textBlock) + { + textBlock.Text = ""; + textBlock.Inlines.Clear(); + foreach (var inline in Inlines) + { + textBlock.Inlines.Add(inline); + } + textBlock.Width = 800; + } + } +} diff --git a/packages/playground/windows/TreeDumpLibrary/TreeDumpLibrary.csproj b/packages/playground/windows/TreeDumpLibrary/TreeDumpLibrary.csproj new file mode 100644 index 00000000000..9d3b04e41b9 --- /dev/null +++ b/packages/playground/windows/TreeDumpLibrary/TreeDumpLibrary.csproj @@ -0,0 +1,151 @@ + + + + + Debug + AnyCPU + {C0A6BD9C-3EE5-4B12-8CE4-CEE95178539C} + winmdobj + Properties + TreeDumpLibrary + TreeDumpLibrary + en-US + UAP + 10.0.18362.0 + 10.0.16299.0 + 16.0 + 512 + {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + false + false + + + x86 + true + bin\x86\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + false + prompt + + + x86 + bin\x86\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + false + prompt + + + ARM + true + bin\ARM\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + false + prompt + + + ARM + bin\ARM\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + false + prompt + + + x64 + true + bin\x64\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + false + prompt + + + x64 + bin\x64\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + false + prompt + + + true + bin\ARM64\Debug\ + DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP + ;2008 + full + ARM64 + false + prompt + + + bin\ARM64\Release\ + TRACE;NETFX_CORE;WINDOWS_UWP + true + ;2008 + pdbonly + ARM64 + false + prompt + + + PackageReference + + + $([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), 'node_modules\react-native-windows\package.json'))\node_modules\react-native-windows\ + + + true + + + + + + + + + + + + + 6.2.8 + + + + + {f7d32bd0-2749-483e-9a0d-1635ef7e3136} + Microsoft.ReactNative + + + {f7d32bd0-2749-483e-9a0d-1635ef7e3136} + Microsoft.ReactNative + False + + + + + + + + 16.0 + + + + \ No newline at end of file diff --git a/packages/playground/windows/TreeDumpLibrary/VisualTreeDumper.cs b/packages/playground/windows/TreeDumpLibrary/VisualTreeDumper.cs new file mode 100644 index 00000000000..32a43aa7bd2 --- /dev/null +++ b/packages/playground/windows/TreeDumpLibrary/VisualTreeDumper.cs @@ -0,0 +1,264 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Windows.Foundation; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Media; + +namespace TreeDumpLibrary +{ + public enum DumpTreeMode + { + Default, + Json + } + + public sealed class VisualTreeDumper + { + class Visitor + { + private readonly IVisualTreeLogger _logger; + private int _indent; + private readonly DefaultFilter _filter; + private readonly IPropertyValueTranslator _translator; + public Visitor(DefaultFilter filter, IPropertyValueTranslator translator, IVisualTreeLogger logger) + { + _indent = 0; + _filter = filter; + _translator = translator; + _logger = logger; + } + public void EndVisitNode(DependencyObject obj, bool isLast) + { + _indent--; + _logger.EndNode(_indent, obj.GetType().FullName, obj, isLast); + } + + public void BeginVisitNode(DependencyObject obj, bool hasProperties) + { + _logger.BeginNode(_indent, obj.GetType().FullName, obj, hasProperties); + _indent++; + } + + public override string ToString() + { + return _logger.ToString(); + } + + public bool ShouldVisitPropertiesForNode(DependencyObject node) + { + return (node as UIElement) != null; + } + + public bool ShouldVisitProperty(PropertyInfo propertyInfo) + { + return _filter.ShouldVisitProperty(propertyInfo.Name); + } + + public void VisitProperty(string propertyName, object value, bool isLast) + { + var v = _translator.PropertyValueToString(propertyName, value); + _logger.LogProperty(_indent + 1, propertyName, v, isLast); + } + + public void BeginChildren() + { + _logger.BeginArray(++_indent, "children"); + } + + public void EndChildren() + { + _logger.EndArray(_indent--, "children"); + } + + public bool ShouldVisitPropertyValue(string propertyName, object value) + { + string s = _translator.PropertyValueToString(propertyName, value); + if (propertyName == "Name") + { + string name = value as string; + return !name.StartsWith(":") && + name != ""; + } + return _filter.ShouldVisitPropertyValue(s); + } + } + + public static string DumpTree(DependencyObject root, DependencyObject excludedNode, IList additionalProperties, DumpTreeMode mode) + { + var propertyFilter = new DefaultFilter(); + ((List)propertyFilter.PropertyNameAllowList).AddRange(additionalProperties); + + IPropertyValueTranslator translator = (mode == DumpTreeMode.Json ? + new JsonPropertyValueTranslator() as IPropertyValueTranslator : + new DefaultPropertyValueTranslator()); + IVisualTreeLogger logger = (mode == DumpTreeMode.Json ? + new JsonVisualTreeLogger() as IVisualTreeLogger : + new DefaultVisualTreeLogger()); + Visitor visitor = new Visitor(propertyFilter, translator, logger); + + WalkThroughTree(root, excludedNode, visitor); + + return visitor.ToString(); + } + + private static void WalkThroughProperties(DependencyObject node, Visitor visitor, bool hasChildren) + { + if (visitor.ShouldVisitPropertiesForNode(node)) + { + var properties = (from property in node.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance) + where visitor.ShouldVisitProperty(property) && + visitor.ShouldVisitPropertyValue(property.Name, + GetObjectProperty(node, property)) + orderby property.Name + select property).ToArray(); + + for (int i = 0; i < properties.Length; i++) + { + var property = properties[i]; + object value = null; + value = GetObjectProperty(node, property); + bool isLast = (i == properties.Length - 1) && !hasChildren; + visitor.VisitProperty(property.Name, value, isLast); + } + } + } + + private static object GetObjectProperty(DependencyObject node, PropertyInfo property) + { + object value; + try + { + value = property.GetValue(node); + } + catch (Exception e) + { + value = "Exception when reading " + property.Name + e.ToString(); + } + + return value; + } + + private static void WalkThroughTree(DependencyObject node, DependencyObject excludedNode, Visitor visitor, bool isLast = true) + { + if (node != null) + { + // Assume that if we have a UIElement, we'll have some properties + visitor.BeginVisitNode(node, node is UIElement); + + var childrenCount = VisualTreeHelper.GetChildrenCount(node); + WalkThroughProperties(node, visitor, childrenCount != 0); + if (childrenCount != 0) + { + visitor.BeginChildren(); + for (int i = 0; i < childrenCount; i++) + { + var child = VisualTreeHelper.GetChild(node, i); + if (child != excludedNode) + { + bool isLastChild = (i == childrenCount - 1); + WalkThroughTree(child, excludedNode, visitor, isLastChild); + } + } + visitor.EndChildren(); + } + visitor.EndVisitNode(node, isLast); + } + } + } + public sealed class DefaultFilter + { + public IList PropertyNameAllowList { get; set; } + + public DefaultFilter() + { + PropertyNameAllowList = new List + { + "Foreground", + "Background", + "Padding", + "Margin", + "RenderSize", + "Visibility", + "CornerRadius", + "BorderThickness", + "Width", + "Height", + "BorderBrush", + "VerticalAlignment", + "HorizontalAlignment", + "Clip", + "FlowDirection", + "Name", + /*"ActualOffset" 19h1*/ + }; + } + + public bool ShouldVisitPropertyValue(string propertyValue) + { + return !string.IsNullOrEmpty(propertyValue) && !propertyValue.Equals("NaN") && !propertyValue.StartsWith("Exception"); + } + + public bool ShouldVisitProperty(string propertyName) + { + return (PropertyNameAllowList.Contains(propertyName)); + } + } + public sealed class DefaultPropertyValueTranslator : IPropertyValueTranslator + { + public string PropertyValueToString(string propertyName, object propertyObject) + { + if (propertyObject == null) + { + return "[NULL]"; + } + + if (propertyObject is SolidColorBrush) + { + return (propertyObject as SolidColorBrush).Color.ToString(); + } + else if (propertyObject is Size) + { + // comparing doubles is numerically unstable so just compare their integer parts + Size size = (Size)propertyObject; + return $"{(int)size.Width},{(int)size.Height}"; + } + return propertyObject.ToString(); + } + } + + public sealed class JsonPropertyValueTranslator : IPropertyValueTranslator + { + public string PropertyValueToString(string propertyName, object propertyObject) + { + if (propertyObject == null) + { + return "null"; + } + else if (propertyObject is int || propertyObject is bool || propertyObject is double) + { + return propertyObject.ToString(); + } + else if (propertyObject is SolidColorBrush) + { + return Quote((propertyObject as SolidColorBrush).Color.ToString()); + } + else if (propertyObject is Size) + { + // comparing doubles is numerically unstable so just compare their integer parts + Size size = (Size)propertyObject; + return $"[{(int)size.Width}, {(int)size.Height}]"; + } + return Quote(propertyObject.ToString()); + } + + public static string Quote(string s) + { + return '"' + s.Replace("\"", "\\\"") + '"'; + } + } +} diff --git a/packages/playground/windows/playground.sln b/packages/playground/windows/playground.sln index 51df10393e6..d2dc188073f 100644 --- a/packages/playground/windows/playground.sln +++ b/packages/playground/windows/playground.sln @@ -40,6 +40,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Shared", "..\..\..\vnext\Sh EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Mso", "..\..\..\vnext\Mso\Mso.vcxitems", "{84E05BFA-CBAF-4F0D-BFB6-4CE85742A57E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TreeDumpLibrary", "..\..\E2ETest\windows\TreeDumpLibrary\TreeDumpLibrary.csproj", "{C0A6BD9C-3EE5-4B12-8CE4-CEE95178539C}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution ..\..\..\vnext\JSI\Shared\JSI.Shared.vcxitems*{0cc28589-39e4-4288-b162-97b959f8b843}*SharedItemsImports = 9 @@ -48,6 +50,7 @@ Global ..\..\..\vnext\Microsoft.ReactNative.Cxx\Microsoft.ReactNative.Cxx.vcxitems*{6b6aa847-b32f-41ac-9d3b-48a8cdfa8ade}*SharedItemsImports = 4 ..\..\..\vnext\Mso\Mso.vcxitems*{84e05bfa-cbaf-4f0d-bfb6-4ce85742a57e}*SharedItemsImports = 9 ..\..\..\vnext\JSI\Shared\JSI.Shared.vcxitems*{a62d504a-16b8-41d2-9f19-e2e86019e5e4}*SharedItemsImports = 4 + ..\..\..\vnext\Microsoft.ReactNative.SharedManaged\Microsoft.ReactNative.SharedManaged.projitems*{c0a6bd9c-3ee5-4b12-8ce4-cee95178539c}*SharedItemsImports = 4 ..\..\..\vnext\Chakra\Chakra.vcxitems*{c38970c0-5fbf-4d69-90d8-cbac225ae895}*SharedItemsImports = 9 ..\..\..\vnext\Microsoft.ReactNative.Cxx\Microsoft.ReactNative.Cxx.vcxitems*{da8b35b3-da00-4b02-bde6-6a397b3fd46b}*SharedItemsImports = 9 ..\..\..\vnext\Chakra\Chakra.vcxitems*{f7d32bd0-2749-483e-9a0d-1635ef7e3136}*SharedItemsImports = 4 @@ -186,6 +189,22 @@ Global {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Release|x64.Build.0 = Release|x64 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Release|x86.ActiveCfg = Release|Win32 {FCA38F3C-7C73-4C47-BE4E-32F77FA8538D}.Release|x86.Build.0 = Release|Win32 + {C0A6BD9C-3EE5-4B12-8CE4-CEE95178539C}.Debug|ARM.ActiveCfg = Debug|ARM + {C0A6BD9C-3EE5-4B12-8CE4-CEE95178539C}.Debug|ARM.Build.0 = Debug|ARM + {C0A6BD9C-3EE5-4B12-8CE4-CEE95178539C}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {C0A6BD9C-3EE5-4B12-8CE4-CEE95178539C}.Debug|ARM64.Build.0 = Debug|ARM64 + {C0A6BD9C-3EE5-4B12-8CE4-CEE95178539C}.Debug|x64.ActiveCfg = Debug|x64 + {C0A6BD9C-3EE5-4B12-8CE4-CEE95178539C}.Debug|x64.Build.0 = Debug|x64 + {C0A6BD9C-3EE5-4B12-8CE4-CEE95178539C}.Debug|x86.ActiveCfg = Debug|x86 + {C0A6BD9C-3EE5-4B12-8CE4-CEE95178539C}.Debug|x86.Build.0 = Debug|x86 + {C0A6BD9C-3EE5-4B12-8CE4-CEE95178539C}.Release|ARM.ActiveCfg = Release|ARM + {C0A6BD9C-3EE5-4B12-8CE4-CEE95178539C}.Release|ARM.Build.0 = Release|ARM + {C0A6BD9C-3EE5-4B12-8CE4-CEE95178539C}.Release|ARM64.ActiveCfg = Release|ARM64 + {C0A6BD9C-3EE5-4B12-8CE4-CEE95178539C}.Release|ARM64.Build.0 = Release|ARM64 + {C0A6BD9C-3EE5-4B12-8CE4-CEE95178539C}.Release|x64.ActiveCfg = Release|x64 + {C0A6BD9C-3EE5-4B12-8CE4-CEE95178539C}.Release|x64.Build.0 = Release|x64 + {C0A6BD9C-3EE5-4B12-8CE4-CEE95178539C}.Release|x86.ActiveCfg = Release|x86 + {C0A6BD9C-3EE5-4B12-8CE4-CEE95178539C}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/packages/playground/windows/playground/MainPage.cpp b/packages/playground/windows/playground/MainPage.cpp index b1e7b1dd4ca..f66818a65cd 100644 --- a/packages/playground/windows/playground/MainPage.cpp +++ b/packages/playground/windows/playground/MainPage.cpp @@ -1,6 +1,7 @@ #include "pch.h" #include "MainPage.h" #include "MainPage.g.cpp" +#include "winrt/TreeDumpLibrary.h" using namespace winrt; using namespace Windows::ApplicationModel::Activation; @@ -54,6 +55,15 @@ void MainPage::OnLoadClick( x_rootElement().Children().Append(m_reactRootView); } +void MainPage::UpdateTreeDump( + Windows::Foundation::IInspectable const & /*sender*/, + Windows::UI::Xaml::RoutedEventArgs const & /*args*/) { + + auto dump = TreeDumpLibrary::VisualTreeDumper::DumpTree( + x_rootElement(), nullptr, {}, TreeDumpLibrary::DumpTreeMode::Json); +} + + void winrt::playground::implementation::MainPage::x_entryPointCombo_SelectionChanged( winrt::Windows::Foundation::IInspectable const & /*sender*/, winrt::Windows::UI::Xaml::Controls::SelectionChangedEventArgs const & /*e*/) { diff --git a/packages/playground/windows/playground/MainPage.h b/packages/playground/windows/playground/MainPage.h index 6f5666185fc..9c865ae117d 100644 --- a/packages/playground/windows/playground/MainPage.h +++ b/packages/playground/windows/playground/MainPage.h @@ -10,6 +10,11 @@ struct MainPage : MainPageT { Windows::Foundation::IInspectable const & /*sender*/, Windows::UI::Xaml::RoutedEventArgs const & /*args*/); + void UpdateTreeDump( + Windows::Foundation::IInspectable const & /*sender*/, + Windows::UI::Xaml::RoutedEventArgs const & /*args*/); + + private: Microsoft::ReactNative::ReactNativeHost Host() noexcept; Microsoft::ReactNative::ReactInstanceSettings InstanceSettings() noexcept; diff --git a/packages/playground/windows/playground/MainPage.xaml b/packages/playground/windows/playground/MainPage.xaml index a4824be9fba..97ed4a95f4e 100644 --- a/packages/playground/windows/playground/MainPage.xaml +++ b/packages/playground/windows/playground/MainPage.xaml @@ -133,6 +133,11 @@ VerticalAlignment="Center" InputScope="Number" MaxLength="5" /> +