Initial Commit. Setup build for windowPostMessageProxy.ts to output as umd module. Setup tests but they don't run properly yet.

This commit is contained in:
Matt Mazzola 2016-05-10 17:37:18 -07:00
Родитель cfd0d6a066
Коммит df0e82f996
15 изменённых файлов: 679 добавлений и 1 удалений

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

@ -0,0 +1,5 @@
node_modules
typings
dist
test/*.js
test/*/*.js

3
.npmignore Normal file
Просмотреть файл

@ -0,0 +1,3 @@
node_modules
typings
test

4
.vscode/settings.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,4 @@
// Place your settings in this file to overwrite default and user settings.
{
"editor.tabSize": 4
}

10
.vscode/tasks.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,10 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "0.1.0",
"command": "tsc",
"isShellCommand": true,
"args": ["-p", "."],
"showOutput": "silent",
"problemMatcher": "$tsc"
}

21
LICENSE Normal file
Просмотреть файл

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016 Microsoft
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.

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

@ -1,2 +1,116 @@
# window-post-message-proxy
A generic messaging component for orchestrating promise based messages to/from a hosting application and an iframed component over the window.postMessage API.
A library used in place of the native window.postMessage which when used on both the sending and receiving windows allow for a nicer asynchronouse promise messaging between the windows.
When sending messages using the proxy, it will apply a unique id to the message, create a deferred object referenced by the id, and pass the message on to the target window.
The target window will also have an instance of the windowPostMessage proxy setup which will send back messages and preserve the unique id.
Then the original sending instance receives the response messag with id, it will look to see if there is matching id in cache and if so resolve the deferred object with the response.
## Installation
```
npm install --save window-post-message-proxy
```
## Basic Usage
```
// Setup
const iframe = document.getElementById("myFrame");
const windowPostMessageProxy = new WindowPostMessageProxy(iframe.contentWindow);
// Send message
const message = {
key: "Value"
};
windowPostMessageProxy.postMessage(message)
.then(response => {
});
```
## Advanced Customization
### Customizing how tracking properties are added to the method
By default the windowPostMessage proxy will store the tracking properties as object on the message by known property: `windowPostMesssageProxy`.
This means if you call:
```
const message = {
key: "Value"
};
windowPostMessageProxy.postMessage(message);
```
The message is actually modified before it's sent to become:
```
{
windowPostMessageProxy: {
id: "ebixvvlbwa3tvtjra4i"
},
key: "Value"
};
```
If you want to customize how the tracking properties are added to and retreived from the message you can provide it at construction time as an object with two funtions. See the interface below:
```
export interface IProcessTrackingProperties {
addTrackingProperties<T>(message: T, trackingProperties: ITrackingProperties): T;
getTrackingProperties(message: any): ITrackingProperties;
}
```
`addTrackingProperties` takes a message adds the tracking properties object an returns the message.
`getTrackingProperties` takes a message and extracts the tracking properties.
Example:
```
const customProcessTrackingProperties = {
addTrackingProperties(message, trackingProperties) {
message.headers = {
'tracking-id': trackingProperties.id
};
return message;
},
getTrackingProperties(message): ITrackingProperties {
return {
id: message.headers['tracking-id']
};
}
};
const windowPostMessageProxy = new WindowPostMessageProxy(iframe.contentWindow, customProcessTrackingProperties);
```
### Customizing how messages are detected as error responses.
By default response messages are considered error message if they contain an error property.
You can override this behavior by passing an `isErrorMessage` function at construction time. See interface:
```
export interface IIsErrorMessage {
(message: any): boolean;
}
```
Example:
```
function isErrorMessage(message: any) {
return !(200 <= message.status && message.status < 300);
}
```
## Building
```
tsc -p .
```
Or run `Ctrl + Shift + B` when in Code.
## Testing
```
gulp test --debug
```
The `--debug` uses Chrome and single run. This is temporary work around since phantomjs is timing out.
You can also enable watching by adding the `--watch` argument.

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

@ -0,0 +1,53 @@
var gulp = require('gulp-help')(require('gulp'));
var ts = require('gulp-typescript'),
karma = require('karma'),
runSequence = require('run-sequence'),
merge2 = require('merge2'),
argv = require('yargs').argv
;
gulp.task('test', 'Run unit tests', function (done) {
return runSequence(
'compile:ts',
'compile:spec',
'test:spec',
done
)
});
gulp.task('compile:ts', 'Compile typescript for powerbi library', function() {
var tsProject = ts.createProject('tsconfig.json');
var tsResult = tsProject.src()
.pipe(ts(tsProject))
;
return merge2(
tsResult.js.pipe(gulp.dest('./dist')),
tsResult.dts.pipe(gulp.dest('./dist'))
);
// return gulp.src(['./spec/**/*.ts', './test/**/*.ts'])
// .pipe(webpack(webpackConfig))
// .pipe(gulp.dest('./dist'));
});
gulp.task('compile:spec', 'Compile typescript for tests', function () {
var tsProject = ts.createProject('tsconfig.json');
var tsResult = gulp.src(['typings/browser/**/*.d.ts', './dist/**/*.js', './test/**/*.ts'])
.pipe(ts(tsProject))
;
return tsResult.js.pipe(gulp.dest('./'));
});
gulp.task('copy', 'Copy test utilities', function () {
return gulp.src(['./test/utility/*.html'])
.pipe(gulp.dest('./dist'));
});
gulp.task('test:spec', 'Runs spec tests', function(done) {
new karma.Server.start({
configFile: __dirname + '/karma.conf.js',
singleRun: argv.watch ? false : true,
captureTimeout: argv.timeout || 20000
}, done);
});

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

@ -0,0 +1,34 @@
var argv = require('yargs').argv;
module.exports = function (config) {
config.set({
frameworks: ['jasmine'],
// See: https://github.com/karma-runner/karma/issues/736
files: [
'./node_modules/jquery/dist/jquery.js',
'./node_modules/es6-promise/dist/es6-promise.js',
'./dist/**/*.js',
{ pattern: './test/**/*.js', included: true },
{ pattern: './test/**/*.html', served: true, included: false }
],
exclude: [],
reporters: argv.debug ? ['spec'] : ['spec', 'coverage'],
autoWatch: true,
browsers: [argv.debug ? 'Chrome' : 'PhantomJS'],
plugins: [
'karma-chrome-launcher',
'karma-jasmine',
'karma-spec-reporter',
'karma-phantomjs-launcher',
'karma-coverage'
],
preprocessors: { './dist/**/*.js': ['coverage'] },
coverageReporter: {
reporters: [
{ type: 'html' },
{ type: 'text-summary' }
]
},
logLevel: argv.debug ? config.LOG_DEBUG : config.LOG_INFO
});
};

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

@ -0,0 +1,44 @@
{
"name": "window-post-message-proxy",
"version": "1.0.0",
"description": "A library used in place of the native window.postMessage which when used on both the sending and receiving windows allow for a nicer asynchronouse promise messaging between the windows",
"main": "dist/windowPostMessageProxy.js",
"typings": "dist/windowPostMessageProxy.d.ts",
"scripts": {
"test": "gulp test --debug",
"prepublish": "tsc -p ."
},
"keywords": [
"window",
"post",
"message",
"postmessage",
"iframe",
"proxy"
],
"author": "Microsoft Power BI",
"license": "MIT",
"dependencies": {
"es6-promise": "^3.1.2"
},
"devDependencies": {
"gulp": "^3.9.1",
"gulp-help": "^1.6.1",
"gulp-typescript": "^2.13.0",
"jasmine-core": "^2.4.1",
"jquery": "^2.2.3",
"karma": "^0.13.22",
"karma-chrome-launcher": "^1.0.1",
"karma-coverage": "^0.5.5",
"karma-jasmine": "^0.3.8",
"karma-phantomjs-launcher": "^1.0.0",
"karma-spec-reporter": "0.0.26",
"merge2": "^1.0.2",
"phantomjs-prebuilt": "^2.1.7",
"run-sequence": "^1.1.5",
"yargs": "^4.6.0"
},
"publishConfig": {
"tag": "beta"
}
}

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

@ -0,0 +1,210 @@
interface IDeferred {
resolve: <T>(value?: T | Thenable<T>) => void,
reject: <T>(error: T) => void,
promise: Promise<any>
}
interface IDeferredCache {
[messageId: string]: IDeferred
}
export interface ITrackingProperties {
id: string;
}
export interface IAddTrackingProperties {
<T>(message: T, trackingProperties: ITrackingProperties): T;
}
export interface IGetTrackingProperties {
(message: any): ITrackingProperties;
}
export interface IProcessTrackingProperties {
addTrackingProperties: IAddTrackingProperties;
getTrackingProperties: IGetTrackingProperties;
}
export interface IIsErrorMessage {
(message: any): boolean;
}
export interface IMessageHandler {
test(message: any): boolean;
handle(message: any): any;
}
export class WindowPostMessageProxy {
// Static
private static defaultAddTrackingProperties<T>(message: T, trackingProperties: ITrackingProperties): T {
(<any>message)[WindowPostMessageProxy.messagePropertyName] = trackingProperties;
return message;
}
private static defaultGetTrackingProperties(message: any): ITrackingProperties {
return message[WindowPostMessageProxy.messagePropertyName];
}
private static defaultIsErrorMessage(message: any): boolean {
return !!message.error;
}
private static messagePropertyName = "windowPostMesssageProxy";
// Private
private name: string;
private addTrackingProperties: IAddTrackingProperties;
private getTrackingProperties: IGetTrackingProperties;
private isErrorMessage: IIsErrorMessage;
private contentWindow: Window;
private pendingRequestPromises: IDeferredCache = {};
private handlers: IMessageHandler[];
private windowMessageHandler: (e: MessageEvent) => any;
constructor(
contentWindow: Window,
processTrackingProperties: IProcessTrackingProperties = {
addTrackingProperties: WindowPostMessageProxy.defaultAddTrackingProperties,
getTrackingProperties: WindowPostMessageProxy.defaultGetTrackingProperties
},
isErrorMessage: IIsErrorMessage = WindowPostMessageProxy.defaultIsErrorMessage
) {
this.addTrackingProperties = processTrackingProperties.addTrackingProperties;
this.getTrackingProperties = processTrackingProperties.getTrackingProperties;
this.isErrorMessage = isErrorMessage;
this.handlers = [];
this.name = `WindowProxyMessageHandler(${WindowPostMessageProxy.createRandomString()})`;
this.contentWindow = contentWindow;
this.windowMessageHandler = (event: MessageEvent) => this.onMessageReceived(event);
this.start();
}
/**
* Adds handler.
* If the first handler whose test method returns true will handle the message and provide a response.
*/
addHandler(handler: IMessageHandler) {
this.handlers.push(handler);
}
/**
* Removes handler.
* The reference must match the original object that was provided when adding the handler.
*/
removeHandler(handler: IMessageHandler) {
const handlerIndex = this.handlers.indexOf(handler);
if(handlerIndex == -1) {
throw new Error(`You attempted to remove a handler but no matching handler was found.`);
}
this.handlers.splice(handlerIndex, 1);
}
/**
* Start listening to message events.
*/
start() {
window.addEventListener('message', this.windowMessageHandler);
}
/**
* Stops listening to message events.
*/
stop() {
window.removeEventListener('message', this.windowMessageHandler);
}
/**
* Post message to target window with tracking properties added and save deferred object referenced by tracking id.
*/
postMessage<T>(message: any): Promise<T> {
// Add tracking properties to indicate message came from this proxy
const trackingProperties: ITrackingProperties = { id: WindowPostMessageProxy.createRandomString() };
this.addTrackingProperties(message, trackingProperties);
this.contentWindow.postMessage(message, "*");
const deferred = WindowPostMessageProxy.createDeferred();
this.pendingRequestPromises[trackingProperties.id] = deferred;
return deferred.promise;
}
/**
* Send response message to target window.
* Response messages re-use tracking properties from a previous request message.
*/
private sendResponse(message: any, trackingProperties: ITrackingProperties): void {
this.addTrackingProperties(message, trackingProperties);
this.contentWindow.postMessage(message, "*");
}
/**
* Message handler.
*/
private onMessageReceived(event: MessageEvent) {
console.log(`${this.name} Received message:`);
console.log(`type: ${event.type}`);
console.log(JSON.stringify(event.data, null, ' '));
let message:any = event.data;
let trackingProperties: ITrackingProperties = this.getTrackingProperties(message);
// If this proxy instance could not find tracking properties then disregard message since we can't reliably respond
if (!trackingProperties) {
return;
}
const deferred = this.pendingRequestPromises[trackingProperties.id];
// If message does not have a known ID, treat it as a request
// Otherwise, treat message as response
if (!deferred) {
const handled = this.handlers.some(handler => {
if(handler.test(message)) {
const responseMessage = handler.handle(message);
this.sendResponse(responseMessage, trackingProperties);
return true;
}
});
}
else {
/**
* If error message reject promise,
* Otherwise, resolve promise
*/
if (this.isErrorMessage(message)) {
deferred.reject(message);
}
else {
deferred.resolve(message);
}
}
}
/**
* Utility to create a deferred object.
*/
// TODO: Look to use RSVP library instead of doing this manually.
// From what I searched RSVP would work better because it has .finally and .deferred but it doesn't have Typings information.
private static createDeferred(): IDeferred {
const deferred: IDeferred = {
resolve: null,
reject: null,
promise: null
};
const promise = new Promise((resolve: () => void, reject: () => void) => {
deferred.resolve = resolve;
deferred.reject = reject;
});
deferred.promise = promise;
return deferred;
}
/**
* Utility to generate random sequence of characters used as tracking id for promises.
*/
private static createRandomString(): string {
return (Math.random() + 1).toString(36).substring(7);
}
}

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

@ -0,0 +1,121 @@
import * as wpmp from '../src/windowPostMessageProxy';
describe('windowPostMessageProxy', function () {
describe('test with proxy configured to message own window', function () {
let windowPostMessageProxy: wpmp.WindowPostMessageProxy;
let handler: wpmp.IMessageHandler;
beforeAll(function () {
windowPostMessageProxy = new wpmp.WindowPostMessageProxy(window);
});
afterAll(function () {
windowPostMessageProxy.stop();
});
beforeEach(() => {
handler = {
test: jasmine.createSpy("testSpy").and.returnValue(true),
handle: jasmine.createSpy("handleSpy").and.returnValue({ handled: true })
};
windowPostMessageProxy.addHandler(handler);
});
afterEach(function () {
(<jasmine.Spy>handler.test).calls.reset();
(<jasmine.Spy>handler.handle).calls.reset();
});
it('postMessage returns a promise which is resolved if a response with matching id is observed and message is NOT considered an error message', function (done) {
// Arrange
const testData = {
message: {
messageTest: "abc123"
}
};
// Act
let messagePromise: Promise<void>;
windowPostMessageProxy.postMessage(testData.message)
.then((response:any) => {
expect(response.messageTest).toEqual(testData.message.messageTest);
done();
});
// Assert
});
it('postMessage returns a proimse which is rejected if a response with matching id is observed and message is considered an error message', function (done) {
// Arrange
const testData = {
message: {
error: true
}
};
// Act
let messagePromise: Promise<void>;
windowPostMessageProxy.postMessage(testData.message)
.catch((message:any) => {
expect(message.error).toEqual(true);
done();
});
// Assert
});
it('By default tracking data is added to the message using the default method', function () {
expect(true).toBe(true);
});
});
// Goal is to test entire post message protocol against live embed page by sending messages and testing response.
// Although this library is suppose to be indepdent of PowerBI's particular use case so these tests should probably live with ReportEmbed pages tests.
xdescribe('test with actual report embed page', function () {
let windowPostMessageProxy: wpmp.WindowPostMessageProxy;
let iframe: HTMLIFrameElement;
let iframeLoaded: JQueryPromise<void>;
beforeAll(function () {
const iframeSrc = "http://embed.powerbi.com/appTokenReportEmbed";
const $iframe = $(`<iframe src="${iframeSrc}" id="testiframe"></iframe>`).appendTo(document.body);
iframe = <HTMLIFrameElement>$iframe.get(0);
windowPostMessageProxy = new wpmp.WindowPostMessageProxy(window);
const iframeLoadedDeferred = $.Deferred<void>();
iframe.addEventListener('load', () => {
iframeLoadedDeferred.resolve();
});
iframeLoaded = iframeLoadedDeferred.promise();
});
afterAll(function () {
// Choose to leave iframe in window, for easier debugging of the tests. Specifically to make sure correct html page was loaded.
// $('#testiframe').remove();
});
it('', function (done) {
// Arrange
const testData = {
message: {
error: true
}
};
// Act
// Assert
iframeLoaded
.then(() => {
windowPostMessageProxy.postMessage(testData.message)
.catch((message:any) => {
expect(message.error).toEqual(true);
done();
});
});
});
});
});

16
test/utility/echo.html Normal file
Просмотреть файл

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Iframe Echo tester</title>
</head>
<body>
<h1>Echo Frame Tester</h1>
<p>Repeats all messages back to the origin window.</p>
<script>
window.isIframe = true;
</script>
<script src="echo.js"></script>
</body>
</html>

15
test/utility/echo.ts Normal file
Просмотреть файл

@ -0,0 +1,15 @@
interface Window {
isIframe: boolean;
onMessage: (event: MessageEvent) => void;
}
// Only register the event listener if current window is iframe.
if(window.isIframe) {
window.onMessage = function onMessage(event: MessageEvent) {
console.log(`echo.html received message: ${event.data}`);
console.log(`window.parent === event.source: ${window.parent === event.source}`);
window.parent.postMessage(event.data, "*");
};
window.addEventListener('message', window.onMessage, false);
}

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

@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "es5",
"module": "es2015",
"declaration": true,
"noImplicitAny": true,
"sourceMap": true,
"outDir": "dist"
},
"exclude": [
"node_modules",
"typings/main",
"typings/main.d.ts",
"dist",
"test"
]
}

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

@ -0,0 +1,11 @@
{
"name": "iframetester",
"version": false,
"dependencies": {},
"ambientDependencies": {
"es6-promise": "registry:dt/es6-promise#0.0.0+20160423074304",
"jasmine": "registry:dt/jasmine#2.2.0+20160412134438",
"jquery": "registry:dt/jquery#1.10.0+20160417213236",
"karma-jasmine": "registry:dt/karma-jasmine#0.0.0+20160316155526"
}
}