Refine search, account, and lab layouts
CI / tests-and-images (pull_request) Successful in 2m32s
Runner smoke test / runner-smoke (pull_request) Successful in 2s

This commit is contained in:
Codex committed 2026-10-24 19:49:55 +03:00
1 parent e5a8326a5e
commit 48cb424afd
7 files changed
+144 -92

No files matched your search

+4
View File
@@ -2,6 +2,10 @@
## Не выпущено
- Frontend `0.3.1`: уточнена компоновка поиска, лаборатории и кабинета;
splitter сохраняет минимальную ширину панелей, а на средних экранах фильтры
открываются доступной боковой панелью. Исправлено отображение футера на
маршруте пользователей и browser fixture смены пароля.
- Frontend `0.3.0` / Backend `0.13.1`: добавлен доступный splitter для
изменения ширины результатов и документа. Язык поиска находится в фильтрах;
состояния открытия, оглавление правовых заголовков, лаборатория и кабинет
+3 -3
View File
@@ -10,7 +10,7 @@ Telegram-бот — только часть рабочего окружения
## Текущее состояние
Сейчас реализованы Telegram-бот-секретарь версии `0.2.2`, backend версии
`0.13.1` и frontend `0.3.0` с поисковым экраном и внутренней лабораторией.
`0.13.1` и frontend `0.3.1` с поисковым экраном и внутренней лабораторией.
Авторизация хранится в
SQLite; первый пользователь `admin` создаётся интерактивной CLI-командой.
Поиск расширяет аббревиатуры организационно-правовых форм и поднимает
@@ -20,7 +20,7 @@ SQLite; первый пользователь `admin` создаётся инт
|---|---:|---|
| Telegram-бот | `0.2.2` | на Synology работает `0.2.1`; обновление после слияния |
| Backend | `0.13.1` | авторизация, поиск с фильтрами, ограниченный staging и статические ресурсы |
| Frontend | `0.3.0` | поиск по правовой базе, просмотр редакций, настраиваемая ширина панелей и внутренняя лаборатория |
| Frontend | `0.3.1` | поиск по правовой базе, просмотр редакций, настраиваемая ширина панелей и внутренняя лаборатория |
| Сбор и обработка правовых данных | `0.8.0` | добавлены relevance set и оценка выдачи |
| RAG и база знаний | — | ещё не созданы |
@@ -63,4 +63,4 @@ python3 -m unittest discover -s tools/telegram-bot -v
---
Акылдаш · Telegram-бот v0.2.2 · Backend v0.13.1 · Frontend v0.3.0
Акылдаш · Telegram-бот v0.2.2 · Backend v0.13.1 · Frontend v0.3.1
+1 -1
View File
@@ -465,7 +465,7 @@ def main() -> int:
else:
documents, fragments = export_bulk(arguments.input, arguments.output, arguments.index, arguments.limit)
destination = arguments.output
print(f"documents={documents} fragments={fragments} destination={destination}\nAkyldash · Backend v{APP_VERSION} · Frontend v0.3.0")
print(f"documents={documents} fragments={fragments} destination={destination}\nAkyldash · Backend v{APP_VERSION} · Frontend v0.3.1")
return 0
+84 -54
View File
@@ -49,13 +49,13 @@
.review__panel-hint { color: var(--muted); font-size: .875rem; }
.review__results-list { display: grid; gap: .75rem; margin: 0; padding: 0; list-style: none; }
.review__result { padding: 1rem 0; border: 0; border-block-end: 1px solid var(--line); border-radius: 0; background: var(--surface); transition-property: background-color, border-color; transition-duration: 150ms; transition-timing-function: cubic-bezier(.2, 0, 0, 1); }
.review__result--selected { background: var(--subtle); box-shadow: none; }
.review__result--selected { padding: 8px 10px; background: var(--subtle); box-shadow: none; }
.review__result:first-child { border-block-start: 1px solid var(--line); }
.review__result-title { display: flex; gap: .65rem; align-items: baseline; width: 100%; min-height: 44px; text-align: start; font-weight: 700; border: 0; padding: 0; background: none; color: var(--brand-dark); }
.review__rank { flex: 0 0 auto; color: var(--brand); font-variant-numeric: tabular-nums; }
.review__meta { margin-block: .5rem; color: var(--muted); font-size: .875rem; }
.review__rating { display: grid; grid-template-columns: repeat(4, 1fr); gap: .4rem; margin-block: 1rem .75rem; padding: 0; border: 0; }
.review__rating legend { width: 100%; margin-block-end: .35rem; font-size: .875rem; font-weight: 700; color: var(--muted); grid-column: 1 / -1; }
.review__meta { margin-block: 4px; color: var(--muted); font-size: .875rem; }
.review__rating { display: grid; grid-template-columns: repeat(4, 1fr); gap: .35rem; margin-block: 8px 6px; padding: 0; border: 0; }
.review__rating legend { width: 100%; margin-block-end: 2px; font-size: .875rem; font-weight: 700; color: var(--muted); grid-column: 1 / -1; }
.review__rating label { display: grid; place-items: center; min-width: 0; min-height: 44px; padding: .45rem .25rem; border: 1px solid var(--line); border-radius: 6px; color: var(--muted); text-align: center; font-size: .875rem; cursor: pointer; }
.review__rating label:has(input:checked) { border-color: var(--brand); background: var(--brand); color: #fff; }
.review__rating label:has(input:focus-visible) { outline: 3px solid var(--focus); outline-offset: 2px; }
@@ -217,6 +217,7 @@
body.is-users-route > .review__header { display: block; width: 100%; padding-block-end: 0; }
body.is-users-route > .review__header > .review__header-content { display: none; }
body.is-users-route > .review__actions { display: none; }
body.is-users-route > .search__footer { display: none; }
body.is-users-route > footer.review__header { display: block; margin-block-start: auto; }
.review__users-page { width: min(1200px, 100%); flex: 1; margin-inline: auto; padding: 28px clamp(16px, 4vw, 48px); }
.review__users-page[hidden] { display: none !important; }
@@ -285,12 +286,11 @@
body:has(.search:not([hidden])) > .search__chrome { display: block; }
.search__chrome { display: none; background: var(--surface); border-block-end: 1px solid var(--line); }
.search[hidden] { display: none !important; }
.search__topbar { min-height: var(--search-header-height); display: grid; grid-template-columns: minmax(390px, 430px) minmax(0, 1fr) auto; gap: 16px; align-items: center; padding: 5px 26px; border-block-end: 1px solid var(--line); }
.search__topbar { min-height: var(--search-header-height); display: grid; grid-template-columns: 166px minmax(0, 1fr) auto; gap: 16px; align-items: center; padding: 5px 26px; border-block-end: 1px solid var(--line); }
.search__brand { display: flex; min-width: 0; align-items: center; gap: 12px; color: var(--ink); text-decoration: none; white-space: nowrap; }
.search__brand-mark { width: 44px; height: 44px; flex: none; color: var(--brand); }
.search__brand-name { font: 700 26px/1.1 var(--serif); }
.search__brand-divider { width: 1px; height: 32px; background: var(--line); }
.search__brand-description { color: var(--muted); font-size: 14px; }
.search__brand-divider, .search__brand-description { display: none; }
.search__query { display: grid; grid-template-columns: minmax(0, 1fr) 160px; gap: 12px; align-items: end; min-width: 0; padding: 0; border: 0; background: transparent; }
.search__query label { display: grid; gap: 4px; color: var(--muted); font-size: 14px; font-weight: 500; }
.search__query label > span { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
@@ -503,25 +503,29 @@
.review__nav-links .is-active { border-color: var(--brand); background: var(--subtle); color: var(--brand); }
.review__nav-links [hidden], .search__account-actions [hidden] { display: none !important; }
.review__nav-account > span { color: var(--muted); }
.review__header-content { width: min(100%, 1440px); }
.review__header-content { width: 100%; max-width: none; padding-inline: 24px; }
body:has(#review:not([hidden])) { min-height: 100dvh; height: 100dvh; overflow: hidden; }
body:has(#review:not([hidden])) > .review__header { padding: 0; }
body:has(#review:not([hidden])) > #review { width: min(100%, 1440px); min-height: 0; margin: 0 auto; padding-inline: clamp(16px, 3vw, 40px); display: flex; flex-direction: column; }
body:has(#review:not([hidden])) > .review__actions { width: min(100%, 1440px); min-height: 96px; box-sizing: border-box; margin: 0 auto; padding: 8px clamp(16px, 3vw, 40px) 0; }
body:has(#review:not([hidden])) > footer.review__header { width: 100%; margin-block-start: 12px; }
.review__header-content, .review__actions-inner { max-width: 1440px; }
body:has(#review:not([hidden])) > #review { width: 100%; max-width: none; min-height: 0; margin: 0; padding: 0; display: flex; flex-direction: column; }
body:has(#review:not([hidden])) > .review__actions { width: 100%; min-height: 96px; flex: 0 0 96px; box-sizing: border-box; margin: 0; padding: 0 24px; }
body:has(#review:not([hidden])) > footer.review__header { width: 100%; margin: 0; }
.review__header-content, .review__actions-inner { width: 100%; max-width: none; }
body:has(#review:not([hidden])) .review__nav { height: 64px; min-height: 64px; padding-block: 0; }
body:has(#review:not([hidden])) .review__header-content { display: grid; grid-template-columns: auto minmax(0, 1fr); grid-template-rows: 32px 20px; column-gap: 24px; align-content: center; height: 64px; }
body:has(#review:not([hidden])) .review__header-top { grid-column: 1; grid-row: 1 / 3; align-self: center; padding: 0; }
body:has(#review:not([hidden])) .review__header-content h1 { grid-column: 2; grid-row: 1; max-width: none; margin: 0; font: 600 26px/32px var(--sans); }
body:has(#review:not([hidden])) .review__header-content p { grid-column: 2; grid-row: 2; max-width: none; margin: 0; font: 400 14px/20px var(--sans); white-space: nowrap; }
.review__search { gap: 12px; padding: 12px 0; border: 0; border-block-end: 1px solid var(--line); border-radius: 0; background: transparent; }
.review__workspace { min-height: 0; max-height: min(58dvh, 620px); flex: 1 1 0; grid-template-columns: minmax(0, .9fr) minmax(0, 1.1fr); gap: 0; align-items: stretch; overflow: hidden; border-block: 1px solid var(--line); background: var(--surface); }
.review__results, .review__document { min-height: 0; max-height: none; padding: 14px clamp(12px, 1.5vw, 22px); border: 0; border-radius: 0; background: var(--surface); }
body:has(#review:not([hidden])) .review__header-content { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; height: 64px; }
body:has(#review:not([hidden])) .review__heading-copy { min-width: 0; display: grid; grid-template-rows: 32px 20px; align-content: center; }
body:has(#review:not([hidden])) .review__header-top { grid-column: 2; grid-row: 1; align-self: center; padding: 0; }
body:has(#review:not([hidden])) .review__header-content h1 { max-width: none; margin: 0; font: 600 26px/32px var(--sans); }
body:has(#review:not([hidden])) .review__header-content p { max-width: none; margin: 0; font: 400 14px/20px var(--sans); white-space: nowrap; }
.review__search { gap: 12px; padding: 12px 24px; border: 0; border-block-end: 1px solid var(--line); border-radius: 0; background: transparent; }
#status.review__status { grid-column: 1 / -1; margin: 0; }
#status.review__status:empty { display: none; }
#status.review__status--success { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.review__workspace { min-height: 0; flex: 1 1 0; grid-template-columns: minmax(0, .9fr) minmax(0, 1.1fr); gap: 0; align-items: stretch; overflow: hidden; border-block: 1px solid var(--line); background: var(--surface); }
.review__results, .review__document { min-height: 0; max-height: none; padding: 14px 24px; border: 0; border-radius: 0; background: var(--surface); }
.review__results { overflow: auto; border-inline-end: 1px solid var(--line); }
.review__document { position: static; overflow: auto; overscroll-behavior: contain; }
.review__document-meta { overflow-wrap: anywhere; }
.review__actions-inner { padding: 5px 0; border: 0; border-block-start: 1px solid var(--line); border-radius: 0; background: transparent; }
.review__actions-inner { padding: 5px 24px; border: 0; border-block-start: 1px solid var(--line); border-radius: 0; background: transparent; }
body:has(#review:not([hidden])) > footer.review__header { height: 44px; min-height: 44px; }
body:has(#review:not([hidden])) > footer.review__header .review__header-content { display: flex; align-items: center; height: 44px; padding-block: 0; }
.search__subnav { flex-wrap: wrap; overflow: visible; white-space: normal; }
@@ -530,22 +534,29 @@
.search__chips { flex: 1 1 8rem; }
.search__filter-language { display: grid; gap: 5px; margin-block: 0 16px; color: var(--muted); font-size: .875rem; font-weight: 600; }
.search__filter-language select { min-height: 42px; width: 100%; padding: 7px 10px; color: var(--ink); background: var(--surface); border: 1px solid var(--line); }
.search__layout { grid-template-columns: minmax(190px, clamp(190px, 18vw, 250px)) minmax(260px, 36fr) 12px minmax(320px, 64fr); }
.search__layout[data-filters-hidden="true"] { grid-template-columns: minmax(260px, 36fr) 12px minmax(320px, 64fr); }
.search__layout { grid-template-columns: minmax(264px, clamp(264px, calc(20vw + 8px), 288px)) minmax(360px, 38.1fr) 8px minmax(560px, 61.9fr); }
.search__layout[data-filters-hidden="true"] { grid-template-columns: minmax(360px, 38.1fr) 8px minmax(560px, 61.9fr); }
.search__filters { margin-inline-end: 8px; }
.search__mobile-filters { display: inline-flex; }
.search__layout[data-filters-hidden="true"] .search__filters { display: none; }
.search__splitter { z-index: 1; align-self: stretch; width: 12px; min-width: 12px; min-height: 100%; padding: 0; border: 0; border-radius: 0; background: linear-gradient(90deg, transparent 4px, var(--brand-dark) 4px 8px, transparent 8px); cursor: col-resize; touch-action: none; }
.search__splitter:hover, .search__splitter:focus-visible { background: linear-gradient(90deg, transparent 3px, var(--brand) 3px 9px, transparent 9px); }
body:has(#review:not([hidden])) > .review__header, body:has(#review:not([hidden])) > footer.review__header { width: min(calc(100% - 2 * clamp(16px, 3vw, 40px)), 1240px); max-width: 1240px; margin-inline: auto; padding-inline: 0; }
body:has(#review:not([hidden])) .review__nav, body:has(#review:not([hidden])) .review__header-content { width: 100%; max-width: none; padding-inline: 0; }
.search__splitter { position: relative; z-index: 1; align-self: stretch; justify-self: center; width: 12px; min-width: 12px; min-height: 100%; margin-inline: -2px; padding: 0; border: 0; border-radius: 0; background: linear-gradient(90deg, transparent 2px, var(--surface-muted) 2px 10px, transparent 10px); cursor: col-resize; touch-action: none; }
.search__splitter::before { position: absolute; inset-block: 0; inset-inline-start: 50%; width: 1px; background: var(--line); content: ''; transform: translateX(-50%); }
.search__splitter::after { position: absolute; inset-block-start: 50%; inset-inline-start: 50%; width: 3px; height: 40px; border-radius: 2px; background: var(--muted); content: ''; transform: translate(-50%, -50%); }
.search__splitter:hover::after, .search__splitter:focus-visible::after { background: var(--brand); }
body:has(#review:not([hidden])) > .review__header, body:has(#review:not([hidden])) > footer.review__header { width: 100%; max-width: none; margin: 0; padding-inline: 0; }
body:has(#review:not([hidden])) .review__nav, body:has(#review:not([hidden])) .review__header-content { width: 100%; max-width: none; padding-inline: 24px; }
body:has(#review:not([hidden])) .review__header-content h1 { font: 600 26px/32px var(--sans); }
body:has(#review:not([hidden])) .review__panel-heading h2 { font: 600 20px/26px var(--sans); }
body.is-users-route > .search__footer { display: none !important; }
.search__pagination[hidden] { display: none; }
.search__document-language[hidden] { display: none; }
.search__account-actions { display: grid; grid-template-columns: max-content max-content; justify-content: start; gap: 8px; }
.search__account-actions a { display: inline-flex; align-items: center; justify-content: center; min-height: 44px; padding: 8px 12px; border: 1px solid var(--line); border-radius: 6px; background: var(--surface); color: var(--brand-dark); font-weight: 600; text-decoration: none; }
.search__account-actions a:hover { border-color: var(--brand); background: var(--subtle); }
#search-logout { grid-column: 1 / -1; justify-self: start; min-height: 36px; margin-block-start: 2px; padding-inline: 0; border: 0; color: var(--muted); background: transparent; }
#account-dialog dl { display: grid; grid-template-columns: minmax(7.5rem, max-content) minmax(0, 1fr); gap: 8px 16px; margin: 16px 0 24px; }
#account-dialog { width: min(560px, calc(100vw - 32px)); max-width: none; }
#account-dialog > .review__page { padding: 16px; }
#account-dialog dt { color: var(--muted); }
#account-dialog dd { min-width: 0; margin: 0; overflow-wrap: anywhere; }
@@ -556,17 +567,22 @@
#account-dialog .review__dialog-heading h2 { max-width: none; padding: 0; font: 600 20px/28px var(--sans); }
#account-dialog .review__dialog-heading button { display: grid; place-items: center; flex: 0 0 44px; width: 44px; min-height: 44px; padding: 0; }
#public-document-body img, #document-body img { max-width: 100%; height: auto; object-fit: contain; }
@media (min-width: 901px) and (max-width: 1175px) {
.search__layout { grid-template-columns: minmax(180px, 22%) minmax(260px, 36fr) 12px minmax(320px, 64fr); }
.search__layout[data-filters-hidden="true"] { grid-template-columns: minmax(260px, 36fr) 12px minmax(320px, 64fr); }
.search__layout:not([data-filters-hidden="true"]) .search__filters { display: flex; }
@media (min-width: 901px) and (max-width: 1191px) {
#search-layout { grid-template-columns: minmax(360px, 38.1fr) minmax(560px, 61.9fr); }
#search-layout .search__filters { display: none; }
#search-layout .search__filters.is-open { position: fixed; z-index: 21; inset: 16px auto 16px 16px; display: block; width: min(360px, calc(100vw - 32px)); overflow: auto; padding: 20px; border: 1px solid var(--line); border-radius: 8px; background: var(--surface); box-shadow: 0 12px 40px rgb(34 28 28 / .2); }
.search__filter-close { display: inline-flex; }
.search__filter-overlay.is-open { position: fixed; z-index: 20; inset: 0; display: block; background: rgb(56 38 42 / .42); }
.search__splitter { display: none; }
}
@media (max-width: 900px) {
body:has(#review:not([hidden])) { min-height: 100svh; height: auto; overflow: visible; }
body:has(#review:not([hidden])) > #review { flex: none; padding-inline: 16px; }
body:has(#review:not([hidden])) > .review__header, body:has(#review:not([hidden])) > footer.review__header { width: calc(100% - 32px); }
body:has(#review:not([hidden])) .review__nav, body:has(#review:not([hidden])) .review__header-content { padding-inline: 0; }
body:has(#review:not([hidden])) .review__header-content { display: block; height: auto; padding-block: 12px; }
body:has(#review:not([hidden])) .review__nav, body:has(#review:not([hidden])) .review__header-content { padding-inline: 16px; }
body:has(#review:not([hidden])) .review__header-content { display: flex; flex-direction: column; align-items: stretch; gap: 8px; height: auto; padding-block: 12px; }
body:has(#review:not([hidden])) .review__heading-copy { display: block; }
body:has(#review:not([hidden])) .review__header-top { align-self: flex-start; }
.review__search { padding-inline: 16px; }
body:has(#review:not([hidden])) .review__header-top { padding-block: 0 8px; }
body:has(#review:not([hidden])) .review__header-content h1, body:has(#review:not([hidden])) .review__header-content p { display: block; }
body:has(#review:not([hidden])) .review__header-content p { white-space: normal; }
@@ -575,7 +591,10 @@
.review__results, .review__document { max-height: none; overflow: visible; border-inline: 0; }
.review__results { border-block-end: 1px solid var(--line); }
.review__document { display: block; }
body:has(#review:not([hidden])) > .review__actions { padding-inline: 16px; }
.review__results, .review__document { padding-inline: 16px; }
body:has(#review:not([hidden])) > .review__actions { flex: 0 0 auto; padding-inline: 0; }
.review__actions-inner { padding-inline: 16px; }
body:has(#review:not([hidden])) > footer.review__header { height: auto; min-height: 44px; }
.search__layout, .search__layout[data-filters-hidden="true"] { display: block; }
.search__splitter { display: none; }
.search__filter-language { margin-block-end: 16px; }
@@ -597,8 +616,7 @@
<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__header-content"><div class="review__header-top"><button id="tutorial-open" type="button">Как это работает</button></div>
<h1 id="route-heading">Проверка качества поиска</h1><p id="route-description">Сопоставляйте запрос с текстом документов и сохраняйте оценку выдачи.</p></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="Акылдаш">
<div class="search__topbar">
@@ -617,8 +635,8 @@
<label class="review__field review__field--small">Язык запроса<select id="language" name="language"><option value="ru">Русский</option><option value="ky">Кыргызский</option></select></label>
<label class="review__field review__field--small">Результатов<select id="page-size" name="page_size"><option>10</option><option>20</option></select></label>
<button class="review__button--primary" type="submit">Показать результаты</button>
<div class="review__status" id="status" role="status" aria-live="polite"></div>
</form>
<div class="review__status" id="status" role="status" aria-live="polite"></div>
<div class="review__workspace">
<section class="review__results" aria-labelledby="results-heading"><div class="review__panel-heading"><h2 id="results-heading">Результаты поиска</h2><span class="review__panel-hint">Оцените каждый документ</span></div><ol class="review__results-list" id="results"><li class="review__empty"><strong>Начните с запроса</strong>Введите юридический вопрос выше, чтобы увидеть результаты.</li></ol></section>
<section class="review__document" id="document-panel" aria-labelledby="document-heading"><div class="review__panel-heading"><h2 id="document-heading">Просмотр документа</h2><span class="review__panel-hint">Откройте результат</span></div><div class="review__document-meta" id="document-meta">Выберите название в списке, чтобы прочитать документ.</div><article class="review__document-body" id="document-body"></article></section>
@@ -633,7 +651,7 @@
<section class="search__document" id="public-document-panel" aria-labelledby="public-document-heading"><div class="search__document-head"><div class="search__breadcrumbs" hidden><span>Документ</span><span aria-hidden="true">/</span><span id="document-breadcrumb">Выберите результат</span></div><div><button class="search__mobile-back" id="document-back" type="button">К результатам</button><button class="search__expand" id="document-expand" type="button" aria-label="Развернуть документ">Развернуть</button></div><h2 id="public-document-heading">Документ</h2></div><p id="public-document-meta" class="search__document-meta"><span id="document-facts">Выберите документ в выдаче, чтобы открыть текст.</span><label class="search__document-language" hidden>Язык<select id="document-language" disabled aria-label="Язык документа"></select></label></p><div class="search__document-toolbar"><details class="search__toc" id="document-toc"><summary>Содержание</summary><nav aria-label="Содержание документа"><ol id="document-toc-list"><li>Оглавление появится после загрузки документа</li></ol></nav></details><label class="search__edition-label"><select id="document-edition" disabled aria-label="Редакция документа"></select></label><button id="document-print" type="button">Печать</button><a id="document-source" class="search__source-link" href="#" target="_blank" rel="noreferrer" hidden>Официальный источник</a></div><div class="search__find"><label for="document-find">Найти в документе</label><input id="document-find" type="search" placeholder="Фраза в документе" disabled><span class="search__find-count" id="document-find-count" aria-live="polite"></span><button id="document-find-prev" type="button" disabled aria-label="Предыдущее совпадение"><svg viewBox="0 0 20 20" width="18" height="18" aria-hidden="true"><path d="m12 4-6 6 6 6" fill="none" stroke="currentColor" stroke-width="1.8"/></svg></button><button id="document-find-next" type="button" disabled aria-label="Следующее совпадение"><svg viewBox="0 0 20 20" width="18" height="18" aria-hidden="true"><path d="m8 4 6 6-6 6" fill="none" stroke="currentColor" stroke-width="1.8"/></svg></button></div><p class="search__document-note" id="document-note" hidden></p><article id="public-document-body" class="search__document-body"></article></section>
</div>
</main>
<footer class="search__footer"><span><strong>Акылдаш</strong> <span aria-hidden="true">·</span> Правовая база Кыргызстана</span><span>Frontend v0.3.0 <span aria-hidden="true">·</span> Backend v0.13.1</span></footer>
<footer class="search__footer"><span><strong>Акылдаш</strong> <span aria-hidden="true">·</span> Правовая база Кыргызстана</span><span>Frontend v0.3.1 <span aria-hidden="true">·</span> Backend v0.13.1</span></footer>
<div class="review__actions"><div class="review__actions-inner"><label>Проверяющий<input id="reviewer" readonly aria-label="Проверяющий"></label><label>Комментарий к выдаче <input id="overall-comment" maxlength="4000" placeholder="Что важно учесть? (необязательно)"></label><button class="review__button--primary" id="save" type="button" disabled>Сначала выполните поиск</button><p id="draft-status" class="review__status" role="status" aria-live="polite">Черновик появится после поиска.</p></div></div>
<dialog id="login-dialog" aria-labelledby="login-heading"><form class="review__page" id="login-form"><h2 id="login-heading">Вход в Акылдаш</h2><p>Введите учетные данные, выданные администратором.</p><label>Логин<input id="login-username" name="username" autocomplete="username" required minlength="3" maxlength="64"></label><label>Пароль<input id="login-password" name="password" type="password" autocomplete="current-password" required></label><p id="login-error" class="review__status review__status--error" role="alert"></p><button class="review__button--primary" type="submit">Войти</button></form></dialog>
<dialog id="password-dialog" aria-labelledby="password-heading"><form class="review__page" id="password-form"><h2 id="password-heading">Создайте новый пароль</h2><p class="review__notice">Для продолжения замените временный пароль. Используйте не менее 12 символов.</p><label>Текущий пароль<input id="current-password" type="password" autocomplete="current-password" required></label><label>Новый пароль<input id="new-password" type="password" autocomplete="new-password" minlength="12" maxlength="256" required></label><label>Повторите новый пароль<input id="confirm-password" type="password" autocomplete="new-password" minlength="12" maxlength="256" required></label><p id="password-error" class="review__status review__status--error" role="alert"></p><button class="review__button--primary" type="submit">Сохранить пароль</button></form></dialog>
@@ -651,7 +669,7 @@
<dialog id="document-dialog"><button class="review__dialog-close" id="dialog-close" type="button">Закрыть</button><h2 id="dialog-heading">Документ</h2><div class="review__document-body" id="dialog-body"></div></dialog>
<dialog class="search__about" id="sources-dialog" aria-labelledby="sources-heading"><button class="review__dialog-close" id="sources-close" type="button">Закрыть</button><h2 id="sources-heading">Источники документов</h2><p>Поиск выполняется по документам, подготовленным из правовых источников Кыргызской Республики. Для каждого результата доступная редакция и ссылка на официальный источник показываются в панели документа.</p><p>Официальный источник: <a href="https://cbd.minjust.gov.kg/" target="_blank" rel="noreferrer">Централизованный банк данных правовой информации Министерства юстиции Кыргызской Республики</a>.</p></dialog>
<div class="review__tour-spotlight" id="tutorial-spotlight" hidden aria-hidden="true"></div><dialog class="review__tour" id="tutorial-dialog" aria-labelledby="tutorial-heading" aria-describedby="tutorial-text"><div class="review__tour-content"><p class="review__tour-kicker">Короткое знакомство</p><h2 id="tutorial-heading"></h2><p id="tutorial-text"></p><div class="review__tour-footer"><span class="review__tour-progress" id="tutorial-progress"></span><div class="review__tour-actions"><button id="tutorial-skip" type="button">Пропустить</button><button class="review__button--primary" id="tutorial-next" type="button">Далее</button></div></div></div></dialog>
<footer class="review__header"><div class="review__header-content">Акылдаш · Frontend v0.3.0 · Backend v0.13.1 · Внутренняя лаборатория релевантности</div></footer>
<footer class="review__header"><div class="review__header-content">Акылдаш · Frontend v0.3.1 · Backend v0.13.1 · Внутренняя лаборатория релевантности</div></footer>
<script>
document.body.classList.add('review__locked');
const DRAFT_KEY = 'akyldash-search-review-draft';
@@ -684,8 +702,8 @@
$('review-review-nav').classList.toggle('is-active', reviewRoute); $('review-search-nav').classList.toggle('is-active', searchRoute); $('review-users-open').classList.toggle('is-active', usersRoute); $('review-nav').classList.toggle('is-active', reviewRoute); $('search-nav').classList.toggle('is-active', searchRoute);
document.body.classList.toggle('is-review-route', reviewRoute);
document.body.classList.toggle('is-search-route', searchRoute); document.body.classList.toggle('is-users-route', usersRoute);
if (usersRoute) { document.title = 'Пользователи · Акылдаш'; $('route-heading').textContent = 'Пользователи'; $('route-description').textContent = 'Управляйте учётными записями. Доступ к действиям проверяется сервером.'; $('tutorial-open').hidden = true; document.querySelector('.review__skip').href = '#users-page'; document.querySelector('footer .review__header-content').textContent = 'Акылдаш · Frontend v0.3.0 · Backend v0.13.1'; }
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.0 · Backend v0.13.1'; }
if (usersRoute) { document.title = 'Пользователи · Акылдаш'; $('route-heading').textContent = 'Пользователи'; $('route-description').textContent = 'Управляйте учётными записями. Доступ к действиям проверяется сервером.'; $('tutorial-open').hidden = true; document.querySelector('.review__skip').href = '#users-page'; document.querySelector('footer .review__header-content').textContent = 'Акылдаш · Frontend v0.3.1 · Backend v0.13.1'; }
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.1 · Backend v0.13.1'; }
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.role === 'admin'; $('search-users-open').hidden = $('review-users-open').hidden = !canManageUsers; 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(); };
@@ -995,19 +1013,31 @@
function initResultsSplitter() {
const layout = $('search-layout'); const splitter = $('search-splitter');
const minimum = 25; const maximum = 55; const storageKey = 'akyldash-search-results-share';
let value = 36;
try { const saved = Number(localStorage.getItem(storageKey)); if (Number.isFinite(saved) && saved >= minimum && saved <= maximum) value = saved; } catch (_) {}
let value = 35.7; let userAdjusted = false;
try { const saved = Number(localStorage.getItem(storageKey)); if (Number.isFinite(saved) && saved >= minimum && saved <= maximum) { value = saved; userAdjusted = true; } } catch (_) {}
const apply = (next, persist = false) => {
value = Math.max(minimum, Math.min(maximum, Math.round(next)));
const filtersHidden = layout.dataset.filtersHidden === 'true';
const wide = matchMedia('(min-width: 1192px)').matches;
const compact = matchMedia('(min-width: 901px) and (max-width: 1191px)').matches;
if (compact) { layout.style.gridTemplateColumns = ''; return; }
if (!wide) return;
const divider = 8;
const filterTrack = filtersHidden ? 0 : Math.min(288, Math.max(264, layout.clientWidth * 0.2 + 8));
const available = layout.clientWidth - filterTrack - divider;
const minShare = Math.max(minimum, 360 / available * 100);
const maxShare = Math.min(maximum, (available - 560) / available * 100);
const preferredShare = (layout.clientWidth * 0.15 + 192) / available * 100;
value = Math.max(minShare, Math.min(maxShare, userAdjusted ? next : preferredShare));
layout.style.gridTemplateColumns = filtersHidden
? `minmax(260px, ${value}fr) 12px minmax(320px, ${100 - value}fr)`
: `minmax(190px, clamp(190px, 18vw, 250px)) minmax(260px, ${value}fr) 12px minmax(320px, ${100 - value}fr)`;
? `minmax(360px, ${value}fr) ${divider}px minmax(560px, ${100 - value}fr)`
: `minmax(264px, ${filterTrack}px) minmax(360px, ${value}fr) ${divider}px minmax(560px, ${100 - value}fr)`;
splitter.setAttribute('aria-valuemin', String(minShare)); splitter.setAttribute('aria-valuemax', String(maxShare));
splitter.setAttribute('aria-valuenow', String(value));
if (persist) { try { localStorage.setItem(storageKey, String(value)); } catch (_) {} }
};
apply(value);
applyResultsSplitter = apply;
window.addEventListener('resize', () => apply(value, true));
let dragging = false;
splitter.addEventListener('pointerdown', (event) => {
if (matchMedia('(max-width: 900px)').matches) return;
@@ -1016,17 +1046,17 @@
splitter.addEventListener('pointermove', (event) => {
if (!dragging) return;
const rect = layout.getBoundingClientRect();
const filterWidth = layout.dataset.filtersHidden === 'true' ? 0 : $('search-filters').getBoundingClientRect().width;
const available = layout.clientWidth - filterWidth - splitter.offsetWidth;
if (available > 0) apply(((event.clientX - rect.left - filterWidth) / available) * 100);
const filterTrack = layout.dataset.filtersHidden === 'true' ? 0 : Math.min(288, Math.max(264, layout.clientWidth * 0.2 + 8));
const available = layout.clientWidth - filterTrack - 8;
if (available > 0) { userAdjusted = true; apply(((event.clientX - rect.left - filterTrack) / available) * 100); }
});
const finishDrag = (event) => { if (!dragging) return; dragging = false; apply(value, true); if (splitter.hasPointerCapture(event.pointerId)) splitter.releasePointerCapture(event.pointerId); };
splitter.addEventListener('pointerup', finishDrag); splitter.addEventListener('pointercancel', finishDrag);
splitter.addEventListener('keydown', (event) => {
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
event.preventDefault();
const next = event.key === 'Home' ? minimum : event.key === 'End' ? maximum : value + (event.key === 'ArrowRight' ? 2 : -2);
apply(next, true);
const next = event.key === 'Home' ? Number(splitter.getAttribute('aria-valuemin')) : event.key === 'End' ? Number(splitter.getAttribute('aria-valuemax')) : value + (event.key === 'ArrowRight' ? 2 : -2);
userAdjusted = true; apply(next, true);
});
return apply;
}
@@ -1241,7 +1271,7 @@
$('document-find').addEventListener('input', highlightDocumentMatches);
$('document-find-prev').addEventListener('click', () => moveDocumentMatch(-1)); $('document-find-next').addEventListener('click', () => moveDocumentMatch(1));
$('mobile-filters-open').addEventListener('click', () => {
if (matchMedia('(max-width: 900px)').matches) openMobileFilters();
if (matchMedia('(max-width: 1191px)').matches) openMobileFilters();
else {
const hidden = $('search-layout').dataset.filtersHidden !== 'true';
$('search-layout').dataset.filtersHidden = String(hidden);
@@ -1306,7 +1336,7 @@
document.querySelectorAll('.review__user-table-head [role="columnheader"]').forEach((cell, index) => { cell.textContent = [words.login, words.role, words.state, words.lastLogin, words.actions][index]; });
$('create-user-heading').textContent = words.create; document.querySelectorAll('#create-user-form label').forEach((label, index) => { const input = label.querySelector('input'); label.firstChild.textContent = `${[words.username, words.tempPassword][index]} `; if (input) label.append(input); }); document.querySelector('#create-user-form p').textContent = words.tempHelp; document.querySelector('#create-user-form button[type="submit"]').textContent = words.create;
$('delete-user-heading').textContent = words.deleteHeading; $('delete-user-confirm').textContent = words.delete; document.querySelectorAll('#create-user-dialog [data-close], #delete-user-dialog [data-close]').forEach((button) => button.textContent = words.cancel);
document.querySelector('footer .review__header-content').textContent = `Акылдаш · Frontend v0.3.0 · Backend v0.13.1`;
document.querySelector('footer .review__header-content').textContent = `Акылдаш · Frontend v0.3.1 · Backend v0.13.1`;
if (usersRoute && account) loadUsers();
};
$('users-ui-ru').addEventListener('click', () => updateUsersLanguage('ru'));
@@ -1391,7 +1421,7 @@
document.querySelectorAll('[data-close]').forEach((button) => button.addEventListener('click', () => $(button.dataset.close).close()));
['review-logout', 'search-logout'].forEach((id) => $(id).addEventListener('click', async () => { await fetch('/auth/logout', {method: 'POST', credentials: 'same-origin', headers: {'Content-Type': 'application/json'}, body: '{}'}); account = null; document.body.classList.add('review__locked'); showLogin(); }));
(async () => { const response = await fetch('/auth/me', {credentials: 'same-origin'}); if (!response.ok) { showLogin(); return; } const payload = await response.json(); if (payload.account.must_change_password) { $('password-dialog').showModal(); $('current-password').focus(); return; } enterWorkspace(payload.account); })();
const setStatus = (text, error = false) => { $('status').textContent = text; $('status').classList.toggle('review__status--error', error); $('status').setAttribute('role', error ? 'alert' : 'status'); $('status').setAttribute('aria-live', error ? 'assertive' : 'polite'); };
const setStatus = (text, error = false) => { const status = $('status'); const busy = !error && /(?:выполняется|загрузка|жүктөлүүдө)/i.test(text); status.textContent = text; status.classList.toggle('review__status--error', error); status.classList.toggle('review__status--busy', busy); status.classList.toggle('review__status--success', Boolean(text) && !error && !busy); status.setAttribute('role', error ? 'alert' : 'status'); status.setAttribute('aria-live', error ? 'assertive' : 'polite'); };
const setError = (text) => setStatus(text, true);
const escapeText = (value) => value == null ? '' : String(value);
const readDraft = () => { try { return JSON.parse(localStorage.getItem(draftStorageKey()) || 'null'); } catch (_) { return null; } };
+46 -30
View File
@@ -5,22 +5,23 @@ const repo = path.resolve(__dirname, '../..');
const artifacts = process.env.VISUAL_REVIEW_ARTIFACTS || path.join(repo, 'docs/product/visual-review');
const html = fs.readFileSync(path.join(repo, 'backend/search/review.html'), 'utf8');
const root = path.join(repo, 'backend/search/static/fonts');
const {docs, reviewDocs, catalogs, article, articleKy} = require('./fixtures.cjs');
const {docs, reviewDocs, catalogs, passwordChangeAccount, article, articleKy} = require('./fixtures.cjs');
fs.mkdirSync(artifacts, {recursive: true});
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:[]};
let failNextEdition = false; let delayNextEdition = false; let delayNextPublicMetadata = false; let failReviewDocumentB = false; let delayReviewDocumentA = false;
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});
const page = await browser.newPage({viewport:{width:1280,height:800},deviceScaleFactor:1});
const page = await browser.newPage({viewport:{width:1440,height:800},deviceScaleFactor:1});
await page.addInitScript(()=>{if(!sessionStorage.getItem('visual-review-splitter-reset')){localStorage.removeItem('akyldash-search-results-share');sessionStorage.setItem('visual-review-splitter-reset','1');}});
const errors=[]; page.on('pageerror',e=>errors.push(e.message));
await page.route('**/*', async route=>{
const req=route.request(), url=new URL(req.url());
if (req.isNavigationRequest()) return route.fulfill({status:200,contentType:'text/html; charset=utf-8',body:html});
const p=url.pathname;
if(p==='/auth/me') return route.fulfill({json:{account:{username:'admin',role:'admin',created_at:1780000000,must_change_password:false}}});
if(p==='/auth/me') return route.fulfill({json:{account:passwordChangeAccount}});
if(p==='/search/filters') return route.fulfill({json:catalogs});
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:{username:'admin',role:'admin',created_at:1780000000,must_change_password:false}}}); }
if(p==='/auth/change-password' && req.method()==='POST') { fixtureState.passwordChanges++; return route.fulfill({json:{account:passwordChangeAccount}}); }
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}}); }
const resetMatch=p.match(/^\/admin\/users\/([^/]+)\/reset-password$/);
@@ -28,7 +29,7 @@ let failNextEdition = false; let delayNextEdition = false; let delayNextPublicMe
const deleteMatch=p.match(/^\/admin\/users\/([^/]+)$/);
if(deleteMatch && req.method()==='DELETE') { fixtureState.userDeletes++; const username=decodeURIComponent(deleteMatch[1]); fixtureState.users=fixtureState.users.filter((candidate)=>candidate.username!==username); return route.fulfill({json:{}}); }
if(p==='/search') {
if(url.searchParams.has('page_size')&&!url.searchParams.has('sort')) return route.fulfill({json:{results:reviewDocs,has_next:false,review_token:'fixture-review-token'}});
if(url.searchParams.has('page_size')&&!url.searchParams.has('sort')) { if(delayNextReviewSearch){delayNextReviewSearch=false;await new Promise(resolve=>setTimeout(resolve,300));} return route.fulfill({json:{results:reviewDocs,has_next:false,review_token:'fixture-review-token'}}); }
return route.fulfill(url.searchParams.get('q')==='service-error' ? {status:503,json:{error:'Fixture search error'}} : {json:{results:url.searchParams.get('q')==='not-found'?[]:docs,has_next:false,review_token:'fixture-review-token'}});
}
if(p==='/documents/doc-2/editions') return route.fulfill({json:{available_languages:[],editions:[]}});
@@ -44,7 +45,10 @@ let failNextEdition = false; let delayNextEdition = false; let delayNextPublicMe
return route.fulfill({status:404,contentType:'application/json',body:'{}'});
});
await page.goto('https://fixture.invalid/'); await page.locator('#search-results-heading').waitFor();
const initialSearch=await page.evaluate(()=>({state:document.querySelector('#public-results').dataset.state,list:document.querySelector('#public-results').textContent.trim(),count:document.querySelector('#result-count').textContent,paginationHidden:document.querySelector('.search__pagination').hidden,breadcrumbHidden:document.querySelector('.search__breadcrumbs').hidden,documentText:document.querySelector('#document-facts').textContent,headingHidden:getComputedStyle(document.querySelector('#public-document-heading')).display==='none',opened:document.querySelectorAll('.search__opened').length}));
await page.waitForFunction(()=>document.querySelector('#search-splitter')?.getAttribute('aria-valuenow')!=='36');
const freshSplitter=await page.evaluate(()=>{const r=s=>document.querySelector(s).getBoundingClientRect(),tracks=getComputedStyle(document.querySelector('#search-layout')).gridTemplateColumns.split(' ');return {filters:r('#search-filters').width,results:r('.search__results').width,gutter:parseFloat(tracks[2]),document:r('#public-document-panel').width,brand:r('.search__brand').width,descriptor:getComputedStyle(document.querySelector('.search__brand-description')).display,brandLabel:document.querySelector('.search__brand').getAttribute('aria-label')};});
await page.setViewportSize({width:1280,height:800});
const initialFooterCount=await page.evaluate(()=>[...document.querySelectorAll('body > footer')].filter(el=>getComputedStyle(el).display!=='none'&&el.getBoundingClientRect().height>0).length); const initialSearch=await page.evaluate(()=>({state:document.querySelector('#public-results').dataset.state,list:document.querySelector('#public-results').textContent.trim(),count:document.querySelector('#result-count').textContent,paginationHidden:document.querySelector('.search__pagination').hidden,breadcrumbHidden:document.querySelector('.search__breadcrumbs').hidden,documentText:document.querySelector('#document-facts').textContent,headingHidden:getComputedStyle(document.querySelector('#public-document-heading')).display==='none',opened:document.querySelectorAll('.search__opened').length}));
await page.goto('https://fixture.invalid/?q=%D0%BD%D0%B0%D0%BB%D0%BE%D0%B3%20%D0%BD%D0%B0%20%D0%BF%D1%80%D0%B8%D0%B1%D1%8B%D0%BB%D1%8C&status=active');
const favicons=await page.evaluate(async()=>Promise.all([...document.querySelectorAll('link[rel="icon"]')].map(async link=>({type:link.type,status:(await fetch(link.href)).status}))));
await page.evaluate(()=>document.fonts.ready);
@@ -59,10 +63,10 @@ let failNextEdition = false; let delayNextEdition = false; let delayNextPublicMe
await page.addStyleTag({content:'.search__fixture-label{display:inline-block;margin-inline-start:0;padding-inline-start:12px;border-inline-start:1px solid #ded5cd;color:#786469;font-size:13px}' });
await page.evaluate(()=>{const n=document.createElement('span');n.className='search__fixture-label';n.textContent='Демонстрационные данные';document.querySelector('#active-filters').after(n);const note=document.querySelector('#document-note');note.textContent='Текст приведён для примера оформления';note.hidden=false});
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 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')); 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 splitterBox=await splitter.boundingBox(); await page.mouse.move(splitterBox.x+6,splitterBox.y+splitterBox.height/2); await page.mouse.down(); await page.mouse.move(splitterBox.x-86,splitterBox.y+splitterBox.height/2,{steps:4}); await page.mouse.up(); const splitterPointer=Number(await splitter.getAttribute('aria-valuenow')); const splitterStored=await page.evaluate(()=>localStorage.getItem('akyldash-search-results-share'));
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=Number(await page.locator('#search-splitter').getAttribute('aria-valuenow'));
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};}); await page.locator('#account-dialog').evaluate(d=>d.close()); await page.locator('#public-query').fill('not-found'); await page.locator('#public-search-form button[type=submit]').click(); await page.waitForFunction(()=>document.querySelector('#result-count').textContent.startsWith('0 ')); await page.locator('#ui-ky').click(); const kyrgyzEmpty=await page.evaluate(()=>({count:document.querySelector('#result-coLine truncated
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'))}));
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}));
await page.setViewportSize({width:390,height:844});
@@ -76,13 +80,14 @@ let failNextEdition = false; let delayNextEdition = false; let delayNextPublicMe
await page.screenshot({path:path.join(artifacts,'mobile-390px.png'),fullPage:true});
await page.locator('#document-back').click();
const back=await page.locator('#search-layout').getAttribute('data-mobile-view'); await page.setViewportSize({width:1024,height:900}); await page.waitForFunction(()=>document.querySelector('#search-filters').getAttribute('aria-hidden')===null); const desktopFilters=await page.evaluate(()=>({visible:getComputedStyle(document.querySelector('#search-filters')).display!=='none',ariaHidden:document.querySelector('#search-filters').getAttribute('aria-hidden')}));
await page.setViewportSize({width:1440,height:900}); await page.goto('https://fixture.invalid/review'); await page.locator('#review').waitFor(); const routes={searchHidden:await page.locator('#search-page').isHidden(),reviewVisible:await page.locator('#review').isVisible()}; const labLayout=await page.evaluate(()=>{const panel=document.querySelector('#document-panel'),body=document.querySelector('#document-body');for(let i=0;i<160;i++){const p=document.createElement('p');p.textContent='Эталонный текст длинной фиктивной редакции для проверки прокрутки панели документа. '.repeat(4);body.append(p);}const save=document.querySelector('#save'),footer=document.querySelector('body > footer.review__header'),r=panel.getBoundingClientRect();return {initialSaveDisabled:save.disabled,initialSaveText:save.textContent,initialDraft:document.querySelector('#draft-status').textContent,panelHeight:r.height,panelClient:panel.clientHeight,panelScroll:panel.scrollHeight,viewport:innerHeight,navHeight:document.querySelector('body > header.review__header .review__nav').getBoundingClientRect().height,titlebarHeight:document.querySelector('body > header.review__header .review__header-content').getBoundingClientRect().height,actionsHeight:document.querySelector('body > .review__actions').getBoundingClientRect().height,footerHeight:document.querySelector('body > footer.review__header').getBoundingClientRect().height,labTitleSize:parseFloat(getComputedStyle(document.querySelector('#route-heading')).fontSize),sectionHeadingSize:parseFloat(getComputedStyle(document.querySelector('.review__results .review__panel-heading h2')).fontSize),headerLeft:document.querySelector('body > header.review__header').getBoundingClientRect().left,headerRight:document.querySelector('body > header.review__header').getBoundingClientRect().right,workspaceLeft:document.querySelector('.review__workspace').getBoundingClientRect().left,workspaceRight:document.querySelector('.review__workspace').getBoundingClientRect().right,saveAfterPanel:Boolean(panel.compareDocumentPosition(save)&Node.DOCUMENT_POSITION_FOLLOWING),footerAfterSave:Boolean(save.compareDocumentPosition(footer)&Node.DOCUMENT_POSITION_FOLLOWING),footerVisible:getComputedStyle(footer).display!=='none',headerLeft:document.querySelector('body > header.review__header').getBoundingClientRect().left,headerRight:document.querySelector('body > header.review__header').getBoundingClientRect().right,workspaceLeft:document.querySelector('.review__workspace').getBoundingClientRect().left,workspaceRight:document.querySelector('.review__workspace').getBoundingClientRect().right,publicFooterVisible:getComputedStyle(document.querySelector('.search__footer')).display!=='none'};});
await page.setViewportSize({width:1440,height:900}); await page.goto('https://fixture.invalid/review'); await page.locator('#review').waitFor(); const routes={searchHidden:await page.locator('#search-page').isHidden(),reviewVisible:await page.locator('#review').isVisible(),visibleFooters:await page.evaluate(()=>[...document.querySelectorAll('body > footer')].filter(el=>getComputedStyle(el).display!=='none'&&el.getBoundingClientRect().height>0).length)}; const labLayout=await page.evaluate(()=>{const panel=document.querySelector('#document-panel'),body=document.querySelector('#document-body');for(let i=0;i<160;i++){const p=document.createElement('p');p.textContent='Эталонный текст длинной фиктивной редакции для проверки прокрутки панели документа. '.repeat(4);body.append(p);}const save=document.querySelector('#save'),footer=document.querySelector('body > footer.review__header'),r=panel.getBoundingClientRect();return {initialSaveDisabled:save.disabled,initialSaveText:save.textContent,initialDraft:document.querySelector('#draft-status').textContent,panelHeight:r.height,panelClient:panel.clientHeight,panelScroll:panel.scrollHeight,viewport:innerHeight,navHeight:document.querySelector('body > header.review__header .review__nav').getBoundingClientRect().height,titlebarHeight:document.querySelector('body > header.review__header .review__header-content').getBoundingClientRect().height,actionsHeight:document.querySelector('body > .review__actions').getBoundingClientRect().height,footerHeight:document.querySelector('body > footer.review__header').getBoundingClientRect().height,labTitleSize:parseFloat(getComputedStyle(document.querySelector('#route-heading')).fontSize),sectionHeadingSize:parseFloat(getComputedStyle(document.querySelector('.review__results .review__panel-heading h2')).fontSize),headerLeft:document.querySelector('body > header.review__header').getBoundingClientRect().left,headerRight:document.querySelector('body > header.review__header').getBoundingClientRect().right,workspaceLeft:document.querySelector('.review__workspace').getBoundingClientRect().left,workspaceRight:document.querySelector('.review__workspace').getBoundingClientRect().right,saveAfterPanel:Boolean(panel.compareDocumentPosition(save)&Node.DOCUMENT_POSITION_FOLLOWING),footerAfterSave:Boolean(save.compareDocumentPosition(footer)&Node.DOCUMENT_POSITION_FOLLOWING),footerVisible:getComputedStyle(footer).display!=='none',headerLeft:document.querySelector('body > header.review__header').getBoundingClientRect().left,headerRight:document.querySelector('body > header.review__header').getBoundingClientRect().right,workspaceLeft:document.querySelector('.review__workspace').getBoundingClientRect().left,workspaceRight:document.querySelector('.review__workspace').getBoundingClientRect().right,publicFooterVisible:getComputedStyle(document.querySelector('.search__footer')).display!=='none'};});
const labGeometry=await page.evaluate(()=>{const rect=s=>document.querySelector(s).getBoundingClientRect(),action=rect('body > .review__actions'),workspace=rect('.review__workspace'),footer=rect('body > footer.review__header');return {headingLeft:rect('#route-heading').left,helpRight:rect('#tutorial-open').right,subtitleLeft:rect('#route-description').left,workspaceActionGap:action.top-workspace.bottom,actionFooterGap:footer.top-action.bottom,actionsHeight:action.height,footerHeight:footer.height};});
const tutorialScreens={}; for(const [name,viewport] of [['desktop',{width:1280,height:800}],['mobile',{width:390,height:844}]]){await page.setViewportSize(viewport); await page.locator('#tutorial-open').click(); tutorialScreens[name]=[]; for(const target of ['#query','#results','#document-panel','#save']){const selector=name==='mobile'&&target==='#document-panel'?'#document-heading':target;const geometry=await page.evaluate((selector)=>{const dialog=document.querySelector('#tutorial-dialog'),target=document.querySelector(selector),d=dialog.getBoundingClientRect(),t=target.getBoundingClientRect(),style=getComputedStyle(dialog),targetStyle=getComputedStyle(target);return {step:document.querySelector('#tutorial-progress').textContent,width:d.width,height:d.height,overlaps:d.left<t.right&&d.right>t.left&&d.top<t.bottom&&d.bottom>t.top,font:style.fontFamily,titleFont:getComputedStyle(document.querySelector('#tutorial-heading')).fontFamily,background:style.backgroundColor,targetDisplay:targetStyle.display,targetInViewport:t.bottom>0&&t.top<innerHeight,target:{left:t.left,top:t.top,right:t.right,bottom:t.bottom}}},selector);if(geometry.targetDisplay==='none'||geometry.target.right===geometry.target.left||geometry.target.bottom===geometry.target.top||!geometry.targetInViewport) throw new Error(`Tutorial target is not visible at ${name}: ${JSON.stringify(geometry)}`);if(geometry.overlaps) throw new Error(`Tutorial overlaps ${target} at ${name}: ${JSON.stringify(geometry)}`);if(geometry.width>320||!geometry.font.includes('Noto Sans')||!geometry.titleFont.includes('Noto Serif')||!geometry.background.includes('0.92')) throw new Error(`Tutorial style mismatch at ${name}: ${JSON.stringify(geometry)}`);tutorialScreens[name].push(geometry);if(target==='#query') await page.screenshot({path:path.join(artifacts,`review-tour-${name}.png`),fullPage:false});if(target!=='#save') await page.locator('#tutorial-next').press('Enter');}await page.locator('#tutorial-next').press('Enter');}
await page.setViewportSize({width:1280,height:800}); await page.locator('#query').fill('налог на прибыль'); await page.locator('#search-form button[type=submit]').click(); await page.locator('.review__result').first().waitFor(); const reviewSaveEnabled=await page.locator('#save').isEnabled(); const commentDisclosure=page.locator('.review__comment-disclosure').first(); const commentStartsCollapsed=!(await commentDisclosure.evaluate(e=>e.open)); await commentDisclosure.locator('summary').click(); await commentDisclosure.locator('textarea').fill('Фиктивный комментарий для проверки черновика'); await page.locator('input[name="rating-0"][value="2"]').check(); await page.waitForFunction(()=>document.querySelector('#draft-status').textContent==='Черновик сохранён на этом устройстве.'); await page.locator('.review__result-title').first().click(); await page.locator('#document-body h2').first().waitFor(); await page.locator('#document-body').evaluate(body=>{for(let i=0;i<160;i++){const p=document.createElement('p');p.textContent='Эталонный длинный документ в безопасном browser fixture. '.repeat(5);body.append(p);}}); await page.setViewportSize({width:1280,height:720}); const reviewScroll=await page.locator('#document-panel').evaluate(panel=>({height:panel.getBoundingClientRect().height,client:panel.clientHeight,scroll:panel.scrollHeight,viewport:innerHeight,internalScroll:panel.scrollHeight>panel.clientHeight,saveAfter:Boolean(panel.compareDocumentPosition(document.querySelector('#save'))&Node.DOCUMENT_POSITION_FOLLOWING),footerAfterSave:Boolean(document.querySelector('#save').compareDocumentPosition(document.querySelector('body > footer.review__header'))&Node.DOCUMENT_POSITION_FOLLOWING)})); await page.locator('#save').scrollIntoViewIfNeeded(); const saveViewport=await page.locator('#save').evaluate(el=>{const r=el.getBoundingClientRect();const footer=document.querySelector('body > footer.review__header').getBoundingClientRect();return {top:r.top,bottom:r.bottom,visible:r.top>=0&&r.bottom<=innerHeight,footerVisible:footer.top>=0&&footer.bottom<=innerHeight}}); await page.screenshot({path:path.join(artifacts,'lab-save-footer-1280x720.png'),fullPage:false}); await page.locator('#save').click(); await page.waitForFunction(()=>document.querySelector('#status').textContent.includes('Оценка сохранена')); const reviewFlow={reviewSaveEnabled,commentStartsCollapsed,commentExpanded:await commentDisclosure.evaluate(e=>e.open),draftStatus:await page.locator('#draft-status').textContent(),reviewScroll,saveViewport,reviewSaved:await page.locator('#status').textContent()};
await page.setViewportSize({width:1280,height:800}); await page.locator('#query').fill('налог на прибыль'); delayNextReviewSearch=true; await page.locator('#search-form button[type=submit]').click(); await page.waitForFunction(()=>document.querySelector('#status').classList.contains('review__status--busy')); const labLoadingStatus=await page.locator('#status').evaluate(el=>({height:el.getBoundingClientRect().height,role:el.getAttribute('role'),busy:el.classList.contains('review__status--busy')})); await page.locator('.review__result').first().waitFor(); const successStatus=await page.locator('#status').evaluate(el=>({height:el.getBoundingClientRect().height,role:el.getAttribute('role'),live:el.getAttribute('aria-live'),success:el.classList.contains('review__status--success')})); await page.locator('.review__result-title').first().click(); await page.locator('#document-body h2').first().waitFor(); const compactResult=await page.locator('.review__result--selected').evaluate(el=>({padding:getComputedStyle(el).padding,titleTarget:el.querySelector('.review__result-title').getBoundingClientRect().height,ratingTarget:el.querySelector('.review__rating label').getBoundingClientRect().height,metaMargin:getComputedStyle(el.querySelector('.review__meta')).marginBlockStart})); const reviewSaveEnabled=await page.locator('#save').isEnabled(); const commentDisclosure=page.locator('.review__comment-disclosure').first(); const commentStartsCollapsed=!(await commentDisclosure.evaluate(e=>e.open)); await commentDisclosure.locator('summary').click(); await commentDisclosure.locator('textarea').fill('Фиктивный комментарий для проверки черновика'); await page.locator('input[name="rating-0"][value="2"]').check(); await page.waitForFunction(()=>document.querySelector('#draft-status').textContent==='Черновик сохранён на этом устройстве.'); await page.locator('.review__result-title').first().click(); await page.locator('#document-body h2').first().waitFor(); await page.locator('#document-body').evaluate(body=>{for(let i=0;i<160;i++){const p=document.createElement('p');p.textContent='Эталонный длинный документ в безопасном browser fixture. '.repeat(5);body.append(p);}}); await page.setViewportSize({width:1280,height:720}); const reviewScroll=await page.locator('#document-panel').evaluate(panel=>({height:panel.getBoundingClientRect().height,client:panel.clientHeight,scroll:panel.scrollHeight,viewport:innerHeight,internalScroll:panel.scrollHeight>panel.clientHeight,saveAfter:Boolean(panel.compareDocumentPosition(document.querySelector('#save'))&Node.DOCUMENT_POSITION_FOLLOWING),footerAfterSave:Boolean(document.querySelector('#save').compareDocumentPosition(document.querySelector('body > footer.review__header'))&Node.DOCUMENT_POSITION_FOLLOWING)})); await page.locator('#save').scrollIntoViewIfNeeded(); const saveViewport=await page.locator('#save').evaluate(el=>{const r=el.getBoundingClientRect();const footer=document.querySelector('body > footer.review__header').getBoundingClientRect();return {top:r.top,bottom:r.bottom,visible:r.top>=0&&r.bottom<=innerHeight,footerVisible:footer.top>=0&&footer.bottom<=innerHeight}}); await page.screenshot({path:path.join(artifacts,'lab-save-footer-1280x720.png'),fullPage:false}); await page.locator('#save').click(); await page.waitForFunction(()=>document.querySelector('#status').textContent.includes('Оценка сохранена')); const reviewFlow={reviewSaveEnabled,commentStartsCollapsed,commentExpanded:await commentDisclosure.evaluate(e=>e.open),draftStatus:await page.locator('#draft-status').textContent(),reviewScroll,saveViewport,reviewSaved:await page.locator('#status').textContent()};
await page.locator('#query').fill('document-switch-fixture'); await page.locator('#search-form button[type=submit]').click(); await page.waitForFunction(()=>document.querySelector('.review__result-title span:last-child')?.textContent==='Fixture document A');
await page.locator('.review__comment-disclosure').first().locator('summary').click(); await page.locator('.review__comment').first().fill('Fixture-only note for result A.'); const fixtureRating=page.locator('input[name="rating-0"][value="2"]'); await fixtureRating.check(); const ratingBeforeDocumentSwitch=await fixtureRating.isChecked();
await page.locator('.review__result-title').nth(0).click(); await page.locator('#document-body h2').filter({hasText:'Fixture document A'}).waitFor();
failReviewDocumentB=true; await page.locator('.review__result-title').nth(1).click(); await page.waitForFunction(()=>document.querySelector('#status').textContent==='Не удалось загрузить документ. Повторите попытку.');
failReviewDocumentB=true; await page.locator('.review__result-title').nth(1).click(); await page.waitForFunction(()=>document.querySelector('#status').textContent==='Не удалось загрузить документ. Повторите попытку.'); const errorStatus=await page.locator('#status').evaluate(el=>({height:el.getBoundingClientRect().height,role:el.getAttribute('role'),live:el.getAttribute('aria-live')}));
const failedDocumentB=await page.evaluate(()=>({selected:document.querySelector('.review__result--selected .review__result-title span:last-child').textContent,heading:document.querySelector('#dialog-heading').textContent,body:document.querySelector('#document-body').textContent,dialogBody:document.querySelector('#dialog-body').textContent,sourceCount:document.querySelectorAll('#document-meta a').length,rating:document.querySelector('input[name="rating-0"]:checked')?.value||null,comment:document.querySelector('.review__comment').value}));
failReviewDocumentB=false; await page.locator('.review__result-title').nth(1).click(); await page.locator('#document-body h2').filter({hasText:'Fixture document B'}).waitFor();
const successfulDocumentB=await page.evaluate(()=>({heading:document.querySelector('#dialog-heading').textContent,body:document.querySelector('#document-body').textContent,source:document.querySelector('#document-meta a')?.href||null,rating:document.querySelector('input[name="rating-0"]:checked')?.value||null,comment:document.querySelector('.review__comment').value}));
@@ -109,12 +114,10 @@ let failNextEdition = false; let delayNextEdition = false; let delayNextPublicMe
await page.setViewportSize({width:1280,height:800});
await page.locator('#search-account-open').click(); await page.evaluate(()=>{const d=document.querySelector('#account-dialog');const controls=[...d.querySelectorAll('a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])')].filter(e=>e.getClientRects().length&&getComputedStyle(e).visibility!=='hidden');controls.at(-1).focus();}); await page.keyboard.press('Tab'); const accountTabTarget=await page.evaluate(()=>({tag:document.activeElement.tagName,id:document.activeElement.id,dialog:document.querySelector('#account-dialog').contains(document.activeElement)})); const accountTabWrap=accountTabTarget.dialog; await page.keyboard.press('Shift+Tab'); const accountReverseTabWrap=await page.evaluate(()=>({id:document.activeElement.id,dialog:document.querySelector('#account-dialog').contains(document.activeElement)})); await page.keyboard.press('Escape');
const viewportWidths=[]; for (const width of [320,390,640,1024,1101,1175,1176,1280,1440,1713]) { await page.setViewportSize({width,height:800}); viewportWidths.push(await page.evaluate(()=>({width:innerWidth,scrollWidth:document.documentElement.scrollWidth}))); } await page.setViewportSize({width:1280,height:800});
const breakpointLayouts=[]; for (const width of [1101,1175,1176]) { await page.setViewportSize({width,height:800}); breakpointLayouts.push(await page.evaluate(()=>({width:innerWidth,scrollWidth:document.documentElement.scrollWidth,columns:getComputedStyle(document.querySelector('#search-layout')).gridTemplateColumns.split(' ').length,filtersHidden:false}))); }
await page.setViewportSize({width:1280,height:800}); await page.locator('#mobile-filters-open').click();
for (const width of [1101,1175,1176]) { await page.setViewportSize({width,height:800}); breakpointLayouts.push(await page.evaluate(()=>({width:innerWidth,scrollWidth:document.documentElement.scrollWidth,columns:getComputedStyle(document.querySelector('#search-layout')).gridTemplateColumns.split(' ').length,filtersHidden:document.querySelector('#search-layout').dataset.filtersHidden==='true'}))); }
await page.setViewportSize({width:1280,height:800}); await page.locator('#mobile-filters-open').click();
const breakpointLayouts=[]; for (const width of [1024,1101,1175,1191]) { await page.setViewportSize({width,height:800}); await page.evaluate(()=>window.dispatchEvent(new Event('resize'))); breakpointLayouts.push(await page.evaluate(()=>{const rect=(selector)=>document.querySelector(selector).getBoundingClientRect();return {width:innerWidth,scrollWidth:document.documentElement.scrollWidth,filtersWidth:rect('#search-filters').width,resultsWidth:rect('.search__results').width,splitterWidth:rect('#search-splitter').width,documentWidth:rect('#public-document-panel').width,filtersOpen:document.querySelector('#search-filters').classList.contains('is-open')}})); }
await page.setViewportSize({width:1024,height:800}); await page.locator('#mobile-filters-open').click(); const filterDrawer=await page.evaluate(()=>({open:document.querySelector('#search-filters').classList.contains('is-open'),modal:document.querySelector('#search-filters').getAttribute('aria-modal'),overlay:getComputedStyle(document.querySelector('#filter-overlay')).display!=='none',results:document.querySelector('.search__results').getBoundingClientRect().width,document:document.querySelector('#public-document-panel').getBoundingClientRect().width})); const filterCloseVisible=await page.locator('#mobile-filters-close').isVisible(); await page.locator('#mobile-filters-close').click(); await page.locator('#mobile-filters-open').click(); await page.keyboard.press('Escape'); const filterDrawerEscape=await page.evaluate(()=>({closed:!document.querySelector('#search-filters').classList.contains('is-open'),focus:document.activeElement.id})); await page.setViewportSize({width:1280,height:800});
const longContent=await page.evaluate(()=>{const result=document.querySelector('.search__result-title'),heading=document.querySelector('#public-document-heading'),oldResult=result.textContent,oldHeading=heading.textContent;const longName='Кыргызская Республика '.repeat(8);result.textContent=longName;heading.textContent=longName;const image=document.createElement('img');image.width=2200;image.height=100;image.src='data:image/svg+xml,'+encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="2200" height="100"><rect width="2200" height="100" fill="gold"/></svg>');document.querySelector('#public-document-body').append(image);const titleWidths=[result,heading].map(el=>({client:el.clientWidth,scroll:el.scrollWidth}));const imageWidth=image.getBoundingClientRect().width,bodyWidth=document.querySelector('#public-document-body').clientWidth,pageWidth=document.documentElement.scrollWidth;result.textContent=oldResult;heading.textContent=oldHeading;image.remove();return {titleWidths,imageWidth,bodyWidth,pageWidth,viewport:innerWidth}});
console.log('safeLinks',JSON.stringify(safeLinks)); console.log('initialSearch',JSON.stringify(initialSearch)); console.log('tocState',JSON.stringify(tocState)); console.log('splitterState',JSON.stringify({splitterStart,splitterKeyboard,splitterHome,splitterEnd,splitterPointer,splitterStored,splitterRestored,stalePublicSelection,markerAfterReload})); console.log('viewportWidths',JSON.stringify(viewportWidths)); console.log('breakpointLayouts',JSON.stringify(breakpointLayouts));
console.log('safeLinks',JSON.stringify(safeLinks)); console.log('initialSearch',JSON.stringify(initialSearch)); console.log('tocState',JSON.stringify(tocState)); console.log('splitterState',JSON.stringify({splitterStart,splitterKeyboard,keyboardMin,keyboardMax,splitterHome,splitterEnd,splitterLeft,splitterRight,splitterPointer,splitterStored,splitterRestored,splitterStoredMax,splitterStoredMin,splitterResize,stalePublicSelection,markerAfterReload})); console.log('freshSplitter',JSON.stringify(freshSplitter)); console.log('splitterDefault',JSON.stringify(splitterDefault)); console.log('filterDrawer',JSON.stringify({filterDrawer,filterCloseVisible,filterDrawerEscape})); console.log('visualDetails',JSON.stringify({successStatus,errorStatus,labLoadingStatus,compactResult,accountGeometry})); console.log('viewportWidths',JSON.stringify(viewportWidths)); console.log('breakpointLayouts',JSON.stringify(breakpointLayouts));
console.log('subnavAcceptance',JSON.stringify({sourcesDialog,sourcesEscapeClosed,sourcesFocusRestored,accountDialogFocusInside,accountEscapeClosed,accountFocusRestored,printState,tableOverflow,reducedMotion,searchError,editionError,editionRetry,previousEditionSource,sourceDuringEditionLoad,sourceAfterEditionError,sourceAfterEditionRetry,noEditions,languageFallback,noEditionExpandedText,noEditionExpandedHeading,noEditionExpandHidden,editionErrorExpandedText,editionErrorExpandedHeading,editionErrorExpandHidden,expandedEscapeClosed,expandedFocusRestored,editionLoading,accountTabWrap,accountTabTarget,accountReverseTabWrap,longContent,tutorialScreens,viewportWidths,breakpointLayouts}));
console.log(JSON.stringify({resultHighlight,fonts,favicons,widths,view,drawer,modalBackground,focusWrap,focusVisible,focusReverseWrap,focusRestored,back,chipsBefore,chipsWithTwo,preservedFilters,urlFilters,openedCount,firstFindCount,findClearedAfterFilterChange,nextFindCount,documentTitleStyle,documentLanguage,kyrgyzUi,kyrgyzEmpty,expanded,accountActions,desktopFilters,routes,errors},null,2));
const referencePath=process.env.REFERENCE_IMAGE || path.join(repo, 'docs/design/tunduk-search-target.png');
@@ -151,8 +154,8 @@ let failNextEdition = false; let delayNextEdition = false; let delayNextPublicMe
["safeLinks.privateImage!==null", safeLinks.privateImage!==null],
["viewportWidths.some(item=>item.scrollWidth>item.width)", viewportWidths.some(item=>item.scrollWidth>item.width)],
["breakpointLayouts.some(item=>item.scrollWidth>item.width)", breakpointLayouts.some(item=>item.scrollWidth>item.width)],
["visible layout does not include the splitter", breakpointLayouts.slice(0,3).some(item=>item.columns!==4)],
["hidden filters do not preserve the two content panels and splitter", breakpointLayouts.slice(3).some(item=>item.columns!==3||!item.filtersHidden)],
["compact filters stay behind the drawer while both panels fit", breakpointLayouts.some(item=>item.filtersWidth!==0||item.resultsWidth<360||item.splitterWidth!==0||item.documentWidth<560)],
["compact filter drawer preserves access without replacing the two content panels", !filterDrawer.open||filterDrawer.modal!=="true"||!filterDrawer.overlay||filterDrawer.results<360||filterDrawer.document<560||!filterCloseVisible||!filterDrawerEscape.closed||filterDrawerEscape.focus!=='mobile-filters-open'],
["resultHighlight!=='\u043d\u0430\u043b\u043e\u0433\u0435 \u043d\u0430 \u043f\u0440\u0438\u0431\u044b\u043b\u044c'", resultHighlight!=='налоге на прибыль'],
["initialSearch.state!=='initial'", initialSearch.state!=='initial'],
["initialSearch.count!==''", initialSearch.count!=='' ],
@@ -180,11 +183,22 @@ let failNextEdition = false; let delayNextEdition = false; let delayNextPublicMe
["navGeometry.languageSelectWidth<120", navGeometry.languageSelectWidth<120],
["navGeometry.searchTextAlign!=='center'", navGeometry.searchTextAlign!=='center'],
["splitterKeyboard<=splitterStart", splitterKeyboard<=splitterStart],
["splitterHome!==25", splitterHome!==25],
["splitterEnd!==55", splitterEnd!==55],
["splitterPointer<25||splitterPointer>=55", splitterPointer<25||splitterPointer>=55],
["Number(splitterStored)!==splitterPointer", Number(splitterStored)!==splitterPointer],
["splitterRestored!==36", splitterRestored!==36],
["splitterDefault.filters!==256||Math.abs(splitterDefault.results-384)>1||Math.abs(splitterDefault.document-624)>1", splitterDefault.filters!==256||Math.abs(splitterDefault.results-384)>1||Math.abs(splitterDefault.document-624)>1],
["successStatus.height>1||successStatus.role!=='status'||successStatus.live!=='polite'||!successStatus.success", successStatus.height>1||successStatus.role!=='status'||successStatus.live!=='polite'||!successStatus.success],
["errorStatus.height<=1||errorStatus.role!=='alert'||errorStatus.live!=='assertive'", errorStatus.height<=1||errorStatus.role!=='alert'||errorStatus.live!=='assertive'],
["labLoadingStatus.height<=1||labLoadingStatus.role!=='status'||!labLoadingStatus.busy", labLoadingStatus.height<=1||labLoadingStatus.role!=='status'||!labLoadingStatus.busy],
["compactResult.padding!=='8px 10px'||compactResult.titleTarget<44||compactResult.ratingTarget<44", compactResult.padding!=='8px 10px'||compactResult.titleTarget<44||compactResult.ratingTarget<44],
["accountGeometry.width!==560||!accountGeometry.logoutSeparate||!accountGeometry.logoutMuted", accountGeometry.width!==560||!accountGeometry.logoutSeparate||!accountGeometry.logoutMuted],
["splitterHome!==keyboardMin", splitterHome!==keyboardMin],
["splitterEnd!==keyboardMax", splitterEnd!==keyboardMax],
["splitterPointer<25||splitterPointer>55", splitterPointer<25||splitterPointer>55],
["splitterLeft.results<360||splitterLeft.document<560||splitterLeft.filters<256", splitterLeft.results<360||splitterLeft.document<560||splitterLeft.filters<256],
["splitterRight.results<360||splitterRight.document<560||splitterRight.filters<256", splitterRight.results<360||splitterRight.document<560||splitterRight.filters<256],
["Number(splitterStored)!==splitterRight.value", Number(splitterStored)!==splitterRight.value],
["splitterRestored.value<splitterRestored.min||splitterRestored.value>splitterRestored.max", splitterRestored.value<splitterRestored.min||splitterRestored.value>splitterRestored.max],
["splitterStoredMax.value!==splitterStoredMax.max||splitterStoredMin.value!==splitterStoredMin.min", splitterStoredMax.value!==splitterStoredMax.max||splitterStoredMin.value!==splitterStoredMin.min],
["splitterResize.results<360||splitterResize.document<560", splitterResize.results<360||splitterResize.document<560],
["freshSplitter.filters!==280||Math.abs(freshSplitter.results-408)>2||freshSplitter.gutter!==8||freshSplitter.document<560||freshSplitter.brand!==166||freshSplitter.descriptor!=='none'||freshSplitter.brandLabel!=='Акылдаш — поиск по правовой базе'", freshSplitter.filters!==280||Math.abs(freshSplitter.results-408)>2||freshSplitter.gutter!==8||freshSplitter.document<560||freshSplitter.brand!==166||freshSplitter.descriptor!=='none'||freshSplitter.brandLabel!=='Акылдаш — поиск по правовой базе'],
["stalePublicSelection.selected!=='doc-3'", stalePublicSelection.selected!=='doc-3'],
["stalePublicSelection.markers!==0", stalePublicSelection.markers!==0],
["markerAfterReload!==1", markerAfterReload!==1],
@@ -250,10 +264,11 @@ let failNextEdition = false; let delayNextEdition = false; let delayNextPublicMe
["!accountActions.headingFontFamily.includes('Noto Sans')", !accountActions.headingFontFamily.includes('Noto Sans')],
["!accountActions.dataBeforeActions", !accountActions.dataBeforeActions],
["!accountActions.dialogFits", !accountActions.dialogFits],
["desktopFilters.visible!==true", desktopFilters.visible!==true],
["desktopFilters.visible!==false", desktopFilters.visible!==false],
["desktopFilters.ariaHidden!==null", desktopFilters.ariaHidden!==null],
["routes.searchHidden!==true", routes.searchHidden!==true],
["routes.reviewVisible!==true", routes.reviewVisible!==true],
["initialFooterCount!==1||routes.visibleFooters!==1", initialFooterCount!==1||routes.visibleFooters!==1],
["!sourcesDialog.open", !sourcesDialog.open],
["sourcesDialog.labelledBy!=='sources-heading'", sourcesDialog.labelledBy!=='sources-heading'],
["!sourcesDialog.focusInside", !sourcesDialog.focusInside],
@@ -312,9 +327,9 @@ let failNextEdition = false; let delayNextEdition = false; let delayNextPublicMe
["!labInitial.saveDisabled", !labLayout.initialSaveDisabled],
["!labLayout.initialSaveText.includes('Сначала выполните поиск')", !labLayout.initialSaveText.includes('Сначала выполните поиск')],
["labLayout.panelScroll<=labLayout.panelClient", labLayout.panelScroll<=labLayout.panelClient],
["labLayout.panelHeight>labLayout.viewport*0.69", labLayout.panelHeight>labLayout.viewport*0.69],
["!labLayout.saveAfterPanel", !labLayout.saveAfterPanel],
["!labLayout.footerAfterSave", !labLayout.footerAfterSave],
["labGeometry.headingLeft!==24||labGeometry.subtitleLeft!==24||labGeometry.helpRight!==1416", labGeometry.headingLeft!==24||labGeometry.subtitleLeft!==24||labGeometry.helpRight!==1416],
["labGeometry.workspaceActionGap>1||labGeometry.actionFooterGap>1", labGeometry.workspaceActionGap>1||labGeometry.actionFooterGap>1],
["labGeometry.actionsHeight!==96||labGeometry.footerHeight!==44", labGeometry.actionsHeight!==96||labGeometry.footerHeight!==44],
["!labLayout.footerVisible", !labLayout.footerVisible],
["labLayout.publicFooterVisible", labLayout.publicFooterVisible],
["labLayout.navHeight!==64", labLayout.navHeight!==64],
@@ -337,7 +352,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(()=>({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})); console.log('usersRouteCheck',JSON.stringify({usersRoute,usersKy,errors})); if(!usersRoute.visible||!usersRoute.admin||usersRoute.accountOpen||!usersRoute.headerVisible||!usersRoute.footerAfterMain||!usersRoute.reviewActionsHidden||!usersRoute.footerAfterUsers||usersRoute.overflow||errors.length||usersKy.lang!=='ky'||usersKy.heading!=='Колдонуучулар') 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})); console.log('usersRouteCheck',JSON.stringify({usersRoute,usersKy,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!=='Колдонуучулар') 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};});
@@ -353,6 +368,7 @@ let failNextEdition = false; let delayNextEdition = false; let delayNextPublicMe
await usersPage.locator('#create-user-open').click(); await usersPage.locator('#new-username').fill('fixture-created'); await usersPage.locator('#temporary-password').fill('Fixture-only-temporary-password-2026'); await usersPage.locator('#create-user-form button[type="submit"]').click(); await usersPage.waitForFunction(()=>!document.querySelector('#create-user-dialog').open); let createdRow=usersPage.locator('#user-list .review__user-row').filter({hasText:'fixture-created'}); await createdRow.waitFor();
await createdRow.locator('.review__user-reset-disclosure summary').click(); await createdRow.locator('.review__user-reset input[type="password"]').fill('Fixture-only-reset-password-2026'); const resetResponse=usersPage.waitForResponse((response)=>new URL(response.url()).pathname==='/admin/users/fixture-created/reset-password'); await createdRow.locator('.review__user-reset button[type="submit"]').click(); await resetResponse;
await usersPage.locator('#review-account-open').click(); await usersPage.locator('#security-tab').click(); const passwordToggle=usersPage.locator('#profile-password-form .review__password-toggle').first(); const securityCloseCount=await usersPage.locator('#security-panel [data-close="account-dialog"]').count(); await passwordToggle.click(); const passwordShown=await usersPage.locator('#profile-current-password').evaluate(input=>input.type==='text'&&input.parentElement.querySelector('.review__password-toggle').getAttribute('aria-pressed')==='true'); await passwordToggle.click(); const passwordHidden=await usersPage.locator('#profile-current-password').evaluate(input=>input.type==='password'&&input.parentElement.querySelector('.review__password-toggle').getAttribute('aria-pressed')==='false'); await usersPage.locator('#profile-current-password').fill('Fixture-only-current-password-2026'); await usersPage.locator('#profile-new-password').fill('Fixture-only-new-password-2026'); await usersPage.locator('#profile-confirm-password').fill('Fixture-only-new-password-2026'); const passwordResponse=usersPage.waitForResponse((response)=>new URL(response.url()).pathname==='/auth/change-password'); await usersPage.locator('#profile-password-form button[type="submit"]').click(); await passwordResponse; await usersPage.waitForFunction(()=>!document.querySelector('#account-dialog').open); if(securityCloseCount!==0||!passwordShown||!passwordHidden) process.exit(1); console.log('accountSecurity',JSON.stringify({securityCloseCount,passwordShown,passwordHidden}));
const passwordAccountAfterChange=await usersPage.locator('#profile-username').textContent(); if(passwordAccountAfterChange!==passwordChangeAccount.username||!await usersPage.locator('#users-page').isVisible()) throw new Error('Fixture password change must return the account payload and restore the admin workspace.');
createdRow=usersPage.locator('#user-list .review__user-row').filter({hasText:'fixture-created'}); await createdRow.waitFor(); await createdRow.locator('.review__user-actions > button').last().click(); await usersPage.locator('#delete-user-confirm').click(); await usersPage.waitForFunction(()=>![...document.querySelectorAll('#user-list .review__user-row')].some(row=>row.textContent.includes('fixture-created')));
const safeFixtureOperations={passwordChanges:fixtureState.passwordChanges,userCreates:fixtureState.userCreates,passwordResets:fixtureState.passwordResets,userDeletes:fixtureState.userDeletes,reviewSaves:fixtureState.reviewSaves,createdUserRemoved:!fixtureState.users.some((user)=>user.username==='fixture-created')}; console.log('safeFixtureOperations',JSON.stringify(safeFixtureOperations));
if(safeFixtureOperations.passwordChanges!==1||safeFixtureOperations.userCreates!==1||safeFixtureOperations.passwordResets!==1||safeFixtureOperations.userDeletes!==1||safeFixtureOperations.reviewSaves<2||!safeFixtureOperations.createdUserRemoved) process.exit(1);
+2 -1
View File
@@ -22,7 +22,8 @@ const visualReviewArticles = {
'review-b':'<h2>Расчёт условной налоговой базы</h2><p>Вымышленный текст для проверки панели чтения и элементов оценки.</p>'
};
const catalogs = {document_types:[{code:'code',labels:{ru:'Кодекс',ky:'Кодекс'},count:12},{code:'law',labels:{ru:'Закон',ky:'Мыйзам'},count:10},{code:'decree',labels:{ru:'Постановление',ky:'Токтом'},count:8}],statuses:[{code:'active',labels:{ru:'Действует',ky:'Күчүндө'},count:24},{code:'repealed',labels:{ru:'Утратил силу',ky:'Күчүн жоготту'},count:5}],authorities:[{code:'ministries_and_committees',labels:{ru:'Министерства и комитеты',ky:'Министрликтер'},count:5}],legal_forces:[]};
const passwordChangeAccount = {username:'fixture-admin',role:'admin',created_at:1780000000,must_change_password:false};
const article = '<h2>Налог на прибыль</h2><p><a href="../../tmp/old.html">Old local reference</a> <a href="http://cbd.minjust.gov.kg/old">Legacy HTTP reference</a> <a href="https://cbd.minjust.gov.kg/doc-1" ping="https://evil.invalid/ping">Official reference</a> <a href="https://127.0.0.1/private">Private address reference</a><img src="../../tmp/old.png" srcset="https://evil.invalid/pixel 1x, https://192.168.1.10/pixel 2x"><img alt="Private address image" src="https://192.168.1.10/pixel"></p><p><b><span>Раздел I. Общие положения</span></b></p><p><b><span>Глава 1. Основные положения</span></b></p><p><b><span>Статья 1. Основные понятия</span></b></p><p>Обычное упоминание статьи 9 не является заголовком.</p><p id="duplicate-heading"><b><span>Статья 1. Повторный номер</span></b></p><p id="duplicate-heading"><b><span>Раздел II. Налоговая отчётность</span></b></p><p>Текст документа приведён для примера оформления. Здесь будут отображаться положения выбранной редакции. По запросу налог на прибыль читатель найдёт нужный фрагмент.</p><h2>Налоговая отчётность</h2><p>В этом разделе отображается текст выбранной редакции. Поиск внутри документа помогает перейти к нужному фрагменту.</p><p>Далее приводятся положения о налоге на прибыль.</p><p>Упоминание запроса налог на прибыль в следующем разделе.</p>';
const articleKy = '<h2>Киреше салыгы</h2><p><b><span>БӨЛҮМ 1. Жалпы жоболор</span></b></p><p><b><span>Глава 1. Негизги жоболор</span></b></p><p><b><span>Берене 1. Негизги түшүнүктөр</span></b></p><p>Текстте берене 9 жөнүндө кадимки эскерүү бар.</p>';
module.exports = {docs, reviewDocs, visualDocs, visualReviewDocs, visualArticle, visualReviewArticles, catalogs, article, articleKy};
module.exports = {docs, reviewDocs, visualDocs, visualReviewDocs, visualArticle, visualReviewArticles, catalogs, passwordChangeAccount, article, articleKy};
@@ -2,7 +2,7 @@
const http = require('node:http');
const fs = require('node:fs');
const path = require('node:path');
const {visualDocs, visualReviewDocs, visualReviewArticles, visualArticle, articleKy, catalogs} = require('./fixtures.cjs');
const {visualDocs, visualReviewDocs, visualReviewArticles, visualArticle, articleKy, catalogs, passwordChangeAccount} = require('./fixtures.cjs');
const repo = path.resolve(__dirname, '../..');
const htmlPath = path.join(repo, 'backend/search/review.html');
@@ -37,8 +37,9 @@ const server = http.createServer(async (request, response) => {
const url = new URL(request.url, 'http://127.0.0.1');
const method = request.method || 'GET';
if (method === 'GET' && ['/', '/review', '/users'].includes(url.pathname)) return sendHtml(response);
if (method === 'GET' && url.pathname === '/auth/me') return json(response, 200, {account:{username:'fixture-admin',role:'admin',created_at:1780000000,must_change_password:false}});
if (method === 'POST' && ['/auth/logout','/auth/change-password'].includes(url.pathname)) return json(response, 200, {ok:true});
if (method === 'GET' && url.pathname === '/auth/me') return json(response, 200, {account:passwordChangeAccount});
if (method === 'POST' && url.pathname === '/auth/logout') return json(response, 200, {ok:true});
if (method === 'POST' && url.pathname === '/auth/change-password') return json(response, 200, {account:passwordChangeAccount});
if (method === 'GET' && url.pathname === '/search/filters') return json(response, 200, catalogs);
if (method === 'GET' && url.pathname === '/search') {
const query = url.searchParams.get('q') || '';