From 0a7d2cfd6bf9d89b26cc5507ecee5da543a78c63 Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Wed, 1 May 2024 09:31:13 -0400 Subject: [PATCH 01/25] Add comment to Linode Create spec file --- .../manager/cypress/e2e/core/linodes/create-linode.spec.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts index a7d7a8c2fb9..f790b0981b1 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts @@ -1,3 +1,9 @@ +/** + * @file Integration tests and end-to-end tests for legacy Linode Create flow. + */ +// TODO Delete this test file when `linodeCreateRefactor` feature flag is retired. +// Move out any tests (e.g. region select test) that aren't covered by new tests. + import { containsVisible, fbtClick, From e8061d50b278625acdb436ba29e41ce0cd846207 Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Wed, 1 May 2024 09:33:32 -0400 Subject: [PATCH 02/25] Rename Linode Create tests to `legacy-create-linode.spec.ts` --- .../{create-linode.spec.ts => legacy-create-linode.spec.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/manager/cypress/e2e/core/linodes/{create-linode.spec.ts => legacy-create-linode.spec.ts} (100%) diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/legacy-create-linode.spec.ts similarity index 100% rename from packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts rename to packages/manager/cypress/e2e/core/linodes/legacy-create-linode.spec.ts From f97576403314d6f45cd4315d1eaca54d875915fa Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Wed, 1 May 2024 10:52:29 -0400 Subject: [PATCH 03/25] Add `data-qa-` attributes, UI helper to select tab panels --- packages/manager/cypress/support/ui/tab-list.ts | 13 ++++++++++++- .../src/components/TabbedPanel/TabbedPanel.tsx | 6 +++++- .../src/features/Linodes/LinodeCreatev2/Summary.tsx | 2 +- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/manager/cypress/support/ui/tab-list.ts b/packages/manager/cypress/support/ui/tab-list.ts index dd4816696dd..a24cd91bd5c 100644 --- a/packages/manager/cypress/support/ui/tab-list.ts +++ b/packages/manager/cypress/support/ui/tab-list.ts @@ -25,6 +25,17 @@ export const tabList = { tabTitle: string, options?: SelectorMatcherOptions ): Cypress.Chainable => { - return cy.get('[data-reach-tab-list]').findByText(tabTitle, options); + return cy.get(`[data-qa-tab="${tabTitle}"]`); + }, + + /** + * Finds a tab panel within a tab list by its title. + * + * @param tabTitle - Title of tab for which to find panel. + * + * @returns Cypress chainable. + */ + findTabPanelByTitle: (tabTitle: string): Cypress.Chainable => { + return cy.get(`[data-qa-tab-panel="${tabTitle}"]`); }, }; diff --git a/packages/manager/src/components/TabbedPanel/TabbedPanel.tsx b/packages/manager/src/components/TabbedPanel/TabbedPanel.tsx index 23bc8782e0a..1612183fce2 100644 --- a/packages/manager/src/components/TabbedPanel/TabbedPanel.tsx +++ b/packages/manager/src/components/TabbedPanel/TabbedPanel.tsx @@ -101,6 +101,7 @@ const TabbedPanel = React.memo((props: TabbedPanelProps) => { {tabs.map((tab, idx) => ( @@ -117,7 +118,10 @@ const TabbedPanel = React.memo((props: TabbedPanelProps) => { {tabs.map((tab, idx) => ( - + {tab.render(rest.children)} ))} diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Summary.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Summary.tsx index e4e9c2da42d..76510bf4875 100644 --- a/packages/manager/src/features/Linodes/LinodeCreatev2/Summary.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Summary.tsx @@ -124,7 +124,7 @@ export const Summary = () => { const summaryItemsToShow = summaryItems.filter((item) => item.show); return ( - + Summary {label} {summaryItemsToShow.length === 0 ? ( From 3eee4f7e5c13515a4bcd27198ca5b362bb85dfe8 Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Wed, 1 May 2024 10:55:20 -0400 Subject: [PATCH 04/25] Format Linode summary plan type label --- .../manager/src/features/Linodes/LinodeCreatev2/Summary.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Summary.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Summary.tsx index 76510bf4875..9501b863392 100644 --- a/packages/manager/src/features/Linodes/LinodeCreatev2/Summary.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Summary.tsx @@ -15,6 +15,7 @@ import { renderMonthlyPriceToCorrectDecimalPlace } from 'src/utilities/pricing/d import { getLinodeRegionPrice } from 'src/utilities/pricing/linodes'; import type { CreateLinodeRequest } from '@linode/api-v4'; +import { extendType } from 'src/utilities/extendType'; export const Summary = () => { const theme = useTheme(); @@ -78,7 +79,7 @@ export const Summary = () => { { item: { details: `$${price?.monthly}/month`, - title: type?.label, + title: type && extendType(type).formattedLabel, }, show: Boolean(type), }, From 2c939788f97a78a7dbe98e5adb2b20c260fc9b6a Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Wed, 1 May 2024 16:40:15 -0400 Subject: [PATCH 05/25] Add data-qa-paper attribute to papers --- packages/manager/src/components/Paper.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/manager/src/components/Paper.tsx b/packages/manager/src/components/Paper.tsx index c12f8ce9530..61bc8f129c3 100644 --- a/packages/manager/src/components/Paper.tsx +++ b/packages/manager/src/components/Paper.tsx @@ -30,6 +30,7 @@ export const Paper = (props: Props) => { {props.error && {props.error}} From 8ba1012b7f18986f5251ea49614025a46ecc899b Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Wed, 1 May 2024 16:43:34 -0400 Subject: [PATCH 06/25] Add Linode create timeout constant --- packages/manager/cypress/support/constants/linodes.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 packages/manager/cypress/support/constants/linodes.ts diff --git a/packages/manager/cypress/support/constants/linodes.ts b/packages/manager/cypress/support/constants/linodes.ts new file mode 100644 index 00000000000..f6eb377242c --- /dev/null +++ b/packages/manager/cypress/support/constants/linodes.ts @@ -0,0 +1,10 @@ +/** + * @file Constants related to Linode tests. + */ + +/** + * Length of time to wait for a Linode to be created. + * + * Equals 3 minutes. + */ +export const LINODE_CREATE_TIMEOUT = 180_000; From 0d4152474d01882d5b386f25c0d327caf047971a Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Wed, 1 May 2024 17:22:40 -0400 Subject: [PATCH 07/25] Add page utilities to ease interaction with Linode create and VPC create forms --- .../manager/cypress/support/ui/pages/index.ts | 15 ++++ .../support/ui/pages/linode-create-page.ts | 72 +++++++++++++++++ .../support/ui/pages/vpc-create-drawer.ts | 80 +++++++++++++++++++ 3 files changed, 167 insertions(+) create mode 100644 packages/manager/cypress/support/ui/pages/index.ts create mode 100644 packages/manager/cypress/support/ui/pages/linode-create-page.ts create mode 100644 packages/manager/cypress/support/ui/pages/vpc-create-drawer.ts diff --git a/packages/manager/cypress/support/ui/pages/index.ts b/packages/manager/cypress/support/ui/pages/index.ts new file mode 100644 index 00000000000..f08f42cd5a9 --- /dev/null +++ b/packages/manager/cypress/support/ui/pages/index.ts @@ -0,0 +1,15 @@ +/** + * @file Index file for Cypress page utility re-exports. + * + * Page utilities are basic JavaScript objects containing functions to perform + * common page-specific interactions. They allow us to minimize code duplication + * across tests that interact with similar pages. + * + * Page utilities are NOT page objects in the traditional UI testing sense. + * Specifically, page utility objects should NOT have state, and page utilities + * should only be concerned with interacting with or asserting the state of + * the DOM. + */ + +export * from './linode-create-page'; +export * from './vpc-create-drawer'; diff --git a/packages/manager/cypress/support/ui/pages/linode-create-page.ts b/packages/manager/cypress/support/ui/pages/linode-create-page.ts new file mode 100644 index 00000000000..99d15de94e6 --- /dev/null +++ b/packages/manager/cypress/support/ui/pages/linode-create-page.ts @@ -0,0 +1,72 @@ +/** + * @file Page utilities for Linode Create page (v2 implementation). + */ + +import { ui } from 'support/ui'; + +/** + * Page utilities for interacting with the Linode create page. + */ +export const linodeCreatePage = { + /** + * Sets the Linode's label. + * + * @param linodeLabel - Linode label to set. + */ + setLabel: (linodeLabel: string) => { + cy.findByLabelText('Linode Label').type(`{selectall}{del}${linodeLabel}`); + }, + + /** + * Sets the Linode's root password. + * + * @param linodePassword - Root password to set. + */ + setRootPassword: (linodePassword: string) => { + cy.findByLabelText('Root Password').as('rootPasswordField').click(); + + cy.get('@rootPasswordField').type(linodePassword, { log: false }); + }, + + /** + * Selects the Image with the given name. + * + * @param imageName - Name of Image to select. + */ + selectImage: (imageName: string) => { + cy.findByText('Choose a Distribution') + .closest('[data-qa-paper]') + .within(() => { + ui.autocomplete.find().click(); + + ui.autocompletePopper + .findByTitle(imageName) + .should('be.visible') + .click(); + }); + }, + + /** + * Select the Region with the given ID. + * + * @param regionId - ID of Region to select. + */ + selectRegionById: (regionId: string) => { + ui.regionSelect.find().click().type(`${regionId}{enter}`); + }, + + /** + * Select the given Linode plan. + * + * @param planTabTitle - Title of tab where desired plan is located. + * @param planTitle - Title of desired plan. + */ + selectPlan: (planTabTitle: string, planTitle: string) => { + ui.tabList.findTabByTitle(planTabTitle).click(); + ui.tabList.findTabPanelByTitle(planTabTitle).within(() => { + cy.findByLabelText(planTitle, { selector: 'tr' }) + .should('be.visible') + .click(); + }); + }, +}; diff --git a/packages/manager/cypress/support/ui/pages/vpc-create-drawer.ts b/packages/manager/cypress/support/ui/pages/vpc-create-drawer.ts new file mode 100644 index 00000000000..20c5635dc4b --- /dev/null +++ b/packages/manager/cypress/support/ui/pages/vpc-create-drawer.ts @@ -0,0 +1,80 @@ +import { ui } from 'support/ui'; + +/** + * Page utilities for interacting with the VPC create drawer. + * + * Assumes that selection context is limited to only the drawer. + */ +export const vpcCreateDrawer = { + /** + * Sets the VPC create form's label field. + * + * @param vpcLabel - VPC label to set. + */ + setLabel: (vpcLabel: string) => { + cy.findByLabelText('VPC Label') + .should('be.visible') + .type(`{selectall}{del}${vpcLabel}`); + }, + + /** + * Sets the VPC create form's description field. + * + * @param vpcDescription - VPC description to set. + */ + setDescription: (vpcDescription: string) => { + cy.findByLabelText('Description', { exact: false }) + .should('be.visible') + .type(`{selectall}{del}${vpcDescription}`); + }, + + /** + * Sets the VPC create form's subnet label. + * + * When handling more than one subnet, an index can be provided to control + * which field is being modified. + * + * @param subnetLabel - Label to set. + * @param subnetIndex - Optional index of subnet for which to update label. + */ + setSubnetLabel: (subnetLabel: string, subnetIndex: number = 0) => { + cy.findByText('Subnet Label', { + selector: `label[for="subnet-label-${subnetIndex}"]`, + }) + .should('be.visible') + .click(); + + cy.focused().type(`{selectall}{del}${subnetLabel}`); + }, + + /** + * Sets the VPC create form's subnet IP address. + * + * When handling more than one subnet, an index can be provided to control + * which field is being modified. + * + * @param subnetIpRange - IP range to set. + * @param subnetIndex - Optional index of subnet for which to update IP range. + */ + setSubnetIpRange: (subnetIpRange: string, subnetIndex: number = 0) => { + cy.findByText('Subnet IP Address Range', { + selector: `label[for="subnet-ipv4-${subnetIndex}"]`, + }) + .should('be.visible') + .click(); + + cy.focused().type(`{selectall}{del}${subnetIpRange}`); + }, + + /** + * Submits the VPC create form. + */ + submit: () => { + ui.buttonGroup + .findButtonByTitle('Create VPC') + .scrollIntoView() + .should('be.visible') + .should('be.enabled') + .click(); + }, +}; From ef1d6af959d354283abd3ed035d2347699a5178e Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Wed, 1 May 2024 17:25:57 -0400 Subject: [PATCH 08/25] Add v2 Linode create end-to-end tests for each plan type --- .../e2e/core/linodes/create-linode.spec.ts | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts new file mode 100644 index 00000000000..1baf5bd0e96 --- /dev/null +++ b/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts @@ -0,0 +1,127 @@ +/** + * @file Linode Create end-to-end tests. + */ + +import { authenticate } from 'support/api/authentication'; +import { interceptCreateLinode } from 'support/intercepts/linodes'; +import { + mockAppendFeatureFlags, + mockGetFeatureFlagClientstream, +} from 'support/intercepts/feature-flags'; +import { makeFeatureFlagData } from 'support/util/feature-flags'; +import { ui } from 'support/ui'; +import { chooseRegion } from 'support/util/regions'; +import { randomLabel, randomString } from 'support/util/random'; +import { LINODE_CREATE_TIMEOUT } from 'support/constants/linodes'; +import { cleanUp } from 'support/util/cleanup'; +import { linodeCreatePage } from 'support/ui/pages'; + +authenticate(); +describe('Create Linode', () => { + before(() => { + cleanUp('linodes'); + }); + + // Enable the `linodeCreateRefactor` feature flag. + // TODO Delete these mocks once `linodeCreateRefactor` feature flag is retired. + beforeEach(() => { + mockAppendFeatureFlags({ + linodeCreateRefactor: makeFeatureFlagData(true), + }); + mockGetFeatureFlagClientstream(); + }); + + /* + * End-to-end tests to create Linodes for each available plan type. + */ + describe('End-to-end', () => { + // Run an end-to-end test to create a basic Linode for each plan type described below. + describe('By plan type', () => { + [ + { + planType: 'Shared CPU', + planLabel: 'Nanode 1 GB', + planId: 'g6-nanode-1', + }, + { + planType: 'Dedicated CPU', + planLabel: 'Dedicated 4 GB', + planId: 'g6-dedicated-2', + }, + { + planType: 'High Memory', + planLabel: 'Linode 24 GB', + planId: 'g7-highmem-1', + }, + { + planType: 'Premium CPU', + planLabel: 'Premium 4 GB', + planId: 'g7-premium-2', + }, + // TODO Include GPU plan types. + ].forEach((planConfig) => { + /* + * - Parameterized end-to-end test to create a Linode for each plan type. + * - Confirms that a Linode of the given plan type can be deployed. + */ + it(`creates a ${planConfig.planType} Linode`, () => { + const linodeRegion = chooseRegion({ + capabilities: ['Linodes', 'Premium Plans'], + }); + const linodeLabel = randomLabel(); + + interceptCreateLinode().as('createLinode'); + cy.visitWithLogin('/linodes/create'); + + // Set Linode label, distribution, plan type, password, etc. + linodeCreatePage.setLabel(linodeLabel); + linodeCreatePage.selectImage('Debian 11'); + linodeCreatePage.selectRegionById(linodeRegion.id); + linodeCreatePage.selectPlan( + planConfig.planType, + planConfig.planLabel + ); + linodeCreatePage.setRootPassword(randomString(32)); + + // Confirm information in summary is shown as expected. + cy.get('[data-qa-linode-create-summary]') + .scrollIntoView() + .within(() => { + cy.findByText('Debian 11').should('be.visible'); + cy.findByText(linodeRegion.label).should('be.visible'); + cy.findByText(planConfig.planLabel).should('be.visible'); + }); + + // Create Linode and confirm it's provisioned as expected. + ui.button + .findByTitle('Create Linode') + .should('be.visible') + .should('be.enabled') + .click(); + + cy.wait('@createLinode').then((xhr) => { + const requestPayload = xhr.request.body; + const responsePayload = xhr.response?.body; + + // Confirm that API request and response contain expected data + expect(requestPayload['label']).to.equal(linodeLabel); + expect(requestPayload['region']).to.equal(linodeRegion.id); + expect(requestPayload['type']).to.equal(planConfig.planId); + + expect(responsePayload['label']).to.equal(linodeLabel); + expect(responsePayload['region']).to.equal(linodeRegion.id); + expect(responsePayload['type']).to.equal(planConfig.planId); + + // Confirm that Cloud redirects to details page + cy.url().should('endWith', `/linodes/${responsePayload['id']}`); + }); + + // TODO Confirm whether or not toast notification should appear here. + cy.findByText('RUNNING', { timeout: LINODE_CREATE_TIMEOUT }).should( + 'be.visible' + ); + }); + }); + }); + }); +}); From 70d751ed894ad51178a73b02f234da02952f6703 Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Wed, 1 May 2024 17:26:58 -0400 Subject: [PATCH 09/25] Add v2 Linode create integration tests for VPC assignment --- .../linodes/create-linode-with-vpc.spec.ts | 267 ++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 packages/manager/cypress/e2e/core/linodes/create-linode-with-vpc.spec.ts diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-vpc.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-vpc.spec.ts new file mode 100644 index 00000000000..668c344f2de --- /dev/null +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-vpc.spec.ts @@ -0,0 +1,267 @@ +import { + linodeFactory, + regionFactory, + subnetFactory, + vpcFactory, +} from 'src/factories'; +import { + mockAppendFeatureFlags, + mockGetFeatureFlagClientstream, +} from 'support/intercepts/feature-flags'; +import { + mockCreateLinode, + mockGetLinodeDetails, +} from 'support/intercepts/linodes'; +import { mockGetRegions } from 'support/intercepts/regions'; +import { + mockCreateVPC, + mockCreateVPCError, + mockGetVPC, + mockGetVPCs, +} from 'support/intercepts/vpc'; +import { ui } from 'support/ui'; +import { linodeCreatePage, vpcCreateDrawer } from 'support/ui/pages'; +import { makeFeatureFlagData } from 'support/util/feature-flags'; +import { + randomIp, + randomLabel, + randomNumber, + randomPhrase, + randomString, +} from 'support/util/random'; +import { chooseRegion } from 'support/util/regions'; + +describe('Create Linode with VPCs', () => { + // TODO Remove feature flag mocks when `linodeCreateRefactor` flag is retired. + beforeEach(() => { + mockAppendFeatureFlags({ + linodeCreateRefactor: makeFeatureFlagData(true), + }); + mockGetFeatureFlagClientstream(); + }); + + /* + * - Confirms UI flow to create a Linode with an existing VPC assigned using mock API data. + * - Confirms that VPC assignment is reflected in create summary section. + * - Confirms that outgoing API request contains expected VPC interface data. + */ + it('can assign existing VPCs during Linode Create flow', () => { + const linodeRegion = chooseRegion({ capabilities: ['VPCs'] }); + + const mockSubnet = subnetFactory.build({ + id: randomNumber(), + label: randomLabel(), + linodes: [], + ipv4: `${randomIp()}/0`, + }); + + const mockVPC = vpcFactory.build({ + id: randomNumber(), + label: randomLabel(), + region: linodeRegion.id, + subnets: [mockSubnet], + }); + + const mockLinode = linodeFactory.build({ + id: randomNumber(), + label: randomLabel(), + region: linodeRegion.id, + // + }); + + mockGetVPCs([mockVPC]).as('getVPCs'); + mockGetVPC(mockVPC).as('getVPC'); + mockCreateLinode(mockLinode).as('createLinode'); + mockGetLinodeDetails(mockLinode.id, mockLinode); + + cy.visitWithLogin('/linodes/create'); + + linodeCreatePage.setLabel(mockLinode.label); + linodeCreatePage.selectImage('Debian 11'); + linodeCreatePage.selectRegionById(linodeRegion.id); + linodeCreatePage.selectPlan('Shared CPU', 'Nanode 1 GB'); + linodeCreatePage.setRootPassword(randomString(32)); + + // Confirm that mocked VPC is shown in the Autocomplete, and then select it. + cy.findByText('Assign VPC').click().type(`${mockVPC.label}`); + + ui.autocompletePopper + .findByTitle(mockVPC.label) + .should('be.visible') + .click(); + + // Confirm that Subnet selection appears and select mock subnet. + cy.findByLabelText('Subnet').should('be.visible').type(mockSubnet.label); + + ui.autocompletePopper + .findByTitle(`${mockSubnet.label} (${mockSubnet.ipv4})`) + .should('be.visible') + .click(); + + // Confirm VPC assignment indicator is shown in Linode summary. + cy.get('[data-qa-linode-create-summary]') + .scrollIntoView() + .within(() => { + cy.findByText('VPC Assigned').should('be.visible'); + }); + + // Create Linode and confirm contents of outgoing API request payload. + ui.button + .findByTitle('Create Linode') + .should('be.visible') + .should('be.enabled') + .click(); + + cy.wait('@createLinode').then((xhr) => { + const requestPayload = xhr.request.body; + const expectedVpcInterface = requestPayload['interfaces'][0]; + + // Confirm that request payload includes VPC interface. + expect(expectedVpcInterface['vpc_id']).to.equal(mockVPC.id); + expect(expectedVpcInterface['ipv4']).to.be.an('object').that.is.empty; + expect(expectedVpcInterface['subnet_id']).to.equal(mockSubnet.id); + expect(expectedVpcInterface['purpose']).to.equal('vpc'); + }); + + // Confirm redirect to new Linode. + cy.url().should('endWith', `/linodes/${mockLinode.id}`); + // TODO Confirm whether toast notification should appear on Linode create. + }); + + /* + * - Confirms UI flow to create a Linode with a new VPC assigned using mock API data. + * - Creates a VPC and a subnet from within the Linode Create flow. + * - Confirms that Cloud responds gracefully when VPC create API request fails. + * - Confirms that outgoing API request contains correct VPC interface data. + */ + it('can assign new VPCs during Linode Create flow', () => { + const linodeRegion = chooseRegion({ capabilities: ['VPCs'] }); + + const mockErrorMessage = 'An unknown error occurred.'; + + const mockSubnet = subnetFactory.build({ + id: randomNumber(), + label: randomLabel(), + linodes: [], + ipv4: '10.0.0.0/24', + }); + + const mockVPC = vpcFactory.build({ + id: randomNumber(), + description: randomPhrase(), + label: randomLabel(), + region: linodeRegion.id, + subnets: [mockSubnet], + }); + + const mockLinode = linodeFactory.build({ + id: randomNumber(), + label: randomLabel(), + region: linodeRegion.id, + }); + + mockGetVPCs([]); + mockCreateLinode(mockLinode).as('createLinode'); + cy.visitWithLogin('/linodes/create'); + + linodeCreatePage.setLabel(mockLinode.label); + linodeCreatePage.selectImage('Debian 11'); + linodeCreatePage.selectRegionById(linodeRegion.id); + linodeCreatePage.selectPlan('Shared CPU', 'Nanode 1 GB'); + linodeCreatePage.setRootPassword(randomString(32)); + + cy.findByText('Create VPC').should('be.visible').click(); + + ui.drawer + .findByTitle('Create VPC') + .should('be.visible') + .within(() => { + vpcCreateDrawer.setLabel(mockVPC.label); + vpcCreateDrawer.setDescription(mockVPC.description); + vpcCreateDrawer.setSubnetLabel(mockSubnet.label); + vpcCreateDrawer.setSubnetIpRange(mockSubnet.ipv4!); + + // Confirm that unexpected API errors are handled gracefully upon + // failed VPC creation. + mockCreateVPCError(mockErrorMessage, 500).as('createVpc'); + vpcCreateDrawer.submit(); + + cy.wait('@createVpc'); + cy.findByText(mockErrorMessage).scrollIntoView().should('be.visible'); + + // Create VPC with successful API response mocked. + mockCreateVPC(mockVPC).as('createVpc'); + vpcCreateDrawer.submit(); + }); + + // Attempt to create Linode before selecting a VPC subnet, and confirm + // that validation error appears. + ui.button + .findByTitle('Create Linode') + .should('be.visible') + .should('be.enabled') + .click(); + + cy.findByText('Subnet is required.').should('be.visible'); + + // Confirm that Subnet selection appears and select mock subnet. + cy.findByLabelText('Subnet').should('be.visible').type(mockSubnet.label); + + ui.autocompletePopper + .findByTitle(`${mockSubnet.label} (${mockSubnet.ipv4})`) + .should('be.visible') + .click(); + + // Check box to assign public IPv4. + cy.findByText('Assign a public IPv4 address for this Linode') + .should('be.visible') + .click(); + + // Create Linode and confirm contents of outgoing API request payload. + ui.button + .findByTitle('Create Linode') + .should('be.visible') + .should('be.enabled') + .click(); + + cy.wait('@createLinode').then((xhr) => { + const requestPayload = xhr.request.body; + const expectedVpcInterface = requestPayload['interfaces'][0]; + + // Confirm that request payload includes VPC interface. + expect(expectedVpcInterface['vpc_id']).to.equal(mockVPC.id); + expect(expectedVpcInterface['ipv4']).to.deep.equal({ nat_1_1: 'any' }); + expect(expectedVpcInterface['subnet_id']).to.equal(mockSubnet.id); + expect(expectedVpcInterface['purpose']).to.equal('vpc'); + }); + + cy.url().should('endWith', `/linodes/${mockLinode.id}`); + // TODO Confirm whether toast notification should appear on Linode create. + }); + + /* + * - Confirms UI flow when attempting to assign VPC to Linode in region without capability. + * - Confirms that VPCs selection is disabled. + * - Confirms that notice text is present to explain that VPCs are unavailable. + */ + it('cannot assign VPCs to Linodes in regions without VPC capability', () => { + const mockRegion = regionFactory.build({ + capabilities: ['Linodes'], + }); + + const vpcNotAvailableMessage = + 'VPC is not available in the selected region.'; + + mockGetRegions([mockRegion]); + cy.visitWithLogin('/linodes/create'); + + linodeCreatePage.selectRegionById(mockRegion.id); + + cy.findByLabelText('Assign VPC') + .scrollIntoView() + .should('be.visible') + .should('be.disabled'); + + cy.findByText(vpcNotAvailableMessage).should('be.visible'); + }); +}); From 784cfa54160027c16cdef2e52c38b0850901ae10 Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Wed, 1 May 2024 18:35:49 -0400 Subject: [PATCH 10/25] Refactor accordion helpers to account for ReactNode accordion titles --- .../manager/cypress/support/ui/accordion.ts | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/manager/cypress/support/ui/accordion.ts b/packages/manager/cypress/support/ui/accordion.ts index 523de4ed3fa..31e8f25cd32 100644 --- a/packages/manager/cypress/support/ui/accordion.ts +++ b/packages/manager/cypress/support/ui/accordion.ts @@ -1,3 +1,23 @@ +/** + * UI helpers for accordion panel headings. + */ +export const accordionHeading = { + /** + * Finds an accordion with the given title. + * + * @param title - Title of the accordion header to find. + * + * @returns Cypress chainable. + */ + findByTitle: (title: string) => { + // We have to rely on the selector because some accordion titles contain + // other React components within them. + return cy.findByText(title, { + selector: '[data-qa-panel-subheading], [data-qa-panel-subheading] *', + }); + }, +}; + /** * UI helpers for accordion panels. */ @@ -19,6 +39,13 @@ export const accordion = { * @returns Cypress chainable. */ findByTitle: (title: string) => { - return cy.get(`[data-qa-panel="${title}"]`).find('[data-qa-panel-details]'); + // We have to rely on the selector because some accordion titles contain + // other React components within them. + return cy + .findByText(title, { + selector: '[data-qa-panel-subheading], [data-qa-panel-subheading] *', + }) + .closest('[data-qa-panel]') + .find('[data-qa-panel-details]'); }, }; From 4abe2f3623cb4d44f3aa270b55af101a0d19d3e9 Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Wed, 1 May 2024 18:40:46 -0400 Subject: [PATCH 11/25] Add WIP Linode Create w/ VLAN tests --- .../linodes/create-linode-with-vlan.spec.ts | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 packages/manager/cypress/e2e/core/linodes/create-linode-with-vlan.spec.ts diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-vlan.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-vlan.spec.ts new file mode 100644 index 00000000000..afe20fb61f3 --- /dev/null +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-vlan.spec.ts @@ -0,0 +1,67 @@ +import { regionFactory } from 'src/factories'; +import { mockGetRegions } from 'support/intercepts/regions'; +import { ui } from 'support/ui'; +import { linodeCreatePage } from 'support/ui/pages'; +import { + mockAppendFeatureFlags, + mockGetFeatureFlagClientstream, +} from 'support/intercepts/feature-flags'; +import { makeFeatureFlagData } from 'support/util/feature-flags'; + +describe('Create Linode with VLANs', () => { + // TODO Remove feature flag mocks when `linodeCreateRefactor` flag is retired. + beforeEach(() => { + mockAppendFeatureFlags({ + linodeCreateRefactor: makeFeatureFlagData(true), + }); + mockGetFeatureFlagClientstream(); + }); + + it.skip('can assign existing VLANs during Linode create flow', () => {}); + + it.skip('can assign new VLANs during Linode create flow', () => {}); + + /* + * - Uses mock API data to confirm that VLANs cannot be assigned to Linodes in regions without capability. + * - Confirms that VLAN fields are disabled before and after selecting a region. + */ + it('cannot assign VLANs in regions without capability', () => { + const availabilityNotice = + 'VLANs are currently available in select regions.'; + + const nonVlanRegion = regionFactory.build({ + capabilities: ['Linodes'], + }); + + const vlanRegion = regionFactory.build({ + capabilities: ['Linodes', 'Vlans'], + }); + + mockGetRegions([nonVlanRegion, vlanRegion]); + cy.visitWithLogin('/linodes/create'); + + // Expand VLAN accordion, confirm VLAN availability notice is displayed and + // that VLAN fields are disabled while no region is selected. + ui.accordionHeading.findByTitle('VLAN').click(); + ui.accordion + .findByTitle('VLAN') + .scrollIntoView() + .within(() => { + cy.contains(availabilityNotice).should('be.visible'); + cy.findByLabelText('VLAN').should('be.disabled'); + cy.findByLabelText(/IPAM Address/).should('be.disabled'); + }); + + // Select a region that is known not to have VLAN capability. + linodeCreatePage.selectRegionById(nonVlanRegion.id); + + // Confirm that VLAN fields are still disabled. + ui.accordion + .findByTitle('VLAN') + .scrollIntoView() + .within(() => { + cy.findByLabelText('VLAN').should('be.disabled'); + cy.findByLabelText(/IPAM Address/).should('be.disabled'); + }); + }); +}); From 030a7ed9815c766aa88480c916154a84e06313d3 Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Wed, 1 May 2024 19:37:45 -0400 Subject: [PATCH 12/25] Add WIP Linode Create w/ SSH Key and user data specs --- .../cypress/e2e/core/linodes/create-linode-with-ssh-key.spec.ts | 0 .../cypress/e2e/core/linodes/create-linode-with-user-data.spec.ts | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 packages/manager/cypress/e2e/core/linodes/create-linode-with-ssh-key.spec.ts create mode 100644 packages/manager/cypress/e2e/core/linodes/create-linode-with-user-data.spec.ts diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-ssh-key.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-ssh-key.spec.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-user-data.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-user-data.spec.ts new file mode 100644 index 00000000000..e69de29bb2d From 9cc4abfb4a967cde975145eb8b82a48d8d41f890 Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Thu, 2 May 2024 15:12:33 -0400 Subject: [PATCH 13/25] Add Linode Create w/ VLAN integration tests --- .../linodes/create-linode-with-vlan.spec.ts | 179 +++++++++++++++++- 1 file changed, 176 insertions(+), 3 deletions(-) diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-vlan.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-vlan.spec.ts index afe20fb61f3..7b3d495de94 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-vlan.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-vlan.spec.ts @@ -1,4 +1,4 @@ -import { regionFactory } from 'src/factories'; +import { linodeFactory, regionFactory, VLANFactory } from 'src/factories'; import { mockGetRegions } from 'support/intercepts/regions'; import { ui } from 'support/ui'; import { linodeCreatePage } from 'support/ui/pages'; @@ -7,6 +7,15 @@ import { mockGetFeatureFlagClientstream, } from 'support/intercepts/feature-flags'; import { makeFeatureFlagData } from 'support/util/feature-flags'; +import { chooseRegion } from 'support/util/regions'; +import { + randomIp, + randomLabel, + randomNumber, + randomString, +} from 'support/util/random'; +import { mockGetVLANs } from 'support/intercepts/vlans'; +import { mockCreateLinode } from 'support/intercepts/linodes'; describe('Create Linode with VLANs', () => { // TODO Remove feature flag mocks when `linodeCreateRefactor` flag is retired. @@ -17,9 +26,173 @@ describe('Create Linode with VLANs', () => { mockGetFeatureFlagClientstream(); }); - it.skip('can assign existing VLANs during Linode create flow', () => {}); + /* + * - Uses mock API data to confirm VLAN attachment UI flow during Linode create. + * - Confirms that outgoing Linode create API request contains expected data for VLAN. + * - Confirms that attached VLAN is reflected in the Linode create summary. + */ + it('can assign existing VLANs during Linode create flow', () => { + const mockLinodeRegion = chooseRegion({ + capabilities: ['Linodes', 'Vlans'], + }); + const mockLinode = linodeFactory.build({ + id: randomNumber(), + label: randomLabel(), + region: mockLinodeRegion.id, + }); + + const mockVlan = VLANFactory.build({ + id: randomNumber(), + label: randomLabel(), + region: mockLinodeRegion.id, + cidr_block: `${randomIp()}/24`, + linodes: [], + }); + + mockGetVLANs([mockVlan]); + mockCreateLinode(mockLinode).as('createLinode'); + cy.visitWithLogin('/linodes/create'); + + // Fill out necessary Linode create fields. + linodeCreatePage.selectRegionById(mockLinodeRegion.id); + linodeCreatePage.selectImage('Debian 11'); + linodeCreatePage.setLabel(mockLinode.label); + linodeCreatePage.selectPlan('Shared CPU', 'Nanode 1 GB'); + linodeCreatePage.setRootPassword(randomString(32)); + + // Open VLAN accordion and select existing VLAN. + ui.accordionHeading.findByTitle('VLAN').click(); + ui.accordion + .findByTitle('VLAN') + .scrollIntoView() + .should('be.visible') + .within(() => { + cy.findByLabelText('VLAN').should('be.enabled').type(mockVlan.label); + + ui.autocompletePopper + .findByTitle(mockVlan.label) + .should('be.visible') + .click(); + + cy.findByLabelText(/IPAM Address/) + .should('be.enabled') + .type(mockVlan.cidr_block); + }); + + // Confirm that VLAN attachment is listed in summary, then create Linode. + cy.get('[data-qa-linode-create-summary]') + .scrollIntoView() + .within(() => { + cy.findByText('VLAN Attached').should('be.visible'); + }); + + ui.button + .findByTitle('Create Linode') + .should('be.visible') + .should('be.enabled') + .click(); + + // Confirm outgoing API request payload has expected data. + cy.wait('@createLinode').then((xhr) => { + const requestPayload = xhr.request.body; + const expectedPublicInterface = requestPayload['interfaces'][0]; + const expectedVlanInterface = requestPayload['interfaces'][1]; + + // Confirm that first interface is for public internet. + expect(expectedPublicInterface['purpose']).to.equal('public'); + + // Confirm that second interface is our chosen VLAN. + expect(expectedVlanInterface['purpose']).to.equal('vlan'); + expect(expectedVlanInterface['label']).to.equal(mockVlan.label); + expect(expectedVlanInterface['ipam_address']).to.equal( + mockVlan.cidr_block + ); + }); + + cy.url().should('endWith', `/linodes/${mockLinode.id}`); + // TODO Confirm whether toast notification should appear on Linode create. + }); + + /* + * - Uses mock API data to confirm VLAN creation and attachment UI flow during Linode create. + * - Confirms that outgoing Linode create API request contains expected data for new VLAN. + * - Confirms that attached VLAN is reflected in the Linode create summary. + */ + it('can assign new VLANs during Linode create flow', () => { + const mockLinodeRegion = chooseRegion({ + capabilities: ['Linodes', 'Vlans'], + }); + const mockLinode = linodeFactory.build({ + id: randomNumber(), + label: randomLabel(), + region: mockLinodeRegion.id, + }); + + const mockVlan = VLANFactory.build({ + id: randomNumber(), + label: randomLabel(), + region: mockLinodeRegion.id, + cidr_block: `${randomIp()}/24`, + linodes: [], + }); + + mockGetVLANs([]); + mockCreateLinode(mockLinode).as('createLinode'); + cy.visitWithLogin('/linodes/create'); + + // Fill out necessary Linode create fields. + linodeCreatePage.selectRegionById(mockLinodeRegion.id); + linodeCreatePage.selectImage('Debian 11'); + linodeCreatePage.setLabel(mockLinode.label); + linodeCreatePage.selectPlan('Shared CPU', 'Nanode 1 GB'); + linodeCreatePage.setRootPassword(randomString(32)); + + // Open VLAN accordion and specify new VLAN. + ui.accordionHeading.findByTitle('VLAN').click(); + ui.accordion + .findByTitle('VLAN') + .scrollIntoView() + .should('be.visible') + .within(() => { + cy.findByLabelText('VLAN').should('be.enabled').type(mockVlan.label); + + ui.autocompletePopper + .findByTitle(`Create "${mockVlan.label}"`) + .should('be.visible') + .click(); + }); - it.skip('can assign new VLANs during Linode create flow', () => {}); + // Confirm that VLAN attachment is listed in summary, then create Linode. + cy.get('[data-qa-linode-create-summary]') + .scrollIntoView() + .within(() => { + cy.findByText('VLAN Attached').should('be.visible'); + }); + + ui.button + .findByTitle('Create Linode') + .should('be.visible') + .should('be.enabled') + .click(); + + // Confirm outgoing API request payload has expected data. + cy.wait('@createLinode').then((xhr) => { + const requestPayload = xhr.request.body; + const expectedPublicInterface = requestPayload['interfaces'][0]; + const expectedVlanInterface = requestPayload['interfaces'][1]; + + // Confirm that first interface is for public internet. + expect(expectedPublicInterface['purpose']).to.equal('public'); + + // Confirm that second interface is our chosen VLAN. + expect(expectedVlanInterface['purpose']).to.equal('vlan'); + expect(expectedVlanInterface['label']).to.equal(mockVlan.label); + expect(expectedVlanInterface['ipam_address']).to.equal(''); + }); + + cy.url().should('endWith', `/linodes/${mockLinode.id}`); + // TODO Confirm whether toast notification should appear on Linode create. + }); /* * - Uses mock API data to confirm that VLANs cannot be assigned to Linodes in regions without capability. From 52d6e8f8790ae0b7e1d6e86ca8f5fded092f2a92 Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Thu, 2 May 2024 15:38:22 -0400 Subject: [PATCH 14/25] Create Linode create mobile screen size tests --- .../core/linodes/create-linode-mobile.spec.ts | 82 +++++++++++++++++++ .../cypress/support/constants/environment.ts | 39 +++++++++ .../support/ui/pages/linode-create-page.ts | 21 +++++ 3 files changed, 142 insertions(+) create mode 100644 packages/manager/cypress/e2e/core/linodes/create-linode-mobile.spec.ts create mode 100644 packages/manager/cypress/support/constants/environment.ts diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-mobile.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-mobile.spec.ts new file mode 100644 index 00000000000..2e1c02808b6 --- /dev/null +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-mobile.spec.ts @@ -0,0 +1,82 @@ +/** + * @file Smoke tests for Linode Create flow across common mobile viewport sizes. + */ + +import { linodeFactory } from 'src/factories'; +import { MOBILE_VIEWPORTS } from 'support/constants/environment'; +import { linodeCreatePage } from 'support/ui/pages'; +import { randomLabel, randomNumber, randomString } from 'support/util/random'; +import { chooseRegion } from 'support/util/regions'; +import { + mockAppendFeatureFlags, + mockGetFeatureFlagClientstream, +} from 'support/intercepts/feature-flags'; +import { makeFeatureFlagData } from 'support/util/feature-flags'; +import { ui } from 'support/ui'; +import { mockCreateLinode } from 'support/intercepts/linodes'; + +describe('Linode create mobile smoke', () => { + // TODO Remove feature flag mocks when `linodeCreateRefactor` flag is retired. + beforeEach(() => { + mockAppendFeatureFlags({ + linodeCreateRefactor: makeFeatureFlagData(true), + }); + mockGetFeatureFlagClientstream(); + }); + + MOBILE_VIEWPORTS.forEach((viewport) => { + ['portrait', 'landscape'].forEach((orientation) => { + /* + * - Confirms Linode create flow can be completed on common mobile screen sizes + * - Creates a basic Nanode and confirms interactions succeed and outgoing request contains expected data. + */ + it(`can create Linode (${viewport.label}, ${orientation})`, () => { + const mockLinodeRegion = chooseRegion(); + const mockLinode = linodeFactory.build({ + id: randomNumber(), + label: randomLabel(), + region: mockLinodeRegion.id, + }); + + mockCreateLinode(mockLinode).as('createLinode'); + + cy.viewport( + orientation === 'portrait' ? viewport.width : viewport.height, + orientation === 'portrait' ? viewport.height : viewport.width + ); + cy.visitWithLogin('/linodes/create'); + + linodeCreatePage.selectImage('Debian 11'); + linodeCreatePage.selectRegionById(mockLinodeRegion.id); + linodeCreatePage.selectPlanCard('Shared CPU', 'Nanode 1 GB'); + linodeCreatePage.setLabel(mockLinode.label); + linodeCreatePage.setRootPassword(randomString(32)); + + cy.get('[data-qa-linode-create-summary]') + .scrollIntoView() + .within(() => { + cy.findByText('Nanode 1 GB').should('be.visible'); + cy.findByText('Debian 11').should('be.visible'); + cy.findByText(mockLinodeRegion.label).should('be.visible'); + }); + + ui.button + .findByTitle('Create Linode') + .should('be.visible') + .should('be.enabled') + .click(); + + cy.wait('@createLinode').then((xhr) => { + const requestBody = xhr.request.body; + + expect(requestBody['image']).to.equal('linode/debian11'); + expect(requestBody['label']).to.equal(mockLinode.label); + expect(requestBody['region']).to.equal(mockLinodeRegion.id); + expect(requestBody['type']).to.equal('g6-nanode-1'); + }); + + cy.url().should('endWith', `/linodes/${mockLinode.id}`); + }); + }); + }); +}); diff --git a/packages/manager/cypress/support/constants/environment.ts b/packages/manager/cypress/support/constants/environment.ts new file mode 100644 index 00000000000..c901359e127 --- /dev/null +++ b/packages/manager/cypress/support/constants/environment.ts @@ -0,0 +1,39 @@ +/** + * @file Constants related to test environment. + */ + +export interface ViewportSize { + width: number; + height: number; + label?: string; +} + +// Array of common mobile viewports against which to test. +export const MOBILE_VIEWPORTS: ViewportSize[] = [ + { + // iPhone 6, 7, 8, SE2, etc. + label: 'iPhone 8', + width: 375, + height: 667, + }, + { + // iPhone 14 Pro, iPhone 15, iPhone 15 Pro, etc. + label: 'iPhone 15', + width: 393, + height: 852, + }, + { + // iPhone 15 Pro Max + label: 'iPhone 15 Pro Max', + width: 430, + height: 932, + }, + { + // Galaxy S22 + label: 'Samsung Galaxy S22', + width: 360, + height: 780, + }, + // TODO Evaluate what devices to include here and how long to allow this list to be. Tablets? + // Do we want to keep this short, or make it long and just choose a random subset each time we do mobile testing? +]; diff --git a/packages/manager/cypress/support/ui/pages/linode-create-page.ts b/packages/manager/cypress/support/ui/pages/linode-create-page.ts index 99d15de94e6..2faac477492 100644 --- a/packages/manager/cypress/support/ui/pages/linode-create-page.ts +++ b/packages/manager/cypress/support/ui/pages/linode-create-page.ts @@ -58,6 +58,8 @@ export const linodeCreatePage = { /** * Select the given Linode plan. * + * Assumes that plans are displayed in a table. + * * @param planTabTitle - Title of tab where desired plan is located. * @param planTitle - Title of desired plan. */ @@ -69,4 +71,23 @@ export const linodeCreatePage = { .click(); }); }, + + /** + * Select the given Linode plan selection card. + * + * Useful for testing Linode create page against mobile viewports. + * + * Assumes that plans are displayed as selection cards. + */ + selectPlanCard: (planTabTitle: string, planTitle: string) => { + ui.tabList.findTabByTitle(planTabTitle).click(); + ui.tabList.findTabPanelByTitle(planTabTitle).within(() => { + cy.findByText(planTitle) + .should('be.visible') + .as('selectionCard') + .scrollIntoView(); + + cy.get('@selectionCard').click(); + }); + }, }; From 54c3838913c9ace6c576ee441a35b57632f02a42 Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Tue, 14 May 2024 10:25:25 -0400 Subject: [PATCH 15/25] Update Linode Create plan panel selection to account for recent refactor --- .../manager/cypress/support/ui/pages/linode-create-page.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/manager/cypress/support/ui/pages/linode-create-page.ts b/packages/manager/cypress/support/ui/pages/linode-create-page.ts index 2faac477492..eaca0557bf3 100644 --- a/packages/manager/cypress/support/ui/pages/linode-create-page.ts +++ b/packages/manager/cypress/support/ui/pages/linode-create-page.ts @@ -66,7 +66,8 @@ export const linodeCreatePage = { selectPlan: (planTabTitle: string, planTitle: string) => { ui.tabList.findTabByTitle(planTabTitle).click(); ui.tabList.findTabPanelByTitle(planTabTitle).within(() => { - cy.findByLabelText(planTitle, { selector: 'tr' }) + cy.get(`[data-qa-plan-row="${planTitle}"]`) + .closest('tr') .should('be.visible') .click(); }); From c13bea7866a9a1742eb05de455234c4abea0c32d Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Tue, 14 May 2024 10:26:11 -0400 Subject: [PATCH 16/25] Add WIP Linode Create user data UI tests --- .../create-linode-with-user-data.spec.ts | 76 +++++++++++++++++++ .../user-data/user-data-config-basic.yml | 14 ++++ 2 files changed, 90 insertions(+) create mode 100644 packages/manager/cypress/fixtures/user-data/user-data-config-basic.yml diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-user-data.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-user-data.spec.ts index e69de29bb2d..d1baeb9ab38 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-user-data.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-user-data.spec.ts @@ -0,0 +1,76 @@ +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { makeFeatureFlagData } from 'support/util/feature-flags'; +import { mockGetFeatureFlagClientstream } from 'support/intercepts/feature-flags'; +import { chooseRegion } from 'support/util/regions'; +import { linodeFactory } from 'src/factories'; +import { randomLabel, randomNumber, randomString } from 'support/util/random'; +import { linodeCreatePage } from 'support/ui/pages'; +import { mockCreateLinode } from 'support/intercepts/linodes'; +import { mockGetLinodeDetails } from 'support/intercepts/linodes'; +import { ui } from 'support/ui'; + +describe('Create Linode with user data', () => { + beforeEach(() => { + mockAppendFeatureFlags({ + linodeCreateRefactor: makeFeatureFlagData(true), + }); + mockGetFeatureFlagClientstream(); + }); + + /* + * - Confirms UI flow to create a Linode with cloud-init user data specified. + * - (TEMPORARILY DISABLED) Confirms that outgoing API request contains expected user data payload. + */ + it('can specify user data during Linode Create flow', () => { + const linodeRegion = chooseRegion({ + capabilities: ['Linodes', 'Metadata'], + }); + const mockLinode = linodeFactory.build({ + id: randomNumber(), + label: randomLabel(), + region: linodeRegion.id, + }); + const userDataFixturePath = 'user-data/user-data-config-basic.yml'; + + mockCreateLinode(mockLinode).as('createLinode'); + mockGetLinodeDetails(mockLinode.id, mockLinode); + + cy.visitWithLogin('/linodes/create'); + + linodeCreatePage.setLabel(mockLinode.label); + linodeCreatePage.selectImage('Debian 11'); + linodeCreatePage.selectRegionById(linodeRegion.id); + linodeCreatePage.selectPlan('Shared CPU', 'Nanode 1 GB'); + linodeCreatePage.setRootPassword(randomString(32)); + + ui.accordionHeading + .findByTitle('Add User Data') + .should('be.visible') + .click(); + + cy.fixture(userDataFixturePath).then((userDataContents) => { + ui.accordion.findByTitle('Add User Data').within(() => { + cy.findByText('User Data').click(); + + cy.focused().type(userDataContents); + }); + + ui.button + .findByTitle('Create Linode') + .should('be.visible') + .should('be.enabled') + .click(); + + cy.wait('@createLinode').then((xhr) => { + // TODO Restore assertions to confirm user data payload. + // May be fixed after PR #10442 is merged. + // const requestPayload = xhr.request.body; + // expect(requestPayload['metadata']['user_data']).to.equal(btoa(userDataContents)); + }); + }); + }); + + it('cannot specify user data when selected region does not support it', () => {}); + + it('cannot specify user data when selected image does not support it', () => {}); +}); diff --git a/packages/manager/cypress/fixtures/user-data/user-data-config-basic.yml b/packages/manager/cypress/fixtures/user-data/user-data-config-basic.yml new file mode 100644 index 00000000000..b93d44c22a6 --- /dev/null +++ b/packages/manager/cypress/fixtures/user-data/user-data-config-basic.yml @@ -0,0 +1,14 @@ +#cloud-config + +# Sample cloud-init config data file. +# See also: https://cloudinit.readthedocs.io/en/latest/explanation/format.html +# +# This config specifies a group will be created named 'foo-group', and that +# a user will be created named 'foo' whose primary group is 'foo-group'. + +groups: + - foo-group + +users: + - name: foo + primary_group: foo-group From ad2dfc005bfda38aeac820e7052e643028a9123d Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Tue, 14 May 2024 12:47:38 -0400 Subject: [PATCH 17/25] Restore user data payload assertion, remove unneeded comments from fixture --- .../core/linodes/create-linode-with-user-data.spec.ts | 10 +++++----- .../fixtures/user-data/user-data-config-basic.yml | 3 --- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-user-data.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-user-data.spec.ts index d1baeb9ab38..8c0a78bfee4 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-user-data.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-user-data.spec.ts @@ -19,7 +19,7 @@ describe('Create Linode with user data', () => { /* * - Confirms UI flow to create a Linode with cloud-init user data specified. - * - (TEMPORARILY DISABLED) Confirms that outgoing API request contains expected user data payload. + * - Confirms that outgoing API request contains expected user data payload. */ it('can specify user data during Linode Create flow', () => { const linodeRegion = chooseRegion({ @@ -62,10 +62,10 @@ describe('Create Linode with user data', () => { .click(); cy.wait('@createLinode').then((xhr) => { - // TODO Restore assertions to confirm user data payload. - // May be fixed after PR #10442 is merged. - // const requestPayload = xhr.request.body; - // expect(requestPayload['metadata']['user_data']).to.equal(btoa(userDataContents)); + const requestPayload = xhr.request.body; + expect(requestPayload['metadata']['user_data']).to.equal( + btoa(userDataContents) + ); }); }); }); diff --git a/packages/manager/cypress/fixtures/user-data/user-data-config-basic.yml b/packages/manager/cypress/fixtures/user-data/user-data-config-basic.yml index b93d44c22a6..b8bc4e3163e 100644 --- a/packages/manager/cypress/fixtures/user-data/user-data-config-basic.yml +++ b/packages/manager/cypress/fixtures/user-data/user-data-config-basic.yml @@ -2,9 +2,6 @@ # Sample cloud-init config data file. # See also: https://cloudinit.readthedocs.io/en/latest/explanation/format.html -# -# This config specifies a group will be created named 'foo-group', and that -# a user will be created named 'foo' whose primary group is 'foo-group'. groups: - foo-group From 472423473245898975d4c2ec9459fe1357e63564 Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Tue, 14 May 2024 13:12:21 -0400 Subject: [PATCH 18/25] Refactor `mockGetImage` to be more flexible --- .../cypress/support/intercepts/images.ts | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/packages/manager/cypress/support/intercepts/images.ts b/packages/manager/cypress/support/intercepts/images.ts index 244e969867a..a9a38804c06 100644 --- a/packages/manager/cypress/support/intercepts/images.ts +++ b/packages/manager/cypress/support/intercepts/images.ts @@ -2,12 +2,12 @@ * @file Cypress intercepts and mocks for Image API requests. */ -import { imageFactory } from '@src/factories'; import { apiMatcher } from 'support/util/intercepts'; import { paginateResponse } from 'support/util/paginate'; import { getFilters } from 'support/util/request'; -import type { Image, ImageStatus } from '@linode/api-v4'; +import type { Image } from '@linode/api-v4'; +import { makeResponse } from 'support/util/response'; /** * Intercepts POST request to create a machine image and mocks the response. @@ -92,20 +92,15 @@ export const mockGetRecoveryImages = ( * @returns Cypress chainable. */ export const mockGetImage = ( - label: string, - id: string, - status: ImageStatus + imageId: string, + image: Image ): Cypress.Chainable => { - const encodedId = encodeURIComponent(id); - return cy.intercept('GET', apiMatcher(`images/${encodedId}*`), (req) => { - return req.reply( - imageFactory.build({ - id, - label, - status, - }) - ); - }); + const encodedId = encodeURIComponent(imageId); + return cy.intercept( + 'GET', + apiMatcher(`images/${encodedId}*`), + makeResponse(image) + ); }; /** From 053ebb0faafe609d72ca50aab19d964fecdc2871 Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Tue, 14 May 2024 13:17:45 -0400 Subject: [PATCH 19/25] Add user data tests for Images and Regions lacking cloud-init capability --- .../create-linode-with-user-data.spec.ts | 95 ++++++++++++++++--- 1 file changed, 84 insertions(+), 11 deletions(-) diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-user-data.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-user-data.spec.ts index 8c0a78bfee4..21096becdf3 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-user-data.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-user-data.spec.ts @@ -1,13 +1,19 @@ -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { imageFactory, linodeFactory, regionFactory } from 'src/factories'; +import { + mockAppendFeatureFlags, + mockGetFeatureFlagClientstream, +} from 'support/intercepts/feature-flags'; +import { mockGetAllImages, mockGetImage } from 'support/intercepts/images'; +import { + mockCreateLinode, + mockGetLinodeDetails, +} from 'support/intercepts/linodes'; +import { mockGetRegions } from 'support/intercepts/regions'; +import { ui } from 'support/ui'; +import { linodeCreatePage } from 'support/ui/pages'; import { makeFeatureFlagData } from 'support/util/feature-flags'; -import { mockGetFeatureFlagClientstream } from 'support/intercepts/feature-flags'; -import { chooseRegion } from 'support/util/regions'; -import { linodeFactory } from 'src/factories'; import { randomLabel, randomNumber, randomString } from 'support/util/random'; -import { linodeCreatePage } from 'support/ui/pages'; -import { mockCreateLinode } from 'support/intercepts/linodes'; -import { mockGetLinodeDetails } from 'support/intercepts/linodes'; -import { ui } from 'support/ui'; +import { chooseRegion } from 'support/util/regions'; describe('Create Linode with user data', () => { beforeEach(() => { @@ -37,12 +43,15 @@ describe('Create Linode with user data', () => { cy.visitWithLogin('/linodes/create'); + // Fill out create form, selecting a region and image that both have + // cloud-init capabilities. linodeCreatePage.setLabel(mockLinode.label); linodeCreatePage.selectImage('Debian 11'); linodeCreatePage.selectRegionById(linodeRegion.id); linodeCreatePage.selectPlan('Shared CPU', 'Nanode 1 GB'); linodeCreatePage.setRootPassword(randomString(32)); + // Expand "Add User Data" accordion and enter user data config. ui.accordionHeading .findByTitle('Add User Data') .should('be.visible') @@ -51,10 +60,11 @@ describe('Create Linode with user data', () => { cy.fixture(userDataFixturePath).then((userDataContents) => { ui.accordion.findByTitle('Add User Data').within(() => { cy.findByText('User Data').click(); - cy.focused().type(userDataContents); }); + // Submit form to create Linode and confirm that outgoing API request + // contains expected user data. ui.button .findByTitle('Create Linode') .should('be.visible') @@ -70,7 +80,70 @@ describe('Create Linode with user data', () => { }); }); - it('cannot specify user data when selected region does not support it', () => {}); + /* + * - Confirms UI flow when creating a Linode using a region that lacks cloud-init capability. + * - Confirms that "Add User Data" section is hidden when selected region lacks cloud-init. + */ + it('cannot specify user data when selected region does not support it', () => { + const mockLinodeRegion = regionFactory.build({ + capabilities: ['Linodes'], + }); - it('cannot specify user data when selected image does not support it', () => {}); + const mockLinode = linodeFactory.build({ + id: randomNumber(), + label: randomLabel(), + region: mockLinodeRegion.id, + }); + + mockGetRegions([mockLinodeRegion]); + + cy.visitWithLogin('/linodes/create'); + + linodeCreatePage.setLabel(mockLinode.label); + linodeCreatePage.selectImage('Debian 11'); + linodeCreatePage.selectRegionById(mockLinodeRegion.id); + linodeCreatePage.selectPlan('Shared CPU', 'Nanode 1 GB'); + + // Confirm that "Add User Data" section is hidden when selected region + // lacks cloud-init capability. + cy.findByText('Add User Data').should('not.exist'); + }); + + /* + * - Confirms UI flow when creating a Linode using an image that lacks cloud-init capability. + * - Confirms that "Add User Data" section is hidden when selected image lacks cloud-init. + */ + it('cannot specify user data when selected image does not support it', () => { + const linodeRegion = chooseRegion({ + capabilities: ['Linodes', 'Metadata'], + }); + const mockLinode = linodeFactory.build({ + id: randomNumber(), + label: randomLabel(), + region: linodeRegion.id, + }); + const mockImage = imageFactory.build({ + id: `linode/${randomLabel()}`, + label: randomLabel(), + created_by: 'linode', + is_public: true, + vendor: 'Debian', + // `cloud-init` is omitted from Image capabilities. + capabilities: [], + }); + + mockGetImage(mockImage.id, mockImage); + mockGetAllImages([mockImage]); + + cy.visitWithLogin('/linodes/create'); + + linodeCreatePage.setLabel(mockLinode.label); + linodeCreatePage.selectImage(mockImage.label); + linodeCreatePage.selectRegionById(linodeRegion.id); + linodeCreatePage.selectPlan('Shared CPU', 'Nanode 1 GB'); + + // Confirm that "Add User Data" section is hidden when selected image + // lacks cloud-init capability. + cy.findByText('Add User Data').should('not.exist'); + }); }); From 93d36a70b3f839de037417f316ffe3c171bec9c1 Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Tue, 14 May 2024 15:17:41 -0400 Subject: [PATCH 20/25] Add tests and mock utils for Linode create SSH key flows --- .../create-linode-with-ssh-key.spec.ts | 186 ++++++++++++++++++ .../cypress/support/intercepts/profile.ts | 46 +++++ 2 files changed, 232 insertions(+) diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-ssh-key.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-ssh-key.spec.ts index e69de29bb2d..07a04310671 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-ssh-key.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-ssh-key.spec.ts @@ -0,0 +1,186 @@ +import { + accountUserFactory, + linodeFactory, + sshKeyFactory, +} from 'src/factories'; +import { + mockAppendFeatureFlags, + mockGetFeatureFlagClientstream, +} from 'support/intercepts/feature-flags'; +import { makeFeatureFlagData } from 'support/util/feature-flags'; +import { randomLabel, randomNumber, randomString } from 'support/util/random'; +import { chooseRegion } from 'support/util/regions'; +import { mockGetUser, mockGetUsers } from 'support/intercepts/account'; +import { mockCreateLinode } from 'support/intercepts/linodes'; +import { linodeCreatePage } from 'support/ui/pages'; +import { ui } from 'support/ui'; +import { mockCreateSSHKey } from 'support/intercepts/profile'; + +describe('Create Linode with SSH Key', () => { + // TODO Remove feature flag mocks when `linodeCreateRefactor` flag is retired. + beforeEach(() => { + mockAppendFeatureFlags({ + linodeCreateRefactor: makeFeatureFlagData(true), + }); + mockGetFeatureFlagClientstream(); + }); + + /* + * - Confirms UI flow when creating a Linode with an authorized SSH key. + * - Confirms that existing SSH keys are listed on page and can be selected. + * - Confirms that outgoing Linode create API request contains authorized user for chosen key. + */ + it('can add an existing SSH key during Linode create flow', () => { + const linodeRegion = chooseRegion(); + const mockLinode = linodeFactory.build({ + id: randomNumber(), + label: randomLabel(), + region: linodeRegion.id, + }); + + const mockSshKey = sshKeyFactory.build({ + label: randomLabel(), + }); + + const mockUser = accountUserFactory.build({ + username: randomLabel(), + ssh_keys: [mockSshKey.label], + }); + + mockGetUsers([mockUser]); + mockGetUser(mockUser); + mockCreateLinode(mockLinode).as('createLinode'); + + cy.visitWithLogin('/linodes/create'); + + linodeCreatePage.setLabel(mockLinode.label); + linodeCreatePage.selectImage('Debian 11'); + linodeCreatePage.selectRegionById(linodeRegion.id); + linodeCreatePage.selectPlan('Shared CPU', 'Nanode 1 GB'); + linodeCreatePage.setRootPassword(randomString(32)); + + // Confirm that SSH key is listed, then select it. + cy.findByText(mockSshKey.label) + .scrollIntoView() + .should('be.visible') + .closest('tr') + .within(() => { + cy.findByText(mockUser.username); + cy.findByLabelText(`Enable SSH for ${mockUser.username}`).click(); + }); + + // Click "Create Linode" button and confirm outgoing request data. + ui.button + .findByTitle('Create Linode') + .should('be.visible') + .should('be.enabled') + .click(); + + // Confirm that outgoing Linode create request contains authorized user that + // corresponds to the selected SSH key. + cy.wait('@createLinode').then((xhr) => { + const requestPayload = xhr.request.body; + expect(requestPayload['authorized_users'][0]).to.equal(mockUser.username); + }); + }); + + /* + * - Confirms UI flow when creating and selecting an SSH key during Linode create flow. + * - Confirms that new SSH key is automatically shown in Linode create page. + * - Confirms that outgoing Linode create API request contains authorized user for new key. + */ + it('can add a new SSH key during Linode create flow', () => { + const linodeRegion = chooseRegion(); + const mockLinode = linodeFactory.build({ + id: randomNumber(), + label: randomLabel(), + region: linodeRegion.id, + }); + + const mockSshKey = sshKeyFactory.build({ + label: randomLabel(), + ssh_key: `ssh-rsa ${randomString(16)}`, + }); + + const mockUser = accountUserFactory.build({ + username: randomLabel(), + ssh_keys: [], + }); + + const mockUserWithKey = { + ...mockUser, + ssh_keys: [mockSshKey.label], + }; + + mockGetUser(mockUser); + mockGetUsers([mockUser]); + mockCreateLinode(mockLinode).as('createLinode'); + mockCreateSSHKey(mockSshKey).as('createSSHKey'); + + cy.visitWithLogin('/linodes/create'); + + linodeCreatePage.setLabel(mockLinode.label); + linodeCreatePage.selectImage('Debian 11'); + linodeCreatePage.selectRegionById(linodeRegion.id); + linodeCreatePage.selectPlan('Shared CPU', 'Nanode 1 GB'); + linodeCreatePage.setRootPassword(randomString(32)); + + // Confirm that no SSH keys are listed for the mocked user. + cy.findByText(mockUser.username) + .scrollIntoView() + .should('be.visible') + .closest('tr') + .within(() => { + cy.findByText('None').should('be.visible'); + cy.findByLabelText(`Enable SSH for ${mockUser.username}`).should( + 'be.disabled' + ); + }); + + // Click "Add an SSH Key" and enter a label and the public key, then submit. + ui.button + .findByTitle('Add an SSH Key') + .should('be.visible') + .should('be.enabled') + .click(); + + mockGetUsers([mockUserWithKey]).as('refetchUsers'); + ui.drawer + .findByTitle('Add SSH Key') + .should('be.visible') + .within(() => { + cy.findByLabelText('Label').type(mockSshKey.label); + cy.findByLabelText('SSH Public Key').type(mockSshKey.ssh_key); + ui.button + .findByTitle('Add Key') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + cy.wait(['@createSSHKey', '@refetchUsers']); + + // Confirm that the new SSH key is listed, and select it to be added to the Linode. + cy.findByText(mockSshKey.label) + .scrollIntoView() + .should('be.visible') + .closest('tr') + .within(() => { + cy.findByLabelText(`Enable SSH for ${mockUser.username}`).click(); + }); + + // Click "Create Linode" button and confirm outgoing request data. + ui.button + .findByTitle('Create Linode') + .should('be.visible') + .should('be.enabled') + .click(); + + // Confirm that outgoing Linode create request contains authorized user that + // corresponds to the new SSH key. + cy.wait('@createLinode').then((xhr) => { + const requestPayload = xhr.request.body; + expect(requestPayload['authorized_users'][0]).to.equal(mockUser.username); + }); + }); +}); diff --git a/packages/manager/cypress/support/intercepts/profile.ts b/packages/manager/cypress/support/intercepts/profile.ts index 3ed9a751483..f9053602749 100644 --- a/packages/manager/cypress/support/intercepts/profile.ts +++ b/packages/manager/cypress/support/intercepts/profile.ts @@ -13,6 +13,7 @@ import type { Profile, SecurityQuestionsData, SecurityQuestionsPayload, + SSHKey, Token, UserPreferences, } from '@linode/api-v4'; @@ -388,3 +389,48 @@ export const mockResetOAuthApps = ( oauthApp ); }; + +/** + * Intercepts GET request to fetch SSH keys and mocks the response. + * + * @param sshKeys - Array of SSH key objects with which to mock response. + * + * @returns Cypress chainable. + */ +export const mockGetSSHKeys = (sshKeys: SSHKey[]): Cypress.Chainable => { + return cy.intercept( + 'GET', + apiMatcher('/profile/sshkeys*'), + paginateResponse(sshKeys) + ); +}; + +/** + * Intercepts GET request to fetch an SSH key and mocks the response. + * + * @param sshKey - SSH key object with which to mock response. + * + * @returns Cypress chainable. + */ +export const mockGetSSHKey = (sshKey: SSHKey): Cypress.Chainable => { + return cy.intercept( + 'GET', + apiMatcher(`/profile/sshkeys/${sshKey.id}`), + makeResponse(sshKey) + ); +}; + +/** + * Intercepts POST request to create an SSH key and mocks the response. + * + * @param sshKey - SSH key object with which to mock response. + * + * @returns Cypress chainable. + */ +export const mockCreateSSHKey = (sshKey: SSHKey): Cypress.Chainable => { + return cy.intercept( + 'POST', + apiMatcher('/profile/sshkeys'), + makeResponse(sshKey) + ); +}; From e8b88f88be948ad35c67d924e8781f70c3cf4214 Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Tue, 14 May 2024 16:01:44 -0400 Subject: [PATCH 21/25] Mock Linode create feature flag to be off for legacy create tests --- .../e2e/core/linodes/legacy-create-linode.spec.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/manager/cypress/e2e/core/linodes/legacy-create-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/legacy-create-linode.spec.ts index f790b0981b1..b0f8401401f 100644 --- a/packages/manager/cypress/e2e/core/linodes/legacy-create-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/legacy-create-linode.spec.ts @@ -2,7 +2,7 @@ * @file Integration tests and end-to-end tests for legacy Linode Create flow. */ // TODO Delete this test file when `linodeCreateRefactor` feature flag is retired. -// Move out any tests (e.g. region select test) that aren't covered by new tests. +// Move out any tests (e.g. region select test) for flows that aren't covered by new tests in the meantime. import { containsVisible, @@ -84,6 +84,13 @@ describe('create linode', () => { cleanUp('linodes'); }); + beforeEach(() => { + mockAppendFeatureFlags({ + linodeCreateRefactor: makeFeatureFlagData(false), + }); + mockGetFeatureFlagClientstream(); + }); + /* * Region select test. * From e26594da3a004a5597eb62df2968b08b4a75406f Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Tue, 14 May 2024 16:18:31 -0400 Subject: [PATCH 22/25] Add changesets --- packages/manager/.changeset/pr-10469-tests-1715717780880.md | 5 +++++ packages/manager/.changeset/pr-10469-tests-1715717804841.md | 5 +++++ packages/manager/.changeset/pr-10469-tests-1715717849362.md | 5 +++++ packages/manager/.changeset/pr-10469-tests-1715717865422.md | 5 +++++ packages/manager/.changeset/pr-10469-tests-1715717889337.md | 5 +++++ 5 files changed, 25 insertions(+) create mode 100644 packages/manager/.changeset/pr-10469-tests-1715717780880.md create mode 100644 packages/manager/.changeset/pr-10469-tests-1715717804841.md create mode 100644 packages/manager/.changeset/pr-10469-tests-1715717849362.md create mode 100644 packages/manager/.changeset/pr-10469-tests-1715717865422.md create mode 100644 packages/manager/.changeset/pr-10469-tests-1715717889337.md diff --git a/packages/manager/.changeset/pr-10469-tests-1715717780880.md b/packages/manager/.changeset/pr-10469-tests-1715717780880.md new file mode 100644 index 00000000000..9ab45c98475 --- /dev/null +++ b/packages/manager/.changeset/pr-10469-tests-1715717780880.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Add Linode Create v2 end-to-end tests ([#10469](https://github.com/linode/manager/pull/10469)) diff --git a/packages/manager/.changeset/pr-10469-tests-1715717804841.md b/packages/manager/.changeset/pr-10469-tests-1715717804841.md new file mode 100644 index 00000000000..8baca1f3156 --- /dev/null +++ b/packages/manager/.changeset/pr-10469-tests-1715717804841.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Add Linode Create v2 integration tests for VLAN flows ([#10469](https://github.com/linode/manager/pull/10469)) diff --git a/packages/manager/.changeset/pr-10469-tests-1715717849362.md b/packages/manager/.changeset/pr-10469-tests-1715717849362.md new file mode 100644 index 00000000000..3d0b7fb4c80 --- /dev/null +++ b/packages/manager/.changeset/pr-10469-tests-1715717849362.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Add Linode Create v2 integration tests for VPC flows ([#10469](https://github.com/linode/manager/pull/10469)) diff --git a/packages/manager/.changeset/pr-10469-tests-1715717865422.md b/packages/manager/.changeset/pr-10469-tests-1715717865422.md new file mode 100644 index 00000000000..1ee3be6625a --- /dev/null +++ b/packages/manager/.changeset/pr-10469-tests-1715717865422.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Add Linode Create v2 integration tests for SSH key flows ([#10469](https://github.com/linode/manager/pull/10469)) diff --git a/packages/manager/.changeset/pr-10469-tests-1715717889337.md b/packages/manager/.changeset/pr-10469-tests-1715717889337.md new file mode 100644 index 00000000000..6f5ee7bf11e --- /dev/null +++ b/packages/manager/.changeset/pr-10469-tests-1715717889337.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Add Linode Create v2 integration tests for cloud-init flows ([#10469](https://github.com/linode/manager/pull/10469)) From 167ee2c8aa60ad9797f8b55e5cc5bce8e855a09d Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Tue, 14 May 2024 17:45:54 -0400 Subject: [PATCH 23/25] Undo `ui.tabList` changes and rework Linode create page interactions; remove unused `data-qa-` attributes --- .../cypress/support/ui/pages/linode-create-page.ts | 8 ++++---- packages/manager/cypress/support/ui/tab-list.ts | 13 +------------ .../src/components/TabbedPanel/TabbedPanel.tsx | 6 +----- 3 files changed, 6 insertions(+), 21 deletions(-) diff --git a/packages/manager/cypress/support/ui/pages/linode-create-page.ts b/packages/manager/cypress/support/ui/pages/linode-create-page.ts index eaca0557bf3..1f24a0899ca 100644 --- a/packages/manager/cypress/support/ui/pages/linode-create-page.ts +++ b/packages/manager/cypress/support/ui/pages/linode-create-page.ts @@ -64,8 +64,8 @@ export const linodeCreatePage = { * @param planTitle - Title of desired plan. */ selectPlan: (planTabTitle: string, planTitle: string) => { - ui.tabList.findTabByTitle(planTabTitle).click(); - ui.tabList.findTabPanelByTitle(planTabTitle).within(() => { + cy.get('[data-qa-tp="Linode Plan"]').within(() => { + ui.tabList.findTabByTitle(planTabTitle).click(); cy.get(`[data-qa-plan-row="${planTitle}"]`) .closest('tr') .should('be.visible') @@ -81,8 +81,8 @@ export const linodeCreatePage = { * Assumes that plans are displayed as selection cards. */ selectPlanCard: (planTabTitle: string, planTitle: string) => { - ui.tabList.findTabByTitle(planTabTitle).click(); - ui.tabList.findTabPanelByTitle(planTabTitle).within(() => { + cy.get('[data-qa-tp="Linode Plan"]').within(() => { + ui.tabList.findTabByTitle(planTabTitle).click(); cy.findByText(planTitle) .should('be.visible') .as('selectionCard') diff --git a/packages/manager/cypress/support/ui/tab-list.ts b/packages/manager/cypress/support/ui/tab-list.ts index a24cd91bd5c..dd4816696dd 100644 --- a/packages/manager/cypress/support/ui/tab-list.ts +++ b/packages/manager/cypress/support/ui/tab-list.ts @@ -25,17 +25,6 @@ export const tabList = { tabTitle: string, options?: SelectorMatcherOptions ): Cypress.Chainable => { - return cy.get(`[data-qa-tab="${tabTitle}"]`); - }, - - /** - * Finds a tab panel within a tab list by its title. - * - * @param tabTitle - Title of tab for which to find panel. - * - * @returns Cypress chainable. - */ - findTabPanelByTitle: (tabTitle: string): Cypress.Chainable => { - return cy.get(`[data-qa-tab-panel="${tabTitle}"]`); + return cy.get('[data-reach-tab-list]').findByText(tabTitle, options); }, }; diff --git a/packages/manager/src/components/TabbedPanel/TabbedPanel.tsx b/packages/manager/src/components/TabbedPanel/TabbedPanel.tsx index 1612183fce2..23bc8782e0a 100644 --- a/packages/manager/src/components/TabbedPanel/TabbedPanel.tsx +++ b/packages/manager/src/components/TabbedPanel/TabbedPanel.tsx @@ -101,7 +101,6 @@ const TabbedPanel = React.memo((props: TabbedPanelProps) => { {tabs.map((tab, idx) => ( @@ -118,10 +117,7 @@ const TabbedPanel = React.memo((props: TabbedPanelProps) => { {tabs.map((tab, idx) => ( - + {tab.render(rest.children)} ))} From 5d66f93dbb82b5af3da4d229fc0227517d0a1a23 Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Mon, 20 May 2024 14:45:29 -0400 Subject: [PATCH 24/25] Only test iPhone portrait viewport for mobile tests as experiment --- .../core/linodes/create-linode-mobile.spec.ts | 83 +++++++++---------- .../cypress/support/constants/environment.ts | 18 ---- 2 files changed, 39 insertions(+), 62 deletions(-) diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-mobile.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-mobile.spec.ts index 2e1c02808b6..bf7774cc0bd 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-mobile.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-mobile.spec.ts @@ -25,58 +25,53 @@ describe('Linode create mobile smoke', () => { }); MOBILE_VIEWPORTS.forEach((viewport) => { - ['portrait', 'landscape'].forEach((orientation) => { - /* - * - Confirms Linode create flow can be completed on common mobile screen sizes - * - Creates a basic Nanode and confirms interactions succeed and outgoing request contains expected data. - */ - it(`can create Linode (${viewport.label}, ${orientation})`, () => { - const mockLinodeRegion = chooseRegion(); - const mockLinode = linodeFactory.build({ - id: randomNumber(), - label: randomLabel(), - region: mockLinodeRegion.id, - }); - - mockCreateLinode(mockLinode).as('createLinode'); + /* + * - Confirms Linode create flow can be completed on common mobile screen sizes + * - Creates a basic Nanode and confirms interactions succeed and outgoing request contains expected data. + */ + it(`can create Linode (${viewport.label})`, () => { + const mockLinodeRegion = chooseRegion(); + const mockLinode = linodeFactory.build({ + id: randomNumber(), + label: randomLabel(), + region: mockLinodeRegion.id, + }); - cy.viewport( - orientation === 'portrait' ? viewport.width : viewport.height, - orientation === 'portrait' ? viewport.height : viewport.width - ); - cy.visitWithLogin('/linodes/create'); + mockCreateLinode(mockLinode).as('createLinode'); - linodeCreatePage.selectImage('Debian 11'); - linodeCreatePage.selectRegionById(mockLinodeRegion.id); - linodeCreatePage.selectPlanCard('Shared CPU', 'Nanode 1 GB'); - linodeCreatePage.setLabel(mockLinode.label); - linodeCreatePage.setRootPassword(randomString(32)); + cy.viewport(viewport.width, viewport.height); + cy.visitWithLogin('/linodes/create'); - cy.get('[data-qa-linode-create-summary]') - .scrollIntoView() - .within(() => { - cy.findByText('Nanode 1 GB').should('be.visible'); - cy.findByText('Debian 11').should('be.visible'); - cy.findByText(mockLinodeRegion.label).should('be.visible'); - }); + linodeCreatePage.selectImage('Debian 11'); + linodeCreatePage.selectRegionById(mockLinodeRegion.id); + linodeCreatePage.selectPlanCard('Shared CPU', 'Nanode 1 GB'); + linodeCreatePage.setLabel(mockLinode.label); + linodeCreatePage.setRootPassword(randomString(32)); - ui.button - .findByTitle('Create Linode') - .should('be.visible') - .should('be.enabled') - .click(); + cy.get('[data-qa-linode-create-summary]') + .scrollIntoView() + .within(() => { + cy.findByText('Nanode 1 GB').should('be.visible'); + cy.findByText('Debian 11').should('be.visible'); + cy.findByText(mockLinodeRegion.label).should('be.visible'); + }); - cy.wait('@createLinode').then((xhr) => { - const requestBody = xhr.request.body; + ui.button + .findByTitle('Create Linode') + .should('be.visible') + .should('be.enabled') + .click(); - expect(requestBody['image']).to.equal('linode/debian11'); - expect(requestBody['label']).to.equal(mockLinode.label); - expect(requestBody['region']).to.equal(mockLinodeRegion.id); - expect(requestBody['type']).to.equal('g6-nanode-1'); - }); + cy.wait('@createLinode').then((xhr) => { + const requestBody = xhr.request.body; - cy.url().should('endWith', `/linodes/${mockLinode.id}`); + expect(requestBody['image']).to.equal('linode/debian11'); + expect(requestBody['label']).to.equal(mockLinode.label); + expect(requestBody['region']).to.equal(mockLinodeRegion.id); + expect(requestBody['type']).to.equal('g6-nanode-1'); }); + + cy.url().should('endWith', `/linodes/${mockLinode.id}`); }); }); }); diff --git a/packages/manager/cypress/support/constants/environment.ts b/packages/manager/cypress/support/constants/environment.ts index c901359e127..545bcd9bc8a 100644 --- a/packages/manager/cypress/support/constants/environment.ts +++ b/packages/manager/cypress/support/constants/environment.ts @@ -10,30 +10,12 @@ export interface ViewportSize { // Array of common mobile viewports against which to test. export const MOBILE_VIEWPORTS: ViewportSize[] = [ - { - // iPhone 6, 7, 8, SE2, etc. - label: 'iPhone 8', - width: 375, - height: 667, - }, { // iPhone 14 Pro, iPhone 15, iPhone 15 Pro, etc. label: 'iPhone 15', width: 393, height: 852, }, - { - // iPhone 15 Pro Max - label: 'iPhone 15 Pro Max', - width: 430, - height: 932, - }, - { - // Galaxy S22 - label: 'Samsung Galaxy S22', - width: 360, - height: 780, - }, // TODO Evaluate what devices to include here and how long to allow this list to be. Tablets? // Do we want to keep this short, or make it long and just choose a random subset each time we do mobile testing? ]; From 2a7d5359d663808c23a29f21f940bbfeb930a96c Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Thu, 23 May 2024 09:38:20 -0400 Subject: [PATCH 25/25] Added changeset: Add Cypress test coverage for Linode Create v2 flow --- packages/manager/.changeset/pr-10469-tests-1716471500474.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-10469-tests-1716471500474.md diff --git a/packages/manager/.changeset/pr-10469-tests-1716471500474.md b/packages/manager/.changeset/pr-10469-tests-1716471500474.md new file mode 100644 index 00000000000..090c468a04d --- /dev/null +++ b/packages/manager/.changeset/pr-10469-tests-1716471500474.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Add Cypress test coverage for Linode Create v2 flow ([#10469](https://github.com/linode/manager/pull/10469))