snapshot of https://github.com/krayin/laravel-crm.git (2.2, 2026-09-18; blobs > 131072B omitted)
Some checks failed
Admin | Playwright Tests / Admin | Playwright Tests | Shard 1 Of 6 (push) Failing after 37s
Admin | Playwright Tests / Admin | Playwright Tests | Shard 2 Of 6 (push) Failing after 7s
Admin | Playwright Tests / Admin | Playwright Tests | Shard 4 Of 6 (push) Failing after 7s
Admin | Playwright Tests / Admin | Playwright Tests | Shard 6 Of 6 (push) Failing after 6s
Linting Tests / PHP 8.3 test on ubuntu-latest (push) Failing after 5s
CI / PHP 8.3 test on ubuntu-latest (push) Failing after 6s
Skills Consistency / validate-skills (push) Successful in 10s
Admin | Playwright Tests / Admin | Playwright Tests | Shard 3 Of 6 (push) Failing after 7s
Admin | Playwright Tests / Admin | Playwright Tests | Shard 5 Of 6 (push) Failing after 6s

This commit is contained in:
upstream-mirror
2026-09-18 04:37:41 +05:00
commit 135ffdb085
1274 changed files with 144880 additions and 0 deletions

View File

@@ -0,0 +1,146 @@
import { expect, Locator, Page } from "@playwright/test";
import CoreLocators from "../locator/CoreLocators";
import { QuickAddPage } from "./QuickAddPage";
/**
* Page object for asserting what a signed-in user is allowed to see and reach.
*
* The three enforcement layers are kept as separate methods on purpose, because they answer
* different questions and a suite that conflates them proves less than it looks:
*
* - `expectNavVisible` / `expectNavHidden` — menu filtering (presentation only)
* - `expectControlHidden` — Blade `bouncer()->hasPermission()` (presentation only)
* - the route probes in `utils/acl.ts` — the middleware (the real security boundary)
*/
export class AclPage extends CoreLocators {
readonly page: Page;
readonly unauthorizedHeading: Locator;
readonly datagridRows: Locator;
constructor(page: Page) {
super(page);
this.page = page;
this.unauthorizedHeading = page.getByText("401 Unauthorized");
this.datagridRows = page.locator("div.row.grid.grid-cols-\\[2fr_1fr\\]");
}
/**
* A left-navigation entry, matched by the path it links to.
*
* The label is not a usable anchor: each sidebar link wraps an icon span and a `<p>`, so its
* accessible name does not match the menu label cleanly. The href comes straight from the menu
* config's route, which is also what `Menu::getItems()` filters on — so this asserts against
* the same thing the code decides.
*/
navItem(path: string): Locator {
return this.page.locator(`nav a[href$="${path}"], aside a[href$="${path}"]`);
}
async expectNavVisible(path: string): Promise<void> {
await expect(
this.navItem(path).first(),
`a navigation entry for ${path} should appear for a user holding its permission`
).toBeVisible();
}
async expectNavHidden(path: string): Promise<void> {
await expect(
this.navItem(path),
`a navigation entry for ${path} must not appear without its permission`
).toHaveCount(0);
}
/**
* Assert a control is absent from the page. Used for create/edit/delete buttons that Blade
* wraps in a permission check.
*/
async expectControlHidden(name: string): Promise<void> {
await expect(
this.page.getByRole("button", { name }),
`"${name}" must not be rendered without its permission`
).toHaveCount(0);
}
async expectControlVisible(name: string): Promise<void> {
await expect(
this.page.getByRole("button", { name }).first(),
`"${name}" should be rendered for a user holding its permission`
).toBeVisible();
}
/**
* Assert the browser landed on the unauthorized page rather than the requested screen.
*/
async expectUnauthorizedPage(): Promise<void> {
await expect(this.unauthorizedHeading.first()).toBeVisible();
}
/**
* Visit a listing and return the visible row text, so a scope assertion can check which
* records the grid actually returned.
*/
async visibleRowText(path: string): Promise<string> {
await this.page.goto(path);
await this.page.waitForLoadState("networkidle");
return (await this.page.locator("body").innerText()).toLowerCase();
}
/**
* Assert a listing contains a record's identifying text.
*/
async expectRecordListed(path: string, title: string): Promise<void> {
const body = await this.visibleRowText(path);
expect(body, `"${title}" should be visible in ${path} for this data scope`).toContain(
title.toLowerCase()
);
}
/**
* Assert a listing does not contain a record's identifying text — the core view-permission
* assertion.
*/
async expectRecordNotListed(path: string, title: string): Promise<void> {
const body = await this.visibleRowText(path);
expect(body, `"${title}" must be hidden in ${path} for this data scope`).not.toContain(
title.toLowerCase()
);
}
/**
* Create a person owned by whoever is signed in.
*
* Quick add is used rather than the full create form for one reason: `PersonController::store()`
* assigns `user_id` from the acting user only on the `quick_add` path. Submitting the standard
* form without picking a Sales Owner saves the person with a null owner, which makes it
* invisible to every non-global user — including its own creator — and would make these scope
* assertions fail for a reason that has nothing to do with `view_permission`.
*/
async createPerson(name: string, email: string, phone?: string): Promise<void> {
const quickAdd = new QuickAddPage(this.page);
await this.page.goto("admin/dashboard").catch(() => undefined);
await quickAdd.createPerson({
name,
email,
/**
* Email and phone are both unique across persons, so a shared literal would fail
* validation with "The value has already been taken." from the second person onward.
*/
contactNumber: phone ?? AclPage.uniquePhone(),
});
}
/**
* A phone number unique to this call, within the 10 digits the form accepts.
*/
static uniquePhone(): string {
return `9${Date.now().toString().slice(-6)}${Math.floor(Math.random() * 900 + 100)}`;
}
}

View File

@@ -0,0 +1,47 @@
import { Page, Locator, expect } from "@playwright/test";
import CoreLocators from "../locator/CoreLocators";
export class AdminPage extends CoreLocators {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly signInButton: Locator;
readonly megaSearchInput: Locator;
readonly profileButton: Locator;
readonly signOutLink: Locator;
constructor(page: Page) {
super(page)
this.page = page;
this.emailInput = page.getByPlaceholder("Email Address");
this.passwordInput = page.getByPlaceholder("Password");
this.signInButton = page.getByRole("button", { name: "Sign In" });
this.megaSearchInput = page.getByPlaceholder("Mega Search").first();
this.profileButton = page.getByRole("button", { name: "E" });
this.signOutLink = page.getByRole("link", { name: "Sign Out" });
}
async adminLogin(email: string, password: string) {
await this.page.goto('admin/login');
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.signInButton.click();
}
async verifyLogin() {
await expect(this.megaSearchInput).toBeVisible();
}
async adminLogout() {
await this.page.waitForTimeout(1000);
await this.profileButton.click();
await this.signOutLink.click();
}
async verifyLogout() {
await expect(this.passwordInput.first()).toBeVisible();
}
}

View File

@@ -0,0 +1,168 @@
import { Page, Locator, expect } from "@playwright/test";
import CoreLocators from "../locator/CoreLocators";
import { buildPersonData, PersonData } from "./PersonsPage";
import { buildProductData, ProductData, ProductPage } from "./ProductPage";
import { generateDescription, generateName } from "../utils/faker";
export type LeadData = {
title: string;
description: string;
value: string;
expectedCloseDate: string; // ISO date string
person: PersonData;
organizationName: string;
product: ProductData;
};
const leadPersonData: PersonData = buildPersonData();
const leadProductData: ProductData = buildProductData();
export const leadData: LeadData = {
title: generateName(),
description: generateDescription(),
value: (Math.floor(Math.random() * 10000)).toString(),
expectedCloseDate: "2028-12-31",
/**
* The lead flow owns its person outright, so it never collides with the one the person or
* quote specs create.
*/
person: leadPersonData,
/**
* The lead flow owns its product too — the quote and product specs each create one of their own.
*/
product: leadProductData,
organizationName: leadPersonData.organizationName
};
export class LeadPage extends CoreLocators {
readonly page: Page;
constructor(page: Page) {
super(page);
this.page = page;
}
async navigateToLeadList() {
await this.page.goto("admin/leads");
}
async getLeadByTitle(title: string) {
return this.page.getByRole('link', { name: ` ${title}` });
}
async createLead(leadData: LeadData) {
const productPage = new ProductPage(this.page);
await productPage.navigateToProductPage();
await productPage.createProductLink.click()
await productPage.productForm(leadData.product);
await this.navigateToLeadList();
await this.createLeadButton.first().click();;
await this.titleInput.fill(leadData.title);
await this.descriptionTextarea.fill(leadData.description);
await this.sourceDropdown.selectOption("1");
await this.typeDropdown.selectOption("1");
await this.userDropdown.selectOption("1");
await this.leadValueInput.fill(leadData.value);
await this.addPersonButton.click();
await this.leadPersonSearchInput.fill(leadData.person.name);
await this.page.waitForTimeout(1000);
const personItem = (await this.selectListItmeByName(leadData.person.name)).first();
await this.page.waitForTimeout(1000);
const isPersonAlreadyPresent = await personItem.isVisible();
if (isPersonAlreadyPresent) {
await personItem.click();
console.log("person allready present");
}
else {
await this.addAsNewButton.click();
await this.personEmailInput.fill(leadData.person.emails);
await this.personPhoneInput.fill(leadData.person.contactNumber);
await this.addOrganizationButton.click(); // this an issue add organiztion is not working if person allready present so that i have added this into else condition.
await this.searchInputField.fill(leadData.title);
await this.addAsNewButton.click();
}
// await this.leadProductAddMoreButton.click();
await this.leadProductSelect.click();
await this.leadProductSearchInput.fill(leadData.product.name);
await this.page.waitForTimeout(1000);
await this.page.locator(`//li[@class="cursor-pointer px-4 py-2 text-gray-800 transition-colors hover:bg-blue-100 dark:text-white dark:hover:bg-gray-900"]`).first().click();
(await this.getElementByTypeAndName('button', "Save")).click();
await this.searchInput.fill(leadData.title);
await this.page.keyboard.press('Enter');
await expect(this.page.getByText(leadData.title).first()).toBeVisible();
await expect(this.leadSuccessToast).toBeVisible();
}
async updateLead(leadData: LeadData) {
// Now update the lead with new data
// Fill updated lead data
// Use locators from LeadPage via page1 context to fill fields
await this.titleInput.fill(leadData.title);
await this.descriptionTextarea.fill(leadData.description);
await this.sourceDropdown.selectOption("1");
await this.typeDropdown.selectOption("1");
await this.userDropdown.selectOption("1");
await this.leadValueInput.fill("1000");
// await this.leadProductAddMoreButton.click();
await this.leadProductSelect.click();
await this.leadProductSearchInput.fill(leadData.product.name);
await this.page.waitForTimeout(1000);
await this.page.locator(`//li[@class="cursor-pointer px-4 py-2 text-gray-800 transition-colors hover:bg-blue-100 dark:text-white dark:hover:bg-gray-900"]`).first().click();
(await this.getElementByTypeAndName('button', "Save")).click();
await this.searchInput.fill(leadData.title);
await this.page.keyboard.press('Enter');
await expect(((await this.getLeadByTitle(leadData.title)).first())).toBeVisible();
await expect(this.leadSuccessToast).toBeVisible();
}
async deleteLead(leadData: LeadData) {
await this.listViewButton.click();
await this.page.waitForTimeout(1000);
await this.listViewLeadSearch.fill(leadData.title);
await this.page.keyboard.press('Enter');
await this.page.waitForTimeout(1000);
await this.deleteLeadButton.isVisible();
await this.deleteLeadButton.click();
await this.agreeButton.click();
await this.listViewLeadSearch.fill(leadData.title);
await this.page.keyboard.press('Enter');
await expect(this.deleteLeadButton).not.toBeVisible();
}
async searchLead(title: string) {
await this.navigateToLeadList();
await this.searchInput.fill(title);
await this.page.keyboard.press('Enter');
await this.page.waitForTimeout(1000);
await (await this.getLeadByTitle(title)).first().click();
await this.page.waitForLoadState('domcontentloaded');
const page1Promise = this.page.waitForEvent('popup');
await this.editLeadButton.click();
const page1 = await page1Promise;
const leadPage = new LeadPage(page1);
return leadPage;
}
async listView() {
await this.page.goto('admin/leads?view_type=table');
await this.page.waitForLoadState('domcontentloaded');
}
}

View File

@@ -0,0 +1,139 @@
import { Page, Locator, FrameLocator, expect } from '@playwright/test';
import CoreLocators from '../locator/CoreLocators';
import { generateDescription, generateEmail, generateName } from '../utils/faker';
export type MailData = {
replyTo: string;
subject: string;
body: string;
};
export const mailData: MailData = {
replyTo: generateEmail(),
subject: generateName(),
body: generateDescription().slice(0, 50),
};
export type emailType = 'bcc' | 'cc' | 'normal'|'both';
export class MailPage extends CoreLocators {
readonly page: Page;
readonly composeEmailButton: Locator;
readonly replyToInput: Locator;
readonly subjectInput: Locator;
readonly sendButton: Locator;
readonly successMessage: Locator;
readonly editorFrame: FrameLocator;
readonly saveDraftButton: Locator;
readonly moveToTrashButton: Locator;
readonly ccMailInput: Locator;
readonly bccMailInput: Locator;
readonly ccButton: Locator;
readonly bccButton: Locator;
constructor(page: Page) {
super(page);
this.page = page;
this.composeEmailButton = page.getByRole('button', { name: 'Compose Mail' });
this.replyToInput = page.getByLabel(/reply to/i).or(page.locator('input[name="temp-reply_to"]'));
this.subjectInput = page.getByLabel(/subject/i).or(page.locator('input[name="subject"]'));
this.sendButton = page.getByRole("button", { name: "Send" });
this.successMessage = page.getByText("Email sent successfully.");
this.editorFrame = page.frameLocator("iframe.tox-edit-area__iframe");
this.saveDraftButton = page.getByRole("button", { name: "Draft" });
this.moveToTrashButton = page.getByRole('button', { name: 'Moved To Trash' })
this.ccMailInput = page.locator('input[name="temp-cc"]');
this.bccMailInput = page.locator('input[name="temp-bcc"]');
this.ccButton = page.getByText('CC', { exact: true });
this.bccButton = page.getByText('BCC', { exact: true });
}
async navigateToMailInboxPage() {
await this.page.goto("admin/mail/inbox");
}
async navigateToMailDraftPage() {
await this.page.goto("admin/mail/draft");
}
async navigateToMailTrashedPage() {
await this.page.goto("admin/mail/trash");
}
async composeEmail(mailData: MailData, type: emailType) {
await this.composeEmailButton.click();
await this.page.waitForTimeout(500);
if (type === 'cc') {
await this.ccButton.click();
await this.ccMailInput.waitFor({ state: 'visible', timeout: 5000 });
await this.ccMailInput.fill("cc" + mailData.replyTo);
} else if (type === 'bcc') {
await this.bccButton.click();
await this.bccMailInput.waitFor({ state: 'visible', timeout: 5000 });
await this.bccMailInput.fill("bcc" + mailData.replyTo);
} else if (type === 'both') {
await this.ccButton.click();
await this.bccButton.click();
await this.bccMailInput.waitFor({ state: 'visible', timeout: 5000 });
await this.ccMailInput.waitFor({ state: 'visible', timeout: 5000 });
await this.ccMailInput.fill("cc" + mailData.replyTo);
await this.bccMailInput.fill("bcc" + mailData.replyTo);
}
await this.replyToInput.fill(mailData.replyTo);
await this.subjectInput.fill(mailData.subject);
await this.editorFrame.locator('body').fill(mailData.body);
await this.sendButton.click();
}
async saveDraftEmail(mailData: MailData, type: emailType) {
await this.composeEmailButton.click();
await this.page.waitForTimeout(500);
if (type === 'cc') {
await this.ccButton.click();
await this.ccMailInput.waitFor({ state: 'visible', timeout: 5000 });
await this.ccMailInput.fill("cc" + mailData.replyTo);
} else if (type === 'bcc') {
await this.bccButton.click();
await this.bccMailInput.waitFor({ state: 'visible', timeout: 5000 });
await this.bccMailInput.fill("bcc" + mailData.replyTo);
} else if (type === 'both') {
await this.ccButton.click();
await this.bccButton.click();
await this.ccMailInput.waitFor({ state: 'visible', timeout: 5000 });
await this.ccMailInput.fill("cc" + mailData.replyTo);
await this.bccMailInput.waitFor({ state: 'visible', timeout: 5000 });
await this.bccMailInput.fill("bcc" + mailData.replyTo);
}
await this.replyToInput.fill(mailData.replyTo);
await this.subjectInput.fill(mailData.subject);
await this.editorFrame.locator('body').fill(mailData.body);
await this.saveDraftButton.click();
await this.navigateToMailDraftPage();
await this.searchEmail(mailData.subject);
await expect(this.page.getByRole('paragraph').filter({ hasText: mailData.subject }).first()).toBeVisible();
}
async movedEmailToTrash() {
await this.page.waitForTimeout(500);
await this.multiSelectCheckbox.click();
await this.page.waitForTimeout(500);
await this.moveToTrashButton.click();
await this.agreeButton.click();
}
async searchEmail(subject: string) {
await this.searchInputExact.fill(subject);
await this.page.keyboard.press('Enter');
}
async deleteEmailFromTrash(subject: string) {
await this.searchEmail(subject);
await this.page.waitForTimeout(500);
await this.multiSelectCheckbox.click();
await this.page.waitForTimeout(500);
await this.deleteButton.click();
await this.agreeButton.click();
}
}

View File

@@ -0,0 +1,99 @@
import { expect, Locator, Page } from "playwright/test";
import CoreLocators from "../locator/CoreLocators";
import { generateFirstName, generateLocation, generateName } from "../utils/faker";
export type OrganizationData = {
name: string;
address: string;
country: string; // country code, e.g. 'IN'
state: string; // state code, e.g. 'DL'
city: string;
postcode: string;
};
export const organizationData: OrganizationData = {
name: generateName(),
address: generateLocation() ,
country: "IN", // India country code
state: "DL", // Delhi state code
city: "New Delhi",
postcode: "110015",
};
export default class OrganizationPage extends CoreLocators{
readonly page:Page
readonly firstOrganization:Locator;
constructor(page:Page)
{
super(page)
this.page=page
this.firstOrganization= page.locator(`(//p[@class="break-words"])[1]`);
}
async navigateToOrganization()
{
await this.page.goto("admin/contacts/organizations");
}
async createOrganization(orgData:OrganizationData) {
await this.createOrgLink.click();
// Fill organization details
await this.orgNameTextbox.fill(orgData.name);
await this.orgAddressTextarea.fill(orgData.address);
await this.orgCountryCombobox.selectOption(orgData.country);
await this.orgStateSelect.selectOption(orgData.state);
await this.orgCityTextbox.fill(orgData.city);
await this.orgPostcodeTextbox.fill(orgData.postcode);
await this.orgExtraDetailsDiv.nth(2).click();
await this.searchInputField.fill('exampl');
await this.orgExampleListItem('Example').click();
// Save organization
await this.saveOrganizationButton.click();
await expect(this.organizationCreateSuccessMessage.first()).toBeVisible();
// await this.searchInputExact.fill(orgData.name);
// await this.page.keyboard.press('Enter');
console.log("There’s an issue: the organization search isn’t working and needs to be fixed.");
await expect(this.page.getByText(orgData.name).first()).toBeVisible();
}
async updateOrganization(orgData:OrganizationData)
{
await this.firstEditIcon.click();
// Fill organization details
await this.orgNameTextbox.fill(orgData.name);
await this.orgAddressTextarea.fill(orgData.address);
await this.orgCountryCombobox.first().selectOption(orgData.country);
await this.orgStateSelect.selectOption(orgData.state);
await this.orgCityTextbox.fill(orgData.city);
await this.orgPostcodeTextbox.fill(orgData.postcode);
// Save organization
await this.saveOrganizationButton.click();
return orgData;
}
async deleteOrganization(){
await this.firstDeleteIcon.click();
await this.agreeButton.click();
}
}

View File

@@ -0,0 +1,121 @@
import { expect, Locator, Page } from "playwright/test";
import CoreLocators from "../locator/CoreLocators";
import { generateEmail, generateFullName, generateName, generatePhoneNumber } from "../utils/faker";
import { organizationData } from "../pages/OrganizationPage";
export type PersonData = {
name: string;
emails: string; // array to hold multiple emails
contactNumber: string; // array to hold multiple contact numbers
jobTitle: string;
salesOwnerId: string; // id or value representing Sales Owner
organizationName: string;
};
/**
* Build a person with an email and contact number unique to this call.
*
* Both fields are unique in Krayin, so two persons built from one shared object cannot both be
* saved — the second submit fails with "The value has already been taken." Each spec that creates
* a person therefore needs its own instance, while keeping that instance stable *within* the spec
* so the create / edit / delete cases all address the same record.
*/
export function buildPersonData(overrides: Partial<PersonData> = {}): PersonData {
return {
name: generateName(),
emails: generateEmail(),
contactNumber: generatePhoneNumber(),
jobTitle: generateName(),
salesOwnerId: "1", // Example sales owner id
organizationName: organizationData.name,
...overrides,
};
}
/**
* Default instance kept for existing importers. Do not create a person from this in more than one
* spec — build a fresh instance instead.
*/
export const personData: PersonData = buildPersonData();
export default class PersonsPage extends CoreLocators {
readonly page: Page;
readonly firstPerson:Locator;
constructor(page: Page) {
super(page),
this.page = page
this.firstPerson= page.locator(`(//div[@class="flex flex-col gap-1.5 dark:text-gray-300"])[1]`);
}
async navigageToPersonsPage() {
await this.page.goto("admin/contacts/persons");
}
async createPerson(personData: PersonData) {
await this.createPersonLink.click();
// Fill person details (use first email/phone if multiple provided)
await this.personNameTextbox.fill(personData.name);
if (personData.emails && personData.emails.length > 0) {
await this.personEmailTextbox.fill(personData.emails);
}
if (personData.contactNumber && personData.contactNumber.length > 0) {
await this.personPhoneTextbox.fill(personData.contactNumber);
}
await this.personJobTitleTextbox.fill(personData.jobTitle || "");
// Assign organization (if provided)
if (personData.organizationName) {
await this.personOrgSelectDiv.click();
await this.searchInputField.fill("Examp");
await this.personOrgListItem("Examp").click();
}
// Save person
await this.savePersonButton.click();
await this.searchInputExact.fill(personData.name);
await this.page.keyboard.press('Enter');
await expect(this.page.getByText(personData.name).first()).toBeVisible();
}
async updatePerson(personData: PersonData) {
await this.searchInputExact.fill(personData.name);
await this.page.keyboard.press('Enter');
await this.firstEditIcon.click();
// Fill person details (use first email/phone if multiple provided)
await this.personNameTextbox.fill(personData.name);
if (personData.emails && personData.emails.length > 0) {
await this.personEmailTextbox.fill(personData.emails);
}
if (personData.contactNumber && personData.contactNumber.length > 0) {
await this.personPhoneTextbox.fill(personData.contactNumber);
}
await this.personJobTitleTextbox.fill(personData.jobTitle || "");
// Assign organization (if provided)
if (personData.organizationName) {
await this.personOrgSelectDiv.click();
await this.searchInputField.fill("Examp");
await this.personOrgListItem("Examp").click();
}
// Save person
await this.savePersonButton.click();
await this.searchInputExact.fill(personData.name);
await expect(this.page.getByText(personData.name).first()).toBeVisible();
}
async personDelete() {
await this.firstDeleteIcon.click();
await this.agreeButton.click();
await expect(this.successMessage.first()).toBeVisible();
}
async personMassDelete() {
await this.multiSelectCheckbox.click();
await this.deleteButton.click();
await this.agreeButton.click();
await expect(this.successMessage.first()).toBeVisible();
}
}

View File

@@ -0,0 +1,86 @@
import { expect, Page } from "playwright/test";
import CoreLocators from "../locator/CoreLocators";
import { generateDescription, generateName, generateSKU } from "../utils/faker";
import { saveData } from "../utils/savedata";
export type ProductData = {
name: string,
description: string,
sku: string,
price: string,
priceInDollars?: string, // Optional if price is stored in a different currency
quantity: string,
}
/**
* Build a product with a SKU unique to this call.
*
* SKU is unique in Krayin, so two products built from one shared object cannot both be saved. The
* product, lead and quote specs each create a product, so each needs its own instance — sharing one
* makes whichever spec runs later fail on the product step.
*/
export function buildProductData(overrides: Partial<ProductData> = {}): ProductData {
return {
name: generateName(),
description: generateDescription(),
sku: generateSKU(),
price: Math.floor(Math.random() * 1000).toString(),
quantity: Math.floor(Math.random() * 100).toString(),
...overrides,
};
}
/**
* Default instance kept for existing importers. Do not create a product from this in more than one
* spec — build a fresh instance instead.
*/
export const productData: ProductData = buildProductData();
export class ProductPage extends CoreLocators {
page: Page;
constructor(page: Page) {
super(page);
this.page = page
}
async navigateToProductPage() {
await this.page.goto("admin/products");
}
async productForm(productdata: ProductData) {
// Fill product form using this
await this.productNameTextbox.waitFor({ state: "visible" });
await this.productNameTextbox.click();
await this.productNameTextbox.fill(productdata.name);
await this.productDescriptionTextarea.fill("");
await this.productDescriptionTextarea.type(productdata.description);
await this.productSkuInput.fill(productdata.sku);
await this.productPriceInput.fill(productdata.price);
await this.productQuantityInput.fill(productdata.quantity);
// Save product
await this.saveProductsButton.click();
await expect(this.successMessage.first()).toBeVisible();
saveData("productData", productdata);
// await this.searchProduct(productdata.name); /* searching is not working now once the issue is fixed uncomment this line */
// await expect(this.page.getByText(productdata.name).first()).toBeVisible();
}
async searchProduct(productName: string) {
await this.searchInputExact.fill(productName);
await this.page.keyboard.press('Enter');
}
async deleteProduct()
{
await this.firstDeleteIcon.click();
await this.agreeButton.click();
await expect(this.successMessage.first()).toBeVisible();
}
}

View File

@@ -0,0 +1,199 @@
import { Page, expect } from "@playwright/test";
import CoreLocators from "../locator/CoreLocators";
import {
generateDescription,
generateEmail,
generateFullName,
generateName,
generatePhoneNumber,
generateRandomNumericString,
generateSKU,
} from "../utils/faker";
export type QuickAddTab = "Lead" | "Person" | "Organization" | "Product" | "Email";
export type QuickAddLeadData = {
title: string;
description: string;
};
export type QuickAddPersonData = {
name: string;
email: string;
contactNumber: string;
};
export type QuickAddOrganizationData = {
name: string;
};
export type QuickAddProductData = {
name: string;
description: string;
sku: string;
quantity: string;
price: string;
};
export type QuickAddMailData = {
replyTo: string;
subject: string;
body: string;
};
export const quickAddLeadData: QuickAddLeadData = {
title: generateName(),
description: generateDescription(),
};
export const quickAddPersonData: QuickAddPersonData = {
name: generateFullName(),
email: generateEmail(),
contactNumber: generatePhoneNumber(),
};
export const quickAddOrganizationData: QuickAddOrganizationData = {
name: generateName() + " Inc",
};
export const quickAddProductData: QuickAddProductData = {
name: "Product " + generateName(),
description: generateDescription(),
sku: generateSKU(),
quantity: generateRandomNumericString(2, 10, 50),
price: generateRandomNumericString(3, 100, 500),
};
export const quickAddMailData: QuickAddMailData = {
replyTo: generateEmail(),
subject: generateName(),
body: generateDescription().slice(0, 50),
};
export class QuickAddPage extends CoreLocators {
readonly page: Page;
constructor(page: Page) {
super(page);
this.page = page;
}
async openQuickAddModal() {
await this.page.goto("admin/dashboard");
await this.quickAddTrigger.waitFor({ state: "visible" });
await this.quickAddTrigger.click();
await expect(this.quickAddModalTitle).toBeVisible();
}
async selectQuickAddTab(tabLabel: QuickAddTab) {
await this.quickAddTab(tabLabel).click();
}
async submitQuickAdd() {
await this.quickAddSaveButton.click();
}
/**
* Every quick add tab exposed by the seeded admin role must be reachable.
*/
async verifyQuickAddTabs(tabs: QuickAddTab[]) {
await this.openQuickAddModal();
for (const tab of tabs) {
await expect(this.quickAddTab(tab)).toBeVisible();
}
}
async createLead(leadData: QuickAddLeadData) {
await this.openQuickAddModal();
await this.selectQuickAddTab("Lead");
await this.quickAddLeadTitleInput.fill(leadData.title);
await this.quickAddLeadDescriptionTextarea.fill(leadData.description);
await this.submitQuickAdd();
await expect(this.quickAddLeadSuccessMsg).toBeVisible();
await this.page.goto("admin/leads");
await this.searchInput.fill(leadData.title);
await this.page.keyboard.press("Enter");
await expect(this.page.getByText(leadData.title).first()).toBeVisible();
}
async createPerson(personData: QuickAddPersonData) {
await this.openQuickAddModal();
await this.selectQuickAddTab("Person");
await this.quickAddPersonNameInput.fill(personData.name);
await this.quickAddPersonEmailInput.fill(personData.email);
await this.quickAddPersonContactInput.fill(personData.contactNumber);
await this.submitQuickAdd();
await expect(this.quickAddPersonSuccessMsg).toBeVisible();
await this.page.goto("admin/contacts/persons");
await this.searchByName(personData.name);
await expect(this.page.getByText(personData.name).first()).toBeVisible();
}
async createOrganization(organizationData: QuickAddOrganizationData) {
await this.openQuickAddModal();
await this.selectQuickAddTab("Organization");
await this.quickAddOrgNameInput.fill(organizationData.name);
await this.submitQuickAdd();
await expect(this.quickAddOrgSuccessMsg).toBeVisible();
await this.page.goto("admin/contacts/organizations");
await this.searchByName(organizationData.name);
await expect(this.page.getByText(organizationData.name).first()).toBeVisible();
}
async createProduct(productData: QuickAddProductData) {
await this.openQuickAddModal();
await this.selectQuickAddTab("Product");
await this.quickAddProductNameInput.fill(productData.name);
await this.quickAddProductDescriptionTextarea.fill(productData.description);
await this.quickAddProductSkuInput.fill(productData.sku);
await this.quickAddProductQuantityInput.fill(productData.quantity);
await this.quickAddProductPriceInput.fill(productData.price);
await this.submitQuickAdd();
await expect(this.quickAddProductSuccessMsg).toBeVisible();
await this.page.goto("admin/products");
await this.searchByName(productData.name);
await expect(this.page.getByText(productData.name).first()).toBeVisible();
}
async createMail(mailData: QuickAddMailData) {
await this.openQuickAddModal();
await this.selectQuickAddTab("Email");
await this.quickAddMailReplyToInput.fill(mailData.replyTo);
await this.quickAddMailSubjectInput.fill(mailData.subject);
await this.quickAddMailEditorFrame.locator("body").fill(mailData.body);
await this.submitQuickAdd();
await expect(this.quickAddMailSuccessMsg).toBeVisible();
}
/**
* Validation errors must keep the modal mounted instead of closing it.
*/
async submitEmptyForm(tabLabel: QuickAddTab) {
await this.openQuickAddModal();
await this.selectQuickAddTab(tabLabel);
await this.submitQuickAdd();
await expect(this.quickAddModalTitle).toBeVisible();
}
}

View File

@@ -0,0 +1,204 @@
import { expect, Locator, Page } from "@playwright/test";
import CoreLocators from "../locator/CoreLocators";
import { buildProductData, ProductData } from "./ProductPage";
import { generateDescription, generateFullName } from "../utils/faker";
import PersonsPage, { buildPersonData, PersonData } from "./PersonsPage";
export type QuoteData = {
subject: string;
description: string;
salesOwnerId: string;
expiredAt: string; // expiration date string
person: PersonData;
leadName: string;
address: string;
countryCode: string; // e.g. 'IN'
stateCode: string; // e.g. 'DL'
city: string;
postcode: string;
product: ProductData;
// Add quote items type here if needed
};
const quotePersonData: PersonData = buildPersonData();
const quoteProductData: ProductData = buildProductData();
export const quoteData: QuoteData = {
subject: generateFullName(),
description: generateDescription(),
salesOwnerId: "1",
expiredAt: "2026-02-28",
/**
* A person of its own, for the same reason the lead flow has one.
*/
person: quotePersonData,
leadName: "Corporate Website Lead",
address: "Plot 45, Industrial Area",
countryCode: "IN",
stateCode: "DL",
city: "New Delhi",
postcode: "110020",
product: quoteProductData,
// Optionally add quote items array if needed
};
export class QuotesPage extends CoreLocators {
readonly page: Page;
// Navigation links/buttons
readonly createQuoteLink: Locator;
readonly saveQuoteButton: Locator;
// Form fields
readonly subjectTextbox: Locator;
readonly descriptionTextbox: Locator;
readonly salesOwnerSelect: Locator;
readonly expiredAtTextbox: Locator;
// Person search and selection
readonly searchTextbox: Locator;
// Billing address fields
readonly billingAddressTextarea: Locator;
readonly billingCountrySelect: Locator;
readonly billingStateSelect: Locator;
readonly billingCityInput: Locator;
readonly billingPostcodeInput: Locator;
// Shipping address fields
readonly shippingAddressTextarea: Locator;
readonly shippingCountrySelect: Locator;
readonly shippingStateSelect: Locator;
readonly shippingCityInput: Locator;
readonly shippingPostcodeInput: Locator;
// Confirmation/Sucess notification locator
constructor(page: Page) {
super(page);
this.page = page;
this.subjectTextbox = page.getByRole("textbox", { name: "Subject *" });
this.descriptionTextbox = page.getByRole("textbox", { name: "Description" });
this.searchTextbox = page.getByRole("textbox", { name: "Search..." });
this.expiredAtTextbox = page.getByRole("textbox", { name: "Expired At *" });
this.billingAddressTextarea = page.locator('textarea[name="billing_address\\[address\\]"]');
this.shippingAddressTextarea = page.locator('textarea[name="shipping_address\\[address\\]"]');
this.billingCityInput = page.locator('input[name="billing_address\\[city\\]"]');
this.billingPostcodeInput = page.locator('input[name="billing_address\\[postcode\\]"]');
this.shippingCityInput = page.locator('input[name="shipping_address\\[city\\]"]');
this.shippingPostcodeInput = page.locator('input[name="shipping_address\\[postcode\\]"]');
this.salesOwnerSelect = page.getByLabel("Sales Owner");
this.billingCountrySelect = page.locator('select[name="billing_address\\[country\\]"]');
this.billingStateSelect = page.locator('select[name="billing_address\\[state\\]"]');
this.shippingCountrySelect = page.locator('select[name="shipping_address\\[country\\]"]');
this.shippingStateSelect = page.locator('select[name="shipping_address\\[state\\]"]');
this.saveQuoteButton = page.getByRole("button", { name: "Save Quote" });
this.createQuoteLink = page.getByRole("link", { name: "Create Quote" });
this.createQuoteLink = page.getByRole("link", { name: "Create Quote" });
}
async navigateToQuotesPage() {
await this.page.goto("admin/quotes");
}
async createQuote(quoteData: QuoteData) {
await this.navigateToQuotesPage();
// Fill Quote Basics
await this.createQuoteLink.click();
await this.subjectTextbox.fill(quoteData.subject);
await this.descriptionTextbox.fill(quoteData.description);
await this.salesOwnerSelect.selectOption(quoteData.salesOwnerId);
await this.expiredAtTextbox.fill(quoteData.expiredAt);
// Link to Person - Click to add and select person by name
await this.quoteAddPersonButton.click();
await this.searchTextbox.fill(quoteData.person.name);
await this.page.waitForTimeout(1000);
await this.quoteSelectListPerson.click(); // or select existing if applicable
// // Link to Lead - Click to add and select lead by name
// await this.quoteLinkToLeadButton.click();
// await this.searchTextbox.fill(quoteData.leadName);
// await this.addAsNewButton.click(); // or select existing
// Fill Billing Address
await this.billingAddressTextarea.fill(quoteData.address);
await this.billingCountrySelect.selectOption(quoteData.countryCode);
await this.billingStateSelect.selectOption(quoteData.stateCode);
await this.billingCityInput.fill(quoteData.city);
await this.billingPostcodeInput.fill(quoteData.postcode);
// Fill Shipping Address
await this.shippingAddressTextarea.fill(quoteData.address);
await this.shippingCountrySelect.selectOption(quoteData.countryCode);
await this.shippingStateSelect.selectOption(quoteData.stateCode);
await this.shippingCityInput.fill(quoteData.city);
await this.shippingPostcodeInput.fill(quoteData.postcode);
// Quote item filling
await this.page.getByText('Click to Add', { exact: true }).first().click();
await this.page.getByPlaceholder("Search Products").fill(quoteData.product.name);
await this.page.waitForTimeout(1000);
await this.page.getByText(quoteData.product.name, { exact: true }).first().click();
// Save the quote
await this.saveQuoteButton.click();
}
async updateQuote(quoteData:QuoteData)
{
await this.firstEditIcon.click();
await this.subjectTextbox.fill(quoteData.subject);
await this.descriptionTextbox.fill(quoteData.description);
await this.salesOwnerSelect.selectOption(quoteData.salesOwnerId);
await this.expiredAtTextbox.fill(quoteData.expiredAt);
// Link to Person - Click to add and select person by name
await this.quoteAddPersonButton.click();
await this.searchTextbox.fill(quoteData.person.name);
await this.page.waitForTimeout(1000);
await this.quoteSelectListPerson.click(); // or select existing if applicable
// Link to Lead - Click to add and select lead by name
// await this.quoteLinkToLeadButton.click();
// await this.searchTextbox.fill(quoteData.leadName);
// await this.addAsNewButton.click(); // or select existing
// Fill Billing Address
await this.billingAddressTextarea.fill(quoteData.address);
await this.billingCountrySelect.selectOption(quoteData.countryCode);
await this.billingStateSelect.selectOption(quoteData.stateCode);
await this.billingCityInput.fill(quoteData.city);
await this.billingPostcodeInput.fill(quoteData.postcode);
// Save the quote
await this.saveQuoteButton.click();
}
async deleteQuote()
{
await this.firstDeleteIcon.click();
await this.agreeButton.click();
await expect(this.successMessage.first()).toBeVisible();
}
}

View File

@@ -0,0 +1,96 @@
import { Page } from "playwright/test";
import CoreLocators from "../locator/CoreLocators";
export class SettingsPage extends CoreLocators {
readonly page: Page;
constructor(page: Page) {
super(page);
this.page = page;
}
// Warehouses
async navigateToWarehouses() {
await this.page.goto("admin/settings/warehouses");
}
// Automation
async navigateToAutomation() {
await this.page.goto("admin/settings/automation");
}
// Attributes
async navigateToAttributes() {
await this.page.goto("admin/settings/attributes");
}
// Email Templates
async navigateToEmailTemplates() {
await this.page.goto("admin/settings/email-templates");
}
// Events
async navigateToEvents() {
await this.page.goto("admin/settings/marketing/events");
}
// Campaigns
async navigateToCampaigns() {
await this.page.goto("admin/settings/marketing/campaigns");
}
// Webhooks
async navigateToWebhooks() {
await this.page.goto("admin/settings/webhooks");
}
// Workflows
async navigateToWorkflows() {
await this.page.goto("admin/settings/workflows");
}
// Data Transfer
async navigateToDataTransfer() {
await this.page.goto("admin/settings/data-transfer/imports");
}
// Other Settings
async navigateToOtherSettings() {
await this.page.goto("admin/settings/other-settings");
}
// Web Forms
async navigateToWebForms() {
await this.page.goto("admin/settings/web-forms");
}
// Tags
async navigateToTags() {
await this.page.goto("admin/settings/tags");
}
// Groups
async navigateToGroups() {
await this.page.goto("admin/settings/groups");
}
// Roles
async navigateToRoles() {
await this.page.goto("admin/settings/roles");
}
// Users / Agents
async navigateToUsers() {
await this.page.goto("admin/settings/users");
}
// Pipelines
async navigateToPipelines() {
await this.page.goto("admin/settings/pipelines");
}
// Sources
async navigateToSources() {
await this.page.goto("admin/settings/sources");
}
}

View File

@@ -0,0 +1,78 @@
import { expect, Locator, Page } from "playwright/test";
import { SettingsPage } from "../SettingsPage";
import { generateFirstName, generateRandomNumericString } from "../../utils/faker";
export type AttributeData = {
name: string,
code: string,
}
export const attributeData: AttributeData = {
name: generateFirstName(),
code: generateRandomNumericString(),
}
export class AttributesPage extends SettingsPage {
readonly page: Page;
readonly createAttributeLink: Locator;
readonly nameTextbox: Locator;
readonly codeTextbox: Locator;
readonly typeDropdown: Locator;
readonly entityTypeDropdown: Locator;
readonly validationDropdown: Locator;
readonly saveAttributeButton: Locator;
readonly successMessage: Locator;
readonly firstEditButton: Locator;
readonly firstDeleteButton: Locator;
readonly deleteConfirmButton: Locator;
constructor(page: Page) {
super(page);
this.page = page;
this.createAttributeLink = page.getByRole('link', { name: 'Create Attribute' });
this.nameTextbox = page.getByRole('textbox', { name: 'Name' });
this.codeTextbox = page.getByRole('textbox', { name: 'Code' });
this.typeDropdown = page.locator('#type');
this.entityTypeDropdown = page.locator('#entity_type');
this.validationDropdown = page.locator('#validation');
this.saveAttributeButton = page.getByRole('button', { name: 'Save Attribute' });
this.successMessage = page.getByText('Success', { exact: true });
this.firstEditButton = page.locator('//span[contains(@class,"icon-edit")]').first();
this.firstDeleteButton = page.locator('//span[contains(@class,"icon-delete")]').first();
this.deleteConfirmButton = page.getByRole('button', { name: 'Agree', exact: true });
}
async createAttribute(attributeData: AttributeData) {
await this.createAttributeLink.click();
await this.nameTextbox.fill(attributeData.name);
await this.codeTextbox.fill(attributeData.name.toLowerCase());
await this.typeDropdown.selectOption({ value: 'textarea' });
await this.entityTypeDropdown.selectOption({ value: 'leads' });
await this.saveAttributeButton.click();
await this.searchAttribute(attributeData.name);
await expect(this.page.getByText(attributeData.name).first()).toBeVisible();
}
async updateAttribute(attributeData: AttributeData) {
await this.firstEditButton.click();
await this.nameTextbox.fill(attributeData.name);
await this.saveAttributeButton.click();
await this.searchAttribute(attributeData.name);
await expect(this.page.getByText(attributeData.name).first()).toBeVisible();
}
async searchAttribute(name: string) {
await this.searchInputExact.fill(name);
await this.page.keyboard.press('Enter');
}
async deleteAttribute()
{
await this.firstDeleteButton.click();
await this.agreeButton.click();
await expect(this.successMessage.first()).toBeVisible();
}
}

View File

@@ -0,0 +1,13 @@
import { Page } from "playwright/test";
import { SettingsPage } from "../SettingsPage";
export class AutomationPage extends SettingsPage {
readonly page: Page;
constructor(page: Page) {
super(page);
this.page = page;
}
}

View File

@@ -0,0 +1,114 @@
import { Page, Locator, expect } from '@playwright/test';
import { generateFullName, generateLastName, generateRandomNumericString } from '../../utils/faker';
import { SettingsPage } from '../SettingsPage';
import { eventdata, EventData } from './EventsPage';
export type CampaignData = {
name: string;
subject: string;
emailTemplateId: string;
event: EventData;
active: number;
}
export const campaignData: CampaignData = {
name: generateFullName(),
subject: generateLastName(),
emailTemplateId: generateRandomNumericString(),
event: eventdata,
active: 1,
}
export class CampaignsPage extends SettingsPage {
readonly page: Page;
// Campaign locators
readonly createCampaignButton: Locator;
readonly campaignNameInput: Locator;
readonly campaignSubjectInput: Locator;
readonly emailTemplateDropdown: Locator;
readonly eventDropdown: Locator;
readonly activeToggle: Locator;
readonly saveCampaignButton: Locator;
readonly firstCampaignEditButton: Locator;
readonly firstCampaignDeleteButton: Locator;
readonly confirmDeleteButton: Locator;
readonly successCreatedMessage: Locator;
readonly successUpdatedMessage: Locator;
readonly successDeletedMessage: Locator;
constructor(page: Page) {
super(page);
this.page = page;
this.createCampaignButton = page.getByRole('button', { name: 'Create Campaigns' })
this.campaignNameInput = page.locator('input[name="name"]');
this.campaignSubjectInput = page.getByRole('textbox', { name: 'Subject *' });
this.emailTemplateDropdown = page.locator('#marketing_template_id');
this.eventDropdown = page.getByLabel('Event');
this.activeToggle = page.locator('.peer.h-5');
this.saveCampaignButton = page.getByRole('button', { name: 'Save Activity' });
this.firstCampaignEditButton = page.locator('.row > div:nth-child(6) > a').first();
this.firstCampaignDeleteButton = page.locator('xpath=(//span[contains(@class, "icon-delete")])[1]');
this.confirmDeleteButton = page.getByRole('button', { name: 'Agree', exact: true });
this.successCreatedMessage = page.getByText(/(created) successfully\./).first();
this.successUpdatedMessage = page.getByText(/(updated) successfully\./).first();
this.successDeletedMessage = page.getByText(/(deleted) successfully\./).first();
}
async gotoCampaigns() {
await this.page.goto("admin/settings/marketing/campaigns");
}
async createCampaign(data: CampaignData) {
await this.createCampaignButton.click();
await this.campaignNameInput.fill(data.name);
await this.campaignSubjectInput.fill(data.subject);
await this.campaignSubjectInput.click(); // For validation if needed
await this.campaignSubjectInput.fill(data.subject);
await this.eventDropdown.selectOption({ label: data.event.name });
await this.emailTemplateDropdown.selectOption({ value: "1" });
if (data.active) {
// Only toggle if not already active, this might require check logic per app specifics
await this.activeToggle.click();
}
await this.saveCampaignButton.click();
await expect(this.successCreatedMessage).toBeVisible();
await this.searchByName(data.name);
await expect(this.page.getByText(data.name).first()).toBeVisible();
}
async editCampaign(data: CampaignData) {
await this.firstCampaignEditButton.click();
await this.campaignNameInput.fill(data.name);
await this.campaignSubjectInput.fill(data.subject);
await this.campaignSubjectInput.click(); // For validation if needed
await this.campaignSubjectInput.fill(data.subject);
await this.eventDropdown.selectOption({ label: data.event.name });
await this.emailTemplateDropdown.selectOption({ value: "1" });
if (data.active) {
// Only toggle if not already active, this might require check logic per app specifics
await this.activeToggle.click();
}
await this.saveCampaignButton.click();
await expect(this.successUpdatedMessage).toBeVisible();
await this.searchInputExact.fill(data.name);
await this.page.keyboard.press('Enter');
await expect(this.page.getByText(data.name).first()).toBeVisible();
}
async deleteCampaign() {
await this.firstCampaignDeleteButton.click();
await this.confirmDeleteButton.click();
await expect(this.successDeletedMessage).toBeVisible();
}
}

View File

@@ -0,0 +1,134 @@
import { Page, Locator, expect } from '@playwright/test';
import { SettingsPage } from '../SettingsPage';
import path from 'path';
import fs from 'fs';
import { parse } from 'fast-csv';
import { format } from '@fast-csv/format';
export type EntityType = 'leads' | 'products' | 'persons';
export class DataTransferPage extends SettingsPage {
readonly page: Page;
// Locators
readonly importPageUrl = 'admin/settings/data-transfer/imports';
readonly createImportLink: Locator;
readonly fileInput: Locator;
readonly processQueueCheckbox: Locator;
readonly processQueueInput: Locator;
readonly entityTypeSelect: Locator;
readonly saveImportButton: Locator;
readonly validateButton: Locator;
readonly runImportButton: Locator;
readonly importRecordsText: Locator;
readonly editIcon: Locator;
readonly deleteIcon: Locator;
readonly confirmDeleteButton: Locator;
constructor(page: Page) {
super(page);
this.page = page;
this.createImportLink = page.locator('a.primary-button');
this.fileInput = page.locator('input[name="file"]');
this.processQueueCheckbox = page.locator('label[for="process_in_queue"]');
this.processQueueInput = page.locator('input[name="process_in_queue"]');
this.entityTypeSelect = page.locator('#import-type');
this.saveImportButton = page.getByRole('button', { name: 'Save Import' });
this.validateButton = page.locator('//button[contains(.,"Validate")]');
this.runImportButton = page.getByRole('button', { name: 'Import' });
this.importRecordsText = page.getByRole('paragraph').filter({ hasText: /^leads$/ });
this.editIcon = page.locator('.icon-edit').first();
this.deleteIcon = page.locator('.icon-delete').first();
this.confirmDeleteButton = page.getByRole('button', { name: 'Agree', exact: true });
}
async createImport(csvFilePath: string, entityType: EntityType) {
await this.createImportLink.click();
await this.setInputFiles('input[name="file"]', csvFilePath);
await this.uncheckProcessQueue();
await this.entityTypeSelect.selectOption(entityType);
await this.saveImportButton.click();
await this.page.locator('//button[contains(.,"Validate")]').click();
await this.runImportButton.click();
// Wait for success record appearance
await expect(this.successMessage.first()).toBeVisible();
}
async updateImport(csvFilePath: string) {
await this.editIcon.click();
await this.setInputFiles('input[name="file"]', csvFilePath);
await this.uncheckProcessQueue();
await this.saveImportButton.click();
// Validation and import
await this.page.locator('//button[@class="primary-button place-self-start"]').click();
await expect(this.page.getByText('Your import is valid. Click')).toBeVisible();
await this.runImportButton.click();
// Wait for success message
await expect(this.page.getByText('Congratulations! Your import')).toBeVisible();
}
async deleteImport() {
await this.deleteIcon.click();
await this.confirmDeleteButton.click();
// Confirm deletion and presence of 'No Records Available.'
await expect(this.successMessage.first()).toBeVisible();
}
/**
* The import must run synchronously in every test, otherwise the records are
* only queued and the assertions race the queue worker. The switch is toggled
* through its label because the real checkbox is visually hidden, and it is
* clicked only when checked so the state stays deterministic on both the
* create form (off by default) and the edit form (reflects the saved import).
*/
async uncheckProcessQueue() {
await this.processQueueInput.waitFor({ state: 'attached' });
if (await this.processQueueInput.isChecked()) {
await this.processQueueCheckbox.click();
}
await expect(this.processQueueInput).not.toBeChecked();
}
// Utility method for setting files, with fallback to Playwright's method
async setInputFiles(selector: string, filePath: string) {
await this.page.setInputFiles(selector, filePath);
}
async updateCsv(filePath: string,column:string, newOrgId: string): Promise<void> {
return new Promise((resolve, reject) => {
const rows: any[] = [];
fs.createReadStream(filePath)
.pipe(parse({ headers: true }))
.on('error', reject)
.on('data', (row) => {
row[column] = newOrgId; // only update this field
rows.push(row);
})
.on('end', () => {
const writeStream = fs.createWriteStream(filePath);
const csvStream = format({ headers: true });
csvStream
.pipe(writeStream)
.on('finish', resolve)
.on('error', reject);
rows.forEach(row => csvStream.write(row));
csvStream.end();
});
});
}
}

View File

@@ -0,0 +1,69 @@
import { Page, Locator, FrameLocator, expect } from '@playwright/test';
import { SettingsPage } from '../SettingsPage';
import { generateDescription, generateName } from '../../utils/faker';
export type EmailTemplateData = {
subject: string;
content: string;
name: string;
};
export const emailTemplateData: EmailTemplateData = {
subject: generateName(),
content: generateDescription(),
name: generateName(),
};
export class EmailTemplatePage extends SettingsPage {
readonly page: Page;
readonly createButton: Locator;
readonly editButton: Locator;
readonly deleteButton: Locator;
readonly subjectInput: Locator;
readonly contentInput: FrameLocator;
readonly nameInput: Locator;
readonly placeholderSelect: Locator;
readonly saveButton: Locator;
readonly deleteConfirmButton: Locator;
readonly listingUrl: string;
constructor(page: Page) {
super(page);
this.page = page;
this.listingUrl = 'admin/settings/email-templates';
this.createButton = page.getByRole('link', { name: 'Create Email Template' });
this.editButton = page.locator('//span[@class="cursor-pointer rounded-md p-1.5 text-2xl transition-all hover:bg-gray-200 dark:hover:bg-gray-800 max-sm:place-self-center icon-edit"]').first();
this.deleteButton = page.locator('//span[@class="cursor-pointer rounded-md p-1.5 text-2xl transition-all hover:bg-gray-200 dark:hover:bg-gray-800 max-sm:place-self-center icon-delete"]').first();
this.subjectInput = page.getByRole('textbox', { name: 'Subject' });
this.contentInput = page.frameLocator(`iframe[id="content_ifr"]`);
this.nameInput = page.getByRole('textbox', { name: 'Name' });
this.placeholderSelect = page.locator('#placeholder');
this.saveButton = page.getByRole('button', { name: 'Save Email Template' });
this.deleteConfirmButton = page.getByRole('button', { name: 'Delete' });
}
async createTemplate(emailTemplateData: EmailTemplateData) {
await this.createButton.click();
await this.subjectInput.fill(emailTemplateData.subject);
await this.nameInput.fill(emailTemplateData.name);
await this.contentInput.locator('body').fill(emailTemplateData.content);
await this.saveButton.click();
}
async updateEmailTemplate(emailTemplateData: EmailTemplateData) {
await this.editButton.click();
await this.subjectInput.fill(emailTemplateData.subject);
await this.contentInput.locator('body').fill(emailTemplateData.content);
await this.saveButton.click();
}
async deleteEmailTemplate() {
await this.firstDeleteIcon.click();
await this.agreeButton.click();
await expect(this.successMessage.first()).toBeVisible();
}
async searchEmailTemplate(name: string) {
await this.searchInputField.fill(name);
await this.page.keyboard.press('Enter');
}
}

View File

@@ -0,0 +1,82 @@
import { Page, Locator, expect } from '@playwright/test';
import { SettingsPage } from '../SettingsPage';
import { generateDescription, generateFullName, generateRandomDateTime } from '../../utils/faker';
export type EventData = {
name: string,
description: string,
date: string
}
export const eventdata: EventData = {
name: generateFullName(),
description: generateDescription(),
date: "2027-11-18",
}
export class EventsPage extends SettingsPage {
readonly page: Page;
readonly createEventButton: Locator;
readonly nameInput: Locator;
readonly descriptionTextarea: Locator;
readonly dateInput: Locator;
readonly saveEventButton: Locator;
readonly editFirstEventButton: Locator;
readonly deleteFirstEventButton: Locator;
readonly confirmDeleteButton: Locator;
readonly successMessage: Locator;
constructor(page: Page) {
super(page);
this.page = page;
this.createEventButton = page.getByRole('button', { name: 'Create Event' });
this.nameInput = page.locator('input[name="name"]');
this.descriptionTextarea = page.locator('textarea[name="description"]');
this.dateInput = page.locator('input[name="date"]');
this.saveEventButton = page.getByRole('button', { name: 'Save Event' });
this.editFirstEventButton = page.locator('.row > div:nth-child(6) > a').first();
this.deleteFirstEventButton = page.locator('div:nth-child(6) > a:nth-child(2)').first();
this.confirmDeleteButton = page.getByRole('button', { name: 'Agree', exact: true });
this.successMessage = page.getByText(/Event (created|updated|deleted) successfully./);
}
async createEvent(eventdata: EventData) {
await this.createEventButton.click();
await this.nameInput.fill(eventdata.name);
await this.descriptionTextarea.fill(eventdata.name);
await this.dateInput.fill(eventdata.date);
await this.saveEventButton.click();
await expect(this.successMessage).toBeVisible();
}
async massDeleteEvents() {
await this.multiSelectCheckbox.click()
await this.deleteButton.click();
await this.agreeButton.click();
await expect(this.noRecordsAvailable).toBeVisible();
}
async editEvents(eventData: EventData) {
await this.firstEditIcon.click();
await this.nameInput.fill(eventdata.name);
await this.descriptionTextarea.fill(eventdata.name);
await this.dateInput.fill(eventdata.date);
await this.saveEventButton.click();
await expect(this.successMessage).toBeVisible();
}
async deleteEvent() {
await this.deleteFirstEventButton.click();
await this.agreeButton.click();
await expect(this.successMessage).toBeVisible();
}
async eventSearch(name: string) {
await this.searchInputExact.fill(name);
await this.page.keyboard.press('Enter');
}
}

View File

@@ -0,0 +1,71 @@
// groupPage.js
import { expect, Locator, Page } from '@playwright/test';
import { SettingsPage } from '../SettingsPage';
import { generateDescription, generateFirstName, generateName } from '../../utils/faker';
export type GroupData={
name:string,
description:string
}
export const groupData:GroupData={
name: generateFirstName()+" Group",
description: generateDescription().slice(0, 60),
}
export class GroupPage extends SettingsPage {
readonly page:Page;
readonly createGroupButton: Locator;
readonly saveGroupButton: Locator;
readonly nameInput: Locator;
readonly descriptionTextarea: Locator;
readonly successMessageGroupCreated: Locator;
readonly successMessageGroupUpdated: Locator;
constructor(page:Page) {
super(page);
this.page = page;
// Buttons
this.createGroupButton = page.getByRole("button", { name: "Create group" });
this.saveGroupButton = page.getByRole("button", { name: "Save Group" });
// Form fields
this.nameInput = page.locator('input[name="name"]');
this.descriptionTextarea = page.locator('textarea[name="description"]');
// Notifications
this.successMessageGroupCreated = page.getByText("Group created successfully.");
this.successMessageGroupUpdated = page.getByText("Group updated successfully.");
}
async createGroup(groupData:GroupData): Promise<void>
{
await this.createGroupButton.click();
await this.nameInput.fill(groupData.name);
await this.descriptionTextarea.fill(groupData.description);
await this.saveGroupButton.click();
await expect(this.successMessageGroupCreated.first()).toBeVisible();
await this.searchGroup(groupData.name);
await expect(this.page.getByText(groupData.name).first()).toBeVisible();
}
async updateGroup(groupData:GroupData): Promise<void>
{
await this.firstEditIcon.click();
await this.nameInput.fill(groupData.name);
await this.descriptionTextarea.fill(groupData.description);
await this.saveGroupButton.click();
await expect(this.successMessageGroupUpdated.first()).toBeVisible();
await this.searchGroup(groupData.name);
await expect(this.page.getByText(groupData.name).first()).toBeVisible();
}
async deleteGroup(): Promise<void>
{
await this.firstDeleteIcon.click();
await this.agreeButton.click();
await expect(this.successMessage.first()).toBeVisible();
}
async searchGroup(name:string): Promise<void>
{
await this.searchInputExact.fill(name);
await this.page.keyboard.press('Enter');
}
}

View File

@@ -0,0 +1,13 @@
import { Page } from "playwright/test";
import { SettingsPage } from "../SettingsPage";
export class OtherSettingsPage extends SettingsPage {
readonly page: Page;
constructor(page: Page) {
super(page);
this.page = page;
}
}

View File

@@ -0,0 +1,13 @@
import { Page } from "playwright/test";
import { SettingsPage } from "../SettingsPage";
export class PipelinesPage extends SettingsPage {
readonly page: Page;
constructor(page: Page) {
super(page);
this.page = page;
}
}

View File

@@ -0,0 +1,212 @@
import { expect, Locator, Page } from "@playwright/test";
import { SettingsPage } from "../SettingsPage";
import { generateDescription, generateFirstName } from "../../utils/faker";
import { PermissionType, runSuffix } from "../../utils/acl";
export type RoleData = {
name: string;
description: string;
permissionType: PermissionType;
/**
* ACL keys to tick in the access-control tree. Only read when `permissionType` is `custom`.
*/
permissions?: string[];
};
/**
* Build role data with a unique name, so repeated runs never collide in the listing.
*/
export function buildRoleData(overrides: Partial<RoleData> = {}): RoleData {
return {
name: `${generateFirstName()} Role ${runSuffix()}`,
description: generateDescription().slice(0, 60),
permissionType: "custom",
permissions: [],
...overrides,
};
}
export class RolesPage extends SettingsPage {
readonly page: Page;
readonly createRoleButton: Locator;
readonly saveRoleButton: Locator;
readonly nameInput: Locator;
readonly descriptionTextarea: Locator;
readonly permissionTypeSelect: Locator;
readonly createSuccessMessage: Locator;
readonly updateSuccessMessage: Locator;
readonly deleteSuccessMessage: Locator;
readonly permissionsExceedOwnError: Locator;
readonly currentRoleEditError: Locator;
readonly currentRoleDeleteError: Locator;
readonly beingUsedError: Locator;
constructor(page: Page) {
super(page);
this.page = page;
this.createRoleButton = page.getByRole("link", { name: "Create Roles" });
this.saveRoleButton = page.getByRole("button", { name: "Save Role" });
this.nameInput = page.locator('input[name="name"]');
this.descriptionTextarea = page.locator('textarea[name="description"]');
this.permissionTypeSelect = page.locator('select[name="permission_type"]');
this.createSuccessMessage = page.getByText("Role created successfully.");
this.updateSuccessMessage = page.getByText("Role updated successfully.");
this.deleteSuccessMessage = page.getByText("Role deleted successfully.");
this.permissionsExceedOwnError = page.getByText(
"You can only create or edit roles whose permissions are the same as or fewer than your own."
);
this.currentRoleEditError = page.getByText("Can not edit the role assigned to your own account.");
this.currentRoleDeleteError = page.getByText("Can not delete role assigned to the current user.");
this.beingUsedError = page.getByText("Role can not be deleted, as this is being used in admin user.");
}
/**
* The access-control tree renders each ACL key as a hidden checkbox carrying the key as its
* `value`.
*
* The `id` is not usable as an anchor: the tree component prefixes it with a token regenerated
* on every render (`mttr4p8q_leads.edit`), so only `value` is stable across page loads. The
* input itself is `hidden`, so the wrapping label is what a user actually clicks.
*/
permissionCheckbox(key: string): Locator {
return this.page.locator(`input[name="permissions[]"][value="${key}"]`);
}
permissionLabel(key: string): Locator {
return this.page.locator(`label:has(input[name="permissions[]"][value="${key}"])`);
}
async navigateToCreateRole(): Promise<void> {
await this.page.goto("admin/settings/roles/create");
}
/**
* Tick an ACL key in the tree.
*
* Selection is hierarchical in both directions: ticking a key also ticks its ancestors AND its
* whole subtree. Ticking `leads` therefore grants `leads.delete` too.
*
* Pass only the most specific keys a role should hold and let the ancestors follow — listing a
* parent alongside its child silently widens the role far beyond what the test intended.
*/
async selectPermission(key: string): Promise<void> {
const label = this.permissionLabel(key);
await expect(label, `permission "${key}" should be offered in the tree`).toBeVisible();
if (!(await this.permissionCheckbox(key).isChecked())) {
await label.click();
}
}
async deselectPermission(key: string): Promise<void> {
if (await this.permissionCheckbox(key).isChecked()) {
await this.permissionLabel(key).click();
}
}
async selectPermissions(keys: string[]): Promise<void> {
for (const key of keys) {
await this.selectPermission(key);
}
}
/**
* Whether an ACL key is offered at all. `Acl::getAuthorizedItems()` prunes the tree to the
* acting user's own permissions, so a key being absent is itself the assertion in the
* escalation suites.
*/
async isPermissionOffered(key: string): Promise<boolean> {
return (await this.permissionLabel(key).count()) > 0;
}
async fillRoleForm(role: RoleData): Promise<void> {
await this.nameInput.fill(role.name);
await this.descriptionTextarea.fill(role.description);
await this.permissionTypeSelect.selectOption(role.permissionType);
if (role.permissionType === "custom") {
await this.selectPermissions(role.permissions ?? []);
}
}
/**
* Create a role through the form and confirm it lands in the listing.
*/
async createRole(role: RoleData): Promise<void> {
await this.navigateToCreateRole();
await this.fillRoleForm(role);
await this.saveRoleButton.click();
await expect(this.createSuccessMessage.first()).toBeVisible();
await this.searchRole(role.name);
await expect(this.page.getByText(role.name).first()).toBeVisible();
}
/**
* Submit the create form without asserting the outcome, for cases where the expected result is
* a refusal rather than a new role.
*/
async submitRoleForm(role: RoleData): Promise<void> {
await this.navigateToCreateRole();
await this.fillRoleForm(role);
await this.saveRoleButton.click();
}
async searchRole(name: string): Promise<void> {
await this.navigateToRoles();
await this.searchInputExact.fill(name);
await this.page.keyboard.press("Enter");
await this.page.waitForLoadState("networkidle");
}
/**
* Open a named role's edit form by clicking its row's edit icon after searching for it, so the
* test never depends on the role's position in an unfiltered listing.
*/
async openRoleForEdit(name: string): Promise<void> {
await this.searchRole(name);
await this.firstEditIcon.click();
await this.page.waitForLoadState("networkidle");
}
async updateRole(name: string, changes: Partial<RoleData>): Promise<void> {
await this.openRoleForEdit(name);
if (changes.name) {
await this.nameInput.fill(changes.name);
}
if (changes.description) {
await this.descriptionTextarea.fill(changes.description);
}
if (changes.permissionType) {
await this.permissionTypeSelect.selectOption(changes.permissionType);
}
if (changes.permissions?.length) {
await this.selectPermissions(changes.permissions);
}
await this.saveRoleButton.click();
}
async deleteRole(name: string): Promise<void> {
await this.searchRole(name);
await this.firstDeleteIcon.click();
await this.agreeButton.click();
}
}

View File

@@ -0,0 +1,39 @@
import { expect, Locator, Page } from "playwright/test";
import { SettingsPage } from "../SettingsPage";
export class SourcesPage extends SettingsPage {
readonly page: Page;
readonly createSourceLink:Locator;
readonly saveSourceButton:Locator;
readonly thisName:Locator;
constructor(page: Page) {
super(page);
this.page = page;
this.createSourceLink=page.getByRole('button', { name: 'Create Source' });
this.saveSourceButton=page.getByRole('button', { name: 'Save Source' })
this.thisName=page.getByRole('textbox', { name: 'Name' });
}
async searchSource(thisName:string)
{
await this.searchInputExact.fill(thisName)
await this.page.keyboard.press('Enter');
}
async sourceForm(sourceName:string)
{
await this.thisName.fill(sourceName);
await this.saveSourceButton.click();
await expect(this.successMessage.first()).toBeVisible();
await this.searchSource(sourceName);
await expect(this.page.getByRole('paragraph').filter({ hasText: sourceName }).first()).toBeVisible();
}
async deleteSource()
{
await this.firstDeleteIcon.click()
await this.agreeButton.click();
await expect(this.successMessage.first()).toBeVisible();
}
}

View File

@@ -0,0 +1,64 @@
import { expect, Page, Locator } from '@playwright/test';
import { generateName } from '../../utils/faker';
import { SettingsPage } from '../SettingsPage';
export class TagsPage extends SettingsPage {
readonly page: Page;
// Locators
readonly gotoTagsPageButton: Locator;
readonly createTagButton: Locator;
readonly tagNameInput: Locator;
readonly saveTagButton: Locator;
readonly tagsUpdatedMessage: Locator;
readonly tagsCreatedMessage: Locator;
readonly tagsDeletedMessage: Locator;
readonly firstEditButton: Locator;
readonly firstDeleteButton: Locator;
readonly confirmDeleteButton: Locator;
constructor(page: Page) {
super(page);
this.page = page;
// Initialize locators
this.gotoTagsPageButton = page.getByRole('link', { name: /Tags/i });
this.createTagButton = page.getByRole('button', { name: 'Create Tag' });
this.tagNameInput = page.getByRole('textbox', { name: 'Name' });
this.saveTagButton = page.getByRole('button', { name: 'Save Tag' });
this.tagsCreatedMessage = page.getByText(/Tag created successfully\./);
this.tagsUpdatedMessage = page.getByText(/Tag updated successfully\./);
this.tagsDeletedMessage = page.getByText(/Tag deleted successfully\./);
this.firstEditButton = page.locator('//span[@class="icon-edit cursor-pointer rounded-md p-1.5 text-2xl transition-all hover:bg-gray-200 dark:hover:bg-gray-800 max-sm:place-self-center"]').first();
this.firstDeleteButton = page.locator('//span[@class="icon-delete cursor-pointer rounded-md p-1.5 text-2xl transition-all hover:bg-gray-200 dark:hover:bg-gray-800 max-sm:place-self-center"]').first();
this.confirmDeleteButton = page.getByRole('button', { name: 'Agree', exact: true });
}
async createTag(name:string) {
await this.createTagButton.click();
await this.tagNameInput.fill(name);
await this.saveTagButton.click();
await expect(this.tagsCreatedMessage).toBeVisible();
}
async udpateTag(newName: string) {
await this.firstEditButton.click();
await this.tagNameInput.fill(newName);
await this.saveTagButton.click();
await expect(this.tagsUpdatedMessage).toBeVisible();
}
async deleteTag() {
await this.firstDeleteButton.click();
await this.confirmDeleteButton.click();
await expect(this.tagsDeletedMessage).toBeVisible();
}
async seacrhTag(name:string): Promise<void>
{
await this.searchInputExact.fill(name);
await this.page.keyboard.press('Enter');
}
// Optional: Any utility methods like search, verify list, etc., can be added here.
}

View File

@@ -0,0 +1,236 @@
import { expect, Locator, Page } from "@playwright/test";
import { SettingsPage } from "../SettingsPage";
import { generateEmail, generateFullName } from "../../utils/faker";
import { TEST_PASSWORD, ViewPermission, runSuffix } from "../../utils/acl";
export type UserData = {
name: string;
email: string;
password: string;
roleName: string;
viewPermission: ViewPermission;
/**
* Group names to associate. Required by the form when `viewPermission` is `group`.
*/
groups?: string[];
status?: boolean;
};
/**
* Build user data with a unique email, so a re-run never trips the unique constraint.
*/
export function buildUserData(overrides: Partial<UserData> = {}): UserData {
const suffix = runSuffix();
return {
name: `${generateFullName()} ${suffix}`,
email: `acl.${suffix}.${generateEmail()}`,
password: TEST_PASSWORD,
roleName: "Administrator",
viewPermission: "global",
groups: [],
status: true,
...overrides,
};
}
export class UsersPage extends SettingsPage {
readonly page: Page;
readonly createUserButton: Locator;
readonly saveUserButton: Locator;
readonly nameInput: Locator;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly confirmPasswordInput: Locator;
readonly roleSelect: Locator;
readonly viewPermissionSelect: Locator;
readonly groupSelect: Locator;
readonly statusCheckbox: Locator;
readonly createSuccessMessage: Locator;
readonly updateSuccessMessage: Locator;
readonly deleteSuccessMessage: Locator;
readonly unauthorizedMessage: Locator;
readonly roleExceedsOwnMessage: Locator;
readonly scopeExceedsOwnMessage: Locator;
readonly ownPrivilegesMessage: Locator;
constructor(page: Page) {
super(page);
this.page = page;
this.createUserButton = page.getByRole("button", { name: "Create User" });
this.saveUserButton = page.getByRole("button", { name: "Save User" });
this.nameInput = page.locator('input[name="name"]');
this.emailInput = page.locator('input[name="email"]');
this.passwordInput = page.locator('input[name="password"]');
this.confirmPasswordInput = page.locator('input[name="confirm_password"]');
this.roleSelect = page.locator('select[name="role_id"]');
this.viewPermissionSelect = page.locator('select[name="view_permission"]');
this.groupSelect = page.locator('select[name="groups[]"]');
/**
* The form ships a hidden `status` input carrying `0` alongside the real checkbox, so the
* name alone is ambiguous — the id is what identifies the control the user toggles.
*/
this.statusCheckbox = page.locator('input#status[type="checkbox"]');
this.createSuccessMessage = page.getByText("User created successfully.");
this.updateSuccessMessage = page.getByText("User updated successfully.");
this.deleteSuccessMessage = page.getByText("User deleted successfully.");
this.unauthorizedMessage = page.getByText("This action is unauthorized.");
this.roleExceedsOwnMessage = page.getByText(
"You can only assign a role whose permissions are the same as or fewer than your own."
);
this.scopeExceedsOwnMessage = page.getByText("You cannot grant a data scope wider than your own.");
this.ownPrivilegesMessage = page.getByText("You cannot change your own role or data scope.");
}
async openCreateModal(): Promise<void> {
await this.navigateToUsers();
await this.createUserButton.click();
await expect(this.nameInput).toBeVisible();
}
async fillUserForm(user: UserData): Promise<void> {
await this.nameInput.fill(user.name);
await this.emailInput.fill(user.email);
await this.passwordInput.fill(user.password);
await this.confirmPasswordInput.fill(user.password);
await this.roleSelect.selectOption({ label: user.roleName });
await this.viewPermissionSelect.selectOption(user.viewPermission);
/**
* The group field is only rendered for the `group` and `individual` scopes, and is
* mandatory for `group`.
*/
if (user.groups?.length) {
await this.groupSelect.selectOption(user.groups.map((label) => ({ label })));
}
/**
* Status is set explicitly rather than only when disabling: the create modal renders the
* toggle unchecked, so a user left at the default is created inactive and cannot sign in
* at all — which would fail every downstream authorization assertion for the wrong reason.
*/
await this.setStatus(user.status ?? true);
}
/**
* Toggle the status control to a known state rather than blind-clicking, so a form that
* already defaults to the wanted value is not flipped away from it.
*/
async setStatus(active: boolean): Promise<void> {
/**
* The checkbox is `sr-only`, so it is toggled through its own element with `force` rather
* than by a normal click, which Playwright would refuse as targeting a hidden element.
*/
if (active) {
await this.statusCheckbox.check({ force: true });
} else {
await this.statusCheckbox.uncheck({ force: true });
}
}
/**
* Create a user through the modal and confirm the success flash.
*/
async createUser(user: UserData): Promise<void> {
await this.openCreateModal();
await this.fillUserForm(user);
await this.saveUserButton.click();
await expect(this.createSuccessMessage.first()).toBeVisible();
}
/**
* Submit the modal without asserting success, for cases where a refusal is the expectation.
*/
async submitUserForm(user: UserData): Promise<void> {
await this.openCreateModal();
await this.fillUserForm(user);
await this.saveUserButton.click();
}
async searchUser(term: string): Promise<void> {
await this.navigateToUsers();
await this.searchInputExact.fill(term);
await this.page.keyboard.press("Enter");
await this.page.waitForLoadState("networkidle");
}
async openUserForEdit(term: string): Promise<void> {
await this.searchUser(term);
await this.firstEditIcon.click();
await expect(this.nameInput).toBeVisible();
}
/**
* Change an existing user's role and/or scope through the edit modal.
*/
async updateUser(term: string, changes: Partial<UserData>): Promise<void> {
await this.openUserForEdit(term);
if (changes.name) {
await this.nameInput.fill(changes.name);
}
if (changes.roleName) {
await this.roleSelect.selectOption({ label: changes.roleName });
}
if (changes.viewPermission) {
await this.viewPermissionSelect.selectOption(changes.viewPermission);
}
if (changes.groups?.length) {
await this.groupSelect.selectOption(changes.groups.map((label) => ({ label })));
}
if (changes.status !== undefined) {
await this.setStatus(changes.status);
}
await this.saveUserButton.click();
}
/**
* Deactivate a user, which the middleware treats as a session-level revocation on that user's
* next request.
*/
async deactivateUser(term: string): Promise<void> {
await this.updateUser(term, { status: false });
await expect(this.updateSuccessMessage.first()).toBeVisible();
}
async deleteUser(term: string): Promise<void> {
await this.searchUser(term);
await this.firstDeleteIcon.click();
await this.agreeButton.click();
}
/**
* Role names offered in the assignment dropdown. A non-administrator must never be offered a
* role broader than their own.
*/
async offeredRoleNames(): Promise<string[]> {
return this.roleSelect.locator("option").allInnerTexts();
}
/**
* Scope values offered in the view-permission dropdown.
*/
async offeredScopes(): Promise<string[]> {
return this.viewPermissionSelect.locator("option").evaluateAll((options) =>
options.map((option) => (option as HTMLOptionElement).value)
);
}
}

View File

@@ -0,0 +1,104 @@
// pages/WarehousePage.ts
import { Page, Locator, expect } from '@playwright/test';
import { SettingsPage } from '../SettingsPage';
import { generateDescription, generateEmail, generateLastName, generateLocation, generateName, generatePhoneNumber, generateRandomNumericString } from '../../utils/faker';
export type WarehouseData = {
name: string;
description: string;
contactName: string;
contactEmail: string;
contactAddress: string;
country: string;
state: string;
city: string;
zipcode: string;
phone: string;
};
export const warehouseData: WarehouseData = {
name: generateLastName(),
description: generateDescription().slice(0,20),
contactName: generateLastName(),
contactEmail:generateEmail(),
contactAddress: 'Plot 123, Industrial Area, Meerut, UP 250001',
country: 'India',
state: 'Uttar Pradesh',
city: 'Meerut',
zipcode: '250001',
phone: generatePhoneNumber(),
};
export class WarehousePage extends SettingsPage {
readonly page: Page;
readonly warehouseCreateLink: Locator;
readonly warehouseNameInput: Locator;
readonly warehouseAddressInput: Locator;
readonly saveButton: Locator;
readonly firstEditButton: Locator;
readonly firstDeleteButton: Locator;
readonly successMessage: Locator;
readonly warehousesTable: Locator;
readonly contactNameInput: Locator;
readonly contactEmailInput: Locator;
readonly contactPhoneInput: Locator;
readonly contactAddressTextarea: Locator;
readonly countrySelect: Locator;
readonly stateSelect: Locator;
readonly cityInput: Locator;
readonly zipcodeInput: Locator;
readonly warehouseDescriptionInput: Locator;
constructor(page: Page) {
super(page);
this.page = page;
this.warehouseCreateLink = page.getByRole('link', { name: 'Create Warehouse' })
this.warehouseNameInput = page.getByRole('textbox', { name: 'Name *', exact: true });
this.warehouseAddressInput = page.locator('textarea[name="contact_address[address]"]');
this.saveButton = page.getByRole('button', { name: 'Save Warehouse' })
this.firstEditButton = page.locator('span.cursor-pointer.icon-edit').first();
this.firstDeleteButton = page.locator('span.cursor-pointer.icon-delete').first();
this.successMessage = page.getByText(/Warehouse.*successfully/);
this.warehousesTable = page.getByRole('table');
this.contactNameInput = page.getByRole('textbox', { name: 'Contact Name *' });
this.contactEmailInput = page.getByRole('textbox', { name: 'Contact Emails *' })
this.contactPhoneInput = page.getByRole('textbox', { name: 'Contact Numbers' })
this.contactAddressTextarea = page.locator('textarea[name="contact_address[address]"]');
this.countrySelect = page.locator('select[name="contact_address[country]"]');
this.stateSelect = page.locator('select[name="contact_address[state]"]');
this.cityInput = page.getByRole('textbox', { name: 'City' });
this.zipcodeInput = page.getByRole('textbox', { name: 'Postcode' });
this.warehouseDescriptionInput = page.getByRole('textbox', { name: 'Description' });
}
async warehouseForm(warehouseData: WarehouseData) {
await this.warehouseNameInput.fill(warehouseData.name);
await this.warehouseDescriptionInput.fill(warehouseData.description);
await this.warehouseAddressInput.fill(warehouseData.contactAddress);
await this.contactNameInput.fill(warehouseData.contactName);
await this.contactEmailInput.fill(warehouseData.contactEmail);
await this.contactPhoneInput.fill(warehouseData.phone);
await this.contactAddressTextarea.fill(warehouseData.contactAddress);
await this.countrySelect.selectOption({ label: warehouseData.country });
await this.stateSelect.selectOption({ label: warehouseData.state });
await this.cityInput.fill(warehouseData.city);
await this.zipcodeInput.fill(warehouseData.zipcode);
await this.saveButton.click();
await expect(this.successMessage.first()).toBeVisible();
await this.searchWarehouse(warehouseData.contactName);
await expect(this.page.getByRole('paragraph').filter({ hasText: warehouseData.contactName }).first()).toBeVisible();
}
async searchWarehouse(warehouseName:string)
{
await this.searchInputExact.fill(warehouseName);
await this.page.keyboard.press('Enter');
}
async deleteWarehouse()
{
await this.firstDeleteButton.click()
await this.agreeButton.click();
await expect(this.successMessage.first()).toBeVisible();
}
}

View File

@@ -0,0 +1,13 @@
import { Page } from "playwright/test";
import { SettingsPage } from "../SettingsPage";
export class WebFormsPage extends SettingsPage {
readonly page: Page;
constructor(page: Page) {
super(page);
this.page = page;
}
}

View File

@@ -0,0 +1,100 @@
import { Page, Locator, expect } from '@playwright/test';
import { SettingsPage } from '../SettingsPage';
import { generateDescription, generateFirstName } from '../../utils/faker';
export type WebhookData = {
url: string;
name: string;
description: string;
entity: string;
};
export const webhookData: WebhookData = {
url: 'https://example.com/webhook',
name: generateFirstName(),
description: generateDescription().slice(0, 50),
entity: 'persons',
};
export class WebhookPage extends SettingsPage {
readonly page: Page;
readonly createButton: Locator;
readonly editButton: Locator;
readonly deleteButton: Locator;
readonly urlEndpointInput: Locator;
readonly nameInput: Locator;
readonly descriptionInput: Locator;
readonly entityTypeSelect: Locator;
readonly queryParamKeyInput: Locator;
readonly queryParamValueInput: Locator;
readonly contentTypeRadio: Locator;
readonly payloadKeyInput: Locator;
readonly payloadValueInput: Locator;
readonly saveButton: Locator;
readonly deleteConfirmButton: Locator;
readonly listingUrl: string;
constructor(page: Page) {
super(page);
this.page = page;
this.listingUrl = 'admin/settings/webhooks';
// Main actions
this.createButton = page.getByRole('link', { name: 'Create Webhook' });
this.editButton = page.locator('(//span[@class="cursor-pointer rounded-md p-1.5 text-2xl transition-all hover:bg-gray-200 dark:hover:bg-gray-800 max-sm:place-self-center icon-edit"])[1]');
this.deleteButton = page.locator('(//span[@class="cursor-pointer rounded-md p-1.5 text-2xl transition-all hover:bg-gray-200 dark:hover:bg-gray-800 max-sm:place-self-center icon-delete"])[1]');
// Form fields
this.urlEndpointInput = page.getByRole('textbox', { name: 'Url Endpoint' });
this.nameInput = page.getByRole('textbox', { name: 'Name' });
this.descriptionInput = page.getByRole('textbox', { name: 'Description' });
this.entityTypeSelect = page.locator('#entity_type');
this.queryParamKeyInput = page.locator('input[name="query_params[0][key]"]');
this.queryParamValueInput = page.locator('[id="query_params[0][value]"]');
this.contentTypeRadio = page.getByRole('radio', { name: 'x-www-form-urlencoded' });
this.payloadKeyInput = page.locator('input[name="payload[0][key]"]');
this.payloadValueInput = page.locator('input[name="payload[0][value]"]');
this.saveButton = page.getByRole('button', { name: 'Save Webhook' });
this.deleteConfirmButton = page.getByRole('button', { name: 'Agree', exact: true });
}
async createWebhook(webhookData: WebhookData) {
await this.createButton.click();
await this.urlEndpointInput.fill(webhookData.url);
await this.nameInput.fill(webhookData.name);
await this.entityTypeSelect.selectOption(webhookData.entity);
await this.descriptionInput.fill(webhookData.description);
await this.queryParamKeyInput.fill('key');
await this.queryParamValueInput.fill('value');
await this.contentTypeRadio.check();
await this.payloadKeyInput.fill('key');
await this.payloadValueInput.fill('value');
await this.saveButton.click();
await expect(this.successMessage.first()).toBeVisible();
}
async updateFirstWebhook(webhookData: WebhookData) {
await this.editButton.click();
await this.urlEndpointInput.fill(webhookData.url);
await this.nameInput.fill(webhookData.name);
await this.descriptionInput.fill(webhookData.description);
await this.entityTypeSelect.selectOption(webhookData.entity);
await this.queryParamKeyInput.fill('updatedKey');
await this.queryParamValueInput.fill('updatedValue');
await this.payloadKeyInput.fill('updatedPayloadKey');
await this.payloadValueInput.fill('updatedPayloadValue');
await this.saveButton.click();
await this.searchWebhook(webhookData.name);
await expect(this.page.locator('p.break-words', { hasText: webhookData.url }).first()).toBeVisible();
}
async deleteWebhook() {
await this.deleteButton.click();
await this.deleteConfirmButton.click();
await expect(this.successMessage.first()).toBeVisible();
}
async searchWebhook(name: string) {
await this.searchInputExact.fill(name);
await this.page.keyboard.press('Enter');
}
}

View File

@@ -0,0 +1,13 @@
import { Page } from "playwright/test";
import { SettingsPage } from "../SettingsPage";
export class WorkflowsPage extends SettingsPage {
readonly page: Page;
constructor(page: Page) {
super(page);
this.page = page;
}
}