e32823d389
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 6m35s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 5m10s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 4m29s
Project CI / Frontend tests (pull_request) Failing after 55s
Project CI / Backend tests (pull_request) Successful in 8m13s
Project CI / AI game creator shell web tests (pull_request) Successful in 5m37s
Project CI / Repository checks (pull_request) Successful in 7m5s
Project CI / Native shell tests (pull_request) Successful in 9m2s
新增创作者公开主页、第四导航入口、详情作者跳转及关注按钮 实现关注与粉丝列表、取消后重新关注、回关及移除粉丝交互 新增私有关注关系表、类型契约、受信过程和站内查询写入接口 按作者过滤公开游戏并保持既有四十八项读取边界 复用共享用户行、关注按钮及游戏卡,接入移动布局和页面深链 补充定向测试、隔离数据库及浏览器验收脚本并归并工程验证文档
259 lines
11 KiB
JavaScript
259 lines
11 KiB
JavaScript
/** 隔离创作者数据库的真实浏览器验收。先运行 test-creator-runtime.py,再启动同库 dev:web。
|
|
* CREATOR_PLAYWRIGHT_DIR 指向临时安装的 playwright;浏览器、截图与登录信息均不入库。
|
|
*/
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const { createRequire } = require('node:module');
|
|
const state = JSON.parse(fs.readFileSync('.app/dev-stack.json', 'utf8'));
|
|
assert.equal(state.database, 'creator-hub-validation');
|
|
const web = state.services.web.url;
|
|
const api = state.services['api-server'].url;
|
|
for (const url of [web, api]) assert.equal(new URL(url).hostname, '127.0.0.1');
|
|
const runtimeRequire = process.env.CREATOR_PLAYWRIGHT_DIR
|
|
? createRequire(
|
|
path.resolve(process.env.CREATOR_PLAYWRIGHT_DIR, 'package.json'),
|
|
)
|
|
: require;
|
|
const { chromium } = runtimeRequire('playwright');
|
|
async function json(endpoint, token, method = 'GET', body) {
|
|
const response = await fetch(api + endpoint, {
|
|
method,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
},
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
});
|
|
assert.ok(response.ok, `${method} ${endpoint}: ${response.status}`);
|
|
const result = await response.json();
|
|
return result.data ?? result;
|
|
}
|
|
async function login(n) {
|
|
return json('/api/auth/entry', null, 'POST', {
|
|
purePhoneNumber: String(13900006000 + n),
|
|
password: 'Creator-test-2026',
|
|
});
|
|
}
|
|
async function waitRows(page, count) {
|
|
await page.waitForFunction(
|
|
(n) => document.querySelectorAll('.creator-list > li').length === n,
|
|
count,
|
|
);
|
|
}
|
|
(async () => {
|
|
const [a, b] = await Promise.all([login(1), login(2)]);
|
|
const browser = await chromium.launch({ headless: true });
|
|
const errors = [];
|
|
try {
|
|
const context = await browser.newContext({
|
|
viewport: { width: 1280, height: 900 },
|
|
});
|
|
await context.addInitScript(
|
|
(token) =>
|
|
localStorage.setItem('genarrative.auth.access-token.v1', token),
|
|
a.token,
|
|
);
|
|
const page = await context.newPage();
|
|
page.on('pageerror', (error) => errors.push(error.message));
|
|
await page.goto(web + '/creators');
|
|
await page.locator('.game-card').first().waitFor();
|
|
assert.equal(await page.locator('.game-card').count(), 48);
|
|
assert.deepEqual(
|
|
await page
|
|
.getByRole('navigation', { name: '平台导航', exact: true })
|
|
.getByRole('button')
|
|
.allTextContents(),
|
|
['创作', '项目', '游戏', '创作者主页', '我的'],
|
|
);
|
|
assert.equal(
|
|
await page
|
|
.locator('.creator-profile')
|
|
.getByRole('button', { name: '取消关注' })
|
|
.count(),
|
|
0,
|
|
);
|
|
await page.screenshot({ path: '/tmp/creator-home-desktop.png' });
|
|
await page.locator('.game-card').first().click();
|
|
await page.locator('.game-detail-page').waitFor();
|
|
await page.locator('.game-detail-page .creator-user-link').click();
|
|
await page.locator('.creator-counts').waitFor();
|
|
assert.equal(new URL(page.url()).searchParams.get('id'), a.user.id);
|
|
await page
|
|
.getByRole('navigation', { name: '平台导航', exact: true })
|
|
.getByRole('button', { name: '创作者主页' })
|
|
.click();
|
|
assert.equal(new URL(page.url()).search, ''); // 同舞台切回本人,URL 改变也会重绘。
|
|
await page.locator('.creator-counts a').first().click();
|
|
await waitRows(page, 20);
|
|
await page.getByRole('button', { name: '加载更多' }).click();
|
|
await waitRows(page, 40);
|
|
await page.locator('.platform-tab-panel').evaluate((el) => {
|
|
el.scrollTop = 650;
|
|
});
|
|
await page.waitForTimeout(100);
|
|
await page.locator('.creator-list .creator-user-link').nth(7).click();
|
|
await page.locator('.creator-counts').waitFor();
|
|
await page.goBack();
|
|
await waitRows(page, 40);
|
|
assert.ok(
|
|
await page
|
|
.locator('.platform-tab-panel')
|
|
.evaluate((el) => el.scrollTop > 200),
|
|
);
|
|
const first = page.locator('.creator-list > li').first();
|
|
const target = new URL(
|
|
await first.locator('a').getAttribute('href'),
|
|
web,
|
|
).searchParams.get('id');
|
|
await first.getByRole('button', { name: '取消关注' }).click();
|
|
await first.getByRole('button', { name: '重新关注' }).waitFor();
|
|
await first.getByRole('button', { name: '重新关注' }).click();
|
|
await first.getByRole('button', { name: '取消关注' }).waitFor();
|
|
await first.getByRole('button', { name: '取消关注' }).click();
|
|
await first.getByRole('button', { name: '重新关注' }).waitFor();
|
|
await page.getByRole('button', { name: '刷新列表' }).click();
|
|
await page.waitForFunction(
|
|
(id) =>
|
|
!document.querySelector(`.creator-list a[href="/creators?id=${id}"]`),
|
|
target,
|
|
);
|
|
await json(`/api/creators/${target}/follow`, a.token, 'PUT');
|
|
// 同一个列表舞台切换到粉丝,验证双向独立和移除确认。
|
|
await page
|
|
.getByRole('navigation', { name: '关系列表' })
|
|
.getByRole('link', { name: '粉丝', exact: true })
|
|
.click();
|
|
await page.getByRole('heading', { name: /我的粉丝/ }).waitFor();
|
|
await waitRows(page, 20);
|
|
const fan = page.locator('.creator-list > li').first();
|
|
const fanId = new URL(
|
|
await fan.locator('a').getAttribute('href'),
|
|
web,
|
|
).searchParams.get('id');
|
|
await fan.getByRole('button', { name: '取消关注' }).click();
|
|
await fan.getByRole('button', { name: '回关' }).waitFor();
|
|
await fan.getByRole('button', { name: '回关' }).click();
|
|
await fan.getByRole('button', { name: '取消关注' }).waitFor();
|
|
await fan.getByRole('button', { name: '移除粉丝' }).click();
|
|
const dialog = page.getByRole('dialog');
|
|
await dialog.getByRole('button', { name: '取消', exact: true }).click();
|
|
await fan.getByRole('button', { name: '移除粉丝' }).click();
|
|
await dialog.getByRole('button', { name: '移除粉丝', exact: true }).click();
|
|
await page.waitForFunction(
|
|
(id) =>
|
|
!document.querySelector(`.creator-list a[href="/creators?id=${id}"]`),
|
|
fanId,
|
|
);
|
|
const relation = await json(`/api/creators/${fanId}/relationship`, a.token);
|
|
assert.equal(relation.relationship.isFollowing, true);
|
|
assert.equal(relation.relationship.isFollowedBy, false);
|
|
console.log(
|
|
'PASS desktop: navigation, author/game links, pagination/back/scroll, unfollow retention, follow-back and remove',
|
|
);
|
|
// 上一步可能移除了 B 的入边,重建只读列表验收所需的双方关系。
|
|
await json(`/api/creators/${a.user.id}/follow`, b.token, 'PUT');
|
|
await json(`/api/creators/${b.user.id}/follow`, a.token, 'PUT');
|
|
// 他人两类列表只有主页链接,不读取访问者与列表用户的关系。
|
|
for (const tab of ['following', 'followers']) {
|
|
const requests = [];
|
|
const record = (request) => {
|
|
if (request.url().includes('/relationship'))
|
|
requests.push(request.url());
|
|
};
|
|
page.on('request', record);
|
|
await page.goto(`${web}/creators/connections?id=${b.user.id}&tab=${tab}`);
|
|
await page.locator('.creator-list > li').first().waitFor();
|
|
assert.equal(await page.locator('.creator-list button').count(), 0);
|
|
assert.equal(requests.length, 0);
|
|
page.off('request', record);
|
|
await page.locator('.creator-list a').first().click();
|
|
await page.locator('.creator-counts').waitFor();
|
|
}
|
|
await page.goto(`${web}/creators?id=${b.user.id}`);
|
|
await page.locator('.creator-relation button').first().waitFor();
|
|
const follow = page.locator('.creator-relation button').first();
|
|
await page.waitForFunction(
|
|
() => !document.querySelector('.creator-relation button')?.disabled,
|
|
);
|
|
const oldLabel = await follow.innerText();
|
|
await follow.click();
|
|
await page.waitForFunction(
|
|
(label) =>
|
|
document.querySelector('.creator-relation button')?.textContent !==
|
|
label &&
|
|
!document.querySelector('.creator-relation button')?.disabled,
|
|
oldLabel,
|
|
);
|
|
await follow.click();
|
|
console.log(
|
|
'PASS public lists: both read-only, no relationship requests, all users link to creator pages',
|
|
);
|
|
await page.setViewportSize({ width: 375, height: 812 });
|
|
await page.goto(web + '/creators');
|
|
await page.locator('.creator-profile').waitFor();
|
|
const welcome = page.getByRole('button', { name: '好', exact: true });
|
|
if (await welcome.isVisible()) await welcome.click();
|
|
assert.deepEqual(
|
|
await page
|
|
.getByRole('navigation', { name: '移动平台导航' })
|
|
.getByRole('button')
|
|
.allTextContents(),
|
|
['游戏', '创作者主页', '我的'],
|
|
);
|
|
assert.ok(
|
|
await page.evaluate(
|
|
() => document.documentElement.scrollWidth <= innerWidth,
|
|
),
|
|
);
|
|
await page.screenshot({ path: '/tmp/creator-home-mobile.png' });
|
|
await page.locator('.creator-counts a').last().click();
|
|
await page.locator('.creator-list > li').first().waitFor();
|
|
assert.ok(
|
|
await page.evaluate(
|
|
() => document.documentElement.scrollWidth <= innerWidth,
|
|
),
|
|
);
|
|
await page.screenshot({ path: '/tmp/creator-followers-mobile.png' });
|
|
await page.locator('.creator-list a').first().focus();
|
|
await page.keyboard.press('Enter');
|
|
await page.locator('.creator-counts').waitFor();
|
|
const guest = await browser.newContext({
|
|
viewport: { width: 375, height: 812 },
|
|
});
|
|
const guestPage = await guest.newPage();
|
|
await guestPage.goto(`${web}/creators?id=${b.user.id}`);
|
|
await guestPage.locator('.creator-profile').waitFor();
|
|
const guestWelcome = guestPage.getByRole('button', {
|
|
name: '好',
|
|
exact: true,
|
|
});
|
|
if (await guestWelcome.isVisible()) await guestWelcome.click();
|
|
for (const tab of ['following', 'followers']) {
|
|
await guestPage.goto(
|
|
`${web}/creators/connections?id=${b.user.id}&tab=${tab}`,
|
|
);
|
|
await guestPage.locator('.creator-list > li').first().waitFor();
|
|
if (await guestWelcome.isVisible()) await guestWelcome.click();
|
|
assert.equal(await guestPage.locator('.creator-list button').count(), 0);
|
|
await guestPage
|
|
.getByRole('button', { name: '返回', exact: true })
|
|
.click();
|
|
await guestPage.locator('.creator-profile').waitFor();
|
|
assert.equal(new URL(guestPage.url()).searchParams.get('id'), b.user.id);
|
|
}
|
|
await guestPage.getByRole('button', { name: '关注', exact: true }).click();
|
|
await guestPage.getByRole('dialog').waitFor();
|
|
assert.equal(new URL(guestPage.url()).searchParams.get('id'), b.user.id);
|
|
assert.deepEqual(errors, []);
|
|
console.log(
|
|
'PASS mobile 375px: three navigation entries, no horizontal overflow, keyboard links, guest login without follow',
|
|
);
|
|
} finally {
|
|
await browser.close();
|
|
}
|
|
})().catch((error) => {
|
|
console.error(error);
|
|
process.exitCode = 1;
|
|
});
|