')
130 |
131 | expect(ast).toEqual(expect.any(Object))
132 | expect(ast).toMatchSnapshot()
133 | })
134 |
135 | test('should accept error with only message', () => {
136 | const error = new utils.BrowserError('my test error')
137 | expect(error.message).toBe('BrowserError: my test error')
138 | })
139 |
140 | test('should accept error with class instance', () => {
141 | class MyTestClass {}
142 | const instance = new MyTestClass()
143 |
144 | const error = new utils.BrowserError(instance, 'my test error')
145 | expect(error.message).toBe('MyTestClass: my test error')
146 | })
147 |
148 | test('should accept error with identifier string', () => {
149 | const error = new utils.BrowserError('TestIdentifier', 'my test error')
150 | expect(error.message).toBe('TestIdentifier: my test error')
151 | })
152 |
153 | test('timers', async () => {
154 | utils.disableTimers()
155 |
156 | const start = process.hrtime.bigint()
157 | const waitPromise = utils.waitFor(250)
158 | jest.runAllTimers()
159 | await waitPromise
160 |
161 | const duration = parseInt(process.hrtime.bigint() - start) / 1e6
162 |
163 | expect(duration).toBeLessThan(250)
164 |
165 | utils.enableTimers()
166 | })
167 |
168 | test('fs: exists', async () => {
169 | expect(await utils.exists(__dirname)).toBe(true)
170 | expect(await utils.exists(path.join(__dirname, '/doesnt-exists'))).toBe(false)
171 | })
172 |
173 | test('fs: stats', async () => {
174 | expect(await utils.stats(__dirname)).toBeTruthy()
175 | expect(await utils.stats(path.join(__dirname, '/doesnt-exists'))).toBe(false)
176 | })
177 | })
178 |
--------------------------------------------------------------------------------
/test/unit/command.xvfb.test.js:
--------------------------------------------------------------------------------
1 | import os from 'os'
2 | import cp from 'child_process'
3 | import kill from 'tree-kill'
4 |
5 | jest.mock('tree-kill')
6 | jest.mock('../../src/utils/timers')
7 |
8 | describe('xvfb', () => {
9 | let Xvfb
10 | beforeEach(async () => {
11 | Xvfb = await import('../../src/commands/xvfb').then(m => m.default || m)
12 | })
13 |
14 | afterEach(() => {
15 | jest.restoreAllMocks()
16 | jest.resetModules()
17 | })
18 |
19 | test('should not throw error on unsupported platforms', () => {
20 | jest.spyOn(os, 'platform').mockReturnValue('not supported')
21 |
22 | expect(() => Xvfb.isSupported()).not.toThrow()
23 | })
24 |
25 | test('should throw error on unsupported platforms when set', () => {
26 | jest.spyOn(os, 'platform').mockReturnValue('not supported')
27 |
28 | expect(() => Xvfb.isSupported(true)).toThrow('not supported')
29 | })
30 |
31 | test('should set browser config when load called', () => {
32 | jest.spyOn(os, 'platform').mockReturnValue('linux')
33 |
34 | const browser = {
35 | hook: () => {},
36 | config: {}
37 | }
38 |
39 | expect(browser.config.xvfb).toBeUndefined()
40 | Xvfb.load(browser)
41 | expect(browser.config.xvfb).toBeUndefined()
42 |
43 | browser.config.xvfb = true
44 |
45 | Xvfb.load(browser)
46 | expect(browser.config.xvfb).toBe(true)
47 | })
48 |
49 | test('should add window args from browser config', () => {
50 | jest.spyOn(os, 'platform').mockReturnValue('linux')
51 |
52 | const width = 111
53 | const height = 222
54 |
55 | const browser = {
56 | hook: () => {},
57 | config: {
58 | xvfb: true,
59 | browserConfig: {
60 | window: { width, height }
61 | }
62 | }
63 | }
64 |
65 | Xvfb.load(browser)
66 | expect(browser.config.xvfb).toEqual(expect.any(Object))
67 | expect(browser.config.xvfb.args).toEqual(expect.any(Array))
68 | expect(browser.config.xvfb.args.length).toBe(1)
69 | expect(browser.config.xvfb.args[0]).toEqual(`-screen 0 ${width}x${height}x24`)
70 | })
71 |
72 | test('should not start twice', () => {
73 | jest.spyOn(os, 'platform').mockReturnValue('linux')
74 | const spawn = jest.spyOn(cp, 'spawn').mockImplementation(() => {
75 | return {
76 | connected: true,
77 | on() {},
78 | stderr: {
79 | on() {}
80 | }
81 | }
82 | })
83 |
84 | expect(Xvfb.isRunning()).toBe(false)
85 |
86 | Xvfb.start()
87 | expect(spawn).toHaveBeenCalledTimes(1)
88 | expect(Xvfb.isRunning()).toBe(true)
89 |
90 | Xvfb.start()
91 | expect(spawn).toHaveBeenCalledTimes(1)
92 | })
93 |
94 | test('should throw error when Xvfb not found', () => {
95 | jest.spyOn(os, 'platform').mockReturnValue('linux')
96 | jest.spyOn(cp, 'spawn').mockImplementation(() => {
97 | return {
98 | connected: true,
99 | on(type, fn) {
100 | if (type === 'error') {
101 | fn({ code: 'ENOENT' })
102 | }
103 | },
104 | stderr: {
105 | on() {}
106 | }
107 | }
108 | })
109 |
110 | expect(() => Xvfb.start()).toThrow('Xvfb not found')
111 | expect(Xvfb.isRunning()).toBe(false)
112 | })
113 |
114 | test('should warn when Xvfb already running and quiet false', () => {
115 | jest.spyOn(os, 'platform').mockReturnValue('linux')
116 | const spy = jest.spyOn(console, 'warn').mockImplementation(() => {})
117 | jest.spyOn(cp, 'spawn').mockImplementation(() => {
118 | return {
119 | connected: true,
120 | on(type, fn) {
121 | if (type === 'close') {
122 | fn(1, 0)
123 | }
124 | },
125 | stderr: {
126 | on(type, fn) {
127 | if (type === 'data') {
128 | fn(`(EE)
129 | Fatal server error:
130 | (EE) Server is already active for display 99
131 | If this server is no longer running, remove /tmp/.X99-lock
132 | and start again.
133 | (EE)`)
134 | }
135 | }
136 | }
137 | }
138 | })
139 |
140 | Xvfb.start()
141 | expect(spy).toHaveBeenCalledTimes(1)
142 | expect(Xvfb.isRunning()).toBe(false)
143 | })
144 |
145 | test('should warn when Xvfb already running unless quiet', () => {
146 | jest.spyOn(os, 'platform').mockReturnValue('linux')
147 | const spy = jest.spyOn(console, 'warn').mockImplementation(() => {})
148 | jest.spyOn(cp, 'spawn').mockImplementation(() => {
149 | return {
150 | connected: true,
151 | on(type, fn) {
152 | if (type === 'close') {
153 | fn(1, 0)
154 | }
155 | },
156 | stderr: {
157 | on(type, fn) {
158 | if (type === 'data') {
159 | fn(`(EE)
160 | Fatal server error:
161 | (EE) Server is already active for display 99
162 | If this server is no longer running, remove /tmp/.X99-lock
163 | and start again.
164 | (EE)`)
165 | }
166 | }
167 | }
168 | }
169 | })
170 |
171 | Xvfb.start({ quiet: true })
172 | expect(spy).not.toHaveBeenCalled()
173 | expect(Xvfb.isRunning()).toBe(false)
174 | })
175 |
176 | test('should warn when Xvfb failed to start', () => {
177 | jest.spyOn(os, 'platform').mockReturnValue('linux')
178 | jest.spyOn(cp, 'spawn').mockImplementation(() => {
179 | return {
180 | connected: true,
181 | on(type, fn) {
182 | if (type === 'close') {
183 | fn(1, 0)
184 | }
185 | },
186 | stderr: {
187 | on(type, fn) {
188 | if (type === 'data') {
189 | fn(`(EE)
190 | Fatal server error:
191 | (EE) Unrecognized option: 0
192 | (EE)
193 | `)
194 | }
195 | }
196 | }
197 | }
198 | })
199 |
200 | expect(() => Xvfb.start()).toThrow('BrowserError: Failed to start Xvfb, Unrecognized option: 0')
201 | expect(Xvfb.isRunning()).toBe(false)
202 | })
203 |
204 | test('should do nothing on stop when not started', () => {
205 | Xvfb.stop()
206 | expect(kill).not.toHaveBeenCalled()
207 | })
208 |
209 | test('should wait on stop for closed to be true', async () => {
210 | const spy = jest.spyOn(console, 'warn').mockImplementation(() => {})
211 | jest.useFakeTimers()
212 |
213 | Xvfb.process = true
214 | Xvfb.closed = false
215 |
216 | const stopPromise = Xvfb.stop()
217 | Xvfb.closed = true
218 | jest.advanceTimersByTime(100)
219 |
220 | await expect(stopPromise).resolves.toBeUndefined()
221 |
222 | jest.advanceTimersByTime(3100)
223 | expect(spy).not.toHaveBeenCalled()
224 | })
225 |
226 | test('should timeout on stop', async () => {
227 | const spy = jest.spyOn(console, 'warn').mockImplementation(() => {})
228 | jest.useFakeTimers()
229 |
230 | Xvfb.process = true
231 | Xvfb.closed = false
232 |
233 | const stopPromise = Xvfb.stop()
234 |
235 | jest.advanceTimersByTime(3100)
236 | await expect(stopPromise).resolves.toBeUndefined()
237 | expect(spy).toHaveBeenCalledTimes(1)
238 | })
239 | })
240 |
--------------------------------------------------------------------------------
/src/browsers/browser.js:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import Hookable from 'hable'
3 | import onExit from 'signal-exit'
4 | import { Xvfb, StaticServer } from '../commands'
5 | import {
6 | abstractGuard,
7 | loadDependency,
8 | isMockedFunction,
9 | disableTimers,
10 | enableTimers,
11 | getBrowserConfigFromString,
12 | getBrowserImportFromConfig,
13 | BrowserError
14 | } from '../utils'
15 | import { browsers } from '.'
16 |
17 | export default class Browser extends Hookable {
18 | constructor(config = {}) {
19 | super()
20 |
21 | abstractGuard('Browser', new.target)
22 |
23 | this.config = {
24 | quiet: false,
25 | ...config
26 | }
27 |
28 | this.ready = false
29 |
30 | if (config.extendPage && typeof config.extendPage === 'function') {
31 | this.hook('page:after', async (page) => {
32 | const extendWith = await config.extendPage(page)
33 | if (extendWith && typeof extendWith === 'object') {
34 | page.extend(extendWith)
35 | }
36 | })
37 | }
38 |
39 | this.capabilities = {}
40 |
41 | // before browserConfig
42 | this.config.browserArguments = this.config.browserArguments || []
43 |
44 | if (this.config.browserConfig) {
45 | for (const key in this.config.browserConfig) {
46 | if (key.startsWith('driver') || key.startsWith('provider') || key === 'browserVariant') {
47 | continue
48 | }
49 |
50 | if (key === 'staticserver') {
51 | if (!this.config.staticServer) {
52 | this.config.staticServer = true
53 | }
54 | continue
55 | }
56 |
57 | if (key === 'xvfb') {
58 | if (this.config.browserConfig[key] === 'false' || this.config.browserConfig.headless) {
59 | continue
60 | }
61 |
62 | if (!this.config.xvfb) {
63 | this.config.xvfb = Xvfb.isSupported()
64 | }
65 |
66 | continue
67 | }
68 |
69 | const fn = `set${key.charAt(0).toUpperCase()}${key.slice(1)}`
70 | if (this[fn]) {
71 | this[fn](this.config.browserConfig[key])
72 | } else {
73 | console.warn(`browserConfig '${key}' could not be set`) // eslint-disable-line no-console
74 | }
75 | }
76 | }
77 |
78 | if (!this.config.xvfb && this.config.xvfb !== false) {
79 | this.config.xvfb = Xvfb.isSupported()
80 | }
81 |
82 | Xvfb.load(this)
83 | StaticServer.load(this)
84 |
85 | if (isMockedFunction(setTimeout, 'setTimeout')) {
86 | // eslint-disable-next-line no-console
87 | console.warn(`Mocked timers detected
88 |
89 | The browser probably won't ever start with globally mocked timers. Will try to automatically use real timers on start and set to use fake timers after start. If the browser still hangs and doesn't start, make sure to only mock the global timers after the browser has started `)
90 |
91 | this.hook('start:before', () => enableTimers())
92 | this.hook('start:after', () => disableTimers())
93 | }
94 | }
95 |
96 | static async get(browserString = '', config = {}) {
97 | const browserConfig = getBrowserConfigFromString(browserString)
98 | const browserImport = getBrowserImportFromConfig(browserConfig)
99 |
100 | if (!browsers[browserImport]) {
101 | throw new BrowserError(`Unknown browser, no import exists for '${browserImport}'`)
102 | }
103 |
104 | try {
105 | // add browserConfig to config
106 | config.browserConfig = browserConfig
107 |
108 | const Browser = await browsers[browserImport]()
109 |
110 | const browserInstance = new Browser(config)
111 | await browserInstance.loadDependencies()
112 | return browserInstance
113 | } catch (e) {
114 | if (e instanceof BrowserError) {
115 | throw e
116 | } else {
117 | throw new BrowserError(`Error occured while loading '${browserConfig.browser || browserString}' browser`, e)
118 | }
119 | }
120 | }
121 |
122 | setLogLevel(level) {}
123 |
124 | async loadDependency(dependency) {
125 | try {
126 | return await loadDependency(dependency)
127 | } catch (e) {
128 | throw new BrowserError(this, e.message)
129 | }
130 | }
131 |
132 | _loadDependencies() {}
133 |
134 | async loadDependencies(...args) {
135 | await this.callHook('dependencies:load')
136 |
137 | await this._loadDependencies(...args)
138 |
139 | await this.callHook('dependencies:loaded')
140 | }
141 |
142 | getUrl(urlPath) {
143 | if (this.config.staticServer) {
144 | const { host, port } = this.config.staticServer
145 | return `http://${host}:${port}${urlPath}`
146 | }
147 |
148 | return `file://${path.join(this.config.folder, urlPath)}`
149 | }
150 |
151 | getCapabilities(capabilities) {
152 | if (!capabilities) {
153 | return this.capabilities
154 | }
155 |
156 | return {
157 | ...this.capabilities,
158 | ...capabilities
159 | }
160 | }
161 |
162 | getCapability(capability) {
163 | return this.capabilities[capability]
164 | }
165 |
166 | addCapability(key, value) {
167 | this.capabilities[key] = value
168 | return this
169 | }
170 |
171 | addCapabilities(capabilities) {
172 | this.capabilities = {
173 | ...this.capabilities,
174 | ...capabilities
175 | }
176 | return this
177 | }
178 |
179 | setWindow(width, height) {
180 | if (!height && typeof width === 'object') {
181 | this.config.window = width
182 | return this
183 | }
184 |
185 | this.config.window = { width, height }
186 | return this
187 | }
188 |
189 | getBrowser(name) {
190 | return this.getCapability('browserName')
191 | }
192 |
193 | setBrowser(name, version = '') {
194 | this.addCapability('browserName', name)
195 |
196 | if (version) {
197 | this.setBrowserVersion(version)
198 | }
199 |
200 | return this
201 | }
202 |
203 | setHeadless() {
204 | this.config.xvfb = false
205 | return this
206 | }
207 |
208 | getBrowserVersion() { return undefined }
209 |
210 | setBrowserVersion() { return this }
211 |
212 | setOs(...args) { return this.setOS(...args) }
213 |
214 | setOsVersion(...args) { return this.setOSVersion(...args) }
215 |
216 | setOS() { return this }
217 |
218 | setOSVersion() { return this }
219 |
220 | setDevice() { return this }
221 |
222 | isReady() {
223 | return this.ready
224 | }
225 |
226 | _start() {}
227 |
228 | _close() {}
229 |
230 | _page() {}
231 |
232 | async start(capabilities, ...args) {
233 | await this.callHook('start:before')
234 |
235 | try {
236 | await this._start(capabilities, ...args)
237 |
238 | await this.callHook('start:after', this.driver)
239 |
240 | this.ready = true
241 |
242 | onExit(() => this.close())
243 |
244 | return this
245 | /* istanbul ignore next */
246 | } catch (e) {
247 | await this.close()
248 |
249 | throw new BrowserError(e)
250 | }
251 | }
252 |
253 | async close(...args) {
254 | await this.callHook('close:before')
255 |
256 | await this._close(...args)
257 |
258 | await this.callHook('close:after')
259 | }
260 |
261 | async page(...args) {
262 | await this.callHook('page:before')
263 |
264 | const page = await this._page(...args)
265 |
266 | await this.callHook('page:after', page)
267 |
268 | return page
269 | }
270 | }
271 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4 |
5 | ### [0.7.5](https://github.com/nuxt-contrib/tib/compare/v0.7.4...v0.7.5) (2020-12-12)
6 |
7 |
8 | ### Bug Fixes
9 |
10 | * replace deprecated waitFor (resolves: [#63](https://github.com/nuxt-contrib/tib/issues/63)) ([542024f](https://github.com/nuxt-contrib/tib/commit/542024f5b37def13e6c7a0846fec481c6486cfa8))
11 |
12 | ### [0.7.4](https://github.com/nuxt-contrib/tib/compare/v0.7.3...v0.7.4) (2019-11-27)
13 |
14 |
15 | ### Bug Fixes
16 |
17 | * **detection:** Invalid Chrome detection command ([#37](https://github.com/nuxt-contrib/tib/issues/37)) ([ded2478](https://github.com/nuxt-contrib/tib/commit/ded2478b0394a34231f17540755c1408cd74901f))
18 |
19 | ### [0.7.3](https://github.com/nuxt-contrib/tib/compare/v0.7.2...v0.7.3) (2019-11-05)
20 |
21 |
22 | ### Bug Fixes
23 |
24 | * **detection:** Chrome detection fails on MacOs Catalina ([#30](https://github.com/nuxt-contrib/tib/issues/30)) ([c2527c6](https://github.com/nuxt-contrib/tib/commit/c2527c6)), closes [#29](https://github.com/nuxt-contrib/tib/issues/29)
25 |
26 | ### [0.7.2](https://github.com/nuxt-contrib/tib/compare/v0.7.1...v0.7.2) (2019-09-09)
27 |
28 |
29 | ### Bug Fixes
30 |
31 | * **selenium:** add mapping for browser log levels to selenium ([d78fd28](https://github.com/nuxt-contrib/tib/commit/d78fd28))
32 | * **static-server:** always set folder when unset ([e4b5cf4](https://github.com/nuxt-contrib/tib/commit/e4b5cf4))
33 | * correct regexp for detecting chrome ([#27](https://github.com/nuxt-contrib/tib/issues/27)) ([28ca90b](https://github.com/nuxt-contrib/tib/commit/28ca90b)), closes [#23](https://github.com/nuxt-contrib/tib/issues/23)
34 |
35 | ### [0.7.1](https://github.com/nuxt-contrib/tib/compare/v0.7.0...v0.7.1) (2019-08-15)
36 |
37 |
38 | ### Bug Fixes
39 |
40 | * remove circular dependencies in utils ([062b66e](https://github.com/nuxt-contrib/tib/commit/062b66e))
41 |
42 | ## [0.7.0](https://github.com/nuxt-contrib/tib/compare/v0.6.5...v0.7.0) (2019-08-15)
43 |
44 |
45 | ### Bug Fixes
46 |
47 | * only slice transpiled code if wrapped in block ([086b395](https://github.com/nuxt-contrib/tib/commit/086b395))
48 |
49 |
50 | ### Features
51 |
52 | * add jsdom browser ([c2288ba](https://github.com/nuxt-contrib/tib/commit/c2288ba))
53 | * support loading page functions from files using webpack ([#19](https://github.com/nuxt-contrib/tib/issues/19)) ([5f3322b](https://github.com/nuxt-contrib/tib/commit/5f3322b))
54 |
55 | ### [0.6.5](https://github.com/nuxt-contrib/tib/compare/v0.6.4...v0.6.5) (2019-07-28)
56 |
57 |
58 | ### Bug Fixes
59 |
60 | * change repository url ([7609958](https://github.com/nuxt-contrib/tib/commit/7609958))
61 |
62 |
63 |
64 | ### [0.6.4](https://github.com/nuxt-contrib/tib/compare/v0.6.3...v0.6.4) (2019-07-28)
65 |
66 |
67 | ### Bug Fixes
68 |
69 | * use 'folder: false' to use bs-local with an internal server ([8bd4abc](https://github.com/nuxt-contrib/tib/commit/8bd4abc))
70 |
71 |
72 |
73 | ### [0.6.3](https://github.com/nuxt-contrib/tib/compare/v0.6.2...v0.6.3) (2019-07-14)
74 |
75 |
76 | ### Bug Fixes
77 |
78 | * **staticserver:** keep ref to config ([e6fde22](https://github.com/nuxt-contrib/tib/commit/e6fde22))
79 |
80 |
81 |
82 | ### [0.6.2](https://github.com/nuxt-contrib/tib/compare/v0.6.1...v0.6.2) (2019-07-14)
83 |
84 |
85 | ### Bug Fixes
86 |
87 | * dont return early in getElementsFromPage ([4324211](https://github.com/nuxt-contrib/tib/commit/4324211))
88 |
89 |
90 | ### Features
91 |
92 | * add quiet option to disable logs ([0025dbb](https://github.com/nuxt-contrib/tib/commit/0025dbb))
93 |
94 |
95 |
96 | ### [0.6.1](https://github.com/nuxt-contrib/tib/compare/v0.6.0...v0.6.1) (2019-07-13)
97 |
98 |
99 | ### Bug Fixes
100 |
101 | * dont overwrite staticServer config ([be5675c](https://github.com/nuxt-contrib/tib/commit/be5675c))
102 |
103 |
104 |
105 | ## [0.6.0](https://github.com/nuxt-contrib/tib/compare/v0.5.2...v0.6.0) (2019-07-13)
106 |
107 |
108 | ### Bug Fixes
109 |
110 | * ignore grep errors for chrome detector ([262ae34](https://github.com/nuxt-contrib/tib/commit/262ae34))
111 |
112 |
113 | ### Features
114 |
115 | * add static webserver command ([9b075d4](https://github.com/nuxt-contrib/tib/commit/9b075d4))
116 | * add trim option to getText(s) ([1caadd0](https://github.com/nuxt-contrib/tib/commit/1caadd0))
117 |
118 |
119 | ### Tests
120 |
121 | * fix for selenium ([7533ed4](https://github.com/nuxt-contrib/tib/commit/7533ed4))
122 |
123 |
124 |
125 | ## [0.5.2](https://github.com/nuxt-contrib/tib/compare/v0.5.1...v0.5.2) (2019-05-05)
126 |
127 |
128 |
129 | ## [0.5.1](https://github.com/nuxt-contrib/tib/compare/v0.5.0...v0.5.1) (2019-04-03)
130 |
131 |
132 | ### Bug Fixes
133 |
134 | * use correct es5 entry point ([cc68e67](https://github.com/nuxt-contrib/tib/commit/cc68e67))
135 |
136 |
137 |
138 | # [0.5.0](https://github.com/nuxt-contrib/tib/compare/v0.4.0...v0.5.0) (2019-03-26)
139 |
140 |
141 | ### Bug Fixes
142 |
143 | * detect chrome correctly on darwin (use egrep) ([c6068a2](https://github.com/nuxt-contrib/tib/commit/c6068a2))
144 | * implement safari correctly ([15c4d8b](https://github.com/nuxt-contrib/tib/commit/15c4d8b))
145 | * only enable xfvb by default on supported platforms ([d6df88c](https://github.com/nuxt-contrib/tib/commit/d6df88c))
146 | * only load xfvb by default on supported platforms ([52fac06](https://github.com/nuxt-contrib/tib/commit/52fac06))
147 |
148 |
149 | ### Features
150 |
151 | * rename browser export to createBrowser ([d62e935](https://github.com/nuxt-contrib/tib/commit/d62e935))
152 |
153 |
154 |
155 | # [0.4.0](https://github.com/nuxt-contrib/tib/compare/v0.3.0...v0.4.0) (2019-03-21)
156 |
157 |
158 | ### Features
159 |
160 | * add mocked timer detection and try to workaround them ([ea9409c](https://github.com/nuxt-contrib/tib/commit/ea9409c))
161 |
162 |
163 |
164 | # [0.3.0](https://github.com/nuxt-contrib/tib/compare/v0.2.2...v0.3.0) (2019-03-21)
165 |
166 |
167 | ### Bug Fixes
168 |
169 | * add exit listeners to exit child procs on interruption ([744dcff](https://github.com/nuxt-contrib/tib/commit/744dcff))
170 |
171 |
172 | ### Features
173 |
174 | * auto transpile when browser version is specified ([f610f25](https://github.com/nuxt-contrib/tib/commit/f610f25))
175 |
176 |
177 |
178 | ## [0.2.2](https://github.com/nuxt-contrib/tib/compare/v0.2.1...v0.2.2) (2019-03-20)
179 |
180 |
181 | ### Bug Fixes
182 |
183 | * **browserstack:** only set default os when not set ([3fa0322](https://github.com/nuxt-contrib/tib/commit/3fa0322))
184 |
185 |
186 |
187 | ## [0.2.1](https://github.com/nuxt-contrib/tib/compare/v0.2.0...v0.2.1) (2019-03-20)
188 |
189 |
190 |
191 | # [0.2.0](https://github.com/nuxt-contrib/tib/compare/v0.1.0...v0.2.0) (2019-03-20)
192 |
193 |
194 | ### Bug Fixes
195 |
196 | * **selenium:** make sync scripts work in run async ([596f2ad](https://github.com/nuxt-contrib/tib/commit/596f2ad))
197 |
198 |
199 | ### Features
200 |
201 | * add cjs build ([aa6cdd4](https://github.com/nuxt-contrib/tib/commit/aa6cdd4))
202 | * improvments ([d3db982](https://github.com/nuxt-contrib/tib/commit/d3db982))
203 | * split puppeteer/puppeteer-core ([341356b](https://github.com/nuxt-contrib/tib/commit/341356b))
204 |
205 |
206 |
207 | # 0.1.0 (2019-03-15)
208 |
209 |
210 | ### Bug Fixes
211 |
212 | * dont throw error when Xvfb is already running (which is fine) ([35eaccb](https://github.com/nuxt-contrib/tib/commit/35eaccb))
213 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # test in browser (tib)
2 |

3 |

4 | [](https://www.npmjs.com/package/tib)
5 | [](https://www.npmjs.com/package/tib)
6 |
7 | Helper classes for e2e browser testing in Node with a uniform interface.
8 |
9 | ## Introduction
10 |
11 | `tib` aims to provide a uniform interface for testing with both jsdom, Puppeteer and Selenium while using either local browsers or a 3rd party provider. This way you can write a single e2e test and simply switch the browser environment by changing the [`BrowserString`](#browser-strings)
12 |
13 | The term `helper classes` stems from that this package wont enforce test functionality on you (which would require another learning curve). `tib` allows you to use the test suite you are already familair with. Use `tib` to retrieve and assert whether the html you expect to be loaded is really loaded, both on page load as after interacting with it through javascript.
14 |
15 | ## Supported browsers/drivers/providers:
16 |
17 | - Puppeteer
18 | - \-core
19 | - Selenium
20 | - Firefox
21 | - Chrome
22 | - Safari
23 | - IE (_untested_)
24 | - Edge (_untested_)
25 | - jsdom
26 | - BrowserStack
27 |
28 | All browser/provider specific dependencies are peer dependencies and are dynamically loaded. You only need to install the peer-dependencies you plan to use
29 |
30 | ## Features
31 |
32 | - Retrieve html as ASTElements (using [`vue-template-compiler`](https://www.npmjs.com/package/vue-template-compiler))
33 | - Very easy to write page function to run in the browser
34 | - just remember to only use language features the loaded page already has polyfills for
35 | - syntax is automatically transpiled when browser version is specified
36 | - e.g. arrow functions will be transpiled to normal functions when you specify 'safari 5.1'
37 | - Supports BrowserStack-Local to easily tests local html files
38 | - Serve your local html files with a simple webserver
39 | - Automatically starts Xvfb for non-headless support (on supported platforms)
40 | - set `xvfb: false` if you want to specify DISPLAY manually
41 |
42 | ## Documentation
43 |
44 | ### Install
45 |
46 | ```bash
47 | $ yarn add -D tib
48 | ```
49 | #### Extra steps on Mac OS with Safari
50 |
51 | Make sure to Enable WebDriver Support, see [here](https://developer.apple.com/documentation/webkit/testing_with_webdriver_in_safari) for more information
52 |
53 | ### Usage
54 |
55 | ```js
56 | import { createBrowser } from 'tib'
57 |
58 | const browserString = 'firefox/headless'
59 | const autoStart = false // default true
60 | const config = {
61 | extendPage(page) {
62 | return {
63 | myPageFn() {
64 | // do something
65 | }
66 | }
67 | }
68 | }
69 |
70 | const browser = await createBrowser(browserString, config, autoStart)
71 | if (!autoStart) {
72 | await browser.start()
73 | }
74 | ```
75 |
76 | ### Browser Strings
77 |
78 | Browser strings are broken up into capability pairs (e.g. `chrome 71` is a capability pair consisting of `browser name` and `browser version`). Those pairs are then matched against a list of known properties (see [constants.js](./src/utils/constants.js) for the full list). Browser and provider properties are used to determine the required import (see [browsers.js](./src/browsers.js)). The remaining properties should be capabilities and are depending on whether the value was recognised applied to the browser instance by calling the corresponding `set
` methods.
79 |
80 | ### API
81 |
82 | Read the [API reference](./docs/API.md)
83 |
84 | ## Example
85 |
86 | also check [our e2e tests](./test/e2e/basic.test.js) for more information
87 |
88 | ```js
89 | import { createBrowser, commands: { Xvfb, BrowserStackLocal } } from 'tib'
90 |
91 | const browserString = 'windows 10/chrome 71/browserstack/local/1920x1080'
92 | // const browserString = 'puppeteer/core/staticserver'
93 |
94 | describe('my e2e test', () => {
95 | let myBrowser
96 |
97 | beforeAll(async () => {
98 | myBrowser = await createBrowser(browserString, {
99 | // if true or undefined then Xvfb is automatically started before
100 | // the browser and the displayNum=99 added to the process.env
101 | xvfb: false,
102 | quiet: false,
103 | folder: process.cwd(),
104 | staticServer: {
105 | host: 'localhost', // or set process.env.HOST
106 | port: 3000 // or set process.env.PORT
107 | },
108 | // only used for BrowserStackLocal browsers
109 | BrowserStackLocal: {
110 | start: true, // default, if false then call 'const pid = await BrowserStackLocal.start()'
111 | stop: true, // default, if false then call 'await BrowserStackLocal.stop(pid)'
112 | user: process.env.BROWSERSTACK_USER,
113 | key: process.env.BROWSERSTACK_KEY
114 | },
115 | extendPage(page) {
116 | return {
117 | getRouteData() {
118 | return page.runScript(() => {
119 | // this function is executed within the page context
120 | // if you use features like Promises and are testing on
121 | // older browsers make sure you have a polyfill already
122 | // loaded
123 | return myRouter.currentRoute
124 | })
125 | },
126 | async navigate(path) {
127 | await page.runAsyncScript((path) => {
128 | return new Promise(resolve => {
129 | myRouter.on('navigationFinished', resolve)
130 | window.myRouter.navigate(path)
131 | })
132 | }, path)
133 | }
134 | }
135 | }
136 | })
137 | })
138 |
139 | afterAll(() => {
140 | if (myBrowser) {
141 | await myBrowser.close()
142 | }
143 | })
144 |
145 | test('router', async () => {
146 | const url = myBrowser.getUrl('/')
147 |
148 | const page = await myBrowser.page(url)
149 |
150 | // you should probably expect and not log this
151 | console.log(await page.getHtml())
152 | console.log(await page.getElement('div'))
153 | console.log(await page.getElements('div'))
154 | console.log(await page.getElementCount('div'))
155 | console.log(await page.getAttribute('div', 'id'))
156 | console.log(await page.getAttributes('div', 'id'))
157 | console.log(await page.getText('h1'))
158 | console.log(await page.getTexts('h1, h2'))
159 | console.log(await page.getTitle())
160 |
161 | await page.navigate('/about')
162 | console.log(await page.getRouteData())
163 |
164 | console.log(await page.getTitle())
165 | })
166 | })
167 | ```
168 |
169 | ## FAQ
170 |
171 | #### I receive a `WebDriverError: invalid argument: can't kill an exited process` error
172 |
173 | Its a Selenium error and means the browser couldnt be started or exited immeditately after start. Try to run with `xvfb: true`
174 |
175 | ## Known issues / caveats
176 |
177 | - If Node force exits then local running commands might keep running (eg geckodriver, chromedriver, Xvfb, browserstack-local)
178 | - _workaround_: none unfortunately
179 | - On CircleCI puppeteer sometimes triggers `Protocol error (Runtime.callFunctionOn): Target closed` error on page.evaluate. This could be related to a version mismatch between the browser and puppeteer.
180 | - _workaround_: use `chrome/selenium`
181 | - with Firefox you cannot run two page functions at the same time, also not when they are async
182 | - _workaround_: combine the functionality you need in a single page function
183 | - with Safari you can get ScriptTimeoutError on asynchronous page function execution. Often the timeout seems false as it is in ms and the scripts are still executed
184 | - _workaround_: wrap runAsyncScript calls in `try/catch` to just ignore the timeout :)
185 |
186 | ## Todo's
187 | - local ie/edge
188 | - more platforms
189 | - SauceLabs (key required)
190 | - others?
191 | - increase coverage
192 |
--------------------------------------------------------------------------------
/src/utils/detectors/chrome.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @license Copyright 2016 Google Inc. All Rights Reserved.
3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
4 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
5 | */
6 | import fs from 'fs'
7 | import path from 'path'
8 | import { execSync, execFileSync } from 'child_process'
9 | import isWsl from 'is-wsl'
10 | import uniq from 'lodash/uniq'
11 |
12 | const newLineRegex = /\r?\n/
13 |
14 | /**
15 | * This class is based on node-get-chrome
16 | * https://github.com/mrlee23/node-get-chrome
17 | * https://github.com/gwuhaolin/chrome-finder
18 | */
19 | export default class ChromeDetector {
20 | constructor() {
21 | this.platform = isWsl ? 'wsl' : process.platform
22 | }
23 |
24 | detect(platform = this.platform) {
25 | const handler = this[platform]
26 | if (typeof handler !== 'function') {
27 | throw new Error(`${platform} is not supported.`)
28 | }
29 | return this[platform]()[0]
30 | }
31 |
32 | darwin() {
33 | const suffixes = [
34 | '/Contents/MacOS/Chromium',
35 | '/Contents/MacOS/Google Chrome Canary',
36 | '/Contents/MacOS/Google Chrome'
37 | ]
38 | const LSREGISTER =
39 | '/System/Library/Frameworks/CoreServices.framework' +
40 | '/Versions/A/Frameworks/LaunchServices.framework' +
41 | '/Versions/A/Support/lsregister'
42 | const installations = []
43 | const customChromePath = this.resolveChromePath()
44 | if (customChromePath) {
45 | installations.push(customChromePath)
46 | }
47 | execSync(
48 | `${LSREGISTER} -dump` +
49 | " | grep -E -i -o '/.+(google chrome( canary)?|chromium)\\.app(\\s|$)'" +
50 | " | grep -E -v 'Caches|TimeMachine|Temporary|/Volumes|\\.Trash'"
51 | )
52 | .toString()
53 | .split(newLineRegex)
54 | .forEach((inst) => {
55 | suffixes.forEach((suffix) => {
56 | const execPath = path.join(inst.trim(), suffix)
57 | if (this.canAccess(execPath)) {
58 | installations.push(execPath)
59 | }
60 | })
61 | })
62 | // Retains one per line to maintain readability.
63 | // clang-format off
64 | const priorities = [
65 | { regex: new RegExp(`^${process.env.HOME}/Applications/.*Chrome.app`), weight: 50 },
66 | { regex: new RegExp(`^${process.env.HOME}/Applications/.*Chrome Canary.app`), weight: 51 },
67 | { regex: new RegExp(`^${process.env.HOME}/Applications/.*Chromium.app`), weight: 52 },
68 | { regex: /^\/Applications\/.*Chrome.app/, weight: 100 },
69 | { regex: /^\/Applications\/.*Chrome Canary.app/, weight: 101 },
70 | { regex: /^\/Applications\/.*Chromium.app/, weight: 102 },
71 | { regex: /^\/Volumes\/.*Chrome.app/, weight: -3 },
72 | { regex: /^\/Volumes\/.*Chrome Canary.app/, weight: -2 },
73 | { regex: /^\/Volumes\/.*Chromium.app/, weight: -1 }
74 | ]
75 | if (process.env.LIGHTHOUSE_CHROMIUM_PATH) {
76 | priorities.push({ regex: new RegExp(process.env.LIGHTHOUSE_CHROMIUM_PATH), weight: 150 })
77 | }
78 | if (process.env.CHROME_PATH) {
79 | priorities.push({ regex: new RegExp(process.env.CHROME_PATH), weight: 151 })
80 | }
81 | // clang-format on
82 | return this.sort(installations, priorities)
83 | }
84 |
85 | /**
86 | * Look for linux executables in 3 ways
87 | * 1. Look into CHROME_PATH env variable
88 | * 2. Look into the directories where .desktop are saved on gnome based distro's
89 | * 3. Look for google-chrome-stable & google-chrome executables by using the which command
90 | */
91 | linux() {
92 | let installations = []
93 | // 1. Look into CHROME_PATH env variable
94 | const customChromePath = this.resolveChromePath()
95 | if (customChromePath) {
96 | installations.push(customChromePath)
97 | }
98 | // 2. Look into the directories where .desktop are saved on gnome based distro's
99 | const desktopInstallationFolders = [
100 | path.join(require('os').homedir(), '.local/share/applications/'),
101 | '/usr/share/applications/'
102 | ]
103 | desktopInstallationFolders.forEach((folder) => {
104 | installations = installations.concat(this.findChromeExecutables(folder))
105 | })
106 | // Look for chromium(-browser) & google-chrome(-stable) executables by using the which command
107 | const executables = [
108 | 'chromium-browser',
109 | 'chromium',
110 | 'google-chrome-stable',
111 | 'google-chrome'
112 | ]
113 | executables.forEach((executable) => {
114 | try {
115 | const chromePath = execFileSync('which', [executable])
116 | .toString()
117 | .split(newLineRegex)[0]
118 | if (this.canAccess(chromePath)) {
119 | installations.push(chromePath)
120 | }
121 | } catch (e) {
122 | // Not installed.
123 | }
124 | })
125 | if (!installations.length) {
126 | throw new Error(
127 | 'The environment variable CHROME_PATH must be set to ' +
128 | 'executable of a build of Chromium version 54.0 or later.'
129 | )
130 | }
131 | const priorities = [
132 | { regex: /chromium-browser$/, weight: 51 },
133 | { regex: /chromium$/, weight: 50 },
134 | { regex: /chrome-wrapper$/, weight: 49 },
135 | { regex: /google-chrome-stable$/, weight: 48 },
136 | { regex: /google-chrome$/, weight: 47 }
137 | ]
138 | if (process.env.LIGHTHOUSE_CHROMIUM_PATH) {
139 | priorities.push({
140 | regex: new RegExp(process.env.LIGHTHOUSE_CHROMIUM_PATH),
141 | weight: 100
142 | })
143 | }
144 | if (process.env.CHROME_PATH) {
145 | priorities.push({ regex: new RegExp(process.env.CHROME_PATH), weight: 101 })
146 | }
147 | return this.sort(uniq(installations.filter(Boolean)), priorities)
148 | }
149 |
150 | wsl() {
151 | // Manually populate the environment variables assuming it's the default config
152 | process.env.LOCALAPPDATA = this.getLocalAppDataPath(process.env.PATH)
153 | process.env.PROGRAMFILES = '/mnt/c/Program Files'
154 | process.env['PROGRAMFILES(X86)'] = '/mnt/c/Program Files (x86)'
155 | return this.win32()
156 | }
157 |
158 | win32() {
159 | const installations = []
160 | const sep = path.sep
161 | const suffixes = [
162 | `${sep}Chromium${sep}Application${sep}chrome.exe`,
163 | `${sep}Google${sep}Chrome SxS${sep}Application${sep}chrome.exe`,
164 | `${sep}Google${sep}Chrome${sep}Application${sep}chrome.exe`,
165 | `${sep}chrome-win32${sep}chrome.exe`,
166 | `${sep}Google${sep}Chrome Beta${sep}Application${sep}chrome.exe`
167 | ]
168 | const prefixes = [
169 | process.env.LOCALAPPDATA,
170 | process.env.PROGRAMFILES,
171 | process.env['PROGRAMFILES(X86)']
172 | ].filter(Boolean)
173 | const customChromePath = this.resolveChromePath()
174 | if (customChromePath) {
175 | installations.push(customChromePath)
176 | }
177 | prefixes.forEach(prefix =>
178 | suffixes.forEach((suffix) => {
179 | const chromePath = path.join(prefix, suffix)
180 | if (this.canAccess(chromePath)) {
181 | installations.push(chromePath)
182 | }
183 | })
184 | )
185 | return installations
186 | }
187 |
188 | resolveChromePath() {
189 | if (this.canAccess(process.env.CHROME_PATH)) {
190 | return process.env.CHROME_PATH
191 | }
192 | if (this.canAccess(process.env.LIGHTHOUSE_CHROMIUM_PATH)) {
193 | console.warn( // eslint-disable-line no-console
194 | 'ChromeLauncher',
195 | 'LIGHTHOUSE_CHROMIUM_PATH is deprecated, use CHROME_PATH env variable instead.'
196 | )
197 | return process.env.LIGHTHOUSE_CHROMIUM_PATH
198 | }
199 | }
200 |
201 | getLocalAppDataPath(path) {
202 | const userRegExp = /\/mnt\/([a-z])\/Users\/([^/:]+)\/AppData\//
203 | const results = userRegExp.exec(path) || []
204 | return `/mnt/${results[1]}/Users/${results[2]}/AppData/Local`
205 | }
206 |
207 | sort(installations, priorities) {
208 | const defaultPriority = 10
209 | return installations
210 | .map((inst) => {
211 | for (const pair of priorities) {
212 | if (pair.regex.test(inst)) {
213 | return { path: inst, weight: pair.weight }
214 | }
215 | }
216 | return { path: inst, weight: defaultPriority }
217 | })
218 | .sort((a, b) => b.weight - a.weight)
219 | .map(pair => pair.path)
220 | }
221 |
222 | canAccess(file) {
223 | if (!file) {
224 | return false
225 | }
226 | try {
227 | fs.accessSync(file)
228 | return true
229 | } catch (e) {
230 | return false
231 | }
232 | }
233 |
234 | findChromeExecutables(folder) {
235 | const argumentsRegex = /(^[^ ]+).*/ // Take everything up to the first space
236 | const chromeExecRegex = '^Exec=/.*/(google-chrome|chrome|chromium)-.*'
237 | const installations = []
238 | if (this.canAccess(folder)) {
239 | // Output of the grep & print looks like:
240 | // /opt/google/chrome/google-chrome --profile-directory
241 | // /home/user/Downloads/chrome-linux/chrome-wrapper %U
242 | let execPaths
243 | const execOptions = {
244 | stdio: [0, 'pipe', 'ignore']
245 | }
246 | // Some systems do not support grep -R so fallback to -r.
247 | // See https://github.com/GoogleChrome/chrome-launcher/issues/46 for more context.
248 | try {
249 | execPaths = execSync(
250 | `grep -ER "${chromeExecRegex}" ${folder} | awk -F '=' '{print $2}'`,
251 | execOptions
252 | )
253 | } catch (e) {
254 | execPaths = execSync(
255 | `grep -Er "${chromeExecRegex}" ${folder} | awk -F '=' '{print $2}'`,
256 | execOptions
257 | )
258 | }
259 | execPaths = execPaths
260 | .toString()
261 | .split(newLineRegex)
262 | .map(execPath => execPath.replace(argumentsRegex, '$1'))
263 | execPaths.forEach(
264 | execPath => this.canAccess(execPath) && installations.push(execPath)
265 | )
266 | }
267 | return installations
268 | }
269 | }
270 |
--------------------------------------------------------------------------------
/docs/API.md:
--------------------------------------------------------------------------------
1 | # API Reference
2 |
3 | > Return types clarification
4 | >
5 | > `resolves`: the function is asynchronous and returns a Promise which resolves to the value
6 | > `returns`: the function is synchronous and returns the value
7 |
8 | ## Configuration
9 |
10 | ### `folder`
11 |
12 | The local folder with static html files you wish to test. This folder is used both by browserstack-local and the static webserver.
13 | If you dont use either of those, the files will be loaded through the `file://` protocol.
14 |
15 | ### `quiet`
16 | _boolean_ (default: `false`)
17 |
18 | If tue then information logging eg by static server and xvfb wont show
19 |
20 | ### `xvfb`
21 | _boolean_ (default: `true`)
22 |
23 | Whether a Xvfb server should be started.
24 |
25 | Is automatically set to `false` for headless browsers or when using a provider
26 |
27 | ### `window`
28 | _object_ (default: `undefined`)
29 |
30 | An object in the form `{ width, height }` where width & height define the window size. Can be overridden by [`setWindow`](#setwindow)
31 |
32 | ### `extendPage`
33 | _function_ (default: `undefined`)
34 |
35 | This function is called after a page is opened and receives the `webpage` instance as argument.
36 |
37 | It should return or resolve to an object which is used in the [page proxy](#pageproxy)
38 |
39 | ### `staticServer`
40 | _boolean_ / _object_
41 |
42 | If an object the configuration for the included static webserver. If `true` then the default configuration will be used.
43 |
44 | - `host` (_string_, default `localhost`)
45 | The hostname / ip to run the static server on. Optionally you can also set the `HOST` env variable.
46 | - `port` (_integer_, default `3000`)
47 | The port the static server should run on. Optionally you can also set the `PORT` env variable.
48 | - `folder` (_string_, optional)
49 | The path used as the webroot for the webserver. If not provided the default `folder` browser config option is used
50 |
51 | ### `BrowserStackLocal`
52 | _object_
53 |
54 | This configuration is only used with browserstack/local browsers
55 |
56 | The object can list the following properties:
57 |
58 | - `start` (_boolean_, default `true`)
59 | Whether to start the browserstack-local daemon on `browser.start`
60 | - `stop` (_boolean_, default `true`)
61 | Whether to stop the browserstack-local daemon on `browser.close`
62 | - `user` (_string_)
63 | Your browserstack user name
64 | > for security reasons it is recommended to use the env var `BROWSERSTACK_USER` instead
65 | - `key` (_string_)
66 | Your browserstack key
67 | > for security reasons it is recommended to use the env var `BROWSERSTACK_KEY` instead
68 | - `folder` (_string_, _boolean_ optional)
69 | The path to the folder which should be accessible through the browserstack-local tunnel. Explicitly set this to `false` if you wish to test an internal server. If any other falsy value is provided the default `folder` browser config option is used.
70 |
71 | ## Browser methods
72 |
73 | ### _`constructor`_
74 | _arguments_
75 | - config (type: `object`)
76 |
77 | ### `start`
78 | _arguments_
79 | - capabilities (type: `string`)
80 | - ...arguments (type: `any`)
81 |
82 | _resolves_ `this`
83 |
84 | _rejects on error_
85 |
86 | Starts the browser and all extra commands
87 |
88 | For Puppeteer capabilities are added as launch options and arguments to the `args` key of launch options
89 | For Selenium arguments are ignored
90 |
91 | ### `isReady`
92 | _returns_ `boolean`
93 |
94 | Returns true when the browser was started succesfully
95 |
96 | ### `close`
97 | _resolves_ `void`
98 |
99 | Closes the browser and all commands that were started
100 |
101 | ### `getUrl`
102 | _arguments_
103 | - path (type: `string`)
104 | _returns_ `url`
105 |
106 | Returns the full url for a path. Depending your browser config it returns the url to/for browserstack-local, static server or just local files.
107 |
108 | ### `page`
109 | _arguments_
110 | - url (type: `string`)
111 | - readyCondition? (type: `string` | `Function`)
112 |
113 | _resolves_ `page proxy` (see below)
114 |
115 | _rejects on error_
116 |
117 | Opens the url on a new webpage in the browser and waits for the readyCondition to become true.
118 |
119 | ##### Page Proxy
120 |
121 | It resolves to a Proxy which returns properties in the following order:
122 | - a `userExtended` property (see configuration options)
123 | - a Webpage property (from tib)
124 | - a Page property (from the BrowserDriver, _Puppeteer only_)
125 | - a BrowserDriver property
126 |
127 | If the requested property doesnt exist on any of the internal objects it returns `undefined`
128 |
129 | Also see the [`webpage:property`](#webpageproperty) hook which is called every time you access a property through the Proxy
130 |
131 | ### `setLogLevel`
132 |
133 | ### `getCapabilities`
134 | _arguments_
135 | - capabilities? (type `object`)
136 |
137 | _returns_ `object`
138 |
139 | Returns the current capabilities. If the `capabilities` argument is specified these capabilities are also returned but are not added to the current capabilities.
140 |
141 | ### `addCapabilities`
142 | _arguments_
143 | - capabilities (type `object`)
144 |
145 | _returns_ `this`
146 |
147 | Adds to or sets the current capabilities with the new `capabilities`
148 |
149 | ### `getCapability`
150 | _arguments_
151 | - capability (type `string`)
152 |
153 | _returns_ `string`
154 |
155 | Returns the value for the requested `capability`
156 |
157 | ### `addCapability`
158 | _arguments_
159 | - key (type `string`)
160 | - value (type `string`)
161 |
162 | _returns_ `this`
163 |
164 | Adds or sets the value of the capability with name `key`
165 |
166 | ### `setWindow`
167 | _arguments_
168 | - width (type `number`)
169 | - height (type `number`)
170 |
171 | Sets the window size.
172 |
173 | Also used for the Xvfb comamand
174 |
175 | ### `getBrowser`
176 | _returns_ `string`
177 |
178 | Returns the browser name
179 |
180 | ### `setBrowser`
181 | _arguments_
182 | - browserName (type `string`)
183 |
184 | > Dont call this unless you have to, if you use a browserstring the browser name should already be set correctly
185 |
186 | Sets the browser name
187 |
188 | ### `setHeadless`
189 | _returns_ `this`
190 |
191 | If called the browser will be started headless (if supported). Disables the Xvfb command
192 |
193 | ### `getBrowserVersion`
194 | _returns_ `string`
195 |
196 | > this capability is probably only usefull when using a provider or selenium server
197 |
198 | Returns the browser version which was set
199 |
200 | ### `setBrowserVersion`
201 | _arguments_
202 | - version (type `string`)
203 |
204 | _returns_ `this`
205 |
206 | > this capability is probably only usefull when using a provider or selenium server
207 |
208 | Sets the browser version
209 |
210 | ### `setOs` / `setOS`
211 | _arguments_
212 | - name (type `string`)
213 | - version (type `string`)
214 |
215 | _returns_ `this`
216 |
217 | > this capability is probably only usefull when using a provider or selenium server
218 |
219 | Sets the os name and os version
220 |
221 | ```js
222 | browser.setOs('windows', 7)
223 | ```
224 |
225 | ### `setOsVersion` / `setOSVersion`
226 | _arguments_
227 | - version (type `string`)
228 |
229 | _returns_ `this`
230 |
231 | > this capability is probably only usefull when using a provider or selenium server
232 |
233 | Sets the os version
234 |
235 | ```js
236 | browser.setOs('windows') // default
237 | browser.setOsVersion(7)
238 | ```
239 |
240 | ### `setDevice`
241 | _arguments_
242 | - name (type `string`)
243 |
244 | _returns_ `this`
245 |
246 | > this capability is probably only usefull when using a provider or selenium server
247 |
248 | Sets the name of the device (eg for mobile testing)
249 |
250 | ### `getLocalFolderUrl`
251 | _arguments_
252 | - path (type `string`)
253 |
254 | > This method is only available in BrowserStackLocal browsers
255 |
256 | Returns the full url for the relative `path` so browserstack can access your code through the browserstack-local tunnel
257 |
258 | ## Webpage methods
259 |
260 | ### getHtml
261 | _resolves_ `string`
262 |
263 | The full html of the loaded Webpage
264 |
265 | ### getTitle
266 | _resolves_ `string`
267 |
268 | Resolves the document title of the loaded Webpage
269 |
270 | ### getElement
271 | _arguments_
272 | - selector (type: `string`)
273 |
274 | _resolves_ `ASTElement`
275 |
276 | Retrieves the html from the matched element and parses the returned outerHTML with `htmlCompiler` to return the corresponding `ASTElement`
277 |
278 | ### getElements
279 | _arguments_
280 | - selector (type: `string`)
281 |
282 | _resolves_ `Array`
283 |
284 | Retrieves the html from matched elements and parses the returned outerHTML with `htmlCompiler` to return an array of the corresponding `ASTElement`s
285 |
286 | ### getElementCount
287 | _arguments_
288 | - selector (type: `string`)
289 |
290 | _resolves_ `number`
291 |
292 | Retrieves the number of elements found
293 |
294 | ### getElementHtml
295 | _arguments_
296 | - selector (type: `string`)
297 |
298 | _resolves_ `string`
299 |
300 | Retrieves the outerHTML for the matched element
301 |
302 | ### getElementsHtml
303 | _arguments_
304 | - selector (type: `string`)
305 |
306 | _resolves_ `Array`
307 |
308 | Retrieves an array with the outerHTML of all matched elements
309 |
310 | ### getAttribute
311 | _arguments_
312 | - selector (type: `string`)
313 | - attribute (type: `string`)
314 |
315 | _resolves_ `string`
316 |
317 | Retrieves the value of the attribute for the matched element
318 |
319 | ### getAttributes
320 | _arguments_
321 | - selector (type: `string`)
322 | - attribute (type: `string`)
323 |
324 | _resolves_ `Array`
325 |
326 | Retrieves an array of the attribute values for the matched elements
327 |
328 | ### getText
329 | _arguments_
330 | - selector (type: `string`)
331 | - trim (type: `boolean`)
332 |
333 | _resolves_ `string`
334 |
335 | Retrieves the `textContent` for the matched element. If trim is true the textContent will be trimmed
336 |
337 | ### getTexts
338 | _arguments_
339 | - selector (type: `string`)
340 | - trim (type: `boolean`)
341 |
342 | _resolves_ `Array`
343 |
344 | Retrieves an array with the `textContent` of all matched elements. If trim is true the textContent's will be trimmed
345 |
346 | ### clickElement
347 | _arguments_
348 | - selector (type: `string`)
349 |
350 | _resolves_ `void`
351 |
352 | Calls click on the matched element
353 |
354 | ### runScript
355 | _arguments_
356 | - pageFunction (type: `Function`)
357 | - ...arguments (type: `any`)
358 |
359 | _returns `any`_
360 |
361 | Executes the synchronous pageFunction in the loaded webpage context. The pageFunction is parsed to a string using `@babel/parser`, then transpiled with `@babel/core/transform` for the specified browser version*. The function is reconstructed on the webpage using `new Function`.
362 | If you need to pass arguments from your test scope to the pageFunction, pass them as additional arguments to runScript. Make sure to only pass serializable variables (e.g. a Function is not).
363 |
364 | It returns whatever `pageFunction` returns
365 |
366 | *Please note that the syntax is transpiled but polyfills are not added automatically. Polyfills need to be already loaded on the webpage. In other words, dont use features in your test pageFunctions which you dont also use in production
367 |
368 | ### runAsyncScript
369 | _arguments_
370 | - pageFunction (type: `Function`)
371 | - ...arguments (type: `any`)
372 |
373 | _returns `any`_
374 |
375 | Does the same as `runScript` but pageFunction can be asynchronous
376 |
377 |
378 | ## Hooks
379 |
380 | ### `dependencies:load`
381 |
382 | Called before any browser dependency is loaded
383 |
384 | ### `dependencies:loaded`
385 |
386 | Called immediately after all browser dependencies are loaded
387 |
388 | ### `start:before`
389 |
390 | Called before the browser is started
391 |
392 | ### `start:after`
393 | _passed arguments_
394 | - driver (type: `object`, the browser driver instance)
395 |
396 | > When starting the browser failed this hook will not be called
397 |
398 | Called immediately after the browser has started
399 |
400 | ### `close:before`
401 |
402 | Called before the browser is closed
403 |
404 | ### `close:after`
405 |
406 | Called immediately after the browser was closed
407 |
408 | ### `page:before`
409 |
410 | Called before a new browser page is opened
411 |
412 | ### `page:created`
413 | _passed arguments_
414 | - page (type: `object`, the page instance)
415 |
416 | Called immediately after a new browser page is instantiated, but before navigation occures* and before the wait condition is triggered.
417 |
418 | *On puppeteer, on selenium instantiating and navigating arent separate actions
419 |
420 | ### `page:after`
421 | _passed arguments_
422 | - page (type: `object`, the page instance)
423 |
424 | Called immediately after a new browser page was opened
425 |
426 | ### `webpage.property`
427 | _passed arguments_
428 | - property (type: `string`)
429 |
430 | Called whenever a property from the page proxy is requested.
431 |
432 | ### `selenium:build:before` _Selenium browsers only_
433 | _passed arguments_
434 | - builder (type: `object`, the Selenium builder instance)
435 |
436 | Called just before the build is called on the Selenium builder to start the browser
437 |
438 | ### `selenium:build:options` _Selenium browsers only_
439 | _passed arguments_
440 | - options (type: `array`)
441 | - builder (type: `object`, the Selenium builder instance)
442 |
443 | Called just before browser specific options are applied
444 |
--------------------------------------------------------------------------------