397 lines
14 KiB
TypeScript
397 lines
14 KiB
TypeScript
|
|
import { test, expect, chromium, Page, Browser, BrowserContext } from "@playwright/test";
|
||
|
|
import { randomUUID } from "crypto";
|
||
|
|
import * as fs from "fs";
|
||
|
|
import * as path from "path";
|
||
|
|
|
||
|
|
const SCREENSHOT_DIR = "./test-results";
|
||
|
|
const VIDEO_DIR = "./test-videos";
|
||
|
|
|
||
|
|
// Ensure directories exist
|
||
|
|
if (!fs.existsSync(SCREENSHOT_DIR)) fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
|
||
|
|
if (!fs.existsSync(VIDEO_DIR)) fs.mkdirSync(VIDEO_DIR, { recursive: true });
|
||
|
|
|
||
|
|
export interface TestUser {
|
||
|
|
email: string;
|
||
|
|
password: string;
|
||
|
|
firstName: string;
|
||
|
|
lastName: string;
|
||
|
|
intent: string;
|
||
|
|
userId?: string;
|
||
|
|
accessToken?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface CompanyUser extends TestUser {
|
||
|
|
companyName: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface JobSeekerUser extends TestUser {
|
||
|
|
education?: string;
|
||
|
|
skills?: string[];
|
||
|
|
resumePath?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
async function generateTestEmail(): Promise<string> {
|
||
|
|
return `test_${randomUUID().slice(0, 8)}@test.com`;
|
||
|
|
}
|
||
|
|
|
||
|
|
async function apiRegister(user: TestUser): Promise<{ user_id: string }> {
|
||
|
|
const response = await fetch("http://localhost:9100/api/auth/register", {
|
||
|
|
method: "POST",
|
||
|
|
headers: { "Content-Type": "application/json" },
|
||
|
|
body: JSON.stringify(user),
|
||
|
|
});
|
||
|
|
return response.json();
|
||
|
|
}
|
||
|
|
|
||
|
|
async function apiVerifyOtp(): Promise<boolean> {
|
||
|
|
const response = await fetch("http://localhost:9100/api/auth/verify-email", {
|
||
|
|
method: "POST",
|
||
|
|
headers: { "Content-Type": "application/json" },
|
||
|
|
body: JSON.stringify({ otp: "123456" }),
|
||
|
|
});
|
||
|
|
return response.ok;
|
||
|
|
}
|
||
|
|
|
||
|
|
async function apiLogin(email: string, password: string): Promise<{ access_token: string }> {
|
||
|
|
const response = await fetch("http://localhost:9100/api/auth/login", {
|
||
|
|
method: "POST",
|
||
|
|
headers: { "Content-Type": "application/json" },
|
||
|
|
body: JSON.stringify({ email, password }),
|
||
|
|
});
|
||
|
|
return response.json();
|
||
|
|
}
|
||
|
|
|
||
|
|
async function setRedisOtp(userId: string): Promise<void> {
|
||
|
|
const { execSync } = await import("child_process");
|
||
|
|
try {
|
||
|
|
execSync(`redis-cli SETEX "otp:code:123456" 900 "${userId}"`, { encoding: "utf8" });
|
||
|
|
} catch (e) {
|
||
|
|
console.log("⚠️ Could not set OTP in Redis:", e);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function takeScreenshot(page: Page, name: string): Promise<void> {
|
||
|
|
const filePath = path.join(SCREENSHOT_DIR, `${name}.png`);
|
||
|
|
await page.screenshot({ path: filePath, fullPage: true });
|
||
|
|
console.log(`📸 Screenshot: ${name}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function registerCompany(): Promise<CompanyUser> {
|
||
|
|
const email = await generateTestEmail();
|
||
|
|
const companyName = `Test Company ${randomUUID().slice(0, 6)}`;
|
||
|
|
const user: TestUser = {
|
||
|
|
email,
|
||
|
|
password: "TestPassword123!",
|
||
|
|
firstName: "John",
|
||
|
|
lastName: "Doe",
|
||
|
|
intent: "company",
|
||
|
|
};
|
||
|
|
|
||
|
|
console.log("\n📝 Step 1: Registering company via API...");
|
||
|
|
const regData = await apiRegister(user);
|
||
|
|
user.userId = regData.user_id;
|
||
|
|
console.log(" ✅ Registered, user_id:", regData.user_id);
|
||
|
|
|
||
|
|
console.log("\n🔐 Step 2: Setting test OTP in Redis...");
|
||
|
|
await setRedisOtp(regData.user_id);
|
||
|
|
console.log(" ✅ Set test OTP: 123456");
|
||
|
|
|
||
|
|
console.log("\n✅ Step 3: Verifying OTP via API...");
|
||
|
|
const verified = await apiVerifyOtp();
|
||
|
|
if (!verified) throw new Error("OTP verification failed");
|
||
|
|
console.log(" ✅ OTP verified!");
|
||
|
|
|
||
|
|
console.log("\n🔑 Step 4: Logging in via API...");
|
||
|
|
const loginData = await apiLogin(email, user.password);
|
||
|
|
user.accessToken = loginData.access_token;
|
||
|
|
console.log(" ✅ Logged in via API!");
|
||
|
|
|
||
|
|
return { ...user, companyName };
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function registerJobSeeker(): Promise<JobSeekerUser> {
|
||
|
|
const email = await generateTestEmail();
|
||
|
|
const user: TestUser = {
|
||
|
|
email,
|
||
|
|
password: "TestPassword123!",
|
||
|
|
firstName: "Jane",
|
||
|
|
lastName: "Smith",
|
||
|
|
intent: "job_seeker",
|
||
|
|
};
|
||
|
|
|
||
|
|
console.log("\n📝 Step 1: Registering job seeker via API...");
|
||
|
|
const regData = await apiRegister(user);
|
||
|
|
user.userId = regData.user_id;
|
||
|
|
console.log(" ✅ Registered, user_id:", regData.user_id);
|
||
|
|
|
||
|
|
console.log("\n🔐 Step 2: Setting test OTP in Redis...");
|
||
|
|
await setRedisOtp(regData.user_id);
|
||
|
|
console.log(" ✅ Set test OTP: 123456");
|
||
|
|
|
||
|
|
console.log("\n✅ Step 3: Verifying OTP via API...");
|
||
|
|
const verified = await apiVerifyOtp();
|
||
|
|
if (!verified) throw new Error("OTP verification failed");
|
||
|
|
console.log(" ✅ OTP verified!");
|
||
|
|
|
||
|
|
console.log("\n🔑 Step 4: Logging in via API...");
|
||
|
|
const loginData = await apiLogin(email, user.password);
|
||
|
|
user.accessToken = loginData.access_token;
|
||
|
|
console.log(" ✅ Logged in via API!");
|
||
|
|
|
||
|
|
return user;
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function loginViaBrowser(
|
||
|
|
context: BrowserContext,
|
||
|
|
email: string,
|
||
|
|
password: string
|
||
|
|
): Promise<Page> {
|
||
|
|
const page = await context.newPage();
|
||
|
|
|
||
|
|
console.log("\n🌐 Opening login page...");
|
||
|
|
await page.goto("http://localhost:3000/login");
|
||
|
|
await page.waitForLoadState("networkidle");
|
||
|
|
await takeScreenshot(page, "01-login-page");
|
||
|
|
|
||
|
|
console.log("✍️ Filling login form...");
|
||
|
|
await page.fill('input[type="email"], input[name="email"]', email);
|
||
|
|
await page.fill('input[type="password"], input[name="password"]', password);
|
||
|
|
await takeScreenshot(page, "02-login-filled");
|
||
|
|
|
||
|
|
console.log("🔐 Handling CAPTCHA (manual entry required)...");
|
||
|
|
console.log(" Please enter CAPTCHA in the browser window...");
|
||
|
|
|
||
|
|
await page.waitForFunction(
|
||
|
|
() => {
|
||
|
|
const btn = document.querySelector(".auth-submit-btn, button[type='submit']");
|
||
|
|
return btn && !(btn as HTMLButtonElement).disabled;
|
||
|
|
},
|
||
|
|
{ timeout: 60000 }
|
||
|
|
);
|
||
|
|
|
||
|
|
await page.click(".auth-submit-btn, button[type='submit']");
|
||
|
|
await page.waitForTimeout(3000);
|
||
|
|
await takeScreenshot(page, "03-after-login");
|
||
|
|
|
||
|
|
return page;
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function setSessionToken(page: Page, token: string): Promise<void> {
|
||
|
|
await page.evaluate(
|
||
|
|
(t) => {
|
||
|
|
window.sessionStorage.setItem("nxtgauge_access_token", t);
|
||
|
|
window.sessionStorage.setItem("nxtgauge_frontend_access_token", t);
|
||
|
|
},
|
||
|
|
token
|
||
|
|
);
|
||
|
|
console.log(" ✅ Session token set");
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function navigateToDashboard(page: Page, role?: string): Promise<void> {
|
||
|
|
const url = role
|
||
|
|
? `http://localhost:3000/dashboard?role=${role}`
|
||
|
|
: "http://localhost:3000/dashboard";
|
||
|
|
await page.goto(url);
|
||
|
|
await page.waitForLoadState("networkidle");
|
||
|
|
await page.waitForTimeout(2000);
|
||
|
|
await takeScreenshot(page, "04-dashboard");
|
||
|
|
console.log(" ✅ Navigated to dashboard");
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function fillCompanyProfile(page: Page, companyName: string): Promise<void> {
|
||
|
|
console.log("\n🏢 Filling company profile...");
|
||
|
|
|
||
|
|
await page.goto("http://localhost:3000/dashboard/profile?role=COMPANY");
|
||
|
|
await page.waitForLoadState("networkidle");
|
||
|
|
await page.waitForTimeout(2000);
|
||
|
|
await takeScreenshot(page, "05-company-profile");
|
||
|
|
|
||
|
|
const nameInput = await page.locator('input[name="companyName"], input[name="name"]').first();
|
||
|
|
if (await nameInput.isVisible().catch(() => false)) {
|
||
|
|
await nameInput.fill(companyName);
|
||
|
|
console.log(" ✅ Company name filled");
|
||
|
|
}
|
||
|
|
|
||
|
|
const websiteInput = await page.locator('input[name="website"], input[placeholder*="website"]').first();
|
||
|
|
if (await websiteInput.isVisible().catch(() => false)) {
|
||
|
|
await websiteInput.fill("https://testcompany.com");
|
||
|
|
console.log(" ✅ Website filled");
|
||
|
|
}
|
||
|
|
|
||
|
|
const phoneInput = await page.locator('input[name="phone"], input[name="companyPhone"]').first();
|
||
|
|
if (await phoneInput.isVisible().catch(() => false)) {
|
||
|
|
await phoneInput.fill("+91 9876543210");
|
||
|
|
console.log(" ✅ Phone filled");
|
||
|
|
}
|
||
|
|
|
||
|
|
await takeScreenshot(page, "06-company-profile-filled");
|
||
|
|
|
||
|
|
const submitBtn = await page.locator('button[type="submit"], button:has-text("Submit")').first();
|
||
|
|
if (await submitBtn.isVisible().catch(() => false)) {
|
||
|
|
await submitBtn.click();
|
||
|
|
await page.waitForTimeout(3000);
|
||
|
|
console.log(" ✅ Profile submitted");
|
||
|
|
await takeScreenshot(page, "07-company-profile-submitted");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function fillJobSeekerProfile(page: Page): Promise<void> {
|
||
|
|
console.log("\n👤 Filling job seeker profile...");
|
||
|
|
|
||
|
|
await page.goto("http://localhost:3000/dashboard/profile?role=JOB_SEEKER");
|
||
|
|
await page.waitForLoadState("networkidle");
|
||
|
|
await page.waitForTimeout(2000);
|
||
|
|
await takeScreenshot(page, "08-jobseeker-profile");
|
||
|
|
|
||
|
|
const firstNameInput = await page.locator('input[name="firstName"], input[name="first_name"]').first();
|
||
|
|
if (await firstNameInput.isVisible().catch(() => false)) {
|
||
|
|
await firstNameInput.fill("Jane");
|
||
|
|
console.log(" ✅ First name filled");
|
||
|
|
}
|
||
|
|
|
||
|
|
const lastNameInput = await page.locator('input[name="lastName"], input[name="last_name"]').first();
|
||
|
|
if (await lastNameInput.isVisible().catch(() => false)) {
|
||
|
|
await lastNameInput.fill("Smith");
|
||
|
|
console.log(" ✅ Last name filled");
|
||
|
|
}
|
||
|
|
|
||
|
|
const bioInput = await page.locator('textarea[name="bio"], textarea[name="about"]').first();
|
||
|
|
if (await bioInput.isVisible().catch(() => false)) {
|
||
|
|
await bioInput.fill("Experienced software developer with 5+ years in the industry.");
|
||
|
|
console.log(" ✅ Bio filled");
|
||
|
|
}
|
||
|
|
|
||
|
|
await takeScreenshot(page, "09-jobseeker-profile-filled");
|
||
|
|
|
||
|
|
const submitBtn = await page.locator('button[type="submit"], button:has-text("Submit")').first();
|
||
|
|
if (await submitBtn.isVisible().catch(() => false)) {
|
||
|
|
await submitBtn.click();
|
||
|
|
await page.waitForTimeout(3000);
|
||
|
|
console.log(" ✅ Profile submitted");
|
||
|
|
await takeScreenshot(page, "10-jobseeker-profile-submitted");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function adminLogin(context: BrowserContext): Promise<Page> {
|
||
|
|
console.log("\n🔐 Opening admin panel...");
|
||
|
|
const adminPage = await context.newPage();
|
||
|
|
await adminPage.goto("http://localhost:3001/login");
|
||
|
|
await adminPage.waitForLoadState("networkidle");
|
||
|
|
await takeScreenshot(adminPage, "11-admin-login");
|
||
|
|
|
||
|
|
console.log("✍️ Filling admin credentials...");
|
||
|
|
await adminPage.fill('input[type="email"], input[name="email"]', "admin@nxtgauge.com");
|
||
|
|
await adminPage.fill('input[type="password"], input[name="password"]', "Admin@nxtgauge1");
|
||
|
|
await adminPage.click('button[type="submit"], button:has-text("Login"), button:has-text("Sign In")');
|
||
|
|
await adminPage.waitForTimeout(3000);
|
||
|
|
await takeScreenshot(adminPage, "12-admin-dashboard");
|
||
|
|
console.log(" ✅ Admin logged in");
|
||
|
|
|
||
|
|
return adminPage;
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function findUserInAdminVerification(
|
||
|
|
adminPage: Page,
|
||
|
|
email: string
|
||
|
|
): Promise<boolean> {
|
||
|
|
console.log(`\n🔍 Searching for ${email} in admin verification...`);
|
||
|
|
|
||
|
|
const verificationLinks = [
|
||
|
|
"text=Verifications",
|
||
|
|
'a:has-text("Verifications")',
|
||
|
|
'[href*="verification"]',
|
||
|
|
"text=Pending",
|
||
|
|
];
|
||
|
|
|
||
|
|
for (const selector of verificationLinks) {
|
||
|
|
const link = adminPage.locator(selector).first();
|
||
|
|
if (await link.isVisible().catch(() => false)) {
|
||
|
|
await link.click();
|
||
|
|
await adminPage.waitForTimeout(2000);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
await takeScreenshot(adminPage, "13-verification-list");
|
||
|
|
|
||
|
|
const searchInput = adminPage.locator('input[placeholder*="search" i], input[name="search"]').first();
|
||
|
|
if (await searchInput.isVisible().catch(() => false)) {
|
||
|
|
await searchInput.fill(email);
|
||
|
|
await adminPage.waitForTimeout(2000);
|
||
|
|
await takeScreenshot(adminPage, "14-search-results");
|
||
|
|
}
|
||
|
|
|
||
|
|
const content = await adminPage.locator("body").innerText();
|
||
|
|
return content.includes(email);
|
||
|
|
}
|
||
|
|
|
||
|
|
test.describe("Nxtgauge E2E Verification Flows", () => {
|
||
|
|
test.setTimeout(600000);
|
||
|
|
|
||
|
|
test("Company Registration → Profile → Admin Verification", async () => {
|
||
|
|
const browser = await chromium.launch({ headless: false, slowMo: 100 });
|
||
|
|
const context = await browser.newContext({
|
||
|
|
viewport: { width: 1400, height: 900 },
|
||
|
|
recordVideo: { dir: VIDEO_DIR, size: { width: 1400, height: 900 } },
|
||
|
|
});
|
||
|
|
|
||
|
|
try {
|
||
|
|
const company = await registerCompany();
|
||
|
|
|
||
|
|
console.log("\n🌐 MANUAL STEP: Open browser and:");
|
||
|
|
console.log(" 1. Go to http://localhost:3000/login");
|
||
|
|
console.log(" 2. Login with:");
|
||
|
|
console.log(" Email:", company.email);
|
||
|
|
console.log(" Password:", company.password);
|
||
|
|
console.log(" 3. Complete CAPTCHA");
|
||
|
|
console.log(" 4. Fill company profile at /dashboard/profile?role=COMPANY");
|
||
|
|
console.log(" 5. Upload business documents");
|
||
|
|
console.log(" 6. Submit for verification");
|
||
|
|
console.log(" Then press Enter to continue to admin verification...");
|
||
|
|
|
||
|
|
await context.close();
|
||
|
|
await browser.close();
|
||
|
|
|
||
|
|
console.log("\n⏳ Waiting 120 seconds for manual browser steps...");
|
||
|
|
await new Promise((r) => setTimeout(r, 120000));
|
||
|
|
|
||
|
|
} catch (error) {
|
||
|
|
console.error("❌ Test failed:", error);
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test("Job Seeker Registration → Profile → Admin Verification", async () => {
|
||
|
|
const browser = await chromium.launch({ headless: false, slowMo: 100 });
|
||
|
|
const context = await browser.newContext({
|
||
|
|
viewport: { width: 1400, height: 900 },
|
||
|
|
recordVideo: { dir: VIDEO_DIR, size: { width: 1400, height: 900 } },
|
||
|
|
});
|
||
|
|
|
||
|
|
try {
|
||
|
|
const jobSeeker = await registerJobSeeker();
|
||
|
|
|
||
|
|
console.log("\n🌐 MANUAL STEP: Open browser and:");
|
||
|
|
console.log(" 1. Go to http://localhost:3000/login");
|
||
|
|
console.log(" 2. Login with:");
|
||
|
|
console.log(" Email:", jobSeeker.email);
|
||
|
|
console.log(" Password:", jobSeeker.password);
|
||
|
|
console.log(" 3. Complete CAPTCHA");
|
||
|
|
console.log(" 4. Fill job seeker profile at /dashboard/profile?role=JOB_SEEKER");
|
||
|
|
console.log(" 5. Add education, skills, resume");
|
||
|
|
console.log(" 6. Submit for verification");
|
||
|
|
console.log(" Then press Enter to continue to admin verification...");
|
||
|
|
|
||
|
|
await context.close();
|
||
|
|
await browser.close();
|
||
|
|
|
||
|
|
console.log("\n⏳ Waiting 120 seconds for manual browser steps...");
|
||
|
|
await new Promise((r) => setTimeout(r, 120000));
|
||
|
|
|
||
|
|
} catch (error) {
|
||
|
|
console.error("❌ Test failed:", error);
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|