Compare commits

...

5 Commits

Author SHA1 Message Date
Matt Miller
c6fbac4728 test: disambiguate missing-node locators after locate-button label change
The extracted LocateNodeButton now carries a per-item accessible name
("Locate {item}"), so getByRole('button', { name }) matched both the row
label button and the locate button, tripping Playwright strict mode. Use
exact matching to target the row label button.
2026-07-13 20:47:02 -07:00
Matt Miller
351f7e84b3 test: align MissingPackGroupRow locate assertions with per-item labels
The LocateNodeButton extraction switched the locate button aria-label
from the static "Locate node on canvas" to the per-item
`rightSidePanel.locateNodeFor` ("Locate {item}"), but these test queries
were not updated, so they failed to find the button.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 19:48:14 -07:00
Matt Miller
4f617f6d13 test: make LocateNodeButton propagation test non-vacuous; per-node locate labels
- Replace closure-captured counter with vi.fn() spy in the click-propagation
  test so it actually fails when @click.stop is removed
- Add keyboard-activation coverage and pin aria-label via DOM assertion
- Give each MissingPackGroupRow locate button a distinct accessible name
  via locateNodeFor, matching ErrorGroupList (WCAG 2.4.6)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 19:29:07 -07:00
Matt Miller
13b5cf41e0 Merge branch 'main' into matt/be-2256-locate-node-button 2026-07-13 22:21:37 -04:00
Matt Miller
5c32def511 refactor: extract LocateNodeButton for errors panel
Deduplicate the byte-identical locate icon-button repeated across four
sites in the errors panel (ErrorNodeCard, TabErrors, MissingPackGroupRow
x2) into a single LocateNodeButton component. Standardizes the decorative
icon's aria-hidden and the click .stop modifier, which previously varied
between sites.
2026-07-08 13:27:15 -07:00
7 changed files with 125 additions and 52 deletions

View File

@@ -40,7 +40,7 @@ test.describe('Errors tab - Missing nodes', { tag: ['@ui', '@canvas'] }, () => {
)
await expect(missingNodeCard.getByText('Unknown pack')).toBeVisible()
await expect(
missingNodeCard.getByRole('button', { name: 'UNKNOWN NODE' })
missingNodeCard.getByRole('button', { name: 'UNKNOWN NODE', exact: true })
).toBeVisible()
})
@@ -57,7 +57,8 @@ test.describe('Errors tab - Missing nodes', { tag: ['@ui', '@canvas'] }, () => {
)
await expect(
missingNodeCard.getByRole('button', {
name: 'MISSING_NODE_TYPE_IN_SUBGRAPH'
name: 'MISSING_NODE_TYPE_IN_SUBGRAPH',
exact: true
})
).toBeVisible()
})
@@ -73,7 +74,9 @@ test.describe('Errors tab - Missing nodes', { tag: ['@ui', '@canvas'] }, () => {
await comfyPage.canvasOps.pan({ x: -800, y: -800 })
const offsetBeforeLocate = await comfyPage.canvasOps.getOffset()
await missingNodeCard.getByRole('button', { name: 'UNKNOWN NODE' }).click()
await missingNodeCard
.getByRole('button', { name: 'UNKNOWN NODE', exact: true })
.click()
await expect
.poll(() => comfyPage.canvasOps.getOffset())
@@ -98,10 +101,12 @@ test.describe('Errors tab - Missing nodes', { tag: ['@ui', '@canvas'] }, () => {
TestIds.dialogs.missingNodePackExpand
)
const firstNode = missingNodeCard.getByRole('button', {
name: 'TEST_MISSING_PACK_NODE_A'
name: 'TEST_MISSING_PACK_NODE_A',
exact: true
})
const secondNode = missingNodeCard.getByRole('button', {
name: 'TEST_MISSING_PACK_NODE_B'
name: 'TEST_MISSING_PACK_NODE_B',
exact: true
})
await expect(packTitle).toBeVisible()

View File

@@ -248,19 +248,14 @@
<i class="icon-[lucide--info] size-3.5" />
</Button>
</span>
<Button
variant="textonly"
size="icon-sm"
class="size-8 shrink-0 text-muted-foreground hover:text-base-foreground focus-visible:ring-inset"
:aria-label="
<LocateNodeButton
:label="
t('rightSidePanel.locateNodeFor', {
item: item.label
})
"
@click.stop="handleLocateNode(item.nodeId)"
>
<i class="icon-[lucide--locate] size-4" />
</Button>
@locate="handleLocateNode(item.nodeId)"
/>
</div>
<TransitionCollapse>
<p
@@ -332,6 +327,7 @@ import TransitionCollapse from '../layout/TransitionCollapse.vue'
import AsyncSearchInput from '@/components/ui/search-input/AsyncSearchInput.vue'
import ErrorCardSection from './ErrorCardSection.vue'
import ErrorNodeCard from './ErrorNodeCard.vue'
import LocateNodeButton from './LocateNodeButton.vue'
import MissingNodeCard from './MissingNodeCard.vue'
import SwapNodesCard from '@/platform/nodeReplacement/components/SwapNodesCard.vue'
import MissingModelCard from '@/platform/missingModel/components/MissingModelCard.vue'

View File

@@ -36,15 +36,10 @@
>
<i class="icon-[lucide--monitor-x] size-4" />
</Button>
<Button
variant="textonly"
size="icon-sm"
class="size-8 shrink-0 text-muted-foreground hover:text-base-foreground focus-visible:ring-inset"
:aria-label="t('rightSidePanel.locateNode')"
@click.stop="handleLocateNode"
>
<i class="icon-[lucide--locate] size-4" />
</Button>
<LocateNodeButton
:label="t('rightSidePanel.locateNode')"
@locate="handleLocateNode"
/>
</div>
</div>
@@ -177,6 +172,7 @@ import { useI18n } from 'vue-i18n'
import Button from '@/components/ui/button/Button.vue'
import { cn } from '@comfyorg/tailwind-utils'
import LocateNodeButton from './LocateNodeButton.vue'
import TransitionCollapse from '../layout/TransitionCollapse.vue'
import type { ErrorCardData, ErrorItem } from './types'

View File

@@ -0,0 +1,57 @@
import { render, screen } from '@testing-library/vue'
import userEvent from '@testing-library/user-event'
import { describe, expect, it, vi } from 'vitest'
import LocateNodeButton from '@/components/rightSidePanel/errors/LocateNodeButton.vue'
describe('LocateNodeButton', () => {
it('exposes the label as the button aria-label', () => {
render(LocateNodeButton, { props: { label: 'Locate node on canvas' } })
expect(
screen.getByRole('button', { name: 'Locate node on canvas' })
).toHaveAttribute('aria-label', 'Locate node on canvas')
})
it('emits locate when clicked', async () => {
const user = userEvent.setup()
const { emitted } = render(LocateNodeButton, {
props: { label: 'Locate node on canvas' }
})
await user.click(
screen.getByRole('button', { name: 'Locate node on canvas' })
)
expect(emitted().locate).toHaveLength(1)
})
it('emits locate on keyboard activation', async () => {
const user = userEvent.setup()
const { emitted } = render(LocateNodeButton, {
props: { label: 'Locate node on canvas' }
})
await user.tab()
await user.keyboard('{Enter}')
expect(emitted().locate).toHaveLength(1)
})
it('stops click propagation so an ancestor handler does not also fire', async () => {
const user = userEvent.setup()
const onAncestorClick = vi.fn()
render({
components: { LocateNodeButton },
setup: () => ({ onAncestorClick }),
template:
'<div @click="onAncestorClick"><LocateNodeButton label="Locate node on canvas" /></div>'
})
await user.click(
screen.getByRole('button', { name: 'Locate node on canvas' })
)
expect(onAncestorClick).not.toHaveBeenCalled()
})
})

View File

@@ -0,0 +1,23 @@
<template>
<Button
variant="textonly"
size="icon-sm"
class="size-8 shrink-0 text-muted-foreground hover:text-base-foreground focus-visible:ring-inset"
:aria-label="label"
@click.stop="emit('locate')"
>
<i aria-hidden="true" class="icon-[lucide--locate] size-4" />
</Button>
</template>
<script setup lang="ts">
import Button from '@/components/ui/button/Button.vue'
const { label } = defineProps<{
label: string
}>()
const emit = defineEmits<{
locate: []
}>()
</script>

View File

@@ -66,7 +66,7 @@ const i18n = createI18n({
search: 'Search'
},
rightSidePanel: {
locateNode: 'Locate node on canvas',
locateNodeFor: 'Locate {item}',
missingNodePacks: {
unknownPack: 'Unknown pack',
installing: 'Installing...',
@@ -162,7 +162,7 @@ describe('MissingPackGroupRow', () => {
})
expect(
screen.queryByRole('button', { name: 'Locate node on canvas' })
screen.queryByRole('button', { name: 'Locate OnlyNode' })
).not.toBeInTheDocument()
})
@@ -263,9 +263,7 @@ describe('MissingPackGroupRow', () => {
})
})
await user.click(
screen.getByRole('button', { name: 'Locate node on canvas' })
)
await user.click(screen.getByRole('button', { name: 'Locate OnlyNode' }))
expect(onLocateNode).toHaveBeenCalledWith('100')
})
@@ -274,9 +272,7 @@ describe('MissingPackGroupRow', () => {
const { user, onLocateNode } = renderRow()
await user.click(screen.getByRole('button', { name: 'Expand' }))
await user.click(
screen.getAllByRole('button', { name: 'Locate node on canvas' })[0]
)
await user.click(screen.getByRole('button', { name: 'Locate MissingA' }))
expect(onLocateNode).toHaveBeenCalledWith('10')
})
@@ -295,7 +291,7 @@ describe('MissingPackGroupRow', () => {
})
})
expect(
screen.queryByRole('button', { name: 'Locate node on canvas' })
screen.queryByRole('button', { name: 'Locate NoId' })
).not.toBeInTheDocument()
})
@@ -312,8 +308,11 @@ describe('MissingPackGroupRow', () => {
expect(screen.getByText('WithId')).toBeInTheDocument()
expect(screen.getByText('WithoutId')).toBeInTheDocument()
expect(
screen.getAllByRole('button', { name: 'Locate node on canvas' })
).toHaveLength(1)
screen.getByRole('button', { name: 'Locate WithId' })
).toBeInTheDocument()
expect(
screen.queryByRole('button', { name: 'Locate WithoutId' })
).not.toBeInTheDocument()
})
})

View File

@@ -154,16 +154,15 @@
</span>
</Button>
</div>
<Button
<LocateNodeButton
v-if="primaryLocatableNodeType"
variant="textonly"
size="icon-sm"
class="size-8 shrink-0 text-muted-foreground hover:text-base-foreground focus-visible:ring-inset"
:aria-label="t('rightSidePanel.locateNode')"
@click="handleLocateNode(primaryLocatableNodeType)"
>
<i aria-hidden="true" class="icon-[lucide--locate] size-4" />
</Button>
:label="
t('rightSidePanel.locateNodeFor', {
item: getLabel(primaryLocatableNodeType)
})
"
@locate="handleLocateNode(primaryLocatableNodeType)"
/>
</div>
<TransitionCollapse>
@@ -203,16 +202,13 @@
{{ getLabel(nodeType) }}
</span>
</span>
<Button
<LocateNodeButton
v-if="isLocatableNodeType(nodeType)"
variant="textonly"
size="icon-sm"
class="size-8 shrink-0 text-muted-foreground hover:text-base-foreground focus-visible:ring-inset"
:aria-label="t('rightSidePanel.locateNode')"
@click="handleLocateNode(nodeType)"
>
<i aria-hidden="true" class="icon-[lucide--locate] size-4" />
</Button>
:label="
t('rightSidePanel.locateNodeFor', { item: getLabel(nodeType) })
"
@locate="handleLocateNode(nodeType)"
/>
</div>
</li>
</ul>
@@ -228,6 +224,7 @@ import { cn } from '@comfyorg/tailwind-utils'
import { selectionEmphasisClass } from './selectionEmphasis'
import Button from '@/components/ui/button/Button.vue'
import DotSpinner from '@/components/common/DotSpinner.vue'
import LocateNodeButton from '@/components/rightSidePanel/errors/LocateNodeButton.vue'
import TransitionCollapse from '@/components/rightSidePanel/layout/TransitionCollapse.vue'
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
import { usePackInstall } from '@/workbench/extensions/manager/composables/nodePack/usePackInstall'