Skip to content

Commit 7b66ba0

Browse files
committed
server: Fix truncation of proxied landing page due to Content-Length
* Strip any Content-Length or Transfer-Encoding response header from the origin server for the landing page as it won't be correct for the modified version that has the injected script tag. * Simplify `finish` event to be a summary that is ideal for single clients.
1 parent 502199e commit 7b66ba0

File tree

7 files changed

+132
-26
lines changed

7 files changed

+132
-26
lines changed

API.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -163,14 +163,21 @@ async function mybrowser (url, signals) {
163163

164164
### Event: `'finish'`
165165

166+
Summary event that is ideal for when you run one test suite in one browser, or if you otherwise don't need a break down of results by client.
167+
166168
* `event.ok <boolean>` Aggregate status of each client's results. If any failed, this is false.
167169
* `event.exitCode <number>` Suggested exit code, 0 for success, 1 for failed.
168-
* `event.bails <Object<string,string>>` For clients that bailed, this contains the bail reason keyed by client ID.
169-
* `event.results <Object<string,Object>>` For clients completed their test, this contains the detailed `result` event object, keyed by client ID.
170+
* `event.total <number>` Aggregated from `result` events.
171+
* `event.passed <number>` Aggregated from `result` events.
172+
* `event.failed <number>` Aggregated from `result` events.
173+
* `event.skips <array>` Carried from the first client that failed, or empty.
174+
* `event.todos <array>` Carried from the first client that failed, or empty.
175+
* `event.failures <array>` Carried from the first client that failed, or empty.
176+
* `event.bailout <false|string>` Carried from the first client that failed, or false.
170177

171178
## QTap reporter events
172179

173-
A client will never emit these events more than once, except for `consoleerror`.
180+
A client will emit each of these events only once, except `consoleerror` which may be emitted any number of times.
174181

175182
### Event: `'client'`
176183

@@ -209,9 +216,9 @@ The `bail` event is emitted when a browser was unable to start or complete a tes
209216

210217
### Event: `'consoleerror'`
211218

212-
The `consoleerror` is event for any warnings or errors that may be observed from the browser console. These are for debug purposes only, and do not indicate that any test has failed. A complete and successful test run, may nonetheless print warnings or errors to the console.
219+
The `consoleerror` event relays any warning or error messages from the browser console. These are for debug purposes only, and do not indicate that a test has failed. A complete and successful test run, may nonetheless print warnings or errors to the console.
213220

214-
It is recommended that reporters only display these if a browser bailed, or if the result includes failed tests.
221+
It is recommended that reporters only display console errors if a test run failed (i.e. there was a failed test result, or the cilent bailed).
215222

216223
* `event.clientId <string>`
217224
* `event.message <string>`

src/qtap.js

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -180,20 +180,33 @@ function run (files, browserNames = 'detect', runOptions = {}) {
180180
const finish = {
181181
ok: true,
182182
exitCode: 0,
183-
bails: {},
184-
results: {}
183+
total: 0,
184+
passed: 0,
185+
failed: 0,
186+
skips: [],
187+
todos: [],
188+
failures: [],
189+
bailout: false
185190
};
186191
eventbus.on('bail', (event) => {
187-
finish.ok = false;
188-
finish.exitCode = 1;
189-
finish.bails[event.clientId] = event;
192+
if (finish.ok) {
193+
finish.ok = false;
194+
finish.exitCode = 1;
195+
finish.bailout = event.reason;
196+
}
190197
});
191198
eventbus.on('result', (event) => {
192-
if (!event.ok) {
199+
finish.total += event.total;
200+
finish.passed += event.passed;
201+
finish.failed += event.failed;
202+
203+
if (finish.ok && !event.ok) {
193204
finish.ok = false;
194205
finish.exitCode = 1;
206+
finish.skips = event.skips;
207+
finish.todos = event.todos;
208+
finish.failures = event.failures;
195209
}
196-
finish.results[event.clientId] = event;
197210
});
198211

199212
// Wait for all tests and browsers to finish/stop, regardless of errors thrown,

src/server.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,6 @@ class ControlServer {
162162
if (!resp.ok) {
163163
throw new Error('Remote URL responded with HTTP ' + resp.status);
164164
}
165-
// TODO: Write a test to confirm that we preserve response headers
166165
headers = resp.headers;
167166
body = await resp.text();
168167
} else {
@@ -257,7 +256,11 @@ class ControlServer {
257256

258257
const testFileResp = await this.getTestFile(clientId);
259258
for (const [name, value] of testFileResp.headers) {
260-
resp.setHeader(name, value);
259+
// Ignore these incompatible headers from the original response,
260+
// as otherwise the browser may truncate the amended test file.
261+
if (!['content-length', 'transfer-encoding'].includes(name.toLowerCase())) {
262+
resp.setHeader(name, value);
263+
}
261264
}
262265
if (!testFileResp.headers.get('Content-Type')) {
263266
resp.setHeader('Content-Type', util.MIME_TYPES.html);

test/fixtures/proxied.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<meta charset="utf-8">
4+
<title>proxied</title>
5+
<link rel="stylesheet" href="../../node_modules/qunit/qunit/qunit.css">
6+
<body>
7+
<div id="qunit"></div>
8+
<div id="qunit-fixture"></div>
9+
<script src="../../node_modules/qunit/qunit/qunit.js"></script>
10+
<script src="proxied.js"></script>
11+
</body>
12+
</html>

test/fixtures/proxied.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/* global QUnit */
2+
3+
QUnit.module('proxied', function () {
4+
QUnit.test('example', function (assert) {
5+
assert.equal(4, 4);
6+
});
7+
QUnit.test('Last-Modified header', function (assert) {
8+
assert.equal(new Date(document.lastModified).toISOString(), '2011-08-12T06:15:00.000Z');
9+
});
10+
});

test/qtap.test.js

Lines changed: 70 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import http from 'node:http';
2+
import fs from 'node:fs';
23
import path from 'node:path';
34
import { fileURLToPath } from 'url';
45
import util from 'node:util';
56

67
import qtap from '../src/qtap.js';
78
import { ControlServer } from '../src/server.js';
9+
import { MIME_TYPES } from '../src/util.js';
810

911
const __filename = fileURLToPath(import.meta.url);
1012
const __dirname = path.dirname(__filename);
11-
const cwd = path.join(__dirname, '..');
13+
const root = path.join(__dirname, '..');
1214
const options = {
13-
cwd,
15+
cwd: root,
1416
idleTimeout: 30,
1517
verbose: !!process.env.CI,
1618
// verbose: true, // debugging
@@ -36,14 +38,9 @@ function debugReporter (eventbus) {
3638
const EXPECTED_FAKE_PASS_4 = {
3739
ok: true,
3840
exitCode: 0,
39-
results: {
40-
client_1: {
41-
clientId: 'client_1',
42-
ok: true, total: 4, passed: 4, failed: 0,
43-
skips: [], todos: [], failures: [],
44-
}
45-
},
46-
bails: {},
41+
total: 4, passed: 4, failed: 0,
42+
skips: [], todos: [], failures: [],
43+
bailout: false
4744
};
4845

4946
QUnit.module('qtap', function (hooks) {
@@ -519,7 +516,6 @@ QUnit.module('qtap', function (hooks) {
519516
});
520517
server.listen();
521518
const port = await new Promise((resolve) => {
522-
// @ts-ignore
523519
server.on('listening', () => resolve(server.address().port));
524520
});
525521

@@ -548,4 +544,67 @@ QUnit.module('qtap', function (hooks) {
548544

549545
server.close();
550546
});
547+
548+
// - The test server must remove any Content-Length response header
549+
// if the origin server sends one, as otherwise the browser will
550+
// truncate the response mid-way and time out.
551+
// - The test server must inject a <base> tag or otherwise ensure that
552+
// any referenced JS and CSS files are requested from the origin server.
553+
QUnit.test('runWaitFor() [proxy resources from custom server]', async function (assert) {
554+
assert.timeout(40_000);
555+
556+
const requestLog = [];
557+
558+
// Static file server
559+
const server = http.createServer((req, resp) => {
560+
const ext = path.extname(req.url).slice(1);
561+
const filePath = path.join(root, req.url);
562+
console.error('# Request ' + req.url + ' for ' + filePath);
563+
let fileBuf;
564+
try {
565+
fileBuf = fs.readFileSync(filePath);
566+
} catch (e) {
567+
resp.writeHead(404);
568+
resp.end();
569+
return;
570+
}
571+
requestLog.push(req.url);
572+
resp.writeHead(200, {
573+
'Content-Type': MIME_TYPES[ext] || MIME_TYPES.bin,
574+
'Content-Length': fileBuf.length,
575+
'Last-Modified': 'Fri, 12 Aug 2011 09:15:00 +0300',
576+
});
577+
resp.write(fileBuf);
578+
resp.end();
579+
});
580+
server.listen();
581+
const port = await new Promise((resolve) => {
582+
server.on('listening', () => resolve(server.address().port));
583+
});
584+
585+
const finish = await qtap.runWaitFor(
586+
`http://localhost:${port}/test/fixtures/proxied.html`,
587+
'firefox',
588+
options
589+
);
590+
server.close();
591+
592+
assert.deepEqual(finish, {
593+
ok: true,
594+
exitCode: 0,
595+
total: 2,
596+
passed: 2,
597+
failed: [],
598+
skips: [],
599+
todos: [],
600+
failures: [],
601+
bailout: false
602+
});
603+
assert.deepEqual(requestLog, [
604+
'/test/fixtures/proxied.html',
605+
'/node_modules/qunit/qunit/qunit.css',
606+
'/node_modules/qunit/qunit/qunit.js',
607+
'/test/fixtures/proxied.js',
608+
], 'requestLog');
609+
});
551610
});

test/structure.test.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ QUnit.module('structure', function () {
1313

1414
let expectedHtmlFiles = fs.readFileSync(path.join(__dirname, 'qtap.test.js'), 'utf8')
1515
.split('\n')
16-
.map((line) => line.replace(/^\s*files: 'test\/fixtures\/(.+\.html)',$|^.*$|/, '$1'))
16+
// match `files: 'test/fixtures/foo.html'`
17+
// match `'/test/fixtures/foo.html'`
18+
.map((line) => line.replace(/^\s*(?:files: )?'\/?test\/fixtures\/(.+\.html)',$|^.*$|/, '$1'))
1719
.filter(Boolean)
1820
.sort();
1921
// Remove duplicates

0 commit comments

Comments
 (0)