my-publish-records 经 MuseMarketAppealMapper.selectLatestByAssetIdAndUser + resolveAppealStatus 回填该物化资产被当前发布者发起的最新申诉态到记录 appealStatus (无物化资产/无申诉则 null;marketAssetId 与 appealStatus 两路共用各只查一次)。 前端 MarketPublish 发布记录副文本以中文徽标渲染「申诉:<待处理/审核中/已关闭…>」。 活体证(反假绿,MSW off 直连重建后单体 48080): - market-publish.spec.ts 第 8 例 chromium 绿(recId=5/marketAssetId=2 回显 appealStatus=closed→「申诉:已关闭」)。 - 后端 MarketPublishServiceTest 14/14 回归通过(appealMapper 注入无 NPE);前端 tsc 干净 + vitest 47/47 + market-publish 8/8。 至此市场生产者飞轮端到端完整:发布(草稿→检查→提交)→上架状态可视化→ 申诉(提交/补充材料/撤回)全生命周期 + 申诉态回显。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
182 lines
9.3 KiB
TypeScript
182 lines
9.3 KiB
TypeScript
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 校验失败。
|
||
// 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 });
|
||
});
|
||
});
|