diff --git a/clients/ember/bin/vizzly-testem-launcher.js b/clients/ember/bin/vizzly-testem-launcher.js index a876c61f..77326d79 100755 --- a/clients/ember/bin/vizzly-testem-launcher.js +++ b/clients/ember/bin/vizzly-testem-launcher.js @@ -61,11 +61,17 @@ let isShuttingDown = false; /** * Clean up resources and exit + * @param {string} reason - Why cleanup was triggered + * @param {number} exitCode - Process exit code (default 0) */ -async function cleanup() { +async function cleanup(reason = 'unknown', exitCode = 0) { if (isShuttingDown) return; isShuttingDown = true; + if (process.env.VIZZLY_LOG_LEVEL === 'debug') { + console.error(`[vizzly-testem-launcher] Cleanup triggered: ${reason}`); + } + try { if (browserInstance) { await closeBrowser(browserInstance); @@ -82,7 +88,7 @@ async function cleanup() { // Ignore cleanup errors } - process.exit(0); + process.exit(exitCode); } /** @@ -112,58 +118,23 @@ async function main() { playwrightOptions, onPageCreated: page => { setPage(page); - page.on('close', cleanup); + page.on('close', async () => await cleanup('page-close')); }, + onBrowserDisconnected: async () => await cleanup('browser-disconnected'), }); - // 4. Monitor for test completion + // 4. Listen for browser crashes let { page } = browserInstance; - - // Wait for a test framework to be available, then hook into its completion - await page.evaluate(() => { - return new Promise(resolve => { - let checkFramework = () => { - // Check for QUnit - if (typeof QUnit !== 'undefined') { - QUnit.done(() => { - console.log('[vizzly-testem] tests-complete'); - }); - resolve(); - return; - } - - // Check for Mocha - if (typeof Mocha !== 'undefined' || typeof mocha !== 'undefined') { - let Runner = (typeof Mocha !== 'undefined' ? Mocha : mocha).Runner; - let originalEmit = Runner.prototype.emit; - Runner.prototype.emit = function (...args) { - if (args[0] === 'end') { - console.log('[vizzly-testem] tests-complete'); - } - return originalEmit.apply(this, args); - }; - resolve(); - return; - } - - // Keep checking until a framework is found - requestAnimationFrame(checkFramework); - }; - checkFramework(); - }); - }); - - // Listen for the completion signal - page.on('console', msg => { - if (msg.text() === '[vizzly-testem] tests-complete') { - cleanup(); - } + page.on('crash', async () => { + console.error('[vizzly-testem-launcher] Page crashed!'); + await cleanup('page-crash', 1); }); // 5. Keep process alive until cleanup is called await new Promise(() => {}); } catch (error) { console.error('[vizzly-testem-launcher] Failed to start:', error.message); + console.error(error.stack); if (screenshotServer) { await stopScreenshotServer(screenshotServer).catch(() => {}); @@ -174,19 +145,24 @@ async function main() { } // Handle graceful shutdown signals from Testem -process.on('SIGTERM', cleanup); -process.on('SIGINT', cleanup); -process.on('SIGHUP', cleanup); +process.on('SIGTERM', async () => await cleanup('SIGTERM')); +process.on('SIGINT', async () => await cleanup('SIGINT')); +process.on('SIGHUP', async () => await cleanup('SIGHUP')); // Handle unexpected errors -process.on('uncaughtException', error => { +process.on('uncaughtException', async error => { console.error('[vizzly-testem-launcher] Uncaught exception:', error.message); - cleanup(); + console.error(error.stack); + await cleanup('uncaughtException', 1); }); -process.on('unhandledRejection', reason => { - console.error('[vizzly-testem-launcher] Unhandled rejection:', reason); - cleanup(); +process.on('unhandledRejection', async (reason, promise) => { + console.error('[vizzly-testem-launcher] Unhandled rejection at:', promise); + console.error('[vizzly-testem-launcher] Reason:', reason); + if (reason instanceof Error) { + console.error(reason.stack); + } + await cleanup('unhandledRejection', 1); }); main(); diff --git a/clients/ember/src/launcher/browser.js b/clients/ember/src/launcher/browser.js index 41760c04..dbba43ea 100644 --- a/clients/ember/src/launcher/browser.js +++ b/clients/ember/src/launcher/browser.js @@ -63,6 +63,7 @@ function getDefaultChromiumArgs() { * @param {boolean} [options.failOnDiff] - Whether tests should fail on visual diffs * @param {Object} [options.playwrightOptions] - Playwright launch options (headless, slowMo, timeout, etc.) * @param {Function} [options.onPageCreated] - Callback when page is created (before navigation) + * @param {Function} [options.onBrowserDisconnected] - Callback when browser disconnects unexpectedly * @returns {Promise} Browser instance with page reference */ export async function launchBrowser(browserType, testUrl, options = {}) { @@ -71,6 +72,7 @@ export async function launchBrowser(browserType, testUrl, options = {}) { failOnDiff, playwrightOptions = {}, onPageCreated, + onBrowserDisconnected, } = options; let factory = browserFactories[browserType]; @@ -101,6 +103,12 @@ export async function launchBrowser(browserType, testUrl, options = {}) { }; let browser = await factory.launch(launchOptions); + + // Listen for unexpected browser disconnection (crash, killed, etc.) + if (onBrowserDisconnected) { + browser.on('disconnected', onBrowserDisconnected); + } + let context = await browser.newContext(); let page = await context.newPage(); diff --git a/clients/ember/test-app/tests/integration/components/alert-test.gjs b/clients/ember/test-app/tests/integration/components/alert-test.gjs index 43d7c81e..9e8b4c9e 100644 --- a/clients/ember/test-app/tests/integration/components/alert-test.gjs +++ b/clients/ember/test-app/tests/integration/components/alert-test.gjs @@ -2,7 +2,7 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { render } from '@ember/test-helpers'; import Alert from 'test-ember-app/components/alert'; -import { vizzlySnapshot } from '@vizzly-testing/ember/test-support'; +import { vizzlyScreenshot } from '@vizzly-testing/ember/test-support'; module('Integration | Component | Alert', function (hooks) { setupRenderingTest(hooks); @@ -33,7 +33,7 @@ module('Integration | Component | Alert', function (hooks) { assert.dom('[data-test-alert="warning"]').exists(); assert.dom('[data-test-alert="error"]').exists(); - await vizzlySnapshot('alert-variants'); + await vizzlyScreenshot('alert-variants'); }); test('it renders without title', async function (assert) { @@ -48,6 +48,6 @@ module('Integration | Component | Alert', function (hooks) { assert.dom('[data-test-alert="no-title"]').exists(); assert.dom('[data-test-alert="no-title"] .alert-title').doesNotExist(); - await vizzlySnapshot('alert-no-title'); + await vizzlyScreenshot('alert-no-title'); }); }); diff --git a/clients/ember/test-app/tests/integration/components/button-test.gjs b/clients/ember/test-app/tests/integration/components/button-test.gjs index 3f7747f7..40136b19 100644 --- a/clients/ember/test-app/tests/integration/components/button-test.gjs +++ b/clients/ember/test-app/tests/integration/components/button-test.gjs @@ -2,7 +2,7 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { render, click } from '@ember/test-helpers'; import Button from 'test-ember-app/components/button'; -import { vizzlySnapshot } from '@vizzly-testing/ember/test-support'; +import { vizzlyScreenshot } from '@vizzly-testing/ember/test-support'; module('Integration | Component | Button', function (hooks) { setupRenderingTest(hooks); @@ -22,7 +22,7 @@ module('Integration | Component | Button', function (hooks) { assert.dom('[data-test-button="danger"]').hasText('Danger'); assert.dom('[data-test-button="ghost"]').hasText('Ghost'); - await vizzlySnapshot('button-variants'); + await vizzlyScreenshot('button-variants'); }); test('it renders all sizes', async function (assert) { @@ -38,7 +38,7 @@ module('Integration | Component | Button', function (hooks) { assert.dom('[data-test-button="medium"]').exists(); assert.dom('[data-test-button="large"]').exists(); - await vizzlySnapshot('button-sizes'); + await vizzlyScreenshot('button-sizes'); }); test('it renders disabled state', async function (assert) { @@ -52,7 +52,7 @@ module('Integration | Component | Button', function (hooks) { assert.dom('[data-test-button="enabled"]').isNotDisabled(); assert.dom('[data-test-button="disabled"]').isDisabled(); - await vizzlySnapshot('button-disabled-state'); + await vizzlyScreenshot('button-disabled-state'); }); test('it handles click events', async function (assert) { diff --git a/clients/ember/test-app/tests/integration/components/card-test.gjs b/clients/ember/test-app/tests/integration/components/card-test.gjs index 2bbff052..46d28377 100644 --- a/clients/ember/test-app/tests/integration/components/card-test.gjs +++ b/clients/ember/test-app/tests/integration/components/card-test.gjs @@ -3,7 +3,7 @@ import { setupRenderingTest } from 'ember-qunit'; import { render } from '@ember/test-helpers'; import Card from 'test-ember-app/components/card'; import Button from 'test-ember-app/components/button'; -import { vizzlySnapshot } from '@vizzly-testing/ember/test-support'; +import { vizzlyScreenshot } from '@vizzly-testing/ember/test-support'; module('Integration | Component | Card', function (hooks) { setupRenderingTest(hooks); @@ -20,7 +20,7 @@ module('Integration | Component | Card', function (hooks) { assert.dom('[data-test-card="basic"]').exists(); assert.dom('[data-test-card="basic"] .card-title').hasText('Basic Card'); - await vizzlySnapshot('card-basic'); + await vizzlyScreenshot('card-basic'); }); test('it renders elevated card', async function (assert) { @@ -34,7 +34,7 @@ module('Integration | Component | Card', function (hooks) { assert.dom('[data-test-card="elevated"]').hasClass('elevated'); - await vizzlySnapshot('card-elevated'); + await vizzlyScreenshot('card-elevated'); }); test('it renders card with actions and footer', async function (assert) { @@ -60,7 +60,7 @@ module('Integration | Component | Card', function (hooks) { assert.dom('[data-test-card="full"] .card-actions').exists(); assert.dom('[data-test-card="full"] .card-footer').exists(); - await vizzlySnapshot('card-with-actions-footer'); + await vizzlyScreenshot('card-with-actions-footer'); }); test('it renders card without title', async function (assert) { @@ -74,6 +74,6 @@ module('Integration | Component | Card', function (hooks) { assert.dom('[data-test-card="no-title"] .card-header').doesNotExist(); - await vizzlySnapshot('card-no-title'); + await vizzlyScreenshot('card-no-title'); }); }); diff --git a/clients/ember/test-app/tests/integration/components/data-table-test.gjs b/clients/ember/test-app/tests/integration/components/data-table-test.gjs index e0d9e258..edb52597 100644 --- a/clients/ember/test-app/tests/integration/components/data-table-test.gjs +++ b/clients/ember/test-app/tests/integration/components/data-table-test.gjs @@ -2,7 +2,7 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { render } from '@ember/test-helpers'; import DataTable from 'test-ember-app/components/data-table'; -import { vizzlySnapshot } from '@vizzly-testing/ember/test-support'; +import { vizzlyScreenshot } from '@vizzly-testing/ember/test-support'; module('Integration | Component | DataTable', function (hooks) { setupRenderingTest(hooks); @@ -31,7 +31,7 @@ module('Integration | Component | DataTable', function (hooks) { assert.dom('.table-header').exists({ count: 4 }); assert.dom('.table-row').exists({ count: 3 }); - await vizzlySnapshot('data-table-with-data'); + await vizzlyScreenshot('data-table-with-data'); }); test('it renders empty state', async function (assert) { @@ -50,7 +50,7 @@ module('Integration | Component | DataTable', function (hooks) { assert.dom('.table-empty').hasText('No users found'); - await vizzlySnapshot('data-table-empty'); + await vizzlyScreenshot('data-table-empty'); }); test('it renders with many rows', async function (assert) { @@ -72,7 +72,7 @@ module('Integration | Component | DataTable', function (hooks) { assert.dom('.table-row').exists({ count: 10 }); - await vizzlySnapshot('data-table-many-rows'); + await vizzlyScreenshot('data-table-many-rows'); }); test('it applies column alignment', async function (assert) { @@ -86,6 +86,6 @@ module('Integration | Component | DataTable', function (hooks) { assert.dom('.table-header.center').exists({ count: 2 }); assert.dom('.table-cell.center').exists({ count: 6 }); // 2 columns * 3 rows - await vizzlySnapshot('data-table-alignment'); + await vizzlyScreenshot('data-table-alignment'); }); }); diff --git a/clients/ember/test-app/tests/integration/components/form-field-test.gjs b/clients/ember/test-app/tests/integration/components/form-field-test.gjs index 9cba9fe9..8247d533 100644 --- a/clients/ember/test-app/tests/integration/components/form-field-test.gjs +++ b/clients/ember/test-app/tests/integration/components/form-field-test.gjs @@ -2,7 +2,7 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { render, fillIn } from '@ember/test-helpers'; import FormField from 'test-ember-app/components/form-field'; -import { vizzlySnapshot } from '@vizzly-testing/ember/test-support'; +import { vizzlyScreenshot } from '@vizzly-testing/ember/test-support'; module('Integration | Component | FormField', function (hooks) { setupRenderingTest(hooks); @@ -23,7 +23,7 @@ module('Integration | Component | FormField', function (hooks) { assert.dom('[data-test-form-field="username"] .form-label').hasText('Username'); assert.dom('[data-test-form-field="username"] .form-hint').hasText('Must be at least 3 characters'); - await vizzlySnapshot('form-field-text'); + await vizzlyScreenshot('form-field-text'); }); test('it renders with error state', async function (assert) { @@ -44,7 +44,7 @@ module('Integration | Component | FormField', function (hooks) { assert.dom('[data-test-form-field="email"] .form-error').hasText('Please enter a valid email address'); assert.dom('[data-test-form-field="email"] .required').exists(); - await vizzlySnapshot('form-field-error'); + await vizzlyScreenshot('form-field-error'); }); test('it renders textarea', async function (assert) { @@ -62,7 +62,7 @@ module('Integration | Component | FormField', function (hooks) { assert.dom('[data-test-form-field="message"] textarea').exists(); - await vizzlySnapshot('form-field-textarea'); + await vizzlyScreenshot('form-field-textarea'); }); test('it renders disabled state', async function (assert) { @@ -79,7 +79,7 @@ module('Integration | Component | FormField', function (hooks) { assert.dom('[data-test-form-field="readonly"] input').isDisabled(); - await vizzlySnapshot('form-field-disabled'); + await vizzlyScreenshot('form-field-disabled'); }); test('it renders various input types', async function (assert) { @@ -120,6 +120,6 @@ module('Integration | Component | FormField', function (hooks) { assert.dom('[data-test-form-field="email-field"] input').hasAttribute('type', 'email'); assert.dom('[data-test-form-field="number"] input').hasAttribute('type', 'number'); - await vizzlySnapshot('form-field-types'); + await vizzlyScreenshot('form-field-types'); }); }); diff --git a/clients/ember/test-app/tests/integration/components/modal-test.gjs b/clients/ember/test-app/tests/integration/components/modal-test.gjs index 35482f3b..0ab4af90 100644 --- a/clients/ember/test-app/tests/integration/components/modal-test.gjs +++ b/clients/ember/test-app/tests/integration/components/modal-test.gjs @@ -3,7 +3,7 @@ import { setupRenderingTest } from 'ember-qunit'; import { render, click } from '@ember/test-helpers'; import Modal from 'test-ember-app/components/modal'; import Button from 'test-ember-app/components/button'; -import { vizzlySnapshot } from '@vizzly-testing/ember/test-support'; +import { vizzlyScreenshot } from '@vizzly-testing/ember/test-support'; module('Integration | Component | Modal', function (hooks) { setupRenderingTest(hooks); @@ -18,7 +18,7 @@ module('Integration | Component | Modal', function (hooks) { assert.dom('[data-test-modal="test-modal"]').exists(); assert.dom('.modal-title').hasText('Test Modal'); - await vizzlySnapshot('modal-open'); + await vizzlyScreenshot('modal-open'); }); test('it does not render when closed', async function (assert) { @@ -39,7 +39,7 @@ module('Integration | Component | Modal', function (hooks) { ); assert.dom('.modal').hasClass('small'); - await vizzlySnapshot('modal-small'); + await vizzlyScreenshot('modal-small'); }); test('it renders large modal', async function (assert) { @@ -51,7 +51,7 @@ module('Integration | Component | Modal', function (hooks) { ); assert.dom('.modal').hasClass('large'); - await vizzlySnapshot('modal-large'); + await vizzlyScreenshot('modal-large'); }); test('it renders with footer', async function (assert) { @@ -69,7 +69,7 @@ module('Integration | Component | Modal', function (hooks) { ); assert.dom('.modal-footer').exists(); - await vizzlySnapshot('modal-with-footer'); + await vizzlyScreenshot('modal-with-footer'); }); test('it calls onClose when close button clicked', async function (assert) {