@yz-social/webrtc 0.0.4 → 0.1.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/README.md ADDED
@@ -0,0 +1,10 @@
1
+
2
+ ## Data channel name event
3
+
4
+ RTCPeerConnection defines a 'datachannel' event, and RTCDataChannel defines an 'open' event, but it is difficult to use them correctly:
5
+ - 'datachannel' fires only for one side of a connection, and only when negotiated:false.
6
+ - To listen for 'open', you must already have the data channel. Not all implementations fire a handler for this when assigned in a 'datachannel' handler, and it can fire multiple times for the same channel name when two sides initiate the channel simultaneously with negotiated:true.
7
+
8
+ ## close event
9
+
10
+ RTCPeerConnection defines a 'signalingstatechange' event in which application handlers can fire code when aPeerConnection.readyState === 'closed', but this not particuarly convenient.
package/index.js CHANGED
@@ -1,2 +1,268 @@
1
- export { WebRTCBase } from './lib/webrtcbase.js';
2
- export { WebRTC } from './lib/webrtc.js';
1
+ //import wrtc from '#wrtc'; // fixme
2
+ const wrtc = (typeof(process) === 'undefined') ? globalThis : (await import('#wrtc')).default;
3
+
4
+ export class WebRTC {
5
+ static iceServers = [ // Some default stun and even turn servers.
6
+
7
+ { urls: 'stun:stun.l.google.com:19302'},
8
+ // https://freestun.net/ Currently 50 KBit/s. (2.5 MBit/s fors $9/month)
9
+ { urls: 'stun:freestun.net:3478' },
10
+
11
+ //{ urls: 'turn:freestun.net:3478', username: 'free', credential: 'free' },
12
+ // Presumably traffic limited. Can generate new credentials at https://speed.cloudflare.com/turn-creds
13
+ // Also https://developers.cloudflare.com/calls/ 1 TB/month, and $0.05 /GB after that.
14
+ { urls: 'turn:turn.speed.cloudflare.com:50000', username: '826226244cd6e5edb3f55749b796235f420fe5ee78895e0dd7d2baa45e1f7a8f49e9239e78691ab38b72ce016471f7746f5277dcef84ad79fc60f8020b132c73', credential: 'aba9b169546eb6dcc7bfb1cdf34544cf95b5161d602e3b5fa7c8342b2e9802fb' }
15
+
16
+ // See also:
17
+ // https://fastturn.net/ Currently 500MB/month? (25 GB/month for $9/month)
18
+ // https://xirsys.com/pricing/ 500 MB/month (50 GB/month for $33/month)
19
+ // Also https://www.npmjs.com/package/node-turn or https://meetrix.io/blog/webrtc/coturn/installation.html
20
+ ];
21
+ constructor({configuration = {iceServers: WebRTC.iceServers}, ...properties}) {
22
+ Object.assign(this, properties);
23
+
24
+ this.pc = new wrtc.RTCPeerConnection(configuration);
25
+ const {promise, resolve} = Promise.withResolvers(); // To indicate when closed.
26
+ promise.resolve = resolve;
27
+ this.closed = promise;
28
+ // Safari doesn't fire signalingstatechange for closing activity.
29
+ this.pc.addEventListener('connectionstatechange', () => { // Only fires by action from other side.
30
+ const state = this.pc.connectionState;
31
+ if (state === 'connected') return this.signalsReadyResolver?.();
32
+ if (['new', 'connecting'].includes(state)) return null;
33
+ // closed, disconnected, failed: resolve this.closed promise.
34
+ this.log('connectionstatechange signaling/connection:', this.pc.signalingState, state);
35
+ return resolve(this.pc);
36
+ });
37
+
38
+ this.connectionStartTime = Date.now();
39
+ this.makingOffer = false;
40
+ this.ignoreOffer = false;
41
+
42
+ this.pc.onicecandidate = e => {
43
+ if (!e.candidate) return;
44
+ //if (this.pc.connectionState === 'connected') return; // Don't waste messages. FIXME
45
+ this.signal({ candidate: e.candidate });
46
+ };
47
+ this.pc.ondatachannel = e => this.ondatachannel(e.channel);
48
+ this.pc.onnegotiationneeded = async () => {
49
+ try {
50
+ this.makingOffer = true;
51
+ this.log('creating offer in state:', this.pc.signalingState);
52
+ const offer = await this.pc.createOffer(); // The current wrtc for NodeJS doesn't yet support setLocalDescription with no arguments.
53
+ if (this.pc.signalingState != "stable") return; // https://blog.mozilla.org/webrtc/perfect-negotiation-in-webrtc/
54
+ this.log('setting local offer in state:', this.pc.signalingState);
55
+ await this.pc.setLocalDescription(offer)
56
+ .catch(e => console.log(this.name, 'ignoring error in setLocalDescription of original offer while in state', this.pc.signalingState, e));
57
+ this.signal({ description: this.pc.localDescription });
58
+ } finally {
59
+ this.makingOffer = false;
60
+ }
61
+ };
62
+ }
63
+ async close() {
64
+ // Do not try to close or wait for data channels. It confuses Safari.
65
+ this.pc.close();
66
+ this.closed.resolve(this.pc); // We do not automatically receive 'connectionstatechange' when our side explicitly closes. (Only if the other does.)
67
+ return this.closed;
68
+ }
69
+ flog(...rest) {
70
+ console.log(new Date(), this.name, ...rest);
71
+ }
72
+ log(...rest) {
73
+ if (this.debug) this.flog(...rest);
74
+ }
75
+ static delay(ms, value) {
76
+ return new Promise(resolve => setTimeout(resolve, ms, value));
77
+ }
78
+
79
+ // Must include NodeJS, and must not include Chrome/Edge. Safari and Firefox can be either.
80
+ explicitRollback = typeof(globalThis.process) !== 'undefined';
81
+ async onSignal({ description, candidate }) {
82
+ // Most of this and onnegotiationneeded is from https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Perfect_negotiation
83
+ if (description) {
84
+ const offerCollision =
85
+ description.type === "offer" &&
86
+ (this.makingOffer || (this.pc.signalingState !== "stable" && !this.settingRemote));
87
+
88
+ this.ignoreOffer = !this.polite && offerCollision;
89
+ this.log('onSignal', description.type, this.pc.signalingState, 'making:', this.makingOffer, 'collision:', offerCollision, 'ignore:', this.ignoreOffer, 'settingRemote:', this.settingRemote);
90
+
91
+ if (this.ignoreOffer) {
92
+ this.log("ignoring offer (collision)");
93
+ return;
94
+ }
95
+
96
+ if (this.explicitRollback && offerCollision) {
97
+ // The current wrtc for NodeJS doesn't yet support automatic rollback. We need to make it explicit.
98
+ await Promise.all([ // See https://blog.mozilla.org/webrtc/perfect-negotiation-in-webrtc/
99
+ this.pc.setLocalDescription({type: 'rollback'})
100
+ .then(() => this.log('rollback ok'), e => this.log(this.name, 'ignoring error in rollback', e)),
101
+ this.pc.setRemoteDescription(description)
102
+ .then(() => this.log('set offer ok'), e => this.log(this.name, 'ignoring error setRemoteDescription with rollback', e))
103
+ ]);
104
+ this.rolledBack = true; // For diagnostics.
105
+ this.log('rolled back. producing answer');
106
+ } else {
107
+ this.settingRemote = true;
108
+ try {
109
+ await this.pc.setRemoteDescription(description)
110
+ .catch(e => this[offerCollision ? 'log' : 'flog'](this.name, 'ignoring error in setRemoteDescription while in state', this.pc.signalingState, e));
111
+ if (offerCollision) this.rolledBack = true;
112
+ } finally {
113
+ this.settingRemote = false;
114
+ }
115
+ }
116
+
117
+ if (description.type === "offer") {
118
+ const answer = await this.pc.createAnswer();
119
+ await this.pc.setLocalDescription(answer)
120
+ .catch(e => this.flog(this.name, 'ignoring error setLocalDescription of answer', e));
121
+ this.signal({ description: this.pc.localDescription });
122
+ }
123
+
124
+ } else if (candidate) {
125
+ //this.log('add ice');
126
+ if (this.pc.connectionState === 'closed' || !this.pc.remoteDescription?.type) { // Adding ice without a proceessed offer/answer will crash. Log and drop the candidate.
127
+ this.log('icecandidate, connection:', this.pc.connectionState, 'signaling:', this.pc.signalingState, 'ice connection:', this.pc.iceConnectionState, 'gathering:', this.pc.iceGatheringState);
128
+ return;
129
+ }
130
+ await this.pc.addIceCandidate(candidate)
131
+ .catch(e => {
132
+ if (!this.ignoreOffer && this.pc.connectionState !== 'closed') throw e;
133
+ });
134
+ }
135
+ }
136
+
137
+ async onSignals(signals) { // Apply a list of signals. Do not wait for a response.
138
+ for (const signal of signals) {
139
+ await this.onSignal(signal); // Wait for, e.g., offer/answer to be processed before adding the next.
140
+ }
141
+ }
142
+ signalsReadyResolver = null;
143
+ pendingSignals = [];
144
+ responseSerializer = Promise.resolve();
145
+ async respond(signals = []) { // Apply a list of signals, and promise a list of responding signals as soon as any are available, or empty list when connected
146
+ // This is used by a peer that is receiving signals in an out-of-band network request, and witing for a response. (Compare transferSignals.)
147
+ return this.responseSerializer = this.responseSerializer.then(async () => {
148
+ this.log('respond', signals.length, 'signals');
149
+ const {promise, resolve} = Promise.withResolvers;
150
+ this.signalsReadyResolver = resolve;
151
+ await this.onSignals(signals);
152
+ await promise;
153
+ this.signalsReadyResolver = null;
154
+ return this.collectPendingSignals();
155
+ });
156
+ }
157
+ collectPendingSignals() { // Return any pendingSignals and reset them.
158
+ // We do not assume a promise can resolve to them because more could be added between the resolve and the (await promise).
159
+ const signals = this.pendingSignals;
160
+ this.pendingSignals = [];
161
+ return signals;
162
+ }
163
+ signal(signal) { // Deal with a new signal on this peer.
164
+ // If this peer is responding to the other side, we arrange our waiting respond() to continue with data for the other side.
165
+ //
166
+ // Otherwise, if this side is allowed to initiate an outbound network request, then this side must define transferSignals(signals)
167
+ // to promise otherSide.respond(signals). If so, we call it with all pending signals (including the new one) and handle the the
168
+ // response. (Which latter may trigger more calls to signal() on our side.)
169
+ //
170
+ // Otherwise, we just remember the signal for some future respond() on our side.
171
+ //
172
+ // Note that this is compatible with both initiating and responding. E.g., both sides could attempt to transfserSignals()
173
+ // at the same time, in overlapping requests. Both will resolve.
174
+ this.log('signal', signal.description?.type || 'icecandidate', 'waiting:', !!this.signalsReadyResolver, 'has transfer:', !!this.transferSignals, 'on pending:', this.pendingSignals.length);
175
+ this.pendingSignals.push(signal);
176
+ const waiting = this.signalsReadyResolver; // As maintained and used by respond()
177
+ if (waiting) {
178
+ waiting();
179
+ return;
180
+ }
181
+ if (!this.transferSignals) return; // Just keep collecting until the next call to respond();
182
+ this.sendPending();
183
+ }
184
+ followupTimer = null;
185
+ sendPending(force = false) { // Send over any signals we have, and process the response.
186
+ this.lastOutboundSignal = Date.now();
187
+ clearTimeout(this.followupTimer);
188
+ this.transferSerializer = this.transferSerializer.then(() => {
189
+ const signals = this.collectPendingSignals();
190
+ if (!force && !signals.length) return null; // A stack of pending signals got rolled together and pending is now empty.
191
+ this.lastOutboundSend = Date.now();
192
+ this.log('sending', signals.length, 'signals');
193
+ return this.transferSignals(signals).then(async response => {
194
+ clearTimeout(this.followupTimer);
195
+ this.lastResponse = Date.now();
196
+ await this.onSignals(response);
197
+ if (this.pc.connectionState === 'connected') return;
198
+ this.followupTimer = setTimeout(() => { // We may have sent everything we had, but still need to poke in order to get more ice from them.
199
+ if (this.pc.connectionState === 'connected') return;
200
+ this.log('************** nothing new to send', this.pc.connectionState, ' ************************');
201
+ this.sendPending(true);
202
+ }, 500);
203
+ });
204
+ });
205
+ }
206
+ transferSerializer = Promise.resolve();
207
+
208
+ dataChannels = {};
209
+ setupChannel(dc) { // Given an open or connecting channel, set it up in a unform way.
210
+ this.log('setup:', dc.label, dc.id, dc.readyState, 'negotiated:', dc.negotiated, 'exists:', !!this[dc.label]);
211
+ this[dc.label] = this.dataChannels[dc.label] = dc;
212
+ dc.webrtc = this;
213
+ dc.onopen = async () => {
214
+ this.log('channel onopen:', dc.label, dc.id, dc.readyState, 'negotiated:', dc.negotiated);
215
+ this.dataChannelPromises[dc.label]?.resolve(this[dc.label]);
216
+ };
217
+ if (dc.readyState === 'open') dc.onopen();
218
+ return dc;
219
+ }
220
+ ondatachannel(dc) {
221
+ // Fires only on the answer side if the offer side opened with negotiated:false
222
+ // This our chance to setupChannel, just as if we had called createChannel
223
+ this.log('ondatachannel:', dc.label, dc.id, dc.readyState, dc.negotiated);
224
+ this.setupChannel(dc);
225
+ dc.onopen(); // It had been opened before we setup, so invoke handler now.
226
+ }
227
+ channelId = 128; // Non-negotiated channel.id get assigned at open by the peer, starting with 0. This avoids conflicts.
228
+ createChannel(name = 'data', {negotiated = false, id = this.channelId++, ...options} = {}) { // Explicitly create channel and set it up.
229
+ return this.setupChannel(this.pc.createDataChannel(name, {negotiated, id, ...options}));
230
+ }
231
+ dataChannelPromises = {};
232
+ getDataChannelPromise(name = 'data') { // Promise to resolve when opened, WITHOUT actually creating one.
233
+ const {promise, resolve, reject} = Promise.withResolvers();
234
+ Object.assign(promise, {resolve, reject});
235
+ return this.dataChannelPromises[name] = promise;
236
+ }
237
+
238
+ async reportConnection(doLogging = false) { // Update self with latest wrtc stats (and log them if doLogging true). See Object.assign for properties.
239
+ const stats = await this.pc.getStats();
240
+ let transport;
241
+ for (const report of stats.values()) {
242
+ if (report.type === 'transport') {
243
+ transport = report;
244
+ break;
245
+ }
246
+ }
247
+ let candidatePair = transport && stats.get(transport.selectedCandidatePairId);
248
+ if (!candidatePair) { // Safari doesn't follow the standard.
249
+ for (const report of stats.values()) {
250
+ if ((report.type === 'candidate-pair') && report.selected) {
251
+ candidatePair = report;
252
+ break;
253
+ }
254
+ }
255
+ }
256
+ if (!candidatePair) {
257
+ console.warn(this.label, 'got stats without candidatePair', Array.from(stats.values()));
258
+ return;
259
+ }
260
+ const remote = stats.get(candidatePair.remoteCandidateId);
261
+ const {protocol, candidateType} = remote;
262
+ const now = Date.now();
263
+ const statsElapsed = now - this.connectionStartTime;
264
+ Object.assign(this, {stats, transport, candidatePair, remote, protocol, candidateType, statsTime: now, statsElapsed});
265
+ if (doLogging) console.info(this.name, 'connected', protocol, candidateType, (statsElapsed/1e3).toFixed(1));
266
+ }
267
+ }
268
+
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2008-2019 Pivotal Labs
2
+ Copyright (c) 2008-2025 The Jasmine developers
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining
5
+ a copy of this software and associated documentation files (the
6
+ "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish,
8
+ distribute, sublicense, and/or sell copies of the Software, and to
9
+ permit persons to whom the Software is furnished to do so, subject to
10
+ the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,28 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Jasmine Spec Runner v5.7.1</title>
6
+
7
+ <link rel="shortcut icon" type="image/png" href="lib/jasmine-5.7.1/jasmine_favicon.png">
8
+ <link rel="stylesheet" href="lib/jasmine-5.7.1/jasmine.css">
9
+
10
+ <script src="lib/jasmine-5.7.1/jasmine.js"></script>
11
+ <script src="lib/jasmine-5.7.1/jasmine-html.js"></script>
12
+ <script src="lib/jasmine-5.7.1/boot0.js"></script>
13
+ <!-- optional: include a file here that configures the Jasmine env -->
14
+ <script src="lib/jasmine-5.7.1/boot1.js"></script>
15
+
16
+ <!-- include source files here... -->
17
+ <script src="src/Player.js"></script>
18
+ <script src="src/Song.js"></script>
19
+
20
+ <!-- include spec files here... -->
21
+ <script src="spec/SpecHelper.js"></script>
22
+ <script src="spec/PlayerSpec.js"></script>
23
+
24
+ </head>
25
+
26
+ <body>
27
+ </body>
28
+ </html>
@@ -0,0 +1,66 @@
1
+ /*
2
+ Copyright (c) 2008-2019 Pivotal Labs
3
+ Copyright (c) 2008-2025 The Jasmine developers
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+ */
24
+
25
+ /**
26
+ This file starts the process of "booting" Jasmine. It initializes Jasmine,
27
+ makes its globals available, and creates the env. This file should be loaded
28
+ after `jasmine.js` and `jasmine_html.js`, but before `boot1.js` or any project
29
+ source files or spec files are loaded.
30
+ */
31
+ (function() {
32
+ const jasmineRequire = window.jasmineRequire || require('./jasmine.js');
33
+
34
+ /**
35
+ * ## Require &amp; Instantiate
36
+ *
37
+ * Require Jasmine's core files. Specifically, this requires and attaches all of Jasmine's code to the `jasmine` reference.
38
+ */
39
+ const jasmine = jasmineRequire.core(jasmineRequire),
40
+ global = jasmine.getGlobal();
41
+ global.jasmine = jasmine;
42
+
43
+ /**
44
+ * Since this is being run in a browser and the results should populate to an HTML page, require the HTML-specific Jasmine code, injecting the same reference.
45
+ */
46
+ jasmineRequire.html(jasmine);
47
+
48
+ /**
49
+ * Create the Jasmine environment. This is used to run all specs in a project.
50
+ */
51
+ const env = jasmine.getEnv();
52
+
53
+ /**
54
+ * ## The Global Interface
55
+ *
56
+ * Build up the functions that will be exposed as the Jasmine public interface. A project can customize, rename or alias any of these functions as desired, provided the implementation remains unchanged.
57
+ */
58
+ const jasmineInterface = jasmineRequire.interface(jasmine, env);
59
+
60
+ /**
61
+ * Add all of the Jasmine global/public interface to the global scope, so a project can use the public interface directly. For example, calling `describe` in specs instead of `jasmine.getEnv().describe`.
62
+ */
63
+ for (const property in jasmineInterface) {
64
+ global[property] = jasmineInterface[property];
65
+ }
66
+ })();
@@ -0,0 +1,134 @@
1
+ /*
2
+ Copyright (c) 2008-2019 Pivotal Labs
3
+ Copyright (c) 2008-2025 The Jasmine developers
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+ */
24
+
25
+ /**
26
+ This file finishes 'booting' Jasmine, performing all of the necessary
27
+ initialization before executing the loaded environment and all of a project's
28
+ specs. This file should be loaded after `boot0.js` but before any project
29
+ source files or spec files are loaded. Thus this file can also be used to
30
+ customize Jasmine for a project.
31
+
32
+ If a project is using Jasmine via the standalone distribution, this file can
33
+ be customized directly. If you only wish to configure the Jasmine env, you
34
+ can load another file that calls `jasmine.getEnv().configure({...})`
35
+ after `boot0.js` is loaded and before this file is loaded.
36
+ */
37
+
38
+ (function() {
39
+ const env = jasmine.getEnv();
40
+
41
+ /**
42
+ * ## Runner Parameters
43
+ *
44
+ * More browser specific code - wrap the query string in an object and to allow for getting/setting parameters from the runner user interface.
45
+ */
46
+
47
+ const queryString = new jasmine.QueryString({
48
+ getWindowLocation: function() {
49
+ return window.location;
50
+ }
51
+ });
52
+
53
+ const filterSpecs = !!queryString.getParam('spec');
54
+
55
+ const config = {
56
+ stopOnSpecFailure: queryString.getParam('stopOnSpecFailure'),
57
+ stopSpecOnExpectationFailure: queryString.getParam(
58
+ 'stopSpecOnExpectationFailure'
59
+ ),
60
+ hideDisabled: queryString.getParam('hideDisabled')
61
+ };
62
+
63
+ const random = queryString.getParam('random');
64
+
65
+ if (random !== undefined && random !== '') {
66
+ config.random = random;
67
+ }
68
+
69
+ const seed = queryString.getParam('seed');
70
+ if (seed) {
71
+ config.seed = seed;
72
+ }
73
+
74
+ /**
75
+ * ## Reporters
76
+ * The `HtmlReporter` builds all of the HTML UI for the runner page. This reporter paints the dots, stars, and x's for specs, as well as all spec names and all failures (if any).
77
+ */
78
+ const htmlReporter = new jasmine.HtmlReporter({
79
+ env: env,
80
+ navigateWithNewParam: function(key, value) {
81
+ return queryString.navigateWithNewParam(key, value);
82
+ },
83
+ addToExistingQueryString: function(key, value) {
84
+ return queryString.fullStringWithNewParam(key, value);
85
+ },
86
+ getContainer: function() {
87
+ return document.body;
88
+ },
89
+ createElement: function() {
90
+ return document.createElement.apply(document, arguments);
91
+ },
92
+ createTextNode: function() {
93
+ return document.createTextNode.apply(document, arguments);
94
+ },
95
+ timer: new jasmine.Timer(),
96
+ filterSpecs: filterSpecs
97
+ });
98
+
99
+ /**
100
+ * The `jsApiReporter` also receives spec results, and is used by any environment that needs to extract the results from JavaScript.
101
+ */
102
+ env.addReporter(jsApiReporter);
103
+ env.addReporter(htmlReporter);
104
+
105
+ /**
106
+ * Filter which specs will be run by matching the start of the full name against the `spec` query param.
107
+ */
108
+ const specFilter = new jasmine.HtmlSpecFilter({
109
+ filterString: function() {
110
+ return queryString.getParam('spec');
111
+ }
112
+ });
113
+
114
+ config.specFilter = function(spec) {
115
+ return specFilter.matches(spec.getFullName());
116
+ };
117
+
118
+ env.configure(config);
119
+
120
+ /**
121
+ * ## Execution
122
+ *
123
+ * Replace the browser window's `onload`, ensure it's called, and then run all of the loaded specs. This includes initializing the `HtmlReporter` instance and then executing the loaded Jasmine environment. All of this will happen after all of the specs are loaded.
124
+ */
125
+ const currentWindowOnload = window.onload;
126
+
127
+ window.onload = function() {
128
+ if (currentWindowOnload) {
129
+ currentWindowOnload();
130
+ }
131
+ htmlReporter.initialize();
132
+ setTimeout(() => env.execute(), 1e3);
133
+ };
134
+ })();