ComfyUI-Manager/tests/playwright/legacy-ui-model-manager.spec.ts
Dr.Lt.Data 4410ebc6a6
Some checks are pending
Publish to PyPI / build-and-publish (push) Waiting to run
Python Linting / Run Ruff (push) Waiting to run
fix(security): harden CSRF with Content-Type gate and expand E2E coverage (#2818)
Defense-in-depth over GET→POST alone: reject the three CORS-safelisted
simple-form Content-Types (x-www-form-urlencoded, multipart/form-data,
text/plain) on 16 no-body POST handlers (glob + legacy) to block
<form method=POST> CSRF that bypasses method-only gating. Move
comfyui_switch_version to a JSON body so the preflight requirement applies.
Split db_mode/policy/update/channel_url_list into GET(read) + POST(write).
Tighten do_fix (high → high+) and gate three previously-ungated config
setters at middle. Resynchronize openapi.yaml (27 paths, 30 operations,
ComfyUISwitchVersionParams as a shared $ref component). Add E2E harness
variants, Playwright config, CSRF/secgate suites, 39-endpoint coverage,
and a CHANGELOG.

Breaking: legacy per-op POST routes (install/uninstall/fix/disable/update/
reinstall/abort_current) are removed; callers already use queue/batch.
Legacy /manager/notice (v1) is removed; /v2/manager/notice is retained.

Reported-by: XlabAI Team of Tencent Xuanwu Lab
CVSS: 8.1 (AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:H)
2026-04-22 05:04:30 +09:00

136 lines
5.8 KiB
TypeScript

/**
* E2E tests: Legacy Model Manager dialog.
*
* Tests the model list grid, filters, and search.
*
* Requires ComfyUI running with --enable-manager-legacy-ui on PORT.
*/
import { test, expect } from '@playwright/test';
import { waitForComfyUI, openManagerMenu, clickMenuButton } from './helpers';
test.describe('Model Manager', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await waitForComfyUI(page);
await openManagerMenu(page);
});
test('opens from Manager menu and renders grid', async ({ page }) => {
await clickMenuButton(page, 'Model Manager');
await page.waitForSelector('#cmm-manager-dialog', {
timeout: 10_000,
});
const grid = page.locator('.cmm-manager-grid, .tg-body').first();
await expect(grid).toBeVisible({ timeout: 15_000 });
});
test('loads model list (non-empty)', async ({ page }) => {
// Wave3 WI-U Cluster H target 3 (LM1): previously rows>0 only. Now also
// verifies the install-state column is rendered for every logical model row.
//
// TurboGrid renders each logical row as TWO DOM `.tg-row` elements (left
// frozen-column pane + right scrollable-column pane). Only the right pane
// carries the "installed" column, which `model-manager.js:342-345` formats
// as EITHER `<div class="cmm-icon-passed">...</div>` (installed===True) OR
// `<button class="cmm-btn-install">Install</button>` (installed===False),
// with a `"Refresh Required"` fallback at :340.
//
// Invariant: the number of install-state indicators equals the number of
// logical rows, i.e. half the DOM-row count. This catches a regression
// where the installed column stops rendering for any model (partial or
// complete).
await clickMenuButton(page, 'Model Manager');
await page.waitForSelector('.cmm-manager-grid, .tg-body', { timeout: 15_000 });
await page.waitForFunction(
() => document.querySelectorAll('.tg-body .tg-row, .cmm-manager-grid tr').length > 0,
{ timeout: 30_000, polling: 1_000 },
);
const rows = page.locator('.tg-body .tg-row, .cmm-manager-grid tr');
const domRowCount = await rows.count();
expect(domRowCount).toBeGreaterThan(0);
// Count install indicators across the whole grid.
const installedCount = await page
.locator('.cmm-icon-passed, .cmm-btn-install')
.count();
const refreshCount = await page
.locator('.tg-body :text("Refresh Required"), .cmm-manager-grid :text("Refresh Required")')
.count();
const totalIndicators = installedCount + refreshCount;
// Each logical model row must expose an install-state indicator.
expect(totalIndicators, 'at least one row must have a valid install-state indicator').toBeGreaterThan(0);
// Expected indicator count: one per logical row. TurboGrid doubles DOM
// rows for the 2-pane layout, so logical_count = domRowCount / 2 when
// dual-pane. For single-pane (fallback) the ratio is 1:1. Accept either.
const logicalRowCount = domRowCount / 2;
const isDualPane = Number.isInteger(logicalRowCount) && totalIndicators === logicalRowCount;
const isSinglePane = totalIndicators === domRowCount;
expect(
isDualPane || isSinglePane,
`install-state indicator count mismatch: totalIndicators=${totalIndicators}, ` +
`domRowCount=${domRowCount}. Expected either ${logicalRowCount} (dual-pane) or ${domRowCount} (single-pane).`,
).toBe(true);
});
test('search input filters the model grid', async ({ page }) => {
await clickMenuButton(page, 'Model Manager');
await page.waitForSelector('.cmm-manager-grid, .tg-body', { timeout: 15_000 });
await page.waitForFunction(
() => document.querySelectorAll('.tg-body .tg-row, .cmm-manager-grid tr').length > 0,
{ timeout: 30_000, polling: 1_000 },
);
const searchInput = page.locator('.cmm-manager-keywords, input[type="text"][placeholder*="earch"], input[type="search"]').first();
await expect(searchInput).toBeVisible({ timeout: 5_000 });
const initialCount = await page.locator('.tg-body .tg-row, .cmm-manager-grid tr').count();
await searchInput.fill('stable diffusion');
// State-based wait: row count must change (or narrow). If the search
// is entirely broken and returns all rows, this will fail the poll.
await expect
.poll(
async () => page.locator('.tg-body .tg-row, .cmm-manager-grid tr').count(),
{ timeout: 10_000 },
)
.not.toBe(initialCount);
const filteredCount = await page.locator('.tg-body .tg-row, .cmm-manager-grid tr').count();
expect(filteredCount).toBeLessThanOrEqual(initialCount);
});
test('filter dropdown is present with expected options', async ({ page }) => {
// Wave3 WI-U Cluster H target 5: previously options.length>0 only.
// Now asserts the filter dropdown surfaces all 4 known states defined by
// ModelManager.initFilter() in js/model-manager.js:74-86 —
// `All`, `Installed`, `Not Installed`, `In Workflow`.
await clickMenuButton(page, 'Model Manager');
await page.waitForSelector('#cmm-manager-dialog', {
timeout: 15_000,
});
const dialog = page.locator('#cmm-manager-dialog').last();
const filterSelect = dialog.locator('select').filter({ hasText: /All|Installed/ }).first();
await expect(filterSelect).toBeVisible({ timeout: 5_000 });
const options = (await filterSelect.locator('option').allTextContents()).map((s) => s.trim());
// Exact set match (normalized): js/model-manager.js:74-86 defines this
// list. If labels change, update this assertion consciously.
const expected = ['All', 'Installed', 'Not Installed', 'In Workflow'];
const actual = new Set(options);
for (const label of expected) {
expect(
actual.has(label),
`filter dropdown missing expected option "${label}". Options=${JSON.stringify(options)}`,
).toBe(true);
}
});
});