[READ-ONLY] a fast, modern browser for the npm registry
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: only show stars/likes in package OG image when there are any (#1215)

authored by

Philippe Serhal and committed by
GitHub
8fc9197c 5f1fb789

+189 -9
+19 -7
app/components/OgImage/Package.vue
··· 53 53 const { stars, refresh: refreshRepoMeta } = useRepoMeta(repositoryUrl) 54 54 55 55 const formattedStars = computed(() => 56 - Intl.NumberFormat('en', { 57 - notation: 'compact', 58 - maximumFractionDigits: 1, 59 - }).format(stars.value), 56 + stars.value > 0 57 + ? Intl.NumberFormat('en', { 58 + notation: 'compact', 59 + maximumFractionDigits: 1, 60 + }).format(stars.value) 61 + : '', 60 62 ) 61 63 62 64 try { ··· 75 77 class="h-full w-full flex flex-col justify-center px-20 bg-[#050505] text-[#fafafa] relative overflow-hidden" 76 78 > 77 79 <div class="relative z-10 flex flex-col gap-6"> 80 + <!-- Package name --> 78 81 <div class="flex items-start gap-4"> 79 82 <div 80 83 class="flex items-center justify-center w-16 h-16 p-4 rounded-xl shadow-lg bg-gradient-to-tr from-[#3b82f6]" ··· 107 110 </h1> 108 111 </div> 109 112 113 + <!-- Version --> 110 114 <div 111 115 class="flex items-center gap-5 text-4xl font-light text-[#a3a3a3]" 112 116 style="font-family: 'Geist Sans', sans-serif" ··· 122 126 > 123 127 {{ resolvedVersion }} 124 128 </span> 129 + 130 + <!-- Downloads (if any) --> 125 131 <span v-if="downloads" class="flex items-center gap-2"> 126 132 <svg 127 133 width="30" ··· 139 145 </svg> 140 146 <span>{{ $n(downloads.downloads) }}/wk</span> 141 147 </span> 142 - <span v-if="pkg?.license" class="flex items-center gap-2"> 148 + 149 + <!-- License (if any) --> 150 + <span v-if="pkg?.license" class="flex items-center gap-2" data-testid="license"> 143 151 <svg 144 152 viewBox="0 0 32 32" 145 153 :fill="primaryColor" ··· 162 170 {{ pkg.license }} 163 171 </span> 164 172 </span> 165 - <span class="flex items-center gap-2"> 173 + 174 + <!-- Stars (if any) --> 175 + <span v-if="formattedStars" class="flex items-center gap-2" data-testid="stars"> 166 176 <svg 167 177 xmlns="http://www.w3.org/2000/svg" 168 178 viewBox="0 0 32 32" ··· 179 189 {{ formattedStars }} 180 190 </span> 181 191 </span> 182 - <span class="flex items-center gap-2"> 192 + 193 + <!-- Likes (if any) --> 194 + <span v-if="likes.totalLikes > 0" class="flex items-center gap-2" data-testid="likes"> 183 195 <svg 184 196 width="32" 185 197 height="32"
+2
app/composables/useRepoMeta.ts
··· 762 762 repoRef, 763 763 meta, 764 764 765 + // TODO(serhalp): Consider removing the zero fallback so callers can make a distinction between 766 + // "unresolved data" and "zero value" 765 767 stars: computed(() => meta.value?.stars ?? 0), 766 768 forks: computed(() => meta.value?.forks ?? 0), 767 769 watchers: computed(() => meta.value?.watchers ?? 0),
+2
playwright.config.ts
··· 18 18 reuseExistingServer: false, 19 19 timeout: 60_000, 20 20 }, 21 + // We currently only test on one browser on one platform 22 + snapshotPathTemplate: '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{ext}', 21 23 use: { 22 24 baseURL, 23 25 trace: 'on-first-retry',
+6 -2
test/e2e/og-image.spec.ts
··· 1 1 import { expect, test } from './test-utils' 2 2 3 + // TODO(serhalp): The nuxt@3.20.2 fixture has no stars. Update fixture to have stars coverage here. 3 4 const paths = ['/', '/package/nuxt/v/3.20.2'] 5 + 4 6 for (const path of paths) { 5 7 test.describe(path, () => { 6 - test.skip(`og image for ${path}`, async ({ page, goto, baseURL }) => { 8 + test(`og image for ${path}`, async ({ page, goto, baseURL }) => { 7 9 await goto(path, { waitUntil: 'domcontentloaded' }) 8 10 9 11 const ogImageUrl = await page.locator('meta[property="og:image"]').getAttribute('content') ··· 19 21 expect(response.headers()['content-type']).toContain('image/png') 20 22 21 23 const imageBuffer = await response.body() 22 - expect(imageBuffer).toMatchSnapshot({ name: `og-image-for-${path.replace(/\//g, '-')}.png` }) 24 + expect(imageBuffer).toMatchSnapshot({ 25 + name: `og-image-for-${path.replace(/\//g, '-')}.png`, 26 + }) 23 27 }) 24 28 }) 25 29 }
test/e2e/og-image.spec.ts-snapshots/og-image-for--.png

This is a binary file and will not be displayed.

test/e2e/og-image.spec.ts-snapshots/og-image-for--package-nuxt-v-3-20-2.png

This is a binary file and will not be displayed.

+160
test/nuxt/components/OgImagePackage.spec.ts
··· 1 + import { mockNuxtImport, mountSuspended, registerEndpoint } from '@nuxt/test-utils/runtime' 2 + import { describe, expect, it, vi, beforeEach } from 'vitest' 3 + 4 + const { mockUseResolvedVersion, mockUsePackageDownloads, mockUsePackage, mockUseRepoMeta } = 5 + vi.hoisted(() => ({ 6 + mockUseResolvedVersion: vi.fn(), 7 + mockUsePackageDownloads: vi.fn(), 8 + mockUsePackage: vi.fn(), 9 + mockUseRepoMeta: vi.fn(), 10 + })) 11 + 12 + mockNuxtImport('useResolvedVersion', () => mockUseResolvedVersion) 13 + mockNuxtImport('usePackageDownloads', () => mockUsePackageDownloads) 14 + mockNuxtImport('usePackage', () => mockUsePackage) 15 + mockNuxtImport('useRepoMeta', () => mockUseRepoMeta) 16 + mockNuxtImport('normalizeGitUrl', () => () => 'https://github.com/test/repo') 17 + 18 + import OgImagePackage from '~/components/OgImage/Package.vue' 19 + 20 + describe('OgImagePackage', () => { 21 + const baseProps = { 22 + name: 'test-package', 23 + version: '1.0.0', 24 + } 25 + 26 + function setupMocks( 27 + overrides: { 28 + stars?: number 29 + totalLikes?: number 30 + downloads?: number | null 31 + license?: string | null 32 + packageName?: string 33 + } = {}, 34 + ) { 35 + const { 36 + stars = 0, 37 + totalLikes = 0, 38 + downloads = 1000, 39 + license = 'MIT', 40 + packageName = 'test-package', 41 + } = overrides 42 + 43 + mockUseResolvedVersion.mockReturnValue({ 44 + data: ref('1.0.0'), 45 + status: ref('success'), 46 + error: ref(null), 47 + }) 48 + 49 + mockUsePackageDownloads.mockReturnValue({ 50 + data: downloads != null ? ref({ downloads }) : ref(null), 51 + refresh: vi.fn().mockResolvedValue(undefined), 52 + }) 53 + 54 + mockUsePackage.mockReturnValue({ 55 + data: ref({ 56 + name: packageName, 57 + license, 58 + requestedVersion: { 59 + repository: { url: 'git+https://github.com/test/repo.git' }, 60 + }, 61 + }), 62 + refresh: vi.fn().mockResolvedValue(undefined), 63 + }) 64 + 65 + mockUseRepoMeta.mockReturnValue({ 66 + stars: computed(() => stars), 67 + refresh: vi.fn().mockResolvedValue(undefined), 68 + }) 69 + 70 + // Mock the likes API endpoint used by useFetch 71 + registerEndpoint(`/api/social/likes/${packageName}`, () => ({ 72 + totalLikes, 73 + userHasLiked: false, 74 + })) 75 + } 76 + 77 + beforeEach(() => { 78 + mockUseResolvedVersion.mockReset() 79 + mockUsePackageDownloads.mockReset() 80 + mockUsePackage.mockReset() 81 + mockUseRepoMeta.mockReset() 82 + }) 83 + 84 + it('renders the package name and version', async () => { 85 + setupMocks({ packageName: 'vue' }) 86 + 87 + const component = await mountSuspended(OgImagePackage, { 88 + props: { ...baseProps, name: 'vue' }, 89 + }) 90 + 91 + expect(component.text()).toContain('vue') 92 + expect(component.text()).toContain('1.0.0') 93 + }) 94 + 95 + describe('license', () => { 96 + it('renders the license when present', async () => { 97 + setupMocks({ license: 'MIT' }) 98 + 99 + const component = await mountSuspended(OgImagePackage, { 100 + props: baseProps, 101 + }) 102 + 103 + expect(component.text()).toContain('MIT') 104 + }) 105 + 106 + it('hides the license section when license is missing', async () => { 107 + setupMocks({ license: null }) 108 + 109 + const component = await mountSuspended(OgImagePackage, { 110 + props: baseProps, 111 + }) 112 + 113 + expect(component.find('[data-testid="license"]').exists()).toBe(false) 114 + }) 115 + }) 116 + 117 + describe('stars', () => { 118 + it('hides stars section when count is 0', async () => { 119 + setupMocks({ stars: 0 }) 120 + 121 + const component = await mountSuspended(OgImagePackage, { 122 + props: baseProps, 123 + }) 124 + 125 + expect(component.find('[data-testid="stars"]').exists()).toBe(false) 126 + }) 127 + 128 + it('shows formatted stars when count is positive', async () => { 129 + setupMocks({ stars: 45200 }) 130 + 131 + const component = await mountSuspended(OgImagePackage, { 132 + props: baseProps, 133 + }) 134 + 135 + expect(component.text()).toContain('45.2K') 136 + }) 137 + }) 138 + 139 + describe('likes', () => { 140 + it('hides likes section when totalLikes is 0', async () => { 141 + setupMocks({ totalLikes: 0 }) 142 + 143 + const component = await mountSuspended(OgImagePackage, { 144 + props: baseProps, 145 + }) 146 + 147 + expect(component.find('[data-testid="likes"]').exists()).toBe(false) 148 + }) 149 + 150 + it('shows likes section when totalLikes is positive', async () => { 151 + setupMocks({ totalLikes: 42 }) 152 + 153 + const component = await mountSuspended(OgImagePackage, { 154 + props: baseProps, 155 + }) 156 + 157 + expect(component.text()).toContain('42') 158 + }) 159 + }) 160 + })