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

Add Cypress (E2E testing) to the project #788

Merged
merged 1 commit into from
Nov 16, 2023
Merged
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
28 changes: 26 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- name: Setup Corepack
run: corepack enable

- name: Install dependencies
- name: Install development dependencies
env:
YARN_ENABLE_IMMUTABLE_INSTALLS: false
run: yarn install
Expand All @@ -35,12 +35,36 @@ jobs:
- name: Run unit tests
run: npm run test

- name: Development build
env:
NODE_OPTIONS: "--openssl-legacy-provider"
NODE_ENV: development
run: npm run build

- name: Run integration tests
env:
NODE_ENV: development
# needs an invalid URL to prevent CORS related delays
CORIOLIS_URL: http://invalidd.it/
run: |
touch .not-first-launch
npm run start &
sleep 5
npm run e2e

- name: Upload failure screenshots
uses: actions/upload-artifact@v3
if: failure()
with:
name: cypress-screenshots
path: cypress/screenshots

- name: Install production dependencies
run: |
rm -rf node_modules
yarn workspaces focus --all --production

- name: Build
- name: Production build
env:
NODE_OPTIONS: "--openssl-legacy-provider"
run: npm run build
11 changes: 8 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
# MacOS
.DS_Store
.happypack

# Install and build
dist
*.log
node_modules
private/cypress/config.js
.env
.not-first-launch

# yarn v3
# Cypress
cypress/screenshots
cypress/videos

# yarn
.pnp.*
.yarn/*
!.yarn/patches
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ Your server will be running at `http://localhost:3000/` (the port is configurabl
- unit tests can be run using `npm run test`
- run `npm run test-release` to check for Typescript, ESLint and prettier errors. This will also run the unit tests and will try to build and start a production version. If eeverything is OK, it will revert to the development installation.

### Integration tests

Integration tests can be executed using `npm run e2e`. All API calls will be mocked, eliminating the need for a running Coriolis instance.

To run the integration tests, you must set the environment variable `NODE_ENV='development'`, then execute `npm run build` and `npm run start`. It is also recommended to set `CORIOLIS_URL` to a non-existent URL (such as <https://invalidd.it/>) to prevent the UI from attempting to connect to a Coriolis instance for CORS checks. Although Cypress is configured to mock API calls, if a valid URL is set, the UI will still attempt to connect to it for CORS checks.

You can also run the integration tests for easier debugging by using `npm run server-dev` and `npm run client-dev`, and by updating the `baseUrl` in `cypress.config.ts` to `<http://localhost:3001>`. The variables `NODE_ENV` and `CORIOLIS_URL`, as described above, are still required. Subsequently, execute the tests using `npx cypress open`. This procedure allows you to update the source code and see the changes reflected in the UI without having to rebuild and restart the server. Additionally, the tests will automatically re-run when you save a test file.

## Development mode

- set env. variable `NODE_ENV='development'`
Expand Down
10 changes: 10 additions & 0 deletions cypress.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineConfig } from "cypress";

export default defineConfig({
e2e: {
baseUrl: "http://localhost:3000",
},
env: {
CORIOLIS_URL: "https://invalidd.it/",
},
});
15 changes: 0 additions & 15 deletions cypress.json

This file was deleted.

117 changes: 117 additions & 0 deletions cypress/e2e/dashboard/dashboard.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/// <reference types="cypress" />

import { DateTime } from "luxon";
import { routeSelectors } from "../../support/routeSelectors";

describe("Dashboard", () => {
beforeEach(() => {
cy.setProjectIdCookie();

cy.mockAuth();

cy.intercept(routeSelectors.APPLIANCES, {
fixture: "licences/appliances.json",
}).as("appliances");
cy.intercept(routeSelectors.STATUS, {
fixture: "licences/status.json",
}).as("status");
cy.intercept(routeSelectors.APPLIANCE_STATUS, {
fixture: "licences/appliance-status.json",
}).as("appliance-status");
});

const waitForAll = () => {
cy.waitMockAuth();

cy.wait(["@appliances", "@status", "@appliance-status"]);
};

it("renders empty dashboard", () => {
cy.intercept(routeSelectors.REPLICAS, {
body: { replicas: [] },
}).as("replicas");
cy.intercept(routeSelectors.MIGRATIONS, {
body: { migrations: [] },
}).as("migrations");
cy.intercept(routeSelectors.ENDPOINTS, {
body: { endpoints: [] },
}).as("endpoints");

cy.visit("/");
waitForAll();
cy.wait(["@replicas", "@migrations", "@endpoints"]);

cy.get("*[class^='DashboardActivity__Message']").should(
"contain.text",
"There is no recent activity"
);

cy.fixture("licences/appliance-status.json").then(applianceStatus => {
cy.get("*[class^='DashboardLicence__TopInfoDateTop']").should(
"contain.text",
`${DateTime.fromISO(
applianceStatus.appliance_licence_status.earliest_licence_expiry_time
)
.toFormat("LLL |yy")
.replace("|", "'")}`
);

cy.get("*[class^='DashboardLicence__ChartHeaderCurrent']").should(
"contain.text",
`${applianceStatus.appliance_licence_status.current_performed_replicas} Used Replica ${applianceStatus.appliance_licence_status.current_performed_migrations} Used Migrations`
);
});

cy.get("button").should("contain.text", "New Replica / Migration");
cy.get("button").should("contain.text", "New Endpoint");
});

it("renders dashboard with data", () => {
cy.intercept(routeSelectors.REPLICAS, {
fixture: "transfers/replicas.json",
}).as("replicas");
cy.intercept(routeSelectors.MIGRATIONS, {
fixture: "transfers/migrations.json",
}).as("migrations");
cy.intercept(routeSelectors.ENDPOINTS, {
fixture: "endpoints/endpoints.json",
}).as("endpoints");

cy.visit("/");
waitForAll();
cy.wait(["@replicas", "@migrations", "@endpoints"]);

cy.loadFixtures(
[
"transfers/replicas.json",
"transfers/migrations.json",
"endpoints/endpoints.json",
],
results => {
const [replicasFixture, migrationsFixture, endpointsFixture] = results;
cy.get("div[class^='DashboardInfoCount__CountBlock']").should(
"contain.text",
`${replicasFixture.replicas.length}Replicas${migrationsFixture.migrations.length}Migrations${endpointsFixture.endpoints.length}Endpoints`
);

const checkItem = (type: "migration" | "replica", item: any) => {
cy.get("div[class^='NotificationDropdown__ItemDescription']").should(
"contain.text",
`New ${type} ${item.id.substr(
0,
7
)}... status: ${item.last_execution_status.toLowerCase()}`
);
};

migrationsFixture.migrations.forEach((migration: any) => {
checkItem("migration", migration);
});

replicasFixture.replicas.forEach((replica: any) => {
checkItem("replica", replica);
});
}
);
});
});
115 changes: 115 additions & 0 deletions cypress/e2e/endpoints/endpoints-list.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/// <reference types="cypress" />

import { routeSelectors } from "../../support/routeSelectors";

describe("Endpoints list", () => {
beforeEach(() => {
cy.setProjectIdCookie();

cy.mockAuth({ filterResources: ["users"] });
cy.intercept(routeSelectors.ENDPOINTS, {
fixture: "endpoints/endpoints",
}).as("endpoints");
});

const waitForAll = () => {
cy.waitMockAuth({ filterResources: ["users"] });
cy.wait(["@endpoints"]);
};

it("renders empty list", () => {
cy.intercept(routeSelectors.ENDPOINTS, {
body: { endpoints: [] },
}).as("endpoints-empty");

cy.visit("/endpoints");
cy.wait(["@endpoints-empty"]);
cy.waitMockAuth({ filterResources: ["users"] });

cy.get("div[class^='MainList__EmptyListMessage']").should(
"contain.text",
"don't have any Cloud Endpoints in this project"
);
cy.get("button").should("contain.text", "Add Endpoint");
});

it("filters list", () => {
cy.visit("/endpoints");
waitForAll();

cy.fixture("endpoints/endpoints").then((endpointsFixture: any) => {
const endpoints = endpointsFixture.endpoints;

cy.get("div[class^='MainListFilter__FilterItem']")
.contains("Azure")
.click();
cy.get("div[class^='EndpointListItem__Wrapper']").should(
"have.length",
endpoints.filter(r => r.type === "azure").length
);

cy.get("div[class^='MainListFilter__FilterItem']")
.contains("VMware")
.click();
cy.get("div[class^='EndpointListItem__Wrapper']").should(
"have.length",
endpoints.filter(r => r.type === "vmware_vsphere").length
);

cy.get("div[class^='SearchButton__Wrapper']").click();
cy.get("input[class*='SearchInput']").type("cor");
cy.get("div[class^='EndpointListItem__Wrapper']").should(
"have.length",
endpoints.filter(
e => e.type === "vmware_vsphere" && e.name.includes("cor")
).length
);
cy.get("div[class^='TextInput__Close']").click();

cy.get("div[class^='MainListFilter__FilterItem']")
.contains("All")
.click();
cy.get("div[class^='EndpointListItem__Wrapper']").should(
"have.length",
endpoints.length
);

cy.get("div[class^='SearchButton__Wrapper']").click();
cy.get("input[class*='SearchInput']").type("cor");
cy.get("div[class^='EndpointListItem__Wrapper']").should(
"have.length",
endpoints.filter(e => e.name.includes("cor")).length
);
});
});

it("does bulk actions", () => {
cy.visit("/endpoints");
waitForAll();

cy.get("div[class^='SearchButton__Wrapper']").click();
cy.get("input[class*='SearchInput']").type("cor");
cy.get(
"div[class^='MainListFilter__Wrapper'] div[class^='Checkbox__Wrapper']"
).click();

cy.fixture("endpoints/endpoints").then((endpointsFixture: any) => {
const endpoints = endpointsFixture.endpoints;
const corEndpoints = endpoints.filter(e => e.name.includes("cor"));
cy.get("div[class^='MainListFilter__SelectionText']").should(
"contain.text",
`${corEndpoints.length} of ${corEndpoints.length}`
);

cy.get("div[class^='ActionDropdown__Wrapper']").click();
cy.get("div[class^='ActionDropdown__ListItem']")
.contains("Delete")
.click();

cy.get("div[class^='AlertModal__Message']").should(
"contain.text",
"they are in use by replicas or migrations"
);
});
});
});
Loading
Loading