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 && (
-