diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f1066ea --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +/.idea/ diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..071272d --- /dev/null +++ b/.jshintrc @@ -0,0 +1,12 @@ +{ + "node": true, + "browser": true, + "bitwise": true, + "undef": true, + "trailing": true, + "quotmark": true, + "indent": 4, + "unused": "vars", + "latedef": "nofunc", + "-W030": false +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5db520c --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ +Tools for Apache Cordova CLI - Plugin Simulation + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..87d2e52 --- /dev/null +++ b/README.md @@ -0,0 +1,239 @@ +A browser based plugin simulation tool to aid development and testing of Cordova applications. + +# Installation + +``` +npm install -g taco-simulate +``` + + +# Usage + +## CLI +From the command line anywhere within a Cordova project, enter the following: + +``` +simulate [] [--target=] +``` + +Where: + +* **platform** is any Cordova platform that has been added to your project. Defaults to `browser`. +* **browser** is the name of the browser to launch your app in. Can be any of the following: `chrome`, `chromium`, `edge`, `firefox`, `ie`, `opera`, `safari`. Defaults to `chrome`. + +## API +You can also `require('taco-simulate')` and launch a simulation via the API: + +```JavaScript +var simulate = require('taco-simulate'); +simulate(opts); +``` + +Where opts is an object with the following properties (all optional): + +* **platform** - any Cordova platform that has been added to your project. Defaults to `browser`. +* **target** - the name of the browser to launch your app in. Can be any of the following: `chrome`, `chromium`, `edge`, `firefox`, `ie`, `opera`, `safari`. Defaults to `chrome`. +* **port** - the desired port for the server to use. Defaults to `8000`. +* **dir** - the directory to launch from (where it should look for a Cordova project). Defaults to cwd. + + +# What it does + +Running the `simulate` command will launch your app in the browser, and open a second browser window displaying UI (the simulation host), that allows you to configure how plugins in your app respond. + +## Features + +* Allows the user to configure plugin simulation through a UI. +* Launches the application in a separate browser window so that it's not launched within an iFrame, to ease up debugging. +* Allows user to persist the settings for a plug-in response. +* Allows plugins to customize their own UI. + +## Supported plugins + +This preview version currently includes built-in support for the following Cordova plugins: + +* [cordova-plugin-camera](https://github.com/apache/cordova-plugin-camera) +* [cordova-plugin-console](https://github.com/apache/cordova-plugin-console) +* [cordova-plugin-contacts](https://github.com/apache/cordova-plugin-contacts) +* [cordova-plugin-device](https://github.com/apache/cordova-plugin-device) +* [cordova-plugin-device-motion](https://github.com/apache/cordova-plugin-device-motion) +* [cordova-plugin-dialogs](https://github.com/apache/cordova-plugin-dialogs) +* [cordova-plugin-file](https://github.com/apache/cordova-plugin-file) +* [cordova-plugin-geolocation](https://github.com/apache/cordova-plugin-geolocation) +* [cordova-plugin-globalization](https://github.com/apache/cordova-plugin-globalization) +* [cordova-plugin-media](https://github.com/apache/cordova-plugin-media) +* [cordova-plugin-vibration](https://github.com/apache/cordova-plugin-vibration) + +## Adding simulation support to plugins + +It also allows for plugins to define their own UI. To add simulation support to a plugin, follow these steps: + +1. Clone the `taco-simulate-server` git repository (`git clone https://github.com/microsoft/taco-simulate-server.git`), as it contains useful example code (see `src/plugins`). +2. Add your plugin UI code to your plugin in `src/simulation`. Follow the file naming conventions seen in the built-in plugins. + +### Detailed steps + +In your plugin project, add a `simulation` folder under `src`, then add any of the following files: + +``` +sim-host-panels.html +sim-host-dialogs.html +sim-host.js +sim-host-handlers.js +app-host.js +app-host-handlers.js +app-host-clobbers.js +``` + +#### Simulation Host Files + +*sim-host-panels.html* + +This defines panels that will appear in the simulation host UI. At the top level, it should contain one or more +`cordova-panel` elements. The `cordova-panel` element should have an `id` which is unique to the plugin (so the plugin +name is one possibility, or the shortened version for common plugins (like just `camera` instead of +`cordova-plugin-camera`). It should also have a `caption` attribute which defines the caption of the panel. + +The contents of the `cordova-panel` element can be regular HTML, or the various custom elements which are supported +(see the existing plugin files for more details). + +This file shouldn't contain any JavaScript (including inline event handlers), nor should it link any JavaScript files. +Any JavaScript required can be provided in the standard JavaScript files described below, or in additional JavaScript +files that can be included using `require()`. + +*sim-host-dialogs.html* + +This defines any dialogs that will be used (dialogs are simple modal popups � such as used for the Camera plugin). At +the top level it should contain one or more `cordova-dialog` elements. Each of these must have `id` and `caption` +attributes (as for `sim-host-panels.html`). The `id` will be used in calls to `dialog.showDialog()` and +`dialog.hideDialog()` (see [taco-simulate-server/src/plugins/cordova-plugin-camera/sim-host.js] +(https://github.com/Microsoft/taco-simulate-server/blob/master/src/plugins/cordova-plugin-camera/sim-host.js) +for example code). + +Other rules for this file are the same as for `sim-host-panels.html`. + +*sim-host.js* + +This file should contain code to initialize your UI. For example � attach event handlers, populate lists etc. It should +set `module.exports` to one of the following: + +1. An object with an `initialize` method, like this: + +``` js +module.exports = { + initialize: function () { + // Your initialization code here. + } +}; +``` + +2. A function that returns an object with an `initialize` method. This function will be passed a single parameter � +`messages` � which is a plugin messaging object that can be used to communicate between `sim-host` and `app-host`. +This form is used when the plugin requires that `messages` object � otherwise the simple form can be used. For example: + +``` js +module.exports = function (messages) { + return { + initialize: function () { + // Your initialization code here. + } + }; +}; +``` + +In both cases, the code *currently* executes in the context of the overall simulation host HTML document. You can use +`getElementById()` or `querySelector()` etc to reference elements in your panel to attach events etc. In the future, +this will change and there will be a well defined, limited, asynchronous API for manipulating elements in your +simulation UI. + +*sim-host-handlers.js* + +This file defines handlers for plugin `exec` calls. It should return an object in the following form: + +``` js +{ + service1: { + action1: function (success, error, args) { + // exec handler + }, + action2: function (success, error, args) { + // exec handler + } + }, + service2: { + action1: function (success, error, args) { + // exec handler + }, + action2: function (success, error, args) { + // exec handler + } + } +} +``` + +It can define handlers for any number of service/action combinations. As for `sim-host.js`, it can return the object +either by; + +1. Setting module.exports to this object. +2. Setting module.exports to a function that returns this object (in which case the messages parameter will be passed to that function). + +#### App Host Files + +*app-host.js* + +This file is injected into the app itself (as part of a single, combined, `app-host.js` file). Typically, it would +contain code to respond to messages from `sim-host` code, and as such `module.exports` should be set a function that +takes a single `messages` parameter. It doesn't need to return anything. + +*app-host-handlers.js* + +This file is to provide `app-host` side handling of `exec` calls (if an `exec` call is handled on the `app-host` side, +then it doesn't need to be handled on the `sim-host` side, and in fact any `sim-host` handler will be ignored). The +format is the same as `sim-host-handlers.js`. + +*app-host-clobbers.js* + +This file provides support for "clobbering" built in JavaScript objects. It's form is similar to `app-host-handlers.js`, +expect that the returned object defines what you are clobbering. For example, the built-in support for the `geolocation` +plugin uses this to support simulating geolocation even when the plugin isn't present in the app (just like `Ripple` +does), by returning the following: + +``` js +{ + navigator: { + geolocation: { + getCurrentPosition: function (successCallback, errorCallback, options) { + // Blah blah blah + }, + watchPosition: function (successCallback, errorCallback, options) { + // Blah blah blah + } + } + } +} +``` + +#### The "messages" Object + +A `messages` object is provided to all standard JavaScript files on both the `app-host` and `sim-host` side of things. +It provides the following methods: + +`messages.call(method, param1, param2 ...)`: Calls a method implemented on "the other side" (that were registered by +calling `messages.register()`) and returns a promise for the return value, that is fulfilled when the method returns. + +`messages.register(method, handler)`: Registers a method handler, which can be called via `messages.call()`. + +`messages.emit(message, data)`: Emits a message with data (scalar value or JavaScript object) which will be received by +any code that registers for it (in both `app-host` and `sim-host`). + +`messages.on(message, handler)`: Register interest in a particular message. + +`messages.off(message, handler)`: Un-register interest in a particular message. + +Note that: +* All the above methods are isolated to the plugin � that is, they can only be used to communicate within the plugin's + own code. For example, when you emit a message, it will only be received by code for the same plugin that registers to + hear it. So different plugins can use the same method and message names without conflict. +* A method call is always sent from `app-host` to `sim-host` or vice versa (that is, a call from `app-host` can only be + handled by a method registered on `sim-host`, and vice versa). +* Emitted messages, on the other hand, are sent both "locally" and across to the "other side". diff --git a/RELEASENOTES.md b/RELEASENOTES.md new file mode 100644 index 0000000..1b6b71f --- /dev/null +++ b/RELEASENOTES.md @@ -0,0 +1,4 @@ +# taco-simulate Release Notes + +### 0.1.0 (Oct 8, 2015) +* Initial test release diff --git a/bin/simulate b/bin/simulate new file mode 100755 index 0000000..e654faa --- /dev/null +++ b/bin/simulate @@ -0,0 +1,41 @@ +#!/usr/bin/env node + +// Copyright (c) Microsoft Corporation. All rights reserved. + +var chalk = require('chalk'); + +var args; +try { + args = processArgs(process.argv); + require('./../src/simulate')({platform: args.platform, target: args.target}); +} catch (e) { + console.log(chalk.red.bold(e.stack)); +} + +function processArgs(args) { + var platform = null; + var target = null; + + args.shift(); // Remove 'node' + args.shift(); // Remove 'simulate' + + args.forEach(function (arg) { + arg = arg.toLowerCase(); + if (arg.indexOf('--target=') === 0) { + if (target) { + throw new Error('Target defined more than once'); + } + target = arg.substring(9); + } else { + if (platform) { + throw new Error('Too many arguments'); + } + platform = arg; + } + }); + + return { + platform: platform, + target: target + }; +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c70ef1a --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "taco-simulate", + "version": "0.1.0", + "description": "A browser based plugin simulation tool to aid development and testing of Cordova applications.", + "bin": { + "simulate": "./bin/simulate" + }, + "main": "src/simulate.js", + "repository": { + "type": "git", + "url": "https://github.com/microsoft/taco-simulate" + }, + "keywords": [ + "taco", + "cordova", + "plugins", + "simulation" + ], + "author": { + "name": "Microsoft Corp." + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/microsoft/taco-simulate/issues" + }, + "homepage": "https://github.com/microsoft/taco-simulate", + "dependencies": { + "chalk": "^1.1.1", + "cordova-serve": "^1.0.0", + "replacestream": "^4.0.0", + "send": "timbarham/send#transform", + "taco-simulate-server": "^0.1.0", + "webcomponents.js": "^0.7.2" + }, + "engines": { + "node": ">= 0.12.0", + "npm": ">= 2.5.1" + } +} diff --git a/src/server/server.js b/src/server/server.js new file mode 100644 index 0000000..9cfa364 --- /dev/null +++ b/src/server/server.js @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +var path = require('path'), + replaceStream = require('replacestream'), + send = require('send'), + simulateServer = require('taco-simulate-server'); + +module.exports.attach = function (app) { + app.get('/simulator/sim-host.css', function (request, response, next) { + // If target browser isn't Chrome (user agent contains 'Chrome', but isn't 'Edge'), remove shadow dom stuff from + // the CSS file. + var userAgent = request.headers['user-agent']; + if (userAgent.indexOf('Chrome') > -1 && userAgent.indexOf('Edge/') === -1) { + next(); + } else { + send(request, path.resolve(simulateServer.dirs.hostRoot['sim-host'], 'sim-host.css'), { + transform: function (stream) { + return stream + .pipe(replaceStream('> ::content >', '>')) + .pipe(replaceStream(/\^|\/shadow\/|\/shadow-deep\/|::shadow|\/deep\/|::content|>>>/g, ' ')); + } + }).pipe(response); + } + }); +}; diff --git a/src/sim-host/close.png b/src/sim-host/close.png new file mode 100644 index 0000000..b97c36d Binary files /dev/null and b/src/sim-host/close.png differ diff --git a/src/sim-host/collapse.png b/src/sim-host/collapse.png new file mode 100644 index 0000000..dfad40b Binary files /dev/null and b/src/sim-host/collapse.png differ diff --git a/src/sim-host/custom-elements.js b/src/sim-host/custom-elements.js new file mode 100644 index 0000000..c2c784f --- /dev/null +++ b/src/sim-host/custom-elements.js @@ -0,0 +1,398 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +var dialog = require('dialog'); + +function initialize() { + registerCustomElement('cordova-panel', function () { + var panel = this; + var content = panel.shadowRoot.querySelector('.cordova-content'); + + this.shadowRoot.querySelector('.cordova-header span').textContent = this.getAttribute('caption'); + + this.shadowRoot.querySelector('.cordova-collapse-icon').addEventListener('click', function (e) { + // Animate showing and hiding the panel. Note that we can't use jQuery for this, because it doesn't work + // with elements in the shadow DOM. + + var collapsed = e.target.classList.contains('cordova-collapsed'); + + if (collapsed) { + e.target.classList.remove('cordova-collapsed'); + + // Trick to get the current computed height (won't animate) - note that we want to get this dynamically + // now rather than storing it before we collapse, since things could happen while we're collapsed that + // change our height. + content.style.display = ''; + content.style.height = ''; + var computedHeight = window.getComputedStyle(content).height; + content.style.height = '0'; + + // Animate to computed height after a timeout + window.setTimeout(function () { + content.style.height = computedHeight; + }, 0); + } else { + e.target.classList.add('cordova-collapsed'); + + // Force height to a value that can be animated, then set to 0 after a timeout. We store the height in + // max-height to use when we're animating back to full height. + content.style.height = window.getComputedStyle(content).height; + window.setTimeout(function () { + content.style.height = '0'; + }, 0); + } + }); + + content.addEventListener('transitionend', function (e) { + // After we've transitioned back to full size, reset height to empty to allow dynamic height changes. + if (parseInt(e.target.style.height) === 0) { + e.target.style.display = 'none'; + } else { + e.target.style.height = ''; + } + }); + }); + + registerCustomElement('cordova-dialog', function () { + this.shadowRoot.querySelector('.cordova-header span').textContent = this.getAttribute('caption'); + this.shadowRoot.querySelector('.cordova-close-icon').addEventListener('click', function () { + dialog.hideDialog(); + }); + }); + + registerCustomElement('cordova-item-list', { + addItem: { + value: function (item) { + this.appendChild(item); + } + }, + removeItem: { + value: function (item) { + this.removeChild(this.children[item]); + } + } + }, function () { + this.classList.add('cordova-group'); + }); + + registerCustomElement('cordova-item', function () { + this.classList.add('cordova-group'); + var item = this; + this.addEventListener('click', function (e) { + if (e.target === this) { + // If the click target is our self, the only thing that could have been clicked is the delete icon. + + var list = this.parentNode; + + // If we're within a list, calculate index in the list + var childIndex = list && list.tagName === 'CORDOVA-ITEM-LIST' ? Array.prototype.indexOf.call(list.children, this) : -1; + + // Raise an event on ourselves + var itemRemovedEvent = new CustomEvent('itemremoved', {detail: {itemIndex: childIndex}, bubbles: true}); + this.dispatchEvent(itemRemovedEvent); + + list.removeChild(this); + } + }); + }); + + registerCustomElement('cordova-panel-row', function () { + this.classList.add('cordova-panel-row'); + this.classList.add('cordova-group'); + }); + + registerCustomElement('cordova-group', function () { + this.classList.add('cordova-group'); + }); + + registerCustomElement('cordova-checkbox', { + checked: { + get: function () { + return this.shadowRoot.getElementById('cordova-checkbox-template-input').checked; + }, + set: function (value) { + setValueSafely(this.shadowRoot.getElementById('cordova-checkbox-template-input'), 'checked', value); + } + } + }, function () { + if (this.parentNode.tagName === 'CORDOVA-PANEL') { + this.classList.add('cordova-panel-row'); + this.classList.add('cordova-group'); + } else { + // Reverse the order of the checkbox and caption + this.shadowRoot.appendChild(this.shadowRoot.querySelector('label')); + } + }); + + registerCustomElement('cordova-radio', { + checked: { + get: function () { + return this.shadowRoot.getElementById('cordova-radio-template-input').checked; + }, + set: function (value) { + setValueSafely(this.shadowRoot.getElementById('cordova-radio-template-input'), 'checked', value); + } + } + }, function () { + var isChecked = this.getAttribute('checked'); + if (isChecked && isChecked.toLowerCase() === 'true') { + this.shadowRoot.querySelector('input').checked = true; + } + + var parentGroup = findParent(this, 'cordova-group'); + if (parentGroup) { + var radioButton = this.shadowRoot.getElementById('cordova-radio-template-input'); + radioButton.setAttribute('name', parentGroup.id); + } + }); + + registerCustomElement('cordova-label', { + textContent: { + set: function (value) { + setValueSafely(this.shadowRoot.querySelector('label'), 'textContent', value); + }, + get: function () { + return this.shadowRoot.querySelector('label').textContent; + } + } + }, function () { + this.shadowRoot.querySelector('label').textContent = this.getAttribute('label'); + }); + + registerCustomElement('cordova-text-entry', { + value: { + set: function (value) { + setValueSafely(this.shadowRoot.querySelector('input'), 'value', value); + }, + + get: function () { + return this.shadowRoot.querySelector('input').value; + } + } + }, function () { + this.shadowRoot.querySelector('label').textContent = this.getAttribute('label'); + this.classList.add('cordova-panel-row'); + this.classList.add('cordova-group'); + }, 'input'); + + registerCustomElement('cordova-labeled-value', { + label: { + set: function (value) { + setValueSafely(this.shadowRoot.querySelector('label'), 'textContent', value); + }, + + get: function() { + return this.shadowRoot.querySelector('label').textContent; + } + }, + value: { + set: function (value) { + setValueSafely(this.shadowRoot.querySelector('span'), 'textContent', value); + }, + + get: function() { + return this.shadowRoot.querySelector('span').textContent; + } + } + }, function () { + this.shadowRoot.querySelector('label').textContent = this.getAttribute('label'); + this.shadowRoot.querySelector('span').textContent = this.getAttribute('value'); + this.classList.add('cordova-panel-row'); + this.classList.add('cordova-group'); + }); + + registerCustomElement('cordova-button', 'button'); + + registerCustomElement('cordova-file', { + input: { + get: function () { + return this.shadowRoot.querySelector('input'); + } + }, + files: { + get: function () { + return this.shadowRoot.querySelector('input').files; + } + } + }, 'input'); + + registerCustomElement('cordova-combo', { + options: { + get: function () { + return this.shadowRoot.querySelector('select').options; + } + }, + selectedIndex: { + get: function () { + return this.shadowRoot.querySelector('select').selectedIndex; + } + }, + value: { + get: function () { + return this.shadowRoot.querySelector('select').value; + } + }, + appendChild: { + value: function (node) { + this.shadowRoot.querySelector('select').appendChild(node); + } + } + }, function () { + this.classList.add('cordova-panel-row'); + this.classList.add('cordova-group'); + var select = this.shadowRoot.querySelector('select'); + var label = this.getAttribute('label'); + if (label) { + this.shadowRoot.querySelector('label').textContent = this.getAttribute('label'); + } else { + select.style.width = this.style.width || '100%'; + select.style.minWidth = this.style.minWidth; + } + // Move option elements to be children of select element + var options = this.querySelectorAll('option'); + Array.prototype.forEach.call(options, function (option) { + select.appendChild(option); + }); + }, 'select'); +} + +function registerCustomElement(name) { + var args = arguments; + function findArg(argType) { + return Array.prototype.find.call(args, function (arg, index) { + return index > 0 && (typeof arg === argType); + }); + } + + var protoProperties = findArg('object'); + var initializeCallback = findArg('function'); + var eventTargetSelector = findArg('string'); + + var constructorName = name.split('-').map(function (bit) { + return bit.charAt(0).toUpperCase() + bit.substr(1); + }).join(''); + + var proto = Object.create(HTMLElement.prototype); + if (protoProperties) { + Object.defineProperties(proto, protoProperties); + } + + function initialize() { + this.initialized = true; + + var eventTarget = eventTargetSelector && this.shadowRoot.querySelector(eventTargetSelector); + if (eventTarget) { + // Make sure added events are redirected. Add more on handlers here as we find they're needed + Object.defineProperties(this, { + addEventListener: { + value: function (a, b, c) { + eventTarget.addEventListener(a, b, c); + } + }, + click: { + value: eventTarget.click + }, + onclick: { + get: function () { + return eventTarget.onclick; + }, + set: function (value) { + eventTarget.onclick = value; + } + }, + onchange: { + get: function () { + return eventTarget.onchange; + }, + set: function (value) { + eventTarget.onchange = value; + } + } + }); + } + + // We don't allow inline event handlers. Detect them and strip. + var atts = this.attributes; + Array.prototype.forEach.call(atts, function (att) { + if (att.name.indexOf('on') === 0) { + console.error('Unsupported inline event handlers detected: ' + name + '.' + att.name + '="' + att.value + '"'); + this.removeAttribute(att.name); + } + }, this); + + + // Initialize if it is required + initializeCallback && initializeCallback.call(this); + + // Apply attributes + } + + proto.attachedCallback = function () { + if (!this.initialized) { + // If it hasn't already been initialized, do so now. + initialize.call(this); + } + }; + + proto.createdCallback = function () { + var t = document.getElementById(name + '-template'); + var shadowRoot = this.createShadowRoot(); + shadowRoot.appendChild(document.importNode(t.content, true)); + + if (initializeCallback && this.ownerDocument === document) { + // If it is being created in the main document, initialize immediately. + initialize.call(this); + } + }; + + window[constructorName] = document.registerElement(name, { + prototype: proto + }); +} + +function findParent(element, tag) { + if (!Array.isArray(tag)) { + tag = [tag]; + } + + var parent = element.parentNode; + return parent && parent.tagName ? tag.indexOf(parent.tagName.toLowerCase()) > -1 ? parent : findParent(parent, tag) : null; +} + +function setValueSafely(el, prop, value) { + // In IE, setting the property when the element hasn't yet been added to the document can fail (like an issue with + // the webcomponents polyfill), so do it after a setTimeout(). + if (el.ownerDocument.contains(el)) { + el[prop] = value; + } else { + window.setTimeout(function () { + el[prop] = value; + }, 0); + } +} + +module.exports = { + initialize: initialize +}; + +if (!Array.prototype.find) { + Array.prototype.find = function(predicate) { + if (this == null) { + throw new TypeError('Array.prototype.find called on null or undefined'); + } + if (typeof predicate !== 'function') { + throw new TypeError('predicate must be a function'); + } + var list = Object(this); + var length = list.length >>> 0; + var thisArg = arguments[1]; + var value; + + for (var i = 0; i < length; i++) { + value = list[i]; + if (predicate.call(thisArg, value, i, list)) { + return value; + } + } + return undefined; + }; +} diff --git a/src/sim-host/delete.png b/src/sim-host/delete.png new file mode 100644 index 0000000..1916f1e Binary files /dev/null and b/src/sim-host/delete.png differ diff --git a/src/sim-host/expand.png b/src/sim-host/expand.png new file mode 100644 index 0000000..c8899e5 Binary files /dev/null and b/src/sim-host/expand.png differ diff --git a/src/sim-host/sim-host.css b/src/sim-host/sim-host.css new file mode 100644 index 0000000..0235df6 --- /dev/null +++ b/src/sim-host/sim-host.css @@ -0,0 +1,285 @@ +/* Copyright (c) Microsoft Corporation. All rights reserved. */ + +body { + margin: 0 !important; + padding: 0 !important; + border: 0 !important; + font-family: 'Helvetica Neue', 'Roboto', 'Segoe UI', sans-serif; + background: rgb(238,238,238); + user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -webkit-user-select: none; + cursor: default; +} + +html { + height: 100%; +} + +h1 { + font-family: 'Helvetica Neue', 'Roboto', 'Segoe UI', sans-serif; + font-size: 16pt; + font-weight: bold; +} + +a { + text-decoration: underline; + outline: none; + cursor: pointer; + font-size: 12px; + color: rgb(128,128,128); +} + +cordova-panel { + width: 320px; + padding: 5px 0; +} + +cordova-dialog { + width: 377px; +} + +cordova-panel, cordova-dialog { + z-index: 50; + position: relative; + -webkit-column-break-inside:avoid; + page-break-inside:avoid; + break-inside:avoid; + display: block; +} + +body /deep/ .cordova-panel-inner { + box-shadow: 2px 2px 5px 1px rgba(0, 0, 0, 0.2); + background: #fff; + border-radius: 3px; + overflow: hidden; +} + +body /deep/ .cordova-header { + padding: 5px 9px; + position: relative; + background-color: #03a9f4; + color: #fff; + font-size: 16px; + height: 24px; + line-height: 24px; +} + +body /deep/ .cordova-widget { + font-family: 'Helvetica Neue', 'Roboto', 'Segoe UI', sans-serif; + font-size: 14px; +} + +/* 12px margin around top level items in the content a panel */ +body /deep/ .cordova-content > ::content > * { + margin: 12px; + position: relative; +} + +.cordova-main { + margin-left: 10px; + margin-top: 5px; + column-width: 330px; + -moz-column-width: 330px; + -webkit-column-width: 330px; + column-gap: 0; + -moz-column-gap: 0; + -webkit-column-gap: 0; +} + +body /deep/ label, body /deep/ p, body /deep/ .p { + color: #3c8b9e; + font-size: 14px; +} + +body /deep/ .cordova-value { + color: black; + font-size: 14px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-left: 6px; +} + +body /deep/ input { + margin: 0 10px; + padding: 0; +} + +body /deep/ .cordova-overlay { + position: fixed; + left: 0; + top: 0; + z-index: 10000; + width: 100%; + height: 100%; + background-color: rgba(245, 245, 245, 0.9); + + display: -webkit-box; + display: -moz-box; + display: -ms-flexbox; + display: -webkit-flex; + display: flex; + justify-content: center; + align-items: center; +} + +cordova-combo /deep/ select { + min-width: 112px; +} + +cordova-button { + min-width: 112px; + display: inline-block; + position: relative; + text-align: center; + zoom: 1; + overflow: visible; +} + +cordova-button /deep/ button { + width: 100%; + padding: .4em 1em; + cursor: pointer; +} + +body /deep/ select { + cursor: pointer; +} + +body /deep/ select, +body /deep/ input[type^=text], input[type^=number] { + padding: 5px; +} + +body /deep/ input, +body /deep/ textarea, +body /deep/ keygen, +body /deep/ select, +body /deep/ button, +body /deep/ isindex { + font-size: 14px; + margin-left: 0; + margin-right: 0; + resize: none; +} + +body /deep/ textarea { + padding: 2px 4px; + width: calc(100% - 10px); /* Account for padding and border */ + overflow-y: auto; +} + +body /deep/ input[type^=text], +body /deep/ input[type^=number] { + width: 100px; +} + +body /deep/ button:active, +body /deep/ textarea:hover, +body /deep/ textarea:active, +body /deep/ input[type^=text]:focus, +body /deep/ input[type^=number]:focus, +body /deep/ input[type^=text]:hover, +body /deep/ input[type^=number]:hover { + border: 1px solid #CCCCCC; + background: #ffffff; + font-weight: normal; + color: #212121; +} + +body /deep/ .cordova-state-default { + border: 1px solid #d3d3d3; + background: #e6e6e6; + font-weight: normal; + color: #555; +} + +body /deep/ input:focus, +body /deep/ textarea:focus, +body /deep/ isindex:focus, +body /deep/ keygen:focus, +body /deep/ select:focus { + outline: none; +} + +body /deep/ cordova-radio { + display: flex; + align-items: center; +} + +body /deep/ .cordova-panel-row { + display: flex; + justify-content: space-between; + align-items: center; +} + +body /deep/ cordova-group, body /deep/ cordova-item-list { + display: block; +} + +body /deep/ .cordova-radio-wrapper.cordova-inline { + display: inline-flex; +} + +body /deep/ .cordova-radio-label { + padding-left: 0.25em; +} + +body /deep/ cordova-item+cordova-item { + display: block; + border-top: #d3d3d3 1px solid; + margin-top: 10px; + padding-top: 10px; +} + +body /deep/ cordova-item:hover:before { + content: url(delete.png); + position: absolute; + margin-left: auto; + right: 0; + background: white; + font-size: 14px; + padding-left: 8px; + font-weight: bold; + color: darkred; + cursor: pointer; +} + +body /deep/ .cordova-collapse-icon { + cursor: pointer; + background: url('collapse.png') no-repeat 50% 50%; + width: 22px; + height: 22px; + position: absolute; + margin-top: 1px; + margin-bottom: auto; + right: 6px; + margin-left: auto; +} + +body /deep/ .cordova-collapse-icon.cordova-collapsed { + background: url('expand.png') no-repeat 50% 50%; +} + +body /deep/ .cordova-collapse-icon:hover, body /deep/ .cordova-close-icon:hover { + background-color: rgba(255, 255, 255, 0.25); +} + +body /deep/ .cordova-close-icon { + cursor: pointer; + background: url('close.png') no-repeat 50% 50%; + width: 22px; + height: 22px; + position: absolute; + margin-top: 1px; + margin-bottom: auto; + right: 6px; + margin-left: auto; +} + +body /deep/ .cordova-content { + overflow: hidden; + transition: height 0.2s; +} diff --git a/src/sim-host/sim-host.html b/src/sim-host/sim-host.html new file mode 100644 index 0000000..f67d8b4 --- /dev/null +++ b/src/sim-host/sim-host.html @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + diff --git a/src/sim-host/sim-host.js b/src/sim-host/sim-host.js new file mode 100644 index 0000000..0f4fab0 --- /dev/null +++ b/src/sim-host/sim-host.js @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +var db = require('db'), + Messages = require('messages'), + customElements = require('./custom-elements'), + socket = require('./socket'), + dialog = require('dialog'); + +var plugins; +var pluginHandlers = {}; + +customElements.initialize(); +socket.initialize(pluginHandlers); + +window.addEventListener('DOMContentLoaded', function () { + sizeContent(); + + // Initialize standard modules, then plugins + db.initialize().then(initializePlugins); +}); + +window.addEventListener('resize', function () { + sizeContent(); +}); + +function sizeContent() { + // Size the content area to keep column widths fixed + var bodyWidth = parseInt(window.getComputedStyle(document.body).width); + var contentWidth = (Math.floor((bodyWidth - 1) / 333) || 1) * 333; + document.querySelector('.cordova-main').style.width = contentWidth + 'px'; +} + +var pluginMessages = {}; +function applyPlugins(plugins, clobberScope) { + Object.keys(plugins).forEach(function (pluginId) { + var plugin = plugins[pluginId]; + if (plugin) { + if (typeof plugin === 'function') { + pluginMessages[pluginId] = pluginMessages[pluginId] || new Messages(pluginId, socket.socket); + plugin = plugin(pluginMessages[pluginId]); + plugins[pluginId] = plugin; + } + if (clobberScope) { + clobber(plugin, clobberScope); + } + } + }); +} + +function clobber(clobbers, scope) { + Object.keys(clobbers).forEach(function (key) { + if (clobbers[key] && typeof clobbers[key] === 'object') { + scope[key] = scope[key] || {}; + clobber(clobbers[key], scope[key]); + } else { + scope[key] = clobbers[key]; + } + }); +} + +function initializePlugins() { + plugins = { + /** PLUGINS **/ + }; + + var pluginHandlersDefinitions = { + /** PLUGIN-HANDLERS **/ + }; + + applyPlugins(plugins); + applyPlugins(pluginHandlersDefinitions, pluginHandlers); + + // Hide and register dialogs + Array.prototype.forEach.call(document.getElementById('popup-window').children, function (dialogRef) { + dialogRef.show = function () { + document.getElementById('popup-window').style.display = ''; + this.style.display = ''; + }; + dialogRef.hide = function () { + document.getElementById('popup-window').style.display = 'none'; + this.style.display = 'none'; + }; + dialog.pluginDialogs[dialogRef.id] = dialogRef; + dialogRef.style.display = 'none'; + }); + + Object.keys(plugins).forEach(function (pluginId) { + try{ + plugins[pluginId].initialize && plugins[pluginId].initialize(); + } catch (e) { + console.error('Error initializing plugin ' + pluginId); + console.error(e); + } + }); +} diff --git a/src/sim-host/socket.js b/src/sim-host/socket.js new file mode 100644 index 0000000..691ac04 --- /dev/null +++ b/src/sim-host/socket.js @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +var socket; + +module.exports.initialize = function (pluginHandlers) { + socket = io(); + module.exports.socket = socket; + + socket.emit('register-simulation-host'); + socket.on('exec', function (data) { + if (!data) { + throw 'Exec called on simulation host without exec info'; + } + + var index = data.index; + if (typeof index !== 'number') { + throw 'Exec called on simulation host without an index specified'; + } + + var success = getSuccess(index); + var failure = getFailure(index); + + var service = data.service; + if (!service) { + throw 'Exec called on simulation host without a service specified'; + } + + var action = data.action; + if (!action) { + throw 'Exec called on simulation host without an action specified'; + } + + console.log('Exec ' + service + '.' + action + ' (index: ' + index + ')'); + + var handler = pluginHandlers[service] && pluginHandlers[service][action]; + if (!handler) { + handler = pluginHandlers['*']['*']; + handler(success, failure, service, action, data.args); + } else { + handler(success, failure, data.args); + } + }); + + socket.on('refresh', function () { + document.location.reload(true); + }); +}; + +function getSuccess(index) { + return function (result) { + console.log('Success callback for index: ' + index + '; result: ' + result); + var data = {index: index, result: result}; + socket.emit('exec-success', data); + }; +} + +function getFailure(index) { + return function (error) { + console.log('Failure callback for index: ' + index + '; error: ' + error); + var data = {index: index, error: error}; + socket.emit('exec-failure', data); + }; +} diff --git a/src/simulate.js b/src/simulate.js new file mode 100644 index 0000000..b225930 --- /dev/null +++ b/src/simulate.js @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +var cordovaServe = require('cordova-serve'), + path = require('path'), + simulateServer = require('taco-simulate-server'); + +module.exports = function (opts) { + require('./server/server').attach(simulateServer.app); + + var target = opts.target || 'chrome'; + var simHostUrl; + + simulateServer(opts, { + simHostRoot: path.join(__dirname, 'sim-host'), + node_modules: path.resolve(__dirname, '..', 'node_modules') + }).then(function (urls) { + simHostUrl = urls.simHostUrl; + return cordovaServe.launchBrowser({target: target, url: urls.appUrl}); + }).then(function () { + return cordovaServe.launchBrowser({target: target, url: simHostUrl}); + }); +};