import { expect, test, type Page } from '@playwright/test'; test.skip(true, '2.0.0 S5 单人版已移除市场、handoff 或多用户前端入口,本 spec 隔离保留待恢复'); /** * 市场生产侧发布草稿 e2e(真实后端,MSW off)。 * * 前置:vite 以 VITE_API_MOCK=false 运行(关 MSW)、真实 muse-server 在 48080。 * 目的:rendered studio 生产侧「发布到市场」表单 → 经唯一合法入口 POST /marketplace/publish-drafts * 把资产写为 Shadow 发布草稿,并经「我的发布记录」(GET /marketplace/my-publish-records) 真实回读渲染(反假绿)。 * 自包含:savePublishDraft 按材料 hash 派生幂等 commandId,故同输入可重复跑(命中回放、不重复建),无需预置 fixture。 */ const TOKEN = 'test1'; // 稳定标题 → 后端材料 hash 幂等键稳定 → 重复跑命中命令回放、不产生重复草稿。 const DRAFT_NAME = '活体发布草稿·e2e'; async function seedToken(page: Page): Promise { await page.addInitScript((t) => { window.localStorage.setItem('accessToken', t); window.localStorage.setItem('tenantId', '1'); }, TOKEN); } test.describe('市场生产侧发布草稿(真实后端, MSW off)', () => { test('保存发布草稿→真打 publish-drafts→「我的发布记录」出现该资产', async ({ page }) => { await seedToken(page); await page.goto('/market/publish'); await page.getByPlaceholder(/在此处输入市场展示标题/).fill(DRAFT_NAME); const [resp] = await Promise.all([ page.waitForResponse( (r) => /\/marketplace\/publish-drafts$/.test(r.url()) && r.request().method() === 'POST', { timeout: 15_000 } ), page.getByRole('button', { name: /保存发布草稿/ }).click(), ]); // Yudao 口径:成功为 HTTP 200 + CommonResult{code:0};草稿落库返回 draftId + status=draft。 expect(resp.status()).toBe(200); const body = await resp.json(); expect(body.code).toBe(0); expect(typeof body.data?.draftId).toBe('number'); expect(body.data?.status).toBe('draft'); // 「我的发布记录」刷新后出现该草稿(证后端 my-publish-records 把草稿作为记录真实回读渲染)。 await expect(page.getByText(DRAFT_NAME).first()).toBeVisible({ timeout: 15_000 }); }); test('缺市场标题被后端拒绝(@NotBlank, 非成功业务码)', async ({ page }) => { await seedToken(page); // 用 page.request 直接打接口绕 UI:缺 name → 后端 @NotBlank 校验失败。 // WHY:page.request 绕开 client.ts,需手动带 X-API-Version / Authorization / tenant-id。 const resp = await page.request.post('/app-api/muse/marketplace/publish-drafts', { headers: { 'X-API-Version': '1', Authorization: `Bearer ${TOKEN}`, 'tenant-id': '1', 'Content-Type': 'application/json', }, data: { assetType: 'agent', sourceId: 1 }, // 故意缺 name }); const body = await resp.json(); expect(body.code).not.toBe(0); // @NotBlank → 业务错误码(Yudao 包成 200+code≠0 或 400) }); test('提交审核全链(保存草稿→运行检查→提交申请)→申请 status=submitted', async ({ page }) => { await seedToken(page); await page.goto('/market/publish'); // 唯一标题:全链会 consumePassedCheck(消费检查),用唯一名每次产新 draft/check/request → 可重复跑(不被回放卡死)。 const uniqueName = `活体上架·e2e-${Date.now()}`; await page.getByPlaceholder(/在此处输入市场展示标题/).fill(uniqueName); // licenseType + 权利声明 已有默认值(发布检查硬门槛),无需额外填写即可通过检查。 // 点「提交审核」触发 save→check→submit 三段链;等待最后一段 publish-requests 的真实写入响应。 const [submitResp] = await Promise.all([ page.waitForResponse( (r) => /\/marketplace\/publish-requests$/.test(r.url()) && r.request().method() === 'POST', { timeout: 20_000 } ), page.getByRole('button', { name: /提交审核/ }).click(), ]); expect(submitResp.status()).toBe(200); const submitBody = await submitResp.json(); expect(submitBody.code).toBe(0); expect(submitBody.data?.status).toBe('submitted'); expect(String(submitBody.data?.requestId ?? '')).not.toBe(''); // 提交后该资产以 submitted 态进入「我的发布记录」(证 my-publish-records 真实回读已提交申请)。 await expect(page.getByText(uniqueName).first()).toBeVisible({ timeout: 15_000 }); }); // 上架状态可视化(读):上架=审核通过自动 markListed(生产者无主动上架动作),生产者侧通过「我的发布记录」 // 的生命周期徽标看到 已上架/已驳回 等终态。前置:/tmp/SetReqStatus.java 已把两条发布申请置为 listed / rejected。 test('我的发布记录渲染发布生命周期徽标(已上架/已驳回)', async ({ page }) => { await seedToken(page); await page.goto('/market/publish'); await expect(page.getByText('已上架').first()).toBeVisible({ timeout: 15_000 }); await expect(page.getByText('已驳回').first()).toBeVisible({ timeout: 15_000 }); }); // 申诉写路:被驳回且已物化(marketAssetId 非空)的记录展示「发起申诉」→ 填理由 → 真打 POST /marketplace/appeals → pending。 // 前置:/tmp/SeedAppeal.java 造 asset(pub=1)+rejected request(asset_id=该物化资产) → 记录 marketAssetId 非空、status=rejected。 // 可重复:申诉允许多次(每次新 commandId 产新 appeal),按钮基于 marketAssetId+状态恒在,无需重置。 test('被驳回资产「发起申诉」→ 真打 appeals → 申诉已提交(pending)', async ({ page }) => { await seedToken(page); await page.goto('/market/publish'); const appealBtn = page.getByRole('button', { name: '发起申诉' }).first(); await expect(appealBtn).toBeVisible({ timeout: 15_000 }); await appealBtn.click(); // 申诉面板展开 → 提交申诉(理由有默认值)→ 等待真实 POST /marketplace/appeals 写入响应。 const [resp] = await Promise.all([ page.waitForResponse( (r) => /\/marketplace\/appeals$/.test(r.url()) && r.request().method() === 'POST', { timeout: 15_000 } ), page.getByRole('button', { name: '提交申诉' }).click(), ]); expect(resp.status()).toBe(200); const body = await resp.json(); expect(body.code).toBe(0); expect(body.data?.status).toBe('pending'); expect(String(body.data?.appealId ?? '')).not.toBe(''); await expect(page.getByText(/申诉已提交/)).toBeVisible({ timeout: 15_000 }); }); // 撤回申诉写路:「我的申诉」中非终态申诉展示「撤回」→ POST .../withdraw(expectedStatus 乐观锁)→ 已撤回。 // 前置:存在 ≥1 个可撤回(pending/reviewing/supplementing)申诉(种子 appeals 1/2/3 + 申诉提交测产生); // 撤回 .first()=最新申诉(DESC 序),不会碰到种子的 supplementing appeal#1。 test('我的申诉「撤回」→ 真打 withdraw → 已撤回', async ({ page }) => { await seedToken(page); await page.goto('/market/publish'); const withdrawBtn = page.getByRole('button', { name: '撤回', exact: true }).first(); await expect(withdrawBtn).toBeVisible({ timeout: 15_000 }); const [resp] = await Promise.all([ page.waitForResponse( (r) => /\/marketplace\/appeals\/\d+\/withdraw$/.test(r.url()) && r.request().method() === 'POST', { timeout: 15_000 } ), withdrawBtn.click(), ]); expect(resp.status()).toBe(200); expect((await resp.json()).code).toBe(0); await expect(page.getByText(/已撤回/)).toBeVisible({ timeout: 15_000 }); }); // 补充材料写路:supplementing 态申诉展示「补充材料」→ 填说明 → POST .../supplements(privacyConfirmed)→ 材料已补充。 // 前置:appeal#1 置 supplementing(种子 /tmp/SetAppealSup.java);提交后→reviewing,故每轮重置。 test('我的申诉「补充材料」→ 真打 supplements → 材料已补充', async ({ page }) => { await seedToken(page); await page.goto('/market/publish'); const supplementBtn = page.getByRole('button', { name: '补充材料' }).first(); await expect(supplementBtn).toBeVisible({ timeout: 15_000 }); await supplementBtn.click(); const [resp] = await Promise.all([ page.waitForResponse( (r) => /\/marketplace\/appeals\/\d+\/supplements$/.test(r.url()) && r.request().method() === 'POST', { timeout: 15_000 } ), page.getByRole('button', { name: '提交补充' }).click(), ]); expect(resp.status()).toBe(200); expect((await resp.json()).code).toBe(0); await expect(page.getByText(/材料已补充/)).toBeVisible({ timeout: 15_000 }); }); // appealStatus 记录回显:有申诉的物化资产记录在「我的发布记录」副文本显示「申诉:<中文状态>」(后端 my-publish-records join 申诉表)。 // 前置:asset_id=2 的记录(marketAssetId 非空)已有申诉(种子 + 申诉提交测产生)。 test('我的发布记录回显 appealStatus(申诉:中文状态)', async ({ page }) => { await seedToken(page); await page.goto('/market/publish'); await expect( page.getByText(/申诉:(待处理|审核中|待补充材料|已关闭|维持原判|已恢复|部分恢复)/).first() ).toBeVisible({ timeout: 15_000 }); }); });