mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-16 20:51:04 +00:00
Compare commits
7 Commits
test-cover
...
test/edit-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b084fc8cff | ||
|
|
21e7163e9e | ||
|
|
e5c81488e4 | ||
|
|
5c07198acb | ||
|
|
6fb90b224d | ||
|
|
a8e1fa8bef | ||
|
|
83ceef8cb3 |
27
.github/workflows/ci-tests-e2e-coverage.yaml
vendored
27
.github/workflows/ci-tests-e2e-coverage.yaml
vendored
@@ -54,6 +54,33 @@ jobs:
|
||||
lcov $ADD_ARGS -o coverage/playwright/coverage.lcov
|
||||
wc -l coverage/playwright/coverage.lcov
|
||||
|
||||
- name: Validate merged coverage
|
||||
run: |
|
||||
SHARD_COUNT=$(find temp/coverage-shards -name 'coverage.lcov' -type f | wc -l | tr -d ' ')
|
||||
if [ "$SHARD_COUNT" -eq 0 ]; then
|
||||
echo "::error::No shard coverage.lcov files found under temp/coverage-shards"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MERGED_SF=$(grep -c '^SF:' coverage/playwright/coverage.lcov || echo 0)
|
||||
MERGED_LH=$(awk -F: '/^LH:/{s+=$2}END{print s+0}' coverage/playwright/coverage.lcov)
|
||||
MERGED_LF=$(awk -F: '/^LF:/{s+=$2}END{print s+0}' coverage/playwright/coverage.lcov)
|
||||
echo "### Merged coverage" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "- **$MERGED_SF** source files" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "- **$MERGED_LH / $MERGED_LF** lines hit" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| Shard | Files | Lines Hit |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "|-------|-------|-----------|" >> "$GITHUB_STEP_SUMMARY"
|
||||
for f in $(find temp/coverage-shards -name 'coverage.lcov' -type f | sort); do
|
||||
SHARD=$(basename "$(dirname "$f")")
|
||||
SHARD_SF=$(grep -c '^SF:' "$f" || echo 0)
|
||||
SHARD_LH=$(awk -F: '/^LH:/{s+=$2}END{print s+0}' "$f")
|
||||
echo "| $SHARD | $SHARD_SF | $SHARD_LH |" >> "$GITHUB_STEP_SUMMARY"
|
||||
if [ "$MERGED_LH" -lt "$SHARD_LH" ]; then
|
||||
echo "::error::Merged LH ($MERGED_LH) < shard LH ($SHARD_LH) in $SHARD — possible data loss"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Upload merged coverage data
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v6
|
||||
|
||||
@@ -55,4 +55,30 @@ test.describe('Focus Mode', { tag: '@ui' }, () => {
|
||||
await comfyPage.setFocusMode(true)
|
||||
await expect(comfyPage.menu.sideToolbar).toBeHidden()
|
||||
})
|
||||
|
||||
test('Focus mode toggle preserves properties panel width', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Open the properties panel
|
||||
await comfyPage.actionbar.propertiesButton.click()
|
||||
await expect(comfyPage.menu.propertiesPanel.root).toBeVisible()
|
||||
|
||||
// Record the initial panel width
|
||||
const initialBox = await comfyPage.menu.propertiesPanel.root.boundingBox()
|
||||
expect(initialBox).not.toBeNull()
|
||||
const initialWidth = initialBox!.width
|
||||
|
||||
// Toggle focus mode on then off
|
||||
await comfyPage.setFocusMode(true)
|
||||
await comfyPage.setFocusMode(false)
|
||||
|
||||
// Properties panel should be visible again with the same width
|
||||
await expect(comfyPage.menu.propertiesPanel.root).toBeVisible()
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const box = await comfyPage.menu.propertiesPanel.root.boundingBox()
|
||||
return box ? Math.abs(box.width - initialWidth) : Infinity
|
||||
})
|
||||
.toBeLessThan(2)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -171,14 +171,10 @@ const sidebarPanelVisible = computed(
|
||||
)
|
||||
|
||||
const firstPanelVisible = computed(
|
||||
() =>
|
||||
!focusMode.value &&
|
||||
(sidebarLocation.value === 'left' || showOffsideSplitter.value)
|
||||
() => sidebarLocation.value === 'left' || showOffsideSplitter.value
|
||||
)
|
||||
const lastPanelVisible = computed(
|
||||
() =>
|
||||
!focusMode.value &&
|
||||
(sidebarLocation.value === 'right' || showOffsideSplitter.value)
|
||||
() => sidebarLocation.value === 'right' || showOffsideSplitter.value
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -268,6 +264,7 @@ const splitterRefreshKey = computed(() => {
|
||||
})
|
||||
|
||||
const firstPanelStyle = computed(() => {
|
||||
if (focusMode.value) return { display: 'none' }
|
||||
if (sidebarLocation.value === 'left') {
|
||||
return { display: sidebarPanelVisible.value ? 'flex' : 'none' }
|
||||
}
|
||||
@@ -275,6 +272,7 @@ const firstPanelStyle = computed(() => {
|
||||
})
|
||||
|
||||
const lastPanelStyle = computed(() => {
|
||||
if (focusMode.value) return { display: 'none' }
|
||||
if (sidebarLocation.value === 'right') {
|
||||
return { display: sidebarPanelVisible.value ? 'flex' : 'none' }
|
||||
}
|
||||
@@ -293,9 +291,13 @@ const lastPanelStyle = computed(() => {
|
||||
background-color: var(--p-primary-color);
|
||||
}
|
||||
|
||||
/* Hide sidebar gutter when sidebar is not visible */
|
||||
:deep(.side-bar-panel[style*='display: none'] + .p-splitter-gutter),
|
||||
:deep(.p-splitter-gutter + .side-bar-panel[style*='display: none']) {
|
||||
/* Hide gutter when adjacent panel is not visible */
|
||||
:deep(
|
||||
[data-pc-name='splitterpanel'][style*='display: none'] + .p-splitter-gutter
|
||||
),
|
||||
:deep(
|
||||
.p-splitter-gutter + [data-pc-name='splitterpanel'][style*='display: none']
|
||||
) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -430,6 +430,17 @@ describe('useLoad3dViewer', () => {
|
||||
|
||||
expect(mockLoad3d.updateStatusMouseOnViewer).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
it('should sync hover state when mouseenter fires before init', async () => {
|
||||
const viewer = useLoad3dViewer(mockNode)
|
||||
const containerRef = document.createElement('div')
|
||||
|
||||
viewer.handleMouseEnter()
|
||||
|
||||
await viewer.initializeViewer(containerRef, mockSourceLoad3d as Load3d)
|
||||
|
||||
expect(mockLoad3d.updateStatusMouseOnViewer).toHaveBeenCalledWith(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('restoreInitialState', () => {
|
||||
|
||||
@@ -86,6 +86,7 @@ export const useLoad3dViewer = (node?: LGraphNode) => {
|
||||
let load3d: Load3d | null = null
|
||||
let sourceLoad3d: Load3d | null = null
|
||||
let currentModelUrl: string | null = null
|
||||
let mouseOnViewer = false
|
||||
|
||||
const initialState = ref<Load3dViewerState>({
|
||||
backgroundColor: '#282828',
|
||||
@@ -304,6 +305,10 @@ export const useLoad3dViewer = (node?: LGraphNode) => {
|
||||
isViewerMode: hasTargetDimensions
|
||||
})
|
||||
|
||||
if (mouseOnViewer) {
|
||||
load3d.updateStatusMouseOnViewer(true)
|
||||
}
|
||||
|
||||
await useLoad3dService().copyLoad3dState(source, load3d)
|
||||
|
||||
const sourceCameraState = source.getCameraState()
|
||||
@@ -416,6 +421,10 @@ export const useLoad3dViewer = (node?: LGraphNode) => {
|
||||
isViewerMode: true
|
||||
})
|
||||
|
||||
if (mouseOnViewer) {
|
||||
load3d.updateStatusMouseOnViewer(true)
|
||||
}
|
||||
|
||||
await load3d.loadModel(modelUrl)
|
||||
currentModelUrl = modelUrl
|
||||
restoreStandaloneConfig(modelUrl)
|
||||
@@ -522,6 +531,7 @@ export const useLoad3dViewer = (node?: LGraphNode) => {
|
||||
* Notifies the viewer that the mouse has entered the viewer area.
|
||||
*/
|
||||
const handleMouseEnter = () => {
|
||||
mouseOnViewer = true
|
||||
load3d?.updateStatusMouseOnViewer(true)
|
||||
}
|
||||
|
||||
@@ -529,6 +539,7 @@ export const useLoad3dViewer = (node?: LGraphNode) => {
|
||||
* Notifies the viewer that the mouse has left the viewer area.
|
||||
*/
|
||||
const handleMouseLeave = () => {
|
||||
mouseOnViewer = false
|
||||
load3d?.updateStatusMouseOnViewer(false)
|
||||
}
|
||||
|
||||
@@ -727,6 +738,7 @@ export const useLoad3dViewer = (node?: LGraphNode) => {
|
||||
if (isStandaloneMode.value) {
|
||||
saveStandaloneConfig()
|
||||
}
|
||||
mouseOnViewer = false
|
||||
load3d?.remove()
|
||||
load3d = null
|
||||
sourceLoad3d = null
|
||||
|
||||
102
src/extensions/core/editAttention.test.ts
Normal file
102
src/extensions/core/editAttention.test.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: {
|
||||
registerExtension: vi.fn(),
|
||||
ui: { settings: { addSetting: vi.fn() } }
|
||||
}
|
||||
}))
|
||||
|
||||
import {
|
||||
addWeightToParentheses,
|
||||
findNearestEnclosure,
|
||||
incrementWeight
|
||||
} from './editAttention'
|
||||
|
||||
describe('incrementWeight', () => {
|
||||
it('increments a weight by the given delta', () => {
|
||||
expect(incrementWeight('1.0', 0.05)).toBe('1.05')
|
||||
})
|
||||
|
||||
it('decrements a weight by the given delta', () => {
|
||||
expect(incrementWeight('1.05', -0.05)).toBe('1')
|
||||
})
|
||||
|
||||
it('returns the original string when weight is not a number', () => {
|
||||
expect(incrementWeight('abc', 0.05)).toBe('abc')
|
||||
})
|
||||
|
||||
it('rounds correctly and avoids floating point accumulation', () => {
|
||||
expect(incrementWeight('1.1', 0.1)).toBe('1.2')
|
||||
})
|
||||
|
||||
it('can produce a weight of zero', () => {
|
||||
expect(incrementWeight('0.05', -0.05)).toBe('0')
|
||||
})
|
||||
|
||||
it('produces negative weights', () => {
|
||||
expect(incrementWeight('0.0', -0.05)).toBe('-0.05')
|
||||
})
|
||||
})
|
||||
|
||||
describe('findNearestEnclosure', () => {
|
||||
it('returns start and end of a simple parenthesized expression', () => {
|
||||
expect(findNearestEnclosure('(cat)', 2)).toEqual({ start: 1, end: 4 })
|
||||
})
|
||||
|
||||
it('returns null when there are no parentheses', () => {
|
||||
expect(findNearestEnclosure('cat dog', 3)).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null when cursor is outside any enclosure', () => {
|
||||
expect(findNearestEnclosure('(cat) dog', 7)).toBeNull()
|
||||
})
|
||||
|
||||
it('finds the inner enclosure when cursor is on nested content', () => {
|
||||
expect(findNearestEnclosure('(outer (inner) end)', 9)).toEqual({
|
||||
start: 8,
|
||||
end: 13
|
||||
})
|
||||
})
|
||||
|
||||
it('finds the outer enclosure when cursor is on outer content', () => {
|
||||
expect(findNearestEnclosure('(outer (inner) end)', 2)).toEqual({
|
||||
start: 1,
|
||||
end: 18
|
||||
})
|
||||
})
|
||||
|
||||
it('returns null for empty string', () => {
|
||||
expect(findNearestEnclosure('', 0)).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null when opening paren has no matching closing paren', () => {
|
||||
expect(findNearestEnclosure('(cat', 2)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('addWeightToParentheses', () => {
|
||||
it('adds weight 1.0 to a bare parenthesized token', () => {
|
||||
expect(addWeightToParentheses('(cat)')).toBe('(cat:1.0)')
|
||||
})
|
||||
|
||||
it('leaves a token that already has a weight unchanged', () => {
|
||||
expect(addWeightToParentheses('(cat:1.5)')).toBe('(cat:1.5)')
|
||||
})
|
||||
|
||||
it('leaves a token without parentheses unchanged', () => {
|
||||
expect(addWeightToParentheses('cat')).toBe('cat')
|
||||
})
|
||||
|
||||
it('leaves a token with scientific notation weight unchanged', () => {
|
||||
expect(addWeightToParentheses('(cat:1e-3)')).toBe('(cat:1e-3)')
|
||||
})
|
||||
|
||||
it('leaves a token with a negative weight unchanged', () => {
|
||||
expect(addWeightToParentheses('(cat:-0.5)')).toBe('(cat:-0.5)')
|
||||
})
|
||||
|
||||
it('adds weight to a multi-word parenthesized token', () => {
|
||||
expect(addWeightToParentheses('(cat dog)')).toBe('(cat dog:1.0)')
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,61 @@
|
||||
import { app } from '../../scripts/app'
|
||||
|
||||
// Allows you to edit the attention weight by holding ctrl (or cmd) and using the up/down arrow keys
|
||||
type Enclosure = {
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
|
||||
export function incrementWeight(weight: string, delta: number): string {
|
||||
const floatWeight = parseFloat(weight)
|
||||
if (isNaN(floatWeight)) return weight
|
||||
const newWeight = floatWeight + delta
|
||||
return String(Number(newWeight.toFixed(10)))
|
||||
}
|
||||
|
||||
export function findNearestEnclosure(
|
||||
text: string,
|
||||
cursorPos: number
|
||||
): Enclosure | null {
|
||||
let start = cursorPos,
|
||||
end = cursorPos
|
||||
let openCount = 0,
|
||||
closeCount = 0
|
||||
|
||||
while (start >= 0) {
|
||||
start--
|
||||
if (text[start] === '(' && openCount === closeCount) break
|
||||
if (text[start] === '(') openCount++
|
||||
if (text[start] === ')') closeCount++
|
||||
}
|
||||
if (start < 0) return null
|
||||
|
||||
openCount = 0
|
||||
closeCount = 0
|
||||
|
||||
while (end < text.length) {
|
||||
if (text[end] === ')' && openCount === closeCount) break
|
||||
if (text[end] === '(') openCount++
|
||||
if (text[end] === ')') closeCount++
|
||||
end++
|
||||
}
|
||||
if (end === text.length) return null
|
||||
|
||||
return { start: start + 1, end: end }
|
||||
}
|
||||
|
||||
export function addWeightToParentheses(text: string): string {
|
||||
const parenRegex = /^\((.*)\)$/
|
||||
const parenMatch = text.match(parenRegex)
|
||||
|
||||
const floatRegex = /:([+-]?(\d*\.)?\d+([eE][+-]?\d+)?)/
|
||||
const floatMatch = text.match(floatRegex)
|
||||
|
||||
if (parenMatch && !floatMatch) {
|
||||
return `(${parenMatch[1]}:1.0)`
|
||||
} else {
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: 'Comfy.EditAttention',
|
||||
@@ -18,65 +73,6 @@ app.registerExtension({
|
||||
defaultValue: 0.05
|
||||
})
|
||||
|
||||
function incrementWeight(weight: string, delta: number): string {
|
||||
const floatWeight = parseFloat(weight)
|
||||
if (isNaN(floatWeight)) return weight
|
||||
const newWeight = floatWeight + delta
|
||||
return String(Number(newWeight.toFixed(10)))
|
||||
}
|
||||
|
||||
type Enclosure = {
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
|
||||
function findNearestEnclosure(
|
||||
text: string,
|
||||
cursorPos: number
|
||||
): Enclosure | null {
|
||||
let start = cursorPos,
|
||||
end = cursorPos
|
||||
let openCount = 0,
|
||||
closeCount = 0
|
||||
|
||||
// Find opening parenthesis before cursor
|
||||
while (start >= 0) {
|
||||
start--
|
||||
if (text[start] === '(' && openCount === closeCount) break
|
||||
if (text[start] === '(') openCount++
|
||||
if (text[start] === ')') closeCount++
|
||||
}
|
||||
if (start < 0) return null
|
||||
|
||||
openCount = 0
|
||||
closeCount = 0
|
||||
|
||||
// Find closing parenthesis after cursor
|
||||
while (end < text.length) {
|
||||
if (text[end] === ')' && openCount === closeCount) break
|
||||
if (text[end] === '(') openCount++
|
||||
if (text[end] === ')') closeCount++
|
||||
end++
|
||||
}
|
||||
if (end === text.length) return null
|
||||
|
||||
return { start: start + 1, end: end }
|
||||
}
|
||||
|
||||
function addWeightToParentheses(text: string): string {
|
||||
const parenRegex = /^\((.*)\)$/
|
||||
const parenMatch = text.match(parenRegex)
|
||||
|
||||
const floatRegex = /:([+-]?(\d*\.)?\d+([eE][+-]?\d+)?)/
|
||||
const floatMatch = text.match(floatRegex)
|
||||
|
||||
if (parenMatch && !floatMatch) {
|
||||
return `(${parenMatch[1]}:1.0)`
|
||||
} else {
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
function editAttention(event: KeyboardEvent) {
|
||||
// @ts-expect-error Runtime narrowing not impl.
|
||||
const inputField: HTMLTextAreaElement = event.composedPath()[0]
|
||||
@@ -92,7 +88,6 @@ app.registerExtension({
|
||||
let end = inputField.selectionEnd
|
||||
let selectedText = inputField.value.substring(start, end)
|
||||
|
||||
// If there is no selection, attempt to find the nearest enclosure, or select the current word
|
||||
if (!selectedText) {
|
||||
const nearestEnclosure = findNearestEnclosure(inputField.value, start)
|
||||
if (nearestEnclosure) {
|
||||
@@ -100,7 +95,6 @@ app.registerExtension({
|
||||
end = nearestEnclosure.end
|
||||
selectedText = inputField.value.substring(start, end)
|
||||
} else {
|
||||
// Select the current word, find the start and end of the word
|
||||
const delimiters = ' .,\\/!?%^*;:{}=-_`~()\r\n\t'
|
||||
|
||||
while (
|
||||
@@ -122,13 +116,11 @@ app.registerExtension({
|
||||
}
|
||||
}
|
||||
|
||||
// If the selection ends with a space, remove it
|
||||
if (selectedText[selectedText.length - 1] === ' ') {
|
||||
selectedText = selectedText.substring(0, selectedText.length - 1)
|
||||
end -= 1
|
||||
}
|
||||
|
||||
// If there are parentheses left and right of the selection, select them
|
||||
if (
|
||||
inputField.value[start - 1] === '(' &&
|
||||
inputField.value[end] === ')'
|
||||
@@ -138,7 +130,6 @@ app.registerExtension({
|
||||
selectedText = inputField.value.substring(start, end)
|
||||
}
|
||||
|
||||
// If the selection is not enclosed in parentheses, add them
|
||||
if (
|
||||
selectedText[0] !== '(' ||
|
||||
selectedText[selectedText.length - 1] !== ')'
|
||||
@@ -146,10 +137,8 @@ app.registerExtension({
|
||||
selectedText = `(${selectedText})`
|
||||
}
|
||||
|
||||
// If the selection does not have a weight, add a weight of 1.0
|
||||
selectedText = addWeightToParentheses(selectedText)
|
||||
|
||||
// Increment the weight
|
||||
const weightDelta = event.key === 'ArrowUp' ? delta : -delta
|
||||
const updatedText = selectedText.replace(
|
||||
/\((.*):([+-]?\d+(?:\.\d+)?)\)/,
|
||||
|
||||
67
src/scripts/pnginfo.test.ts
Normal file
67
src/scripts/pnginfo.test.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { getWebpMetadata } from './pnginfo'
|
||||
|
||||
function buildExifPayload(workflowJson: string): Uint8Array {
|
||||
const fullStr = `workflow:${workflowJson}\0`
|
||||
const strBytes = new TextEncoder().encode(fullStr)
|
||||
|
||||
const headerSize = 22
|
||||
const buf = new Uint8Array(headerSize + strBytes.length)
|
||||
const dv = new DataView(buf.buffer)
|
||||
|
||||
buf.set([0x49, 0x49], 0)
|
||||
dv.setUint16(2, 0x002a, true)
|
||||
dv.setUint32(4, 8, true)
|
||||
dv.setUint16(8, 1, true)
|
||||
dv.setUint16(10, 0, true)
|
||||
dv.setUint16(12, 2, true)
|
||||
dv.setUint32(14, strBytes.length, true)
|
||||
dv.setUint32(18, 22, true)
|
||||
buf.set(strBytes, 22)
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
function buildWebp(precedingChunkLength: number, workflowJson: string): File {
|
||||
const exifPayload = buildExifPayload(workflowJson)
|
||||
const precedingPadded = precedingChunkLength + (precedingChunkLength % 2)
|
||||
const totalSize = 12 + (8 + precedingPadded) + (8 + exifPayload.length)
|
||||
|
||||
const buffer = new Uint8Array(totalSize)
|
||||
const dv = new DataView(buffer.buffer)
|
||||
|
||||
buffer.set([0x52, 0x49, 0x46, 0x46], 0)
|
||||
dv.setUint32(4, totalSize - 8, true)
|
||||
buffer.set([0x57, 0x45, 0x42, 0x50], 8)
|
||||
|
||||
buffer.set([0x56, 0x50, 0x38, 0x20], 12)
|
||||
dv.setUint32(16, precedingChunkLength, true)
|
||||
|
||||
const exifStart = 20 + precedingPadded
|
||||
buffer.set([0x45, 0x58, 0x49, 0x46], exifStart)
|
||||
dv.setUint32(exifStart + 4, exifPayload.length, true)
|
||||
buffer.set(exifPayload, exifStart + 8)
|
||||
|
||||
return new File([buffer], 'test.webp', { type: 'image/webp' })
|
||||
}
|
||||
|
||||
describe('getWebpMetadata', () => {
|
||||
it('finds workflow when a preceding chunk has odd length (RIFF padding)', async () => {
|
||||
const workflow = '{"nodes":[]}'
|
||||
const file = buildWebp(3, workflow)
|
||||
|
||||
const metadata = await getWebpMetadata(file)
|
||||
|
||||
expect(metadata.workflow).toBe(workflow)
|
||||
})
|
||||
|
||||
it('finds workflow when preceding chunk has even length (no padding)', async () => {
|
||||
const workflow = '{"nodes":[1]}'
|
||||
const file = buildWebp(4, workflow)
|
||||
|
||||
const metadata = await getWebpMetadata(file)
|
||||
|
||||
expect(metadata.workflow).toBe(workflow)
|
||||
})
|
||||
})
|
||||
52
src/utils/errorReportUtil.test.ts
Normal file
52
src/utils/errorReportUtil.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { ISerialisedGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type { SystemStats } from '@/schemas/apiSchema'
|
||||
|
||||
import type { ErrorReportData } from './errorReportUtil'
|
||||
import { generateErrorReport } from './errorReportUtil'
|
||||
|
||||
const baseSystemStats: SystemStats = {
|
||||
system: {
|
||||
os: 'linux',
|
||||
comfyui_version: '1.0.0',
|
||||
python_version: '3.11',
|
||||
pytorch_version: '2.0',
|
||||
embedded_python: false,
|
||||
argv: ['main.py'],
|
||||
ram_total: 0,
|
||||
ram_free: 0
|
||||
},
|
||||
devices: []
|
||||
}
|
||||
|
||||
const baseWorkflow = { nodes: [], links: [] } as unknown as ISerialisedGraph
|
||||
|
||||
function buildError(serverLogs: unknown): ErrorReportData {
|
||||
return {
|
||||
exceptionType: 'RuntimeError',
|
||||
exceptionMessage: 'boom',
|
||||
systemStats: baseSystemStats,
|
||||
serverLogs: serverLogs as string,
|
||||
workflow: baseWorkflow
|
||||
}
|
||||
}
|
||||
|
||||
describe('generateErrorReport', () => {
|
||||
it('embeds string serverLogs verbatim', () => {
|
||||
const report = generateErrorReport(buildError('line one\nline two'))
|
||||
|
||||
expect(report).toContain('line one\nline two')
|
||||
expect(report).not.toContain('[object Object]')
|
||||
})
|
||||
|
||||
it('stringifies object serverLogs instead of rendering [object Object]', () => {
|
||||
const report = generateErrorReport(
|
||||
buildError({ entries: [{ msg: 'hello' }] })
|
||||
)
|
||||
|
||||
expect(report).not.toContain('[object Object]')
|
||||
expect(report).toContain('"entries"')
|
||||
expect(report).toContain('"msg": "hello"')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user