Compare commits
18 Commits
codex/fron
...
jaeone/err
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
905bc50129 | ||
|
|
5bf41a41bd | ||
|
|
a7f14a0b3f | ||
|
|
e6d1a9d4a2 | ||
|
|
0b4a960467 | ||
|
|
060957d66c | ||
|
|
1a98362984 | ||
|
|
ab33746b3e | ||
|
|
55c4e807a1 | ||
|
|
74147d7ee2 | ||
|
|
16fdcab4be | ||
|
|
96c6dbcb69 | ||
|
|
ffb4ae34c6 | ||
|
|
59341a4b71 | ||
|
|
97fffb5394 | ||
|
|
4972e4d42f | ||
|
|
8d8ae7dfc0 | ||
|
|
38c034e1d6 |
@@ -22,29 +22,6 @@ reviews:
|
||||
docstrings:
|
||||
mode: 'off'
|
||||
custom_checks:
|
||||
- name: Reproduction and contribution evidence
|
||||
mode: error
|
||||
instructions: |
|
||||
Use only PR metadata already available in the review context: the PR
|
||||
description, changed-file list relative to the base, and diff content.
|
||||
Do not run shell commands or follow links to infer missing evidence.
|
||||
|
||||
Fail when the PR description does not contain concrete reproduction or
|
||||
validation steps that another reviewer can follow.
|
||||
|
||||
The PR must select exactly one evidence type:
|
||||
1. Visual: require either distinct Before and After screenshot links, or
|
||||
one screencast link that demonstrates the before and after behavior.
|
||||
2. Non-visual: allow this only when the diff has no user-visible behavior
|
||||
or presentation change. Require an `N/A:` rationale plus a concrete
|
||||
test command and result or relevant log evidence.
|
||||
|
||||
Fail placeholders, generic statements such as `done` or `tested`, a
|
||||
screenshot used as both Before and After, and Non-visual selections for
|
||||
changes to rendered UI, styling, interaction, or user-visible states.
|
||||
Pass otherwise. The repository metadata workflow validates field shape;
|
||||
this check validates whether the supplied evidence matches the diff.
|
||||
|
||||
- name: End-to-end regression coverage for fixes
|
||||
mode: error
|
||||
instructions: |
|
||||
|
||||
31
.github/ISSUE_TEMPLATE/bug-report.yaml
vendored
@@ -39,37 +39,6 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: evidence-type
|
||||
attributes:
|
||||
label: Evidence type
|
||||
description: Choose the evidence that supports this report.
|
||||
options:
|
||||
- Visual
|
||||
- Non-visual
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: evidence
|
||||
attributes:
|
||||
label: Evidence
|
||||
description: >-
|
||||
Visual reports require distinct before and after links or one screencast.
|
||||
For non-visual reports, explain why visuals do not apply and include test
|
||||
or log output.
|
||||
placeholder: |
|
||||
Visual:
|
||||
Before: Drag in a screenshot.
|
||||
After: Drag in a screenshot.
|
||||
Or Screencast: Drag in one recording that shows both states.
|
||||
|
||||
Non-visual:
|
||||
N/A: Explain why visual evidence does not apply.
|
||||
Test/log evidence: Paste the command and result, logs, or a link.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: severity
|
||||
attributes:
|
||||
|
||||
40
.github/ISSUE_TEMPLATE/feature-request.yaml
vendored
@@ -29,46 +29,6 @@ body:
|
||||
- While collaborating with team members...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: example
|
||||
attributes:
|
||||
label: Concrete example or steps
|
||||
description: Show the exact workflow or steps where the problem occurs.
|
||||
placeholder: |
|
||||
1. Open a workflow with...
|
||||
2. Try to...
|
||||
3. Observe...
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: evidence-type
|
||||
attributes:
|
||||
label: Evidence type
|
||||
description: Choose the evidence that supports this request.
|
||||
options:
|
||||
- Visual
|
||||
- Non-visual
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: evidence
|
||||
attributes:
|
||||
label: Evidence
|
||||
description: >-
|
||||
Visual requests require distinct before and after links or one screencast.
|
||||
For non-visual requests, explain why visuals do not apply and include test
|
||||
or log output.
|
||||
placeholder: |
|
||||
Visual:
|
||||
Before: Drag in a screenshot.
|
||||
After: Drag in a screenshot.
|
||||
Or Screencast: Drag in one recording that shows both states.
|
||||
|
||||
Non-visual:
|
||||
N/A: Explain why visual evidence does not apply.
|
||||
Test/log evidence: Paste the command and result, logs, or a link.
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: frequency
|
||||
attributes:
|
||||
|
||||
33
.github/pull_request_template.md
vendored
@@ -15,35 +15,6 @@
|
||||
<!-- If this PR fixes an issue, uncomment and update the line below -->
|
||||
<!-- Fixes #ISSUE_NUMBER -->
|
||||
|
||||
## Reproduction or validation steps
|
||||
## Screenshots (if applicable)
|
||||
|
||||
<!-- Required. List the exact steps a reviewer can follow. -->
|
||||
|
||||
1.
|
||||
|
||||
## Evidence type
|
||||
|
||||
<!-- Required. Check exactly one. -->
|
||||
|
||||
- [ ] Visual
|
||||
- [ ] Non-visual
|
||||
|
||||
## Before
|
||||
|
||||
<!-- Visual: add a screenshot link. Leave blank when using a screencast. -->
|
||||
|
||||
## After
|
||||
|
||||
<!-- Visual: add a screenshot link. Leave blank when using a screencast. -->
|
||||
|
||||
## Screencast
|
||||
|
||||
<!-- Visual alternative: add one link that demonstrates before and after. -->
|
||||
|
||||
## Non-visual rationale
|
||||
|
||||
<!-- Non-visual only. Start with "N/A:" and explain why visuals do not apply. -->
|
||||
|
||||
## Test or log evidence
|
||||
|
||||
<!-- Non-visual only. Paste the command and result, logs, or a link. -->
|
||||
<!-- Add screenshots or video recording to help explain your changes -->
|
||||
|
||||
@@ -41,20 +41,6 @@ jobs:
|
||||
title: '[chore] Update electron-types to ${{ steps.get-version.outputs.NEW_VERSION }}'
|
||||
body: |
|
||||
Automated update of desktop API types to version ${{ steps.get-version.outputs.NEW_VERSION }}.
|
||||
|
||||
## Reproduction or validation steps
|
||||
1. Review the package and lockfile changes for the expected version.
|
||||
2. Confirm the desktop API type checks pass in CI.
|
||||
|
||||
## Evidence type
|
||||
- [ ] Visual
|
||||
- [x] Non-visual
|
||||
|
||||
## Non-visual rationale
|
||||
N/A: This automated dependency update has no visual behavior.
|
||||
|
||||
## Test or log evidence
|
||||
`pnpm install --workspace-root @comfyorg/comfyui-electron-types@latest` passed in workflow run `${{ github.run_id }}`.
|
||||
branch: update-electron-types-${{ steps.get-version.outputs.NEW_VERSION }}
|
||||
base: main
|
||||
labels: |
|
||||
|
||||
@@ -98,20 +98,6 @@ jobs:
|
||||
- Generated on: ${{ github.event.repository.updated_at }}
|
||||
|
||||
These types are automatically generated using openapi-typescript.
|
||||
|
||||
## Reproduction or validation steps
|
||||
1. Review the generated type diff against the referenced Manager commit.
|
||||
2. Confirm the generated type lint and repository checks pass in CI.
|
||||
|
||||
## Evidence type
|
||||
- [ ] Visual
|
||||
- [x] Non-visual
|
||||
|
||||
## Non-visual rationale
|
||||
N/A: This automated generated-type update has no visual behavior.
|
||||
|
||||
## Test or log evidence
|
||||
`pnpm lint:fix:no-cache -- ./src/types/generatedManagerTypes.ts` passed in workflow run `${{ github.run_id }}`.
|
||||
branch: update-manager-types-${{ steps.manager-info.outputs.commit }}
|
||||
base: ${{ inputs.target_branch }}
|
||||
labels: Manager
|
||||
|
||||
117
.github/workflows/ci-issue-evidence.yaml
vendored
@@ -1,117 +0,0 @@
|
||||
# Description: Labels and reports issues blocked by missing evidence or a Comfy human owner.
|
||||
name: 'CI: Issue Evidence'
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited, reopened, assigned, unassigned]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
concurrency:
|
||||
group: issue-evidence-${{ github.event.issue.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
issue-evidence:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout default-branch validator
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.workflow_sha }}
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Validate issue evidence
|
||||
id: evidence
|
||||
continue-on-error: true
|
||||
run: node scripts/cicd/validate-evidence.ts issue "$GITHUB_EVENT_PATH"
|
||||
|
||||
- name: Validate human maintainer assignment
|
||||
id: maintainer
|
||||
continue-on-error: true
|
||||
env:
|
||||
COMFY_ORG_MEMBERS_READ_TOKEN: ${{ secrets.COMFY_ORG_MEMBERS_READ_TOKEN }}
|
||||
run: node scripts/cicd/validate-maintainer.ts "$GITHUB_EVENT_PATH"
|
||||
|
||||
- name: Update blocked labels
|
||||
if: always()
|
||||
uses: actions/github-script@v8
|
||||
env:
|
||||
EVIDENCE_VALID: ${{ steps.evidence.outputs.valid }}
|
||||
MAINTAINER_VALID: ${{ steps.maintainer.outputs.valid }}
|
||||
with:
|
||||
script: |
|
||||
const issue_number = context.issue.number
|
||||
const { owner, repo } = context.repo
|
||||
const policies = [
|
||||
{
|
||||
description: 'Blocked until required reproduction and evidence details are added',
|
||||
name: 'blocked: needs-evidence',
|
||||
valid: process.env.EVIDENCE_VALID === 'true'
|
||||
},
|
||||
{
|
||||
description: 'Blocked until an active human comfy_frontend_devs member is assigned',
|
||||
name: 'blocked: needs-maintainer',
|
||||
valid: process.env.MAINTAINER_VALID === 'true'
|
||||
}
|
||||
]
|
||||
|
||||
for (const policy of policies) {
|
||||
if (policy.valid) {
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner,
|
||||
repo,
|
||||
issue_number,
|
||||
name: policy.name
|
||||
})
|
||||
} catch (error) {
|
||||
if (error.status !== 404) throw error
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
await github.rest.issues.getLabel({
|
||||
owner,
|
||||
repo,
|
||||
name: policy.name
|
||||
})
|
||||
} catch (error) {
|
||||
if (error.status !== 404) throw error
|
||||
await github.rest.issues.createLabel({
|
||||
owner,
|
||||
repo,
|
||||
name: policy.name,
|
||||
color: 'B60205',
|
||||
description: policy.description
|
||||
}).catch((createError) => {
|
||||
const alreadyExists =
|
||||
createError.status === 422 &&
|
||||
createError.response?.data?.errors?.some(
|
||||
(detail) => detail.code === 'already_exists'
|
||||
)
|
||||
if (!alreadyExists) throw createError
|
||||
})
|
||||
}
|
||||
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number,
|
||||
labels: [policy.name]
|
||||
})
|
||||
}
|
||||
|
||||
- name: Fail blocked contribution
|
||||
if: >-
|
||||
always() &&
|
||||
(steps.evidence.outputs.valid != 'true' ||
|
||||
steps.maintainer.outputs.valid != 'true')
|
||||
run: exit 1
|
||||
67
.github/workflows/ci-pr-evidence.yaml
vendored
@@ -1,67 +0,0 @@
|
||||
# Description: Enforces PR evidence and a Comfy human owner with trusted base-branch code.
|
||||
name: 'CI: PR Evidence'
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
[
|
||||
opened,
|
||||
edited,
|
||||
synchronize,
|
||||
reopened,
|
||||
ready_for_review,
|
||||
assigned,
|
||||
unassigned
|
||||
]
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
concurrency:
|
||||
group: pr-evidence-${{ github.event.pull_request.number || github.event.merge_group.head_sha }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
pr-evidence:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout trusted validator
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.workflow_sha }}
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Validate human maintainer assignment
|
||||
id: maintainer
|
||||
if: github.event_name == 'pull_request_target'
|
||||
continue-on-error: true
|
||||
env:
|
||||
COMFY_ORG_MEMBERS_READ_TOKEN: ${{ secrets.COMFY_ORG_MEMBERS_READ_TOKEN }}
|
||||
run: node scripts/cicd/validate-maintainer.ts "$GITHUB_EVENT_PATH"
|
||||
|
||||
- name: Validate pull request evidence
|
||||
id: evidence
|
||||
if: github.event_name == 'pull_request_target'
|
||||
continue-on-error: true
|
||||
run: node scripts/cicd/validate-evidence.ts pr "$GITHUB_EVENT_PATH"
|
||||
|
||||
- name: Enforce contribution policy
|
||||
if: >-
|
||||
github.event_name == 'pull_request_target' &&
|
||||
(steps.maintainer.outputs.valid != 'true' ||
|
||||
steps.evidence.outputs.valid != 'true')
|
||||
run: exit 1
|
||||
|
||||
- name: Validate current merge group metadata
|
||||
if: github.event_name == 'merge_group'
|
||||
env:
|
||||
COMFY_ORG_MEMBERS_READ_TOKEN: ${{ secrets.COMFY_ORG_MEMBERS_READ_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: node scripts/cicd/validate-merge-group.ts "$GITHUB_EVENT_PATH"
|
||||
14
.github/workflows/i18n-update-nodes.yaml
vendored
@@ -49,20 +49,6 @@ jobs:
|
||||
Automated PR to update locales for node definitions
|
||||
|
||||
This PR was created automatically by the frontend update workflow.
|
||||
|
||||
## Reproduction or validation steps
|
||||
1. Review the locale diff for the expected node-definition strings.
|
||||
2. Confirm locale generation and repository checks pass in CI.
|
||||
|
||||
## Evidence type
|
||||
- [ ] Visual
|
||||
- [x] Non-visual
|
||||
|
||||
## Non-visual rationale
|
||||
N/A: This automated locale data update has no visual behavior.
|
||||
|
||||
## Test or log evidence
|
||||
`pnpm locale` passed in workflow run `${{ github.run_id }}`.
|
||||
branch: update-locales-node-defs-${{ github.event.inputs.trigger_type }}-${{ github.run_id }}
|
||||
base: main
|
||||
labels: dependencies
|
||||
|
||||
16
.github/workflows/pr-backport.yaml
vendored
@@ -348,34 +348,22 @@ jobs:
|
||||
run: |
|
||||
# Get PR data for manual triggers
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json title,author,body)
|
||||
PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json title,author)
|
||||
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
|
||||
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login')
|
||||
PR_SOURCE_BODY=$(echo "$PR_DATA" | jq -r '.body // ""')
|
||||
else
|
||||
PR_TITLE=$(jq -r '.pull_request.title' "$GITHUB_EVENT_PATH")
|
||||
PR_AUTHOR=$(jq -r '.pull_request.user.login' "$GITHUB_EVENT_PATH")
|
||||
PR_SOURCE_BODY=$(jq -r '.pull_request.body // ""' "$GITHUB_EVENT_PATH")
|
||||
fi
|
||||
|
||||
for backport in ${{ steps.backport.outputs.success }}; do
|
||||
IFS=':' read -r target branch <<< "${backport}"
|
||||
|
||||
PR_BODY=$(cat <<EOF
|
||||
Backport of #${PR_NUMBER} to \`${target}\`.
|
||||
|
||||
Automatically created by the backport workflow.
|
||||
|
||||
## Source pull request evidence
|
||||
${PR_SOURCE_BODY}
|
||||
EOF
|
||||
)
|
||||
|
||||
if PR_URL=$(gh pr create \
|
||||
--base "${target}" \
|
||||
--head "${branch}" \
|
||||
--title "[backport ${target}] ${PR_TITLE}" \
|
||||
--body "${PR_BODY}" \
|
||||
--body "Backport of #${PR_NUMBER} to \`${target}\`"$'\n\n'"Automatically created by backport workflow." \
|
||||
--label "backport" 2>&1); then
|
||||
|
||||
# Extract PR number from URL
|
||||
|
||||
@@ -23,6 +23,10 @@ on:
|
||||
required: false
|
||||
default: 'Comfy-Org/ComfyUI'
|
||||
type: string
|
||||
target_branch:
|
||||
description: 'Optional: force a specific release branch, e.g. core/1.47 or core/2.0. Overrides the pin-derived target — use to skip a dead minor or do an out-of-cadence / major release.'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
check-release-week:
|
||||
@@ -49,12 +53,15 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Summary
|
||||
env:
|
||||
TARGET_BRANCH_OVERRIDE: ${{ inputs.target_branch }}
|
||||
run: |
|
||||
echo "## Release Check" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Is release week: ${{ steps.check.outputs.is_release_week }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Manual trigger: ${{ github.event_name == 'workflow_dispatch' }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Release type: ${{ inputs.release_type || 'minor (scheduled)' }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Target branch override: ${TARGET_BRANCH_OVERRIDE:-(none — pin-derived)}" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
resolve-version:
|
||||
needs: check-release-week
|
||||
@@ -103,6 +110,7 @@ jobs:
|
||||
working-directory: frontend
|
||||
env:
|
||||
RELEASE_TYPE: ${{ inputs.release_type || 'minor' }}
|
||||
TARGET_BRANCH: ${{ inputs.target_branch }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
|
||||
14
.github/workflows/release-version-bump.yaml
vendored
@@ -202,20 +202,6 @@ jobs:
|
||||
${{ steps.capitalised.outputs.capitalised }} version increment to ${{ steps.bump-version.outputs.NEW_VERSION }}
|
||||
|
||||
**Base branch:** `${{ steps.prepared-inputs.outputs.branch }}`
|
||||
|
||||
## Reproduction or validation steps
|
||||
1. Review the package and lockfile version changes.
|
||||
2. Confirm release checks pass for the target branch in CI.
|
||||
|
||||
## Evidence type
|
||||
- [ ] Visual
|
||||
- [x] Non-visual
|
||||
|
||||
## Non-visual rationale
|
||||
N/A: This automated version bump has no visual behavior.
|
||||
|
||||
## Test or log evidence
|
||||
`pnpm version ${{ steps.prepared-inputs.outputs.version_type }} --no-git-tag-version` passed in workflow run `${{ github.run_id }}`.
|
||||
branch: version-bump-${{ steps.bump-version.outputs.NEW_VERSION }}
|
||||
base: ${{ steps.prepared-inputs.outputs.branch }}
|
||||
labels: |
|
||||
|
||||
14
.github/workflows/release-website.yaml
vendored
@@ -63,20 +63,6 @@ jobs:
|
||||
snapshot (with a warning annotation in CI).
|
||||
|
||||
Triggered by workflow run `${{ github.run_id }}`.
|
||||
|
||||
## Reproduction or validation steps
|
||||
1. Review both regenerated snapshot diffs for expected remote data.
|
||||
2. Confirm the Vercel website preview builds successfully.
|
||||
|
||||
## Evidence type
|
||||
- [ ] Visual
|
||||
- [x] Non-visual
|
||||
|
||||
## Non-visual rationale
|
||||
N/A: This automated snapshot refresh changes data files only.
|
||||
|
||||
## Test or log evidence
|
||||
Status: success for snapshot generation in workflow run `${{ github.run_id }}`.
|
||||
branch: chore/refresh-website-snapshots-${{ github.run_id }}
|
||||
base: main
|
||||
labels: |
|
||||
|
||||
14
.github/workflows/version-bump-desktop-ui.yaml
vendored
@@ -86,20 +86,6 @@ jobs:
|
||||
${{ steps.capitalised.outputs.capitalised }} version increment for @comfyorg/desktop-ui to ${{ steps.bump-version.outputs.NEW_VERSION }}
|
||||
|
||||
**Base branch:** `${{ github.event.inputs.branch }}`
|
||||
|
||||
## Reproduction or validation steps
|
||||
1. Review the desktop UI package and lockfile version changes.
|
||||
2. Confirm desktop UI release checks pass for the target branch in CI.
|
||||
|
||||
## Evidence type
|
||||
- [ ] Visual
|
||||
- [x] Non-visual
|
||||
|
||||
## Non-visual rationale
|
||||
N/A: This automated package version bump has no visual behavior.
|
||||
|
||||
## Test or log evidence
|
||||
`pnpm -C apps/desktop-ui version ${{ github.event.inputs.version_type }} --no-git-tag-version` passed in workflow run `${{ github.run_id }}`.
|
||||
branch: desktop-ui-version-bump-${{ steps.bump-version.outputs.NEW_VERSION }}
|
||||
base: ${{ github.event.inputs.branch }}
|
||||
labels: |
|
||||
|
||||
21
.github/workflows/weekly-docs-check.yaml
vendored
@@ -84,7 +84,6 @@ jobs:
|
||||
- ## Review Notes section with any important context
|
||||
3. Be specific about which files were updated and why
|
||||
4. If no changes were needed, write a brief message stating documentation is up to date
|
||||
5. Do not add evidence-contract headings; the workflow appends those sections
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
claude_args: "--max-turns 256 --allowedTools 'Bash(git status),Bash(git diff),Bash(git log),Bash(pnpm:*),Bash(npm:*),Bash(node:*),Bash(tsc:*),Bash(echo:*),Read,Write,Edit,Glob,Grep'"
|
||||
continue-on-error: false
|
||||
@@ -128,26 +127,6 @@ jobs:
|
||||
EOF
|
||||
fi
|
||||
|
||||
- name: Add required PR evidence
|
||||
if: steps.check_changes.outputs.has_changes == 'true'
|
||||
run: |
|
||||
cat >> /tmp/pr-body-${{ github.run_id }}.md <<'EOF'
|
||||
|
||||
## Reproduction or validation steps
|
||||
1. Review each documentation diff against the current implementation.
|
||||
2. Confirm referenced commands and links work as documented.
|
||||
|
||||
## Evidence type
|
||||
- [ ] Visual
|
||||
- [x] Non-visual
|
||||
|
||||
## Non-visual rationale
|
||||
N/A: This automated review changes documentation only.
|
||||
|
||||
## Test or log evidence
|
||||
Status: success. The documentation review completed before this PR was created.
|
||||
EOF
|
||||
|
||||
- name: Create or Update Pull Request
|
||||
if: steps.check_changes.outputs.has_changes == 'true'
|
||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
|
||||
|
||||
@@ -258,11 +258,8 @@ The original litegraph repository (https://github.com/Comfy-Org/litegraph.js) is
|
||||
|
||||
1. Ensure your branch is up to date with main
|
||||
2. Run all tests and ensure they pass
|
||||
3. Create a pull request with a clear title, description, and exact reproduction or validation steps
|
||||
4. Include contribution evidence:
|
||||
- For visual behavior or presentation changes, add distinct Before and After screenshots or one screencast that demonstrates both states.
|
||||
- For a truly non-visual change, select Non-visual, add an `N/A:` rationale, and include the test command and result or relevant logs.
|
||||
5. Use conventional commit format for PR titles:
|
||||
3. Create a pull request with a clear title and description
|
||||
4. Use conventional commit format for PR titles:
|
||||
- `feat:` for new features
|
||||
- `fix:` for bug fixes
|
||||
- `docs:` for documentation
|
||||
@@ -273,51 +270,9 @@ The original litegraph repository (https://github.com/Comfy-Org/litegraph.js) is
|
||||
### Review Process
|
||||
|
||||
1. All PRs require at least one review
|
||||
2. Issues and pull requests under active execution require an active human member of the Comfy-Org `comfy_frontend_devs` team as an assignee. Automation and service accounts are executors, not accountable owners. Choose the person with the most context, using CODEOWNERS and recent changes to the related code path. The policy preserves existing assignees; COMOS performs contextual selection for artifacts it creates.
|
||||
3. Address review feedback promptly
|
||||
4. Keep PRs focused - one feature/fix per PR
|
||||
5. Large features should be discussed in an issue first
|
||||
|
||||
The `pr-evidence` check fails until both the evidence contract and human owner
|
||||
contract are satisfied. Issues receive `blocked: needs-evidence` and
|
||||
`blocked: needs-maintainer` labels as applicable, and the policy workflow fails
|
||||
until both conditions are fixed. Opaque GitHub attachment links in Screencast
|
||||
fields are fetched without credentials and accepted only when GitHub serves
|
||||
video content or an animated GIF. A static screenshot labeled as a screencast
|
||||
does not satisfy the check.
|
||||
|
||||
Repository administrators must configure the
|
||||
`COMFY_ORG_MEMBERS_READ_TOKEN` Actions secret before enabling `pr-evidence` in
|
||||
the ProtectMain or Core release branch rulesets. Use a dedicated fine-grained
|
||||
token with `Comfy-Org` organization `Members: read` permission, including team
|
||||
membership reads, and no repository write permission. The trusted workflows use
|
||||
it to verify the assignee's GitHub user type, active organization membership,
|
||||
and active `comfy_frontend_devs` team membership. They fail closed when the
|
||||
secret or any lookup is unavailable. Merge the workflow to the default branch
|
||||
and verify a successful check before making it required.
|
||||
|
||||
#### Contribution policy rollout
|
||||
|
||||
Do not make `pr-evidence` required when the workflow first lands. Use this
|
||||
sequence so existing open contributions are not blocked without warning:
|
||||
|
||||
1. Configure and verify `COMFY_ORG_MEMBERS_READ_TOKEN`.
|
||||
2. Merge the policy workflows while `pr-evidence` is not a required check. New
|
||||
and updated contributions still report failures and blocked labels, which is
|
||||
the audit signal.
|
||||
3. Observe a small canary across manual, fork, and automated contributions.
|
||||
Confirm assignment edits rerun the check and valid edits clear issue labels.
|
||||
4. Audit and backfill the active pull request and issue queues with evidence and
|
||||
a contextual Comfy-Org human assignee.
|
||||
5. Add `pr-evidence` to ProtectMain ruleset `991238` only after the active main
|
||||
queue is ready.
|
||||
6. Test a canary against both `core/**` and `cloud/**`. Add `pr-evidence` to Core
|
||||
release branches ruleset `7297873` only after both targets are confirmed to
|
||||
trigger the trusted workflow. Confirm the merge-group check refetches and
|
||||
validates current PR bodies and assignees before enabling the ruleset.
|
||||
|
||||
The workflow is intentionally fail closed during the audit phase. It remains
|
||||
non-blocking only because the rulesets do not require its check yet.
|
||||
2. Address review feedback promptly
|
||||
3. Keep PRs focused - one feature/fix per PR
|
||||
4. Large features should be discussed in an issue first
|
||||
|
||||
## Questions?
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ test.describe('Careers page @smoke', () => {
|
||||
})
|
||||
|
||||
test('has correct title', async ({ page }) => {
|
||||
await expect(page).toHaveTitle('Careers — Comfy')
|
||||
await expect(page).toHaveTitle('Careers - Comfy')
|
||||
})
|
||||
|
||||
test('Roles section heading is visible', async ({ page }) => {
|
||||
@@ -72,7 +72,7 @@ test.describe('Careers page role links', () => {
|
||||
test.describe('Careers page (zh-CN) @smoke', () => {
|
||||
test('renders localized heading and roles', async ({ page }) => {
|
||||
await page.goto('/zh-CN/careers')
|
||||
await expect(page).toHaveTitle('招聘 — Comfy')
|
||||
await expect(page).toHaveTitle('招聘 - Comfy')
|
||||
await expect(
|
||||
page.getByRole('heading', { name: '职位', level: 2 })
|
||||
).toBeVisible()
|
||||
|
||||
@@ -9,7 +9,7 @@ test.describe('Cloud nodes page @smoke', () => {
|
||||
|
||||
test('has correct title', async ({ page }) => {
|
||||
await expect(page).toHaveTitle(
|
||||
'Custom-node packs on Comfy Cloud — supported by default'
|
||||
'Custom-node packs on Comfy Cloud - supported by default'
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ test.describe('Cloud page @smoke', () => {
|
||||
})
|
||||
|
||||
test('has correct title', async ({ page }) => {
|
||||
await expect(page).toHaveTitle('Comfy Cloud — AI in the Cloud')
|
||||
await expect(page).toHaveTitle('Comfy Cloud - AI in the Cloud')
|
||||
})
|
||||
|
||||
test('HeroSection heading and subtitle are visible', async ({ page }) => {
|
||||
|
||||
@@ -16,7 +16,7 @@ test.describe('Download page @smoke', () => {
|
||||
|
||||
test('has correct title', async ({ page }) => {
|
||||
await expect(page).toHaveTitle(
|
||||
'Download Comfy Desktop — Run AI on Your Hardware'
|
||||
'Download Comfy Desktop - Run AI on Your Hardware'
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ test.describe('Homepage @smoke', () => {
|
||||
})
|
||||
|
||||
test('has correct title', async ({ page }) => {
|
||||
await expect(page).toHaveTitle('Comfy — Professional Control of Visual AI')
|
||||
await expect(page).toHaveTitle('Comfy - Professional Control of Visual AI')
|
||||
})
|
||||
|
||||
test('HeroSection heading is visible', async ({ page }) => {
|
||||
|
||||
@@ -13,7 +13,7 @@ test.describe('Learning page @smoke', () => {
|
||||
})
|
||||
|
||||
test('has correct title', async ({ page }) => {
|
||||
await expect(page).toHaveTitle('Learning — Comfy')
|
||||
await expect(page).toHaveTitle('Learning - Comfy')
|
||||
})
|
||||
|
||||
test('hero headline references ComfyUI', async ({ page }) => {
|
||||
@@ -137,7 +137,7 @@ test.describe('Learning page (zh-CN) @smoke', () => {
|
||||
test('renders localized title, headings, and tutorials', async ({ page }) => {
|
||||
await page.goto('/zh-CN/learning')
|
||||
|
||||
await expect(page).toHaveTitle('学习 — Comfy')
|
||||
await expect(page).toHaveTitle('学习 - Comfy')
|
||||
await expect(page.getByRole('heading', { level: 1 })).toContainText(
|
||||
/[一-鿿]/
|
||||
)
|
||||
|
||||
@@ -31,6 +31,26 @@ test.describe('Desktop navigation @smoke', () => {
|
||||
}
|
||||
})
|
||||
|
||||
test('NEW badge shows on Products and Community only', async ({ page }) => {
|
||||
const nav = page.getByRole('navigation', { name: 'Main navigation' })
|
||||
const desktopLinks = nav.getByTestId('desktop-nav-links')
|
||||
|
||||
for (const label of ['Products', 'Community']) {
|
||||
await expect(
|
||||
desktopLinks
|
||||
.getByRole('button', { name: label })
|
||||
.getByText('NEW', { exact: true })
|
||||
).toBeVisible()
|
||||
}
|
||||
|
||||
await expect(
|
||||
desktopLinks.getByRole('button', { name: 'Company' }).getByText('NEW')
|
||||
).toHaveCount(0)
|
||||
await expect(
|
||||
desktopLinks.getByRole('link', { name: 'Pricing' }).getByText('NEW')
|
||||
).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('CTA buttons are visible', async ({ page }) => {
|
||||
const nav = page.getByRole('navigation', { name: 'Main navigation' })
|
||||
const desktopCTA = nav.getByTestId('desktop-nav-cta')
|
||||
@@ -117,6 +137,27 @@ test.describe('Mobile menu @mobile', () => {
|
||||
}
|
||||
})
|
||||
|
||||
test('NEW badge shows on Products and Community only', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Toggle menu' }).click()
|
||||
|
||||
const menu = page.getByRole('dialog')
|
||||
|
||||
for (const label of ['Products', 'Community']) {
|
||||
await expect(
|
||||
menu.getByRole('button', { name: label }).getByText('NEW', {
|
||||
exact: true
|
||||
})
|
||||
).toBeVisible()
|
||||
}
|
||||
|
||||
await expect(
|
||||
menu.getByRole('button', { name: 'Company' }).getByText('NEW')
|
||||
).toHaveCount(0)
|
||||
await expect(
|
||||
menu.getByRole('link', { name: 'Pricing' }).getByText('NEW')
|
||||
).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('clicking section with subitems drills down and back works', async ({
|
||||
page
|
||||
}) => {
|
||||
|
||||
@@ -22,7 +22,7 @@ test.describe('Payment success page @smoke', () => {
|
||||
})
|
||||
|
||||
test('has correct title and is noindex', async ({ page }) => {
|
||||
await expect(page).toHaveTitle('Payment Successful — Comfy')
|
||||
await expect(page).toHaveTitle('Payment Successful - Comfy')
|
||||
await expectNoIndex(page)
|
||||
})
|
||||
|
||||
@@ -54,7 +54,7 @@ test.describe('Payment failed page @smoke', () => {
|
||||
})
|
||||
|
||||
test('has correct title and is noindex', async ({ page }) => {
|
||||
await expect(page).toHaveTitle('Payment Failed — Comfy')
|
||||
await expect(page).toHaveTitle('Payment Failed - Comfy')
|
||||
await expectNoIndex(page)
|
||||
})
|
||||
|
||||
@@ -84,7 +84,7 @@ test.describe('Payment failed page @smoke', () => {
|
||||
test.describe('Payment pages zh-CN @smoke', () => {
|
||||
test('zh-CN success page renders and links correctly', async ({ page }) => {
|
||||
await page.goto('/zh-CN/payment/success')
|
||||
await expect(page).toHaveTitle('支付成功 — Comfy')
|
||||
await expect(page).toHaveTitle('支付成功 - Comfy')
|
||||
await expectNoIndex(page)
|
||||
await expect(
|
||||
page.getByRole('heading', { name: '支付成功', level: 1 })
|
||||
@@ -99,7 +99,7 @@ test.describe('Payment pages zh-CN @smoke', () => {
|
||||
|
||||
test('zh-CN failed page renders and links correctly', async ({ page }) => {
|
||||
await page.goto('/zh-CN/payment/failed')
|
||||
await expect(page).toHaveTitle('支付失败 — Comfy')
|
||||
await expect(page).toHaveTitle('支付失败 - Comfy')
|
||||
await expectNoIndex(page)
|
||||
await expect(
|
||||
page.getByRole('heading', { name: '无法完成支付', level: 1 })
|
||||
|
||||
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 96 KiB |
@@ -16,6 +16,7 @@ import type { NavItem } from '../../../data/mainNavigation'
|
||||
import type { Locale } from '../../../i18n/translations'
|
||||
import NavColumn from './NavColumn.vue'
|
||||
import NavFeaturedCard from './NavFeaturedCard.vue'
|
||||
import NewBadge from './NewBadge.vue'
|
||||
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
const mainNavigation = getMainNavigation(locale)
|
||||
@@ -42,7 +43,10 @@ function isNavItemActive(navItem: NavItem, path: string): boolean {
|
||||
<NavigationMenuTrigger
|
||||
:active="isNavItemActive(navItem, currentPath)"
|
||||
>
|
||||
{{ navItem.label }}
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<span class="ppformula-text-center">{{ navItem.label }}</span>
|
||||
<NewBadge v-if="navItem.badge" :locale="locale" size="xxs" />
|
||||
</span>
|
||||
</NavigationMenuTrigger>
|
||||
<NavigationMenuContent class="w-auto" data-testid="nav-dropdown">
|
||||
<ul class="flex w-max gap-16">
|
||||
|
||||
@@ -8,6 +8,7 @@ import { lockScroll, unlockScroll } from '../../../composables/scrollLock'
|
||||
import type { Locale } from '../../../i18n/translations.ts'
|
||||
import { t } from '../../../i18n/translations.ts'
|
||||
import NavLinkContent from './NavLinkContent.vue'
|
||||
import NewBadge from './NewBadge.vue'
|
||||
import Sheet from '@/components/ui/sheet/Sheet.vue'
|
||||
import SheetContent from '@/components/ui/sheet/SheetContent.vue'
|
||||
import SheetDescription from '@/components/ui/sheet/SheetDescription.vue'
|
||||
@@ -96,7 +97,8 @@ onUnmounted(() => {
|
||||
:href="item.columns ? undefined : item.href"
|
||||
@click="item.columns && (activeSection = item.label)"
|
||||
>
|
||||
{{ item.label }}
|
||||
<span class="ppformula-text-center">{{ item.label }}</span>
|
||||
<NewBadge v-if="item.badge" :locale="locale" size="xxs" />
|
||||
<template #append>
|
||||
<ChevronRight class="size-7" />
|
||||
</template>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import Badge from '@/components/ui/badge/Badge.vue'
|
||||
import { ArrowUpRight } from '@lucide/vue'
|
||||
|
||||
import type { NavColumnItem } from '../../../data/mainNavigation'
|
||||
import type { Locale } from '../../../i18n/translations'
|
||||
import { t } from '../../../i18n/translations'
|
||||
import NewBadge from './NewBadge.vue'
|
||||
|
||||
defineProps<{ item: NavColumnItem; locale: Locale }>()
|
||||
</script>
|
||||
@@ -12,9 +11,7 @@ defineProps<{ item: NavColumnItem; locale: Locale }>()
|
||||
<template>
|
||||
<span class="flex items-center gap-2">
|
||||
<span class="ppformula-text-center">{{ item.label }}</span>
|
||||
<Badge v-if="item.badge" size="xs" variant="accent">
|
||||
{{ t('nav.badgeNew', locale) }}
|
||||
</Badge>
|
||||
<NewBadge v-if="item.badge" :locale="locale" size="xs" />
|
||||
<ArrowUpRight
|
||||
v-if="item.external"
|
||||
class="text-primary-comfy-yellow size-4"
|
||||
|
||||
15
apps/website/src/components/common/HeaderMain/NewBadge.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import Badge from '@/components/ui/badge/Badge.vue'
|
||||
|
||||
import type { BadgeVariants } from '@/components/ui/badge'
|
||||
import type { Locale } from '../../../i18n/translations'
|
||||
import { t } from '../../../i18n/translations'
|
||||
|
||||
defineProps<{ locale: Locale; size?: BadgeVariants['size'] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Badge :size="size" variant="accent">
|
||||
{{ t('nav.badgeNew', locale) }}
|
||||
</Badge>
|
||||
</template>
|
||||
@@ -6,14 +6,15 @@ export const badgeVariants = cva({
|
||||
variants: {
|
||||
size: {
|
||||
md: 'px-4 py-1 text-xs',
|
||||
xs: 'px-2 py-0.5 text-[9px]'
|
||||
xs: 'px-2 py-0.5 text-[9px]',
|
||||
xxs: 'px-1.5 py-px text-[8px]'
|
||||
},
|
||||
variant: {
|
||||
default: 'bg-transparency-ink-t80',
|
||||
subtle: 'bg-transparency-white-t4 text-primary-comfy-canvas',
|
||||
category: 'text-primary-comfy-yellow px-0 font-semibold uppercase',
|
||||
accent:
|
||||
'before:bg-primary-comfy-yellow relative isolate overflow-visible rounded-none bg-transparent px-2 py-0.5 text-[9px] font-bold tracking-wide text-primary-comfy-ink uppercase before:absolute before:inset-0 before:-z-10 before:-skew-x-12 before:rounded-sm'
|
||||
'before:bg-primary-comfy-yellow relative isolate overflow-visible rounded-none bg-transparent font-bold tracking-wide text-primary-comfy-ink uppercase before:absolute before:inset-0 before:-z-10 before:-skew-x-12 before:rounded-sm'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -30,15 +30,23 @@ export type NavItem =
|
||||
label: string
|
||||
columns: NavColumn[]
|
||||
featured?: NavFeatured
|
||||
badge?: 'new'
|
||||
href?: never
|
||||
}
|
||||
| { label: string; href: string; columns?: never; featured?: never }
|
||||
| {
|
||||
label: string
|
||||
href: string
|
||||
badge?: 'new'
|
||||
columns?: never
|
||||
featured?: never
|
||||
}
|
||||
|
||||
export function getMainNavigation(locale: Locale): NavItem[] {
|
||||
const routes = getRoutes(locale)
|
||||
return [
|
||||
{
|
||||
label: t('nav.products', locale),
|
||||
badge: 'new',
|
||||
featured: {
|
||||
imageSrc: 'https://media.comfy.org/website/nav/mcp-card.webp',
|
||||
imageAlt: t('nav.featuredProductsAlt', locale),
|
||||
@@ -95,6 +103,7 @@ export function getMainNavigation(locale: Locale): NavItem[] {
|
||||
{ label: t('nav.pricing', locale), href: routes.cloudPricing },
|
||||
{
|
||||
label: t('nav.community', locale),
|
||||
badge: 'new',
|
||||
featured: {
|
||||
imageSrc: 'https://media.comfy.org/website/nav/featured-demo-card.jpg',
|
||||
imageAlt: t('nav.featuredCommunityAlt', locale),
|
||||
|
||||
@@ -823,7 +823,7 @@ const translations = {
|
||||
'zh-CN': 'Comfy Cloud 支持的自定义节点包'
|
||||
},
|
||||
'cloudNodes.meta.title': {
|
||||
en: 'Custom-node packs on Comfy Cloud — supported by default',
|
||||
en: 'Custom-node packs on Comfy Cloud - supported by default',
|
||||
'zh-CN': 'Comfy Cloud 自定义节点包合集——开箱即用'
|
||||
},
|
||||
'cloudNodes.meta.description': {
|
||||
@@ -1845,8 +1845,8 @@ const translations = {
|
||||
|
||||
// MCP – Meta
|
||||
'mcp.meta.title': {
|
||||
en: 'Comfy MCP — Drive ComfyUI from any AI agent',
|
||||
'zh-CN': 'Comfy MCP — 让任何 AI 智能体驱动 ComfyUI'
|
||||
en: 'Comfy MCP - Drive ComfyUI from any AI agent',
|
||||
'zh-CN': 'Comfy MCP - 让任何 AI 智能体驱动 ComfyUI'
|
||||
},
|
||||
'mcp.meta.description': {
|
||||
en: 'Comfy MCP exposes the full ComfyUI engine over the Model Context Protocol. Generate images, video, audio, and 3D from Claude Code, Claude Desktop, and any MCP-compatible client.',
|
||||
@@ -3483,8 +3483,8 @@ const translations = {
|
||||
},
|
||||
|
||||
'affiliate-terms.page.title': {
|
||||
en: 'Affiliate Terms — Comfy',
|
||||
'zh-CN': 'Affiliate Terms — Comfy'
|
||||
en: 'Affiliate Terms - Comfy',
|
||||
'zh-CN': 'Affiliate Terms - Comfy'
|
||||
},
|
||||
'affiliate-terms.page.description': {
|
||||
en: 'Comfy.org Affiliate Program Terms and Conditions.',
|
||||
@@ -3896,8 +3896,8 @@ const translations = {
|
||||
'This document reproduces the current template of the Enterprise Customer Agreement for reference only. The executed Agreement between Comfy and Customer, together with any signed Order Forms, governs the relationship between the parties. To request an executable copy, please contact <a href="mailto:sales@comfy.org" class="text-white underline">sales@comfy.org</a>.'
|
||||
},
|
||||
'enterprise-msa.page.title': {
|
||||
en: 'Enterprise MSA — Comfy',
|
||||
'zh-CN': 'Enterprise MSA — Comfy'
|
||||
en: 'Enterprise MSA - Comfy',
|
||||
'zh-CN': 'Enterprise MSA - Comfy'
|
||||
},
|
||||
'enterprise-msa.page.description': {
|
||||
en: 'Comfy Enterprise Customer Agreement — the master services agreement that governs Comfy Enterprise deployments of Comfy Cloud, Comfy API, and related products.',
|
||||
@@ -4361,8 +4361,8 @@ const translations = {
|
||||
|
||||
// Affiliate page (/affiliates) — head metadata
|
||||
'affiliate.page.title': {
|
||||
en: 'Comfy.org Affiliate Program — Become a Partner',
|
||||
'zh-CN': 'Comfy.org 联盟计划 — 成为合作伙伴'
|
||||
en: 'Comfy.org Affiliate Program - Become a Partner',
|
||||
'zh-CN': 'Comfy.org 联盟计划 - 成为合作伙伴'
|
||||
},
|
||||
'affiliate.page.description': {
|
||||
en: 'Earn 30% recurring commission for 3 months on every Comfy Cloud subscription you refer. Apply to become a Comfy Partner.',
|
||||
@@ -4387,8 +4387,8 @@ const translations = {
|
||||
// Launches page (/launches) — head metadata
|
||||
// zh-CN strings pending native review (see apps/website/.scratch/drops-page/PRD.md)
|
||||
'launches.page.title': {
|
||||
en: 'ComfyUI Live Demo & Q&A — June 29 Launch Livestream',
|
||||
'zh-CN': 'ComfyUI 直播演示与问答 — 6 月 29 日发布直播'
|
||||
en: 'ComfyUI Live Demo & Q&A - June 29 Launch Livestream',
|
||||
'zh-CN': 'ComfyUI 直播演示与问答 - 6 月 29 日发布直播'
|
||||
},
|
||||
'launches.page.description': {
|
||||
en: 'Join the ComfyUI livestream on June 29 for a hands-on product demo and live Q&A. See what’s new across desktop, cloud, and community, and get your questions answered.',
|
||||
|
||||
@@ -3,7 +3,7 @@ import BaseLayout from '../layouts/BaseLayout.astro'
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="404 — Page Not Found — Comfy"
|
||||
title="404 - Page Not Found - Comfy"
|
||||
noindex>
|
||||
<div
|
||||
class="flex w-full flex-col items-center p-4 h-[calc(100dvh-12rem)]">
|
||||
|
||||
@@ -16,7 +16,7 @@ const { siteUrl, locale } = pageContext(
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="About Us — Comfy"
|
||||
title="About Us - Comfy"
|
||||
pageType="AboutPage"
|
||||
mainEntityId={organizationId(siteUrl)}
|
||||
breadcrumbs={[
|
||||
|
||||
@@ -42,7 +42,7 @@ const roles = itemListNode(
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Careers — Comfy"
|
||||
title="Careers - Comfy"
|
||||
description="Join the team building the operating system for generative AI. Open roles in engineering, design, marketing, and more."
|
||||
pageType="CollectionPage"
|
||||
mainEntityId={jsonLdId(url, 'itemlist')}
|
||||
|
||||
@@ -3,6 +3,6 @@ import BaseLayout from '../layouts/BaseLayout.astro'
|
||||
import ComingSoon from '../components/common/ComingSoon.astro'
|
||||
---
|
||||
|
||||
<BaseLayout title="Case Studies — Comfy">
|
||||
<BaseLayout title="Case Studies - Comfy">
|
||||
<ComingSoon />
|
||||
</BaseLayout>
|
||||
|
||||
@@ -11,7 +11,7 @@ import { t } from '../../i18n/translations'
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Comfy Cloud — AI in the Cloud"
|
||||
title="Comfy Cloud - AI in the Cloud"
|
||||
description={t('cloud.hero.subtitle', 'en')}
|
||||
keywords={['comfyui web app', 'comfyui app', 'comfyui online', 'comfyui cloud', 'comfy cloud', 'comfy ui application', 'comfyui browser', 'cloud comfyui', 'managed comfyui']}
|
||||
>
|
||||
|
||||
@@ -20,7 +20,7 @@ const productId = jsonLdId(url, 'product')
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Pricing — Comfy Cloud"
|
||||
title="Pricing - Comfy Cloud"
|
||||
mainEntityId={productId}
|
||||
breadcrumbs={[
|
||||
{ name: t('breadcrumb.home', locale), url: absoluteUrl(Astro.site, '/') },
|
||||
|
||||
@@ -13,7 +13,7 @@ const { siteUrl, locale } = pageContext(
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Contact — Comfy"
|
||||
title="Contact - Comfy"
|
||||
pageType="ContactPage"
|
||||
mainEntityId={organizationId(siteUrl)}
|
||||
breadcrumbs={[
|
||||
|
||||
@@ -11,7 +11,7 @@ import { loadStories } from '../utils/loadStories'
|
||||
const stories = (await loadStories('en')).map(toCardProps)
|
||||
---
|
||||
|
||||
<BaseLayout title="Customer Stories — Comfy">
|
||||
<BaseLayout title="Customer Stories - Comfy">
|
||||
<HeroSection client:load />
|
||||
<StorySection stories={stories} />
|
||||
<FeedbackSection client:load />
|
||||
|
||||
@@ -17,7 +17,7 @@ export async function getStaticPaths() {
|
||||
const { entry, next } = Astro.props
|
||||
---
|
||||
|
||||
<BaseLayout title={`${entry.data.title} — Comfy`}>
|
||||
<BaseLayout title={`${entry.data.title} - Comfy`}>
|
||||
<DetailHeroSection
|
||||
label={entry.data.category}
|
||||
title={entry.data.title}
|
||||
|
||||
@@ -58,7 +58,7 @@ const learningResource: JsonLdNode = {
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={`${title} — Comfy`}
|
||||
title={`${title} - Comfy`}
|
||||
description={description}
|
||||
ogImage={demo.ogImage}
|
||||
mainEntityId={learningId}
|
||||
|
||||
@@ -3,6 +3,6 @@ import BaseLayout from '../../layouts/BaseLayout.astro'
|
||||
import ComingSoon from '../../components/common/ComingSoon.astro'
|
||||
---
|
||||
|
||||
<BaseLayout title="Demos — Comfy" description="Interactive demos and tutorials for ComfyUI.">
|
||||
<BaseLayout title="Demos - Comfy" description="Interactive demos and tutorials for ComfyUI.">
|
||||
<ComingSoon />
|
||||
</BaseLayout>
|
||||
|
||||
@@ -23,7 +23,7 @@ const { siteUrl, locale } = pageContext(
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Download Comfy Desktop — Run AI on Your Hardware"
|
||||
title="Download Comfy Desktop - Run AI on Your Hardware"
|
||||
description={t('download.hero.subtitle', 'en')}
|
||||
mainEntityId={comfyUiSoftwareId(siteUrl)}
|
||||
breadcrumbs={[
|
||||
|
||||
@@ -5,7 +5,7 @@ import GallerySection from '../components/gallery/GallerySection.vue'
|
||||
import ContactSection from '../components/gallery/ContactSection.vue'
|
||||
---
|
||||
|
||||
<BaseLayout title="Gallery — Comfy">
|
||||
<BaseLayout title="Gallery - Comfy">
|
||||
<HeroSection />
|
||||
<GallerySection client:load />
|
||||
<ContactSection />
|
||||
|
||||
@@ -24,7 +24,7 @@ const { siteUrl } = pageContext(
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Comfy — Professional Control of Visual AI"
|
||||
title="Comfy - Professional Control of Visual AI"
|
||||
description={t("hero.subtitle", "en")}
|
||||
mainEntityId={comfyUiSoftwareId(siteUrl)}
|
||||
extraJsonLd={[
|
||||
|
||||
@@ -12,7 +12,7 @@ import { externalLinks } from '../config/routes'
|
||||
const routes = getRoutes('en')
|
||||
---
|
||||
|
||||
<BaseLayout title="Learning — Comfy">
|
||||
<BaseLayout title="Learning - Comfy">
|
||||
<HeroSection client:load />
|
||||
<FeaturedWorkflowSection client:visible />
|
||||
<TutorialsSection client:visible />
|
||||
|
||||
@@ -7,7 +7,7 @@ import ProductShowcaseSection from '../components/home/ProductShowcaseSection.vu
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Models — Comfy"
|
||||
title="Models - Comfy"
|
||||
description="Run the world's leading AI models in ComfyUI. Browse every supported model with community workflow templates ready to run."
|
||||
>
|
||||
<ModelsHeroSection
|
||||
|
||||
@@ -103,7 +103,7 @@ const faqPage: JsonLdNode = {
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={`${pageTitle} — Comfy`}
|
||||
title={`${pageTitle} - Comfy`}
|
||||
description={pageDescription}
|
||||
ogImage={model.thumbnailUrl}
|
||||
mainEntityId={softwareId}
|
||||
|
||||
@@ -43,7 +43,7 @@ const dirLabel: Record<string, string> = {
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={`${title} — Comfy`}
|
||||
title={`${title} - Comfy`}
|
||||
description={subtitle}
|
||||
pageType="CollectionPage"
|
||||
mainEntityId={jsonLdId(url, 'itemlist')}
|
||||
|
||||
@@ -4,7 +4,7 @@ import PaymentStatusSection from '../../components/payment/PaymentStatusSection.
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Payment Failed — Comfy"
|
||||
title="Payment Failed - Comfy"
|
||||
description="Your payment was not completed."
|
||||
noindex
|
||||
>
|
||||
|
||||
@@ -4,7 +4,7 @@ import PaymentStatusSection from '../../components/payment/PaymentStatusSection.
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Payment Successful — Comfy"
|
||||
title="Payment Successful - Comfy"
|
||||
description="Your payment was processed successfully."
|
||||
noindex
|
||||
>
|
||||
|
||||
@@ -5,7 +5,7 @@ import HeroSection from '../components/legal/HeroSection.vue'
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Privacy Policy — Comfy"
|
||||
title="Privacy Policy - Comfy"
|
||||
description="Comfy privacy policy. Learn how we collect, use, and protect your personal information."
|
||||
noindex
|
||||
>
|
||||
|
||||
@@ -5,7 +5,7 @@ import HeroSection from '../../components/legal/HeroSection.vue'
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Desktop Privacy Policy — Comfy"
|
||||
title="Desktop Privacy Policy - Comfy"
|
||||
description="Privacy policy for Comfy Desktop. Named processors, lawful basis under GDPR/UK GDPR, retention periods, and your rights."
|
||||
>
|
||||
<HeroSection title="Desktop Privacy Policy" />
|
||||
|
||||
@@ -6,7 +6,7 @@ import { t } from '../i18n/translations'
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Terms of Service — Comfy"
|
||||
title="Terms of Service - Comfy"
|
||||
description="Terms of Service governing use of the Comfy Products, including Comfy Cloud, Comfy API, and Comfy Enterprise."
|
||||
noindex
|
||||
>
|
||||
|
||||
@@ -3,6 +3,6 @@ import BaseLayout from '../layouts/BaseLayout.astro'
|
||||
import ComingSoon from '../components/common/ComingSoon.astro'
|
||||
---
|
||||
|
||||
<BaseLayout title="Videos — Comfy">
|
||||
<BaseLayout title="Videos - Comfy">
|
||||
<ComingSoon />
|
||||
</BaseLayout>
|
||||
|
||||
@@ -16,7 +16,7 @@ const { siteUrl, locale } = pageContext(
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="关于我们 — Comfy"
|
||||
title="关于我们 - Comfy"
|
||||
description="了解 ComfyUI 背后的团队和使命——开源的生成式 AI 平台。"
|
||||
pageType="AboutPage"
|
||||
mainEntityId={organizationId(siteUrl)}
|
||||
|
||||
@@ -42,7 +42,7 @@ const roles = itemListNode(
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="招聘 — Comfy"
|
||||
title="招聘 - Comfy"
|
||||
description="加入构建生成式 AI 操作系统的团队。工程、设计、市场营销等岗位开放招聘中。"
|
||||
pageType="CollectionPage"
|
||||
mainEntityId={jsonLdId(url, 'itemlist')}
|
||||
|
||||
@@ -3,6 +3,6 @@ import BaseLayout from '../../layouts/BaseLayout.astro'
|
||||
import ComingSoon from '../../components/common/ComingSoon.astro'
|
||||
---
|
||||
|
||||
<BaseLayout title="案例研究 — Comfy">
|
||||
<BaseLayout title="案例研究 - Comfy">
|
||||
<ComingSoon />
|
||||
</BaseLayout>
|
||||
|
||||
@@ -11,7 +11,7 @@ import { t } from '../../../i18n/translations'
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Comfy Cloud — 云端 AI"
|
||||
title="Comfy Cloud - 云端 AI"
|
||||
description={t('cloud.hero.subtitle', 'zh-CN')}
|
||||
keywords={['comfyui web app', 'comfyui app', 'comfyui online', 'comfyui cloud', 'ComfyUI 网页版', 'ComfyUI 云端', 'ComfyUI 应用', 'Comfy Cloud', '云端 ComfyUI']}
|
||||
>
|
||||
|
||||
@@ -20,7 +20,7 @@ const productId = jsonLdId(url, 'product')
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="定价 — Comfy Cloud"
|
||||
title="定价 - Comfy Cloud"
|
||||
mainEntityId={productId}
|
||||
breadcrumbs={[
|
||||
{
|
||||
|
||||
@@ -13,7 +13,7 @@ const { siteUrl, locale } = pageContext(
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="联系我们 — Comfy"
|
||||
title="联系我们 - Comfy"
|
||||
pageType="ContactPage"
|
||||
mainEntityId={organizationId(siteUrl)}
|
||||
breadcrumbs={[
|
||||
|
||||
@@ -11,7 +11,7 @@ import { loadStories } from '../../utils/loadStories'
|
||||
const stories = (await loadStories('zh-CN')).map(toCardProps)
|
||||
---
|
||||
|
||||
<BaseLayout title="客户故事 — Comfy">
|
||||
<BaseLayout title="客户故事 - Comfy">
|
||||
<HeroSection locale="zh-CN" client:load />
|
||||
<StorySection stories={stories} locale="zh-CN" />
|
||||
<FeedbackSection locale="zh-CN" client:load />
|
||||
|
||||
@@ -17,7 +17,7 @@ export async function getStaticPaths() {
|
||||
const { entry, next } = Astro.props
|
||||
---
|
||||
|
||||
<BaseLayout title={`${entry.data.title} — Comfy`}>
|
||||
<BaseLayout title={`${entry.data.title} - Comfy`}>
|
||||
<DetailHeroSection
|
||||
label={entry.data.category}
|
||||
title={entry.data.title}
|
||||
|
||||
@@ -58,7 +58,7 @@ const learningResource: JsonLdNode = {
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={`${title} — Comfy`}
|
||||
title={`${title} - Comfy`}
|
||||
description={description}
|
||||
ogImage={demo.ogImage}
|
||||
mainEntityId={learningId}
|
||||
|
||||
@@ -3,7 +3,7 @@ import BaseLayout from '../../../layouts/BaseLayout.astro'
|
||||
import { t } from '../../../i18n/translations'
|
||||
---
|
||||
|
||||
<BaseLayout title="演示 — Comfy" description="ComfyUI 的互动演示和教程。">
|
||||
<BaseLayout title="演示 - Comfy" description="ComfyUI 的互动演示和教程。">
|
||||
<section class="flex min-h-[60vh] items-center justify-center px-6">
|
||||
<div class="text-center">
|
||||
<h1 class="text-primary-comfy-canvas text-4xl font-light">
|
||||
|
||||
@@ -23,7 +23,7 @@ const { siteUrl, locale } = pageContext(
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="下载 Comfy 桌面版 — 在您的硬件上运行 AI"
|
||||
title="下载 Comfy 桌面版 - 在您的硬件上运行 AI"
|
||||
description={t('download.hero.subtitle', 'zh-CN')}
|
||||
mainEntityId={comfyUiSoftwareId(siteUrl)}
|
||||
breadcrumbs={[
|
||||
|
||||
@@ -5,7 +5,7 @@ import GallerySection from '../../components/gallery/GallerySection.vue'
|
||||
import ContactSection from '../../components/gallery/ContactSection.vue'
|
||||
---
|
||||
|
||||
<BaseLayout title="作品集 — Comfy">
|
||||
<BaseLayout title="作品集 - Comfy">
|
||||
<HeroSection locale="zh-CN" />
|
||||
<GallerySection locale="zh-CN" client:load />
|
||||
<ContactSection locale="zh-CN" />
|
||||
|
||||
@@ -24,7 +24,7 @@ const { siteUrl } = pageContext(
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Comfy — 视觉 AI 的最强可控性"
|
||||
title="Comfy - 视觉 AI 的最强可控性"
|
||||
description={t('hero.subtitle', 'zh-CN')}
|
||||
mainEntityId={comfyUiSoftwareId(siteUrl)}
|
||||
extraJsonLd={[comfyUiApplicationNode(siteUrl), comfyUiSourceCodeNode(siteUrl)]}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { learningEvents } from '../../data/events'
|
||||
const routes = getRoutes('zh-CN')
|
||||
---
|
||||
|
||||
<BaseLayout title="学习 — Comfy">
|
||||
<BaseLayout title="学习 - Comfy">
|
||||
<HeroSection locale="zh-CN" client:load />
|
||||
<FeaturedWorkflowSection locale="zh-CN" client:visible />
|
||||
<TutorialsSection locale="zh-CN" client:visible />
|
||||
|
||||
@@ -7,7 +7,7 @@ import ProductShowcaseSection from '../../components/home/ProductShowcaseSection
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="模型 — Comfy"
|
||||
title="模型 - Comfy"
|
||||
description="在 ComfyUI 中运行世界领先的 AI 模型。浏览所有支持的模型及社区工作流模板。"
|
||||
>
|
||||
<ModelsHeroSection
|
||||
|
||||
@@ -3,6 +3,6 @@ import BaseLayout from '../../../layouts/BaseLayout.astro'
|
||||
import PaymentStatusSection from '../../../components/payment/PaymentStatusSection.vue'
|
||||
---
|
||||
|
||||
<BaseLayout title="支付失败 — Comfy" description="您的支付未能完成。" noindex>
|
||||
<BaseLayout title="支付失败 - Comfy" description="您的支付未能完成。" noindex>
|
||||
<PaymentStatusSection status="failed" locale="zh-CN" />
|
||||
</BaseLayout>
|
||||
|
||||
@@ -3,6 +3,6 @@ import BaseLayout from '../../../layouts/BaseLayout.astro'
|
||||
import PaymentStatusSection from '../../../components/payment/PaymentStatusSection.vue'
|
||||
---
|
||||
|
||||
<BaseLayout title="支付成功 — Comfy" description="您的支付已成功完成。" noindex>
|
||||
<BaseLayout title="支付成功 - Comfy" description="您的支付已成功完成。" noindex>
|
||||
<PaymentStatusSection status="success" locale="zh-CN" />
|
||||
</BaseLayout>
|
||||
|
||||
@@ -5,7 +5,7 @@ import HeroSection from '../../components/legal/HeroSection.vue'
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="隐私政策 — Comfy"
|
||||
title="隐私政策 - Comfy"
|
||||
description="Comfy 隐私政策。了解我们如何收集、使用和保护您的个人信息。"
|
||||
noindex
|
||||
>
|
||||
|
||||
@@ -5,7 +5,7 @@ import HeroSection from '../../../components/legal/HeroSection.vue'
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Desktop 隐私政策 — Comfy"
|
||||
title="Desktop 隐私政策 - Comfy"
|
||||
description="Comfy Desktop 隐私政策。命名的数据处理方、GDPR/UK GDPR 下的合法依据、保留期限和您的权利。"
|
||||
>
|
||||
<HeroSection title="Desktop 隐私政策" />
|
||||
|
||||
@@ -3,6 +3,6 @@ import BaseLayout from '../../layouts/BaseLayout.astro'
|
||||
import ComingSoon from '../../components/common/ComingSoon.astro'
|
||||
---
|
||||
|
||||
<BaseLayout title="视频 — Comfy">
|
||||
<BaseLayout title="视频 - Comfy">
|
||||
<ComingSoon />
|
||||
</BaseLayout>
|
||||
|
||||
@@ -61,7 +61,12 @@ const { drop, locale } = defineProps<{
|
||||
class="size-full object-cover object-center transition-transform duration-500 ease-out group-hover/pill-trigger:scale-105"
|
||||
/>
|
||||
</div>
|
||||
<Badge v-if="drop.badge" variant="accent" class="absolute top-6 left-8">
|
||||
<Badge
|
||||
v-if="drop.badge"
|
||||
size="xs"
|
||||
variant="accent"
|
||||
class="absolute top-6 left-8"
|
||||
>
|
||||
{{ drop.badge[locale] }}
|
||||
</Badge>
|
||||
</CardContent>
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
ModelLibrarySidebarTab,
|
||||
NodeLibrarySidebarTab,
|
||||
NodeLibrarySidebarTabV2,
|
||||
SidebarTab,
|
||||
WorkflowsSidebarTab
|
||||
} from '@e2e/fixtures/components/SidebarTab'
|
||||
import { Topbar } from '@e2e/fixtures/components/Topbar'
|
||||
@@ -70,6 +71,7 @@ class ComfyPropertiesPanel {
|
||||
}
|
||||
|
||||
class ComfyMenu {
|
||||
private _appsTab: SidebarTab | null = null
|
||||
private _assetsTab: AssetsSidebarTab | null = null
|
||||
private _modelLibraryTab: ModelLibrarySidebarTab | null = null
|
||||
private _nodeLibraryTab: NodeLibrarySidebarTab | null = null
|
||||
@@ -104,6 +106,11 @@ class ComfyMenu {
|
||||
return this._nodeLibraryTabV2
|
||||
}
|
||||
|
||||
get appsTab() {
|
||||
this._appsTab ??= new SidebarTab(this.page, 'apps')
|
||||
return this._appsTab
|
||||
}
|
||||
|
||||
get assetsTab() {
|
||||
this._assetsTab ??= new AssetsSidebarTab(this.page)
|
||||
return this._assetsTab
|
||||
|
||||
@@ -4,7 +4,7 @@ import { expect } from '@playwright/test'
|
||||
import type { WorkspaceStore } from '@e2e/types/globals'
|
||||
import { TestIds } from '@e2e/fixtures/selectors'
|
||||
|
||||
class SidebarTab {
|
||||
export class SidebarTab {
|
||||
public readonly tabButton: Locator
|
||||
public readonly selectedTabButton: Locator
|
||||
|
||||
|
||||
40
browser_tests/fixtures/components/WorkflowActionsDropdown.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
import { TestIds } from '@e2e/fixtures/selectors'
|
||||
|
||||
/**
|
||||
* The graph/app view-mode toggle and its workflow actions dropdown.
|
||||
* A separate instance mounts in each host - the subgraph breadcrumb (graph
|
||||
* mode) and the app-mode center panel - and unmounts as the mode flips.
|
||||
*/
|
||||
export class WorkflowActionsDropdown {
|
||||
/** The segmented graph/app toggle hosting the workflow actions trigger. */
|
||||
public readonly viewModeToggle: Locator
|
||||
/** The active segment; opens the workflow actions menu. */
|
||||
public readonly trigger: Locator
|
||||
/** The inactive segment that switches into app mode. */
|
||||
public readonly enterAppModeSegment: Locator
|
||||
/** The inactive segment that switches back to the node graph. */
|
||||
public readonly enterGraphSegment: Locator
|
||||
/** The workflow actions dropdown menu. */
|
||||
public readonly menu: Locator
|
||||
|
||||
constructor(page: Page) {
|
||||
this.viewModeToggle = page.getByTestId(
|
||||
TestIds.workflowActions.viewModeToggle
|
||||
)
|
||||
this.trigger = this.triggerIn(this.viewModeToggle)
|
||||
this.enterAppModeSegment = this.viewModeToggle.getByRole('button', {
|
||||
name: 'Enter app mode'
|
||||
})
|
||||
this.enterGraphSegment = this.viewModeToggle.getByRole('button', {
|
||||
name: 'Enter node graph'
|
||||
})
|
||||
this.menu = page.getByRole('menu', { name: 'Workflow actions' })
|
||||
}
|
||||
|
||||
/** The trigger as rendered inside a specific mode's host. */
|
||||
triggerIn(host: Locator): Locator {
|
||||
return host.getByRole('button', { name: 'Workflow actions' })
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
||||
import { TestIds } from '@e2e/fixtures/selectors'
|
||||
|
||||
import { OutputHistoryComponent } from '@e2e/fixtures/components/OutputHistory'
|
||||
import { WorkflowActionsDropdown } from '@e2e/fixtures/components/WorkflowActionsDropdown'
|
||||
import { AppModeWidgetHelper } from '@e2e/fixtures/helpers/AppModeWidgetHelper'
|
||||
import { BuilderFooterHelper } from '@e2e/fixtures/helpers/BuilderFooterHelper'
|
||||
import { BuilderSaveAsHelper } from '@e2e/fixtures/helpers/BuilderSaveAsHelper'
|
||||
@@ -19,6 +20,7 @@ export class AppModeHelper {
|
||||
readonly outputHistory: OutputHistoryComponent
|
||||
readonly steps: BuilderStepsHelper
|
||||
readonly widgets: AppModeWidgetHelper
|
||||
readonly workflowActions: WorkflowActionsDropdown
|
||||
|
||||
/** The "Connect an output" popover shown when saving without outputs. */
|
||||
public readonly connectOutputPopover: Locator
|
||||
@@ -77,6 +79,7 @@ export class AppModeHelper {
|
||||
this.outputHistory = new OutputHistoryComponent(comfyPage.page)
|
||||
this.steps = new BuilderStepsHelper(comfyPage)
|
||||
this.widgets = new AppModeWidgetHelper(comfyPage)
|
||||
this.workflowActions = new WorkflowActionsDropdown(comfyPage.page)
|
||||
|
||||
this.connectOutputPopover = this.page.getByTestId(
|
||||
TestIds.builder.connectOutputPopover
|
||||
@@ -185,10 +188,7 @@ export class AppModeHelper {
|
||||
.waitFor({ state: 'hidden', timeout: 5000 })
|
||||
.catch(() => {})
|
||||
|
||||
await this.page
|
||||
.getByRole('button', { name: 'Workflow actions' })
|
||||
.first()
|
||||
.click()
|
||||
await this.workflowActions.trigger.click()
|
||||
await this.page
|
||||
.getByRole('menuitem', { name: /Build app|Edit app/ })
|
||||
.click()
|
||||
|
||||
@@ -239,6 +239,9 @@ export const TestIds = {
|
||||
renameInput: 'subgraph-breadcrumb-rename-input',
|
||||
menu: (key: string) => `subgraph-breadcrumb-menu-${key}`
|
||||
},
|
||||
workflowActions: {
|
||||
viewModeToggle: 'view-mode-toggle'
|
||||
},
|
||||
templates: {
|
||||
content: 'template-workflows-content',
|
||||
workflowCard: (id: string) => `template-workflow-${id}`
|
||||
@@ -276,6 +279,7 @@ export const TestIds = {
|
||||
overlay: 'loading-overlay'
|
||||
},
|
||||
load3d: {
|
||||
categoryMenu: 'load3d-category-menu',
|
||||
recordingDuration: 'load3d-recording-duration'
|
||||
},
|
||||
load3dViewer: {
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { mergeTests } from '@playwright/test'
|
||||
|
||||
import {
|
||||
comfyPageFixture as test,
|
||||
comfyExpect as expect
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture
|
||||
} from '@e2e/fixtures/ComfyPage'
|
||||
import { subgraphBreadcrumbFixture } from '@e2e/fixtures/helpers/SubgraphBreadcrumbHelper'
|
||||
import { TestIds } from '@e2e/fixtures/selectors'
|
||||
|
||||
const test = mergeTests(comfyPageFixture, subgraphBreadcrumbFixture)
|
||||
|
||||
test.describe('App mode usage', () => {
|
||||
test('Drag and Drop @vue-nodes', async ({ comfyPage, comfyFiles }) => {
|
||||
const { centerPanel } = comfyPage.appMode
|
||||
@@ -137,6 +142,117 @@ test.describe('App mode usage', () => {
|
||||
await expect.poll(() => fileComboWidget.getValue()).toBe(targetImage)
|
||||
})
|
||||
|
||||
test('Shows a single side toolbar per mode, filtered to assets + apps in app mode', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const { sideToolbar, nodeLibraryTab, assetsTab, appsTab } = comfyPage.menu
|
||||
|
||||
await test.step('Graph mode shows the full toolbar', async () => {
|
||||
await expect(sideToolbar).toHaveCount(1)
|
||||
await expect(nodeLibraryTab.tabButton).toBeVisible()
|
||||
})
|
||||
|
||||
await test.step('App mode shows only assets + apps', async () => {
|
||||
await comfyPage.appMode.enterAppModeWithInputs([['3', 'seed']])
|
||||
await expect(comfyPage.appMode.centerPanel).toBeVisible()
|
||||
|
||||
await expect(sideToolbar).toHaveCount(1)
|
||||
await expect(assetsTab.tabButton).toBeVisible()
|
||||
await expect(appsTab.tabButton).toBeVisible()
|
||||
await expect(nodeLibraryTab.tabButton).toBeHidden()
|
||||
})
|
||||
})
|
||||
|
||||
test('Workflow actions menu keeps the same position across graph/app mode', async ({
|
||||
comfyPage,
|
||||
subgraphBreadcrumb
|
||||
}) => {
|
||||
const { workflowActions, centerPanel } = comfyPage.appMode
|
||||
|
||||
// Toggling graph<->app mode happens from this control, so it must not move
|
||||
// out from under the cursor as the mode flips.
|
||||
const graphActions = workflowActions.triggerIn(
|
||||
subgraphBreadcrumb.panel.root
|
||||
)
|
||||
await expect(graphActions).toBeVisible()
|
||||
const graphBox = await graphActions.boundingBox()
|
||||
|
||||
expect(graphBox).not.toBeNull()
|
||||
|
||||
await comfyPage.appMode.enterAppModeWithInputs([['3', 'seed']])
|
||||
await expect(centerPanel).toBeVisible()
|
||||
|
||||
const appActions = workflowActions.triggerIn(centerPanel)
|
||||
await expect(appActions).toBeVisible()
|
||||
|
||||
// The toggle segments reorder (morph) as the mode flips, so poll until the
|
||||
// active control settles at the same x it occupied in graph mode.
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const box = await appActions.boundingBox()
|
||||
return box ? Math.abs(box.x - graphBox!.x) : Infinity
|
||||
})
|
||||
.toBeLessThanOrEqual(1)
|
||||
})
|
||||
|
||||
test('Toggle segment flips mode without opening the menu', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const { workflowActions } = comfyPage.appMode
|
||||
await expect(workflowActions.viewModeToggle).toBeVisible()
|
||||
|
||||
await workflowActions.enterAppModeSegment.click()
|
||||
|
||||
await expect(comfyPage.appMode.centerPanel).toBeVisible()
|
||||
// The inactive segment switches mode; it must not also open the actions menu.
|
||||
await expect(workflowActions.menu).toBeHidden()
|
||||
await expect(workflowActions.viewModeToggle).toBeVisible()
|
||||
})
|
||||
|
||||
test('Toggle segment flips mode via keyboard without opening the menu', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const { workflowActions } = comfyPage.appMode
|
||||
await workflowActions.enterAppModeSegment.focus()
|
||||
await workflowActions.enterAppModeSegment.press('Enter')
|
||||
|
||||
await expect(comfyPage.appMode.centerPanel).toBeVisible()
|
||||
await expect(workflowActions.menu).toBeHidden()
|
||||
await expect(workflowActions.trigger).toBeFocused()
|
||||
})
|
||||
|
||||
test('Mode toggle re-appears after exiting the builder to graph mode', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const toggle = comfyPage.appMode.workflowActions.viewModeToggle
|
||||
await comfyPage.appMode.enableLinearMode()
|
||||
await expect(toggle).toBeVisible()
|
||||
|
||||
await comfyPage.appMode.enterBuilder()
|
||||
await expect(toggle).toBeHidden()
|
||||
await expect(comfyPage.appMode.centerPanel).toBeHidden()
|
||||
|
||||
await comfyPage.appMode.footer.exitButton.click()
|
||||
// Exiting the builder lands in graph mode: the app-mode-only center panel
|
||||
// stays hidden while the graph-mode toggle host re-mounts and the toggle
|
||||
// re-appears.
|
||||
await expect(toggle).toBeVisible()
|
||||
await expect(comfyPage.appMode.centerPanel).toBeHidden()
|
||||
})
|
||||
|
||||
test('Mode toggle survives a sidebar tab remounting the app panel', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const toggle = comfyPage.appMode.workflowActions.viewModeToggle
|
||||
await comfyPage.appMode.enterAppModeWithInputs([['3', 'seed']])
|
||||
await expect(comfyPage.appMode.centerPanel).toBeVisible()
|
||||
await expect(toggle).toBeVisible()
|
||||
|
||||
// Opening a sidebar tab remounts the app panel; the toggle re-renders with it.
|
||||
await comfyPage.menu.assetsTab.tabButton.click()
|
||||
await expect(toggle).toBeVisible()
|
||||
})
|
||||
|
||||
test.describe('Mobile', { tag: ['@mobile'] }, () => {
|
||||
test('panel navigation', async ({ comfyPage }) => {
|
||||
const { mobile } = comfyPage.appMode
|
||||
|
||||
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
@@ -11,39 +11,47 @@ export class Load3DHelper {
|
||||
}
|
||||
|
||||
get menuButton(): Locator {
|
||||
return this.node.getByRole('button', { name: /show menu/i })
|
||||
return this.node.getByTestId(TestIds.load3d.categoryMenu)
|
||||
}
|
||||
|
||||
private get menuPanel(): Locator {
|
||||
return this.node.page().getByRole('dialog')
|
||||
}
|
||||
|
||||
get recordingButton(): Locator {
|
||||
return this.node.getByRole('button', { name: /start recording/i })
|
||||
return this.node.getByRole('button', { name: 'Record', exact: true })
|
||||
}
|
||||
|
||||
get stopRecordingButton(): Locator {
|
||||
return this.node.getByRole('button', { name: /stop recording/i })
|
||||
}
|
||||
|
||||
get exportRecordingButton(): Locator {
|
||||
return this.node.getByRole('button', { name: /export recording/i })
|
||||
}
|
||||
|
||||
get clearRecordingButton(): Locator {
|
||||
return this.node.getByRole('button', { name: /clear recording/i })
|
||||
}
|
||||
|
||||
get recordingDuration(): Locator {
|
||||
get recordingMenuButton(): Locator {
|
||||
return this.node.getByTestId(TestIds.load3d.recordingDuration)
|
||||
}
|
||||
|
||||
get downloadRecordingMenuItem(): Locator {
|
||||
return this.menuPanel.getByRole('button', { name: 'Download Recording' })
|
||||
}
|
||||
|
||||
get startNewRecordingMenuItem(): Locator {
|
||||
return this.menuPanel.getByRole('button', { name: 'Start New Recording' })
|
||||
}
|
||||
|
||||
get deleteRecordingMenuItem(): Locator {
|
||||
return this.menuPanel.getByRole('button', { name: 'Delete Recording' })
|
||||
}
|
||||
|
||||
get gridToggleButton(): Locator {
|
||||
return this.node.getByRole('button', { name: /show grid/i })
|
||||
}
|
||||
|
||||
get uploadBackgroundImageButton(): Locator {
|
||||
return this.node.getByRole('button', { name: /upload background image/i })
|
||||
return this.node.getByRole('button', { name: 'BG Image' })
|
||||
}
|
||||
|
||||
get removeBackgroundImageButton(): Locator {
|
||||
return this.node.getByRole('button', { name: /remove background image/i })
|
||||
return this.node.getByRole('button', { name: 'Remove BG' })
|
||||
}
|
||||
|
||||
get panoramaModeButton(): Locator {
|
||||
@@ -54,6 +62,10 @@ export class Load3DHelper {
|
||||
return this.node.locator('input[type="color"]')
|
||||
}
|
||||
|
||||
get exportButton(): Locator {
|
||||
return this.node.getByRole('button', { name: 'Export', exact: true })
|
||||
}
|
||||
|
||||
get openViewerButton(): Locator {
|
||||
return this.node.getByRole('button', { name: /open in 3d viewer/i })
|
||||
}
|
||||
@@ -63,11 +75,15 @@ export class Load3DHelper {
|
||||
}
|
||||
|
||||
getMenuCategory(name: string): Locator {
|
||||
return this.node.getByText(name, { exact: true })
|
||||
return this.menuPanel.getByRole('button', { name, exact: true })
|
||||
}
|
||||
|
||||
get gizmoToggleButton(): Locator {
|
||||
return this.node.getByRole('button', { name: 'Gizmo' })
|
||||
// The category chip is also named "Gizmo" once that category is active;
|
||||
// only the toggle carries aria-pressed.
|
||||
return this.node
|
||||
.getByRole('button', { name: 'Gizmo' })
|
||||
.and(this.node.locator('[aria-pressed]'))
|
||||
}
|
||||
|
||||
get gizmoTranslateButton(): Locator {
|
||||
@@ -83,13 +99,17 @@ export class Load3DHelper {
|
||||
}
|
||||
|
||||
get gizmoResetButton(): Locator {
|
||||
return this.node.getByRole('button', { name: 'Reset Transform' })
|
||||
return this.node.getByRole('button', { name: 'Reset', exact: true })
|
||||
}
|
||||
|
||||
async openMenu(): Promise<void> {
|
||||
await this.menuButton.click()
|
||||
}
|
||||
|
||||
async openRecordingMenu(): Promise<void> {
|
||||
await this.recordingMenuButton.click()
|
||||
}
|
||||
|
||||
async openGizmoCategory(): Promise<void> {
|
||||
await this.openMenu()
|
||||
await this.getMenuCategory('Gizmo').click()
|
||||
|
||||
@@ -47,10 +47,12 @@ test.describe('Load3D', () => {
|
||||
await load3d.openMenu()
|
||||
|
||||
await expect(load3d.getMenuCategory('Scene')).toBeVisible()
|
||||
await expect(load3d.getMenuCategory('Model')).toBeVisible()
|
||||
await expect(load3d.getMenuCategory('3D Model')).toBeVisible()
|
||||
await expect(load3d.getMenuCategory('Camera')).toBeVisible()
|
||||
await expect(load3d.getMenuCategory('Light')).toBeVisible()
|
||||
await expect(load3d.getMenuCategory('Export')).toBeVisible()
|
||||
await expect(load3d.getMenuCategory('HDRI')).toBeVisible()
|
||||
await expect(load3d.getMenuCategory('Gizmo')).toBeVisible()
|
||||
await expect(load3d.exportButton).toBeVisible()
|
||||
|
||||
await expect(load3d.node).toHaveScreenshot(
|
||||
'load3d-controls-menu-open.png',
|
||||
@@ -253,7 +255,7 @@ test.describe('Load3D', () => {
|
||||
}
|
||||
)
|
||||
|
||||
test('Recording controls show stop/export/clear buttons after a recording', async ({
|
||||
test('Recording controls collapse into a duration chip with a menu after a recording', async ({
|
||||
comfyPage,
|
||||
load3d
|
||||
}) => {
|
||||
@@ -265,20 +267,25 @@ test.describe('Load3D', () => {
|
||||
await expect(load3d.stopRecordingButton).toBeVisible()
|
||||
})
|
||||
|
||||
await test.step('Stop recording surfaces export/clear controls and a 1s duration', async () => {
|
||||
await test.step('Stop recording surfaces the duration chip with a 1s duration', async () => {
|
||||
// Record for 1s wall-clock so the duration display settles on 00:01.
|
||||
await comfyPage.delay(1000)
|
||||
await load3d.stopRecordingButton.click()
|
||||
await expect(load3d.recordingButton).toBeVisible()
|
||||
await expect(load3d.exportRecordingButton).toBeVisible()
|
||||
await expect(load3d.clearRecordingButton).toBeVisible()
|
||||
await expect(load3d.recordingDuration).toHaveText('00:01')
|
||||
await expect(load3d.recordingMenuButton).toBeVisible()
|
||||
await expect(load3d.recordingMenuButton).toHaveText('00:01')
|
||||
})
|
||||
|
||||
await test.step('Clear recording removes export and clear controls', async () => {
|
||||
await load3d.clearRecordingButton.click()
|
||||
await expect(load3d.exportRecordingButton).toHaveCount(0)
|
||||
await expect(load3d.clearRecordingButton).toHaveCount(0)
|
||||
await test.step('Chip menu offers download, re-record and delete actions', async () => {
|
||||
await load3d.openRecordingMenu()
|
||||
await expect(load3d.downloadRecordingMenuItem).toBeVisible()
|
||||
await expect(load3d.startNewRecordingMenuItem).toBeVisible()
|
||||
await expect(load3d.deleteRecordingMenuItem).toBeVisible()
|
||||
})
|
||||
|
||||
await test.step('Deleting the recording restores the record button', async () => {
|
||||
await load3d.deleteRecordingMenuItem.click()
|
||||
await expect(load3d.recordingMenuButton).toHaveCount(0)
|
||||
await expect(load3d.recordingButton).toBeVisible()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 40 KiB |
@@ -46,7 +46,10 @@ test.describe('Mask Editor', { tag: '@vue-nodes' }, () => {
|
||||
{ tag: ['@smoke', '@screenshot'] },
|
||||
async ({ comfyPage, maskEditor }) => {
|
||||
const { nodeId } = await maskEditor.loadImageOnNode()
|
||||
await comfyPage.canvasOps.pan({ x: 0, y: 40 }, { x: 300, y: 300 })
|
||||
// Center the node so its header clears the view-mode toggle floating
|
||||
// at the top-left of the canvas.
|
||||
const nodeRef = await comfyPage.nodeOps.getNodeRefById(nodeId)
|
||||
await nodeRef.centerOnNode()
|
||||
|
||||
const nodeHeader = comfyPage.vueNodes
|
||||
.getNodeLocator(nodeId)
|
||||
|
||||
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |