oh-my-muse/muse-studio/e2e/market-publish.spec.ts
zizi 6dcabf69fb feat(solo): 收口治理入口并裁剪 studio 表面
完成 2.0.0 S4 的外部 AI、admin、account 与 solo SQL 收口,并推进 S5 studio 市场/handoff/旧编辑器入口裁剪。S5 的前端单测、构建与 route-only e2e 已留证;MSW-off 创作主线 e2e 仍因本地后端活体启动口径待修正,已在总账标为未完成验收。
2026-07-08 11:45:26 +08:00

184 lines
9.4 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';
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<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 });
});
// 撤回申诉写路:「我的申诉」中非终态申诉展示「撤回」→ 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 });
});
});