diff --git a/.github/workflows/e2e-test-commitly.yml b/.github/workflows/e2e-test-commitly.yml index b0b70b2a0..ccfc611ca 100644 --- a/.github/workflows/e2e-test-commitly.yml +++ b/.github/workflows/e2e-test-commitly.yml @@ -65,12 +65,12 @@ jobs: cd packages/tests-e2e npx playwright install --with-deps - - name: Install and run E2E Test for react + - name: Run Complete tests for react run: | cd packages/tests-e2e BRANCH_NAME=$(echo $BRANCH_NAME_RAW | tr '/_' '-') export PLAYWRIGHT_TEST_URL="https://$BRANCH_NAME.react.playground.corbado.io" - PLAYWRIGHT_TEST_URL=$PLAYWRIGHT_TEST_URL npx playwright test --config=playwright.config.ui.ts + PLAYWRIGHT_TEST_URL=$PLAYWRIGHT_TEST_URL npx playwright test --config=playwright.config.complete.ts env: PLAYWRIGHT_NUM_CORES: 4 PLAYWRIGHT_JWT_TOKEN: ${{ secrets.PLAYWRIGHT_JWT_TOKEN }} @@ -85,6 +85,25 @@ jobs: - uses: actions/upload-artifact@v4 if: always() with: - name: playwright-report + name: playwright-report-complete-react + path: packages/tests-e2e/playwright-report/ + retention-days: 30 + + - name: Run Connect tests for react + run: | + cd packages/tests-e2e + BRANCH_NAME=$(echo $BRANCH_NAME_RAW | tr '/_' '-') + export PLAYWRIGHT_TEST_URL="https://$BRANCH_NAME.connect-next.playground.corbado.io" + PLAYWRIGHT_TEST_URL=$PLAYWRIGHT_TEST_URL npx playwright test --config=playwright.config.connect.ts + env: + PLAYWRIGHT_NUM_CORES: 4 + GITHUB_RUN_ID: ${{ github.run_id }} + SLACK_BOT_USER_OAUTH_TOKEN: ${{ secrets.SLACK_BOT_USER_OAUTH_TOKEN }} + GITHUB_BRANCH_NAME: ${{ env.BRANCH_NAME_RAW }} + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report-connect-react path: packages/tests-e2e/playwright-report/ retention-days: 30 diff --git a/.github/workflows/e2e-test-nightly.yml b/.github/workflows/e2e-test-nightly.yml index a1f06dd59..5250c1979 100644 --- a/.github/workflows/e2e-test-nightly.yml +++ b/.github/workflows/e2e-test-nightly.yml @@ -64,12 +64,12 @@ jobs: cd packages/tests-e2e npx playwright install --with-deps - - name: Install and run E2E Test for react + - name: Run Complete tests for react run: | cd packages/tests-e2e BRANCH_NAME=$(echo $BRANCH_NAME_RAW | tr '/_' '-') export PLAYWRIGHT_TEST_URL="https://$BRANCH_NAME.react.playground.corbado.io" - PLAYWRIGHT_TEST_URL=$PLAYWRIGHT_TEST_URL npx playwright test --config=playwright.config.ui.ts + PLAYWRIGHT_TEST_URL=$PLAYWRIGHT_TEST_URL npx playwright test --config=playwright.config.complete.ts env: PLAYWRIGHT_NUM_CORES: 4 PLAYWRIGHT_JWT_TOKEN: ${{ secrets.PLAYWRIGHT_JWT_TOKEN }} @@ -84,16 +84,16 @@ jobs: - uses: actions/upload-artifact@v4 if: always() with: - name: playwright-report-react + name: playwright-report-complete-react path: packages/tests-e2e/playwright-report/ retention-days: 30 - - name: Install and run E2E Test for web-js + - name: Run Complete tests for web-js run: | cd packages/tests-e2e BRANCH_NAME=$(echo $BRANCH_NAME_RAW | tr '/_' '-') export PLAYWRIGHT_TEST_URL="https://$BRANCH_NAME.web-js.playground.corbado.io" - PLAYWRIGHT_TEST_URL=$PLAYWRIGHT_TEST_URL npx playwright test --config=playwright.config.ui.ts + PLAYWRIGHT_TEST_URL=$PLAYWRIGHT_TEST_URL npx playwright test --config=playwright.config.complete.ts env: PLAYWRIGHT_NUM_CORES: 4 PLAYWRIGHT_JWT_TOKEN: ${{ secrets.PLAYWRIGHT_JWT_TOKEN }} @@ -108,16 +108,16 @@ jobs: - uses: actions/upload-artifact@v4 if: always() with: - name: playwright-report-web-js + name: playwright-report-complete-web-js path: packages/tests-e2e/playwright-report/ retention-days: 30 - - name: Install and run E2E Test for web-js-script + - name: Run Complete tests for web-js-script run: | cd packages/tests-e2e BRANCH_NAME=$(echo $BRANCH_NAME_RAW | tr '/_' '-') export PLAYWRIGHT_TEST_URL="https://$BRANCH_NAME.web-js-script.playground.corbado.io" - PLAYWRIGHT_TEST_URL=$PLAYWRIGHT_TEST_URL npx playwright test --config=playwright.config.ui.ts + PLAYWRIGHT_TEST_URL=$PLAYWRIGHT_TEST_URL npx playwright test --config=playwright.config.complete.ts env: PLAYWRIGHT_NUM_CORES: 4 PLAYWRIGHT_JWT_TOKEN: ${{ secrets.PLAYWRIGHT_JWT_TOKEN }} @@ -132,6 +132,25 @@ jobs: - uses: actions/upload-artifact@v4 if: always() with: - name: playwright-report-web-js-script + name: playwright-report-complete-web-js-script + path: packages/tests-e2e/playwright-report/ + retention-days: 30 + + - name: Run Connect tests for react + run: | + cd packages/tests-e2e + BRANCH_NAME=$(echo $BRANCH_NAME_RAW | tr '/_' '-') + export PLAYWRIGHT_TEST_URL="https://$BRANCH_NAME.connect-next.playground.corbado.io" + PLAYWRIGHT_TEST_URL=$PLAYWRIGHT_TEST_URL npx playwright test --config=playwright.config.connect.ts + env: + PLAYWRIGHT_NUM_CORES: 4 + GITHUB_RUN_ID: ${{ github.run_id }} + SLACK_BOT_USER_OAUTH_TOKEN: ${{ secrets.SLACK_BOT_USER_OAUTH_TOKEN }} + GITHUB_BRANCH_NAME: ${{ env.BRANCH_NAME_RAW }} + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report-connect-react path: packages/tests-e2e/playwright-report/ retention-days: 30 diff --git a/.gitignore b/.gitignore index 2449d64f2..4b08410a6 100644 --- a/.gitignore +++ b/.gitignore @@ -24,7 +24,8 @@ packages/tests-e2e/blob-report packages/tests-e2e/all-blob-reports packages/tests-e2e/playwright/.cach/ packages/tests-e2e/test-states -packages/tests-e2e/.env.local +packages/tests-e2e/.env.complete.local +packages/tests-e2e/.env.connect.local .vercel # scripts diff --git a/package-lock.json b/package-lock.json index 4ad715a1e..25c01a4f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5209,6 +5209,126 @@ "node": ">= 10" } }, + "node_modules/@next/swc-darwin-x64": { + "version": "14.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.4.tgz", + "integrity": "sha512-QVadW73sWIO6E2VroyUjuAxhWLZWEpiFqHdZdoQ/AMpN9YWGuHV8t2rChr0ahy+irKX5mlDU7OY68k3n4tAZTg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.4.tgz", + "integrity": "sha512-KT6GUrb3oyCfcfJ+WliXuJnD6pCpZiosx2X3k66HLR+DMoilRb76LpWPGb4tZprawTtcnyrv75ElD6VncVamUQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.4.tgz", + "integrity": "sha512-Alv8/XGSs/ytwQcbCHwze1HmiIkIVhDHYLjczSVrf0Wi2MvKn/blt7+S6FJitj3yTlMwMxII1gIJ9WepI4aZ/A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.4.tgz", + "integrity": "sha512-ze0ShQDBPCqxLImzw4sCdfnB3lRmN3qGMB2GWDRlq5Wqy4G36pxtNOo2usu/Nm9+V2Rh/QQnrRc2l94kYFXO6Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.4.tgz", + "integrity": "sha512-8dwC0UJoc6fC7PX70csdaznVMNr16hQrTDAMPvLPloazlcaWfdPogq+UpZX6Drqb1OBlwowz8iG7WR0Tzk/diQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.4.tgz", + "integrity": "sha512-jxyg67NbEWkDyvM+O8UDbPAyYRZqGLQDTPwvrBBeOSyVWW/jFQkQKQ70JDqDSYg1ZDdl+E3nkbFbq8xM8E9x8A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.4.tgz", + "integrity": "sha512-twrmN753hjXRdcrZmZttb/m5xaCBFa48Dt3FbeEItpJArxriYDunWxJn+QFXdJ3hPkm4u7CKxncVvnmgQMY1ag==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "14.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.4.tgz", + "integrity": "sha512-tkLrjBzqFTP8DVrAAQmZelEahfR9OxWpFR++vAI9FBhCiIxtwHwBHC23SBHCTURBtwB4kc/x44imVOnkKGNVGg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "license": "MIT", @@ -27350,126 +27470,6 @@ "devDependencies": { "dotenv-webpack": "^8.0.1" } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.4.tgz", - "integrity": "sha512-QVadW73sWIO6E2VroyUjuAxhWLZWEpiFqHdZdoQ/AMpN9YWGuHV8t2rChr0ahy+irKX5mlDU7OY68k3n4tAZTg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.4.tgz", - "integrity": "sha512-KT6GUrb3oyCfcfJ+WliXuJnD6pCpZiosx2X3k66HLR+DMoilRb76LpWPGb4tZprawTtcnyrv75ElD6VncVamUQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.4.tgz", - "integrity": "sha512-Alv8/XGSs/ytwQcbCHwze1HmiIkIVhDHYLjczSVrf0Wi2MvKn/blt7+S6FJitj3yTlMwMxII1gIJ9WepI4aZ/A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.4.tgz", - "integrity": "sha512-ze0ShQDBPCqxLImzw4sCdfnB3lRmN3qGMB2GWDRlq5Wqy4G36pxtNOo2usu/Nm9+V2Rh/QQnrRc2l94kYFXO6Q==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.4.tgz", - "integrity": "sha512-8dwC0UJoc6fC7PX70csdaznVMNr16hQrTDAMPvLPloazlcaWfdPogq+UpZX6Drqb1OBlwowz8iG7WR0Tzk/diQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.4.tgz", - "integrity": "sha512-jxyg67NbEWkDyvM+O8UDbPAyYRZqGLQDTPwvrBBeOSyVWW/jFQkQKQ70JDqDSYg1ZDdl+E3nkbFbq8xM8E9x8A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.4.tgz", - "integrity": "sha512-twrmN753hjXRdcrZmZttb/m5xaCBFa48Dt3FbeEItpJArxriYDunWxJn+QFXdJ3hPkm4u7CKxncVvnmgQMY1ag==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.4.tgz", - "integrity": "sha512-tkLrjBzqFTP8DVrAAQmZelEahfR9OxWpFR++vAI9FBhCiIxtwHwBHC23SBHCTURBtwB4kc/x44imVOnkKGNVGg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } } } } diff --git a/packages/tests-e2e/.env.ci b/packages/tests-e2e/.env.complete.ci similarity index 100% rename from packages/tests-e2e/.env.ci rename to packages/tests-e2e/.env.complete.ci diff --git a/packages/tests-e2e/.env.example b/packages/tests-e2e/.env.complete.example similarity index 100% rename from packages/tests-e2e/.env.example rename to packages/tests-e2e/.env.complete.example diff --git a/packages/tests-e2e/.env.connect.ci b/packages/tests-e2e/.env.connect.ci new file mode 100644 index 000000000..d49ab9346 --- /dev/null +++ b/packages/tests-e2e/.env.connect.ci @@ -0,0 +1,3 @@ +#DEVELOPERPANEL_API_URL=https://console.cloud.corbado-staging.io +#BACKEND_API_URL=https://backendapi.cloud.corbado-staging.io +#FRONTEND_API_URL_SUFFIX=frontendapi.cloud.corbado-staging.io diff --git a/packages/tests-e2e/.env.connect.example b/packages/tests-e2e/.env.connect.example new file mode 100644 index 000000000..6f5490c95 --- /dev/null +++ b/packages/tests-e2e/.env.connect.example @@ -0,0 +1,6 @@ +# save to .env.local +PLAYWRIGHT_TEST_URL=https://develop.connect-next.playground.corbado.io +DEVELOPERPANEL_API_URL=https://console.cloud.corbado-staging.io +BACKEND_API_URL=https://backendapi.cloud.corbado-staging.io +FRONTEND_API_URL_SUFFIX= +PLAYWRIGHT_JWT_TOKEN= diff --git a/packages/tests-e2e/README.md b/packages/tests-e2e/README.md index 7cbb7412c..78fbe14ee 100644 --- a/packages/tests-e2e/README.md +++ b/packages/tests-e2e/README.md @@ -31,13 +31,13 @@ Now Playwright is ready to test the local Playground deployment. ```console $ cd packages/tests-e2e -$ npx playwright test --config=playwright.config.ui.ts --ui --project=nightly +$ npx playwright test --config=playwright.config.complete.ts --ui --project=nightly ``` ### From CLI ```console -npx playwright test --config=playwright.config.ui.ts --project=nightly +npx playwright test --config=playwright.config.complete.ts --project=nightly ``` Alternatively, you can do: diff --git a/packages/tests-e2e/package.json b/packages/tests-e2e/package.json index 6cbd6bce9..e70418723 100644 --- a/packages/tests-e2e/package.json +++ b/packages/tests-e2e/package.json @@ -12,7 +12,10 @@ "url": "git+https://github.com/corbado/javascript.git" }, "scripts": { - "e2e:ui": "playwright test --config=playwright.config.ui.ts", + "e2e:complete": "playwright test --config=playwright.config.complete.ts", + "e2e:complete:ui": "playwright test --config=playwright.config.complete.ts --ui", + "e2e:connect": "playwright test --config=playwright.config.connect.ts", + "e2e:connect:ui": "playwright test --config=playwright.config.connect.ts --ui", "e2e:report": "npx playwright show-report" }, "bugs": { diff --git a/packages/tests-e2e/playwright.config.ui.ts b/packages/tests-e2e/playwright.config.complete.ts similarity index 80% rename from packages/tests-e2e/playwright.config.ui.ts rename to packages/tests-e2e/playwright.config.complete.ts index 390471336..79f2e8140 100644 --- a/packages/tests-e2e/playwright.config.ui.ts +++ b/packages/tests-e2e/playwright.config.complete.ts @@ -2,19 +2,19 @@ import { defineConfig } from '@playwright/test'; import dotenv from 'dotenv'; import path from 'path'; -import { operationTimeout, totalTimeout } from './src/utils/constants'; +import { operationTimeout, totalTimeout } from './src/complete/utils/constants'; if (process.env.CI) { // I have no idea why process.env.PLAYWRIGHT_PROJECT_ID is set as the value in .env.local before // this point. This environment variable is not set in the workflow file (e2e-test.yml), so the // value should theoretically be undefined. For now the 'override' option fixes the issue. - dotenv.config({ path: path.resolve(__dirname, '.env.ci'), override: true }); + dotenv.config({ path: path.resolve(__dirname, '.env.complete.ci'), override: true }); } else { - dotenv.config({ path: path.resolve(__dirname, '.env.local'), override: true }); + dotenv.config({ path: path.resolve(__dirname, '.env.complete.local'), override: true }); } export default defineConfig({ - testDir: './src', + testDir: './src/complete', // fullyParallel: true, forbidOnly: !!process.env.CI, retries: 4, @@ -53,21 +53,22 @@ export default defineConfig({ actionTimeout: operationTimeout, // default: none navigationTimeout: operationTimeout, // default: none baseURL: process.env.PLAYWRIGHT_TEST_URL, - trace: 'retain-on-failure', screenshot: 'only-on-failure', + video: 'retain-on-failure', + trace: 'retain-on-failure', }, projects: [ { name: 'corbado-auth-general', - testMatch: ['ui/corbado-auth-general/*.ts'], + testMatch: ['scenarios/corbado-auth-general/*.ts'], }, { name: 'corbado-auth-component-configs', - testMatch: ['ui/corbado-auth-component-configs/*.ts'], + testMatch: ['scenarios/corbado-auth-component-configs/*.ts'], }, { name: 'passkey-list-general', - testMatch: ['ui/passkey-list-general/*.ts'], + testMatch: ['scenarios/passkey-list-general/*.ts'], }, ], }); diff --git a/packages/tests-e2e/playwright.config.connect.ts b/packages/tests-e2e/playwright.config.connect.ts new file mode 100644 index 000000000..3e01e1a93 --- /dev/null +++ b/packages/tests-e2e/playwright.config.connect.ts @@ -0,0 +1,71 @@ +import { defineConfig } from '@playwright/test'; +import dotenv from 'dotenv'; +import path from 'path'; + +import { operationTimeout, totalTimeout } from './src/connect/utils/Constants'; + +if (process.env.CI) { + dotenv.config({ path: path.resolve(__dirname, '.env.connect.ci'), override: true }); +} else { + dotenv.config({ path: path.resolve(__dirname, '.env.connect.local'), override: true }); +} + +export default defineConfig({ + testDir: './src/connect', + // fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: 4, + workers: process.env.CI + ? process.env.PLAYWRIGHT_NUM_CORES + ? parseInt(process.env.PLAYWRIGHT_NUM_CORES, 10) - 1 + : undefined + : undefined, + reporter: [ + [ + '../../node_modules/playwright-slack-report/dist/src/SlackReporter.js', + { + channels: ['corbado-tests'], + sendResults: 'always', + showInThread: true, + meta: [ + { + key: 'Test Run Info', + value: `https://github.com/corbado/javascript/actions/runs/${process.env.GITHUB_RUN_ID}`, + }, + { key: 'branch', value: `${process.env.GITHUB_BRANCH_NAME}` }, + ], + }, + ], + ['html'], + ], + timeout: totalTimeout, // default: 30000ms + expect: { + timeout: operationTimeout, // default: 5000ms + }, + use: { + actionTimeout: operationTimeout, // default: none + navigationTimeout: operationTimeout, // default: none + baseURL: process.env.PLAYWRIGHT_TEST_URL, + screenshot: 'only-on-failure', + video: 'retain-on-failure', + trace: 'retain-on-failure', + }, + projects: [ + { + name: 'login-component', + testMatch: ['scenarios/login.spec.ts'], + }, + { + name: 'append-component', + testMatch: ['scenarios/append.spec.ts'], + }, + { + name: 'passkey-list-component', + testMatch: ['scenarios/passkey-list.spec.ts'], + }, + { + name: 'misc', + testMatch: ['scenarios/misc.spec.ts'], + }, + ], +}); diff --git a/packages/tests-e2e/src/fixtures/CorbadoAuth.ts b/packages/tests-e2e/src/complete/fixtures/CorbadoAuth.ts similarity index 91% rename from packages/tests-e2e/src/fixtures/CorbadoAuth.ts rename to packages/tests-e2e/src/complete/fixtures/CorbadoAuth.ts index 30dc8e57d..2cd1edf0e 100644 --- a/packages/tests-e2e/src/fixtures/CorbadoAuth.ts +++ b/packages/tests-e2e/src/complete/fixtures/CorbadoAuth.ts @@ -4,7 +4,7 @@ import { CorbadoAuthModel } from '../models/CorbadoAuthModel'; import { VirtualAuthenticator } from '../models/utils/VirtualAuthenticator'; export const test = base.extend<{ model: CorbadoAuthModel }>({ - model: async ({ page }, use, testInfo) => { + model: async ({ page }, use) => { const virtualAuthenticator = new VirtualAuthenticator(); await virtualAuthenticator.initializeCDPSession(page); diff --git a/packages/tests-e2e/src/fixtures/PasskeyList.ts b/packages/tests-e2e/src/complete/fixtures/PasskeyList.ts similarity index 91% rename from packages/tests-e2e/src/fixtures/PasskeyList.ts rename to packages/tests-e2e/src/complete/fixtures/PasskeyList.ts index 7de5c51b6..3082163e7 100644 --- a/packages/tests-e2e/src/fixtures/PasskeyList.ts +++ b/packages/tests-e2e/src/complete/fixtures/PasskeyList.ts @@ -4,7 +4,7 @@ import { PasskeyListModel } from '../models/PasskeyListModel'; import { VirtualAuthenticator } from '../models/utils/VirtualAuthenticator'; export const passkeyListTest = test.extend<{ model: PasskeyListModel }>({ - model: async ({ page }, use, testInfo) => { + model: async ({ page }, use) => { const virtualAuthenticator = new VirtualAuthenticator(); await virtualAuthenticator.initializeCDPSession(page); diff --git a/packages/tests-e2e/src/models/CorbadoAuthModel.ts b/packages/tests-e2e/src/complete/models/CorbadoAuthModel.ts similarity index 100% rename from packages/tests-e2e/src/models/CorbadoAuthModel.ts rename to packages/tests-e2e/src/complete/models/CorbadoAuthModel.ts diff --git a/packages/tests-e2e/src/models/PasskeyListModel.ts b/packages/tests-e2e/src/complete/models/PasskeyListModel.ts similarity index 100% rename from packages/tests-e2e/src/models/PasskeyListModel.ts rename to packages/tests-e2e/src/complete/models/PasskeyListModel.ts index 5e92feb95..ac14fc440 100644 --- a/packages/tests-e2e/src/models/PasskeyListModel.ts +++ b/packages/tests-e2e/src/complete/models/PasskeyListModel.ts @@ -1,12 +1,12 @@ import type { Page } from '@playwright/test'; import { expect } from '@playwright/test'; -import type { VirtualAuthenticator } from './utils/VirtualAuthenticator'; -import { SignupInitBlockModel } from './corbado-auth-blocks/SignupInitBlockModel'; -import { PasskeyAppendBlockModel } from './corbado-auth-blocks/PasskeyAppendBlockModel'; +import { ScreenNames } from '../utils/constants'; // The idea of this model is to only test on PasskeyList import { EmailVerifyBlockModel, OtpCodeType } from './corbado-auth-blocks/EmailVerifyBlockModel'; import { expectScreen } from './corbado-auth-blocks/expectScreen'; -import { ScreenNames } from '../utils/constants'; // The idea of this model is to only test on PasskeyList +import { PasskeyAppendBlockModel } from './corbado-auth-blocks/PasskeyAppendBlockModel'; +import { SignupInitBlockModel } from './corbado-auth-blocks/SignupInitBlockModel'; +import type { VirtualAuthenticator } from './utils/VirtualAuthenticator'; // The idea of this model is to only test on PasskeyList // We don't want to mix the test with other components (e.g. CorbadoAuthModel) diff --git a/packages/tests-e2e/src/models/corbado-auth-blocks/EmailVerifyBlockModel.ts b/packages/tests-e2e/src/complete/models/corbado-auth-blocks/EmailVerifyBlockModel.ts similarity index 97% rename from packages/tests-e2e/src/models/corbado-auth-blocks/EmailVerifyBlockModel.ts rename to packages/tests-e2e/src/complete/models/corbado-auth-blocks/EmailVerifyBlockModel.ts index c5fa257a6..acb6916cb 100644 --- a/packages/tests-e2e/src/models/corbado-auth-blocks/EmailVerifyBlockModel.ts +++ b/packages/tests-e2e/src/complete/models/corbado-auth-blocks/EmailVerifyBlockModel.ts @@ -1,4 +1,4 @@ -import type { BrowserContext, Page } from '@playwright/test'; +import type { Page } from '@playwright/test'; import { expect } from '@playwright/test'; import type { AuthType } from '../../utils/constants'; diff --git a/packages/tests-e2e/src/models/corbado-auth-blocks/LoginInitBlockModel.ts b/packages/tests-e2e/src/complete/models/corbado-auth-blocks/LoginInitBlockModel.ts similarity index 100% rename from packages/tests-e2e/src/models/corbado-auth-blocks/LoginInitBlockModel.ts rename to packages/tests-e2e/src/complete/models/corbado-auth-blocks/LoginInitBlockModel.ts diff --git a/packages/tests-e2e/src/models/corbado-auth-blocks/PasskeyAppendBlockModel.ts b/packages/tests-e2e/src/complete/models/corbado-auth-blocks/PasskeyAppendBlockModel.ts similarity index 97% rename from packages/tests-e2e/src/models/corbado-auth-blocks/PasskeyAppendBlockModel.ts rename to packages/tests-e2e/src/complete/models/corbado-auth-blocks/PasskeyAppendBlockModel.ts index 8653dc549..6c1e457e2 100644 --- a/packages/tests-e2e/src/models/corbado-auth-blocks/PasskeyAppendBlockModel.ts +++ b/packages/tests-e2e/src/complete/models/corbado-auth-blocks/PasskeyAppendBlockModel.ts @@ -17,10 +17,6 @@ export class PasskeyAppendBlockModel { return this.page.getByRole('button', { name: 'Log in' }).click(); } - fillFullName(value: string) { - return; - } - async startPasskeyOperation(complete: boolean) { const operationTrigger = () => this.page.getByRole('button', { name: 'Create account' }).click(); if (complete) { diff --git a/packages/tests-e2e/src/models/corbado-auth-blocks/PasskeyVerifyBlockModel.ts b/packages/tests-e2e/src/complete/models/corbado-auth-blocks/PasskeyVerifyBlockModel.ts similarity index 100% rename from packages/tests-e2e/src/models/corbado-auth-blocks/PasskeyVerifyBlockModel.ts rename to packages/tests-e2e/src/complete/models/corbado-auth-blocks/PasskeyVerifyBlockModel.ts diff --git a/packages/tests-e2e/src/models/corbado-auth-blocks/PhoneVerifyBlockModel.ts b/packages/tests-e2e/src/complete/models/corbado-auth-blocks/PhoneVerifyBlockModel.ts similarity index 100% rename from packages/tests-e2e/src/models/corbado-auth-blocks/PhoneVerifyBlockModel.ts rename to packages/tests-e2e/src/complete/models/corbado-auth-blocks/PhoneVerifyBlockModel.ts diff --git a/packages/tests-e2e/src/models/corbado-auth-blocks/SignupInitBlockModel.ts b/packages/tests-e2e/src/complete/models/corbado-auth-blocks/SignupInitBlockModel.ts similarity index 98% rename from packages/tests-e2e/src/models/corbado-auth-blocks/SignupInitBlockModel.ts rename to packages/tests-e2e/src/complete/models/corbado-auth-blocks/SignupInitBlockModel.ts index f73a77ad5..d96de9c3a 100644 --- a/packages/tests-e2e/src/models/corbado-auth-blocks/SignupInitBlockModel.ts +++ b/packages/tests-e2e/src/complete/models/corbado-auth-blocks/SignupInitBlockModel.ts @@ -49,10 +49,6 @@ export class SignupInitBlockModel { await elem.fill(value); } - fillFullName(value: string) { - return; - } - submitPrimary() { return this.page.getByRole('button', { name: 'Continue', exact: true }).click(); } diff --git a/packages/tests-e2e/src/models/corbado-auth-blocks/expectScreen.ts b/packages/tests-e2e/src/complete/models/corbado-auth-blocks/expectScreen.ts similarity index 100% rename from packages/tests-e2e/src/models/corbado-auth-blocks/expectScreen.ts rename to packages/tests-e2e/src/complete/models/corbado-auth-blocks/expectScreen.ts diff --git a/packages/tests-e2e/src/models/corbado-auth-blocks/socialLogin.ts b/packages/tests-e2e/src/complete/models/corbado-auth-blocks/socialLogin.ts similarity index 100% rename from packages/tests-e2e/src/models/corbado-auth-blocks/socialLogin.ts rename to packages/tests-e2e/src/complete/models/corbado-auth-blocks/socialLogin.ts diff --git a/packages/tests-e2e/src/models/utils/VirtualAuthenticator.ts b/packages/tests-e2e/src/complete/models/utils/VirtualAuthenticator.ts similarity index 100% rename from packages/tests-e2e/src/models/utils/VirtualAuthenticator.ts rename to packages/tests-e2e/src/complete/models/utils/VirtualAuthenticator.ts diff --git a/packages/tests-e2e/src/ui/corbado-auth-component-configs/email-username-verification-at-signup.spec.ts b/packages/tests-e2e/src/complete/scenarios/corbado-auth-component-configs/email-username-verification-at-signup.spec.ts similarity index 100% rename from packages/tests-e2e/src/ui/corbado-auth-component-configs/email-username-verification-at-signup.spec.ts rename to packages/tests-e2e/src/complete/scenarios/corbado-auth-component-configs/email-username-verification-at-signup.spec.ts diff --git a/packages/tests-e2e/src/ui/corbado-auth-component-configs/email-verification-at-login.spec.ts b/packages/tests-e2e/src/complete/scenarios/corbado-auth-component-configs/email-verification-at-login.spec.ts similarity index 100% rename from packages/tests-e2e/src/ui/corbado-auth-component-configs/email-verification-at-login.spec.ts rename to packages/tests-e2e/src/complete/scenarios/corbado-auth-component-configs/email-verification-at-login.spec.ts diff --git a/packages/tests-e2e/src/ui/corbado-auth-component-configs/email-verification-at-signup.spec.ts b/packages/tests-e2e/src/complete/scenarios/corbado-auth-component-configs/email-verification-at-signup.spec.ts similarity index 100% rename from packages/tests-e2e/src/ui/corbado-auth-component-configs/email-verification-at-signup.spec.ts rename to packages/tests-e2e/src/complete/scenarios/corbado-auth-component-configs/email-verification-at-signup.spec.ts diff --git a/packages/tests-e2e/src/ui/corbado-auth-component-configs/email-verification-none.spec.ts b/packages/tests-e2e/src/complete/scenarios/corbado-auth-component-configs/email-verification-none.spec.ts similarity index 100% rename from packages/tests-e2e/src/ui/corbado-auth-component-configs/email-verification-none.spec.ts rename to packages/tests-e2e/src/complete/scenarios/corbado-auth-component-configs/email-verification-none.spec.ts diff --git a/packages/tests-e2e/src/ui/corbado-auth-component-configs/phone-verification-at-signup.spec.ts b/packages/tests-e2e/src/complete/scenarios/corbado-auth-component-configs/phone-verification-at-signup.spec.ts similarity index 100% rename from packages/tests-e2e/src/ui/corbado-auth-component-configs/phone-verification-at-signup.spec.ts rename to packages/tests-e2e/src/complete/scenarios/corbado-auth-component-configs/phone-verification-at-signup.spec.ts diff --git a/packages/tests-e2e/src/ui/corbado-auth-component-configs/username.spec.ts b/packages/tests-e2e/src/complete/scenarios/corbado-auth-component-configs/username.spec.ts similarity index 100% rename from packages/tests-e2e/src/ui/corbado-auth-component-configs/username.spec.ts rename to packages/tests-e2e/src/complete/scenarios/corbado-auth-component-configs/username.spec.ts diff --git a/packages/tests-e2e/src/ui/corbado-auth-general/email-link-verify-obfuscation.spec.ts b/packages/tests-e2e/src/complete/scenarios/corbado-auth-general/email-link-verify-obfuscation.spec.ts similarity index 100% rename from packages/tests-e2e/src/ui/corbado-auth-general/email-link-verify-obfuscation.spec.ts rename to packages/tests-e2e/src/complete/scenarios/corbado-auth-general/email-link-verify-obfuscation.spec.ts diff --git a/packages/tests-e2e/src/ui/corbado-auth-general/email-otp-verify-general.spec.ts b/packages/tests-e2e/src/complete/scenarios/corbado-auth-general/email-otp-verify-general.spec.ts similarity index 100% rename from packages/tests-e2e/src/ui/corbado-auth-general/email-otp-verify-general.spec.ts rename to packages/tests-e2e/src/complete/scenarios/corbado-auth-general/email-otp-verify-general.spec.ts diff --git a/packages/tests-e2e/src/ui/corbado-auth-general/login-init-email.spec.ts b/packages/tests-e2e/src/complete/scenarios/corbado-auth-general/login-init-email.spec.ts similarity index 100% rename from packages/tests-e2e/src/ui/corbado-auth-general/login-init-email.spec.ts rename to packages/tests-e2e/src/complete/scenarios/corbado-auth-general/login-init-email.spec.ts diff --git a/packages/tests-e2e/src/ui/corbado-auth-general/login-init-no-public-signup.spec.ts b/packages/tests-e2e/src/complete/scenarios/corbado-auth-general/login-init-no-public-signup.spec.ts similarity index 98% rename from packages/tests-e2e/src/ui/corbado-auth-general/login-init-no-public-signup.spec.ts rename to packages/tests-e2e/src/complete/scenarios/corbado-auth-general/login-init-no-public-signup.spec.ts index fa8e24f8f..284c4f43d 100644 --- a/packages/tests-e2e/src/ui/corbado-auth-general/login-init-no-public-signup.spec.ts +++ b/packages/tests-e2e/src/complete/scenarios/corbado-auth-general/login-init-no-public-signup.spec.ts @@ -37,7 +37,7 @@ test.describe('login-init no public signup', () => { await expect(page.getByRole('button', { name: 'Sign up' })).toBeHidden(); }); - test('switch to signup should not be possible (hashCode)', async ({ model, page }) => { + test('switch to signup should not be possible (hashCode)', async ({ model }) => { await model.load(projectId, true, 'signup-init'); await model.expectScreen(ScreenNames.InitLogin); diff --git a/packages/tests-e2e/src/ui/corbado-auth-general/phone-otp-verify-obfuscation.spec.ts b/packages/tests-e2e/src/complete/scenarios/corbado-auth-general/phone-otp-verify-obfuscation.spec.ts similarity index 100% rename from packages/tests-e2e/src/ui/corbado-auth-general/phone-otp-verify-obfuscation.spec.ts rename to packages/tests-e2e/src/complete/scenarios/corbado-auth-general/phone-otp-verify-obfuscation.spec.ts diff --git a/packages/tests-e2e/src/ui/corbado-auth-general/routing.spec.ts b/packages/tests-e2e/src/complete/scenarios/corbado-auth-general/routing.spec.ts similarity index 100% rename from packages/tests-e2e/src/ui/corbado-auth-general/routing.spec.ts rename to packages/tests-e2e/src/complete/scenarios/corbado-auth-general/routing.spec.ts diff --git a/packages/tests-e2e/src/ui/corbado-auth-general/signup-init.spec.ts b/packages/tests-e2e/src/complete/scenarios/corbado-auth-general/signup-init.spec.ts similarity index 100% rename from packages/tests-e2e/src/ui/corbado-auth-general/signup-init.spec.ts rename to packages/tests-e2e/src/complete/scenarios/corbado-auth-general/signup-init.spec.ts diff --git a/packages/tests-e2e/src/ui/corbado-auth-general/socials.spec.ts b/packages/tests-e2e/src/complete/scenarios/corbado-auth-general/socials.spec.ts similarity index 100% rename from packages/tests-e2e/src/ui/corbado-auth-general/socials.spec.ts rename to packages/tests-e2e/src/complete/scenarios/corbado-auth-general/socials.spec.ts diff --git a/packages/tests-e2e/src/ui/passkey-list-general/general.spec.ts b/packages/tests-e2e/src/complete/scenarios/passkey-list-general/general.spec.ts similarity index 100% rename from packages/tests-e2e/src/ui/passkey-list-general/general.spec.ts rename to packages/tests-e2e/src/complete/scenarios/passkey-list-general/general.spec.ts diff --git a/packages/tests-e2e/src/utils/constants.ts b/packages/tests-e2e/src/complete/utils/constants.ts similarity index 100% rename from packages/tests-e2e/src/utils/constants.ts rename to packages/tests-e2e/src/complete/utils/constants.ts diff --git a/packages/tests-e2e/src/utils/developerpanel.ts b/packages/tests-e2e/src/complete/utils/developerpanel.ts similarity index 100% rename from packages/tests-e2e/src/utils/developerpanel.ts rename to packages/tests-e2e/src/complete/utils/developerpanel.ts diff --git a/packages/tests-e2e/src/utils/random.ts b/packages/tests-e2e/src/complete/utils/random.ts similarity index 100% rename from packages/tests-e2e/src/utils/random.ts rename to packages/tests-e2e/src/complete/utils/random.ts diff --git a/packages/tests-e2e/src/connect/fixtures/BaseTest.ts b/packages/tests-e2e/src/connect/fixtures/BaseTest.ts new file mode 100644 index 000000000..19c5cbb73 --- /dev/null +++ b/packages/tests-e2e/src/connect/fixtures/BaseTest.ts @@ -0,0 +1,17 @@ +import { test as base } from '@playwright/test'; + +import { BaseModel } from '../models/BaseModel'; +import { VirtualAuthenticator } from '../utils/VirtualAuthenticator'; + +export const test = base.extend<{ model: BaseModel }>({ + model: async ({ page }, use) => { + const authenticator = new VirtualAuthenticator(); + await authenticator.initializeCDPSession(page); + + const model = new BaseModel(page, authenticator); + + await use(model); + }, +}); + +export { expect } from '@playwright/test'; diff --git a/packages/tests-e2e/src/connect/models/AppendModel.ts b/packages/tests-e2e/src/connect/models/AppendModel.ts new file mode 100644 index 000000000..f17b0c7f9 --- /dev/null +++ b/packages/tests-e2e/src/connect/models/AppendModel.ts @@ -0,0 +1,26 @@ +import type { Page } from '@playwright/test'; + +import type { VirtualAuthenticator } from '../utils/VirtualAuthenticator'; + +export class AppendModel { + page: Page; + authenticator: VirtualAuthenticator; + + constructor(page: Page, authenticator: VirtualAuthenticator) { + this.page = page; + this.authenticator = authenticator; + } + + async appendPasskey(): Promise { + const operationTrigger = () => this.page.getByRole('button', { name: 'Continue' }).click(); + await this.authenticator.startAndCompletePasskeyOperation(operationTrigger); + } + + confirmAppended(): Promise { + return this.page.getByRole('button', { name: 'Continue' }).click(); + } + + skipAppend(): Promise { + return this.page.locator('.cb-append-skip').click(); + } +} diff --git a/packages/tests-e2e/src/connect/models/BaseModel.ts b/packages/tests-e2e/src/connect/models/BaseModel.ts new file mode 100644 index 000000000..996225a3c --- /dev/null +++ b/packages/tests-e2e/src/connect/models/BaseModel.ts @@ -0,0 +1,89 @@ +import type { Page } from '@playwright/test'; + +import { ScreenNames } from '../utils/Constants'; +import { expectScreen } from '../utils/ExpectScreen'; +import type { VirtualAuthenticator } from '../utils/VirtualAuthenticator'; +import { AppendModel } from './AppendModel'; +import { HomeModel } from './HomeModel'; +import { LoginModel } from './LoginModel'; +import { PasskeyListModel } from './PasskeyListModel'; +import { SignupModel } from './SignupModel'; + +export class BaseModel { + page: Page; + authenticator: VirtualAuthenticator; + signup: SignupModel; + login: LoginModel; + append: AppendModel; + home: HomeModel; + passkeyList: PasskeyListModel; + email = ''; + + constructor(page: Page, authenticator: VirtualAuthenticator) { + this.page = page; + this.authenticator = authenticator; + this.signup = new SignupModel(page); + this.login = new LoginModel(page, authenticator); + this.append = new AppendModel(page, authenticator); + this.home = new HomeModel(page); + this.passkeyList = new PasskeyListModel(page, authenticator); + } + + addWebAuthn() { + return this.authenticator.addWebAuthn(); + } + + removeWebAuthn() { + return this.authenticator.removeWebAuthn(); + } + + loadInvitationToken() { + return this.page.goto('/login?invitationToken=inv-token-correct'); + } + + loadSignup() { + return this.page.goto('/signup'); + } + + loadLogin() { + return this.page.goto('/login'); + } + + expectScreen(screenName: ScreenNames) { + return expectScreen(this.page, screenName); + } + + async createUser(invited: boolean, append: boolean) { + this.email = await this.signup.autofillCredentials(); + await this.signup.submit(); + if (invited) { + await this.expectScreen(ScreenNames.PasskeyAppend); + if (append) { + await this.append.appendPasskey(); + await this.expectScreen(ScreenNames.PasskeyAppended); + await this.append.confirmAppended(); + } else { + await this.append.skipAppend(); + } + } + } + + // async deleteUser() { + // const cookies = await this.page.context().cookies(); + // const longSessionCookie = cookies.find(cookie => cookie.name === 'cbo_long_session'); + // const longSessionCookieValue = longSessionCookie?.value; + // + // expect(longSessionCookieValue).toBeDefined(); + // expect(process.env.FRONTEND_API_URL).toBeDefined(); + // + // const response = await fetch(`${process.env.FRONTEND_API_URL}/v2/me`, { + // method: 'DELETE', + // headers: { + // 'Content-Type': 'application/json', + // Cookie: `cbo_long_session=${longSessionCookieValue}`, + // }, + // }); + // + // expect(response.ok).toBeTruthy(); + // } +} diff --git a/packages/tests-e2e/src/connect/models/HomeModel.ts b/packages/tests-e2e/src/connect/models/HomeModel.ts new file mode 100644 index 000000000..d412a4cd1 --- /dev/null +++ b/packages/tests-e2e/src/connect/models/HomeModel.ts @@ -0,0 +1,17 @@ +import type { Page } from '@playwright/test'; + +export class HomeModel { + page: Page; + + constructor(page: Page) { + this.page = page; + } + + logout(): Promise { + return this.page.getByRole('button', { name: 'Logout' }).click(); + } + + gotoPasskeyList(): Promise { + return this.page.getByRole('button', { name: 'Passkey List' }).click(); + } +} diff --git a/packages/tests-e2e/src/connect/models/LoginModel.ts b/packages/tests-e2e/src/connect/models/LoginModel.ts new file mode 100644 index 000000000..eed6e05c3 --- /dev/null +++ b/packages/tests-e2e/src/connect/models/LoginModel.ts @@ -0,0 +1,43 @@ +import { expect } from '@playwright/test'; +import type { Page } from '@playwright/test'; + +import type { VirtualAuthenticator } from '../utils/VirtualAuthenticator'; + +export class LoginModel { + page: Page; + authenticator: VirtualAuthenticator; + + constructor(page: Page, authenticator: VirtualAuthenticator) { + this.page = page; + this.authenticator = authenticator; + } + + submitPasskeyButton(): Promise { + const operationTrigger = () => this.page.locator('.cb-passkey-button').click(); + return this.authenticator.startAndCompletePasskeyOperation(operationTrigger); + } + + removePasskeyButton(): Promise { + return this.page.locator('.cb-switch').click(); + } + + async submitEmail(email: string, withPasskey: boolean): Promise { + await this.page.getByLabel('Email address').fill(email); + if (withPasskey) { + const operationTrigger = () => this.page.getByRole('button', { name: 'Login' }).click(); + await this.authenticator.startAndCompletePasskeyOperation(operationTrigger); + } else { + await this.page.getByRole('button', { name: 'Login' }).click(); + } + } + + async submitFallbackCredentials(email: string, password: string, emailAutofilled = false): Promise { + if (emailAutofilled) { + await expect(this.page.getByPlaceholder('Email')).toHaveValue(email); + } else { + await this.page.getByPlaceholder('Email').fill(email); + } + await this.page.getByPlaceholder('Password').fill(password); + await this.page.getByRole('button', { name: 'Login' }).click(); + } +} diff --git a/packages/tests-e2e/src/connect/models/PasskeyListModel.ts b/packages/tests-e2e/src/connect/models/PasskeyListModel.ts new file mode 100644 index 000000000..52f47392e --- /dev/null +++ b/packages/tests-e2e/src/connect/models/PasskeyListModel.ts @@ -0,0 +1,36 @@ +import type { Page } from '@playwright/test'; +import { expect } from '@playwright/test'; + +import type { VirtualAuthenticator } from '../utils/VirtualAuthenticator'; + +export class PasskeyListModel { + page: Page; + authenticator: VirtualAuthenticator; + + constructor(page: Page, authenticator: VirtualAuthenticator) { + this.page = page; + this.authenticator = authenticator; + } + + expectPasskeys(n: number) { + return expect(this.page.locator('.cb-passkey-list-item')).toHaveCount(n); + } + + async deletePasskey(index: number) { + await this.page.locator('.cb-passkey-list-item-delete-icon').nth(index).click(); + await this.page.getByRole('button', { name: 'Delete' }).click(); + } + + async appendPasskey(complete: boolean) { + const operationTrigger = () => this.page.getByRole('button', { name: 'Add a passkey' }).click(); + if (complete) { + await this.authenticator.startAndCompletePasskeyOperation(operationTrigger); + } else { + await this.authenticator.startAndCancelPasskeyOperation(operationTrigger, () => + expect(this.page.locator('.cb-notification-text')).toHaveText( + 'You have cancelled setting up your passkey. Please try again.', + ), + ); + } + } +} diff --git a/packages/tests-e2e/src/connect/models/SignupModel.ts b/packages/tests-e2e/src/connect/models/SignupModel.ts new file mode 100644 index 000000000..5b9cd1097 --- /dev/null +++ b/packages/tests-e2e/src/connect/models/SignupModel.ts @@ -0,0 +1,18 @@ +import type { Page } from '@playwright/test'; + +export class SignupModel { + page: Page; + + constructor(page: Page) { + this.page = page; + } + + async autofillCredentials(): Promise { + await this.page.getByRole('button', { name: 'auto' }).click(); + return await this.page.getByPlaceholder('Email').inputValue(); + } + + submit(): Promise { + return this.page.getByRole('button', { name: 'Sign up' }).click(); + } +} diff --git a/packages/tests-e2e/src/connect/scenarios/append.spec.ts b/packages/tests-e2e/src/connect/scenarios/append.spec.ts new file mode 100644 index 000000000..9afd56cc3 --- /dev/null +++ b/packages/tests-e2e/src/connect/scenarios/append.spec.ts @@ -0,0 +1,17 @@ +import { test } from '../fixtures/BaseTest'; +import { ScreenNames } from '../utils/Constants'; +import { loadPasskeyAppend, setupUser, setupVirtualAuthenticator } from './hooks'; + +test.describe('append component', () => { + setupVirtualAuthenticator(test); + setupUser(test, true, false); + loadPasskeyAppend(test); + + test('successful passkey append on login', async ({ model }) => { + await model.append.appendPasskey(); + await model.expectScreen(ScreenNames.PasskeyAppended); + + await model.append.confirmAppended(); + await model.expectScreen(ScreenNames.Home); + }); +}); diff --git a/packages/tests-e2e/src/connect/scenarios/hooks.ts b/packages/tests-e2e/src/connect/scenarios/hooks.ts new file mode 100644 index 000000000..eaa287ded --- /dev/null +++ b/packages/tests-e2e/src/connect/scenarios/hooks.ts @@ -0,0 +1,113 @@ +import type { + PlaywrightTestArgs, + PlaywrightTestOptions, + PlaywrightWorkerArgs, + PlaywrightWorkerOptions, + TestType, +} from '@playwright/test'; + +import type { BaseModel } from '../models/BaseModel'; +import { password, ScreenNames } from '../utils/Constants'; + +export function setupVirtualAuthenticator( + test: TestType< + PlaywrightTestArgs & PlaywrightTestOptions & { model: BaseModel }, + PlaywrightWorkerArgs & PlaywrightWorkerOptions + >, +) { + test.beforeEach(async ({ model }) => { + await model.addWebAuthn(); + }); + + test.afterEach(async ({ model }) => { + await model.removeWebAuthn(); + }); +} + +// export function loadInvitationToken( +// test: TestType< +// PlaywrightTestArgs & PlaywrightTestOptions & { model: BaseModel }, +// PlaywrightWorkerArgs & PlaywrightWorkerOptions +// >, +// ) { +// test.beforeEach(async ({ model }) => { +// await model.loadInvitationToken(); +// }); +// } + +export function loadSignup( + test: TestType< + PlaywrightTestArgs & PlaywrightTestOptions & { model: BaseModel }, + PlaywrightWorkerArgs & PlaywrightWorkerOptions + >, +) { + test.beforeEach(async ({ model }) => { + await model.loadSignup(); + }); +} + +export function loadLogin( + test: TestType< + PlaywrightTestArgs & PlaywrightTestOptions & { model: BaseModel }, + PlaywrightWorkerArgs & PlaywrightWorkerOptions + >, +) { + test.beforeEach(async ({ model }) => { + await model.loadLogin(); + }); +} + +export function setupUser( + test: TestType< + PlaywrightTestArgs & PlaywrightTestOptions & { model: BaseModel }, + PlaywrightWorkerArgs & PlaywrightWorkerOptions + >, + invited = true, + append = true, +) { + test.beforeEach(async ({ model }) => { + if (invited) { + await model.loadInvitationToken(); + } + await model.loadSignup(); + await model.expectScreen(ScreenNames.InitSignup); + await model.createUser(invited, append); + await model.expectScreen(ScreenNames.Home); + }); + + // test.afterEach(async ({ model }) => { + // await model.deleteUser(); + // }); +} + +// assumes that setupUser(test, true, false) has been called right before +export function loadPasskeyAppend( + test: TestType< + PlaywrightTestArgs & PlaywrightTestOptions & { model: BaseModel }, + PlaywrightWorkerArgs & PlaywrightWorkerOptions + >, +) { + test.beforeEach(async ({ model }) => { + await model.home.logout(); + await model.expectScreen(ScreenNames.InitLogin); + + await model.login.submitEmail(model.email, false); + await model.expectScreen(ScreenNames.InitLoginFallback); + + await model.login.submitFallbackCredentials(model.email, password, true); + await model.expectScreen(ScreenNames.PasskeyAppend); + }); +} + +// assumes that setupUser(test, true, true) has been called right before +export function loadPasskeyList( + test: TestType< + PlaywrightTestArgs & PlaywrightTestOptions & { model: BaseModel }, + PlaywrightWorkerArgs & PlaywrightWorkerOptions + >, +) { + test.beforeEach(async ({ model }) => { + await model.home.gotoPasskeyList(); + await model.expectScreen(ScreenNames.PasskeyList); + }); +} diff --git a/packages/tests-e2e/src/connect/scenarios/login.spec.ts b/packages/tests-e2e/src/connect/scenarios/login.spec.ts new file mode 100644 index 000000000..230229727 --- /dev/null +++ b/packages/tests-e2e/src/connect/scenarios/login.spec.ts @@ -0,0 +1,58 @@ +import { test } from '../fixtures/BaseTest'; +import { password, ScreenNames } from '../utils/Constants'; +import { setupUser, setupVirtualAuthenticator } from './hooks'; + +test.describe('login component (without invitation token)', () => { + setupUser(test, false); + + test('successful login with credentials', async ({ model }) => { + await model.home.logout(); + await model.expectScreen(ScreenNames.InitLoginFallback); + + await model.login.submitFallbackCredentials(model.email, password); + await model.expectScreen(ScreenNames.Home); + }); +}); + +test.describe('login component (with invitation token, without passkeys)', () => { + setupVirtualAuthenticator(test); + setupUser(test, true, false); + + test('successful login with credentials', async ({ model }) => { + await model.home.logout(); + await model.expectScreen(ScreenNames.InitLogin); + + await model.login.submitEmail(model.email, false); + await model.expectScreen(ScreenNames.InitLoginFallback); + + await model.login.submitFallbackCredentials(model.email, password, true); + await model.expectScreen(ScreenNames.PasskeyAppend); + + await model.append.skipAppend(); + await model.expectScreen(ScreenNames.Home); + }); +}); + +test.describe('login component (with invitation token, with passkeys)', () => { + setupVirtualAuthenticator(test); + setupUser(test, true, true); + + test('successful login with passkey (one-tap)', async ({ model }) => { + await model.home.logout(); + await model.expectScreen(ScreenNames.InitLoginOneTap); + + await model.login.submitPasskeyButton(); + await model.expectScreen(ScreenNames.Home); + }); + + test('successful login with passkey', async ({ model }) => { + await model.home.logout(); + await model.expectScreen(ScreenNames.InitLoginOneTap); + + await model.login.removePasskeyButton(); + await model.expectScreen(ScreenNames.InitLogin); + + await model.login.submitEmail(model.email, true); + await model.expectScreen(ScreenNames.Home); + }); +}); diff --git a/packages/tests-e2e/src/connect/scenarios/misc.spec.ts b/packages/tests-e2e/src/connect/scenarios/misc.spec.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/tests-e2e/src/connect/scenarios/passkey-list.spec.ts b/packages/tests-e2e/src/connect/scenarios/passkey-list.spec.ts new file mode 100644 index 000000000..1d102baae --- /dev/null +++ b/packages/tests-e2e/src/connect/scenarios/passkey-list.spec.ts @@ -0,0 +1,16 @@ +import { test } from '../fixtures/BaseTest'; +import { loadPasskeyList, setupUser, setupVirtualAuthenticator } from './hooks'; + +test.describe('passkey-list component', () => { + setupVirtualAuthenticator(test); + setupUser(test, true, true); + loadPasskeyList(test); + + test('list, delete, append passkey', async ({ model }) => { + await model.passkeyList.expectPasskeys(1); + await model.passkeyList.deletePasskey(0); + await model.passkeyList.expectPasskeys(0); + await model.passkeyList.appendPasskey(true); + await model.passkeyList.expectPasskeys(1); + }); +}); diff --git a/packages/tests-e2e/src/connect/utils/Constants.ts b/packages/tests-e2e/src/connect/utils/Constants.ts new file mode 100644 index 000000000..3b3a444d4 --- /dev/null +++ b/packages/tests-e2e/src/connect/utils/Constants.ts @@ -0,0 +1,16 @@ +export enum ScreenNames { + InitSignup, + InitLogin, + InitLoginFallback, + InitLoginOneTap, + PasskeyAppend, + PasskeyAppended, + Home, + PasskeyList, +} + +export const phone = '+4915121609839'; +export const password = 'asdfasdf'; + +export const totalTimeout = process.env.CI ? 30000 : 40000; +export const operationTimeout = 10000; diff --git a/packages/tests-e2e/src/connect/utils/ExpectScreen.ts b/packages/tests-e2e/src/connect/utils/ExpectScreen.ts new file mode 100644 index 000000000..a27179fb8 --- /dev/null +++ b/packages/tests-e2e/src/connect/utils/ExpectScreen.ts @@ -0,0 +1,47 @@ +import type { Page } from '@playwright/test'; +import { expect } from '@playwright/test'; + +import { ScreenNames } from './Constants'; + +export const expectScreen = async (page: Page, screenName: ScreenNames) => { + switch (screenName) { + case ScreenNames.InitSignup: + await expect(page.locator('div.font-bold.text-xl')).toHaveText('Signup'); + return; + + case ScreenNames.InitLogin: + await expect(page.locator('div.cb-connect-login').getByRole('textbox', { name: 'Email address' })).toBeVisible(); + return; + + case ScreenNames.InitLoginFallback: + await expect(page.locator('div.font-bold.text-xl')).toHaveText('Login'); + return; + + case ScreenNames.InitLoginOneTap: + await expect(page.locator('div.cb-connect-login')).toContainText('Welcome back'); + return; + + case ScreenNames.PasskeyAppend: + await expect(page.locator('.cb-connect-container').locator('.cb-append-header')).toContainText( + 'Simplify Your Login', + ); + return; + + case ScreenNames.PasskeyAppended: + await expect(page.locator('.cb-connect-container').locator('.cb-append-success-header')).toContainText( + 'Passkey Created Successfully', + ); + return; + + case ScreenNames.Home: + await expect(page.locator('div.font-bold.text-xl')).toHaveText('Home'); + return; + + case ScreenNames.PasskeyList: + await expect(page.locator('.cb-connect-passkey-list')).toBeVisible(); + return; + + default: + throw new Error('Invalid screen'); + } +}; diff --git a/packages/tests-e2e/src/connect/utils/VirtualAuthenticator.ts b/packages/tests-e2e/src/connect/utils/VirtualAuthenticator.ts new file mode 100644 index 000000000..a771ecc86 --- /dev/null +++ b/packages/tests-e2e/src/connect/utils/VirtualAuthenticator.ts @@ -0,0 +1,94 @@ +import type { CDPSession, Page } from '@playwright/test'; + +import { operationTimeout } from './Constants'; + +export class VirtualAuthenticator { + #cdpClient: CDPSession | null = null; + #authenticatorId = ''; + + async initializeCDPSession(page: Page) { + this.#cdpClient = await page.context().newCDPSession(page); + } + + async addWebAuthn(passkeySupported = true) { + if (!this.#cdpClient) { + throw new Error('CDP client not initialized'); + } + + await this.#cdpClient.send('WebAuthn.enable'); + const result = await this.#cdpClient.send('WebAuthn.addVirtualAuthenticator', { + options: passkeySupported + ? { + protocol: 'ctap2', + transport: 'internal', + hasResidentKey: true, + hasUserVerification: true, + automaticPresenceSimulation: false, + } + : { + protocol: 'u2f', + transport: 'usb', + }, + }); + + this.#authenticatorId = result.authenticatorId; + } + + async removeWebAuthn() { + if (!this.#cdpClient) { + throw new Error('CDP client not initialized'); + } + + await this.#cdpClient.send('WebAuthn.removeVirtualAuthenticator', { + authenticatorId: this.#authenticatorId, + }); + } + + async startAndCompletePasskeyOperation(operationTrigger: () => Promise) { + if (!this.#cdpClient) { + throw new Error('CDP client not initialized'); + } + + const operationCompleted = new Promise(resolve => { + this.#cdpClient?.on('WebAuthn.credentialAdded', () => resolve()); + this.#cdpClient?.on('WebAuthn.credentialAsserted', () => resolve()); + }); + + const wait = new Promise(resolve => setTimeout(resolve, operationTimeout)); + await this.#setWebAuthnUserVerified(this.#cdpClient, this.#authenticatorId, true); + await this.#setWebAuthnAutomaticPresenceSimulation(this.#cdpClient, this.#authenticatorId, true); + + await operationTrigger(); + + await Promise.race([operationCompleted, wait.then(() => Promise.reject('Passkey input timeout'))]); + await this.#setWebAuthnAutomaticPresenceSimulation(this.#cdpClient, this.#authenticatorId, false); + } + + async startAndCancelPasskeyOperation(operationTrigger: () => Promise, postOperationCheck: () => Promise) { + if (!this.#cdpClient) { + throw new Error('CDP client not initialized'); + } + + await this.#setWebAuthnUserVerified(this.#cdpClient, this.#authenticatorId, false); + await this.#setWebAuthnAutomaticPresenceSimulation(this.#cdpClient, this.#authenticatorId, true); + + await operationTrigger(); + + await postOperationCheck(); + await this.#setWebAuthnAutomaticPresenceSimulation(this.#cdpClient, this.#authenticatorId, false); + } + + #setWebAuthnAutomaticPresenceSimulation(client: CDPSession, authenticatorId: string, automatic: boolean) { + return client.send('WebAuthn.setAutomaticPresenceSimulation', { + authenticatorId: authenticatorId, + enabled: automatic, + }); + } + + #setWebAuthnUserVerified(client: CDPSession, authenticatorId: string, isUserVerified: boolean) { + return client.send('WebAuthn.setUserVerified', { + authenticatorId, + isUserVerified, + }); + } +}