oh-my-muse/muse-studio/e2e/market-publish.spec.ts
lili a00c7581c9 feat(market): 申诉(appeal)写路 UI 打通——后端暴露物化 assetId 解 gap
根因:my-publish-records 的 assetId 是 publish-record id(草稿/申请占位),
非 submitAppeal.requireAsset 所需的物化 muse_market_asset.id(资产仅在 admin
审核通过 markListed 时物化),生产者侧 UI 取不到正确 assetId。

后端最小契约改动(muse-module-market):
- PublishRecordItem / MarketPublishRecordItemRespVO 加 marketAssetId 字段;
- MarketPublishServiceImpl.resolveMarketAssetId:request.asset_id 命中真实
  muse_market_asset 且归属当前发布者才返回其 id,否则 null(fail-closed,不放开入口)。

前端(muse-studio):
- useSubmitAppeal hook(POST /marketplace/appeals,新 commandId 幂等);
- MarketPublish「我的发布记录」:仅 marketAssetId 非空 + 状态可申诉
  (rejected/compliance_blocked→review_rejection、delisted→delist、recalled→recall)
  的记录放开「发起申诉」→ 面板填理由 → 提交 → 申诉已提交(pending)。

活体证(反假绿,MSW off 直连重建后单体 48080):
- market-publish.spec.ts 第 5 例 chromium 绿(点发起申诉→填→提交→pending)。
- DB 证:muse_market_appeal 追加行 status=pending、commandId 为前端 UUID(e2e 点击产 appealId=3)。
- curl 正负路:自有已驳回→appealId/pending;他人资产→1044000024 无权访问;不存在→1044000003 资产不存在。
- 后端 MarketPublishServiceTest 14/14 回归通过;前端 tsc 干净 + vitest 47/47 + market-publish 5/5。
- 种子 /tmp/SeedAppeal.java(asset(pub=1)+rejected request,asset_id=物化资产)。

仍 :补充材料(supplements)/撤回(withdraw)申诉 UI、记录上 appealStatus 回显。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 06:33:44 -07:00

130 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { expect, test, type Page } from '@playwright/test';
/**
* 市场生产侧发布草稿 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<void> {
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 校验失败。
// WHYpage.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 });
});
});