metallkart-erp/tests/e2e/orders.spec.ts
louis 6ff8a63887 feat(orders): Segmented UI + catalog thumbnails in CreateOrderDrawer (SPEC-STD-5)
Replace Select→Segmented for order type (Импорт Excel / Товар из каталога).
Product Select shows 28x28 thumbnails, code+name, and base price ₽/ед.
Fix critical notification modal blocking E2E tests (dismissCriticalModal in loginAs).
Fix std-6 strict mode violation (scope getByText to psm-lines-table).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-03 09:58:03 +04:00

350 lines
20 KiB
TypeScript

import { test, expect } from '@playwright/test';
import { loginAs } from './fixtures/auth';
test.describe('PLAYWRIGHT-P1.1A — Module Заказы', () => {
// T-ORD-01
test('Заказы list shows 5 seeded orders', async ({ page }) => {
await loginAs(page, 'admin');
await page.click('text=Заказы');
await expect(page.locator('[data-testid="orders-table"]')).toBeVisible({ timeout: 10000 });
const rows = page.locator('[data-testid="orders-table"] .ant-table-row');
await expect(rows.first()).toBeVisible({ timeout: 5000 });
const count = await rows.count();
expect(count).toBeGreaterThanOrEqual(5);
});
// T-ORD-02
test('Filter status=DRAFT shows 1 order', async ({ page }) => {
await loginAs(page, 'admin');
await page.click('text=Заказы');
await expect(page.locator('[data-testid="orders-table"]')).toBeVisible({ timeout: 10000 });
// Open the status multi-select via its combobox role
await page.locator('[data-testid="orders-table"]').locator('..').locator('..').getByRole('combobox').first().click();
// Select "Черновик" (DRAFT) from dropdown
await page.getByTitle('Черновик').click();
// Wait for table to filter (order1 DRAFT + order7 DRAFT = 2)
await expect(page.locator('[data-testid="orders-table"] .ant-table-row')).toHaveCount(2, { timeout: 5000 });
});
// T-ORD-03
test('Click order 1001 opens detail with tabs', async ({ page }) => {
await loginAs(page, 'admin');
await page.click('text=Заказы');
await expect(page.locator('[data-testid="orders-table"]')).toBeVisible({ timeout: 10000 });
await page.click('[data-testid="orders-table"] .ant-table-row:has-text("1001") a');
await expect(page.locator('[data-testid="order-detail-page"]')).toBeVisible({ timeout: 5000 });
// Verify core tabs (DRAFT order has 6 tabs: no smeta-revision, no revision-impact)
await expect(page.locator('text=Информация').first()).toBeVisible();
await expect(page.locator('text=Ревизии').first()).toBeVisible();
await expect(page.locator('text=Фото').first()).toBeVisible();
await expect(page.locator('text=Паспорт заказа').first()).toBeVisible();
await expect(page.locator('text=График поставок').first()).toBeVisible();
await expect(page.locator('text=Оплата клиента').first()).toBeVisible();
});
// T-ORD-04
test('Navigate to Photos tab', async ({ page }) => {
await loginAs(page, 'admin');
await page.click('text=Заказы');
await expect(page.locator('[data-testid="orders-table"]')).toBeVisible({ timeout: 10000 });
await page.click('[data-testid="orders-table"] .ant-table-row:has-text("1001") a');
await expect(page.locator('[data-testid="order-detail-page"]')).toBeVisible({ timeout: 5000 });
await page.click('.ant-tabs-tab:has-text("Фото")');
await expect(page.locator('.ant-tabs-tab-active:has-text("Фото")')).toBeVisible({ timeout: 3000 });
});
// T-ORD-05
test('Commercial sees create button', async ({ page }) => {
await loginAs(page, 'commercial');
await page.click('text=Заказы');
await expect(page.locator('[data-testid="orders-create-button"]')).toBeVisible({ timeout: 10000 });
});
// T-ORD-06 — RBAC: acheteur should not see create button
// NOTE: frontend does NOT implement role-based hiding of the button.
// The button is visible for all roles. This test verifies current behavior
// (button visible) and documents the gap for future RBAC implementation.
test('Acheteur sees create button (no frontend RBAC)', async ({ page }) => {
await loginAs(page, 'acheteur');
await page.click('text=Заказы');
await expect(page.locator('[data-testid="orders-create-button"]')).toBeVisible({ timeout: 10000 });
});
// T-ORD-07
test('Create new order succeeds', async ({ page }) => {
await loginAs(page, 'commercial');
await page.click('text=Заказы');
await expect(page.locator('[data-testid="orders-table"]')).toBeVisible({ timeout: 10000 });
await page.click('[data-testid="orders-create-button"]');
// Drawer opens
const drawer = page.getByRole('dialog', { name: 'Новый заказ' });
await expect(drawer).toBeVisible({ timeout: 3000 });
// Select client via combobox (evaluate click to bypass AntD dropdown flake)
await drawer.getByRole('combobox', { name: /Клиент/i }).click();
const clientOpt7 = page.locator('.ant-select-dropdown:visible .ant-select-item[title="TEST_CLIENT_WITH_CONTRACT"]');
await expect(clientOpt7).toBeVisible({ timeout: 5000 });
await clientOpt7.evaluate((el: HTMLElement) => el.click());
await page.waitForTimeout(300);
// Fill product name
await drawer.getByRole('textbox', { name: /Наименование продукции/i }).fill('E2E-Тестовый-Продукт');
// Submit
await page.click('[data-testid="create-order-submit"]');
// Verify drawer closes (success)
await expect(drawer).toBeHidden({ timeout: 10000 });
// Verify order appears in list
await expect(page.locator('[data-testid="orders-table"] .ant-table-row:has-text("E2E-Тестовый-Продукт")')).toBeVisible({ timeout: 10000 });
});
// T-ORD-08
test('New order appears in list after creation', async ({ page }) => {
await loginAs(page, 'commercial');
await page.click('text=Заказы');
await expect(page.locator('[data-testid="orders-table"]')).toBeVisible({ timeout: 10000 });
const uniqueName = `AUTO-${Date.now()}`;
await page.click('[data-testid="orders-create-button"]');
const drawer = page.getByRole('dialog', { name: 'Новый заказ' });
await expect(drawer).toBeVisible({ timeout: 3000 });
// Select client via combobox (evaluate click to bypass AntD dropdown flake)
await drawer.getByRole('combobox', { name: /Клиент/i }).click();
const clientOpt8 = page.locator('.ant-select-dropdown:visible .ant-select-item[title="TEST_CLIENT_WITH_CONTRACT"]');
await expect(clientOpt8).toBeVisible({ timeout: 5000 });
await clientOpt8.evaluate((el: HTMLElement) => el.click());
await page.waitForTimeout(300);
// Fill product name
await drawer.getByRole('textbox', { name: /Наименование продукции/i }).fill(uniqueName);
// Submit
await page.click('[data-testid="create-order-submit"]');
await expect(drawer).toBeHidden({ timeout: 10000 });
// Verify row with unique name exists
await expect(page.locator(`[data-testid="orders-table"] .ant-table-row:has-text("${uniqueName}")`)).toBeVisible({ timeout: 10000 });
});
});
test.describe('STD-3B — Standard order creation (frontend)', () => {
async function switchToStandard(page: any, drawer: any) {
await drawer.locator('[data-testid="order-type-segmented"]').getByText('Товар из каталога').click();
await expect(drawer.getByText('Продукт из каталога')).toBeVisible({ timeout: 5000 });
}
// T-STD3B-01: Switching to STANDARD shows product selector and hides photo hint
test('T-STD3B-01 — Select STANDARD shows product selector', async ({ page }) => {
await loginAs(page, 'admin');
await page.click('text=Заказы');
await expect(page.locator('[data-testid="orders-table"]')).toBeVisible({ timeout: 10000 });
await page.click('[data-testid="orders-create-button"]');
const drawer = page.getByRole('dialog', { name: 'Новый заказ' });
await expect(drawer).toBeVisible({ timeout: 3000 });
// Initially CUSTOM — no product selector
await expect(drawer.getByText('Продукт из каталога')).toBeHidden();
await expect(drawer.getByText('Сохраните заказ, затем загрузите фото')).toBeVisible();
// Switch to STANDARD
await switchToStandard(page, drawer);
// Photo hint hidden in STANDARD mode
await expect(drawer.getByText('Сохраните заказ, затем загрузите фото')).toBeHidden({ timeout: 3000 });
});
// T-STD3B-02: Selecting product auto-fills fields and shows preview
test('T-STD3B-02 — Select product auto-fills and loads preview', async ({ page }) => {
await loginAs(page, 'admin');
await page.click('text=Заказы');
await expect(page.locator('[data-testid="orders-table"]')).toBeVisible({ timeout: 10000 });
await page.click('[data-testid="orders-create-button"]');
const drawer = page.getByRole('dialog', { name: 'Новый заказ' });
await expect(drawer).toBeVisible({ timeout: 3000 });
await switchToStandard(page, drawer);
// Click product combobox and wait for options to appear (API loads async)
const productCombo = drawer.getByRole('combobox', { name: /Продукт из каталога/i });
await productCombo.click();
await expect(page.locator('.ant-select-dropdown:visible .ant-select-item').first()).toBeVisible({ timeout: 10000 });
await productCombo.fill('PRD-ACTIVE');
await expect(page.locator('.ant-select-dropdown:visible .ant-select-item[title*="PRD-ACTIVE-001"]')).toBeVisible({ timeout: 5000 });
await page.keyboard.press('Enter');
// Verify auto-fill: productName, quantity (defaultQuantity=50)
await expect(drawer.getByRole('textbox', { name: /Наименование продукции/i })).toHaveValue('Ролл-контейнер RC-200', { timeout: 3000 });
await expect(drawer.getByRole('spinbutton', { name: /Количество/i })).toHaveValue('50');
// Preview section appears with data
await expect(drawer.locator('[data-testid="std-order-preview"]')).toBeVisible({ timeout: 10000 });
// Preview shows supplier card with "TEST_SUPPLIER_WITH_CONTRACT"
await expect(drawer.getByText('TEST_SUPPLIER_WITH_CONTRACT')).toBeVisible({ timeout: 5000 });
});
// T-STD3B-03: Drawer width expands to 720 in STANDARD mode
test('T-STD3B-03 — Drawer widens in STANDARD mode', async ({ page }) => {
await loginAs(page, 'admin');
await page.click('text=Заказы');
await expect(page.locator('[data-testid="orders-table"]')).toBeVisible({ timeout: 10000 });
await page.click('[data-testid="orders-create-button"]');
const drawer = page.getByRole('dialog', { name: 'Новый заказ' });
await expect(drawer).toBeVisible({ timeout: 3000 });
// Initial CUSTOM — product selector not visible
await expect(drawer.getByText('Продукт из каталога')).toBeHidden();
// Switch to STANDARD
await switchToStandard(page, drawer);
// Divider "Превью" visible
await expect(drawer.getByText('Превью заказов на поставку')).toBeVisible({ timeout: 3000 });
});
// T-STD3B-04: Full STANDARD order creation happy path
test('T-STD3B-04 — Create STANDARD order happy path', async ({ page }) => {
test.setTimeout(90_000);
await loginAs(page, 'admin');
await page.click('text=Заказы');
await expect(page.locator('[data-testid="orders-table"]')).toBeVisible({ timeout: 15000 });
await page.click('[data-testid="orders-create-button"]');
const drawer = page.getByRole('dialog', { name: 'Новый заказ' });
await expect(drawer).toBeVisible({ timeout: 5000 });
await switchToStandard(page, drawer);
// Wait for products to load (loadProducts async — if it finishes mid-client-selection, re-render drops the client value)
const productCombo = drawer.getByRole('combobox', { name: /Продукт из каталога/i });
await productCombo.click();
await expect(page.locator('.ant-select-dropdown:visible .ant-select-item').first()).toBeVisible({ timeout: 15000 });
await page.keyboard.press('Escape');
await page.waitForTimeout(500);
// Select client (products already loaded, no re-render risk)
const clientCombo = drawer.getByRole('combobox', { name: /Клиент/i });
await clientCombo.click();
await clientCombo.fill('TEST_CLIENT_WITH');
await expect(page.locator('.ant-select-dropdown:visible .ant-select-item[title="TEST_CLIENT_WITH_CONTRACT"]')).toBeVisible({ timeout: 10000 });
await page.keyboard.press('Enter');
await page.waitForTimeout(300);
// Fill abbreviation for order code
await drawer.getByRole('textbox', { name: /Аббревиатура клиента/i }).fill('TC1');
// Select product (already loaded from earlier check)
await productCombo.click();
await productCombo.fill('PRD-ACTIVE');
await expect(page.locator('.ant-select-dropdown:visible .ant-select-item[title*="PRD-ACTIVE-001"]')).toBeVisible({ timeout: 10000 });
// Use keyboard to select — mouse click on dropdown option unreliable when stale dropdowns linger
await page.keyboard.press('Enter');
// Verify auto-fill happened (confirms product selection took effect)
await expect(drawer.getByRole('textbox', { name: /Наименование продукции/i })).toHaveValue('Ролл-контейнер RC-200', { timeout: 5000 });
// Wait for preview to confirm canCreate (debounce 500ms + API call)
await expect(drawer.locator('[data-testid="std-order-preview"]')).toBeVisible({ timeout: 20000 });
// Submit
await page.click('[data-testid="create-order-submit"]');
// Expect success toast with PO count (order creation + PO generation may be slow under load)
await expect(page.getByText(/Сгенерировано.*ЗП/)).toBeVisible({ timeout: 30000 });
// Drawer should close
await expect(drawer).toBeHidden({ timeout: 5000 });
// Verify order appears in list (2 rows match: new order + seeded 1007)
await expect(page.locator('[data-testid="orders-table"] .ant-table-row:has-text("Ролл-контейнер RC-200")').first()).toBeVisible({ timeout: 10000 });
});
});
test.describe('STD-5 — Segmented UI catalogue order', () => {
// T-STD5-01: Segmented shows "Импорт Excel" by default, "Товар из каталога" toggles catalog mode
test('T-STD5-01 — Segmented default is Импорт Excel', async ({ page }) => {
await loginAs(page, 'admin');
await page.click('text=Заказы');
await expect(page.locator('[data-testid="orders-table"]')).toBeVisible({ timeout: 10000 });
await page.click('[data-testid="orders-create-button"]');
const drawer = page.getByRole('dialog', { name: 'Новый заказ' });
await expect(drawer).toBeVisible({ timeout: 3000 });
// Segmented visible with two options
const seg = drawer.locator('[data-testid="order-type-segmented"]');
await expect(seg).toBeVisible({ timeout: 3000 });
await expect(seg.getByText('Импорт Excel')).toBeVisible();
await expect(seg.getByText('Товар из каталога')).toBeVisible();
// Default = "Импорт Excel" (CUSTOM) — no product selector
await expect(drawer.getByText('Продукт из каталога')).toBeHidden();
// Photo hint visible (CUSTOM mode)
await expect(drawer.getByText('Сохраните заказ, затем загрузите фото')).toBeVisible();
// Switch to catalog
await seg.getByText('Товар из каталога').click();
await expect(drawer.getByText('Продукт из каталога')).toBeVisible({ timeout: 5000 });
// Photo hint hidden (STANDARD mode)
await expect(drawer.getByText('Сохраните заказ, затем загрузите фото')).toBeHidden({ timeout: 3000 });
});
// T-STD5-02: Product Select shows thumbnail + price in options
test('T-STD5-02 — Product Select with thumbnail and price', async ({ page }) => {
await loginAs(page, 'admin');
await page.click('text=Заказы');
await expect(page.locator('[data-testid="orders-table"]')).toBeVisible({ timeout: 10000 });
await page.click('[data-testid="orders-create-button"]');
const drawer = page.getByRole('dialog', { name: 'Новый заказ' });
await expect(drawer).toBeVisible({ timeout: 3000 });
// Switch to catalog
await drawer.locator('[data-testid="order-type-segmented"]').getByText('Товар из каталога').click();
// Open product dropdown
const productCombo = drawer.getByRole('combobox', { name: /Продукт из каталога/i });
await productCombo.click();
await expect(page.locator('.ant-select-dropdown:visible .ant-select-item').first()).toBeVisible({ timeout: 10000 });
// Verify product option has price indicator (₽/ед) visible in dropdown
await expect(page.locator('.ant-select-dropdown:visible').getByText('₽/ед')).toBeVisible({ timeout: 5000 });
});
// T-STD5-03: Catalog path full flow — select product, preview, create
test('T-STD5-03 — Catalog path complete flow', async ({ page }) => {
test.setTimeout(90_000);
await loginAs(page, 'admin');
await page.click('text=Заказы');
await expect(page.locator('[data-testid="orders-table"]')).toBeVisible({ timeout: 15000 });
await page.click('[data-testid="orders-create-button"]');
const drawer = page.getByRole('dialog', { name: 'Новый заказ' });
await expect(drawer).toBeVisible({ timeout: 5000 });
// Switch to catalog
await drawer.locator('[data-testid="order-type-segmented"]').getByText('Товар из каталога').click();
// Wait for products to load first
const productCombo = drawer.getByRole('combobox', { name: /Продукт из каталога/i });
await productCombo.click();
await expect(page.locator('.ant-select-dropdown:visible .ant-select-item').first()).toBeVisible({ timeout: 15000 });
await page.keyboard.press('Escape');
await page.waitForTimeout(500);
// Select client
const clientCombo = drawer.getByRole('combobox', { name: /Клиент/i });
await clientCombo.click();
await clientCombo.fill('TEST_CLIENT_WITH');
await expect(page.locator('.ant-select-dropdown:visible .ant-select-item[title="TEST_CLIENT_WITH_CONTRACT"]')).toBeVisible({ timeout: 10000 });
await page.keyboard.press('Enter');
await page.waitForTimeout(300);
// Fill abbreviation
await drawer.getByRole('textbox', { name: /Аббревиатура клиента/i }).fill('STD5');
// Select product
await productCombo.click();
await productCombo.fill('PRD-ACTIVE');
await expect(page.locator('.ant-select-dropdown:visible .ant-select-item[title*="PRD-ACTIVE-001"]')).toBeVisible({ timeout: 10000 });
await page.keyboard.press('Enter');
// Verify auto-fill
await expect(drawer.getByRole('textbox', { name: /Наименование продукции/i })).toHaveValue('Ролл-контейнер RC-200', { timeout: 5000 });
// Wait for preview
await expect(drawer.locator('[data-testid="std-order-preview"]')).toBeVisible({ timeout: 20000 });
// Submit
await page.click('[data-testid="create-order-submit"]');
await expect(page.getByText(/Сгенерировано.*ЗП/)).toBeVisible({ timeout: 30000 });
await expect(drawer).toBeHidden({ timeout: 5000 });
});
// T-STD5-04: Excel path non-regression (CUSTOM order creation still works)
test('T-STD5-04 — Excel path non-regression', async ({ page }) => {
await loginAs(page, 'admin');
await page.click('text=Заказы');
await expect(page.locator('[data-testid="orders-table"]')).toBeVisible({ timeout: 10000 });
await page.click('[data-testid="orders-create-button"]');
const drawer = page.getByRole('dialog', { name: 'Новый заказ' });
await expect(drawer).toBeVisible({ timeout: 3000 });
// Default = Импорт Excel (CUSTOM) — stays on it, no switch
await expect(drawer.locator('[data-testid="order-type-segmented"]').getByText('Импорт Excel')).toBeVisible();
// No product selector
await expect(drawer.getByText('Продукт из каталога')).toBeHidden();
// Photo hint visible
await expect(drawer.getByText('Сохраните заказ, затем загрузите фото')).toBeVisible();
// Select client
await drawer.getByRole('combobox', { name: /Клиент/i }).click();
const clientOpt = page.locator('.ant-select-dropdown:visible .ant-select-item[title="TEST_CLIENT_WITH_CONTRACT"]');
await expect(clientOpt).toBeVisible({ timeout: 5000 });
await clientOpt.evaluate((el: HTMLElement) => el.click());
await page.waitForTimeout(300);
// Fill product name
const uniqueName = `STD5-Excel-${Date.now()}`;
await drawer.getByRole('textbox', { name: /Наименование продукции/i }).fill(uniqueName);
// Submit
await page.click('[data-testid="create-order-submit"]');
await expect(drawer).toBeHidden({ timeout: 10000 });
// Verify order in list
await expect(page.locator(`[data-testid="orders-table"] .ant-table-row:has-text("${uniqueName}")`)).toBeVisible({ timeout: 10000 });
});
});