Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions cypress/e2e/po/components/list-row.po.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import ComponentPo from '@/cypress/e2e/po/components/component.po';
import ComponentPo, { GetOptions } from '@/cypress/e2e/po/components/component.po';

export default class ListRowPo extends ComponentPo {
column(index: number) {
return this.self().find('td').eq(index);
/**
* @param options timeout for the column lookup. Cypress timeouts are per
* command, so the timeout given to whatever produced the row does not carry
* over and the column otherwise falls back to `defaultCommandTimeout`.
*/
column(index: number, options?: GetOptions) {
return this.self().find('td', options).eq(index);
}

/**
Expand Down
4 changes: 2 additions & 2 deletions cypress/e2e/po/components/sortable-table.po.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,8 @@ export default class SortableTablePo extends ComponentPo {
return new ListRowPo(this.rowElementWithPartialName(name));
}

rowWithName(name: string) {
return new ListRowPo(this.rowElementWithName(name));
rowWithName(name: string, options?: GetOptions) {
return new ListRowPo(this.rowElementWithName(name, options));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import ClusterSnapshotsListPo from '~/cypress/e2e/po/lists/cluster-snapshots-lis
import TabbedPo from '~/cypress/e2e/po/components/tabbed.po';
import ClusterRecentEventsListPo from '~/cypress/e2e/po/lists/cluster-recent-events-list.po';
import DetailDrawer from '@/cypress/e2e/po/side-bars/detail-drawer.po';
import { GetOptions } from '@/cypress/e2e/po/components/component.po';

/**
* Covers core functionality that's common to the dashboard's cluster detail pages
Expand Down Expand Up @@ -42,8 +43,18 @@ export default abstract class ClusterManagerDetailPagePo extends BaseDetailPageP
return new ClusterProvisioningLogPo(this.self()).logsContainer(options);
}

kubectlCommandForImported() {
return this.self().get('code');
/**
* Tab button for the registration tab.
*
* Only rendered once the cluster registration token has resolved, so callers
* generally need to pass a timeout.
*/
registrationTab(options?: GetOptions) {
return this.self().get('[data-testid="btn-registration"]', options);
}

kubectlCommandForImported(options?: GetOptions) {
return this.self().get('code', options);
}

poolsList(tabId: 'machine' | 'node') {
Expand Down
5 changes: 3 additions & 2 deletions cypress/e2e/po/lists/provisioning.cattle.io.cluster.po.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import BaseResourceList from '@/cypress/e2e/po/lists/base-resource-list.po';
import { GetOptions } from '@/cypress/e2e/po/components/component.po';

/**
* List component for provisioning.cattle.io.cluster resources
Expand All @@ -20,8 +21,8 @@ export default class ProvClusterListPo extends BaseResourceList {
return this.resourceTable().sortableTable().rowWithName(name).column(index);
}

state(clusterName: string) {
return this.resourceTable().sortableTable().rowWithName(clusterName).column(1);
state(clusterName: string, options?: GetOptions) {
return this.resourceTable().sortableTable().rowWithName(clusterName, options).column(1, options);
}

name(clusterName: string) {
Expand Down
30 changes: 28 additions & 2 deletions cypress/e2e/po/pages/cluster-manager/cluster-manager-list.po.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { BaseListPagePo } from '@/cypress/e2e/po/pages/base/base-list-page.po';
import ProvClusterListPo from '@/cypress/e2e/po/lists/provisioning.cattle.io.cluster.po';
import BurgerMenuPo from '@/cypress/e2e/po/side-bars/burger-side-menu.po';
import { GetOptions } from '@/cypress/e2e/po/components/component.po';

/**
* List page for provisioning.cattle.io.cluster resources
Expand Down Expand Up @@ -43,16 +44,41 @@ export default class ClusterManagerListPagePo extends BaseListPagePo {
return new ProvClusterListPo('[data-testid="cluster-list"]');
}

/**
* Waits until the cluster list is rendered.
*
* `ResourceList` loads the per-type list component through a dynamic import,
* so this container does not exist until that chunk has been fetched and
* mounted. The first navigation of a run needs a generous timeout for that.
*
* Waits on the list rather than on an action button, which actions render
* depends on the user's permissions.
*/
waitForListReady(options?: GetOptions) {
return this.list().checkVisible(options);
}

/**
* Convenience method
*/
sortableTable() {
return this.list().resourceTable().sortableTable();
}

/**
* Import button in the list masthead.
*
* Found by test id rather than by position. The button is conditional on
* `canImport`, so an index based lookup silently resolves to the create
* button whenever it is absent. The masthead is a sibling of `cluster-list`,
* so this cannot be scoped to the list container.
*/
importClusterButton(options?: GetOptions) {
return cy.get('[data-testid="cluster-manager-list-import"]', options);
}

importCluster() {
return this.list().masthead().actions().eq(0)
.click();
return this.importClusterButton().click();
}

createCluster() {
Expand Down
83 changes: 62 additions & 21 deletions cypress/e2e/tests/pages/manager/cluster-manager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -430,14 +430,24 @@ describe('Cluster Manager', { testIsolation: 'off', tags: ['@manager', '@adminUs
const cacert = 'cacert';
const privateRegistry = 'registry.io';

before(() => {
// Absorb the cold start here rather than inside a test. The list
// component is pulled in through a dynamic import, so the first
// navigation of a run has to fetch it before the page has any content.
clusterList.goTo();
clusterList.checkIsCurrentPage();
clusterList.waitForListReady(EXTRA_LONG_TIMEOUT_OPT);
});

describe('Generic', () => {
it('can create new cluster', () => {
it('can create new cluster', { retries: 0 }, () => {
importGenericName = createClusterTestName('import-generic');
cy.intercept('GET', `${ USERS_BASE_URL }?*`).as('getUsers');
cy.intercept('POST', `/v3/${ importType }s`).as('importRequest');

clusterList.goTo();
clusterList.checkIsCurrentPage();
clusterList.waitForListReady(MEDIUM_TIMEOUT_OPT);
clusterList.importCluster();

importClusterPage.waitForPage('mode=import');
Expand All @@ -461,8 +471,9 @@ describe('Cluster Manager', { testIsolation: 'off', tags: ['@manager', '@adminUs

importClusterPage.create();

cy.wait('@importRequest').then((intercept) => {
expect(intercept.response.statusCode).to.eq(201);
cy.wait('@importRequest', { requestTimeout: LONG_TIMEOUT_OPT.timeout, responseTimeout: LONG_TIMEOUT_OPT.timeout }).then((intercept) => {
// Fail fast if the backend rejects the import.
expect(intercept.response?.statusCode, 'Cluster import POST status').to.eq(201);
expect(intercept.request.body).to.deep.equal({
type: importType,
agentEnvVars: [],
Expand All @@ -476,12 +487,33 @@ describe('Cluster Manager', { testIsolation: 'off', tags: ['@manager', '@adminUs
cy.getClusterIdByName(importGenericName).then((clusterId) => {
const detailClusterPage = new ClusterManagerDetailImportedGenericPagePo(undefined, clusterId);

detailClusterPage.waitForPage(undefined, 'registration');
detailClusterPage.kubectlCommandForImported().contains('--insecure').then(($value) => {
// The create form redirects here once the cluster exists, assert that
// rather than navigating, so a regression is caught not stepped over.
detailClusterPage.waitForPage(undefined, undefined, LONG_TIMEOUT_OPT);

// The `#registration` fragment is a side effect of Tabbed picking a
// default tab, which it does once, while no tab is active. If the
// registration token is slow another tab wins and the fragment is
// never written, so a longer timeout cannot fix it. Click the tab.
detailClusterPage.registrationTab(LONG_TIMEOUT_OPT).click();
detailClusterPage.waitForPage(undefined, 'registration', MEDIUM_TIMEOUT_OPT);

detailClusterPage.kubectlCommandForImported(MEDIUM_TIMEOUT_OPT).contains('--insecure').then(($value) => {
const kubectlCommand = $value.text();

expect(kubectlCommand).to.contain('--insecure');
cy.log(kubectlCommand);

// Assert the waiting state before applying the manifest, where no
// agent can have connected yet. After the apply it is a race.
ClusterManagerListPagePo.navTo();
clusterList.waitForPage();
clusterList.list().state(importGenericName, MEDIUM_TIMEOUT_OPT).should(($el) => {
const status = $el.text().trim();

expect(['Pending', 'Provisioning', 'Waiting']).to.include(status);
});

cy.exec(kubectlCommand, { failOnNonZeroExit: false, timeout: RESTART_TIMEOUT_OPT.timeout }).then((result) => {
cy.log(result.stderr);
cy.log(result.stdout);
Expand All @@ -492,13 +524,7 @@ describe('Cluster Manager', { testIsolation: 'off', tags: ['@manager', '@adminUs

ClusterManagerListPagePo.navTo();
clusterList.waitForPage();
clusterList.list().state(importGenericName).should('be.visible', EXTRA_LONG_TIMEOUT_OPT)
.and(($el) => {
const status = $el.text().trim();

expect(['Provisioning', 'Waiting']).to.include(status);
});
clusterList.list().state(importGenericName).contains('Active', EXTRA_LONG_TIMEOUT_OPT);
clusterList.list().state(importGenericName, MEDIUM_TIMEOUT_OPT).contains('Active', EXTRA_LONG_TIMEOUT_OPT);
// Issue #6836: Provider field on Imported clusters states "Imported" instead of cluster type
clusterList.list().provider(importGenericName).should('contain.text', 'Imported');
clusterList.list().providerSubType(importGenericName).should('contain.text', 'K3s');
Expand All @@ -509,10 +535,17 @@ describe('Cluster Manager', { testIsolation: 'off', tags: ['@manager', '@adminUs
const editImportedClusterPage = new ClusterManagerEditImportedPagePo(undefined, 'fleet-default', clusterId);

cy.intercept('GET', `${ USERS_BASE_URL }?*`).as('pageLoad');
// goTo() rather than navTo(), the pageLoad intercept needs a full page
// load. An SPA navigation leaves the store populated from the previous
// test, so the users request never fires again.
// Wait for the list and the row before touching the action menu.
clusterList.goTo();
clusterList.list().actionMenu(importGenericName).getMenuItem('Edit Config').click();
clusterList.waitForPage();
clusterList.waitForListReady(MEDIUM_TIMEOUT_OPT);
clusterList.sortableTable().rowElementWithName(importGenericName, MEDIUM_TIMEOUT_OPT).should('be.visible').scrollIntoView();
clusterList.list().actionMenu(importGenericName).getMenuItem('Edit Config').click({ force: true });

editImportedClusterPage.waitForPage('mode=edit');
editImportedClusterPage.waitForPage('mode=edit', undefined, LONG_TIMEOUT_OPT);

editImportedClusterPage.nameNsDescription().name().value().should('eq', importGenericName);
cy.wait('@pageLoad');
Expand Down Expand Up @@ -546,12 +579,14 @@ describe('Cluster Manager', { testIsolation: 'off', tags: ['@manager', '@adminUs

editImportedClusterPage.save();

// We should be taken back to the list page if the save was successful
clusterList.waitForPage();
// We should be taken back to the list page if the save was successful.
// The save PUT and redirect can exceed the default timeout under load.
clusterList.waitForPage(undefined, undefined, LONG_TIMEOUT_OPT);

clusterList.list().actionMenu(importGenericName).getMenuItem('Edit Config').click();
clusterList.sortableTable().rowElementWithName(importGenericName, MEDIUM_TIMEOUT_OPT).should('be.visible').scrollIntoView();
clusterList.list().actionMenu(importGenericName).getMenuItem('Edit Config').click({ force: true });

editImportedClusterPage.waitForPage('mode=edit');
editImportedClusterPage.waitForPage('mode=edit', undefined, LONG_TIMEOUT_OPT);
editImportedClusterPage.ace().fqdn().value().should('eq', fqdn );
editImportedClusterPage.ace().caCerts().value().should('eq', cacert );

Expand All @@ -563,7 +598,11 @@ describe('Cluster Manager', { testIsolation: 'off', tags: ['@manager', '@adminUs

it('can delete cluster by bulk actions', () => {
clusterList.goTo();
clusterList.sortableTable().rowElementWithName(importGenericName).should('exist', MEDIUM_TIMEOUT_OPT);
clusterList.waitForPage();
clusterList.waitForListReady(MEDIUM_TIMEOUT_OPT);
// Timeouts belong on the command that yields the element; .should()
// ignores an options argument (it is read as the assertion message).
clusterList.sortableTable().rowElementWithName(importGenericName, MEDIUM_TIMEOUT_OPT).should('be.visible').scrollIntoView();
clusterList.sortableTable().rowSelectCtlWithName(importGenericName).set();
clusterList.sortableTable().bulkActionDropDownOpen();
clusterList.sortableTable().bulkActionDropDownButton('Delete').click();
Expand All @@ -573,8 +612,10 @@ describe('Cluster Manager', { testIsolation: 'off', tags: ['@manager', '@adminUs
promptRemove.confirm(importGenericName);
promptRemove.remove();

clusterList.waitForPage();
clusterList.sortableTable().rowElementWithName(importGenericName).should('not.exist');
clusterList.waitForPage(undefined, undefined, LONG_TIMEOUT_OPT);
// Cluster removal is asynchronous, the row lingers while the backend
// finalizes the delete.
clusterList.sortableTable().rowElementWithName(importGenericName, EXTRA_LONG_TIMEOUT_OPT).should('not.exist');
});
});
});
Expand Down
Loading