Skip to content

Commit a3e9e64

Browse files
committed
Opt-in support for top-level-await
This can be enabled by adding enableTopLevelAwait: true to the configuration. It's off by default because HTTP requests for scripts are performed sequentially rather than potentially in parallel. This comes at a cost in performance but is the only way to ensure a stable order when randomization is disabled or a random seed is specified.
1 parent cdaa4f4 commit a3e9e64

File tree

12 files changed

+271
-53
lines changed

12 files changed

+271
-53
lines changed

lib/server.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ class Server {
5757
}
5858

5959
getSupportFiles() {
60-
var result = ['/__support__/loadEsModule.js'];
60+
const result = ['/__support__/loaders.js'];
6161
if (!this.useHtmlReporter) {
6262
result.push('/__support__/clearReporters.js');
6363
}
@@ -129,6 +129,7 @@ class Server {
129129
cssFiles: self.allCss(),
130130
jasmineJsFiles: self.jasmineJs(),
131131
userJsFiles: self.userJs(),
132+
enableTopLevelAwait: self.options.enableTopLevelAwait || false,
132133
})
133134
);
134135
} catch (error) {

lib/support/loadEsModule.js

Lines changed: 0 additions & 20 deletions
This file was deleted.

lib/support/loaders.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/* eslint-env browser, jasmine */
2+
3+
window._jasmine_loadEsModule = function(src) {
4+
var script = document.createElement('script');
5+
script.type = 'module';
6+
7+
// Safari reports syntax errors in ES modules as a script element error
8+
// event rather than a global error event. Rethrow so that Jasmine can
9+
// pick it up and fail the suite.
10+
script.addEventListener('error', function(event) {
11+
var msg =
12+
'An error occurred while loading ' +
13+
src +
14+
'. Check the browser console for details.';
15+
throw new Error(msg);
16+
});
17+
18+
script.src = src;
19+
document.head.appendChild(script);
20+
};
21+
22+
window._jasmine_loadWithTopLevelAwaitSupport = async function(scriptUrls) {
23+
const scriptsLoaded = (async function() {
24+
// Load scripts sequentially to ensure that users can get a stable order
25+
// by disabling randomization or setting a seed. This can be considerably
26+
// slower than the normal way of doing things because the browser won't
27+
// parallelize the HTTP requests. But it's the only way to ensure that the
28+
// describes will execute in a consistent order in the presence of top-level
29+
// await.
30+
for (const url of scriptUrls) {
31+
const isEsm = url.endsWith('.mjs');
32+
33+
if (isEsm) {
34+
try {
35+
await import(url);
36+
} catch (e) {
37+
setTimeout(function() {
38+
// Rethrow the error so jasmine-core's global error handler can pick it up.
39+
throw e;
40+
});
41+
await new Promise(function(resolve) {
42+
setTimeout(resolve);
43+
});
44+
}
45+
} else {
46+
await new Promise(function(resolve) {
47+
const script = document.createElement('script');
48+
script.addEventListener('load', function() {
49+
resolve();
50+
});
51+
script.addEventListener('error', function() {
52+
resolve();
53+
});
54+
script.src = url;
55+
document.head.appendChild(script);
56+
});
57+
}
58+
}
59+
})();
60+
61+
const bootJasmine = window.onload;
62+
window.onload = function() {
63+
scriptsLoaded.then(bootJasmine);
64+
};
65+
};

lib/types.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,13 @@
111111
* @type boolean | undefined
112112
* @default true
113113
*/
114+
/**
115+
* Whether to enable support for top-level await. This option is off by default
116+
* because it comes with a performance penalty.
117+
* @name Configuration#enableTopLevelAwait
118+
* @type boolean | undefined
119+
* @default false
120+
*/
114121

115122
/**
116123
* Describes a web browser.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"jasmine-core": "^5.0.0-beta.0"
4747
},
4848
"devDependencies": {
49+
"ejs-lint": "^2.0.0",
4950
"eslint": "^8.38.0",
5051
"eslint-plugin-jasmine": "^4.1.3",
5152
"jasmine": "^5.0.0-beta.0",

run.html.ejs

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,23 @@
1313
<% jasmineJsFiles.forEach(function(jsFile) { %>
1414
<script src="<%= jsFile %>" type="text/javascript"></script>
1515
<% }) %>
16-
<% userJsFiles.forEach(function(jsFile) { %>
17-
<% if (jsFile.endsWith('.mjs')) { %>
18-
<script type="module">_jasmine_loadEsModule('<%= jsFile %>')</script>
19-
<% } else { %>
20-
<script src="<%= jsFile %>" type="text/javascript"></script>
21-
<% } %>
22-
<% }) %>
16+
<% if (enableTopLevelAwait) { %>
17+
<script type="module">
18+
await _jasmine_loadWithTopLevelAwaitSupport([
19+
<% userJsFiles.forEach(function(jsFile) { %>
20+
'<%= jsFile %>',
21+
<% }) %>
22+
]);
23+
</script>
24+
<% } else { %>
25+
<% userJsFiles.forEach(function(jsFile) { %>
26+
<% if (jsFile.endsWith('.mjs')) { %>
27+
<script type="module">_jasmine_loadEsModule('<%= jsFile %>')</script>
28+
<% } else { %>
29+
<script src="<%= jsFile %>" type="text/javascript"></script>
30+
<% } %>
31+
<% }) %>
32+
<% } %>
2333

2434
<div id="jasmine_content"></div>
2535
</body>

spec/esmIntegrationSpec.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,59 @@ describe('ESM integration', function() {
5555
},
5656
30 * 1000
5757
);
58+
59+
it(
60+
'optionally supports top-level await',
61+
function(done) {
62+
let timedOut = false;
63+
let timerId;
64+
const jasmineBrowserProcess = exec(
65+
'node ../../../bin/jasmine-browser-runner runSpecs',
66+
{ cwd: 'spec/fixtures/topLevelAwait' },
67+
function(err, stdout, stderr) {
68+
try {
69+
if (timedOut) {
70+
return;
71+
}
72+
73+
clearTimeout(timerId);
74+
75+
if (!err) {
76+
expect(stdout).toContain('3 specs, 0 failures');
77+
// Verify that specs ran in the expected order
78+
expect(stdout).toContain(
79+
'Spec started: is a spec in aSpec.mjs\n' +
80+
'.Spec started: verifies that ES modules in helpers were awaited\n' +
81+
'.Spec started: is a spec in bSpec.js\n'
82+
);
83+
done();
84+
} else {
85+
if (err.code !== 1 || stdout === '' || stderr !== '') {
86+
// Some kind of unexpected failure happened. Include all the info
87+
// that we have.
88+
done.fail(
89+
`Child suite failed with error:\n${err}\n\n` +
90+
`stdout:\n${stdout}\n\n` +
91+
`stderr:\n${stderr}`
92+
);
93+
} else {
94+
// A normal suite failure. Just include the output.
95+
done.fail(`Child suite failed with output:\n${stdout}`);
96+
}
97+
}
98+
} catch (e) {
99+
done.fail(e);
100+
}
101+
}
102+
);
103+
104+
timerId = setTimeout(function() {
105+
// Kill the child processs if we're about to time out, to free up
106+
// the port.
107+
timedOut = true;
108+
jasmineBrowserProcess.kill();
109+
}, 29 * 1000);
110+
},
111+
30 * 1000
112+
);
58113
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
await new Promise(function(resolve) {
2+
// 100ms is usually long enough to make the specs fail if top level await
3+
// isn't supported, but short enough not to be too obnoxious.
4+
// This is inherently non-deterministic but should at least consistently pass
5+
// if the code is correct.
6+
setTimeout(resolve, 100);
7+
})
8+
9+
it('is a spec in aSpec.mjs', function() {});
10+
11+
it('verifies that ES modules in helpers were awaited', function() {
12+
expect(window._helperLoaded).toBeTrue();
13+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
it('is a spec in bSpec.js', function() {});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
await new Promise(function(resolve) {
2+
// 100ms is usually long enough to make the specs fail if top level await
3+
// isn't supported, but short enough not to be too obnoxious.
4+
// This is inherently non-deterministic but should at least consistently pass
5+
// if the code is correct.
6+
setTimeout(resolve, 100);
7+
})
8+
window._helperLoaded = true;

0 commit comments

Comments
 (0)