fix: localize account roles and align lab brand
This commit is contained in:
1 parent
a1bb3eab98
commit
830e1d0f0d
3 files changed
+16
-15
No files matched your search
@@ -25,7 +25,7 @@
|
||||
.review__header p { max-width: 65ch; margin: 0; color: var(--muted); }
|
||||
.review__header-top { display: flex; justify-content: space-between; gap: 1rem; align-items: start; }
|
||||
.review__eyebrow { display: inline-flex; align-items: center; gap: .5rem; color: var(--ink); font: 600 1.1rem/1.25 var(--serif); }
|
||||
.review__eyebrow::before { content: ''; width: 1.5rem; height: 1.5rem; border: 2px solid var(--brand); border-radius: 50%; background: linear-gradient(90deg, transparent 45%, var(--brand) 45% 55%, transparent 55%), linear-gradient(0deg, transparent 45%, var(--brand) 45% 55%, transparent 55%); }
|
||||
.review__eyebrow-mark { display: block; flex: 0 0 1.5rem; width: 1.5rem; height: 1.5rem; }
|
||||
.review__main { padding-block-end: 2rem; }
|
||||
.review__search { display: grid; grid-template-columns: minmax(18rem, 1fr) 9rem 9rem auto; align-items: end; gap: .75rem; padding: 1rem; background: var(--surface); border: 1px solid var(--line); border-radius: 6px; }
|
||||
.review__field { display: grid; gap: .35rem; min-width: 0; }
|
||||
@@ -621,7 +621,7 @@
|
||||
<body>
|
||||
<a class="review__skip" href="#results">Перейти к результатам</a>
|
||||
<header class="review__header">
|
||||
<div class="review__nav"><div class="review__eyebrow">Акылдаш</div><nav class="review__nav-links" aria-label="Основная навигация"><a id="review-search-nav" href="/">Поиск</a><a id="review-review-nav" href="/review">Лаборатория</a><button id="review-account-open" type="button">Личный кабинет</button><a id="review-users-open" href="/users" hidden>Пользователи</a></nav><div class="review__nav-account"><span id="review-signed-in-user"></span><button id="review-logout" type="button">Выйти</button></div></div>
|
||||
<div class="review__nav"><div class="review__eyebrow"><svg class="review__eyebrow-mark" viewBox="0 0 48 48" aria-hidden="true"><circle cx="24" cy="24" r="20" fill="none" stroke="currentColor" stroke-width="3"/><path d="M24 5v38M5 24h38M11 11l26 26M37 11 11 37M17 7c2 8 12 8 14 0M17 41c2-8 12-8 14 0M7 17c8 2 8 12 0 14M41 17c-8 2-8 12 0 14" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/></svg>Акылдаш</div><nav class="review__nav-links" aria-label="Основная навигация"><a id="review-search-nav" href="/">Поиск</a><a id="review-review-nav" href="/review">Лаборатория</a><button id="review-account-open" type="button">Личный кабинет</button><a id="review-users-open" href="/users" hidden>Пользователи</a></nav><div class="review__nav-account"><span id="review-signed-in-user"></span><button id="review-logout" type="button">Выйти</button></div></div>
|
||||
<div class="review__header-content"><div class="review__heading-copy"><h1 id="route-heading">Проверка качества поиска</h1><p id="route-description">Сопоставляйте запрос с текстом документов и сохраняйте оценку выдачи.</p></div><div class="review__header-top"><button id="tutorial-open" type="button">Как это работает</button></div></div>
|
||||
</header>
|
||||
<header class="search__chrome" aria-label="Акылдаш">
|
||||
@@ -697,10 +697,11 @@
|
||||
};
|
||||
const copy = (key) => searchCopy[uiLanguage][key];
|
||||
const modalCopy = {
|
||||
ru: {account: 'Личный кабинет', lab: 'Лаборатория', users: 'Пользователи', logout: 'Выйти', role: 'Роль', admin: 'Администратор', created: 'Дата создания', changePassword: 'Изменить пароль', passwordRequirement: 'Новый пароль должен содержать не менее 12 символов.', currentPassword: 'Текущий пароль', newPassword: 'Новый пароль', confirmPassword: 'Повторите новый пароль', change: 'Изменить пароль', close: 'Закрыть', showPassword: 'Показать пароль', hidePassword: 'Скрыть пароль', usersHeading: 'Управление пользователями', temporaryPassword: 'Временный пароль', temporaryPasswordHelp: 'Пароль — от 12 символов. Новый пользователь сменит его при первом входе. Передайте временный пароль отдельно.', createUser: 'Создать пользователя', sourcesHeading: 'Источники документов', sourceIntro: 'Поиск выполняется по документам, подготовленным из правовых источников Кыргызской Республики. Для каждого результата доступная редакция и ссылка на официальный источник показываются в панели документа.', officialSource: 'Официальный источник:', sourceName: 'Централизованный банк данных правовой информации Министерства юстиции Кыргызской Республики', loginHeading: 'Вход в Акылдаш', loginHelp: 'Введите учетные данные, выданные администратором.', username: 'Логин', password: 'Пароль', login: 'Войти', newPasswordHeading: 'Создайте новый пароль', newPasswordHelp: 'Для продолжения замените временный пароль. Используйте не менее 12 символов.', loginError: 'Не удалось войти. Проверьте логин и пароль.', passwordMismatch: 'Новые пароли не совпадают.', passwordChangeError: 'Не удалось изменить пароль.', usersLoadError: 'Не удалось загрузить список пользователей.', passwordResetError: 'Не удалось сбросить пароль.', userDeleteError: 'Не удалось удалить учетную запись.', userCreateError: 'Не удалось создать учетную запись.'},
|
||||
ky: {account: 'Жеке кабинет', lab: 'Лаборатория', users: 'Колдонуучулар', logout: 'Чыгуу', role: 'Ролу', admin: 'Администратор', created: 'Түзүлгөн күнү', changePassword: 'Сырсөздү өзгөртүү', passwordRequirement: 'Жаңы сырсөз кеминде 12 белгиден турушу керек.', currentPassword: 'Учурдагы сырсөз', newPassword: 'Жаңы сырсөз', confirmPassword: 'Жаңы сырсөздү кайталаңыз', change: 'Сырсөздү өзгөртүү', close: 'Жабуу', showPassword: 'Сырсөздү көрсөтүү', hidePassword: 'Сырсөздү жашыруу', usersHeading: 'Колдонуучуларды башкаруу', temporaryPassword: 'Убактылуу сырсөз', temporaryPasswordHelp: 'Сырсөз кеминде 12 белгиден турушу керек. Жаңы колдонуучу биринчи киргенде аны өзгөртөт. Убактылуу сырсөздү өзүнчө жөнөтүңүз.', createUser: 'Колдонуучу түзүү', sourcesHeading: 'Документтердин булактары', sourceIntro: 'Издөө Кыргыз Республикасынын укуктук булактарынан даярдалган документтер боюнча жүргүзүлөт. Ар бир документ үчүн жеткиликтүү редакция жана расмий булактын шилтемеси көрсөтүлөт.', officialSource: 'Расмий булак:', sourceName: 'Кыргыз Республикасынын Юстиция министрлигинин укуктук маалыматтарынын борборлоштурулган банкы', loginHeading: 'Акылдашка кирүү', loginHelp: 'Администратор берген эсептик дайындарды киргизиңиз.', username: 'Колдонуучунун аты', password: 'Сырсөз', login: 'Кирүү', newPasswordHeading: 'Жаңы сырсөз түзүңүз', newPasswordHelp: 'Улантуу үчүн убактылуу сырсөздү алмаштырыңыз. Кеминде 12 белги колдонуңуз.', loginError: 'Кирүү мүмкүн болгон жок. Колдонуучунун атын жана сырсөздү текшериңиз.', passwordMismatch: 'Жаңы сырсөздөр дал келген жок.', passwordChangeError: 'Сырсөздү өзгөртүү мүмкүн болгон жок.', usersLoadError: 'Колдонуучулардын тизмесин жүктөө мүмкүн болгон жок.', passwordResetError: 'Сырсөздү баштапкы абалга келтирүү мүмкүн болгон жок.', userDeleteError: 'Колдонуучуну өчүрүү мүмкүн болгон жок.', userCreateError: 'Колдонуучуну түзүү мүмкүн болгон жок.'}
|
||||
ru: {account: 'Личный кабинет', lab: 'Лаборатория', users: 'Пользователи', logout: 'Выйти', role: 'Роль', admin: 'Администратор', user: 'Пользователь', created: 'Дата создания', changePassword: 'Изменить пароль', passwordRequirement: 'Новый пароль должен содержать не менее 12 символов.', currentPassword: 'Текущий пароль', newPassword: 'Новый пароль', confirmPassword: 'Повторите новый пароль', change: 'Изменить пароль', close: 'Закрыть', showPassword: 'Показать пароль', hidePassword: 'Скрыть пароль', usersHeading: 'Управление пользователями', temporaryPassword: 'Временный пароль', temporaryPasswordHelp: 'Пароль — от 12 символов. Новый пользователь сменит его при первом входе. Передайте временный пароль отдельно.', createUser: 'Создать пользователя', sourcesHeading: 'Источники документов', sourceIntro: 'Поиск выполняется по документам, подготовленным из правовых источников Кыргызской Республики. Для каждого результата доступная редакция и ссылка на официальный источник показываются в панели документа.', officialSource: 'Официальный источник:', sourceName: 'Централизованный банк данных правовой информации Министерства юстиции Кыргызской Республики', loginHeading: 'Вход в Акылдаш', loginHelp: 'Введите учетные данные, выданные администратором.', username: 'Логин', password: 'Пароль', login: 'Войти', newPasswordHeading: 'Создайте новый пароль', newPasswordHelp: 'Для продолжения замените временный пароль. Используйте не менее 12 символов.', loginError: 'Не удалось войти. Проверьте логин и пароль.', passwordMismatch: 'Новые пароли не совпадают.', passwordChangeError: 'Не удалось изменить пароль.', usersLoadError: 'Не удалось загрузить список пользователей.', passwordResetError: 'Не удалось сбросить пароль.', userDeleteError: 'Не удалось удалить учетную запись.', userCreateError: 'Не удалось создать учетную запись.'},
|
||||
ky: {account: 'Жеке кабинет', lab: 'Лаборатория', users: 'Колдонуучулар', logout: 'Чыгуу', role: 'Ролу', admin: 'Администратор', user: 'Колдонуучу', created: 'Түзүлгөн күнү', changePassword: 'Сырсөздү өзгөртүү', passwordRequirement: 'Жаңы сырсөз кеминде 12 белгиден турушу керек.', currentPassword: 'Учурдагы сырсөз', newPassword: 'Жаңы сырсөз', confirmPassword: 'Жаңы сырсөздү кайталаңыз', change: 'Сырсөздү өзгөртүү', close: 'Жабуу', showPassword: 'Сырсөздү көрсөтүү', hidePassword: 'Сырсөздү жашыруу', usersHeading: 'Колдонуучуларды башкаруу', temporaryPassword: 'Убактылуу сырсөз', temporaryPasswordHelp: 'Сырсөз кеминде 12 белгиден турушу керек. Жаңы колдонуучу биринчи киргенде аны өзгөртөт. Убактылуу сырсөздү өзүнчө жөнөтүңүз.', createUser: 'Колдонуучу түзүү', sourcesHeading: 'Документтердин булактары', sourceIntro: 'Издөө Кыргыз Республикасынын укуктук булактарынан даярдалган документтер боюнча жүргүзүлөт. Ар бир документ үчүн жеткиликтүү редакция жана расмий булактын шилтемеси көрсөтүлөт.', officialSource: 'Расмий булак:', sourceName: 'Кыргыз Республикасынын Юстиция министрлигинин укуктук маалыматтарынын борборлоштурулган банкы', loginHeading: 'Акылдашка кирүү', loginHelp: 'Администратор берген эсептик дайындарды киргизиңиз.', username: 'Колдонуучунун аты', password: 'Сырсөз', login: 'Кирүү', newPasswordHeading: 'Жаңы сырсөз түзүңүз', newPasswordHelp: 'Улантуу үчүн убактылуу сырсөздү алмаштырыңыз. Кеминде 12 белги колдонуңуз.', loginError: 'Кирүү мүмкүн болгон жок. Колдонуучунун атын жана сырсөздү текшериңиз.', passwordMismatch: 'Жаңы сырсөздөр дал келген жок.', passwordChangeError: 'Сырсөздү өзгөртүү мүмкүн болгон жок.', usersLoadError: 'Колдонуучулардын тизмесин жүктөө мүмкүн болгон жок.', passwordResetError: 'Сырсөздү баштапкы абалга келтирүү мүмкүн болгон жок.', userDeleteError: 'Колдонуучуну өчүрүү мүмкүн болгон жок.', userCreateError: 'Колдонуучуну түзүү мүмкүн болгон жок.'}
|
||||
};
|
||||
const modalMessage = (key) => modalCopy[uiLanguage][key];
|
||||
const roleLabel = (user, language = uiLanguage) => user?.can_manage_users === true ? modalCopy[language].admin : modalCopy[language].user;
|
||||
const reviewRoute = location.pathname === '/review';
|
||||
const usersRoute = location.pathname === '/users';
|
||||
const searchRoute = !reviewRoute && !usersRoute;
|
||||
@@ -712,7 +713,7 @@
|
||||
else if (!reviewRoute) { document.title = 'Поиск по правовой базе · Акылдаш'; document.querySelector('.review__header-content').hidden = true; $('tutorial-open').hidden = true; document.querySelector('.review__skip').href = '#public-results'; document.querySelector('footer .review__header-content').textContent = 'Акылдаш · Frontend v0.3.3 · Backend v0.13.2'; }
|
||||
async function apiFetch(path, options = {}) { const response = await fetch(path, {credentials: 'same-origin', ...options}); if (response.status === 401 && path !== '/auth/login') { document.body.classList.add('review__locked'); if (!$('login-dialog').open) $('login-dialog').showModal(); $('login-username').focus(); } return response; }
|
||||
const showLogin = () => { document.body.classList.add('review__locked'); if (!$('login-dialog').open) $('login-dialog').showModal(); $('login-username').focus(); };
|
||||
const enterWorkspace = (user) => { account = user; $('signed-in-user').textContent = user.username; $('review-signed-in-user').textContent = user.username; $('reviewer').value = user.username; $('profile-username').textContent = user.username; $('profile-role').textContent = user.role; $('profile-created').textContent = new Date(user.created_at * 1000).toLocaleDateString('ru-RU'); const canManageUsers = user.can_manage_users === true; $('review-nav').hidden = true; $('review-users-open').hidden = !canManageUsers || searchRoute; $('search-users-open').hidden = !canManageUsers || !searchRoute; document.body.classList.remove('review__locked'); if (usersRoute) { if (!canManageUsers) { location.replace('/'); return; } document.body.classList.add('is-admin'); const savedLanguage = (() => { try { return localStorage.getItem('akyldash-ui-language') || 'ru'; } catch (_) { return 'ru'; } })(); updateUsersLanguage(savedLanguage); } else if (searchRoute) initSearchPage(); };
|
||||
const enterWorkspace = (user) => { account = user; $('signed-in-user').textContent = user.username; $('review-signed-in-user').textContent = user.username; $('reviewer').value = user.username; $('profile-username').textContent = user.username; $('profile-role').textContent = roleLabel(user); $('profile-created').textContent = new Date(user.created_at * 1000).toLocaleDateString('ru-RU'); const canManageUsers = user.can_manage_users === true; $('review-nav').hidden = true; $('review-users-open').hidden = !canManageUsers || searchRoute; $('search-users-open').hidden = !canManageUsers || !searchRoute; document.body.classList.remove('review__locked'); if (usersRoute) { if (!canManageUsers) { location.replace('/'); return; } document.body.classList.add('is-admin'); const savedLanguage = (() => { try { return localStorage.getItem('akyldash-ui-language') || 'ru'; } catch (_) { return 'ru'; } })(); updateUsersLanguage(savedLanguage); } else if (searchRoute) initSearchPage(); };
|
||||
const filterFields = [['statuses', {ru: 'Статус', ky: 'Статус'}, 'status'], ['document_types', {ru: 'Вид документа', ky: 'Документтин түрү'}, 'document_type'], ['authorities', {ru: 'Орган принятия', ky: 'Кабыл алган орган'}, 'authority']];
|
||||
let filterCatalogs = {};
|
||||
let filterCatalogsLoaded = false;
|
||||
@@ -1210,7 +1211,7 @@
|
||||
$('document-source').textContent = copy('source');
|
||||
const modal = modalCopy[uiLanguage];
|
||||
$('account-heading').textContent = modal.account; $('review-nav').textContent = modal.lab; $('search-users-open').textContent = modal.usersHeading; $('search-logout').textContent = modal.logout;
|
||||
$('profile-role').textContent = account?.role || '';
|
||||
$('profile-role').textContent = account ? roleLabel(account) : '';
|
||||
document.querySelectorAll('.review__password-toggle').forEach((button) => { const visible = button.getAttribute('aria-pressed') === 'true'; const label = visible ? modal.hidePassword : modal.showPassword; button.textContent = label; button.setAttribute('aria-label', label); button.title = label; });
|
||||
$('profile-tab').textContent = ky ? 'Профиль' : 'Профиль'; $('security-tab').textContent = ky ? 'Коопсуздук' : 'Безопасность';
|
||||
document.querySelectorAll('[data-close]').forEach((button) => { button.setAttribute('aria-label', modal.close); button.title = modal.close; });
|
||||
@@ -1359,7 +1360,7 @@
|
||||
const row = document.createElement('li'); row.className = 'review__user-row'; row.setAttribute('role', 'row');
|
||||
const cell = (label, value, className = '') => { const item = document.createElement('span'); item.setAttribute('role', 'cell'); item.dataset.label = label; item.className = className; item.textContent = value; return item; };
|
||||
const words = userCopy[usersLanguage];
|
||||
row.append(cell(words.login, user.username, 'review__user-name'), cell(words.role, user.role || words.role), cell(words.state, user.must_change_password ? words.statusChange : words.active), cell(words.lastLogin, formatUserDate(user.last_login_at)));
|
||||
row.append(cell(words.login, user.username, 'review__user-name'), cell(words.role, roleLabel(user, usersLanguage)), cell(words.state, user.must_change_password ? words.statusChange : words.active), cell(words.lastLogin, formatUserDate(user.last_login_at)));
|
||||
const actions = document.createElement('div'); actions.className = 'review__user-actions'; actions.setAttribute('role', 'cell'); actions.dataset.label = 'Действия';
|
||||
const history = document.createElement('details'); history.className = 'review__user-history';
|
||||
const historySummary = document.createElement('summary'); historySummary.textContent = words.history;
|
||||
|
||||
@@ -8,7 +8,7 @@ const root = path.join(repo, 'backend/search/static/fonts');
|
||||
const {docs, reviewDocs, catalogs, passwordChangeAccount, ordinaryAccount, article, articleKy} = require('./fixtures.cjs');
|
||||
fs.mkdirSync(artifacts, {recursive: true});
|
||||
let fixtureAccount = passwordChangeAccount;
|
||||
const fixtureState = {users:[{username:'fixture-user',role:'user',must_change_password:true,last_login_at:null,first_login_at:1780000000,password_reset_at:null}],passwordChanges:0,userCreates:0,passwordResets:0,userDeletes:0,reviewSaves:0,lastReviewRatings:[]};
|
||||
const fixtureState = {users:[{username:'fixture-user',role:'Пользователь',can_manage_users:false,must_change_password:true,last_login_at:null,first_login_at:1780000000,password_reset_at:null}],passwordChanges:0,userCreates:0,passwordResets:0,userDeletes:0,reviewSaves:0,lastReviewRatings:[]};
|
||||
let failNextEdition = false; let delayNextEdition = false; let delayNextPublicMetadata = false; let failReviewDocumentB = false; let delayReviewDocumentA = false; let delayNextReviewSearch = false;
|
||||
(async()=>{
|
||||
const browser = await chromium.launch({headless:true});
|
||||
@@ -24,7 +24,7 @@ let failNextEdition = false; let delayNextEdition = false; let delayNextPublicMe
|
||||
if(p==='/search-reviews' && req.method()==='POST') { fixtureState.reviewSaves++; fixtureState.lastReviewRatings=req.postDataJSON().results.map((result)=>({code:result.code,rating:result.rating})); return route.fulfill({json:{id:42}}); }
|
||||
if(p==='/auth/change-password' && req.method()==='POST') { fixtureState.passwordChanges++; return route.fulfill({json:{account:fixtureAccount}}); }
|
||||
if(p==='/admin/users' && req.method()==='GET') return route.fulfill({json:{users:fixtureState.users}});
|
||||
if(p==='/admin/users' && req.method()==='POST') { const body=req.postDataJSON(); fixtureState.userCreates++; fixtureState.users.push({username:body.username,role:'user',must_change_password:true,last_login_at:null,first_login_at:1780000000,password_reset_at:null}); return route.fulfill({status:201,json:{username:body.username}}); }
|
||||
if(p==='/admin/users' && req.method()==='POST') { const body=req.postDataJSON(); fixtureState.userCreates++; fixtureState.users.push({username:body.username,role:'Пользователь',can_manage_users:false,must_change_password:true,last_login_at:null,first_login_at:1780000000,password_reset_at:null}); return route.fulfill({status:201,json:{username:body.username}}); }
|
||||
const resetMatch=p.match(/^\/admin\/users\/([^/]+)\/reset-password$/);
|
||||
if(resetMatch && req.method()==='POST') { fixtureState.passwordResets++; const username=decodeURIComponent(resetMatch[1]); const user=fixtureState.users.find((candidate)=>candidate.username===username); if(user) user.must_change_password=true; return route.fulfill({json:{username}}); }
|
||||
const deleteMatch=p.match(/^\/admin\/users\/([^/]+)$/);
|
||||
@@ -66,7 +66,7 @@ let failNextEdition = false; let delayNextEdition = false; let delayNextPublicMe
|
||||
console.log('subnavBoxes',await page.evaluate(()=>[...document.querySelector('.search__subnav').children].map(el=>({className:el.className||el.id,text:el.innerText,rect:(()=>{const r=el.getBoundingClientRect();return {left:r.left,right:r.right,width:r.width}})()})))); const navGeometry=await page.evaluate(()=>{const box=s=>{const r=document.querySelector(s).getBoundingClientRect();return {left:r.left,right:r.right}};const language=document.querySelector('.search__filter-language');const select=language.querySelector('select');const r=language.getBoundingClientRect();const filterHeader=document.querySelector('.search__filters .review__panel-heading').getBoundingClientRect(),filterHeading=document.querySelector('#filters-heading').getBoundingClientRect(),filterReset=document.querySelector('#clear-filters').getBoundingClientRect();return {search:box('#search-nav'),sources:box('#sources-open'),lab:box('#search-review-nav'),labNavVisible:!document.querySelector('#search-review-nav').hidden,filterHeadingSameRow:filterHeading.top<filterReset.bottom&&filterHeading.bottom>filterReset.top,filterLanguageGap:Math.round(language.getBoundingClientRect().top-filterHeader.bottom),splitterWidth:document.querySelector('#search-splitter').getBoundingClientRect().width,filterHeadingSize:parseFloat(getComputedStyle(document.querySelector('#filters-heading')).fontSize),resultsHeadingSize:parseFloat(getComputedStyle(document.querySelector('#search-results-heading')).fontSize),languageInFilters:document.querySelector('#search-filters').contains(language),languageWidth:r.width,languageSelectWidth:select.getBoundingClientRect().width,searchTextAlign:getComputedStyle(document.querySelector('#search-nav')).justifyContent,status:box('#active-filters .search__chip'),account:box('#search-account-open'),accountContentFits:(()=>{const e=document.querySelector('#search-account-open');return e.scrollWidth<=e.clientWidth})()}}); console.log('navGeometry',navGeometry); const documentTitleStyle=await page.locator('#public-document-heading').evaluate(el=>({fontSize:parseFloat(getComputedStyle(el).fontSize),lineHeight:parseFloat(getComputedStyle(el).lineHeight),fontFamily:getComputedStyle(el).fontFamily})); const resultHighlight=await page.locator('.search__result--selected .search__result-snippet mark').first().textContent(); const openedCount=await page.locator('.search__opened').count(); await page.locator('#public-document-panel').evaluate(el=>el.scrollTop=0); await page.evaluate(()=>document.activeElement.blur()); console.log('desktopBoxes', await page.evaluate(()=>Object.fromEntries(['.search__chrome','.search__layout','.search__footer','.search__document'].map(s=>[s,(()=>{const r=document.querySelector(s).getBoundingClientRect();return {top:r.top,bottom:r.bottom,height:r.height}})()])))); console.log('docElements',await page.evaluate(()=>Object.fromEntries(['.search__document-head','#public-document-heading','#public-document-meta','.search__document-toolbar','.search__find','#document-note','#public-document-body h2'].map(s=>[s,(()=>{const r=document.querySelector(s).getBoundingClientRect();return {top:r.top,bottom:r.bottom,height:r.height}})()])))); await page.screenshot({path:path.join(artifacts,'desktop-1280x800.png'),fullPage:false});
|
||||
const splitterDefault=await page.evaluate(()=>{const r=s=>document.querySelector(s).getBoundingClientRect();return {filters:r('#search-filters').width,results:r('.search__results').width,document:r('#public-document-panel').width};}); const splitter=page.locator('#search-splitter'); const splitterStart=Number(await splitter.getAttribute('aria-valuenow')); await splitter.focus(); await page.keyboard.press('ArrowRight'); const splitterKeyboard=Number(await splitter.getAttribute('aria-valuenow')); const keyboardMin=Number(await splitter.getAttribute('aria-valuemin')),keyboardMax=Number(await splitter.getAttribute('aria-valuemax')); await page.keyboard.press('Home'); const splitterHome=Number(await splitter.getAttribute('aria-valuenow')); await page.keyboard.press('End'); const splitterEnd=Number(await splitter.getAttribute('aria-valuenow')); const drag=async x=>{const box=await splitter.boundingBox();await page.mouse.move(box.x+6,box.y+box.height/2);await page.mouse.down();await page.mouse.move(x,box.y+box.height/2,{steps:5});await page.mouse.up();}; await drag(0); const splitterLeft=await page.evaluate(()=>({value:Number(document.querySelector('#search-splitter').getAttribute('aria-valuenow')),filters:document.querySelector('#search-filters').getBoundingClientRect().width,results:document.querySelector('.search__results').getBoundingClientRect().width,document:document.querySelector('#public-document-panel').getBoundingClientRect().width})); await drag(1280); const splitterRight=await page.evaluate(()=>({value:Number(document.querySelector('#search-splitter').getAttribute('aria-valuenow')),filters:document.querySelector('#search-filters').getBoundingClientRect().width,results:document.querySelector('.search__results').getBoundingClientRect().width,document:document.querySelector('#public-document-panel').getBoundingClientRect().width})); const splitterPointer=splitterLeft.value; const splitterStored=await page.evaluate(()=>localStorage.getItem('akyldash-search-results-share'));
|
||||
delayNextPublicMetadata=true; const delayedPublicRequest=page.waitForRequest(request=>new URL(request.url()).pathname==='/documents/doc-1/editions'); await page.locator('.search__result-title').first().click(); await delayedPublicRequest; await page.locator('.search__result-title').nth(2).click(); await page.waitForFunction(()=>document.querySelector('#document-facts').textContent==='Fixture editions error'); await page.waitForTimeout(300); const stalePublicSelection=await page.evaluate(()=>({selected:document.querySelector('.search__result--selected').dataset.code,markers:document.querySelectorAll('.search__opened').length})); await page.locator('.search__result-title').first().click(); await page.locator('#public-document-body h2').first().waitFor(); const markerAfterReload=await page.locator('.search__opened').count();
|
||||
await page.evaluate(()=>{localStorage.setItem('akyldash-search-results-share','36');}); await page.reload(); await page.locator('#search-results-heading').waitFor(); await page.locator('.search__result-title').first().waitFor(); const splitterRestored=await page.locator('#search-splitter').evaluate(el=>({value:Number(el.getAttribute('aria-valuenow')),min:Number(el.getAttribute('aria-valuemin')),max:Number(el.getAttribute('aria-valuemax'))})); await page.evaluate(()=>localStorage.setItem('akyldash-search-results-share','55')); await page.reload(); await page.locator('.search__result-title').first().waitFor(); await page.waitForFunction(()=>document.querySelector('#search-splitter')?.getAttribute('aria-valuenow')!=='36'); const splitterStoredMax=await page.locator('#search-splitter').evaluate(el=>({value:Number(el.getAttribute('aria-valuenow')),max:Number(el.getAttribute('aria-valuemax'))})); await page.setViewportSize({width:1440,height:800}); await page.evaluate(()=>window.dispatchEvent(new Event('resize'))); const splitterResize=await page.evaluate(()=>({results:document.querySelector('.search__results').getBoundingClientRect().width,document:document.querySelector('#public-document-panel').getBoundingClientRect().width})); await page.setViewportSize({width:1280,height:800}); await page.evaluate(()=>window.dispatchEvent(new Event('resize'))); await page.evaluate(()=>localStorage.setItem('akyldash-search-results-share','25')); await page.reload(); await page.locator('.search__result-title').first().waitFor(); await page.waitForFunction(()=>document.querySelector('#search-splitter')?.getAttribute('aria-valuenow')!=='36'); const splitterStoredMin=await page.locator('#search-splitter').evaluate(el=>({value:Number(el.getAttribute('aria-valuenow')),min:Number(el.getAttribute('aria-valuemin'))}));
|
||||
await page.evaluate(()=>{localStorage.setItem('akyldash-search-results-share','36');}); await page.reload(); await page.locator('#search-results-heading').waitFor(); await page.locator('.search__result-title').first().waitFor(); const splitterRestored=await page.locator('#search-splitter').evaluate(el=>({value:Number(el.getAttribute('aria-valuenow')),min:Number(el.getAttribute('aria-valuemin')),max:Number(el.getAttribute('aria-valuemax'))})); await page.evaluate(()=>localStorage.setItem('akyldash-search-results-share','55')); await page.reload(); await page.locator('.search__result-title').first().waitFor(); await page.waitForFunction(()=>document.querySelector('#search-splitter')?.getAttribute('aria-valuenow')!=='36'); const splitterStoredMax=await page.locator('#search-splitter').evaluate(el=>({value:Number(el.getAttribute('aria-valuenow')),max:Number(el.getAttribute('aria-valuemax'))})); await page.setViewportSize({width:1440,height:800}); await page.evaluate(()=>window.dispatchEvent(new Event('resize'))); await page.evaluate(()=>new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve)))); const splitterResize=await page.evaluate(()=>({results:document.querySelector('.search__results').getBoundingClientRect().width,document:document.querySelector('#public-document-panel').getBoundingClientRect().width})); await page.setViewportSize({width:1280,height:800}); await page.evaluate(()=>window.dispatchEvent(new Event('resize'))); await page.evaluate(()=>new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve)))); await page.evaluate(()=>localStorage.setItem('akyldash-search-results-share','25')); await page.reload(); await page.locator('.search__result-title').first().waitFor(); await page.waitForFunction(()=>document.querySelector('#search-splitter')?.getAttribute('aria-valuenow')!=='36'); const splitterStoredMin=await page.locator('#search-splitter').evaluate(el=>({value:Number(el.getAttribute('aria-valuenow')),min:Number(el.getAttribute('aria-valuemin'))}));
|
||||
const chipsBefore=await page.locator('#active-filters .search__chip').count(); await page.locator('input[name="filter-document_type"][value="code"]').check(); await page.waitForFunction(()=>new URL(location.href).searchParams.get('document_type')==='code'); const chipsWithTwo=await page.locator('#active-filters .search__chip').count(); await page.locator('#active-filters button').first().click(); await page.waitForFunction(()=>new URL(location.href).searchParams.get('status')===null); const preservedFilters=await page.evaluate(()=>({status:new URL(location.href).searchParams.get('status'),documentType:new URL(location.href).searchParams.get('document_type')})); await page.locator('#active-filters button').first().click(); const urlFilters=await page.evaluate(()=>({status:new URL(location.href).searchParams.get('status'),documentType:new URL(location.href).searchParams.get('document_type')})); const firstFindCount=await page.locator('#document-find-count').textContent(); const findClearedAfterFilterChange=!(await page.locator('#document-find-next').isVisible()); await page.locator('input[name="filter-status"][value="active"]').check(); await page.locator('.search__result-title').first().click(); await page.locator('#public-document-body h2').first().waitFor(); await page.locator('#document-find').fill('налог на прибыль'); await page.waitForFunction(()=>!document.querySelector('#document-find-next').disabled); await page.locator('#document-find-next').click(); const nextFindCount=await page.locator('#document-find-count').textContent(); await page.locator('#ui-ky').click(); const documentLanguage=await page.locator('#document-language').inputValue(); const kyrgyzUi=await page.evaluate(()=>({htmlLang:document.documentElement.lang,queryLabel:document.querySelector('label[for=public-query]').textContent,filterHeading:document.querySelectorAll('#filter-fields h3')[1].textContent,statusHeading:document.querySelectorAll('#filter-fields h3')[0].textContent,authority:document.querySelector('#filter-authority option').textContent,activeStatus:document.querySelector('#active-filters .search__chip').textContent.trim(),queryPlaceholder:document.querySelector('#public-query').placeholder,resultCount:document.querySelector('#result-count').textContent,findCount:document.querySelector('#document-find-count').textContent,opened:document.querySelector('.search__opened').textContent,statusValues:[...document.querySelectorAll('input[name="filter-status"]')].map(input=>{const spans=input.parentElement.querySelectorAll('span');return `${spans[0].textContent} (${spans[1].textContent})`}),documentTypes:[...document.querySelectorAll('input[name="filter-document_type"]')].map(input=>{const spans=input.parentElement.querySelectorAll('span');return `${spans[0].textContent} (${spans[1].textContent})`})})); await page.locator('#ui-ru').click(); await page.locator('#document-expand').click(); const expanded=await page.locator('#search-layout').evaluate(e=>e.classList.contains('is-expanded')); await page.keyboard.press('Escape'); const expandedEscapeClosed=await page.locator('#search-layout').evaluate(e=>!e.classList.contains('is-expanded')); const expandedFocusRestored=await page.evaluate(()=>document.activeElement.id); await page.locator('#search-account-open').click(); const accountActions=await page.locator('#account-dialog').evaluate(d=>{const close=d.querySelector('.review__dialog-heading button'),profile=d.querySelector('dl');return {open:d.open,heading:d.querySelector('#account-heading').textContent,reviewVisible:!d.querySelector('#review-nav').hidden,reviewHref:d.querySelector('#review-nav').getAttribute('href'),usersVisible:!d.querySelector('#search-users-open').hidden,usersHref:d.querySelector('#search-users-open').getAttribute('href'),usersLabel:d.querySelector('#search-users-open').textContent,logoutVisible:!d.querySelector('#search-logout').hidden,closeIcon:Boolean(close.querySelector('svg')),closeText:close.textContent.trim(),closeLabel:close.getAttribute('aria-label'),profileGrid:getComputedStyle(profile).display,profileRows:profile.children.length,headingHeight:d.querySelector('.review__dialog-heading').getBoundingClientRect().height,headingFontSize:parseFloat(getComputedStyle(d.querySelector('.review__dialog-heading h2')).fontSize),headingFontFamily:getComputedStyle(d.querySelector('.review__dialog-heading h2')).fontFamily,dataBeforeActions:Boolean(profile.compareDocumentPosition(d.querySelector('.search__account-actions'))&Node.DOCUMENT_POSITION_FOLLOWING),dialogFits:d.scrollWidth<=d.clientWidth};}); const accountGeometry=await page.locator('#account-dialog').evaluate(d=>{const actions=d.querySelector('.search__account-actions'),logout=d.querySelector('#search-logout'),first=actions.querySelector('a').getBoundingClientRect(),out=logout.getBoundingClientRect();return {width:d.getBoundingClientRect().width,logoutSeparate:out.top>=first.bottom,logoutMuted:getComputedStyle(logout).backgroundColor==='rgba(0, 0Line truncated
|
||||
const fonts = await page.evaluate(async()=>({sans:document.fonts.check('400 16px "Noto Sans"'),serif:document.fonts.check('600 24px "Noto Serif"'),kyrgyzSans:(await document.fonts.load('400 16px "Noto Sans"','ӨөҮүҢң')).length>0,kyrgyzSerif:(await document.fonts.load('600 24px "Noto Serif"','ӨөҮүҢң')).length>0,ids:[...document.querySelectorAll('[id]')].map(x=>x.id).filter((id,i,a)=>a.indexOf(id)!==i)}));
|
||||
const widths={}; for(const width of [1440,1280,1024,1713]){await page.setViewportSize({width,height:900});widths[width]=await page.evaluate(()=>({scrollWidth:document.documentElement.scrollWidth,viewport:innerWidth,layout:document.querySelector('#search-layout').getBoundingClientRect().width,columns:getComputedStyle(document.querySelector('#search-layout')).gridTemplateColumns.split(' ').map(parseFloat)}));await page.screenshot({path:path.join(artifacts,`desktop-${width}px.png`)});} await page.setViewportSize({width:320,height:800}); widths[320]=await page.evaluate(()=>({scrollWidth:document.documentElement.scrollWidth,viewport:innerWidth}));
|
||||
@@ -353,7 +353,7 @@ let failNextEdition = false; let delayNextEdition = false; let delayNextPublicMe
|
||||
["!reviewFlow.reviewScroll.footerAfterSave", !reviewFlow.reviewScroll.footerAfterSave],
|
||||
["!reviewFlow.reviewSaved.includes('Оценка сохранена')", !reviewFlow.reviewSaved.includes('Оценка сохранена')]
|
||||
].filter(([, failed]) => failed).map(([name]) => name); console.log('labLayout',JSON.stringify(labLayout)); console.log('reviewFlow',JSON.stringify(reviewFlow)); console.log('failedCoreChecks', JSON.stringify(failedCoreChecks)); if (failedCoreChecks.length) process.exit(1);
|
||||
const usersPage=page; await usersPage.setViewportSize({width:320,height:760}); await usersPage.goto('https://fixture.invalid/users'); await usersPage.locator('#user-list .review__user-row').waitFor(); const usersRoute=await usersPage.evaluate(()=>({visibleFooters:[...document.querySelectorAll('body > footer')].filter(el=>getComputedStyle(el).display!=='none'&&el.getBoundingClientRect().height>0).length,visible:!document.querySelector('#users-page').hidden,admin:document.body.classList.contains('is-admin'),accountOpen:document.querySelector('#account-dialog').open,headerVisible:getComputedStyle(document.querySelector('body > header.review__header')).display!=='none',footerAfterMain:Boolean(document.querySelector('#users-page').compareDocumentPosition(document.querySelector('body > footer.review__header')) & Node.DOCUMENT_POSITION_FOLLOWING),reviewActionsHidden:getComputedStyle(document.querySelector('body > .review__actions')).display==='none',footerAfterUsers:document.querySelector('body > footer.review__header').getBoundingClientRect().top>=document.querySelector('#user-list').getBoundingClientRect().bottom,overflow:document.documentElement.scrollWidth>innerWidth})); await usersPage.locator('#users-ui-ky').click(); const usersKy=await usersPage.evaluate(()=>({lang:document.documentElement.lang,heading:document.querySelector('#users-heading').textContent})); const usersNavContract=await usersPage.evaluate(()=>({headerUsers:!document.querySelector('#review-users-open').hidden,dialogUsers:!document.querySelector('#search-users-open').hidden,dialogLab:!document.querySelector('#review-nav').hidden,headerLab:!document.querySelector('#review-review-nav').hidden})); console.log('usersRouteCheck',JSON.stringify({usersRoute,usersKy,usersNavContract,errors})); if(usersRoute.visibleFooters!==1||!usersRoute.visible||!usersRoute.admin||usersRoute.accountOpen||!usersRoute.headerVisible||!usersRoute.footerAfterMain||!usersRoute.reviewActionsHidden||!usersRoute.footerAfterUsers||usersRoute.overflow||errors.length||usersKy.lang!=='ky'||usersKy.heading!=='Колдонуучулар'||!usersNavContract.headerUsers||usersNavContract.dialogUsers||usersNavContract.dialogLab||!usersNavContract.headerLab) process.exit(1);
|
||||
const usersPage=page; await usersPage.setViewportSize({width:320,height:760}); await usersPage.goto('https://fixture.invalid/users'); await usersPage.locator('#user-list .review__user-row').waitFor(); const usersRoute=await usersPage.evaluate(()=>({visibleFooters:[...document.querySelectorAll('body > footer')].filter(el=>getComputedStyle(el).display!=='none'&&el.getBoundingClientRect().height>0).length,visible:!document.querySelector('#users-page').hidden,admin:document.body.classList.contains('is-admin'),accountOpen:document.querySelector('#account-dialog').open,headerVisible:getComputedStyle(document.querySelector('body > header.review__header')).display!=='none',footerAfterMain:Boolean(document.querySelector('#users-page').compareDocumentPosition(document.querySelector('body > footer.review__header')) & Node.DOCUMENT_POSITION_FOLLOWING),reviewActionsHidden:getComputedStyle(document.querySelector('body > .review__actions')).display==='none',footerAfterUsers:document.querySelector('body > footer.review__header').getBoundingClientRect().top>=document.querySelector('#user-list').getBoundingClientRect().bottom,overflow:document.documentElement.scrollWidth>innerWidth})); await usersPage.locator('#users-ui-ky').click(); const usersKy=await usersPage.evaluate(()=>({lang:document.documentElement.lang,heading:document.querySelector('#users-heading').textContent})); const localizedRoleCells=await usersPage.locator('#user-list .review__user-row').evaluateAll(rows=>rows.map(row=>row.children[1].textContent)); const usersNavContract=await usersPage.evaluate(()=>({headerUsers:!document.querySelector('#review-users-open').hidden,dialogUsers:!document.querySelector('#search-users-open').hidden,dialogLab:!document.querySelector('#review-nav').hidden,headerLab:!document.querySelector('#review-review-nav').hidden})); console.log('usersRouteCheck',JSON.stringify({usersRoute,usersKy,usersNavContract,errors})); if(usersRoute.visibleFooters!==1||!usersRoute.visible||!usersRoute.admin||usersRoute.accountOpen||!usersRoute.headerVisible||!usersRoute.footerAfterMain||!usersRoute.reviewActionsHidden||!usersRoute.footerAfterUsers||usersRoute.overflow||errors.length||usersKy.lang!=='ky'||usersKy.heading!=='Колдонуучулар'||!localizedRoleCells.includes('Колдонуучу')||!usersNavContract.headerUsers||usersNavContract.dialogUsers||usersNavContract.dialogLab||!usersNavContract.headerLab) process.exit(1);
|
||||
const reflowWidth=640; await usersPage.setViewportSize({width:reflowWidth,height:450});
|
||||
await usersPage.goto('https://fixture.invalid/?q=%D0%BD%D0%B0%D0%BB%D0%BE%D0%B3'); await usersPage.locator('.search__result-title').first().waitFor();
|
||||
const search640=await usersPage.evaluate(()=>{const button=document.querySelector('#search-account-open'),r=button.getBoundingClientRect();return {documentWidth:document.documentElement.scrollWidth,accountVisible:r.width>0&&getComputedStyle(button).visibility!=='hidden',accountWithinViewport:r.left>=0&&r.right<=innerWidth};});
|
||||
@@ -391,7 +391,7 @@ let failNextEdition = false; let delayNextEdition = false; let delayNextPublicMe
|
||||
}
|
||||
}
|
||||
await usersPage.locator('#ui-ru').click();
|
||||
await usersPage.goto('https://fixture.invalid/review'); await usersPage.waitForFunction(()=>document.querySelector('#profile-username').textContent==='fixture-user'&&!document.body.classList.contains('review__locked'));
|
||||
await usersPage.goto('https://fixture.invalid/review'); await usersPage.waitForFunction(()=>document.querySelector('#profile-username').textContent==='admin'&&!document.body.classList.contains('review__locked'));
|
||||
layoutRegressions.reviewNav=await usersPage.evaluate(()=>{
|
||||
const read=id=>{const el=document.getElementById(id),s=getComputedStyle(el),r=el.getBoundingClientRect();return {tag:el.tagName,href:el.getAttribute('href'),active:el.classList.contains('is-active'),height:r.height,display:s.display,alignItems:s.alignItems,color:s.color,background:s.backgroundColor,borderColor:s.borderTopColor};};
|
||||
return {search:read('review-search-nav'),lab:read('review-review-nav')};
|
||||
@@ -430,7 +430,7 @@ let failNextEdition = false; let delayNextEdition = false; let delayNextPublicMe
|
||||
const mobileFailures=Object.entries(layoutRegressions.mobile).filter(([,value])=>value.overlaps||value.back.tag!=='BUTTON'||value.back.name!=='К результатам'||value.back.height<44||value.textRects.some(rect=>rect.left<value.breadcrumb.left-1||rect.right>value.breadcrumb.right+1)).map(([viewport])=>viewport);
|
||||
if(brandFailures.length||navFailures||mobileFailures.length) throw new Error(JSON.stringify({brandFailures,navFailures,mobileFailures,layoutRegressions}));
|
||||
fixtureAccount=ordinaryAccount;
|
||||
await usersPage.goto('https://fixture.invalid/review'); await usersPage.locator('#review').waitFor();
|
||||
await usersPage.goto('https://fixture.invalid/review'); await usersPage.waitForFunction(()=>document.querySelector('#profile-username').textContent==='fixture-user'&&!document.body.classList.contains('review__locked')); await usersPage.locator('#review').waitFor();
|
||||
const ordinaryNav=await usersPage.evaluate(()=>({role:document.querySelector('#profile-role').textContent,headerUsers:!document.querySelector('#review-users-open').hidden,accountUsers:!document.querySelector('#search-users-open').hidden,adminPage:document.body.classList.contains('is-admin')}));
|
||||
await usersPage.goto('https://fixture.invalid/users'); await usersPage.waitForURL('https://fixture.invalid/'); await usersPage.waitForFunction(()=>document.querySelector('#profile-username').textContent==='fixture-user'&&!document.body.classList.contains('review__locked')); await usersPage.locator('#search-results-heading').waitFor();
|
||||
const ordinaryRoute=await usersPage.evaluate(()=>({path:location.pathname,usersHidden:document.querySelector('#users-page').hidden,adminPage:document.body.classList.contains('is-admin')}));
|
||||
|
||||
@@ -9,7 +9,7 @@ const htmlPath = path.join(repo, 'backend/search/review.html');
|
||||
const staticRoot = path.join(repo, 'backend/search/static');
|
||||
const html = fs.readFileSync(htmlPath, 'utf8');
|
||||
const port = Number(process.argv[2] || 4173);
|
||||
let users = [{username:'fixture-user',role:'user',must_change_password:false,last_login_at:null,first_login_at:1780000000,password_reset_at:null}];
|
||||
let users = [{username:'admin',role:'Администратор',can_manage_users:true,must_change_password:false,last_login_at:null,first_login_at:1780000000,password_reset_at:null},{username:'fixture-user',role:'Пользователь',can_manage_users:false,must_change_password:false,last_login_at:null,first_login_at:1780000000,password_reset_at:null}];
|
||||
|
||||
const warning = `<div style="position:relative;z-index:1000;padding:5px 12px;background:#38262a;color:#fff;font:600 12px/1.4 sans-serif;text-align:center;letter-spacing:.02em">ЛОКАЛЬНЫЙ PREVIEW · только синтетические данные · внешние ссылки отключены</div><script>document.addEventListener('click',event=>{const link=event.target.closest('a[href]');if(!link)return;const target=new URL(link.href,location.href);if(target.origin!==location.origin){event.preventDefault();event.stopImmediatePropagation();}},true);</script>`;
|
||||
const previewHtml = html.replace('<body>', `<body>${warning}`);
|
||||
@@ -60,7 +60,7 @@ const server = http.createServer(async (request, response) => {
|
||||
if (method === 'GET' && url.pathname === '/admin/users') return json(response, 200, {users});
|
||||
if (method === 'POST' && url.pathname === '/admin/users') {
|
||||
const body = await readBody(request); const username = String(body.username || 'fixture-created');
|
||||
users.push({username,role:'user',must_change_password:true,last_login_at:null,first_login_at:1780000000,password_reset_at:null});
|
||||
users.push({username,role:'Пользователь',can_manage_users:false,must_change_password:true,last_login_at:null,first_login_at:1780000000,password_reset_at:null});
|
||||
return json(response, 201, {username});
|
||||
}
|
||||
if (method === 'POST' && /^\/admin\/users\/[^/]+\/reset-password$/.test(url.pathname)) return json(response, 200, {ok:true});
|
||||
|
||||
Reference in new issue
Block a user