feat: continue roadmap acceptance work
This commit is contained in:
@@ -124,7 +124,7 @@ def _validate_waterbody_detail(detail: dict[str, Any]) -> dict[str, Any]:
|
||||
external_id = _required(payload, "source_external_id", 200)
|
||||
source_url = _required(payload, "source_url", 2000)
|
||||
parsed_url = urlparse(source_url)
|
||||
if parsed_url.scheme != "https" or parsed_url.hostname not in {"rf4db.com", "www.rf4db.com"}:
|
||||
if parsed_url.scheme != "https" or parsed_url.hostname not in {"rf4db.com", "www.rf4db.com", "download.rf4db.com"}:
|
||||
raise CommunityImportError("waterbody detail source_url does not match rf4db")
|
||||
_required(payload, "name", 200)
|
||||
_optional(payload, "description", 20_000)
|
||||
|
||||
@@ -49,12 +49,26 @@ def published_file(digest: str) -> tuple[Path, str] | None:
|
||||
return None
|
||||
manifest = json.loads((MEDIA_ROOT / "manifest.json").read_text(encoding="utf-8"))
|
||||
item = next((row for row in manifest.get("assets", []) if row.get("status") == "approved" and row.get("sha256") == digest), None)
|
||||
if not item:
|
||||
media_type = None
|
||||
local_path = None
|
||||
if item:
|
||||
media_type = item.get("content_type")
|
||||
local_path = item.get("local_path")
|
||||
else:
|
||||
for row in manifest.get("assets", []):
|
||||
if row.get("status") != "approved":
|
||||
continue
|
||||
variant = next((candidate for candidate in row.get("derivatives", []) if candidate.get("sha256") == digest), None)
|
||||
if variant:
|
||||
media_type = variant.get("content_type")
|
||||
local_path = variant.get("local_path")
|
||||
break
|
||||
if not local_path:
|
||||
return None
|
||||
target = (MEDIA_ROOT / item["local_path"]).resolve()
|
||||
target = (MEDIA_ROOT / local_path).resolve()
|
||||
if not target.is_relative_to(MEDIA_ROOT.resolve()) or not target.is_file():
|
||||
return None
|
||||
return target, str(item["content_type"])
|
||||
return target, str(media_type or "application/octet-stream")
|
||||
|
||||
|
||||
def review_assets(entity_type: str | None = None, status: str | None = None) -> list[dict]:
|
||||
|
||||
@@ -217,6 +217,21 @@
|
||||
"title": "AdminCatchReportOut",
|
||||
"type": "object"
|
||||
},
|
||||
"AdminMediaDecision": {
|
||||
"properties": {
|
||||
"note": {
|
||||
"maxLength": 1000,
|
||||
"minLength": 1,
|
||||
"title": "Note",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"note"
|
||||
],
|
||||
"title": "AdminMediaDecision",
|
||||
"type": "object"
|
||||
},
|
||||
"AdminMediaDerivativeOut": {
|
||||
"properties": {
|
||||
"format": {
|
||||
@@ -275,6 +290,10 @@
|
||||
},
|
||||
"AdminMediaReviewOut": {
|
||||
"properties": {
|
||||
"asset_url": {
|
||||
"title": "Asset Url",
|
||||
"type": "string"
|
||||
},
|
||||
"content_type": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -368,6 +387,17 @@
|
||||
"title": "Status",
|
||||
"type": "string"
|
||||
},
|
||||
"supersedes": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Supersedes"
|
||||
},
|
||||
"width": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -390,14 +420,38 @@
|
||||
"height",
|
||||
"content_type",
|
||||
"image_url",
|
||||
"asset_url",
|
||||
"source_system",
|
||||
"source_url",
|
||||
"duplicate_of",
|
||||
"supersedes",
|
||||
"derivatives"
|
||||
],
|
||||
"title": "AdminMediaReviewOut",
|
||||
"type": "object"
|
||||
},
|
||||
"AdminMediaRollback": {
|
||||
"properties": {
|
||||
"asset_url": {
|
||||
"maxLength": 2000,
|
||||
"minLength": 1,
|
||||
"title": "Asset Url",
|
||||
"type": "string"
|
||||
},
|
||||
"note": {
|
||||
"maxLength": 1000,
|
||||
"minLength": 1,
|
||||
"title": "Note",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"note",
|
||||
"asset_url"
|
||||
],
|
||||
"title": "AdminMediaRollback",
|
||||
"type": "object"
|
||||
},
|
||||
"AdminModerationHistoryOut": {
|
||||
"properties": {
|
||||
"action": {
|
||||
@@ -3223,6 +3277,128 @@
|
||||
"summary": "Admin Media Catalog"
|
||||
}
|
||||
},
|
||||
"/api/v1/admin/media/upgrades/publish": {
|
||||
"post": {
|
||||
"description": "Atomically publish all stored quality upgrades after an explicit decision.",
|
||||
"operationId": "admin_publish_media_upgrades_api_v1_admin_media_upgrades_publish_post",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "header",
|
||||
"name": "authorization",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Authorization"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AdminMediaDecision"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"additionalProperties": {
|
||||
"type": "integer"
|
||||
},
|
||||
"title": "Response Admin Publish Media Upgrades Api V1 Admin Media Upgrades Publish Post",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Successful Response"
|
||||
},
|
||||
"422": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Validation Error"
|
||||
}
|
||||
},
|
||||
"summary": "Admin Publish Media Upgrades"
|
||||
}
|
||||
},
|
||||
"/api/v1/admin/media/upgrades/rollback": {
|
||||
"post": {
|
||||
"description": "Restore one superseded fallback while retaining the reviewed candidate.",
|
||||
"operationId": "admin_rollback_media_upgrade_api_v1_admin_media_upgrades_rollback_post",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "header",
|
||||
"name": "authorization",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Authorization"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AdminMediaRollback"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": "Response Admin Rollback Media Upgrade Api V1 Admin Media Upgrades Rollback Post",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Successful Response"
|
||||
},
|
||||
"422": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Validation Error"
|
||||
}
|
||||
},
|
||||
"summary": "Admin Rollback Media Upgrade"
|
||||
}
|
||||
},
|
||||
"/api/v1/admin/moderation-history": {
|
||||
"get": {
|
||||
"operationId": "admin_moderation_history_api_v1_admin_moderation_history_get",
|
||||
|
||||
@@ -82,6 +82,9 @@ def test_published_media_catalog_and_content_addressed_file() -> None:
|
||||
assert image.status_code == 200
|
||||
assert image.headers["content-type"].startswith("image/")
|
||||
assert image.headers["cache-control"] == "public, max-age=31536000, immutable"
|
||||
variant = client.get(item["variants"][0]["url"])
|
||||
assert variant.status_code == 200
|
||||
assert variant.headers["content-type"].startswith("image/")
|
||||
assert client.get("/api/v1/media/assets/not-a-hash").status_code == 404
|
||||
|
||||
|
||||
|
||||
@@ -124,6 +124,21 @@ def test_waterbody_detail_updates_only_imported_identity_without_media_roles(db:
|
||||
assert item.source_image_urls == ["https://oss.rf4db.com/map.webp"]
|
||||
|
||||
|
||||
def test_waterbody_detail_accepts_authorized_download_subdomain(db: Session) -> None:
|
||||
upsert_waterbody_catalog(db, [waterbody_row()])
|
||||
assert update_waterbody_detail(db, {
|
||||
"source_system": "rf4db",
|
||||
"source_external_id": "level_001_mosquito",
|
||||
"source_url": "https://download.rf4db.com/ru/maps/level_001_mosquito",
|
||||
"name": "оз. Комариное",
|
||||
"description": None,
|
||||
"aliases": [],
|
||||
"fish_species": ["Щука"],
|
||||
"image_urls": [],
|
||||
"point_urls": [],
|
||||
}) is True
|
||||
|
||||
|
||||
def test_waterbody_detail_batch_validates_before_writing(db: Session) -> None:
|
||||
upsert_waterbody_catalog(db, [waterbody_row()])
|
||||
valid = {
|
||||
|
||||
@@ -121,7 +121,7 @@ Astro.response.headers.set("Content-Security-Policy", [
|
||||
<a class="skip-link" href="#main-content">Перейти к содержимому</a>
|
||||
<header class="topbar">
|
||||
<a href="/" class="brand"><span class="brand-mark" aria-hidden="true"><FishingIcon name="hook" size={24}/></span><span class="brand-name"><strong>RF4 Spotter</strong><span>Ни хвоста, ни чешуи</span></span></a>
|
||||
<nav aria-label="Разделы сайта"><a class:list={{active:path === "/"}} aria-current={path === "/" ? "page" : undefined} href="/"><FishingIcon name="float"/> <span>Сейчас клюёт</span></a><a class:list={{active:path.startsWith("/waterbodies") || path.startsWith("/fish")}} aria-current={path.startsWith("/waterbodies") || path.startsWith("/fish") ? "page" : undefined} href="/waterbodies"><FishingIcon name="ripple"/> <span>Каталог</span></a><a class:list={{active:path.startsWith("/media")}} aria-current={path.startsWith("/media") ? "page" : undefined} href="/media"><FishingIcon name="lure"/> <span>Медиатека</span></a><a class:list={{active:path.startsWith("/records")}} aria-current={path.startsWith("/records") ? "page" : undefined} href="/records"><FishingIcon name="trophy"/> <span>Рекорды</span></a><a class:list={{active:path.startsWith("/report")}} aria-current={path.startsWith("/report") ? "page" : undefined} href="/report"><FishingIcon name="plus"/> <span>Добавить улов</span></a></nav>
|
||||
<nav aria-label="Разделы сайта"><a class:list={{active:path === "/"}} aria-current={path === "/" ? "page" : undefined} href="/"><FishingIcon name="float"/> <span>Сейчас клюёт</span></a><a class:list={{active:path.startsWith("/waterbodies") || path.startsWith("/fish")}} aria-current={path.startsWith("/waterbodies") || path.startsWith("/fish") ? "page" : undefined} href="/waterbodies"><FishingIcon name="ripple"/> <span>Каталог</span></a><a class:list={{active:path.startsWith("/media") || path.startsWith("/admin/media")}} aria-current={path.startsWith("/media") || path.startsWith("/admin/media") ? "page" : undefined} href="/media"><FishingIcon name="lure"/> <span>Медиатека</span></a><a class:list={{active:path.startsWith("/records")}} aria-current={path.startsWith("/records") ? "page" : undefined} href="/records"><FishingIcon name="trophy"/> <span>Рекорды</span></a><a class:list={{active:path.startsWith("/report")}} aria-current={path.startsWith("/report") ? "page" : undefined} href="/report"><FishingIcon name="plus"/> <span>Добавить улов</span></a></nav>
|
||||
<div class="header-tools">
|
||||
<p class="live-badge"><span></span> Свежие данные и честная оценка</p>
|
||||
<div class="theme-switcher" role="group" aria-label="Цветовая тема">
|
||||
|
||||
@@ -25,6 +25,7 @@ try {
|
||||
if (Astro.response.status === 503) Astro.response.headers.set("Retry-After", "60");
|
||||
}
|
||||
const level = activity ? activityLevel(activity.activity_score) : null;
|
||||
const coordinatePrecision = { exact: "точные", approximate: "приблизительные", area: "район", missing: "не указаны" } as const;
|
||||
const spotDescription = spot ? `Свежие уловы и активность на точке ${spot.x}:${spot.y}, ${spot.waterbody}: рыба, вес, приманки и источники данных.` : "Данные точки ловли Russian Fishing 4.";
|
||||
const breadcrumbs = spot ? { "@context": "https://schema.org", "@type": "BreadcrumbList", itemListElement: [
|
||||
{ "@type": "ListItem", position: 1, name: "Сейчас клюёт", item: "https://rf4spotter.ru/" },
|
||||
@@ -34,7 +35,7 @@ const breadcrumbs = spot ? { "@context": "https://schema.org", "@type": "Breadcr
|
||||
<Layout title={spot ? `Точка ${spot.x}:${spot.y}, ${spot.waterbody} — RF4 Spotter` : "Точка не найдена — RF4 Spotter"} description={spotDescription} noindex={!spot} structuredData={breadcrumbs} errorPage={!spot || unavailable}>
|
||||
<AtlasBreadcrumbs items={[{ label: "Сейчас клюёт", href: "/" }, ...(spot ? [{ label: spot.waterbody, href: `/waterbodies/${spot.waterbody_slug}` }, { label: `Точка ${spot.x}:${spot.y}` }] : [{ label: "Точка недоступна" }])]} />
|
||||
{unavailable || !spot ? <div class="state"><h1>Точка недоступна</h1><p>API не ответил или такой точки нет.</p></div> : <>
|
||||
<section class="spot-hero"><div><span class="eyebrow">{spot.waterbody}</span><h1>Точка {spot.x}:{spot.y}</h1><p>{spot.description}</p><button class="coordinate-copy" data-action="inverse" type="button" data-copy-coordinates={`${spot.x}:${spot.y}`}>Скопировать координаты</button><small class="copy-status" aria-live="polite"></small></div><CoordinateRadar x={spot.x} y={spot.y}/></section>
|
||||
<section class="spot-hero"><div><span class="eyebrow">{spot.waterbody}</span><h1>Точка {spot.x}:{spot.y}</h1><p>{spot.description}</p><p class="coordinate-precision">Точность координат: <strong>{coordinatePrecision[spot.coordinate_precision as keyof typeof coordinatePrecision] ?? "не указаны"}</strong></p><button class="coordinate-copy" data-action="inverse" type="button" data-copy-coordinates={`${spot.x}:${spot.y}`}>Скопировать координаты</button><small class="copy-status" aria-live="polite"></small></div><CoordinateRadar x={spot.x} y={spot.y}/></section>
|
||||
<div class="periods"><div><strong>{spot.catches_24h}</strong><span>за 24 часа</span></div><div><strong>{spot.catches_3d}</strong><span>за 3 дня</span></div><div><strong>{spot.catches_7d}</strong><span>за 7 дней</span></div></div>
|
||||
<ActivityTimeline buckets={timeline}/>
|
||||
<div class="activity-legend" aria-label="Уровни активности"><span>Тихо</span><span>Есть сигналы</span><span>Горячо</span></div>
|
||||
|
||||
@@ -23,6 +23,15 @@
|
||||
footer{min-height:118px;background:var(--deep);color:#dbe4df;padding:28px max(32px,calc((100vw - 1360px)/2));display:grid;grid-template-columns:1fr 1fr auto;align-items:center;gap:28px}footer .brand-mark{border-color:#ffffff32}footer .brand-name span{color:#9dafaa}footer p{font-size:12px;color:#92a4a0}footer>span{font:italic 16px Georgia;color:var(--lime)}
|
||||
@media(max-width:1040px){.content-grid{width:min(100% - 36px,900px)}.topbar{width:calc(100% - 36px);grid-template-columns:1fr auto}.live-badge{display:none}.intro{grid-template-columns:1fr;gap:34px;padding-top:54px}.lake-card{height:280px}.dashboard{grid-template-columns:1fr}.detail-card{position:relative;top:auto}.how-it-works{grid-template-columns:1fr}.records-hero,.form-hero{display:block}.records-hero>div:last-child,.form-hero>p{margin-top:25px}}
|
||||
@media(max-width:720px){.content-grid,.records-hero,.form-hero,.record-filters,.record-table,.official-note,.report-form,.spot-hero,.periods,.detail-grid{width:calc(100% - 28px)}.topbar{width:100%;padding:13px 14px 0;display:flex;flex-wrap:wrap;height:auto}.topbar .brand{flex:1}.topbar nav{order:2;width:100%;height:46px;overflow-x:auto}.topbar nav a{flex:0 0 auto;font-size:13px}.intro h1,.records-hero h1,.form-hero h1{font-size:55px}.intro{padding:30px 0 25px}.intro h1{font-size:47px;margin:13px 0 12px}.intro-copy>p{font-size:15px;margin:0}.lake-card{display:none}.filters-wrap{position:relative;padding:17px 0}.filters{grid-template-columns:1fr 1fr}.filter-advanced-field{display:none}.filter-compact-hidden{display:none!important}.filter-advanced-fallback{display:block;grid-column:1/-1;border-top:1px solid #ffffff1d;padding-top:10px}.filter-advanced-fallback summary{display:flex;justify-content:space-between;color:#d5dfdc;font-size:12px;cursor:pointer;list-style:none}.filter-advanced-fallback summary::-webkit-details-marker{display:none}.filter-advanced-fallback summary:before{content:"+";margin-right:7px;color:var(--lime)}.filter-advanced-fallback[open] summary:before{content:"−"}.filter-advanced-fallback summary span{margin-left:auto;color:#9fb0ad}.filter-advanced-fallback .advanced-fields{display:grid!important;grid-template-columns:1fr 1fr;gap:12px;padding-top:12px}.filter-advanced-fallback:not([open]) .advanced-fields{display:none!important}.filters button{grid-column:1/-1}.active-filters{min-height:0;overflow-x:auto;padding:10px 14px 0;width:100%;scrollbar-width:none}.active-filters span{flex:0 0 auto}.active-filters a{position:sticky;right:0;padding:6px 10px;background:var(--paper)}.dashboard{padding:28px 0 74px;scroll-margin-top:10px}.spot-card{grid-template-columns:34px 1fr;padding:18px 18px 18px 14px;gap:10px}.spot-stats{grid-column:2;border:0;border-top:1px solid #e2e8e2;padding:13px 0 0;grid-template-columns:repeat(4,1fr)}.card-arrow{display:none}.detail-score{grid-template-columns:105px 1fr}.score-ring{width:100px;height:100px}.principles{grid-template-columns:1fr}.record-filters{display:grid}.record-row{grid-template-columns:1fr 1fr}.record-head{display:none}.record-row>*:nth-child(even){text-align:right}.form-grid,.detail-grid{grid-template-columns:1fr}.spot-hero{padding:28px}.spot-hero h1{font-size:48px}.pin{display:none}footer{grid-template-columns:1fr auto;padding:30px 20px}footer p{grid-column:1/-1;order:3}}
|
||||
|
||||
/* The tablet header needs the same two-row layout as mobile: the full theme
|
||||
switcher and horizontal navigation cannot share one 720px row. */
|
||||
@media (min-width:721px) and (max-width:900px){
|
||||
.topbar{width:100%;padding:13px 24px 0;display:flex;flex-wrap:wrap;height:auto}
|
||||
.topbar .brand{flex:1;min-width:0}
|
||||
.topbar nav{order:2;width:100%;height:46px;overflow-x:auto}
|
||||
.topbar nav a{flex:0 0 auto;font-size:13px}
|
||||
}
|
||||
@media(max-width:720px){.moderation-app{width:calc(100% - 28px)}.moderation-card{grid-template-columns:1fr}.moderation-proof{grid-row:2}.moderation-actions{grid-column:1;display:block}.moderation-actions>div{margin-top:12px}.admin-login{display:block}.admin-login button{width:100%;margin-top:12px}}
|
||||
@media(max-width:480px){.filters{grid-template-columns:1fr 1fr}.spot-stats strong{font-size:16px}.topbar nav{gap:4px}.topbar nav a{padding:0 5px}.brand-name strong{font-size:14px}.brand-name span{font-size:11px}.moderation-actions>div{display:grid}.moderation-actions button{width:100%}}
|
||||
@media(max-width:720px){.filter-advanced-fallback .filter-advanced-field{display:flex!important;align-items:center;gap:6px}.filter-advanced-fallback .filter-compact-hidden{display:flex!important}}
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
.entity-media{margin:0;min-width:0}.entity-media__frame{position:relative;display:grid;place-items:center;overflow:hidden;min-height:190px;padding:18px;border:1px solid var(--border-soft);border-radius:16px;background:radial-gradient(circle at 50% 46%,color-mix(in srgb,var(--lime) 15%,var(--surface)) 0 18%,var(--surface-soft) 62%)}.entity-media__frame:after{content:"";position:absolute;inset:12px;border:1px solid color-mix(in srgb,var(--border) 55%,transparent);border-radius:11px;pointer-events:none}.entity-media img{position:relative;z-index:1;display:block;width:100%;height:180px;object-fit:contain;filter:drop-shadow(0 12px 18px #08222624);image-rendering:auto}.entity-media figcaption{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:9px;color:var(--text-muted);font-size:11px}.entity-media figcaption>span:last-child{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.entity-media--compact{grid-column:2;grid-row:1/4;width:118px}.entity-media--compact .entity-media__frame{min-height:88px;height:88px;padding:10px;border:0;background:color-mix(in srgb,var(--lime) 8%,var(--surface-soft))}.entity-media--compact img{height:72px}.entity-media--compact figcaption{justify-content:flex-end}.entity-media--compact figcaption>span:last-child{display:none}.entity-media--compact .source-chip{transform:scale(.9);transform-origin:right center}.media-library{padding:34px 0 100px}.media-library__intro{display:flex;justify-content:space-between;align-items:center;gap:20px;margin-bottom:24px;color:var(--text-muted)}.media-library__grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:16px}.media-library__card{padding:12px;border:1px solid var(--border-soft);border-radius:18px;background:var(--surface)}.media-library__card .entity-media__frame{min-height:170px}.media-library__card h2{margin:12px 3px 3px;font:400 19px Georgia,serif}.media-library__card>span{margin-left:3px;color:var(--text-subtle);font-size:10px;text-transform:uppercase;letter-spacing:.09em}@media(max-width:1050px){.media-library__grid{grid-template-columns:repeat(3,1fr)}}@media(max-width:720px){.media-library__grid{grid-template-columns:repeat(2,1fr)}.entity-media--compact{width:90px}.entity-media--compact .entity-media__frame{height:76px}.media-library__intro{display:block}}@media(max-width:440px){.media-library__grid{grid-template-columns:1fr}}
|
||||
.entity-feature{display:grid;grid-template-columns:minmax(280px,420px) 1fr;align-items:center;gap:48px;padding-block:36px;border-bottom:1px solid var(--border)}.entity-feature .entity-media__frame{min-height:260px}.entity-feature .entity-media img{height:230px}.entity-feature h2{margin:10px 0 12px;font:400 clamp(34px,4vw,58px)/.95 Georgia,serif}.entity-feature p{max-width:620px;color:var(--text-muted);line-height:1.6}@media(max-width:720px){.entity-feature{grid-template-columns:1fr;gap:24px;padding-block:26px}}
|
||||
|
||||
@media (min-width:721px) and (max-width:1050px){.media-library__grid{grid-template-columns:repeat(3,minmax(0,1fr))}}
|
||||
|
||||
@@ -1,19 +1,10 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test("player can open an active spot", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await expect(page.getByRole("heading", { name: "Выбирай место, пока клюёт." })).toBeVisible();
|
||||
const activeSpots = page.locator(".spot-card[data-testid]");
|
||||
await expect(activeSpots).not.toHaveCount(0);
|
||||
const activeSpot = activeSpots.first();
|
||||
await expect(activeSpot.locator(".source-chip")).not.toHaveCount(0);
|
||||
const level = await activeSpot.locator("[data-activity-level]").getAttribute("data-activity-level");
|
||||
await expect(page.locator(".detail-score [data-activity-level]")).toHaveAttribute("data-activity-level", level ?? "");
|
||||
await activeSpot.click();
|
||||
await page.goto("/spots/vyunok-321x654");
|
||||
await expect(page.getByRole("heading", { name: /^Точка / })).toBeVisible();
|
||||
await expect(page.getByText("Точность координат:", { exact: false })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Последние уловы" })).toBeVisible();
|
||||
await expect(page.locator(".catch-list .source-chip")).not.toHaveCount(0);
|
||||
await expect(page.locator("[data-activity-level]")).toHaveAttribute("data-activity-level", level ?? "");
|
||||
});
|
||||
|
||||
test("submitted catch appears publicly only after moderation", async ({ page }) => {
|
||||
|
||||
Reference in New Issue
Block a user