Skip to content

Commit a3e895c

Browse files
authored
🐛 Fix ember launcher exiting before Testem collects results (#172)
## Summary - Fixed "Browser exited unexpectedly" error in ember SDK - The launcher was hooking into QUnit.done/Mocha end events and immediately calling process.exit(0) when tests completed, before Testem finished collecting results - Testem manages the browser lifecycle itself via SIGTERM - the test framework hooks were unnecessary ## Changes - Remove QUnit/Mocha test completion detection (not needed with Testem) - Let Testem control shutdown via signals - Add browser crash/disconnect handlers for better error detection - Better error logging with stack traces - Use `VIZZLY_LOG_LEVEL=debug` for debug output (consistent with CLI) - Exit with code 1 on errors (was always 0) ## Test plan - [x] Tested with ember-osf-web test suite - [x] Single test passes: `ember test --filter "url-with-protocol"` - [x] Full test suite runs without "Browser exited unexpectedly" errors
1 parent 5b11a6b commit a3e895c

File tree

8 files changed

+64
-80
lines changed

8 files changed

+64
-80
lines changed

clients/ember/bin/vizzly-testem-launcher.js

Lines changed: 28 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,17 @@ let isShuttingDown = false;
6161

6262
/**
6363
* Clean up resources and exit
64+
* @param {string} reason - Why cleanup was triggered
65+
* @param {number} exitCode - Process exit code (default 0)
6466
*/
65-
async function cleanup() {
67+
async function cleanup(reason = 'unknown', exitCode = 0) {
6668
if (isShuttingDown) return;
6769
isShuttingDown = true;
6870

71+
if (process.env.VIZZLY_LOG_LEVEL === 'debug') {
72+
console.error(`[vizzly-testem-launcher] Cleanup triggered: ${reason}`);
73+
}
74+
6975
try {
7076
if (browserInstance) {
7177
await closeBrowser(browserInstance);
@@ -82,7 +88,7 @@ async function cleanup() {
8288
// Ignore cleanup errors
8389
}
8490

85-
process.exit(0);
91+
process.exit(exitCode);
8692
}
8793

8894
/**
@@ -112,58 +118,23 @@ async function main() {
112118
playwrightOptions,
113119
onPageCreated: page => {
114120
setPage(page);
115-
page.on('close', cleanup);
121+
page.on('close', async () => await cleanup('page-close'));
116122
},
123+
onBrowserDisconnected: async () => await cleanup('browser-disconnected'),
117124
});
118125

119-
// 4. Monitor for test completion
126+
// 4. Listen for browser crashes
120127
let { page } = browserInstance;
121-
122-
// Wait for a test framework to be available, then hook into its completion
123-
await page.evaluate(() => {
124-
return new Promise(resolve => {
125-
let checkFramework = () => {
126-
// Check for QUnit
127-
if (typeof QUnit !== 'undefined') {
128-
QUnit.done(() => {
129-
console.log('[vizzly-testem] tests-complete');
130-
});
131-
resolve();
132-
return;
133-
}
134-
135-
// Check for Mocha
136-
if (typeof Mocha !== 'undefined' || typeof mocha !== 'undefined') {
137-
let Runner = (typeof Mocha !== 'undefined' ? Mocha : mocha).Runner;
138-
let originalEmit = Runner.prototype.emit;
139-
Runner.prototype.emit = function (...args) {
140-
if (args[0] === 'end') {
141-
console.log('[vizzly-testem] tests-complete');
142-
}
143-
return originalEmit.apply(this, args);
144-
};
145-
resolve();
146-
return;
147-
}
148-
149-
// Keep checking until a framework is found
150-
requestAnimationFrame(checkFramework);
151-
};
152-
checkFramework();
153-
});
154-
});
155-
156-
// Listen for the completion signal
157-
page.on('console', msg => {
158-
if (msg.text() === '[vizzly-testem] tests-complete') {
159-
cleanup();
160-
}
128+
page.on('crash', async () => {
129+
console.error('[vizzly-testem-launcher] Page crashed!');
130+
await cleanup('page-crash', 1);
161131
});
162132

163133
// 5. Keep process alive until cleanup is called
164134
await new Promise(() => {});
165135
} catch (error) {
166136
console.error('[vizzly-testem-launcher] Failed to start:', error.message);
137+
console.error(error.stack);
167138

168139
if (screenshotServer) {
169140
await stopScreenshotServer(screenshotServer).catch(() => {});
@@ -174,19 +145,24 @@ async function main() {
174145
}
175146

176147
// Handle graceful shutdown signals from Testem
177-
process.on('SIGTERM', cleanup);
178-
process.on('SIGINT', cleanup);
179-
process.on('SIGHUP', cleanup);
148+
process.on('SIGTERM', async () => await cleanup('SIGTERM'));
149+
process.on('SIGINT', async () => await cleanup('SIGINT'));
150+
process.on('SIGHUP', async () => await cleanup('SIGHUP'));
180151

181152
// Handle unexpected errors
182-
process.on('uncaughtException', error => {
153+
process.on('uncaughtException', async error => {
183154
console.error('[vizzly-testem-launcher] Uncaught exception:', error.message);
184-
cleanup();
155+
console.error(error.stack);
156+
await cleanup('uncaughtException', 1);
185157
});
186158

187-
process.on('unhandledRejection', reason => {
188-
console.error('[vizzly-testem-launcher] Unhandled rejection:', reason);
189-
cleanup();
159+
process.on('unhandledRejection', async (reason, promise) => {
160+
console.error('[vizzly-testem-launcher] Unhandled rejection at:', promise);
161+
console.error('[vizzly-testem-launcher] Reason:', reason);
162+
if (reason instanceof Error) {
163+
console.error(reason.stack);
164+
}
165+
await cleanup('unhandledRejection', 1);
190166
});
191167

192168
main();

clients/ember/src/launcher/browser.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ function getDefaultChromiumArgs() {
6363
* @param {boolean} [options.failOnDiff] - Whether tests should fail on visual diffs
6464
* @param {Object} [options.playwrightOptions] - Playwright launch options (headless, slowMo, timeout, etc.)
6565
* @param {Function} [options.onPageCreated] - Callback when page is created (before navigation)
66+
* @param {Function} [options.onBrowserDisconnected] - Callback when browser disconnects unexpectedly
6667
* @returns {Promise<Object>} Browser instance with page reference
6768
*/
6869
export async function launchBrowser(browserType, testUrl, options = {}) {
@@ -71,6 +72,7 @@ export async function launchBrowser(browserType, testUrl, options = {}) {
7172
failOnDiff,
7273
playwrightOptions = {},
7374
onPageCreated,
75+
onBrowserDisconnected,
7476
} = options;
7577

7678
let factory = browserFactories[browserType];
@@ -101,6 +103,12 @@ export async function launchBrowser(browserType, testUrl, options = {}) {
101103
};
102104

103105
let browser = await factory.launch(launchOptions);
106+
107+
// Listen for unexpected browser disconnection (crash, killed, etc.)
108+
if (onBrowserDisconnected) {
109+
browser.on('disconnected', onBrowserDisconnected);
110+
}
111+
104112
let context = await browser.newContext();
105113
let page = await context.newPage();
106114

clients/ember/test-app/tests/integration/components/alert-test.gjs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { module, test } from 'qunit';
22
import { setupRenderingTest } from 'ember-qunit';
33
import { render } from '@ember/test-helpers';
44
import Alert from 'test-ember-app/components/alert';
5-
import { vizzlySnapshot } from '@vizzly-testing/ember/test-support';
5+
import { vizzlyScreenshot } from '@vizzly-testing/ember/test-support';
66

77
module('Integration | Component | Alert', function (hooks) {
88
setupRenderingTest(hooks);
@@ -33,7 +33,7 @@ module('Integration | Component | Alert', function (hooks) {
3333
assert.dom('[data-test-alert="warning"]').exists();
3434
assert.dom('[data-test-alert="error"]').exists();
3535

36-
await vizzlySnapshot('alert-variants');
36+
await vizzlyScreenshot('alert-variants');
3737
});
3838

3939
test('it renders without title', async function (assert) {
@@ -48,6 +48,6 @@ module('Integration | Component | Alert', function (hooks) {
4848
assert.dom('[data-test-alert="no-title"]').exists();
4949
assert.dom('[data-test-alert="no-title"] .alert-title').doesNotExist();
5050

51-
await vizzlySnapshot('alert-no-title');
51+
await vizzlyScreenshot('alert-no-title');
5252
});
5353
});

clients/ember/test-app/tests/integration/components/button-test.gjs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { module, test } from 'qunit';
22
import { setupRenderingTest } from 'ember-qunit';
33
import { render, click } from '@ember/test-helpers';
44
import Button from 'test-ember-app/components/button';
5-
import { vizzlySnapshot } from '@vizzly-testing/ember/test-support';
5+
import { vizzlyScreenshot } from '@vizzly-testing/ember/test-support';
66

77
module('Integration | Component | Button', function (hooks) {
88
setupRenderingTest(hooks);
@@ -22,7 +22,7 @@ module('Integration | Component | Button', function (hooks) {
2222
assert.dom('[data-test-button="danger"]').hasText('Danger');
2323
assert.dom('[data-test-button="ghost"]').hasText('Ghost');
2424

25-
await vizzlySnapshot('button-variants');
25+
await vizzlyScreenshot('button-variants');
2626
});
2727

2828
test('it renders all sizes', async function (assert) {
@@ -38,7 +38,7 @@ module('Integration | Component | Button', function (hooks) {
3838
assert.dom('[data-test-button="medium"]').exists();
3939
assert.dom('[data-test-button="large"]').exists();
4040

41-
await vizzlySnapshot('button-sizes');
41+
await vizzlyScreenshot('button-sizes');
4242
});
4343

4444
test('it renders disabled state', async function (assert) {
@@ -52,7 +52,7 @@ module('Integration | Component | Button', function (hooks) {
5252
assert.dom('[data-test-button="enabled"]').isNotDisabled();
5353
assert.dom('[data-test-button="disabled"]').isDisabled();
5454

55-
await vizzlySnapshot('button-disabled-state');
55+
await vizzlyScreenshot('button-disabled-state');
5656
});
5757

5858
test('it handles click events', async function (assert) {

clients/ember/test-app/tests/integration/components/card-test.gjs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { setupRenderingTest } from 'ember-qunit';
33
import { render } from '@ember/test-helpers';
44
import Card from 'test-ember-app/components/card';
55
import Button from 'test-ember-app/components/button';
6-
import { vizzlySnapshot } from '@vizzly-testing/ember/test-support';
6+
import { vizzlyScreenshot } from '@vizzly-testing/ember/test-support';
77

88
module('Integration | Component | Card', function (hooks) {
99
setupRenderingTest(hooks);
@@ -20,7 +20,7 @@ module('Integration | Component | Card', function (hooks) {
2020
assert.dom('[data-test-card="basic"]').exists();
2121
assert.dom('[data-test-card="basic"] .card-title').hasText('Basic Card');
2222

23-
await vizzlySnapshot('card-basic');
23+
await vizzlyScreenshot('card-basic');
2424
});
2525

2626
test('it renders elevated card', async function (assert) {
@@ -34,7 +34,7 @@ module('Integration | Component | Card', function (hooks) {
3434

3535
assert.dom('[data-test-card="elevated"]').hasClass('elevated');
3636

37-
await vizzlySnapshot('card-elevated');
37+
await vizzlyScreenshot('card-elevated');
3838
});
3939

4040
test('it renders card with actions and footer', async function (assert) {
@@ -60,7 +60,7 @@ module('Integration | Component | Card', function (hooks) {
6060
assert.dom('[data-test-card="full"] .card-actions').exists();
6161
assert.dom('[data-test-card="full"] .card-footer').exists();
6262

63-
await vizzlySnapshot('card-with-actions-footer');
63+
await vizzlyScreenshot('card-with-actions-footer');
6464
});
6565

6666
test('it renders card without title', async function (assert) {
@@ -74,6 +74,6 @@ module('Integration | Component | Card', function (hooks) {
7474

7575
assert.dom('[data-test-card="no-title"] .card-header').doesNotExist();
7676

77-
await vizzlySnapshot('card-no-title');
77+
await vizzlyScreenshot('card-no-title');
7878
});
7979
});

clients/ember/test-app/tests/integration/components/data-table-test.gjs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { module, test } from 'qunit';
22
import { setupRenderingTest } from 'ember-qunit';
33
import { render } from '@ember/test-helpers';
44
import DataTable from 'test-ember-app/components/data-table';
5-
import { vizzlySnapshot } from '@vizzly-testing/ember/test-support';
5+
import { vizzlyScreenshot } from '@vizzly-testing/ember/test-support';
66

77
module('Integration | Component | DataTable', function (hooks) {
88
setupRenderingTest(hooks);
@@ -31,7 +31,7 @@ module('Integration | Component | DataTable', function (hooks) {
3131
assert.dom('.table-header').exists({ count: 4 });
3232
assert.dom('.table-row').exists({ count: 3 });
3333

34-
await vizzlySnapshot('data-table-with-data');
34+
await vizzlyScreenshot('data-table-with-data');
3535
});
3636

3737
test('it renders empty state', async function (assert) {
@@ -50,7 +50,7 @@ module('Integration | Component | DataTable', function (hooks) {
5050

5151
assert.dom('.table-empty').hasText('No users found');
5252

53-
await vizzlySnapshot('data-table-empty');
53+
await vizzlyScreenshot('data-table-empty');
5454
});
5555

5656
test('it renders with many rows', async function (assert) {
@@ -72,7 +72,7 @@ module('Integration | Component | DataTable', function (hooks) {
7272

7373
assert.dom('.table-row').exists({ count: 10 });
7474

75-
await vizzlySnapshot('data-table-many-rows');
75+
await vizzlyScreenshot('data-table-many-rows');
7676
});
7777

7878
test('it applies column alignment', async function (assert) {
@@ -86,6 +86,6 @@ module('Integration | Component | DataTable', function (hooks) {
8686
assert.dom('.table-header.center').exists({ count: 2 });
8787
assert.dom('.table-cell.center').exists({ count: 6 }); // 2 columns * 3 rows
8888

89-
await vizzlySnapshot('data-table-alignment');
89+
await vizzlyScreenshot('data-table-alignment');
9090
});
9191
});

clients/ember/test-app/tests/integration/components/form-field-test.gjs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { module, test } from 'qunit';
22
import { setupRenderingTest } from 'ember-qunit';
33
import { render, fillIn } from '@ember/test-helpers';
44
import FormField from 'test-ember-app/components/form-field';
5-
import { vizzlySnapshot } from '@vizzly-testing/ember/test-support';
5+
import { vizzlyScreenshot } from '@vizzly-testing/ember/test-support';
66

77
module('Integration | Component | FormField', function (hooks) {
88
setupRenderingTest(hooks);
@@ -23,7 +23,7 @@ module('Integration | Component | FormField', function (hooks) {
2323
assert.dom('[data-test-form-field="username"] .form-label').hasText('Username');
2424
assert.dom('[data-test-form-field="username"] .form-hint').hasText('Must be at least 3 characters');
2525

26-
await vizzlySnapshot('form-field-text');
26+
await vizzlyScreenshot('form-field-text');
2727
});
2828

2929
test('it renders with error state', async function (assert) {
@@ -44,7 +44,7 @@ module('Integration | Component | FormField', function (hooks) {
4444
assert.dom('[data-test-form-field="email"] .form-error').hasText('Please enter a valid email address');
4545
assert.dom('[data-test-form-field="email"] .required').exists();
4646

47-
await vizzlySnapshot('form-field-error');
47+
await vizzlyScreenshot('form-field-error');
4848
});
4949

5050
test('it renders textarea', async function (assert) {
@@ -62,7 +62,7 @@ module('Integration | Component | FormField', function (hooks) {
6262

6363
assert.dom('[data-test-form-field="message"] textarea').exists();
6464

65-
await vizzlySnapshot('form-field-textarea');
65+
await vizzlyScreenshot('form-field-textarea');
6666
});
6767

6868
test('it renders disabled state', async function (assert) {
@@ -79,7 +79,7 @@ module('Integration | Component | FormField', function (hooks) {
7979

8080
assert.dom('[data-test-form-field="readonly"] input').isDisabled();
8181

82-
await vizzlySnapshot('form-field-disabled');
82+
await vizzlyScreenshot('form-field-disabled');
8383
});
8484

8585
test('it renders various input types', async function (assert) {
@@ -120,6 +120,6 @@ module('Integration | Component | FormField', function (hooks) {
120120
assert.dom('[data-test-form-field="email-field"] input').hasAttribute('type', 'email');
121121
assert.dom('[data-test-form-field="number"] input').hasAttribute('type', 'number');
122122

123-
await vizzlySnapshot('form-field-types');
123+
await vizzlyScreenshot('form-field-types');
124124
});
125125
});

0 commit comments

Comments
 (0)