mirror of
https://github.com/dartdavros/chatballs.git
synced 2026-10-05 09:14:58 +03:00
249 lines
13 KiB
TypeScript
249 lines
13 KiB
TypeScript
import { expect, type Page, test } from "@playwright/test";
|
|
|
|
const ORGANIZATION_PUBLIC_ID = "123e4567-e89b-12d3-a456-426614174000";
|
|
const SECOND_ORGANIZATION_PUBLIC_ID = "223e4567-e89b-12d3-a456-426614174000";
|
|
|
|
// Эти сценарии относятся только к internal-ui; на других проектах пропускаем.
|
|
test.beforeEach(({}, testInfo) => {
|
|
test.skip(testInfo.project.name !== "internal-ui", "internal-ui only");
|
|
});
|
|
|
|
const OWNER = {
|
|
id: 1,
|
|
email: "owner@edevs.tech",
|
|
fullName: "Владелец",
|
|
role: "OWNER",
|
|
positionTitle: "Владелец",
|
|
organization: "edevs",
|
|
organizationName: "Edevs",
|
|
department: null,
|
|
mustChangePassword: false,
|
|
totpRequired: false,
|
|
totpEnabled: false,
|
|
capabilities: [
|
|
"company.view", "departments.view", "employees.view", "employees.manage", "employees.manage_privileged", "ownership.transfer", "products.view", "ai.view",
|
|
"ai.manage", "integrations.view", "conversations.view", "customers.view", "sales.view",
|
|
"support.view",
|
|
],
|
|
accessScopes: [{
|
|
scopeType: "ORGANIZATION",
|
|
departmentId: null,
|
|
departmentCode: null,
|
|
capabilities: [
|
|
"company.view", "departments.view", "employees.view", "employees.manage", "employees.manage_privileged", "ownership.transfer", "products.view", "ai.view",
|
|
"ai.manage", "integrations.view", "conversations.view", "customers.view", "sales.view",
|
|
"support.view",
|
|
],
|
|
}],
|
|
};
|
|
|
|
// «Оператор» — рабочая функция обычного сотрудника (EMPLOYEE) в отделе (ADR-HUB-0027).
|
|
const OPERATOR_CAPABILITIES = ["conversations.view", "conversations.operate", "customers.view", "sales.view", "sales.operate"];
|
|
const OPERATOR = {
|
|
...OWNER,
|
|
id: 2,
|
|
email: "operator@edevs.tech",
|
|
fullName: "Оператор",
|
|
role: "EMPLOYEE",
|
|
positionTitle: "Оператор отдела продаж",
|
|
department: "sales",
|
|
capabilities: OPERATOR_CAPABILITIES,
|
|
accessScopes: [{
|
|
scopeType: "DEPARTMENT",
|
|
departmentId: 1,
|
|
departmentCode: "sales",
|
|
capabilities: OPERATOR_CAPABILITIES,
|
|
}],
|
|
};
|
|
|
|
const identityFor = (membership: typeof OWNER) => ({
|
|
id: membership.id,
|
|
email: membership.email,
|
|
fullName: membership.fullName,
|
|
mustChangePassword: membership.mustChangePassword,
|
|
totpEnabled: membership.totpEnabled,
|
|
memberships: [{
|
|
id: membership.id,
|
|
organizationPublicId: ORGANIZATION_PUBLIC_ID,
|
|
organization: membership.organization,
|
|
organizationName: membership.organizationName,
|
|
role: membership.role,
|
|
positionTitle: membership.positionTitle,
|
|
department: membership.department,
|
|
totpRequired: membership.totpRequired,
|
|
capabilities: membership.capabilities,
|
|
accessScopes: membership.accessScopes,
|
|
}],
|
|
});
|
|
|
|
const OWNER_IDENTITY = identityFor(OWNER);
|
|
const OPERATOR_IDENTITY = identityFor(OPERATOR);
|
|
|
|
async function mockData(page: Page) {
|
|
const permissions = {
|
|
canView: true, canUpdateProfile: true, canChangeRole: true, canChangePlacement: true,
|
|
canChangeAccess: true, canBlock: true, canUnblock: false, canResetPassword: true,
|
|
canTerminateSessions: true, canTransferOwnership: false,
|
|
};
|
|
const employee = {
|
|
id: 7, email: "d.sokolov@edevs.tech", fullName: "Дмитрий Соколов", phone: "+7 903 118 77 51",
|
|
role: "EMPLOYEE", positionTitle: "Менеджер по продажам", department: "sales", departmentName: "Отдел продаж",
|
|
createdAt: "2026-05-20T10:00:00Z", lastLogin: "2026-07-13T08:30:00Z", isActive: true, isBlocked: false,
|
|
mustChangePassword: false, totpRequired: false, totpEnabled: true, permissions,
|
|
accessAssignments: [{
|
|
id: 11, profileId: 1, profileName: "Продажи · оператор", scopeType: "DEPARTMENT",
|
|
departmentId: 1, departmentCode: "sales", departmentName: "Отдел продаж",
|
|
capabilities: ["sales.view", "sales.operate", "customers.view", "conversations.view"],
|
|
}],
|
|
};
|
|
const ownerEmployee = {
|
|
id: 1, email: OWNER.email, fullName: OWNER.fullName, phone: "+7 916 000 11 22",
|
|
role: "OWNER", positionTitle: OWNER.positionTitle, department: null, departmentName: null,
|
|
createdAt: "2026-02-02T10:00:00Z", lastLogin: "2026-07-13T08:40:00Z", isActive: true, isBlocked: false,
|
|
mustChangePassword: false, totpRequired: false, totpEnabled: true,
|
|
permissions: { ...permissions, canChangeRole: false, canChangePlacement: false, canTransferOwnership: true },
|
|
accessAssignments: [],
|
|
};
|
|
const profiles = [{
|
|
id: 1, name: "Продажи · оператор", description: "Работа с диалогами, клиентами и продажами отдела.",
|
|
isSystem: false, isActive: true, capabilities: ["sales.view", "sales.operate", "customers.view", "conversations.view"],
|
|
allowedScopes: ["DEPARTMENT", "ORGANIZATION"], assignedCount: 1,
|
|
}];
|
|
const capabilities = [
|
|
{ code: "sales.view", name: "Просмотр продаж", description: "", allowedScopes: ["DEPARTMENT", "ORGANIZATION"], assignable: true, protected: false },
|
|
{ code: "sales.operate", name: "Работа с продажами", description: "", allowedScopes: ["DEPARTMENT", "ORGANIZATION"], assignable: true, protected: false },
|
|
{ code: "employees.manage_privileged", name: "Управление привилегированными сотрудниками", description: "", allowedScopes: ["ORGANIZATION"], assignable: false, protected: true },
|
|
{ code: "ownership.transfer", name: "Передача владения", description: "", allowedScopes: ["ORGANIZATION"], assignable: false, protected: true },
|
|
];
|
|
await page.route("**/api/v1/organizations/*/access-profiles/capabilities/", (route) => route.fulfill({ json: { items: capabilities } }));
|
|
await page.route("**/api/v1/organizations/*/access-profiles/", (route) => route.fulfill({ json: { items: profiles } }));
|
|
await page.route("**/api/v1/organizations/*/employees/**", (route) => {
|
|
const path = new URL(route.request().url()).pathname;
|
|
if (path.endsWith("/employees/7/")) {
|
|
return route.fulfill({ json: { employee: { ...employee, activeSessionCount: 1, auditEvents: [{ action: "identity.employee_created", result: "SUCCESS", createdAt: "2026-05-20T10:00:00Z" }] } } });
|
|
}
|
|
return route.fulfill({ json: { items: [ownerEmployee, employee] } });
|
|
});
|
|
await page.route("**/api/v1/organizations/*/company/departments/**", (route) => route.fulfill({ json: { items: [{ id: 1, code: "sales", name: "Отдел продаж", status: "ACTIVE", memberCount: 1, operatorCount: 1, activeOperatorCount: 1, agentCount: 0, products: [] }] } }));
|
|
await page.route("**/api/v1/organizations/*/company/products/**", (route) => route.fulfill({ json: { items: [] } }));
|
|
await page.route("**/api/v1/organizations/*/ai/agents/**", (route) => route.fulfill({ json: { items: [] } }));
|
|
}
|
|
|
|
async function mockSession(page: Page, user: object | null) {
|
|
await page.route("**/api/v1/auth/session/", (route) =>
|
|
route.fulfill({ json: user ? { authenticated: true, user } : { authenticated: false } }),
|
|
);
|
|
}
|
|
|
|
async function login(page: Page, user: object) {
|
|
await mockSession(page, null);
|
|
await mockData(page);
|
|
await page.route("**/api/v1/auth/login/", (route) => route.fulfill({ json: { authenticated: true, user } }));
|
|
await page.goto("/");
|
|
await page.getByPlaceholder("you@domain.ru").fill("user@edevs.tech");
|
|
await page.getByPlaceholder("Пароль").fill("Password-123");
|
|
await page.getByRole("button", { name: "Войти" }).click();
|
|
}
|
|
|
|
test("OWNER logs in and sees every available sidebar section collapsed", async ({ page }) => {
|
|
await login(page, OWNER_IDENTITY);
|
|
|
|
await expect(page).toHaveURL(new RegExp(`/organizations/${ORGANIZATION_PUBLIC_ID}/`));
|
|
await expect(page.getByRole("heading", { name: "Командный центр" })).toBeVisible();
|
|
await expect(page.locator(".hub-nav-section-toggle", { hasText: "Продажи" })).toHaveAttribute("aria-expanded", "false");
|
|
await expect(page.locator(".hub-nav-section-toggle", { hasText: "Поддержка" })).toHaveAttribute("aria-expanded", "false");
|
|
await expect(page.locator(".hub-nav-section-toggle", { hasText: "AI" })).toHaveAttribute("aria-expanded", "false");
|
|
await expect(page.getByRole("button", { name: "Отделы", exact: true })).toHaveCount(0);
|
|
});
|
|
|
|
test("OPERATOR logs in and sees the accessible department in the main sidebar", async ({ page }) => {
|
|
await login(page, OPERATOR_IDENTITY);
|
|
|
|
await expect(page).toHaveURL(/\/departments\/sales\/dialogs/);
|
|
await expect(page.locator(".hub-nav-section-toggle", { hasText: "Продажи" })).toHaveAttribute("aria-expanded", "false");
|
|
await expect(page.getByRole("button", { name: "Поддержка", exact: true })).toHaveCount(0);
|
|
});
|
|
|
|
test("OPERATOR opening an owner-only route sees the 403 permission screen", async ({ page }) => {
|
|
await mockSession(page, OPERATOR_IDENTITY);
|
|
await mockData(page);
|
|
|
|
await page.goto("/employees");
|
|
|
|
await expect(page.getByText("403 · Доступ запрещён")).toBeVisible();
|
|
await expect(page.getByRole("button", { name: "Вернуться" })).toBeVisible();
|
|
});
|
|
|
|
test("the organization URL selects one membership without a global session tenant", async ({ page }) => {
|
|
const secondMembership = {
|
|
...OWNER_IDENTITY.memberships[0],
|
|
id: 3,
|
|
organizationPublicId: SECOND_ORGANIZATION_PUBLIC_ID,
|
|
organization: "second",
|
|
organizationName: "Second",
|
|
};
|
|
const requests: string[] = [];
|
|
page.on("request", (request) => {
|
|
if (request.url().includes("/api/v1/organizations/")) requests.push(request.url());
|
|
});
|
|
await mockSession(page, {
|
|
...OWNER_IDENTITY,
|
|
memberships: [...OWNER_IDENTITY.memberships, secondMembership],
|
|
});
|
|
await mockData(page);
|
|
|
|
await page.goto(`/organizations/${SECOND_ORGANIZATION_PUBLIC_ID}/`);
|
|
|
|
await expect(page.getByRole("heading", { name: "Командный центр" })).toBeVisible();
|
|
await expect.poll(() => requests.some((url) => url.includes(SECOND_ORGANIZATION_PUBLIC_ID))).toBe(true);
|
|
});
|
|
|
|
test("internal UI renders at the minimum supported width of 1024px", async ({ page }) => {
|
|
await page.setViewportSize({ width: 1024, height: 768 });
|
|
await mockSession(page, OWNER_IDENTITY);
|
|
await mockData(page);
|
|
|
|
await page.goto("/");
|
|
|
|
await expect(page.getByRole("heading", { name: "Командный центр" })).toBeVisible();
|
|
await page.screenshot({ path: "test-results/internal-ui-owner-1024.png" });
|
|
});
|
|
|
|
test("employee stage 3 screens follow the approved baseline", async ({ page }) => {
|
|
await page.setViewportSize({ width: 1440, height: 940 });
|
|
await mockSession(page, OWNER_IDENTITY);
|
|
await mockData(page);
|
|
|
|
await page.goto("/employees");
|
|
await expect(page.getByRole("heading", { name: "Сотрудники" })).toBeVisible();
|
|
await expect(page.getByText("Дмитрий Соколов")).toBeVisible();
|
|
await expect(page.getByRole("columnheader", { name: "ДОСТУП" })).toBeVisible();
|
|
await page.screenshot({ path: "test-results/employees-stage-3-list.png", fullPage: true });
|
|
|
|
await page.getByRole("button", { name: "Добавить сотрудника" }).click();
|
|
await expect(page.getByRole("complementary", { name: "Новый сотрудник" })).toBeVisible();
|
|
await expect(page.getByRole("heading", { name: "1 Учётные данные" })).toBeVisible();
|
|
await page.screenshot({ path: "test-results/employees-stage-3-create.png", fullPage: true });
|
|
await page.getByRole("button", { name: "Закрыть", exact: true }).click();
|
|
|
|
await page.getByRole("button", { name: "Действия: Владелец" }).click();
|
|
await page.getByRole("button", { name: "Передать владение" }).click();
|
|
const transferDialog = page.getByRole("dialog", { name: "Передача владения" });
|
|
await expect(transferDialog).toBeVisible();
|
|
await page.screenshot({ path: "test-results/employees-stage-3-ownership.png", fullPage: true });
|
|
await transferDialog.getByRole("button", { name: "Отмена" }).click();
|
|
|
|
await page.getByRole("button", { name: /^Дмитрий Соколов d\.sokolov/ }).click();
|
|
await expect(page).toHaveURL(/\/employees\/7$/);
|
|
await expect(page.getByRole("heading", { name: "Дмитрий Соколов" })).toBeVisible();
|
|
await expect(page.getByRole("heading", { name: "Профили доступа и scopes" })).toBeVisible();
|
|
await page.screenshot({ path: "test-results/employees-stage-3-detail.png", fullPage: true });
|
|
|
|
await page.getByRole("button", { name: "Все сотрудники" }).click();
|
|
await page.getByRole("button", { name: "Профили доступа" }).click();
|
|
await expect(page).toHaveURL(/\/employees\/access-profiles$/);
|
|
await expect(page.getByRole("heading", { name: "Профили доступа", exact: true })).toBeVisible();
|
|
await expect(page.getByText("Продажи · оператор").first()).toBeVisible();
|
|
await page.screenshot({ path: "test-results/employees-stage-3-profiles.png", fullPage: true });
|
|
});
|