-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Convert to typescript and directly use jsdom
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
1 parent
94eb45e
commit bb21fe7
Showing
27 changed files
with
1,398 additions
and
4,659 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
interface BrowserOptions { | ||
runScripts: 'dangerously' | 'outside-only'; | ||
} | ||
export default BrowserOptions; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.