270 lines
12 KiB
TypeScript
270 lines
12 KiB
TypeScript
import { test, expect, chromium } from "@playwright/test";
|
||
import { randomUUID } from "crypto";
|
||
import { execSync } from "child_process";
|
||
import * as fs from "fs";
|
||
|
||
const SCREENSHOT_DIR = "/Users/ashwin/workspace/nxtgauge-frontend-solid/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`;
|
||
const testPassword = "TestPassword123!";
|
||
|
||
console.log("📧 Email:", testEmail);
|
||
|
||
const browser = await chromium.launch({ headless: false, slowMo: 30 });
|
||
const context = await browser.newContext({ 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();
|
||
expect(regData.user_id).toBeTruthy();
|
||
console.log(" ✅ PASS: Registration successful, user_id:", regData.user_id);
|
||
} catch (e: any) {
|
||
console.log(" ❌ FAIL: Registration failed -", e.message);
|
||
throw e;
|
||
}
|
||
|
||
// ==================== STEP 2: OTP VIA REDIS ====================
|
||
console.log("\n🔐 STEP 2: OTP via Redis");
|
||
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));
|
||
} catch (e: any) {
|
||
console.log(" ❌ FAIL: OTP verification failed -", e.message);
|
||
throw e;
|
||
}
|
||
|
||
// ==================== STEP 4: LOGIN VIA API ====================
|
||
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 || "";
|
||
expect(accessToken).toBeTruthy();
|
||
console.log(" ✅ PASS: Login successful, token length:", accessToken.length);
|
||
} catch (e: any) {
|
||
console.log(" ❌ FAIL: Login failed -", e.message);
|
||
throw e;
|
||
}
|
||
|
||
// ==================== 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({
|
||
email, roleKey: "JOB_SEEKER", role: "JOB_SEEKER", active_role: "JOB_SEEKER",
|
||
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", active_role: "JOB_SEEKER",
|
||
selectedProfessionalRole: "JOB_SEEKER", name: "Jane Smith", fullName: "Jane Smith", id: userId
|
||
}));
|
||
sessionStorage.setItem("nxtgauge_access_token", token);
|
||
}, { 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.waitForTimeout(3000);
|
||
await page.screenshot({ path: `${SCREENSHOT_DIR}/step05_dashboard.png`, fullPage: true });
|
||
console.log(" ✅ PASS: Dashboard loaded");
|
||
} catch (e: any) {
|
||
console.log(" ❌ FAIL: Dashboard load failed -", e.message);
|
||
await page.screenshot({ path: `${SCREENSHOT_DIR}/step05_dashboard_FAIL.png`, fullPage: true });
|
||
throw e;
|
||
}
|
||
|
||
// ==================== STEP 6: PROFILE FORM ====================
|
||
console.log("\n📋 STEP 6: Navigate to profile form (click 'My Profile')");
|
||
try {
|
||
const profileBtn = page.getByRole("button", { name: /my profile/i });
|
||
const isVisible = await profileBtn.isVisible().catch(() => false);
|
||
if (isVisible) {
|
||
await profileBtn.click();
|
||
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.waitForTimeout(3000);
|
||
}
|
||
await page.screenshot({ path: `${SCREENSHOT_DIR}/step06_profile_form.png`, fullPage: true });
|
||
console.log(" ✅ PASS: Profile form displayed");
|
||
} catch (e: any) {
|
||
console.log(" ⚠️ WARN: Profile navigation -", e.message);
|
||
await page.screenshot({ path: `${SCREENSHOT_DIR}/step06_profile_form.png`, fullPage: true });
|
||
}
|
||
|
||
// ==================== STEP 6B: FILL PROFILE FORM ====================
|
||
console.log("\n📝 STEP 6b: Fill job seeker profile fields");
|
||
try {
|
||
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);
|
||
const isVisible = await input.isVisible().catch(() => false);
|
||
const isDisabled = await input.isDisabled().catch(() => false);
|
||
if (isVisible && !isDisabled) {
|
||
const placeholder = await input.getAttribute("placeholder").catch(() => "");
|
||
const id = await input.getAttribute("id").catch(() => "");
|
||
const name = await input.getAttribute("name").catch(() => "");
|
||
const type = await input.getAttribute("type").catch(() => "");
|
||
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();
|
||
console.log(" Total textareas found:", textareaCount);
|
||
for (let i = 0; i < textareaCount; i++) {
|
||
const ta = textareas.nth(i);
|
||
const isVisible = await ta.isVisible().catch(() => false);
|
||
if (isVisible) {
|
||
const placeholder = await ta.getAttribute("placeholder").catch(() => "");
|
||
const name = await ta.getAttribute("name").catch(() => "");
|
||
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) {
|
||
console.log(" ⚠️ WARN: Could not fill profile -", e.message);
|
||
await page.screenshot({ path: `${SCREENSHOT_DIR}/step06b_profile_fill_FAIL.png`, fullPage: true });
|
||
}
|
||
|
||
// ==================== STEP 7: SUBMIT VERIFICATION ====================
|
||
console.log("\n📤 STEP 7: Submit verification");
|
||
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)) {
|
||
await docsTab.click();
|
||
await page.waitForTimeout(2000);
|
||
await page.screenshot({ path: `${SCREENSHOT_DIR}/step07_documents_tab.png`, fullPage: true });
|
||
console.log(" ✅ Switched to Documents tab");
|
||
}
|
||
} else {
|
||
await submitBtn.click();
|
||
await page.waitForTimeout(3000);
|
||
await page.screenshot({ path: `${SCREENSHOT_DIR}/step07_verification_submitted.png`, fullPage: true });
|
||
console.log(" ✅ PASS: Verification submitted!");
|
||
}
|
||
} else {
|
||
console.log(" ⚠️ INFO: Submit button not found - checking verification status");
|
||
const statusText = await page.locator("body").innerText().catch(() => "");
|
||
if (statusText.includes("Submitted") || statusText.includes("verification")) {
|
||
console.log(" ✅ Already on verification page or already submitted");
|
||
}
|
||
await page.screenshot({ path: `${SCREENSHOT_DIR}/step07_submit_notfound.png`, fullPage: true });
|
||
}
|
||
} catch (e: any) {
|
||
console.log(" ⚠️ WARN: Verification submit -", e.message);
|
||
await page.screenshot({ path: `${SCREENSHOT_DIR}/step07_verification_FAIL.png`, fullPage: true });
|
||
}
|
||
|
||
await page.screenshot({ path: `${SCREENSHOT_DIR}/step99_final.png`, fullPage: true });
|
||
|
||
console.log("\n========== TEST COMPLETE ==========");
|
||
console.log("📸 Screenshots:", SCREENSHOT_DIR);
|
||
console.log("📧 Test email:", testEmail);
|
||
console.log("🔑 Password:", testPassword);
|
||
console.log("👤 Name: Jane Smith");
|
||
|
||
await new Promise(r => setTimeout(r, 1000));
|
||
await browser.close();
|
||
});
|
||
});
|