From 9dd2f8c5b056fd5c3e5dbe361784c61cc06780d1 Mon Sep 17 00:00:00 2001 From: Cameron Blankenbuehler Date: Sat, 19 Aug 2023 00:04:22 -0400 Subject: [PATCH 1/6] Add EventCard and GroupEventCard components - The admin dashboard now renders recurring events (with a groupId) as a single item on the admin dashboard --- .../src/features/adminDashboard/EventCard.js | 18 +++++ .../features/adminDashboard/GroupEventCard.js | 12 +++ client/src/pages/AdminDashboard.js | 80 +++++++++++++------ 3 files changed, 84 insertions(+), 26 deletions(-) create mode 100644 client/src/features/adminDashboard/EventCard.js create mode 100644 client/src/features/adminDashboard/GroupEventCard.js diff --git a/client/src/features/adminDashboard/EventCard.js b/client/src/features/adminDashboard/EventCard.js new file mode 100644 index 0000000..5111ce8 --- /dev/null +++ b/client/src/features/adminDashboard/EventCard.js @@ -0,0 +1,18 @@ +import React from "react"; + +function EventCard({ event }) { + return ( +
+ +
+ ); +} + +export default EventCard; diff --git a/client/src/features/adminDashboard/GroupEventCard.js b/client/src/features/adminDashboard/GroupEventCard.js new file mode 100644 index 0000000..b7a59ab --- /dev/null +++ b/client/src/features/adminDashboard/GroupEventCard.js @@ -0,0 +1,12 @@ +import React from "react"; +import EventCard from "features/adminDashboard/EventCard"; + +function GroupEventCard({ events }) { + return ( +
+ +
+ ); +} + +export default GroupEventCard; diff --git a/client/src/pages/AdminDashboard.js b/client/src/pages/AdminDashboard.js index c1ab614..044e9ff 100644 --- a/client/src/pages/AdminDashboard.js +++ b/client/src/pages/AdminDashboard.js @@ -2,15 +2,39 @@ import React from "react"; import { useEffect, useState } from "react"; import "index.css"; import DataService from "services/dataService"; +import EventCard from "features/adminDashboard/EventCard"; +import GroupEventCard from "features/adminDashboard/GroupEventCard"; export const AdminDashboard = () => { - const [allEvents, setAllEvents] = useState({}); + const [singleEvents, setSingleEvents] = useState({}); + const [groupEvents, setGroupEvents] = useState({}); const [loading, setLoading] = useState(false); const handleGetAllEvents = async () => { setLoading(true); const response = await DataService.getAll(); - setAllEvents(response.data); + + // Filter only single events + setSingleEvents(response.data.filter(e => e.groupId === null)); + + // Group remaining events by groupId + let gEvents = response.data.reduce((map, event) => { + // Ignore events without a groupId + if (event.groupId === null) { + return map; + } + + // Collect events with a groupId + if (event.groupId in map) { + map[event.groupId].push(event); + } else { + map[event.groupId] = [event]; + } + + return map; + }, {}); + + setGroupEvents(gEvents); setLoading(false); }; @@ -20,7 +44,7 @@ export const AdminDashboard = () => { fetch(); }, []); - let eventStatus = loading ? "LOADING..." : Object.keys(allEvents).length; + let eventStatus = loading ? "LOADING..." : Object.keys(singleEvents).length; return (
@@ -35,29 +59,33 @@ export const AdminDashboard = () => { 🗘 Refresh Events
-
- {/* Display all events in an unordered list when the data is finished loading */} - {loading || - Object.keys(allEvents).map(key => { - return ( -
-
    -
  • - {allEvents[key].title} -
  • -
  • - {allEvents[key].description} -
  • -
    -
  • Scheduled by: {allEvents[key].user.displayName}
  • -
-
- ); - })} -
+
+
+ {/* Display all events in an unordered list when the data is finished loading */} + {loading || + Object.keys(singleEvents).map(key => { + return ( + + ); + })} +
+

RECURRING EVENTS ({Object.keys(groupEvents).length})

+
+
+ {loading || + Object.keys(groupEvents).map(key => { + return ( + + ); + })} +
+
); }; From 0a942a21e00bc0dcb3d64413f84c09e9870f45bb Mon Sep 17 00:00:00 2001 From: Cameron Blankenbuehler Date: Sat, 19 Aug 2023 11:46:05 -0400 Subject: [PATCH 2/6] Style singular and concurrent event sections --- .../features/adminDashboard/GroupEventCard.js | 2 +- client/src/pages/AdminDashboard.js | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/client/src/features/adminDashboard/GroupEventCard.js b/client/src/features/adminDashboard/GroupEventCard.js index b7a59ab..8bf4282 100644 --- a/client/src/features/adminDashboard/GroupEventCard.js +++ b/client/src/features/adminDashboard/GroupEventCard.js @@ -3,7 +3,7 @@ import EventCard from "features/adminDashboard/EventCard"; function GroupEventCard({ events }) { return ( -
+
); diff --git a/client/src/pages/AdminDashboard.js b/client/src/pages/AdminDashboard.js index 044e9ff..eb8f8bd 100644 --- a/client/src/pages/AdminDashboard.js +++ b/client/src/pages/AdminDashboard.js @@ -59,7 +59,12 @@ export const AdminDashboard = () => { 🗘 Refresh Events -
+
+

+ Singular Events ( + {Object.keys(groupEvents).length} + ) +

{/* Display all events in an unordered list when the data is finished loading */} {loading || @@ -72,9 +77,15 @@ export const AdminDashboard = () => { ); })}
-

RECURRING EVENTS ({Object.keys(groupEvents).length})

-
+

+ Recurring Events ( + + {Object.keys(groupEvents).length} + + ) +

+
{loading || Object.keys(groupEvents).map(key => { return ( From 69be73b65cdb8fc6cc56c547f5f77d33f479be25 Mon Sep 17 00:00:00 2001 From: Cameron Blankenbuehler Date: Wed, 23 Aug 2023 17:14:12 -0400 Subject: [PATCH 3/6] Fix event counts for singular and recurring event sub-sections --- client/src/pages/AdminDashboard.js | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/client/src/pages/AdminDashboard.js b/client/src/pages/AdminDashboard.js index eb8f8bd..60760de 100644 --- a/client/src/pages/AdminDashboard.js +++ b/client/src/pages/AdminDashboard.js @@ -44,7 +44,15 @@ export const AdminDashboard = () => { fetch(); }, []); - let eventStatus = loading ? "LOADING..." : Object.keys(singleEvents).length; + const singleEventCount = loading + ? "LOADING..." + : Object.keys(singleEvents).length; + const groupEventCount = loading + ? "LOADING..." + : Object.keys(groupEvents).length; + const eventStatus = loading + ? "LOADING..." + : singleEventCount + groupEventCount; return (
@@ -59,12 +67,12 @@ export const AdminDashboard = () => { 🗘 Refresh Events
-
+

Singular Events ( - {Object.keys(groupEvents).length} - ) + {singleEventCount})

+
{/* Display all events in an unordered list when the data is finished loading */} {loading || @@ -80,10 +88,7 @@ export const AdminDashboard = () => {

Recurring Events ( - - {Object.keys(groupEvents).length} - - ) + {groupEventCount})


{loading || From d9475cebfcb63b166687ae7ea7efd6599fe353cb Mon Sep 17 00:00:00 2001 From: Cameron Blankenbuehler Date: Wed, 23 Aug 2023 21:10:45 -0400 Subject: [PATCH 4/6] Refactor Admin Dashboard tests for split single and recurring event sections --- .../src/features/adminDashboard/EventCard.js | 7 +- cypress/e2e/admin-dashboard.cy.js | 123 ++++++++++++++---- cypress/support/commands.js | 11 ++ 3 files changed, 114 insertions(+), 27 deletions(-) diff --git a/client/src/features/adminDashboard/EventCard.js b/client/src/features/adminDashboard/EventCard.js index 5111ce8..8f4842c 100644 --- a/client/src/features/adminDashboard/EventCard.js +++ b/client/src/features/adminDashboard/EventCard.js @@ -2,7 +2,10 @@ import React from "react"; function EventCard({ event }) { return ( -
+
  • {event.title} @@ -11,7 +14,7 @@ function EventCard({ event }) {
  • Scheduled by: {event.user.displayName}
-
+
); } diff --git a/cypress/e2e/admin-dashboard.cy.js b/cypress/e2e/admin-dashboard.cy.js index cb7262a..6dd976f 100644 --- a/cypress/e2e/admin-dashboard.cy.js +++ b/cypress/e2e/admin-dashboard.cy.js @@ -5,31 +5,59 @@ import tgt from "../support/tgt"; // Generate test event const TEST_EVENT_AUTHOR = "100_DEVER"; const TEST_EVENT_GROUP_ID = "admin_text_event_id"; -const TEST_EVENTS = [ +const SINGULAR_TEST_EVENTS = [ { - title: "1st EVENT", - description: "1st Description", + title: "1st SINGULAR EVENT", + description: "1st Singular Description", location: "1st Location", - groupId: TEST_EVENT_GROUP_ID, + groupId: null, startAt: Date.now(), - endAt: Date.now() + 3600, + endAt: Date.now() + 60 * 60, }, { - title: "2nd EVENT", - description: "2nd Description", + title: "2nd SINGULAR EVENT", + description: "2nd Singular Description", location: "2nd Location", + groupId: null, + startAt: Date.now() + 60 * 60, + endAt: Date.now() + 60 * 60 * 2, + }, +]; + +const GROUP_TEST_EVENTS = [ + { + title: "1st GROUP EVENT", + description: "1st Group Description", + location: "1st Location", groupId: TEST_EVENT_GROUP_ID, startAt: Date.now(), - endAt: Date.now() + 3600, + endAt: Date.now() + 60 * 60, + }, + { + title: "2nd GROUP EVENT", + description: "2nd Group Description", + location: "2nd Location", + groupId: TEST_EVENT_GROUP_ID, + startAt: Date.now() + 60 * 60 * 24, + endAt: Date.now() + 60 * 60 * 24 + 3600, }, ]; -const addTestEvents = (test_events = TEST_EVENTS) => { +const GROUP_EVENT_COUNTS_BY_GROUP_ID = GROUP_TEST_EVENTS.reduce( + (acc, event) => { + acc[event.groupId] = acc[event.groupId] || 0; + return { [event.groupId]: acc[event.groupId] + 1, ...acc }; + }, + {} +); + +const addTestEvents = test_events => { cy.url().then(return_url => { cy.visit("/"); // Generate test events cy.createOwnEvents(TEST_EVENT_AUTHOR, ...test_events); + // TODO add group events // Close event creation modal tgt.modal.close(); @@ -53,7 +81,7 @@ describe("Admin Dashboard", () => { cy.get("h1").contains("Admin Dashboard"); }); - describe("Add Events", () => { + describe("Add events", () => { before(() => { // Navigate to admin dashboard cy.visit("/admindashboard"); @@ -64,29 +92,29 @@ describe("Admin Dashboard", () => { }); beforeEach(() => { // Add test events - addTestEvents(TEST_EVENTS); + addTestEvents([...SINGULAR_TEST_EVENTS, ...GROUP_TEST_EVENTS]); // Navigate to admin dashboard cy.visit("/admindashboard"); }); - it("Event titles and descriptions", () => { - TEST_EVENTS.forEach((test_event, i) => { + it("Single event titles and descriptions", () => { + SINGULAR_TEST_EVENTS.forEach((test_event, i) => { // Ensure test events rendered with correct title cy.get("#events") - .find(`.event:nth-of-type(${i + 1}) li:nth-of-type(1)`) + .find(`.single-event:nth-of-type(${i + 1}) li:nth-of-type(1)`) .invoke("text") .should("contain", test_event.title); // Ensure test event rendered with correct description cy.get("#events") - .find(`.event:nth-of-type(${i + 1}) li:nth-of-type(2)`) + .find(`.single-event:nth-of-type(${i + 1}) li:nth-of-type(2)`) .invoke("text") .should("contain", test_event.description); }); }); - it("Event Counts", () => { + it("Total event counts", () => { // TODO: there should be a better way to wait for a React re-render to update the // event count, but a manual wait works currently cy.wait(100); @@ -100,7 +128,9 @@ describe("Admin Dashboard", () => { .then(function () { // Ensure updated event count is 1 greater then initial event count expect(this.newEventCount).to.eq( - this.initialEventCount + TEST_EVENTS.length + this.initialEventCount + + SINGULAR_TEST_EVENTS.length + + Object.entries(GROUP_EVENT_COUNTS_BY_GROUP_ID).length ); }); }); @@ -111,7 +141,7 @@ describe("Admin Dashboard", () => { before(() => {}); beforeEach(() => { // Add test events - addTestEvents(TEST_EVENTS); + addTestEvents([...SINGULAR_TEST_EVENTS, ...GROUP_TEST_EVENTS]); // Reload page and navigate to admin dashboard // cy.reload(); @@ -122,32 +152,75 @@ describe("Admin Dashboard", () => { aliasHeaderEventCount("initialEventCount"); }); - it("Event Counts", () => { - // TODO: there should be a better way to wait for a React re-render to update the - // event count, but a manual wait works currently + it("Delete singular events", () => { + // Delete all singular events by id + cy.getAllEvents().then(({ body }) => { + body.forEach(event => { + if (!event.groupId) { + cy.deleteOwnEvent(event._id); + } + }); + }); + + cy.visit("/admindashboard"); cy.wait(100); + // Create alias for updated event count + aliasHeaderEventCount("newEventCount"); + + cy.get("h1") + .should("be.visible") + .should("not.contain", "LOADING") + .then(function () { + // Ensure updated event count matches initial count minus deleted events + expect(this.newEventCount).to.eq( + this.initialEventCount - SINGULAR_TEST_EVENTS.length + ); + }); + }); + + it("Delete group events", () => { + // Delete all group events cy.deleteOwnGroupEvents(TEST_EVENT_AUTHOR, TEST_EVENT_GROUP_ID); + cy.visit("/admindashboard"); + cy.wait(100); + // Create alias for updated event count aliasHeaderEventCount("newEventCount"); - cy.visit("/admindashboard"); cy.get("h1") .should("be.visible") .should("not.contain", "LOADING") .then(function () { // Ensure updated event count matches initial count minus deleted events expect(this.newEventCount).to.eq( - this.initialEventCount - TEST_EVENTS.length + this.initialEventCount - + Object.entries(GROUP_EVENT_COUNTS_BY_GROUP_ID).length ); }); }); it("Event titles and descriptions no longer exist", () => { + // Delete all singular events by id + cy.getAllEvents().then(({ body }) => { + body.forEach(event => { + if (!event.groupId) { + cy.deleteOwnEvent(event._id); + } + }); + }); + + // Delete all group events cy.deleteOwnGroupEvents(TEST_EVENT_AUTHOR, TEST_EVENT_GROUP_ID); - // Ensure all events were deleted - cy.get("#events").not("be.visible"); + + cy.wait(100); + + // Ensure single events are no longer displayed + cy.get("#single-events .single-event").should("not.exist"); + + // Ensure group events are no longer displayed + cy.get("#group-events .group-event").should("not.exist"); }); }); }); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 72128af..da6f3c3 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -42,3 +42,14 @@ Cypress.Commands.add("deleteOwnGroupEvents", (userCode, groupId) => { cy.reload(); }); + +Cypress.Commands.add("deleteOwnEvent", id => { + if (id) { + cy.request("DELETE", `/events/${id}`); + } +}); + +Cypress.Commands.add("getAllEvents", userCode => { + // cy.login(userCode) + return cy.request("GET", "/events"); +}); From 6457b93cfdcb80a45794692800f33d70a648017a Mon Sep 17 00:00:00 2001 From: Cameron Blankenbuehler Date: Mon, 28 Aug 2023 15:43:05 -0400 Subject: [PATCH 5/6] Add group event title and description tests and clean up comments --- cypress/e2e/admin-dashboard.cy.js | 126 +++++++++++++++++------------- 1 file changed, 70 insertions(+), 56 deletions(-) diff --git a/cypress/e2e/admin-dashboard.cy.js b/cypress/e2e/admin-dashboard.cy.js index 6dd976f..d8b8539 100644 --- a/cypress/e2e/admin-dashboard.cy.js +++ b/cypress/e2e/admin-dashboard.cy.js @@ -2,9 +2,10 @@ import tgt from "../support/tgt"; -// Generate test event const TEST_EVENT_AUTHOR = "100_DEVER"; const TEST_EVENT_GROUP_ID = "admin_text_event_id"; + +// Generate singular test events const SINGULAR_TEST_EVENTS = [ { title: "1st SINGULAR EVENT", @@ -24,6 +25,7 @@ const SINGULAR_TEST_EVENTS = [ }, ]; +// Generate group (recurring) test events const GROUP_TEST_EVENTS = [ { title: "1st GROUP EVENT", @@ -34,15 +36,17 @@ const GROUP_TEST_EVENTS = [ endAt: Date.now() + 60 * 60, }, { - title: "2nd GROUP EVENT", - description: "2nd Group Description", - location: "2nd Location", + title: "1st GROUP EVENT", + description: "1st Group Description", + location: "1st Location", groupId: TEST_EVENT_GROUP_ID, startAt: Date.now() + 60 * 60 * 24, endAt: Date.now() + 60 * 60 * 24 + 3600, }, ]; +// Calculate group event counts (how many times a group event recurs) +// indexed by its groupId property const GROUP_EVENT_COUNTS_BY_GROUP_ID = GROUP_TEST_EVENTS.reduce( (acc, event) => { acc[event.groupId] = acc[event.groupId] || 0; @@ -51,13 +55,12 @@ const GROUP_EVENT_COUNTS_BY_GROUP_ID = GROUP_TEST_EVENTS.reduce( {} ); +// Utility function to add test events consistently before each test const addTestEvents = test_events => { cy.url().then(return_url => { - cy.visit("/"); - // Generate test events + cy.visit("/"); cy.createOwnEvents(TEST_EVENT_AUTHOR, ...test_events); - // TODO add group events // Close event creation modal tgt.modal.close(); @@ -81,62 +84,74 @@ describe("Admin Dashboard", () => { cy.get("h1").contains("Admin Dashboard"); }); - describe("Add events", () => { - before(() => { - // Navigate to admin dashboard - cy.visit("/admindashboard"); - cy.get("#events").should("exist"); + describe("Page updates when events change", () => { + describe("Add events", () => { + before(() => { + // Navigate to admin dashboard + cy.visit("/admindashboard"); + cy.get("#events").should("exist"); - // Create alias for initial event count - aliasHeaderEventCount("initialEventCount"); - }); - beforeEach(() => { - // Add test events - addTestEvents([...SINGULAR_TEST_EVENTS, ...GROUP_TEST_EVENTS]); + // Create alias for initial event count + aliasHeaderEventCount("initialEventCount"); + }); + beforeEach(() => { + // Add test events + addTestEvents([...SINGULAR_TEST_EVENTS, ...GROUP_TEST_EVENTS]); - // Navigate to admin dashboard - cy.visit("/admindashboard"); - }); + // Navigate to admin dashboard + cy.visit("/admindashboard"); + }); - it("Single event titles and descriptions", () => { - SINGULAR_TEST_EVENTS.forEach((test_event, i) => { - // Ensure test events rendered with correct title - cy.get("#events") - .find(`.single-event:nth-of-type(${i + 1}) li:nth-of-type(1)`) - .invoke("text") - .should("contain", test_event.title); - - // Ensure test event rendered with correct description - cy.get("#events") - .find(`.single-event:nth-of-type(${i + 1}) li:nth-of-type(2)`) - .invoke("text") - .should("contain", test_event.description); + it("Single event titles and descriptions", () => { + SINGULAR_TEST_EVENTS.forEach((test_event, i) => { + // Ensure test events rendered with correct title + cy.get("#events") + .find(`.single-event:nth-of-type(${i + 1}) li:nth-of-type(1)`) + .invoke("text") + .should("contain", test_event.title); + + // Ensure test event rendered with correct description + cy.get("#events") + .find(`.single-event:nth-of-type(${i + 1}) li:nth-of-type(2)`) + .invoke("text") + .should("contain", test_event.description); + }); }); - }); - it("Total event counts", () => { - // TODO: there should be a better way to wait for a React re-render to update the - // event count, but a manual wait works currently - cy.wait(100); - - // Create alias for updated event count - aliasHeaderEventCount("newEventCount"); - - cy.get("h1") - .should("be.visible") - .should("not.contain", "LOADING") - .then(function () { - // Ensure updated event count is 1 greater then initial event count - expect(this.newEventCount).to.eq( - this.initialEventCount + - SINGULAR_TEST_EVENTS.length + - Object.entries(GROUP_EVENT_COUNTS_BY_GROUP_ID).length - ); + it("Group event titles and descriptions", () => { + GROUP_TEST_EVENTS.forEach((test_event, i) => { + // Ensure test events rendered with correct title and description + cy.get("#events") + .find(`.group-event`) + .invoke("text") + .should("contain", test_event.title) + .should("contain", test_event.description); }); + }); + + it("Total event counts", () => { + // TODO: there should be a better way to wait for a React re-render to update the + // event counts, but a manual wait works currently. This applies to other similar + // manual waits in other admin dashboard tests. + cy.wait(100); + + // Create alias for updated event count + aliasHeaderEventCount("newEventCount"); + + cy.get("h1") + .should("be.visible") + .should("not.contain", "LOADING") + .then(function () { + // Ensure updated event count is 1 greater then initial event count + expect(this.newEventCount).to.eq( + this.initialEventCount + + SINGULAR_TEST_EVENTS.length + + Object.entries(GROUP_EVENT_COUNTS_BY_GROUP_ID).length + ); + }); + }); }); - }); - describe("Page updates when events change", () => { describe("Delete Events", () => { before(() => {}); beforeEach(() => { @@ -144,7 +159,6 @@ describe("Admin Dashboard", () => { addTestEvents([...SINGULAR_TEST_EVENTS, ...GROUP_TEST_EVENTS]); // Reload page and navigate to admin dashboard - // cy.reload(); cy.visit("/admindashboard"); cy.wait(100); From be9b3001a85323a93f514cc10bcbe534d34cd34f Mon Sep 17 00:00:00 2001 From: Cameron Blankenbuehler Date: Sun, 10 Sep 2023 22:41:49 -0400 Subject: [PATCH 6/6] Fix EventCard render error when user not logged in When the DataService fails to retrieve the displayName for the user, "UNKNOWN" is displayed in its place. --- client/src/features/adminDashboard/EventCard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/features/adminDashboard/EventCard.js b/client/src/features/adminDashboard/EventCard.js index 8f4842c..1a1c975 100644 --- a/client/src/features/adminDashboard/EventCard.js +++ b/client/src/features/adminDashboard/EventCard.js @@ -12,7 +12,7 @@ function EventCard({ event }) {
  • {event.description}

  • -
  • Scheduled by: {event.user.displayName}
  • +
  • Scheduled by: {event.user?.displayName || "UNKNOWN"}
  • );