@jsenv/core 24.3.3 → 24.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/executeTestPlan.js +7 -1
- package/src/internal/browser_launcher/from_playwright.js +314 -0
- package/src/internal/executing/executeConcurrently.js +10 -7
- package/src/internal/executing/executePlan.js +2 -0
- package/src/launchBrowser.js +33 -501
- package/src/internal/browser_launcher/createSharing.js +0 -70
package/package.json
CHANGED
package/src/executeTestPlan.js
CHANGED
|
@@ -36,11 +36,16 @@ export const executeTestPlan = async ({
|
|
|
36
36
|
cooldownBetweenExecutions,
|
|
37
37
|
|
|
38
38
|
maxExecutionsInParallel,
|
|
39
|
-
|
|
40
39
|
completedExecutionLogAbbreviation = false,
|
|
41
40
|
completedExecutionLogMerging = false,
|
|
42
41
|
logSummary = true,
|
|
43
42
|
updateProcessExitCode = true,
|
|
43
|
+
// stopAfterExecute: true to ensure runtime is stopped once executed
|
|
44
|
+
// because we have what we wants: execution is completed and
|
|
45
|
+
// we have associated coverage and capturedConsole
|
|
46
|
+
// passsing false means all node process and browsers launched stays opened
|
|
47
|
+
// (can eventually be used for debug)
|
|
48
|
+
stopAfterExecute = true,
|
|
44
49
|
|
|
45
50
|
coverage = process.argv.includes("--cover") ||
|
|
46
51
|
process.argv.includes("--coverage"),
|
|
@@ -145,6 +150,7 @@ export const executeTestPlan = async ({
|
|
|
145
150
|
|
|
146
151
|
defaultMsAllocatedPerExecution,
|
|
147
152
|
maxExecutionsInParallel,
|
|
153
|
+
stopAfterExecute,
|
|
148
154
|
cooldownBetweenExecutions,
|
|
149
155
|
completedExecutionLogMerging,
|
|
150
156
|
completedExecutionLogAbbreviation,
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
// https://github.com/microsoft/playwright/blob/master/docs/api.md
|
|
2
|
+
|
|
3
|
+
import { createDetailedMessage } from "@jsenv/logger"
|
|
4
|
+
import {
|
|
5
|
+
Abort,
|
|
6
|
+
createCallbackListNotifiedOnce,
|
|
7
|
+
createCallbackList,
|
|
8
|
+
raceProcessTeardownEvents,
|
|
9
|
+
} from "@jsenv/abort"
|
|
10
|
+
import { memoize } from "@jsenv/filesystem"
|
|
11
|
+
|
|
12
|
+
import { trackPageToNotify } from "./trackPageToNotify.js"
|
|
13
|
+
import { executeHtmlFile } from "./executeHtmlFile.js"
|
|
14
|
+
|
|
15
|
+
export const createRuntimeFromPlaywright = ({
|
|
16
|
+
browserName,
|
|
17
|
+
browserVersion,
|
|
18
|
+
coveragePlaywrightAPIAvailable = false,
|
|
19
|
+
ignoreErrorHook = () => false,
|
|
20
|
+
transformErrorHook = (error) => error,
|
|
21
|
+
tab = false,
|
|
22
|
+
}) => {
|
|
23
|
+
const runtime = {
|
|
24
|
+
name: browserName,
|
|
25
|
+
version: browserVersion,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let browserAndContextPromise
|
|
29
|
+
runtime.launch = async ({
|
|
30
|
+
signal = new AbortController().signal,
|
|
31
|
+
executablePath,
|
|
32
|
+
browserServerLogLevel,
|
|
33
|
+
|
|
34
|
+
projectDirectoryUrl,
|
|
35
|
+
compileServerOrigin,
|
|
36
|
+
outDirectoryRelativeUrl,
|
|
37
|
+
|
|
38
|
+
collectPerformance,
|
|
39
|
+
measurePerformance,
|
|
40
|
+
collectCoverage,
|
|
41
|
+
coverageIgnorePredicate,
|
|
42
|
+
coverageForceIstanbul,
|
|
43
|
+
|
|
44
|
+
headless = true,
|
|
45
|
+
stopOnExit = true,
|
|
46
|
+
ignoreHTTPSErrors = true,
|
|
47
|
+
stopAfterAllExecutionCallbackList,
|
|
48
|
+
}) => {
|
|
49
|
+
const stopCallbackList = createCallbackListNotifiedOnce()
|
|
50
|
+
const stoppedCallbackList = createCallbackListNotifiedOnce()
|
|
51
|
+
const errorCallbackList = createCallbackList()
|
|
52
|
+
const outputCallbackList = createCallbackList()
|
|
53
|
+
|
|
54
|
+
const stop = memoize(async (reason) => {
|
|
55
|
+
await stopCallbackList.notify({ reason })
|
|
56
|
+
stoppedCallbackList.notify({ reason })
|
|
57
|
+
return { graceful: false }
|
|
58
|
+
})
|
|
59
|
+
const closeBrowser = async () => {
|
|
60
|
+
const { browser } = await browserAndContextPromise
|
|
61
|
+
browserAndContextPromise = null
|
|
62
|
+
await stopBrowser(browser)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (
|
|
66
|
+
!browserAndContextPromise ||
|
|
67
|
+
!tab ||
|
|
68
|
+
!stopAfterAllExecutionCallbackList
|
|
69
|
+
) {
|
|
70
|
+
browserAndContextPromise = (async () => {
|
|
71
|
+
const browser = await launchBrowserUsingPlaywright({
|
|
72
|
+
signal,
|
|
73
|
+
browserName,
|
|
74
|
+
stopOnExit,
|
|
75
|
+
playwrightOptions: {
|
|
76
|
+
headless,
|
|
77
|
+
executablePath,
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
const browserContext = await browser.newContext({ ignoreHTTPSErrors })
|
|
81
|
+
return { browser, browserContext }
|
|
82
|
+
})()
|
|
83
|
+
|
|
84
|
+
// when using chromium tab during multiple executions we reuse the chromium browser
|
|
85
|
+
// and only once all executions are done we close the browser
|
|
86
|
+
if (tab && stopAfterAllExecutionCallbackList) {
|
|
87
|
+
stopAfterAllExecutionCallbackList.add(async () => {
|
|
88
|
+
await closeBrowser()
|
|
89
|
+
})
|
|
90
|
+
} else {
|
|
91
|
+
stopCallbackList.add(async () => {
|
|
92
|
+
await closeBrowser()
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const { browser, browserContext } = await browserAndContextPromise
|
|
98
|
+
// https://github.com/GoogleChrome/puppeteer/blob/v1.4.0/docs/api.md#event-disconnected
|
|
99
|
+
browser.on("disconnected", () => {
|
|
100
|
+
stop()
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
const page = await browserContext.newPage()
|
|
104
|
+
stoppedCallbackList.add(async () => {
|
|
105
|
+
try {
|
|
106
|
+
await page.close()
|
|
107
|
+
} catch (e) {
|
|
108
|
+
if (isTargetClosedError(e)) {
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
throw e
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
const stopTrackingToNotify = trackPageToNotify(page, {
|
|
115
|
+
onError: (error) => {
|
|
116
|
+
error = transformErrorHook(error)
|
|
117
|
+
if (!ignoreErrorHook(error)) {
|
|
118
|
+
errorCallbackList.notify(error)
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
onConsole: outputCallbackList.notify,
|
|
122
|
+
})
|
|
123
|
+
stoppedCallbackList.add(stopTrackingToNotify)
|
|
124
|
+
|
|
125
|
+
const execute = createExecuteHook({
|
|
126
|
+
page,
|
|
127
|
+
runtime,
|
|
128
|
+
browserServerLogLevel,
|
|
129
|
+
|
|
130
|
+
projectDirectoryUrl,
|
|
131
|
+
compileServerOrigin,
|
|
132
|
+
outDirectoryRelativeUrl,
|
|
133
|
+
|
|
134
|
+
collectPerformance,
|
|
135
|
+
measurePerformance,
|
|
136
|
+
collectCoverage,
|
|
137
|
+
coverageIgnorePredicate,
|
|
138
|
+
coverageForceIstanbul,
|
|
139
|
+
|
|
140
|
+
coveragePlaywrightAPIAvailable,
|
|
141
|
+
transformErrorHook,
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
stopCallbackList,
|
|
146
|
+
stoppedCallbackList,
|
|
147
|
+
errorCallbackList,
|
|
148
|
+
outputCallbackList,
|
|
149
|
+
execute,
|
|
150
|
+
stop,
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (!tab) {
|
|
154
|
+
runtime.tab = createRuntimeFromPlaywright({
|
|
155
|
+
browserName,
|
|
156
|
+
browserVersion,
|
|
157
|
+
coveragePlaywrightAPIAvailable,
|
|
158
|
+
ignoreErrorHook,
|
|
159
|
+
transformErrorHook,
|
|
160
|
+
tab: true,
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
return runtime
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const stopBrowser = async (browser) => {
|
|
167
|
+
const disconnected = browser.isConnected()
|
|
168
|
+
? new Promise((resolve) => {
|
|
169
|
+
const disconnectedCallback = () => {
|
|
170
|
+
browser.removeListener("disconnected", disconnectedCallback)
|
|
171
|
+
resolve()
|
|
172
|
+
}
|
|
173
|
+
browser.on("disconnected", disconnectedCallback)
|
|
174
|
+
})
|
|
175
|
+
: Promise.resolve()
|
|
176
|
+
|
|
177
|
+
// for some reason without this 100ms timeout
|
|
178
|
+
// browser.close() never resolves (playwright does not like something)
|
|
179
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
await browser.close()
|
|
183
|
+
} catch (e) {
|
|
184
|
+
if (isTargetClosedError(e)) {
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
throw e
|
|
188
|
+
}
|
|
189
|
+
await disconnected
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const launchBrowserUsingPlaywright = async ({
|
|
193
|
+
signal,
|
|
194
|
+
browserName,
|
|
195
|
+
stopOnExit,
|
|
196
|
+
playwrightOptions,
|
|
197
|
+
}) => {
|
|
198
|
+
const launchBrowserOperation = Abort.startOperation()
|
|
199
|
+
launchBrowserOperation.addAbortSignal(signal)
|
|
200
|
+
const playwright = await importPlaywright({ browserName })
|
|
201
|
+
if (stopOnExit) {
|
|
202
|
+
launchBrowserOperation.addAbortSource((abort) => {
|
|
203
|
+
return raceProcessTeardownEvents(
|
|
204
|
+
{
|
|
205
|
+
SIGHUP: true,
|
|
206
|
+
SIGTERM: true,
|
|
207
|
+
SIGINT: true,
|
|
208
|
+
beforeExit: true,
|
|
209
|
+
exit: true,
|
|
210
|
+
},
|
|
211
|
+
abort,
|
|
212
|
+
)
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const browserClass = playwright[browserName]
|
|
217
|
+
try {
|
|
218
|
+
const browser = await browserClass.launch({
|
|
219
|
+
...playwrightOptions,
|
|
220
|
+
// let's handle them to close properly browser + remove listener
|
|
221
|
+
// instead of relying on playwright to do so
|
|
222
|
+
handleSIGINT: false,
|
|
223
|
+
handleSIGTERM: false,
|
|
224
|
+
handleSIGHUP: false,
|
|
225
|
+
})
|
|
226
|
+
launchBrowserOperation.throwIfAborted()
|
|
227
|
+
return browser
|
|
228
|
+
} catch (e) {
|
|
229
|
+
if (launchBrowserOperation.signal.aborted && isTargetClosedError(e)) {
|
|
230
|
+
// rethrow the abort error
|
|
231
|
+
launchBrowserOperation.throwIfAborted()
|
|
232
|
+
}
|
|
233
|
+
throw e
|
|
234
|
+
} finally {
|
|
235
|
+
await launchBrowserOperation.end()
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const importPlaywright = async ({ browserName }) => {
|
|
240
|
+
try {
|
|
241
|
+
const namespace = await import("playwright")
|
|
242
|
+
return namespace
|
|
243
|
+
} catch (e) {
|
|
244
|
+
if (e.code === "ERR_MODULE_NOT_FOUND") {
|
|
245
|
+
throw new Error(
|
|
246
|
+
createDetailedMessage(
|
|
247
|
+
`"playwright" not found. You need playwright in your dependencies when using "${browserName}Runtime"`,
|
|
248
|
+
{
|
|
249
|
+
suggestion: `npm install --save-dev playwright`,
|
|
250
|
+
},
|
|
251
|
+
),
|
|
252
|
+
{ cause: e },
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
throw e
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const createExecuteHook = ({
|
|
260
|
+
page,
|
|
261
|
+
runtime,
|
|
262
|
+
projectDirectoryUrl,
|
|
263
|
+
compileServerOrigin,
|
|
264
|
+
compileServerId,
|
|
265
|
+
outDirectoryRelativeUrl,
|
|
266
|
+
|
|
267
|
+
collectPerformance,
|
|
268
|
+
measurePerformance,
|
|
269
|
+
collectCoverage,
|
|
270
|
+
coverageIgnorePredicate,
|
|
271
|
+
coverageForceIstanbul,
|
|
272
|
+
|
|
273
|
+
coveragePlaywrightAPIAvailable,
|
|
274
|
+
transformErrorHook,
|
|
275
|
+
}) => {
|
|
276
|
+
const execute = async ({ signal, fileRelativeUrl }) => {
|
|
277
|
+
const executeOperation = Abort.startOperation()
|
|
278
|
+
executeOperation.addAbortSignal(signal)
|
|
279
|
+
executeOperation.throwIfAborted()
|
|
280
|
+
const result = await executeHtmlFile(fileRelativeUrl, {
|
|
281
|
+
runtime,
|
|
282
|
+
executeOperation,
|
|
283
|
+
|
|
284
|
+
projectDirectoryUrl,
|
|
285
|
+
compileServerOrigin,
|
|
286
|
+
compileServerId,
|
|
287
|
+
outDirectoryRelativeUrl,
|
|
288
|
+
|
|
289
|
+
page,
|
|
290
|
+
measurePerformance,
|
|
291
|
+
collectPerformance,
|
|
292
|
+
collectCoverage,
|
|
293
|
+
coverageForceIstanbul,
|
|
294
|
+
coveragePlaywrightAPIAvailable,
|
|
295
|
+
coverageIgnorePredicate,
|
|
296
|
+
transformErrorHook,
|
|
297
|
+
})
|
|
298
|
+
return result
|
|
299
|
+
}
|
|
300
|
+
return execute
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const isTargetClosedError = (error) => {
|
|
304
|
+
if (error.message.match(/Protocol error \(.*?\): Target closed/)) {
|
|
305
|
+
return true
|
|
306
|
+
}
|
|
307
|
+
if (error.message.match(/Protocol error \(.*?\): Browser.*?closed/)) {
|
|
308
|
+
return true
|
|
309
|
+
}
|
|
310
|
+
if (error.message.includes("browserContext.close: Browser closed")) {
|
|
311
|
+
return true
|
|
312
|
+
}
|
|
313
|
+
return false
|
|
314
|
+
}
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
normalizeStructuredMetaMap,
|
|
12
12
|
urlToMeta,
|
|
13
13
|
} from "@jsenv/filesystem"
|
|
14
|
-
import { Abort } from "@jsenv/abort"
|
|
14
|
+
import { Abort, createCallbackListNotifiedOnce } from "@jsenv/abort"
|
|
15
15
|
|
|
16
16
|
import { launchAndExecute } from "../executing/launchAndExecute.js"
|
|
17
17
|
import { reportToCoverage } from "./coverage/reportToCoverage.js"
|
|
@@ -34,6 +34,7 @@ export const executeConcurrently = async (
|
|
|
34
34
|
defaultMsAllocatedPerExecution = 30000,
|
|
35
35
|
cooldownBetweenExecutions = 0,
|
|
36
36
|
maxExecutionsInParallel = 1,
|
|
37
|
+
stopAfterExecute,
|
|
37
38
|
completedExecutionLogMerging,
|
|
38
39
|
completedExecutionLogAbbreviation,
|
|
39
40
|
|
|
@@ -137,6 +138,8 @@ export const executeConcurrently = async (
|
|
|
137
138
|
let timedoutCount = 0
|
|
138
139
|
let erroredCount = 0
|
|
139
140
|
let completedCount = 0
|
|
141
|
+
const stopAfterAllExecutionCallbackList = createCallbackListNotifiedOnce()
|
|
142
|
+
|
|
140
143
|
const executionsDone = await executeInParallel({
|
|
141
144
|
multipleExecutionsOperation,
|
|
142
145
|
maxExecutionsInParallel,
|
|
@@ -153,12 +156,7 @@ export const executeConcurrently = async (
|
|
|
153
156
|
measurePerformance: false,
|
|
154
157
|
collectPerformance: false,
|
|
155
158
|
captureConsole: true,
|
|
156
|
-
|
|
157
|
-
// because we have what we wants: execution is completed and
|
|
158
|
-
// we have associated coverage and capturedConsole
|
|
159
|
-
// passsing false means all node process and browsers launched stays opened
|
|
160
|
-
// (can eventually be used for debug)
|
|
161
|
-
stopAfterExecute: true,
|
|
159
|
+
stopAfterExecute,
|
|
162
160
|
stopAfterExecuteReason: "execution-done",
|
|
163
161
|
allocatedMs: defaultMsAllocatedPerExecution,
|
|
164
162
|
...paramsFromStep,
|
|
@@ -212,6 +210,7 @@ export const executeConcurrently = async (
|
|
|
212
210
|
collectCoverage: coverage,
|
|
213
211
|
coverageIgnorePredicate,
|
|
214
212
|
coverageForceIstanbul,
|
|
213
|
+
stopAfterAllExecutionCallbackList,
|
|
215
214
|
...executionParams.runtimeParams,
|
|
216
215
|
},
|
|
217
216
|
executeParams: {
|
|
@@ -283,6 +282,10 @@ export const executeConcurrently = async (
|
|
|
283
282
|
},
|
|
284
283
|
})
|
|
285
284
|
|
|
285
|
+
if (stopAfterExecute) {
|
|
286
|
+
stopAfterAllExecutionCallbackList.notify()
|
|
287
|
+
}
|
|
288
|
+
|
|
286
289
|
const summaryCounts = reportToSummary(report)
|
|
287
290
|
|
|
288
291
|
const summary = {
|
|
@@ -26,6 +26,7 @@ export const executePlan = async (
|
|
|
26
26
|
|
|
27
27
|
defaultMsAllocatedPerExecution,
|
|
28
28
|
maxExecutionsInParallel,
|
|
29
|
+
stopAfterExecute,
|
|
29
30
|
cooldownBetweenExecutions,
|
|
30
31
|
completedExecutionLogMerging,
|
|
31
32
|
completedExecutionLogAbbreviation,
|
|
@@ -156,6 +157,7 @@ export const executePlan = async (
|
|
|
156
157
|
|
|
157
158
|
defaultMsAllocatedPerExecution,
|
|
158
159
|
maxExecutionsInParallel,
|
|
160
|
+
stopAfterExecute,
|
|
159
161
|
cooldownBetweenExecutions,
|
|
160
162
|
completedExecutionLogMerging,
|
|
161
163
|
completedExecutionLogAbbreviation,
|
package/src/launchBrowser.js
CHANGED
|
@@ -1,511 +1,43 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { createDetailedMessage } from "@jsenv/logger"
|
|
4
|
-
import {
|
|
5
|
-
Abort,
|
|
6
|
-
createCallbackListNotifiedOnce,
|
|
7
|
-
createCallbackList,
|
|
8
|
-
raceProcessTeardownEvents,
|
|
9
|
-
} from "@jsenv/abort"
|
|
10
|
-
import { memoize } from "@jsenv/filesystem"
|
|
11
|
-
|
|
12
|
-
import { fetchUrl } from "./internal/fetchUrl.js"
|
|
13
|
-
import { validateResponse } from "./internal/response_validation.js"
|
|
14
|
-
import { trackPageToNotify } from "./internal/browser_launcher/trackPageToNotify.js"
|
|
15
|
-
import { createSharing } from "./internal/browser_launcher/createSharing.js"
|
|
16
|
-
import { executeHtmlFile } from "./internal/browser_launcher/executeHtmlFile.js"
|
|
1
|
+
import { createRuntimeFromPlaywright } from "@jsenv/core/src/internal/browser_launcher/from_playwright.js"
|
|
17
2
|
import {
|
|
18
3
|
PLAYWRIGHT_CHROMIUM_VERSION,
|
|
19
4
|
PLAYWRIGHT_FIREFOX_VERSION,
|
|
20
5
|
PLAYWRIGHT_WEBKIT_VERSION,
|
|
21
6
|
} from "./playwright_browser_versions.js"
|
|
22
7
|
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
debug = false,
|
|
47
|
-
debugPort = 0,
|
|
48
|
-
stopOnExit = true,
|
|
49
|
-
share = false,
|
|
50
|
-
}) => {
|
|
51
|
-
const launchBrowserOperation = Abort.startOperation()
|
|
52
|
-
launchBrowserOperation.addAbortSignal(signal)
|
|
53
|
-
|
|
54
|
-
const sharingToken = share
|
|
55
|
-
? chromiumSharing.getSharingToken({
|
|
56
|
-
chromiumExecutablePath,
|
|
57
|
-
headless,
|
|
58
|
-
debug,
|
|
59
|
-
debugPort,
|
|
60
|
-
})
|
|
61
|
-
: chromiumSharing.getUniqueSharingToken()
|
|
62
|
-
if (!sharingToken.isUsed()) {
|
|
63
|
-
const { chromium } = await importPlaywright({ browserName: "chromium" })
|
|
64
|
-
const launchOperation = launchBrowser("chromium", {
|
|
65
|
-
browserClass: chromium,
|
|
66
|
-
launchBrowserOperation,
|
|
67
|
-
options: {
|
|
68
|
-
headless,
|
|
69
|
-
executablePath: chromiumExecutablePath,
|
|
70
|
-
...(debug ? { devtools: true } : {}),
|
|
71
|
-
args: [
|
|
72
|
-
// https://github.com/GoogleChrome/puppeteer/issues/1834
|
|
73
|
-
// https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md#tips
|
|
74
|
-
// "--disable-dev-shm-usage",
|
|
75
|
-
...(debug ? [`--remote-debugging-port=${debugPort}`] : []),
|
|
76
|
-
],
|
|
77
|
-
},
|
|
78
|
-
stopOnExit,
|
|
79
|
-
})
|
|
80
|
-
sharingToken.setSharedValue(launchOperation)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const [browserPromise, stopUsingBrowser] = sharingToken.useSharedValue()
|
|
84
|
-
launchBrowserOperation.addEndCallback(stopUsingBrowser)
|
|
85
|
-
const browser = await browserPromise
|
|
86
|
-
|
|
87
|
-
if (debug) {
|
|
88
|
-
// https://github.com/puppeteer/puppeteer/blob/v2.0.0/docs/api.md#browserwsendpoint
|
|
89
|
-
// https://chromedevtools.github.io/devtools-protocol/#how-do-i-access-the-browser-target
|
|
90
|
-
const webSocketEndpoint = browser.wsEndpoint()
|
|
91
|
-
const webSocketUrl = new URL(webSocketEndpoint)
|
|
92
|
-
const browserEndpoint = `http://${webSocketUrl.host}/json/version`
|
|
93
|
-
const browserResponse = await fetchUrl(browserEndpoint, {
|
|
94
|
-
signal,
|
|
95
|
-
ignoreHttpsError: true,
|
|
96
|
-
})
|
|
97
|
-
const { isValid, message, details } = await validateResponse(
|
|
98
|
-
browserResponse,
|
|
99
|
-
)
|
|
100
|
-
if (!isValid) {
|
|
101
|
-
throw new Error(createDetailedMessage(message, details))
|
|
8
|
+
export const chromiumRuntime = createRuntimeFromPlaywright({
|
|
9
|
+
browserName: "chromium",
|
|
10
|
+
browserVersion: PLAYWRIGHT_CHROMIUM_VERSION,
|
|
11
|
+
coveragePlaywrightAPIAvailable: true,
|
|
12
|
+
})
|
|
13
|
+
export const chromiumTabRuntime = chromiumRuntime.tab
|
|
14
|
+
|
|
15
|
+
export const firefoxRuntime = createRuntimeFromPlaywright({
|
|
16
|
+
browserName: "firefox",
|
|
17
|
+
browserVersion: PLAYWRIGHT_FIREFOX_VERSION,
|
|
18
|
+
})
|
|
19
|
+
export const firefoxTabRuntime = firefoxRuntime.tab
|
|
20
|
+
|
|
21
|
+
export const webkitRuntime = createRuntimeFromPlaywright({
|
|
22
|
+
browserName: "webkit",
|
|
23
|
+
browserVersion: PLAYWRIGHT_WEBKIT_VERSION,
|
|
24
|
+
ignoreErrorHook: (error) => {
|
|
25
|
+
// we catch error during execution but safari throw unhandled rejection
|
|
26
|
+
// in a non-deterministic way.
|
|
27
|
+
// I suppose it's due to some race condition to decide if the promise is catched or not
|
|
28
|
+
// for now we'll ignore unhandled rejection on wekbkit
|
|
29
|
+
if (error.name === "Unhandled Promise Rejection") {
|
|
30
|
+
return true
|
|
102
31
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const browserHooks = browserToRuntimeHooks(browser, {
|
|
110
|
-
runtime: chromiumRuntime,
|
|
111
|
-
browserServerLogLevel,
|
|
112
|
-
|
|
113
|
-
projectDirectoryUrl,
|
|
114
|
-
compileServerOrigin,
|
|
115
|
-
compileServerId,
|
|
116
|
-
outDirectoryRelativeUrl,
|
|
117
|
-
|
|
118
|
-
collectPerformance,
|
|
119
|
-
measurePerformance,
|
|
120
|
-
collectCoverage,
|
|
121
|
-
coverageIgnorePredicate,
|
|
122
|
-
coverageForceIstanbul,
|
|
123
|
-
coveragePlaywrightAPIAvailable: true,
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
return {
|
|
127
|
-
browser,
|
|
128
|
-
...browserHooks,
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
export const chromiumTabRuntime = {
|
|
132
|
-
...chromiumRuntime,
|
|
133
|
-
launch: (params) =>
|
|
134
|
-
chromiumRuntime.launch({
|
|
135
|
-
shared: true,
|
|
136
|
-
...params,
|
|
137
|
-
}),
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const firefoxSharing = createSharing()
|
|
141
|
-
export const firefoxRuntime = {
|
|
142
|
-
name: "firefox",
|
|
143
|
-
version: PLAYWRIGHT_FIREFOX_VERSION,
|
|
144
|
-
}
|
|
145
|
-
firefoxRuntime.launch = async ({
|
|
146
|
-
signal = new AbortController().signal,
|
|
147
|
-
firefoxExecutablePath,
|
|
148
|
-
browserServerLogLevel,
|
|
149
|
-
|
|
150
|
-
projectDirectoryUrl,
|
|
151
|
-
compileServerOrigin,
|
|
152
|
-
outDirectoryRelativeUrl,
|
|
153
|
-
|
|
154
|
-
collectPerformance,
|
|
155
|
-
measurePerformance,
|
|
156
|
-
collectCoverage,
|
|
157
|
-
coverageIgnorePredicate,
|
|
158
|
-
coverageForceIstanbul,
|
|
159
|
-
|
|
160
|
-
headless = true,
|
|
161
|
-
stopOnExit = true,
|
|
162
|
-
share = false,
|
|
163
|
-
}) => {
|
|
164
|
-
const launchBrowserOperation = Abort.startOperation()
|
|
165
|
-
launchBrowserOperation.addAbortSignal(signal)
|
|
166
|
-
|
|
167
|
-
const sharingToken = share
|
|
168
|
-
? firefoxSharing.getSharingToken({ firefoxExecutablePath, headless })
|
|
169
|
-
: firefoxSharing.getUniqueSharingToken()
|
|
170
|
-
if (!sharingToken.isUsed()) {
|
|
171
|
-
const { firefox } = await importPlaywright({ browserName: "firefox" })
|
|
172
|
-
const launchOperation = launchBrowser("firefox", {
|
|
173
|
-
browserClass: firefox,
|
|
174
|
-
|
|
175
|
-
launchBrowserOperation,
|
|
176
|
-
options: {
|
|
177
|
-
headless,
|
|
178
|
-
executablePath: firefoxExecutablePath,
|
|
179
|
-
},
|
|
180
|
-
stopOnExit,
|
|
181
|
-
})
|
|
182
|
-
sharingToken.setSharedValue(launchOperation)
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const [browserPromise, stopUsingBrowser] = sharingToken.useSharedValue()
|
|
186
|
-
launchBrowserOperation.addEndCallback(stopUsingBrowser)
|
|
187
|
-
const browser = await browserPromise
|
|
188
|
-
|
|
189
|
-
const browserHooks = browserToRuntimeHooks(browser, {
|
|
190
|
-
runtime: firefoxRuntime,
|
|
191
|
-
launchBrowserOperation,
|
|
192
|
-
browserServerLogLevel,
|
|
193
|
-
|
|
194
|
-
projectDirectoryUrl,
|
|
195
|
-
compileServerOrigin,
|
|
196
|
-
outDirectoryRelativeUrl,
|
|
197
|
-
|
|
198
|
-
collectPerformance,
|
|
199
|
-
measurePerformance,
|
|
200
|
-
collectCoverage,
|
|
201
|
-
coverageIgnorePredicate,
|
|
202
|
-
coverageForceIstanbul,
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
return {
|
|
206
|
-
browser,
|
|
207
|
-
...browserHooks,
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
export const firefoxTabRuntime = {
|
|
211
|
-
...firefoxRuntime,
|
|
212
|
-
launch: (params) =>
|
|
213
|
-
firefoxRuntime.launch({
|
|
214
|
-
shared: true,
|
|
215
|
-
...params,
|
|
216
|
-
}),
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const webkitSharing = createSharing()
|
|
220
|
-
export const webkitRuntime = {
|
|
221
|
-
name: "webkit",
|
|
222
|
-
version: PLAYWRIGHT_WEBKIT_VERSION,
|
|
223
|
-
}
|
|
224
|
-
webkitRuntime.launch = async ({
|
|
225
|
-
signal = new AbortController().signal,
|
|
226
|
-
browserServerLogLevel,
|
|
227
|
-
webkitExecutablePath,
|
|
228
|
-
|
|
229
|
-
projectDirectoryUrl,
|
|
230
|
-
compileServerOrigin,
|
|
231
|
-
outDirectoryRelativeUrl,
|
|
232
|
-
|
|
233
|
-
collectPerformance,
|
|
234
|
-
measurePerformance,
|
|
235
|
-
collectCoverage,
|
|
236
|
-
coverageIgnorePredicate,
|
|
237
|
-
coverageForceIstanbul,
|
|
238
|
-
|
|
239
|
-
headless = true,
|
|
240
|
-
stopOnExit = true,
|
|
241
|
-
share = false,
|
|
242
|
-
}) => {
|
|
243
|
-
const launchBrowserOperation = Abort.startOperation()
|
|
244
|
-
launchBrowserOperation.addAbortSignal(signal)
|
|
245
|
-
|
|
246
|
-
const sharingToken = share
|
|
247
|
-
? webkitSharing.getSharingToken({ webkitExecutablePath, headless })
|
|
248
|
-
: webkitSharing.getUniqueSharingToken()
|
|
249
|
-
|
|
250
|
-
if (!sharingToken.isUsed()) {
|
|
251
|
-
const { webkit } = await await importPlaywright({ browserName: "webkit" })
|
|
252
|
-
const launchOperation = launchBrowser("webkit", {
|
|
253
|
-
browserClass: webkit,
|
|
254
|
-
launchBrowserOperation,
|
|
255
|
-
options: {
|
|
256
|
-
headless,
|
|
257
|
-
executablePath: webkitExecutablePath,
|
|
258
|
-
},
|
|
259
|
-
stopOnExit,
|
|
260
|
-
})
|
|
261
|
-
sharingToken.setSharedValue(launchOperation)
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const [browserPromise, stopUsingBrowser] = sharingToken.useSharedValue()
|
|
265
|
-
launchBrowserOperation.addEndCallback(stopUsingBrowser)
|
|
266
|
-
const browser = await browserPromise
|
|
267
|
-
|
|
268
|
-
const browserHooks = browserToRuntimeHooks(browser, {
|
|
269
|
-
runtime: webkitRuntime,
|
|
270
|
-
launchBrowserOperation,
|
|
271
|
-
browserServerLogLevel,
|
|
272
|
-
|
|
273
|
-
projectDirectoryUrl,
|
|
274
|
-
compileServerOrigin,
|
|
275
|
-
outDirectoryRelativeUrl,
|
|
276
|
-
|
|
277
|
-
collectPerformance,
|
|
278
|
-
measurePerformance,
|
|
279
|
-
collectCoverage,
|
|
280
|
-
coverageIgnorePredicate,
|
|
281
|
-
coverageForceIstanbul,
|
|
282
|
-
ignoreErrorHook: (error) => {
|
|
283
|
-
// we catch error during execution but safari throw unhandled rejection
|
|
284
|
-
// in a non-deterministic way.
|
|
285
|
-
// I suppose it's due to some race condition to decide if the promise is catched or not
|
|
286
|
-
// for now we'll ignore unhandled rejection on wekbkit
|
|
287
|
-
if (error.name === "Unhandled Promise Rejection") {
|
|
288
|
-
return true
|
|
289
|
-
}
|
|
290
|
-
return false
|
|
291
|
-
},
|
|
292
|
-
transformErrorHook: (error) => {
|
|
293
|
-
// Force error stack to contain the error message
|
|
294
|
-
// because it's not the case on webkit
|
|
295
|
-
error.stack = `${error.message}
|
|
32
|
+
return false
|
|
33
|
+
},
|
|
34
|
+
transformErrorHook: (error) => {
|
|
35
|
+
// Force error stack to contain the error message
|
|
36
|
+
// because it's not the case on webkit
|
|
37
|
+
error.stack = `${error.message}
|
|
296
38
|
at ${error.stack}`
|
|
297
39
|
|
|
298
|
-
|
|
299
|
-
},
|
|
300
|
-
})
|
|
301
|
-
|
|
302
|
-
return {
|
|
303
|
-
browser,
|
|
304
|
-
...browserHooks,
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
export const webkitTabRuntime = {
|
|
308
|
-
...webkitRuntime,
|
|
309
|
-
launch: (params) =>
|
|
310
|
-
webkitRuntime.launch({
|
|
311
|
-
shared: true,
|
|
312
|
-
...params,
|
|
313
|
-
}),
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const launchBrowser = async (
|
|
317
|
-
browserName,
|
|
318
|
-
{ launchBrowserOperation, browserClass, options, stopOnExit },
|
|
319
|
-
) => {
|
|
320
|
-
if (stopOnExit) {
|
|
321
|
-
launchBrowserOperation.addAbortSource((abort) => {
|
|
322
|
-
return raceProcessTeardownEvents(
|
|
323
|
-
{
|
|
324
|
-
SIGHUP: true,
|
|
325
|
-
SIGTERM: true,
|
|
326
|
-
SIGINT: true,
|
|
327
|
-
beforeExit: true,
|
|
328
|
-
exit: true,
|
|
329
|
-
},
|
|
330
|
-
abort,
|
|
331
|
-
)
|
|
332
|
-
})
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
try {
|
|
336
|
-
const browser = await browserClass.launch({
|
|
337
|
-
...options,
|
|
338
|
-
// let's handle them to close properly browser + remove listener
|
|
339
|
-
// instead of relying on playwright to do so
|
|
340
|
-
handleSIGINT: false,
|
|
341
|
-
handleSIGTERM: false,
|
|
342
|
-
handleSIGHUP: false,
|
|
343
|
-
})
|
|
344
|
-
launchBrowserOperation.throwIfAborted()
|
|
345
|
-
return browser
|
|
346
|
-
} catch (e) {
|
|
347
|
-
if (launchBrowserOperation.signal.aborted && isTargetClosedError(e)) {
|
|
348
|
-
// rethrow the abort error
|
|
349
|
-
launchBrowserOperation.throwIfAborted()
|
|
350
|
-
}
|
|
351
|
-
throw e
|
|
352
|
-
} finally {
|
|
353
|
-
await launchBrowserOperation.end()
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const importPlaywright = async ({ browserName }) => {
|
|
358
|
-
try {
|
|
359
|
-
const namespace = await import("playwright")
|
|
360
|
-
return namespace
|
|
361
|
-
} catch (e) {
|
|
362
|
-
if (e.code === "ERR_MODULE_NOT_FOUND") {
|
|
363
|
-
throw new Error(
|
|
364
|
-
createDetailedMessage(
|
|
365
|
-
`"playwright" not found. You need playwright in your dependencies when using "${browserName}Runtime"`,
|
|
366
|
-
{
|
|
367
|
-
suggestion: `npm install --save-dev playwright`,
|
|
368
|
-
},
|
|
369
|
-
),
|
|
370
|
-
{ cause: e },
|
|
371
|
-
)
|
|
372
|
-
}
|
|
373
|
-
throw e
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
const stopBrowser = async (browser) => {
|
|
378
|
-
const disconnected = browser.isConnected()
|
|
379
|
-
? new Promise((resolve) => {
|
|
380
|
-
const disconnectedCallback = () => {
|
|
381
|
-
browser.removeListener("disconnected", disconnectedCallback)
|
|
382
|
-
resolve()
|
|
383
|
-
}
|
|
384
|
-
browser.on("disconnected", disconnectedCallback)
|
|
385
|
-
})
|
|
386
|
-
: Promise.resolve()
|
|
387
|
-
|
|
388
|
-
// for some reason without this 100ms timeout
|
|
389
|
-
// browser.close() never resolves (playwright does not like something)
|
|
390
|
-
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
391
|
-
|
|
392
|
-
await browser.close()
|
|
393
|
-
await disconnected
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
const browserToRuntimeHooks = (
|
|
397
|
-
browser,
|
|
398
|
-
{
|
|
399
|
-
runtime,
|
|
400
|
-
projectDirectoryUrl,
|
|
401
|
-
compileServerOrigin,
|
|
402
|
-
compileServerId,
|
|
403
|
-
outDirectoryRelativeUrl,
|
|
404
|
-
|
|
405
|
-
collectPerformance,
|
|
406
|
-
measurePerformance,
|
|
407
|
-
collectCoverage,
|
|
408
|
-
coverageIgnorePredicate,
|
|
409
|
-
coverageForceIstanbul,
|
|
410
|
-
coveragePlaywrightAPIAvailable = false,
|
|
411
|
-
ignoreErrorHook = () => false,
|
|
412
|
-
transformErrorHook = (error) => error,
|
|
40
|
+
return error
|
|
413
41
|
},
|
|
414
|
-
)
|
|
415
|
-
|
|
416
|
-
const stoppedCallbackList = createCallbackListNotifiedOnce()
|
|
417
|
-
const stop = memoize(async (reason) => {
|
|
418
|
-
await stopCallbackList.notify({ reason })
|
|
419
|
-
stoppedCallbackList.notify({ reason })
|
|
420
|
-
return { graceful: false }
|
|
421
|
-
})
|
|
422
|
-
|
|
423
|
-
// https://github.com/GoogleChrome/puppeteer/blob/v1.4.0/docs/api.md#event-disconnected
|
|
424
|
-
browser.on("disconnected", () => {
|
|
425
|
-
stop()
|
|
426
|
-
})
|
|
427
|
-
|
|
428
|
-
stopCallbackList.add(async () => {
|
|
429
|
-
await stopBrowser(browser)
|
|
430
|
-
})
|
|
431
|
-
|
|
432
|
-
const errorCallbackList = createCallbackList()
|
|
433
|
-
|
|
434
|
-
const outputCallbackList = createCallbackList()
|
|
435
|
-
|
|
436
|
-
const execute = async ({
|
|
437
|
-
signal,
|
|
438
|
-
fileRelativeUrl,
|
|
439
|
-
ignoreHTTPSErrors = true, // we mostly use self signed certificates during tests
|
|
440
|
-
}) => {
|
|
441
|
-
const executeOperation = Abort.startOperation()
|
|
442
|
-
executeOperation.addAbortSignal(signal)
|
|
443
|
-
executeOperation.throwIfAborted()
|
|
444
|
-
// open a tab to execute to the file
|
|
445
|
-
const browserContext = await browser.newContext({ ignoreHTTPSErrors })
|
|
446
|
-
executeOperation.throwIfAborted()
|
|
447
|
-
const page = await browserContext.newPage()
|
|
448
|
-
executeOperation.addEndCallback(async () => {
|
|
449
|
-
try {
|
|
450
|
-
await browserContext.close()
|
|
451
|
-
} catch (e) {
|
|
452
|
-
if (isTargetClosedError(e)) {
|
|
453
|
-
return
|
|
454
|
-
}
|
|
455
|
-
throw e
|
|
456
|
-
}
|
|
457
|
-
})
|
|
458
|
-
// track tab error and console
|
|
459
|
-
const stopTrackingToNotify = trackPageToNotify(page, {
|
|
460
|
-
onError: (error) => {
|
|
461
|
-
error = transformErrorHook(error)
|
|
462
|
-
if (!ignoreErrorHook(error)) {
|
|
463
|
-
errorCallbackList.notify(error)
|
|
464
|
-
}
|
|
465
|
-
},
|
|
466
|
-
onConsole: outputCallbackList.notify,
|
|
467
|
-
})
|
|
468
|
-
stoppedCallbackList.add(stopTrackingToNotify)
|
|
469
|
-
|
|
470
|
-
const result = await executeHtmlFile(fileRelativeUrl, {
|
|
471
|
-
runtime,
|
|
472
|
-
executeOperation,
|
|
473
|
-
|
|
474
|
-
projectDirectoryUrl,
|
|
475
|
-
compileServerOrigin,
|
|
476
|
-
compileServerId,
|
|
477
|
-
outDirectoryRelativeUrl,
|
|
478
|
-
|
|
479
|
-
page,
|
|
480
|
-
measurePerformance,
|
|
481
|
-
collectPerformance,
|
|
482
|
-
collectCoverage,
|
|
483
|
-
coverageForceIstanbul,
|
|
484
|
-
coveragePlaywrightAPIAvailable,
|
|
485
|
-
coverageIgnorePredicate,
|
|
486
|
-
transformErrorHook,
|
|
487
|
-
})
|
|
488
|
-
return result
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
return {
|
|
492
|
-
stoppedCallbackList,
|
|
493
|
-
errorCallbackList,
|
|
494
|
-
outputCallbackList,
|
|
495
|
-
execute,
|
|
496
|
-
stop,
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
const isTargetClosedError = (error) => {
|
|
501
|
-
if (error.message.match(/Protocol error \(.*?\): Target closed/)) {
|
|
502
|
-
return true
|
|
503
|
-
}
|
|
504
|
-
if (error.message.match(/Protocol error \(.*?\): Browser.*?closed/)) {
|
|
505
|
-
return true
|
|
506
|
-
}
|
|
507
|
-
if (error.message.includes("browserContext.close: Browser closed")) {
|
|
508
|
-
return true
|
|
509
|
-
}
|
|
510
|
-
return false
|
|
511
|
-
}
|
|
42
|
+
})
|
|
43
|
+
export const webkitTabRuntime = webkitRuntime.tab
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
export const createSharing = ({ argsToId = argsToIdFallback } = {}) => {
|
|
2
|
-
const tokenMap = {}
|
|
3
|
-
|
|
4
|
-
const getSharingToken = (...args) => {
|
|
5
|
-
const id = argsToId(args)
|
|
6
|
-
|
|
7
|
-
if (id in tokenMap) {
|
|
8
|
-
return tokenMap[id]
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const sharingToken = createSharingToken({
|
|
12
|
-
unusedCallback: () => {
|
|
13
|
-
delete tokenMap[id]
|
|
14
|
-
},
|
|
15
|
-
})
|
|
16
|
-
tokenMap[id] = sharingToken
|
|
17
|
-
return sharingToken
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const getUniqueSharingToken = () => {
|
|
21
|
-
return createSharingToken()
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return { getSharingToken, getUniqueSharingToken }
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const createSharingToken = ({ unusedCallback = () => {} } = {}) => {
|
|
28
|
-
let useCount = 0
|
|
29
|
-
let sharedValue
|
|
30
|
-
let cleanup
|
|
31
|
-
const sharingToken = {
|
|
32
|
-
isUsed: () => useCount > 0,
|
|
33
|
-
|
|
34
|
-
setSharedValue: (value, cleanupFunction = () => {}) => {
|
|
35
|
-
sharedValue = value
|
|
36
|
-
cleanup = cleanupFunction
|
|
37
|
-
},
|
|
38
|
-
|
|
39
|
-
useSharedValue: () => {
|
|
40
|
-
useCount++
|
|
41
|
-
|
|
42
|
-
let stopped = false
|
|
43
|
-
let stopUsingReturnValue
|
|
44
|
-
const stopUsing = () => {
|
|
45
|
-
// ensure if stopUsing is called many times
|
|
46
|
-
// it returns the same value and does not decrement useCount more than once
|
|
47
|
-
if (stopped) {
|
|
48
|
-
return stopUsingReturnValue
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
stopped = true
|
|
52
|
-
useCount--
|
|
53
|
-
if (useCount === 0) {
|
|
54
|
-
unusedCallback()
|
|
55
|
-
sharedValue = undefined
|
|
56
|
-
stopUsingReturnValue = cleanup()
|
|
57
|
-
} else {
|
|
58
|
-
stopUsingReturnValue = undefined
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return stopUsingReturnValue
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return [sharedValue, stopUsing]
|
|
65
|
-
},
|
|
66
|
-
}
|
|
67
|
-
return sharingToken
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const argsToIdFallback = (args) => JSON.stringify(args)
|