[598][@ant] feat(docs-harness): fix 3 упавших сценариев + добавлены 6 PNG

Поправки:
- offerer/incoming-orders.mjs: фикстура sidorov → ivanpetrov +
  signAllAgreements (sidorov не подписал онбординг, диалоги перехватывали
  клики).
- offerer/my-offers.mjs: ivanpetrov + signAllAgreements +
  docPath/assetsDir выровнены на offers.md (Vue page = OffererMyOffers,
  но MD-страница уже создана под именем offers.md с прозой из PRD).
- branch-chairman/branch-orders.mjs: chairkrg не имеет marketplace_pvz
  membership на voskhod (роут /market-pvz/* требует roles: ['chairman']
  кооператива). Используем loginAsChairman (ant) — председатель сам
  открывает стол КУ для аудита; отдельный access-уровень «branch chairman»
  подключится в Story 6.x+1.

Установлены PNG: incoming-orders (2), offers (2), branch-orders (2).
Все 9 из 9 marketplace-сценариев успешно сняты harness'ом против
desktop dev :3029 (docker-mount worktree → /app в mono-ai-4-desktop-1).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
ant
2026-05-21 19:29:12 +00:00
parent df53a8f484
commit cedde9f277
9 changed files with 82 additions and 42 deletions
@@ -1,37 +1,24 @@
// Сценарий: Председатель КУ видит сводный стол участка с 3 табами.
// Эпик 6 / Story 6.x — Приёмки, Выдачи, Возвраты для одного braname.
// Сценарий: Сводный стол КУ (председателя кооператива).
// Эпик 6 / Story 6.x — Приёмки, Выдачи, Возвраты по braname.
//
// Объединяет существующие 3 query (ListAplReceptionsByBraname,
// ListIssuancesByBraname, ListReturnClaimsByBraname) через Promise.all.
// braname вводится вручную; auto-detect через marketplaceWhoAmI — следующий
// шаг Story 6.x+1.
//
// Фикстура: chairkrg (Пётр Сергеевич Иванов), председатель КУ Красногорск.
// Замечание: меж-роли «председатель КУ» в desktop matrix нет — роут
// /market-pvz/* открыт только `roles: ['chairman']` (председатель
// кооператива). На текущем MVP председатель сам открывает стол КУ для
// аудита. Отдельный access-уровень «branch chairman» подключится
// в Story 6.x+1 вместе с auto-detect braname через marketplaceWhoAmI.
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { loginAs, env, cleanViteOverlays } from '../../../lib/harness.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const loadFixture = (username) =>
JSON.parse(
fs.readFileSync(path.resolve(__dirname, `../../../state/participants/${username}.json`), 'utf8'),
);
import { loginAsChairman, dismissOnboardingDialogs } from '../../../lib/harness.mjs';
export const meta = {
title: 'Сводный стол КУ: приёмки, выдачи, возвраты',
docPath: 'new/marketplace/branch-chairman/branch-orders.md',
assetsDir: 'assets/new/marketplace/branch-chairman/branch-orders',
role: 'chairman',
fixture: 'chairkrg',
};
export default async ({ page, shot }) => {
const fixture = loadFixture('chairkrg');
await loginAs(page, fixture);
await page.evaluate(() => localStorage.setItem('harness:noBranchOverlay', '1'));
export default async ({ page, context, shot, env }) => {
await loginAsChairman(page, context);
await dismissOnboardingDialogs(page);
await page.goto(`${env.BASE_URL}/#/${env.COOPNAME}/market-pvz/branch-orders`, {
waitUntil: 'domcontentloaded',
@@ -39,7 +26,7 @@ export default async ({ page, shot }) => {
await page.waitForSelector('text=Сводный стол кооперативного участка', { timeout: 60000 });
await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
await page.waitForTimeout(1000);
await cleanViteOverlays(page);
await dismissOnboardingDialogs(page);
await shot(
page,
@@ -47,14 +34,13 @@ export default async ({ page, shot }) => {
'Сводный стол КУ до ввода braname: подсказка «Введите ID вашего КУ» + поле ввода. Auto-detect через marketplace_whoami появится на следующем шаге Story 6.x+1 — председатель КУ привязан к одному branch через trustee.',
);
// Ввести braname для КУ Красногорск
const branameInput = page.locator('input[aria-label*="braname"], label:has-text("braname") input, label:has-text("ID кооперативного участка") input').first();
if (await branameInput.isVisible().catch(() => false)) {
await branameInput.fill('krg');
await page.locator('button:has-text("Загрузить")').click();
await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
await page.waitForTimeout(1500);
await cleanViteOverlays(page);
await dismissOnboardingDialogs(page);
await shot(
page,
'02-loaded-tabs',
@@ -4,7 +4,8 @@
// Канон OrderCard с role='offerer'. Фильтр по статусу через q-tabs.
// Действия по акцепту партии — на отдельной странице «Подготовка отгрузки».
//
// Фикстура: sidorov (Дмитрий Николаевич Сидоров), поставщик.
// Фикстура: ivanpetrov (как и offer-create.mjs — единая модель пайщика
// для marketplace MVP scenarios).
import fs from 'node:fs';
import path from 'node:path';
@@ -18,18 +19,44 @@ const loadFixture = (username) =>
fs.readFileSync(path.resolve(__dirname, `../../../state/participants/${username}.json`), 'utf8'),
);
async function signAllAgreements(page) {
await page.waitForFunction(
() => !document.body.innerText.includes('Формируем документ'),
{ timeout: 30000 },
).catch(() => {});
await page.waitForTimeout(500);
for (let i = 0; i < 8; i++) {
const clicked = await page.evaluate(() => {
const portals = Array.from(document.querySelectorAll('[id^="q-portal--dialog--"]'))
.filter((p) => getComputedStyle(p).display !== 'none');
if (portals.length === 0) return false;
const top = portals[portals.length - 1];
const btn = Array.from(top.querySelectorAll('button'))
.find((b) => b.textContent?.trim() === 'Подписать' && !b.disabled);
if (!btn) return false;
btn.scrollIntoView({ block: 'center', behavior: 'instant' });
btn.click();
return true;
});
if (!clicked) break;
await page.waitForTimeout(3500);
}
}
export const meta = {
title: 'Входящие заказы поставщика',
docPath: 'new/marketplace/offerer/incoming-orders.md',
assetsDir: 'assets/new/marketplace/offerer/incoming-orders',
role: 'user',
fixture: 'sidorov',
fixture: 'ivanpetrov',
};
export default async ({ page, shot }) => {
const fixture = loadFixture('sidorov');
const fixture = loadFixture('ivanpetrov');
await loginAs(page, fixture);
await page.evaluate(() => localStorage.setItem('harness:noBranchOverlay', '1'));
await signAllAgreements(page);
await page.goto(`${env.BASE_URL}/#/${env.COOPNAME}/market/incoming-orders`, {
waitUntil: 'domcontentloaded',
@@ -45,10 +72,9 @@ export default async ({ page, shot }) => {
'Стол поставщика: лента входящих заказов от пайщиков. Фильтр по статусу через табы — «Все», «Ждут моего акцепта», «Индивидуальные ожидающие», «Приняты», «Поставка готова», «Приняты кооперативом», «Получены», «Отменены». Polling 15s.',
);
// Переключиться на фильтр «Ждут моего акцепта» — это критичный статус для J2.
const pendingTab = page.locator('button:has-text("Ждут моего акцепта"), [role="tab"]:has-text("Ждут моего акцепта")').first();
const pendingTab = page.locator('[role="tab"]:has-text("Ждут моего акцепта"), button:has-text("Ждут моего акцепта")').first();
if (await pendingTab.isVisible().catch(() => false)) {
await pendingTab.click();
await pendingTab.click().catch(() => {});
await page.waitForTimeout(900);
await cleanViteOverlays(page);
await shot(
@@ -1,10 +1,10 @@
// Сценарий: Поставщик видит свои Offer'ы во всех 4 статусах.
// Эпик 3 / Story 3.4 — обзор Предложений поставщика.
//
// Канон CatalogOfferCard, client-side фильтр по статусу через q-btn-toggle,
// поиск по названию. Для REJECTED — reject_reason под карточкой.
// Канон CatalogOfferCard, client-side фильтр через q-btn-toggle, поиск
// по названию. Для REJECTED — reject_reason под карточкой.
//
// Фикстура: sidorov (Дмитрий Николаевич Сидоров), поставщик.
// Фикстура: ivanpetrov (единая модель пайщика для marketplace MVP).
import fs from 'node:fs';
import path from 'node:path';
@@ -18,18 +18,47 @@ const loadFixture = (username) =>
fs.readFileSync(path.resolve(__dirname, `../../../state/participants/${username}.json`), 'utf8'),
);
async function signAllAgreements(page) {
await page.waitForFunction(
() => !document.body.innerText.includes('Формируем документ'),
{ timeout: 30000 },
).catch(() => {});
await page.waitForTimeout(500);
for (let i = 0; i < 8; i++) {
const clicked = await page.evaluate(() => {
const portals = Array.from(document.querySelectorAll('[id^="q-portal--dialog--"]'))
.filter((p) => getComputedStyle(p).display !== 'none');
if (portals.length === 0) return false;
const top = portals[portals.length - 1];
const btn = Array.from(top.querySelectorAll('button'))
.find((b) => b.textContent?.trim() === 'Подписать' && !b.disabled);
if (!btn) return false;
btn.scrollIntoView({ block: 'center', behavior: 'instant' });
btn.click();
return true;
});
if (!clicked) break;
await page.waitForTimeout(3500);
}
}
export const meta = {
// docPath/assetsDir выровнены на existing offers.md (Vue-page = OffererMyOffers,
// но MD-файл уже создан под именем offers.md с прозой из PRD; имя сценария
// оставлено my-offers.mjs для совместимости с command line).
title: 'Мои предложения: каталог Offer\'ов поставщика',
docPath: 'new/marketplace/offerer/my-offers.md',
assetsDir: 'assets/new/marketplace/offerer/my-offers',
docPath: 'new/marketplace/offerer/offers.md',
assetsDir: 'assets/new/marketplace/offerer/offers',
role: 'user',
fixture: 'sidorov',
fixture: 'ivanpetrov',
};
export default async ({ page, shot }) => {
const fixture = loadFixture('sidorov');
const fixture = loadFixture('ivanpetrov');
await loginAs(page, fixture);
await page.evaluate(() => localStorage.setItem('harness:noBranchOverlay', '1'));
await signAllAgreements(page);
await page.goto(`${env.BASE_URL}/#/${env.COOPNAME}/market/my-offers`, {
waitUntil: 'domcontentloaded',
@@ -45,10 +74,9 @@ export default async ({ page, shot }) => {
'Стол поставщика «Мои предложения»: 4 stat-карточки в шапке (всего / активных / на модерации / отклонены), поиск по названию и фильтр по статусу. Карточки CatalogOfferCard со статусом-чипом (Опубликовано / На модерации / Отклонено / Снято).',
);
// Переключиться на «На модерации» — критичный статус для нового поставщика.
const pendingBtn = page.locator('button:has-text("На модерации")').first();
if (await pendingBtn.isVisible().catch(() => false)) {
await pendingBtn.click();
await pendingBtn.click().catch(() => {});
await page.waitForTimeout(700);
await cleanViteOverlays(page);
await shot(
Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB