From 7b6743eb21bb7afb24af4624a6c63fe1e80355ed Mon Sep 17 00:00:00 2001 From: RenkuBot <53332360+RenkuBot@users.noreply.github.com> Date: Mon, 18 Sep 2023 16:01:22 +0200 Subject: [PATCH 1/2] chore: Update sbt from 1.9.5 to 1.9.6 (#3256) --- acceptance-tests/project/build.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acceptance-tests/project/build.properties b/acceptance-tests/project/build.properties index 51b51fce68..27430827bc 100644 --- a/acceptance-tests/project/build.properties +++ b/acceptance-tests/project/build.properties @@ -1 +1 @@ -sbt.version=1.9.5 +sbt.version=1.9.6 From 43a5f4b56fc84432a627e1b8e7fd7bfa0b8fcf44 Mon Sep 17 00:00:00 2001 From: Lorenzo Cavazzi <43481553+lorenzo-cavazzi@users.noreply.github.com> Date: Tue, 19 Sep 2023 10:19:06 +0200 Subject: [PATCH 2/2] test(cypress): optimize acceptance tests and cover missing UI features (#3232) * Separate and expand dataset tests. * Test sessions on projects without privileges, as anonymous users. * Verify that running commands in sessions impacts the other UI pages. * Use shared Cypress functions for common tasks. * Update and simplify the readme file. fix #3184 --- .github/workflows/pull-request-test.yml | 1 + cypress-tests/README.md | 138 ++++------- .../cypress/e2e/checkWorkflows.cy.ts | 34 ++- .../cypress/e2e/privateProject.cy.ts | 78 ++----- cypress-tests/cypress/e2e/publicProject.cy.ts | 189 +++------------ .../cypress/e2e/rstudioSession.cy.ts | 4 +- cypress-tests/cypress/e2e/testDatasets.cy.ts | 176 ++++++++++++++ .../cypress/e2e/updateProjects.cy.ts | 90 ++++--- cypress-tests/cypress/e2e/useSession.cy.ts | 152 ++++++++---- .../cypress/e2e/verifyInfrastructure.cy.ts | 6 +- .../cypress/support/commands/datasets.ts | 48 ++++ .../cypress/support/commands/general.ts | 32 +-- .../cypress/support/commands/login.ts | 115 +++++---- .../cypress/support/commands/projects.ts | 221 ++++++++++++++---- .../cypress/support/commands/sessions.ts | 67 ++++-- cypress-tests/cypress/support/e2e.ts | 6 +- cypress-tests/cypress/support/index.ts | 5 +- 17 files changed, 801 insertions(+), 561 deletions(-) create mode 100644 cypress-tests/cypress/e2e/testDatasets.cy.ts create mode 100644 cypress-tests/cypress/support/commands/datasets.ts diff --git a/.github/workflows/pull-request-test.yml b/.github/workflows/pull-request-test.yml index 94fd4a23ec..845c3b1985 100644 --- a/.github/workflows/pull-request-test.yml +++ b/.github/workflows/pull-request-test.yml @@ -143,6 +143,7 @@ jobs: publicProject, privateProject, updateProjects, + testDatasets, useSession, checkWorkflows, rstudioSession diff --git a/cypress-tests/README.md b/cypress-tests/README.md index a0baceeea1..882143bd7e 100644 --- a/cypress-tests/README.md +++ b/cypress-tests/README.md @@ -1,108 +1,58 @@ -# Cypress Tests for Renku +# Cypress tests for Renku -This repository aims to fill in gaps in current tests with Cypress. +This section includes a set of acceptance tests for RenkuLab. The tests are +written using [Cypress](https://www.cypress.io), run in Chrome, and the main target +is the web interface of RenkuLab. -# Test suites +## Running the tests -We currently have the following test suites +You need a fully working RenkuLab deployment to run the tests against. You will also +need a user account for the deployment. -## R-Studio sessions +If you wish to run the tests locally, you will need to clone the repository and have a +recent version of Node.js installed. Then follow these steps: -- Register a user / login -- Create a R project -- Launch a session -- Open the session in the iframe view and test basic functionality -- Stop the session -- Delete the created project +- Create a `cypress.env.json` file with the necessary user credentials. You can use the + `cypress.env.template.json` file as a template. +- Install the dependencies with `npm install`. +- Run the tests with `npm run e2e`. If you prefer not to use the GUI, you can run the + tests in headless mode with `npm run e2e:headless`. -## Public Project +Here is a list of the environment variable that you should set in the +`cypress.env.json` file: -- Register a user / login -- Create a Python project -- Search for the project -- Check the content in the Overview tab -- Check the content in the Files tab -- Create and modify a dataset -- View the project settings +| VARIABLE | USE | +|-----------------|-------------------------------------------------------| +| BASE_URL | Full URL of the target environment. | +| TEST_EMAIL | The email used to register on Keycloak. | +| TEST_PASSWORD | Password. | +| TEST_FIRST_NAME | First name. | +| TEST_LAST_NAME | Last name. | +| TEST_USERNAME | Username. Usually, it's the email without the domain. | +> Tip: you might prefer not to save you password in plain text in the `cypress.env.json` + file. In that case, you can use the `TEST_PASSWORD` variable to the command line when + running the tests. For example `TEST_PASSWORD=mySecretPassword npm run e2e`.` -# Limitations +## Integration with CI pipeline -- The tests expect to have to register first. If the registration fails - then the tests will try to login with the provided credentials. After - this the tests will check if the login succceeded or if an additional - login is required (as is the case of CI deployments). These are the only - login scenarios that are supported. The tests cannot handle logging in - through any but the Renku login screens. -- The tests expect to use the one set of provided credentials to log in - to up to 2 different Renku deployments. In the case where the Gitlab used by - Renku is part of another deployment the tests will try to log in twice. -- Using `https` if possible is reccomended in the `BASE_URL` environment - variable. Completing the registration and logging in fails if the `http` - is used on the standard dev and/or CI Renku deployments. -- Using the electron browser from cypress will not work because rstudio does not load at - all in the electron browser. -- Tests currently do not run on Firefox. +The primary purpose of the tests is to spot regressions early on new pull requests. +Most Renku repositories have a CI pipeline that runs the tests automatically for every +commit. That requires deploying a full RenkuLab instance using the +`/deploy #persist` command. The tests are run automatically in headless mode unless +the flag `#notest` is included. -# Requirements +You can read more details in the documentation in the +[renku-actions repository](https://github.com/SwissDataScienceCenter/renku-actions/tree/master/test-renku-cypress). -- Node LTS or later. -- On Linux you will need additional libraries see [here](https://docs.cypress.io/guides/getting-started/installing-cypress#Linux-Prerequisites). -- You can also swap the regular `Dockerfile` in `.devcontainer/devcontainer.json` with `Dockerfile.cypress` - and launch a dev container. In the dev container you have to run the tests headless but you will - have access to a Chrome browser. +Mind that including the `#persist` flag is currently necessary since the deployment +is deleted automatically after running a separate set of acceptance tests; the Cypress +tests generally run much quicker but they are not guaranteed to finish on time. +Re-running single tests would also fail when the deployment is deleted. -# To install +## Limitations -``` -npm install -``` - -# To run - -The tests use environment variables to get certain information. -You can set these as shell enviornment variables before running, or -you can copy the `cypress.env.template.json` file to `cypress.env.json` and set the variables there. - -**Run with environment variables set in cypress.env.json** -``` -npm run e2e -``` -**Run passing in environment variables** -``` -TEST_USERNAME=my.username TEST_EMAIL=my.username@email.com TEST_PASSWORD=XXXXX TEST_FIRST_NAME=fname TEST_LAST_NAME=lname BASE_URL=https://dev.renku.ch npm run e2e -``` - -# Integration with CI pipeline - -In repos that have integrated the actions for running the cypress tests -(currently `renku` and `renku-ui`), it is possible to run these tests -in the CI pipline with the deploy command. - -In the `renku` repo, the cypress-tests are run when a PR is deployed. - -E.g., - -``` -/deploy -``` - -In other repos, it is necessary to explicitly request that the -cypress tests are run by adding the `#cypress` token (the deployment) -also needs to be persisted: - -``` -/deploy #persist #cypress -``` - -Finally, it is possible to run the cypress acceptance tests without -the selenium acceptance tests: - -``` -/deploy #persist #notest #cypress -``` - -To incorporate the cypress tests into the CI pipleines of other -projects, see the documentation in the [renku-actions repo](https://github.com/SwissDataScienceCenter/renku-actions/tree/master/test-renku-cypress). - -The action in the [renku-ui repo](https://github.com/SwissDataScienceCenter/renku-ui/blob/master/.github/workflows/acceptance-tests.yml#L109) could also be helpful to look at. +- Using the electron browser from Cypress will not work because a few features in + RenkuLab do not work with that (E.G: RStudio does not load at all). +- Tests currently do not run on Firefox. Please use [Chrome](https://www.google.com/chrome) + or [Chromium](https://www.chromium.org) instead. diff --git a/cypress-tests/cypress/e2e/checkWorkflows.cy.ts b/cypress-tests/cypress/e2e/checkWorkflows.cy.ts index e454e41fbc..da6e0670b4 100644 --- a/cypress-tests/cypress/e2e/checkWorkflows.cy.ts +++ b/cypress-tests/cypress/e2e/checkWorkflows.cy.ts @@ -34,15 +34,11 @@ describe("Workflows pages", () => { cy.visitAndLoadProject(project); // Go the the workflows page and check the details of a workflow - cy.dataCy("project-navbar") - .contains("a.nav-link", "Workflows") - .should("be.visible") - .click(); - + cy.getProjectSection("Workflows").click(); cy.get("[data-cy=workflows-page]", { timeout: TIMEOUTS.long }).should( "be.visible" ); - cy.dataCy("workflows-browser") + cy.getDataCy("workflows-browser") .should("be.visible") .children() .should("have.length", 9) @@ -50,39 +46,39 @@ describe("Workflows pages", () => { .should("be.visible") .click(); - cy.dataCy("workflow-details") + cy.getDataCy("workflow-details") .should("be.visible") .contains("This is defintely a useless workflow.") .should("be.visible"); - cy.dataCy("workflow-details").contains("keyword1,").should("be.visible"); - cy.dataCy("workflow-details") + cy.getDataCy("workflow-details").contains("keyword1,").should("be.visible"); + cy.getDataCy("workflow-details") .contains("renku workflow execute useless-workflow-with-kw") .should("be.visible"); // Play with the workflow browser and check composite workflows link other workflows - cy.dataCy("workflows-browser") + cy.getDataCy("workflows-browser") .should("be.visible") .children() .should("have.length", 9); - cy.dataCy("workflows-browser") + cy.getDataCy("workflows-browser") .get(".rk-tree-item--children") .should("not.exist"); - cy.dataCy("workflows-browser") + cy.getDataCy("workflows-browser") .children() .contains("parent-composite") .should("be.visible") .click(); - cy.dataCy("workflows-browser").children().should("have.length", 11); - cy.dataCy("workflows-browser") + cy.getDataCy("workflows-browser").children().should("have.length", 11); + cy.getDataCy("workflows-browser") .get(".rk-tree-item--children") .should("be.visible"); - cy.dataCy("workflow-details").within(() => { + cy.getDataCy("workflow-details").within(() => { cy.root().contains("Workflow (Composite)").should("be.visible"); cy.root().contains("div h5", "composite1").should("be.visible").click(); }); - cy.dataCy("workflow-details").within(() => { + cy.getDataCy("workflow-details").within(() => { cy.root() .contains("renku workflow execute composite1") .should("be.visible"); @@ -94,19 +90,19 @@ describe("Workflows pages", () => { .click(); }); - cy.dataCy("workflow-details") + cy.getDataCy("workflow-details") .contains("div", "input-2") .should("be.visible") .click(); // Check links to the file browser - cy.dataCy("workflow-details") + cy.getDataCy("workflow-details") .contains("td", "Default value") .should("be.visible") .siblings("td") .contains("span", "m") .should("be.visible"); - cy.dataCy("workflow-details") + cy.getDataCy("workflow-details") .get("a#icon-link-5") .should("be.visible") .click(); diff --git a/cypress-tests/cypress/e2e/privateProject.cy.ts b/cypress-tests/cypress/e2e/privateProject.cy.ts index edbb13c1f5..b3d85c3cf6 100644 --- a/cypress-tests/cypress/e2e/privateProject.cy.ts +++ b/cypress-tests/cypress/e2e/privateProject.cy.ts @@ -1,7 +1,7 @@ import { TIMEOUTS } from "../../config"; import { ProjectIdentifier, - generatorProjectName + generatorProjectName, } from "../support/commands/projects"; import { validateLogin } from "../support/commands/general"; @@ -35,7 +35,11 @@ describe("Basic public project functionality", () => { // Create a project for the local spec if (projectTestConfig.shouldCreateProject) { cy.visit("/"); - cy.createProject({ templateName: "Python", ...projectIdentifier, visibility: "private" }); + cy.createProject({ + templateName: "Python", + ...projectIdentifier, + visibility: "private", + }); } }); @@ -53,18 +57,17 @@ describe("Basic public project functionality", () => { it("Can search for project only when logged in", () => { // Assess the project has been indexed properly - cy.dataCy("project-navbar", true) - .contains("a.nav-link", "Settings") - .should("be.visible") - .click(); - cy.dataCy("project-settings-knowledge-graph") + cy.getProjectSection("Settings").click(); + cy.getDataCy("project-settings-knowledge-graph") .contains("Project indexing", { timeout: TIMEOUTS.vlong }) .should("exist"); - cy.dataCy("kg-status-section-open").should("exist").click(); - cy.dataCy("project-settings-knowledge-graph") + cy.getDataCy("kg-status-section-open").should("exist").click(); + cy.getDataCy("project-settings-knowledge-graph") .contains("Everything indexed", { timeout: TIMEOUTS.vlong }) .should("exist"); - cy.dataCy("visibility-private").should("be.visible").should("be.checked"); + cy.getDataCy("visibility-private") + .should("be.visible") + .should("be.checked"); cy.searchForProject(projectIdentifier, true); // logout and search for the project and log back in @@ -76,19 +79,18 @@ describe("Basic public project functionality", () => { it("Can always search for project after changing the visibility", () => { // Change visibility to public - cy.dataCy("project-navbar", true) - .contains("a.nav-link", "Settings") - .should("be.visible") - .click(); - cy.dataCy("project-settings-knowledge-graph") + cy.getProjectSection("Settings").click(); + cy.getDataCy("project-settings-knowledge-graph") .contains("Project indexing", { timeout: TIMEOUTS.vlong }) .should("exist"); - cy.dataCy("visibility-private").should("be.visible").should("be.checked"); - cy.dataCy("visibility-public").should("be.visible").check(); + cy.getDataCy("visibility-private") + .should("be.visible") + .should("be.checked"); + cy.getDataCy("visibility-public").should("be.visible").check(); cy.get(".modal") .contains("Change visibility to Public") .should("be.visible"); - cy.dataCy("update-visibility-btn").should("be.visible").click(); + cy.getDataCy("update-visibility-btn").should("be.visible").click(); cy.get(".modal .alert-success", { timeout: TIMEOUTS.long }) .contains("The visibility of the project has been modified") .should("be.visible"); @@ -99,14 +101,14 @@ describe("Basic public project functionality", () => { // Check all is up-to-date and ready. cy.get(".modal button.btn-close").should("be.visible").click(); - cy.dataCy("kg-status-section-open").should("exist").click(); - cy.dataCy("project-settings-knowledge-graph") + cy.getDataCy("kg-status-section-open").should("exist").click(); + cy.getDataCy("project-settings-knowledge-graph") .contains("Everything indexed", { timeout: TIMEOUTS.vlong }) .should("exist"); - cy.dataCy("visibility-private") + cy.getDataCy("visibility-private") .should("be.visible") .should("not.be.checked"); - cy.dataCy("visibility-public").should("be.visible").should("be.checked"); + cy.getDataCy("visibility-public").should("be.visible").should("be.checked"); // Search the project as both logged in and logged out. cy.searchForProject(projectIdentifier, true); @@ -114,42 +116,12 @@ describe("Basic public project functionality", () => { cy.get("#nav-hamburger").should("be.visible").click(); cy.searchForProject(projectIdentifier, false); cy.robustLogin(); - }); it("Deleting the project removes it from the search page", () => { // Delete the project cy.visitAndLoadProject(projectIdentifier); - - const slug = projectIdentifier.namespace + "/" + projectIdentifier.name; - cy.intercept("DELETE", `/ui-server/api/kg/projects/${slug}`).as( - "deleteProject" - ); - cy.dataCy("project-navbar", true) - .contains("a.nav-link", "Settings") - .should("exist") - .click(); - cy.dataCy("project-settings-general-delete-project") - .should("be.visible") - .find("button") - .contains("Delete project") - .should("be.visible") - .click(); - cy.contains("Are you absolutely sure?"); - cy.get("input[name=project-settings-general-delete-confirm-box]").type( - slug - ); - cy.get("button") - .contains("Yes, delete this project") - .should("be.visible") - .should("be.enabled") - .click(); - cy.wait("@deleteProject"); - - cy.url().should("not.contain", `/projects/${slug}`); - cy.get(".Toastify") - .contains(`Project ${slug} deleted`) - .should("be.visible"); + cy.deleteProject(projectIdentifier); // Check that the project is not listed anymore on the search page cy.searchForProject(projectIdentifier, false); diff --git a/cypress-tests/cypress/e2e/publicProject.cy.ts b/cypress-tests/cypress/e2e/publicProject.cy.ts index 65689da5f7..a5be46a76d 100644 --- a/cypress-tests/cypress/e2e/publicProject.cy.ts +++ b/cypress-tests/cypress/e2e/publicProject.cy.ts @@ -1,5 +1,3 @@ -import { v4 as uuidv4 } from "uuid"; - import { TIMEOUTS } from "../../config"; import { ProjectIdentifier, @@ -17,30 +15,13 @@ const projectTestConfig = { // ? Uncomment to debug using an existing project // projectTestConfig.shouldCreateProject = false; -// projectTestConfig.projectName = "cypress-publicproject-4ed4fb12c5e6"; +// projectTestConfig.projectName = "cypress-publicproject-8e01a2e0a8c1"; const projectIdentifier: ProjectIdentifier = { name: projectTestConfig.projectName, namespace: username, }; -/** - * Helper function to re-try clicking on a project page link when it detatches from the DOM - * @param page - target project sub-page - */ -function robustNavigateToProjectPage(page: string) { - // Requery when the element is detatched - cy.getProjectPageLink(projectIdentifier, page) - .first() - .then(($el) => - Cypress.dom.isDetached($el) - ? cy.getProjectPageLink(projectIdentifier, page) - : $el - ) - .first() - .click(); -} - describe("Basic public project functionality", () => { before(() => { // Use a session to preserve login data @@ -71,17 +52,14 @@ describe("Basic public project functionality", () => { cy.visitAndLoadProject(projectIdentifier); }); - it.only("Can search for project", () => { - // assess the project has been indexed properly -- this might take time if it was recently created - cy.dataCy("project-navbar", true) - .contains("a.nav-link", "Settings") - .should("be.visible") - .click(); - cy.dataCy("project-settings-knowledge-graph") + it("Can search for project", () => { + // Assess the project has been indexed properly.This might take time for new projects. + cy.getProjectSection("Settings").click(); + cy.getDataCy("project-settings-knowledge-graph") .contains("Project indexing", { timeout: TIMEOUTS.vlong }) .should("exist"); - cy.dataCy("kg-status-section-open").should("exist").click(); - cy.dataCy("project-settings-knowledge-graph") + cy.getDataCy("kg-status-section-open").should("exist").click(); + cy.getDataCy("project-settings-knowledge-graph") .contains("Everything indexed", { timeout: TIMEOUTS.vlong }) .should("exist"); cy.searchForProject(projectIdentifier); @@ -94,6 +72,7 @@ describe("Basic public project functionality", () => { }); it("Can see overview content and check the clone URLs", () => { + cy.getProjectSection("Overview").click(); cy.contains("README.md").should("be.visible"); cy.contains("This is a Renku project").should("be.visible"); if (projectTestConfig.shouldCreateProject) { @@ -102,9 +81,9 @@ describe("Basic public project functionality", () => { }).should("be.visible"); } - // Check the clone URLs + // Check the URL to clone is correct. const projectUrl = projectUrlFromIdentifier(projectIdentifier); - // The clone url does not include "/projects" in it + // ? The clone url does not include "/projects" in it const cloneSubUrl = projectUrl.substring("/projects".length).toLowerCase(); cy.get("button").contains("Clone").should("be.visible").click(); cy.contains("Clone with Renku") @@ -131,22 +110,21 @@ describe("Basic public project functionality", () => { it("Verify project version is up to date", () => { cy.contains("Status").should("not.exist"); - cy.dataCy("project-status-icon-element").should("not.exist"); - cy.dataCy("project-navbar", true) - .contains("a.nav-link", "Settings") - .should("exist") - .click(); - cy.dataCy("project-version-section-open").should("exist").click(); - cy.dataCy("project-settings-migration-status") - .contains("This project uses the latest") - .should("exist"); - cy.dataCy("project-settings-knowledge-graph") + cy.getDataCy("project-status-icon-element").should("not.exist"); + cy.getProjectSection("Settings").click(); + cy.getDataCy("project-version-section-open").should("exist").click(); + if (projectTestConfig.shouldCreateProject) { + cy.getDataCy("project-settings-migration-status") + .contains("This project uses the latest") + .should("exist"); + } + cy.getDataCy("project-settings-knowledge-graph") .contains("Project indexing") .should("exist"); }); it("Can view files", () => { - cy.contains("Files").should("exist").click(); + cy.getProjectSection("Files").click(); cy.get("div#tree-content").contains(".renku").should("exist").click(); cy.get("div#tree-content").contains("metadata").should("exist").click(); cy.getProjectPageLink( @@ -162,93 +140,26 @@ describe("Basic public project functionality", () => { .should("be.visible"); }); - it("Can work with datasets", () => { - let migrationsInvoked = false; - cy.intercept("/ui-server/api/renku/cache.migrations_check*", (req) => { - migrationsInvoked = true; - }).as("checkMigrations"); - const datasetName = `cypress-dataset-${uuidv4().substring(24)}`; - const datasetTitle = datasetName.replace("-", " "); - cy.getProjectPageLink(projectIdentifier, "/datasets").click(); - // wait for migrations check to terminate - if (migrationsInvoked) cy.wait("@checkMigrations"); - // A project we just created should have no datasets - if (projectTestConfig.shouldCreateProject) { - cy.contains("No datasets found for this project.", { - timeout: TIMEOUTS.vlong, - }).should("be.visible"); - } - - // Create a dataset - cy.get("#plus-dropdown").should("exist").click(); - cy.get("#navbar-dataset-new").should("exist").click(); - cy.dataCy("input-title").type(datasetTitle); - cy.dataCy("input-keywords").type("test{enter}automated test{enter}"); - cy.get("div.ck.ck-editor__main div.ck.ck-content") - .should("exist") - .type("This is a test dataset"); - cy.intercept("/ui-server/api/renku/*/datasets.list?git_url=*").as( - "listDatasets" - ); - cy.dataCy("submit-button").click(); - cy.get(".progress-box").should("be.visible"); - cy.wait("@listDatasets", { timeout: TIMEOUTS.vlong }); - - // Check that the content is as expected - cy.contains(datasetTitle).should("be.visible"); - cy.contains("#test").should("be.visible"); - cy.contains("#automated test").should("be.visible"); - cy.contains("This is a test dataset").should("be.visible"); - - // Modify the dataset - cy.dataCy("edit-dataset-button").last().click(); - cy.dataCy("input-keywords").type("modified{enter}"); - cy.dataCy("submit-button").click(); - cy.contains("Modifying dataset").should("be.visible"); - cy.wait("@listDatasets", { timeout: TIMEOUTS.vlong }); - cy.contains("#modified").should("be.visible"); - - // Check that we can see the dataset - cy.getProjectPageLink(projectIdentifier, "/datasets").click(); - cy.dataCy("entity-description") - .contains("This is a test dataset") - .should("exist"); - cy.contains(datasetTitle).should("be.visible").click(); - - // Delete the dataset - cy.dataCy("delete-dataset-button").click(); - cy.contains("Are you sure you want to delete dataset").should("be.visible"); - cy.get(".modal").contains("Delete dataset").click(); - cy.get(".modal").contains("Deleting dataset...").should("be.visible"); - }); - it("Can view and modify sessions settings", () => { - cy.dataCy("project-navbar").contains("Settings").should("exist").click(); cy.intercept("/ui-server/api/renku/*/config.set").as("configSet"); cy.intercept("/ui-server/api/renku/*/config.show?git_url=*").as( "getConfig" ); - const navigateToSettingsSessions = ({ - waitForApis, - }: { waitForApis?: boolean } = {}) => { - robustNavigateToProjectPage("/settings"); - cy.get(".form-rk-green form").contains("Project Tags").should("exist"); - robustNavigateToProjectPage("/settings/sessions"); - cy.get("h3").contains("Session settings").should("exist"); - cy.intercept("/ui-server/api/data/resource_pools").as("getResourcePools"); - if (waitForApis) cy.wait("@getConfig", { timeout: TIMEOUTS.long }); - }; - // Make sure the renku.ini is in a pristine state - robustNavigateToProjectPage("/files"); + cy.getProjectSection("Files").click(); cy.get("div#tree-content").contains(".renku").should("exist").click(); cy.get("div#tree-content").contains("renku.ini").should("exist").click(); cy.get("pre.hljs").should("be.visible"); cy.get("pre.hljs").contains("cpu_request").should("not.exist"); // Add a compute requirement for sessions - navigateToSettingsSessions({ waitForApis: true }); + cy.getProjectSection("Settings").click(); + cy.getDataCy("settings-navbar") + .contains("a", "Sessions") + .should("exist") + .click(); + cy.wait("@getConfig", { timeout: TIMEOUTS.long }); cy.contains("label", "Number of CPUs") .parent() .find("input.form-control") @@ -260,12 +171,16 @@ describe("Basic public project functionality", () => { cy.wait("@configSet"); cy.contains(".badge", "Saved"); - robustNavigateToProjectPage("/files"); - cy.get("div#tree-content").contains("renku.ini").should("exist").click(); + cy.getProjectSection("Files").click(); + cy.get("#tree-content").contains("renku.ini").should("exist").click(); cy.get(".hljs.language-ini").contains("[interactive]").should("be.visible"); cy.get("pre.hljs").contains("cpu_request = 1.5").should("exist"); - navigateToSettingsSessions(); + cy.getProjectSection("Settings").click(); + cy.getDataCy("settings-navbar") + .contains("a", "Sessions") + .should("exist") + .click(); cy.get("#project-settings-sessions-interactive-cpu-request-reset") .should("be.visible") .click(); @@ -273,46 +188,14 @@ describe("Basic public project functionality", () => { cy.wait("@configSet"); cy.contains(".badge", "Saved"); - robustNavigateToProjectPage("/files"); + cy.getProjectSection("Files").click(); cy.get("div#tree-content").contains("renku.ini").should("exist").click(); cy.get(".hljs.language-ini").contains("[interactive]").should("be.visible"); cy.get("pre.hljs").contains("cpu_request").should("not.exist"); }); it("Can delete the project from the UI", () => { - const slug = projectIdentifier.namespace + "/" + projectIdentifier.name; - cy.intercept("DELETE", `/ui-server/api/kg/projects/${slug}`).as( - "deleteProject" - ); - - // Delete the project - cy.dataCy("project-navbar", true) - .contains("a.nav-link", "Settings") - .should("exist") - .click(); - cy.dataCy("project-settings-general-delete-project") - .should("be.visible") - .find("button") - .contains("Delete project") - .should("be.visible") - .click(); - cy.contains("Are you absolutely sure?"); - cy.get("input[name=project-settings-general-delete-confirm-box]").type( - slug - ); - cy.get("button") - .contains("Yes, delete this project") - .should("be.visible") - .should("be.enabled") - .click(); - cy.wait("@deleteProject"); - - cy.url().should("not.contain", `/projects/${slug}`); - cy.get(".Toastify") - .contains(`Project ${slug} deleted`) - .should("be.visible"); - - // Check that the project is not listed anymore on the search page + cy.deleteProject(projectIdentifier, true); cy.searchForProject(projectIdentifier, false); }); }); diff --git a/cypress-tests/cypress/e2e/rstudioSession.cy.ts b/cypress-tests/cypress/e2e/rstudioSession.cy.ts index 90c9a5d4e0..4e485cbdc7 100644 --- a/cypress-tests/cypress/e2e/rstudioSession.cy.ts +++ b/cypress-tests/cypress/e2e/rstudioSession.cy.ts @@ -43,7 +43,7 @@ describe("Basic rstudio functionality", () => { after(() => { if (projectTestConfig.shouldCreateProject) - cy.deleteProject(projectIdentifier); + cy.deleteProjectFromAPI(projectIdentifier); }); beforeEach(() => { @@ -97,7 +97,7 @@ describe("Basic rstudio functionality", () => { }); // Stops the session - cy.stopSessionFromIframe(); + cy.stopSession(); } ); }); diff --git a/cypress-tests/cypress/e2e/testDatasets.cy.ts b/cypress-tests/cypress/e2e/testDatasets.cy.ts new file mode 100644 index 0000000000..7cc99c8db9 --- /dev/null +++ b/cypress-tests/cypress/e2e/testDatasets.cy.ts @@ -0,0 +1,176 @@ +import { TIMEOUTS } from "../../config"; +import { + ProjectIdentifier, + generatorProjectName, +} from "../support/commands/projects"; +import { validateLogin } from "../support/commands/general"; +import { generatorDatasetName } from "../support/commands/datasets"; + +const username = Cypress.env("TEST_USERNAME"); + +const projectTestConfig = { + shouldCreateProject: true, + projectName: generatorProjectName("testDatasets"), +}; + +// ? Uncomment to debug using an existing project +// projectTestConfig.shouldCreateProject = false; +// projectTestConfig.projectName = "cypress-usedatasets-a572ce0e177d"; + +const projectIdentifier: ProjectIdentifier = { + name: projectTestConfig.projectName, + namespace: username, +}; +const generatedDatasetName = generatorDatasetName("Dataset"); + +describe("Basic datasets functionality", () => { + before(() => { + // Use a session to preserve login data + cy.session( + "login-publicProject", + () => { + cy.robustLogin(); + }, + validateLogin + ); + + // Create a project for the local spec + if (projectTestConfig.shouldCreateProject) { + cy.visit("/"); + cy.createProject({ templateName: "Python", ...projectIdentifier }); + } + + // Intercept listing datasets + cy.intercept("/ui-server/api/renku/*/datasets.list?git_url=*", (req) => { + listDatasetsInvoked = true; + }).as("listDatasets"); + }); + + beforeEach(() => { + // Restore the session + cy.session( + "login-publicProject", + () => { + cy.robustLogin(); + }, + validateLogin + ); + cy.visitAndLoadProject(projectIdentifier); + + // Reset dataset interceptor + listDatasetsInvoked = false; + }); + + after(() => { + if (projectTestConfig.shouldCreateProject) + cy.deleteProjectFromAPI(projectIdentifier); + }); + + const keywords = ["test", "automated test", "Cypress test"]; + const description = "This is a test dataset form a Cypress tests"; + + let listDatasetsInvoked = false; + + it("Create a dataset", () => { + cy.getProjectSection("Datasets").click(); + if (listDatasetsInvoked) + cy.wait("@listDatasets", { timeout: TIMEOUTS.long }); + + // A new project should not contain datasets + if (projectTestConfig.shouldCreateProject) { + cy.contains("No datasets found for this project.", { + timeout: TIMEOUTS.vlong, + }).should("be.visible"); + } + // Create a dataset + cy.get("#plus-dropdown").should("exist").click(); + cy.get("#navbar-dataset-new").should("exist").click(); + cy.getDataCy("input-title").type(generatedDatasetName.name); + + cy.getDataCy("input-keywords").type( + keywords.reduce((text, value) => `${text}${value}{enter}`, "") + ); + cy.get("div.ck.ck-editor__main div.ck.ck-content") + .should("exist") + .type(description); + listDatasetsInvoked = false; + cy.getDataCy("submit-button").click(); + cy.get(".progress-box").should("be.visible"); + if (listDatasetsInvoked) + cy.wait("@listDatasets", { timeout: TIMEOUTS.long }); + + // Check that the content is correct + cy.getDataCy("dataset-title") + .contains(generatedDatasetName.name) + .should("be.visible"); + for (const keyword of keywords) { + cy.getDataCy("entity-tag-list") + .contains(`#${keyword}`) + .should("be.visible"); + } + cy.contains(description).get(".renku-markdown").should("be.visible"); + }); + + it("Modify the dataset and search for it", () => { + // Add a keyword and check it is visible + cy.getProjectSection("Datasets").click(); + if (listDatasetsInvoked) + cy.wait("@listDatasets", { timeout: TIMEOUTS.long }); + cy.getDataCy("list-card-title") + .contains(generatedDatasetName.name) + .should("be.visible") + .click(); + cy.getDataCy("edit-dataset-button").last().click(); + const newKeyword = "additioanl keyword"; + cy.getDataCy("input-keywords").type(`${newKeyword}{enter}`); + listDatasetsInvoked = false; + cy.getDataCy("submit-button").click(); + cy.contains("Modifying dataset").should("be.visible"); + if (listDatasetsInvoked) + cy.wait("@listDatasets", { timeout: TIMEOUTS.long }); + keywords.push(newKeyword); + for (const keyword of keywords) { + cy.getDataCy("entity-tag-list") + .contains(`#${keyword}`) + .should("be.visible"); + } + + // Search for the dataset after the project has been indexed + cy.getDataCy("go-back-button").should("be.visible").click(); + cy.getProjectSection("Settings").click(); + cy.getDataCy("kg-status-section-open").should("exist").click(); + cy.getDataCy("project-settings-knowledge-graph") + .contains("Everything indexed", { timeout: TIMEOUTS.vlong }) + .should("exist"); + cy.searchForDataset(generatedDatasetName.slug); + }); + + it("Delete the dataset and verify it is not searchable anaymore", () => { + cy.getProjectSection("Datasets").click(); + if (listDatasetsInvoked) + cy.wait("@listDatasets", { timeout: TIMEOUTS.long }); + cy.getDataCy("list-card-title") + .contains(generatedDatasetName.name) + .should("be.visible") + .click(); + + cy.getDataCy("delete-dataset-button").click(); + cy.contains("Are you sure you want to delete dataset").should("be.visible"); + cy.get(".modal").contains("Delete dataset").click(); + cy.get(".modal").contains("Deleting dataset...").should("be.visible"); + + // Check the dataset is gone after the project has been indexed + if (projectTestConfig.shouldCreateProject) { + cy.contains("No datasets found for this project.", { + timeout: TIMEOUTS.vlong, + }).should("be.visible"); + } + cy.getProjectSection("Settings").click(); + cy.getDataCy("kg-status-section-open").should("exist").click(); + cy.getDataCy("project-settings-knowledge-graph") + .contains("Everything indexed", { timeout: TIMEOUTS.vlong }) + .should("exist"); + // ? Currently, it doesn't disappear instantly + // cy.searchForDataset(generatedDatasetName.slug, false); + }); +}); diff --git a/cypress-tests/cypress/e2e/updateProjects.cy.ts b/cypress-tests/cypress/e2e/updateProjects.cy.ts index 3602f88e93..480b4927c8 100644 --- a/cypress-tests/cypress/e2e/updateProjects.cy.ts +++ b/cypress-tests/cypress/e2e/updateProjects.cy.ts @@ -49,7 +49,7 @@ describe("Fork and update old projects", () => { name: projects.v8, }; cy.visitAndLoadProject(forkedProject, true); - cy.dataCy("header-project") + cy.getDataCy("header-project") .contains("Error obtaining datasets") .should("be.visible"); cy.forkProject(forkedProject, tempName); @@ -60,58 +60,54 @@ describe("Fork and update old projects", () => { ? { namespace: username, name: tempName } : { namespace: projects.namespace, name: projects.v8 }; if (!projects.shouldFork) cy.visitAndLoadProject(targetProject, true); - cy.dataCy("project-navbar", true) - .contains("a.nav-link", "Settings") - .as("project-settings-link") - .should("be.visible"); - cy.get("@project-settings-link").should("be.visible").click(); + cy.getProjectSection("Settings").click(); // verify project requires update - cy.dataCy("project-status-icon-element").should("be.visible"); - cy.dataCy("project-settings-migration-status") + cy.getDataCy("project-status-icon-element").should("be.visible"); + cy.getDataCy("project-settings-migration-status") .contains("Project update required") .should("be.visible"); - cy.dataCy("project-version-section-open").should("be.visible").click(); - cy.dataCy("project-settings-migration-status") + cy.getDataCy("project-version-section-open").should("be.visible").click(); + cy.getDataCy("project-settings-migration-status") .contains("still on version 8 while the latest version is") .should("be.visible"); - cy.dataCy("project-settings-migration-status") + cy.getDataCy("project-settings-migration-status") .get("#button-update-projectMigrationStatus") .as("button-trigger-migration") .should("be.visible"); cy.get("@button-trigger-migration").should("be.visible").click(); - cy.dataCy("project-settings-migration-status") + cy.getDataCy("project-settings-migration-status") .contains("button", "Updating") .should("be.visible"); - cy.dataCy("project-settings-migration-status") + cy.getDataCy("project-settings-migration-status") .contains("Refreshing project data") .should("be.visible"); - cy.dataCy("project-settings-migration-status") + cy.getDataCy("project-settings-migration-status") .contains("This project uses the latest") .should("be.visible"); - cy.dataCy("project-status-icon-element").should("not.exist"); + cy.getDataCy("project-status-icon-element").should("not.exist"); // update template - cy.dataCy("project-settings-migration-status") + cy.getDataCy("project-settings-migration-status") .contains("There is a new version of the template") .should("be.visible"); - cy.dataCy("project-settings-migration-status") + cy.getDataCy("project-settings-migration-status") .get("#button-update-projectMigrationStatus") .as("button-update-template") .should("be.visible"); cy.get("@button-update-template").should("be.visible").click(); - cy.dataCy("project-settings-migration-status") + cy.getDataCy("project-settings-migration-status") .contains("button", "Updating") .should("be.visible"); - cy.dataCy("project-settings-migration-status") + cy.getDataCy("project-settings-migration-status") .contains("Refreshing project data") .should("be.visible"); - cy.dataCy("project-settings-migration-status") + cy.getDataCy("project-settings-migration-status") .contains("Project up to date") .should("be.visible"); // delete the project - if (projects.shouldFork) cy.deleteProject(targetProject); + if (projects.shouldFork) cy.deleteProjectFromAPI(targetProject); }); it("Update an outdated project - verify commits have been added", () => { @@ -138,82 +134,76 @@ describe("Fork and update old projects", () => { commitFetched = true; } ).as("getCommits"); - cy.getProjectPageLink(targetProject, "overview/commits") - .should("be.visible") + cy.getDataCy("project-overview-nav") + .contains("a", "Commits") + .should("exist") .click(); if (!commitFetched) { cy.wait(1000); // eslint-disable-line cypress/no-unnecessary-waiting - cy.dataCy("refresh-commits") + cy.getDataCy("refresh-commits") .as("refresh-commits-button-1") .should("be.visible"); cy.get("@refresh-commits-button-1").should("be.visible").click(); cy.wait(1000); // eslint-disable-line cypress/no-unnecessary-waiting } cy.wait("@getCommits", { timeout: TIMEOUTS.long }); - cy.dataCy("project-overview-content") + cy.getDataCy("project-overview-content") .get(".card-body ul li.commit-object") .should("have.length", 1); // go to project settings and verify it requires an upodate - cy.dataCy("project-navbar") - .contains("a.nav-link", "Settings") - .as("project-settings-link") - .should("be.visible"); - cy.get("@project-settings-link").should("be.visible").click(); - cy.dataCy("project-status-icon-element").should("be.visible"); - cy.dataCy("project-settings-migration-status") + cy.getProjectSection("Settings").click(); + cy.getDataCy("project-status-icon-element").should("be.visible"); + cy.getDataCy("project-settings-migration-status") .contains("Project update required") .should("be.visible"); - cy.dataCy("project-version-section-open").should("be.visible").click(); - cy.dataCy("project-settings-migration-status") + cy.getDataCy("project-version-section-open").should("be.visible").click(); + cy.getDataCy("project-settings-migration-status") .contains("still on version 9 while the latest version is") .should("be.visible"); // update project - cy.dataCy("project-settings-migration-status") + cy.getDataCy("project-settings-migration-status") .get("#button-update-projectMigrationStatus") .as("button-trigger-migration") .should("be.visible"); cy.get("@button-trigger-migration").should("be.visible").click(); - cy.dataCy("project-settings-migration-status") + cy.getDataCy("project-settings-migration-status") .contains("button", "Updating") .should("be.visible"); - cy.dataCy("project-settings-migration-status") + cy.getDataCy("project-settings-migration-status") .contains("Refreshing project data") .should("be.visible"); - cy.dataCy("project-settings-migration-status") + cy.getDataCy("project-settings-migration-status") .contains("This project uses the latest") .should("be.visible"); - cy.dataCy("project-status-icon-element").should("not.exist"); + cy.getDataCy("project-status-icon-element").should("not.exist"); // verify the commits were added commitFetched = false; - cy.dataCy("project-navbar", true) - .contains("a.nav-link", "Overview") - .as("project-overview-link") - .should("be.visible"); - cy.get("@project-overview-link").should("be.visible").click(); - cy.getProjectPageLink(targetProject, "overview/commits") - .should("be.visible") + cy.getProjectSection("Overview").click(); + cy.getDataCy("project-overview-nav") + .contains("a", "Commits") + .should("exist") .click(); if (!commitFetched) { cy.wait(1000); // eslint-disable-line cypress/no-unnecessary-waiting - cy.dataCy("refresh-commits") + cy.getDataCy("refresh-commits") .as("refresh-commits-button-2") .should("be.visible"); cy.get("@refresh-commits-button-2").should("be.visible").click(); cy.wait(1000); // eslint-disable-line cypress/no-unnecessary-waiting } cy.wait("@getCommits", { timeout: TIMEOUTS.long }); - cy.dataCy("project-overview-content") + cy.getDataCy("project-overview-content") .get(".card-body ul li.commit-object") .should("have.length.greaterThan", 1); - cy.dataCy("project-overview-content") + cy.getDataCy("project-overview-content") .get(".card-body ul li.commit-object") .contains("migrate to latest version") .should("be.visible"); // delete the project - if (projects.shouldFork) cy.deleteProject(targetProject); + if (projects.shouldFork) cy.deleteProjectFromAPI(targetProject); }); }); diff --git a/cypress-tests/cypress/e2e/useSession.cy.ts b/cypress-tests/cypress/e2e/useSession.cy.ts index 496e820e18..b9972448ce 100644 --- a/cypress-tests/cypress/e2e/useSession.cy.ts +++ b/cypress-tests/cypress/e2e/useSession.cy.ts @@ -10,18 +10,23 @@ const projectTestConfig = { }; const workflow = { name: "dummyworkflow", - output: "o.txt" // ? Keep the name short or it won't show up entirely in the file browser + output: "o.txt", // ? Keep the name short or it won't show up entirely in the file browser }; // ? Modify the config -- useful for debugging // projectTestConfig.shouldCreateProject = false; -// projectTestConfig.projectName = "cypress-usesession-2f2b5f2c2ee8"; +// projectTestConfig.projectName = "cypress-usesession-a8c6823e40ff"; const projectIdentifier = { name: projectTestConfig.projectName, namespace: username, }; +const projectWithoutPermissions = { + namespace: "renku-ui-tests", + name: "stable-project", +}; + describe("Basic public project functionality", () => { before(() => { // Use a session to preserve login data @@ -42,7 +47,7 @@ describe("Basic public project functionality", () => { after(() => { if (projectTestConfig.shouldCreateProject) - cy.deleteProject(projectIdentifier); + cy.deleteProjectFromAPI(projectIdentifier); }); beforeEach(() => { @@ -54,27 +59,26 @@ describe("Basic public project functionality", () => { }, validateLogin ); - cy.visitAndLoadProject(projectIdentifier); - cy.stopAllSessionsForProject(projectIdentifier); }); it("Start a new session on the project and interact with the terminal.", () => { + cy.visitAndLoadProject(projectIdentifier); + cy.stopAllSessionsForProject(projectIdentifier); + // Start a session with options let serversInvoked = false; cy.intercept("/ui-server/api/notebooks/servers*", (req) => { serversInvoked = true; }).as("getServers"); if (projectTestConfig.shouldCreateProject) { - cy.dataCy("project-overview-content") + cy.getDataCy("project-overview-content") .contains("your new Renku project", { timeout: TIMEOUTS.long }) .should("exist"); } - cy.getProjectPageLink(projectIdentifier, "sessions") - .should("exist") - .click(); + cy.getProjectSection("Sessions").click(); if (serversInvoked) cy.wait("@getServers"); cy.getProjectPageLink(projectIdentifier, "sessions/new") - .should("exist") + .should("be.visible") .first() .click(); @@ -108,7 +112,7 @@ describe("Basic public project functionality", () => { .contains("Back") .should("exist") .click(); - cy.dataCy("open-session").should("exist").click(); + cy.getDataCy("open-session").should("exist").click(); cy.get(".progress-box .progress-title") .contains("Starting Session") .should("exist"); @@ -127,59 +131,105 @@ describe("Basic public project functionality", () => { // Run a dummy workflow cy.get(".xterm-helper-textarea") .click() - .type(`renku run --name ${workflow.name} echo "123" > ${workflow.output}{enter}`); - cy.get("#filebrowser").should("be.visible").contains(workflow.output).should("be.visible"); - cy.get("#jp-git-sessions") - .get(`button[title="Push committed changes (ahead by 1 commits)"]`) - .should("not.exist"); - - // Push the changes - // ? Switch to using the Save session button as soon as it works again. - // ? Reference: https://github.com/SwissDataScienceCenter/renku-notebooks/issues/1575 - // // cy.dataCy("save-session-button").should("be.visible").click(); - // // cy.get(".modal-session").contains("1 commit will be pushed").should("be.visible"); - // // cy.dataCy("save-session-modal-button").should("be.visible").click(); - cy.get(`[data-id="jp-git-sessions"]`).should("be.visible").click(); - cy.get("#jp-git-sessions").contains(projectTestConfig.projectName).should("be.visible"); - cy.get("#jp-git-sessions") - .get(`button[title="Push committed changes (ahead by 1 commits)"]`) - .should("exist") - .click(); - cy.get("#jp-git-sessions") - .get(`button[title="Push committed changes"]`, { timeout: TIMEOUTS.long }) - .should("exist"); - cy.get("#jp-git-sessions") - .get(`button[title="Push committed changes (ahead by 1 commits)"]`) - .should("not.exist"); + .type( + `renku run --name ${workflow.name} echo "123" > ${workflow.output}{enter}` + ); + cy.get("#filebrowser") + .should("be.visible") + .contains(workflow.output) + .should("be.visible"); }); + // Save the changes + cy.getDataCy("save-session-button").should("be.visible").click(); + cy.get(".modal-session") + .contains("1 commit will be pushed") + .should("be.visible"); + cy.getDataCy("save-session-modal-button").should("be.visible").click(); + cy.get(".modal") + .contains("Saving Session", { timeout: TIMEOUTS.long }) + .should("be.visible"); + cy.get(".modal") + .contains("There are no changes", { timeout: TIMEOUTS.long }) + .should("be.visible"); + cy.get(".modal .btn-close").should("be.visible").click(); + // Stop the session - cy.dataCy("stop-session-button").should("exist").click(); - cy.dataCy("stop-session-modal-button").should("exist").click(); - cy.dataCy("stopping-btn").should("exist"); - cy.get(".renku-container", { timeout: TIMEOUTS.long }) - .should("exist") - .contains("No currently running sessions") + cy.stopSession(); + + // Be sure the project have been indexed + cy.getDataCy("go-back-button").click(); + cy.getProjectSection("Settings").click(); + cy.getDataCy("kg-status-section-open").should("exist").click(); + cy.getDataCy("project-settings-knowledge-graph") + .contains("Everything indexed", { timeout: TIMEOUTS.vlong }) .should("exist"); - // Go the the workflows page and check the new workflow appears - cy.dataCy("go-back-button").click(); - cy.dataCy("project-navbar") - .contains("a.nav-link", "Workflows") - .should("be.visible") - .click(); - - cy.dataCy("workflows-browser") + // Go the workflows page and check the new workflow appears + cy.getProjectSection("Workflows").click(); + cy.getDataCy("workflows-browser") .should("be.visible") .children() .should("have.length", 1) .contains(workflow.name) .should("be.visible") .click(); - - cy.dataCy("workflow-details") + cy.getDataCy("workflow-details") .should("be.visible") .contains(`echo 123 > ${workflow.output}`) .should("be.visible"); + + // Go the file page and check the lineage exists + cy.getProjectSection("Files").click(); + cy.get("div.tree-container") + .contains("button", "Lineage") + .should("be.visible") + .click(); + cy.get("#tree-content").contains(workflow.output).should("exist").click(); + cy.get(".graphContainer").contains(workflow.output).should("exist"); + }); + + it("Start a new session as anonymous user.", () => { + // Log out and go to the project again + cy.visit("/"); + cy.logout(); + cy.visitAndLoadProject(projectIdentifier); + + // Check we show the appropriate message + cy.getProjectSection("Sessions").click(); + cy.getProjectPageLink(projectIdentifier, "sessions/new") + .should("be.visible") + .first() + .click(); + cy.get(".alert-info").contains("As an anonymous user").should("be.visible"); + + // Quickstart a session and check it spins up + cy.getDataCy("go-back-button").click(); + cy.quickstartSession(); + + // Stop the session + cy.stopSession(); + }); + + it("Start a new session on a project without permissions.", () => { + // Log out and go to the project again + cy.visitAndLoadProject(projectWithoutPermissions); + + // Check we show the appropriate message + cy.getProjectSection("Sessions").click(); + cy.getProjectPageLink(projectWithoutPermissions, "sessions/new") + .should("be.visible") + .first() + .click(); + cy.get(".alert-info") + .contains("You have limited permissions for this project") + .should("be.visible"); + + // Quickstart a session and check it spins up + cy.getDataCy("go-back-button").click(); + cy.quickstartSession(); + + // Stop the session + cy.stopSession(); }); }); diff --git a/cypress-tests/cypress/e2e/verifyInfrastructure.cy.ts b/cypress-tests/cypress/e2e/verifyInfrastructure.cy.ts index 14b2f38993..ac4d91b89b 100644 --- a/cypress-tests/cypress/e2e/verifyInfrastructure.cy.ts +++ b/cypress-tests/cypress/e2e/verifyInfrastructure.cy.ts @@ -56,7 +56,8 @@ describe("Verify the infrastructure is ready", () => { }); // Core should read the config file of a Renku project - const coreUrl = "/ui-server/api/renku/config.show" + + const coreUrl = + "/ui-server/api/renku/config.show" + "?git_url=https%3A%2F%2Fgitlab.dev.renku.ch%2Frenku-ui-tests%2Frenku-project-v10"; cy.request(coreUrl).then((resp) => { if (resp.status >= 400 || !("result" in resp.body)) @@ -71,7 +72,8 @@ describe("Verify the infrastructure is ready", () => { }); // Graph should return an empty list of entities for a weird search - const graphUrl = "/ui-server/api/kg/entities" + + const graphUrl = + "/ui-server/api/kg/entities" + "?query=nonExistingLongWordThatShouldReturnEmpty"; cy.request(graphUrl).then((resp) => { if (resp.status >= 400 || resp.body.length !== 0) diff --git a/cypress-tests/cypress/support/commands/datasets.ts b/cypress-tests/cypress/support/commands/datasets.ts new file mode 100644 index 0000000000..ff9c518031 --- /dev/null +++ b/cypress-tests/cypress/support/commands/datasets.ts @@ -0,0 +1,48 @@ +import { v4 as uuidv4 } from "uuid"; + +import { ProjectIdentifier } from "./projects"; + +export type DatasetIdentifier = ProjectIdentifier & { + datasetName: string; + datasetSlug: string; +}; + +export type DatasetNames = { + name: string; + slug: string; +}; + +export function generatorDatasetName(name: string): DatasetNames { + if (!name) throw new Error("Project name cannot be empty"); + const datasetName = `cypress ${name.toLowerCase()} ${uuidv4().substring(24)}`; + return { + name: datasetName, + slug: datasetName.replace(/ /g, "-"), + }; +} + +function searchForDataset(name: string, shouldExist = true) { + cy.visit("/search"); + cy.getDataCy("list-card").should("be.visible"); + cy.getDataCy("type-entity-dataset").should("be.visible").check(); + cy.get("input[placeholder='Search...']") + .should("be.visible") + .type(name) + .type("{enter}"); + if (shouldExist) + cy.get("[data-cy='list-card-title']").contains(name).should("be.visible"); + else cy.get(name).should("not.exist"); +} + +export default function registerDatasetsCommands() { + Cypress.Commands.add("searchForDataset", searchForDataset); +} + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Cypress { + interface Chainable { + searchForDataset(name: string, shouldExist?: boolean); + } + } +} diff --git a/cypress-tests/cypress/support/commands/general.ts b/cypress-tests/cypress/support/commands/general.ts index ddb52953fe..5fe0f25abb 100644 --- a/cypress-tests/cypress/support/commands/general.ts +++ b/cypress-tests/cypress/support/commands/general.ts @@ -1,50 +1,40 @@ import { TIMEOUTS } from "../../../config"; -import { ProjectIdentifier } from "./projects"; export const validateLogin = { validate() { // This returns 401 when not properly logged in cy.request("/ui-server/api/user"); - } + }, }; export const getIframe = (selector: string) => { // https://github.com/cypress-io/cypress-example-recipes/blob/master/examples/blogs__iframes/cypress/support/e2e.js cy.log("getIframeBody"); cy.get(selector, { timeout: TIMEOUTS.long }) - .its("0.contentDocument.body").should("not.be.empty"); - return cy.get(selector, { timeout: TIMEOUTS.long }) - .its("0.contentDocument").then(cy.wrap); + .its("0.contentDocument.body") + .should("not.be.empty"); + return cy + .get(selector, { timeout: TIMEOUTS.long }) + .its("0.contentDocument") + .then(cy.wrap); }; -export function searchForProject(props: ProjectIdentifier, shouldExist = true) { - cy.visit("/search"); - cy.get("input[placeholder='Search...']").should("be.visible").type(props.name).type("{enter}"); - if (shouldExist) - cy.get("[data-cy='list-card-title']").contains(props.name).should("be.visible"); - else - cy.get(props.name).should("not.exist"); -} - -function dataCy(value: string, exist: true) { - if (exist) - return cy.get(`[data-cy=${value}]`).should("exist"); +function getDataCy(value: string, exist: true) { + if (exist) return cy.get(`[data-cy=${value}]`).should("exist"); return cy.get(`[data-cy=${value}]`); } export default function registerGeneralCommands() { Cypress.Commands.add("getIframe", getIframe); - Cypress.Commands.add("searchForProject", searchForProject); - Cypress.Commands.add("dataCy", dataCy); + Cypress.Commands.add("getDataCy", getDataCy); } declare global { // eslint-disable-next-line @typescript-eslint/no-namespace namespace Cypress { interface Chainable { - dataCy(value: string, exist?: boolean); + getDataCy(value: string, exist?: boolean); getIframe(selector: string): Chainable; - searchForProject(props: ProjectIdentifier, shouldExist?: boolean); } } } diff --git a/cypress-tests/cypress/support/commands/login.ts b/cypress-tests/cypress/support/commands/login.ts index 990eb8cc3f..767b5a4574 100644 --- a/cypress-tests/cypress/support/commands/login.ts +++ b/cypress-tests/cypress/support/commands/login.ts @@ -1,4 +1,4 @@ -const renkuLogin = (credentials: { username: string, password: string }[]) => { +const renkuLogin = (credentials: { username: string; password: string }[]) => { for (const { username, password } of credentials) { cy.get("#username").type(username); cy.get("#password").type(password, { log: false }); @@ -6,38 +6,71 @@ const renkuLogin = (credentials: { username: string, password: string }[]) => { } cy.url().then((url) => { const parsedUrl = new URL(url); - if (parsedUrl.pathname.includes("gitlab") || parsedUrl.host.includes("gitlab")) - cy.get(".doorkeeper-authorize >>>> .btn-danger").should("be.visible").should("be.enabled").click(); - + if ( + parsedUrl.pathname.includes("gitlab") || + parsedUrl.host.includes("gitlab") + ) { + cy.get(".doorkeeper-authorize >>>> .btn-danger") + .should("be.visible") + .should("be.enabled") + .click(); + } }); }; -const register = (email: string, password: string, firstName?: string, lastName?: string) => { +const register = ( + email: string, + password: string, + firstName?: string, + lastName?: string +) => { cy.visit("/login"); // ? wait to be assess whether tokens were refreshed automatically or we really need to register cy.wait(1000); // eslint-disable-line cypress/no-unnecessary-waiting - cy.request({ failOnStatusCode: false, url: "ui-server/api/user" }).then((resp) => { - if (resp.status === 200) - return; + cy.request({ failOnStatusCode: false, url: "ui-server/api/user" }).then( + (resp) => { + if (resp.status === 200) return; - cy.get("div#kc-registration").find("a").should("be.visible").click(); - cy.get(`input[name="firstName"]`).should("be.visible").click().clear() - .type(firstName ? firstName : "Renku Cypress"); - cy.get(`input[name="lastName"]`).should("be.visible").click().clear().type(lastName ? lastName : "Test"); - cy.get(`input[name="email"]`).should("be.visible").click().clear().type(email); - cy.get(`input[name="password"]`).should("be.visible").click().clear().type(password, { log: false }); - cy.get(`input[name="password-confirm"]`).should("be.visible").click().clear().type(password, { log: false }); - cy.get(`input[type="submit"]`).should("be.visible").should("be.enabled").click(); - }); + cy.get("div#kc-registration").find("a").should("be.visible").click(); + cy.get(`input[name="firstName"]`) + .should("be.visible") + .click() + .clear() + .type(firstName ? firstName : "Renku Cypress"); + cy.get(`input[name="lastName"]`) + .should("be.visible") + .click() + .clear() + .type(lastName ? lastName : "Test"); + cy.get(`input[name="email"]`) + .should("be.visible") + .click() + .clear() + .type(email); + cy.get(`input[name="password"]`) + .should("be.visible") + .click() + .clear() + .type(password, { log: false }); + cy.get(`input[name="password-confirm"]`) + .should("be.visible") + .click() + .clear() + .type(password, { log: false }); + cy.get(`input[type="submit"]`) + .should("be.visible") + .should("be.enabled") + .click(); + } + ); }; - type RegisterAndVerifyProps = { - email: string, - password: string, - firstName?: string, - lastName?: string + email: string; + password: string; + firstName?: string; + lastName?: string; }; function registerAndVerify(props: RegisterAndVerifyProps) { @@ -76,34 +109,34 @@ function registerAndVerify(props: RegisterAndVerifyProps) { } type RobustLoginProps = { - email: string, - password: string, - firstName?: string, - lastName?: string + email: string; + password: string; + firstName?: string; + lastName?: string; }; function robustLogin(props?: RobustLoginProps) { - // Check if we are already logged in - cy.request({ failOnStatusCode: false, url: "ui-server/api/user" }).then((resp) => { - // we are already logged in - if (resp.status === 200) return; + cy.request({ failOnStatusCode: false, url: "ui-server/api/user" }).then( + (resp) => { + // we are already logged in + if (resp.status >= 200 && resp.status < 400) return; - const localProps = { - email: Cypress.env("TEST_EMAIL"), - password: Cypress.env("TEST_PASSWORD"), - firstName: Cypress.env("TEST_FIRST_NAME"), - lastName: Cypress.env("TEST_LAST_NAME"), - // any passed-in props should overwrite, so spread props last - ...props, - }; + const localProps = { + email: Cypress.env("TEST_EMAIL"), + password: Cypress.env("TEST_PASSWORD"), + firstName: Cypress.env("TEST_FIRST_NAME"), + lastName: Cypress.env("TEST_LAST_NAME"), + // any passed-in props should overwrite, so spread props last + ...props, + }; - return registerAndVerify(localProps); - }); + return registerAndVerify(localProps); + } + ); } function logout() { -// cy.visit("/"); cy.get("#profile-dropdown").should("be.visible").click(); cy.get("#logout-link").should("be.visible").click(); } diff --git a/cypress-tests/cypress/support/commands/projects.ts b/cypress-tests/cypress/support/commands/projects.ts index 2cd128a4dc..71041a4843 100644 --- a/cypress-tests/cypress/support/commands/projects.ts +++ b/cypress-tests/cypress/support/commands/projects.ts @@ -4,36 +4,62 @@ import { TIMEOUTS } from "../../../config"; // Helper functions -type ProjectIdentifier = { - name: string - namespace?: string, -} +export type ProjectIdentifier = { + name: string; + namespace?: string; +}; export function generatorProjectName(name: string): string { - if (!name) - throw new Error("Project name cannot be empty"); + if (!name) throw new Error("Project name cannot be empty"); return `cypress-${name.toLowerCase()}-${uuidv4().substring(24)}`; } -function fullProjectIdentifier(identifier: ProjectIdentifier): Required { +export function fullProjectIdentifier( + identifier: ProjectIdentifier +): Required { return { namespace: Cypress.env("TEST_USERNAME"), ...identifier }; } -function projectUrlFromIdentifier(id: ProjectIdentifier) { +export function projectUrlFromIdentifier(id: ProjectIdentifier) { return `/projects/${id.namespace}/${id.name}`; } -function projectPageLinkSelector(identifier: ProjectIdentifier, subpage: string) { +export function projectPageLinkSelector( + identifier: ProjectIdentifier, + subpage: string +) { const subpageUrl = projectSubpageUrl(identifier, subpage); return `a[href='${subpageUrl}']`; } +function getProjectPageLink(identifier: ProjectIdentifier, subpage: string) { + const selector = projectPageLinkSelector(identifier, subpage); + return cy.get(selector).should("exist"); +} + function projectSubpageUrl(identifier: ProjectIdentifier, subpage: string) { - const projectUrl = projectUrlFromIdentifier(fullProjectIdentifier(identifier)); + const projectUrl = projectUrlFromIdentifier( + fullProjectIdentifier(identifier) + ); const subPath = subpage.startsWith("/") ? subpage : `/${subpage}`; return `${projectUrl}${subPath}`; } +function searchForProject(props: ProjectIdentifier, shouldExist = true) { + cy.visit("/search"); + cy.get("input[placeholder='Search...']") + .should("be.visible") + .type(props.name) + .type("{enter}"); + if (shouldExist) { + cy.get("[data-cy='list-card-title']") + .contains(props.name) + .should("be.visible"); + } + else { + cy.get(props.name).should("not.exist"); + } +} interface NewProjectProps extends ProjectIdentifier { templateName?: string; @@ -42,78 +68,153 @@ interface NewProjectProps extends ProjectIdentifier { function createProject(newProjectProps: NewProjectProps) { cy.visit("/projects/new"); - cy.dataCy("field-group-title").should("be.visible").clear().type(newProjectProps.name); - if (newProjectProps.namespace) - cy.get("#namespace-input").should("be.visible").clear().type(newProjectProps.namespace); + cy.getDataCy("field-group-title") + .should("be.visible") + .clear() + .type(newProjectProps.name); + if (newProjectProps.namespace) { + cy.get("#namespace-input") + .should("be.visible") + .clear() + .type(newProjectProps.namespace); + } if (newProjectProps.templateName) cy.contains(newProjectProps.templateName).should("be.visible").click(); - if (newProjectProps.visibility) - cy.dataCy(`visibility-${newProjectProps.visibility}`).should("be.visible").click(); + if (newProjectProps.visibility) { + cy.getDataCy(`visibility-${newProjectProps.visibility}`) + .should("be.visible") + .click(); + } // The button may take some time before it is clickable - cy.get("[data-cy=create-project-button]", { timeout: TIMEOUTS.vlong }).should("be.enabled").click(); - cy.url({ timeout: TIMEOUTS.vlong }).should("contain", newProjectProps.name.toLowerCase()); - cy.get(`[data-cy="header-project"]`, { timeout: TIMEOUTS.vlong }).should("be.visible"); + cy.get("[data-cy=create-project-button]", { timeout: TIMEOUTS.vlong }) + .should("be.enabled") + .click(); + cy.url({ timeout: TIMEOUTS.vlong }).should( + "contain", + newProjectProps.name.toLowerCase() + ); + cy.get(`[data-cy="header-project"]`, { timeout: TIMEOUTS.vlong }).should( + "be.visible" + ); cy.get("ul.nav-pills-underline").should("be.visible"); } -function deleteProject(identifier: ProjectIdentifier) { +function deleteProjectFromAPI(identifier: ProjectIdentifier) { + const id = fullProjectIdentifier(identifier); + cy.request({ + failOnStatusCode: false, + method: "DELETE", + url: `/ui-server/api/kg/projects/${id.namespace}/${id.name}`, + }); +} + +function deleteProject( + identifier: ProjectIdentifier, + navigateToProject = false +) { + if (navigateToProject) cy.visitAndLoadProject(identifier); + const id = fullProjectIdentifier(identifier); cy.request({ failOnStatusCode: false, method: "DELETE", - url: `/ui-server/api/projects/${id.namespace}%2F${id.name}` + url: `/ui-server/api/projects/${id.namespace}%2F${id.name}`, }); + + const slug = identifier.namespace + "/" + identifier.name; + cy.intercept("DELETE", `/ui-server/api/kg/projects/${slug}`).as( + "deleteProject" + ); + + // Delete the project + cy.getProjectSection("Settings").click(); + cy.getDataCy("project-settings-general-delete-project") + .should("be.visible") + .find("button") + .contains("Delete project") + .should("be.visible") + .click(); + cy.contains("Are you absolutely sure?"); + cy.get("input[name=project-settings-general-delete-confirm-box]").type(slug); + cy.get("button") + .contains("Yes, delete this project") + .should("be.visible") + .should("be.enabled") + .click(); + cy.wait("@deleteProject"); + + cy.url().should("not.contain", `/projects/${slug}`); + cy.get(".Toastify").contains(`Project ${slug} deleted`).should("be.visible"); } function forkProject(identifier: ProjectIdentifier, newName: string) { // open the Fork project modal let projectsInvoked = false; - cy.intercept("/ui-server/api/graphql", req => { projectsInvoked = true; }).as("getProjects"); - cy.dataCy("project-overview-content").get("#fork-project").should("be.visible").click(); - if (projectsInvoked) - cy.wait("@getProjects", { timeout: TIMEOUTS.long }); - cy.get("#slug", { timeout: TIMEOUTS.long }).should("be.visible").should("contain.value", identifier.name); + cy.intercept("/ui-server/api/graphql", (req) => { + projectsInvoked = true; + }).as("getProjects"); + cy.getDataCy("project-overview-content") + .get("#fork-project") + .should("be.visible") + .click(); + if (projectsInvoked) cy.wait("@getProjects", { timeout: TIMEOUTS.long }); + cy.get("#slug", { timeout: TIMEOUTS.long }) + .should("be.visible") + .should("contain.value", identifier.name); // fill in the new Title name - cy.dataCy("field-group-title").should("exist").click().clear().type(newName); - cy.get("#slug", { timeout: TIMEOUTS.long }).should("be.visible").should("contain.value", newName); - cy.get(".modal-content").contains("already taken in the selected namespace").should("not.exist"); + cy.getDataCy("field-group-title") + .should("exist") + .click() + .clear() + .type(newName); + cy.get("#slug", { timeout: TIMEOUTS.long }) + .should("be.visible") + .should("contain.value", newName); + cy.get(".modal-content") + .contains("already taken in the selected namespace") + .should("not.exist"); // create the project cy.intercept("/ui-server/api/projects/*/fork").as("forkProject"); - cy.get(".modal-footer button").contains("Fork Project").should("be.visible").click(); + cy.get(".modal-footer button") + .contains("Fork Project") + .should("be.visible") + .click(); cy.get(".modal").contains("Forking the project").should("be.visible"); cy.wait("@forkProject", { timeout: TIMEOUTS.vlong }); // check the new project is ready - cy.intercept("ui-server/api/renku/cache.migrations_check*").as("getMigrationsCheck"); + cy.intercept("ui-server/api/renku/cache.migrations_check*").as( + "getMigrationsCheck" + ); cy.wait("@getMigrationsCheck", { timeout: TIMEOUTS.long }); - cy.dataCy("header-project").contains(newName).should("be.visible"); - cy.dataCy("header-project").contains("forked from").should("be.visible"); - cy.dataCy("header-project").contains(identifier.namespace + "/" + identifier.name).should("be.visible"); -} - -function getProjectPageLink(identifier: ProjectIdentifier, subpage: string) { - const selector = projectPageLinkSelector(identifier, subpage); - return cy.get(selector).should("exist"); + cy.getDataCy("header-project").contains(newName).should("be.visible"); + cy.getDataCy("header-project").contains("forked from").should("be.visible"); + cy.getDataCy("header-project") + .contains(identifier.namespace + "/" + identifier.name) + .should("be.visible"); } -function visitAndLoadProject(identifier: ProjectIdentifier, skipOutdated = false) { +function visitAndLoadProject( + identifier: ProjectIdentifier, + skipOutdated = false +) { // load project and wait for the relevant resources to be loaded cy.intercept("/ui-server/api/user").as("getUser"); cy.intercept("/ui-server/api/renku/*/datasets.list*").as("getDatasets"); let versionInvoked = false; - cy.intercept("/ui-server/api/renku/versions", req => { versionInvoked = true; }).as("getVersion"); + cy.intercept("/ui-server/api/renku/versions", (req) => { + versionInvoked = true; + }).as("getVersion"); cy.visitProject(identifier); cy.wait("@getUser", { timeout: TIMEOUTS.long }); - if (versionInvoked) - cy.wait("@getVersion", { timeout: TIMEOUTS.long }); - if (!skipOutdated) - cy.wait("@getDatasets", { timeout: TIMEOUTS.long }); + if (versionInvoked) cy.wait("@getVersion", { timeout: TIMEOUTS.long }); + if (!skipOutdated) cy.wait("@getDatasets", { timeout: TIMEOUTS.long }); // Other elements are re-rendered at this point; waiting 1 sec helps preventing "unmounted" errors // eslint-disable-next-line cypress/no-unnecessary-waiting @@ -131,30 +232,50 @@ function visitProjectPageLink(identifier: ProjectIdentifier, subpage: string) { return cy.visit(url); } +type ProjectSection = + | "Overview" + | "Files" + | "Datasets" + | "Workflows" + | "Sessions" + | "Settings"; +function getProjectSection(section: ProjectSection) { + return cy + .getDataCy("project-navbar", true) + .contains("a.nav-link", section) + .should("be.visible"); +} + export default function registerProjectCommands() { Cypress.Commands.add("createProject", createProject); Cypress.Commands.add("deleteProject", deleteProject); - Cypress.Commands.add("getProjectPageLink", getProjectPageLink); + Cypress.Commands.add("deleteProjectFromAPI", deleteProjectFromAPI); Cypress.Commands.add("forkProject", forkProject); + Cypress.Commands.add("getProjectPageLink", getProjectPageLink); + Cypress.Commands.add("getProjectSection", getProjectSection); + Cypress.Commands.add("searchForProject", searchForProject); Cypress.Commands.add("visitProject", visitProject); Cypress.Commands.add("visitProjectPageLink", visitProjectPageLink); Cypress.Commands.add("visitAndLoadProject", visitAndLoadProject); } -export { fullProjectIdentifier, projectPageLinkSelector, projectUrlFromIdentifier }; -export type { ProjectIdentifier }; - declare global { // eslint-disable-next-line @typescript-eslint/no-namespace namespace Cypress { interface Chainable { createProject(newProjectProps: NewProjectProps); - deleteProject(identifier: ProjectIdentifier); + deleteProject(identifier: ProjectIdentifier, loadProject?: boolean); + deleteProjectFromAPI(identifier: ProjectIdentifier); forkProject(identifier: ProjectIdentifier, newName: string); getProjectPageLink(identifier: ProjectIdentifier, subpage: string); + getProjectSection(section: ProjectSection); + searchForProject(props: ProjectIdentifier, shouldExist?: boolean); visitProject(identifier: ProjectIdentifier); visitProjectPageLink(identifier: ProjectIdentifier, subpage: string); - visitAndLoadProject(identifier: ProjectIdentifier, skipOutdated?: boolean); + visitAndLoadProject( + identifier: ProjectIdentifier, + skipOutdated?: boolean + ); } } } diff --git a/cypress-tests/cypress/support/commands/sessions.ts b/cypress-tests/cypress/support/commands/sessions.ts index 03483ca4fb..fe1ee583c8 100644 --- a/cypress-tests/cypress/support/commands/sessions.ts +++ b/cypress-tests/cypress/support/commands/sessions.ts @@ -1,4 +1,3 @@ - import { TIMEOUTS } from "../../../config"; import { fullProjectIdentifier } from "./projects"; import type { ProjectIdentifier } from "./projects"; @@ -7,19 +6,28 @@ function startSession(identifier: ProjectIdentifier) { const id = fullProjectIdentifier(identifier); cy.visit(`/projects/${id.namespace}/${id.name}/sessions/new`); cy.get(".btn-rk-green", { timeout: TIMEOUTS.long }) - .contains("Start session").should("be.visible").should("be.enabled").click(); + .contains("Start session") + .should("be.visible") + .should("be.enabled") + .click(); - cy.contains("Connecting with your session", { timeout: TIMEOUTS.long }).should("be.visible"); - cy.contains("Connecting with your session", { timeout: TIMEOUTS.vlong }).should("not.exist"); + cy.contains("Connecting with your session", { + timeout: TIMEOUTS.long, + }).should("be.visible"); + cy.contains("Connecting with your session", { + timeout: TIMEOUTS.vlong, + }).should("not.exist"); } function waitForImageToBuild(identifier: ProjectIdentifier) { const id = fullProjectIdentifier(identifier); cy.visit(`/projects/${id.namespace}/${id.name}/sessions/new`); - cy.get(".btn-rk-green", { timeout: TIMEOUTS.vlong }).should("be.visible").should("be.enabled"); + cy.get(".btn-rk-green", { timeout: TIMEOUTS.vlong }) + .should("be.visible") + .should("be.enabled"); } -const stopAllSessionsForProject = (identifier: ProjectIdentifier)=> { +const stopAllSessionsForProject = (identifier: ProjectIdentifier) => { const id = fullProjectIdentifier(identifier); cy.intercept("/ui-server/api/notebooks/servers*").as("getSessions"); cy.visit(`/projects/${id.namespace}/${id.name}/sessions`); @@ -40,36 +48,55 @@ const stopAllSessionsForProject = (identifier: ProjectIdentifier)=> { } }); cy.contains("No currently running sessions.", { timeout: TIMEOUTS.long }); - cy.dataCy("go-back-button").click(); + cy.getDataCy("go-back-button").click(); }; -export const stopSessionFromIframe = () => { - cy.intercept({ method: "DELETE", url: /.*\/api\/notebooks\/servers\/.*/, times: 1 }, (req) => { - req.continue((res) => { - expect(res.statusCode).to.eq(204); - }); - }); - cy.get(`[data-cy="stop-session-button"]`).should("be.visible").click(); - cy.get("div.modal-session").should("be.visible").should("not.be.empty"); - cy.get(`[data-cy="stop-session-modal-button"]`).should("be.visible").click(); - cy.contains("Stopping...", { timeout: TIMEOUTS.long }).should("be.visible"); +export const stopSession = () => { + // Stop the session + cy.getDataCy("stop-session-button").should("exist").click(); + cy.getDataCy("stop-session-modal-button").should("exist").click(); + cy.get(".renku-container", { timeout: TIMEOUTS.long }).contains( + "No currently running sessions.", + { timeout: TIMEOUTS.long } + ); }; +function quickstartSession() { + cy.get(".start-session-button").should("not.be.disabled").click(); + cy.get(".progress-box .progress-title").should("exist"); + cy.get(".progress-box .progress-title") + .contains("Starting Session") + .should("exist"); + cy.get(".progress-box .progress-title", { timeout: TIMEOUTS.vlong }).should( + "not.exist" + ); + cy.getIframe("iframe#session-iframe").within(() => { + cy.get(".jp-Launcher-content", { timeout: TIMEOUTS.long }).should( + "be.visible" + ); + cy.get(".jp-Launcher-section").should("be.visible"); + cy.get('.jp-LauncherCard[title="Start a new terminal session"]').should( + "be.visible" + ); + }); +} + export default function registerSessionCommands() { + Cypress.Commands.add("quickstartSession", quickstartSession); Cypress.Commands.add("startSession", startSession); Cypress.Commands.add("waitForImageToBuild", waitForImageToBuild); - Cypress.Commands.add("stopSessionFromIframe", stopSessionFromIframe); + Cypress.Commands.add("stopSession", stopSession); Cypress.Commands.add("stopAllSessionsForProject", stopAllSessionsForProject); } - declare global { // eslint-disable-next-line @typescript-eslint/no-namespace namespace Cypress { interface Chainable { startSession(identifier: ProjectIdentifier); waitForImageToBuild(identifier: ProjectIdentifier); - stopSessionFromIframe(); + stopSession(); + quickstartSession(); stopAllSessionsForProject: typeof stopAllSessionsForProject; } } diff --git a/cypress-tests/cypress/support/e2e.ts b/cypress-tests/cypress/support/e2e.ts index 4ed448a196..d21e3785b0 100644 --- a/cypress-tests/cypress/support/e2e.ts +++ b/cypress-tests/cypress/support/e2e.ts @@ -1,16 +1,16 @@ -/// +import "cypress-localstorage-commands"; +import registerDatasetsCommands from "./commands/datasets"; import registerGeneralCommands from "./commands/general"; import registerLoginCommands from "./commands/login"; import registerProjectCommands from "./commands/projects"; import registerSessionCommands from "./commands/sessions"; -import "cypress-localstorage-commands"; registerGeneralCommands(); registerLoginCommands(); registerProjectCommands(); registerSessionCommands(); - +registerDatasetsCommands(); Cypress.on("uncaught:exception", (err) => { return false; diff --git a/cypress-tests/cypress/support/index.ts b/cypress-tests/cypress/support/index.ts index 430cb1b5db..b1856346b0 100644 --- a/cypress-tests/cypress/support/index.ts +++ b/cypress-tests/cypress/support/index.ts @@ -1,12 +1,13 @@ -/// +import "cypress-localstorage-commands"; +import registerDatasetsCommands from "./commands/datasets"; import registerGeneralCommands from "./commands/general"; import registerLoginCommands from "./commands/login"; import registerProjectCommands from "./commands/projects"; import registerSessionCommands from "./commands/sessions"; -import "cypress-localstorage-commands"; registerGeneralCommands(); registerLoginCommands(); registerProjectCommands(); registerSessionCommands(); +registerDatasetsCommands();