Skip to content

Commit

Permalink
Convert to typescript and directly use jsdom
Browse files Browse the repository at this point in the history
I was able to contribute the navigation features directly back to
jsdom, which while not currently finished, will definitely lead to a
more stable implementation in the long run.
  • Loading branch information
ForbesLindesay committed Jul 8, 2017
1 parent 94eb45e commit bb21fe7
Show file tree
Hide file tree
Showing 27 changed files with 1,398 additions and 4,659 deletions.
4,187 changes: 0 additions & 4,187 deletions package-lock.json

This file was deleted.

27 changes: 21 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,24 +1,37 @@
{
"name": "taxi-rank",
"version": "1.2.4",
"version": "1.2.5",
"main": "lib/index.js",
"description": "A JSDom based Selenium Webdriver API",
"keywords": [],
"files": [
"cli.js",
"lib/"
"lib/",
"types/"
],
"bin": {
"taxi-rank": "./cli.js"
},
"dependencies": {
"@forbeslindesay/jsdom": "^11.1.8",
"@types/body-parser": "^1.16.4",
"@types/express": "^4.0.36",
"@types/jsdom": "^11.0.1",
"@types/node": "^8.0.3",
"@types/parse5": "^3.0.0",
"@types/tough-cookie": "^2.3.0",
"@types/uuid": "^3.0.0",
"babel-runtime": "^6.18.0",
"body-parser": "^1.17.2",
"express": "^4.15.3",
"jsdom": "^11.0.0",
"minimist": "^1.2.0",
"node-storage-shim": "^1.0.1",
"parse5": "^3.0.2",
"throat": "^4.0.0",
"uuid": "^3.0.1",
"zombie": "^5.0.7"
"webidl-conversions": "^4.0.1",
"whatwg-url": "^5.0.0"
},
"devDependencies": {
"babel-cli": "*",
Expand All @@ -31,12 +44,14 @@
"eslint": "*",
"eslint-config-forbeslindesay": "*",
"jest": "*",
"sync-request": "^4.0.3"
"sync-request": "^4.0.3",
"typescript": "^2.4.0"
},
"scripts": {
"prepublish": "npm run build",
"build": "babel src --out-dir lib",
"build": "rimraf lib && tsc",
"lint": "eslint src",
"pretest": "npm run build",
"test": "babel-node test && npm run lint"
},
"repository": {
Expand All @@ -48,4 +63,4 @@
"url": "http://github.com/ForbesLindesay"
},
"license": "MIT"
}
}
89 changes: 89 additions & 0 deletions src/Browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import {CookieJar, DOMWindow} from '@forbeslindesay/jsdom';
import BrowserOptions from './BrowserOptions';
import eventChannel from './eventChannel';
import Tab from './Tab';
import TabOptions from './TabOptions';
import StorageShim = require('node-storage-shim');

class Browser {
private _currentTab: Tab | null = null;
private _nextTabID: number = 0;
private readonly _tabsByID: Map<string, Tab> = new Map();
private readonly _tabsByName: Map<string, Tab> = new Map();
private readonly _localStorage: Map<string, StorageShim> = new Map();

public readonly runScripts: 'dangerously' | 'outside-only';
public readonly cookies = new CookieJar();

public readonly onInactive = eventChannel<Tab>();
public readonly onActive = eventChannel<Tab>();
public readonly onBeforeParse = eventChannel<DOMWindow>();
constructor(options: BrowserOptions) {
this.runScripts = options.runScripts;
this.open({
url: 'about:blank',
});
}

findTab(nameOrID: string): Tab | null {
const tabByName = this._tabsByName.get(nameOrID);
if (tabByName) {
return tabByName;
}
const tabByID = this._tabsByID.get(nameOrID);
if (tabByID) {
return tabByID;
}
return null;
}

/**
* Opens and returns a tab. If an open window by the same name already exists,
* opens this window in the same tab. Omit name or use '_blank' to always open
* a new tab.
*/
open(options: TabOptions): Tab {
const named = options.name && this.findTab(options.name);
if (named) {
if (this._currentTab) {
this.onInactive.emit(this._currentTab);
}
named.setLocation(options);
this._currentTab = named;
this.onActive.emit(named);
return named;
}
const id = '' + (this._nextTabID++);
const tab = new Tab(this, options, id, this._localStorage, this._onTabClose);
this._tabsByID.set(id, tab);
if (options.name) {
this._tabsByName.set(options.name, tab);
}
if (this._currentTab) {
this.onInactive.emit(this._currentTab);
}
this._currentTab = tab;
this.onActive.emit(tab);
return tab;
}

get currentTab(): Tab | null {
return this._currentTab;
}
private _onTabClose = (tab: Tab) => {
this._tabsByID.delete(tab.id);
if (tab.name) {
this._tabsByName.delete(tab.name);
}
if (this._currentTab === tab) {
this.open({
url: 'about:blank',
});
}
};

dispose() {

}
}
export default Browser;
4 changes: 4 additions & 0 deletions src/BrowserOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
interface BrowserOptions {
runScripts: 'dangerously' | 'outside-only';
}
export default BrowserOptions;
6 changes: 6 additions & 0 deletions src/MouseButton.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const enum MouseButton {
LEFT = 0,
MIDDLE = 1,
RIGHT = 2,
}
export default MouseButton;
5 changes: 5 additions & 0 deletions src/StorageLevel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const enum StorageLevel {
Local = 'local_storage',
Session = 'session_storage',
}
export default StorageLevel;
101 changes: 101 additions & 0 deletions src/Tab.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import {JSDOM, DOMWindow} from '@forbeslindesay/jsdom';
import {parseURL, serializeURL, URLData} from 'whatwg-url';
import Browser from './Browser';
import TabOptions from './TabOptions';
import StorageShim = require('node-storage-shim');

class Tab {
private readonly _browser: Browser;
private _url: URLData;
private _dom: Promise<JSDOM>;
public readonly id: string;
public readonly name: string | void;
private readonly _sessionStorage: Map<string, StorageShim> = new Map();
private readonly _localStorage: Map<string, StorageShim>;
private readonly _onClose: (tab: Tab) => void;
constructor(browser: Browser, options: TabOptions, id: string, localStorage: Map<string, StorageShim>, onClose: (tab: Tab) => void) {
this._browser = browser;
this.id = id;
this.name = options.name;
this._localStorage = localStorage;
this._onClose = onClose;
this.setLocation(options);
}
private _beforeParse = (window: DOMWindow) => {
// polyfill local storage
const host = window.location.host;
const sessionStorage = this._sessionStorage.get(host) || new StorageShim();
const localStorage = this._localStorage.get(host) || new StorageShim();
this._sessionStorage.set(host, sessionStorage);
this._localStorage.set(host, localStorage);
(window as any).sessionStorage = sessionStorage;
(window as any).localStorage = localStorage;

// polyfill console.dir
window.console.dir = window.console.log;

// polyfill requestAnimationFrame and cancelAnimationFrame
let lastTime = 0;
if (!window.requestAnimationFrame)
(window as any).requestAnimationFrame = function(callback: (delay: number) => void, element: any) {
var currTime = new Date().getTime();
var timeToCall = Math.max(0, 16 - (currTime - lastTime));
var id = window.setTimeout(function() { callback(currTime + timeToCall); },
timeToCall);
lastTime = currTime + timeToCall;
return id;
};

if (!window.cancelAnimationFrame)
window.cancelAnimationFrame = function(id) {
clearTimeout(id);
};

this._browser.onBeforeParse.emit(window);
};
setLocation(options: TabOptions): void {
const url = parseURL(options.url);
if (url === 'failure') {
throw new Error('Invalid url ' + options.url);
}
this._url = url;
if (url.scheme === 'about') {
this._dom = Promise.resolve(new JSDOM('', {
cookieJar: this._browser.cookies,
url: options.url,
referrer: options.referrer,
resources: 'usable',
runScripts: this._browser.runScripts,
beforeParse: this._beforeParse,
}));
} else {
this._dom = JSDOM.fromURL(options.url, {
cookieJar: this._browser.cookies,
referrer: options.referrer,
resources: 'usable',
runScripts: this._browser.runScripts,
beforeParse: this._beforeParse,
});
}
}
get dom() {
return this._dom;
}
whenReady(): Promise<JSDOM> {
return this._dom.then((dom): JSDOM | Promise<JSDOM> => {
const state = dom.window.document.readyState
if (state === 'complete' || state === 'interactive') {
return dom;
}
return new Promise((resolve, reject) => {
dom.window.document.addEventListener('DOMContentLoaded', () => resolve(dom));
});
});
}
async close() {
const dom = await this.dom;
dom.window.close();
this._onClose(this);
}
}
export default Tab;
16 changes: 16 additions & 0 deletions src/TabOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {DOMWindow} from '@forbeslindesay/jsdom';

interface TabOptions {
/**
* Window name (optional)
*/
name?: string;
/**
* Opening window (window.open call)
*/
opener?: DOMWindow;
referrer?: string;
url: string;
html?: string;
}
export default TabOptions;
Loading

0 comments on commit bb21fe7

Please sign in to comment.