Merged PR 83686: React Wrapper

React Wrapper library for PowerBI-JavaScript

###Limitations as discussed:
|Sr. no.| Limitation|
|:------|:------|
|1|`powerbi.preload()` is not supported|
|2|`powerbi.load()` is not supported as of now|
|3|Power BI Report Authoring support is removed as of now, until the import issue gets fixed|

## Licenses (Dependency)
|Package| License |
|:------|:------:|
|powerbi-client|MIT|
|powerbi-report-authoring|MIT|
## Licenses (Dev dependency)
|Package| License |
|:------|:------:|
|@types/react<br/> react|MIT|
|@types/react-dom<br/> react-dom|MIT|
|@types/jasmine<br/>jasmine-core|MIT|
|eslint|MIT|
|eslint-plugin-react|MIT|
|karma|MIT|
|karma-chrome-launcher|MIT|
|karma-jasmine|MIT|
|ts-loader|MIT|
|typescript|Apache-2.0|
|webpack|MIT|
|webpack-cli|MIT|
|@typescript-eslint/eslint-plugin|MIT|
|@typescript-eslint/parser|MIT|
## Licenses (Demo)
|Package| License |
|:------|:------:|
|webpack-dev-server|MIT|
|style-loader|MIT|
|css-loader|MIT|

Related work items: #299176, #354581, #355208, #355675, #356714, #357149, #357176, #357181, #357906, #362139, #366497, #367147, #368022
This commit is contained in:
Anant Singh 2020-06-02 10:00:31 +00:00
Родитель da95abd107
Коммит 57dd3e651a
32 изменённых файлов: 1815 добавлений и 16 удалений

34
.eslintrc.js Normal file
Просмотреть файл

@ -0,0 +1,34 @@
module.exports = {
parser: "@typescript-eslint/parser", // Specifies the ESLint parser
extends: [
"plugin:react/recommended", // Uses the recommended rules from @eslint-plugin-react
"plugin:@typescript-eslint/recommended" // Uses the recommended rules from @typescript-eslint/eslint-plugin
],
parserOptions: {
ecmaFeatures: {
jsx: true // Allows for the parsing of JSX
}
},
rules: {
'@typescript-eslint/no-this-alias': [
'error',
{
allowDestructuring: true, // Allow `const { props, state } = this`; false by default
allowedNames: ['thisObj'], // Allow `const self = this`; `[]` by default
},
],
'@typescript-eslint/no-empty-interface': [
'error',
{
allowSingleExtends: true
}
],
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-extra-semi": "off"
},
settings: {
react: {
version: "detect" // Tells eslint-plugin-react to automatically detect the version of React to use
}
}
};

27
.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,27 @@
# dependencies
**/node_modules
/.pnp
.pnp.js
**/package-lock.json
# testing
/coverage
/compiledTests
# production
/dist
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.vscode
*.tgz

20
.pipelines/build.ps1 Normal file
Просмотреть файл

@ -0,0 +1,20 @@
$exitCode = 0;
Write-Host "start: npm run build"
& npm run build
Write-Host "done: npm run build"
$exitCode += $LASTEXITCODE;
# Check linting
Write-Host "start: npm run lint"
& npm run lint
Write-Host "done: npm run lint"
$exitCode += $LASTEXITCODE;
Write-Host "start: Get dist folder files"
& dir "dist"
Write-Host "Done: Get dist folder files"
exit $exitCode

Просмотреть файл

@ -0,0 +1,6 @@
setlocal enabledelayedexpansion
pushd "%~dp0\.."
powershell.exe -ExecutionPolicy Unrestricted -NoProfile -WindowStyle Hidden -NonInteractive -File "%~dp0%~1"
endlocal
popd
exit /B %ERRORLEVEL%

13
.pipelines/package.ps1 Normal file
Просмотреть файл

@ -0,0 +1,13 @@
$exitCode = 0;
Write-Host "start: npm pack"
& npm pack
Write-Host "done: npm pack"
$exitCode += $LASTEXITCODE;
Write-Host "start: Get content of current folder"
& dir
Write-Host "done: Get content of current folder"
exit $exitCode

Просмотреть файл

@ -0,0 +1,83 @@
environment:
host:
os: 'windows'
flavor: 'server'
version: '2016'
runtime:
provider: 'appcontainer'
image: 'cdpxwinrs5.azurecr.io/global/vse2019/16.3.7:latest'
source_mode: 'map'
artifact_publish_options:
publish_to_legacy_artifacts: false
publish_to_pipeline_artifacts: true
publish_to_cloudvault_artifacts: false
package_sources:
npm:
feeds:
registry: https://powerbi.pkgs.visualstudio.com/_packaging/SDK.Externals/npm/registry/
version:
major: 1 # <---- Required field but not being used for 'custom' scheme
minor: 0 # <---- Required field but not being used for 'custom' scheme
system: 'custom' # <---- Set this to 'custom'. we will build the version using package.json in versioning commands.
versioning:
commands:
- !!defaultcommand
name: 'Set Version'
arguments: 'version.ps1'
command: '.pipelines\cdpx_run_ps.cmd'
restore:
commands:
- !!defaultcommand
name: 'NPM Install'
arguments: 'restore.ps1'
command: '.pipelines\cdpx_run_ps.cmd'
build:
commands:
- !!buildcommand
name: 'Build'
arguments: 'build.ps1'
command: '.pipelines\cdpx_run_ps.cmd'
artifacts:
- from: 'dist'
to: 'build_artifacts'
include:
- '**/*'
exclude:
- '**/node_modules/**/*.*'
- to: 'source'
include:
- '**/*'
exclude:
- '**/.pipelines/**/*.*'
- '**/.vscode/**/*.*'
- '**/test/**/*.*'
- '**/demo/**/*.*'
- '**/dist/**/*.*'
- '**/node_modules/**/*.*'
- !!buildcommand
name: 'Package'
arguments: 'package.ps1'
command: '.pipelines\cdpx_run_ps.cmd'
artifacts:
- include:
- "**/*.tgz"
test:
commands:
- !!testcommand
name: 'Test powerbi-client-react'
arguments: 'test.ps1'
command: '.pipelines\cdpx_run_ps.cmd'
testresults:
- title: 'powerbi-client-react test results'
type: 'jasmine'
from: 'reports'
# include:
# - "**coverage/**/index.html"

25
.pipelines/restore.ps1 Normal file
Просмотреть файл

@ -0,0 +1,25 @@
Write-Host "Start build ..."
Write-Host "Global node/npm paths ..."
& where.exe npm
& where.exe node
Write-Host "Global node version"
& node -v
Write-Host "Global npm version"
& npm -v
$exitCode = 0;
Write-Host "start: try install latest npm version"
& npm install npm@latest -g
Write-Host "done: try install latest npm version"
# Do not update $exitCode because we do not want to fail if install latest npm version fails.
Write-Host "start: npm install"
& npm install --no-audit --no-save
Write-Host "done: npm install"
$exitCode += $LASTEXITCODE;
exit $exitCode

9
.pipelines/test.ps1 Normal file
Просмотреть файл

@ -0,0 +1,9 @@
$exitCode = 0;
Write-Host "start: npm run test"
& npm run test
Write-Host "done: npm run test"
$exitCode += $LASTEXITCODE;
exit $exitCode;

15
.pipelines/version.ps1 Normal file
Просмотреть файл

@ -0,0 +1,15 @@
try {
# package.json is in root folder, while version.ps1 runs in .pipelines folder.
$version = (Get-Content "package.json") -join "`n" | ConvertFrom-Json | Select -ExpandProperty "version"
$revision = $env:CDP_DEFINITION_BUILD_COUNT_DAY
$buildNumber = "$version.$revision"
Write-Host "Build Number is" $buildNumber
[Environment]::SetEnvironmentVariable("CustomBuildNumber", $buildNumber, "User") # This will allow you to use it from env var in later steps of the same phase
Write-Host "##vso[build.updatebuildnumber]${buildNumber}" # This will update build number on your build
}
catch {
Write-Error $_.Exception
exit 1;
}

42
CONTRIBUTING.md Normal file
Просмотреть файл

@ -0,0 +1,42 @@
# Contributing
## Setup
Clone the repository:
```
git clone <url>
```
Navigate to the cloned directory
Install local dependencies:
```
npm install
```
## Build:
```
npm run build
```
Or if using VScode: `Ctrl + Shift + B`
## Test
```
npm test
```
By default the tests run using Chrome
The build and tests use webpack to compile all the source modules into one bundled module that can be executed in the browser.
## Running the demo
If you want to embed any powerbi artifact in demo, set the `embedUrl` and `accessToken` in the [config file](demo\config.ts) for that artifact type.
Serve the demo:
```
npm run demo
```
Open the address to view in the browser:
http://localhost:8080/

13
LICENCE Normal file
Просмотреть файл

@ -0,0 +1,13 @@
powerbi-client-react
Copyright (c) Microsoft Corporation
All rights reserved.
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

146
README.md
Просмотреть файл

@ -1,20 +1,134 @@
# Introduction
TODO: Give a short introduction of your project. Let this section explain the objectives or the motivation behind this project.
# powerbi-client-react
A React wrapper library for embedding PowerBI artifacts.
# Getting Started
TODO: Guide users through getting your code up and running on their own system. In this section you can talk about:
1. Installation process
2. Software dependencies
3. Latest releases
4. API references
## Table of contents
# Build and Test
TODO: Describe and show how to build your code and run the tests.
<!--ts-->
* [Sample Usage](#sample-usage)
* [Run Demo](#run-demo)
* [Docs](#docs)
* Props interface
* PowerBI Embed
* Get reference to embedded object
* How to set new accessToken
* Set event handlers
* Reset event handlers
* Apply style class
* Update settings (Report only)
* PowerBI Bootstrap
* [Flow diagram](#flow-diagram-for-the-wrapper-component)
* [Dependencies](#dependencies)
<!--te-->
# Contribute
TODO: Explain how other users and developers can contribute to make your code better.
## Sample Usage
If you want to learn more about creating good readme files then refer the following [guidelines](https://docs.microsoft.com/en-us/azure/devops/repos/git/create-a-readme?view=azure-devops). You can also seek inspiration from the below readme files:
- [ASP.NET Core](https://github.com/aspnet/Home)
- [Visual Studio Code](https://github.com/Microsoft/vscode)
- [Chakra Core](https://github.com/Microsoft/ChakraCore)
How to import:
```jsx
import { PowerBIEmbed } from 'powerbi-client-react';
```
How to bootstrap a PowerBI report:
```jsx
<PowerBIEmbed
embedConfig = {{
type: 'report', // Supported types: report, dashboard, tile, visual and qna
id: undefined,
embedUrl: undefined,
accessToken: undefined, // Keep as empty string, null or undefined
tokenType: models.TokenType.Embed
}}
/>
```
How to embed a PowerBI report:
```jsx
<PowerBIEmbed
embedConfig = {{
type: 'report', // Supported types: report, dashboard, tile, visual and qna
id: '<Report Id>',
embedUrl: '<Embed Url>',
accessToken: '<Access Token>',
tokenType: models.TokenType.Embed,
settings: {
panes: {
filters: {
expanded: false,
visible: false
}
},
background: models.BackgroundType.Transparent,
}
}}
eventHandlers = {
new Map([
['loaded', function () {console.log('Report loaded');}],
['rendered', function () {console.log('Report rendered');}],
['error', function (event) {console.log(event.detail);}]
])}
cssClassName = { "report-style-class" }
getEmbed = { (embeddedReport) => {
this.report = embeddedReport as Report;
}}
/>
```
## Run Demo
To run the demo on localhost, run the following commands:
```
npm install
npm run install:demo
npm run demo
```
Redirect to http://localhost:8080/ to view in the browser.
## Docs
|Topic|Details|
|:------|:------|
|PowerBI Embed|To embed your powerbi artifact, pass the component with atleast _type_, _embedUrl_ and _accessToken_ in _embedConfig_ prop.|
|Get reference to embedded object|Pass a callback method which accepts the embedded object as parameter to the _getEmbed_ of props.<br/>Refer to the _getEmbed_ prop in [Sample Usage](#sample-usage).|
|Apply style class|Pass the name(s) of classes to be set as "classname" for the embed container div via _className_ of props.|
|Set event handlers|Pass a map object of event name (string) and event handler (function) to the _eventHandlers_ of props. <br/>Key: Event name <br/>Value: Method to be triggered|
|Reset event handlers|To reset event handler for an event, set the event handler's value as `null` in the _eventHandlers_ map of props.|
|How to set new accessToken|To set new accessToken in the same embedded powerbi artifact, pass the updated _accessToken_ in _embedConfig_ of props.<br/>Example scenario: _Current token has expired_.|
|Update settings (Report type only)|To update the report settings, update the _embedConfig.settings_ property of props.<br/>Refer to the _embedConfig.settings_ prop in [Sample Usage](#sample-usage).|
|PowerBI Bootstrap|To [bootstrap your powerbi entity](https://github.com/microsoft/PowerBI-JavaScript/wiki/Bootstrap-For-Better-Performance), call the component without _accessToken_ in _embedConfig_ of props.<br/>__Note__: _embedConfig_ of props should atleast contain __type__ of the powerbi artifact being embedded. <br/>Eg: "report", "dashboard", "tile", "visual" or "qna".<br/>Refer How to bootstrap a report section in [Sample Usage](#sample-usage).|
### Props interface:
```ts
interface EmbedProps {
// Configuration for embedding the PowerBI entity
embedConfig: IEmbedConfiguration | IQnaEmbedConfiguration
// Callback method to get the embedded PowerBI entity object (Optional)
getEmbed?: { (embeddedComponent: Embed): void }
// Map of pair of event name and its handler method to be triggered on the event (Optional)
eventHandlers?: Map<string, service.IEventHandler<any> | null>
// CSS class to be set on the embedding container (Optional)
cssClassName?: string
// Provide a custom implementation of PowerBI service (Optional)
service?: service.Service
}
```
### Flow Diagram for the Wrapper Component:
![Flow Diagram](./resources/react_wrapper_flow_diagram.png)
## Dependencies
1. powerbi-client
## Peer-Dependencies
1. react

21
config/src/tsconfig.json Normal file
Просмотреть файл

@ -0,0 +1,21 @@
{
"include": [
"../../src/**/*.tsx",
"../../src/**/*.ts"
],
"compilerOptions": {
"lib": ["ES2016"],
"target": "es5",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"noErrorTruncation": true,
"module": "ES6",
"moduleResolution": "node",
"jsx": "react",
"sourceMap": true,
"noImplicitAny": true,
"declaration": true,
"outDir": "../../dist",
}
}

Просмотреть файл

@ -0,0 +1,32 @@
let path = require('path');
module.exports = {
entry: path.resolve('src/PowerBIEmbed.tsx'),
output: {
library: 'powerbi-client-react',
libraryTarget: 'amd',
path: path.resolve('dist'),
filename: 'powerbi-client-react.js'
},
module: {
rules: [
{
test: /\.ts(x)?$/,
loader: 'ts-loader',
options: {
configFile: path.resolve('config/src/tsconfig.json')
},
exclude: /node_modules/
},
]
},
resolve: {
modules: ['node_modules'],
extensions: [
'.tsx',
'.ts',
'.js'
]
},
devtool: 'source-map',
};

53
config/test/karma.conf.js Normal file
Просмотреть файл

@ -0,0 +1,53 @@
let path = require('path');
module.exports = function(config) {
config.set({
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: '',
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['jasmine'],
// list of files / patterns to load in the browser
files: [
path.resolve('compiledTests/**/*spec.js')
],
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
},
// test results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['progress'],
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: false,
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['ChromeHeadless'],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: true,
// Concurrency level
// how many browser should be started simultaneous
concurrency: Infinity
})
}

20
config/test/tsconfig.json Normal file
Просмотреть файл

@ -0,0 +1,20 @@
{
"include": [
"../../test/**/*.tsx",
"../../test/**/*.ts"
],
"compilerOptions": {
"target": "es6",
"lib": [
"ES2016",
"dom"
],
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": false,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"jsx": "react"
}
}

Просмотреть файл

@ -0,0 +1,33 @@
let path = require('path');
module.exports = {
mode: 'development',
entry: {
PowerBIEmbedTest: path.resolve('test/PowerBIEmbed.spec.tsx'),
utilsTest: path.resolve('test/utils.spec.ts'),
},
output: {
path: path.resolve('compiledTests'),
filename: '[name].spec.js'
},
devtool: 'source-map',
module: {
rules: [
{
test: /\.ts(x)?$/,
loader: 'ts-loader',
options: {
configFile: path.resolve('config/test/tsconfig.json')
},
exclude: /node_modules/
},
]
},
resolve: {
extensions: [
'.tsx',
'.ts',
'.js'
]
},
};

24
demo/DemoApp.css Normal file
Просмотреть файл

@ -0,0 +1,24 @@
.report-style-class {
height: 560px;
width: 960px;
}
body {
padding: 24px;
font-family: 'Segoe UI';
}
h3 {
color: #175C97;
}
button {
background: #337AB7;
border-radius: 5px;
margin-right: 15px;
color: #FFFFFF;
height: 35px;
width: 150px;
font-size: medium;
border: 0;
}

105
demo/DemoApp.tsx Normal file
Просмотреть файл

@ -0,0 +1,105 @@
import React, { useState } from 'react';
import { models, Report } from 'powerbi-client';
import { PowerBIEmbed } from 'powerbi-client-react';
import './DemoApp.css';
// Root Component to demonstrate usage of wrapper component
function DemoApp () {
// PowerBI Report object (to be received via callback)
let report: Report;
// API end-point url to get embed config for a sample report
const sampleReportUrl = 'https://aka.ms/sampleReportEmbedConfig';
// Report config useState hook
// Values for properties like embedUrl, accessToken and settings will be set on click of buttons below
const [sampleReportConfig, setReportConfig] = useState({
type: 'report',
embedUrl: undefined,
tokenType: models.TokenType.Embed,
accessToken: undefined,
settings: undefined,
});
// Map of event handlers to be applied to the embedding report
const eventHandlersMap = new Map([
['loaded', function () {console.log('Report has loaded');}],
['rendered', function () {
console.log('Report has rendered');
// Update display message
setMessage('Report is Embedded!')
}],
['error', function (event) { console.error(event.detail); }]
]);
// Fetch sample report's config (eg. embedUrl and AccessToken) for embedding
const mockSignIn = async () => {
// Update display message
setMessage('Fetching accessToken')
const reportConfigResponse = await fetch(sampleReportUrl);
if (!reportConfigResponse.ok) {
console.error(`Failed to fetch config for report. Status: ${ reportConfigResponse.status } ${ reportConfigResponse.statusText }`);
return;
}
const reportConfig = await reportConfigResponse.json();
// Update display message
setMessage('AccessToken is set successfully. Loading the PowerBI Report')
// Update the state "sampleReportConfig" and re-render DemoApp component
setReportConfig({
...sampleReportConfig,
embedUrl: reportConfig.embedUrl,
accessToken: reportConfig.embedToken.token
});
}
const changeSettings = () => {
// Update the state "sampleReportConfig" and re-render DemoApp component
setReportConfig({
...sampleReportConfig,
settings: {
panes: {
filters: {
expanded: false,
visible: false
}
}
}
});
}
const [displayMessage, setMessage] = useState(`The report is bootstraped. Click 'Embed Report' button below to provide Access Token`);
return (
<div>
<h3>Sample Report:</h3>
<PowerBIEmbed
embedConfig = { sampleReportConfig }
eventHandlers = { eventHandlersMap }
cssClassName = { "report-style-class" }
getEmbeddedComponent = { (embedObject:Report) => {
report = embedObject;
console.log(`Embedded object of type "${ report.embedtype }" received`);
} }
/>
<h4>
{ displayMessage }
</h4>
<button onClick = { mockSignIn }>
Embed Report</button>
<button onClick = { changeSettings }>
Hide filter pane</button>
</div>
);
}
export default DemoApp;

7
demo/index.html Normal file
Просмотреть файл

@ -0,0 +1,7 @@
<html>
<body>
<div id="root"></div>
<script src="./bundle.js"></script>
</body>
</html>

8
demo/index.tsx Normal file
Просмотреть файл

@ -0,0 +1,8 @@
import React from 'react';
import ReactDOM from 'react-dom';
import DemoApp from './DemoApp';
ReactDOM.render(
<DemoApp/>,
document.getElementById('root')
);

28
demo/package.json Normal file
Просмотреть файл

@ -0,0 +1,28 @@
{
"name": "powerbi-client-react-demo",
"version": "0.1.0",
"description": "Demo for usage of powerbi-client-react",
"scripts": {
"demo": "webpack-dev-server --content-base ./ --config ./webpack.config.js"
},
"license": "MIT",
"dependencies": {
"powerbi-client-react": "file:.."
},
"peerDependencies": {
"react": ">= 16.8"
},
"devDependencies": {
"@types/react": "^16.9.35",
"@types/react-dom": "^16.9.8",
"css-loader": "^3.5.3",
"style-loader": "^1.2.1",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"ts-loader": "^7.0.5",
"typescript": "^3.9.3",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.11.0"
}
}

18
demo/tsconfig.json Normal file
Просмотреть файл

@ -0,0 +1,18 @@
{
"include": [
"./**/*.tsx",
"./**/*.ts"
],
"compilerOptions": {
"target": "es5",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": false,
"noErrorTruncation": true,
"forceConsistentCasingInFileNames": true,
"module": "ES6",
"moduleResolution": "node",
"resolveJsonModule": true,
"jsx": "react",
}
}

33
demo/webpack.config.js Normal file
Просмотреть файл

@ -0,0 +1,33 @@
let path = require('path');
module.exports = {
mode: 'development',
entry: path.resolve('index.tsx'),
output: {
path: __dirname,
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.ts(x)?$/,
loader: 'ts-loader'
},
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
]
},
resolve: {
extensions: [
'.tsx',
'.ts',
'.js',
]
},
devtool: 'source-map',
};

48
package.json Normal file
Просмотреть файл

@ -0,0 +1,48 @@
{
"name": "powerbi-client-react",
"version": "0.1.0",
"description": "React wrapper for powerbi-client library",
"main": "dist/powerbi-client-react.js",
"types": "dist/powerbi-client-react.d.ts",
"files": [
"dist"
],
"scripts": {
"prebuild": "npm run lint",
"build": "webpack --mode=production --config config/src/webpack.config.js",
"build:dev": "webpack --mode=development --config config/src/webpack.config.js",
"pretest": "webpack --config config/test/webpack.config.js",
"test": "karma start config/test/karma.conf.js",
"install:demo": "cd demo && npm install",
"demo": "cd demo && npm run demo",
"lint": "eslint src/**/*.{ts,tsx}"
},
"keywords": [],
"license": "MIT",
"dependencies": {
"powerbi-client": "^2.11.0"
},
"peerDependencies": {
"react": ">= 16"
},
"devDependencies": {
"@types/jasmine": "^3.5.10",
"@types/node": "^14.0.5",
"@types/react": "^16.9.35",
"@types/react-dom": "^16.9.8",
"@typescript-eslint/eslint-plugin": "^3.1.0",
"@typescript-eslint/parser": "^3.0.2",
"eslint": "^7.1.0",
"eslint-plugin-react": "^7.20.0",
"jasmine-core": "^3.5.0",
"karma": "^5.0.9",
"karma-chrome-launcher": "^3.1.0",
"karma-jasmine": "^3.1.1",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"ts-loader": "^7.0.5",
"typescript": "^3.9.3",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11"
}
}

Двоичные данные
resources/react_wrapper_flow_diagram.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 36 KiB

281
src/PowerBIEmbed.tsx Normal file
Просмотреть файл

@ -0,0 +1,281 @@
import React from "react";
import { service, factories, Report, Embed, Dashboard, Tile, Qna, IEmbedConfiguration, Visual } from 'powerbi-client';
import { IQnaEmbedConfiguration, IEmbedSettings, IVisualEmbedConfiguration } from "embed";
import { stringifyMap } from './utils';
/**
* Props interface for PowerBIEmbed component
*/
export interface EmbedProps {
// Configuration for embedding the PowerBI entity
embedConfig: IEmbedConfiguration | IQnaEmbedConfiguration | IVisualEmbedConfiguration;
// Callback method to get the embedded PowerBI entity object (Optional)
getEmbeddedComponent?: { (embeddedComponent: Embed): void };
// Map of pair of event name and its handler method to be triggered on the event (Optional)
eventHandlers?: Map<string, service.IEventHandler<any> | null>;
// CSS class to be set on the embedding container (Optional)
cssClassName?: string;
// Provide a custom implementation of PowerBI service (Optional)
service?: service.Service;
}
export enum EmbedType {
Report = 'report',
Dashboard = 'dashboard',
Tile = 'tile',
Qna = 'qna',
Visual = 'visual'
}
/**
* Base react component to embed Power BI entities like: reports, dashboards, tiles, visual and qna containers.
*/
export class PowerBIEmbed extends React.Component<EmbedProps> {
// Embedded entity
private embed?: Embed;
// Powerbi service
private powerbi: service.Service;
// Ref to the HTML div element
private containerRef = React.createRef<HTMLDivElement>();
// JSON stringify of prev event handler map
private prevEventHandlerMapString = '';
constructor(props: EmbedProps) {
super(props);
if (this.props.service) {
this.powerbi = this.props.service;
}
else {
this.powerbi = new service.Service(
factories.hpmFactory,
factories.wpmpFactory,
factories.routerFactory);
}
};
componentDidMount(): void {
// Check if HTML container is available
if (this.containerRef.current) {
// Decide to bootstrap or embed
if (this.props.embedConfig.accessToken && this.props.embedConfig.embedUrl) {
this.embed = this.powerbi.embed(this.containerRef.current, this.props.embedConfig);
}
else {
this.embed = this.powerbi.bootstrap(this.containerRef.current, this.props.embedConfig);
}
}
// Invoke callback method in Props
this.getEmbedCallback();
// Set event handlers if available
if (this.props.eventHandlers && this.embed) {
this.setEventHandlers(this.embed, this.props.eventHandlers);
}
};
componentDidUpdate(prevProps: EmbedProps): void {
this.embedOrUpdateAccessToken(prevProps);
// Set event handlers if available
if (this.props.eventHandlers && this.embed) {
this.setEventHandlers(this.embed, this.props.eventHandlers);
}
// Update settings in embedConfig of props
this.updateSettings();
};
componentWillUnmount(): void {
// Clean Up
if (this.containerRef.current) {
this.powerbi.reset(this.containerRef.current);
}
};
render(): JSX.Element {
return (
<div
ref={this.containerRef}
className={this.props.cssClassName}>
</div>
)
};
/**
* Choose to _embed_ the powerbi entity or _update the accessToken_ in the embedded entity
* or do nothing when the embedUrl and accessToken did not update in the new props
*
* @param prevProps EmbedProps
* @returns void
*/
private embedOrUpdateAccessToken(prevProps: EmbedProps): void {
// Check if Embed URL and Access Token are present in current props
if (!this.props.embedConfig.accessToken || !this.props.embedConfig.embedUrl) {
return;
}
// Embed in the following scenarios
// 1. AccessToken was not provided in prev props (E.g. Report was bootstrapped earlier)
// 2. Embed URL is updated (E.g. New report is to be embedded)
if (this.containerRef.current
&& (!prevProps.embedConfig.accessToken
|| this.props.embedConfig.embedUrl !== prevProps.embedConfig.embedUrl)) {
this.embed = this.powerbi.embed(this.containerRef.current, this.props.embedConfig);
}
// Set new access token,
// when access token is updated but embed Url is same
else if (this.props.embedConfig.accessToken !== prevProps.embedConfig.accessToken
&& this.props.embedConfig.embedUrl === prevProps.embedConfig.embedUrl
&& this.embed) {
this.embed.setAccessToken(this.props.embedConfig.accessToken)
.catch(error => {
console.error(`setAccessToken error: ${error}`);
});
}
// Invoke callback method in Props
this.getEmbedCallback();
}
/**
* Sets all event handlers from the props on the embedded entity
*
* @param embed Embedded object
* @param eventHandlers Array of eventhandlers to be set on embedded entity
* @returns void
*/
private setEventHandlers(
embed: Embed,
eventHandlerMap: Map<string, service.IEventHandler<any> | null>): void {
// Get string representation of eventHandlerMap
const eventHandlerMapString = stringifyMap(this.props.eventHandlers);
// Check if event handler map changed
if (this.prevEventHandlerMapString === eventHandlerMapString) {
return;
}
// Update prev string representation of event handler map
this.prevEventHandlerMapString = eventHandlerMapString;
// List of allowed events
let allowedEvents = Embed.allowedEvents;
const entityType = embed.embedtype;
// Append entity specific events
switch (entityType) {
case EmbedType.Report:
allowedEvents = [...allowedEvents, ...Report.allowedEvents]
break;
case EmbedType.Dashboard:
allowedEvents = [...allowedEvents, ...Dashboard.allowedEvents]
break;
case EmbedType.Tile:
allowedEvents = [...allowedEvents, ...Tile.allowedEvents]
break;
case EmbedType.Qna:
allowedEvents = [...allowedEvents, ...Qna.allowedEvents]
break;
case EmbedType.Visual:
allowedEvents = [...allowedEvents, ...Visual.allowedEvents]
break;
default:
console.error(`Invalid embed type ${entityType}`);
}
// Holds list of events which are not allowed
const invalidEvents: Array<string> = [];
// Apply all provided event handlers
eventHandlerMap.forEach(function (eventHandlerMethod, eventName) {
// Check if this event is allowed
if (allowedEvents.includes(eventName)) {
// Removes event handler for this event
embed.off(eventName);
if (eventHandlerMethod) {
// Set single event handler
embed.on(eventName, eventHandlerMethod);
}
}
else {
// Add this event name to the list of invalid events
invalidEvents.push(eventName);
}
});
// Handle invalid events
if (invalidEvents.length) {
console.error(`Following events are invalid: ${invalidEvents.join(',')}`);
}
};
/**
* Returns the embedded object via _getEmbed_ callback method provided in props
*
* @returns void
*/
private getEmbedCallback(): void {
if (this.props.getEmbeddedComponent && this.embed) {
this.props.getEmbeddedComponent(this.embed);
}
};
/**
* Update settings from props of the embedded artifact
*
* @returns void
*/
private updateSettings(): void {
if (!this.embed || !this.props.embedConfig.settings) {
return;
}
switch (this.props.embedConfig.type) {
case EmbedType.Report:
// Typecasted to IEmbedSettings as props.embedConfig.settings can be ISettings via IQnaEmbedConfiguration
const settings = this.props.embedConfig.settings as IEmbedSettings;
// Upcast to Report and call updateSettings
(this.embed as Report).updateSettings(settings)
.catch((error: any) => {
console.error(`Error in method updateSettings: ${error}`);
});
break;
case EmbedType.Dashboard:
case EmbedType.Tile:
case EmbedType.Qna:
case EmbedType.Visual:
// updateSettings not applicable for these embedding types
break;
default:
console.error(`Invalid embed type ${this.props.embedConfig.type}`);
}
};
}

Просмотреть файл

@ -0,0 +1,5 @@
export {
PowerBIEmbed,
EmbedProps,
EmbedType
} from './PowerBIEmbed'

40
src/utils.ts Normal file
Просмотреть файл

@ -0,0 +1,40 @@
import { EmbedProps } from "./PowerBIEmbed";
/**
* Get JSON string representation of the given map.
*
* @param map Map of event and corresponding handler method
*
* For example:
* Input:
* ```
* Map([
['loaded', null],
['rendered', function () { console.log('Rendered'); }]
]);
* ```
* Output:
* ```
* `[["loaded",""],["rendered","function () { console.log('Rendered'); }"]]`
* ```
*/
export function stringifyMap(map: EmbedProps['eventHandlers']): string {
// Return empty string for empty/null map
if (!map) {
return '';
}
// Get entries of map as array
const mapEntries = Array.from(map);
// Return JSON string
return JSON.stringify(mapEntries.map((mapEntry) => {
// Convert event handler method to a string containing its source code for comparison
return [
mapEntry[0],
mapEntry[1] ? mapEntry[1].toString() : ''
];
}));
};

569
test/PowerBIEmbed.spec.tsx Normal file
Просмотреть файл

@ -0,0 +1,569 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { act, isElement } from 'react-dom/test-utils';
import { Report, Dashboard, service, factories} from 'powerbi-client';
import { PowerBIEmbed } from '../src/PowerBIEmbed';
import { mockPowerBIService, mockedMethods } from "./mockService";
describe('tests of PowerBIEmbed', function () {
let container: HTMLDivElement | null;
beforeEach(function () {
container = document.createElement('div');
document.body.appendChild(container);
// Reset all methods in PowerBI Service spy object
mockedMethods.forEach(mockedMethod => {
mockPowerBIService[mockedMethod].calls.reset();
});
});
afterEach(function () {
if (container){
document.body.removeChild(container);
container = null;
}
});
describe('basic tests', function () {
it('is a react component', () => {
const component = <PowerBIEmbed embedConfig={{ type: 'report' }} />
expect(isElement(component)).toBe(true);
});
it('renders exactly one div', () => {
act(() => {
ReactDOM.render(<PowerBIEmbed embedConfig = {{type: 'report'}}/>, container);
});
const divCount = container.querySelectorAll('div').length;
expect(divCount).toBe(1);
});
it('renders exactly one iframe', () => {
act(() => {
ReactDOM.render(<PowerBIEmbed embedConfig = {{type: 'report'}}/>, container);
});
const divCount = container.querySelectorAll('iframe').length;
expect(divCount).toBe(1);
});
it('sets the css classes', () => {
const inputCssClass = 'test-class another-test-class';
act(() => {
ReactDOM.render(
<PowerBIEmbed
embedConfig = {{type: 'report'}}
cssClassName = {inputCssClass}/>
, container);
});
const divClass = container.querySelectorAll('div')[0].className;
expect(divClass).toBe(inputCssClass);
});
it('gets the embedded report object', () => {
let testReport = undefined;
act(() => {
ReactDOM.render(
<PowerBIEmbed
embedConfig={{ type: 'report' }}
getEmbeddedComponent={(callbackReport) => {
testReport = callbackReport;
}}
/>, container);
});
expect(testReport).not.toBe(undefined);
expect(testReport instanceof Report).toBe(true);
});
it('gets the embedded dashboard object', () => {
let testReport = undefined;
act(() => {
ReactDOM.render(
<PowerBIEmbed
embedConfig={{ type: 'dashboard' }}
getEmbeddedComponent={(callbackReport) => {
testReport = callbackReport;
}}
/>, container);
});
expect(testReport).not.toBe(undefined);
expect(testReport instanceof Dashboard).toBe(true);
});
});
it('updates powerbi report settings', () => {
let testReport: Report = undefined;
act(() => {
ReactDOM.render(
<PowerBIEmbed
embedConfig={{ type: 'report' }}
getEmbeddedComponent={(callbackReport: Report) => {
testReport = callbackReport;
}}
/>, container);
});
spyOn(testReport, 'updateSettings').and.callThrough();
// Update settings via props
act(() => {
ReactDOM.render(
<PowerBIEmbed
embedConfig={{ type: 'report', settings: { filterPaneEnabled: false } }}
getEmbeddedComponent={(callbackReport: Report) => {
testReport = callbackReport;
}}
/>, container);
});
expect(testReport.updateSettings).toHaveBeenCalledTimes(1);
expect(testReport.updateSettings).toHaveBeenCalledWith({ filterPaneEnabled: false });
});
it('sets new token received in updated props (case: Token expired)', () => {
// Arrange
let testReport:Report = undefined;
let config = {
type: 'report',
id: 'fakeId',
embedUrl: 'fakeUrl',
accessToken: 'fakeToken'
};
// New accessToken
let newConfig = {
type: 'report',
id: 'fakeId',
embedUrl: 'fakeUrl',
accessToken: 'newfakeToken'
};
act(() => {
ReactDOM.render(
<PowerBIEmbed
embedConfig = { config }
getEmbeddedComponent = {(callbackReport:Report) => {
testReport = callbackReport;
}}
/>, container);
});
spyOn(testReport, 'setAccessToken').and.callThrough();
// Act
// Update accessToken via props
act(() => {
ReactDOM.render(
<PowerBIEmbed
embedConfig = { newConfig }
getEmbeddedComponent = {(callbackReport:Report) => {
testReport = callbackReport;
}}
/>, container);
});
// Assert
expect(testReport).toBeDefined();
expect(testReport.setAccessToken).toHaveBeenCalledTimes(1);
expect(testReport.setAccessToken).toHaveBeenCalledWith(newConfig.accessToken);
});
describe('test powerbi service interaction',() => {
it('embeds report when accessToken provided', () => {
let config = {
type: 'report',
id: 'fakeId',
embedUrl: 'fakeUrl',
accessToken: 'fakeToken'
};
act(() => {
ReactDOM.render(
<PowerBIEmbed
embedConfig = { config }
service = { mockPowerBIService }
/>, container);
});
expect(mockPowerBIService.bootstrap).toHaveBeenCalledTimes(0);
expect(mockPowerBIService.embed).toHaveBeenCalledTimes(1);
});
it('bootstraps report when no accessToken provided', () => {
let config = {
type: 'report',
id: 'fakeId',
embedUrl: 'fakeUrl'
};
act(() => {
ReactDOM.render(
<PowerBIEmbed
embedConfig = { config }
service = { mockPowerBIService }
/>, container);
});
expect(mockPowerBIService.embed).toHaveBeenCalledTimes(0);
expect(mockPowerBIService.bootstrap).toHaveBeenCalledTimes(1);
});
it('first bootstraps, then embeds when accessToken is available', () => {
// Arrange
const config = {
type: 'report',
id: 'fakeId',
embedUrl: 'fakeUrl',
accessToken: null
};
const newConfig = {
type: 'report',
id: 'fakeId',
embedUrl: 'fakeUrl',
accessToken: 'fakeToken'
};
// Act
// Without accessToken (bootstrap)
act(() => {
ReactDOM.render(
<PowerBIEmbed
embedConfig = { config }
service = { mockPowerBIService }
/>, container);
});
// Assert
expect(mockPowerBIService.embed).toHaveBeenCalledTimes(0);
expect(mockPowerBIService.bootstrap).toHaveBeenCalledTimes(1);
// Reset for next Act
mockPowerBIService.embed.calls.reset();
mockPowerBIService.bootstrap.calls.reset();
// With accessToken (embed)
act(() => {
ReactDOM.render(
<PowerBIEmbed
embedConfig = { newConfig }
service = { mockPowerBIService }
/>, container);
});
expect(mockPowerBIService.bootstrap).toHaveBeenCalledTimes(0);
expect(mockPowerBIService.embed).toHaveBeenCalledTimes(1);
});
it('does not embed again when accessToken and embedUrl are same', () => {
const config = {
type: 'report',
id: 'fakeId',
embedUrl: 'fakeUrl',
accessToken: 'fakeToken',
};
const newConfig = {
type: 'report',
id: 'fakeId',
embedUrl: 'fakeUrl',
accessToken: 'fakeToken',
settings: { filterPaneEnabled: false }
};
act(() => {
ReactDOM.render(
<PowerBIEmbed
embedConfig = { config }
service = { mockPowerBIService }
/>, container);
});
expect(mockPowerBIService.embed).toHaveBeenCalledTimes(1);
mockPowerBIService.embed.calls.reset();
// With accessToken (embed)
act(() => {
ReactDOM.render(
<PowerBIEmbed
embedConfig = { newConfig }
service = { mockPowerBIService }
/>, container);
});
expect(mockPowerBIService.embed).not.toHaveBeenCalled();
});
it('powerbi.reset called when component unmounts', () => {
let config = {
type: 'report',
id: 'fakeId',
embedUrl: 'fakeUrl',
accessToken: 'fakeToken'
};
act(() => {
ReactDOM.render(
<PowerBIEmbed
embedConfig = { config }
service = { mockPowerBIService }
/>, container);
});
act(() => {
ReactDOM.unmountComponentAtNode(container);
});
expect(mockPowerBIService.reset).toHaveBeenCalled();
});
it("new report's url in updated props", () => {
let config = {
type: 'report',
id: 'fakeId',
embedUrl: 'fakeUrl',
accessToken: 'fakeToken'
};
act(() => {
ReactDOM.render(
<PowerBIEmbed
embedConfig = { config }
service = { mockPowerBIService }
/>, container);
});
config.embedUrl = 'newFakeUrl';
act(() => {
ReactDOM.render(
<PowerBIEmbed
embedConfig = { config }
service = { mockPowerBIService }
/>, container);
});
expect(mockPowerBIService.embed).toHaveBeenCalled();
});
});
describe('tests for setting event handlers', () => {
it('clears and sets the event handlers', () => {
// Arrange
let testReport:Report = undefined;
let eventHandlers = new Map([
['loaded', function () { }],
['rendered', function () { }],
['error', function () { }]
]);
// Initialise testReport
act(() => {
ReactDOM.render(
<PowerBIEmbed
embedConfig = { {type:'report'} }
getEmbeddedComponent = {(callbackReport:Report) => {
testReport = callbackReport;
}}
/>, container);
});
spyOn(testReport, 'off');
spyOn(testReport, 'on');
// Act
act(() => {
ReactDOM.render(
<PowerBIEmbed
embedConfig = { {type:'report'} }
eventHandlers = { eventHandlers }
/>, container);
});
// Assert
expect(testReport.off).toHaveBeenCalledTimes(eventHandlers.size);
expect(testReport.on).toHaveBeenCalledTimes(eventHandlers.size);
});
it('clears the already set event handlers in case of null provided for handler', () => {
// Arrange
let testReport:Report = undefined;
const eventHandlers = new Map([
['loaded', function () { }],
['rendered', function () { }],
['error', function () { }]
]);
const newEventHandlers = new Map([
['loaded', null],
['rendered', null],
['error', function () { }]
]);
// Initialise testReport
act(() => {
ReactDOM.render(
<PowerBIEmbed
embedConfig = { {type:'report'} }
getEmbeddedComponent = {(callbackReport:Report) => {
testReport = callbackReport;
}}
/>, container);
});
spyOn(testReport, 'off');
spyOn(testReport, 'on');
// Act
act(() => {
ReactDOM.render(
<PowerBIEmbed
embedConfig = { {type:'report'} }
eventHandlers = { newEventHandlers }
/>, container);
});
// Assert
expect(testReport.off).toHaveBeenCalledTimes(eventHandlers.size);
// Two events are turned off in new eventhandlers
expect(testReport.on).toHaveBeenCalledTimes(eventHandlers.size - 2);
});
it('does not console error for valid events for report', () => {
const eventHandlers = new Map([
['loaded', function () { }],
['saved', function () { }],
['rendered', function () { }],
['saveAsTriggered', function () { }],
['dataSelected', function () { }],
['buttonClicked', function () { }],
['filtersApplied', function () { }],
['pageChanged', function () { }],
['commandTriggered', function () { }],
['swipeStart', function () { }],
['swipeEnd', function () { }],
['bookmarkApplied', function () { }],
['dataHyperlinkClicked', function () { }],
['error', function () { }]
]);
spyOn(console, 'error');
act(() => {
ReactDOM.render(
<PowerBIEmbed
embedConfig={{ type: 'report' }}
eventHandlers={eventHandlers}
/>, container);
});
expect(console.error).not.toHaveBeenCalled();
});
it('consoles error for invalid events', () => {
// Arrange
const invalidEvent1 = 'invalidEvent1';
const invalidEvent2 = 'invalidEvent2';
const errorMessage = `Following events are invalid: ${invalidEvent1},${invalidEvent2}`;
const eventHandlers = new Map([
[invalidEvent1, function () { }],
['rendered', function () { }],
['error', function () { }],
[invalidEvent2, function () { }],
]);
const powerbi = new service.Service(
factories.hpmFactory,
factories.wpmpFactory,
factories.routerFactory);
const embed = powerbi.bootstrap(container, {type:'tile'});
spyOn(console, 'error');
// Act
const powerbiembed = new PowerBIEmbed({
embedConfig: { type: 'tile' },
eventHandlers: eventHandlers
});
// Ignoring next line as setEventHandlers is a private method
// @ts-ignore
powerbiembed.setEventHandlers(embed, eventHandlers);
expect(console.error).toHaveBeenCalledWith(errorMessage);
});
it('does not set the same eventhandler map again', () => {
// Arrange
let testReport:Report = undefined;
const eventHandlers = new Map([
['loaded', function () { }],
['rendered', function () { }],
['error', function () { }]
]);
const newSameEventHandlers = new Map([
['loaded', function () { }],
['rendered', function () { }],
['error', function () { }]
]);
// Initialise testReport
act(() => {
ReactDOM.render(
<PowerBIEmbed
embedConfig = { {type:'report'} }
getEmbeddedComponent = {(callbackReport:Report) => {
testReport = callbackReport;
}}
eventHandlers = { eventHandlers }
/>, container);
});
spyOn(testReport, 'off');
spyOn(testReport, 'on');
// Act
act(() => {
ReactDOM.render(
<PowerBIEmbed
embedConfig = { {type:'report'} }
eventHandlers = { newSameEventHandlers }
/>, container);
});
// Assert
expect(testReport.off).not.toHaveBeenCalled();
expect(testReport.on).not.toHaveBeenCalled();
});
});
});

5
test/mockService.ts Normal file
Просмотреть файл

@ -0,0 +1,5 @@
const mockedMethods = ['init', 'embed', 'bootstrap', 'load', 'get', 'reset', 'preload'];
const mockPowerBIService = jasmine.createSpyObj('mockService', mockedMethods);
export { mockPowerBIService, mockedMethods };

68
test/utils.spec.ts Normal file
Просмотреть файл

@ -0,0 +1,68 @@
import { service } from 'powerbi-client';
import { stringifyMap } from '../src/utils';
describe('tests of PowerBIEmbed', function () {
let container: HTMLDivElement | null;
beforeEach(function () {
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(function () {
if (container){
document.body.removeChild(container);
container = null;
}
});
// Tests for utils stringifyMap
describe('tests PowerBIEmbed stringifyMap method', () => {
it('stringifies the event handler map', () => {
// Arrange
const eventHandlerMap = new Map([
['loaded', function () { console.log('Report loaded'); }],
['rendered', function () { console.log('Rendered'); }]
]);
const expectedString = `[["loaded","function () { console.log('Report loaded'); }"],["rendered","function () { console.log('Rendered'); }"]]`;
// Act
const jsonStringOutput = stringifyMap(eventHandlerMap);
// Assert
expect(jsonStringOutput).toBe(expectedString);
});
it('stringifies empty event handler map', () => {
// Arrange
const eventHandlerMap = new Map<string, service.IEventHandler<any>>([]);
const expectedString = `[]`;
// Act
const jsonStringOutput = stringifyMap(eventHandlerMap);
// Assert
expect(jsonStringOutput).toBe(expectedString);
});
it('stringifies null in event handler map', () => {
// Arrange
const eventHandlerMap = new Map([
['loaded', null],
['rendered', function () { console.log('Rendered'); }]
]);
const expectedString = `[["loaded",""],["rendered","function () { console.log('Rendered'); }"]]`;
// Act
const jsonStringOutput = stringifyMap(eventHandlerMap);
// Assert
expect(jsonStringOutput).toBe(expectedString);
});
});
});