mirror of
https://github.com/Comfy-Org/ComfyUI-Manager.git
synced 2026-05-09 00:22:51 +08:00
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)
153 lines
6.0 KiB
TypeScript
153 lines
6.0 KiB
TypeScript
/**
|
|
* E2E tests: Legacy Custom Nodes Manager dialog.
|
|
*
|
|
* Tests the TurboGrid-based custom node list, filters, search,
|
|
* and basic row interactions.
|
|
*
|
|
* Requires ComfyUI running with --enable-manager-legacy-ui on PORT.
|
|
*/
|
|
|
|
import { test, expect } from '@playwright/test';
|
|
import { waitForComfyUI, openManagerMenu, clickMenuButton } from './helpers';
|
|
|
|
test.describe('Custom Nodes 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, 'Custom Nodes Manager');
|
|
|
|
// Wait for the custom nodes dialog to appear
|
|
await page.waitForSelector('#cn-manager-dialog', {
|
|
timeout: 10_000,
|
|
});
|
|
|
|
// The grid should be present
|
|
const grid = page.locator('.cn-manager-grid, .tg-body').first();
|
|
await expect(grid).toBeVisible({ timeout: 15_000 });
|
|
});
|
|
|
|
test('loads custom node list (non-empty)', async ({ page }) => {
|
|
await clickMenuButton(page, 'Custom Nodes Manager');
|
|
await page.waitForSelector('.cn-manager-grid, .tg-body', { timeout: 15_000 });
|
|
|
|
// Wait for data to load — grid rows should appear
|
|
await page.waitForFunction(
|
|
() => {
|
|
const rows = document.querySelectorAll('.tg-body .tg-row, .cn-manager-grid tr');
|
|
return rows.length > 0;
|
|
},
|
|
{ timeout: 30_000, polling: 1_000 },
|
|
);
|
|
|
|
const rows = page.locator('.tg-body .tg-row, .cn-manager-grid tr');
|
|
const count = await rows.count();
|
|
expect(count).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('filter dropdown changes displayed nodes', async ({ page }) => {
|
|
await clickMenuButton(page, 'Custom Nodes Manager');
|
|
await page.waitForSelector('.cn-manager-grid, .tg-body', { timeout: 15_000 });
|
|
|
|
// Wait for initial data load
|
|
await page.waitForFunction(
|
|
() => document.querySelectorAll('.tg-body .tg-row, .cn-manager-grid tr').length > 0,
|
|
{ timeout: 30_000, polling: 1_000 },
|
|
);
|
|
|
|
// Find the filter select (class: cn-manager-filter) and switch to "Installed"
|
|
const filterSelect = page.locator('select.cn-manager-filter').first();
|
|
// Hard-fail if filter UI missing — that's a regression, not a skip condition
|
|
await expect(filterSelect).toBeVisible({ timeout: 5_000 });
|
|
|
|
const initialCount = await page.locator('.tg-body .tg-row').count();
|
|
await filterSelect.selectOption({ label: 'Installed' });
|
|
// Wait for row count to actually CHANGE (state-based, not wall-clock).
|
|
// If filter is broken and returns everything, this will fail within 10s.
|
|
await expect
|
|
.poll(async () => page.locator('.tg-body .tg-row').count(), { timeout: 10_000 })
|
|
.not.toBe(initialCount);
|
|
|
|
// Installed count should be <= total
|
|
const filteredCount = await page.locator('.tg-body .tg-row').count();
|
|
expect(filteredCount).toBeLessThanOrEqual(initialCount);
|
|
});
|
|
|
|
test('search input filters the grid', async ({ page }) => {
|
|
await clickMenuButton(page, 'Custom Nodes Manager');
|
|
await page.waitForSelector('.cn-manager-grid, .tg-body', { timeout: 15_000 });
|
|
|
|
await page.waitForFunction(
|
|
() => document.querySelectorAll('.tg-body .tg-row, .cn-manager-grid tr').length > 0,
|
|
{ timeout: 30_000, polling: 1_000 },
|
|
);
|
|
|
|
// Find search input
|
|
const searchInput = page.locator('.cn-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, .cn-manager-grid tr').count();
|
|
await searchInput.fill('ComfyUI-Manager');
|
|
// State-based wait: count must actually narrow (or become 0)
|
|
await expect
|
|
.poll(
|
|
async () => page.locator('.tg-body .tg-row, .cn-manager-grid tr').count(),
|
|
{ timeout: 10_000 },
|
|
)
|
|
.toBeLessThan(initialCount);
|
|
|
|
const filteredCount = await page.locator('.tg-body .tg-row, .cn-manager-grid tr').count();
|
|
expect(filteredCount).toBeLessThanOrEqual(initialCount);
|
|
});
|
|
|
|
test('footer buttons are present', async ({ page }) => {
|
|
// Wave3 WI-U Cluster H target 4: strengthen from OR-of-2 to AND-of-all-
|
|
// always-visible-admin-buttons. js/custom-nodes-manager.js:26-34 defines 6
|
|
// footer buttons, but `.cn-manager-restart` and `.cn-manager-stop` are
|
|
// `display: none` by default in custom-nodes-manager.css:47-62 (shown only
|
|
// via showRestart()/showStop() — conditional on restart-required /
|
|
// task-running state). In a clean Manager state, neither is visible.
|
|
//
|
|
// The 4 ALWAYS-visible footer admin buttons are:
|
|
// - "Install via Git URL" — primary install entrypoint
|
|
// - "Used In Workflow" — filter to workflow-referenced nodes
|
|
// - "Check Update" — refresh available-update list
|
|
// - "Check Missing" — scan for missing nodes
|
|
//
|
|
// We assert all 4 are visible (AND semantics). Hidden-by-default Restart/
|
|
// Stop are checked structurally — exist in DOM but may be hidden.
|
|
await clickMenuButton(page, 'Custom Nodes Manager');
|
|
await page.waitForSelector('#cn-manager-dialog', {
|
|
timeout: 15_000,
|
|
});
|
|
|
|
const dialog = page.locator('#cn-manager-dialog').last();
|
|
|
|
// AND semantics: every always-visible footer button MUST be visible.
|
|
const alwaysVisibleButtons = [
|
|
'Install via Git URL',
|
|
'Used In Workflow',
|
|
'Check Update',
|
|
'Check Missing',
|
|
];
|
|
for (const label of alwaysVisibleButtons) {
|
|
await expect(
|
|
dialog.locator(`button:has-text("${label}")`).first(),
|
|
`always-visible footer button "${label}" must be present and visible`,
|
|
).toBeVisible();
|
|
}
|
|
|
|
// Structural presence for conditional buttons — they exist in the DOM but
|
|
// are hidden until showRestart()/showStop() toggles `display: block`.
|
|
for (const cls of ['.cn-manager-restart', '.cn-manager-stop']) {
|
|
await expect(
|
|
dialog.locator(cls),
|
|
`conditional footer button ${cls} must be present in DOM (may be hidden)`,
|
|
).toHaveCount(1);
|
|
}
|
|
});
|
|
});
|