-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
131 lines (122 loc) · 4.15 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import BrowserPageOpener from './lib/browser.js'
import JsdomPageOpener from './lib/jsdom.js'
import {OpenedPage} from './lib/types.js' // eslint-disable-line no-unused-vars
/**
* Enables tests to open an application's own page URLs both in the browser and
* in Node.js using jsdom.
*
* Usage:
*
* ```js
* import { afterEach, beforeAll, describe, expect, test } from 'vitest'
* import TestPageOpener from 'test-page-opener'
*
* describe('TestPageOpener', () => {
* let opener
*
* beforeAll(async () => {opener = await TestPageOpener.create('/basedir/')})
* afterEach(() => opener.closeAll())
*
* test('loads page with module successfully', async () => {
* const { document } = await opener.open('path/to/index.html')
* const appElem = document.querySelector('#app')
*
* expect(appElem).not.toBeNull()
* expect(appElem.textContent).toContain('Hello, World!')
* })
* })
* ```
*/
export default class TestPageOpener {
static #isConstructing = false
#basePath
#impl
/** @type {OpenedPage[]} */
#opened
/**
* Based on the private constructor idiom.
* @see https://developer.mozilla.org/docs/Web/JavaScript/Reference/Classes/Private_properties#simulating_private_constructors
* @access private
* @param {string} basePath - base path of the application under test
* @param {(BrowserPageOpener|JsdomPageOpener)} impl - either a browser or
* jsdom implementation for opening HTML pages
*/
constructor(basePath, impl) {
if (!TestPageOpener.#isConstructing) {
throw new Error('use TestPageOpener.create() instead')
}
if (!basePath.startsWith('/') || !basePath.endsWith('/')) {
const msg = 'basePath should start with \'/\' and end with \'/\''
throw new Error(`${msg}, got: "${basePath}"`)
}
this.#basePath = basePath
this.#impl = impl
this.#opened = []
TestPageOpener.#isConstructing = false
}
/**
* Creates a new TestPageOpener instance.
*
* Call this once for each test class or `describe()` block, e.g.:
*
* ```js
* let opener
* beforeAll(async () => {opener = await TestPageOpener.create('/basedir/')})
* ```
* @param {string} basePath - base path of the application under test; must
* start with '/' and end with '/'
* @returns {Promise<TestPageOpener>} - a new TestPageOpener initialized to
* open pages in the current test environment, either via jsdom or the
* browser
*/
static async create(basePath) {
const impl = globalThis.window ?
new BrowserPageOpener(globalThis.window) :
new JsdomPageOpener(await import('jsdom'))
TestPageOpener.#isConstructing = true
return new TestPageOpener(basePath, impl)
}
/**
* Opens a page using the current environment's implementation.
*
* The returned object contains the opened `window`, the fully loaded
* `document`, and a `close()` function for closing the window properly.
* Usually you will pull the `document` from the return value and call
* `opener.closeAll()` from `afterEach()`, e.g.:
*
* ```js
* const { document } = await opener.open('path/to/index.html')
* ```
* @param {string} pagePath - path to the HTML file relative to the basePath
* specified during `TestPageOpener.create()`
* @returns {Promise<OpenedPage>} - object representing the opened page
* @throws {Error} if pagePath is malformed or opening page failed
*/
async open(pagePath) {
if (pagePath.startsWith('/')) {
const msg = 'page path shouldn\'t start with \'/\''
throw new Error(`${msg}, got: "${pagePath}"`)
}
const page = await this.#impl.open(this.#basePath, pagePath)
this.#opened.push(page)
return page
}
/**
* Closes the window object for all currently opened pages.
*
* Call this from the teardown function after each test case, e.g.:
*
* ```js
* afterEach(() => opener.closeAll())
* ```
*/
closeAll() {
this.#opened.forEach(p => p.close())
this.#opened = []
}
}