ComfyUI-Manager/tests/playwright/helpers.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

129 lines
4.6 KiB
TypeScript

/**
* Shared helpers for ComfyUI Manager Playwright E2E tests.
*
* The legacy UI is dialog-based: a "Manager" menu button on the ComfyUI
* top-bar opens ManagerMenuDialog, from which sub-dialogs (CustomNodes,
* Model, Snapshot) are launched.
*/
import { type Page, expect } from '@playwright/test';
/** Wait for the ComfyUI page to be fully loaded (queue ready). */
export async function waitForComfyUI(page: Page) {
// ComfyUI shows the canvas once the app is ready. Wait for the
// system_stats endpoint to respond — same check the Python E2E uses.
await page.waitForFunction(
async () => {
try {
const r = await fetch('/system_stats');
return r.ok;
} catch {
return false;
}
},
{ timeout: 30_000, polling: 1_000 },
);
// Give the extensions a moment to register their menu items.
await page.waitForTimeout(3_000);
// Close any overlay that might be covering the toolbar.
// Press Escape to dismiss popups/modals/sidebars.
await page.keyboard.press('Escape');
await page.waitForTimeout(1_000);
await page.keyboard.press('Escape');
await page.waitForTimeout(500);
}
/** Open the Manager Menu dialog via the top-bar button. */
export async function openManagerMenu(page: Page) {
// The legacy UI registers a "Manager" button via ComfyButton (new style)
// or a plain <button> (old style). The new-style button uses the
// "puzzle" icon and has tooltip "ComfyUI Manager" / content "Manager".
//
// ComfyButton renders as a structure like:
// <button class="comfyui-button" title="ComfyUI Manager">
// <span class="icon">...</span>
// <span>Manager</span>
// </button>
//
// We try multiple selectors to handle both old and new ComfyUI layouts.
const selectors = [
'button[title="ComfyUI Manager"]', // new-style ComfyButton
'button.comfyui-button:has-text("Manager")', // new-style fallback
'button:has-text("Manager")', // old-style plain button
];
for (const sel of selectors) {
const btn = page.locator(sel).first();
if (await btn.isVisible({ timeout: 3_000 }).catch(() => false)) {
await btn.click();
await page.waitForSelector('#cm-manager-dialog, .comfy-modal', { timeout: 10_000 });
return;
}
}
// Last resort: find any button with "Manager" in tooltip or text via DOM
const found = await page.evaluate(() => {
const buttons = document.querySelectorAll('button');
for (const btn of buttons) {
const text = btn.textContent?.toLowerCase() || '';
const title = btn.getAttribute('title')?.toLowerCase() || '';
if (text.includes('manager') || title.includes('manager')) {
(btn as HTMLElement).click();
return true;
}
}
return false;
});
if (found) {
// Wait for the dialog by polling for the element in DOM
await page.waitForFunction(
() => !!document.getElementById('cm-manager-dialog'),
{ timeout: 10_000, polling: 500 },
);
return;
}
await page.screenshot({ path: 'test-results/debug-manager-btn-not-found.png' });
throw new Error('Could not find Manager button in ComfyUI toolbar');
}
/** Click a button inside the Manager Menu dialog by its visible text. */
export async function clickMenuButton(page: Page, text: string) {
const dialog = page.locator('#cm-manager-dialog').first();
await dialog.locator(`button:has-text("${text}")`).click();
}
/** Close the topmost dialog via its X (close) button or Escape. */
export async function closeDialog(page: Page) {
// Try clicking close buttons on visible dialogs. The manager-menu dialog
// (`#cm-manager-dialog`) is a ComfyDialog with `.p-dialog-close-button` (X),
// while sub-dialogs use `.cm-close-btn`. Try both.
for (const sel of [
'#cn-manager-dialog button.cm-close-btn',
'#cmm-manager-dialog button.cm-close-btn',
'#snapshot-manager-dialog button.cm-close-btn',
'#cm-manager-dialog button.cm-close-btn',
'#cm-manager-dialog .p-dialog-close-button',
'.cm-close-btn',
'.p-dialog-close-button',
]) {
const btn = page.locator(sel).last();
if (await btn.isVisible({ timeout: 500 }).catch(() => false)) {
await btn.click();
await page.waitForTimeout(300);
return;
}
}
// Fallback: press Escape (ComfyDialog may not honor this reliably)
await page.keyboard.press('Escape');
await page.waitForTimeout(300);
}
/** Assert the Manager Menu dialog is visible and contains expected sections. */
export async function assertManagerMenuVisible(page: Page) {
const dialog = page.locator('#cm-manager-dialog').first();
await expect(dialog).toBeVisible();
}