182 lines
6.4 KiB
TypeScript
182 lines
6.4 KiB
TypeScript
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
accessDocument: vi.fn(),
|
|
collectVideoAssets: vi.fn(),
|
|
compileVideoTimeline: vi.fn(),
|
|
createVideoTimelineDeps: vi.fn(),
|
|
createQuizLayoutProbe: vi.fn(),
|
|
emitHyperframes: vi.fn(),
|
|
packageVideoZip: vi.fn(),
|
|
stageState: vi.fn(),
|
|
toSrt: vi.fn(),
|
|
toVtt: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('@/lib/video-export', () => ({
|
|
compileVideoTimeline: mocks.compileVideoTimeline,
|
|
emitHyperframes: mocks.emitHyperframes,
|
|
toSrt: mocks.toSrt,
|
|
toVtt: mocks.toVtt,
|
|
}));
|
|
vi.mock('@/lib/store', () => ({
|
|
useStageStore: { getState: mocks.stageState },
|
|
}));
|
|
vi.mock('@/lib/document-store', () => ({
|
|
accessDocument: mocks.accessDocument,
|
|
}));
|
|
vi.mock('@/lib/video-export-app/timeline-deps', () => ({
|
|
createVideoTimelineDeps: mocks.createVideoTimelineDeps,
|
|
}));
|
|
vi.mock('@/lib/video-export-app/quiz-layout', () => ({
|
|
createQuizLayoutProbe: mocks.createQuizLayoutProbe,
|
|
}));
|
|
vi.mock('@/lib/video-export-app/collect', () => ({
|
|
collectVideoAssets: mocks.collectVideoAssets,
|
|
}));
|
|
vi.mock('@/lib/video-export-app/package-zip', () => ({
|
|
packageVideoZip: mocks.packageVideoZip,
|
|
}));
|
|
|
|
import { buildExportZip, compileSubtitles } from '@/lib/video-export-app/build-export-zip';
|
|
import { getVideoExportCoverLabels } from '@/lib/video-export-app/cover-config';
|
|
|
|
const ENV_KEY = 'NEXT_PUBLIC_VIDEO_EXPORT_CTA_DESTINATION';
|
|
const originalEnv = process.env[ENV_KEY];
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mocks.stageState.mockReturnValue({
|
|
stage: { id: 'stage-1', name: 'Stage' },
|
|
scenes: [{ id: 'scene-1' }],
|
|
});
|
|
mocks.createVideoTimelineDeps.mockResolvedValue({
|
|
timing: {},
|
|
assets: {},
|
|
records: {},
|
|
});
|
|
mocks.createQuizLayoutProbe.mockResolvedValue({ measureQuestionList: vi.fn() });
|
|
mocks.compileVideoTimeline.mockReturnValue({ diagnostics: [] });
|
|
mocks.emitHyperframes.mockReturnValue({ files: [] });
|
|
mocks.collectVideoAssets.mockResolvedValue({ blobs: new Map(), missing: [] });
|
|
mocks.packageVideoZip.mockResolvedValue(new Blob(['zip']));
|
|
});
|
|
|
|
describe('compileSubtitles Quiz timing', () => {
|
|
it('uses the selected resolution/locale Quiz probe while skipping slide geometry only', async () => {
|
|
mocks.accessDocument.mockResolvedValue(undefined);
|
|
const quizLayout = { measureQuestionList: vi.fn() };
|
|
mocks.createQuizLayoutProbe.mockResolvedValue(quizLayout);
|
|
mocks.compileVideoTimeline.mockReturnValue({ diagnostics: [], subtitles: [] });
|
|
|
|
await compileSubtitles({ resolution: '1080p', locale: 'en-US' });
|
|
|
|
expect(mocks.createVideoTimelineDeps).toHaveBeenCalledWith(
|
|
expect.objectContaining({ skipGeometry: true }),
|
|
);
|
|
expect(mocks.createQuizLayoutProbe).toHaveBeenCalledWith(
|
|
expect.objectContaining({ width: 1920, height: 1080, locale: 'en-US' }),
|
|
);
|
|
expect(mocks.compileVideoTimeline).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({ quizLayout }),
|
|
);
|
|
});
|
|
});
|
|
|
|
afterAll(() => {
|
|
if (originalEnv === undefined) delete process.env[ENV_KEY];
|
|
else process.env[ENV_KEY] = originalEnv;
|
|
});
|
|
|
|
describe('buildExportZip CTA boundary', () => {
|
|
it('does not expose a runtime diagnostics sidecar that cannot be updated after packaging', async () => {
|
|
mocks.accessDocument.mockResolvedValue(undefined);
|
|
|
|
const result = await buildExportZip({ resolution: '720p', locale: 'en-US' });
|
|
|
|
expect(result).not.toHaveProperty('runtimeDiagnosticsPath');
|
|
});
|
|
});
|
|
|
|
describe('buildExportZip app boundary', () => {
|
|
it('premeasures Quiz layout with the selected resolution/locale and injects the sync probe', async () => {
|
|
mocks.accessDocument.mockResolvedValue(undefined);
|
|
const quizLayout = { measureQuestionList: vi.fn() };
|
|
mocks.createQuizLayoutProbe.mockResolvedValue(quizLayout);
|
|
|
|
await buildExportZip({ resolution: '4k', locale: 'zh-CN' });
|
|
|
|
expect(mocks.createQuizLayoutProbe).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
scenes: [{ id: 'scene-1' }],
|
|
width: 3840,
|
|
height: 2160,
|
|
locale: 'zh-CN',
|
|
labels: getVideoExportCoverLabels('zh-CN'),
|
|
}),
|
|
);
|
|
expect(mocks.compileVideoTimeline).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({ quizLayout }),
|
|
);
|
|
});
|
|
|
|
it('freezes the configured CTA and complete locale labels before its first await', async () => {
|
|
let releaseDocument!: (value: unknown) => void;
|
|
mocks.accessDocument.mockReturnValue(
|
|
new Promise((resolve) => {
|
|
releaseDocument = resolve;
|
|
}),
|
|
);
|
|
process.env[ENV_KEY] = 'https://Courses.Example.com/start/';
|
|
|
|
const building = buildExportZip({ resolution: '720p', locale: 'en-US' });
|
|
process.env[ENV_KEY] = 'off';
|
|
releaseDocument({ document: { stage: { name: 'Resolved stage' } } });
|
|
await building;
|
|
|
|
expect(mocks.emitHyperframes).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
cta: { destination: 'courses.example.com/start' },
|
|
labels: expect.objectContaining({
|
|
quizCtaPrompt: 'Want to try an interactive quiz?',
|
|
pblCtaPrompt: 'Want to explore project-based learning?',
|
|
ctaVisit: 'Visit',
|
|
}),
|
|
locale: 'en-US',
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('does not warn for an explicitly disabled CTA', async () => {
|
|
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
mocks.accessDocument.mockResolvedValue(undefined);
|
|
process.env[ENV_KEY] = ' OFF ';
|
|
|
|
await buildExportZip({ resolution: '720p', locale: 'zh-CN' });
|
|
|
|
expect(warn).not.toHaveBeenCalled();
|
|
expect(mocks.emitHyperframes.mock.calls[0][1]).toMatchObject({ cta: null });
|
|
warn.mockRestore();
|
|
});
|
|
|
|
it('warns once per process for invalid environment values and disables the CTA', async () => {
|
|
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
mocks.accessDocument.mockResolvedValue(undefined);
|
|
process.env[ENV_KEY] = 'ftp://unsafe.example';
|
|
|
|
await buildExportZip({ resolution: '720p', locale: 'en-US' });
|
|
await buildExportZip({ resolution: '1080p', locale: 'en-US' });
|
|
|
|
expect(warn).toHaveBeenCalledTimes(1);
|
|
expect(warn).toHaveBeenCalledWith(
|
|
expect.stringContaining('NEXT_PUBLIC_VIDEO_EXPORT_CTA_DESTINATION'),
|
|
);
|
|
expect(mocks.emitHyperframes.mock.calls[0][1]).toMatchObject({ cta: null });
|
|
expect(mocks.emitHyperframes.mock.calls[1][1]).toMatchObject({ cta: null });
|
|
warn.mockRestore();
|
|
});
|
|
});
|