Основные изменения: - Добавлены E2E тесты для проверки ownership (TC-E2E-102/103): * test_admin_can_edit_any_post — admin может редактировать любой пост * test_user_cannot_edit_other_users_post — user не может редактировать чужой пост - Исправлены use cases (UpdatePost, DeletePost, PublishPost) — добавлена проверка роли admin - Обновлены web routes и API routes для передачи роли в use cases - Добавлены unit тесты для admin-сценариев Реструктуризация тестов: - Удалены старые API тесты (tests/api/) — требуют переработки - Удалены старые integration тесты (tests/integration/) - Переработаны E2E тесты: удалены старые, добавлены новые с POM - Добавлена документация тестов: FEATURE_*.md, TEST_MODEL.md, AGENTS.md Инфраструктура: - Добавлен MockKeycloakClient для dev-режима - Добавлены статические файлы: EasyMDE, Highlight.js, стили markdown - Обновлены шаблоны: base.html, post_form.html, post_detail.html - Обновлена DI конфигурация и провайдеры Документация: - tests/FEATURE_RBAC.md — матрица тестов RBAC - tests/FEATURE_POST_LIFECYCLE.md — тесты жизненного цикла поста - tests/FEATURE_DOMAIN_FOUNDATION.md — тесты доменного слоя - tests/FEATURE_INFRASTRUCTURE.md — тесты инфраструктуры - tests/TEST_MODEL.md — глобальная матрица покрытия - app/presentation/web/AGENTS.md — гайд по Web UI - tests/AGENTS.md — гайд по тестированию
338 lines
12 KiB
HTML
338 lines
12 KiB
HTML
<header class="site-header" data-testid="site-header">
|
|
<div class="container" data-testid="header-container">
|
|
<a href="/web/" class="site-logo" data-testid="nav-logo">
|
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" data-testid="logo-icon">
|
|
<rect width="32" height="32" rx="6" fill="var(--color-primary)"/>
|
|
<path d="M8 12h16M8 16h12M8 20h8" stroke="white" stroke-width="2" stroke-linecap="round"/>
|
|
</svg>
|
|
<span data-testid="logo-text">Blog</span>
|
|
</a>
|
|
|
|
{% include "partials/nav.html" %}
|
|
|
|
<div class="header-actions" data-testid="header-actions">
|
|
<button
|
|
type="button"
|
|
class="mobile-menu-btn"
|
|
data-testid="mobile-menu-toggle"
|
|
aria-label="Toggle menu"
|
|
aria-expanded="false"
|
|
aria-controls="mobile-nav"
|
|
>
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="menu-icon-open">
|
|
<path d="M3 12h18M3 6h18M3 18h18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
</svg>
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="menu-icon-close" style="display: none;">
|
|
<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
</svg>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="theme-toggle"
|
|
data-testid="theme-toggle"
|
|
aria-label="Toggle dark mode"
|
|
title="Toggle dark mode"
|
|
>
|
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" data-testid="theme-light-icon" style="display: none;">
|
|
<path d="M10 2v2M10 16v2M4.22 4.22l1.42 1.42M14.36 14.36l1.42 1.42M2 10h2M16 10h2M4.22 15.78l1.42-1.42M14.36 5.64l1.42-1.42" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
<circle cx="10" cy="10" r="3" stroke="currentColor" stroke-width="2"/>
|
|
</svg>
|
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" data-testid="theme-dark-icon" style="display: none;">
|
|
<path d="M17.293 13.293A8 8 0 116.707 2.707a8.003 8.003 0 0010.586 10.586z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
</svg>
|
|
</button>
|
|
|
|
{% if user %}
|
|
<div class="user-menu" data-testid="user-menu">
|
|
<button
|
|
type="button"
|
|
class="user-menu-toggle"
|
|
data-testid="user-menu-toggle"
|
|
aria-haspopup="true"
|
|
aria-expanded="false"
|
|
>
|
|
<span class="avatar avatar-sm" data-testid="user-avatar">{{ user.username[0]|upper }}</span>
|
|
<span class="user-name" data-testid="user-name">{{ user.username }}</span>
|
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-left: 0.25rem;">
|
|
<path d="M2 4L6 8L10 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
</svg>
|
|
</button>
|
|
<div class="user-menu-dropdown" data-testid="user-menu-dropdown">
|
|
<a href="/web/profile" class="user-menu-item" data-testid="user-menu-profile">
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-right: 0.5rem;">
|
|
<circle cx="8" cy="6" r="3" stroke="currentColor" stroke-width="2"/>
|
|
<path d="M2 14c0-3 3-5 6-5s6 2 6 5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
</svg>
|
|
Profile
|
|
</a>
|
|
{% if can_create %}
|
|
<a href="/web/posts/new" class="user-menu-item" data-testid="user-menu-new-post">
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-right: 0.5rem;">
|
|
<path d="M8 2v12M2 8h12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
</svg>
|
|
New Post
|
|
</a>
|
|
{% endif %}
|
|
<div class="user-menu-divider" data-testid="user-menu-divider"></div>
|
|
<a href="/auth/logout" class="user-menu-item user-menu-item-danger" data-testid="user-menu-logout">
|
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-right: 0.5rem;">
|
|
<path d="M10 12h2a2 2 0 002-2V6a2 2 0 00-2-2h-2M6 12l-3-3m0 0l3-3m-3 3h8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
</svg>
|
|
Sign Out
|
|
</a>
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<a href="/auth/login" class="btn btn-primary btn-sm" data-testid="btn-login">
|
|
Sign In
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<style>
|
|
.user-menu {
|
|
position: relative;
|
|
}
|
|
|
|
.user-menu-toggle {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.375rem 0.75rem;
|
|
background: transparent;
|
|
border: 1px solid var(--color-border);
|
|
border-radius: 6px;
|
|
color: var(--color-text);
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.user-menu-toggle:hover {
|
|
background-color: var(--color-hover);
|
|
border-color: var(--color-secondary-dark-2);
|
|
}
|
|
|
|
.user-name {
|
|
font-weight: 500;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.user-menu-dropdown {
|
|
position: absolute;
|
|
top: 100%;
|
|
right: 0;
|
|
margin-top: 0.5rem;
|
|
min-width: 180px;
|
|
background-color: var(--color-box-body);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: 8px;
|
|
box-shadow: 0 4px 12px var(--color-shadow);
|
|
opacity: 0;
|
|
visibility: hidden;
|
|
transform: translateY(-10px);
|
|
transition: all 0.2s ease;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.user-menu:hover .user-menu-dropdown,
|
|
.user-menu-toggle:focus + .user-menu-dropdown,
|
|
.user-menu-dropdown:hover {
|
|
opacity: 1;
|
|
visibility: visible;
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.user-menu-item {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 0.75rem 1rem;
|
|
color: var(--color-text);
|
|
text-decoration: none;
|
|
font-size: 0.875rem;
|
|
transition: background-color 0.2s ease;
|
|
}
|
|
|
|
.user-menu-item:first-child {
|
|
border-radius: 8px 8px 0 0;
|
|
}
|
|
|
|
.user-menu-item:last-child {
|
|
border-radius: 0 0 8px 8px;
|
|
}
|
|
|
|
.user-menu-item:hover {
|
|
background-color: var(--color-hover);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.user-menu-item-danger {
|
|
color: var(--color-red);
|
|
}
|
|
|
|
.user-menu-item-danger:hover {
|
|
background-color: var(--color-error-bg);
|
|
}
|
|
|
|
.user-menu-divider {
|
|
height: 1px;
|
|
background-color: var(--color-border);
|
|
margin: 0.25rem 0;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.user-name {
|
|
display: none;
|
|
}
|
|
|
|
.mobile-menu-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 2.5rem;
|
|
height: 2.5rem;
|
|
padding: 0;
|
|
background: transparent;
|
|
border: 1px solid var(--color-border);
|
|
border-radius: 6px;
|
|
color: var(--color-text);
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.mobile-menu-btn:hover {
|
|
background-color: var(--color-hover);
|
|
}
|
|
|
|
.mobile-menu-btn[aria-expanded="true"] {
|
|
background-color: var(--color-primary);
|
|
border-color: var(--color-primary);
|
|
color: var(--color-primary-contrast);
|
|
}
|
|
|
|
.mobile-nav {
|
|
display: none;
|
|
position: fixed;
|
|
top: 4rem;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-color: var(--color-body);
|
|
z-index: 99;
|
|
padding: 2rem;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.mobile-nav.is-open {
|
|
display: block;
|
|
}
|
|
|
|
.mobile-nav .nav-link {
|
|
display: block;
|
|
padding: 1rem 0;
|
|
font-size: 1.25rem;
|
|
border-bottom: 1px solid var(--color-border);
|
|
border-bottom-color: transparent;
|
|
}
|
|
|
|
.mobile-nav .nav-link:last-child {
|
|
border-bottom: none;
|
|
}
|
|
}
|
|
|
|
@media (min-width: 769px) {
|
|
.mobile-menu-btn {
|
|
display: none;
|
|
}
|
|
|
|
.mobile-nav {
|
|
display: none !important;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<!-- Mobile Navigation Menu -->
|
|
<nav class="mobile-nav" id="mobile-nav" data-testid="mobile-nav" aria-label="Mobile navigation">
|
|
<a href="/web/" class="nav-link {% if active_page == 'home' %}active{% endif %}" data-testid="mobile-nav-link-home">
|
|
Home
|
|
</a>
|
|
<a href="/web/posts" class="nav-link {% if active_page == 'posts' %}active{% endif %}" data-testid="mobile-nav-link-posts">
|
|
Posts
|
|
</a>
|
|
<a href="/web/about" class="nav-link {% if active_page == 'about' %}active{% endif %}" data-testid="mobile-nav-link-about">
|
|
About
|
|
</a>
|
|
</nav>
|
|
|
|
<script>
|
|
(function() {
|
|
'use strict';
|
|
|
|
const menuBtn = document.querySelector('[data-testid="mobile-menu-toggle"]');
|
|
const mobileNav = document.getElementById('mobile-nav');
|
|
const menuIconOpen = menuBtn?.querySelector('.menu-icon-open');
|
|
const menuIconClose = menuBtn?.querySelector('.menu-icon-close');
|
|
|
|
function toggleMenu() {
|
|
if (!mobileNav || !menuBtn) return;
|
|
|
|
const isOpen = mobileNav.classList.toggle('is-open');
|
|
menuBtn.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
|
|
|
if (menuIconOpen && menuIconClose) {
|
|
menuIconOpen.style.display = isOpen ? 'none' : 'block';
|
|
menuIconClose.style.display = isOpen ? 'block' : 'none';
|
|
}
|
|
|
|
// Prevent body scroll when menu is open
|
|
document.body.style.overflow = isOpen ? 'hidden' : '';
|
|
}
|
|
|
|
function closeMenu() {
|
|
if (!mobileNav || !menuBtn) return;
|
|
|
|
mobileNav.classList.remove('is-open');
|
|
menuBtn.setAttribute('aria-expanded', 'false');
|
|
|
|
if (menuIconOpen && menuIconClose) {
|
|
menuIconOpen.style.display = 'block';
|
|
menuIconClose.style.display = 'none';
|
|
}
|
|
|
|
document.body.style.overflow = '';
|
|
}
|
|
|
|
if (menuBtn) {
|
|
menuBtn.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
toggleMenu();
|
|
});
|
|
}
|
|
|
|
// Close menu when clicking on a link
|
|
if (mobileNav) {
|
|
mobileNav.querySelectorAll('a').forEach(function(link) {
|
|
link.addEventListener('click', closeMenu);
|
|
});
|
|
}
|
|
|
|
// Close menu on escape key
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Escape' && mobileNav?.classList.contains('is-open')) {
|
|
closeMenu();
|
|
}
|
|
});
|
|
|
|
// Close menu when clicking outside
|
|
document.addEventListener('click', function(e) {
|
|
if (mobileNav?.classList.contains('is-open') &&
|
|
!mobileNav.contains(e.target) &&
|
|
!menuBtn?.contains(e.target)) {
|
|
closeMenu();
|
|
}
|
|
});
|
|
})();
|
|
</script>
|