fix(gps-tracks): address ET-011 review — JS UI tests + flat 403 contract
All checks were successful
All checks were successful
Закрывает findings из docs/work-items/ET-011/12-review.md (REQUEST_CHANGES,
попытка 3/3):
P1-01 — добавлены поведенческие JS unit-тесты UI download-flow
- tests/web/track_download.test.js — 28 кейсов (node --test):
• _parseFilenameFromCD — RFC 5987 приоритет, plain fallback,
битый percent-encoding, null/empty (REQ-F-05.2, AC-2 UI)
• _handleDownloadError — 400/403/404/413/5xx тосты, defensive
при отсутствии showToast, поддержка flat (ADR-015 §G) и legacy
wrapped 403-форм (REQ-F-05.4, AC-7 UI)
• _renderTrackPopupHtml — наличие кнопки, aria-label «Скачать GPX»,
data-track-id, отсутствие при невалидном id, регрессия прочих
полей (REQ-F-01, AC-1)
- tests/web/test_track_download.py — pytest-обёртка (статические
проверки + запуск Node-раннера), исполняется в обычном pytest tests/
- 04b-ui-test-cases.md: AC-13 (mobile-bbox) явно маркирован как
MANUAL release-smoke (Playwright-раннер в проекте не настроен;
альтернатива согласована reviewer'ом в P1-01).
P2-01 — устранено расхождение «doc vs runtime» по контракту 403
- endpoint.py: HTTPException(detail={...}) → JSONResponse(content={...}),
чтобы FastAPI не оборачивал dict во второй слой «detail». Контракт
теперь совпадает с ADR-015 §G и ADR-014 §6:
{"detail":"source_forbidden","external_urls":[...]}
- test_gps_tracks_download.py IT-05: упрощено — body уже плоский,
без двухуровневого `body.get("detail", body)` workaround.
- gps_tracks.js::_handleDownloadError: flat-форма стала приоритетной,
wrapped-форма оставлена как defensive fallback (с комментарием).
Регрессия: 89/89 API-тестов + 24/24 предыдущих JS-тестов + 28 новых
JS-тестов download-flow проходят. ruff check — clean.
Refs: ET-011
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -247,14 +247,11 @@ async def test_it05_source_forbidden_403(tmp_path):
|
||||
|
||||
assert resp.status_code == 403
|
||||
body = resp.json()
|
||||
# FastAPI обёртывает наш detail-dict в {"detail": {...}}.
|
||||
detail = body.get("detail", body)
|
||||
if isinstance(detail, dict):
|
||||
assert detail.get("detail") == "source_forbidden"
|
||||
assert detail.get("external_urls") == ["https://www.wikiloc.com/abc"]
|
||||
else:
|
||||
# Если FastAPI отдал просто строку — должно содержать source_forbidden
|
||||
assert "source_forbidden" in str(detail)
|
||||
# ADR-015 §G: одноуровневый контракт через JSONResponse в endpoint.py
|
||||
# (см. P2-01 в 12-review.md). Раньше FastAPI оборачивал detail-dict
|
||||
# в {"detail": {...}}; сейчас body == {"detail": "...", "external_urls": [...]}.
|
||||
assert body.get("detail") == "source_forbidden"
|
||||
assert body.get("external_urls") == ["https://www.wikiloc.com/abc"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
93
tests/web/test_track_download.py
Normal file
93
tests/web/test_track_download.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""ET-011 — pytest-обёртка для JS unit-тестов download-UI.
|
||||
|
||||
Контекст: test-plan §E2E-01..E2E-04 предусматривал Playwright-спеку
|
||||
`tests/web/test_track_download.spec.ts`, но в проекте нет настроенного
|
||||
Playwright-раннера. Reviewer ET-011 (12-review.md, P1-01) разрешил закрыть
|
||||
UI-сторону AC-1 / AC-2 / AC-7 поведенческими JS unit-тестами через
|
||||
`node --test`. AC-13 (mobile-bbox) оставлен как manual smoke
|
||||
(см. 04b-ui-test-cases.md TC-UI-02).
|
||||
|
||||
Этот файл — pytest-точка-входа, запускающая Node-раннер. Так JS-тесты
|
||||
исполняются в обычном `pytest tests/` без отдельных шагов в Makefile/CI.
|
||||
|
||||
Запуск JS-тестов напрямую:
|
||||
node --test tests/web/track_download.test.js
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from shutil import which
|
||||
|
||||
import pytest
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
GPS_TRACKS_JS = REPO_ROOT / "src" / "web" / "gps_tracks.js"
|
||||
JS_TEST = REPO_ROOT / "tests" / "web" / "track_download.test.js"
|
||||
|
||||
|
||||
def _read(path: Path) -> str:
|
||||
assert path.is_file(), f"не найден {path}"
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
# ─── Статические проверки: ET-011 артефакты на месте ─────────────────────────
|
||||
|
||||
|
||||
def test_download_helpers_defined_in_gps_tracks_js():
|
||||
"""ET-011: новые функции download-UI объявлены в gps_tracks.js."""
|
||||
js = _read(GPS_TRACKS_JS)
|
||||
for symbol in (
|
||||
"function _parseFilenameFromCD(",
|
||||
"function _handleDownloadError(",
|
||||
"async function _downloadPublicTrack(",
|
||||
):
|
||||
assert symbol in js, (
|
||||
f"ET-011: символ `{symbol}` не найден в src/web/gps_tracks.js"
|
||||
)
|
||||
|
||||
|
||||
def test_popup_renders_download_button_markup():
|
||||
"""AC-1: _renderTrackPopupHtml содержит маркап кнопки «Скачать GPX»."""
|
||||
js = _read(GPS_TRACKS_JS)
|
||||
# Существенные куски, по которым держится UI-контракт
|
||||
assert 'aria-label="Скачать GPX"' in js, (
|
||||
"AC-1: aria-label='Скачать GPX' отсутствует в gps_tracks.js"
|
||||
)
|
||||
assert "track-popup-download-btn" in js, (
|
||||
"AC-1: CSS-класс кнопки track-popup-download-btn отсутствует"
|
||||
)
|
||||
assert "data-track-id=" in js, (
|
||||
"ADR-014 §3.b: data-track-id для делегированного клика отсутствует"
|
||||
)
|
||||
|
||||
|
||||
def test_js_test_file_exists():
|
||||
"""JS-тест присутствует в репозитории — иначе субтесты ниже бессмыслены."""
|
||||
assert JS_TEST.is_file(), f"не найден JS-тест {JS_TEST}"
|
||||
|
||||
|
||||
# ─── Поведенческие JS unit-тесты через Node ──────────────────────────────────
|
||||
|
||||
|
||||
node_required = pytest.mark.skipif(
|
||||
which("node") is None,
|
||||
reason="node не установлен — поведенческие JS unit-тесты пропущены",
|
||||
)
|
||||
|
||||
|
||||
@node_required
|
||||
def test_js_track_download_unit_tests_pass():
|
||||
"""ET-011 P1-01: AC-1 / AC-2 / AC-7 (UI) — JS-тесты download-flow."""
|
||||
node = which("node")
|
||||
result = subprocess.run(
|
||||
[node, "--test", str(JS_TEST)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=str(REPO_ROOT),
|
||||
)
|
||||
assert result.returncode == 0, (
|
||||
f"JS unit-тесты track_download упали (код {result.returncode}):\n"
|
||||
f"STDOUT:\n{result.stdout}\n\nSTDERR:\n{result.stderr}"
|
||||
)
|
||||
359
tests/web/track_download.test.js
Normal file
359
tests/web/track_download.test.js
Normal file
@@ -0,0 +1,359 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* ET-011 — поведенческие JS unit-тесты UI для скачивания GPX из popup
|
||||
* публичного трека (src/web/gps_tracks.js).
|
||||
*
|
||||
* Контекст: test-plan §E2E-01..E2E-04 предусматривал Playwright-спеку
|
||||
* (`tests/web/test_track_download.spec.ts`), но в проекте нет настроенного
|
||||
* Playwright-раннера. Reviewer ET-011 (12-review.md, P1-01) явно разрешил
|
||||
* закрыть UI-сторону AC-1 / AC-2 / AC-7 этими JS unit-тестами, оставив
|
||||
* AC-13 (mobile-bbox) как manual smoke (см. 04b-ui-test-cases.md TC-UI-02).
|
||||
*
|
||||
* Покрываются:
|
||||
* - _parseFilenameFromCD — REQ-F-05.2, AC-2 (UI-чтение хедера)
|
||||
* - _handleDownloadError — REQ-F-05.4, AC-7 (toast по статусу)
|
||||
* - _renderTrackPopupHtml — REQ-F-01, AC-1 (кнопка в popup,
|
||||
* aria-label, тапабельный data-track-id)
|
||||
*
|
||||
* Запуск: node --test tests/web/track_download.test.js
|
||||
* В CI оборачивается pytest-тестом tests/web/test_track_download.py.
|
||||
*/
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const GPS_TRACKS_PATH = path.join(__dirname, '..', '..', 'src', 'web', 'gps_tracks.js');
|
||||
|
||||
// ─── Загрузчик модуля ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Загружает gps_tracks.js в изолированный контекст new Function, подставляя
|
||||
* мок-объекты вместо браузерных глобалов (`window`, `document`, `showToast`).
|
||||
*
|
||||
* Возвращает приватные функции, требуемые в тестах:
|
||||
* _parseFilenameFromCD, _handleDownloadError, _renderTrackPopupHtml.
|
||||
*
|
||||
* @param {object} [opts]
|
||||
* @param {object} [opts.win] мок window
|
||||
* @param {object} [opts.doc] мок document
|
||||
* @param {Function|null} [opts.showToast] мок showToast (null → отсутствует)
|
||||
* @returns {{
|
||||
* _parseFilenameFromCD: Function,
|
||||
* _handleDownloadError: Function,
|
||||
* _renderTrackPopupHtml: Function,
|
||||
* }}
|
||||
*/
|
||||
function loadDownloadModule(opts) {
|
||||
const o = opts || {};
|
||||
const win = o.win || {};
|
||||
win.localStorage = win.localStorage || {
|
||||
getItem: () => null,
|
||||
setItem: () => {},
|
||||
};
|
||||
const doc = o.doc || {
|
||||
getElementById: () => null,
|
||||
querySelectorAll: () => ({ forEach: () => {} }),
|
||||
};
|
||||
// showToast === undefined → typeof === 'undefined' → ранний return в
|
||||
// _handleDownloadError (defensive). null → typeof === 'object' → тоже return.
|
||||
const showToast = Object.prototype.hasOwnProperty.call(o, 'showToast')
|
||||
? o.showToast
|
||||
: undefined;
|
||||
|
||||
const src = fs.readFileSync(GPS_TRACKS_PATH, 'utf8');
|
||||
const factory = new Function(
|
||||
'window', 'document', 'showToast',
|
||||
src +
|
||||
'\nreturn {' +
|
||||
' _parseFilenameFromCD,' +
|
||||
' _handleDownloadError,' +
|
||||
' _renderTrackPopupHtml,' +
|
||||
'};'
|
||||
);
|
||||
return factory(win, doc, showToast);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// _parseFilenameFromCD — RFC 5987 + plain filename
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
test('CD: null → null', () => {
|
||||
const { _parseFilenameFromCD } = loadDownloadModule();
|
||||
assert.equal(_parseFilenameFromCD(null), null);
|
||||
});
|
||||
|
||||
test('CD: undefined → null', () => {
|
||||
const { _parseFilenameFromCD } = loadDownloadModule();
|
||||
assert.equal(_parseFilenameFromCD(undefined), null);
|
||||
});
|
||||
|
||||
test('CD: пустая строка → null', () => {
|
||||
const { _parseFilenameFromCD } = loadDownloadModule();
|
||||
assert.equal(_parseFilenameFromCD(''), null);
|
||||
});
|
||||
|
||||
test('CD: без параметров filename → null', () => {
|
||||
const { _parseFilenameFromCD } = loadDownloadModule();
|
||||
assert.equal(_parseFilenameFromCD('attachment'), null);
|
||||
});
|
||||
|
||||
test('CD: plain filename="track.gpx" → "track.gpx"', () => {
|
||||
const { _parseFilenameFromCD } = loadDownloadModule();
|
||||
assert.equal(
|
||||
_parseFilenameFromCD('attachment; filename="track.gpx"'),
|
||||
'track.gpx',
|
||||
);
|
||||
});
|
||||
|
||||
test('CD: plain filename без кавычек → значение до ; ', () => {
|
||||
const { _parseFilenameFromCD } = loadDownloadModule();
|
||||
assert.equal(
|
||||
_parseFilenameFromCD('attachment; filename=track.gpx'),
|
||||
'track.gpx',
|
||||
);
|
||||
});
|
||||
|
||||
test('CD: filename*=UTF-8\'\'<percent> приоритетнее plain filename (RFC 5987)', () => {
|
||||
const { _parseFilenameFromCD } = loadDownloadModule();
|
||||
// backend всегда отдаёт оба параметра; для не-ASCII имени берётся star
|
||||
const cd = 'attachment; filename="track-1.gpx"; '
|
||||
+ "filename*=UTF-8''%D0%9F%D0%BE%20%D0%B3%D1%80%D1%8F%D0%B7%D0%B8.gpx";
|
||||
assert.equal(_parseFilenameFromCD(cd), 'По грязи.gpx');
|
||||
});
|
||||
|
||||
test('CD: filename* с битым percent-encoding → fallback на plain filename', () => {
|
||||
const { _parseFilenameFromCD } = loadDownloadModule();
|
||||
// %ZZ — невалидный percent (decodeURIComponent кинет)
|
||||
const cd = 'attachment; filename="track-1.gpx"; '
|
||||
+ "filename*=UTF-8''%ZZbroken.gpx";
|
||||
assert.equal(_parseFilenameFromCD(cd), 'track-1.gpx');
|
||||
});
|
||||
|
||||
test('CD: filename* без последующего ; (конец строки) — декодируется до конца', () => {
|
||||
const { _parseFilenameFromCD } = loadDownloadModule();
|
||||
// ADR-014 §F: backend кладёт filename* последним параметром
|
||||
const cd = "attachment; filename=\"a.gpx\"; filename*=UTF-8''%D0%90.gpx";
|
||||
assert.equal(_parseFilenameFromCD(cd), 'А.gpx');
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// _handleDownloadError — REQ-F-05.4, AC-7
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/** Создаёт мок showToast, копящий последние вызовы. */
|
||||
function makeToastSpy() {
|
||||
const calls = [];
|
||||
const fn = (msg) => { calls.push(msg); };
|
||||
return { fn, calls };
|
||||
}
|
||||
|
||||
test('Error: 404 → toast «Трек не найден.»', () => {
|
||||
const spy = makeToastSpy();
|
||||
const { _handleDownloadError } = loadDownloadModule({ showToast: spy.fn });
|
||||
_handleDownloadError(404, {});
|
||||
assert.equal(spy.calls.length, 1);
|
||||
assert.equal(spy.calls[0], 'Трек не найден.');
|
||||
});
|
||||
|
||||
test('Error: 413 → toast про размер', () => {
|
||||
const spy = makeToastSpy();
|
||||
const { _handleDownloadError } = loadDownloadModule({ showToast: spy.fn });
|
||||
_handleDownloadError(413, {});
|
||||
assert.equal(spy.calls.length, 1);
|
||||
assert.match(spy.calls[0], /слишком большой/i);
|
||||
});
|
||||
|
||||
test('Error: 400 → toast про формат', () => {
|
||||
const spy = makeToastSpy();
|
||||
const { _handleDownloadError } = loadDownloadModule({ showToast: spy.fn });
|
||||
_handleDownloadError(400, {});
|
||||
assert.equal(spy.calls.length, 1);
|
||||
assert.match(spy.calls[0], /формат/i);
|
||||
});
|
||||
|
||||
test('Error: 500 / unknown → дефолтный toast «Не удалось скачать.»', () => {
|
||||
const spy = makeToastSpy();
|
||||
const { _handleDownloadError } = loadDownloadModule({ showToast: spy.fn });
|
||||
_handleDownloadError(500, {});
|
||||
assert.equal(spy.calls.length, 1);
|
||||
assert.match(spy.calls[0], /Не удалось скачать/);
|
||||
});
|
||||
|
||||
test('Error: 403 с external_urls (ADR-015 §G flat-форма) → toast с URL', () => {
|
||||
// ADR-015 §G: backend → JSONResponse{detail, external_urls} (без вложенности).
|
||||
const spy = makeToastSpy();
|
||||
const { _handleDownloadError } = loadDownloadModule({ showToast: spy.fn });
|
||||
_handleDownloadError(403, {
|
||||
detail: 'source_forbidden',
|
||||
external_urls: ['https://www.wikiloc.com/abc'],
|
||||
});
|
||||
assert.equal(spy.calls.length, 1);
|
||||
assert.match(spy.calls[0], /Источник запрещает/);
|
||||
assert.ok(
|
||||
spy.calls[0].includes('https://www.wikiloc.com/abc'),
|
||||
`toast должен содержать external_url, было: ${spy.calls[0]}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('Error: 403 с body.detail.external_urls (legacy wrapped-форма) → URL читается', () => {
|
||||
// Defensive fallback на старую форму до P2-01 (когда HTTPException
|
||||
// оборачивал detail в {"detail": {...}}). Тест защищает frontend от
|
||||
// регресса, если кто-то восстановит HTTPException-вариант.
|
||||
const spy = makeToastSpy();
|
||||
const { _handleDownloadError } = loadDownloadModule({ showToast: spy.fn });
|
||||
_handleDownloadError(403, {
|
||||
detail: {
|
||||
detail: 'source_forbidden',
|
||||
external_urls: ['https://wikiloc.com/x'],
|
||||
},
|
||||
});
|
||||
assert.equal(spy.calls.length, 1);
|
||||
assert.ok(
|
||||
spy.calls[0].includes('https://wikiloc.com/x'),
|
||||
`toast должен содержать external_url из legacy формы, было: ${spy.calls[0]}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('Error: 403 без external_urls → toast без URL', () => {
|
||||
const spy = makeToastSpy();
|
||||
const { _handleDownloadError } = loadDownloadModule({ showToast: spy.fn });
|
||||
_handleDownloadError(403, { detail: 'source_forbidden' });
|
||||
assert.equal(spy.calls.length, 1);
|
||||
assert.match(spy.calls[0], /Источник запрещает/);
|
||||
// в сообщении не должно быть http-URL
|
||||
assert.ok(
|
||||
!/https?:\/\//.test(spy.calls[0]),
|
||||
`toast не должен содержать URL когда external_urls пуст, было: ${spy.calls[0]}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('Error: 403 с external_urls = [] → toast без URL', () => {
|
||||
const spy = makeToastSpy();
|
||||
const { _handleDownloadError } = loadDownloadModule({ showToast: spy.fn });
|
||||
_handleDownloadError(403, { detail: 'source_forbidden', external_urls: [] });
|
||||
assert.equal(spy.calls.length, 1);
|
||||
assert.ok(!/https?:\/\//.test(spy.calls[0]));
|
||||
});
|
||||
|
||||
test('Error: showToast отсутствует → не падаем (defensive)', () => {
|
||||
// showToast не передан → typeof === 'undefined' → ранний return
|
||||
const { _handleDownloadError } = loadDownloadModule();
|
||||
assert.doesNotThrow(() => _handleDownloadError(404, {}));
|
||||
assert.doesNotThrow(() => _handleDownloadError(403, { external_urls: ['x'] }));
|
||||
assert.doesNotThrow(() => _handleDownloadError(500, {}));
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// _renderTrackPopupHtml — REQ-F-01, AC-1
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
test('Popup: при валидном числовом id рендерится кнопка «Скачать GPX»', () => {
|
||||
const { _renderTrackPopupHtml } = loadDownloadModule();
|
||||
const html = _renderTrackPopupHtml({
|
||||
id: 42,
|
||||
name: 'Test Trail',
|
||||
activity_type: 'enduro',
|
||||
length_km: 5.3,
|
||||
points_count: 100,
|
||||
});
|
||||
// AC-1: aria-label «Скачать GPX» обязателен (REQ-F-01)
|
||||
assert.match(html, /aria-label="Скачать GPX"/);
|
||||
// структура / классы для CSS (.track-popup-download-btn — ADR-014 §3.a)
|
||||
assert.match(html, /class="track-popup-download-btn"/);
|
||||
assert.match(html, /<div class="track-popup-actions">/);
|
||||
// data-track-id — для делегированного обработчика (ADR-014 §3.b)
|
||||
assert.match(html, /data-track-id="42"/);
|
||||
// SVG download-иконка
|
||||
assert.match(html, /<svg [^>]*viewBox="0 0 24 24"/);
|
||||
});
|
||||
|
||||
test('Popup: id в виде строки "7" тоже даёт кнопку (Number() приводит)', () => {
|
||||
const { _renderTrackPopupHtml } = loadDownloadModule();
|
||||
const html = _renderTrackPopupHtml({
|
||||
id: '7',
|
||||
name: 'Test',
|
||||
});
|
||||
assert.match(html, /data-track-id="7"/);
|
||||
assert.match(html, /aria-label="Скачать GPX"/);
|
||||
});
|
||||
|
||||
test('Popup: id = 0 → кнопка НЕ рендерится (Path int ge=1 на бэке)', () => {
|
||||
// backend требует ge=1; защищаем frontend от запроса /download/0
|
||||
const { _renderTrackPopupHtml } = loadDownloadModule();
|
||||
const html = _renderTrackPopupHtml({
|
||||
id: 0,
|
||||
name: 'Test',
|
||||
});
|
||||
assert.doesNotMatch(html, /track-popup-download-btn/);
|
||||
assert.doesNotMatch(html, /Скачать GPX/);
|
||||
});
|
||||
|
||||
test('Popup: id = null → кнопка НЕ рендерится', () => {
|
||||
const { _renderTrackPopupHtml } = loadDownloadModule();
|
||||
const html = _renderTrackPopupHtml({ id: null, name: 'Test' });
|
||||
assert.doesNotMatch(html, /track-popup-download-btn/);
|
||||
});
|
||||
|
||||
test('Popup: id отсутствует → кнопка НЕ рендерится', () => {
|
||||
const { _renderTrackPopupHtml } = loadDownloadModule();
|
||||
const html = _renderTrackPopupHtml({ name: 'Test' });
|
||||
assert.doesNotMatch(html, /track-popup-download-btn/);
|
||||
});
|
||||
|
||||
test('Popup: id = "abc" (мусор) → кнопка НЕ рендерится', () => {
|
||||
const { _renderTrackPopupHtml } = loadDownloadModule();
|
||||
const html = _renderTrackPopupHtml({ id: 'abc', name: 'Test' });
|
||||
assert.doesNotMatch(html, /track-popup-download-btn/);
|
||||
});
|
||||
|
||||
test('Popup: id = -1 → кнопка НЕ рендерится (защита от patho-кейсов)', () => {
|
||||
const { _renderTrackPopupHtml } = loadDownloadModule();
|
||||
const html = _renderTrackPopupHtml({ id: -1, name: 'Test' });
|
||||
assert.doesNotMatch(html, /track-popup-download-btn/);
|
||||
});
|
||||
|
||||
test('Popup-регрессия: остаются прежние поля (имя, активность, длина)', () => {
|
||||
const { _renderTrackPopupHtml } = loadDownloadModule();
|
||||
const html = _renderTrackPopupHtml({
|
||||
id: 1,
|
||||
name: 'Озеро',
|
||||
activity_type: 'enduro',
|
||||
length_km: 12.5,
|
||||
points_count: 250,
|
||||
user: 'tester',
|
||||
created_at: '2024-05-01T00:00:00Z',
|
||||
});
|
||||
assert.match(html, /<div class="track-popup-name">Озеро<\/div>/);
|
||||
assert.match(html, /Эндуро/); // GPS_ACTIVITY_LABELS.enduro
|
||||
assert.match(html, /12\.5 км/);
|
||||
assert.match(html, /250 точек/);
|
||||
assert.match(html, /tester/);
|
||||
});
|
||||
|
||||
test('Popup: actionsHtml идёт ПЕРЕД sourcesHtml (ADR-014 §3.a)', () => {
|
||||
const { _renderTrackPopupHtml } = loadDownloadModule();
|
||||
const html = _renderTrackPopupHtml({
|
||||
id: 9,
|
||||
name: 'X',
|
||||
sources: ['osm'],
|
||||
external_urls: ['https://www.openstreetmap.org/way/9'],
|
||||
});
|
||||
const idxActions = html.indexOf('track-popup-actions');
|
||||
const idxSources = html.indexOf('track-popup-sources');
|
||||
assert.notEqual(idxActions, -1, 'actionsHtml присутствует');
|
||||
assert.notEqual(idxSources, -1, 'sourcesHtml присутствует');
|
||||
assert.ok(
|
||||
idxActions < idxSources,
|
||||
'actionsHtml (кнопка) должен идти раньше sourcesHtml',
|
||||
);
|
||||
});
|
||||
|
||||
test('Popup: без источников всё равно рендерится кнопка (если id ок)', () => {
|
||||
const { _renderTrackPopupHtml } = loadDownloadModule();
|
||||
const html = _renderTrackPopupHtml({ id: 3, name: 'NoSources' });
|
||||
assert.match(html, /track-popup-download-btn/);
|
||||
assert.doesNotMatch(html, /track-popup-sources/);
|
||||
});
|
||||
Reference in New Issue
Block a user