From 7dfa3b5bdce7c441ffb7d34497020150a91391e6 Mon Sep 17 00:00:00 2001 From: Fuxing Loh <4266087+fuxingloh@users.noreply.github.com> Date: Wed, 28 Jun 2023 17:12:39 +0800 Subject: [PATCH] feat(app): setup playwright testing (#52) #### What this PR does / why we need it: For e2e testing. --- .github/labeler.yml | 24 +----------- .github/labels.yml | 4 -- .github/workflows/ci.yml | 61 +++++++++++++++++++++++++++++ README.md | 36 +++++++++++++++++ app/.gitignore | 4 +- app/app/detail.e2e.ts | 10 +++++ app/app/detail.tsx | 1 + app/app/index.e2e.ts | 9 +++++ app/app/index.tsx | 4 +- app/metro.config.js | 1 + app/package.json | 2 + app/playwright.config.js | 55 ++++++++++++++++++++++++++ packages/eslint-config/index.js | 23 +++++++---- packages/eslint-config/package.json | 3 +- yarn.lock | 22 ++++++++++- 15 files changed, 219 insertions(+), 40 deletions(-) create mode 100644 app/app/detail.e2e.ts create mode 100644 app/app/index.e2e.ts create mode 100644 app/playwright.config.js diff --git a/.github/labeler.yml b/.github/labeler.yml index f3714fb..6aea741 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -36,30 +36,10 @@ labels: matcher: files: '.github/**' - - label: area/sticky-jest + - label: area/app sync: true matcher: - files: 'packages/sticky-jest/**' - - - label: area/sticky-prettier - sync: true - matcher: - files: 'packages/sticky-prettier/**' - - - label: area/sticky-turbo - sync: true - matcher: - files: 'packages/sticky-turbo/**' - - - label: area/sticky-turbo-jest - sync: true - matcher: - files: 'packages/sticky-turbo-jest/**' - - - label: area/sticky-typescript - sync: true - matcher: - files: 'packages/sticky-typescript/**' + files: 'app/**' - label: area/eslint-config sync: true diff --git a/.github/labels.yml b/.github/labels.yml index a480046..d6878ce 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -43,10 +43,6 @@ name: kind/dependencies description: Pull requests that update a dependency file -# Area -- color: fbca04 - name: area/contracts - # Priority - color: d93f0b name: priority/urgent-now diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a256c88..1af3661 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,3 +114,64 @@ jobs: with: working-directory: ./app command: eas update --branch pr-${{ github.event.number }} --message "Update with commit ${{ github.sha }}" + + test_playwright: + name: Test [Playwright] + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + project: [chromium] + shardIndex: [1, 2] + shardTotal: [2] + steps: + - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + + - run: corepack enable yarn + + - uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 + with: + node-version-file: '.nvmrc' + cache: yarn + + - run: yarn install --frozen-lockfile + + - uses: actions/cache/restore@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1 + id: cache-restore + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }} + + - name: playwright install + working-directory: app + if: steps.cache-restore.outputs.cache-hit != 'true' + run: yarn playwright install --with-deps + + - name: playwright test + working-directory: app + run: yarn playwright test --project=${{ matrix.project }} --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} + + - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 + if: always() + with: + name: app-playwright + path: app/playwright/ + retention-days: 7 + + - uses: actions/cache/save@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1 + if: always() && steps.cache-restore.outputs.cache-hit != 'true' && matrix.shardIndex == 1 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }} + + test_completed: + name: Test [completed] + runs-on: ubuntu-latest + if: always() + needs: + - test_playwright + steps: + - run: | + if ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled') }} ; then + exit 1 + fi diff --git a/README.md b/README.md index fd7abe1..67818f4 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,42 @@ [![CI](https://github.com/levaintech/keychain/actions/workflows/ci.yml/badge.svg)](https://github.com/levaintech/keychain/actions/workflows/ci.yml) +## Testing + +### Unit (`*.unit.ts` via jest) + +The purpose of unit tests is to test each unit of code in isolation from the rest of the code to quickly pinpoint where +code is and isn't working as expected. You'll put unit tests in the src directory in accompanying each file with the +code that they're testing. + +They must follow the naming semantic of `*.unit.ts` and placed together in the same directory as the code you're +testing. + +### Integration (`*.i9n.ts` via jest) + +Unit tests do have one major disadvantage: even if the units work well in isolation, you do not know if they work well +together. An integration test takes a small group of units, often two units, and tests their behavior as a whole, +verifying that they coherently work together. + +They must follow the naming semantic of `*.i9n.ts` and placed contextually in the same directory as the context you're +testing. + +### End-to-end (`*.e2e.ts` via playwright) + +End-to-end testing verifies that our software works correctly from the beginning to the end of a particular user flow. +It replicates expected user behavior and various usage scenarios to ensure that your software works as whole. e2e +testing should mimic (as much as possible) a production equivalent environment and data to simulate real-world +situations. + +They must follow the naming semantic of `*.e2e.ts` and placed contextually in the same directory as the context you're +testing. + +To have a sane end-to-end testing experience, we use [Playwright](https://playwright.dev/) as our main and only e2e +testing framework. Playwright is built to enable cross-browser web automation that is ever-green, capable, reliable and +fast. While this isn't a replacement for native mobile testing, it does provide a great way to test our application +end-to-end. For avoidance of doubt, this means that we will not be using any other e2e testing framework other than +Playwright. + ## Security > While we strive to be as transparent as possible to maintain the integrity of our ecosystem, we also understand that diff --git a/app/.gitignore b/app/.gitignore index fa80e13..cd2d416 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1,2 +1,2 @@ -tailwind.css -tailwind.json \ No newline at end of file +tailwind.json +playwright/ \ No newline at end of file diff --git a/app/app/detail.e2e.ts b/app/app/detail.e2e.ts new file mode 100644 index 0000000..c8317f5 --- /dev/null +++ b/app/app/detail.e2e.ts @@ -0,0 +1,10 @@ +import { expect, test } from '@playwright/test'; + +test.describe('Detail', async () => { + test('should go to /', async ({ page, baseURL }) => { + await page.goto('/'); + + await page.getByTestId('GoToDetail').click(); + await expect(page.getByTestId('DetailScreen')).toHaveText('Details Screen'); + }); +}); diff --git a/app/app/detail.tsx b/app/app/detail.tsx index c0aa7d3..8fa1649 100644 --- a/app/app/detail.tsx +++ b/app/app/detail.tsx @@ -10,6 +10,7 @@ export default function Details(): JSX.Element { { // Go back to the previous screen using the imperative API. router.back(); diff --git a/app/app/index.e2e.ts b/app/app/index.e2e.ts new file mode 100644 index 0000000..310ec21 --- /dev/null +++ b/app/app/index.e2e.ts @@ -0,0 +1,9 @@ +import { expect, test } from '@playwright/test'; + +test.describe('Example', async () => { + test('should go to /', async ({ page, baseURL }) => { + await page.goto('/'); + await expect(page).toHaveURL(`${baseURL}/`); + await expect(page.getByTestId('KeyChain')).toContainText('KeyChain'); + }); +}); diff --git a/app/app/index.tsx b/app/app/index.tsx index 0195624..a6c63f1 100644 --- a/app/app/index.tsx +++ b/app/app/index.tsx @@ -8,13 +8,13 @@ export default function Home(): JSX.Element { return ( - + KeyChain — Open up index.tsx to start working on your app! {/* Use the `Screen` component to configure the layout. */} {/* Use the `Link` component to enable optimized client-side routing. */} - + Go to Detail diff --git a/app/metro.config.js b/app/metro.config.js index 1444077..06e67bb 100644 --- a/app/metro.config.js +++ b/app/metro.config.js @@ -13,6 +13,7 @@ const postcss = require('postcss'); */ module.exports = (async () => { const config = getDefaultConfig(__dirname); + config.resolver.blockList = /^.+\.e2e\.ts$/; await postCSS(process.env.METRO_CONFIG_JS_WATCH === 'true'); return config; })(); diff --git a/app/package.json b/app/package.json index cc9c6fd..87725e5 100644 --- a/app/package.json +++ b/app/package.json @@ -6,6 +6,7 @@ "scripts": { "export": "expo export", "lint": "eslint .", + "playwright:open": "playwright install && playwright test --ui", "start": "export METRO_CONFIG_JS_WATCH=true && expo start", "version": "node version.mjs" }, @@ -46,6 +47,7 @@ "devDependencies": { "@levain-keychain/eslint-config": "0.0.0", "@parcel/watcher": "^2.1.0", + "@playwright/test": "^1.35.1", "@types/react": "~18.0.27", "@types/react-native": "^0.72.2", "classnames": "^2.3.2", diff --git a/app/playwright.config.js b/app/playwright.config.js new file mode 100644 index 0000000..854dd10 --- /dev/null +++ b/app/playwright.config.js @@ -0,0 +1,55 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: 'app', + testMatch: '*.e2e.ts', + + webServer: { + command: 'yarn run start', + // http://localhost:19000/ isn't reliable to determine if the server is ready. + // However, AppEntry.bundle will only be available after the "Web Bundling complete" message. + url: 'http://localhost:19000/AppEntry.bundle?platform=web&hot=false', + reuseExistingServer: !process.env.CI, + timeout: 180 * 1000, + stdout: 'pipe', + stderr: 'pipe', + }, + + // Run all tests in parallel. + fullyParallel: true, + + // Fail the build on CI if you accidentally left test.only in the source code. + forbidOnly: !!process.env.CI, + + // Retry on CI only. + retries: process.env.CI ? 1 : 0, + + // Opt out of parallel tests on CI. + workers: process.env.CI ? 1 : undefined, + + outputDir: './playwright/test-results', + + // Reporter to use + reporter: [ + // For GitHub Actions CI to generate annotations + ['github'], + ['html', { outputFolder: './playwright/test-report' }], + ], + + use: { + // Base URL to use in actions like `await page.goto('/')`. + baseURL: 'http://localhost:19000', + // Collect trace when retrying the failed test. + trace: 'on', + // equivalent to cypress: macbook-16 + viewport: { width: 1200, height: 1200 }, + video: 'on', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}); diff --git a/packages/eslint-config/index.js b/packages/eslint-config/index.js index cf58155..00c38cc 100644 --- a/packages/eslint-config/index.js +++ b/packages/eslint-config/index.js @@ -1,11 +1,18 @@ module.exports = { - extends: [ - 'universe', - 'universe/shared/typescript-analysis', - '@stickyjs' - ], + extends: ['universe', 'universe/shared/typescript-analysis', '@stickyjs'], rules: { 'import/no-default-export': 'off', - '@typescript-eslint/explicit-function-return-type': ['error', {allowExpressions: true}], - } -}; \ No newline at end of file + '@typescript-eslint/explicit-function-return-type': ['error', { allowExpressions: true }], + }, + overrides: [ + { + files: ['**/*.e2e.ts'], + extends: ['prettier', 'plugin:playwright/playwright-test'], + rules: { + 'unused-imports/no-unused-imports': 'error', + 'unused-imports/no-unused-vars': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + }, + }, + ], +}; diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index e36b074..7b589f5 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -5,6 +5,7 @@ "main": "index.js", "dependencies": { "@stickyjs/eslint-config": "1.2.0", - "eslint-config-universe": "11.2.0" + "eslint-config-universe": "11.2.0", + "eslint-plugin-playwright": "^0.12.0" } } diff --git a/yarn.lock b/yarn.lock index 0dc42ae..6603e35 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1756,6 +1756,16 @@ picocolors "^1.0.0" tslib "^2.5.0" +"@playwright/test@^1.35.1": + version "1.35.1" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.35.1.tgz#a596b61e15b980716696f149cc7a2002f003580c" + integrity sha512-b5YoFe6J9exsMYg0pQAobNDR85T1nLumUYgUTtKm4d21iX2L7WqKq9dW8NGJ+2vX0etZd+Y7UeuqsxDXm9+5ZA== + dependencies: + "@types/node" "*" + playwright-core "1.35.1" + optionalDependencies: + fsevents "2.3.2" + "@radix-ui/react-compose-refs@1.0.0": version "1.0.0" resolved "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz" @@ -4140,6 +4150,11 @@ eslint-plugin-node@^11.1.0: resolve "^1.10.1" semver "^6.1.0" +eslint-plugin-playwright@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-playwright/-/eslint-plugin-playwright-0.12.0.tgz#0c728e07c217b5ea48acef46c52eefba9cf8ebd3" + integrity sha512-KXuzQjVzca5irMT/7rvzJKsVDGbQr43oQPc8i+SLEBqmfrTxlwMwRqfv9vtZqh4hpU0jmrnA/EOfwtls+5QC1w== + eslint-plugin-prettier@^4.2.1: version "4.2.1" resolved "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz" @@ -4835,7 +4850,7 @@ fs.realpath@^1.0.0: resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@^2.3.2, fsevents@~2.3.2: +fsevents@2.3.2, fsevents@^2.3.2, fsevents@~2.3.2: version "2.3.2" resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== @@ -7451,6 +7466,11 @@ pkg-up@^3.1.0: dependencies: find-up "^3.0.0" +playwright-core@1.35.1: + version "1.35.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.35.1.tgz#52c1e6ffaa6a8c29de1a5bdf8cce0ce290ffb81d" + integrity sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg== + plist@^3.0.5: version "3.0.6" resolved "https://registry.npmjs.org/plist/-/plist-3.0.6.tgz"