diff --git a/cypress/component/FlatmapServerTest.cy.js b/cypress/component/FlatmapServerTest.cy.js new file mode 100644 index 00000000..439c45f0 --- /dev/null +++ b/cypress/component/FlatmapServerTest.cy.js @@ -0,0 +1,86 @@ +import MultiFlatmapVuer from '../../src/components/MultiFlatmapVuer.vue'; +const FLATMAP_API = 'https://mapcore-demo.org/current/flatmap/v3/'; + +describe('MultiFlatmapVuer Error Handling', () => { + + it('should handle 500 Internal Server Error', () => { + cy.intercept('GET', FLATMAP_API, { + statusCode: 500, + body: 'Internal Server Error', + }).as('serverError'); + + cy.mount(MultiFlatmapVuer, { + props: { + flatmapAPI: FLATMAP_API, + initial: 'Rat', + availableSpecies: { + Rat: { + taxo: 'NCBITaxon:10114', + iconClass: 'mapicon-icon_rat', + }, + }, + }, + }); + + cy.wait('@serverError'); + + cy.get('.flatmap-error').should('exist'); + cy.contains('MultiFlatmap Error!').should('be.visible'); + cy.contains('unexpected error').should('be.visible'); + cy.contains('try again later').should('be.visible'); + }); + + it('handles 500 with valid JSON body (would bypass catch)', () => { + cy.intercept('GET', '**/flatmap/v3/', { + statusCode: 500, + headers: { 'content-type': 'application/json' }, + body: [], // valid JSON array -> response.json() resolves + }).as('serverErrorJson'); + + cy.mount(MultiFlatmapVuer, { + props: { + flatmapAPI: FLATMAP_API, + initial: 'Rat', + availableSpecies: { Rat: { taxo: 'NCBITaxon:10114' } }, + }, + }); + + cy.wait('@serverErrorJson'); + cy.get('.flatmap-error').should('exist'); + }); + + it('should handle 404 endpoint error', () => { + cy.intercept('GET', FLATMAP_API, { + statusCode: 404, + body: { status_code: 404 }, + }).as('notFoundError'); + + cy.mount(MultiFlatmapVuer, { + props: { + flatmapAPI: FLATMAP_API, + initial: 'Rat', + }, + }); + + cy.wait('@notFoundError'); + + cy.contains('flatmap API endpoint is incorrect').should('be.visible'); + }); + + it('should handle network failure', () => { + cy.intercept('GET', FLATMAP_API, { + forceNetworkError: true, + }).as('networkError'); + + cy.mount(MultiFlatmapVuer, { + props: { + flatmapAPI: FLATMAP_API, + initial: 'Rat', + }, + }); + + cy.wait('@networkError'); + + cy.get('.flatmap-error').should('exist'); + }); +}); diff --git a/src/components.d.ts b/src/components.d.ts index 5600e53a..f762859d 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -8,6 +8,7 @@ export {} declare module 'vue' { export interface GlobalComponents { DynamicLegends: typeof import('./components/legends/DynamicLegends.vue')['default'] + ElAutocomplete: typeof import('element-plus/es')['ElAutocomplete'] ElButton: typeof import('element-plus/es')['ElButton'] ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup'] @@ -24,6 +25,7 @@ declare module 'vue' { ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] ElRow: typeof import('element-plus/es')['ElRow'] ElSelect: typeof import('element-plus/es')['ElSelect'] + ElSwitch: typeof import('element-plus/es')['ElSwitch'] FlatmapError: typeof import('./components/FlatmapError.vue')['default'] FlatmapVuer: typeof import('./components/FlatmapVuer.vue')['default'] LegendItem: typeof import('./components/legends/LegendItem.vue')['default'] diff --git a/src/components/MultiFlatmapVuer.vue b/src/components/MultiFlatmapVuer.vue index 0fe5f6cf..cdfe1450 100644 --- a/src/components/MultiFlatmapVuer.vue +++ b/src/components/MultiFlatmapVuer.vue @@ -166,10 +166,35 @@ export default { if (this.requireInitialisation) { //It has not been initialised yet this.requireInitialisation = false - fetch(this.flatmapAPI) - .then((response) => response.json()) + const controller = new AbortController(); + const signal = controller.signal; + const timeoutId = setTimeout(() => controller.abort(), 5000); + + fetch(this.flatmapAPI, {signal}) + .then((response) => { + if (!response.ok) { + // HTTP-level errors + if (response.status === 404) { + this.multiflatmapError = {}; + this.multiflatmapError['title'] = 'MultiFlatmap Error!'; + this.multiflatmapError['messages'] = [ + `Sorry, the component could not be loaded because the specified + flatmap API endpoint is incorrect. Please check the endpoint URL + or contact support if the problem persists.` + ]; + this.initialised = true; + resolve(); + this.resolveList.forEach((other) => other()); + + return Promise.reject({ handled: true }); + } + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + return response.json() + }) .then((data) => { - if (data.status_code === 404) { + // Application-level 404 in a 200 response + if (data && data.status_code === 404) { console.error('Flatmap API endpoint is incorrect', data); this.multiflatmapError = {}; this.multiflatmapError['title'] = 'MultiFlatmap Error!'; @@ -178,6 +203,11 @@ export default { flatmap API endpoint is incorrect. Please check the endpoint URL or contact support if the problem persists.` ]; + + this.initialised = true; + resolve(); + this.resolveList.forEach((other) => other()); + return; } //Check each key in the provided availableSpecies against the one Object.keys(this.availableSpecies).forEach((key) => { @@ -237,6 +267,7 @@ export default { }) }) .catch((error) => { + if (error && error.handled) return; console.error('Error fetching flatmap:', error) this.initialised = true; this.multiflatmapError = {}; @@ -251,6 +282,9 @@ export default { other() }) }) + .finally(() => { + clearTimeout(timeoutId); + }); } else if (this.initialised) { //resolve as it has been initialised resolve()