Skip to content

Commit c2288ba

Browse files
committed
feat: add jsdom browser
1 parent 086b395 commit c2288ba

File tree

12 files changed

+333
-54
lines changed

12 files changed

+333
-54
lines changed

.circleci/config.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,19 @@ jobs:
104104
environment:
105105
BROWSER_STRING: chrome/browserstack/local
106106

107+
test-e2e-jsdom:
108+
docker:
109+
- image: circleci/node
110+
steps:
111+
- checkout
112+
- attach_workspace:
113+
at: ~/project
114+
- run:
115+
name: E2E Tests
116+
command: yarn test:e2e --coverage && yarn coverage
117+
environment:
118+
BROWSER_STRING: jsdom
119+
107120
workflows:
108121
version: 2
109122

@@ -116,3 +129,4 @@ workflows:
116129
- test-e2e-firefox: { requires: [lint] }
117130
- test-e2e-chrome: { requires: [lint] }
118131
- test-e2e-browserstack: { requires: [lint] }
132+
- test-e2e-jsdom: { requires: [lint] }

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
"geckodriver": "^1.16.2",
6767
"glob": "^7.1.4",
6868
"jest": "^24.8.0",
69+
"jsdom": "^15.1.1",
6970
"lodash.defaultsdeep": "^4.6.1",
7071
"node-env-file": "^0.1.8",
7172
"puppeteer": "^1.18.1",
@@ -85,6 +86,7 @@
8586
"chromedriver": "^75.1.0",
8687
"express": "^4.17.1",
8788
"geckodriver": "^1.16.2",
89+
"jsdom": "^15.1.1",
8890
"puppeteer": "^1.18.1",
8991
"puppeteer-core": "^1.18.1",
9092
"selenium-webdriver": "^4.0.0-alpha.1",

src/browser.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,12 @@ The browser probably won't ever start with globally mocked timers. Will try to a
223223
return this.ready
224224
}
225225

226+
_start() {}
227+
228+
_close() {}
229+
230+
_page() {}
231+
226232
async start(capabilities, ...args) {
227233
await this.callHook('start:before')
228234

src/browsers.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const browsers = {
2525
'ie/browserstack': () => _interopDefault(import('./ie/browserstack')),
2626
'ie/browserstack/local': () => _interopDefault(import('./ie/browserstack/local')),
2727
'ie/selenium': () => _interopDefault(import('./ie/selenium')),
28+
'jsdom': () => _interopDefault(import('./jsdom')),
2829
'puppeteer': () => _interopDefault(import('./puppeteer')),
2930
'puppeteer/core': () => _interopDefault(import('./puppeteer/core')),
3031
'safari': () => _interopDefault(import('./safari')),

src/jsdom/index.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import Browser from '../browser'
2+
import Webpage from './webpage'
3+
4+
export default class JsdomBrowser extends Browser {
5+
constructor(config = {}) {
6+
config.xvfb = false
7+
8+
super(config)
9+
10+
this.config.jsdom = this.config.jsdom || {}
11+
12+
this.logLevels = []
13+
}
14+
15+
async _loadDependencies() {
16+
if (!JsdomBrowser.jsdom) {
17+
JsdomBrowser.jsdom = await this.loadDependency('jsdom')
18+
}
19+
20+
// call super after setting core
21+
super._loadDependencies()
22+
}
23+
24+
setHeadless() {
25+
this.config.jsdom.pretendToBeVisual = false
26+
}
27+
28+
setLogLevel(types) {
29+
this.config.jsdom.virtualConsole = true
30+
31+
if (types && typeof types === 'string') {
32+
types = [types]
33+
}
34+
35+
this.logLevels = types
36+
}
37+
38+
_start() {
39+
this.driver = JsdomBrowser.jsdom
40+
}
41+
42+
_page(url, readyCondition) {
43+
const page = new Webpage(this)
44+
return page.open(url, readyCondition)
45+
}
46+
}

src/jsdom/webpage.js

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import BrowserError from '../utils/error'
2+
import Webpage from '../webpage'
3+
4+
export default class JsdomWebpage extends Webpage {
5+
async wrapWithGlobals(fn) {
6+
global.window = this.window
7+
global.document = this.document
8+
9+
const ret = await fn()
10+
11+
delete global.window
12+
delete global.document
13+
14+
return ret
15+
}
16+
17+
async open(url, readyCondition = 'body') {
18+
const jsdomOpts = this.browser.config.jsdom || {}
19+
20+
const options = {
21+
resources: 'usable',
22+
runScripts: 'dangerously',
23+
virtualConsole: false,
24+
pretendToBeVisual: true,
25+
beforeParse(window) {
26+
// Mock window.scrollTo
27+
window.scrollTo = () => {}
28+
29+
if (typeof jsdomOpts.beforeParse === 'function') {
30+
jsdomOpts.beforeParse(window)
31+
}
32+
},
33+
...jsdomOpts
34+
}
35+
36+
const onJsdomError = (err) => {
37+
throw new BrowserError(this, err)
38+
}
39+
40+
if (options.virtualConsole === 'trues') {
41+
const logLevels = this.browser.logLevels
42+
43+
const pageConsole = new Proxy({}, {
44+
get(target, type) {
45+
if (logLevels.includes(type)) {
46+
return console[type] // eslint-disable-line no-console
47+
}
48+
49+
return _ => _
50+
}
51+
})
52+
53+
const virtualConsole = new this.driver.VirtualConsole()
54+
virtualConsole.on('jsdomError', onJsdomError)
55+
virtualConsole.sendTo(pageConsole)
56+
57+
options.virtualConsole = virtualConsole
58+
} else {
59+
delete options.virtualConsole
60+
}
61+
62+
if (url.startsWith('file://')) {
63+
this.page = await this.driver.JSDOM.fromFile(url.substr(7), options)
64+
} else {
65+
this.page = await this.driver.JSDOM.fromURL(url, options)
66+
}
67+
68+
await this.browser.callHook('page:created', this.page)
69+
70+
this.browser.hook('close:before', () => {
71+
if (options.virtualConsole) {
72+
options.virtualConsole.removeListener('jsdomError', onJsdomError)
73+
}
74+
75+
this.page.window.close()
76+
})
77+
78+
this.window = this.page.window
79+
this.document = this.page.window.document
80+
81+
if (readyCondition) {
82+
const t = this
83+
await new Promise((resolve, reject) => {
84+
let iter = 1
85+
86+
async function waitForElement() {
87+
let isReady
88+
89+
if (typeof readyCondition === 'function') {
90+
isReady = await t.wrapWithGlobals(readyCondition)
91+
} else {
92+
isReady = !!t.document.querySelector(readyCondition)
93+
}
94+
95+
if (isReady) {
96+
resolve()
97+
return
98+
}
99+
100+
if (iter > 100) {
101+
reject(new BrowserError(t, `Timeout reached on waiting for readyCondition: ${readyCondition}`))
102+
return
103+
}
104+
105+
setTimeout(waitForElement, 100)
106+
iter++
107+
}
108+
109+
waitForElement()
110+
})
111+
}
112+
113+
return this.returnProxy()
114+
}
115+
116+
runScript(fn, ...args) {
117+
return this.wrapWithGlobals(() => fn(...args))
118+
}
119+
120+
runAsyncScript(fn, ...args) {
121+
return this.wrapWithGlobals(() => fn(...args))
122+
}
123+
124+
getHtml() {
125+
return this.document.documentElement.outerHTML
126+
}
127+
128+
getTitle() {
129+
return this.document.title
130+
}
131+
132+
getElementFromPage(pageFunction, selector, ...args) {
133+
const el = this.document.querySelector(selector)
134+
if (!el) {
135+
return Promise.resolve(null)
136+
}
137+
138+
return Promise.resolve(pageFunction(el, ...args))
139+
}
140+
141+
getElementsFromPage(pageFunction, selector, ...args) {
142+
const els = Array.from(this.document.querySelectorAll(selector))
143+
return Promise.resolve(pageFunction(els, ...args))
144+
}
145+
}

src/selenium/webpage.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@ export default class SeleniumWebpage extends Webpage {
77

88
await this.browser.callHook('page:created', this.driver)
99

10-
SeleniumWebpage.By = this.browser.constructor.webdriver.By
11-
SeleniumWebpage.until = this.browser.constructor.webdriver.until
10+
if (!SeleniumWebpage.By) {
11+
SeleniumWebpage.By = this.browser.constructor.webdriver.By
12+
SeleniumWebpage.Condition = this.browser.constructor.webdriver.Condition
13+
SeleniumWebpage.until = this.browser.constructor.webdriver.until
14+
}
1215

1316
if (readyCondition) {
1417
if (typeof readyCondition === 'function') {
15-
await this.driver.wait(readyCondition)
18+
await this.driver.wait(this.runScript(readyCondition))
1619
} else {
1720
const el = await SeleniumWebpage.until.elementLocated(SeleniumWebpage.By.css(readyCondition))
1821
await SeleniumWebpage.until.elementIsVisible(el)

src/utils/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export const browsers = [
66
'chrome',
77
'firefox',
88
'ie',
9+
'jsdom',
910
'edge',
1011
'safari'
1112
]

test/e2e/basic.test.js

Lines changed: 2 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -76,49 +76,7 @@ describe(browserString, () => {
7676
}
7777
}, false)
7878

79-
// attempts to get some logging from Firefox (NOTHING WORKS FOR ME ATM)
80-
if (browserString.includes('firefox')) {
81-
/* browser.hook('selenium:build:before', (builder) => {
82-
const service = new browser.constructor.client.ServiceBuilder()
83-
service.enableVerboseLogging(true)
84-
builder.setFirefoxService(service)
85-
})
86-
87-
browser.hook('selenium:build:options', (options, builder, browserInstance) => {
88-
options.setPreference('marionette.logging', 'TRACE')
89-
options.setPreference('devtools.console.stdout.content', true)
90-
})/**/
91-
92-
/*
93-
browser.hook('start:after', async (driver) => {
94-
await driver.installAddon(path.join(__dirname, '../utils/webconsoletap-1.0-fx.xpi'))
95-
})
96-
97-
browser.hook('webpage:property', async (property) => {
98-
if (!property.startsWith('get')) {
99-
// dont run this on page fn's called in this hook itself
100-
return
101-
}
102-
103-
const consoleLogs = await page.runAsyncScript(() => {
104-
window.console.requestDump()
105-
106-
var consoleLogs
107-
function dumpTimeout(fn) {
108-
setTimeout(() => {
109-
consoleLogs = window.console.getDump()
110-
if (consoleLogs === null) {
111-
dumpTimeout(fn)
112-
} else {
113-
fn(consoleLogs)
114-
}
115-
}, 50)
116-
}
117-
return new Promise(resolve => dumpTimeout(resolve))
118-
})
119-
})
120-
*/
121-
}
79+
browser.setLogLevel(['warn', 'error', 'info', 'log'])
12280

12381
await browser.start()
12482
} catch (e) {
@@ -150,7 +108,7 @@ describe(browserString, () => {
150108

151109
const url = browser.getUrl(webPath)
152110

153-
page = await browser.page(url)
111+
page = await browser.page(url, () => !!window.$vueMeta)
154112

155113
const html = await page.getHtml()
156114
expect(html).toBeDefined()

test/unit/browser.test.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ const browsers = {
3232
'ie/browserstack/local': async (name) => {
3333
const browser = await standardBrowserTest(name)
3434
},
35+
'jsdom': async (name) => {
36+
const browser = await standardBrowserTest(name)
37+
},
3538
'edge': async (name) => {
3639
await expect(createBrowser(name)).rejects.toThrow()
3740
},

0 commit comments

Comments
 (0)