diff --git a/.env.example b/.env.example index 1d4935c12..f04e5452a 100644 --- a/.env.example +++ b/.env.example @@ -7,3 +7,4 @@ SHOPWARE_API_TYPE="store-api" SHOPWARE_ACCESS_TOKEN="" SHOPWARE_USE_SEO_URLS="true" SHOPWARE_REVALIDATION_SECRET="" +BASE_E2E_URL="" \ No newline at end of file diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml new file mode 100644 index 000000000..ecfefe3d7 --- /dev/null +++ b/.github/workflows/e2e-tests.yml @@ -0,0 +1,45 @@ +name: e2e + +on: + deployment_status: + +jobs: + run-e2e: + name: Playwright testing deployment ${{ github.event.deployment_status.target_url }} + if: github.event_name == 'deployment_status' && github.event.deployment_status.state == 'success' && !contains(github.event.deployment_status.target_url, 'frontends-docs') + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v3 + + - name: Install Node.js + uses: actions/setup-node@v3 + with: + node-version: 20 + + - run: corepack enable + - run: pnpm --version + - uses: actions/setup-node@v3 + with: + node-version: 20 + cache: 'pnpm' + cache-dependency-path: '**/pnpm-lock.yaml' + - name: install + run: pnpm install --frozen-lockfile --prefer-offline + + - name: Install dependencies with Playwright + run: | + pnpm playwright install --with-deps + + - name: Run tests + run: pnpm run test:e2e + env: + BASE_E2E_URL: ${{ github.event.deployment_status.target_url }} + + - name: Upload artifacts + uses: actions/upload-artifact@v3 + if: ${{ failure() }} + with: + name: reports + path: apps/e2e-tests/reports/ + retention-days: 7 diff --git a/README.md b/README.md index 30e44d502..60daaec63 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Next.js Commerce with Shopware -A Next.js 13 and App Router-ready ecommerce template featuring: +A Next.js 14 and App Router-ready ecommerce template featuring: - Next.js App Router - Optimized for SEO using Next.js's Metadata diff --git a/e2e-tests/.gitignore b/e2e-tests/.gitignore new file mode 100644 index 000000000..68c5d18f0 --- /dev/null +++ b/e2e-tests/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e-tests/package-lock.json b/e2e-tests/package-lock.json new file mode 100644 index 000000000..8ac95f0f3 --- /dev/null +++ b/e2e-tests/package-lock.json @@ -0,0 +1,91 @@ +{ + "name": "e2e-tests", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "e2e-tests", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.39.0", + "@types/node": "^20.8.10" + } + }, + "node_modules/@playwright/test": { + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.39.0.tgz", + "integrity": "sha512-3u1iFqgzl7zr004bGPYiN/5EZpRUSFddQBra8Rqll5N0/vfpqlP9I9EXqAoGacuAbX6c9Ulg/Cjqglp5VkK6UQ==", + "dev": true, + "dependencies": { + "playwright": "1.39.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@types/node": { + "version": "20.8.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.10.tgz", + "integrity": "sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.39.0.tgz", + "integrity": "sha512-naE5QT11uC/Oiq0BwZ50gDmy8c8WLPRTEWuSSFVG2egBka/1qMoSqYQcROMT9zLwJ86oPofcTH2jBY/5wWOgIw==", + "dev": true, + "dependencies": { + "playwright-core": "1.39.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.39.0.tgz", + "integrity": "sha512-+k4pdZgs1qiM+OUkSjx96YiKsXsmb59evFoqv8SKO067qBA+Z2s/dCzJij/ZhdQcs2zlTAgRKfeiiLm8PQ2qvw==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + } + } +} diff --git a/e2e-tests/package.json b/e2e-tests/package.json new file mode 100644 index 000000000..923378828 --- /dev/null +++ b/e2e-tests/package.json @@ -0,0 +1,19 @@ +{ + "name": "e2e-tests", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": {}, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.39.0", + "@types/node": "^20.8.10" + }, + "dependencies": { + "dotenv": "^16.3.1", + "find-up": "^7.0.0" + }, + "type": "module" +} diff --git a/e2e-tests/page-objects/AbstractPage.ts b/e2e-tests/page-objects/AbstractPage.ts new file mode 100644 index 000000000..c50c5eec5 --- /dev/null +++ b/e2e-tests/page-objects/AbstractPage.ts @@ -0,0 +1,13 @@ +import { Page } from '@playwright/test'; + +export class AbstractPage { + readonly page: Page; + + constructor(page: Page) { + this.page = page; + } + + async wait(time) { + await this.page.waitForTimeout(time); + } +} diff --git a/e2e-tests/page-objects/CategoryPage.ts b/e2e-tests/page-objects/CategoryPage.ts new file mode 100644 index 000000000..845243886 --- /dev/null +++ b/e2e-tests/page-objects/CategoryPage.ts @@ -0,0 +1,27 @@ +import { Page } from '@playwright/test'; + +export class CategoryPage { + readonly page: Page; + + constructor(page: Page) { + this.page = page; + } + + async changePage() { + await this.page.waitForLoadState('networkidle'); + await this.page.getByLabel('Next page').click(); + } + + async checkCategoryFilter() { + await this.page.waitForLoadState('networkidle'); + await this.page.getByRole('link', { name: 'Price: High to low' }).click(); + } +} + +// getByLabel('Pagination', { exact: true }) +// getByLabel('Next page') + +// getByRole('link', { name: 'Price: High to low' }) +// getByRole('link', { name: 'Price: Low to high' }) +// getByRole('link', { name: 'Latest arrivals' }) +// getByRole('link', { name: 'Trending' }) diff --git a/e2e-tests/page-objects/HomePage.ts b/e2e-tests/page-objects/HomePage.ts new file mode 100644 index 000000000..2a67361df --- /dev/null +++ b/e2e-tests/page-objects/HomePage.ts @@ -0,0 +1,36 @@ +import { Locator, Page } from '@playwright/test'; +import { AbstractPage } from './AbstractPage'; + +export class HomePage extends AbstractPage { + //readonly page: Page + readonly sliderLocator: Locator; + + constructor(page: Page) { + super(page); + this.sliderLocator = page.locator("ul[class='flex animate-carousel gap-4']"); + } + + async visitMainPage() { + await this.page.goto('/'); + } + + async openProductPage() { + this.page + .getByRole('link', { name: 'LIGHT CLOTH TAUPE BRIGHT LIGHT CLOTH TAUPE BRIGHT €139.00EUR' }) + .click(); + } + + async openVariantsCartPage() { + await this.page.waitForLoadState('networkidle'); + await this.page.goto('/product/LAVENDA-Product-Variants/SW20004'); + } + + async openCateoryPage() { + await this.page.getByRole('link', { name: 'Products' }).click(); + } + + async goToCmsPages() { + await this.page.getByRole('link', { name: 'Defective Product' }).click(); + await this.page.waitForURL('**/Defective-Product'); + } +} diff --git a/e2e-tests/page-objects/ProductPage.ts b/e2e-tests/page-objects/ProductPage.ts new file mode 100644 index 000000000..55ce0b52f --- /dev/null +++ b/e2e-tests/page-objects/ProductPage.ts @@ -0,0 +1,44 @@ +import { expect, Locator, Page } from '@playwright/test'; + +export class ProductPage { + readonly page: Page; + readonly addToCartButton: Locator; + readonly variant: Locator; + readonly variantText: Locator; + readonly productOption: Locator; + readonly miniCartLink: Locator; + readonly productRemove: Locator; + + constructor(page: Page) { + this.page = page; + this.addToCartButton = page.getByTestId('add-to-cart-button'); + this.variant = page.getByTestId('product-variant'); + this.variantText = page.getByTestId('product-variant-text'); + this.productOption = page.getByTestId('cart-product-options'); + this.miniCartLink = page.getByTestId('cart-button'); + this.productRemove = page.getByTestId('product-remove-button'); + } + + async addToCart() { + await expect(async () => { + await this.page.getByLabel('Add to cart').waitFor(); + await this.page.getByLabel('Add to cart').dispatchEvent('click'); + await expect(this.page.locator('div').filter({ hasText: /^My Cart$/ })).toBeVisible(); + }).toPass({ + // Probe, wait 1s, probe, wait 2s, probe, wait 10s, probe, wait 10s, probe, .... Defaults to [100, 250, 500, 1000]. + intervals: [1_000, 2_000, 10_000], + timeout: 60_000 + }); + } + + async selectVariant() { + await this.page.getByRole('button', { name: 'M' }).click(); + await this.page.waitForLoadState('networkidle'); + await this.page.getByRole('button', { name: 'blue' }).click(); + await this.page.waitForLoadState('networkidle'); + } + + // async changeProductVariant(){ + // await + // } +} diff --git a/e2e-tests/playwright.config.ts b/e2e-tests/playwright.config.ts new file mode 100644 index 000000000..ab8a19f24 --- /dev/null +++ b/e2e-tests/playwright.config.ts @@ -0,0 +1,76 @@ +import type { PlaywrightTestConfig } from '@playwright/test'; +import { devices } from '@playwright/test'; +import dotenv from 'dotenv'; +import { findUpSync } from 'find-up'; + +// export const findEnv = () => findUpSync(process.env.ENV_FILE || ".env"); + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ + +// dotenv.config({ path: findEnv() }); +dotenv.config({ path: findUpSync(process.env.ENV_FILE || '.env') }); +/** + * See https://playwright.dev/docs/test-configuration. + */ +const newLocal = 'http://localhost:3000'; +const baseURL = process.env.BASE_E2E_URL || newLocal; + +console.log('Running tests for: ', baseURL); + +const config: PlaywrightTestConfig = { + testDir: './tests', + outputDir: './reports', + /* Maximum time one test can run for. */ + timeout: 30 * 1000, + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: process.env.CI ? 30000 : 5000 + }, + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: false, //!!process.env.CI + /* Retry on CI only */ + retries: process.env.CI ? 2 : 1, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 4 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://localhost:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'retain-on-failure', + testIdAttribute: 'data-testid', + baseURL + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'] + } + }, + + { + name: 'firefox', + use: { + ...devices['Desktop Firefox'] + } + } + ] +}; + +export default config; diff --git a/e2e-tests/pnpm-lock.yaml b/e2e-tests/pnpm-lock.yaml new file mode 100644 index 000000000..21a56191c --- /dev/null +++ b/e2e-tests/pnpm-lock.yaml @@ -0,0 +1,115 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + dotenv: + specifier: ^16.3.1 + version: 16.3.1 + find-up: + specifier: ^7.0.0 + version: 7.0.0 + +devDependencies: + '@playwright/test': + specifier: ^1.39.0 + version: 1.39.0 + '@types/node': + specifier: ^20.8.10 + version: 20.8.10 + +packages: + + /@playwright/test@1.39.0: + resolution: {integrity: sha512-3u1iFqgzl7zr004bGPYiN/5EZpRUSFddQBra8Rqll5N0/vfpqlP9I9EXqAoGacuAbX6c9Ulg/Cjqglp5VkK6UQ==} + engines: {node: '>=16'} + hasBin: true + dependencies: + playwright: 1.39.0 + dev: true + + /@types/node@20.8.10: + resolution: {integrity: sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w==} + dependencies: + undici-types: 5.26.5 + dev: true + + /dotenv@16.3.1: + resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==} + engines: {node: '>=12'} + dev: false + + /find-up@7.0.0: + resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} + engines: {node: '>=18'} + dependencies: + locate-path: 7.2.0 + path-exists: 5.0.0 + unicorn-magic: 0.1.0 + dev: false + + /fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /locate-path@7.2.0: + resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + p-locate: 6.0.0 + dev: false + + /p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + yocto-queue: 1.0.0 + dev: false + + /p-locate@6.0.0: + resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + p-limit: 4.0.0 + dev: false + + /path-exists@5.0.0: + resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: false + + /playwright-core@1.39.0: + resolution: {integrity: sha512-+k4pdZgs1qiM+OUkSjx96YiKsXsmb59evFoqv8SKO067qBA+Z2s/dCzJij/ZhdQcs2zlTAgRKfeiiLm8PQ2qvw==} + engines: {node: '>=16'} + hasBin: true + dev: true + + /playwright@1.39.0: + resolution: {integrity: sha512-naE5QT11uC/Oiq0BwZ50gDmy8c8WLPRTEWuSSFVG2egBka/1qMoSqYQcROMT9zLwJ86oPofcTH2jBY/5wWOgIw==} + engines: {node: '>=16'} + hasBin: true + dependencies: + playwright-core: 1.39.0 + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + dev: true + + /unicorn-magic@0.1.0: + resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} + engines: {node: '>=18'} + dev: false + + /yocto-queue@1.0.0: + resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} + engines: {node: '>=12.20'} + dev: false diff --git a/e2e-tests/tests/category-tests.spec.ts b/e2e-tests/tests/category-tests.spec.ts new file mode 100644 index 000000000..93ff1ea98 --- /dev/null +++ b/e2e-tests/tests/category-tests.spec.ts @@ -0,0 +1,30 @@ +import { expect, test } from '@playwright/test'; +import { CategoryPage } from '../page-objects/CategoryPage'; +import { HomePage } from '../page-objects/HomePage'; + +test.describe.only('Category pagination', () => { + let homePage: HomePage; + let categoryPage: CategoryPage; + + // Before Hook + test.beforeEach(async ({ page }) => { + homePage = new HomePage(page); + categoryPage = new CategoryPage(page); + + await homePage.visitMainPage(); + }); + + test('Category pagination verification', async ({ page }) => { + await homePage.visitMainPage(); + await homePage.openCateoryPage(); + await categoryPage.changePage(); + await expect(page).toHaveURL('/search/Products?page=2'); + }); + + test('Category filters verification', async ({ page }) => { + await homePage.visitMainPage(); + await homePage.openCateoryPage(); + await categoryPage.checkCategoryFilter(); + await expect(page).toHaveURL('/search/Products?sort=price-desc'); + }); +}); diff --git a/e2e-tests/tests/homepage-tests.spec.ts b/e2e-tests/tests/homepage-tests.spec.ts new file mode 100644 index 000000000..cbfc6e2a9 --- /dev/null +++ b/e2e-tests/tests/homepage-tests.spec.ts @@ -0,0 +1,27 @@ +import { expect, test } from '@playwright/test'; +import { HomePage } from '../page-objects/HomePage'; + +test.describe.only('CMS links', () => { + let homePage: HomePage; + + // Before Hook + test.beforeEach(async ({ page }) => { + homePage = new HomePage(page); + + await homePage.visitMainPage(); + }); + + test('Footer CMS links verification', async ({ page }) => { + await homePage.visitMainPage(); + await homePage.goToCmsPages(); + await page.waitForLoadState('domcontentloaded'); + await expect(page).toHaveTitle( + 'Defective Product | Next.js Commerce with Shopware Composable Frontends' + ); + }); + + test('Home page slider verification', async ({ page }) => { + await homePage.visitMainPage(); + await page.locator("ul[class='flex animate-carousel gap-4']").isVisible(); + }); +}); diff --git a/e2e-tests/tests/product-add-to-cart.spec.ts b/e2e-tests/tests/product-add-to-cart.spec.ts new file mode 100644 index 000000000..cd51e63d4 --- /dev/null +++ b/e2e-tests/tests/product-add-to-cart.spec.ts @@ -0,0 +1,35 @@ +import { expect, test } from '@playwright/test'; +import { HomePage } from '../page-objects/HomePage'; +import { ProductPage } from '../page-objects/ProductPage'; + +test.describe.only('add product to cart', () => { + let homePage: HomePage; + let productPage: ProductPage; + + // Before Hook + test.beforeEach(async ({ page }) => { + homePage = new HomePage(page); + productPage = new ProductPage(page); + + await homePage.visitMainPage(); + }); + + test('Add to cart simple product', async ({ page }) => { + await homePage.visitMainPage(); + await homePage.openProductPage(); + await productPage.addToCart(); + await expect( + page.getByText('LIGHT CLOTH TAUPE BRIGHTLIGHT CLOTH TAUPE BRIGHT€139.00EUR1') + ).toBeVisible(); + }); + + test('Add to cart variant product', async ({ page }) => { + await homePage.visitMainPage(); + await homePage.openVariantsCartPage(); + await productPage.selectVariant(); + await productPage.addToCart(); + await expect( + page.getByText('LAVENDA Product VariantsLAVENDA Product Variants€22.95EUR1') + ).toBeVisible(); + }); +});