Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update and expand events Cypress tests. DDFHER-30 #1649

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ tasks:
- task dev:cli -- drush user:create local_administrator --password="test"
- task dev:cli -- drush user:role:add 'local_administrator' local_administrator

- task dev:cli -- drush user:create external_system --password="external_system"
- task dev:cli -- drush user:create external_system --password="test"
- task dev:cli -- drush user:role:add 'external_system' external_system

- task dev:cli -- drush user:create patron --password="test"
Expand Down
371 changes: 290 additions & 81 deletions cypress/e2e/events.cy.ts
Original file line number Diff line number Diff line change
@@ -1,97 +1,306 @@
import * as dayjs from "dayjs";
import 'dayjs/locale/da'

Check failure on line 2 in cypress/e2e/events.cy.ts

View workflow job for this annotation

GitHub Actions / Lint Cypress tests

Replace `'dayjs/locale/da'` with `"dayjs/locale/da";`
import "cypress-if";

const events = {
singleEvent: {
title: "Single event",
subtitle: "A subtitle",
recurType: "Custom/Single Event",
start: dayjs("2030-01-01T10:00:00"),
end: dayjs("2030-01-01T16:00:00"),
dayjs.locale('da')

Check failure on line 5 in cypress/e2e/events.cy.ts

View workflow job for this annotation

GitHub Actions / Lint Cypress tests

Replace `'da')` with `"da");`

// The fields of the eventseries entity, that we use when creating and comparing.
export interface EventType {
rasben marked this conversation as resolved.
Show resolved Hide resolved
uuid?: string,

Check failure on line 9 in cypress/e2e/events.cy.ts

View workflow job for this annotation

GitHub Actions / Lint Cypress tests

Replace `,` with `;`
rasben marked this conversation as resolved.
Show resolved Hide resolved
title?: string,

Check failure on line 10 in cypress/e2e/events.cy.ts

View workflow job for this annotation

GitHub Actions / Lint Cypress tests

Replace `,` with `;`
subtitle: string,

Check failure on line 11 in cypress/e2e/events.cy.ts

View workflow job for this annotation

GitHub Actions / Lint Cypress tests

Replace `,` with `;`
recurType: 'custom' | 'weekly_recurring_date',

Check failure on line 12 in cypress/e2e/events.cy.ts

View workflow job for this annotation

GitHub Actions / Lint Cypress tests

Replace `'custom'·|·'weekly_recurring_date',` with `"custom"·|·"weekly_recurring_date";`
ticketManagerRelevance: boolean,

Check failure on line 13 in cypress/e2e/events.cy.ts

View workflow job for this annotation

GitHub Actions / Lint Cypress tests

Replace `,` with `;`
state: string,

Check failure on line 14 in cypress/e2e/events.cy.ts

View workflow job for this annotation

GitHub Actions / Lint Cypress tests

Replace `,` with `;`
rasben marked this conversation as resolved.
Show resolved Hide resolved
start: dayjs,

Check failure on line 15 in cypress/e2e/events.cy.ts

View workflow job for this annotation

GitHub Actions / Lint Cypress tests

Replace `,` with `;`
end: dayjs,

Check failure on line 16 in cypress/e2e/events.cy.ts

View workflow job for this annotation

GitHub Actions / Lint Cypress tests

Replace `,` with `;`
status: boolean,
}

// This interface only has what we currently use - ideally, it could be
// expanded to follow the Swagger spec.
interface EventApiType {
rasben marked this conversation as resolved.
Show resolved Hide resolved
url: string,
uuid: string,
title: string,
description: string,
ticket_manager_relevance: boolean,
state: string,
date_time: {
start: string,
end: string,
},
};
external_data: {
url: string,
admin_url: string,
}
}

const setDate = (field: "Start date" | "End date", date: dayjs.Dayjs) => {
cy.findByText(field)
.siblings()
.findByLabelText("Date")
.type(date.format("YYYY-MM-DD"));
cy.findByText(field)
.siblings()
.findByLabelText("Time")
.type(date.format("HH:mm"));
export const eventBase: EventType = {
rasben marked this conversation as resolved.
Show resolved Hide resolved
// Creating a random title, that we can use as a makeshift ID.
subtitle: Math.random().toString(36).slice(2, 7),
rasben marked this conversation as resolved.
Show resolved Hide resolved
// Generating a random boolean.
ticketManagerRelevance: true,
rasben marked this conversation as resolved.
Show resolved Hide resolved
recurType: "custom",
start: dayjs("2030-12-15T10:00:00"),
end: dayjs("2031-02-15T16:00:00"),
state: "Active",
status: true,
};

describe("Events", () => {
it("can be created with a single occurrence", () => {
// Login as admin.
cy.drupalLogin("/events/add/default");
cy.findByLabelText("Title").type(events.singleEvent.title);
cy.findByLabelText("Subtitle").type(events.singleEvent.subtitle);
cy.findByLabelText("Recur Type").select(events.singleEvent.recurType, {
// We have to use force when using Select2.
force: true,
// This function assumes we're already on the create-event-series page.
// This function is only pulled out, as the date part of the creation is fairly
// complex, due to Recurring_Events.
function setEventSeriesDate(event: EventType) {
// The Drupal module is inconsistent with naming.
const fieldKey = (event.recurType == 'custom') ? `${event.recurType}_date` : event.recurType;

cy.get(`[name="${fieldKey}[0][value][date]"]`)
.type(event.start.format("YYYY-MM-DD")).focus();

cy.wait(1000);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please reconsider whether it is necessary to use cy.wait().

It is considered an antipattern. Even if it takes a bit before the application code sets the end date Cypress should wait for that to occur by default.


cy.get(`[name="${fieldKey}[0][end_value][date]"]`)
// Checking that the end date is pre-filled, based on start date.
.should("have.value", event.start.format("YYYY-MM-DD"))
.type(event.end.format("YYYY-MM-DD"));
Comment on lines +63 to +66
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please reconsider the structure here.

You have a function setEventSeriesDate() where the name signals setting a value. With this addition it also does something else.

I suggest splitting this into smaller functions. I also suggest putting the check for whether the event end date and time are prefilled into a separate test as it was the case before.


if (event.recurType == 'custom') {
cy.get(`[name="${fieldKey}[0][value][time]"]`)
.type(event.start.format("HH:mm")).focus();

cy.wait(500);

cy.get(`[name="${fieldKey}[0][end_value][time]"]`)
// Checking that the end time is pre-filled, based on start time.
.should("have.value", event.start.add(1, "hour").format("HH:mm"))
.type(event.end.format("HH:mm"));
}
else if (event.recurType == 'weekly_recurring_date') {
// The non-custom display, has a different way of choosing times.
const startTimeLabel = event.start.format("HH:mm");
const endTimeLabel = event.end.format("HH:mm");

cy.get('[name="weekly_recurring_date[0][time]"]').select(startTimeLabel)
cy.contains('Set End Time').click();
cy.get('[name="weekly_recurring_date[0][end_time][time]"]').select(endTimeLabel)

// Clicking all the days off, so we can easier match the dates later.
cy.get('[name^="weekly_recurring_date[0][days]["]').click({multiple: true});
rasben marked this conversation as resolved.
Show resolved Hide resolved
}
}

function createEventSeries(event: EventType) {
// Recurring_event throws weird exceptions here, that we want to avoid failing
// the whole cypress test.
Cypress.on('uncaught:exception', () => {
// returning false here prevents Cypress from
// failing the test
return false
})
Comment on lines +94 to +100
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate the comment but this does not seem like a good thing.

I am going to check this in action before I decide on whether it is necessary.


cy.drupalLogin("/events/add/default");

cy.findByLabelText("Title").type(event.title);
cy.findByLabelText("Subtitle").type(event.subtitle);

cy.findByLabelText("State").select(event.state, {
// We have to use force when using Select2.
force: true,
});

if (!event.ticketManagerRelevance) {
cy.contains('Show sidebar panel').click();
cy.findByLabelText("Relevant for ticket manager").click();
cy.contains('Close sidebar panel').click();
}

if (!event.status) {
cy.get('[data-drupal-selector="edit-status-value"]').click();
}

cy.findByLabelText("Recur Type").select(event.recurType, {
// We have to use force when using Select2.
force: true,
});
Comment on lines +112 to +125
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason why you went for data attributes instead of labels here?

To me the relevance for ticket manager and recur type setting are easier to read as they rely on UI texts using findByLabelText() and the like.


setEventSeriesDate(event);
Comment on lines +122 to +127
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider if setting the recur type should be a part of the date setting.

As far as I can tell setEventSeriesDate() depends on the recur type being set.


cy.findByRole("button", { name: "Save" }).click();
}

function deleteEventSeries(event: EventType) {
cy.drupalLogin("/admin/content/eventseries");

cy.contains(event.title)
.parents("tr")
.find("td li.dropbutton-toggle button")
.click()
.then(($button) => {
cy.wrap($button)
.parent(".dropbutton-toggle")
.parent("ul.dropbutton")
.find("li.delete a")
.click();
cy.get(".ui-dialog .form-submit")
.filter(":visible")
.should("exist")
.click();
});
setDate("Start date", events.singleEvent.start);
setDate("End date", events.singleEvent.end);
cy.findByRole("button", { name: "Save" }).click();
}

function findEventsInAPI(event: EventType) {
// Make an API call to get the list of events
return cy.request("/api/v1/events").then((response) => {
expect(response.status).to.eq(200);
Comment on lines +153 to +155
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use cy.api() for such requests.

It a plugin that we use elsewhere for these things. It makes managing API requests in test cases easier.


const events = response.body as EventApiType[];
const matchingEvents = events.filter((apiEvent: any) => apiEvent.title === event.title);

if (matchingEvents.length) {
// Let's check that the interface values has been set.
const apiEvent = matchingEvents[0];

expect(event.title).to.eq(apiEvent.title);
expect(event.ticketManagerRelevance).to.eq(apiEvent.ticket_manager_relevance);
expect(event.start.format("YYYY-MM-DDTHH:mm:ssZ")).to.eq(apiEvent.date_time.start);
Comment on lines +161 to +166
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please reconsider your code structure.

This is a find method. I suggest putting your assertions in a separate function.


if (event.recurType == 'custom') {
expect(event.end.format("YYYY-MM-DDTHH:mm:ssZ")).to.eq(apiEvent.date_time.end);
} else {
// If it is a reccurring, non-custom event, the end date will be the
// same as the start, but the time will be that of the end date.
const date = event.start.format("YYYY-MM-DD") + event.end.format("THH:mm:ssZ");
expect(date).to.eq(apiEvent.date_time.end);
}
}

return cy.wrap(matchingEvents);
rasben marked this conversation as resolved.
Show resolved Hide resolved
});
}

function visitEventEditLink() {
cy.get('.event-list-stacked a').first().click().then(() => {
cy.document().then((doc) => {
const eventUrl = doc.querySelector('link[rel="shortlink"]').getAttribute('href');
const editLink = `${eventUrl}/edit`;

// Now use Cypress to visit the edit link
cy.visit(editLink);
});
});
}
Comment on lines +182 to +192
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please reconsider this approach.

  1. It assumes that you are located on a specific page
  2. It has expectations for the CSS structure. This is usually something that we want to avoid.
  3. It is based on a combination of head attributes and string manipulation

I would suggest a function which, based on an EventType found the event in the event list based on the title and went to the edit page from there.


describe("Events API", () => {
it("Series with single instance", () => {
rasben marked this conversation as resolved.
Show resolved Hide resolved
const event = {...eventBase};
event.title = Math.random().toString(36).slice(2, 7);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a reocurring approach to generating a random string.

Please put it in a separate function or create a factory method which provides an event ready for testing.

In general I wonder why you went with this approach when other tests use explicit values for individual cases.


// Ensure that the core data from the event is displayed on the resulting page.
// @todo This should probably be replaced by a visual regression test.
cy.contains(events.singleEvent.title);
cy.contains(events.singleEvent.start.format("DD MMMM YYYY"));
cy.contains(
`${events.singleEvent.start.format(
"HH:mm"
)} - ${events.singleEvent.end.format("HH:mm")}`
);
createEventSeries(event)

findEventsInAPI(event).then((events) => {
expect(events.length).to.eq(1, `Expected exactly one eventinstance, but found ${events.length}`);
});

deleteEventSeries(event);
});

it("prefills end date/time based on start date/time", () => {
// Login as admin.
cy.drupalLogin("/events/add/default");
setDate("Start date", events.singleEvent.start);
cy.findByText("End date")
.siblings()
.findByLabelText("Date")
.focus()
.should("have.value", events.singleEvent.start.format("YYYY-MM-DD"));
cy.findByText("End date")
.siblings()
.findByLabelText("Time")
.focus()
.should(
"have.value",
events.singleEvent.start.add(1, "hour").format("HH:mm")
);
it("Unpublished events should not show up in API", () => {
rasben marked this conversation as resolved.
Show resolved Hide resolved
const event = {...eventBase};
event.status = false;
event.title = Math.random().toString(36).slice(2, 7);
createEventSeries(event)

findEventsInAPI(event).then((events) => {
expect(events.length).to.eq(0, `Expected no events, due to unpublished, but found ${events.length}`);
});

deleteEventSeries(event);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This cleans up after each test. Cypress recommends cleaning up before.

});

it("Series with many instances", () => {
const event = {...eventBase};
event.title = Math.random().toString(36).slice(2, 7);
event.recurType = "weekly_recurring_date";

createEventSeries(event);

findEventsInAPI(event).then((events) => {
expect(events.length).to.greaterThan(1, `Expected to find multiple events in series, but found ${events.length}`);
return events[0];
});

// Let's edit some details, that we can later look up in the API.
visitEventEditLink();

const newTitle = Math.random().toString(36).slice(2, 7);
cy.findByLabelText("Title").type(newTitle);
cy.findByRole("button", { name: "Save" }).click();

const newEvent = {...event};
newEvent.title = newTitle;

findEventsInAPI(newEvent).then((events) => {
expect(events.length).to.eq(1, `Expected exactly one eventinstance after editing, but found ${events.length}`);
});

// Unpublishing the same instance.
cy.get('.breadcrumb').contains(event.title).click();
visitEventEditLink();

cy.get('[id="edit-status-value"]').click();
cy.findByRole("button", { name: "Save" }).click();

findEventsInAPI(newEvent).then((events) => {
expect(events.length).to.eq(0, `Expected unpublished event to not show up in API`);
});
Comment on lines +247 to +256
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest putting this in a separate test.

It seems to deal with a separate issue.


deleteEventSeries(event);
});

before(() => {
cy.drupalLogin("/admin/content/eventseries");
// Delete all preexisting instances of each event.
cy.get("a")
.contains(events.singleEvent.title)
.if()
.each(() => {
// We have to repeat the selector as Cypress will otherwise complain about
// missing references to elements when clicking the page.
cy.findAllByRole("link", { name: events.singleEvent.title })
.first()
.click();
cy.findByRole("link", {
name: `Edit ${events.singleEvent.title}`,
}).click();
cy.findByRole("button", { name: "More actions" })
.click()
.parent()
.findByRole("link", { name: "Delete" })
.click();
cy.findByRole("dialog")
.findByRole("button", { name: "Delete" })
.click();

// Return to the event list to prepare for the next iteration.
cy.visit("/admin/content/eventseries");
it("Updating an event using API", () => {
const event = {...eventBase};
event.title = Math.random().toString(36).slice(2, 7);
createEventSeries(event);

const patchBody = {
state: 'SoldOut',
external_data: {
url: "https://event.local",
admin_url: "https://admin.local",
}
}

findEventsInAPI(event).then((events) => {
const apiEvent = events[0];
// Resetting the logged-in session, for calling API.
cy.clearCookies();
cy.clearAllSessionStorage();
rasben marked this conversation as resolved.
Show resolved Hide resolved

const extUsername = 'external_system';
const extPassword = Cypress.env("CYPRESS_DRUPAL_PASSWORD");

cy.request({
method: 'PATCH',
url: `/api/v1/events/${apiEvent.uuid}`,
auth: {
user: extUsername,
pass: extPassword,
},
body: patchBody
}).then((response) => {
expect(response.status).to.eq(200);
});
});

findEventsInAPI(event).then((events) => {
expect(events.length).to.eq(1, `Expected to find the PATCH'ed event using title ${event.title}`);
const apiEvent = events[0];
expect(apiEvent.state).to.eq(patchBody.state, `Expected updated event state to be ${patchBody.state}, but found ${apiEvent.state}`);
expect(apiEvent.external_data.url).to.eq(patchBody.external_data.url, `Expected updated event external url to be ${patchBody.external_data.url}, but found ${apiEvent.external_data.url}`);
expect(apiEvent.external_data.admin_url).to.eq(patchBody.external_data.admin_url, `Expected updated event external admin url to be ${patchBody.external_data.admin_url}, but found ${apiEvent.external_data.admin_url}`);
});

deleteEventSeries(event);
});
});
Loading
Loading