fix(ui): don't show state before profile/likes finish loading#1910
fix(ui): don't show state before profile/likes finish loading#1910
Conversation
Like cards showed totalLikes: 0 default before the client-side fetch resolved, causing a visible 0 -> X jump. Now shows a pulse skeleton and neutral heart icon during the pending state.
The invite empty state briefly flashed on page load because the auth session (server: false) hadn't resolved yet, making the "not own profile" check pass incorrectly. Closes #1781
|
The latest updates on your projects. Learn more about Vercel for GitHub.
2 Skipped Deployments
|
📝 WalkthroughWalkthroughAdds pending-state handling for package likes and profile user fetches. Package LikeCard now reads a Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
app/components/Package/LikeCard.vueapp/pages/profile/[identity]/index.vuetest/nuxt/components/PackageLikeCard.spec.tstest/nuxt/components/ProfileInviteSection.spec.ts
| const { data: likesData, status: likesStatus } = useFetch(() => `/api/social/likes/${name.value}`, { | ||
| default: () => ({ totalLikes: 0, userHasLiked: false }), | ||
| server: false, | ||
| }) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
rg -n "likesStatus|isLikeActionPending|likeAction|:disabled|aria-busy" app/components/Package/LikeCard.vue -C3Repository: npmx-dev/npmx.dev
Length of output: 2014
Prevent user interactions whilst initial likes data is still pending.
The UI displays a pending indicator but does not block the button. Users can click during the loading window, and likeAction will use default fallback values to compute the state change, risking incorrect optimistic updates based on uninitialised data.
The likeAction function only guards isLikeActionPending (line 33) but does not check whether likesStatus is pending. Additionally, the button lacks both :disabled and :aria-busy attributes to reflect the pending state.
Suggested fix
const likeAction = async () => {
if (user.value?.handle == null) {
authModal.open()
return
}
- if (isLikeActionPending.value) return
+ if (likesStatus.value === 'pending' || isLikeActionPending.value) return <button
`@click.prevent`="likeAction"
type="button"
+ :disabled="likesStatus === 'pending' || isLikeActionPending"
+ :aria-busy="likesStatus === 'pending'"
:title="
likesData?.userHasLiked ? $t('package.likes.unlike') : $t('package.likes.like')
"| it('shows a loading skeleton instead of zero while like data is pending', async () => { | ||
| wrapper = await mountLikeCard('https://npmx.dev/package/vue') | ||
|
|
||
| const button = wrapper.find('button') | ||
| expect(button.text()).not.toContain('0') | ||
|
|
||
| const skeleton = button.find('.animate-pulse') | ||
| expect(skeleton.exists()).toBe(true) | ||
| }) | ||
|
|
||
| it('shows a neutral heart icon while like data is pending', async () => { | ||
| wrapper = await mountLikeCard('https://npmx.dev/package/vue') | ||
|
|
||
| const icon = wrapper.find('button span[aria-hidden]') | ||
| expect(icon.classes()).toContain('i-lucide-heart') | ||
| expect(icon.classes()).not.toContain('i-lucide-heart-plus') | ||
| expect(icon.classes()).not.toContain('i-lucide-heart-minus') | ||
| }) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Read the full spec file to understand setup and mocking
cat -n test/nuxt/components/PackageLikeCard.spec.ts
# Also search for any mock or endpoint setup patterns
rg -n "registerEndpoint|mockNuxtImport|useFakeTimers|setTimeout|new Promise|beforeEach|vi\." test/nuxt/components/PackageLikeCard.spec.ts || echo "No timing control patterns found"Repository: npmx-dev/npmx.dev
Length of output: 2780
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find and read the LikeCard component
find . -name "LikeCard.vue" -type f | head -5Repository: npmx-dev/npmx.dev
Length of output: 98
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Read the LikeCard component
cat -n ~/components/Package/LikeCard.vue 2>/dev/null || cat -n src/components/Package/LikeCard.vue 2>/dev/null || find . -path "*components/Package/LikeCard.vue" -exec cat -n {} \;Repository: npmx-dev/npmx.dev
Length of output: 4465
Control fetch timing to stabilise pending-state assertions.
The tests at lines 48–65 check pending-state UI (skeleton, neutral heart icon) immediately after mount. However, useFetch() status in the component is not mocked, so its resolution is uncontrolled. If the endpoint responds before assertions run, the pending state disappears and tests fail.
Introduce a deliberate delay in the endpoint response using registerEndpoint():
Stabilisation example
+import { registerEndpoint } from '@nuxt/test-utils/runtime'
+
+registerEndpoint('/api/social/likes/vue', async () => {
+ await new Promise(resolve => setTimeout(resolve, 50))
+ return { totalLikes: 12, userHasLiked: false }
+})
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
to avoid the extra layout shift, let's not show the skeleton or heart at all until the data loads. |
There was a problem hiding this comment.
♻️ Duplicate comments (1)
test/nuxt/components/PackageLikeCard.spec.ts (1)
48-53:⚠️ Potential issue | 🟠 MajorControl fetch timing to stabilise pending-state assertion.
The test checks the pending-state UI immediately after
mountSuspendedresolves, butuseFetch()status is not mocked. Without explicit control over the endpoint response, this test is timing-dependent: if the fetch resolves before the assertion runs, the button will appear and the test will fail.Introduce a deliberate delay in the endpoint response using
registerEndpoint()to stabilise the test:Stabilisation example
+import { mountSuspended, registerEndpoint } from '@nuxt/test-utils/runtime' -import { mountSuspended } from '@nuxt/test-utils/runtime' +// Add before the test or in a beforeEach block +registerEndpoint('/api/social/likes/vue', async () => { + await new Promise(resolve => setTimeout(resolve, 50)) + return { totalLikes: 12, userHasLiked: false } +})
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: a9f7f5be-b5ad-48a1-b2a1-d111fa49b5da
📒 Files selected for processing (2)
app/components/Package/LikeCard.vuetest/nuxt/components/PackageLikeCard.spec.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- app/components/Package/LikeCard.vue
🔗 Linked issue
Closes #1781
🧭 Context
See the issue. There were 2 visual glitches on the profile page.
📚 Description
fix: show skeleton instead of 0 in like cards while loading
Like cards showed the "zero likes" default before the client-side fetch resolved, causing a visible 0 -> X jump. This changes it to show a skeleton loader with a neutral-ish heart icon during the pending state.
fix: wait for auth before showing profile invite section
The invite empty state briefly flashed on page load because the auth session (
server: false) hadn't resolved yet, making the "not own profile" state flash briefly.npmx.fix.1910.demo.mp4
Tip
We could probably avoid the slight layout shift when the number of likes comes in, but I don't see an obvious solution since the number has an unpredictable string length and the heart icon is to the left of it... We can fix that later.