Skip to content

Commit d14a875

Browse files
committed
Playwright/Cypress examples
1 parent 413db73 commit d14a875

18 files changed

+2717
-61
lines changed

.env.local.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@ WORKOS_API_KEY=
33
NEXT_PUBLIC_WORKOS_REDIRECT_URI=http://localhost:3000/callback
44
WORKOS_COOKIE_PASSWORD=
55

6+
# When using a custom auth domain
7+
# WORKOS_API_HOSTNAME=auth.yourapp.com

.env.test.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
TEST_BASE_URL=http://localhost:3000
2+
3+
TEST_PASSWORD=

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,14 @@
44
/node_modules
55
/.pnp
66
.pnp.js
7+
.yarn/install-state.gz
78

89
# testing
910
/coverage
11+
/test-results/
12+
/playwright-report/
13+
/blob-report/
14+
/playwright/.auth
1015

1116
# next.js
1217
/.next/
@@ -26,10 +31,15 @@ yarn-error.log*
2631

2732
# local env files
2833
.env*.local
34+
.env.test
2935

3036
# vercel
3137
.vercel
3238

3339
# typescript
3440
*.tsbuildinfo
3541
next-env.d.ts
42+
43+
# cypress
44+
cypress/videos
45+
cypress/screenshots

cypress.config.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { defineConfig } from "cypress";
2+
import dotenv from "dotenv";
3+
import path from "path";
4+
import { registerWorkOSTasks } from "./cypress/plugins/workos";
5+
6+
// Load environment variables from .env files
7+
dotenv.config({ path: path.resolve(__dirname, ".env.local") });
8+
dotenv.config({ path: path.resolve(__dirname, ".env.test") });
9+
10+
export default defineConfig({
11+
e2e: {
12+
baseUrl: process.env.TEST_BASE_URL,
13+
supportFile: "cypress/support/e2e.ts",
14+
specPattern: "cypress/e2e/**/*.cy.{js,jsx,ts,tsx}",
15+
viewportWidth: 1280,
16+
viewportHeight: 720,
17+
video: false,
18+
screenshotOnRunFailure: true,
19+
20+
setupNodeEvents(on, config) {
21+
// Register WorkOS authentication tasks
22+
registerWorkOSTasks(on, config);
23+
24+
// Pass through environment variables to Cypress
25+
config.env = {
26+
...config.env,
27+
WORKOS_CLIENT_ID: process.env.WORKOS_CLIENT_ID,
28+
WORKOS_API_KEY: process.env.WORKOS_API_KEY,
29+
TEST_BASE_URL: process.env.TEST_BASE_URL,
30+
TEST_EMAIL: process.env.TEST_EMAIL,
31+
TEST_PASSWORD: process.env.TEST_PASSWORD,
32+
};
33+
return config;
34+
},
35+
},
36+
});

cypress/README.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# E2E Testing with Cypress and WorkOS AuthKit
2+
3+
This directory contains Cypress tests for WorkOS AuthKit authentication using programmatic authentication.
4+
5+
## Setup
6+
7+
1. **Environment Variables** (same as Playwright tests)
8+
9+
```bash
10+
# WorkOS Configuration
11+
WORKOS_CLIENT_ID=your_client_id
12+
WORKOS_API_KEY=your_api_key
13+
WORKOS_COOKIE_PASSWORD=your_cookie_password
14+
15+
# Test Configuration
16+
TEST_BASE_URL=http://localhost:3000
17+
```
18+
19+
2. **Run Tests**
20+
21+
```bash
22+
npm run test:cypress # Headless
23+
npm run test:cypress:open # Interactive
24+
```
25+
26+
## Authentication
27+
28+
### **Custom Command**
29+
30+
```typescript
31+
// Authenticate as specific user
32+
cy.login(Cypress.env("TEST_EMAIL"), Cypress.env("TEST_PASSWORD"));
33+
```
34+
35+
### **Session Caching**
36+
37+
- First use: API authentication + session creation
38+
- Subsequent uses: Cached session (faster)
39+
- Auto-validation: Ensures session is still valid
40+
41+
## Usage
42+
43+
**Authenticated Tests:**
44+
45+
```typescript
46+
describe("Admin Features", () => {
47+
beforeEach(() => {
48+
cy.login(Cypress.env("TEST_EMAIL"), Cypress.env("TEST_PASSWORD"));
49+
});
50+
51+
it("can access admin panel", () => {
52+
cy.visit("/admin"); // Already authenticated
53+
});
54+
});
55+
```
56+
57+
**Unauthenticated Tests:**
58+
59+
```typescript
60+
describe("Public Features", () => {
61+
// No beforeEach = unauthenticated
62+
63+
it("shows login page", () => {
64+
cy.visit("/");
65+
});
66+
});
67+
```
68+
69+
## Test Endpoint
70+
71+
Uses the same endpoint as Playwright tests:
72+
73+
**Endpoint:** `POST /api/test/set-session`
74+
75+
**Request Body:**
76+
77+
```json
78+
{
79+
"user": { "email": "...", "id": "...", ... },
80+
"accessToken": "...",
81+
"refreshToken": "..."
82+
}
83+
```
84+
85+
**Response:** Creates encrypted session cookie using WorkOS AuthKit's `saveSession` method.
86+
87+
## Files
88+
89+
- `plugins/workos.ts` - WorkOS authentication plugin
90+
- `support/commands.ts` - Authentication command and user resolution
91+
- `support/e2e.ts` - Test configuration
92+
- `e2e/authenticated-flows.cy.ts` - Tests for logged-in users
93+
- `e2e/unauthenticated-flows.cy.ts` - Tests for anonymous users
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
describe("Authenticated User Flows", () => {
2+
beforeEach(() => {
3+
// Use username/password from environment variables
4+
cy.login(Cypress.env("TEST_EMAIL"), Cypress.env("TEST_PASSWORD"));
5+
});
6+
7+
it("homepage shows authenticated state", () => {
8+
cy.visit("/");
9+
10+
// Should see welcome message for authenticated user
11+
cy.contains(/welcome back/i).should("be.visible");
12+
13+
// Should see account navigation
14+
cy.get("a")
15+
.contains(/view account/i)
16+
.should("be.visible");
17+
18+
// Should see sign out button
19+
// There are multiple sign out buttons, so we need to make sure we see at least one
20+
cy.get("button")
21+
.contains(/sign out/i)
22+
.first()
23+
.should("be.visible");
24+
25+
// Should NOT see sign in button
26+
cy.get("a")
27+
.contains(/sign in/i)
28+
.should("not.exist");
29+
});
30+
31+
it("can navigate to account page", () => {
32+
cy.visit("/");
33+
34+
// Click on "View account" link
35+
cy.get("a")
36+
.contains(/view account/i)
37+
.click();
38+
39+
// Should navigate to account page
40+
cy.url().should("include", "/account");
41+
42+
// Should see account details heading
43+
cy.get("h1, h2, h3")
44+
.contains(/account details/i)
45+
.should("be.visible");
46+
47+
// Should see some user information fields
48+
cy.contains("Email").should("be.visible");
49+
50+
// Should see the email address in a readonly input
51+
cy.get("input[readonly]")
52+
.filter(`[value="${Cypress.env("TEST_EMAIL")}"]`)
53+
.should("exist");
54+
55+
// Should see user ID field
56+
cy.contains("Id", { matchCase: false }).should("be.visible");
57+
});
58+
59+
it("account page is protected - direct access works when authenticated", () => {
60+
// Direct navigation to protected route should work when authenticated
61+
cy.visit("/account");
62+
63+
// Should see account details page
64+
cy.get("h1, h2, h3")
65+
.contains(/account details/i)
66+
.should("be.visible");
67+
68+
// Should see user information
69+
cy.contains("Email").should("be.visible");
70+
});
71+
72+
it("can sign out successfully", () => {
73+
cy.visit("/");
74+
75+
// Verify we start authenticated
76+
cy.contains(/welcome back/i).should("be.visible");
77+
78+
// Click sign out button
79+
cy.get("button")
80+
.contains(/sign out/i)
81+
.first()
82+
.click();
83+
84+
// Wait for sign out to complete and page to update
85+
cy.url().should("eq", Cypress.config("baseUrl") + "/");
86+
87+
// Should see unauthenticated state
88+
cy.get("h1, h2, h3")
89+
.contains(/authkit authentication example/i)
90+
.should("be.visible");
91+
cy.get("a")
92+
.contains(/sign in with authkit/i)
93+
.should("be.visible");
94+
cy.contains(/sign in to view your account details/i).should("be.visible");
95+
96+
// Should NOT see authenticated elements
97+
cy.contains(/welcome back/i).should("not.exist");
98+
cy.contains(/sign out/i).should("not.exist");
99+
});
100+
101+
it("navigation between pages works correctly", () => {
102+
// Start at home
103+
cy.visit("/");
104+
cy.contains(/welcome back/i).should("be.visible");
105+
106+
// Go to account
107+
cy.get("a")
108+
.contains(/view account/i)
109+
.click();
110+
cy.url().should("include", "/account");
111+
cy.get("h1, h2, h3")
112+
.contains(/account details/i)
113+
.should("be.visible");
114+
115+
// Go back to home via browser navigation
116+
cy.go("back");
117+
cy.url().should("not.include", "/account");
118+
cy.contains(/welcome back/i).should("be.visible");
119+
120+
// Go forward again
121+
cy.go("forward");
122+
cy.url().should("include", "/account");
123+
cy.get("h1, h2, h3")
124+
.contains(/account details/i)
125+
.should("be.visible");
126+
});
127+
});
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
describe("Unauthenticated User Flows", () => {
2+
it("homepage shows unauthenticated state", () => {
3+
cy.visit("/");
4+
5+
// Should see the unauthenticated homepage
6+
cy.get("h1, h2, h3")
7+
.contains(/authkit authentication example/i)
8+
.should("be.visible");
9+
cy.contains(/sign in to view your account details/i).should("be.visible");
10+
11+
// Should see sign in button
12+
cy.get("a")
13+
.contains(/sign in with authkit/i)
14+
.should("be.visible");
15+
16+
// Should NOT see authenticated elements
17+
cy.contains(/welcome back/i).should("not.exist");
18+
cy.get("a")
19+
.contains(/view account/i)
20+
.should("not.exist");
21+
cy.contains("button", /sign out/i).should("not.exist");
22+
});
23+
24+
it("account page redirects unauthenticated users to login", () => {
25+
// Try to access protected route directly
26+
cy.visit("/account");
27+
28+
// Should be redirected to WorkOS login (the URL will change away from the app)
29+
cy.url({ timeout: 10000 }).should("not.contain", "/account");
30+
31+
cy.get("input").should("be.visible");
32+
});
33+
34+
it("sign in button navigates to WorkOS login", () => {
35+
cy.visit("/");
36+
37+
// Click the sign in button
38+
cy.get("a")
39+
.contains(/sign in with authkit/i)
40+
.click();
41+
42+
// URL should not be the original app URL
43+
const baseURL = Cypress.env("TEST_BASE_URL");
44+
cy.url().should("not.eq", baseURL + "/");
45+
cy.url().should("not.contain", new URL(baseURL).host);
46+
});
47+
});

cypress/plugins/workos.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import type { PluginEvents, PluginConfigOptions } from "cypress";
2+
3+
interface AuthenticateParams {
4+
email: string;
5+
password: string;
6+
workosApiKey: string;
7+
workosClientId: string;
8+
}
9+
10+
/**
11+
* WorkOS authentication plugin for Cypress
12+
* Provides tasks for programmatic authentication using WorkOS SDK
13+
*/
14+
export function registerWorkOSTasks(
15+
on: PluginEvents,
16+
config: PluginConfigOptions
17+
) {
18+
on("task", {
19+
authenticateWithWorkOS({
20+
email,
21+
password,
22+
workosApiKey,
23+
workosClientId,
24+
}: AuthenticateParams) {
25+
// Import WorkOS SDK in Node.js context (can't be imported in browser context)
26+
const { WorkOS } = require("@workos-inc/node");
27+
28+
const workos = new WorkOS(workosApiKey, {
29+
apiHostname: process.env.WORKOS_API_HOSTNAME,
30+
});
31+
32+
return workos.userManagement.authenticateWithPassword({
33+
clientId: workosClientId,
34+
email,
35+
password,
36+
});
37+
},
38+
});
39+
}

0 commit comments

Comments
 (0)