From 707660280f7d7f950eb1f9a873fc58c1e2dd85a8 Mon Sep 17 00:00:00 2001 From: Benjamin Piouffle Date: Fri, 26 May 2023 14:47:31 +0200 Subject: [PATCH] test(e2e): change PDF approach to work with headless browsers --- .github/workflows/ci.yml | 1 + .github/workflows/e2e.yml | 17 +- .gitignore | 1 + components/CreateGiftCardsSuccess.js | 2 +- components/transactions/TransactionDetails.js | 1 + cypress.config.js | 6 +- lib/transactions.js | 4 +- lib/url-helpers.js | 10 +- package-lock.json | 165 ++++++++++++++++++ scripts/run_e2e_tests.sh | 6 +- .../02-collective.transactions.test.js | 21 +-- .../integration/09-giftcards-admin.test.js | 7 +- test/cypress/integration/27-expenses.test.js | 13 +- .../scripts/get-text-from-pdf-content.ts | 5 + test/cypress/scripts/list-files.ts | 13 -- test/cypress/scripts/read-pdf.ts | 9 - test/cypress/support/commands.js | 36 +--- 17 files changed, 229 insertions(+), 88 deletions(-) create mode 100644 test/cypress/scripts/get-text-from-pdf-content.ts delete mode 100644 test/cypress/scripts/list-files.ts delete mode 100644 test/cypress/scripts/read-pdf.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c1307ae57e..d0dadc1e76f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,7 @@ env: NODE_ENV: test WEBSITE_URL: http://localhost:3000 API_URL: http://localhost:3060 + PDF_SERVICE_URL: http://localhost:3002 API_KEY: dvl-1510egmf4a23d80342403fb599qd CI: true diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 535e98c6d2c..72545e08fef 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -8,6 +8,8 @@ env: OC_ENV: ci NODE_ENV: test WEBSITE_URL: http://localhost:3000 + IMAGES_URL: http://localhost:3001 + PDF_SERVICE_URL: http://localhost:3002 API_URL: http://localhost:3060 API_KEY: dvl-1510egmf4a23d80342403fb599qd CI: true @@ -15,7 +17,6 @@ env: E2E_TEST: 1 PGHOST: localhost PGUSER: postgres - IMAGES_URL: http://localhost:3001 CYPRESS_RECORD: false CYPRESS_VIDEO: false CYPRESS_VIDEO_UPLOAD_ON_PASSES: false @@ -255,6 +256,13 @@ jobs: env: CYPRESS_TEST_FILES: ${{ matrix.files }} + - name: Archive logs + uses: actions/upload-artifact@v3 + with: + name: logs + path: logs + if: ${{ failure() }} + - name: Archive test screenshots uses: actions/upload-artifact@v3 with: @@ -262,5 +270,12 @@ jobs: path: test/cypress/screenshots if: ${{ failure() }} + - name: Archive download folder + uses: actions/upload-artifact@v3 + with: + name: downloads + path: test/cypress/downloads + if: ${{ failure() }} + - name: Report Coverage run: npm run test:coverage diff --git a/.gitignore b/.gitignore index 3e7aed43015..f0444a5b85d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ node_modules npm-debug.log.* ./report.*.json *.log +logs yarn.lock .DS_Store build diff --git a/components/CreateGiftCardsSuccess.js b/components/CreateGiftCardsSuccess.js index 5cfd6d0b597..efed638efab 100644 --- a/components/CreateGiftCardsSuccess.js +++ b/components/CreateGiftCardsSuccess.js @@ -68,7 +68,7 @@ export default class CreateGiftCardsSuccess extends React.Component { }; renderManualSuccess() { - const filename = `${this.props.collectiveSlug}-giftcards-${Date.now()}.pdf`; + const filename = `${this.props.collectiveSlug}-giftcards.pdf`; const downloadUrl = giftCardsDownloadUrl(filename); return ( diff --git a/components/transactions/TransactionDetails.js b/components/transactions/TransactionDetails.js index 75994246904..c7a6ddd21e3 100644 --- a/components/transactions/TransactionDetails.js +++ b/components/transactions/TransactionDetails.js @@ -271,6 +271,7 @@ const TransactionDetails = ({ displayActions, transaction, onMutationSuccess }) {showDownloadInvoiceButton && ( { // ---- Routes to other Open Collective services ---- export const collectiveInvoiceURL = (collectiveSlug, hostSlug, startDate, endDate, format) => { - return `${invoiceServiceURL}/receipts/collectives/${collectiveSlug}/${hostSlug}/${startDate}/${endDate}/receipt.${format}`; + return `${PDF_SERVICE_URL}/receipts/collectives/${collectiveSlug}/${hostSlug}/${startDate}/${endDate}/receipt.${format}`; }; export const transactionInvoiceURL = transactionUUID => { - return `${invoiceServiceURL}/receipts/transactions/${transactionUUID}/receipt.pdf`; + return `${PDF_SERVICE_URL}/receipts/transactions/${transactionUUID}/receipt.pdf`; }; export const expenseInvoiceUrl = expenseId => { - return `${invoiceServiceURL}/expense/${expenseId}/invoice.pdf`; + return `${PDF_SERVICE_URL}/expense/${expenseId}/invoice.pdf`; }; /** @@ -54,7 +54,7 @@ export const expenseInvoiceUrl = expenseId => { * @param {string} filename - filename **with** extension */ export const giftCardsDownloadUrl = filename => { - return `${invoiceServiceURL}/giftcards/from-data/${filename}`; + return `${PDF_SERVICE_URL}/giftcards/from-data/${filename}`; }; // ---- Routes to external services ---- diff --git a/package-lock.json b/package-lock.json index 8826b440ee6..a3ccfbf6bf0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6345,6 +6345,126 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@next/swc-android-arm-eabi": { + "version": "12.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.3.4.tgz", + "integrity": "sha512-cM42Cw6V4Bz/2+j/xIzO8nK/Q3Ly+VSlZJTa1vHzsocJRYz8KT6MrreXaci2++SIZCF1rVRCDgAg5PpqRibdIA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-android-arm64": { + "version": "12.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-12.3.4.tgz", + "integrity": "sha512-5jf0dTBjL+rabWjGj3eghpLUxCukRhBcEJgwLedewEA/LJk2HyqCvGIwj5rH+iwmq1llCWbOky2dO3pVljrapg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "12.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.3.4.tgz", + "integrity": "sha512-DqsSTd3FRjQUR6ao0E1e2OlOcrF5br+uegcEGPVonKYJpcr0MJrtYmPxd4v5T6UCJZ+XzydF7eQo5wdGvSZAyA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "12.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-12.3.4.tgz", + "integrity": "sha512-PPF7tbWD4k0dJ2EcUSnOsaOJ5rhT3rlEt/3LhZUGiYNL8KvoqczFrETlUx0cUYaXe11dRA3F80Hpt727QIwByQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-freebsd-x64": { + "version": "12.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.3.4.tgz", + "integrity": "sha512-KM9JXRXi/U2PUM928z7l4tnfQ9u8bTco/jb939pdFUHqc28V43Ohd31MmZD1QzEK4aFlMRaIBQOWQZh4D/E5lQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm-gnueabihf": { + "version": "12.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.3.4.tgz", + "integrity": "sha512-3zqD3pO+z5CZyxtKDTnOJ2XgFFRUBciOox6EWkoZvJfc9zcidNAQxuwonUeNts6Xbm8Wtm5YGIRC0x+12YH7kw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "12.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.3.4.tgz", + "integrity": "sha512-kiX0vgJGMZVv+oo1QuObaYulXNvdH/IINmvdZnVzMO/jic/B8EEIGlZ8Bgvw8LCjH3zNVPO3mGrdMvnEEPEhKA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "12.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.3.4.tgz", + "integrity": "sha512-EETZPa1juczrKLWk5okoW2hv7D7WvonU+Cf2CgsSoxgsYbUCZ1voOpL4JZTOb6IbKMDo6ja+SbY0vzXZBUMvkQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@next/swc-linux-x64-gnu": { "version": "12.3.4", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.3.4.tgz", @@ -6375,6 +6495,51 @@ "node": ">= 10" } }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "12.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.3.4.tgz", + "integrity": "sha512-Sd0qFUJv8Tj0PukAYbCCDbmXcMkbIuhnTeHm9m4ZGjCf6kt7E/RMs55Pd3R5ePjOkN7dJEuxYBehawTR/aPDSQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "12.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.3.4.tgz", + "integrity": "sha512-rt/vv/vg/ZGGkrkKcuJ0LyliRdbskQU+91bje+PgoYmxTZf/tYs6IfbmgudBJk6gH3QnjHWbkphDdRQrseRefQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "12.3.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.3.4.tgz", + "integrity": "sha512-DQ20JEfTBZAgF8QCjYfJhv2/279M6onxFjdG/+5B0Cyj00/EdBxiWb2eGGFgQhrBbNv/lsvzFbbi0Ptf8Vw/bg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", diff --git a/scripts/run_e2e_tests.sh b/scripts/run_e2e_tests.sh index 1e9ce7f8d85..d04ece2a1fa 100755 --- a/scripts/run_e2e_tests.sh +++ b/scripts/run_e2e_tests.sh @@ -1,5 +1,7 @@ #!/bin/bash +mkdir -p logs + echo "> Starting maildev server" npx maildev@2.0.5 & MAILDEV_PID=$! @@ -35,7 +37,7 @@ if [ -z "$IMAGES_FOLDER" ]; then else cd $IMAGES_FOLDER fi -npm start & +npm start >../logs/images-service.txt 2>&1 & IMAGES_PID=$! cd - @@ -45,7 +47,7 @@ if [ -z "$PDF_FOLDER" ]; then else cd $PDF_FOLDER fi -PORT=3002 npm start & +PORT=3002 npm start >../logs/pdf-service.txt 2>&1 & PDF_PID=$! cd - diff --git a/test/cypress/integration/02-collective.transactions.test.js b/test/cypress/integration/02-collective.transactions.test.js index f13d27a371e..c26bcfe1a7b 100644 --- a/test/cypress/integration/02-collective.transactions.test.js +++ b/test/cypress/integration/02-collective.transactions.test.js @@ -21,15 +21,16 @@ describe('collective.transactions', () => { cy.login({ redirect: '/brusselstogether/transactions' }); cy.contains('button[data-cy=transaction-details]', 'View Details').first().click(); cy.getByDataCy('download-transaction-receipt-btn').first().click(); - cy.waitForDownload('brusselstogether_2017-12-04_b961becd-cb85-6c70-6ec5-075151203084.pdf').then(file => { - cy.task('readPdf', file) - .should('contain', 'BrusselsTogether ASBL') // Bill from - .should('contain', 'Frederik') // Bill to - .should('contain', 'brusselstogetherasbl_b961becd-cb85-6c70-6ec5-075151203084') - .should('contain', `Contribution #1037`) - .should('contain', '2017-12-04') - .should('contain', 'monthly recurring subscription') - .should('contain', '$10.00'); - }); + cy.getByDataCy('download-transaction-receipt-btn').first().should('have.attr', 'data-loading', 'true'); // Downloading + cy.getByDataCy('download-transaction-receipt-btn').first().should('have.attr', 'data-loading', 'false'); // Downloaded + const filename = 'brusselstogether_2017-12-04_b961becd-cb85-6c70-6ec5-075151203084.pdf'; + cy.getDownloadedPDFContent(filename) + .should('contain', 'BrusselsTogether ASBL') // Bill from + .should('contain', 'Frederik') // Bill to + .should('contain', 'brusselstogetherasbl_b961becd-cb85-6c70-6ec5-075151203084') + .should('contain', `Contribution #1037`) + .should('contain', '2017-12-04') + .should('contain', 'monthly recurring subscription') + .should('contain', '$10.00'); }); }); diff --git a/test/cypress/integration/09-giftcards-admin.test.js b/test/cypress/integration/09-giftcards-admin.test.js index 43d21104ddd..9a13a545a70 100644 --- a/test/cypress/integration/09-giftcards-admin.test.js +++ b/test/cypress/integration/09-giftcards-admin.test.js @@ -38,11 +38,10 @@ describe('Gift cards admin', () => { }); // Download the PDF + // Mock date to make sure we have the same filename cy.getByDataCy('download-gift-cards-btn').click(); - const fileRegex = new RegExp(`${collectiveSlug}-giftcards-\\d+\\.pdf$`); - cy.waitForDownload(fileRegex).then(file => { - cy.task('readPdf', file).should('contain', '$542.00 Gift Card from TestOrg'); - }); + const filename = `${collectiveSlug}-giftcards.pdf`; + cy.getDownloadedPDFContent(filename).should('contain', '$542.00 Gift Card from TestOrg'); // Links should also be added to gift cards list cy.contains('a[href$="/admin/gift-cards"]', 'Back to Gift Cards list').click(); diff --git a/test/cypress/integration/27-expenses.test.js b/test/cypress/integration/27-expenses.test.js index f3aa79aacc8..434cf2ed0b7 100644 --- a/test/cypress/integration/27-expenses.test.js +++ b/test/cypress/integration/27-expenses.test.js @@ -561,13 +561,12 @@ describe('New expense flow', () => { it('Downloads PDF', () => { cy.visit(expenseUrl); cy.getByDataCy('download-expense-invoice-btn').click(); - const fileRegex = new RegExp(`Expense-${expense.legacyId}-.*.pdf`); - cy.waitForDownload(fileRegex).then(file => { - cy.task('readPdf', file) - .should('contain', `Expense #${expense.legacyId}: Expense for E2E tests`) - .should('contain', 'Collective: Test Collective') - .should('contain', '$10.00'); - }); + const date = new Date(expense.createdAt).toISOString().split('T')[0]; + const filename = `Expense-${expense.legacyId}-${collective.slug}-invoice-${date}.pdf`; + cy.getDownloadedPDFContent(filename) + .should('contain', `Expense #${expense.legacyId}: Expense for E2E tests`) + .should('contain', 'Collective: Test Collective') + .should('contain', '$10.00'); }); it('Approve, unapprove, reject and pay actions on expense', () => { diff --git a/test/cypress/scripts/get-text-from-pdf-content.ts b/test/cypress/scripts/get-text-from-pdf-content.ts new file mode 100644 index 00000000000..d4ecde043ba --- /dev/null +++ b/test/cypress/scripts/get-text-from-pdf-content.ts @@ -0,0 +1,5 @@ +const pdf = require('pdf-parse'); // eslint-disable-line node/no-unpublished-require + +export const getTextFromPdfContent = (pdfContent: string): Promise => { + return pdf(pdfContent).then(({ text }) => text); +}; diff --git a/test/cypress/scripts/list-files.ts b/test/cypress/scripts/list-files.ts deleted file mode 100644 index c044f991880..00000000000 --- a/test/cypress/scripts/list-files.ts +++ /dev/null @@ -1,13 +0,0 @@ -const fs = require('fs-extra'); // eslint-disable-line node/no-unpublished-require -const path = require('path'); - -export const listFiles = (downloadsFolder): string[] => { - try { - const files = fs.readdirSync(downloadsFolder); - return files.map(file => path.join(downloadsFolder, file)); - } catch (e) { - // eslint-disable-next-line no-console - console.warn(`Could not read ${downloadsFolder} folder: ${e}`); - return []; - } -}; diff --git a/test/cypress/scripts/read-pdf.ts b/test/cypress/scripts/read-pdf.ts deleted file mode 100644 index 594ea523764..00000000000 --- a/test/cypress/scripts/read-pdf.ts +++ /dev/null @@ -1,9 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const pdf = require('pdf-parse'); // eslint-disable-line node/no-unpublished-require - -export const readPdf = (pathToPdf: string): Promise => { - const resolvedPath = path.resolve(pathToPdf); - const dataBuffer = fs.readFileSync(resolvedPath); - return pdf(dataBuffer).then(({ text }) => text); -}; diff --git a/test/cypress/support/commands.js b/test/cypress/support/commands.js index bf00e25dcc5..f5dd77ba1f6 100644 --- a/test/cypress/support/commands.js +++ b/test/cypress/support/commands.js @@ -229,6 +229,7 @@ Cypress.Commands.add('createExpense', ({ userEmail = defaultTestUserEmail, accou createExpense(expense: $expense, account: $account) { id legacyId + createdAt account { id slug @@ -616,40 +617,15 @@ Cypress.Commands.add( /** * Wait for a file to be downloaded */ -Cypress.Commands.add('waitForDownload', fileMatcher => { - waitForDownload(fileMatcher); +Cypress.Commands.add('getDownloadedPDFContent', (filename, options) => { + const downloadFolder = Cypress.config('downloadsFolder'); + cy.readFile(`${downloadFolder}/${filename}`, null, options).then(pdfFileContent => { + cy.task('getTextFromPdfContent', pdfFileContent); + }); }); // ---- Private ---- -function waitForDownload(fileMatcher, timeout = 10000) { - const downloadFolder = Cypress.config('downloadsFolder'); - cy.task('listFiles', downloadFolder).then(files => { - let file; - if (fileMatcher instanceof RegExp) { - file = files.find(file => fileMatcher.test(file)); - } else if (typeof fileMatcher === 'string') { - file = files.find(file => file.endsWith(fileMatcher)); - } else if (typeof fileMatcher === 'function') { - file = files.find(fileMatcher); - } else { - throw new Error(`Invalid fileMatcher: ${fileMatcher}`); - } - - if (file) { - return file; - } else if (timeout > 0) { - cy.wait(100); - return waitForDownload(fileMatcher, timeout - 100); - } else { - const filesListStr = files.length > 0 ? files.join(', ') : 'None'; - return assert.fail( - `Could not find file for ${fileMatcher}: waitForDownload timed out. Downloaded files: ${filesListStr}`, - ); - } - }); -} - /** * @param {object} user - should have `email` and `id` set */