feat: publish av-tools 0.1.0
This commit is contained in:
191
tests/unit/project/project-reducer-selectors.test.ts
Normal file
191
tests/unit/project/project-reducer-selectors.test.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
ProjectReducerError,
|
||||
projectReducer,
|
||||
} from '../../../src/project/project.reducer';
|
||||
import {
|
||||
selectCanExport,
|
||||
selectMissingAssets,
|
||||
selectTimelineDuration,
|
||||
selectTimelineEntries,
|
||||
selectUnusedAssets,
|
||||
} from '../../../src/project/project.selectors';
|
||||
import { createEmptyProject } from '../../../src/project/project.schema';
|
||||
import {
|
||||
CREATED_AT,
|
||||
UPDATED_AT,
|
||||
mediaAsset,
|
||||
populatedProject,
|
||||
} from './project-fixture';
|
||||
|
||||
describe('project reducer', () => {
|
||||
it('adds assets and sequential clips without mutating prior state', () => {
|
||||
const empty = createEmptyProject({
|
||||
id: 'p',
|
||||
timestamp: CREATED_AT,
|
||||
});
|
||||
const withAsset = projectReducer(empty, {
|
||||
type: 'asset/add',
|
||||
asset: mediaAsset(),
|
||||
updatedAt: UPDATED_AT,
|
||||
});
|
||||
const withClip = projectReducer(withAsset, {
|
||||
type: 'clip/add',
|
||||
clip: {
|
||||
id: 'clip-1',
|
||||
assetId: 'asset-1',
|
||||
sourceInSeconds: 0,
|
||||
sourceOutSeconds: 2,
|
||||
},
|
||||
updatedAt: UPDATED_AT,
|
||||
});
|
||||
|
||||
expect(empty.assets).toEqual([]);
|
||||
expect(withAsset.timeline).toEqual([]);
|
||||
expect(withClip.timeline).toHaveLength(1);
|
||||
expect(withClip.updatedAt).toBe(UPDATED_AT);
|
||||
});
|
||||
|
||||
it('reorders clips deterministically and returns identity for a no-op', () => {
|
||||
const project = {
|
||||
...populatedProject(),
|
||||
timeline: [
|
||||
populatedProject().timeline[0]!,
|
||||
{
|
||||
id: 'clip-2',
|
||||
assetId: 'asset-1',
|
||||
sourceInSeconds: 4.5,
|
||||
sourceOutSeconds: 6,
|
||||
},
|
||||
],
|
||||
};
|
||||
const moved = projectReducer(project, {
|
||||
type: 'clip/move',
|
||||
clipId: 'clip-2',
|
||||
toIndex: 0,
|
||||
updatedAt: UPDATED_AT,
|
||||
});
|
||||
expect(moved.timeline.map((clip) => clip.id)).toEqual(['clip-2', 'clip-1']);
|
||||
expect(
|
||||
projectReducer(moved, {
|
||||
type: 'clip/move',
|
||||
clipId: 'clip-2',
|
||||
toIndex: 0,
|
||||
updatedAt: UPDATED_AT,
|
||||
})
|
||||
).toBe(moved);
|
||||
});
|
||||
|
||||
it('removes dependent clips/subtitles and clears stale UI selection', () => {
|
||||
const project = {
|
||||
...populatedProject(),
|
||||
subtitles: [
|
||||
{ id: 'subtitle-1', assetId: 'asset-1', mode: 'mux' as const },
|
||||
],
|
||||
};
|
||||
const result = projectReducer(project, {
|
||||
type: 'asset/remove',
|
||||
assetId: 'asset-1',
|
||||
updatedAt: UPDATED_AT,
|
||||
});
|
||||
|
||||
expect(result.assets).toEqual([]);
|
||||
expect(result.timeline).toEqual([]);
|
||||
expect(result.subtitles).toEqual([]);
|
||||
expect(result.ui).toMatchObject({
|
||||
selectedAssetId: undefined,
|
||||
selectedClipId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('guards duplicate IDs, missing assets and invalid ranges', () => {
|
||||
const project = populatedProject();
|
||||
expect(() =>
|
||||
projectReducer(project, {
|
||||
type: 'asset/add',
|
||||
asset: mediaAsset(),
|
||||
updatedAt: UPDATED_AT,
|
||||
})
|
||||
).toThrow(ProjectReducerError);
|
||||
expect(() =>
|
||||
projectReducer(project, {
|
||||
type: 'clip/add',
|
||||
clip: {
|
||||
id: 'bad',
|
||||
assetId: 'missing',
|
||||
sourceInSeconds: 0,
|
||||
sourceOutSeconds: 1,
|
||||
},
|
||||
updatedAt: UPDATED_AT,
|
||||
})
|
||||
).toThrow('unknown asset');
|
||||
expect(() =>
|
||||
projectReducer(project, {
|
||||
type: 'clip/update',
|
||||
clipId: 'clip-1',
|
||||
changes: { sourceOutSeconds: 0 },
|
||||
updatedAt: UPDATED_AT,
|
||||
})
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it('does not mark the document updated for transient UI state', () => {
|
||||
const project = populatedProject();
|
||||
const result = projectReducer(project, {
|
||||
type: 'ui/set',
|
||||
ui: { inspectorPanel: 'output' },
|
||||
});
|
||||
expect(result.updatedAt).toBe(project.updatedAt);
|
||||
});
|
||||
});
|
||||
|
||||
describe('project selectors', () => {
|
||||
it('derives timeline positions and total duration for a sequential edit', () => {
|
||||
const project = {
|
||||
...populatedProject(),
|
||||
timeline: [
|
||||
populatedProject().timeline[0]!,
|
||||
{
|
||||
id: 'clip-2',
|
||||
assetId: 'asset-1',
|
||||
sourceInSeconds: 5,
|
||||
sourceOutSeconds: 6.25,
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(selectTimelineDuration(project)).toBe(4.75);
|
||||
expect(selectTimelineEntries(project)).toMatchObject([
|
||||
{
|
||||
timelineStartSeconds: 0,
|
||||
timelineEndSeconds: 3.5,
|
||||
durationSeconds: 3.5,
|
||||
},
|
||||
{
|
||||
timelineStartSeconds: 3.5,
|
||||
timelineEndSeconds: 4.75,
|
||||
durationSeconds: 1.25,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('identifies missing, unused and export-blocking sources', () => {
|
||||
const project = {
|
||||
...populatedProject(),
|
||||
assets: [
|
||||
mediaAsset({ sourceStatus: 'missing' }),
|
||||
mediaAsset({ id: 'asset-unused', originalName: 'unused.wav' }),
|
||||
],
|
||||
};
|
||||
expect(selectMissingAssets(project).map((asset) => asset.id)).toEqual([
|
||||
'asset-1',
|
||||
]);
|
||||
expect(selectUnusedAssets(project).map((asset) => asset.id)).toEqual([
|
||||
'asset-unused',
|
||||
]);
|
||||
expect(selectCanExport(project)).toEqual({
|
||||
canExport: false,
|
||||
reasons: ['1 referenced source file is missing.'],
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user