精简游戏评价回归测试与读取实现
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m0s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m55s
Project CI / Backend tests (pull_request) Successful in 5m33s
Project CI / Native shell tests (pull_request) Successful in 6m10s
Project CI / Frontend tests (pull_request) Successful in 2m18s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 10m51s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 11m16s
Project CI / Repository checks (pull_request) Successful in 2m19s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m41s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m0s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m55s
Project CI / Backend tests (pull_request) Successful in 5m33s
Project CI / Native shell tests (pull_request) Successful in 6m10s
Project CI / Frontend tests (pull_request) Successful in 2m18s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 10m51s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 11m16s
Project CI / Repository checks (pull_request) Successful in 2m19s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m41s
将后台评价功能回归与旧库迁移验收解耦,独立准备隔离账号凭据 收窄重复输入矩阵和测试专用竞态场景,保留安全与幂等覆盖 复用公开版本判断,减少评价列表的重复过滤和中间集合 同步回归步骤与本轮验证证据
This commit is contained in:
@@ -383,6 +383,7 @@
|
||||
|
||||
### 迁移与验收
|
||||
|
||||
- 日常评价回归与旧库迁移验收独立运行。先用 `check-game-distribution-ratings-e2e.mjs` 在显式指定的本机 `reviews-e2e` 隔离库生成新 fixture,再运行 `check-game-distribution-review-moderation-e2e.mjs check`;后台凭据通过该脚本的 `prepare` 模式提前准备,按现有后台 owner 配置启动 API。`check` 以本次读取的评价为基线,不依赖旧快照或迁移证据;`snapshot` / `migration` 仅用于下面的旧库升级验收。非法输入的完整矩阵保留在领域/API 测试,真实 HTTP 回归保留代表性拒绝、无写入及权限、统计、幂等与删除重建验证。
|
||||
- 将隐藏字段追加在已有 Rust 表结构末尾并设置明确默认值 false,按 SpacetimeDB 2.8.3 兼容追加规则发布;新增管理记录表为空。不手工回填或清空旧评价。
|
||||
- 同步 `migration.rs`、后端表目录、生成绑定和 schema 检查。上线前用隔离数据库验证旧 schema 的评价升级后内容、唯一性、时间保留且全部公开;不把新数据库空表测试当成存量迁移证据。
|
||||
- 回滚应用时保留新增字段和管理记录;旧后端不认识隐藏状态,会重新暴露已隐藏评价,因此产生隐藏记录后禁止直接回退到未过滤隐藏记录的旧读取实现,采用兼容修复。
|
||||
@@ -400,4 +401,6 @@
|
||||
|
||||
真实运行时通过项目 dev 脚本启动隔离数据库与 API(`/healthz` 正常);旧 wasm 带 21 条评价兼容升级,逐项核对 ID、正文、评分、创建/修改时间均保持且隐藏默认 false。`scripts/check-game-distribution-review-moderation-e2e.mjs` 完成 57 项 HTTP 检查,另验证伪造管理员身份不能覆盖认证操作人。浏览器验证仅操作本地合成账号与游戏,没有生产数据写入或 OSS 上传。
|
||||
|
||||
回归脚本精简后,在现有本机隔离服务上以新 fixture 复跑:网站 40 项、后台 51 项 HTTP 检查通过,后台 `prepare` / `check` 不再依赖旧库验收产物。网站交互 11 项、评价领域 5 项、Spacetime module 编译、网站类型检查及定向格式/静态检查通过。此轮未重新发布 module 或重跑旧库迁移、浏览器验收;HTTP 结果用于验证脚本回归流程,新的内部读取简化由编译和源码条件对照验证。
|
||||
|
||||
未验证:生产部署、真实移动设备/系统输入法、无关全量测试和真实游戏包游玩。永久删除由真实 HTTP 验证,浏览器仅验证提示与取消;网络失败草稿由交互自动化验证,未在浏览器断网复现。用户验收尚未完成,计划和里程碑保留;不据此宣称已上线。
|
||||
|
||||
@@ -236,14 +236,9 @@ async function main() {
|
||||
);
|
||||
await invisible(privateGame, author.token, '未公开游戏');
|
||||
for (const [label, body, status] of [
|
||||
['未选择评分', { comment: '仅文字' }, 400],
|
||||
['非整数', { score: 1.5 }, 400],
|
||||
['字符串评分', { score: '5' }, 400],
|
||||
['评论类型', { score: 5, comment: 42 }, 400],
|
||||
['0 分', { score: 0 }, 422],
|
||||
['11 分', { score: 11 }, 422],
|
||||
['4001 emoji', { score: 5, comment: '😀'.repeat(4001) }, 422],
|
||||
['4001 空白先校验', { score: 5, comment: ' '.repeat(4001) }, 422],
|
||||
])
|
||||
check(
|
||||
`${label}拒绝 ${status}`,
|
||||
@@ -405,14 +400,7 @@ async function main() {
|
||||
(await list(gameId, '?pageSize=1')).data.reviews.length === 1 &&
|
||||
(await list(gameId, '?pageSize=50')).data.reviews.length === 21,
|
||||
);
|
||||
for (const query of [
|
||||
'?page=0',
|
||||
'?page=-1',
|
||||
'?page=1.5',
|
||||
'?page=abc',
|
||||
'?pageSize=0',
|
||||
'?pageSize=51',
|
||||
]) {
|
||||
for (const query of ['?page=0', '?pageSize=51']) {
|
||||
check(
|
||||
`非法分页 ${query} 返回 400`,
|
||||
(await list(gameId, query)).status === 400,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// 后台评价管理真实验收:旧库snapshot → 无损发布新模块 → migration → check。
|
||||
// E2E_REVIEWS_DATABASE=genarrative-reviews-e2e node scripts/check-game-distribution-review-moderation-e2e.mjs snapshot|migration|check
|
||||
// 当前schema回归:prepare → 注入owner凭据并重启API → ratings-e2e新fixture → check。
|
||||
// 旧库迁移独立验收:旧schema运行ratings-e2e → snapshot → 无损发布新模块/API → migration。
|
||||
// E2E_REVIEWS_DATABASE=genarrative-reviews-e2e node scripts/check-game-distribution-review-moderation-e2e.mjs prepare|snapshot|migration|check
|
||||
// 复用 ratings-e2e 产生的21条评价与 .app/reviews-e2e-browser.json;凭据/快照只保存ignored .app。
|
||||
// API重启前从 .app/review-moderation-e2e/credentials.json 注入 owner 用户名/密码。
|
||||
import assert from 'node:assert/strict';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
const state = JSON.parse(readFileSync('.app/dev-stack.json', 'utf8'));
|
||||
const database = process.env.E2E_REVIEWS_DATABASE;
|
||||
@@ -17,14 +18,17 @@ assert(
|
||||
['127.0.0.1', 'localhost'].includes(new URL(base).hostname),
|
||||
'只允许本机API',
|
||||
);
|
||||
const fixture = JSON.parse(
|
||||
readFileSync('.app/reviews-e2e-browser.json', 'utf8'),
|
||||
);
|
||||
const mode = process.argv[2] ?? 'check';
|
||||
const fixture =
|
||||
mode === 'prepare'
|
||||
? null
|
||||
: JSON.parse(readFileSync('.app/reviews-e2e-browser.json', 'utf8'));
|
||||
const dir = resolve('.app/review-moderation-e2e');
|
||||
const credentialPath = resolve(dir, 'credentials.json');
|
||||
const snapshotPath = resolve(dir, 'old-reviews.json');
|
||||
const adminPrefix = '/admin/api/game-distribution';
|
||||
const publicPrefix = `/api/game-distribution/games/${fixture.gameId}`;
|
||||
const publicPrefix =
|
||||
fixture && `/api/game-distribution/games/${fixture.gameId}`;
|
||||
let checks = 0;
|
||||
function check(label, ok) {
|
||||
assert(ok, label);
|
||||
@@ -68,6 +72,27 @@ const oldFields = (review) =>
|
||||
(key) => [key, review[key]],
|
||||
),
|
||||
);
|
||||
function prepare() {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
if (!existsSync(credentialPath))
|
||||
writeFileSync(
|
||||
credentialPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
owner: {
|
||||
username: 'reviews_e2e_owner',
|
||||
password: `Rev-${randomUUID()}!`,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
console.log(
|
||||
'owner凭据已准备;从ignored credentials.json注入API环境变量后重启API。',
|
||||
);
|
||||
}
|
||||
async function snapshot() {
|
||||
const reviews = await api(`${publicPrefix}/reviews?pageSize=50`);
|
||||
assert(
|
||||
@@ -78,6 +103,7 @@ async function snapshot() {
|
||||
reviews.data.reviews.every((review) => !Object.hasOwn(review, 'isHidden')),
|
||||
'此模式只接受未追加isHidden的旧schema API',
|
||||
);
|
||||
mkdirSync(dir, { recursive: true });
|
||||
writeFileSync(
|
||||
snapshotPath,
|
||||
JSON.stringify(
|
||||
@@ -90,31 +116,8 @@ async function snapshot() {
|
||||
2,
|
||||
),
|
||||
);
|
||||
if (!existsSync(credentialPath))
|
||||
writeFileSync(
|
||||
credentialPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
owner: {
|
||||
username: 'reviews_e2e_owner',
|
||||
password: `Rev-${randomUUID()}!`,
|
||||
},
|
||||
allowed: {
|
||||
username: `reviews_allowed_${Date.now()}`,
|
||||
password: `Rev-${randomUUID()}!`,
|
||||
},
|
||||
denied: {
|
||||
username: `reviews_denied_${Date.now()}`,
|
||||
password: `Rev-${randomUUID()}!`,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
console.log(
|
||||
`旧schema快照已保存:${reviews.data.total}条;凭据仅在ignored文件。`,
|
||||
`旧schema快照已保存:${reviews.data.total}条;快照仅在ignored文件。`,
|
||||
);
|
||||
}
|
||||
async function migration() {
|
||||
@@ -152,10 +155,9 @@ async function run() {
|
||||
const health = await fetch(`${base}/healthz`);
|
||||
check('API健康', health.ok);
|
||||
assert(
|
||||
existsSync(resolve(dir, 'migration-verified.json')),
|
||||
'先运行migration保存存量验证证据',
|
||||
existsSync(credentialPath),
|
||||
'先运行prepare,并向API注入owner凭据后重启',
|
||||
);
|
||||
const old = JSON.parse(readFileSync(snapshotPath, 'utf8'));
|
||||
const credentials = JSON.parse(readFileSync(credentialPath, 'utf8'));
|
||||
const ownerLogin = await login(credentials.owner, true);
|
||||
const owner = ownerLogin.token;
|
||||
@@ -163,6 +165,10 @@ async function run() {
|
||||
['allowed', ['game-reviews']],
|
||||
['denied', ['game-management']],
|
||||
]) {
|
||||
credentials[name] = {
|
||||
username: `reviews_${name}_${randomUUID().replaceAll('-', '')}`,
|
||||
password: `Rev-${randomUUID()}!`,
|
||||
};
|
||||
const created = await api('/admin/api/accounts', {
|
||||
method: 'POST',
|
||||
token: owner,
|
||||
@@ -176,6 +182,9 @@ async function run() {
|
||||
});
|
||||
assert(created.status === 200, `后台fixture账号创建失败 ${created.status}`);
|
||||
}
|
||||
writeFileSync(credentialPath, JSON.stringify(credentials, null, 2), {
|
||||
mode: 0o600,
|
||||
});
|
||||
const allowedLogin = await login(credentials.allowed, true);
|
||||
const allowed = allowedLogin.token;
|
||||
const denied = (await login(credentials.denied, true)).token;
|
||||
@@ -226,6 +235,7 @@ async function run() {
|
||||
all.data.total >= 21 &&
|
||||
all.headers.get('cache-control') === 'no-store',
|
||||
);
|
||||
const baseline = all.data.reviews.map(oldFields);
|
||||
const page2 = await query(listPath + '&page=2');
|
||||
check(
|
||||
'后台默认分页20与跨页',
|
||||
@@ -268,13 +278,8 @@ async function run() {
|
||||
);
|
||||
for (const [label, request, key, expected] of [
|
||||
['隐藏空原因', body('hide', ' '), randomUUID(), 422],
|
||||
['删除缺原因', body('delete'), randomUUID(), 422],
|
||||
['原因4001码点', body('hide', '😀'.repeat(4001)), randomUUID(), 422],
|
||||
['非法动作', body('unknown', 'x'), randomUUID(), 400],
|
||||
['无创建时间', { action: 'hide', reason: 'x' }, randomUUID(), 400],
|
||||
['错误时间', body('hide', 'x', 'not-a-date'), randomUUID(), 400],
|
||||
['缺key', body('hide', 'x'), undefined, 400],
|
||||
['过长key', body('hide', 'x'), 'k'.repeat(129), 400],
|
||||
]) {
|
||||
const response =
|
||||
key === undefined
|
||||
@@ -287,6 +292,13 @@ async function run() {
|
||||
check(`${label}拒绝`, response.status === expected);
|
||||
}
|
||||
const preHide = (await myReview()).data.review;
|
||||
check(
|
||||
'无效管理请求没有变更或操作记录',
|
||||
JSON.stringify(oldFields(preHide)) ===
|
||||
JSON.stringify(oldFields(edited.data.review)) &&
|
||||
!preHide.isHidden &&
|
||||
(await detail()).data.operations.length === 0,
|
||||
);
|
||||
const total = (await publicReviews()).data.total;
|
||||
const hideKey = randomUUID();
|
||||
const hidden = await moderation(
|
||||
@@ -465,19 +477,14 @@ async function run() {
|
||||
(await query(listPath + '&pageSize=50')).data.reviews
|
||||
.filter((row) => row.id !== before.id)
|
||||
.every((row) => {
|
||||
const previous = old.reviews.find((value) => value.id === row.id);
|
||||
const previous = baseline.find((value) => value.id === row.id);
|
||||
return (
|
||||
previous &&
|
||||
JSON.stringify(oldFields(row)) === JSON.stringify(previous)
|
||||
);
|
||||
}),
|
||||
);
|
||||
for (const suffix of [
|
||||
'?status=unknown',
|
||||
'?page=0',
|
||||
'?pageSize=51',
|
||||
'?page=1.5',
|
||||
])
|
||||
for (const suffix of ['?status=unknown', '?page=0'])
|
||||
check(`错误筛选${suffix}400`, (await query(suffix)).status === 400);
|
||||
const privateGames = await api(
|
||||
`${adminPrefix}/user-review-games?pageSize=50`,
|
||||
@@ -596,13 +603,16 @@ async function run() {
|
||||
`全部${checks}项通过;browser fixture 已写入ignored .app/review-moderation-e2e/browser.json。`,
|
||||
);
|
||||
}
|
||||
await (
|
||||
process.argv[2] === 'snapshot'
|
||||
? snapshot()
|
||||
: process.argv[2] === 'migration'
|
||||
? migration()
|
||||
: run()
|
||||
).catch((error) => {
|
||||
console.error(`FAIL ${error.message}`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
const modes = { prepare, snapshot, migration, check: run };
|
||||
await Promise.resolve()
|
||||
.then(() => {
|
||||
assert(
|
||||
Object.hasOwn(modes, mode),
|
||||
'模式须为prepare、snapshot、migration或check',
|
||||
);
|
||||
return modes[mode]();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(`FAIL ${error.message}`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
|
||||
@@ -173,7 +173,6 @@ mod tests {
|
||||
fn review_identity_sort_and_summary_are_stable() {
|
||||
assert_ne!(review_id("a", "bc"), review_id("ab", "c"));
|
||||
assert_ne!(review_id("a:", "b"), review_id("a", ":b"));
|
||||
assert_eq!(review_id("中文", "😀"), review_id("中文", "😀"));
|
||||
assert_eq!(review_summary([].into_iter()), (None, 0));
|
||||
assert_eq!(review_summary([8, 8, 9].into_iter()), (Some(8.3), 3));
|
||||
assert_eq!(review_summary([10, 1].into_iter()), (Some(5.5), 2));
|
||||
|
||||
@@ -96,13 +96,14 @@ fn ensure_game_distribution_review_visible(
|
||||
ctx: &ReducerContext,
|
||||
game_id: &str,
|
||||
) -> Result<(), String> {
|
||||
let visible = get_public_game_distribution_game_tx(
|
||||
ctx,
|
||||
GameDistributionPublicGameInput {
|
||||
game_id: game_id.to_string(),
|
||||
},
|
||||
)?
|
||||
.is_some();
|
||||
let visible = ctx
|
||||
.db
|
||||
.game_distribution_game()
|
||||
.game_id()
|
||||
.find(game_id.to_string())
|
||||
.filter(|game| game.visibility == GAME_DISTRIBUTION_VISIBILITY_PUBLISHED)
|
||||
.and_then(|game| public_game_distribution_version(ctx, &game))
|
||||
.is_some();
|
||||
if visible {
|
||||
Ok(())
|
||||
} else {
|
||||
@@ -165,11 +166,8 @@ pub fn list_game_distribution_user_reviews_and_return(
|
||||
.filter(&game_id)
|
||||
.filter(|review| !review.is_hidden)
|
||||
.collect::<Vec<_>>();
|
||||
let (average_score, rating_count) = module_game_distribution::visible_review_summary(
|
||||
reviews
|
||||
.iter()
|
||||
.map(|review| (review.score, review.is_hidden)),
|
||||
);
|
||||
let (average_score, rating_count) =
|
||||
module_game_distribution::review_summary(reviews.iter().map(|review| review.score));
|
||||
reviews.sort_by(|left, right| {
|
||||
module_game_distribution::review_order(
|
||||
left.created_at.to_micros_since_unix_epoch(),
|
||||
@@ -470,7 +468,7 @@ pub fn list_admin_game_distribution_user_reviews_and_return(
|
||||
.keyword
|
||||
.clone()
|
||||
.and_then(normalize_game_distribution_optional);
|
||||
let candidates = if let Some(game_id) = &game_id {
|
||||
let mut reviews = if let Some(game_id) = &game_id {
|
||||
tx.db
|
||||
.game_distribution_review()
|
||||
.by_game_distribution_review_game_id()
|
||||
@@ -485,21 +483,18 @@ pub fn list_admin_game_distribution_user_reviews_and_return(
|
||||
} else {
|
||||
tx.db.game_distribution_review().iter().collect::<Vec<_>>()
|
||||
};
|
||||
let mut reviews = candidates
|
||||
.into_iter()
|
||||
.filter(|review| {
|
||||
module_game_distribution::review_matches_filters(
|
||||
&review.game_id,
|
||||
&review.user_id,
|
||||
&review.comment,
|
||||
review.is_hidden,
|
||||
game_id.as_deref(),
|
||||
user_id.as_deref(),
|
||||
keyword.as_deref(),
|
||||
&input.status,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
reviews.retain(|review| {
|
||||
module_game_distribution::review_matches_filters(
|
||||
&review.game_id,
|
||||
&review.user_id,
|
||||
&review.comment,
|
||||
review.is_hidden,
|
||||
game_id.as_deref(),
|
||||
user_id.as_deref(),
|
||||
keyword.as_deref(),
|
||||
&input.status,
|
||||
)
|
||||
});
|
||||
reviews.sort_by(|left, right| {
|
||||
module_game_distribution::review_order(
|
||||
left.created_at.to_micros_since_unix_epoch(),
|
||||
@@ -3333,19 +3328,25 @@ fn game_distribution_version_snapshot_for_game(
|
||||
snapshot
|
||||
}
|
||||
|
||||
fn public_game_distribution_snapshot(
|
||||
fn public_game_distribution_version(
|
||||
ctx: &ReducerContext,
|
||||
game: &GameDistributionGame,
|
||||
) -> Option<GameDistributionPublicGameSnapshot> {
|
||||
) -> Option<GameDistributionVersion> {
|
||||
let version_id = game.active_version_id.as_ref()?;
|
||||
let version = ctx
|
||||
.db
|
||||
ctx.db
|
||||
.game_distribution_version()
|
||||
.version_id()
|
||||
.find(version_id)
|
||||
.filter(|version| {
|
||||
version.game_id == game.game_id && version.status == GAME_DISTRIBUTION_VERSION_PUBLISHED
|
||||
})?;
|
||||
})
|
||||
}
|
||||
|
||||
fn public_game_distribution_snapshot(
|
||||
ctx: &ReducerContext,
|
||||
game: &GameDistributionGame,
|
||||
) -> Option<GameDistributionPublicGameSnapshot> {
|
||||
let version = public_game_distribution_version(ctx, game)?;
|
||||
Some(GameDistributionPublicGameSnapshot {
|
||||
game: game_distribution_game_snapshot(game),
|
||||
current_version: Some(game_distribution_version_snapshot_for_game(ctx, &version)),
|
||||
|
||||
@@ -86,13 +86,7 @@ function deferred<T>() {
|
||||
}
|
||||
function Harness() {
|
||||
const state = useGameReviews('game-1', true);
|
||||
return (
|
||||
<>
|
||||
<button onClick={() => void state.loadMine()}>重读个人</button>
|
||||
<button onClick={() => void state.loadPage(state.page)}>刷新列表</button>
|
||||
<GameReviews state={state} canReview onLogin={mocks.login} />
|
||||
</>
|
||||
);
|
||||
return <GameReviews state={state} canReview onLogin={mocks.login} />;
|
||||
}
|
||||
async function form() {
|
||||
return screen.findByRole('textbox', { name: '评论(可选)' });
|
||||
@@ -319,17 +313,14 @@ describe('游戏评价', () => {
|
||||
expect(mocks.list.mock.calls[2]).toEqual(['game-1', 1]);
|
||||
});
|
||||
|
||||
it('保存后迟到的个人与列表读取不能覆盖新记录或摘要', async () => {
|
||||
const oldMine = deferred<{ review: GameDistributionReview | null }>();
|
||||
it('保存后迟到的初始列表不能覆盖新摘要或结束刷新状态', async () => {
|
||||
const oldList = deferred<GameDistributionReviewsResponse>();
|
||||
const refreshed = deferred<GameDistributionReviewsResponse>();
|
||||
const saving = deferred<{
|
||||
review: GameDistributionReview;
|
||||
ratingSummary: { averageScore: number; ratingCount: number };
|
||||
}>();
|
||||
mocks.mine
|
||||
.mockResolvedValueOnce({ review: review() })
|
||||
.mockReturnValueOnce(oldMine.promise);
|
||||
mocks.mine.mockResolvedValueOnce({ review: review() });
|
||||
mocks.list
|
||||
.mockReturnValueOnce(oldList.promise)
|
||||
.mockReturnValueOnce(refreshed.promise);
|
||||
@@ -338,8 +329,6 @@ describe('游戏评价', () => {
|
||||
fireEvent.click(await screen.findByRole('button', { name: '编辑评价' }));
|
||||
fireEvent.change(await form(), { target: { value: '已保存' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '提交' }));
|
||||
// 保存仍在途时发出个人读取;保存成功后该读取不能覆盖权威记录。
|
||||
fireEvent.click(screen.getByRole('button', { name: '重读个人' }));
|
||||
const updated = review({ comment: '已保存' });
|
||||
await act(async () =>
|
||||
saving.resolve({
|
||||
@@ -348,17 +337,15 @@ describe('游戏评价', () => {
|
||||
}),
|
||||
);
|
||||
await screen.findByText('已保存');
|
||||
mocks.list.mockReturnValueOnce(new Promise(() => {}));
|
||||
fireEvent.click(screen.getByRole('button', { name: '刷新列表' }));
|
||||
await act(async () => {
|
||||
oldMine.resolve({ review: null });
|
||||
oldList.resolve(list());
|
||||
refreshed.reject(new Error('过期读取失败'));
|
||||
});
|
||||
expect(screen.getByText('已保存')).toBeTruthy();
|
||||
expect(screen.getByText('9.0/10 · 1 人评分')).toBeTruthy();
|
||||
expect(screen.queryByText('过期读取失败')).toBeNull();
|
||||
expect(screen.getByText('正在加载评价…')).toBeTruthy();
|
||||
await act(async () => refreshed.resolve(list([updated])));
|
||||
expect(screen.queryByText('正在加载评价…')).toBeNull();
|
||||
expect(screen.getAllByText('已保存')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('快速翻页只接受最新请求,旧失败和加载结束不覆盖当前页', async () => {
|
||||
|
||||
Reference in New Issue
Block a user