Skip to content

Commit 8fc9197

Browse files
authored
fix: only show stars/likes in package OG image when there are any (#1215)
1 parent 5f1fb78 commit 8fc9197

File tree

7 files changed

+189
-9
lines changed

7 files changed

+189
-9
lines changed

app/components/OgImage/Package.vue

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,12 @@ const { data: likes, refresh: refreshLikes } = useFetch(() => `/api/social/likes
5353
const { stars, refresh: refreshRepoMeta } = useRepoMeta(repositoryUrl)
5454
5555
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+
: '',
6062
)
6163
6264
try {
@@ -75,6 +77,7 @@ try {
7577
class="h-full w-full flex flex-col justify-center px-20 bg-[#050505] text-[#fafafa] relative overflow-hidden"
7678
>
7779
<div class="relative z-10 flex flex-col gap-6">
80+
<!-- Package name -->
7881
<div class="flex items-start gap-4">
7982
<div
8083
class="flex items-center justify-center w-16 h-16 p-4 rounded-xl shadow-lg bg-gradient-to-tr from-[#3b82f6]"
@@ -107,6 +110,7 @@ try {
107110
</h1>
108111
</div>
109112

113+
<!-- Version -->
110114
<div
111115
class="flex items-center gap-5 text-4xl font-light text-[#a3a3a3]"
112116
style="font-family: 'Geist Sans', sans-serif"
@@ -122,6 +126,8 @@ try {
122126
>
123127
{{ resolvedVersion }}
124128
</span>
129+
130+
<!-- Downloads (if any) -->
125131
<span v-if="downloads" class="flex items-center gap-2">
126132
<svg
127133
width="30"
@@ -139,7 +145,9 @@ try {
139145
</svg>
140146
<span>{{ $n(downloads.downloads) }}/wk</span>
141147
</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">
143151
<svg
144152
viewBox="0 0 32 32"
145153
:fill="primaryColor"
@@ -162,7 +170,9 @@ try {
162170
{{ pkg.license }}
163171
</span>
164172
</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">
166176
<svg
167177
xmlns="http://www.w3.org/2000/svg"
168178
viewBox="0 0 32 32"
@@ -179,7 +189,9 @@ try {
179189
{{ formattedStars }}
180190
</span>
181191
</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">
183195
<svg
184196
width="32"
185197
height="32"

app/composables/useRepoMeta.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -762,6 +762,8 @@ export function useRepoMeta(repositoryUrl: MaybeRefOrGetter<string | null | unde
762762
repoRef,
763763
meta,
764764

765+
// TODO(serhalp): Consider removing the zero fallback so callers can make a distinction between
766+
// "unresolved data" and "zero value"
765767
stars: computed(() => meta.value?.stars ?? 0),
766768
forks: computed(() => meta.value?.forks ?? 0),
767769
watchers: computed(() => meta.value?.watchers ?? 0),

playwright.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export default defineConfig<ConfigOptions>({
1818
reuseExistingServer: false,
1919
timeout: 60_000,
2020
},
21+
// We currently only test on one browser on one platform
22+
snapshotPathTemplate: '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{ext}',
2123
use: {
2224
baseURL,
2325
trace: 'on-first-retry',

test/e2e/og-image.spec.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { expect, test } from './test-utils'
22

3+
// TODO(serhalp): The nuxt@3.20.2 fixture has no stars. Update fixture to have stars coverage here.
34
const paths = ['/', '/package/nuxt/v/3.20.2']
5+
46
for (const path of paths) {
57
test.describe(path, () => {
6-
test.skip(`og image for ${path}`, async ({ page, goto, baseURL }) => {
8+
test(`og image for ${path}`, async ({ page, goto, baseURL }) => {
79
await goto(path, { waitUntil: 'domcontentloaded' })
810

911
const ogImageUrl = await page.locator('meta[property="og:image"]').getAttribute('content')
@@ -19,7 +21,9 @@ for (const path of paths) {
1921
expect(response.headers()['content-type']).toContain('image/png')
2022

2123
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+
})
2327
})
2428
})
2529
}
34.3 KB
Loading
31.3 KB
Loading
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
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+
})

0 commit comments

Comments
 (0)