Skip to content
19 changes: 16 additions & 3 deletions app/components/CopyToClipboardButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@ const props = defineProps<{

const buttonCopyText = computed(() => props.copyText || $t('common.copy'))
const buttonCopiedText = computed(() => props.copiedText || $t('common.copied'))
const buttonAriaLabelCopy = computed(() => props.ariaLabelCopy || $t('common.copy'))
const buttonAriaLabelCopied = computed(() => props.ariaLabelCopied || $t('common.copied'))
const buttonAriaLabelCopy = computed(
() => props.ariaLabelCopy || props.copyText || $t('common.copy'),
)
const buttonAriaLabelCopied = computed(
() => props.ariaLabelCopied || props.copiedText || $t('common.copied'),
)

const emit = defineEmits<{
click: []
Expand Down Expand Up @@ -85,8 +89,17 @@ function handleClick() {
}

@media (hover: none) {
/* On touch devices show the button statically (no hover possible) */
.copyButton {
display: none;
clip: auto;
clip-path: none;
height: auto;
overflow: visible;
width: auto;
opacity: 1;
translate: none;
pointer-events: auto;
transition: none;
}
}
</style>
60 changes: 60 additions & 0 deletions test/nuxt/components/CopyToClipboardButton.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { describe, expect, it } from 'vitest'
import { mountSuspended } from '@nuxt/test-utils/runtime'
import CopyToClipboardButton from '~/components/CopyToClipboardButton.vue'

describe('CopyToClipboardButton', () => {
it('aria-label matches visible copy text when copyText is provided', async () => {
const wrapper = await mountSuspended(CopyToClipboardButton, {
props: {
copied: false,
copyText: 'Copy package name',
},
})

const button = wrapper.find('button')
expect(button.attributes('aria-label')).toBe('Copy package name')
expect(button.text()).toContain('Copy package name')
})

it('aria-label uses ariaLabelCopy when explicitly provided', async () => {
const wrapper = await mountSuspended(CopyToClipboardButton, {
props: {
copied: false,
copyText: 'Copy package name',
ariaLabelCopy: 'Copy the package name to clipboard',
},
})

const button = wrapper.find('button')
expect(button.attributes('aria-label')).toBe('Copy the package name to clipboard')
})

it('aria-label reflects copiedText when copied is true', async () => {
const wrapper = await mountSuspended(CopyToClipboardButton, {
props: {
copied: true,
copyText: 'Copy package name',
copiedText: 'Copied!',
},
})

const button = wrapper.find('button')
expect(button.attributes('aria-label')).toBe('Copied!')
expect(button.text()).toContain('Copied!')
})

it('aria-label matches visible text - no label/content mismatch', async () => {
const wrapper = await mountSuspended(CopyToClipboardButton, {
props: {
copied: false,
copyText: 'Copy install command',
},
})

const button = wrapper.find('button')
const ariaLabel = button.attributes('aria-label') ?? ''
const visibleText = button.text()
// The aria-label should equal the visible text (not some other string)
expect(visibleText).toContain(ariaLabel)
})
Comment on lines +55 to +59
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Final mismatch test uses a weak/reversed assertion for its stated intent.

This can pass when aria-label is only a substring of visible text, so it does not reliably guard against label/content-name mismatch.

Proposed test tightening
-    const ariaLabel = button.attributes('aria-label') ?? ''
-    const visibleText = button.text()
-    // The aria-label should equal the visible text (not some other string)
-    expect(visibleText).toContain(ariaLabel)
+    const ariaLabel = (button.attributes('aria-label') ?? '').trim()
+    const visibleText = button.text().trim()
+    // The aria-label should equal the visible text (not some other string)
+    expect(ariaLabel).toBe(visibleText)

})
Loading