mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 08:52:21 +00:00
fix: show honest skills.sh Trending provenance (#3423)
* fix: show honest skills.sh trending provenance * test: type mixed trending fixtures
This commit is contained in:
@@ -129,7 +129,7 @@ describe("HomeListingSection", () => {
|
||||
expect(screen.queryByRole("button", { name: "Grid view" })).toBeNull();
|
||||
});
|
||||
|
||||
it("identifies skills.sh rows by their source owner and upstream install count", async () => {
|
||||
it("identifies skills.sh rows without presenting upstream installs as downloads", async () => {
|
||||
const external = {
|
||||
...makeTrending("reddit-automation", "reddit-automation", 0, 12_345, 0),
|
||||
id: "skills-sh:doany-skills/skills/reddit-automation",
|
||||
@@ -161,10 +161,11 @@ describe("HomeListingSection", () => {
|
||||
|
||||
expect(screen.getByText("@doany-skills")).toBeTruthy();
|
||||
const sourceBadge = screen.getByText("skills.sh");
|
||||
const downloads = screen.getByLabelText("Downloads");
|
||||
expect(downloads.firstElementChild).toBe(sourceBadge);
|
||||
const source = screen.getByLabelText("Source");
|
||||
expect(source.firstElementChild).toBe(sourceBadge);
|
||||
expect(sourceBadge.getAttribute("title")).toBeNull();
|
||||
expect(downloads.textContent).toContain("12.3k");
|
||||
expect(source.textContent).toBe("skills.sh");
|
||||
expect(screen.queryByText("12.3k")).toBeNull();
|
||||
|
||||
fireEvent.pointerMove(sourceBadge);
|
||||
expect((await screen.findByRole("tooltip")).textContent).toBe("Synced from skills.sh");
|
||||
@@ -202,7 +203,7 @@ describe("HomeListingSection", () => {
|
||||
|
||||
const sourceBadge = screen.getByText("skills.sh");
|
||||
expect(sourceBadge.getAttribute("title")).toBeNull();
|
||||
expect(screen.getByLabelText("Downloads").textContent).toBe("skills.sh");
|
||||
expect(screen.getByLabelText("Source").textContent).toBe("skills.sh");
|
||||
});
|
||||
|
||||
it("hides unavailable Trending and falls back to the Featured feed", async () => {
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import type { ReactNode } from "react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { TooltipProvider } from "../components/ui/tooltip";
|
||||
import type { CanonicalTrendingItem } from "../lib/trendingApi";
|
||||
import { Route as SkillsRoute, SkillsIndex } from "../routes/skills/index";
|
||||
import {
|
||||
convexHttpMock,
|
||||
@@ -153,6 +155,41 @@ describe("SkillsIndex", () => {
|
||||
expect(screen.queryByLabelText("24-hour installs")).toBeNull();
|
||||
});
|
||||
|
||||
it("shows skills.sh provenance without presenting lifetime installs as downloads", async () => {
|
||||
fetchCanonicalTrendingPageMock.mockResolvedValue(
|
||||
canonicalPage([makeSkillsShTrending("external", "External Skill", 12_345)]),
|
||||
);
|
||||
|
||||
render(
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<SkillsIndex />
|
||||
</TooltipProvider>,
|
||||
);
|
||||
|
||||
expect(await screen.findByText("External Skill")).toBeTruthy();
|
||||
expect(screen.getByLabelText("Source").textContent).toBe("skills.sh");
|
||||
expect(screen.queryByLabelText("24-hour downloads")).toBeNull();
|
||||
expect(screen.queryByText("12.3k")).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps skills.sh provenance visible in the Trending grid", async () => {
|
||||
searchMock = { view: "grid" };
|
||||
fetchCanonicalTrendingPageMock.mockResolvedValue(
|
||||
canonicalPage([makeSkillsShTrending("external", "External Skill", 12_345)]),
|
||||
);
|
||||
|
||||
render(
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<SkillsIndex />
|
||||
</TooltipProvider>,
|
||||
);
|
||||
|
||||
expect(await screen.findByText("External Skill")).toBeTruthy();
|
||||
expect(screen.getByLabelText("Source").textContent).toBe("skills.sh");
|
||||
expect(screen.queryByLabelText("24-hour downloads")).toBeNull();
|
||||
expect(screen.queryByText("12.3k")).toBeNull();
|
||||
});
|
||||
|
||||
it("hides disabled Trending and falls back to the Featured feed", async () => {
|
||||
fetchCatalogDiscoveryCapabilitiesMock.mockResolvedValue({
|
||||
apiVersion: 0,
|
||||
@@ -448,7 +485,7 @@ function getLastListPageArgs() {
|
||||
return (call?.[1] ?? {}) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function canonicalPage(items: ReturnType<typeof makeTrending>[], nextCursor: string | null = null) {
|
||||
function canonicalPage(items: CanonicalTrendingItem[], nextCursor: string | null = null) {
|
||||
return {
|
||||
kind: "skills" as const,
|
||||
snapshotId: "snapshot-1",
|
||||
@@ -496,6 +533,31 @@ function makeTrending(
|
||||
};
|
||||
}
|
||||
|
||||
function makeSkillsShTrending(slug: string, displayName: string, lifetime: number) {
|
||||
return {
|
||||
...makeTrending(slug, displayName, 0, lifetime, 0),
|
||||
id: `skills-sh:example/skills/${slug}`,
|
||||
source: "skills-sh" as const,
|
||||
canonicalUrl: `/skills-sh/example/skills/${slug}`,
|
||||
publisher: null,
|
||||
sourceIdentity: {
|
||||
id: `example/skills/${slug}`,
|
||||
owner: "example",
|
||||
repo: "skills",
|
||||
host: null,
|
||||
lifetimeInstalls: lifetime,
|
||||
},
|
||||
metrics: {
|
||||
trending24hDownloads: null,
|
||||
trending24hInstalls: null,
|
||||
trending24hBookmarks: null,
|
||||
lifetimeInstalls: lifetime,
|
||||
lifetimeInstallsPeriod: "lifetime" as const,
|
||||
updatedAt: 1,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function makeListResult(slug: string, displayName: string) {
|
||||
return {
|
||||
skill: {
|
||||
|
||||
@@ -130,7 +130,6 @@ function HomeListingSkillRow({ entry }: { entry: SkillPageEntry }) {
|
||||
const owner = isSkillsSh
|
||||
? (item.sourceIdentity?.owner ?? item.sourceIdentity?.host)
|
||||
: item.publisher?.handle;
|
||||
const upstreamInstalls = item.sourceIdentity?.lifetimeInstalls ?? item.metrics.lifetimeInstalls;
|
||||
return (
|
||||
<Link to={item.canonicalUrl} className="home-v2-listing-row">
|
||||
<div className="home-v2-listing-row-body">
|
||||
@@ -145,7 +144,7 @@ function HomeListingSkillRow({ entry }: { entry: SkillPageEntry }) {
|
||||
</p>
|
||||
</div>
|
||||
{isSkillsSh ? (
|
||||
<div className="home-v2-listing-row-stats is-skills-sh" aria-label="Downloads">
|
||||
<div className="home-v2-listing-row-stats is-skills-sh" aria-label="Source">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge variant="compact" size="sm">
|
||||
@@ -156,11 +155,6 @@ function HomeListingSkillRow({ entry }: { entry: SkillPageEntry }) {
|
||||
Synced from skills.sh
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{typeof upstreamInstalls === "number" ? (
|
||||
<span title={`${upstreamInstalls.toLocaleString()} skills.sh installs`}>
|
||||
{formatCompactStat(upstreamInstalls)}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
) : typeof item.metrics.trending24hDownloads === "number" ? (
|
||||
<div className="home-v2-listing-row-stats" aria-label="Downloads">
|
||||
|
||||
@@ -8,6 +8,7 @@ import { SkillListItem } from "../../components/SkillListItem";
|
||||
import { SkillStatsTripletLine } from "../../components/SkillStats";
|
||||
import { Badge } from "../../components/ui/badge";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../../components/ui/tooltip";
|
||||
import { getSkillBadges } from "../../lib/badges";
|
||||
import { formatCompactStat } from "../../lib/numberFormat";
|
||||
import { timeAgo } from "../../lib/timeAgo";
|
||||
@@ -58,8 +59,22 @@ function TrendingSkillListItem({ item }: { item: TrendingSkillListEntry }) {
|
||||
<p className="skill-list-item-summary">{truncateText(trending.summary, 80)}</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="skill-list-item-meta" aria-label="24-hour downloads">
|
||||
{typeof trending.metrics.trending24hDownloads === "number" ? (
|
||||
<div
|
||||
className="skill-list-item-meta"
|
||||
aria-label={trending.source === "skills-sh" ? "Source" : "24-hour downloads"}
|
||||
>
|
||||
{trending.source === "skills-sh" ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge variant="compact" size="sm">
|
||||
skills.sh
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" align="center">
|
||||
Synced from skills.sh
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : typeof trending.metrics.trending24hDownloads === "number" ? (
|
||||
<span className="skill-list-item-meta-item">
|
||||
<Download size={14} aria-hidden="true" />
|
||||
{formatCompactStat(trending.metrics.trending24hDownloads)}
|
||||
@@ -94,7 +109,20 @@ function TrendingSkillCard({ item }: { item: TrendingSkillListEntry }) {
|
||||
{trending.summary}
|
||||
</p>
|
||||
) : null}
|
||||
{typeof trending.metrics.trending24hDownloads === "number" ? (
|
||||
{trending.source === "skills-sh" ? (
|
||||
<div className="skill-card-grid-meta" aria-label="Source">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge variant="compact" size="sm">
|
||||
skills.sh
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" align="center">
|
||||
Synced from skills.sh
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
) : typeof trending.metrics.trending24hDownloads === "number" ? (
|
||||
<div className="skill-card-grid-meta" aria-label="24-hour downloads">
|
||||
<span>
|
||||
<Download size={14} aria-hidden="true" />
|
||||
|
||||
Reference in New Issue
Block a user