diff --git a/client/src/features/adminDashboard/EventCard.js b/client/src/features/adminDashboard/EventCard.js new file mode 100644 index 0000000..1a1c975 --- /dev/null +++ b/client/src/features/adminDashboard/EventCard.js @@ -0,0 +1,21 @@ +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..8bf4282 --- /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..60760de 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,15 @@ export const AdminDashboard = () => { fetch(); }, []); - let eventStatus = loading ? "LOADING..." : Object.keys(allEvents).length; + const singleEventCount = loading + ? "LOADING..." + : Object.keys(singleEvents).length; + const groupEventCount = loading + ? "LOADING..." + : Object.keys(groupEvents).length; + const eventStatus = loading + ? "LOADING..." + : singleEventCount + groupEventCount; return (
@@ -35,29 +67,41 @@ 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}
  • -
-
- ); - })} -
+
+

+ Singular Events ( + {singleEventCount}) +

+
+
+ {/* Display all events in an unordered list when the data is finished loading */} + {loading || + Object.keys(singleEvents).map(key => { + return ( + + ); + })} +
+
+

+ Recurring Events ( + {groupEventCount}) +

+
+ {loading || + Object.keys(groupEvents).map(key => { + return ( + + ); + })} +
+
); }; diff --git a/cypress/e2e/admin-dashboard.cy.js b/cypress/e2e/admin-dashboard.cy.js index cb7262a..d8b8539 100644 --- a/cypress/e2e/admin-dashboard.cy.js +++ b/cypress/e2e/admin-dashboard.cy.js @@ -2,33 +2,64 @@ 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 = [ + +// Generate singular 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, + }, +]; + +// Generate group (recurring) test events +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: "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, }, ]; -const addTestEvents = (test_events = TEST_EVENTS) => { - cy.url().then(return_url => { - cy.visit("/"); +// 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; + return { [event.groupId]: acc[event.groupId] + 1, ...acc }; + }, + {} +); +// Utility function to add test events consistently before each test +const addTestEvents = test_events => { + cy.url().then(return_url => { // Generate test events + cy.visit("/"); cy.createOwnEvents(TEST_EVENT_AUTHOR, ...test_events); // Close event creation modal @@ -53,68 +84,81 @@ 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(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("Event titles and descriptions", () => { - 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)`) - .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)`) - .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("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 + TEST_EVENTS.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(() => { // Add test events - addTestEvents(TEST_EVENTS); + addTestEvents([...SINGULAR_TEST_EVENTS, ...GROUP_TEST_EVENTS]); // Reload page and navigate to admin dashboard - // cy.reload(); cy.visit("/admindashboard"); cy.wait(100); @@ -122,32 +166,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"); +});