test(admin-e2e): market 子域转真后端真连(下架/申诉真打+真 PG 反查落库)
market 5 个 skip 用例全是 MSW payload 拦截的负向断言(验前端预览绑定守卫不发陈旧 payload、靠 museAdminPayloadsByPage),真后端 MSW-off 无 payload 收集器无法逐字搬、且纯前端逻辑已 vitest 覆盖,故替换为真后端真连用例。 真连(反假绿:不只看 UI toast,真 PG 反查 muse_market_* 真实行):①市场治理页渲染三工作台(按激活 tabpanel 收口,ant Tabs 不卸载隐藏 pane);②下架真打命令→真 PG 反查 listing_status=delisted+新增 1 条 applied delist 治理动作;③申诉 maintained 真打处理→真 PG 反查 status=maintained、resolver_id=1。global-setup 加 resetMarketFixture(资产复位 listed+申诉复位 supplementing+软删历轮累积终态申诉(42 条 closed 挤出首页锚点)+清理治理动作/预览)、每轮幂等自愈。 揪出 mock 臆想契约:assetId/appealId 是数字串(非 asset-e2e);publish status DB submitted→API pending(后端 toOpenApiStatus 唯一特例、mock 四态不成立);appeal.resolution 列存处理理由文本非结论枚举(结论在 status);delist expectedStatus 只接受 listed(传别的 400);preview actionType 实为 5 值(含 review_rejection)非 4 值。 isIgnoredRuntimeIssue 增忽略 Failed to load resource: net::ERR_*(资源懒加载抖动、非应用错误,命令正确性另有真 PG 反查兜底)——同解 account 首跑偶发红根因(原误判 console.error,实为 dev-server 负载下 chunk 懒加载超时)。独立连跑全量 account+market e2e x2 各 4 passed;psql 复核 asset1=delisted/appeal1=maintained resolver=1/gov_action=1,与报告一致。restore 票据路径 e2e 未覆盖(用例走 maintained、后端不消费 restore 预览),restore 仍只 mock-service 单测无真 PG IT、缺口待补(:397 注释标注)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c5d1762faa
commit
ce637ac2b6
@ -17,6 +17,12 @@ import { Client } from 'pg';
|
||||
*
|
||||
* 说明:account 子域只读 users/usage-records/purchase-records 三个列表端点,其数据由 muse_slice_live 基础种子
|
||||
* 提供(user1 的 account profile / purchase 记录已存在),本前置不重置 account fixture,仅补齐权限前提。
|
||||
*
|
||||
* market 子域(资产治理/发布审核/申诉处理)是写命令子域:e2e 会真打下架/召回/审核/申诉处理命令,直接改
|
||||
* muse_slice_live 共享行。为保证每轮可复现绿(反"上一轮把资产下架/申诉处理成终态导致本轮按钮 disabled"),
|
||||
* 本前置在权限 seed 后再幂等复位 market fixture(见 resetMarketFixture):把稳定锚点资产/申诉复位到 e2e 期望
|
||||
* 初态,并清理历轮累积的治理预览/治理动作。复位按 tenant_id=1(mock-token user1 所属租户)精确限定,只碰 e2e
|
||||
* 锚点行(资产 1/4、申诉 1、资产 2),不影响 account/publish 等其他子域数据。
|
||||
*/
|
||||
async function globalSetup(): Promise<void> {
|
||||
if (process.env.E2E_SKIP_SEED === '1') {
|
||||
@ -63,6 +69,9 @@ async function globalSetup(): Promise<void> {
|
||||
console.log(
|
||||
`[admin e2e globalSetup] seed system_user_role(super_admin) rowCount=${seed.rowCount}(0=已存在)`,
|
||||
);
|
||||
|
||||
// 复位 market 写命令子域 fixture(详见函数注释)。
|
||||
await resetMarketFixture(client);
|
||||
} finally {
|
||||
await client.end();
|
||||
}
|
||||
@ -90,4 +99,86 @@ async function globalSetup(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 复位 market 子域 e2e fixture(幂等、tenant_id=1 限定)。
|
||||
*
|
||||
* market e2e 锚点(由 muse_slice_live 基础种子提供,本函数只复位状态,不新建资产/申诉):
|
||||
* - 资产 id=1「活体市场资产·测试」: listed,e2e 在「资产治理」tab 对它做影响预览/下架(真打命令改 listing_status)。
|
||||
* - 资产 id=4「活体市场资产·安装e2e」: listed,资产列表只读断言锚点(与 account 子域共用)。
|
||||
* - 资产 id=2「活体申诉演练资产」: not_listed,申诉 1 的关联资产。
|
||||
* - 申诉 id=1: supplementing(开放态),e2e 在「申诉处理」tab 对它生成影响预览并以 maintained 结论处理。
|
||||
*
|
||||
* 为什么要复位:下架/召回会把资产 listing_status 改成 delisted/recalled、申诉处理会把 status 推进到终态,
|
||||
* 都会让下一轮的「下架/处理」按钮 disabled(前端 canDelistAsset/canResolveAppeal 只放行特定初态),导致
|
||||
* 第二轮假红。复位把锚点拉回初态,并删除历轮累积的治理预览(每次预览写一行 ready)和治理动作(每次下架写一行),
|
||||
* 让 e2e 对 muse_market_governance_action 的"本轮新增一行"DB 断言可精确成立。
|
||||
*
|
||||
* 复位是 best-effort 的硬前置:若锚点资产/申诉不存在(切片库未灌基础种子),抛错让套件 fail-fast,
|
||||
* 避免 market e2e 在缺数据时跑出误导性的失败。
|
||||
*/
|
||||
async function resetMarketFixture(client: Client): Promise<void> {
|
||||
// 锚点存在性校验:缺任一锚点说明切片库未就绪,直接报错(market e2e 不可能真绿)。
|
||||
const anchors = await client.query(
|
||||
`SELECT id, listing_status FROM muse_market_asset
|
||||
WHERE tenant_id = 1 AND deleted = false AND id IN (1, 2, 4)`,
|
||||
);
|
||||
const anchorIds = new Set(anchors.rows.map((row) => Number(row.id)));
|
||||
const missingAssets = [1, 2, 4].filter((id) => !anchorIds.has(id));
|
||||
const appeal = await client.query(
|
||||
`SELECT id, status FROM muse_market_appeal
|
||||
WHERE tenant_id = 1 AND deleted = false AND id = 1`,
|
||||
);
|
||||
if (missingAssets.length > 0 || appeal.rowCount === 0) {
|
||||
throw new Error(
|
||||
`[admin e2e globalSetup] market 锚点缺失(缺资产 ${missingAssets.join(',') || '无'}、` +
|
||||
`申诉1 ${appeal.rowCount === 0 ? '缺失' : '存在'});` +
|
||||
'请确认 muse_slice_live 已灌 market 基础种子,或置 E2E_SKIP_SEED=1 跳过。',
|
||||
);
|
||||
}
|
||||
|
||||
// 1) 资产 1/4 复位为已上架(撤销历轮下架/召回);仅状态偏离时才写,rowCount 反映实际修复条数。
|
||||
const resetAssets = await client.query(
|
||||
`UPDATE muse_market_asset
|
||||
SET listing_status = 'listed', status = 'active'
|
||||
WHERE tenant_id = 1 AND deleted = false AND id IN (1, 4)
|
||||
AND (listing_status <> 'listed' OR status <> 'active')`,
|
||||
);
|
||||
|
||||
// 2) 申诉 1 复位为 supplementing 开放态(撤销历轮 maintained/restored 处理结论)。
|
||||
const resetAppeal = await client.query(
|
||||
`UPDATE muse_market_appeal
|
||||
SET status = 'supplementing', resolution = NULL, resolver_id = NULL, resolved_at = NULL
|
||||
WHERE tenant_id = 1 AND deleted = false AND id = 1 AND status <> 'supplementing'`,
|
||||
);
|
||||
|
||||
// 2b) 软删历轮累积的终态申诉(关联资产 2、非锚点 id≠1):前端申诉 tab 只拉 pageNo=1/pageSize=20 且按 id DESC,
|
||||
// 历轮 e2e 处理出的 closed/maintained 申诉(都挂在资产 2 上)会把唯一开放态锚点申诉 1(最老 id)挤出首页,
|
||||
// 导致申诉 tab 看不到可处理行。软删(deleted=true)让列表(查询过滤 deleted=false)只剩锚点申诉 1,
|
||||
// 使行定位唯一、按钮可点;不动用户真实申诉(仅清 e2e 在资产 2 上造的终态噪声),也不碰锚点申诉 1。
|
||||
const softDeleteClosedAppeals = await client.query(
|
||||
`UPDATE muse_market_appeal
|
||||
SET deleted = true
|
||||
WHERE tenant_id = 1 AND deleted = false AND id <> 1 AND asset_id = 2
|
||||
AND status IN ('closed', 'maintained', 'restored')`,
|
||||
);
|
||||
|
||||
// 3) 清理历轮累积的治理动作与治理预览(只清 e2e 锚点资产 1/2/4 的行),让本轮 DB 断言可精确计数。
|
||||
// 申诉处理(maintained)不写治理动作,故这里清理足以让"下架本轮新增一行 delist 治理动作"成立。
|
||||
const clearActions = await client.query(
|
||||
`DELETE FROM muse_market_governance_action
|
||||
WHERE tenant_id = 1 AND asset_id IN (1, 2, 4)`,
|
||||
);
|
||||
const clearPreviews = await client.query(
|
||||
`DELETE FROM muse_market_governance_preview
|
||||
WHERE tenant_id = 1 AND asset_id IN (1, 2, 4)`,
|
||||
);
|
||||
|
||||
console.log(
|
||||
`[admin e2e globalSetup] reset market fixture: assets=${resetAssets.rowCount} ` +
|
||||
`appeal=${resetAppeal.rowCount} softDeleteClosedAppeals=${softDeleteClosedAppeals.rowCount} ` +
|
||||
`clearActions=${clearActions.rowCount} clearPreviews=${clearPreviews.rowCount}` +
|
||||
`(均为修复/清理条数,0=已是初态)`,
|
||||
);
|
||||
}
|
||||
|
||||
export default globalSetup;
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { Client } from 'pg';
|
||||
import { expect, test } from 'playwright/test';
|
||||
import type { Page } from 'playwright/test';
|
||||
|
||||
@ -7,6 +8,73 @@ const commonResult = (data: unknown) => ({
|
||||
msg: '',
|
||||
});
|
||||
|
||||
// market 子域是写命令子域:e2e 真打下架/申诉处理命令后,必须直连真实 PG 核验 muse_market_* 真实行
|
||||
// (反假绿:不能只看 UI toast,要看 DB listing_status/governance_action 真的变了)。连接凭据从环境变量读
|
||||
// (MUSE_POSTGRES_*,见 ~/.config/muse-repo/infra.env;绝不入库),与 global-setup 同源。
|
||||
async function withMarketDb<T>(fn: (client: Client) => Promise<T>): Promise<T> {
|
||||
const client = new Client({
|
||||
host: process.env.MUSE_POSTGRES_HOST,
|
||||
port: Number(process.env.MUSE_POSTGRES_PORT ?? '5433'),
|
||||
database: process.env.MUSE_POSTGRES_DATABASE ?? 'muse_slice_live',
|
||||
user: process.env.MUSE_POSTGRES_USERNAME ?? 'root',
|
||||
password: process.env.MUSE_POSTGRES_PASSWORD,
|
||||
ssl: false,
|
||||
connectionTimeoutMillis: 10_000,
|
||||
});
|
||||
await client.connect();
|
||||
try {
|
||||
return await fn(client);
|
||||
} finally {
|
||||
await client.end();
|
||||
}
|
||||
}
|
||||
|
||||
/** 读取 market 锚点资产当前上架状态(tenant_id=1),用于真打命令后的 DB 反查。 */
|
||||
async function queryAssetListingStatus(assetId: number): Promise<string> {
|
||||
return withMarketDb(async (client) => {
|
||||
const result = await client.query(
|
||||
`SELECT listing_status FROM muse_market_asset
|
||||
WHERE tenant_id = 1 AND deleted = false AND id = $1`,
|
||||
[assetId],
|
||||
);
|
||||
return result.rows[0]?.listing_status ?? '';
|
||||
});
|
||||
}
|
||||
|
||||
/** 统计某资产某类型 applied 治理动作行数(tenant_id=1),用于"真打下架本轮新增治理动作"DB 断言。 */
|
||||
async function countGovernanceAction(
|
||||
assetId: number,
|
||||
actionType: string,
|
||||
): Promise<number> {
|
||||
return withMarketDb(async (client) => {
|
||||
const result = await client.query(
|
||||
`SELECT count(*)::int AS c FROM muse_market_governance_action
|
||||
WHERE tenant_id = 1 AND asset_id = $1 AND action_type = $2 AND status = 'applied'`,
|
||||
[assetId, actionType],
|
||||
);
|
||||
return result.rows[0]?.c ?? 0;
|
||||
});
|
||||
}
|
||||
|
||||
/** 读取申诉当前状态/结论/处理人(tenant_id=1),用于真打申诉处理命令后的 DB 反查。 */
|
||||
async function queryAppealFacts(
|
||||
appealId: number,
|
||||
): Promise<{ resolution: null | string; resolverId: null | string; status: string }> {
|
||||
return withMarketDb(async (client) => {
|
||||
const result = await client.query(
|
||||
`SELECT status, resolution, resolver_id FROM muse_market_appeal
|
||||
WHERE tenant_id = 1 AND deleted = false AND id = $1`,
|
||||
[appealId],
|
||||
);
|
||||
const row = result.rows[0] ?? {};
|
||||
return {
|
||||
resolution: row.resolution ?? null,
|
||||
resolverId: row.resolver_id == null ? null : String(row.resolver_id),
|
||||
status: row.status ?? '',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// 运行时确认的真实 Pinia 持久化 key:namespace=`${VITE_APP_NAMESPACE}-${VITE_APP_VERSION}-${env}`,
|
||||
// 而 VITE_APP_NAMESPACE 未在任何 .env 定义 → 运行时取值为字符串 'undefined';VITE_APP_VERSION=package.json 版本 5.7.0;
|
||||
// dev 环境 env='dev';persist 后缀 store id='core-access'(见 packages/stores setup.ts `${namespace}-${storeKey}`)。
|
||||
@ -22,6 +90,13 @@ const museAdminPayloadsByPage = new WeakMap<Page, Record<string, unknown>[]>();
|
||||
const isIgnoredRuntimeIssue = (message: string) =>
|
||||
/ResizeObserver loop (completed with undelivered notifications|limit exceeded)/i.test(
|
||||
message,
|
||||
) ||
|
||||
// dev-server 活体 e2e 噪声:并发负载下 Vite 偶发某个懒加载 chunk/静态资源请求超时,
|
||||
// 浏览器报 `Failed to load resource: net::ERR_TIMED_OUT` 的 console.error。这是环境/网络抖动,
|
||||
// 不是应用正确性信号(命令/契约断言另有真后端+真 PG 反查兜底),故与 ResizeObserver 噪声一并忽略,
|
||||
// 避免严格 console.error 门禁把可复现的资源超时误判成红。真正的组件解析失败/未捕获 rejection 仍会命中。
|
||||
/Failed to load resource: net::ERR_(TIMED_OUT|CONNECTION_|ABORTED|FAILED)/i.test(
|
||||
message,
|
||||
);
|
||||
|
||||
// 转真后端后不再有"未处理的 muse mock"概念(muse 端点直打 48080);保留同名空壳让既有调用点与
|
||||
@ -252,210 +327,129 @@ test.skip('任务取消和来源传播重试提交 commandId 与 reason', async
|
||||
expectNoUnhandledMuseAdminApis(page);
|
||||
});
|
||||
|
||||
test.skip('市场下架不能复用其他资产的影响预览票据', async ({ page }) => {
|
||||
// market 子域 —— 转真后端(MSW-off,真打 48080 + 真 PG 反查)。
|
||||
// 锚点全部对齐 muse_slice_live 真数据(curl + psql 实证,见 global-setup resetMarketFixture 注释):
|
||||
// 资产列表真返资产 1「活体市场资产·测试」(listed)、4「活体市场资产·安装e2e」(listed)、2「活体申诉演练资产」(not_listed);
|
||||
// 申诉列表真返开放态申诉 1(supplementing,关联资产 2,review_rejection 类型);
|
||||
// 发布审核队列真返 status=pending(后端把 DB 'submitted' 映射成 'pending')的真发布申请。
|
||||
// 真分页 wrapper={pageNo,pageSize,total,list},前端 PageResult 只取 list/total。
|
||||
test('市场治理页对真后端 market 子域渲染三大工作台', async ({ page }) => {
|
||||
await page.goto('/muse/market');
|
||||
|
||||
// 页头与边界文案。
|
||||
await expect(page.getByRole('heading', { name: '市场治理' })).toBeVisible();
|
||||
await expect(page.getByText('不修改用户私有副本').first()).toBeVisible();
|
||||
|
||||
// 审核队列 tab(默认 active):真发布申请(后端 'submitted'→'pending')。
|
||||
// 注意 ant Tabs 不卸载非激活 pane,锚点文案在多个 pane 都存在 → 必须按激活 tabpanel 收口可见性断言。
|
||||
const reviewPanel = page.getByRole('tabpanel', { name: '审核队列' });
|
||||
await expect(reviewPanel.getByText('市场审核是发布硬门槛')).toBeVisible();
|
||||
|
||||
// 资产治理 tab:真资产行(脱敏摘要,无私有正文)。
|
||||
await page.getByRole('tab', { name: '资产治理' }).click();
|
||||
const assetPanel = page.getByRole('tabpanel', { name: '资产治理' });
|
||||
await expect(assetPanel.getByText('不改用户私有副本')).toBeVisible();
|
||||
await expect(assetPanel.getByText('活体市场资产·测试')).toBeVisible();
|
||||
await expect(assetPanel.getByText('活体市场资产·安装e2e')).toBeVisible();
|
||||
|
||||
// 申诉处理 tab:真开放态申诉(关联资产 2)。
|
||||
await page.getByRole('tab', { name: '申诉处理' }).click();
|
||||
const appealPanel = page.getByRole('tabpanel', { name: '申诉处理' });
|
||||
await expect(appealPanel.getByText('恢复上架不能绕过合规审核')).toBeVisible();
|
||||
await expect(appealPanel.getByText('活体申诉演练资产')).toBeVisible();
|
||||
|
||||
expectNoUnhandledMuseAdminApis(page);
|
||||
});
|
||||
|
||||
// 市场资产下架真命令:在「资产治理」tab 对锚点资产 1 生成真影响预览 → 真打下架命令 →
|
||||
// 真 PG 反查 listing_status='delisted' + 新增一条 applied delist 治理动作(反假绿:不只看 UI toast)。
|
||||
// 注意:本用例真实改 muse_slice_live 共享行(资产 1 → delisted);下一轮 globalSetup.resetMarketFixture
|
||||
// 会把它复位回 listed。同一轮内本用例必须排在"召回"等同样针对资产 1 的写用例之前。
|
||||
test('市场资产下架真打命令并落库脱敏治理动作', async ({ page }) => {
|
||||
const assetId = 1;
|
||||
// 前置真值核验:资产 1 必须处于 listed(globalSetup 已复位),否则前端"下架"按钮逻辑前提不成立。
|
||||
expect(await queryAssetListingStatus(assetId)).toBe('listed');
|
||||
|
||||
await page.goto('/muse/market');
|
||||
await page.getByRole('tab', { name: '资产治理' }).click();
|
||||
await page
|
||||
.getByRole('row', { name: /^市场资产/ })
|
||||
.getByRole('button')
|
||||
.first()
|
||||
.click();
|
||||
await page.locator('.ant-modal-footer .ant-btn-primary').click();
|
||||
|
||||
// 锚点资产 1 行(按激活 tabpanel 收口,避开 ant 不卸载的隐藏 pane 同名行)。
|
||||
// ant-design-vue 会在两个中文字按钮间插空格,故"下架"可达名是「下 架」→ 用 /下\s*架/ 容错。
|
||||
const assetPanel = page.getByRole('tabpanel', { name: '资产治理' });
|
||||
const assetRow = assetPanel.getByRole('row', { name: /活体市场资产·测试/ });
|
||||
await assetRow.getByRole('button', { name: /下\s*架/ }).click();
|
||||
|
||||
const dialog = page.getByRole('dialog', { name: /市场治理:活体市场资产·测试/ });
|
||||
const previewInput = dialog.getByPlaceholder('先执行影响预览后自动带入');
|
||||
|
||||
// 真生成影响预览:后端返回 gov-preview-* 票据,前端回写到只读输入框。
|
||||
await dialog.getByRole('button', { name: '生成当前对象影响预览' }).click();
|
||||
await expect(previewInput).toHaveValue(/^gov-preview-/);
|
||||
|
||||
// 填可审计原因并真打下架命令。
|
||||
await dialog.locator('textarea').fill('e2e 真后端下架验证');
|
||||
await dialog.locator('.ant-modal-footer .ant-btn-primary').click();
|
||||
|
||||
// UI 反馈:成功 toast(命令 code:0)。
|
||||
await expect(page.getByText('市场资产下架命令已提交')).toBeVisible();
|
||||
|
||||
// 反假绿核心:真 PG 反查资产真的下架 + 真的落了一条 applied delist 治理动作。
|
||||
await expect
|
||||
.poll(() =>
|
||||
museAdminPayloadsByPage
|
||||
.get(page)
|
||||
?.find(
|
||||
(item) =>
|
||||
item.method === 'POST' &&
|
||||
item.path === '/market/assets/asset-e2e/governance-impact',
|
||||
)?.payload,
|
||||
)
|
||||
.toEqual(
|
||||
expect.objectContaining({
|
||||
actionType: 'delist',
|
||||
commandId: expect.stringMatching(/^market-impact-preview-/),
|
||||
scope: 'stop_new_acquire',
|
||||
}),
|
||||
);
|
||||
await page.locator('.ant-modal-footer .ant-btn-default').click();
|
||||
.poll(() => queryAssetListingStatus(assetId), { timeout: 10_000 })
|
||||
.toBe('delisted');
|
||||
expect(await countGovernanceAction(assetId, 'delist')).toBe(1);
|
||||
|
||||
await page
|
||||
.getByRole('row', { name: /另一市场资产/ })
|
||||
.getByRole('button')
|
||||
.nth(1)
|
||||
.click();
|
||||
await expect(page.getByPlaceholder('先执行影响预览后自动带入')).toHaveValue('');
|
||||
await page
|
||||
.locator('.ant-modal textarea')
|
||||
.fill('尝试复用上一资产影响预览');
|
||||
await page.locator('.ant-modal-footer .ant-btn-primary').click();
|
||||
|
||||
const delistPayload = museAdminPayloadsByPage
|
||||
.get(page)
|
||||
?.find(
|
||||
(item) =>
|
||||
item.method === 'POST' && item.path === '/market/assets/asset-two/delist',
|
||||
)?.payload;
|
||||
|
||||
expect(delistPayload).toBeUndefined();
|
||||
expectNoUnhandledMuseAdminApis(page);
|
||||
});
|
||||
|
||||
test.skip('市场下架切换范围后必须重新生成影响预览', async ({ page }) => {
|
||||
await page.goto('/muse/market');
|
||||
await page.getByRole('tab', { name: '资产治理' }).click();
|
||||
await page
|
||||
.getByRole('row', { name: /^市场资产/ })
|
||||
.getByRole('button')
|
||||
.nth(1)
|
||||
.click();
|
||||
// 市场申诉处理真命令:在「申诉处理」tab 对开放态申诉 1(supplementing)生成真影响预览 →
|
||||
// 以默认结论 maintained 真打处理命令 → 真 PG 反查申诉 status='maintained'、resolver_id=当前 admin(1)。
|
||||
//
|
||||
// restore 断层留意(诚实记录):本用例走 maintained 结论,后端不消费 restore 影响预览票据
|
||||
// (MarketAppealServiceImpl 仅 restored/partially_restored 才走 restoreAssetByPreview→consumeReadyPreview)。
|
||||
// appeal restore 票据一次性消费路径只有 mock-service 单测、无真 PG IT,且本 e2e 未覆盖 restore 分支
|
||||
// (FE openAppealAction 默认 resolution=maintained,本用例不切到 restored)。restore 真后端覆盖缺口待补。
|
||||
test('市场申诉以 maintained 结论真打处理命令并落库', async ({ page }) => {
|
||||
const appealId = 1;
|
||||
// 前置真值核验:申诉 1 必须为开放态 supplementing(globalSetup 已复位),前端"处理"按钮才放行。
|
||||
const before = await queryAppealFacts(appealId);
|
||||
expect(before.status).toBe('supplementing');
|
||||
|
||||
const dialog = page.getByRole('dialog', { name: '市场治理:市场资产' });
|
||||
const previewInput = dialog.getByPlaceholder('先执行影响预览后自动带入');
|
||||
|
||||
await dialog.getByRole('button', { name: '生成当前对象影响预览' }).click();
|
||||
await expect(previewInput).toHaveValue(
|
||||
'preview-asset-e2e-delist-stop_new_acquire',
|
||||
);
|
||||
|
||||
await dialog.locator('.ant-select-selector').click();
|
||||
await page
|
||||
.locator('.ant-select-dropdown:not(.ant-select-dropdown-hidden)')
|
||||
.getByText('停止新绑定')
|
||||
.click();
|
||||
|
||||
await expect(previewInput).toHaveValue('');
|
||||
await dialog.locator('textarea').fill('切换范围后不能复用旧影响预览');
|
||||
await dialog.locator('.ant-modal-footer .ant-btn-primary').click();
|
||||
|
||||
const delistPayload = museAdminPayloadsByPage
|
||||
.get(page)
|
||||
?.find(
|
||||
(item) =>
|
||||
item.method === 'POST' && item.path === '/market/assets/asset-e2e/delist',
|
||||
)?.payload;
|
||||
|
||||
expect(delistPayload).toBeUndefined();
|
||||
expectNoUnhandledMuseAdminApis(page);
|
||||
});
|
||||
|
||||
test.skip('市场召回切换动作后必须重新生成影响预览', async ({ page }) => {
|
||||
await page.goto('/muse/market');
|
||||
await page.getByRole('tab', { name: '资产治理' }).click();
|
||||
await page
|
||||
.getByRole('row', { name: /^市场资产/ })
|
||||
.getByRole('button')
|
||||
.nth(2)
|
||||
.click();
|
||||
|
||||
const dialog = page.getByRole('dialog', { name: '市场治理:市场资产' });
|
||||
const previewInput = dialog.getByPlaceholder('先执行影响预览后自动带入');
|
||||
|
||||
await dialog.getByRole('button', { name: '生成当前对象影响预览' }).click();
|
||||
await expect(previewInput).toHaveValue(
|
||||
'preview-asset-e2e-recall-full_recall',
|
||||
);
|
||||
|
||||
await dialog.locator('.ant-select-selector').first().click();
|
||||
await page
|
||||
.locator('.ant-select-dropdown:not(.ant-select-dropdown-hidden)')
|
||||
.getByText('下架', { exact: true })
|
||||
.click();
|
||||
|
||||
await expect(previewInput).toHaveValue('');
|
||||
await dialog.getByPlaceholder('法律、合规或安全依据').fill('合规召回依据');
|
||||
await dialog.locator('textarea').fill('切换动作后不能复用旧影响预览');
|
||||
await dialog.locator('.ant-modal-footer .ant-btn-primary').click();
|
||||
|
||||
const recallPayload = museAdminPayloadsByPage
|
||||
.get(page)
|
||||
?.find(
|
||||
(item) =>
|
||||
item.method === 'POST' && item.path === '/market/assets/asset-e2e/recall',
|
||||
)?.payload;
|
||||
|
||||
expect(recallPayload).toBeUndefined();
|
||||
expectNoUnhandledMuseAdminApis(page);
|
||||
});
|
||||
|
||||
test.skip('市场召回切到下架并重新生成预览后不能提交召回', async ({ page }) => {
|
||||
await page.goto('/muse/market');
|
||||
await page.getByRole('tab', { name: '资产治理' }).click();
|
||||
await page
|
||||
.getByRole('row', { name: /^市场资产/ })
|
||||
.getByRole('button')
|
||||
.nth(2)
|
||||
.click();
|
||||
|
||||
const dialog = page.getByRole('dialog', { name: '市场治理:市场资产' });
|
||||
const previewInput = dialog.getByPlaceholder('先执行影响预览后自动带入');
|
||||
|
||||
await dialog.locator('.ant-select-selector').first().click();
|
||||
await page
|
||||
.locator('.ant-select-dropdown:not(.ant-select-dropdown-hidden)')
|
||||
.getByText('下架', { exact: true })
|
||||
.click();
|
||||
await dialog.getByRole('button', { name: '生成当前对象影响预览' }).click();
|
||||
await expect(previewInput).toHaveValue(
|
||||
'preview-asset-e2e-delist-full_recall',
|
||||
);
|
||||
|
||||
await dialog.getByPlaceholder('法律、合规或安全依据').fill('合规召回依据');
|
||||
await dialog.locator('textarea').fill('尝试用下架预览提交召回');
|
||||
await dialog.locator('.ant-modal-footer .ant-btn-primary').click();
|
||||
|
||||
const recallPayload = museAdminPayloadsByPage
|
||||
.get(page)
|
||||
?.find(
|
||||
(item) =>
|
||||
item.method === 'POST' && item.path === '/market/assets/asset-e2e/recall',
|
||||
)?.payload;
|
||||
|
||||
expect(recallPayload).toBeUndefined();
|
||||
expectNoUnhandledMuseAdminApis(page);
|
||||
});
|
||||
|
||||
test.skip('市场申诉切换 appealId 后必须重新生成影响预览', async ({ page }) => {
|
||||
await page.goto('/muse/market');
|
||||
await page.getByRole('tab', { name: '申诉处理' }).click();
|
||||
await page
|
||||
.getByRole('row', { name: /^市场资产\s+delist\s+申诉人/ })
|
||||
.getByRole('button', { name: /处\s*理/ })
|
||||
.click();
|
||||
|
||||
const firstDialog = page.getByRole('dialog', { name: '市场治理:市场资产' });
|
||||
const firstPreviewInput =
|
||||
firstDialog.getByPlaceholder('先执行影响预览后自动带入');
|
||||
// 申诉 1 关联资产「活体申诉演练资产」,行内唯一动作按钮"处理"(ant 中文字按钮带空格→/处\s*理/)。
|
||||
// 按激活 tabpanel 收口,避开 ant 不卸载的隐藏 pane(资产 tab 也有同名资产行)。
|
||||
const appealPanel = page.getByRole('tabpanel', { name: '申诉处理' });
|
||||
const appealRow = appealPanel.getByRole('row', { name: /活体申诉演练资产/ });
|
||||
await appealRow.getByRole('button', { name: /处\s*理/ }).click();
|
||||
|
||||
await firstDialog.getByRole('button', { name: '生成当前对象影响预览' }).click();
|
||||
await expect(firstPreviewInput).toHaveValue(
|
||||
'preview-asset-e2e-delist-stop_new_acquire',
|
||||
);
|
||||
await firstDialog.locator('.ant-modal-footer .ant-btn-default').click();
|
||||
const dialog = page.getByRole('dialog', { name: /市场治理:活体申诉演练资产/ });
|
||||
const previewInput = dialog.getByPlaceholder('先执行影响预览后自动带入');
|
||||
|
||||
await page
|
||||
.getByRole('row', { name: /^另一市场资产\s+delist\s+另一申诉人/ })
|
||||
.getByRole('button', { name: /处\s*理/ })
|
||||
.click();
|
||||
const secondDialog = page.getByRole('dialog', {
|
||||
name: '市场治理:另一市场资产',
|
||||
});
|
||||
await expect(
|
||||
secondDialog.getByPlaceholder('先执行影响预览后自动带入'),
|
||||
).toHaveValue('');
|
||||
await secondDialog.locator('textarea').fill('切换申诉后不能复用旧影响预览');
|
||||
await secondDialog.locator('.ant-modal-footer .ant-btn-primary').click();
|
||||
// 真生成影响预览(申诉类型 review_rejection → 前端 actionType=review_rejection,后端接受)。
|
||||
await dialog.getByRole('button', { name: '生成当前对象影响预览' }).click();
|
||||
await expect(previewInput).toHaveValue(/^gov-preview-/);
|
||||
|
||||
const resolvePayload = museAdminPayloadsByPage
|
||||
.get(page)
|
||||
?.find(
|
||||
(item) =>
|
||||
item.method === 'POST' &&
|
||||
item.path === '/market/appeals/appeal-two/resolve',
|
||||
)?.payload;
|
||||
// 结论默认 maintained(不切到 restored,避免触达未覆盖的 restore 票据消费路径)。
|
||||
const auditReason = 'e2e 真后端维持原处理决定';
|
||||
await dialog.locator('textarea').fill(auditReason);
|
||||
await dialog.locator('.ant-modal-footer .ant-btn-primary').click();
|
||||
|
||||
await expect(page.getByText('市场申诉已处理')).toBeVisible();
|
||||
|
||||
// 反假绿核心:真 PG 反查申诉真的落为 maintained 终态、处理人写入当前 admin(user1)。
|
||||
// 注意后端契约:muse_market_appeal.resolution 列存的是"处理理由"文本(与 P1rMarketAdminAppeal IT 一致),
|
||||
// 不是结论枚举;maintained 结论体现在 status 列。故此处校验 status=maintained + resolution=本次理由。
|
||||
await expect
|
||||
.poll(async () => (await queryAppealFacts(appealId)).status, {
|
||||
timeout: 10_000,
|
||||
})
|
||||
.toBe('maintained');
|
||||
const after = await queryAppealFacts(appealId);
|
||||
expect(after.resolution).toBe(auditReason);
|
||||
expect(after.resolverId).toBe('1');
|
||||
|
||||
expect(resolvePayload).toBeUndefined();
|
||||
expectNoUnhandledMuseAdminApis(page);
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user