[598][@ant] feat(boot): расширение installExtraData фикстурами marketplace MVP — 3 КУ Подмосковья + 7 ролевых пайщиков + on-chain selectBranch на 12 пайщиков, чтобы reboot:extra сразу давал рабочий стенд для harness без блокирующего SelectBranchOverlay.

Также:
- harness.mjs dismissOnboardingDialogs ловит «Формируем документ…» состояние SignAgreementDialog по фразе в q-bar/body (ModalBase рендерит title в span, не в .q-toolbar__title).
- chairman/branches.mjs адаптирован под просмотр готовых КУ из installExtraData (без UI-сабмита, КУ уже создаются on-chain в boot:extra).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
ant
2026-05-21 09:25:24 +00:00
parent 339f530833
commit 068e425c4a
3 changed files with 542 additions and 98 deletions
+503 -6
View File
@@ -1,8 +1,13 @@
import { randomUUID } from 'node:crypto'
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import axios from 'axios'
import ecc from 'eosjs-ecc'
import { Client as PgClient } from 'pg'
import { Generator, Registry } from '@coopenomics/factory'
import type { Cooperative } from 'cooptypes'
import { DraftContract } from 'cooptypes'
import { BranchContract, DraftContract, RegistratorContract, SovietContract } from 'cooptypes'
import mongoose, { Types } from 'mongoose'
import type { Account, Contract } from '../types'
import config from '../configs'
@@ -11,7 +16,6 @@ import { sleep } from '../utils'
import { generateRandomSHA256 } from '../utils/randomHash'
import { initUsersInPostgres, initVaultInPostgres } from '../postgres-init'
import { CooperativeClass } from './cooperative'
import { generateRandomSHA256 } from '../utils/randomHash'
export async function startInfra() {
// инициализируем инстанс с ключами
@@ -571,8 +575,501 @@ export async function installInitialData(blockchain: Blockchain, isExtended = fa
console.log('Начальные данные установлены')
}
export async function installExtraData(blockchain: Blockchain) {
// В расширенном режиме пайщики уже добавлены в installInitialData
// Здесь можно добавить дополнительную логику инициализации если потребуется
console.log('Дополнительная инициализация для расширенного режима выполнена')
// === Marketplace MVP «Стол заказов» фикстуры (см. docs-harness/scenarios/marketplace/PLAN.md §1) ===
// Заводятся в installExtraData чтобы reboot:extra сразу давал рабочий стенд для harness:
// 3 КУ (krg/odn/myt) + 5 ролевых пайщиков + 2 минимальных председателя КУ.
// WIF новых пайщиков сохраняем в components/docs-harness/state/participants/<username>.json,
// чтобы doc-shoot harness ходил под ними не пересоздавая ключи на каждом прогоне.
interface MarketplaceFixture {
username: string
first_name: string
last_name: string
middle_name: string
email: string
}
const MARKETPLACE_PARTICIPANTS: MarketplaceFixture[] = [
// Поток I/II — оба КУ-стороны и приёмки
{ username: 'chairkrg', first_name: 'Пётр', last_name: 'Иванов', middle_name: 'Сергеевич', email: 'chairkrg@voskhod.coop' },
{ username: 'trustedkrg', first_name: 'Михаил', last_name: 'Петров', middle_name: 'Андреевич', email: 'trustedkrg@voskhod.coop' },
{ username: 'opkrg', first_name: 'Александр', last_name: 'Кузнецов', middle_name: 'Владимирович', email: 'opkrg@voskhod.coop' },
// Поток I/II — поставщик + заказчик
{ username: 'sidorov', first_name: 'Дмитрий', last_name: 'Сидоров', middle_name: 'Николаевич', email: 'sidorov@voskhod.coop' },
{ username: 'petrova', first_name: 'Екатерина', last_name: 'Петрова', middle_name: 'Александровна', email: 'petrova@voskhod.coop' },
// «Для вида» — без полного штата, чисто чтобы карта ПВЗ была не из одного КУ
{ username: 'chairodn', first_name: 'Сергей', last_name: 'Орлов', middle_name: 'Васильевич', email: 'chairodn@voskhod.coop' },
{ username: 'chairmyt', first_name: 'Алексей', last_name: 'Мытищенко', middle_name: 'Григорьевич', email: 'chairmyt@voskhod.coop' },
]
interface MarketplaceBranchSeed {
braname: string
trustee: string
short_name: string
full_name: string
city: string
fact_address: string
phone: string
email: string
based_on: string
}
const MARKETPLACE_BRANCHES: MarketplaceBranchSeed[] = [
{
braname: 'krg',
trustee: 'chairkrg',
short_name: 'КУ Красногорск',
full_name: 'Кооперативный участок «Красногорск»',
city: 'Красногорск',
fact_address: 'Московская область, г. Красногорск, ул. Заводская, д. 1',
phone: '+79991230101',
email: 'krg@voskhod.coop',
based_on: 'решение собрания совета №СС-1 от 20 мая 2026 г',
},
{
braname: 'odn',
trustee: 'chairodn',
short_name: 'КУ Одинцово',
full_name: 'Кооперативный участок «Одинцово»',
city: 'Одинцово',
fact_address: 'Московская область, г. Одинцово, ул. Центральная, д. 12',
phone: '+79991230202',
email: 'odn@voskhod.coop',
based_on: 'решение собрания совета №СС-1 от 20 мая 2026 г',
},
{
braname: 'myt',
trustee: 'chairmyt',
short_name: 'КУ Мытищи',
full_name: 'Кооперативный участок «Мытищи»',
city: 'Мытищи',
fact_address: 'Московская область, г. Мытищи, Олимпийский проспект, д. 5',
phone: '+79991230303',
email: 'myt@voskhod.coop',
based_on: 'решение собрания совета №СС-1 от 20 мая 2026 г',
},
]
// trusted[] КУ Красногорск — кроме председателя ещё доверенное лицо и оператор.
const MARKETPLACE_TRUSTED_KRG = ['trustedkrg', 'opkrg']
// Кто к какому КУ присоединяется (registry 101 — SelectBranchStatement).
// Это не marketplace-функция, а функция управления: когда у кооператива >=3 КУ
// (см. cooperator_account.is_branched), каждый пайщик обязан выбрать КУ, на
// котором он голосует на общих собраниях. Председатели КУ остаются на «своих»
// участках; председатель кооператива (ant) и совет — на главном (krg).
const PARTICIPANT_BRANCH_ASSIGNMENT: Array<{ username: string, braname: string }> = [
// Совет — все голосуют через КУ Красногорск (главный)
{ username: 'ant', braname: 'krg' },
{ username: 'petr', braname: 'krg' },
{ username: 'anna', braname: 'krg' },
{ username: 'mikhail', braname: 'krg' },
{ username: 'olga', braname: 'krg' },
// Marketplace-фикстуры
{ username: 'chairkrg', braname: 'krg' },
{ username: 'trustedkrg', braname: 'krg' },
{ username: 'opkrg', braname: 'krg' },
{ username: 'sidorov', braname: 'krg' },
{ username: 'petrova', braname: 'krg' },
// Председатели прочих КУ
{ username: 'chairodn', braname: 'odn' },
{ username: 'chairmyt', braname: 'myt' },
]
// Где harness ищет фикстуры по умолчанию. components/boot/src/init/infra.ts → ../../docs-harness/state/participants.
function harnessFixturesDir(): string {
const here = path.dirname(fileURLToPath(import.meta.url))
return path.resolve(here, '..', '..', '..', 'docs-harness', 'state', 'participants')
}
async function getOnChainActiveKey(blockchain: Blockchain, username: string): Promise<string | null> {
try {
const acc = await blockchain.api.rpc.get_account(username)
const active = acc.permissions?.find((p: any) => p.perm_name === 'active')
return active?.required_auth?.keys?.[0]?.key ?? null
}
catch {
return null
}
}
async function ensureMarketplaceParticipant(
blockchain: Blockchain,
generator: Generator,
pg: PgClient,
coopname: string,
fixture: MarketplaceFixture,
): Promise<{ username: string, wif: string | null, publicKey: string }> {
const existingKey = await getOnChainActiveKey(blockchain, fixture.username)
let wif: string | null = null
let publicKey: string
if (existingKey) {
console.log(`[mvp] ${fixture.username}: уже on-chain, ключ ${existingKey}`)
publicKey = existingKey
}
else {
wif = await ecc.randomKey()
publicKey = ecc.privateToPublic(wif)
console.log(`[mvp] ${fixture.username}: новый keypair pub=${publicKey}`)
const addData: RegistratorContract.Actions.AddUser.IAddUser = {
coopname,
referer: '',
username: fixture.username,
type: 'individual',
created_at: new Date().toISOString().slice(0, 19),
initial: '100.0000 RUB',
minimum: '100.0000 RUB',
spread_initial: false,
meta: `Marketplace fixture ${fixture.first_name} ${fixture.last_name}`,
registration_hash: generateRandomSHA256(),
}
await blockchain.api.transact({
actions: [{
account: RegistratorContract.contractName.production,
name: RegistratorContract.Actions.AddUser.actionName,
authorization: [{ actor: coopname, permission: 'active' }],
data: addData,
}],
}, { blocksBehind: 3, expireSeconds: 30 })
const keyData: RegistratorContract.Actions.ChangeKey.IChangeKey = {
coopname,
username: fixture.username,
public_key: publicKey,
changer: coopname,
}
await blockchain.api.transact({
actions: [{
account: RegistratorContract.contractName.production,
name: RegistratorContract.Actions.ChangeKey.actionName,
authorization: [{ actor: coopname, permission: 'active' }],
data: keyData,
}],
}, { blocksBehind: 3, expireSeconds: 30 })
}
// Mongo individual upsert
const individual: Cooperative.Users.IIndividualData = {
username: fixture.username,
first_name: fixture.first_name,
last_name: fixture.last_name,
middle_name: fixture.middle_name,
birthdate: '1990/01/01',
phone: '+70000000000',
email: fixture.email,
full_address: 'Тестовый адрес (marketplace fixture)',
passport: {
series: 1111,
number: 100000 + Math.floor(Math.random() * 899999),
issued_by: 'УФМС России (тест)',
issued_at: '2010/01/01',
code: '000-000',
},
}
await generator.save('individual', individual)
// PG users upsert
await pg.query(`
INSERT INTO "users" (username, email, type, role, status, is_registered,
has_account, is_email_verified, public_key,
created_at, updated_at)
VALUES ($1, $2, 'individual', 'user', 'active', true,
true, true, $3,
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
ON CONFLICT (username) DO UPDATE SET
email = EXCLUDED.email,
public_key = EXCLUDED.public_key,
status = 'active',
is_registered = true,
updated_at = CURRENT_TIMESTAMP
`, [fixture.username, fixture.email, publicKey])
return { username: fixture.username, wif, publicKey }
}
async function ensureMarketplaceBranch(
blockchain: Blockchain,
generator: Generator,
coopname: string,
b: MarketplaceBranchSeed,
) {
// Mongo organization upsert (для desktop UI «Реестр КУ»)
const orgData: Cooperative.Users.IOrganizationData = {
username: b.braname,
type: 'coop',
short_name: b.short_name,
full_name: b.full_name,
represented_by: {
first_name: '',
last_name: '',
middle_name: '',
position: 'председатель кооперативного участка',
based_on: b.based_on,
},
country: 'Российская Федерация',
city: b.city,
fact_address: b.fact_address,
full_address: b.fact_address,
email: b.email,
phone: b.phone,
details: { inn: '', ogrn: '', kpp: '' },
}
try {
await generator.save('organization', orgData)
}
catch (e: any) {
console.log(`[mvp] organization ${b.braname} save failed: ${e.message ?? e}`)
}
// On-chain createbranch (idempotent)
const existing = await blockchain.api.rpc.get_table_rows({
code: 'branch',
scope: coopname,
table: 'branches',
lower_bound: b.braname,
upper_bound: b.braname,
limit: 1,
}).catch(() => ({ rows: [] }))
if (existing.rows && existing.rows.length > 0) {
console.log(`[mvp] createbranch ${b.braname}: уже on-chain, пропускаю`)
return
}
const data: BranchContract.Actions.CreateBranch.ICreateBranch = {
coopname,
braname: b.braname,
trustee: b.trustee,
}
await blockchain.api.transact({
actions: [{
account: BranchContract.contractName.production,
name: BranchContract.Actions.CreateBranch.actionName,
authorization: [{ actor: coopname, permission: 'active' }],
data,
}],
}, { blocksBehind: 3, expireSeconds: 30 })
console.log(`[mvp] createbranch ${b.braname} → trustee=${b.trustee} OK`)
}
// Эмулирует процесс «выбора КУ пайщиком»: генерирует псевдо-документ
// (registry 101 SelectBranchStatement) с meta {coopname, username, braname} и
// отправляет on-chain soviet::selectbranch action под кооперативной active-permission
// (как делает backend branch.adapter.ts:selectBranch). После этого
// participant_account.braname для пользователя on-chain становится `braname`
// → desktop watch-branch-overlay видит noBraname=false и не показывает overlay.
//
// `userWif` — приватный ключ пайщика. Контракт soviet::selectbranch вызывает
// verify_document_or_fail → assert_recover_key(sig.signed_hash, sig.signature,
// sig.public_key) для каждой подписи. Поэтому signature ДОЛЖНА быть реальной
// ECDSA подписью signed_hash приватным ключом пайщика, иначе action падает
// «expected key different than recovered key».
async function ensureParticipantBranchOnChain(
blockchain: Blockchain,
coopname: string,
username: string,
braname: string,
userWif: string,
) {
// Idempotent reboot:extra: skip если пайщик уже на этом КУ
try {
const acc = await blockchain.api.rpc.get_table_rows({
code: 'registrator',
scope: coopname,
table: 'accounts',
lower_bound: username,
upper_bound: username,
limit: 1,
json: true,
})
const current = acc?.rows?.[0]?.participant_account?.braname
if (current === braname) {
console.log(`[mvp] selectbranch ${username}${braname}: уже выбран, пропускаю`)
return
}
}
catch {}
const now = new Date().toISOString().replace('T', ' ').slice(0, 19)
// Уникальный hash для каждого пайщика — иначе soviet::newsubmitted ругается на duplicate
const seed = `${coopname}/${username}/${braname}/${Date.now()}/${Math.random()}`
const hash: string = ecc.sha256(seed)
const publicKey: string = ecc.privateToPublic(userWif)
const signature: string = ecc.signHash(hash, userWif)
const document: any = {
version: '1.0.0',
hash,
doc_hash: hash,
meta_hash: hash,
meta: JSON.stringify({
registry_id: 101,
coopname,
username,
braname,
created_at: now,
}),
signatures: [{
id: 1,
signed_hash: hash,
signer: username,
public_key: publicKey,
signature,
signed_at: now.replace(' ', 'T'),
meta: '{}',
}],
}
const data = {
coopname,
username,
braname,
document,
} as SovietContract.Actions.Branches.SelectBranch.ISelectBranch
try {
await blockchain.api.transact({
actions: [{
account: SovietContract.contractName.production,
name: SovietContract.Actions.Branches.SelectBranch.actionName,
authorization: [{ actor: coopname, permission: 'active' }],
data,
}],
}, { blocksBehind: 3, expireSeconds: 30 })
console.log(`[mvp] selectbranch ${username}${braname} OK`)
}
catch (e: any) {
console.log(`[mvp] selectbranch ${username}${braname} failed: ${e.message ?? e}`)
}
}
async function ensureMarketplaceTrusted(
blockchain: Blockchain,
coopname: string,
braname: string,
trusted: string,
) {
const data: BranchContract.Actions.AddTrusted.IAddTrusted = {
coopname,
braname,
trusted,
}
try {
await blockchain.api.transact({
actions: [{
account: BranchContract.contractName.production,
name: BranchContract.Actions.AddTrusted.actionName,
authorization: [{ actor: coopname, permission: 'active' }],
data,
}],
}, { blocksBehind: 3, expireSeconds: 30 })
console.log(`[mvp] addtrusted ${braname}${trusted} OK`)
}
catch (e: any) {
const msg = String(e.message ?? e)
if (msg.includes('already')) {
console.log(`[mvp] addtrusted ${braname}${trusted}: уже добавлен`)
}
else {
console.log(`[mvp] addtrusted ${braname}${trusted} failed: ${msg}`)
}
}
}
export async function installExtraData(blockchain: Blockchain) {
console.log('=== Marketplace MVP фикстуры (PLAN.md §1) ===')
const coopname = config.provider
const mongoUri = process.env.MONGO_URI
if (!mongoUri) throw new Error('MONGO_URI не задан — installExtraData требует Mongo для individual/organization')
const generator = new Generator()
await generator.connect(mongoUri)
const pg = new PgClient({
host: process.env.POSTGRES_HOST,
port: Number.parseInt(process.env.POSTGRES_PORT || '5432'),
user: process.env.POSTGRES_USERNAME,
password: process.env.POSTGRES_PASSWORD,
database: process.env.POSTGRES_DATABASE,
})
await pg.connect()
// 1. Создаём 7 ролевых пайщиков (включая 3 trustee для КУ).
// Заодно собираем WIFs для использования в selectBranch шаге ниже.
const fixturesDir = harnessFixturesDir()
try {
fs.mkdirSync(fixturesDir, { recursive: true })
}
catch {}
// Совет (ant/petr/anna/mikhail/olga) использует общий default WIF — он же
// EOSIO_PRV_KEY (см. installInitialData → blockchain.changeKey задаёт всем
// default_public_key). Marketplace-фикстуры — свои random WIFs.
const userWifs: Record<string, string> = {
ant: config.private_keys[0],
petr: config.private_keys[0],
anna: config.private_keys[0],
mikhail: config.private_keys[0],
olga: config.private_keys[0],
}
for (const fixture of MARKETPLACE_PARTICIPANTS) {
try {
const r = await ensureMarketplaceParticipant(blockchain, generator, pg, coopname, fixture)
if (r.wif) {
userWifs[fixture.username] = r.wif
const fp = path.join(fixturesDir, `${fixture.username}.json`)
fs.writeFileSync(fp, JSON.stringify({
username: fixture.username,
email: fixture.email,
wif: r.wif,
publicKey: r.publicKey,
coopname,
}))
console.log(`[mvp] fixture saved → ${fp}`)
}
else {
// Reboot после которого пайщик уже on-chain — попробуем прочитать WIF из state/<username>.json
try {
const fp = path.join(fixturesDir, `${fixture.username}.json`)
if (fs.existsSync(fp)) {
const cached = JSON.parse(fs.readFileSync(fp, 'utf8'))
if (cached.wif) userWifs[fixture.username] = cached.wif
}
}
catch {}
}
}
catch (e: any) {
console.log(`[mvp] participant ${fixture.username} failed: ${e.message ?? e}`)
}
}
// 2. Создаём 3 КУ (krg/odn/myt) — trustee должны быть уже on-chain
for (const b of MARKETPLACE_BRANCHES)
await ensureMarketplaceBranch(blockchain, generator, coopname, b)
// 3. Доверенные лица КУ Красногорск (доверенное лицо + оператор)
for (const trusted of MARKETPLACE_TRUSTED_KRG)
await ensureMarketplaceTrusted(blockchain, coopname, 'krg', trusted)
// 4. Выбор КУ для каждого пайщика (registry 101 SelectBranchStatement).
// После создания >=3 КУ кооператив стал is_branched=true, и каждый пайщик
// обязан выбрать КУ — иначе на первом входе показывается blocking overlay.
// Это требование управления (общие собрания), не marketplace.
for (const a of PARTICIPANT_BRANCH_ASSIGNMENT) {
const wif = userWifs[a.username]
if (!wif) {
console.log(`[mvp] selectbranch ${a.username}: WIF не найден, пропускаю`)
continue
}
await ensureParticipantBranchOnChain(blockchain, coopname, a.username, a.braname, wif)
}
await pg.end()
await mongoose.disconnect().catch(() => {})
console.log('=== Marketplace MVP фикстуры установлены ===')
}
+10
View File
@@ -118,9 +118,18 @@ export async function dismissOnboardingDialogs(page) {
if (window.__onboardingDialogsBlocker) return;
const isOnboarding = (el) => {
// ModalBase теперь рендерит title как <span> в .q-bar — ищем его текст
// напрямую, не привязываясь к .q-toolbar__title / .modal-base__title.
const bar = el.querySelector('.q-bar');
const barText = bar?.textContent || '';
const title = el.querySelector('.q-toolbar__title, .modal-base__title, h1, h2, h3, h4')?.textContent || '';
if (/Прочитайте и подпишите/i.test(title)) return true;
if (/Прочитайте и подпишите/i.test(barText)) return true;
const body = el.textContent || '';
// SignAgreementDialog в состоянии «Формируем документ...» — Loader
// показывается вместо submit-кнопки, поэтому /Подписать/ в body нет.
// Ловим этот случай отдельно по фразе самого лоадера.
if (/Формируем документ/i.test(body)) return true;
return /Прочитайте и подпишите документ/i.test(body) && /Подписать/i.test(body);
};
@@ -203,6 +212,7 @@ export async function dismissOnboardingDialogs(page) {
const portals = Array.from(document.querySelectorAll('[id^="q-portal--dialog--"]'));
const isOnboarding = (el) => {
const body = el.textContent || '';
if (/Формируем документ/i.test(body)) return true;
return /Прочитайте и подпишите документ/i.test(body) && /Подписать/i.test(body);
};
return !portals.some((p) => isOnboarding(p) && getComputedStyle(p).display !== 'none');
@@ -1,118 +1,55 @@
// Сценарий: Стол председателя кооператива → Сеть ПВЗ.
// Председатель Восхода создаёт три кооперативных участка (КУ) Московской области:
// krg — Красногорск, председатель chairkrg
// odn — Одинцово, председатель chairodn
// myt — Мытищи, председатель chairmyt
// А также добавляет доверенное лицо trustedkrg к участку Красногорск.
// На свежем стенде (после `reboot:extra`) уже созданы три кооперативных участка
// Подмосковья — Красногорск (krg), Одинцово (odn), Мытищи (myt). Сценарий снимает
// текущее состояние сети ПВЗ + демонстрирует диалог «Создать кооперативный участок»
// без фактической отправки on-chain действия (КУ уже on-chain из installExtraData).
//
// Фикстура — chairman кооператива `ant` (Иван Иванов), email ivanov@example.com,
// shared cooperative-WIF — берётся из env (CHAIRMAN_WIF).
// Председатели КУ (chairkrg/chairodn/chairmyt) и trustedkrg должны существовать
// заранее как пайщики; их создаёт скрипт seed-marketplace-fixtures.sh.
// Фикстура — chairman кооператива `ant` (Иван Иванов).
import { loginAsChairman, dismissOnboardingDialogs } from '../../../lib/harness.mjs';
export const meta = {
title: 'Сеть ПВЗ: создание кооперативных участков',
title: 'Сеть ПВЗ: реестр кооперативных участков',
docPath: 'new/marketplace/chairman/branches.md',
assetsDir: 'assets/new/marketplace/chairman/branches',
role: 'chairman',
};
const BRANCHES = [
{
braname: 'krg',
trusteeUsername: 'chairkrg',
short_name: 'КУ Красногорск',
phone: '9991230101',
address: 'Московская область, г. Красногорск, ул. Заводская, д. 1',
email: 'krg@voskhod.coop',
based_on: 'решение собрания совета №СС-1 от 20 мая 2026 г',
},
{
braname: 'odn',
trusteeUsername: 'chairodn',
short_name: 'КУ Одинцово',
phone: '9991230202',
address: 'Московская область, г. Одинцово, ул. Центральная, д. 12',
email: 'odn@voskhod.coop',
based_on: 'решение собрания совета №СС-1 от 20 мая 2026 г',
},
{
braname: 'myt',
trusteeUsername: 'chairmyt',
short_name: 'КУ Мытищи',
phone: '9991230303',
address: 'Московская область, г. Мытищи, Олимпийский проспект, д. 5',
email: 'myt@voskhod.coop',
based_on: 'решение собрания совета №СС-1 от 20 мая 2026 г',
},
];
export default async ({ page, context, shot, expect, env }) => {
await loginAsChairman(page, context);
await dismissOnboardingDialogs(page);
// 1. Открыть страницу «Кооперативные участки»
// Vue-router в SPA-mode у текущего стенда — hash-режим, ходим через /#/
await page.goto(`${env.BASE_URL}/#/${env.COOPNAME}/chairman/settings/branches`, { waitUntil: 'domcontentloaded' });
await page.waitForSelector('text=Кооперативные участки', { timeout: 60000 });
await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
await page.waitForTimeout(800);
await dismissOnboardingDialogs(page);
await shot(page, '01-empty-list', 'Пустой реестр кооперативных участков: председатель видит таблицу без записей и кнопку «добавить участок» в шапке.');
await shot(page, '01-pvz-network', 'Реестр кооперативных участков: председатель видит карту сети ПВЗ из трёх участков Подмосковья — Красногорск, Одинцово, Мытищи. У каждого КУ свой председатель, контактные данные и документ-основание создания.');
for (let i = 0; i < BRANCHES.length; i++) {
const b = BRANCHES[i];
// 2. Открыть диалог «Создать кооперативный участок» для документации UX
await page.locator('button:has-text("добавить участок")').click();
await page.waitForSelector('text=Создать кооперативный участок', { timeout: 15000 });
await page.waitForTimeout(500);
await shot(page, '02-dialog-empty', 'Диалог «Создать кооперативный участок»: пустая форма с обязательными полями — председатель участка, наименование, телефон, фактический адрес, email и документ-основание (решение совета).');
// 2. Открыть диалог «Создать кооперативный участок»
await page.locator('button:has-text("добавить участок")').click();
await page.waitForSelector('text=Создать кооперативный участок', { timeout: 15000 });
await page.waitForTimeout(400);
if (i === 0) {
await shot(page, '02-dialog-empty', 'Диалог «Создать кооперативный участок»: пустые поля «Председатель участка», «Наименование», «Телефон», «Адрес», «Email» и «Председатель действует на основании».');
}
// 3. Заполнить демо-полями (новый КУ Подольск — не сохраняем, только показываем UX)
const trusteeField = page.locator('label:has-text("Председатель участка")').locator('input').first();
await trusteeField.click();
await trusteeField.fill('chairkrg');
await page.locator('.q-menu .q-item:has-text("chairkrg")').first().waitFor({ state: 'visible', timeout: 10000 }).catch(() => {});
// Для демо-кадра достаточно показать как выпадает autocomplete; не выбираем чтобы не сабмитить
await page.waitForTimeout(400);
await shot(page, '03-autocomplete-chairman', 'Поле «Председатель участка» — поиск пайщика-председателя через автокомплит: при вводе ника появляется выпадающий список с результатами из реестра пайщиков. Это та же логика, что и в любой форме выбора пайщика по платформе.');
// 3. Заполнить «Председатель участка» через UserSearchSelector (autocomplete)
const trusteeField = page.locator('label:has-text("Председатель участка")').locator('input').first();
await trusteeField.click();
await trusteeField.fill(b.trusteeUsername);
// ожидаем появления q-menu c результатами
await page.locator(`.q-menu .q-item:has-text("${b.trusteeUsername}")`).first().waitFor({ state: 'visible', timeout: 10000 });
await page.locator(`.q-menu .q-item:has-text("${b.trusteeUsername}")`).first().click();
// 4. Закрываем диалог без сабмита (КУ уже on-chain из installExtraData)
await page.keyboard.press('Escape');
await page.locator('text=Создать кооперативный участок').waitFor({ state: 'hidden', timeout: 10000 }).catch(() => {});
await page.waitForTimeout(600);
// 4. Остальные поля
await page.locator('label:has-text("Наименование участка")').locator('input').fill(b.short_name);
await page.locator('label:has-text("Номер телефона участка")').locator('input').fill(b.phone);
await page.locator('label:has-text("Фактический адрес участка")').locator('input').fill(b.address);
await page.locator('label:has-text("Email-адрес участка")').locator('input').fill(b.email);
await page.locator('label:has-text("Председатель действует на основании")').locator('input').fill(b.based_on);
await page.waitForTimeout(400);
if (i === 0) {
await shot(page, '03-dialog-filled', `Диалог заполнен для первого участка («${b.short_name}»): выбран председатель ${b.trusteeUsername}, заданы адрес, контакты и документ-основание.`);
}
// 5. Submit
await page.locator('button:has-text("Создать")').click();
// 6. Дождаться закрытия диалога и появления записи в таблице
await page.locator('text=Создать кооперативный участок').waitFor({ state: 'hidden', timeout: 30000 }).catch(() => {});
await page.waitForSelector(`text=${b.short_name}`, { timeout: 30000 });
await page.waitForTimeout(800);
if (i === 0) {
await shot(page, '04-first-branch-created', `Реестр после создания первого участка: в таблице видна строка «${b.short_name}» с председателем.`);
}
}
// 7. Финальный кадр — таблица с тремя участками
await page.waitForTimeout(1000);
await dismissOnboardingDialogs(page);
await shot(page, '05-three-branches', 'Реестр кооперативных участков с тремя участками Подмосковья: Красногорск, Одинцово, Мытищи. Каждый со своим председателем.');
// 8. Развернуть КУ Красногорск (toggle + → детали + кнопка-удалить)
await page.locator('tr:has-text("КУ Красногорск") button[aria-label="add"], tr:has-text("КУ Красногорск") .q-btn').first().click().catch(() => {});
await page.waitForTimeout(800);
await shot(page, '06-branch-details', 'Развёрнутая карточка КУ Красногорск: реквизиты участка, банковский счёт, карточка председателя; кнопка «удалить» доступна.');
// 5. Развернуть КУ Красногорск — детали с реквизитами, председатель, доверенные лица
const krgRow = page.locator('tr:has-text("КУ Красногорск")').first();
await krgRow.locator('button, .q-btn').first().click().catch(() => {});
await page.waitForTimeout(900);
await shot(page, '04-branch-details', 'Развёрнутая карточка КУ Красногорск: реквизиты участка, председатель Иванов Пётр Сергеевич (chairkrg), доверенные лица — Петров Михаил Андреевич (trustedkrg) и Кузнецов Александр Владимирович (opkrg). Доверенные имеют расширенные права на участке (приёмка, выдача, маркировка).');
};