Files
Yoan Huang f648b59e85 refactor: update PPT editing schema and tests to use element specifications instead of HTML
- Changed PPT editing actions to utilize structured element specifications for slide creation.
- Updated test cases to reflect the new element-based approach, ensuring proper validation and rendering.
- Enhanced error handling for out-of-bounds elements in the canvas.
- Revised descriptions and assertions in tool contracts to maintain clarity and enforce capability boundaries.
- Improved snapshot tests to align with the new schema definitions and element structures.
2026-08-29 10:50:11 +08:00

219 lines
6.9 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
buildPptDiffManifest,
diffPptElements,
resolveMovedPptSlideIds
} from '../../../../src/main/services/ai/human/tools/ppt/diff'
import type {
PptDiffElementSnapshot,
PptDiffSlideSnapshot
} from '../../../../src/main/services/ai/human/tools/ppt/types'
const element = (elementId: string, fingerprint: string, zIndex = 0): PptDiffElementSnapshot => ({
elementId,
fingerprint,
zIndex
})
const slide = (
slideId: string,
index: number,
elements: PptDiffElementSnapshot[] = [],
pageFingerprint = 'page'
): PptDiffSlideSnapshot => ({ slideId, index, elements, pageFingerprint })
const manifest = (input: {
before: PptDiffSlideSnapshot[]
after: PptDiffSlideSnapshot[]
movedCandidates?: Set<string>
movedElementCandidates?: Set<string>
lineage?: Map<string, string>
}) =>
buildPptDiffManifest({
beforeSlides: input.before,
afterSlides: input.after,
beforeFrames: new Map(),
afterFrames: new Map(),
operation: 'updated',
movedCandidates: input.movedCandidates,
movedElementCandidates: input.movedElementCandidates,
elementLineage: input.lineage
})
describe('ppt net diff', () => {
it('does not mark later slides as moved after middle insertion or deletion', () => {
expect(
resolveMovedPptSlideIds({
beforeSlideIds: ['A', 'B', 'C', 'D'],
afterSlideIds: ['A', 'C', 'D']
})
).toEqual(new Set())
expect(
resolveMovedPptSlideIds({
beforeSlideIds: ['A', 'B', 'C', 'D'],
afterSlideIds: ['A', 'X', 'B', 'C', 'D']
})
).toEqual(new Set())
})
it('marks only the explicitly moved slide and collapses a move restored to baseline', () => {
expect(
resolveMovedPptSlideIds({
beforeSlideIds: ['A', 'B', 'C', 'D'],
afterSlideIds: ['B', 'C', 'A', 'D'],
movedCandidates: new Set(['A'])
})
).toEqual(new Set(['A']))
expect(
resolveMovedPptSlideIds({
beforeSlideIds: ['A', 'B', 'C'],
afterSlideIds: ['A', 'B', 'C'],
movedCandidates: new Set(['A'])
})
).toEqual(new Set())
})
it('uses stable ids, replacement lineage, and exact fingerprints for element matching', () => {
expect(
diffPptElements({
before: [element('same', 'A'), element('formula-old', 'B'), element('renumbered-old', 'C')],
after: [
element('same', 'A2'),
element('formula-new', 'B2'),
element('renumbered-new', 'C')
],
lineage: new Map([['formula-old', 'formula-new']])
})
).toEqual([
{ kind: 'modified', beforeElementId: 'same', afterElementId: 'same' },
{
kind: 'modified',
beforeElementId: 'formula-old',
afterElementId: 'formula-new'
}
])
})
it('counts element additions, deletions, modifications, and slide movement as net changes', () => {
const result = manifest({
before: [slide('A', 1, [element('keep', 'old'), element('delete', 'old')])],
after: [slide('A', 1, [element('keep', 'new'), element('add', 'new')])],
movedCandidates: new Set(['A'])
})
expect(result.slides[0]).toMatchObject({
changes: ['modified'],
positiveCount: 2,
negativeCount: 1
})
expect({ positive: result.positiveCount, negative: result.negativeCount }).toEqual({
positive: 2,
negative: 1
})
const movedAndModified = manifest({
before: [slide('A', 1, [element('one', 'old'), element('two', 'old')]), slide('B', 2)],
after: [slide('B', 1), slide('A', 2, [element('one', 'new'), element('two', 'new')])],
movedCandidates: new Set(['A'])
})
expect(movedAndModified.slides.find((item) => item.slideId === 'A')).toMatchObject({
changes: ['modified', 'moved'],
positiveCount: 3,
negativeCount: 0
})
})
it('collapses restored edits and temporary elements to zero net changes', () => {
const result = manifest({
before: [slide('A', 1, [element('one', 'baseline')])],
after: [slide('A', 1, [element('one', 'baseline')])],
movedCandidates: new Set(['A'])
})
expect(result.slides[0]).toMatchObject({
changed: false,
changes: [],
elementChanges: [],
positiveCount: 0,
negativeCount: 0
})
expect(result.positiveCount + result.negativeCount).toBe(0)
})
it('counts page-level background or notes fingerprints once and collapses restoration', () => {
const changed = manifest({
before: [slide('A', 1, [], 'page-before')],
after: [slide('A', 1, [], 'page-after')]
})
expect(changed.slides[0]).toMatchObject({
changes: ['modified'],
positiveCount: 1,
negativeCount: 0
})
const restored = manifest({
before: [slide('A', 1, [], 'same-page')],
after: [slide('A', 1, [], 'same-page')]
})
expect(restored.positiveCount + restored.negativeCount).toBe(0)
})
it('counts only the element explicitly moved in z-order and ignores natural renumbering', () => {
expect(
diffPptElements({
before: [element('A', 'same', 0), element('B', 'same', 1), element('C', 'same', 2)],
after: [element('B', 'same', 0), element('C', 'same', 1), element('A', 'same', 2)],
movedCandidates: new Set(['A'])
})
).toEqual([{ kind: 'modified', beforeElementId: 'A', afterElementId: 'A' }])
expect(
diffPptElements({
before: [element('A', 'same', 0), element('B', 'same', 1), element('C', 'same', 2)],
after: [element('A', 'same', 0), element('C', 'same', 1)]
})
).toEqual([{ kind: 'deleted', beforeElementId: 'B' }])
})
it('subsumes child elements when an entire slide is added or deleted', () => {
const result = manifest({
before: [slide('deleted', 1, [element('old-1', 'A'), element('old-2', 'B')])],
after: [slide('added', 1, [element('new-1', 'A'), element('new-2', 'B')])]
})
expect(
result.slides.map((item) => [item.changes, item.positiveCount, item.negativeCount])
).toEqual([
[['deleted'], 0, 1],
[['added'], 1, 0]
])
})
it('counts replaced slides as modified with all old elements deleted and all new elements added', () => {
const result = buildPptDiffManifest({
beforeSlides: [slide('A', 1, [element('old-1', 'same-fp'), element('old-2', 'other-fp')])],
afterSlides: [
slide('A', 1, [element('new-1', 'same-fp'), element('new-2', 'fresh'), element('new-3', 'x')])
],
beforeFrames: new Map(),
afterFrames: new Map(),
operation: 'updated',
replacedSlideIds: new Set(['A'])
})
expect(result.slides[0]).toMatchObject({
changed: true,
changes: ['modified'],
positiveCount: 3,
negativeCount: 2
})
expect(result.slides[0]?.elementChanges.map((change) => change.kind).sort()).toEqual([
'added',
'added',
'added',
'deleted',
'deleted'
])
expect(result.slides[0]?.elementChanges.some((change) => change.kind === 'modified')).toBe(
false
)
})
})