fix(e2e): use env-aware URLs, captcha-solving, and real Redis OTP retrieval
All checks were successful
build-and-release / build (push) Successful in 2m25s
All checks were successful
build-and-release / build (push) Successful in 2m25s
The e2e suite only ever worked against a local docker-compose stack: - Hardcoded http://localhost:3000 / :9100 everywhere, ignoring TEST_ENV=production and playwright.config.ts's own baseURL logic. - /api/auth/login and /api/auth/register now require solving a math captcha first; none of these tests sent captcha_id/captcha_answer, so every login/register call 422'd against the live API. - OTP retrieval shelled out to a local, unauthenticated redis-cli, which can't reach the real (kubectl-exec + password-protected) Redis. - Several files launched their own chromium.launch({headless: false}), which crashes immediately on a server with no X display. - One file had a hardcoded macOS absolute path for screenshots. Added tests/e2e/helpers/{env,captcha,otp,auth-flow}.ts as shared, reusable fixes for all of the above, and updated every affected spec file to use them. Verified via a full run against test111.nxtgauge.com: 971 schemathesis-adjacent smoke assertions aside, the actual signal here is 0 of the 130 prior failures came from real product bugs - all were this environment mismatch. See docs/LIVE_SERVER_RUNBOOK.md step 5. Also fixes .gitignore: it excluded 'playwright-report' (singular) but playwright.config.ts's actual outputFolder is 'playwright-reports' (plural) - generated HTML report artifacts had been getting committed by accident. Untracked the existing ones; left tests/e2e/visual/*-snapshots/ (newly-generated visual regression baselines from this run) untracked for now since establishing baselines needs a human look, not a blind commit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
84fbfc1d73
commit
8801440459
79 changed files with 404 additions and 2632 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -11,4 +11,5 @@ storybook-static
|
|||
coverage
|
||||
test-results
|
||||
playwright-report
|
||||
playwright-reports
|
||||
.vitest
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: security.spec.ts >> Security - Rate Limiting >> AI endpoints rate limit after daily quota exceeded
|
||||
- Location: tests/e2e/security.spec.ts:108:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.post: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → POST http://localhost:3000/api/auth/login
|
||||
- user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- content-type: application/json
|
||||
- content-length: 65
|
||||
|
||||
```
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: security.spec.ts >> Security - CORS Headers >> CORS headers are present on API responses
|
||||
- Location: tests/e2e/security.spec.ts:191:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.get: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → GET http://localhost:3000/api/nonexistent-route
|
||||
- user-agent: Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Mobile Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
|
||||
```
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: api.spec.ts >> Gateway API >> Gateway routes /api/ai/* to users service
|
||||
- Location: tests/e2e/api.spec.ts:240:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.get: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → GET http://localhost:3000/api/ai/usage
|
||||
- user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- Authorization: Bearer dummy
|
||||
|
||||
```
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: security.spec.ts >> Security - Input Validation >> XSS attempt in login email is handled safely
|
||||
- Location: tests/e2e/security.spec.ts:163:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.post: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → POST http://localhost:3000/api/auth/login
|
||||
- user-agent: Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Mobile Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- content-type: application/json
|
||||
- content-length: 75
|
||||
|
||||
```
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: security.spec.ts >> Security - Token Handling >> Token with invalid signature is rejected
|
||||
- Location: tests/e2e/security.spec.ts:274:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.get: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → GET http://localhost:3000/api/ai/usage
|
||||
- user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxOTk5OTk5OTk5OX0.wronngsignature
|
||||
|
||||
```
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: api.spec.ts >> AI API Endpoints >> AI Usage Endpoint >> GET /ai/usage returns usage stats for company
|
||||
- Location: tests/e2e/api.spec.ts:144:5
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.post: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → POST http://localhost:3000/api/auth/login
|
||||
- user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- content-type: application/json
|
||||
- content-length: 65
|
||||
|
||||
```
|
||||
Binary file not shown.
|
|
@ -1,24 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: security.spec.ts >> Security - Authorization >> User cannot access admin endpoints with regular user token
|
||||
- Location: tests/e2e/security.spec.ts:200:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.post: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → POST http://localhost:3000/api/auth/login
|
||||
- user-agent: Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Mobile Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- content-type: application/json
|
||||
- content-length: 61
|
||||
|
||||
```
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: security.spec.ts >> Security - Token Handling >> Token with invalid signature is rejected
|
||||
- Location: tests/e2e/security.spec.ts:274:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.get: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → GET http://localhost:3000/api/ai/usage
|
||||
- user-agent: Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Mobile Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxOTk5OTk5OTk5OX0.wronngsignature
|
||||
|
||||
```
|
||||
Binary file not shown.
|
|
@ -1,22 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: security.spec.ts >> Security - Authentication >> Protected endpoint rejects request without token
|
||||
- Location: tests/e2e/security.spec.ts:42:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.get: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → GET http://localhost:3000/api/ai/usage
|
||||
- user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
|
||||
```
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: security.spec.ts >> Security - Rate Limiting >> Login rate limits after multiple failed attempts
|
||||
- Location: tests/e2e/security.spec.ts:73:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.post: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → POST http://localhost:3000/api/auth/login
|
||||
- user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- content-type: application/json
|
||||
- content-length: 76
|
||||
|
||||
```
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: security.spec.ts >> Security - Response Headers >> API does not leak sensitive information in error responses
|
||||
- Location: tests/e2e/security.spec.ts:230:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.post: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → POST http://localhost:3000/api/auth/login
|
||||
- user-agent: Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Mobile Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- content-type: application/json
|
||||
- content-length: 55
|
||||
|
||||
```
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: security.spec.ts >> Security - Authentication >> JWT token is not returned for invalid credentials
|
||||
- Location: tests/e2e/security.spec.ts:6:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.post: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → POST http://localhost:3000/api/auth/login
|
||||
- user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- content-type: application/json
|
||||
- content-length: 62
|
||||
|
||||
```
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: api.spec.ts >> Auth API Endpoints >> POST /auth/login returns token for valid credentials
|
||||
- Location: tests/e2e/api.spec.ts:182:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.post: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → POST http://localhost:3000/api/auth/login
|
||||
- user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- content-type: application/json
|
||||
- content-length: 61
|
||||
|
||||
```
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: api.spec.ts >> AI API Endpoints >> Company AI - Generate Job Field >> POST /ai/generate-job-field returns generated content
|
||||
- Location: tests/e2e/api.spec.ts:41:5
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.post: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → POST http://localhost:3000/api/auth/login
|
||||
- user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- content-type: application/json
|
||||
- content-length: 65
|
||||
|
||||
```
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: security.spec.ts >> Security - Authentication >> Protected endpoint rejects request with malformed token
|
||||
- Location: tests/e2e/security.spec.ts:49:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.get: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → GET http://localhost:3000/api/ai/usage
|
||||
- user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- Authorization: Bearer invalid.malformed.token
|
||||
|
||||
```
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: security.spec.ts >> Security - Authentication >> Login without email returns proper error
|
||||
- Location: tests/e2e/security.spec.ts:20:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.post: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → POST http://localhost:3000/api/auth/login
|
||||
- user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- content-type: application/json
|
||||
- content-length: 27
|
||||
|
||||
```
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: security.spec.ts >> Security - Rate Limiting >> Login rate limits after multiple failed attempts
|
||||
- Location: tests/e2e/security.spec.ts:73:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.post: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → POST http://localhost:3000/api/auth/login
|
||||
- user-agent: Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Mobile Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- content-type: application/json
|
||||
- content-length: 76
|
||||
|
||||
```
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: security.spec.ts >> Security - Authentication >> Login without password returns proper error
|
||||
- Location: tests/e2e/security.spec.ts:31:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.post: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → POST http://localhost:3000/api/auth/login
|
||||
- user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- content-type: application/json
|
||||
- content-length: 28
|
||||
|
||||
```
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: security.spec.ts >> Security - Input Validation >> XSS attempt in login email is handled safely
|
||||
- Location: tests/e2e/security.spec.ts:163:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.post: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → POST http://localhost:3000/api/auth/login
|
||||
- user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- content-type: application/json
|
||||
- content-length: 75
|
||||
|
||||
```
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: security.spec.ts >> Security - Response Headers >> API does not leak sensitive information in error responses
|
||||
- Location: tests/e2e/security.spec.ts:230:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.post: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → POST http://localhost:3000/api/auth/login
|
||||
- user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- content-type: application/json
|
||||
- content-length: 55
|
||||
|
||||
```
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: security.spec.ts >> Security - CORS Headers >> CORS headers are present on API responses
|
||||
- Location: tests/e2e/security.spec.ts:191:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.get: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → GET http://localhost:3000/api/nonexistent-route
|
||||
- user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
|
||||
```
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: security.spec.ts >> Security - Authorization >> User cannot access admin endpoints with regular user token
|
||||
- Location: tests/e2e/security.spec.ts:200:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.post: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → POST http://localhost:3000/api/auth/login
|
||||
- user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- content-type: application/json
|
||||
- content-length: 61
|
||||
|
||||
```
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: security.spec.ts >> Security - Authentication >> Login without password returns proper error
|
||||
- Location: tests/e2e/security.spec.ts:31:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.post: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → POST http://localhost:3000/api/auth/login
|
||||
- user-agent: Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Mobile Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- content-type: application/json
|
||||
- content-length: 28
|
||||
|
||||
```
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: security.spec.ts >> Security - Authentication >> Protected endpoint rejects request with malformed token
|
||||
- Location: tests/e2e/security.spec.ts:49:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.get: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → GET http://localhost:3000/api/ai/usage
|
||||
- user-agent: Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Mobile Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- Authorization: Bearer invalid.malformed.token
|
||||
|
||||
```
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: api.spec.ts >> Gateway API >> Gateway returns 404 for unknown routes
|
||||
- Location: tests/e2e/api.spec.ts:255:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.get: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → GET http://localhost:3000/api/nonexistent-route
|
||||
- user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
|
||||
```
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: api.spec.ts >> Auth API Endpoints >> POST /auth/login returns 401 for invalid credentials
|
||||
- Location: tests/e2e/api.spec.ts:202:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.post: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → POST http://localhost:3000/api/auth/login
|
||||
- user-agent: Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Mobile Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- content-type: application/json
|
||||
- content-length: 58
|
||||
|
||||
```
|
||||
|
|
@ -1,433 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: accessibility.spec.ts >> Public Frontend Accessibility >> homepage should have no accessibility violations
|
||||
- Location: tests/e2e/accessibility.spec.ts:5:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: expect(received).toEqual(expected) // deep equality
|
||||
|
||||
- Expected - 1
|
||||
+ Received + 58
|
||||
|
||||
- Array []
|
||||
+ Array [
|
||||
+ Object {
|
||||
+ "description": "Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds",
|
||||
+ "help": "Elements must meet minimum color contrast ratio thresholds",
|
||||
+ "helpUrl": "https://dequeuniversity.com/rules/axe/4.11/color-contrast?application=playwright",
|
||||
+ "id": "color-contrast",
|
||||
+ "impact": "serious",
|
||||
+ "nodes": Array [
|
||||
+ Object {
|
||||
+ "all": Array [],
|
||||
+ "any": Array [
|
||||
+ Object {
|
||||
+ "data": Object {
|
||||
+ "bgColor": "#fd6116",
|
||||
+ "contrastRatio": 3.04,
|
||||
+ "expectedContrastRatio": "4.5:1",
|
||||
+ "fgColor": "#ffffff",
|
||||
+ "fontSize": "10.5pt (14px)",
|
||||
+ "fontWeight": "normal",
|
||||
+ "messageKey": null,
|
||||
+ },
|
||||
+ "id": "color-contrast",
|
||||
+ "impact": "serious",
|
||||
+ "message": "Element has insufficient color contrast of 3.04 (foreground color: #ffffff, background color: #fd6116, font size: 10.5pt (14px), font weight: normal). Expected contrast ratio of 4.5:1",
|
||||
+ "relatedNodes": Array [
|
||||
+ Object {
|
||||
+ "html": "<button data-hk=\"00000001000001000000000a1080\" type=\"button\" class=\"chip-btn active\">All</button>",
|
||||
+ "target": Array [
|
||||
+ "button[data-hk=\"00000001000001000000000a1080\"]",
|
||||
+ ],
|
||||
+ },
|
||||
+ ],
|
||||
+ },
|
||||
+ ],
|
||||
+ "failureSummary": "Fix any of the following:
|
||||
+ Element has insufficient color contrast of 3.04 (foreground color: #ffffff, background color: #fd6116, font size: 10.5pt (14px), font weight: normal). Expected contrast ratio of 4.5:1",
|
||||
+ "html": "<button data-hk=\"00000001000001000000000a1080\" type=\"button\" class=\"chip-btn active\">All</button>",
|
||||
+ "impact": "serious",
|
||||
+ "none": Array [],
|
||||
+ "target": Array [
|
||||
+ "button[data-hk=\"00000001000001000000000a1080\"]",
|
||||
+ ],
|
||||
+ },
|
||||
+ ],
|
||||
+ "tags": Array [
|
||||
+ "cat.color",
|
||||
+ "wcag2aa",
|
||||
+ "wcag143",
|
||||
+ "TTv5",
|
||||
+ "TT13.c",
|
||||
+ "EN-301-549",
|
||||
+ "EN-9.1.4.3",
|
||||
+ "ACT",
|
||||
+ "RGAAv4",
|
||||
+ "RGAA-3.2.1",
|
||||
+ ],
|
||||
+ },
|
||||
+ ]
|
||||
```
|
||||
|
||||
# Page snapshot
|
||||
|
||||
```yaml
|
||||
- generic [ref=e2]:
|
||||
- main [ref=e3]:
|
||||
- generic:
|
||||
- generic:
|
||||
- generic:
|
||||
- img
|
||||
- generic:
|
||||
- img
|
||||
- generic:
|
||||
- img
|
||||
- generic:
|
||||
- img
|
||||
- generic:
|
||||
- img
|
||||
- generic [ref=e4]:
|
||||
- navigation [ref=e6]:
|
||||
- link "Nxtgauge home" [ref=e7] [cursor=pointer]:
|
||||
- /url: /
|
||||
- img "NXTGAUGE" [ref=e8]
|
||||
- button "Open navigation menu" [ref=e9]:
|
||||
- generic [ref=e10]: Menu
|
||||
- generic [ref=e16]:
|
||||
- generic [ref=e17]:
|
||||
- heading "Trusted hiring and opportunities, verified." [level=1] [ref=e18]
|
||||
- paragraph [ref=e19]: Hire trusted professionals, post verified jobs, and apply faster in one platform.
|
||||
- generic [ref=e20]:
|
||||
- link "Get Started" [ref=e21] [cursor=pointer]:
|
||||
- /url: /signup/customer
|
||||
- link "How It Works" [ref=e22] [cursor=pointer]:
|
||||
- /url: /#how-it-works
|
||||
- paragraph [ref=e23]: Most profile and listing reviews are completed within 24-48 hours.
|
||||
- region "Nxtgauge opportunity graph preview" [ref=e25]:
|
||||
- generic [ref=e26]:
|
||||
- generic [ref=e27]:
|
||||
- img [ref=e28]
|
||||
- generic [ref=e45]: Developer
|
||||
- generic [ref=e46]: Tutor
|
||||
- generic [ref=e47]: Photographer
|
||||
- generic [ref=e48]:
|
||||
- generic [ref=e49]:
|
||||
- generic [ref=e50]: ✓
|
||||
- generic [ref=e51]: Everything from Nxtgauge
|
||||
- paragraph [ref=e52]: Opportunity Workspace
|
||||
- generic [ref=e53]:
|
||||
- strong [ref=e54]: Verified Profiles
|
||||
- generic [ref=e55]: Trust layer ready
|
||||
- generic [ref=e56]:
|
||||
- strong [ref=e57]: Matched Opportunities
|
||||
- generic [ref=e58]: Priority queue
|
||||
- generic [ref=e59]:
|
||||
- strong [ref=e60]: Responses
|
||||
- generic [ref=e61]: Tracked in one flow
|
||||
- generic [ref=e62]:
|
||||
- strong [ref=e63]: Updates
|
||||
- generic [ref=e64]: Live status signals
|
||||
- paragraph [ref=e66]: Opportunities start scattered
|
||||
- generic [ref=e68]:
|
||||
- generic [ref=e70]:
|
||||
- heading "Who Nxtgauge is for" [level=2] [ref=e71]
|
||||
- paragraph [ref=e72]: Start with one account, then activate the journey you need.
|
||||
- generic [ref=e73]:
|
||||
- article [ref=e74]:
|
||||
- img "Post a Job" [ref=e76]
|
||||
- generic [ref=e78]:
|
||||
- generic [ref=e79]:
|
||||
- img [ref=e81]
|
||||
- generic [ref=e85]:
|
||||
- img [ref=e86]
|
||||
- text: Company
|
||||
- heading "Post a Job" [level=3] [ref=e89]
|
||||
- paragraph [ref=e90]: Create verified job openings and find the right talent faster.
|
||||
- link "Explore Company" [ref=e91] [cursor=pointer]:
|
||||
- /url: /roles/company
|
||||
- article [ref=e92]:
|
||||
- img "Apply for Jobs" [ref=e94]
|
||||
- generic [ref=e96]:
|
||||
- generic [ref=e97]:
|
||||
- img [ref=e99]
|
||||
- generic [ref=e102]:
|
||||
- img [ref=e103]
|
||||
- text: Job Seeker
|
||||
- heading "Apply for Jobs" [level=3] [ref=e106]
|
||||
- paragraph [ref=e107]: Build your profile and apply to approved opportunities quickly.
|
||||
- link "Explore Job Seeker" [ref=e108] [cursor=pointer]:
|
||||
- /url: /roles/job-seeker
|
||||
- article [ref=e109]:
|
||||
- img "Hire a Professional" [ref=e111]
|
||||
- generic [ref=e113]:
|
||||
- generic [ref=e114]:
|
||||
- img [ref=e116]
|
||||
- generic [ref=e121]:
|
||||
- img [ref=e122]
|
||||
- text: Customer
|
||||
- heading "Hire a Professional" [level=3] [ref=e125]
|
||||
- paragraph [ref=e126]: Post your requirement and discover trusted specialists.
|
||||
- link "Explore Customer" [ref=e127] [cursor=pointer]:
|
||||
- /url: /roles/customer
|
||||
- article [ref=e128]:
|
||||
- img "Join as Professional" [ref=e130]
|
||||
- generic [ref=e132]:
|
||||
- generic [ref=e133]:
|
||||
- img [ref=e135]
|
||||
- generic [ref=e139]:
|
||||
- img [ref=e140]
|
||||
- text: Professional
|
||||
- heading "Join as Professional" [level=3] [ref=e143]
|
||||
- paragraph [ref=e144]: Create a trusted profile and grow through verified demand.
|
||||
- link "Explore Professionals" [ref=e145] [cursor=pointer]:
|
||||
- /url: /professionals
|
||||
- generic [ref=e147]:
|
||||
- generic [ref=e148]:
|
||||
- heading "Explore professional categories" [level=2] [ref=e150]
|
||||
- link "View all professionals" [ref=e151] [cursor=pointer]:
|
||||
- /url: /professionals
|
||||
- generic [ref=e152]:
|
||||
- button "All" [ref=e153] [cursor=pointer]
|
||||
- button "Creative" [ref=e154] [cursor=pointer]
|
||||
- button "Tech" [ref=e155] [cursor=pointer]
|
||||
- button "Education" [ref=e156] [cursor=pointer]
|
||||
- button "Wellness" [ref=e157] [cursor=pointer]
|
||||
- button "Events" [ref=e158] [cursor=pointer]
|
||||
- button "Marketing" [ref=e159] [cursor=pointer]
|
||||
- generic [ref=e160]:
|
||||
- article [ref=e161]:
|
||||
- img "Developer" [ref=e163]
|
||||
- generic [ref=e165]:
|
||||
- generic [ref=e167]: tech
|
||||
- heading "Developer" [level=3] [ref=e168]
|
||||
- paragraph [ref=e169]: Join Nxtgauge as a Developer and connect with customers and companies through a trust-first workflow.
|
||||
- generic [ref=e170]:
|
||||
- link "Explore Developer" [ref=e171] [cursor=pointer]:
|
||||
- /url: /professionals/developer
|
||||
- link "Join Developer" [ref=e172] [cursor=pointer]:
|
||||
- /url: /signup?intent=professional&role=DEVELOPER
|
||||
- article [ref=e173]:
|
||||
- img "Photographer" [ref=e175]
|
||||
- generic [ref=e177]:
|
||||
- generic [ref=e179]: creative
|
||||
- heading "Photographer" [level=3] [ref=e180]
|
||||
- paragraph [ref=e181]: Join as a Photographer and receive opportunities where trust and profile quality matter.
|
||||
- generic [ref=e182]:
|
||||
- link "Explore Photographer" [ref=e183] [cursor=pointer]:
|
||||
- /url: /professionals/photographer
|
||||
- link "Join Photographer" [ref=e184] [cursor=pointer]:
|
||||
- /url: /signup?intent=professional&role=PHOTOGRAPHER
|
||||
- article [ref=e185]:
|
||||
- img "Makeup Artist" [ref=e187]
|
||||
- generic [ref=e189]:
|
||||
- generic [ref=e191]: creative
|
||||
- heading "Makeup Artist" [level=3] [ref=e192]
|
||||
- paragraph [ref=e193]: Join Nxtgauge as a Makeup Artist and connect with verified requirements from customers and event planners.
|
||||
- generic [ref=e194]:
|
||||
- link "Explore Makeup Artist" [ref=e195] [cursor=pointer]:
|
||||
- /url: /professionals/makeup-artist
|
||||
- link "Join Makeup Artist" [ref=e196] [cursor=pointer]:
|
||||
- /url: /signup?intent=professional&role=MAKEUP_ARTIST
|
||||
- article [ref=e197]:
|
||||
- img "Tutor" [ref=e199]
|
||||
- generic [ref=e201]:
|
||||
- generic [ref=e203]: education
|
||||
- heading "Tutor" [level=3] [ref=e204]
|
||||
- paragraph [ref=e205]: Join as a Tutor to receive verified educational requirements and present your profile with confidence.
|
||||
- generic [ref=e206]:
|
||||
- link "Explore Tutor" [ref=e207] [cursor=pointer]:
|
||||
- /url: /professionals/tutor
|
||||
- link "Join Tutor" [ref=e208] [cursor=pointer]:
|
||||
- /url: /signup?intent=professional&role=TUTOR
|
||||
- article [ref=e209]:
|
||||
- img "Video Editor" [ref=e211]
|
||||
- generic [ref=e213]:
|
||||
- generic [ref=e215]: creative
|
||||
- heading "Video Editor" [level=3] [ref=e216]
|
||||
- paragraph [ref=e217]: Join as a Video Editor and connect with customers and companies through a review-based trust model.
|
||||
- generic [ref=e218]:
|
||||
- link "Explore Video Editor" [ref=e219] [cursor=pointer]:
|
||||
- /url: /professionals/video-editor
|
||||
- link "Join Video Editor" [ref=e220] [cursor=pointer]:
|
||||
- /url: /signup?intent=professional&role=VIDEO_EDITOR
|
||||
- article [ref=e221]:
|
||||
- img "Graphic Designer" [ref=e223]
|
||||
- generic [ref=e225]:
|
||||
- generic [ref=e227]: creative
|
||||
- heading "Graphic Designer" [level=3] [ref=e228]
|
||||
- paragraph [ref=e229]: Join as a Graphic Designer to connect with verified requirements and role-relevant opportunities.
|
||||
- generic [ref=e230]:
|
||||
- link "Explore Graphic Designer" [ref=e231] [cursor=pointer]:
|
||||
- /url: /professionals/graphic-designer
|
||||
- link "Join Graphic Designer" [ref=e232] [cursor=pointer]:
|
||||
- /url: /signup?intent=professional&role=GRAPHIC_DESIGNER
|
||||
- article [ref=e233]:
|
||||
- img "Social Media Manager" [ref=e235]
|
||||
- generic [ref=e237]:
|
||||
- generic [ref=e239]: marketing
|
||||
- heading "Social Media Manager" [level=3] [ref=e240]
|
||||
- paragraph [ref=e241]: Join as a Social Media Manager and grow with trusted customer and company requirements.
|
||||
- generic [ref=e242]:
|
||||
- link "Explore Social Media Manager" [ref=e243] [cursor=pointer]:
|
||||
- /url: /professionals/social-media-manager
|
||||
- link "Join Social Media Manager" [ref=e244] [cursor=pointer]:
|
||||
- /url: /signup?intent=professional&role=SOCIAL_MEDIA_MANAGER
|
||||
- article [ref=e245]:
|
||||
- img "Fitness Trainer" [ref=e247]
|
||||
- generic [ref=e249]:
|
||||
- generic [ref=e251]: wellness
|
||||
- heading "Fitness Trainer" [level=3] [ref=e252]
|
||||
- paragraph [ref=e253]: Join as a Fitness Trainer and grow through profile-led visibility in a trust-first marketplace.
|
||||
- generic [ref=e254]:
|
||||
- link "Explore Fitness Trainer" [ref=e255] [cursor=pointer]:
|
||||
- /url: /professionals/fitness-trainer
|
||||
- link "Join Fitness Trainer" [ref=e256] [cursor=pointer]:
|
||||
- /url: /signup?intent=professional&role=FITNESS_TRAINER
|
||||
- generic [ref=e258]:
|
||||
- generic [ref=e259]:
|
||||
- paragraph [ref=e260]: Why Nxtgauge
|
||||
- heading "Trust, approvals, and better matching in one flow." [level=2] [ref=e261]
|
||||
- generic [ref=e262]:
|
||||
- article [ref=e263]:
|
||||
- generic [ref=e265]:
|
||||
- paragraph [ref=e266]: Capability 1 of 6
|
||||
- img [ref=e268]
|
||||
- heading "Verified profiles & businesses" [level=3] [ref=e271]
|
||||
- paragraph [ref=e272]: Identity and profile checks reduce fake submissions and spam.
|
||||
- generic [ref=e274]:
|
||||
- button "Show Verified profiles & businesses" [ref=e275]:
|
||||
- img [ref=e276]
|
||||
- button "Show Approval-based quality (24-48 hours)" [ref=e279]:
|
||||
- img [ref=e280]
|
||||
- button "Show Smart matching using tags/skills" [ref=e282]:
|
||||
- img [ref=e283]
|
||||
- button "Show Focused discovery with filters" [ref=e285]:
|
||||
- img [ref=e286]
|
||||
- button "Show Controlled contact visibility" [ref=e289]:
|
||||
- img [ref=e290]
|
||||
- button "Show Notifications & updates" [ref=e293]:
|
||||
- img [ref=e294]
|
||||
- generic [ref=e297]:
|
||||
- button "Previous slide" [ref=e298]: ◀
|
||||
- button "Next slide" [ref=e299]: ▶
|
||||
- generic [ref=e301]:
|
||||
- generic [ref=e302]:
|
||||
- paragraph [ref=e303]: How it works
|
||||
- heading "Clear journey, zero confusion" [level=2] [ref=e304]
|
||||
- article [ref=e305]:
|
||||
- generic [ref=e306]:
|
||||
- img "Customers role" [ref=e308]
|
||||
- generic [ref=e309]:
|
||||
- paragraph [ref=e310]: Customers
|
||||
- heading "Hire trusted professionals with less noise" [level=3] [ref=e311]
|
||||
- paragraph [ref=e312]: Post requirements, pass review checks, and receive better-fit responses.
|
||||
- generic [ref=e313]:
|
||||
- paragraph [ref=e314]: Step Flow
|
||||
- generic [ref=e315]:
|
||||
- generic [ref=e316]:
|
||||
- generic [ref=e317]: "1"
|
||||
- generic [ref=e318]:
|
||||
- heading "Create account" [level=4] [ref=e319]
|
||||
- paragraph [ref=e320]: Sign up quickly with customer intent.
|
||||
- generic [ref=e321]:
|
||||
- generic [ref=e322]: "2"
|
||||
- generic [ref=e323]:
|
||||
- heading "Share requirement" [level=4] [ref=e324]
|
||||
- paragraph [ref=e325]: Add budget, scope, and timing.
|
||||
- generic [ref=e326]:
|
||||
- generic [ref=e327]: "3"
|
||||
- generic [ref=e328]:
|
||||
- heading "Review and verify" [level=4] [ref=e329]
|
||||
- paragraph [ref=e330]: Quality checks run before visibility.
|
||||
- generic [ref=e331]:
|
||||
- generic [ref=e332]: "4"
|
||||
- generic [ref=e333]:
|
||||
- heading "Track responses" [level=4] [ref=e334]
|
||||
- paragraph [ref=e335]: Monitor replies and status updates.
|
||||
- generic [ref=e337]:
|
||||
- button "Previous role" [ref=e338]: ←
|
||||
- button "Next role" [ref=e339]: →
|
||||
- generic [ref=e346]:
|
||||
- heading "Frequently asked questions" [level=2] [ref=e347]
|
||||
- paragraph [ref=e348]: Quick answers before you create your account.
|
||||
- generic [ref=e349]:
|
||||
- article [ref=e350]:
|
||||
- button "What is Nxtgauge? ⌄" [ref=e351] [cursor=pointer]:
|
||||
- generic [ref=e352]: What is Nxtgauge?
|
||||
- generic [ref=e353]: ⌄
|
||||
- paragraph [ref=e354]: Nxtgauge connects customers, professionals, companies, and job seekers in one trusted platform.
|
||||
- article [ref=e355]:
|
||||
- button "Who can join Nxtgauge? ⌄" [ref=e356] [cursor=pointer]:
|
||||
- generic [ref=e357]: Who can join Nxtgauge?
|
||||
- generic [ref=e358]: ⌄
|
||||
- article [ref=e359]:
|
||||
- button "Why does Nxtgauge require verification? ⌄" [ref=e360] [cursor=pointer]:
|
||||
- generic [ref=e361]: Why does Nxtgauge require verification?
|
||||
- generic [ref=e362]: ⌄
|
||||
- article [ref=e363]:
|
||||
- button "How long does account approval take? ⌄" [ref=e364] [cursor=pointer]:
|
||||
- generic [ref=e365]: How long does account approval take?
|
||||
- generic [ref=e366]: ⌄
|
||||
- article [ref=e367]:
|
||||
- button "Do I need to choose my role during signup? ⌄" [ref=e368] [cursor=pointer]:
|
||||
- generic [ref=e369]: Do I need to choose my role during signup?
|
||||
- generic [ref=e370]: ⌄
|
||||
- generic [ref=e372]:
|
||||
- generic [ref=e373]:
|
||||
- paragraph [ref=e374]: Quick Actions
|
||||
- heading "Start with one account. Choose your path after." [level=2] [ref=e375]
|
||||
- paragraph [ref=e376]: Hire, post, apply, or join as a professional from stable role-specific pages.
|
||||
- generic [ref=e377]:
|
||||
- link "Get Started" [ref=e378] [cursor=pointer]:
|
||||
- /url: /signup/customer
|
||||
- link "Explore Professionals" [ref=e379] [cursor=pointer]:
|
||||
- /url: /professionals
|
||||
- link "Post a Job" [ref=e380] [cursor=pointer]:
|
||||
- /url: /signup/company
|
||||
- generic [ref=e382]:
|
||||
- img "NXTGAUGE" [ref=e383]
|
||||
- paragraph [ref=e384]: © 2026 Nxtgauge. All rights reserved.
|
||||
- generic [ref=e385]:
|
||||
- link "Professionals" [ref=e386] [cursor=pointer]:
|
||||
- /url: /professionals
|
||||
- link "Terms" [ref=e387] [cursor=pointer]:
|
||||
- /url: /terms
|
||||
- link "Privacy" [ref=e388] [cursor=pointer]:
|
||||
- /url: /privacy
|
||||
- link "Help Center" [ref=e389] [cursor=pointer]:
|
||||
- /url: /help-center
|
||||
- button "AI Assistant" [ref=e390] [cursor=pointer]:
|
||||
- img [ref=e391]
|
||||
```
|
||||
|
||||
# Test source
|
||||
|
||||
```ts
|
||||
1 | import { test, expect } from "@playwright/test";
|
||||
2 | import AxeBuilder from "@axe-core/playwright";
|
||||
3 |
|
||||
4 | test.describe("Public Frontend Accessibility", () => {
|
||||
5 | test("homepage should have no accessibility violations", async ({ page }) => {
|
||||
6 | await page.goto("/");
|
||||
7 | const results = await new AxeBuilder({ page }).analyze();
|
||||
> 8 | expect(results.violations).toEqual([]);
|
||||
| ^ Error: expect(received).toEqual(expected) // deep equality
|
||||
9 | });
|
||||
10 |
|
||||
11 | test("professionals listing page should be accessible", async ({ page }) => {
|
||||
12 | await page.goto("/professionals");
|
||||
13 | const results = await new AxeBuilder({ page }).analyze();
|
||||
14 | expect(results.violations).toEqual([]);
|
||||
15 | });
|
||||
16 | });
|
||||
17 |
|
||||
```
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: api.spec.ts >> AI API Endpoints >> Company AI - Generate Job Field >> POST /ai/generate-job-field returns generated content
|
||||
- Location: tests/e2e/api.spec.ts:41:5
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.post: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → POST http://localhost:3000/api/auth/login
|
||||
- user-agent: Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Mobile Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- content-type: application/json
|
||||
- content-length: 65
|
||||
|
||||
```
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: api.spec.ts >> Auth API Endpoints >> POST /auth/login rate limits after too many attempts
|
||||
- Location: tests/e2e/api.spec.ts:215:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.post: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → POST http://localhost:3000/api/auth/login
|
||||
- user-agent: Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Mobile Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- content-type: application/json
|
||||
- content-length: 65
|
||||
|
||||
```
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: api.spec.ts >> Auth API Endpoints >> POST /auth/login returns token for valid credentials
|
||||
- Location: tests/e2e/api.spec.ts:182:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.post: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → POST http://localhost:3000/api/auth/login
|
||||
- user-agent: Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Mobile Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- content-type: application/json
|
||||
- content-length: 61
|
||||
|
||||
```
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: security.spec.ts >> Security - Token Handling >> Expired token is rejected
|
||||
- Location: tests/e2e/security.spec.ts:262:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.get: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → GET http://localhost:3000/api/ai/usage
|
||||
- user-agent: Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Mobile Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZXhwIjoxNjAwMDAwMDAwfQ.dummysignature
|
||||
|
||||
```
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: api.spec.ts >> AI API Endpoints >> Company AI - Generate Job Field >> POST /ai/generate-job-field rate limits after daily quota
|
||||
- Location: tests/e2e/api.spec.ts:91:5
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.post: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → POST http://localhost:3000/api/auth/login
|
||||
- user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- content-type: application/json
|
||||
- content-length: 65
|
||||
|
||||
```
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: security.spec.ts >> Security - Authentication >> JWT token is not returned for invalid credentials
|
||||
- Location: tests/e2e/security.spec.ts:6:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.post: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → POST http://localhost:3000/api/auth/login
|
||||
- user-agent: Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Mobile Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- content-type: application/json
|
||||
- content-length: 62
|
||||
|
||||
```
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: api.spec.ts >> AI API Endpoints >> AI Usage Endpoint >> GET /ai/usage returns usage stats for company
|
||||
- Location: tests/e2e/api.spec.ts:144:5
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.post: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → POST http://localhost:3000/api/auth/login
|
||||
- user-agent: Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Mobile Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- content-type: application/json
|
||||
- content-length: 65
|
||||
|
||||
```
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1 MiB |
|
|
@ -1,24 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: security.spec.ts >> Security - Input Validation >> Very long input is handled without crash
|
||||
- Location: tests/e2e/security.spec.ts:175:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.post: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → POST http://localhost:3000/api/auth/login
|
||||
- user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- content-type: application/json
|
||||
- content-length: 20038
|
||||
|
||||
```
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: security.spec.ts >> Security - Input Validation >> SQL injection in login email is handled safely
|
||||
- Location: tests/e2e/security.spec.ts:149:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.post: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → POST http://localhost:3000/api/auth/login
|
||||
- user-agent: Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Mobile Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- content-type: application/json
|
||||
- content-length: 45
|
||||
|
||||
```
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: api.spec.ts >> Auth API Endpoints >> POST /auth/login returns 401 for invalid credentials
|
||||
- Location: tests/e2e/api.spec.ts:202:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.post: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → POST http://localhost:3000/api/auth/login
|
||||
- user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- content-type: application/json
|
||||
- content-length: 58
|
||||
|
||||
```
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: api.spec.ts >> Gateway API >> Gateway returns 404 for unknown routes
|
||||
- Location: tests/e2e/api.spec.ts:255:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.get: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → GET http://localhost:3000/api/nonexistent-route
|
||||
- user-agent: Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Mobile Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
|
||||
```
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: security.spec.ts >> Security - Input Validation >> Very long input is handled without crash
|
||||
- Location: tests/e2e/security.spec.ts:175:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.post: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → POST http://localhost:3000/api/auth/login
|
||||
- user-agent: Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Mobile Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- content-type: application/json
|
||||
- content-length: 20038
|
||||
|
||||
```
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: security.spec.ts >> Security - Token Handling >> Expired token is rejected
|
||||
- Location: tests/e2e/security.spec.ts:262:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.get: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → GET http://localhost:3000/api/ai/usage
|
||||
- user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZXhwIjoxNjAwMDAwMDAwfQ.dummysignature
|
||||
|
||||
```
|
||||
Binary file not shown.
|
|
@ -1,24 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: security.spec.ts >> Security - Rate Limiting >> AI endpoints rate limit after daily quota exceeded
|
||||
- Location: tests/e2e/security.spec.ts:108:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.post: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → POST http://localhost:3000/api/auth/login
|
||||
- user-agent: Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Mobile Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- content-type: application/json
|
||||
- content-length: 65
|
||||
|
||||
```
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: security.spec.ts >> Security - Authentication >> Protected endpoint rejects request without token
|
||||
- Location: tests/e2e/security.spec.ts:42:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.get: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → GET http://localhost:3000/api/ai/usage
|
||||
- user-agent: Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Mobile Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
|
||||
```
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 360 KiB |
Binary file not shown.
|
|
@ -1,22 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: security.spec.ts >> Security - Response Headers >> API error responses do not expose server internals
|
||||
- Location: tests/e2e/security.spec.ts:248:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.get: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → GET http://localhost:3000/api/api/nonexistent
|
||||
- user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
|
||||
```
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: security.spec.ts >> Security - Input Validation >> SQL injection in login email is handled safely
|
||||
- Location: tests/e2e/security.spec.ts:149:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.post: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → POST http://localhost:3000/api/auth/login
|
||||
- user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- content-type: application/json
|
||||
- content-length: 45
|
||||
|
||||
```
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: api.spec.ts >> Auth API Endpoints >> POST /auth/login rate limits after too many attempts
|
||||
- Location: tests/e2e/api.spec.ts:215:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.post: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → POST http://localhost:3000/api/auth/login
|
||||
- user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- content-type: application/json
|
||||
- content-length: 65
|
||||
|
||||
```
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: api.spec.ts >> Gateway API >> Gateway routes /api/ai/* to users service
|
||||
- Location: tests/e2e/api.spec.ts:240:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.get: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → GET http://localhost:3000/api/ai/usage
|
||||
- user-agent: Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Mobile Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- Authorization: Bearer dummy
|
||||
|
||||
```
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: api.spec.ts >> AI API Endpoints >> Company AI - Generate Job Field >> POST /ai/generate-job-field rate limits after daily quota
|
||||
- Location: tests/e2e/api.spec.ts:91:5
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.post: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → POST http://localhost:3000/api/auth/login
|
||||
- user-agent: Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Mobile Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- content-type: application/json
|
||||
- content-length: 65
|
||||
|
||||
```
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: security.spec.ts >> Security - Authentication >> Login without email returns proper error
|
||||
- Location: tests/e2e/security.spec.ts:20:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.post: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → POST http://localhost:3000/api/auth/login
|
||||
- user-agent: Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Mobile Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- content-type: application/json
|
||||
- content-length: 27
|
||||
|
||||
```
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: security.spec.ts >> Security - Authentication >> Protected endpoint rejects request with empty Bearer token
|
||||
- Location: tests/e2e/security.spec.ts:60:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.get: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → GET http://localhost:3000/api/ai/usage
|
||||
- user-agent: Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Mobile Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- Authorization: Bearer
|
||||
|
||||
```
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: security.spec.ts >> Security - Authentication >> Protected endpoint rejects request with empty Bearer token
|
||||
- Location: tests/e2e/security.spec.ts:60:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.get: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → GET http://localhost:3000/api/ai/usage
|
||||
- user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
- Authorization: Bearer
|
||||
|
||||
```
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: security.spec.ts >> Security - Response Headers >> API error responses do not expose server internals
|
||||
- Location: tests/e2e/security.spec.ts:248:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: apiRequestContext.get: connect ECONNREFUSED ::1:3000
|
||||
Call log:
|
||||
- → GET http://localhost:3000/api/api/nonexistent
|
||||
- user-agent: Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Mobile Safari/537.36
|
||||
- accept: */*
|
||||
- accept-encoding: gzip,deflate,br
|
||||
|
||||
```
|
||||
|
|
@ -1,442 +0,0 @@
|
|||
# Instructions
|
||||
|
||||
- Following Playwright test failed.
|
||||
- Explain why, be concise, respect Playwright best practices.
|
||||
- Provide a snippet of code with the fix, if possible.
|
||||
|
||||
# Test info
|
||||
|
||||
- Name: accessibility.spec.ts >> Public Frontend Accessibility >> homepage should have no accessibility violations
|
||||
- Location: tests/e2e/accessibility.spec.ts:5:3
|
||||
|
||||
# Error details
|
||||
|
||||
```
|
||||
Error: expect(received).toEqual(expected) // deep equality
|
||||
|
||||
- Expected - 1
|
||||
+ Received + 58
|
||||
|
||||
- Array []
|
||||
+ Array [
|
||||
+ Object {
|
||||
+ "description": "Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds",
|
||||
+ "help": "Elements must meet minimum color contrast ratio thresholds",
|
||||
+ "helpUrl": "https://dequeuniversity.com/rules/axe/4.11/color-contrast?application=playwright",
|
||||
+ "id": "color-contrast",
|
||||
+ "impact": "serious",
|
||||
+ "nodes": Array [
|
||||
+ Object {
|
||||
+ "all": Array [],
|
||||
+ "any": Array [
|
||||
+ Object {
|
||||
+ "data": Object {
|
||||
+ "bgColor": "#fd6116",
|
||||
+ "contrastRatio": 3.04,
|
||||
+ "expectedContrastRatio": "4.5:1",
|
||||
+ "fgColor": "#ffffff",
|
||||
+ "fontSize": "10.5pt (14px)",
|
||||
+ "fontWeight": "normal",
|
||||
+ "messageKey": null,
|
||||
+ },
|
||||
+ "id": "color-contrast",
|
||||
+ "impact": "serious",
|
||||
+ "message": "Element has insufficient color contrast of 3.04 (foreground color: #ffffff, background color: #fd6116, font size: 10.5pt (14px), font weight: normal). Expected contrast ratio of 4.5:1",
|
||||
+ "relatedNodes": Array [
|
||||
+ Object {
|
||||
+ "html": "<button data-hk=\"00000001000001000000000a1080\" type=\"button\" class=\"chip-btn active\">All</button>",
|
||||
+ "target": Array [
|
||||
+ "button[data-hk=\"00000001000001000000000a1080\"]",
|
||||
+ ],
|
||||
+ },
|
||||
+ ],
|
||||
+ },
|
||||
+ ],
|
||||
+ "failureSummary": "Fix any of the following:
|
||||
+ Element has insufficient color contrast of 3.04 (foreground color: #ffffff, background color: #fd6116, font size: 10.5pt (14px), font weight: normal). Expected contrast ratio of 4.5:1",
|
||||
+ "html": "<button data-hk=\"00000001000001000000000a1080\" type=\"button\" class=\"chip-btn active\">All</button>",
|
||||
+ "impact": "serious",
|
||||
+ "none": Array [],
|
||||
+ "target": Array [
|
||||
+ "button[data-hk=\"00000001000001000000000a1080\"]",
|
||||
+ ],
|
||||
+ },
|
||||
+ ],
|
||||
+ "tags": Array [
|
||||
+ "cat.color",
|
||||
+ "wcag2aa",
|
||||
+ "wcag143",
|
||||
+ "TTv5",
|
||||
+ "TT13.c",
|
||||
+ "EN-301-549",
|
||||
+ "EN-9.1.4.3",
|
||||
+ "ACT",
|
||||
+ "RGAAv4",
|
||||
+ "RGAA-3.2.1",
|
||||
+ ],
|
||||
+ },
|
||||
+ ]
|
||||
```
|
||||
|
||||
# Page snapshot
|
||||
|
||||
```yaml
|
||||
- generic [ref=e2]:
|
||||
- main [ref=e3]:
|
||||
- generic:
|
||||
- generic:
|
||||
- generic:
|
||||
- img
|
||||
- generic:
|
||||
- img
|
||||
- generic:
|
||||
- img
|
||||
- generic:
|
||||
- img
|
||||
- generic:
|
||||
- img
|
||||
- generic [ref=e4]:
|
||||
- navigation [ref=e6]:
|
||||
- link "Nxtgauge home" [ref=e7] [cursor=pointer]:
|
||||
- /url: /
|
||||
- img "NXTGAUGE" [ref=e8]
|
||||
- generic [ref=e9]:
|
||||
- link "Home" [ref=e10] [cursor=pointer]:
|
||||
- /url: /
|
||||
- link "Professionals" [ref=e11] [cursor=pointer]:
|
||||
- /url: /professionals
|
||||
- link "About Us" [ref=e12] [cursor=pointer]:
|
||||
- /url: /about
|
||||
- link "Help Center" [ref=e13] [cursor=pointer]:
|
||||
- /url: /help-center
|
||||
- link "Contact Us" [ref=e14] [cursor=pointer]:
|
||||
- /url: /contact
|
||||
- link "Login" [ref=e16] [cursor=pointer]:
|
||||
- /url: /login
|
||||
- generic [ref=e18]:
|
||||
- generic [ref=e19]:
|
||||
- heading "Trusted hiring and opportunities, verified." [level=1] [ref=e20]
|
||||
- paragraph [ref=e21]: Hire trusted professionals, post verified jobs, and apply faster in one platform.
|
||||
- generic [ref=e22]:
|
||||
- link "Get Started" [ref=e23] [cursor=pointer]:
|
||||
- /url: /signup/customer
|
||||
- link "How It Works" [ref=e24] [cursor=pointer]:
|
||||
- /url: /#how-it-works
|
||||
- paragraph [ref=e25]: Most profile and listing reviews are completed within 24-48 hours.
|
||||
- region "Nxtgauge opportunity graph preview" [ref=e27]:
|
||||
- generic [ref=e28]:
|
||||
- generic [ref=e29]:
|
||||
- img [ref=e30]
|
||||
- generic [ref=e47]: Developer
|
||||
- generic [ref=e48]:
|
||||
- generic [ref=e49]:
|
||||
- generic [ref=e50]: ✓
|
||||
- generic [ref=e51]: Everything from Nxtgauge
|
||||
- paragraph [ref=e52]: Opportunity Workspace
|
||||
- generic [ref=e53]:
|
||||
- strong [ref=e54]: Verified Profiles
|
||||
- generic [ref=e55]: Trust layer ready
|
||||
- generic [ref=e56]:
|
||||
- strong [ref=e57]: Matched Opportunities
|
||||
- generic [ref=e58]: Priority queue
|
||||
- generic [ref=e59]:
|
||||
- strong [ref=e60]: Responses
|
||||
- generic [ref=e61]: Tracked in one flow
|
||||
- generic [ref=e62]:
|
||||
- strong [ref=e63]: Updates
|
||||
- generic [ref=e64]: Live status signals
|
||||
- paragraph [ref=e66]: Opportunities start scattered
|
||||
- generic [ref=e68]:
|
||||
- generic [ref=e70]:
|
||||
- heading "Who Nxtgauge is for" [level=2] [ref=e71]
|
||||
- paragraph [ref=e72]: Start with one account, then activate the journey you need.
|
||||
- generic [ref=e73]:
|
||||
- article [ref=e74]:
|
||||
- img "Post a Job" [ref=e76]
|
||||
- generic [ref=e78]:
|
||||
- generic [ref=e79]:
|
||||
- img [ref=e81]
|
||||
- generic [ref=e85]:
|
||||
- img [ref=e86]
|
||||
- text: Company
|
||||
- heading "Post a Job" [level=3] [ref=e89]
|
||||
- paragraph [ref=e90]: Create verified job openings and find the right talent faster.
|
||||
- link "Explore Company" [ref=e91] [cursor=pointer]:
|
||||
- /url: /roles/company
|
||||
- article [ref=e92]:
|
||||
- img "Apply for Jobs" [ref=e94]
|
||||
- generic [ref=e96]:
|
||||
- generic [ref=e97]:
|
||||
- img [ref=e99]
|
||||
- generic [ref=e102]:
|
||||
- img [ref=e103]
|
||||
- text: Job Seeker
|
||||
- heading "Apply for Jobs" [level=3] [ref=e106]
|
||||
- paragraph [ref=e107]: Build your profile and apply to approved opportunities quickly.
|
||||
- link "Explore Job Seeker" [ref=e108] [cursor=pointer]:
|
||||
- /url: /roles/job-seeker
|
||||
- article [ref=e109]:
|
||||
- img "Hire a Professional" [ref=e111]
|
||||
- generic [ref=e113]:
|
||||
- generic [ref=e114]:
|
||||
- img [ref=e116]
|
||||
- generic [ref=e121]:
|
||||
- img [ref=e122]
|
||||
- text: Customer
|
||||
- heading "Hire a Professional" [level=3] [ref=e125]
|
||||
- paragraph [ref=e126]: Post your requirement and discover trusted specialists.
|
||||
- link "Explore Customer" [ref=e127] [cursor=pointer]:
|
||||
- /url: /roles/customer
|
||||
- article [ref=e128]:
|
||||
- img "Join as Professional" [ref=e130]
|
||||
- generic [ref=e132]:
|
||||
- generic [ref=e133]:
|
||||
- img [ref=e135]
|
||||
- generic [ref=e139]:
|
||||
- img [ref=e140]
|
||||
- text: Professional
|
||||
- heading "Join as Professional" [level=3] [ref=e143]
|
||||
- paragraph [ref=e144]: Create a trusted profile and grow through verified demand.
|
||||
- link "Explore Professionals" [ref=e145] [cursor=pointer]:
|
||||
- /url: /professionals
|
||||
- generic [ref=e147]:
|
||||
- generic [ref=e148]:
|
||||
- heading "Explore professional categories" [level=2] [ref=e150]
|
||||
- link "View all professionals" [ref=e151] [cursor=pointer]:
|
||||
- /url: /professionals
|
||||
- generic [ref=e152]:
|
||||
- button "All" [ref=e153] [cursor=pointer]
|
||||
- button "Creative" [ref=e154] [cursor=pointer]
|
||||
- button "Tech" [ref=e155] [cursor=pointer]
|
||||
- button "Education" [ref=e156] [cursor=pointer]
|
||||
- button "Wellness" [ref=e157] [cursor=pointer]
|
||||
- button "Events" [ref=e158] [cursor=pointer]
|
||||
- button "Marketing" [ref=e159] [cursor=pointer]
|
||||
- generic [ref=e160]:
|
||||
- article [ref=e161]:
|
||||
- img "Developer" [ref=e163]
|
||||
- generic [ref=e165]:
|
||||
- generic [ref=e167]: tech
|
||||
- heading "Developer" [level=3] [ref=e168]
|
||||
- paragraph [ref=e169]: Join Nxtgauge as a Developer and connect with customers and companies through a trust-first workflow.
|
||||
- generic [ref=e170]:
|
||||
- link "Explore Developer" [ref=e171] [cursor=pointer]:
|
||||
- /url: /professionals/developer
|
||||
- link "Join Developer" [ref=e172] [cursor=pointer]:
|
||||
- /url: /signup?intent=professional&role=DEVELOPER
|
||||
- article [ref=e173]:
|
||||
- img "Photographer" [ref=e175]
|
||||
- generic [ref=e177]:
|
||||
- generic [ref=e179]: creative
|
||||
- heading "Photographer" [level=3] [ref=e180]
|
||||
- paragraph [ref=e181]: Join as a Photographer and receive opportunities where trust and profile quality matter.
|
||||
- generic [ref=e182]:
|
||||
- link "Explore Photographer" [ref=e183] [cursor=pointer]:
|
||||
- /url: /professionals/photographer
|
||||
- link "Join Photographer" [ref=e184] [cursor=pointer]:
|
||||
- /url: /signup?intent=professional&role=PHOTOGRAPHER
|
||||
- article [ref=e185]:
|
||||
- img "Makeup Artist" [ref=e187]
|
||||
- generic [ref=e189]:
|
||||
- generic [ref=e191]: creative
|
||||
- heading "Makeup Artist" [level=3] [ref=e192]
|
||||
- paragraph [ref=e193]: Join Nxtgauge as a Makeup Artist and connect with verified requirements from customers and event planners.
|
||||
- generic [ref=e194]:
|
||||
- link "Explore Makeup Artist" [ref=e195] [cursor=pointer]:
|
||||
- /url: /professionals/makeup-artist
|
||||
- link "Join Makeup Artist" [ref=e196] [cursor=pointer]:
|
||||
- /url: /signup?intent=professional&role=MAKEUP_ARTIST
|
||||
- article [ref=e197]:
|
||||
- img "Tutor" [ref=e199]
|
||||
- generic [ref=e201]:
|
||||
- generic [ref=e203]: education
|
||||
- heading "Tutor" [level=3] [ref=e204]
|
||||
- paragraph [ref=e205]: Join as a Tutor to receive verified educational requirements and present your profile with confidence.
|
||||
- generic [ref=e206]:
|
||||
- link "Explore Tutor" [ref=e207] [cursor=pointer]:
|
||||
- /url: /professionals/tutor
|
||||
- link "Join Tutor" [ref=e208] [cursor=pointer]:
|
||||
- /url: /signup?intent=professional&role=TUTOR
|
||||
- article [ref=e209]:
|
||||
- img "Video Editor" [ref=e211]
|
||||
- generic [ref=e213]:
|
||||
- generic [ref=e215]: creative
|
||||
- heading "Video Editor" [level=3] [ref=e216]
|
||||
- paragraph [ref=e217]: Join as a Video Editor and connect with customers and companies through a review-based trust model.
|
||||
- generic [ref=e218]:
|
||||
- link "Explore Video Editor" [ref=e219] [cursor=pointer]:
|
||||
- /url: /professionals/video-editor
|
||||
- link "Join Video Editor" [ref=e220] [cursor=pointer]:
|
||||
- /url: /signup?intent=professional&role=VIDEO_EDITOR
|
||||
- article [ref=e221]:
|
||||
- img "Graphic Designer" [ref=e223]
|
||||
- generic [ref=e225]:
|
||||
- generic [ref=e227]: creative
|
||||
- heading "Graphic Designer" [level=3] [ref=e228]
|
||||
- paragraph [ref=e229]: Join as a Graphic Designer to connect with verified requirements and role-relevant opportunities.
|
||||
- generic [ref=e230]:
|
||||
- link "Explore Graphic Designer" [ref=e231] [cursor=pointer]:
|
||||
- /url: /professionals/graphic-designer
|
||||
- link "Join Graphic Designer" [ref=e232] [cursor=pointer]:
|
||||
- /url: /signup?intent=professional&role=GRAPHIC_DESIGNER
|
||||
- article [ref=e233]:
|
||||
- img "Social Media Manager" [ref=e235]
|
||||
- generic [ref=e237]:
|
||||
- generic [ref=e239]: marketing
|
||||
- heading "Social Media Manager" [level=3] [ref=e240]
|
||||
- paragraph [ref=e241]: Join as a Social Media Manager and grow with trusted customer and company requirements.
|
||||
- generic [ref=e242]:
|
||||
- link "Explore Social Media Manager" [ref=e243] [cursor=pointer]:
|
||||
- /url: /professionals/social-media-manager
|
||||
- link "Join Social Media Manager" [ref=e244] [cursor=pointer]:
|
||||
- /url: /signup?intent=professional&role=SOCIAL_MEDIA_MANAGER
|
||||
- article [ref=e245]:
|
||||
- img "Fitness Trainer" [ref=e247]
|
||||
- generic [ref=e249]:
|
||||
- generic [ref=e251]: wellness
|
||||
- heading "Fitness Trainer" [level=3] [ref=e252]
|
||||
- paragraph [ref=e253]: Join as a Fitness Trainer and grow through profile-led visibility in a trust-first marketplace.
|
||||
- generic [ref=e254]:
|
||||
- link "Explore Fitness Trainer" [ref=e255] [cursor=pointer]:
|
||||
- /url: /professionals/fitness-trainer
|
||||
- link "Join Fitness Trainer" [ref=e256] [cursor=pointer]:
|
||||
- /url: /signup?intent=professional&role=FITNESS_TRAINER
|
||||
- generic [ref=e258]:
|
||||
- generic [ref=e259]:
|
||||
- paragraph [ref=e260]: Why Nxtgauge
|
||||
- heading "Trust, approvals, and better matching in one flow." [level=2] [ref=e261]
|
||||
- generic [ref=e262]:
|
||||
- article [ref=e263]:
|
||||
- generic [ref=e265]:
|
||||
- paragraph [ref=e266]: Capability 1 of 6
|
||||
- img [ref=e268]
|
||||
- heading "Verified profiles & businesses" [level=3] [ref=e271]
|
||||
- paragraph [ref=e272]: Identity and profile checks reduce fake submissions and spam.
|
||||
- generic [ref=e274]:
|
||||
- button "Show Verified profiles & businesses" [ref=e275]:
|
||||
- img [ref=e276]
|
||||
- button "Show Approval-based quality (24-48 hours)" [ref=e279]:
|
||||
- img [ref=e280]
|
||||
- button "Show Smart matching using tags/skills" [ref=e282]:
|
||||
- img [ref=e283]
|
||||
- button "Show Focused discovery with filters" [ref=e285]:
|
||||
- img [ref=e286]
|
||||
- button "Show Controlled contact visibility" [ref=e289]:
|
||||
- img [ref=e290]
|
||||
- button "Show Notifications & updates" [ref=e293]:
|
||||
- img [ref=e294]
|
||||
- generic [ref=e297]:
|
||||
- button "Previous slide" [ref=e298]: ◀
|
||||
- button "Next slide" [ref=e299]: ▶
|
||||
- generic [ref=e301]:
|
||||
- generic [ref=e302]:
|
||||
- paragraph [ref=e303]: How it works
|
||||
- heading "Clear journey, zero confusion" [level=2] [ref=e304]
|
||||
- article [ref=e305]:
|
||||
- generic [ref=e306]:
|
||||
- img "Customers role" [ref=e308]
|
||||
- generic [ref=e309]:
|
||||
- paragraph [ref=e310]: Customers
|
||||
- heading "Hire trusted professionals with less noise" [level=3] [ref=e311]
|
||||
- paragraph [ref=e312]: Post requirements, pass review checks, and receive better-fit responses.
|
||||
- generic [ref=e313]:
|
||||
- paragraph [ref=e314]: Step Flow
|
||||
- generic [ref=e315]:
|
||||
- generic [ref=e316]:
|
||||
- generic [ref=e317]: "1"
|
||||
- generic [ref=e318]:
|
||||
- heading "Create account" [level=4] [ref=e319]
|
||||
- paragraph [ref=e320]: Sign up quickly with customer intent.
|
||||
- generic [ref=e321]:
|
||||
- generic [ref=e322]: "2"
|
||||
- generic [ref=e323]:
|
||||
- heading "Share requirement" [level=4] [ref=e324]
|
||||
- paragraph [ref=e325]: Add budget, scope, and timing.
|
||||
- generic [ref=e326]:
|
||||
- generic [ref=e327]: "3"
|
||||
- generic [ref=e328]:
|
||||
- heading "Review and verify" [level=4] [ref=e329]
|
||||
- paragraph [ref=e330]: Quality checks run before visibility.
|
||||
- generic [ref=e331]:
|
||||
- generic [ref=e332]: "4"
|
||||
- generic [ref=e333]:
|
||||
- heading "Track responses" [level=4] [ref=e334]
|
||||
- paragraph [ref=e335]: Monitor replies and status updates.
|
||||
- generic [ref=e337]:
|
||||
- button "Previous role" [ref=e338]: ←
|
||||
- button "Next role" [ref=e339]: →
|
||||
- generic [ref=e346]:
|
||||
- heading "Frequently asked questions" [level=2] [ref=e347]
|
||||
- paragraph [ref=e348]: Quick answers before you create your account.
|
||||
- generic [ref=e349]:
|
||||
- article [ref=e350]:
|
||||
- button "What is Nxtgauge? ⌄" [ref=e351] [cursor=pointer]:
|
||||
- generic [ref=e352]: What is Nxtgauge?
|
||||
- generic [ref=e353]: ⌄
|
||||
- paragraph [ref=e354]: Nxtgauge connects customers, professionals, companies, and job seekers in one trusted platform.
|
||||
- article [ref=e355]:
|
||||
- button "Who can join Nxtgauge? ⌄" [ref=e356] [cursor=pointer]:
|
||||
- generic [ref=e357]: Who can join Nxtgauge?
|
||||
- generic [ref=e358]: ⌄
|
||||
- article [ref=e359]:
|
||||
- button "Why does Nxtgauge require verification? ⌄" [ref=e360] [cursor=pointer]:
|
||||
- generic [ref=e361]: Why does Nxtgauge require verification?
|
||||
- generic [ref=e362]: ⌄
|
||||
- article [ref=e363]:
|
||||
- button "How long does account approval take? ⌄" [ref=e364] [cursor=pointer]:
|
||||
- generic [ref=e365]: How long does account approval take?
|
||||
- generic [ref=e366]: ⌄
|
||||
- article [ref=e367]:
|
||||
- button "Do I need to choose my role during signup? ⌄" [ref=e368] [cursor=pointer]:
|
||||
- generic [ref=e369]: Do I need to choose my role during signup?
|
||||
- generic [ref=e370]: ⌄
|
||||
- generic [ref=e372]:
|
||||
- generic [ref=e373]:
|
||||
- paragraph [ref=e374]: Quick Actions
|
||||
- heading "Start with one account. Choose your path after." [level=2] [ref=e375]
|
||||
- paragraph [ref=e376]: Hire, post, apply, or join as a professional from stable role-specific pages.
|
||||
- generic [ref=e377]:
|
||||
- link "Get Started" [ref=e378] [cursor=pointer]:
|
||||
- /url: /signup/customer
|
||||
- link "Explore Professionals" [ref=e379] [cursor=pointer]:
|
||||
- /url: /professionals
|
||||
- link "Post a Job" [ref=e380] [cursor=pointer]:
|
||||
- /url: /signup/company
|
||||
- generic [ref=e382]:
|
||||
- img "NXTGAUGE" [ref=e383]
|
||||
- paragraph [ref=e384]: © 2026 Nxtgauge. All rights reserved.
|
||||
- generic [ref=e385]:
|
||||
- link "Professionals" [ref=e386] [cursor=pointer]:
|
||||
- /url: /professionals
|
||||
- link "Terms" [ref=e387] [cursor=pointer]:
|
||||
- /url: /terms
|
||||
- link "Privacy" [ref=e388] [cursor=pointer]:
|
||||
- /url: /privacy
|
||||
- link "Help Center" [ref=e389] [cursor=pointer]:
|
||||
- /url: /help-center
|
||||
- button "AI Assistant" [ref=e390] [cursor=pointer]:
|
||||
- img [ref=e391]
|
||||
```
|
||||
|
||||
# Test source
|
||||
|
||||
```ts
|
||||
1 | import { test, expect } from "@playwright/test";
|
||||
2 | import AxeBuilder from "@axe-core/playwright";
|
||||
3 |
|
||||
4 | test.describe("Public Frontend Accessibility", () => {
|
||||
5 | test("homepage should have no accessibility violations", async ({ page }) => {
|
||||
6 | await page.goto("/");
|
||||
7 | const results = await new AxeBuilder({ page }).analyze();
|
||||
> 8 | expect(results.violations).toEqual([]);
|
||||
| ^ Error: expect(received).toEqual(expected) // deep equality
|
||||
9 | });
|
||||
10 |
|
||||
11 | test("professionals listing page should be accessible", async ({ page }) => {
|
||||
12 | await page.goto("/professionals");
|
||||
13 | const results = await new AxeBuilder({ page }).analyze();
|
||||
14 | expect(results.violations).toEqual([]);
|
||||
15 | });
|
||||
16 | });
|
||||
17 |
|
||||
```
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,15 +1,19 @@
|
|||
import { test, expect, request } from "@playwright/test";
|
||||
import { solveCaptcha } from "./helpers/captcha";
|
||||
|
||||
const API_BASE = process.env.TEST_ENV === 'production'
|
||||
? "https://test111.nxtgauge.com/api"
|
||||
const API_BASE = process.env.TEST_ENV === 'production'
|
||||
? "https://test111.nxtgauge.com/api"
|
||||
: "http://localhost:3000/api";
|
||||
|
||||
async function getAuthToken(): Promise<string | null> {
|
||||
const ctx = await request.newContext();
|
||||
const { captcha_id, captcha_answer } = await solveCaptcha();
|
||||
const res = await ctx.post(`${API_BASE}/auth/login`, {
|
||||
data: {
|
||||
email: "testtutora2026@example.com",
|
||||
password: "Test1234!",
|
||||
captcha_id,
|
||||
captcha_answer,
|
||||
},
|
||||
});
|
||||
if (!res.ok()) return null;
|
||||
|
|
@ -19,10 +23,13 @@ async function getAuthToken(): Promise<string | null> {
|
|||
|
||||
async function getCompanyAuthToken(): Promise<string | null> {
|
||||
const ctx = await request.newContext();
|
||||
const { captcha_id, captcha_answer } = await solveCaptcha();
|
||||
const res = await ctx.post(`${API_BASE}/auth/login`, {
|
||||
data: {
|
||||
email: "testcompany@example.com",
|
||||
password: "TestPassword123!",
|
||||
captcha_id,
|
||||
captcha_answer,
|
||||
},
|
||||
});
|
||||
if (!res.ok()) return null;
|
||||
|
|
@ -183,10 +190,13 @@ test.describe("AI API Endpoints", () => {
|
|||
test.describe("Auth API Endpoints", () => {
|
||||
test("POST /auth/login returns token for valid credentials", async () => {
|
||||
const ctx = await request.newContext();
|
||||
const { captcha_id, captcha_answer } = await solveCaptcha();
|
||||
const res = await ctx.post(`${API_BASE}/auth/login`, {
|
||||
data: {
|
||||
email: "testtutora2026@example.com",
|
||||
password: "Test1234!",
|
||||
captcha_id,
|
||||
captcha_answer,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -203,10 +213,13 @@ test.describe("Auth API Endpoints", () => {
|
|||
|
||||
test("POST /auth/login returns 401 for invalid credentials", async () => {
|
||||
const ctx = await request.newContext();
|
||||
const { captcha_id, captcha_answer } = await solveCaptcha();
|
||||
const res = await ctx.post(`${API_BASE}/auth/login`, {
|
||||
data: {
|
||||
email: "invalid@example.com",
|
||||
password: "wrongpassword",
|
||||
captcha_id,
|
||||
captcha_answer,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -217,18 +230,24 @@ test.describe("Auth API Endpoints", () => {
|
|||
test("POST /auth/login rate limits after too many attempts", async () => {
|
||||
const ctx = await request.newContext();
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const { captcha_id, captcha_answer } = await solveCaptcha();
|
||||
await ctx.post(`${API_BASE}/auth/login`, {
|
||||
data: {
|
||||
email: "testtutora2026@example.com",
|
||||
password: "wrongpassword",
|
||||
captcha_id,
|
||||
captcha_answer,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const { captcha_id, captcha_answer } = await solveCaptcha();
|
||||
const res = await ctx.post(`${API_BASE}/auth/login`, {
|
||||
data: {
|
||||
email: "testtutora2026@example.com",
|
||||
password: "Test1234!",
|
||||
captcha_id,
|
||||
captcha_answer,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { test, expect, chromium, BrowserContext, Page } from "@playwright/test";
|
||||
import { randomUUID } from "crypto";
|
||||
import { execSync } from "child_process";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { apiRegister, apiVerifyEmail, apiLogin } from "./helpers/auth-flow";
|
||||
import { APP_BASE_URL } from "./helpers/env";
|
||||
|
||||
const SCREENSHOT_DIR = "/Users/ashwin/workspace/nxtgauge-frontend-solid/test-results/company-admin-e2e";
|
||||
const SCREENSHOT_DIR = path.join(__dirname, "..", "..", "test-results", "company-admin-e2e");
|
||||
|
||||
if (!fs.existsSync(SCREENSHOT_DIR)) {
|
||||
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
|
||||
|
|
@ -20,63 +22,30 @@ interface TestUser {
|
|||
companyName?: string;
|
||||
}
|
||||
|
||||
async function getOTPFromRedis(userId: string): Promise<string | null> {
|
||||
try {
|
||||
const plainKey = `otp:plain:${userId}`;
|
||||
let otpCode = execSync(`redis-cli GET "${plainKey}"`, { encoding: "utf8" }).trim();
|
||||
if (otpCode && otpCode.length >= 4) return otpCode;
|
||||
|
||||
const keys = execSync("redis-cli KEYS 'otp:code:*'", { encoding: "utf8" })
|
||||
.trim().split("\n").filter(Boolean);
|
||||
for (const k of keys) {
|
||||
const v = execSync(`redis-cli GET "${k}"`, { encoding: "utf8" }).trim();
|
||||
if (v === userId) {
|
||||
otpCode = k.replace("otp:code:", "");
|
||||
return otpCode;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function registerUser(user: TestUser): Promise<TestUser> {
|
||||
console.log(`\n📝 Registering ${user.intent} via API...`);
|
||||
const regResponse = await fetch("http://localhost:9100/api/auth/register", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(user),
|
||||
const regData = await apiRegister({
|
||||
email: user.email,
|
||||
password: user.password,
|
||||
first_name: user.firstName,
|
||||
last_name: user.lastName,
|
||||
intent: user.intent,
|
||||
});
|
||||
const regData = await regResponse.json();
|
||||
if (!regData.user_id) throw new Error(`Registration failed: ${JSON.stringify(regData)}`);
|
||||
user.userId = regData.user_id;
|
||||
console.log(` ✅ Registered, user_id: ${user.userId}`);
|
||||
|
||||
// Get OTP from Redis
|
||||
// Get OTP from Redis + verify via API
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
const otpCode = await getOTPFromRedis(user.userId!);
|
||||
if (!otpCode) throw new Error("Could not get OTP from Redis");
|
||||
console.log(` ✅ OTP retrieved: ${otpCode}`);
|
||||
|
||||
// Verify OTP
|
||||
const verifyResponse = await fetch("http://localhost:9100/api/auth/verify-email", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ user_id: user.userId, otp: otpCode })
|
||||
});
|
||||
if (!verifyResponse.ok) throw new Error("OTP verification failed");
|
||||
const verified = await apiVerifyEmail(user.email, user.userId!);
|
||||
if (!verified) throw new Error("OTP verification failed");
|
||||
console.log(` ✅ OTP verified!`);
|
||||
|
||||
// Login
|
||||
const loginResponse = await fetch("http://localhost:9100/api/auth/login", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email: user.email, password: user.password })
|
||||
});
|
||||
const loginData = await loginResponse.json();
|
||||
if (!loginData.access_token) throw new Error("Login failed");
|
||||
user.accessToken = loginData.access_token;
|
||||
const loginData = await apiLogin(user.email, user.password);
|
||||
const accessToken = loginData?.access_token || "";
|
||||
if (!accessToken) throw new Error("Login failed");
|
||||
user.accessToken = accessToken;
|
||||
console.log(` ✅ Logged in, token length: ${user.accessToken!.length}`);
|
||||
|
||||
return user;
|
||||
|
|
@ -130,8 +99,8 @@ test.describe("Company E2E Flow with Admin Verification", () => {
|
|||
await registerUser(companyUser);
|
||||
|
||||
// ==================== BROWSER SETUP ====================
|
||||
const browser = await chromium.launch({ headless: false, slowMo: 30 });
|
||||
const context = await browser.newContext({ viewport: { width: 1400, height: 900 } });
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ baseURL: APP_BASE_URL, viewport: { width: 1400, height: 900 } });
|
||||
|
||||
// ==================== COMPANY FRONTEND FLOW ====================
|
||||
console.log("\n" + "=".repeat(60));
|
||||
|
|
@ -142,7 +111,7 @@ test.describe("Company E2E Flow with Admin Verification", () => {
|
|||
await setupFrontendAuth(companyPage, companyUser);
|
||||
|
||||
// Navigate to dashboard
|
||||
await companyPage.goto("http://localhost:3000/dashboard?role=COMPANY", { waitUntil: "networkidle", timeout: 15000 });
|
||||
await companyPage.goto("/dashboard?role=COMPANY", { waitUntil: "networkidle", timeout: 15000 });
|
||||
await new Promise(r => setTimeout(r, 3000));
|
||||
await takeScreenshot(companyPage, "01_company_dashboard");
|
||||
console.log(" ✅ Company dashboard loaded");
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { test, expect, chromium, BrowserContext, Page } from "@playwright/test";
|
||||
import { randomUUID } from "crypto";
|
||||
import { execSync } from "child_process";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { apiRegister, apiVerifyEmail, apiLogin } from "./helpers/auth-flow";
|
||||
import { APP_BASE_URL, API_BASE } from "./helpers/env";
|
||||
|
||||
const SCREENSHOT_DIR = "/Users/ashwin/workspace/nxtgauge-frontend-solid/test-results/company-complete-e2e";
|
||||
const SCREENSHOT_DIR = path.join(__dirname, "..", "..", "test-results", "company-complete-e2e");
|
||||
|
||||
if (!fs.existsSync(SCREENSHOT_DIR)) {
|
||||
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
|
||||
|
|
@ -20,63 +22,30 @@ interface TestUser {
|
|||
companyName?: string;
|
||||
}
|
||||
|
||||
async function getOTPFromRedis(userId: string): Promise<string | null> {
|
||||
try {
|
||||
const plainKey = `otp:plain:${userId}`;
|
||||
let otpCode = execSync(`redis-cli GET "${plainKey}"`, { encoding: "utf8" }).trim();
|
||||
if (otpCode && otpCode.length >= 4) return otpCode;
|
||||
|
||||
const keys = execSync("redis-cli KEYS 'otp:code:*'", { encoding: "utf8" })
|
||||
.trim().split("\n").filter(Boolean);
|
||||
for (const k of keys) {
|
||||
const v = execSync(`redis-cli GET "${k}"`, { encoding: "utf8" }).trim();
|
||||
if (v === userId) {
|
||||
otpCode = k.replace("otp:code:", "");
|
||||
return otpCode;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function registerUser(user: TestUser): Promise<TestUser> {
|
||||
console.log(`\n📝 Registering ${user.intent} via API...`);
|
||||
const regResponse = await fetch("http://localhost:9100/api/auth/register", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(user),
|
||||
const regData = await apiRegister({
|
||||
email: user.email,
|
||||
password: user.password,
|
||||
first_name: user.firstName,
|
||||
last_name: user.lastName,
|
||||
intent: user.intent,
|
||||
});
|
||||
const regData = await regResponse.json();
|
||||
if (!regData.user_id) throw new Error(`Registration failed: ${JSON.stringify(regData)}`);
|
||||
user.userId = regData.user_id;
|
||||
console.log(` ✅ Registered, user_id: ${user.userId}`);
|
||||
|
||||
// Get OTP from Redis
|
||||
// Get OTP from Redis + verify via API
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
const otpCode = await getOTPFromRedis(user.userId!);
|
||||
if (!otpCode) throw new Error("Could not get OTP from Redis");
|
||||
console.log(` ✅ OTP retrieved: ${otpCode}`);
|
||||
|
||||
// Verify OTP
|
||||
const verifyResponse = await fetch("http://localhost:9100/api/auth/verify-email", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ user_id: user.userId, otp: otpCode })
|
||||
});
|
||||
if (!verifyResponse.ok) throw new Error("OTP verification failed");
|
||||
const verified = await apiVerifyEmail(user.email, user.userId!);
|
||||
if (!verified) throw new Error("OTP verification failed");
|
||||
console.log(` ✅ OTP verified!`);
|
||||
|
||||
// Login
|
||||
const loginResponse = await fetch("http://localhost:9100/api/auth/login", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email: user.email, password: user.password })
|
||||
});
|
||||
const loginData = await loginResponse.json();
|
||||
if (!loginData.access_token) throw new Error("Login failed");
|
||||
user.accessToken = loginData.access_token;
|
||||
const loginData = await apiLogin(user.email, user.password);
|
||||
const accessToken = loginData?.access_token || "";
|
||||
if (!accessToken) throw new Error("Login failed");
|
||||
user.accessToken = accessToken;
|
||||
console.log(` ✅ Logged in, token length: ${user.accessToken!.length}`);
|
||||
|
||||
return user;
|
||||
|
|
@ -130,8 +99,8 @@ test.describe("Company Complete E2E with Admin Approval", () => {
|
|||
await registerUser(companyUser);
|
||||
|
||||
// ==================== BROWSER SETUP ====================
|
||||
const browser = await chromium.launch({ headless: false, slowMo: 30 });
|
||||
const context = await browser.newContext({ viewport: { width: 1400, height: 900 } });
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ baseURL: APP_BASE_URL, viewport: { width: 1400, height: 900 } });
|
||||
|
||||
// ==================== COMPANY FRONTEND FLOW ====================
|
||||
console.log("\n" + "=".repeat(60));
|
||||
|
|
@ -142,7 +111,7 @@ test.describe("Company Complete E2E with Admin Approval", () => {
|
|||
await setupFrontendAuth(companyPage, companyUser);
|
||||
|
||||
// Navigate to dashboard
|
||||
await companyPage.goto("http://localhost:3000/dashboard?role=COMPANY", { waitUntil: "networkidle", timeout: 15000 });
|
||||
await companyPage.goto("/dashboard?role=COMPANY", { waitUntil: "networkidle", timeout: 15000 });
|
||||
await new Promise(r => setTimeout(r, 3000));
|
||||
await takeScreenshot(companyPage, "01_company_dashboard");
|
||||
console.log(" ✅ Company dashboard loaded");
|
||||
|
|
@ -223,7 +192,7 @@ test.describe("Company Complete E2E with Admin Approval", () => {
|
|||
// Click submit for verification via API
|
||||
console.log(" ℹ️ Submit button enabled, submitting via API...");
|
||||
|
||||
const submitResponse = await fetch("http://localhost:9100/api/profile/submit-for-verification", {
|
||||
const submitResponse = await fetch(`${API_BASE}/profile/submit-for-verification`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
|
|
|||
|
|
@ -7,11 +7,12 @@
|
|||
*/
|
||||
import { test, expect, chromium, BrowserContext, Page } from "@playwright/test";
|
||||
import { randomUUID } from "crypto";
|
||||
import { execSync } from "child_process";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { apiRegister, apiVerifyEmail, apiLogin } from "./helpers/auth-flow";
|
||||
import { APP_BASE_URL, API_BASE } from "./helpers/env";
|
||||
|
||||
const SCREENSHOT_DIR = "/Users/ashwin/workspace/nxtgauge-frontend-solid/test-results/company-e2e-complete";
|
||||
const SCREENSHOT_DIR = path.join(__dirname, "..", "..", "test-results", "company-e2e-complete");
|
||||
|
||||
if (!fs.existsSync(SCREENSHOT_DIR)) {
|
||||
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
|
||||
|
|
@ -28,74 +29,30 @@ interface TestUser {
|
|||
companyName?: string;
|
||||
}
|
||||
|
||||
async function getOTPFromRedis(userId: string): Promise<string | null> {
|
||||
try {
|
||||
// Try to get OTP from Redis using multiple key patterns
|
||||
const plainKey = `otp:plain:${userId}`;
|
||||
let otpCode = execSync(`redis-cli GET "${plainKey}"`, { encoding: "utf8" }).trim();
|
||||
if (otpCode && otpCode.length >= 4) return otpCode;
|
||||
|
||||
// Try otp:code:* pattern
|
||||
const keys = execSync("redis-cli KEYS 'otp:code:*'", { encoding: "utf8" })
|
||||
.trim().split("\n").filter(Boolean);
|
||||
for (const k of keys) {
|
||||
const v = execSync(`redis-cli GET "${k}"`, { encoding: "utf8" }).trim();
|
||||
if (v === userId) {
|
||||
otpCode = k.replace("otp:code:", "");
|
||||
return otpCode;
|
||||
}
|
||||
}
|
||||
// Fallback: try any otp key matching the userId
|
||||
const allKeys = execSync("redis-cli KEYS 'otp:*'", { encoding: "utf8" })
|
||||
.trim().split("\n").filter(Boolean);
|
||||
for (const k of allKeys) {
|
||||
const v = execSync(`redis-cli GET "${k}"`, { encoding: "utf8" }).trim();
|
||||
if (v === userId) {
|
||||
return k.split(":").pop() || null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function registerUser(user: TestUser): Promise<TestUser> {
|
||||
console.log(`\n📝 Registering ${user.intent} via API...`);
|
||||
const regResponse = await fetch("http://localhost:9100/api/auth/register", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(user),
|
||||
const regData = await apiRegister({
|
||||
email: user.email,
|
||||
password: user.password,
|
||||
first_name: user.firstName,
|
||||
last_name: user.lastName,
|
||||
intent: user.intent,
|
||||
});
|
||||
const regData = await regResponse.json();
|
||||
if (!regData.user_id) throw new Error(`Registration failed: ${JSON.stringify(regData)}`);
|
||||
user.userId = regData.user_id;
|
||||
console.log(` ✅ Registered, user_id: ${user.userId}`);
|
||||
|
||||
// Wait for OTP to be generated
|
||||
// Wait for OTP to be generated, then verify via API
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
const otpCode = await getOTPFromRedis(user.userId!);
|
||||
if (!otpCode) throw new Error("Could not get OTP from Redis");
|
||||
console.log(` ✅ OTP retrieved: ${otpCode}`);
|
||||
|
||||
// Verify OTP
|
||||
const verifyResponse = await fetch("http://localhost:9100/api/auth/verify-email", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ user_id: user.userId, otp: otpCode })
|
||||
});
|
||||
if (!verifyResponse.ok) throw new Error("OTP verification failed");
|
||||
const verified = await apiVerifyEmail(user.email, user.userId!);
|
||||
if (!verified) throw new Error("OTP verification failed");
|
||||
console.log(` ✅ OTP verified!`);
|
||||
|
||||
// Login
|
||||
const loginResponse = await fetch("http://localhost:9100/api/auth/login", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email: user.email, password: user.password })
|
||||
});
|
||||
const loginData = await loginResponse.json();
|
||||
if (!loginData.access_token) throw new Error("Login failed");
|
||||
user.accessToken = loginData.access_token;
|
||||
const loginData = await apiLogin(user.email, user.password);
|
||||
const accessToken = loginData?.access_token || "";
|
||||
if (!accessToken) throw new Error("Login failed");
|
||||
user.accessToken = accessToken;
|
||||
console.log(` ✅ Logged in, token length: ${user.accessToken!.length}`);
|
||||
|
||||
return user;
|
||||
|
|
@ -149,8 +106,8 @@ test.describe("Company E2E Complete Flow", () => {
|
|||
await registerUser(companyUser);
|
||||
|
||||
// ==================== BROWSER SETUP ====================
|
||||
const browser = await chromium.launch({ headless: false, slowMo: 30 });
|
||||
const context = await browser.newContext({ viewport: { width: 1400, height: 900 } });
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ baseURL: APP_BASE_URL, viewport: { width: 1400, height: 900 } });
|
||||
|
||||
// ==================== DASHBOARD FLOW ====================
|
||||
console.log("\n" + "=".repeat(60));
|
||||
|
|
@ -161,7 +118,7 @@ test.describe("Company E2E Complete Flow", () => {
|
|||
await setupFrontendAuth(companyPage, companyUser);
|
||||
|
||||
// Navigate to dashboard
|
||||
await companyPage.goto("http://localhost:3000/dashboard?role=COMPANY", { waitUntil: "networkidle", timeout: 15000 });
|
||||
await companyPage.goto("/dashboard?role=COMPANY", { waitUntil: "networkidle", timeout: 15000 });
|
||||
await new Promise(r => setTimeout(r, 3000));
|
||||
await takeScreenshot(companyPage, "01_dashboard_loaded");
|
||||
console.log(" ✅ Company dashboard loaded");
|
||||
|
|
@ -287,7 +244,7 @@ test.describe("Company E2E Complete Flow", () => {
|
|||
|
||||
// Try to submit verification via API first to ensure it works
|
||||
console.log(" ℹ️ Attempting verification submission via API...");
|
||||
const submitResponse = await fetch("http://localhost:9100/api/profile/submit-for-verification", {
|
||||
const submitResponse = await fetch(`${API_BASE}/profile/submit-for-verification`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
|
@ -331,7 +288,7 @@ test.describe("Company E2E Complete Flow", () => {
|
|||
console.log(" ⚠️ Submit button still disabled - checking missing fields...");
|
||||
|
||||
// Check what fields are missing via API
|
||||
const profileRes = await fetch("http://localhost:9100/api/companies/profile/me", {
|
||||
const profileRes = await fetch(`${API_BASE}/companies/profile/me`, {
|
||||
headers: { "Authorization": `Bearer ${companyUser.accessToken}` }
|
||||
});
|
||||
if (profileRes.ok) {
|
||||
|
|
|
|||
|
|
@ -1,35 +1,16 @@
|
|||
import { test, expect, chromium } from "@playwright/test";
|
||||
import { randomUUID } from "crypto";
|
||||
import { execSync } from "child_process";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { apiRegister, apiVerifyEmail, apiLogin } from "./helpers/auth-flow";
|
||||
import { APP_BASE_URL } from "./helpers/env";
|
||||
|
||||
const SCREENSHOT_DIR = "/Users/ashwin/workspace/nxtgauge-frontend-solid/test-results/company-e2e";
|
||||
const SCREENSHOT_DIR = path.join(__dirname, "..", "..", "test-results", "company-e2e");
|
||||
|
||||
if (!fs.existsSync(SCREENSHOT_DIR)) {
|
||||
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
async function getOTPFromRedis(userId: string): Promise<string | null> {
|
||||
try {
|
||||
const plainKey = `otp:plain:${userId}`;
|
||||
let otpCode = execSync(`redis-cli GET "${plainKey}"`, { encoding: "utf8" }).trim();
|
||||
if (otpCode && otpCode.length >= 4) return otpCode;
|
||||
|
||||
const keys = execSync("redis-cli KEYS 'otp:code:*'", { encoding: "utf8" })
|
||||
.trim().split("\n").filter(Boolean);
|
||||
for (const k of keys) {
|
||||
const v = execSync(`redis-cli GET "${k}"`, { encoding: "utf8" }).trim();
|
||||
if (v === userId) {
|
||||
otpCode = k.replace("otp:code:", "");
|
||||
return otpCode;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
test.describe("Company E2E Full Flow", () => {
|
||||
test("complete company registration → OTP → login → dashboard → profile → verification", async () => {
|
||||
const testEmail = `e2ecompany${randomUUID().slice(0, 8)}@test.com`;
|
||||
|
|
@ -39,19 +20,14 @@ test.describe("Company E2E Full Flow", () => {
|
|||
console.log("📧 Email:", testEmail);
|
||||
console.log("🏢 Company:", testCompanyName);
|
||||
|
||||
const browser = await chromium.launch({ headless: false, slowMo: 30 });
|
||||
const context = await browser.newContext({ viewport: { width: 1400, height: 900 } });
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ baseURL: APP_BASE_URL, viewport: { width: 1400, height: 900 } });
|
||||
|
||||
// ==================== STEP 1: REGISTER VIA API ====================
|
||||
console.log("\n📝 STEP 1: Register via API");
|
||||
let regData: any;
|
||||
try {
|
||||
const regResponse = await fetch("http://localhost:9100/api/auth/register", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email: testEmail, first_name: "John", last_name: "Doe", password: testPassword, intent: "company" })
|
||||
});
|
||||
regData = await regResponse.json();
|
||||
regData = await apiRegister({ email: testEmail, first_name: "John", last_name: "Doe", password: testPassword, intent: "company" });
|
||||
expect(regData.user_id).toBeTruthy();
|
||||
console.log(" ✅ PASS: Registration successful, user_id:", regData.user_id);
|
||||
} catch (e: any) {
|
||||
|
|
@ -59,30 +35,13 @@ test.describe("Company E2E Full Flow", () => {
|
|||
throw e;
|
||||
}
|
||||
|
||||
// ==================== STEP 2: OTP VIA REDIS ====================
|
||||
console.log("\n🔐 STEP 2: OTP via Redis");
|
||||
// ==================== STEP 2+3: OTP VIA REDIS + VERIFY VIA API ====================
|
||||
console.log("\n🔐 STEP 2+3: OTP via Redis, verify via API");
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
let otpCode: string | null = null;
|
||||
try {
|
||||
otpCode = await getOTPFromRedis(regData.user_id);
|
||||
expect(otpCode).toBeTruthy();
|
||||
console.log(" ✅ PASS: OTP retrieved from Redis:", otpCode);
|
||||
} catch (e: any) {
|
||||
console.log(" ❌ FAIL: Could not get OTP -", e.message);
|
||||
throw e;
|
||||
}
|
||||
|
||||
// ==================== STEP 3: VERIFY OTP VIA API ====================
|
||||
console.log("\n✅ STEP 3: Verify OTP via API");
|
||||
try {
|
||||
const verifyResponse = await fetch("http://localhost:9100/api/auth/verify-email", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ user_id: regData.user_id, otp: otpCode })
|
||||
});
|
||||
const verifyData = await verifyResponse.json();
|
||||
expect(verifyResponse.ok).toBe(true);
|
||||
console.log(" ✅ PASS: OTP verified! Response:", JSON.stringify(verifyData));
|
||||
const verified = await apiVerifyEmail(testEmail, regData.user_id);
|
||||
expect(verified).toBe(true);
|
||||
console.log(" ✅ PASS: OTP retrieved and verified!");
|
||||
} catch (e: any) {
|
||||
console.log(" ❌ FAIL: OTP verification failed -", e.message);
|
||||
throw e;
|
||||
|
|
@ -92,13 +51,8 @@ test.describe("Company E2E Full Flow", () => {
|
|||
console.log("\n🔑 STEP 4: Login via API");
|
||||
let accessToken = "";
|
||||
try {
|
||||
const loginResponse = await fetch("http://localhost:9100/api/auth/login", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email: testEmail, password: testPassword })
|
||||
});
|
||||
const loginData = await loginResponse.json();
|
||||
accessToken = loginData.access_token || "";
|
||||
const loginData = await apiLogin(testEmail, testPassword);
|
||||
accessToken = loginData?.access_token || "";
|
||||
expect(accessToken).toBeTruthy();
|
||||
console.log(" ✅ PASS: Login successful, token length:", accessToken.length);
|
||||
} catch (e: any) {
|
||||
|
|
@ -125,7 +79,7 @@ test.describe("Company E2E Full Flow", () => {
|
|||
}, { token: accessToken, email: testEmail, userId: regData.user_id });
|
||||
|
||||
try {
|
||||
await page.goto("http://localhost:3000/dashboard?role=COMPANY", { waitUntil: "networkidle", timeout: 15000 });
|
||||
await page.goto("/dashboard?role=COMPANY", { waitUntil: "networkidle", timeout: 15000 });
|
||||
await page.waitForTimeout(3000);
|
||||
await page.screenshot({ path: `${SCREENSHOT_DIR}/step05_dashboard.png`, fullPage: true });
|
||||
console.log(" ✅ PASS: Dashboard loaded");
|
||||
|
|
|
|||
|
|
@ -1,17 +1,12 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import AxeBuilder from "@axe-core/playwright";
|
||||
import { apiLogin } from "./helpers/auth-flow";
|
||||
|
||||
async function setupAuth(page: any): Promise<boolean> {
|
||||
const res = await page.request.post("http://localhost:3000/api/auth/login", {
|
||||
data: {
|
||||
email: "testcompany@example.com",
|
||||
password: "TestPassword123!",
|
||||
},
|
||||
});
|
||||
if (!res.ok()) return false;
|
||||
const data = await res.json();
|
||||
const token = data.access_token;
|
||||
await page.goto("http://localhost:3000/dashboard");
|
||||
const loginData = await apiLogin("testcompany@example.com", "TestPassword123!");
|
||||
if (!loginData) return false;
|
||||
const token = loginData.access_token;
|
||||
await page.goto("/dashboard");
|
||||
await page.evaluate((t: string) => {
|
||||
window.sessionStorage.setItem("nxtgauge_access_token", t);
|
||||
window.sessionStorage.setItem("nxtgauge_frontend_access_token", t);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { test, expect, chromium } from "@playwright/test";
|
||||
import { randomUUID } from "crypto";
|
||||
import { APP_BASE_URL } from "./helpers/env";
|
||||
|
||||
// Generate random test data
|
||||
const testEmail = `testcompany${randomUUID().slice(0, 8)}@test.com`;
|
||||
|
|
@ -17,11 +18,11 @@ test.setTimeout(300000); // 5 minutes to allow manual CAPTCHA entry
|
|||
test("Company signup -> verification flow", async () => {
|
||||
// Launch browser with UI visible
|
||||
const browser = await chromium.launch({
|
||||
headless: false,
|
||||
slowMo: 200,
|
||||
headless: true,
|
||||
});
|
||||
|
||||
const context = await browser.newContext({
|
||||
baseURL: APP_BASE_URL,
|
||||
viewport: { width: 1400, height: 900 },
|
||||
recordVideo: {
|
||||
dir: "./test-videos/",
|
||||
|
|
@ -34,7 +35,7 @@ test("Company signup -> verification flow", async () => {
|
|||
try {
|
||||
// Step 1: Navigate to public website signup
|
||||
console.log("🌐 Step 1: Opening public website signup...");
|
||||
await page.goto("http://localhost:3001/signup?intent=company");
|
||||
await page.goto("/signup?intent=company");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await page.screenshot({ path: "./test-results/01-signup-page.png", fullPage: true });
|
||||
|
|
@ -126,7 +127,7 @@ test("Company signup -> verification flow", async () => {
|
|||
|
||||
// Should be redirected to login, or navigate there
|
||||
if (!page.url().includes("/login")) {
|
||||
await page.goto("http://localhost:3001/login");
|
||||
await page.goto("/login");
|
||||
await page.waitForLoadState("networkidle");
|
||||
}
|
||||
|
||||
|
|
@ -196,7 +197,7 @@ test("Company signup -> verification flow", async () => {
|
|||
// Step 7: Open Admin Panel
|
||||
console.log("🔐 Step 7: Opening admin panel...");
|
||||
const adminPage = await context.newPage();
|
||||
await adminPage.goto("http://localhost:3000/login");
|
||||
await adminPage.goto("/login");
|
||||
await adminPage.waitForLoadState("networkidle");
|
||||
|
||||
await adminPage.screenshot({ path: "./test-results/07-admin-login.png", fullPage: true });
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { test, expect, chromium, BrowserContext, Page } from "@playwright/test";
|
||||
import { randomUUID } from "crypto";
|
||||
import { execSync } from "child_process";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { apiRegister, apiVerifyEmail, apiLogin } from "./helpers/auth-flow";
|
||||
import { APP_BASE_URL } from "./helpers/env";
|
||||
|
||||
const SCREENSHOT_DIR = "/Users/ashwin/workspace/nxtgauge-frontend-solid/test-results/full-e2e";
|
||||
const SCREENSHOT_DIR = path.join(__dirname, "..", "..", "test-results", "full-e2e");
|
||||
|
||||
if (!fs.existsSync(SCREENSHOT_DIR)) {
|
||||
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
|
||||
|
|
@ -20,63 +22,30 @@ interface TestUser {
|
|||
companyName?: string;
|
||||
}
|
||||
|
||||
async function getOTPFromRedis(userId: string): Promise<string | null> {
|
||||
try {
|
||||
const plainKey = `otp:plain:${userId}`;
|
||||
let otpCode = execSync(`redis-cli GET "${plainKey}"`, { encoding: "utf8" }).trim();
|
||||
if (otpCode && otpCode.length >= 4) return otpCode;
|
||||
|
||||
const keys = execSync("redis-cli KEYS 'otp:code:*'", { encoding: "utf8" })
|
||||
.trim().split("\n").filter(Boolean);
|
||||
for (const k of keys) {
|
||||
const v = execSync(`redis-cli GET "${k}"`, { encoding: "utf8" }).trim();
|
||||
if (v === userId) {
|
||||
otpCode = k.replace("otp:code:", "");
|
||||
return otpCode;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function registerUser(user: TestUser): Promise<TestUser> {
|
||||
console.log(`\n📝 Registering ${user.intent} via API...`);
|
||||
const regResponse = await fetch("http://localhost:9100/api/auth/register", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(user),
|
||||
const regData = await apiRegister({
|
||||
email: user.email,
|
||||
password: user.password,
|
||||
first_name: user.firstName,
|
||||
last_name: user.lastName,
|
||||
intent: user.intent,
|
||||
});
|
||||
const regData = await regResponse.json();
|
||||
if (!regData.user_id) throw new Error(`Registration failed: ${JSON.stringify(regData)}`);
|
||||
user.userId = regData.user_id;
|
||||
console.log(` ✅ Registered, user_id: ${user.userId}`);
|
||||
|
||||
// Get OTP from Redis
|
||||
// Get OTP from Redis + verify via API
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
const otpCode = await getOTPFromRedis(user.userId!);
|
||||
if (!otpCode) throw new Error("Could not get OTP from Redis");
|
||||
console.log(` ✅ OTP retrieved: ${otpCode}`);
|
||||
|
||||
// Verify OTP
|
||||
const verifyResponse = await fetch("http://localhost:9100/api/auth/verify-email", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ user_id: user.userId, otp: otpCode })
|
||||
});
|
||||
if (!verifyResponse.ok) throw new Error("OTP verification failed");
|
||||
const verified = await apiVerifyEmail(user.email, user.userId!);
|
||||
if (!verified) throw new Error("OTP verification failed");
|
||||
console.log(` ✅ OTP verified!`);
|
||||
|
||||
// Login
|
||||
const loginResponse = await fetch("http://localhost:9100/api/auth/login", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email: user.email, password: user.password })
|
||||
});
|
||||
const loginData = await loginResponse.json();
|
||||
if (!loginData.access_token) throw new Error("Login failed");
|
||||
user.accessToken = loginData.access_token;
|
||||
const loginData = await apiLogin(user.email, user.password);
|
||||
const accessToken = loginData?.access_token || "";
|
||||
if (!accessToken) throw new Error("Login failed");
|
||||
user.accessToken = accessToken;
|
||||
console.log(` ✅ Logged in, token length: ${user.accessToken!.length}`);
|
||||
|
||||
return user;
|
||||
|
|
@ -139,8 +108,8 @@ test.describe("Full Company + Job Seeker E2E with Admin Verification", () => {
|
|||
await registerUser(jobSeekerUser);
|
||||
|
||||
// ==================== BROWSER SETUP ====================
|
||||
const browser = await chromium.launch({ headless: false, slowMo: 30 });
|
||||
const context = await browser.newContext({ viewport: { width: 1400, height: 900 } });
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ baseURL: APP_BASE_URL, viewport: { width: 1400, height: 900 } });
|
||||
|
||||
// ==================== COMPANY FRONTEND FLOW ====================
|
||||
console.log("\n" + "=".repeat(60));
|
||||
|
|
@ -150,7 +119,7 @@ test.describe("Full Company + Job Seeker E2E with Admin Verification", () => {
|
|||
const companyPage = await context.newPage();
|
||||
await setupFrontendAuth(companyPage, companyUser);
|
||||
|
||||
await companyPage.goto("http://localhost:3000/dashboard?role=COMPANY", { waitUntil: "networkidle", timeout: 15000 });
|
||||
await companyPage.goto("/dashboard?role=COMPANY", { waitUntil: "networkidle", timeout: 15000 });
|
||||
await new Promise(r => setTimeout(r, 3000));
|
||||
await takeScreenshot(companyPage, "01_company_dashboard");
|
||||
console.log(" ✅ Company dashboard loaded");
|
||||
|
|
@ -204,7 +173,7 @@ test.describe("Full Company + Job Seeker E2E with Admin Verification", () => {
|
|||
const jsPage = await context.newPage();
|
||||
await setupFrontendAuth(jsPage, jobSeekerUser);
|
||||
|
||||
await jsPage.goto("http://localhost:3000/dashboard?role=JOB_SEEKER", { waitUntil: "networkidle", timeout: 15000 });
|
||||
await jsPage.goto("/dashboard?role=JOB_SEEKER", { waitUntil: "networkidle", timeout: 15000 });
|
||||
await new Promise(r => setTimeout(r, 3000));
|
||||
await takeScreenshot(jsPage, "04_jobseeker_dashboard");
|
||||
console.log(" ✅ Job seeker dashboard loaded");
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { test, expect, request } from "@playwright/test";
|
||||
|
||||
const API_BASE = "http://localhost:3000/api";
|
||||
import { apiLogin } from "./helpers/auth-flow";
|
||||
import { API_BASE } from "./helpers/env";
|
||||
|
||||
const PHONE_PATTERNS = [
|
||||
/\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/,
|
||||
|
|
@ -33,29 +33,13 @@ function hasContactInfo(text: string): boolean {
|
|||
}
|
||||
|
||||
async function getCompanyToken(): Promise<string | null> {
|
||||
const ctx = await request.newContext();
|
||||
const res = await ctx.post(`${API_BASE}/auth/login`, {
|
||||
data: {
|
||||
email: "testcompany@example.com",
|
||||
password: "TestPassword123!",
|
||||
},
|
||||
});
|
||||
if (!res.ok()) return null;
|
||||
const data = await res.json();
|
||||
return data.access_token || null;
|
||||
const loginData = await apiLogin("testcompany@example.com", "TestPassword123!");
|
||||
return loginData?.access_token || null;
|
||||
}
|
||||
|
||||
async function getJobSeekerToken(): Promise<string | null> {
|
||||
const ctx = await request.newContext();
|
||||
const res = await ctx.post(`${API_BASE}/auth/login`, {
|
||||
data: {
|
||||
email: "testtutora2026@example.com",
|
||||
password: "Test1234!",
|
||||
},
|
||||
});
|
||||
if (!res.ok()) return null;
|
||||
const data = await res.json();
|
||||
return data.access_token || null;
|
||||
const loginData = await apiLogin("testtutora2026@example.com", "Test1234!");
|
||||
return loginData?.access_token || null;
|
||||
}
|
||||
|
||||
test.describe("Guard Rails - AI Content Safety", () => {
|
||||
|
|
|
|||
65
tests/e2e/helpers/auth-flow.ts
Normal file
65
tests/e2e/helpers/auth-flow.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { API_BASE } from "./env";
|
||||
import { solveCaptcha } from "./captcha";
|
||||
import { getOtpFromRedis } from "./otp";
|
||||
|
||||
export interface RegisterInput {
|
||||
email: string;
|
||||
password: string;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
phone?: string;
|
||||
intent?: string;
|
||||
profession?: string;
|
||||
}
|
||||
|
||||
/** POST /api/auth/register (captcha-solved automatically). */
|
||||
export async function apiRegister(input: RegisterInput): Promise<{ user_id: string; [k: string]: any }> {
|
||||
const { captcha_id, captcha_answer } = await solveCaptcha();
|
||||
const res = await fetch(`${API_BASE}/auth/register`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ ...input, captcha_id, captcha_answer }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
throw new Error(`Register failed (${res.status}): ${JSON.stringify(data)}`);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/** POST /api/auth/verify-email, sourcing the OTP straight from Redis. */
|
||||
export async function apiVerifyEmail(email: string, userId: string): Promise<boolean> {
|
||||
const otp = await getOtpFromRedis(userId);
|
||||
if (!otp) {
|
||||
console.log(`⚠️ Could not retrieve OTP from Redis for user ${userId}`);
|
||||
return false;
|
||||
}
|
||||
const res = await fetch(`${API_BASE}/auth/verify-email`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ otp, email }),
|
||||
});
|
||||
return res.ok;
|
||||
}
|
||||
|
||||
/** POST /api/auth/login (captcha-solved automatically). Returns null on non-2xx. */
|
||||
export async function apiLogin(email: string, password: string): Promise<{ access_token: string; [k: string]: any } | null> {
|
||||
const { captcha_id, captcha_answer } = await solveCaptcha();
|
||||
const res = await fetch(`${API_BASE}/auth/login`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email, password, captcha_id, captcha_answer }),
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/** Full register → verify → login convenience flow, returning the access token. */
|
||||
export async function registerVerifyLogin(input: RegisterInput): Promise<{ user_id: string; access_token: string }> {
|
||||
const regData = await apiRegister(input);
|
||||
const verified = await apiVerifyEmail(input.email, regData.user_id);
|
||||
if (!verified) throw new Error(`Email verification failed for ${input.email}`);
|
||||
const loginData = await apiLogin(input.email, input.password);
|
||||
if (!loginData) throw new Error(`Login failed for ${input.email} after verification`);
|
||||
return { user_id: regData.user_id, access_token: loginData.access_token };
|
||||
}
|
||||
28
tests/e2e/helpers/captcha.ts
Normal file
28
tests/e2e/helpers/captcha.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { API_BASE } from "./env";
|
||||
|
||||
/**
|
||||
* Fetches a fresh math captcha from the API and solves it.
|
||||
* Captchas are single-use (Redis-backed) — call this immediately before the
|
||||
* request that needs it, never cache/reuse a solved captcha_id.
|
||||
*/
|
||||
export async function solveCaptcha(): Promise<{ captcha_id: string; captcha_answer: string }> {
|
||||
const res = await fetch(`${API_BASE}/auth/captcha`, { method: "POST" });
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to fetch captcha: ${res.status} ${await res.text()}`);
|
||||
}
|
||||
const { captcha_id, challenge } = await res.json();
|
||||
|
||||
// Challenge format observed from the API: "N + N = ?" / "N - N = ?" (also
|
||||
// tolerate "*" just in case). Keep this in sync with
|
||||
// apps/users/src/handlers/auth.rs's captcha generator.
|
||||
const match = String(challenge).match(/(-?\d+)\s*([+\-*])\s*(-?\d+)/);
|
||||
if (!match) {
|
||||
throw new Error(`Unrecognized captcha challenge format: "${challenge}"`);
|
||||
}
|
||||
const [, aStr, op, bStr] = match;
|
||||
const a = Number(aStr);
|
||||
const b = Number(bStr);
|
||||
const answer = op === "+" ? a + b : op === "-" ? a - b : a * b;
|
||||
|
||||
return { captcha_id, captcha_answer: String(answer) };
|
||||
}
|
||||
11
tests/e2e/helpers/env.ts
Normal file
11
tests/e2e/helpers/env.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* Single source of truth for which environment the e2e suite targets.
|
||||
* Mirrors the same TEST_ENV=production condition used in playwright.config.ts's
|
||||
* `use.baseURL` — keep these in sync.
|
||||
*/
|
||||
export const APP_BASE_URL =
|
||||
process.env.TEST_ENV === "production"
|
||||
? "https://test111.nxtgauge.com"
|
||||
: "http://localhost:3000";
|
||||
|
||||
export const API_BASE = `${APP_BASE_URL}/api`;
|
||||
42
tests/e2e/helpers/otp.ts
Normal file
42
tests/e2e/helpers/otp.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { execFileSync } from "child_process";
|
||||
|
||||
/**
|
||||
* Reads a registration OTP straight out of Redis, the same way a human would
|
||||
* never get to (the API deliberately never returns it — see
|
||||
* apps/users/src/handlers/auth.rs's RegisterResponse.otp, which is only
|
||||
* populated for the DEMO_ACCOUNT_EMAILS allowlist, unset in this environment).
|
||||
*
|
||||
* This only works when the machine running the tests has `kubectl` access to
|
||||
* the cluster (redis-master-0 in the `data` namespace) — i.e. run from the
|
||||
* same host used for the rest of the live-server runbook, not arbitrary CI.
|
||||
* Auths via the redis pod's own mounted password file rather than a secret
|
||||
* value passed on our end, matching how docs/LIVE_SERVER_RUNBOOK.md's manual
|
||||
* verification steps do it.
|
||||
*/
|
||||
export async function getOtpFromRedis(userId: string): Promise<string | null> {
|
||||
try {
|
||||
const out = execFileSync(
|
||||
"kubectl",
|
||||
[
|
||||
"-n",
|
||||
"data",
|
||||
"exec",
|
||||
"redis-master-0",
|
||||
"--",
|
||||
"sh",
|
||||
"-c",
|
||||
'redis-cli -a "$(cat /opt/bitnami/redis/secrets/redis-password)" GET "otp:plain:$1"',
|
||||
"--", // end of sh -c's own options, "$1" below binds to this
|
||||
userId,
|
||||
],
|
||||
{ encoding: "utf8" }
|
||||
).trim();
|
||||
|
||||
// redis-cli prints "(nil)" for a missing key rather than exiting non-zero.
|
||||
if (!out || out === "(nil)" || !/^\d{4,}$/.test(out)) return null;
|
||||
return out;
|
||||
} catch (e: any) {
|
||||
console.log("⚠️ Could not read OTP from Redis via kubectl:", e.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +1,9 @@
|
|||
import { test, expect, chromium } from "@playwright/test";
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { randomUUID } from "crypto";
|
||||
import { execSync } from "child_process";
|
||||
import * as path from "path";
|
||||
import { apiRegister, apiVerifyEmail, apiLogin } from "./helpers/auth-flow";
|
||||
|
||||
const SCREENSHOT_DIR = "/Users/ashwin/workspace/nxtgauge-frontend-solid/test-results/job-seeker-complete";
|
||||
|
||||
async function getOTPFromRedis(userId: string): Promise<string | null> {
|
||||
try {
|
||||
const plainKey = `otp:plain:${userId}`;
|
||||
let otpCode = execSync(`redis-cli GET "${plainKey}"`, { encoding: "utf8" }).trim();
|
||||
if (otpCode && otpCode.length >= 4) return otpCode;
|
||||
|
||||
const keys = execSync("redis-cli KEYS 'otp:code:*'", { encoding: "utf8" })
|
||||
.trim().split("\n").filter(Boolean);
|
||||
for (const k of keys) {
|
||||
const v = execSync(`redis-cli GET "${k}"`, { encoding: "utf8" }).trim();
|
||||
if (v === userId) {
|
||||
otpCode = k.replace("otp:code:", "");
|
||||
return otpCode;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const SCREENSHOT_DIR = path.join(__dirname, "..", "..", "test-results", "job-seeker-complete");
|
||||
|
||||
test.describe("Job Seeker E2E Complete Flow", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
|
|
@ -40,95 +20,74 @@ test.describe("Job Seeker E2E Complete Flow", () => {
|
|||
|
||||
// ==================== STEP 1: REGISTER VIA API ====================
|
||||
console.log("\n📝 STEP 1: Register via API");
|
||||
let regData: any;
|
||||
const regResponse = await fetch("http://localhost:9100/api/auth/register", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
email: testEmail,
|
||||
first_name: "Jane",
|
||||
last_name: "Smith",
|
||||
password: testPassword,
|
||||
intent: "job_seeker"
|
||||
})
|
||||
const regData = await apiRegister({
|
||||
email: testEmail,
|
||||
first_name: "Jane",
|
||||
last_name: "Smith",
|
||||
password: testPassword,
|
||||
intent: "job_seeker"
|
||||
});
|
||||
regData = await regResponse.json();
|
||||
expect(regData.user_id).toBeTruthy();
|
||||
console.log(" ✅ Registration successful, user_id:", regData.user_id);
|
||||
|
||||
// ==================== STEP 2: OTP VIA REDIS ====================
|
||||
console.log("\n🔐 STEP 2: Get OTP via Redis");
|
||||
// ==================== STEP 2+3: OTP VIA REDIS + VERIFY VIA API ====================
|
||||
console.log("\n🔐 STEP 2+3: Get OTP via Redis and verify via API");
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
let otpCode = await getOTPFromRedis(regData.user_id);
|
||||
expect(otpCode).toBeTruthy();
|
||||
console.log(" ✅ OTP retrieved:", otpCode);
|
||||
|
||||
// ==================== STEP 3: VERIFY OTP VIA API ====================
|
||||
console.log("\n✅ STEP 3: Verify OTP via API");
|
||||
const verifyResponse = await fetch("http://localhost:9100/api/auth/verify-email", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ user_id: regData.user_id, otp: otpCode })
|
||||
});
|
||||
expect(verifyResponse.ok).toBe(true);
|
||||
const verified = await apiVerifyEmail(testEmail, regData.user_id);
|
||||
expect(verified).toBe(true);
|
||||
console.log(" ✅ OTP verified!");
|
||||
|
||||
// ==================== STEP 4: LOGIN VIA API ====================
|
||||
console.log("\n🔑 STEP 4: Login via API");
|
||||
const loginResponse = await fetch("http://localhost:9100/api/auth/login", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email: testEmail, password: testPassword })
|
||||
});
|
||||
const loginData = await loginResponse.json();
|
||||
const accessToken = loginData.access_token;
|
||||
const loginData = await apiLogin(testEmail, testPassword);
|
||||
const accessToken = loginData?.access_token || "";
|
||||
expect(accessToken).toBeTruthy();
|
||||
console.log(" ✅ Login successful, token length:", accessToken.length);
|
||||
|
||||
// ==================== STEP 5: DASHBOARD ====================
|
||||
console.log("\n🌐 STEP 5: Navigate to dashboard");
|
||||
|
||||
|
||||
// Seed sessionStorage and localStorage with auth data (auth.tsx uses sessionStorage for token)
|
||||
await page.addInitScript(({ token, email, userId }) => {
|
||||
// auth.tsx getToken() reads from sessionStorage
|
||||
sessionStorage.setItem("nxtgauge_access_token", token);
|
||||
|
||||
|
||||
// localStorage for user data (used by various components)
|
||||
localStorage.setItem("nxtgauge_user", JSON.stringify({
|
||||
email,
|
||||
roleKey: "JOB_SEEKER",
|
||||
role: "JOB_SEEKER",
|
||||
email,
|
||||
roleKey: "JOB_SEEKER",
|
||||
role: "JOB_SEEKER",
|
||||
active_role: "JOB_SEEKER",
|
||||
selectedProfessionalRole: "JOB_SEEKER",
|
||||
name: "Jane Smith",
|
||||
fullName: "Jane Smith",
|
||||
selectedProfessionalRole: "JOB_SEEKER",
|
||||
name: "Jane Smith",
|
||||
fullName: "Jane Smith",
|
||||
id: userId
|
||||
}));
|
||||
localStorage.setItem("nxtgauge_auth_user", JSON.stringify({
|
||||
email,
|
||||
roleKey: "JOB_SEEKER",
|
||||
role: "JOB_SEEKER",
|
||||
email,
|
||||
roleKey: "JOB_SEEKER",
|
||||
role: "JOB_SEEKER",
|
||||
active_role: "JOB_SEEKER",
|
||||
selectedProfessionalRole: "JOB_SEEKER",
|
||||
name: "Jane Smith",
|
||||
fullName: "Jane Smith",
|
||||
selectedProfessionalRole: "JOB_SEEKER",
|
||||
name: "Jane Smith",
|
||||
fullName: "Jane Smith",
|
||||
id: userId
|
||||
}));
|
||||
localStorage.setItem("nxtgauge_signup_profile_v1", JSON.stringify({
|
||||
email,
|
||||
roleKey: "JOB_SEEKER",
|
||||
role: "JOB_SEEKER",
|
||||
email,
|
||||
roleKey: "JOB_SEEKER",
|
||||
role: "JOB_SEEKER",
|
||||
active_role: "JOB_SEEKER",
|
||||
selectedProfessionalRole: "JOB_SEEKER",
|
||||
name: "Jane Smith",
|
||||
fullName: "Jane Smith",
|
||||
selectedProfessionalRole: "JOB_SEEKER",
|
||||
name: "Jane Smith",
|
||||
fullName: "Jane Smith",
|
||||
id: userId
|
||||
}));
|
||||
}, { token: accessToken, email: testEmail, userId: regData.user_id });
|
||||
|
||||
await page.goto("http://localhost:3000/dashboard?role=JOB_SEEKER", { waitUntil: "networkidle", timeout: 15000 });
|
||||
await page.goto("/dashboard?role=JOB_SEEKER", { waitUntil: "networkidle", timeout: 15000 });
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
|
||||
// Check dashboard loaded - URL should not redirect to login
|
||||
const currentUrl = page.url();
|
||||
expect(currentUrl).not.toContain("/login");
|
||||
|
|
@ -136,21 +95,21 @@ test.describe("Job Seeker E2E Complete Flow", () => {
|
|||
|
||||
// ==================== STEP 6: PROFILE FORM ====================
|
||||
console.log("\n📋 STEP 6: Navigate to profile");
|
||||
|
||||
|
||||
// Click My Profile button
|
||||
const profileBtn = page.getByRole("button", { name: /my profile/i });
|
||||
if (await profileBtn.isVisible().catch(() => false)) {
|
||||
await profileBtn.click();
|
||||
await page.waitForTimeout(2000);
|
||||
} else {
|
||||
await page.goto("http://localhost:3000/dashboard/profile?role=JOB_SEEKER", { waitUntil: "networkidle" });
|
||||
await page.goto("/dashboard/profile?role=JOB_SEEKER", { waitUntil: "networkidle" });
|
||||
await page.waitForTimeout(2000);
|
||||
}
|
||||
console.log(" ✅ Profile page displayed");
|
||||
|
||||
// ==================== STEP 6b: FILL PROFILE FORM ====================
|
||||
console.log("\n📝 STEP 6b: Fill job seeker profile");
|
||||
|
||||
|
||||
// Fill basic fields using label selectors since inputs have no name/id
|
||||
const fieldMappings: Record<string, string> = {
|
||||
"First Name": "Jane",
|
||||
|
|
@ -187,7 +146,7 @@ test.describe("Job Seeker E2E Complete Flow", () => {
|
|||
|
||||
// ==================== STEP 7: DOCUMENTS TAB ====================
|
||||
console.log("\n📄 STEP 7: Upload documents");
|
||||
|
||||
|
||||
// Switch to Documents tab
|
||||
const docsTab = page.getByRole("button", { name: /documents/i });
|
||||
if (await docsTab.isVisible().catch(() => false)) {
|
||||
|
|
@ -201,15 +160,15 @@ test.describe("Job Seeker E2E Complete Flow", () => {
|
|||
|
||||
// ==================== STEP 8: SUBMIT FOR VERIFICATION ====================
|
||||
console.log("\n📤 STEP 8: Submit for verification");
|
||||
|
||||
|
||||
const submitBtn = page.getByRole("button", { name: /submit for verification/i });
|
||||
|
||||
|
||||
if (await submitBtn.isVisible({ timeout: 2000 }).catch(() => false)) {
|
||||
const isDisabled = await submitBtn.isDisabled().catch(() => true);
|
||||
|
||||
|
||||
if (isDisabled) {
|
||||
console.log(" ⚠️ Submit button disabled - checking what's missing");
|
||||
|
||||
|
||||
// Check for missing fields message
|
||||
const bodyText = await page.locator("body").innerText();
|
||||
if (bodyText.includes("required") || bodyText.includes("missing")) {
|
||||
|
|
@ -218,7 +177,7 @@ test.describe("Job Seeker E2E Complete Flow", () => {
|
|||
} else {
|
||||
await submitBtn.click();
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
|
||||
// Check for success message
|
||||
const bodyText = await page.locator("body").innerText();
|
||||
if (bodyText.includes("Submitted") || bodyText.includes("success")) {
|
||||
|
|
|
|||
|
|
@ -1,35 +1,16 @@
|
|||
import { test, expect, chromium } from "@playwright/test";
|
||||
import { randomUUID } from "crypto";
|
||||
import { execSync } from "child_process";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { apiRegister, apiVerifyEmail, apiLogin } from "./helpers/auth-flow";
|
||||
import { APP_BASE_URL } from "./helpers/env";
|
||||
|
||||
const SCREENSHOT_DIR = "/Users/ashwin/workspace/nxtgauge-frontend-solid/test-results/job-seeker-e2e";
|
||||
const SCREENSHOT_DIR = path.join(__dirname, "..", "..", "test-results", "job-seeker-e2e");
|
||||
|
||||
if (!fs.existsSync(SCREENSHOT_DIR)) {
|
||||
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
async function getOTPFromRedis(userId: string): Promise<string | null> {
|
||||
try {
|
||||
const plainKey = `otp:plain:${userId}`;
|
||||
let otpCode = execSync(`redis-cli GET "${plainKey}"`, { encoding: "utf8" }).trim();
|
||||
if (otpCode && otpCode.length >= 4) return otpCode;
|
||||
|
||||
const keys = execSync("redis-cli KEYS 'otp:code:*'", { encoding: "utf8" })
|
||||
.trim().split("\n").filter(Boolean);
|
||||
for (const k of keys) {
|
||||
const v = execSync(`redis-cli GET "${k}"`, { encoding: "utf8" }).trim();
|
||||
if (v === userId) {
|
||||
otpCode = k.replace("otp:code:", "");
|
||||
return otpCode;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
test.describe("Job Seeker E2E Full Flow", () => {
|
||||
test("complete job seeker registration → OTP → login → dashboard → profile → verification", async () => {
|
||||
const testEmail = `e2ejobseeker${randomUUID().slice(0, 8)}@test.com`;
|
||||
|
|
@ -37,19 +18,14 @@ test.describe("Job Seeker E2E Full Flow", () => {
|
|||
|
||||
console.log("📧 Email:", testEmail);
|
||||
|
||||
const browser = await chromium.launch({ headless: false, slowMo: 30 });
|
||||
const context = await browser.newContext({ viewport: { width: 1400, height: 900 } });
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ baseURL: APP_BASE_URL, viewport: { width: 1400, height: 900 } });
|
||||
|
||||
// ==================== STEP 1: REGISTER VIA API ====================
|
||||
console.log("\n📝 STEP 1: Register via API");
|
||||
let regData: any;
|
||||
try {
|
||||
const regResponse = await fetch("http://localhost:9100/api/auth/register", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email: testEmail, first_name: "Jane", last_name: "Smith", password: testPassword, intent: "job_seeker" })
|
||||
});
|
||||
regData = await regResponse.json();
|
||||
regData = await apiRegister({ email: testEmail, first_name: "Jane", last_name: "Smith", password: testPassword, intent: "job_seeker" });
|
||||
expect(regData.user_id).toBeTruthy();
|
||||
console.log(" ✅ PASS: Registration successful, user_id:", regData.user_id);
|
||||
} catch (e: any) {
|
||||
|
|
@ -57,30 +33,13 @@ test.describe("Job Seeker E2E Full Flow", () => {
|
|||
throw e;
|
||||
}
|
||||
|
||||
// ==================== STEP 2: OTP VIA REDIS ====================
|
||||
console.log("\n🔐 STEP 2: OTP via Redis");
|
||||
// ==================== STEP 2+3: OTP VIA REDIS + VERIFY VIA API ====================
|
||||
console.log("\n🔐 STEP 2+3: OTP via Redis, verify via API");
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
let otpCode: string | null = null;
|
||||
try {
|
||||
otpCode = await getOTPFromRedis(regData.user_id);
|
||||
expect(otpCode).toBeTruthy();
|
||||
console.log(" ✅ PASS: OTP retrieved from Redis:", otpCode);
|
||||
} catch (e: any) {
|
||||
console.log(" ❌ FAIL: Could not get OTP -", e.message);
|
||||
throw e;
|
||||
}
|
||||
|
||||
// ==================== STEP 3: VERIFY OTP VIA API ====================
|
||||
console.log("\n✅ STEP 3: Verify OTP via API");
|
||||
try {
|
||||
const verifyResponse = await fetch("http://localhost:9100/api/auth/verify-email", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ user_id: regData.user_id, otp: otpCode })
|
||||
});
|
||||
const verifyData = await verifyResponse.json();
|
||||
expect(verifyResponse.ok).toBe(true);
|
||||
console.log(" ✅ PASS: OTP verified! Response:", JSON.stringify(verifyData));
|
||||
const verified = await apiVerifyEmail(testEmail, regData.user_id);
|
||||
expect(verified).toBe(true);
|
||||
console.log(" ✅ PASS: OTP retrieved and verified!");
|
||||
} catch (e: any) {
|
||||
console.log(" ❌ FAIL: OTP verification failed -", e.message);
|
||||
throw e;
|
||||
|
|
@ -90,13 +49,8 @@ test.describe("Job Seeker E2E Full Flow", () => {
|
|||
console.log("\n🔑 STEP 4: Login via API");
|
||||
let accessToken = "";
|
||||
try {
|
||||
const loginResponse = await fetch("http://localhost:9100/api/auth/login", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email: testEmail, password: testPassword })
|
||||
});
|
||||
const loginData = await loginResponse.json();
|
||||
accessToken = loginData.access_token || "";
|
||||
const loginData = await apiLogin(testEmail, testPassword);
|
||||
accessToken = loginData?.access_token || "";
|
||||
expect(accessToken).toBeTruthy();
|
||||
console.log(" ✅ PASS: Login successful, token length:", accessToken.length);
|
||||
} catch (e: any) {
|
||||
|
|
@ -107,7 +61,7 @@ test.describe("Job Seeker E2E Full Flow", () => {
|
|||
// ==================== STEP 5: DASHBOARD ====================
|
||||
console.log("\n🌐 STEP 5: Navigate to dashboard?role=JOB_SEEKER");
|
||||
const page = await context.newPage();
|
||||
|
||||
|
||||
await page.addInitScript(({ token, email, userId }) => {
|
||||
localStorage.setItem("nxtgauge_access_token", token);
|
||||
localStorage.setItem("nxtgauge_user", JSON.stringify({
|
||||
|
|
@ -122,7 +76,7 @@ test.describe("Job Seeker E2E Full Flow", () => {
|
|||
}, { token: accessToken, email: testEmail, userId: regData.user_id });
|
||||
|
||||
try {
|
||||
await page.goto("http://localhost:3000/dashboard?role=JOB_SEEKER", { waitUntil: "networkidle", timeout: 15000 });
|
||||
await page.goto("/dashboard?role=JOB_SEEKER", { waitUntil: "networkidle", timeout: 15000 });
|
||||
await page.waitForTimeout(3000);
|
||||
await page.screenshot({ path: `${SCREENSHOT_DIR}/step05_dashboard.png`, fullPage: true });
|
||||
console.log(" ✅ PASS: Dashboard loaded");
|
||||
|
|
@ -142,7 +96,7 @@ test.describe("Job Seeker E2E Full Flow", () => {
|
|||
await page.waitForTimeout(3000);
|
||||
} else {
|
||||
console.log(" ⚠️ My Profile button not visible, trying direct navigation");
|
||||
await page.goto("http://localhost:3000/dashboard/profile?role=JOB_SEEKER", { waitUntil: "networkidle" });
|
||||
await page.goto("/dashboard/profile?role=JOB_SEEKER", { waitUntil: "networkidle" });
|
||||
await page.waitForTimeout(3000);
|
||||
}
|
||||
await page.screenshot({ path: `${SCREENSHOT_DIR}/step06_profile_form.png`, fullPage: true });
|
||||
|
|
@ -158,7 +112,7 @@ test.describe("Job Seeker E2E Full Flow", () => {
|
|||
const inputs = page.locator("input");
|
||||
const count = await inputs.count();
|
||||
console.log(" Total inputs found:", count);
|
||||
|
||||
|
||||
// Log all inputs with their attributes
|
||||
for (let i = 0; i < Math.min(count, 15); i++) {
|
||||
const input = inputs.nth(i);
|
||||
|
|
@ -172,30 +126,30 @@ test.describe("Job Seeker E2E Full Flow", () => {
|
|||
console.log(` Input ${i}: type="${type}" placeholder="${placeholder}" id="${id}" name="${name}"`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Try to find and fill common job seeker fields
|
||||
const nameFields = ["first_name", "firstName", "First Name", "first-name"];
|
||||
const lastNameFields = ["last_name", "lastName", "Last Name", "last-name"];
|
||||
const emailField = page.locator('input[name="email"], input[id="email"]').first();
|
||||
const phoneField = page.locator('input[name="phone"], input[id="phone"], input[placeholder*="phone" i]').first();
|
||||
const locationField = page.locator('input[name="location"], input[id="location"], input[placeholder*="location" i]').first();
|
||||
|
||||
|
||||
if (await emailField.isVisible().catch(() => false)) {
|
||||
console.log(" ℹ️ Email field already has:", await emailField.inputValue().catch(() => ""));
|
||||
}
|
||||
|
||||
|
||||
// Fill phone
|
||||
if (await phoneField.isVisible().catch(() => false)) {
|
||||
await phoneField.fill("9876543210");
|
||||
console.log(" ✅ Filled phone");
|
||||
}
|
||||
|
||||
|
||||
// Fill location
|
||||
if (await locationField.isVisible().catch(() => false)) {
|
||||
await locationField.fill("Chennai");
|
||||
console.log(" ✅ Filled location");
|
||||
}
|
||||
|
||||
|
||||
// Try textareas too
|
||||
const textareas = page.locator("textarea");
|
||||
const textareaCount = await textareas.count();
|
||||
|
|
@ -209,7 +163,7 @@ test.describe("Job Seeker E2E Full Flow", () => {
|
|||
console.log(` Textarea ${i}: placeholder="${placeholder}" name="${name}"`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
await page.screenshot({ path: `${SCREENSHOT_DIR}/step06b_profile_inputs.png`, fullPage: true });
|
||||
console.log(" ✅ Profile form analyzed");
|
||||
} catch (e: any) {
|
||||
|
|
@ -222,13 +176,13 @@ test.describe("Job Seeker E2E Full Flow", () => {
|
|||
try {
|
||||
const submitBtn = page.getByRole("button", { name: /submit for verification/i });
|
||||
const btnVisible = await submitBtn.isVisible().catch(() => false);
|
||||
|
||||
|
||||
if (btnVisible) {
|
||||
const isDisabled = await submitBtn.isDisabled().catch(() => true);
|
||||
if (isDisabled) {
|
||||
console.log(" ⚠️ INFO: Submit button disabled - profile needs more fields filled");
|
||||
await page.screenshot({ path: `${SCREENSHOT_DIR}/step07_submit_disabled.png`, fullPage: true });
|
||||
|
||||
|
||||
// Try Documents tab
|
||||
const docsTab = page.getByRole("tab", { name: /documents/i }).first();
|
||||
if (await docsTab.isVisible().catch(() => false)) {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,18 @@
|
|||
import { test, expect, request } from "@playwright/test";
|
||||
import { solveCaptcha } from "./helpers/captcha";
|
||||
|
||||
const API_BASE = "https://test111.nxtgauge.com/api";
|
||||
|
||||
test.describe("Security - Authentication", () => {
|
||||
test("JWT token is not returned for invalid credentials", async () => {
|
||||
const ctx = await request.newContext();
|
||||
const { captcha_id, captcha_answer } = await solveCaptcha();
|
||||
const res = await ctx.post(`${API_BASE}/auth/login`, {
|
||||
data: {
|
||||
email: "nonexistent@example.com",
|
||||
password: "wrongpassword",
|
||||
captcha_id,
|
||||
captcha_answer,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -19,9 +23,12 @@ test.describe("Security - Authentication", () => {
|
|||
|
||||
test("Login without email returns proper error", async () => {
|
||||
const ctx = await request.newContext();
|
||||
const { captcha_id, captcha_answer } = await solveCaptcha();
|
||||
const res = await ctx.post(`${API_BASE}/auth/login`, {
|
||||
data: {
|
||||
password: "somepassword",
|
||||
captcha_id,
|
||||
captcha_answer,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -30,9 +37,12 @@ test.describe("Security - Authentication", () => {
|
|||
|
||||
test("Login without password returns proper error", async () => {
|
||||
const ctx = await request.newContext();
|
||||
const { captcha_id, captcha_answer } = await solveCaptcha();
|
||||
const res = await ctx.post(`${API_BASE}/auth/login`, {
|
||||
data: {
|
||||
email: "test@example.com",
|
||||
captcha_id,
|
||||
captcha_answer,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -76,10 +86,13 @@ test.describe("Security - Rate Limiting", () => {
|
|||
|
||||
let rateLimited = false;
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const { captcha_id, captcha_answer } = await solveCaptcha();
|
||||
const res = await ctx.post(`${API_BASE}/auth/login`, {
|
||||
data: {
|
||||
email: uniqueEmail,
|
||||
password: "wrongpassword",
|
||||
captcha_id,
|
||||
captcha_answer,
|
||||
},
|
||||
});
|
||||
if (res.status() === 429) {
|
||||
|
|
@ -89,10 +102,13 @@ test.describe("Security - Rate Limiting", () => {
|
|||
}
|
||||
|
||||
if (!rateLimited) {
|
||||
const { captcha_id, captcha_answer } = await solveCaptcha();
|
||||
const res = await ctx.post(`${API_BASE}/auth/login`, {
|
||||
data: {
|
||||
email: uniqueEmail,
|
||||
password: "anypassword",
|
||||
captcha_id,
|
||||
captcha_answer,
|
||||
},
|
||||
});
|
||||
if (res.status() === 429) {
|
||||
|
|
@ -107,10 +123,13 @@ test.describe("Security - Rate Limiting", () => {
|
|||
|
||||
test("AI endpoints rate limit after daily quota exceeded", async () => {
|
||||
const ctx = await request.newContext();
|
||||
const { captcha_id, captcha_answer } = await solveCaptcha();
|
||||
const loginRes = await ctx.post(`${API_BASE}/auth/login`, {
|
||||
data: {
|
||||
email: "testcompany@example.com",
|
||||
password: "TestPassword123!",
|
||||
captcha_id,
|
||||
captcha_answer,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -148,10 +167,13 @@ test.describe("Security - Rate Limiting", () => {
|
|||
test.describe("Security - Input Validation", () => {
|
||||
test("SQL injection in login email is handled safely", async () => {
|
||||
const ctx = await request.newContext();
|
||||
const { captcha_id, captcha_answer } = await solveCaptcha();
|
||||
const res = await ctx.post(`${API_BASE}/auth/login`, {
|
||||
data: {
|
||||
email: "' OR '1'='1",
|
||||
password: "anything",
|
||||
captcha_id,
|
||||
captcha_answer,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -162,10 +184,13 @@ test.describe("Security - Input Validation", () => {
|
|||
|
||||
test("XSS attempt in login email is handled safely", async () => {
|
||||
const ctx = await request.newContext();
|
||||
const { captcha_id, captcha_answer } = await solveCaptcha();
|
||||
const res = await ctx.post(`${API_BASE}/auth/login`, {
|
||||
data: {
|
||||
email: "<script>alert('xss')</script>@example.com",
|
||||
password: "password",
|
||||
captcha_id,
|
||||
captcha_answer,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -175,11 +200,14 @@ test.describe("Security - Input Validation", () => {
|
|||
test("Very long input is handled without crash", async () => {
|
||||
const ctx = await request.newContext();
|
||||
const longString = "a".repeat(10000);
|
||||
const { captcha_id, captcha_answer } = await solveCaptcha();
|
||||
|
||||
const res = await ctx.post(`${API_BASE}/auth/login`, {
|
||||
data: {
|
||||
email: `${longString}@example.com`,
|
||||
password: longString,
|
||||
captcha_id,
|
||||
captcha_answer,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -199,10 +227,13 @@ test.describe("Security - CORS Headers", () => {
|
|||
test.describe("Security - Authorization", () => {
|
||||
test("User cannot access admin endpoints with regular user token", async () => {
|
||||
const ctx = await request.newContext();
|
||||
const { captcha_id, captcha_answer } = await solveCaptcha();
|
||||
const loginRes = await ctx.post(`${API_BASE}/auth/login`, {
|
||||
data: {
|
||||
email: "testtutora2026@example.com",
|
||||
password: "Test1234!",
|
||||
captcha_id,
|
||||
captcha_answer,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -229,11 +260,14 @@ test.describe("Security - Authorization", () => {
|
|||
test.describe("Security - Response Headers", () => {
|
||||
test("API does not leak sensitive information in error responses", async () => {
|
||||
const ctx = await request.newContext();
|
||||
const { captcha_id, captcha_answer } = await solveCaptcha();
|
||||
|
||||
const res = await ctx.post(`${API_BASE}/auth/login`, {
|
||||
data: {
|
||||
email: "test@example.com",
|
||||
password: "wrongpassword",
|
||||
captcha_id,
|
||||
captcha_answer,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -183,7 +183,7 @@ async function wireApiMock(
|
|||
}
|
||||
|
||||
async function runSignup(page: any, scenario: SignupScenario, email: string) {
|
||||
await page.goto(`http://localhost:3000${scenario.signupUrl}`);
|
||||
await page.goto(scenario.signupUrl);
|
||||
|
||||
await page.fill("#first-name", "Ari");
|
||||
await page.fill("#last-name", "Tester");
|
||||
|
|
@ -271,7 +271,7 @@ test.describe("Signup and verification submission by role", () => {
|
|||
await runSignup(page, scenario, email);
|
||||
await seedSession(page, scenario, email);
|
||||
|
||||
await page.goto(`http://localhost:3000/dashboard?role=${scenario.dashboardRole}`);
|
||||
await page.goto(`/dashboard?role=${scenario.dashboardRole}`);
|
||||
await page.getByRole("button", { name: /my profile/i }).click();
|
||||
|
||||
const submitButton = page.getByRole("button", { name: /submit for verification/i });
|
||||
|
|
@ -292,7 +292,7 @@ test.describe("Signup and verification submission by role", () => {
|
|||
await runSignup(page, scenario, email);
|
||||
await seedSession(page, scenario, email);
|
||||
|
||||
await page.goto(`http://localhost:3000/dashboard?role=${scenario.dashboardRole}`);
|
||||
await page.goto(`/dashboard?role=${scenario.dashboardRole}`);
|
||||
await page.getByRole("button", { name: /my profile/i }).click();
|
||||
|
||||
const submitButton = page.getByRole("button", { name: /submit for verification/i });
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { test, expect, Page } from "@playwright/test";
|
||||
|
||||
async function loginViaApi(page: Page, email: string, password: string): Promise<{ access_token: string; active_role: string }> {
|
||||
const res = await page.request.post("http://localhost:3000/api/auth/login", {
|
||||
const res = await page.request.post("/api/auth/login", {
|
||||
data: { email, password },
|
||||
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||
});
|
||||
|
|
@ -17,7 +17,7 @@ async function loginViaApi(page: Page, email: string, password: string): Promise
|
|||
}
|
||||
|
||||
async function injectAuth(page: Page, email: string, token: string, role: string) {
|
||||
await page.goto("http://localhost:3000/");
|
||||
await page.goto("/");
|
||||
await page.evaluate(
|
||||
({ email, token, role }) => {
|
||||
const payload = {
|
||||
|
|
@ -54,7 +54,7 @@ test.describe("Dashboard Role Resolution", () => {
|
|||
await injectAuth(page, email, access_token, active_role);
|
||||
|
||||
// Step 3: Navigate to dashboard with role param
|
||||
await page.goto(`http://localhost:3000/dashboard?role=${active_role}`);
|
||||
await page.goto(`/dashboard?role=${active_role}`);
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForTimeout(3000); // Wait for effects and bundle load
|
||||
|
||||
|
|
@ -87,7 +87,7 @@ test.describe("Dashboard Role Resolution", () => {
|
|||
const password = "Test1234!";
|
||||
|
||||
// First check if this email has PHOTOGRAPHER role
|
||||
const checkRes = await page.request.post("http://localhost:3000/api/auth/check-email", {
|
||||
const checkRes = await page.request.post("/api/auth/check-email", {
|
||||
data: { email },
|
||||
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||
});
|
||||
|
|
@ -112,14 +112,14 @@ test.describe("Dashboard Role Resolution", () => {
|
|||
} catch {
|
||||
console.log(`[Login] ${email} login failed, trying API login with different approach`);
|
||||
// Try without password for discovery
|
||||
const checkData = await (await page.request.post("http://localhost:3000/api/auth/check-email", {
|
||||
const checkData = await (await page.request.post("/api/auth/check-email", {
|
||||
data: { email },
|
||||
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||
})).json();
|
||||
console.log(`[Check Email Result]:`, checkData);
|
||||
|
||||
// If we can't login, at least verify the role detection
|
||||
await page.goto("http://localhost:3000/login");
|
||||
await page.goto("/login");
|
||||
await page.fill("#login-email", email);
|
||||
await page.waitForTimeout(500);
|
||||
await page.screenshot({ path: "test-results/login-email-check.png" });
|
||||
|
|
@ -130,7 +130,7 @@ test.describe("Dashboard Role Resolution", () => {
|
|||
|
||||
await injectAuth(page, email, token, targetRole);
|
||||
|
||||
await page.goto(`http://localhost:3000/dashboard?role=${targetRole}`);
|
||||
await page.goto(`/dashboard?role=${targetRole}`);
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
|
|
@ -155,7 +155,7 @@ test.describe("Dashboard Role Resolution", () => {
|
|||
|
||||
test("direct dashboard navigation with role param should show correct role", async ({ page }) => {
|
||||
// This test just verifies the URL param is respected
|
||||
await page.goto("http://localhost:3000/dashboard?role=PHOTOGRAPHER");
|
||||
await page.goto("/dashboard?role=PHOTOGRAPHER");
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ test("API login shows correct TUTOR dashboard", async ({ page }) => {
|
|||
page.on("pageerror", (err) => errors.push(err.message));
|
||||
|
||||
// Login via API
|
||||
const loginRes = await page.request.post("http://localhost:3000/api/auth/login", {
|
||||
const loginRes = await page.request.post("/api/auth/login", {
|
||||
data: { email: "testtutora2026@example.com", password: "Test1234!" },
|
||||
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||
});
|
||||
|
|
@ -14,7 +14,7 @@ test("API login shows correct TUTOR dashboard", async ({ page }) => {
|
|||
const role = loginData.user?.active_role || "TUTOR";
|
||||
|
||||
// Inject auth
|
||||
await page.goto("http://localhost:3000/");
|
||||
await page.goto("/");
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.evaluate(
|
||||
({ token, role }) => {
|
||||
|
|
@ -37,7 +37,7 @@ test("API login shows correct TUTOR dashboard", async ({ page }) => {
|
|||
);
|
||||
|
||||
// Navigate to dashboard
|
||||
await page.goto(`http://localhost:3000/dashboard?role=${role}`);
|
||||
await page.goto(`/dashboard?role=${role}`);
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
await page.waitForTimeout(3000);
|
||||
await page.screenshot({ path: "test-results/dashboard-tutor-check.png", fullPage: true });
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue