Skip to content

Commit 4047505

Browse files
author
Matt Mazzola
committed
Add hpm and wpmp to SDK and implement methods to send requests. Make spyHpm and start unit testing sdk calls to hpm
1 parent a0c9d76 commit 4047505

File tree

9 files changed

+457
-29
lines changed

9 files changed

+457
-29
lines changed

e2e/protocol.e2e.spec.ts

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
import * as core from '../src/core';
2+
import { Report } from '../src/report';
13
import * as Wpmp from 'window-post-message-proxy';
24
import * as Hpm from 'http-post-message';
35
import * as Router from 'powerbi-router';
46
import { spyApp, setup } from './utility/mockReportEmbed';
7+
import * as factories from '../src/factories';
8+
import { spyHpm } from './utility/mockHpm';
59

610
declare global {
711
interface Window {
@@ -1787,3 +1791,154 @@ describe('Protocol', function () {
17871791
});
17881792
});
17891793
});
1794+
1795+
describe('SDK-to-MockApp (UNIT tests)', function () {
1796+
let $element: JQuery;
1797+
let iframe: HTMLIFrameElement;
1798+
let iframeHpm: Hpm.HttpPostMessage;
1799+
let powerbi: core.PowerBi;
1800+
let report: Report;
1801+
let hpmSpy: jasmine.Spy;
1802+
1803+
beforeAll(function () {
1804+
const spyHpmFactory: factories.IHpmFactory = () => {
1805+
return <Hpm.HttpPostMessage><any>spyHpm;
1806+
};
1807+
const noop: factories.IWpmpFactory = () => {
1808+
return <Wpmp.WindowPostMessageProxy>null;
1809+
};
1810+
1811+
powerbi = new core.PowerBi(spyHpmFactory, noop);
1812+
1813+
$element = $(`<div class="powerbi-report-container"></div>`)
1814+
.appendTo(document.body);
1815+
1816+
const iframeSrc = "base/e2e/utility/noop.html";
1817+
const embedConfiguration = {
1818+
type: "report",
1819+
reportId: "fakeReportId",
1820+
accessToken: 'fakeToken',
1821+
embedUrl: iframeSrc
1822+
};
1823+
report = <Report>powerbi.embed($element[0], embedConfiguration);
1824+
1825+
iframe = <HTMLIFrameElement>$element.find('iframe')[0];
1826+
1827+
// Register Iframe side
1828+
iframeHpm = setup(iframe.contentWindow, window, true);
1829+
1830+
// Reset load handler
1831+
spyHpm.post.calls.reset();
1832+
});
1833+
1834+
afterAll(function () {
1835+
// TODO: Should call remove using the powerbi service first to clean up intenral references to DOM inside this element
1836+
$element.remove();
1837+
});
1838+
1839+
describe('SDK-to-HPM', function () {
1840+
afterEach(function () {
1841+
spyHpm.get.calls.reset();
1842+
spyHpm.post.calls.reset();
1843+
spyHpm.patch.calls.reset();
1844+
spyHpm.put.calls.reset();
1845+
spyHpm.delete.calls.reset();
1846+
});
1847+
1848+
describe('load', function () {
1849+
it('report.load() sends POST /report/load with configuration in body', function () {
1850+
// Arrange
1851+
const testData = {
1852+
embedConfiguration: {
1853+
id: 'fakeId',
1854+
accessToken: 'fakeToken'
1855+
}
1856+
};
1857+
1858+
spyHpm.post.and.returnValue(Promise.resolve(null));
1859+
1860+
// Act
1861+
report.load(testData.embedConfiguration);
1862+
1863+
// Assert
1864+
expect(spyHpm.post).toHaveBeenCalledWith('/report/load', testData.embedConfiguration);
1865+
});
1866+
1867+
it('report.load() returns promise that rejects with validation error if the load configuration is invalid', function (done) {
1868+
// Arrange
1869+
const testData = {
1870+
embedConfiguration: {
1871+
id: 'fakeId',
1872+
accessToken: 'fakeToken'
1873+
},
1874+
errorResponse: {
1875+
body: {
1876+
message: "invalid configuration object"
1877+
}
1878+
}
1879+
};
1880+
1881+
spyHpm.post.and.returnValue(Promise.reject(testData.errorResponse));
1882+
1883+
// Act
1884+
report.load(testData.embedConfiguration)
1885+
.catch(error => {
1886+
expect(spyHpm.post).toHaveBeenCalledWith('/report/load', testData.embedConfiguration);
1887+
expect(error).toEqual(testData.errorResponse.body);
1888+
// Assert
1889+
done();
1890+
});
1891+
});
1892+
1893+
it('report.load() returns promise that rejects with server error if there was an error loading the report', function (done) {
1894+
// Arrange
1895+
const testData = {
1896+
embedConfiguration: {
1897+
id: 'fakeId',
1898+
accessToken: 'fakeToken'
1899+
},
1900+
errorResponse: {
1901+
body: {
1902+
message: "Access Token is invalid"
1903+
}
1904+
}
1905+
};
1906+
1907+
spyHpm.post.and.returnValue(Promise.reject(testData.errorResponse));
1908+
1909+
// Act
1910+
report.load(testData.embedConfiguration)
1911+
.catch(error => {
1912+
expect(spyHpm.post).toHaveBeenCalledWith('/report/load', testData.embedConfiguration);
1913+
expect(error).toEqual(testData.errorResponse.body);
1914+
// Assert
1915+
done();
1916+
});
1917+
});
1918+
});
1919+
1920+
describe('pages', function () {
1921+
it('report.getPages() sends GET /report/pages', function () {
1922+
// Arrange
1923+
const testData = {
1924+
embedConfiguration: {
1925+
id: 'fakeId',
1926+
accessToken: 'fakeToken'
1927+
}
1928+
};
1929+
1930+
// Act
1931+
report.load(testData.embedConfiguration);
1932+
1933+
// Assert
1934+
expect(spyHpm.post).toHaveBeenCalledWith('/report/load', testData.embedConfiguration);
1935+
});
1936+
});
1937+
});
1938+
1939+
// describe('REPORT-to-SDK', function () {
1940+
// it('should fail', function () {
1941+
// expect(true).toBe(false);
1942+
// });
1943+
// });
1944+
});

e2e/utility/mockHpm.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const spyHpm = {
2+
get: jasmine.createSpy("get").and.returnValue(Promise.resolve(null)),
3+
post: jasmine.createSpy("post").and.returnValue(Promise.resolve(null)),
4+
patch: jasmine.createSpy("patch").and.returnValue(Promise.resolve(null)),
5+
put: jasmine.createSpy("put").and.returnValue(Promise.resolve(null)),
6+
delete: jasmine.createSpy("delete").and.returnValue(Promise.resolve(null))
7+
};

e2e/utility/mockReportEmbed.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ export function setup(iframeContentWindow: Window, parentWindow: Window, logMess
1818
logMessages: false
1919
});
2020
const hpm = new Hpm.HttpPostMessage(wpmp, {
21-
origin: 'powerbi'
21+
'origin': 'reportEmbedMock',
22+
'x-version': '1.0.0'
2223
});
2324
const router = new Router.Router(wpmp);
2425
const app = mockApp;

src/core.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import { Embed, IEmbedConstructor, IEmbedOptions } from './embed';
1+
import { Embed, IEmbedConstructor, IEmbedOptions, IHpmFactory, IWpmpFactory } from './embed';
22
import { Report } from './report';
33
import { Tile } from './tile';
44
import { Utils } from './util';
5+
import * as wpmp from 'window-post-message-proxy';
6+
import * as hpm from 'http-post-message';
57

68
export interface IPowerBiElement extends HTMLElement {
79
powerBiEmbed: Embed;
@@ -13,9 +15,14 @@ export interface IPowerBiConfiguration {
1315
}
1416

1517
export class PowerBi {
18+
1619
/**
1720
* List of components this service can embed.
1821
*/
22+
/**
23+
* TODO: See if it's possible to remove need for this interface and just use Embed base object as common between Tile and Report
24+
* This was only put it to allow both types of components to be in the same list
25+
*/
1926
private static components: IEmbedConstructor[] = [
2027
Tile,
2128
Report
@@ -50,7 +57,13 @@ export class PowerBi {
5057
/** List of components (Reports/Tiles) that have been embedded using this service instance. */
5158
private embeds: Embed[];
5259

53-
constructor(config: IPowerBiConfiguration = {}) {
60+
private hpmFactory: IHpmFactory;
61+
private wpmpFactory: IWpmpFactory;
62+
63+
constructor(hpmFactory: IHpmFactory, wpmpFactory: IWpmpFactory, config: IPowerBiConfiguration = {}) {
64+
this.hpmFactory = hpmFactory;
65+
this.wpmpFactory = wpmpFactory;
66+
5467
this.embeds = [];
5568
window.addEventListener('message', this.onReceiveMessage.bind(this), false);
5669

@@ -115,8 +128,8 @@ export class PowerBi {
115128
// The getGlobalAccessToken function is only here so that the components (Tile | Report) can get the global access token without needing reference
116129
// to the service that they are registered within becaues it creates circular dependencies
117130
config.getGlobalAccessToken = () => this.accessToken;
118-
119-
const component = new Component(element, config);
131+
132+
const component = new Component(this.hpmFactory, this.wpmpFactory, element, config);
120133
element.powerBiEmbed = component;
121134
this.embeds.push(component);
122135

@@ -206,6 +219,4 @@ export class PowerBi {
206219
}
207220
}
208221
}
209-
}
210-
211-
// export default PowerBi;
222+
}

src/embed.ts

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import { Utils } from './util';
2+
import * as wpmp from 'window-post-message-proxy';
3+
import * as hpm from 'http-post-message';
4+
import * as router from 'powerbi-router';
5+
import * as filters from 'powerbi-filters';
26

37
declare global {
48
interface Document {
@@ -18,9 +22,16 @@ declare global {
1822
}
1923
}
2024

25+
/**
26+
* TODO: Consider adding type: "report" | "tile" property to indicate what type of object to embed
27+
*
28+
* This would align with goal of having single embed page which adapts to the thing being embedded
29+
* instead of having M x N embed pages where M is type of object (report, tile) and N is authorization
30+
* type (PaaS, SaaS, Anonymous)
31+
*/
2132
export interface ILoadMessage {
22-
action: string;
2333
accessToken: string;
34+
id: string;
2435
}
2536

2637
export interface IEmbedOptions {
@@ -35,7 +46,15 @@ export interface IEmbedOptions {
3546
}
3647

3748
export interface IEmbedConstructor {
38-
new(...args: any[]): Embed;
49+
new(hpmFactory: IHpmFactory, wpmpFactory: IWpmpFactory, element: HTMLElement, options: IEmbedOptions): Embed;
50+
}
51+
52+
export interface IHpmFactory {
53+
(wpmp: wpmp.WindowPostMessageProxy): hpm.HttpPostMessage;
54+
}
55+
56+
export interface IWpmpFactory {
57+
(window: Window, name?: string, logMessages?: boolean): wpmp.WindowPostMessageProxy;
3958
}
4059

4160
export abstract class Embed {
@@ -55,11 +74,14 @@ export abstract class Embed {
5574
filterPaneEnabled: true
5675
};
5776

77+
wpmp: wpmp.WindowPostMessageProxy;
78+
hpm: hpm.HttpPostMessage;
79+
router: router.Router;
5880
element: HTMLElement;
5981
iframe: HTMLIFrameElement;
6082
options: IEmbedOptions;
6183

62-
constructor(element: HTMLElement, options: IEmbedOptions) {
84+
constructor(hpmFactory: IHpmFactory, wpmpFactory: IWpmpFactory, element: HTMLElement, options: IEmbedOptions) {
6385
this.element = element;
6486

6587
// TODO: Change when Object.assign is available.
@@ -72,14 +94,18 @@ export abstract class Embed {
7294
this.element.innerHTML = iframeHtml;
7395
this.iframe = <HTMLIFrameElement>this.element.childNodes[0];
7496
this.iframe.addEventListener('load', () => this.load(this.options, false), false);
97+
98+
this.wpmp = wpmpFactory(this.iframe.contentWindow, 'SdkReportWpmp', true);
99+
this.hpm = hpmFactory(this.wpmp);
100+
this.router = new router.Router(this.wpmp);
75101
}
76102

77103
/**
78104
* Handler for when the iframe has finished loading the powerbi placeholder page.
79105
* This is used to inject configuration options such as access token, loadAction, etc
80106
* which allow iframe to load the actual report with authentication.
81107
*/
82-
load(options: IEmbedOptions, requireId: boolean = false, message: ILoadMessage = null) {
108+
load(options: IEmbedOptions, requireId: boolean = false, message: ILoadMessage = null): Promise<void> {
83109
if(!message) {
84110
throw new Error(`You called load without providing message properties from the concrete embeddable class.`);
85111
}
@@ -90,12 +116,10 @@ export abstract class Embed {
90116

91117
Utils.assign(message, baseMessage);
92118

93-
const event = {
94-
message
95-
};
96-
97-
Utils.raiseCustomEvent(this.element, event.message.action, event);
98-
this.iframe.contentWindow.postMessage(JSON.stringify(event.message), '*');
119+
return this.hpm.post('/report/load', message)
120+
.catch((response: hpm.IResponse) => {
121+
throw response.body;
122+
});
99123
}
100124

101125
/**

src/factories.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* TODO: Need to find better place for these factory functions or refactor how we handle dependency injection
3+
* Need to
4+
*/
5+
import { IHpmFactory, IWpmpFactory } from './embed';
6+
import * as wpmp from 'window-post-message-proxy';
7+
import * as hpm from 'http-post-message';
8+
9+
export {
10+
IHpmFactory,
11+
IWpmpFactory
12+
};
13+
14+
export const hpmFactory: IHpmFactory = (wpmp) => {
15+
return new hpm.HttpPostMessage(wpmp, {
16+
'origin': 'sdk',
17+
'x-sdk-type': 'js',
18+
'x-sdk-version': '2.0.0'
19+
});
20+
};
21+
22+
export const wpmpFactory: IWpmpFactory = (window, name?: string, logMessages?: boolean) => {
23+
return new wpmp.WindowPostMessageProxy(window, {
24+
processTrackingProperties: {
25+
addTrackingProperties: hpm.HttpPostMessage.addTrackingProperties,
26+
getTrackingProperties: hpm.HttpPostMessage.getTrackingProperties,
27+
},
28+
isErrorMessage: hpm.HttpPostMessage.isErrorMessage,
29+
name,
30+
logMessages
31+
});
32+
};

src/powerbi.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { PowerBi } from './core';
2+
import { IHpmFactory, IWpmpFactory } from './embed';
3+
import * as factories from './factories';
24

35
declare global {
46
interface Window {
@@ -13,4 +15,4 @@ declare global {
1315
* Create instance of class with default config for normal usage.
1416
*/
1517
window.Powerbi = PowerBi;
16-
window.powerbi = new PowerBi();
18+
window.powerbi = new PowerBi(factories.hpmFactory, factories.wpmpFactory);

0 commit comments

Comments
 (0)