diff --git a/.claude/commands/create-frontend-release.md b/.claude/commands/create-frontend-release.md index ae2240462..5554b6955 100644 --- a/.claude/commands/create-frontend-release.md +++ b/.claude/commands/create-frontend-release.md @@ -391,7 +391,7 @@ echo "Last stable release: $LAST_STABLE" ```bash # Trigger the workflow -gh workflow run version-bump.yaml -f version_type=${VERSION_TYPE} +gh workflow run release-version-bump.yaml -f version_type=${VERSION_TYPE} # Workflow runs quickly - usually creates PR within 30 seconds echo "Workflow triggered. Waiting for PR creation..." @@ -443,28 +443,21 @@ echo "Workflow triggered. Waiting for PR creation..." gh pr view ${PR_NUMBER} --json labels | jq -r '.labels[].name' | grep -q "Release" || \ echo "ERROR: Release label missing! Add it immediately!" ``` -2. Check for update-locales commits: - ```bash - # WARNING: update-locales may add [skip ci] which blocks release workflow! - gh pr view ${PR_NUMBER} --json commits | grep -q "skip ci" && \ - echo "WARNING: [skip ci] detected - release workflow may not trigger!" - ``` -3. Verify version number in package.json -4. Review all changed files -5. Ensure no unintended changes included -6. Wait for required PR checks: +2. Verify version number in package.json +3. Review all changed files +4. Ensure no unintended changes included +5. Wait for required PR checks: ```bash gh pr checks ${PR_NUMBER} --watch ``` -7. **FINAL CODE REVIEW**: Release label present and no [skip ci]? +6. **FINAL CODE REVIEW**: Release label present and no [skip ci]? ### Step 12: Pre-Merge Validation 1. **Review Requirements**: Release PRs require approval -2. Monitor CI checks - watch for update-locales -3. **CRITICAL WARNING**: If update-locales adds [skip ci], the release workflow won't trigger! -4. Check no new commits to main since PR creation -5. **DEPLOYMENT READINESS**: Ready to merge? +2. Monitor CI checks +3. Check no new commits to main since PR creation +4. **DEPLOYMENT READINESS**: Ready to merge? ### Step 13: Execute Release diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 000000000..36b3beb69 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,6 @@ +issue_enrichment: + auto_enrich: + enabled: true +reviews: + auto_review: + drafts: true diff --git a/.gitattributes b/.gitattributes index 17591e2d4..c153cc320 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,17 +1,5 @@ -# Default -* text=auto - -# Force TS to LF to make the unixy scripts not break on Windows -*.cjs text eol=lf -*.js text eol=lf -*.json text eol=lf -*.mjs text eol=lf -*.mts text eol=lf -*.snap text eol=lf -*.ts text eol=lf -*.vue text eol=lf -*.yaml text eol=lf -*.yml text eol=lf +# Force all text files to use LF line endings +* text=auto eol=lf # Generated files packages/registry-types/src/comfyRegistryTypes.ts linguist-generated=true diff --git a/.github/ISSUE_TEMPLATE/bug-report.yaml b/.github/ISSUE_TEMPLATE/bug-report.yaml index f59926d26..bde3829f7 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yaml +++ b/.github/ISSUE_TEMPLATE/bug-report.yaml @@ -10,10 +10,7 @@ body: options: - label: I am running the latest version of ComfyUI required: true - - label: I have searched existing issues to make sure this isn't a duplicate - required: true - - label: I have tested with all custom nodes disabled ([see how](https://docs.comfy.org/troubleshooting/custom-node-issues#step-1%3A-test-with-all-custom-nodes-disabled)) - required: true + - label: I have custom nodes enabled - type: textarea id: description diff --git a/.github/ISSUE_TEMPLATE/feature-request.yaml b/.github/ISSUE_TEMPLATE/feature-request.yaml index 0d8173b28..2d26a0981 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yaml +++ b/.github/ISSUE_TEMPLATE/feature-request.yaml @@ -4,13 +4,6 @@ labels: [] type: Feature body: - - type: checkboxes - attributes: - label: Is there an existing issue for this? - description: Please search to see if an issue already exists for the problem you're experiencing, and that it's not addressed in a recent build/commit. - options: - - label: I have searched the existing issues and checked the recent builds/commits - required: true - type: markdown attributes: value: | diff --git a/.github/workflows/ci-lint-format.yaml b/.github/workflows/ci-lint-format.yaml index f463af16c..df3f30c38 100644 --- a/.github/workflows/ci-lint-format.yaml +++ b/.github/workflows/ci-lint-format.yaml @@ -21,6 +21,7 @@ jobs: uses: actions/checkout@v6 with: ref: ${{ !github.event.pull_request.head.repo.fork && github.head_ref || github.ref }} + token: ${{ secrets.PR_GH_TOKEN }} - name: Setup frontend uses: ./.github/actions/setup-frontend diff --git a/.github/workflows/ci-tests-e2e.yaml b/.github/workflows/ci-tests-e2e.yaml index b3c4e1998..34d24f7f0 100644 --- a/.github/workflows/ci-tests-e2e.yaml +++ b/.github/workflows/ci-tests-e2e.yaml @@ -37,7 +37,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 container: - image: ghcr.io/comfy-org/comfyui-ci-container:0.0.10 + image: ghcr.io/comfy-org/comfyui-ci-container:0.0.12 credentials: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} @@ -85,7 +85,7 @@ jobs: needs: setup runs-on: ubuntu-latest container: - image: ghcr.io/comfy-org/comfyui-ci-container:0.0.10 + image: ghcr.io/comfy-org/comfyui-ci-container:0.0.12 credentials: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/i18n-update-core.yaml b/.github/workflows/i18n-update-core.yaml index 7b0299ab1..5f0985b93 100644 --- a/.github/workflows/i18n-update-core.yaml +++ b/.github/workflows/i18n-update-core.yaml @@ -17,6 +17,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v6 + with: + token: ${{ secrets.PR_GH_TOKEN }} # Setup playwright environment - name: Setup ComfyUI Frontend diff --git a/.github/workflows/pr-update-playwright-expectations.yaml b/.github/workflows/pr-update-playwright-expectations.yaml index 1372c8126..b9cf9bb75 100644 --- a/.github/workflows/pr-update-playwright-expectations.yaml +++ b/.github/workflows/pr-update-playwright-expectations.yaml @@ -77,7 +77,7 @@ jobs: needs: setup runs-on: ubuntu-latest container: - image: ghcr.io/comfy-org/comfyui-ci-container:0.0.10 + image: ghcr.io/comfy-org/comfyui-ci-container:0.0.12 credentials: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} @@ -173,6 +173,7 @@ jobs: uses: actions/checkout@v6 with: ref: ${{ needs.setup.outputs.branch }} + token: ${{ secrets.PR_GH_TOKEN }} # Download all changed snapshot files from shards - name: Download snapshot artifacts diff --git a/.github/workflows/release-draft-create.yaml b/.github/workflows/release-draft-create.yaml index 1e32e450b..74d6c9898 100644 --- a/.github/workflows/release-draft-create.yaml +++ b/.github/workflows/release-draft-create.yaml @@ -50,6 +50,7 @@ jobs: ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }} ENABLE_MINIFY: 'true' USE_PROD_CONFIG: 'true' + IS_NIGHTLY: ${{ case(github.ref == 'refs/heads/main', 'true', 'false') }} run: | pnpm install --frozen-lockfile pnpm build diff --git a/.oxlintrc.json b/.oxlintrc.json index a0769d8f4..fb5d00dab 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -110,6 +110,17 @@ "rules": { "no-console": "allow" } + }, + { + "files": ["browser_tests/**/*.ts"], + "rules": { + "typescript/no-explicit-any": "error", + "no-async-promise-executor": "error", + "no-control-regex": "error", + "no-useless-rename": "error", + "no-unused-private-class-members": "error", + "unicorn/no-empty-file": "error" + } } ] } diff --git a/AGENTS.md b/AGENTS.md index 3eeac44cc..4603eeabc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,7 +21,7 @@ See @docs/guidance/\*.md for file-type-specific conventions (auto-loaded by glob - i18n: `src/i18n.ts`, - Entry Point: `src/main.ts`. - Tests: - - unit/component in `tests-ui/` and `src/**/*.test.ts` + - unit/component in `src/**/*.test.ts` - E2E (Playwright) in `browser_tests/**/*.spec.ts` - Public assets: `public/` - Build output: `dist/` @@ -44,7 +44,7 @@ The project uses **Nx** for build orchestration and task management - `pnpm build`: Type-check then production build to `dist/` - `pnpm preview`: Preview the production build locally - `pnpm test:unit`: Run Vitest unit tests -- `pnpm test:browser`: Run Playwright E2E tests (`browser_tests/`) +- `pnpm test:browser:local`: Run Playwright E2E tests (`browser_tests/`) - `pnpm lint` / `pnpm lint:fix`: Lint (ESLint) - `pnpm format` / `pnpm format:check`: oxfmt - `pnpm typecheck`: Vue TSC type checking @@ -264,7 +264,7 @@ A particular type of complexity is over-engineering, where developers have made ## Repository Navigation -- Check README files in key folders (tests-ui, browser_tests, composables, etc.) +- Check README files in key folders (browser_tests, composables, etc.) - Prefer running single tests for performance - Use --help for unfamiliar CLI tools diff --git a/browser_tests/AGENTS.md b/browser_tests/AGENTS.md index 177529ee1..b5cbe18c4 100644 --- a/browser_tests/AGENTS.md +++ b/browser_tests/AGENTS.md @@ -6,3 +6,9 @@ See `@docs/guidance/playwright.md` for Playwright best practices (auto-loaded fo - `assets/` - Test data (JSON workflows, fixtures) - Tests use premade JSON workflows to load desired graph state + +## After Making Changes + +- Run `pnpm typecheck:browser` after modifying TypeScript files in this directory +- Run `pnpm exec eslint browser_tests/path/to/file.ts` to lint specific files +- Run `pnpm exec oxlint browser_tests/path/to/file.ts` to check with oxlint diff --git a/browser_tests/assets/workflowInMedia/workflow_itxt.png b/browser_tests/assets/workflowInMedia/workflow_itxt.png new file mode 100644 index 000000000..95ce757bc Binary files /dev/null and b/browser_tests/assets/workflowInMedia/workflow_itxt.png differ diff --git a/browser_tests/fixtures/ComfyPage.ts b/browser_tests/fixtures/ComfyPage.ts index 2d5df88b0..643907163 100644 --- a/browser_tests/fixtures/ComfyPage.ts +++ b/browser_tests/fixtures/ComfyPage.ts @@ -1,40 +1,52 @@ -import type { APIRequestContext, Locator, Page } from '@playwright/test' +import type { + APIRequestContext, + ExpectMatcherState, + Locator, + Page +} from '@playwright/test' import { test as base, expect } from '@playwright/test' import dotenv from 'dotenv' -import * as fs from 'fs' -import type { LGraphNode, LGraph } from '../../src/lib/litegraph/src/litegraph' -import type { NodeId } from '../../src/platform/workflow/validation/schemas/workflowSchema' -import type { KeyCombo } from '../../src/platform/keybindings' -import type { useWorkspaceStore } from '../../src/stores/workspaceStore' +import { TestIds } from './selectors' import { NodeBadgeMode } from '../../src/types/nodeSource' import { ComfyActionbar } from '../helpers/actionbar' import { ComfyTemplates } from '../helpers/templates' import { ComfyMouse } from './ComfyMouse' import { VueNodeHelpers } from './VueNodeHelpers' import { ComfyNodeSearchBox } from './components/ComfyNodeSearchBox' +import { ContextMenu } from './components/ContextMenu' import { SettingDialog } from './components/SettingDialog' +import { BottomPanel } from './components/BottomPanel' import { NodeLibrarySidebarTab, WorkflowsSidebarTab } from './components/SidebarTab' import { Topbar } from './components/Topbar' -import type { Position, Size } from './types' -import { NodeReference, SubgraphSlotReference } from './utils/litegraphUtils' +import { CanvasHelper } from './helpers/CanvasHelper' +import { ClipboardHelper } from './helpers/ClipboardHelper' +import { CommandHelper } from './helpers/CommandHelper' +import { DebugHelper } from './helpers/DebugHelper' +import { DragDropHelper } from './helpers/DragDropHelper' +import { KeyboardHelper } from './helpers/KeyboardHelper' +import { NodeOperationsHelper } from './helpers/NodeOperationsHelper' +import { SettingsHelper } from './helpers/SettingsHelper' +import { SubgraphHelper } from './helpers/SubgraphHelper' +import { ToastHelper } from './helpers/ToastHelper' +import { WorkflowHelper } from './helpers/WorkflowHelper' +import type { NodeReference } from './utils/litegraphUtils' +import type { WorkspaceStore } from '../types/globals' dotenv.config() -type WorkspaceStore = ReturnType - class ComfyPropertiesPanel { readonly root: Locator readonly panelTitle: Locator readonly searchBox: Locator constructor(readonly page: Page) { - this.root = page.getByTestId('properties-panel') + this.root = page.getByTestId(TestIds.propertiesPanel.root) this.panelTitle = this.root.locator('h3') - this.searchBox = this.root.getByPlaceholder('Search...') + this.searchBox = this.root.getByPlaceholder(/^Search/) } } @@ -45,16 +57,12 @@ class ComfyMenu { public readonly sideToolbar: Locator public readonly propertiesPanel: ComfyPropertiesPanel - public readonly themeToggleButton: Locator - public readonly saveButton: Locator + public readonly modeToggleButton: Locator constructor(public readonly page: Page) { - this.sideToolbar = page.locator('.side-tool-bar-container') - this.themeToggleButton = page.locator('.comfy-vue-theme-toggle') + this.sideToolbar = page.getByTestId(TestIds.sidebar.toolbar) + this.modeToggleButton = page.getByTestId(TestIds.sidebar.modeToggle) this.propertiesPanel = new ComfyPropertiesPanel(page) - this.saveButton = page - .locator('button[title="Save the current workflow"]') - .nth(0) } get buttons() { @@ -77,33 +85,28 @@ class ComfyMenu { } async toggleTheme() { - await this.themeToggleButton.click() - await this.page.evaluate(() => { - return new Promise((resolve) => { - window['app'].ui.settings.addEventListener( - 'Comfy.ColorPalette.change', - resolve, - { once: true } + const currentTheme = await this.getThemeId() + await this.modeToggleButton.click() + await this.page.waitForFunction( + (prevTheme) => { + const settings = window.app?.ui?.settings + return ( + settings && + settings.getSettingValue('Comfy.ColorPalette') !== prevTheme ) - - setTimeout(resolve, 5000) - }) - }) + }, + currentTheme, + { timeout: 5000 } + ) } async getThemeId() { return await this.page.evaluate(async () => { - return await window['app'].ui.settings.getSettingValue( - 'Comfy.ColorPalette' - ) + return await window.app!.ui.settings.getSettingValue('Comfy.ColorPalette') }) } } -type FolderStructure = { - [key: string]: FolderStructure | string -} - type KeysOfType = { [K in keyof T]: T[K] extends Match ? K : never }[keyof T] @@ -125,7 +128,7 @@ class ConfirmDialog { async click(locator: KeysOfType) { const loc = this[locator] - await expect(loc).toBeVisible() + await loc.waitFor({ state: 'visible' }) await loc.click() // Wait for the dialog mask to disappear after confirming @@ -137,7 +140,9 @@ class ConfirmDialog { // Wait for workflow service to finish if it's busy await this.page.waitForFunction( - () => window['app']?.extensionManager?.workflow?.isBusy === false, + () => + (window.app?.extensionManager as WorkspaceStore | undefined)?.workflow + ?.isBusy === false, undefined, { timeout: 3000 } ) @@ -159,9 +164,6 @@ export class ComfyPage { // Inputs public readonly workflowUploadInput: Locator - // Toasts - public readonly visibleToasts: Locator - // Components public readonly searchBox: ComfyNodeSearchBox public readonly menu: ComfyMenu @@ -170,6 +172,19 @@ export class ComfyPage { public readonly settingDialog: SettingDialog public readonly confirmDialog: ConfirmDialog public readonly vueNodes: VueNodeHelpers + public readonly debug: DebugHelper + public readonly subgraph: SubgraphHelper + public readonly canvasOps: CanvasHelper + public readonly nodeOps: NodeOperationsHelper + public readonly settings: SettingsHelper + public readonly keyboard: KeyboardHelper + public readonly clipboard: ClipboardHelper + public readonly workflow: WorkflowHelper + public readonly contextMenu: ContextMenu + public readonly toast: ToastHelper + public readonly dragDrop: DragDropHelper + public readonly command: CommandHelper + public readonly bottomPanel: BottomPanel /** Worker index to test user ID */ public readonly userIds: string[] = [] @@ -190,10 +205,9 @@ export class ComfyPage { this.resetViewButton = page.getByRole('button', { name: 'Reset View' }) this.queueButton = page.getByRole('button', { name: 'Queue Prompt' }) this.runButton = page - .getByTestId('queue-button') + .getByTestId(TestIds.topbar.queueButton) .getByRole('button', { name: 'Run' }) this.workflowUploadInput = page.locator('#comfy-file-input') - this.visibleToasts = page.locator('.p-toast-message:visible') this.searchBox = new ComfyNodeSearchBox(page) this.menu = new ComfyMenu(page) @@ -202,62 +216,23 @@ export class ComfyPage { this.settingDialog = new SettingDialog(page, this) this.confirmDialog = new ConfirmDialog(page) this.vueNodes = new VueNodeHelpers(page) + this.debug = new DebugHelper(page, this.canvas) + this.subgraph = new SubgraphHelper(this) + this.canvasOps = new CanvasHelper(page, this.canvas, this.resetViewButton) + this.nodeOps = new NodeOperationsHelper(this) + this.settings = new SettingsHelper(page) + this.keyboard = new KeyboardHelper(page, this.canvas) + this.clipboard = new ClipboardHelper(this.keyboard) + this.workflow = new WorkflowHelper(this) + this.contextMenu = new ContextMenu(page) + this.toast = new ToastHelper(page) + this.dragDrop = new DragDropHelper(page, this.assetPath.bind(this)) + this.command = new CommandHelper(page) + this.bottomPanel = new BottomPanel(page) } - convertLeafToContent(structure: FolderStructure): FolderStructure { - const result: FolderStructure = {} - - for (const [key, value] of Object.entries(structure)) { - if (typeof value === 'string') { - const filePath = this.assetPath(value) - result[key] = fs.readFileSync(filePath, 'utf-8') - } else { - result[key] = this.convertLeafToContent(value) - } - } - - return result - } - - async getGraphNodesCount(): Promise { - return await this.page.evaluate(() => { - return window['app']?.graph?.nodes?.length || 0 - }) - } - - async getSelectedGraphNodesCount(): Promise { - return await this.page.evaluate(() => { - return ( - window['app']?.graph?.nodes?.filter( - (node: any) => node.is_selected === true - ).length || 0 - ) - }) - } - - async setupWorkflowsDirectory(structure: FolderStructure) { - const resp = await this.request.post( - `${this.url}/api/devtools/setup_folder_structure`, - { - data: { - tree_structure: this.convertLeafToContent(structure), - base_path: `user/${this.id}/workflows` - } - } - ) - - if (resp.status() !== 200) { - throw new Error( - `Failed to setup workflows directory: ${await resp.text()}` - ) - } - - await this.page.evaluate(async () => { - await window['app'].extensionManager.workflow.syncWorkflows() - }) - - // Wait for Vue to re-render the workflow list - await this.nextFrame() + get visibleToasts() { + return this.toast.visibleToasts } async setupUser(username: string) { @@ -285,7 +260,7 @@ export class ComfyPage { return await resp.json() } - async setupSettings(settings: Record) { + async setupSettings(settings: Record) { const resp = await this.request.post( `${this.url}/api/devtools/set_settings`, { @@ -338,9 +313,9 @@ export class ComfyPage { await this.page.waitForFunction(() => document.fonts.ready) await this.page.waitForFunction( () => - // window['app'] => GraphCanvas ready - // window['app'].extensionManager => GraphView ready - window['app'] && window['app'].extensionManager + // window.app => GraphCanvas ready + // window.app.extensionManager => GraphView ready + window.app && window.app.extensionManager ) await this.page.waitForSelector('.p-blockui-mask', { state: 'hidden' }) await this.nextFrame() @@ -350,79 +325,6 @@ export class ComfyPage { return `./browser_tests/assets/${fileName}` } - async executeCommand(commandId: string) { - await this.page.evaluate((id: string) => { - return window['app'].extensionManager.command.execute(id) - }, commandId) - } - - async registerCommand( - commandId: string, - command: (() => void) | (() => Promise) - ) { - await this.page.evaluate( - ({ commandId, commandStr }) => { - const app = window['app'] - const randomSuffix = Math.random().toString(36).substring(2, 8) - const extensionName = `TestExtension_${randomSuffix}` - - app.registerExtension({ - name: extensionName, - commands: [ - { - id: commandId, - function: eval(commandStr) - } - ] - }) - }, - { commandId, commandStr: command.toString() } - ) - } - - async registerKeybinding(keyCombo: KeyCombo, command: () => void) { - await this.page.evaluate( - ({ keyCombo, commandStr }) => { - const app = window['app'] - const randomSuffix = Math.random().toString(36).substring(2, 8) - const extensionName = `TestExtension_${randomSuffix}` - const commandId = `TestCommand_${randomSuffix}` - - app.registerExtension({ - name: extensionName, - keybindings: [ - { - combo: keyCombo, - commandId: commandId - } - ], - commands: [ - { - id: commandId, - function: eval(commandStr) - } - ] - }) - }, - { keyCombo, commandStr: command.toString() } - ) - } - - async setSetting(settingId: string, settingValue: any) { - return await this.page.evaluate( - async ({ id, value }) => { - await window['app'].extensionManager.setting.set(id, value) - }, - { id: settingId, value: settingValue } - ) - } - - async getSetting(settingId: string) { - return await this.page.evaluate(async (id) => { - return await window['app'].extensionManager.setting.get(id) - }, settingId) - } - async goto() { await this.page.goto(this.url) } @@ -437,37 +339,6 @@ export class ComfyPage { return new Promise((resolve) => setTimeout(resolve, ms)) } - async loadWorkflow(workflowName: string) { - await this.workflowUploadInput.setInputFiles( - this.assetPath(`${workflowName}.json`) - ) - await this.nextFrame() - } - - async deleteWorkflow( - workflowName: string, - whenMissing: 'ignoreMissing' | 'throwIfMissing' = 'ignoreMissing' - ) { - // Open workflows tab - const { workflowsTab } = this.menu - await workflowsTab.open() - - // Action to take if workflow missing - if (whenMissing === 'ignoreMissing') { - const workflows = await workflowsTab.getTopLevelSavedWorkflowNames() - if (!workflows.includes(workflowName)) return - } - - // Delete workflow - await workflowsTab.getPersistedItem(workflowName).click({ button: 'right' }) - await this.clickContextMenuItem('Delete') - await this.confirmDialog.delete.click() - - // Clear toast & close tab - await this.closeToasts(1) - await workflowsTab.close() - } - /** * Attach a screenshot to the test report. * By default, screenshots are only taken in non-CI environments. @@ -494,1080 +365,22 @@ export class ComfyPage { }) } - async resetView() { - if (await this.resetViewButton.isVisible()) { - await this.resetViewButton.click() - } - // Avoid "Reset View" button highlight. - await this.page.mouse.move(10, 10) - await this.nextFrame() - } - - async getToastErrorCount() { - return await this.page - .locator('.p-toast-message.p-toast-message-error') - .count() - } - - async getVisibleToastCount() { - return await this.visibleToasts.count() - } - - async closeToasts(requireCount = 0) { - if (requireCount) await expect(this.visibleToasts).toHaveCount(requireCount) - - // Clear all toasts - const toastCloseButtons = await this.page - .locator('.p-toast-close-button') - .all() - for (const button of toastCloseButtons) { - await button.click() - } - await expect(this.visibleToasts).toHaveCount(0) - } - - async clickTextEncodeNode1() { - await this.canvas.click({ - position: { - x: 618, - y: 191 - } - }) - await this.nextFrame() - } - - async clickTextEncodeNodeToggler() { - await this.canvas.click({ - position: { - x: 430, - y: 171 - } - }) - await this.nextFrame() - } - - async clickTextEncodeNode2() { - await this.canvas.click({ - position: { - x: 622, - y: 400 - } - }) - await this.nextFrame() - } - - async clickEmptySpace() { - await this.canvas.click({ - position: { - x: 35, - y: 31 - } - }) - await this.nextFrame() - } - - async dragAndDrop(source: Position, target: Position) { - await this.page.mouse.move(source.x, source.y) - await this.page.mouse.down() - await this.page.mouse.move(target.x, target.y, { steps: 100 }) - await this.page.mouse.up() - await this.nextFrame() - } - - async dragAndDropExternalResource( - options: { - fileName?: string - url?: string - dropPosition?: Position - waitForUpload?: boolean - } = {} - ) { - const { - dropPosition = { x: 100, y: 100 }, - fileName, - url, - waitForUpload = false - } = options - - if (!fileName && !url) - throw new Error('Must provide either fileName or url') - - const evaluateParams: { - dropPosition: Position - fileName?: string - fileType?: string - buffer?: Uint8Array | number[] - url?: string - } = { dropPosition } - - // Dropping a file from the filesystem - if (fileName) { - const filePath = this.assetPath(fileName) - const buffer = fs.readFileSync(filePath) - - const getFileType = (fileName: string) => { - if (fileName.endsWith('.png')) return 'image/png' - if (fileName.endsWith('.svg')) return 'image/svg+xml' - if (fileName.endsWith('.webp')) return 'image/webp' - if (fileName.endsWith('.webm')) return 'video/webm' - if (fileName.endsWith('.json')) return 'application/json' - if (fileName.endsWith('.glb')) return 'model/gltf-binary' - if (fileName.endsWith('.avif')) return 'image/avif' - return 'application/octet-stream' - } - - evaluateParams.fileName = fileName - evaluateParams.fileType = getFileType(fileName) - evaluateParams.buffer = [...new Uint8Array(buffer)] - } - - // Dropping a URL (e.g., dropping image across browser tabs in Firefox) - if (url) evaluateParams.url = url - - // Set up response waiter for file uploads before triggering the drop - const uploadResponsePromise = waitForUpload - ? this.page.waitForResponse( - (resp) => resp.url().includes('/upload/') && resp.status() === 200, - { timeout: 10000 } - ) - : null - - // Execute the drag and drop in the browser - await this.page.evaluate(async (params) => { - const dataTransfer = new DataTransfer() - - // Add file if provided - if (params.buffer && params.fileName && params.fileType) { - const file = new File( - [new Uint8Array(params.buffer)], - params.fileName, - { - type: params.fileType - } - ) - dataTransfer.items.add(file) - } - - // Add URL data if provided - if (params.url) { - dataTransfer.setData('text/uri-list', params.url) - dataTransfer.setData('text/x-moz-url', params.url) - } - - const targetElement = document.elementFromPoint( - params.dropPosition.x, - params.dropPosition.y - ) - - if (!targetElement) { - console.error('No element found at drop position:', params.dropPosition) - return { success: false, error: 'No element at position' } - } - - const eventOptions = { - bubbles: true, - cancelable: true, - dataTransfer, - clientX: params.dropPosition.x, - clientY: params.dropPosition.y - } - - const dragOverEvent = new DragEvent('dragover', eventOptions) - const dropEvent = new DragEvent('drop', eventOptions) - - Object.defineProperty(dropEvent, 'preventDefault', { - value: () => {}, - writable: false - }) - - Object.defineProperty(dropEvent, 'stopPropagation', { - value: () => {}, - writable: false - }) - - targetElement.dispatchEvent(dragOverEvent) - targetElement.dispatchEvent(dropEvent) - - return { - success: true, - targetInfo: { - tagName: targetElement.tagName, - id: targetElement.id, - classList: Array.from(targetElement.classList) - } - } - }, evaluateParams) - - // Wait for file upload to complete - if (uploadResponsePromise) { - await uploadResponsePromise - } - - await this.nextFrame() - } - - async dragAndDropFile( - fileName: string, - options: { dropPosition?: Position; waitForUpload?: boolean } = {} - ) { - return this.dragAndDropExternalResource({ fileName, ...options }) - } - - async dragAndDropURL(url: string, options: { dropPosition?: Position } = {}) { - return this.dragAndDropExternalResource({ url, ...options }) - } - - async dragNode2() { - await this.dragAndDrop({ x: 622, y: 400 }, { x: 622, y: 300 }) - await this.nextFrame() - } - - // Default graph positions - get clipTextEncodeNode1InputSlot(): Position { - return { x: 427, y: 198 } - } - - get clipTextEncodeNode2InputSlot(): Position { - return { x: 422, y: 402 } - } - - // A point on input edge. - get clipTextEncodeNode2InputLinkPath(): Position { - return { - x: 395, - y: 422 - } - } - - get loadCheckpointNodeClipOutputSlot(): Position { - return { x: 332, y: 509 } - } - - get emptySpace(): Position { - return { x: 427, y: 98 } - } - - get promptDialogInput() { - return this.page.locator('.p-dialog-content input[type="text"]') - } - - async fillPromptDialog(value: string) { - await this.promptDialogInput.fill(value) - await this.page.keyboard.press('Enter') - await this.promptDialogInput.waitFor({ state: 'hidden' }) - await this.nextFrame() - } - - async disconnectEdge() { - await this.dragAndDrop(this.clipTextEncodeNode1InputSlot, this.emptySpace) - } - - async connectEdge( - options: { - reverse?: boolean - } = {} - ) { - const { reverse = false } = options - const start = reverse - ? this.clipTextEncodeNode1InputSlot - : this.loadCheckpointNodeClipOutputSlot - const end = reverse - ? this.loadCheckpointNodeClipOutputSlot - : this.clipTextEncodeNode1InputSlot - - await this.dragAndDrop(start, end) - } - - async adjustWidgetValue() { - // Adjust Empty Latent Image's width input. - const page = this.page - await page.locator('#graph-canvas').click({ - position: { - x: 724, - y: 645 - } - }) - const dialogInput = page.locator('.graphdialog input[type="text"]') - await dialogInput.click() - await dialogInput.fill('128') - await dialogInput.press('Enter') - await this.nextFrame() - } - - async zoom(deltaY: number, steps: number = 1) { - await this.page.mouse.move(10, 10) - for (let i = 0; i < steps; i++) { - await this.page.mouse.wheel(0, deltaY) - } - await this.nextFrame() - } - - async pan(offset: Position, safeSpot?: Position) { - safeSpot = safeSpot || { x: 10, y: 10 } - await this.page.mouse.move(safeSpot.x, safeSpot.y) - await this.page.mouse.down() - await this.page.mouse.move(offset.x + safeSpot.x, offset.y + safeSpot.y) - await this.page.mouse.up() - await this.nextFrame() - } - - async panWithTouch(offset: Position, safeSpot?: Position) { - safeSpot = safeSpot || { x: 10, y: 10 } - const client = await this.page.context().newCDPSession(this.page) - await client.send('Input.dispatchTouchEvent', { - type: 'touchStart', - touchPoints: [safeSpot] - }) - await client.send('Input.dispatchTouchEvent', { - type: 'touchMove', - touchPoints: [{ x: offset.x + safeSpot.x, y: offset.y + safeSpot.y }] - }) - await client.send('Input.dispatchTouchEvent', { - type: 'touchEnd', - touchPoints: [] - }) - await this.nextFrame() - } - - async rightClickCanvas(x: number = 10, y: number = 10) { - await this.page.mouse.click(x, y, { button: 'right' }) - await this.nextFrame() - } - - async clickContextMenuItem(name: string): Promise { - await this.page.getByRole('menuitem', { name }).click() - await this.nextFrame() - } - - /** - * Clicks on a litegraph context menu item (uses .litemenu-entry selector). - * Use this for canvas/node context menus, not PrimeVue menus. - */ - async clickLitegraphContextMenuItem(name: string): Promise { - await this.page.locator(`.litemenu-entry:has-text("${name}")`).click() - await this.nextFrame() - } - - /** - * Core helper method for interacting with subgraph I/O slots. - * Handles both input/output slots and both right-click/double-click actions. - * - * @param slotType - 'input' or 'output' - * @param action - 'rightClick' or 'doubleClick' - * @param slotName - Optional specific slot name to target - * @private - */ - private async interactWithSubgraphSlot( - slotType: 'input' | 'output', - action: 'rightClick' | 'doubleClick', - slotName?: string - ): Promise { - const foundSlot = await this.page.evaluate( - async (params) => { - const { slotType, action, targetSlotName } = params - const app = window['app'] - const currentGraph = app.canvas.graph - - // Check if we're in a subgraph - if (currentGraph.constructor.name !== 'Subgraph') { - throw new Error( - 'Not in a subgraph - this method only works inside subgraphs' - ) - } - - // Get the appropriate node and slots - const node = - slotType === 'input' - ? currentGraph.inputNode - : currentGraph.outputNode - const slots = - slotType === 'input' ? currentGraph.inputs : currentGraph.outputs - - if (!node) { - throw new Error(`No ${slotType} node found in subgraph`) - } - - if (!slots || slots.length === 0) { - throw new Error(`No ${slotType} slots found in subgraph`) - } - - // Filter slots based on target name and action type - const slotsToTry = targetSlotName - ? slots.filter((slot) => slot.name === targetSlotName) - : action === 'rightClick' - ? slots - : [slots[0]] // Right-click tries all, double-click uses first - - if (slotsToTry.length === 0) { - throw new Error( - targetSlotName - ? `${slotType} slot '${targetSlotName}' not found` - : `No ${slotType} slots available to try` - ) - } - - // Handle the interaction based on action type - if (action === 'rightClick') { - // Right-click: try each slot until one works - for (const slot of slotsToTry) { - if (!slot.pos) continue - - const event = { - canvasX: slot.pos[0], - canvasY: slot.pos[1], - button: 2, // Right mouse button - preventDefault: () => {}, - stopPropagation: () => {} - } - - if (node.onPointerDown) { - node.onPointerDown( - event, - app.canvas.pointer, - app.canvas.linkConnector - ) - } - - // Wait briefly for menu to appear - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Check if context menu appeared - const menuExists = document.querySelector('.litemenu-entry') - if (menuExists) { - return { - success: true, - slotName: slot.name, - x: slot.pos[0], - y: slot.pos[1] - } - } - } - } else if (action === 'doubleClick') { - // Double-click: use first slot with bounding rect center - const slot = slotsToTry[0] - if (!slot.boundingRect) { - throw new Error(`${slotType} slot bounding rect not found`) - } - - const rect = slot.boundingRect - const testX = rect[0] + rect[2] / 2 // x + width/2 - const testY = rect[1] + rect[3] / 2 // y + height/2 - - const event = { - canvasX: testX, - canvasY: testY, - button: 0, // Left mouse button - preventDefault: () => {}, - stopPropagation: () => {} - } - - if (node.onPointerDown) { - node.onPointerDown( - event, - app.canvas.pointer, - app.canvas.linkConnector - ) - - // Trigger double-click - if (app.canvas.pointer.onDoubleClick) { - app.canvas.pointer.onDoubleClick(event) - } - } - - // Wait briefly for dialog to appear - await new Promise((resolve) => setTimeout(resolve, 200)) - - return { success: true, slotName: slot.name, x: testX, y: testY } - } - - return { success: false } - }, - { slotType, action, targetSlotName: slotName } - ) - - if (!foundSlot.success) { - const actionText = - action === 'rightClick' ? 'open context menu for' : 'double-click' - throw new Error( - slotName - ? `Could not ${actionText} ${slotType} slot '${slotName}'` - : `Could not find any ${slotType} slot to ${actionText}` - ) - } - - // Wait for the appropriate UI element to appear - if (action === 'rightClick') { - await this.page.waitForSelector('.litemenu-entry', { - state: 'visible', - timeout: 5000 - }) - } else { - await this.nextFrame() - } - } - - /** - * Right-clicks on a subgraph input slot to open the context menu. - * Must be called when inside a subgraph. - * - * This method uses the actual slot positions from the subgraph.inputs array, - * which contain the correct coordinates for each input slot. These positions - * are different from the visual node positions and are specifically where - * the slots are rendered on the input node. - * - * @param inputName Optional name of the specific input slot to target (e.g., 'text'). - * If not provided, tries all available input slots until one works. - * @returns Promise that resolves when the context menu appears - */ - async rightClickSubgraphInputSlot(inputName?: string): Promise { - return this.interactWithSubgraphSlot('input', 'rightClick', inputName) - } - - /** - * Right-clicks on a subgraph output slot to open the context menu. - * Must be called when inside a subgraph. - * - * Similar to rightClickSubgraphInputSlot but for output slots. - * - * @param outputName Optional name of the specific output slot to target. - * If not provided, tries all available output slots until one works. - * @returns Promise that resolves when the context menu appears - */ - async rightClickSubgraphOutputSlot(outputName?: string): Promise { - return this.interactWithSubgraphSlot('output', 'rightClick', outputName) - } - - /** - * Double-clicks on a subgraph input slot to rename it. - * Must be called when inside a subgraph. - * - * @param inputName Optional name of the specific input slot to target (e.g., 'text'). - * If not provided, tries the first available input slot. - * @returns Promise that resolves when the rename dialog appears - */ - async doubleClickSubgraphInputSlot(inputName?: string): Promise { - return this.interactWithSubgraphSlot('input', 'doubleClick', inputName) - } - - /** - * Double-clicks on a subgraph output slot to rename it. - * Must be called when inside a subgraph. - * - * @param outputName Optional name of the specific output slot to target. - * If not provided, tries the first available output slot. - * @returns Promise that resolves when the rename dialog appears - */ - async doubleClickSubgraphOutputSlot(outputName?: string): Promise { - return this.interactWithSubgraphSlot('output', 'doubleClick', outputName) - } - - /** - * Get a reference to a subgraph input slot - */ - async getSubgraphInputSlot( - slotName?: string - ): Promise { - return new SubgraphSlotReference('input', slotName || '', this) - } - - /** - * Get a reference to a subgraph output slot - */ - async getSubgraphOutputSlot( - slotName?: string - ): Promise { - return new SubgraphSlotReference('output', slotName || '', this) - } - - /** - * Connect a regular node output to a subgraph input. - * This creates a new input slot on the subgraph if targetInputName is not provided. - */ - async connectToSubgraphInput( - sourceNode: NodeReference, - sourceSlotIndex: number, - targetInputName?: string - ): Promise { - const sourceSlot = await sourceNode.getOutput(sourceSlotIndex) - const targetSlot = await this.getSubgraphInputSlot(targetInputName) - - const targetPosition = targetInputName - ? await targetSlot.getPosition() // Connect to existing slot - : await targetSlot.getOpenSlotPosition() // Create new slot - - await this.dragAndDrop(await sourceSlot.getPosition(), targetPosition) - await this.nextFrame() - } - - /** - * Connect a subgraph input to a regular node input. - * This creates a new input slot on the subgraph if sourceInputName is not provided. - */ - async connectFromSubgraphInput( - targetNode: NodeReference, - targetSlotIndex: number, - sourceInputName?: string - ): Promise { - const sourceSlot = await this.getSubgraphInputSlot(sourceInputName) - const targetSlot = await targetNode.getInput(targetSlotIndex) - - const sourcePosition = sourceInputName - ? await sourceSlot.getPosition() // Connect from existing slot - : await sourceSlot.getOpenSlotPosition() // Create new slot - - const targetPosition = await targetSlot.getPosition() - - await this.dragAndDrop(sourcePosition, targetPosition) - await this.nextFrame() - } - - /** - * Connect a regular node output to a subgraph output. - * This creates a new output slot on the subgraph if targetOutputName is not provided. - */ - async connectToSubgraphOutput( - sourceNode: NodeReference, - sourceSlotIndex: number, - targetOutputName?: string - ): Promise { - const sourceSlot = await sourceNode.getOutput(sourceSlotIndex) - const targetSlot = await this.getSubgraphOutputSlot(targetOutputName) - - const targetPosition = targetOutputName - ? await targetSlot.getPosition() // Connect to existing slot - : await targetSlot.getOpenSlotPosition() // Create new slot - - await this.dragAndDrop(await sourceSlot.getPosition(), targetPosition) - await this.nextFrame() - } - - /** - * Connect a subgraph output to a regular node input. - * This creates a new output slot on the subgraph if sourceOutputName is not provided. - */ - async connectFromSubgraphOutput( - targetNode: NodeReference, - targetSlotIndex: number, - sourceOutputName?: string - ): Promise { - const sourceSlot = await this.getSubgraphOutputSlot(sourceOutputName) - const targetSlot = await targetNode.getInput(targetSlotIndex) - - const sourcePosition = sourceOutputName - ? await sourceSlot.getPosition() // Connect from existing slot - : await sourceSlot.getOpenSlotPosition() // Create new slot - - await this.dragAndDrop(sourcePosition, await targetSlot.getPosition()) - await this.nextFrame() - } - - /** - * Add a visual marker at a position for debugging - */ - async debugAddMarker( - position: Position, - id: string = 'debug-marker' - ): Promise { - await this.page.evaluate( - ([pos, markerId]) => { - // Remove existing marker if present - const existing = document.getElementById(markerId) - if (existing) existing.remove() - - // Create marker - const marker = document.createElement('div') - marker.id = markerId - marker.style.position = 'fixed' - marker.style.left = `${pos.x - 10}px` - marker.style.top = `${pos.y - 10}px` - marker.style.width = '20px' - marker.style.height = '20px' - marker.style.border = '2px solid red' - marker.style.borderRadius = '50%' - marker.style.backgroundColor = 'rgba(255, 0, 0, 0.3)' - marker.style.pointerEvents = 'none' - marker.style.zIndex = '10000' - document.body.appendChild(marker) - }, - [position, id] as const - ) - } - - /** - * Remove debug markers - */ - async debugRemoveMarkers(): Promise { - await this.page.evaluate(() => { - document - .querySelectorAll('[id^="debug-marker"]') - .forEach((el) => el.remove()) - }) - } - - /** - * Take a screenshot and attach it to the test report for debugging - * This is a convenience method that combines screenshot capture and test attachment - * - * @param testInfo The Playwright TestInfo object (from test parameters) - * @param name Name for the attachment - * @param options Optional screenshot options (defaults to page screenshot) - */ - async debugAttachScreenshot( - testInfo: any, - name: string, - options?: { - fullPage?: boolean - element?: 'canvas' | 'page' - markers?: Array<{ position: Position; id?: string }> - } - ): Promise { - // Add markers if requested - if (options?.markers) { - for (const marker of options.markers) { - await this.debugAddMarker(marker.position, marker.id) - } - } - - // Take screenshot - default to page if not specified - let screenshot: Buffer - const targetElement = options?.element || 'page' - - if (targetElement === 'canvas') { - screenshot = await this.canvas.screenshot() - } else if (options?.fullPage) { - screenshot = await this.page.screenshot({ fullPage: true }) - } else { - screenshot = await this.page.screenshot() - } - - // Attach to test report - await testInfo.attach(name, { - body: screenshot, - contentType: 'image/png' - }) - - // Clean up markers if we added any - if (options?.markers) { - await this.debugRemoveMarkers() - } - } - - async doubleClickCanvas() { - await this.page.mouse.dblclick(10, 10, { delay: 5 }) - await this.nextFrame() - } - - /** - * Capture the canvas as a PNG and save it for debugging - */ - async debugSaveCanvasScreenshot(filename: string): Promise { - await this.page.evaluate(async (filename) => { - const canvas = document.getElementById( - 'graph-canvas' - ) as HTMLCanvasElement - if (!canvas) { - throw new Error('Canvas not found') - } - - // Convert canvas to blob - return new Promise((resolve) => { - canvas.toBlob(async (blob) => { - if (!blob) { - throw new Error('Failed to create blob from canvas') - } - - // Create a download link and trigger it - const url = URL.createObjectURL(blob) - const a = document.createElement('a') - a.href = url - a.download = filename - document.body.appendChild(a) - a.click() - document.body.removeChild(a) - URL.revokeObjectURL(url) - resolve() - }, 'image/png') - }) - }, filename) - } - - /** - * Capture canvas as base64 data URL for inspection - */ - async debugGetCanvasDataURL(): Promise { - return await this.page.evaluate(() => { - const canvas = document.getElementById( - 'graph-canvas' - ) as HTMLCanvasElement - if (!canvas) { - throw new Error('Canvas not found') - } - return canvas.toDataURL('image/png') - }) - } - - /** - * Create an overlay div with the canvas image for easier Playwright screenshot - */ - async debugShowCanvasOverlay(): Promise { - await this.page.evaluate(() => { - const canvas = document.getElementById( - 'graph-canvas' - ) as HTMLCanvasElement - if (!canvas) { - throw new Error('Canvas not found') - } - - // Remove existing overlay if present - const existingOverlay = document.getElementById('debug-canvas-overlay') - if (existingOverlay) { - existingOverlay.remove() - } - - // Create overlay div - const overlay = document.createElement('div') - overlay.id = 'debug-canvas-overlay' - overlay.style.position = 'fixed' - overlay.style.top = '0' - overlay.style.left = '0' - overlay.style.zIndex = '9999' - overlay.style.backgroundColor = 'white' - overlay.style.padding = '10px' - overlay.style.border = '2px solid red' - - // Create image from canvas - const img = document.createElement('img') - img.src = canvas.toDataURL('image/png') - img.style.maxWidth = '800px' - img.style.maxHeight = '600px' - overlay.appendChild(img) - - document.body.appendChild(overlay) - }) - } - - /** - * Remove the debug canvas overlay - */ - async debugHideCanvasOverlay(): Promise { - await this.page.evaluate(() => { - const overlay = document.getElementById('debug-canvas-overlay') - if (overlay) { - overlay.remove() - } - }) - } - - async clickEmptyLatentNode() { - await this.canvas.click({ - position: { - x: 724, - y: 625 - } - }) - await this.page.mouse.move(10, 10) - await this.nextFrame() - } - - async rightClickEmptyLatentNode() { - await this.canvas.click({ - position: { - x: 724, - y: 645 - }, - button: 'right' - }) - await this.page.mouse.move(10, 10) - await this.nextFrame() - } - - async selectNodes(nodeTitles: string[]) { - await this.page.keyboard.down('Control') - for (const nodeTitle of nodeTitles) { - const nodes = await this.getNodeRefsByTitle(nodeTitle) - for (const node of nodes) { - await node.click('title') - } - } - await this.page.keyboard.up('Control') - await this.nextFrame() - } - - async select2Nodes() { - // Select 2 CLIP nodes. - await this.page.keyboard.down('Control') - await this.clickTextEncodeNode1() - await this.clickTextEncodeNode2() - await this.page.keyboard.up('Control') - await this.nextFrame() - } - - async ctrlSend(keyToPress: string, locator: Locator | null = this.canvas) { - const target = locator ?? this.page.keyboard - await target.press(`Control+${keyToPress}`) - await this.nextFrame() - } - - async ctrlA(locator?: Locator | null) { - await this.ctrlSend('KeyA', locator) - } - - async ctrlB(locator?: Locator | null) { - await this.ctrlSend('KeyB', locator) - } - - async ctrlC(locator?: Locator | null) { - await this.ctrlSend('KeyC', locator) - } - - async ctrlV(locator?: Locator | null) { - await this.ctrlSend('KeyV', locator) - } - - async ctrlZ(locator?: Locator | null) { - await this.ctrlSend('KeyZ', locator) - } - - async ctrlY(locator?: Locator | null) { - await this.ctrlSend('KeyY', locator) - } - - async ctrlArrowUp(locator?: Locator | null) { - await this.ctrlSend('ArrowUp', locator) - } - - async ctrlArrowDown(locator?: Locator | null) { - await this.ctrlSend('ArrowDown', locator) - } - async closeMenu() { await this.page.click('button.comfy-close-menu-btn') await this.nextFrame() } - async closeDialog() { - await this.page.locator('.p-dialog-close-button').click({ force: true }) - await expect(this.page.locator('.p-dialog')).toBeHidden() - } - - async resizeNode( - nodePos: Position, - nodeSize: Size, - ratioX: number, - ratioY: number, - revertAfter: boolean = false - ) { - const bottomRight = { - x: nodePos.x + nodeSize.width, - y: nodePos.y + nodeSize.height - } - const target = { - x: nodePos.x + nodeSize.width * ratioX, - y: nodePos.y + nodeSize.height * ratioY - } - // -1 to be inside the node. -2 because nodes currently get an arbitrary +1 to width. - await this.dragAndDrop( - { x: bottomRight.x - 2, y: bottomRight.y - 1 }, - target - ) - await this.nextFrame() - if (revertAfter) { - await this.dragAndDrop({ x: target.x - 2, y: target.y - 1 }, bottomRight) - await this.nextFrame() - } - } - - async resizeKsamplerNode( - percentX: number, - percentY: number, - revertAfter: boolean = false - ) { - const ksamplerPos = { - x: 863, - y: 156 - } - const ksamplerSize = { - width: 315, - height: 292 - } - return this.resizeNode( - ksamplerPos, - ksamplerSize, - percentX, - percentY, - revertAfter - ) - } - - async resizeLoadCheckpointNode( - percentX: number, - percentY: number, - revertAfter: boolean = false - ) { - const loadCheckpointPos = { - x: 26, - y: 444 - } - const loadCheckpointSize = { - width: 315, - height: 127 - } - return this.resizeNode( - loadCheckpointPos, - loadCheckpointSize, - percentX, - percentY, - revertAfter - ) - } - - async resizeEmptyLatentNode( - percentX: number, - percentY: number, - revertAfter: boolean = false - ) { - const emptyLatentPos = { - x: 473, - y: 579 - } - const emptyLatentSize = { - width: 315, - height: 136 - } - return this.resizeNode( - emptyLatentPos, - emptyLatentSize, - percentX, - percentY, - revertAfter - ) - } - async clickDialogButton(prompt: string, buttonText: string = 'Yes') { const modal = this.page.locator( `.comfy-modal-content:has-text("${prompt}")` ) - await expect(modal).toBeVisible() + await modal.waitFor({ state: 'visible' }) await modal .locator('.comfyui-button', { hasText: buttonText }) .click() - await expect(modal).toBeHidden() - } - - async convertAllNodesToGroupNode(groupNodeName: string) { - await this.canvas.press('Control+a') - const node = await this.getFirstNodeRef() - await node!.clickContextMenuOption('Convert to Group Node') - await this.fillPromptDialog(groupNodeName) - await this.nextFrame() - } - - async convertOffsetToCanvas(pos: [number, number]) { - return this.page.evaluate((pos) => { - return window['app'].canvas.ds.convertOffsetToCanvas(pos) - }, pos) + await modal.waitFor({ state: 'hidden' }) } /** Get number of DOM widgets on the canvas. */ @@ -1575,142 +388,12 @@ export class ComfyPage { return await this.page.locator('.dom-widget').count() } - async getNodeRefById(id: NodeId) { - return new NodeReference(id, this) - } - async getNodes(): Promise { - return await this.page.evaluate(() => { - return window['app'].graph.nodes - }) - } - async waitForGraphNodes(count: number) { - await this.page.waitForFunction((count) => { - return window['app']?.canvas.graph?.nodes?.length === count - }, count) - } - async getNodeRefsByType( - type: string, - includeSubgraph: boolean = false - ): Promise { - return Promise.all( - ( - await this.page.evaluate( - ({ type, includeSubgraph }) => { - const graph = ( - includeSubgraph ? window['app'].canvas.graph : window['app'].graph - ) as LGraph - const nodes = graph.nodes - return nodes - .filter((n: LGraphNode) => n.type === type) - .map((n: LGraphNode) => n.id) - }, - { type, includeSubgraph } - ) - ).map((id: NodeId) => this.getNodeRefById(id)) - ) - } - async getNodeRefsByTitle(title: string): Promise { - return Promise.all( - ( - await this.page.evaluate((title) => { - return window['app'].graph.nodes - .filter((n: LGraphNode) => n.title === title) - .map((n: LGraphNode) => n.id) - }, title) - ).map((id: NodeId) => this.getNodeRefById(id)) - ) - } - - async getFirstNodeRef(): Promise { - const id = await this.page.evaluate(() => { - return window['app'].graph.nodes[0]?.id - }) - if (!id) return null - return this.getNodeRefById(id) - } - async moveMouseToEmptyArea() { - await this.page.mouse.move(10, 10) - } - async getUndoQueueSize() { - return this.page.evaluate(() => { - const workflow = (window['app'].extensionManager as WorkspaceStore) - .workflow.activeWorkflow - return workflow?.changeTracker.undoQueue.length - }) - } - async getRedoQueueSize() { - return this.page.evaluate(() => { - const workflow = (window['app'].extensionManager as WorkspaceStore) - .workflow.activeWorkflow - return workflow?.changeTracker.redoQueue.length - }) - } - async isCurrentWorkflowModified() { - return this.page.evaluate(() => { - return (window['app'].extensionManager as WorkspaceStore).workflow - .activeWorkflow?.isModified - }) - } - async getExportedWorkflow({ api = false }: { api?: boolean } = {}) { - return this.page.evaluate(async (api) => { - return (await window['app'].graphToPrompt())[api ? 'output' : 'workflow'] - }, api) - } async setFocusMode(focusMode: boolean) { await this.page.evaluate((focusMode) => { - window['app'].extensionManager.focusMode = focusMode + ;(window.app!.extensionManager as WorkspaceStore).focusMode = focusMode }, focusMode) await this.nextFrame() } - - /** - * Get the position of a group by title. - * @param title The title of the group to find - * @returns The group's canvas position - * @throws Error if group not found - */ - async getGroupPosition(title: string): Promise { - const pos = await this.page.evaluate((title) => { - const groups = window['app'].graph.groups - const group = groups.find((g: { title: string }) => g.title === title) - if (!group) return null - return { x: group.pos[0], y: group.pos[1] } - }, title) - if (!pos) throw new Error(`Group "${title}" not found`) - return pos - } - - /** - * Drag a group by its title. - * @param options.name The title of the group to drag - * @param options.deltaX Horizontal drag distance in screen pixels - * @param options.deltaY Vertical drag distance in screen pixels - */ - async dragGroup(options: { - name: string - deltaX: number - deltaY: number - }): Promise { - const { name, deltaX, deltaY } = options - const screenPos = await this.page.evaluate((title) => { - const app = window['app'] - const groups = app.graph.groups - const group = groups.find((g: { title: string }) => g.title === title) - if (!group) return null - // Position in the title area of the group - const clientPos = app.canvasPosToClientPos([ - group.pos[0] + 50, - group.pos[1] + 15 - ]) - return { x: clientPos[0], y: clientPos[1] } - }, name) - if (!screenPos) throw new Error(`Group "${name}" not found`) - - await this.dragAndDrop(screenPos, { - x: screenPos.x + deltaX, - y: screenPos.y + deltaY - }) - } } export const testComfySnapToGridGridSize = 50 @@ -1766,6 +449,7 @@ const makeMatcher = function ( type: string ) { return async function ( + this: ExpectMatcherState, node: NodeReference, options?: { timeout?: number; intervals?: number[] } ) { diff --git a/browser_tests/fixtures/VueNodeHelpers.ts b/browser_tests/fixtures/VueNodeHelpers.ts index 3c11cfda2..64d76481c 100644 --- a/browser_tests/fixtures/VueNodeHelpers.ts +++ b/browser_tests/fixtures/VueNodeHelpers.ts @@ -3,6 +3,7 @@ */ import type { Locator, Page } from '@playwright/test' +import { TestIds } from './selectors' import { VueNodeFixture } from './utils/vueNodeFixtures' export class VueNodeHelpers { @@ -148,9 +149,9 @@ export class VueNodeHelpers { * Get a specific widget by node title and widget name */ getWidgetByName(nodeTitle: string, widgetName: string): Locator { - return this.getNodeByTitle(nodeTitle).locator( - `_vue=[widget.name="${widgetName}"]` - ) + return this.getNodeByTitle(nodeTitle).getByLabel(widgetName, { + exact: true + }) } /** @@ -159,8 +160,8 @@ export class VueNodeHelpers { getInputNumberControls(widget: Locator) { return { input: widget.locator('input'), - decrementButton: widget.getByTestId('decrement'), - incrementButton: widget.getByTestId('increment') + decrementButton: widget.getByTestId(TestIds.widgets.decrement), + incrementButton: widget.getByTestId(TestIds.widgets.increment) } } @@ -170,7 +171,7 @@ export class VueNodeHelpers { */ async enterSubgraph(nodeId?: string): Promise { const locator = nodeId ? this.getNodeLocator(nodeId) : this.page - const editButton = locator.getByTestId('subgraph-enter-button') + const editButton = locator.getByTestId(TestIds.widgets.subgraphEnterButton) await editButton.click() } } diff --git a/browser_tests/fixtures/components/BaseDialog.ts b/browser_tests/fixtures/components/BaseDialog.ts new file mode 100644 index 000000000..62b4ff64c --- /dev/null +++ b/browser_tests/fixtures/components/BaseDialog.ts @@ -0,0 +1,31 @@ +import type { Locator, Page } from '@playwright/test' + +export class BaseDialog { + readonly root: Locator + readonly closeButton: Locator + + constructor( + public readonly page: Page, + testId?: string + ) { + this.root = testId ? page.getByTestId(testId) : page.locator('.p-dialog') + this.closeButton = this.root.getByRole('button', { name: 'Close' }) + } + + async isVisible(): Promise { + return this.root.isVisible() + } + + async waitForVisible(): Promise { + await this.root.waitFor({ state: 'visible' }) + } + + async waitForHidden(): Promise { + await this.root.waitFor({ state: 'hidden' }) + } + + async close(): Promise { + await this.closeButton.click({ force: true }) + await this.waitForHidden() + } +} diff --git a/browser_tests/fixtures/components/BottomPanel.ts b/browser_tests/fixtures/components/BottomPanel.ts new file mode 100644 index 000000000..85ef2694a --- /dev/null +++ b/browser_tests/fixtures/components/BottomPanel.ts @@ -0,0 +1,35 @@ +import type { Locator, Page } from '@playwright/test' + +class ShortcutsTab { + readonly essentialsTab: Locator + readonly viewControlsTab: Locator + readonly manageButton: Locator + readonly keyBadges: Locator + readonly subcategoryTitles: Locator + + constructor(readonly page: Page) { + this.essentialsTab = page.getByRole('tab', { name: /Essential/i }) + this.viewControlsTab = page.getByRole('tab', { name: /View Controls/i }) + this.manageButton = page.getByRole('button', { name: /Manage Shortcuts/i }) + this.keyBadges = page.locator('.key-badge') + this.subcategoryTitles = page.locator('.subcategory-title') + } +} + +export class BottomPanel { + readonly root: Locator + readonly keyboardShortcutsButton: Locator + readonly toggleButton: Locator + readonly shortcuts: ShortcutsTab + + constructor(readonly page: Page) { + this.root = page.locator('.bottom-panel') + this.keyboardShortcutsButton = page.getByRole('button', { + name: /Keyboard Shortcuts/i + }) + this.toggleButton = page.getByRole('button', { + name: /Toggle Bottom Panel/i + }) + this.shortcuts = new ShortcutsTab(page) + } +} diff --git a/browser_tests/fixtures/components/ComfyNodeSearchBox.ts b/browser_tests/fixtures/components/ComfyNodeSearchBox.ts index 59a33e254..f2a96fa8f 100644 --- a/browser_tests/fixtures/components/ComfyNodeSearchBox.ts +++ b/browser_tests/fixtures/components/ComfyNodeSearchBox.ts @@ -30,7 +30,7 @@ export class ComfyNodeSearchFilterSelectionPanel { async addFilter(filterValue: string, filterType: string) { await this.selectFilterType(filterType) await this.selectFilterValue(filterValue) - await this.page.locator('button:has-text("Add")').click() + await this.page.getByRole('button', { name: 'Add', exact: true }).click() } } diff --git a/browser_tests/fixtures/components/ContextMenu.ts b/browser_tests/fixtures/components/ContextMenu.ts new file mode 100644 index 000000000..c502ccdd5 --- /dev/null +++ b/browser_tests/fixtures/components/ContextMenu.ts @@ -0,0 +1,54 @@ +import type { Locator, Page } from '@playwright/test' + +export class ContextMenu { + constructor(public readonly page: Page) {} + + get primeVueMenu() { + return this.page.locator('.p-contextmenu, .p-menu') + } + + get litegraphMenu() { + return this.page.locator('.litemenu') + } + + get menuItems() { + return this.page.locator('.p-menuitem, .litemenu-entry') + } + + async clickMenuItem(name: string): Promise { + await this.page.getByRole('menuitem', { name }).click() + } + + async clickLitegraphMenuItem(name: string): Promise { + await this.page.locator(`.litemenu-entry:has-text("${name}")`).click() + } + + async isVisible(): Promise { + const primeVueVisible = await this.primeVueMenu + .isVisible() + .catch(() => false) + const litegraphVisible = await this.litegraphMenu + .isVisible() + .catch(() => false) + return primeVueVisible || litegraphVisible + } + + async waitForHidden(): Promise { + const waitIfExists = async (locator: Locator, menuName: string) => { + const count = await locator.count() + if (count > 0) { + await locator.waitFor({ state: 'hidden' }).catch((error: Error) => { + console.warn( + `[waitForHidden] ${menuName} waitFor failed:`, + error.message + ) + }) + } + } + + await Promise.all([ + waitIfExists(this.primeVueMenu, 'primeVueMenu'), + waitIfExists(this.litegraphMenu, 'litegraphMenu') + ]) + } +} diff --git a/browser_tests/fixtures/components/SettingDialog.ts b/browser_tests/fixtures/components/SettingDialog.ts index e9040a3a9..4587d85f3 100644 --- a/browser_tests/fixtures/components/SettingDialog.ts +++ b/browser_tests/fixtures/components/SettingDialog.ts @@ -1,20 +1,20 @@ import type { Page } from '@playwright/test' import type { ComfyPage } from '../ComfyPage' +import { TestIds } from '../selectors' +import { BaseDialog } from './BaseDialog' -export class SettingDialog { +export class SettingDialog extends BaseDialog { constructor( - public readonly page: Page, + page: Page, public readonly comfyPage: ComfyPage - ) {} - - get root() { - return this.page.locator('div.settings-container') + ) { + super(page, TestIds.dialogs.settings) } async open() { - await this.comfyPage.executeCommand('Comfy.ShowSettingsDialog') - await this.page.waitForSelector('div.settings-container') + await this.comfyPage.command.executeCommand('Comfy.ShowSettingsDialog') + await this.waitForVisible() } /** @@ -41,8 +41,9 @@ export class SettingDialog { } async goToAboutPanel() { - const aboutButton = this.page.locator('li[aria-label="About"]') - await aboutButton.click() - await this.page.waitForSelector('div.about-container') + await this.page.getByTestId(TestIds.dialogs.settingsTabAbout).click() + await this.page + .getByTestId(TestIds.dialogs.about) + .waitFor({ state: 'visible' }) } } diff --git a/browser_tests/fixtures/components/SidebarTab.ts b/browser_tests/fixtures/components/SidebarTab.ts index 3254e27c8..02da0b043 100644 --- a/browser_tests/fixtures/components/SidebarTab.ts +++ b/browser_tests/fixtures/components/SidebarTab.ts @@ -1,5 +1,8 @@ import type { Locator, Page } from '@playwright/test' +import type { WorkspaceStore } from '../../types/globals' +import { TestIds } from '../selectors' + class SidebarTab { constructor( public readonly page: Page, @@ -31,16 +34,16 @@ class SidebarTab { } export class NodeLibrarySidebarTab extends SidebarTab { - constructor(public readonly page: Page) { + constructor(public override readonly page: Page) { super(page, 'node-library') } get nodeLibrarySearchBoxInput() { - return this.page.locator('.node-lib-search-box input[type="text"]') + return this.page.getByPlaceholder('Search Nodes...') } get nodeLibraryTree() { - return this.page.locator('.node-lib-tree-explorer') + return this.page.getByTestId(TestIds.sidebar.nodeLibrary) } get nodePreview() { @@ -55,12 +58,12 @@ export class NodeLibrarySidebarTab extends SidebarTab { return this.tabContainer.locator('.new-folder-button') } - async open() { + override async open() { await super.open() await this.nodeLibraryTree.waitFor({ state: 'visible' }) } - async close() { + override async close() { if (!this.tabButton.isVisible()) { return } @@ -69,30 +72,40 @@ export class NodeLibrarySidebarTab extends SidebarTab { await this.nodeLibraryTree.waitFor({ state: 'hidden' }) } - folderSelector(folderName: string) { - return `.p-tree-node-content:has(> .tree-explorer-node-label:has(.tree-folder .node-label:has-text("${folderName}")))` - } - getFolder(folderName: string) { - return this.page.locator(this.folderSelector(folderName)) - } - - nodeSelector(nodeName: string) { - return `.p-tree-node-content:has(> .tree-explorer-node-label:has(.tree-leaf .node-label:has-text("${nodeName}")))` + return this.page.locator( + `[data-testid="node-tree-folder"][data-folder-name="${folderName}"]` + ) } getNode(nodeName: string) { - return this.page.locator(this.nodeSelector(nodeName)) + return this.page.locator( + `[data-testid="node-tree-leaf"][data-node-name="${nodeName}"]` + ) + } + + nodeSelector(nodeName: string): string { + return `[data-testid="node-tree-leaf"][data-node-name="${nodeName}"]` + } + + folderSelector(folderName: string): string { + return `[data-testid="node-tree-folder"][data-folder-name="${folderName}"]` + } + + getNodeInFolder(nodeName: string, folderName: string) { + return this.getFolder(folderName) + .locator('xpath=ancestor::li') + .locator(`[data-testid="node-tree-leaf"][data-node-name="${nodeName}"]`) } } export class WorkflowsSidebarTab extends SidebarTab { - constructor(public readonly page: Page) { + constructor(public override readonly page: Page) { super(page, 'workflows') } get root() { - return this.page.locator('.workflows-sidebar-tab') + return this.page.getByTestId(TestIds.sidebar.workflows) } async getOpenedWorkflowNames() { @@ -140,7 +153,9 @@ export class WorkflowsSidebarTab extends SidebarTab { // Wait for workflow service to finish renaming await this.page.waitForFunction( - () => !window['app']?.extensionManager?.workflow?.isBusy, + () => + !(window.app?.extensionManager as WorkspaceStore | undefined)?.workflow + ?.isBusy, undefined, { timeout: 3000 } ) diff --git a/browser_tests/fixtures/components/Topbar.ts b/browser_tests/fixtures/components/Topbar.ts index c5e7c8155..e91311c7b 100644 --- a/browser_tests/fixtures/components/Topbar.ts +++ b/browser_tests/fixtures/components/Topbar.ts @@ -1,5 +1,6 @@ import type { Locator, Page } from '@playwright/test' -import { expect } from '@playwright/test' + +import type { WorkspaceStore } from '../../types/globals' export class Topbar { private readonly menuLocator: Locator @@ -57,7 +58,7 @@ export class Topbar { async closeWorkflowTab(tabName: string) { const tab = this.getWorkflowTab(tabName) - await tab.locator('.close-button').click({ force: true }) + await tab.getByRole('button', { name: 'Close' }).click({ force: true }) } getSaveDialog(): Locator { @@ -86,7 +87,7 @@ export class Topbar { // Wait for workflow service to finish saving await this.page.waitForFunction( - () => !window['app'].extensionManager.workflow.isBusy, + () => !(window.app!.extensionManager as WorkspaceStore).workflow.isBusy, undefined, { timeout: 3000 } ) @@ -122,7 +123,7 @@ export class Topbar { */ async closeTopbarMenu() { await this.page.locator('body').click({ position: { x: 300, y: 10 } }) - await expect(this.menuLocator).not.toBeVisible() + await this.menuLocator.waitFor({ state: 'hidden' }) } /** diff --git a/browser_tests/fixtures/constants/defaultGraphPositions.ts b/browser_tests/fixtures/constants/defaultGraphPositions.ts new file mode 100644 index 000000000..576aecad6 --- /dev/null +++ b/browser_tests/fixtures/constants/defaultGraphPositions.ts @@ -0,0 +1,51 @@ +import type { Position } from './types' + +/** + * Hardcoded positions for the default graph loaded in tests. + * These coordinates are specific to the default workflow viewport. + */ +export const DefaultGraphPositions = { + // Node click positions + textEncodeNode1: { x: 618, y: 191 }, + textEncodeNode2: { x: 622, y: 400 }, + textEncodeNodeToggler: { x: 430, y: 171 }, + emptySpaceClick: { x: 35, y: 31 }, + + // Slot positions + clipTextEncodeNode1InputSlot: { x: 427, y: 198 }, + clipTextEncodeNode2InputSlot: { x: 422, y: 402 }, + clipTextEncodeNode2InputLinkPath: { x: 395, y: 422 }, + loadCheckpointNodeClipOutputSlot: { x: 332, y: 509 }, + emptySpace: { x: 427, y: 98 }, + + // Widget positions + emptyLatentWidgetClick: { x: 724, y: 645 }, + + // Node positions and sizes for resize operations + ksampler: { + pos: { x: 863, y: 156 }, + size: { width: 315, height: 292 } + }, + loadCheckpoint: { + pos: { x: 26, y: 444 }, + size: { width: 315, height: 127 } + }, + emptyLatent: { + pos: { x: 473, y: 579 }, + size: { width: 315, height: 136 } + } +} as const satisfies { + textEncodeNode1: Position + textEncodeNode2: Position + textEncodeNodeToggler: Position + emptySpaceClick: Position + clipTextEncodeNode1InputSlot: Position + clipTextEncodeNode2InputSlot: Position + clipTextEncodeNode2InputLinkPath: Position + loadCheckpointNodeClipOutputSlot: Position + emptySpace: Position + emptyLatentWidgetClick: Position + ksampler: { pos: Position; size: { width: number; height: number } } + loadCheckpoint: { pos: Position; size: { width: number; height: number } } + emptyLatent: { pos: Position; size: { width: number; height: number } } +} diff --git a/browser_tests/fixtures/constants/types.ts b/browser_tests/fixtures/constants/types.ts new file mode 100644 index 000000000..57aece7a2 --- /dev/null +++ b/browser_tests/fixtures/constants/types.ts @@ -0,0 +1,9 @@ +export interface Position { + x: number + y: number +} + +export interface Size { + width: number + height: number +} diff --git a/browser_tests/fixtures/helpers/CanvasHelper.ts b/browser_tests/fixtures/helpers/CanvasHelper.ts new file mode 100644 index 000000000..dfd730c8e --- /dev/null +++ b/browser_tests/fixtures/helpers/CanvasHelper.ts @@ -0,0 +1,184 @@ +import type { Locator, Page } from '@playwright/test' + +import { DefaultGraphPositions } from '../constants/defaultGraphPositions' +import type { Position } from '../types' + +export class CanvasHelper { + constructor( + private page: Page, + private canvas: Locator, + private resetViewButton: Locator + ) {} + + private async nextFrame(): Promise { + await this.page.evaluate(() => { + return new Promise(requestAnimationFrame) + }) + } + + async resetView(): Promise { + if (await this.resetViewButton.isVisible()) { + await this.resetViewButton.click() + } + await this.page.mouse.move(10, 10) + await this.nextFrame() + } + + async zoom(deltaY: number, steps: number = 1): Promise { + await this.page.mouse.move(10, 10) + for (let i = 0; i < steps; i++) { + await this.page.mouse.wheel(0, deltaY) + } + await this.nextFrame() + } + + async pan(offset: Position, safeSpot?: Position): Promise { + safeSpot = safeSpot || { x: 10, y: 10 } + await this.page.mouse.move(safeSpot.x, safeSpot.y) + await this.page.mouse.down() + await this.page.mouse.move(offset.x + safeSpot.x, offset.y + safeSpot.y) + await this.page.mouse.up() + await this.nextFrame() + } + + async panWithTouch(offset: Position, safeSpot?: Position): Promise { + safeSpot = safeSpot || { x: 10, y: 10 } + const client = await this.page.context().newCDPSession(this.page) + await client.send('Input.dispatchTouchEvent', { + type: 'touchStart', + touchPoints: [safeSpot] + }) + await client.send('Input.dispatchTouchEvent', { + type: 'touchMove', + touchPoints: [{ x: offset.x + safeSpot.x, y: offset.y + safeSpot.y }] + }) + await client.send('Input.dispatchTouchEvent', { + type: 'touchEnd', + touchPoints: [] + }) + await this.nextFrame() + } + + async rightClick(x: number = 10, y: number = 10): Promise { + await this.page.mouse.click(x, y, { button: 'right' }) + await this.nextFrame() + } + + async doubleClick(): Promise { + await this.page.mouse.dblclick(10, 10, { delay: 5 }) + await this.nextFrame() + } + + async click(position: Position): Promise { + await this.canvas.click({ position }) + await this.nextFrame() + } + + async clickEmptySpace(): Promise { + await this.canvas.click({ position: DefaultGraphPositions.emptySpaceClick }) + await this.nextFrame() + } + + async dragAndDrop(source: Position, target: Position): Promise { + await this.page.mouse.move(source.x, source.y) + await this.page.mouse.down() + await this.page.mouse.move(target.x, target.y, { steps: 100 }) + await this.page.mouse.up() + await this.nextFrame() + } + + async moveMouseToEmptyArea(): Promise { + await this.page.mouse.move(10, 10) + } + + async getScale(): Promise { + return this.page.evaluate(() => { + return window.app!.canvas.ds.scale + }) + } + + async setScale(scale: number): Promise { + await this.page.evaluate((s) => { + window.app!.canvas.ds.scale = s + }, scale) + await this.nextFrame() + } + + async convertOffsetToCanvas( + pos: [number, number] + ): Promise<[number, number]> { + return this.page.evaluate((pos) => { + return window.app!.canvas.ds.convertOffsetToCanvas(pos) + }, pos) + } + + async getNodeCenterByTitle(title: string): Promise { + return this.page.evaluate((title) => { + const app = window.app! + const node = app.graph.nodes.find( + (n: { title: string }) => n.title === title + ) + if (!node) return null + + const centerX = node.pos[0] + node.size[0] / 2 + const centerY = node.pos[1] + node.size[1] / 2 + const [clientX, clientY] = app.canvasPosToClientPos([centerX, centerY]) + return { x: clientX, y: clientY } + }, title) + } + + async getGroupPosition(title: string): Promise { + const pos = await this.page.evaluate((title) => { + const groups = window.app!.graph.groups + const group = groups.find((g: { title: string }) => g.title === title) + if (!group) return null + return { x: group.pos[0], y: group.pos[1] } + }, title) + if (!pos) throw new Error(`Group "${title}" not found`) + return pos + } + + async dragGroup(options: { + name: string + deltaX: number + deltaY: number + }): Promise { + const { name, deltaX, deltaY } = options + const screenPos = await this.page.evaluate((title) => { + const app = window.app! + const groups = app.graph.groups + const group = groups.find((g: { title: string }) => g.title === title) + if (!group) return null + const clientPos = app.canvasPosToClientPos([ + group.pos[0] + 50, + group.pos[1] + 15 + ]) + return { x: clientPos[0], y: clientPos[1] } + }, name) + if (!screenPos) throw new Error(`Group "${name}" not found`) + + await this.dragAndDrop(screenPos, { + x: screenPos.x + deltaX, + y: screenPos.y + deltaY + }) + } + + async disconnectEdge(): Promise { + await this.dragAndDrop( + DefaultGraphPositions.clipTextEncodeNode1InputSlot, + DefaultGraphPositions.emptySpace + ) + } + + async connectEdge(options: { reverse?: boolean } = {}): Promise { + const { reverse = false } = options + const start = reverse + ? DefaultGraphPositions.clipTextEncodeNode1InputSlot + : DefaultGraphPositions.loadCheckpointNodeClipOutputSlot + const end = reverse + ? DefaultGraphPositions.loadCheckpointNodeClipOutputSlot + : DefaultGraphPositions.clipTextEncodeNode1InputSlot + + await this.dragAndDrop(start, end) + } +} diff --git a/browser_tests/fixtures/helpers/ClipboardHelper.ts b/browser_tests/fixtures/helpers/ClipboardHelper.ts new file mode 100644 index 000000000..074c3d19f --- /dev/null +++ b/browser_tests/fixtures/helpers/ClipboardHelper.ts @@ -0,0 +1,15 @@ +import type { Locator } from '@playwright/test' + +import type { KeyboardHelper } from './KeyboardHelper' + +export class ClipboardHelper { + constructor(private readonly keyboard: KeyboardHelper) {} + + async copy(locator?: Locator | null): Promise { + await this.keyboard.ctrlSend('KeyC', locator ?? null) + } + + async paste(locator?: Locator | null): Promise { + await this.keyboard.ctrlSend('KeyV', locator ?? null) + } +} diff --git a/browser_tests/fixtures/helpers/CommandHelper.ts b/browser_tests/fixtures/helpers/CommandHelper.ts new file mode 100644 index 000000000..966f99b81 --- /dev/null +++ b/browser_tests/fixtures/helpers/CommandHelper.ts @@ -0,0 +1,76 @@ +import type { Page } from '@playwright/test' + +import type { KeyCombo } from '../../../src/platform/keybindings/types' + +export class CommandHelper { + constructor(private readonly page: Page) {} + + async executeCommand(commandId: string): Promise { + await this.page.evaluate((id: string) => { + return window.app!.extensionManager.command.execute(id) + }, commandId) + } + + async registerCommand( + commandId: string, + command: (() => void) | (() => Promise) + ): Promise { + // SECURITY: eval() is intentionally used here to deserialize/execute functions + // passed from controlled test code across the Node/Playwright browser boundary. + // Execution happens in isolated Playwright browser contexts with test-only data. + // This pattern is unsafe for production and must not be copied elsewhere. + await this.page.evaluate( + ({ commandId, commandStr }) => { + const app = window.app! + const randomSuffix = Math.random().toString(36).substring(2, 8) + const extensionName = `TestExtension_${randomSuffix}` + + app.registerExtension({ + name: extensionName, + commands: [ + { + id: commandId, + function: eval(commandStr) + } + ] + }) + }, + { commandId, commandStr: command.toString() } + ) + } + + async registerKeybinding( + keyCombo: KeyCombo, + command: () => void + ): Promise { + // SECURITY: eval() is intentionally used here to deserialize/execute functions + // passed from controlled test code across the Node/Playwright browser boundary. + // Execution happens in isolated Playwright browser contexts with test-only data. + // This pattern is unsafe for production and must not be copied elsewhere. + await this.page.evaluate( + ({ keyCombo, commandStr }) => { + const app = window.app! + const randomSuffix = Math.random().toString(36).substring(2, 8) + const extensionName = `TestExtension_${randomSuffix}` + const commandId = `TestCommand_${randomSuffix}` + + app.registerExtension({ + name: extensionName, + keybindings: [ + { + combo: keyCombo, + commandId: commandId + } + ], + commands: [ + { + id: commandId, + function: eval(commandStr) + } + ] + }) + }, + { keyCombo, commandStr: command.toString() } + ) + } +} diff --git a/browser_tests/fixtures/helpers/DebugHelper.ts b/browser_tests/fixtures/helpers/DebugHelper.ts new file mode 100644 index 000000000..819a876dd --- /dev/null +++ b/browser_tests/fixtures/helpers/DebugHelper.ts @@ -0,0 +1,167 @@ +import type { Locator, Page, TestInfo } from '@playwright/test' + +import type { Position } from '../types' + +export interface DebugScreenshotOptions { + fullPage?: boolean + element?: 'canvas' | 'page' + markers?: Array<{ position: Position; id?: string }> +} + +export class DebugHelper { + constructor( + private page: Page, + private canvas: Locator + ) {} + + async addMarker( + position: Position, + id: string = 'debug-marker' + ): Promise { + await this.page.evaluate( + ([pos, markerId]) => { + const existing = document.getElementById(markerId) + if (existing) existing.remove() + + const marker = document.createElement('div') + marker.id = markerId + marker.style.position = 'fixed' + marker.style.left = `${pos.x - 10}px` + marker.style.top = `${pos.y - 10}px` + marker.style.width = '20px' + marker.style.height = '20px' + marker.style.border = '2px solid red' + marker.style.borderRadius = '50%' + marker.style.backgroundColor = 'rgba(255, 0, 0, 0.3)' + marker.style.pointerEvents = 'none' + marker.style.zIndex = '10000' + document.body.appendChild(marker) + }, + [position, id] as const + ) + } + + async removeMarkers(): Promise { + await this.page.evaluate(() => { + document + .querySelectorAll('[id^="debug-marker"]') + .forEach((el) => el.remove()) + }) + } + + async attachScreenshot( + testInfo: TestInfo, + name: string, + options?: DebugScreenshotOptions + ): Promise { + if (options?.markers) { + for (const marker of options.markers) { + await this.addMarker(marker.position, marker.id) + } + } + + let screenshot: Buffer + const targetElement = options?.element || 'page' + + if (targetElement === 'canvas') { + screenshot = await this.canvas.screenshot() + } else if (options?.fullPage) { + screenshot = await this.page.screenshot({ fullPage: true }) + } else { + screenshot = await this.page.screenshot() + } + + await testInfo.attach(name, { + body: screenshot, + contentType: 'image/png' + }) + + if (options?.markers) { + await this.removeMarkers() + } + } + + async saveCanvasScreenshot(filename: string): Promise { + await this.page.evaluate(async (filename) => { + const canvas = document.getElementById( + 'graph-canvas' + ) as HTMLCanvasElement + if (!canvas) { + throw new Error('Canvas not found') + } + + return new Promise((resolve) => { + canvas.toBlob(async (blob) => { + if (!blob) { + throw new Error('Failed to create blob from canvas') + } + + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = filename + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + resolve() + }, 'image/png') + }) + }, filename) + } + + async getCanvasDataURL(): Promise { + return await this.page.evaluate(() => { + const canvas = document.getElementById( + 'graph-canvas' + ) as HTMLCanvasElement + if (!canvas) { + throw new Error('Canvas not found') + } + return canvas.toDataURL('image/png') + }) + } + + async showCanvasOverlay(): Promise { + await this.page.evaluate(() => { + const canvas = document.getElementById( + 'graph-canvas' + ) as HTMLCanvasElement + if (!canvas) { + throw new Error('Canvas not found') + } + + const existingOverlay = document.getElementById('debug-canvas-overlay') + if (existingOverlay) { + existingOverlay.remove() + } + + const overlay = document.createElement('div') + overlay.id = 'debug-canvas-overlay' + overlay.style.position = 'fixed' + overlay.style.top = '0' + overlay.style.left = '0' + overlay.style.zIndex = '9999' + overlay.style.backgroundColor = 'white' + overlay.style.padding = '10px' + overlay.style.border = '2px solid red' + + const img = document.createElement('img') + img.src = canvas.toDataURL('image/png') + img.style.maxWidth = '800px' + img.style.maxHeight = '600px' + overlay.appendChild(img) + + document.body.appendChild(overlay) + }) + } + + async hideCanvasOverlay(): Promise { + await this.page.evaluate(() => { + const overlay = document.getElementById('debug-canvas-overlay') + if (overlay) { + overlay.remove() + } + }) + } +} diff --git a/browser_tests/fixtures/helpers/DragDropHelper.ts b/browser_tests/fixtures/helpers/DragDropHelper.ts new file mode 100644 index 000000000..f55c73915 --- /dev/null +++ b/browser_tests/fixtures/helpers/DragDropHelper.ts @@ -0,0 +1,161 @@ +import { readFileSync } from 'fs' + +import type { Page } from '@playwright/test' + +import type { Position } from '../types' + +export class DragDropHelper { + constructor( + private readonly page: Page, + private readonly assetPath: (fileName: string) => string + ) {} + + private async nextFrame(): Promise { + await this.page.evaluate(() => { + return new Promise((resolve) => { + requestAnimationFrame(() => resolve()) + }) + }) + } + + async dragAndDropExternalResource( + options: { + fileName?: string + url?: string + dropPosition?: Position + waitForUpload?: boolean + } = {} + ): Promise { + const { + dropPosition = { x: 100, y: 100 }, + fileName, + url, + waitForUpload = false + } = options + + if (!fileName && !url) + throw new Error('Must provide either fileName or url') + + const evaluateParams: { + dropPosition: Position + fileName?: string + fileType?: string + buffer?: Uint8Array | number[] + url?: string + } = { dropPosition } + + if (fileName) { + const filePath = this.assetPath(fileName) + const buffer = readFileSync(filePath) + + const getFileType = (fileName: string) => { + if (fileName.endsWith('.png')) return 'image/png' + if (fileName.endsWith('.svg')) return 'image/svg+xml' + if (fileName.endsWith('.webp')) return 'image/webp' + if (fileName.endsWith('.webm')) return 'video/webm' + if (fileName.endsWith('.json')) return 'application/json' + if (fileName.endsWith('.glb')) return 'model/gltf-binary' + if (fileName.endsWith('.avif')) return 'image/avif' + return 'application/octet-stream' + } + + evaluateParams.fileName = fileName + evaluateParams.fileType = getFileType(fileName) + evaluateParams.buffer = [...new Uint8Array(buffer)] + } + + if (url) evaluateParams.url = url + + const uploadResponsePromise = waitForUpload + ? this.page.waitForResponse( + (resp) => resp.url().includes('/upload/') && resp.status() === 200, + { timeout: 10000 } + ) + : null + + await this.page.evaluate(async (params) => { + const dataTransfer = new DataTransfer() + + if (params.buffer && params.fileName && params.fileType) { + const file = new File( + [new Uint8Array(params.buffer)], + params.fileName, + { + type: params.fileType + } + ) + dataTransfer.items.add(file) + } + + if (params.url) { + dataTransfer.setData('text/uri-list', params.url) + dataTransfer.setData('text/x-moz-url', params.url) + } + + const targetElement = document.elementFromPoint( + params.dropPosition.x, + params.dropPosition.y + ) + + if (!targetElement) { + throw new Error( + `No element found at drop position: (${params.dropPosition.x}, ${params.dropPosition.y}). ` + + `document.elementFromPoint returned null. Ensure the target is visible and not obscured.` + ) + } + + const eventOptions = { + bubbles: true, + cancelable: true, + dataTransfer, + clientX: params.dropPosition.x, + clientY: params.dropPosition.y + } + + const dragOverEvent = new DragEvent('dragover', eventOptions) + const dropEvent = new DragEvent('drop', eventOptions) + + Object.defineProperty(dropEvent, 'preventDefault', { + value: () => {}, + writable: false + }) + + Object.defineProperty(dropEvent, 'stopPropagation', { + value: () => {}, + writable: false + }) + + targetElement.dispatchEvent(dragOverEvent) + targetElement.dispatchEvent(dropEvent) + + return { + success: true, + targetInfo: { + tagName: targetElement.tagName, + id: targetElement.id, + classList: Array.from(targetElement.classList) + } + } + }, evaluateParams) + + if (uploadResponsePromise) { + await uploadResponsePromise + } + + await this.nextFrame() + } + + async dragAndDropFile( + fileName: string, + options: { dropPosition?: Position; waitForUpload?: boolean } = {} + ): Promise { + return this.dragAndDropExternalResource({ fileName, ...options }) + } + + async dragAndDropURL( + url: string, + options: { dropPosition?: Position } = {} + ): Promise { + return this.dragAndDropExternalResource({ url, ...options }) + } +} diff --git a/browser_tests/fixtures/helpers/KeyboardHelper.ts b/browser_tests/fixtures/helpers/KeyboardHelper.ts new file mode 100644 index 000000000..b2c30346e --- /dev/null +++ b/browser_tests/fixtures/helpers/KeyboardHelper.ts @@ -0,0 +1,45 @@ +import type { Locator, Page } from '@playwright/test' + +export class KeyboardHelper { + constructor( + private readonly page: Page, + private readonly canvas: Locator + ) {} + + private async nextFrame(): Promise { + await this.page.evaluate(() => new Promise(requestAnimationFrame)) + } + + async ctrlSend( + keyToPress: string, + locator: Locator | null = this.canvas + ): Promise { + const target = locator ?? this.page.keyboard + await target.press(`Control+${keyToPress}`) + await this.nextFrame() + } + + async selectAll(locator?: Locator | null): Promise { + await this.ctrlSend('KeyA', locator) + } + + async bypass(locator?: Locator | null): Promise { + await this.ctrlSend('KeyB', locator) + } + + async undo(locator?: Locator | null): Promise { + await this.ctrlSend('KeyZ', locator) + } + + async redo(locator?: Locator | null): Promise { + await this.ctrlSend('KeyY', locator) + } + + async moveUp(locator?: Locator | null): Promise { + await this.ctrlSend('ArrowUp', locator) + } + + async moveDown(locator?: Locator | null): Promise { + await this.ctrlSend('ArrowDown', locator) + } +} diff --git a/browser_tests/fixtures/helpers/NodeOperationsHelper.ts b/browser_tests/fixtures/helpers/NodeOperationsHelper.ts new file mode 100644 index 000000000..5f11b473b --- /dev/null +++ b/browser_tests/fixtures/helpers/NodeOperationsHelper.ts @@ -0,0 +1,182 @@ +import type { Locator } from '@playwright/test' + +import type { + LGraph, + LGraphNode +} from '../../../src/lib/litegraph/src/litegraph' +import type { NodeId } from '../../../src/platform/workflow/validation/schemas/workflowSchema' +import type { ComfyPage } from '../ComfyPage' +import { DefaultGraphPositions } from '../constants/defaultGraphPositions' +import type { Position, Size } from '../types' +import { NodeReference } from '../utils/litegraphUtils' + +export class NodeOperationsHelper { + constructor(private comfyPage: ComfyPage) {} + + private get page() { + return this.comfyPage.page + } + + async getGraphNodesCount(): Promise { + return await this.page.evaluate(() => { + return window.app?.graph?.nodes?.length || 0 + }) + } + + async getSelectedGraphNodesCount(): Promise { + return await this.page.evaluate(() => { + return ( + window.app?.graph?.nodes?.filter( + (node: LGraphNode) => node.is_selected === true + ).length || 0 + ) + }) + } + + async getNodes(): Promise { + return await this.page.evaluate(() => { + return window.app!.graph.nodes + }) + } + + async waitForGraphNodes(count: number): Promise { + await this.page.waitForFunction((count) => { + return window.app?.canvas.graph?.nodes?.length === count + }, count) + } + + async getFirstNodeRef(): Promise { + const id = await this.page.evaluate(() => { + return window.app!.graph.nodes[0]?.id + }) + if (!id) return null + return this.getNodeRefById(id) + } + + async getNodeRefById(id: NodeId): Promise { + return new NodeReference(id, this.comfyPage) + } + + async getNodeRefsByType( + type: string, + includeSubgraph: boolean = false + ): Promise { + return Promise.all( + ( + await this.page.evaluate( + ({ type, includeSubgraph }) => { + const graph = ( + includeSubgraph ? window.app!.canvas.graph : window.app!.graph + ) as LGraph + const nodes = graph.nodes + return nodes + .filter((n: LGraphNode) => n.type === type) + .map((n: LGraphNode) => n.id) + }, + { type, includeSubgraph } + ) + ).map((id: NodeId) => this.getNodeRefById(id)) + ) + } + + async getNodeRefsByTitle(title: string): Promise { + return Promise.all( + ( + await this.page.evaluate((title) => { + return window + .app!.graph.nodes.filter((n: LGraphNode) => n.title === title) + .map((n: LGraphNode) => n.id) + }, title) + ).map((id: NodeId) => this.getNodeRefById(id)) + ) + } + + async selectNodes(nodeTitles: string[]): Promise { + await this.page.keyboard.down('Control') + try { + for (const nodeTitle of nodeTitles) { + const nodes = await this.getNodeRefsByTitle(nodeTitle) + for (const node of nodes) { + await node.click('title') + } + } + } finally { + await this.page.keyboard.up('Control') + await this.comfyPage.nextFrame() + } + } + + async resizeNode( + nodePos: Position, + nodeSize: Size, + ratioX: number, + ratioY: number, + revertAfter: boolean = false + ): Promise { + const bottomRight = { + x: nodePos.x + nodeSize.width, + y: nodePos.y + nodeSize.height + } + const target = { + x: nodePos.x + nodeSize.width * ratioX, + y: nodePos.y + nodeSize.height * ratioY + } + // -1 to be inside the node. -2 because nodes currently get an arbitrary +1 to width. + await this.comfyPage.canvasOps.dragAndDrop( + { x: bottomRight.x - 2, y: bottomRight.y - 1 }, + target + ) + await this.comfyPage.nextFrame() + if (revertAfter) { + await this.comfyPage.canvasOps.dragAndDrop( + { x: target.x - 2, y: target.y - 1 }, + bottomRight + ) + await this.comfyPage.nextFrame() + } + } + + async convertAllNodesToGroupNode(groupNodeName: string): Promise { + await this.comfyPage.canvas.press('Control+a') + const node = await this.getFirstNodeRef() + if (!node) { + throw new Error('No nodes found to convert') + } + await node.clickContextMenuOption('Convert to Group Node') + await this.fillPromptDialog(groupNodeName) + await this.comfyPage.nextFrame() + } + + get promptDialogInput(): Locator { + return this.page.locator('.p-dialog-content input[type="text"]') + } + + async fillPromptDialog(value: string): Promise { + await this.promptDialogInput.fill(value) + await this.page.keyboard.press('Enter') + await this.promptDialogInput.waitFor({ state: 'hidden' }) + await this.comfyPage.nextFrame() + } + + async dragTextEncodeNode2(): Promise { + await this.comfyPage.canvasOps.dragAndDrop( + DefaultGraphPositions.textEncodeNode2, + { + x: DefaultGraphPositions.textEncodeNode2.x, + y: 300 + } + ) + await this.comfyPage.nextFrame() + } + + async adjustEmptyLatentWidth(): Promise { + await this.page.locator('#graph-canvas').click({ + position: DefaultGraphPositions.emptyLatentWidgetClick + }) + const dialogInput = this.page.locator('.graphdialog input[type="text"]') + await dialogInput.click() + await dialogInput.fill('128') + await dialogInput.press('Enter') + await this.comfyPage.nextFrame() + } +} diff --git a/browser_tests/fixtures/helpers/SettingsHelper.ts b/browser_tests/fixtures/helpers/SettingsHelper.ts new file mode 100644 index 000000000..8ff512d95 --- /dev/null +++ b/browser_tests/fixtures/helpers/SettingsHelper.ts @@ -0,0 +1,20 @@ +import type { Page } from '@playwright/test' + +export class SettingsHelper { + constructor(private readonly page: Page) {} + + async setSetting(settingId: string, settingValue: unknown): Promise { + await this.page.evaluate( + async ({ id, value }) => { + await window.app!.extensionManager.setting.set(id, value) + }, + { id: settingId, value: settingValue } + ) + } + + async getSetting(settingId: string): Promise { + return (await this.page.evaluate(async (id) => { + return await window.app!.extensionManager.setting.get(id) + }, settingId)) as T + } +} diff --git a/browser_tests/fixtures/helpers/SubgraphHelper.ts b/browser_tests/fixtures/helpers/SubgraphHelper.ts new file mode 100644 index 000000000..eb74c2fb5 --- /dev/null +++ b/browser_tests/fixtures/helpers/SubgraphHelper.ts @@ -0,0 +1,325 @@ +import type { Page } from '@playwright/test' + +import type { + CanvasPointerEvent, + Subgraph +} from '@/lib/litegraph/src/litegraph' + +import type { ComfyPage } from '../ComfyPage' +import type { NodeReference } from '../utils/litegraphUtils' +import { SubgraphSlotReference } from '../utils/litegraphUtils' + +export class SubgraphHelper { + constructor(private readonly comfyPage: ComfyPage) {} + + private get page(): Page { + return this.comfyPage.page + } + + /** + * Core helper method for interacting with subgraph I/O slots. + * Handles both input/output slots and both right-click/double-click actions. + * + * @param slotType - 'input' or 'output' + * @param action - 'rightClick' or 'doubleClick' + * @param slotName - Optional specific slot name to target + */ + private async interactWithSubgraphSlot( + slotType: 'input' | 'output', + action: 'rightClick' | 'doubleClick', + slotName?: string + ): Promise { + const foundSlot = await this.page.evaluate( + async (params) => { + const { slotType, action, targetSlotName } = params + const app = window.app! + const currentGraph = app.canvas!.graph! + + // Check if we're in a subgraph + if (currentGraph.constructor.name !== 'Subgraph') { + throw new Error( + 'Not in a subgraph - this method only works inside subgraphs' + ) + } + + const subgraph = currentGraph as Subgraph + + // Get the appropriate node and slots + const node = + slotType === 'input' ? subgraph.inputNode : subgraph.outputNode + const slots = slotType === 'input' ? subgraph.inputs : subgraph.outputs + + if (!node) { + throw new Error(`No ${slotType} node found in subgraph`) + } + + if (!slots || slots.length === 0) { + throw new Error(`No ${slotType} slots found in subgraph`) + } + + // Filter slots based on target name and action type + const slotsToTry = targetSlotName + ? slots.filter((slot) => slot.name === targetSlotName) + : action === 'rightClick' + ? slots + : [slots[0]] // Right-click tries all, double-click uses first + + if (slotsToTry.length === 0) { + throw new Error( + targetSlotName + ? `${slotType} slot '${targetSlotName}' not found` + : `No ${slotType} slots available to try` + ) + } + + // Handle the interaction based on action type + if (action === 'rightClick') { + // Right-click: try each slot until one works + for (const slot of slotsToTry) { + if (!slot.pos) continue + + const event = { + canvasX: slot.pos[0], + canvasY: slot.pos[1], + button: 2, // Right mouse button + preventDefault: () => {}, + stopPropagation: () => {} + } + + if (node.onPointerDown) { + node.onPointerDown( + event as unknown as CanvasPointerEvent, + app.canvas.pointer, + app.canvas.linkConnector + ) + return { + success: true, + slotName: slot.name, + x: slot.pos[0], + y: slot.pos[1] + } + } + } + } else if (action === 'doubleClick') { + // Double-click: use first slot with bounding rect center + const slot = slotsToTry[0] + if (!slot.boundingRect) { + throw new Error(`${slotType} slot bounding rect not found`) + } + + const rect = slot.boundingRect + const testX = rect[0] + rect[2] / 2 // x + width/2 + const testY = rect[1] + rect[3] / 2 // y + height/2 + + const event = { + canvasX: testX, + canvasY: testY, + button: 0, // Left mouse button + preventDefault: () => {}, + stopPropagation: () => {} + } + + if (node.onPointerDown) { + node.onPointerDown( + event as unknown as CanvasPointerEvent, + app.canvas.pointer, + app.canvas.linkConnector + ) + + // Trigger double-click + if (app.canvas.pointer.onDoubleClick) { + app.canvas.pointer.onDoubleClick( + event as unknown as CanvasPointerEvent + ) + } + } + + return { success: true, slotName: slot.name, x: testX, y: testY } + } + + return { success: false } + }, + { slotType, action, targetSlotName: slotName } + ) + + if (!foundSlot.success) { + const actionText = + action === 'rightClick' ? 'open context menu for' : 'double-click' + throw new Error( + slotName + ? `Could not ${actionText} ${slotType} slot '${slotName}'` + : `Could not find any ${slotType} slot to ${actionText}` + ) + } + + // Wait for the appropriate UI element to appear + if (action === 'rightClick') { + await this.page.waitForSelector('.litemenu-entry', { + state: 'visible', + timeout: 5000 + }) + } else { + await this.comfyPage.nextFrame() + } + } + + /** + * Right-clicks on a subgraph input slot to open the context menu. + * Must be called when inside a subgraph. + * + * This method uses the actual slot positions from the subgraph.inputs array, + * which contain the correct coordinates for each input slot. These positions + * are different from the visual node positions and are specifically where + * the slots are rendered on the input node. + * + * @param inputName Optional name of the specific input slot to target (e.g., 'text'). + * If not provided, tries all available input slots until one works. + * @returns Promise that resolves when the context menu appears + */ + async rightClickInputSlot(inputName?: string): Promise { + return this.interactWithSubgraphSlot('input', 'rightClick', inputName) + } + + /** + * Right-clicks on a subgraph output slot to open the context menu. + * Must be called when inside a subgraph. + * + * Similar to rightClickInputSlot but for output slots. + * + * @param outputName Optional name of the specific output slot to target. + * If not provided, tries all available output slots until one works. + * @returns Promise that resolves when the context menu appears + */ + async rightClickOutputSlot(outputName?: string): Promise { + return this.interactWithSubgraphSlot('output', 'rightClick', outputName) + } + + /** + * Double-clicks on a subgraph input slot to rename it. + * Must be called when inside a subgraph. + * + * @param inputName Optional name of the specific input slot to target (e.g., 'text'). + * If not provided, tries the first available input slot. + * @returns Promise that resolves when the rename dialog appears + */ + async doubleClickInputSlot(inputName?: string): Promise { + return this.interactWithSubgraphSlot('input', 'doubleClick', inputName) + } + + /** + * Double-clicks on a subgraph output slot to rename it. + * Must be called when inside a subgraph. + * + * @param outputName Optional name of the specific output slot to target. + * If not provided, tries the first available output slot. + * @returns Promise that resolves when the rename dialog appears + */ + async doubleClickOutputSlot(outputName?: string): Promise { + return this.interactWithSubgraphSlot('output', 'doubleClick', outputName) + } + + /** + * Get a reference to a subgraph input slot + */ + getInputSlot(slotName?: string): SubgraphSlotReference { + return new SubgraphSlotReference('input', slotName || '', this.comfyPage) + } + + /** + * Get a reference to a subgraph output slot + */ + getOutputSlot(slotName?: string): SubgraphSlotReference { + return new SubgraphSlotReference('output', slotName || '', this.comfyPage) + } + + /** + * Connect a regular node output to a subgraph input. + * This creates a new input slot on the subgraph if targetInputName is not provided. + */ + async connectToInput( + sourceNode: NodeReference, + sourceSlotIndex: number, + targetInputName?: string + ): Promise { + const sourceSlot = await sourceNode.getOutput(sourceSlotIndex) + const targetSlot = this.getInputSlot(targetInputName) + + const targetPosition = targetInputName + ? await targetSlot.getPosition() // Connect to existing slot + : await targetSlot.getOpenSlotPosition() // Create new slot + + await this.comfyPage.canvasOps.dragAndDrop( + await sourceSlot.getPosition(), + targetPosition + ) + await this.comfyPage.nextFrame() + } + + /** + * Connect a subgraph input to a regular node input. + * This creates a new input slot on the subgraph if sourceInputName is not provided. + */ + async connectFromInput( + targetNode: NodeReference, + targetSlotIndex: number, + sourceInputName?: string + ): Promise { + const sourceSlot = this.getInputSlot(sourceInputName) + const targetSlot = await targetNode.getInput(targetSlotIndex) + + const sourcePosition = sourceInputName + ? await sourceSlot.getPosition() // Connect from existing slot + : await sourceSlot.getOpenSlotPosition() // Create new slot + + const targetPosition = await targetSlot.getPosition() + + await this.comfyPage.canvasOps.dragAndDrop(sourcePosition, targetPosition) + await this.comfyPage.nextFrame() + } + + /** + * Connect a regular node output to a subgraph output. + * This creates a new output slot on the subgraph if targetOutputName is not provided. + */ + async connectToOutput( + sourceNode: NodeReference, + sourceSlotIndex: number, + targetOutputName?: string + ): Promise { + const sourceSlot = await sourceNode.getOutput(sourceSlotIndex) + const targetSlot = this.getOutputSlot(targetOutputName) + + const targetPosition = targetOutputName + ? await targetSlot.getPosition() // Connect to existing slot + : await targetSlot.getOpenSlotPosition() // Create new slot + + await this.comfyPage.canvasOps.dragAndDrop( + await sourceSlot.getPosition(), + targetPosition + ) + await this.comfyPage.nextFrame() + } + + /** + * Connect a subgraph output to a regular node input. + * This creates a new output slot on the subgraph if sourceOutputName is not provided. + */ + async connectFromOutput( + targetNode: NodeReference, + targetSlotIndex: number, + sourceOutputName?: string + ): Promise { + const sourceSlot = this.getOutputSlot(sourceOutputName) + const targetSlot = await targetNode.getInput(targetSlotIndex) + + const sourcePosition = sourceOutputName + ? await sourceSlot.getPosition() // Connect from existing slot + : await sourceSlot.getOpenSlotPosition() // Create new slot + + await this.comfyPage.canvasOps.dragAndDrop( + sourcePosition, + await targetSlot.getPosition() + ) + await this.comfyPage.nextFrame() + } +} diff --git a/browser_tests/fixtures/helpers/ToastHelper.ts b/browser_tests/fixtures/helpers/ToastHelper.ts new file mode 100644 index 000000000..544e20316 --- /dev/null +++ b/browser_tests/fixtures/helpers/ToastHelper.ts @@ -0,0 +1,39 @@ +import { expect } from '@playwright/test' +import type { Locator, Page } from '@playwright/test' + +export class ToastHelper { + constructor(private readonly page: Page) {} + + get visibleToasts(): Locator { + return this.page.locator('.p-toast-message:visible') + } + + async getToastErrorCount(): Promise { + return await this.page + .locator('.p-toast-message.p-toast-message-error') + .count() + } + + async getVisibleToastCount(): Promise { + return await this.visibleToasts.count() + } + + async closeToasts(requireCount = 0): Promise { + if (requireCount) { + await this.visibleToasts + .nth(requireCount - 1) + .waitFor({ state: 'visible' }) + } + + // Clear all toasts + const toastCloseButtons = await this.page + .locator('.p-toast-close-button') + .all() + for (const button of toastCloseButtons) { + await button.click() + } + + // Assert all toasts are closed + await expect(this.visibleToasts).toHaveCount(0, { timeout: 1000 }) + } +} diff --git a/browser_tests/fixtures/helpers/WorkflowHelper.ts b/browser_tests/fixtures/helpers/WorkflowHelper.ts new file mode 100644 index 000000000..234e8bd63 --- /dev/null +++ b/browser_tests/fixtures/helpers/WorkflowHelper.ts @@ -0,0 +1,126 @@ +import { readFileSync } from 'fs' + +import type { + ComfyApiWorkflow, + ComfyWorkflowJSON +} from '../../../src/platform/workflow/validation/schemas/workflowSchema' +import type { WorkspaceStore } from '../../types/globals' +import type { ComfyPage } from '../ComfyPage' + +type FolderStructure = { + [key: string]: FolderStructure | string +} + +export class WorkflowHelper { + constructor(private readonly comfyPage: ComfyPage) {} + + convertLeafToContent(structure: FolderStructure): FolderStructure { + const result: FolderStructure = {} + + for (const [key, value] of Object.entries(structure)) { + if (typeof value === 'string') { + const filePath = this.comfyPage.assetPath(value) + result[key] = readFileSync(filePath, 'utf-8') + } else { + result[key] = this.convertLeafToContent(value) + } + } + + return result + } + + async setupWorkflowsDirectory(structure: FolderStructure) { + const resp = await this.comfyPage.request.post( + `${this.comfyPage.url}/api/devtools/setup_folder_structure`, + { + data: { + tree_structure: this.convertLeafToContent(structure), + base_path: `user/${this.comfyPage.id}/workflows` + } + } + ) + + if (resp.status() !== 200) { + throw new Error( + `Failed to setup workflows directory: ${await resp.text()}` + ) + } + + await this.comfyPage.page.evaluate(async () => { + await ( + window.app!.extensionManager as WorkspaceStore + ).workflow.syncWorkflows() + }) + + // Wait for Vue to re-render the workflow list + await this.comfyPage.nextFrame() + } + + async loadWorkflow(workflowName: string) { + await this.comfyPage.workflowUploadInput.setInputFiles( + this.comfyPage.assetPath(`${workflowName}.json`) + ) + await this.comfyPage.nextFrame() + } + + async deleteWorkflow( + workflowName: string, + whenMissing: 'ignoreMissing' | 'throwIfMissing' = 'ignoreMissing' + ) { + // Open workflows tab + const { workflowsTab } = this.comfyPage.menu + await workflowsTab.open() + + // Action to take if workflow missing + if (whenMissing === 'ignoreMissing') { + const workflows = await workflowsTab.getTopLevelSavedWorkflowNames() + if (!workflows.includes(workflowName)) return + } + + // Delete workflow + await workflowsTab.getPersistedItem(workflowName).click({ button: 'right' }) + await this.comfyPage.contextMenu.clickMenuItem('Delete') + await this.comfyPage.nextFrame() + await this.comfyPage.confirmDialog.delete.click() + + // Clear toast & close tab + await this.comfyPage.toast.closeToasts(1) + await workflowsTab.close() + } + + async getUndoQueueSize(): Promise { + return this.comfyPage.page.evaluate(() => { + const workflow = (window.app!.extensionManager as WorkspaceStore).workflow + .activeWorkflow + return workflow?.changeTracker.undoQueue.length + }) + } + + async getRedoQueueSize(): Promise { + return this.comfyPage.page.evaluate(() => { + const workflow = (window.app!.extensionManager as WorkspaceStore).workflow + .activeWorkflow + return workflow?.changeTracker.redoQueue.length + }) + } + + async isCurrentWorkflowModified(): Promise { + return this.comfyPage.page.evaluate(() => { + return (window.app!.extensionManager as WorkspaceStore).workflow + .activeWorkflow?.isModified + }) + } + + async getExportedWorkflow(options: { api: true }): Promise + async getExportedWorkflow(options?: { + api?: false + }): Promise + async getExportedWorkflow(options?: { + api?: boolean + }): Promise { + const api = options?.api ?? false + return this.comfyPage.page.evaluate(async (api) => { + return (await window.app!.graphToPrompt())[api ? 'output' : 'workflow'] + }, api) + } +} diff --git a/browser_tests/fixtures/selectors.ts b/browser_tests/fixtures/selectors.ts new file mode 100644 index 000000000..4ad8d1d82 --- /dev/null +++ b/browser_tests/fixtures/selectors.ts @@ -0,0 +1,77 @@ +/** + * Centralized test selectors for browser tests. + * Use data-testid attributes for stable selectors. + */ + +export const TestIds = { + sidebar: { + toolbar: 'side-toolbar', + nodeLibrary: 'node-library-tree', + nodeLibrarySearch: 'node-library-search', + workflows: 'workflows-sidebar', + modeToggle: 'mode-toggle' + }, + tree: { + folder: 'tree-folder', + leaf: 'tree-leaf', + node: 'tree-node' + }, + canvas: { + main: 'graph-canvas', + contextMenu: 'canvas-context-menu', + toggleMinimapButton: 'toggle-minimap-button', + toggleLinkVisibilityButton: 'toggle-link-visibility-button' + }, + dialogs: { + settings: 'settings-dialog', + settingsContainer: 'settings-container', + settingsTabAbout: 'settings-tab-about', + confirm: 'confirm-dialog', + about: 'about-panel', + whatsNewSection: 'whats-new-section' + }, + topbar: { + queueButton: 'queue-button', + saveButton: 'save-workflow-button' + }, + nodeLibrary: { + bookmarksSection: 'node-library-bookmarks-section' + }, + propertiesPanel: { + root: 'properties-panel' + }, + node: { + titleInput: 'node-title-input' + }, + widgets: { + decrement: 'decrement', + increment: 'increment', + subgraphEnterButton: 'subgraph-enter-button' + }, + templates: { + content: 'template-workflows-content', + workflowCard: (id: string) => `template-workflow-${id}` + }, + user: { + currentUserIndicator: 'current-user-indicator' + } +} as const + +/** + * Helper type for accessing nested TestIds (excludes function values) + */ +export type TestIdValue = + | (typeof TestIds.sidebar)[keyof typeof TestIds.sidebar] + | (typeof TestIds.tree)[keyof typeof TestIds.tree] + | (typeof TestIds.canvas)[keyof typeof TestIds.canvas] + | (typeof TestIds.dialogs)[keyof typeof TestIds.dialogs] + | (typeof TestIds.topbar)[keyof typeof TestIds.topbar] + | (typeof TestIds.nodeLibrary)[keyof typeof TestIds.nodeLibrary] + | (typeof TestIds.propertiesPanel)[keyof typeof TestIds.propertiesPanel] + | (typeof TestIds.node)[keyof typeof TestIds.node] + | (typeof TestIds.widgets)[keyof typeof TestIds.widgets] + | Exclude< + (typeof TestIds.templates)[keyof typeof TestIds.templates], + (id: string) => string + > + | (typeof TestIds.user)[keyof typeof TestIds.user] diff --git a/browser_tests/fixtures/utils/litegraphUtils.ts b/browser_tests/fixtures/utils/litegraphUtils.ts index ea5a0b78f..08e504150 100644 --- a/browser_tests/fixtures/utils/litegraphUtils.ts +++ b/browser_tests/fixtures/utils/litegraphUtils.ts @@ -1,3 +1,4 @@ +import { expect } from '@playwright/test' import type { Page } from '@playwright/test' import type { NodeId } from '../../../src/platform/workflow/validation/schemas/workflowSchema' @@ -22,10 +23,10 @@ export class SubgraphSlotReference { async getPosition(): Promise { const pos: [number, number] = await this.comfyPage.page.evaluate( ([type, slotName]) => { - const currentGraph = window['app'].canvas.graph + const currentGraph = window.app!.canvas.graph! - // Check if we're in a subgraph - if (currentGraph.constructor.name !== 'Subgraph') { + // Check if we're in a subgraph (subgraphs have inputNode property) + if (!('inputNode' in currentGraph)) { throw new Error( 'Not in a subgraph - this method only works inside subgraphs' ) @@ -51,7 +52,7 @@ export class SubgraphSlotReference { } // Convert from offset to canvas coordinates - const canvasPos = window['app'].canvas.ds.convertOffsetToCanvas([ + const canvasPos = window.app!.canvas.ds.convertOffsetToCanvas([ slot.pos[0], slot.pos[1] ]) @@ -69,9 +70,10 @@ export class SubgraphSlotReference { async getOpenSlotPosition(): Promise { const pos: [number, number] = await this.comfyPage.page.evaluate( ([type]) => { - const currentGraph = window['app'].canvas.graph + const currentGraph = window.app!.canvas.graph! - if (currentGraph.constructor.name !== 'Subgraph') { + // Check if we're in a subgraph (subgraphs have inputNode property) + if (!('inputNode' in currentGraph)) { throw new Error( 'Not in a subgraph - this method only works inside subgraphs' ) @@ -85,7 +87,7 @@ export class SubgraphSlotReference { } // Convert from offset to canvas coordinates - const canvasPos = window['app'].canvas.ds.convertOffsetToCanvas([ + const canvasPos = window.app!.canvas.ds.convertOffsetToCanvas([ node.emptySlot.pos[0], node.emptySlot.pos[1] ]) @@ -111,12 +113,12 @@ class NodeSlotReference { const pos: [number, number] = await this.node.comfyPage.page.evaluate( ([type, id, index]) => { // Use canvas.graph to get the current graph (works in both main graph and subgraphs) - const node = window['app'].canvas.graph.getNodeById(id) + const node = window.app!.canvas.graph!.getNodeById(id) if (!node) throw new Error(`Node ${id} not found.`) const rawPos = node.getConnectionPos(type === 'input', index) const convertedPos = - window['app'].canvas.ds.convertOffsetToCanvas(rawPos) + window.app!.canvas.ds!.convertOffsetToCanvas(rawPos) // Debug logging - convert Float64Arrays to regular arrays for visibility console.warn( @@ -126,7 +128,7 @@ class NodeSlotReference { nodeSize: [node.size[0], node.size[1]], rawConnectionPos: [rawPos[0], rawPos[1]], convertedPos: [convertedPos[0], convertedPos[1]], - currentGraphType: window['app'].canvas.graph.constructor.name + currentGraphType: window.app!.canvas.graph!.constructor.name } ) @@ -142,7 +144,7 @@ class NodeSlotReference { async getLinkCount() { return await this.node.comfyPage.page.evaluate( ([type, id, index]) => { - const node = window['app'].canvas.graph.getNodeById(id) + const node = window.app!.canvas.graph!.getNodeById(id) if (!node) throw new Error(`Node ${id} not found.`) if (type === 'input') { return node.inputs[index].link == null ? 0 : 1 @@ -155,7 +157,7 @@ class NodeSlotReference { async removeLinks() { await this.node.comfyPage.page.evaluate( ([type, id, index]) => { - const node = window['app'].canvas.graph.getNodeById(id) + const node = window.app!.canvas.graph!.getNodeById(id) if (!node) throw new Error(`Node ${id} not found.`) if (type === 'input') { node.disconnectInput(index) @@ -180,15 +182,15 @@ class NodeWidgetReference { async getPosition(): Promise { const pos: [number, number] = await this.node.comfyPage.page.evaluate( ([id, index]) => { - const node = window['app'].canvas.graph.getNodeById(id) + const node = window.app!.canvas.graph!.getNodeById(id) if (!node) throw new Error(`Node ${id} not found.`) - const widget = node.widgets[index] + const widget = node.widgets![index] if (!widget) throw new Error(`Widget ${index} not found.`) - const [x, y, w, h] = node.getBounding() - return window['app'].canvasPosToClientPos([ + const [x, y, w, _h] = node.getBounding() + return window.app!.canvasPosToClientPos([ x + w / 2, - y + window['LiteGraph']['NODE_TITLE_HEIGHT'] + widget.last_y + 1 + y + window.LiteGraph!['NODE_TITLE_HEIGHT'] + widget.last_y! + 1 ]) }, [this.node.id, this.index] as const @@ -205,9 +207,9 @@ class NodeWidgetReference { async getSocketPosition(): Promise { const pos: [number, number] = await this.node.comfyPage.page.evaluate( ([id, index]) => { - const node = window['app'].graph.getNodeById(id) + const node = window.app!.graph!.getNodeById(id) if (!node) throw new Error(`Node ${id} not found.`) - const widget = node.widgets[index] + const widget = node.widgets![index] if (!widget) throw new Error(`Widget ${index} not found.`) const slot = node.inputs.find( @@ -216,9 +218,9 @@ class NodeWidgetReference { if (!slot) throw new Error(`Socket ${widget.name} not found.`) const [x, y] = node.getBounding() - return window['app'].canvasPosToClientPos([ - x + slot.pos[0], - y + slot.pos[1] + window['LiteGraph']['NODE_TITLE_HEIGHT'] + return window.app!.canvasPosToClientPos([ + x + slot.pos![0], + y + slot.pos![1] + window.LiteGraph!['NODE_TITLE_HEIGHT'] ]) }, [this.node.id, this.index] as const @@ -239,7 +241,7 @@ class NodeWidgetReference { const pos = await this.getPosition() const canvas = this.node.comfyPage.canvas const canvasPos = (await canvas.boundingBox())! - await this.node.comfyPage.dragAndDrop( + await this.node.comfyPage.canvasOps.dragAndDrop( { x: canvasPos.x + pos.x, y: canvasPos.y + pos.y @@ -254,9 +256,9 @@ class NodeWidgetReference { async getValue() { return await this.node.comfyPage.page.evaluate( ([id, index]) => { - const node = window['app'].graph.getNodeById(id) + const node = window.app!.graph!.getNodeById(id) if (!node) throw new Error(`Node ${id} not found.`) - const widget = node.widgets[index] + const widget = node.widgets![index] if (!widget) throw new Error(`Widget ${index} not found.`) return widget.value }, @@ -271,7 +273,7 @@ export class NodeReference { ) {} async exists(): Promise { return await this.comfyPage.page.evaluate((id) => { - const node = window['app'].canvas.graph.getNodeById(id) + const node = window.app!.canvas.graph!.getNodeById(id) return !!node }, this.id) } @@ -279,7 +281,7 @@ export class NodeReference { return this.getProperty('type') } async getPosition(): Promise { - const pos = await this.comfyPage.convertOffsetToCanvas( + const pos = await this.comfyPage.canvasOps.convertOffsetToCanvas( await this.getProperty<[number, number]>('pos') ) return { @@ -288,12 +290,11 @@ export class NodeReference { } } async getBounding(): Promise { - const [x, y, width, height]: [number, number, number, number] = - await this.comfyPage.page.evaluate((id) => { - const node = window['app'].canvas.graph.getNodeById(id) - if (!node) throw new Error('Node not found') - return node.getBounding() - }, this.id) + const [x, y, width, height] = await this.comfyPage.page.evaluate((id) => { + const node = window.app!.canvas.graph!.getNodeById(id) + if (!node) throw new Error('Node not found') + return [...node.getBounding()] as [number, number, number, number] + }, this.id) return { x, y, @@ -311,6 +312,11 @@ export class NodeReference { async getFlags(): Promise<{ collapsed?: boolean; pinned?: boolean }> { return await this.getProperty('flags') } + async getTitlePosition(): Promise { + const nodePos = await this.getPosition() + const nodeSize = await this.getSize() + return { x: nodePos.x + nodeSize.width / 2, y: nodePos.y - 15 } + } async isPinned() { return !!(await this.getFlags()).pinned } @@ -323,9 +329,9 @@ export class NodeReference { async getProperty(prop: string): Promise { return await this.comfyPage.page.evaluate( ([id, prop]) => { - const node = window['app'].canvas.graph.getNodeById(id) + const node = window.app!.canvas.graph!.getNodeById(id) if (!node) throw new Error('Node not found') - return node[prop] + return (node as unknown as Record)[prop] }, [this.id, prop] as const ) @@ -343,16 +349,16 @@ export class NodeReference { position: 'title' | 'collapse', options?: Parameters[1] & { moveMouseToEmptyArea?: boolean } ) { - const nodePos = await this.getPosition() - const nodeSize = await this.getSize() let clickPos: Position switch (position) { case 'title': - clickPos = { x: nodePos.x + nodeSize.width / 2, y: nodePos.y - 15 } + clickPos = await this.getTitlePosition() break - case 'collapse': + case 'collapse': { + const nodePos = await this.getPosition() clickPos = { x: nodePos.x + 5, y: nodePos.y - 10 } break + } default: throw new Error(`Invalid click position ${position}`) } @@ -369,12 +375,12 @@ export class NodeReference { }) await this.comfyPage.nextFrame() if (moveMouseToEmptyArea) { - await this.comfyPage.moveMouseToEmptyArea() + await this.comfyPage.canvasOps.moveMouseToEmptyArea() } } async copy() { await this.click('title') - await this.comfyPage.ctrlC() + await this.comfyPage.clipboard.copy() await this.comfyPage.nextFrame() } async connectWidget( @@ -384,7 +390,7 @@ export class NodeReference { ) { const originSlot = await this.getOutput(originSlotIndex) const targetWidget = await targetNode.getWidget(targetWidgetIndex) - await this.comfyPage.dragAndDrop( + await this.comfyPage.canvasOps.dragAndDrop( await originSlot.getPosition(), await targetWidget.getSocketPosition() ) @@ -397,7 +403,7 @@ export class NodeReference { ) { const originSlot = await this.getOutput(originSlotIndex) const targetSlot = await targetNode.getInput(targetSlotIndex) - await this.comfyPage.dragAndDrop( + await this.comfyPage.canvasOps.dragAndDrop( await originSlot.getPosition(), await targetSlot.getPosition() ) @@ -415,9 +421,9 @@ export class NodeReference { } async convertToGroupNode(groupNodeName: string = 'GroupNode') { await this.clickContextMenuOption('Convert to Group Node') - await this.comfyPage.fillPromptDialog(groupNodeName) + await this.comfyPage.nodeOps.fillPromptDialog(groupNodeName) await this.comfyPage.nextFrame() - const nodes = await this.comfyPage.getNodeRefsByType( + const nodes = await this.comfyPage.nodeOps.getNodeRefsByType( `workflow>${groupNodeName}` ) if (nodes.length !== 1) { @@ -428,7 +434,8 @@ export class NodeReference { async convertToSubgraph() { await this.clickContextMenuOption('Convert to Subgraph') await this.comfyPage.nextFrame() - const nodes = await this.comfyPage.getNodeRefsByTitle('New Subgraph') + const nodes = + await this.comfyPage.nodeOps.getNodeRefsByTitle('New Subgraph') if (nodes.length !== 1) { throw new Error( `Did not find single subgraph node (found=${nodes.length})` @@ -446,7 +453,7 @@ export class NodeReference { } async navigateIntoSubgraph() { const titleHeight = await this.comfyPage.page.evaluate(() => { - return window['LiteGraph']['NODE_TITLE_HEIGHT'] + return window.LiteGraph!['NODE_TITLE_HEIGHT'] }) const nodePos = await this.getPosition() const nodeSize = await this.getSize() @@ -458,13 +465,14 @@ export class NodeReference { { x: nodePos.x + 20, y: nodePos.y + titleHeight + 5 } ] - let isInSubgraph = false - let attempts = 0 - const maxAttempts = 3 - - while (!isInSubgraph && attempts < maxAttempts) { - attempts++ + const checkIsInSubgraph = async () => { + return this.comfyPage.page.evaluate(() => { + const graph = window.app!.canvas.graph + return graph?.constructor?.name === 'Subgraph' + }) + } + await expect(async () => { for (const position of clickPositions) { // Clear any selection first await this.comfyPage.canvas.click({ @@ -477,24 +485,9 @@ export class NodeReference { await this.comfyPage.canvas.dblclick({ position, force: true }) await this.comfyPage.nextFrame() - // Check if we successfully entered the subgraph - isInSubgraph = await this.comfyPage.page.evaluate(() => { - const graph = window['app'].canvas.graph - return graph?.constructor?.name === 'Subgraph' - }) - - if (isInSubgraph) break + if (await checkIsInSubgraph()) return } - - if (!isInSubgraph && attempts < maxAttempts) { - await this.comfyPage.page.waitForTimeout(500) - } - } - - if (!isInSubgraph) { - throw new Error( - 'Failed to navigate into subgraph after ' + attempts + ' attempts' - ) - } + throw new Error('Not in subgraph yet') + }).toPass({ timeout: 5000, intervals: [100, 200, 500] }) } } diff --git a/browser_tests/fixtures/utils/vueNodeFixtures.ts b/browser_tests/fixtures/utils/vueNodeFixtures.ts index fca464405..e7368d775 100644 --- a/browser_tests/fixtures/utils/vueNodeFixtures.ts +++ b/browser_tests/fixtures/utils/vueNodeFixtures.ts @@ -1,4 +1,3 @@ -import { expect } from '@playwright/test' import type { Locator } from '@playwright/test' /** DOM-centric helper for a single Vue-rendered node on the canvas. */ @@ -40,7 +39,7 @@ export class VueNodeFixture { async setTitle(value: string): Promise { await this.header.dblclick() const input = this.titleInput - await expect(input).toBeVisible() + await input.waitFor({ state: 'visible' }) await input.fill(value) await input.press('Enter') } @@ -48,7 +47,7 @@ export class VueNodeFixture { async cancelTitleEdit(): Promise { await this.header.dblclick() const input = this.titleInput - await expect(input).toBeVisible() + await input.waitFor({ state: 'visible' }) await input.press('Escape') } diff --git a/browser_tests/fixtures/ws.ts b/browser_tests/fixtures/ws.ts index f1ab1a538..02e68bdb7 100644 --- a/browser_tests/fixtures/ws.ts +++ b/browser_tests/fixtures/ws.ts @@ -1,7 +1,7 @@ import { test as base } from '@playwright/test' export const webSocketFixture = base.extend<{ - ws: { trigger(data: any, url?: string): Promise } + ws: { trigger(data: unknown, url?: string): Promise } }>({ ws: [ async ({ page }, use) => { @@ -10,7 +10,7 @@ export const webSocketFixture = base.extend<{ await page.evaluate(function () { // Create a wrapper for WebSocket that stores them globally // so we can look it up to trigger messages - const store: Record = ((window as any).__ws__ = {}) + const store: Record = (window.__ws__ = {}) window.WebSocket = class extends window.WebSocket { constructor( ...rest: ConstructorParameters @@ -34,7 +34,7 @@ export const webSocketFixture = base.extend<{ u.pathname = '/' url = u.toString() + 'ws' } - const ws: WebSocket = (window as any).__ws__[url] + const ws: WebSocket = window.__ws__![url] ws.dispatchEvent( new MessageEvent('message', { data diff --git a/browser_tests/globalSetup.ts b/browser_tests/globalSetup.ts index 881ef11c4..b43b77c6a 100644 --- a/browser_tests/globalSetup.ts +++ b/browser_tests/globalSetup.ts @@ -5,7 +5,7 @@ import { backupPath } from './utils/backupUtils' dotenv.config() -export default function globalSetup(config: FullConfig) { +export default function globalSetup(_config: FullConfig) { if (!process.env.CI) { if (process.env.TEST_COMFYUI_DIR) { backupPath([process.env.TEST_COMFYUI_DIR, 'user']) diff --git a/browser_tests/globalTeardown.ts b/browser_tests/globalTeardown.ts index aeed77294..c69f563df 100644 --- a/browser_tests/globalTeardown.ts +++ b/browser_tests/globalTeardown.ts @@ -5,7 +5,7 @@ import { restorePath } from './utils/backupUtils' dotenv.config() -export default function globalTeardown(config: FullConfig) { +export default function globalTeardown(_config: FullConfig) { if (!process.env.CI && process.env.TEST_COMFYUI_DIR) { restorePath([process.env.TEST_COMFYUI_DIR, 'user']) restorePath([process.env.TEST_COMFYUI_DIR, 'models']) diff --git a/browser_tests/helpers/actionbar.ts b/browser_tests/helpers/actionbar.ts index 6c368c4d6..29f679e0c 100644 --- a/browser_tests/helpers/actionbar.ts +++ b/browser_tests/helpers/actionbar.ts @@ -1,6 +1,8 @@ import type { Locator, Page } from '@playwright/test' import type { AutoQueueMode } from '../../src/stores/queueStore' +import { TestIds } from '../fixtures/selectors' +import type { WorkspaceStore } from '../types/globals' export class ComfyActionbar { public readonly root: Locator @@ -26,7 +28,7 @@ class ComfyQueueButton { public readonly primaryButton: Locator public readonly dropdownButton: Locator constructor(public readonly actionbar: ComfyActionbar) { - this.root = actionbar.root.getByTestId('queue-button') + this.root = actionbar.root.getByTestId(TestIds.topbar.queueButton) this.primaryButton = this.root.locator('.p-splitbutton-button') this.dropdownButton = this.root.locator('.p-splitbutton-dropdown') } @@ -42,13 +44,14 @@ class ComfyQueueButtonOptions { public async setMode(mode: AutoQueueMode) { await this.page.evaluate((mode) => { - window['app'].extensionManager.queueSettings.mode = mode + ;(window.app!.extensionManager as WorkspaceStore).queueSettings.mode = + mode }, mode) } public async getMode() { return await this.page.evaluate(() => { - return window['app'].extensionManager.queueSettings.mode + return (window.app!.extensionManager as WorkspaceStore).queueSettings.mode }) } } diff --git a/browser_tests/helpers/fitToView.ts b/browser_tests/helpers/fitToView.ts index af6c10e9d..6aea86dba 100644 --- a/browser_tests/helpers/fitToView.ts +++ b/browser_tests/helpers/fitToView.ts @@ -23,7 +23,7 @@ export async function fitToViewInstant( { selectionOnly: boolean } >( ({ selectionOnly }) => { - const app = window['app'] + const app = window.app if (!app?.canvas) return null const canvas = app.canvas @@ -90,7 +90,7 @@ export async function fitToViewInstant( await comfyPage.page.evaluate( ({ bounds, zoom }) => { - const app = window['app'] + const app = window.app if (!app?.canvas) return const canvas = app.canvas diff --git a/browser_tests/helpers/subgraphTestUtils.ts b/browser_tests/helpers/subgraphTestUtils.ts new file mode 100644 index 000000000..0eae24651 --- /dev/null +++ b/browser_tests/helpers/subgraphTestUtils.ts @@ -0,0 +1,16 @@ +import type { LGraph, Subgraph } from '../../src/lib/litegraph/src/litegraph' +import { isSubgraph } from '../../src/utils/typeGuardUtil' + +/** + * Assertion helper for tests where being in a subgraph is a precondition. + * Throws a clear error if the graph is not a Subgraph. + */ +export function assertSubgraph( + graph: LGraph | Subgraph | null | undefined +): asserts graph is Subgraph { + if (!isSubgraph(graph)) { + throw new Error( + 'Expected to be in a subgraph context, but graph is not a Subgraph' + ) + } +} diff --git a/browser_tests/helpers/templates.ts b/browser_tests/helpers/templates.ts index ca74096ad..ddbf1f79d 100644 --- a/browser_tests/helpers/templates.ts +++ b/browser_tests/helpers/templates.ts @@ -6,18 +6,19 @@ import type { TemplateInfo, WorkflowTemplates } from '../../src/platform/workflow/templates/types/template' +import { TestIds } from '../fixtures/selectors' export class ComfyTemplates { readonly content: Locator readonly allTemplateCards: Locator constructor(readonly page: Page) { - this.content = page.getByTestId('template-workflows-content') + this.content = page.getByTestId(TestIds.templates.content) this.allTemplateCards = page.locator('[data-testid^="template-workflow-"]') } - async waitForMinimumCardCount(count: number) { - return await expect(async () => { + async expectMinimumCardCount(count: number) { + await expect(async () => { const cardCount = await this.allTemplateCards.count() expect(cardCount).toBeGreaterThanOrEqual(count) }).toPass({ @@ -26,14 +27,16 @@ export class ComfyTemplates { } async loadTemplate(id: string) { - const templateCard = this.content.getByTestId(`template-workflow-${id}`) + const templateCard = this.content.getByTestId( + TestIds.templates.workflowCard(id) + ) await templateCard.scrollIntoViewIfNeeded() await templateCard.getByRole('img').click() } async getAllTemplates(): Promise { const templates: WorkflowTemplates[] = await this.page.evaluate(() => - window['app'].api.getCoreWorkflowTemplates() + window.app!.api.getCoreWorkflowTemplates() ) return templates.flatMap((t) => t.templates) } diff --git a/browser_tests/tests/actionbar.spec.ts b/browser_tests/tests/actionbar.spec.ts index 1a18577d7..4d57b65f4 100644 --- a/browser_tests/tests/actionbar.spec.ts +++ b/browser_tests/tests/actionbar.spec.ts @@ -1,15 +1,16 @@ import type { Response } from '@playwright/test' import { expect, mergeTests } from '@playwright/test' -import type { StatusWsMessage } from '../../src/schemas/apiSchema.ts' -import { comfyPageFixture } from '../fixtures/ComfyPage.ts' -import { webSocketFixture } from '../fixtures/ws.ts' +import type { StatusWsMessage } from '../../src/schemas/apiSchema' +import { comfyPageFixture } from '../fixtures/ComfyPage' +import { webSocketFixture } from '../fixtures/ws' +import type { WorkspaceStore } from '../types/globals' const test = mergeTests(comfyPageFixture, webSocketFixture) test.describe('Actionbar', { tag: '@ui' }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') }) /** @@ -49,13 +50,14 @@ test.describe('Actionbar', { tag: '@ui' }, () => { // Find and set the width on the latent node const triggerChange = async (value: number) => { return await comfyPage.page.evaluate((value) => { - const node = window['app'].graph._nodes.find( + const node = window.app!.graph!._nodes.find( (n) => n.type === 'EmptyLatentImage' ) - node.widgets[0].value = value - window[ - 'app' - ].extensionManager.workflow.activeWorkflow.changeTracker.checkState() + node!.widgets![0].value = value + + ;( + window.app!.extensionManager as WorkspaceStore + ).workflow.activeWorkflow?.changeTracker.checkState() }, value) } diff --git a/browser_tests/tests/backgroundImageUpload.spec.ts b/browser_tests/tests/backgroundImageUpload.spec.ts index b620f5441..0f49787b2 100644 --- a/browser_tests/tests/backgroundImageUpload.spec.ts +++ b/browser_tests/tests/backgroundImageUpload.spec.ts @@ -3,18 +3,18 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Background Image Upload', () => { test.beforeEach(async ({ comfyPage }) => { // Reset the background image setting before each test - await comfyPage.setSetting('Comfy.Canvas.BackgroundImage', '') + await comfyPage.settings.setSetting('Comfy.Canvas.BackgroundImage', '') }) test.afterEach(async ({ comfyPage }) => { // Clean up background image setting after each test - await comfyPage.setSetting('Comfy.Canvas.BackgroundImage', '') + await comfyPage.settings.setSetting('Comfy.Canvas.BackgroundImage', '') }) test('should show background image upload component in settings', async ({ @@ -34,16 +34,18 @@ test.describe('Background Image Upload', () => { await expect(backgroundImageSetting).toBeVisible() // Verify the component has the expected elements using semantic selectors - const urlInput = backgroundImageSetting.locator('input[type="text"]') + const urlInput = backgroundImageSetting.getByRole('textbox') await expect(urlInput).toBeVisible() await expect(urlInput).toHaveAttribute('placeholder') - const uploadButton = backgroundImageSetting.locator( - 'button:has(.pi-upload)' - ) + const uploadButton = backgroundImageSetting.getByRole('button', { + name: /upload/i + }) await expect(uploadButton).toBeVisible() - const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)') + const clearButton = backgroundImageSetting.getByRole('button', { + name: /clear/i + }) await expect(clearButton).toBeVisible() await expect(clearButton).toBeDisabled() // Should be disabled when no image }) @@ -63,9 +65,9 @@ test.describe('Background Image Upload', () => { '#Comfy\\.Canvas\\.BackgroundImage' ) // Click the upload button to trigger file input - const uploadButton = backgroundImageSetting.locator( - 'button:has(.pi-upload)' - ) + const uploadButton = backgroundImageSetting.getByRole('button', { + name: /upload/i + }) // Set up file upload handler const fileChooserPromise = comfyPage.page.waitForEvent('filechooser') @@ -76,15 +78,17 @@ test.describe('Background Image Upload', () => { await fileChooser.setFiles(comfyPage.assetPath('image32x32.webp')) // Verify the URL input now has an API URL - const urlInput = backgroundImageSetting.locator('input[type="text"]') + const urlInput = backgroundImageSetting.getByRole('textbox') await expect(urlInput).toHaveValue(/^\/api\/view\?.*subfolder=backgrounds/) // Verify clear button is now enabled - const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)') + const clearButton = backgroundImageSetting.getByRole('button', { + name: /clear/i + }) await expect(clearButton).toBeEnabled() // Verify the setting value was actually set - const settingValue = await comfyPage.getSetting( + const settingValue = await comfyPage.settings.getSetting( 'Comfy.Canvas.BackgroundImage' ) expect(settingValue).toMatch(/^\/api\/view\?.*subfolder=backgrounds/) @@ -107,18 +111,20 @@ test.describe('Background Image Upload', () => { '#Comfy\\.Canvas\\.BackgroundImage' ) // Enter URL in the input field - const urlInput = backgroundImageSetting.locator('input[type="text"]') + const urlInput = backgroundImageSetting.getByRole('textbox') await urlInput.fill(testImageUrl) // Trigger blur event to ensure the value is set await urlInput.blur() // Verify clear button is now enabled - const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)') + const clearButton = backgroundImageSetting.getByRole('button', { + name: /clear/i + }) await expect(clearButton).toBeEnabled() // Verify the setting value was updated - const settingValue = await comfyPage.getSetting( + const settingValue = await comfyPage.settings.getSetting( 'Comfy.Canvas.BackgroundImage' ) expect(settingValue).toBe(testImageUrl) @@ -130,7 +136,10 @@ test.describe('Background Image Upload', () => { const testImageUrl = 'https://example.com/test-image.png' // First set a background image - await comfyPage.setSetting('Comfy.Canvas.BackgroundImage', testImageUrl) + await comfyPage.settings.setSetting( + 'Comfy.Canvas.BackgroundImage', + testImageUrl + ) // Open settings dialog await comfyPage.page.keyboard.press('Control+,') @@ -144,11 +153,13 @@ test.describe('Background Image Upload', () => { '#Comfy\\.Canvas\\.BackgroundImage' ) // Verify the input has the test URL - const urlInput = backgroundImageSetting.locator('input[type="text"]') + const urlInput = backgroundImageSetting.getByRole('textbox') await expect(urlInput).toHaveValue(testImageUrl) // Verify clear button is enabled - const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)') + const clearButton = backgroundImageSetting.getByRole('button', { + name: /clear/i + }) await expect(clearButton).toBeEnabled() // Click the clear button @@ -161,7 +172,7 @@ test.describe('Background Image Upload', () => { await expect(clearButton).toBeDisabled() // Verify the setting value was cleared - const settingValue = await comfyPage.getSetting( + const settingValue = await comfyPage.settings.getSetting( 'Comfy.Canvas.BackgroundImage' ) expect(settingValue).toBe('') @@ -182,9 +193,9 @@ test.describe('Background Image Upload', () => { '#Comfy\\.Canvas\\.BackgroundImage' ) // Hover over upload button and verify tooltip appears - const uploadButton = backgroundImageSetting.locator( - 'button:has(.pi-upload)' - ) + const uploadButton = backgroundImageSetting.getByRole('button', { + name: /upload/i + }) await uploadButton.hover() const uploadTooltip = comfyPage.page.locator('.p-tooltip:visible') @@ -194,12 +205,14 @@ test.describe('Background Image Upload', () => { await comfyPage.page.locator('body').hover() // Set a background to enable clear button - const urlInput = backgroundImageSetting.locator('input[type="text"]') + const urlInput = backgroundImageSetting.getByRole('textbox') await urlInput.fill('https://example.com/test.png') await urlInput.blur() // Hover over clear button and verify tooltip appears - const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)') + const clearButton = backgroundImageSetting.getByRole('button', { + name: /clear/i + }) await clearButton.hover() const clearTooltip = comfyPage.page.locator('.p-tooltip:visible') @@ -220,8 +233,10 @@ test.describe('Background Image Upload', () => { const backgroundImageSetting = comfyPage.page.locator( '#Comfy\\.Canvas\\.BackgroundImage' ) - const urlInput = backgroundImageSetting.locator('input[type="text"]') - const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)') + const urlInput = backgroundImageSetting.getByRole('textbox') + const clearButton = backgroundImageSetting.getByRole('button', { + name: /clear/i + }) // Initially clear button should be disabled await expect(clearButton).toBeDisabled() diff --git a/browser_tests/tests/bottomPanelShortcuts.spec.ts b/browser_tests/tests/bottomPanelShortcuts.spec.ts index 354f0fb51..fba61ebd4 100644 --- a/browser_tests/tests/bottomPanelShortcuts.spec.ts +++ b/browser_tests/tests/bottomPanelShortcuts.spec.ts @@ -4,53 +4,33 @@ import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') }) test('should toggle shortcuts panel visibility', async ({ comfyPage }) => { - // Initially shortcuts panel should be hidden - await expect(comfyPage.page.locator('.bottom-panel')).not.toBeVisible() + const { bottomPanel } = comfyPage - // Click shortcuts toggle button in sidebar - await comfyPage.page - .locator('button[aria-label*="Keyboard Shortcuts"]') - .click() - - // Shortcuts panel should now be visible - await expect(comfyPage.page.locator('.bottom-panel')).toBeVisible() - - // Click toggle button again to hide - await comfyPage.page - .locator('button[aria-label*="Keyboard Shortcuts"]') - .click() - - // Panel should be hidden again - await expect(comfyPage.page.locator('.bottom-panel')).not.toBeVisible() + await expect(bottomPanel.root).not.toBeVisible() + await bottomPanel.keyboardShortcutsButton.click() + await expect(bottomPanel.root).toBeVisible() + await bottomPanel.keyboardShortcutsButton.click() + await expect(bottomPanel.root).not.toBeVisible() }) test('should display essentials shortcuts tab', async ({ comfyPage }) => { - // Open shortcuts panel - await comfyPage.page - .locator('button[aria-label*="Keyboard Shortcuts"]') - .click() + const { bottomPanel } = comfyPage - // Essentials tab should be visible and active by default - await expect( - comfyPage.page.getByRole('tab', { name: /Essential/i }) - ).toBeVisible() - await expect( - comfyPage.page.getByRole('tab', { name: /Essential/i }) - ).toHaveAttribute('aria-selected', 'true') + await bottomPanel.keyboardShortcutsButton.click() - // Should display shortcut categories - await expect( - comfyPage.page.locator('.subcategory-title').first() - ).toBeVisible() + await expect(bottomPanel.shortcuts.essentialsTab).toBeVisible() + await expect(bottomPanel.shortcuts.essentialsTab).toHaveAttribute( + 'aria-selected', + 'true' + ) - // Should display some keyboard shortcuts - await expect(comfyPage.page.locator('.key-badge').first()).toBeVisible() + await expect(bottomPanel.shortcuts.subcategoryTitles.first()).toBeVisible() + await expect(bottomPanel.shortcuts.keyBadges.first()).toBeVisible() - // Should have workflow, node, and queue sections await expect( comfyPage.page.getByRole('heading', { name: 'Workflow' }) ).toBeVisible() @@ -63,23 +43,18 @@ test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => { }) test('should display view controls shortcuts tab', async ({ comfyPage }) => { - // Open shortcuts panel - await comfyPage.page - .locator('button[aria-label*="Keyboard Shortcuts"]') - .click() + const { bottomPanel } = comfyPage - // Click view controls tab - await comfyPage.page.getByRole('tab', { name: /View Controls/i }).click() + await bottomPanel.keyboardShortcutsButton.click() + await bottomPanel.shortcuts.viewControlsTab.click() - // View controls tab should be active - await expect( - comfyPage.page.getByRole('tab', { name: /View Controls/i }) - ).toHaveAttribute('aria-selected', 'true') + await expect(bottomPanel.shortcuts.viewControlsTab).toHaveAttribute( + 'aria-selected', + 'true' + ) - // Should display view controls shortcuts - await expect(comfyPage.page.locator('.key-badge').first()).toBeVisible() + await expect(bottomPanel.shortcuts.keyBadges.first()).toBeVisible() - // Should have view and panel controls sections await expect( comfyPage.page.getByRole('heading', { name: 'View' }) ).toBeVisible() @@ -89,54 +64,48 @@ test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => { }) test('should switch between shortcuts tabs', async ({ comfyPage }) => { - // Open shortcuts panel - await comfyPage.page - .locator('button[aria-label*="Keyboard Shortcuts"]') - .click() + const { bottomPanel } = comfyPage - // Essentials should be active initially - await expect( - comfyPage.page.getByRole('tab', { name: /Essential/i }) - ).toHaveAttribute('aria-selected', 'true') + await bottomPanel.keyboardShortcutsButton.click() - // Click view controls tab - await comfyPage.page.getByRole('tab', { name: /View Controls/i }).click() + await expect(bottomPanel.shortcuts.essentialsTab).toHaveAttribute( + 'aria-selected', + 'true' + ) - // View controls should now be active - await expect( - comfyPage.page.getByRole('tab', { name: /View Controls/i }) - ).toHaveAttribute('aria-selected', 'true') - await expect( - comfyPage.page.getByRole('tab', { name: /Essential/i }) - ).not.toHaveAttribute('aria-selected', 'true') + await bottomPanel.shortcuts.viewControlsTab.click() - // Switch back to essentials - await comfyPage.page.getByRole('tab', { name: /Essential/i }).click() + await expect(bottomPanel.shortcuts.viewControlsTab).toHaveAttribute( + 'aria-selected', + 'true' + ) + await expect(bottomPanel.shortcuts.essentialsTab).not.toHaveAttribute( + 'aria-selected', + 'true' + ) - // Essentials should be active again - await expect( - comfyPage.page.getByRole('tab', { name: /Essential/i }) - ).toHaveAttribute('aria-selected', 'true') - await expect( - comfyPage.page.getByRole('tab', { name: /View Controls/i }) - ).not.toHaveAttribute('aria-selected', 'true') + await bottomPanel.shortcuts.essentialsTab.click() + + await expect(bottomPanel.shortcuts.essentialsTab).toHaveAttribute( + 'aria-selected', + 'true' + ) + await expect(bottomPanel.shortcuts.viewControlsTab).not.toHaveAttribute( + 'aria-selected', + 'true' + ) }) test('should display formatted keyboard shortcuts', async ({ comfyPage }) => { - // Open shortcuts panel - await comfyPage.page - .locator('button[aria-label*="Keyboard Shortcuts"]') - .click() + const { bottomPanel } = comfyPage - // Wait for shortcuts to load - await comfyPage.page.waitForSelector('.key-badge') + await bottomPanel.keyboardShortcutsButton.click() - // Check for common formatted keys - const keyBadges = comfyPage.page.locator('.key-badge') + const keyBadges = bottomPanel.shortcuts.keyBadges + await keyBadges.first().waitFor({ state: 'visible' }) const count = await keyBadges.count() expect(count).toBeGreaterThanOrEqual(1) - // Should show formatted modifier keys const badgeText = await keyBadges.allTextContents() const hasModifiers = badgeText.some((text) => ['Ctrl', 'Cmd', 'Shift', 'Alt'].includes(text) @@ -144,91 +113,89 @@ test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => { expect(hasModifiers).toBeTruthy() }) - test('should maintain panel state when switching to terminal', async ({ + test('should maintain panel state when switching between panels', async ({ comfyPage }) => { + const { bottomPanel } = comfyPage + // Open shortcuts panel first - await comfyPage.page - .locator('button[aria-label*="Keyboard Shortcuts"]') - .click() - await expect(comfyPage.page.locator('.bottom-panel')).toBeVisible() - - // Open terminal panel (should switch panels) - await comfyPage.page - .locator('button[aria-label*="Toggle Bottom Panel"]') - .click() - - // Panel should still be visible but showing terminal content - await expect(comfyPage.page.locator('.bottom-panel')).toBeVisible() - - // Switch back to shortcuts - await comfyPage.page - .locator('button[aria-label*="Keyboard Shortcuts"]') - .click() - - // Should show shortcuts content again + await bottomPanel.keyboardShortcutsButton.click() + await expect(bottomPanel.root).toBeVisible() await expect( comfyPage.page.locator('[id*="tab_shortcuts-essentials"]') ).toBeVisible() + + // Try to open terminal panel - may show terminal OR close shortcuts + // depending on whether terminal tabs have loaded (async loading) + await bottomPanel.toggleButton.click() + + // Check if terminal tabs loaded (Logs tab visible) or fell back to shortcuts toggle + const logsTab = comfyPage.page.getByRole('tab', { name: /Logs/i }) + const hasTerminalTabs = await logsTab.isVisible().catch(() => false) + + if (hasTerminalTabs) { + // Terminal panel is visible - verify we can switch back to shortcuts + await expect(bottomPanel.root).toBeVisible() + + // Switch back to shortcuts + await bottomPanel.keyboardShortcutsButton.click() + + // Should show shortcuts content again + await expect( + comfyPage.page.locator('[id*="tab_shortcuts-essentials"]') + ).toBeVisible() + } else { + // Terminal tabs not loaded - button toggled shortcuts off, reopen for verification + await bottomPanel.keyboardShortcutsButton.click() + await expect(bottomPanel.root).toBeVisible() + await expect( + comfyPage.page.locator('[id*="tab_shortcuts-essentials"]') + ).toBeVisible() + } }) test('should handle keyboard navigation', async ({ comfyPage }) => { - // Open shortcuts panel - await comfyPage.page - .locator('button[aria-label*="Keyboard Shortcuts"]') - .click() + const { bottomPanel } = comfyPage - // Focus the first tab - await comfyPage.page.getByRole('tab', { name: /Essential/i }).focus() + await bottomPanel.keyboardShortcutsButton.click() + await bottomPanel.shortcuts.essentialsTab.focus() - // Use arrow keys to navigate between tabs await comfyPage.page.keyboard.press('ArrowRight') - // View controls tab should now have focus - await expect( - comfyPage.page.getByRole('tab', { name: /View Controls/i }) - ).toBeFocused() + await expect(bottomPanel.shortcuts.viewControlsTab).toBeFocused() - // Press Enter to activate the tab await comfyPage.page.keyboard.press('Enter') - // Tab should be selected - await expect( - comfyPage.page.getByRole('tab', { name: /View Controls/i }) - ).toHaveAttribute('aria-selected', 'true') + await expect(bottomPanel.shortcuts.viewControlsTab).toHaveAttribute( + 'aria-selected', + 'true' + ) }) test('should close panel by clicking shortcuts button again', async ({ comfyPage }) => { - // Open shortcuts panel - await comfyPage.page - .locator('button[aria-label*="Keyboard Shortcuts"]') - .click() - await expect(comfyPage.page.locator('.bottom-panel')).toBeVisible() + const { bottomPanel } = comfyPage - // Click shortcuts button again to close - await comfyPage.page - .locator('button[aria-label*="Keyboard Shortcuts"]') - .click() + await bottomPanel.keyboardShortcutsButton.click() + await expect(bottomPanel.root).toBeVisible() - // Panel should be hidden - await expect(comfyPage.page.locator('.bottom-panel')).not.toBeVisible() + await bottomPanel.keyboardShortcutsButton.click() + await expect(bottomPanel.root).not.toBeVisible() }) test('should display shortcuts in organized columns', async ({ comfyPage }) => { - // Open shortcuts panel - await comfyPage.page - .locator('button[aria-label*="Keyboard Shortcuts"]') - .click() + const { bottomPanel } = comfyPage - // Should have 3-column grid layout - await expect(comfyPage.page.locator('.md\\:grid-cols-3')).toBeVisible() + await bottomPanel.keyboardShortcutsButton.click() - // Should have multiple subcategory sections - const subcategoryTitles = comfyPage.page.locator('.subcategory-title') + await expect( + comfyPage.page.locator('[data-testid="shortcuts-columns"]') + ).toBeVisible() + + const subcategoryTitles = bottomPanel.shortcuts.subcategoryTitles const titleCount = await subcategoryTitles.count() expect(titleCount).toBeGreaterThanOrEqual(2) }) @@ -236,43 +203,30 @@ test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => { test('should open shortcuts panel with Ctrl+Shift+K', async ({ comfyPage }) => { - // Initially shortcuts panel should be hidden - await expect(comfyPage.page.locator('.bottom-panel')).not.toBeVisible() + const { bottomPanel } = comfyPage + + await expect(bottomPanel.root).not.toBeVisible() - // Press Ctrl+Shift+K to open shortcuts panel await comfyPage.page.keyboard.press('Control+Shift+KeyK') - // Shortcuts panel should now be visible - await expect(comfyPage.page.locator('.bottom-panel')).toBeVisible() - - // Should show essentials tab by default - await expect( - comfyPage.page.getByRole('tab', { name: /Essential/i }) - ).toHaveAttribute('aria-selected', 'true') + await expect(bottomPanel.root).toBeVisible() + await expect(bottomPanel.shortcuts.essentialsTab).toHaveAttribute( + 'aria-selected', + 'true' + ) }) test('should open settings dialog when clicking manage shortcuts button', async ({ comfyPage }) => { - // Open shortcuts panel - await comfyPage.page - .locator('button[aria-label*="Keyboard Shortcuts"]') - .click() + const { bottomPanel } = comfyPage - // Manage shortcuts button should be visible - await expect( - comfyPage.page.getByRole('button', { name: /Manage Shortcuts/i }) - ).toBeVisible() + await bottomPanel.keyboardShortcutsButton.click() - // Click manage shortcuts button - await comfyPage.page - .getByRole('button', { name: /Manage Shortcuts/i }) - .click() + await expect(bottomPanel.shortcuts.manageButton).toBeVisible() + await bottomPanel.shortcuts.manageButton.click() - // Settings dialog should open with keybinding tab await expect(comfyPage.page.getByRole('dialog')).toBeVisible() - - // Should show keybinding settings (check for keybinding-related content) await expect( comfyPage.page.getByRole('option', { name: 'Keybinding' }) ).toBeVisible() diff --git a/browser_tests/tests/browserTabTitle.spec.ts b/browser_tests/tests/browserTabTitle.spec.ts index f3ad9515e..06bf55ec8 100644 --- a/browser_tests/tests/browserTabTitle.spec.ts +++ b/browser_tests/tests/browserTabTitle.spec.ts @@ -1,16 +1,18 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +import type { WorkspaceStore } from '../types/globals' test.describe('Browser tab title', { tag: '@smoke' }, () => { test.describe('Beta Menu', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') }) test('Can display workflow name', async ({ comfyPage }) => { const workflowName = await comfyPage.page.evaluate(async () => { - return window['app'].extensionManager.workflow.activeWorkflow.filename + return (window.app!.extensionManager as WorkspaceStore).workflow + .activeWorkflow?.filename }) expect(await comfyPage.page.title()).toBe(`*${workflowName} - ComfyUI`) }) @@ -21,7 +23,8 @@ test.describe('Browser tab title', { tag: '@smoke' }, () => { comfyPage }) => { const workflowName = await comfyPage.page.evaluate(async () => { - return window['app'].extensionManager.workflow.activeWorkflow.filename + return (window.app!.extensionManager as WorkspaceStore).workflow + .activeWorkflow?.filename }) expect(await comfyPage.page.title()).toBe(`${workflowName} - ComfyUI`) @@ -30,19 +33,21 @@ test.describe('Browser tab title', { tag: '@smoke' }, () => { const textBox = comfyPage.widgetTextBox await textBox.fill('Hello World') - await comfyPage.clickEmptySpace() + await comfyPage.canvasOps.clickEmptySpace() expect(await comfyPage.page.title()).toBe(`*test - ComfyUI`) // Delete the saved workflow for cleanup. await comfyPage.page.evaluate(async () => { - return window['app'].extensionManager.workflow.activeWorkflow.delete() + return ( + window.app!.extensionManager as WorkspaceStore + ).workflow.activeWorkflow?.delete() }) }) }) test.describe('Legacy Menu', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test('Can display default title', async ({ comfyPage }) => { diff --git a/browser_tests/tests/changeTracker.spec.ts b/browser_tests/tests/changeTracker.spec.ts index e115192c9..80d00c336 100644 --- a/browser_tests/tests/changeTracker.spec.ts +++ b/browser_tests/tests/changeTracker.spec.ts @@ -6,69 +6,69 @@ import { async function beforeChange(comfyPage: ComfyPage) { await comfyPage.page.evaluate(() => { - window['app'].canvas.emitBeforeChange() + window.app!.canvas!.emitBeforeChange() }) } async function afterChange(comfyPage: ComfyPage) { await comfyPage.page.evaluate(() => { - window['app'].canvas.emitAfterChange() + window.app!.canvas!.emitAfterChange() }) } test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Change Tracker', { tag: '@workflow' }, () => { test.describe('Undo/Redo', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') - await comfyPage.setupWorkflowsDirectory({}) + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.workflow.setupWorkflowsDirectory({}) }) test('Can undo multiple operations', async ({ comfyPage }) => { - expect(await comfyPage.getUndoQueueSize()).toBe(0) - expect(await comfyPage.getRedoQueueSize()).toBe(0) + expect(await comfyPage.workflow.getUndoQueueSize()).toBe(0) + expect(await comfyPage.workflow.getRedoQueueSize()).toBe(0) // Save, confirm no errors & workflow modified flag removed await comfyPage.menu.topbar.saveWorkflow('undo-redo-test') - expect(await comfyPage.getToastErrorCount()).toBe(0) - expect(await comfyPage.isCurrentWorkflowModified()).toBe(false) - expect(await comfyPage.getUndoQueueSize()).toBe(0) - expect(await comfyPage.getRedoQueueSize()).toBe(0) + expect(await comfyPage.toast.getToastErrorCount()).toBe(0) + expect(await comfyPage.workflow.isCurrentWorkflowModified()).toBe(false) + expect(await comfyPage.workflow.getUndoQueueSize()).toBe(0) + expect(await comfyPage.workflow.getRedoQueueSize()).toBe(0) - const node = (await comfyPage.getFirstNodeRef())! + const node = (await comfyPage.nodeOps.getFirstNodeRef())! await node.click('title') await node.click('collapse') await expect(node).toBeCollapsed() - expect(await comfyPage.isCurrentWorkflowModified()).toBe(true) - expect(await comfyPage.getUndoQueueSize()).toBe(1) - expect(await comfyPage.getRedoQueueSize()).toBe(0) + expect(await comfyPage.workflow.isCurrentWorkflowModified()).toBe(true) + expect(await comfyPage.workflow.getUndoQueueSize()).toBe(1) + expect(await comfyPage.workflow.getRedoQueueSize()).toBe(0) - await comfyPage.ctrlB() + await comfyPage.keyboard.bypass() await expect(node).toBeBypassed() - expect(await comfyPage.isCurrentWorkflowModified()).toBe(true) - expect(await comfyPage.getUndoQueueSize()).toBe(2) - expect(await comfyPage.getRedoQueueSize()).toBe(0) + expect(await comfyPage.workflow.isCurrentWorkflowModified()).toBe(true) + expect(await comfyPage.workflow.getUndoQueueSize()).toBe(2) + expect(await comfyPage.workflow.getRedoQueueSize()).toBe(0) - await comfyPage.ctrlZ() + await comfyPage.keyboard.undo() await expect(node).not.toBeBypassed() - expect(await comfyPage.isCurrentWorkflowModified()).toBe(true) - expect(await comfyPage.getUndoQueueSize()).toBe(1) - expect(await comfyPage.getRedoQueueSize()).toBe(1) + expect(await comfyPage.workflow.isCurrentWorkflowModified()).toBe(true) + expect(await comfyPage.workflow.getUndoQueueSize()).toBe(1) + expect(await comfyPage.workflow.getRedoQueueSize()).toBe(1) - await comfyPage.ctrlZ() + await comfyPage.keyboard.undo() await expect(node).not.toBeCollapsed() - expect(await comfyPage.isCurrentWorkflowModified()).toBe(false) - expect(await comfyPage.getUndoQueueSize()).toBe(0) - expect(await comfyPage.getRedoQueueSize()).toBe(2) + expect(await comfyPage.workflow.isCurrentWorkflowModified()).toBe(false) + expect(await comfyPage.workflow.getUndoQueueSize()).toBe(0) + expect(await comfyPage.workflow.getRedoQueueSize()).toBe(2) }) }) test('Can group multiple change actions into a single transaction', async ({ comfyPage }) => { - const node = (await comfyPage.getFirstNodeRef())! + const node = (await comfyPage.nodeOps.getFirstNodeRef())! expect(node).toBeTruthy() await expect(node).not.toBeCollapsed() await expect(node).not.toBeBypassed() @@ -77,27 +77,27 @@ test.describe('Change Tracker', { tag: '@workflow' }, () => { // Bypass + collapse node await node.click('title') await node.click('collapse') - await comfyPage.ctrlB() + await comfyPage.keyboard.bypass() await expect(node).toBeCollapsed() await expect(node).toBeBypassed() // Undo, undo, ensure both changes undone - await comfyPage.ctrlZ() + await comfyPage.keyboard.undo() await expect(node).not.toBeBypassed() await expect(node).toBeCollapsed() - await comfyPage.ctrlZ() + await comfyPage.keyboard.undo() await expect(node).not.toBeBypassed() await expect(node).not.toBeCollapsed() // Prevent clicks registering a double-click - await comfyPage.clickEmptySpace() + await comfyPage.canvasOps.clickEmptySpace() await node.click('title') // Run again, but within a change transaction await beforeChange(comfyPage) await node.click('collapse') - await comfyPage.ctrlB() + await comfyPage.keyboard.bypass() await expect(node).toBeCollapsed() await expect(node).toBeBypassed() @@ -105,7 +105,7 @@ test.describe('Change Tracker', { tag: '@workflow' }, () => { await afterChange(comfyPage) // Ensure undo reverts both changes - await comfyPage.ctrlZ() + await comfyPage.keyboard.undo() await expect(node).not.toBeBypassed() await expect(node).not.toBeCollapsed() }) @@ -113,10 +113,10 @@ test.describe('Change Tracker', { tag: '@workflow' }, () => { test('Can nest multiple change transactions without adding undo steps', async ({ comfyPage }) => { - const node = (await comfyPage.getFirstNodeRef())! + const node = (await comfyPage.nodeOps.getFirstNodeRef())! const bypassAndPin = async () => { await beforeChange(comfyPage) - await comfyPage.ctrlB() + await comfyPage.keyboard.bypass() await expect(node).toBeBypassed() await comfyPage.page.keyboard.press('KeyP') await comfyPage.nextFrame() @@ -142,30 +142,30 @@ test.describe('Change Tracker', { tag: '@workflow' }, () => { await multipleChanges() - await comfyPage.ctrlZ() + await comfyPage.keyboard.undo() await expect(node).not.toBeBypassed() await expect(node).not.toBePinned() await expect(node).not.toBeCollapsed() - await comfyPage.ctrlY() + await comfyPage.keyboard.redo() await expect(node).toBeBypassed() await expect(node).toBePinned() await expect(node).toBeCollapsed() }) test('Can detect changes in workflow.extra', async ({ comfyPage }) => { - expect(await comfyPage.getUndoQueueSize()).toBe(0) + expect(await comfyPage.workflow.getUndoQueueSize()).toBe(0) await comfyPage.page.evaluate(() => { - window['app'].graph.extra.foo = 'bar' + window.app!.graph!.extra.foo = 'bar' }) // Click empty space to trigger a change detection. - await comfyPage.clickEmptySpace() - expect(await comfyPage.getUndoQueueSize()).toBe(1) + await comfyPage.canvasOps.clickEmptySpace() + expect(await comfyPage.workflow.getUndoQueueSize()).toBe(1) }) test('Ignores changes in workflow.ds', async ({ comfyPage }) => { - expect(await comfyPage.getUndoQueueSize()).toBe(0) - await comfyPage.pan({ x: 10, y: 10 }) - expect(await comfyPage.getUndoQueueSize()).toBe(0) + expect(await comfyPage.workflow.getUndoQueueSize()).toBe(0) + await comfyPage.canvasOps.pan({ x: 10, y: 10 }) + expect(await comfyPage.workflow.getUndoQueueSize()).toBe(0) }) }) diff --git a/browser_tests/tests/colorPalette.spec.ts b/browser_tests/tests/colorPalette.spec.ts index 0118955fb..5ec6453c0 100644 --- a/browser_tests/tests/colorPalette.spec.ts +++ b/browser_tests/tests/colorPalette.spec.ts @@ -1,13 +1,13 @@ import { expect } from '@playwright/test' -import type { Palette } from '../../src/schemas/colorPaletteSchema' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +import type { WorkspaceStore } from '../types/globals' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) -const customColorPalettes: Record = { +const customColorPalettes = { obsidian: { version: 102, id: 'obsidian', @@ -153,40 +153,48 @@ const customColorPalettes: Record = { test.describe('Color Palette', { tag: ['@screenshot', '@settings'] }, () => { test('Can show custom color palette', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.CustomColorPalettes', customColorPalettes) + await comfyPage.settings.setSetting( + 'Comfy.CustomColorPalettes', + customColorPalettes + ) // Reload to apply the new setting. Setting Comfy.CustomColorPalettes directly // doesn't update the store immediately. await comfyPage.setup() - await comfyPage.loadWorkflow('nodes/every_node_color') - await comfyPage.setSetting('Comfy.ColorPalette', 'obsidian_dark') + await comfyPage.workflow.loadWorkflow('nodes/every_node_color') + await comfyPage.settings.setSetting('Comfy.ColorPalette', 'obsidian_dark') await expect(comfyPage.canvas).toHaveScreenshot( 'custom-color-palette-obsidian-dark-all-colors.png' ) - await comfyPage.setSetting('Comfy.ColorPalette', 'light_red') + await comfyPage.settings.setSetting('Comfy.ColorPalette', 'light_red') await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot( 'custom-color-palette-light-red.png' ) - await comfyPage.setSetting('Comfy.ColorPalette', 'dark') + await comfyPage.settings.setSetting('Comfy.ColorPalette', 'dark') await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot('default-color-palette.png') }) test('Can add custom color palette', async ({ comfyPage }) => { - await comfyPage.page.evaluate((p) => { - window['app'].extensionManager.colorPalette.addCustomColorPalette(p) + await comfyPage.page.evaluate(async (p) => { + await ( + window.app!.extensionManager as WorkspaceStore + ).colorPalette.addCustomColorPalette(p) }, customColorPalettes.obsidian_dark) - expect(await comfyPage.getToastErrorCount()).toBe(0) + expect(await comfyPage.toast.getToastErrorCount()).toBe(0) - await comfyPage.setSetting('Comfy.ColorPalette', 'obsidian_dark') + await comfyPage.settings.setSetting('Comfy.ColorPalette', 'obsidian_dark') await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot( 'custom-color-palette-obsidian-dark.png' ) // Legacy `custom_` prefix is still supported - await comfyPage.setSetting('Comfy.ColorPalette', 'custom_obsidian_dark') + await comfyPage.settings.setSetting( + 'Comfy.ColorPalette', + 'custom_obsidian_dark' + ) await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot( 'custom-color-palette-obsidian-dark.png' @@ -199,20 +207,20 @@ test.describe( { tag: ['@screenshot', '@settings'] }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.loadWorkflow('nodes/every_node_color') + await comfyPage.workflow.loadWorkflow('nodes/every_node_color') }) test('should adjust opacity via node opacity setting', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Node.Opacity', 0.5) + await comfyPage.settings.setSetting('Comfy.Node.Opacity', 0.5) // Drag mouse to force canvas to redraw await comfyPage.page.mouse.move(0, 0) await expect(comfyPage.canvas).toHaveScreenshot('node-opacity-0.5.png') - await comfyPage.setSetting('Comfy.Node.Opacity', 1.0) + await comfyPage.settings.setSetting('Comfy.Node.Opacity', 1.0) await comfyPage.page.mouse.move(8, 8) await expect(comfyPage.canvas).toHaveScreenshot('node-opacity-1.png') @@ -221,8 +229,8 @@ test.describe( test('should persist color adjustments when changing themes', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Node.Opacity', 0.2) - await comfyPage.setSetting('Comfy.ColorPalette', 'arc') + await comfyPage.settings.setSetting('Comfy.Node.Opacity', 0.2) + await comfyPage.settings.setSetting('Comfy.ColorPalette', 'arc') await comfyPage.nextFrame() await comfyPage.page.mouse.move(0, 0) await expect(comfyPage.canvas).toHaveScreenshot( @@ -233,8 +241,8 @@ test.describe( test('should not serialize color adjustments in workflow', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Node.Opacity', 0.5) - await comfyPage.setSetting('Comfy.ColorPalette', 'light') + await comfyPage.settings.setSetting('Comfy.Node.Opacity', 0.5) + await comfyPage.settings.setSetting('Comfy.ColorPalette', 'light') await comfyPage.nextFrame() const parsed = await ( await comfyPage.page.waitForFunction( @@ -262,7 +270,7 @@ test.describe( test('should lighten node colors when switching to light theme', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.ColorPalette', 'light') + await comfyPage.settings.setSetting('Comfy.ColorPalette', 'light') await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot( 'node-lightened-colors.png' @@ -271,9 +279,9 @@ test.describe( test.describe('Context menu color adjustments', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.ColorPalette', 'light') - await comfyPage.setSetting('Comfy.Node.Opacity', 0.3) - const node = await comfyPage.getFirstNodeRef() + await comfyPage.settings.setSetting('Comfy.ColorPalette', 'light') + await comfyPage.settings.setSetting('Comfy.Node.Opacity', 0.3) + const node = await comfyPage.nodeOps.getFirstNodeRef() await node?.clickContextMenuOption('Colors') }) diff --git a/browser_tests/tests/commands.spec.ts b/browser_tests/tests/commands.spec.ts index acf99d177..d4e4f7fbf 100644 --- a/browser_tests/tests/commands.spec.ts +++ b/browser_tests/tests/commands.spec.ts @@ -3,52 +3,52 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Keybindings', { tag: '@keyboard' }, () => { test('Should execute command', async ({ comfyPage }) => { - await comfyPage.registerCommand('TestCommand', () => { - window['foo'] = true + await comfyPage.command.registerCommand('TestCommand', () => { + window.foo = true }) - await comfyPage.executeCommand('TestCommand') - expect(await comfyPage.page.evaluate(() => window['foo'])).toBe(true) + await comfyPage.command.executeCommand('TestCommand') + expect(await comfyPage.page.evaluate(() => window.foo)).toBe(true) }) test('Should execute async command', async ({ comfyPage }) => { - await comfyPage.registerCommand('TestCommand', async () => { + await comfyPage.command.registerCommand('TestCommand', async () => { await new Promise((resolve) => setTimeout(() => { - window['foo'] = true + window.foo = true resolve() }, 5) ) }) - await comfyPage.executeCommand('TestCommand') - expect(await comfyPage.page.evaluate(() => window['foo'])).toBe(true) + await comfyPage.command.executeCommand('TestCommand') + expect(await comfyPage.page.evaluate(() => window.foo)).toBe(true) }) test('Should handle command errors', async ({ comfyPage }) => { - await comfyPage.registerCommand('TestCommand', () => { + await comfyPage.command.registerCommand('TestCommand', () => { throw new Error('Test error') }) - await comfyPage.executeCommand('TestCommand') - expect(await comfyPage.getToastErrorCount()).toBe(1) + await comfyPage.command.executeCommand('TestCommand') + expect(await comfyPage.toast.getToastErrorCount()).toBe(1) }) test('Should handle async command errors', async ({ comfyPage }) => { - await comfyPage.registerCommand('TestCommand', async () => { - await new Promise((resolve, reject) => + await comfyPage.command.registerCommand('TestCommand', async () => { + await new Promise((_resolve, reject) => setTimeout(() => { reject(new Error('Test error')) }, 5) ) }) - await comfyPage.executeCommand('TestCommand') - expect(await comfyPage.getToastErrorCount()).toBe(1) + await comfyPage.command.executeCommand('TestCommand') + expect(await comfyPage.toast.getToastErrorCount()).toBe(1) }) }) diff --git a/browser_tests/tests/copyPaste.spec.ts b/browser_tests/tests/copyPaste.spec.ts index a04cc191b..4481d00c0 100644 --- a/browser_tests/tests/copyPaste.spec.ts +++ b/browser_tests/tests/copyPaste.spec.ts @@ -1,24 +1,31 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +import { DefaultGraphPositions } from '../fixtures/constants/defaultGraphPositions' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Copy Paste', { tag: ['@screenshot', '@workflow'] }, () => { test('Can copy and paste node', async ({ comfyPage }) => { - await comfyPage.clickEmptyLatentNode() + await comfyPage.canvas.click({ + position: DefaultGraphPositions.emptyLatentWidgetClick + }) await comfyPage.page.mouse.move(10, 10) - await comfyPage.ctrlC() - await comfyPage.ctrlV() + await comfyPage.nextFrame() + await comfyPage.clipboard.copy() + await comfyPage.clipboard.paste() await expect(comfyPage.canvas).toHaveScreenshot('copied-node.png') }) test('Can copy and paste node with link', async ({ comfyPage }) => { - await comfyPage.clickTextEncodeNode1() + await comfyPage.canvas.click({ + position: DefaultGraphPositions.textEncodeNode1 + }) + await comfyPage.nextFrame() await comfyPage.page.mouse.move(10, 10) - await comfyPage.ctrlC() + await comfyPage.clipboard.copy() await comfyPage.page.keyboard.press('Control+Shift+V') await expect(comfyPage.canvas).toHaveScreenshot('copied-node-with-link.png') }) @@ -28,9 +35,9 @@ test.describe('Copy Paste', { tag: ['@screenshot', '@workflow'] }, () => { await textBox.click() const originalString = await textBox.inputValue() await textBox.selectText() - await comfyPage.ctrlC(null) - await comfyPage.ctrlV(null) - await comfyPage.ctrlV(null) + await comfyPage.clipboard.copy(null) + await comfyPage.clipboard.paste(null) + await comfyPage.clipboard.paste(null) const resultString = await textBox.inputValue() expect(resultString).toBe(originalString + originalString) }) @@ -44,7 +51,7 @@ test.describe('Copy Paste', { tag: ['@screenshot', '@workflow'] }, () => { y: 281 } }) - await comfyPage.ctrlC(null) + await comfyPage.clipboard.copy(null) // Empty latent node's width await comfyPage.canvas.click({ position: { @@ -52,7 +59,7 @@ test.describe('Copy Paste', { tag: ['@screenshot', '@workflow'] }, () => { y: 643 } }) - await comfyPage.ctrlV(null) + await comfyPage.clipboard.paste(null) await comfyPage.page.keyboard.press('Enter') await expect(comfyPage.canvas).toHaveScreenshot('copied-widget-value.png') }) @@ -63,15 +70,19 @@ test.describe('Copy Paste', { tag: ['@screenshot', '@workflow'] }, () => { test('Paste in text area with node previously copied', async ({ comfyPage }) => { - await comfyPage.clickEmptyLatentNode() - await comfyPage.ctrlC(null) + await comfyPage.canvas.click({ + position: DefaultGraphPositions.emptyLatentWidgetClick + }) + await comfyPage.page.mouse.move(10, 10) + await comfyPage.nextFrame() + await comfyPage.clipboard.copy(null) const textBox = comfyPage.widgetTextBox await textBox.click() await textBox.inputValue() await textBox.selectText() - await comfyPage.ctrlC(null) - await comfyPage.ctrlV(null) - await comfyPage.ctrlV(null) + await comfyPage.clipboard.copy(null) + await comfyPage.clipboard.paste(null) + await comfyPage.clipboard.paste(null) await expect(comfyPage.canvas).toHaveScreenshot( 'paste-in-text-area-with-node-previously-copied.png' ) @@ -82,10 +93,10 @@ test.describe('Copy Paste', { tag: ['@screenshot', '@workflow'] }, () => { await textBox.click() await textBox.inputValue() await textBox.selectText() - await comfyPage.ctrlC(null) + await comfyPage.clipboard.copy(null) // Unfocus textbox. await comfyPage.page.mouse.click(10, 10) - await comfyPage.ctrlV(null) + await comfyPage.clipboard.paste(null) await expect(comfyPage.canvas).toHaveScreenshot('no-node-copied.png') }) @@ -103,19 +114,19 @@ test.describe('Copy Paste', { tag: ['@screenshot', '@workflow'] }, () => { test('Can undo paste multiple nodes as single action', async ({ comfyPage }) => { - const initialCount = await comfyPage.getGraphNodesCount() + const initialCount = await comfyPage.nodeOps.getGraphNodesCount() expect(initialCount).toBeGreaterThan(1) await comfyPage.canvas.click() - await comfyPage.ctrlA() + await comfyPage.keyboard.selectAll() await comfyPage.page.mouse.move(10, 10) - await comfyPage.ctrlC() - await comfyPage.ctrlV() + await comfyPage.clipboard.copy() + await comfyPage.clipboard.paste() - const pasteCount = await comfyPage.getGraphNodesCount() + const pasteCount = await comfyPage.nodeOps.getGraphNodesCount() expect(pasteCount).toBe(initialCount * 2) - await comfyPage.ctrlZ() - const undoCount = await comfyPage.getGraphNodesCount() + await comfyPage.keyboard.undo() + const undoCount = await comfyPage.nodeOps.getGraphNodesCount() expect(undoCount).toBe(initialCount) }) }) diff --git a/browser_tests/tests/copyPaste.spec.ts-snapshots/copied-node-chromium-linux.png b/browser_tests/tests/copyPaste.spec.ts-snapshots/copied-node-chromium-linux.png index 782465751..f3d85d8da 100644 Binary files a/browser_tests/tests/copyPaste.spec.ts-snapshots/copied-node-chromium-linux.png and b/browser_tests/tests/copyPaste.spec.ts-snapshots/copied-node-chromium-linux.png differ diff --git a/browser_tests/tests/copyPaste.spec.ts-snapshots/paste-in-text-area-with-node-previously-copied-chromium-linux.png b/browser_tests/tests/copyPaste.spec.ts-snapshots/paste-in-text-area-with-node-previously-copied-chromium-linux.png index b1f181c75..e589ba6a7 100644 Binary files a/browser_tests/tests/copyPaste.spec.ts-snapshots/paste-in-text-area-with-node-previously-copied-chromium-linux.png and b/browser_tests/tests/copyPaste.spec.ts-snapshots/paste-in-text-area-with-node-previously-copied-chromium-linux.png differ diff --git a/browser_tests/tests/customIcons.spec.ts b/browser_tests/tests/customIcons.spec.ts index eb4eccfd5..ed9680406 100644 --- a/browser_tests/tests/customIcons.spec.ts +++ b/browser_tests/tests/customIcons.spec.ts @@ -24,7 +24,7 @@ async function verifyCustomIconSvg(iconElement: Locator) { test.describe('Custom Icons', { tag: '@settings' }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') }) test('sidebar tab icons use custom SVGs', async ({ comfyPage }) => { diff --git a/browser_tests/tests/dialog.spec.ts b/browser_tests/tests/dialog.spec.ts index cfa1c8cd2..fddbfbc70 100644 --- a/browser_tests/tests/dialog.spec.ts +++ b/browser_tests/tests/dialog.spec.ts @@ -1,18 +1,19 @@ import type { Locator } from '@playwright/test' import { expect } from '@playwright/test' -import type { Keybinding } from '../../src/platform/keybindings' +import type { Keybinding } from '../../src/platform/keybindings/types' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +import { DefaultGraphPositions } from '../fixtures/constants/defaultGraphPositions' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Load workflow warning', { tag: '@ui' }, () => { test('Should display a warning when loading a workflow with missing nodes', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('missing/missing_nodes') + await comfyPage.workflow.loadWorkflow('missing/missing_nodes') // Wait for the element with the .comfy-missing-nodes selector to be visible const missingNodesWarning = comfyPage.page.locator('.comfy-missing-nodes') @@ -22,7 +23,7 @@ test.describe('Load workflow warning', { tag: '@ui' }, () => { test('Should display a warning when loading a workflow with missing nodes in subgraphs', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('missing/missing_nodes_in_subgraph') + await comfyPage.workflow.loadWorkflow('missing/missing_nodes_in_subgraph') // Wait for the element with the .comfy-missing-nodes selector to be visible const missingNodesWarning = comfyPage.page.locator('.comfy-missing-nodes') @@ -36,22 +37,26 @@ test.describe('Load workflow warning', { tag: '@ui' }, () => { }) test('Does not report warning on undo/redo', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default') + await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default') - await comfyPage.loadWorkflow('missing/missing_nodes') - await comfyPage.closeDialog() + await comfyPage.workflow.loadWorkflow('missing/missing_nodes') + await comfyPage.page + .locator('.p-dialog') + .getByRole('button', { name: 'Close' }) + .click({ force: true }) + await comfyPage.page.locator('.p-dialog').waitFor({ state: 'hidden' }) // Wait for any async operations to complete after dialog closes await comfyPage.nextFrame() // Make a change to the graph - await comfyPage.doubleClickCanvas() + await comfyPage.canvasOps.doubleClick() await comfyPage.searchBox.fillAndSelectFirstNode('KSampler') // Undo and redo the change - await comfyPage.ctrlZ() + await comfyPage.keyboard.undo() await expect(comfyPage.page.locator('.comfy-missing-nodes')).not.toBeVisible() - await comfyPage.ctrlY() + await comfyPage.keyboard.redo() await expect(comfyPage.page.locator('.comfy-missing-nodes')).not.toBeVisible() }) @@ -59,7 +64,7 @@ test.describe('Execution error', () => { test('Should display an error message when an execution error occurs', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('nodes/execution_error') + await comfyPage.workflow.loadWorkflow('nodes/execution_error') await comfyPage.queueButton.click() await comfyPage.nextFrame() @@ -71,7 +76,10 @@ test.describe('Execution error', () => { test.describe('Missing models warning', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Workflow.ShowMissingModelsWarning', true) + await comfyPage.settings.setSetting( + 'Comfy.Workflow.ShowMissingModelsWarning', + true + ) await comfyPage.page.evaluate((url: string) => { return fetch(`${url}/api/devtools/cleanup_fake_model`) }, comfyPage.url) @@ -80,7 +88,7 @@ test.describe('Missing models warning', () => { test('Should display a warning when missing models are found', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('missing/missing_models') + await comfyPage.workflow.loadWorkflow('missing/missing_models') const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models') await expect(missingModelsWarning).toBeVisible() @@ -97,7 +105,9 @@ test.describe('Missing models warning', () => { comfyPage }) => { // Load workflow that has a node with models metadata at the node level - await comfyPage.loadWorkflow('missing/missing_models_from_node_properties') + await comfyPage.workflow.loadWorkflow( + 'missing/missing_models_from_node_properties' + ) const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models') await expect(missingModelsWarning).toBeVisible() @@ -146,7 +156,7 @@ test.describe('Missing models warning', () => { { times: 1 } ) - await comfyPage.loadWorkflow('missing/missing_models') + await comfyPage.workflow.loadWorkflow('missing/missing_models') const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models') await expect(missingModelsWarning).not.toBeVisible() @@ -157,7 +167,9 @@ test.describe('Missing models warning', () => { }) => { // This tests the scenario where outdated model metadata exists in the workflow // but the actual selected models (widget values) have changed - await comfyPage.loadWorkflow('missing/model_metadata_widget_mismatch') + await comfyPage.workflow.loadWorkflow( + 'missing/model_metadata_widget_mismatch' + ) // The missing models warning should NOT appear const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models') @@ -171,7 +183,7 @@ test.describe('Missing models warning', () => { }) => { // The fake_model.safetensors is served by // https://github.com/Comfy-Org/ComfyUI_devtools/blob/main/__init__.py - await comfyPage.loadWorkflow('missing/missing_models') + await comfyPage.workflow.loadWorkflow('missing/missing_models') const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models') await expect(missingModelsWarning).toBeVisible() @@ -190,11 +202,11 @@ test.describe('Missing models warning', () => { let closeButton: Locator test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting( + await comfyPage.settings.setSetting( 'Comfy.Workflow.ShowMissingModelsWarning', true ) - await comfyPage.loadWorkflow('missing/missing_models') + await comfyPage.workflow.loadWorkflow('missing/missing_models') checkbox = comfyPage.page.getByLabel("Don't show this again") closeButton = comfyPage.page.getByLabel('Close') @@ -210,7 +222,7 @@ test.describe('Missing models warning', () => { await closeButton.click() await changeSettingPromise - const settingValue = await comfyPage.getSetting( + const settingValue = await comfyPage.settings.getSetting( 'Comfy.Workflow.ShowMissingModelsWarning' ) expect(settingValue).toBe(false) @@ -221,7 +233,7 @@ test.describe('Missing models warning', () => { }) => { await closeButton.click() - const settingValue = await comfyPage.getSetting( + const settingValue = await comfyPage.settings.getSetting( 'Comfy.Workflow.ShowMissingModelsWarning' ) expect(settingValue).toBe(true) @@ -252,9 +264,11 @@ test.describe('Settings', () => { test('Can change canvas zoom speed setting', async ({ comfyPage }) => { const maxSpeed = 2.5 - await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', maxSpeed) + await comfyPage.settings.setSetting('Comfy.Graph.ZoomSpeed', maxSpeed) await test.step('Setting should persist', async () => { - expect(await comfyPage.getSetting('Comfy.Graph.ZoomSpeed')).toBe(maxSpeed) + expect(await comfyPage.settings.getSetting('Comfy.Graph.ZoomSpeed')).toBe( + maxSpeed + ) }) }) @@ -311,7 +325,7 @@ test.describe('Support', () => { test('Should open external zendesk link with OSS tag', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') const pagePromise = comfyPage.page.context().waitForEvent('page') await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Support']) const newPage = await pagePromise @@ -331,13 +345,13 @@ test.describe('Error dialog', () => { comfyPage }) => { await comfyPage.page.evaluate(() => { - const graph = window['graph'] - graph.configure = () => { + const graph = window.graph! + ;(graph as { configure: () => void }).configure = () => { throw new Error('Error on configure!') } }) - await comfyPage.loadWorkflow('default') + await comfyPage.workflow.loadWorkflow('default') const errorDialog = comfyPage.page.locator('.comfy-error-report') await expect(errorDialog).toBeVisible() @@ -347,7 +361,7 @@ test.describe('Error dialog', () => { comfyPage }) => { await comfyPage.page.evaluate(async () => { - const app = window['app'] + const app = window.app! app.api.queuePrompt = () => { throw new Error('Error on queuePrompt!') } @@ -362,9 +376,13 @@ test.describe('Signin dialog', () => { test('Paste content to signin dialog should not paste node on canvas', async ({ comfyPage }) => { - const nodeNum = (await comfyPage.getNodes()).length - await comfyPage.clickEmptyLatentNode() - await comfyPage.ctrlC() + const nodeNum = (await comfyPage.nodeOps.getNodes()).length + await comfyPage.canvas.click({ + position: DefaultGraphPositions.emptyLatentWidgetClick + }) + await comfyPage.page.mouse.move(10, 10) + await comfyPage.nextFrame() + await comfyPage.clipboard.copy() const textBox = comfyPage.widgetTextBox await textBox.click() @@ -373,7 +391,7 @@ test.describe('Signin dialog', () => { await textBox.press('Control+c') await comfyPage.page.evaluate(() => { - void window['app'].extensionManager.dialog.showSignInDialog() + void window.app!.extensionManager.dialog.showSignInDialog() }) const input = comfyPage.page.locator('#comfy-org-sign-in-password') @@ -381,6 +399,6 @@ test.describe('Signin dialog', () => { await input.press('Control+v') await expect(input).toHaveValue('test_password') - expect(await comfyPage.getNodes()).toHaveLength(nodeNum) + expect(await comfyPage.nodeOps.getNodes()).toHaveLength(nodeNum) }) }) diff --git a/browser_tests/tests/domWidget.spec.ts b/browser_tests/tests/domWidget.spec.ts index d0468210e..d8e2dab9b 100644 --- a/browser_tests/tests/domWidget.spec.ts +++ b/browser_tests/tests/domWidget.spec.ts @@ -3,12 +3,12 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('DOM Widget', { tag: '@widget' }, () => { test('Collapsed multiline textarea is not visible', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('widgets/collapsed_multiline') + await comfyPage.workflow.loadWorkflow('widgets/collapsed_multiline') const textareaWidget = comfyPage.page.locator('.comfy-multiline-input') await expect(textareaWidget).not.toBeVisible() }) @@ -21,7 +21,7 @@ test.describe('DOM Widget', { tag: '@widget' }, () => { await expect(firstMultiline).toBeVisible() await expect(lastMultiline).toBeVisible() - const nodes = await comfyPage.getNodeRefsByType('CLIPTextEncode') + const nodes = await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode') for (const node of nodes) { await node.click('collapse') } @@ -33,8 +33,8 @@ test.describe('DOM Widget', { tag: '@widget' }, () => { 'Position update when entering focus mode', { tag: '@screenshot' }, async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') - await comfyPage.executeCommand('Workspace.ToggleFocusMode') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.command.executeCommand('Workspace.ToggleFocusMode') await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot('focus-mode-on.png') } @@ -68,9 +68,9 @@ test.describe('DOM Widget', { tag: '@widget' }, () => { .first() await expect(textareaWidget).toBeVisible() - await comfyPage.setSetting('Comfy.Sidebar.Size', 'small') - await comfyPage.setSetting('Comfy.Sidebar.Location', 'left') - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.Sidebar.Size', 'small') + await comfyPage.settings.setSetting('Comfy.Sidebar.Location', 'left') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.nextFrame() let oldPos: [number, number] @@ -85,15 +85,15 @@ test.describe('DOM Widget', { tag: '@widget' }, () => { // --- test --- - await comfyPage.setSetting('Comfy.Sidebar.Size', 'normal') + await comfyPage.settings.setSetting('Comfy.Sidebar.Size', 'normal') await comfyPage.nextFrame() await checkBboxChange() - await comfyPage.setSetting('Comfy.Sidebar.Location', 'right') + await comfyPage.settings.setSetting('Comfy.Sidebar.Location', 'right') await comfyPage.nextFrame() await checkBboxChange() - await comfyPage.setSetting('Comfy.UseNewMenu', 'Bottom') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Bottom') await comfyPage.nextFrame() await checkBboxChange() }) diff --git a/browser_tests/tests/execution.spec.ts b/browser_tests/tests/execution.spec.ts index 2173c8623..1c8b68269 100644 --- a/browser_tests/tests/execution.spec.ts +++ b/browser_tests/tests/execution.spec.ts @@ -3,7 +3,7 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Execution', { tag: ['@smoke', '@workflow'] }, () => { @@ -11,12 +11,15 @@ test.describe('Execution', { tag: ['@smoke', '@workflow'] }, () => { 'Report error on unconnected slot', { tag: '@screenshot' }, async ({ comfyPage }) => { - await comfyPage.disconnectEdge() - await comfyPage.clickEmptySpace() + await comfyPage.canvasOps.disconnectEdge() + await comfyPage.canvasOps.clickEmptySpace() - await comfyPage.executeCommand('Comfy.QueuePrompt') + await comfyPage.command.executeCommand('Comfy.QueuePrompt') await expect(comfyPage.page.locator('.comfy-error-report')).toBeVisible() - await comfyPage.page.locator('.p-dialog-close-button').click() + await comfyPage.page + .locator('.p-dialog') + .getByRole('button', { name: 'Close' }) + .click() await comfyPage.page.locator('.comfy-error-report').waitFor({ state: 'hidden' }) @@ -32,17 +35,17 @@ test.describe( { tag: ['@smoke', '@workflow'] }, () => { test('Execute to selected output nodes', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('execution/partial_execution') - const input = await comfyPage.getNodeRefById(3) - const output1 = await comfyPage.getNodeRefById(1) - const output2 = await comfyPage.getNodeRefById(4) + await comfyPage.workflow.loadWorkflow('execution/partial_execution') + const input = await comfyPage.nodeOps.getNodeRefById(3) + const output1 = await comfyPage.nodeOps.getNodeRefById(1) + const output2 = await comfyPage.nodeOps.getNodeRefById(4) expect(await (await input.getWidget(0)).getValue()).toBe('foo') expect(await (await output1.getWidget(0)).getValue()).toBe('') expect(await (await output2.getWidget(0)).getValue()).toBe('') await output1.click('title') - await comfyPage.executeCommand('Comfy.QueueSelectedOutputNodes') + await comfyPage.command.executeCommand('Comfy.QueueSelectedOutputNodes') await expect(async () => { expect(await (await input.getWidget(0)).getValue()).toBe('foo') expect(await (await output1.getWidget(0)).getValue()).toBe('foo') diff --git a/browser_tests/tests/extensionAPI.spec.ts b/browser_tests/tests/extensionAPI.spec.ts index 38f4a6c1d..97d4424c9 100644 --- a/browser_tests/tests/extensionAPI.spec.ts +++ b/browser_tests/tests/extensionAPI.spec.ts @@ -1,23 +1,32 @@ import { expect } from '@playwright/test' +import type { Settings } from '../../src/schemas/apiSchema' import type { SettingParams } from '../../src/platform/settings/types' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +/** + * Type helper for test settings with arbitrary IDs. + * Extensions can register settings with any ID, but SettingParams.id + * is typed as keyof Settings for autocomplete. This helper allows + * arbitrary IDs in tests while keeping type safety for other fields. + */ +type TestSettingId = keyof Settings + test.describe('Topbar commands', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') }) test('Should allow registering topbar commands', async ({ comfyPage }) => { await comfyPage.page.evaluate(() => { - window['app'].registerExtension({ + window.app!.registerExtension({ name: 'TestExtension1', commands: [ { id: 'foo', label: 'foo-command', function: () => { - window['foo'] = true + window.foo = true } } ], @@ -31,15 +40,15 @@ test.describe('Topbar commands', () => { }) await comfyPage.menu.topbar.triggerTopbarCommand(['ext', 'foo-command']) - expect(await comfyPage.page.evaluate(() => window['foo'])).toBe(true) + expect(await comfyPage.page.evaluate(() => window.foo)).toBe(true) }) test('Should not allow register command defined in other extension', async ({ comfyPage }) => { - await comfyPage.registerCommand('foo', () => alert(1)) + await comfyPage.command.registerCommand('foo', () => alert(1)) await comfyPage.page.evaluate(() => { - window['app'].registerExtension({ + window.app!.registerExtension({ name: 'TestExtension1', menuCommands: [ { @@ -56,14 +65,14 @@ test.describe('Topbar commands', () => { test('Should allow registering keybindings', async ({ comfyPage }) => { await comfyPage.page.evaluate(() => { - const app = window['app'] + const app = window.app! app.registerExtension({ name: 'TestExtension1', commands: [ { id: 'TestCommand', function: () => { - window['TestCommand'] = true + window.TestCommand = true } } ], @@ -77,68 +86,77 @@ test.describe('Topbar commands', () => { }) await comfyPage.page.keyboard.press('k') - expect(await comfyPage.page.evaluate(() => window['TestCommand'])).toBe( - true - ) + expect(await comfyPage.page.evaluate(() => window.TestCommand)).toBe(true) }) test.describe('Settings', () => { test('Should allow adding settings', async ({ comfyPage }) => { await comfyPage.page.evaluate(() => { - window['app'].registerExtension({ + window.app!.registerExtension({ name: 'TestExtension1', settings: [ { - id: 'TestSetting', + // Extensions can register arbitrary setting IDs + id: 'TestSetting' as TestSettingId, name: 'Test Setting', type: 'text', defaultValue: 'Hello, world!', onChange: () => { - window['changeCount'] = (window['changeCount'] ?? 0) + 1 + window.changeCount = (window.changeCount ?? 0) + 1 } } ] }) }) // onChange is called when the setting is first added - expect(await comfyPage.page.evaluate(() => window['changeCount'])).toBe(1) - expect(await comfyPage.getSetting('TestSetting')).toBe('Hello, world!') + expect(await comfyPage.page.evaluate(() => window.changeCount)).toBe(1) + expect(await comfyPage.settings.getSetting('TestSetting')).toBe( + 'Hello, world!' + ) - await comfyPage.setSetting('TestSetting', 'Hello, universe!') - expect(await comfyPage.getSetting('TestSetting')).toBe('Hello, universe!') - expect(await comfyPage.page.evaluate(() => window['changeCount'])).toBe(2) + await comfyPage.settings.setSetting('TestSetting', 'Hello, universe!') + expect(await comfyPage.settings.getSetting('TestSetting')).toBe( + 'Hello, universe!' + ) + expect(await comfyPage.page.evaluate(() => window.changeCount)).toBe(2) }) test('Should allow setting boolean settings', async ({ comfyPage }) => { await comfyPage.page.evaluate(() => { - window['app'].registerExtension({ + window.app!.registerExtension({ name: 'TestExtension1', settings: [ { - id: 'Comfy.TestSetting', + // Extensions can register arbitrary setting IDs + id: 'Comfy.TestSetting' as TestSettingId, name: 'Test Setting', type: 'boolean', defaultValue: false, onChange: () => { - window['changeCount'] = (window['changeCount'] ?? 0) + 1 + window.changeCount = (window.changeCount ?? 0) + 1 } } ] }) }) - expect(await comfyPage.getSetting('Comfy.TestSetting')).toBe(false) - expect(await comfyPage.page.evaluate(() => window['changeCount'])).toBe(1) + expect(await comfyPage.settings.getSetting('Comfy.TestSetting')).toBe( + false + ) + expect(await comfyPage.page.evaluate(() => window.changeCount)).toBe(1) await comfyPage.settingDialog.open() await comfyPage.settingDialog.toggleBooleanSetting('Comfy.TestSetting') - expect(await comfyPage.getSetting('Comfy.TestSetting')).toBe(true) - expect(await comfyPage.page.evaluate(() => window['changeCount'])).toBe(2) + expect(await comfyPage.settings.getSetting('Comfy.TestSetting')).toBe( + true + ) + expect(await comfyPage.page.evaluate(() => window.changeCount)).toBe(2) }) test.describe('Passing through attrs to setting components', () => { const testCases: Array<{ - config: Partial + config: Pick & + Partial> selector: string }> = [ { @@ -191,13 +209,13 @@ test.describe('Topbar commands', () => { comfyPage }) => { await comfyPage.page.evaluate((config) => { - window['app'].registerExtension({ + window.app!.registerExtension({ name: 'TestExtension1', settings: [ { - id: 'Comfy.TestSetting', + // Extensions can register arbitrary setting IDs + id: 'Comfy.TestSetting' as TestSettingId, name: 'Test', - // The `disabled` attr is common to all settings components attrs: { disabled: true }, ...config } @@ -224,7 +242,7 @@ test.describe('Topbar commands', () => { test.describe('About panel', () => { test('Should allow adding badges', async ({ comfyPage }) => { await comfyPage.page.evaluate(() => { - window['app'].registerExtension({ + window.app!.registerExtension({ name: 'TestExtension1', aboutPageBadges: [ { @@ -247,61 +265,71 @@ test.describe('Topbar commands', () => { test.describe('Dialog', () => { test('Should allow showing a prompt dialog', async ({ comfyPage }) => { await comfyPage.page.evaluate(() => { - void window['app'].extensionManager.dialog - .prompt({ + void window + .app!.extensionManager.dialog.prompt({ title: 'Test Prompt', message: 'Test Prompt Message' }) - .then((value: string) => { - window['value'] = value + .then((value: string | null) => { + ;(window as unknown as Record)['value'] = value }) }) - await comfyPage.fillPromptDialog('Hello, world!') - expect(await comfyPage.page.evaluate(() => window['value'])).toBe( - 'Hello, world!' - ) + await comfyPage.nodeOps.fillPromptDialog('Hello, world!') + expect( + await comfyPage.page.evaluate( + () => (window as unknown as Record)['value'] + ) + ).toBe('Hello, world!') }) test('Should allow showing a confirmation dialog', async ({ comfyPage }) => { await comfyPage.page.evaluate(() => { - void window['app'].extensionManager.dialog - .confirm({ + void window + .app!.extensionManager.dialog.confirm({ title: 'Test Confirm', message: 'Test Confirm Message' }) - .then((value: boolean) => { - window['value'] = value + .then((value: boolean | null) => { + ;(window as unknown as Record)['value'] = value }) }) await comfyPage.confirmDialog.click('confirm') - expect(await comfyPage.page.evaluate(() => window['value'])).toBe(true) + expect( + await comfyPage.page.evaluate( + () => (window as unknown as Record)['value'] + ) + ).toBe(true) }) test('Should allow dismissing a dialog', async ({ comfyPage }) => { await comfyPage.page.evaluate(() => { - window['value'] = 'foo' - void window['app'].extensionManager.dialog - .confirm({ + ;(window as unknown as Record)['value'] = 'foo' + void window + .app!.extensionManager.dialog.confirm({ title: 'Test Confirm', message: 'Test Confirm Message' }) - .then((value: boolean) => { - window['value'] = value + .then((value: boolean | null) => { + ;(window as unknown as Record)['value'] = value }) }) await comfyPage.confirmDialog.click('reject') - expect(await comfyPage.page.evaluate(() => window['value'])).toBeNull() + expect( + await comfyPage.page.evaluate( + () => (window as unknown as Record)['value'] + ) + ).toBeNull() }) }) test.describe('Selection Toolbox', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true) + await comfyPage.settings.setSetting('Comfy.Canvas.SelectionToolbox', true) }) test('Should allow adding commands to selection toolbox', async ({ @@ -309,7 +337,7 @@ test.describe('Topbar commands', () => { }) => { // Register an extension with a selection toolbox command await comfyPage.page.evaluate(() => { - window['app'].registerExtension({ + window.app!.registerExtension({ name: 'TestExtension1', commands: [ { @@ -317,7 +345,9 @@ test.describe('Topbar commands', () => { label: 'Test Command', icon: 'pi pi-star', function: () => { - window['selectionCommandExecuted'] = true + ;(window as unknown as Record)[ + 'selectionCommandExecuted' + ] = true } } ], @@ -325,7 +355,7 @@ test.describe('Topbar commands', () => { }) }) - await comfyPage.selectNodes(['CLIP Text Encode (Prompt)']) + await comfyPage.nodeOps.selectNodes(['CLIP Text Encode (Prompt)']) // Click the command button in the selection toolbox const toolboxButton = comfyPage.page.locator( @@ -333,9 +363,13 @@ test.describe('Topbar commands', () => { ) await toolboxButton.click() - // Verify the command was executed expect( - await comfyPage.page.evaluate(() => window['selectionCommandExecuted']) + await comfyPage.page.evaluate( + () => + (window as unknown as Record)[ + 'selectionCommandExecuted' + ] + ) ).toBe(true) }) }) diff --git a/browser_tests/tests/featureFlags.spec.ts b/browser_tests/tests/featureFlags.spec.ts index 73f1cd2f5..aec0fc5bc 100644 --- a/browser_tests/tests/featureFlags.spec.ts +++ b/browser_tests/tests/featureFlags.spec.ts @@ -3,7 +3,7 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => { @@ -25,7 +25,7 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => { const originalSend = WebSocket.prototype.send WebSocket.prototype.send = function (data) { try { - const parsed = JSON.parse(data) + const parsed = JSON.parse(data as string) if (parsed.type === 'feature_flags') { window.__capturedMessages!.clientFeatureFlags = parsed } @@ -38,11 +38,11 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => { // Monitor for server feature flags const checkInterval = setInterval(() => { if ( - window['app']?.api?.serverFeatureFlags && - Object.keys(window['app'].api.serverFeatureFlags).length > 0 + window.app?.api?.serverFeatureFlags && + Object.keys(window.app.api.serverFeatureFlags).length > 0 ) { window.__capturedMessages!.serverFeatureFlags = - window['app'].api.serverFeatureFlags + window.app.api.serverFeatureFlags clearInterval(checkInterval) } }, 100) @@ -96,7 +96,7 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => { }) => { // Get the actual server feature flags from the backend const serverFlags = await comfyPage.page.evaluate(() => { - return window['app']!.api.serverFeatureFlags + return window.app!.api.serverFeatureFlags }) // Verify we received real feature flags from the backend @@ -115,26 +115,22 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => { }) => { // Test serverSupportsFeature with real backend flags const supportsPreviewMetadata = await comfyPage.page.evaluate(() => { - return window['app']!.api.serverSupportsFeature( - 'supports_preview_metadata' - ) + return window.app!.api.serverSupportsFeature('supports_preview_metadata') }) // The method should return a boolean based on the backend's value expect(typeof supportsPreviewMetadata).toBe('boolean') // Test non-existent feature - should always return false const supportsNonExistent = await comfyPage.page.evaluate(() => { - return window['app']!.api.serverSupportsFeature( - 'non_existent_feature_xyz' - ) + return window.app!.api.serverSupportsFeature('non_existent_feature_xyz') }) expect(supportsNonExistent).toBe(false) // Test that the method only returns true for boolean true values const testResults = await comfyPage.page.evaluate(() => { // Temporarily modify serverFeatureFlags to test behavior - const original = window['app']!.api.serverFeatureFlags - window['app']!.api.serverFeatureFlags = { + const original = window.app!.api.serverFeatureFlags + window.app!.api.serverFeatureFlags = { bool_true: true, bool_false: false, string_value: 'yes', @@ -143,15 +139,15 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => { } const results = { - bool_true: window['app']!.api.serverSupportsFeature('bool_true'), - bool_false: window['app']!.api.serverSupportsFeature('bool_false'), - string_value: window['app']!.api.serverSupportsFeature('string_value'), - number_value: window['app']!.api.serverSupportsFeature('number_value'), - null_value: window['app']!.api.serverSupportsFeature('null_value') + bool_true: window.app!.api.serverSupportsFeature('bool_true'), + bool_false: window.app!.api.serverSupportsFeature('bool_false'), + string_value: window.app!.api.serverSupportsFeature('string_value'), + number_value: window.app!.api.serverSupportsFeature('number_value'), + null_value: window.app!.api.serverSupportsFeature('null_value') } // Restore original - window['app']!.api.serverFeatureFlags = original + window.app!.api.serverFeatureFlags = original return results }) @@ -168,20 +164,20 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => { }) => { // Test getServerFeature method const previewMetadataValue = await comfyPage.page.evaluate(() => { - return window['app']!.api.getServerFeature('supports_preview_metadata') + return window.app!.api.getServerFeature('supports_preview_metadata') }) expect(typeof previewMetadataValue).toBe('boolean') // Test getting max_upload_size const maxUploadSize = await comfyPage.page.evaluate(() => { - return window['app']!.api.getServerFeature('max_upload_size') + return window.app!.api.getServerFeature('max_upload_size') }) expect(typeof maxUploadSize).toBe('number') expect(maxUploadSize).toBeGreaterThan(0) // Test getServerFeature with default value for non-existent feature const defaultValue = await comfyPage.page.evaluate(() => { - return window['app']!.api.getServerFeature( + return window.app!.api.getServerFeature( 'non_existent_feature_xyz', 'default' ) @@ -194,7 +190,7 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => { }) => { // Test getServerFeatures returns all flags const allFeatures = await comfyPage.page.evaluate(() => { - return window['app']!.api.getServerFeatures() + return window.app!.api.getServerFeatures() }) expect(allFeatures).toBeTruthy() @@ -207,14 +203,14 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => { test('Client feature flags are immutable', async ({ comfyPage }) => { // Test that getClientFeatureFlags returns a copy const immutabilityTest = await comfyPage.page.evaluate(() => { - const flags1 = window['app']!.api.getClientFeatureFlags() - const flags2 = window['app']!.api.getClientFeatureFlags() + const flags1 = window.app!.api.getClientFeatureFlags() + const flags2 = window.app!.api.getClientFeatureFlags() // Modify the first object flags1.test_modification = true // Get flags again to check if original was modified - const flags3 = window['app']!.api.getClientFeatureFlags() + const flags3 = window.app!.api.getClientFeatureFlags() return { areEqual: flags1 === flags2, @@ -240,14 +236,14 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => { }) => { const immutabilityTest = await comfyPage.page.evaluate(() => { // Get a copy of server features - const features1 = window['app']!.api.getServerFeatures() + const features1 = window.app!.api.getServerFeatures() // Try to modify it features1.supports_preview_metadata = false features1.new_feature = 'added' // Get another copy - const features2 = window['app']!.api.getServerFeatures() + const features2 = window.app!.api.getServerFeatures() return { modifiedValue: features1.supports_preview_metadata, @@ -286,35 +282,26 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => { // Monitor when feature flags arrive by checking periodically const checkFeatureFlags = setInterval(() => { if ( - window['app']?.api?.serverFeatureFlags?.supports_preview_metadata !== + window.app?.api?.serverFeatureFlags?.supports_preview_metadata !== undefined ) { - window.__appReadiness = { - ...window.__appReadiness, - featureFlagsReceived: true - } + window.__appReadiness!.featureFlagsReceived = true clearInterval(checkFeatureFlags) } }, 10) // Monitor API initialization const checkApi = setInterval(() => { - if (window['app']?.api) { - window.__appReadiness = { - ...window.__appReadiness, - apiInitialized: true - } + if (window.app?.api) { + window.__appReadiness!.apiInitialized = true clearInterval(checkApi) } }, 10) // Monitor app initialization const checkApp = setInterval(() => { - if (window['app']?.graph) { - window.__appReadiness = { - ...window.__appReadiness, - appInitialized: true - } + if (window.app?.graph) { + window.__appReadiness!.appInitialized = true clearInterval(checkApp) } }, 10) @@ -333,7 +320,7 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => { // Wait for feature flags to be received await newPage.waitForFunction( () => - window['app']?.api?.serverFeatureFlags?.supports_preview_metadata !== + window.app?.api?.serverFeatureFlags?.supports_preview_metadata !== undefined, { timeout: 10000 @@ -344,7 +331,7 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => { const readiness = await newPage.evaluate(() => { return { ...window.__appReadiness, - currentFlags: window['app']!.api.serverFeatureFlags + currentFlags: window.app!.api.serverFeatureFlags } }) diff --git a/browser_tests/tests/graph.spec.ts b/browser_tests/tests/graph.spec.ts index 340b5b697..9ddd1e48c 100644 --- a/browser_tests/tests/graph.spec.ts +++ b/browser_tests/tests/graph.spec.ts @@ -3,24 +3,24 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Graph', { tag: ['@smoke', '@canvas'] }, () => { // Should be able to fix link input slot index after swap the input order // Ref: https://github.com/Comfy-Org/ComfyUI_frontend/issues/3348 test('Fix link input slots', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('inputs/input_order_swap') + await comfyPage.workflow.loadWorkflow('inputs/input_order_swap') expect( await comfyPage.page.evaluate(() => { - return window['app'].graph.links.get(1)?.target_slot + return window.app!.graph!.links.get(1)?.target_slot }) ).toBe(1) }) test('Validate workflow links', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Validation.Workflows', true) - await comfyPage.loadWorkflow('links/bad_link') - await expect(comfyPage.getVisibleToastCount()).resolves.toBe(2) + await comfyPage.settings.setSetting('Comfy.Validation.Workflows', true) + await comfyPage.workflow.loadWorkflow('links/bad_link') + await expect.poll(() => comfyPage.toast.getVisibleToastCount()).toBe(2) }) }) diff --git a/browser_tests/tests/graphCanvasMenu.spec.ts b/browser_tests/tests/graphCanvasMenu.spec.ts index 4e6d1b2fb..168518898 100644 --- a/browser_tests/tests/graphCanvasMenu.spec.ts +++ b/browser_tests/tests/graphCanvasMenu.spec.ts @@ -1,34 +1,37 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +import { TestIds } from '../fixtures/selectors' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Graph Canvas Menu', { tag: ['@screenshot', '@canvas'] }, () => { test.beforeEach(async ({ comfyPage }) => { // Set link render mode to spline to make sure it's not affected by other tests' // side effects. - await comfyPage.setSetting('Comfy.LinkRenderMode', 2) + await comfyPage.settings.setSetting('Comfy.LinkRenderMode', 2) // Enable canvas menu for all tests - await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true) + await comfyPage.settings.setSetting('Comfy.Graph.CanvasMenu', true) }) test( 'Can toggle link visibility', { tag: '@screenshot' }, async ({ comfyPage }) => { - const button = comfyPage.page.getByTestId('toggle-link-visibility-button') + const button = comfyPage.page.getByTestId( + TestIds.canvas.toggleLinkVisibilityButton + ) await button.click() await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot( 'canvas-with-hidden-links.png' ) const hiddenLinkRenderMode = await comfyPage.page.evaluate(() => { - return window['LiteGraph'].HIDDEN_LINK + return window.LiteGraph!.HIDDEN_LINK }) - expect(await comfyPage.getSetting('Comfy.LinkRenderMode')).toBe( + expect(await comfyPage.settings.getSetting('Comfy.LinkRenderMode')).toBe( hiddenLinkRenderMode ) @@ -37,16 +40,18 @@ test.describe('Graph Canvas Menu', { tag: ['@screenshot', '@canvas'] }, () => { await expect(comfyPage.canvas).toHaveScreenshot( 'canvas-with-visible-links.png' ) - expect(await comfyPage.getSetting('Comfy.LinkRenderMode')).not.toBe( - hiddenLinkRenderMode - ) + expect( + await comfyPage.settings.getSetting('Comfy.LinkRenderMode') + ).not.toBe(hiddenLinkRenderMode) } ) test('Toggle minimap button is clickable and has correct test id', async ({ comfyPage }) => { - const minimapButton = comfyPage.page.getByTestId('toggle-minimap-button') + const minimapButton = comfyPage.page.getByTestId( + TestIds.canvas.toggleMinimapButton + ) await expect(minimapButton).toBeVisible() await expect(minimapButton).toBeEnabled() diff --git a/browser_tests/tests/groupNode.spec.ts b/browser_tests/tests/groupNode.spec.ts index 9dbbe58f8..6af103c8f 100644 --- a/browser_tests/tests/groupNode.spec.ts +++ b/browser_tests/tests/groupNode.spec.ts @@ -1,11 +1,15 @@ import { expect } from '@playwright/test' +import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema' + import type { ComfyPage } from '../fixtures/ComfyPage' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +import type { NodeLibrarySidebarTab } from '../fixtures/components/SidebarTab' +import { DefaultGraphPositions } from '../fixtures/constants/defaultGraphPositions' import type { NodeReference } from '../fixtures/utils/litegraphUtils' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Group Node', { tag: '@node' }, () => { @@ -13,30 +17,34 @@ test.describe('Group Node', { tag: '@node' }, () => { const groupNodeName = 'DefautWorkflowGroupNode' const groupNodeCategory = 'group nodes>workflow' const groupNodeBookmarkName = `workflow>${groupNodeName}` - let libraryTab + let libraryTab: NodeLibrarySidebarTab test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') libraryTab = comfyPage.menu.nodeLibraryTab - await comfyPage.convertAllNodesToGroupNode(groupNodeName) + await comfyPage.nodeOps.convertAllNodesToGroupNode(groupNodeName) await libraryTab.open() }) - test('Is added to node library sidebar', async ({ comfyPage }) => { - expect(await libraryTab.getFolder('group nodes').count()).toBe(1) + test('Is added to node library sidebar', async ({ + comfyPage: _comfyPage + }) => { + expect(await libraryTab.getFolder(groupNodeCategory).count()).toBe(1) }) test('Can be added to canvas using node library sidebar', async ({ comfyPage }) => { - const initialNodeCount = await comfyPage.getGraphNodesCount() + const initialNodeCount = await comfyPage.nodeOps.getGraphNodesCount() // Add group node from node library sidebar await libraryTab.getFolder(groupNodeCategory).click() await libraryTab.getNode(groupNodeName).click() // Verify the node is added to the canvas - expect(await comfyPage.getGraphNodesCount()).toBe(initialNodeCount + 1) + expect(await comfyPage.nodeOps.getGraphNodesCount()).toBe( + initialNodeCount + 1 + ) }) test('Can be bookmarked and unbookmarked', async ({ comfyPage }) => { @@ -48,7 +56,7 @@ test.describe('Group Node', { tag: '@node' }, () => { // Verify the node is added to the bookmarks tab expect( - await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') + await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2') ).toEqual([groupNodeBookmarkName]) // Verify the bookmark node with the same name is added to the tree expect(await libraryTab.getNode(groupNodeName).count()).not.toBe(0) @@ -62,7 +70,7 @@ test.describe('Group Node', { tag: '@node' }, () => { // Verify the node is removed from the bookmarks tab expect( - await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') + await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2') ).toHaveLength(0) }) @@ -94,8 +102,8 @@ test.describe('Group Node', { tag: '@node' }, () => { { tag: '@screenshot' }, async ({ comfyPage }) => { const groupNodeName = 'DefautWorkflowGroupNode' - await comfyPage.convertAllNodesToGroupNode(groupNodeName) - await comfyPage.doubleClickCanvas() + await comfyPage.nodeOps.convertAllNodesToGroupNode(groupNodeName) + await comfyPage.canvasOps.doubleClick() await comfyPage.nextFrame() await comfyPage.searchBox.fillAndSelectFirstNode(groupNodeName) await expect(comfyPage.canvas).toHaveScreenshot( @@ -105,8 +113,8 @@ test.describe('Group Node', { tag: '@node' }, () => { ) test('Displays tooltip on title hover', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.EnableTooltips', true) - await comfyPage.convertAllNodesToGroupNode('Group Node') + await comfyPage.settings.setSetting('Comfy.EnableTooltips', true) + await comfyPage.nodeOps.convertAllNodesToGroupNode('Group Node') await comfyPage.page.mouse.move(47, 173) await expect(comfyPage.page.locator('.node-tooltip')).toBeVisible() }) @@ -114,9 +122,9 @@ test.describe('Group Node', { tag: '@node' }, () => { test('Manage group opens with the correct group selected', async ({ comfyPage }) => { - const makeGroup = async (name, type1, type2) => { - const node1 = (await comfyPage.getNodeRefsByType(type1))[0] - const node2 = (await comfyPage.getNodeRefsByType(type2))[0] + const makeGroup = async (name: string, type1: string, type2: string) => { + const node1 = (await comfyPage.nodeOps.getNodeRefsByType(type1))[0] + const node2 = (await comfyPage.nodeOps.getNodeRefsByType(type2))[0] await node1.click('title') await node2.click('title', { modifiers: ['Shift'] @@ -144,7 +152,7 @@ test.describe('Group Node', { tag: '@node' }, () => { test('Preserves hidden input configuration when containing duplicate node types', async ({ comfyPage }) => { - await comfyPage.loadWorkflow( + await comfyPage.workflow.loadWorkflow( 'groupnodes/group_node_identical_nodes_hidden_inputs' ) await comfyPage.nextFrame() @@ -155,16 +163,14 @@ test.describe('Group Node', { tag: '@node' }, () => { const totalInputCount = await comfyPage.page.evaluate((nodeName) => { const { extra: { groupNodes } - } = window['app'].graph - const { nodes } = groupNodes[nodeName] - return nodes.reduce((acc: number, node) => { - return acc + node.inputs.length - }, 0) + } = window.app!.graph! + const { nodes } = groupNodes![nodeName] + return nodes.reduce((acc, node) => acc + (node.inputs?.length ?? 0), 0) }, groupNodeName) const visibleInputCount = await comfyPage.page.evaluate((id) => { - const node = window['app'].graph.getNodeById(id) - return node.inputs.length + const node = window.app!.graph!.getNodeById(id) + return node!.inputs.length }, groupNodeId) // Verify there are 4 total inputs (2 VAE decode nodes with 2 inputs each) @@ -178,7 +184,7 @@ test.describe('Group Node', { tag: '@node' }, () => { comfyPage }) => { const expectSingleNode = async (type: string) => { - const nodes = await comfyPage.getNodeRefsByType(type) + const nodes = await comfyPage.nodeOps.getNodeRefsByType(type) expect(nodes).toHaveLength(1) return nodes[0] } @@ -213,8 +219,8 @@ test.describe('Group Node', { tag: '@node' }, () => { test('Loads from a workflow using the legacy path separator ("/")', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('groupnodes/legacy_group_node') - expect(await comfyPage.getGraphNodesCount()).toBe(1) + await comfyPage.workflow.loadWorkflow('groupnodes/legacy_group_node') + expect(await comfyPage.nodeOps.getGraphNodesCount()).toBe(1) await expect( comfyPage.page.locator('.comfy-missing-nodes') ).not.toBeVisible() @@ -230,7 +236,7 @@ test.describe('Group Node', { tag: '@node' }, () => { const isRegisteredLitegraph = async (comfyPage: ComfyPage) => { return await comfyPage.page.evaluate((nodeType: string) => { - return !!window['LiteGraph'].registered_node_types[nodeType] + return !!window.LiteGraph!.registered_node_types[nodeType] }, GROUP_NODE_TYPE) } @@ -246,17 +252,17 @@ test.describe('Group Node', { tag: '@node' }, () => { comfyPage: ComfyPage, expectedCount: number ) => { - expect(await comfyPage.getNodeRefsByType(GROUP_NODE_TYPE)).toHaveLength( - expectedCount - ) + expect( + await comfyPage.nodeOps.getNodeRefsByType(GROUP_NODE_TYPE) + ).toHaveLength(expectedCount) expect(await isRegisteredLitegraph(comfyPage)).toBe(true) expect(await isRegisteredNodeDefStore(comfyPage)).toBe(true) } test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') - await comfyPage.loadWorkflow(WORKFLOW_NAME) - groupNode = await comfyPage.getFirstNodeRef() + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.workflow.loadWorkflow(WORKFLOW_NAME) + groupNode = await comfyPage.nodeOps.getFirstNodeRef() if (!groupNode) throw new Error(`Group node not found in workflow ${WORKFLOW_NAME}`) await groupNode.copy() @@ -265,7 +271,7 @@ test.describe('Group Node', { tag: '@node' }, () => { test('Copies and pastes group node within the same workflow', async ({ comfyPage }) => { - await comfyPage.ctrlV() + await comfyPage.clipboard.paste() await verifyNodeLoaded(comfyPage, 2) }) @@ -273,12 +279,12 @@ test.describe('Group Node', { tag: '@node' }, () => { comfyPage }) => { // Set setting - await comfyPage.setSetting('Comfy.ConfirmClear', false) + await comfyPage.settings.setSetting('Comfy.ConfirmClear', false) // Clear workflow - await comfyPage.executeCommand('Comfy.ClearWorkflow') + await comfyPage.command.executeCommand('Comfy.ClearWorkflow') - await comfyPage.ctrlV() + await comfyPage.clipboard.paste() await verifyNodeLoaded(comfyPage, 1) }) @@ -286,15 +292,15 @@ test.describe('Group Node', { tag: '@node' }, () => { comfyPage }) => { await comfyPage.menu.topbar.triggerTopbarCommand(['New']) - await comfyPage.ctrlV() + await comfyPage.clipboard.paste() await verifyNodeLoaded(comfyPage, 1) }) test('Copies and pastes group node across different workflows', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('default') - await comfyPage.ctrlV() + await comfyPage.workflow.loadWorkflow('default') + await comfyPage.clipboard.paste() await verifyNodeLoaded(comfyPage, 1) }) @@ -302,14 +308,15 @@ test.describe('Group Node', { tag: '@node' }, () => { comfyPage }) => { await comfyPage.menu.topbar.triggerTopbarCommand(['New']) - await comfyPage.ctrlV() + await comfyPage.clipboard.paste() const currentGraphState = await comfyPage.page.evaluate(() => - window['app'].graph.serialize() + window.app!.graph!.serialize() ) await test.step('Load workflow containing a group node pasted from a different workflow', async () => { await comfyPage.page.evaluate( - (workflow) => window['app'].loadGraphData(workflow), + (workflow) => + window.app!.loadGraphData(workflow as ComfyWorkflowJSON), currentGraphState ) await comfyPage.nextFrame() @@ -320,15 +327,18 @@ test.describe('Group Node', { tag: '@node' }, () => { test.describe('Keybindings', () => { test('Convert to group node, no selection', async ({ comfyPage }) => { - expect(await comfyPage.getVisibleToastCount()).toBe(0) + await expect.poll(() => comfyPage.toast.getVisibleToastCount()).toBe(0) await comfyPage.page.keyboard.press('Alt+g') - expect(await comfyPage.getVisibleToastCount()).toBe(1) + await expect.poll(() => comfyPage.toast.getVisibleToastCount()).toBe(1) }) test('Convert to group node, selected 1 node', async ({ comfyPage }) => { - expect(await comfyPage.getVisibleToastCount()).toBe(0) - await comfyPage.clickTextEncodeNode1() + await expect.poll(() => comfyPage.toast.getVisibleToastCount()).toBe(0) + await comfyPage.canvas.click({ + position: DefaultGraphPositions.textEncodeNode1 + }) + await comfyPage.nextFrame() await comfyPage.page.keyboard.press('Alt+g') - expect(await comfyPage.getVisibleToastCount()).toBe(1) + await expect.poll(() => comfyPage.toast.getVisibleToastCount()).toBe(1) }) }) }) diff --git a/browser_tests/tests/interaction.spec.ts b/browser_tests/tests/interaction.spec.ts index a7bcc651d..87c3e8a0b 100644 --- a/browser_tests/tests/interaction.spec.ts +++ b/browser_tests/tests/interaction.spec.ts @@ -7,15 +7,17 @@ import { testComfySnapToGridGridSize } from '../fixtures/ComfyPage' import type { ComfyPage } from '../fixtures/ComfyPage' +import { DefaultGraphPositions } from '../fixtures/constants/defaultGraphPositions' +import { TestIds } from '../fixtures/selectors' import type { NodeReference } from '../fixtures/utils/litegraphUtils' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Item Interaction', { tag: ['@screenshot', '@node'] }, () => { test('Can select/delete all items', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('groups/mixed_graph_items') + await comfyPage.workflow.loadWorkflow('groups/mixed_graph_items') await comfyPage.canvas.press('Control+a') await expect(comfyPage.canvas).toHaveScreenshot('selected-all.png') await comfyPage.canvas.press('Delete') @@ -23,7 +25,7 @@ test.describe('Item Interaction', { tag: ['@screenshot', '@node'] }, () => { }) test('Can pin/unpin items with keyboard shortcut', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('groups/mixed_graph_items') + await comfyPage.workflow.loadWorkflow('groups/mixed_graph_items') await comfyPage.canvas.press('Control+a') await comfyPage.canvas.press('KeyP') await comfyPage.nextFrame() @@ -51,11 +53,13 @@ test.describe('Node Interaction', () => { test(`Can add multiple nodes to selection using ${modifier}+Click`, async ({ comfyPage }) => { - const clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode') + const clipNodes = + await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode') for (const node of clipNodes) { await node.click('title', { modifiers: [modifier] }) } - const selectedNodeCount = await comfyPage.getSelectedGraphNodesCount() + const selectedNodeCount = + await comfyPage.nodeOps.getSelectedGraphNodesCount() expect(selectedNodeCount).toBe(clipNodes.length) }) }) @@ -65,9 +69,15 @@ test.describe('Node Interaction', () => { { tag: '@screenshot' }, async ({ comfyPage }) => { await expect(comfyPage.canvas).toHaveScreenshot('default.png') - await comfyPage.clickTextEncodeNode1() + await comfyPage.canvas.click({ + position: DefaultGraphPositions.textEncodeNode1 + }) + await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot('selected-node1.png') - await comfyPage.clickTextEncodeNode2() + await comfyPage.canvas.click({ + position: DefaultGraphPositions.textEncodeNode2 + }) + await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot('selected-node2.png') } ) @@ -80,7 +90,7 @@ test.describe('Node Interaction', () => { const clipNode2Pos = await clipNodes[1].getPosition() const offset = 64 await comfyPage.page.keyboard.down('Meta') - await comfyPage.dragAndDrop( + await comfyPage.canvasOps.dragAndDrop( { x: Math.min(clipNode1Pos.x, clipNode2Pos.x) - offset, y: Math.min(clipNode1Pos.y, clipNode2Pos.y) - offset @@ -94,9 +104,10 @@ test.describe('Node Interaction', () => { } test('Can drag-select nodes with Meta (mac)', async ({ comfyPage }) => { - const clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode') + const clipNodes = + await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode') await dragSelectNodes(comfyPage, clipNodes) - expect(await comfyPage.getSelectedGraphNodesCount()).toBe( + expect(await comfyPage.nodeOps.getSelectedGraphNodesCount()).toBe( clipNodes.length ) }) @@ -104,7 +115,8 @@ test.describe('Node Interaction', () => { test('Can move selected nodes using the Comfy.Canvas.MoveSelectedNodes.{Up|Down|Left|Right} commands', async ({ comfyPage }) => { - const clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode') + const clipNodes = + await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode') const getPositions = () => Promise.all(clipNodes.map((node) => node.getPosition())) const testDirection = async ({ @@ -116,7 +128,7 @@ test.describe('Node Interaction', () => { }) => { const originalPositions = await getPositions() await dragSelectNodes(comfyPage, clipNodes) - await comfyPage.executeCommand( + await comfyPage.command.executeCommand( `Comfy.Canvas.MoveSelectedNodes.${direction}` ) await comfyPage.canvas.press(`Control+Arrow${direction}`) @@ -155,14 +167,20 @@ test.describe('Node Interaction', () => { }) test('Can drag node', { tag: '@screenshot' }, async ({ comfyPage }) => { - await comfyPage.dragNode2() + await comfyPage.nodeOps.dragTextEncodeNode2() await expect(comfyPage.canvas).toHaveScreenshot('dragged-node1.png') }) test.describe('Edge Interaction', { tag: '@screenshot' }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.LinkRelease.Action', 'no action') - await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'no action') + await comfyPage.settings.setSetting( + 'Comfy.LinkRelease.Action', + 'no action' + ) + await comfyPage.settings.setSetting( + 'Comfy.LinkRelease.ActionShift', + 'no action' + ) }) // Test both directions of edge connection. @@ -170,11 +188,11 @@ test.describe('Node Interaction', () => { test(`Can disconnect/connect edge ${reverse ? 'reverse' : 'normal'}`, async ({ comfyPage }) => { - await comfyPage.disconnectEdge() + await comfyPage.canvasOps.disconnectEdge() await expect(comfyPage.canvas).toHaveScreenshot('disconnected-edge.png') - await comfyPage.connectEdge({ reverse }) + await comfyPage.canvasOps.connectEdge({ reverse }) // Move mouse to empty area to avoid slot highlight. - await comfyPage.moveMouseToEmptyArea() + await comfyPage.canvasOps.moveMouseToEmptyArea() // Litegraph renders edge with a slight offset. await expect(comfyPage.canvas).toHaveScreenshot('default.png', { maxDiffPixels: 50 @@ -183,14 +201,14 @@ test.describe('Node Interaction', () => { }) test('Can move link', async ({ comfyPage }) => { - await comfyPage.dragAndDrop( - comfyPage.clipTextEncodeNode1InputSlot, - comfyPage.emptySpace + await comfyPage.canvasOps.dragAndDrop( + DefaultGraphPositions.clipTextEncodeNode1InputSlot, + DefaultGraphPositions.emptySpace ) await expect(comfyPage.canvas).toHaveScreenshot('disconnected-edge.png') - await comfyPage.dragAndDrop( - comfyPage.clipTextEncodeNode2InputSlot, - comfyPage.clipTextEncodeNode1InputSlot + await comfyPage.canvasOps.dragAndDrop( + DefaultGraphPositions.clipTextEncodeNode2InputSlot, + DefaultGraphPositions.clipTextEncodeNode1InputSlot ) await expect(comfyPage.canvas).toHaveScreenshot('moved-link.png') }) @@ -199,15 +217,15 @@ test.describe('Node Interaction', () => { test.skip('Can copy link by shift-drag existing link', async ({ comfyPage }) => { - await comfyPage.dragAndDrop( - comfyPage.clipTextEncodeNode1InputSlot, - comfyPage.emptySpace + await comfyPage.canvasOps.dragAndDrop( + DefaultGraphPositions.clipTextEncodeNode1InputSlot, + DefaultGraphPositions.emptySpace ) await expect(comfyPage.canvas).toHaveScreenshot('disconnected-edge.png') await comfyPage.page.keyboard.down('Shift') - await comfyPage.dragAndDrop( - comfyPage.clipTextEncodeNode2InputLinkPath, - comfyPage.clipTextEncodeNode1InputSlot + await comfyPage.canvasOps.dragAndDrop( + DefaultGraphPositions.clipTextEncodeNode2InputLinkPath, + DefaultGraphPositions.clipTextEncodeNode1InputSlot ) await comfyPage.page.keyboard.up('Shift') await expect(comfyPage.canvas).toHaveScreenshot('copied-link.png') @@ -217,11 +235,11 @@ test.describe('Node Interaction', () => { comfyPage, comfyMouse }) => { - await comfyPage.setSetting('Comfy.Node.AutoSnapLinkToSlot', true) - await comfyPage.setSetting('Comfy.Node.SnapHighlightsNode', true) + await comfyPage.settings.setSetting('Comfy.Node.AutoSnapLinkToSlot', true) + await comfyPage.settings.setSetting('Comfy.Node.SnapHighlightsNode', true) - await comfyMouse.move(comfyPage.clipTextEncodeNode1InputSlot) - await comfyMouse.drag(comfyPage.clipTextEncodeNode2InputSlot) + await comfyMouse.move(DefaultGraphPositions.clipTextEncodeNode1InputSlot) + await comfyMouse.drag(DefaultGraphPositions.clipTextEncodeNode2InputSlot) await expect(comfyPage.canvas).toHaveScreenshot('snapped-highlighted.png') }) }) @@ -230,7 +248,7 @@ test.describe('Node Interaction', () => { 'Can adjust widget value', { tag: '@screenshot' }, async ({ comfyPage }) => { - await comfyPage.adjustWidgetValue() + await comfyPage.nodeOps.adjustEmptyLatentWidth() await expect(comfyPage.canvas).toHaveScreenshot( 'adjusted-widget-value.png' ) @@ -238,7 +256,7 @@ test.describe('Node Interaction', () => { ) test('Link snap to slot', { tag: '@screenshot' }, async ({ comfyPage }) => { - await comfyPage.loadWorkflow('links/snap_to_slot') + await comfyPage.workflow.loadWorkflow('links/snap_to_slot') await expect(comfyPage.canvas).toHaveScreenshot('snap_to_slot.png') const outputSlotPos = { @@ -249,7 +267,7 @@ test.describe('Node Interaction', () => { x: 748, y: 77 } - await comfyPage.dragAndDrop(outputSlotPos, samplerNodeCenterPos) + await comfyPage.canvasOps.dragAndDrop(outputSlotPos, samplerNodeCenterPos) await expect(comfyPage.canvas).toHaveScreenshot('snap_to_slot_linked.png') }) @@ -258,7 +276,7 @@ test.describe('Node Interaction', () => { 'Can batch move links by drag with shift', { tag: '@screenshot' }, async ({ comfyPage }) => { - await comfyPage.loadWorkflow('links/batch_move_links') + await comfyPage.workflow.loadWorkflow('links/batch_move_links') await expect(comfyPage.canvas).toHaveScreenshot('batch_move_links.png') const outputSlot1Pos = { @@ -271,7 +289,7 @@ test.describe('Node Interaction', () => { } await comfyPage.page.keyboard.down('Shift') - await comfyPage.dragAndDrop(outputSlot1Pos, outputSlot2Pos) + await comfyPage.canvasOps.dragAndDrop(outputSlot1Pos, outputSlot2Pos) await comfyPage.page.keyboard.up('Shift') await expect(comfyPage.canvas).toHaveScreenshot( @@ -304,12 +322,18 @@ test.describe('Node Interaction', () => { { tag: '@screenshot' }, async ({ comfyPage }) => { await expect(comfyPage.canvas).toHaveScreenshot('default.png') - await comfyPage.clickTextEncodeNodeToggler() + await comfyPage.canvas.click({ + position: DefaultGraphPositions.textEncodeNodeToggler + }) + await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot( 'text-encode-toggled-off.png' ) await comfyPage.delay(1000) - await comfyPage.clickTextEncodeNodeToggler() + await comfyPage.canvas.click({ + position: DefaultGraphPositions.textEncodeNodeToggler + }) + await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot( 'text-encode-toggled-back-open.png' ) @@ -345,7 +369,7 @@ test.describe('Node Interaction', () => { x: 167, y: 143 } - await comfyPage.loadWorkflow('nodes/single_save_image_node') + await comfyPage.workflow.loadWorkflow('nodes/single_save_image_node') await comfyPage.canvas.click({ position: textWidgetPos }) @@ -365,7 +389,7 @@ test.describe('Node Interaction', () => { 'Can double click node title to edit', { tag: '@screenshot' }, async ({ comfyPage }) => { - await comfyPage.loadWorkflow('nodes/single_ksampler') + await comfyPage.workflow.loadWorkflow('nodes/single_ksampler') await comfyPage.canvas.dblclick({ position: { x: 50, @@ -382,7 +406,7 @@ test.describe('Node Interaction', () => { test('Double click node body does not trigger edit', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('nodes/single_ksampler') + await comfyPage.workflow.loadWorkflow('nodes/single_ksampler') await comfyPage.canvas.dblclick({ position: { x: 50, @@ -397,8 +421,11 @@ test.describe('Node Interaction', () => { 'Can group selected nodes', { tag: '@screenshot' }, async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.GroupSelectedNodes.Padding', 10) - await comfyPage.select2Nodes() + await comfyPage.settings.setSetting( + 'Comfy.GroupSelectedNodes.Padding', + 10 + ) + await comfyPage.nodeOps.selectNodes(['CLIP Text Encode (Prompt)']) await comfyPage.page.keyboard.down('Control') await comfyPage.page.keyboard.press('KeyG') await comfyPage.page.keyboard.up('Control') @@ -416,10 +443,10 @@ test.describe('Node Interaction', () => { 'Can fit group to contents', { tag: '@screenshot' }, async ({ comfyPage }) => { - await comfyPage.loadWorkflow('groups/oversized_group') - await comfyPage.ctrlA() + await comfyPage.workflow.loadWorkflow('groups/oversized_group') + await comfyPage.keyboard.selectAll() await comfyPage.nextFrame() - await comfyPage.executeCommand('Comfy.Graph.FitGroupToContents') + await comfyPage.command.executeCommand('Comfy.Graph.FitGroupToContents') await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot( 'group-fit-to-contents.png' @@ -428,11 +455,15 @@ test.describe('Node Interaction', () => { ) test('Can pin/unpin nodes', { tag: '@screenshot' }, async ({ comfyPage }) => { - await comfyPage.select2Nodes() - await comfyPage.executeCommand('Comfy.Canvas.ToggleSelectedNodes.Pin') + await comfyPage.nodeOps.selectNodes(['CLIP Text Encode (Prompt)']) + await comfyPage.command.executeCommand( + 'Comfy.Canvas.ToggleSelectedNodes.Pin' + ) await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot('nodes-pinned.png') - await comfyPage.executeCommand('Comfy.Canvas.ToggleSelectedNodes.Pin') + await comfyPage.command.executeCommand( + 'Comfy.Canvas.ToggleSelectedNodes.Pin' + ) await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot('nodes-unpinned.png') }) @@ -441,7 +472,7 @@ test.describe('Node Interaction', () => { 'Can bypass/unbypass nodes with keyboard shortcut', { tag: '@screenshot' }, async ({ comfyPage }) => { - await comfyPage.select2Nodes() + await comfyPage.nodeOps.selectNodes(['CLIP Text Encode (Prompt)']) await comfyPage.canvas.press('Control+b') await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot('nodes-bypassed.png') @@ -454,7 +485,7 @@ test.describe('Node Interaction', () => { test.describe('Group Interaction', { tag: '@screenshot' }, () => { test('Can double click group title to edit', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('groups/single_group') + await comfyPage.workflow.loadWorkflow('groups/single_group') await comfyPage.canvas.dblclick({ position: { x: 50, @@ -470,16 +501,16 @@ test.describe('Group Interaction', { tag: '@screenshot' }, () => { test.describe('Canvas Interaction', { tag: '@screenshot' }, () => { test('Can zoom in/out', async ({ comfyPage }) => { - await comfyPage.zoom(-100) + await comfyPage.canvasOps.zoom(-100) await expect(comfyPage.canvas).toHaveScreenshot('zoomed-in.png') - await comfyPage.zoom(200) + await comfyPage.canvasOps.zoom(200) await expect(comfyPage.canvas).toHaveScreenshot('zoomed-out.png') }) test('Can zoom very far out', async ({ comfyPage }) => { - await comfyPage.zoom(100, 12) + await comfyPage.canvasOps.zoom(100, 12) await expect(comfyPage.canvas).toHaveScreenshot('zoomed-very-far-out.png') - await comfyPage.zoom(-100, 12) + await comfyPage.canvasOps.zoom(-100, 12) await expect(comfyPage.canvas).toHaveScreenshot('zoomed-back-in.png') }) @@ -488,11 +519,11 @@ test.describe('Canvas Interaction', { tag: '@screenshot' }, () => { }) => { await comfyPage.page.keyboard.down('Control') await comfyPage.page.keyboard.down('Shift') - await comfyPage.dragAndDrop({ x: 10, y: 100 }, { x: 10, y: 40 }) + await comfyPage.canvasOps.dragAndDrop({ x: 10, y: 100 }, { x: 10, y: 40 }) await expect(comfyPage.canvas).toHaveScreenshot('zoomed-in-ctrl-shift.png') - await comfyPage.dragAndDrop({ x: 10, y: 40 }, { x: 10, y: 160 }) + await comfyPage.canvasOps.dragAndDrop({ x: 10, y: 40 }, { x: 10, y: 160 }) await expect(comfyPage.canvas).toHaveScreenshot('zoomed-out-ctrl-shift.png') - await comfyPage.dragAndDrop({ x: 10, y: 280 }, { x: 10, y: 220 }) + await comfyPage.canvasOps.dragAndDrop({ x: 10, y: 280 }, { x: 10, y: 220 }) await expect(comfyPage.canvas).toHaveScreenshot( 'zoomed-default-ctrl-shift.png' ) @@ -503,35 +534,35 @@ test.describe('Canvas Interaction', { tag: '@screenshot' }, () => { test('Can zoom in/out after decreasing canvas zoom speed setting', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', 1.05) - await comfyPage.zoom(-100, 4) + await comfyPage.settings.setSetting('Comfy.Graph.ZoomSpeed', 1.05) + await comfyPage.canvasOps.zoom(-100, 4) await expect(comfyPage.canvas).toHaveScreenshot( 'zoomed-in-low-zoom-speed.png' ) - await comfyPage.zoom(100, 8) + await comfyPage.canvasOps.zoom(100, 8) await expect(comfyPage.canvas).toHaveScreenshot( 'zoomed-out-low-zoom-speed.png' ) - await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', 1.1) + await comfyPage.settings.setSetting('Comfy.Graph.ZoomSpeed', 1.1) }) test('Can zoom in/out after increasing canvas zoom speed', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', 1.5) - await comfyPage.zoom(-100, 4) + await comfyPage.settings.setSetting('Comfy.Graph.ZoomSpeed', 1.5) + await comfyPage.canvasOps.zoom(-100, 4) await expect(comfyPage.canvas).toHaveScreenshot( 'zoomed-in-high-zoom-speed.png' ) - await comfyPage.zoom(100, 8) + await comfyPage.canvasOps.zoom(100, 8) await expect(comfyPage.canvas).toHaveScreenshot( 'zoomed-out-high-zoom-speed.png' ) - await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', 1.1) + await comfyPage.settings.setSetting('Comfy.Graph.ZoomSpeed', 1.1) }) test('Can pan', async ({ comfyPage }) => { - await comfyPage.pan({ x: 200, y: 200 }) + await comfyPage.canvasOps.pan({ x: 200, y: 200 }) await expect(comfyPage.canvas).toHaveScreenshot('panned.png') }) @@ -602,9 +633,9 @@ test.describe('Canvas Interaction', { tag: '@screenshot' }, () => { }) test('Can pan when dragging a link', async ({ comfyPage, comfyMouse }) => { - const posSlot1 = comfyPage.clipTextEncodeNode1InputSlot + const posSlot1 = DefaultGraphPositions.clipTextEncodeNode1InputSlot await comfyMouse.move(posSlot1) - const posEmpty = comfyPage.emptySpace + const posEmpty = DefaultGraphPositions.emptySpace await comfyMouse.drag(posEmpty) await expect(comfyPage.canvas).toHaveScreenshot('dragging-link1.png') @@ -623,23 +654,23 @@ test.describe('Canvas Interaction', { tag: '@screenshot' }, () => { test('Can pan very far and back', async ({ comfyPage }) => { // intentionally slice the edge of where the clip text encode dom widgets are - await comfyPage.pan({ x: -800, y: -300 }, { x: 1000, y: 10 }) + await comfyPage.canvasOps.pan({ x: -800, y: -300 }, { x: 1000, y: 10 }) await expect(comfyPage.canvas).toHaveScreenshot('panned-step-one.png') - await comfyPage.pan({ x: -200, y: 0 }, { x: 1000, y: 10 }) + await comfyPage.canvasOps.pan({ x: -200, y: 0 }, { x: 1000, y: 10 }) await expect(comfyPage.canvas).toHaveScreenshot('panned-step-two.png') - await comfyPage.pan({ x: -2200, y: -2200 }, { x: 1000, y: 10 }) + await comfyPage.canvasOps.pan({ x: -2200, y: -2200 }, { x: 1000, y: 10 }) await expect(comfyPage.canvas).toHaveScreenshot('panned-far-away.png') - await comfyPage.pan({ x: 2200, y: 2200 }, { x: 1000, y: 10 }) + await comfyPage.canvasOps.pan({ x: 2200, y: 2200 }, { x: 1000, y: 10 }) await expect(comfyPage.canvas).toHaveScreenshot('panned-back-from-far.png') - await comfyPage.pan({ x: 200, y: 0 }, { x: 1000, y: 10 }) + await comfyPage.canvasOps.pan({ x: 200, y: 0 }, { x: 1000, y: 10 }) await expect(comfyPage.canvas).toHaveScreenshot('panned-back-to-two.png') - await comfyPage.pan({ x: 800, y: 300 }, { x: 1000, y: 10 }) + await comfyPage.canvasOps.pan({ x: 800, y: 300 }, { x: 1000, y: 10 }) await expect(comfyPage.canvas).toHaveScreenshot('panned-back-to-one.png') }) test('@mobile Can pan with touch', async ({ comfyPage }) => { await comfyPage.closeMenu() - await comfyPage.panWithTouch({ x: 200, y: 200 }) + await comfyPage.canvasOps.panWithTouch({ x: 200, y: 200 }) await expect(comfyPage.canvas).toHaveScreenshot('panned-touch.png') }) }) @@ -652,41 +683,41 @@ test.describe('Widget Interaction', () => { await expect(textBox).toHaveValue('') await textBox.fill('Hello World') await expect(textBox).toHaveValue('Hello World') - await comfyPage.ctrlZ(null) + await comfyPage.keyboard.undo(null) await expect(textBox).toHaveValue('') }) test('Undo attention edit', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.EditAttention.Delta', 0.05) + await comfyPage.settings.setSetting('Comfy.EditAttention.Delta', 0.05) const textBox = comfyPage.widgetTextBox await textBox.click() await textBox.fill('1girl') await expect(textBox).toHaveValue('1girl') await textBox.selectText() - await comfyPage.ctrlArrowUp(null) + await comfyPage.keyboard.moveUp(null) await expect(textBox).toHaveValue('(1girl:1.05)') - await comfyPage.ctrlZ(null) + await comfyPage.keyboard.undo(null) await expect(textBox).toHaveValue('1girl') }) }) test.describe('Load workflow', { tag: '@screenshot' }, () => { test('Can load workflow with string node id', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('nodes/string_node_id') + await comfyPage.workflow.loadWorkflow('nodes/string_node_id') await expect(comfyPage.canvas).toHaveScreenshot('string_node_id.png') }) test('Can load workflow with ("STRING",) input node', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('inputs/string_input') + await comfyPage.workflow.loadWorkflow('inputs/string_input') await expect(comfyPage.canvas).toHaveScreenshot('string_input.png') }) test('Restore workflow on reload (switch workflow)', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('nodes/single_ksampler') + await comfyPage.workflow.loadWorkflow('nodes/single_ksampler') await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler.png') await comfyPage.setup({ clearStorage: false }) await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler.png') @@ -695,10 +726,10 @@ test.describe('Load workflow', { tag: '@screenshot' }, () => { test('Restore workflow on reload (modify workflow)', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('nodes/single_ksampler') - const node = (await comfyPage.getFirstNodeRef())! + await comfyPage.workflow.loadWorkflow('nodes/single_ksampler') + const node = (await comfyPage.nodeOps.getFirstNodeRef())! await node.click('collapse') - await comfyPage.clickEmptySpace() + await comfyPage.canvasOps.clickEmptySpace() await expect(comfyPage.canvas).toHaveScreenshot( 'single_ksampler_modified.png' ) @@ -716,7 +747,7 @@ test.describe('Load workflow', { tag: '@screenshot' }, () => { `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}${extension}` test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') workflowA = generateUniqueFilename() await comfyPage.menu.topbar.saveWorkflow(workflowA) @@ -734,7 +765,7 @@ test.describe('Load workflow', { tag: '@screenshot' }, () => { test('Restores topbar workflow tabs after reload', async ({ comfyPage }) => { - await comfyPage.setSetting( + await comfyPage.settings.setSetting( 'Comfy.Workflow.WorkflowTabsPosition', 'Topbar' ) @@ -747,7 +778,7 @@ test.describe('Load workflow', { tag: '@screenshot' }, () => { }) test('Restores sidebar workflows after reload', async ({ comfyPage }) => { - await comfyPage.setSetting( + await comfyPage.settings.setSetting( 'Comfy.Workflow.WorkflowTabsPosition', 'Sidebar' ) @@ -770,34 +801,40 @@ test.describe('Load workflow', { tag: '@screenshot' }, () => { }) test('Auto fit view after loading workflow', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.EnableWorkflowViewRestore', false) - await comfyPage.loadWorkflow('nodes/single_ksampler') + await comfyPage.settings.setSetting( + 'Comfy.EnableWorkflowViewRestore', + false + ) + await comfyPage.workflow.loadWorkflow('nodes/single_ksampler') await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler_fit.png') }) }) test.describe('Load duplicate workflow', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') }) test('A workflow can be loaded multiple times in a row', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('nodes/single_ksampler') + await comfyPage.workflow.loadWorkflow('nodes/single_ksampler') await comfyPage.menu.workflowsTab.open() - await comfyPage.executeCommand('Comfy.NewBlankWorkflow') - await comfyPage.loadWorkflow('nodes/single_ksampler') - expect(await comfyPage.getGraphNodesCount()).toBe(1) + await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow') + await comfyPage.workflow.loadWorkflow('nodes/single_ksampler') + expect(await comfyPage.nodeOps.getGraphNodesCount()).toBe(1) }) }) test.describe('Viewport settings', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') - await comfyPage.setSetting('Comfy.Workflow.WorkflowTabsPosition', 'Topbar') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting( + 'Comfy.Workflow.WorkflowTabsPosition', + 'Topbar' + ) - await comfyPage.setupWorkflowsDirectory({}) + await comfyPage.workflow.setupWorkflowsDirectory({}) }) test('Keeps viewport settings when changing tabs', async ({ @@ -807,7 +844,7 @@ test.describe('Viewport settings', () => { const changeTab = async (tab: Locator) => { await tab.click() await comfyPage.nextFrame() - await comfyMouse.move(comfyPage.emptySpace) + await comfyMouse.move(DefaultGraphPositions.emptySpace) // If tooltip is visible, wait for it to hide await expect( @@ -816,11 +853,13 @@ test.describe('Viewport settings', () => { } // Screenshot the canvas element - await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true) + await comfyPage.settings.setSetting('Comfy.Graph.CanvasMenu', true) - const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button') + const toggleButton = comfyPage.page.getByTestId( + TestIds.canvas.toggleMinimapButton + ) await toggleButton.click() - await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false) + await comfyPage.settings.setSetting('Comfy.Graph.CanvasMenu', false) await comfyPage.menu.topbar.saveWorkflow('Workflow A') await comfyPage.nextFrame() @@ -837,7 +876,7 @@ test.describe('Viewport settings', () => { const tabB = comfyPage.menu.topbar.getWorkflowTab('Workflow B') await changeTab(tabB) - await comfyMouse.move(comfyPage.emptySpace) + await comfyMouse.move(DefaultGraphPositions.emptySpace) for (let i = 0; i < 4; i++) { await comfyMouse.wheel(0, 60) } @@ -865,13 +904,19 @@ test.describe('Viewport settings', () => { test.describe('Canvas Navigation', { tag: '@screenshot' }, () => { test.describe('Legacy Mode', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Canvas.NavigationMode', 'legacy') + await comfyPage.settings.setSetting( + 'Comfy.Canvas.NavigationMode', + 'legacy' + ) }) test('Left-click drag in empty area should pan canvas', async ({ comfyPage }) => { - await comfyPage.dragAndDrop({ x: 50, y: 50 }, { x: 150, y: 150 }) + await comfyPage.canvasOps.dragAndDrop( + { x: 50, y: 50 }, + { x: 150, y: 150 } + ) await expect(comfyPage.canvas).toHaveScreenshot( 'legacy-left-drag-pan.png' ) @@ -904,8 +949,11 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => { }) test('Left-click on node should not pan canvas', async ({ comfyPage }) => { - await comfyPage.clickTextEncodeNode1() - const selectedCount = await comfyPage.getSelectedGraphNodesCount() + await comfyPage.canvas.click({ + position: DefaultGraphPositions.textEncodeNode1 + }) + await comfyPage.nextFrame() + const selectedCount = await comfyPage.nodeOps.getSelectedGraphNodesCount() expect(selectedCount).toBe(1) await expect(comfyPage.canvas).toHaveScreenshot( 'legacy-click-node-select.png' @@ -915,18 +963,22 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => { test.describe('Standard Mode', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Canvas.NavigationMode', 'standard') + await comfyPage.settings.setSetting( + 'Comfy.Canvas.NavigationMode', + 'standard' + ) }) test('Left-click drag in empty area should select nodes', async ({ comfyPage }) => { - const clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode') + const clipNodes = + await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode') const clipNode1Pos = await clipNodes[0].getPosition() const clipNode2Pos = await clipNodes[1].getPosition() const offset = 64 - await comfyPage.dragAndDrop( + await comfyPage.canvasOps.dragAndDrop( { x: Math.min(clipNode1Pos.x, clipNode2Pos.x) - offset, y: Math.min(clipNode1Pos.y, clipNode2Pos.y) - offset @@ -937,7 +989,7 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => { } ) - const selectedCount = await comfyPage.getSelectedGraphNodesCount() + const selectedCount = await comfyPage.nodeOps.getSelectedGraphNodesCount() expect(selectedCount).toBe(clipNodes.length) await expect(comfyPage.canvas).toHaveScreenshot( 'standard-left-drag-select.png' @@ -977,8 +1029,11 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => { test('Left-click on node should select node (not start selection box)', async ({ comfyPage }) => { - await comfyPage.clickTextEncodeNode1() - const selectedCount = await comfyPage.getSelectedGraphNodesCount() + await comfyPage.canvas.click({ + position: DefaultGraphPositions.textEncodeNode1 + }) + await comfyPage.nextFrame() + const selectedCount = await comfyPage.nodeOps.getSelectedGraphNodesCount() expect(selectedCount).toBe(1) await expect(comfyPage.canvas).toHaveScreenshot( 'standard-click-node-select.png' @@ -991,7 +1046,10 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => { await comfyPage.nextFrame() await comfyPage.page.keyboard.down('Space') - await comfyPage.dragAndDrop({ x: 50, y: 50 }, { x: 150, y: 150 }) + await comfyPage.canvasOps.dragAndDrop( + { x: 50, y: 50 }, + { x: 150, y: 150 } + ) await comfyPage.page.keyboard.up('Space') await expect(comfyPage.canvas).toHaveScreenshot( 'standard-space-drag-pan.png' @@ -1001,11 +1059,12 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => { test('Space key overrides default left-click behavior', async ({ comfyPage }) => { - const clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode') + const clipNodes = + await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode') const clipNode1Pos = await clipNodes[0].getPosition() const offset = 64 - await comfyPage.dragAndDrop( + await comfyPage.canvasOps.dragAndDrop( { x: clipNode1Pos.x - offset, y: clipNode1Pos.y - offset @@ -1017,16 +1076,16 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => { ) const selectedCountAfterDrag = - await comfyPage.getSelectedGraphNodesCount() + await comfyPage.nodeOps.getSelectedGraphNodesCount() expect(selectedCountAfterDrag).toBeGreaterThan(0) - await comfyPage.clickEmptySpace() + await comfyPage.canvasOps.clickEmptySpace() const selectedCountAfterClear = - await comfyPage.getSelectedGraphNodesCount() + await comfyPage.nodeOps.getSelectedGraphNodesCount() expect(selectedCountAfterClear).toBe(0) await comfyPage.page.keyboard.down('Space') - await comfyPage.dragAndDrop( + await comfyPage.canvasOps.dragAndDrop( { x: clipNode1Pos.x - offset, y: clipNode1Pos.y - offset @@ -1039,7 +1098,7 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => { await comfyPage.page.keyboard.up('Space') const selectedCountAfterSpaceDrag = - await comfyPage.getSelectedGraphNodesCount() + await comfyPage.nodeOps.getSelectedGraphNodesCount() expect(selectedCountAfterSpaceDrag).toBe(0) }) }) @@ -1047,7 +1106,10 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => { test('Shift + mouse wheel should pan canvas horizontally', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Canvas.MouseWheelScroll', 'panning') + await comfyPage.settings.setSetting( + 'Comfy.Canvas.MouseWheelScroll', + 'panning' + ) await comfyPage.page.click('canvas') await comfyPage.nextFrame() @@ -1085,11 +1147,17 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => { test('Multiple modifier keys work correctly in legacy mode', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Canvas.NavigationMode', 'legacy') + await comfyPage.settings.setSetting( + 'Comfy.Canvas.NavigationMode', + 'legacy' + ) await comfyPage.page.keyboard.down('Alt') await comfyPage.page.keyboard.down('Shift') - await comfyPage.dragAndDrop({ x: 50, y: 50 }, { x: 150, y: 150 }) + await comfyPage.canvasOps.dragAndDrop( + { x: 50, y: 50 }, + { x: 150, y: 150 } + ) await comfyPage.page.keyboard.up('Shift') await comfyPage.page.keyboard.up('Alt') @@ -1109,7 +1177,10 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => { }) } - await comfyPage.setSetting('Comfy.Canvas.NavigationMode', 'legacy') + await comfyPage.settings.setSetting( + 'Comfy.Canvas.NavigationMode', + 'legacy' + ) await comfyPage.page.mouse.move(50, 50) await comfyPage.page.mouse.down() expect(await getCursorStyle()).toBe('grabbing') diff --git a/browser_tests/tests/keybindings.spec.ts b/browser_tests/tests/keybindings.spec.ts index feb3b0f5d..4810a3fb1 100644 --- a/browser_tests/tests/keybindings.spec.ts +++ b/browser_tests/tests/keybindings.spec.ts @@ -3,22 +3,22 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Keybindings', { tag: '@keyboard' }, () => { test('Should not trigger non-modifier keybinding when typing in input fields', async ({ comfyPage }) => { - await comfyPage.registerKeybinding({ key: 'k' }, () => { - window['TestCommand'] = true + await comfyPage.command.registerKeybinding({ key: 'k' }, () => { + window.TestCommand = true }) const textBox = comfyPage.widgetTextBox await textBox.click() await textBox.fill('k') await expect(textBox).toHaveValue('k') - expect(await comfyPage.page.evaluate(() => window['TestCommand'])).toBe( + expect(await comfyPage.page.evaluate(() => window.TestCommand)).toBe( undefined ) }) @@ -26,8 +26,8 @@ test.describe('Keybindings', { tag: '@keyboard' }, () => { test('Should not trigger modifier keybinding when typing in input fields', async ({ comfyPage }) => { - await comfyPage.registerKeybinding({ key: 'k', ctrl: true }, () => { - window['TestCommand'] = true + await comfyPage.command.registerKeybinding({ key: 'k', ctrl: true }, () => { + window.TestCommand = true }) const textBox = comfyPage.widgetTextBox @@ -35,23 +35,21 @@ test.describe('Keybindings', { tag: '@keyboard' }, () => { await textBox.fill('q') await textBox.press('Control+k') await expect(textBox).toHaveValue('q') - expect(await comfyPage.page.evaluate(() => window['TestCommand'])).toBe( - true - ) + expect(await comfyPage.page.evaluate(() => window.TestCommand)).toBe(true) }) test('Should not trigger keybinding reserved by text input when typing in input fields', async ({ comfyPage }) => { - await comfyPage.registerKeybinding({ key: 'Ctrl+v' }, () => { - window['TestCommand'] = true + await comfyPage.command.registerKeybinding({ key: 'Ctrl+v' }, () => { + window.TestCommand = true }) const textBox = comfyPage.widgetTextBox await textBox.click() await textBox.press('Control+v') await expect(textBox).toBeFocused() - expect(await comfyPage.page.evaluate(() => window['TestCommand'])).toBe( + expect(await comfyPage.page.evaluate(() => window.TestCommand)).toBe( undefined ) }) diff --git a/browser_tests/tests/litegraphEvent.spec.ts b/browser_tests/tests/litegraphEvent.spec.ts index 12d6ec9c4..a412200b5 100644 --- a/browser_tests/tests/litegraphEvent.spec.ts +++ b/browser_tests/tests/litegraphEvent.spec.ts @@ -3,7 +3,7 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) function listenForEvent(): Promise { @@ -17,7 +17,7 @@ function listenForEvent(): Promise { test.describe('Canvas Event', { tag: '@canvas' }, () => { test('Emit litegraph:canvas empty-release', async ({ comfyPage }) => { const eventPromise = comfyPage.page.evaluate(listenForEvent) - const disconnectPromise = comfyPage.disconnectEdge() + const disconnectPromise = comfyPage.canvasOps.disconnectEdge() const event = await eventPromise await disconnectPromise @@ -29,7 +29,7 @@ test.describe('Canvas Event', { tag: '@canvas' }, () => { test('Emit litegraph:canvas empty-double-click', async ({ comfyPage }) => { const eventPromise = comfyPage.page.evaluate(listenForEvent) - const doubleClickPromise = comfyPage.doubleClickCanvas() + const doubleClickPromise = comfyPage.canvasOps.doubleClick() const event = await eventPromise await doubleClickPromise diff --git a/browser_tests/tests/loadWorkflowInMedia.spec.ts b/browser_tests/tests/loadWorkflowInMedia.spec.ts index 754f373a7..d8940f2cd 100644 --- a/browser_tests/tests/loadWorkflowInMedia.spec.ts +++ b/browser_tests/tests/loadWorkflowInMedia.spec.ts @@ -3,7 +3,7 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe( @@ -16,6 +16,7 @@ test.describe( 'no_workflow.webp', 'large_workflow.webp', 'workflow_prompt_parameters.png', + 'workflow_itxt.png', 'workflow.webm', // Skipped due to 3d widget unstable visual result. // 3d widget shows grid after fully loaded. @@ -32,7 +33,7 @@ test.describe( test(`Load workflow in ${fileName} (drop from filesystem)`, async ({ comfyPage }) => { - await comfyPage.dragAndDropFile(`workflowInMedia/${fileName}`) + await comfyPage.dragDrop.dragAndDropFile(`workflowInMedia/${fileName}`) await expect(comfyPage.canvas).toHaveScreenshot(`${fileName}.png`) }) }) @@ -44,7 +45,7 @@ test.describe( test(`Load workflow from URL ${url} (drop from different browser tabs)`, async ({ comfyPage }) => { - await comfyPage.dragAndDropURL(url) + await comfyPage.dragDrop.dragAndDropURL(url) const readableName = url.split('/').pop() await expect(comfyPage.canvas).toHaveScreenshot( `dropped_workflow_url_${readableName}.png` diff --git a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-itxt-png-chromium-linux.png b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-itxt-png-chromium-linux.png new file mode 100644 index 000000000..2d007b4e2 Binary files /dev/null and b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-itxt-png-chromium-linux.png differ diff --git a/browser_tests/tests/lodThreshold.spec.ts b/browser_tests/tests/lodThreshold.spec.ts index e1098cd28..e1bf11dae 100644 --- a/browser_tests/tests/lodThreshold.spec.ts +++ b/browser_tests/tests/lodThreshold.spec.ts @@ -3,7 +3,7 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => { @@ -11,11 +11,11 @@ test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => { comfyPage }) => { // Load a workflow with some nodes to render - await comfyPage.loadWorkflow('default') + await comfyPage.workflow.loadWorkflow('default') // Get initial LOD state and settings const initialState = await comfyPage.page.evaluate(() => { - const canvas = window['app'].canvas + const canvas = window.app!.canvas return { lowQuality: canvas.low_quality, scale: canvas.ds.scale, @@ -32,11 +32,11 @@ test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => { // Can't access private _lowQualityZoomThreshold directly // Zoom out just above threshold (should still be high quality) - await comfyPage.zoom(120, 5) // Zoom out 5 steps + await comfyPage.canvasOps.zoom(120, 5) // Zoom out 5 steps await comfyPage.nextFrame() const aboveThresholdState = await comfyPage.page.evaluate(() => { - const canvas = window['app'].canvas + const canvas = window.app!.canvas return { lowQuality: canvas.low_quality, scale: canvas.ds.scale @@ -49,12 +49,12 @@ test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => { } // Zoom out more to trigger LOD (below threshold) - await comfyPage.zoom(120, 5) // Zoom out 5 more steps + await comfyPage.canvasOps.zoom(120, 5) // Zoom out 5 more steps await comfyPage.nextFrame() // Check that LOD is now active const zoomedOutState = await comfyPage.page.evaluate(() => { - const canvas = window['app'].canvas + const canvas = window.app!.canvas return { lowQuality: canvas.low_quality, scale: canvas.ds.scale @@ -65,12 +65,12 @@ test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => { expect(zoomedOutState.lowQuality).toBe(true) // Zoom back in to disable LOD (above threshold) - await comfyPage.zoom(-120, 15) // Zoom in 15 steps + await comfyPage.canvasOps.zoom(-120, 15) // Zoom in 15 steps await comfyPage.nextFrame() // Check that LOD is now inactive const zoomedInState = await comfyPage.page.evaluate(() => { - const canvas = window['app'].canvas + const canvas = window.app!.canvas return { lowQuality: canvas.low_quality, scale: canvas.ds.scale @@ -84,14 +84,17 @@ test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => { test('Should update threshold when font size setting changes', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('default') + await comfyPage.workflow.loadWorkflow('default') // Change the font size setting to 14px (more aggressive LOD) - await comfyPage.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 14) + await comfyPage.settings.setSetting( + 'LiteGraph.Canvas.MinFontSizeForLOD', + 14 + ) // Check that font size updated const newState = await comfyPage.page.evaluate(() => { - const canvas = window['app'].canvas + const canvas = window.app!.canvas return { minFontSize: canvas.min_font_size_for_lod } @@ -102,16 +105,16 @@ test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => { // At default zoom, LOD should still be inactive (scale is exactly 1.0, not less than) const lodState = await comfyPage.page.evaluate(() => { - return window['app'].canvas.low_quality + return window.app!.canvas.low_quality }) expect(lodState).toBe(false) // Zoom out slightly to trigger LOD - await comfyPage.zoom(120, 1) // Zoom out 1 step + await comfyPage.canvasOps.zoom(120, 1) // Zoom out 1 step await comfyPage.nextFrame() const afterZoom = await comfyPage.page.evaluate(() => { - const canvas = window['app'].canvas + const canvas = window.app!.canvas return { lowQuality: canvas.low_quality, scale: canvas.ds.scale @@ -125,18 +128,18 @@ test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => { test('Should disable LOD when font size is set to 0', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('default') + await comfyPage.workflow.loadWorkflow('default') // Disable LOD by setting font size to 0 - await comfyPage.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 0) + await comfyPage.settings.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 0) // Zoom out significantly - await comfyPage.zoom(120, 20) // Zoom out 20 steps + await comfyPage.canvasOps.zoom(120, 20) // Zoom out 20 steps await comfyPage.nextFrame() // LOD should remain disabled even at very low zoom const state = await comfyPage.page.evaluate(() => { - const canvas = window['app'].canvas + const canvas = window.app!.canvas return { lowQuality: canvas.low_quality, scale: canvas.ds.scale, @@ -154,15 +157,15 @@ test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => { { tag: '@screenshot' }, async ({ comfyPage }) => { // Load a workflow with text-heavy nodes for clear visual difference - await comfyPage.loadWorkflow('default') + await comfyPage.workflow.loadWorkflow('default') // Set zoom level clearly below the threshold to ensure LOD activates const targetZoom = 0.4 // Well below default threshold of ~0.571 // Zoom to target level await comfyPage.page.evaluate((zoom) => { - window['app'].canvas.ds.scale = zoom - window['app'].canvas.setDirty(true, true) + window.app!.canvas.ds.scale = zoom + window.app!.canvas.setDirty(true, true) }, targetZoom) await comfyPage.nextFrame() @@ -172,7 +175,7 @@ test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => { ) const lowQualityState = await comfyPage.page.evaluate(() => { - const canvas = window['app'].canvas + const canvas = window.app!.canvas return { lowQuality: canvas.low_quality, scale: canvas.ds.scale @@ -181,7 +184,10 @@ test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => { expect(lowQualityState.lowQuality).toBe(true) // Disable LOD to see high quality at same zoom - await comfyPage.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 0) + await comfyPage.settings.setSetting( + 'LiteGraph.Canvas.MinFontSizeForLOD', + 0 + ) await comfyPage.nextFrame() // Take snapshot with LOD disabled (full quality at same zoom) @@ -190,7 +196,7 @@ test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => { ) const highQualityState = await comfyPage.page.evaluate(() => { - const canvas = window['app'].canvas + const canvas = window.app!.canvas return { lowQuality: canvas.low_quality, scale: canvas.ds.scale diff --git a/browser_tests/tests/menu.spec.ts b/browser_tests/tests/menu.spec.ts index 06ab9a9a8..50005afc5 100644 --- a/browser_tests/tests/menu.spec.ts +++ b/browser_tests/tests/menu.spec.ts @@ -4,14 +4,14 @@ import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.describe('Menu', { tag: '@ui' }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') }) test('Can register sidebar tab', async ({ comfyPage }) => { const initialChildrenCount = await comfyPage.menu.buttons.count() await comfyPage.page.evaluate(async () => { - window['app'].extensionManager.registerSidebarTab({ + window.app!.extensionManager.registerSidebarTab({ id: 'search', icon: 'pi pi-search', title: 'search', @@ -30,11 +30,11 @@ test.describe('Menu', { tag: '@ui' }, () => { test.describe('Workflows topbar tabs', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting( + await comfyPage.settings.setSetting( 'Comfy.Workflow.WorkflowTabsPosition', 'Topbar' ) - await comfyPage.setupWorkflowsDirectory({}) + await comfyPage.workflow.setupWorkflowsDirectory({}) }) test('Can show opened workflows', async ({ comfyPage }) => { @@ -101,13 +101,12 @@ test.describe('Menu', { tag: '@ui' }, () => { const checkmark = bottomPanelMenuItem.locator('.pi-check') // Check initial state of bottom panel (it's initially hidden) - const bottomPanel = comfyPage.page.locator('.bottom-panel') - await expect(bottomPanel).not.toBeVisible() + const { bottomPanel } = comfyPage + await expect(bottomPanel.root).not.toBeVisible() // Checkmark should be invisible initially (panel is hidden) await expect(checkmark).toHaveClass(/invisible/) - // Click Bottom Panel to toggle it on await bottomPanelItem.click() // Verify menu is still visible after clicking @@ -115,7 +114,7 @@ test.describe('Menu', { tag: '@ui' }, () => { await expect(viewSubmenu).toBeVisible() // Verify bottom panel is now visible - await expect(bottomPanel).toBeVisible() + await expect(bottomPanel.root).toBeVisible() // Checkmark should now be visible (panel is shown) await expect(checkmark).not.toHaveClass(/invisible/) @@ -128,7 +127,7 @@ test.describe('Menu', { tag: '@ui' }, () => { await expect(viewSubmenu).toBeVisible() // Verify bottom panel is hidden again - await expect(bottomPanel).not.toBeVisible() + await expect(bottomPanel.root).not.toBeVisible() // Checkmark should be invisible again (panel is hidden) await expect(checkmark).toHaveClass(/invisible/) @@ -155,7 +154,7 @@ test.describe('Menu', { tag: '@ui' }, () => { test('Can catch error when executing command', async ({ comfyPage }) => { await comfyPage.page.evaluate(() => { - window['app'].registerExtension({ + window.app!.registerExtension({ name: 'TestExtension1', commands: [ { @@ -175,7 +174,7 @@ test.describe('Menu', { tag: '@ui' }, () => { }) }) await comfyPage.menu.topbar.triggerTopbarCommand(['ext', 'foo-command']) - expect(await comfyPage.getVisibleToastCount()).toBe(1) + await expect.poll(() => comfyPage.toast.getVisibleToastCount()).toBe(1) }) test('Can navigate Theme menu and switch between Dark and Light themes', async ({ @@ -214,7 +213,9 @@ test.describe('Menu', { tag: '@ui' }, () => { await comfyPage.attachScreenshot('theme-menu-light-active') // Verify ColorPalette setting is set to "light" - expect(await comfyPage.getSetting('Comfy.ColorPalette')).toBe('light') + expect(await comfyPage.settings.getSetting('Comfy.ColorPalette')).toBe( + 'light' + ) // Close menu to see theme change await topbar.closeTopbarMenu() @@ -238,7 +239,9 @@ test.describe('Menu', { tag: '@ui' }, () => { await comfyPage.attachScreenshot('theme-menu-dark-active') // Verify ColorPalette setting is set to "dark" - expect(await comfyPage.getSetting('Comfy.ColorPalette')).toBe('dark') + expect(await comfyPage.settings.getSetting('Comfy.ColorPalette')).toBe( + 'dark' + ) // Close menu await topbar.closeTopbarMenu() @@ -251,16 +254,20 @@ test.describe('Menu', { tag: '@ui' }, () => { test(`Can migrate deprecated menu positions (${position})`, async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', position) - expect(await comfyPage.getSetting('Comfy.UseNewMenu')).toBe('Top') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', position) + expect(await comfyPage.settings.getSetting('Comfy.UseNewMenu')).toBe( + 'Top' + ) }) test(`Can migrate deprecated menu positions on initial load (${position})`, async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', position) + await comfyPage.settings.setSetting('Comfy.UseNewMenu', position) await comfyPage.setup() - expect(await comfyPage.getSetting('Comfy.UseNewMenu')).toBe('Top') + expect(await comfyPage.settings.getSetting('Comfy.UseNewMenu')).toBe( + 'Top' + ) }) }) }) diff --git a/browser_tests/tests/minimap.spec.ts b/browser_tests/tests/minimap.spec.ts index b30adb933..175d5324d 100644 --- a/browser_tests/tests/minimap.spec.ts +++ b/browser_tests/tests/minimap.spec.ts @@ -1,16 +1,15 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +import { TestIds } from '../fixtures/selectors' test.describe('Minimap', { tag: '@canvas' }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') - await comfyPage.setSetting('Comfy.Minimap.Visible', true) - await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true) - await comfyPage.loadWorkflow('default') - await comfyPage.page.waitForFunction( - () => window['app'] && window['app'].canvas - ) + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.Minimap.Visible', true) + await comfyPage.settings.setSetting('Comfy.Graph.CanvasMenu', true) + await comfyPage.workflow.loadWorkflow('default') + await comfyPage.page.waitForFunction(() => window.app && window.app.canvas) }) test('Validate minimap is visible by default', async ({ comfyPage }) => { @@ -35,7 +34,9 @@ test.describe('Minimap', { tag: '@canvas' }, () => { }) test('Validate minimap toggle button state', async ({ comfyPage }) => { - const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button') + const toggleButton = comfyPage.page.getByTestId( + TestIds.canvas.toggleMinimapButton + ) await expect(toggleButton).toBeVisible() @@ -45,7 +46,9 @@ test.describe('Minimap', { tag: '@canvas' }, () => { test('Validate minimap can be toggled off and on', async ({ comfyPage }) => { const minimapContainer = comfyPage.page.locator('.litegraph-minimap') - const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button') + const toggleButton = comfyPage.page.getByTestId( + TestIds.canvas.toggleMinimapButton + ) await expect(minimapContainer).toBeVisible() diff --git a/browser_tests/tests/mobileBaseline.spec.ts b/browser_tests/tests/mobileBaseline.spec.ts index 9e9ddec94..1b7b7e4d5 100644 --- a/browser_tests/tests/mobileBaseline.spec.ts +++ b/browser_tests/tests/mobileBaseline.spec.ts @@ -1,22 +1,24 @@ -import { comfyPageFixture as test } from '../fixtures/ComfyPage' import { expect } from '@playwright/test' +import { comfyPageFixture as test } from '../fixtures/ComfyPage' +import { TestIds } from '../fixtures/selectors' + test.describe( 'Mobile Baseline Snapshots', { tag: ['@mobile', '@screenshot'] }, () => { test('@mobile empty canvas', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.ConfirmClear', false) - await comfyPage.executeCommand('Comfy.ClearWorkflow') + await comfyPage.settings.setSetting('Comfy.ConfirmClear', false) + await comfyPage.command.executeCommand('Comfy.ClearWorkflow') await expect(async () => { - expect(await comfyPage.getGraphNodesCount()).toBe(0) - }).toPass({ timeout: 256 }) + expect(await comfyPage.nodeOps.getGraphNodesCount()).toBe(0) + }).toPass({ timeout: 5000 }) await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot('mobile-empty-canvas.png') }) test('@mobile default workflow', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('default') + await comfyPage.workflow.loadWorkflow('default') await expect(comfyPage.canvas).toHaveScreenshot( 'mobile-default-workflow.png' ) @@ -30,7 +32,9 @@ test.describe( 'mobile-settings-dialog.png', { mask: [ - comfyPage.settingDialog.root.getByTestId('current-user-indicator') + comfyPage.settingDialog.root.getByTestId( + TestIds.user.currentUserIndicator + ) ] } ) diff --git a/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-default-workflow-mobile-chrome-linux.png b/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-default-workflow-mobile-chrome-linux.png index c7334eb16..c0276b2ac 100644 Binary files a/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-default-workflow-mobile-chrome-linux.png and b/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-default-workflow-mobile-chrome-linux.png differ diff --git a/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-settings-dialog-mobile-chrome-linux.png b/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-settings-dialog-mobile-chrome-linux.png index 6bb4ac6e3..17de9b03a 100644 Binary files a/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-settings-dialog-mobile-chrome-linux.png and b/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-settings-dialog-mobile-chrome-linux.png differ diff --git a/browser_tests/tests/nodeBadge.spec.ts b/browser_tests/tests/nodeBadge.spec.ts index f4cf493a9..873ac1005 100644 --- a/browser_tests/tests/nodeBadge.spec.ts +++ b/browser_tests/tests/nodeBadge.spec.ts @@ -5,14 +5,14 @@ import { NodeBadgeMode } from '../../src/types/nodeSource' import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Node Badge', { tag: ['@screenshot', '@smoke', '@node'] }, () => { test('Can add badge', async ({ comfyPage }) => { await comfyPage.page.evaluate(() => { - const LGraphBadge = window['LGraphBadge'] - const app = window['app'] as ComfyApp + const LGraphBadge = window.LGraphBadge! + const app = window.app as ComfyApp const graph = app.graph const nodes = graph.nodes @@ -28,8 +28,8 @@ test.describe('Node Badge', { tag: ['@screenshot', '@smoke', '@node'] }, () => { test('Can add multiple badges', async ({ comfyPage }) => { await comfyPage.page.evaluate(() => { - const LGraphBadge = window['LGraphBadge'] - const app = window['app'] as ComfyApp + const LGraphBadge = window.LGraphBadge! + const app = window.app as ComfyApp const graph = app.graph const nodes = graph.nodes @@ -48,8 +48,8 @@ test.describe('Node Badge', { tag: ['@screenshot', '@smoke', '@node'] }, () => { test('Can add badge left-side', async ({ comfyPage }) => { await comfyPage.page.evaluate(() => { - const LGraphBadge = window['LGraphBadge'] - const app = window['app'] as ComfyApp + const LGraphBadge = window.LGraphBadge! + const app = window.app as ComfyApp const graph = app.graph const nodes = graph.nodes @@ -73,11 +73,17 @@ test.describe( Object.values(NodeBadgeMode).forEach(async (mode) => { test(`Shows node badges (${mode})`, async ({ comfyPage }) => { // Execution error workflow has both custom node and core node. - await comfyPage.loadWorkflow('nodes/execution_error') - await comfyPage.setSetting('Comfy.NodeBadge.NodeSourceBadgeMode', mode) - await comfyPage.setSetting('Comfy.NodeBadge.NodeIdBadgeMode', mode) + await comfyPage.workflow.loadWorkflow('nodes/execution_error') + await comfyPage.settings.setSetting( + 'Comfy.NodeBadge.NodeSourceBadgeMode', + mode + ) + await comfyPage.settings.setSetting( + 'Comfy.NodeBadge.NodeIdBadgeMode', + mode + ) await comfyPage.nextFrame() - await comfyPage.resetView() + await comfyPage.canvasOps.resetView() await expect(comfyPage.canvas).toHaveScreenshot( `node-badge-${mode}.png` ) @@ -93,14 +99,14 @@ test.describe( test('Can show node badge with unknown color palette', async ({ comfyPage }) => { - await comfyPage.setSetting( + await comfyPage.settings.setSetting( 'Comfy.NodeBadge.NodeIdBadgeMode', NodeBadgeMode.ShowAll ) - await comfyPage.setSetting('Comfy.ColorPalette', 'unknown') + await comfyPage.settings.setSetting('Comfy.ColorPalette', 'unknown') await comfyPage.nextFrame() // Click empty space to trigger canvas re-render. - await comfyPage.clickEmptySpace() + await comfyPage.canvasOps.clickEmptySpace() await expect(comfyPage.canvas).toHaveScreenshot( 'node-badge-unknown-color-palette.png' ) @@ -109,14 +115,14 @@ test.describe( test('Can show node badge with light color palette', async ({ comfyPage }) => { - await comfyPage.setSetting( + await comfyPage.settings.setSetting( 'Comfy.NodeBadge.NodeIdBadgeMode', NodeBadgeMode.ShowAll ) - await comfyPage.setSetting('Comfy.ColorPalette', 'light') + await comfyPage.settings.setSetting('Comfy.ColorPalette', 'light') await comfyPage.nextFrame() // Click empty space to trigger canvas re-render. - await comfyPage.clickEmptySpace() + await comfyPage.canvasOps.clickEmptySpace() await expect(comfyPage.canvas).toHaveScreenshot( 'node-badge-light-color-palette.png' ) diff --git a/browser_tests/tests/nodeDisplay.spec.ts b/browser_tests/tests/nodeDisplay.spec.ts index 541ddaf0f..061419838 100644 --- a/browser_tests/tests/nodeDisplay.spec.ts +++ b/browser_tests/tests/nodeDisplay.spec.ts @@ -3,40 +3,40 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) // If an input is optional by node definition, it should be shown as // a hollow circle no matter what shape it was defined in the workflow JSON. test.describe('Optional input', { tag: ['@screenshot', '@node'] }, () => { test('No shape specified', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('inputs/optional_input_no_shape') + await comfyPage.workflow.loadWorkflow('inputs/optional_input_no_shape') await expect(comfyPage.canvas).toHaveScreenshot('optional_input.png') }) test('Wrong shape specified', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('inputs/optional_input_wrong_shape') + await comfyPage.workflow.loadWorkflow('inputs/optional_input_wrong_shape') await expect(comfyPage.canvas).toHaveScreenshot('optional_input.png') }) test('Correct shape specified', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('inputs/optional_input_correct_shape') + await comfyPage.workflow.loadWorkflow('inputs/optional_input_correct_shape') await expect(comfyPage.canvas).toHaveScreenshot('optional_input.png') }) test('Force input', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('inputs/force_input') + await comfyPage.workflow.loadWorkflow('inputs/force_input') await expect(comfyPage.canvas).toHaveScreenshot('force_input.png') }) test('Default input', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('inputs/default_input') + await comfyPage.workflow.loadWorkflow('inputs/default_input') await expect(comfyPage.canvas).toHaveScreenshot('default_input.png') }) test('Only optional inputs', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('inputs/only_optional_inputs') - expect(await comfyPage.getGraphNodesCount()).toBe(1) + await comfyPage.workflow.loadWorkflow('inputs/only_optional_inputs') + expect(await comfyPage.nodeOps.getGraphNodesCount()).toBe(1) await expect( comfyPage.page.locator('.comfy-missing-nodes') ).not.toBeVisible() @@ -47,37 +47,45 @@ test.describe('Optional input', { tag: ['@screenshot', '@node'] }, () => { ) }) test('Old workflow with converted input', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('inputs/old_workflow_converted_input') - const node = await comfyPage.getNodeRefById('1') - const inputs = await node.getProperty('inputs') + await comfyPage.workflow.loadWorkflow('inputs/old_workflow_converted_input') + const node = await comfyPage.nodeOps.getNodeRefById('1') + const inputs = (await node.getProperty('inputs')) as { + name: string + link?: number | null + }[] const vaeInput = inputs.find((w) => w.name === 'vae') const convertedInput = inputs.find((w) => w.name === 'strength') expect(vaeInput).toBeDefined() expect(convertedInput).toBeDefined() - expect(vaeInput.link).toBeNull() - expect(convertedInput.link).not.toBeNull() + expect(vaeInput!.link).toBeNull() + expect(convertedInput!.link).not.toBeNull() }) test('Renamed converted input', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('inputs/renamed_converted_widget') - const node = await comfyPage.getNodeRefById('3') - const inputs = await node.getProperty('inputs') + await comfyPage.workflow.loadWorkflow('inputs/renamed_converted_widget') + const node = await comfyPage.nodeOps.getNodeRefById('3') + const inputs = (await node.getProperty('inputs')) as { name: string }[] const renamedInput = inputs.find((w) => w.name === 'breadth') expect(renamedInput).toBeUndefined() }) test('slider', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('inputs/simple_slider') + await comfyPage.workflow.loadWorkflow('inputs/simple_slider') await expect(comfyPage.canvas).toHaveScreenshot('simple_slider.png') }) test('unknown converted widget', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Workflow.ShowMissingNodesWarning', false) - await comfyPage.loadWorkflow('missing/missing_nodes_converted_widget') + await comfyPage.settings.setSetting( + 'Comfy.Workflow.ShowMissingNodesWarning', + false + ) + await comfyPage.workflow.loadWorkflow( + 'missing/missing_nodes_converted_widget' + ) await expect(comfyPage.canvas).toHaveScreenshot( 'missing_nodes_converted_widget.png' ) }) test('dynamically added input', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('inputs/dynamically_added_input') + await comfyPage.workflow.loadWorkflow('inputs/dynamically_added_input') await expect(comfyPage.canvas).toHaveScreenshot( 'dynamically_added_input.png' ) diff --git a/browser_tests/tests/nodeHelp.spec.ts b/browser_tests/tests/nodeHelp.spec.ts index 095cb37ec..5260ade0d 100644 --- a/browser_tests/tests/nodeHelp.spec.ts +++ b/browser_tests/tests/nodeHelp.spec.ts @@ -12,7 +12,7 @@ async function selectNodeWithPan(comfyPage: ComfyPage, nodeRef: NodeReference) { const nodePos = await nodeRef.getPosition() await comfyPage.page.evaluate((pos) => { - const app = window['app']! + const app = window.app! const canvas = app.canvas canvas.ds.offset[0] = -pos.x + canvas.canvas.width / 2 canvas.ds.offset[1] = -pos.y + canvas.canvas.height / 2 + 100 @@ -26,17 +26,18 @@ async function selectNodeWithPan(comfyPage: ComfyPage, nodeRef: NodeReference) { test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => { test.beforeEach(async ({ comfyPage }) => { await comfyPage.setup() - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') }) test.describe('Selection Toolbox', () => { test('Should open help menu for selected node', async ({ comfyPage }) => { // Load a workflow with a node - await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true) - await comfyPage.loadWorkflow('default') + await comfyPage.settings.setSetting('Comfy.Canvas.SelectionToolbox', true) + await comfyPage.workflow.loadWorkflow('default') // Select a single node (KSampler) using node references - const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler') + const ksamplerNodes = + await comfyPage.nodeOps.getNodeRefsByType('KSampler') if (ksamplerNodes.length === 0) { throw new Error('No KSampler nodes found in the workflow') } @@ -87,7 +88,9 @@ test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => { await ksamplerNode.hover() // Click the help button - const helpButton = ksamplerNode.locator('button:has(.pi-question)') + const helpButton = ksamplerNode.getByRole('button', { + name: /learn more/i + }) await expect(helpButton).toBeVisible() await helpButton.click() @@ -117,7 +120,9 @@ test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => { .filter({ hasText: 'KSampler' }) .first() await ksamplerNode.hover() - const helpButton = ksamplerNode.locator('button:has(.pi-question)') + const helpButton = ksamplerNode.getByRole('button', { + name: /learn more/i + }) await helpButton.click() // Verify help page is shown @@ -141,7 +146,7 @@ test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => { test.describe('Help Content', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true) + await comfyPage.settings.setSetting('Comfy.Canvas.SelectionToolbox', true) }) test('Should display loading state while fetching help', async ({ @@ -157,8 +162,9 @@ test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => { }) // Load workflow and select a node - await comfyPage.loadWorkflow('default') - const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler') + await comfyPage.workflow.loadWorkflow('default') + const ksamplerNodes = + await comfyPage.nodeOps.getNodeRefsByType('KSampler') await selectNodeWithPan(comfyPage, ksamplerNodes[0]) // Click help button @@ -189,8 +195,9 @@ test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => { }) // Load workflow and select a node - await comfyPage.loadWorkflow('default') - const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler') + await comfyPage.workflow.loadWorkflow('default') + const ksamplerNodes = + await comfyPage.nodeOps.getNodeRefsByType('KSampler') await selectNodeWithPan(comfyPage, ksamplerNodes[0]) // Click help button @@ -226,8 +233,9 @@ test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => { }) }) - await comfyPage.loadWorkflow('default') - const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler') + await comfyPage.workflow.loadWorkflow('default') + const ksamplerNodes = + await comfyPage.nodeOps.getNodeRefsByType('KSampler') await selectNodeWithPan(comfyPage, ksamplerNodes[0]) const helpButton = comfyPage.page.locator( @@ -276,8 +284,9 @@ test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => { }) }) - await comfyPage.loadWorkflow('default') - const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler') + await comfyPage.workflow.loadWorkflow('default') + const ksamplerNodes = + await comfyPage.nodeOps.getNodeRefsByType('KSampler') await selectNodeWithPan(comfyPage, ksamplerNodes[0]) const helpButton = comfyPage.page.locator( @@ -323,7 +332,7 @@ test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => { comfyPage }) => { // First load workflow with custom node - await comfyPage.loadWorkflow('groupnodes/group_node_v1.3.3') + await comfyPage.workflow.loadWorkflow('groupnodes/group_node_v1.3.3') // Mock custom node documentation with fallback await comfyPage.page.route( @@ -347,10 +356,10 @@ This is documentation for a custom node. // Find and select a custom/group node const nodeRefs = await comfyPage.page.evaluate(() => { - return window['app']!.graph!.nodes.map((n) => n.id) + return window.app!.graph!.nodes.map((n) => n.id) }) if (nodeRefs.length > 0) { - const firstNode = await comfyPage.getNodeRefById(nodeRefs[0]) + const firstNode = await comfyPage.nodeOps.getNodeRefById(nodeRefs[0]) await selectNodeWithPan(comfyPage, firstNode) } @@ -393,8 +402,9 @@ This is documentation for a custom node. }) }) - await comfyPage.loadWorkflow('default') - const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler') + await comfyPage.workflow.loadWorkflow('default') + const ksamplerNodes = + await comfyPage.nodeOps.getNodeRefsByType('KSampler') await selectNodeWithPan(comfyPage, ksamplerNodes[0]) const helpButton = comfyPage.page.locator( @@ -460,10 +470,11 @@ This is English documentation. }) // Set locale to Japanese - await comfyPage.setSetting('Comfy.Locale', 'ja') + await comfyPage.settings.setSetting('Comfy.Locale', 'ja') - await comfyPage.loadWorkflow('default') - const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler') + await comfyPage.workflow.loadWorkflow('default') + const ksamplerNodes = + await comfyPage.nodeOps.getNodeRefsByType('KSampler') await selectNodeWithPan(comfyPage, ksamplerNodes[0]) const helpButton = comfyPage.page.locator( @@ -478,7 +489,7 @@ This is English documentation. await expect(helpPage).toContainText('これは日本語のドキュメントです') // Reset locale - await comfyPage.setSetting('Comfy.Locale', 'en') + await comfyPage.settings.setSetting('Comfy.Locale', 'en') }) test('Should handle network errors gracefully', async ({ comfyPage }) => { @@ -487,8 +498,9 @@ This is English documentation. await route.abort('failed') }) - await comfyPage.loadWorkflow('default') - const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler') + await comfyPage.workflow.loadWorkflow('default') + const ksamplerNodes = + await comfyPage.nodeOps.getNodeRefsByType('KSampler') await selectNodeWithPan(comfyPage, ksamplerNodes[0]) const helpButton = comfyPage.page.locator( @@ -530,11 +542,12 @@ This is English documentation. } ) - await comfyPage.loadWorkflow('default') + await comfyPage.workflow.loadWorkflow('default') await fitToViewInstant(comfyPage) // Select KSampler first - const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler') + const ksamplerNodes = + await comfyPage.nodeOps.getNodeRefsByType('KSampler') await selectNodeWithPan(comfyPage, ksamplerNodes[0]) const helpButton = comfyPage.page.locator( @@ -549,7 +562,7 @@ This is English documentation. await expect(helpPage).toContainText('This is KSampler documentation') // Now select Checkpoint Loader - const checkpointNodes = await comfyPage.getNodeRefsByType( + const checkpointNodes = await comfyPage.nodeOps.getNodeRefsByType( 'CheckpointLoaderSimple' ) await selectNodeWithPan(comfyPage, checkpointNodes[0]) diff --git a/browser_tests/tests/nodeSearchBox.spec.ts b/browser_tests/tests/nodeSearchBox.spec.ts index f227b2932..89bfe4a5e 100644 --- a/browser_tests/tests/nodeSearchBox.spec.ts +++ b/browser_tests/tests/nodeSearchBox.spec.ts @@ -2,32 +2,39 @@ import { comfyExpect as expect, comfyPageFixture as test } from '../fixtures/ComfyPage' +import type { ComfyPage } from '../fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Node search box', { tag: '@node' }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.LinkRelease.Action', 'search box') - await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'search box') - await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default') + await comfyPage.settings.setSetting( + 'Comfy.LinkRelease.Action', + 'search box' + ) + await comfyPage.settings.setSetting( + 'Comfy.LinkRelease.ActionShift', + 'search box' + ) + await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default') }) test(`Can trigger on empty canvas double click`, async ({ comfyPage }) => { - await comfyPage.doubleClickCanvas() + await comfyPage.canvasOps.doubleClick() await expect(comfyPage.searchBox.input).toHaveCount(1) }) test(`Can trigger on group body double click`, async ({ comfyPage }) => { - await comfyPage.loadWorkflow('groups/single_group_only') + await comfyPage.workflow.loadWorkflow('groups/single_group_only') await comfyPage.page.mouse.dblclick(50, 50, { delay: 5 }) await comfyPage.nextFrame() await expect(comfyPage.searchBox.input).toHaveCount(1) }) test('Can trigger on link release', async ({ comfyPage }) => { - await comfyPage.disconnectEdge() + await comfyPage.canvasOps.disconnectEdge() await expect(comfyPage.searchBox.input).toHaveCount(1) }) @@ -37,24 +44,24 @@ test.describe('Node search box', { tag: '@node' }, () => { // Start fresh to test new user behavior await comfyPage.setup({ clearStorage: true }) // Simulate new user with 1.24.1+ installed version - await comfyPage.setSetting('Comfy.InstalledVersion', '1.24.1') - await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default') + await comfyPage.settings.setSetting('Comfy.InstalledVersion', '1.24.1') + await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default') // Don't set LinkRelease settings explicitly to test versioned defaults - await comfyPage.disconnectEdge() + await comfyPage.canvasOps.disconnectEdge() await expect(comfyPage.searchBox.input).toHaveCount(1) await expect(comfyPage.searchBox.input).toBeVisible() }) test('Can add node', { tag: '@screenshot' }, async ({ comfyPage }) => { - await comfyPage.doubleClickCanvas() + await comfyPage.canvasOps.doubleClick() await expect(comfyPage.searchBox.input).toHaveCount(1) await comfyPage.searchBox.fillAndSelectFirstNode('KSampler') await expect(comfyPage.canvas).toHaveScreenshot('added-node.png') }) test('Can auto link node', { tag: '@screenshot' }, async ({ comfyPage }) => { - await comfyPage.disconnectEdge() + await comfyPage.canvasOps.disconnectEdge() // Select the second item as the first item is always reroute await comfyPage.searchBox.fillAndSelectFirstNode('CLIPTextEncode', { suggestionIndex: 0 @@ -66,18 +73,18 @@ test.describe('Node search box', { tag: '@node' }, () => { 'Can auto link batch moved node', { tag: '@screenshot' }, async ({ comfyPage }) => { - await comfyPage.loadWorkflow('links/batch_move_links') + await comfyPage.workflow.loadWorkflow('links/batch_move_links') + + // Get the CLIP output slot (index 1) from the first CheckpointLoaderSimple node (id: 4) + const checkpointNode = await comfyPage.nodeOps.getNodeRefById(4) + const clipOutputSlot = await checkpointNode.getOutput(1) + const outputSlotPos = await clipOutputSlot.getPosition() + + // Use a position in the empty canvas area (top-left corner) + const emptySpacePos = { x: 5, y: 5 } - const outputSlot1Pos = { - x: 304, - y: 127 - } - const emptySpacePos = { - x: 5, - y: 5 - } await comfyPage.page.keyboard.down('Shift') - await comfyPage.dragAndDrop(outputSlot1Pos, emptySpacePos) + await comfyPage.canvasOps.dragAndDrop(outputSlotPos, emptySpacePos) await comfyPage.page.keyboard.up('Shift') // Select the second item as the first item is always reroute @@ -94,7 +101,7 @@ test.describe('Node search box', { tag: '@node' }, () => { 'Link release connecting to node with no slots', { tag: '@screenshot' }, async ({ comfyPage }) => { - await comfyPage.disconnectEdge() + await comfyPage.canvasOps.disconnectEdge() await expect(comfyPage.searchBox.input).toHaveCount(1) await comfyPage.page.locator('.p-chip-remove-icon').click() await comfyPage.searchBox.fillAndSelectFirstNode('KSampler') @@ -106,7 +113,7 @@ test.describe('Node search box', { tag: '@node' }, () => { test('Has correct aria-labels on search results', async ({ comfyPage }) => { const node = 'Load Checkpoint' - await comfyPage.doubleClickCanvas() + await comfyPage.canvasOps.doubleClick() await comfyPage.searchBox.input.waitFor({ state: 'visible' }) await comfyPage.searchBox.input.fill(node) await comfyPage.searchBox.dropdown.waitFor({ state: 'visible' }) @@ -117,7 +124,7 @@ test.describe('Node search box', { tag: '@node' }, () => { test('@mobile Can trigger on empty canvas tap', async ({ comfyPage }) => { await comfyPage.closeMenu() - await comfyPage.loadWorkflow('nodes/single_ksampler') + await comfyPage.workflow.loadWorkflow('nodes/single_ksampler') const screenCenter = { x: 200, y: 400 @@ -132,7 +139,10 @@ test.describe('Node search box', { tag: '@node' }, () => { }) test.describe('Filtering', () => { - const expectFilterChips = async (comfyPage, expectedTexts: string[]) => { + const expectFilterChips = async ( + comfyPage: ComfyPage, + expectedTexts: string[] + ) => { const chips = comfyPage.searchBox.filterChips // Check that the number of chips matches the expected count @@ -149,7 +159,7 @@ test.describe('Node search box', { tag: '@node' }, () => { } test.beforeEach(async ({ comfyPage }) => { - await comfyPage.doubleClickCanvas() + await comfyPage.canvasOps.doubleClick() }) test('Can add filter', async ({ comfyPage }) => { @@ -241,7 +251,7 @@ test.describe('Node search box', { tag: '@node' }, () => { test.describe('Input focus behavior', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.doubleClickCanvas() + await comfyPage.canvasOps.doubleClick() }) test('focuses input after adding a filter', async ({ comfyPage }) => { @@ -259,16 +269,22 @@ test.describe('Node search box', { tag: '@node' }, () => { test.describe('Release context menu', { tag: '@node' }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.LinkRelease.Action', 'context menu') - await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'search box') - await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default') + await comfyPage.settings.setSetting( + 'Comfy.LinkRelease.Action', + 'context menu' + ) + await comfyPage.settings.setSetting( + 'Comfy.LinkRelease.ActionShift', + 'search box' + ) + await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default') }) test( 'Can trigger on link release', { tag: '@screenshot' }, async ({ comfyPage }) => { - await comfyPage.disconnectEdge() + await comfyPage.canvasOps.disconnectEdge() const contextMenu = comfyPage.page.locator('.litecontextmenu') // Wait for context menu with correct title (slot name | slot type) // The title shows the output slot name and type from the disconnected link @@ -287,9 +303,10 @@ test.describe('Release context menu', { tag: '@node' }, () => { 'Can search and add node from context menu', { tag: '@screenshot' }, async ({ comfyPage, comfyMouse }) => { - await comfyPage.disconnectEdge() + await comfyPage.canvasOps.disconnectEdge() await comfyMouse.move({ x: 10, y: 10 }) - await comfyPage.clickContextMenuItem('Search') + await comfyPage.contextMenu.clickMenuItem('Search') + await comfyPage.nextFrame() await comfyPage.searchBox.fillAndSelectFirstNode('CLIP Prompt') await expect(comfyPage.canvas).toHaveScreenshot( 'link-context-menu-search.png' @@ -303,11 +320,11 @@ test.describe('Release context menu', { tag: '@node' }, () => { // Start fresh to test existing user behavior await comfyPage.setup({ clearStorage: true }) // Simulate existing user with pre-1.24.1 version - await comfyPage.setSetting('Comfy.InstalledVersion', '1.23.0') - await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default') + await comfyPage.settings.setSetting('Comfy.InstalledVersion', '1.23.0') + await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default') // Don't set LinkRelease settings explicitly to test versioned defaults - await comfyPage.disconnectEdge() + await comfyPage.canvasOps.disconnectEdge() // Context menu should appear, search box should not await expect(comfyPage.searchBox.input).toHaveCount(0) const contextMenu = comfyPage.page.locator('.litecontextmenu') @@ -319,12 +336,15 @@ test.describe('Release context menu', { tag: '@node' }, () => { }) => { // Start fresh and simulate new user who should get search box by default await comfyPage.setup({ clearStorage: true }) - await comfyPage.setSetting('Comfy.InstalledVersion', '1.24.1') + await comfyPage.settings.setSetting('Comfy.InstalledVersion', '1.24.1') // But explicitly set to context menu (overriding versioned default) - await comfyPage.setSetting('Comfy.LinkRelease.Action', 'context menu') - await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default') + await comfyPage.settings.setSetting( + 'Comfy.LinkRelease.Action', + 'context menu' + ) + await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default') - await comfyPage.disconnectEdge() + await comfyPage.canvasOps.disconnectEdge() // Context menu should appear due to explicit setting, not search box await expect(comfyPage.searchBox.input).toHaveCount(0) const contextMenu = comfyPage.page.locator('.litecontextmenu') diff --git a/browser_tests/tests/noteNode.spec.ts b/browser_tests/tests/noteNode.spec.ts index 4bb551962..c50a533ea 100644 --- a/browser_tests/tests/noteNode.spec.ts +++ b/browser_tests/tests/noteNode.spec.ts @@ -3,12 +3,12 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Note Node', { tag: '@node' }, () => { test('Can load node nodes', { tag: '@screenshot' }, async ({ comfyPage }) => { - await comfyPage.loadWorkflow('nodes/note_nodes') + await comfyPage.workflow.loadWorkflow('nodes/note_nodes') await expect(comfyPage.canvas).toHaveScreenshot('note_nodes.png') }) }) diff --git a/browser_tests/tests/primitiveNode.spec.ts b/browser_tests/tests/primitiveNode.spec.ts index 2e2e11ae5..aa0e029e0 100644 --- a/browser_tests/tests/primitiveNode.spec.ts +++ b/browser_tests/tests/primitiveNode.spec.ts @@ -4,21 +4,25 @@ import { comfyPageFixture as test } from '../fixtures/ComfyPage' import type { NodeReference } from '../fixtures/utils/litegraphUtils' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Primitive Node', { tag: ['@screenshot', '@node'] }, () => { test('Can load with correct size', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('primitive/primitive_node') + await comfyPage.workflow.loadWorkflow('primitive/primitive_node') await expect(comfyPage.canvas).toHaveScreenshot('primitive_node.png') }) // When link is dropped on widget, it should automatically convert the widget // to input. test('Can connect to widget', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('primitive/primitive_node_unconnected') - const primitiveNode: NodeReference = await comfyPage.getNodeRefById(1) - const ksamplerNode: NodeReference = await comfyPage.getNodeRefById(2) + await comfyPage.workflow.loadWorkflow( + 'primitive/primitive_node_unconnected' + ) + const primitiveNode: NodeReference = + await comfyPage.nodeOps.getNodeRefById(1) + const ksamplerNode: NodeReference = + await comfyPage.nodeOps.getNodeRefById(2) // Connect the output of the primitive node to the input of first widget of the ksampler node await primitiveNode.connectWidget(0, ksamplerNode, 0) await expect(comfyPage.canvas).toHaveScreenshot( @@ -27,11 +31,13 @@ test.describe('Primitive Node', { tag: ['@screenshot', '@node'] }, () => { }) test('Can connect to dom widget', async ({ comfyPage }) => { - await comfyPage.loadWorkflow( + await comfyPage.workflow.loadWorkflow( 'primitive/primitive_node_unconnected_dom_widget' ) - const primitiveNode: NodeReference = await comfyPage.getNodeRefById(1) - const clipEncoderNode: NodeReference = await comfyPage.getNodeRefById(2) + const primitiveNode: NodeReference = + await comfyPage.nodeOps.getNodeRefById(1) + const clipEncoderNode: NodeReference = + await comfyPage.nodeOps.getNodeRefById(2) await primitiveNode.connectWidget(0, clipEncoderNode, 0) await expect(comfyPage.canvas).toHaveScreenshot( 'primitive_node_connected_dom_widget.png' @@ -39,9 +45,13 @@ test.describe('Primitive Node', { tag: ['@screenshot', '@node'] }, () => { }) test('Can connect to static primitive node', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('primitive/static_primitive_unconnected') - const primitiveNode: NodeReference = await comfyPage.getNodeRefById(1) - const ksamplerNode: NodeReference = await comfyPage.getNodeRefById(2) + await comfyPage.workflow.loadWorkflow( + 'primitive/static_primitive_unconnected' + ) + const primitiveNode: NodeReference = + await comfyPage.nodeOps.getNodeRefById(1) + const ksamplerNode: NodeReference = + await comfyPage.nodeOps.getNodeRefById(2) await primitiveNode.connectWidget(0, ksamplerNode, 0) await expect(comfyPage.canvas).toHaveScreenshot( 'static_primitive_connected.png' @@ -51,7 +61,7 @@ test.describe('Primitive Node', { tag: ['@screenshot', '@node'] }, () => { test('Report missing nodes when connect to missing node', async ({ comfyPage }) => { - await comfyPage.loadWorkflow( + await comfyPage.workflow.loadWorkflow( 'primitive/primitive_node_connect_missing_node' ) // Wait for the element with the .comfy-missing-nodes selector to be visible diff --git a/browser_tests/tests/propertiesPanel/propertiesPanel.spec.ts b/browser_tests/tests/propertiesPanel/propertiesPanel.spec.ts index ff452b54a..32ff8722d 100644 --- a/browser_tests/tests/propertiesPanel/propertiesPanel.spec.ts +++ b/browser_tests/tests/propertiesPanel/propertiesPanel.spec.ts @@ -10,7 +10,10 @@ test.describe('Properties panel', () => { await expect(propertiesPanel.panelTitle).toContainText('Workflow Overview') - await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)']) + await comfyPage.nodeOps.selectNodes([ + 'KSampler', + 'CLIP Text Encode (Prompt)' + ]) await expect(propertiesPanel.panelTitle).toContainText('3 items selected') await expect(propertiesPanel.root.getByText('KSampler')).toHaveCount(1) diff --git a/browser_tests/tests/propertiesPanel/propertiesPanelPosition.spec.ts b/browser_tests/tests/propertiesPanel/propertiesPanelPosition.spec.ts index ee2c85eda..eefbe6720 100644 --- a/browser_tests/tests/propertiesPanel/propertiesPanelPosition.spec.ts +++ b/browser_tests/tests/propertiesPanel/propertiesPanelPosition.spec.ts @@ -1,6 +1,7 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../../fixtures/ComfyPage' +import { TestIds } from '../../fixtures/selectors' test.describe('Properties panel position', () => { test.beforeEach(async ({ comfyPage }) => { @@ -12,10 +13,12 @@ test.describe('Properties panel position', () => { test('positions on the right when sidebar is on the left', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Sidebar.Location', 'left') + await comfyPage.settings.setSetting('Comfy.Sidebar.Location', 'left') await comfyPage.nextFrame() - const propertiesPanel = comfyPage.page.getByTestId('properties-panel') + const propertiesPanel = comfyPage.page.getByTestId( + TestIds.propertiesPanel.root + ) const sidebar = comfyPage.page.locator('.side-bar-panel').first() await expect(propertiesPanel).toBeVisible() @@ -36,10 +39,12 @@ test.describe('Properties panel position', () => { test('positions on the left when sidebar is on the right', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Sidebar.Location', 'right') + await comfyPage.settings.setSetting('Comfy.Sidebar.Location', 'right') await comfyPage.nextFrame() - const propertiesPanel = comfyPage.page.getByTestId('properties-panel') + const propertiesPanel = comfyPage.page.getByTestId( + TestIds.propertiesPanel.root + ) const sidebar = comfyPage.page.locator('.side-bar-panel').first() await expect(propertiesPanel).toBeVisible() @@ -60,10 +65,12 @@ test.describe('Properties panel position', () => { test('close button icon updates based on sidebar location', async ({ comfyPage }) => { - const propertiesPanel = comfyPage.page.getByTestId('properties-panel') + const propertiesPanel = comfyPage.page.getByTestId( + TestIds.propertiesPanel.root + ) // When sidebar is on the left, panel is on the right - await comfyPage.setSetting('Comfy.Sidebar.Location', 'left') + await comfyPage.settings.setSetting('Comfy.Sidebar.Location', 'left') await comfyPage.nextFrame() await expect(propertiesPanel).toBeVisible() @@ -74,7 +81,7 @@ test.describe('Properties panel position', () => { await expect(closeButtonLeft).toHaveClass(/lucide--panel-right/) // When sidebar is on the right, panel is on the left - await comfyPage.setSetting('Comfy.Sidebar.Location', 'right') + await comfyPage.settings.setSetting('Comfy.Sidebar.Location', 'right') await comfyPage.nextFrame() const closeButtonRight = propertiesPanel diff --git a/browser_tests/tests/recordAudio.spec.ts b/browser_tests/tests/recordAudio.spec.ts index d343990c1..07d9c4808 100644 --- a/browser_tests/tests/recordAudio.spec.ts +++ b/browser_tests/tests/recordAudio.spec.ts @@ -3,7 +3,7 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Record Audio Node', { tag: '@screenshot' }, () => { @@ -11,7 +11,7 @@ test.describe('Record Audio Node', { tag: '@screenshot' }, () => { comfyPage }) => { // Open the search box by double clicking on the canvas - await comfyPage.doubleClickCanvas() + await comfyPage.canvasOps.doubleClick() await expect(comfyPage.searchBox.input).toHaveCount(1) // Search for and add the RecordAudio node @@ -19,7 +19,8 @@ test.describe('Record Audio Node', { tag: '@screenshot' }, () => { await comfyPage.nextFrame() // Verify the RecordAudio node was added - const recordAudioNodes = await comfyPage.getNodeRefsByType('RecordAudio') + const recordAudioNodes = + await comfyPage.nodeOps.getNodeRefsByType('RecordAudio') expect(recordAudioNodes.length).toBe(1) // Take a screenshot of the canvas with the RecordAudio node diff --git a/browser_tests/tests/releaseNotifications.spec.ts b/browser_tests/tests/releaseNotifications.spec.ts index a45527de7..74ae32c38 100644 --- a/browser_tests/tests/releaseNotifications.spec.ts +++ b/browser_tests/tests/releaseNotifications.spec.ts @@ -1,10 +1,11 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +import { TestIds } from '../fixtures/selectors' test.describe('Release Notifications', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') }) test('should show help center with release information', async ({ @@ -50,7 +51,9 @@ test.describe('Release Notifications', () => { await expect(helpMenu).toBeVisible() // Verify "What's New?" section shows the release - const whatsNewSection = comfyPage.page.getByTestId('whats-new-section') + const whatsNewSection = comfyPage.page.getByTestId( + TestIds.dialogs.whatsNewSection + ) await expect(whatsNewSection).toBeVisible() // Should show the release version @@ -79,7 +82,9 @@ test.describe('Release Notifications', () => { await expect(helpMenu).toBeVisible() // Verify "What's New?" section shows no releases - const whatsNewSection = comfyPage.page.getByTestId('whats-new-section') + const whatsNewSection = comfyPage.page.getByTestId( + TestIds.dialogs.whatsNewSection + ) await expect(whatsNewSection).toBeVisible() // Should show "No recent releases" message @@ -125,7 +130,9 @@ test.describe('Release Notifications', () => { await expect(helpMenu).toBeVisible() // Should show no releases due to error - const whatsNewSection = comfyPage.page.getByTestId('whats-new-section') + const whatsNewSection = comfyPage.page.getByTestId( + TestIds.dialogs.whatsNewSection + ) await expect( whatsNewSection.locator('text=No recent releases') ).toBeVisible() @@ -135,7 +142,10 @@ test.describe('Release Notifications', () => { comfyPage }) => { // Disable version update notifications - await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', false) + await comfyPage.settings.setSetting( + 'Comfy.Notification.ShowVersionUpdates', + false + ) // Mock release API with test data await comfyPage.page.route('**/releases**', async (route) => { @@ -175,7 +185,9 @@ test.describe('Release Notifications', () => { await expect(helpMenu).toBeVisible() // Verify "What's New?" section is hidden - const whatsNewSection = comfyPage.page.getByTestId('whats-new-section') + const whatsNewSection = comfyPage.page.getByTestId( + TestIds.dialogs.whatsNewSection + ) await expect(whatsNewSection).not.toBeVisible() // Should not show any popups or toasts @@ -189,7 +201,10 @@ test.describe('Release Notifications', () => { comfyPage }) => { // Disable version update notifications - await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', false) + await comfyPage.settings.setSetting( + 'Comfy.Notification.ShowVersionUpdates', + false + ) // Track API calls let apiCallCount = 0 @@ -220,7 +235,10 @@ test.describe('Release Notifications', () => { comfyPage }) => { // Enable version update notifications (default behavior) - await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', true) + await comfyPage.settings.setSetting( + 'Comfy.Notification.ShowVersionUpdates', + true + ) // Mock release API with test data await comfyPage.page.route('**/releases**', async (route) => { @@ -260,7 +278,9 @@ test.describe('Release Notifications', () => { await expect(helpMenu).toBeVisible() // Verify "What's New?" section is visible - const whatsNewSection = comfyPage.page.getByTestId('whats-new-section') + const whatsNewSection = comfyPage.page.getByTestId( + TestIds.dialogs.whatsNewSection + ) await expect(whatsNewSection).toBeVisible() // Should show the release @@ -299,7 +319,10 @@ test.describe('Release Notifications', () => { }) // Start with notifications enabled - await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', true) + await comfyPage.settings.setSetting( + 'Comfy.Notification.ShowVersionUpdates', + true + ) await comfyPage.setup({ mockReleases: false }) // Open help center @@ -308,14 +331,19 @@ test.describe('Release Notifications', () => { await helpCenterButton.click() // Verify "What's New?" section is visible - const whatsNewSection = comfyPage.page.getByTestId('whats-new-section') + const whatsNewSection = comfyPage.page.getByTestId( + TestIds.dialogs.whatsNewSection + ) await expect(whatsNewSection).toBeVisible() // Close help center await comfyPage.page.click('.help-center-backdrop') // Disable notifications - await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', false) + await comfyPage.settings.setSetting( + 'Comfy.Notification.ShowVersionUpdates', + false + ) // Reopen help center await helpCenterButton.click() @@ -328,7 +356,10 @@ test.describe('Release Notifications', () => { comfyPage }) => { // Disable notifications - await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', false) + await comfyPage.settings.setSetting( + 'Comfy.Notification.ShowVersionUpdates', + false + ) // Mock empty releases await comfyPage.page.route('**/releases**', async (route) => { @@ -359,7 +390,9 @@ test.describe('Release Notifications', () => { await expect(helpMenu).toBeVisible() // Section should be hidden regardless of empty releases - const whatsNewSection = comfyPage.page.getByTestId('whats-new-section') + const whatsNewSection = comfyPage.page.getByTestId( + TestIds.dialogs.whatsNewSection + ) await expect(whatsNewSection).not.toBeVisible() }) }) diff --git a/browser_tests/tests/remoteWidgets.spec.ts b/browser_tests/tests/remoteWidgets.spec.ts index cc7cad64f..b09421bc6 100644 --- a/browser_tests/tests/remoteWidgets.spec.ts +++ b/browser_tests/tests/remoteWidgets.spec.ts @@ -26,23 +26,23 @@ test.describe('Remote COMBO Widget', { tag: '@widget' }, () => { nodeName: string ): Promise => { return await comfyPage.page.evaluate((name) => { - const node = window['app'].graph.nodes.find((node) => node.title === name) - return node.widgets[0].options.values + const node = window.app!.graph!.nodes.find((node) => node.title === name) + return node!.widgets![0].options.values as string[] | undefined }, nodeName) } const getWidgetValue = async (comfyPage: ComfyPage, nodeName: string) => { return await comfyPage.page.evaluate((name) => { - const node = window['app'].graph.nodes.find((node) => node.title === name) - return node.widgets[0].value + const node = window.app!.graph!.nodes.find((node) => node.title === name) + return node!.widgets![0].value }, nodeName) } const clickRefreshButton = (comfyPage: ComfyPage, nodeName: string) => { return comfyPage.page.evaluate((name) => { - const node = window['app'].graph.nodes.find((node) => node.title === name) - const buttonWidget = node.widgets.find((w) => w.name === 'refresh') - return buttonWidget?.callback() + const node = window.app!.graph!.nodes.find((node) => node.title === name) + const buttonWidget = node!.widgets!.find((w) => w.name === 'refresh') + return buttonWidget?.callback?.(buttonWidget.value) }, nodeName) } @@ -52,12 +52,12 @@ test.describe('Remote COMBO Widget', { tag: '@widget' }, () => { } test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') }) test.describe('Loading options', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.page.route( '**/api/models/checkpoints**', async (route, request) => { @@ -89,10 +89,10 @@ test.describe('Remote COMBO Widget', { tag: '@widget' }, () => { comfyPage }) => { const nodeName = 'Remote Widget Node' - await comfyPage.loadWorkflow('inputs/remote_widget') + await comfyPage.workflow.loadWorkflow('inputs/remote_widget') const node = await comfyPage.page.evaluate((name) => { - return window['app'].graph.nodes.find((node) => node.title === name) + return window.app!.graph!.nodes.find((node) => node.title === name) }, nodeName) expect(node).toBeDefined() @@ -176,7 +176,7 @@ test.describe('Remote COMBO Widget', { tag: '@widget' }, () => { test('refresh button is visible in selection toolbar when node is selected', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true) + await comfyPage.settings.setSetting('Comfy.Canvas.SelectionToolbox', true) const nodeName = 'Remote Widget Node' await addRemoteWidgetNode(comfyPage, nodeName) @@ -196,7 +196,7 @@ test.describe('Remote COMBO Widget', { tag: '@widget' }, () => { // Fulfill each request with a unique timestamp await comfyPage.page.route( '**/api/models/checkpoints**', - async (route, request) => { + async (route, _request) => { await route.fulfill({ body: JSON.stringify([Date.now()]), status: 200 @@ -257,13 +257,11 @@ test.describe('Remote COMBO Widget', { tag: '@widget' }, () => { await addRemoteWidgetNode(comfyPage, nodeName) await waitForWidgetUpdate(comfyPage) - // Wait for timeout and backoff, then force re-render, repeat - const requestTimeout = 512 - await comfyPage.page.waitForTimeout(requestTimeout) - await waitForWidgetUpdate(comfyPage) - await comfyPage.page.waitForTimeout(requestTimeout * 2) - await waitForWidgetUpdate(comfyPage) - await comfyPage.page.waitForTimeout(requestTimeout * 3) + // Wait for exponential backoff retries to accumulate timestamps + await expect(async () => { + await waitForWidgetUpdate(comfyPage) + expect(timestamps.length).toBeGreaterThanOrEqual(3) + }).toPass({ timeout: 10000, intervals: [500, 1000, 1500] }) // Verify exponential backoff between retries const intervals = timestamps.slice(1).map((t, i) => t - timestamps[i]) diff --git a/browser_tests/tests/rerouteNode.spec.ts b/browser_tests/tests/rerouteNode.spec.ts index 48f155419..93f92d1d5 100644 --- a/browser_tests/tests/rerouteNode.spec.ts +++ b/browser_tests/tests/rerouteNode.spec.ts @@ -5,16 +5,16 @@ import { getMiddlePoint } from '../fixtures/utils/litegraphUtils' test.describe('Reroute Node', { tag: ['@screenshot', '@node'] }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') }) test.afterEach(async ({ comfyPage }) => { - await comfyPage.setupWorkflowsDirectory({}) + await comfyPage.workflow.setupWorkflowsDirectory({}) }) test('loads from inserted workflow', async ({ comfyPage }) => { const workflowName = 'single_connected_reroute_node.json' - await comfyPage.setupWorkflowsDirectory({ + await comfyPage.workflow.setupWorkflowsDirectory({ [workflowName]: 'links/single_connected_reroute_node.json' }) await comfyPage.setup() @@ -43,12 +43,12 @@ test.describe( { tag: ['@screenshot', '@node'] }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') - await comfyPage.setSetting('LiteGraph.Reroute.SplineOffset', 80) + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('LiteGraph.Reroute.SplineOffset', 80) }) test('loads from workflow', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('reroute/native_reroute') + await comfyPage.workflow.loadWorkflow('reroute/native_reroute') await expect(comfyPage.canvas).toHaveScreenshot('native_reroute.png') }) @@ -56,10 +56,10 @@ test.describe( comfyPage }) => { const loadCheckpointNode = ( - await comfyPage.getNodeRefsByTitle('Load Checkpoint') + await comfyPage.nodeOps.getNodeRefsByTitle('Load Checkpoint') )[0] const clipEncodeNode = ( - await comfyPage.getNodeRefsByTitle('CLIP Text Encode (Prompt)') + await comfyPage.nodeOps.getNodeRefsByTitle('CLIP Text Encode (Prompt)') )[0] const slot1 = await loadCheckpointNode.getOutput(1) @@ -82,10 +82,10 @@ test.describe( comfyPage }) => { const loadCheckpointNode = ( - await comfyPage.getNodeRefsByTitle('Load Checkpoint') + await comfyPage.nodeOps.getNodeRefsByTitle('Load Checkpoint') )[0] const clipEncodeNode = ( - await comfyPage.getNodeRefsByTitle('CLIP Text Encode (Prompt)') + await comfyPage.nodeOps.getNodeRefsByTitle('CLIP Text Encode (Prompt)') )[0] const slot1 = await loadCheckpointNode.getOutput(1) @@ -109,7 +109,7 @@ test.describe( comfyPage }) => { // https://github.com/Comfy-Org/ComfyUI_frontend/issues/4695 - await comfyPage.loadWorkflow( + await comfyPage.workflow.loadWorkflow( 'reroute/single-native-reroute-default-workflow' ) diff --git a/browser_tests/tests/rightClickMenu.spec.ts b/browser_tests/tests/rightClickMenu.spec.ts index 3114a9456..6ddc9942e 100644 --- a/browser_tests/tests/rightClickMenu.spec.ts +++ b/browser_tests/tests/rightClickMenu.spec.ts @@ -2,9 +2,10 @@ import { expect } from '@playwright/test' import { NodeBadgeMode } from '../../src/types/nodeSource' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +import { DefaultGraphPositions } from '../fixtures/constants/defaultGraphPositions' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe( @@ -12,7 +13,7 @@ test.describe( { tag: ['@screenshot', '@ui'] }, () => { test('Can add node', async ({ comfyPage }) => { - await comfyPage.rightClickCanvas() + await comfyPage.canvasOps.rightClick() await expect(comfyPage.canvas).toHaveScreenshot('right-click-menu.png') await comfyPage.page.getByText('Add Node').click() await comfyPage.nextFrame() @@ -24,7 +25,7 @@ test.describe( }) test('Can add group', async ({ comfyPage }) => { - await comfyPage.rightClickCanvas() + await comfyPage.canvasOps.rightClick() await expect(comfyPage.canvas).toHaveScreenshot('right-click-menu.png') await comfyPage.page.getByText('Add Group', { exact: true }).click() await comfyPage.nextFrame() @@ -34,13 +35,16 @@ test.describe( }) test('Can convert to group node', async ({ comfyPage }) => { - await comfyPage.select2Nodes() + await comfyPage.nodeOps.selectNodes(['CLIP Text Encode (Prompt)']) await expect(comfyPage.canvas).toHaveScreenshot('selected-2-nodes.png') - await comfyPage.rightClickCanvas() - await comfyPage.clickContextMenuItem('Convert to Group Node (Deprecated)') - await comfyPage.promptDialogInput.fill('GroupNode2CLIP') + await comfyPage.canvasOps.rightClick() + await comfyPage.contextMenu.clickMenuItem( + 'Convert to Group Node (Deprecated)' + ) + await comfyPage.nextFrame() + await comfyPage.nodeOps.promptDialogInput.fill('GroupNode2CLIP') await comfyPage.page.keyboard.press('Enter') - await comfyPage.promptDialogInput.waitFor({ state: 'hidden' }) + await comfyPage.nodeOps.promptDialogInput.waitFor({ state: 'hidden' }) await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot( 'right-click-node-group-node.png' @@ -51,7 +55,12 @@ test.describe( test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => { test('Can open properties panel', async ({ comfyPage }) => { - await comfyPage.rightClickEmptyLatentNode() + await comfyPage.canvas.click({ + position: DefaultGraphPositions.emptyLatentWidgetClick, + button: 'right' + }) + await comfyPage.page.mouse.move(10, 10) + await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png') await comfyPage.page.getByText('Properties Panel').click() await comfyPage.nextFrame() @@ -61,7 +70,12 @@ test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => { }) test('Can collapse', async ({ comfyPage }) => { - await comfyPage.rightClickEmptyLatentNode() + await comfyPage.canvas.click({ + position: DefaultGraphPositions.emptyLatentWidgetClick, + button: 'right' + }) + await comfyPage.page.mouse.move(10, 10) + await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png') await comfyPage.page.getByText('Collapse').click() await comfyPage.nextFrame() @@ -71,16 +85,21 @@ test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => { }) test('Can collapse (Node Badge)', async ({ comfyPage }) => { - await comfyPage.setSetting( + await comfyPage.settings.setSetting( 'Comfy.NodeBadge.NodeIdBadgeMode', NodeBadgeMode.ShowAll ) - await comfyPage.setSetting( + await comfyPage.settings.setSetting( 'Comfy.NodeBadge.NodeSourceBadgeMode', NodeBadgeMode.ShowAll ) - await comfyPage.rightClickEmptyLatentNode() + await comfyPage.canvas.click({ + position: DefaultGraphPositions.emptyLatentWidgetClick, + button: 'right' + }) + await comfyPage.page.mouse.move(10, 10) + await comfyPage.nextFrame() await comfyPage.page.getByText('Collapse').click() await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot( @@ -89,7 +108,12 @@ test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => { }) test('Can bypass', async ({ comfyPage }) => { - await comfyPage.rightClickEmptyLatentNode() + await comfyPage.canvas.click({ + position: DefaultGraphPositions.emptyLatentWidgetClick, + button: 'right' + }) + await comfyPage.page.mouse.move(10, 10) + await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png') await comfyPage.page.getByText('Bypass').click() await comfyPage.nextFrame() @@ -99,46 +123,89 @@ test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => { }) test('Can pin and unpin', async ({ comfyPage }) => { - await comfyPage.rightClickEmptyLatentNode() + await comfyPage.canvas.click({ + position: DefaultGraphPositions.emptyLatentWidgetClick, + button: 'right' + }) + await comfyPage.page.mouse.move(10, 10) + await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png') await comfyPage.page.click('.litemenu-entry:has-text("Pin")') await comfyPage.nextFrame() - await comfyPage.dragAndDrop({ x: 621, y: 617 }, { x: 16, y: 16 }) + + // Get EmptyLatentImage node title position dynamically (for dragging) + const emptyLatentNode = await comfyPage.nodeOps.getNodeRefById(5) + const titlePos = await emptyLatentNode.getTitlePosition() + await comfyPage.canvasOps.dragAndDrop(titlePos, { x: 16, y: 16 }) await expect(comfyPage.canvas).toHaveScreenshot('node-pinned.png') - await comfyPage.rightClickEmptyLatentNode() + await comfyPage.canvas.click({ + position: DefaultGraphPositions.emptyLatentWidgetClick, + button: 'right' + }) + await comfyPage.page.mouse.move(10, 10) + await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot( 'right-click-pinned-node.png' ) await comfyPage.page.click('.litemenu-entry:has-text("Unpin")') await comfyPage.nextFrame() - await comfyPage.rightClickEmptyLatentNode() + await comfyPage.canvas.click({ + position: DefaultGraphPositions.emptyLatentWidgetClick, + button: 'right' + }) + await comfyPage.page.mouse.move(10, 10) + await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot( 'right-click-unpinned-node.png' ) }) test('Can move after unpin', async ({ comfyPage }) => { - await comfyPage.rightClickEmptyLatentNode() + await comfyPage.canvas.click({ + position: DefaultGraphPositions.emptyLatentWidgetClick, + button: 'right' + }) + await comfyPage.page.mouse.move(10, 10) + await comfyPage.nextFrame() await comfyPage.page.click('.litemenu-entry:has-text("Pin")') await comfyPage.nextFrame() - await comfyPage.rightClickEmptyLatentNode() + await comfyPage.canvas.click({ + position: DefaultGraphPositions.emptyLatentWidgetClick, + button: 'right' + }) + await comfyPage.page.mouse.move(10, 10) + await comfyPage.nextFrame() await comfyPage.page.click('.litemenu-entry:has-text("Unpin")') await comfyPage.nextFrame() - await comfyPage.dragAndDrop({ x: 496, y: 618 }, { x: 200, y: 590 }) + + // Get EmptyLatentImage node title position dynamically (for dragging) + const emptyLatentNode = await comfyPage.nodeOps.getNodeRefById(5) + const titlePos = await emptyLatentNode.getTitlePosition() + await comfyPage.canvasOps.dragAndDrop(titlePos, { x: 200, y: 590 }) await expect(comfyPage.canvas).toHaveScreenshot( 'right-click-unpinned-node-moved.png' ) }) test('Can pin/unpin selected nodes', async ({ comfyPage }) => { - await comfyPage.select2Nodes() + await comfyPage.nodeOps.selectNodes(['CLIP Text Encode (Prompt)']) await comfyPage.page.keyboard.down('Control') - await comfyPage.rightClickEmptyLatentNode() + await comfyPage.canvas.click({ + position: DefaultGraphPositions.emptyLatentWidgetClick, + button: 'right' + }) + await comfyPage.page.mouse.move(10, 10) + await comfyPage.nextFrame() await comfyPage.page.click('.litemenu-entry:has-text("Pin")') await comfyPage.page.keyboard.up('Control') await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot('selected-nodes-pinned.png') - await comfyPage.rightClickEmptyLatentNode() + await comfyPage.canvas.click({ + position: DefaultGraphPositions.emptyLatentWidgetClick, + button: 'right' + }) + await comfyPage.page.mouse.move(10, 10) + await comfyPage.nextFrame() await comfyPage.page.click('.litemenu-entry:has-text("Unpin")') await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot( @@ -147,8 +214,8 @@ test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => { }) test('Can clone pinned nodes', async ({ comfyPage }) => { - const nodeCount = await comfyPage.getGraphNodesCount() - const node = (await comfyPage.getFirstNodeRef())! + const nodeCount = await comfyPage.nodeOps.getGraphNodesCount() + const node = (await comfyPage.nodeOps.getFirstNodeRef())! await node.clickContextMenuOption('Pin') await comfyPage.nextFrame() await node.click('title', { button: 'right' }) @@ -161,6 +228,6 @@ test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => { await cloneItem.click() await expect(cloneItem).toHaveCount(0) await comfyPage.nextFrame() - expect(await comfyPage.getGraphNodesCount()).toBe(nodeCount + 1) + expect(await comfyPage.nodeOps.getGraphNodesCount()).toBe(nodeCount + 1) }) }) diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-unpinned-node-moved-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-unpinned-node-moved-chromium-linux.png index 1f3146378..0701d1d4c 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-unpinned-node-moved-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-unpinned-node-moved-chromium-linux.png differ diff --git a/browser_tests/tests/selectionToolbox.spec.ts b/browser_tests/tests/selectionToolbox.spec.ts index fd74a4aa7..fb0966d80 100644 --- a/browser_tests/tests/selectionToolbox.spec.ts +++ b/browser_tests/tests/selectionToolbox.spec.ts @@ -5,14 +5,14 @@ import { comfyPageFixture } from '../fixtures/ComfyPage' const test = comfyPageFixture test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) const BLUE_COLOR = 'rgb(51, 51, 85)' const RED_COLOR = 'rgb(85, 51, 51)' test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true) + await comfyPage.settings.setSetting('Comfy.Canvas.SelectionToolbox', true) }) test('shows selection toolbox', async ({ comfyPage }) => { @@ -20,7 +20,10 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => { await expect(comfyPage.selectionToolbox).not.toBeVisible() // Select multiple nodes - await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)']) + await comfyPage.nodeOps.selectNodes([ + 'KSampler', + 'CLIP Text Encode (Prompt)' + ]) // Selection toolbox should be visible with multiple nodes selected await expect(comfyPage.selectionToolbox).toBeVisible() @@ -33,11 +36,11 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => { test('shows at correct position when node is pasted', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('nodes/single_ksampler') - await comfyPage.selectNodes(['KSampler']) - await comfyPage.ctrlC() + await comfyPage.workflow.loadWorkflow('nodes/single_ksampler') + await comfyPage.nodeOps.selectNodes(['KSampler']) + await comfyPage.clipboard.copy() await comfyPage.page.mouse.move(100, 100) - await comfyPage.ctrlV() + await comfyPage.clipboard.paste() const toolboxContainer = comfyPage.selectionToolbox await expect(toolboxContainer).toBeVisible() @@ -54,8 +57,8 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => { test('hide when select and drag happen at the same time', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('nodes/single_ksampler') - const node = (await comfyPage.getNodeRefsByTitle('KSampler'))[0] + await comfyPage.workflow.loadWorkflow('nodes/single_ksampler') + const node = (await comfyPage.nodeOps.getNodeRefsByTitle('KSampler'))[0] const nodePos = await node.getPosition() // Drag on the title of the node @@ -68,7 +71,7 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => { test('shows border only with multiple selections', async ({ comfyPage }) => { // Select single node - await comfyPage.selectNodes(['KSampler']) + await comfyPage.nodeOps.selectNodes(['KSampler']) // Selection toolbox should be visible but without border await expect(comfyPage.selectionToolbox).toBeVisible() @@ -78,7 +81,10 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => { ) // Select multiple nodes - await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)']) + await comfyPage.nodeOps.selectNodes([ + 'KSampler', + 'CLIP Text Encode (Prompt)' + ]) // Selection border should show with multiple selections (canvas-based) await expect(comfyPage.canvas).toHaveScreenshot( @@ -86,7 +92,7 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => { ) // Deselect to single node - await comfyPage.selectNodes(['CLIP Text Encode (Prompt)']) + await comfyPage.nodeOps.selectNodes(['CLIP Text Encode (Prompt)']) // Border should be hidden again (canvas-based) await expect(comfyPage.canvas).toHaveScreenshot( @@ -98,7 +104,7 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => { comfyPage }) => { // A group + a KSampler node - await comfyPage.loadWorkflow('groups/single_group') + await comfyPage.workflow.loadWorkflow('groups/single_group') // Select group + node should show bypass button await comfyPage.page.focus('canvas') @@ -110,7 +116,7 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => { ).toBeVisible() // Deselect node (Only group is selected) should hide bypass button - await comfyPage.selectNodes(['KSampler']) + await comfyPage.nodeOps.selectNodes(['KSampler']) await expect( comfyPage.page.locator( '.selection-toolbox *[data-testid="bypass-button"]' @@ -123,7 +129,7 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => { comfyPage }) => { // Select a node - await comfyPage.selectNodes(['KSampler']) + await comfyPage.nodeOps.selectNodes(['KSampler']) // Color picker button should be visible const colorPickerButton = comfyPage.page.locator( @@ -151,7 +157,9 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => { // Node should have the selected color class/style // Note: Exact verification method depends on how color is applied to nodes - const selectedNode = (await comfyPage.getNodeRefsByTitle('KSampler'))[0] + const selectedNode = ( + await comfyPage.nodeOps.getNodeRefsByTitle('KSampler') + )[0] expect(await selectedNode.getProperty('color')).not.toBeNull() }) @@ -159,7 +167,10 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => { comfyPage }) => { // Select multiple nodes - await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)']) + await comfyPage.nodeOps.selectNodes([ + 'KSampler', + 'CLIP Text Encode (Prompt)' + ]) const colorPickerButton = comfyPage.page.locator( '.selection-toolbox .pi-circle-fill' @@ -183,22 +194,25 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => { comfyPage }) => { // Select first node and color it - await comfyPage.selectNodes(['KSampler']) + await comfyPage.nodeOps.selectNodes(['KSampler']) await comfyPage.page.locator('.selection-toolbox .pi-circle-fill').click() await comfyPage.page .locator('.color-picker-container i[data-testid="blue"]') .click() - await comfyPage.selectNodes(['KSampler']) + await comfyPage.nodeOps.selectNodes(['KSampler']) // Select second node and color it differently - await comfyPage.selectNodes(['CLIP Text Encode (Prompt)']) + await comfyPage.nodeOps.selectNodes(['CLIP Text Encode (Prompt)']) await comfyPage.page.locator('.selection-toolbox .pi-circle-fill').click() await comfyPage.page .locator('.color-picker-container i[data-testid="red"]') .click() // Select both nodes - await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)']) + await comfyPage.nodeOps.selectNodes([ + 'KSampler', + 'CLIP Text Encode (Prompt)' + ]) // Color picker should show null/mixed state const colorPickerButton = comfyPage.page.locator( @@ -211,17 +225,17 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => { comfyPage }) => { // First color a node - await comfyPage.selectNodes(['KSampler']) + await comfyPage.nodeOps.selectNodes(['KSampler']) await comfyPage.page.locator('.selection-toolbox .pi-circle-fill').click() await comfyPage.page .locator('.color-picker-container i[data-testid="blue"]') .click() // Clear selection - await comfyPage.selectNodes(['KSampler']) + await comfyPage.nodeOps.selectNodes(['KSampler']) // Re-select the node - await comfyPage.selectNodes(['KSampler']) + await comfyPage.nodeOps.selectNodes(['KSampler']) // Color picker button should show the correct color const colorPickerButton = comfyPage.page.locator( @@ -234,7 +248,7 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => { comfyPage }) => { // Select a node and color it - await comfyPage.selectNodes(['KSampler']) + await comfyPage.nodeOps.selectNodes(['KSampler']) await comfyPage.page.locator('.selection-toolbox .pi-circle-fill').click() await comfyPage.page .locator('.color-picker-container i[data-testid="blue"]') @@ -245,7 +259,9 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => { await comfyPage.nextFrame() // Node should be uncolored again - const selectedNode = (await comfyPage.getNodeRefsByTitle('KSampler'))[0] + const selectedNode = ( + await comfyPage.nodeOps.getNodeRefsByTitle('KSampler') + )[0] expect(await selectedNode.getProperty('color')).toBeUndefined() }) }) diff --git a/browser_tests/tests/selectionToolboxSubmenus.spec.ts b/browser_tests/tests/selectionToolboxSubmenus.spec.ts index c5d74c167..c97d5d5b3 100644 --- a/browser_tests/tests/selectionToolboxSubmenus.spec.ts +++ b/browser_tests/tests/selectionToolboxSubmenus.spec.ts @@ -4,7 +4,7 @@ import { comfyPageFixture as test } from '../fixtures/ComfyPage' import type { ComfyPage } from '../fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe( @@ -12,15 +12,16 @@ test.describe( { tag: '@ui' }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true) - await comfyPage.loadWorkflow('nodes/single_ksampler') + await comfyPage.settings.setSetting('Comfy.Canvas.SelectionToolbox', true) + await comfyPage.workflow.loadWorkflow('nodes/single_ksampler') await comfyPage.nextFrame() - await comfyPage.selectNodes(['KSampler']) + await comfyPage.nodeOps.selectNodes(['KSampler']) await comfyPage.nextFrame() }) const openMoreOptions = async (comfyPage: ComfyPage) => { - const ksamplerNodes = await comfyPage.getNodeRefsByTitle('KSampler') + const ksamplerNodes = + await comfyPage.nodeOps.getNodeRefsByTitle('KSampler') if (ksamplerNodes.length === 0) { throw new Error('No KSampler nodes found') } @@ -28,9 +29,14 @@ test.describe( // Drag the KSampler to the center of the screen const nodePos = await ksamplerNodes[0].getPosition() const viewportSize = comfyPage.page.viewportSize() + if (!viewportSize) { + throw new Error( + 'Viewport size is null - page may not be properly initialized' + ) + } const centerX = viewportSize.width / 3 const centerY = viewportSize.height / 2 - await comfyPage.dragAndDrop( + await comfyPage.canvasOps.dragAndDrop( { x: nodePos.x, y: nodePos.y }, { x: centerX, y: centerY } ) @@ -85,7 +91,9 @@ test.describe( }) test('changes node shape via Shape submenu', async ({ comfyPage }) => { - const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0] + const nodeRef = ( + await comfyPage.nodeOps.getNodeRefsByTitle('KSampler') + )[0] const initialShape = await nodeRef.getProperty('shape') await openMoreOptions(comfyPage) @@ -106,7 +114,9 @@ test.describe( test('changes node color via Color submenu swatch', async ({ comfyPage }) => { - const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0] + const nodeRef = ( + await comfyPage.nodeOps.getNodeRefsByTitle('KSampler') + )[0] const initialColor = await nodeRef.getProperty( 'color' ) @@ -126,7 +136,9 @@ test.describe( }) test('renames a node using Rename action', async ({ comfyPage }) => { - const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0] + const nodeRef = ( + await comfyPage.nodeOps.getNodeRefsByTitle('KSampler') + )[0] await openMoreOptions(comfyPage) await comfyPage.page .getByText('Rename', { exact: true }) diff --git a/browser_tests/tests/sidebar/nodeLibrary.spec.ts b/browser_tests/tests/sidebar/nodeLibrary.spec.ts index 4f476376a..6f18bcdee 100644 --- a/browser_tests/tests/sidebar/nodeLibrary.spec.ts +++ b/browser_tests/tests/sidebar/nodeLibrary.spec.ts @@ -4,9 +4,12 @@ import { comfyPageFixture as test } from '../../fixtures/ComfyPage' test.describe('Node library sidebar', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', []) - await comfyPage.setSetting('Comfy.NodeLibrary.BookmarksCustomization', {}) + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', []) + await comfyPage.settings.setSetting( + 'Comfy.NodeLibrary.BookmarksCustomization', + {} + ) // Open the sidebar const tab = comfyPage.menu.nodeLibraryTab await tab.open() @@ -26,7 +29,7 @@ test.describe('Node library sidebar', () => { ) expect(previewVisible).toBe(true) - const count = await comfyPage.getGraphNodesCount() + const count = await comfyPage.nodeOps.getGraphNodesCount() // Drag the node onto the canvas const canvasSelector = '#graph-canvas' @@ -46,7 +49,7 @@ test.describe('Node library sidebar', () => { }) // Verify the node is added to the canvas - expect(await comfyPage.getGraphNodesCount()).toBe(count + 1) + expect(await comfyPage.nodeOps.getGraphNodesCount()).toBe(count + 1) }) test('Bookmark node', async ({ comfyPage }) => { @@ -58,7 +61,7 @@ test.describe('Node library sidebar', () => { // Verify the bookmark is added to the bookmarks tab expect( - await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') + await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2') ).toEqual(['KSamplerAdvanced']) // Verify the bookmark node with the same name is added to the tree. expect(await tab.getNode('KSampler (Advanced)').count()).toBe(2) @@ -69,7 +72,9 @@ test.describe('Node library sidebar', () => { }) test('Ignores unrecognized node', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo']) + await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ + 'foo' + ]) const tab = comfyPage.menu.nodeLibraryTab expect(await tab.getFolder('sampling').count()).toBe(1) @@ -77,7 +82,9 @@ test.describe('Node library sidebar', () => { }) test('Displays empty bookmarks folder', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) + await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ + 'foo/' + ]) const tab = comfyPage.menu.nodeLibraryTab expect(await tab.getFolder('foo').count()).toBe(1) }) @@ -91,12 +98,14 @@ test.describe('Node library sidebar', () => { await textInput.press('Enter') expect(await tab.getFolder('New Folder').count()).toBe(1) expect( - await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') + await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2') ).toEqual(['New Folder/']) }) test('Can add nested bookmark folder', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) + await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ + 'foo/' + ]) const tab = comfyPage.menu.nodeLibraryTab await tab.getFolder('foo').click({ button: 'right' }) @@ -108,24 +117,28 @@ test.describe('Node library sidebar', () => { expect(await tab.getFolder('bar').count()).toBe(1) expect( - await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') + await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2') ).toEqual(['foo/', 'foo/bar/']) }) test('Can delete bookmark folder', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) + await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ + 'foo/' + ]) const tab = comfyPage.menu.nodeLibraryTab await tab.getFolder('foo').click({ button: 'right' }) await comfyPage.page.getByLabel('Delete').click() expect( - await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') + await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2') ).toEqual([]) }) test('Can rename bookmark folder', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) + await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ + 'foo/' + ]) const tab = comfyPage.menu.nodeLibraryTab await tab.getFolder('foo').click({ button: 'right' }) @@ -136,14 +149,16 @@ test.describe('Node library sidebar', () => { await comfyPage.page.keyboard.press('Enter') expect( - await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') + await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2') ).toEqual(['bar/']) }) test('Can add bookmark by dragging node to bookmark folder', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) + await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ + 'foo/' + ]) const tab = comfyPage.menu.nodeLibraryTab await tab.getFolder('sampling').click() await comfyPage.page.dragAndDrop( @@ -151,7 +166,7 @@ test.describe('Node library sidebar', () => { tab.folderSelector('foo') ) expect( - await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') + await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2') ).toEqual(['foo/', 'foo/KSamplerAdvanced']) }) @@ -162,51 +177,60 @@ test.describe('Node library sidebar', () => { await tab.getFolder('sampling').click() await tab.getNode('KSampler (Advanced)').locator('.bookmark-button').click() expect( - await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') + await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2') ).toEqual(['KSamplerAdvanced']) }) test('Can unbookmark node (Top level bookmark)', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ + await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ 'KSamplerAdvanced' ]) const tab = comfyPage.menu.nodeLibraryTab await tab.getNode('KSampler (Advanced)').locator('.bookmark-button').click() expect( - await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') + await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2') ).toEqual([]) }) test('Can unbookmark node (Library node bookmark)', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ + await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ 'KSamplerAdvanced' ]) const tab = comfyPage.menu.nodeLibraryTab await tab.getFolder('sampling').click() - await comfyPage.page - .locator(tab.nodeSelector('KSampler (Advanced)')) - .nth(1) + await tab + .getNodeInFolder('KSampler (Advanced)', 'sampling') .locator('.bookmark-button') .click() expect( - await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') + await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2') ).toEqual([]) }) test('Can customize icon', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) + await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ + 'foo/' + ]) const tab = comfyPage.menu.nodeLibraryTab await tab.getFolder('foo').click({ button: 'right' }) await comfyPage.page.getByLabel('Customize').click() - await comfyPage.page - .locator('.icon-field .p-selectbutton > *:nth-child(2)') - .click() - await comfyPage.page - .locator('.color-field .p-selectbutton > *:nth-child(2)') - .click() - await comfyPage.page.getByRole('button', { name: 'Confirm' }).click() + const dialog = comfyPage.page.getByRole('dialog', { + name: 'Customize Folder' + }) + // Select Folder icon (2nd button in Icon group) + const iconGroup = dialog.getByText('Icon').locator('..').getByRole('group') + await iconGroup.getByRole('button').nth(1).click() + // Select Blue color (2nd button in Color group) + const colorGroup = dialog + .getByText('Color') + .locator('..') + .getByRole('group') + await colorGroup.getByRole('button').nth(1).click() + await dialog.getByRole('button', { name: 'Confirm' }).click() await comfyPage.nextFrame() expect( - await comfyPage.getSetting('Comfy.NodeLibrary.BookmarksCustomization') + await comfyPage.settings.getSetting( + 'Comfy.NodeLibrary.BookmarksCustomization' + ) ).toEqual({ 'foo/': { icon: 'pi-folder', @@ -216,17 +240,24 @@ test.describe('Node library sidebar', () => { }) // If color is left as default, it should not be saved test('Can customize icon (default field)', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) + await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ + 'foo/' + ]) const tab = comfyPage.menu.nodeLibraryTab await tab.getFolder('foo').click({ button: 'right' }) await comfyPage.page.getByLabel('Customize').click() - await comfyPage.page - .locator('.icon-field .p-selectbutton > *:nth-child(2)') - .click() - await comfyPage.page.getByRole('button', { name: 'Confirm' }).click() + const dialog = comfyPage.page.getByRole('dialog', { + name: 'Customize Folder' + }) + // Select Folder icon (2nd button in Icon group) + const iconGroup = dialog.getByText('Icon').locator('..').getByRole('group') + await iconGroup.getByRole('button').nth(1).click() + await dialog.getByRole('button', { name: 'Confirm' }).click() await comfyPage.nextFrame() expect( - await comfyPage.getSetting('Comfy.NodeLibrary.BookmarksCustomization') + await comfyPage.settings.getSetting( + 'Comfy.NodeLibrary.BookmarksCustomization' + ) ).toEqual({ 'foo/': { icon: 'pi-folder' @@ -238,7 +269,9 @@ test.describe('Node library sidebar', () => { comfyPage }) => { // Open customization dialog - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) + await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ + 'foo/' + ]) const tab = comfyPage.menu.nodeLibraryTab await tab.getFolder('foo').click({ button: 'right' }) await comfyPage.page.getByLabel('Customize').click() @@ -258,16 +291,19 @@ test.describe('Node library sidebar', () => { await comfyPage.page.locator('.p-colorpicker-color-background').click() // Finalize the customization - await comfyPage.page - .locator('.icon-field .p-selectbutton > *:nth-child(2)') - .click() - await comfyPage.page.getByRole('button', { name: 'Confirm' }).click() + const dialog = comfyPage.page.getByRole('dialog', { + name: 'Customize Folder' + }) + // Select Folder icon (2nd button in Icon group) + const iconGroup = dialog.getByText('Icon').locator('..').getByRole('group') + await iconGroup.getByRole('button').nth(1).click() + await dialog.getByRole('button', { name: 'Confirm' }).click() await comfyPage.nextFrame() // Verify the color selection is saved - const setting = await comfyPage.getSetting( - 'Comfy.NodeLibrary.BookmarksCustomization' - ) + const setting = await comfyPage.settings.getSetting< + Record + >('Comfy.NodeLibrary.BookmarksCustomization') await expect(setting).toHaveProperty(['foo/', 'color']) await expect(setting['foo/'].color).not.toBeNull() await expect(setting['foo/'].color).not.toBeUndefined() @@ -275,13 +311,18 @@ test.describe('Node library sidebar', () => { }) test('Can rename customized bookmark folder', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) - await comfyPage.setSetting('Comfy.NodeLibrary.BookmarksCustomization', { - 'foo/': { - icon: 'pi-folder', - color: '#007bff' + await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ + 'foo/' + ]) + await comfyPage.settings.setSetting( + 'Comfy.NodeLibrary.BookmarksCustomization', + { + 'foo/': { + icon: 'pi-folder', + color: '#007bff' + } } - }) + ) const tab = comfyPage.menu.nodeLibraryTab await tab.getFolder('foo').click({ button: 'right' }) await comfyPage.page @@ -292,10 +333,12 @@ test.describe('Node library sidebar', () => { await comfyPage.nextFrame() await expect(async () => { expect( - await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') + await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2') ).toEqual(['bar/']) expect( - await comfyPage.getSetting('Comfy.NodeLibrary.BookmarksCustomization') + await comfyPage.settings.getSetting( + 'Comfy.NodeLibrary.BookmarksCustomization' + ) ).toEqual({ 'bar/': { icon: 'pi-folder', @@ -308,27 +351,34 @@ test.describe('Node library sidebar', () => { }) test('Can delete customized bookmark folder', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) - await comfyPage.setSetting('Comfy.NodeLibrary.BookmarksCustomization', { - 'foo/': { - icon: 'pi-folder', - color: '#007bff' + await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ + 'foo/' + ]) + await comfyPage.settings.setSetting( + 'Comfy.NodeLibrary.BookmarksCustomization', + { + 'foo/': { + icon: 'pi-folder', + color: '#007bff' + } } - }) + ) const tab = comfyPage.menu.nodeLibraryTab await tab.getFolder('foo').click({ button: 'right' }) await comfyPage.page.getByLabel('Delete').click() await comfyPage.nextFrame() expect( - await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') + await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2') ).toEqual([]) expect( - await comfyPage.getSetting('Comfy.NodeLibrary.BookmarksCustomization') + await comfyPage.settings.getSetting( + 'Comfy.NodeLibrary.BookmarksCustomization' + ) ).toEqual({}) }) test('Can filter nodes in both trees', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ + await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ 'foo/', 'foo/KSamplerAdvanced', 'KSampler' @@ -336,8 +386,6 @@ test.describe('Node library sidebar', () => { const tab = comfyPage.menu.nodeLibraryTab await tab.nodeLibrarySearchBoxInput.fill('KSampler') - // Node search box is debounced and may take some time to update. - await comfyPage.page.waitForTimeout(1000) - expect(await tab.getNode('KSampler (Advanced)').count()).toBe(2) + await expect(tab.getNode('KSampler (Advanced)')).toHaveCount(2) }) }) diff --git a/browser_tests/tests/sidebar/workflows.spec.ts b/browser_tests/tests/sidebar/workflows.spec.ts index 86f37b23d..7ff026e53 100644 --- a/browser_tests/tests/sidebar/workflows.spec.ts +++ b/browser_tests/tests/sidebar/workflows.spec.ts @@ -4,8 +4,11 @@ import { comfyPageFixture as test } from '../../fixtures/ComfyPage' test.describe('Workflows sidebar', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') - await comfyPage.setSetting('Comfy.Workflow.WorkflowTabsPosition', 'Sidebar') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting( + 'Comfy.Workflow.WorkflowTabsPosition', + 'Sidebar' + ) // Open the sidebar const tab = comfyPage.menu.workflowsTab @@ -13,7 +16,7 @@ test.describe('Workflows sidebar', () => { }) test.afterEach(async ({ comfyPage }) => { - await comfyPage.setupWorkflowsDirectory({}) + await comfyPage.workflow.setupWorkflowsDirectory({}) }) test('Can create new blank workflow', async ({ comfyPage }) => { @@ -22,7 +25,7 @@ test.describe('Workflows sidebar', () => { '*Unsaved Workflow.json' ]) - await comfyPage.executeCommand('Comfy.NewBlankWorkflow') + await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow') expect(await tab.getOpenedWorkflowNames()).toEqual([ '*Unsaved Workflow.json', '*Unsaved Workflow (2).json' @@ -30,7 +33,7 @@ test.describe('Workflows sidebar', () => { }) test('Can show top level saved workflows', async ({ comfyPage }) => { - await comfyPage.setupWorkflowsDirectory({ + await comfyPage.workflow.setupWorkflowsDirectory({ 'workflow1.json': 'default.json', 'workflow2.json': 'default.json' }) @@ -50,20 +53,20 @@ test.describe('Workflows sidebar', () => { expect.arrayContaining(['workflow1.json']) ) - await comfyPage.executeCommand('Comfy.DuplicateWorkflow') + await comfyPage.command.executeCommand('Comfy.DuplicateWorkflow') expect(await tab.getOpenedWorkflowNames()).toEqual([ 'workflow1.json', '*workflow1 (Copy).json' ]) - await comfyPage.executeCommand('Comfy.DuplicateWorkflow') + await comfyPage.command.executeCommand('Comfy.DuplicateWorkflow') expect(await tab.getOpenedWorkflowNames()).toEqual([ 'workflow1.json', '*workflow1 (Copy).json', '*workflow1 (Copy) (2).json' ]) - await comfyPage.executeCommand('Comfy.DuplicateWorkflow') + await comfyPage.command.executeCommand('Comfy.DuplicateWorkflow') expect(await tab.getOpenedWorkflowNames()).toEqual([ 'workflow1.json', '*workflow1 (Copy).json', @@ -73,28 +76,30 @@ test.describe('Workflows sidebar', () => { }) test('Can open workflow after insert', async ({ comfyPage }) => { - await comfyPage.setupWorkflowsDirectory({ + await comfyPage.workflow.setupWorkflowsDirectory({ 'workflow1.json': 'nodes/single_ksampler.json' }) const tab = comfyPage.menu.workflowsTab await tab.open() - await comfyPage.executeCommand('Comfy.LoadDefaultWorkflow') - const originalNodeCount = (await comfyPage.getNodes()).length + await comfyPage.command.executeCommand('Comfy.LoadDefaultWorkflow') + const originalNodeCount = (await comfyPage.nodeOps.getNodes()).length await tab.insertWorkflow(tab.getPersistedItem('workflow1.json')) - await comfyPage.nextFrame() - expect((await comfyPage.getNodes()).length).toEqual(originalNodeCount + 1) + await expect + .poll(() => comfyPage.nodeOps.getNodes().then((n) => n.length)) + .toEqual(originalNodeCount + 1) await tab.getPersistedItem('workflow1.json').click() - await comfyPage.nextFrame() - expect((await comfyPage.getNodes()).length).toEqual(1) + await expect + .poll(() => comfyPage.nodeOps.getNodes().then((n) => n.length)) + .toEqual(1) }) test('Can rename nested workflow from opened workflow item', async ({ comfyPage }) => { - await comfyPage.setupWorkflowsDirectory({ + await comfyPage.workflow.setupWorkflowsDirectory({ foo: { 'bar.json': 'default.json' } @@ -116,7 +121,7 @@ test.describe('Workflows sidebar', () => { }) test('Can save workflow as', async ({ comfyPage }) => { - await comfyPage.executeCommand('Comfy.NewBlankWorkflow') + await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow') await comfyPage.menu.topbar.saveWorkflowAs('workflow3.json') expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([ '*Unsaved Workflow.json', @@ -134,17 +139,17 @@ test.describe('Workflows sidebar', () => { test('Exported workflow does not contain localized slot names', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('default') - const exportedWorkflow = await comfyPage.getExportedWorkflow({ + await comfyPage.workflow.loadWorkflow('default') + const exportedWorkflow = await comfyPage.workflow.getExportedWorkflow({ api: false }) expect(exportedWorkflow).toBeDefined() for (const node of exportedWorkflow.nodes) { - for (const slot of node.inputs) { + for (const slot of node.inputs ?? []) { expect(slot.localized_name).toBeUndefined() expect(slot.label).toBeUndefined() } - for (const slot of node.outputs) { + for (const slot of node.outputs ?? []) { expect(slot.localized_name).toBeUndefined() expect(slot.label).toBeUndefined() } @@ -154,7 +159,7 @@ test.describe('Workflows sidebar', () => { test('Can export same workflow with different locales', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('default') + await comfyPage.workflow.loadWorkflow('default') // Setup download listener before triggering the export const downloadPromise = comfyPage.page.waitForEvent('download') @@ -165,14 +170,14 @@ test.describe('Workflows sidebar', () => { expect(download.suggestedFilename()).toBe('exported_default.json') // Get the exported workflow content - const downloadedContent = await comfyPage.getExportedWorkflow({ + const downloadedContent = await comfyPage.workflow.getExportedWorkflow({ api: false }) - await comfyPage.setSetting('Comfy.Locale', 'zh') + await comfyPage.settings.setSetting('Comfy.Locale', 'zh') await comfyPage.setup() - const downloadedContentZh = await comfyPage.getExportedWorkflow({ + const downloadedContentZh = await comfyPage.workflow.getExportedWorkflow({ api: false }) @@ -199,7 +204,7 @@ test.describe('Workflows sidebar', () => { test('Can save temporary workflow with unmodified name', async ({ comfyPage }) => { - expect(await comfyPage.isCurrentWorkflowModified()).toBe(false) + expect(await comfyPage.workflow.isCurrentWorkflowModified()).toBe(false) await comfyPage.menu.topbar.saveWorkflow('Unsaved Workflow') // Should not trigger the overwrite dialog @@ -207,7 +212,7 @@ test.describe('Workflows sidebar', () => { await comfyPage.page.locator('.comfy-modal-content:visible').count() ).toBe(0) - expect(await comfyPage.isCurrentWorkflowModified()).toBe(false) + expect(await comfyPage.workflow.isCurrentWorkflowModified()).toBe(false) }) test('Can overwrite other workflows with save as', async ({ comfyPage }) => { @@ -238,12 +243,16 @@ test.describe('Workflows sidebar', () => { test('Does not report warning when switching between opened workflows', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('missing/missing_nodes') - await comfyPage.closeDialog() + await comfyPage.workflow.loadWorkflow('missing/missing_nodes') + await comfyPage.page + .locator('.p-dialog') + .getByRole('button', { name: 'Close' }) + .click({ force: true }) + await comfyPage.page.locator('.p-dialog').waitFor({ state: 'hidden' }) // Load blank workflow await comfyPage.menu.workflowsTab.open() - await comfyPage.executeCommand('Comfy.NewBlankWorkflow') + await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow') // Switch back to the missing_nodes workflow await comfyPage.menu.workflowsTab.switchToWorkflow('missing_nodes') @@ -271,14 +280,14 @@ test.describe('Workflows sidebar', () => { test('Can close saved workflow with command', async ({ comfyPage }) => { const tab = comfyPage.menu.workflowsTab await comfyPage.menu.topbar.saveWorkflow('workflow1.json') - await comfyPage.executeCommand('Workspace.CloseWorkflow') + await comfyPage.command.executeCommand('Workspace.CloseWorkflow') expect(await tab.getOpenedWorkflowNames()).toEqual([ '*Unsaved Workflow.json' ]) }) test('Can delete workflows (confirm disabled)', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Workflow.ConfirmDelete', false) + await comfyPage.settings.setSetting('Comfy.Workflow.ConfirmDelete', false) const { topbar, workflowsTab } = comfyPage.menu @@ -288,7 +297,8 @@ test.describe('Workflows sidebar', () => { await workflowsTab.getOpenedItem(filename).click({ button: 'right' }) await comfyPage.nextFrame() - await comfyPage.clickContextMenuItem('Delete') + await comfyPage.contextMenu.clickMenuItem('Delete') + await comfyPage.nextFrame() await expect(workflowsTab.getOpenedItem(filename)).not.toBeVisible() expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([ @@ -304,7 +314,8 @@ test.describe('Workflows sidebar', () => { expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([filename]) await workflowsTab.getOpenedItem(filename).click({ button: 'right' }) - await comfyPage.clickContextMenuItem('Delete') + await comfyPage.contextMenu.clickMenuItem('Delete') + await comfyPage.nextFrame() await comfyPage.confirmDialog.click('delete') @@ -315,7 +326,7 @@ test.describe('Workflows sidebar', () => { }) test('Can duplicate workflow from context menu', async ({ comfyPage }) => { - await comfyPage.setupWorkflowsDirectory({ + await comfyPage.workflow.setupWorkflowsDirectory({ 'workflow1.json': 'default.json' }) @@ -325,7 +336,8 @@ test.describe('Workflows sidebar', () => { await workflowsTab .getPersistedItem('workflow1.json') .click({ button: 'right' }) - await comfyPage.clickContextMenuItem('Duplicate') + await comfyPage.contextMenu.clickMenuItem('Duplicate') + await comfyPage.nextFrame() expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([ '*Unsaved Workflow.json', @@ -334,7 +346,7 @@ test.describe('Workflows sidebar', () => { }) test('Can drop workflow from workflows sidebar', async ({ comfyPage }) => { - await comfyPage.setupWorkflowsDirectory({ + await comfyPage.workflow.setupWorkflowsDirectory({ 'workflow1.json': 'default.json' }) @@ -345,7 +357,7 @@ test.describe('Workflows sidebar', () => { comfyPage.menu.workflowsTab.getPersistedItem('workflow1.json') await expect(workflowItem).toBeVisible({ timeout: 3000 }) - const nodeCount = await comfyPage.getGraphNodesCount() + const nodeCount = await comfyPage.nodeOps.getGraphNodesCount() // Get the bounding box of the canvas element const canvasBoundingBox = (await comfyPage.page @@ -366,7 +378,7 @@ test.describe('Workflows sidebar', () => { // Wait for nodes to be inserted after drag-drop with retryable assertion await expect - .poll(() => comfyPage.getGraphNodesCount(), { timeout: 3000 }) + .poll(() => comfyPage.nodeOps.getGraphNodesCount(), { timeout: 3000 }) .toBe(nodeCount * 2) }) }) diff --git a/browser_tests/tests/subgraph-rename-dialog.spec.ts b/browser_tests/tests/subgraph-rename-dialog.spec.ts index f917bdb9b..b0a36bae8 100644 --- a/browser_tests/tests/subgraph-rename-dialog.spec.ts +++ b/browser_tests/tests/subgraph-rename-dialog.spec.ts @@ -3,7 +3,6 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' // Constants -const INITIAL_NAME = 'initial_slot_name' const RENAMED_NAME = 'renamed_slot_name' const SECOND_RENAMED_NAME = 'second_renamed_name' @@ -14,26 +13,34 @@ const SELECTORS = { test.describe('Subgraph Slot Rename Dialog', { tag: '@subgraph' }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test('Shows current slot label (not stale) in rename dialog', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/basic-subgraph') + await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph') - const subgraphNode = await comfyPage.getNodeRefById('2') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2') await subgraphNode.navigateIntoSubgraph() // Get initial slot label const initialInputLabel = await comfyPage.page.evaluate(() => { - const graph = window['app'].canvas.graph + const graph = window.app!.canvas.graph + if (!graph || !('inputNode' in graph)) return null return graph.inputs?.[0]?.label || graph.inputs?.[0]?.name || null }) + if (initialInputLabel === null) { + throw new Error( + 'Expected subgraph to have an input slot label for rightClickInputSlot' + ) + } + // First rename - await comfyPage.rightClickSubgraphInputSlot(initialInputLabel) - await comfyPage.clickLitegraphContextMenuItem('Rename Slot') + await comfyPage.subgraph.rightClickInputSlot(initialInputLabel) + await comfyPage.contextMenu.clickLitegraphMenuItem('Rename Slot') + await comfyPage.nextFrame() await comfyPage.page.waitForSelector(SELECTORS.promptDialog, { state: 'visible' @@ -55,7 +62,9 @@ test.describe('Subgraph Slot Rename Dialog', { tag: '@subgraph' }, () => { // Verify the rename worked const afterFirstRename = await comfyPage.page.evaluate(() => { - const graph = window['app'].canvas.graph + const graph = window.app!.canvas.graph + if (!graph || !('inputNode' in graph)) + return { label: null, name: null, displayName: null } const slot = graph.inputs?.[0] return { label: slot?.label || null, @@ -67,8 +76,9 @@ test.describe('Subgraph Slot Rename Dialog', { tag: '@subgraph' }, () => { // Now rename again - this is where the bug would show // We need to use the index-based approach since the method looks for slot.name - await comfyPage.rightClickSubgraphInputSlot() - await comfyPage.clickLitegraphContextMenuItem('Rename Slot') + await comfyPage.subgraph.rightClickInputSlot() + await comfyPage.contextMenu.clickLitegraphMenuItem('Rename Slot') + await comfyPage.nextFrame() await comfyPage.page.waitForSelector(SELECTORS.promptDialog, { state: 'visible' @@ -97,7 +107,8 @@ test.describe('Subgraph Slot Rename Dialog', { tag: '@subgraph' }, () => { // Verify the second rename worked const afterSecondRename = await comfyPage.page.evaluate(() => { - const graph = window['app'].canvas.graph + const graph = window.app!.canvas.graph + if (!graph || !('inputNode' in graph)) return null return graph.inputs?.[0]?.label || null }) expect(afterSecondRename).toBe(SECOND_RENAMED_NAME) @@ -106,20 +117,28 @@ test.describe('Subgraph Slot Rename Dialog', { tag: '@subgraph' }, () => { test('Shows current output slot label in rename dialog', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/basic-subgraph') + await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph') - const subgraphNode = await comfyPage.getNodeRefById('2') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2') await subgraphNode.navigateIntoSubgraph() // Get initial output slot label const initialOutputLabel = await comfyPage.page.evaluate(() => { - const graph = window['app'].canvas.graph + const graph = window.app!.canvas.graph + if (!graph || !('inputNode' in graph)) return null return graph.outputs?.[0]?.label || graph.outputs?.[0]?.name || null }) + if (initialOutputLabel === null) { + throw new Error( + 'Expected subgraph to have an output slot label for rightClickOutputSlot' + ) + } + // First rename - await comfyPage.rightClickSubgraphOutputSlot(initialOutputLabel) - await comfyPage.clickLitegraphContextMenuItem('Rename Slot') + await comfyPage.subgraph.rightClickOutputSlot(initialOutputLabel) + await comfyPage.contextMenu.clickLitegraphMenuItem('Rename Slot') + await comfyPage.nextFrame() await comfyPage.page.waitForSelector(SELECTORS.promptDialog, { state: 'visible' @@ -141,8 +160,9 @@ test.describe('Subgraph Slot Rename Dialog', { tag: '@subgraph' }, () => { // Now rename again to check for stale content // We need to use the index-based approach since the method looks for slot.name - await comfyPage.rightClickSubgraphOutputSlot() - await comfyPage.clickLitegraphContextMenuItem('Rename Slot') + await comfyPage.subgraph.rightClickOutputSlot() + await comfyPage.contextMenu.clickLitegraphMenuItem('Rename Slot') + await comfyPage.nextFrame() await comfyPage.page.waitForSelector(SELECTORS.promptDialog, { state: 'visible' diff --git a/browser_tests/tests/subgraph.spec.ts b/browser_tests/tests/subgraph.spec.ts index 1719ba059..cc78b4953 100644 --- a/browser_tests/tests/subgraph.spec.ts +++ b/browser_tests/tests/subgraph.spec.ts @@ -18,7 +18,7 @@ const SELECTORS = { test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) // Helper to get subgraph slot count @@ -26,8 +26,11 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { comfyPage: typeof test.prototype.comfyPage, type: 'inputs' | 'outputs' ): Promise { - return await comfyPage.page.evaluate((slotType) => { - return window['app'].canvas.graph[slotType]?.length || 0 + return await comfyPage.page.evaluate((slotType: 'inputs' | 'outputs') => { + const graph = window.app!.canvas.graph + // isSubgraph check: subgraphs have isRootGraph === false + if (!graph || !('inputNode' in graph)) return 0 + return graph[slotType]?.length || 0 }, type) } @@ -36,7 +39,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { comfyPage: typeof test.prototype.comfyPage ): Promise { return await comfyPage.page.evaluate(() => { - return window['app'].canvas.graph.nodes?.length || 0 + return window.app!.canvas.graph!.nodes?.length || 0 }) } @@ -45,22 +48,22 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { comfyPage: typeof test.prototype.comfyPage ): Promise { return await comfyPage.page.evaluate(() => { - const graph = window['app'].canvas.graph + const graph = window.app!.canvas.graph return graph?.constructor?.name === 'Subgraph' }) } test.describe('I/O Slot Management', () => { test('Can add input slots to subgraph', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/basic-subgraph') + await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph') - const subgraphNode = await comfyPage.getNodeRefById('2') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2') await subgraphNode.navigateIntoSubgraph() const initialCount = await getSubgraphSlotCount(comfyPage, 'inputs') - const vaeEncodeNode = await comfyPage.getNodeRefById('2') + const vaeEncodeNode = await comfyPage.nodeOps.getNodeRefById('2') - await comfyPage.connectFromSubgraphInput(vaeEncodeNode, 0) + await comfyPage.subgraph.connectFromInput(vaeEncodeNode, 0) await comfyPage.nextFrame() const finalCount = await getSubgraphSlotCount(comfyPage, 'inputs') @@ -68,15 +71,15 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { }) test('Can add output slots to subgraph', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/basic-subgraph') + await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph') - const subgraphNode = await comfyPage.getNodeRefById('2') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2') await subgraphNode.navigateIntoSubgraph() const initialCount = await getSubgraphSlotCount(comfyPage, 'outputs') - const vaeEncodeNode = await comfyPage.getNodeRefById('2') + const vaeEncodeNode = await comfyPage.nodeOps.getNodeRefById('2') - await comfyPage.connectToSubgraphOutput(vaeEncodeNode, 0) + await comfyPage.subgraph.connectToOutput(vaeEncodeNode, 0) await comfyPage.nextFrame() const finalCount = await getSubgraphSlotCount(comfyPage, 'outputs') @@ -84,16 +87,17 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { }) test('Can remove input slots from subgraph', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/basic-subgraph') + await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph') - const subgraphNode = await comfyPage.getNodeRefById('2') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2') await subgraphNode.navigateIntoSubgraph() const initialCount = await getSubgraphSlotCount(comfyPage, 'inputs') expect(initialCount).toBeGreaterThan(0) - await comfyPage.rightClickSubgraphInputSlot() - await comfyPage.clickLitegraphContextMenuItem('Remove Slot') + await comfyPage.subgraph.rightClickInputSlot() + await comfyPage.contextMenu.clickLitegraphMenuItem('Remove Slot') + await comfyPage.nextFrame() // Force re-render await comfyPage.canvas.click({ position: { x: 100, y: 100 } }) @@ -104,16 +108,17 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { }) test('Can remove output slots from subgraph', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/basic-subgraph') + await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph') - const subgraphNode = await comfyPage.getNodeRefById('2') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2') await subgraphNode.navigateIntoSubgraph() const initialCount = await getSubgraphSlotCount(comfyPage, 'outputs') expect(initialCount).toBeGreaterThan(0) - await comfyPage.rightClickSubgraphOutputSlot() - await comfyPage.clickLitegraphContextMenuItem('Remove Slot') + await comfyPage.subgraph.rightClickOutputSlot() + await comfyPage.contextMenu.clickLitegraphMenuItem('Remove Slot') + await comfyPage.nextFrame() // Force re-render await comfyPage.canvas.click({ position: { x: 100, y: 100 } }) @@ -124,18 +129,20 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { }) test('Can rename I/O slots', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/basic-subgraph') + await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph') - const subgraphNode = await comfyPage.getNodeRefById('2') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2') await subgraphNode.navigateIntoSubgraph() const initialInputLabel = await comfyPage.page.evaluate(() => { - const graph = window['app'].canvas.graph + const graph = window.app!.canvas.graph + if (!graph || !('inputNode' in graph)) return null return graph.inputs?.[0]?.label || null }) - await comfyPage.rightClickSubgraphInputSlot(initialInputLabel) - await comfyPage.clickLitegraphContextMenuItem('Rename Slot') + await comfyPage.subgraph.rightClickInputSlot(initialInputLabel!) + await comfyPage.contextMenu.clickLitegraphMenuItem('Rename Slot') + await comfyPage.nextFrame() await comfyPage.page.waitForSelector(SELECTORS.promptDialog, { state: 'visible' @@ -148,7 +155,8 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { await comfyPage.nextFrame() const newInputName = await comfyPage.page.evaluate(() => { - const graph = window['app'].canvas.graph + const graph = window.app!.canvas.graph + if (!graph || !('inputNode' in graph)) return null return graph.inputs?.[0]?.label || null }) @@ -157,17 +165,18 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { }) test('Can rename input slots via double-click', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/basic-subgraph') + await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph') - const subgraphNode = await comfyPage.getNodeRefById('2') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2') await subgraphNode.navigateIntoSubgraph() const initialInputLabel = await comfyPage.page.evaluate(() => { - const graph = window['app'].canvas.graph + const graph = window.app!.canvas.graph + if (!graph || !('inputNode' in graph)) return null return graph.inputs?.[0]?.label || null }) - await comfyPage.doubleClickSubgraphInputSlot(initialInputLabel) + await comfyPage.subgraph.doubleClickInputSlot(initialInputLabel!) await comfyPage.page.waitForSelector(SELECTORS.promptDialog, { state: 'visible' @@ -180,7 +189,8 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { await comfyPage.nextFrame() const newInputName = await comfyPage.page.evaluate(() => { - const graph = window['app'].canvas.graph + const graph = window.app!.canvas.graph + if (!graph || !('inputNode' in graph)) return null return graph.inputs?.[0]?.label || null }) @@ -189,17 +199,18 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { }) test('Can rename output slots via double-click', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/basic-subgraph') + await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph') - const subgraphNode = await comfyPage.getNodeRefById('2') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2') await subgraphNode.navigateIntoSubgraph() const initialOutputLabel = await comfyPage.page.evaluate(() => { - const graph = window['app'].canvas.graph + const graph = window.app!.canvas.graph + if (!graph || !('inputNode' in graph)) return null return graph.outputs?.[0]?.label || null }) - await comfyPage.doubleClickSubgraphOutputSlot(initialOutputLabel) + await comfyPage.subgraph.doubleClickOutputSlot(initialOutputLabel!) await comfyPage.page.waitForSelector(SELECTORS.promptDialog, { state: 'visible' @@ -213,7 +224,8 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { await comfyPage.nextFrame() const newOutputName = await comfyPage.page.evaluate(() => { - const graph = window['app'].canvas.graph + const graph = window.app!.canvas.graph + if (!graph || !('inputNode' in graph)) return null return graph.outputs?.[0]?.label || null }) @@ -224,19 +236,21 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { test('Right-click context menu still works alongside double-click', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/basic-subgraph') + await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph') - const subgraphNode = await comfyPage.getNodeRefById('2') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2') await subgraphNode.navigateIntoSubgraph() const initialInputLabel = await comfyPage.page.evaluate(() => { - const graph = window['app'].canvas.graph + const graph = window.app!.canvas.graph + if (!graph || !('inputNode' in graph)) return null return graph.inputs?.[0]?.label || null }) // Test that right-click still works for renaming - await comfyPage.rightClickSubgraphInputSlot(initialInputLabel) - await comfyPage.clickLitegraphContextMenuItem('Rename Slot') + await comfyPage.subgraph.rightClickInputSlot(initialInputLabel!) + await comfyPage.contextMenu.clickLitegraphMenuItem('Rename Slot') + await comfyPage.nextFrame() await comfyPage.page.waitForSelector(SELECTORS.promptDialog, { state: 'visible' @@ -250,7 +264,8 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { await comfyPage.nextFrame() const newInputName = await comfyPage.page.evaluate(() => { - const graph = window['app'].canvas.graph + const graph = window.app!.canvas.graph + if (!graph || !('inputNode' in graph)) return null return graph.inputs?.[0]?.label || null }) @@ -261,20 +276,25 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { test('Can double-click on slot label text to rename', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/basic-subgraph') + await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph') - const subgraphNode = await comfyPage.getNodeRefById('2') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2') await subgraphNode.navigateIntoSubgraph() const initialInputLabel = await comfyPage.page.evaluate(() => { - const graph = window['app'].canvas.graph + const graph = window.app!.canvas.graph + if (!graph || !('inputNode' in graph)) return null return graph.inputs?.[0]?.label || null }) // Use direct pointer event approach to double-click on label await comfyPage.page.evaluate(() => { - const app = window['app'] + const app = window.app! + const graph = app.canvas.graph + if (!graph || !('inputNode' in graph)) { + throw new Error('Expected to be in subgraph') + } const input = graph.inputs?.[0] if (!input?.labelPos) { @@ -285,13 +305,15 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { const testX = input.labelPos[0] const testY = input.labelPos[1] + // Create a minimal mock event with required properties + // Full PointerEvent creation is unnecessary for this test const leftClickEvent = { canvasX: testX, canvasY: testY, - button: 0, // Left mouse button + button: 0, preventDefault: () => {}, stopPropagation: () => {} - } + } as Parameters[0] const inputNode = graph.inputNode if (inputNode?.onPointerDown) { @@ -322,7 +344,8 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { await comfyPage.nextFrame() const newInputName = await comfyPage.page.evaluate(() => { - const graph = window['app'].canvas.graph + const graph = window.app!.canvas.graph + if (!graph || !('inputNode' in graph)) return null return graph.inputs?.[0]?.label || null }) @@ -332,9 +355,11 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { test('Can create widget from link with compressed target_slot', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/subgraph-compressed-target-slot') + await comfyPage.workflow.loadWorkflow( + 'subgraphs/subgraph-compressed-target-slot' + ) const step = await comfyPage.page.evaluate(() => { - return window['app'].graph.nodes[0].widgets[0].options.step + return window.app!.graph!.nodes[0].widgets![0].options.step }) expect(step).toBe(10) }) @@ -342,19 +367,17 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { test.describe('Subgraph Creation and Deletion', () => { test('Can create subgraph from selected nodes', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('default') + await comfyPage.workflow.loadWorkflow('default') - const initialNodeCount = await getGraphNodeCount(comfyPage) - - await comfyPage.ctrlA() + await comfyPage.keyboard.selectAll() await comfyPage.nextFrame() - const node = await comfyPage.getNodeRefById('5') + const node = await comfyPage.nodeOps.getNodeRefById('5') await node.convertToSubgraph() await comfyPage.nextFrame() const subgraphNodes = - await comfyPage.getNodeRefsByTitle(NEW_SUBGRAPH_TITLE) + await comfyPage.nodeOps.getNodeRefsByTitle(NEW_SUBGRAPH_TITLE) expect(subgraphNodes.length).toBe(1) const finalNodeCount = await getGraphNodeCount(comfyPage) @@ -362,9 +385,9 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { }) test('Can delete subgraph node', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/basic-subgraph') + await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph') - const subgraphNode = await comfyPage.getNodeRefById('2') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2') expect(await subgraphNode.exists()).toBe(true) const initialNodeCount = await getGraphNodeCount(comfyPage) @@ -376,7 +399,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { const finalNodeCount = await getGraphNodeCount(comfyPage) expect(finalNodeCount).toBe(initialNodeCount - 1) - const deletedNode = await comfyPage.getNodeRefById('2') + const deletedNode = await comfyPage.nodeOps.getNodeRefById('2') expect(await deletedNode.exists()).toBe(false) }) @@ -384,9 +407,9 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { test('Can copy subgraph node by dragging + alt', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/basic-subgraph') + await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph') - const subgraphNode = await comfyPage.getNodeRefById('2') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2') // Get position of subgraph node const subgraphPos = await subgraphNode.getPosition() @@ -404,7 +427,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { // Find all subgraph nodes const subgraphNodes = - await comfyPage.getNodeRefsByTitle(NEW_SUBGRAPH_TITLE) + await comfyPage.nodeOps.getNodeRefsByTitle(NEW_SUBGRAPH_TITLE) // Expect a second subgraph node to be created (2 total) expect(subgraphNodes.length).toBe(2) @@ -413,9 +436,9 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { test('Copying subgraph node by dragging + alt creates a new subgraph node with unique type', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/basic-subgraph') + await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph') - const subgraphNode = await comfyPage.getNodeRefById('2') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2') // Get position of subgraph node const subgraphPos = await subgraphNode.getPosition() @@ -433,7 +456,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { // Find all subgraph nodes and expect all unique IDs const subgraphNodes = - await comfyPage.getNodeRefsByTitle(NEW_SUBGRAPH_TITLE) + await comfyPage.nodeOps.getNodeRefsByTitle(NEW_SUBGRAPH_TITLE) // Expect the second subgraph node to have a unique type const nodeType1 = await subgraphNodes[0].getType() @@ -445,21 +468,21 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { test.describe('Operations Inside Subgraphs', () => { test('Can copy and paste nodes in subgraph', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/basic-subgraph') + await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph') - const subgraphNode = await comfyPage.getNodeRefById('2') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2') await subgraphNode.navigateIntoSubgraph() const initialNodeCount = await getGraphNodeCount(comfyPage) const nodesInSubgraph = await comfyPage.page.evaluate(() => { - const nodes = window['app'].canvas.graph.nodes + const nodes = window.app!.canvas.graph!.nodes return nodes?.[0]?.id || null }) expect(nodesInSubgraph).not.toBeNull() - const nodeToClone = await comfyPage.getNodeRefById( + const nodeToClone = await comfyPage.nodeOps.getNodeRefById( String(nodesInSubgraph) ) await nodeToClone.click('title') @@ -476,13 +499,13 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { }) test('Can undo and redo operations in subgraph', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/basic-subgraph') + await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph') - const subgraphNode = await comfyPage.getNodeRefById('2') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2') await subgraphNode.navigateIntoSubgraph() // Add a node - await comfyPage.doubleClickCanvas() + await comfyPage.canvasOps.doubleClick() await comfyPage.searchBox.fillAndSelectFirstNode('Note') await comfyPage.nextFrame() @@ -490,14 +513,14 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { const initialCount = await getGraphNodeCount(comfyPage) // Undo - await comfyPage.ctrlZ() + await comfyPage.keyboard.undo() await comfyPage.nextFrame() const afterUndoCount = await getGraphNodeCount(comfyPage) expect(afterUndoCount).toBe(initialCount - 1) // Redo - await comfyPage.ctrlY() + await comfyPage.keyboard.redo() await comfyPage.nextFrame() const afterRedoCount = await getGraphNodeCount(comfyPage) @@ -507,16 +530,16 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { test.describe('Subgraph Navigation and UI', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') }) test('Breadcrumb updates when subgraph node title is changed', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/nested-subgraph') + await comfyPage.workflow.loadWorkflow('subgraphs/nested-subgraph') await comfyPage.nextFrame() - const subgraphNode = await comfyPage.getNodeRefById('10') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('10') const nodePos = await subgraphNode.getPosition() const nodeSize = await subgraphNode.getSize() @@ -565,7 +588,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { test('DOM widget visibility persists through subgraph navigation', async ({ comfyPage }) => { - await comfyPage.loadWorkflow( + await comfyPage.workflow.loadWorkflow( 'subgraphs/subgraph-with-promoted-text-widget' ) await comfyPage.nextFrame() @@ -575,7 +598,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { await expect(parentTextarea).toBeVisible() await expect(parentTextarea).toHaveCount(1) - const subgraphNode = await comfyPage.getNodeRefById('11') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('11') expect(await subgraphNode.exists()).toBe(true) await subgraphNode.navigateIntoSubgraph() @@ -598,14 +621,14 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { test('DOM widget content is preserved through navigation', async ({ comfyPage }) => { - await comfyPage.loadWorkflow( + await comfyPage.workflow.loadWorkflow( 'subgraphs/subgraph-with-promoted-text-widget' ) const textarea = comfyPage.page.locator(SELECTORS.domWidget) await textarea.fill(TEST_WIDGET_CONTENT) - const subgraphNode = await comfyPage.getNodeRefById('11') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('11') await subgraphNode.navigateIntoSubgraph() const subgraphTextarea = comfyPage.page.locator(SELECTORS.domWidget) @@ -621,7 +644,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { test('DOM elements are cleaned up when subgraph node is removed', async ({ comfyPage }) => { - await comfyPage.loadWorkflow( + await comfyPage.workflow.loadWorkflow( 'subgraphs/subgraph-with-promoted-text-widget' ) @@ -630,7 +653,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { .count() expect(initialCount).toBe(1) - const subgraphNode = await comfyPage.getNodeRefById('11') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('11') await subgraphNode.click('title') await comfyPage.page.keyboard.press('Delete') @@ -646,23 +669,24 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { comfyPage }) => { // Enable new menu for breadcrumb navigation - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') const workflowName = 'subgraphs/subgraph-with-promoted-text-widget' - await comfyPage.loadWorkflow(workflowName) + await comfyPage.workflow.loadWorkflow(workflowName) const textareaCount = await comfyPage.page .locator(SELECTORS.domWidget) .count() expect(textareaCount).toBe(1) - const subgraphNode = await comfyPage.getNodeRefById('11') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('11') // Navigate into subgraph (method now handles retries internally) await subgraphNode.navigateIntoSubgraph() - await comfyPage.rightClickSubgraphInputSlot('text') - await comfyPage.clickLitegraphContextMenuItem('Remove Slot') + await comfyPage.subgraph.rightClickInputSlot('text') + await comfyPage.contextMenu.clickLitegraphMenuItem('Remove Slot') + await comfyPage.nextFrame() // Wait for breadcrumb to be visible await comfyPage.page.waitForSelector(SELECTORS.breadcrumb, { @@ -682,7 +706,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { // Check that the subgraph node has no widgets after removing the text slot const widgetCount = await comfyPage.page.evaluate(() => { - return window['app'].canvas.graph.nodes[0].widgets?.length || 0 + return window.app!.canvas.graph!.nodes[0].widgets?.length || 0 }) expect(widgetCount).toBe(0) @@ -691,7 +715,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { test('Multiple promoted widgets are handled correctly', async ({ comfyPage }) => { - await comfyPage.loadWorkflow( + await comfyPage.workflow.loadWorkflow( 'subgraphs/subgraph-with-multiple-promoted-widgets' ) @@ -700,7 +724,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { .count() expect(parentCount).toBeGreaterThan(1) - const subgraphNode = await comfyPage.getNodeRefById('11') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('11') await subgraphNode.navigateIntoSubgraph() const subgraphCount = await comfyPage.page @@ -720,15 +744,15 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { test.describe('Navigation Hotkeys', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') }) test('Navigation hotkey can be customized', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/basic-subgraph') + await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph') await comfyPage.nextFrame() // Change the Exit Subgraph keybinding from Escape to Alt+Q - await comfyPage.setSetting('Comfy.Keybinding.NewBindings', [ + await comfyPage.settings.setSetting('Comfy.Keybinding.NewBindings', [ { commandId: 'Comfy.Graph.ExitSubgraph', combo: { @@ -740,7 +764,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { } ]) - await comfyPage.setSetting('Comfy.Keybinding.UnsetBindings', [ + await comfyPage.settings.setSetting('Comfy.Keybinding.UnsetBindings', [ { commandId: 'Comfy.Graph.ExitSubgraph', combo: { @@ -754,10 +778,12 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { // Reload the page await comfyPage.page.reload() - await comfyPage.page.waitForTimeout(1024) + await comfyPage.setup() + await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph') + await comfyPage.nextFrame() // Navigate into subgraph - const subgraphNode = await comfyPage.getNodeRefById('2') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2') await subgraphNode.navigateIntoSubgraph() await comfyPage.page.waitForSelector(SELECTORS.breadcrumb) @@ -780,10 +806,10 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { test('Escape prioritizes closing dialogs over exiting subgraph', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/basic-subgraph') + await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph') await comfyPage.nextFrame() - const subgraphNode = await comfyPage.getNodeRefById('2') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2') await subgraphNode.navigateIntoSubgraph() await comfyPage.page.waitForSelector(SELECTORS.breadcrumb) diff --git a/browser_tests/tests/templates.spec.ts b/browser_tests/tests/templates.spec.ts index 334c761af..54f6360aa 100644 --- a/browser_tests/tests/templates.spec.ts +++ b/browser_tests/tests/templates.spec.ts @@ -15,8 +15,11 @@ async function checkTemplateFileExists( test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') - await comfyPage.setSetting('Comfy.Workflow.ShowMissingModelsWarning', false) + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting( + 'Comfy.Workflow.ShowMissingModelsWarning', + false + ) }) test('should have a JSON workflow file for each template', async ({ @@ -72,13 +75,13 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => { test('Can load template workflows', async ({ comfyPage }) => { // Clear the workflow await comfyPage.menu.workflowsTab.open() - await comfyPage.executeCommand('Comfy.NewBlankWorkflow') + await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow') await expect(async () => { - expect(await comfyPage.getGraphNodesCount()).toBe(0) + expect(await comfyPage.nodeOps.getGraphNodesCount()).toBe(0) }).toPass({ timeout: 250 }) // Load a template - await comfyPage.executeCommand('Comfy.BrowseTemplates') + await comfyPage.command.executeCommand('Comfy.BrowseTemplates') await expect(comfyPage.templates.content).toBeVisible() await comfyPage.page @@ -89,7 +92,7 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => { // Ensure we now have some nodes await expect(async () => { - expect(await comfyPage.getGraphNodesCount()).toBeGreaterThan(0) + expect(await comfyPage.nodeOps.getGraphNodesCount()).toBeGreaterThan(0) }).toPass({ timeout: 250 }) }) @@ -97,7 +100,7 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => { comfyPage }) => { // Set the tutorial as not completed to mark the user as a first-time user - await comfyPage.setSetting('Comfy.TutorialCompleted', false) + await comfyPage.settings.setSetting('Comfy.TutorialCompleted', false) // Load the page await comfyPage.setup({ clearStorage: true }) @@ -107,9 +110,9 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => { }) test('Uses proper locale files for templates', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Locale', 'fr') + await comfyPage.settings.setSetting('Comfy.Locale', 'fr') - await comfyPage.executeCommand('Comfy.BrowseTemplates') + await comfyPage.command.executeCommand('Comfy.BrowseTemplates') const dialog = comfyPage.page.getByRole('dialog').filter({ has: comfyPage.page.getByRole('heading', { name: 'Modèles', exact: true }) @@ -134,7 +137,7 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => { comfyPage }) => { // Set locale to a language that doesn't have a template file - await comfyPage.setSetting('Comfy.Locale', 'de') // German - no index.de.json exists + await comfyPage.settings.setSetting('Comfy.Locale', 'de') // German - no index.de.json exists // Wait for the German request (expected to 404) const germanRequestPromise = comfyPage.page.waitForRequest( @@ -161,7 +164,7 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => { ) // Load the templates dialog - await comfyPage.executeCommand('Comfy.BrowseTemplates') + await comfyPage.command.executeCommand('Comfy.BrowseTemplates') await expect(comfyPage.templates.content).toBeVisible() // Verify German was requested first, then English as fallback @@ -181,7 +184,7 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => { comfyPage }) => { // Open templates dialog - await comfyPage.executeCommand('Comfy.BrowseTemplates') + await comfyPage.command.executeCommand('Comfy.BrowseTemplates') await comfyPage.templates.content.waitFor({ state: 'visible' }) const templateGrid = comfyPage.page.locator( @@ -189,20 +192,20 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => { ) const nav = comfyPage.page.locator('header', { hasText: 'Templates' }) - await comfyPage.templates.waitForMinimumCardCount(1) + await comfyPage.templates.expectMinimumCardCount(1) await expect(templateGrid).toBeVisible() await expect(nav).toBeVisible() // Nav should be visible at desktop size const mobileSize = { width: 640, height: 800 } await comfyPage.page.setViewportSize(mobileSize) - await comfyPage.templates.waitForMinimumCardCount(1) + await comfyPage.templates.expectMinimumCardCount(1) await expect(templateGrid).toBeVisible() // Nav header is clipped by overflow-hidden parent at mobile size await expect(nav).not.toBeInViewport() const tabletSize = { width: 1024, height: 800 } await comfyPage.page.setViewportSize(tabletSize) - await comfyPage.templates.waitForMinimumCardCount(1) + await comfyPage.templates.expectMinimumCardCount(1) await expect(templateGrid).toBeVisible() await expect(nav).toBeVisible() // Nav should be visible at tablet size }) @@ -272,7 +275,7 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => { }) // Open templates dialog - await comfyPage.executeCommand('Comfy.BrowseTemplates') + await comfyPage.command.executeCommand('Comfy.BrowseTemplates') await expect(comfyPage.templates.content).toBeVisible() // Wait for cards to load diff --git a/browser_tests/tests/useSettingSearch.spec.ts b/browser_tests/tests/useSettingSearch.spec.ts index aa3481408..43ddd09e7 100644 --- a/browser_tests/tests/useSettingSearch.spec.ts +++ b/browser_tests/tests/useSettingSearch.spec.ts @@ -1,27 +1,37 @@ import { expect } from '@playwright/test' +import type { Settings } from '../../src/schemas/apiSchema' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +/** + * Type helper for test settings with arbitrary IDs. + * Extensions can register settings with any ID, but SettingParams.id + * is typed as keyof Settings for autocomplete. + */ +type TestSettingId = keyof Settings + test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Settings Search functionality', { tag: '@settings' }, () => { test.beforeEach(async ({ comfyPage }) => { // Register test settings to verify hidden/deprecated filtering await comfyPage.page.evaluate(() => { - window['app'].registerExtension({ + window.app!.registerExtension({ name: 'TestSettingsExtension', settings: [ { - id: 'TestHiddenSetting', + // Extensions can register arbitrary setting IDs + id: 'TestHiddenSetting' as TestSettingId, name: 'Test Hidden Setting', type: 'hidden', defaultValue: 'hidden_value', category: ['Test', 'Hidden'] }, { - id: 'TestDeprecatedSetting', + // Extensions can register arbitrary setting IDs + id: 'TestDeprecatedSetting' as TestSettingId, name: 'Test Deprecated Setting', type: 'text', defaultValue: 'deprecated_value', @@ -29,7 +39,8 @@ test.describe('Settings Search functionality', { tag: '@settings' }, () => { category: ['Test', 'Deprecated'] }, { - id: 'TestVisibleSetting', + // Extensions can register arbitrary setting IDs + id: 'TestVisibleSetting' as TestSettingId, name: 'Test Visible Setting', type: 'text', defaultValue: 'visible_value', @@ -109,19 +120,14 @@ test.describe('Settings Search functionality', { tag: '@settings' }, () => { const settingsDialog = comfyPage.page.locator('.settings-container') await expect(settingsDialog).toBeVisible() - // Get categories and click on different ones - const categories = comfyPage.page.locator( - '.settings-sidebar .p-listbox-option' - ) - const categoryCount = await categories.count() + // Click on a specific category (Appearance) to verify category switching + const appearanceCategory = comfyPage.page.getByRole('option', { + name: 'Appearance' + }) + await appearanceCategory.click() - if (categoryCount > 1) { - // Click on the second category - await categories.nth(1).click() - - // Verify the category is selected - await expect(categories.nth(1)).toHaveClass(/p-listbox-option-selected/) - } + // Verify the category is selected + await expect(appearanceCategory).toHaveClass(/p-listbox-option-selected/) }) test('settings content area is visible', async ({ comfyPage }) => { diff --git a/browser_tests/tests/versionMismatchWarnings.spec.ts b/browser_tests/tests/versionMismatchWarnings.spec.ts index 15c55873f..6c0a89034 100644 --- a/browser_tests/tests/versionMismatchWarnings.spec.ts +++ b/browser_tests/tests/versionMismatchWarnings.spec.ts @@ -37,8 +37,8 @@ test.describe('Version Mismatch Warnings', { tag: '@slow' }, () => { } test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') - await comfyPage.setSetting( + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting( 'Comfy.VersionCompatibility.DisableWarnings', false ) @@ -103,10 +103,9 @@ test.describe('Version Mismatch Warnings', { tag: '@slow' }, () => { await comfyPage.setup() // Locate the warning toast and dismiss it - const warningToast = comfyPage.page - .locator('div') - .filter({ hasText: 'Version Compatibility' }) - .nth(3) + const warningToast = comfyPage.page.locator('.p-toast-message').filter({ + hasText: 'Version Compatibility' + }) await warningToast.waitFor({ state: 'visible' }) const dismissButton = warningToast.getByRole('button', { name: 'Close' }) await dismissButton.click() diff --git a/browser_tests/tests/viewport.spec.ts b/browser_tests/tests/viewport.spec.ts index c0d65ca38..b7320ee0e 100644 --- a/browser_tests/tests/viewport.spec.ts +++ b/browser_tests/tests/viewport.spec.ts @@ -6,7 +6,9 @@ test.describe('Viewport', { tag: ['@screenshot', '@smoke', '@canvas'] }, () => { test('Fits view to nodes when saved viewport position is offscreen', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('viewport/default-viewport-saved-offscreen') + await comfyPage.workflow.loadWorkflow( + 'viewport/default-viewport-saved-offscreen' + ) // Wait a few frames for rendering to stabilize for (let i = 0; i < 5; i++) { diff --git a/browser_tests/tests/viewport.spec.ts-snapshots/viewport-fits-when-saved-offscreen-chromium-linux.png b/browser_tests/tests/viewport.spec.ts-snapshots/viewport-fits-when-saved-offscreen-chromium-linux.png index 1afdb11a2..f6ac2afd0 100644 Binary files a/browser_tests/tests/viewport.spec.ts-snapshots/viewport-fits-when-saved-offscreen-chromium-linux.png and b/browser_tests/tests/viewport.spec.ts-snapshots/viewport-fits-when-saved-offscreen-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/groups/groups.spec.ts b/browser_tests/tests/vueNodes/groups/groups.spec.ts index 19404f7d4..5f691bbee 100644 --- a/browser_tests/tests/vueNodes/groups/groups.spec.ts +++ b/browser_tests/tests/vueNodes/groups/groups.spec.ts @@ -7,8 +7,8 @@ const CREATE_GROUP_HOTKEY = 'Control+g' test.describe('Vue Node Groups', { tag: '@screenshot' }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) - await comfyPage.setSetting('Comfy.Minimap.ShowGroups', true) + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.settings.setSetting('Comfy.Minimap.ShowGroups', true) await comfyPage.vueNodes.waitForNodes() }) @@ -24,9 +24,9 @@ test.describe('Vue Node Groups', { tag: '@screenshot' }, () => { test('should allow fitting group to contents', async ({ comfyPage }) => { await comfyPage.setup() - await comfyPage.loadWorkflow('groups/oversized_group') - await comfyPage.ctrlA() - await comfyPage.executeCommand('Comfy.Graph.FitGroupToContents') + await comfyPage.workflow.loadWorkflow('groups/oversized_group') + await comfyPage.keyboard.selectAll() + await comfyPage.command.executeCommand('Comfy.Graph.FitGroupToContents') await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot( 'vue-groups-fit-to-contents.png' @@ -36,18 +36,20 @@ test.describe('Vue Node Groups', { tag: '@screenshot' }, () => { test('should move nested groups together when dragging outer group', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('groups/nested-groups-1-inner-node') + await comfyPage.workflow.loadWorkflow('groups/nested-groups-1-inner-node') // Get initial positions with null guards - const outerInitial = await comfyPage.getGroupPosition('Outer Group') - const innerInitial = await comfyPage.getGroupPosition('Inner Group') + const outerInitial = + await comfyPage.canvasOps.getGroupPosition('Outer Group') + const innerInitial = + await comfyPage.canvasOps.getGroupPosition('Inner Group') const initialOffsetX = innerInitial.x - outerInitial.x const initialOffsetY = innerInitial.y - outerInitial.y // Drag the outer group const dragDelta = { x: 100, y: 80 } - await comfyPage.dragGroup({ + await comfyPage.canvasOps.dragGroup({ name: 'Outer Group', deltaX: dragDelta.x, deltaY: dragDelta.y @@ -55,8 +57,10 @@ test.describe('Vue Node Groups', { tag: '@screenshot' }, () => { // Use retrying assertion to wait for positions to update await expect(async () => { - const outerFinal = await comfyPage.getGroupPosition('Outer Group') - const innerFinal = await comfyPage.getGroupPosition('Inner Group') + const outerFinal = + await comfyPage.canvasOps.getGroupPosition('Outer Group') + const innerFinal = + await comfyPage.canvasOps.getGroupPosition('Inner Group') const finalOffsetX = innerFinal.x - outerFinal.x const finalOffsetY = innerFinal.y - outerFinal.y diff --git a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png index 298a09d66..ca4ed5019 100644 Binary files a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png and b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png index 7b20ddbeb..eb2918a1e 100644 Binary files a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png and b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts b/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts index ed18ab367..c7f5ce92c 100644 --- a/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts +++ b/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts @@ -5,7 +5,7 @@ import { test.describe('Vue Nodes Canvas Pan', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.vueNodes.waitForNodes() }) @@ -13,7 +13,10 @@ test.describe('Vue Nodes Canvas Pan', () => { '@mobile Can pan with touch', { tag: '@screenshot' }, async ({ comfyPage }) => { - await comfyPage.panWithTouch({ x: 64, y: 64 }, { x: 256, y: 256 }) + await comfyPage.canvasOps.panWithTouch( + { x: 64, y: 64 }, + { x: 256, y: 256 } + ) await expect(comfyPage.canvas).toHaveScreenshot( 'vue-nodes-paned-with-touch.png' ) diff --git a/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts-snapshots/vue-nodes-paned-with-touch-mobile-chrome-linux.png b/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts-snapshots/vue-nodes-paned-with-touch-mobile-chrome-linux.png index 915fb36d9..6a98eafc4 100644 Binary files a/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts-snapshots/vue-nodes-paned-with-touch-mobile-chrome-linux.png and b/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts-snapshots/vue-nodes-paned-with-touch-mobile-chrome-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts b/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts index 0b064dd81..e391c289d 100644 --- a/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts +++ b/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts @@ -5,8 +5,8 @@ import { test.describe('Vue Nodes Zoom', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) - await comfyPage.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 8) + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.settings.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 8) await comfyPage.vueNodes.waitForNodes() }) @@ -26,7 +26,7 @@ test.describe('Vue Nodes Zoom', () => { // the node. The node should not capture the drag while drag-zooming. await comfyPage.page.keyboard.down('Control') await comfyPage.page.keyboard.down('Shift') - await comfyPage.dragAndDrop( + await comfyPage.canvasOps.dragAndDrop( { x: 200, y: 300 }, { x: nodeMidpointX, y: nodeMidpointY } ) diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts index b22baf5b1..ab41c2785 100644 --- a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts +++ b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts @@ -25,7 +25,7 @@ async function getInputLinkDetails( ) { return await page.evaluate( ([targetNodeId, targetSlot]) => { - const app = window['app'] + const app = window.app const graph = app?.canvas?.graph ?? app?.graph if (!graph) return null @@ -100,10 +100,10 @@ async function connectSlots( test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) // await comfyPage.setup() - await comfyPage.loadWorkflow('vueNodes/simple-triple') + await comfyPage.workflow.loadWorkflow('vueNodes/simple-triple') await comfyPage.vueNodes.waitForNodes() await fitToViewInstant(comfyPage) }) @@ -112,7 +112,9 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { comfyPage, comfyMouse }) => { - const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0] + const samplerNode = ( + await comfyPage.nodeOps.getNodeRefsByType('KSampler') + )[0] expect(samplerNode).toBeTruthy() const slot = slotLocator(comfyPage.page, samplerNode.id, 0, false) @@ -142,8 +144,10 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { test('should create a link when dropping on a compatible slot', async ({ comfyPage }) => { - const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0] - const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0] + const samplerNode = ( + await comfyPage.nodeOps.getNodeRefsByType('KSampler') + )[0] + const vaeNode = (await comfyPage.nodeOps.getNodeRefsByType('VAEDecode'))[0] expect(samplerNode && vaeNode).toBeTruthy() const samplerOutput = await samplerNode.getOutput(0) @@ -172,8 +176,12 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { test('should not create a link when slot types are incompatible', async ({ comfyPage }) => { - const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0] - const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0] + const samplerNode = ( + await comfyPage.nodeOps.getNodeRefsByType('KSampler') + )[0] + const clipNode = ( + await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode') + )[0] expect(samplerNode && clipNode).toBeTruthy() const samplerOutput = await samplerNode.getOutput(0) @@ -200,7 +208,9 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { test('should not create a link when dropping onto a slot on the same node', async ({ comfyPage }) => { - const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0] + const samplerNode = ( + await comfyPage.nodeOps.getNodeRefsByType('KSampler') + )[0] expect(samplerNode).toBeTruthy() const samplerOutput = await samplerNode.getOutput(0) @@ -221,8 +231,10 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { comfyPage, comfyMouse }) => { - const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0] - const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0] + const samplerNode = ( + await comfyPage.nodeOps.getNodeRefsByType('KSampler') + )[0] + const vaeNode = (await comfyPage.nodeOps.getNodeRefsByType('VAEDecode'))[0] expect(samplerNode && vaeNode).toBeTruthy() const samplerOutputCenter = await getSlotCenter( comfyPage.page, @@ -258,8 +270,10 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { comfyPage, comfyMouse }) => { - const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0] - const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0] + const samplerNode = ( + await comfyPage.nodeOps.getNodeRefsByType('KSampler') + )[0] + const vaeNode = (await comfyPage.nodeOps.getNodeRefsByType('VAEDecode'))[0] expect(samplerNode && vaeNode).toBeTruthy() const samplerOutput = await samplerNode.getOutput(0) @@ -315,8 +329,10 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { comfyPage, comfyMouse }) => { - const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0] - const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0] + const samplerNode = ( + await comfyPage.nodeOps.getNodeRefsByType('KSampler') + )[0] + const vaeNode = (await comfyPage.nodeOps.getNodeRefsByType('VAEDecode'))[0] expect(samplerNode && vaeNode).toBeTruthy() const samplerOutput = await samplerNode.getOutput(0) @@ -398,8 +414,10 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { comfyPage, comfyMouse }) => { - const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0] - const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0] + const samplerNode = ( + await comfyPage.nodeOps.getNodeRefsByType('KSampler') + )[0] + const vaeNode = (await comfyPage.nodeOps.getNodeRefsByType('VAEDecode'))[0] const samplerOutput = await samplerNode.getOutput(0) const vaeInput = await vaeNode.getInput(0) @@ -419,7 +437,7 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { // This avoids relying on an exact path hit-test position. await comfyPage.page.evaluate( ([targetNodeId, targetSlot, clientPoint]) => { - const app = window['app'] + const app = window.app const graph = app?.canvas?.graph ?? app?.graph if (!graph) throw new Error('Graph not available') const node = graph.getNodeById(targetNodeId) @@ -433,7 +451,7 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { if (!link) throw new Error('Link not found') // Convert the client/canvas pixel coordinates to graph space - const pos = app.canvas.ds.convertCanvasToOffset([ + const pos = app!.canvas.ds.convertCanvasToOffset([ clientPoint.x, clientPoint.y ]) @@ -483,8 +501,10 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { comfyPage, comfyMouse }) => { - const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0] - const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0] + const samplerNode = ( + await comfyPage.nodeOps.getNodeRefsByType('KSampler') + )[0] + const vaeNode = (await comfyPage.nodeOps.getNodeRefsByType('VAEDecode'))[0] expect(samplerNode && vaeNode).toBeTruthy() const samplerOutput = await samplerNode.getOutput(0) @@ -505,7 +525,7 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { // This avoids relying on an exact path hit-test position. await comfyPage.page.evaluate( ([targetNodeId, targetSlot, clientPoint]) => { - const app = window['app'] + const app = window.app const graph = app?.canvas?.graph ?? app?.graph if (!graph) throw new Error('Graph not available') const node = graph.getNodeById(targetNodeId) @@ -519,7 +539,7 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { if (!link) throw new Error('Link not found') // Convert the client/canvas pixel coordinates to graph space - const pos = app.canvas.ds.convertCanvasToOffset([ + const pos = app!.canvas.ds.convertCanvasToOffset([ clientPoint.x, clientPoint.y ]) @@ -572,8 +592,12 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { comfyPage, comfyMouse }) => { - const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0] - const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0] + const clipNode = ( + await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode') + )[0] + const samplerNode = ( + await comfyPage.nodeOps.getNodeRefsByType('KSampler') + )[0] expect(clipNode && samplerNode).toBeTruthy() // Step 1: Connect CLIP's only output (index 0) to KSampler's second input (index 1) @@ -642,8 +666,12 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { comfyPage, comfyMouse }) => { - const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0] - const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0] + const clipNode = ( + await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode') + )[0] + const samplerNode = ( + await comfyPage.nodeOps.getNodeRefsByType('KSampler') + )[0] expect(clipNode && samplerNode).toBeTruthy() const clipOutput = await clipNode.getOutput(0) @@ -697,8 +725,12 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { comfyPage, comfyMouse }) => { - const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0] - const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0] + const clipNode = ( + await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode') + )[0] + const samplerNode = ( + await comfyPage.nodeOps.getNodeRefsByType('KSampler') + )[0] expect(clipNode && samplerNode).toBeTruthy() // Start drag from CLIP output[0] @@ -746,8 +778,12 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { comfyPage, comfyMouse }) => { - const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0] - const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0] + const clipNode = ( + await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode') + )[0] + const samplerNode = ( + await comfyPage.nodeOps.getNodeRefsByType('KSampler') + )[0] expect(clipNode && samplerNode).toBeTruthy() // Drag from CLIP output[0] to KSampler input[2] (third slot) which is the @@ -791,8 +827,12 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { test('should batch disconnect all links with ctrl+alt+click on slot', async ({ comfyPage }) => { - const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0] - const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0] + const clipNode = ( + await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode') + )[0] + const samplerNode = ( + await comfyPage.nodeOps.getNodeRefsByType('KSampler') + )[0] expect(clipNode && samplerNode).toBeTruthy() await connectSlots( @@ -832,12 +872,14 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { comfyPage, comfyMouse }) => { - await comfyPage.setSetting( + await comfyPage.settings.setSetting( 'Comfy.LinkRelease.ActionShift', 'context menu' ) - const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0] + const samplerNode = ( + await comfyPage.nodeOps.getNodeRefsByType('KSampler') + )[0] expect(samplerNode).toBeTruthy() const outputCenter = await getSlotCenter( @@ -864,7 +906,7 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { // Pinned endpoint should not change with mouse movement while menu is open const before = await comfyPage.page.evaluate(() => { - const snap = window['app']?.canvas?.linkConnector?.state?.snapLinksPos + const snap = window.app?.canvas?.linkConnector?.state?.snapLinksPos return Array.isArray(snap) ? [snap[0], snap[1]] : null }) expect(before).not.toBeNull() @@ -872,7 +914,7 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { // Move mouse elsewhere and verify snap position is unchanged await comfyMouse.move({ x: dropPos.x + 160, y: dropPos.y + 100 }) const after = await comfyPage.page.evaluate(() => { - const snap = window['app']?.canvas?.linkConnector?.state?.snapLinksPos + const snap = window.app?.canvas?.linkConnector?.state?.snapLinksPos return Array.isArray(snap) ? [snap[0], snap[1]] : null }) expect(after).toEqual(before) @@ -882,13 +924,15 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { comfyPage, comfyMouse }) => { - await comfyPage.setSetting( + await comfyPage.settings.setSetting( 'Comfy.LinkRelease.ActionShift', 'context menu' ) - await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default') + await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default') - const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0] + const samplerNode = ( + await comfyPage.nodeOps.getNodeRefsByType('KSampler') + )[0] expect(samplerNode).toBeTruthy() const outputCenter = await getSlotCenter( @@ -909,7 +953,8 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { } // Open Search from the context menu - await comfyPage.clickContextMenuItem('Search') + await comfyPage.contextMenu.clickMenuItem('Search') + await comfyPage.nextFrame() // Search box opens with prefilled type filter based on link type (LATENT) await expect(comfyPage.searchBox.input).toBeVisible() @@ -928,7 +973,7 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { expect(await samplerOutput.getLinkCount()).toBe(1) // One of the VAEDecode nodes should have an incoming link on input[0] - const vaeNodes = await comfyPage.getNodeRefsByType('VAEDecode') + const vaeNodes = await comfyPage.nodeOps.getNodeRefsByType('VAEDecode') let linked = false for (const vae of vaeNodes) { const details = await getInputLinkDetails(comfyPage.page, vae.id, 0) @@ -945,9 +990,14 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { comfyPage, comfyMouse }) => { - await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'search box') + await comfyPage.settings.setSetting( + 'Comfy.LinkRelease.ActionShift', + 'search box' + ) - const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0] + const samplerNode = ( + await comfyPage.nodeOps.getNodeRefsByType('KSampler') + )[0] expect(samplerNode).toBeTruthy() const outputCenter = await getSlotCenter( @@ -980,7 +1030,7 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { const samplerOutput = await samplerNode.getOutput(0) expect(await samplerOutput.getLinkCount()).toBe(1) - const vaeNodes = await comfyPage.getNodeRefsByType('VAEDecode') + const vaeNodes = await comfyPage.nodeOps.getNodeRefsByType('VAEDecode') let linked = false for (const vae of vaeNodes) { const details = await getInputLinkDetails(comfyPage.page, vae.id, 0) @@ -999,24 +1049,28 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { comfyMouse }) => { // Setup workflow with a KSampler node - await comfyPage.executeCommand('Comfy.NewBlankWorkflow') - await comfyPage.waitForGraphNodes(0) - await comfyPage.executeCommand('Workspace.SearchBox.Toggle') + await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow') + await comfyPage.nodeOps.waitForGraphNodes(0) + await comfyPage.command.executeCommand('Workspace.SearchBox.Toggle') await comfyPage.nextFrame() await comfyPage.searchBox.fillAndSelectFirstNode('KSampler') - await comfyPage.waitForGraphNodes(1) + await comfyPage.nodeOps.waitForGraphNodes(1) // Convert the KSampler node to a subgraph - let ksamplerNode = (await comfyPage.getNodeRefsByType('KSampler'))?.[0] + let ksamplerNode = ( + await comfyPage.nodeOps.getNodeRefsByType('KSampler') + )?.[0] await comfyPage.vueNodes.selectNode(String(ksamplerNode.id)) - await comfyPage.executeCommand('Comfy.Graph.ConvertToSubgraph') + await comfyPage.command.executeCommand('Comfy.Graph.ConvertToSubgraph') // Enter the subgraph await comfyPage.vueNodes.enterSubgraph() await fitToViewInstant(comfyPage) // Get the KSampler node inside the subgraph - ksamplerNode = (await comfyPage.getNodeRefsByType('KSampler', true))?.[0] + ksamplerNode = ( + await comfyPage.nodeOps.getNodeRefsByType('KSampler', true) + )?.[0] const positiveInput = await ksamplerNode.getInput(1) const negativeInput = await ksamplerNode.getInput(2) @@ -1027,7 +1081,7 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { true ) - const sourceSlot = await comfyPage.getSubgraphInputSlot() + const sourceSlot = await comfyPage.subgraph.getInputSlot() const calculatedSourcePos = await sourceSlot.getOpenSlotPosition() await comfyMouse.move(calculatedSourcePos) diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png index 0485e0302..ec3bbf55c 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png index e5b95630f..1deb01b1e 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png index d47e5ece3..79e507879 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png index e89433357..60fdea045 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png index d108f0803..b7e42f2ae 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png index 0f003c13e..3d7c044e9 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png index 67eaac039..6cf0851ee 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png index 0f55f3a83..0a274b718 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts index 07b23e088..402b31466 100644 --- a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts +++ b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts @@ -7,9 +7,9 @@ import { fitToViewInstant } from '../../../../helpers/fitToView' test.describe('Vue Node Bring to Front', { tag: '@screenshot' }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) - await comfyPage.loadWorkflow('vueNodes/simple-triple') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.workflow.loadWorkflow('vueNodes/simple-triple') await comfyPage.vueNodes.waitForNodes() await fitToViewInstant(comfyPage) }) @@ -61,7 +61,7 @@ test.describe('Vue Node Bring to Front', { tag: '@screenshot' }, () => { if (!ksamplerHeader) throw new Error('KSampler header not found') // Drag KSampler on top of CLIP Text Encode - await comfyPage.dragAndDrop( + await comfyPage.canvasOps.dragAndDrop( { x: ksamplerHeader.x + 50, y: ksamplerHeader.y + 10 }, clipCenter ) @@ -108,7 +108,7 @@ test.describe('Vue Node Bring to Front', { tag: '@screenshot' }, () => { const vaeHeader = await comfyPage.page.getByText('VAE Decode').boundingBox() if (!vaeHeader) throw new Error('VAE Decode header not found') - await comfyPage.dragAndDrop( + await comfyPage.canvasOps.dragAndDrop( { x: vaeHeader.x + 50, y: vaeHeader.y + 10 }, { x: clipCenter.x - 50, y: clipCenter.y } ) diff --git a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-after-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-after-chromium-linux.png index 200f93290..b5b47d924 100644 Binary files a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-after-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-after-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-before-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-before-chromium-linux.png index 19a5538ed..13fd9e7c7 100644 Binary files a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-before-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-before-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-after-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-after-chromium-linux.png index 86a1dac87..b9c6a650c 100644 Binary files a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-after-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-after-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-before-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-before-chromium-linux.png index dc312b134..ff37b84cf 100644 Binary files a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-before-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-before-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts index fd74872e5..42f8557be 100644 --- a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts +++ b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts @@ -7,7 +7,7 @@ import type { Position } from '../../../../fixtures/types' test.describe('Vue Node Moving', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.vueNodes.waitForNodes() }) @@ -35,7 +35,7 @@ test.describe('Vue Node Moving', () => { async ({ comfyPage }) => { const loadCheckpointHeaderPos = await getLoadCheckpointHeaderPos(comfyPage) - await comfyPage.dragAndDrop(loadCheckpointHeaderPos, { + await comfyPage.canvasOps.dragAndDrop(loadCheckpointHeaderPos, { x: 256, y: 256 }) @@ -52,11 +52,11 @@ test.describe('Vue Node Moving', () => { { tag: '@screenshot' }, async ({ comfyPage }) => { // Disable minimap (gets in way of the node on small screens) - await comfyPage.setSetting('Comfy.Minimap.Visible', false) + await comfyPage.settings.setSetting('Comfy.Minimap.Visible', false) const loadCheckpointHeaderPos = await getLoadCheckpointHeaderPos(comfyPage) - await comfyPage.panWithTouch( + await comfyPage.canvasOps.panWithTouch( { x: 64, y: 64 diff --git a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-chromium-linux.png index 4b5733a29..f2ac8ddc2 100644 Binary files a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-touch-mobile-chrome-linux.png b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-touch-mobile-chrome-linux.png index f4bac78b7..9248ad0d3 100644 Binary files a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-touch-mobile-chrome-linux.png and b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-touch-mobile-chrome-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/remove.spec.ts b/browser_tests/tests/vueNodes/interactions/node/remove.spec.ts index e7a610643..342b3fb2d 100644 --- a/browser_tests/tests/vueNodes/interactions/node/remove.spec.ts +++ b/browser_tests/tests/vueNodes/interactions/node/remove.spec.ts @@ -3,14 +3,14 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../../../../fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Vue Nodes - Delete Key Interaction', () => { test.beforeEach(async ({ comfyPage }) => { // Enable Vue nodes rendering - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) - await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false) + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.settings.setSetting('Comfy.Graph.CanvasMenu', false) await comfyPage.setup() }) @@ -24,7 +24,7 @@ test.describe('Vue Nodes - Delete Key Interaction', () => { expect(initialNodeCount).toBeGreaterThan(0) // Select all Vue nodes - await comfyPage.ctrlA() + await comfyPage.keyboard.selectAll() // Verify all Vue nodes are selected const selectedCount = await comfyPage.vueNodes.getSelectedNodeCount() @@ -83,7 +83,7 @@ test.describe('Vue Nodes - Delete Key Interaction', () => { test('Delete key does not delete node when typing in Vue node widgets', async ({ comfyPage }) => { - const initialNodeCount = await comfyPage.getGraphNodesCount() + const initialNodeCount = await comfyPage.nodeOps.getGraphNodesCount() // Find a text input widget in a Vue node const textWidget = comfyPage.page @@ -98,7 +98,7 @@ test.describe('Vue Nodes - Delete Key Interaction', () => { await textWidget.press('Delete') // Node count should remain the same - const finalNodeCount = await comfyPage.getGraphNodesCount() + const finalNodeCount = await comfyPage.nodeOps.getGraphNodesCount() expect(finalNodeCount).toBe(initialNodeCount) }) diff --git a/browser_tests/tests/vueNodes/interactions/node/rename.spec.ts b/browser_tests/tests/vueNodes/interactions/node/rename.spec.ts index 5cd6b2fea..942b642b2 100644 --- a/browser_tests/tests/vueNodes/interactions/node/rename.spec.ts +++ b/browser_tests/tests/vueNodes/interactions/node/rename.spec.ts @@ -2,11 +2,12 @@ import { comfyExpect as expect, comfyPageFixture as test } from '../../../../fixtures/ComfyPage' +import { TestIds } from '../../../../fixtures/selectors' test.describe('Vue Nodes Renaming', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false) - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.settings.setSetting('Comfy.Graph.CanvasMenu', false) + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.setup() await comfyPage.vueNodes.waitForNodes() }) @@ -46,7 +47,9 @@ test.describe('Vue Nodes Renaming', () => { if (!nodeBbox) throw new Error('Node not found') await loadCheckpointNode.dblclick() - const editingTitleInput = comfyPage.page.getByTestId('node-title-input') + const editingTitleInput = comfyPage.page.getByTestId( + TestIds.node.titleInput + ) await expect(editingTitleInput).not.toBeVisible() }) }) diff --git a/browser_tests/tests/vueNodes/interactions/node/resize.spec.ts b/browser_tests/tests/vueNodes/interactions/node/resize.spec.ts index c8e015ffa..fc6108195 100644 --- a/browser_tests/tests/vueNodes/interactions/node/resize.spec.ts +++ b/browser_tests/tests/vueNodes/interactions/node/resize.spec.ts @@ -5,7 +5,7 @@ import { test.describe('Vue Node Resizing', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.vueNodes.waitForNodes() }) diff --git a/browser_tests/tests/vueNodes/interactions/node/select.spec.ts b/browser_tests/tests/vueNodes/interactions/node/select.spec.ts index 98b0a63f6..4541072e1 100644 --- a/browser_tests/tests/vueNodes/interactions/node/select.spec.ts +++ b/browser_tests/tests/vueNodes/interactions/node/select.spec.ts @@ -4,12 +4,12 @@ import { } from '../../../../fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Vue Node Selection', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.vueNodes.waitForNodes() }) @@ -78,7 +78,7 @@ test.describe('Vue Node Selection', () => { const initialPos = await checkpointNodeHeader.boundingBox() if (!initialPos) throw new Error('Failed to get header position') - await comfyPage.dragAndDrop( + await comfyPage.canvasOps.dragAndDrop( { x: initialPos.x + 10, y: initialPos.y + 10 }, { x: initialPos.x + 100, y: initialPos.y + 100 } ) diff --git a/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts b/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts index fb9ff2872..0a88b3a97 100644 --- a/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts +++ b/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts @@ -8,10 +8,10 @@ const BYPASS_CLASS = /before:bg-bypass\/60/ test.describe('Vue Node Bypass', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') - await comfyPage.setSetting('Comfy.Minimap.Visible', false) - await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true) + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.Minimap.Visible', false) + await comfyPage.settings.setSetting('Comfy.Graph.CanvasMenu', true) await comfyPage.vueNodes.waitForNodes() }) diff --git a/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts-snapshots/vue-node-bypassed-state-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts-snapshots/vue-node-bypassed-state-chromium-linux.png index 6d4f41790..8edd1d47e 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts-snapshots/vue-node-bypassed-state-chromium-linux.png and b/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts-snapshots/vue-node-bypassed-state-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/collapse.spec.ts b/browser_tests/tests/vueNodes/nodeStates/collapse.spec.ts index 8e8e22995..bb3b722d6 100644 --- a/browser_tests/tests/vueNodes/nodeStates/collapse.spec.ts +++ b/browser_tests/tests/vueNodes/nodeStates/collapse.spec.ts @@ -5,9 +5,9 @@ import { test.describe('Vue Node Collapse', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false) - await comfyPage.setSetting('Comfy.EnableTooltips', true) - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.settings.setSetting('Comfy.Graph.CanvasMenu', false) + await comfyPage.settings.setSetting('Comfy.EnableTooltips', true) + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.setup() await comfyPage.vueNodes.waitForNodes() }) diff --git a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts index 369c7e195..dfdd173b9 100644 --- a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts +++ b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts @@ -5,9 +5,9 @@ import { test.describe('Vue Node Custom Colors', { tag: '@screenshot' }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') - await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true) - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.Canvas.SelectionToolbox', true) + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.vueNodes.waitForNodes() }) @@ -31,7 +31,7 @@ test.describe('Vue Node Custom Colors', { tag: '@screenshot' }, () => { }) test('should load node colors from workflow', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('nodes/every_node_color') + await comfyPage.workflow.loadWorkflow('nodes/every_node_color') await expect(comfyPage.canvas).toHaveScreenshot( 'vue-node-custom-colors-dark-all-colors.png' ) @@ -40,8 +40,8 @@ test.describe('Vue Node Custom Colors', { tag: '@screenshot' }, () => { test('should show brightened node colors on light theme', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.ColorPalette', 'light') - await comfyPage.loadWorkflow('nodes/every_node_color') + await comfyPage.settings.setSetting('Comfy.ColorPalette', 'light') + await comfyPage.workflow.loadWorkflow('nodes/every_node_color') await expect(comfyPage.canvas).toHaveScreenshot( 'vue-node-custom-colors-light-all-colors.png' ) diff --git a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-color-blue-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-color-blue-chromium-linux.png index 226ebb711..7bae69f8f 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-color-blue-chromium-linux.png and b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-color-blue-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-dark-all-colors-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-dark-all-colors-chromium-linux.png index ad7a79ae0..04147441a 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-dark-all-colors-chromium-linux.png and b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-dark-all-colors-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-light-all-colors-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-light-all-colors-chromium-linux.png index 5b94967b8..5f85529cb 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-light-all-colors-chromium-linux.png and b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-light-all-colors-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/error.spec.ts b/browser_tests/tests/vueNodes/nodeStates/error.spec.ts index b8c718239..a9623039c 100644 --- a/browser_tests/tests/vueNodes/nodeStates/error.spec.ts +++ b/browser_tests/tests/vueNodes/nodeStates/error.spec.ts @@ -7,7 +7,7 @@ const ERROR_CLASS = /border-node-stroke-error/ test.describe('Vue Node Error', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.vueNodes.waitForNodes() }) @@ -15,7 +15,7 @@ test.describe('Vue Node Error', () => { comfyPage }) => { await comfyPage.setup() - await comfyPage.loadWorkflow('missing/missing_nodes') + await comfyPage.workflow.loadWorkflow('missing/missing_nodes') // Expect error state on missing unknown node const unknownNode = comfyPage.page.locator('[data-node-id]').filter({ @@ -28,7 +28,7 @@ test.describe('Vue Node Error', () => { comfyPage }) => { await comfyPage.setup() - await comfyPage.loadWorkflow('nodes/execution_error') + await comfyPage.workflow.loadWorkflow('nodes/execution_error') await comfyPage.runButton.click() const raiseErrorNode = comfyPage.vueNodes.getNodeByTitle('Raise Error') diff --git a/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts b/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts index e59d79cfd..b4b4fd8c4 100644 --- a/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts +++ b/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts @@ -8,7 +8,7 @@ const MUTE_OPACITY = '0.5' test.describe('Vue Node Mute', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.vueNodes.waitForNodes() }) diff --git a/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts-snapshots/vue-node-muted-state-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts-snapshots/vue-node-muted-state-chromium-linux.png index b551a56ba..922f96565 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts-snapshots/vue-node-muted-state-chromium-linux.png and b/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts-snapshots/vue-node-muted-state-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/pin.spec.ts b/browser_tests/tests/vueNodes/nodeStates/pin.spec.ts index 27f1ad1ac..f8f174fae 100644 --- a/browser_tests/tests/vueNodes/nodeStates/pin.spec.ts +++ b/browser_tests/tests/vueNodes/nodeStates/pin.spec.ts @@ -8,7 +8,7 @@ const PIN_INDICATOR = '[data-testid="node-pin-indicator"]' test.describe('Vue Node Pin', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.vueNodes.waitForNodes() }) @@ -55,7 +55,7 @@ test.describe('Vue Node Pin', () => { // Try to drag the node const headerPos = await checkpointNodeHeader.boundingBox() if (!headerPos) throw new Error('Failed to get header position') - await comfyPage.dragAndDrop( + await comfyPage.canvasOps.dragAndDrop( { x: headerPos.x, y: headerPos.y }, { x: headerPos.x + 256, y: headerPos.y + 256 } ) @@ -71,7 +71,7 @@ test.describe('Vue Node Pin', () => { await comfyPage.page.keyboard.press(PIN_HOTKEY) // Try to drag the node again - await comfyPage.dragAndDrop( + await comfyPage.canvasOps.dragAndDrop( { x: headerPos.x, y: headerPos.y }, { x: headerPos.x + 256, y: headerPos.y + 256 } ) diff --git a/browser_tests/tests/vueNodes/widgets/int/integerWidget.spec.ts b/browser_tests/tests/vueNodes/widgets/int/integerWidget.spec.ts index ee6b6bbfb..90e1f17da 100644 --- a/browser_tests/tests/vueNodes/widgets/int/integerWidget.spec.ts +++ b/browser_tests/tests/vueNodes/widgets/int/integerWidget.spec.ts @@ -5,14 +5,14 @@ import { test.describe('Vue Integer Widget', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.setup() }) test('should be disabled and not allow changing value when link connected to slot', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('vueNodes/linked-int-widget') + await comfyPage.workflow.loadWorkflow('vueNodes/linked-int-widget') await comfyPage.vueNodes.waitForNodes() const seedWidget = comfyPage.vueNodes diff --git a/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts b/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts index 2ae178d22..c7186e2d6 100644 --- a/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts +++ b/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts @@ -5,7 +5,7 @@ import { test.describe('Vue Upload Widgets', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.vueNodes.waitForNodes() }) @@ -14,7 +14,7 @@ test.describe('Vue Upload Widgets', () => { { tag: '@screenshot' }, async ({ comfyPage }) => { await comfyPage.setup() - await comfyPage.loadWorkflow('widgets/all_load_widgets') + await comfyPage.workflow.loadWorkflow('widgets/all_load_widgets') await comfyPage.vueNodes.waitForNodes() await expect(comfyPage.canvas).toHaveScreenshot( diff --git a/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts-snapshots/vue-nodes-upload-widgets-chromium-linux.png b/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts-snapshots/vue-nodes-upload-widgets-chromium-linux.png index 745415a11..84feb7443 100644 Binary files a/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts-snapshots/vue-nodes-upload-widgets-chromium-linux.png and b/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts-snapshots/vue-nodes-upload-widgets-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/widgets/text/multilineStringWidget.spec.ts b/browser_tests/tests/vueNodes/widgets/text/multilineStringWidget.spec.ts index bb08232a2..8a81772a8 100644 --- a/browser_tests/tests/vueNodes/widgets/text/multilineStringWidget.spec.ts +++ b/browser_tests/tests/vueNodes/widgets/text/multilineStringWidget.spec.ts @@ -1,12 +1,12 @@ import { - type ComfyPage, comfyExpect as expect, comfyPageFixture as test } from '../../../../fixtures/ComfyPage' +import type { ComfyPage } from '../../../../fixtures/ComfyPage' test.describe('Vue Multiline String Widget', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.vueNodes.waitForNodes() }) diff --git a/browser_tests/tests/vueNodes/widgets/widgetReactivity.spec.ts b/browser_tests/tests/vueNodes/widgets/widgetReactivity.spec.ts index 6f3701c12..8a3548614 100644 --- a/browser_tests/tests/vueNodes/widgets/widgetReactivity.spec.ts +++ b/browser_tests/tests/vueNodes/widgets/widgetReactivity.spec.ts @@ -2,10 +2,11 @@ import { comfyExpect as expect, comfyPageFixture as test } from '../../../fixtures/ComfyPage' +import type { TestGraphAccess } from '../../../types/globals' test.describe('Vue Widget Reactivity', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.vueNodes.waitForNodes() }) test('Should display added widgets', async ({ comfyPage }) => { @@ -13,18 +14,21 @@ test.describe('Vue Widget Reactivity', () => { 'css=[data-testid="node-body-4"] > .lg-node-widgets > div' ) await comfyPage.page.evaluate(() => { - const node = window['graph']._nodes_by_id['4'] - node.widgets.push(node.widgets[0]) + const graph = window.graph as TestGraphAccess + const node = graph._nodes_by_id['4'] + node.widgets!.push(node.widgets![0]) }) await expect(loadCheckpointNode).toHaveCount(2) await comfyPage.page.evaluate(() => { - const node = window['graph']._nodes_by_id['4'] - node.widgets[2] = node.widgets[0] + const graph = window.graph as TestGraphAccess + const node = graph._nodes_by_id['4'] + node.widgets![2] = node.widgets![0] }) await expect(loadCheckpointNode).toHaveCount(3) await comfyPage.page.evaluate(() => { - const node = window['graph']._nodes_by_id['4'] - node.widgets.splice(0, 0, node.widgets[0]) + const graph = window.graph as TestGraphAccess + const node = graph._nodes_by_id['4'] + node.widgets!.splice(0, 0, node.widgets![0]) }) await expect(loadCheckpointNode).toHaveCount(4) }) @@ -33,18 +37,21 @@ test.describe('Vue Widget Reactivity', () => { 'css=[data-testid="node-body-3"] > .lg-node-widgets > div' ) await comfyPage.page.evaluate(() => { - const node = window['graph']._nodes_by_id['3'] - node.widgets.pop() + const graph = window.graph as TestGraphAccess + const node = graph._nodes_by_id['3'] + node.widgets!.pop() }) await expect(loadCheckpointNode).toHaveCount(5) await comfyPage.page.evaluate(() => { - const node = window['graph']._nodes_by_id['3'] - node.widgets.length-- + const graph = window.graph as TestGraphAccess + const node = graph._nodes_by_id['3'] + node.widgets!.length-- }) await expect(loadCheckpointNode).toHaveCount(4) await comfyPage.page.evaluate(() => { - const node = window['graph']._nodes_by_id['3'] - node.widgets.splice(0, 1) + const graph = window.graph as TestGraphAccess + const node = graph._nodes_by_id['3'] + node.widgets!.splice(0, 1) }) await expect(loadCheckpointNode).toHaveCount(3) }) diff --git a/browser_tests/tests/widget.spec.ts b/browser_tests/tests/widget.spec.ts index a0dfb0a6e..fd8afb06a 100644 --- a/browser_tests/tests/widget.spec.ts +++ b/browser_tests/tests/widget.spec.ts @@ -1,33 +1,55 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +import { DefaultGraphPositions } from '../fixtures/constants/defaultGraphPositions' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Combo text widget', { tag: ['@screenshot', '@widget'] }, () => { test('Truncates text when resized', async ({ comfyPage }) => { - await comfyPage.resizeLoadCheckpointNode(0.2, 1) + await comfyPage.nodeOps.resizeNode( + DefaultGraphPositions.loadCheckpoint.pos, + DefaultGraphPositions.loadCheckpoint.size, + 0.2, + 1 + ) await expect(comfyPage.canvas).toHaveScreenshot( 'load-checkpoint-resized-min-width.png' ) await comfyPage.closeMenu() - await comfyPage.resizeKsamplerNode(0.2, 1) + await comfyPage.nodeOps.resizeNode( + DefaultGraphPositions.ksampler.pos, + DefaultGraphPositions.ksampler.size, + 0.2, + 1 + ) await expect(comfyPage.canvas).toHaveScreenshot( `ksampler-resized-min-width.png` ) }) test("Doesn't truncate when space still available", async ({ comfyPage }) => { - await comfyPage.resizeEmptyLatentNode(0.8, 0.8) + await comfyPage.nodeOps.resizeNode( + DefaultGraphPositions.emptyLatent.pos, + DefaultGraphPositions.emptyLatent.size, + 0.8, + 0.8 + ) await expect(comfyPage.canvas).toHaveScreenshot( 'empty-latent-resized-80-percent.png' ) }) test('Can revert to full text', async ({ comfyPage }) => { - await comfyPage.resizeLoadCheckpointNode(0.8, 1, true) + await comfyPage.nodeOps.resizeNode( + DefaultGraphPositions.loadCheckpoint.pos, + DefaultGraphPositions.loadCheckpoint.size, + 0.8, + 1, + true + ) await expect(comfyPage.canvas).toHaveScreenshot('resized-to-original.png') }) @@ -36,13 +58,15 @@ test.describe('Combo text widget', { tag: ['@screenshot', '@widget'] }, () => { }) => { const getComboValues = async () => comfyPage.page.evaluate(() => { - return window['app'].graph.nodes - .find((node) => node.title === 'Node With Optional Combo Input') - .widgets.find((widget) => widget.name === 'optional_combo_input') + return window + .app!.graph!.nodes.find( + (node) => node.title === 'Node With Optional Combo Input' + )! + .widgets!.find((widget) => widget.name === 'optional_combo_input')! .options.values }) - await comfyPage.loadWorkflow('inputs/optional_combo_input') + await comfyPage.workflow.loadWorkflow('inputs/optional_combo_input') const initialComboValues = await getComboValues() // Focus canvas @@ -52,16 +76,16 @@ test.describe('Combo text widget', { tag: ['@screenshot', '@widget'] }, () => { await comfyPage.page.keyboard.press('r') // Wait for nodes' widgets to be updated - await comfyPage.page.waitForTimeout(500) - - const refreshedComboValues = await getComboValues() - expect(refreshedComboValues).not.toEqual(initialComboValues) + await expect(async () => { + const refreshedComboValues = await getComboValues() + expect(refreshedComboValues).not.toEqual(initialComboValues) + }).toPass({ timeout: 5000 }) }) test('Should refresh combo values of nodes with v2 combo input spec', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('inputs/node_with_v2_combo_input') + await comfyPage.workflow.loadWorkflow('inputs/node_with_v2_combo_input') // click canvas to focus await comfyPage.page.mouse.click(400, 300) // press R to trigger refresh @@ -71,9 +95,12 @@ test.describe('Combo text widget', { tag: ['@screenshot', '@widget'] }, () => { await comfyPage.nextFrame() // get the combo widget's values const comboValues = await comfyPage.page.evaluate(() => { - return window['app'].graph.nodes - .find((node) => node.title === 'Node With V2 Combo Input') - .widgets.find((widget) => widget.name === 'combo_input').options.values + return window + .app!.graph!.nodes.find( + (node) => node.title === 'Node With V2 Combo Input' + )! + .widgets!.find((widget) => widget.name === 'combo_input')!.options + .values }) expect(comboValues).toEqual(['A', 'B']) }) @@ -81,9 +108,9 @@ test.describe('Combo text widget', { tag: ['@screenshot', '@widget'] }, () => { test.describe('Boolean widget', { tag: ['@screenshot', '@widget'] }, () => { test('Can toggle', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('widgets/boolean_widget') + await comfyPage.workflow.loadWorkflow('widgets/boolean_widget') await expect(comfyPage.canvas).toHaveScreenshot('boolean_widget.png') - const node = (await comfyPage.getFirstNodeRef())! + const node = (await comfyPage.nodeOps.getFirstNodeRef())! const widget = await node.getWidget(0) await widget.click() await expect(comfyPage.canvas).toHaveScreenshot( @@ -94,42 +121,42 @@ test.describe('Boolean widget', { tag: ['@screenshot', '@widget'] }, () => { test.describe('Slider widget', { tag: ['@screenshot', '@widget'] }, () => { test('Can drag adjust value', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('inputs/simple_slider') - const node = (await comfyPage.getFirstNodeRef())! + await comfyPage.workflow.loadWorkflow('inputs/simple_slider') + const node = (await comfyPage.nodeOps.getFirstNodeRef())! const widget = await node.getWidget(0) await comfyPage.page.evaluate(() => { - const widget = window['app'].graph.nodes[0].widgets[0] + const widget = window.app!.graph!.nodes[0].widgets![0] widget.callback = (value: number) => { - window['widgetValue'] = value + window.widgetValue = value } }) await widget.dragHorizontal(50) await expect(comfyPage.canvas).toHaveScreenshot('slider_widget_dragged.png') expect( - await comfyPage.page.evaluate(() => window['widgetValue']) + await comfyPage.page.evaluate(() => window.widgetValue) ).toBeDefined() }) }) test.describe('Number widget', { tag: ['@screenshot', '@widget'] }, () => { test('Can drag adjust value', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('widgets/seed_widget') + await comfyPage.workflow.loadWorkflow('widgets/seed_widget') - const node = (await comfyPage.getFirstNodeRef())! + const node = (await comfyPage.nodeOps.getFirstNodeRef())! const widget = await node.getWidget(0) await comfyPage.page.evaluate(() => { - const widget = window['app'].graph.nodes[0].widgets[0] + const widget = window.app!.graph!.nodes[0].widgets![0] widget.callback = (value: number) => { - window['widgetValue'] = value + window.widgetValue = value } }) await widget.dragHorizontal(50) await expect(comfyPage.canvas).toHaveScreenshot('seed_widget_dragged.png') expect( - await comfyPage.page.evaluate(() => window['widgetValue']) + await comfyPage.page.evaluate(() => window.widgetValue) ).toBeDefined() }) }) @@ -141,11 +168,11 @@ test.describe( test('Auto expand node when widget is added dynamically', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('nodes/single_ksampler') + await comfyPage.workflow.loadWorkflow('nodes/single_ksampler') await comfyPage.page.evaluate(() => { - window['graph'].nodes[0].addWidget('number', 'new_widget', 10) - window['graph'].setDirtyCanvas(true, true) + window.app!.graph!.nodes[0].addWidget('number', 'new_widget', 10, null) + window.app!.graph!.setDirtyCanvas(true, true) }) await expect(comfyPage.canvas).toHaveScreenshot( @@ -157,20 +184,20 @@ test.describe( test.describe('Image widget', { tag: ['@screenshot', '@widget'] }, () => { test('Can load image', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('widgets/load_image_widget') + await comfyPage.workflow.loadWorkflow('widgets/load_image_widget') await expect(comfyPage.canvas).toHaveScreenshot('load_image_widget.png') }) test('Can drag and drop image', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('widgets/load_image_widget') + await comfyPage.workflow.loadWorkflow('widgets/load_image_widget') // Get position of the load image node - const nodes = await comfyPage.getNodeRefsByType('LoadImage') + const nodes = await comfyPage.nodeOps.getNodeRefsByType('LoadImage') const loadImageNode = nodes[0] const { x, y } = await loadImageNode.getPosition() // Drag and drop image file onto the load image node - await comfyPage.dragAndDropFile('image32x32.webp', { + await comfyPage.dragDrop.dragAndDropFile('image32x32.webp', { dropPosition: { x, y } }) @@ -188,8 +215,8 @@ test.describe('Image widget', { tag: ['@screenshot', '@widget'] }, () => { test('Can change image by changing the filename combo value', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('widgets/load_image_widget') - const nodes = await comfyPage.getNodeRefsByType('LoadImage') + await comfyPage.workflow.loadWorkflow('widgets/load_image_widget') + const nodes = await comfyPage.nodeOps.getNodeRefsByType('LoadImage') const loadImageNode = nodes[0] // Click the combo widget used to select the image filename @@ -224,14 +251,14 @@ test.describe('Image widget', { tag: ['@screenshot', '@widget'] }, () => { image1.src = src const image2 = new Image() image2.src = src - const targetNode = graph.nodes[6] + const targetNode = graph!.nodes[6] targetNode.imgs = [image1, image2] targetNode.imageIndex = 1 - app.canvas.setDirty(true) + app!.canvas.setDirty(true) const x = targetNode.pos[0] + targetNode.size[0] - 41 - const y = targetNode.pos[1] + targetNode.widgets.at(-1).last_y + 30 - return app.canvasPosToClientPos([x, y]) + const y = targetNode.pos[1] + targetNode.widgets!.at(-1)!.last_y! + 30 + return app!.canvasPosToClientPos([x, y]) }) const clip = { x, y, width: 35, height: 35 } @@ -250,17 +277,17 @@ test.describe( test.skip('Shows preview of uploaded animated image', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('widgets/load_animated_webp') + await comfyPage.workflow.loadWorkflow('widgets/load_animated_webp') // Get position of the load animated webp node - const nodes = await comfyPage.getNodeRefsByType( + const nodes = await comfyPage.nodeOps.getNodeRefsByType( 'DevToolsLoadAnimatedImageTest' ) const loadAnimatedWebpNode = nodes[0] const { x, y } = await loadAnimatedWebpNode.getPosition() // Drag and drop image file onto the load animated webp node - await comfyPage.dragAndDropFile('animated_webp.webp', { + await comfyPage.dragDrop.dragAndDropFile('animated_webp.webp', { dropPosition: { x, y } }) @@ -279,17 +306,17 @@ test.describe( }) test('Can drag-and-drop animated webp image', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('widgets/load_animated_webp') + await comfyPage.workflow.loadWorkflow('widgets/load_animated_webp') // Get position of the load animated webp node - const nodes = await comfyPage.getNodeRefsByType( + const nodes = await comfyPage.nodeOps.getNodeRefsByType( 'DevToolsLoadAnimatedImageTest' ) const loadAnimatedWebpNode = nodes[0] const { x, y } = await loadAnimatedWebpNode.getPosition() // Drag and drop image file onto the load animated webp node - await comfyPage.dragAndDropFile('animated_webp.webp', { + await comfyPage.dragDrop.dragAndDropFile('animated_webp.webp', { dropPosition: { x, y }, waitForUpload: true }) @@ -301,23 +328,24 @@ test.describe( }) test('Can preview saved animated webp image', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('widgets/save_animated_webp') + await comfyPage.workflow.loadWorkflow('widgets/save_animated_webp') // Get position of the load animated webp node - const loadNodes = await comfyPage.getNodeRefsByType( + const loadNodes = await comfyPage.nodeOps.getNodeRefsByType( 'DevToolsLoadAnimatedImageTest' ) const loadAnimatedWebpNode = loadNodes[0] const { x, y } = await loadAnimatedWebpNode.getPosition() // Drag and drop image file onto the load animated webp node - await comfyPage.dragAndDropFile('animated_webp.webp', { + await comfyPage.dragDrop.dragAndDropFile('animated_webp.webp', { dropPosition: { x, y } }) await comfyPage.nextFrame() // Get the SaveAnimatedWEBP node - const saveNodes = await comfyPage.getNodeRefsByType('SaveAnimatedWEBP') + const saveNodes = + await comfyPage.nodeOps.getNodeRefsByType('SaveAnimatedWEBP') const saveAnimatedWebpNode = saveNodes[0] if (!saveAnimatedWebpNode) throw new Error('SaveAnimatedWEBP node not found') @@ -326,8 +354,8 @@ test.describe( await comfyPage.page.evaluate( ([loadId, saveId]) => { // Set the output of the SaveAnimatedWEBP node to equal the loader node's image - window['app'].nodeOutputs[saveId] = window['app'].nodeOutputs[loadId] - app.canvas.setDirty(true) + window.app!.nodeOutputs[saveId] = window.app!.nodeOutputs[loadId] + app!.canvas.setDirty(true) }, [loadAnimatedWebpNode.id, saveAnimatedWebpNode.id] ) @@ -340,7 +368,7 @@ test.describe( test.describe('Load audio widget', { tag: ['@screenshot', '@widget'] }, () => { test('Can load audio', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('widgets/load_audio_widget') + await comfyPage.workflow.loadWorkflow('widgets/load_audio_widget') // Wait for the audio widget to be rendered in the DOM await comfyPage.page.waitForSelector('.comfy-audio', { state: 'attached' }) await comfyPage.nextFrame() @@ -353,13 +381,13 @@ test.describe('Unserialized widgets', { tag: '@widget' }, () => { comfyPage }) => { // Add workflow w/ LoadImage node, which contains file upload and image preview widgets (not serialized) - await comfyPage.loadWorkflow('widgets/load_image_widget') + await comfyPage.workflow.loadWorkflow('widgets/load_image_widget') // Move mouse and click to trigger the `graphEqual` check in `changeTracker.ts` await comfyPage.page.mouse.move(10, 10) await comfyPage.page.mouse.click(10, 10) // Expect the graph to not be modified - expect(await comfyPage.isCurrentWorkflowModified()).toBe(false) + expect(await comfyPage.workflow.isCurrentWorkflowModified()).toBe(false) }) }) diff --git a/browser_tests/tests/workflowTabThumbnail.spec.ts b/browser_tests/tests/workflowTabThumbnail.spec.ts index 1f4d9464d..bd8a5efe1 100644 --- a/browser_tests/tests/workflowTabThumbnail.spec.ts +++ b/browser_tests/tests/workflowTabThumbnail.spec.ts @@ -5,8 +5,11 @@ import type { ComfyPage } from '../fixtures/ComfyPage' test.describe('Workflow Tab Thumbnails', { tag: '@workflow' }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') - await comfyPage.setSetting('Comfy.Workflow.WorkflowTabsPosition', 'Topbar') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting( + 'Comfy.Workflow.WorkflowTabsPosition', + 'Topbar' + ) await comfyPage.setup() }) @@ -90,9 +93,9 @@ test.describe('Workflow Tab Thumbnails', { tag: '@workflow' }, () => { canvasArea!.x + canvasArea!.width - 100, 100 ) - await comfyPage.delay(300) // Wait for the popover to hide + await expect(comfyPage.page.locator('.workflow-popover-fade')).toBeHidden() - await comfyPage.rightClickCanvas(200, 200) + await comfyPage.canvasOps.rightClick(200, 200) await comfyPage.page.getByText('Add Node').click() await comfyPage.nextFrame() await comfyPage.page.getByText(category).click() diff --git a/browser_tests/tsconfig.json b/browser_tests/tsconfig.json index 46e5a9bcd..8e943db4b 100644 --- a/browser_tests/tsconfig.json +++ b/browser_tests/tsconfig.json @@ -5,7 +5,13 @@ "noEmit": true, "noUnusedLocals": true, "noUnusedParameters": true, - "resolveJsonModule": true + "resolveJsonModule": true, + "types": ["vite/client"] }, - "include": ["**/*.ts"] + "include": [ + "**/*.ts", + "../src/types/**/*.d.ts", + "../global.d.ts", + "types/**/*.d.ts" + ] } diff --git a/browser_tests/types/globals.d.ts b/browser_tests/types/globals.d.ts new file mode 100644 index 000000000..1d38a31a6 --- /dev/null +++ b/browser_tests/types/globals.d.ts @@ -0,0 +1,69 @@ +import type { LGraph } from '@/lib/litegraph/src/LGraph' +// eslint-disable-next-line unused-imports/no-unused-imports -- used in typeof +import type { LGraphBadge } from '@/lib/litegraph/src/LGraphBadge' +import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' +import type { LiteGraphGlobal } from '@/lib/litegraph/src/LiteGraphGlobal' +import type { ComfyApp } from '@/scripts/app' +import type { useWorkspaceStore } from '@/stores/workspaceStore' + +/** + * Helper type for accessing nodes by ID in browser tests. + * Provides typed access to graph internals without requiring `any`. + */ +export interface TestGraphAccess { + _nodes_by_id: Record +} + +interface AppReadiness { + featureFlagsReceived: boolean + apiInitialized: boolean + appInitialized: boolean +} + +interface CapturedMessages { + clientFeatureFlags: unknown + serverFeatureFlags: unknown +} + +declare global { + interface Window { + app?: ComfyApp + graph?: LGraph + LiteGraph?: LiteGraphGlobal + LGraphBadge?: typeof LGraphBadge + + // Test-specific globals used for assertions + foo?: boolean + TestCommand?: boolean + changeCount?: number + widgetValue?: unknown + + // Feature flags test globals + __capturedMessages?: CapturedMessages + __appReadiness?: AppReadiness + + /** + * WebSocket store used by test fixtures for mocking WebSocket connections. + * @see browser_tests/fixtures/ws.ts + */ + __ws__?: Record + } + + const app: ComfyApp | undefined + const graph: LGraph | undefined + const LiteGraph: LiteGraphGlobal | undefined + const LGraphBadge: typeof LGraphBadge | undefined +} + +/** + * Internal store type for browser test access. + * Used to access properties not exposed via the public ExtensionManager interface. + * + * @example + * ```ts + * await page.evaluate(() => { + * ;(window.app!.extensionManager as WorkspaceStore).workflow.syncWorkflows() + * }) + * ``` + */ +export type WorkspaceStore = ReturnType diff --git a/build/plugins/comfyAPIPlugin.ts b/build/plugins/comfyAPIPlugin.ts index 394914dbe..16135cbd7 100644 --- a/build/plugins/comfyAPIPlugin.ts +++ b/build/plugins/comfyAPIPlugin.ts @@ -9,7 +9,11 @@ interface ShimResult { const SKIP_WARNING_FILES = new Set(['scripts/app', 'scripts/api']) /** Files that will be removed in v1.34 */ -const DEPRECATED_FILES = ['scripts/ui', 'extensions/core/groupNode'] as const +const DEPRECATED_FILES = [ + 'scripts/ui', + 'extensions/core/groupNode', + 'extensions/core/nodeTemplates' +] as const function getWarningMessage( fileKey: string, diff --git a/docs/guidance/playwright.md b/docs/guidance/playwright.md index 7825249cf..4eefbc08d 100644 --- a/docs/guidance/playwright.md +++ b/docs/guidance/playwright.md @@ -14,6 +14,67 @@ See `docs/testing/*.md` for detailed patterns. - Prefer specific selectors (role, label, test-id) - Test across viewports +## Window Globals + +Browser tests access `window.app`, `window.graph`, and `window.LiteGraph` which are +optional in the main app types. In E2E tests, use non-null assertions (`!`): + +```typescript +window.app!.graph!.nodes +window.LiteGraph!.registered_node_types +``` + +This is the **only context** where non-null assertions are acceptable. + +**TODO:** Consolidate these references into a central utility (e.g., `getApp()`) that +performs proper runtime type checking, removing the need for scattered `!` assertions. + +## Type Assertions in E2E Tests + +E2E tests may use **specific** type assertions when needed, but **never** `as any`. + +### Acceptable Patterns + +```typescript +// ✅ Non-null assertions for window globals +window.app!.extensionManager + +// ✅ Specific type assertions with documentation +// Extensions can register arbitrary setting IDs +id: 'TestSetting' as TestSettingId + +// ✅ Test-local type helpers +type TestSettingId = keyof Settings +``` + +### Forbidden Patterns + +```typescript +// ❌ Never use `as any` +settings: testData as any + +// ❌ Never modify production types to satisfy test errors +// Don't add test settings to src/schemas/apiSchema.ts + +// ❌ Don't chain through unknown to bypass types +data as unknown as SomeType // Avoid; prefer explicit typings or helpers +``` + +### Accessing Internal State + +When tests need internal store properties (e.g., `.workflow`, `.focusMode`): + +```typescript +// ✅ Access stores directly in page.evaluate +await page.evaluate(() => { + const store = useWorkflowStore() + return store.activeWorkflow +}) + +// ❌ Don't change public API types to expose internals +// Keep app.extensionManager typed as ExtensionManager, not WorkspaceStore +``` + ## Test Tags Tags are respected by config: @@ -29,6 +90,6 @@ Tags are respected by config: ## Running Tests ```bash -pnpm test:browser # Run all E2E tests -pnpm test:browser -- --ui # Interactive UI mode +pnpm test:browser:local # Run all E2E tests +pnpm test:browser:local -- --ui # Interactive UI mode ``` diff --git a/docs/guidance/typescript.md b/docs/guidance/typescript.md index 9a6dd102b..17b2f3989 100644 --- a/docs/guidance/typescript.md +++ b/docs/guidance/typescript.md @@ -14,6 +14,28 @@ globs: - Type assertions are a last resort; they lead to brittle code - Avoid `@ts-expect-error` - fix the underlying issue instead +### Type Assertion Hierarchy + +When you must handle uncertain types, prefer these approaches in order: + +1. ✅ **No assertion** — Properly typed from the start +2. ✅ **Type narrowing** — `if ('prop' in obj)` or type guards +3. ⚠️ **Specific assertion** — `as SpecificType` when you truly know the type +4. ⚠️ **`unknown` with narrowing** — For genuinely unknown data +5. ❌ **`as any`** — FORBIDDEN + +### Zod Schema Rules + +- Never use `z.any()` — it disables validation and propagates `any` into types +- Use `z.unknown()` if the type is genuinely unknown, then narrow it +- Never add test-only settings/types to production schemas + +### Public API Contracts + +- Keep public API types stable (e.g., `ExtensionManager` interface) +- Don't expose internal implementation types (e.g., Pinia store internals) +- Reactive refs (`ComputedRef`) should be unwrapped before exposing + ## Utility Libraries - Use `es-toolkit` for utility functions (not lodash) diff --git a/package.json b/package.json index 86db51eb8..4cd32bb40 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@comfyorg/comfyui-frontend", - "version": "1.39.2", + "version": "1.39.5", "private": true, "description": "Official front-end implementation of ComfyUI", "homepage": "https://comfy.org", @@ -47,6 +47,7 @@ "test:browser:local": "cross-env PLAYWRIGHT_LOCAL=1 pnpm test:browser", "test:unit": "nx run test", "typecheck": "vue-tsc --noEmit", + "typecheck:browser": "vue-tsc --project browser_tests/tsconfig.json", "typecheck:desktop": "nx run @comfyorg/desktop-ui:typecheck", "zipdist": "node scripts/zipdist.js", "clean": "nx reset" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eced908c6..d742311b7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,8 +46,8 @@ catalogs: specifier: ^1.0.3 version: 1.0.3 '@playwright/test': - specifier: ^1.57.0 - version: 1.57.0 + specifier: ^1.58.1 + version: 1.58.1 '@primeuix/forms': specifier: 0.0.2 version: 0.0.2 @@ -121,11 +121,11 @@ catalogs: specifier: ^2.4.6 version: 2.4.6 '@vueuse/core': - specifier: ^11.0.0 - version: 11.0.0 + specifier: ^14.2.0 + version: 14.2.0 '@vueuse/integrations': - specifier: ^13.9.0 - version: 13.9.0 + specifier: ^14.2.0 + version: 14.2.0 '@webgpu/types': specifier: ^0.1.66 version: 0.1.66 @@ -218,7 +218,7 @@ catalogs: version: 1.1.1 pinia: specifier: ^3.0.4 - version: 2.2.2 + version: 3.0.4 postcss-html: specifier: ^1.8.0 version: 1.8.0 @@ -290,7 +290,7 @@ catalogs: version: 3.5.13 vue-component-type-helpers: specifier: ^3.2.1 - version: 3.2.1 + version: 3.2.4 vue-eslint-parser: specifier: ^10.2.0 version: 10.2.0 @@ -376,7 +376,7 @@ importers: version: 4.2.5 '@sentry/vue': specifier: 'catalog:' - version: 10.32.1(pinia@2.2.2(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3)))(vue@3.5.13(typescript@5.9.3)) + version: 10.32.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3)))(vue@3.5.13(typescript@5.9.3)) '@sparkjsdev/spark': specifier: 'catalog:' version: 0.1.10 @@ -403,10 +403,10 @@ importers: version: 2.10.4 '@vueuse/core': specifier: 'catalog:' - version: 11.0.0(vue@3.5.13(typescript@5.9.3)) + version: 14.2.0(vue@3.5.13(typescript@5.9.3)) '@vueuse/integrations': specifier: 'catalog:' - version: 13.9.0(axios@1.13.2)(fuse.js@7.0.0)(vue@3.5.13(typescript@5.9.3)) + version: 14.2.0(axios@1.13.2)(fuse.js@7.0.0)(vue@3.5.13(typescript@5.9.3)) '@xterm/addon-fit': specifier: ^0.10.0 version: 0.10.0(@xterm/xterm@5.5.0) @@ -466,7 +466,7 @@ importers: version: 15.0.11 pinia: specifier: 'catalog:' - version: 2.2.2(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3)) + version: 3.0.4(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3)) primeicons: specifier: 'catalog:' version: 7.0.0 @@ -527,7 +527,7 @@ importers: version: 22.2.6(@babel/traverse@7.28.5)(@zkochan/js-yaml@0.0.7)(eslint@9.39.1(jiti@2.6.1))(nx@22.2.6) '@nx/playwright': specifier: 'catalog:' - version: 22.2.6(@babel/traverse@7.28.5)(@playwright/test@1.57.0)(@zkochan/js-yaml@0.0.7)(eslint@9.39.1(jiti@2.6.1))(nx@22.2.6) + version: 22.2.6(@babel/traverse@7.28.5)(@playwright/test@1.58.1)(@zkochan/js-yaml@0.0.7)(eslint@9.39.1(jiti@2.6.1))(nx@22.2.6) '@nx/storybook': specifier: 'catalog:' version: 22.2.4(@babel/traverse@7.28.5)(@zkochan/js-yaml@0.0.7)(eslint@9.39.1(jiti@2.6.1))(nx@22.2.6)(storybook@10.1.9(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3) @@ -536,10 +536,10 @@ importers: version: 22.2.6(@babel/traverse@7.28.5)(nx@22.2.6)(typescript@5.9.3)(vite@8.0.0-beta.8(@types/node@24.10.4)(esbuild@0.27.1)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vitest@4.0.16) '@pinia/testing': specifier: 'catalog:' - version: 1.0.3(pinia@2.2.2(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3))) + version: 1.0.3(pinia@3.0.4(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3))) '@playwright/test': specifier: 'catalog:' - version: 1.57.0 + version: 1.58.1 '@sentry/vite-plugin': specifier: 'catalog:' version: 4.6.0 @@ -722,7 +722,7 @@ importers: version: 4.0.16(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.1)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2) vue-component-type-helpers: specifier: 'catalog:' - version: 3.2.1 + version: 3.2.4 vue-eslint-parser: specifier: 'catalog:' version: 10.2.0(eslint@9.39.1(jiti@2.6.1)) @@ -752,10 +752,10 @@ importers: version: 4.2.5 '@vueuse/core': specifier: 'catalog:' - version: 11.0.0(vue@3.5.13(typescript@5.9.3)) + version: 14.2.0(vue@3.5.13(typescript@5.9.3)) pinia: specifier: 'catalog:' - version: 2.2.2(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3)) + version: 3.0.4(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3)) primeicons: specifier: 'catalog:' version: 7.0.0 @@ -924,8 +924,8 @@ packages: '@atlaskit/pragmatic-drag-and-drop@1.3.1': resolution: {integrity: sha512-MptcLppK78B2eplL5fHk93kfCbZ6uCpt33YauBPrOwI5zcHYJhZGeaGEaAXoVAHnSJOdQUhy6kGVVC9qggz2Fg==} - '@babel/code-frame@7.27.1': - resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} '@babel/compat-data@7.28.5': @@ -1450,8 +1450,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/runtime@7.28.4': - resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} + '@babel/runtime@7.28.6': + resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} engines: {node: '>=6.9.0'} '@babel/standalone@7.28.5': @@ -2416,41 +2416,49 @@ packages: resolution: {integrity: sha512-RcVICKulADKSKgLpLLtBqbf6HInWUrVznnrELNUkRbAmcf89KcaZD49VZROADTe8hHYeSw1JGqmiUuvP30KY2g==} cpu: [arm64] os: [linux] + libc: [glibc] '@nx/nx-linux-arm64-gnu@22.2.6': resolution: {integrity: sha512-mW13bpfNalzm1X2UVBkmPJTI++NntjWHVv0TNFPXKTm3W8nBYd5uoN+EC3YRw8AoVqFR+pNkit5kHRSLFjjx3A==} cpu: [arm64] os: [linux] + libc: [glibc] '@nx/nx-linux-arm64-musl@22.2.4': resolution: {integrity: sha512-I1Er5bUJ/cXq/82KgFVd/u1F4urzcegPXTqI8H/TIrhNIf3jCNThXsbOvDb8R8pGzq8QgIojkF2HiL5BRvKf7w==} cpu: [arm64] os: [linux] + libc: [musl] '@nx/nx-linux-arm64-musl@22.2.6': resolution: {integrity: sha512-9gHCDIjf8dsEZyQFHZvB4Kp/e649lxf1Df/tExpoedLct91vrluTzbDKvXILg2FkaNUehryZYFSQy3y5Qpl5iQ==} cpu: [arm64] os: [linux] + libc: [musl] '@nx/nx-linux-x64-gnu@22.2.4': resolution: {integrity: sha512-5n2+0Hl+baEtsWjK9Mh4ipCwIyxbGCQs6cEzDjf9MlscL7YvYRlUub4tLBU3qREFfycm2RExWoRbb5BSGNpRHg==} cpu: [x64] os: [linux] + libc: [glibc] '@nx/nx-linux-x64-gnu@22.2.6': resolution: {integrity: sha512-7m3gcJ1FEFEqZtYXhKHnY2+b+WEP2oK1PjBdVoa9YW1QXgCGLUXyEb703eMM7vAtLR0mM2/ih+o2WNSRze+tgg==} cpu: [x64] os: [linux] + libc: [glibc] '@nx/nx-linux-x64-musl@22.2.4': resolution: {integrity: sha512-89KSy981mS4nvO2n7UpbDEpj2J3EyuIbSBrjXQ7hCBO+1b7/KNRBnxJ6Lspo689A7FUguHweC+wbIqvhuXT2mg==} cpu: [x64] os: [linux] + libc: [musl] '@nx/nx-linux-x64-musl@22.2.6': resolution: {integrity: sha512-VE7KqT8W4rILaBWrwO5pY3K14fwNMDmQTfdpY2GC5gkpwqkMPBwj4nb2hKY0jzylI1nqR629IT4elmatKTiF4g==} cpu: [x64] os: [linux] + libc: [musl] '@nx/nx-win32-arm64-msvc@22.2.4': resolution: {integrity: sha512-ziFjoeJlYE2eQIwMVsZm6h8Zl5eMSj9EMsE4E3RGWat9oYGJnH8nMZ/bF4OptvHIfiGZs2pHxFtq9wtturaFWw==} @@ -2557,41 +2565,49 @@ packages: resolution: {integrity: sha512-SVjjjtMW66Mza76PBGJLqB0KKyFTBnxmtDXLJPbL6ZPGSctcXVmujz7/WAc0rb9m2oV0cHQTtVjnq6orQnI/jg==} cpu: [arm64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-arm64-musl@11.15.0': resolution: {integrity: sha512-JDv2/AycPF2qgzEiDeMJCcSzKNDm3KxNg0KKWipoKEMDFqfM7LxNwwSVyAOGmrYlE4l3dg290hOMsr9xG7jv9g==} cpu: [arm64] os: [linux] + libc: [musl] '@oxc-resolver/binding-linux-ppc64-gnu@11.15.0': resolution: {integrity: sha512-zbu9FhvBLW4KJxo7ElFvZWbSt4vP685Qc/Gyk/Ns3g2gR9qh2qWXouH8PWySy+Ko/qJ42+HJCLg+ZNcxikERfg==} cpu: [ppc64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-riscv64-gnu@11.15.0': resolution: {integrity: sha512-Kfleehe6B09C2qCnyIU01xLFqFXCHI4ylzkicfX/89j+gNHh9xyNdpEvit88Kq6i5tTGdavVnM6DQfOE2qNtlg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-riscv64-musl@11.15.0': resolution: {integrity: sha512-J7LPiEt27Tpm8P+qURDwNc8q45+n+mWgyys4/V6r5A8v5gDentHRGUx3iVk5NxdKhgoGulrzQocPTZVosq25Eg==} cpu: [riscv64] os: [linux] + libc: [musl] '@oxc-resolver/binding-linux-s390x-gnu@11.15.0': resolution: {integrity: sha512-+8/d2tAScPjVJNyqa7GPGnqleTB/XW9dZJQ2D/oIM3wpH3TG+DaFEXBbk4QFJ9K9AUGBhvQvWU2mQyhK/yYn3Q==} cpu: [s390x] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-x64-gnu@11.15.0': resolution: {integrity: sha512-xtvSzH7Nr5MCZI2FKImmOdTl9kzuQ51RPyLh451tvD2qnkg3BaqI9Ox78bTk57YJhlXPuxWSOL5aZhKAc9J6qg==} cpu: [x64] os: [linux] + libc: [glibc] '@oxc-resolver/binding-linux-x64-musl@11.15.0': resolution: {integrity: sha512-14YL1zuXj06+/tqsuUZuzL0T425WA/I4nSVN1kBXeC5WHxem6lQ+2HGvG+crjeJEqHgZUT62YIgj88W+8E7eyg==} cpu: [x64] os: [linux] + libc: [musl] '@oxc-resolver/binding-openharmony-arm64@11.15.0': resolution: {integrity: sha512-/7Qli+1Wk93coxnrQaU8ySlICYN8HsgyIrzqjgIkQEpI//9eUeaeIHZptNl2fMvBGeXa7k2QgLbRNaBRgpnvMw==} @@ -2632,21 +2648,25 @@ packages: resolution: {integrity: sha512-GubkQeQT5d3B/Jx/IiR7NMkSmXrCZcVI0BPh1i7mpFi8HgD1hQ/LbhiBKAMsMqs5bbugdQOgBEl8bOhe8JhW1g==} cpu: [arm64] os: [linux] + libc: [glibc] '@oxfmt/linux-arm64-musl@0.26.0': resolution: {integrity: sha512-OEypUwK69bFPj+aa3/LYCnlIUPgoOLu//WNcriwpnWNmt47808Ht7RJSg+MNK8a7pSZHpXJ5/E6CRK/OTwFdaQ==} cpu: [arm64] os: [linux] + libc: [musl] '@oxfmt/linux-x64-gnu@0.26.0': resolution: {integrity: sha512-xO6iEW2bC6ZHyOTPmPWrg/nM6xgzyRPaS84rATy6F8d79wz69LdRdJ3l/PXlkqhi7XoxhvX4ExysA0Nf10ZZEQ==} cpu: [x64] os: [linux] + libc: [glibc] '@oxfmt/linux-x64-musl@0.26.0': resolution: {integrity: sha512-Z3KuZFC+MIuAyFCXBHY71kCsdRq1ulbsbzTe71v+hrEv7zVBn6yzql+/AZcgfIaKzWO9OXNuz5WWLWDmVALwow==} cpu: [x64] os: [linux] + libc: [musl] '@oxfmt/win32-arm64@0.26.0': resolution: {integrity: sha512-3zRbqwVWK1mDhRhTknlQFpRFL9GhEB5GfU6U7wawnuEwpvi39q91kJ+SRJvJnhyPCARkjZBd1V8XnweN5IFd1g==} @@ -2702,21 +2722,25 @@ packages: resolution: {integrity: sha512-yb/k8GaMDgnX2LyO6km33kKItZ/n573SlbiHBBFU2HmeU7tzEHL5jHkHQXXcysUkapmqHd7UsDhOZDqPmXaQRg==} cpu: [arm64] os: [linux] + libc: [glibc] '@oxlint/linux-arm64-musl@1.33.0': resolution: {integrity: sha512-03pt9IO1C4ZfVOW6SQiOK26mzklAhLM3Kc79OXpX1kgZRlxk+rvFoMhlgCOzn7tEdrEgbePkBoxNnwDnJDFqJQ==} cpu: [arm64] os: [linux] + libc: [musl] '@oxlint/linux-x64-gnu@1.33.0': resolution: {integrity: sha512-Z7ImLWM50FoVXzYvyxUQ+QwBkBfRyK4YdLEGonyAGMp7iT3DksonDaTK9ODnJ1qHyAyAZCvuqXD7AEDsDvzDbA==} cpu: [x64] os: [linux] + libc: [glibc] '@oxlint/linux-x64-musl@1.33.0': resolution: {integrity: sha512-idb55Uzu5kkqqpMiVUfI9nP7zOqPZinQKsIRQAIU40wILcf/ijvhNZKIu3ucDMmye0n6IWOaSnxIRL5W2fNoUQ==} cpu: [x64] os: [linux] + libc: [musl] '@oxlint/win32-arm64@1.33.0': resolution: {integrity: sha512-wKKFt7cubfrLelNzdmDsNSmtBrlSUe1fWus587+uSxDZdpFbQ7liU0gsUlCbcHvym0H1Tc2O3K3cnLrgQORLPQ==} @@ -2746,8 +2770,8 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@playwright/test@1.57.0': - resolution: {integrity: sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==} + '@playwright/test@1.58.1': + resolution: {integrity: sha512-6LdVIUERWxQMmUSSQi0I53GgCBYgM2RpGngCPY7hSeju+VrKjq3lvs7HpJoPbDiY5QM5EYRtRX5fvrinnMAz3w==} engines: {node: '>=18'} hasBin: true @@ -2795,7 +2819,7 @@ packages: '@primevue/themes@4.2.5': resolution: {integrity: sha512-8F7yA36xYIKtNuAuyBdZZEks/bKDwlhH5WjpqGGB0FdwfAEoBYsynQ5sdqcT2Lb/NsajHmS5lc++Ttlvr1g1Lw==} engines: {node: '>=12.11.0'} - deprecated: This package is deprecated. Use @primeuix/themes instead. + deprecated: 'Deprecated. This package is no longer maintained. Please migrate to @primeuix/themes: https://www.npmjs.com/package/@primeuix/themes' '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -2865,24 +2889,28 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-arm64-musl@1.0.0-beta.60': resolution: {integrity: sha512-8xlqGLDtTP8sBfYwneTDu8+PRm5reNEHAuI/+6WPy9y350ls0KTFd3EJCOWEXWGW0F35ko9Fn9azmurBTjqOrQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@rolldown/binding-linux-x64-gnu@1.0.0-beta.60': resolution: {integrity: sha512-iR4nhVouVZK1CiGGGyz+prF5Lw9Lmz30Rl36Hajex+dFVFiegka604zBwzTp5Tl0BZnr50ztnVJ30tGrBhDr8Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-musl@1.0.0-beta.60': resolution: {integrity: sha512-HbfNcqNeqxFjSMf1Kpe8itr2e2lr0Bm6HltD2qXtfU91bSSikVs9EWsa1ThshQ1v2ZvxXckGjlVLtah6IoslPg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@rolldown/binding-openharmony-arm64@1.0.0-beta.60': resolution: {integrity: sha512-BiiamFcgTJ+ZFOUIMO9AHXUo9WXvHVwGfSrJ+Sv0AsTd2w3VN7dJGiH3WRcxKFetljJHWvGbM4fdpY5lf6RIvw==} @@ -2960,56 +2988,67 @@ packages: resolution: {integrity: sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.53.5': resolution: {integrity: sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.53.5': resolution: {integrity: sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.53.5': resolution: {integrity: sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.53.5': resolution: {integrity: sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.53.5': resolution: {integrity: sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.53.5': resolution: {integrity: sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.53.5': resolution: {integrity: sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.53.5': resolution: {integrity: sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.53.5': resolution: {integrity: sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.53.5': resolution: {integrity: sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openharmony-arm64@4.53.5': resolution: {integrity: sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==} @@ -3277,24 +3316,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.12': resolution: {integrity: sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.12': resolution: {integrity: sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.12': resolution: {integrity: sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.12': resolution: {integrity: sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg==} @@ -3623,9 +3666,6 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} - '@types/web-bluetooth@0.0.20': - resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==} - '@types/web-bluetooth@0.0.21': resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} @@ -3770,41 +3810,49 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] + libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -3971,20 +4019,26 @@ packages: '@vue/compiler-vue2@2.7.16': resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} - '@vue/devtools-api@6.6.3': - resolution: {integrity: sha512-0MiMsFma/HqA6g3KLKn+AGpL1kgKhFWszC9U29NfpWK5LE7bjeXxySWJrOJ77hBz+TBrBQ7o4QJqbPbqbs8rJw==} - '@vue/devtools-api@6.6.4': resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + '@vue/devtools-api@7.7.9': + resolution: {integrity: sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==} + '@vue/devtools-core@8.0.5': resolution: {integrity: sha512-dpCw8nl0GDBuiL9SaY0mtDxoGIEmU38w+TQiYEPOLhW03VDC0lfNMYXS/qhl4I0YlysGp04NLY4UNn6xgD0VIQ==} peerDependencies: vue: ^3.0.0 + '@vue/devtools-kit@7.7.9': + resolution: {integrity: sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==} + '@vue/devtools-kit@8.0.5': resolution: {integrity: sha512-q2VV6x1U3KJMTQPUlRMyWEKVbcHuxhqJdSr6Jtjz5uAThAIrfJ6WVZdGZm5cuO63ZnSUz0RCsVwiUUb0mDV0Yg==} + '@vue/devtools-shared@7.7.9': + resolution: {integrity: sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==} + '@vue/devtools-shared@8.0.5': resolution: {integrity: sha512-bRLn6/spxpmgLk+iwOrR29KrYnJjG9DGpHGkDFG82UM21ZpJ39ztUT9OXX3g+usW7/b2z+h46I9ZiYyB07XMXg==} @@ -4033,25 +4087,22 @@ packages: '@vue/test-utils@2.4.6': resolution: {integrity: sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==} - '@vueuse/core@11.0.0': - resolution: {integrity: sha512-shibzNGjmRjZucEm97B8V0NO5J3vPHMCE/mltxQ3vHezbDoFQBMtK11XsfwfPionxSbo+buqPmsCljtYuXIBpw==} - '@vueuse/core@12.8.2': resolution: {integrity: sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==} - '@vueuse/core@13.9.0': - resolution: {integrity: sha512-ts3regBQyURfCE2BcytLqzm8+MmLlo5Ln/KLoxDVcsZ2gzIwVNnQpQOL/UKV8alUqjSZOlpFZcRNsLRqj+OzyA==} + '@vueuse/core@14.2.0': + resolution: {integrity: sha512-tpjzVl7KCQNVd/qcaCE9XbejL38V6KJAEq/tVXj7mDPtl6JtzmUdnXelSS+ULRkkrDgzYVK7EerQJvd2jR794Q==} peerDependencies: vue: ^3.5.0 - '@vueuse/integrations@13.9.0': - resolution: {integrity: sha512-SDobKBbPIOe0cVL7QxMzGkuUGHvWTdihi9zOrrWaWUgFKe15cwEcwfWmgrcNzjT6kHnNmWuTajPHoIzUjYNYYQ==} + '@vueuse/integrations@14.2.0': + resolution: {integrity: sha512-Yuo5XbIi6XkfSXOYKd5SBZwyBEyO3Hd41eeG2555hDbE0Maz/P0BfPJDYhgDXjS9xI0jkWUUp1Zh5lXHOgkwLw==} peerDependencies: async-validator: ^4 axios: ^1 change-case: ^5 drauu: ^0.4 - focus-trap: ^7 + focus-trap: ^7 || ^8 fuse.js: ^7 idb-keyval: ^6 jwt-decode: ^4 @@ -4086,23 +4137,17 @@ packages: universal-cookie: optional: true - '@vueuse/metadata@11.0.0': - resolution: {integrity: sha512-0TKsAVT0iUOAPWyc9N79xWYfovJVPATiOPVKByG6jmAYdDiwvMVm9xXJ5hp4I8nZDxpCcYlLq/Rg9w1Z/jrGcg==} - '@vueuse/metadata@12.8.2': resolution: {integrity: sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==} - '@vueuse/metadata@13.9.0': - resolution: {integrity: sha512-1AFRvuiGphfF7yWixZa0KwjYH8ulyjDCC0aFgrGRz8+P4kvDFSdXLVfTk5xAN9wEuD1J6z4/myMoYbnHoX07zg==} - - '@vueuse/shared@11.0.0': - resolution: {integrity: sha512-i4ZmOrIEjSsL94uAEt3hz88UCz93fMyP/fba9S+vypX90fKg3uYX9cThqvWc9aXxuTzR0UGhOKOTQd//Goh1nQ==} + '@vueuse/metadata@14.2.0': + resolution: {integrity: sha512-i3axTGjU8b13FtyR4Keeama+43iD+BwX9C2TmzBVKqjSHArF03hjkp2SBZ1m72Jk2UtrX0aYCugBq2R1fhkuAQ==} '@vueuse/shared@12.8.2': resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==} - '@vueuse/shared@13.9.0': - resolution: {integrity: sha512-e89uuTLMh0U5cZ9iDpEI2senqPGfbPRTHM/0AaQkcxnpqjkZqDYP8rpfm7edOz8s+pOCOROEy1PIveSW8+fL5g==} + '@vueuse/shared@14.2.0': + resolution: {integrity: sha512-Z0bmluZTlAXgUcJ4uAFaML16JcD8V0QG00Db3quR642I99JXIDRa2MI2LGxiLVhcBjVnL1jOzIvT5TT2lqJlkA==} peerDependencies: vue: ^3.5.0 @@ -6192,48 +6237,56 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-gnu@1.30.2: resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.30.1: resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-arm64-musl@1.30.2: resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.30.1: resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-gnu@1.30.2: resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.30.1: resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-linux-x64-musl@1.30.2: resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.30.1: resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} @@ -6926,6 +6979,9 @@ packages: resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} engines: {node: '>= 14.16'} + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + perfect-debounce@2.0.0: resolution: {integrity: sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==} @@ -6949,15 +7005,12 @@ packages: engines: {node: '>=0.10'} hasBin: true - pinia@2.2.2: - resolution: {integrity: sha512-ja2XqFWZC36mupU4z1ZzxeTApV7DOw44cV4dhQ9sGwun+N89v/XP7+j7q6TanS1u1tdbK4r+1BUx7heMaIdagA==} + pinia@3.0.4: + resolution: {integrity: sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==} peerDependencies: - '@vue/composition-api': ^1.4.0 - typescript: '>=4.4.4' - vue: ^2.6.14 || ^3.3.0 + typescript: '>=4.5.0' + vue: ^3.5.11 peerDependenciesMeta: - '@vue/composition-api': - optional: true typescript: optional: true @@ -6967,13 +7020,13 @@ packages: pkg-types@2.3.0: resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} - playwright-core@1.57.0: - resolution: {integrity: sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==} + playwright-core@1.58.1: + resolution: {integrity: sha512-bcWzOaTxcW+VOOGBCQgnaKToLJ65d6AqfLVKEWvexyS3AS6rbXl+xdpYRMGSRBClPvyj44njOWoxjNdL/H9UNg==} engines: {node: '>=18'} hasBin: true - playwright@1.57.0: - resolution: {integrity: sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==} + playwright@1.58.1: + resolution: {integrity: sha512-+2uTZHxSCcxjvGc5C891LrS1/NlxglGxzrC4seZiVjcYVQfUa87wBL6rTDqzGjuoWNjnBzRqKmF6zRYGMvQUaQ==} engines: {node: '>=18'} hasBin: true @@ -7689,6 +7742,7 @@ packages: tar@7.4.3: resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} engines: {node: '>=18'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me terser@5.39.2: resolution: {integrity: sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==} @@ -8165,11 +8219,8 @@ packages: vue-component-type-helpers@2.2.12: resolution: {integrity: sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==} - vue-component-type-helpers@3.2.1: - resolution: {integrity: sha512-gKV7XOkQl4urSuLHNY1tnVQf7wVgtb/mKbRyxSLWGZUY9RK7aDPhBenTjm+i8ZFe0zC2PZeHMPtOZXZfyaFOzQ==} - - vue-component-type-helpers@3.2.2: - resolution: {integrity: sha512-x8C2nx5XlUNM0WirgfTkHjJGO/ABBxlANZDtHw2HclHtQnn+RFPTnbjMJn8jHZW4TlUam0asHcA14lf1C6Jb+A==} + vue-component-type-helpers@3.2.4: + resolution: {integrity: sha512-05lR16HeZDcDpB23ku5b5f1fBOoHqFnMiKRr2CiEvbG5Ux4Yi0McmQBOET0dR0nxDXosxyVqv67q6CzS3AK8rw==} vue-demi@0.14.10: resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} @@ -8196,6 +8247,7 @@ packages: vue-i18n@9.14.3: resolution: {integrity: sha512-C+E0KE8ihKjdYCQx8oUkXX+8tBItrYNMnGJuzEPevBARQFUN2tKez6ZVOvBrWH0+KT5wEk3vOWjNk7ygb2u9ig==} engines: {node: '>= 16'} + deprecated: v9 and v10 no longer supported. please migrate to v11. about maintenance status, see https://vue-i18n.intlify.dev/guide/maintenance.html peerDependencies: vue: ^3.0.0 @@ -8315,8 +8367,8 @@ packages: resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} engines: {node: '>= 0.4'} - which-typed-array@1.1.19: - resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + which-typed-array@1.1.20: + resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} engines: {node: '>= 0.4'} which@1.3.1: @@ -8613,11 +8665,11 @@ snapshots: '@atlaskit/pragmatic-drag-and-drop@1.3.1': dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 bind-event-listener: 3.0.0 raf-schd: 4.0.3 - '@babel/code-frame@7.27.1': + '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 js-tokens: 4.0.0 @@ -8627,7 +8679,7 @@ snapshots: '@babel/core@7.28.5': dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.29.0 '@babel/generator': 7.28.5 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) @@ -9309,19 +9361,19 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/runtime@7.28.4': {} + '@babel/runtime@7.28.6': {} '@babel/standalone@7.28.5': {} '@babel/template@7.27.2': dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.29.0 '@babel/parser': 7.28.5 '@babel/types': 7.28.5 '@babel/traverse@7.28.5': dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.29.0 '@babel/generator': 7.28.5 '@babel/helper-globals': 7.28.0 '@babel/parser': 7.28.5 @@ -10360,7 +10412,7 @@ snapshots: '@babel/plugin-transform-runtime': 7.28.5(@babel/core@7.28.5) '@babel/preset-env': 7.28.5(@babel/core@7.28.5) '@babel/preset-typescript': 7.28.5(@babel/core@7.28.5) - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 '@nx/devkit': 22.2.4(nx@22.2.6) '@nx/workspace': 22.2.4 '@zkochan/js-yaml': 0.0.7 @@ -10396,7 +10448,7 @@ snapshots: '@babel/plugin-transform-runtime': 7.28.5(@babel/core@7.28.5) '@babel/preset-env': 7.28.5(@babel/core@7.28.5) '@babel/preset-typescript': 7.28.5(@babel/core@7.28.5) - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 '@nx/devkit': 22.2.6(nx@22.2.6) '@nx/workspace': 22.2.6 '@zkochan/js-yaml': 0.0.7 @@ -10484,7 +10536,7 @@ snapshots: '@nx/nx-win32-x64-msvc@22.2.6': optional: true - '@nx/playwright@22.2.6(@babel/traverse@7.28.5)(@playwright/test@1.57.0)(@zkochan/js-yaml@0.0.7)(eslint@9.39.1(jiti@2.6.1))(nx@22.2.6)': + '@nx/playwright@22.2.6(@babel/traverse@7.28.5)(@playwright/test@1.58.1)(@zkochan/js-yaml@0.0.7)(eslint@9.39.1(jiti@2.6.1))(nx@22.2.6)': dependencies: '@nx/devkit': 22.2.6(nx@22.2.6) '@nx/eslint': 22.2.6(@babel/traverse@7.28.5)(@zkochan/js-yaml@0.0.7)(eslint@9.39.1(jiti@2.6.1))(nx@22.2.6) @@ -10492,7 +10544,7 @@ snapshots: minimatch: 9.0.3 tslib: 2.8.1 optionalDependencies: - '@playwright/test': 1.57.0 + '@playwright/test': 1.58.1 transitivePeerDependencies: - '@babel/traverse' - '@swc-node/register' @@ -10742,18 +10794,18 @@ snapshots: esquery: 1.6.0 typescript: 5.9.3 - '@pinia/testing@1.0.3(pinia@2.2.2(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3)))': + '@pinia/testing@1.0.3(pinia@3.0.4(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3)))': dependencies: - pinia: 2.2.2(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3)) + pinia: 3.0.4(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3)) '@pkgjs/parseargs@0.11.0': optional: true '@pkgr/core@0.2.9': {} - '@playwright/test@1.57.0': + '@playwright/test@1.58.1': dependencies: - playwright: 1.57.0 + playwright: 1.58.1 '@pnpm/config.env-replace@1.1.0': {} @@ -11086,13 +11138,13 @@ snapshots: - encoding - supports-color - '@sentry/vue@10.32.1(pinia@2.2.2(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3)))(vue@3.5.13(typescript@5.9.3))': + '@sentry/vue@10.32.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3)))(vue@3.5.13(typescript@5.9.3))': dependencies: '@sentry/browser': 10.32.1 '@sentry/core': 10.32.1 vue: 3.5.13(typescript@5.9.3) optionalDependencies: - pinia: 2.2.2(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3)) + pinia: 3.0.4(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3)) '@sinclair/typebox@0.34.40': {} @@ -11199,7 +11251,7 @@ snapshots: storybook: 10.1.9(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) type-fest: 2.19.0 vue: 3.5.13(typescript@5.9.3) - vue-component-type-helpers: 3.2.2 + vue-component-type-helpers: 3.2.4 '@swc/helpers@0.5.17': dependencies: @@ -11292,8 +11344,8 @@ snapshots: '@testing-library/dom@10.4.1': dependencies: - '@babel/code-frame': 7.27.1 - '@babel/runtime': 7.28.4 + '@babel/code-frame': 7.29.0 + '@babel/runtime': 7.28.6 '@types/aria-query': 5.0.4 aria-query: 5.3.0 dom-accessibility-api: 0.5.16 @@ -11609,8 +11661,6 @@ snapshots: '@types/unist@3.0.3': {} - '@types/web-bluetooth@0.0.20': {} - '@types/web-bluetooth@0.0.21': {} '@types/webxr@0.5.20': {} @@ -11975,7 +12025,7 @@ snapshots: '@vue/babel-plugin-resolve-type@1.4.0(@babel/core@7.28.5)': dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.29.0 '@babel/core': 7.28.5 '@babel/helper-module-imports': 7.27.1 '@babel/helper-plugin-utils': 7.27.1 @@ -12062,10 +12112,12 @@ snapshots: de-indent: 1.0.2 he: 1.2.0 - '@vue/devtools-api@6.6.3': {} - '@vue/devtools-api@6.6.4': {} + '@vue/devtools-api@7.7.9': + dependencies: + '@vue/devtools-kit': 7.7.9 + '@vue/devtools-core@8.0.5(vite@8.0.0-beta.8(@types/node@24.10.4)(esbuild@0.27.1)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))': dependencies: '@vue/devtools-kit': 8.0.5 @@ -12090,6 +12142,16 @@ snapshots: transitivePeerDependencies: - vite + '@vue/devtools-kit@7.7.9': + dependencies: + '@vue/devtools-shared': 7.7.9 + birpc: 2.9.0 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.2 + '@vue/devtools-kit@8.0.5': dependencies: '@vue/devtools-shared': 8.0.5 @@ -12100,6 +12162,10 @@ snapshots: speakingurl: 14.0.1 superjson: 2.2.2 + '@vue/devtools-shared@7.7.9': + dependencies: + rfdc: 1.4.1 + '@vue/devtools-shared@8.0.5': dependencies: rfdc: 1.4.1 @@ -12173,16 +12239,6 @@ snapshots: js-beautify: 1.15.1 vue-component-type-helpers: 2.2.12 - '@vueuse/core@11.0.0(vue@3.5.13(typescript@5.9.3))': - dependencies: - '@types/web-bluetooth': 0.0.20 - '@vueuse/metadata': 11.0.0 - '@vueuse/shared': 11.0.0(vue@3.5.13(typescript@5.9.3)) - vue-demi: 0.14.10(vue@3.5.13(typescript@5.9.3)) - transitivePeerDependencies: - - '@vue/composition-api' - - vue - '@vueuse/core@12.8.2(typescript@5.9.3)': dependencies: '@types/web-bluetooth': 0.0.21 @@ -12192,34 +12248,25 @@ snapshots: transitivePeerDependencies: - typescript - '@vueuse/core@13.9.0(vue@3.5.13(typescript@5.9.3))': + '@vueuse/core@14.2.0(vue@3.5.13(typescript@5.9.3))': dependencies: '@types/web-bluetooth': 0.0.21 - '@vueuse/metadata': 13.9.0 - '@vueuse/shared': 13.9.0(vue@3.5.13(typescript@5.9.3)) + '@vueuse/metadata': 14.2.0 + '@vueuse/shared': 14.2.0(vue@3.5.13(typescript@5.9.3)) vue: 3.5.13(typescript@5.9.3) - '@vueuse/integrations@13.9.0(axios@1.13.2)(fuse.js@7.0.0)(vue@3.5.13(typescript@5.9.3))': + '@vueuse/integrations@14.2.0(axios@1.13.2)(fuse.js@7.0.0)(vue@3.5.13(typescript@5.9.3))': dependencies: - '@vueuse/core': 13.9.0(vue@3.5.13(typescript@5.9.3)) - '@vueuse/shared': 13.9.0(vue@3.5.13(typescript@5.9.3)) + '@vueuse/core': 14.2.0(vue@3.5.13(typescript@5.9.3)) + '@vueuse/shared': 14.2.0(vue@3.5.13(typescript@5.9.3)) vue: 3.5.13(typescript@5.9.3) optionalDependencies: axios: 1.13.2 fuse.js: 7.0.0 - '@vueuse/metadata@11.0.0': {} - '@vueuse/metadata@12.8.2': {} - '@vueuse/metadata@13.9.0': {} - - '@vueuse/shared@11.0.0(vue@3.5.13(typescript@5.9.3))': - dependencies: - vue-demi: 0.14.10(vue@3.5.13(typescript@5.9.3)) - transitivePeerDependencies: - - '@vue/composition-api' - - vue + '@vueuse/metadata@14.2.0': {} '@vueuse/shared@12.8.2(typescript@5.9.3)': dependencies: @@ -12227,7 +12274,7 @@ snapshots: transitivePeerDependencies: - typescript - '@vueuse/shared@13.9.0(vue@3.5.13(typescript@5.9.3))': + '@vueuse/shared@14.2.0(vue@3.5.13(typescript@5.9.3))': dependencies: vue: 3.5.13(typescript@5.9.3) @@ -12487,7 +12534,7 @@ snapshots: automation-events@7.1.11: dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 tslib: 2.8.1 available-typed-arrays@1.0.7: @@ -12514,7 +12561,7 @@ snapshots: babel-plugin-macros@3.1.0: dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 cosmiconfig: 7.1.0 resolve: 1.22.11 @@ -12609,7 +12656,7 @@ snapshots: broker-factory@3.1.7: dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 fast-unique-numbers: 9.0.22 tslib: 2.8.1 worker-factory: 7.0.43 @@ -13216,7 +13263,7 @@ snapshots: typed-array-byte-offset: 1.0.4 typed-array-length: 1.0.7 unbox-primitive: 1.1.0 - which-typed-array: 1.1.19 + which-typed-array: 1.1.20 optional: true es-define-property@1.0.1: {} @@ -13554,27 +13601,27 @@ snapshots: extendable-media-recorder-wav-encoder-broker@7.0.119: dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 broker-factory: 3.1.7 extendable-media-recorder-wav-encoder-worker: 8.0.116 tslib: 2.8.1 extendable-media-recorder-wav-encoder-worker@8.0.116: dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 tslib: 2.8.1 worker-factory: 7.0.43 extendable-media-recorder-wav-encoder@7.0.129: dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 extendable-media-recorder-wav-encoder-broker: 7.0.119 extendable-media-recorder-wav-encoder-worker: 8.0.116 tslib: 2.8.1 extendable-media-recorder@9.2.27: dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 media-encoder-host: 9.0.20 multi-buffer-data-view: 6.0.22 recorder-audio-worklet: 6.0.48 @@ -13598,7 +13645,7 @@ snapshots: fast-unique-numbers@9.0.22: dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 tslib: 2.8.1 fast-uri@3.0.3: {} @@ -14181,7 +14228,7 @@ snapshots: is-language-code@3.1.0: dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 is-map@2.0.3: optional: true @@ -14239,7 +14286,7 @@ snapshots: is-typed-array@1.1.15: dependencies: - which-typed-array: 1.1.19 + which-typed-array: 1.1.20 optional: true is-unicode-supported@0.1.0: {} @@ -14852,7 +14899,7 @@ snapshots: media-encoder-host-broker@8.0.19: dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 broker-factory: 3.1.7 fast-unique-numbers: 9.0.22 media-encoder-host-worker: 10.0.19 @@ -14860,14 +14907,14 @@ snapshots: media-encoder-host-worker@10.0.19: dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 extendable-media-recorder-wav-encoder-broker: 7.0.119 tslib: 2.8.1 worker-factory: 7.0.43 media-encoder-host@9.0.20: dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 media-encoder-host-broker: 8.0.19 media-encoder-host-worker: 10.0.19 tslib: 2.8.1 @@ -15154,7 +15201,7 @@ snapshots: multi-buffer-data-view@6.0.22: dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 tslib: 2.8.1 nano-spawn@2.0.0: {} @@ -15509,7 +15556,7 @@ snapshots: parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.29.0 error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -15557,6 +15604,8 @@ snapshots: pathval@2.0.1: {} + perfect-debounce@1.0.0: {} + perfect-debounce@2.0.0: {} picocolors@1.1.1: {} @@ -15569,11 +15618,10 @@ snapshots: pidtree@0.6.0: {} - pinia@2.2.2(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3)): + pinia@3.0.4(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3)): dependencies: - '@vue/devtools-api': 6.6.4 + '@vue/devtools-api': 7.7.9 vue: 3.5.13(typescript@5.9.3) - vue-demi: 0.14.10(vue@3.5.13(typescript@5.9.3)) optionalDependencies: typescript: 5.9.3 @@ -15589,11 +15637,11 @@ snapshots: exsolve: 1.0.8 pathe: 2.0.3 - playwright-core@1.57.0: {} + playwright-core@1.58.1: {} - playwright@1.57.0: + playwright@1.58.1: dependencies: - playwright-core: 1.57.0 + playwright-core: 1.58.1 optionalDependencies: fsevents: 2.3.2 @@ -15930,12 +15978,12 @@ snapshots: recorder-audio-worklet-processor@5.0.35: dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 tslib: 2.8.1 recorder-audio-worklet@6.0.48: dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 broker-factory: 3.1.7 fast-unique-numbers: 9.0.22 recorder-audio-worklet-processor: 5.0.35 @@ -16334,7 +16382,7 @@ snapshots: standardized-audio-context@25.3.77: dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 automation-events: 7.1.11 tslib: 2.8.1 @@ -16497,7 +16545,7 @@ snapshots: subscribable-things@2.1.53: dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 rxjs-interop: 2.0.0 tslib: 2.8.1 @@ -17201,9 +17249,7 @@ snapshots: vue-component-type-helpers@2.2.12: {} - vue-component-type-helpers@3.2.1: {} - - vue-component-type-helpers@3.2.2: {} + vue-component-type-helpers@3.2.4: {} vue-demi@0.14.10(vue@3.5.13(typescript@5.9.3)): dependencies: @@ -17241,7 +17287,7 @@ snapshots: dependencies: '@intlify/core-base': 9.14.3 '@intlify/shared': 9.14.3 - '@vue/devtools-api': 6.6.3 + '@vue/devtools-api': 6.6.4 vue: 3.5.13(typescript@5.9.3) vue-inbrowser-compiler-independent-utils@4.71.1(vue@3.5.13(typescript@5.9.3)): @@ -17250,7 +17296,7 @@ snapshots: vue-router@4.4.3(vue@3.5.13(typescript@5.9.3)): dependencies: - '@vue/devtools-api': 6.6.3 + '@vue/devtools-api': 6.6.4 vue: 3.5.13(typescript@5.9.3) vue-tsc@3.2.1(typescript@5.9.3): @@ -17350,7 +17396,7 @@ snapshots: isarray: 2.0.5 which-boxed-primitive: 1.1.1 which-collection: 1.0.2 - which-typed-array: 1.1.19 + which-typed-array: 1.1.20 optional: true which-collection@1.0.2: @@ -17361,7 +17407,7 @@ snapshots: is-weakset: 2.0.4 optional: true - which-typed-array@1.1.19: + which-typed-array@1.1.20: dependencies: available-typed-arrays: 1.0.7 call-bind: 1.0.8 @@ -17400,7 +17446,7 @@ snapshots: worker-factory@7.0.43: dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.28.6 fast-unique-numbers: 9.0.22 tslib: 2.8.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0c91ec424..89e14f9cb 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -16,7 +16,7 @@ catalog: '@nx/storybook': 22.2.4 '@nx/vite': 22.2.6 '@pinia/testing': ^1.0.3 - '@playwright/test': ^1.57.0 + '@playwright/test': ^1.58.1 '@primeuix/forms': 0.0.2 '@primeuix/styled': 0.3.2 '@primeuix/utils': ^0.3.2 @@ -41,8 +41,8 @@ catalog: '@vitest/coverage-v8': ^4.0.16 '@vitest/ui': ^4.0.16 '@vue/test-utils': ^2.4.6 - '@vueuse/core': ^11.0.0 - '@vueuse/integrations': ^13.9.0 + '@vueuse/core': ^14.2.0 + '@vueuse/integrations': ^14.2.0 '@webgpu/types': ^0.1.66 algoliasearch: ^5.21.0 axios: ^1.8.2 @@ -62,8 +62,8 @@ catalog: happy-dom: ^20.0.11 husky: ^9.1.7 jiti: 2.6.1 - jsonata: ^2.1.0 jsdom: ^27.4.0 + jsonata: ^2.1.0 knip: ^5.75.1 lint-staged: ^16.2.7 markdown-table: ^3.0.4 diff --git a/src/components/bottomPanel/tabs/shortcuts/ShortcutsList.vue b/src/components/bottomPanel/tabs/shortcuts/ShortcutsList.vue index d8992332b..3938facfb 100644 --- a/src/components/bottomPanel/tabs/shortcuts/ShortcutsList.vue +++ b/src/components/bottomPanel/tabs/shortcuts/ShortcutsList.vue @@ -1,6 +1,9 @@ - diff --git a/src/components/graph/CanvasModeSelector.vue b/src/components/graph/CanvasModeSelector.vue index e6c78fd1a..48efb50b1 100644 --- a/src/components/graph/CanvasModeSelector.vue +++ b/src/components/graph/CanvasModeSelector.vue @@ -55,6 +55,7 @@