b9e72e6231
- fillForm rewritten to label-for locators (.el-form-item:has([for="..."])) from recon 2026-05-19
- createOp: external_id from page.waitForResponse('rt-project-save') body, not DOM
- updateOp: same save endpoint intercept; row found by data-id or text
- listOp: Vuex state strategy 1, DOM scrape strategy 2, empty array fallback
- Known gaps (JSDoc + stderr warnings): workdays not in add-project form (portal default);
regions require id->name mapping (skipped in Tier-2 MVP, logged to stderr)
- Test: HTTP fixture server serves rt-form-element-ui.html + handles /admin/visit/rt-project-save
- Fixture: .v-dialog--active wrapper + 10 .el-form-item (label[for=...]) + type-select popup in body
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
271 lines
11 KiB
HTML
271 lines
11 KiB
HTML
<!DOCTYPE html>
|
|
<html><head><meta charset="utf-8"><title>RT Project Form Fixture — Element UI + Vuetify dialog</title>
|
|
<style>
|
|
/* Minimal stubs so Playwright class-based locators work */
|
|
.el-form-item { margin-bottom: 12px; }
|
|
.el-form-item__label { display: inline-block; min-width: 140px; }
|
|
.el-form-item__content { display: inline-block; }
|
|
.el-input__inner { border: 1px solid #cccccc; padding: 4px 8px; }
|
|
.el-checkbox { cursor: pointer; margin-right: 8px; }
|
|
.el-checkbox__input.is-checked .el-checkbox__inner { background: #409eff; }
|
|
.el-checkbox__inner { display: inline-block; width: 14px; height: 14px; border: 1px solid #cccccc; }
|
|
.el-switch { cursor: pointer; }
|
|
.el-switch.is-checked .el-switch__core { background: #409eff; }
|
|
.el-switch__core { display: inline-block; width: 40px; height: 20px; border-radius: 10px; background: #cccccc; }
|
|
.el-select-dropdown { position: absolute; background: #ffffff; border: 1px solid #cccccc; z-index: 9999; min-width: 120px; }
|
|
.el-select-dropdown__item { padding: 6px 12px; cursor: pointer; }
|
|
.el-select-dropdown__item:hover { background: #f5f7fa; }
|
|
.el-button { padding: 6px 16px; cursor: pointer; border: 1px solid #cccccc; background: #ffffff; }
|
|
.el-input-number .el-input__inner { width: 80px; }
|
|
</style>
|
|
</head><body>
|
|
|
|
<!-- Vuetify dialog wrapper — required by manage-project.js locator ".v-dialog--active button:has-text(...)" -->
|
|
<div class="v-dialog v-dialog--active v-dialog--persistent" style="padding:16px;">
|
|
|
|
<form class="el-form el-form--label-left">
|
|
|
|
<!-- 1. Tag -->
|
|
<div class="el-form-item">
|
|
<label class="el-form-item__label" for="tag">Тег</label>
|
|
<div class="el-form-item__content">
|
|
<div class="el-input">
|
|
<input type="text" class="el-input__inner" id="tag-fixture">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 2. Источник данных (B1/B2/B3 checkboxes) — label for="srcrt" -->
|
|
<div class="el-form-item">
|
|
<label class="el-form-item__label" for="srcrt">Источник данных</label>
|
|
<div class="el-form-item__content" id="srcrt-container">
|
|
<label class="el-checkbox is-checked" data-platform="B1">
|
|
<span class="el-checkbox__input is-checked">
|
|
<span class="el-checkbox__inner"></span>
|
|
<input type="checkbox" class="el-checkbox__original" checked>
|
|
</span>
|
|
<span class="el-checkbox__label">B1</span>
|
|
</label>
|
|
<label class="el-checkbox is-checked" data-platform="B2">
|
|
<span class="el-checkbox__input is-checked">
|
|
<span class="el-checkbox__inner"></span>
|
|
<input type="checkbox" class="el-checkbox__original" checked>
|
|
</span>
|
|
<span class="el-checkbox__label">B2</span>
|
|
</label>
|
|
<label class="el-checkbox is-checked" data-platform="B3">
|
|
<span class="el-checkbox__input is-checked">
|
|
<span class="el-checkbox__inner"></span>
|
|
<input type="checkbox" class="el-checkbox__original" checked>
|
|
</span>
|
|
<span class="el-checkbox__label">B3</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 3. Name — label for="name" -->
|
|
<div class="el-form-item">
|
|
<label class="el-form-item__label" for="name">Название проекта</label>
|
|
<div class="el-form-item__content">
|
|
<div class="el-input">
|
|
<input type="text" class="el-input__inner" id="name-fixture">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 4. Type select — label for="type" -->
|
|
<div class="el-form-item">
|
|
<label class="el-form-item__label" for="type">Источники сбора</label>
|
|
<div class="el-form-item__content">
|
|
<div class="el-select" id="type-select-container">
|
|
<!-- readonly input that shows selected value; clicking it opens dropdown popup in body -->
|
|
<div class="el-input">
|
|
<input type="text" class="el-input__inner" id="type-select-input" readonly
|
|
value="Сайты" placeholder="Выберите" data-current-value="Сайты">
|
|
<span class="el-input__suffix"><span class="el-select__caret">▼</span></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 5. Slider «Период» — no label-for, no DTO field, leave default -->
|
|
<div class="el-form-item">
|
|
<label class="el-form-item__label">Период</label>
|
|
<div class="el-form-item__content">
|
|
<div class="el-slider" aria-valuemin="0" aria-valuemax="24" aria-valuetext="10-18">
|
|
<span style="font-size:12px;color:#999999">10-18 (default)</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 6. Switch «Включить» — no label-for; identified by .el-switch in form-item -->
|
|
<div class="el-form-item" id="switch-form-item">
|
|
<label class="el-form-item__label">Статус</label>
|
|
<div class="el-form-item__content">
|
|
<div class="el-switch" id="active-switch">
|
|
<input type="checkbox" class="el-switch__input" id="active-switch-input">
|
|
<span class="el-switch__core"></span>
|
|
<span>Включить</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 7. Regions — label for="regions", el-select multiple -->
|
|
<div class="el-form-item">
|
|
<label class="el-form-item__label" for="regions">Регион</label>
|
|
<div class="el-form-item__content">
|
|
<div class="el-select el-select--multiple">
|
|
<input type="text" class="el-input__inner" id="regions-input" placeholder="Выберите регионы">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 8. limit_off — no label-for, no DTO field -->
|
|
<div class="el-form-item">
|
|
<label class="el-form-item__label" for="limit_off">Разделять по проектам</label>
|
|
<div class="el-form-item__content">
|
|
<label class="el-checkbox">
|
|
<span class="el-checkbox__input">
|
|
<span class="el-checkbox__inner"></span>
|
|
<input type="checkbox" class="el-checkbox__original">
|
|
</span>
|
|
<span class="el-checkbox__label">Да</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 9. Content (uniqueKey / domains) — label for="content", el-tabs -->
|
|
<div class="el-form-item">
|
|
<label class="el-form-item__label" for="content">Список сайтов</label>
|
|
<div class="el-form-item__content">
|
|
<div class="el-tabs">
|
|
<div class="el-tabs__header">
|
|
<div class="el-tabs__item is-active" data-tab="list">Список</div>
|
|
<div class="el-tabs__item" data-tab="file">Файл</div>
|
|
</div>
|
|
<div class="el-tabs__content">
|
|
<textarea class="el-textarea__inner" id="content-textarea" rows="4" style="width:100%"></textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 10. Limit — label for="limit", el-input-number -->
|
|
<div class="el-form-item">
|
|
<label class="el-form-item__label" for="limit">Лимит в день</label>
|
|
<div class="el-form-item__content">
|
|
<div class="el-input-number">
|
|
<span class="el-input-number__decrease">-</span>
|
|
<div class="el-input">
|
|
<input type="text" class="el-input__inner" id="limit-input" value="10">
|
|
</div>
|
|
<span class="el-input-number__increase">+</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</form><!-- end .el-form -->
|
|
|
|
<!-- Save/Cancel buttons OUTSIDE form, INSIDE .v-dialog--active -->
|
|
<div style="margin-top:16px;">
|
|
<button type="button" class="el-button" id="save-btn">Сохранить</button>
|
|
<button type="button" class="el-button" id="cancel-btn">Отмена</button>
|
|
</div>
|
|
|
|
</div><!-- end .v-dialog--active -->
|
|
|
|
<script>
|
|
(function() {
|
|
'use strict';
|
|
|
|
// ---- Checkbox toggle behaviour ----
|
|
// Click on .el-checkbox toggles .is-checked on itself and .el-checkbox__input child
|
|
document.querySelectorAll('#srcrt-container .el-checkbox').forEach(function(cb) {
|
|
cb.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
var isChecked = cb.classList.contains('is-checked');
|
|
cb.classList.toggle('is-checked', !isChecked);
|
|
var cbInput = cb.querySelector('.el-checkbox__input');
|
|
if (cbInput) cbInput.classList.toggle('is-checked', !isChecked);
|
|
var rawInput = cb.querySelector('input.el-checkbox__original');
|
|
if (rawInput) rawInput.checked = !isChecked;
|
|
});
|
|
});
|
|
|
|
// ---- Switch toggle behaviour ----
|
|
var switchEl = document.getElementById('active-switch');
|
|
if (switchEl) {
|
|
switchEl.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
var isChecked = switchEl.classList.contains('is-checked');
|
|
switchEl.classList.toggle('is-checked', !isChecked);
|
|
var inp = document.getElementById('active-switch-input');
|
|
if (inp) inp.checked = !isChecked;
|
|
});
|
|
}
|
|
|
|
// ---- Type select popup ----
|
|
// When input#type-select-input is clicked, create a dropdown in body
|
|
var typeInput = document.getElementById('type-select-input');
|
|
var typeOptions = ['Сайты', 'Звонки', 'СМС', 'Ретро сайты', 'Ретро звонки'];
|
|
|
|
function removeDropdown() {
|
|
var existing = document.querySelector('body > .el-select-dropdown');
|
|
if (existing) existing.remove();
|
|
}
|
|
|
|
if (typeInput) {
|
|
typeInput.addEventListener('click', function(e) {
|
|
e.stopPropagation();
|
|
removeDropdown();
|
|
var dropdown = document.createElement('div');
|
|
dropdown.className = 'el-select-dropdown el-popper';
|
|
dropdown.style.position = 'absolute';
|
|
dropdown.style.left = '20px';
|
|
dropdown.style.top = '200px';
|
|
var ul = document.createElement('ul');
|
|
ul.className = 'el-scrollbar__view el-select-dropdown__list';
|
|
typeOptions.forEach(function(opt) {
|
|
var li = document.createElement('li');
|
|
li.className = 'el-select-dropdown__item';
|
|
li.textContent = opt;
|
|
li.addEventListener('click', function(e2) {
|
|
e2.stopPropagation();
|
|
typeInput.value = opt;
|
|
typeInput.setAttribute('data-current-value', opt);
|
|
removeDropdown();
|
|
});
|
|
ul.appendChild(li);
|
|
});
|
|
dropdown.appendChild(ul);
|
|
document.body.appendChild(dropdown);
|
|
});
|
|
}
|
|
|
|
// Close dropdown on outside click
|
|
document.addEventListener('click', function() {
|
|
removeDropdown();
|
|
});
|
|
|
|
// ---- Save button: POST to /admin/visit/rt-project-save on the same origin ----
|
|
// NOTE: NO fetch mock here — the HTTP server (manage-project.test.js) handles
|
|
// this route and returns {status:"OK",id:"99001"}. Playwright's waitForResponse
|
|
// intercepts real network requests, not mocked fetch.
|
|
document.getElementById('save-btn').addEventListener('click', function() {
|
|
var payload = {
|
|
tag: document.getElementById('tag-fixture') ? document.getElementById('tag-fixture').value : '',
|
|
name: document.getElementById('name-fixture') ? document.getElementById('name-fixture').value : '',
|
|
type: typeInput ? typeInput.getAttribute('data-current-value') : 'Сайты',
|
|
limit: document.getElementById('limit-input') ? document.getElementById('limit-input').value : '10',
|
|
};
|
|
fetch('/admin/visit/rt-project-save', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload),
|
|
});
|
|
});
|
|
|
|
})();
|
|
</script>
|
|
</body></html>
|