Feat/webworker for incremental render (#51)
* feat: web worker for incremental render * feat: optimizing incremental render for date archives * feat: more work on web worker * fix: blogmark process handled defaulting wrong --------- Co-authored-by: hugo <hugoms@me.com>
This commit is contained in:
352
tests/engine/ApplyValidationWorkerService.test.ts
Normal file
352
tests/engine/ApplyValidationWorkerService.test.ts
Normal file
@@ -0,0 +1,352 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
buildApplyValidationWorkerTasks,
|
||||
type ApplyValidationWorkerParams,
|
||||
type ApplyValidationLanguageParams,
|
||||
} from '../../src/main/engine/ApplyValidationWorkerService';
|
||||
import type { PostData } from '../../src/main/engine/PostEngine';
|
||||
import { buildGenerationPostIndex } from '../../src/main/engine/GenerationPostIndexService';
|
||||
import type { TargetedValidationPlan } from '../../src/main/engine/ValidationApplyPlannerService';
|
||||
import type { SerializedBlogGenerationOptions } from '../../src/main/engine/GenerationWorkerData';
|
||||
|
||||
function makePost(overrides: Partial<PostData> = {}): PostData {
|
||||
const createdAt = overrides.createdAt ?? new Date('2025-01-15T10:00:00Z');
|
||||
return {
|
||||
id: overrides.id ?? 'post-1',
|
||||
projectId: 'test',
|
||||
title: overrides.title ?? 'Test Post',
|
||||
slug: overrides.slug ?? 'test-post',
|
||||
content: overrides.content ?? '# Test',
|
||||
status: 'published',
|
||||
createdAt,
|
||||
updatedAt: overrides.updatedAt ?? createdAt,
|
||||
publishedAt: createdAt,
|
||||
tags: overrides.tags ?? [],
|
||||
categories: overrides.categories ?? [],
|
||||
availableLanguages: overrides.availableLanguages ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
function makeBaseParams(overrides?: Partial<ApplyValidationWorkerParams>): ApplyValidationWorkerParams {
|
||||
const options: SerializedBlogGenerationOptions = {
|
||||
projectId: 'test',
|
||||
projectName: 'Test Blog',
|
||||
dataDir: '/data',
|
||||
baseUrl: 'https://example.com',
|
||||
};
|
||||
|
||||
return {
|
||||
options,
|
||||
maxPostsPerPage: 50,
|
||||
htmlDir: '/data/html',
|
||||
mediaItems: [],
|
||||
backlinksRecord: {},
|
||||
hashMapEntries: [],
|
||||
postFilePathEntries: [],
|
||||
postMediaLinksEntries: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makeEmptyTargetedPlan(overrides?: Partial<TargetedValidationPlan>): TargetedValidationPlan {
|
||||
return {
|
||||
requestedPostIds: new Set(),
|
||||
requestedCategorySet: new Set(),
|
||||
requestedTagSet: new Set(),
|
||||
requestedYears: new Set(),
|
||||
requestedYearMonths: new Set(),
|
||||
requestedYearMonthDays: new Set(),
|
||||
requestedPageSlugs: new Set(),
|
||||
requestRootRoutes: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('buildApplyValidationWorkerTasks', () => {
|
||||
it('returns empty tasks when plan has no requests', () => {
|
||||
const posts = [makePost()];
|
||||
const postIndex = buildGenerationPostIndex(posts);
|
||||
|
||||
const tasks = buildApplyValidationWorkerTasks(
|
||||
makeBaseParams(),
|
||||
{
|
||||
targetedPlan: makeEmptyTargetedPlan(),
|
||||
publishedRoutePosts: posts,
|
||||
publishedListPosts: posts,
|
||||
generationPostIndex: postIndex,
|
||||
years: new Map(),
|
||||
yearMonths: new Map(),
|
||||
yearMonthDays: new Map(),
|
||||
},
|
||||
);
|
||||
|
||||
expect(tasks).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('creates a single-post task containing only requested post IDs', () => {
|
||||
const post1 = makePost({ id: 'p1', slug: 'post-one', tags: ['alpha'], categories: ['news'] });
|
||||
const post2 = makePost({ id: 'p2', slug: 'post-two', tags: ['beta'], categories: ['other'] });
|
||||
const posts = [post1, post2];
|
||||
const postIndex = buildGenerationPostIndex(posts);
|
||||
|
||||
const tasks = buildApplyValidationWorkerTasks(
|
||||
makeBaseParams(),
|
||||
{
|
||||
targetedPlan: makeEmptyTargetedPlan({
|
||||
requestedPostIds: new Set(['p1']),
|
||||
}),
|
||||
publishedRoutePosts: posts,
|
||||
publishedListPosts: posts,
|
||||
generationPostIndex: postIndex,
|
||||
years: new Map(),
|
||||
yearMonths: new Map(),
|
||||
yearMonthDays: new Map(),
|
||||
},
|
||||
);
|
||||
|
||||
const singleTasks = tasks.filter((t) => t.section === 'single');
|
||||
expect(singleTasks).toHaveLength(1);
|
||||
expect(singleTasks[0].posts).toHaveLength(1);
|
||||
expect(singleTasks[0].posts[0].id).toBe('p1');
|
||||
});
|
||||
|
||||
it('creates a category task containing only requested categories', () => {
|
||||
const post1 = makePost({ id: 'p1', categories: ['news'], tags: [] });
|
||||
const post2 = makePost({ id: 'p2', categories: ['other'], tags: [] });
|
||||
const posts = [post1, post2];
|
||||
const postIndex = buildGenerationPostIndex(posts);
|
||||
|
||||
const tasks = buildApplyValidationWorkerTasks(
|
||||
makeBaseParams(),
|
||||
{
|
||||
targetedPlan: makeEmptyTargetedPlan({
|
||||
requestedCategorySet: new Set(['news']),
|
||||
}),
|
||||
publishedRoutePosts: posts,
|
||||
publishedListPosts: posts,
|
||||
generationPostIndex: postIndex,
|
||||
years: new Map(),
|
||||
yearMonths: new Map(),
|
||||
yearMonthDays: new Map(),
|
||||
},
|
||||
);
|
||||
|
||||
const catTasks = tasks.filter((t) => t.section === 'category');
|
||||
expect(catTasks).toHaveLength(1);
|
||||
expect(catTasks[0].allCategories).toEqual(['news']);
|
||||
expect(catTasks[0].postsByCategoryEntries).toHaveLength(1);
|
||||
expect(catTasks[0].postsByCategoryEntries![0][0]).toBe('news');
|
||||
});
|
||||
|
||||
it('creates a tag task containing only requested tags', () => {
|
||||
const post1 = makePost({ id: 'p1', tags: ['alpha'] });
|
||||
const post2 = makePost({ id: 'p2', tags: ['beta'] });
|
||||
const posts = [post1, post2];
|
||||
const postIndex = buildGenerationPostIndex(posts);
|
||||
|
||||
const tasks = buildApplyValidationWorkerTasks(
|
||||
makeBaseParams(),
|
||||
{
|
||||
targetedPlan: makeEmptyTargetedPlan({
|
||||
requestedTagSet: new Set(['alpha']),
|
||||
}),
|
||||
publishedRoutePosts: posts,
|
||||
publishedListPosts: posts,
|
||||
generationPostIndex: postIndex,
|
||||
years: new Map(),
|
||||
yearMonths: new Map(),
|
||||
yearMonthDays: new Map(),
|
||||
},
|
||||
);
|
||||
|
||||
const tagTasks = tasks.filter((t) => t.section === 'tag');
|
||||
expect(tagTasks).toHaveLength(1);
|
||||
expect(tagTasks[0].allTags).toEqual(['alpha']);
|
||||
expect(tagTasks[0].postsByTagEntries).toHaveLength(1);
|
||||
expect(tagTasks[0].postsByTagEntries![0][0]).toBe('alpha');
|
||||
});
|
||||
|
||||
it('creates a date task containing only requested year/month/day archives', () => {
|
||||
const post1 = makePost({ id: 'p1', createdAt: new Date('2025-01-15T10:00:00Z') });
|
||||
const post2 = makePost({ id: 'p2', createdAt: new Date('2024-06-20T10:00:00Z') });
|
||||
const posts = [post1, post2];
|
||||
const postIndex = buildGenerationPostIndex(posts);
|
||||
|
||||
const years = new Map<number, Date>([[2025, new Date()], [2024, new Date()]]);
|
||||
const yearMonths = new Map<string, Date>([['2025/01', new Date()], ['2024/06', new Date()]]);
|
||||
const yearMonthDays = new Map<string, Date>([['2025/01/15', new Date()], ['2024/06/20', new Date()]]);
|
||||
|
||||
const tasks = buildApplyValidationWorkerTasks(
|
||||
makeBaseParams(),
|
||||
{
|
||||
targetedPlan: makeEmptyTargetedPlan({
|
||||
requestedYears: new Set([2025]),
|
||||
requestedYearMonths: new Set(['2025/01']),
|
||||
requestedYearMonthDays: new Set(['2025/01/15']),
|
||||
}),
|
||||
publishedRoutePosts: posts,
|
||||
publishedListPosts: posts,
|
||||
generationPostIndex: postIndex,
|
||||
years,
|
||||
yearMonths,
|
||||
yearMonthDays,
|
||||
},
|
||||
);
|
||||
|
||||
const dateTasks = tasks.filter((t) => t.section === 'date');
|
||||
expect(dateTasks).toHaveLength(1);
|
||||
expect(dateTasks[0].yearsEntries).toHaveLength(1);
|
||||
expect(dateTasks[0].yearsEntries![0][0]).toBe(2025);
|
||||
expect(dateTasks[0].yearMonthsEntries).toHaveLength(1);
|
||||
expect(dateTasks[0].yearMonthsEntries![0][0]).toBe('2025/01');
|
||||
expect(dateTasks[0].yearMonthDaysEntries).toHaveLength(1);
|
||||
expect(dateTasks[0].yearMonthDaysEntries![0][0]).toBe('2025/01/15');
|
||||
});
|
||||
|
||||
it('creates a core task when requestRootRoutes is true', () => {
|
||||
const posts = [makePost()];
|
||||
const postIndex = buildGenerationPostIndex(posts);
|
||||
|
||||
const tasks = buildApplyValidationWorkerTasks(
|
||||
makeBaseParams(),
|
||||
{
|
||||
targetedPlan: makeEmptyTargetedPlan({
|
||||
requestRootRoutes: true,
|
||||
}),
|
||||
publishedRoutePosts: posts,
|
||||
publishedListPosts: posts,
|
||||
generationPostIndex: postIndex,
|
||||
years: new Map(),
|
||||
yearMonths: new Map(),
|
||||
yearMonthDays: new Map(),
|
||||
},
|
||||
);
|
||||
|
||||
const coreTasks = tasks.filter((t) => t.section === 'core');
|
||||
expect(coreTasks).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('does not create core task when requestRootRoutes is false', () => {
|
||||
const posts = [makePost()];
|
||||
const postIndex = buildGenerationPostIndex(posts);
|
||||
|
||||
const tasks = buildApplyValidationWorkerTasks(
|
||||
makeBaseParams(),
|
||||
{
|
||||
targetedPlan: makeEmptyTargetedPlan({
|
||||
requestRootRoutes: false,
|
||||
requestedPostIds: new Set(['post-1']),
|
||||
}),
|
||||
publishedRoutePosts: posts,
|
||||
publishedListPosts: posts,
|
||||
generationPostIndex: postIndex,
|
||||
years: new Map(),
|
||||
yearMonths: new Map(),
|
||||
yearMonthDays: new Map(),
|
||||
},
|
||||
);
|
||||
|
||||
expect(tasks.filter((t) => t.section === 'core')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('creates tasks across multiple sections for a comprehensive plan', () => {
|
||||
const post1 = makePost({
|
||||
id: 'p1', slug: 'target-post',
|
||||
categories: ['news'], tags: ['alpha'],
|
||||
createdAt: new Date('2025-01-15T10:00:00Z'),
|
||||
});
|
||||
const post2 = makePost({
|
||||
id: 'p2', slug: 'other-post',
|
||||
categories: ['other'], tags: ['beta'],
|
||||
createdAt: new Date('2024-06-20T10:00:00Z'),
|
||||
});
|
||||
const posts = [post1, post2];
|
||||
const postIndex = buildGenerationPostIndex(posts);
|
||||
|
||||
const years = new Map<number, Date>([[2025, new Date()]]);
|
||||
const yearMonths = new Map<string, Date>([['2025/01', new Date()]]);
|
||||
const yearMonthDays = new Map<string, Date>([['2025/01/15', new Date()]]);
|
||||
|
||||
const tasks = buildApplyValidationWorkerTasks(
|
||||
makeBaseParams(),
|
||||
{
|
||||
targetedPlan: makeEmptyTargetedPlan({
|
||||
requestRootRoutes: true,
|
||||
requestedPostIds: new Set(['p1']),
|
||||
requestedCategorySet: new Set(['news']),
|
||||
requestedTagSet: new Set(['alpha']),
|
||||
requestedYears: new Set([2025]),
|
||||
requestedYearMonths: new Set(['2025/01']),
|
||||
requestedYearMonthDays: new Set(['2025/01/15']),
|
||||
}),
|
||||
publishedRoutePosts: posts,
|
||||
publishedListPosts: posts,
|
||||
generationPostIndex: postIndex,
|
||||
years,
|
||||
yearMonths,
|
||||
yearMonthDays,
|
||||
},
|
||||
);
|
||||
|
||||
expect(tasks.filter((t) => t.section === 'core')).toHaveLength(1);
|
||||
expect(tasks.filter((t) => t.section === 'single')).toHaveLength(1);
|
||||
expect(tasks.filter((t) => t.section === 'category')).toHaveLength(1);
|
||||
expect(tasks.filter((t) => t.section === 'tag')).toHaveLength(1);
|
||||
expect(tasks.filter((t) => t.section === 'date')).toHaveLength(1);
|
||||
expect(tasks).toHaveLength(5);
|
||||
});
|
||||
|
||||
it('sets language prefix on tasks for language subtree rendering', () => {
|
||||
const posts = [makePost({ id: 'p1', tags: ['alpha'] })];
|
||||
const postIndex = buildGenerationPostIndex(posts);
|
||||
|
||||
const tasks = buildApplyValidationWorkerTasks(
|
||||
makeBaseParams(),
|
||||
{
|
||||
targetedPlan: makeEmptyTargetedPlan({
|
||||
requestedTagSet: new Set(['alpha']),
|
||||
}),
|
||||
publishedRoutePosts: posts,
|
||||
publishedListPosts: posts,
|
||||
generationPostIndex: postIndex,
|
||||
years: new Map(),
|
||||
yearMonths: new Map(),
|
||||
yearMonthDays: new Map(),
|
||||
languagePrefix: '/fr',
|
||||
mainLanguage: 'en',
|
||||
},
|
||||
);
|
||||
|
||||
expect(tasks).toHaveLength(1);
|
||||
expect(tasks[0].languagePrefix).toBe('/fr');
|
||||
expect(tasks[0].mainLanguage).toBe('en');
|
||||
expect(tasks[0].options.language).toBe('fr');
|
||||
});
|
||||
|
||||
it('uses all route posts as lookupPosts regardless of targeted plan', () => {
|
||||
const post1 = makePost({ id: 'p1', slug: 'target', tags: ['alpha'] });
|
||||
const post2 = makePost({ id: 'p2', slug: 'other', tags: ['beta'] });
|
||||
const posts = [post1, post2];
|
||||
const postIndex = buildGenerationPostIndex(posts);
|
||||
|
||||
const tasks = buildApplyValidationWorkerTasks(
|
||||
makeBaseParams(),
|
||||
{
|
||||
targetedPlan: makeEmptyTargetedPlan({
|
||||
requestedPostIds: new Set(['p1']),
|
||||
}),
|
||||
publishedRoutePosts: posts,
|
||||
publishedListPosts: posts,
|
||||
generationPostIndex: postIndex,
|
||||
years: new Map(),
|
||||
yearMonths: new Map(),
|
||||
yearMonthDays: new Map(),
|
||||
},
|
||||
);
|
||||
|
||||
// Single task has only 1 post to render...
|
||||
expect(tasks[0].posts).toHaveLength(1);
|
||||
// ...but lookupPosts contains ALL posts for slug resolution
|
||||
expect(tasks[0].lookupPosts).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user