Skip to content
8 changes: 6 additions & 2 deletions app/components/Package/SkillsModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,15 @@ function getWarningTooltip(skill: SkillListItem): string | undefined {
</code>
<button
type="button"
class="absolute top-0 inset-ie-0 px-2 py-0.5 font-mono text-xs text-fg-muted bg-bg-subtle/80 border border-border rounded transition-colors duration-200 opacity-0 group-hover/cmd:opacity-100 hover:(text-fg border-border-hover) active:scale-95 focus-visible:opacity-100 focus-visible:outline-accent/70"
class="absolute top-0 inset-ie-0 px-2 py-0.5 font-mono text-xs text-fg-muted bg-bg-subtle/80 border border-border border-solid rounded transition-colors hover:(text-fg border-border-hover) active:scale-95 focus-visible:outline-accent/70"
:aria-label="$t('package.get_started.copy_command')"
@click.stop="copyCommand"
>
<span aria-live="polite">{{ copied ? $t('common.copied') : $t('common.copy') }}</span>
<span
:class="copied ? 'i-carbon:checkmark' : 'i-carbon:copy'"
class="size-4 inline-block"
aria-hidden="true"
/>
</button>
</div>
</div>
Expand Down
8 changes: 6 additions & 2 deletions app/components/Terminal/Execute.vue
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,15 @@ const copyExecuteCommand = () => copyExecute(getFullExecuteCommand())
>
<button
type="button"
class="px-2 py-0.5 font-mono text-xs text-fg-muted bg-bg-subtle/80 border border-border rounded transition-colors duration-200 opacity-0 group-hover/executecmd:opacity-100 hover:(text-fg border-border-hover) active:scale-95 focus-visible:opacity-100 focus-visible:outline-accent/70"
class="p-1 ms-1 flex items-center font-mono text-xs text-fg-muted bg-bg-subtle/80 border border-border border-solid rounded transition-colors duration-200 hover:(text-fg border-border-hover) active:scale-95 focus-visible:outline-accent/70"
:aria-label="$t('package.get_started.copy_command')"
@click.stop="copyExecuteCommand"
>
{{ executeCopied ? $t('common.copied') : $t('common.copy') }}
<span
:class="executeCopied ? 'i-carbon:checkmark' : 'i-carbon:copy'"
class="size-4 inline-block"
aria-hidden="true"
/>
</button>
</div>
</div>
Expand Down
27 changes: 19 additions & 8 deletions app/components/Terminal/Install.vue
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,15 @@ const copyCreateCommand = () => copyCreate(getFullCreateCommand())
>
<button
type="button"
class="px-2 py-0.5 font-mono text-xs text-fg-muted bg-bg-subtle/80 border border-border rounded transition-colors duration-200 opacity-0 group-hover/installcmd:opacity-100 hover:(text-fg border-border-hover) active:scale-95 focus-visible:opacity-100 focus-visible:outline-accent/70 select-none"
class="p-1 ms-1 flex items-center font-mono text-xs text-fg-muted bg-bg-subtle/80 border border-border border-solid rounded transition-colors duration-200 hover:(text-fg border-border-hover) active:scale-95 focus-visible:outline-accent/70"
:aria-label="$t('package.get_started.copy_command')"
@click.stop="copyInstallCommand"
>
<span aria-live="polite">{{ copied ? $t('common.copied') : $t('common.copy') }}</span>
<span
:class="copied ? 'i-carbon:checkmark' : 'i-carbon:copy'"
class="size-4 inline-block"
aria-hidden="true"
/>
</button>
</div>

Expand Down Expand Up @@ -187,10 +191,15 @@ const copyCreateCommand = () => copyCreate(getFullCreateCommand())
>
<button
type="button"
class="px-2 py-0.5 font-mono text-xs text-fg-muted bg-bg-subtle/80 border border-border rounded transition-colors duration-200 opacity-0 group-hover/runcmd:opacity-100 hover:(text-fg border-border-hover) active:scale-95 focus-visible:opacity-100 focus-visible:outline-accent/70 select-none"
class="p-1 ms-1 flex items-center font-mono text-xs text-fg-muted bg-bg-subtle/80 border border-border border-solid rounded transition-colors duration-200 hover:(text-fg border-border-hover) active:scale-95 focus-visible:outline-accent/70"
:aria-label="$t('package.run.locally')"
@click.stop="copyRunCommand(executableInfo?.primaryCommand)"
>
{{ runCopied ? $t('common.copied') : $t('common.copy') }}
<span
:class="runCopied ? 'i-carbon:checkmark' : 'i-carbon:copy'"
class="size-4 inline-block"
aria-hidden="true"
/>
</button>
</div>
</template>
Expand Down Expand Up @@ -232,13 +241,15 @@ const copyCreateCommand = () => copyCreate(getFullCreateCommand())
>
<button
type="button"
class="px-2 py-0.5 font-mono text-xs text-fg-muted bg-bg-subtle/80 border border-border rounded transition-colors duration-200 opacity-0 group-hover/createcmd:opacity-100 hover:(text-fg border-border-hover) active:scale-95 focus-visible:opacity-100 focus-visible:outline-accent/70 select-none"
class="p-1 ms-1 flex items-center font-mono text-xs text-fg-muted bg-bg-subtle/80 border border-border border-solid rounded transition-colors duration-200 hover:(text-fg border-border-hover) active:scale-95 focus-visible:outline-accent/70"
:aria-label="$t('package.create.copy_command')"
@click.stop="copyCreateCommand"
>
<span aria-live="polite">{{
createCopied ? $t('common.copied') : $t('common.copy')
}}</span>
<span
:class="createCopied ? 'i-carbon:checkmark' : 'i-carbon:copy'"
class="size-4 inline-block"
aria-hidden="true"
/>
</button>
</div>
</template>
Expand Down
79 changes: 56 additions & 23 deletions test/e2e/create-command.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ test.describe('Create Command', () => {
})

test.describe('Copy Functionality', () => {
test('hovering create command shows copy button', async ({ page, goto }) => {
test('copy button is always visible', async ({ page, goto }) => {
await goto('/package/vite', { waitUntil: 'hydration' })

await expect(page.locator('h1')).toContainText('vite', { timeout: 15000 })
Expand All @@ -75,14 +75,8 @@ test.describe('Create Command', () => {
const createCommandContainer = page.locator('.group\\/createcmd').first()
await expect(createCommandContainer).toBeVisible({ timeout: 20000 })

// Copy button should initially be hidden (opacity-0)
// Copy button should always be visible
const copyButton = createCommandContainer.locator('button')
await expect(copyButton).toHaveCSS('opacity', '0')

// Hover over the container
await createCommandContainer.hover()

// Copy button should become visible
await expect(copyButton).toHaveCSS('opacity', '1')
})

Expand All @@ -104,9 +98,6 @@ test.describe('Create Command', () => {
const createCommandContainer = page.locator('.group\\/createcmd').first()
await expect(createCommandContainer).toBeVisible({ timeout: 20000 })

await createCommandContainer.hover()

// Click the copy button
const copyButton = createCommandContainer.locator('button')
await copyButton.click()

Expand All @@ -123,21 +114,15 @@ test.describe('Create Command', () => {
})

test.describe('Install Command Copy', () => {
test('hovering install command shows copy button', async ({ page, goto }) => {
test('copy button is always visible', async ({ page, goto }) => {
await goto('/package/is-odd', { waitUntil: 'hydration' })

// Find the install command container
const installCommandContainer = page.locator('.group\\/installcmd').first()
await expect(installCommandContainer).toBeVisible()

// Copy button should initially be hidden
// Copy button should always be visible
const copyButton = installCommandContainer.locator('button')
await expect(copyButton).toHaveCSS('opacity', '0')

// Hover over the container
await installCommandContainer.hover()

// Copy button should become visible
await expect(copyButton).toHaveCSS('opacity', '1')
})

Expand All @@ -151,11 +136,7 @@ test.describe('Create Command', () => {

await goto('/package/is-odd', { waitUntil: 'hydration' })

// Find and hover over the install command container
const installCommandContainer = page.locator('.group\\/installcmd').first()
await installCommandContainer.hover()

// Click the copy button
const copyButton = installCommandContainer.locator('button')
await copyButton.click()

Expand All @@ -170,4 +151,56 @@ test.describe('Create Command', () => {
await expect(copyButton).not.toContainText(/copied/i)
})
})

test.describe('Run Command Copy', () => {
test('copy button is always visible', async ({ page, goto }) => {
await goto('/package/vite', { waitUntil: 'hydration' })

await expect(page.locator('h1')).toContainText('vite', { timeout: 15000 })

await expect(page.locator('main header').locator('text=/v\\d+\\.\\d+/')).toBeVisible({
timeout: 15000,
})

// Find the run command container
const runCommandContainer = page.locator('.group\\/runcmd').first()
await expect(runCommandContainer).toBeVisible({ timeout: 20000 })

// Copy button should always be visible
const copyButton = runCommandContainer.locator('button')
await expect(copyButton).toHaveCSS('opacity', '1')
})

test('clicking copy button copies run command and shows confirmation', async ({
page,
goto,
context,
}) => {
// Grant clipboard permissions
await context.grantPermissions(['clipboard-read', 'clipboard-write'])

await goto('/package/vite', { waitUntil: 'hydration' })
await expect(page.locator('h1')).toContainText('vite', { timeout: 15000 })

await expect(page.locator('main header').locator('text=/v\\d+\\.\\d+/')).toBeVisible({
timeout: 15000,
})

const runCommandContainer = page.locator('.group\\/runcmd').first()
await expect(runCommandContainer).toBeVisible({ timeout: 20000 })

const copyButton = runCommandContainer.locator('button')
await copyButton.click()

// Button text should change to "copied!"
await expect(copyButton).toContainText(/copied/i)

// Verify clipboard content contains the run command
const clipboardContent = await page.evaluate(() => navigator.clipboard.readText())
expect(clipboardContent).toMatch(/npx vite/i)

await expect(copyButton).toContainText(/copy/i, { timeout: 5000 })
await expect(copyButton).not.toContainText(/copied/i)
})
})
})
Loading