├── test ├── .eslintrc ├── fixtures │ ├── errorCore.js │ └── spyCore.js ├── utils.js ├── getNextCoreId-test.js ├── tests.webpack.js ├── coreInit-test.js ├── operations-test.js └── loadCore-test.js ├── .babelrc ├── src ├── utils │ └── noop.js ├── index.js ├── getNextCoreId.js ├── coreInit.js ├── operations.js └── loadCore.js ├── demo ├── build │ ├── favicon.png │ └── index.html ├── static │ ├── Lato-Bold.woff │ ├── tiny_grid.png │ ├── Lato-Black.woff │ └── Lato-Regular.woff ├── src │ ├── core.js │ ├── core.error.js │ ├── style2.less │ ├── CoreStatus.js │ ├── core.green.js │ ├── index.js │ ├── style.less │ └── DemoUI.js ├── .babelrc ├── webpack.config.babel.js └── package.json ├── .eslintrc ├── .npmignore ├── CHANGELOG.md ├── .travis.yml ├── scripts └── deploy.sh ├── LICENSE ├── karma.conf.js ├── package.json ├── karma-sauce.conf.js └── README.md /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins": ["transform-object-rest-spread"] 4 | } 5 | -------------------------------------------------------------------------------- /src/utils/noop.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore next */ 2 | function noop() {} 3 | 4 | export default noop 5 | -------------------------------------------------------------------------------- /demo/build/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chromakode/isolated-core/HEAD/demo/build/favicon.png -------------------------------------------------------------------------------- /demo/static/Lato-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chromakode/isolated-core/HEAD/demo/static/Lato-Bold.woff -------------------------------------------------------------------------------- /demo/static/tiny_grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chromakode/isolated-core/HEAD/demo/static/tiny_grid.png -------------------------------------------------------------------------------- /demo/static/Lato-Black.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chromakode/isolated-core/HEAD/demo/static/Lato-Black.woff -------------------------------------------------------------------------------- /demo/static/Lato-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chromakode/isolated-core/HEAD/demo/static/Lato-Regular.woff -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "rules": { 5 | "semi": [2, "never"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import coreInit from './coreInit' 2 | import loadCore from './loadCore' 3 | 4 | export { 5 | coreInit, 6 | loadCore, 7 | } 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | demo/ 2 | scripts/ 3 | src/ 4 | test/ 5 | coverage/ 6 | .babelrc 7 | .eslintrc 8 | .eslintignore 9 | .travis.yml 10 | karma*.conf.js 11 | -------------------------------------------------------------------------------- /demo/src/core.js: -------------------------------------------------------------------------------- 1 | import { coreInit } from 'isolated-core' 2 | 3 | coreInit({ 4 | scriptURL: 'main.js', 5 | run: core => require('./').init(core), 6 | }) 7 | -------------------------------------------------------------------------------- /demo/src/core.error.js: -------------------------------------------------------------------------------- 1 | import { coreInit } from 'isolated-core' 2 | 3 | coreInit({ 4 | scriptURL: 'error.js', 5 | run: () => { 6 | throw new Error('this core crashes!') 7 | }, 8 | }) 9 | -------------------------------------------------------------------------------- /test/fixtures/errorCore.js: -------------------------------------------------------------------------------- 1 | import { coreInit } from '../../src' 2 | 3 | coreInit({ 4 | scriptURL: '/base/test/fixtures/errorCore.js', 5 | run: () => { 6 | throw new Error('oh noes!') 7 | }, 8 | }) 9 | -------------------------------------------------------------------------------- /demo/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"], 3 | "plugins": ["transform-object-rest-spread"], 4 | "env": { 5 | "development": { 6 | "presets": ["react-hmre"], 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | export function cacheBust(url) { 2 | const karma = window.top.karma 3 | if (karma && karma.files.hasOwnProperty(url)) { 4 | return url + '?' + karma.files[url] 5 | } 6 | return url + '?' + String(Math.random()).substr(2) 7 | } 8 | -------------------------------------------------------------------------------- /src/getNextCoreId.js: -------------------------------------------------------------------------------- 1 | export default function getNextCoreId(doc) { 2 | let lastCoreId = doc._lastCoreId 3 | if (lastCoreId === undefined) { 4 | lastCoreId = -1 5 | } 6 | const nextCoreId = lastCoreId + 1 7 | doc._lastCoreId = nextCoreId 8 | return nextCoreId 9 | } 10 | -------------------------------------------------------------------------------- /demo/src/style2.less: -------------------------------------------------------------------------------- 1 | body { 2 | // Thanks to http://subtlepatterns.com/tiny-grid/ 3 | background: url('../static/tiny_grid.png'); 4 | margin: 0; 5 | border: .5rem solid mix(hsla(120, 35%, 64%, 1), black, 35%); 6 | } 7 | 8 | @media (max-width: 420px) { 9 | body { 10 | border-width: .25rem; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | This project adheres to [Semantic Versioning](http://semver.org/). 4 | 5 | ## 0.1.1 - 2016-01-29 6 | ### Fixed 7 | - Superficial README updates. 8 | 9 | ## 0.1.0 - 2016-01-29 10 | ### Added 11 | - Initial public release. 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "5" 5 | 6 | script: 7 | - npm run lint 8 | - if [ "$SAUCE_ACCESS_KEY" ]; then npm run test:sauce; else npm run test; fi 9 | 10 | after_success: 11 | - npm install coveralls lcov-result-merger 12 | - $(npm bin)/lcov-result-merger './coverage/*/lcov.info' | $(npm bin)/coveralls 13 | - ./scripts/deploy.sh 14 | -------------------------------------------------------------------------------- /test/getNextCoreId-test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import getNextCoreId from '../src/getNextCoreId' 3 | 4 | describe('getNextCoreId', () => { 5 | it('increments on each call and sets doc._lastCoreID', () => { 6 | const fakeDoc = {} 7 | expect(getNextCoreId(fakeDoc)).toBe(0) 8 | expect(fakeDoc._lastCoreId).toBe(0) 9 | expect(getNextCoreId(fakeDoc)).toBe(1) 10 | expect(fakeDoc._lastCoreId).toBe(1) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /test/tests.webpack.js: -------------------------------------------------------------------------------- 1 | // Inspired by: 2 | // https://github.com/deepsweet/isparta-loader 3 | // https://github.com/webpack/karma-webpack#alternative-usage 4 | // http://kentor.me/posts/testing-react-and-flux-applications-with-karma-and-webpack/ 5 | 6 | import 'core-js' 7 | 8 | // Require all tests. 9 | const testsContext = require.context('.', true, /-test.js$/) 10 | testsContext.keys().forEach(testsContext) 11 | 12 | // Require all source files so we get full coverage stats. 13 | const srcContext = require.context('../src/', true) 14 | srcContext.keys().forEach(srcContext) 15 | -------------------------------------------------------------------------------- /demo/src/CoreStatus.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | 3 | export default function CoreStatus({ loading, ready, coreRef, error, errorDetail }) { 4 | if (error) { 5 | return {error} error: "{String(errorDetail)}" 6 | } else if (loading) { 7 | return loading... 8 | } else if (ready) { 9 | return core #{coreRef.id} ready! 10 | } 11 | return not loaded 12 | } 13 | 14 | CoreStatus.propTypes = { 15 | loading: PropTypes.bool, 16 | ready: PropTypes.bool, 17 | error: PropTypes.string, 18 | } 19 | -------------------------------------------------------------------------------- /src/coreInit.js: -------------------------------------------------------------------------------- 1 | import loadCore from './loadCore' 2 | import { attachCore } from './operations' 3 | 4 | export default function coreInit(opts) { 5 | if (!window._core) { 6 | // We're running in a top level script. 7 | // Load the first core and attach it. 8 | return loadCore(opts, document).then(coreRef => { 9 | attachCore(coreRef.context, document) 10 | return coreRef 11 | }) 12 | } 13 | 14 | const core = { 15 | id: window._core.id, 16 | args: window._core.args, 17 | ready: handlers => window._core.onReady(handlers), 18 | } 19 | 20 | try { 21 | opts.run(core) 22 | } catch (err) { 23 | window._core.onExecutionError(err) 24 | throw err 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /demo/src/core.green.js: -------------------------------------------------------------------------------- 1 | import { coreInit } from 'isolated-core' 2 | 3 | const CSS2 = { __html: require('./style2.less') } 4 | 5 | coreInit({ 6 | scriptURL: 'green.js', 7 | run: core => { 8 | const React = require('react') 9 | const DemoUI = require('./DemoUI').default 10 | 11 | // Monkeypatch in some extra render nodes. 12 | const origRender = DemoUI.prototype.render 13 | DemoUI.prototype.render = function render() { 14 | return ( 15 |
16 | {origRender.apply(this)} 17 | 7 | 8 | 9 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /demo/webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack' 2 | import path from 'path' 3 | 4 | export default { 5 | entry: { 6 | main: './src/core.js', 7 | green: './src/core.green.js', 8 | error: './src/core.error.js', 9 | }, 10 | 11 | output: { 12 | filename: '[name].js', 13 | path: path.join(__dirname, 'build'), 14 | }, 15 | 16 | module: { 17 | loaders: [ 18 | { 19 | test: /\.js$/, 20 | exclude: /node_modules/, 21 | loader: 'babel', 22 | }, 23 | { 24 | test: /\.less$/, 25 | loaders: ['css', 'autoprefixer', 'less'], 26 | }, 27 | { 28 | test: /\.woff$|\.png$/, 29 | loader: 'url', 30 | }, 31 | ], 32 | }, 33 | 34 | plugins: [ 35 | new webpack.DefinePlugin({ 36 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), 37 | }), 38 | ], 39 | } 40 | -------------------------------------------------------------------------------- /scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # based on: 4 | # - https://gist.github.com/domenic/ec8b0fc8ab45f39403dd 5 | # - http://www.steveklabnik.com/automatically_update_github_pages_with_travis_example/ 6 | 7 | set -o errexit -o nounset 8 | 9 | if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then 10 | echo "Skipping deploy: pull request." 11 | exit 1 12 | fi 13 | 14 | if [ "$TRAVIS_BRANCH" != "master" ]; then 15 | echo "Skipping deploy: branch not master." 16 | exit 1 17 | fi 18 | 19 | REV=$(git rev-parse --short HEAD) 20 | 21 | cd demo 22 | npm install 23 | NODE_ENV=production npm run build 24 | cd build 25 | 26 | git init 27 | git config user.name "CI" 28 | git config user.email "chromaci@chromakode.com" 29 | 30 | git add -A . 31 | git commit -m "Auto-build of ${REV}" 32 | git push -f "https://${GH_TOKEN}@${GH_REF}" HEAD:gh-pages > /dev/null 2>&1 33 | 34 | echo "✔ Deployed successfully." 35 | -------------------------------------------------------------------------------- /demo/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import DemoUI from './DemoUI' 4 | 5 | export function init(core) { 6 | if (process.env.NODE_ENV !== 'production') { 7 | require('webpack/hot/dev-server') 8 | require('webpack-dev-server/client?http://0.0.0.0:8080') 9 | } 10 | 11 | console.info(`core #${core.id} loaded`) // eslint-disable-line no-console 12 | 13 | core.ready({ 14 | attach(uidocument) { 15 | ReactDOM.render( 16 | , 19 | uidocument.getElementById('container') 20 | ) 21 | console.info(`core #${core.id} attached`) // eslint-disable-line no-console 22 | }, 23 | 24 | detach(uidocument) { 25 | ReactDOM.unmountComponentAtNode(uidocument.getElementById('container')) 26 | console.info(`core #${core.id} detached`) // eslint-disable-line no-console 27 | }, 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "isolated-core-demo", 3 | "description": "A guide and sample implementation for Isolated Core.", 4 | "version": "0.0.1", 5 | "license": "MIT", 6 | "author": { 7 | "name": "Max Goodman", 8 | "email": "c@chromakode.com" 9 | }, 10 | "scripts": { 11 | "start": "webpack-dev-server --hot --content-base build/", 12 | "build": "webpack -p --progress" 13 | }, 14 | "main": "lib/core.js", 15 | "dependencies": { 16 | "isolated-core": "../", 17 | "classnames": "~2.2.3", 18 | "react": "^0.14.0", 19 | "react-dom": "^0.14.0" 20 | }, 21 | "devDependencies": { 22 | "babel": "~6.3.26", 23 | "babel-plugin-transform-object-rest-spread": "~6.3.13", 24 | "babel-preset-es2015": "~6.3.13", 25 | "babel-preset-react": "~6.3.13", 26 | "babel-preset-react-hmre": "~1.0.1", 27 | "babel-loader": "~6.2.1", 28 | "url-loader": "~0.5.7", 29 | "less-loader": "~2.2.2", 30 | "css-loader": "~0.23.1", 31 | "autoprefixer-loader": "~3.2.0", 32 | "less": "~2.6.0", 33 | "webpack": "~1.12.0", 34 | "webpack-dev-server": "~1.14.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/fixtures/spyCore.js: -------------------------------------------------------------------------------- 1 | import 'core-js' 2 | import { coreInit, loadCore } from '../../src' 3 | import { cacheBust } from '../utils' 4 | 5 | const scriptURL = cacheBust('/base/test/fixtures/spyCore.js') 6 | 7 | coreInit({ 8 | scriptURL, 9 | run: core => { 10 | window.top.coreEvent('init', core) 11 | 12 | let nextCoreRef 13 | window.loadNextCore = function loadNextCore() { 14 | return loadCore({ scriptURL }).then(coreRef => nextCoreRef = coreRef) 15 | } 16 | 17 | window.launchNextCore = function launchNextCore() { 18 | nextCoreRef.launchCore() 19 | // When we call launchCore, the current iframe is removed from the DOM. 20 | // Subsequent statements should not be executed. 21 | window.top.coreEvent('xxx', core) 22 | } 23 | 24 | const handlers = { 25 | attach(uidocument) { 26 | window.top.coreEvent('attach', core, uidocument) 27 | }, 28 | 29 | detach(uidocument) { 30 | window.top.coreEvent('detach', core, uidocument) 31 | }, 32 | } 33 | 34 | window.top.coreEvent('ready', core, handlers) 35 | core.ready(handlers) 36 | }, 37 | }) 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Max Goodman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | 3 | module.exports = function(config) { 4 | config.set({ 5 | frameworks: ['mocha'], 6 | files: [ 7 | 'test/tests.webpack.js', 8 | { 9 | pattern: 'test/fixtures/*.js', 10 | included: false, 11 | served: true, 12 | watched: true, 13 | }, 14 | ], 15 | preprocessors: { 16 | 'test/tests.webpack.js': ['webpack', 'sourcemap'], 17 | 'test/fixtures/*.js': ['webpack'], 18 | }, 19 | webpack: { 20 | devtool: 'inline-source-map', 21 | module: { 22 | preLoaders: [ 23 | { 24 | test: /\.js$/, 25 | include: path.resolve('src'), 26 | loader: 'isparta', 27 | }, 28 | ], 29 | loaders: [ 30 | { 31 | test: /\.js$/, 32 | exclude: /node_modules/, 33 | loader: 'babel', 34 | }, 35 | ], 36 | }, 37 | }, 38 | webpackMiddleware: { 39 | noInfo: true, 40 | watchOptions: { 41 | poll: true, 42 | }, 43 | }, 44 | browsers: ['PhantomJS'], 45 | reporters: ['mocha', 'coverage'], 46 | coverageReporter: { 47 | reporters: [ 48 | { type: 'text' }, 49 | { type: 'lcovonly' }, 50 | ], 51 | }, 52 | port: 9876, 53 | singleRun: true, 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "isolated-core", 3 | "description": "Seamless client side updates using iframes.", 4 | "version": "0.1.1", 5 | "keywords": [ 6 | "update", 7 | "iframe" 8 | ], 9 | "license": "MIT", 10 | "author": { 11 | "name": "Max Goodman", 12 | "email": "c@chromakode.com" 13 | }, 14 | "scripts": { 15 | "lint": "eslint ./src ./test ./demo/src", 16 | "test": "karma start karma.conf.js", 17 | "test:sauce": "karma start karma-sauce.conf.js", 18 | "compile": "babel -d lib/ src/", 19 | "prepublish": "npm run compile" 20 | }, 21 | "main": "lib/index.js", 22 | "repository": "chromakode/isolated-core", 23 | "devDependencies": { 24 | "core-js": "~2.0.3", 25 | "babel": "~6.3.26", 26 | "babel-core": "~6.4.5", 27 | "babel-register": "~6.3.13", 28 | "babel-cli": "~6.3.17", 29 | "babel-plugin-transform-object-rest-spread": "~6.3.13", 30 | "babel-preset-es2015": "~6.3.13", 31 | "webpack": "~1.12.11", 32 | "mocha": "~2.3.4", 33 | "karma": "~0.13.19", 34 | "karma-mocha": "~0.2.1", 35 | "karma-mocha-reporter": "~1.1.5", 36 | "karma-phantomjs-launcher": "~0.2.3", 37 | "karma-sauce-launcher": "~0.3.0", 38 | "karma-coverage": "~0.5.3", 39 | "karma-webpack": "~1.7.0", 40 | "karma-sourcemap-loader": "~0.3.7", 41 | "babel-loader": "~6.2.1", 42 | "isparta-loader": "~2.0.0", 43 | "phantomjs": "~1.9.19", 44 | "expect": "~1.13.4", 45 | "eslint": "~1.10.3", 46 | "eslint-config-airbnb": "~2.0.0", 47 | "eslint-plugin-react": "~3.11.3", 48 | "babel-eslint": "~4.1.6", 49 | "escope": "~3.3.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/loadCore.js: -------------------------------------------------------------------------------- 1 | import getNextCoreId from './getNextCoreId' 2 | import { destroyCore, swapCore } from './operations' 3 | 4 | function coreInfo(context) { 5 | return { 6 | id: context._core.id, 7 | args: context._core.args, 8 | destroyCore: () => destroyCore(context), 9 | context, 10 | } 11 | } 12 | 13 | export default function loadCore(opts, uidocument = window._core.uidocument) { 14 | return new Promise((resolve, reject) => { 15 | const coreId = getNextCoreId(uidocument) 16 | const envEl = uidocument.createElement('iframe') 17 | envEl.setAttribute('data-coreid', coreId) 18 | uidocument.body.appendChild(envEl) 19 | 20 | const envDoc = envEl.contentDocument 21 | envDoc.open() 22 | 23 | const envContext = envEl.contentWindow 24 | const coreData = envContext._core = { 25 | id: coreId, 26 | uidocument: uidocument, 27 | args: opts.args, 28 | 29 | onReady(handlers) { 30 | coreData.attach = handlers.attach 31 | coreData.detach = handlers.detach 32 | resolve({ 33 | launchCore: () => swapCore(window, envContext, uidocument), 34 | ...coreInfo(envContext), 35 | }) 36 | }, 37 | 38 | onExecutionError(err) { 39 | reject({ 40 | type: 'js', 41 | err, 42 | ...coreInfo(envContext), 43 | }) 44 | }, 45 | 46 | onLoadError(src) { 47 | reject({ 48 | type: 'request', 49 | src, 50 | ...coreInfo(envContext), 51 | }) 52 | }, 53 | } 54 | 55 | const contentHTML = `` 56 | envDoc.write(contentHTML) 57 | envDoc.close() 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /test/coreInit-test.js: -------------------------------------------------------------------------------- 1 | import expect, { createSpy } from 'expect' 2 | import { cacheBust } from './utils' 3 | import { coreInit } from '../src/' 4 | 5 | describe('coreInit', () => { 6 | describe('called in a top-level script', () => { 7 | let coreEvent 8 | 9 | beforeEach(() => { 10 | coreEvent = window.top.coreEvent = createSpy() 11 | }) 12 | 13 | afterEach(() => { 14 | delete window.top.coreEvent 15 | delete document._lastCoreId 16 | }) 17 | 18 | it('loads and attaches the first core', () => { 19 | const fakeRun = createSpy() 20 | 21 | return coreInit({ 22 | scriptURL: cacheBust('/base/test/fixtures/spyCore.js'), 23 | run: fakeRun, 24 | }).then(coreRef => { 25 | expect(fakeRun).toNotHaveBeenCalled() 26 | expect(coreEvent.calls[0].arguments[0]).toEqual('init') 27 | expect(coreEvent.calls[1].arguments[0]).toEqual('ready') 28 | expect(coreEvent.calls[2].arguments[0]).toEqual('attach') 29 | expect(coreEvent.calls[2].arguments[2]).toEqual(document) 30 | expect(coreRef.context.frameElement.getAttribute('data-core-active')).toBe('') 31 | coreRef.destroyCore() 32 | }) 33 | }) 34 | }) 35 | 36 | describe('called in a core', () => { 37 | beforeEach(() => { 38 | window._core = { 39 | id: 0, 40 | args: undefined, 41 | onExecutionError: createSpy(), 42 | onReady: createSpy(), 43 | } 44 | }) 45 | 46 | afterEach(() => { 47 | delete window._core 48 | }) 49 | 50 | it('passes core data object to run function', () => { 51 | coreInit({ 52 | scriptURL: cacheBust('/base/test/fixtures/spyCore.js'), 53 | run: core => { 54 | expect(core.id).toBe(0) 55 | expect(core.args).toBe(undefined) 56 | expect(core.ready).toBeA('function') 57 | 58 | const mockHandlers = { attach() {}, detach() {} } 59 | core.ready(mockHandlers) 60 | expect(window._core.onReady).toHaveBeenCalledWith(mockHandlers) 61 | }, 62 | }) 63 | }) 64 | 65 | it('if run function throws, calls onExecutionError handler and re-throws', () => { 66 | const fakeError = new Error('oh noes!') 67 | try { 68 | coreInit({ 69 | scriptURL: cacheBust('/base/test/fixtures/spyCore.js'), 70 | run: () => { 71 | throw fakeError 72 | }, 73 | }) 74 | } catch (err) { 75 | expect(err).toBe(fakeError) 76 | } 77 | expect(window._core.onExecutionError).toHaveBeenCalledWith(fakeError) 78 | }) 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /test/operations-test.js: -------------------------------------------------------------------------------- 1 | import expect, { createSpy } from 'expect' 2 | import noop from '../src/utils/noop' 3 | import { 4 | destroyCore, 5 | detachCore, 6 | attachCore, 7 | swapCore, 8 | } from '../src/operations' 9 | 10 | function mockCallback() {} 11 | function mockWin() { 12 | return { 13 | _core: { 14 | attach: createSpy(), 15 | detach: createSpy(), 16 | onReady: mockCallback, 17 | onExecutionError: mockCallback, 18 | onLoadError: mockCallback, 19 | }, 20 | frameElement: { 21 | setAttribute: createSpy(), 22 | removeAttribute: createSpy(), 23 | parentNode: { 24 | removeChild: createSpy(), 25 | }, 26 | }, 27 | } 28 | } 29 | 30 | function expectDestroyed(fakeWin) { 31 | expect(fakeWin._core.onReady).toBe(noop) 32 | expect(fakeWin._core.onExecutionError).toBe(noop) 33 | expect(fakeWin._core.onLoadError).toBe(noop) 34 | expect(fakeWin.frameElement.parentNode.removeChild).toHaveBeenCalledWith(fakeWin.frameElement) 35 | } 36 | 37 | function expectDetached(fakeWin, fakeDoc) { 38 | expect(fakeWin.frameElement.removeAttribute).toHaveBeenCalledWith('data-core-active') 39 | expect(fakeWin._core.detach).toHaveBeenCalledWith(fakeDoc) 40 | } 41 | 42 | function expectAttached(fakeWin, fakeDoc) { 43 | expect(fakeWin.frameElement.setAttribute).toHaveBeenCalledWith('data-core-active', '') 44 | expect(fakeWin._core.attach).toHaveBeenCalledWith(fakeDoc) 45 | } 46 | 47 | describe('core operations', () => { 48 | describe('destroyCore', () => { 49 | it('sets callbacks to noops and removes frame element', () => { 50 | const fakeWin = mockWin() 51 | destroyCore(fakeWin) 52 | expectDestroyed(fakeWin) 53 | }) 54 | }) 55 | 56 | describe('detachCore', () => { 57 | it('removes data-core-active attribute and calls detach handler', () => { 58 | const fakeDoc = {} 59 | const fakeWin = mockWin() 60 | detachCore(fakeWin, fakeDoc) 61 | expectDetached(fakeWin, fakeDoc) 62 | }) 63 | }) 64 | 65 | describe('attachCore', () => { 66 | it('sets data-core-active attribute and calls attach handler', () => { 67 | const fakeDoc = {} 68 | const fakeWin = mockWin() 69 | attachCore(fakeWin, fakeDoc) 70 | expectAttached(fakeWin, fakeDoc) 71 | }) 72 | }) 73 | 74 | describe('swapCore', () => { 75 | it('detaches current context, attaches next context, and destroys current context', () => { 76 | const fakeDoc = {} 77 | const fakeWin = mockWin() 78 | const fakeNextWin = mockWin() 79 | swapCore(fakeWin, fakeNextWin, fakeDoc) 80 | expectDetached(fakeWin, fakeDoc) 81 | expectAttached(fakeNextWin, fakeDoc) 82 | expectDestroyed(fakeWin) 83 | }) 84 | }) 85 | }) 86 | -------------------------------------------------------------------------------- /karma-sauce.conf.js: -------------------------------------------------------------------------------- 1 | var baseConfig = require('./karma.conf') 2 | 3 | module.exports = function(config) { 4 | baseConfig(config) 5 | 6 | var customLaunchers = { 7 | sl_chrome_46: { 8 | base: 'SauceLabs', 9 | browserName: 'chrome', 10 | platform: 'Windows 7', 11 | version: '46.0', 12 | }, 13 | sl_chrome_47: { 14 | base: 'SauceLabs', 15 | browserName: 'chrome', 16 | platform: 'Windows 7', 17 | version: '47.0', 18 | }, 19 | sl_firefox_41: { 20 | base: 'SauceLabs', 21 | browserName: 'firefox', 22 | platform: 'Windows 7', 23 | version: '41.0', 24 | }, 25 | sl_firefox_42: { 26 | base: 'SauceLabs', 27 | browserName: 'firefox', 28 | platform: 'Windows 7', 29 | version: '42.0', 30 | }, 31 | sl_firefox_43: { 32 | base: 'SauceLabs', 33 | browserName: 'firefox', 34 | platform: 'Windows 7', 35 | version: '43.0', 36 | }, 37 | sl_ie_9: { 38 | base: 'SauceLabs', 39 | browserName: 'internet explorer', 40 | platform: 'Windows 7', 41 | version: '9.0', 42 | }, 43 | sl_ie_10: { 44 | base: 'SauceLabs', 45 | browserName: 'internet explorer', 46 | platform: 'Windows 7', 47 | version: '10.0', 48 | }, 49 | sl_ie_11: { 50 | base: 'SauceLabs', 51 | browserName: 'internet explorer', 52 | platform: 'Windows 10', 53 | version: '11.0', 54 | }, 55 | sl_edge: { 56 | base: 'SauceLabs', 57 | browserName: 'MicrosoftEdge', 58 | platform: 'Windows 10', 59 | version: '20.10240', 60 | }, 61 | sl_safari_6: { 62 | base: 'SauceLabs', 63 | browserName: 'safari', 64 | platform: 'OS X 10.8', 65 | version: '6.0', 66 | }, 67 | sl_safari_7: { 68 | base: 'SauceLabs', 69 | browserName: 'safari', 70 | platform: 'OS X 10.9', 71 | version: '7.0', 72 | }, 73 | sl_safari_8: { 74 | base: 'SauceLabs', 75 | browserName: 'safari', 76 | platform: 'OS X 10.10', 77 | version: '8.0', 78 | }, 79 | sl_safari_9: { 80 | base: 'SauceLabs', 81 | browserName: 'safari', 82 | platform: 'OS X 10.11', 83 | version: '9.0', 84 | }, 85 | sl_ios_safari_8: { 86 | base: 'SauceLabs', 87 | browserName: 'iphone', 88 | deviceName: 'iPhone 4s', 89 | platform: 'OS X 10.10', 90 | version: '8.0', 91 | }, 92 | sl_ios_safari_9: { 93 | base: 'SauceLabs', 94 | browserName: 'iphone', 95 | deviceName: 'iPhone 6', 96 | platform: 'OS X 10.10', 97 | version: '9.2', 98 | }, 99 | sl_android_4: { 100 | base: 'SauceLabs', 101 | browserName: 'android', 102 | deviceName: 'Android Emulator', 103 | platform: 'Linux', 104 | version: '4.0', 105 | }, 106 | sl_android_5: { 107 | base: 'SauceLabs', 108 | browserName: 'android', 109 | deviceName: 'Android Emulator', 110 | platform: 'Linux', 111 | version: '5.1', 112 | }, 113 | } 114 | 115 | config.set({ 116 | sauceLabs: { 117 | testName: 'Isolated Core Automated Tests', 118 | }, 119 | captureTimeout: 3 * 60000, 120 | customLaunchers: customLaunchers, 121 | browsers: Object.keys(customLaunchers), 122 | reporters: ['mocha', 'coverage', 'saucelabs'], 123 | coverageReporter: { 124 | reporters: [ 125 | { type: 'lcovonly' }, 126 | ], 127 | }, 128 | }) 129 | } 130 | -------------------------------------------------------------------------------- /demo/src/style.less: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Lato'; 3 | src: url('../static/Lato-Regular.woff') format('woff'); 4 | font-weight: normal; 5 | font-style: normal; 6 | } 7 | 8 | @font-face { 9 | font-family: 'Lato'; 10 | src: url('../static/Lato-Bold.woff') format('woff'); 11 | font-weight: bold; 12 | font-style: normal; 13 | } 14 | 15 | @font-face { 16 | font-family: 'Lato'; 17 | src: url('../static/Lato-Black.woff') format('woff'); 18 | font-weight: 900; 19 | font-style: normal; 20 | } 21 | 22 | @green: hsla(120, 35%, 64%, 1); 23 | 24 | html { 25 | font-size: 14px; 26 | } 27 | 28 | body { 29 | font-family: Lato; 30 | margin: .5rem; 31 | box-sizing: border-box; 32 | } 33 | 34 | .inner, .content { 35 | max-width: 800px; 36 | margin: 0 auto; 37 | } 38 | 39 | .title { 40 | background: @green; 41 | padding: 7.5vw 0; 42 | 43 | a { 44 | color: black; 45 | text-decoration: none; 46 | } 47 | 48 | h1 { 49 | font-size: 3rem; 50 | font-weight: 900; 51 | text-align: center; 52 | 53 | .box { 54 | padding: .25em .5em; 55 | border: .18em solid black; 56 | background: white; 57 | white-space: nowrap; 58 | animation-name: box; 59 | animation-iteration-count: 1; 60 | animation-timing-function: ease; 61 | animation-duration: 1s; 62 | @keyframes box { 63 | 0% { 64 | opacity: 0; 65 | } 66 | 25% { 67 | padding: .75em 2em; 68 | border-color: white; 69 | opacity: 0; 70 | } 71 | 100% { 72 | padding: .25em .5em; 73 | border-color: black; 74 | opacity: 1; 75 | } 76 | } 77 | } 78 | } 79 | } 80 | 81 | .description { 82 | padding: 2.25rem 2rem; 83 | background: mix(@green, black, 35%); 84 | 85 | h2 { 86 | font-size: 1.5rem; 87 | text-align: center; 88 | color: mix(@green, white, 25%); 89 | margin: 0; 90 | } 91 | } 92 | 93 | .content { 94 | padding: 3rem 2rem; 95 | font-size: 1.25rem; 96 | 97 | h3 { 98 | font-size: 1.75rem; 99 | } 100 | 101 | .scroll-notice { 102 | display: none; 103 | } 104 | 105 | .steps { 106 | li { 107 | margin-bottom: 2.5rem; 108 | opacity: .25; 109 | 110 | &.current { 111 | opacity: 1; 112 | } 113 | 114 | &.done button { 115 | color: white; 116 | font-weight: bold; 117 | background: darken(@green, 32%); 118 | border-bottom-color: darken(@green, 40%); 119 | } 120 | } 121 | 122 | aside { 123 | font-size: .8em; 124 | opacity: .65; 125 | } 126 | } 127 | 128 | hr { 129 | border: none; 130 | border-bottom: 1px solid #bbb; 131 | margin: 3rem 0; 132 | } 133 | 134 | .diagram-step { 135 | display: flex; 136 | align-items: center; 137 | margin: 2rem 0; 138 | 139 | p, .diagram { 140 | flex: 1; 141 | } 142 | 143 | p { 144 | margin: 0; 145 | } 146 | 147 | .diagram { 148 | margin-left: 2rem; 149 | } 150 | } 151 | 152 | .box { 153 | display: flex; 154 | flex-direction: column; 155 | 156 | .label { 157 | align-self: flex-start; 158 | font-size: .85rem; 159 | padding: 2px 4px; 160 | margin-bottom: -2px; 161 | background: black; 162 | color: white; 163 | z-index: 1; 164 | } 165 | 166 | .inside { 167 | display: flex; 168 | font-size: 1rem; 169 | flex-direction: column; 170 | border: 2px solid black; 171 | padding: .5rem; 172 | background: #eee; 173 | 174 | & > * { 175 | margin: .25rem 0; 176 | 177 | &:first-child { 178 | margin-top: 0; 179 | } 180 | 181 | &:last-child { 182 | margin-bottom: 0; 183 | } 184 | } 185 | } 186 | 187 | .minor { 188 | opacity: .5; 189 | } 190 | 191 | &.core0 { 192 | @color: spin(darken(@green, 15%), 100); 193 | 194 | .label { 195 | background: @color; 196 | } 197 | 198 | .inside { 199 | border-color: @color; 200 | } 201 | } 202 | 203 | &.core1 { 204 | @color: spin(darken(@green, 15%), 265); 205 | 206 | .label { 207 | background: @color; 208 | } 209 | 210 | .inside { 211 | border-color: @color; 212 | } 213 | } 214 | } 215 | 216 | .badges { 217 | text-align: center; 218 | } 219 | 220 | .sauce-matrix { 221 | max-width: 100%; 222 | } 223 | } 224 | 225 | .core-status { 226 | margin: 0 .5em; 227 | transition: 228 | opacity .25s ease, 229 | color .25s ease; 230 | 231 | &.loading { 232 | color: black; 233 | opacity: .5; 234 | } 235 | 236 | &.ready { 237 | color: darken(@green, 20%); 238 | font-weight: bold; 239 | } 240 | 241 | &.error { 242 | color: darken(spin(@green, 240), 20%); 243 | font-weight: bold; 244 | } 245 | 246 | &.none { 247 | opacity: 0; 248 | } 249 | } 250 | 251 | button { 252 | padding: .5rem 1rem; 253 | font-size: inherit; 254 | text-align: left; 255 | vertical-align: middle; 256 | background: #e5e5e5; 257 | border: none; 258 | border-bottom: 3px solid #ccc; 259 | border-radius: 3px; 260 | 261 | &:focus { 262 | outline: none; 263 | } 264 | 265 | &:not(:disabled):active { 266 | margin-top: 3px; 267 | border-bottom-width: 0; 268 | } 269 | } 270 | 271 | footer { 272 | margin-bottom: 1.5rem; 273 | text-align: center; 274 | opacity: .5; 275 | 276 | .logo { 277 | vertical-align: middle; 278 | margin-right: .5rem; 279 | width: 1.25rem; 280 | } 281 | 282 | a { 283 | color: black; 284 | } 285 | } 286 | 287 | .ribbon { 288 | position: fixed; 289 | top: 0; 290 | right: 0; 291 | border: 0; 292 | z-index: 100; 293 | } 294 | 295 | @media (max-width: 420px) { 296 | html { 297 | font-size: 13px; 298 | } 299 | 300 | body { 301 | margin: .25rem; 302 | } 303 | 304 | .description, .content { 305 | padding: 1.25rem; 306 | } 307 | 308 | .title h1 { 309 | font-size: 2.5rem; 310 | } 311 | 312 | .description h2 { 313 | font-size: 1.35rem; 314 | } 315 | 316 | .content { 317 | h3 { 318 | font-size: 1.45rem; 319 | } 320 | 321 | .scroll-notice { 322 | display: inline; 323 | opacity: .5; 324 | } 325 | 326 | ol { 327 | padding-left: 2rem; 328 | } 329 | 330 | .diagram-step { 331 | flex-direction: column; 332 | align-items: stretch; 333 | 334 | .diagram { 335 | margin-left: 0; 336 | margin-top: 1rem; 337 | } 338 | } 339 | } 340 | 341 | .core-status { 342 | display: inline-block; 343 | margin: .5rem 0; 344 | 345 | &.none { 346 | display: none; 347 | } 348 | } 349 | 350 | .ribbon { 351 | display: none; 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /test/loadCore-test.js: -------------------------------------------------------------------------------- 1 | import expect, { createSpy, spyOn } from 'expect' 2 | import { cacheBust } from './utils' 3 | import { coreInit, loadCore } from '../src/' 4 | 5 | function checkCoreInfo(coreInfo) { 6 | expect(coreInfo.id).toBe(0) 7 | expect(coreInfo.args).toBe(undefined) 8 | expect(coreInfo.destroyCore).toBeA('function') 9 | const envEl = document.querySelector(`[data-coreid="${coreInfo.id}"]`) 10 | expect(coreInfo.context).toBe(envEl.contentWindow) 11 | } 12 | 13 | describe('loadCore', () => { 14 | let coreEvent 15 | 16 | beforeEach(() => { 17 | window._core = { 18 | uidocument: document, 19 | } 20 | coreEvent = window.top.coreEvent = createSpy() 21 | }) 22 | 23 | afterEach(() => { 24 | delete window.top.coreEvent 25 | delete document._lastCoreId 26 | expect(document.querySelector('[data-coreid]')).toBe(null, 'Expected core to be destroyed after test') 27 | }) 28 | 29 | it('creates an iframe containing script with data-coreid attribute set, and removes when destroyed', () => { 30 | return loadCore({ 31 | scriptURL: cacheBust('/base/test/fixtures/spyCore.js'), 32 | }).then(coreRef => { 33 | const envEl = coreRef.context.frameElement 34 | expect(envEl.getAttribute('data-coreid')).toBe('0') 35 | expect(envEl.parentNode).toBe(document.body) 36 | expect(envEl.contentDocument.doctype.name).toBe('html') 37 | 38 | const scriptEl = envEl.contentDocument.getElementsByTagName('script')[0] 39 | expect(scriptEl).toExist() 40 | expect(scriptEl.src).toInclude('/base/test/fixtures/spyCore.js') 41 | expect(scriptEl.getAttribute('onerror')).toBe('_core.onLoadError(this.src)') 42 | 43 | coreRef.destroyCore() 44 | expect(envEl.parentNode).toBe(null) 45 | }) 46 | }) 47 | 48 | it('populates iframe context with _core data', () => { 49 | const args = { it: 'works' } 50 | return loadCore({ 51 | scriptURL: cacheBust('/base/test/fixtures/spyCore.js'), 52 | args, 53 | }).then(coreRef => { 54 | const coreData = coreRef.context._core 55 | expect(coreData.id).toBe(0) 56 | expect(coreData.uidocument).toBe(document) 57 | expect(coreData.args).toBe(args) 58 | expect(coreData.onReady).toBeA('function') 59 | expect(coreData.onExecutionError).toBeA('function') 60 | expect(coreData.onLoadError).toBeA('function') 61 | coreRef.destroyCore() 62 | }) 63 | }) 64 | 65 | it('when a core becomes ready, saves handlers to coreData and resolves with a coreInfo object containing a launchCore method', () => { 66 | return loadCore({ 67 | scriptURL: cacheBust('/base/test/fixtures/spyCore.js'), 68 | }).then(coreRef => { 69 | expect(coreEvent.calls[1].arguments[0]).toBe('ready') 70 | const handlers = coreEvent.calls[1].arguments[2] 71 | 72 | const coreData = coreRef.context._core 73 | expect(coreData.attach).toBe(handlers.attach) 74 | expect(coreData.detach).toBe(handlers.detach) 75 | 76 | checkCoreInfo(coreRef) 77 | expect(coreRef.launchCore).toBeA('function') 78 | const operations = require('../src/operations') 79 | const swapCoreSpy = spyOn(operations, 'swapCore') 80 | coreRef.launchCore() 81 | expect(swapCoreSpy).toHaveBeenCalledWith(window, coreRef.context, document) 82 | swapCoreSpy.restore() 83 | 84 | coreRef.destroyCore() 85 | }) 86 | }) 87 | 88 | it('launchCore method swaps with current core, sets data-core-active attribute, and destroys the previous core', done => { 89 | delete window._core 90 | 91 | function expectCoreEvent(index, name, coreId, thirdArgument) { 92 | expect(coreEvent.calls[index].arguments[0]).toBe(name) 93 | expect(coreEvent.calls[index].arguments[1].id).toBe(coreId) 94 | if (thirdArgument) { 95 | expect(coreEvent.calls[index].arguments[2]).toBe(thirdArgument) 96 | } 97 | } 98 | 99 | let hoistedFirstEnvEl 100 | let hoistedSecondEnvEl 101 | let hoistedSecondCoreRef 102 | 103 | coreInit({ 104 | scriptURL: cacheBust('/base/test/fixtures/spyCore.js'), 105 | }).then(firstCoreRef => { 106 | expect(firstCoreRef.id).toBe(0) 107 | const firstEnvEl = hoistedFirstEnvEl = firstCoreRef.context.frameElement 108 | expect(firstEnvEl.parentNode).toBe(document.body) 109 | expect(firstEnvEl.getAttribute('data-coreid')).toBe('0') 110 | expect(firstEnvEl.getAttribute('data-core-active')).toBe('') 111 | expectCoreEvent(0, 'init', 0) 112 | expectCoreEvent(1, 'ready', 0) 113 | expectCoreEvent(2, 'attach', 0, document) 114 | 115 | firstCoreRef.context.loadNextCore().then(secondCoreRef => { 116 | hoistedSecondCoreRef = secondCoreRef 117 | 118 | expect(secondCoreRef.id).toBe(1) 119 | const secondEnvEl = hoistedSecondEnvEl = secondCoreRef.context.frameElement 120 | expect(secondEnvEl.getAttribute('data-coreid')).toBe('1') 121 | expect(secondEnvEl.getAttribute('data-core-active')).toBe(null) 122 | expectCoreEvent(3, 'init', 1) 123 | expectCoreEvent(4, 'ready', 1) 124 | 125 | firstCoreRef.context.launchNextCore() 126 | }) 127 | }) 128 | 129 | // Since the execution of the first core will stop after launchCore is 130 | // called, we use a timeout in the current context to continue testing. 131 | function waitForSecondCore() { 132 | if (coreEvent.calls.length < 6) { 133 | setTimeout(waitForSecondCore, 0) 134 | return 135 | } 136 | 137 | expectCoreEvent(5, 'detach', 0, document) 138 | expectCoreEvent(6, 'attach', 1, document) 139 | expect(coreEvent.calls.length).toBe(7) 140 | 141 | expect(hoistedFirstEnvEl.getAttribute('data-core-active')).toBe(null) 142 | expect(hoistedFirstEnvEl.parentNode).toBe(null) 143 | expect(hoistedSecondEnvEl.getAttribute('data-core-active')).toBe('') 144 | 145 | hoistedSecondCoreRef.destroyCore() 146 | done() 147 | } 148 | waitForSecondCore() 149 | }) 150 | 151 | it('rejects if a script throws an exception with an errInfo object, and removes when destroyed', () => { 152 | return loadCore({ 153 | scriptURL: cacheBust('/base/test/fixtures/errorCore.js'), 154 | }).then( 155 | () => { 156 | throw new Error('Expected promise to be rejected') 157 | }, 158 | 159 | errInfo => { 160 | checkCoreInfo(errInfo) 161 | 162 | expect(errInfo.type).toBe('js') 163 | expect(errInfo.err).toExist() 164 | expect(errInfo.err.message).toBe('oh noes!') 165 | 166 | const envEl = errInfo.context.frameElement 167 | expect(envEl.parentNode).toBe(document.body) 168 | errInfo.destroyCore() 169 | expect(envEl.parentNode).toBe(null) 170 | } 171 | ) 172 | }) 173 | 174 | it('rejects if a script fails to load with an errInfo object, and removes when destroyed', () => { 175 | return loadCore({ 176 | scriptURL: cacheBust('/base/test/fixtures/nonexistent.js'), 177 | }).then( 178 | () => { 179 | throw new Error('Expected promise to be rejected') 180 | }, 181 | 182 | errInfo => { 183 | checkCoreInfo(errInfo) 184 | 185 | expect(errInfo.type).toBe('request') 186 | expect(errInfo.src).toInclude('/base/test/fixtures/nonexistent.js') 187 | 188 | const envEl = errInfo.context.frameElement 189 | expect(envEl.parentNode).toBe(document.body) 190 | errInfo.destroyCore() 191 | expect(envEl.parentNode).toBe(null) 192 | } 193 | ) 194 | }) 195 | }) 196 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Isolated Core 2 | 3 | [![Build Status](https://img.shields.io/travis/chromakode/isolated-core/master.svg?style=flat-square)](https://travis-ci.org/chromakode/isolated-core) 4 | [![Coverage Status](https://img.shields.io/coveralls/chromakode/isolated-core/master.svg?style=flat-square)](https://coveralls.io/github/chromakode/isolated-core?branch=master) 5 | [![npm](https://img.shields.io/npm/v/isolated-core.svg?style=flat-square)](https://www.npmjs.com/package/isolated-core) 6 | [![npm](https://img.shields.io/npm/l/isolated-core.svg?style=flat-square)](https://github.com/chromakode/isolated-core/blob/master/LICENSE) 7 | 8 | A library for seamless in-page cold updates using iframes. 9 | 10 | [:zap: **DEMO**](http://chromakode.github.io/isolated-core/) 11 | 12 | 13 | ## Introduction 14 | 15 | In long running web apps, such as chat clients or music players, users leave pages open for weeks. It's useful to push code updates to existing clients, but in-page updates must be extremely fast and reliable to not become disruptive to the user experience. 16 | 17 | With Isolated Core, your client-side JS (the "core") is contained within an `