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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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