From 7d2b173bb9de81afb911ded0ee3ce6812ba02995 Mon Sep 17 00:00:00 2001 From: Luis Marsiglia Date: Tue, 18 Oct 2022 17:10:21 -0400 Subject: [PATCH] feat: add e2e tests --- .gitignore | 1 + package.json | 7 +- playwright.config.ts | 49 ++++++++++++++ pnpm-lock.yaml | 152 +++++-------------------------------------- test/e2e.test.ts | 106 ++++++++++++++++++++++++++++++ tsconfig.json | 9 ++- vitest.config.ts | 13 ---- 7 files changed, 185 insertions(+), 152 deletions(-) create mode 100644 playwright.config.ts create mode 100644 test/e2e.test.ts delete mode 100644 vitest.config.ts diff --git a/.gitignore b/.gitignore index c3e8c52..01e3ac9 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ logs node_modules temp .vercel +test/output diff --git a/package.json b/package.json index 58d0af0..a3d45c5 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,8 @@ "lint:fix": "eslint . --fix", "prepublishOnly": "nr build", "release": "bumpp && npm publish", - "test": "vitest", + "test": "pnpm test:e2e", + "test:e2e": "npx playwright test", "typecheck": "tsc --noEmit", "example:dev": "pnpm build && pnpm run --filter=example dev", "example:build": "pnpm run --filter=example build", @@ -48,6 +49,7 @@ "devDependencies": { "@antfu/ni": "0.17.2", "@antfu/utils": "0.5.2", + "@playwright/test": "1.27.1", "@types/react": "18.0.21", "@types/react-dom": "18.0.6", "bumpp": "8.2.1", @@ -55,8 +57,7 @@ "pnpm": "7.12.1", "rimraf": "3.0.2", "typescript": "4.8.4", - "unbuild": "0.8.8", - "vitest": "0.22.1" + "unbuild": "0.8.8" }, "keywords": [ "turnstile", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..4a8fbdb --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,49 @@ +import type { PlaywrightTestConfig } from '@playwright/test' +import { devices } from '@playwright/test' + +const PORT = process.env.PORT || 3000 +const baseURL = `http://localhost:${PORT}` + +// Reference: https://playwright.dev/docs/test-configuration +const config: PlaywrightTestConfig = { + timeout: 30 * 1000, + testDir: 'test', + testMatch: 'test/e2e.test.ts', + forbidOnly: Boolean(process.env.CI), + retries: process.env.CI ? 2 : 0, + + // Run your local dev server before starting the tests: + // https://playwright.dev/docs/test-advanced#launching-a-development-web-server-during-the-tests + webServer: { + command: 'pnpm run --filter=example dev', + url: baseURL, + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI + }, + + use: { + trace: 'retry-with-trace', + headless: true, + baseURL + }, + + projects: [ + { + name: 'Desktop Chrome', + use: { + ...devices['Desktop Chrome'] + } + } + // { + // name: 'Mobile Chrome', + // use: { + // ...devices['Pixel 5'] + // } + // }, + // { + // name: 'Mobile Safari', + // use: devices['iPhone 13'] + // } + ] +} +export default config diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0a3925b..accc279 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,6 +6,7 @@ importers: specifiers: '@antfu/ni': 0.17.2 '@antfu/utils': 0.5.2 + '@playwright/test': 1.27.1 '@types/react': 18.0.21 '@types/react-dom': 18.0.6 bumpp: 8.2.1 @@ -14,10 +15,10 @@ importers: rimraf: 3.0.2 typescript: 4.8.4 unbuild: 0.8.8 - vitest: 0.22.1 devDependencies: '@antfu/ni': 0.17.2 '@antfu/utils': 0.5.2 + '@playwright/test': 1.27.1 '@types/react': 18.0.21 '@types/react-dom': 18.0.6 bumpp: 8.2.1 @@ -26,7 +27,6 @@ importers: rimraf: 3.0.2 typescript: 4.8.4 unbuild: 0.8.8 - vitest: 0.22.1 packages/eslint-config-marsi: specifiers: @@ -588,6 +588,15 @@ packages: tslib: 2.4.0 dev: true + /@playwright/test/1.27.1: + resolution: {integrity: sha512-mrL2q0an/7tVqniQQF6RBL2saskjljXzqNcCOVMUjRIgE6Y38nCNaP+Dc2FBW06bcpD3tqIws/HT9qiMHbNU0A==} + engines: {node: '>=14'} + hasBin: true + dependencies: + '@types/node': 18.11.0 + playwright-core: 1.27.1 + dev: true + /@rollup/plugin-alias/3.1.9_rollup@2.78.1: resolution: {integrity: sha512-QI5fsEvm9bDzt32k39wpOwZhVzRcL5ydcffUHMyLVaVaLeC70I8TJZ17F1z1eMoLu4E/UOcH9BWVkKpIKdrfiw==} engines: {node: '>=8.0.0'} @@ -678,16 +687,6 @@ packages: tslib: 2.4.0 dev: false - /@types/chai-subset/1.3.3: - resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==} - dependencies: - '@types/chai': 4.3.3 - dev: true - - /@types/chai/4.3.3: - resolution: {integrity: sha512-hC7OMnszpxhZPduX+m+nrx+uFoLkWOMiR4oa/AZF3MuSETYTZmFfJAHqZEM8MVlvfG7BEUcgvtwoCTxBp6hm3g==} - dev: true - /@types/estree/0.0.39: resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} dev: true @@ -983,10 +982,6 @@ packages: es-shim-unscopables: 1.0.0 dev: true - /assertion-error/1.1.0: - resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} - dev: true - /ast-types-flow/0.0.7: resolution: {integrity: sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==} dev: true @@ -1103,19 +1098,6 @@ packages: /caniuse-lite/1.0.30001419: resolution: {integrity: sha512-aFO1r+g6R7TW+PNQxKzjITwLOyDhVRLjW0LcwS/HCZGUUKTGNp9+IwLC4xyDSZBygVL/mxaFR3HIV6wEKQuSzw==} - /chai/4.3.6: - resolution: {integrity: sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==} - engines: {node: '>=4'} - dependencies: - assertion-error: 1.1.0 - check-error: 1.0.2 - deep-eql: 3.0.1 - get-func-name: 2.0.0 - loupe: 2.3.4 - pathval: 1.1.1 - type-detect: 4.0.8 - dev: true - /chalk/2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -1138,10 +1120,6 @@ packages: engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} dev: true - /check-error/1.0.2: - resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==} - dev: true - /chokidar/3.5.3: resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} engines: {node: '>= 8.10.0'} @@ -1262,13 +1240,6 @@ packages: ms: 2.1.2 dev: true - /deep-eql/3.0.1: - resolution: {integrity: sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==} - engines: {node: '>=0.12'} - dependencies: - type-detect: 4.0.8 - dev: true - /deep-is/0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true @@ -2429,10 +2400,6 @@ packages: engines: {node: '>=6.9.0'} dev: true - /get-func-name/2.0.0: - resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==} - dev: true - /get-intrinsic/1.1.2: resolution: {integrity: sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==} dependencies: @@ -2857,11 +2824,6 @@ packages: engines: {node: '>=10'} dev: true - /local-pkg/0.4.2: - resolution: {integrity: sha512-mlERgSPrbxU3BP4qBqAvvwlgW4MTg78iwJdGGnv7kibKjWcJksrG3t6LB5lXI93wXRDvG4NpUgJFmTG4T6rdrg==} - engines: {node: '>=14'} - dev: true - /locate-path/2.0.0: resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==} engines: {node: '>=4'} @@ -2887,12 +2849,6 @@ packages: dependencies: js-tokens: 4.0.0 - /loupe/2.3.4: - resolution: {integrity: sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==} - dependencies: - get-func-name: 2.0.0 - dev: true - /lru-cache/6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} @@ -3222,10 +3178,6 @@ packages: resolution: {integrity: sha512-YWgqEdxf36R6vcsyj0A+yT/rDRPe0wui4J9gRR7T4whjU5Lx/jZOr75ckEgTNaLVQABAwsrlzHRpIKcCdXAQ5A==} dev: true - /pathval/1.1.1: - resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} - dev: true - /picocolors/1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} @@ -3247,6 +3199,12 @@ packages: pathe: 0.3.4 dev: true + /playwright-core/1.27.1: + resolution: {integrity: sha512-9EmeXDncC2Pmp/z+teoVYlvmPWUC6ejSSYZUln7YaP89Z6lpAaiaAnqroUt/BoLo8tn7WYShcfaCh+xofZa44Q==} + engines: {node: '>=14'} + hasBin: true + dev: true + /pnpm/7.12.1: resolution: {integrity: sha512-kk60T3TKXutcEnpUNAlJBpG+PzVdYZ3e/g4eivgvmWsY4H3R/8c/nVWTS5Ji/NCPfzO2oUudkN5AyCd0eTKODQ==} engines: {node: '>=14.6'} @@ -3717,16 +3675,6 @@ packages: globrex: 0.1.2 dev: true - /tinypool/0.2.4: - resolution: {integrity: sha512-Vs3rhkUH6Qq1t5bqtb816oT+HeJTXfwt2cbPH17sWHIYKTotQIFPk3tf2fgqRrVyMDVOc1EnPgzIxfIulXVzwQ==} - engines: {node: '>=14.0.0'} - dev: true - - /tinyspy/1.0.2: - resolution: {integrity: sha512-bSGlgwLBYf7PnUsQ6WOc6SJ3pGOcd+d8AA6EUnLDDM0kWEstC1JIlSZA3UNliDXhd9ABoS7hiRBDCu+XP/sf1Q==} - engines: {node: '>=14.0.0'} - dev: true - /to-fast-properties/2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} @@ -3882,72 +3830,6 @@ packages: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: true - /vite/3.1.8: - resolution: {integrity: sha512-m7jJe3nufUbuOfotkntGFupinL/fmuTNuQmiVE7cH2IZMuf4UbfbGYMUT3jVWgGYuRVLY9j8NnrRqgw5rr5QTg==} - engines: {node: ^14.18.0 || >=16.0.0} - hasBin: true - peerDependencies: - less: '*' - sass: '*' - stylus: '*' - terser: ^5.4.0 - peerDependenciesMeta: - less: - optional: true - sass: - optional: true - stylus: - optional: true - terser: - optional: true - dependencies: - esbuild: 0.15.10 - postcss: 8.4.18 - resolve: 1.22.1 - rollup: 2.78.1 - optionalDependencies: - fsevents: 2.3.2 - dev: true - - /vitest/0.22.1: - resolution: {integrity: sha512-+x28YTnSLth4KbXg7MCzoDAzPJlJex7YgiZbUh6YLp0/4PqVZ7q7/zyfdL0OaPtKTpNiQFPpMC8Y2MSzk8F7dw==} - engines: {node: '>=v14.16.0'} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@vitest/browser': '*' - '@vitest/ui': '*' - happy-dom: '*' - jsdom: '*' - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@vitest/browser': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - dependencies: - '@types/chai': 4.3.3 - '@types/chai-subset': 1.3.3 - '@types/node': 18.11.0 - chai: 4.3.6 - debug: 4.3.4 - local-pkg: 0.4.2 - tinypool: 0.2.4 - tinyspy: 1.0.2 - vite: 3.1.8 - transitivePeerDependencies: - - less - - sass - - stylus - - supports-color - - terser - dev: true - /which-boxed-primitive/1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} dependencies: diff --git a/test/e2e.test.ts b/test/e2e.test.ts new file mode 100644 index 0000000..56c993e --- /dev/null +++ b/test/e2e.test.ts @@ -0,0 +1,106 @@ +import type { Browser, Page } from '@playwright/test' +import fs from 'node:fs/promises' +import path from 'node:path' +import { chromium, expect, test } from '@playwright/test' + +const scriptId = 'cf-turnstile-script' +const containerId = 'cf-turnstile' +const demoToken = 'XXXX.DUMMY.TOKEN.XXXX' +let browser: Browser +let page: Page + +const deleteScreenshots = async (directory: string) => { + for (const file of await fs.readdir(directory)) { + await fs.unlink(path.join(directory, file)) + } +} + +const ensureFrameVisible = async () => { + await expect(page.locator('iframe')).toBeVisible() + await expect(page.locator('iframe')).toHaveCount(1) +} + +const ensureFrameHidden = async () => { + await expect(page.locator('iframe')).toBeHidden() + await expect(page.locator('iframe')).toHaveCount(0) +} + +const ensureChallengeSolved = async () => { + await expect(page.locator('[name="cf-turnstile-response"]')).toHaveValue(demoToken) +} + +const ensureChallengeNotSolved = async () => { + await expect(page.locator('[name="cf-turnstile-response"]')).toHaveValue('') +} + +test.describe.configure({ mode: 'serial' }) +const ssPath = './test/output' + +test.use({ + colorScheme: 'dark' +}) + +test.beforeAll(async () => { + await deleteScreenshots(ssPath) + browser = await chromium.launch() + page = await browser.newPage() + await page.goto('/') + await page.screenshot({ path: `${ssPath}/0-before-all.png` }) +}) + +test.afterAll(async () => { + await browser.close() +}) + +test('demo page rendered', async () => { + await expect(page.locator('h1')).toContainText('React Turnstile Demo') +}) + +test('script injected', async () => { + await expect(page.locator(`#${scriptId}`)).toHaveCount(1) +}) + +test('widget container rendered', async () => { + await expect(page.locator(`#${containerId}`)).toHaveCount(1) +}) + +test('widget iframe is visible', async () => { + await ensureFrameVisible() + const iframe = page.frameLocator('iframe[src^="https://challenges.cloudflare.com"]') + await expect(iframe.locator('body')).toContainText('Testing only.') + await page.screenshot({ path: `${ssPath}/1-widget-visible.png` }) +}) + +test('challenge has been solved', async () => { + await ensureChallengeSolved() + await page.screenshot({ path: `${ssPath}/2-challenge-solved.png` }) +}) + +test('widget can be removed', async () => { + await page.locator('button', { hasText: 'Remove' }).click() + await ensureFrameHidden() + await page.screenshot({ path: `${ssPath}/3-widget-removed.png` }) +}) + +test('widget can be explicity rendered', async () => { + await page.locator('button', { hasText: 'Render' }).click() + await ensureFrameVisible() + await ensureChallengeSolved() + await page.screenshot({ path: `${ssPath}/4-widget-rendered.png` }) +}) + +test('widget can be reset', async () => { + await page.locator('button', { hasText: 'Reset' }).click() + await ensureChallengeNotSolved() + await ensureChallengeSolved() + await page.screenshot({ path: `${ssPath}/7-widget-reset.png` }) +}) + +test('can get the token', async () => { + page.on('dialog', async dialog => { + expect(dialog.message()).toContain(demoToken) + await dialog.accept() + }) + + await page.locator('button', { hasText: 'Get response' }).click() +}) diff --git a/tsconfig.json b/tsconfig.json index 1cfffe0..e6a7e2d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,13 @@ "~/*": ["./*"] } }, - "include": ["src", "packages", "build.config.ts", "vitest.config.ts"], + "include": [ + "src", + "packages", + "test", + "build.config.ts", + "vitest.config.ts", + "playwright.config.ts" + ], "exclude": ["node_modules"] } diff --git a/vitest.config.ts b/vitest.config.ts deleted file mode 100644 index 09acac7..0000000 --- a/vitest.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { resolve } from 'path' -import { defineConfig } from 'vitest/config' - -const r = (p: string) => resolve(__dirname, p) - -export default defineConfig({ - test: { - globals: false, - reporters: 'verbose', - environment: 'node' - }, - root: r('./test') -})