diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..7bd5f0b --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +nodejs 20.10.0 diff --git a/Dockerfile b/Dockerfile index ad86563..f1d4b6b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM mcr.microsoft.com/playwright:v1.30.0-focal -# Sets argument as environmental variable -ENV PLAYWRIGHT_FOLDERNAME=checkout +# Sets argument as environmental variable (default value) +ENV PLAYWRIGHT_FOLDERNAME=checkout/v5 # Copy from current directory to `/e2e` COPY . /e2e diff --git a/README.md b/README.md index 33afb5f..4f839a8 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,13 @@ It can be executed in 2 ways: - Install and run Playwright tests on Node - Run the `ghcr.io/adyen-examples/adyen-testing-suite` Docker image +**Note:** The E2E Testing Suite supports Checkout testing with both Adyen Drop-in v5 and v6. + ## Run in Node ### Pre-requisites -* Node 17+ +* Node 20+ * Start sample application on `localhost:8080` * Make sure the sample application uses English language diff --git a/playwright.config.js b/playwright.config.js index 8d72a6c..6fb1bcd 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -10,7 +10,7 @@ const config = { testDir: './tests', /* Maximum time one test can run for. */ - timeout: 5 * 60 * 1000, + timeout: process.env.CI ? 5 * 60 * 1000: 1 * 60 * 1000, expect: { /** @@ -19,7 +19,7 @@ const config = { * * note: waiting longer on CI */ - timeout: process.env.CI ? 40 * 1000 : 20 * 1000 + timeout: process.env.CI ? 40 * 1000 : 10 * 1000 }, /* Run tests in files in parallel */ @@ -42,7 +42,7 @@ const config = { baseURL: process.env.URL || 'http://localhost:8080', /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ - actionTimeout: 240 * 1000, + actionTimeout: process.env.CI ? 240 * 1000 : 10 * 1000, /* Base URL to use in actions like `await page.goto('/')`. */ // baseURL: 'http://localhost:3000', diff --git a/tests/checkout/card.spec.js b/tests/checkout/v5/card.spec.js similarity index 95% rename from tests/checkout/card.spec.js rename to tests/checkout/v5/card.spec.js index 8bbfec2..282f5e5 100644 --- a/tests/checkout/card.spec.js +++ b/tests/checkout/v5/card.spec.js @@ -1,6 +1,6 @@ // @ts-check const { test, expect } = require('@playwright/test'); -const utilities = require('../utilities'); +const utilities = require('../../utilities'); test('Card', async ({ page }) => { await page.goto('/'); diff --git a/tests/checkout/dropin-card.spec.js b/tests/checkout/v5/dropin-card.spec.js similarity index 96% rename from tests/checkout/dropin-card.spec.js rename to tests/checkout/v5/dropin-card.spec.js index 9037b7e..de3ccab 100644 --- a/tests/checkout/dropin-card.spec.js +++ b/tests/checkout/v5/dropin-card.spec.js @@ -1,6 +1,6 @@ // @ts-check const { test, expect } = require('@playwright/test'); -const utilities = require('../utilities'); +const utilities = require('../../utilities'); test('Dropin Card', async ({ page }) => { await page.goto('/'); diff --git a/tests/checkout/dropin-sepa.spec.js b/tests/checkout/v5/dropin-sepa.spec.js similarity index 100% rename from tests/checkout/dropin-sepa.spec.js rename to tests/checkout/v5/dropin-sepa.spec.js diff --git a/tests/checkout/ideal.spec.js b/tests/checkout/v5/ideal.spec.js similarity index 100% rename from tests/checkout/ideal.spec.js rename to tests/checkout/v5/ideal.spec.js diff --git a/tests/checkout/klarna-paynow.spec.js b/tests/checkout/v5/klarna-paynow.spec.js similarity index 100% rename from tests/checkout/klarna-paynow.spec.js rename to tests/checkout/v5/klarna-paynow.spec.js diff --git a/tests/checkout/webhook-failure.spec.js b/tests/checkout/v5/webhook-failure.spec.js similarity index 100% rename from tests/checkout/webhook-failure.spec.js rename to tests/checkout/v5/webhook-failure.spec.js diff --git a/tests/checkout/webhook.spec.js b/tests/checkout/v5/webhook.spec.js similarity index 97% rename from tests/checkout/webhook.spec.js rename to tests/checkout/v5/webhook.spec.js index 75ef8a3..66dacae 100644 --- a/tests/checkout/webhook.spec.js +++ b/tests/checkout/v5/webhook.spec.js @@ -1,6 +1,6 @@ // @ts-check const { test, expect } = require('@playwright/test'); -const utilities = require('../utilities'); +const utilities = require('../../utilities'); // test webhook is successfully delivered test('Webhook Notification', async ({ request }) => { diff --git a/tests/checkout/v6/card.spec.js b/tests/checkout/v6/card.spec.js new file mode 100644 index 0000000..9c57fbd --- /dev/null +++ b/tests/checkout/v6/card.spec.js @@ -0,0 +1,33 @@ +// @ts-check +const { test, expect } = require('@playwright/test'); +const utilities = require('../../utilities'); + +test('Card', async ({ page }) => { + await page.goto('/'); + + await expect(page).toHaveTitle(/Checkout Demo/); + await expect(page.locator('text="Select a demo"')).toBeVisible(); + + // Select "Card" + await page.getByRole('link', { name: 'Card', exact: true }).click(); + await expect(page.locator('text="Cart"')).toBeVisible(); + + // Click "Continue to checkout" + await page.getByRole('link', { name: 'Continue to checkout' }).click(); + + // Wait for load event + await page.waitForLoadState('load'); + + // Assert that "Card number" is visible within iframe + await expect(page.locator('text="Card number"')).toBeVisible(); + + // Fill card details + await utilities.fillComponentCardDetailsV6(page); + + // Click "Pay" button + const payButton = page.locator('.adyen-checkout__button__text >> visible=true'); + await expect(payButton).toBeVisible(); + await payButton.click(); + + await expect(page.locator('text="Return Home"')).toBeVisible(); +}); diff --git a/tests/checkout/v6/dropin-card.spec.js b/tests/checkout/v6/dropin-card.spec.js new file mode 100644 index 0000000..7cef65c --- /dev/null +++ b/tests/checkout/v6/dropin-card.spec.js @@ -0,0 +1,40 @@ +// @ts-check +const { test, expect } = require('@playwright/test'); +const utilities = require('../../utilities'); + +test('Dropin Card', async ({ page }) => { + await page.goto('/'); + + await expect(page).toHaveTitle(/Checkout Demo/); + await expect(page.locator('text="Select a demo"')).toBeVisible(); + + // Select "Drop-in" + await page.getByRole('link', { name: 'Drop-in' }).click(); + await expect(page.locator('text="Cart"')).toBeVisible(); + + // Click "Continue to checkout" + await page.getByRole('link', { name: 'Continue to checkout' }).click(); + + // Wait for load event + await page.waitForLoadState('load'); + + // Assert that "Cards" is visible + await expect(page.locator('text="Cards"')).toBeVisible(); + + // Click "Cards" + const radioButton = await page.getByRole('radio', { name: 'Cards' }); + await radioButton.click(); + + // Wait for load event + await page.waitForLoadState('load'); + + // Fill card details + await utilities.fillDropinCardDetailsV6(page); + + // Click "Pay" button + const payButton = page.locator('.adyen-checkout__button__text >> visible=true'); + await expect(payButton).toBeVisible(); + await payButton.click(); + + await expect(page.locator('text="Return Home"')).toBeVisible(); +}); \ No newline at end of file diff --git a/tests/checkout/v6/dropin-sepa.spec.js b/tests/checkout/v6/dropin-sepa.spec.js new file mode 100644 index 0000000..82b7670 --- /dev/null +++ b/tests/checkout/v6/dropin-sepa.spec.js @@ -0,0 +1,34 @@ +// @ts-check +const { test, expect } = require('@playwright/test'); + +test('Dropin SEPA', async ({ page }) => { + await page.goto('/'); + + await expect(page).toHaveTitle(/Checkout Demo/); + await expect(page.locator('text="Select a demo"')).toBeVisible(); + + // Select "Drop-in" + await page.getByRole('link', { name: 'Drop-in' }).click(); + await expect(page.locator('text="Cart"')).toBeVisible(); + + // Click "Continue to checkout" + await page.getByRole('link', { name: 'Continue to checkout' }).click(); + + // Wait for load event + await page.waitForLoadState('load'); + + // Assert that "SEPA Direct Debit" is visible + await expect(page.locator('text="SEPA Direct Debit"')).toBeVisible(); + + // Select "SEPA" + await page.locator('button[id^="button-sepadirectdebit"]').click(); + await page.fill('input[name="ownerName"]', "A. Klaassen"); + await page.fill('input[name="ibanNumber"]', "NL13TEST0123456789"); + + // Click "Pay" button + const payButton = page.locator('.adyen-checkout__button.adyen-checkout__button--pay >> visible=true'); + await expect(payButton).toBeVisible(); + await payButton.click(); + + await expect(page.locator('text="Return Home"')).toBeVisible(); +}); diff --git a/tests/checkout/v6/googlepay.spec.js b/tests/checkout/v6/googlepay.spec.js new file mode 100644 index 0000000..308e360 --- /dev/null +++ b/tests/checkout/v6/googlepay.spec.js @@ -0,0 +1,21 @@ +// @ts-check +const { test, expect } = require('@playwright/test'); + +test('GooglePay', async ({ page }) => { + await page.goto('/'); + + await expect(page).toHaveTitle(/Checkout Demo/); + await expect(page.locator('text="Select a demo"')).toBeVisible(); + + // Select "Google Pay" + await page.getByRole('link', { name: 'Google Pay' }).click(); + await expect(page.locator('text="Cart"')).toBeVisible(); + + // Click "Continue to checkout" + await page.getByRole('link', { name: 'Continue to checkout' }).click(); + + // Check Google Pay button is visible + const googlePayButton = await page.locator('button[aria-label="Buy with GPay"]'); + await expect(googlePayButton).toBeVisible(); + +}); \ No newline at end of file diff --git a/tests/checkout/v6/ideal.spec.js b/tests/checkout/v6/ideal.spec.js new file mode 100644 index 0000000..2ed67de --- /dev/null +++ b/tests/checkout/v6/ideal.spec.js @@ -0,0 +1,36 @@ +// @ts-check +const { test, expect } = require('@playwright/test'); + +// test for iDEAL2 +test('iDEAL', async ({ page }) => { + await page.goto('/'); + + await expect(page).toHaveTitle(/Checkout Demo/); + await expect(page.locator('text="Select a demo"')).toBeVisible(); + + // Select "iDEAL" + await page.getByRole('link', { name: 'iDEAL' }).click(); + await expect(page.locator('text="Cart"')).toBeVisible(); + + // Click "Continue to checkout" + await page.getByRole('link', { name: 'Continue to checkout' }).click(); + + // Click "Continue to iDEAL" + await page.getByRole('button', { name: 'Continue to iDEAL' }).click(); + await expect(page.locator('text="Scan with your banking app to pay"')).toBeVisible(); + + // Click "Select your bank" + await page.getByRole('button', { name: 'Select your bank' }).click(); + + // Click "TESTNL2A" + await page.getByRole('button', { name: 'TESTNL2A' }).click(); + await expect(page.locator('text="Which test simulation to run?"')).toBeVisible(); + + // Click "Success" + await page.getByRole('button', { name: 'Success' }).click(); + + // Click "Continue" + await page.getByRole('link', { name: 'Return Home' }).click(); + +}); + diff --git a/tests/checkout/v6/klarna-paynow.spec.js b/tests/checkout/v6/klarna-paynow.spec.js new file mode 100644 index 0000000..ab72e6d --- /dev/null +++ b/tests/checkout/v6/klarna-paynow.spec.js @@ -0,0 +1,21 @@ +// @ts-check +const { test, expect } = require('@playwright/test'); + +test('Klarna Pay Now', async ({ page }) => { + await page.goto('/'); + + await expect(page).toHaveTitle(/Checkout Demo/); + await expect(page.locator('text="Select a demo"')).toBeVisible(); + + // Select "Klarna Pay Now" + await page.getByRole('link', { name: 'Klarna - Pay now' }).click(); + await expect(page.locator('text="Cart"')).toBeVisible(); + + // Click "Continue to checkout" + await page.getByRole('link', { name: 'Continue to checkout' }).click(); + await expect(page.locator('text="Continue to Pay now with Klarna."')).toBeVisible(); + + // Click "Continue to Klarna" + await page.locator('text="Continue to Pay now with Klarna."').click(); + await expect(page).toHaveTitle("Complete your purchase"); +}); \ No newline at end of file diff --git a/tests/checkout/v6/webhook-failure.spec.js b/tests/checkout/v6/webhook-failure.spec.js new file mode 100644 index 0000000..c38edf0 --- /dev/null +++ b/tests/checkout/v6/webhook-failure.spec.js @@ -0,0 +1,42 @@ +// @ts-check +const { test, expect } = require('@playwright/test'); + +// test webhook is rejected (invalid HMAC signature) +test('Webhook Notification', async ({ request }) => { + const notifications = await request.post(`/api/webhooks/notifications`, { + data: { + "live": "false", + "notificationItems":[ + { + "NotificationRequestItem":{ + "additionalData":{ + "hmacSignature":"INVALID_HMAC_SIGNATURE" + }, + "eventCode":"AUTHORISATION", + "success":"true", + "eventDate":"2019-06-28T18:03:50+01:00", + "merchantAccountCode":"YOUR_MERCHANT_ACCOUNT", + "pspReference": "7914073381342284", + "merchantReference": "YOUR_REFERENCE", + "amount": { + "value":24999, + "currency":"EUR" + } + } + } + ] + } + }); + + /// Verify notification is not accepted (invalid HMAC) + + // Status code not 404 (verify webhook is found) + expect(notifications.status()).not.toEqual(404); + + // Status code not 200 (verify webhook does not accept the notification ie HMAC invalid) + expect(notifications.status()).not.toEqual(200); + + // Body response does not contain [accepted] + notifications.text() + .then(value => {expect(value).not.toEqual("[accepted]");} ); +}); diff --git a/tests/checkout/v6/webhook.spec.js b/tests/checkout/v6/webhook.spec.js new file mode 100644 index 0000000..66dacae --- /dev/null +++ b/tests/checkout/v6/webhook.spec.js @@ -0,0 +1,56 @@ +// @ts-check +const { test, expect } = require('@playwright/test'); +const utilities = require('../../utilities'); + +// test webhook is successfully delivered +test('Webhook Notification', async ({ request }) => { + + var notificationRequestItem = { + "eventCode":"AUTHORISATION", + "success":"true", + "eventDate":"2019-06-28T18:03:50+01:00", + "merchantAccountCode":"YOUR_MERCHANT_ACCOUNT", + "pspReference": "7914073381342284", + "merchantReference": "YOUR_REFERENCE", + "amount": { + "value":1130, + "currency":"EUR" + } + }; + + // calculate signature from payload + const hmacSignature = await utilities.calculateHmacSignature(notificationRequestItem); + // add hmacSignature to 'additionalData' + notificationRequestItem["additionalData"] = {"hmacSignature" : ""+hmacSignature+""} + + // POST webhook + const notifications = await request.post(`/api/webhooks/notifications`, { + data: { + "live": "false", + "notificationItems":[ + { + "NotificationRequestItem": notificationRequestItem + } + ] + } + }); + + var notifications_status = notifications.status(); + + if (notifications_status === 202) { + // Verify status code 202 + expect(notifications.status()).toEqual(202); + + // Verify empty response body + notifications.text() + .then(value => { expect(value).toEqual(""); }); + } else { + // Verify legacy webhook acknowledgment (status code 200) + expect(notifications.status()).toEqual(200); + + // Verify legacy webhook acknowledgment (response body `[accepted]`) + notifications.text() + .then(value => { expect(value).toEqual("[accepted]"); }); + } +}); + diff --git a/tests/utilities.js b/tests/utilities.js index 45cea40..4f45a61 100644 --- a/tests/utilities.js +++ b/tests/utilities.js @@ -19,7 +19,72 @@ module.exports = { /** valid AccountHolder id for AfP tests: use env variable to not expose valid Id */ USERNAME: process.env.AFP_USERNAME || "na", - /** Utility function to fill card details on the Adyen.Web.Component - Sets default values automatically if you do not overwrite it manually. + /** + * Adyen Drop-in v6 + * Utility function to fill card details on the Adyen.Web.Component v6 - Sets default values automatically if you do not overwrite it manually. + * See: https://docs.adyen.com/online-payments/build-your-integration/?platform=Web&integration=Components. + * Example usage: + * ``` + * const utilities = '../utilities'; + * utilities.fillComponentCardDetailsV6(page); // Example #1: Call with default values + * utilities.fillComponentCardDetailsV6(page, { nameOnCard = 'DECLINED' }); // Example #2: Replaces only 'J. Smith' with 'DECLINED' + * ``` + */ + async fillComponentCardDetailsV6(page, { cardNumber = this.CARD_NUMBER, expiryDate = this.EXPIRY_DATE, cvc = this.CVC, nameOnCard = this.NAME_ON_CARD} = {}) { + // Find iframe and fill "Card number" field + const cardNumberFrame = await page.frameLocator('iframe[title*="card number"]'); + await cardNumberFrame.getByPlaceholder('1234 5678 9012 3456').fill(cardNumber); + + // Find iframe and fill "Expiry date" field + const expiryDateFrame = await page.frameLocator('iframe[title*="expiry date"]'); + await expiryDateFrame.getByPlaceholder('MM/YY').fill(expiryDate); + + // Find iframe and fill "CVC" field + const cvcFrame = await page.frameLocator('iframe[title*="security code"]'); + await cvcFrame.getByPlaceholder('123').fill(cvc); + + // Find and fill "Name on card" field - Note: this field is not contained within an iframe + await page.getByPlaceholder('J. Smith').fill(nameOnCard); + }, + + /** + * Adyen Drop-in v6 + * Utility function to fill card details on the Adyen.Web.Dropin v6 - Sets default values automatically if you do not overwrite it manually. + * See: https://docs.adyen.com/online-payments/build-your-integration/?platform=Web&integration=Drop-in. + * Example usage: + * ``` + * const utilities = '../utilities'; + * utilities.fillDropinCardDetails(page); // Example #1: Call with default values + * utilities.fillDropinCardDetails(page, { nameOnCard = 'DECLINED' }); // Example #2: Replaces only 'J. Smith' with 'DECLINED' + * ``` + */ + async fillDropinCardDetailsV6(page, { cardNumber = this.CARD_NUMBER, expiryDate = this.EXPIRY_DATE, cvc = this.CVC, nameOnCard = this.NAME_ON_CARD} = {}) { + // Find iframe and fill "Card number" field + const cardNumberFrame = await page.frameLocator('iframe[title*="card number"]'); + await cardNumberFrame.getByPlaceholder('1234 5678 9012 3456').fill(cardNumber); + + // Find iframe and fill "Expiry date" field + const expiryDateFrame = await page.frameLocator('iframe[title*="expiry date"]'); + await expiryDateFrame.getByPlaceholder('MM/YY').fill(expiryDate); + + // Find iframe and fill "CVC" field + const cardRegion = await page.getByRole('region[name="Cards"i]'); + if (await cardRegion.count() === 0) { + // Handle drop-in + await page.frameLocator('iframe[title="Iframe for security code"]').getByPlaceholder('123').fill(cvc); + } else { + // Handle drop-in when there are are multiple CVC fields with stored payment methods, we select the correct CVC field + await cardRegion.frameLocator('iframe[title*="security code"]').getByPlaceholder('123').fill(cvc); + } + + // Find and fill "Name on card" field - Note: this field is not contained within an iframe + await page.getByPlaceholder('J. Smith').fill(nameOnCard); + }, + + + /** + * Adyen Drop-in v5 + * Utility function to fill card details on the Adyen.Web.Component v5- Sets default values automatically if you do not overwrite it manually. * See: https://docs.adyen.com/online-payments/build-your-integration/?platform=Web&integration=Components. * Example usage: * ``` @@ -45,7 +110,9 @@ module.exports = { await page.getByPlaceholder('J. Smith').fill(nameOnCard); }, - /** Utility function to fill card details on the Adyen.Web.Dropin - Sets default values automatically if you do not overwrite it manually. + /** + * Adyen Drop-in v5 + * Utility function to fill card details on the Adyen.Web.Dropin v5 - Sets default values automatically if you do not overwrite it manually. * See: https://docs.adyen.com/online-payments/build-your-integration/?platform=Web&integration=Drop-in. * Example usage: * ```