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

test(ui-cypress): add Cypress setup with TypeScript support for E2E and … #2315

Open
wants to merge 3 commits into
base: dev
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
5 changes: 5 additions & 0 deletions webapp/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,8 @@ dist-ssr
*.njsproj
*.sln
*.sw?

# Cypress
cypress/screenshots
cypress/videos
cypress/downloads
37 changes: 37 additions & 0 deletions webapp/cypress.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Copyright (c) 2025, RTE (https://www.rte-france.com)
*
* See AUTHORS.txt
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* SPDX-License-Identifier: MPL-2.0
*
* This file is part of the Antares project.
*/

import { defineConfig } from "cypress";
import { resolve } from "path";

export default defineConfig({
e2e: {
supportFile: "cypress/support/e2e.ts",
specPattern: "cypress/e2e/**/*.cy.{ts,tsx}",
baseUrl: "http://localhost:3000",
viewportWidth: 1280,
viewportHeight: 720,
},
component: {
supportFile: "cypress/support/component.ts",
specPattern: "cypress/component/**/*.cy.{ts,tsx}",
devServer: {
framework: "react",
bundler: "vite",
viteConfig: {
configFile: resolve(import.meta.dirname, "vite.config.ts"),
},
},
},
});
18 changes: 18 additions & 0 deletions webapp/cypress/constants/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Copyright (c) 2025, RTE (https://www.rte-france.com)
*
* See AUTHORS.txt
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* SPDX-License-Identifier: MPL-2.0
*
* This file is part of the Antares project.
*/

export const AppPages = {
studies: "/studies",
settings: "/settings",
} as const;
6 changes: 6 additions & 0 deletions webapp/cypress/cypress.env.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"USERNAME": "testuser",
"PASSWORD": "testpass",
"AUTH_TOKEN": null,
"REFRESH_TOKEN": null
}
66 changes: 66 additions & 0 deletions webapp/cypress/e2e/auth.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* Copyright (c) 2025, RTE (https://www.rte-france.com)
*
* See AUTHORS.txt
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* SPDX-License-Identifier: MPL-2.0
*
* This file is part of the Antares project.
*/

describe("Authentication Flow", () => {
describe("Login Page", () => {
beforeEach(() => {
cy.visit("/");
});

it("should not allow submit when form is empty", () => {
cy.findByRole("button", { name: /connexion/i }).should("be.disabled");
});

it("should show error when using invalid credentials", () => {
// Fill form with invalid credentials
cy.findByLabelText(/nni/i).type("invalid_user");
// TODO: fix the accessibility issue, and replace the css selector by "cy-data" or use testing lib API.
cy.get('input[name="password"]').type("invalid_password");

// Submit wrong user
cy.findByRole("button", { name: /connexion/i })
.should("be.enabled")
.click();

// Verify the login attempt fails
// TODO: move css selector to "cy-data" or testing lib API.
cy.get(".notistack-SnackbarContainer").within(() => {
cy.findByText("Error while submitting").should("exist");
// TODO: assertions below fail because of accessibility lack
//cy.findByText("Status: 401").should("exist");
//cy.findByText("Exception: HTTPException").should("exist");
//cy.findByText("Description: Bad username or password").should("exist");
});
});
});

describe("Successful Authentication", () => {
it("should store tokens for subsequent tests", () => {
// Call login command with authenticated user
cy.visit("/");
cy.findByLabelText(/nni/i).type("admin");
// TODO: move css selector to "cy-data" or testing lib API.
cy.get('input[name="password"]').type("admin");
cy.findByRole("button", { name: /connexion/i })
.should("be.enabled")
.click();

cy.login();

// Verify tokens are stored
cy.wrap(Cypress.env("ACCESS_TOKEN")).should("be.a", "string");
cy.wrap(Cypress.env("REFRESH_TOKEN")).should("be.a", "string");
});
});
});
86 changes: 86 additions & 0 deletions webapp/cypress/e2e/studies.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/**
* Copyright (c) 2025, RTE (https://www.rte-france.com)
*
* See AUTHORS.txt
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* SPDX-License-Identifier: MPL-2.0
*
* This file is part of the Antares project.
*/

describe("Studies Page", () => {
beforeEach(() => {
cy.login().then(() => {
cy.intercept(
{
method: "GET",
url: "/v1/studies",
headers: {
Authorization: `Bearer ${Cypress.env("AUTH_TOKEN")}`,
},
},
{
statusCode: 200,
body: [
{ id: "1", name: "Study 1", description: "First study" },
{ id: "2", name: "Study 2", description: "Second study" },
],
},
).as("getStudies");

cy.visit("/studies");
cy.wait("@getStudies");
});
});

it("should display the studies page correctly", () => {
cy.findByRole("heading", { name: /studies/i }).should("exist");

cy.findByRole("list", { name: /studies list/i }).within(() => {
cy.findByText("Study 1").should("exist");
cy.findByText("Study 2").should("exist");
});
});

it("should handle loading state", () => {
cy.intercept(
{
method: "GET",
url: "/v1/studies",
headers: {
Authorization: `Bearer ${Cypress.env("AUTH_TOKEN")}`,
},
},
(req) => {
req.reply({
delay: 1000,
body: [],
});
},
).as("delayedStudies");
cy.visit("/studies");
cy.findByRole("status", { name: /loading/i }).should("exist");
});

it("should handle error state", () => {
cy.intercept(
{
method: "GET",
url: "/v1/studies",
headers: {
Authorization: `Bearer ${Cypress.env("AUTH_TOKEN")}`,
},
},
{
forceNetworkError: true,
},
).as("errorStudies");

cy.visit("/studies");
cy.findByRole("alert", { name: /error/i }).should("contain", "Failed to load studies");
});
});
Empty file.
18 changes: 18 additions & 0 deletions webapp/cypress/support/component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Copyright (c) 2025, RTE (https://www.rte-france.com)
*
* See AUTHORS.txt
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* SPDX-License-Identifier: MPL-2.0
*
* This file is part of the Antares project.
*/

import { mount } from "cypress/react";
import "@testing-library/cypress/add-commands";

Cypress.Commands.add("mount", mount);
56 changes: 56 additions & 0 deletions webapp/cypress/support/e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* Copyright (c) 2025, RTE (https://www.rte-france.com)
*
* See AUTHORS.txt
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* SPDX-License-Identifier: MPL-2.0
*
* This file is part of the Antares project.
*/

import { AppPages } from "@cypress/constants";
import "@testing-library/cypress/add-commands";

Cypress.Commands.add(
"login",
(username = Cypress.env("USERNAME"), password = Cypress.env("PASSWORD")) => {
cy.session([username, password], () => {
cy.request({
method: "POST",
url: "/v1/login",
body: { username, password },
headers: {
"Content-Type": "application/json",
},
}).then(({ body }) => {
// Store tokens in env vars
Cypress.env("ACCESS_TOKEN", body.access_token);
Cypress.env("REFRESH_TOKEN", body.refresh_token);
});
});

// Verify successful login
cy.visit("/");
cy.findByRole("heading", { name: /antares web/i }).should("exist");
cy.url().should("include", "/studies");
},
);

Cypress.Commands.add("navigateTo", (page) => {
const path = AppPages[page];

cy.location("pathname").then((currentPath) => {
if (currentPath !== path) {
cy.visit(path);
}
});
});

// Global intercepts for common API calls
beforeEach(() => {
cy.intercept("GET", "/v1/health", { statusCode: 200 }).as("healthCheck");
});
17 changes: 17 additions & 0 deletions webapp/cypress/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"types": ["cypress", "@testing-library/cypress"],
"baseUrl": ".",
Copy link
Member

Choose a reason for hiding this comment

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

'baseUrl' and 'paths' are already in the extended config

Copy link
Member Author

Choose a reason for hiding this comment

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

this one overwrite the base config, it's intended

"paths": {
"@app/*": ["../src/*"], // React app imports
"@cypress/*": ["./*"] // Cypress-specific imports
},
"skipLibCheck": false, // check d.ts files for Cypress
},
"include": [
"**/*.ts",
"../src/**/*.ts",
"../src/**/*.tsx"
]
}
44 changes: 44 additions & 0 deletions webapp/cypress/types/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* Copyright (c) 2025, RTE (https://www.rte-france.com)
*
* See AUTHORS.txt
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* SPDX-License-Identifier: MPL-2.0
*
* This file is part of the Antares project.
*/

import { mount } from "cypress/react";
import type { TAppPages } from ".";

declare global {
namespace Cypress {
interface Chainable {
/**
* Full authentication flow using API calls and Testing Library
*
* @example cy.login() // Uses env vars
* @example cy.login('customUser', 'securePass')
*/
login(username?: string, password?: string): Chainable<void>;

/**
* Navigate to a specific page in the application
*
* @example cy.navigateTo('studies')
*/
navigateTo(page: TAppPages): Chainable<void>;

/**
* Mount component for testing
*
* @example cy.mount(<MyComponent />)
*/
mount: typeof mount;
}
}
}
17 changes: 17 additions & 0 deletions webapp/cypress/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Copyright (c) 2025, RTE (https://www.rte-france.com)
*
* See AUTHORS.txt
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* SPDX-License-Identifier: MPL-2.0
*
* This file is part of the Antares project.
*/

import type { AppPages } from "@cypress/constants";

export type TAppPages = keyof typeof AppPages;
Loading
Loading