diff --git a/.github/workflows/cypress_integration_testing.yml b/.github/workflows/cypress_integration_testing.yml new file mode 100644 index 00000000..8a7435d2 --- /dev/null +++ b/.github/workflows/cypress_integration_testing.yml @@ -0,0 +1,26 @@ +name: Cypress Tests + +on: + workflow_dispatch: + schedule: + cron: 45 4 1 */1 * + +jobs: + tap_testing: + runs-on: ubuntu-latest + strategy: + matrix: + resource_type: [water, food, foraging, bathroom] + steps: + - uses: actions/setup-node@v4 + with: + node-version: 20 + - name: Checkout + uses: actions/checkout@v4 + - name: Cypress run + uses: cypress-io/github-action@v6 + with: + start: yarn start:cypress + browser: chrome + spec: cypress/integration/desktop/tapcheck.${{ matrix.resource_type }}.cy.js + config-file: cypress.integration.config.mjs diff --git a/cypress.integration.config.mjs b/cypress.integration.config.mjs new file mode 100644 index 00000000..196a8fb4 --- /dev/null +++ b/cypress.integration.config.mjs @@ -0,0 +1,35 @@ +import { defineConfig } from 'cypress'; +import { createClient } from '@supabase/supabase-js'; + +const databaseUrl = 'https://wantycfbnzzocsbthqzs.supabase.co'; +const resourceDatabaseName = 'resources'; +const databaseApiKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6IndhbnR5Y2Zibnp6b2NzYnRocXpzIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzcwNDY2OTgsImV4cCI6MjA1MjYyMjY5OH0.yczsMOx3Y-zsWu-GjYEajIb0yw9fYWEIUglmmfM1zCY'; + +const supabase = createClient(databaseUrl, databaseApiKey); + +async function getResources() { + const { data, error } = await supabase.from(resourceDatabaseName).select('*'); + if (error) { + throw error; + } + return data; +} + +export default defineConfig({ + e2e: { + setupNodeEvents(on, config) { + on('task', { + getResources, + log(message) { + console.log(message) + + return null + } + }) + }, + watchForFileChanges: false, + specPattern: 'cypress/integration/desktop/*.cy.{js,jsx,ts,tsx}', + baseUrl: 'http://localhost:5173', + video: true + } +}); diff --git a/cypress/integration/desktop/tapcheck.bathroom.cy.js b/cypress/integration/desktop/tapcheck.bathroom.cy.js new file mode 100644 index 00000000..eee97dbb --- /dev/null +++ b/cypress/integration/desktop/tapcheck.bathroom.cy.js @@ -0,0 +1,76 @@ +// Primary Goal: Test each tap in without incurring heavy use of the Maps API, such as loading dynamic maps multiple times + +// - Builds a queue of all RESOURCE_TYPE locations +// - While the queue is not empty, pulls an item from the queue and tests clicking it +// - If the site crashes, records the tap that crashed, reloads the site, and continues looping + +const RESOURCE_TYPE = "BATHROOM" + +let exception_ocurred = false; +let exception_message = ""; + +// For each resource type, test each site detail permutation and confirm only the expected number of taps appear. +describe('site info', () => { + beforeEach(() => { + cy.visit('/'); + }); + + it(`should tap through all ${RESOURCE_TYPE} sites`, () => { + let test_target_queue = [5, 4, 3, 2, 1]; + let test_target_failures = []; + + cy.on('uncaught:exception', (err, runnable) => { + exception_message = err.message; + exception_ocurred = true; + return false; + }) + + const testLocation = () => { + const target_location_id = test_target_queue.pop() + cy.task('log', `Testing ${target_location_id}`) + cy.get(`[title=data-cy-${target_location_id}]`).click({ force: true, timeout: 8000 }).then(() => { + cy.task('log', `Tested location ${target_location_id}`) + if (!exception_ocurred) { + cy.get('[data-cy=close-selected-tap] > button').click() + } + else { + cy.task('log', `An error ocurred when loading location ${target_location_id}: ${exception_message}`) + test_target_failures.push(target_location_id) + cy.reload() + cy.switchResourceType(RESOURCE_TYPE) + cy.zoomMapOutMax() + } + if (test_target_queue.length == 0) { + cy.task('log', `Errors ocurred when loading the following ${test_target_failures.length} locations: ${test_target_failures}`) + + if (test_target_failures.length > 0) { + throw new Error(`Errors ocurred when loading the following locations: ${test_target_failures}`) + } + return + } + exception_ocurred = false + exception_message = ""; + + cy.wait(2000, { log: false }) + testLocation() + }) + } + + cy.switchResourceType(RESOURCE_TYPE) + cy.zoomMapOutMax() + + cy.task('getResources').then((resources) => { + let water_resources = resources.filter((resource) => resource['resource_type'] === RESOURCE_TYPE) + test_target_queue = [...Array(water_resources.length).keys()] + cy.task('log', `test_target_queue: ${test_target_queue}`) + + // For targeted debugging + // test_target_queue = [106, 107, 108, 109, 110]; + + exception_ocurred = false; + exception_message = ""; + testLocation() + }) + }); +} +); diff --git a/cypress/integration/desktop/tapcheck.food.cy.js b/cypress/integration/desktop/tapcheck.food.cy.js new file mode 100644 index 00000000..18545d6d --- /dev/null +++ b/cypress/integration/desktop/tapcheck.food.cy.js @@ -0,0 +1,76 @@ +// Primary Goal: Test each tap in without incurring heavy use of the Maps API, such as loading dynamic maps multiple times + +// - Builds a queue of all RESOURCE_TYPE locations +// - While the queue is not empty, pulls an item from the queue and tests clicking it +// - If the site crashes, records the tap that crashed, reloads the site, and continues looping + +const RESOURCE_TYPE = "FOOD" + +let exception_ocurred = false; +let exception_message = ""; + +// For each resource type, test each site detail permutation and confirm only the expected number of taps appear. +describe('site info', () => { + beforeEach(() => { + cy.visit('/'); + }); + + it(`should tap through all ${RESOURCE_TYPE} sites`, () => { + let test_target_queue = [5, 4, 3, 2, 1]; + let test_target_failures = []; + + cy.on('uncaught:exception', (err, runnable) => { + exception_message = err.message; + exception_ocurred = true; + return false; + }) + + const testLocation = () => { + const target_location_id = test_target_queue.pop() + cy.task('log', `Testing ${target_location_id}`) + cy.get(`[title=data-cy-${target_location_id}]`).click({ force: true, timeout: 8000 }).then(() => { + cy.task('log', `Tested location ${target_location_id}`) + if (!exception_ocurred) { + cy.get('[data-cy=close-selected-tap] > button').click() + } + else { + cy.task('log', `An error ocurred when loading location ${target_location_id}: ${exception_message}`) + test_target_failures.push(target_location_id) + cy.reload() + cy.switchResourceType(RESOURCE_TYPE) + cy.zoomMapOutMax() + } + if (test_target_queue.length == 0) { + cy.task('log', `Errors ocurred when loading the following ${test_target_failures.length} locations: ${test_target_failures}`) + + if (test_target_failures.length > 0) { + throw new Error(`Errors ocurred when loading the following locations: ${test_target_failures}`) + } + return + } + exception_ocurred = false + exception_message = ""; + + cy.wait(2000, { log: false }) + testLocation() + }) + } + + cy.switchResourceType(RESOURCE_TYPE) + cy.zoomMapOutMax() + + cy.task('getResources').then((resources) => { + let water_resources = resources.filter((resource) => resource['resource_type'] === RESOURCE_TYPE) + test_target_queue = [...Array(water_resources.length).keys()] + cy.task('log', `test_target_queue: ${test_target_queue}`) + + // For targeted debugging + // test_target_queue = [106, 107, 108, 109, 110]; + + exception_ocurred = false; + exception_message = ""; + testLocation() + }) + }); +} +); diff --git a/cypress/integration/desktop/tapcheck.foraging.cy.js b/cypress/integration/desktop/tapcheck.foraging.cy.js new file mode 100644 index 00000000..fbb9f88c --- /dev/null +++ b/cypress/integration/desktop/tapcheck.foraging.cy.js @@ -0,0 +1,76 @@ +// Primary Goal: Test each tap in without incurring heavy use of the Maps API, such as loading dynamic maps multiple times + +// - Builds a queue of all RESOURCE_TYPE locations +// - While the queue is not empty, pulls an item from the queue and tests clicking it +// - If the site crashes, records the tap that crashed, reloads the site, and continues looping + +const RESOURCE_TYPE = "FORAGE" + +let exception_ocurred = false; +let exception_message = ""; + +// For each resource type, test each site detail permutation and confirm only the expected number of taps appear. +describe('site info', () => { + beforeEach(() => { + cy.visit('/'); + }); + + it(`should tap through all ${RESOURCE_TYPE} sites`, () => { + let test_target_queue = [5, 4, 3, 2, 1]; + let test_target_failures = []; + + cy.on('uncaught:exception', (err, runnable) => { + exception_message = err.message; + exception_ocurred = true; + return false; + }) + + const testLocation = () => { + const target_location_id = test_target_queue.pop() + cy.task('log', `Testing ${target_location_id}`) + cy.get(`[title=data-cy-${target_location_id}]`).click({ force: true, timeout: 8000 }).then(() => { + cy.task('log', `Tested location ${target_location_id}`) + if (!exception_ocurred) { + cy.get('[data-cy=close-selected-tap] > button').click() + } + else { + cy.task('log', `An error ocurred when loading location ${target_location_id}: ${exception_message}`) + test_target_failures.push(target_location_id) + cy.reload() + cy.switchResourceType(RESOURCE_TYPE) + cy.zoomMapOutMax() + } + if (test_target_queue.length == 0) { + cy.task('log', `Errors ocurred when loading the following ${test_target_failures.length} locations: ${test_target_failures}`) + + if (test_target_failures.length > 0) { + throw new Error(`Errors ocurred when loading the following locations: ${test_target_failures}`) + } + return + } + exception_ocurred = false + exception_message = ""; + + cy.wait(2000, { log: false }) + testLocation() + }) + } + + cy.switchResourceType(RESOURCE_TYPE) + cy.zoomMapOutMax() + + cy.task('getResources').then((resources) => { + let water_resources = resources.filter((resource) => resource['resource_type'] === RESOURCE_TYPE) + test_target_queue = [...Array(water_resources.length).keys()] + cy.task('log', `test_target_queue: ${test_target_queue}`) + + // For targeted debugging + // test_target_queue = [106, 107, 108, 109, 110]; + + exception_ocurred = false; + exception_message = ""; + testLocation() + }) + }); +} +); diff --git a/cypress/integration/desktop/tapcheck.water.cy.js b/cypress/integration/desktop/tapcheck.water.cy.js new file mode 100644 index 00000000..7e317522 --- /dev/null +++ b/cypress/integration/desktop/tapcheck.water.cy.js @@ -0,0 +1,76 @@ +// Primary Goal: Test each tap in without incurring heavy use of the Maps API, such as loading dynamic maps multiple times + +// - Builds a queue of all RESOURCE_TYPE locations +// - While the queue is not empty, pulls an item from the queue and tests clicking it +// - If the site crashes, records the tap that crashed, reloads the site, and continues looping + +const RESOURCE_TYPE = "WATER" + +let exception_ocurred = false; +let exception_message = ""; + +// For each resource type, test each site detail permutation and confirm only the expected number of taps appear. +describe('site info', () => { + beforeEach(() => { + cy.visit('/'); + }); + + it(`should tap through all ${RESOURCE_TYPE} sites`, () => { + let test_target_queue = [5, 4, 3, 2, 1]; + let test_target_failures = []; + + cy.on('uncaught:exception', (err, runnable) => { + exception_message = err.message; + exception_ocurred = true; + return false; + }) + + const testLocation = () => { + const target_location_id = test_target_queue.pop() + cy.task('log', `Testing ${target_location_id}`) + cy.get(`[title=data-cy-${target_location_id}]`).click({ force: true, timeout: 8000 }).then(() => { + cy.task('log', `Tested location ${target_location_id}`) + if (!exception_ocurred) { + cy.get('[data-cy=close-selected-tap] > button').click() + } + else { + cy.task('log', `An error ocurred when loading location ${target_location_id}: ${exception_message}`) + test_target_failures.push(target_location_id) + cy.reload() + cy.switchResourceType(RESOURCE_TYPE) + cy.zoomMapOutMax() + } + if (test_target_queue.length == 0) { + cy.task('log', `Errors ocurred when loading the following ${test_target_failures.length} locations: ${test_target_failures}`) + + if (test_target_failures.length > 0) { + throw new Error(`Errors ocurred when loading the following locations: ${test_target_failures}`) + } + return + } + exception_ocurred = false + exception_message = ""; + + cy.wait(2000, { log: false }) + testLocation() + }) + } + + cy.switchResourceType(RESOURCE_TYPE) + cy.zoomMapOutMax() + + cy.task('getResources').then((resources) => { + let water_resources = resources.filter((resource) => resource['resource_type'] === RESOURCE_TYPE) + test_target_queue = [...Array(water_resources.length).keys()] + cy.task('log', `test_target_queue: ${test_target_queue}`) + + // For targeted debugging + // test_target_queue = [106, 107, 108, 109, 110]; + + exception_ocurred = false; + exception_message = ""; + testLocation() + }) + }); +} +); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index ca4d256f..fad33469 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -23,3 +23,16 @@ // // -- This will overwrite an existing command -- // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) + +Cypress.Commands.add('switchResourceType', (resourceType) => { + cy.get('[data-cy=button-resource-type-menu]').click() + cy.get(`[data-cy=button-${resourceType}-data-selector]`).click() + cy.get('[data-cy=button-resource-type-menu]').click() +}) + +Cypress.Commands.add('zoomMapOutMax', () => { + // Zoom out the map to allow all taps to be rendered globally + for (let n = 0; n < 15; n++) { + cy.get('div > [title="Zoom out"]').click() + } +}) diff --git a/package.json b/package.json index 86e0b7c1..7ae0398a 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,8 @@ }, "scripts": { "start": "vite", - "start:cypress": "VITE_CYPRESS_TEST=true yarn start", + "start:cypress": "VITE_CYPRESS_TEST=true VITE_GMAP_OPTIMIZED=false yarn start", + "start:integration": "VITE_GMAP_OPTIMIZED=false yarn start", "build": "vite build", "testDataGen": "node cypress/testDataGenerator.js", "test": "yarn testDataGen && cypress run", diff --git a/src/components/ReactGoogleMaps/ReactGoogleMaps.jsx b/src/components/ReactGoogleMaps/ReactGoogleMaps.jsx index 74a644e3..4d9bd4d8 100644 --- a/src/components/ReactGoogleMaps/ReactGoogleMaps.jsx +++ b/src/components/ReactGoogleMaps/ReactGoogleMaps.jsx @@ -302,6 +302,9 @@ const ReactGoogleMaps = () => { // This is used for marker targeting as we are unable to add custom properties with this library. // We should eventually replace this so that we can still enable the use of screen readers in the future. title={`data-cy-${index}`} + // Setting the map to be optimized unless it is explicitly set to false + // Setting it to false is used in testing to allow for targetting Markers via the DOM + optimized={import.meta.env.VITE_GMAP_OPTIMIZED !== 'false'} /> ); })} diff --git a/src/components/SelectedTapDetails/SelectedTapDetails.jsx b/src/components/SelectedTapDetails/SelectedTapDetails.jsx index 8ed03540..e826c108 100644 --- a/src/components/SelectedTapDetails/SelectedTapDetails.jsx +++ b/src/components/SelectedTapDetails/SelectedTapDetails.jsx @@ -256,30 +256,32 @@ const SelectedTapDetails = ({ {/* */} {/* On mobile, show the minimize button. On desktop, show the close button */} - {isMobile && ( - - - - )} - {!isMobile && ( - { - closeModal(); - }} - sx={{ - position: 'absolute', - right: '20px', - top: 5, - color: 'black' - }} - size="large" - > - - - )} - {/* Currently the three dot button does nothing */} +
+ {isMobile && ( + + + + )} + {!isMobile && ( + { + closeModal(); + }} + sx={{ + position: 'absolute', + right: '20px', + top: 5, + color: 'black' + }} + size="large" + > + + + )} + {/* Currently the three dot button does nothing */} +
)}