From 53eef4fbd7e1caf32f0183cadbc0e4cf05524c34 Mon Sep 17 00:00:00 2001 From: Zachary Williams Date: Tue, 11 Oct 2022 09:21:32 -0500 Subject: [PATCH] fix: angular and nuxt ct tests now fail on uncaught exceptions (#24122) --- npm/angular/src/mount.ts | 20 ++++++- npm/vue2/src/index.ts | 10 ++-- .../e2e/runner/ct-framework-errors.cy.ts | 58 ++++++++++++++++--- .../angular/src/app/components/errors.ts | 2 +- 4 files changed, 76 insertions(+), 14 deletions(-) diff --git a/npm/angular/src/mount.ts b/npm/angular/src/mount.ts index 0743f3aa0bc8..7af3a98ed5e0 100644 --- a/npm/angular/src/mount.ts +++ b/npm/angular/src/mount.ts @@ -8,7 +8,7 @@ window.Mocha['__zone_patch__'] = false import 'zone.js/testing' import { CommonModule } from '@angular/common' -import { Component, EventEmitter, SimpleChange, SimpleChanges, Type } from '@angular/core' +import { Component, ErrorHandler, EventEmitter, Injectable, SimpleChange, SimpleChanges, Type } from '@angular/core' import { ComponentFixture, getTestBed, @@ -99,6 +99,13 @@ export type MountResponse = { // so we'll patch here pending a fix in that library globalThis.it.skip = globalThis.xit +@Injectable() +class CypressAngularErrorHandler implements ErrorHandler { + handleError (error: Error): void { + throw error + } +} + /** * Bootstraps the TestModuleMetaData passed to the TestBed * @@ -120,6 +127,17 @@ function bootstrapModule ( testModuleMetaData.imports = [] } + if (!testModuleMetaData.providers) { + testModuleMetaData.providers = [] + } + + // Replace default error handler since it will swallow uncaught exceptions. + // We want these to be uncaught so Cypress catches it and fails the test + testModuleMetaData.providers.push({ + provide: ErrorHandler, + useClass: CypressAngularErrorHandler, + }) + // check if the component is a standalone component if ((component as any).ɵcmp.standalone) { testModuleMetaData.imports.push(component) diff --git a/npm/vue2/src/index.ts b/npm/vue2/src/index.ts index 0966bdf0d79b..86d94af09d2b 100644 --- a/npm/vue2/src/index.ts +++ b/npm/vue2/src/index.ts @@ -273,11 +273,11 @@ declare global { * @see https://github.com/cypress-io/cypress/issues/7910 */ function failTestOnVueError (err, vm, info) { - console.error(`Vue error`) - console.error(err) - console.error('component:', vm) - console.error('info:', info) - window.top.onerror(err) + // Vue 2 try catches the error-handler so push the error to be caught outside + // of the handler. + setTimeout(() => { + throw err + }) } function registerAutoDestroy ($destroy: () => void) { diff --git a/packages/app/cypress/e2e/runner/ct-framework-errors.cy.ts b/packages/app/cypress/e2e/runner/ct-framework-errors.cy.ts index f17a425adf17..a41c697ca0a3 100644 --- a/packages/app/cypress/e2e/runner/ct-framework-errors.cy.ts +++ b/packages/app/cypress/e2e/runner/ct-framework-errors.cy.ts @@ -40,9 +40,11 @@ function loadErrorSpec (options: Options): VerifyFunc { cy.get('.runnable-header', { log: false }).should('be.visible') // Extended timeout needed due to lengthy Angular bootstrap on Windows cy.contains('Your tests are loading...', { timeout: 60000, log: false }).should('not.exist') + // Then ensure the tests have finished + cy.get('[aria-label="Rerun all tests"]', { timeout: 30000 }) cy.findByLabelText('Stats').within(() => { - cy.get('.passed .num', { timeout: 30000 }).should('have.text', `${passCount}`) - cy.get('.failed .num', { timeout: 30000 }).should('have.text', `${failCount}`) + cy.get('.passed .num').should('have.text', `${passCount}`) + cy.get('.failed .num').should('have.text', `${failCount}`) }) // Return scoped verify function with spec options baked in @@ -285,12 +287,14 @@ describe('Nuxt', { projectName: 'nuxtjs-vue2-configured', configFile: 'cypress.config.js', filePath: 'components/Errors.cy.js', - failCount: 3, + failCount: 4, }) verify('error on mount', { fileName: 'Errors.vue', line: 19, + uncaught: true, + uncaughtMessage: 'mount error', message: [ 'mount error', ], @@ -298,6 +302,21 @@ describe('Nuxt', { codeFrameText: 'Errors.vue', }) + verify('sync error', { + fileName: 'Errors.vue', + line: 24, + uncaught: true, + uncaughtMessage: 'sync error', + message: [ + 'The following error originated from your application code', + 'sync error', + ], + stackRegex: /Errors\.vue:24/, + codeFrameText: 'Errors.vue', + }).then(() => { + verifyErrorOnlyCapturedOnce('Error: sync error') + }) + verify('async error', { fileName: 'Errors.vue', line: 28, @@ -413,12 +432,37 @@ angularVersions.forEach((angularVersion) => { projectName: `angular-${angularVersion}`, configFile: 'cypress.config.ts', filePath: 'src/app/errors.cy.ts', - failCount: 1, + failCount: 3, + passCount: 1, + }) + + verify('sync error', { + fileName: 'errors.ts', + line: 14, + column: 11, + uncaught: true, + uncaughtMessage: 'sync error', + message: [ + 'The following error originated from your application code', + 'sync error', + ], + }).then(() => { + verifyErrorOnlyCapturedOnce('Error: sync error') }) - // Angular uses ZoneJS which encapsulates errors thrown by components - // Thus, the mount, sync, and async error case errors will not propagate to Cypress - // and thus won't fail the tests + verify('async error', { + fileName: 'errors.ts', + line: 19, + column: 13, + uncaught: true, + uncaughtMessage: 'async error', + message: [ + 'The following error originated from your application code', + 'async error', + ], + }).then(() => { + verifyErrorOnlyCapturedOnce('Error: async error') + }) verify('command failure', { line: 21, diff --git a/system-tests/project-fixtures/angular/src/app/components/errors.ts b/system-tests/project-fixtures/angular/src/app/components/errors.ts index 475871a4f11c..ba83b8fd3e4c 100644 --- a/system-tests/project-fixtures/angular/src/app/components/errors.ts +++ b/system-tests/project-fixtures/angular/src/app/components/errors.ts @@ -17,6 +17,6 @@ export class ErrorsComponent { asyncError() { setTimeout(() => { throw new Error('async error') - }, 50) + }) } }