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
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:
146
packages/Webkul/Admin/tests/e2e-pw/pages/AclPage.ts
Normal file
146
packages/Webkul/Admin/tests/e2e-pw/pages/AclPage.ts
Normal 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)}`;
|
||||
}
|
||||
}
|
||||
47
packages/Webkul/Admin/tests/e2e-pw/pages/AdminPage.ts
Normal file
47
packages/Webkul/Admin/tests/e2e-pw/pages/AdminPage.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
168
packages/Webkul/Admin/tests/e2e-pw/pages/LeadPage.ts
Normal file
168
packages/Webkul/Admin/tests/e2e-pw/pages/LeadPage.ts
Normal 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');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
139
packages/Webkul/Admin/tests/e2e-pw/pages/MailPage.ts
Normal file
139
packages/Webkul/Admin/tests/e2e-pw/pages/MailPage.ts
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
99
packages/Webkul/Admin/tests/e2e-pw/pages/OrganizationPage.ts
Normal file
99
packages/Webkul/Admin/tests/e2e-pw/pages/OrganizationPage.ts
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
121
packages/Webkul/Admin/tests/e2e-pw/pages/PersonsPage.ts
Normal file
121
packages/Webkul/Admin/tests/e2e-pw/pages/PersonsPage.ts
Normal 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();
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
86
packages/Webkul/Admin/tests/e2e-pw/pages/ProductPage.ts
Normal file
86
packages/Webkul/Admin/tests/e2e-pw/pages/ProductPage.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
199
packages/Webkul/Admin/tests/e2e-pw/pages/QuickAddPage.ts
Normal file
199
packages/Webkul/Admin/tests/e2e-pw/pages/QuickAddPage.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
204
packages/Webkul/Admin/tests/e2e-pw/pages/QuotesPage.ts
Normal file
204
packages/Webkul/Admin/tests/e2e-pw/pages/QuotesPage.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
96
packages/Webkul/Admin/tests/e2e-pw/pages/SettingsPage.ts
Normal file
96
packages/Webkul/Admin/tests/e2e-pw/pages/SettingsPage.ts
Normal 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");
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
212
packages/Webkul/Admin/tests/e2e-pw/pages/settings/RolesPage.ts
Normal file
212
packages/Webkul/Admin/tests/e2e-pw/pages/settings/RolesPage.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
236
packages/Webkul/Admin/tests/e2e-pw/pages/settings/UsersPage.ts
Normal file
236
packages/Webkul/Admin/tests/e2e-pw/pages/settings/UsersPage.ts
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user