test: add full playwright ui regression coverage
Some checks failed
docker-images / build-and-push (admin, admin, termi-astro-admin, admin/Dockerfile) (push) Successful in 52s
docker-images / build-and-push (backend, backend, termi-astro-backend, backend/Dockerfile) (push) Failing after 13s
docker-images / build-and-push (frontend, frontend, termi-astro-frontend, frontend/Dockerfile) (push) Successful in 32s
ui-regression / playwright-regression (push) Failing after 14m24s

This commit is contained in:
2026-04-02 00:55:34 +08:00
parent 7de4ddc3ee
commit ee0bec4a78
32 changed files with 5100 additions and 336 deletions

View File

@@ -0,0 +1,92 @@
import type { Review } from './api/client';
export type ReviewStatus = 'completed' | 'in-progress' | 'dropped';
export type ParsedReview = Omit<Review, 'tags'> & {
tags: string[];
normalizedStatus: ReviewStatus;
coverIsImage: boolean;
coverUrl: string | null;
linkUrl: string | null;
externalLink: boolean;
};
const reviewCoverCatalog: Record<string, string> = {
'《漫长的季节》': '/review-covers/the-long-season.svg',
'《十三邀》': '/review-covers/thirteen-invites.svg',
'《黑神话:悟空》': '/review-covers/black-myth-wukong.svg',
'《置身事内》': '/review-covers/placed-within.svg',
'《宇宙探索编辑部》': '/review-covers/journey-to-the-west-editorial.svg',
'《疲惫生活中的英雄梦想》': '/review-covers/hero-dreams-in-tired-life.svg',
};
export function normalizeReviewStatus(status: string | null | undefined): ReviewStatus {
const normalized = String(status || '').trim().toLowerCase();
if (normalized === 'published' || normalized === 'completed' || normalized === 'done') {
return 'completed';
}
if (
normalized === 'draft' ||
normalized === 'in-progress' ||
normalized === 'watching' ||
normalized === 'reading' ||
normalized === 'listening'
) {
return 'in-progress';
}
if (normalized === 'dropped' || normalized === 'abandoned') {
return 'dropped';
}
return 'completed';
}
export function parseReviewTags(value: string | null | undefined): string[] {
if (!value) {
return [];
}
try {
const parsed = JSON.parse(value);
return Array.isArray(parsed) ? parsed.filter((item): item is string => typeof item === 'string') : [];
} catch {
return [];
}
}
export function isImageCover(cover: string | null | undefined) {
const normalized = String(cover || '').trim();
return /^(https?:)?\/\//.test(normalized) || normalized.startsWith('/');
}
export function resolveReviewCover(review: Pick<Review, 'cover' | 'title'>) {
if (isImageCover(review.cover)) {
return String(review.cover).trim();
}
return reviewCoverCatalog[review.title] || null;
}
export function normalizeLinkUrl(value: string | null | undefined) {
const trimmed = String(value || '').trim();
return trimmed ? trimmed : null;
}
export function isExternalLink(value: string | null | undefined) {
return /^(https?:)?\/\//.test(String(value || '').trim());
}
export function parseReview(review: Review): ParsedReview {
return {
...review,
tags: parseReviewTags(review.tags),
normalizedStatus: normalizeReviewStatus(review.status),
coverIsImage: isImageCover(review.cover),
coverUrl: resolveReviewCover(review),
linkUrl: normalizeLinkUrl(review.link_url),
externalLink: isExternalLink(review.link_url),
};
}

View File

@@ -1,194 +0,0 @@
import type { Post, Category, Tag, FriendLink } from '../types';
// Mock data for static site generation
export const mockPosts: Post[] = [
{
id: '1',
slug: 'welcome-to-termi',
title: '欢迎来到 Termi 终端博客',
description: '这是一个基于终端风格的现代博客平台,结合了极客美学与极致性能。',
date: '2024-03-20',
readTime: '3 分钟',
type: 'article',
tags: ['astro', 'svelte', 'tailwind'],
category: '技术',
pinned: true,
image: 'https://picsum.photos/1200/600?random=1'
},
{
id: '2',
slug: 'astro-ssg-guide',
title: 'Astro 静态站点生成指南',
description: '学习如何使用 Astro 构建高性能的静态网站,掌握群岛架构的核心概念。',
date: '2024-03-18',
readTime: '5 分钟',
type: 'article',
tags: ['astro', 'ssg', 'performance'],
category: '前端'
},
{
id: '3',
slug: 'tailwind-v4-features',
title: 'Tailwind CSS v4 新特性解析',
description: '探索 Tailwind CSS v4 带来的全新特性,包括改进的性能和更简洁的配置。',
date: '2024-03-15',
readTime: '4 分钟',
type: 'article',
tags: ['tailwind', 'css', 'design'],
category: '前端'
},
{
id: '4',
slug: 'daily-thought-1',
title: '关于代码与咖啡的思考',
description: '写代码就像冲咖啡,需要耐心和恰到好处的温度。今天尝试了几款新豆子,每一杯都有不同的风味。',
date: '2024-03-14',
readTime: '1 分钟',
type: 'tweet',
tags: ['life', 'coding'],
category: '随笔',
images: [
'https://images.unsplash.com/photo-1495474472287-4d71bcdd2085?w=400&h=400&fit=crop',
'https://images.unsplash.com/photo-1509042239860-f550ce710b93?w=400&h=400&fit=crop',
'https://images.unsplash.com/photo-1514432324607-a09d9b4aefdd?w=400&h=400&fit=crop'
]
},
{
id: '5',
slug: 'svelte-5-runes',
title: 'Svelte 5 Runes 完全指南',
description: '深入了解 Svelte 5 的 Runes 系统,掌握下一代响应式编程范式。',
date: '2024-03-10',
readTime: '8 分钟',
type: 'article',
tags: ['svelte', 'javascript', 'frontend'],
category: '前端'
}
];
export const mockCategories: Category[] = [
{ id: '1', name: '技术', slug: 'tech', icon: 'fa-code', count: 3 },
{ id: '2', name: '前端', slug: 'frontend', icon: 'fa-laptop-code', count: 3 },
{ id: '3', name: '随笔', slug: 'essay', icon: 'fa-pen', count: 1 },
{ id: '4', name: '生活', slug: 'life', icon: 'fa-coffee', count: 1 }
];
export const mockTags: Tag[] = [
{ id: '1', name: 'astro', slug: 'astro', count: 1 },
{ id: '2', name: 'svelte', slug: 'svelte', count: 2 },
{ id: '3', name: 'tailwind', slug: 'tailwind', count: 2 },
{ id: '4', name: 'frontend', slug: 'frontend', count: 2 },
{ id: '5', name: 'ssg', slug: 'ssg', count: 1 },
{ id: '6', name: 'css', slug: 'css', count: 1 },
{ id: '7', name: 'javascript', slug: 'javascript', count: 1 },
{ id: '8', name: 'life', slug: 'life', count: 1 },
{ id: '9', name: 'coding', slug: 'coding', count: 1 }
];
export const mockFriendLinks: FriendLink[] = [
{
id: '1',
name: 'Astro 官网',
url: 'https://astro.build',
avatar: 'https://astro.build/favicon.svg',
description: '极速内容驱动的网站框架',
category: '技术博客'
},
{
id: '2',
name: 'Svelte 官网',
url: 'https://svelte.dev',
avatar: 'https://svelte.dev/favicon.png',
description: '控制论增强的 Web 应用',
category: '技术博客'
},
{
id: '3',
name: 'Tailwind CSS',
url: 'https://tailwindcss.com',
avatar: 'https://tailwindcss.com/favicons/favicon-32x32.png',
description: '实用优先的 CSS 框架',
category: '技术博客'
}
];
export const mockSiteConfig = {
name: 'Termi',
description: '终端风格的内容平台',
author: 'InitCool',
url: 'https://termi.dev',
social: {
github: 'https://github.com',
twitter: 'https://twitter.com',
email: 'mailto:hello@termi.dev'
}
};
export const mockSystemStats = [
{ label: 'Last Update', value: '2024-03-20' },
{ label: 'Posts', value: '12' },
{ label: 'Visitors', value: '1.2k' }
];
export const mockTechStack = [
{ name: 'Astro' },
{ name: 'Svelte' },
{ name: 'Tailwind CSS' },
{ name: 'TypeScript' },
{ name: 'Vercel' }
];
export const mockHomeAboutIntro = '一名热爱技术的前端开发者,专注于构建高性能、优雅的用户界面。相信代码不仅是工具,更是一种艺术表达。';
// Helper functions
export function getPinnedPost(): Post | null {
return mockPosts.find(p => p.pinned) || null;
}
export function getRecentPosts(limit: number = 5): Post[] {
return mockPosts.slice(0, limit);
}
export function getAllPosts(): Post[] {
return mockPosts;
}
export function getPostBySlug(slug: string): Post | undefined {
return mockPosts.find(p => p.slug === slug);
}
export function getPostsByTag(tag: string): Post[] {
return mockPosts.filter(p => p.tags.includes(tag));
}
export function getPostsByCategory(category: string): Post[] {
return mockPosts.filter(p => p.category === category);
}
export function getAllCategories(): Category[] {
return mockCategories;
}
export function getAllTags(): Tag[] {
return mockTags;
}
export function getAllFriendLinks(): FriendLink[] {
return mockFriendLinks;
}
export function getSiteConfig() {
return mockSiteConfig;
}
export function getSystemStats() {
return mockSystemStats;
}
export function getTechStack() {
return mockTechStack;
}
export function getHomeAboutIntro() {
return mockHomeAboutIntro;
}