mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-12 08:50:17 +00:00
## Summary Follow-up to the closed earlier attempt in #11646. This PR keeps the same user-facing goal, but changes the implementation to reuse the existing missing model pipeline for refresh instead of maintaining a separate candidate-only recheck path. Adds a missing model refresh action in the Errors tab by reusing the existing missing model pipeline, so users can re-check models after downloading or manually placing files without reloading the workflow. ## Changes - **What**: - Adds `app.refreshMissingModels()` as a reusable refresh entry point for the current root graph. - Splits node definition reloading into `app.reloadNodeDefs()` so missing-model refresh can pull fresh `object_info` without showing the generic combo refresh success flow. - Reuses the existing missing model pipeline instead of adding a separate candidate-only checker. The refresh path serializes the current graph, reuses active workflow model metadata when available, falls back to current missing-model metadata, and then reruns the same candidate discovery/enrichment/surfacing flow used during workflow load. - Adds missing model refresh state and error handling to `missingModelStore`. - Adds a Refresh button next to Download all in the missing model card action bar. - Moves Download all from the Errors tab header into the missing model card, so the Download all and Refresh actions render or hide together. - Changes Download all visibility from “more than one downloadable model” to “at least one downloadable model.” - Keeps the action bar hidden when there are no downloadable missing models; Cloud still does not render this action area. - Normalizes active workflow `pendingWarnings` updates so resolved missing model warnings do not get revived by stale empty warning objects. - Adds test IDs and coverage for the new action bar, refresh state, refresh delegation, pending warning sync, and E2E refresh behavior. - **Breaking**: None. - **Dependencies**: None. ## Review Focus The main design choice is intentionally reusing the missing model pipeline for refresh instead of implementing a smaller candidate-only recheck. The earlier candidate-only approach was cheaper, but it created a separate source of truth for missing-model resolution and made edge cases harder to reason about. In particular, it could diverge from the behavior used when a workflow is loaded, and it did not naturally handle the case where a model becomes missing after the workflow is already open. This version pays the cost of refreshing node definitions and rerunning the missing-model scan for the current graph, but keeps the refresh behavior aligned with workflow load semantics. Expected behavior by environment: - OSS browser: - The action bar appears when at least one missing model has a downloadable URL and directory. - Download all uses the existing browser download path. - Refresh reloads `object_info`, refreshes node definitions/combo values, reruns missing-model detection for the current graph, and clears the error if the selected model is now available. - OSS desktop: - The same action bar appears under the same downloadable-model condition. - Download all uses the existing Electron DownloadManager path. - Refresh uses the same missing-model pipeline as browser, so manually placed files or desktop-downloaded files can be rechecked without reloading the workflow. - Cloud: - The action bar remains hidden because model download/import is not supported in this section for Cloud. A few boundaries are intentional: - This PR does not add automatic filesystem watching. Browser OSS cannot reliably observe local model folder changes, so the user-triggered Refresh button remains the cross-environment mechanism. - This PR does not redesign the public `refreshComboInNodes` API beyond extracting `reloadNodeDefs()` for reuse. Further cleanup of toast behavior or a more explicit object-info reload API can be follow-up work. - This PR keeps refresh scoped to missing-model validation; missing media and missing nodes continue to use their existing flows. Linear: FE-417 ## Screenshots (if applicable) https://github.com/user-attachments/assets/2e02799f-1374-4377-b7b3-172241517772 ## Validation - `pnpm format` - `pnpm lint` (passes; existing unrelated warning remains in `src/platform/workspace/composables/useWorkspaceBilling.test.ts`) - `pnpm typecheck` - `pnpm test:unit` - `pnpm test:browser:local -- --project=chromium browser_tests/tests/propertiesPanel/errorsTabMissingModels.spec.ts` - `pnpm build` - `NX_SKIP_NX_CACHE=true DISTRIBUTION=desktop USE_PROD_CONFIG=true NODE_OPTIONS='--max-old-space-size=8192' pnpm exec nx build` - Manual desktop verification through `~/Projects/desktop` after copying the desktop build into `assets/ComfyUI/web_custom_versions/desktop_app`: - confirmed the FE bundle is built with `DISTRIBUTION = "desktop"` - confirmed missing model Download uses the desktop download path instead of browser download - confirmed Refresh can clear the missing model error after the model is available - Push hook: `pnpm knip --cache` ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-11661-feat-refresh-missing-models-through-pipeline-34f6d73d3650811488defee54a7a6667) by [Unito](https://www.unito.io)
157 lines
5.0 KiB
TypeScript
157 lines
5.0 KiB
TypeScript
import { expect } from '@playwright/test'
|
|
|
|
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
|
|
import { TestIds } from '@e2e/fixtures/selectors'
|
|
import {
|
|
interceptClipboardWrite,
|
|
getClipboardText
|
|
} from '@e2e/helpers/clipboardSpy'
|
|
import {
|
|
cleanupFakeModel,
|
|
loadWorkflowAndOpenErrorsTab
|
|
} from '@e2e/tests/propertiesPanel/ErrorsTabHelper'
|
|
|
|
test.describe('Errors tab - Missing models', { tag: '@ui' }, () => {
|
|
test.beforeEach(async ({ comfyPage }) => {
|
|
await comfyPage.settings.setSetting(
|
|
'Comfy.RightSidePanel.ShowErrorsTab',
|
|
true
|
|
)
|
|
await cleanupFakeModel(comfyPage)
|
|
})
|
|
|
|
test('Should show missing models group in errors tab', async ({
|
|
comfyPage
|
|
}) => {
|
|
await loadWorkflowAndOpenErrorsTab(comfyPage, 'missing/missing_models')
|
|
|
|
await expect(
|
|
comfyPage.page.getByTestId(TestIds.dialogs.missingModelsGroup)
|
|
).toBeVisible()
|
|
})
|
|
|
|
test('Should display model name with referencing node count', async ({
|
|
comfyPage
|
|
}) => {
|
|
await loadWorkflowAndOpenErrorsTab(comfyPage, 'missing/missing_models')
|
|
|
|
const modelsGroup = comfyPage.page.getByTestId(
|
|
TestIds.dialogs.missingModelsGroup
|
|
)
|
|
await expect(modelsGroup).toContainText(/fake_model\.safetensors\s*\(\d+\)/)
|
|
})
|
|
|
|
test('Should expand model row to show referencing nodes', async ({
|
|
comfyPage
|
|
}) => {
|
|
await loadWorkflowAndOpenErrorsTab(
|
|
comfyPage,
|
|
'missing/missing_models_with_nodes'
|
|
)
|
|
|
|
const locateButton = comfyPage.page.getByTestId(
|
|
TestIds.dialogs.missingModelLocate
|
|
)
|
|
await expect(locateButton.first()).toBeHidden()
|
|
|
|
const expandButton = comfyPage.page.getByTestId(
|
|
TestIds.dialogs.missingModelExpand
|
|
)
|
|
await expect(expandButton.first()).toBeVisible()
|
|
await expandButton.first().click()
|
|
|
|
await expect(locateButton.first()).toBeVisible()
|
|
})
|
|
|
|
test('Should copy model name to clipboard', async ({ comfyPage }) => {
|
|
await loadWorkflowAndOpenErrorsTab(comfyPage, 'missing/missing_models')
|
|
await interceptClipboardWrite(comfyPage.page)
|
|
|
|
const copyButton = comfyPage.page.getByTestId(
|
|
TestIds.dialogs.missingModelCopyName
|
|
)
|
|
await expect(copyButton.first()).toBeVisible()
|
|
await copyButton.first().dispatchEvent('click')
|
|
|
|
const copiedText = await getClipboardText(comfyPage.page)
|
|
expect(copiedText).toContain('fake_model.safetensors')
|
|
})
|
|
|
|
test.describe('OSS-specific', { tag: '@oss' }, () => {
|
|
test('Should show Copy URL button for non-asset models', async ({
|
|
comfyPage
|
|
}) => {
|
|
await loadWorkflowAndOpenErrorsTab(comfyPage, 'missing/missing_models')
|
|
|
|
const copyUrlButton = comfyPage.page.getByTestId(
|
|
TestIds.dialogs.missingModelCopyUrl
|
|
)
|
|
await expect(copyUrlButton.first()).toBeVisible()
|
|
})
|
|
|
|
test('Should show Download button for downloadable models', async ({
|
|
comfyPage
|
|
}) => {
|
|
await loadWorkflowAndOpenErrorsTab(comfyPage, 'missing/missing_models')
|
|
|
|
const downloadButton = comfyPage.page.getByTestId(
|
|
TestIds.dialogs.missingModelDownload
|
|
)
|
|
await expect(downloadButton.first()).toBeVisible()
|
|
})
|
|
|
|
test('Should render Download all and Refresh actions for one downloadable model', async ({
|
|
comfyPage
|
|
}) => {
|
|
await loadWorkflowAndOpenErrorsTab(comfyPage, 'missing/missing_models')
|
|
|
|
await expect(
|
|
comfyPage.page.getByTestId(TestIds.dialogs.missingModelActions)
|
|
).toBeVisible()
|
|
await expect(
|
|
comfyPage.page.getByTestId(TestIds.dialogs.missingModelDownloadAll)
|
|
).toBeVisible()
|
|
await expect(
|
|
comfyPage.page.getByTestId(TestIds.dialogs.missingModelRefresh)
|
|
).toBeVisible()
|
|
})
|
|
|
|
test('Should clear resolved missing model when Refresh is clicked', async ({
|
|
comfyPage
|
|
}) => {
|
|
await loadWorkflowAndOpenErrorsTab(comfyPage, 'missing/missing_models')
|
|
await comfyPage.page.route(/\/object_info$/, async (route) => {
|
|
const response = await route.fetch()
|
|
const objectInfo = await response.json()
|
|
const ckptName =
|
|
objectInfo.CheckpointLoaderSimple.input.required.ckpt_name
|
|
ckptName[0] = [...ckptName[0], 'fake_model.safetensors']
|
|
await route.fulfill({ response, json: objectInfo })
|
|
})
|
|
|
|
const objectInfoResponse = comfyPage.page.waitForResponse((response) => {
|
|
const url = new URL(response.url())
|
|
return url.pathname.endsWith('/object_info') && response.ok()
|
|
})
|
|
const modelFoldersResponse = comfyPage.page.waitForResponse(
|
|
(response) => {
|
|
const url = new URL(response.url())
|
|
return url.pathname.endsWith('/experiment/models') && response.ok()
|
|
}
|
|
)
|
|
const refreshButton = comfyPage.page.getByTestId(
|
|
TestIds.dialogs.missingModelRefresh
|
|
)
|
|
|
|
await Promise.all([
|
|
objectInfoResponse,
|
|
modelFoldersResponse,
|
|
refreshButton.click()
|
|
])
|
|
await expect(
|
|
comfyPage.page.getByTestId(TestIds.dialogs.missingModelsGroup)
|
|
).toBeHidden()
|
|
})
|
|
})
|
|
})
|