mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-08 16:17:58 +00:00
Compare commits
2 Commits
refactor/c
...
subgraph/n
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
560c15dd6f | ||
|
|
3ce6bbd567 |
27
.github/workflows/danger.yaml
vendored
Normal file
27
.github/workflows/danger.yaml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: Danger PR Review
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, synchronize]
|
||||
|
||||
jobs:
|
||||
danger:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run Danger
|
||||
run: npx danger ci
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
DANGER_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
154
.github/workflows/pr-checks.yml
vendored
154
.github/workflows/pr-checks.yml
vendored
@@ -1,154 +0,0 @@
|
||||
name: PR Checks
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, synchronize, reopened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_run: ${{ steps.check-changes.outputs.should_run }}
|
||||
has_browser_tests: ${{ steps.check-coverage.outputs.has_browser_tests }}
|
||||
has_screen_recording: ${{ steps.check-recording.outputs.has_recording }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Ensure base branch is available
|
||||
run: |
|
||||
# Fetch the specific base commit to ensure it's available for git diff
|
||||
git fetch origin ${{ github.event.pull_request.base.sha }}
|
||||
|
||||
- name: Check if significant changes exist
|
||||
id: check-changes
|
||||
run: |
|
||||
# Get list of changed files
|
||||
CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }})
|
||||
|
||||
# Filter for src/ files
|
||||
SRC_FILES=$(echo "$CHANGED_FILES" | grep '^src/' || true)
|
||||
|
||||
if [ -z "$SRC_FILES" ]; then
|
||||
echo "No src/ files changed"
|
||||
echo "should_run=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Count lines changed in src files
|
||||
TOTAL_LINES=0
|
||||
for file in $SRC_FILES; do
|
||||
if [ -f "$file" ]; then
|
||||
# Count added lines (non-empty)
|
||||
ADDED=$(git diff ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }} -- "$file" | grep '^+' | grep -v '^+++' | grep -v '^+$' | wc -l)
|
||||
# Count removed lines (non-empty)
|
||||
REMOVED=$(git diff ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }} -- "$file" | grep '^-' | grep -v '^---' | grep -v '^-$' | wc -l)
|
||||
TOTAL_LINES=$((TOTAL_LINES + ADDED + REMOVED))
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Total lines changed in src/: $TOTAL_LINES"
|
||||
|
||||
if [ $TOTAL_LINES -gt 3 ]; then
|
||||
echo "should_run=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "should_run=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Check browser test coverage
|
||||
id: check-coverage
|
||||
if: steps.check-changes.outputs.should_run == 'true'
|
||||
run: |
|
||||
# Check if browser tests were updated
|
||||
BROWSER_TEST_CHANGES=$(git diff --name-only ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }} | grep '^browser_tests/.*\.ts$' || true)
|
||||
|
||||
if [ -n "$BROWSER_TEST_CHANGES" ]; then
|
||||
echo "has_browser_tests=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "has_browser_tests=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Check for screen recording
|
||||
id: check-recording
|
||||
if: steps.check-changes.outputs.should_run == 'true'
|
||||
run: |
|
||||
# Check PR body for screen recording
|
||||
PR_BODY="${{ github.event.pull_request.body }}"
|
||||
|
||||
# Check for GitHub user attachments or YouTube links
|
||||
if echo "$PR_BODY" | grep -qiE 'github\.com/user-attachments/assets/[a-f0-9-]+|youtube\.com/watch|youtu\.be/'; then
|
||||
echo "has_recording=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "has_recording=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Final check and create results
|
||||
id: final-check
|
||||
if: always()
|
||||
run: |
|
||||
# Initialize results
|
||||
WARNINGS_JSON=""
|
||||
|
||||
# Only run checks if should_run is true
|
||||
if [ "${{ steps.check-changes.outputs.should_run }}" == "true" ]; then
|
||||
# Check browser test coverage
|
||||
if [ "${{ steps.check-coverage.outputs.has_browser_tests }}" != "true" ]; then
|
||||
if [ -n "$WARNINGS_JSON" ]; then
|
||||
WARNINGS_JSON="${WARNINGS_JSON},"
|
||||
fi
|
||||
WARNINGS_JSON="${WARNINGS_JSON}{\"message\":\"⚠️ **Warning: E2E Test Coverage Missing**\\n\\nIf this PR modifies behavior that can be covered by browser-based E2E tests, those tests are required. PRs lacking applicable test coverage may not be reviewed until added. Please add or update browser tests to ensure code quality and prevent regressions.\"}"
|
||||
fi
|
||||
|
||||
# Check screen recording
|
||||
if [ "${{ steps.check-recording.outputs.has_recording }}" != "true" ]; then
|
||||
if [ -n "$WARNINGS_JSON" ]; then
|
||||
WARNINGS_JSON="${WARNINGS_JSON},"
|
||||
fi
|
||||
WARNINGS_JSON="${WARNINGS_JSON}{\"message\":\"⚠️ **Warning: Visual Documentation Missing**\\n\\nIf this PR changes user-facing behavior, visual proof (screen recording or screenshot) is required. PRs without applicable visual documentation may not be reviewed until provided.\\nYou can add it by:\\n\\n- GitHub: Drag & drop media directly into the PR description\\n\\n- YouTube: Include a link to a short demo\"}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create results JSON
|
||||
if [ -n "$WARNINGS_JSON" ]; then
|
||||
# Create JSON with warnings
|
||||
cat > pr-check-results.json << EOF
|
||||
{
|
||||
"fails": [],
|
||||
"warnings": [$WARNINGS_JSON],
|
||||
"messages": [],
|
||||
"markdowns": []
|
||||
}
|
||||
EOF
|
||||
echo "failed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
# Create JSON with success
|
||||
cat > pr-check-results.json << 'EOF'
|
||||
{
|
||||
"fails": [],
|
||||
"warnings": [],
|
||||
"messages": [],
|
||||
"markdowns": []
|
||||
}
|
||||
EOF
|
||||
echo "failed=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# Write PR metadata
|
||||
echo "${{ github.event.pull_request.number }}" > pr-number.txt
|
||||
echo "${{ github.event.pull_request.head.sha }}" > pr-sha.txt
|
||||
|
||||
- name: Upload results
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: pr-check-results-${{ github.run_id }}
|
||||
path: |
|
||||
pr-check-results.json
|
||||
pr-number.txt
|
||||
pr-sha.txt
|
||||
retention-days: 1
|
||||
149
.github/workflows/pr-comment.yml
vendored
149
.github/workflows/pr-comment.yml
vendored
@@ -1,149 +0,0 @@
|
||||
name: PR Comment
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["PR Checks"]
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
statuses: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
if: github.event.workflow_run.event == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: pr-check-results-${{ github.event.workflow_run.id }}
|
||||
path: /tmp/pr-artifacts
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
- name: Post results
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Helper function to safely read files
|
||||
function safeReadFile(filePath) {
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) return null;
|
||||
return fs.readFileSync(filePath, 'utf8').trim();
|
||||
} catch (e) {
|
||||
console.error(`Error reading ${filePath}:`, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Read artifact files
|
||||
const artifactDir = '/tmp/pr-artifacts';
|
||||
const prNumber = safeReadFile(path.join(artifactDir, 'pr-number.txt'));
|
||||
const prSha = safeReadFile(path.join(artifactDir, 'pr-sha.txt'));
|
||||
const resultsJson = safeReadFile(path.join(artifactDir, 'pr-check-results.json'));
|
||||
|
||||
// Validate PR number
|
||||
if (!prNumber || isNaN(parseInt(prNumber))) {
|
||||
throw new Error('Invalid or missing PR number');
|
||||
}
|
||||
|
||||
// Parse and validate results
|
||||
let results;
|
||||
try {
|
||||
results = JSON.parse(resultsJson || '{}');
|
||||
} catch (e) {
|
||||
console.error('Failed to parse check results:', e);
|
||||
|
||||
// Post error comment
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: parseInt(prNumber),
|
||||
body: `⚠️ PR checks failed to complete properly. Error parsing results: ${e.message}`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Format check messages
|
||||
const messages = [];
|
||||
|
||||
if (results.fails && results.fails.length > 0) {
|
||||
messages.push('### ❌ Failures\n' + results.fails.map(f => f.message).join('\n\n'));
|
||||
}
|
||||
|
||||
if (results.warnings && results.warnings.length > 0) {
|
||||
messages.push('### ⚠️ Warnings\n' + results.warnings.map(w => w.message).join('\n\n'));
|
||||
}
|
||||
|
||||
if (results.messages && results.messages.length > 0) {
|
||||
messages.push('### 💬 Messages\n' + results.messages.map(m => m.message).join('\n\n'));
|
||||
}
|
||||
|
||||
if (results.markdowns && results.markdowns.length > 0) {
|
||||
messages.push(...results.markdowns.map(m => m.message));
|
||||
}
|
||||
|
||||
// Find existing bot comment
|
||||
const comments = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: parseInt(prNumber)
|
||||
});
|
||||
|
||||
const botComment = comments.data.find(comment =>
|
||||
comment.user.type === 'Bot' &&
|
||||
comment.body.includes('<!-- pr-checks-comment -->')
|
||||
);
|
||||
|
||||
// Post comment if there are any messages
|
||||
if (messages.length > 0) {
|
||||
const body = messages.join('\n\n');
|
||||
const commentBody = `<!-- pr-checks-comment -->\n${body}`;
|
||||
|
||||
if (botComment) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: botComment.id,
|
||||
body: commentBody
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: parseInt(prNumber),
|
||||
body: commentBody
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// No messages - delete existing comment if present
|
||||
if (botComment) {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: botComment.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Set commit status based on failures
|
||||
if (prSha) {
|
||||
const hasFailures = results.fails && results.fails.length > 0;
|
||||
const hasWarnings = results.warnings && results.warnings.length > 0;
|
||||
await github.rest.repos.createCommitStatus({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
sha: prSha,
|
||||
state: hasFailures ? 'failure' : 'success',
|
||||
context: 'pr-checks',
|
||||
description: hasFailures
|
||||
? `${results.fails.length} check(s) failed`
|
||||
: hasWarnings
|
||||
? `${results.warnings.length} warning(s)`
|
||||
: 'All checks passed'
|
||||
});
|
||||
}
|
||||
@@ -1,382 +0,0 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Feature Flags', () => {
|
||||
test('Client and server exchange feature flags on connection', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Navigate to a new page to capture the initial WebSocket connection
|
||||
const newPage = await comfyPage.page.context().newPage()
|
||||
|
||||
// Set up monitoring before navigation
|
||||
await newPage.addInitScript(() => {
|
||||
// This runs before any page scripts
|
||||
window.__capturedMessages = {
|
||||
clientFeatureFlags: null,
|
||||
serverFeatureFlags: null
|
||||
}
|
||||
|
||||
// Capture outgoing client messages
|
||||
const originalSend = WebSocket.prototype.send
|
||||
WebSocket.prototype.send = function (data) {
|
||||
try {
|
||||
const parsed = JSON.parse(data)
|
||||
if (parsed.type === 'feature_flags') {
|
||||
window.__capturedMessages.clientFeatureFlags = parsed
|
||||
}
|
||||
} catch (e) {
|
||||
// Not JSON, ignore
|
||||
}
|
||||
return originalSend.call(this, data)
|
||||
}
|
||||
|
||||
// Monitor for server feature flags
|
||||
const checkInterval = setInterval(() => {
|
||||
if (
|
||||
window['app']?.api?.serverFeatureFlags &&
|
||||
Object.keys(window['app'].api.serverFeatureFlags).length > 0
|
||||
) {
|
||||
window.__capturedMessages.serverFeatureFlags =
|
||||
window['app'].api.serverFeatureFlags
|
||||
clearInterval(checkInterval)
|
||||
}
|
||||
}, 100)
|
||||
|
||||
// Clear after 10 seconds
|
||||
setTimeout(() => clearInterval(checkInterval), 10000)
|
||||
})
|
||||
|
||||
// Navigate to the app
|
||||
await newPage.goto(comfyPage.url)
|
||||
|
||||
// Wait for both client and server feature flags
|
||||
await newPage.waitForFunction(
|
||||
() =>
|
||||
window.__capturedMessages.clientFeatureFlags !== null &&
|
||||
window.__capturedMessages.serverFeatureFlags !== null,
|
||||
{ timeout: 10000 }
|
||||
)
|
||||
|
||||
// Get the captured messages
|
||||
const messages = await newPage.evaluate(() => window.__capturedMessages)
|
||||
|
||||
// Verify client sent feature flags
|
||||
expect(messages.clientFeatureFlags).toBeTruthy()
|
||||
expect(messages.clientFeatureFlags).toHaveProperty('type', 'feature_flags')
|
||||
expect(messages.clientFeatureFlags).toHaveProperty('data')
|
||||
expect(messages.clientFeatureFlags.data).toHaveProperty(
|
||||
'supports_preview_metadata'
|
||||
)
|
||||
expect(
|
||||
typeof messages.clientFeatureFlags.data.supports_preview_metadata
|
||||
).toBe('boolean')
|
||||
|
||||
// Verify server sent feature flags back
|
||||
expect(messages.serverFeatureFlags).toBeTruthy()
|
||||
expect(messages.serverFeatureFlags).toHaveProperty(
|
||||
'supports_preview_metadata'
|
||||
)
|
||||
expect(typeof messages.serverFeatureFlags.supports_preview_metadata).toBe(
|
||||
'boolean'
|
||||
)
|
||||
expect(messages.serverFeatureFlags).toHaveProperty('max_upload_size')
|
||||
expect(typeof messages.serverFeatureFlags.max_upload_size).toBe('number')
|
||||
expect(Object.keys(messages.serverFeatureFlags).length).toBeGreaterThan(0)
|
||||
|
||||
await newPage.close()
|
||||
})
|
||||
|
||||
test('Server feature flags are received and accessible', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Wait for connection to establish
|
||||
await comfyPage.page.waitForTimeout(1000)
|
||||
|
||||
// Get the actual server feature flags from the backend
|
||||
const serverFlags = await comfyPage.page.evaluate(() => {
|
||||
return window['app'].api.serverFeatureFlags
|
||||
})
|
||||
|
||||
// Verify we received real feature flags from the backend
|
||||
expect(serverFlags).toBeTruthy()
|
||||
expect(Object.keys(serverFlags).length).toBeGreaterThan(0)
|
||||
|
||||
// The backend should send feature flags
|
||||
expect(serverFlags).toHaveProperty('supports_preview_metadata')
|
||||
expect(typeof serverFlags.supports_preview_metadata).toBe('boolean')
|
||||
expect(serverFlags).toHaveProperty('max_upload_size')
|
||||
expect(typeof serverFlags.max_upload_size).toBe('number')
|
||||
})
|
||||
|
||||
test('serverSupportsFeature method works with real backend flags', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Wait for connection
|
||||
await comfyPage.page.waitForTimeout(1000)
|
||||
|
||||
// Test serverSupportsFeature with real backend flags
|
||||
const supportsPreviewMetadata = await comfyPage.page.evaluate(() => {
|
||||
return window['app'].api.serverSupportsFeature(
|
||||
'supports_preview_metadata'
|
||||
)
|
||||
})
|
||||
// The method should return a boolean based on the backend's value
|
||||
expect(typeof supportsPreviewMetadata).toBe('boolean')
|
||||
|
||||
// Test non-existent feature - should always return false
|
||||
const supportsNonExistent = await comfyPage.page.evaluate(() => {
|
||||
return window['app'].api.serverSupportsFeature('non_existent_feature_xyz')
|
||||
})
|
||||
expect(supportsNonExistent).toBe(false)
|
||||
|
||||
// Test that the method only returns true for boolean true values
|
||||
const testResults = await comfyPage.page.evaluate(() => {
|
||||
// Temporarily modify serverFeatureFlags to test behavior
|
||||
const original = window['app'].api.serverFeatureFlags
|
||||
window['app'].api.serverFeatureFlags = {
|
||||
bool_true: true,
|
||||
bool_false: false,
|
||||
string_value: 'yes',
|
||||
number_value: 1,
|
||||
null_value: null
|
||||
}
|
||||
|
||||
const results = {
|
||||
bool_true: window['app'].api.serverSupportsFeature('bool_true'),
|
||||
bool_false: window['app'].api.serverSupportsFeature('bool_false'),
|
||||
string_value: window['app'].api.serverSupportsFeature('string_value'),
|
||||
number_value: window['app'].api.serverSupportsFeature('number_value'),
|
||||
null_value: window['app'].api.serverSupportsFeature('null_value')
|
||||
}
|
||||
|
||||
// Restore original
|
||||
window['app'].api.serverFeatureFlags = original
|
||||
return results
|
||||
})
|
||||
|
||||
// serverSupportsFeature should only return true for boolean true values
|
||||
expect(testResults.bool_true).toBe(true)
|
||||
expect(testResults.bool_false).toBe(false)
|
||||
expect(testResults.string_value).toBe(false)
|
||||
expect(testResults.number_value).toBe(false)
|
||||
expect(testResults.null_value).toBe(false)
|
||||
})
|
||||
|
||||
test('getServerFeature method works with real backend data', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Wait for connection
|
||||
await comfyPage.page.waitForTimeout(1000)
|
||||
|
||||
// Test getServerFeature method
|
||||
const previewMetadataValue = await comfyPage.page.evaluate(() => {
|
||||
return window['app'].api.getServerFeature('supports_preview_metadata')
|
||||
})
|
||||
expect(typeof previewMetadataValue).toBe('boolean')
|
||||
|
||||
// Test getting max_upload_size
|
||||
const maxUploadSize = await comfyPage.page.evaluate(() => {
|
||||
return window['app'].api.getServerFeature('max_upload_size')
|
||||
})
|
||||
expect(typeof maxUploadSize).toBe('number')
|
||||
expect(maxUploadSize).toBeGreaterThan(0)
|
||||
|
||||
// Test getServerFeature with default value for non-existent feature
|
||||
const defaultValue = await comfyPage.page.evaluate(() => {
|
||||
return window['app'].api.getServerFeature(
|
||||
'non_existent_feature_xyz',
|
||||
'default'
|
||||
)
|
||||
})
|
||||
expect(defaultValue).toBe('default')
|
||||
})
|
||||
|
||||
test('getServerFeatures returns all backend feature flags', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Wait for connection
|
||||
await comfyPage.page.waitForTimeout(1000)
|
||||
|
||||
// Test getServerFeatures returns all flags
|
||||
const allFeatures = await comfyPage.page.evaluate(() => {
|
||||
return window['app'].api.getServerFeatures()
|
||||
})
|
||||
|
||||
expect(allFeatures).toBeTruthy()
|
||||
expect(allFeatures).toHaveProperty('supports_preview_metadata')
|
||||
expect(typeof allFeatures.supports_preview_metadata).toBe('boolean')
|
||||
expect(allFeatures).toHaveProperty('max_upload_size')
|
||||
expect(Object.keys(allFeatures).length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test('Client feature flags are immutable', async ({ comfyPage }) => {
|
||||
// Test that getClientFeatureFlags returns a copy
|
||||
const immutabilityTest = await comfyPage.page.evaluate(() => {
|
||||
const flags1 = window['app'].api.getClientFeatureFlags()
|
||||
const flags2 = window['app'].api.getClientFeatureFlags()
|
||||
|
||||
// Modify the first object
|
||||
flags1.test_modification = true
|
||||
|
||||
// Get flags again to check if original was modified
|
||||
const flags3 = window['app'].api.getClientFeatureFlags()
|
||||
|
||||
return {
|
||||
areEqual: flags1 === flags2,
|
||||
hasModification: flags3.test_modification !== undefined,
|
||||
hasSupportsPreview: flags1.supports_preview_metadata !== undefined,
|
||||
supportsPreviewValue: flags1.supports_preview_metadata
|
||||
}
|
||||
})
|
||||
|
||||
// Verify they are different objects (not the same reference)
|
||||
expect(immutabilityTest.areEqual).toBe(false)
|
||||
|
||||
// Verify modification didn't affect the original
|
||||
expect(immutabilityTest.hasModification).toBe(false)
|
||||
|
||||
// Verify the flags contain expected properties
|
||||
expect(immutabilityTest.hasSupportsPreview).toBe(true)
|
||||
expect(typeof immutabilityTest.supportsPreviewValue).toBe('boolean') // From clientFeatureFlags.json
|
||||
})
|
||||
|
||||
test('Server features are immutable when accessed via getServerFeatures', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Wait for connection to establish
|
||||
await comfyPage.page.waitForTimeout(1000)
|
||||
|
||||
const immutabilityTest = await comfyPage.page.evaluate(() => {
|
||||
// Get a copy of server features
|
||||
const features1 = window['app'].api.getServerFeatures()
|
||||
|
||||
// Try to modify it
|
||||
features1.supports_preview_metadata = false
|
||||
features1.new_feature = 'added'
|
||||
|
||||
// Get another copy
|
||||
const features2 = window['app'].api.getServerFeatures()
|
||||
|
||||
return {
|
||||
modifiedValue: features1.supports_preview_metadata,
|
||||
originalValue: features2.supports_preview_metadata,
|
||||
hasNewFeature: features2.new_feature !== undefined,
|
||||
hasSupportsPreview: features2.supports_preview_metadata !== undefined
|
||||
}
|
||||
})
|
||||
|
||||
// The modification should only affect the copy
|
||||
expect(immutabilityTest.modifiedValue).toBe(false)
|
||||
expect(typeof immutabilityTest.originalValue).toBe('boolean') // Backend sends boolean for supports_preview_metadata
|
||||
expect(immutabilityTest.hasNewFeature).toBe(false)
|
||||
expect(immutabilityTest.hasSupportsPreview).toBe(true)
|
||||
})
|
||||
|
||||
test('Feature flags are negotiated early in connection lifecycle', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// This test verifies that feature flags are available early in the app lifecycle
|
||||
// which is important for protocol negotiation
|
||||
|
||||
// Create a new page to ensure clean state
|
||||
const newPage = await comfyPage.page.context().newPage()
|
||||
|
||||
// Set up monitoring before navigation
|
||||
await newPage.addInitScript(() => {
|
||||
// Track when various app components are ready
|
||||
;(window as any).__appReadiness = {
|
||||
featureFlagsReceived: false,
|
||||
apiInitialized: false,
|
||||
appInitialized: false
|
||||
}
|
||||
|
||||
// Monitor when feature flags arrive by checking periodically
|
||||
const checkFeatureFlags = setInterval(() => {
|
||||
if (
|
||||
window['app']?.api?.serverFeatureFlags?.supports_preview_metadata !==
|
||||
undefined
|
||||
) {
|
||||
;(window as any).__appReadiness.featureFlagsReceived = true
|
||||
clearInterval(checkFeatureFlags)
|
||||
}
|
||||
}, 10)
|
||||
|
||||
// Monitor API initialization
|
||||
const checkApi = setInterval(() => {
|
||||
if (window['app']?.api) {
|
||||
;(window as any).__appReadiness.apiInitialized = true
|
||||
clearInterval(checkApi)
|
||||
}
|
||||
}, 10)
|
||||
|
||||
// Monitor app initialization
|
||||
const checkApp = setInterval(() => {
|
||||
if (window['app']?.graph) {
|
||||
;(window as any).__appReadiness.appInitialized = true
|
||||
clearInterval(checkApp)
|
||||
}
|
||||
}, 10)
|
||||
|
||||
// Clean up after 10 seconds
|
||||
setTimeout(() => {
|
||||
clearInterval(checkFeatureFlags)
|
||||
clearInterval(checkApi)
|
||||
clearInterval(checkApp)
|
||||
}, 10000)
|
||||
})
|
||||
|
||||
// Navigate to the app
|
||||
await newPage.goto(comfyPage.url)
|
||||
|
||||
// Wait for feature flags to be received
|
||||
await newPage.waitForFunction(
|
||||
() =>
|
||||
window['app']?.api?.serverFeatureFlags?.supports_preview_metadata !==
|
||||
undefined,
|
||||
{
|
||||
timeout: 10000
|
||||
}
|
||||
)
|
||||
|
||||
// Get readiness state
|
||||
const readiness = await newPage.evaluate(() => {
|
||||
return {
|
||||
...(window as any).__appReadiness,
|
||||
currentFlags: window['app'].api.serverFeatureFlags
|
||||
}
|
||||
})
|
||||
|
||||
// Verify feature flags are available
|
||||
expect(readiness.currentFlags).toHaveProperty('supports_preview_metadata')
|
||||
expect(typeof readiness.currentFlags.supports_preview_metadata).toBe(
|
||||
'boolean'
|
||||
)
|
||||
expect(readiness.currentFlags).toHaveProperty('max_upload_size')
|
||||
|
||||
// Verify feature flags were received (we detected them via polling)
|
||||
expect(readiness.featureFlagsReceived).toBe(true)
|
||||
|
||||
// Verify API was initialized (feature flags require API)
|
||||
expect(readiness.apiInitialized).toBe(true)
|
||||
|
||||
await newPage.close()
|
||||
})
|
||||
|
||||
test('Backend /features endpoint returns feature flags', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Test the HTTP endpoint directly
|
||||
const response = await comfyPage.page.request.get(
|
||||
`${comfyPage.url}/api/features`
|
||||
)
|
||||
expect(response.ok()).toBe(true)
|
||||
|
||||
const features = await response.json()
|
||||
expect(features).toBeTruthy()
|
||||
expect(features).toHaveProperty('supports_preview_metadata')
|
||||
expect(typeof features.supports_preview_metadata).toBe('boolean')
|
||||
expect(features).toHaveProperty('max_upload_size')
|
||||
expect(Object.keys(features).length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
@@ -336,4 +336,54 @@ test.describe('Node library sidebar', () => {
|
||||
await comfyPage.page.waitForTimeout(1000)
|
||||
expect(await tab.getNode('KSampler (Advanced)').count()).toBe(2)
|
||||
})
|
||||
|
||||
test('Subgraph node display name updates in library when renamed', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Load a workflow with subgraphs to populate the node library
|
||||
await comfyPage.loadWorkflow('nested-subgraph')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const tab = comfyPage.menu.nodeLibraryTab
|
||||
|
||||
// Navigate to subgraph folder in node library
|
||||
await tab.getFolder('subgraph').click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Get initial subgraph node name in the library
|
||||
const initialSubgraphNodeCount = await tab.getNode('Subgraph Node').count()
|
||||
expect(initialSubgraphNodeCount).toBeGreaterThan(0)
|
||||
|
||||
// Get the subgraph node by ID (node 10 is the subgraph)
|
||||
const subgraphNode = await comfyPage.getNodeRefById('10')
|
||||
const nodePos = await subgraphNode.getPosition()
|
||||
const nodeSize = await subgraphNode.getSize()
|
||||
|
||||
// Double-click on the title area of the subgraph node to edit
|
||||
await comfyPage.canvas.dblclick({
|
||||
position: {
|
||||
x: nodePos.x + nodeSize.width / 2,
|
||||
y: nodePos.y - 10 // Title area is above the node body
|
||||
},
|
||||
delay: 5
|
||||
})
|
||||
|
||||
// Wait for title editor to appear
|
||||
await expect(comfyPage.page.locator('.node-title-editor')).toBeVisible()
|
||||
|
||||
// Clear existing text and type new title
|
||||
await comfyPage.page.keyboard.press('Control+a')
|
||||
const newTitle = 'Renamed Subgraph Node'
|
||||
await comfyPage.page.keyboard.type(newTitle)
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
|
||||
// Wait a frame for the update to complete
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Verify the node library shows the updated title
|
||||
expect(await tab.getNode(newTitle).count()).toBeGreaterThan(0)
|
||||
|
||||
// Verify the old node name is no longer in the library
|
||||
expect(await tab.getNode('Subgraph Node').count()).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
55
browser_tests/tests/subgraph/subgraphPersistence.spec.ts
Normal file
55
browser_tests/tests/subgraph/subgraphPersistence.spec.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Subgraph Persistence', () => {
|
||||
test('Node library updates when subgraph title changes', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Load a workflow with subgraphs to populate the node library
|
||||
await comfyPage.loadWorkflow('nested-subgraph')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const tab = comfyPage.menu.nodeLibraryTab
|
||||
|
||||
// Navigate to subgraph folder in node library
|
||||
await tab.getFolder('subgraph').click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Get initial subgraph node name in the library
|
||||
const initialSubgraphNodeCount = await tab.getNode('Subgraph Node').count()
|
||||
expect(initialSubgraphNodeCount).toBeGreaterThan(0)
|
||||
|
||||
// Get the subgraph node by ID (node 10 is the subgraph)
|
||||
const subgraphNode = await comfyPage.getNodeRefById('10')
|
||||
const nodePos = await subgraphNode.getPosition()
|
||||
const nodeSize = await subgraphNode.getSize()
|
||||
|
||||
// Double-click on the title area of the subgraph node to edit
|
||||
await comfyPage.canvas.dblclick({
|
||||
position: {
|
||||
x: nodePos.x + nodeSize.width / 2,
|
||||
y: nodePos.y - 10 // Title area is above the node body
|
||||
},
|
||||
delay: 5
|
||||
})
|
||||
|
||||
// Wait for title editor to appear
|
||||
await expect(comfyPage.page.locator('.node-title-editor')).toBeVisible()
|
||||
|
||||
// Clear existing text and type new title
|
||||
await comfyPage.page.keyboard.press('Control+a')
|
||||
const newTitle = 'Renamed Subgraph Node'
|
||||
await comfyPage.page.keyboard.type(newTitle)
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
|
||||
// Wait a frame for the update to complete
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Verify the node library shows the updated title
|
||||
expect(await tab.getNode(newTitle).count()).toBeGreaterThan(0)
|
||||
|
||||
// Verify the old node name is no longer in the library
|
||||
expect(await tab.getNode('Subgraph Node').count()).toBe(0)
|
||||
})
|
||||
})
|
||||
72
dangerfile.ts
Normal file
72
dangerfile.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { danger, fail } from 'danger'
|
||||
|
||||
// Check if we should run the checks
|
||||
const shouldRunChecks = async () => {
|
||||
const allChangedFiles = [
|
||||
...danger.git.modified_files,
|
||||
...danger.git.created_files
|
||||
]
|
||||
const srcChanges = allChangedFiles.filter((file) => file.startsWith('src/'))
|
||||
|
||||
if (srcChanges.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check total lines changed in src files
|
||||
let totalLinesChanged = 0
|
||||
for (const file of srcChanges) {
|
||||
const diff = await danger.git.diffForFile(file)
|
||||
if (diff) {
|
||||
// Count only lines with actual content (non-empty after trimming whitespace)
|
||||
// This excludes empty lines and lines containing only spaces/tabs
|
||||
const additions =
|
||||
diff.added?.split('\n').filter((line) => line.trim()).length || 0
|
||||
const deletions =
|
||||
diff.removed?.split('\n').filter((line) => line.trim()).length || 0
|
||||
totalLinesChanged += additions + deletions
|
||||
}
|
||||
}
|
||||
|
||||
return totalLinesChanged > 3
|
||||
}
|
||||
|
||||
// Check if browser tests were updated
|
||||
const checkBrowserTestCoverage = () => {
|
||||
const allChangedFiles = [
|
||||
...danger.git.modified_files,
|
||||
...danger.git.created_files
|
||||
]
|
||||
const hasBrowserTestChanges = allChangedFiles.some(
|
||||
(file) => file.startsWith('browser_tests/') && file.endsWith('.ts')
|
||||
)
|
||||
|
||||
if (!hasBrowserTestChanges) {
|
||||
fail(`🧪 **E2E Test Coverage Missing**
|
||||
|
||||
All changes should be covered under E2E testing. Please add or update browser tests.`)
|
||||
}
|
||||
}
|
||||
|
||||
// Check for screen recording in PR description
|
||||
const checkScreenRecording = () => {
|
||||
const description = danger.github.pr.body || ''
|
||||
const hasRecording =
|
||||
/github\.com\/user-attachments\/assets\/[a-f0-9-]+/i.test(description) ||
|
||||
/youtube\.com\/watch|youtu\.be\//i.test(description)
|
||||
|
||||
if (!hasRecording) {
|
||||
fail(`📹 **Visual Documentation Missing**
|
||||
|
||||
Please add a screen recording or screenshot:
|
||||
- GitHub: Drag & drop media to PR description
|
||||
- YouTube: Add YouTube link`)
|
||||
}
|
||||
}
|
||||
|
||||
// Run the checks only if conditions are met
|
||||
shouldRunChecks().then((shouldRun) => {
|
||||
if (shouldRun) {
|
||||
checkBrowserTestCoverage()
|
||||
checkScreenRecording()
|
||||
}
|
||||
})
|
||||
806
package-lock.json
generated
806
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -45,6 +45,7 @@
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"chalk": "^5.3.0",
|
||||
"danger": "^13.0.4",
|
||||
"eslint": "^9.12.0",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"eslint-plugin-prettier": "^5.2.6",
|
||||
|
||||
@@ -38,14 +38,23 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import FileDownload from '@/components/common/FileDownload.vue'
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import { MODEL_SOURCES } from '@/constants/urls'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
|
||||
/** @todo Read this from server internal API rather than hardcoding here */
|
||||
const allowedSources = MODEL_SOURCES.allowedDomains
|
||||
// TODO: Read this from server internal API rather than hardcoding here
|
||||
// as some installations may wish to use custom sources
|
||||
const allowedSources = [
|
||||
'https://civitai.com/',
|
||||
'https://huggingface.co/',
|
||||
'http://localhost:' // Included for testing usage only
|
||||
]
|
||||
const allowedSuffixes = ['.safetensors', '.sft']
|
||||
const whiteListedUrls = new Set(MODEL_SOURCES.whitelistedUrls)
|
||||
// Models that fail above conditions but are still allowed
|
||||
const whiteListedUrls = new Set([
|
||||
'https://huggingface.co/stabilityai/stable-zero123/resolve/main/stable_zero123.ckpt',
|
||||
'https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_depth_sd14v1.pth?download=true',
|
||||
'https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth'
|
||||
])
|
||||
|
||||
interface ModelInfo {
|
||||
name: string
|
||||
|
||||
@@ -22,9 +22,11 @@ import EditableText from '@/components/common/EditableText.vue'
|
||||
import { useAbsolutePosition } from '@/composables/element/useAbsolutePosition'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useCanvasStore, useTitleEditorStore } from '@/stores/graphStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
|
||||
const showInput = ref(false)
|
||||
const editedTitle = ref('')
|
||||
@@ -48,6 +50,9 @@ const onEdit = (newValue: string) => {
|
||||
const target = titleEditorStore.titleEditorTarget
|
||||
if (target instanceof LGraphNode && target.isSubgraphNode?.()) {
|
||||
target.subgraph.name = trimmedTitle
|
||||
|
||||
// Also update the node definition display name in the store
|
||||
nodeDefStore.updateNodeDefDisplayName(target.type, trimmedTitle)
|
||||
}
|
||||
|
||||
app.graph.setDirtyCanvas(true, true)
|
||||
|
||||
@@ -123,7 +123,6 @@ import Button from 'primevue/button'
|
||||
import { type CSSProperties, computed, nextTick, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { COMFY_URLS, GITHUB_REPOS, getDesktopGuideUrl } from '@/constants/urls'
|
||||
import { type ReleaseNote } from '@/services/releaseService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useReleaseStore } from '@/stores/releaseStore'
|
||||
@@ -144,10 +143,11 @@ interface MenuItem {
|
||||
|
||||
// Constants
|
||||
const EXTERNAL_LINKS = {
|
||||
DOCS: COMFY_URLS.docs.base,
|
||||
DISCORD: COMFY_URLS.community.discord,
|
||||
GITHUB: GITHUB_REPOS.comfyui,
|
||||
UPDATE_GUIDE: COMFY_URLS.docs.installation.update
|
||||
DOCS: 'https://docs.comfy.org/',
|
||||
DISCORD: 'https://www.comfy.org/discord',
|
||||
GITHUB: 'https://github.com/comfyanonymous/ComfyUI',
|
||||
DESKTOP_GUIDE: 'https://comfyorg.notion.site/',
|
||||
UPDATE_GUIDE: 'https://docs.comfy.org/installation/update_comfyui'
|
||||
} as const
|
||||
|
||||
const TIME_UNITS = {
|
||||
@@ -199,7 +199,7 @@ const menuItems = computed<MenuItem[]>(() => {
|
||||
type: 'item',
|
||||
label: t('helpCenter.desktopUserGuide'),
|
||||
action: () => {
|
||||
openExternalLink(getDesktopGuideUrl(locale.value))
|
||||
openExternalLink(EXTERNAL_LINKS.DESKTOP_GUIDE)
|
||||
emit('close')
|
||||
}
|
||||
},
|
||||
@@ -451,13 +451,13 @@ const onUpdate = (_: ReleaseNote): void => {
|
||||
const getChangelogUrl = (): string => {
|
||||
const isChineseLocale = locale.value === 'zh'
|
||||
return isChineseLocale
|
||||
? COMFY_URLS.docs.changelog.zh
|
||||
: COMFY_URLS.docs.changelog.en
|
||||
? 'https://docs.comfy.org/zh-CN/changelog'
|
||||
: 'https://docs.comfy.org/changelog'
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
onMounted(async () => {
|
||||
if (showVersionUpdates.value && !hasReleases.value) {
|
||||
if (!hasReleases.value) {
|
||||
await releaseStore.fetchReleases()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -48,7 +48,6 @@
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { COMFY_URLS } from '@/constants/urls'
|
||||
import type { ReleaseNote } from '@/services/releaseService'
|
||||
import { useReleaseStore } from '@/stores/releaseStore'
|
||||
import { formatVersionAnchor } from '@/utils/formatUtil'
|
||||
@@ -73,8 +72,8 @@ const shouldShow = computed(
|
||||
const changelogUrl = computed(() => {
|
||||
const isChineseLocale = locale.value === 'zh'
|
||||
const baseUrl = isChineseLocale
|
||||
? COMFY_URLS.docs.changelog.zh
|
||||
: COMFY_URLS.docs.changelog.en
|
||||
? 'https://docs.comfy.org/zh-CN/changelog'
|
||||
: 'https://docs.comfy.org/changelog'
|
||||
|
||||
if (latestRelease.value?.version) {
|
||||
const versionAnchor = formatVersionAnchor(latestRelease.value.version)
|
||||
@@ -121,7 +120,7 @@ const handleLearnMore = () => {
|
||||
}
|
||||
|
||||
const handleUpdate = () => {
|
||||
window.open(COMFY_URLS.docs.installation.update, '_blank')
|
||||
window.open('https://docs.comfy.org/installation/update_comfyui', '_blank')
|
||||
dismissToast()
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,6 @@ import { marked } from 'marked'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { COMFY_URLS } from '@/constants/urls'
|
||||
import type { ReleaseNote } from '@/services/releaseService'
|
||||
import { useReleaseStore } from '@/stores/releaseStore'
|
||||
import { formatVersionAnchor } from '@/utils/formatUtil'
|
||||
@@ -93,8 +92,8 @@ const shouldShow = computed(
|
||||
const changelogUrl = computed(() => {
|
||||
const isChineseLocale = locale.value === 'zh'
|
||||
const baseUrl = isChineseLocale
|
||||
? COMFY_URLS.docs.changelog.zh
|
||||
: COMFY_URLS.docs.changelog.en
|
||||
? 'https://docs.comfy.org/zh-CN/changelog'
|
||||
: 'https://docs.comfy.org/changelog'
|
||||
|
||||
if (latestRelease.value?.version) {
|
||||
const versionAnchor = formatVersionAnchor(latestRelease.value.version)
|
||||
@@ -218,8 +217,7 @@ defineExpose({
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
overflow: hidden;
|
||||
padding: 32px 32px 24px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ import Panel from 'primevue/panel'
|
||||
import { ModelRef, computed, onMounted, ref } from 'vue'
|
||||
|
||||
import MirrorItem from '@/components/install/mirror/MirrorItem.vue'
|
||||
import { PYPI_MIRROR, PYTHON_MIRROR, UVMirror } from '@/constants/mirrors'
|
||||
import { PYPI_MIRROR, PYTHON_MIRROR, UVMirror } from '@/constants/uvMirrors'
|
||||
import { t } from '@/i18n'
|
||||
import { isInChina } from '@/utils/networkUtil'
|
||||
import { ValidationState, mergeValidationStates } from '@/utils/validationUtil'
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import UrlInput from '@/components/common/UrlInput.vue'
|
||||
import { UVMirror } from '@/constants/mirrors'
|
||||
import { UVMirror } from '@/constants/uvMirrors'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
import { checkMirrorReachable } from '@/utils/networkUtil'
|
||||
import { ValidationState } from '@/utils/validationUtil'
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
DEFAULT_DARK_COLOR_PALETTE,
|
||||
DEFAULT_LIGHT_COLOR_PALETTE
|
||||
} from '@/constants/coreColorPalettes'
|
||||
import { COMFY_URLS, GITHUB_REPOS } from '@/constants/urls'
|
||||
import { t } from '@/i18n'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
@@ -544,7 +543,10 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
menubarLabel: 'ComfyUI Issues',
|
||||
versionAdded: '1.5.5',
|
||||
function: () => {
|
||||
window.open(GITHUB_REPOS.comfyuiIssues, '_blank')
|
||||
window.open(
|
||||
'https://github.com/comfyanonymous/ComfyUI/issues',
|
||||
'_blank'
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -554,7 +556,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
menubarLabel: 'ComfyUI Docs',
|
||||
versionAdded: '1.5.5',
|
||||
function: () => {
|
||||
window.open(COMFY_URLS.docs.base, '_blank')
|
||||
window.open('https://docs.comfy.org/', '_blank')
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -564,7 +566,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
menubarLabel: 'Comfy-Org Discord',
|
||||
versionAdded: '1.5.5',
|
||||
function: () => {
|
||||
window.open(COMFY_URLS.community.discord, '_blank')
|
||||
window.open('https://www.comfy.org/discord', '_blank')
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -644,7 +646,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
menubarLabel: 'ComfyUI Forum',
|
||||
versionAdded: '1.8.2',
|
||||
function: () => {
|
||||
window.open(COMFY_URLS.community.forum.base, '_blank')
|
||||
window.open('https://forum.comfy.org/', '_blank')
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"supports_preview_metadata": false
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
/**
|
||||
* Base domain configuration and core website URLs
|
||||
* Forkers can change the base domain to use their own
|
||||
*/
|
||||
export const COMFY_BASE_DOMAIN =
|
||||
import.meta.env.VITE_COMFY_BASE_DOMAIN || 'comfy.org'
|
||||
|
||||
const WEBSITE_BASE_URL = `https://www.${COMFY_BASE_DOMAIN}`
|
||||
|
||||
export const COMFY_WEBSITE_URLS = {
|
||||
base: WEBSITE_BASE_URL,
|
||||
termsOfService: `${WEBSITE_BASE_URL}/terms-of-service`,
|
||||
privacy: `${WEBSITE_BASE_URL}/privacy`
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
/**
|
||||
* URL constants for ComfyUI frontend
|
||||
* Centralized location for all URL references
|
||||
*/
|
||||
import { COMFY_BASE_DOMAIN } from '@/config/comfyDomain'
|
||||
|
||||
const DOCS_BASE_URL = `https://docs.${COMFY_BASE_DOMAIN}`
|
||||
const FORUM_BASE_URL = `https://forum.${COMFY_BASE_DOMAIN}`
|
||||
const WEBSITE_BASE_URL = `https://www.${COMFY_BASE_DOMAIN}`
|
||||
|
||||
export const COMFY_URLS = {
|
||||
website: {
|
||||
base: WEBSITE_BASE_URL,
|
||||
termsOfService: `${WEBSITE_BASE_URL}/terms-of-service`,
|
||||
privacy: `${WEBSITE_BASE_URL}/privacy`
|
||||
},
|
||||
docs: {
|
||||
base: DOCS_BASE_URL,
|
||||
changelog: {
|
||||
en: `${DOCS_BASE_URL}/changelog`,
|
||||
zh: `${DOCS_BASE_URL}/zh-CN/changelog`
|
||||
},
|
||||
installation: {
|
||||
update: `${DOCS_BASE_URL}/installation/update_comfyui`
|
||||
},
|
||||
tutorials: {
|
||||
apiNodes: {
|
||||
overview: `${DOCS_BASE_URL}/tutorials/api-nodes/overview`,
|
||||
faq: `${DOCS_BASE_URL}/tutorials/api-nodes/faq`,
|
||||
pricing: `${DOCS_BASE_URL}/tutorials/api-nodes/pricing`,
|
||||
apiKeyLogin: `${DOCS_BASE_URL}/interface/user#logging-in-with-an-api-key`,
|
||||
nonWhitelistedLogin: `${DOCS_BASE_URL}/tutorials/api-nodes/overview#log-in-with-api-key-on-non-whitelisted-websites`
|
||||
}
|
||||
},
|
||||
getLocalized: (path: string, locale: string) => {
|
||||
return locale === 'zh' || locale === 'zh-CN'
|
||||
? `${DOCS_BASE_URL}/zh-CN/${path}`
|
||||
: `${DOCS_BASE_URL}/${path}`
|
||||
}
|
||||
},
|
||||
community: {
|
||||
discord: `${WEBSITE_BASE_URL}/discord`,
|
||||
forum: {
|
||||
base: `${FORUM_BASE_URL}/`,
|
||||
v1Feedback: `${FORUM_BASE_URL}/c/v1-feedback/`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const GITHUB_REPOS = {
|
||||
comfyui: 'https://github.com/comfyanonymous/ComfyUI',
|
||||
comfyuiIssues: 'https://github.com/comfyanonymous/ComfyUI/issues',
|
||||
frontend: 'https://github.com/Comfy-Org/ComfyUI_frontend',
|
||||
electron: 'https://github.com/Comfy-Org/electron',
|
||||
desktopPlatforms:
|
||||
'https://github.com/Comfy-Org/desktop#currently-supported-platforms'
|
||||
}
|
||||
|
||||
export const MODEL_SOURCES = {
|
||||
repos: {
|
||||
civitai: 'https://civitai.com/',
|
||||
huggingface: 'https://huggingface.co/'
|
||||
},
|
||||
whitelistedUrls: [
|
||||
'https://huggingface.co/stabilityai/stable-zero123/resolve/main/stable_zero123.ckpt',
|
||||
'https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_depth_sd14v1.pth?download=true',
|
||||
'https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth'
|
||||
],
|
||||
allowedDomains: [
|
||||
'https://civitai.com/',
|
||||
'https://huggingface.co/',
|
||||
'http://localhost:' // TODO: Remove in production
|
||||
]
|
||||
}
|
||||
|
||||
export const DEVELOPER_TOOLS = {
|
||||
git: 'https://git-scm.com/downloads/',
|
||||
vcRedist: 'https://aka.ms/vs/17/release/vc_redist.x64.exe',
|
||||
uv: 'https://docs.astral.sh/uv/getting-started/installation/'
|
||||
}
|
||||
|
||||
// Platform and locale-aware desktop guide URL generator
|
||||
export const getDesktopGuideUrl = (locale: string): string => {
|
||||
const isChineseLocale = locale === 'zh'
|
||||
const isMacOS =
|
||||
typeof navigator !== 'undefined' &&
|
||||
navigator.platform.toUpperCase().indexOf('MAC') >= 0
|
||||
|
||||
const platform = isMacOS ? 'macos' : 'windows'
|
||||
const baseUrl = isChineseLocale
|
||||
? `https://docs.${COMFY_BASE_DOMAIN}/zh-CN/installation/desktop`
|
||||
: `https://docs.${COMFY_BASE_DOMAIN}/installation/desktop`
|
||||
|
||||
return `${baseUrl}/${platform}`
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import log from 'loglevel'
|
||||
|
||||
import { PYTHON_MIRROR } from '@/constants/mirrors'
|
||||
import { getDesktopGuideUrl } from '@/constants/urls'
|
||||
import { i18n, t } from '@/i18n'
|
||||
import { PYTHON_MIRROR } from '@/constants/uvMirrors'
|
||||
import { t } from '@/i18n'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
@@ -160,7 +159,7 @@ import { checkMirrorReachable } from '@/utils/networkUtil'
|
||||
label: 'Desktop User Guide',
|
||||
icon: 'pi pi-book',
|
||||
function() {
|
||||
window.open(getDesktopGuideUrl(i18n.global.locale.value), '_blank')
|
||||
window.open('https://comfyorg.notion.site/', '_blank')
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -113,8 +113,6 @@ const zLogRawResponse = z.object({
|
||||
entries: z.array(zLogEntry)
|
||||
})
|
||||
|
||||
const zFeatureFlagsWsMessage = z.record(z.string(), z.any())
|
||||
|
||||
export type StatusWsMessageStatus = z.infer<typeof zStatusWsMessageStatus>
|
||||
export type StatusWsMessage = z.infer<typeof zStatusWsMessage>
|
||||
export type ProgressWsMessage = z.infer<typeof zProgressWsMessage>
|
||||
@@ -134,7 +132,6 @@ export type ProgressTextWsMessage = z.infer<typeof zProgressTextWsMessage>
|
||||
export type DisplayComponentWsMessage = z.infer<
|
||||
typeof zDisplayComponentWsMessage
|
||||
>
|
||||
export type FeatureFlagsWsMessage = z.infer<typeof zFeatureFlagsWsMessage>
|
||||
// End of ws messages
|
||||
|
||||
const zPromptInputItem = z.object({
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import axios from 'axios'
|
||||
|
||||
import defaultClientFeatureFlags from '@/config/clientFeatureFlags.json'
|
||||
import type {
|
||||
DisplayComponentWsMessage,
|
||||
EmbeddingsResponse,
|
||||
@@ -12,7 +11,6 @@ import type {
|
||||
ExecutionStartWsMessage,
|
||||
ExecutionSuccessWsMessage,
|
||||
ExtensionsResponse,
|
||||
FeatureFlagsWsMessage,
|
||||
HistoryTaskItem,
|
||||
LogsRawResponse,
|
||||
LogsWsMessage,
|
||||
@@ -107,7 +105,6 @@ interface BackendApiCalls {
|
||||
b_preview: Blob
|
||||
progress_text: ProgressTextWsMessage
|
||||
display_component: DisplayComponentWsMessage
|
||||
feature_flags: FeatureFlagsWsMessage
|
||||
}
|
||||
|
||||
/** Dictionary of all api calls */
|
||||
@@ -237,19 +234,6 @@ export class ComfyApi extends EventTarget {
|
||||
|
||||
reportedUnknownMessageTypes = new Set<string>()
|
||||
|
||||
/**
|
||||
* Get feature flags supported by this frontend client.
|
||||
* Returns a copy to prevent external modification.
|
||||
*/
|
||||
getClientFeatureFlags(): Record<string, unknown> {
|
||||
return { ...defaultClientFeatureFlags }
|
||||
}
|
||||
|
||||
/**
|
||||
* Feature flags received from the backend server.
|
||||
*/
|
||||
serverFeatureFlags: Record<string, unknown> = {}
|
||||
|
||||
/**
|
||||
* The auth token for the comfy org account if the user is logged in.
|
||||
* This is only used for {@link queuePrompt} now. It is not directly
|
||||
@@ -391,15 +375,6 @@ export class ComfyApi extends EventTarget {
|
||||
|
||||
this.socket.addEventListener('open', () => {
|
||||
opened = true
|
||||
|
||||
// Send feature flags as the first message
|
||||
this.socket!.send(
|
||||
JSON.stringify({
|
||||
type: 'feature_flags',
|
||||
data: this.getClientFeatureFlags()
|
||||
})
|
||||
)
|
||||
|
||||
if (isReconnect) {
|
||||
this.dispatchCustomEvent('reconnected')
|
||||
}
|
||||
@@ -493,14 +468,6 @@ export class ComfyApi extends EventTarget {
|
||||
case 'b_preview':
|
||||
this.dispatchCustomEvent(msg.type, msg.data)
|
||||
break
|
||||
case 'feature_flags':
|
||||
// Store server feature flags
|
||||
this.serverFeatureFlags = msg.data
|
||||
console.log(
|
||||
'Server feature flags received:',
|
||||
this.serverFeatureFlags
|
||||
)
|
||||
break
|
||||
default:
|
||||
if (this.#registered.has(msg.type)) {
|
||||
// Fallback for custom types - calls super direct.
|
||||
@@ -995,33 +962,6 @@ export class ComfyApi extends EventTarget {
|
||||
async getCustomNodesI18n(): Promise<Record<string, any>> {
|
||||
return (await axios.get(this.apiURL('/i18n'))).data
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the server supports a specific feature.
|
||||
* @param featureName The name of the feature to check
|
||||
* @returns true if the feature is supported, false otherwise
|
||||
*/
|
||||
serverSupportsFeature(featureName: string): boolean {
|
||||
return this.serverFeatureFlags[featureName] === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a server feature flag value.
|
||||
* @param featureName The name of the feature to get
|
||||
* @param defaultValue The default value if the feature is not found
|
||||
* @returns The feature value or default
|
||||
*/
|
||||
getServerFeature<T = unknown>(featureName: string, defaultValue?: T): T {
|
||||
return (this.serverFeatureFlags[featureName] ?? defaultValue) as T
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all server feature flags.
|
||||
* @returns Copy of all server feature flags
|
||||
*/
|
||||
getServerFeatures(): Record<string, unknown> {
|
||||
return { ...this.serverFeatureFlags }
|
||||
}
|
||||
}
|
||||
|
||||
export const api = new ComfyApi()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { COMFY_URLS, GITHUB_REPOS } from '@/constants/urls'
|
||||
import { AboutPageBadge } from '@/types/comfy'
|
||||
import { electronAPI, isElectron } from '@/utils/envUtil'
|
||||
|
||||
@@ -25,20 +24,20 @@ export const useAboutPanelStore = defineStore('aboutPanel', () => {
|
||||
? 'v' + electronAPI().getComfyUIVersion()
|
||||
: coreVersion.value
|
||||
}`,
|
||||
url: GITHUB_REPOS.comfyui,
|
||||
url: 'https://github.com/comfyanonymous/ComfyUI',
|
||||
icon: 'pi pi-github'
|
||||
},
|
||||
{
|
||||
label: `ComfyUI_frontend v${frontendVersion}`,
|
||||
url: GITHUB_REPOS.frontend,
|
||||
url: 'https://github.com/Comfy-Org/ComfyUI_frontend',
|
||||
icon: 'pi pi-github'
|
||||
},
|
||||
{
|
||||
label: 'Discord',
|
||||
url: COMFY_URLS.community.discord,
|
||||
url: 'https://www.comfy.org/discord',
|
||||
icon: 'pi pi-discord'
|
||||
},
|
||||
{ label: 'ComfyOrg', url: COMFY_URLS.website.base, icon: 'pi pi-globe' }
|
||||
{ label: 'ComfyOrg', url: 'https://www.comfy.org/', icon: 'pi pi-globe' }
|
||||
])
|
||||
|
||||
const allBadges = computed<AboutPageBadge[]>(() => [
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { type LGraphNode, LiteGraph } from '@comfyorg/litegraph'
|
||||
import axios from 'axios'
|
||||
import _ from 'lodash'
|
||||
import { defineStore } from 'pinia'
|
||||
@@ -304,6 +304,28 @@ export const useNodeDefStore = defineStore('nodeDef', () => {
|
||||
nodeDefsByName.value[nodeDef.name] = nodeDefImpl
|
||||
nodeDefsByDisplayName.value[nodeDef.display_name] = nodeDefImpl
|
||||
}
|
||||
function updateNodeDefDisplayName(nodeType: string, newDisplayName: string) {
|
||||
const existingNodeDef = nodeDefsByName.value[nodeType]
|
||||
if (!existingNodeDef) return
|
||||
|
||||
// Remove old display name mapping
|
||||
delete nodeDefsByDisplayName.value[existingNodeDef.display_name]
|
||||
|
||||
// Clone and mutate like bookmark store does to preserve references
|
||||
const clonedNodeDef = _.clone(existingNodeDef) as any
|
||||
clonedNodeDef.display_name = newDisplayName
|
||||
|
||||
// Update both mappings
|
||||
nodeDefsByName.value[nodeType] = clonedNodeDef
|
||||
nodeDefsByDisplayName.value[newDisplayName] = clonedNodeDef
|
||||
|
||||
// Also update the LiteGraph registered node type's static title
|
||||
// This is necessary for new instances to use the updated display name
|
||||
const RegisteredNodeClass = LiteGraph.registered_node_types[nodeType]
|
||||
if (RegisteredNodeClass) {
|
||||
RegisteredNodeClass.title = newDisplayName
|
||||
}
|
||||
}
|
||||
function fromLGraphNode(node: LGraphNode): ComfyNodeDefImpl | null {
|
||||
// Frontend-only nodes don't have nodeDef
|
||||
// @ts-expect-error Optional chaining used in index
|
||||
@@ -324,6 +346,7 @@ export const useNodeDefStore = defineStore('nodeDef', () => {
|
||||
|
||||
updateNodeDefs,
|
||||
addNodeDef,
|
||||
updateNodeDefDisplayName,
|
||||
fromLGraphNode
|
||||
}
|
||||
})
|
||||
|
||||
@@ -699,27 +699,6 @@ export interface paths {
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/publishers/{publisherId}/nodes/{nodeId}/claim-my-node': {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path?: never
|
||||
cookie?: never
|
||||
}
|
||||
get?: never
|
||||
put?: never
|
||||
/**
|
||||
* Claim nodeId into publisherId for the authenticated publisher
|
||||
* @description This endpoint allows a publisher to claim an unclaimed node that they own the repo, which is identified by the nodeId. The unclaimed node's repository must be owned by the authenticated user.
|
||||
*
|
||||
*/
|
||||
post: operations['claimMyNode']
|
||||
delete?: never
|
||||
options?: never
|
||||
head?: never
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/publishers/{publisherId}/nodes/v2': {
|
||||
parameters: {
|
||||
query?: never
|
||||
@@ -1082,23 +1061,6 @@ export interface paths {
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/bulk/nodes/versions': {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path?: never
|
||||
cookie?: never
|
||||
}
|
||||
get?: never
|
||||
put?: never
|
||||
/** Retrieve multiple node versions in a single request */
|
||||
post: operations['getBulkNodeVersions']
|
||||
delete?: never
|
||||
options?: never
|
||||
head?: never
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/versions': {
|
||||
parameters: {
|
||||
query?: never
|
||||
@@ -1133,26 +1095,6 @@ export interface paths {
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/admin/nodes/{nodeId}': {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path?: never
|
||||
cookie?: never
|
||||
}
|
||||
get?: never
|
||||
/**
|
||||
* Admin Update Node
|
||||
* @description Only admins can update a node with admin privileges.
|
||||
*/
|
||||
put: operations['adminUpdateNode']
|
||||
post?: never
|
||||
delete?: never
|
||||
options?: never
|
||||
head?: never
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/admin/nodes/{nodeId}/versions/{versionNumber}': {
|
||||
parameters: {
|
||||
query?: never
|
||||
@@ -3009,7 +2951,7 @@ export interface paths {
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/proxy/moonvalley/prompts/text-to-video': {
|
||||
'/proxy/moonvalley/text-to-video': {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
@@ -3026,7 +2968,7 @@ export interface paths {
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/proxy/moonvalley/prompts/text-to-image': {
|
||||
'/proxy/moonvalley/text-to-image': {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
@@ -3115,37 +3057,6 @@ export interface paths {
|
||||
export type webhooks = Record<string, never>
|
||||
export interface components {
|
||||
schemas: {
|
||||
ClaimMyNodeRequest: {
|
||||
/** @description GitHub token to verify if the user owns the repo of the node */
|
||||
GH_TOKEN: string
|
||||
}
|
||||
BulkNodeVersionsRequest: {
|
||||
/** @description List of node ID and version pairs to retrieve */
|
||||
node_versions: components['schemas']['NodeVersionIdentifier'][]
|
||||
}
|
||||
NodeVersionIdentifier: {
|
||||
/** @description The unique identifier of the node */
|
||||
node_id: string
|
||||
/** @description The version of the node */
|
||||
version: string
|
||||
}
|
||||
BulkNodeVersionsResponse: {
|
||||
/** @description List of retrieved node versions with their status */
|
||||
node_versions: components['schemas']['BulkNodeVersionResult'][]
|
||||
}
|
||||
BulkNodeVersionResult: {
|
||||
/** @description The node and version identifier */
|
||||
identifier: components['schemas']['NodeVersionIdentifier']
|
||||
/**
|
||||
* @description Status of the retrieval operation
|
||||
* @enum {string}
|
||||
*/
|
||||
status: 'success' | 'not_found' | 'error'
|
||||
/** @description The retrieved node version data (only present if status is success) */
|
||||
node_version?: components['schemas']['NodeVersion']
|
||||
/** @description Error message if retrieval failed (only present if status is error) */
|
||||
error_message?: string
|
||||
}
|
||||
PersonalAccessToken: {
|
||||
/**
|
||||
* Format: uuid
|
||||
@@ -8802,212 +8713,71 @@ export interface components {
|
||||
| 'computer-use-preview'
|
||||
| 'computer-use-preview-2025-03-11'
|
||||
| 'chatgpt-4o-latest'
|
||||
MoonvalleyTextToVideoInferenceParams: {
|
||||
/**
|
||||
* @description Height of the generated video in pixels
|
||||
* @default 1080
|
||||
*/
|
||||
MoonvalleyInferenceParams: {
|
||||
/** @default 1080 */
|
||||
height: number
|
||||
/**
|
||||
* @description Width of the generated video in pixels
|
||||
* @default 1920
|
||||
*/
|
||||
/** @default 1920 */
|
||||
width: number
|
||||
/**
|
||||
* @description Number of frames to generate
|
||||
* @default 64
|
||||
*/
|
||||
/** @default 64 */
|
||||
num_frames: number
|
||||
/**
|
||||
* @description Frames per second of the generated video
|
||||
* @default 24
|
||||
*/
|
||||
/** @default 24 */
|
||||
fps: number
|
||||
/**
|
||||
* Format: float
|
||||
* @description Guidance scale for generation control
|
||||
* @default 10
|
||||
* @default 12.5
|
||||
*/
|
||||
guidance_scale: number
|
||||
/** @description Random seed for generation (default: random) */
|
||||
seed?: number
|
||||
/**
|
||||
* @description Number of denoising steps
|
||||
* @default 80
|
||||
*/
|
||||
/** @default 80 */
|
||||
steps: number
|
||||
/**
|
||||
* @description Whether to use timestep transformation
|
||||
* @default true
|
||||
*/
|
||||
/** @default true */
|
||||
use_timestep_transform: boolean
|
||||
/**
|
||||
* Format: float
|
||||
* @description Shift value for generation control
|
||||
* @default 3
|
||||
*/
|
||||
shift_value: number
|
||||
/**
|
||||
* @description Whether to use guidance scheduling
|
||||
* @default true
|
||||
*/
|
||||
/** @default true */
|
||||
use_guidance_schedule: boolean
|
||||
/**
|
||||
* @description Whether to add quality guidance
|
||||
* @default true
|
||||
*/
|
||||
/** @default true */
|
||||
add_quality_guidance: boolean
|
||||
/**
|
||||
* Format: float
|
||||
* @description CLIP value for generation control
|
||||
* @default 3
|
||||
*/
|
||||
clip_value: number
|
||||
/**
|
||||
* @description Whether to use negative prompts
|
||||
* @default false
|
||||
*/
|
||||
/** @default false */
|
||||
use_negative_prompts: boolean
|
||||
/** @description Negative prompt text */
|
||||
negative_prompt?: string
|
||||
/**
|
||||
* @description Number of warmup steps (calculated based on num_frames)
|
||||
* @default 0
|
||||
*/
|
||||
warmup_steps: number
|
||||
/**
|
||||
* @description Number of cooldown steps (calculated based on num_frames)
|
||||
* @default 75
|
||||
*/
|
||||
cooldown_steps: number
|
||||
warmup_steps?: number
|
||||
cooldown_steps?: number
|
||||
/**
|
||||
* Format: float
|
||||
* @description Caching coefficient for optimization
|
||||
* @default 0.3
|
||||
*/
|
||||
caching_coefficient: number
|
||||
/**
|
||||
* @description Number of caching warmup steps
|
||||
* @default 3
|
||||
*/
|
||||
/** @default 3 */
|
||||
caching_warmup: number
|
||||
/**
|
||||
* @description Number of caching cooldown steps
|
||||
* @default 3
|
||||
*/
|
||||
/** @default 3 */
|
||||
caching_cooldown: number
|
||||
/**
|
||||
* @description Index of the conditioning frame
|
||||
* @default 0
|
||||
*/
|
||||
conditioning_frame_index: number
|
||||
}
|
||||
MoonvalleyVideoToVideoInferenceParams: {
|
||||
/**
|
||||
* Format: float
|
||||
* @description Guidance scale for generation control
|
||||
* @default 15
|
||||
*/
|
||||
guidance_scale: number
|
||||
/** @description Random seed for generation (default: random) */
|
||||
seed?: number
|
||||
/**
|
||||
* @description Number of denoising steps
|
||||
* @default 80
|
||||
*/
|
||||
steps: number
|
||||
/**
|
||||
* @description Whether to use timestep transformation
|
||||
* @default true
|
||||
*/
|
||||
use_timestep_transform: boolean
|
||||
/**
|
||||
* Format: float
|
||||
* @description Shift value for generation control
|
||||
* @default 3
|
||||
*/
|
||||
shift_value: number
|
||||
/**
|
||||
* @description Whether to use guidance scheduling
|
||||
* @default true
|
||||
*/
|
||||
use_guidance_schedule: boolean
|
||||
/**
|
||||
* @description Whether to add quality guidance
|
||||
* @default true
|
||||
*/
|
||||
add_quality_guidance: boolean
|
||||
/**
|
||||
* Format: float
|
||||
* @description CLIP value for generation control
|
||||
* @default 3
|
||||
*/
|
||||
clip_value: number
|
||||
/**
|
||||
* @description Whether to use negative prompts
|
||||
* @default false
|
||||
*/
|
||||
use_negative_prompts: boolean
|
||||
/** @description Negative prompt text */
|
||||
negative_prompt?: string
|
||||
/**
|
||||
* @description Number of warmup steps (calculated based on num_frames)
|
||||
* @default 24
|
||||
*/
|
||||
warmup_steps: number
|
||||
/**
|
||||
* @description Number of cooldown steps (calculated based on num_frames)
|
||||
* @default 36
|
||||
*/
|
||||
cooldown_steps: number
|
||||
/**
|
||||
* Format: float
|
||||
* @description Caching coefficient for optimization
|
||||
* @default 0.3
|
||||
*/
|
||||
caching_coefficient: number
|
||||
/**
|
||||
* @description Number of caching warmup steps
|
||||
* @default 3
|
||||
*/
|
||||
caching_warmup: number
|
||||
/**
|
||||
* @description Number of caching cooldown steps
|
||||
* @default 3
|
||||
*/
|
||||
caching_cooldown: number
|
||||
/**
|
||||
* @description Index of the conditioning frame
|
||||
* @default 0
|
||||
*/
|
||||
/** @default 0 */
|
||||
conditioning_frame_index: number
|
||||
}
|
||||
MoonvalleyTextToImageRequest: {
|
||||
prompt_text?: string
|
||||
image_url?: string
|
||||
inference_params?: components['schemas']['MoonvalleyTextToVideoInferenceParams']
|
||||
inference_params?: components['schemas']['MoonvalleyInferenceParams']
|
||||
webhook_url?: string
|
||||
}
|
||||
MoonvalleyTextToVideoRequest: {
|
||||
prompt_text?: string
|
||||
image_url?: string
|
||||
inference_params?: components['schemas']['MoonvalleyTextToVideoInferenceParams']
|
||||
inference_params?: components['schemas']['MoonvalleyInferenceParams']
|
||||
webhook_url?: string
|
||||
}
|
||||
MoonvalleyVideoToVideoRequest: {
|
||||
/** @description Describes the video to generate */
|
||||
prompt_text: string
|
||||
/** @description Url to control video */
|
||||
MoonvalleyVideoToVideoRequest: components['schemas']['MoonvalleyTextToVideoRequest'] & {
|
||||
video_url: string
|
||||
/**
|
||||
* @description Supported types for video control
|
||||
* @enum {string}
|
||||
*/
|
||||
control_type: 'motion_control' | 'pose_control'
|
||||
/** @description Parameters for video-to-video generation inference */
|
||||
inference_params?: components['schemas']['MoonvalleyVideoToVideoInferenceParams']
|
||||
/** @description Optional webhook URL for notifications */
|
||||
webhook_url?: string
|
||||
control_type: string
|
||||
}
|
||||
MoonvalleyPromptResponse: {
|
||||
id?: string
|
||||
@@ -10651,89 +10421,6 @@ export interface operations {
|
||||
}
|
||||
}
|
||||
}
|
||||
claimMyNode: {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path: {
|
||||
publisherId: string
|
||||
nodeId: string
|
||||
}
|
||||
cookie?: never
|
||||
}
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': components['schemas']['ClaimMyNodeRequest']
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
/** @description Node claimed successfully */
|
||||
204: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
/** @description Bad request, invalid input data */
|
||||
400: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
/** @description Forbidden - various authorization and permission issues
|
||||
* Includes:
|
||||
* - The authenticated user does not have permission to claim the node
|
||||
* - The node is already claimed by another publisher
|
||||
* - The GH_TOKEN is invalid
|
||||
* - The repository is not owned by the authenticated GitHub user
|
||||
* */
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
/** @description Too many requests - GitHub API rate limit exceeded */
|
||||
429: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
/** @description Internal server error */
|
||||
500: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
/** @description Service unavailable - GitHub API is currently unavailable */
|
||||
503: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
listNodesForPublisherV2: {
|
||||
parameters: {
|
||||
query?: {
|
||||
@@ -12022,48 +11709,6 @@ export interface operations {
|
||||
}
|
||||
}
|
||||
}
|
||||
getBulkNodeVersions: {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path?: never
|
||||
cookie?: never
|
||||
}
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': components['schemas']['BulkNodeVersionsRequest']
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
/** @description Successfully retrieved node versions */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['BulkNodeVersionsResponse']
|
||||
}
|
||||
}
|
||||
/** @description Bad request, invalid input */
|
||||
400: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
/** @description Internal server error */
|
||||
500: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
listAllNodeVersions: {
|
||||
parameters: {
|
||||
query?: {
|
||||
@@ -12189,75 +11834,6 @@ export interface operations {
|
||||
}
|
||||
}
|
||||
}
|
||||
adminUpdateNode: {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path: {
|
||||
nodeId: string
|
||||
}
|
||||
cookie?: never
|
||||
}
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Node']
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
/** @description Node updated successfully */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['Node']
|
||||
}
|
||||
}
|
||||
/** @description Bad request, invalid input data. */
|
||||
400: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
/** @description Forbidden */
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
/** @description Node not found */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
/** @description Internal server error */
|
||||
500: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ErrorResponse']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
adminUpdateNodeVersion: {
|
||||
parameters: {
|
||||
query?: never
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
describe('API Feature Flags', () => {
|
||||
let mockWebSocket: any
|
||||
const wsEventHandlers: { [key: string]: (event: any) => void } = {}
|
||||
|
||||
beforeEach(() => {
|
||||
// Use fake timers
|
||||
vi.useFakeTimers()
|
||||
|
||||
// Mock WebSocket
|
||||
mockWebSocket = {
|
||||
readyState: 1, // WebSocket.OPEN
|
||||
send: vi.fn(),
|
||||
close: vi.fn(),
|
||||
addEventListener: vi.fn(
|
||||
(event: string, handler: (event: any) => void) => {
|
||||
wsEventHandlers[event] = handler
|
||||
}
|
||||
),
|
||||
removeEventListener: vi.fn()
|
||||
}
|
||||
|
||||
// Mock WebSocket constructor
|
||||
global.WebSocket = vi.fn().mockImplementation(() => mockWebSocket) as any
|
||||
|
||||
// Reset API state
|
||||
api.serverFeatureFlags = {}
|
||||
|
||||
// Mock getClientFeatureFlags to return test feature flags
|
||||
vi.spyOn(api, 'getClientFeatureFlags').mockReturnValue({
|
||||
supports_preview_metadata: true,
|
||||
api_version: '1.0.0',
|
||||
capabilities: ['bulk_operations', 'async_nodes']
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('Feature flags negotiation', () => {
|
||||
it('should send client feature flags as first message on connection', async () => {
|
||||
// Initialize API connection
|
||||
const initPromise = api.init()
|
||||
|
||||
// Simulate connection open
|
||||
wsEventHandlers['open'](new Event('open'))
|
||||
|
||||
// Check that feature flags were sent as first message
|
||||
expect(mockWebSocket.send).toHaveBeenCalledTimes(1)
|
||||
const sentMessage = JSON.parse(mockWebSocket.send.mock.calls[0][0])
|
||||
expect(sentMessage).toEqual({
|
||||
type: 'feature_flags',
|
||||
data: {
|
||||
supports_preview_metadata: true,
|
||||
api_version: '1.0.0',
|
||||
capabilities: ['bulk_operations', 'async_nodes']
|
||||
}
|
||||
})
|
||||
|
||||
// Simulate server response with status message
|
||||
wsEventHandlers['message']({
|
||||
data: JSON.stringify({
|
||||
type: 'status',
|
||||
data: {
|
||||
status: { exec_info: { queue_remaining: 0 } },
|
||||
sid: 'test-sid'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Simulate server feature flags response
|
||||
wsEventHandlers['message']({
|
||||
data: JSON.stringify({
|
||||
type: 'feature_flags',
|
||||
data: {
|
||||
supports_preview_metadata: true,
|
||||
async_execution: true,
|
||||
supported_formats: ['webp', 'jpeg', 'png'],
|
||||
api_version: '1.0.0',
|
||||
max_upload_size: 104857600,
|
||||
capabilities: ['isolated_nodes', 'dynamic_models']
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
await initPromise
|
||||
|
||||
// Check that server features were stored
|
||||
expect(api.serverFeatureFlags).toEqual({
|
||||
supports_preview_metadata: true,
|
||||
async_execution: true,
|
||||
supported_formats: ['webp', 'jpeg', 'png'],
|
||||
api_version: '1.0.0',
|
||||
max_upload_size: 104857600,
|
||||
capabilities: ['isolated_nodes', 'dynamic_models']
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle server without feature flags support', async () => {
|
||||
// Initialize API connection
|
||||
const initPromise = api.init()
|
||||
|
||||
// Simulate connection open
|
||||
wsEventHandlers['open'](new Event('open'))
|
||||
|
||||
// Clear the send mock to reset
|
||||
mockWebSocket.send.mockClear()
|
||||
|
||||
// Simulate server response with status but no feature flags
|
||||
wsEventHandlers['message']({
|
||||
data: JSON.stringify({
|
||||
type: 'status',
|
||||
data: {
|
||||
status: { exec_info: { queue_remaining: 0 } },
|
||||
sid: 'test-sid'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Simulate some other message (not feature flags)
|
||||
wsEventHandlers['message']({
|
||||
data: JSON.stringify({
|
||||
type: 'execution_start',
|
||||
data: {}
|
||||
})
|
||||
})
|
||||
|
||||
await initPromise
|
||||
|
||||
// Server features should remain empty
|
||||
expect(api.serverFeatureFlags).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Feature checking methods', () => {
|
||||
beforeEach(() => {
|
||||
// Set up some test features
|
||||
api.serverFeatureFlags = {
|
||||
supports_preview_metadata: true,
|
||||
async_execution: false,
|
||||
capabilities: ['isolated_nodes', 'dynamic_models']
|
||||
}
|
||||
})
|
||||
|
||||
it('should check if server supports a boolean feature', () => {
|
||||
expect(api.serverSupportsFeature('supports_preview_metadata')).toBe(true)
|
||||
expect(api.serverSupportsFeature('async_execution')).toBe(false)
|
||||
expect(api.serverSupportsFeature('non_existent_feature')).toBe(false)
|
||||
})
|
||||
|
||||
it('should get server feature value', () => {
|
||||
expect(api.getServerFeature('supports_preview_metadata')).toBe(true)
|
||||
expect(api.getServerFeature('capabilities')).toEqual([
|
||||
'isolated_nodes',
|
||||
'dynamic_models'
|
||||
])
|
||||
expect(api.getServerFeature('non_existent_feature')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Client feature flags configuration', () => {
|
||||
it('should use mocked client feature flags', () => {
|
||||
// Verify mocked flags are returned
|
||||
const clientFlags = api.getClientFeatureFlags()
|
||||
expect(clientFlags).toEqual({
|
||||
supports_preview_metadata: true,
|
||||
api_version: '1.0.0',
|
||||
capabilities: ['bulk_operations', 'async_nodes']
|
||||
})
|
||||
})
|
||||
|
||||
it('should return a copy of client feature flags', () => {
|
||||
// Temporarily restore the real implementation for this test
|
||||
vi.mocked(api.getClientFeatureFlags).mockRestore()
|
||||
|
||||
// Verify that modifications to returned object don't affect original
|
||||
const clientFlags1 = api.getClientFeatureFlags()
|
||||
const clientFlags2 = api.getClientFeatureFlags()
|
||||
|
||||
// Should be different objects
|
||||
expect(clientFlags1).not.toBe(clientFlags2)
|
||||
|
||||
// But with same content
|
||||
expect(clientFlags1).toEqual(clientFlags2)
|
||||
|
||||
// Modifying one should not affect the other
|
||||
clientFlags1.test_flag = true
|
||||
expect(api.getClientFeatureFlags()).not.toHaveProperty('test_flag')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Integration with preview messages', () => {
|
||||
it('should affect preview message handling based on feature support', () => {
|
||||
// Test with metadata support
|
||||
api.serverFeatureFlags = { supports_preview_metadata: true }
|
||||
expect(api.serverSupportsFeature('supports_preview_metadata')).toBe(true)
|
||||
|
||||
// Test without metadata support
|
||||
api.serverFeatureFlags = {}
|
||||
expect(api.serverSupportsFeature('supports_preview_metadata')).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,192 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { COMFY_BASE_DOMAIN, COMFY_WEBSITE_URLS } from '@/config/comfyDomain'
|
||||
import { PYPI_MIRROR, PYTHON_MIRROR } from '@/constants/mirrors'
|
||||
import {
|
||||
COMFY_URLS,
|
||||
DEVELOPER_TOOLS,
|
||||
GITHUB_REPOS,
|
||||
MODEL_SOURCES,
|
||||
getDesktopGuideUrl
|
||||
} from '@/constants/urls'
|
||||
|
||||
describe('URL Constants', () => {
|
||||
describe('URL Format Validation', () => {
|
||||
it('should have valid HTTPS URLs throughout', () => {
|
||||
const httpsPattern =
|
||||
/^https:\/\/[a-z0-9]+([-.][a-z0-9]+)*\.[a-z]{2,}(:[0-9]{1,5})?(\/.*)?$/i
|
||||
|
||||
// Test COMFY_URLS
|
||||
expect(COMFY_URLS.website.base).toMatch(httpsPattern)
|
||||
expect(COMFY_URLS.docs.base).toMatch(httpsPattern)
|
||||
expect(COMFY_URLS.community.discord).toMatch(httpsPattern)
|
||||
|
||||
// Test GITHUB_REPOS
|
||||
Object.values(GITHUB_REPOS).forEach((url) => {
|
||||
expect(url).toMatch(httpsPattern)
|
||||
})
|
||||
|
||||
// Test MODEL_SOURCES
|
||||
Object.values(MODEL_SOURCES.repos).forEach((url) => {
|
||||
expect(url).toMatch(httpsPattern)
|
||||
})
|
||||
|
||||
// Test DEVELOPER_TOOLS
|
||||
Object.values(DEVELOPER_TOOLS).forEach((url) => {
|
||||
expect(url).toMatch(httpsPattern)
|
||||
})
|
||||
})
|
||||
|
||||
it('should have proper GitHub URL format', () => {
|
||||
const githubPattern = /^https:\/\/github\.com\/[\w-]+\/[\w-]+(\/[\w-]+)?$/
|
||||
|
||||
expect(GITHUB_REPOS.comfyui).toMatch(githubPattern)
|
||||
expect(GITHUB_REPOS.comfyuiIssues).toMatch(githubPattern)
|
||||
expect(GITHUB_REPOS.frontend).toMatch(githubPattern)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Mirror Configuration', () => {
|
||||
it('should have valid mirror URLs', () => {
|
||||
const urlPattern =
|
||||
/^https?:\/\/[a-z0-9]+([-.][a-z0-9]+)*\.[a-z]{2,}(:[0-9]{1,5})?(\/.*)?$/i
|
||||
|
||||
expect(PYTHON_MIRROR.mirror).toMatch(urlPattern)
|
||||
expect(PYTHON_MIRROR.fallbackMirror).toMatch(urlPattern)
|
||||
expect(PYPI_MIRROR.mirror).toMatch(urlPattern)
|
||||
expect(PYPI_MIRROR.fallbackMirror).toMatch(urlPattern)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Domain Configuration', () => {
|
||||
it('should have valid domain format', () => {
|
||||
const domainPattern = /^[a-z0-9]+([-.][a-z0-9]+)*\.[a-z]{2,}$/i
|
||||
expect(COMFY_BASE_DOMAIN).toMatch(domainPattern)
|
||||
})
|
||||
|
||||
it('should construct proper website URLs from base domain', () => {
|
||||
const expectedBase = `https://www.${COMFY_BASE_DOMAIN}`
|
||||
expect(COMFY_WEBSITE_URLS.base).toBe(expectedBase)
|
||||
expect(COMFY_WEBSITE_URLS.termsOfService).toBe(
|
||||
`${expectedBase}/terms-of-service`
|
||||
)
|
||||
expect(COMFY_WEBSITE_URLS.privacy).toBe(`${expectedBase}/privacy`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Localization', () => {
|
||||
it('should handle valid language codes in getLocalized', () => {
|
||||
const validLanguageCodes = ['en', 'zh', 'ja', 'ko', 'es', 'fr', 'de']
|
||||
|
||||
validLanguageCodes.forEach((lang) => {
|
||||
const result = COMFY_URLS.docs.getLocalized('test-path', lang)
|
||||
expect(result).toMatch(
|
||||
/^https:\/\/docs\.comfy\.org\/(([a-z]{2}-[A-Z]{2}\/)?test-path)$/
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should properly format localized paths', () => {
|
||||
expect(COMFY_URLS.docs.getLocalized('test-path', 'en')).toBe(
|
||||
'https://docs.comfy.org/test-path'
|
||||
)
|
||||
expect(COMFY_URLS.docs.getLocalized('test-path', 'zh')).toBe(
|
||||
'https://docs.comfy.org/zh-CN/test-path'
|
||||
)
|
||||
expect(COMFY_URLS.docs.getLocalized('', 'en')).toBe(
|
||||
'https://docs.comfy.org/'
|
||||
)
|
||||
expect(COMFY_URLS.docs.getLocalized('', 'zh')).toBe(
|
||||
'https://docs.comfy.org/zh-CN/'
|
||||
)
|
||||
})
|
||||
|
||||
it('should generate platform and locale-aware desktop guide URLs', () => {
|
||||
// Mock navigator for testing
|
||||
const originalNavigator = global.navigator
|
||||
|
||||
// Test Windows platform
|
||||
Object.defineProperty(global, 'navigator', {
|
||||
value: { platform: 'Win32' },
|
||||
writable: true
|
||||
})
|
||||
|
||||
const winUrl = getDesktopGuideUrl('en')
|
||||
expect(winUrl).toBe('https://docs.comfy.org/installation/desktop/windows')
|
||||
|
||||
// Test macOS platform
|
||||
Object.defineProperty(global, 'navigator', {
|
||||
value: { platform: 'MacIntel' },
|
||||
writable: true
|
||||
})
|
||||
|
||||
const macUrl = getDesktopGuideUrl('en')
|
||||
expect(macUrl).toBe('https://docs.comfy.org/installation/desktop/macos')
|
||||
|
||||
// Test Chinese locale with macOS
|
||||
const zhMacUrl = getDesktopGuideUrl('zh')
|
||||
expect(zhMacUrl).toBe(
|
||||
'https://docs.comfy.org/zh-CN/installation/desktop/macos'
|
||||
)
|
||||
|
||||
// Test Chinese locale with Windows
|
||||
Object.defineProperty(global, 'navigator', {
|
||||
value: { platform: 'Win32' },
|
||||
writable: true
|
||||
})
|
||||
|
||||
const zhWinUrl = getDesktopGuideUrl('zh')
|
||||
expect(zhWinUrl).toBe(
|
||||
'https://docs.comfy.org/zh-CN/installation/desktop/windows'
|
||||
)
|
||||
|
||||
// Test other locales default to English
|
||||
const frUrl = getDesktopGuideUrl('fr')
|
||||
expect(frUrl).toBe('https://docs.comfy.org/installation/desktop/windows')
|
||||
|
||||
// Test environment without navigator
|
||||
Object.defineProperty(global, 'navigator', {
|
||||
value: undefined,
|
||||
writable: true
|
||||
})
|
||||
|
||||
const noNavUrl = getDesktopGuideUrl('en')
|
||||
expect(noNavUrl).toBe(
|
||||
'https://docs.comfy.org/installation/desktop/windows'
|
||||
)
|
||||
|
||||
// Restore original navigator
|
||||
Object.defineProperty(global, 'navigator', {
|
||||
value: originalNavigator,
|
||||
writable: true
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Security', () => {
|
||||
it('should only use secure HTTPS for external URLs', () => {
|
||||
const allUrls = [
|
||||
...Object.values(GITHUB_REPOS),
|
||||
...Object.values(MODEL_SOURCES.repos),
|
||||
...Object.values(DEVELOPER_TOOLS),
|
||||
COMFY_URLS.website.base,
|
||||
COMFY_URLS.docs.base,
|
||||
COMFY_URLS.community.discord
|
||||
]
|
||||
|
||||
allUrls.forEach((url) => {
|
||||
expect(url.startsWith('https://')).toBe(true)
|
||||
expect(url.startsWith('http://')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
it('should have valid URL allowlist for model sources', () => {
|
||||
const urlPattern =
|
||||
/^https?:\/\/([a-z0-9]+([-.][a-z0-9]+)*\.[a-z]{2,}|localhost)(:[0-9]+)?\/?.*$/i
|
||||
|
||||
MODEL_SOURCES.allowedDomains.forEach((url) => {
|
||||
expect(url).toMatch(urlPattern)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
142
tests-ui/tests/store/nodeDefStore.test.ts
Normal file
142
tests-ui/tests/store/nodeDefStore.test.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { LiteGraph } from '@comfyorg/litegraph'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { ComfyNodeDef as ComfyNodeDefV1 } from '@/schemas/nodeDefSchema'
|
||||
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
|
||||
// Mock LiteGraph
|
||||
vi.mock('@comfyorg/litegraph', async () => {
|
||||
const actual = await vi.importActual('@comfyorg/litegraph')
|
||||
return {
|
||||
...actual,
|
||||
LiteGraph: {
|
||||
registered_node_types: {}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
describe('useNodeDefStore', () => {
|
||||
let store: ReturnType<typeof useNodeDefStore>
|
||||
|
||||
beforeEach(() => {
|
||||
// Set up Pinia
|
||||
setActivePinia(createPinia())
|
||||
// Create fresh store instance
|
||||
store = useNodeDefStore()
|
||||
// Clear registered node types
|
||||
LiteGraph.registered_node_types = {}
|
||||
})
|
||||
|
||||
describe('updateNodeDefDisplayName', () => {
|
||||
const createTestNodeDef = (
|
||||
name: string,
|
||||
displayName: string
|
||||
): ComfyNodeDefV1 => ({
|
||||
name,
|
||||
display_name: displayName,
|
||||
category: 'test',
|
||||
python_module: 'test_module',
|
||||
description: 'Test node',
|
||||
input: { required: {}, optional: {} },
|
||||
output: [],
|
||||
output_node: false
|
||||
})
|
||||
|
||||
it('should update display name in nodeDefsByName mapping', () => {
|
||||
const nodeDef = createTestNodeDef('TestNode', 'Original Display Name')
|
||||
store.addNodeDef(nodeDef)
|
||||
|
||||
store.updateNodeDefDisplayName('TestNode', 'New Display Name')
|
||||
|
||||
const updatedNodeDef = store.nodeDefsByName['TestNode']
|
||||
expect(updatedNodeDef.display_name).toBe('New Display Name')
|
||||
})
|
||||
|
||||
it('should update nodeDefsByDisplayName mapping', () => {
|
||||
const nodeDef = createTestNodeDef('TestNode', 'Original Display Name')
|
||||
store.addNodeDef(nodeDef)
|
||||
|
||||
store.updateNodeDefDisplayName('TestNode', 'New Display Name')
|
||||
|
||||
// Old display name should be removed
|
||||
expect(
|
||||
store.nodeDefsByDisplayName['Original Display Name']
|
||||
).toBeUndefined()
|
||||
// New display name should be added
|
||||
expect(store.nodeDefsByDisplayName['New Display Name']).toBeDefined()
|
||||
expect(store.nodeDefsByDisplayName['New Display Name'].name).toBe(
|
||||
'TestNode'
|
||||
)
|
||||
})
|
||||
|
||||
it('should update the object while preserving clone pattern', () => {
|
||||
const nodeDef = createTestNodeDef('TestNode', 'Original Display Name')
|
||||
store.addNodeDef(nodeDef)
|
||||
|
||||
const originalRef = store.nodeDefsByName['TestNode']
|
||||
store.updateNodeDefDisplayName('TestNode', 'New Display Name')
|
||||
const updatedRef = store.nodeDefsByName['TestNode']
|
||||
|
||||
// Clone creates a new reference but preserves properties
|
||||
expect(updatedRef).not.toBe(originalRef)
|
||||
expect(updatedRef.display_name).toBe('New Display Name')
|
||||
expect(updatedRef.name).toBe('TestNode')
|
||||
expect(updatedRef.category).toBe('test')
|
||||
})
|
||||
|
||||
it('should update LiteGraph registered node type title', () => {
|
||||
const nodeDef = createTestNodeDef('TestNode', 'Original Display Name')
|
||||
store.addNodeDef(nodeDef)
|
||||
|
||||
// Simulate a registered LiteGraph node type
|
||||
const mockNodeClass = { title: 'Original Display Name' }
|
||||
LiteGraph.registered_node_types['TestNode'] = mockNodeClass as any
|
||||
|
||||
store.updateNodeDefDisplayName('TestNode', 'New Display Name')
|
||||
|
||||
expect(mockNodeClass.title).toBe('New Display Name')
|
||||
})
|
||||
|
||||
it('should handle non-existent node type gracefully', () => {
|
||||
// Should not throw when updating non-existent node
|
||||
expect(() => {
|
||||
store.updateNodeDefDisplayName('NonExistentNode', 'New Display Name')
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('should handle missing LiteGraph registered type gracefully', () => {
|
||||
const nodeDef = createTestNodeDef('TestNode', 'Original Display Name')
|
||||
store.addNodeDef(nodeDef)
|
||||
|
||||
// No registered LiteGraph type
|
||||
expect(LiteGraph.registered_node_types['TestNode']).toBeUndefined()
|
||||
|
||||
// Should not throw
|
||||
expect(() => {
|
||||
store.updateNodeDefDisplayName('TestNode', 'New Display Name')
|
||||
}).not.toThrow()
|
||||
|
||||
// Store should still be updated
|
||||
expect(store.nodeDefsByName['TestNode'].display_name).toBe(
|
||||
'New Display Name'
|
||||
)
|
||||
})
|
||||
|
||||
it('should work with ComfyNodeDefImpl instances', () => {
|
||||
const nodeDef = createTestNodeDef('TestNode', 'Original Display Name')
|
||||
const nodeDefImpl = new ComfyNodeDefImpl(nodeDef)
|
||||
|
||||
// Manually add to store mappings
|
||||
store.nodeDefsByName['TestNode'] = nodeDefImpl
|
||||
store.nodeDefsByDisplayName['Original Display Name'] = nodeDefImpl
|
||||
|
||||
store.updateNodeDefDisplayName('TestNode', 'New Display Name')
|
||||
|
||||
expect(store.nodeDefsByName['TestNode'].display_name).toBe(
|
||||
'New Display Name'
|
||||
)
|
||||
expect(store.nodeDefsByDisplayName['New Display Name']).toBeDefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user