303 lines
7.9 KiB
TypeScript
303 lines
7.9 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { articles as fallbackArticles } from '@/data/blog';
|
|
|
|
type FetchMock = ReturnType<typeof vi.fn>;
|
|
|
|
const encodedSlug = '%e6%b5%8b%e8%af%95-milkdown';
|
|
const decodedSlug = '测试-milkdown';
|
|
|
|
const publicArticle = {
|
|
authorName: '作者',
|
|
categoriesResolved: [
|
|
{
|
|
count: 2,
|
|
id: 10,
|
|
name: '技术',
|
|
slug: 'tech',
|
|
},
|
|
],
|
|
comment_status: 'open',
|
|
contentHtml: '<h2>标题</h2><p>正文 & Milkdown</p>',
|
|
cover: 'https://img.demo/cover.jpg',
|
|
date: '2026-06-05T10:20:30',
|
|
excerptText: '摘要',
|
|
id: 100,
|
|
slug: encodedSlug,
|
|
tagsResolved: [
|
|
{
|
|
count: 1,
|
|
id: 20,
|
|
name: 'Milkdown',
|
|
slug: 'milkdown',
|
|
},
|
|
],
|
|
title: {
|
|
rendered: 'Milkdown & Markdown',
|
|
},
|
|
};
|
|
|
|
describe('useBlogArticles', () => {
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
vi.resetModules();
|
|
});
|
|
|
|
it('loads and normalizes public blog articles for Blog Web views', async () => {
|
|
const fetchMock = mockFetch([
|
|
{
|
|
body: {
|
|
code: 200,
|
|
data: {
|
|
list: [publicArticle],
|
|
total: 1,
|
|
},
|
|
},
|
|
status: 200,
|
|
},
|
|
]);
|
|
const { useBlogArticles } = await import('@/hooks/useBlogArticles');
|
|
const blogArticles = useBlogArticles();
|
|
|
|
await blogArticles.loadArticles();
|
|
|
|
const [article] = blogArticles.articles.value;
|
|
const listRequest = fetchMock.mock.calls[0]?.[0];
|
|
expect(listRequest).toBeDefined();
|
|
const requestUrl = new URL(`${listRequest}`);
|
|
|
|
expect(requestUrl.pathname).toBe('/api/blog/article/public/list');
|
|
expect(requestUrl.searchParams.get('pageNo')).toBe('1');
|
|
expect(requestUrl.searchParams.get('pageSize')).toBe('50');
|
|
expect(article).toMatchObject({
|
|
author: '作者',
|
|
category: '技术',
|
|
categorySlug: 'tech',
|
|
content: ['标题 正文 & Milkdown'],
|
|
contentHtml: '<h2>标题</h2><p>正文 & Milkdown</p>',
|
|
cover: 'https://img.demo/cover.jpg',
|
|
date: '2026-06-05 10:20',
|
|
excerpt: '摘要',
|
|
headings: [
|
|
{
|
|
id: 'header-id-1',
|
|
level: 2,
|
|
text: '标题',
|
|
},
|
|
],
|
|
readTime: '1 分钟',
|
|
slug: decodedSlug,
|
|
tags: ['Milkdown'],
|
|
title: 'Milkdown & Markdown',
|
|
});
|
|
expect(blogArticles.categories.value).toEqual([
|
|
{
|
|
color: 'blue',
|
|
count: 1,
|
|
description: '技术 分类下的文章。',
|
|
label: '技术',
|
|
slug: 'tech',
|
|
},
|
|
]);
|
|
expect(blogArticles.tags.value).toEqual([
|
|
{
|
|
color: 'blue',
|
|
count: 1,
|
|
label: 'Milkdown',
|
|
slug: 'milkdown',
|
|
},
|
|
]);
|
|
expect(blogArticles.searchArticles('正文')[0]?.slug).toBe(decodedSlug);
|
|
expect(blogArticles.loadedFromApi.value).toBe(true);
|
|
});
|
|
|
|
it('falls back to static articles when blog public list is unavailable', async () => {
|
|
mockFetch([
|
|
{
|
|
body: {
|
|
code: 502,
|
|
msg: '博客文章接口不可用',
|
|
},
|
|
status: 502,
|
|
},
|
|
]);
|
|
const { useBlogArticles } = await import('@/hooks/useBlogArticles');
|
|
const blogArticles = useBlogArticles();
|
|
|
|
await blogArticles.loadArticles();
|
|
|
|
expect(blogArticles.loadedFromApi.value).toBe(false);
|
|
expect(blogArticles.articles.value).toEqual(fallbackArticles);
|
|
});
|
|
|
|
it('falls back to static articles when blog public list is reachable but has no articles', async () => {
|
|
mockFetch([
|
|
{
|
|
body: {
|
|
code: 200,
|
|
data: {
|
|
list: [],
|
|
total: 0,
|
|
},
|
|
},
|
|
status: 200,
|
|
},
|
|
]);
|
|
const { useBlogArticles } = await import('@/hooks/useBlogArticles');
|
|
const blogArticles = useBlogArticles();
|
|
|
|
await blogArticles.loadArticles();
|
|
|
|
expect(blogArticles.loadedFromApi.value).toBe(false);
|
|
expect(blogArticles.articles.value).toEqual(fallbackArticles);
|
|
});
|
|
|
|
it('keeps fallback article covers on previous blog assets instead of Argon demo images', async () => {
|
|
expect(fallbackArticles.map((article) => article.cover)).not.toContain('/argon/theme/landing.jpg');
|
|
expect(fallbackArticles.map((article) => article.cover)).not.toContain('/argon/theme/img-2-1200x1000.jpg');
|
|
expect(fallbackArticles.map((article) => article.cover)).not.toContain('/argon/theme/img-1-1200x1000.jpg');
|
|
expect(fallbackArticles.map((article) => article.cover)).not.toContain('/argon/theme/promo-1.png');
|
|
expect(fallbackArticles.every((article) => article.cover.startsWith('https://s3.kwitsukasa.top/images/'))).toBe(true);
|
|
});
|
|
|
|
it('uses the previous blog cover when public API articles have no cover', async () => {
|
|
mockFetch([
|
|
{
|
|
body: {
|
|
code: 200,
|
|
data: {
|
|
list: [
|
|
{
|
|
...publicArticle,
|
|
cover: '',
|
|
},
|
|
],
|
|
total: 1,
|
|
},
|
|
},
|
|
status: 200,
|
|
},
|
|
]);
|
|
const { useBlogArticles } = await import('@/hooks/useBlogArticles');
|
|
const blogArticles = useBlogArticles();
|
|
|
|
await blogArticles.loadArticles();
|
|
|
|
expect(blogArticles.articles.value[0]?.cover).toBe('https://s3.kwitsukasa.top/images/bg-冬滚滚.png');
|
|
});
|
|
|
|
it('fetches public article detail when cached list data does not include html content', async () => {
|
|
const fetchMock = mockFetch([
|
|
{
|
|
body: {
|
|
code: 200,
|
|
data: {
|
|
list: [
|
|
{
|
|
...publicArticle,
|
|
contentHtml: '',
|
|
},
|
|
],
|
|
total: 1,
|
|
},
|
|
},
|
|
status: 200,
|
|
},
|
|
{
|
|
body: {
|
|
code: 200,
|
|
data: {
|
|
...publicArticle,
|
|
contentHtml: '<h1>详情标题</h1><p>详情正文</p>',
|
|
},
|
|
},
|
|
status: 200,
|
|
},
|
|
]);
|
|
const { useBlogArticles } = await import('@/hooks/useBlogArticles');
|
|
const blogArticles = useBlogArticles();
|
|
|
|
const article = await blogArticles.loadArticle(encodedSlug);
|
|
const detailRequest = fetchMock.mock.calls[1]?.[0];
|
|
expect(detailRequest).toBeDefined();
|
|
const detailUrl = new URL(`${detailRequest}`);
|
|
|
|
expect(detailUrl.pathname).toBe('/api/blog/article/public/detail');
|
|
expect(detailUrl.searchParams.get('slug')).toBe(decodedSlug);
|
|
expect(article?.contentHtml).toBe('<h1>详情标题</h1><p>详情正文</p>');
|
|
expect(article?.headings).toEqual([
|
|
{
|
|
id: 'header-id-1',
|
|
level: 1,
|
|
text: '详情标题',
|
|
},
|
|
]);
|
|
expect(blogArticles.getArticleBySlug(decodedSlug)?.content).toEqual([
|
|
'详情标题 详情正文',
|
|
]);
|
|
});
|
|
|
|
it('derives article catalog headings from public API html when heading metadata is absent', async () => {
|
|
mockFetch([
|
|
{
|
|
body: {
|
|
code: 200,
|
|
data: {
|
|
list: [
|
|
{
|
|
...publicArticle,
|
|
contentHtml: '<h2 id="intro">介绍</h2><p>正文</p><h3>细节</h3>',
|
|
},
|
|
],
|
|
total: 1,
|
|
},
|
|
},
|
|
status: 200,
|
|
},
|
|
]);
|
|
const { useBlogArticles } = await import('@/hooks/useBlogArticles');
|
|
const blogArticles = useBlogArticles();
|
|
|
|
await blogArticles.loadArticles();
|
|
|
|
expect(blogArticles.articles.value[0]?.headings).toEqual([
|
|
{
|
|
id: 'intro',
|
|
level: 2,
|
|
text: '介绍',
|
|
},
|
|
{
|
|
id: 'header-id-2',
|
|
level: 3,
|
|
text: '细节',
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
function mockFetch(
|
|
responses: Array<{
|
|
body: unknown;
|
|
status: number;
|
|
}>,
|
|
) {
|
|
const fetchMock: FetchMock = vi.fn(async () => {
|
|
const response = responses.shift();
|
|
|
|
if (!response) {
|
|
throw new Error('没有更多 mock response');
|
|
}
|
|
|
|
return new Response(JSON.stringify(response.body), {
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
status: response.status,
|
|
});
|
|
});
|
|
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
|
|
return fetchMock;
|
|
}
|