From 5402d9a25761f73770bc576d931caf4faae22e94 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Tue, 18 Jul 2023 11:37:13 +0100 Subject: [PATCH 01/13] E2E - Re-enable standard user page not found tests - Add standard user to local cluster, give pods visibility, - new commands to add cluster and project roles. also to find a project - Run not found tests as both admin and non-admin --- .../tests/navigation/not-found-page.spec.ts | 28 ++--- cypress/globals.d.ts | 7 +- cypress/support/commands.ts | 101 +++++++++++++++--- shell/types/userPreferences.d.ts | 2 +- 4 files changed, 110 insertions(+), 28 deletions(-) diff --git a/cypress/e2e/tests/navigation/not-found-page.spec.ts b/cypress/e2e/tests/navigation/not-found-page.spec.ts index c859fcfb8cf..15aae7f12ba 100644 --- a/cypress/e2e/tests/navigation/not-found-page.spec.ts +++ b/cypress/e2e/tests/navigation/not-found-page.spec.ts @@ -1,16 +1,16 @@ import NotFoundPagePo from '@/cypress/e2e/po/pages/not-found-page.po'; -import ClusterManagerListPagePo from '~/cypress/e2e/po/pages/cluster-manager/cluster-manager-list.po'; -import { WorkloadsPodsListPagePo } from '~/cypress/e2e/po/pages/explorer/workloads-pods.po'; -import WorkloadListPagePo from '~/cypress/e2e/po/pages/explorer/workloads.po'; -import HomePagePo from '~/cypress/e2e/po/pages/home.po'; -import PagePo from '~/cypress/e2e/po/pages/page.po'; +import ClusterManagerListPagePo from '@/cypress/e2e/po/pages/cluster-manager/cluster-manager-list.po'; +import { WorkloadsPodsListPagePo } from '@/cypress/e2e/po/pages/explorer/workloads-pods.po'; +import WorkloadListPagePo from '@/cypress/e2e/po/pages/explorer/workloads.po'; +import HomePagePo from '@/cypress/e2e/po/pages/home.po'; +import PagePo from '@/cypress/e2e/po/pages/page.po'; -describe('Not found page display', () => { +describe('Not found page display', { tags: ['@adminUser', '@standardUser'] }, () => { beforeEach(() => { cy.login(); }); - it('Will show a 404 if we do not have a valid Product id on the route path', { tags: ['@adminUser', '@standardUser'] }, () => { + it('Will show a 404 if we do not have a valid Product id on the route path', () => { const notFound = new NotFoundPagePo('/c/_/bogus-product-id'); notFound.goTo(); @@ -20,7 +20,7 @@ describe('Not found page display', () => { notFound.errorMessage().contains('Product bogus-product-id not found'); }); - it('Will show a 404 if we do not have a valid Resource type on the route path', { tags: ['@adminUser', '@standardUser'] }, () => { + it('Will show a 404 if we do not have a valid Resource type on the route path', () => { const notFound = new NotFoundPagePo('/c/_/manager/bogus-resource-type'); notFound.goTo(); @@ -30,7 +30,7 @@ describe('Not found page display', () => { notFound.errorMessage().contains('Resource type bogus-resource-type not found'); }); - it('Will show a 404 if we do not have a valid Resource id on the route path', { tags: ['@adminUser', '@standardUser'] }, () => { + it('Will show a 404 if we do not have a valid Resource id on the route path', () => { const notFound = new NotFoundPagePo('/c/_/manager/provisioning.cattle.io.cluster/fleet-default/bogus-resource-id'); notFound.goTo(); @@ -40,7 +40,7 @@ describe('Not found page display', () => { notFound.errorMessage().contains('Resource provisioning.cattle.io.cluster with id fleet-default/bogus-resource-id not found, unable to display resource details'); }); - it('Will show a 404 if we do not have a valid product + resource + resource id', { tags: ['@adminUser', '@standardUser'] }, () => { + it('Will show a 404 if we do not have a valid product + resource + resource id', () => { const notFound = new NotFoundPagePo('/c/_/bogus-product-id/bogus-resource/bogus-resource-id'); notFound.goTo(); @@ -50,7 +50,7 @@ describe('Not found page display', () => { notFound.errorMessage().contains('Product bogus-product-id not found'); }); - it('Will not show a 404 if we have a valid product + resource', { tags: ['@adminUser', '@standardUser'] }, () => { + it('Will not show a 404 if we have a valid product + resource', () => { const clusterManager = new NotFoundPagePo('/c/_/manager/provisioning.cattle.io.cluster'); clusterManager.goTo(); @@ -58,7 +58,7 @@ describe('Not found page display', () => { clusterManager.errorTitle().should('not.exist'); }); - it('Will not show a 404 for a valid type from the Norman API', { tags: ['@adminUser', '@standardUser'] }, () => { + it('Will not show a 404 for a valid type from the Norman API', () => { const cloudCredCreatePage = new NotFoundPagePo('/c/_/manager/cloudCredential/create'); cloudCredCreatePage.goTo(); @@ -66,7 +66,7 @@ describe('Not found page display', () => { cloudCredCreatePage.errorTitle().should('not.exist'); }); - it('Will not show a 404 for a valid type that does not have a real schema', { tags: '@adminUser' }, () => { + it('Will not show a 404 for a valid type that does not have a real schema', () => { const workloadPage = new NotFoundPagePo('/c/local/explorer/workload'); workloadPage.goTo(); @@ -74,7 +74,7 @@ describe('Not found page display', () => { workloadPage.errorTitle().should('not.exist'); }); - it('Will not show a 404 if we have a valid product + resource and we nav to page', { tags: '@adminUser' }, () => { + it('Will not show a 404 if we have a valid product + resource and we nav to page', () => { const page = new PagePo(''); const homePage = new HomePagePo(); const notFoundPage = new NotFoundPagePo(''); diff --git a/cypress/globals.d.ts b/cypress/globals.d.ts index 870b185fc71..805ab88e08b 100644 --- a/cypress/globals.d.ts +++ b/cypress/globals.d.ts @@ -1,4 +1,5 @@ import { Verbs } from '@shell/types/api'; +import { UserPreferences } from '@shell/types/userPreferences'; type Matcher = '$' | '^' | '~' | '*' | ''; @@ -9,9 +10,13 @@ declare namespace Cypress { state(state: any): any; login(username?: string, password?: string, cacheSession?: boolean): Chainable; - byLabel(label: string,): Chainable; + byLabel(label: string): Chainable; + createUser(username: string, role?: string): Chainable; setGlobalRoleBinding(userId: string, role: string): Chainable; + setClusterRoleBinding(clusterId: string, userPrincipalId: string, role: string): Chainable; + setProjectRoleBinding(clusterId: string, userPrincipalId: string, projectName: string, role: string): Chainable; + getProject(clusterId: string, projectName: string): Chainable; /** * Wrapper for cy.get() to simply define the data-testid value that allows you to pass a matcher to find the element. diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index c15c7d00392..73f630b5d3f 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -76,17 +76,24 @@ Cypress.Commands.add('createUser', (username, role?) => { username, password: Cypress.env('password') } - }).then((resp) => { - if (resp.status === 422 && resp.body.message === 'Username is already in use.') { - cy.log('User already exists. Skipping user creation'); - } else { - expect(resp.status).to.eq(201); + }) + .then((resp) => { + if (resp.status === 422 && resp.body.message === 'Username is already in use.') { + cy.log('User already exists. Skipping user creation'); + + return ''; + } else { + expect(resp.status).to.eq(201); - if (role) { - cy.setGlobalRoleBinding(resp.body.id, role); + const userPrincipalId = resp.body.principalIds[0]; + + if (role) { + return cy.setGlobalRoleBinding(userPrincipalId, role) + .then(() => cy.setClusterRoleBinding('local', userPrincipalId, 'cluster-member')) + .then(() => cy.setProjectRoleBinding('local', userPrincipalId, 'Default', 'project-member')); + } } - } - }); + }); }); /** @@ -105,9 +112,79 @@ Cypress.Commands.add('setGlobalRoleBinding', (userId, role) => { globalRoleId: role, userId } - }).then((resp) => { - expect(resp.status).to.eq(201); - }); + }) + .then((resp) => { + expect(resp.status).to.eq(201); + }); +}); + +/** + * Set cluster role binding for user via api request + * + */ +Cypress.Commands.add('setClusterRoleBinding', (clusterId, userPrincipalId, role) => { + return cy.request({ + method: 'POST', + url: `${ Cypress.env('api') }/v3/clusterroletemplatebindings`, + headers: { + 'x-api-csrf': token.value, + Accept: 'application/json' + }, + body: { + type: 'clusterRoleTemplateBinding', + clusterId, + roleTemplateId: role, + userPrincipalId + } + }) + .then((resp) => { + expect(resp.status).to.eq(201); + }); +}); + +/** + * Set project role binding for user via api request + * + */ +Cypress.Commands.add('setProjectRoleBinding', (clusterId, userPrincipalId, projectName, role) => { + return cy.getProject(clusterId, projectName) + .then((project) => cy.request({ + method: 'POST', + url: `${ Cypress.env('api') }/v3/projectroletemplatebindings`, + headers: { + 'x-api-csrf': token.value, + Accept: 'application/json' + }, + body: { + type: 'projectroletemplatebinding', + roleTemplateId: role, + userPrincipalId, + projectId: project.id + } + })) + .then((resp) => { + expect(resp.status).to.eq(201); + }); +}); + +/** + * Get the project with the given name + */ +Cypress.Commands.add('getProject', (clusterId, projectName) => { + return cy.request({ + method: 'GET', + url: `${ Cypress.env('api') }/v3/projects?name=${ projectName }&clusterId=${ clusterId }`, + headers: { + 'x-api-csrf': token.value, + Accept: 'application/json' + }, + }) + .then((resp) => { + expect(resp.status).to.eq(200); + expect(resp.body?.data?.length).to.eq(1); + + return resp.body.data[0]; + }); }); /** diff --git a/shell/types/userPreferences.d.ts b/shell/types/userPreferences.d.ts index a48590268e9..a3c9ee9220a 100644 --- a/shell/types/userPreferences.d.ts +++ b/shell/types/userPreferences.d.ts @@ -1,5 +1,5 @@ // eslint-disable-next-line no-unused-vars -interface UserPreferences { +export interface UserPreferences { 'after-login-route': string, cluster: string, 'group-by': string, From b09f721feac8960894e7dbd3247d1ed9197d9d3f Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Tue, 18 Jul 2023 13:32:13 +0100 Subject: [PATCH 02/13] Add debug to investigate non-failing failing tasks --- .github/workflows/test.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index d220e8332bf..2bcc1730eff 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -56,7 +56,10 @@ jobs: - name: Run standard user tests if: ${{ success() || failure() }} run: | - yarn e2e:prod && yarn docker:local:stop + yarn e2e:prod + echo "Result of e2e $?" + yarn docker:local:stop + echo "Result of docker $?" mkdir -p coverage-artifacts/coverage cp coverage/e2e/coverage-final.json coverage-artifacts/coverage/coverage-e2e.json env: From ff06fbfc61118b81c79d4bb7eac4fd6b55f82c45 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Tue, 18 Jul 2023 16:27:16 +0100 Subject: [PATCH 03/13] fix - manageButton, changlog typos from previous pr - remove debug from previous commit - main bug which meant created user didn't have global user role --- .github/workflows/test.yaml | 2 -- cypress/e2e/po/pages/home.po.ts | 2 +- cypress/e2e/tests/pages/home.spec.ts | 4 ++-- cypress/support/commands.ts | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 2bcc1730eff..edd9f416270 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -57,9 +57,7 @@ jobs: if: ${{ success() || failure() }} run: | yarn e2e:prod - echo "Result of e2e $?" yarn docker:local:stop - echo "Result of docker $?" mkdir -p coverage-artifacts/coverage cp coverage/e2e/coverage-final.json coverage-artifacts/coverage/coverage-e2e.json env: diff --git a/cypress/e2e/po/pages/home.po.ts b/cypress/e2e/po/pages/home.po.ts index d981e95da0a..b1f92b7d81e 100644 --- a/cypress/e2e/po/pages/home.po.ts +++ b/cypress/e2e/po/pages/home.po.ts @@ -57,7 +57,7 @@ export default class HomePagePo extends PagePo { return new HomeClusterListPo('[data-testid="cluster-list-container"]'); } - manangeButton() { + manageButton() { return cy.getId('cluster-management-manage-button'); } diff --git a/cypress/e2e/tests/pages/home.spec.ts b/cypress/e2e/tests/pages/home.spec.ts index 388b61842f0..4b2954a973c 100644 --- a/cypress/e2e/tests/pages/home.spec.ts +++ b/cypress/e2e/tests/pages/home.spec.ts @@ -16,7 +16,7 @@ describe('Home Page', () => { it('Can navigate to What\'s new page', { tags: ['@adminUser', '@standardUser'] }, () => { /** - * Verify changlog banner is hidden after clicking link + * Verify changelog banner is hidden after clicking link * Verify release notes link is valid github page */ const text: string[] = []; @@ -128,7 +128,7 @@ describe('Home Page', () => { const clusterManagerPage = new ClusterManagerListPagePo('_'); const genericCreateClusterPage = new ClusterManagerImportGenericPagePo('_'); - homePage.manangeButton().click(); + homePage.manageButton().click(); clusterManagerPage.waitForPage(); HomePagePo.goToAndWaitForGet(); diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 73f630b5d3f..175779cdb54 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -88,7 +88,7 @@ Cypress.Commands.add('createUser', (username, role?) => { const userPrincipalId = resp.body.principalIds[0]; if (role) { - return cy.setGlobalRoleBinding(userPrincipalId, role) + return cy.setGlobalRoleBinding(resp.body.id, role) .then(() => cy.setClusterRoleBinding('local', userPrincipalId, 'cluster-member')) .then(() => cy.setProjectRoleBinding('local', userPrincipalId, 'Default', 'project-member')); } From cff522846161541deea66b4392eefcf62919d62e Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Tue, 18 Jul 2023 18:13:39 +0100 Subject: [PATCH 04/13] Remove cluster role binding for standard user - this shouldn't be needed to get to the screens we need --- cypress/support/commands.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 175779cdb54..cec58d51c4b 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -89,7 +89,6 @@ Cypress.Commands.add('createUser', (username, role?) => { if (role) { return cy.setGlobalRoleBinding(resp.body.id, role) - .then(() => cy.setClusterRoleBinding('local', userPrincipalId, 'cluster-member')) .then(() => cy.setProjectRoleBinding('local', userPrincipalId, 'Default', 'project-member')); } } From 0080233b6007c4c49fe6578e779c42a202377e30 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Wed, 19 Jul 2023 11:19:07 +0100 Subject: [PATCH 05/13] Improve createUser params --- cypress/e2e/tests/setup/rancher-setup.spec.ts | 10 ++++++- cypress/globals.d.ts | 18 +++++++++++- cypress/support/commands.ts | 28 +++++++++++++++---- 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/cypress/e2e/tests/setup/rancher-setup.spec.ts b/cypress/e2e/tests/setup/rancher-setup.spec.ts index 32efa8415e7..7de5f3ebb66 100644 --- a/cypress/e2e/tests/setup/rancher-setup.spec.ts +++ b/cypress/e2e/tests/setup/rancher-setup.spec.ts @@ -50,6 +50,14 @@ describe('Rancher setup', { tags: '@adminUser' }, () => { cy.login(); // Note: the username argument here should match the TEST_USERNAME env var used when running non-admin tests - cy.createUser('standard_user', 'user'); + cy.createUser({ + username: 'standard_user0', + globalRole: { role: 'user' }, + projectRole: { + clusterId: 'local', + projectName: 'Default', + role: 'project-member', + } + }); }); }); diff --git a/cypress/globals.d.ts b/cypress/globals.d.ts index 805ab88e08b..679e8c370a3 100644 --- a/cypress/globals.d.ts +++ b/cypress/globals.d.ts @@ -3,6 +3,22 @@ import { UserPreferences } from '@shell/types/userPreferences'; type Matcher = '$' | '^' | '~' | '*' | ''; +export type CreateUserParams = { + username: string, + globalRole?: { + role: string, + }, + clusterRole?: { + clusterId: string, + role: string, + }, + projectRole?: { + clusterId: string, + projectName: string, + role: string, + } +} + // eslint-disable-next-line no-unused-vars declare namespace Cypress { interface Chainable { @@ -12,7 +28,7 @@ declare namespace Cypress { login(username?: string, password?: string, cacheSession?: boolean): Chainable; byLabel(label: string): Chainable; - createUser(username: string, role?: string): Chainable; + createUser(params: CreateUserParams): Chainable; setGlobalRoleBinding(userId: string, role: string): Chainable; setClusterRoleBinding(clusterId: string, userPrincipalId: string, role: string): Chainable; setProjectRoleBinding(clusterId: string, userPrincipalId: string, projectName: string, role: string): Chainable; diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index cec58d51c4b..6973d549cc3 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -1,5 +1,6 @@ import { LoginPagePo } from '@/cypress/e2e/po/pages/login-page.po'; import { Matcher } from '@/cypress/support/types'; +import { CreateUserParams } from '@/cypress/globals'; let token: any; @@ -60,7 +61,11 @@ Cypress.Commands.add('login', ( /** * Create user via api request */ -Cypress.Commands.add('createUser', (username, role?) => { +Cypress.Commands.add('createUser', (params: CreateUserParams) => { + const { + username, globalRole, clusterRole, projectRole + } = params; + return cy.request({ method: 'POST', url: `${ Cypress.env('api') }/v3/users`, @@ -87,9 +92,22 @@ Cypress.Commands.add('createUser', (username, role?) => { const userPrincipalId = resp.body.principalIds[0]; - if (role) { - return cy.setGlobalRoleBinding(resp.body.id, role) - .then(() => cy.setProjectRoleBinding('local', userPrincipalId, 'Default', 'project-member')); + if (globalRole) { + return cy.setGlobalRoleBinding(resp.body.id, globalRole.role) + .then(() => { + if (clusterRole) { + const { clusterId, role } = clusterRole; + + return cy.setClusterRoleBinding(clusterId, userPrincipalId, role); + } + }) + .then(() => { + if (projectRole) { + const { clusterId, projectName, role } = projectRole; + + return cy.setProjectRoleBinding(clusterId, userPrincipalId, projectName, role); + } + }); } } }); @@ -147,7 +165,7 @@ Cypress.Commands.add('setClusterRoleBinding', (clusterId, userPrincipalId, role) */ Cypress.Commands.add('setProjectRoleBinding', (clusterId, userPrincipalId, projectName, role) => { return cy.getProject(clusterId, projectName) - .then((project) => cy.request({ + .then((project: any) => cy.request({ method: 'POST', url: `${ Cypress.env('api') }/v3/projectroletemplatebindings`, headers: { From b56dc84994e79997585d06067ab47e91f147bd9a Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Wed, 19 Jul 2023 11:19:40 +0100 Subject: [PATCH 06/13] Ensure we only fetch mgmt nodes in cluster (local) dashboard if we can --- shell/pages/c/_cluster/explorer/index.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/pages/c/_cluster/explorer/index.vue b/shell/pages/c/_cluster/explorer/index.vue index 50783f18391..34f5bceb2b5 100644 --- a/shell/pages/c/_cluster/explorer/index.vue +++ b/shell/pages/c/_cluster/explorer/index.vue @@ -103,7 +103,7 @@ export default { `Determine etcd metrics` ); - if (this.currentCluster.isLocal) { + if (this.currentCluster.isLocal && this.$store.getters['management/schemaFor'](MANAGEMENT.NODE)) { this.$store.dispatch('management/findAll', { type: MANAGEMENT.NODE }); } } From 28c7100baa7da97891dfe7fc7afb7c30649ab6bd Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Wed, 19 Jul 2023 13:42:51 +0100 Subject: [PATCH 07/13] e2e fixes - fix 100% failure for api service page url - improvements to extension test --- cypress/e2e/po/pages/extensions.po.ts | 6 ++-- .../e2e/po/pages/extensions/kubewarden.po.ts | 4 +-- .../pages/explorer/api/api-services.spec.ts | 2 +- cypress/e2e/tests/pages/extensions.spec.ts | 32 +++++++++++-------- .../tests/pages/extensions/kubewarden.spec.ts | 12 +++---- 5 files changed, 30 insertions(+), 26 deletions(-) diff --git a/cypress/e2e/po/pages/extensions.po.ts b/cypress/e2e/po/pages/extensions.po.ts index b922c80e373..28f92314d21 100644 --- a/cypress/e2e/po/pages/extensions.po.ts +++ b/cypress/e2e/po/pages/extensions.po.ts @@ -7,16 +7,16 @@ import NameNsDescriptionPo from '@/cypress/e2e/po/components/name-ns-description import ReposListPagePo from '@/cypress/e2e/po/pages/repositories.po'; import AppClusterRepoEditPo from '@/cypress/e2e/po/edit/catalog.cattle.io.clusterrepo.po'; -export default class ExtensionsPo extends PagePo { +export default class ExtensionsPagePo extends PagePo { static url = '/c/local/uiplugins' static goTo(): Cypress.Chainable { - return super.goTo(ExtensionsPo.url); + return super.goTo(ExtensionsPagePo.url); } extensionTabs: TabbedPo; constructor() { - super(ExtensionsPo.url); + super(ExtensionsPagePo.url); this.extensionTabs = new TabbedPo('[data-testid="extension-tabs"]'); } diff --git a/cypress/e2e/po/pages/extensions/kubewarden.po.ts b/cypress/e2e/po/pages/extensions/kubewarden.po.ts index a1f36f2db18..67bfccb3c0a 100644 --- a/cypress/e2e/po/pages/extensions/kubewarden.po.ts +++ b/cypress/e2e/po/pages/extensions/kubewarden.po.ts @@ -1,5 +1,5 @@ import PagePo from '@/cypress/e2e/po/pages/page.po'; -import ExtensionsPo from '@/cypress/e2e/po/pages/extensions.po'; +import ExtensionsPagePo from '@/cypress/e2e/po/pages/extensions.po'; export default class KubewardenExtensionPo extends PagePo { private static createPath(clusterId: string) { @@ -16,7 +16,7 @@ export default class KubewardenExtensionPo extends PagePo { /** add ui-plugin-charts repository */ addChartsRepoIfNeeded(): void { - const extensionsPo: ExtensionsPo = new ExtensionsPo(); + const extensionsPo: ExtensionsPagePo = new ExtensionsPagePo(); extensionsPo.waitForPage(); diff --git a/cypress/e2e/tests/pages/explorer/api/api-services.spec.ts b/cypress/e2e/tests/pages/explorer/api/api-services.spec.ts index 0be039e1006..77337191d42 100644 --- a/cypress/e2e/tests/pages/explorer/api/api-services.spec.ts +++ b/cypress/e2e/tests/pages/explorer/api/api-services.spec.ts @@ -14,7 +14,7 @@ describe('Cluster Explorer', { tags: ['@adminUser'] }, () => { apiServicesPage.waitForRequests(); }); - it('Should be able to use shift+j to select corre', () => { + it('Should be able to use shift+j to select rows and the count of selected is correct', () => { apiServicesPage.title().should('contain', 'APIServices'); const sortableTable = apiServicesPage.sortableTable(); diff --git a/cypress/e2e/tests/pages/extensions.spec.ts b/cypress/e2e/tests/pages/extensions.spec.ts index e0bf70fa608..111e8e44aa0 100644 --- a/cypress/e2e/tests/pages/extensions.spec.ts +++ b/cypress/e2e/tests/pages/extensions.spec.ts @@ -1,4 +1,4 @@ -import ExtensionsPo from '@/cypress/e2e/po/pages/extensions.po'; +import ExtensionsPagePo from '@/cypress/e2e/po/pages/extensions.po'; import ReposListPagePo from '@/cypress/e2e/po/pages/repositories.po'; const EXTENSION_NAME = 'clock'; @@ -8,8 +8,10 @@ describe('Extensions page', { tags: '@adminUser' }, () => { before(() => { cy.login(); - ExtensionsPo.goTo(); - const extensionsPo = new ExtensionsPo(); + const extensionsPo = new ExtensionsPagePo(); + + extensionsPo.goTo(); + extensionsPo.waitForPage(); // install extensions operator if it's not installed extensionsPo.installExtensionsOperatorIfNeeded(); @@ -20,11 +22,11 @@ describe('Extensions page', { tags: '@adminUser' }, () => { beforeEach(() => { cy.login(); - ExtensionsPo.goTo(); + ExtensionsPagePo.goTo(); }); it('using "Add Rancher Repositories" should add a new repository (Partners repo)', () => { - const extensionsPo = new ExtensionsPo(); + const extensionsPo = new ExtensionsPagePo(); // go to "add rancher repositories" extensionsPo.extensionMenuToggle(); @@ -43,7 +45,7 @@ describe('Extensions page', { tags: '@adminUser' }, () => { }); it('Should disable and enable extension support', () => { - const extensionsPo = new ExtensionsPo(); + const extensionsPo = new ExtensionsPagePo(); // open menu and click on disable extension support extensionsPo.extensionMenuToggle(); @@ -60,7 +62,8 @@ describe('Extensions page', { tags: '@adminUser' }, () => { // wait for operation to finish and refresh... extensionsPo.extensionTabs.checkVisible(); - ExtensionsPo.goTo(); + extensionsPo.goTo(); + extensionsPo.waitForPage(); // let's make sure all went good extensionsPo.extensionTabAvailableClick(); @@ -68,19 +71,20 @@ describe('Extensions page', { tags: '@adminUser' }, () => { }); it('New repos banner should only appear once (after dismiss should NOT appear again)', () => { - const extensionsPo = new ExtensionsPo(); + const extensionsPo = new ExtensionsPagePo(); extensionsPo.self().find('[data-testid="extensions-new-repos-banner-action-btn"]').click(); extensionsPo.self().find('[data-testid="extensions-new-repos-banner"]').should('not.exist'); // let's refresh the page to make sure it doesn't appear again... - ExtensionsPo.goTo(); + extensionsPo.goTo(); + extensionsPo.waitForPage(); extensionsPo.title().should('contain', 'Extensions'); extensionsPo.self().find('[data-testid="extensions-new-repos-banner"]').should('not.exist'); }); it('Should toggle the extension details', () => { - const extensionsPo = new ExtensionsPo(); + const extensionsPo = new ExtensionsPagePo(); extensionsPo.extensionTabAvailableClick(); @@ -108,7 +112,7 @@ describe('Extensions page', { tags: '@adminUser' }, () => { }); it('Should install an extension', () => { - const extensionsPo = new ExtensionsPo(); + const extensionsPo = new ExtensionsPagePo(); extensionsPo.extensionTabAvailableClick(); @@ -132,7 +136,7 @@ describe('Extensions page', { tags: '@adminUser' }, () => { }); it('Should update an extension version', () => { - const extensionsPo = new ExtensionsPo(); + const extensionsPo = new ExtensionsPagePo(); extensionsPo.extensionTabInstalledClick(); @@ -151,7 +155,7 @@ describe('Extensions page', { tags: '@adminUser' }, () => { }); it('Should rollback an extension version', () => { - const extensionsPo = new ExtensionsPo(); + const extensionsPo = new ExtensionsPagePo(); extensionsPo.extensionTabInstalledClick(); @@ -170,7 +174,7 @@ describe('Extensions page', { tags: '@adminUser' }, () => { }); it('Should uninstall an extension', () => { - const extensionsPo = new ExtensionsPo(); + const extensionsPo = new ExtensionsPagePo(); extensionsPo.extensionTabInstalledClick(); diff --git a/cypress/e2e/tests/pages/extensions/kubewarden.spec.ts b/cypress/e2e/tests/pages/extensions/kubewarden.spec.ts index e285f9fec5c..134b6c87371 100644 --- a/cypress/e2e/tests/pages/extensions/kubewarden.spec.ts +++ b/cypress/e2e/tests/pages/extensions/kubewarden.spec.ts @@ -1,4 +1,4 @@ -import ExtensionsPo from '@/cypress/e2e/po/pages/extensions.po'; +import ExtensionsPagePo from '@/cypress/e2e/po/pages/extensions.po'; import { ChartsPage } from '@/cypress/e2e/po/pages/charts.po'; import ReposListPagePo from '@/cypress/e2e/po/pages/repositories.po'; import ProductNavPo from '@/cypress/e2e/po/side-bars/product-side-nav.po'; @@ -10,8 +10,8 @@ describe('Kubewarden Extension', { tags: '@adminUser' }, () => { before(() => { cy.login(); - ExtensionsPo.goTo(); - const extensionsPo = new ExtensionsPo(); + ExtensionsPagePo.goTo(); + const extensionsPo = new ExtensionsPagePo(); const kubewardenPo = new KubewardenExtensionPo(); // install extensions operator if it's not installed @@ -21,11 +21,11 @@ describe('Kubewarden Extension', { tags: '@adminUser' }, () => { beforeEach(() => { cy.login(); - ExtensionsPo.goTo(); + ExtensionsPagePo.goTo(); }); it('Should install Kubewarden extension', () => { - const extensionsPo = new ExtensionsPo(); + const extensionsPo = new ExtensionsPagePo(); extensionsPo.extensionTabAvailableClick(); @@ -83,7 +83,7 @@ describe('Kubewarden Extension', { tags: '@adminUser' }, () => { }); it('Should uninstall Kubewarden', () => { - const extensionsPo = new ExtensionsPo(); + const extensionsPo = new ExtensionsPagePo(); extensionsPo.extensionTabInstalledClick(); From a191ebde2c56cc9f8d5bd38188a4bf2bd15c6040 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Wed, 19 Jul 2023 17:03:45 +0100 Subject: [PATCH 08/13] Fix c&p error, go overboard on user avatar open check --- cypress/e2e/po/side-bars/user-menu.po.ts | 10 +++++++--- cypress/e2e/tests/setup/rancher-setup.spec.ts | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/cypress/e2e/po/side-bars/user-menu.po.ts b/cypress/e2e/po/side-bars/user-menu.po.ts index 9e5519dce2a..ae9b67a9e95 100644 --- a/cypress/e2e/po/side-bars/user-menu.po.ts +++ b/cypress/e2e/po/side-bars/user-menu.po.ts @@ -88,8 +88,12 @@ export default class UserMenuPo extends ComponentPo { * @returns */ clickMenuItem(label: string) { - this.ensureOpen().then(() => { - return this.getMenuItems().contains(label).click(); - }); + this.ensureOpen() + .then(() => this.ensureOpen()) + .then(() => this.ensureOpen()) + .then(() => this.ensureOpen()) + .then(() => { + return this.getMenuItems().contains(label).click(); + }); } } diff --git a/cypress/e2e/tests/setup/rancher-setup.spec.ts b/cypress/e2e/tests/setup/rancher-setup.spec.ts index 7de5f3ebb66..1b85bb1cf4d 100644 --- a/cypress/e2e/tests/setup/rancher-setup.spec.ts +++ b/cypress/e2e/tests/setup/rancher-setup.spec.ts @@ -51,7 +51,7 @@ describe('Rancher setup', { tags: '@adminUser' }, () => { // Note: the username argument here should match the TEST_USERNAME env var used when running non-admin tests cy.createUser({ - username: 'standard_user0', + username: 'standard_user', globalRole: { role: 'user' }, projectRole: { clusterId: 'local', From 18748c4dd19b83516ca0c6a8185055a53a2aade1 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Thu, 20 Jul 2023 14:08:09 +0100 Subject: [PATCH 09/13] Improve user avatar fix - toggle is really open, as multiple clicks on element won't close menu - go open spam crazy --- cypress/e2e/po/side-bars/user-menu.po.ts | 28 ++++++++++++------------ 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/cypress/e2e/po/side-bars/user-menu.po.ts b/cypress/e2e/po/side-bars/user-menu.po.ts index ae9b67a9e95..46df7de3db2 100644 --- a/cypress/e2e/po/side-bars/user-menu.po.ts +++ b/cypress/e2e/po/side-bars/user-menu.po.ts @@ -28,11 +28,15 @@ export default class UserMenuPo extends ComponentPo { } /** - * Toggle user menu - * @returns + * Open the user menu + * + * Multiple clicks because sometimes just one ... isn't enough + * */ - toggle(): Cypress.Chainable { - return this.self().click(); + open(): Cypress.Chainable { + this.self().click(); + this.self().click(); + this.self().click(); } /** @@ -56,12 +60,12 @@ export default class UserMenuPo extends ComponentPo { if ($el.attr('style')?.includes('visibility: hidden')) { cy.log('User Avatar open but hidden, giving it a nudge'); - return this.toggle(); + return this.open(); } } else { cy.log('User Avatar not open, opening'); - return this.toggle(); + return this.open(); } }) .then(() => this.isOpen()); @@ -87,13 +91,9 @@ export default class UserMenuPo extends ComponentPo { * @param label * @returns */ - clickMenuItem(label: string) { - this.ensureOpen() - .then(() => this.ensureOpen()) - .then(() => this.ensureOpen()) - .then(() => this.ensureOpen()) - .then(() => { - return this.getMenuItems().contains(label).click(); - }); + clickMenuItem(label: 'Preferences' | 'Account & API Keys' | 'Log Out') { + this.ensureOpen().then(() => { + return this.getMenuItems().contains(label).click(); + }); } } From a3bb0192dc037622ce98f5bd57517c2dd0ef2ce4 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Thu, 20 Jul 2023 14:20:45 +0100 Subject: [PATCH 10/13] Add logging to debug 422 on successful log in --- cypress/e2e/po/side-bars/user-menu.po.ts | 1 + cypress/e2e/tests/pages/login.spec.ts | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/po/side-bars/user-menu.po.ts b/cypress/e2e/po/side-bars/user-menu.po.ts index 46df7de3db2..2ba3736485d 100644 --- a/cypress/e2e/po/side-bars/user-menu.po.ts +++ b/cypress/e2e/po/side-bars/user-menu.po.ts @@ -37,6 +37,7 @@ export default class UserMenuPo extends ComponentPo { this.self().click(); this.self().click(); this.self().click(); + this.self().click(); } /** diff --git a/cypress/e2e/tests/pages/login.spec.ts b/cypress/e2e/tests/pages/login.spec.ts index 98213cb913c..efcabef6178 100644 --- a/cypress/e2e/tests/pages/login.spec.ts +++ b/cypress/e2e/tests/pages/login.spec.ts @@ -1,5 +1,7 @@ import { LoginPagePo } from '@/cypress/e2e/po/pages/login-page.po'; +const successStatusCode = 200; + describe('Local authentication', { tags: ['@adminUser', '@standardUser'] }, () => { it('Log in with valid credentials', () => { LoginPagePo.goTo(); @@ -8,7 +10,11 @@ describe('Local authentication', { tags: ['@adminUser', '@standardUser'] }, () = cy.login(Cypress.env('username'), Cypress.env('password'), false); cy.wait('@loginReq').then((login) => { - expect(login.response?.statusCode).to.equal(200); + if (login.response?.statusCode !== successStatusCode) { + cy.log('Login incorrectly failed', login.response?.statusCode, login.response?.statusMessage, JSON.stringify(login.response?.body || {})); + } + expect(login.response?.statusCode).to.equal(successStatusCode); + cy.url().should('not.equal', `${ Cypress.config().baseUrl }/auth/login`); }); }); @@ -21,7 +27,11 @@ describe('Local authentication', { tags: ['@adminUser', '@standardUser'] }, () = cy.login(Cypress.env('username'), `${ Cypress.env('password') }abc`, false); cy.wait('@loginReq').then((login) => { - expect(login.response?.statusCode).to.not.equal(200); + if (login.response?.statusCode === successStatusCode) { + cy.log('Login incorrectly succeeded', login.response?.statusCode, login.response?.statusMessage, JSON.stringify(login.response?.body || {})); + } + expect(login.response?.statusCode).to.not.equal(successStatusCode); + // URL is partial as it may change based on the authentication configuration present cy.url().should('include', `${ Cypress.config().baseUrl }/auth/login`); }); From 4e4157dce383a1eb6655db70a09e0b8e3d302e66 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Thu, 20 Jul 2023 17:53:58 +0100 Subject: [PATCH 11/13] Update to flaky `New repos banner should only appear once (after dismiss should NOT appear again)` - this expects the partner extension repo to not be installed - sometimes it is.... so confirm and if exist remove it - also fixes / adds - rowNames for non-cluster lists - list functionaltiy for no rows --- .../e2e/po/components/sortable-table.po.ts | 17 ++++-- cypress/e2e/po/pages/extensions.po.ts | 15 ++++++ .../e2e/tests/pages/cluster-manager.spec.ts | 8 +-- cypress/e2e/tests/pages/extensions.spec.ts | 54 ++++++++++++++++--- 4 files changed, 79 insertions(+), 15 deletions(-) diff --git a/cypress/e2e/po/components/sortable-table.po.ts b/cypress/e2e/po/components/sortable-table.po.ts index 6b6910e38a6..ddceb47ca89 100644 --- a/cypress/e2e/po/components/sortable-table.po.ts +++ b/cypress/e2e/po/components/sortable-table.po.ts @@ -91,10 +91,13 @@ export default class SortableTablePo extends ComponentPo { return new ListRowPo(this.rowElementWithName(name)); } - rowNames() { - return this.rowElements().find('.cluster-link').then(($els: any) => { + /** + * Get rows names. To avoid the 'no rows' on first load use `noRowsShouldNotExist` + */ + rowNames(rowNameSelector = 'td:nth-of-type(3)') { + return this.rowElements().find(rowNameSelector).then(($els: any) => { return ( - Cypress.$.makeArray($els).map((el: any) => el.innerText) + Cypress.$.makeArray($els).map((el: any) => el.innerText as string) ); }); } @@ -103,6 +106,14 @@ export default class SortableTablePo extends ComponentPo { return new ActionMenuPo(); } + noRowsShouldNotExist() { + return this.noRowsText().should('not.exist'); + } + + noRowsText() { + return this.self().find('tbody').find('.no-rows'); + } + /** * Check row element count on sortable table * @param isEmpty true if empty state expected (empty state message should display on row 1) diff --git a/cypress/e2e/po/pages/extensions.po.ts b/cypress/e2e/po/pages/extensions.po.ts index 28f92314d21..dbc864d1b99 100644 --- a/cypress/e2e/po/pages/extensions.po.ts +++ b/cypress/e2e/po/pages/extensions.po.ts @@ -6,6 +6,8 @@ import ActionMenuPo from '@/cypress/e2e/po/components/action-menu.po'; import NameNsDescriptionPo from '@/cypress/e2e/po/components/name-ns-description.po'; import ReposListPagePo from '@/cypress/e2e/po/pages/repositories.po'; import AppClusterRepoEditPo from '@/cypress/e2e/po/edit/catalog.cattle.io.clusterrepo.po'; +import BannersPo from '~/cypress/e2e/po/components/banners.po'; +import ComponentPo from '~/cypress/e2e/po/components/component.po'; export default class ExtensionsPagePo extends PagePo { static url = '/c/local/uiplugins' @@ -28,6 +30,14 @@ export default class ExtensionsPagePo extends PagePo { return this.self().getId('extensions-page-title').invoke('text'); } + waitForTitle() { + return this.title().should('contain', 'Extensions'); + } + + loading() { + return this.self().get('.data-loading'); + } + /** * install extensions operator */ @@ -204,6 +214,11 @@ export default class ExtensionsPagePo extends PagePo { return this.extensionReloadBanner().getId('extension-reload-banner-reload-btn').click(); } + // ------------------ new repos banner ------------------ + repoBanner() { + return new BannersPo('[data-testid="extensions-new-repos-banner"]', this.self()); + } + // ------------------ extension menu ------------------ private extensionMenu() { return this.self().getId('extensions-page-menu'); diff --git a/cypress/e2e/tests/pages/cluster-manager.spec.ts b/cypress/e2e/tests/pages/cluster-manager.spec.ts index 947f701e792..8ed908754f6 100644 --- a/cypress/e2e/tests/pages/cluster-manager.spec.ts +++ b/cypress/e2e/tests/pages/cluster-manager.spec.ts @@ -120,7 +120,7 @@ describe('Cluster Manager', { tags: '@adminUser' }, () => { clusterList.sortableTable().rowElementWithName(rke2CustomName).should('exist', { timeout: 15000 }); clusterList.list().actionMenu(rke2CustomName).getMenuItem('Delete').click(); - clusterList.sortableTable().rowNames().then((rows: any) => { + clusterList.sortableTable().rowNames('.cluster-link').then((rows: any) => { const promptRemove = new PromptRemove(); promptRemove.confirm(rke2CustomName); @@ -128,7 +128,7 @@ describe('Cluster Manager', { tags: '@adminUser' }, () => { clusterList.waitForPage(); clusterList.sortableTable().checkRowCount(false, rows.length - 1); - clusterList.sortableTable().rowNames().should('not.contain', rke2CustomName); + clusterList.sortableTable().rowNames('.cluster-link').should('not.contain', rke2CustomName); }); }); }); @@ -257,7 +257,7 @@ describe('Cluster Manager', { tags: '@adminUser' }, () => { clusterList.sortableTable().bulkActionDropDownOpen(); clusterList.sortableTable().bulkActionDropDownButton('Delete').click(); - clusterList.sortableTable().rowNames().then((rows: any) => { + clusterList.sortableTable().rowNames('.cluster-link').then((rows: any) => { const promptRemove = new PromptRemove(); promptRemove.confirm(importGenericName); @@ -265,7 +265,7 @@ describe('Cluster Manager', { tags: '@adminUser' }, () => { clusterList.waitForPage(); clusterList.sortableTable().checkRowCount(false, rows.length - 1); - clusterList.sortableTable().rowNames().should('not.contain', importGenericName); + clusterList.sortableTable().rowNames('.cluster-link').should('not.contain', importGenericName); }); }); }); diff --git a/cypress/e2e/tests/pages/extensions.spec.ts b/cypress/e2e/tests/pages/extensions.spec.ts index 111e8e44aa0..d1141645cae 100644 --- a/cypress/e2e/tests/pages/extensions.spec.ts +++ b/cypress/e2e/tests/pages/extensions.spec.ts @@ -1,8 +1,10 @@ import ExtensionsPagePo from '@/cypress/e2e/po/pages/extensions.po'; import ReposListPagePo from '@/cypress/e2e/po/pages/repositories.po'; +import PromptRemove from '~/cypress/e2e/po/prompts/promptRemove.po'; const EXTENSION_NAME = 'clock'; -const PARTNERS_REPO_URL = 'https://github.com/rancher/partner-extensions'; +const UI_PLUGINS_PARTNERS_REPO_URL = 'https://github.com/rancher/partner-extensions'; +const UI_PLUGINS_PARTNERS_REPO_NAME = 'partner-extensions'; describe('Extensions page', { tags: '@adminUser' }, () => { before(() => { @@ -22,12 +24,13 @@ describe('Extensions page', { tags: '@adminUser' }, () => { beforeEach(() => { cy.login(); - ExtensionsPagePo.goTo(); }); it('using "Add Rancher Repositories" should add a new repository (Partners repo)', () => { const extensionsPo = new ExtensionsPagePo(); + extensionsPo.goTo(); + // go to "add rancher repositories" extensionsPo.extensionMenuToggle(); extensionsPo.addRepositoriesClick(); @@ -41,12 +44,14 @@ describe('Extensions page', { tags: '@adminUser' }, () => { appRepoList.goTo(); appRepoList.waitForPage(); - appRepoList.sortableTable().rowElementWithName(PARTNERS_REPO_URL).should('exist'); + appRepoList.sortableTable().rowElementWithName(UI_PLUGINS_PARTNERS_REPO_URL).should('exist'); }); it('Should disable and enable extension support', () => { const extensionsPo = new ExtensionsPagePo(); + extensionsPo.goTo(); + // open menu and click on disable extension support extensionsPo.extensionMenuToggle(); extensionsPo.disableExtensionsClick(); @@ -71,25 +76,50 @@ describe('Extensions page', { tags: '@adminUser' }, () => { }); it('New repos banner should only appear once (after dismiss should NOT appear again)', () => { + const appRepoList = new ReposListPagePo('local', 'apps'); + + // Ensure that the banner should be shown (by confirming that a required repo isn't there) + appRepoList.goTo(); + appRepoList.waitForPage(); + appRepoList.sortableTable().noRowsShouldNotExist(); + appRepoList.sortableTable().rowNames().then((names) => { + if (names.includes(UI_PLUGINS_PARTNERS_REPO_NAME)) { + appRepoList.list().actionMenu(UI_PLUGINS_PARTNERS_REPO_NAME).getMenuItem('Delete').click(); + const promptRemove = new PromptRemove(); + + return promptRemove.remove(); + } + }); + + // Now go to extensions (by nav, not page load....) + appRepoList.navToMenuEntry('Extensions'); + const extensionsPo = new ExtensionsPagePo(); - extensionsPo.self().find('[data-testid="extensions-new-repos-banner-action-btn"]').click(); - extensionsPo.self().find('[data-testid="extensions-new-repos-banner"]').should('not.exist'); + extensionsPo.waitForPage(); + extensionsPo.loading().should('not.exist'); + + extensionsPo.repoBanner().checkVisible(); + extensionsPo.repoBanner().self().find('[data-testid="extensions-new-repos-banner-action-btn"]').click(); + extensionsPo.repoBanner().checkNotExists(); // let's refresh the page to make sure it doesn't appear again... extensionsPo.goTo(); extensionsPo.waitForPage(); - extensionsPo.title().should('contain', 'Extensions'); - extensionsPo.self().find('[data-testid="extensions-new-repos-banner"]').should('not.exist'); + extensionsPo.waitForTitle(); + extensionsPo.loading().should('not.exist'); + extensionsPo.repoBanner().checkNotExists(); }); it('Should toggle the extension details', () => { const extensionsPo = new ExtensionsPagePo(); + extensionsPo.goTo(); + extensionsPo.extensionTabAvailableClick(); // we should be on the extensions page - extensionsPo.title().should('contain', 'Extensions'); + extensionsPo.waitForTitle(); // show extension details extensionsPo.extensionCardClick(EXTENSION_NAME); @@ -114,6 +144,8 @@ describe('Extensions page', { tags: '@adminUser' }, () => { it('Should install an extension', () => { const extensionsPo = new ExtensionsPagePo(); + extensionsPo.goTo(); + extensionsPo.extensionTabAvailableClick(); // click on install button on card @@ -138,6 +170,8 @@ describe('Extensions page', { tags: '@adminUser' }, () => { it('Should update an extension version', () => { const extensionsPo = new ExtensionsPagePo(); + extensionsPo.goTo(); + extensionsPo.extensionTabInstalledClick(); // click on update button on card @@ -157,6 +191,8 @@ describe('Extensions page', { tags: '@adminUser' }, () => { it('Should rollback an extension version', () => { const extensionsPo = new ExtensionsPagePo(); + extensionsPo.goTo(); + extensionsPo.extensionTabInstalledClick(); // click on the rollback button on card @@ -176,6 +212,8 @@ describe('Extensions page', { tags: '@adminUser' }, () => { it('Should uninstall an extension', () => { const extensionsPo = new ExtensionsPagePo(); + extensionsPo.goTo(); + extensionsPo.extensionTabInstalledClick(); // click on uninstall button on card From 3eb29b709334577abc1f59362f266aab41165f9d Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Thu, 20 Jul 2023 18:19:33 +0100 Subject: [PATCH 12/13] Fix lint --- cypress/e2e/po/pages/extensions.po.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/cypress/e2e/po/pages/extensions.po.ts b/cypress/e2e/po/pages/extensions.po.ts index dbc864d1b99..90eaf716709 100644 --- a/cypress/e2e/po/pages/extensions.po.ts +++ b/cypress/e2e/po/pages/extensions.po.ts @@ -7,7 +7,6 @@ import NameNsDescriptionPo from '@/cypress/e2e/po/components/name-ns-description import ReposListPagePo from '@/cypress/e2e/po/pages/repositories.po'; import AppClusterRepoEditPo from '@/cypress/e2e/po/edit/catalog.cattle.io.clusterrepo.po'; import BannersPo from '~/cypress/e2e/po/components/banners.po'; -import ComponentPo from '~/cypress/e2e/po/components/component.po'; export default class ExtensionsPagePo extends PagePo { static url = '/c/local/uiplugins' From ff065e508e2bcf34229f33b8623c8e443034805e Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Fri, 21 Jul 2023 11:15:03 +0100 Subject: [PATCH 13/13] Beef up flaky extension banner test - banner should be visibile when test starts - banner is visible if v3 setting is not 'true' and there is a missing helm repo - previously confirmed the repo is missing, now confirm the v3 setting is correct - split rancher api commands out from base commands, added get/set commands --- cypress/e2e/tests/pages/extensions.spec.ts | 16 ++++ cypress/globals.d.ts | 5 +- cypress/support/commands/commands.ts | 52 ++++++++++++ .../rancher-api-commands.ts} | 84 ++++++++----------- cypress/support/e2e.ts | 3 +- 5 files changed, 111 insertions(+), 49 deletions(-) create mode 100644 cypress/support/commands/commands.ts rename cypress/support/{commands.ts => commands/rancher-api-commands.ts} (79%) diff --git a/cypress/e2e/tests/pages/extensions.spec.ts b/cypress/e2e/tests/pages/extensions.spec.ts index d1141645cae..c1d07045c29 100644 --- a/cypress/e2e/tests/pages/extensions.spec.ts +++ b/cypress/e2e/tests/pages/extensions.spec.ts @@ -76,6 +76,22 @@ describe('Extensions page', { tags: '@adminUser' }, () => { }); it('New repos banner should only appear once (after dismiss should NOT appear again)', () => { + cy.getRancherResource('v3', 'setting', 'display-add-extension-repos-banner', null).then((resp: Cypress.Response) => { + const notFound = resp.status === 404; + const requiredValue = resp.body?.value === 'true'; + + if (notFound || requiredValue) { + cy.log('Good test state', '/v3/setting/display-add-extension-repos-banner', resp.status, JSON.stringify(resp?.body || {})); + } else { + cy.log('Bad test state', '/v3/setting/display-add-extension-repos-banner', resp.status, JSON.stringify(resp?.body || {})); + + return cy.setRancherResource('v3', 'setting', 'display-add-extension-repos-banner', { + ...resp.body, + value: 'true' + }); + } + }); + const appRepoList = new ReposListPagePo('local', 'apps'); // Ensure that the banner should be shown (by confirming that a required repo isn't there) diff --git a/cypress/globals.d.ts b/cypress/globals.d.ts index 679e8c370a3..9cdd8c2d069 100644 --- a/cypress/globals.d.ts +++ b/cypress/globals.d.ts @@ -32,7 +32,10 @@ declare namespace Cypress { setGlobalRoleBinding(userId: string, role: string): Chainable; setClusterRoleBinding(clusterId: string, userPrincipalId: string, role: string): Chainable; setProjectRoleBinding(clusterId: string, userPrincipalId: string, projectName: string, role: string): Chainable; - getProject(clusterId: string, projectName: string): Chainable; + getProjectByName(clusterId: string, projectName: string): Chainable; + + getRancherResource(prefix: 'v3' | 'v1', resourceType: string, resourceId: string, expectedStatusCode: string): Chainable; + setRancherResource(prefix: 'v3' | 'v1', resourceType: string, resourceId: string, body: string): Chainable; /** * Wrapper for cy.get() to simply define the data-testid value that allows you to pass a matcher to find the element. diff --git a/cypress/support/commands/commands.ts b/cypress/support/commands/commands.ts new file mode 100644 index 00000000000..23884eae0dd --- /dev/null +++ b/cypress/support/commands/commands.ts @@ -0,0 +1,52 @@ +import { Matcher } from '@/cypress/support/types'; + +/** + * Get input field for given label + */ +Cypress.Commands.add('byLabel', (label) => { + return cy.get('.labeled-input').contains(label).siblings('input'); +}); + +/** + * Wrap the cy.find() command to simplify the selector declaration of the data-testid + */ +Cypress.Commands.add('findId', (id: string, matcher?: Matcher = '') => { + return cy.find(`[data-testid${ matcher }="${ id }"]`); +}); + +/** + * Wrap the cy.get() command to simplify the selector declaration of the data-testid + */ +Cypress.Commands.add('getId', (id: string, matcher?: Matcher = '') => { + return cy.get(`[data-testid${ matcher }="${ id }"]`); +}); + +Cypress.Commands.add('keyboardControls', (triggerKeys: any = {}, count = 1) => { + for (let i = 0; i < count; i++) { + cy.get('body').trigger('keydown', triggerKeys); + } +}); + +/** + * Intercept all requests and return + * @param {array} intercepts - Array of intercepts to return + * return {array} - Array of intercepted request strings + * return {string} - Intercepted request string + */ +Cypress.Commands.add('interceptAllRequests', (method = '/GET/POST/PUT/PATCH/', urls = ['/v1/*']) => { + const interceptedUrls: string[] = urls.map((cUrl, i) => { + cy.intercept(method, cUrl).as(`interceptAllRequests${ i }`); + + return `@interceptAllRequests${ i }`; + }); + + return cy.wrap(interceptedUrls); +}); + +Cypress.Commands.add('iFrame', () => { + return cy + .get('[data-testid="ember-iframe"]', { log: false }) + .its('0.contentDocument.body', { log: false }) + .should('not.be.empty') + .then((body) => cy.wrap(body)); +}); diff --git a/cypress/support/commands.ts b/cypress/support/commands/rancher-api-commands.ts similarity index 79% rename from cypress/support/commands.ts rename to cypress/support/commands/rancher-api-commands.ts index 6973d549cc3..65e788459bd 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands/rancher-api-commands.ts @@ -1,7 +1,9 @@ import { LoginPagePo } from '@/cypress/e2e/po/pages/login-page.po'; -import { Matcher } from '@/cypress/support/types'; import { CreateUserParams } from '@/cypress/globals'; +// This file contains commands which makes API requests to the rancher API. +// It includes the `login` command to store the `token` to use + let token: any; /** @@ -164,7 +166,7 @@ Cypress.Commands.add('setClusterRoleBinding', (clusterId, userPrincipalId, role) * */ Cypress.Commands.add('setProjectRoleBinding', (clusterId, userPrincipalId, projectName, role) => { - return cy.getProject(clusterId, projectName) + return cy.getProjectByName(clusterId, projectName) .then((project: any) => cy.request({ method: 'POST', url: `${ Cypress.env('api') }/v3/projectroletemplatebindings`, @@ -187,7 +189,7 @@ Cypress.Commands.add('setProjectRoleBinding', (clusterId, userPrincipalId, proje /** * Get the project with the given name */ -Cypress.Commands.add('getProject', (clusterId, projectName) => { +Cypress.Commands.add('getProjectByName', (clusterId, projectName) => { return cy.request({ method: 'GET', url: `${ Cypress.env('api') }/v3/projects?name=${ projectName }&clusterId=${ clusterId }`, @@ -204,27 +206,6 @@ Cypress.Commands.add('getProject', (clusterId, projectName) => { }); }); -/** - * Get input field for given label - */ -Cypress.Commands.add('byLabel', (label) => { - return cy.get('.labeled-input').contains(label).siblings('input'); -}); - -/** - * Wrap the cy.find() command to simplify the selector declaration of the data-testid - */ -Cypress.Commands.add('findId', (id: string, matcher?: Matcher = '') => { - return cy.find(`[data-testid${ matcher }="${ id }"]`); -}); - -/** - * Wrap the cy.get() command to simplify the selector declaration of the data-testid - */ -Cypress.Commands.add('getId', (id: string, matcher?: Matcher = '') => { - return cy.get(`[data-testid${ matcher }="${ id }"]`); -}); - /** * Override user preferences to default values, allowing to pass custom preferences for a deterministic scenario */ @@ -271,32 +252,41 @@ Cypress.Commands.add('requestBase64Image', (url: string) => { }); }); -Cypress.Commands.add('keyboardControls', (triggerKeys: any = {}, count = 1) => { - for (let i = 0; i < count; i++) { - cy.get('body').trigger('keydown', triggerKeys); - } -}); - /** - * Intercept all requests and return - * @param {array} intercepts - Array of intercepts to return - * return {array} - Array of intercepted request strings - * return {string} - Intercepted request string + * Get a v3 / v1 resource */ -Cypress.Commands.add('interceptAllRequests', (method = '/GET/POST/PUT/PATCH/', urls = ['/v1/*']) => { - const interceptedUrls: string[] = urls.map((cUrl, i) => { - cy.intercept(method, cUrl).as(`interceptAllRequests${ i }`); - - return `@interceptAllRequests${ i }`; - }); +Cypress.Commands.add('getRancherResource', (prefix, resourceType, resourceId, expectedStatusCode = 200) => { + return cy.request({ + method: 'GET', + url: `${ Cypress.env('api') }/${ prefix }/${ resourceType }/${ resourceId }`, + headers: { + 'x-api-csrf': token.value, + Accept: 'application/json' + }, + }) + .then((resp) => { + if (expectedStatusCode) { + expect(resp.status).to.eq(expectedStatusCode); + } - return cy.wrap(interceptedUrls); + return resp; + }); }); -Cypress.Commands.add('iFrame', () => { - return cy - .get('[data-testid="ember-iframe"]', { log: false }) - .its('0.contentDocument.body', { log: false }) - .should('not.be.empty') - .then((body) => cy.wrap(body)); +/** + * set a v3 / v1 resource + */ +Cypress.Commands.add('setRancherResource', (prefix, resourceType, resourceId, body) => { + return cy.request({ + method: 'PUT', + url: `${ Cypress.env('api') }/${ prefix }/${ resourceType }/${ resourceId }`, + headers: { + 'x-api-csrf': token.value, + Accept: 'application/json' + }, + body + }) + .then((resp) => { + expect(resp.status).to.eq(200); + }); }); diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index bef88307252..71a94e659f4 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -1,5 +1,6 @@ import '@cypress/code-coverage/support'; -import './commands'; +import './commands/commands'; +import './commands/rancher-api-commands.ts'; import registerCypressGrep from '@cypress/grep/src/support'; registerCypressGrep();