diff --git a/lib/javascript/fullstack_demo/.gitignore b/lib/javascript/fullstack_demo/.gitignore index 252d8cd2d..6c43033fd 100644 --- a/lib/javascript/fullstack_demo/.gitignore +++ b/lib/javascript/fullstack_demo/.gitignore @@ -48,4 +48,5 @@ db.json # Cypress /cypress/snapshots/actual -/cypress/snapshots/diff \ No newline at end of file +/cypress/snapshots/diff +cypress.env.json \ No newline at end of file diff --git a/lib/javascript/fullstack_demo/README.md b/lib/javascript/fullstack_demo/README.md index be8beda1b..7a7e63995 100644 --- a/lib/javascript/fullstack_demo/README.md +++ b/lib/javascript/fullstack_demo/README.md @@ -29,6 +29,56 @@ By default, the app is configured to store data on the web server in a JSON file To deploy this app, sign up for an account on Vercel and create a project pointing to your fork of this repo. You'll need to configure the environment variables you setup in your local `.env` file using the Vercel UI. +## Testing + +The application includes both unit tests (Vitest) and end-to-end tests (Cypress). + +### End-to-End Testing with Cypress and Clerk + +The app is configured with Cypress for end-to-end testing, including authentication testing with Clerk. + +1. Set up testing environment: + - Create a `cypress.env.json` file with your Clerk testing keys: + ```json + { + "CLERK_PUBLISHABLE_KEY": "your_clerk_publishable_key_for_testing", + "CLERK_SECRET_KEY": "your_clerk_secret_key_for_testing" + } + ``` + +2. Run Cypress tests: + ```bash + # Open Cypress test runner in interactive mode + npm run cy:open:e2e + + # Run tests headlessly + npm run cy:run:e2e + ``` + +3. Authentication approach: + - We use Clerk's Testing Tokens approach to bypass the UI authentication flow entirely. + - This avoids cross-origin issues that would require `cy.origin()`. + - The `setupClerkTestingToken()` function from `@clerk/testing/cypress` handles authentication behind the scenes. + - This allows tests to directly access protected routes without redirects to Clerk's authentication domain. + +4. Test example: + ```typescript + import { setupClerkTestingToken } from '@clerk/testing/cypress'; + + describe('Authentication Test', () => { + beforeEach(() => { + // Set up authentication token - no UI interaction needed + setupClerkTestingToken(); + + // Visit protected route directly + cy.visit('/'); + + // Verify authentication successful + cy.get('header').find('button').should('exist'); + }); + }); + ``` + ## Tools Here are a list of tools used to compile this demo. You can read the official documentation to learn how various aspects of the demo function. @@ -39,3 +89,5 @@ Here are a list of tools used to compile this demo. You can read the official do - React (https://react.dev) - LowDB (https://github.com/typicode/lowdb) - Upstash Redis (https://upstash.com/docs/redis/overall/getstarted) +- Cypress (https://cypress.io) +- @clerk/testing (https://clerk.dev/docs/testing/integration-testing) diff --git a/lib/javascript/fullstack_demo/cypress.config.ts b/lib/javascript/fullstack_demo/cypress.config.ts index c81e39296..39ef516d8 100644 --- a/lib/javascript/fullstack_demo/cypress.config.ts +++ b/lib/javascript/fullstack_demo/cypress.config.ts @@ -1,3 +1,4 @@ +import { clerkSetup } from '@clerk/testing/cypress'; import { defineConfig } from 'cypress'; import { configureVisualRegression } from 'cypress-visual-regression'; @@ -15,4 +16,15 @@ export default defineConfig({ configureVisualRegression(on); }, }, + e2e: { + baseUrl: 'http://localhost:3000', + setupNodeEvents(on, config) { + configureVisualRegression(on); + return clerkSetup({ config }); + }, + env: { + visualRegressionType: 'regression', + }, + screenshotsFolder: './cypress/snapshots/actual', + }, }); diff --git a/lib/javascript/fullstack_demo/cypress/e2e/auth.cy.ts b/lib/javascript/fullstack_demo/cypress/e2e/auth.cy.ts new file mode 100644 index 000000000..24be7c260 --- /dev/null +++ b/lib/javascript/fullstack_demo/cypress/e2e/auth.cy.ts @@ -0,0 +1,22 @@ +import { setupClerkTestingToken } from '@clerk/testing/cypress'; + +describe('Authentication Flow', () => { + beforeEach(() => { + setupClerkTestingToken(); + + cy.visit('/'); // Visit the homepage + + // Use our custom command to login with Clerk testing + cy.clerkSignIn({ + strategy: 'email_code', + identifier: 'testbot123+clerk_test@gmail.com', // Use a test email that Clerk recognizes + }); + }); + + it('should successfully login to the homepage', () => { + // Verify the Todo App components are visible (adjust selector to match your app) + cy.origin('http://localhost:3000', () => { + cy.contains('Todo App', { timeout: 5000 }).should('be.visible'); + }); + }); +}); diff --git a/lib/javascript/fullstack_demo/cypress/e2e/todo.cy.ts b/lib/javascript/fullstack_demo/cypress/e2e/todo.cy.ts new file mode 100644 index 000000000..c7c56786b --- /dev/null +++ b/lib/javascript/fullstack_demo/cypress/e2e/todo.cy.ts @@ -0,0 +1,45 @@ +import { setupClerkTestingToken } from '@clerk/testing/cypress'; + +describe('Todo App Functionality', () => { + beforeEach(() => { + setupClerkTestingToken(); + + cy.visit('/'); // Visit the homepage + + // Use our custom command to login with Clerk testing + cy.clerkSignIn({ + strategy: 'email_code', + identifier: 'testbot123+clerk_test@gmail.com', // Use a test email that Clerk recognizes + }); + }); + + it('should allow adding a new todo item', () => { + cy.origin('http://localhost:3000', () => { + // Create a unique todo item + const todoText = `Test Todo ${Date.now()}`; + + // Find the input field and type the text + cy.get('input[placeholder*="Add"]').type(todoText); + + // Submit the form (either by clicking a button or pressing Enter) + cy.get('form').submit(); + // Or use this if the form doesn't have a submit button: cy.get('input').type('{enter}'); + + // Verify the new todo appeared in the list + cy.contains(todoText).should('be.visible'); + }); + }); + + it('should allow marking a todo item as completed', () => { + cy.origin('http://localhost:3000', () => { + // First, ensure there is at least one todo item to mark as completed + cy.get('.todo').last().as('lastTodo'); + + // Click the checkbox or toggle button to mark it as completed + cy.get('@lastTodo').find('input[type="checkbox"]').check(); + + // Verify the todo item is marked as completed (adjust selector as needed) + cy.get('@lastTodo').find('input[type="checkbox"]').should('be.checked'); + }); + }); +}); diff --git a/lib/javascript/fullstack_demo/cypress/support/commands.ts b/lib/javascript/fullstack_demo/cypress/support/commands.ts index 0324d2927..618071408 100644 --- a/lib/javascript/fullstack_demo/cypress/support/commands.ts +++ b/lib/javascript/fullstack_demo/cypress/support/commands.ts @@ -1,2 +1,4 @@ import { addCompareSnapshotCommand } from 'cypress-visual-regression/dist/command'; + +// Add visual regression testing command addCompareSnapshotCommand(); diff --git a/lib/javascript/fullstack_demo/cypress/support/e2e.ts b/lib/javascript/fullstack_demo/cypress/support/e2e.ts new file mode 100644 index 000000000..342a443b5 --- /dev/null +++ b/lib/javascript/fullstack_demo/cypress/support/e2e.ts @@ -0,0 +1,22 @@ +/// +import { addClerkCommands } from '@clerk/testing/cypress'; +import './commands'; + +// Cypress-specific imports +import 'cypress-visual-regression'; +import { addCompareSnapshotCommand } from 'cypress-visual-regression/dist/command'; + +// Add visual regression testing command +addCompareSnapshotCommand(); + +// Add Clerk commands for testing +addClerkCommands({ Cypress, cy }); + +// This ensures TypeScript understands Cypress commands +declare global { + namespace Cypress { + interface Chainable { + // Add any custom command types here + } + } +} diff --git a/lib/javascript/fullstack_demo/package-lock.json b/lib/javascript/fullstack_demo/package-lock.json index 7725becd4..08d60213a 100644 --- a/lib/javascript/fullstack_demo/package-lock.json +++ b/lib/javascript/fullstack_demo/package-lock.json @@ -19,6 +19,7 @@ "winston": "^3.17.0" }, "devDependencies": { + "@clerk/testing": "^1.7.5", "@testing-library/dom": "^10.4.0", "@testing-library/react": "^16.1.0", "@types/node": "^22", @@ -412,21 +413,65 @@ } }, "node_modules/@clerk/backend": { - "version": "1.21.2", - "resolved": "https://registry.npmjs.org/@clerk/backend/-/backend-1.21.2.tgz", - "integrity": "sha512-N8BTxCCRPYXUTcWIKEyOX11IDftgycM36iwMr9wr0y9RUpHwr5rXBehtC3ITKDyqcelzll8rnlF9NsXQmH7vPg==", + "version": "1.34.0", + "resolved": "https://registry.npmjs.org/@clerk/backend/-/backend-1.34.0.tgz", + "integrity": "sha512-9rZ8hQJVpX5KX2bEpiuVXfpjhojQCiqCWADJDdCI0PCeKxn58Ep0JPYiIcczg4VKUc3a7jve9vXylykG2XajLQ==", "license": "MIT", "dependencies": { - "@clerk/shared": "^2.20.2", - "@clerk/types": "^4.39.4", - "cookie": "0.7.0", - "snakecase-keys": "5.4.4", - "tslib": "2.4.1" + "@clerk/shared": "^3.9.5", + "@clerk/types": "^4.59.3", + "cookie": "1.0.2", + "snakecase-keys": "8.0.1", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=18.17.0" + }, + "peerDependencies": { + "svix": "^1.62.0" + }, + "peerDependenciesMeta": { + "svix": { + "optional": true + } + } + }, + "node_modules/@clerk/backend/node_modules/@clerk/shared": { + "version": "3.9.5", + "resolved": "https://registry.npmjs.org/@clerk/shared/-/shared-3.9.5.tgz", + "integrity": "sha512-KeIug5qV4LnzZD+16SLkJvdONPs2HQ7I1A7jbHYOGB37vQrQrus64Wu5XeNzbWFTN1Z5fAPSGuja8MfT2cBT4A==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@clerk/types": "^4.59.3", + "dequal": "2.0.3", + "glob-to-regexp": "0.4.1", + "js-cookie": "3.0.5", + "std-env": "^3.9.0", + "swr": "^2.3.3" }, "engines": { "node": ">=18.17.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-0", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } } }, + "node_modules/@clerk/backend/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/@clerk/clerk-react": { "version": "5.21.0", "resolved": "https://registry.npmjs.org/@clerk/clerk-react/-/clerk-react-5.21.0.tgz", @@ -498,23 +543,76 @@ } } }, - "node_modules/@clerk/types": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@clerk/types/-/types-4.40.0.tgz", - "integrity": "sha512-9QdllXYujsjYLbvPg9Kq1rWOemX5FB0r6Ijy8HOxwjKN+TPlxUnGcs+t7IwU+M5gdmZ2KV6aA6d1a2q2FlSoiA==", + "node_modules/@clerk/testing": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@clerk/testing/-/testing-1.7.5.tgz", + "integrity": "sha512-orBMQAs4agjCUWKLgrHnGDZlRhlgwSoA6HZv5b+8RIjCOmV6/+k6Ny8PcqBoXRfO7IilcoN/uIAdvJJFwUkrKg==", + "dev": true, "license": "MIT", "dependencies": { - "csstype": "3.1.1" + "@clerk/backend": "^1.34.0", + "@clerk/shared": "^3.9.5", + "@clerk/types": "^4.59.3", + "dotenv": "16.4.7" }, "engines": { "node": ">=18.17.0" + }, + "peerDependencies": { + "@playwright/test": "^1", + "cypress": "^13" + }, + "peerDependenciesMeta": { + "@playwright/test": { + "optional": true + }, + "cypress": { + "optional": true + } } }, - "node_modules/@clerk/types/node_modules/csstype": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", - "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==", - "license": "MIT" + "node_modules/@clerk/testing/node_modules/@clerk/shared": { + "version": "3.9.5", + "resolved": "https://registry.npmjs.org/@clerk/shared/-/shared-3.9.5.tgz", + "integrity": "sha512-KeIug5qV4LnzZD+16SLkJvdONPs2HQ7I1A7jbHYOGB37vQrQrus64Wu5XeNzbWFTN1Z5fAPSGuja8MfT2cBT4A==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@clerk/types": "^4.59.3", + "dequal": "2.0.3", + "glob-to-regexp": "0.4.1", + "js-cookie": "3.0.5", + "std-env": "^3.9.0", + "swr": "^2.3.3" + }, + "engines": { + "node": ">=18.17.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-0", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/@clerk/types": { + "version": "4.59.3", + "resolved": "https://registry.npmjs.org/@clerk/types/-/types-4.59.3.tgz", + "integrity": "sha512-xwOO/hfABzbFr3f1RaVXHsDDQ0+jYpm84GiaUDxo+mLsYUgD9f2GmGjKkgWybXzvsBsgZlycSwRXkeDD6utFqg==", + "license": "MIT", + "dependencies": { + "csstype": "3.1.3" + }, + "engines": { + "node": ">=18.17.0" + } }, "node_modules/@colors/colors": { "version": "1.5.0", @@ -3676,12 +3774,12 @@ "license": "MIT" }, "node_modules/cookie": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.0.tgz", - "integrity": "sha512-qCf+V4dtlNhSRXGAZatc1TasyFO6GjohcOul807YOb5ik3+kQSnb4d7iajeCL8QHaJ4uZEjCgiCJerKXwdRVlQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=18" } }, "node_modules/core-util-is": { @@ -3749,7 +3847,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, "license": "MIT" }, "node_modules/cypress": { @@ -4089,6 +4186,19 @@ "tslib": "^2.0.3" } }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -8640,17 +8750,17 @@ } }, "node_modules/snakecase-keys": { - "version": "5.4.4", - "resolved": "https://registry.npmjs.org/snakecase-keys/-/snakecase-keys-5.4.4.tgz", - "integrity": "sha512-YTywJG93yxwHLgrYLZjlC75moVEX04LZM4FHfihjHe1FCXm+QaLOFfSf535aXOAd0ArVQMWUAe8ZPm4VtWyXaA==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/snakecase-keys/-/snakecase-keys-8.0.1.tgz", + "integrity": "sha512-Sj51kE1zC7zh6TDlNNz0/Jn1n5HiHdoQErxO8jLtnyrkJW/M5PrI7x05uDgY3BO7OUQYKCvmeMurW6BPUdwEOw==", "license": "MIT", "dependencies": { "map-obj": "^4.1.0", "snake-case": "^3.0.4", - "type-fest": "^2.5.2" + "type-fest": "^4.15.0" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/source-map-js": { @@ -8705,9 +8815,9 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz", - "integrity": "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==", + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", "license": "MIT" }, "node_modules/steno": { @@ -9047,9 +9157,9 @@ } }, "node_modules/swr": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.0.tgz", - "integrity": "sha512-NyZ76wA4yElZWBHzSgEJc28a0u6QZvhb6w0azeL2k7+Q1gAzVK+IqQYXhVOC/mzi+HZIozrZvBVeSeOZNR2bqA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.3.tgz", + "integrity": "sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A==", "license": "MIT", "dependencies": { "dequal": "^2.0.3", @@ -9970,12 +10080,12 @@ } }, "node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", "license": "(MIT OR CC0-1.0)", "engines": { - "node": ">=12.20" + "node": ">=16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" diff --git a/lib/javascript/fullstack_demo/package.json b/lib/javascript/fullstack_demo/package.json index 1908007d0..1e5f14d21 100644 --- a/lib/javascript/fullstack_demo/package.json +++ b/lib/javascript/fullstack_demo/package.json @@ -1,6 +1,7 @@ { "name": "fullstack_demo", "version": "0.1.0", + "type": "module", "private": true, "scripts": { "dev": "next dev", @@ -12,6 +13,8 @@ "test:coverage": "vitest run --coverage", "postinstall": "prisma generate", "cy:open": "cypress open", + "cy:open:e2e": "cypress open --e2e", + "cy:run:e2e": "cypress run --e2e", "db:seed": "tsx prisma/seed.ts" }, "dependencies": { @@ -25,6 +28,7 @@ "winston": "^3.17.0" }, "devDependencies": { + "@clerk/testing": "^1.7.5", "@testing-library/dom": "^10.4.0", "@testing-library/react": "^16.1.0", "@types/node": "^22",