Skip to content

Commit

Permalink
test: [M3-8393] - Cypress test for Account Maintenance CSV downloads (#…
Browse files Browse the repository at this point in the history
…11168)

* Updated test confirm maintenance details in the tables - to validate entry in downloaded csv files

* Added changeset: Cypress test for Account Maintenance CSV downloads

* Removed console.log

* Removed library - papaparse to parse csv;Implemented script to parse the csv file

* Removed it.only from test

* Using specific locator for Download CSV and added code to delete downloaded file after assertion

* Refeactoring as per review comment for delete files

* Added assertion in readFile to ensure file content is not empty
  • Loading branch information
subsingh-akamai authored Nov 22, 2024
1 parent c81667d commit c59c943
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 7 deletions.
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-11168-tests-1730108478631.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Tests
---

Cypress test for Account Maintenance CSV downloads ([#11168](https://github.com/linode/manager/pull/11168))
124 changes: 117 additions & 7 deletions packages/manager/cypress/e2e/core/account/account-maintenance.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@
import { mockGetMaintenance } from 'support/intercepts/account';
import { accountMaintenanceFactory } from 'src/factories';
import { parseCsv } from 'support/util/csv';

describe('Maintenance', () => {
/*
* - Confirm user can navigate to account maintenance page via user menu.
* - When there is no pending maintenance, "No pending maintenance." is shown in the table.
* - When there is no completed maintenance, "No completed maintenance." is shown in the table.
*/
beforeEach(() => {
const downloadsFolder = Cypress.config('downloadsFolder');
const filePatterns = '{pending-maintenance*,completed-maintenance*}';
// Delete the file before the test
cy.exec(`rm -f ${downloadsFolder}/${filePatterns}`, {
failOnNonZeroExit: false,
}).then((result) => {
if (result.code === 0) {
cy.log(`Deleted file: ${filePatterns}`);
} else {
cy.log(`Failed to delete file: ${filePatterns}`);
}
});
});

it('table empty when no maintenance', () => {
mockGetMaintenance([], []).as('getMaintenance');

Expand Down Expand Up @@ -118,12 +134,106 @@ describe('Maintenance', () => {
});
});

// Confirm download buttons work
cy.get('button')
.filter(':contains("Download CSV")')
.should('be.visible')
.should('be.enabled')
.click({ multiple: true });
// TODO Need to add assertions to confirm CSV contains the expected contents on first trial (M3-8393)
// Validate content of the downloaded CSV for pending maintenance
cy.get('a[download*="pending-maintenance"]')
.invoke('attr', 'download')
.then((fileName) => {
const downloadsFolder = Cypress.config('downloadsFolder');

// Locate the <a> element for pending-maintenance and then find its sibling <button> element
cy.get('a[download*="pending-maintenance"]')
.siblings('button')
.should('be.visible')
.and('contain', 'Download CSV')
.click();

// Map the expected CSV content to match the structure of the downloaded CSV
const expectedPendingMigrationContent = accountpendingMaintenance.map(
(maintenance) => ({
entity_label: maintenance.entity.label,
entity_type: maintenance.entity.type,
type: maintenance.type,
status: maintenance.status,
reason: maintenance.reason,
})
);

// Read the downloaded CSV and compare its content to the expected CSV content
cy.readFile(`${downloadsFolder}/${fileName}`)
.should('not.eq', null)
.should('not.eq', '')
.then((csvContent) => {
const parsedCsvPendingMigration = parseCsv(csvContent);
expect(parsedCsvPendingMigration.length).to.equal(
expectedPendingMigrationContent.length
);
// Map the parsedCsv to match the structure of expectedCsvContent
const actualPendingMigrationCsvContent = parsedCsvPendingMigration.map(
(entry: any) => ({
entity_label: entry['Entity Label'],
entity_type: entry['Entity Type'],
type: entry['Type'],
status: entry['Status'],
reason: entry['Reason'],
})
);

expect(actualPendingMigrationCsvContent).to.deep.equal(
expectedPendingMigrationContent
);
});
});

// Validate content of the downloaded CSV for completed maintenance
cy.get('a[download*="completed-maintenance"]')
.invoke('attr', 'download')
.then((fileName) => {
const downloadsFolder = Cypress.config('downloadsFolder');

// Locate the <a> element for completed-maintenance and then find its sibling <button> element
cy.get('a[download*="completed-maintenance"]')
.siblings('button')
.should('be.visible')
.and('contain', 'Download CSV')
.click();

// Map the expected CSV content to match the structure of the downloaded CSV
const expectedCompletedMigrationContent = accountcompletedMaintenance.map(
(maintenance) => ({
entity_label: maintenance.entity.label,
entity_type: maintenance.entity.type,
type: maintenance.type,
status: maintenance.status,
reason: maintenance.reason,
})
);

// Read the downloaded CSV and compare its content to the expected CSV content
cy.readFile(`${downloadsFolder}/${fileName}`)
.should('not.eq', null)
.should('not.eq', '')
.then((csvContent) => {
const parsedCsvCompletedMigration = parseCsv(csvContent);

expect(parsedCsvCompletedMigration.length).to.equal(
expectedCompletedMigrationContent.length
);

// Map the parsedCsv to match the structure of expectedCsvContent
const actualCompletedMigrationCsvContent = parsedCsvCompletedMigration.map(
(entry: any) => ({
entity_label: entry['Entity Label'],
entity_type: entry['Entity Type'],
type: entry['Type'],
status: entry['Status'],
reason: entry['Reason'],
})
);

expect(actualCompletedMigrationCsvContent).to.deep.equal(
expectedCompletedMigrationContent
);
});
});
});
});
48 changes: 48 additions & 0 deletions packages/manager/cypress/support/util/csv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* @file Utilities for handling csv files.
*/

/**
* Parses a CSV string and returns an array of objects representing the data.
*
* @param {string} csvContent - The CSV content as a string.
* @returns {any[]} - An array of objects where each object represents a row in the CSV.
* The keys of the objects are the headers from the CSV.
*/

export function parseCsv(csvContent: string): any[] {
// Split the CSV content into lines and filter out any empty lines
const lines = csvContent.split('\n').filter((line) => line.trim() !== '');

// Extract the headers from the first line and remove any quotes
const headers = lines[0]
.split(',')
.map((header) => header.trim().replace(/^"|"$/g, ''));

// Map the remaining lines to objects using the headers
const data = lines.slice(1).map((line) => {
// Split each line into values, handling quoted values with commas and embedded quotes
// The regular expression matches:
// - Values enclosed in double quotes, which may contain commas and escaped double quotes (e.g., "value, with, commas" or "value with ""embedded"" quotes")
// - Values not enclosed in double quotes, which are separated by commas
// The map function then:
// - Trims any leading or trailing whitespace from each value
// - Removes the enclosing double quotes from quoted values
// - Replaces any escaped double quotes within quoted values with a single double quote
const values = line
.match(/("([^"]|"")*"|[^",\s]+)(?=\s*,|\s*$)/g)
?.map((value) => value.trim().replace(/^"|"$/g, '').replace(/""/g, '"'));

// Create an object to represent the row
const entry: any = {};
headers.forEach((header, index) => {
entry[header] = values ? values[index] : '';
});

// Return the object representing the row
return entry;
});

// Return the array of objects representing the CSV data
return data;
}

0 comments on commit c59c943

Please sign in to comment.