mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-25 23:07:46 +00:00
Compare commits
18 Commits
feat/cloud
...
test-fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b3bc77926 | ||
|
|
c94cedf8ee | ||
|
|
ba355b543d | ||
|
|
e9b641cfb7 | ||
|
|
56153596d9 | ||
|
|
b679bfe8f8 | ||
|
|
54979701d0 | ||
|
|
64e704c2f9 | ||
|
|
b1050e3195 | ||
|
|
ba100c4a04 | ||
|
|
cafd2de961 | ||
|
|
1f3fb90b1b | ||
|
|
27afd01297 | ||
|
|
535f857330 | ||
|
|
adb15aac40 | ||
|
|
8752f1b06d | ||
|
|
90c2c0fae0 | ||
|
|
34155bccb1 |
116
.github/actions/comment-release-links/action.yaml
vendored
Normal file
116
.github/actions/comment-release-links/action.yaml
vendored
Normal file
@@ -0,0 +1,116 @@
|
||||
name: Post Release Summary Comment
|
||||
description: Post or update a PR comment summarizing release links with diff, derived versions, and optional extras.
|
||||
author: ComfyUI Frontend Team
|
||||
|
||||
inputs:
|
||||
issue-number:
|
||||
description: Optional PR number override (defaults to the current pull request)
|
||||
default: ''
|
||||
version_file:
|
||||
description: Path to the JSON file containing the current version (relative to repo root)
|
||||
required: true
|
||||
|
||||
outputs:
|
||||
prev_version:
|
||||
description: Previous version derived from the parent commit
|
||||
value: ${{ steps.build.outputs.prev_version }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Build comment body
|
||||
id: build
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
VERSION_FILE="${{ inputs.version_file }}"
|
||||
REPO="${{ github.repository }}"
|
||||
|
||||
if [[ -z "$VERSION_FILE" ]]; then
|
||||
echo '::error::version_file input is required' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PREV_JSON=$(git show HEAD^1:"$VERSION_FILE" 2>/dev/null || true)
|
||||
if [[ -z "$PREV_JSON" ]]; then
|
||||
echo "::error::Unable to read $VERSION_FILE from parent commit" >&2
|
||||
exit 1
|
||||
fi
|
||||
PREV_VERSION=$(printf '%s' "$PREV_JSON" | node -pe "const data = JSON.parse(require('fs').readFileSync(0, 'utf8')); if (!data.version) { process.exit(1); } data.version")
|
||||
if [[ -z "$PREV_VERSION" ]]; then
|
||||
echo "::error::Unable to determine previous version from $VERSION_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NEW_VERSION=$(node -pe "const fs=require('fs');const data=JSON.parse(fs.readFileSync(process.argv[1],'utf8'));if(!data.version){process.exit(1);}data.version" "$VERSION_FILE")
|
||||
if [[ -z "$NEW_VERSION" ]]; then
|
||||
echo "::error::Unable to determine current version from $VERSION_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MARKER='release-summary'
|
||||
MESSAGE='Publish jobs finished successfully:'
|
||||
LINKS_VALUE=''
|
||||
|
||||
case "$VERSION_FILE" in
|
||||
package.json)
|
||||
LINKS_VALUE=$'PyPI|https://pypi.org/project/comfyui-frontend-package/{{version}}/\n''npm types|https://npm.im/@comfyorg/comfyui-frontend-types@{{version}}'
|
||||
;;
|
||||
apps/desktop-ui/package.json)
|
||||
MARKER='desktop-release-summary'
|
||||
LINKS_VALUE='npm desktop UI|https://npm.im/@comfyorg/desktop-ui@{{version}}'
|
||||
;;
|
||||
esac
|
||||
|
||||
DIFF_PREFIX='v'
|
||||
DIFF_LABEL='Diff'
|
||||
DIFF_URL="https://github.com/${REPO}/compare/${DIFF_PREFIX}${PREV_VERSION}...${DIFF_PREFIX}${NEW_VERSION}"
|
||||
COMMENT_FILE=$(mktemp)
|
||||
|
||||
{
|
||||
printf '<!--%s:%s%s-->\n' "$MARKER" "$DIFF_PREFIX" "$NEW_VERSION"
|
||||
printf '%s\n\n' "$MESSAGE"
|
||||
printf -- '- %s: [%s%s...%s%s](%s)\n' "$DIFF_LABEL" "$DIFF_PREFIX" "$PREV_VERSION" "$DIFF_PREFIX" "$NEW_VERSION" "$DIFF_URL"
|
||||
|
||||
while IFS= read -r RAW_LINE; do
|
||||
LINE=$(printf '%s' "$RAW_LINE" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
[[ -z "$LINE" ]] && continue
|
||||
if [[ "$LINE" != *"|"* ]]; then
|
||||
echo "::warning::Skipping malformed link entry: $LINE" >&2
|
||||
continue
|
||||
fi
|
||||
LABEL=${LINE%%|*}
|
||||
URL_TEMPLATE=${LINE#*|}
|
||||
URL=${URL_TEMPLATE//\{\{version\}\}/$NEW_VERSION}
|
||||
URL=${URL//\{\{prev_version\}\}/$PREV_VERSION}
|
||||
printf -- '- %s: %s\n' "$LABEL" "$URL"
|
||||
done <<< "$LINKS_VALUE"
|
||||
|
||||
printf '\n'
|
||||
} > "$COMMENT_FILE"
|
||||
|
||||
{
|
||||
echo "body<<'EOF'"
|
||||
cat "$COMMENT_FILE"
|
||||
echo 'EOF'
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
echo "prev_version=$PREV_VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "marker_search=<!--$MARKER:" >> "$GITHUB_OUTPUT"
|
||||
echo "new_version=$NEW_VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Find existing comment
|
||||
id: find
|
||||
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad
|
||||
with:
|
||||
issue-number: ${{ inputs.issue-number || github.event.pull_request.number }}
|
||||
comment-author: github-actions[bot]
|
||||
body-includes: ${{ steps.build.outputs.marker_search }}
|
||||
|
||||
- name: Post or update comment
|
||||
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9
|
||||
with:
|
||||
issue-number: ${{ inputs.issue-number || github.event.pull_request.number }}
|
||||
comment-id: ${{ steps.find.outputs.comment-id }}
|
||||
body: ${{ steps.build.outputs.body }}
|
||||
edit-mode: replace
|
||||
51
.github/workflows/ci-tests-e2e-cloud.yaml
vendored
51
.github/workflows/ci-tests-e2e-cloud.yaml
vendored
@@ -1,51 +0,0 @@
|
||||
name: "CI: Tests E2E Cloud"
|
||||
description: "Cloud E2E testing with Playwright against stagingcloud.comfy.org"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [cloud/*, main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
playwright-tests-cloud:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup frontend
|
||||
uses: ./.github/actions/setup-frontend
|
||||
with:
|
||||
include_build_step: false # Cloud tests don't need build
|
||||
|
||||
- name: Setup Playwright
|
||||
uses: ./.github/actions/setup-playwright
|
||||
|
||||
- name: Run Playwright cloud tests
|
||||
id: playwright
|
||||
env:
|
||||
CLOUD_TEST_EMAIL: ${{ secrets.CLOUD_TEST_EMAIL }}
|
||||
CLOUD_TEST_PASSWORD: ${{ secrets.CLOUD_TEST_PASSWORD }}
|
||||
run: |
|
||||
PLAYWRIGHT_JSON_OUTPUT_NAME=playwright-report/report.json \
|
||||
pnpm exec playwright test --config=playwright.cloud.config.ts \
|
||||
--reporter=list \
|
||||
--reporter=html \
|
||||
--reporter=json
|
||||
|
||||
- name: Upload Playwright report
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report-cloud
|
||||
path: ./playwright-report/
|
||||
retention-days: 30
|
||||
@@ -1,9 +1,10 @@
|
||||
---
|
||||
name: Publish Desktop UI on PR Merge
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [ closed ]
|
||||
branches: [ main, core/* ]
|
||||
types: ['closed']
|
||||
branches: [main, core/*]
|
||||
paths:
|
||||
- 'apps/desktop-ui/package.json'
|
||||
|
||||
@@ -57,3 +58,26 @@ jobs:
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
comment_desktop_publish:
|
||||
name: Comment Desktop Publish Summary
|
||||
needs:
|
||||
- resolve
|
||||
- publish
|
||||
if: success()
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout merge commit
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.merge_commit_sha }}
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Post desktop release summary comment
|
||||
uses: ./.github/actions/comment-release-links
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
version_file: apps/desktop-ui/package.json
|
||||
|
||||
51
.github/workflows/release-draft-create.yaml
vendored
51
.github/workflows/release-draft-create.yaml
vendored
@@ -1,9 +1,10 @@
|
||||
---
|
||||
name: Release Draft Create
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [ closed ]
|
||||
branches: [ main, core/* ]
|
||||
types: ['closed']
|
||||
branches: [main, core/*]
|
||||
paths:
|
||||
- 'package.json'
|
||||
|
||||
@@ -30,7 +31,9 @@ jobs:
|
||||
|
||||
- name: Get current version
|
||||
id: current_version
|
||||
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
|
||||
run: |
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
- name: Check if prerelease
|
||||
id: check_prerelease
|
||||
run: |
|
||||
@@ -71,7 +74,8 @@ jobs:
|
||||
name: dist-files
|
||||
- name: Create release
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
|
||||
uses: >-
|
||||
softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
@@ -79,9 +83,14 @@ jobs:
|
||||
dist.zip
|
||||
tag_name: v${{ needs.build.outputs.version }}
|
||||
target_commitish: ${{ github.event.pull_request.base.ref }}
|
||||
make_latest: ${{ github.event.pull_request.base.ref == 'main' && needs.build.outputs.is_prerelease == 'false' }}
|
||||
draft: ${{ github.event.pull_request.base.ref != 'main' || needs.build.outputs.is_prerelease == 'true' }}
|
||||
prerelease: ${{ needs.build.outputs.is_prerelease == 'true' }}
|
||||
make_latest: >-
|
||||
${{ github.event.pull_request.base.ref == 'main' &&
|
||||
needs.build.outputs.is_prerelease == 'false' }}
|
||||
draft: >-
|
||||
${{ github.event.pull_request.base.ref != 'main' ||
|
||||
needs.build.outputs.is_prerelease == 'true' }}
|
||||
prerelease: >-
|
||||
${{ needs.build.outputs.is_prerelease == 'true' }}
|
||||
generate_release_notes: true
|
||||
|
||||
publish_pypi:
|
||||
@@ -110,7 +119,8 @@ jobs:
|
||||
env:
|
||||
COMFYUI_FRONTEND_VERSION: ${{ needs.build.outputs.version }}
|
||||
- name: Publish pypi package
|
||||
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc
|
||||
uses: >-
|
||||
pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc
|
||||
with:
|
||||
password: ${{ secrets.PYPI_TOKEN }}
|
||||
packages-dir: comfyui_frontend_package/dist
|
||||
@@ -122,3 +132,28 @@ jobs:
|
||||
version: ${{ needs.build.outputs.version }}
|
||||
ref: ${{ github.event.pull_request.merge_commit_sha }}
|
||||
secrets: inherit
|
||||
|
||||
comment_release_summary:
|
||||
name: Comment Release Summary
|
||||
needs:
|
||||
- draft_release
|
||||
- publish_pypi
|
||||
- publish_types
|
||||
if: success()
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout merge commit
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.merge_commit_sha }}
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Post release summary comment
|
||||
uses: ./.github/actions/comment-release-links
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
version_file: package.json
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -58,7 +58,6 @@ coverage/
|
||||
/playwright/.cache/
|
||||
browser_tests/**/*-win32.png
|
||||
browser_tests/local/
|
||||
browser_tests/.auth/
|
||||
|
||||
.env
|
||||
|
||||
|
||||
@@ -74,8 +74,15 @@ const config: StorybookConfig = {
|
||||
'@': process.cwd() + '/src'
|
||||
}
|
||||
},
|
||||
esbuild: {
|
||||
// Prevent minification of identifiers to preserve _sfc_main
|
||||
minifyIdentifiers: false,
|
||||
keepNames: true
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
// Disable tree-shaking for Storybook to prevent Vue SFC exports from being removed
|
||||
treeshake: false,
|
||||
onwarn: (warning, warn) => {
|
||||
// Suppress specific warnings
|
||||
if (
|
||||
|
||||
@@ -75,8 +75,15 @@ const config: StorybookConfig = {
|
||||
'@frontend-locales': process.cwd() + '/../../src/locales'
|
||||
}
|
||||
},
|
||||
esbuild: {
|
||||
// Prevent minification of identifiers to preserve _sfc_main
|
||||
minifyIdentifiers: false,
|
||||
keepNames: true
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
// Disable tree-shaking for Storybook to prevent Vue SFC exports from being removed
|
||||
treeshake: false,
|
||||
onwarn: (warning, warn) => {
|
||||
// Suppress specific warnings
|
||||
if (
|
||||
|
||||
206
apps/desktop-ui/src/components/common/LanguageSelector.vue
Normal file
206
apps/desktop-ui/src/components/common/LanguageSelector.vue
Normal file
@@ -0,0 +1,206 @@
|
||||
<template>
|
||||
<Select
|
||||
:id="dropdownId"
|
||||
v-model="selectedLocale"
|
||||
:options="localeOptions"
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
:disabled="isSwitching"
|
||||
:pt="dropdownPt"
|
||||
:size="props.size"
|
||||
class="language-selector"
|
||||
@change="onLocaleChange"
|
||||
>
|
||||
<template #value="{ value }">
|
||||
<span :class="valueClass">
|
||||
<i class="pi pi-language" :class="iconClass" />
|
||||
<span>{{ displayLabel(value as SupportedLocale) }}</span>
|
||||
</span>
|
||||
</template>
|
||||
<template #option="{ option }">
|
||||
<span :class="optionClass">
|
||||
<i class="pi pi-language" :class="iconClass" />
|
||||
<span class="leading-none">{{ option.label }}</span>
|
||||
</span>
|
||||
</template>
|
||||
</Select>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Select from 'primevue/select'
|
||||
import type { SelectChangeEvent } from 'primevue/select'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { i18n, loadLocale, st } from '@/i18n'
|
||||
|
||||
type VariantKey = 'dark' | 'light'
|
||||
type SizeKey = 'small' | 'large'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
variant?: VariantKey
|
||||
size?: SizeKey
|
||||
}>(),
|
||||
{
|
||||
variant: 'dark',
|
||||
size: 'small'
|
||||
}
|
||||
)
|
||||
|
||||
const dropdownId = `language-select-${Math.random().toString(36).slice(2)}`
|
||||
|
||||
const LOCALES = [
|
||||
['en', 'English'],
|
||||
['zh', '中文'],
|
||||
['zh-TW', '繁體中文'],
|
||||
['ru', 'Русский'],
|
||||
['ja', '日本語'],
|
||||
['ko', '한국어'],
|
||||
['fr', 'Français'],
|
||||
['es', 'Español'],
|
||||
['ar', 'عربي'],
|
||||
['tr', 'Türkçe']
|
||||
] as const satisfies ReadonlyArray<[string, string]>
|
||||
|
||||
type SupportedLocale = (typeof LOCALES)[number][0]
|
||||
|
||||
const SIZE_PRESETS = {
|
||||
large: {
|
||||
wrapper: 'px-3 py-1 min-w-[7rem]',
|
||||
gap: 'gap-2',
|
||||
valueText: 'text-xs',
|
||||
optionText: 'text-sm',
|
||||
icon: 'text-sm'
|
||||
},
|
||||
small: {
|
||||
wrapper: 'px-2 py-0.5 min-w-[5rem]',
|
||||
gap: 'gap-1',
|
||||
valueText: 'text-[0.65rem]',
|
||||
optionText: 'text-xs',
|
||||
icon: 'text-xs'
|
||||
}
|
||||
} as const satisfies Record<SizeKey, Record<string, string>>
|
||||
|
||||
const VARIANT_PRESETS = {
|
||||
light: {
|
||||
root: 'bg-white/80 border border-neutral-200 text-neutral-700 rounded-full shadow-sm backdrop-blur hover:border-neutral-400 transition-colors focus-visible:ring-offset-2 focus-visible:ring-offset-white',
|
||||
trigger: 'text-neutral-500 hover:text-neutral-700',
|
||||
item: 'text-neutral-700 bg-transparent hover:bg-neutral-100 focus-visible:outline-none',
|
||||
valueText: 'text-neutral-600',
|
||||
optionText: 'text-neutral-600',
|
||||
icon: 'text-neutral-500'
|
||||
},
|
||||
dark: {
|
||||
root: 'bg-neutral-900/70 border border-neutral-700 text-neutral-200 rounded-full shadow-sm backdrop-blur hover:border-neutral-500 transition-colors focus-visible:ring-offset-2 focus-visible:ring-offset-neutral-900',
|
||||
trigger: 'text-neutral-400 hover:text-neutral-200',
|
||||
item: 'text-neutral-200 bg-transparent hover:bg-neutral-800/80 focus-visible:outline-none',
|
||||
valueText: 'text-neutral-100',
|
||||
optionText: 'text-neutral-100',
|
||||
icon: 'text-neutral-300'
|
||||
}
|
||||
} as const satisfies Record<VariantKey, Record<string, string>>
|
||||
|
||||
const selectedLocale = ref<string>(i18n.global.locale.value)
|
||||
const isSwitching = ref(false)
|
||||
|
||||
const sizePreset = computed(() => SIZE_PRESETS[props.size as SizeKey])
|
||||
const variantPreset = computed(
|
||||
() => VARIANT_PRESETS[props.variant as VariantKey]
|
||||
)
|
||||
|
||||
const dropdownPt = computed(() => ({
|
||||
root: {
|
||||
class: `${variantPreset.value.root} ${sizePreset.value.wrapper}`
|
||||
},
|
||||
trigger: {
|
||||
class: variantPreset.value.trigger
|
||||
},
|
||||
item: {
|
||||
class: `${variantPreset.value.item} ${sizePreset.value.optionText}`
|
||||
}
|
||||
}))
|
||||
|
||||
const valueClass = computed(() =>
|
||||
[
|
||||
'flex items-center font-medium uppercase tracking-wide leading-tight',
|
||||
sizePreset.value.gap,
|
||||
sizePreset.value.valueText,
|
||||
variantPreset.value.valueText
|
||||
].join(' ')
|
||||
)
|
||||
|
||||
const optionClass = computed(() =>
|
||||
[
|
||||
'flex items-center leading-tight',
|
||||
sizePreset.value.gap,
|
||||
variantPreset.value.optionText,
|
||||
sizePreset.value.optionText
|
||||
].join(' ')
|
||||
)
|
||||
|
||||
const iconClass = computed(() =>
|
||||
[sizePreset.value.icon, variantPreset.value.icon].join(' ')
|
||||
)
|
||||
|
||||
const localeOptions = computed(() =>
|
||||
LOCALES.map(([value, fallback]) => ({
|
||||
value,
|
||||
label: st(`settings.Comfy_Locale.options.${value}`, fallback)
|
||||
}))
|
||||
)
|
||||
|
||||
const labelLookup = computed(() =>
|
||||
localeOptions.value.reduce<Record<string, string>>((acc, option) => {
|
||||
acc[option.value] = option.label
|
||||
return acc
|
||||
}, {})
|
||||
)
|
||||
|
||||
function displayLabel(locale?: SupportedLocale) {
|
||||
if (!locale) {
|
||||
return st('settings.Comfy_Locale.name', 'Language')
|
||||
}
|
||||
|
||||
return labelLookup.value[locale] ?? locale
|
||||
}
|
||||
|
||||
watch(
|
||||
() => i18n.global.locale.value,
|
||||
(newLocale) => {
|
||||
if (newLocale !== selectedLocale.value) {
|
||||
selectedLocale.value = newLocale
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
async function onLocaleChange(event: SelectChangeEvent) {
|
||||
const nextLocale = event.value as SupportedLocale | undefined
|
||||
|
||||
if (!nextLocale || nextLocale === i18n.global.locale.value) {
|
||||
return
|
||||
}
|
||||
|
||||
isSwitching.value = true
|
||||
try {
|
||||
await loadLocale(nextLocale)
|
||||
i18n.global.locale.value = nextLocale
|
||||
} catch (error) {
|
||||
console.error(`Failed to change locale to "${nextLocale}"`, error)
|
||||
selectedLocale.value = i18n.global.locale.value
|
||||
} finally {
|
||||
isSwitching.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../../assets/css/style.css';
|
||||
|
||||
:deep(.p-dropdown-panel .p-dropdown-item) {
|
||||
@apply transition-colors;
|
||||
}
|
||||
|
||||
:deep(.p-dropdown) {
|
||||
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-yellow/60 focus-visible:ring-offset-2;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<BaseViewTemplate dark>
|
||||
<BaseViewTemplate dark hide-language-selector>
|
||||
<div class="h-full p-8 2xl:p-16 flex flex-col items-center justify-center">
|
||||
<div
|
||||
class="bg-neutral-800 rounded-lg shadow-lg p-6 w-full max-w-[600px] flex flex-col gap-6"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<BaseViewTemplate dark>
|
||||
<div class="flex items-center justify-center min-h-screen">
|
||||
<div class="grid grid-rows-2 gap-8">
|
||||
<div class="grid gap-8">
|
||||
<!-- Top container: Logo -->
|
||||
<div class="flex items-end justify-center">
|
||||
<img
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
<template>
|
||||
<div
|
||||
class="font-sans w-screen h-screen flex flex-col"
|
||||
class="font-sans w-screen h-screen flex flex-col relative"
|
||||
:class="[
|
||||
dark
|
||||
? 'text-neutral-300 bg-neutral-900 dark-theme'
|
||||
: 'text-neutral-900 bg-neutral-300'
|
||||
]"
|
||||
>
|
||||
<div v-if="showLanguageSelector" class="absolute top-6 right-6 z-10">
|
||||
<LanguageSelector :variant="variant" />
|
||||
</div>
|
||||
<!-- Virtual top menu for native window (drag handle) -->
|
||||
<div
|
||||
v-show="isNativeWindow()"
|
||||
@@ -20,14 +23,20 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onMounted, ref } from 'vue'
|
||||
import { computed, nextTick, onMounted, ref } from 'vue'
|
||||
|
||||
import LanguageSelector from '@/components/common/LanguageSelector.vue'
|
||||
|
||||
import { electronAPI, isElectron, isNativeWindow } from '../../utils/envUtil'
|
||||
|
||||
const { dark = false } = defineProps<{
|
||||
const { dark = false, hideLanguageSelector = false } = defineProps<{
|
||||
dark?: boolean
|
||||
hideLanguageSelector?: boolean
|
||||
}>()
|
||||
|
||||
const variant = computed(() => (dark ? 'dark' : 'light'))
|
||||
const showLanguageSelector = computed(() => !hideLanguageSelector)
|
||||
|
||||
const darkTheme = {
|
||||
color: 'rgba(0, 0, 0, 0)',
|
||||
symbolColor: '#d4d4d4'
|
||||
|
||||
@@ -140,7 +140,7 @@ When writing new tests, follow these patterns:
|
||||
|
||||
```typescript
|
||||
// Import the test fixture
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Feature Name', () => {
|
||||
// Set up test environment if needed
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import type { APIRequestContext, Page } from '@playwright/test'
|
||||
|
||||
import { ComfyPage } from './ComfyPage'
|
||||
import type { FolderStructure } from './ComfyPage'
|
||||
|
||||
/**
|
||||
* Cloud-specific implementation of ComfyPage.
|
||||
* Uses Firebase auth persistence and cloud API for settings.
|
||||
*/
|
||||
export class CloudComfyPage extends ComfyPage {
|
||||
constructor(
|
||||
page: Page,
|
||||
request: APIRequestContext,
|
||||
parallelIndex: number = 0
|
||||
) {
|
||||
super(page, request, parallelIndex)
|
||||
}
|
||||
|
||||
async setupUser(username: string): Promise<string | null> {
|
||||
// No-op for cloud - user already authenticated via Firebase in globalSetup
|
||||
// Firebase auth is persisted via storageState in the fixture
|
||||
return null
|
||||
}
|
||||
|
||||
async setupSettings(settings: Record<string, any>): Promise<void> {
|
||||
// Cloud uses batch settings API (not devtools)
|
||||
// Firebase auth token is automatically included from restored localStorage
|
||||
const resp = await this.request.post(`${this.url}/api/settings`, {
|
||||
data: settings
|
||||
})
|
||||
|
||||
if (!resp.ok()) {
|
||||
throw new Error(`Failed to setup cloud settings: ${await resp.text()}`)
|
||||
}
|
||||
}
|
||||
|
||||
async setupWorkflowsDirectory(structure: FolderStructure): Promise<void> {
|
||||
// Cloud workflow API not yet implemented
|
||||
// For initial smoke tests, we can skip this functionality
|
||||
console.warn(
|
||||
'setupWorkflowsDirectory: not yet implemented for cloud mode - skipping'
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { APIRequestContext, Locator, Page } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
import { test as base, expect } from '@playwright/test'
|
||||
import dotenv from 'dotenv'
|
||||
import * as fs from 'fs'
|
||||
|
||||
@@ -7,8 +7,10 @@ import type { LGraphNode } from '../../src/lib/litegraph/src/litegraph'
|
||||
import type { NodeId } from '../../src/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { KeyCombo } from '../../src/schemas/keyBindingSchema'
|
||||
import type { useWorkspaceStore } from '../../src/stores/workspaceStore'
|
||||
import { NodeBadgeMode } from '../../src/types/nodeSource'
|
||||
import { ComfyActionbar } from '../helpers/actionbar'
|
||||
import { ComfyTemplates } from '../helpers/templates'
|
||||
import { ComfyMouse } from './ComfyMouse'
|
||||
import { VueNodeHelpers } from './VueNodeHelpers'
|
||||
import { ComfyNodeSearchBox } from './components/ComfyNodeSearchBox'
|
||||
import { SettingDialog } from './components/SettingDialog'
|
||||
@@ -92,7 +94,7 @@ class ComfyMenu {
|
||||
}
|
||||
}
|
||||
|
||||
export type FolderStructure = {
|
||||
type FolderStructure = {
|
||||
[key: string]: FolderStructure | string
|
||||
}
|
||||
|
||||
@@ -120,11 +122,7 @@ class ConfirmDialog {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract base class for ComfyUI page objects.
|
||||
* Subclasses must implement backend-specific methods for different environments (localhost, cloud, etc.)
|
||||
*/
|
||||
export abstract class ComfyPage {
|
||||
export class ComfyPage {
|
||||
private _history: TaskHistory | null = null
|
||||
|
||||
public readonly url: string
|
||||
@@ -158,13 +156,12 @@ export abstract class ComfyPage {
|
||||
|
||||
/** Test user ID for the current context */
|
||||
get id() {
|
||||
return this.userIds[this.parallelIndex]
|
||||
return this.userIds[comfyPageFixture.info().parallelIndex]
|
||||
}
|
||||
|
||||
constructor(
|
||||
public readonly page: Page,
|
||||
public readonly request: APIRequestContext,
|
||||
public readonly parallelIndex: number = 0
|
||||
public readonly request: APIRequestContext
|
||||
) {
|
||||
this.url = process.env.PLAYWRIGHT_TEST_URL || 'http://localhost:8188'
|
||||
this.canvas = page.locator('#graph-canvas')
|
||||
@@ -218,24 +215,65 @@ export abstract class ComfyPage {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup workflows directory structure. Implementation varies by environment.
|
||||
* @param structure - Folder structure to create
|
||||
*/
|
||||
abstract setupWorkflowsDirectory(structure: FolderStructure): Promise<void>
|
||||
async setupWorkflowsDirectory(structure: FolderStructure) {
|
||||
const resp = await this.request.post(
|
||||
`${this.url}/api/devtools/setup_folder_structure`,
|
||||
{
|
||||
data: {
|
||||
tree_structure: this.convertLeafToContent(structure),
|
||||
base_path: `user/${this.id}/workflows`
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Setup user for testing. Implementation varies by environment.
|
||||
* @param username - Username to setup
|
||||
* @returns User ID or null if not applicable
|
||||
*/
|
||||
abstract setupUser(username: string): Promise<string | null>
|
||||
if (resp.status() !== 200) {
|
||||
throw new Error(
|
||||
`Failed to setup workflows directory: ${await resp.text()}`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup settings for testing. Implementation varies by environment.
|
||||
* @param settings - Settings object to apply
|
||||
*/
|
||||
abstract setupSettings(settings: Record<string, any>): Promise<void>
|
||||
await this.page.evaluate(async () => {
|
||||
await window['app'].extensionManager.workflow.syncWorkflows()
|
||||
})
|
||||
}
|
||||
|
||||
async setupUser(username: string) {
|
||||
const res = await this.request.get(`${this.url}/api/users`)
|
||||
if (res.status() !== 200)
|
||||
throw new Error(`Failed to retrieve users: ${await res.text()}`)
|
||||
|
||||
const apiRes = await res.json()
|
||||
const user = Object.entries(apiRes?.users ?? {}).find(
|
||||
([, name]) => name === username
|
||||
)
|
||||
const id = user?.[0]
|
||||
|
||||
return id ? id : await this.createUser(username)
|
||||
}
|
||||
|
||||
async createUser(username: string) {
|
||||
const resp = await this.request.post(`${this.url}/api/users`, {
|
||||
data: { username }
|
||||
})
|
||||
|
||||
if (resp.status() !== 200)
|
||||
throw new Error(`Failed to create user: ${await resp.text()}`)
|
||||
|
||||
return await resp.json()
|
||||
}
|
||||
|
||||
async setupSettings(settings: Record<string, any>) {
|
||||
const resp = await this.request.post(
|
||||
`${this.url}/api/devtools/set_settings`,
|
||||
{
|
||||
data: settings
|
||||
}
|
||||
)
|
||||
|
||||
if (resp.status() !== 200) {
|
||||
throw new Error(`Failed to setup settings: ${await resp.text()}`)
|
||||
}
|
||||
}
|
||||
|
||||
setupHistory(): TaskHistory {
|
||||
this._history ??= new TaskHistory(this)
|
||||
@@ -1590,8 +1628,50 @@ export abstract class ComfyPage {
|
||||
}
|
||||
}
|
||||
|
||||
// Re-export shared constants and fixture
|
||||
export { testComfySnapToGridGridSize } from './constants'
|
||||
export const testComfySnapToGridGridSize = 50
|
||||
|
||||
export const comfyPageFixture = base.extend<{
|
||||
comfyPage: ComfyPage
|
||||
comfyMouse: ComfyMouse
|
||||
}>({
|
||||
comfyPage: async ({ page, request }, use, testInfo) => {
|
||||
const comfyPage = new ComfyPage(page, request)
|
||||
|
||||
const { parallelIndex } = testInfo
|
||||
const username = `playwright-test-${parallelIndex}`
|
||||
const userId = await comfyPage.setupUser(username)
|
||||
comfyPage.userIds[parallelIndex] = userId
|
||||
|
||||
try {
|
||||
await comfyPage.setupSettings({
|
||||
'Comfy.UseNewMenu': 'Top',
|
||||
// Hide canvas menu/info/selection toolbox by default.
|
||||
'Comfy.Graph.CanvasInfo': false,
|
||||
'Comfy.Graph.CanvasMenu': false,
|
||||
'Comfy.Canvas.SelectionToolbox': false,
|
||||
// Hide all badges by default.
|
||||
'Comfy.NodeBadge.NodeIdBadgeMode': NodeBadgeMode.None,
|
||||
'Comfy.NodeBadge.NodeSourceBadgeMode': NodeBadgeMode.None,
|
||||
// Disable tooltips by default to avoid flakiness.
|
||||
'Comfy.EnableTooltips': false,
|
||||
'Comfy.userId': userId,
|
||||
// Set tutorial completed to true to avoid loading the tutorial workflow.
|
||||
'Comfy.TutorialCompleted': true,
|
||||
'Comfy.SnapToGrid.GridSize': testComfySnapToGridGridSize,
|
||||
'Comfy.VueNodes.AutoScaleLayout': false
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
await comfyPage.setup()
|
||||
await use(comfyPage)
|
||||
},
|
||||
comfyMouse: async ({ comfyPage }, use) => {
|
||||
const comfyMouse = new ComfyMouse(comfyPage)
|
||||
await use(comfyMouse)
|
||||
}
|
||||
})
|
||||
|
||||
const makeMatcher = function <T>(
|
||||
getValue: (node: NodeReference) => Promise<T> | T,
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import { test as base } from '@playwright/test'
|
||||
|
||||
import { CloudComfyPage } from './CloudComfyPage'
|
||||
import { ComfyMouse } from './ComfyMouse'
|
||||
import type { ComfyPage } from './ComfyPage'
|
||||
|
||||
/**
|
||||
* Cloud-specific fixture for ComfyPage.
|
||||
* Uses Firebase auth persisted from globalSetupCloud.ts.
|
||||
*/
|
||||
export const comfyPageCloudFixture = base.extend<{
|
||||
comfyPage: ComfyPage
|
||||
comfyMouse: ComfyMouse
|
||||
}>({
|
||||
// Use the storageState saved by globalSetupCloud
|
||||
storageState: 'browser_tests/.auth/cloudUser.json',
|
||||
|
||||
comfyPage: async ({ page, request }, use) => {
|
||||
const comfyPage = new CloudComfyPage(page, request)
|
||||
|
||||
// Note: No setupUser needed - Firebase auth persisted via storageState
|
||||
// Setup cloud-specific settings (optional - can customize per test)
|
||||
try {
|
||||
await comfyPage.setupSettings({
|
||||
'Comfy.UseNewMenu': 'Top',
|
||||
// Hide canvas menu/info/selection toolbox by default.
|
||||
'Comfy.Graph.CanvasInfo': false,
|
||||
'Comfy.Graph.CanvasMenu': false,
|
||||
'Comfy.Canvas.SelectionToolbox': false,
|
||||
// Disable tooltips by default to avoid flakiness.
|
||||
'Comfy.EnableTooltips': false,
|
||||
// Set tutorial completed to true to avoid loading the tutorial workflow.
|
||||
'Comfy.TutorialCompleted': true
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('Failed to setup cloud settings:', e)
|
||||
}
|
||||
|
||||
// Don't mock releases for cloud - cloud handles its own releases
|
||||
await comfyPage.setup({ mockReleases: false })
|
||||
await use(comfyPage)
|
||||
},
|
||||
|
||||
comfyMouse: async ({ comfyPage }, use) => {
|
||||
const comfyMouse = new ComfyMouse(comfyPage)
|
||||
await use(comfyMouse)
|
||||
}
|
||||
})
|
||||
@@ -1,78 +0,0 @@
|
||||
import type { APIRequestContext, Page } from '@playwright/test'
|
||||
|
||||
import { ComfyPage } from './ComfyPage'
|
||||
import type { FolderStructure } from './ComfyPage'
|
||||
|
||||
/**
|
||||
* Localhost-specific implementation of ComfyPage.
|
||||
* Uses devtools API and multi-user mode for test isolation.
|
||||
*/
|
||||
export class LocalhostComfyPage extends ComfyPage {
|
||||
constructor(
|
||||
page: Page,
|
||||
request: APIRequestContext,
|
||||
parallelIndex: number = 0
|
||||
) {
|
||||
super(page, request, parallelIndex)
|
||||
}
|
||||
|
||||
async setupWorkflowsDirectory(structure: FolderStructure): Promise<void> {
|
||||
const resp = await this.request.post(
|
||||
`${this.url}/api/devtools/setup_folder_structure`,
|
||||
{
|
||||
data: {
|
||||
tree_structure: this.convertLeafToContent(structure),
|
||||
base_path: `user/${this.id}/workflows`
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (resp.status() !== 200) {
|
||||
throw new Error(
|
||||
`Failed to setup workflows directory: ${await resp.text()}`
|
||||
)
|
||||
}
|
||||
|
||||
await this.page.evaluate(async () => {
|
||||
await window['app'].extensionManager.workflow.syncWorkflows()
|
||||
})
|
||||
}
|
||||
|
||||
async setupUser(username: string): Promise<string | null> {
|
||||
const res = await this.request.get(`${this.url}/api/users`)
|
||||
if (res.status() !== 200)
|
||||
throw new Error(`Failed to retrieve users: ${await res.text()}`)
|
||||
|
||||
const apiRes = await res.json()
|
||||
const user = Object.entries(apiRes?.users ?? {}).find(
|
||||
([, name]) => name === username
|
||||
)
|
||||
const id = user?.[0]
|
||||
|
||||
return id ? id : await this.createUser(username)
|
||||
}
|
||||
|
||||
private async createUser(username: string): Promise<string> {
|
||||
const resp = await this.request.post(`${this.url}/api/users`, {
|
||||
data: { username }
|
||||
})
|
||||
|
||||
if (resp.status() !== 200)
|
||||
throw new Error(`Failed to create user: ${await resp.text()}`)
|
||||
|
||||
return await resp.json()
|
||||
}
|
||||
|
||||
async setupSettings(settings: Record<string, any>): Promise<void> {
|
||||
const resp = await this.request.post(
|
||||
`${this.url}/api/devtools/set_settings`,
|
||||
{
|
||||
data: settings
|
||||
}
|
||||
)
|
||||
|
||||
if (resp.status() !== 200) {
|
||||
throw new Error(`Failed to setup settings: ${await resp.text()}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import { test as base } from '@playwright/test'
|
||||
|
||||
import { NodeBadgeMode } from '../../src/types/nodeSource'
|
||||
import type { ComfyPage } from './ComfyPage'
|
||||
import { ComfyMouse } from './ComfyMouse'
|
||||
import { testComfySnapToGridGridSize } from './constants'
|
||||
import { LocalhostComfyPage } from './LocalhostComfyPage'
|
||||
|
||||
/**
|
||||
* Localhost fixture for ComfyPage.
|
||||
* Creates a test user and sets up default settings for stable testing.
|
||||
*/
|
||||
export const comfyPageFixture = base.extend<{
|
||||
comfyPage: ComfyPage
|
||||
comfyMouse: ComfyMouse
|
||||
}>({
|
||||
comfyPage: async ({ page, request }, use, testInfo) => {
|
||||
const { parallelIndex } = testInfo
|
||||
const comfyPage = new LocalhostComfyPage(page, request, parallelIndex)
|
||||
|
||||
const username = `playwright-test-${parallelIndex}`
|
||||
const userId = await comfyPage.setupUser(username)
|
||||
if (userId) {
|
||||
comfyPage.userIds[parallelIndex] = userId
|
||||
}
|
||||
|
||||
try {
|
||||
await comfyPage.setupSettings({
|
||||
'Comfy.UseNewMenu': 'Top',
|
||||
// Hide canvas menu/info/selection toolbox by default.
|
||||
'Comfy.Graph.CanvasInfo': false,
|
||||
'Comfy.Graph.CanvasMenu': false,
|
||||
'Comfy.Canvas.SelectionToolbox': false,
|
||||
// Hide all badges by default.
|
||||
'Comfy.NodeBadge.NodeIdBadgeMode': NodeBadgeMode.None,
|
||||
'Comfy.NodeBadge.NodeSourceBadgeMode': NodeBadgeMode.None,
|
||||
// Disable tooltips by default to avoid flakiness.
|
||||
'Comfy.EnableTooltips': false,
|
||||
'Comfy.userId': userId,
|
||||
// Set tutorial completed to true to avoid loading the tutorial workflow.
|
||||
'Comfy.TutorialCompleted': true,
|
||||
'Comfy.SnapToGrid.GridSize': testComfySnapToGridGridSize,
|
||||
'Comfy.VueNodes.AutoScaleLayout': false
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
await comfyPage.setup()
|
||||
await use(comfyPage)
|
||||
},
|
||||
comfyMouse: async ({ comfyPage }, use) => {
|
||||
const comfyMouse = new ComfyMouse(comfyPage)
|
||||
await use(comfyMouse)
|
||||
}
|
||||
})
|
||||
@@ -1,4 +0,0 @@
|
||||
/**
|
||||
* Shared constants for browser tests
|
||||
*/
|
||||
export const testComfySnapToGridGridSize = 50
|
||||
@@ -1,63 +0,0 @@
|
||||
import { chromium } from '@playwright/test'
|
||||
import type { FullConfig } from '@playwright/test'
|
||||
import dotenv from 'dotenv'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
/**
|
||||
* Global setup for cloud tests.
|
||||
* Authenticates with Firebase and saves auth state for test reuse.
|
||||
*/
|
||||
export default async function globalSetupCloud(config: FullConfig) {
|
||||
const CLOUD_TEST_EMAIL = process.env.CLOUD_TEST_EMAIL
|
||||
const CLOUD_TEST_PASSWORD = process.env.CLOUD_TEST_PASSWORD
|
||||
|
||||
if (!CLOUD_TEST_EMAIL || !CLOUD_TEST_PASSWORD) {
|
||||
throw new Error(
|
||||
'CLOUD_TEST_EMAIL and CLOUD_TEST_PASSWORD must be set in environment variables'
|
||||
)
|
||||
}
|
||||
|
||||
const browser = await chromium.launch()
|
||||
const context = await browser.newContext()
|
||||
const page = await context.newPage()
|
||||
|
||||
try {
|
||||
// Navigate to cloud login page
|
||||
await page.goto('https://stagingcloud.comfy.org/cloud/login', {
|
||||
waitUntil: 'networkidle',
|
||||
timeout: 30000
|
||||
})
|
||||
|
||||
// Fill in email and password
|
||||
await page.fill('#cloud-sign-in-email', CLOUD_TEST_EMAIL)
|
||||
await page.fill('#cloud-sign-in-password', CLOUD_TEST_PASSWORD)
|
||||
|
||||
// Click login button
|
||||
await page.click('button[type="submit"]')
|
||||
|
||||
// Wait for redirect to main app
|
||||
await page.waitForURL('**/cloud', { timeout: 30000 })
|
||||
|
||||
// Wait a bit for auth tokens to be written to localStorage
|
||||
await page.waitForTimeout(2000)
|
||||
|
||||
// Ensure .auth directory exists
|
||||
const authDir = path.join(__dirname, '.auth')
|
||||
if (!fs.existsSync(authDir)) {
|
||||
fs.mkdirSync(authDir, { recursive: true })
|
||||
}
|
||||
|
||||
// Save authentication state (includes localStorage with Firebase tokens)
|
||||
await context.storageState({
|
||||
path: 'browser_tests/.auth/cloudUser.json'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to authenticate:', error)
|
||||
throw error
|
||||
} finally {
|
||||
await browser.close()
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
# Cloud E2E Tests
|
||||
|
||||
## Setup
|
||||
|
||||
Cloud tests run against `https://stagingcloud.comfy.org` with Firebase authentication.
|
||||
|
||||
### Required GitHub Secrets
|
||||
|
||||
Add these to repository settings → Secrets → Actions:
|
||||
|
||||
- `CLOUD_TEST_EMAIL`: Firebase test account email
|
||||
- `CLOUD_TEST_PASSWORD`: Firebase test account password
|
||||
|
||||
### Running Locally
|
||||
|
||||
```bash
|
||||
# Set environment variables
|
||||
export CLOUD_TEST_EMAIL="your-test-email@example.com"
|
||||
export CLOUD_TEST_PASSWORD="your-password"
|
||||
|
||||
# Run cloud tests
|
||||
pnpm exec playwright test --config=playwright.cloud.config.ts
|
||||
```
|
||||
|
||||
### Running in CI
|
||||
|
||||
Workflow: `.github/workflows/ci-tests-e2e-cloud.yaml`
|
||||
|
||||
Trigger manually via Actions tab → "CI: Tests E2E Cloud" → Run workflow
|
||||
|
||||
### Test Structure
|
||||
|
||||
- Tests tagged with `@cloud` run only in cloud config
|
||||
- Auth handled once in `globalSetupCloud.ts`
|
||||
- Auth state saved to `browser_tests/.auth/cloudUser.json`
|
||||
- Cloud fixture in `fixtures/ComfyPageCloud.ts`
|
||||
@@ -2,7 +2,7 @@ import type { Response } from '@playwright/test'
|
||||
import { expect, mergeTests } from '@playwright/test'
|
||||
|
||||
import type { StatusWsMessage } from '../../src/schemas/apiSchema.ts'
|
||||
import { comfyPageFixture } from '../fixtures/comfyPageFixture.ts'
|
||||
import { comfyPageFixture } from '../fixtures/ComfyPage.ts'
|
||||
import { webSocketFixture } from '../fixtures/ws.ts'
|
||||
|
||||
const test = mergeTests(comfyPageFixture, webSocketFixture)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Bottom Panel Shortcuts', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Browser tab title', () => {
|
||||
test.describe('Beta Menu', () => {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { ComfyPage } from '../fixtures/ComfyPage'
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyExpect as expect } from '../fixtures/ComfyPage'
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../fixtures/ComfyPage'
|
||||
|
||||
async function beforeChange(comfyPage: ComfyPage) {
|
||||
await comfyPage.page.evaluate(() => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
/**
|
||||
* @cloud
|
||||
* Cloud E2E tests.
|
||||
* Tests run against stagingcloud.comfy.org with authenticated user.
|
||||
*/
|
||||
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageCloudFixture as test } from '../fixtures/ComfyPageCloud'
|
||||
|
||||
test.describe('Cloud E2E @cloud', () => {
|
||||
test('loads app with authentication', async ({ comfyPage }) => {
|
||||
// App should be loaded from setup()
|
||||
await expect(comfyPage.canvas).toBeVisible()
|
||||
|
||||
// Verify we're authenticated (cloud-specific check)
|
||||
const isAuthenticated = await comfyPage.page.evaluate(() => {
|
||||
// Check for Firebase auth in localStorage
|
||||
const keys = Object.keys(localStorage)
|
||||
return keys.some(
|
||||
(key) => key.startsWith('firebase:') || key.includes('authUser')
|
||||
)
|
||||
})
|
||||
expect(isAuthenticated).toBe(true)
|
||||
})
|
||||
|
||||
test('can interact with canvas', async ({ comfyPage }) => {
|
||||
// Basic canvas interaction
|
||||
await comfyPage.doubleClickCanvas()
|
||||
await expect(comfyPage.searchBox.input).toBeVisible()
|
||||
|
||||
// Close search box
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await expect(comfyPage.searchBox.input).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('can access settings dialog', async ({ comfyPage }) => {
|
||||
// Open settings dialog
|
||||
await comfyPage.page.click('button[data-testid="settings-button"]', {
|
||||
timeout: 10000
|
||||
})
|
||||
|
||||
// Settings dialog should be visible
|
||||
await expect(comfyPage.page.locator('.p-dialog')).toBeVisible()
|
||||
|
||||
// Close settings
|
||||
await comfyPage.closeDialog()
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import type { Palette } from '../../src/schemas/colorPaletteSchema'
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import type { Locator } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
async function verifyCustomIconSvg(iconElement: Locator) {
|
||||
const svgVariable = await iconElement.evaluate((element) => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Locator } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import type { Keybinding } from '../../src/schemas/keyBindingSchema'
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import type { SettingParams } from '../../src/platform/settings/types'
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Topbar commands', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import type { ComfyPage } from '../fixtures/ComfyPage'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
import type { NodeReference } from '../fixtures/utils/litegraphUtils'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
|
||||
@@ -2,9 +2,11 @@ import type { Locator } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
import type { Position } from '@vueuse/core'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import {
|
||||
comfyPageFixture as test,
|
||||
testComfySnapToGridGridSize
|
||||
} from '../fixtures/ComfyPage'
|
||||
import type { ComfyPage } from '../fixtures/ComfyPage'
|
||||
import { testComfySnapToGridGridSize } from '../fixtures/ComfyPage'
|
||||
import type { NodeReference } from '../fixtures/utils/litegraphUtils'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Menu', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Minimap', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { expect } from '@playwright/test'
|
||||
|
||||
import type { ComfyApp } from '../../src/scripts/app'
|
||||
import { NodeBadgeMode } from '../../src/types/nodeSource'
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyExpect as expect } from '../fixtures/ComfyPage'
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../fixtures/ComfyPage'
|
||||
|
||||
// TODO: there might be a better solution for this
|
||||
// Helper function to pan canvas and select node
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyExpect as expect } from '../fixtures/ComfyPage'
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
import type { NodeReference } from '../fixtures/utils/litegraphUtils'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Release Notifications', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import type { ComfyPage } from '../fixtures/ComfyPage'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Remote COMBO Widget', () => {
|
||||
const mockOptions = ['d', 'c', 'b', 'a']
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
import { getMiddlePoint } from '../fixtures/utils/litegraphUtils'
|
||||
|
||||
test.describe('Reroute Node', () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { NodeBadgeMode } from '../../src/types/nodeSource'
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture } from '../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture } from '../fixtures/ComfyPage'
|
||||
|
||||
const test = comfyPageFixture
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Node library sidebar', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
|
||||
|
||||
test.describe.skip('Queue sidebar', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Workflows sidebar', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
// Constants
|
||||
const INITIAL_NAME = 'initial_slot_name'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
// Constants
|
||||
const RENAMED_INPUT_NAME = 'renamed_input'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
async function checkTemplateFileExists(
|
||||
page: Page,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import type { SystemStats } from '../../src/schemas/apiSchema'
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Version Mismatch Warnings', () => {
|
||||
const ALWAYS_AHEAD_OF_INSTALLED_VERSION = '100.100.100'
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { comfyPageFixture as test } from '../../../fixtures/comfyPageFixture'
|
||||
import { comfyExpect as expect } from '../../../fixtures/ComfyPage'
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../fixtures/ComfyPage'
|
||||
|
||||
const CREATE_GROUP_HOTKEY = 'Control+g'
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { comfyPageFixture as test } from '../../../../fixtures/comfyPageFixture'
|
||||
import { comfyExpect as expect } from '../../../../fixtures/ComfyPage'
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Vue Nodes Canvas Pan', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { comfyPageFixture as test } from '../../../../fixtures/comfyPageFixture'
|
||||
import { comfyExpect as expect } from '../../../../fixtures/ComfyPage'
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Vue Nodes Zoom', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
|
||||
@@ -2,8 +2,10 @@ import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
import type { NodeId } from '../../../../../src/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { getSlotKey } from '../../../../../src/renderer/core/layout/slots/slotIdentifier'
|
||||
import { comfyPageFixture as test } from '../../../../fixtures/comfyPageFixture'
|
||||
import { comfyExpect as expect } from '../../../../fixtures/ComfyPage'
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../../fixtures/ComfyPage'
|
||||
import { getMiddlePoint } from '../../../../fixtures/utils/litegraphUtils'
|
||||
import { fitToViewInstant } from '../../../../helpers/fitToView'
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { comfyPageFixture as test } from '../../../../fixtures/comfyPageFixture'
|
||||
import { comfyExpect as expect } from '../../../../fixtures/ComfyPage'
|
||||
import type { ComfyPage } from '../../../../fixtures/ComfyPage'
|
||||
import {
|
||||
type ComfyPage,
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../../fixtures/ComfyPage'
|
||||
import type { Position } from '../../../../fixtures/types'
|
||||
|
||||
test.describe('Vue Node Moving', () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../../../../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../../../../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { comfyPageFixture as test } from '../../../../fixtures/comfyPageFixture'
|
||||
import { comfyExpect as expect } from '../../../../fixtures/ComfyPage'
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Vue Nodes Renaming', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { comfyPageFixture as test } from '../../../../fixtures/comfyPageFixture'
|
||||
import { comfyExpect as expect } from '../../../../fixtures/ComfyPage'
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { comfyPageFixture as test } from '../../../fixtures/comfyPageFixture'
|
||||
import { comfyExpect as expect } from '../../../fixtures/ComfyPage'
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../fixtures/ComfyPage'
|
||||
|
||||
const BYPASS_HOTKEY = 'Control+b'
|
||||
const BYPASS_CLASS = /before:bg-bypass\/60/
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { comfyPageFixture as test } from '../../../fixtures/comfyPageFixture'
|
||||
import { comfyExpect as expect } from '../../../fixtures/ComfyPage'
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Vue Node Collapse', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { comfyPageFixture as test } from '../../../fixtures/comfyPageFixture'
|
||||
import { comfyExpect as expect } from '../../../fixtures/ComfyPage'
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Vue Node Custom Colors', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { comfyPageFixture as test } from '../../../fixtures/comfyPageFixture'
|
||||
import { comfyExpect as expect } from '../../../fixtures/ComfyPage'
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../fixtures/ComfyPage'
|
||||
|
||||
const ERROR_CLASS = /border-node-stroke-error/
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../../../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../../../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { comfyPageFixture as test } from '../../../fixtures/comfyPageFixture'
|
||||
import { comfyExpect as expect } from '../../../fixtures/ComfyPage'
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../fixtures/ComfyPage'
|
||||
|
||||
const MUTE_HOTKEY = 'Control+m'
|
||||
const MUTE_OPACITY = '0.5'
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { comfyPageFixture as test } from '../../../fixtures/comfyPageFixture'
|
||||
import { comfyExpect as expect } from '../../../fixtures/ComfyPage'
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../fixtures/ComfyPage'
|
||||
|
||||
const PIN_HOTKEY = 'p'
|
||||
const PIN_INDICATOR = '[data-testid="node-pin-indicator"]'
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { comfyPageFixture as test } from '../../../../fixtures/comfyPageFixture'
|
||||
import { comfyExpect as expect } from '../../../../fixtures/ComfyPage'
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Vue Integer Widget', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { comfyPageFixture as test } from '../../../../fixtures/comfyPageFixture'
|
||||
import { comfyExpect as expect } from '../../../../fixtures/ComfyPage'
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Vue Upload Widgets', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { comfyPageFixture as test } from '../../../../fixtures/comfyPageFixture'
|
||||
import { comfyExpect as expect } from '../../../../fixtures/ComfyPage'
|
||||
import type { ComfyPage } from '../../../../fixtures/ComfyPage'
|
||||
import {
|
||||
type ComfyPage,
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Vue Multiline String Widget', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { comfyPageFixture as test } from '../../../fixtures/comfyPageFixture'
|
||||
import { comfyExpect as expect } from '../../../fixtures/ComfyPage'
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Vue Widget Reactivity', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
|
||||
import type { ComfyPage } from '../fixtures/ComfyPage'
|
||||
import { type ComfyPage, comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Workflow Tab Thumbnails', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.32.2",
|
||||
"version": "1.32.4",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
|
||||
/**
|
||||
* Playwright configuration for cloud E2E tests.
|
||||
* Tests run against stagingcloud.comfy.org with authenticated user.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './browser_tests/tests',
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
reporter: 'html',
|
||||
|
||||
// Cloud tests need more time due to network latency
|
||||
timeout: 75000, // 5x the default 15000ms
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
|
||||
use: {
|
||||
trace: 'on-first-retry',
|
||||
// Cloud URL - can override with PLAYWRIGHT_TEST_URL env var
|
||||
baseURL: process.env.PLAYWRIGHT_TEST_URL || 'https://stagingcloud.comfy.org'
|
||||
},
|
||||
|
||||
// Authenticate once before all tests
|
||||
globalSetup: './browser_tests/globalSetupCloud.ts',
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
timeout: 75000,
|
||||
grep: /@cloud/ // Only run tests tagged with @cloud
|
||||
}
|
||||
]
|
||||
})
|
||||
@@ -31,7 +31,7 @@ export default defineConfig({
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
timeout: 15000,
|
||||
grepInvert: /@mobile|@cloud/ // Run all tests except those tagged with @mobile or @cloud
|
||||
grepInvert: /@mobile/ // Run all tests except those tagged with @mobile
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
26
src/App.vue
26
src/App.vue
@@ -16,6 +16,10 @@ import { computed, onMounted } from 'vue'
|
||||
|
||||
import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
|
||||
import config from '@/config'
|
||||
import { t } from '@/i18n'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
|
||||
|
||||
@@ -23,6 +27,8 @@ import { electronAPI, isElectron } from './utils/envUtil'
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const conflictDetection = useConflictDetection()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const dialogService = useDialogService()
|
||||
const isLoading = computed<boolean>(() => workspaceStore.spinner)
|
||||
const handleKey = (e: KeyboardEvent) => {
|
||||
workspaceStore.shiftDown = e.shiftKey
|
||||
@@ -48,6 +54,26 @@ onMounted(() => {
|
||||
document.addEventListener('contextmenu', showContextMenu)
|
||||
}
|
||||
|
||||
// Handle Vite preload errors (e.g., when assets are deleted after deployment)
|
||||
window.addEventListener('vite:preloadError', async (_event) => {
|
||||
// Auto-reload if app is not ready or there are no unsaved changes
|
||||
if (!app.vueAppReady || !workflowStore.activeWorkflow?.isModified) {
|
||||
window.location.reload()
|
||||
} else {
|
||||
// Show confirmation dialog if there are unsaved changes
|
||||
await dialogService
|
||||
.confirm({
|
||||
title: t('g.vitePreloadErrorTitle'),
|
||||
message: t('g.vitePreloadErrorMessage')
|
||||
})
|
||||
.then((confirmed) => {
|
||||
if (confirmed) {
|
||||
window.location.reload()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Initialize conflict detection in background
|
||||
// This runs async and doesn't block UI setup
|
||||
void conflictDetection.initializeConflictDetection()
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<div
|
||||
class="actionbar-container pointer-events-auto flex h-12 items-center rounded-lg border border-[var(--interface-stroke)] px-2 shadow-interface"
|
||||
>
|
||||
<ActionBarButtons />
|
||||
<!-- Support for legacy topbar elements attached by custom scripts, hidden if no elements present -->
|
||||
<div
|
||||
ref="legacyCommandsContainerRef"
|
||||
@@ -24,6 +25,7 @@ import { onMounted, ref } from 'vue'
|
||||
|
||||
import ComfyActionbar from '@/components/actionbar/ComfyActionbar.vue'
|
||||
import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
|
||||
import ActionBarButtons from '@/components/topbar/ActionBarButtons.vue'
|
||||
import CurrentUserButton from '@/components/topbar/CurrentUserButton.vue'
|
||||
import LoginButton from '@/components/topbar/LoginButton.vue'
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
|
||||
@@ -103,7 +103,7 @@ if (isComponentWidget(props.widget)) {
|
||||
const load3DSceneRef = ref<InstanceType<typeof Load3DScene> | null>(null)
|
||||
|
||||
const {
|
||||
// configs
|
||||
// config
|
||||
sceneConfig,
|
||||
modelConfig,
|
||||
cameraConfig,
|
||||
|
||||
29
src/components/topbar/ActionBarButtons.vue
Normal file
29
src/components/topbar/ActionBarButtons.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div class="flex h-full shrink-0 items-center gap-1">
|
||||
<Button
|
||||
v-for="(button, index) in actionBarButtonStore.buttons"
|
||||
:key="index"
|
||||
v-tooltip.bottom="button.tooltip"
|
||||
:label="button.label"
|
||||
:aria-label="button.tooltip || button.label"
|
||||
:class="button.class"
|
||||
text
|
||||
rounded
|
||||
severity="secondary"
|
||||
class="h-7"
|
||||
@click="button.onClick"
|
||||
>
|
||||
<template #icon>
|
||||
<i :class="button.icon" />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Button from 'primevue/button'
|
||||
|
||||
import { useActionBarButtonStore } from '@/stores/actionBarButtonStore'
|
||||
|
||||
const actionBarButtonStore = useActionBarButtonStore()
|
||||
</script>
|
||||
@@ -5,6 +5,7 @@ import { t } from '@/i18n'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
|
||||
const DEFAULT_TITLE = 'ComfyUI'
|
||||
const TITLE_SUFFIX = ' - ComfyUI'
|
||||
@@ -13,6 +14,7 @@ export const useBrowserTabTitle = () => {
|
||||
const executionStore = useExecutionStore()
|
||||
const settingStore = useSettingStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
|
||||
const executionText = computed(() =>
|
||||
executionStore.isIdle
|
||||
@@ -24,11 +26,27 @@ export const useBrowserTabTitle = () => {
|
||||
() => settingStore.get('Comfy.UseNewMenu') !== 'Disabled'
|
||||
)
|
||||
|
||||
const isAutoSaveEnabled = computed(
|
||||
() => settingStore.get('Comfy.Workflow.AutoSave') === 'after delay'
|
||||
)
|
||||
|
||||
const isActiveWorkflowModified = computed(
|
||||
() => !!workflowStore.activeWorkflow?.isModified
|
||||
)
|
||||
const isActiveWorkflowPersisted = computed(
|
||||
() => !!workflowStore.activeWorkflow?.isPersisted
|
||||
)
|
||||
|
||||
const shouldShowUnsavedIndicator = computed(() => {
|
||||
if (workspaceStore.shiftDown) return false
|
||||
if (isAutoSaveEnabled.value) return false
|
||||
if (!isActiveWorkflowPersisted.value) return true
|
||||
if (isActiveWorkflowModified.value) return true
|
||||
return false
|
||||
})
|
||||
|
||||
const isUnsavedText = computed(() =>
|
||||
workflowStore.activeWorkflow?.isModified ||
|
||||
!workflowStore.activeWorkflow?.isPersisted
|
||||
? ' *'
|
||||
: ''
|
||||
shouldShowUnsavedIndicator.value ? ' *' : ''
|
||||
)
|
||||
const workflowNameText = computed(() => {
|
||||
const workflowName = workflowStore.activeWorkflow?.filename
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
|
||||
import { useModelSelectorDialog } from '@/composables/useModelSelectorDialog'
|
||||
@@ -20,7 +21,7 @@ import { useAssetBrowserDialog } from '@/platform/assets/composables/useAssetBro
|
||||
import { createModelNodeFromAsset } from '@/platform/assets/utils/createModelNodeFromAsset'
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { SUPPORT_URL } from '@/platform/support/config'
|
||||
import { buildSupportUrl } from '@/platform/support/config'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import type { ExecutionTriggerSource } from '@/platform/telemetry/types'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
@@ -840,7 +841,12 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
label: 'Contact Support',
|
||||
versionAdded: '1.17.8',
|
||||
function: () => {
|
||||
window.open(SUPPORT_URL, '_blank')
|
||||
const { userEmail, resolvedUserInfo } = useCurrentUser()
|
||||
const supportUrl = buildSupportUrl({
|
||||
userEmail: userEmail.value,
|
||||
userId: resolvedUserInfo.value?.id
|
||||
})
|
||||
window.open(supportUrl, '_blank')
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
23
src/extensions/core/cloudFeedbackTopbarButton.ts
Normal file
23
src/extensions/core/cloudFeedbackTopbarButton.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { t } from '@/i18n'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
import type { ActionBarButton } from '@/types/comfy'
|
||||
|
||||
// Zendesk feedback URL - update this with the actual URL
|
||||
const ZENDESK_FEEDBACK_URL =
|
||||
'https://support.comfy.org/hc/en-us/requests/new?ticket_form_id=43066738713236'
|
||||
|
||||
const buttons: ActionBarButton[] = [
|
||||
{
|
||||
icon: 'icon-[lucide--message-circle-question-mark]',
|
||||
label: t('actionbar.feedback'),
|
||||
tooltip: t('actionbar.feedbackTooltip'),
|
||||
onClick: () => {
|
||||
window.open(ZENDESK_FEEDBACK_URL, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
useExtensionService().registerExtension({
|
||||
name: 'Comfy.Cloud.FeedbackButton',
|
||||
actionBarButtons: buttons
|
||||
})
|
||||
@@ -29,6 +29,7 @@ if (isCloud) {
|
||||
await import('./cloudRemoteConfig')
|
||||
await import('./cloudBadges')
|
||||
await import('./cloudSessionCookie')
|
||||
await import('./cloudFeedbackTopbarButton')
|
||||
|
||||
if (window.__CONFIG__?.subscription_required) {
|
||||
await import('./cloudSubscription')
|
||||
|
||||
@@ -4,6 +4,7 @@ import { app } from '../../scripts/app'
|
||||
|
||||
const saveNodeTypes = new Set([
|
||||
'SaveImage',
|
||||
'SaveVideo',
|
||||
'SaveAnimatedWEBP',
|
||||
'SaveWEBM',
|
||||
'SaveAudio',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user