fix(supplier): manage-project.js robustness — conditional type/tab clicks + diag dump

Найдено при Task 4 live-smoke form-канала:
- type-select и вкладка «Список» кликались безусловно → re-click уже-активного
  значения ремоунтит Element UI tab-pane (textarea детачится). Теперь кликаем
  только при реальной смене значения + waitForTimeout после смены типа.
- defensive: проверка непустого textarea после fill content.
- diag: на status!=OK дамп фактически отправленного rt-project-save body в stderr.
This commit is contained in:
Дмитрий
2026-05-19 17:16:02 +03:00
parent c1af7ddcc2
commit 702bf22ac1
+44 -14
View File
@@ -124,14 +124,24 @@ async function fillForm(page, dto) {
'"Ретро сайты" / "Ретро звонки" are not supported in Tier-2 form channel.',
);
}
// Кликаем input чтобы открыть dropdown
await fieldByFor(page, 'type').locator('.el-select input.el-input__inner').click();
// Dropdown рендерится снаружи формы в body — ждём его появления
const dropdownOption = page.locator('.el-select-dropdown__item', {
hasText: new RegExp(`^${signalLabel}$`),
});
await dropdownOption.waitFor({ state: 'visible', timeout: TIMEOUT_MS });
await dropdownOption.click();
// Тип меняем ТОЛЬКО если текущее значение ≠ нужное. Смена типа ремоунтит
// content tab-pane (Сайты/Звонки/СМС — разные поля сбора) → если сразу
// после type-select заполнять content, fill попадёт в detached textarea
// (Vue ещё не закончил ре-рендер) → rt-project-save уходит с пустым
// `content` → портал «Введите домены». Verified live 2026-05-19.
const typeInput = fieldByFor(page, 'type').locator('.el-select input.el-input__inner');
const currentType = (await typeInput.inputValue().catch(() => '')).trim();
if (currentType !== signalLabel) {
await typeInput.click();
// Dropdown рендерится снаружи формы в body — ждём его появления
const dropdownOption = page.locator('.el-select-dropdown__item', {
hasText: new RegExp(`^${signalLabel}$`),
});
await dropdownOption.waitFor({ state: 'visible', timeout: TIMEOUT_MS });
await dropdownOption.click();
// Ждём, пока Vue завершит ре-рендер content tab-pane после смены типа.
await page.waitForTimeout(1000);
}
// --- 7. Regions (label for="regions") — SKIP, gap зафиксирован в JSDoc ---
// DTO несёт int[] id; форма требует имена. Mapping не реализован для MVP.
@@ -154,13 +164,30 @@ async function fillForm(page, dto) {
: null;
if (contentLines) {
const contentField = fieldByFor(page, 'content');
// Убедиться что вкладка «Список» активна (она default, но на всякий случай)
const listTab = contentField.locator('.el-tabs__item', { hasText: 'Список' });
const listTabCount = await listTab.count();
if (listTabCount > 0) {
await listTab.click();
// Вкладка «Список» — default active. Кликаем ТОЛЬКО если она НЕ активна:
// клик по вкладке Element UI ремоунтит tab-pane → textarea детачится,
// и последующий .fill() гонится с ре-рендером (домены теряются →
// rt-project-save уходит с пустым `content` → портал «Введите домены»).
// Verified live 2026-05-19: re-click активной вкладки ломал save.
const listTab = contentField.locator('.el-tabs__item', { hasText: 'Список' }).first();
if ((await listTab.count()) > 0) {
const tabClass = (await listTab.getAttribute('class')) || '';
if (!tabClass.includes('is-active')) {
await listTab.click();
await contentField.locator('textarea.el-textarea__inner')
.waitFor({ state: 'visible', timeout: TIMEOUT_MS });
}
}
const contentTa = contentField.locator('textarea.el-textarea__inner');
await contentTa.fill(contentLines);
// Defensive: убедиться, что значение действительно осело в textarea
// (если поле детачнулось ре-рендером — fill уйдёт в пустоту).
const filledValue = await contentTa.inputValue();
if (filledValue.trim() === '') {
throw new Error(
'Content textarea empty after fill — likely tab/type re-render race; domains lost',
);
}
await contentField.locator('textarea.el-textarea__inner').fill(contentLines);
}
// --- 10. Limit (label for="limit") ---
@@ -215,6 +242,9 @@ async function createOp(page, args) {
const body = await saveResponse.json();
if (body.status !== 'OK') {
// DIAG: дамп фактически отправленного тела — для расследования "Введите домены"
const sentBody = saveResponse.request().postData();
process.stderr.write(JSON.stringify({ diag_sent_body: sentBody }) + '\n');
throw new Error(`Portal rejected save: ${body.message || 'unknown error'}`);
}
const externalId = String(body.id ?? '');