From 9b4d8df259cd97e1e2ca43f79c66fc1abd49bb20 Mon Sep 17 00:00:00 2001 From: jaeone94 <89377375+jaeone94@users.noreply.github.com> Date: Thu, 7 May 2026 13:34:05 +0900 Subject: [PATCH] test: cover primevue reka overlay z-index --- .../tests/primevueRekaOverlayZIndex.spec.ts | 575 ++++++++++++++++++ 1 file changed, 575 insertions(+) create mode 100644 browser_tests/tests/primevueRekaOverlayZIndex.spec.ts diff --git a/browser_tests/tests/primevueRekaOverlayZIndex.spec.ts b/browser_tests/tests/primevueRekaOverlayZIndex.spec.ts new file mode 100644 index 0000000000..cb8280c8bf --- /dev/null +++ b/browser_tests/tests/primevueRekaOverlayZIndex.spec.ts @@ -0,0 +1,575 @@ +import type { Locator, Page } from '@playwright/test' +import { expect } from '@playwright/test' + +import type { AlgoliaNodePack } from '@/types/algoliaTypes' +import type { + AssetMetadata, + AssetItem, + ModelFolder +} from '@/platform/assets/schemas/assetSchema' +import type { components as ManagerComponents } from '@/workbench/extensions/manager/types/generatedManagerTypes' +import type { components as RegistryComponents } from '@comfyorg/registry-types' +import { + STABLE_CHECKPOINT, + STABLE_LORA, + STABLE_VAE +} from '@e2e/fixtures/data/assetFixtures' +import { + makeTemplate, + mockTemplateIndex +} from '@e2e/fixtures/data/templateFixtures' +import { mockSystemStats } from '@e2e/fixtures/data/systemStats' +import type { ComfyPage } from '@e2e/fixtures/ComfyPage' +import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage' +import { TestIds } from '@e2e/fixtures/selectors' + +type InstalledPacksResponse = + ManagerComponents['schemas']['InstalledPacksResponse'] +type RegistryNodePack = RegistryComponents['schemas']['Node'] + +interface AlgoliaSearchResult { + hits: Array | AlgoliaSuggestionHit> + nbHits: number + page: number + nbPages: number + hitsPerPage: number +} + +interface AlgoliaSearchResponse { + results: AlgoliaSearchResult[] +} + +interface AlgoliaSuggestionHit { + query: string + popularity: number +} + +const RAISED_DIALOG_Z_INDEX = 4200 + +const MODEL_ASSETS: AssetItem[] = [ + { + ...STABLE_CHECKPOINT, + id: 'fe-569-checkpoint', + tags: STABLE_CHECKPOINT.tags ?? [], + is_immutable: false + }, + { + ...STABLE_LORA, + id: 'fe-569-lora', + tags: STABLE_LORA.tags ?? [], + is_immutable: false + }, + { + ...STABLE_VAE, + id: 'fe-569-vae', + tags: STABLE_VAE.tags ?? [], + is_immutable: false + } +] + +const MODEL_FOLDERS: ModelFolder[] = [ + { name: 'checkpoints', folders: [] }, + { name: 'loras', folders: [] }, + { name: 'vae', folders: [] } +] + +const MOCK_PACK_A: RegistryNodePack = { + id: 'test-pack-a', + name: 'Test Pack A', + description: 'A test custom node pack', + downloads: 5000, + status: 'NodeStatusActive', + publisher: { id: 'test-publisher', name: 'Test Publisher' }, + latest_version: { version: '1.0.0', status: 'NodeVersionStatusActive' }, + repository: 'https://github.com/test/pack-a', + tags: ['image', 'processing'] +} + +const MOCK_PACK_B: RegistryNodePack = { + id: 'test-pack-b', + name: 'Test Pack B', + description: 'Another test custom node pack for testing search', + downloads: 3000, + status: 'NodeStatusActive', + publisher: { id: 'another-publisher', name: 'Another Publisher' }, + latest_version: { version: '2.1.0', status: 'NodeVersionStatusActive' }, + repository: 'https://github.com/test/pack-b', + tags: ['video', 'generation'] +} + +const MOCK_INSTALLED_PACKS: InstalledPacksResponse = { + 'test-pack-a': { + ver: '1.0.0', + cnr_id: 'test-pack-a', + enabled: true + } +} + +const MOCK_HIT_A: Partial = { + objectID: 'test-pack-a', + id: 'test-pack-a', + name: 'Test Pack A', + description: 'A test custom node pack', + total_install: 5000, + status: 'NodeStatusActive', + publisher_id: 'test-publisher', + latest_version: '1.0.0', + latest_version_status: 'NodeVersionStatusActive', + repository_url: 'https://github.com/test/pack-a', + comfy_nodes: ['TestNodeA'], + create_time: '2024-01-01T00:00:00Z', + update_time: '2024-06-01T00:00:00Z', + license: 'MIT', + tags: ['image', 'processing'] +} + +const MOCK_HIT_B: Partial = { + objectID: 'test-pack-b', + id: 'test-pack-b', + name: 'Test Pack B', + description: 'Another test custom node pack', + total_install: 3000, + status: 'NodeStatusActive', + publisher_id: 'another-publisher', + latest_version: '2.1.0', + latest_version_status: 'NodeVersionStatusActive', + repository_url: 'https://github.com/test/pack-b', + comfy_nodes: ['TestNodeB'], + create_time: '2024-02-01T00:00:00Z', + update_time: '2024-07-01T00:00:00Z', + license: 'Apache-2.0', + tags: ['video', 'generation'] +} + +const MOCK_ALGOLIA_RESPONSE: AlgoliaSearchResponse = { + results: [ + { + hits: [MOCK_HIT_A, MOCK_HIT_B], + nbHits: 2, + page: 0, + nbPages: 1, + hitsPerPage: 20 + }, + { + hits: [{ query: 'Test Pack A', popularity: 100 }], + nbHits: 1, + page: 0, + nbPages: 1, + hitsPerPage: 20 + } + ] +} + +test.use({ + initialFeatureFlags: { + model_upload_button_enabled: true, + private_models_enabled: true + } +}) + +test.describe('PrimeVue dialog child overlays', () => { + test('keeps workflow template filters above the template dialog', async ({ + comfyPage + }) => { + await mockTemplateLibrary(comfyPage) + await forcePrimeVueDialogZIndex(comfyPage.page) + + await comfyPage.command.executeCommand('Comfy.BrowseTemplates') + const dialog = comfyPage.templatesDialog.root + await expect(comfyPage.templates.content).toBeVisible() + + await dialog.getByRole('button', { name: /Model Filter/ }).click() + await expectOverlayItemAboveRaisedDialog( + comfyPage.page.getByRole('option', { name: 'Flux' }) + ) + await comfyPage.page.keyboard.press('Escape') + + await dialog.getByRole('combobox', { name: /Sort by/ }).click() + await expectOverlayItemAboveRaisedDialog( + comfyPage.page.getByRole('option', { name: 'Popular' }) + ) + }) + + test('keeps manager header controls above the manager dialog', async ({ + comfyPage + }) => { + await setupManagerDialog(comfyPage) + await forcePrimeVueDialogZIndex(comfyPage.page) + await comfyPage.command.executeCommand('Comfy.OpenManagerDialog') + + const dialog = comfyPage.page.getByRole('dialog').last() + await expect(dialog).toBeVisible() + + await dialog.getByText('Node Pack').first().click() + await expectOverlayItemAboveRaisedDialog( + comfyPage.page.getByRole('option', { name: 'Nodes' }) + ) + await comfyPage.page.getByRole('option', { name: 'Nodes' }).click() + + await dialog.getByRole('combobox', { name: 'Sort' }).click() + await expectOverlayItemAboveRaisedDialog( + comfyPage.page.getByRole('option', { name: 'Name' }) + ) + }) + + test('keeps asset browser filters above the asset browser dialog', async ({ + comfyPage + }) => { + const dialog = await openAssetBrowser(comfyPage) + + await dialog.getByRole('button', { name: 'File formats' }).click() + await expectOverlayItemAboveRaisedDialog( + comfyPage.page.getByRole('option', { name: '.safetensors' }) + ) + await comfyPage.page.keyboard.press('Escape') + + await dialog.getByRole('combobox', { name: 'Sort by' }).click() + await expectOverlayItemAboveRaisedDialog( + comfyPage.page.getByRole('option', { name: 'A-Z' }) + ) + }) + + test('keeps the model info selector above the asset browser dialog', async ({ + comfyPage + }) => { + const dialog = await openAssetBrowser(comfyPage) + const card = dialog + .locator('[data-component-id="AssetCard"]') + .filter({ hasText: STABLE_CHECKPOINT.name }) + .first() + + await card.hover() + await card.getByRole('button', { name: 'Model Info' }).click() + + const modelInfoPanel = dialog.locator( + '[data-component-id="ModelInfoPanel"]' + ) + await expect(modelInfoPanel).toBeVisible() + + await modelInfoPanel.getByRole('combobox').click() + await expectOverlayItemAboveRaisedDialog( + comfyPage.page.getByRole('option', { name: 'LoRA' }) + ) + }) + + test('keeps the upload model selector above the upload dialog', async ({ + comfyPage + }) => { + const assetBrowserDialog = await openAssetBrowser(comfyPage) + await mockRemoteModelMetadata(comfyPage.page) + + await assetBrowserDialog + .locator('[data-attr="upload-model-button"]') + .click() + + const uploadDialog = comfyPage.page.getByRole('dialog').last() + await expect(uploadDialog).toContainText('Import a model') + + await uploadDialog + .locator('[data-attr="upload-model-step1-url-input"]') + .fill('https://civitai.com/models/123/fe-569-test') + await uploadDialog + .locator('[data-attr="upload-model-step1-continue-button"]') + .click() + + const trigger = uploadDialog.locator( + '[data-attr="upload-model-step2-type-selector"]' + ) + await expect(trigger).toBeVisible() + await trigger.click() + + await expectOverlayItemAboveRaisedDialog( + comfyPage.page.getByRole('option', { name: 'LoRA' }) + ) + }) + + test('keeps keybinding controls above the settings dialog', async ({ + comfyPage + }) => { + await registerNoBindingCommand(comfyPage) + await forcePrimeVueDialogZIndex(comfyPage.page) + await comfyPage.settingDialog.open() + await comfyPage.settingDialog.category('Keybinding').click() + + const dialog = comfyPage.settingDialog.root + await expect( + comfyPage.page.getByPlaceholder('Search Keybindings...') + ).toBeVisible() + + await dialog + .locator('#keybinding-panel-actions') + .locator('button[role="combobox"]') + .click() + await expectOverlayItemAboveRaisedDialog( + comfyPage.page.getByRole('option', { name: 'Default Preset' }) + ) + await comfyPage.page.getByRole('option', { name: 'Default Preset' }).click() + + await searchKeybindings(comfyPage.page, 'Comfy.SaveWorkflow') + const saveWorkflowCommand = /^Comfy\.SaveWorkflow$/ + await getCommandRow(comfyPage.page, saveWorkflowCommand) + .getByTitle(saveWorkflowCommand) + .click({ button: 'right' }) + await expectOverlayItemAboveRaisedDialog( + comfyPage.page.getByRole('menuitem', { name: /Change keybinding/i }) + ) + + await comfyPage.page.getByTestId(TestIds.keybindings.presetMenu).click() + await expectOverlayItemAboveRaisedDialog( + comfyPage.page.getByRole('menuitem', { name: 'Import preset' }) + ) + }) +}) + +async function forcePrimeVueDialogZIndex(page: Page) { + await page.addStyleTag({ + content: ` + .p-dialog-mask { + z-index: ${RAISED_DIALOG_Z_INDEX} !important; + } + ` + }) +} + +async function expectOverlayItemAboveRaisedDialog(item: Locator) { + await expect(item).toBeVisible() + + const overlay = item.locator( + 'xpath=ancestor-or-self::*[contains(@style, "z-index")][1]' + ) + await expect(overlay).toHaveCount(1) + + const overlayZIndex = await readZIndex(overlay) + + expect(overlayZIndex).toBeGreaterThan(RAISED_DIALOG_Z_INDEX) +} + +async function readZIndex(locator: Locator): Promise { + const rawZIndex = await locator.evaluate( + (element) => getComputedStyle(element).zIndex + ) + const zIndex = Number.parseInt(rawZIndex, 10) + + expect(Number.isFinite(zIndex)).toBe(true) + + return zIndex +} + +async function mockTemplateLibrary(comfyPage: ComfyPage) { + await comfyPage.settings.setSetting('Comfy.Templates.SelectedModels', []) + await comfyPage.settings.setSetting('Comfy.Templates.SelectedUseCases', []) + await comfyPage.settings.setSetting('Comfy.Templates.SelectedRunsOn', []) + await comfyPage.settings.setSetting('Comfy.Templates.SortBy', 'default') + + await comfyPage.page.route('**/templates/**.webp', async (route) => { + await route.fulfill({ + status: 200, + path: 'browser_tests/assets/example.webp', + headers: { + 'Content-Type': 'image/webp', + 'Cache-Control': 'no-store' + } + }) + }) + + await comfyPage.page.route('**/templates/index.json', async (route) => { + await route.fulfill({ + status: 200, + body: JSON.stringify( + mockTemplateIndex([ + makeTemplate({ + name: 'flux-template', + title: 'Flux Template', + models: ['Flux'], + tags: ['Image'] + }), + makeTemplate({ + name: 'wan-template', + title: 'Wan Template', + models: ['Wan 2.2'], + tags: ['Video'], + openSource: false + }) + ]) + ), + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-store' + } + }) + }) +} + +async function setupManagerDialog(comfyPage: ComfyPage) { + const statsWithManager = { + ...mockSystemStats, + system: { + ...mockSystemStats.system, + argv: ['main.py', '--enable-manager'] + } + } + + await comfyPage.page.route('**/system_stats**', async (route) => { + await route.fulfill({ json: statsWithManager }) + }) + + await comfyPage.page.route('**/v2/customnode/installed**', async (route) => { + await route.fulfill({ json: MOCK_INSTALLED_PACKS }) + }) + + await comfyPage.page.route('**/v2/manager/queue/status**', async (route) => { + await route.fulfill({ + json: { + history: {}, + running_queue: [], + pending_queue: [], + installed_packs: {} + } + }) + }) + + await comfyPage.page.route('**/v2/manager/queue/history**', async (route) => { + await route.fulfill({ json: {} }) + }) + + await comfyPage.page.route('**/*.algolia.net/**', async (route) => { + await route.fulfill({ json: MOCK_ALGOLIA_RESPONSE }) + }) + await comfyPage.page.route('**/*.algolianet.com/**', async (route) => { + await route.fulfill({ json: MOCK_ALGOLIA_RESPONSE }) + }) + + const registryListResponse = { + total: 2, + nodes: [MOCK_PACK_A, MOCK_PACK_B], + page: 1, + limit: 64, + totalPages: 1 + } + + await comfyPage.page.route( + '**/api.comfy.org/nodes/search**', + async (route) => { + await route.fulfill({ json: registryListResponse }) + } + ) + + await comfyPage.page.route( + (url) => url.hostname === 'api.comfy.org' && url.pathname === '/nodes', + async (route) => { + await route.fulfill({ json: registryListResponse }) + } + ) + + await comfyPage.page.route( + '**/v2/customnode/getmappings**', + async (route) => { + await route.fulfill({ json: {} }) + } + ) + + await comfyPage.page.route( + '**/v2/customnode/import_fail_info**', + async (route) => { + await route.fulfill({ json: {} }) + } + ) + + await comfyPage.setup() + + await comfyPage.page.evaluate(() => { + const api = window.app!.api + api.serverFeatureFlags.value = { + ...api.serverFeatureFlags.value, + extension: { + manager: { + supports_v4: true, + supports_csrf_post: true + } + } + } + }) +} + +async function openAssetBrowser(comfyPage: ComfyPage): Promise { + await mockModelFolders(comfyPage.page) + await forcePrimeVueDialogZIndex(comfyPage.page) + await showAssetBrowserModal(comfyPage.page) + + const assetBrowser = comfyPage.page.locator( + '[data-component-id="AssetBrowserModal"]' + ) + await expect(assetBrowser).toBeVisible() + await expect( + assetBrowser.getByRole('heading', { name: STABLE_CHECKPOINT.name }) + ).toBeVisible() + + return assetBrowser.locator('xpath=ancestor::*[@role="dialog"][1]') +} + +async function showAssetBrowserModal(page: Page) { + await page.evaluate((assets) => { + Object.assign(window, { __fe569ModelAssets: assets }) + }, MODEL_ASSETS) + + await page.addScriptTag({ + type: 'module', + content: ` + const { useDialogService } = await import('/src/services/dialogService.ts') + const AssetBrowserModal = (await import('/src/platform/assets/components/AssetBrowserModal.vue')).default + + useDialogService().showLayoutDialog({ + key: 'fe-569-asset-browser', + component: AssetBrowserModal, + props: { + showLeftPanel: true, + assetType: 'models', + title: 'Model Library', + overrideAssets: window.__fe569ModelAssets, + onClose: () => {} + } + }) + ` + }) +} + +async function mockModelFolders(page: Page) { + await page.route('**/experiment/models', async (route) => { + await route.fulfill({ json: MODEL_FOLDERS }) + }) +} + +async function mockRemoteModelMetadata(page: Page) { + const metadata: AssetMetadata = { + content_length: 1234, + final_url: 'https://civitai.com/api/download/models/123', + filename: 'fe-569-upload-test.safetensors', + tags: [] + } + + await page.route('**/assets/remote-metadata**', async (route) => { + await route.fulfill({ json: metadata }) + }) +} + +async function registerNoBindingCommand(comfyPage: ComfyPage) { + await comfyPage.page.evaluate(() => { + const app = window.app! + app.registerExtension({ + name: 'TestExtension.PrimeVueRekaOverlayZIndex', + commands: [ + { id: 'TestCommand.PrimeVueRekaOverlay.NoBinding', function: () => {} } + ] + }) + }) +} + +async function searchKeybindings(page: Page, query: string) { + await page.getByPlaceholder('Search Keybindings...').fill(query) +} + +function getCommandRow(page: Page, commandTitle: RegExp): Locator { + return page + .locator('.keybinding-panel tr') + .filter({ has: page.getByTitle(commandTitle) }) +}