diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 00000000..33b52c92 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,38 @@ +name: e2e + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + playwright: + runs-on: ubuntu-latest + container: + image: mcr.microsoft.com/playwright:v1.45.0-jammy + timeout-minutes: 60 + + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Use Node.js 20.10.0 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + with: + node-version: '20.10.0' + cache: 'yarn' + - name: Install dependencies + run: | + rm -rf node_modules && rm -rf ./react-example/node_modules + yarn install --frozen-lockfile && yarn --cwd ./react-example install --frozen-lockfile + - name: Start React Sample App + run: yarn start-without-watch:all:react + - name: Run Playwright tests + run: yarn playwright test + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/package.json b/package.json index e93efb54..1174f342 100644 --- a/package.json +++ b/package.json @@ -43,9 +43,11 @@ }, "scripts": { "start": "concurrently \"ttsc -w\" \"babel src --watch --out-dir ./dist --extensions '.ts,.tsx'\" \"webpack-dev-server --config webpack.dev.config.js\" -n 'tsc,babel,webpack' -k", + "start-without-watch": "concurrently \"webpack-dev-server --config webpack.dev.config.js\" -n 'tsc,webpack' -k", "install:all": "yarn && cd example && yarn && cd ../react-example && yarn", "start:all": "concurrently \"yarn start\" \"cd example && yarn start\" -n 'module,website' -k", "start:all:react": "concurrently \"yarn start\" \"cd react-example && yarn start\" -n 'module,website' -k", + "start-without-watch:all:react": "concurrently \"yarn start-without-watch\" \"cd react-example && yarn start-without-watch\" -n 'module,website' -k", "build": "ttsc && babel src --out-dir ./dist --extensions '.ts,.tsx' && webpack", "build:node": "yarn build --config webpack.node.config.js", "test": "jest --config jest.config.js --collectCoverage", @@ -113,4 +115,4 @@ "eslint" ] } -} \ No newline at end of file +} diff --git a/react-example/e2e/pages/commercePage.ts b/react-example/e2e/pages/commercePage.ts new file mode 100644 index 00000000..2a298dd8 --- /dev/null +++ b/react-example/e2e/pages/commercePage.ts @@ -0,0 +1,60 @@ +import { type Locator, type Page } from '@playwright/test'; + +export class CommercePage { + readonly page: Page; + readonly cartInput: Locator; + readonly purchaseInput: Locator; + readonly cartSubmitButton: Locator; + readonly purchaseSubmitButton: Locator; + readonly cartResponse: Locator; + readonly purchaseResponse: Locator; + + constructor(page: Page) { + this.page = page; + this.cartInput = page.getByTestId('cart-input'); + this.cartSubmitButton = page.getByTestId('cart-submit'); + this.cartResponse = page.getByTestId('cart-response'); + this.purchaseInput = page.getByTestId('purchase-input'); + this.purchaseSubmitButton = page.getByTestId('purchase-submit'); + this.purchaseResponse = page.getByTestId('purchase-response'); + } + + async goto() { + await this.page.goto('/commerce'); + } + + async mock200POSTPurchaseRequest() { + await this.page.route( + 'https://api.iterable.com/api/commerce/trackPurchase', + async (route) => { + const json = { + msg: 'success mocked from playwright', + code: 'Success', + params: { + id: 'mock-playwright-id' + } + }; + await route.fulfill({ json }); + } + ); + } + + async mock400POSTPurchaseRequest() { + await this.page.route( + 'https://api.iterable.com/api/commerce/trackPurchase', + async (route) => { + const json = { + code: 'GenericError', + msg: 'Client-side error mocked from playwright', + clientErrors: [ + { + error: 'items[0].name is a required field', + field: 'items[0].name' + } + ] + }; + await route.fulfill({ json }); + } + ); + } +} diff --git a/react-example/e2e/test/commerce.spec.ts b/react-example/e2e/test/commerce.spec.ts new file mode 100644 index 00000000..424e4360 --- /dev/null +++ b/react-example/e2e/test/commerce.spec.ts @@ -0,0 +1,43 @@ +import { test, expect } from '@playwright/test'; +import { CommercePage } from '../pages/commercePage'; + +test.describe('Commerce tests', () => { + test.beforeEach(async ({ page }) => { + const commercePage = new CommercePage(page); + await commercePage.goto(); + }); + + test('200 POST purchase request', async ({ page }) => { + const commercePage = new CommercePage(page); + await commercePage.mock200POSTPurchaseRequest(); + + await commercePage.purchaseInput.fill('SomeItem'); + await commercePage.purchaseSubmitButton.click(); + await expect(commercePage.purchaseResponse).toContainText( + JSON.stringify({ + msg: 'success mocked from playwright', + code: 'Success', + params: { + id: 'mock-playwright-id' + } + }) + ); + }); + + test('400 POST purchase request', async ({ page }) => { + const commercePage = new CommercePage(page); + await commercePage.mock400POSTPurchaseRequest(); + + await commercePage.purchaseInput.fill('SomeItem'); + await commercePage.purchaseSubmitButton.click(); + await expect(commercePage.purchaseResponse).toContainText( + JSON.stringify({ + code: 'GenericError', + msg: 'Client-side error mocked from playwright', + clientErrors: [ + { error: 'items[0].name is a required field', field: 'items[0].name' } + ] + }) + ); + }); +}); diff --git a/react-example/package.json b/react-example/package.json index e9c18611..fdf2184d 100644 --- a/react-example/package.json +++ b/react-example/package.json @@ -20,12 +20,12 @@ ], "scripts": { "build": "tsc && webpack", - "start": "concurrently \"tsc -w --pretty\" \"webpack-dev-server\" -n 'tsc,webpack' -k", + "start": "concurrently \"tsc -w --pretty\" \"webpack-dev-server\" -n 'tsc,webpack' -k --watch", + "start-without-watch": "concurrently \"tsc -w --pretty\" \"webpack-dev-server\" -n 'tsc,webpack' -k", "test": "jest --config jest.config.js", "format": "prettier --write \"src/**/*.{ts,tsx}\" \"src/**/*.js\"", "typecheck": "tsc --noEmit true --emitDeclarationOnly false", - "lint": "eslint . --ext '.ts,.tsx'", - "cypress": "cypress open" + "lint": "eslint . --ext '.ts,.tsx'" }, "devDependencies": { "@babel/core": "^7.5.0", diff --git a/react-example/playwright.config.ts b/react-example/playwright.config.ts index 7e2134fc..dc6827c8 100644 --- a/react-example/playwright.config.ts +++ b/react-example/playwright.config.ts @@ -23,9 +23,8 @@ export default defineConfig({ reporter: 'html', /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { - /* Base URL to use in actions like `await page.goto('/')`. */ - // baseURL: 'http://127.0.0.1:3000', - + baseURL: 'http://127.0.0.1:8080', + testIdAttribute: 'data-test', /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry' }, diff --git a/react-example/src/views/Commerce.tsx b/react-example/src/views/Commerce.tsx index ce7a7347..0f4d6f3b 100644 --- a/react-example/src/views/Commerce.tsx +++ b/react-example/src/views/Commerce.tsx @@ -67,34 +67,48 @@ export const Commerce: FC = () => {
setCartItem(e.target.value)} id="item-1" placeholder="e.g. keyboard" data-qa-cart-input /> - - {updateCartResponse} + + {updateCartResponse} + POST /trackPurchase
setPurchaseItem(e.target.value)} id="item-2" placeholder="e.g. keyboard" data-qa-purchase-input /> - - {trackPurchaseResponse} + + {trackPurchaseResponse} +
); diff --git a/react-example/webpack.config.js b/react-example/webpack.config.js index d4adb6b0..71f41019 100644 --- a/react-example/webpack.config.js +++ b/react-example/webpack.config.js @@ -12,6 +12,7 @@ module.exports = { filename: 'index.js', path: path.resolve(__dirname, 'dist') }, + watch: process.argv.indexOf('--watch') > -1, module: { rules: [ {