From da779bec7e81ad350878a3fd1839c251d36f7eaa Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Mon, 25 Sep 2023 14:41:16 -0400 Subject: [PATCH 01/12] Add ARIA label and QA data attribute --- .../manager/src/features/VPCs/VPCCreate/MultipleSubnetInput.tsx | 2 +- packages/manager/src/features/VPCs/VPCCreate/SubnetNode.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/manager/src/features/VPCs/VPCCreate/MultipleSubnetInput.tsx b/packages/manager/src/features/VPCs/VPCCreate/MultipleSubnetInput.tsx index ce364f7612a..ddd8fa549ef 100644 --- a/packages/manager/src/features/VPCs/VPCCreate/MultipleSubnetInput.tsx +++ b/packages/manager/src/features/VPCs/VPCCreate/MultipleSubnetInput.tsx @@ -49,7 +49,7 @@ export const MultipleSubnetInput = (props: Props) => { return ( {subnets.map((subnet, subnetIdx) => ( - + {subnetIdx !== 0 && } diff --git a/packages/manager/src/features/VPCs/VPCCreate/SubnetNode.tsx b/packages/manager/src/features/VPCs/VPCCreate/SubnetNode.tsx index 6d8618f6378..cdc19a5d9f0 100644 --- a/packages/manager/src/features/VPCs/VPCCreate/SubnetNode.tsx +++ b/packages/manager/src/features/VPCs/VPCCreate/SubnetNode.tsx @@ -71,7 +71,7 @@ export const SubnetNode = (props: Props) => { {isRemovable && !!idx && ( - + From f5a83a9790cfc84f8faaeeb78a28a7f0fe9f962f Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Mon, 25 Sep 2023 14:45:21 -0400 Subject: [PATCH 02/12] Add WIP VPC create test --- .../cypress/e2e/core/vpc/vpc-create.spec.ts | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts diff --git a/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts b/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts new file mode 100644 index 00000000000..0ba4b602ddd --- /dev/null +++ b/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts @@ -0,0 +1,139 @@ +/** + * @file Integration tests for VPC create flow. + */ + +import type { Subnet, VPC } from '@linode/api-v4'; +import { vpcFactory, subnetFactory } from '@src/factories'; +import { + mockAppendFeatureFlags, + mockGetFeatureFlagClientstream, +} from 'support/intercepts/feature-flags'; +import { makeFeatureFlagData } from 'support/util/feature-flags'; +import { + randomLabel, + randomPhrase, + randomIp, + randomNumber, +} from 'support/util/random'; +import { chooseRegion } from 'support/util/regions'; +import { ui } from 'support/ui'; + +export const getSubnetNodeSection = (index: number) => { + return cy.get(`[data-qa-subnet-node="${index}"]`); +}; + +describe('VPC create flow', () => { + it('can create a VPC', () => { + const vpcRegion = chooseRegion(); + + const mockSubnets: Subnet[] = subnetFactory.buildList(5); + const mockSubnetDelete: Subnet = subnetFactory.build(); + + const mockInvalidIpRange = `${randomIp()}/${randomNumber(33, 100)}`; + + const mockVpc: VPC = vpcFactory.build({ + label: randomLabel(), + region: vpcRegion.id, + description: randomPhrase(), + subnets: subnetFactory.buildList(5), + }); + + mockAppendFeatureFlags({ + vpc: makeFeatureFlagData(true), + }).as('getFeatureFlags'); + mockGetFeatureFlagClientstream().as('getClientstream'); + + cy.visitWithLogin('/vpcs/create'); + cy.wait(['@getFeatureFlags', '@getClientstream']); + + cy.findByText('Region') + .should('be.visible') + .click() + .type(`${vpcRegion.label}{enter}`); + + cy.findByText('VPC label').should('be.visible').click().type(mockVpc.label); + + cy.findByText('Description') + .should('be.visible') + .click() + .type(mockVpc.description); + + // Fill out the first Subnet. + // Insert an invalid IP address range to confirm client side validation. + getSubnetNodeSection(0) + .should('be.visible') + .within(() => { + cy.findByText('Subnet label') + .should('be.visible') + .click() + .type(mockSubnets[0].label); + + cy.findByText('Subnet IP Address Range') + .should('be.visible') + .click() + .type(`{selectAll}{backspace}`) + .type(mockInvalidIpRange); + }); + + ui.button + .findByTitle('Create VPC') + .should('be.visible') + .should('be.enabled') + .click(); + + cy.findByText('The IPv4 range must be in CIDR format').should('be.visible'); + + // Replace invalid IP address range with valid range. + cy.findByText('Subnet IP Address Range') + .should('be.visible') + .click() + .type(`{selectAll}{backspace}`) + .type(`${randomIp()}/${randomNumber(0, 32)}`); + + // Add another subnet that will later be removed. + ui.button + .findByTitle('Add a Subnet') + .should('be.visible') + .should('be.enabled') + .click(); + + getSubnetNodeSection(1) + .should('be.visible') + .within(() => { + cy.findByText('Subnet label') + .should('be.visible') + .click() + .type(mockSubnetDelete.label); + + cy.findByText('Subnet IP Address Range') + .should('be.visible') + .click() + .type(`{selectAll}{backspace}`) + .type(`${randomIp()}/${randomNumber(0, 32)}`); + + cy.findByLabelText('Remove Subnet') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + // Confirm that Subnet section has been removed from the page. + getSubnetNodeSection(1).should('not.exist'); + cy.findByText(mockSubnetDelete.label).should('not.exist'); + + // cy.get('[data-qa-subnet-node="1"]') + // .should('be.visible') + // .within(() => { + // cy.findByText('Subnet label') + // .should('be.visible') + // .click() + // .type(mockSubnetDelete.label) + + // cy.findByText('Subnet IP Address Range') + // .should('be.visible') + // .click() + // .type(`{selectAll}{backspace}`) + // .type(`${randomIp()}/${randomNumber(0, 32)}`); + // }); + }); +}); From 6daeed72a8cfc1d65a994cb905bf4f2b5a49fb02 Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Thu, 28 Sep 2023 12:49:56 -0400 Subject: [PATCH 03/12] Add QA data attribute for VPC detail summary section --- packages/manager/src/features/VPCs/VPCDetail/VPCDetail.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/manager/src/features/VPCs/VPCDetail/VPCDetail.tsx b/packages/manager/src/features/VPCs/VPCDetail/VPCDetail.tsx index bfe300a56c5..347a2211a68 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/VPCDetail.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/VPCDetail.tsx @@ -130,7 +130,7 @@ const VPCDetail = () => { - + {summaryData.map((col) => { return ( From 6cbd312a077485120c9844e40fcfa50f4515746c Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Thu, 28 Sep 2023 12:53:43 -0400 Subject: [PATCH 04/12] Add intercept utilities for VPC create request --- .../manager/cypress/support/intercepts/vpc.ts | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/packages/manager/cypress/support/intercepts/vpc.ts b/packages/manager/cypress/support/intercepts/vpc.ts index cfd5b38e3d7..5a8212d10df 100644 --- a/packages/manager/cypress/support/intercepts/vpc.ts +++ b/packages/manager/cypress/support/intercepts/vpc.ts @@ -7,6 +7,7 @@ import { paginateResponse } from 'support/util/paginate'; import type { Subnet, VPC } from '@linode/api-v4'; import { makeResponse } from 'support/util/response'; +import { makeErrorResponse } from 'support/util/errors'; /** * Intercepts GET request to fetch a VPC and mocks response. @@ -30,6 +31,38 @@ export const mockGetVPCs = (vpcs: VPC[]): Cypress.Chainable => { return cy.intercept('GET', apiMatcher('vpcs*'), paginateResponse(vpcs)); }; +/** + * Intercepts POST request to create a VPC and mocks the response. + * + * @param vpc - VPC object with which to mock response. + * + * @returns Cypress chainable. + */ +export const mockCreateVPC = (vpc: VPC): Cypress.Chainable => { + return cy.intercept('POST', apiMatcher('vpcs'), makeResponse(vpc)); +}; + +/** + * Intercepts POST request to create a VPC and mocks an HTTP error response. + * + * By default, a 500 response is mocked. + * + * @param errorMessage - Optional error message with which to mock response. + * @param errorCode - Optional error code with which to mock response. Default is `500`. + * + * @returns Cypress chainable. + */ +export const mockCreateVPCError = ( + errorMessage: string = 'An error has occurred', + errorCode: number = 500 +): Cypress.Chainable => { + return cy.intercept( + 'POST', + apiMatcher('vpcs'), + makeErrorResponse(errorMessage, errorCode) + ); +}; + /** * Intercepts PUT request to update a VPC and mocks response. * From 2a43394d4270e9d6ba4a37cd2cb3b325ecbfae08 Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Thu, 28 Sep 2023 12:55:57 -0400 Subject: [PATCH 05/12] Add integration test for VPC create flow --- .../cypress/e2e/core/vpc/vpc-create.spec.ts | 200 +++++++++++++++--- 1 file changed, 166 insertions(+), 34 deletions(-) diff --git a/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts b/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts index 0ba4b602ddd..ca44d6407b9 100644 --- a/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts +++ b/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts @@ -3,41 +3,77 @@ */ import type { Subnet, VPC } from '@linode/api-v4'; -import { vpcFactory, subnetFactory } from '@src/factories'; +import { vpcFactory, subnetFactory, linodeFactory } from '@src/factories'; import { mockAppendFeatureFlags, mockGetFeatureFlagClientstream, } from 'support/intercepts/feature-flags'; +import { + mockCreateVPCError, + mockCreateVPC, + mockDeleteSubnet, + mockGetVPC, + mockGetSubnets, +} from 'support/intercepts/vpc'; import { makeFeatureFlagData } from 'support/util/feature-flags'; import { randomLabel, randomPhrase, randomIp, randomNumber, + randomString, } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; import { ui } from 'support/ui'; +import { buildArray } from 'support/util/arrays'; -export const getSubnetNodeSection = (index: number) => { +/** + * Gets the "Add a Subnet" section with the given index. + * + * @returns Cypress chainable. + */ +const getSubnetNodeSection = (index: number) => { return cy.get(`[data-qa-subnet-node="${index}"]`); }; describe('VPC create flow', () => { + /* + * - Confirms VPC creation flow using mock API data. + * - Confirms that users can create and delete subnets. + * - Confirms client side validation when entering invalid IP ranges. + * - Confirms that UI handles API errors gracefully. + * - Confirms that UI redirects to created VPC page after creating a VPC. + */ it('can create a VPC', () => { const vpcRegion = chooseRegion(); - const mockSubnets: Subnet[] = subnetFactory.buildList(5); - const mockSubnetDelete: Subnet = subnetFactory.build(); + const mockSubnets: Subnet[] = buildArray(3, (index: number) => { + return subnetFactory.build({ + label: randomLabel(), + id: randomNumber(10000, 99999), + ipv4: `${randomIp()}/${randomNumber(0, 32)}`, + linodes: linodeFactory.buildList(index + 1), + }); + }); + const mockSubnetToDelete: Subnet = subnetFactory.build(); const mockInvalidIpRange = `${randomIp()}/${randomNumber(33, 100)}`; const mockVpc: VPC = vpcFactory.build({ + id: randomNumber(10000, 99999), label: randomLabel(), region: vpcRegion.id, description: randomPhrase(), - subnets: subnetFactory.buildList(5), + subnets: mockSubnets, }); + const ipValidationErrorMessage = 'The IPv4 range must be in CIDR format'; + const vpcCreationErrorMessage = 'An unknown error has occurred.'; + const totalSubnetUniqueLinodes = mockSubnets.reduce( + (acc: number, cur: Subnet) => acc + cur.linodes.length, + 0 + ); + mockAppendFeatureFlags({ vpc: makeFeatureFlagData(true), }).as('getFeatureFlags'); @@ -59,7 +95,7 @@ describe('VPC create flow', () => { .type(mockVpc.description); // Fill out the first Subnet. - // Insert an invalid IP address range to confirm client side validation. + // Insert an invalid empty IP range to confirm client side validation. getSubnetNodeSection(0) .should('be.visible') .within(() => { @@ -71,8 +107,7 @@ describe('VPC create flow', () => { cy.findByText('Subnet IP Address Range') .should('be.visible') .click() - .type(`{selectAll}{backspace}`) - .type(mockInvalidIpRange); + .type(`{selectAll}{backspace}`); }); ui.button @@ -81,59 +116,156 @@ describe('VPC create flow', () => { .should('be.enabled') .click(); - cy.findByText('The IPv4 range must be in CIDR format').should('be.visible'); + cy.findByText(ipValidationErrorMessage).should('be.visible'); + + // Enter a random non-IP address string to further test client side validation. + cy.findByText('Subnet IP Address Range') + .should('be.visible') + .click() + .type(`{selectAll}{backspace}`) + .type(randomString(18)); + + ui.button + .findByTitle('Create VPC') + .should('be.visible') + .should('be.enabled') + .click(); + + cy.findByText(ipValidationErrorMessage).should('be.visible'); + + // Enter a valid IP address with an invalid network prefix to further test client side validation. + cy.findByText('Subnet IP Address Range') + .should('be.visible') + .click() + .type(`{selectAll}{backspace}`) + .type(mockInvalidIpRange); + + ui.button + .findByTitle('Create VPC') + .should('be.visible') + .should('be.enabled') + .click(); + + cy.findByText(ipValidationErrorMessage).should('be.visible'); // Replace invalid IP address range with valid range. cy.findByText('Subnet IP Address Range') .should('be.visible') .click() .type(`{selectAll}{backspace}`) - .type(`${randomIp()}/${randomNumber(0, 32)}`); + .type(mockSubnets[0].ipv4!); - // Add another subnet that will later be removed. + // Add another subnet that we will remove later. ui.button .findByTitle('Add a Subnet') .should('be.visible') .should('be.enabled') .click(); + // Fill out subnet section, but leave label blank, then attempt to create + // VPC with missing subnet label. getSubnetNodeSection(1) .should('be.visible') .within(() => { - cy.findByText('Subnet label') - .should('be.visible') - .click() - .type(mockSubnetDelete.label); - cy.findByText('Subnet IP Address Range') .should('be.visible') .click() .type(`{selectAll}{backspace}`) - .type(`${randomIp()}/${randomNumber(0, 32)}`); + .type(mockSubnetToDelete.ipv4!); + }); + + ui.button + .findByTitle('Create VPC') + .should('be.visible') + .should('be.enabled') + .click(); + + // Confirm that label validation message is displayed, then remove the + // subnet and confirm that UI responds accordingly. + getSubnetNodeSection(1) + .should('be.visible') + .within(() => { + cy.findByText('Label is required').should('be.visible'); + // Delete subnet. cy.findByLabelText('Remove Subnet') .should('be.visible') .should('be.enabled') .click(); }); - // Confirm that Subnet section has been removed from the page. getSubnetNodeSection(1).should('not.exist'); - cy.findByText(mockSubnetDelete.label).should('not.exist'); - - // cy.get('[data-qa-subnet-node="1"]') - // .should('be.visible') - // .within(() => { - // cy.findByText('Subnet label') - // .should('be.visible') - // .click() - // .type(mockSubnetDelete.label) - - // cy.findByText('Subnet IP Address Range') - // .should('be.visible') - // .click() - // .type(`{selectAll}{backspace}`) - // .type(`${randomIp()}/${randomNumber(0, 32)}`); - // }); + cy.findByText(mockSubnetToDelete.label).should('not.exist'); + + // Continue adding remaining subnets. + mockSubnets.slice(1).forEach((mockSubnet: Subnet, index: number) => { + ui.button + .findByTitle('Add a Subnet') + .should('be.visible') + .should('be.enabled') + .click(); + + getSubnetNodeSection(index + 1) + .should('be.visible') + .within(() => { + cy.findByText('Subnet label') + .should('be.visible') + .click() + .type(mockSubnet.label); + + cy.findByText('Subnet IP Address Range') + .should('be.visible') + .click() + .type(`{selectAll}{backspace}`) + .type(`${randomIp()}/${randomNumber(0, 32)}`); + }); + }); + + // Click "Create VPC", mock an HTTP 500 error and confirm UI displays the message. + mockCreateVPCError(vpcCreationErrorMessage).as('createVPC'); + ui.button + .findByTitle('Create VPC') + .should('be.visible') + .should('be.enabled') + .click(); + + cy.wait('@createVPC'); + cy.findByText(vpcCreationErrorMessage).should('be.visible'); + + // Click "Create VPC", mock a successful response and confirm that Cloud + // redirects to the VPC details page for the new VPC. + mockCreateVPC(mockVpc).as('createVPC'); + mockGetSubnets(mockVpc.id, mockVpc.subnets).as('getSubnets'); + ui.button + .findByTitle('Create VPC') + .should('be.visible') + .should('be.enabled') + .click(); + + cy.wait('@createVPC'); + cy.url().should('endWith', `/vpcs/${mockVpc.id}`); + cy.wait('@getSubnets'); + + // Confirm that new VPC information is displayed on details page as expected. + cy.findByText(mockVpc.label).should('be.visible'); + cy.get('[data-qa-vpc-summary]') + .should('be.visible') + .within(() => { + cy.contains(`Subnets ${mockVpc.subnets.length}`).should('be.visible'); + cy.contains(`Linodes ${totalSubnetUniqueLinodes}`).should('be.visible'); + cy.contains(`VPC ID ${mockVpc.id}`).should('be.visible'); + cy.contains(`Region ${vpcRegion.label}`).should('be.visible'); + }); + + mockSubnets.forEach((mockSubnet: Subnet) => { + cy.findByText(mockSubnet.label) + .should('be.visible') + .closest('tr') + .within(() => { + cy.findByText(mockSubnet.id).should('be.visible'); + cy.findByText(mockSubnet.ipv4!).should('be.visible'); + cy.findByText(mockSubnet.linodes.length).should('be.visible'); + }); + }); }); }); From fc504c8c5df6f8d505491cf763fc82010c6b3d5c Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Fri, 29 Sep 2023 16:17:37 -0400 Subject: [PATCH 06/12] Change "VPC label" to "VPC Label" and "Subnet label" to "Subnet Label" --- packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts | 6 +++--- packages/manager/src/features/VPCs/VPCCreate/SubnetNode.tsx | 2 +- packages/manager/src/features/VPCs/VPCCreate/VPCCreate.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts b/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts index ca44d6407b9..77820f722b8 100644 --- a/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts +++ b/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts @@ -87,7 +87,7 @@ describe('VPC create flow', () => { .click() .type(`${vpcRegion.label}{enter}`); - cy.findByText('VPC label').should('be.visible').click().type(mockVpc.label); + cy.findByText('VPC Label').should('be.visible').click().type(mockVpc.label); cy.findByText('Description') .should('be.visible') @@ -99,7 +99,7 @@ describe('VPC create flow', () => { getSubnetNodeSection(0) .should('be.visible') .within(() => { - cy.findByText('Subnet label') + cy.findByText('Subnet Label') .should('be.visible') .click() .type(mockSubnets[0].label); @@ -208,7 +208,7 @@ describe('VPC create flow', () => { getSubnetNodeSection(index + 1) .should('be.visible') .within(() => { - cy.findByText('Subnet label') + cy.findByText('Subnet Label') .should('be.visible') .click() .type(mockSubnet.label); diff --git a/packages/manager/src/features/VPCs/VPCCreate/SubnetNode.tsx b/packages/manager/src/features/VPCs/VPCCreate/SubnetNode.tsx index a9d2785cf01..8dfe9c2f7b4 100644 --- a/packages/manager/src/features/VPCs/VPCCreate/SubnetNode.tsx +++ b/packages/manager/src/features/VPCs/VPCCreate/SubnetNode.tsx @@ -63,7 +63,7 @@ export const SubnetNode = (props: Props) => { disabled={disabled} errorText={subnet.labelError} inputId={`subnet-label-${idx}`} - label="Subnet label" + label="Subnet Label" onChange={onLabelChange} placeholder="Enter a subnet label" value={subnet.label} diff --git a/packages/manager/src/features/VPCs/VPCCreate/VPCCreate.tsx b/packages/manager/src/features/VPCs/VPCCreate/VPCCreate.tsx index 57772ed675b..8faca4fe750 100644 --- a/packages/manager/src/features/VPCs/VPCCreate/VPCCreate.tsx +++ b/packages/manager/src/features/VPCs/VPCCreate/VPCCreate.tsx @@ -247,7 +247,7 @@ const VPCCreate = () => { } disabled={userCannotAddVPC} errorText={errors.label} - label="VPC label" + label="VPC Label" value={values.label} /> Date: Fri, 29 Sep 2023 16:20:02 -0400 Subject: [PATCH 07/12] Use src util to get Subnet Linode count --- packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts b/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts index 77820f722b8..d3a45ef54f0 100644 --- a/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts +++ b/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts @@ -11,8 +11,6 @@ import { import { mockCreateVPCError, mockCreateVPC, - mockDeleteSubnet, - mockGetVPC, mockGetSubnets, } from 'support/intercepts/vpc'; import { makeFeatureFlagData } from 'support/util/feature-flags'; @@ -26,6 +24,7 @@ import { import { chooseRegion } from 'support/util/regions'; import { ui } from 'support/ui'; import { buildArray } from 'support/util/arrays'; +import { getUniqueLinodesFromSubnets } from 'src/features/VPCs/utils'; /** * Gets the "Add a Subnet" section with the given index. @@ -69,10 +68,7 @@ describe('VPC create flow', () => { const ipValidationErrorMessage = 'The IPv4 range must be in CIDR format'; const vpcCreationErrorMessage = 'An unknown error has occurred.'; - const totalSubnetUniqueLinodes = mockSubnets.reduce( - (acc: number, cur: Subnet) => acc + cur.linodes.length, - 0 - ); + const totalSubnetUniqueLinodes = getUniqueLinodesFromSubnets(mockSubnets); mockAppendFeatureFlags({ vpc: makeFeatureFlagData(true), From 0b3495a3c16a02627890188657ba453d82ff89ec Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Mon, 9 Oct 2023 15:08:35 -0400 Subject: [PATCH 08/12] Replace "Add a Subnet" with "Add another Subnet" for updated button label --- packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts b/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts index d3a45ef54f0..375cc47ea00 100644 --- a/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts +++ b/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts @@ -27,7 +27,7 @@ import { buildArray } from 'support/util/arrays'; import { getUniqueLinodesFromSubnets } from 'src/features/VPCs/utils'; /** - * Gets the "Add a Subnet" section with the given index. + * Gets the "Add another Subnet" section with the given index. * * @returns Cypress chainable. */ @@ -153,7 +153,7 @@ describe('VPC create flow', () => { // Add another subnet that we will remove later. ui.button - .findByTitle('Add a Subnet') + .findByTitle('Add another Subnet') .should('be.visible') .should('be.enabled') .click(); @@ -196,7 +196,7 @@ describe('VPC create flow', () => { // Continue adding remaining subnets. mockSubnets.slice(1).forEach((mockSubnet: Subnet, index: number) => { ui.button - .findByTitle('Add a Subnet') + .findByTitle('Add another Subnet') .should('be.visible') .should('be.enabled') .click(); From 4104e0cbc09b42f70b1a4172e378b85b1db86e66 Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Mon, 9 Oct 2023 15:24:48 -0400 Subject: [PATCH 09/12] Add test to confirm UI flow when creating VPC without Subnets --- .../cypress/e2e/core/vpc/vpc-create.spec.ts | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts b/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts index 375cc47ea00..f5fa8ec468e 100644 --- a/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts +++ b/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts @@ -264,4 +264,83 @@ describe('VPC create flow', () => { }); }); }); + + /* + * - Confirms VPC creation flow without creating subnets using mock API data. + * - Confirms that users can delete the pre-existing subnet in the create form. + * - Confirms that "Add another Subnet" button label updates to reflect no subnets. + * - Confirms that Cloud Manager UI responds accordingly when creating a VPC without subnets. + */ + it('can create a VPC without any subnets', () => { + const vpcRegion = chooseRegion(); + const mockVpc: VPC = vpcFactory.build({ + id: randomNumber(10000, 99999), + label: randomLabel(), + region: vpcRegion.id, + description: randomPhrase(), + subnets: [], + }); + + const totalSubnetUniqueLinodes = getUniqueLinodesFromSubnets([]); + + mockAppendFeatureFlags({ + vpc: makeFeatureFlagData(true), + }).as('getFeatureFlags'); + mockGetFeatureFlagClientstream().as('getClientstream'); + + cy.visitWithLogin('/vpcs/create'); + cy.wait(['@getFeatureFlags', '@getClientstream']); + + cy.findByText('Region') + .should('be.visible') + .click() + .type(`${vpcRegion.label}{enter}`); + + cy.findByText('VPC Label').should('be.visible').click().type(mockVpc.label); + + // Remove the subnet. + getSubnetNodeSection(0) + .should('be.visible') + .within(() => { + cy.findByLabelText('Remove Subnet') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + // Confirm that subnet button label is "Add a Subnet" when there are no + // subnets. + mockCreateVPC(mockVpc).as('createVpc'); + mockGetSubnets(mockVpc.id, []).as('getSubnets'); + ui.button + .findByTitle('Add a Subnet') + .should('be.visible') + .should('be.enabled'); + + cy.findByText('Add another Subnet').should('not.exist'); + + // Create the VPC and confirm the user is redirected to the details page. + ui.button + .findByTitle('Create VPC') + .should('be.visible') + .should('be.enabled') + .click(); + + cy.wait('@createVpc'); + cy.url().should('endWith', `/vpcs/${mockVpc.id}`); + cy.wait('@getSubnets'); + + // Confirm that the expected VPC information is shown, and that no Subnets + // are listed in the table. + cy.get('[data-qa-vpc-summary]') + .should('be.visible') + .within(() => { + cy.contains(`Subnets ${mockVpc.subnets.length}`).should('be.visible'); + cy.contains(`Linodes ${totalSubnetUniqueLinodes}`).should('be.visible'); + cy.contains(`VPC ID ${mockVpc.id}`).should('be.visible'); + cy.contains(`Region ${vpcRegion.label}`).should('be.visible'); + }); + + cy.findByText('No Subnets are assigned.').should('be.visible'); + }); }); From 1642f649b91b9748f2b26316253b23276ef63897 Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Mon, 9 Oct 2023 16:01:40 -0400 Subject: [PATCH 10/12] Update tests to account for "label" -> "Label", change label in Subnet delete dialog --- .../VPCs/VPCCreate/MultipleSubnetInput.test.tsx | 2 +- .../src/features/VPCs/VPCCreate/SubnetNode.test.tsx | 2 +- .../src/features/VPCs/VPCCreate/VPCCreate.test.tsx | 10 +++++----- .../VPCs/VPCDetail/SubnetCreateDrawer.test.tsx | 2 +- .../VPCs/VPCDetail/SubnetDeleteDialog.test.tsx | 2 +- .../src/features/VPCs/VPCDetail/SubnetDeleteDialog.tsx | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/manager/src/features/VPCs/VPCCreate/MultipleSubnetInput.test.tsx b/packages/manager/src/features/VPCs/VPCCreate/MultipleSubnetInput.test.tsx index 030a6ba0b31..1dc1a9020ba 100644 --- a/packages/manager/src/features/VPCs/VPCCreate/MultipleSubnetInput.test.tsx +++ b/packages/manager/src/features/VPCs/VPCCreate/MultipleSubnetInput.test.tsx @@ -32,7 +32,7 @@ describe('MultipleSubnetInput', () => { ); - expect(getAllByText('Subnet label')).toHaveLength(3); + expect(getAllByText('Subnet Label')).toHaveLength(3); expect(getAllByText('Subnet IP Address Range')).toHaveLength(3); getByDisplayValue('subnet 1'); getByDisplayValue('subnet 2'); diff --git a/packages/manager/src/features/VPCs/VPCCreate/SubnetNode.test.tsx b/packages/manager/src/features/VPCs/VPCCreate/SubnetNode.test.tsx index 55df3e286ab..538b2beab9b 100644 --- a/packages/manager/src/features/VPCs/VPCCreate/SubnetNode.test.tsx +++ b/packages/manager/src/features/VPCs/VPCCreate/SubnetNode.test.tsx @@ -54,7 +54,7 @@ describe('SubnetNode', () => { /> ); - const label = screen.getByText('Subnet label'); + const label = screen.getByText('Subnet Label'); expect(label).toBeInTheDocument(); const ipAddress = screen.getByText('Subnet IP Address Range'); expect(ipAddress).toBeInTheDocument(); diff --git a/packages/manager/src/features/VPCs/VPCCreate/VPCCreate.test.tsx b/packages/manager/src/features/VPCs/VPCCreate/VPCCreate.test.tsx index cd71db1ccf5..fa913d5332d 100644 --- a/packages/manager/src/features/VPCs/VPCCreate/VPCCreate.test.tsx +++ b/packages/manager/src/features/VPCs/VPCCreate/VPCCreate.test.tsx @@ -19,11 +19,11 @@ describe('VPC create page', () => { const { getAllByText } = renderWithTheme(); getAllByText('Region'); - getAllByText('VPC label'); + getAllByText('VPC Label'); getAllByText('Select a Region'); getAllByText('Description'); getAllByText('Subnets'); - getAllByText('Subnet label'); + getAllByText('Subnet Label'); getAllByText('Subnet IP Address Range'); getAllByText('Add another Subnet'); getAllByText('Create VPC'); @@ -61,7 +61,7 @@ describe('VPC create page', () => { userEvent.click(addSubnet); }); - const subnetLabels = screen.getAllByText('Subnet label'); + const subnetLabels = screen.getAllByText('Subnet Label'); const subnetIps = screen.getAllByText('Subnet IP Address Range'); expect(subnetLabels).toHaveLength(2); expect(subnetIps).toHaveLength(2); @@ -72,7 +72,7 @@ describe('VPC create page', () => { userEvent.click(deleteSubnet); }); - const subnetLabelAfter = screen.getAllByText('Subnet label'); + const subnetLabelAfter = screen.getAllByText('Subnet Label'); const subnetIpsAfter = screen.getAllByText('Subnet IP Address Range'); expect(subnetLabelAfter).toHaveLength(1); expect(subnetIpsAfter).toHaveLength(1); @@ -80,7 +80,7 @@ describe('VPC create page', () => { it('should display that a subnet ip is invalid and require a subnet label if a user adds an invalid subnet ip', async () => { renderWithTheme(); - const subnetLabel = screen.getByText('Subnet label'); + const subnetLabel = screen.getByText('Subnet Label'); expect(subnetLabel).toBeInTheDocument(); const subnetIp = screen.getByText('Subnet IP Address Range'); expect(subnetIp).toBeInTheDocument(); diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetCreateDrawer.test.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetCreateDrawer.test.tsx index 163d66eb968..1656d7c3cbb 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetCreateDrawer.test.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetCreateDrawer.test.tsx @@ -23,7 +23,7 @@ describe('Create Subnet Drawer', () => { expect(createSubnetTexts[0]).toBeVisible(); // the Drawer title expect(createSubnetTexts[1]).toBeVisible(); // the button - const label = getByText('Subnet label'); + const label = getByText('Subnet Label'); expect(label).toBeVisible(); expect(label).toBeEnabled(); diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetDeleteDialog.test.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetDeleteDialog.test.tsx index 05b89cebfe6..65e09131ad9 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetDeleteDialog.test.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetDeleteDialog.test.tsx @@ -17,7 +17,7 @@ describe('Delete Subnet dialog', () => { const { getByText } = renderWithTheme(); getByText('Delete Subnet some subnet'); - getByText('Subnet label'); + getByText('Subnet Label'); getByText('Cancel'); getByText('Delete'); }); diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetDeleteDialog.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetDeleteDialog.tsx index e73f97a74f5..eafa651c56e 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetDeleteDialog.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetDeleteDialog.tsx @@ -38,7 +38,7 @@ export const SubnetDeleteDialog = (props: Props) => { type: 'Subnet', }} errors={error} - label="Subnet label" + label="Subnet Label" loading={isLoading} onClick={onDeleteSubnet} onClose={onClose} From d669e76f5f068941f44bdcc7c95e21b2b258ad68 Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Mon, 9 Oct 2023 16:03:38 -0400 Subject: [PATCH 11/12] Added changeset: Cypress tests for VPC create flows --- packages/manager/.changeset/pr-9730-tests-1696881818677.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-9730-tests-1696881818677.md diff --git a/packages/manager/.changeset/pr-9730-tests-1696881818677.md b/packages/manager/.changeset/pr-9730-tests-1696881818677.md new file mode 100644 index 00000000000..29a4fc68f46 --- /dev/null +++ b/packages/manager/.changeset/pr-9730-tests-1696881818677.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Cypress tests for VPC create flows ([#9730](https://github.com/linode/manager/pull/9730)) From 55c80bba83be94ce6d40c61de4bb13752252dde9 Mon Sep 17 00:00:00 2001 From: Joe D'Amore Date: Tue, 10 Oct 2023 09:08:54 -0400 Subject: [PATCH 12/12] Replace "Subnet label" with "Subnet Label" to fix tests --- .../manager/cypress/e2e/core/vpc/vpc-details-page.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/manager/cypress/e2e/core/vpc/vpc-details-page.spec.ts b/packages/manager/cypress/e2e/core/vpc/vpc-details-page.spec.ts index f27ffd74602..ec8bb0bf6ad 100644 --- a/packages/manager/cypress/e2e/core/vpc/vpc-details-page.spec.ts +++ b/packages/manager/cypress/e2e/core/vpc/vpc-details-page.spec.ts @@ -171,7 +171,7 @@ describe('VPC details page', () => { .findByTitle(`Delete Subnet ${mockSubnet.label}`) .should('be.visible') .within(() => { - cy.findByLabelText('Subnet label') + cy.findByLabelText('Subnet Label') .should('be.visible') .click() .type(mockSubnet.label); @@ -238,7 +238,7 @@ describe('VPC details page', () => { .findByTitle('Create Subnet') .should('be.visible') .within(() => { - cy.findByText('Subnet label') + cy.findByText('Subnet Label') .should('be.visible') .click() .type(mockSubnet.label);