import io import shutil import tempfile from django.core.files.base import ContentFile from django.core.files.uploadedfile import SimpleUploadedFile from django.test import Client, TestCase, override_settings from django.urls import reverse from PIL import Image from .forms import HalftoneGenerateForm from .models import TbHypn0Item, TbVote from .services.halftone import ( analyze_svg_structure, encode_to_base36, generate_halftone_svg, prepare_active_svg, prepare_gallery_svg, ) from .services.naming import generate_hypno_title, generate_title_openrouter class BaseMediaTestCase(TestCase): """Базовый тестовый класс с изоляцией MEDIA_ROOT и STORAGES во временной папке.""" @classmethod def setUpClass(cls): super().setUpClass() cls.temp_media = tempfile.mkdtemp() cls.override_media = override_settings( MEDIA_ROOT=cls.temp_media, STORAGES={ "default": { "BACKEND": "django.core.files.storage.FileSystemStorage", "OPTIONS": { "location": cls.temp_media, }, }, "staticfiles": { "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", }, }, ) cls.override_media.enable() @classmethod def tearDownClass(cls): cls.override_media.disable() shutil.rmtree(cls.temp_media, ignore_errors=True) super().tearDownClass() class HalftoneServiceTests(BaseMediaTestCase): """Тестирование сервиса генерации SVG (services/halftone.py).""" def setUp(self): # Создаем простое тестовое изображение 50x50 с градиентом self.img = Image.new("RGB", (50, 50), color="white") for x in range(25): for y in range(50): self.img.putpixel((x, y), (0, 0, 0)) def test_encode_to_base36(self): self.assertEqual(encode_to_base36(0), "a") self.assertEqual(encode_to_base36(1), "b") with self.assertRaises(ValueError): encode_to_base36(-1) def test_generate_default_svg(self): svg = generate_halftone_svg(self.img, cols=30, max_radius=6) self.assertIn("", svg) self.assertIn("", svg) self.assertIn('Generated by Hypn0", svg) def test_generate_shapes(self): shapes = ["circle", "ring", "square", "diamond", "triangle", "hexagon", "star", "cross", "line", "wave", "heart"] for shape in shapes: svg = generate_halftone_svg(self.img, cols=20, max_radius=5, shape=shape) self.assertIn("", svg) self.assertIn("", svg) def test_generate_static_blink_zero(self): svg = generate_halftone_svg(self.img, cols=20, blink=0) self.assertIn("animation:none", svg) self.assertNotIn("@keyframes noise", svg) def test_generate_animated_params(self): svg = generate_halftone_svg( self.img, cols=20, blink=8, rotation=12, scale=920, angle=15, color="#ff5500", ) self.assertIn("@keyframes noise", svg) self.assertIn("rotate(12deg)", svg) self.assertIn("scale(0.92)", svg) self.assertIn("#ff5500", svg) def test_generate_from_bytes(self): buf = io.BytesIO() self.img.save(buf, format="PNG") svg = generate_halftone_svg(buf.getvalue(), cols=20) self.assertIn(" width = 10 * 12 = 120, height = 30 * 12 = 360 self.assertIn('viewBox="0 0 120 360"', svg_tall) # Для горизонтального изображения (300x100) wide_img = Image.new("RGB", (300, 100), color="black") svg_wide = generate_halftone_svg(wide_img, cols=30, max_radius=5) self.assertIn('viewBox="0 0 360 120"', svg_wide) class HalftoneFormTests(BaseMediaTestCase): """Тестирование формы валидации HalftoneGenerateForm.""" def test_form_validation(self): buf = io.BytesIO() img = Image.new("RGB", (30, 30), color="red") img.save(buf, format="PNG") buf.seek(0) uploaded = SimpleUploadedFile("test.png", buf.read(), content_type="image/png") form = HalftoneGenerateForm( data={ "shape": "square", "cols": 60, "max_radius": 7, "blink": 5, "rotation": 10, "scale": 950, "angle": -10, }, files={"image": uploaded}, ) self.assertTrue(form.is_valid()) self.assertEqual(form.cleaned_data["shape"], "square") self.assertEqual(form.cleaned_data["cols"], 60) class HalftoneViewTests(BaseMediaTestCase): """Тестирование view-обработчиков (index, generate).""" def setUp(self): self.client = Client() def test_index_page(self): response = self.client.get(reverse("hypn0_site:index")) self.assertEqual(response.status_code, 200) self.assertContains(response, "logo-hypn0.svg") self.assertContains(response, 'id="preview"') self.assertContains(response, 'name="csrfmiddlewaretoken"') self.assertContains(response, 'X-CSRFToken') self.assertContains(response, "handleFileChange") self.assertContains(response, "imagePreviewUrl") self.assertContains(response, "Гипножаба облизывается на:") # Проверяем, что csrf_token не пустой в hx-headers content = response.content.decode("utf-8") self.assertNotIn('hx-headers=\'{"X-CSRFToken": ""}\'', content) def test_generate_with_enforce_csrf(self): csrf_client = Client(enforce_csrf_checks=True) response = csrf_client.get(reverse("hypn0_site:index")) self.assertEqual(response.status_code, 200) buf = io.BytesIO() img = Image.new("RGB", (40, 40), color="black") img.save(buf, format="PNG") buf.seek(0) uploaded = SimpleUploadedFile("test.png", buf.read(), content_type="image/png") # Без CSRF токена вернет 403 bad_response = csrf_client.post( reverse("hypn0_site:generate"), data={"shape": "circle", "image": uploaded}, ) self.assertEqual(bad_response.status_code, 403) # С CSRF токеном в POST data вернет 200 csrf_token = response.cookies["csrftoken"].value buf.seek(0) uploaded = SimpleUploadedFile("test.png", buf.read(), content_type="image/png") good_response = csrf_client.post( reverse("hypn0_site:generate"), data={ "csrfmiddlewaretoken": csrf_token, "shape": "circle", "image": uploaded, }, ) self.assertEqual(good_response.status_code, 200) self.assertContains(good_response, " 5) # Проверка детерминированности по seed self.assertEqual(generate_hypno_title(seed=42), title) def test_openrouter_stub(self): res = generate_title_openrouter(b"dummy_bytes") self.assertIsNone(res) class GalleryPreparationTests(BaseMediaTestCase): """Тестирование подготовки SVG к галерейному хранению.""" def test_prepare_gallery_svg(self): raw_svg = '' prepared = prepare_gallery_svg(raw_svg) self.assertIn("svg{--hypn0-play:paused}", prepared) self.assertIn(":host(:hover) svg,svg:hover{--hypn0-play:running!important}", prepared) self.assertIn("animation-play-state:var(--hypn0-play,paused)!important", prepared) class PublishViewTests(BaseMediaTestCase): """Тестирование эндпоинта публикации кандидата в галерею (/publish).""" def setUp(self): self.client = Client() self.sample_svg = '' def test_publish_without_cookie_returns_slug_protest(self): response = self.client.post( reverse("hypn0_site:publish"), data={ "svg_content": self.sample_svg, "shape": "circle", "cols": "35", }, ) self.assertEqual(response.status_code, 200) self.assertContains(response, "Мозговые слизняки протестуют!") self.assertContains(response, "Подчиниться и опубликовать") self.assertEqual(TbHypn0Item.objects.count(), 0) def test_publish_with_cookie_success(self): vid = "123e4567-e89b-12d3-a456-426614174000" self.client.cookies["hypn0_vid"] = vid response = self.client.post( reverse("hypn0_site:publish"), data={ "svg_content": self.sample_svg, "shape": "diamond", "cols": "40", "max_radius": "6", "blink": "7", "rotation": "5", "scale": "950", "angle": "10", "color": "#10b981", }, ) self.assertEqual(response.status_code, 200) self.assertContains(response, "Уже в галерее транса") self.assertContains(response, "идет прорастание психо-паутины...") # Проверяем запись в БД self.assertEqual(TbHypn0Item.objects.count(), 1) item = TbHypn0Item.objects.first() self.assertTrue(len(item.s_hash_id) >= 6) self.assertIn(f"#{item.s_hash_id}", response.content.decode("utf-8")) self.assertEqual(item.i_level, TbHypn0Item.Level.CANDIDATE) self.assertEqual(item.i_likes_count, 1) self.assertEqual(item.j_metadata["shape"], "diamond") self.assertEqual(item.j_metadata["cols"], 40) self.assertEqual(item.j_metadata["color"], "#10b981") # Проверяем авторский голос в TbVote self.assertEqual(TbVote.objects.count(), 1) author_vote = TbVote.objects.first() self.assertEqual(author_vote.k_item, item) self.assertEqual(author_vote.i_direction, TbVote.Direction.AUTHOR) def test_publish_empty_svg_returns_error(self): self.client.cookies["hypn0_vid"] = "123e4567-e89b-12d3-a456-426614174000" response = self.client.post( reverse("hypn0_site:publish"), data={"svg_content": ""}, ) self.assertEqual(response.status_code, 200) self.assertContains(response, "Нет данных SVG для публикации") self.assertEqual(TbHypn0Item.objects.count(), 0) class SvgAnalysisAndActiveSvgTests(BaseMediaTestCase): """Тестирование анализа структуры SVG и очистки от паузы.""" def test_prepare_active_svg(self): gallery_svg = '' active = prepare_active_svg(gallery_svg) self.assertNotIn("animation-play-state:paused!important", active) def test_prepare_active_svg_preserves_color_and_animation(self): gallery_svg = ( '' '' ) active = prepare_active_svg(gallery_svg) self.assertIn("fill:#e6b400", active) self.assertIn("stroke:#e6b400", active) self.assertIn("animation:noise 1.36s", active) self.assertNotIn("svg{--hypn0-play:paused}", active) self.assertNotIn("animation-play-state:var(--hypn0-play,paused)!important", active) def test_analyze_svg_structure(self): svg = ( '' '' '' '' '' '' '' '' ) stats = analyze_svg_structure(svg) self.assertEqual(stats["use_count"], 3) self.assertEqual(stats["total_oscillators"], 3) self.assertEqual(stats["defs_count"], 2) self.assertEqual(stats["circle_count"], 1) self.assertEqual(stats["rect_count"], 1) self.assertEqual(stats["groups_count"], 3) self.assertEqual(stats["keyframes_count"], 1) self.assertEqual(stats["viewbox"], "0 0 800 600") self.assertEqual(stats["width"], 800) self.assertEqual(stats["height"], 600) self.assertEqual(stats["aspect_ratio"], "4:3") self.assertTrue(stats["total_bytes"] > 0) class GalleryDetailAndDownloadTests(BaseMediaTestCase): """Тестирование страниц детального просмотра картины, скачивания и голосования.""" def setUp(self): self.client = Client() self.vid = "123e4567-e89b-12d3-a456-426614174000" svg_code = ( '' '' '' '' '' ) self.item = TbHypn0Item( s_title="Астральный Транс Сознания #42", file_svg=ContentFile(svg_code.encode("utf-8"), name="test_item.svg"), j_metadata={ "shape": "circle", "cols": 35, "max_radius": 8, "blink": 6, "rotation": 0, "scale": 980, "angle": 0, "color": "#a855ff", }, i_level=TbHypn0Item.Level.CANDIDATE, is_public=True, ) self.item.save(visitor_uuid_or_fp=self.vid) def test_gallery_detail_view_success(self): response = self.client.get(reverse("hypn0_site:gallery_detail", kwargs={"hash_id": self.item.s_hash_id})) self.assertEqual(response.status_code, 200) self.assertContains(response, "Астральный Транс Сознания #42") self.assertContains(response, f"#{self.item.s_hash_id}") self.assertContains(response, "Векторная анатомия") self.assertContains(response, "Синтез психо-сетки") self.assertContains(response, "Астральный паспорт") self.assertContains(response, "Копировать SVG-код") self.assertContains(response, "Скачать .svg") # Проверка инкремента просмотров self.item.refresh_from_db() self.assertGreaterEqual(self.item.i_views_count, 2) def test_gallery_detail_404_on_invalid_hash(self): response = self.client.get(reverse("hypn0_site:gallery_detail", kwargs={"hash_id": "nonexistent999"})) self.assertEqual(response.status_code, 404) def test_gallery_download_view_success(self): response = self.client.get(reverse("hypn0_site:gallery_download", kwargs={"hash_id": self.item.s_hash_id})) self.assertEqual(response.status_code, 200) self.assertEqual(response["Content-Type"], "image/svg+xml") self.assertIn(f'filename="hypn0-{self.item.s_hash_id}.svg"', response["Content-Disposition"]) # Должен быть чистый активный SVG (без паузы анимации) self.assertNotIn("animation-play-state:paused!important", response.content.decode("utf-8")) def test_gallery_vote_with_cookie(self): # Новый посетитель voter_vid = "987e6543-e21b-12d3-a456-426614174999" self.client.cookies["hypn0_vid"] = voter_vid response = self.client.post(reverse("hypn0_site:gallery_vote", kwargs={"hash_id": self.item.s_hash_id})) self.assertEqual(response.status_code, 200) self.item.refresh_from_db() self.assertEqual(self.item.i_likes_count, 2) def test_gallery_vote_without_cookie_returns_notice(self): client = Client() response = client.post(reverse("hypn0_site:gallery_vote", kwargs={"hash_id": self.item.s_hash_id})) self.assertEqual(response.status_code, 200) self.assertContains(response, "Подчинитесь воле Гипножабы!") class GalleryRandomAndNavigationTests(BaseMediaTestCase): """Тестирование случайной навигации и пула хэшей (/gallery/random).""" def setUp(self): self.client = Client() self.vid = "123e4567-e89b-12d3-a456-426614174000" # Создаем 3 картины в галерее self.items = [] for i in range(3): svg_code = f'' item = TbHypn0Item( s_title=f"Гипно Картина #{i}", file_svg=ContentFile(svg_code.encode("utf-8"), name=f"test_item_{i}.svg"), j_metadata={"cols": 30 + i}, is_public=True, ) item.save(visitor_uuid_or_fp=self.vid) self.items.append(item) def test_gallery_random_direct_redirect(self): response = self.client.get(reverse("hypn0_site:gallery_random")) self.assertEqual(response.status_code, 302) all_hashes = [it.s_hash_id for it in self.items] self.assertTrue(any(h in response.url for h in all_hashes)) def test_gallery_random_json_pool(self): response = self.client.get(reverse("hypn0_site:gallery_random"), data={"format": "json", "limit": 2}) self.assertEqual(response.status_code, 200) data = response.json() self.assertIn("next_hash_id", data) self.assertIn("pool", data) self.assertEqual(len(data["pool"]), 2) all_hashes = {it.s_hash_id for it in self.items} self.assertIn(data["next_hash_id"], all_hashes) def test_gallery_random_exclude_filters_seen(self): exclude_hashes = [self.items[0].s_hash_id, self.items[1].s_hash_id] response = self.client.get( reverse("hypn0_site:gallery_random"), data={"format": "json", "exclude": ",".join(exclude_hashes)}, ) self.assertEqual(response.status_code, 200) data = response.json() self.assertEqual(data["next_hash_id"], self.items[2].s_hash_id) def test_gallery_random_exclude_all_loops_circle(self): # Если исключены все 3 картины, кольцо замыкается exclude_hashes = [it.s_hash_id for it in self.items] response = self.client.get( reverse("hypn0_site:gallery_random"), data={ "format": "json", "exclude": ",".join(exclude_hashes), "current": self.items[0].s_hash_id, }, ) self.assertEqual(response.status_code, 200) data = response.json() self.assertIsNotNone(data["next_hash_id"]) # Текущий элемент исключен, выбран один из оставшихся self.assertIn(data["next_hash_id"], [self.items[1].s_hash_id, self.items[2].s_hash_id]) def test_gallery_random_empty_database(self): TbHypn0Item.objects.all().delete() # Direct visit redirects to index response = self.client.get(reverse("hypn0_site:gallery_random")) self.assertEqual(response.status_code, 302) self.assertEqual(response.url, reverse("hypn0_site:index")) # JSON returns null next_hash_id json_resp = self.client.get(reverse("hypn0_site:gallery_random"), data={"format": "json"}) self.assertEqual(json_resp.status_code, 200) self.assertIsNone(json_resp.json()["next_hash_id"]) self.assertEqual(json_resp.json()["pool"], []) class UnconsciousMatrixTests(BaseMediaTestCase): """Тестирование Матрицы бессознательного (build_unconscious_matrix) и навигации.""" def setUp(self): self.client = Client() self.vid = "123e4567-e89b-12d3-a456-426614174000" # Создаем 5 картин self.items = [] for i in range(5): svg_code = f'' item = TbHypn0Item( s_title=f"Тестовый транс #{i}", file_svg=ContentFile(svg_code.encode("utf-8"), name=f"test_matrix_{i}.svg"), j_metadata={"cols": 20 + i}, is_public=True, ) item.save(visitor_uuid_or_fp=self.vid) self.items.append(item) def test_build_unconscious_matrix_basic(self): from hypn0_site.views import build_unconscious_matrix current = self.items[0].s_hash_id matrix_items, prev_h, next_h, prev_url, next_url, seed = build_unconscious_matrix(current, None) self.assertEqual(len(matrix_items), 5) # Проверяем, что current отмечен как is_current current_node = [n for n in matrix_items if n["s_hash_id"] == current][0] self.assertTrue(current_node["is_current"]) # Другие узлы не current other_nodes = [n for n in matrix_items if n["s_hash_id"] != current] self.assertTrue(all(not n["is_current"] for n in other_nodes)) # Ссылки содержат seed self.assertIn(f"seed={seed}", prev_url) self.assertIn(f"seed={seed}", next_url) def test_matrix_stability_with_fixed_seed(self): from hypn0_site.views import build_unconscious_matrix root_hash = self.items[2].s_hash_id fixed_seed = f"42109_{root_hash}" # Первый запрос m1, prev1, next1, _, _, seed1 = build_unconscious_matrix(self.items[0].s_hash_id, fixed_seed) # Второй запрос с тем же seed на другую картину m2, prev2, next2, _, _, seed2 = build_unconscious_matrix(self.items[1].s_hash_id, fixed_seed) self.assertEqual(seed1, fixed_seed) self.assertEqual(seed2, fixed_seed) # Состав и порядок хэшей в матрице должны быть идентичны hashes1 = [n["s_hash_id"] for n in m1] hashes2 = [n["s_hash_id"] for n in m2] self.assertEqual(hashes1, hashes2) def test_gallery_detail_renders_matrix_and_card_bg_style(self): item = self.items[0] response = self.client.get(reverse("hypn0_site:gallery_detail", kwargs={"hash_id": item.s_hash_id})) self.assertEqual(response.status_code, 200) self.assertContains(response, "Матрица бессознательного") self.assertContains(response, "5 узлов") self.assertContains(response, f"#{item.s_hash_id}") self.assertContains(response, "btn-nav-prev") self.assertContains(response, "btn-nav-next") self.assertContains(response, "--card-bg-light") self.assertContains(response, "--card-bg-dark") self.assertContains(response, "bg-[var(--card-bg-light)]") class GalleryFreshFloorTests(BaseMediaTestCase): """Тестирование 1-го этажа («Плеск бессознательного»), выборки и пагинации.""" def setUp(self): self.client = Client() self.vid = "123e4567-e89b-12d3-a456-426614174000" def test_get_floor_fresh_filtering_and_ordering(self): from hypn0_site.views import get_floor_fresh # 1. Создаем картины разных уровней и с разным числом просмотров svg_bytes = b'' # Свежий кандидат с 5 просмотрами item_cand = TbHypn0Item( s_title="Кандидат", file_svg=ContentFile(svg_bytes, name="c.svg"), i_views_count=5, i_level=TbHypn0Item.Level.CANDIDATE, is_public=True, ) item_cand.save(visitor_uuid_or_fp=self.vid) # Level 1 с 1 просмотром (должен быть первым, т.к. просмотров меньше) item_lvl1 = TbHypn0Item( s_title="Level 1", file_svg=ContentFile(svg_bytes, name="l1.svg"), i_views_count=1, i_level=TbHypn0Item.Level.LEVEL_1, is_public=True, ) item_lvl1.save(visitor_uuid_or_fp=self.vid) # Level 2 (2-й этаж, не должен попасть в 1-й) item_lvl2 = TbHypn0Item( s_title="Level 2 Curated", file_svg=ContentFile(svg_bytes, name="l2.svg"), i_views_count=0, i_level=TbHypn0Item.Level.LEVEL_2, is_public=True, ) item_lvl2.save(visitor_uuid_or_fp=self.vid) # Непубличная картина (не должна попасть) item_private = TbHypn0Item( s_title="Private", file_svg=ContentFile(svg_bytes, name="p.svg"), i_views_count=0, i_level=TbHypn0Item.Level.CANDIDATE, is_public=False, ) item_private.save(visitor_uuid_or_fp=self.vid) results = list(get_floor_fresh(limit=10)) self.assertEqual(len(results), 2) # Сначала с наименьшим i_views_count self.assertEqual(results[0].pk, item_lvl1.pk) self.assertEqual(results[1].pk, item_cand.pk) def test_index_view_renders_fresh_stream(self): # Создаем 2 картины для 1 этажа svg_bytes = b'' item = TbHypn0Item( s_title="Свежий шедевр", file_svg=ContentFile(svg_bytes, name="f1.svg"), i_views_count=2, i_level=TbHypn0Item.Level.CANDIDATE, is_public=True, ) item.save(visitor_uuid_or_fp=self.vid) response = self.client.get(reverse("hypn0_site:index")) self.assertEqual(response.status_code, 200) self.assertContains(response, "Плеск бессознательного") self.assertContains(response, "FRESH STREAM") self.assertContains(response, f"gallery-card-{item.s_hash_id}") self.assertContains(response, f"¤{item.s_hash_id}") def test_publish_includes_oob_swap_for_fresh_grid(self): self.client.cookies["hypn0_vid"] = self.vid sample_svg = '' response = self.client.post( reverse("hypn0_site:publish"), data={ "svg_content": sample_svg, "shape": "circle", "cols": "35", }, ) self.assertEqual(response.status_code, 200) self.assertContains(response, 'hx-swap-oob="afterbegin:#fresh-stream-grid"') item = TbHypn0Item.objects.first() self.assertContains(response, f"gallery-card-{item.s_hash_id}") def test_gallery_floor_fresh_pagination(self): # Создаем 20 картин svg_bytes = b'' for i in range(20): item = TbHypn0Item( s_title=f"Кандидат #{i}", file_svg=ContentFile(svg_bytes, name=f"cand_{i}.svg"), i_views_count=i, i_level=TbHypn0Item.Level.CANDIDATE, is_public=True, ) item.save(visitor_uuid_or_fp=self.vid) # Страница 1 (должно быть 16 штук) url = reverse("hypn0_site:gallery_floor", kwargs={"floor_slug": "fresh"}) response_p1 = self.client.get(url) self.assertEqual(response_p1.status_code, 200) self.assertContains(response_p1, "Плеск бессознательного") self.assertContains(response_p1, "FRESH STREAM") self.assertEqual(len(response_p1.context["page_obj"]), 16) self.assertContains(response_p1, "Фаза 1 из 2") # Страница 2 (должно быть 4 штуки) response_p2 = self.client.get(url, data={"page": 2}) self.assertEqual(response_p2.status_code, 200) self.assertEqual(len(response_p2.context["page_obj"]), 4) self.assertContains(response_p2, "Фаза 2 из 2") def test_gallery_floors_curated_and_top(self): from hypn0_site.views import get_floor_curated, get_floor_top svg_bytes = b'' item_curated = TbHypn0Item( s_title="Одобренная картина", file_svg=ContentFile(svg_bytes, name="cur.svg"), i_level=TbHypn0Item.Level.LEVEL_2, f_score=42.5, is_public=True, ) item_curated.save(visitor_uuid_or_fp=self.vid) item_top = TbHypn0Item( s_title="Шедевр золотого фонда", file_svg=ContentFile(svg_bytes, name="top.svg"), i_level=TbHypn0Item.Level.IMMORTAL, i_likes_count=150, f_score=99.0, is_public=True, ) item_top.save(visitor_uuid_or_fp=self.vid) # Проверяем выборки curated_res = list(get_floor_curated()) self.assertEqual(len(curated_res), 1) self.assertEqual(curated_res[0].pk, item_curated.pk) top_res = list(get_floor_top()) self.assertEqual(len(top_res), 1) self.assertEqual(top_res[0].pk, item_top.pk) # Проверяем рендеринг страниц этажей url_curated = reverse("hypn0_site:gallery_floor", kwargs={"floor_slug": "curated"}) resp_c = self.client.get(url_curated) self.assertEqual(resp_c.status_code, 200) self.assertContains(resp_c, "Одобрено Мозговым Слизнем") self.assertContains(resp_c, "CURATED") url_top = reverse("hypn0_site:gallery_floor", kwargs={"floor_slug": "top"}) resp_t = self.client.get(url_top) self.assertEqual(resp_t.status_code, 200) self.assertContains(resp_t, "Глубокий транс") self.assertContains(resp_t, "TOP TIER") # Проверяем отображение на главной странице resp_idx = self.client.get(reverse("hypn0_site:index")) self.assertEqual(resp_idx.status_code, 200) self.assertContains(resp_idx, f"gallery-card-{item_curated.s_hash_id}") self.assertContains(resp_idx, f"gallery-card-{item_top.s_hash_id}") def test_gallery_floor_unknown_404(self): url = reverse("hypn0_site:gallery_floor", kwargs={"floor_slug": "non_existent"}) response = self.client.get(url) self.assertEqual(response.status_code, 404) class CardBgStyleTests(BaseMediaTestCase): """Тестирование вычисления адаптивных стилей подложки карточки.""" def test_extreme_white_color_forces_dark_background(self): item = TbHypn0Item(j_metadata={"color": "#ffffff"}) style = item.card_bg_style self.assertIn("--card-bg-light: rgb(12, 12, 15);", style) self.assertIn("--card-bg-dark: rgb(12, 12, 15);", style) def test_extreme_black_color_forces_light_background(self): item = TbHypn0Item(j_metadata={"color": "#000000"}) style = item.card_bg_style self.assertIn("--card-bg-light: rgb(244, 244, 246);", style) self.assertIn("--card-bg-dark: rgb(244, 244, 246);", style) def test_colorful_intermediate_color_creates_theme_adaptive_background(self): item = TbHypn0Item(j_metadata={"color": "#a855ff"}) style = item.card_bg_style self.assertIn("--card-bg-light: rgb(250, 247, 252);", style) self.assertIn("--card-bg-dark: rgb(20, 14, 33);", style) def test_fallback_on_empty_or_invalid_color(self): item_none = TbHypn0Item(j_metadata=None) self.assertIn("--card-bg-light:", item_none.card_bg_style) self.assertIn("--card-bg-dark:", item_none.card_bg_style) item_invalid = TbHypn0Item(j_metadata={"color": "invalid-hex"}) self.assertIn("--card-bg-light:", item_invalid.card_bg_style) self.assertIn("--card-bg-dark:", item_invalid.card_bg_style) def test_card_svg_property_and_shadow_dom_rendering(self): item = TbHypn0Item( s_title="Тестовый SVG", file_svg=ContentFile(b'', name="test_card.svg"), j_metadata={"color": "#10b981"}, is_public=True, ) item.save(visitor_uuid_or_fp="123e4567-e89b-12d3-a456-426614174000") self.assertIn('id="test-svg"', item.card_svg) response = self.client.get(reverse("hypn0_site:index")) self.assertContains(response, 'template shadowrootmode="open"') self.assertContains(response, "hypn0-card-svg") self.assertContains(response, "--hypn0-play: running !important")