feat: added unit tests for timeUtil and xmlUtil.
This commit is contained in:
@@ -0,0 +1,316 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { parseTimeSignature, getTimeSignatureErrorMessage, beatsToTimeString, formatLocalDateTime } from './timeUtil'
|
||||
import { TIME_CONSTANTS } from '../constants/coreConstants'
|
||||
|
||||
describe('timeUtil', () => {
|
||||
describe('parseTimeSignature', () => {
|
||||
it('should parse valid time signatures', () => {
|
||||
expect(parseTimeSignature('4/4')).toEqual({ numerator: 4, denominator: 4 })
|
||||
expect(parseTimeSignature('3/4')).toEqual({ numerator: 3, denominator: 4 })
|
||||
expect(parseTimeSignature('6/8')).toEqual({ numerator: 6, denominator: 8 })
|
||||
expect(parseTimeSignature('12/8')).toEqual({ numerator: 12, denominator: 8 })
|
||||
expect(parseTimeSignature('2/4')).toEqual({ numerator: 2, denominator: 4 })
|
||||
})
|
||||
|
||||
it('should handle whitespace around input', () => {
|
||||
expect(parseTimeSignature(' 4/4 ')).toEqual({ numerator: 4, denominator: 4 })
|
||||
expect(parseTimeSignature(' 3/4 ')).toEqual({ numerator: 3, denominator: 4 })
|
||||
expect(parseTimeSignature('\t6/8\n')).toEqual({ numerator: 6, denominator: 8 })
|
||||
})
|
||||
|
||||
it('should return null for invalid formats', () => {
|
||||
expect(parseTimeSignature('4')).toBeNull()
|
||||
expect(parseTimeSignature('4/4/4')).toBeNull()
|
||||
expect(parseTimeSignature('4-4')).toBeNull()
|
||||
expect(parseTimeSignature('4:4')).toBeNull()
|
||||
expect(parseTimeSignature('')).toBeNull()
|
||||
expect(parseTimeSignature('/')).toBeNull()
|
||||
expect(parseTimeSignature('4/')).toBeNull()
|
||||
expect(parseTimeSignature('/4')).toBeNull()
|
||||
})
|
||||
|
||||
it('should return null for non-numeric values', () => {
|
||||
expect(parseTimeSignature('a/4')).toBeNull()
|
||||
expect(parseTimeSignature('4/b')).toBeNull()
|
||||
expect(parseTimeSignature('x/y')).toBeNull()
|
||||
// Note: parseInt('4.5') returns 4, so these will parse as integers
|
||||
// Testing the actual behavior of parseInt
|
||||
expect(parseTimeSignature('4.5/4')).toEqual({ numerator: 4, denominator: 4 })
|
||||
expect(parseTimeSignature('4/4.5')).toEqual({ numerator: 4, denominator: 4 })
|
||||
})
|
||||
|
||||
it('should return null for numerators not in available list', () => {
|
||||
// Assuming TIME_CONSTANTS has specific available numerators
|
||||
expect(parseTimeSignature('99/4')).toBeNull()
|
||||
expect(parseTimeSignature('0/4')).toBeNull()
|
||||
expect(parseTimeSignature('-1/4')).toBeNull()
|
||||
})
|
||||
|
||||
it('should return null for denominators not in available list', () => {
|
||||
// Assuming TIME_CONSTANTS has specific available denominators
|
||||
expect(parseTimeSignature('4/99')).toBeNull()
|
||||
expect(parseTimeSignature('4/0')).toBeNull()
|
||||
expect(parseTimeSignature('4/-1')).toBeNull()
|
||||
})
|
||||
|
||||
it('should validate against TIME_CONSTANTS available values', () => {
|
||||
// Test that function actually uses TIME_CONSTANTS for validation
|
||||
const validNumerator = TIME_CONSTANTS.AVAILABLE_TIME_SIGNATURE_NUMERATORS[0]
|
||||
const validDenominator = TIME_CONSTANTS.AVAILABLE_TIME_SIGNATURE_DENOMINATORS[0]
|
||||
const invalidNumerator = 999 // Assuming this is not in the available list
|
||||
const invalidDenominator = 999 // Assuming this is not in the available list
|
||||
|
||||
expect(parseTimeSignature(`${validNumerator}/${validDenominator}`)).not.toBeNull()
|
||||
expect(parseTimeSignature(`${invalidNumerator}/${validDenominator}`)).toBeNull()
|
||||
expect(parseTimeSignature(`${validNumerator}/${invalidDenominator}`)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getTimeSignatureErrorMessage', () => {
|
||||
it('should return a formatted error message with available options', () => {
|
||||
const message = getTimeSignatureErrorMessage()
|
||||
|
||||
expect(message).toContain('Invalid time signature format')
|
||||
expect(message).toContain('numerator/denominator')
|
||||
expect(message).toContain('Available numerators:')
|
||||
expect(message).toContain('Available denominators:')
|
||||
expect(message).toContain('Examples: 4/4, 3/4, 6/8, 12/8')
|
||||
})
|
||||
|
||||
it('should include actual available values from TIME_CONSTANTS', () => {
|
||||
const message = getTimeSignatureErrorMessage()
|
||||
|
||||
// Check that it includes values from TIME_CONSTANTS
|
||||
TIME_CONSTANTS.AVAILABLE_TIME_SIGNATURE_NUMERATORS.forEach(numerator => {
|
||||
expect(message).toContain(numerator.toString())
|
||||
})
|
||||
|
||||
TIME_CONSTANTS.AVAILABLE_TIME_SIGNATURE_DENOMINATORS.forEach(denominator => {
|
||||
expect(message).toContain(denominator.toString())
|
||||
})
|
||||
})
|
||||
|
||||
it('should be a consistent message format', () => {
|
||||
const message1 = getTimeSignatureErrorMessage()
|
||||
const message2 = getTimeSignatureErrorMessage()
|
||||
|
||||
expect(message1).toBe(message2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('beatsToTimeString', () => {
|
||||
it('should format beats to BBB:B | mm:ss:mmm format', () => {
|
||||
// Test basic 4/4 time signature
|
||||
expect(beatsToTimeString(0, 120, { numerator: 4, denominator: 4 }))
|
||||
.toBe('001:1 | 00:00:000')
|
||||
|
||||
expect(beatsToTimeString(4, 120, { numerator: 4, denominator: 4 }))
|
||||
.toBe('002:1 | 00:02:000')
|
||||
|
||||
expect(beatsToTimeString(8, 120, { numerator: 4, denominator: 4 }))
|
||||
.toBe('003:1 | 00:04:000')
|
||||
})
|
||||
|
||||
it('should handle different time signatures correctly', () => {
|
||||
// 3/4 time signature - 3 beats per bar
|
||||
expect(beatsToTimeString(0, 120, { numerator: 3, denominator: 4 }))
|
||||
.toBe('001:1 | 00:00:000')
|
||||
|
||||
expect(beatsToTimeString(3, 120, { numerator: 3, denominator: 4 }))
|
||||
.toBe('002:1 | 00:01:500')
|
||||
|
||||
expect(beatsToTimeString(6, 120, { numerator: 3, denominator: 4 }))
|
||||
.toBe('003:1 | 00:03:000')
|
||||
|
||||
// 6/8 time signature - 6 beats per bar
|
||||
expect(beatsToTimeString(0, 120, { numerator: 6, denominator: 8 }))
|
||||
.toBe('001:1 | 00:00:000')
|
||||
|
||||
expect(beatsToTimeString(6, 120, { numerator: 6, denominator: 8 }))
|
||||
.toBe('002:1 | 00:03:000')
|
||||
})
|
||||
|
||||
it('should handle different BPM values correctly', () => {
|
||||
// 60 BPM - 1 beat per second
|
||||
expect(beatsToTimeString(1, 60, { numerator: 4, denominator: 4 }))
|
||||
.toBe('001:2 | 00:01:000')
|
||||
|
||||
expect(beatsToTimeString(4, 60, { numerator: 4, denominator: 4 }))
|
||||
.toBe('002:1 | 00:04:000')
|
||||
|
||||
// 240 BPM - 4 beats per second
|
||||
expect(beatsToTimeString(1, 240, { numerator: 4, denominator: 4 }))
|
||||
.toBe('001:2 | 00:00:250')
|
||||
|
||||
expect(beatsToTimeString(4, 240, { numerator: 4, denominator: 4 }))
|
||||
.toBe('002:1 | 00:01:000')
|
||||
})
|
||||
|
||||
it('should handle fractional beats correctly', () => {
|
||||
expect(beatsToTimeString(1.5, 120, { numerator: 4, denominator: 4 }))
|
||||
.toBe('001:2 | 00:00:750')
|
||||
|
||||
expect(beatsToTimeString(2.25, 120, { numerator: 4, denominator: 4 }))
|
||||
.toBe('001:3 | 00:01:125')
|
||||
|
||||
expect(beatsToTimeString(4.75, 120, { numerator: 4, denominator: 4 }))
|
||||
.toBe('002:1 | 00:02:375')
|
||||
})
|
||||
|
||||
it('should pad numbers correctly in output format', () => {
|
||||
// Test bar padding (BBB format)
|
||||
expect(beatsToTimeString(0, 120, { numerator: 4, denominator: 4 }))
|
||||
.toMatch(/^001:/)
|
||||
|
||||
expect(beatsToTimeString(40, 120, { numerator: 4, denominator: 4 }))
|
||||
.toMatch(/^011:/) // Bar 11
|
||||
|
||||
expect(beatsToTimeString(396, 120, { numerator: 4, denominator: 4 }))
|
||||
.toMatch(/^100:/) // Bar 100
|
||||
|
||||
// Test time padding (mm:ss:mmm format)
|
||||
expect(beatsToTimeString(1, 60, { numerator: 4, denominator: 4 }))
|
||||
.toMatch(/\| 00:01:000$/)
|
||||
|
||||
expect(beatsToTimeString(75, 60, { numerator: 4, denominator: 4 }))
|
||||
.toMatch(/\| 01:15:000$/) // 1 minute 15 seconds
|
||||
})
|
||||
|
||||
it('should handle large beat values', () => {
|
||||
const result = beatsToTimeString(1000, 120, { numerator: 4, denominator: 4 })
|
||||
expect(result).toMatch(/^251:1 \| \d{2}:\d{2}:\d{3}$/)
|
||||
})
|
||||
|
||||
it('should handle edge case of zero BPM gracefully', () => {
|
||||
// This might cause division by zero, should handle gracefully
|
||||
expect(() => beatsToTimeString(1, 0, { numerator: 4, denominator: 4 }))
|
||||
.not.toThrow()
|
||||
})
|
||||
|
||||
it('should handle negative beats', () => {
|
||||
// Edge case - negative beats may produce negative values in time format
|
||||
const result = beatsToTimeString(-1, 120, { numerator: 4, denominator: 4 })
|
||||
expect(result).toBeDefined()
|
||||
expect(typeof result).toBe('string')
|
||||
// The function may produce negative time values for negative beats
|
||||
expect(result).toMatch(/^\d{3}:\d \| -?\d+:-?\d+:-?\d+$/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatLocalDateTime', () => {
|
||||
let mockDate: Date
|
||||
|
||||
beforeEach(() => {
|
||||
// Use a fixed date for consistent testing
|
||||
mockDate = new Date('2025-08-21T19:57:11.123Z')
|
||||
})
|
||||
|
||||
it('should format date with correct structure', () => {
|
||||
const result = formatLocalDateTime(mockDate)
|
||||
|
||||
// Should contain date parts
|
||||
expect(result).toMatch(/\d{4}/) // Year
|
||||
expect(result).toMatch(/\d{2}/) // Month/day/hour/minute/second
|
||||
expect(result).toContain(':') // Time separator
|
||||
expect(result).toMatch(/GMT[+-]\d+|UTC|[A-Z]{3,4}/) // Timezone
|
||||
})
|
||||
|
||||
it('should use 24-hour format', () => {
|
||||
const morningDate = new Date('2025-08-21T09:30:00Z')
|
||||
const eveningDate = new Date('2025-08-21T21:30:00Z')
|
||||
|
||||
const morningResult = formatLocalDateTime(morningDate)
|
||||
const eveningResult = formatLocalDateTime(eveningDate)
|
||||
|
||||
// Should not contain AM/PM indicators
|
||||
expect(morningResult).not.toMatch(/AM|PM/i)
|
||||
expect(eveningResult).not.toMatch(/AM|PM/i)
|
||||
})
|
||||
|
||||
it('should include timezone information', () => {
|
||||
const result = formatLocalDateTime(mockDate)
|
||||
|
||||
// Should contain some timezone indicator
|
||||
expect(result).toMatch(/GMT[+-]\d+|UTC|[A-Z]{3,4}|\+\d{4}|-\d{4}/)
|
||||
})
|
||||
|
||||
it('should handle different dates consistently', () => {
|
||||
const dates = [
|
||||
new Date('2025-01-01T00:00:00Z'),
|
||||
new Date('2025-06-15T12:30:45Z'),
|
||||
new Date('2025-12-31T23:59:59Z')
|
||||
]
|
||||
|
||||
dates.forEach(date => {
|
||||
const result = formatLocalDateTime(date)
|
||||
expect(result).toBeDefined()
|
||||
expect(typeof result).toBe('string')
|
||||
expect(result.length).toBeGreaterThan(10)
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle edge dates', () => {
|
||||
const edgeDates = [
|
||||
new Date('1970-01-01T00:00:00Z'), // Unix epoch
|
||||
new Date('2038-01-19T03:14:07Z'), // Near 32-bit timestamp limit
|
||||
new Date('2100-12-31T23:59:59Z') // Future date
|
||||
]
|
||||
|
||||
edgeDates.forEach(date => {
|
||||
expect(() => formatLocalDateTime(date)).not.toThrow()
|
||||
const result = formatLocalDateTime(date)
|
||||
expect(typeof result).toBe('string')
|
||||
expect(result.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
it('should be consistent for the same date', () => {
|
||||
const result1 = formatLocalDateTime(mockDate)
|
||||
const result2 = formatLocalDateTime(mockDate)
|
||||
|
||||
expect(result1).toBe(result2)
|
||||
})
|
||||
|
||||
it('should handle leap year dates', () => {
|
||||
const leapYearDate = new Date('2024-02-29T12:00:00Z') // Leap year
|
||||
|
||||
expect(() => formatLocalDateTime(leapYearDate)).not.toThrow()
|
||||
const result = formatLocalDateTime(leapYearDate)
|
||||
expect(result).toContain('2024')
|
||||
expect(result).toContain('02')
|
||||
expect(result).toContain('29')
|
||||
})
|
||||
})
|
||||
|
||||
describe('integration tests', () => {
|
||||
it('should work together for typical DAW workflow', () => {
|
||||
// Parse time signature
|
||||
const timeSignature = parseTimeSignature('4/4')
|
||||
expect(timeSignature).not.toBeNull()
|
||||
|
||||
// Use parsed time signature in time formatting
|
||||
const timeString = beatsToTimeString(16, 120, timeSignature!)
|
||||
expect(timeString).toBe('005:1 | 00:08:000')
|
||||
|
||||
// Format current time
|
||||
const now = new Date()
|
||||
const formattedTime = formatLocalDateTime(now)
|
||||
expect(formattedTime).toBeDefined()
|
||||
})
|
||||
|
||||
it('should handle error cases gracefully in workflow', () => {
|
||||
// Invalid time signature should not break workflow
|
||||
const invalidTimeSignature = parseTimeSignature('invalid')
|
||||
expect(invalidTimeSignature).toBeNull()
|
||||
|
||||
// Get error message for user feedback
|
||||
const errorMessage = getTimeSignatureErrorMessage()
|
||||
expect(errorMessage).toContain('Invalid time signature')
|
||||
|
||||
// Fallback to default time signature
|
||||
const fallbackTimeSignature = { numerator: 4, denominator: 4 }
|
||||
const timeString = beatsToTimeString(8, 120, fallbackTimeSignature)
|
||||
expect(timeString).toBe('003:1 | 00:04:000')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,331 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { extractXMLFromString, wrapXmlBlocksInContent } from './xmlUtil'
|
||||
import {
|
||||
longTextWithAddNotes,
|
||||
longTextWithReadMusic,
|
||||
multipleXmlBlocks,
|
||||
nestedXmlContent,
|
||||
xmlWithAttributes,
|
||||
malformedXml,
|
||||
noXmlContent,
|
||||
emptyAndWhitespaceXml
|
||||
} from '../test/fixtures/xml-samples'
|
||||
|
||||
describe('xmlUtil', () => {
|
||||
describe('extractXMLFromString', () => {
|
||||
it('should extract simple XML blocks', () => {
|
||||
const input = `Here is some text with <test>content</test> and more text.`
|
||||
const result = extractXMLFromString(input)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0]).toBe('<test>content</test>')
|
||||
})
|
||||
|
||||
it('should extract multiple XML blocks', () => {
|
||||
const input = `<first>content1</first> some text <second>content2</second>`
|
||||
const result = extractXMLFromString(input)
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0]).toBe('<first>content1</first>')
|
||||
expect(result[1]).toBe('<second>content2</second>')
|
||||
})
|
||||
|
||||
it('should extract XML blocks with nested elements', () => {
|
||||
const input = `<outer><inner>nested content</inner></outer>`
|
||||
const result = extractXMLFromString(input)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0]).toBe('<outer><inner>nested content</inner></outer>')
|
||||
})
|
||||
|
||||
it('should extract XML blocks with attributes', () => {
|
||||
const input = `<tag attr="value" id="123">content</tag>`
|
||||
const result = extractXMLFromString(input)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0]).toBe('<tag attr="value" id="123">content</tag>')
|
||||
})
|
||||
|
||||
it('should handle multiline XML blocks', () => {
|
||||
const input = `<multiline>
|
||||
<line1>content1</line1>
|
||||
<line2>content2</line2>
|
||||
</multiline>`
|
||||
const result = extractXMLFromString(input)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0]).toContain('<multiline>')
|
||||
expect(result[0]).toContain('<line1>content1</line1>')
|
||||
expect(result[0]).toContain('<line2>content2</line2>')
|
||||
expect(result[0]).toContain('</multiline>')
|
||||
})
|
||||
|
||||
it('should extract XML from complex nested content fixture', () => {
|
||||
const result = extractXMLFromString(nestedXmlContent)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0]).toContain('<container>')
|
||||
expect(result[0]).toContain('<inner_element>')
|
||||
expect(result[0]).toContain('<deep_nested>')
|
||||
expect(result[0]).toContain('<value>Test Content</value>')
|
||||
expect(result[0]).toContain('</container>')
|
||||
})
|
||||
|
||||
it('should extract multiple XML blocks from fixture', () => {
|
||||
const result = extractXMLFromString(multipleXmlBlocks)
|
||||
|
||||
expect(result).toHaveLength(3)
|
||||
expect(result[0]).toContain('<add_notes>')
|
||||
expect(result[0]).toContain('</add_notes>')
|
||||
expect(result[1]).toContain('<read_music>')
|
||||
expect(result[1]).toContain('</read_music>')
|
||||
expect(result[2]).toContain('<modify_tempo>')
|
||||
expect(result[2]).toContain('</modify_tempo>')
|
||||
})
|
||||
|
||||
it('should extract XML with attributes from fixture', () => {
|
||||
const result = extractXMLFromString(xmlWithAttributes)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0]).toContain('region_id="main"')
|
||||
expect(result[0]).toContain('track="melody"')
|
||||
expect(result[0]).toContain('id="1"')
|
||||
expect(result[0]).toContain('velocity="127"')
|
||||
})
|
||||
|
||||
it('should handle the long text with add_notes fixture (Case 1)', () => {
|
||||
const result = extractXMLFromString(longTextWithAddNotes)
|
||||
|
||||
expect(result).toHaveLength(2) // Contains thinking tag and add_notes block
|
||||
|
||||
// Find the add_notes block
|
||||
const addNotesBlock = result.find(block => block.includes('<add_notes>'))
|
||||
expect(addNotesBlock).toBeDefined()
|
||||
expect(addNotesBlock).toContain('<notes>')
|
||||
expect(addNotesBlock).toContain('<note>')
|
||||
expect(addNotesBlock).toContain('<pitch>C4</pitch>')
|
||||
expect(addNotesBlock).toContain('<start_beat>0</start_beat>')
|
||||
expect(addNotesBlock).toContain('<length>4</length>')
|
||||
expect(addNotesBlock).toContain('</add_notes>')
|
||||
|
||||
// Should contain all 12 notes
|
||||
const noteMatches = addNotesBlock!.match(/<note>/g)
|
||||
expect(noteMatches).toHaveLength(12)
|
||||
})
|
||||
|
||||
it('should handle the long text with read_music fixture (Case 2)', () => {
|
||||
const result = extractXMLFromString(longTextWithReadMusic)
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
|
||||
// First XML block (inside thinking tag)
|
||||
expect(result[0]).toContain('<read_music>')
|
||||
expect(result[0]).toContain('<start_beat>0</start_beat>')
|
||||
expect(result[0]).toContain('<length>32</length>')
|
||||
expect(result[0]).toContain('</read_music>')
|
||||
|
||||
// Second XML block (at the end)
|
||||
expect(result[1]).toContain('<read_music>')
|
||||
expect(result[1]).toContain('<start_beat>0</start_beat>')
|
||||
expect(result[1]).toContain('<length>32</length>')
|
||||
expect(result[1]).toContain('</read_music>')
|
||||
})
|
||||
|
||||
it('should handle malformed XML gracefully', () => {
|
||||
const result = extractXMLFromString(malformedXml)
|
||||
|
||||
// Should extract valid XML blocks (ignores malformed ones)
|
||||
expect(result.length).toBeGreaterThanOrEqual(1)
|
||||
|
||||
// Find the definitely valid block
|
||||
const validBlock = result.find(block => block.includes('<valid_tag>'))
|
||||
expect(validBlock).toBeDefined()
|
||||
expect(validBlock).toContain('<content>This is valid</content>')
|
||||
})
|
||||
|
||||
it('should return empty array for content with no XML', () => {
|
||||
const result = extractXMLFromString(noXmlContent)
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it('should handle empty and whitespace XML', () => {
|
||||
const result = extractXMLFromString(emptyAndWhitespaceXml)
|
||||
|
||||
expect(result).toHaveLength(3)
|
||||
expect(result[0]).toBe('<empty_tag></empty_tag>')
|
||||
expect(result[1]).toContain('<whitespace_tag>')
|
||||
expect(result[1]).toContain('</whitespace_tag>')
|
||||
expect(result[2]).toContain('<mixed_content>')
|
||||
expect(result[2]).toContain('Some text with spaces')
|
||||
expect(result[2]).toContain('</mixed_content>')
|
||||
})
|
||||
|
||||
it('should handle XML with underscores and hyphens in tag names', () => {
|
||||
const input = `<tag_name>content</tag_name> and <tag-name>content</tag-name>`
|
||||
const result = extractXMLFromString(input)
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0]).toBe('<tag_name>content</tag_name>')
|
||||
expect(result[1]).toBe('<tag-name>content</tag-name>')
|
||||
})
|
||||
|
||||
it('should handle self-closing tags (not currently supported)', () => {
|
||||
const input = `<self-closing /> and <normal>content</normal>`
|
||||
const result = extractXMLFromString(input)
|
||||
|
||||
// Current implementation doesn't support self-closing tags
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0]).toBe('<normal>content</normal>')
|
||||
})
|
||||
|
||||
it('should trim whitespace around extracted XML', () => {
|
||||
const input = ` <tag>content</tag> `
|
||||
const result = extractXMLFromString(input)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0]).toBe('<tag>content</tag>')
|
||||
})
|
||||
})
|
||||
|
||||
describe('wrapXmlBlocksInContent', () => {
|
||||
it('should wrap single XML block in fenced code block', () => {
|
||||
const input = `Here is <test>content</test> in text.`
|
||||
const result = wrapXmlBlocksInContent(input)
|
||||
|
||||
expect(result).toBe('Here is ```xml\n<test>content</test>\n``` in text.')
|
||||
})
|
||||
|
||||
it('should wrap multiple XML blocks', () => {
|
||||
const input = `<first>content1</first> text <second>content2</second>`
|
||||
const result = wrapXmlBlocksInContent(input)
|
||||
|
||||
expect(result).toContain('```xml\n<first>content1</first>\n```')
|
||||
expect(result).toContain('```xml\n<second>content2</second>\n```')
|
||||
})
|
||||
|
||||
it('should return original content when no XML blocks present', () => {
|
||||
const input = noXmlContent
|
||||
const result = wrapXmlBlocksInContent(input)
|
||||
|
||||
expect(result).toBe(input)
|
||||
})
|
||||
|
||||
it('should handle empty input', () => {
|
||||
expect(wrapXmlBlocksInContent('')).toBe('')
|
||||
expect(wrapXmlBlocksInContent(null as any)).toBeNull()
|
||||
expect(wrapXmlBlocksInContent(undefined as any)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should wrap XML blocks from multipleXmlBlocks fixture', () => {
|
||||
const result = wrapXmlBlocksInContent(multipleXmlBlocks)
|
||||
|
||||
expect(result).toContain('```xml\n<add_notes>')
|
||||
expect(result).toContain('</add_notes>\n```')
|
||||
expect(result).toContain('```xml\n<read_music>')
|
||||
expect(result).toContain('</read_music>\n```')
|
||||
expect(result).toContain('```xml\n<modify_tempo>')
|
||||
expect(result).toContain('</modify_tempo>\n```')
|
||||
|
||||
// Should preserve the surrounding text
|
||||
expect(result).toContain('Here\'s how to add multiple musical elements:')
|
||||
expect(result).toContain('First, let\'s add some notes:')
|
||||
expect(result).toContain('Then we can read the current music:')
|
||||
})
|
||||
|
||||
it('should wrap the long add_notes XML block (Case 1)', () => {
|
||||
const result = wrapXmlBlocksInContent(longTextWithAddNotes)
|
||||
|
||||
expect(result).toContain('```xml\n<add_notes>')
|
||||
expect(result).toContain('</add_notes>\n```')
|
||||
|
||||
// Should preserve the thinking content and other text
|
||||
expect(result).toContain('<thinking>')
|
||||
expect(result).toContain('Perfect! I can see this is a beautiful')
|
||||
expect(result).toContain('Let me start by adding the harmony')
|
||||
})
|
||||
|
||||
it('should wrap the long read_music XML blocks (Case 2)', () => {
|
||||
const result = wrapXmlBlocksInContent(longTextWithReadMusic)
|
||||
|
||||
// Should contain two wrapped XML blocks
|
||||
const fencedBlocks = result.match(/```xml\n<read_music>[\s\S]*?<\/read_music>\n```/g)
|
||||
expect(fencedBlocks).toHaveLength(2)
|
||||
|
||||
// Should preserve the thinking content and other text
|
||||
expect(result).toContain('<thinking>')
|
||||
expect(result).toContain('I\'ll help you create a pad harmony track')
|
||||
expect(result).toContain('Let me first read the existing music')
|
||||
})
|
||||
|
||||
it('should handle nested XML correctly', () => {
|
||||
const result = wrapXmlBlocksInContent(nestedXmlContent)
|
||||
|
||||
expect(result).toContain('```xml\n<container>')
|
||||
expect(result).toContain('<inner_element>')
|
||||
expect(result).toContain('<deep_nested>')
|
||||
expect(result).toContain('<value>Test Content</value>')
|
||||
expect(result).toContain('</container>\n```')
|
||||
})
|
||||
|
||||
it('should preserve XML block integrity when wrapping', () => {
|
||||
const input = `Text before\n<complex>\n <nested>value</nested>\n <another>content</another>\n</complex>\nText after`
|
||||
const result = wrapXmlBlocksInContent(input)
|
||||
|
||||
expect(result).toBe(`Text before\n\`\`\`xml\n<complex>\n <nested>value</nested>\n <another>content</another>\n</complex>\n\`\`\`\nText after`)
|
||||
})
|
||||
|
||||
it('should handle duplicate XML blocks correctly', () => {
|
||||
const input = `<same>content</same> and then <same>content</same> again`
|
||||
const result = wrapXmlBlocksInContent(input)
|
||||
|
||||
// Both instances should be wrapped
|
||||
const wrappedBlocks = result.match(/```xml\n<same>content<\/same>\n```/g)
|
||||
expect(wrappedBlocks).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('edge cases and error handling', () => {
|
||||
it('should handle very large XML blocks', () => {
|
||||
const largeContent = 'x'.repeat(10000)
|
||||
const input = `<large>${largeContent}</large>`
|
||||
|
||||
const extracted = extractXMLFromString(input)
|
||||
expect(extracted).toHaveLength(1)
|
||||
expect(extracted[0]).toContain(largeContent)
|
||||
|
||||
const wrapped = wrapXmlBlocksInContent(input)
|
||||
expect(wrapped).toContain('```xml\n<large>')
|
||||
expect(wrapped).toContain('</large>\n```')
|
||||
})
|
||||
|
||||
it('should handle XML with special characters', () => {
|
||||
const input = `<tag>Content with < > & " '</tag>`
|
||||
|
||||
const extracted = extractXMLFromString(input)
|
||||
expect(extracted).toHaveLength(1)
|
||||
expect(extracted[0]).toContain('< > & " '')
|
||||
|
||||
const wrapped = wrapXmlBlocksInContent(input)
|
||||
expect(wrapped).toContain('```xml\n<tag>Content with < > & " '</tag>\n```')
|
||||
})
|
||||
|
||||
it('should handle XML with CDATA sections', () => {
|
||||
const input = `<tag><![CDATA[Some content with <special> chars]]></tag>`
|
||||
|
||||
const extracted = extractXMLFromString(input)
|
||||
expect(extracted).toHaveLength(1)
|
||||
expect(extracted[0]).toContain('<![CDATA[')
|
||||
expect(extracted[0]).toContain(']]>')
|
||||
})
|
||||
|
||||
it('should handle mixed content with partial XML-like text', () => {
|
||||
const input = `This < is not XML and neither > is this <incomplete and <valid>content</valid> is valid`
|
||||
|
||||
const extracted = extractXMLFromString(input)
|
||||
expect(extracted).toHaveLength(1)
|
||||
expect(extracted[0]).toBe('<valid>content</valid>')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user