Skip to content

Commit 4920ac8

Browse files
committed
Merge branch 'main' into feat/i18n-at
2 parents 76b1333 + 1fd2d53 commit 4920ac8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+637
-1342
lines changed

CONTRIBUTING.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -752,6 +752,23 @@ pnpm test:browser:ui # Run with Playwright UI
752752

753753
Make sure to read about [Playwright best practices](https://playwright.dev/docs/best-practices) and don't rely on classes/IDs but try to follow user-replicable behaviour (like selecting an element based on text content instead).
754754

755+
#### Updating snapshots
756+
757+
Some tests use image snapshots that must match the CI environment (Linux). If you need to update them, and aren't running Linux, you can use Docker to run in the same environment:
758+
759+
```bash
760+
docker run --rm \
761+
-e CI=true \
762+
-e NODE_OPTIONS="--max-old-space-size=4096" \
763+
-v $(pwd):/work \
764+
-w /work \
765+
mcr.microsoft.com/playwright:v1.58.2-noble \
766+
sh -c "npm install -g pnpm && pnpm install && pnpm vp run build:test && pnpm vp run test:browser:prebuilt --update-snapshots"
767+
```
768+
769+
> [!NOTE]
770+
> If the build runs out of memory, increase `--max-old-space-size` to `8192`.
771+
755772
### Test fixtures (mocking external APIs)
756773

757774
E2E tests use a fixture system to mock external API requests, ensuring tests are deterministic and don't hit real APIs. This is handled at two levels:

app/components/ColumnPicker.vue

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,6 @@ const columnLabels = computed(() => ({
4949
updated: $t('filters.columns.published'),
5050
maintainers: $t('filters.columns.maintainers'),
5151
keywords: $t('filters.columns.keywords'),
52-
qualityScore: $t('filters.columns.quality_score'),
53-
popularityScore: $t('filters.columns.popularity_score'),
54-
maintenanceScore: $t('filters.columns.maintenance_score'),
55-
combinedScore: $t('filters.columns.combined_score'),
5652
security: $t('filters.columns.security'),
5753
selection: $t('filters.columns.selection'),
5854
}))

app/components/Package/List.vue

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,12 @@ watch(
112112
{ immediate: true },
113113
)
114114
115+
// Tracks how many items came from the last new-search batch.
116+
// Items at index < newSearchBatchSize are from the new search → no animation.
117+
// Items at index >= newSearchBatchSize were loaded via scroll → animate with stagger.
118+
// Using an index threshold avoids any timing dependency on nextTick / virtual list paint.
119+
const newSearchBatchSize = shallowRef(Infinity)
120+
115121
// Reset scroll state when results change significantly (new search)
116122
watch(
117123
() => props.results,
@@ -123,6 +129,7 @@ watch(
123129
(oldResults.length > 0 && newResults[0]?.package.name !== oldResults[0]?.package.name)
124130
) {
125131
hasScrolledToInitial.value = false
132+
newSearchBatchSize.value = newResults.length
126133
}
127134
},
128135
)
@@ -172,9 +179,16 @@ defineExpose({
172179
:show-publisher="showPublisher"
173180
:index="index"
174181
:search-query="searchQuery"
175-
class="motion-safe:animate-fade-in motion-safe:animate-fill-both"
182+
:class="
183+
index >= newSearchBatchSize &&
184+
'motion-safe:animate-fade-in motion-safe:animate-fill-both'
185+
"
186+
:style="
187+
index >= newSearchBatchSize
188+
? { animationDelay: `${Math.min((index - newSearchBatchSize) * 0.02, 0.3)}s` }
189+
: {}
190+
"
176191
:filters="filters"
177-
:style="{ animationDelay: `${Math.min(index * 0.02, 0.3)}s` }"
178192
@click-keyword="emit('clickKeyword', $event)"
179193
/>
180194
</div>
@@ -224,8 +238,15 @@ defineExpose({
224238
:show-publisher="showPublisher"
225239
:index="index"
226240
:search-query="searchQuery"
227-
class="motion-safe:animate-fade-in motion-safe:animate-fill-both"
228-
:style="{ animationDelay: `${Math.min(index * 0.02, 0.3)}s` }"
241+
:class="
242+
index >= newSearchBatchSize &&
243+
'motion-safe:animate-fade-in motion-safe:animate-fill-both'
244+
"
245+
:style="
246+
index >= newSearchBatchSize
247+
? { animationDelay: `${Math.min((index - newSearchBatchSize) * 0.02, 0.3)}s` }
248+
: {}
249+
"
229250
:filters="filters"
230251
@click-keyword="emit('clickKeyword', $event)"
231252
/>

app/components/Package/ListToolbar.vue

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,6 @@ const sortKeyLabelKeys = computed<Record<SortKey, string>>(() => ({
102102
'downloads-year': t('filters.sort.downloads_year'),
103103
'updated': t('filters.sort.published'),
104104
'name': t('filters.sort.name'),
105-
'quality': t('filters.sort.quality'),
106-
'popularity': t('filters.sort.popularity'),
107-
'maintenance': t('filters.sort.maintenance'),
108-
'score': t('filters.sort.score'),
109105
}))
110106
111107
function getSortKeyLabelKey(key: SortKey): string {

app/components/Package/Table.vue

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -37,21 +37,13 @@ const columnToSortKey: Record<string, SortKey> = {
3737
name: 'name',
3838
downloads: 'downloads-week',
3939
updated: 'updated',
40-
qualityScore: 'quality',
41-
popularityScore: 'popularity',
42-
maintenanceScore: 'maintenance',
43-
combinedScore: 'score',
4440
}
4541
4642
// Default direction for each column
4743
const columnDefaultDirection: Record<string, 'asc' | 'desc'> = {
4844
name: 'asc',
4945
downloads: 'desc',
5046
updated: 'desc',
51-
qualityScore: 'desc',
52-
popularityScore: 'desc',
53-
maintenanceScore: 'desc',
54-
combinedScore: 'desc',
5547
}
5648
5749
function isColumnSorted(id: string): boolean {
@@ -97,10 +89,6 @@ const columnLabels = computed(() => ({
9789
updated: t('filters.columns.published'),
9890
maintainers: t('filters.columns.maintainers'),
9991
keywords: t('filters.columns.keywords'),
100-
qualityScore: t('filters.columns.quality_score'),
101-
popularityScore: t('filters.columns.popularity_score'),
102-
maintenanceScore: t('filters.columns.maintenance_score'),
103-
combinedScore: t('filters.columns.combined_score'),
10492
security: t('filters.columns.security'),
10593
selection: t('filters.columns.selection'),
10694
}))
@@ -264,38 +252,6 @@ function getColumnLabel(id: ColumnId): string {
264252
{{ getColumnLabel('keywords') }}
265253
</th>
266254

267-
<th
268-
v-if="isColumnVisible('qualityScore')"
269-
scope="col"
270-
class="py-3 px-3 text-xs text-start text-fg-muted font-mono font-medium uppercase tracking-wider whitespace-nowrap select-none text-end"
271-
>
272-
{{ getColumnLabel('qualityScore') }}
273-
</th>
274-
275-
<th
276-
v-if="isColumnVisible('popularityScore')"
277-
scope="col"
278-
class="py-3 px-3 text-xs text-start text-fg-muted font-mono font-medium uppercase tracking-wider whitespace-nowrap select-none text-end"
279-
>
280-
{{ getColumnLabel('popularityScore') }}
281-
</th>
282-
283-
<th
284-
v-if="isColumnVisible('maintenanceScore')"
285-
scope="col"
286-
class="py-3 px-3 text-xs text-start text-fg-muted font-mono font-medium uppercase tracking-wider whitespace-nowrap select-none text-end"
287-
>
288-
{{ getColumnLabel('maintenanceScore') }}
289-
</th>
290-
291-
<th
292-
v-if="isColumnVisible('combinedScore')"
293-
scope="col"
294-
class="py-3 px-3 text-xs text-start text-fg-muted font-mono font-medium uppercase tracking-wider whitespace-nowrap select-none text-end"
295-
>
296-
{{ getColumnLabel('combinedScore') }}
297-
</th>
298-
299255
<th
300256
v-if="isColumnVisible('security')"
301257
scope="col"

app/components/Package/TableRow.vue

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,13 @@ const emit = defineEmits<{
1414
}>()
1515
1616
const pkg = computed(() => props.result.package)
17-
const score = computed(() => props.result.score)
1817
1918
const updatedDate = computed(() => props.result.package.date)
2019
const { isPackageSelected, togglePackageSelection, canSelectMore } = usePackageSelection()
2120
const isSelected = computed<boolean>(() => {
2221
return isPackageSelected(props.result.package.name)
2322
})
2423
25-
function formatScore(value?: number): string {
26-
if (value === undefined || value === 0) return '-'
27-
return Math.round(value * 100).toString()
28-
}
29-
3024
function isColumnVisible(id: string): boolean {
3125
return props.columns.find(c => c.id === id)?.visible ?? false
3226
}
@@ -163,38 +157,6 @@ const compactNumberFormatter = useCompactNumberFormatter()
163157
<span v-else class="text-fg-subtle">-</span>
164158
</td>
165159

166-
<!-- Quality Score -->
167-
<td
168-
v-if="isColumnVisible('qualityScore')"
169-
class="py-2 px-3 font-mono text-xs text-fg-muted text-end tabular-nums"
170-
>
171-
{{ formatScore(score?.detail?.quality) }}
172-
</td>
173-
174-
<!-- Popularity Score -->
175-
<td
176-
v-if="isColumnVisible('popularityScore')"
177-
class="py-2 px-3 font-mono text-xs text-fg-muted text-end tabular-nums"
178-
>
179-
{{ formatScore(score?.detail?.popularity) }}
180-
</td>
181-
182-
<!-- Maintenance Score -->
183-
<td
184-
v-if="isColumnVisible('maintenanceScore')"
185-
class="py-2 px-3 font-mono text-xs text-fg-muted text-end tabular-nums"
186-
>
187-
{{ formatScore(score?.detail?.maintenance) }}
188-
</td>
189-
190-
<!-- Combined Score -->
191-
<td
192-
v-if="isColumnVisible('combinedScore')"
193-
class="py-2 px-3 font-mono text-xs text-fg-muted text-end tabular-nums"
194-
>
195-
{{ formatScore(score?.final) }}
196-
</td>
197-
198160
<!-- Security -->
199161
<td v-if="isColumnVisible('security')" class="py-2 px-3">
200162
<span v-if="result.flags?.insecure" class="text-syntax-kw">

app/composables/npm/search-utils.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ export function metaToSearchResult(meta: PackageMetaResponse): NpmSearchResult {
1111
author: meta.author,
1212
maintainers: meta.maintainers,
1313
},
14-
score: { final: 0, detail: { quality: 0, popularity: 0, maintenance: 0 } },
1514
searchScore: 0,
1615
downloads: meta.weeklyDownloads !== undefined ? { weekly: meta.weeklyDownloads } : undefined,
1716
updated: meta.date,

app/composables/npm/useAlgoliaSearch.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -91,14 +91,6 @@ function hitToSearchResult(hit: AlgoliaHit): NpmSearchResult {
9191
}))
9292
: [],
9393
},
94-
score: {
95-
final: 0,
96-
detail: {
97-
quality: hit.popular ? 1 : 0,
98-
popularity: hit.downloadsRatio,
99-
maintenance: 0,
100-
},
101-
},
10294
searchScore: 0,
10395
downloads: {
10496
weekly: Math.round(hit.downloadsLast30Days / 4.3),

app/composables/useGlobalSearch.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { debounce } from 'perfect-debounce'
44
// Pages that have their own local filter using ?q
55
const pagesWithLocalFilter = new Set(['~username', 'org'])
66

7+
const SEARCH_DEBOUNCE_MS = 100
8+
79
export function useGlobalSearch(place: 'header' | 'content' = 'content') {
810
const { settings } = useSettings()
911
const { searchProvider } = useSearchProvider()
@@ -27,10 +29,14 @@ export function useGlobalSearch(place: 'header' | 'content' = 'content') {
2729
// Syncs instantly when instantSearch is on, but only on Enter press when off
2830
const committedSearchQuery = useState<string>('committed-search-query', () => searchQuery.value)
2931

32+
const commitSearchQuery = debounce((val: string) => {
33+
committedSearchQuery.value = val
34+
}, SEARCH_DEBOUNCE_MS)
35+
3036
// This is basically doing instant search as user types
3137
watch(searchQuery, val => {
3238
if (settings.value.instantSearch) {
33-
committedSearchQuery.value = val
39+
commitSearchQuery(val)
3440
}
3541
})
3642

@@ -71,10 +77,11 @@ export function useGlobalSearch(place: 'header' | 'content' = 'content') {
7177
})
7278
}
7379

74-
const updateUrlQuery = debounce(updateUrlQueryImpl, 250)
80+
const updateUrlQuery = debounce(updateUrlQueryImpl, SEARCH_DEBOUNCE_MS)
7581

7682
function flushUpdateUrlQuery() {
7783
// Commit the current query when explicitly submitted (Enter pressed)
84+
commitSearchQuery.cancel()
7885
committedSearchQuery.value = searchQuery.value
7986
// When instant search is off the debounce queue is empty, so call directly
8087
if (!settings.value.instantSearch) {

app/composables/useStructuredFilters.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -332,18 +332,6 @@ export function useStructuredFilters(options: UseStructuredFiltersOptions) {
332332
case 'name':
333333
diff = a.package.name.localeCompare(b.package.name)
334334
break
335-
case 'quality':
336-
diff = (a.score?.detail?.quality ?? 0) - (b.score?.detail?.quality ?? 0)
337-
break
338-
case 'popularity':
339-
diff = (a.score?.detail?.popularity ?? 0) - (b.score?.detail?.popularity ?? 0)
340-
break
341-
case 'maintenance':
342-
diff = (a.score?.detail?.maintenance ?? 0) - (b.score?.detail?.maintenance ?? 0)
343-
break
344-
case 'score':
345-
diff = (a.score?.final ?? 0) - (b.score?.final ?? 0)
346-
break
347335
case 'relevance':
348336
// Relevance preserves server order (already sorted by search relevance)
349337
diff = 0

0 commit comments

Comments
 (0)