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:
@@ -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