├── .gitignore ├── README.md ├── TODO ├── index.js ├── package.json └── test ├── fixture └── script.js ├── index.js ├── interactive.js └── specs ├── document.spec.js └── window.spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | node_modules 4 | feather 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-as-browser 2 | 3 | **Create a browser-like environment within a Node.js context** 4 | 5 | ## Install 6 | ``` 7 | $ npm install node-as-browser 8 | ``` 9 | 10 | ## Use 11 | ```js 12 | var nodeAsBrowser = require('node-as-browser'); 13 | 14 | nodeAsBrowser.init(global); 15 | 16 | console.log(navigator.userAgent); 17 | ``` 18 | 19 | ## Scope 20 | ```js 21 | var nodeAsBrowser = require('node-as-browser'); 22 | 23 | var fakeWindow = {}; 24 | nodeAsBrowser.init(fakeWindow); 25 | 26 | console.log(fakeWindow.navigator.userAgent); 27 | ``` 28 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | // We can replicate script.onload behavior using the approach below 5 | // but apparently this is no longer needed since jsdom version upgrade? 6 | // 7 | // function mockDomNodeInserted (win) { 8 | // var nodeImpl = require('jsdom/lib/jsdom/living/nodes/Node-impl.js').implementation.prototype; 9 | // var oldInsertBefore = nodeImpl.insertBefore; 10 | // var NODE_COUNT = 0; 11 | // 12 | // var newInsertBefore = function (newElement, refElement) { 13 | // NODE_COUNT++; 14 | // var nodeId = 'INSERTED' + NODE_COUNT; 15 | // 16 | // if (typeof newElement.onload === 'function') { 17 | // win.document.addEventListener('DOMNodeInserted', function (ev) { 18 | // if (ev.detail.id === nodeId) { 19 | // newElement.onload.call(newElement); 20 | // } 21 | // }); 22 | // } 23 | // 24 | // var event = new CustomEvent('DOMNodeInserted', { detail: { id: nodeId, type: newElement.nodeName.toLowerCase() } }); 25 | // win.document.dispatchEvent(event); 26 | // oldInsertBefore.apply(this, arguments); 27 | // }; 28 | // 29 | // nodeImpl.insertBefore = newInsertBefore; 30 | // } 31 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | var fetch = require('node-fetch'); 3 | var JSDOM = require("jsdom").JSDOM; 4 | var usertiming = require('usertiming'); 5 | 6 | function newJSDOM (html, options) { 7 | return new JSDOM(html || '', options); 8 | } 9 | 10 | function enhanceToString () { 11 | "use strict"; // causes fn.call(null) to return [object Null] instead of [object global] 12 | 13 | var oldToString = Object.prototype.toString; 14 | Object.prototype.toString = function () { 15 | var ts = oldToString.call(this); 16 | if (ts === '[object Object]') { 17 | if (this.nodeType) { 18 | return this.toString(); 19 | } 20 | return ts; 21 | } 22 | return ts; 23 | }; 24 | } 25 | 26 | function init (options) { 27 | options = options || {}; 28 | var context = options.context || global; 29 | 30 | if (options.url) { 31 | options.url = options.url; 32 | } 33 | 34 | options.runScripts = 'dangerously'; 35 | options.resources = "usable"; 36 | 37 | this._dom = newJSDOM(options.html, options); 38 | var win = this._dom.window; 39 | 40 | // allow child windows 41 | win.open = function (url) { 42 | return newJSDOM(null, { url: url }).window; 43 | }; 44 | 45 | win.navigator.sendBeacon = function () {}; 46 | 47 | win.document.hasFocus = function () { return true; }; 48 | 49 | // Adds LocalStorage 50 | // This should only be needed until jsdom adds this feature 51 | // https://github.com/tmpvar/jsdom/issues/1137 52 | win.localStorage = win.sessionStorage = (function () { 53 | return { 54 | getItem: function (key) { 55 | return this[key] || null; 56 | }, 57 | setItem: function (key, value) { 58 | this[key] = value; 59 | }, 60 | removeItem: function (key) { 61 | this[key] = null; 62 | }, 63 | }; 64 | })(); 65 | 66 | // https://github.com/tmpvar/jsdom/issues/1510 67 | win.performance = usertiming; 68 | win.performance.navigation = { 69 | redirectCount: 0, 70 | type: 1 71 | }; 72 | 73 | win.fetch = fetch; 74 | 75 | win.screen.availTop = 0; 76 | win.screen.availLeft = 0; 77 | win.screen.width = win.innerWidth; 78 | win.screen.height = win.innerHeight; 79 | win.screen.colorDepth = 32; 80 | win.screen.pixelDepth = 32; 81 | 82 | context.window = win; 83 | 84 | for (var x in win) { 85 | if (typeof context[x] === 'undefined') { 86 | try { 87 | context[x] = win[x]; 88 | } catch (e) { 89 | // not sure what to do if this fails 90 | } 91 | } 92 | } 93 | 94 | for (var y in win._core) { 95 | if (typeof context[y] === 'undefined') { 96 | context[y] = win[y]; 97 | } 98 | } 99 | 100 | context.Image = win.Image; 101 | 102 | // needs to be overriden here specifically, but not sure why 103 | // somehow allows external scripts to run in same context 104 | context.document = win.document; 105 | 106 | // Enhance toString output for DOM nodes 107 | enhanceToString(); 108 | 109 | return context; 110 | } 111 | 112 | function reconfigure (options) { 113 | this._dom.reconfigure(options); 114 | } 115 | 116 | function NodeAsBrowser () { 117 | this._dom = null; 118 | this.init = init; 119 | this.reconfigure = reconfigure; 120 | } 121 | 122 | module.exports = new NodeAsBrowser(); 123 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-as-browser", 3 | "version": "1.4.0", 4 | "description": "Create a browser-like environment within a Node.js context", 5 | "main": "index.js", 6 | "scripts": { 7 | "repl": "node ./test/interactive.js && node", 8 | "test": "node test" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/seebigs/node-as-browser.git" 13 | }, 14 | "author": "Chris Bigelow ", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/seebigs/node-as-browser/issues" 18 | }, 19 | "dependencies": { 20 | "jsdom": "^11.1.0", 21 | "node-fetch": "^1.6.3", 22 | "usertiming": "^0.1.8" 23 | }, 24 | "devDependencies": { 25 | "feather-test-browser": "^1.2.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/fixture/script.js: -------------------------------------------------------------------------------- 1 | document.body.innerHTML += 'LOADED'; 2 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Run automated validations 3 | */ 4 | 5 | var FeatherTestBrowser = require('feather-test-browser'); 6 | global.nodeAsBrowser = require('../index.js'); 7 | 8 | nodeAsBrowser.init({ 9 | html: '
' 10 | }); 11 | 12 | var mockTest = new FeatherTestBrowser({ 13 | specs: './specs' 14 | }); 15 | 16 | mockTest.run(); 17 | -------------------------------------------------------------------------------- /test/interactive.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Open a command line prompt to evaluate expressions in the node-as-browser environment 3 | */ 4 | 5 | var nodeAsBrowser = require('../index.js'); 6 | var repl = require("repl"); 7 | 8 | nodeAsBrowser.init(global); 9 | 10 | var replServer = repl.start({ 11 | useGlobal: true 12 | }); 13 | 14 | console.log('You are now in the node-as-browser interactive environment...\n'); 15 | -------------------------------------------------------------------------------- /test/specs/document.spec.js: -------------------------------------------------------------------------------- 1 | 2 | describe('document.cookie', function (expect) { 3 | expect(typeof document.cookie).toBe('string'); 4 | }); 5 | 6 | describe('document.appendChild', function (expect) { 7 | expect(typeof document.appendChild).toBe('function'); 8 | }); 9 | 10 | describe('document.body', function (expect) { 11 | expect(document.body.toString()).toBe('[object HTMLBodyElement]'); 12 | }); 13 | 14 | describe('document.documentElement.childNodes', function (expect) { 15 | expect(document.documentElement.childNodes.length).toBe(2); 16 | }); 17 | 18 | describe('document.getElementById', function (expect) { 19 | expect(document.getElementById('gremlins').parentNode).toBe(document.body); 20 | }); 21 | 22 | describe('document.getElementsByClassName', function (expect) { 23 | expect(document.getElementsByClassName('mogwai').length).toBe(2); 24 | }); 25 | 26 | describe('document.hasFocus', function (expect) { 27 | expect(document.hasFocus()).toBe(true); 28 | }); 29 | 30 | describe('document.childNodes[0].nodeName', function (expect) { 31 | expect(document.body.childNodes[0].nodeName).toBe('DIV'); 32 | }); 33 | 34 | describe('document.querySelectorAll', function (expect) { 35 | expect(document.querySelectorAll('.mogwai').length).toBe(2); 36 | }); 37 | 38 | describe('document.readyState', function (expect) { 39 | expect(document.readyState).toBe('complete'); 40 | }); 41 | 42 | describe('document.body.getAttribute', function (expect) { 43 | expect(document.body.getAttribute('data-action')).toBe('bhave'); 44 | }); 45 | 46 | describe('document.body.getBoundingClientRect', function (expect) { 47 | expect(document.body.getBoundingClientRect().top).toBe(0); 48 | }); 49 | 50 | describe('document.body.style', function (expect) { 51 | expect(document.body.style.color).toBe('red'); 52 | }); 53 | 54 | describe('document.appendChild script execution and onload', function (expect, done) { 55 | var s = document.createElement('script'); 56 | s.src = __dirname + '/../fixture/script.js'; 57 | s.onload = function () { 58 | expect(this.nodeName).toBe('SCRIPT'); 59 | expect(document.body.innerHTML.indexOf('LOADED')).not.toBe(-1); 60 | done(); 61 | }; 62 | document.head.appendChild(s); 63 | }); 64 | -------------------------------------------------------------------------------- /test/specs/window.spec.js: -------------------------------------------------------------------------------- 1 | 2 | describe('navigator', function (expect) { 3 | expect(navigator.userAgent).toContain(' jsdom/'); 4 | expect(typeof navigator.sendBeacon).toBe('function'); 5 | }); 6 | 7 | describe('HTMLElement', function (expect) { 8 | expect(HTMLElement.toString()).toContain('HTMLElement'); 9 | }); 10 | 11 | describe('CSSRule', function (expect) { 12 | expect(CSSRule.toString()).toContain('CSSRule'); 13 | }); 14 | 15 | describe('JSON', function (expect) { 16 | expect(JSON.stringify({ abc: 123 })).toBe('{"abc":123}'); 17 | }); 18 | 19 | describe('XMLHttpRequest', function (expect) { 20 | expect(new XMLHttpRequest().toString()).toBe('[object XMLHttpRequest]'); 21 | }); 22 | 23 | describe('addEventListener', function (expect) { 24 | expect(typeof addEventListener).toBe('function'); 25 | }); 26 | 27 | describe('open', function (expect) { 28 | expect(typeof open('http://url').close).toBe('function'); 29 | }); 30 | 31 | describe('blur', function (expect) { 32 | expect(typeof blur).toBe('function'); 33 | }); 34 | 35 | describe('localStorage', function (expect) { 36 | expect(typeof localStorage.setItem).toBe('function'); 37 | }); 38 | 39 | describe('self', function (expect) { 40 | var winHasSelf = self === window; 41 | expect(winHasSelf).toBe(true); 42 | }); 43 | 44 | describe('innerWidth', function (expect) { 45 | expect(innerWidth > 1).toBe(true); 46 | }); 47 | 48 | describe('fetch', function (expect) { 49 | expect(typeof fetch).toBe('function'); 50 | }); 51 | 52 | describe('Promise', function (expect) { 53 | expect(typeof Promise).toBe('function'); 54 | }); 55 | 56 | describe('Image', function (expect) { 57 | expect(typeof Image).toBe('function'); 58 | }); 59 | 60 | describe('screen', function (expect) { 61 | expect(typeof screen.colorDepth).toBe('number', 'screen.colorDepth'); 62 | }); 63 | 64 | describe('performance', function (expect) { 65 | expect(typeof performance).toBe('object'); 66 | expect(typeof performance.getEntries).toBe('function', 'getEntries'); 67 | expect(typeof performance.mark).toBe('function', 'mark'); 68 | expect(typeof performance.measure).toBe('function', 'measure'); 69 | expect(typeof performance.now).toBe('function', 'now'); 70 | expect(typeof performance.navigation).toBe('object', 'navigation'); 71 | }); 72 | 73 | describe('location', function (expect) { 74 | var url = 'https://www.example.com:8888/page.html?one=1&two=2#anchor'; 75 | nodeAsBrowser.reconfigure({ url: url }); 76 | expect(location.href).toBe(url); 77 | expect(location.protocol).toBe('https:'); 78 | expect(location.hostname).toBe('www.example.com'); 79 | expect(location.port).toBe('8888'); 80 | expect(location.pathname).toBe('/page.html'); 81 | expect(location.search).toBe('?one=1&two=2'); 82 | expect(location.hash).toBe('#anchor'); 83 | nodeAsBrowser.reconfigure({ url: 'about:blank' }); 84 | }); 85 | --------------------------------------------------------------------------------