From 336906d165a033130d9e791c3408a5ddf34fd5ee Mon Sep 17 00:00:00 2001 From: Yonas Berhe Date: Fri, 14 Jun 2024 16:49:59 -0700 Subject: [PATCH] vai pagination tests - pods --- .../explorer/workloads/pods/pods-get.ts | 233 +++++++++++++++ cypress/e2e/po/components/header-row.po.ts | 27 ++ .../e2e/po/components/sortable-table.po.ts | 8 +- .../dashboard/cluster-dashboard.spec.ts | 53 ++-- .../pages/explorer/namespace-picker.spec.ts | 16 +- .../pages/explorer/workloads/pods.spec.ts | 274 ++++++++++++++---- .../tests/pages/fleet/fleet-clusters.spec.ts | 4 +- cypress/e2e/tests/pages/fleet/gitrepo.spec.ts | 4 +- cypress/globals.d.ts | 13 +- .../support/commands/rancher-api-commands.ts | 46 ++- 10 files changed, 575 insertions(+), 103 deletions(-) create mode 100644 cypress/e2e/blueprints/explorer/workloads/pods/pods-get.ts create mode 100644 cypress/e2e/po/components/header-row.po.ts diff --git a/cypress/e2e/blueprints/explorer/workloads/pods/pods-get.ts b/cypress/e2e/blueprints/explorer/workloads/pods/pods-get.ts new file mode 100644 index 00000000000..8329ffa8429 --- /dev/null +++ b/cypress/e2e/blueprints/explorer/workloads/pods/pods-get.ts @@ -0,0 +1,233 @@ +// GET /v1/pods - small set of pods data +const podsGetResponseSmallSet = { + type: 'collection', + links: { self: 'https://yonasb29.qa.rancher.space/v1/pods' }, + createTypes: { pod: 'https://yonasb29.qa.rancher.space/v1/pods' }, + actions: {}, + resourceType: 'pod', + revision: '123', + count: 3, + data: [ + { + id: 'cattle-fleet-local-system/fleet-agent-0', + type: 'pod', + links: { + remove: 'https://yonasb29.qa.rancher.space/v1/pods/cattle-fleet-local-system/fleet-agent-0', + self: 'https://yonasb29.qa.rancher.space/v1/pods/cattle-fleet-local-system/fleet-agent-0', + update: 'https://yonasb29.qa.rancher.space/v1/pods/cattle-fleet-local-system/fleet-agent-0', + view: 'https://yonasb29.qa.rancher.space/api/v1/namespaces/cattle-fleet-local-system/pods/fleet-agent-0' + }, + apiVersion: 'v1', + kind: 'Pod', + metadata: { + annotations: {}, + creationTimestamp: '2024-06-14T15:50:07Z', + fields: [ + 'fleet-agent-0', + '2/2', + 'Running', + '0', + '7h29m', + '10.42.2.15', + 'ip-172-31-12-33.us-east-2.compute.internal', + '', + '' + ], + generateName: 'fleet-agent-', + labels: {}, + name: 'aaa-e2e-vai-test-pod-name', + namespace: 'cattle-fleet-local-system', + ownerReferences: [], + relationships: [], + resourceVersion: '7553', + state: { + error: false, + message: '', + name: 'running', + transitioning: false + }, + uid: '76de6052-8eda-42bc-98d5-dd19c335e977' + }, + spec: { + affinity: { + nodeAffinity: { + preferredDuringSchedulingIgnoredDuringExecution: [ + { + preference: { + matchExpressions: [ + { + key: 'fleet.cattle.io/agent', + operator: 'In', + values: [ + 'true' + ] + } + ] + }, + weight: 1 + } + ] + } + }, + containers: [], + dnsPolicy: 'ClusterFirst', + enableServiceLinks: true, + hostname: 'fleet-agent-0', + initContainers: [], + nodeName: 'ip-172-31-12-33.us-east-2.compute.internal', + nodeSelector: { 'kubernetes.io/os': 'linux' }, + preemptionPolicy: 'PreemptLowerPriority', + priority: 0, + restartPolicy: 'Always', + schedulerName: 'default-scheduler', + securityContext: { + runAsGroup: 1000, + runAsNonRoot: true, + runAsUser: 1000 + }, + serviceAccount: 'fleet-agent', + serviceAccountName: 'fleet-agent', + subdomain: 'fleet-agent', + terminationGracePeriodSeconds: 30, + tolerations: [], + volumes: [] + }, + status: {} + }, + { + id: 'kube-system/rke2-snapshot-controller-59cc9cd8f4-dr2nv', + type: 'pod', + links: { + remove: 'https://yonasb29.qa.rancher.space/v1/pods/kube-system/rke2-snapshot-controller-59cc9cd8f4-dr2nv', + self: 'https://yonasb29.qa.rancher.space/v1/pods/kube-system/rke2-snapshot-controller-59cc9cd8f4-dr2nv', + update: 'https://yonasb29.qa.rancher.space/v1/pods/kube-system/rke2-snapshot-controller-59cc9cd8f4-dr2nv', + view: 'https://yonasb29.qa.rancher.space/api/v1/namespaces/kube-system/pods/rke2-snapshot-controller-59cc9cd8f4-dr2nv' + }, + apiVersion: 'v1', + kind: 'Pod', + metadata: { + annotations: { + 'cni.projectcalico.org/containerID': '212cd9f120c58c007d065f7702a2f3f6c7f2f8e8850af7ed14f56ef577c61615', + 'cni.projectcalico.org/podIP': '10.42.0.11/32', + 'cni.projectcalico.org/podIPs': '10.42.0.11/32' + }, + creationTimestamp: '2024-06-14T15:39:59Z', + fields: [ + 'rke2-snapshot-controller-59cc9cd8f4-dr2nv', + '1/1', + 'Running', + '0', + '7h39m', + '10.42.0.11', + 'ip-172-31-14-130.us-east-2.compute.internal', + '', + '' + ], + generateName: 'rke2-snapshot-controller-59cc9cd8f4-', + labels: {}, + name: 'rke2-snapshot-controller-59cc9cd8f4-dr2nv', + namespace: 'kube-system', + ownerReferences: [], + relationships: [], + resourceVersion: '1176', + state: { + error: false, + message: '', + name: 'running', + transitioning: false + }, + uid: 'a2157217-0b62-4d95-adcd-686f79925d4d' + }, + spec: { + containers: [], + dnsPolicy: 'ClusterFirst', + enableServiceLinks: true, + nodeName: 'ip-172-31-14-130.us-east-2.compute.internal', + nodeSelector: { 'kubernetes.io/os': 'linux' }, + preemptionPolicy: 'PreemptLowerPriority', + priority: 0, + restartPolicy: 'Always', + schedulerName: 'default-scheduler', + securityContext: {}, + serviceAccount: 'rke2-snapshot-controller', + serviceAccountName: 'rke2-snapshot-controller', + terminationGracePeriodSeconds: 30, + tolerations: [], + volumes: [] + }, + status: {} + }, + { + id: 'kube-system/rke2-snapshot-validation-webhook-54c5989b65-87xqh', + type: 'pod', + links: { + remove: 'https://yonasb29.qa.rancher.space/v1/pods/kube-system/rke2-snapshot-validation-webhook-54c5989b65-87xqh', + self: 'https://yonasb29.qa.rancher.space/v1/pods/kube-system/rke2-snapshot-validation-webhook-54c5989b65-87xqh', + update: 'https://yonasb29.qa.rancher.space/v1/pods/kube-system/rke2-snapshot-validation-webhook-54c5989b65-87xqh', + view: 'https://yonasb29.qa.rancher.space/api/v1/namespaces/kube-system/pods/rke2-snapshot-validation-webhook-54c5989b65-87xqh' + }, + apiVersion: 'v1', + kind: 'Pod', + metadata: { + annotations: {}, + creationTimestamp: '2024-06-14T15:39:52Z', + fields: [ + 'rke2-snapshot-validation-webhook-54c5989b65-87xqh', + '1/1', + 'Running', + '0', + '7h39m', + '10.42.0.9', + 'ip-172-31-14-130.us-east-2.compute.internal', + '', + '' + ], + generateName: 'rke2-snapshot-validation-webhook-54c5989b65-', + labels: {}, + name: 'rke2-snapshot-validation-webhook-54c5989b65-87xqh', + namespace: 'kube-system', + ownerReferences: [], + relationships: [], + resourceVersion: '1138', + state: { + error: false, + message: '', + name: 'running', + transitioning: false + }, + uid: '929106df-13d9-43fc-9c76-09ad0292aab1' + }, + spec: { + containers: [], + dnsPolicy: 'ClusterFirst', + enableServiceLinks: true, + nodeName: 'ip-172-31-14-130.us-east-2.compute.internal', + nodeSelector: { 'kubernetes.io/os': 'linux' }, + preemptionPolicy: 'PreemptLowerPriority', + priority: 0, + restartPolicy: 'Always', + schedulerName: 'default-scheduler', + securityContext: {}, + serviceAccount: 'rke2-snapshot-validation-webhook', + serviceAccountName: 'rke2-snapshot-validation-webhook', + terminationGracePeriodSeconds: 30, + tolerations: [], + volumes: [] + }, + status: {} + } + ] +}; + +function reply(statusCode: number, body: any) { + return (req) => { + req.reply({ + statusCode, + body + }); + }; +} + +export function generatePodsDataSmall(): Cypress.Chainable { + return cy.intercept('GET', '/v1/pods?*', reply(200, podsGetResponseSmallSet)).as('podsDataSmall'); +} diff --git a/cypress/e2e/po/components/header-row.po.ts b/cypress/e2e/po/components/header-row.po.ts new file mode 100644 index 00000000000..a1aa84e6180 --- /dev/null +++ b/cypress/e2e/po/components/header-row.po.ts @@ -0,0 +1,27 @@ +import ComponentPo from '@/cypress/e2e/po/components/component.po'; + +export default class HeaderRowPo extends ComponentPo { + column(index: number) { + return this.self().find('th').eq(index); + } + + get(selector: string, options?: any) { + return this.self().get(selector, options); + } + + within(selector: string, options?: any) { + return this.self().within(() => { + this.get(selector, options); + }); + } + + /** + * check table header sort order + * @param index + * @param direction up or down (i.e. DESC or ASC) + */ + checkSortOrder(index: number, direction: string) { + this.column(index).find('.sort').find('.icon-stack > i').should('have.length', 2) + .and('have.class', `icon-sort-${ direction }`); + } +} diff --git a/cypress/e2e/po/components/sortable-table.po.ts b/cypress/e2e/po/components/sortable-table.po.ts index 850d10f0752..8aed12da636 100644 --- a/cypress/e2e/po/components/sortable-table.po.ts +++ b/cypress/e2e/po/components/sortable-table.po.ts @@ -4,6 +4,7 @@ import CheckboxInputPo from '@/cypress/e2e/po/components/checkbox-input.po'; import ListRowPo from '@/cypress/e2e/po/components/list-row.po'; import PromptRemove from '@/cypress/e2e/po/prompts/promptRemove.po'; import PaginationPo from '@/cypress/e2e/po/components/pagination.po'; +import HeaderRowPo from '@/cypress/e2e/po/components/header-row.po'; export default class SortableTablePo extends ComponentPo { // @@ -98,7 +99,12 @@ export default class SortableTablePo extends ComponentPo { } tableHeaderRow() { - return this.self().find('thead tr'); + return new HeaderRowPo(this.self()); + } + + // sort + sort(index: number) { + return this.tableHeaderRow().column(index).find('.sort'); } subRows() { diff --git a/cypress/e2e/tests/pages/explorer/dashboard/cluster-dashboard.spec.ts b/cypress/e2e/tests/pages/explorer/dashboard/cluster-dashboard.spec.ts index f7d6e621394..94dc747cffe 100644 --- a/cypress/e2e/tests/pages/explorer/dashboard/cluster-dashboard.spec.ts +++ b/cypress/e2e/tests/pages/explorer/dashboard/cluster-dashboard.spec.ts @@ -9,7 +9,6 @@ import { NodesPagePo } from '@/cypress/e2e/po/pages/explorer/nodes.po'; import { EventsPagePo } from '@/cypress/e2e/po/pages/explorer/events.po'; import * as path from 'path'; import { generateEventsDataLarge, generateEventsDataSmall, eventsNoDataset } from '@/cypress/e2e/blueprints/explorer/cluster/events'; -import { groupByPayload } from '@/cypress/e2e/blueprints/user_preferences/group_by'; import HomePagePo from '@/cypress/e2e/po/pages/home.po'; const configMapYaml = `apiVersion: v1 @@ -223,7 +222,7 @@ describe('Cluster Dashboard', { testIsolation: 'off', tags: ['@explorer', '@admi // create ns cy.get('@projId').then((projId) => { - cy.createNamespace(nsName, projId); + cy.createNamespaceInProject(nsName, projId); }); // create pod @@ -247,16 +246,13 @@ describe('Cluster Dashboard', { testIsolation: 'off', tags: ['@explorer', '@admi events.sortableTable().rowElements().should('have.length.gte', 2); }); - describe('pagination', { tags: ['@vai'] }, () => { + describe('List', { tags: ['@vai'] }, () => { const events = new EventsPagePo('local'); + const eventName = 'aaa-e2e-vai-test-event-name'; before('set up', () => { // set user preferences: update resource filter - cy.getRancherResource('v3', 'users?me=true').then((resp: Cypress.Response) => { - const userId = resp.body.data[0].id.trim(); - - cy.setRancherResource('v1', 'userpreferences', userId, groupByPayload(userId, 'local', 'none', '{\"local\":[]}')); - }); + cy.updateResourceListViewPref('local', 'none', '{\"local\":[]}'); HomePagePo.goTo(); // this is needed for updated user preferences to load in the UI }); @@ -322,9 +318,26 @@ describe('Cluster Dashboard', { testIsolation: 'off', tags: ['@explorer', '@admi events.sortableTable().pagination().leftButton().isDisabled(); }); - it('sorting changes the order of paginated events data', () => { - const eventName = 'aaa-e2e-vai-test-event-name'; + it('filter events', () => { + // generate large set of events data + generateEventsDataLarge(); + ClusterDashboardPagePo.navTo(); + clusterDashboard.waitForPage(undefined, 'cluster-events'); + EventsPagePo.navTo(); + cy.wait('@eventsDataLarge'); + events.waitForPage(); + + events.sortableTable().checkVisible(); + events.sortableTable().checkLoadingIndicatorNotVisible(); + events.sortableTable().checkRowCount(false, 100); + + // filter by name + events.sortableTable().filter(eventName); + events.sortableTable().checkRowCount(false, 1); + events.sortableTable().rowElementWithName(eventName).scrollIntoView().should('be.visible'); + }); + it('sorting changes the order of paginated events data', () => { // generate large set of events data generateEventsDataLarge(); ClusterDashboardPagePo.navTo(); @@ -333,14 +346,17 @@ describe('Cluster Dashboard', { testIsolation: 'off', tags: ['@explorer', '@admi events.waitForPage(); cy.wait('@eventsDataLarge'); + // check table is sorted by `last seen` in ASC order by default + events.sortableTable().tableHeaderRow().checkSortOrder(2, 'down'); + // sort by name in ASC order - events.sortableTable().tableHeaderRow().contains('Name').click(); + events.sortableTable().sort(11).click(); // event name should be visible on first page (sorted in ASC order) events.sortableTable().rowElementWithName(eventName).scrollIntoView().should('be.visible'); // sort by name in DESC order - events.sortableTable().tableHeaderRow().contains('Name').click(); + events.sortableTable().sort(11).click(); // event name should be NOT visible on first page (sorted in DESC order) events.sortableTable().rowElementWithName(eventName).should('not.exist'); @@ -367,6 +383,7 @@ describe('Cluster Dashboard', { testIsolation: 'off', tags: ['@explorer', '@admi events.sortableTable().pagination().checkNotExists(); }); }); + it('can view events table empty if no events', { tags: ['@vai'] }, () => { const events = new EventsPagePo('local'); @@ -382,7 +399,7 @@ describe('Cluster Dashboard', { testIsolation: 'off', tags: ['@explorer', '@admi const expectedHeaders = ['Reason', 'Object', 'Message', 'Name', 'Date']; clusterDashboard.eventsList().resourceTable().sortableTable().tableHeaderRow() - .find('.table-header-container .content') + .within('.table-header-container .content') .each((el, i) => { expect(el.text().trim()).to.eq(expectedHeaders[i]); }); @@ -397,7 +414,7 @@ describe('Cluster Dashboard', { testIsolation: 'off', tags: ['@explorer', '@admi 'Subobject', 'Source', 'Message', 'First Seen', 'Count', 'Name', 'Namespace']; events.eventslist().resourceTable().sortableTable().tableHeaderRow() - .find('.table-header-container .content') + .within('.table-header-container .content') .each((el, i) => { expect(el.text().trim()).to.eq(expectedFullHeaders[i]); }); @@ -409,12 +426,6 @@ describe('Cluster Dashboard', { testIsolation: 'off', tags: ['@explorer', '@admi cy.deleteRancherResource('v1', 'namespaces', `${ nsName }`); cy.deleteRancherResource('v3', 'projects', this.projId); } - - // reset user preferences: update resource filter - cy.getRancherResource('v3', 'users?me=true').then((resp: Cypress.Response) => { - const userId = resp.body.data[0].id.trim(); - - cy.setRancherResource('v1', 'userpreferences', userId, groupByPayload(userId, 'local', 'none', '{"local":["all://user"]}')); - }); + cy.updateResourceListViewPref('local', 'none', '{"local":["all://user"]}'); }); }); diff --git a/cypress/e2e/tests/pages/explorer/namespace-picker.spec.ts b/cypress/e2e/tests/pages/explorer/namespace-picker.spec.ts index 045ceee3443..39eb403cd99 100644 --- a/cypress/e2e/tests/pages/explorer/namespace-picker.spec.ts +++ b/cypress/e2e/tests/pages/explorer/namespace-picker.spec.ts @@ -2,7 +2,6 @@ import ClusterDashboardPagePo from '@/cypress/e2e/po/pages/explorer/cluster-dash import { NamespaceFilterPo } from '@/cypress/e2e/po/components/namespace-filter.po'; import { WorkloadsPodsListPagePo } from '@/cypress/e2e/po/pages/explorer/workloads-pods.po'; import HomePagePo from '@/cypress/e2e/po/pages/home.po'; -import { groupByPayload } from '@/cypress/e2e/blueprints/user_preferences/group_by'; const namespacePicker = new NamespaceFilterPo(); @@ -27,11 +26,7 @@ describe('Namespace picker', { testIsolation: 'off' }, () => { // Verify multiple namespaces within Project: System display when filtering by Project // group workloads by namespace - cy.getRancherResource('v3', 'users?me=true').then((resp: Cypress.Response) => { - const userId = resp.body.data[0].id.trim(); - - cy.setRancherResource('v1', 'userpreferences', userId, groupByPayload(userId, 'local', 'metadata.namespace', '{"local":["all://user"]}')); - }); + cy.updateResourceListViewPref('local', 'metadata.namespace', '{"local":["all://user"]}'); const workloadsPodPage = new WorkloadsPodsListPagePo('local'); @@ -192,7 +187,7 @@ describe('Namespace picker', { testIsolation: 'off' }, () => { const projId = resp.body.id.trim(); // create ns - cy.createNamespace(nsName, projId); + cy.createNamespaceInProject(nsName, projId); // check ns picker namespacePicker.toggle(); @@ -213,11 +208,6 @@ describe('Namespace picker', { testIsolation: 'off' }, () => { }); after('clean up', () => { - // get user id - cy.getRancherResource('v3', 'users?me=true').then((resp: Cypress.Response) => { - const userId = resp.body.data[0].id.trim(); - - cy.setRancherResource('v1', 'userpreferences', userId, groupByPayload(userId, 'local', 'none', '{"local":["all://user"]}')); - }); + cy.updateResourceListViewPref('local', 'none', '{"local":["all://user"]}'); }); }); diff --git a/cypress/e2e/tests/pages/explorer/workloads/pods.spec.ts b/cypress/e2e/tests/pages/explorer/workloads/pods.spec.ts index e9c558bec0f..a57eebc1017 100644 --- a/cypress/e2e/tests/pages/explorer/workloads/pods.spec.ts +++ b/cypress/e2e/tests/pages/explorer/workloads/pods.spec.ts @@ -1,93 +1,251 @@ import { WorkloadsPodsListPagePo, WorkLoadsPodDetailsPagePo } from '@/cypress/e2e/po/pages/explorer/workloads-pods.po'; import { createPodBlueprint, clonePodBlueprint } from '@/cypress/e2e/blueprints/explorer/workload-pods'; import PodPo from '@/cypress/e2e/po/components/workloads/pod.po'; +import HomePagePo from '@/cypress/e2e/po/pages/home.po'; +import { generatePodsDataSmall } from '@/cypress/e2e/blueprints/explorer/workloads/pods/pods-get'; -describe('Cluster Explorer', () => { - beforeEach(() => { +describe('Pods', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] }, () => { + const workloadsPodPage = new WorkloadsPodsListPagePo('local'); + const nsName = `namespace${ +new Date() }`; + const nsName2 = `namespace${ +new Date() }abc`; + const uniquePod = 'aaa-e2e-test-pod-name'; + const podNamesList = []; + + before(() => { cy.login(); }); - describe('Workloads', () => { - describe('Pods', () => { - const workloadsPodPage = new WorkloadsPodsListPagePo('local'); + describe('List', { tags: ['@vai'] }, () => { + before('set up', () => { + cy.updateResourceListViewPref('local', 'none', '{\"local\":[]}'); + + // create namespaces + cy.createNamespace(nsName); + cy.createNamespace(nsName2); + + // create pods + let i = 0; + + while (i < 100) { + const podName = `e2e-${ Cypress._.uniqueId(Date.now().toString()) }`; - describe('Should open a terminal', { tags: ['@explorer', '@adminUser'] }, () => { - beforeEach(() => { - workloadsPodPage.goTo(); + cy.createPod(nsName, podName, 'nginx:latest', false).then(() => { + podNamesList.push(`pod-${ podName }`); }); - it('should open a pod shell', () => { - const shellPodPo = new PodPo(); + i++; + } + + // create unique pod for sorting test + cy.createPod(nsName2, uniquePod, 'nginx:latest'); + }); + + it('pagination is visible and user is able navigate through pods data', () => { + // get pods count + cy.getRancherResource('v1', 'pods').then((resp: Cypress.Response) => { + const count = resp.body.count; - shellPodPo.openPodShell(); + WorkloadsPodsListPagePo.navTo(); + workloadsPodPage.waitForPage(); + + // pagination is visible + workloadsPodPage.sortableTable().pagination().checkVisible(); + + // basic checks on navigation buttons + workloadsPodPage.sortableTable().pagination().beginningButton().isDisabled(); + workloadsPodPage.sortableTable().pagination().leftButton().isDisabled(); + workloadsPodPage.sortableTable().pagination().rightButton().isEnabled(); + workloadsPodPage.sortableTable().pagination().endButton().isEnabled(); + + // check text before navigation + workloadsPodPage.sortableTable().pagination().paginationText().then((el) => { + expect(el.trim()).to.eq(`1 - 100 of ${ count } Pods`); }); - }); - describe('When cloning a pod', { tags: ['@explorer', '@adminUser'] }, () => { - const { name: origPodName, namespace } = createPodBlueprint.metadata; - const { name: clonePodName } = clonePodBlueprint.metadata; + // navigate to next page - right button + workloadsPodPage.sortableTable().pagination().rightButton().click(); + + // check text and buttons after navigation + workloadsPodPage.sortableTable().pagination().paginationText().then((el) => { + expect(el.trim()).to.eq(`101 - ${ count } of ${ count } Pods`); + }); + workloadsPodPage.sortableTable().pagination().beginningButton().isEnabled(); + workloadsPodPage.sortableTable().pagination().leftButton().isEnabled(); + + // navigate to first page - left button + workloadsPodPage.sortableTable().pagination().leftButton().click(); + + // check text and buttons after navigation + workloadsPodPage.sortableTable().pagination().paginationText().then((el) => { + expect(el.trim()).to.eq(`1 - 100 of ${ count } Pods`); + }); + workloadsPodPage.sortableTable().pagination().beginningButton().isDisabled(); + workloadsPodPage.sortableTable().pagination().leftButton().isDisabled(); - beforeEach(() => { - cy.intercept('GET', `/v1/pods/${ namespace }/${ origPodName }?exclude=metadata.managedFields`).as('origPod'); - cy.intercept('GET', `/v1/pods/${ namespace }/${ clonePodName }?exclude=metadata.managedFields`).as('clonedPod'); + // navigate to last page - end button + workloadsPodPage.sortableTable().pagination().endButton().click(); - workloadsPodPage.goTo(); + // check text after navigation + workloadsPodPage.sortableTable().pagination().paginationText().then((el) => { + expect(el.trim()).to.eq(`101 - ${ count } of ${ count } Pods`); + }); - const createPodPo = new PodPo(); + // navigate to first page - beginning button + workloadsPodPage.sortableTable().pagination().beginningButton().click(); - createPodPo.createPodViaKubectl(createPodBlueprint); + // check text and buttons after navigation + workloadsPodPage.sortableTable().pagination().paginationText().then((el) => { + expect(el.trim()).to.eq(`1 - 100 of ${ count } Pods`); }); + workloadsPodPage.sortableTable().pagination().beginningButton().isDisabled(); + workloadsPodPage.sortableTable().pagination().leftButton().isDisabled(); + }); + }); + + it('filter pods', () => { + WorkloadsPodsListPagePo.navTo(); + workloadsPodPage.waitForPage(); + + workloadsPodPage.sortableTable().checkVisible(); + workloadsPodPage.sortableTable().checkLoadingIndicatorNotVisible(); + workloadsPodPage.sortableTable().checkRowCount(false, 100); + + // filter by name + workloadsPodPage.sortableTable().filter(podNamesList[0]); + workloadsPodPage.sortableTable().checkRowCount(false, 1); + workloadsPodPage.sortableTable().rowElementWithName(podNamesList[0]).should('be.visible'); + + // filter by namespace + workloadsPodPage.sortableTable().filter(nsName2); + workloadsPodPage.sortableTable().checkRowCount(false, 1); + workloadsPodPage.sortableTable().rowElementWithName(`pod-${ uniquePod }`).should('be.visible'); + }); + + it('sorting changes the order of paginated pods data', () => { + WorkloadsPodsListPagePo.navTo(); + workloadsPodPage.waitForPage(); + + /** + * Sort by Name + */ + // check table is sorted by name in ASC order by default + workloadsPodPage.sortableTable().tableHeaderRow().checkSortOrder(2, 'down'); - it(`Should have same spec as the original pod`, () => { - const cloneCreatePodPage = new WorkLoadsPodDetailsPagePo(origPodName, { mode: 'clone' }); + // pod name should be visible on first page (sorted in ASC order) + workloadsPodPage.sortableTable().rowElementWithName(podNamesList[0]).scrollIntoView().should('be.visible'); - cloneCreatePodPage.goTo(); + // sort by name in DESC order + workloadsPodPage.sortableTable().sort(2).click({ force: true }); + workloadsPodPage.sortableTable().tableHeaderRow().checkSortOrder(2, 'up'); - let origPodSpec: any; + // pod name should be NOT visible on first page (sorted in DESC order) + workloadsPodPage.sortableTable().rowElementWithName(podNamesList[0]).should('not.exist'); - cy.wait('@origPod', { timeout: 20000 }) - .then(({ response }) => { - expect(response?.statusCode).to.eq(200); - origPodSpec = response?.body.spec; - expect(origPodSpec.containers[0].resources).to.deep.eq(createPodBlueprint.spec.containers[0].resources); - }); + // navigate to last page + workloadsPodPage.sortableTable().pagination().endButton().click(); - const createClonePo = new PodPo(); + // pod name should be visible on last page (sorted in DESC order) + workloadsPodPage.sortableTable().rowElementWithName(podNamesList[0]).scrollIntoView().should('be.visible'); + }); - // Each pod need a unique name - createClonePo.nameNsDescription().name().set(clonePodName); - createClonePo.save(); + it('pagination is hidden', () => { + // generate small set of pods data + generatePodsDataSmall(); + HomePagePo.goTo(); // this is needed here for the intercept to work + WorkloadsPodsListPagePo.navTo(); + workloadsPodPage.waitForPage(); + cy.wait('@podsDataSmall'); + + workloadsPodPage.sortableTable().checkVisible(); + workloadsPodPage.sortableTable().checkLoadingIndicatorNotVisible(); + workloadsPodPage.sortableTable().checkRowCount(false, 3); + workloadsPodPage.sortableTable().pagination().checkNotExists(); + }); + }); - workloadsPodPage.waitForPage(); - workloadsPodPage.list().checkVisible(); - workloadsPodPage.list().resourceTable().sortableTable().filter(clonePodName); - workloadsPodPage.list().resourceTable().sortableTable().rowWithName(clonePodName) - .checkExists(); + describe('Should open a terminal', () => { + beforeEach(() => { + workloadsPodPage.goTo(); + }); - // Simple test to assert we haven't broken Pods detail page - // https://github.com/rancher/dashboard/issues/10490 - const clonedPodPage = new WorkLoadsPodDetailsPagePo(clonePodName); + it('should open a pod shell', () => { + const shellPodPo = new PodPo(); + + shellPodPo.openPodShell(); + }); + }); - clonedPodPage.goTo();// Needs to be goTo to ensure http request is fired - clonedPodPage.waitForPage(); + describe('When cloning a pod', () => { + const { name: origPodName, namespace } = createPodBlueprint.metadata; + const { name: clonePodName } = clonePodBlueprint.metadata; - cy.wait('@clonedPod', { timeout: 20000 }) - .then(({ response }) => { - expect(response?.statusCode).to.eq(200); + beforeEach(() => { + cy.intercept('GET', `/v1/pods/${ namespace }/${ origPodName }?exclude=metadata.managedFields`).as('origPod'); + cy.intercept('GET', `/v1/pods/${ namespace }/${ clonePodName }?exclude=metadata.managedFields`).as('clonedPod'); - const clonedSpec = response?.body?.spec; + workloadsPodPage.goTo(); - // In Dashboard adds empty affinity object by default - // Remove this to compare - if (!Object.keys(clonedSpec.affinity).length) { - delete clonedSpec.affinity; - } + const createPodPo = new PodPo(); - expect(clonedSpec).to.deep.eq(origPodSpec); - expect(clonedSpec.containers[0].resources).to.deep.eq(createPodBlueprint.spec.containers[0].resources); - }); + createPodPo.createPodViaKubectl(createPodBlueprint); + }); + + it(`Should have same spec as the original pod`, () => { + const cloneCreatePodPage = new WorkLoadsPodDetailsPagePo(origPodName, { mode: 'clone' }); + + cloneCreatePodPage.goTo(); + + let origPodSpec: any; + + cy.wait('@origPod', { timeout: 20000 }) + .then(({ response }) => { + expect(response?.statusCode).to.eq(200); + origPodSpec = response?.body.spec; + expect(origPodSpec.containers[0].resources).to.deep.eq(createPodBlueprint.spec.containers[0].resources); + }); + + const createClonePo = new PodPo(); + + // Each pod need a unique name + createClonePo.nameNsDescription().name().set(clonePodName); + createClonePo.save(); + + workloadsPodPage.waitForPage(); + workloadsPodPage.list().checkVisible(); + workloadsPodPage.list().resourceTable().sortableTable().filter(clonePodName); + workloadsPodPage.list().resourceTable().sortableTable().rowWithName(clonePodName) + .checkExists(); + + // Simple test to assert we haven't broken Pods detail page + // https://github.com/rancher/dashboard/issues/10490 + const clonedPodPage = new WorkLoadsPodDetailsPagePo(clonePodName); + + clonedPodPage.goTo();// Needs to be goTo to ensure http request is fired + clonedPodPage.waitForPage(); + + cy.wait('@clonedPod', { timeout: 20000 }) + .then(({ response }) => { + expect(response?.statusCode).to.eq(200); + + const clonedSpec = response?.body?.spec; + + // In Dashboard adds empty affinity object by default + // Remove this to compare + if (!Object.keys(clonedSpec.affinity).length) { + delete clonedSpec.affinity; + } + + expect(clonedSpec).to.deep.eq(origPodSpec); + expect(clonedSpec.containers[0].resources).to.deep.eq(createPodBlueprint.spec.containers[0].resources); }); - }); }); }); + + after(() => { + // delete namespace (this will also delete all pods in it) + cy.deleteRancherResource('v1', 'namespaces', nsName); + + // delete single pod + cy.deleteRancherResource('v1', `pods/${ nsName2 }`, `pod-${ uniquePod }`, false); + }); }); diff --git a/cypress/e2e/tests/pages/fleet/fleet-clusters.spec.ts b/cypress/e2e/tests/pages/fleet/fleet-clusters.spec.ts index 1a81b8c8ac2..a7b176514c6 100644 --- a/cypress/e2e/tests/pages/fleet/fleet-clusters.spec.ts +++ b/cypress/e2e/tests/pages/fleet/fleet-clusters.spec.ts @@ -359,7 +359,7 @@ describe('Fleet Clusters', { tags: ['@fleet', '@adminUser'] }, () => { const expectedHeaders = ['State', 'Name', 'Bundles Ready', 'Repos Ready', 'Resources', 'Last Seen', 'Age']; fleetClusterListPage.clusterList().resourceTable().sortableTable().tableHeaderRow() - .find('.table-header-container .content') + .within('.table-header-container .content') .each((el, i) => { expect(el.text().trim()).to.eq(expectedHeaders[i]); }); @@ -376,7 +376,7 @@ describe('Fleet Clusters', { tags: ['@fleet', '@adminUser'] }, () => { fleetClusterDetailsPage.gitReposTab().list().resourceTable().sortableTable() .tableHeaderRow() - .find('.table-header-container .content') + .within('.table-header-container .content') .each((el, i) => { expect(el.text().trim()).to.eq(expectedHeadersDetailsView[i]); }); diff --git a/cypress/e2e/tests/pages/fleet/gitrepo.spec.ts b/cypress/e2e/tests/pages/fleet/gitrepo.spec.ts index ac9dd2d3c14..39e75a5f3de 100644 --- a/cypress/e2e/tests/pages/fleet/gitrepo.spec.ts +++ b/cypress/e2e/tests/pages/fleet/gitrepo.spec.ts @@ -154,7 +154,7 @@ describe('Git Repo', { testIsolation: 'off', tags: ['@fleet', '@adminUser'] }, ( const expectedHeadersListView = ['State', 'Name', 'Repo', 'Target', 'Clusters Ready', 'Resources', 'Age']; listPage.repoList().resourceTable().sortableTable().tableHeaderRow() - .find('.table-header-container .content') + .within('.table-header-container .content') .each((el, i) => { expect(el.text().trim()).to.eq(expectedHeadersListView[i]); }); @@ -171,7 +171,7 @@ describe('Git Repo', { testIsolation: 'off', tags: ['@fleet', '@adminUser'] }, ( gitRepoDetails.bundlesTab().list().resourceTable().sortableTable() .tableHeaderRow() - .find('.table-header-container .content') + .within('.table-header-container .content') .each((el, i) => { expect(el.text().trim()).to.eq(expectedHeadersDetailsView[i]); }); diff --git a/cypress/globals.d.ts b/cypress/globals.d.ts index 2186aa1595d..a2e1f7e2fac 100644 --- a/cypress/globals.d.ts +++ b/cypress/globals.d.ts @@ -59,8 +59,9 @@ declare global { setProjectRoleBinding(clusterId: string, userPrincipalId: string, projectName: string, role: string): Chainable; getProjectByName(clusterId: string, projectName: string): Chainable; createProject(projName: string, clusterId: string, userId: string): Chainable; - createNamespace(nsName: string, projId: string): Chainable; - createPod(nsName: string, podName: string, image: string): Chainable; + createNamespaceInProject(nsName: string, projId: string): Chainable; + createNamespace(nsName: string): Chainable; + createPod(nsName: string, podName: string, image: string, failOnStatusCode?: boolean): Chainable; createAwsCloudCredentials(nsName: string, cloudCredName: string, defaultRegion: string, accessKey: string, secretKey: string): Chainable; createAmazonMachineConfig(instanceType: string, region: string, vpcId: string, zone: string, type: string, clusterName: string, namespace: string): Chainable; createAmazonRke2Cluster(params: CreateAmazonRke2ClusterParams): Chainable; @@ -71,6 +72,14 @@ declare global { deleteRancherResource(prefix: 'v3' | 'v1' | 'k8s', resourceType: string, resourceId: string, failOnStatusCode?: boolean): Chainable; deleteNodeTemplate(nodeTemplateId: string, timeout?: number) + /** + * update resource list view preference + * @param clusterName + * @param groupBy to update resource list view to 'flat list', 'group by namespaces', or 'group by node' ('none', 'metadata.namespace', or 'role') + * @param namespaceFilter to filter by 'only user namespaces', 'all namespace', etc. ('{"local":["all://user"]}', '{\"local\":[]}', etc.) + */ + updateResourceListViewPref(clusterName: string, groupBy:string, namespaceFilter: string): Chainable; + /** * Wrapper for cy.get() to simply define the data-testid value that allows you to pass a matcher to find the element. * @param id Value used for the data-testid attribute of the element. diff --git a/cypress/support/commands/rancher-api-commands.ts b/cypress/support/commands/rancher-api-commands.ts index 87000a82273..01af41cbb09 100644 --- a/cypress/support/commands/rancher-api-commands.ts +++ b/cypress/support/commands/rancher-api-commands.ts @@ -1,5 +1,6 @@ import { LoginPagePo } from '@/cypress/e2e/po/pages/login-page.po'; import { CreateUserParams, CreateAmazonRke2ClusterParams } from '@/cypress/globals'; +import { groupByPayload } from '@/cypress/e2e/blueprints/user_preferences/group_by'; // This file contains commands which makes API requests to the rancher API. // It includes the `login` command to store the `token` to use @@ -235,9 +236,9 @@ Cypress.Commands.add('createProject', (projName, clusterId, userId) => { }); /** - * create a namespace + * create a namespace in project */ -Cypress.Commands.add('createNamespace', (nsName, projId) => { +Cypress.Commands.add('createNamespaceInProject', (nsName, projId) => { return cy.request({ method: 'POST', url: `${ Cypress.env('api') }/v1/namespaces`, @@ -266,10 +267,35 @@ Cypress.Commands.add('createNamespace', (nsName, projId) => { }); }); +/** + * create a namespace + */ +Cypress.Commands.add('createNamespace', (nsName) => { + return cy.request({ + method: 'POST', + url: `${ Cypress.env('api') }/v1/namespaces`, + headers: { + 'x-api-csrf': token.value, + Accept: 'application/json' + }, + body: { + type: 'namespace', + metadata: { + annotations: { 'field.cattle.io/containerDefaultResourceLimit': '{}' }, + name: nsName + }, + disableOpenApiValidation: false + } + }) + .then((resp) => { + expect(resp.status).to.eq(201); + }); +}); + /** * Create pod */ -Cypress.Commands.add('createPod', (nsName, podName, image) => { +Cypress.Commands.add('createPod', (nsName, podName, image, failOnStatusCode = true) => { return cy.request({ method: 'POST', url: `${ Cypress.env('api') }/v1/pods`, @@ -277,6 +303,7 @@ Cypress.Commands.add('createPod', (nsName, podName, image) => { 'x-api-csrf': token.value, Accept: 'application/json' }, + failOnStatusCode, body: { type: 'pod', metadata: { @@ -295,7 +322,9 @@ Cypress.Commands.add('createPod', (nsName, podName, image) => { } }) .then((resp) => { - expect(resp.status).to.eq(201); + if (failOnStatusCode) { + expect(resp.status).to.eq(201); + } }); }); @@ -622,3 +651,12 @@ Cypress.Commands.add('createAmazonMachineConfig', (instanceType, region, vpcId, expect(resp.status).to.eq(201); }); }); + +// update resource list view preference +Cypress.Commands.add('updateResourceListViewPref', (clusterName: string, groupBy:string, namespaceFilter: string) => { + return cy.getRancherResource('v3', 'users?me=true').then((resp: Cypress.Response) => { + const userId = resp.body.data[0].id.trim(); + + cy.setRancherResource('v1', 'userpreferences', userId, groupByPayload(userId, clusterName, groupBy, namespaceFilter)); + }); +});