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:
276
packages/Webkul/Admin/tests/e2e-pw/utils/acl.ts
Normal file
276
packages/Webkul/Admin/tests/e2e-pw/utils/acl.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
import { Browser, BrowserContext, expect, Page, APIResponse } from "@playwright/test";
|
||||
|
||||
import { AdminPage } from "../pages/AdminPage";
|
||||
|
||||
/**
|
||||
* Shared vocabulary and helpers for the authorization suites.
|
||||
*
|
||||
* Krayin authorization has two orthogonal dimensions and the helpers below are split along the
|
||||
* same seam:
|
||||
*
|
||||
* - ACL -> which actions a role may perform (roles.permission_type + roles.permissions)
|
||||
* - View scope -> whose records those actions touch (users.view_permission)
|
||||
*
|
||||
* Everything here works against a signed-in browser context so a probe carries the same session
|
||||
* cookies the UI would, which is the only way a direct-request test proves anything about the
|
||||
* middleware.
|
||||
*/
|
||||
|
||||
export const ADMIN_EMAIL = "admin@example.com";
|
||||
export const ADMIN_PASSWORD = "admin123";
|
||||
|
||||
/**
|
||||
* Password handed to every user these suites create. Kept above Krayin's six character minimum.
|
||||
*/
|
||||
export const TEST_PASSWORD = "password123";
|
||||
|
||||
export type ViewPermission = "global" | "group" | "individual";
|
||||
|
||||
export type PermissionType = "all" | "custom";
|
||||
|
||||
/**
|
||||
* ACL keys used across the suites, named rather than repeated as string literals so a rename in
|
||||
* `Admin/Config/acl.php` breaks in one place.
|
||||
*/
|
||||
export const ACL = {
|
||||
dashboard: "dashboard",
|
||||
|
||||
leads: "leads",
|
||||
leadsCreate: "leads.create",
|
||||
leadsView: "leads.view",
|
||||
leadsEdit: "leads.edit",
|
||||
leadsDelete: "leads.delete",
|
||||
|
||||
quotes: "quotes",
|
||||
quotesCreate: "quotes.create",
|
||||
|
||||
contacts: "contacts",
|
||||
contactsPersons: "contacts.persons",
|
||||
contactsPersonsCreate: "contacts.persons.create",
|
||||
contactsPersonsView: "contacts.persons.view",
|
||||
contactsPersonsQuickCreate: "contacts.persons.create.quick-create",
|
||||
contactsPersonsDelete: "contacts.persons.delete",
|
||||
|
||||
settings: "settings",
|
||||
settingsGroups: "settings.user.groups",
|
||||
settingsRoles: "settings.user.roles",
|
||||
settingsRolesCreate: "settings.user.roles.create",
|
||||
settingsRolesEdit: "settings.user.roles.edit",
|
||||
settingsRolesDelete: "settings.user.roles.delete",
|
||||
settingsUsers: "settings.user.users",
|
||||
settingsUsersCreate: "settings.user.users.create",
|
||||
settingsUsersEdit: "settings.user.users.edit",
|
||||
settingsUsersDelete: "settings.user.users.delete",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Admin routes exercised by the direct-request probes, paired with the ACL key that must guard
|
||||
* them. Paths are relative so they resolve against the configured `baseURL`.
|
||||
*/
|
||||
export const GUARDED_ROUTES: Array<{ path: string; permission: string; label: string }> = [
|
||||
{ path: "admin/leads", permission: ACL.leads, label: "lead listing" },
|
||||
{ path: "admin/leads/create", permission: ACL.leadsCreate, label: "lead create form" },
|
||||
{ path: "admin/quotes", permission: ACL.quotes, label: "quote listing" },
|
||||
{ path: "admin/quotes/create", permission: ACL.quotesCreate, label: "quote create form" },
|
||||
{ path: "admin/contacts/persons", permission: ACL.contactsPersons, label: "person listing" },
|
||||
{ path: "admin/settings/roles", permission: ACL.settingsRoles, label: "role listing" },
|
||||
{ path: "admin/settings/roles/create", permission: ACL.settingsRolesCreate, label: "role create form" },
|
||||
{ path: "admin/settings/users", permission: ACL.settingsUsers, label: "user listing" },
|
||||
{ path: "admin/settings/groups", permission: ACL.settingsGroups, label: "group listing" },
|
||||
];
|
||||
|
||||
/**
|
||||
* Routes every authenticated user may reach regardless of role, mirroring the middleware's
|
||||
* allow-list. A regression that widens the allow-list shows up as a route appearing here that
|
||||
* should not.
|
||||
*/
|
||||
export const ALLOWLISTED_ROUTES = [
|
||||
"admin/account",
|
||||
];
|
||||
|
||||
export type Session = {
|
||||
context: BrowserContext;
|
||||
page: Page;
|
||||
email: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sign in as an arbitrary user in a brand new browser context.
|
||||
*
|
||||
* A separate context per user is deliberate: the admin fixture's storage state must not leak into
|
||||
* a test that is meant to act as a restricted user, and reusing one context would let an earlier
|
||||
* session's cookies answer for a later assertion.
|
||||
*/
|
||||
export async function signInAs(
|
||||
browser: Browser,
|
||||
baseURL: string | undefined,
|
||||
email: string,
|
||||
password: string
|
||||
): Promise<Session> {
|
||||
const context = await browser.newContext({ baseURL });
|
||||
const page = await context.newPage();
|
||||
|
||||
await new AdminPage(page).adminLogin(email, password);
|
||||
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
return { context, page, email };
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign in and assert the attempt actually authenticated, so a suite never proceeds to
|
||||
* authorization assertions while silently signed out — an unauthenticated request is redirected to
|
||||
* the login page and would pass a naive "not 200" check for the wrong reason.
|
||||
*
|
||||
* The landing URL is deliberately not pinned to the dashboard: a user whose role lacks the
|
||||
* `dashboard` permission is dropped on the first screen they can reach (`/admin/leads`, say), so
|
||||
* leaving the login page is what actually signals success.
|
||||
*/
|
||||
export async function signInExpectingSuccess(
|
||||
browser: Browser,
|
||||
baseURL: string | undefined,
|
||||
email: string,
|
||||
password: string
|
||||
): Promise<Session> {
|
||||
const session = await signInAs(browser, baseURL, email, password);
|
||||
|
||||
await expect(session.page, `${email} should be signed in`).not.toHaveURL(/\/admin\/login/);
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Laravel accepts the `XSRF-TOKEN` cookie echoed back as an `X-XSRF-TOKEN` header, which is how
|
||||
* the admin panel's own axios calls pass CSRF verification. Reusing that keeps a POST probe
|
||||
* failing on authorization rather than on a missing token.
|
||||
*/
|
||||
export async function csrfHeaders(context: BrowserContext): Promise<Record<string, string>> {
|
||||
const cookies = await context.cookies();
|
||||
|
||||
const token = cookies.find((cookie) => cookie.name === "XSRF-TOKEN");
|
||||
|
||||
return {
|
||||
"X-XSRF-TOKEN": token ? decodeURIComponent(token.value) : "",
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue a GET against an admin route using the session's cookies, without following redirects, so
|
||||
* the raw status is what gets asserted.
|
||||
*/
|
||||
export async function getRoute(session: Session, path: string): Promise<APIResponse> {
|
||||
return session.page.request.get(path, {
|
||||
failOnStatusCode: false,
|
||||
maxRedirects: 0,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue a POST against an admin route with CSRF headers attached.
|
||||
*/
|
||||
export async function postRoute(
|
||||
session: Session,
|
||||
path: string,
|
||||
form: Record<string, string> = {}
|
||||
): Promise<APIResponse> {
|
||||
return session.page.request.post(path, {
|
||||
failOnStatusCode: false,
|
||||
maxRedirects: 0,
|
||||
headers: await csrfHeaders(session.context),
|
||||
form,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert a response was refused by authorization.
|
||||
*
|
||||
* 401 is what `Bouncer::allow()` aborts with. A redirect to the login page (302) is accepted only
|
||||
* as a session-level refusal; a 200 always fails, because rendering the page means the guard did
|
||||
* not run.
|
||||
*/
|
||||
export function expectDenied(response: APIResponse, label: string): void {
|
||||
expect(
|
||||
response.status(),
|
||||
`${label} must be refused for a user without the permission (got ${response.status()})`
|
||||
).not.toBe(200);
|
||||
|
||||
expect(
|
||||
[401, 403, 302, 419].includes(response.status()),
|
||||
`${label} should be refused with 401/403 or a redirect, got ${response.status()}`
|
||||
).toBeTruthy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert a response was served.
|
||||
*/
|
||||
export function expectAllowed(response: APIResponse, label: string): void {
|
||||
expect(response.status(), `${label} should be reachable`).toBe(200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close every session opened by a test, tolerating one that failed to open.
|
||||
*/
|
||||
export async function closeSessions(...sessions: Array<Session | undefined>): Promise<void> {
|
||||
for (const session of sessions) {
|
||||
await session?.context.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A run-scoped suffix, so records created by one run never collide with a previous one and a
|
||||
* failed run leaves behind identifiable rows.
|
||||
*/
|
||||
export function runSuffix(): string {
|
||||
return `${Date.now().toString().slice(-6)}${Math.floor(Math.random() * 1000)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a record id out of a datagrid's own JSON response.
|
||||
*
|
||||
* The person listing renders no per-row links, so there is no href to scrape for an id. The grid
|
||||
* populates itself from this endpoint, so asking it directly is both more stable than parsing the
|
||||
* DOM and closer to what the page actually does.
|
||||
*
|
||||
* Returns `null` when the record is not present for this session — which for a scoped user is a
|
||||
* meaningful answer, not a failure.
|
||||
*/
|
||||
export async function findRecordId(
|
||||
session: Session,
|
||||
path: string,
|
||||
column: string,
|
||||
value: string
|
||||
): Promise<number | null> {
|
||||
const response = await session.page.request.get(
|
||||
`${path}?pagination[page]=1&pagination[per_page]=200`,
|
||||
{
|
||||
headers: { "X-Requested-With": "XMLHttpRequest", Accept: "application/json" },
|
||||
failOnStatusCode: false,
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status() !== 200) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const body = await response.json();
|
||||
|
||||
/**
|
||||
* A grid column is not always a scalar: the users grid renders its name cell from an object
|
||||
* (`{ image, name }`), which the Blade reads as `record.name.name`. Unwrap that shape before
|
||||
* comparing, or every user lookup silently misses.
|
||||
*/
|
||||
const cellText = (cell: unknown): string => {
|
||||
if (cell && typeof cell === "object" && "name" in (cell as Record<string, unknown>)) {
|
||||
return String((cell as Record<string, unknown>).name ?? "").trim();
|
||||
}
|
||||
|
||||
return String(cell ?? "").trim();
|
||||
};
|
||||
|
||||
const match = (body.records ?? []).find(
|
||||
(record: Record<string, unknown>) => cellText(record[column]) === value
|
||||
);
|
||||
|
||||
return match ? Number(match.id) : null;
|
||||
}
|
||||
58
packages/Webkul/Admin/tests/e2e-pw/utils/address.ts
Normal file
58
packages/Webkul/Admin/tests/e2e-pw/utils/address.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import * as forms from './form';
|
||||
import { generateRandomNumericString } from './faker';
|
||||
|
||||
const address = async (page) => {
|
||||
await page.fill('input[name="billing.company_name"]', forms.form.lastName);
|
||||
await page.fill('input[name="billing.first_name"]', forms.form.firstName);
|
||||
await page.fill('input[name="billing.last_name"]', forms.form.lastName);
|
||||
await page.fill('input[name="billing.email"]', forms.form.email);
|
||||
await page.fill('input[name="billing.address.[0]"]', forms.form.firstName);
|
||||
await page.selectOption('select[name="billing.country"]', 'IN');
|
||||
await page.selectOption('select[name="billing.state"]', 'UP');
|
||||
await page.fill('input[name="billing.city"]', forms.form.lastName);
|
||||
await page.fill('input[name="billing.postcode"]', generateRandomNumericString(6));
|
||||
await page.fill('input[name="billing.phone"]', forms.form.phone);
|
||||
|
||||
const exists = await page.$$('input[name="billing.save_address"]');
|
||||
|
||||
if (exists.length != 0) {
|
||||
await page.click('input[name="billing.save_address"] + label');
|
||||
await page.press('input[name="billing.phone"]', 'Enter');
|
||||
} else {
|
||||
const checkbox = await page.$$('input[name="billing.use_for_shipping"]');
|
||||
|
||||
if (Math.floor(Math.random() * 20) % 3 == 1 ? false : true) {
|
||||
if (! checkbox[0].isChecked()) {
|
||||
await page.click('input[name="billing.use_for_shipping"] + label');
|
||||
}
|
||||
} else {
|
||||
if (checkbox[0].isChecked()) {
|
||||
await page.click('input[name="billing.use_for_shipping"] + label');
|
||||
}
|
||||
|
||||
await page.fill('input[name="shipping.company_name"]', forms.form.lastName);
|
||||
await page.fill('input[name="shipping.first_name"]', forms.form.firstName);
|
||||
await page.fill('input[name="shipping.last_name"]', forms.form.lastName);
|
||||
await page.fill('input[name="shipping.email"]', forms.form.email);
|
||||
await page.fill('input[name="shipping.address.[0]"]', forms.form.firstName);
|
||||
await page.selectOption('select[name="shipping.country"]', 'IN');
|
||||
await page.selectOption('select[name="shipping.state"]', 'UP');
|
||||
await page.fill('input[name="shipping.city"]', forms.form.lastName);
|
||||
await page.fill('input[name="shipping.postcode"]', '201301');
|
||||
await page.fill('input[name="shipping.phone"]', forms.form.phone);
|
||||
}
|
||||
|
||||
const nextButton = await page.$$('button.primary-button:visible');
|
||||
await nextButton[0].click();
|
||||
}
|
||||
|
||||
const output = await forms.testForm(page)
|
||||
|
||||
if (output == null) {
|
||||
return address(page);
|
||||
} else {
|
||||
return 'done';
|
||||
}
|
||||
}
|
||||
|
||||
export default address;
|
||||
159
packages/Webkul/Admin/tests/e2e-pw/utils/customer.ts
Normal file
159
packages/Webkul/Admin/tests/e2e-pw/utils/customer.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import { expect } from "../setup";
|
||||
import {
|
||||
generateName,
|
||||
generateFirstName,
|
||||
generateLastName,
|
||||
generateEmail,
|
||||
generateDescription,
|
||||
} from "./faker";
|
||||
|
||||
export async function register(page) {
|
||||
const credentials = {
|
||||
firstName: generateFirstName(),
|
||||
lastName: generateLastName(),
|
||||
email: generateEmail(),
|
||||
password: "admin123",
|
||||
};
|
||||
await page.getByPlaceholder("First Name").fill(credentials.firstName);
|
||||
await page.getByPlaceholder("Last Name").fill(credentials.lastName);
|
||||
await page.getByPlaceholder("email@example.com").fill(credentials.email);
|
||||
await page
|
||||
.getByPlaceholder("Password", { exact: true })
|
||||
.fill(credentials.password);
|
||||
await page.getByPlaceholder("Confirm Password").fill(credentials.password);
|
||||
|
||||
|
||||
const isAgreementVisible = await page.locator("#agreement").nth(1).isVisible();
|
||||
|
||||
if (isAgreementVisible) {
|
||||
await page.getByText("I agree with this statement.").click();
|
||||
}
|
||||
|
||||
const isNewsletterVisible = await page
|
||||
.locator("#main form div")
|
||||
.filter({ hasText: "Subscribe to newsletter" })
|
||||
.locator("label")
|
||||
.first()
|
||||
.isVisible();
|
||||
|
||||
if (isNewsletterVisible) {
|
||||
await page
|
||||
.locator("#main form div")
|
||||
.filter({ hasText: "Subscribe to newsletter" })
|
||||
.locator("label")
|
||||
.first()
|
||||
.click();
|
||||
}
|
||||
|
||||
await page.getByRole("button", { name: "Register" }).click();
|
||||
|
||||
await expect(
|
||||
page
|
||||
.getByText(
|
||||
"Account created successfully, an e-mail has been sent for verification."
|
||||
)
|
||||
.first()
|
||||
).toBeVisible();
|
||||
|
||||
return credentials;
|
||||
}
|
||||
|
||||
export async function loginAsCustomer(page) {
|
||||
const credentials = await register(page);
|
||||
|
||||
await page.goto("customer/login");
|
||||
await page.getByPlaceholder("email@example.com").click();
|
||||
await page.getByPlaceholder("email@example.com").fill(credentials.email);
|
||||
await page.getByPlaceholder("email@example.com").press("Tab");
|
||||
await page.getByPlaceholder("Password").fill(credentials.password);
|
||||
await page.getByRole("button", { name: "Sign In" }).click();
|
||||
|
||||
return credentials;
|
||||
}
|
||||
|
||||
export async function addAddress(page) {
|
||||
await page.getByLabel("Profile").click();
|
||||
await page.getByRole("link", { name: "Profile" }).click();
|
||||
await page.getByRole("link", { name: " Address " }).click();
|
||||
await page.getByRole("link", { name: "Add Address" }).click();
|
||||
await page.getByPlaceholder("Company Name").click();
|
||||
await page.getByPlaceholder("Company Name").fill(generateName());
|
||||
await page.getByPlaceholder("Company Name").press("Tab");
|
||||
await page.getByPlaceholder("First Name").fill(generateFirstName());
|
||||
await page.getByPlaceholder("First Name").press("Tab");
|
||||
await page.getByPlaceholder("Last Name").fill(generateLastName());
|
||||
await page.getByPlaceholder("Last Name").press("Tab");
|
||||
await page
|
||||
.getByPlaceholder("Email", { exact: true })
|
||||
.fill("test@example.com");
|
||||
await page.getByPlaceholder("Email", { exact: true }).press("Tab");
|
||||
await page.getByPlaceholder("Vat ID").press("Tab");
|
||||
await page.getByPlaceholder("Street Address").fill("Demo");
|
||||
await page.getByPlaceholder("Street Address").press("Tab");
|
||||
await page.getByLabel("Country").selectOption("DZ");
|
||||
await page.getByPlaceholder("State").click();
|
||||
await page.getByPlaceholder("State").fill("any");
|
||||
await page.getByPlaceholder("City").click();
|
||||
await page.getByPlaceholder("City").fill("any");
|
||||
await page.getByPlaceholder("Post Code").click();
|
||||
await page.getByPlaceholder("Post Code").fill("123456");
|
||||
await page.getByPlaceholder("Phone").click();
|
||||
await page.getByPlaceholder("Phone").fill("9876543210");
|
||||
await page
|
||||
.locator("#main form div")
|
||||
.filter({ hasText: "Set as Default" })
|
||||
.locator("label")
|
||||
.first()
|
||||
.click();
|
||||
await page
|
||||
.locator("#main form div")
|
||||
.filter({ hasText: "Set as Default" })
|
||||
.locator("label")
|
||||
.first()
|
||||
.click();
|
||||
await page.getByRole("button", { name: "Save" }).click();
|
||||
|
||||
await expect(
|
||||
page.getByText("Address have been successfully added.").first()
|
||||
).toBeVisible();
|
||||
}
|
||||
|
||||
export async function addWishlist(page) {
|
||||
await page.locator(".action-items > span").first().click();
|
||||
await page
|
||||
.locator(
|
||||
"div:nth-child(9) > div:nth-child(2) > div > .-mt-9 > .action-items > span"
|
||||
)
|
||||
.first()
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.getByText("Item Successfully Added To Wishlist").first()
|
||||
).toBeVisible();
|
||||
}
|
||||
|
||||
export async function addReview(page) {
|
||||
const review = {
|
||||
title: generateName(),
|
||||
comment: generateDescription(),
|
||||
};
|
||||
|
||||
await page
|
||||
.locator("#main div")
|
||||
.filter({ hasText: "New Products View All New" })
|
||||
.getByLabel("Arctic Touchscreen Winter")
|
||||
.click();
|
||||
await page.getByRole("button", { name: "Reviews" }).click();
|
||||
await page.locator("#review-tab").getByText("Write a Review").click();
|
||||
await page.locator("#review-tab span").nth(3).click();
|
||||
await page.locator("#review-tab span").nth(4).click();
|
||||
await page.getByPlaceholder("Title").fill(review.title);
|
||||
await page.getByPlaceholder("Comment").fill(review.comment);
|
||||
await page.getByRole("button", { name: "Submit Review" }).click();
|
||||
|
||||
await expect(
|
||||
page.getByText("Review submitted successfully.").first()
|
||||
).toBeVisible();
|
||||
|
||||
return review;
|
||||
}
|
||||
856
packages/Webkul/Admin/tests/e2e-pw/utils/faker.ts
Normal file
856
packages/Webkul/Admin/tests/e2e-pw/utils/faker.ts
Normal file
@@ -0,0 +1,856 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const usedNames = new Set();
|
||||
const usedEmails = new Set();
|
||||
const usedNumbers = new Set();
|
||||
const usedSlugs = new Set();
|
||||
const usedCurrencies = new Set();
|
||||
const usedLocales = new Set();
|
||||
|
||||
|
||||
|
||||
export function generateName() {
|
||||
const adjectives = [
|
||||
"Cool",
|
||||
"Smart",
|
||||
"Fast",
|
||||
"Sleek",
|
||||
"Innovative",
|
||||
"Shiny",
|
||||
"Bold",
|
||||
"Elegant",
|
||||
"Epic",
|
||||
"Mystic",
|
||||
"Brilliant",
|
||||
"Luminous",
|
||||
];
|
||||
|
||||
const nouns = [
|
||||
"Star",
|
||||
"Vision",
|
||||
"Echo",
|
||||
"Spark",
|
||||
"Horizon",
|
||||
"Nova",
|
||||
"Shadow",
|
||||
"Wave",
|
||||
"Pulse",
|
||||
"Vortex",
|
||||
"Zenith",
|
||||
"Element",
|
||||
];
|
||||
|
||||
let name = "";
|
||||
|
||||
do {
|
||||
const adj = adjectives[Math.floor(Math.random() * adjectives.length)];
|
||||
const noun = nouns[Math.floor(Math.random() * nouns.length)];
|
||||
|
||||
name = `${adj} ${noun}`;
|
||||
} while (usedNames.has(name));
|
||||
|
||||
usedNames.add(name);
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
export function generateFirstName() {
|
||||
const firstNames = [
|
||||
"James",
|
||||
"Emma",
|
||||
"Liam",
|
||||
"Olivia",
|
||||
"Noah",
|
||||
"Ava",
|
||||
"William",
|
||||
"Sophia",
|
||||
"Benjamin",
|
||||
"Isabella",
|
||||
"Lucas",
|
||||
"Mia",
|
||||
];
|
||||
|
||||
return firstNames[Math.floor(Math.random() * firstNames.length)];
|
||||
}
|
||||
|
||||
export function generateLastName() {
|
||||
const lastNames = [
|
||||
"Smith",
|
||||
"Johnson",
|
||||
"Brown",
|
||||
"Williams",
|
||||
"Jones",
|
||||
"Garcia",
|
||||
"Miller",
|
||||
"Davis",
|
||||
"Rodriguez",
|
||||
"Martinez",
|
||||
"Hernandez",
|
||||
"Lopez",
|
||||
];
|
||||
|
||||
return lastNames[Math.floor(Math.random() * lastNames.length)];
|
||||
}
|
||||
|
||||
export function generateFullName() {
|
||||
return `${generateFirstName()} ${generateLastName()}`;
|
||||
}
|
||||
|
||||
export function generateEmail() {
|
||||
const adjectives = [
|
||||
"Cool",
|
||||
"Smart",
|
||||
"Fast",
|
||||
"Sleek",
|
||||
"Innovative",
|
||||
"Shiny",
|
||||
"Bold",
|
||||
"Elegant",
|
||||
"Epic",
|
||||
"Mystic",
|
||||
"Brilliant",
|
||||
"Luminous",
|
||||
];
|
||||
|
||||
const nouns = [
|
||||
"Star",
|
||||
"Vision",
|
||||
"Echo",
|
||||
"Spark",
|
||||
"Horizon",
|
||||
"Nova",
|
||||
"Shadow",
|
||||
"Wave",
|
||||
"Pulse",
|
||||
"Vortex",
|
||||
"Zenith",
|
||||
"Element",
|
||||
];
|
||||
|
||||
let email = "";
|
||||
|
||||
do {
|
||||
const adj = adjectives[Math.floor(Math.random() * adjectives.length)];
|
||||
const noun = nouns[Math.floor(Math.random() * nouns.length)];
|
||||
const number = Math.floor(1000 + Math.random() * 9000);
|
||||
|
||||
email = `${adj}${noun}${number}@example.com`.toLowerCase();
|
||||
} while (usedEmails.has(email));
|
||||
|
||||
usedEmails.add(email);
|
||||
|
||||
return email;
|
||||
}
|
||||
|
||||
export function generatePhoneNumber() {
|
||||
let phoneNumber;
|
||||
|
||||
do {
|
||||
phoneNumber = Math.floor(6000000000 + Math.random() * 4000000000);
|
||||
} while (usedNumbers.has(phoneNumber));
|
||||
|
||||
usedNumbers.add(phoneNumber);
|
||||
|
||||
return `${phoneNumber}`;
|
||||
}
|
||||
|
||||
export function generateSKU() {
|
||||
const letters = Array.from({ length: 3 }, () =>
|
||||
String.fromCharCode(65 + Math.floor(Math.random() * 26))
|
||||
).join("");
|
||||
|
||||
const numbers = Math.floor(1000 + Math.random() * 9000);
|
||||
|
||||
return `${letters}${numbers}`;
|
||||
}
|
||||
|
||||
export function generateSlug(delimiter = "-") {
|
||||
let slug;
|
||||
|
||||
do {
|
||||
const name = generateName();
|
||||
|
||||
const randomStr = Math.random().toString(36).substring(2, 8);
|
||||
|
||||
slug = `${name
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, delimiter)}${delimiter}${randomStr}`;
|
||||
} while (usedSlugs.has(slug));
|
||||
|
||||
usedSlugs.add(slug);
|
||||
|
||||
return slug;
|
||||
}
|
||||
|
||||
export function generateDescription(length = 255) {
|
||||
const phrases = [
|
||||
"An innovative and sleek design.",
|
||||
"Built for speed and efficiency.",
|
||||
"Experience the future today.",
|
||||
"A perfect blend of style and power.",
|
||||
"Engineered to perfection.",
|
||||
"Designed for those who dream big.",
|
||||
"Unleash creativity with this masterpiece.",
|
||||
"A game-changer in every way.",
|
||||
"Smart, fast, and reliable.",
|
||||
"The perfect companion for your journey.",
|
||||
"Crafted with precision and excellence.",
|
||||
"Innovation that redefines possibilities.",
|
||||
"Enhancing your experience like never before.",
|
||||
"Where technology meets elegance.",
|
||||
"Power, performance, and perfection combined.",
|
||||
"Redefining the way you experience the world.",
|
||||
"A masterpiece of engineering and design.",
|
||||
"Unmatched quality and exceptional performance.",
|
||||
"Designed to elevate your lifestyle.",
|
||||
"Beyond expectations, beyond limits.",
|
||||
];
|
||||
|
||||
let description = "";
|
||||
|
||||
while (description.length < length) {
|
||||
let phrase = phrases[Math.floor(Math.random() * phrases.length)];
|
||||
|
||||
if (description.length + phrase.length <= length) {
|
||||
description += (description ? " " : "") + phrase;
|
||||
} else {
|
||||
description +=
|
||||
" " + phrase.substring(0, length - description.length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return description.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random numeric string with specified length or within a numeric range.
|
||||
*
|
||||
* @param {number} [length=10] - The length of the random string (used only when min and max are undefined)
|
||||
* @param {number|null|undefined} [min=undefined] - Minimum value (inclusive) when generating a number in range
|
||||
* @param {number|null|undefined} [max=undefined] - Maximum value (inclusive) when generating a number in range
|
||||
* @returns {string} - Random numeric string
|
||||
*/
|
||||
export function generateRandomNumericString(length: number = 10, min?: number | null, max?: number | null): string {
|
||||
// Generate a number within range
|
||||
if (min !== null && min !== undefined && max !== null && max !== undefined) {
|
||||
// Input validation
|
||||
if (!Number.isInteger(min) || !Number.isInteger(max)) {
|
||||
throw new Error("Min and max must be integers when provided.");
|
||||
}
|
||||
|
||||
if (min > max) {
|
||||
throw new Error("Min value cannot be greater than max value.");
|
||||
}
|
||||
|
||||
// Generate a random number within the range and convert to string
|
||||
return Math.floor(Math.random() * (max - min + 1) + min).toString();
|
||||
}
|
||||
|
||||
// Generate a random string of specified length
|
||||
if (!Number.isInteger(length) || length <= 0) {
|
||||
throw new Error("Length must be a positive integer.");
|
||||
}
|
||||
|
||||
// More efficient method for generating random digits
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += Math.floor(Math.random() * 10);
|
||||
}
|
||||
|
||||
// Ensure first character is not zero for a consistent length string
|
||||
if (length > 1 && result[0] === '0') {
|
||||
result = String(1 + Math.floor(Math.random() * 9)) + result.substring(1);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function generateHostname() {
|
||||
const words = [
|
||||
"tech",
|
||||
"cloud",
|
||||
"byte",
|
||||
"stream",
|
||||
"nexus",
|
||||
"core",
|
||||
"pulse",
|
||||
"data",
|
||||
"sync",
|
||||
"wave",
|
||||
"hub",
|
||||
"zone",
|
||||
];
|
||||
|
||||
const domains = [".com", ".net", ".io", ".ai", ".xyz", ".co"];
|
||||
|
||||
const part1 = words[Math.floor(Math.random() * words.length)];
|
||||
const part2 = words[Math.floor(Math.random() * words.length)];
|
||||
const domain = domains[Math.floor(Math.random() * domains.length)];
|
||||
|
||||
return `https://${part1}${part2}${domain}`;
|
||||
}
|
||||
|
||||
export function generateCurrency() {
|
||||
const currencies = [
|
||||
{
|
||||
name: "US Dollar",
|
||||
code: "USD",
|
||||
symbol: "$",
|
||||
decimalDigits: "2",
|
||||
groupSeparator: ",",
|
||||
decimalSeparator: ".",
|
||||
},
|
||||
{
|
||||
name: "Euro",
|
||||
code: "EUR",
|
||||
symbol: "€",
|
||||
decimalDigits: "2",
|
||||
groupSeparator: ".",
|
||||
decimalSeparator: ",",
|
||||
},
|
||||
{
|
||||
name: "British Pound",
|
||||
code: "GBP",
|
||||
symbol: "£",
|
||||
decimalDigits: "2",
|
||||
groupSeparator: ",",
|
||||
decimalSeparator: ".",
|
||||
},
|
||||
{
|
||||
name: "Japanese Yen",
|
||||
code: "JPY",
|
||||
symbol: "¥",
|
||||
decimalDigits: "0",
|
||||
groupSeparator: ",",
|
||||
decimalSeparator: ".",
|
||||
},
|
||||
{
|
||||
name: "Australian Dollar",
|
||||
code: "AUD",
|
||||
symbol: "A$",
|
||||
decimalDigits: "2",
|
||||
groupSeparator: ",",
|
||||
decimalSeparator: ".",
|
||||
},
|
||||
{
|
||||
name: "Canadian Dollar",
|
||||
code: "CAD",
|
||||
symbol: "C$",
|
||||
decimalDigits: "2",
|
||||
groupSeparator: ",",
|
||||
decimalSeparator: ".",
|
||||
},
|
||||
{
|
||||
name: "Swiss Franc",
|
||||
code: "CHF",
|
||||
symbol: "CHF",
|
||||
decimalDigits: "2",
|
||||
groupSeparator: "'",
|
||||
decimalSeparator: ".",
|
||||
},
|
||||
{
|
||||
name: "Chinese Yuan",
|
||||
code: "CNY",
|
||||
symbol: "¥",
|
||||
decimalDigits: "2",
|
||||
groupSeparator: ",",
|
||||
decimalSeparator: ".",
|
||||
},
|
||||
{
|
||||
name: "Indian Rupee",
|
||||
code: "INR",
|
||||
symbol: "₹",
|
||||
decimalDigits: "2",
|
||||
groupSeparator: ",",
|
||||
decimalSeparator: ".",
|
||||
},
|
||||
{
|
||||
name: "South Korean Won",
|
||||
code: "KRW",
|
||||
symbol: "₩",
|
||||
decimalDigits: "0",
|
||||
groupSeparator: ",",
|
||||
decimalSeparator: ".",
|
||||
},
|
||||
{
|
||||
name: "Mexican Peso",
|
||||
code: "MXN",
|
||||
symbol: "MX$",
|
||||
decimalDigits: "2",
|
||||
groupSeparator: ",",
|
||||
decimalSeparator: ".",
|
||||
},
|
||||
{
|
||||
name: "Russian Ruble",
|
||||
code: "RUB",
|
||||
symbol: "₽",
|
||||
decimalDigits: "2",
|
||||
groupSeparator: " ",
|
||||
decimalSeparator: ",",
|
||||
},
|
||||
{
|
||||
name: "Brazilian Real",
|
||||
code: "BRL",
|
||||
symbol: "R$",
|
||||
decimalDigits: "2",
|
||||
groupSeparator: ".",
|
||||
decimalSeparator: ",",
|
||||
},
|
||||
{
|
||||
name: "South African Rand",
|
||||
code: "ZAR",
|
||||
symbol: "R",
|
||||
decimalDigits: "2",
|
||||
groupSeparator: " ",
|
||||
decimalSeparator: ",",
|
||||
},
|
||||
{
|
||||
name: "New Zealand Dollar",
|
||||
code: "NZD",
|
||||
symbol: "NZ$",
|
||||
decimalDigits: "2",
|
||||
groupSeparator: ",",
|
||||
decimalSeparator: ".",
|
||||
},
|
||||
{
|
||||
name: "Singapore Dollar",
|
||||
code: "SGD",
|
||||
symbol: "S$",
|
||||
decimalDigits: "2",
|
||||
groupSeparator: ",",
|
||||
decimalSeparator: ".",
|
||||
},
|
||||
{
|
||||
name: "Hong Kong Dollar",
|
||||
code: "HKD",
|
||||
symbol: "HK$",
|
||||
decimalDigits: "2",
|
||||
groupSeparator: ",",
|
||||
decimalSeparator: ".",
|
||||
},
|
||||
{
|
||||
name: "Norwegian Krone",
|
||||
code: "NOK",
|
||||
symbol: "kr",
|
||||
decimalDigits: "2",
|
||||
groupSeparator: " ",
|
||||
decimalSeparator: ",",
|
||||
},
|
||||
{
|
||||
name: "Swedish Krona",
|
||||
code: "SEK",
|
||||
symbol: "kr",
|
||||
decimalDigits: "2",
|
||||
groupSeparator: " ",
|
||||
decimalSeparator: ",",
|
||||
},
|
||||
{
|
||||
name: "Danish Krone",
|
||||
code: "DKK",
|
||||
symbol: "kr",
|
||||
decimalDigits: "2",
|
||||
groupSeparator: ".",
|
||||
decimalSeparator: ",",
|
||||
},
|
||||
{
|
||||
name: "Polish Złoty",
|
||||
code: "PLN",
|
||||
symbol: "zł",
|
||||
decimalDigits: "2",
|
||||
groupSeparator: " ",
|
||||
decimalSeparator: ",",
|
||||
},
|
||||
{
|
||||
name: "Turkish Lira",
|
||||
code: "TRY",
|
||||
symbol: "₺",
|
||||
decimalDigits: "2",
|
||||
groupSeparator: ".",
|
||||
decimalSeparator: ",",
|
||||
},
|
||||
{
|
||||
name: "Thai Baht",
|
||||
code: "THB",
|
||||
symbol: "฿",
|
||||
decimalDigits: "2",
|
||||
groupSeparator: ",",
|
||||
decimalSeparator: ".",
|
||||
},
|
||||
{
|
||||
name: "Indonesian Rupiah",
|
||||
code: "IDR",
|
||||
symbol: "Rp",
|
||||
decimalDigits: "0",
|
||||
groupSeparator: ".",
|
||||
decimalSeparator: ",",
|
||||
},
|
||||
{
|
||||
name: "Malaysian Ringgit",
|
||||
code: "MYR",
|
||||
symbol: "RM",
|
||||
decimalDigits: "2",
|
||||
groupSeparator: ",",
|
||||
decimalSeparator: ".",
|
||||
},
|
||||
{
|
||||
name: "Philippine Peso",
|
||||
code: "PHP",
|
||||
symbol: "₱",
|
||||
decimalDigits: "2",
|
||||
groupSeparator: ",",
|
||||
decimalSeparator: ".",
|
||||
},
|
||||
{
|
||||
name: "Israeli Shekel",
|
||||
code: "ILS",
|
||||
symbol: "₪",
|
||||
decimalDigits: "2",
|
||||
groupSeparator: ",",
|
||||
decimalSeparator: ".",
|
||||
},
|
||||
{
|
||||
name: "Saudi Riyal",
|
||||
code: "SAR",
|
||||
symbol: "﷼",
|
||||
decimalDigits: "2",
|
||||
groupSeparator: ",",
|
||||
decimalSeparator: ".",
|
||||
},
|
||||
{
|
||||
name: "UAE Dirham",
|
||||
code: "AED",
|
||||
symbol: "د.إ",
|
||||
decimalDigits: "2",
|
||||
groupSeparator: ",",
|
||||
decimalSeparator: ".",
|
||||
},
|
||||
{
|
||||
name: "Czech Koruna",
|
||||
code: "CZK",
|
||||
symbol: "Kč",
|
||||
decimalDigits: "2",
|
||||
groupSeparator: " ",
|
||||
decimalSeparator: ",",
|
||||
},
|
||||
{
|
||||
name: "Hungarian Forint",
|
||||
code: "HUF",
|
||||
symbol: "Ft",
|
||||
decimalDigits: "0",
|
||||
groupSeparator: " ",
|
||||
decimalSeparator: ",",
|
||||
},
|
||||
{
|
||||
name: "Romanian Leu",
|
||||
code: "RON",
|
||||
symbol: "lei",
|
||||
decimalDigits: "2",
|
||||
groupSeparator: ".",
|
||||
decimalSeparator: ",",
|
||||
},
|
||||
{
|
||||
name: "Bulgarian Lev",
|
||||
code: "BGN",
|
||||
symbol: "лв",
|
||||
decimalDigits: "2",
|
||||
groupSeparator: " ",
|
||||
decimalSeparator: ",",
|
||||
},
|
||||
{
|
||||
name: "Croatian Kuna",
|
||||
code: "HRK",
|
||||
symbol: "kn",
|
||||
decimalDigits: "2",
|
||||
groupSeparator: ".",
|
||||
decimalSeparator: ",",
|
||||
},
|
||||
{
|
||||
name: "Icelandic Króna",
|
||||
code: "ISK",
|
||||
symbol: "kr",
|
||||
decimalDigits: "0",
|
||||
groupSeparator: ".",
|
||||
decimalSeparator: ",",
|
||||
},
|
||||
{
|
||||
name: "Ukrainian Hryvnia",
|
||||
code: "UAH",
|
||||
symbol: "₴",
|
||||
decimalDigits: "2",
|
||||
groupSeparator: " ",
|
||||
decimalSeparator: ",",
|
||||
},
|
||||
{
|
||||
name: "Pakistani Rupee",
|
||||
code: "PKR",
|
||||
symbol: "₨",
|
||||
decimalDigits: "2",
|
||||
groupSeparator: ",",
|
||||
decimalSeparator: ".",
|
||||
},
|
||||
{
|
||||
name: "Bangladeshi Taka",
|
||||
code: "BDT",
|
||||
symbol: "৳",
|
||||
decimalDigits: "2",
|
||||
groupSeparator: ",",
|
||||
decimalSeparator: ".",
|
||||
},
|
||||
{
|
||||
name: "Sri Lankan Rupee",
|
||||
code: "LKR",
|
||||
symbol: "Rs",
|
||||
decimalDigits: "2",
|
||||
groupSeparator: ",",
|
||||
decimalSeparator: ".",
|
||||
},
|
||||
{
|
||||
name: "Nepalese Rupee",
|
||||
code: "NPR",
|
||||
symbol: "₨",
|
||||
decimalDigits: "2",
|
||||
groupSeparator: ",",
|
||||
decimalSeparator: ".",
|
||||
},
|
||||
{
|
||||
name: "Kuwaiti Dinar",
|
||||
code: "KWD",
|
||||
symbol: "د.ك",
|
||||
decimalDigits: "3",
|
||||
groupSeparator: ",",
|
||||
decimalSeparator: ".",
|
||||
},
|
||||
{
|
||||
name: "Qatari Riyal",
|
||||
code: "QAR",
|
||||
symbol: "ر.ق",
|
||||
decimalDigits: "2",
|
||||
groupSeparator: ",",
|
||||
decimalSeparator: ".",
|
||||
},
|
||||
{
|
||||
name: "Omani Rial",
|
||||
code: "OMR",
|
||||
symbol: "ر.ع.",
|
||||
decimalDigits: "3",
|
||||
groupSeparator: ",",
|
||||
decimalSeparator: ".",
|
||||
},
|
||||
{
|
||||
name: "Bahraini Dinar",
|
||||
code: "BHD",
|
||||
symbol: "ب.د",
|
||||
decimalDigits: "3",
|
||||
groupSeparator: ",",
|
||||
decimalSeparator: ".",
|
||||
},
|
||||
{
|
||||
name: "Argentine Peso",
|
||||
code: "ARS",
|
||||
symbol: "$",
|
||||
decimalDigits: "2",
|
||||
groupSeparator: ".",
|
||||
decimalSeparator: ",",
|
||||
},
|
||||
{
|
||||
name: "Chilean Peso",
|
||||
code: "CLP",
|
||||
symbol: "$",
|
||||
decimalDigits: "0",
|
||||
groupSeparator: ".",
|
||||
decimalSeparator: ",",
|
||||
},
|
||||
{
|
||||
name: "Colombian Peso",
|
||||
code: "COP",
|
||||
symbol: "$",
|
||||
decimalDigits: "0",
|
||||
groupSeparator: ".",
|
||||
decimalSeparator: ",",
|
||||
},
|
||||
{
|
||||
name: "Peruvian Sol",
|
||||
code: "PEN",
|
||||
symbol: "S/.",
|
||||
decimalDigits: "2",
|
||||
groupSeparator: ",",
|
||||
decimalSeparator: ".",
|
||||
},
|
||||
{
|
||||
name: "Venezuelan Bolívar",
|
||||
code: "VES",
|
||||
symbol: "Bs.",
|
||||
decimalDigits: "2",
|
||||
groupSeparator: ".",
|
||||
decimalSeparator: ",",
|
||||
},
|
||||
];
|
||||
|
||||
if (usedCurrencies.size >= currencies.length) {
|
||||
throw new Error("All currencies have been used.");
|
||||
}
|
||||
|
||||
let currency;
|
||||
|
||||
do {
|
||||
const randomIndex = Math.floor(Math.random() * currencies.length);
|
||||
|
||||
currency = currencies[randomIndex];
|
||||
} while (usedCurrencies.has(currency.code));
|
||||
|
||||
usedCurrencies.add(currency.code);
|
||||
|
||||
return currency;
|
||||
}
|
||||
|
||||
export function generateLocale() {
|
||||
const locales = [
|
||||
{
|
||||
name: "English",
|
||||
code: "en",
|
||||
direction: "LTR"
|
||||
},
|
||||
{
|
||||
name: "French",
|
||||
code: "fr",
|
||||
direction: "LTR"
|
||||
},
|
||||
{
|
||||
name: "Spanish",
|
||||
code: "es",
|
||||
direction: "LTR"
|
||||
},
|
||||
{
|
||||
name: "Arabic",
|
||||
code: "ar",
|
||||
direction: "RTL"
|
||||
},
|
||||
{
|
||||
name: "Hebrew",
|
||||
code: "he",
|
||||
direction: "RTL"
|
||||
},
|
||||
{
|
||||
name: "Japanese",
|
||||
code: "ja",
|
||||
direction: "LTR"
|
||||
},
|
||||
{
|
||||
name: "Chinese",
|
||||
code: "zh_CN",
|
||||
direction: "LTR"
|
||||
},
|
||||
{
|
||||
name: "Hindi",
|
||||
code: "hi_IN",
|
||||
direction: "LTR"
|
||||
},
|
||||
{
|
||||
name: "Bengali",
|
||||
code: "bn",
|
||||
direction: "LTR"
|
||||
},
|
||||
{
|
||||
name: "German",
|
||||
code: "de",
|
||||
direction: "LTR"
|
||||
},
|
||||
{
|
||||
name: "Persian",
|
||||
code: "fa",
|
||||
direction: "LTR"
|
||||
},
|
||||
{
|
||||
name: "Italian",
|
||||
code: "it",
|
||||
direction: "LTR"
|
||||
},
|
||||
{
|
||||
name: "Dutch",
|
||||
code: "nl",
|
||||
direction: "LTR"
|
||||
},
|
||||
];
|
||||
|
||||
if (usedLocales.size >= locales.length) {
|
||||
throw new Error("All locales have been used.");
|
||||
}
|
||||
|
||||
let locale;
|
||||
|
||||
do {
|
||||
const randomIndex = Math.floor(Math.random() * locales.length);
|
||||
locale = locales[randomIndex];
|
||||
} while (usedLocales.has(locale.code));
|
||||
|
||||
usedLocales.add(locale.code);
|
||||
|
||||
return locale;
|
||||
}
|
||||
|
||||
export function randomElement(array) {
|
||||
return array[Math.floor(Math.random() * array.length)];
|
||||
}
|
||||
|
||||
export function getImageFile(
|
||||
directory = path.resolve(__dirname, "../data/images/")
|
||||
) {
|
||||
if (!fs.existsSync(directory)) {
|
||||
throw new Error(`Directory does not exist: ${directory}`);
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(directory);
|
||||
const imageFiles = files.filter((file) =>
|
||||
/\.(gif|jpeg|jpg|png|svg|webp)$/i.test(file)
|
||||
);
|
||||
|
||||
if (!imageFiles.length) {
|
||||
throw new Error("No image files found in the directory.");
|
||||
}
|
||||
|
||||
const randomIndex = Math.floor(Math.random() * imageFiles.length);
|
||||
|
||||
return path.join(directory, imageFiles[randomIndex]);
|
||||
}
|
||||
|
||||
export function generateRandomDateTime(end = new Date(new Date().setFullYear(new Date().getFullYear() + 10))) {
|
||||
const start = new Date();
|
||||
const date = new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime()));
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
}
|
||||
|
||||
export function generateLocation() {
|
||||
const location = [
|
||||
"New York",
|
||||
"Los Angeles",
|
||||
"Chicago",
|
||||
"Houston",
|
||||
"Phoenix",
|
||||
"Philadelphia",
|
||||
"San Antonio",
|
||||
"San Diego",
|
||||
"Dallas",
|
||||
"San Jose",
|
||||
"Austin",
|
||||
"Jacksonville",
|
||||
"San Francisco",
|
||||
"Indianapolis",
|
||||
"Columbus",
|
||||
"Fort Worth",
|
||||
];
|
||||
|
||||
return location[Math.floor(Math.random() * location.length)];
|
||||
}
|
||||
198
packages/Webkul/Admin/tests/e2e-pw/utils/form.ts
Normal file
198
packages/Webkul/Admin/tests/e2e-pw/utils/form.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
function getRandomImageFile(
|
||||
directory = path.resolve(__dirname, "../data/images/")
|
||||
) {
|
||||
if (!fs.existsSync(directory)) {
|
||||
throw new Error(`Directory does not exist: ${directory}`);
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(directory);
|
||||
const imageFiles = files.filter((file) =>
|
||||
/\.(gif|jpeg|jpg|png|svg|webp)$/i.test(file)
|
||||
);
|
||||
|
||||
if (!imageFiles.length) {
|
||||
throw new Error("No image files found in the directory.");
|
||||
}
|
||||
|
||||
const randomIndex = Math.floor(Math.random() * imageFiles.length);
|
||||
return path.join(directory, imageFiles[randomIndex]);
|
||||
}
|
||||
|
||||
const testForm = async (page) => {
|
||||
const getError = await page
|
||||
.waitForSelector(".text-red-600.text-xs.italic", { timeout: 2000 })
|
||||
.catch(() => null);
|
||||
var message = "";
|
||||
|
||||
if (getError) {
|
||||
const errors = await page.$$(".text-red-600.text-xs.italic");
|
||||
|
||||
for (let error of errors) {
|
||||
message = await error.evaluate((el) => el.innerText);
|
||||
}
|
||||
return null;
|
||||
} else {
|
||||
const iconExists = await page
|
||||
.waitForSelector(
|
||||
".flex.items-center.break-all.text-sm > .icon-toast-done.rounded-full.bg-white.text-2xl",
|
||||
{ timeout: 5000 }
|
||||
)
|
||||
.catch(() => null);
|
||||
|
||||
if (iconExists) {
|
||||
const messages = await page.$$(
|
||||
".flex.items-center.break-all.text-sm > .icon-toast-done.rounded-full.bg-white.text-2xl"
|
||||
);
|
||||
const icons = await page.$$(
|
||||
".flex.items-center.break-all.text-sm + .cursor-pointer.underline"
|
||||
);
|
||||
|
||||
message = await messages[0].evaluate(
|
||||
(el) => el.parentNode.innerText
|
||||
);
|
||||
await icons[0].click();
|
||||
return null;
|
||||
} else {
|
||||
return page.url();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const form = {
|
||||
firstName: `Test${Math.random().toString(36).substring(7)}`,
|
||||
lastName: `User${Math.random().toString(36).substring(7)}`,
|
||||
email: `test${Math.random().toString(36).substring(7)}@example.com`,
|
||||
phone: `555${Math.floor(Math.random() * 10000000)
|
||||
.toString()
|
||||
.padStart(7, "0")}`,
|
||||
};
|
||||
|
||||
function generateRandomPassword(minLength, maxLength) {
|
||||
const characters =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()";
|
||||
const length =
|
||||
Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength;
|
||||
let password = "";
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
password += characters.charAt(
|
||||
Math.floor(Math.random() * characters.length)
|
||||
);
|
||||
}
|
||||
|
||||
return password;
|
||||
}
|
||||
|
||||
function generateRandomStringWithSpaces(length) {
|
||||
const characters =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let result = "";
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (Math.random() < 0.1) {
|
||||
result += " ";
|
||||
} else {
|
||||
const randomIndex = Math.floor(Math.random() * characters.length);
|
||||
result += characters[randomIndex];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function generateRandomProductName() {
|
||||
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
let result = "";
|
||||
for (let i = 1; i < 3; i++) {
|
||||
if (Math.random() < 0.1) {
|
||||
result += " ";
|
||||
} else {
|
||||
const randomIndex = Math.floor(Math.random() * characters.length);
|
||||
result += characters[randomIndex];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function generateRandomHtmlContent() {
|
||||
const elements = ["span", "a", "em", "strong", "b", "i", "u"];
|
||||
const randomElement = elements[Math.floor(Math.random() * elements.length)];
|
||||
const randomText = Math.random().toString(36).substring(7);
|
||||
const randomHref = `https://example.com/${Math.random()
|
||||
.toString(36)
|
||||
.substring(7)}`;
|
||||
|
||||
switch (randomElement) {
|
||||
case "a":
|
||||
return `<a href="${randomHref}">${randomText}</a>`;
|
||||
case "span":
|
||||
return `<span>${randomText}</span>`;
|
||||
case "em":
|
||||
return `<em>${randomText}</em>`;
|
||||
case "strong":
|
||||
return `<strong>${randomText}</strong>`;
|
||||
case "b":
|
||||
return `<b>${randomText}</b>`;
|
||||
case "i":
|
||||
return `<i>${randomText}</i>`;
|
||||
case "u":
|
||||
return `<u>${randomText}</u>`;
|
||||
default:
|
||||
return `<span>${randomText}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function fillParagraphWithRandomHtml(multi) {
|
||||
const randomHtmlContent = new Array(multi)
|
||||
.fill("")
|
||||
.map(() => generateRandomHtmlContent())
|
||||
.join(" ");
|
||||
|
||||
return randomHtmlContent;
|
||||
}
|
||||
|
||||
function generateRandomUrl() {
|
||||
const protocols = ["http", "https"];
|
||||
const domain = `example${Math.random().toString(36).substring(7)}.com`;
|
||||
const path = `/${Math.random().toString(36).substring(7)}`;
|
||||
const protocol = protocols[Math.floor(Math.random() * protocols.length)];
|
||||
return `${protocol}://${domain}${path}`;
|
||||
}
|
||||
|
||||
function generateRandomDateTimeRange() {
|
||||
// Generate valid "from" and "to" dates
|
||||
const fromDate = new Date(2024, 9, 29, 12, 0, 0); // Start: 2024-10-29 12:00:00
|
||||
const toDate = new Date(2024, 10, 29, 12, 0, 0); // End: 2024-11-29 12:00:00
|
||||
|
||||
// Randomize within the range
|
||||
const randomFrom = new Date(
|
||||
fromDate.getTime() +
|
||||
Math.random() * (toDate.getTime() - fromDate.getTime() - 86400000)
|
||||
); // Ensure `to` is at least 1 day ahead
|
||||
const randomTo = new Date(
|
||||
randomFrom.getTime() +
|
||||
Math.random() * (toDate.getTime() - randomFrom.getTime())
|
||||
);
|
||||
|
||||
return {
|
||||
from: randomFrom.toISOString().replace("T", " ").split(".")[0],
|
||||
to: randomTo.toISOString().replace("T", " ").split(".")[0],
|
||||
};
|
||||
}
|
||||
|
||||
export {
|
||||
form,
|
||||
getRandomImageFile,
|
||||
testForm,
|
||||
generateRandomPassword,
|
||||
generateRandomStringWithSpaces,
|
||||
fillParagraphWithRandomHtml,
|
||||
generateRandomProductName,
|
||||
generateRandomUrl,
|
||||
generateRandomDateTimeRange,
|
||||
};
|
||||
221
packages/Webkul/Admin/tests/e2e-pw/utils/gdpr.ts
Normal file
221
packages/Webkul/Admin/tests/e2e-pw/utils/gdpr.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
import { expect } from "../setup";
|
||||
import { generateDescription } from "../utils/faker";
|
||||
|
||||
export async function enableGDPR(adminPage) {
|
||||
/**
|
||||
* Navigating to the gdpr configuration page.
|
||||
*/
|
||||
await adminPage.goto("admin/configuration/general/gdpr");
|
||||
|
||||
const isEnabled = adminPage.locator("label > div").first().check();
|
||||
|
||||
/**
|
||||
* If not enabled, then we enable it.
|
||||
*/
|
||||
if (!isEnabled) {
|
||||
const gdprsettingToggle = adminPage.locator("label > div").first();
|
||||
await gdprsettingToggle.waitFor({ state: "visible", timeout: 5000 });
|
||||
await adminPage.locator("label > div").first().click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifying enable state.
|
||||
*/
|
||||
const toggleInput = adminPage.locator("label > div").first();
|
||||
await expect(toggleInput).toBeChecked();
|
||||
|
||||
/**
|
||||
* Click the 'Save Configuration' button.
|
||||
*/
|
||||
await adminPage.getByRole("button", { name: "Save Configuration" }).click();
|
||||
}
|
||||
|
||||
export async function disableGDPR(adminPage) {
|
||||
/**
|
||||
* Navigating to the gdpr configuration page.
|
||||
*/
|
||||
await adminPage.goto("admin/configuration/general/gdpr");
|
||||
|
||||
const isDisabled = adminPage.locator("label > div").first().uncheck();
|
||||
|
||||
/**
|
||||
* If not disabled, then we disable it.
|
||||
*/
|
||||
if (!isDisabled) {
|
||||
const gdprsettingToggle = adminPage.locator("label > div").first();
|
||||
await gdprsettingToggle.waitFor({ state: "visible", timeout: 5000 });
|
||||
await adminPage.locator("label > div").first().click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifying disable state.
|
||||
*/
|
||||
const toggleInput = adminPage.locator("label > div").first();
|
||||
await expect(toggleInput).not.toBeChecked();
|
||||
|
||||
/**
|
||||
* Click the 'Save Configuration' button.
|
||||
*/
|
||||
await adminPage.getByRole("button", { name: "Save Configuration" }).click();
|
||||
}
|
||||
|
||||
export async function enableGDPRAgreement(adminPage) {
|
||||
const agreement = {
|
||||
checkboxLabel: "I agree with this statement.",
|
||||
content: generateDescription(),
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigating to the gdpr configuration page.
|
||||
*/
|
||||
await adminPage.goto("admin/configuration/general/gdpr");
|
||||
const isEnabled = adminPage
|
||||
.locator("div:nth-child(4) > div > .mb-4 > .relative > div")
|
||||
.first()
|
||||
.check();
|
||||
|
||||
/**
|
||||
* If not enabled, then we enable it.
|
||||
*/
|
||||
if (!isEnabled) {
|
||||
const gdprsettingToggle = adminPage
|
||||
.locator("div:nth-child(4) > div > .mb-4 > .relative > div")
|
||||
.first();
|
||||
await gdprsettingToggle.waitFor({ state: "visible", timeout: 5000 });
|
||||
await adminPage
|
||||
.locator("div:nth-child(4) > div > .mb-4 > .relative > div")
|
||||
.first()
|
||||
.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifying enable state.
|
||||
*/
|
||||
const toggleInput = adminPage
|
||||
.locator("div:nth-child(4) > div > .mb-4 > .relative > div")
|
||||
.first();
|
||||
await expect(toggleInput).toBeChecked();
|
||||
|
||||
/**
|
||||
* Fill the agreement checkbox label.
|
||||
*/
|
||||
await adminPage
|
||||
.getByRole("textbox", { name: "Agreement Checkbox Label" })
|
||||
.click();
|
||||
await adminPage
|
||||
.getByRole("textbox", {
|
||||
name: "Agreement Checkbox Label",
|
||||
})
|
||||
.fill(agreement.checkboxLabel);
|
||||
await adminPage.click('button[type="submit"].primary-button:visible');
|
||||
|
||||
/**
|
||||
* Fill the agreement content.
|
||||
*/
|
||||
await adminPage.waitForSelector('#general_gdpr__agreement__agreement_content__ifr', { state: 'visible' });
|
||||
await adminPage.frameLocator('#general_gdpr__agreement__agreement_content__ifr').locator('body').clear();
|
||||
await adminPage.fillInTinymce(
|
||||
"#general_gdpr__agreement__agreement_content__ifr",
|
||||
agreement.content
|
||||
);
|
||||
|
||||
/**
|
||||
* Save the configuration.
|
||||
*/
|
||||
await adminPage.click('button[type="submit"].primary-button:visible');
|
||||
|
||||
return agreement;
|
||||
}
|
||||
|
||||
export async function disableGDPRAgreement(adminPage) {
|
||||
/**
|
||||
* Navigating to the gdpr configuration page.
|
||||
*/
|
||||
await adminPage.goto("admin/configuration/general/gdpr");
|
||||
|
||||
const isDisabled = adminPage
|
||||
.locator("div:nth-child(4) > div > .mb-4 > .relative > div")
|
||||
.first()
|
||||
.uncheck();
|
||||
|
||||
/**
|
||||
* If not disabled, then we disable it.
|
||||
*/
|
||||
if (!isDisabled) {
|
||||
const gdprsettingToggle = adminPage
|
||||
.locator("div:nth-child(4) > div > .mb-4 > .relative > div")
|
||||
.first();
|
||||
await gdprsettingToggle.waitFor({ state: "visible", timeout: 5000 });
|
||||
await adminPage
|
||||
.locator("div:nth-child(4) > div > .mb-4 > .relative > div")
|
||||
.first()
|
||||
.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifying disable state.
|
||||
*/
|
||||
const toggleInput = adminPage
|
||||
.locator("div:nth-child(4) > div > .mb-4 > .relative > div")
|
||||
.first();
|
||||
await expect(toggleInput).not.toBeChecked();
|
||||
|
||||
/**
|
||||
* Click the 'Save Configuration' button.
|
||||
*/
|
||||
await adminPage.getByRole("button", { name: "Save Configuration" }).click();
|
||||
}
|
||||
|
||||
export async function enableCookiesNotice(adminPage, position = "bottom-left") {
|
||||
/**
|
||||
* Navigating to the gdpr configuration page.
|
||||
*/
|
||||
await adminPage.goto("admin/configuration/general/gdpr");
|
||||
|
||||
const isEnabled = adminPage
|
||||
.locator("div:nth-child(6) > div > .mb-4 > .relative > div")
|
||||
.check();
|
||||
|
||||
/**
|
||||
* If not enabled, then we enable it.
|
||||
*/
|
||||
if (!isEnabled) {
|
||||
const gdprsettingToggle = adminPage.locator(
|
||||
"div:nth-child(6) > div > .mb-4 > .relative > div"
|
||||
);
|
||||
await gdprsettingToggle.waitFor({ state: "visible", timeout: 5000 });
|
||||
await adminPage
|
||||
.locator("div:nth-child(6) > div > .mb-4 > .relative > div")
|
||||
.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifying enable state.
|
||||
*/
|
||||
const toggleInput = adminPage.locator(
|
||||
"div:nth-child(6) > div > .mb-4 > .relative > div"
|
||||
);
|
||||
await expect(toggleInput).toBeChecked();
|
||||
|
||||
await adminPage
|
||||
.locator('select[name="general[gdpr][cookie][position]"]')
|
||||
.selectOption(position);
|
||||
|
||||
await adminPage
|
||||
.getByRole("textbox", {
|
||||
name: "Static Block Identifier",
|
||||
})
|
||||
.fill("cookie block");
|
||||
|
||||
await adminPage
|
||||
.getByRole("textbox", {
|
||||
name: "Description",
|
||||
})
|
||||
.fill("this website uses cookies to ensure you");
|
||||
|
||||
/**
|
||||
* Save the configuration.
|
||||
*/
|
||||
await adminPage.click('button[type="submit"].primary-button:visible');
|
||||
}
|
||||
|
||||
17
packages/Webkul/Admin/tests/e2e-pw/utils/lang.ts
Normal file
17
packages/Webkul/Admin/tests/e2e-pw/utils/lang.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// Absolute path to /lang directory
|
||||
const LANG_DIR = path.resolve(
|
||||
__dirname,
|
||||
'../../../../../src/Resources/lang/'
|
||||
);
|
||||
|
||||
export const fetchConfig= async () => {
|
||||
const res = await fetch(LANG_DIR);
|
||||
const appConfig = await res.json();
|
||||
return appConfig;
|
||||
};
|
||||
49
packages/Webkul/Admin/tests/e2e-pw/utils/savedata.ts
Normal file
49
packages/Webkul/Admin/tests/e2e-pw/utils/savedata.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const dataFile = path.resolve(
|
||||
__dirname,
|
||||
"../test-data/runtime.json"
|
||||
);
|
||||
|
||||
function readJson(): Record<string, any> {
|
||||
if (!fs.existsSync(dataFile)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(dataFile, "utf-8"));
|
||||
} catch {
|
||||
// Corrupt or partial write protection
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function writeJson(data: Record<string, any>) {
|
||||
const dir = path.dirname(dataFile);
|
||||
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
export function saveData(key: string, value: unknown): void {
|
||||
const data = readJson();
|
||||
data[key] = value;
|
||||
writeJson(data);
|
||||
}
|
||||
|
||||
export function getData<T>(key: string): T {
|
||||
const data = readJson();
|
||||
|
||||
if (!(key in data)) {
|
||||
throw new Error(`❌ Runtime data key not found: "${key}"`);
|
||||
}
|
||||
|
||||
return data[key] as T;
|
||||
}
|
||||
29
packages/Webkul/Admin/tests/e2e-pw/utils/tinymce.ts
Normal file
29
packages/Webkul/Admin/tests/e2e-pw/utils/tinymce.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Page, expect } from "@playwright/test";
|
||||
|
||||
export class CommonPage {
|
||||
readonly page: Page;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill TinyMCE editor inside an iframe
|
||||
* @param iframeSelector - iframe CSS or XPath selector
|
||||
* @param content - text content to insert into TinyMCE
|
||||
*/
|
||||
async fillInTinymce(iframeSelector: string, content: string) {
|
||||
await this.page.waitForSelector(iframeSelector);
|
||||
const iframe = this.page.frameLocator(iframeSelector);
|
||||
const editorBody = iframe.locator("body");
|
||||
|
||||
await expect(editorBody).toBeVisible();
|
||||
|
||||
await editorBody.click();
|
||||
await editorBody.press("Control+A"); // Select all existing content
|
||||
await editorBody.press("Backspace"); // Clear existing content
|
||||
await editorBody.pressSequentially(content);
|
||||
|
||||
await expect(editorBody).toHaveText(content);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user