feat(studio): 市场推荐位接入真后端 recommendations(A 类:接已就绪后端)
A 类(缺口盘点:后端已就绪、前端 0 消费):02F GET /marketplace/recommendations 后端就绪 (curl code:0 + 真实推荐资产 + recommendationReason),但 studio 从不调用、推荐理由仅 mock 造、UI 未渲染(盘点 C3)。接入: - useMarketRecommendations hook(返回 MarketAssetCard[],推荐失败不阻断主列表) - MarketBrowse「为你推荐」区:渲染推荐资产 + recommendationReason,点击进详情 - mock handler + vitest 推荐契约;market-recommendations.spec.ts 真后端 e2e - 连带修 live-read market 断言加 .first()(推荐位与列表可同时渲染同名资产→strict mode) 验证:全量 e2e 44→45/0;vitest 79→80;tsc/eslint 绿。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1686843e71
commit
cac3b8837f
@ -28,7 +28,8 @@ test.describe('活体只读旅程(真实后端,MSW off)', () => {
|
||||
test('市场浏览渲染真实资产(market)', async ({ page }) => {
|
||||
await seedToken(page);
|
||||
await page.goto('/market');
|
||||
await expect(page.getByText('活体市场资产·测试')).toBeVisible({ timeout: 15_000 });
|
||||
// .first():推荐位与资产列表可能同时渲染同一资产(推荐区新增),strict 模式需唯一匹配;渲染即证真实市场数据已加载。
|
||||
await expect(page.getByText('活体市场资产·测试').first()).toBeVisible({ timeout: 15_000 });
|
||||
});
|
||||
|
||||
test('个人中心渲染真实账户(account)', async ({ page }) => {
|
||||
|
||||
42
muse-studio/e2e/market-recommendations.spec.ts
Normal file
42
muse-studio/e2e/market-recommendations.spec.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { expect, test, type Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* 市场推荐位 e2e(真实后端,MSW off)——A 类切片:后端 GET /marketplace/recommendations 就绪、此前 studio 零调用。
|
||||
*
|
||||
* 反假绿:盘点(02F C3)确认后端推荐接口就绪(curl code:0 + 真实推荐资产 + recommendationReason),
|
||||
* 但前端从不调用、推荐理由仅 mock 造、UI 未渲染。本测进市场发现页 → 等真实 GET /marketplace/recommendations
|
||||
* → 验响应 code:0(证真后端就绪被 UI 消费)+「为你推荐」区按真实数据渲染。
|
||||
* 设计:不硬编码推荐条数/内容(后端推荐算法可变),依真实响应数据断言——有推荐则验区块 + 首个推荐名渲染,
|
||||
* 空推荐则不阻断(契合"推荐失败不阻断主列表"语义)。
|
||||
*/
|
||||
const TOKEN = 'test1';
|
||||
|
||||
async function seedToken(page: Page): Promise<void> {
|
||||
await page.addInitScript((t) => {
|
||||
window.localStorage.setItem('accessToken', t);
|
||||
window.localStorage.setItem('tenantId', '1');
|
||||
}, TOKEN);
|
||||
}
|
||||
|
||||
test('市场发现页渲染个性化推荐位(真实后端 recommendations)', async ({ page }) => {
|
||||
await seedToken(page);
|
||||
|
||||
// 进市场页 → 捕获真实 GET /marketplace/recommendations(随首屏发出)。
|
||||
const recoResp = page.waitForResponse(
|
||||
(r) =>
|
||||
/\/marketplace\/recommendations$/.test(new URL(r.url()).pathname) &&
|
||||
r.request().method() === 'GET',
|
||||
{ timeout: 15_000 }
|
||||
);
|
||||
await page.goto('/market');
|
||||
const resp = await recoResp;
|
||||
expect(resp.status()).toBe(200);
|
||||
const body = await resp.json();
|
||||
expect(body.code).toBe(0); // 后端推荐接口就绪(非 fail-closed)。
|
||||
|
||||
// 依真实响应断言:有推荐资产时「为你推荐」区渲染 + 首个推荐名可见(证真后端数据被 UI 消费)。
|
||||
if (Array.isArray(body.data) && body.data.length > 0) {
|
||||
await expect(page.getByRole('heading', { name: '为你推荐' })).toBeVisible({ timeout: 10_000 });
|
||||
await expect(page.getByText(String(body.data[0].name)).first()).toBeVisible({ timeout: 10_000 });
|
||||
}
|
||||
});
|
||||
@ -134,6 +134,11 @@ function toAssetCard(asset: MarketAssetDetail): MarketAssetCard {
|
||||
}
|
||||
|
||||
export const marketHandlers = [
|
||||
// GET /app-api/muse/marketplace/recommendations — 个性化推荐(含推荐理由),返回 MarketAssetCard 数组
|
||||
http.get('/app-api/muse/marketplace/recommendations', () => {
|
||||
const list = mockAssets.filter((asset) => asset.assetType === 'agent').map(toAssetCard);
|
||||
return HttpResponse.json({ code: 0, msg: 'success', data: list });
|
||||
}),
|
||||
// GET /app-api/muse/marketplace/assets — 市场资产发现列表
|
||||
http.get('/app-api/muse/marketplace/assets', ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
|
||||
@ -4,6 +4,7 @@ import {
|
||||
useAcquireMarketAsset,
|
||||
useInstallMarketAsset,
|
||||
useMarketplaceCategories,
|
||||
useMarketRecommendations,
|
||||
useMarketAssets,
|
||||
type MarketAssetType,
|
||||
} from '../hooks/useMarket';
|
||||
@ -26,6 +27,8 @@ export default function MarketBrowse() {
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const { data, isLoading, isError } = useMarketAssets(assetType, keyword);
|
||||
const { data: categories } = useMarketplaceCategories();
|
||||
// 个性化推荐(含推荐理由);推荐失败不阻断主列表——仅在有数据时渲染推荐区。
|
||||
const { data: recommendations } = useMarketRecommendations();
|
||||
const acquireAsset = useAcquireMarketAsset();
|
||||
const installAsset = useInstallMarketAsset();
|
||||
|
||||
@ -89,6 +92,30 @@ export default function MarketBrowse() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 个性化推荐位:展示后端推荐资产 + 推荐理由(recommendationReason),点击进资产详情。后端就绪、此前 UI 未渲染。 */}
|
||||
{recommendations && recommendations.length > 0 && (
|
||||
<section className="mt-6" aria-label="为你推荐">
|
||||
<h2 className="text-sm font-bold text-slate-700">为你推荐</h2>
|
||||
<div className="mt-3 grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||
{recommendations.slice(0, 3).map((rec) => (
|
||||
<Link
|
||||
key={rec.assetId}
|
||||
to={`/market/assets/${rec.assetId}`}
|
||||
className="rounded-2xl border border-indigo-100 bg-indigo-50/40 p-4 shadow-sm transition hover:border-indigo-300"
|
||||
>
|
||||
<div className="text-sm font-bold text-slate-800">{rec.name}</div>
|
||||
{rec.recommendationReason && (
|
||||
<div className="mt-1 text-xs font-medium text-indigo-600">荐 · {rec.recommendationReason}</div>
|
||||
)}
|
||||
<div className="mt-1 text-xs text-slate-400">
|
||||
{getAssetTypeLabel(rec.assetType)} · {rec.publisherName}
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<div className="mt-8">
|
||||
{isLoading ? (
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
useInstallMarketAsset,
|
||||
useMarketAssetDetail,
|
||||
useMarketAssets,
|
||||
useMarketRecommendations,
|
||||
} from './useMarket';
|
||||
|
||||
const createTestQueryClient = () =>
|
||||
@ -43,6 +44,17 @@ describe('useMarket API Hooks 单元测试', () => {
|
||||
expect(result.current.data?.list[0]?.assetType).toBe('agent');
|
||||
});
|
||||
|
||||
it('应能获取个性化推荐资产(含推荐理由 recommendationReason)', async () => {
|
||||
const { result } = renderHook(() => useMarketRecommendations(), { wrapper });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
// mock recommendations 返回 agent 资产;toAssetCard 给 agent 设 recommendationReason(证推荐理由被解出)。
|
||||
expect(Array.isArray(result.current.data)).toBe(true);
|
||||
expect(result.current.data?.length).toBeGreaterThan(0);
|
||||
expect(result.current.data?.[0]?.recommendationReason).toBe('适合长篇结构检查');
|
||||
});
|
||||
|
||||
it('应能获取授权后安装智能体资产', async () => {
|
||||
const assetId = '20000000-0000-4000-8000-000000000002';
|
||||
const { result: acquireHook } = renderHook(() => useAcquireMarketAsset(), { wrapper });
|
||||
|
||||
@ -42,6 +42,19 @@ export function useMarketplaceCategories() {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询市场个性化推荐资产(含推荐理由 recommendationReason),用于发现页推荐位。
|
||||
* 后端 listMarketplaceRecommendations 返回 MarketAssetCard 数组;推荐加载失败不阻断主列表
|
||||
* (调用方仅在有数据时渲染推荐区,hook 错误态不影响 useMarketAssets)。此前后端就绪、studio 零调用。
|
||||
*/
|
||||
export function useMarketRecommendations() {
|
||||
return useQuery<MarketAssetCard[], Error>({
|
||||
queryKey: ['marketRecommendations'],
|
||||
queryFn: () => api.get<MarketAssetCard[]>('/marketplace/recommendations'),
|
||||
staleTime: 60_000,
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取市场资产授权。授权不等于安装或绑定。 */
|
||||
export function useAcquireMarketAsset() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user