diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 9d592deb7..20a6bc0ab 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -1,4 +1,4 @@ -name: E2E Cypress tests +name: E2E Playwright tests env: LABEL_RUNNING: e2e-running LABEL_SUCCESS: e2e-passed @@ -29,11 +29,11 @@ jobs: uses: actions/cache@v2 id: step-cache-node-modules env: - cache-name: cache-cypress-node-modules-test + cache-name: cache-node-modules-test with: path: | node_modules - /home/runner/.cache/Cypress + packages/*/node_modules key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('yarn.lock') }} - name: Install dependencies if: steps.step-cache-node-modules.outputs.cache-hit != 'true' @@ -41,7 +41,7 @@ jobs: yarn --frozen-lockfile test: - name: Cypress tests + name: Playwright tests needs: init if: github.event.label.name == 'e2e-running' || github.event_name == 'push' runs-on: ubuntu-latest @@ -68,12 +68,13 @@ jobs: type: remove - name: Cache node_modules uses: actions/cache@v2 + id: step-cache-node-modules env: - cache-name: cache-cypress-node-modules-test + cache-name: cache-node-modules-test with: path: | node_modules - /home/runner/.cache/Cypress + packages/*/node_modules key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('yarn.lock') }} - name: get preview url id: get-preview-url @@ -81,11 +82,16 @@ jobs: with: token: ${{ secrets.GITHUB_TOKEN }} platform: StorefrontCloud - - name: yarn test e2e + - name: Set the env value + id: set-env-value + run: | + echo "BASE_URL=${{ github.event.inputs.page_url || steps.get-preview-url.outputs.comment_url }}" >> $GITHUB_ENV + - name: yarn test theme e2e run: | - yarn cypress run --config baseUrl=${{ github.event.inputs.page_url || steps.get-preview-url.outputs.comment_url }} + npx playwright install + yarn test:theme env: - CI: true + BASE_URL: ${{ github.event.inputs.page_url || steps.get-preview-url.outputs.comment_url }} # Take care of labels - name: remove running label if: failure() || success() diff --git a/package.json b/package.json index a21b05abf..175e3d662 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "lint": "prettier --write --parser typescript \"packages/**/*.ts\"", "test": "NODE_OPTIONS=--unhandled-rejections=warn jest --runInBand", "test:e2e": "jest --e2e=true --runInBand", - "test:cypress": "cypress run", + "test:theme": "cd ./packages/default-theme && yarn test:e2e", "test:coverage": "yarn test --coverage", "docs:dev": "vuepress dev docs", "docs:build": "vuepress build docs", diff --git a/packages/default-theme/__e2e__/fixtures/example.json b/packages/default-theme/__e2e__/fixtures/example.json deleted file mode 100644 index da18d9352..000000000 --- a/packages/default-theme/__e2e__/fixtures/example.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "Using fixtures to represent data", - "email": "hello@cypress.io", - "body": "Fixtures are a great way to mock data for responses to routes" -} \ No newline at end of file diff --git a/packages/default-theme/__e2e__/helpers/testHelpers.ts b/packages/default-theme/__e2e__/helpers/testHelpers.ts new file mode 100644 index 000000000..6ee1f50f6 --- /dev/null +++ b/packages/default-theme/__e2e__/helpers/testHelpers.ts @@ -0,0 +1,48 @@ +import faker from "faker"; +import { Page, expect } from "@playwright/test"; + +export async function fillRegistrationForm({ page }: { page: Page }) { + await expect(page.locator("#Salutation")).toContainText("Not specified"); + + // Fill [data-testid="registration-first-name-input"] [data-testid="registration-first-name-input"] + await page.fill( + '[data-testid="registration-first-name-input"] [data-testid="registration-first-name-input"]', + faker.name.firstName() + ); + + // Fill [data-testid="registration-last-name-input"] [data-testid="registration-last-name-input"] + await page.fill( + '[data-testid="registration-last-name-input"] [data-testid="registration-last-name-input"]', + faker.name.lastName() + ); + + // Click [data-testid="guest-registration-checkbox"] div + await page.click('[data-testid="guest-registration-checkbox"] div'); + + // Fill [data-testid="registration-email-input"] [data-testid="registration-email-input"] + await page.fill( + '[data-testid="registration-email-input"] [data-testid="registration-email-input"]', + faker.internet.email() + ); + + // Fill [data-testid="registration-street-input"] [data-testid="registration-street-input"] + await page.fill( + '[data-testid="registration-street-input"] [data-testid="registration-street-input"]', + faker.address.streetName() + ); + + // Fill [data-testid="registration-zipcode-input"] [data-testid="registration-zipcode-input"] + await page.fill( + '[data-testid="registration-zipcode-input"] [data-testid="registration-zipcode-input"]', + faker.address.zipCode() + ); + + // Fill [data-testid="registration-city-input"] [data-testid="registration-city-input"] + await page.fill( + '[data-testid="registration-city-input"] [data-testid="registration-city-input"]', + faker.address.cityName() + ); + + // Click [data-testid="register-button"] + await page.click('[data-testid="register-button"]'); +} diff --git a/packages/default-theme/__e2e__/playwright.config.ts b/packages/default-theme/__e2e__/playwright.config.ts new file mode 100644 index 000000000..2bf27b8a3 --- /dev/null +++ b/packages/default-theme/__e2e__/playwright.config.ts @@ -0,0 +1,12 @@ +import { PlaywrightTestConfig } from "@playwright/test"; + +const baseURL = process.env.BASE_URL || "http://localhost:3000"; +console.error("Test for base url: ", baseURL); +const config: PlaywrightTestConfig = { + use: { + baseURL, + }, + timeout: 120000, + retries: 2, +}; +export default config; diff --git a/packages/default-theme/__e2e__/plugins/index.js b/packages/default-theme/__e2e__/plugins/index.js deleted file mode 100644 index 8dd144a6c..000000000 --- a/packages/default-theme/__e2e__/plugins/index.js +++ /dev/null @@ -1,21 +0,0 @@ -/// -// *********************************************************** -// This example plugins/index.js can be used to load plugins -// -// You can change the location of this file or turn off loading -// the plugins file with the 'pluginsFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/plugins-guide -// *********************************************************** - -// This function is called when a project is opened or re-opened (e.g. due to -// the project's config changing) - -/** - * @type {Cypress.PluginConfig} - */ -module.exports = (on, config) => { - // `on` is used to hook into various events Cypress emits - // `config` is the resolved Cypress config -}; diff --git a/packages/default-theme/__e2e__/specs/desktopHappyPathExistingUser.spec.ts b/packages/default-theme/__e2e__/specs/desktopHappyPathExistingUser.spec.ts new file mode 100644 index 000000000..1ac11102c --- /dev/null +++ b/packages/default-theme/__e2e__/specs/desktopHappyPathExistingUser.spec.ts @@ -0,0 +1,60 @@ +import { test } from "@playwright/test"; + +test.use({ + viewport: { + width: 1920, + height: 1080, + }, +}); + +test("[DESKTOP] Happy path logged in user", async ({ page, baseURL }) => { + await page.goto('/'); + + await page.click('[data-testid="login-icon"]'); + // Fill [data-testid="email-input"] [data-testid="email-input"] + await page.fill( + '[data-testid="email-input"] [data-testid="email-input"]', + "1d7b9fef36a34367ad02993594db3fc9rlegros@example.com" + ); + // Fill [data-testid="password-input"] [data-testid="password-input"] + await page.fill( + '[data-testid="password-input"] [data-testid="password-input"]', + "shopware" + ); + // Click [data-testid="submit-login-button"] + await page.click('[data-testid="submit-login-button"]'); + + await Promise.all([ + page.waitForNavigation(), + page.click('[data-testid="top-navigation"] a'), + ]); + + await Promise.all([ + page.hover('[data-testid="product-card"]'), + page.click('[data-testid="product-card"] .sf-product-card__add-button'), + ]); + + await page.click('[data-testid="cart-icon"]'); + await Promise.all([ + page.waitForNavigation(), + page.click('[data-testid="goToCheckout-button"]'), + ]); + + await page.click( + '[data-testid="checkout-payment-method-Cash-on-delivery"] div' + ); + await page.click( + '[data-testid="checkout-payment-method-Cash-on-delivery"] .is-active' + ); + + // Click [data-testid="termsAgreementCheckbox"] div + await page.click('[data-testid="termsAgreementCheckbox"] div'); + + // Click [data-testid="place-my-order"] + await Promise.all([ + page.waitForNavigation(), + page.click('[data-testid="place-my-order"]'), + ]); + + await page.click('h2:has-text("Thank you")'); +}); diff --git a/packages/default-theme/__e2e__/specs/desktopHappyPathGuestUser.spec.ts b/packages/default-theme/__e2e__/specs/desktopHappyPathGuestUser.spec.ts new file mode 100644 index 000000000..607dde085 --- /dev/null +++ b/packages/default-theme/__e2e__/specs/desktopHappyPathGuestUser.spec.ts @@ -0,0 +1,76 @@ +import { test } from "@playwright/test"; +import { fillRegistrationForm } from "../helpers/testHelpers"; + +test.use({ + viewport: { + width: 1920, + height: 1080, + }, +}); + +test("[DESKTOP] Happy path guest user", async ({ page }) => { + // Go to / + await page.goto("/"); + + // Click [data-testid="search-bar"] + await page.click('[data-testid="search-bar"]'); + + // Fill [data-testid="search-bar"] + await page.fill('[data-testid="search-bar"]', "aaa"); + + // Click [data-testid="quicksearch-result"] + await Promise.all([ + page.waitForNavigation(), + page.click('[data-testid="quicksearch-result"]'), + ]); + + // Click [data-testid="button-addToCart"] + await page.click('[data-testid="button-addToCart"]'); + + // Click [data-testid="cart-icon"] + await page.click('[data-testid="cart-icon"]'); + + // Click text=Total items 1 + await page.click("text=Total items 1"); + + // Click [data-testid="goToCheckout-button"] + await Promise.all([ + page.waitForNavigation(), + page.click('[data-testid="goToCheckout-button"]'), + ]); + + await fillRegistrationForm({ page }); + + // Click text=Shipping methods + await page.click("text=Shipping methods"); + + await page.click( + '[data-testid="checkout-payment-method-Cash-on-delivery"] div' + ); + await page.click( + '[data-testid="checkout-payment-method-Cash-on-delivery"] .is-active' + ); + + // Check if we can submit order without agreeing to Terms + // Click [data-testid="place-my-order"] + await page.click('[data-testid="place-my-order"]'); + // Click text=This field is required + await page.click("text=This field is required"); + // Click [data-testid="termsAgreementCheckbox"] div + await page.click('[data-testid="termsAgreementCheckbox"] div'); + + // Click [data-testid="place-my-order"] + await Promise.all([ + page.waitForNavigation(), + page.click('[data-testid="place-my-order"]'), + ]); + + // Click h2:has-text("Thank you") + await page.click('h2:has-text("Thank you")'); + + // Click text=Payment method Cash on delivery + await page.click("text=Payment method Cash on delivery"); + + // Close page + await page.close(); +}); diff --git a/packages/default-theme/__e2e__/specs/languageSwitcher.spec.js b/packages/default-theme/__e2e__/specs/languageSwitcher.todo.js similarity index 100% rename from packages/default-theme/__e2e__/specs/languageSwitcher.spec.js rename to packages/default-theme/__e2e__/specs/languageSwitcher.todo.js diff --git a/packages/default-theme/__e2e__/specs/mobileHappyPathGuestUser.spec.ts b/packages/default-theme/__e2e__/specs/mobileHappyPathGuestUser.spec.ts new file mode 100644 index 000000000..78616bf8c --- /dev/null +++ b/packages/default-theme/__e2e__/specs/mobileHappyPathGuestUser.spec.ts @@ -0,0 +1,74 @@ +import { test } from "@playwright/test"; +import { fillRegistrationForm } from "../helpers/testHelpers"; + +test.use({ + viewport: { + width: 320, + height: 480, + }, +}); + +test("[MOBILE] Happy path guest user", async ({ page }) => { + // Go to / + await page.goto("/"); + + // Click [data-testid="search-bar"] + await page.click('[data-testid="search-bar"]'); + + // Fill [data-testid="search-bar"] + await page.fill('[data-testid="search-bar"]', "aaa"); + + // Press Enter + await Promise.all([ + page.waitForNavigation(), + page.press('[data-testid="search-bar"]', "Enter"), + ]); + + // Click img[alt="Levis X-star Wars Galaxy Far Away Pullover Hoodie Junior"] + await Promise.all([ + page.waitForNavigation(), + page.click('[data-testid="product-card"] img'), + ]); + + // Click [data-testid="button-addToCart"] + await page.click('[data-testid="button-addToCart"]'); + + // Click [aria-label="Go to Cart"] + await page.click('[aria-label="Go to Cart"]'); + + // Click [data-testid="goToCheckout-button"] + await Promise.all([ + page.waitForNavigation(), + page.click('[data-testid="goToCheckout-button"]'), + ]); + + await fillRegistrationForm({ page }); + + // Click text=Shipping methods + await page.click("text=Shipping methods"); + + await page.click( + '[data-testid="checkout-payment-method-Cash-on-delivery"] div' + ); + await page.click( + '[data-testid="checkout-payment-method-Cash-on-delivery"] .is-active' + ); + + // Click [data-testid="termsAgreementCheckbox"] div + await page.click('[data-testid="termsAgreementCheckbox"] div'); + + // Click [data-testid="place-my-order"] + await Promise.all([ + page.waitForNavigation(), + page.click('[data-testid="place-my-order"]'), + ]); + + // Click h2:has-text("Thank you") + await page.click('h2:has-text("Thank you")'); + + // Click text=Payment method Cash on delivery + await page.click("text=Payment method Cash on delivery"); + + // Close page + await page.close(); +}); diff --git a/packages/default-theme/__e2e__/specs/passwordReset.spec.js b/packages/default-theme/__e2e__/specs/passwordReset.todo.js similarity index 100% rename from packages/default-theme/__e2e__/specs/passwordReset.spec.js rename to packages/default-theme/__e2e__/specs/passwordReset.todo.js diff --git a/packages/default-theme/__e2e__/specs/shoppingHappyPath.spec.js b/packages/default-theme/__e2e__/specs/shoppingHappyPath.spec.js deleted file mode 100644 index 5f09ce2b7..000000000 --- a/packages/default-theme/__e2e__/specs/shoppingHappyPath.spec.js +++ /dev/null @@ -1,81 +0,0 @@ -describe("Shopping happy path", () => { - beforeEach(() => { - cy.intercept("GET", "**/store-api/currency").as("getCurrencies"); - cy.visit("/"); - cy.wait("@getCurrencies"); - }); - - it("[DESKTOP]: checkout as guest user", () => { - cy.get("[data-cy=search-bar]").click(); - cy.get("[data-cy=search-bar]").type("aaa"); - cy.get(".search-suggestions__product:nth-child(1)").click(); - - cy.get("[data-cy=button-addToCart]").click(); - - cy.get('[data-cy="cart-icon"] > .sf-badge').click(); - cy.get("[data-cy=goToCheckout-button]").click(); - - cy.fillAndExecuteRegistrationForm(); - - cy.get("[data-cy=checkout-payment-method-Cash-on-delivery] input").click(); - - cy.get("[data-cy=place-my-order]").click(); - - cy.contains("Thank you"); - }); - - it("[DESKTOP]: checkout path as logged in user", () => { - cy.login(); - - cy.get("[data-cy=search-bar]").click(); - cy.get("[data-cy=search-bar]").type("aaa"); - cy.get(".search-suggestions__product:nth-child(1)").click(); - cy.get("[data-cy=button-addToCart]").click(); - - cy.get('[data-cy="cart-icon"] > .sf-badge').click(); - cy.get("[data-cy=goToCheckout-button]").click(); - - cy.get("[data-cy=checkout-payment-method-Cash-on-delivery] input").click(); - - cy.get("[data-cy=place-my-order]").click(); - - cy.contains("Thank you"); - }); - - it("[MOBILE]: checkout as guest user", () => { - cy.viewport("iphone-5"); - cy.get("[data-cy=search-bar]").click(); - cy.get("[data-cy=search-bar]").type("aaa{enter}"); - cy.get(".sf-product-card:nth-child(1) img").click(); - cy.get("[data-cy=button-addToCart]").click(); - - cy.get('[data-cy="bottom-navigation-cart"]').click(); - cy.get("[data-cy=goToCheckout-button]").click(); - - cy.fillAndExecuteRegistrationForm(); - - cy.get("[data-cy=checkout-payment-method-Cash-on-delivery] input").click(); - - cy.get("[data-cy=place-my-order]").click(); - cy.contains("Thank you"); - }); - - it("[MOBILE]: checkout as logged in user", () => { - cy.viewport("iphone-5"); - cy.login(); - - cy.get("[data-cy=search-bar]").click(); - cy.get("[data-cy=search-bar]").type("aaa{enter}"); - cy.get(".sf-product-card:nth-child(1) img").click(); - cy.get("[data-cy=button-addToCart]").click(); - - cy.get('[data-cy="bottom-navigation-cart"]').click(); - cy.get("[data-cy=goToCheckout-button]").click(); - - cy.get("[data-cy=checkout-payment-method-Cash-on-delivery] input").click(); - - cy.get("[data-cy=place-my-order]").click(); - - cy.contains("Thank you"); - }); -}); diff --git a/packages/default-theme/__e2e__/support/commands.js b/packages/default-theme/__e2e__/support/commands.js deleted file mode 100644 index 69b5bc6e5..000000000 --- a/packages/default-theme/__e2e__/support/commands.js +++ /dev/null @@ -1,97 +0,0 @@ -// *********************************************** -// This example commands.js shows you how to -// create various custom commands and overwrite -// existing commands. -// -// For more comprehensive examples of custom -// commands please read more here: -// https://on.cypress.io/custom-commands -// *********************************************** -// -// -// -- This is a parent command -- -// Cypress.Commands.add("login", (email, password) => { ... }) -// -// -// -- This is a child command -- -// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) -// -// -// -- This is a dual command -- -// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) -// -// -// -- This will overwrite an existing command -- - -// const { createPartiallyEmittedExpression } = require("typescript") -import faker from "faker"; - -// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) - -Cypress.Commands.overwrite("click", (originalFunction, subject, options) => { - // we're invoking click with force by default to avoid scrolling problems - return originalFunction(subject, { - ...options, - force: true, - }); -}); - -Cypress.Commands.overwrite( - "type", - (originalFunction, subject, string, options) => { - // we're invoking type with force by default to avoid scrolling problems - return originalFunction(subject, string, { - ...options, - force: true, - }); - } -); - -Cypress.Commands.add( - "login", - ({ - username = "1d7b9fef36a34367ad02993594db3fc9rlegros@example.com", - password = "shopware", - } = {}) => { - cy.intercept({ - method: "POST", - url: "**/store-api/account/customer*", - }).as("invokeLogin"); - - cy.get('[data-cy="login-icon"]').click(); - cy.get("input[data-cy=email-input]").type(username); - cy.get("input[data-cy=password-input]").type(password); - cy.get("[data-cy=submit-login-button]").click(); - - cy.wait("@invokeLogin").its("response.statusCode").should("eq", 200); - } -); - -Cypress.Commands.add("fillAndExecuteRegistrationForm", ({ email } = {}) => { - cy.intercept({ - url: "**/store-api/account/register", - }).as("invokeRegistration"); - - cy.get("input[data-cy=registration-first-name-input]").type( - faker.name.firstName() - ); - cy.get("input[data-cy=registration-last-name-input]").type( - faker.name.lastName() - ); - cy.get("input[data-cy=registration-email-input]").type( - email || faker.internet.email() - ); - cy.get("[data-cy=guest-registration-checkbox] input").check(); - cy.get("input[data-cy=registration-street-input]").type( - faker.address.streetName() - ); - cy.get("input[data-cy=registration-zipcode-input]").type( - faker.address.zipCode() - ); - cy.get("input[data-cy=registration-city-input]").type( - faker.address.cityName() - ); - cy.get("[data-cy=register-button]").click(); - - cy.wait("@invokeRegistration").its("response.statusCode").should("eq", 200); -}); diff --git a/packages/default-theme/__e2e__/support/index.js b/packages/default-theme/__e2e__/support/index.js deleted file mode 100644 index 413b0ecfa..000000000 --- a/packages/default-theme/__e2e__/support/index.js +++ /dev/null @@ -1,17 +0,0 @@ -// *********************************************************** -// This example support/index.js is processed and -// loaded automatically before your test files. -// -// This is a great place to put global configuration and -// behavior that modifies Cypress. -// -// You can change the location of this file or turn off -// automatically serving support files with the -// 'supportFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/configuration -// *********************************************************** - -// Import commands.js using ES2015 syntax: -import "./commands"; diff --git a/packages/default-theme/package.json b/packages/default-theme/package.json index 95e258c9b..e7fc1359a 100644 --- a/packages/default-theme/package.json +++ b/packages/default-theme/package.json @@ -12,7 +12,8 @@ "build": "shopware-pwa build-theme", "dev": "shopware-pwa dev-theme", "lint": "prettier --write './**/*.{js,vue}'", - "test": "jest" + "test": "jest", + "test:e2e": "playwright test --config=__e2e__/playwright.config.ts" }, "husky": { "hooks": { @@ -35,9 +36,9 @@ }, "peerDependencies": { "@vue/composition-api": "^1.3.1", - "vue": "^2.0.0 || >=3.0.0-rc.0", + "core-js": "^3.19.0", "nuxt": "^2.15.8", - "core-js": "^3.19.0" + "vue": "^2.0.0 || >=3.0.0-rc.0" }, "peerDependenciesMeta": { "@vue/composition-api": { @@ -48,6 +49,7 @@ "@babel/runtime-corejs3": "^7.16.0", "@nuxtjs/eslint-config": "^6.0.1", "@nuxtjs/eslint-module": "^3.0.2", + "@playwright/test": "^1.17.1", "@shopware-pwa/cli": "1.2.0", "@vue/composition-api": "^1.3.1", "@vue/test-utils": "^1.2.2", @@ -61,6 +63,7 @@ "jest": "^27.3.1", "lint-staged": "^11.2.6", "lodash": "^4.17.21", + "playwright": "^1.17.1", "prettier": "^2.4.1", "vue-jest": "^4.0.0-0" } diff --git a/packages/default-theme/src/cms/elements/CmsElementContactForm.vue b/packages/default-theme/src/cms/elements/CmsElementContactForm.vue index 167a001ee..a6d5d9d3a 100644 --- a/packages/default-theme/src/cms/elements/CmsElementContactForm.vue +++ b/packages/default-theme/src/cms/elements/CmsElementContactForm.vue @@ -13,13 +13,13 @@ :label="$t('Salutation')" :valid="!$v.salutationId.$error" :error-message="$t('Salutation must be selected')" - data-cy="salutation-select" + data-testid="salutation-select" > {{ salutationOption.name }} diff --git a/packages/default-theme/src/components/SwBottomMoreActions.vue b/packages/default-theme/src/components/SwBottomMoreActions.vue index 1a4a90581..b1e5628f6 100644 --- a/packages/default-theme/src/components/SwBottomMoreActions.vue +++ b/packages/default-theme/src/components/SwBottomMoreActions.vue @@ -16,7 +16,7 @@