diff --git a/browser_tests/fixtures/components/SidebarTab.ts b/browser_tests/fixtures/components/SidebarTab.ts index 3db6d04bc1..bcece779a3 100644 --- a/browser_tests/fixtures/components/SidebarTab.ts +++ b/browser_tests/fixtures/components/SidebarTab.ts @@ -322,6 +322,9 @@ export class AssetsSidebarTab extends SidebarTab { // --- Folder view --- public readonly backToAssetsButton: Locator + // --- Panel chrome --- + public readonly panelHeader: Locator + // --- Loading --- public readonly skeletonLoaders: Locator @@ -358,6 +361,7 @@ export class AssetsSidebarTab extends SidebarTab { this.deleteSelectedButton = page.getByTestId('assets-delete-selected') this.downloadSelectedButton = page.getByTestId('assets-download-selected') this.backToAssetsButton = page.getByText('Back to all assets') + this.panelHeader = page.locator('.comfy-vue-side-bar-header') this.skeletonLoaders = page.locator( '.sidebar-content-container .animate-pulse' ) diff --git a/browser_tests/tests/sidebar/assetsSidebarTab.spec.ts b/browser_tests/tests/sidebar/assetsSidebarTab.spec.ts index becfffc4cb..30fb7ec8af 100644 --- a/browser_tests/tests/sidebar/assetsSidebarTab.spec.ts +++ b/browser_tests/tests/sidebar/assetsSidebarTab.spec.ts @@ -276,3 +276,255 @@ test.describe('FE-130 assets sidebar route mocks', () => { ) }) }) + +test.describe('FE-910 marquee selection and select all', () => { + test.beforeEach(async ({ jobsRoutes, page, comfyPage }) => { + await jobsRoutes.mockJobsQueue([]) + await jobsRoutes.mockJobsHistory(generatedJobs) + await mockInputFiles(page, ['imported.png']) + await mockViewFiles(page, viewFiles) + await comfyPage.setup() + await comfyPage.menu.assetsTab.open() + }) + + test('Ctrl/Cmd+A selects every asset while the panel is hovered', async ({ + comfyPage + }) => { + const tab = comfyPage.menu.assetsTab + + await expect(tab.assetCards).toHaveCount(2) + + await tab.getAssetCardByName('alpha').hover() + await comfyPage.page.keyboard.press('ControlOrMeta+a') + + await expect(tab.selectedCards).toHaveCount(2) + }) + + test('a marquee that begins in the panel header selects the cards', async ({ + comfyPage + }) => { + const tab = comfyPage.menu.assetsTab + const { page } = comfyPage + + await expect(tab.assetCards).toHaveCount(2) + await expect(tab.selectedCards).toHaveCount(0) + + const header = await tab.panelHeader.boundingBox() + const beta = await tab.getAssetCardByName('beta').boundingBox() + if (!header || !beta) { + throw new Error('panel header or asset card has no layout box') + } + + // Begin the rubber-band in the header (above the grid), then drag down + // across both cards. + await page.mouse.move(header.x + 24, header.y + 20) + await page.mouse.down() + await page.mouse.move(beta.x + 8, beta.y + beta.height - 8, { steps: 14 }) + await page.mouse.up() + + await expect(tab.selectedCards).toHaveCount(2) + await expect(tab.selectionFooter).toBeVisible() + }) + + test('Ctrl/Cmd+A leaves assets unselected while the canvas is hovered', async ({ + comfyPage + }) => { + const tab = comfyPage.menu.assetsTab + const { page } = comfyPage + + await expect(tab.assetCards).toHaveCount(2) + + const viewport = page.viewportSize() + if (!viewport) throw new Error('viewport size is unavailable') + + // Hover the canvas (not the panel); Ctrl/Cmd+A must yield to the canvas. + await page.mouse.move(viewport.width - 100, viewport.height / 2) + await page.keyboard.press('ControlOrMeta+a') + + await expect(tab.selectedCards).toHaveCount(0) + }) + + test('a modifier-held marquee adds to the existing selection', async ({ + comfyPage + }) => { + const tab = comfyPage.menu.assetsTab + const { page } = comfyPage + + await expect(tab.assetCards).toHaveCount(2) + + await tab.getAssetCardByName('alpha').click() + await expect(tab.selectedCards).toHaveCount(1) + + const beta = await tab.getAssetCardByName('beta').boundingBox() + if (!beta) throw new Error('beta card has no layout box') + + // Hold a modifier so the marquee is additive, then rubber-band over beta. + await page.keyboard.down('Control') + await page.mouse.move(beta.x + 12, beta.y + 12) + await page.mouse.down() + await page.mouse.move(beta.x + beta.width - 12, beta.y + beta.height - 12, { + steps: 12 + }) + await page.mouse.up() + await page.keyboard.up('Control') + + await expect(tab.selectedCards).toHaveCount(2) + }) + + test('a Ctrl/Cmd+Shift marquee removes the covered cards from the selection', async ({ + comfyPage + }) => { + const tab = comfyPage.menu.assetsTab + const { page } = comfyPage + + await expect(tab.assetCards).toHaveCount(2) + + await tab.getAssetCardByName('alpha').hover() + await page.keyboard.press('ControlOrMeta+a') + await expect(tab.selectedCards).toHaveCount(2) + + const beta = await tab.getAssetCardByName('beta').boundingBox() + if (!beta) throw new Error('beta card has no layout box') + + // Ctrl+Shift makes the marquee subtractive: rubber-band over beta only. + await page.keyboard.down('Control') + await page.keyboard.down('Shift') + await page.mouse.move(beta.x + 12, beta.y + 12) + await page.mouse.down() + await page.mouse.move(beta.x + beta.width - 12, beta.y + beta.height - 12, { + steps: 12 + }) + await page.mouse.up() + await page.keyboard.up('Shift') + await page.keyboard.up('Control') + + await expect(tab.selectedCards).toHaveCount(1) + await expect(tab.getAssetCardByName('alpha')).toHaveAttribute( + 'data-selected', + 'true' + ) + }) + + test('Ctrl/Cmd-dragging from an asset card starts a marquee selection', async ({ + comfyPage + }) => { + const tab = comfyPage.menu.assetsTab + const { page } = comfyPage + + await expect(tab.assetCards).toHaveCount(2) + await expect(tab.selectedCards).toHaveCount(0) + + const alpha = await tab.getAssetCardByName('alpha').boundingBox() + const beta = await tab.getAssetCardByName('beta').boundingBox() + if (!alpha || !beta) throw new Error('asset cards have no layout box') + + // Ctrl bypasses card drag, so a press that begins on a card rubber-bands. + await page.keyboard.down('Control') + await page.mouse.move(alpha.x + alpha.width / 2, alpha.y + alpha.height / 2) + await page.mouse.down() + await page.mouse.move(beta.x + beta.width - 6, beta.y + beta.height - 6, { + steps: 12 + }) + await page.mouse.up() + await page.keyboard.up('Control') + + await expect(tab.selectedCards).toHaveCount(2) + await expect(tab.selectionFooter).toBeVisible() + }) + + test('Ctrl/Cmd-dragging within a single card selects only that card', async ({ + comfyPage + }) => { + const tab = comfyPage.menu.assetsTab + const { page } = comfyPage + + await expect(tab.assetCards).toHaveCount(2) + + const alpha = tab.getAssetCardByName('alpha') + const box = await alpha.boundingBox() + if (!box) throw new Error('alpha card has no layout box') + + const start = { x: box.x + box.width / 2, y: box.y + box.height / 2 } + await page.keyboard.down('Control') + await page.mouse.move(start.x, start.y) + await page.mouse.down() + await page.mouse.move(start.x + 12, start.y + 12, { steps: 4 }) + await page.mouse.up() + await page.keyboard.up('Control') + + await expect(tab.selectedCards).toHaveCount(1) + await expect(alpha).toHaveAttribute('data-selected', 'true') + }) + + test('Ctrl/Cmd+A in the focused search input does not select assets', async ({ + comfyPage + }) => { + const tab = comfyPage.menu.assetsTab + const query = 'alpha' + + await tab.searchInput.fill(query) + await expect(tab.assetCards).toHaveCount(1) + + await tab.searchInput.focus() + await comfyPage.page.keyboard.press('ControlOrMeta+a') + + await expect(tab.selectedCards).toHaveCount(0) + await expect + .poll(() => + tab.searchInput.evaluate((el: HTMLInputElement) => { + return { start: el.selectionStart, end: el.selectionEnd } + }) + ) + .toEqual({ start: 0, end: query.length }) + }) + + test('a drag starting in the search input does not marquee-select assets', async ({ + comfyPage + }) => { + const tab = comfyPage.menu.assetsTab + const { page } = comfyPage + + await expect(tab.assetCards).toHaveCount(2) + + const search = await tab.searchInput.boundingBox() + const beta = await tab.getAssetCardByName('beta').boundingBox() + if (!search || !beta) + throw new Error('search box or card has no layout box') + + await page.mouse.move( + search.x + search.width / 2, + search.y + search.height / 2 + ) + await page.mouse.down() + await page.mouse.move(beta.x + beta.width / 2, beta.y + beta.height / 2, { + steps: 12 + }) + await page.mouse.up() + + await expect(tab.selectedCards).toHaveCount(0) + }) + + test('Ctrl/Cmd+A does not select assets while an aria-modal dialog is open', async ({ + comfyPage + }) => { + const tab = comfyPage.menu.assetsTab + await expect(tab.assetCards).toHaveCount(2) + + await comfyPage.page.evaluate(() => { + const dialog = document.createElement('div') + dialog.id = 'test-modal' + dialog.setAttribute('role', 'dialog') + dialog.setAttribute('aria-modal', 'true') + document.body.appendChild(dialog) + }) + + await tab.getAssetCardByName('alpha').hover() + await comfyPage.page.keyboard.press('ControlOrMeta+a') + + await expect(tab.selectedCards).toHaveCount(0) + + await comfyPage.page.evaluate(() => { + document.getElementById('test-modal')?.remove() + }) + }) +}) diff --git a/src/components/graph/GraphCanvas.vue b/src/components/graph/GraphCanvas.vue index 0d46429c1f..426e6a44fb 100644 --- a/src/components/graph/GraphCanvas.vue +++ b/src/components/graph/GraphCanvas.vue @@ -39,6 +39,10 @@