├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── build.yml │ ├── codeql-analysis.yml │ └── release.yml ├── .gitignore ├── .snyk ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── karma.conf.ts ├── package-lock.json ├── package.json ├── src ├── VisualBuilderBase.ts ├── common.ts ├── dataViewBuilder │ ├── dataViewBuilder.ts │ ├── matrixBuilder.ts │ └── testDataViewBuilder.ts ├── helpers │ ├── color.ts │ ├── helpers.ts │ └── visualTestHelpers.ts ├── index.ts └── mocks │ ├── mockDownloadService.ts │ ├── mockHostCapabilities.ts │ ├── mockIAcquireAADTokenService.ts │ ├── mockIAuthenticationService.ts │ ├── mockIColorPalette.ts │ ├── mockIEventService.ts │ ├── mockILocale.ts │ ├── mockILocalizationManager.ts │ ├── mockISelectionId.ts │ ├── mockISelectionIdBuilder.ts │ ├── mockISelectionManager.ts │ ├── mockIStorageService.ts │ ├── mockIStorageV2Service.ts │ ├── mockITelemetryService.ts │ ├── mockITooltipService.ts │ ├── mockIVisualLicenseManager.ts │ ├── mockIWebAccessService.ts │ ├── mockSubSelectionService.ts │ ├── mockVisualHost.ts │ └── mocks.ts ├── test ├── dataViewBuilder │ └── testDataViewBuilderTest.ts ├── helpers │ ├── colorTest.ts │ └── helpersTest.ts └── mocks │ ├── mockIColorPaletteTest.ts │ ├── mockISelectionIdBuilderTest.ts │ ├── mockISelectionIdTest.ts │ ├── mockIStorageServiceTest.ts │ ├── mockIStorageV2ServiceTest.ts │ ├── mockITooltipServiceTest.ts │ └── mockVisualHostTest.ts ├── tsconfig.json └── webpack.config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | test 5 | .eslintrc.js 6 | karma.conf.ts 7 | webpack.config.js 8 | lib -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | "browser": true, 4 | "es6": true, 5 | "es2017": true 6 | }, 7 | root: true, 8 | parser: "@typescript-eslint/parser", 9 | parserOptions: { 10 | project: "tsconfig.json", 11 | tsconfigRootDir: ".", 12 | }, 13 | plugins: [ 14 | "powerbi-visuals" 15 | ], 16 | extends: [ 17 | "plugin:powerbi-visuals/recommended" 18 | ], 19 | rules: { 20 | "@typescript-eslint/no-unused-vars": "off" 21 | } 22 | }; -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: build 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [18.x, 20.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: npm audit 28 | continue-on-error: true 29 | - run: npm ci 30 | - run: npm outdated 31 | continue-on-error: true 32 | - run: npm run lint --if-present 33 | - run: npm test 34 | env: 35 | CI: true 36 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "Code scanning - action" 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 4 * * 0' 8 | 9 | jobs: 10 | CodeQL-Build: 11 | permissions: 12 | contents: read 13 | actions: read 14 | security-events: write 15 | 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v2 21 | with: 22 | # We must fetch at least the immediate parents so that if this is 23 | # a pull request then we can checkout the head. 24 | fetch-depth: 2 25 | 26 | # If this run was triggered by a pull request event, then checkout 27 | # the head of the pull request instead of the merge commit. 28 | - run: git checkout HEAD^2 29 | if: ${{ github.event_name == 'pull_request' }} 30 | 31 | # Initializes the CodeQL tools for scanning. 32 | - name: Initialize CodeQL 33 | uses: github/codeql-action/init@v2 34 | # Override language selection by uncommenting this and choosing your languages 35 | # with: 36 | # languages: go, javascript, csharp, python, cpp, java 37 | 38 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 39 | # If this step fails, then you should remove it and run the build manually (see below) 40 | - name: Autobuild 41 | uses: github/codeql-action/autobuild@v2 42 | 43 | # ℹ️ Command-line programs to run using the OS shell. 44 | # 📚 https://git.io/JvXDl 45 | 46 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 47 | # and modify them (or add more) to build your code if your project 48 | # uses a compiled language 49 | 50 | #- run: | 51 | # make bootstrap 52 | # make release 53 | 54 | - name: Perform CodeQL Analysis 55 | uses: github/codeql-action/analyze@v2 56 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | environment: automated-release 10 | runs-on: ubuntu-latest 11 | env: 12 | GH_TOKEN: ${{secrets.GH_TOKEN}} 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Add config details 16 | run: | 17 | git config --global user.name ${{secrets.NAME}} 18 | git config --global user.email ${{secrets.EMAIL}} 19 | - name: Move release to draft 20 | run: gh release edit $TAG_NAME --draft=true 21 | env: 22 | TAG_NAME: ${{ github.event.release.tag_name }} 23 | - name: Run npm install, build and test 24 | run: | 25 | npm i 26 | npm run build 27 | npm run test 28 | npm run lint 29 | - run: zip -r release.zip lib *.md LICENSE package.json 30 | - name: Upload production artifacts 31 | run: | 32 | gh release upload $TAG_NAME "release.zip#release" 33 | gh release edit $TAG_NAME --draft=false 34 | env: 35 | TAG_NAME: ${{ github.event.release.tag_name }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | typings/ 4 | dist/ 5 | *.log 6 | /coverage 7 | /lib 8 | *.lock 9 | .vscode/ 10 | .tmp -------------------------------------------------------------------------------- /.snyk: -------------------------------------------------------------------------------- 1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. 2 | version: v1.15.0 3 | ignore: {} 4 | # patches apply the minimum changes required to fix a vulnerability 5 | patch: 6 | SNYK-JS-LODASH-567746: 7 | - istanbul-instrumenter-loader > istanbul-lib-instrument > babel-types > lodash: 8 | patched: '2020-06-18T05:18:03.428Z' 9 | - istanbul-instrumenter-loader > istanbul-lib-instrument > babel-generator > lodash: 10 | patched: '2020-06-18T05:18:03.428Z' 11 | - istanbul-instrumenter-loader > istanbul-lib-instrument > babel-traverse > lodash: 12 | patched: '2020-06-18T05:18:03.428Z' 13 | - istanbul-instrumenter-loader > istanbul-lib-instrument > babel-template > lodash: 14 | patched: '2020-06-18T05:18:03.428Z' 15 | - istanbul-instrumenter-loader > istanbul-lib-instrument > babel-generator > babel-types > lodash: 16 | patched: '2020-06-18T05:18:03.428Z' 17 | - istanbul-instrumenter-loader > istanbul-lib-instrument > babel-traverse > babel-types > lodash: 18 | patched: '2020-06-18T05:18:03.428Z' 19 | - istanbul-instrumenter-loader > istanbul-lib-instrument > babel-template > babel-types > lodash: 20 | patched: '2020-06-18T05:18:03.428Z' 21 | - istanbul-instrumenter-loader > istanbul-lib-instrument > babel-template > babel-traverse > lodash: 22 | patched: '2020-06-18T05:18:03.428Z' 23 | - istanbul-instrumenter-loader > istanbul-lib-instrument > babel-template > babel-traverse > babel-types > lodash: 24 | patched: '2020-06-18T05:18:03.428Z' 25 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## 6.1.1 3 | * powerbi-visuals-api update to 5.9.0 4 | * Add createOpaqueUtils to IVisualHost mock 5 | 6 | ## 6.1.0 7 | * Now `createVisualHost` method has only **one parameter** with interface `CreateVisualHostOptions` (includes all previous parameters) 8 | * Added mock service for `IVisualLocalStorageV2Service` 9 | * Added mock service for `IVisualSubSelectionService` 10 | 11 | ## 6.0.2 12 | * Added missing mock functions 13 | 14 | ## 6.0.2 15 | * Packages update 16 | * Vulnerabilities patched 17 | * add mock `IAcquireAADTokenService` to `MockIVisualHost` 18 | * add mock method `canDrill` to `MockIVisualHost` 19 | 20 | ## 6.0.1 21 | * Packages update 22 | * Removed coveralls 23 | 24 | ## 6.0.0 25 | * Packages update 26 | * Vulnerabilities patched 27 | 28 | ## 3.2.0 29 | * migrated to `coverage-istanbul-loader` from `istanbul-instrumenter-loader` 30 | 31 | ## 3.1.0 32 | * updated `powerbi-visual-api` to 5.1.0 33 | * accordingly to PowerBI API v5.1.0, removed `enumerateObjectInstances` method support. Details are available here: [DOC article](https://learn.microsoft.com/en-us/power-bi/developer/visuals/format-pane); 34 | 35 | ## 3.0.0 36 | * added `pointerEvent` method to test pointer events 37 | * moved to `eslint` from `tslint` 38 | * updated dependencies to fix vulnerabilities 39 | 40 | ## 2.6.0 41 | * fix for `uuid` 42 | ## 2.5.0 43 | * powerbi-visual-api updated to 4.2.0 44 | * uuid package functionality replaced with crypto method 45 | 46 | ## 2.4.4 47 | * updated dependencies to fix vulnerabilities 48 | 49 | ## 2.4.1 50 | * fixed issue with SVGElement failure 51 | 52 | ## 2.4.0 53 | * added support for matrix dataview 54 | 55 | ## 2.3.4 56 | * fixed Jquery "each" method usage 57 | 58 | ## 2.3.3 59 | * d3 dependencies updated 60 | 61 | ## 2.3.2 62 | * jQuery object usage removed 63 | 64 | ## 2.3.1 65 | * JQuery.each bugfix 66 | 67 | ## 2.3.0 68 | * JQuery and Jasmine-Jquery removed. 69 | * JQuery3dClicks interface is not supported any more. 70 | 71 | ## 2.2.1 72 | * Packages update 73 | 74 | ## 2.2.0 75 | * Update packages to fix vulnerabilities. 76 | * Update powerbi-visual-api to 2.6.0 77 | * Update MockSelectionBuilder (add withTable, withMatrixNode methods) 78 | 79 | ## 2.1.7 80 | * Fix select method of MockISelectionManager. 81 | Selection manager should select all passed selections if multiselect is false 82 | 83 | ## 2.1.6 84 | * Added EventService mock 85 | * Updated SelectionManager mock 86 | 87 | ## 2.1.5 88 | * Added StorageService mock 89 | 90 | ## 2.1.4 91 | * Update packages to fix vulnerabilities 92 | 93 | ## 2.0.0 94 | * Moved to webpack 3, commonjs style. 95 | 96 | ## 1.0.2 97 | * Added multiple colors logic to mock palette. Only red color was available. 98 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | If you would like to contribute to the Power BI visuals TestUtils there are many ways you can help. 3 | 4 | ## Reporting issues 5 | We use [GitHub issues](https://github.com/Microsoft/powerbi-visuals-utils-testutils/issues) as an issue tracker for the repository. Firstly, please search in open issues and try to make sure your problem doesn't exist. If there is an issue, add your comments to this issue. 6 | If there are no issues yet, please open a new one. 7 | 8 | ## Contributing Code 9 | If you would like to contribute an improvement or a fix please take a look at our [Development Workflow](./docs/dev/development-workflow.md) 10 | 11 | ## Sending a Pull Request 12 | Before submitting a pull request please make sure the following is done: 13 | 14 | 1. Fork [the repository](https://github.com/Microsoft/powerbi-visuals-utils-testutils) 15 | 2. Create a branch from the ```master``` 16 | 3. Ensure that the code style checks are passed ([How to lint the source code](./docs/dev/development-workflow.md#how-to-lint-the-source-code)) 17 | 4. Ensure that the unit tests are passed ([How to run unit tests locally](./docs/dev/development-workflow.md#how-to-run-unit-tests-locally)) 18 | 5. Complete the [CLA](#contributor-license-agreement-cla) 19 | 20 | ### Code of Conduct 21 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 22 | 23 | ### Contributor License Agreement (CLA) 24 | You will need to complete a Contributor License Agreement (CLA). Briefly, this agreement testifies that you are granting us permission to use the submitted change according to the terms of the project's license, and that the work being submitted is under appropriate copyright. 25 | 26 | Please submit a Contributor License Agreement (CLA) before submitting a pull request. You may visit [https://cla.microsoft.com](https://cla.microsoft.com) to sign digitally. Alternatively, download the agreement ([Microsoft Contribution License Agreement.docx](https://www.codeplex.com/Download?ProjectName=typescript&DownloadId=822190)), sign, scan, and email it back to . Be sure to include your github user name along with the agreement. Once we have received the signed CLA, we'll review the request. 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Power BI Visualizations 2 | 3 | Copyright (c) Microsoft Corporation 4 | 5 | All rights reserved. 6 | 7 | MIT License 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in all 17 | copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Microsoft Power BI visuals TestUtils 2 | ![Build](https://github.com/microsoft/powerbi-visuals-utils-testutils/workflows/build/badge.svg) [![npm version](https://img.shields.io/npm/v/powerbi-visuals-utils-testutils.svg)](https://www.npmjs.com/package/powerbi-visuals-utils-testutils) [![npm](https://img.shields.io/npm/dm/powerbi-visuals-utils-testutils.svg)](https://www.npmjs.com/package/powerbi-visuals-utils-testutils) 3 | 4 | > TestUtils is a set of mocks and fakes in order to simplify unit testing for Power BI custom visuals 5 | 6 | ## Usage 7 | * [Usage Guide](https://docs.microsoft.com/en-us/power-bi/developer/visuals/utils-test) 8 | 9 | ## 2.3.0 Migration note 10 | 11 | From version 2.3.0 `testDom` function returns `HTMLElement` instead of `JQuery` object. If you are using JQuery in tests, wrap the `testDom` calls with `$(...)` for compatibility: 12 | 13 | ```typescript 14 | // 2.2.1 and below 15 | let element: JQuery = testDom("100", "100"); 16 | // 2.3.0 and above 17 | let element: JQuery = $(testDom("100", "100")); 18 | ``` 19 | 20 | The motivation is not to force JQuery usage. It might be not necessary in tests. In lots of cases `element.get(0)` is the next operation after receiving an element with `testDom`. Now JQuery is not required to use powerbi-visuals-utils-testutils, so you can drop this dependency. If you keep it, you can easily migrate your code to 2.3.* version using the example above. 21 | 22 | 23 | ## Contributing 24 | * Read our [contribution guideline](./CONTRIBUTING.md) to find out how to contribute bugs fixes and improvements 25 | * [Issue Tracker](https://github.com/Microsoft/powerbi-visuals-utils-testutils/issues) 26 | * [Development workflow](./docs/dev/development-workflow.md) 27 | * [How to build](./docs/dev/development-workflow.md#how-to-build) 28 | * [How to run unit tests locally](./docs/dev/development-workflow.md#how-to-run-unit-tests-locally) 29 | 30 | ## License 31 | See the [LICENSE](./LICENSE) file for license rights and limitations (MIT). 32 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | -------------------------------------------------------------------------------- /karma.conf.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visualizations 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | 27 | "use strict"; 28 | 29 | const webpackConfig = require("./webpack.config.js"); 30 | const tsconfig = require("./tsconfig.json"); 31 | 32 | const testRecursivePath = "test/**/*.ts"; 33 | const srcOriginalRecursivePath = "src/**/*.ts"; 34 | const coverageFolder = "coverage"; 35 | 36 | process.env.CHROME_BIN = require('playwright-chromium').chromium.executablePath(); 37 | module.exports = (config) => { 38 | config.set({ 39 | browsers: ["ChromeHeadless"], 40 | colors: true, 41 | frameworks: ["jasmine"], 42 | reporters: [ 43 | "progress", 44 | "coverage-istanbul" 45 | ], 46 | coverageIstanbulReporter: { 47 | reports: ["html", "lcovonly", "text-summary"], 48 | combineBrowserReports: true, 49 | fixWebpackSourcePaths: true 50 | }, 51 | singleRun: true, 52 | plugins: [ 53 | "karma-coverage", 54 | "karma-typescript", 55 | "karma-webpack", 56 | "karma-jasmine", 57 | "karma-sourcemap-loader", 58 | "karma-chrome-launcher", 59 | "karma-coverage-istanbul-reporter" 60 | ], 61 | files: [ 62 | testRecursivePath, 63 | { 64 | pattern: srcOriginalRecursivePath, 65 | included: false, 66 | served: true 67 | } 68 | ], 69 | preprocessors: { 70 | [testRecursivePath]: ["webpack"] 71 | }, 72 | typescriptPreprocessor: { 73 | options: tsconfig.compilerOptions 74 | }, 75 | coverageReporter: { 76 | dir: coverageFolder, 77 | reporters: [ 78 | { type: "html" }, 79 | { type: "lcov" } 80 | ] 81 | }, 82 | remapIstanbulReporter: { 83 | reports: { 84 | lcovonly: coverageFolder + "/lcov.info", 85 | html: coverageFolder, 86 | "text-summary": null 87 | } 88 | }, 89 | mime: { 90 | "text/x-typescript": ["ts", "tsx"] 91 | }, 92 | webpack: webpackConfig, 93 | webpackMiddleware: { 94 | stats: "errors-only" 95 | } 96 | }); 97 | }; 98 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "powerbi-visuals-utils-testutils", 3 | "version": "6.1.1", 4 | "description": "powerbi-visuals-utils-testutils", 5 | "main": "lib/index.js", 6 | "module": "lib/index.js", 7 | "jsnext:main": "lib/index.js", 8 | "types": "lib/index.d.ts", 9 | "sideEffects": false, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/Microsoft/powerbi-visuals-utils-testutils.git" 13 | }, 14 | "keywords": [ 15 | "powerbi-visuals-utils" 16 | ], 17 | "author": "Microsoft", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/Microsoft/powerbi-visuals-utils-testutils/issues" 21 | }, 22 | "homepage": "https://github.com/Microsoft/powerbi-visuals-utils-testutils#readme", 23 | "directories": { 24 | "lib": "lib" 25 | }, 26 | "scripts": { 27 | "build": "tsc", 28 | "build:map": "tsc --sourceMap", 29 | "test": "karma start", 30 | "pretest": "npm run lint", 31 | "lint": "npx eslint . --ext .js,.jsx,.ts,.tsx", 32 | "snyk-protect": "snyk-protect" 33 | }, 34 | "devDependencies": { 35 | "@snyk/protect": "^1.1286.2", 36 | "@types/d3-timer": "3.0.2", 37 | "@types/jasmine": "5.1.4", 38 | "@types/jquery": "^3.5.29", 39 | "@types/karma": "^6.3.8", 40 | "@types/node": "^20.12.3", 41 | "@types/webpack": "5.28.5", 42 | "@typescript-eslint/eslint-plugin": "^5.62.0", 43 | "@typescript-eslint/parser": "^5.62.0", 44 | "chalk": "5.3.0", 45 | "coverage-istanbul-loader": "^3.0.5", 46 | "eslint": "^8.57.0", 47 | "eslint-plugin-powerbi-visuals": "^0.8.1", 48 | "jasmine": "5.1.0", 49 | "karma": "^6.4.3", 50 | "karma-chrome-launcher": "3.2.0", 51 | "karma-coverage": "2.2.1", 52 | "karma-coverage-istanbul-reporter": "3.0.3", 53 | "karma-jasmine": "5.1.0", 54 | "karma-sourcemap-loader": "0.4.0", 55 | "karma-typescript": "5.5.4", 56 | "karma-typescript-preprocessor": "0.4.0", 57 | "karma-webpack": "5.0.1", 58 | "playwright-chromium": "^1.42.1", 59 | "ts-loader": "9.5.1", 60 | "ts-node": "^10.9.2", 61 | "typescript": "^4.9.5", 62 | "webpack": "5.90.3", 63 | "webpack-node-externals": "^3.0.0" 64 | }, 65 | "dependencies": { 66 | "d3-array": "3.2.4", 67 | "d3-timer": "3.0.1", 68 | "lodash-es": "4.17.21", 69 | "powerbi-visuals-api": "^5.9.0", 70 | "powerbi-visuals-utils-formattingmodel": "^6.0.2", 71 | "powerbi-visuals-utils-typeutils": "^6.0.3" 72 | }, 73 | "optionalDependencies": { 74 | "fsevents": "*" 75 | }, 76 | "snyk": true 77 | } 78 | -------------------------------------------------------------------------------- /src/VisualBuilderBase.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visualizations 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | 27 | import { renderTimeout } from "./helpers/visualTestHelpers"; 28 | import { testDom, flushAllD3Transitions } from "./helpers/helpers"; 29 | import { createVisualHost } from "./mocks/mocks"; 30 | 31 | // powerbi 32 | import powerbi from "powerbi-visuals-api"; 33 | import DataView = powerbi.DataView; 34 | import IViewport = powerbi.IViewport; 35 | 36 | // powerbi.extensibility.visual 37 | import IVisual = powerbi.extensibility.visual.IVisual; 38 | import VisualConstructorOptions = powerbi.extensibility.visual.VisualConstructorOptions; 39 | import VisualUpdateOptions = powerbi.extensibility.visual.VisualUpdateOptions; 40 | import IVisualHost = powerbi.extensibility.visual.IVisualHost; 41 | 42 | export abstract class VisualBuilderBase { 43 | public element: HTMLElement; 44 | public viewport: IViewport; 45 | public visualHost: IVisualHost; 46 | 47 | protected visual: T; 48 | 49 | constructor( 50 | width: number = 800, 51 | height: number = 600, 52 | guid?: string, 53 | element: HTMLElement = testDom(height, width)) { 54 | 55 | this.element = element; 56 | 57 | if (guid) { 58 | this.element.classList.add(`visual-${guid}`); 59 | } 60 | 61 | this.visualHost = createVisualHost({}); 62 | 63 | this.viewport = { 64 | height: height, 65 | width: width 66 | }; 67 | 68 | this.init(); 69 | } 70 | 71 | protected abstract build(options: VisualConstructorOptions): T; 72 | 73 | public init(): void { 74 | this.visual = this.build({ 75 | element: this.element, 76 | host: this.visualHost 77 | }); 78 | } 79 | 80 | public destroy(): void { 81 | if (this.visual && this.visual.destroy) { 82 | this.visual.destroy(); 83 | } 84 | } 85 | 86 | public update(dataView: DataView[] | DataView): void { 87 | this.visual.update({ 88 | dataViews: Array.isArray(dataView) ? dataView : [dataView], 89 | viewport: this.viewport 90 | } as VisualUpdateOptions); 91 | } 92 | 93 | public updateRenderTimeout(dataViews: DataView[] | DataView, fn: (() => any), timeout?: number): number { 94 | this.update(dataViews); 95 | 96 | return renderTimeout(fn, timeout); 97 | } 98 | 99 | public updateFlushAllD3Transitions(dataViews: DataView[] | DataView): void { 100 | this.update(dataViews); 101 | 102 | flushAllD3Transitions(); 103 | } 104 | 105 | public updateflushAllD3TransitionsRenderTimeout( 106 | dataViews: DataView[] | DataView, 107 | fn: () => any, 108 | timeout?: number): number { 109 | 110 | this.update(dataViews); 111 | 112 | flushAllD3Transitions(); 113 | 114 | return renderTimeout(fn, timeout); 115 | } 116 | 117 | } -------------------------------------------------------------------------------- /src/common.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visualizations 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | 27 | export const DefaultWaitForRender = 10; -------------------------------------------------------------------------------- /src/dataViewBuilder/dataViewBuilder.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visualizations 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | 27 | import powerbi from "powerbi-visuals-api"; 28 | 29 | import isEmpty from "lodash-es/isEmpty"; 30 | import first from "lodash-es/first"; 31 | import toPlainObject from "lodash-es/toPlainObject"; 32 | import some from "lodash-es/some"; 33 | import includes from "lodash-es/includes"; 34 | import extend from "lodash-es/extend"; 35 | 36 | import DataView = powerbi.DataView; 37 | import ValueTypeDescriptor = powerbi.ValueTypeDescriptor; 38 | import DataViewCategoryColumn = powerbi.DataViewCategoryColumn; 39 | import DataViewMetadataColumn = powerbi.DataViewMetadataColumn; 40 | import DataViewColumnAggregates = powerbi.DataViewColumnAggregates; 41 | import CustomVisualOpaqueIdentity = powerbi.visuals.CustomVisualOpaqueIdentity; 42 | import PrimitiveValue = powerbi.PrimitiveValue; 43 | import DataViewCategorical = powerbi.DataViewCategorical; 44 | import DataViewValueColumn = powerbi.DataViewValueColumn; 45 | import DataViewValueColumns = powerbi.DataViewValueColumns; 46 | import DataViewValueColumnGroup = powerbi.DataViewValueColumnGroup; 47 | 48 | /** Utility for creating a DataView from columns of data. */ 49 | export interface IDataViewBuilderCategorical { 50 | withCategory(options: DataViewBuilderCategoryColumnOptions): IDataViewBuilderCategorical; 51 | withCategories(categories: DataViewCategoryColumn[]): IDataViewBuilderCategorical; 52 | withValues(options: DataViewBuilderValuesOptions): IDataViewBuilderCategorical; 53 | withGroupedValues(options: DataViewBuilderGroupedValuesOptions): IDataViewBuilderCategorical; 54 | 55 | build(): DataView; 56 | } 57 | 58 | export interface DataViewBuilderColumnOptions { 59 | source: DataViewMetadataColumn; 60 | } 61 | 62 | 63 | export interface DataViewBuilderValuesOptions { 64 | columns: DataViewBuilderValuesColumnOptions[]; 65 | } 66 | 67 | export interface DataViewBuilderGroupedValuesOptions { 68 | groupColumn: DataViewBuilderCategoryColumnOptions; 69 | valueColumns: DataViewBuilderColumnOptions[]; 70 | data: DataViewBuilderSeriesData[][]; 71 | } 72 | 73 | export interface DataViewBuilderCategoryColumnOptions extends DataViewBuilderColumnOptions { 74 | values: PrimitiveValue[]; 75 | identityFrom: any; 76 | } 77 | 78 | /** Indicates the source set of identities. */ 79 | export interface DataViewBuilderColumnIdentitySource { 80 | fields: any[]; 81 | identities?: CustomVisualOpaqueIdentity[]; 82 | } 83 | 84 | export interface DataViewBuilderValuesColumnOptions extends 85 | DataViewBuilderColumnOptions, 86 | DataViewBuilderSeriesData { } 87 | 88 | export interface DataViewBuilderSeriesData { 89 | values: PrimitiveValue[]; 90 | highlights?: PrimitiveValue[]; 91 | 92 | /** Client-computed maximum value for a column. */ 93 | maxLocal?: any; 94 | 95 | /** Client-computed maximum value for a column. */ 96 | minLocal?: any; 97 | } 98 | 99 | export function createCategoricalDataViewBuilder(): IDataViewBuilderCategorical { 100 | return new CategoricalDataViewBuilder(); 101 | } 102 | 103 | interface ColumnMetadata { 104 | column: DataViewMetadataColumn; 105 | identityFrom: DataViewBuilderColumnIdentitySource; 106 | values: PrimitiveValue[]; 107 | } 108 | 109 | class CategoricalDataViewBuilder implements IDataViewBuilderCategorical { 110 | private categories: DataViewCategoryColumn[]; 111 | private staticMeasureColumns: DataViewMetadataColumn[]; 112 | private dynamicMeasureColumns: DataViewMetadataColumn[]; 113 | private dynamicSeriesMetadata: ColumnMetadata; 114 | private staticSeriesValues: DataViewBuilderValuesColumnOptions[]; 115 | private dynamicSeriesValues: DataViewBuilderSeriesData[][]; 116 | 117 | constructor() { 118 | this.categories = []; 119 | this.staticMeasureColumns = []; 120 | this.dynamicMeasureColumns = []; 121 | } 122 | 123 | public withCategory(options: DataViewBuilderCategoryColumnOptions): IDataViewBuilderCategorical { 124 | const categoryValues = options.values, 125 | identityFrom = options.identityFrom, 126 | sourceType = options.source.type; 127 | 128 | const categoryColumn: DataViewCategoryColumn = { 129 | source: options.source, 130 | identityFields: options.identityFrom.fields, 131 | identity: options.identityFrom.identities || [], 132 | values: categoryValues, 133 | }; 134 | 135 | if (!options.identityFrom.identities) { 136 | for (let categoryIndex = 0, categoryLength = categoryValues.length; 137 | categoryIndex < categoryLength; 138 | categoryIndex++) { 139 | 140 | categoryColumn.identity.push(getScopeIdentity( 141 | identityFrom, 142 | categoryIndex, 143 | categoryValues[categoryIndex], 144 | sourceType)); 145 | } 146 | } 147 | 148 | if (!this.categories) { 149 | this.categories = []; 150 | } 151 | 152 | this.categories.push(categoryColumn); 153 | 154 | return this; 155 | } 156 | 157 | public withCategories(categories: DataViewCategoryColumn[]): IDataViewBuilderCategorical { 158 | if (isEmpty(this.categories)) { 159 | this.categories = categories; 160 | } 161 | else { 162 | Array.prototype.push.apply(this.categories, categories); 163 | } 164 | 165 | return this; 166 | } 167 | 168 | /** 169 | * Adds static series columns. 170 | * 171 | * Note that it is illegal to have both dynamic series and static series in a visual DataViewCategorical. It is only legal to have them both in 172 | * a query DataViewCategorical, where DataViewTransform is expected to split them up into separate visual DataViewCategorical objects. 173 | */ 174 | public withValues(options: DataViewBuilderValuesOptions): IDataViewBuilderCategorical { 175 | const columns = options.columns; 176 | 177 | for (const column of columns) { 178 | this.staticMeasureColumns.push(column.source); 179 | } 180 | 181 | this.staticSeriesValues = columns; 182 | 183 | return this; 184 | } 185 | 186 | /** 187 | * Adds dynamic series columns. 188 | * 189 | * Note that it is illegal to have both dynamic series and static series in a visual DataViewCategorical. It is only legal to have them both in 190 | * a query DataViewCategorical, where DataViewTransform is expected to split them up into separate visual DataViewCategorical objects. 191 | */ 192 | public withGroupedValues(options: DataViewBuilderGroupedValuesOptions): IDataViewBuilderCategorical { 193 | const groupColumn = options.groupColumn; 194 | 195 | this.dynamicSeriesMetadata = { 196 | column: groupColumn.source, 197 | identityFrom: groupColumn.identityFrom, 198 | values: groupColumn.values, 199 | }; 200 | 201 | const valueColumns = options.valueColumns; 202 | for (const valueColumn of valueColumns) { 203 | this.dynamicMeasureColumns.push(valueColumn.source); 204 | } 205 | 206 | this.dynamicSeriesValues = options.data; 207 | 208 | return this; 209 | } 210 | 211 | private fillData(dataViewValues: DataViewValueColumns) { 212 | const categoryColumn = first(this.categories), 213 | categoryLength = (categoryColumn && categoryColumn.values) 214 | ? categoryColumn.values.length 215 | : 0; 216 | 217 | if (this.hasDynamicSeries()) { 218 | for (let seriesIndex = 0, seriesLength = this.dynamicSeriesMetadata.values.length; 219 | seriesIndex < seriesLength; 220 | seriesIndex++) { 221 | 222 | const seriesMeasures = this.dynamicSeriesValues[seriesIndex]; 223 | 224 | for (let measureIndex = 0, measuresLen = this.dynamicMeasureColumns.length; 225 | measureIndex < measuresLen; 226 | measureIndex++) { 227 | 228 | const groupIndex: number = seriesIndex * measuresLen + measureIndex; 229 | 230 | applySeriesData( 231 | dataViewValues[groupIndex], 232 | seriesMeasures[measureIndex], 233 | categoryLength); 234 | } 235 | } 236 | } 237 | 238 | if (this.hasStaticSeries()) { 239 | // Note: when the target categorical has both dynamic and static series, append static measures at the end of the values array. 240 | const staticColumnsStartingIndex = this.hasDynamicSeries() 241 | ? (this.dynamicSeriesValues.length * this.dynamicMeasureColumns.length) 242 | : 0; 243 | 244 | for (let measureIndex = 0, measuresLen = this.staticMeasureColumns.length; 245 | measureIndex < measuresLen; 246 | measureIndex++) { 247 | 248 | applySeriesData( 249 | dataViewValues[staticColumnsStartingIndex + measureIndex], 250 | this.staticSeriesValues[measureIndex], 251 | categoryLength); 252 | } 253 | } 254 | } 255 | 256 | /** 257 | * Returns the DataView with metadata and DataViewCategorical. 258 | * Returns undefined if the combination of parameters is illegal, such as having both dynamic series and static series when building a visual DataView. 259 | */ 260 | public build(): DataView { 261 | const metadataColumns: DataViewMetadataColumn[] = []; 262 | const categorical: DataViewCategorical = {}; 263 | 264 | const categoryMetadata = this.categories; 265 | const dynamicSeriesMetadata = this.dynamicSeriesMetadata; 266 | 267 | // --- Build metadata columns and value groups --- 268 | for (const columnMetadata of categoryMetadata) { 269 | pushIfNotExists(metadataColumns, columnMetadata.source); 270 | } 271 | 272 | if (this.hasDynamicSeries()) { 273 | // Dynamic series, or Dynamic & Static series. 274 | pushIfNotExists(metadataColumns, dynamicSeriesMetadata.column); 275 | 276 | // categorical.values = DataViewTransform.createValueColumns([], dynamicSeriesMetadata.identityFrom.fields, dynamicSeriesMetadata.column); 277 | categorical.values = createValueColumns([], dynamicSeriesMetadata.identityFrom.fields, dynamicSeriesMetadata.column); 278 | 279 | // For each series value we will make one column per measure 280 | const seriesValues = dynamicSeriesMetadata.values; 281 | for (let seriesIndex = 0; seriesIndex < seriesValues.length; seriesIndex++) { 282 | const seriesValue = seriesValues[seriesIndex]; 283 | const seriesIdentity = getScopeIdentity(dynamicSeriesMetadata.identityFrom, seriesIndex, seriesValue, dynamicSeriesMetadata.column.type); 284 | 285 | for (const measure of this.dynamicMeasureColumns) { 286 | const column = toPlainObject(measure); 287 | 288 | column.groupName = seriesValue; 289 | 290 | pushIfNotExists(metadataColumns, column); 291 | categorical.values.push({ 292 | source: column, 293 | values: [], 294 | identity: seriesIdentity, 295 | }); 296 | } 297 | } 298 | 299 | // If there is no data we should add a column that contains a pointer to the dynamic measure columns, for consistency with the dsrReader 300 | if (seriesValues.length === 0) { 301 | for (const measure of this.dynamicMeasureColumns) { 302 | const column: DataViewMetadataColumn = toPlainObject(measure); 303 | 304 | pushIfNotExists(metadataColumns, column); 305 | 306 | categorical.values.push({ source: column, values: [] }); 307 | } 308 | } 309 | 310 | if (this.hasStaticSeries()) { 311 | // IMPORTANT: In the Dynamic & Static series case, the groups array shall not include any static group. This is to match the behavior of production code that creates query DataView objects. 312 | // Get the current return value of grouped() before adding static measure columns, an use that as the return value of this categorical. 313 | // Otherwise, the default behavior of DataViewValueColumns.grouped() from DataViewTransform.createValueColumns() is to create series groups from all measure columns. 314 | const dynamicSeriesGroups: DataViewValueColumnGroup[] = categorical.values.grouped(); 315 | 316 | categorical.values.grouped = () => dynamicSeriesGroups; 317 | 318 | this.appendStaticMeasureColumns(metadataColumns, categorical.values); 319 | } 320 | } 321 | else { 322 | // Static series only / no series 323 | categorical.values = createValueColumns(); 324 | 325 | this.appendStaticMeasureColumns(metadataColumns, categorical.values); 326 | } 327 | 328 | const categories = this.categories; 329 | if (!isEmpty(categories)) { 330 | categorical.categories = categories; 331 | } 332 | 333 | // --- Fill in data point values --- 334 | this.fillData(categorical.values); 335 | 336 | const dataView: DataView = { 337 | metadata: { 338 | columns: metadataColumns, 339 | }, 340 | categorical: categorical, 341 | }; 342 | 343 | if (this.isLegalDataView(dataView)) { 344 | return dataView; 345 | } 346 | } 347 | 348 | private appendStaticMeasureColumns( 349 | metadataColumns: DataViewMetadataColumn[], 350 | valueColumns: DataViewValueColumns): void { 351 | 352 | if (!isEmpty(this.staticMeasureColumns)) { 353 | for (const column of this.staticMeasureColumns) { 354 | pushIfNotExists(metadataColumns, column); 355 | valueColumns.push({ 356 | source: column, 357 | values: [], 358 | }); 359 | } 360 | } 361 | } 362 | 363 | private isLegalDataView(dataView: DataView): boolean { 364 | if (this.hasDynamicSeries() 365 | && this.hasStaticSeries() 366 | && CategoricalDataViewBuilder.isVisualDataView(dataView.metadata.columns)) { 367 | // It is illegal to have both dynamic series and static series in a visual DataViewCategorical, 368 | // because the DataViewValueColumns interface today cannot express that 100% (see its 'source' property and return value of its 'grouped()' function). 369 | return false; 370 | } 371 | 372 | return true; 373 | } 374 | 375 | /** 376 | * This function infers that if any metadata column has 'queryName', 377 | * then the user of this builder is building a visual DataView (as opposed to query DataView). 378 | * 379 | * @param metadataColumns The complete collection of metadata columns in the categorical. 380 | */ 381 | private static isVisualDataView(metadataColumns: DataViewMetadataColumn[]): boolean { 382 | return !isEmpty(metadataColumns) && 383 | some(metadataColumns, (metadataColumn) => !!metadataColumn.queryName); 384 | } 385 | 386 | private hasDynamicSeries(): boolean { 387 | return !!this.dynamicSeriesMetadata; // In Map visual scenarios, you can have dynamic series without measure columns 388 | } 389 | 390 | private hasStaticSeries(): boolean { 391 | return !!this.staticSeriesValues; 392 | } 393 | } 394 | 395 | function getScopeIdentity( 396 | source: DataViewBuilderColumnIdentitySource, 397 | index: number, 398 | value: PrimitiveValue, 399 | valueType: ValueTypeDescriptor): CustomVisualOpaqueIdentity { 400 | 401 | const identities: CustomVisualOpaqueIdentity[] = source.identities; 402 | 403 | if (identities) { 404 | return identities[index]; 405 | } 406 | 407 | return { 408 | expr: {}, 409 | key: "", 410 | kind: 0 411 | }; 412 | } 413 | 414 | function pushIfNotExists(items: DataViewMetadataColumn[], itemToAdd: DataViewMetadataColumn): void { 415 | if (includes(items, itemToAdd)) { 416 | return; 417 | } 418 | 419 | items.push(itemToAdd); 420 | } 421 | 422 | function applySeriesData( 423 | target: DataViewValueColumn, 424 | source: DataViewBuilderSeriesData, 425 | categoryLength: number): void { 426 | 427 | const values: PrimitiveValue[] = source.values; 428 | 429 | target.values = values; 430 | 431 | const highlights: PrimitiveValue[] = source.highlights; 432 | 433 | if (highlights) { 434 | target.highlights = highlights; 435 | } 436 | 437 | let aggregates: DataViewColumnAggregates; 438 | if (source.minLocal !== undefined) { 439 | if (!aggregates) 440 | aggregates = {}; 441 | 442 | aggregates.minLocal = source.minLocal; 443 | } 444 | 445 | if (source.maxLocal !== undefined) { 446 | if (!aggregates) 447 | aggregates = {}; 448 | 449 | aggregates.maxLocal = source.maxLocal; 450 | } 451 | 452 | if (aggregates) { 453 | target.source.aggregates = aggregates; 454 | extend(target, aggregates); 455 | } 456 | } 457 | 458 | export function createValueColumns( 459 | values: DataViewValueColumn[] = [], 460 | valueIdentityFields?: any[], 461 | source?: DataViewMetadataColumn): DataViewValueColumns { 462 | 463 | const result = values; 464 | setGrouped(result); 465 | 466 | if (valueIdentityFields) { 467 | result.identityFields = valueIdentityFields; 468 | } 469 | 470 | if (source) { 471 | result.source = source; 472 | } 473 | 474 | return result; 475 | } 476 | 477 | export function setGrouped(values: DataViewValueColumns, groupedResult?: DataViewValueColumnGroup[]): void { 478 | values.grouped = groupedResult 479 | ? () => groupedResult 480 | : () => groupValues(values); 481 | } 482 | 483 | /** Group together the values with a common identity. */ 484 | function groupValues(values: DataViewValueColumn[]): DataViewValueColumnGroup[] { 485 | const groups: DataViewValueColumnGroup[] = []; 486 | let currentGroup: DataViewValueColumnGroup; 487 | 488 | for (let i = 0, len = values.length; i < len; i++) { 489 | const value = values[i]; 490 | 491 | if (!currentGroup || currentGroup.identity !== value.identity) { 492 | currentGroup = { 493 | values: [] 494 | }; 495 | 496 | if (value.identity) { 497 | currentGroup.identity = value.identity; 498 | 499 | const source = value.source; 500 | 501 | // allow null, which will be formatted as (Blank). 502 | if (source.groupName !== undefined) 503 | currentGroup.name = source.groupName; 504 | else if (source.displayName) 505 | currentGroup.name = source.displayName; 506 | } 507 | 508 | groups.push(currentGroup); 509 | } 510 | 511 | currentGroup.values.push(value); 512 | } 513 | 514 | return groups; 515 | } -------------------------------------------------------------------------------- /src/dataViewBuilder/matrixBuilder.ts: -------------------------------------------------------------------------------- 1 | 2 | import powerbi from "powerbi-visuals-api"; 3 | import { ValueType } from "powerbi-visuals-utils-typeutils/lib/valueType"; 4 | 5 | import every from "lodash-es/every"; 6 | import map from "lodash-es/map"; 7 | import last from "lodash-es/last"; 8 | import range from "lodash-es/range"; 9 | import flatMap from "lodash-es/flatMap"; 10 | 11 | import DataViewMatrix = powerbi.DataViewMatrix; 12 | import DataViewMetadataColumn = powerbi.DataViewMetadataColumn; 13 | import PrimitiveValue = powerbi.PrimitiveValue; 14 | import DataViewMatrixNode = powerbi.DataViewMatrixNode; 15 | import DataViewObjects = powerbi.DataViewObjects; 16 | 17 | 18 | export interface ResourceTableMetadata { 19 | name: string; 20 | schemaName?: string; 21 | } 22 | 23 | export interface ResourceColumnMetadata { 24 | name: string; 25 | displayName: string; 26 | type: powerbi.ValueTypeDescriptor; 27 | format?: string; 28 | } 29 | 30 | interface ResourceMetadata { 31 | column: ResourceColumnMetadata[]; 32 | } 33 | 34 | interface DataViewBuilderBaseColumnOptions { 35 | metadata: ResourceColumnMetadata; 36 | metadataObjects?: DataViewObjects; 37 | role: string; 38 | index?: number; 39 | queryName?: string; 40 | aggregates?: powerbi.DataViewColumnAggregates; 41 | } 42 | 43 | interface DataViewBuilderCompositeColumnOptions { 44 | columns: DataViewBuilderBaseColumnOptions[]; 45 | dataObjects?: DataViewObjects[]; 46 | } 47 | 48 | interface DataViewBuilderColumnOptions extends DataViewBuilderBaseColumnOptions { 49 | dataObjects?: DataViewObjects[]; 50 | } 51 | 52 | interface LevelMetadata { 53 | sources: powerbi.DataViewMetadataColumn[]; 54 | tableIndices: number[]; 55 | } 56 | 57 | interface HierarchyMetadata { 58 | levels: LevelMetadata[]; 59 | } 60 | // Data Format 61 | // | columnName1 | columnName2 | 62 | // |===========================| 63 | // | cat1 | | 64 | // | | cat2 | 65 | // | | cat2 | 66 | // | | cat2 | 67 | export class DataTable { 68 | table: any[][]; 69 | columnNames: string[]; 70 | rowCount: number; 71 | constructor(table: any[][]) { 72 | // Assume the first row is a header row. 73 | this.columnNames = table[0]; 74 | this.rowCount = table.length - 1; 75 | 76 | // Drop the first (header) row 77 | this.table = table.slice(1); 78 | } 79 | public getColumnIndex(name: string): number { 80 | const index = this.columnNames.findIndex((c) => c === name); 81 | return index; 82 | } 83 | public forEachRow(iterator: (row: any[]) => void): void { 84 | for (let rowIndex = 0; rowIndex < this.rowCount; rowIndex++) { 85 | iterator(this.table[rowIndex]); 86 | } 87 | } 88 | } 89 | 90 | export class ColumnMetadataBuilder { 91 | 92 | constructor(tableMetadata?: ResourceTableMetadata) { 93 | const tableName = tableMetadata?.name ?? "table"; 94 | const schemaName = tableMetadata?.schemaName ?? undefined; 95 | } 96 | 97 | public buildCategoryColumnMetadata(columnOptions: DataViewBuilderBaseColumnOptions): DataViewMetadataColumn { 98 | const metadata = columnOptions.metadata; 99 | 100 | const column = this.buildBasicColumnMetadata(columnOptions); 101 | 102 | column.isMeasure = false; 103 | column.identityExprs = [column.expr]; 104 | 105 | return column; 106 | } 107 | 108 | public buildValueColumnMetadata(columnOptions: DataViewBuilderBaseColumnOptions): DataViewMetadataColumn { 109 | const metadata = columnOptions.metadata; 110 | 111 | const column = this.buildBasicColumnMetadata(columnOptions); 112 | 113 | column.isMeasure = true; 114 | 115 | return column; 116 | } 117 | 118 | private buildBasicColumnMetadata(columnOptions: DataViewBuilderBaseColumnOptions): DataViewMetadataColumn { 119 | const column = columnOptions.metadata; 120 | const columnType = ValueType.fromDescriptor(column.type); 121 | 122 | const columnMetadata: DataViewMetadataColumn = { 123 | displayName: column.displayName, 124 | queryName: columnOptions.queryName ?? column.name, 125 | index: columnOptions.index, 126 | type: columnType, 127 | roles: { [columnOptions.role]: true }, 128 | format: column.format, 129 | }; 130 | 131 | if (columnOptions.metadataObjects) 132 | columnMetadata.objects = columnOptions.metadataObjects; 133 | 134 | if (columnOptions.aggregates) 135 | columnMetadata.aggregates = columnOptions.aggregates; 136 | 137 | return columnMetadata; 138 | } 139 | } 140 | 141 | 142 | export class MatrixDataViewBuilder { 143 | private highlights: PrimitiveValue[][]; 144 | private level: PrimitiveValue[]; 145 | private levelColumn: DataViewMetadataColumn; 146 | private levelIdentities: any[]; // DataViewScopeIdentity 147 | private levelObjects: DataViewObjects[]; 148 | private values: PrimitiveValue[][]; 149 | private valueColumns: DataViewMetadataColumn[]; 150 | 151 | private objects: DataViewObjects; 152 | private readonly columnMetadataBuilder: ColumnMetadataBuilder; 153 | private rowGroupingOptions: DataViewBuilderCompositeColumnOptions[] = []; 154 | private columnGroupingOptions: DataViewBuilderCompositeColumnOptions[] = []; 155 | private valuesOptions: DataViewBuilderColumnOptions[]; 156 | 157 | constructor(private readonly table: DataTable, tableMetadata?: ResourceTableMetadata) { 158 | if (!tableMetadata) 159 | tableMetadata = { name: "table" }; 160 | 161 | this.columnMetadataBuilder = new ColumnMetadataBuilder(tableMetadata); 162 | } 163 | 164 | 165 | public withRowGroups(options: DataViewBuilderCompositeColumnOptions[]): this { 166 | this.rowGroupingOptions.push(...options); 167 | return this; 168 | } 169 | 170 | public withRowGroup(options: DataViewBuilderCompositeColumnOptions): this { 171 | return this.withRowGroups([options]); 172 | } 173 | 174 | public withColumnGroups(options: DataViewBuilderCompositeColumnOptions[]): this { 175 | this.columnGroupingOptions.push(...options); 176 | return this; 177 | } 178 | 179 | public withColumnGroup(options: DataViewBuilderCompositeColumnOptions): this { 180 | return this.withColumnGroups([options]); 181 | } 182 | 183 | public withValues(options: DataViewBuilderBaseColumnOptions[]): this { 184 | this.valuesOptions = options; 185 | return this; 186 | } 187 | 188 | 189 | public createRootMatrixNode(rows: string[][], rowNames: string[], columns?: [][], columnNames?: string[]): DataViewMatrixNode { 190 | const root = { 191 | children: [] 192 | }; 193 | const length = rows[0].length; 194 | if (rows.find(row => row.length !== length) || columns && columns.find(col => col.length !== length)) { 195 | throw "Invalid data input"; 196 | } 197 | 198 | const depth: number = rows.length - 1; 199 | let identityIndexIterator = 0; 200 | let currentRoot: DataViewMatrixNode; 201 | for (let i = 0; i < length; i++) { 202 | currentRoot = root; 203 | for (let j = 0; j <= depth; j++) { 204 | const foundNode = currentRoot.children.filter(node => node.value === rows[i][j]); 205 | if (foundNode.length > 1) { 206 | throw "Unexpected duplicate found"; 207 | } else if (foundNode.length === 1) { 208 | currentRoot = foundNode.pop(); 209 | } else if (foundNode.length === 0) { 210 | const newNode: DataViewMatrixNode = { 211 | children: [], 212 | identity: { 213 | identityIndex: identityIndexIterator++ 214 | }, 215 | value: rows[i][j] 216 | }; 217 | if (j === depth) { 218 | // fill values 219 | } 220 | currentRoot.children.push(newNode); 221 | currentRoot = newNode; 222 | } 223 | } 224 | } 225 | return root; 226 | } 227 | 228 | private buildLevelMetadata(columns: DataViewBuilderBaseColumnOptions[], isMeasure: boolean): LevelMetadata { 229 | const sources: powerbi.DataViewMetadataColumn[] = []; 230 | const tableIndices: number[] = []; 231 | for (const columnOptions of columns) { 232 | const sourceColumn = isMeasure 233 | ? this.columnMetadataBuilder.buildValueColumnMetadata(columnOptions) 234 | : this.columnMetadataBuilder.buildCategoryColumnMetadata(columnOptions); 235 | sources.push(sourceColumn); 236 | tableIndices.push(this.table.getColumnIndex(columnOptions.metadata.name)); 237 | } 238 | 239 | return { 240 | sources: sources, 241 | tableIndices: tableIndices, 242 | }; 243 | } 244 | 245 | private buildHierarchyMetadata(options: DataViewBuilderCompositeColumnOptions[]): HierarchyMetadata { 246 | const hierarchy: HierarchyMetadata = { 247 | levels: [] 248 | }; 249 | 250 | for (const levelOptions of options) { 251 | const columnSet: LevelMetadata = this.buildLevelMetadata(levelOptions.columns, false); 252 | hierarchy.levels.push(columnSet); 253 | } 254 | 255 | return hierarchy; 256 | } 257 | 258 | private sequenceEqual(left: T[], right: U[], comparison: (x: T, y: U) => boolean): boolean { 259 | 260 | // Normalize falsy to null 261 | if (!left) { left = null; } 262 | if (!right) { right = null; } 263 | 264 | // T can be same as U, and it is possible for left and right to be the same array object... 265 | if (left === right) { 266 | return true; 267 | } 268 | 269 | if (!!left !== !!right) { 270 | return false; 271 | } 272 | 273 | const len = left.length; 274 | if (len !== right.length) { 275 | return false; 276 | } 277 | 278 | let i = 0; 279 | while (i < len && comparison(left[i], right[i])) { 280 | ++i; 281 | } 282 | 283 | return i === len; 284 | } 285 | 286 | private findMatchingNodeInList(nodes: powerbi.DataViewMatrixNode[], values: any[]): powerbi.DataViewMatrixNode | undefined { 287 | for (const node of nodes) { 288 | if (this.sequenceEqual(node.levelValues, values, (a, b) => a.value === b)) 289 | return node; 290 | } 291 | } 292 | private compareValue(a: any, b: any): number { 293 | if (a === b) 294 | return 0; 295 | 296 | if (a == null) 297 | return -1; 298 | 299 | if (b == null) 300 | return 1; 301 | 302 | if (a < b) 303 | return -1; 304 | 305 | if (a > b) 306 | return 1; 307 | 308 | return 0; 309 | } 310 | private insertInSortedOrder(node: powerbi.DataViewMatrixNode, list: powerbi.DataViewMatrixNode[]): void { 311 | for (let i = 0; i < list.length; i++) { 312 | const currNode = list[i]; 313 | for (let j = 0; j < currNode.levelValues.length; j++) { 314 | const comparison = this.compareValue(node.levelValues[j].value, currNode.levelValues[j].value); 315 | if (comparison < 0) { 316 | list.splice(i, 0, node); 317 | return; 318 | } 319 | else if (comparison > 0) { 320 | break; 321 | } 322 | } 323 | } 324 | 325 | list.push(node); 326 | } 327 | 328 | private ensureValueInHierarchy( 329 | hierarchy: HierarchyMetadata, 330 | node: powerbi.DataViewMatrixNode, 331 | row: any[], 332 | insertSorted: boolean): powerbi.DataViewMatrixNode { 333 | for (let levelIdx = 0; levelIdx < hierarchy.levels.length; levelIdx++) { 334 | const level = hierarchy.levels[levelIdx]; 335 | // const columnExprs = level.sources.map((col) => col.expr); 336 | 337 | const isLeafLevel = levelIdx === hierarchy.levels.length - 1; 338 | 339 | // TODO: describe the difference here, or create value nodes separately 340 | if (isLeafLevel && every(level.sources, (source) => source.isMeasure)) { 341 | node.children = map(level.sources, (source, i) => { 342 | const child: powerbi.DataViewMatrixNode = { 343 | level: levelIdx, 344 | }; 345 | 346 | // We omit the levelSourceIndex only when it is 0 347 | if (i !== 0) 348 | child.levelSourceIndex = i; 349 | 350 | return child; 351 | }); 352 | } 353 | else { 354 | if (!node.children) { 355 | node.children = []; 356 | // node.childIdentityFields = columnExprs; 357 | } 358 | 359 | const columnValues = level.tableIndices.map((index) => row[index]); 360 | let child = this.findMatchingNodeInList(node.children, columnValues); 361 | if (!child) { 362 | child = { 363 | level: levelIdx, 364 | levelValues: columnValues.map((value, i) => ({ 365 | value: value, 366 | levelSourceIndex: i 367 | })), 368 | // TODO: support a different column for identities to simulate GOK 369 | // identity: mocks.dataViewScopeIdentityCompositeWithEquality(columnExprs, columnValues), 370 | }; 371 | 372 | // DEPRECATED: these values are deprecated 373 | child.value = last(columnValues); 374 | 375 | // let levelSourceIndex = columnValues.length - 1; 376 | // if (levelSourceIndex !== 0) 377 | // child.levelSourceIndex = levelSourceIndex; 378 | 379 | if (insertSorted) 380 | this.insertInSortedOrder(child, node.children); 381 | else 382 | node.children.push(child); 383 | } 384 | 385 | node = child; 386 | } 387 | } 388 | 389 | return node; 390 | } 391 | private findValueInHierarchy( 392 | isMatch: (node: powerbi.DataViewMatrixNode, level: number) => boolean, 393 | root: powerbi.DataViewMatrixNode, 394 | hierarchyDepth: number): number { 395 | let count = 0; 396 | const stack: { 397 | level: number; 398 | partialMatch: boolean; 399 | node: powerbi.DataViewMatrixNode 400 | }[] = []; 401 | 402 | stack.unshift({ 403 | partialMatch: true, 404 | level: -1, 405 | node: root, 406 | }); 407 | 408 | while (stack.length > 0) { 409 | const { node, level, partialMatch } = stack.shift(); 410 | const atLeaf = level >= hierarchyDepth; // columnValues.length - 2; 411 | 412 | const match = partialMatch 413 | && (level === -1 // root node does not have levelValues 414 | || isMatch(node, level)); 415 | 416 | if (!atLeaf) { 417 | stack.unshift(...map(node.children, (child) => ({ 418 | partialMatch: match, 419 | level: level + 1, 420 | node: child, 421 | }))); 422 | } 423 | else { 424 | if (match) { 425 | return count; 426 | } 427 | else { 428 | // Assumes there is only 1 level of children under this node, the nodes for the measures 429 | count += node.children.length; 430 | } 431 | } 432 | } 433 | 434 | return count; 435 | } 436 | private forEachLeaf(node: powerbi.DataViewMatrixNode, visitor: (node: powerbi.DataViewMatrixNode) => void): void { 437 | if (node.children) { 438 | for (const child of node.children) { 439 | this.forEachLeaf(child, visitor); 440 | } 441 | } 442 | else { 443 | visitor(node); 444 | } 445 | } 446 | 447 | private readData( 448 | rowHierarchyMetadata: HierarchyMetadata, 449 | columnHierarchyMetadata: HierarchyMetadata, 450 | measureMetadata: LevelMetadata | undefined, 451 | table: DataTable): powerbi.DataViewMatrix { 452 | const columnLevels = columnHierarchyMetadata.levels.map((level) => ({ 453 | sources: level.sources, 454 | })); 455 | 456 | const matrix: powerbi.DataViewMatrix = { 457 | columns: { 458 | levels: columnLevels, 459 | root: {} 460 | }, 461 | rows: { 462 | levels: rowHierarchyMetadata.levels.map((level) => ({ 463 | sources: level.sources, 464 | })), 465 | root: {}, 466 | }, 467 | valueSources: measureMetadata?.sources ?? [], 468 | }; 469 | 470 | // Fill in empty children array for dimensions that don't have any grouping 471 | if (matrix.columns.levels.length === 0) { matrix.columns.root.children = [] } 472 | if (matrix.rows.levels.length === 0) { matrix.rows.root.children = [] } 473 | 474 | // We do two passes over the data table, The first pass fills in the group instances of row & column hierarchies. 475 | // The second pass fills in the measure values. 476 | // We need to do this as a second pass because the index of the measure value will depend on the column hierarchy. 477 | // See the doc for findValueInHierarchy 478 | 479 | // Pass 1 480 | table.forEachRow((row) => { 481 | this.ensureValueInHierarchy( 482 | columnHierarchyMetadata, 483 | matrix.columns.root, 484 | row, 485 | true, // explicitly sort the column data 486 | ); 487 | this.ensureValueInHierarchy( 488 | rowHierarchyMetadata, 489 | matrix.rows.root, 490 | row, 491 | false, // rows are sorted in the order they appear in the data 492 | ); 493 | }); 494 | 495 | if (measureMetadata) { 496 | // We ignore the last level of values, we only want to find a matching path up to, but not including, the measure nodes 497 | const hierarchyDepth = columnLevels.length - 2; 498 | 499 | // Pass 1.5, fill in intersections with null values 500 | const leafCount = this.findValueInHierarchy( 501 | (_node) => false, 502 | matrix.columns.root, 503 | hierarchyDepth, 504 | ); 505 | this.forEachLeaf( 506 | matrix.rows.root, 507 | (leafNode) => { 508 | leafNode.values = 509 | range(0, leafCount) 510 | .reduce((bag, i) => { 511 | bag[i] = { value: null }; 512 | return bag; 513 | }, 514 | {}); 515 | } 516 | ); 517 | 518 | // Pass 2 519 | table.forEachRow((row) => { 520 | // Find the leaf node in the column heirarchy and calculate the index 521 | const columnValues: any[][] = []; 522 | for (const level of columnHierarchyMetadata.levels) { 523 | columnValues.push(level.tableIndices.map((index) => row[index])); 524 | } 525 | 526 | const isMatch = (node: powerbi.DataViewMatrixNode, level: number) => { 527 | return this.sequenceEqual(columnValues[level], node.levelValues, (value, levelValue) => value === levelValue.value); 528 | }; 529 | 530 | const indexOfColumnLeaf = this.findValueInHierarchy(isMatch, matrix.columns.root, hierarchyDepth); 531 | // debug.assert((leafCount === 0 && indexOfColumnLeaf === 0) || indexOfColumnLeaf < leafCount, 'could not find leaf '); 532 | 533 | // Find the leaf node in the row hierarchy matching this data row. TODO: find? we shouldn't be adding nodes here 534 | const rowNode = this.ensureValueInHierarchy( 535 | rowHierarchyMetadata, 536 | matrix.rows.root, 537 | row, 538 | false, 539 | ); 540 | 541 | if (rowNode.values == null) 542 | rowNode.values = {}; 543 | 544 | for (let i = 0; i < measureMetadata.tableIndices.length; i++) { 545 | const tableIdx = measureMetadata.tableIndices[i]; 546 | const measureValue = row[tableIdx]; 547 | 548 | const valueNode: powerbi.DataViewMatrixNodeValue = { 549 | value: measureValue, 550 | }; 551 | 552 | // We omit the valueSourceIndex when it is 0 553 | if (i !== 0) 554 | valueNode.valueSourceIndex = i; 555 | 556 | rowNode.values[indexOfColumnLeaf + i] = valueNode; 557 | } 558 | }); 559 | } 560 | 561 | return matrix; 562 | } 563 | 564 | public build(): powerbi.DataView { 565 | // Build a column hierarchy based on metadata 566 | let columnHierarchyMetadata: HierarchyMetadata = { levels: [] }; 567 | 568 | if (this.columnGroupingOptions) { 569 | columnHierarchyMetadata = this.buildHierarchyMetadata(this.columnGroupingOptions); 570 | // columnHierarchyMetadata = this.buildHierarchyMetadata(this.columnGroupingOptions); 571 | } 572 | 573 | // Build measures level 574 | // Append the measure level to column hierarchy 575 | let measureMetadata: LevelMetadata; 576 | if (this.valuesOptions) { 577 | measureMetadata = this.buildLevelMetadata(this.valuesOptions, true); 578 | columnHierarchyMetadata.levels.push(measureMetadata); 579 | } 580 | 581 | // Build a row hierarchy based on metadata 582 | const rowHierarchyMetadata = this.buildHierarchyMetadata(this.rowGroupingOptions); 583 | 584 | // Build nodes from data 585 | const matrix = this.readData(rowHierarchyMetadata, columnHierarchyMetadata, measureMetadata, this.table); 586 | 587 | return { 588 | metadata: { 589 | columns: [ 590 | ...flatMap(rowHierarchyMetadata.levels, (level) => level.sources), 591 | ...flatMap(columnHierarchyMetadata.levels, (level) => level.sources), 592 | // ...measureMetadata.sources, 593 | ], 594 | objects: this.objects, 595 | }, 596 | matrix: matrix, 597 | }; 598 | } 599 | 600 | 601 | public buildMatrix(): DataViewMatrix { 602 | const dataViewMatrix: DataViewMatrix = { 603 | rows: { 604 | levels: [{ sources: [this.levelColumn] }], 605 | root: { 606 | children: [], 607 | // childIdentityFields: [SQExprBuilder.fieldExpr({ column: { name: 'col', schema: 's', entity: 'e' } })], 608 | } 609 | }, 610 | columns: { 611 | levels: [], 612 | root: undefined, 613 | }, 614 | valueSources: [] 615 | }; 616 | 617 | for (const index in this.valueColumns) { 618 | dataViewMatrix.valueSources.push(this.valueColumns[index]); 619 | } 620 | 621 | const matrixRowsChildren = dataViewMatrix.rows.root.children; 622 | for (const index in this.level) { 623 | const node: DataViewMatrixNode = { 624 | values: [], 625 | level: 0, 626 | levelValues: [], 627 | value: this.level[index], 628 | // identity: mocks.dataViewScopeIdentity(this.level[index]) 629 | }; 630 | if (this.levelIdentities) { 631 | node.identity = this.levelIdentities[index]; 632 | } 633 | 634 | if (this.levelObjects) { 635 | node.objects = this.levelObjects[index]; 636 | } 637 | 638 | for (let i = 0; i < this.valueColumns.length; i++) { 639 | node.values[i] = { value: this.values[i][index], valueSourceIndex: i, highlight: this.highlights[i][index] }; 640 | } 641 | 642 | matrixRowsChildren.push(node); 643 | } 644 | return dataViewMatrix; 645 | } 646 | } 647 | -------------------------------------------------------------------------------- /src/dataViewBuilder/testDataViewBuilder.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visualizations 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | import powerbi from "powerbi-visuals-api"; 27 | import DataView = powerbi.DataView; 28 | import DataViewObjects = powerbi.DataViewObjects; 29 | import DataViewCategoryColumn = powerbi.DataViewCategoryColumn; 30 | import DataViewMetadataColumn = powerbi.DataViewMetadataColumn; 31 | import PrimitiveValue = powerbi.PrimitiveValue; 32 | import DataViewValueColumn = powerbi.DataViewValueColumn; 33 | import DataViewCategoricalColumn = powerbi.DataViewCategoricalColumn; 34 | 35 | import { DataTable, MatrixDataViewBuilder, ResourceColumnMetadata } from "../dataViewBuilder/matrixBuilder"; 36 | 37 | import { 38 | DataViewBuilderColumnOptions, DataViewBuilderValuesColumnOptions, 39 | DataViewBuilderColumnIdentitySource, IDataViewBuilderCategorical, 40 | createCategoricalDataViewBuilder, DataViewBuilderCategoryColumnOptions, 41 | DataViewBuilderSeriesData 42 | } from "./dataViewBuilder"; 43 | 44 | import isEmpty from "lodash-es/isEmpty"; 45 | import includes from "lodash-es/includes"; 46 | import uniq from "lodash-es/uniq"; 47 | import sum from "lodash-es/sum"; 48 | import map from "lodash-es/map"; 49 | import max from "lodash-es/max"; 50 | import sortBy from "lodash-es/sortBy"; 51 | import range from "lodash-es/range"; 52 | import clone from "lodash-es/clone"; 53 | import toArray from "lodash-es/toArray"; 54 | import groupBy from "lodash-es/groupBy"; 55 | import flatten from "lodash-es/flatten"; 56 | import take from "lodash-es/take"; 57 | import findIndex from "lodash-es/findIndex"; 58 | 59 | export type CustomizeColumnFn = (source: DataViewMetadataColumn) => void; 60 | 61 | export interface TestDataViewBuilderColumnOptions extends DataViewBuilderColumnOptions { 62 | values: any[]; 63 | } 64 | 65 | export interface TestDataViewBuilderCategoryColumnOptions extends TestDataViewBuilderColumnOptions { 66 | objects?: DataViewObjects[]; 67 | isGroup?: boolean; 68 | } 69 | 70 | export interface DataViewBuilderAllColumnOptions { 71 | categories: TestDataViewBuilderCategoryColumnOptions[]; 72 | grouped: TestDataViewBuilderCategoryColumnOptions[]; 73 | values: DataViewBuilderValuesColumnOptions[]; 74 | } 75 | 76 | export abstract class TestDataViewBuilder { 77 | static DataViewName: string = "Data"; 78 | private aggregateFunction: (array: number[]) => number = sum; 79 | 80 | protected static createMatrixDataViewBuilder(table: DataTable) { 81 | const tableMetadata = { 82 | name: "table" 83 | }; 84 | return new MatrixDataViewBuilder(table, tableMetadata); 85 | } 86 | 87 | static setDefaultQueryName(source: DataViewMetadataColumn): DataViewMetadataColumn { 88 | if (!source.queryName) { 89 | source.queryName = TestDataViewBuilder.DataViewName + "." + source.displayName; 90 | } 91 | 92 | return source; 93 | } 94 | 95 | static getDataViewBuilderColumnIdentitySources(options: TestDataViewBuilderColumnOptions[] | TestDataViewBuilderColumnOptions): DataViewBuilderColumnIdentitySource[] { 96 | const optionsArray: TestDataViewBuilderColumnOptions[] = (Array.isArray(options) ? options : [options]); 97 | 98 | // eslint-disable-next-line @typescript-eslint/no-empty-function 99 | const fields = optionsArray.map(() => { }), 100 | optionsIdentityExpressions: any[][] = optionsArray.map((opt) => opt.values) 101 | let identityExpressions: any[] = []; 102 | 103 | if (optionsIdentityExpressions.length > 1) { 104 | const identityExpressionsLength = optionsIdentityExpressions.length, 105 | identityExpressionsValuesLength = max(map(optionsIdentityExpressions, x => x.length)); 106 | 107 | for (let vi = 0; vi < identityExpressionsValuesLength; vi++) { 108 | const current: any = optionsIdentityExpressions[0][vi]; 109 | 110 | identityExpressions.push(current); 111 | } 112 | } else { 113 | identityExpressions = optionsIdentityExpressions[0]; 114 | } 115 | 116 | return optionsArray.map((opt, i) => { 117 | fields: fields, 118 | identities: identityExpressions 119 | }); 120 | } 121 | 122 | static getValuesTable(categories?: DataViewCategoryColumn[], values?: DataViewValueColumn[]): any[][] { 123 | const columns = sortBy((categories || []).concat(values || []), x => x.source.index), 124 | maxLength: number = max(columns.map(x => x.values.length)); 125 | 126 | return range(maxLength).map(i => columns.map(x => x.values[i])); 127 | } 128 | 129 | static createDataViewBuilderColumnOptions( 130 | categoriesColumns: (TestDataViewBuilderCategoryColumnOptions | TestDataViewBuilderCategoryColumnOptions[])[], 131 | valuesColumns: (DataViewBuilderValuesColumnOptions | DataViewBuilderValuesColumnOptions[])[], 132 | filter?: (options: TestDataViewBuilderColumnOptions) => boolean, 133 | customizeColumns?: CustomizeColumnFn): DataViewBuilderAllColumnOptions { 134 | 135 | const filterColumns = filter 136 | ? (options) => Array.isArray(options.values) && filter(options) 137 | : (options) => Array.isArray(options.values); 138 | 139 | const resultCategoriesColumns = isEmpty(categoriesColumns) ? [] : (flatten(categoriesColumns)).filter(filterColumns); 140 | 141 | const resultValuesColumns = isEmpty(valuesColumns) ? [] : (flatten(valuesColumns)).filter(filterColumns); 142 | 143 | const allColumns = (resultCategoriesColumns || []).concat(resultValuesColumns || []); 144 | 145 | allColumns.forEach((x: DataViewCategoricalColumn, i) => x.source.index = i); 146 | 147 | if (customizeColumns) { 148 | allColumns.forEach((column: TestDataViewBuilderColumnOptions) => customizeColumns(column.source)); 149 | } 150 | 151 | allColumns.forEach((column: TestDataViewBuilderColumnOptions) => { 152 | if (column.source.format) { 153 | const objects = column.source.objects = (column.source.objects || {}); 154 | 155 | objects.general = objects.general || {}; 156 | objects.general.formatString = objects.general.formatString || column.source.format; 157 | } 158 | }); 159 | 160 | return { 161 | categories: resultCategoriesColumns.filter(x => !x.isGroup), 162 | grouped: resultCategoriesColumns.filter(x => x.isGroup), 163 | values: resultValuesColumns 164 | }; 165 | } 166 | 167 | static setUpDataViewBuilderColumnOptions( 168 | options: DataViewBuilderAllColumnOptions, 169 | aggregateFunction: (array: number[]) => number): DataViewBuilderAllColumnOptions { 170 | 171 | const resultOptions = clone(options); 172 | 173 | resultOptions.categories = resultOptions.categories && resultOptions.categories.map(x => clone(x)); 174 | resultOptions.values = resultOptions.values && resultOptions.values.map(x => clone(x)); 175 | resultOptions.grouped = resultOptions.grouped && resultOptions.grouped.map(x => clone(x)); 176 | 177 | if (!isEmpty(options.categories)) { 178 | resultOptions.categories.forEach(x => x.source = TestDataViewBuilder.setDefaultQueryName(x.source)); 179 | const allRows: any[][] = TestDataViewBuilder.getValuesTable(options.categories, options.values); 180 | const categoriesLength = options.categories.length; 181 | const grouped = toArray(groupBy(allRows, x => take(x, categoriesLength))); 182 | resultOptions.categories.forEach((c, i) => c.values = grouped.map(x => x[0][i] === undefined ? null : x[0][i])); 183 | 184 | if (!isEmpty(options.values) && isEmpty(options.grouped)) { // Not completed for group categories 185 | resultOptions.values.forEach((c, i) => 186 | c.values = grouped.map(v => aggregateFunction(v.map(x => x[categoriesLength + i] || 0)))); 187 | } 188 | } 189 | 190 | if (!isEmpty(options.values)) { 191 | resultOptions.values.forEach(x => x.source = TestDataViewBuilder.setDefaultQueryName(x.source)); 192 | } 193 | 194 | if (!isEmpty(options.grouped)) { 195 | resultOptions.grouped.forEach(x => x.source = TestDataViewBuilder.setDefaultQueryName(x.source)); 196 | } 197 | 198 | return resultOptions; 199 | } 200 | 201 | static setUpDataView(dataView: DataView, options: DataViewBuilderAllColumnOptions): DataView { 202 | if (!isEmpty(options.categories) && isEmpty(options.grouped)) { 203 | const category = dataView.categorical.categories[0]; 204 | 205 | // Tree. (completed only for one category) 206 | dataView.tree = { 207 | root: { 208 | childIdentityFields: category.identityFields, 209 | children: category.values.map((value: PrimitiveValue, index: number) => { 210 | return { 211 | values: [value], 212 | name: value, 213 | identity: category.identity && category.identity[index] 214 | }; 215 | }) 216 | } 217 | }; 218 | 219 | // Table. 220 | dataView.table = { 221 | columns: dataView.categorical.categories.concat( 222 | dataView.categorical.values || []).map(x => x.source), 223 | identityFields: category.identityFields, 224 | rows: TestDataViewBuilder.getValuesTable(dataView.categorical.categories, dataView.categorical.values) 225 | }; 226 | 227 | if (isEmpty(options.values)) { 228 | delete dataView.categorical.values; 229 | } 230 | } 231 | 232 | if (dataView.categorical.values) { 233 | const grouped = dataView.categorical.values.grouped(); 234 | dataView.categorical.values.grouped = () => grouped; 235 | } 236 | 237 | return dataView; 238 | } 239 | 240 | protected createCategoricalDataViewBuilder( 241 | categoriesColumns: (TestDataViewBuilderCategoryColumnOptions | TestDataViewBuilderCategoryColumnOptions[])[], 242 | valuesColumns: (DataViewBuilderValuesColumnOptions | DataViewBuilderValuesColumnOptions[])[], 243 | columnNames: string[], 244 | customizeColumns?: CustomizeColumnFn): IDataViewBuilderCategorical { 245 | 246 | const builder = createCategoricalDataViewBuilder(); 247 | 248 | const originalOptions = TestDataViewBuilder.createDataViewBuilderColumnOptions( 249 | categoriesColumns, 250 | valuesColumns, 251 | columnNames && (options => includes(columnNames, options.source.displayName)), 252 | customizeColumns); 253 | 254 | const options = TestDataViewBuilder.setUpDataViewBuilderColumnOptions(originalOptions, this.aggregateFunction); 255 | 256 | if (!isEmpty(options.categories)) { 257 | const identityFrom = TestDataViewBuilder.getDataViewBuilderColumnIdentitySources(options.categories); 258 | 259 | builder.withCategories(options.categories.map((category, i) => { 260 | source: category.source, 261 | values: category.values, 262 | objects: category.objects, 263 | identity: identityFrom[i].identities, 264 | identityFields: identityFrom[i].fields 265 | })); 266 | } 267 | 268 | if (!isEmpty(options.grouped)) { 269 | const groupedCategory = options.grouped[0]; // Finished only for one category. 270 | 271 | const categoryValues = originalOptions.categories 272 | && originalOptions.categories[0] 273 | && originalOptions.categories[0].values 274 | || []; 275 | 276 | const uniqueCategoryValues = uniq(categoryValues) || [undefined], 277 | uniqueGroupedValues = uniq(groupedCategory.values); 278 | 279 | builder.withGroupedValues({ 280 | groupColumn: { 281 | source: groupedCategory.source, 282 | values: uniqueGroupedValues, 283 | identityFrom: TestDataViewBuilder.getDataViewBuilderColumnIdentitySources(groupedCategory)[0] 284 | } as DataViewBuilderCategoryColumnOptions, 285 | valueColumns: options.values.map(x => { source: x.source }), 286 | data: uniqueGroupedValues.map(groupedValue => options.values.map((column, columnIndex) => 287 | { 288 | values: column.values && uniqueCategoryValues 289 | .map(categoryValue => { 290 | const index = findIndex(range(categoryValues.length), 291 | i => categoryValues[i] === categoryValue && groupedCategory.values[i] === groupedValue); 292 | return column.values[index] === undefined ? null : column.values[index]; 293 | }), 294 | highlights: column.highlights, 295 | minLocal: column.minLocal, 296 | maxLocal: column.maxLocal 297 | })) 298 | }); 299 | } else if (!isEmpty(options.values)) { 300 | builder.withValues({ columns: options.values }); 301 | } 302 | 303 | const builderBuild = builder.build.bind(builder); 304 | 305 | builder.build = () => { 306 | return TestDataViewBuilder.setUpDataView(builderBuild(), options); 307 | }; 308 | 309 | return builder; 310 | } 311 | 312 | public abstract getDataView(columnNames?: string[]): DataView; 313 | } 314 | -------------------------------------------------------------------------------- /src/helpers/color.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visualizations 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | 27 | export interface RgbColor { 28 | R: number; 29 | G: number; 30 | B: number; 31 | A?: number; 32 | } 33 | 34 | export function getSolidColorStructuralObject(color: string): any { 35 | return { solid: { color: color } }; 36 | } 37 | 38 | export function assertColorsMatch(actual: string, expected: string, invert: boolean = false): void { 39 | const rgbActual: RgbColor = parseColorString(actual), 40 | rgbExpected: RgbColor = parseColorString(expected); 41 | 42 | if (invert) { 43 | return expect(rgbActual).not.toEqual(rgbExpected); 44 | } 45 | 46 | return expect(rgbActual).toEqual(rgbExpected); 47 | } 48 | 49 | export function parseColorString(color: string): RgbColor { 50 | if (color.indexOf("#") >= 0) { 51 | if (color.length === 7) { 52 | // #RRGGBB 53 | const result: RegExpExecArray = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(color); 54 | 55 | if (result == null || result.length < 4) { 56 | return; 57 | } 58 | 59 | return { 60 | R: parseInt(result[1], 16), 61 | G: parseInt(result[2], 16), 62 | B: parseInt(result[3], 16), 63 | }; 64 | } else if (color.length === 4) { 65 | // #RGB 66 | const result: RegExpExecArray = /^#?([a-f\d])([a-f\d])([a-f\d])$/i.exec(color); 67 | 68 | if (result == null || result.length < 4) { 69 | return; 70 | } 71 | 72 | return { 73 | R: parseInt(result[1] + result[1], 16), 74 | G: parseInt(result[2] + result[2], 16), 75 | B: parseInt(result[3] + result[3], 16), 76 | }; 77 | } 78 | } 79 | else if (color.indexOf("rgb(") >= 0) { 80 | // rgb(R, G, B) 81 | const result: RegExpExecArray = /^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/.exec(color); 82 | 83 | if (result == null || result.length < 4) { 84 | return; 85 | } 86 | 87 | return { 88 | R: parseInt(result[1], 10), 89 | G: parseInt(result[2], 10), 90 | B: parseInt(result[3], 10), 91 | }; 92 | } 93 | else if (color.indexOf("rgba(") >= 0) { 94 | // rgba(R, G, B, A) 95 | const result: RegExpExecArray = /^rgba\((\d+),\s*(\d+),\s*(\d+),\s*(\d*(?:\.\d+)?)\)$/.exec(color); 96 | 97 | if (result == null || result.length < 5) { 98 | return; 99 | } 100 | 101 | return { 102 | R: parseInt(result[1], 10), 103 | G: parseInt(result[2], 10), 104 | B: parseInt(result[3], 10), 105 | A: parseFloat(result[4]), 106 | }; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/helpers/helpers.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visualizations 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | import { timerFlush } from "d3-timer"; 27 | 28 | import range from "lodash-es/range"; 29 | import includes from "lodash-es/includes"; 30 | 31 | function each(element: JQuery | HTMLElement, fn: (i: number, el: HTMLElement) => any) { 32 | if (element instanceof Element) { 33 | fn(0, element); 34 | } else { 35 | element.each(fn); 36 | } 37 | } 38 | 39 | export function getUuid() { 40 | const array = new Uint16Array(1) 41 | const uuid = window.crypto.getRandomValues(array); 42 | return uuid; 43 | } 44 | 45 | 46 | export function testDom(height: number | string, width: number | string): HTMLElement { 47 | const element: HTMLElement = document.createElement("div"), 48 | heightWithUnits: string = isFinite(Number(height)) ? `${Number(height)}px` : String(height), 49 | widthWithUnits: string = isFinite(Number(width)) ? `${Number(width)}px` : String(width), 50 | id = "item_" + getUuid(); 51 | 52 | element.id = id; 53 | element.style.height = heightWithUnits; 54 | element.style.width = widthWithUnits; 55 | element.style.position = "relative"; 56 | element.className = "visual"; 57 | 58 | document.body.appendChild(element); 59 | return document.getElementById(id); 60 | } 61 | 62 | export enum ClickEventType { 63 | Default = 0, 64 | CtrlKey = 1, 65 | AltKey = 2, 66 | ShiftKey = 4, 67 | MetaKey = 8, 68 | } 69 | 70 | export enum MouseEventType { 71 | click, 72 | mousedown, 73 | mouseup, 74 | mouseover, 75 | mousemove, 76 | mouseout, 77 | } 78 | 79 | export enum PointerEventType { 80 | pointerover = "pointerover", 81 | pointerenter = "pointerenter", 82 | pointerdown = "pointerdown", 83 | pointermove = "pointermove", 84 | pointerup = "pointerup", 85 | pointercancel = "pointercancel", 86 | pointerout = "pointerout", 87 | pointerleave = "pointerleave", 88 | gotpointercapture = "gotpointercapture", 89 | lostpointercapture = "lostpointercapture" 90 | } 91 | 92 | export enum PointerType { 93 | mouse = "mouse", 94 | pen = "pen", 95 | touch = "touch" 96 | } 97 | 98 | export function d3Click(element: JQuery | HTMLElement | SVGElement, x: number, y: number, eventType?: ClickEventType, button?: number): void { 99 | mouseEvent.call(element, MouseEventType.click, x, y, eventType, button); 100 | } 101 | 102 | export function d3MouseDown(element: JQuery | HTMLElement, x: number, y: number, eventType?: ClickEventType, button?: number): void { 103 | mouseEvent.call(element, MouseEventType.mousedown, x, y, eventType, button); 104 | } 105 | 106 | export function d3MouseUp(element: JQuery | HTMLElement, x: number, y: number, eventType?: ClickEventType, button?: number): void { 107 | mouseEvent.call(element, MouseEventType.mouseup, x, y, eventType); 108 | } 109 | 110 | export function d3MouseOver(element: JQuery | HTMLElement, x: number, y: number, eventType?: ClickEventType, button?: number): void { 111 | mouseEvent.call(element, MouseEventType.mouseover, x, y, eventType, button); 112 | } 113 | 114 | export function d3MouseMove(element: JQuery | HTMLElement, x: number, y: number, eventType?: ClickEventType, button?: number): void { 115 | mouseEvent.call(element, MouseEventType.mousemove, x, y, eventType, button); 116 | } 117 | 118 | export function d3MouseOut(element: JQuery | HTMLElement, x: number, y: number, eventType?: ClickEventType, button?: number): void { 119 | mouseEvent.call(element, MouseEventType.mouseout, x, y, eventType, button); 120 | } 121 | 122 | export function d3KeyEvent(element: JQuery | HTMLElement, typeArg: string, keyArg: string, keyCode: number): void { 123 | keyEvent.call(element, typeArg, keyArg, keyCode); 124 | } 125 | 126 | export function d3TouchStart(element: JQuery | HTMLElement, touchList?: TouchList): void { 127 | each(this, function (i, e) { 128 | const evt = createTouchStartEvent(touchList); 129 | e.dispatchEvent(evt); 130 | }); 131 | } 132 | 133 | export function d3TouchMove(element: JQuery | HTMLElement, touchList?: TouchList): void { 134 | each(this, function (i, e) { 135 | const evt = createTouchMoveEvent(touchList); 136 | e.dispatchEvent(evt); 137 | }); 138 | } 139 | 140 | export function d3TouchEnd(element: JQuery | HTMLElement, touchList?: TouchList): void { 141 | each(this, function (i, e) { 142 | const evt = createTouchEndEvent(touchList); 143 | e.dispatchEvent(evt); 144 | }); 145 | } 146 | 147 | export function pointerEvent(element: JQuery | HTMLElement, pointerEventType: PointerEventType, pointerType: PointerType, x: number, y: number): void { 148 | each(this, function (i, e) { 149 | const evt = createPointerEvent(pointerEventType, pointerType, x, y); 150 | e.dispatchEvent(evt); 151 | }); 152 | } 153 | 154 | export function d3ContextMenu(element: JQuery | HTMLElement, x: number, y: number): void { 155 | each(this, function (i, e) { 156 | const evt = createContextMenuEvent(x, y); 157 | e.dispatchEvent(evt); 158 | }); 159 | } 160 | 161 | // Defining a simulated click event (see http://stackoverflow.com/questions/9063383/how-to-invoke-click-event-programmaticaly-in-d3) 162 | function mouseEvent( 163 | mouseEventType: MouseEventType, 164 | x: number, 165 | y: number, 166 | eventType?: ClickEventType, 167 | button?: number): void { 168 | 169 | const clickEventType: ClickEventType = eventType || ClickEventType.Default; 170 | 171 | each(this, function (i, e) { 172 | const evt: MouseEvent = createMouseEvent(mouseEventType, clickEventType, x, y, button); 173 | 174 | e.dispatchEvent(evt); 175 | }); 176 | } 177 | 178 | function keyEvent(typeArg: string, keyArg: string, keyCode: number): void { 179 | each(this, function (i, e) { 180 | const evt: KeyboardEvent = new KeyboardEvent(typeArg, 181 | { 182 | key: keyArg, 183 | bubbles: true, 184 | cancelable: true, 185 | location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD, 186 | repeat: false, 187 | view: window, 188 | } as KeyboardEventInit); 189 | e.dispatchEvent(evt); 190 | }); 191 | } 192 | 193 | /** 194 | * Creates mouse event 195 | * @param eventType {ClickEventType}. 196 | * @param x clientX. 197 | * @param y clientY. 198 | * @param eventName {string} Event name e.g click, mousedown ... 199 | */ 200 | export function createMouseEvent( 201 | mouseEventType: MouseEventType, 202 | eventType: ClickEventType, 203 | x: number, 204 | y: number, 205 | button: number = 0): MouseEvent { 206 | 207 | const clickEventType: ClickEventType = eventType || ClickEventType.Default, 208 | evt: MouseEvent = document.createEvent("MouseEvents"); 209 | 210 | evt.initMouseEvent( 211 | MouseEventType[mouseEventType], // type 212 | true, // canBubble 213 | true, // cancelable 214 | window, // view 215 | 0, // detail 216 | x, // screenX 217 | y, // screenY 218 | x, // clientX 219 | y, // clientY 220 | !!(clickEventType & ClickEventType.CtrlKey), // ctrlKey 221 | !!(clickEventType & ClickEventType.AltKey), // altKey 222 | !!(clickEventType & ClickEventType.ShiftKey), // shiftKey 223 | !!(clickEventType & ClickEventType.MetaKey), // metaKey 224 | button, // button 225 | null); // relatedTarget 226 | 227 | return evt; 228 | } 229 | 230 | export function createTouchStartEvent(touchList?: TouchList): UIEvent { 231 | // NOTE: phantomjs does not support TouchEvent 232 | const evt: UIEvent = document.createEvent("UIEvent"); 233 | 234 | evt.initEvent("touchstart", true, true); 235 | 236 | if (touchList) { 237 | (evt).touches = touchList; 238 | } 239 | 240 | return evt; 241 | } 242 | 243 | export function createTouchMoveEvent(touchList?: TouchList): UIEvent { 244 | // NOTE: phantomjs does not support TouchEvent 245 | const evt: UIEvent = document.createEvent("UIEvent"); 246 | 247 | evt.initEvent("touchmove", true, true); 248 | 249 | if (touchList) { 250 | (evt).touches = touchList; 251 | } 252 | 253 | return evt; 254 | } 255 | 256 | export function createTouchEndEvent(touchList?: TouchList): UIEvent { 257 | // NOTE: phantomjs does not support TouchEvent 258 | const evt: UIEvent = document.createEvent("UIEvent"); 259 | 260 | evt.initEvent("touchend", true, true); 261 | 262 | if (touchList) { 263 | (evt).touches = touchList; 264 | } 265 | 266 | return evt; 267 | } 268 | 269 | export function createPointerEvent(pointerEventType: PointerEventType, pointerType: PointerType, x: number, y: number): PointerEvent { 270 | const evt: PointerEvent = new PointerEvent(pointerEventType, { 271 | pointerId: 1, 272 | bubbles: true, 273 | cancelable: true, 274 | pointerType: pointerType, 275 | width: 1, 276 | height: 1, 277 | isPrimary: true, 278 | clientX: x, 279 | clientY: y 280 | }); 281 | return evt; 282 | } 283 | 284 | export function createContextMenuEvent(x: number, y: number): MouseEvent { 285 | const evt: MouseEvent = document.createEvent("MouseEvents"); 286 | 287 | evt.initMouseEvent( 288 | "contextmenu", // type 289 | true, // canBubble 290 | true, // cancelable 291 | window, // view 292 | 0, // detail 293 | x, // screenX 294 | y, // screenY 295 | x, // clientX 296 | y, // clientY 297 | false, // ctrlKey 298 | false, // altKey 299 | false, // shiftKey 300 | false, // metaKey 301 | 0, // button 302 | null); // relatedTarget 303 | 304 | return evt; 305 | } 306 | 307 | export function createTouchesList(touches: Touch[]): TouchList { 308 | const touchesList: TouchList = touches; 309 | 310 | (touches).item = (index: number): any => { 311 | return this.arr[index]; 312 | }; 313 | 314 | return touchesList; 315 | } 316 | 317 | export function createTouch(x: number, y: number, element: JQuery | HTMLElement, id: number = 0): Touch { 318 | const newElement: HTMLElement = Object.prototype.hasOwnProperty.call(element, "get") ? (element).get(0) : element; 319 | 320 | return { 321 | pageX: x, 322 | pageY: y, 323 | screenX: x, 324 | screenY: y, 325 | clientX: x, 326 | clientY: y, 327 | target: newElement, 328 | identifier: id, 329 | force: 1, 330 | radiusX: 1, 331 | radiusY: 1, 332 | rotationAngle: 0, 333 | }; 334 | } 335 | 336 | export function clickElement(element: JQuery | HTMLElement, ctrlKey: boolean = false): void { 337 | const newElement: HTMLElement = Object.prototype.hasOwnProperty.call(element, "get") ? (element).get(0) : element; 338 | 339 | 340 | const rect = newElement.getBoundingClientRect(), 341 | coordinatesTop: number = rect.top + document.body.scrollTop, 342 | coordinatesLeft: number = rect.left + document.body.scrollLeft, 343 | width: number = newElement.offsetWidth, 344 | height: number = newElement.offsetHeight, 345 | eventType: ClickEventType = ctrlKey 346 | ? ClickEventType.CtrlKey 347 | : ClickEventType.Default; 348 | 349 | d3Click(element, 350 | coordinatesLeft + (width / 2), 351 | coordinatesTop + (height / 2), 352 | eventType); 353 | } 354 | 355 | /** 356 | * Forces all D3 transitions to complete. 357 | * Normally, zero-delay transitions are executed after an instantaneous delay (<10ms). 358 | * This can cause a brief flicker if the browser renders the page twice: once at the end of the first event loop, 359 | * then again immediately on the first timer callback. By flushing the timer queue at the end of the first event loop, 360 | * you can run any zero-delay transitions immediately and avoid the flicker. 361 | * 362 | * These flickers are noticable on IE, and with a large number of webviews(not recommend you ever do this) on iOS. 363 | */ 364 | export function flushAllD3Transitions() { 365 | const now = Date.now; 366 | Date.now = function () { return Infinity; }; 367 | // timer.flush(); 368 | timerFlush(); 369 | Date.now = now; 370 | } 371 | 372 | export function getRandomNumbers(count: number, min: number = 0, max: number = 1): number[] { 373 | return range(count).map(x => getRandomNumber(min, max)); 374 | } 375 | 376 | export function getRandomNumber( 377 | min: number, 378 | max: number, 379 | exceptionList?: number[], 380 | changeResult: (value: any) => number = x => x): number { 381 | 382 | const cryptoObj = (window).crypto || (window).msCrypto; 383 | const randomValue = +("0." + cryptoObj.getRandomValues(new Uint8Array(1))); 384 | const result = changeResult(randomValue * (max - min) + min); 385 | 386 | if (exceptionList && exceptionList.length && includes(exceptionList, result)) { 387 | return getRandomNumber(min, max, exceptionList); 388 | } 389 | 390 | return result; 391 | } -------------------------------------------------------------------------------- /src/helpers/visualTestHelpers.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visualizations 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | import { DefaultWaitForRender } from "../common"; 27 | 28 | export function renderTimeout(fn: () => any, timeout: number = DefaultWaitForRender): number { 29 | return window.setTimeout(fn, timeout); 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { MockILocale } from "./mocks/mockILocale"; 2 | import { MockITooltipService } from "./mocks/mockITooltipService"; 3 | import { MockISelectionManager } from "./mocks/mockISelectionManager"; 4 | import { MockISelectionIdBuilder } from "./mocks/mockISelectionIdBuilder"; 5 | import { MockIAuthenticationService } from "./mocks/mockIAuthenticationService"; 6 | import { MockITelemetryService } from "./mocks/mockITelemetryService"; 7 | import { MockILocalizationManager } from "./mocks/mockILocalizationManager"; 8 | import { MockISelectionId } from "./mocks/mockISelectionId"; 9 | import { MockIColorPalette } from "./mocks/mockIColorPalette"; 10 | import { MockIVisualHost } from "./mocks/mockVisualHost"; 11 | import { MockIEventService } from "./mocks/mockIEventService"; 12 | import { MockIStorageService } from "./mocks/mockIStorageService"; 13 | import { MockIStorageV2Service } from "./mocks/mockIStorageV2Service"; 14 | import { MockHostCapabilities } from "./mocks/mockHostCapabilities"; 15 | import { MockDownloadService } from "./mocks/mockDownloadService"; 16 | import { MockIVisualLicenseManager } from "./mocks/mockIVisualLicenseManager"; 17 | import { MockIWebAccessService } from "./mocks/mockIWebAccessService"; 18 | import { MockIAcquireAADTokenService } from "./mocks/mockIAcquireAADTokenService"; 19 | import { MockSubSelectionService } from "./mocks/mockSubSelectionService"; 20 | 21 | import { 22 | testDom, 23 | createContextMenuEvent, 24 | createMouseEvent, 25 | createTouch, 26 | createTouchesList, 27 | createTouchEndEvent, 28 | createTouchMoveEvent, 29 | createTouchStartEvent, 30 | getRandomNumber, 31 | getRandomNumbers, 32 | clickElement, 33 | ClickEventType, 34 | MouseEventType 35 | } from "./helpers/helpers"; 36 | 37 | import { 38 | assertColorsMatch, 39 | getSolidColorStructuralObject, 40 | RgbColor, 41 | parseColorString 42 | } from "./helpers/color"; 43 | 44 | import { 45 | d3Click, 46 | d3TouchStart, 47 | d3TouchMove, 48 | d3TouchEnd, 49 | PointerEventType, 50 | PointerType, 51 | pointerEvent, 52 | d3ContextMenu, 53 | d3MouseDown, 54 | d3MouseUp, 55 | d3MouseOver, 56 | d3MouseMove, 57 | d3MouseOut, 58 | d3KeyEvent 59 | } from "./helpers/helpers"; 60 | 61 | import { 62 | renderTimeout 63 | } from "./helpers/visualTestHelpers"; 64 | 65 | import { 66 | createVisualHost, 67 | createSelectionId, 68 | createColorPalette, 69 | createLocale, 70 | createSelectionIdBuilder, 71 | createSelectionManager, 72 | createTooltipService, 73 | createHostCapabilities, 74 | CreateVisualHostOptions 75 | } from "./mocks/mocks"; 76 | import { setGrouped, createValueColumns, createCategoricalDataViewBuilder } from "./dataViewBuilder/dataViewBuilder"; 77 | import { MatrixDataViewBuilder} from "./dataViewBuilder/matrixBuilder"; 78 | import * as testDataViewBuilder from "./dataViewBuilder/testDataViewBuilder"; 79 | import * as visualBuilderBase from "./VisualBuilderBase"; 80 | import VisualBuilderBase = visualBuilderBase.VisualBuilderBase; 81 | 82 | export { 83 | MatrixDataViewBuilder, 84 | 85 | testDom, 86 | createContextMenuEvent, 87 | createMouseEvent, 88 | createTouch, 89 | createTouchesList, 90 | createTouchEndEvent, 91 | createTouchMoveEvent, 92 | createTouchStartEvent, 93 | clickElement, 94 | createVisualHost, 95 | createSelectionId, 96 | createColorPalette, 97 | createLocale, 98 | createSelectionIdBuilder, 99 | createSelectionManager, 100 | createTooltipService, 101 | createHostCapabilities, 102 | CreateVisualHostOptions, 103 | setGrouped, 104 | createValueColumns, 105 | createCategoricalDataViewBuilder, 106 | VisualBuilderBase, 107 | testDataViewBuilder, 108 | getRandomNumber, 109 | getRandomNumbers, 110 | MockILocale, 111 | MockITooltipService, 112 | MockISelectionManager, 113 | MockISelectionIdBuilder, 114 | MockIAuthenticationService, 115 | MockITelemetryService, 116 | MockILocalizationManager, 117 | MockISelectionId, 118 | MockIColorPalette, 119 | MockIVisualHost, 120 | MockIEventService, 121 | MockIStorageService, 122 | MockIStorageV2Service, 123 | MockHostCapabilities, 124 | MockDownloadService, 125 | MockIVisualLicenseManager, 126 | MockIWebAccessService, 127 | MockIAcquireAADTokenService, 128 | MockSubSelectionService, 129 | renderTimeout, 130 | assertColorsMatch, 131 | getSolidColorStructuralObject, 132 | RgbColor, 133 | parseColorString, 134 | 135 | d3Click, 136 | d3TouchStart, 137 | d3TouchMove, 138 | d3TouchEnd, 139 | PointerEventType, 140 | PointerType, 141 | pointerEvent, 142 | d3ContextMenu, 143 | d3MouseDown, 144 | d3MouseUp, 145 | d3MouseOver, 146 | d3MouseMove, 147 | d3MouseOut, 148 | d3KeyEvent, 149 | 150 | ClickEventType, 151 | MouseEventType 152 | }; -------------------------------------------------------------------------------- /src/mocks/mockDownloadService.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visualizations 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | 27 | // powerbi 28 | import powerbi from "powerbi-visuals-api"; 29 | import IPromise = powerbi.IPromise; 30 | import PrivilegeStatus = powerbi.PrivilegeStatus; 31 | 32 | // powerbi.extensibility 33 | import IDownloadService = powerbi.extensibility.IDownloadService; 34 | 35 | export class MockDownloadService implements IDownloadService { 36 | public exportVisualsContent(content: string, fileName: string, fileType: string, fileDescription: string): IPromise { 37 | return new Promise((resolve, reject) => { 38 | resolve(); 39 | }) as any; 40 | } 41 | public exportVisualsContentExtended(content: string, fileName: string, fileType: string, fileDescription: string): IPromise { 42 | return new Promise((resolve, reject) => { 43 | resolve(); 44 | }) as any; 45 | } 46 | public exportStatus(): IPromise { 47 | return new Promise((resolve, reject) => { 48 | resolve(PrivilegeStatus.Allowed); 49 | }) as any; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/mocks/mockHostCapabilities.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visualizations 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | 27 | // powerbi 28 | import powerbi from "powerbi-visuals-api"; 29 | 30 | // powerbi.extensibility 31 | import HostCapabilities = powerbi.extensibility.HostCapabilities; 32 | 33 | export class MockHostCapabilities implements HostCapabilities { 34 | public allowInteractions: boolean; 35 | public allowModalDialog: boolean; 36 | 37 | constructor( 38 | allowInteractions: boolean = false, 39 | allowModalDialog: boolean = false 40 | ) { 41 | this.allowInteractions = allowInteractions; 42 | this.allowModalDialog = allowModalDialog; 43 | } 44 | 45 | public set hostCapabilities(capabilities: HostCapabilities) { 46 | this.allowInteractions = capabilities.allowInteractions; 47 | this.allowModalDialog = capabilities.allowModalDialog; 48 | } 49 | 50 | public get hostCapabilities(): HostCapabilities { 51 | return { 52 | allowInteractions: this.allowModalDialog, 53 | allowModalDialog: this.allowModalDialog, 54 | }; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/mocks/mockIAcquireAADTokenService.ts: -------------------------------------------------------------------------------- 1 | import powerbi from "powerbi-visuals-api"; 2 | import IAcquireAADTokenService = powerbi.extensibility.IAcquireAADTokenService; 3 | import AcquireAADTokenResult = powerbi.extensibility.AcquireAADTokenResult; 4 | import PrivilegeStatus = powerbi.PrivilegeStatus; 5 | 6 | export class MockIAcquireAADTokenService implements IAcquireAADTokenService { 7 | acquireAADToken(): powerbi.IPromise { 8 | return new Promise((resolve) => { 9 | const token: AcquireAADTokenResult = { accessToken: "" }; 10 | resolve(token); 11 | }) as any; 12 | } 13 | 14 | acquireAADTokenstatus(): powerbi.IPromise { 15 | return new Promise((resolve) => { 16 | resolve(PrivilegeStatus.Allowed); 17 | }) as any; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/mocks/mockIAuthenticationService.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visualizations 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | 27 | // powerbi.extensibility 28 | import powerbi from "powerbi-visuals-api"; 29 | import IAuthenticationService = powerbi.extensibility.IAuthenticationService; 30 | 31 | export class MockIAuthenticationService implements IAuthenticationService { 32 | private token; 33 | 34 | constructor(token: string) { 35 | this.token = token; 36 | } 37 | 38 | getResourceUrl(visualId?: string): powerbi.IPromise { 39 | throw new Error("Method not implemented."); 40 | } 41 | getAADAuthenticationToken(visualId?: string): powerbi.IPromise { 42 | throw new Error("Method not implemented."); 43 | } 44 | 45 | getAADToken(visualId?: string): powerbi.IPromise { 46 | 47 | return new Promise((resolve, reject) => { 48 | resolve(this.token); 49 | }) as any; 50 | } 51 | } -------------------------------------------------------------------------------- /src/mocks/mockIColorPalette.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visualizations 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | 27 | // powerbi.extensibility 28 | import powerbi from "powerbi-visuals-api"; 29 | import IColorPalette = powerbi.extensibility.ISandboxExtendedColorPalette; 30 | import IColorInfo = powerbi.IColorInfo; 31 | 32 | export class MockIColorPalette implements IColorPalette { 33 | /** 34 | * This array represents the default colors of the IColorPalette. 35 | */ 36 | private static DefaultColors: IColorInfo[] = [ 37 | // First loop 38 | { value: "#01B8AA" }, 39 | { value: "#374649" }, 40 | { value: "#FD625E" }, 41 | { value: "#F2C80F" }, 42 | { value: "#5F6B6D" }, 43 | { value: "#8AD4EB" }, 44 | { value: "#FE9666" }, // Bethany"s Mango 45 | { value: "#A66999" }, 46 | { value: "#3599B8" }, 47 | { value: "#DFBFBF" }, 48 | 49 | // Second loop 50 | { value: "#4AC5BB" }, 51 | { value: "#5F6B6D" }, 52 | { value: "#FB8281" }, 53 | { value: "#F4D25A" }, 54 | { value: "#7F898A" }, 55 | { value: "#A4DDEE" }, 56 | { value: "#FDAB89" }, 57 | { value: "#B687AC" }, 58 | { value: "#28738A" }, 59 | { value: "#A78F8F" }, 60 | 61 | // Third loop 62 | { value: "#168980" }, 63 | { value: "#293537" }, 64 | { value: "#BB4A4A" }, 65 | { value: "#B59525" }, 66 | { value: "#475052" }, 67 | { value: "#6A9FB0" }, 68 | { value: "#BD7150" }, 69 | { value: "#7B4F71" }, 70 | { value: "#1B4D5C" }, 71 | { value: "#706060" }, 72 | 73 | // Fourth loop 74 | { value: "#0F5C55" }, 75 | { value: "#1C2325" }, 76 | { value: "#7D3231" }, 77 | { value: "#796419" }, 78 | { value: "#303637" }, 79 | { value: "#476A75" }, 80 | { value: "#7E4B36" }, 81 | { value: "#52354C" }, 82 | { value: "#0D262E" }, 83 | { value: "#544848" }, 84 | ]; 85 | 86 | private colors: IColorInfo[]; 87 | private colorIndex: number = 0; 88 | 89 | constructor(colors: IColorInfo[] = []) { 90 | this.colors = colors; 91 | } 92 | 93 | public getColor(key: string): IColorInfo { 94 | let color = this.colors[key]; 95 | if (color) { 96 | return color; 97 | } 98 | 99 | const colors = MockIColorPalette.DefaultColors; 100 | color = this.colors[key] = colors[this.colorIndex++]; 101 | 102 | if (this.colorIndex >= colors.length) { 103 | this.colorIndex = 0; 104 | } 105 | 106 | return color; 107 | } 108 | 109 | public isHighContrast: true; 110 | public foreground = {value: "#333333" }; 111 | public foregroundLight = {value: "#FFF" }; 112 | public foregroundDark = {value: "#000" }; 113 | public foregroundNeutralLight = {value: "#EAEAEA" }; 114 | public foregroundNeutralDark = {value: "#212121" }; 115 | public foregroundNeutralSecondary = {value: "#666666" }; 116 | public foregroundNeutralSecondaryAlt = {value: "#777777" }; 117 | public foregroundNeutralSecondaryAlt2 = {value: "#888888" }; 118 | public foregroundNeutralTertiary = {value: "#A6A6A6" }; 119 | public foregroundNeutralTertiaryAlt = {value: "#C8C8C8" }; 120 | public foregroundSelected = {value: "#333333" }; 121 | public foregroundButton = {value: "#666666" }; 122 | public background = {value: "#FFF" }; 123 | public backgroundLight = {value: "#EAEAEA" }; 124 | public backgroundNeutral = {value: "#C8C8C8" }; 125 | public backgroundDark = {value: "#000" }; 126 | public hyperlink = {value: "#1F3A93" }; 127 | public visitedHyperlink = {value: "#551A8B" }; 128 | public mapPushpin = {value: "#FF5F00" }; 129 | public shapeStroke = {value: "#01B8AA" }; 130 | public selection = {value: undefined }; 131 | public separator = {value: undefined }; 132 | public negative = {value: undefined }; 133 | public neutral = {value: undefined }; 134 | public positive = {value: undefined }; 135 | 136 | public reset(): IColorPalette { 137 | return this; 138 | } 139 | } -------------------------------------------------------------------------------- /src/mocks/mockIEventService.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visualizations 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | 27 | /* eslint-disable @typescript-eslint/no-empty-function */ 28 | 29 | // powerbi 30 | import powerbi from "powerbi-visuals-api"; 31 | import IVisualEventService = powerbi.extensibility.IVisualEventService; 32 | import VisualUpdateOptions = powerbi.extensibility.VisualUpdateOptions; 33 | 34 | export class MockIEventService implements IVisualEventService { 35 | /** 36 | * Called just before the actual rendering was started. 37 | */ 38 | renderingStarted(options: VisualUpdateOptions): void { } 39 | 40 | /** 41 | * Called immediately after finishing rendering successfully 42 | */ 43 | renderingFinished(options: VisualUpdateOptions): void { } 44 | 45 | /** 46 | * Called when rendering failed with optional reason string 47 | */ 48 | renderingFailed(options: VisualUpdateOptions, reason?: string): void { } 49 | } -------------------------------------------------------------------------------- /src/mocks/mockILocale.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visualizations 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | 27 | import keys from "lodash-es/keys"; 28 | 29 | export class MockILocale { 30 | private currentLocale: string; 31 | // eslint-disable-next-line @typescript-eslint/ban-types 32 | private locales: Object; 33 | // eslint-disable-next-line @typescript-eslint/ban-types 34 | private static DefaultLocales: Object = { 35 | "en": "en-US", 36 | "ru": "ru-RU" 37 | }; 38 | 39 | // eslint-disable-next-line @typescript-eslint/ban-types 40 | constructor(locales: Object = MockILocale.DefaultLocales) { 41 | this.locales = locales; 42 | this.locale = keys(locales)[0]; 43 | } 44 | 45 | public set locale(key: string) { 46 | this.currentLocale = this.locales[key] || MockILocale.DefaultLocales[key] || MockILocale.DefaultLocales["en"]; 47 | } 48 | 49 | public get locale(): string { 50 | return this.currentLocale; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/mocks/mockILocalizationManager.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visualizations 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | // powerbi.extensibility 27 | import powerbi from "powerbi-visuals-api"; 28 | import ILocalizationManager = powerbi.extensibility.ILocalizationManager; 29 | 30 | export class MockILocalizationManager implements ILocalizationManager { 31 | private displayNames: {[key: string]: string}; 32 | 33 | private static DefaultDispalyNames = { 34 | "Visual_General": "General", 35 | "Visual_General_Bins": "Bins", 36 | "Visual_DataColors": "Data colors", 37 | "Visual_DataColors_Fill": "Fill", 38 | "Visual_XAxis": "X-Axis", 39 | "Visual_Show": "Show", 40 | "Visual_Color": "Color", 41 | "Visual_Title": "Title", 42 | "Visual_DisplayUnits": "Display Units", 43 | "Visual_Precision": "Decimal Places", 44 | "Visual_Style": "Style", 45 | "Visual_Style_ShowTitleOnly": "Show title only", 46 | "Visual_Style_ShowUnitOnly": "Show unit only", 47 | "Visual_Style_ShowBoth": "Show both", 48 | "Visual_YAxis": "Y-Axis", 49 | "Visual_YAxis_Start": "Start", 50 | "Visual_YAxis_End": "End", 51 | "Visual_XAxis_Start": "Start", 52 | "Visual_XAxis_End": "End", 53 | "Visual_Position": "Position", 54 | "Visual_Position_Left": "Left", 55 | "Visual_Position_Right": "Right", 56 | "Visual_DataLabels": "Data Labels", 57 | "Visual_TextSize": "Text Size", 58 | "Visual_Values": "Values", 59 | "Visual_Frequency": "Frequency", 60 | "Visual_Fill": "Fill" 61 | }; 62 | 63 | constructor(displayNames) { 64 | this.displayNames = displayNames || {}; 65 | } 66 | 67 | getDisplayName(key: string): string { 68 | let displayName: string = this.displayNames[key]; 69 | if (displayName) { 70 | return displayName; 71 | } 72 | 73 | const defaultDisplayNames: {[key: string]: string} = MockILocalizationManager.DefaultDispalyNames; 74 | displayName = defaultDisplayNames[key]; 75 | 76 | return displayName || key; 77 | } 78 | } -------------------------------------------------------------------------------- /src/mocks/mockISelectionId.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visualizations 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | 27 | // powerbi.data 28 | import powerbi from "powerbi-visuals-api"; 29 | import Selector = powerbi.data.Selector; 30 | import SelectorsByColumn = powerbi.data.SelectorsByColumn; 31 | 32 | // powerbi.visuals 33 | import ISelectionId = powerbi.visuals.ISelectionId; 34 | 35 | let measureId: number = 0; 36 | 37 | export class MockISelectionId implements ISelectionId { 38 | private key: string; 39 | private measures: number[]; 40 | constructor(key: string) { 41 | this.measures = [measureId++]; 42 | this.key = key; 43 | } 44 | 45 | public compareMeasures = (current, others) => { 46 | return current === others; 47 | } 48 | 49 | public equals(other: ISelectionId): boolean { 50 | return this === other; 51 | } 52 | 53 | public includes(other: ISelectionId, ignoreHighlight?: boolean): boolean { 54 | return this === other; 55 | } 56 | 57 | public getKey(): string { 58 | return this.key; 59 | } 60 | 61 | public getSelector(): Selector { 62 | return {}; 63 | } 64 | 65 | public getSelectorsByColumn(): SelectorsByColumn { 66 | return {}; 67 | } 68 | 69 | public hasIdentity(): boolean { 70 | return true; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/mocks/mockISelectionIdBuilder.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visualizations 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | import { createSelectionId } from "./mocks"; 27 | import powerbi from "powerbi-visuals-api"; 28 | // powerbi 29 | import DataViewCategoryColumn = powerbi.DataViewCategoryColumn; 30 | import DataViewValueColumn = powerbi.DataViewValueColumn; 31 | import DataViewValueColumnGroup = powerbi.DataViewValueColumnGroup; 32 | import DataViewValueColumns = powerbi.DataViewValueColumns; 33 | 34 | // powerbi.visuals 35 | import ISelectionIdBuilder = powerbi.visuals.ISelectionIdBuilder; 36 | import ISelectionId = powerbi.visuals.ISelectionId; 37 | import DataViewTable = powerbi.DataViewTable; 38 | import DataViewMatrixNode = powerbi.DataViewMatrixNode; 39 | import DataViewHierarchyLevel = powerbi.DataViewHierarchyLevel; 40 | 41 | export class MockISelectionIdBuilder implements ISelectionIdBuilder { 42 | public withCategory(categoryColumn: DataViewCategoryColumn, index: number): this { 43 | return this; 44 | } 45 | 46 | public withSeries( 47 | seriesColumn: DataViewValueColumns, 48 | valueColumn: DataViewValueColumn | DataViewValueColumnGroup): this { 49 | 50 | return this; 51 | } 52 | 53 | public withMeasure(measureId: string): this { 54 | return this; 55 | } 56 | 57 | public createSelectionId(): ISelectionId { 58 | return createSelectionId(); 59 | } 60 | 61 | public withMatrixNode(matrixNode: DataViewMatrixNode, levels: DataViewHierarchyLevel[]): this { 62 | return this; 63 | } 64 | 65 | public withTable(table: DataViewTable, rowIndex: number): this { 66 | return this; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/mocks/mockISelectionManager.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visualizations 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | 27 | // powerbi 28 | import powerbi from "powerbi-visuals-api"; 29 | import IPromise = powerbi.IPromise; 30 | 31 | // powerbi.visuals 32 | import ISelectionId = powerbi.visuals.ISelectionId; 33 | import IPoint = powerbi.extensibility.IPoint; 34 | 35 | // powerbi.extensibility 36 | import ISelectionManager = powerbi.extensibility.ISelectionManager; 37 | 38 | export class MockISelectionManager implements ISelectionManager { 39 | private selectionIds: ISelectionId[] = []; 40 | 41 | private callback: (ids: ISelectionId[]) => void; 42 | 43 | // eslint-disable-next-line @typescript-eslint/ban-types 44 | public toggleExpandCollapse(selectionId: ISelectionId): IPromise<{}> { 45 | return new Promise((resolve, reject) => { 46 | resolve(); 47 | }) as any; 48 | } 49 | 50 | // eslint-disable-next-line @typescript-eslint/ban-types 51 | public showContextMenu(selectionId: ISelectionId, position: IPoint): IPromise<{}> { 52 | return new Promise((resolve, reject) => { 53 | resolve(); 54 | }) as any; 55 | } 56 | 57 | public select(selectionId: ISelectionId | ISelectionId[], multiSelect?: boolean): IPromise { 58 | const selectionIds: ISelectionId[] = [].concat(selectionId); 59 | 60 | // if no multiselect reset current selection and save new passed selections; 61 | if (!multiSelect) { 62 | this.selectionIds = selectionIds; 63 | } else { 64 | // if multiselect then check all passed selections 65 | selectionIds.forEach( (id: ISelectionId) => { 66 | // if selectionManager has passed selection in list of current selections 67 | if (this.containsSelection(id)) { 68 | // need to exclude from selection (selection of selected element should deselect element) 69 | this.selectionIds = this.selectionIds.filter((selectedId: ISelectionId) => { 70 | return !selectedId.equals(id); 71 | }); 72 | } else { 73 | // otherwise include the new selection into current selections 74 | this.selectionIds.push(id); 75 | } 76 | }); 77 | } 78 | 79 | return new Promise((resolve, reject) => { 80 | resolve(this.selectionIds); 81 | }) as any; 82 | } 83 | 84 | public hasSelection(): boolean { 85 | return this.selectionIds.length > 0; 86 | } 87 | 88 | // eslint-disable-next-line @typescript-eslint/ban-types 89 | public clear(): IPromise<{}> { 90 | this.selectionIds = []; 91 | 92 | return new Promise((resolve, reject) => { 93 | resolve(); 94 | }) as any; 95 | } 96 | 97 | public getSelectionIds(): ISelectionId[] { 98 | return this.selectionIds; 99 | } 100 | 101 | // eslint-disable-next-line @typescript-eslint/no-empty-function 102 | public applySelectionFilter(): void { } 103 | 104 | public containsSelection(id: ISelectionId) { 105 | return this.selectionIds.some((selectionId: ISelectionId) => { 106 | return selectionId.equals(id); 107 | }); 108 | } 109 | 110 | public registerOnSelectCallback(callback: (ids: ISelectionId[]) => void): void { 111 | this.callback = callback; 112 | } 113 | 114 | public simutateSelection(selections: ISelectionId[]): void { 115 | this.callback(selections); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/mocks/mockIStorageService.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visualizations 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | 27 | // powerbi 28 | import powerbi from "powerbi-visuals-api"; 29 | import IPromise = powerbi.IPromise; 30 | import PrivilegeStatus = powerbi.PrivilegeStatus; 31 | import ILocalVisualStorageService = powerbi.extensibility.ILocalVisualStorageService 32 | 33 | function getLocalStorageStatus() { 34 | try { 35 | return PrivilegeStatus.Allowed; 36 | } 37 | catch (e) { 38 | return PrivilegeStatus.NotDeclared; 39 | } 40 | } 41 | 42 | export class MockIStorageService implements ILocalVisualStorageService { 43 | 44 | public status(): IPromise { 45 | const status: PrivilegeStatus = getLocalStorageStatus(); 46 | 47 | return new Promise((resolve, reject) => { 48 | resolve(status); 49 | }) as any; 50 | } 51 | 52 | public get(key: string): IPromise { 53 | const data: string = localStorage.getItem(key); 54 | 55 | return new Promise((resolve, reject) => { 56 | resolve(data); 57 | }) as any; 58 | } 59 | 60 | public set(key: string, data: string): IPromise { 61 | localStorage.setItem(key, data); 62 | 63 | return new Promise((resolve, reject) => { 64 | resolve(data.length); 65 | }) as any; 66 | } 67 | 68 | public remove(key: string): void { 69 | localStorage.removeItem(key); 70 | } 71 | } -------------------------------------------------------------------------------- /src/mocks/mockIStorageV2Service.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visualizations 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | 27 | // powerbi 28 | import powerbi from "powerbi-visuals-api"; 29 | import IPromise = powerbi.IPromise; 30 | import PrivilegeStatus = powerbi.PrivilegeStatus; 31 | import IVisualLocalStorageV2Service = powerbi.extensibility.IVisualLocalStorageV2Service 32 | import StorageV2ResultInfo = powerbi.extensibility.StorageV2ResultInfo 33 | 34 | function getLocalStorageStatus() { 35 | try { 36 | return PrivilegeStatus.Allowed; 37 | } 38 | catch (e) { 39 | return PrivilegeStatus.NotDeclared; 40 | } 41 | } 42 | 43 | export class MockIStorageV2Service implements IVisualLocalStorageV2Service { 44 | 45 | public status(): IPromise { 46 | const status: PrivilegeStatus = getLocalStorageStatus(); 47 | 48 | return new Promise((resolve, reject) => { 49 | resolve(status); 50 | }) as any; 51 | } 52 | 53 | public get(key: string): IPromise { 54 | const data: string | null = localStorage.getItem(key); 55 | 56 | return new Promise((resolve, reject) => { 57 | resolve(data); 58 | }) as any; 59 | } 60 | 61 | public set(key: string, data: string): IPromise { 62 | localStorage.setItem(key, data); 63 | 64 | return new Promise((resolve, reject) => { 65 | resolve({ success: true }); 66 | }) as any; 67 | } 68 | 69 | public remove(key: string): void { 70 | localStorage.removeItem(key); 71 | } 72 | } -------------------------------------------------------------------------------- /src/mocks/mockITelemetryService.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visualizations 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | 27 | // powerbi.extensibility 28 | import powerbi from "powerbi-visuals-api"; 29 | import ITelemetryService = powerbi.extensibility.ITelemetryService; 30 | 31 | export class MockITelemetryService implements ITelemetryService { 32 | instanceId: string; 33 | // eslint-disable-next-line @typescript-eslint/no-empty-function 34 | trace(veType: powerbi.VisualEventType, payload?: string) { 35 | } 36 | } -------------------------------------------------------------------------------- /src/mocks/mockITooltipService.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visualizations 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | import powerbi from "powerbi-visuals-api"; 27 | import ITooltipService = powerbi.extensibility.ITooltipService; 28 | import TooltipShowOptions = powerbi.extensibility.TooltipShowOptions; 29 | import TooltipMoveOptions = powerbi.extensibility.TooltipMoveOptions; 30 | import TooltipHideOptions = powerbi.extensibility.TooltipHideOptions; 31 | 32 | export class MockITooltipService implements ITooltipService { 33 | private isEnabled: boolean; 34 | 35 | constructor(isEnabled: boolean = true) { 36 | this.isEnabled = isEnabled; 37 | } 38 | 39 | public enabled(): boolean { 40 | return this.isEnabled; 41 | } 42 | 43 | // eslint-disable-next-line @typescript-eslint/no-empty-function 44 | public show(options: TooltipShowOptions): void { } 45 | // eslint-disable-next-line @typescript-eslint/no-empty-function 46 | public move(options: TooltipMoveOptions): void { } 47 | // eslint-disable-next-line @typescript-eslint/no-empty-function 48 | public hide(options: TooltipHideOptions): void { } 49 | } 50 | -------------------------------------------------------------------------------- /src/mocks/mockIVisualLicenseManager.ts: -------------------------------------------------------------------------------- 1 | // powerbi 2 | import powerbi from "powerbi-visuals-api"; 3 | import IPromise = powerbi.IPromise; 4 | import LicenseNotificationType = powerbi.LicenseNotificationType; 5 | import ServicePlanState = powerbi.ServicePlanState; 6 | 7 | //powerbi.extensibility 8 | import IVisualLicenseManager = powerbi.extensibility.IVisualLicenseManager; 9 | 10 | //powerbi.extensibility.visual 11 | import visual = powerbi.extensibility.visual; 12 | 13 | export class MockIVisualLicenseManager implements IVisualLicenseManager { 14 | public getAvailableServicePlans(): IPromise { 15 | return new Promise((resolve, reject) => { 16 | const result = { 17 | plans: [{ 18 | spIdentifier: "spIdentifier", 19 | state: ServicePlanState.Active 20 | }], 21 | isLicenseUnsupportedEnv: false, 22 | isLicenseInfoAvailable: true, 23 | } 24 | resolve(result); 25 | }) as any; 26 | } 27 | public notifyLicenseRequired(notificationType: LicenseNotificationType): IPromise { 28 | return new Promise((resolve, reject) => { 29 | resolve(true); 30 | }) as any; 31 | } 32 | public notifyFeatureBlocked(tooltip: string): IPromise { 33 | return new Promise((resolve, reject) => { 34 | resolve(false); 35 | }) as any; 36 | } 37 | public clearLicenseNotification(): IPromise { 38 | return new Promise((resolve, reject) => { 39 | resolve(true); 40 | }) as any; 41 | } 42 | } -------------------------------------------------------------------------------- /src/mocks/mockIWebAccessService.ts: -------------------------------------------------------------------------------- 1 | // powerbi 2 | import powerbi from "powerbi-visuals-api"; 3 | import IPromise = powerbi.IPromise; 4 | import PrivilegeStatus = powerbi.PrivilegeStatus; 5 | 6 | //powerbi.extensibility 7 | import IWebAccessService = powerbi.extensibility.IWebAccessService; 8 | 9 | export class MockIWebAccessService implements IWebAccessService { 10 | public webAccessStatus(url: string): IPromise { 11 | return new Promise((resolve, rejects) => { 12 | resolve(PrivilegeStatus.Allowed) 13 | }) as any; 14 | } 15 | } -------------------------------------------------------------------------------- /src/mocks/mockSubSelectionService.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visualizations 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | /* eslint-disable @typescript-eslint/no-empty-function */ 27 | 28 | import powerbi from "powerbi-visuals-api"; 29 | import IVisualSubSelectionService = powerbi.extensibility.IVisualSubSelectionService 30 | import CustomVisualSubSelection = powerbi.visuals.CustomVisualSubSelection 31 | import SubSelectionRegionOutline = powerbi.visuals.SubSelectionRegionOutline 32 | 33 | export class MockSubSelectionService implements IVisualSubSelectionService { 34 | 35 | public subSelect(subSelection: CustomVisualSubSelection): void {} 36 | 37 | public updateRegionOutlines(outlines: SubSelectionRegionOutline[]): void {} 38 | } -------------------------------------------------------------------------------- /src/mocks/mockVisualHost.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars*/ 2 | /* 3 | * Power BI Visualizations 4 | * 5 | * Copyright (c) Microsoft Corporation 6 | * All rights reserved. 7 | * MIT License 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the ""Software""), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in 17 | * all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | * THE SOFTWARE. 26 | */ 27 | 28 | import powerbi from "powerbi-visuals-api"; 29 | import ITooltipService = powerbi.extensibility.ITooltipService; 30 | 31 | import { createSelectionIdBuilder } from "./mocks"; 32 | import { MockILocale } from "./mockILocale"; 33 | // powerbi 34 | import VisualObjectInstancesToPersist = powerbi.VisualObjectInstancesToPersist; 35 | import DrillArgs = powerbi.DrillArgs; 36 | 37 | // powerbi.visuals 38 | import ISelectionIdBuilder = powerbi.visuals.ISelectionIdBuilder; 39 | import CustomVisualOpaqueIdentity = powerbi.visuals.CustomVisualOpaqueIdentity; 40 | 41 | // powerbi.extensibility 42 | import ISelectionManager = powerbi.extensibility.ISelectionManager; 43 | import IColorPalette = powerbi.extensibility.ISandboxExtendedColorPalette; 44 | import IVisualEventService = powerbi.extensibility.IVisualEventService; 45 | import IDownloadService = powerbi.extensibility.IDownloadService; 46 | import HostCapabilities = powerbi.extensibility.HostCapabilities; 47 | import IVisualLicenseManager = powerbi.extensibility.IVisualLicenseManager; 48 | import IWebAccessService = powerbi.extensibility.IWebAccessService; 49 | import ILocalVisualStorageService = powerbi.extensibility.ILocalVisualStorageService; 50 | import IVisualLocalStorageV2Service = powerbi.extensibility.IVisualLocalStorageV2Service; 51 | import IVisualSubSelectionService = powerbi.extensibility.IVisualSubSelectionService; 52 | import ICustomVisualsOpaqueUtils = powerbi.extensibility.ICustomVisualsOpaqueUtils; 53 | 54 | // powerbi.extensibility.visual 55 | import IVisualHost = powerbi.extensibility.visual.IVisualHost; 56 | import ModalDialogResult = powerbi.extensibility.visual.ModalDialogResult 57 | import DialogOpenOptions = powerbi.extensibility.visual.DialogOpenOptions 58 | import CustomVisualApplyCustomSortArgs = powerbi.extensibility.visual.CustomVisualApplyCustomSortArgs; 59 | import IAcquireAADTokenService = powerbi.extensibility.IAcquireAADTokenService; 60 | 61 | export interface IMockVisualHostOptions { 62 | colorPalette?: IColorPalette, 63 | selectionManager?: ISelectionManager, 64 | tooltipServiceInstance?: ITooltipService, 65 | localeInstance?: MockILocale, 66 | localizationManager?: powerbi.extensibility.ILocalizationManager, 67 | telemetryService?: powerbi.extensibility.ITelemetryService, 68 | authService?: powerbi.extensibility.IAuthenticationService, 69 | storageService?: ILocalVisualStorageService, 70 | eventService?: IVisualEventService, 71 | hostCapabilities?: HostCapabilities, 72 | downloadService?: IDownloadService, 73 | licenseManager?: IVisualLicenseManager, 74 | webAccessService?: IWebAccessService, 75 | acquireAADTokenService?: IAcquireAADTokenService, 76 | modalDialogResult?: powerbi.extensibility.visual.ModalDialogResult, 77 | storageV2Service?: IVisualLocalStorageV2Service, 78 | subSelectionService?: IVisualSubSelectionService 79 | } 80 | 81 | export class MockIVisualHost implements IVisualHost { 82 | private colorPaletteInstance: IColorPalette; 83 | private selectionManager: ISelectionManager; 84 | private tooltipServiceInstance: ITooltipService; 85 | private localeInstance: MockILocale; 86 | private localizationManager: powerbi.extensibility.ILocalizationManager; 87 | private telemetryService: powerbi.extensibility.ITelemetryService; 88 | private authService: powerbi.extensibility.IAuthenticationService; 89 | private localStorageService: ILocalVisualStorageService; 90 | private visualEventService: IVisualEventService; 91 | public hostCapabilities: HostCapabilities; 92 | public downloadService: IDownloadService; 93 | public licenseManager: IVisualLicenseManager; 94 | public webAccessService: IWebAccessService; 95 | public acquireAADTokenService: IAcquireAADTokenService; 96 | public modalDialogResult: ModalDialogResult; 97 | public storageV2Service: IVisualLocalStorageV2Service; 98 | public subSelectionService: IVisualSubSelectionService; 99 | public hostEnv: powerbi.common.CustomVisualHostEnv = 1; 100 | 101 | constructor({ 102 | colorPalette, 103 | selectionManager, 104 | subSelectionService, 105 | tooltipServiceInstance, 106 | localeInstance, 107 | localizationManager, 108 | telemetryService, 109 | authService, 110 | storageService, 111 | storageV2Service, 112 | eventService, 113 | hostCapabilities, 114 | downloadService, 115 | licenseManager, 116 | webAccessService, 117 | acquireAADTokenService, 118 | modalDialogResult, 119 | }: IMockVisualHostOptions) { 120 | 121 | this.colorPaletteInstance = colorPalette; 122 | this.selectionManager = selectionManager; 123 | this.subSelectionService = subSelectionService; 124 | this.tooltipServiceInstance = tooltipServiceInstance; 125 | this.localeInstance = localeInstance; 126 | this.telemetryService = telemetryService; 127 | this.authService = authService; 128 | this.localizationManager = localizationManager; 129 | this.localStorageService = storageService; 130 | this.visualEventService = eventService; 131 | this.storageV2Service = storageV2Service; 132 | this.hostCapabilities = hostCapabilities; 133 | this.downloadService = downloadService; 134 | this.licenseManager = licenseManager; 135 | this.webAccessService = webAccessService; 136 | this.acquireAADTokenService = acquireAADTokenService; 137 | this.modalDialogResult = modalDialogResult; 138 | } 139 | 140 | public createSelectionIdBuilder(): ISelectionIdBuilder { 141 | return createSelectionIdBuilder(); 142 | } 143 | 144 | public createSelectionManager(): ISelectionManager { 145 | return this.selectionManager; 146 | } 147 | 148 | public get colorPalette(): IColorPalette { 149 | return this.colorPaletteInstance; 150 | } 151 | 152 | public get locale(): string { 153 | return this.localeInstance.locale; 154 | } 155 | 156 | public set locale(language) { 157 | this.localeInstance.locale = language; 158 | } 159 | 160 | public applyJsonFilter(filter: powerbi.IFilter, objectName: string, propertyName: string, action: powerbi.FilterAction) {} 161 | 162 | public get telemetry() { 163 | return this.telemetryService; 164 | } 165 | 166 | public get authenticationService() { 167 | return this.authenticationService; 168 | } 169 | 170 | public persistProperties(changes: VisualObjectInstancesToPersist) { } 171 | 172 | public get tooltipService(): ITooltipService { 173 | return this.tooltipServiceInstance; 174 | } 175 | 176 | public launchUrl(url: string) { 177 | window.open(url); 178 | } 179 | 180 | public get storageService(): ILocalVisualStorageService { 181 | return this.localStorageService; 182 | } 183 | 184 | public get eventService(): IVisualEventService { 185 | return this.visualEventService; 186 | } 187 | 188 | public get instanceId() { 189 | return "instanceId"; 190 | } 191 | 192 | public fetchMoreData() { 193 | return true; 194 | } 195 | 196 | public refreshHostData() {} 197 | 198 | public createLocalizationManager(): powerbi.extensibility.ILocalizationManager { 199 | return { 200 | getDisplayName: (key: string) => "" 201 | }; 202 | } 203 | 204 | public drill(args: DrillArgs): void {} 205 | 206 | public setCanDrill(drillAllowed: boolean): void {} 207 | 208 | public applyCustomSort(args: CustomVisualApplyCustomSortArgs): void {} 209 | 210 | public switchFocusModeState(on: boolean): void {} 211 | 212 | public displayWarningIcon(hoverText: string, detailedText: string): void {} 213 | 214 | public openModalDialog(dialogId: string, options?: DialogOpenOptions, initialState?: object): powerbi.IPromise { 215 | return new Promise((resolve, rejects) => { 216 | resolve(this.modalDialogResult) 217 | }) as any; 218 | } 219 | 220 | public createOpaqueUtils(): ICustomVisualsOpaqueUtils { 221 | return { 222 | compareCustomVisualOpaqueIdentities: (identity1: CustomVisualOpaqueIdentity, identity2: CustomVisualOpaqueIdentity) => true 223 | }; 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /src/mocks/mocks.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visualizations 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | import powerbi from "powerbi-visuals-api"; 27 | // powerbi 28 | import IColorInfo = powerbi.IColorInfo; 29 | 30 | // powerbi.visuals 31 | import ISelectionIdBuilder = powerbi.visuals.ISelectionIdBuilder; 32 | import ISelectionId = powerbi.visuals.ISelectionId; 33 | 34 | // powerbi.extensibility 35 | import IDownloadService = powerbi.extensibility.IDownloadService; 36 | import IColorPalette = powerbi.extensibility.ISandboxExtendedColorPalette; 37 | import ISelectionManager = powerbi.extensibility.ISelectionManager; 38 | import ITooltipService = powerbi.extensibility.ITooltipService; 39 | import IVisualHost = powerbi.extensibility.visual.IVisualHost; 40 | import IVisualEventService = powerbi.extensibility.IVisualEventService; 41 | import ILocalVisualStorageService = powerbi.extensibility.ILocalVisualStorageService 42 | import IVisualLicenseManager = powerbi.extensibility.IVisualLicenseManager; 43 | import IVisualLocalStorageV2Service = powerbi.extensibility.IVisualLocalStorageV2Service 44 | import IWebAccessService = powerbi.extensibility.IWebAccessService; 45 | import IAcquireAADTokenService = powerbi.extensibility.IAcquireAADTokenService; 46 | import ModalDialogResult = powerbi.extensibility.visual.ModalDialogResult 47 | import HostCapabilities = powerbi.extensibility.HostCapabilities; 48 | import IVisualSubSelectionService = powerbi.extensibility.IVisualSubSelectionService 49 | 50 | import { MockILocale } from "./mockILocale"; 51 | import { MockITooltipService } from "./mockITooltipService"; 52 | import { MockISelectionManager } from "./mockISelectionManager"; 53 | import { MockISelectionIdBuilder } from "./mockISelectionIdBuilder"; 54 | import { MockIAuthenticationService } from "./mockIAuthenticationService"; 55 | import { MockITelemetryService } from "./mockITelemetryService"; 56 | import { MockILocalizationManager } from "./mockILocalizationManager"; 57 | import { MockISelectionId } from "./mockISelectionId"; 58 | import { MockIColorPalette } from "./mockIColorPalette"; 59 | import { MockIVisualHost } from "./mockVisualHost"; 60 | import { MockIEventService } from "./mockIEventService"; 61 | import { MockIStorageService } from "./mockIStorageService"; 62 | import { MockIStorageV2Service } from "./mockIStorageV2Service"; 63 | import { MockHostCapabilities } from "./mockHostCapabilities"; 64 | import { MockDownloadService } from "./mockDownloadService"; 65 | import { MockIVisualLicenseManager } from "./mockIVisualLicenseManager"; 66 | import { MockIWebAccessService } from "./mockIWebAccessService"; 67 | import { MockIAcquireAADTokenService } from "./mockIAcquireAADTokenService"; 68 | import { MockSubSelectionService } from "./mockSubSelectionService"; 69 | 70 | export interface CreateVisualHostOptions { 71 | // eslint-disable-next-line @typescript-eslint/ban-types 72 | locale?: Object, 73 | allowInteractions?: boolean, 74 | colors?: IColorInfo[], 75 | isEnabled?: boolean, 76 | displayNames?: any, 77 | token?: string, 78 | modalDialogResult?: ModalDialogResult 79 | } 80 | 81 | export function createVisualHost({locale, allowInteractions, colors, isEnabled, displayNames, token, modalDialogResult}: CreateVisualHostOptions): IVisualHost { 82 | return new MockIVisualHost({ 83 | colorPalette: createColorPalette(colors), 84 | selectionManager: createSelectionManager(), 85 | subSelectionService: createSubSelectionService(), 86 | tooltipServiceInstance: createTooltipService(isEnabled), 87 | localeInstance: createLocale(locale), 88 | localizationManager: createLocalizationManager(displayNames), 89 | telemetryService: createTelemetryService(), 90 | authService: createAuthenticationService(token), 91 | storageService: createStorageService(), 92 | storageV2Service: createStorageV2Service(), 93 | eventService: createEventService(), 94 | hostCapabilities: createHostCapabilities(allowInteractions), 95 | downloadService: createDownloadService(), 96 | licenseManager: licenseManager(), 97 | webAccessService: webAccessService(), 98 | acquireAADTokenService: acquireAADTokenService(), 99 | modalDialogResult 100 | }) 101 | } 102 | 103 | export function createColorPalette(colors?: IColorInfo[]): IColorPalette { 104 | return new MockIColorPalette(colors); 105 | } 106 | 107 | export function createSelectionId(key: string = ""): ISelectionId { 108 | return new MockISelectionId(key); 109 | } 110 | 111 | export function createSelectionIdBuilder(): ISelectionIdBuilder { 112 | return new MockISelectionIdBuilder(); 113 | } 114 | 115 | export function createSelectionManager(): ISelectionManager { 116 | return new MockISelectionManager(); 117 | } 118 | 119 | export function createTooltipService(isEnabled?: boolean): ITooltipService { 120 | return new MockITooltipService(isEnabled); 121 | } 122 | 123 | // eslint-disable-next-line @typescript-eslint/ban-types 124 | export function createLocale(locales?: Object): MockILocale { 125 | return new MockILocale(locales); 126 | } 127 | 128 | export function createLocalizationManager(displayNames?: any): powerbi.extensibility.ILocalizationManager { 129 | return new MockILocalizationManager(displayNames); 130 | } 131 | 132 | export function createTelemetryService(): powerbi.extensibility.ITelemetryService { 133 | return new MockITelemetryService(); 134 | } 135 | 136 | export function createAuthenticationService(token?: string): powerbi.extensibility.IAuthenticationService { 137 | return new MockIAuthenticationService(token); 138 | } 139 | 140 | export function createStorageService(): ILocalVisualStorageService { 141 | return new MockIStorageService(); 142 | } 143 | 144 | export function createStorageV2Service(): IVisualLocalStorageV2Service { 145 | return new MockIStorageV2Service(); 146 | } 147 | 148 | export function createEventService(): IVisualEventService { 149 | return new MockIEventService(); 150 | } 151 | 152 | export function createHostCapabilities(allowInteractions): HostCapabilities { 153 | return new MockHostCapabilities(allowInteractions); 154 | } 155 | 156 | export function createDownloadService(): IDownloadService { 157 | return new MockDownloadService(); 158 | } 159 | 160 | export function licenseManager(): IVisualLicenseManager { 161 | return new MockIVisualLicenseManager(); 162 | } 163 | 164 | export function webAccessService(): IWebAccessService { 165 | return new MockIWebAccessService(); 166 | } 167 | 168 | export function acquireAADTokenService(): IAcquireAADTokenService { 169 | return new MockIAcquireAADTokenService(); 170 | } 171 | 172 | export function createSubSelectionService(): IVisualSubSelectionService { 173 | return new MockSubSelectionService(); 174 | } 175 | -------------------------------------------------------------------------------- /test/helpers/colorTest.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visualizations 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | 27 | import { parseColorString } from "../../src/helpers/color"; 28 | 29 | describe("parseColorString", () => { 30 | it("invalid hex", () => { 31 | let invalidStrings: string[] = [ 32 | "#", 33 | "#12", 34 | "#1234", 35 | "#12345", 36 | "#12345", 37 | "#1234567", 38 | "#xxx", 39 | "#xxxxxx", 40 | ]; 41 | 42 | invalidStrings.forEach((invalidString: string) => { 43 | expect(parseColorString(invalidString)).toBeUndefined(); 44 | }); 45 | }); 46 | 47 | it("valid hex", () => { 48 | expect(parseColorString("#09f")).toEqual({ 49 | R: 0, 50 | G: parseInt("99", 16), 51 | B: parseInt("ff", 16), 52 | }); 53 | 54 | expect(parseColorString("#09afAa")).toEqual({ 55 | R: parseInt("09", 16), 56 | G: parseInt("af", 16), 57 | B: parseInt("aa", 16), 58 | }); 59 | }); 60 | 61 | it("invalid rgb()", () => { 62 | let invalidStrings: string[] = [ 63 | "rgb()", 64 | "rgb(1)", 65 | "rgb(1, 2)", 66 | "rgb(1, 2, 3, 4)", 67 | "rgb(1.0, 2, 3)", 68 | "rgb(aa, 2, 3)", 69 | ]; 70 | 71 | invalidStrings.forEach((invalidString: string) => { 72 | expect(parseColorString(invalidString)).toBeUndefined(); 73 | }); 74 | }); 75 | 76 | it("valid rgb()", () => { 77 | expect(parseColorString("rgb(1, 2, 3)")).toEqual({ 78 | R: 1, 79 | G: 2, 80 | B: 3, 81 | }); 82 | }); 83 | 84 | it("invalid rgba()", () => { 85 | let invalidStrings: string[] = [ 86 | "rgba()", 87 | "rgba(1)", 88 | "rgba(1, 2)", 89 | "rgba(1, 2, 3)", 90 | "rgba(1.0, 2, 3)", 91 | "rgba(aa, 2, 3)", 92 | ]; 93 | 94 | invalidStrings.forEach((invalidString: string) => { 95 | expect(parseColorString(invalidString)).toBeUndefined(); 96 | }); 97 | }); 98 | 99 | it("valid rgba()", () => { 100 | expect(parseColorString("rgba(1, 2, 3, 1.0)")).toEqual({ 101 | R: 1, 102 | G: 2, 103 | B: 3, 104 | A: 1.0, 105 | }); 106 | 107 | expect(parseColorString("rgba(1, 2, 3, 0.19)")).toEqual({ 108 | R: 1, 109 | G: 2, 110 | B: 3, 111 | A: 0.19, 112 | }); 113 | 114 | expect(parseColorString("rgba(1, 2, 3, .19)")).toEqual({ 115 | R: 1, 116 | G: 2, 117 | B: 3, 118 | A: 0.19, 119 | }); 120 | 121 | expect(parseColorString("rgba(1, 2, 3, 1)")).toEqual({ 122 | R: 1, 123 | G: 2, 124 | B: 3, 125 | A: 1.0, 126 | }); 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /test/helpers/helpersTest.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visualizations 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | 27 | import { testDom, getRandomNumber, getRandomNumbers, getUuid } from "../../src/helpers/helpers"; 28 | 29 | describe("testDom", () => { 30 | it("should return an element", () => { 31 | let element: HTMLElement = testDom(500, 500); 32 | 33 | expect(element).toBeDefined(); 34 | }); 35 | }); 36 | 37 | describe("uuid", () => { 38 | it("should be unique across multiple tests", () => { 39 | const uuids: Uint16Array[] = []; 40 | const testsAmount = 1000; 41 | 42 | for (let i = 0; i < testsAmount; i++) { 43 | uuids.push(getUuid()); 44 | } 45 | 46 | const uniqueUUids = [...new Set(uuids)]; 47 | expect(uuids).toEqual(uniqueUUids); 48 | }); 49 | }); 50 | 51 | describe("getRandomNumber", () => { 52 | it("should return a number between min and max", () => { 53 | const min: number = 150, 54 | max: number = 300; 55 | 56 | let result: number = getRandomNumber(min, max); 57 | 58 | expect(result).toBeGreaterThan(min); 59 | expect(result).toBeLessThan(max); 60 | }); 61 | }); 62 | 63 | describe("getRandomNumbers", () => { 64 | it("should return an array with given length", () => { 65 | const min: number = 150, 66 | max: number = 300, 67 | length: number = 15; 68 | 69 | let result: number[] = getRandomNumbers(length, min, max); 70 | 71 | expect(result.length).toBe(length); 72 | }); 73 | 74 | it("should return an array of number that are between min and max", () => { 75 | const min: number = 150, 76 | max: number = 300, 77 | length: number = 15; 78 | 79 | let result: number[] = getRandomNumbers(length, min, max); 80 | 81 | result.forEach((value: number) => { 82 | expect(value).toBeGreaterThanOrEqual(min); 83 | expect(value).toBeLessThanOrEqual(max); 84 | }); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /test/mocks/mockIColorPaletteTest.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visualizations 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | // powerbi 27 | import powerbi from "powerbi-visuals-api"; 28 | import IColorInfo = powerbi.IColorInfo; 29 | 30 | // // powerbi.extensibility 31 | import IColorPalette = powerbi.extensibility.IColorPalette; 32 | 33 | // powerbi.extensibility.utils.test.mocks 34 | import { MockIColorPalette } from "../../src/mocks/mockIColorPalette"; 35 | import { createColorPalette } from "../../src/mocks/mocks"; 36 | 37 | describe("MockIColorPalette", () => { 38 | let colorPalette: IColorPalette; 39 | 40 | beforeEach(() => { 41 | colorPalette = createColorPalette(); 42 | }); 43 | 44 | describe("getColor", () => { 45 | it("the method should be defined", () => { 46 | expect(colorPalette.getColor).toBeDefined(); 47 | }); 48 | 49 | it("should return #01B8AA as a first color", () => { 50 | const color: IColorInfo = colorPalette.getColor("0"); 51 | 52 | expect(color.value).toBe("#01B8AA"); 53 | }); 54 | }); 55 | }); 56 | 57 | describe("createColorPalette", () => { 58 | it("should return an instance of MockIColorPalette", () => { 59 | const instance: IColorPalette = createColorPalette(); 60 | 61 | expect(instance instanceof MockIColorPalette).toBeTruthy(); 62 | }); 63 | }); -------------------------------------------------------------------------------- /test/mocks/mockISelectionIdBuilderTest.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visualizations 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | 27 | import powerbi from "powerbi-visuals-api"; 28 | import ISelectionIdBuilder = powerbi.visuals.ISelectionIdBuilder; 29 | import ISelectionId = powerbi.visuals.ISelectionId; 30 | 31 | // powerbi.extensibility.utils.test.mocks 32 | import { MockISelectionId } from "../../src/mocks/mockISelectionId"; 33 | import { createSelectionIdBuilder } from "../../src/mocks/mocks"; 34 | import { MockISelectionIdBuilder } from "../../src/mocks/mockISelectionIdBuilder"; 35 | 36 | describe("MockISelectionIdBuilder", () => { 37 | let selectionIdBuilder: ISelectionIdBuilder; 38 | 39 | beforeEach(() => { 40 | selectionIdBuilder = createSelectionIdBuilder(); 41 | }); 42 | 43 | describe("withCategory", () => { 44 | it("should return an instance of ISelectionIdBuilder", () => { 45 | const result: ISelectionIdBuilder = selectionIdBuilder.withCategory(null, 0); 46 | 47 | expect(result instanceof MockISelectionIdBuilder); 48 | }); 49 | }); 50 | 51 | describe("withSeries", () => { 52 | it("should return an instance of ISelectionIdBuilder", () => { 53 | const result: ISelectionIdBuilder = selectionIdBuilder.withSeries(null, null); 54 | 55 | expect(result instanceof MockISelectionIdBuilder); 56 | }); 57 | }); 58 | 59 | describe("withMeasure", () => { 60 | it("should return an instance of ISelectionIdBuilder", () => { 61 | const result: ISelectionIdBuilder = selectionIdBuilder.withMeasure(null); 62 | expect(result instanceof MockISelectionIdBuilder); 63 | }); 64 | }); 65 | 66 | describe("createSelectionId", () => { 67 | it("should return an instance of MockISelectionId", () => { 68 | const selectionId: ISelectionId = selectionIdBuilder.createSelectionId(); 69 | 70 | expect(selectionId instanceof MockISelectionId).toBeTruthy(); 71 | }); 72 | }); 73 | }); 74 | 75 | describe("createSelectionIdBuilder", () => { 76 | it("should return an instance of ISelectionIdBuilder", () => { 77 | const instance: ISelectionIdBuilder = createSelectionIdBuilder(); 78 | 79 | expect(instance instanceof MockISelectionIdBuilder).toBeTruthy(); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /test/mocks/mockISelectionIdTest.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visualizations 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | // powerbi.data 27 | 28 | import powerbi from "powerbi-visuals-api"; 29 | import Selector = powerbi.data.Selector; 30 | import SelectorsByColumn = powerbi.data.SelectorsByColumn; 31 | import ISelectionId = powerbi.visuals.ISelectionId; 32 | 33 | // powerbi.extensibility.utils.test.mocks 34 | import { MockISelectionId } from "../../src/mocks/mockISelectionId"; 35 | import { createSelectionId } from "../../src/mocks/mocks"; 36 | 37 | describe("MockISelectionId", () => { 38 | const key: string = "Custom_Key"; 39 | 40 | let selectionId: ISelectionId; 41 | 42 | beforeEach(() => { 43 | selectionId = createSelectionId(key); 44 | }); 45 | 46 | describe("equals", () => { 47 | it("should return true if the instances are the same", () => { 48 | expect(selectionId.equals(selectionId)).toBeTruthy(); 49 | }); 50 | }); 51 | 52 | describe("includes", () => { 53 | it("should return true if the instances are the same", () => { 54 | expect(selectionId.includes(selectionId)).toBeTruthy(); 55 | }); 56 | }); 57 | 58 | describe("getKey", () => { 59 | it("should return the given key", () => { 60 | expect(selectionId.getKey()).toBe(key); 61 | }); 62 | }); 63 | 64 | describe("getSelector", () => { 65 | it("should return a plain object", () => { 66 | const selector: Selector = selectionId.getSelector(); 67 | 68 | expect(Object.keys(selector).length).toBe(0); 69 | }); 70 | }); 71 | 72 | describe("getSelectorsByColumn", () => { 73 | it("should return a plain object", () => { 74 | const selector: SelectorsByColumn = selectionId.getSelectorsByColumn(); 75 | 76 | expect(Object.keys(selector).length).toBe(0); 77 | }); 78 | }); 79 | 80 | describe("hasIdentity", () => { 81 | it("should return true", () => { 82 | expect(selectionId.hasIdentity()).toBeTruthy(); 83 | }); 84 | }); 85 | }); 86 | 87 | describe("createSelectionId", () => { 88 | it("should return an instance of MockISelectionId", () => { 89 | const instance: ISelectionId = createSelectionId(); 90 | 91 | expect(instance instanceof MockISelectionId).toBeTruthy(); 92 | }); 93 | }); -------------------------------------------------------------------------------- /test/mocks/mockIStorageServiceTest.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visualizations 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | import powerbi from "powerbi-visuals-api"; 27 | import ILocalVisualStorageService = powerbi.extensibility.ILocalVisualStorageService 28 | import { createStorageService } from "../../src/mocks/mocks"; 29 | 30 | const keyToBeStored: string = "LS_KEY"; 31 | const objectToBeStored = { 32 | keyDigit: 1, 33 | keyString: "Hello", 34 | keyArray: [1, "s4", false], 35 | keyObject: { 36 | key0: 11, 37 | key1: "Hello", 38 | key2: { 39 | a: 13 40 | } 41 | } 42 | }; 43 | 44 | describe("MockIStorageService", () => { 45 | let mockStorageService: ILocalVisualStorageService; 46 | const objectToBeStoredStringifyed: string = JSON.stringify(objectToBeStored); 47 | 48 | beforeAll(() => { 49 | mockStorageService = createStorageService(); 50 | }); 51 | 52 | it("MockIStorageService.remove method test", () => { 53 | localStorage.setItem(keyToBeStored, objectToBeStoredStringifyed); 54 | let localStorageItem: string | null = localStorage.getItem(keyToBeStored); 55 | expect(localStorageItem).toBeTruthy(); 56 | 57 | mockStorageService.remove(keyToBeStored); 58 | localStorageItem = localStorage.getItem(keyToBeStored); 59 | expect(localStorageItem).toBeNull(); 60 | }); 61 | 62 | it("MockIStorageService.set method test", (done) => { 63 | mockStorageService.set(keyToBeStored, objectToBeStoredStringifyed).then((data: number) => { 64 | const localStorageItem: string | null = localStorage.getItem(keyToBeStored); 65 | expect(localStorageItem).toBeTruthy(); 66 | expect(localStorageItem).toEqual(objectToBeStoredStringifyed); 67 | expect(data).toEqual(objectToBeStoredStringifyed.length); 68 | done(); 69 | }); 70 | }); 71 | 72 | it("MockIStorageService.get method test", (done) => { 73 | const localStorageItem: string | null = localStorage.getItem(keyToBeStored); 74 | mockStorageService.get(keyToBeStored).then((data: string | null) => { 75 | expect(data).toEqual(localStorageItem); 76 | done(); 77 | }); 78 | }); 79 | }); -------------------------------------------------------------------------------- /test/mocks/mockIStorageV2ServiceTest.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visualizations 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | import powerbi from "powerbi-visuals-api"; 27 | import IVisualLocalStorageV2Service = powerbi.extensibility.IVisualLocalStorageV2Service 28 | import StorageV2ResultInfo = powerbi.extensibility.StorageV2ResultInfo 29 | import { createStorageV2Service } from "../../src/mocks/mocks"; 30 | 31 | const keyToBeStored: string = "LS_KEY"; 32 | const objectToBeStored = { 33 | keyDigit: 1, 34 | keyString: "Hello", 35 | keyArray: [1, "s4", false], 36 | keyObject: { 37 | key0: 11, 38 | key1: "Hello", 39 | key2: { 40 | a: 13 41 | } 42 | } 43 | }; 44 | 45 | describe("MockIStorageV2Service", () => { 46 | let mockStorageService: IVisualLocalStorageV2Service; 47 | const objectToBeStoredStringifyed: string = JSON.stringify(objectToBeStored); 48 | 49 | beforeAll(() => { 50 | mockStorageService = createStorageV2Service(); 51 | }); 52 | 53 | it("MockIStorageV2Service.remove method test", () => { 54 | localStorage.setItem(keyToBeStored, objectToBeStoredStringifyed); 55 | let localStorageItem: string | null = localStorage.getItem(keyToBeStored); 56 | expect(localStorageItem).toBeTruthy(); 57 | 58 | mockStorageService.remove(keyToBeStored); 59 | localStorageItem = localStorage.getItem(keyToBeStored); 60 | expect(localStorageItem).toBeNull(); 61 | }); 62 | 63 | it("MockIStorageV2Service.set method test", (done) => { 64 | mockStorageService.set(keyToBeStored, objectToBeStoredStringifyed).then((data: StorageV2ResultInfo) => { 65 | const localStorageItem: string | null = localStorage.getItem(keyToBeStored); 66 | expect(localStorageItem).toBeTruthy(); 67 | expect(localStorageItem).toEqual(objectToBeStoredStringifyed); 68 | expect(data.success).toBeTrue(); 69 | done(); 70 | }); 71 | }); 72 | 73 | it("MockIStorageV2Service.get method test", (done) => { 74 | const localStorageItem: string | null = localStorage.getItem(keyToBeStored); 75 | mockStorageService.get(keyToBeStored).then((data: string | null) => { 76 | expect(data).toEqual(localStorageItem); 77 | done(); 78 | }); 79 | }); 80 | }); -------------------------------------------------------------------------------- /test/mocks/mockITooltipServiceTest.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visualizations 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | 27 | import powerbi from "powerbi-visuals-api"; 28 | import ITooltipService = powerbi.extensibility.ITooltipService; 29 | 30 | import { createTooltipService, } from "../../src/mocks/mocks"; 31 | describe("MockITooltipService", () => { 32 | describe("enabled", () => { 33 | it("should return true", () => { 34 | const isEnabled: boolean = true, 35 | tooltipService: ITooltipService = createTooltipService(isEnabled); 36 | 37 | expect(tooltipService.enabled()).toBe(isEnabled); 38 | }); 39 | 40 | it("should return false", () => { 41 | const isEnabled: boolean = false, 42 | tooltipService: ITooltipService = createTooltipService(isEnabled); 43 | 44 | expect(tooltipService.enabled()).toBe(isEnabled); 45 | }); 46 | }); 47 | 48 | describe("show", () => { 49 | it("the method should be defined", () => { 50 | const tooltipService: ITooltipService = createTooltipService(); 51 | 52 | expect(tooltipService.show).toBeDefined(); 53 | }); 54 | }); 55 | 56 | describe("move", () => { 57 | it("the method should be defined", () => { 58 | const tooltipService: ITooltipService = createTooltipService(); 59 | 60 | expect(tooltipService.move).toBeDefined(); 61 | }); 62 | }); 63 | 64 | describe("hide", () => { 65 | it("the method should be defined", () => { 66 | const tooltipService: ITooltipService = createTooltipService(); 67 | 68 | expect(tooltipService.hide).toBeDefined(); 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/mocks/mockVisualHostTest.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Power BI Visualizations 3 | * 4 | * Copyright (c) Microsoft Corporation 5 | * All rights reserved. 6 | * MIT License 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the ""Software""), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | 27 | import powerbi from "powerbi-visuals-api"; 28 | import IVisualHost = powerbi.extensibility.visual.IVisualHost; 29 | import { createVisualHost } from "../../src/mocks/mocks"; 30 | 31 | describe("MockIVisualHost", () => { 32 | let visualHost: IVisualHost; 33 | 34 | beforeEach(() => { 35 | visualHost = createVisualHost({}); 36 | }); 37 | 38 | describe("createSelectionIdBuilder", () => { 39 | it("shouldn't return null", () => { 40 | expect(visualHost.createSelectionIdBuilder()).not.toBeNull(); 41 | }); 42 | }); 43 | 44 | describe("createSelectionManager", () => { 45 | it("shouldn't return null", () => { 46 | expect(visualHost.createSelectionManager()).not.toBeNull(); 47 | }); 48 | }); 49 | 50 | describe("colorPalette", () => { 51 | it("shouldn't return null", () => { 52 | expect(visualHost.colorPalette).not.toBeNull(); 53 | }); 54 | }); 55 | 56 | describe("locale", () => { 57 | it("shouldn't return null", () => { 58 | expect(visualHost.locale).not.toBeNull(); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": false, 4 | "emitDecoratorMetadata": true, 5 | "experimentalDecorators": true, 6 | "declaration": true, 7 | "sourceMap": true, 8 | "module": "ES2015", 9 | "moduleResolution": "node", 10 | "target": "ES2015", 11 | "lib": [ 12 | "es2015", 13 | "dom" 14 | ], 15 | "outDir": "./lib" 16 | }, 17 | "files": [ 18 | "src/index.ts" 19 | ] 20 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | var webpack = require("webpack"); 4 | module.exports = { 5 | entry: './src/index.ts', 6 | devtool: 'source-map', 7 | module: { 8 | rules: [ 9 | { 10 | test: /\.tsx?$/, 11 | use: 'ts-loader', 12 | exclude: /node_modules/ 13 | }, 14 | { 15 | test: /\.tsx?$/i, 16 | enforce: 'post', 17 | include: /(src)/, 18 | exclude: /(node_modules|resources\/js\/vendor)/, 19 | loader: 'coverage-istanbul-loader', 20 | options: { esModules: true } 21 | }, 22 | { 23 | test: /\.json$/, 24 | loader: 'json-loader' 25 | } 26 | ] 27 | }, 28 | externals: { 29 | "powerbi-visuals-api": '{}' 30 | }, 31 | resolve: { 32 | extensions: ['.tsx', '.ts', '.js', '.css'] 33 | }, 34 | output: { 35 | path: path.resolve(__dirname, ".tmp") 36 | }, 37 | plugins: [ 38 | new webpack.ProvidePlugin({ 39 | process: "process/browser" 40 | }) 41 | ] 42 | }; 43 | --------------------------------------------------------------------------------