2e14873f2d
Track application source and tests; exclude local env, user workspaces, and runtime data via .gitignore. Co-authored-by: Cursor <cursoragent@cursor.com>
186 lines
7.4 KiB
JavaScript
186 lines
7.4 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* john 账号水果主题 E2E(仅本地):工作区 HTML → 预览图 → 保存到空间 → 可点击链接
|
||
* 用法:node scripts/fruit-theme-john-e2e.mjs
|
||
* 环境:自动启动本地 server(MINDSPACE_E2E_PORT);勿对生产 URL 跑本脚本
|
||
*/
|
||
import assert from 'node:assert/strict';
|
||
import { spawn } from 'node:child_process';
|
||
import fs from 'node:fs/promises';
|
||
import path from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { loadH5Environment } from './load-env.mjs';
|
||
import { createDbPool } from '../db.mjs';
|
||
import { createUserAuth } from '../user-auth.mjs';
|
||
import { buildPublicUrl } from '../user-publish.mjs';
|
||
import {
|
||
ensureWorkspaceHtmlThumbnail,
|
||
workspaceThumbnailRelativePath,
|
||
} from '../mindspace-workspace-thumbnails.mjs';
|
||
import { isModernFeedThumbnail } from '../mindspace-thumbnails.mjs';
|
||
|
||
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||
loadH5Environment(import.meta.dirname);
|
||
|
||
if (/go\.tkmind\.cn|goo\.tkmind\.cn|120\.26\.184\.105/i.test(process.env.BASE_URL ?? '')) {
|
||
throw new Error('本脚本仅用于本地验证,请勿设置生产 BASE_URL');
|
||
}
|
||
|
||
const portalPort = Number(process.env.MINDSPACE_E2E_PORT ?? 18140);
|
||
const BASE_URL = `http://127.0.0.1:${portalPort}`;
|
||
const HTML_NAME = 'fruit-theme-test.html';
|
||
const USERNAME = 'john';
|
||
const JOHN_PASSWORD = process.env.JOHN_PASSWORD ?? process.env.H5_ACCESS_PASSWORD ?? '981122tj';
|
||
const publishDir = path.join(root, 'MindSpace', USERNAME);
|
||
const htmlPath = path.join(publishDir, HTML_NAME);
|
||
const thumbRel = workspaceThumbnailRelativePath(HTML_NAME);
|
||
const publicPageUrl = buildPublicUrl(BASE_URL, USERNAME, HTML_NAME);
|
||
const publicThumbUrl = buildPublicUrl(BASE_URL, USERNAME, thumbRel);
|
||
const agentReplyLink = `[夏日鲜果指南 · 水果主题测试](${publicPageUrl})`;
|
||
|
||
const portal = spawn(process.execPath, ['server.mjs'], {
|
||
cwd: root,
|
||
env: {
|
||
...process.env,
|
||
H5_PORT: String(portalPort),
|
||
H5_PUBLIC_BASE_URL: BASE_URL,
|
||
TKMIND_API_TARGET: 'http://127.0.0.1:9',
|
||
},
|
||
stdio: ['ignore', 'pipe', 'pipe'],
|
||
});
|
||
let portalLogs = '';
|
||
portal.stdout.on('data', (chunk) => {
|
||
portalLogs += chunk.toString();
|
||
});
|
||
portal.stderr.on('data', (chunk) => {
|
||
portalLogs += chunk.toString();
|
||
});
|
||
|
||
async function waitForPortal() {
|
||
for (let attempt = 0; attempt < 80; attempt += 1) {
|
||
if (portal.exitCode != null) throw new Error(`本地 server 提前退出\n${portalLogs}`);
|
||
try {
|
||
const response = await fetch(`${BASE_URL}/auth/status`);
|
||
if (response.ok) return;
|
||
} catch {
|
||
// still starting
|
||
}
|
||
await new Promise((resolve) => setTimeout(resolve, 150));
|
||
}
|
||
throw new Error(`本地 server 启动超时\n${portalLogs}`);
|
||
}
|
||
|
||
function cookieHeader(setCookie) {
|
||
if (!setCookie) return '';
|
||
const parts = Array.isArray(setCookie) ? setCookie : [setCookie];
|
||
return parts.map((c) => c.split(';')[0]).join('; ');
|
||
}
|
||
|
||
async function request(pathname, { method = 'GET', body, cookie } = {}) {
|
||
const headers = {};
|
||
if (body) headers['Content-Type'] = 'application/json';
|
||
if (cookie) headers.Cookie = cookie;
|
||
const response = await fetch(`${BASE_URL}${pathname}`, { method, headers, body: body ? JSON.stringify(body) : undefined });
|
||
const type = response.headers.get('content-type') ?? '';
|
||
const payload = type.includes('json') ? await response.json() : await response.text();
|
||
return { response, payload, setCookie: response.headers.getSetCookie?.() ?? response.headers.get('set-cookie') };
|
||
}
|
||
|
||
async function ensureJohnPassword() {
|
||
const pool = createDbPool();
|
||
const auth = createUserAuth(pool, { h5Root: root });
|
||
let login = await auth.login({ username: USERNAME, password: JOHN_PASSWORD, ip: '127.0.0.1' });
|
||
if (!login.ok) {
|
||
const [rows] = await pool.query(`SELECT id, email FROM h5_users WHERE username = ? LIMIT 1`, [
|
||
USERNAME,
|
||
]);
|
||
const user = rows[0];
|
||
if (!user) throw new Error('本地数据库无 john 用户,请先注册或导入数据');
|
||
const email = user.email ?? `${USERNAME}@local.test`;
|
||
if (!user.email) {
|
||
await pool.query(`UPDATE h5_users SET email = ? WHERE id = ?`, [email, user.id]);
|
||
}
|
||
const reset = await auth.resetPassword({ username: USERNAME, email, password: JOHN_PASSWORD });
|
||
assert.equal(reset.ok, true, reset.message ?? '重置 john 本地密码失败');
|
||
login = await auth.login({ username: USERNAME, password: JOHN_PASSWORD, ip: '127.0.0.1' });
|
||
}
|
||
assert.equal(login.ok, true, login.error ?? 'john 本地登录失败');
|
||
const { response, setCookie } = await request('/auth/login', {
|
||
method: 'POST',
|
||
body: { username: USERNAME, password: JOHN_PASSWORD },
|
||
});
|
||
assert.equal(response.status, 200, 'john HTTP 登录应成功');
|
||
await pool.end();
|
||
return cookieHeader(setCookie);
|
||
}
|
||
|
||
async function main() {
|
||
console.log('=== john 水果主题 E2E(本地) ===');
|
||
await waitForPortal();
|
||
console.log(`BASE_URL: ${BASE_URL}`);
|
||
|
||
const html = await fs.readFile(htmlPath, 'utf8');
|
||
assert.match(html, /mindspace-cover/, 'HTML 应包含 mindspace-cover');
|
||
|
||
const svg = await ensureWorkspaceHtmlThumbnail(publishDir, HTML_NAME, html, {
|
||
title: '夏日鲜果指南',
|
||
subtitle: '水果主题测试',
|
||
});
|
||
assert.equal(isModernFeedThumbnail(svg), true, '工作区预览图应为新版 3:4');
|
||
assert.match(svg, /美食|鲜果|FRUIT/i, '预览图应体现水果/美食主题');
|
||
const hasPhoto = /preserveAspectRatio="xMidYMid slice"/.test(svg);
|
||
console.log(`✓ 工作区预览图已生成: ${thumbRel}(含主图: ${hasPhoto ? '是' : '否'})`);
|
||
|
||
const pageCheck = await request(`/MindSpace/${USERNAME}/${HTML_NAME}`);
|
||
assert.equal(pageCheck.response.status, 200, `公网 HTML 应可访问: ${publicPageUrl}`);
|
||
console.log(`✓ 公网 HTML 200: ${publicPageUrl}`);
|
||
|
||
const thumbCheck = await request(`/MindSpace/${USERNAME}/${thumbRel}`);
|
||
assert.equal(thumbCheck.response.status, 200, `公网预览图应可访问: ${publicThumbUrl}`);
|
||
assert.match(String(thumbCheck.payload), /width="540" height="720"/, '线上预览图为 3:4');
|
||
console.log(`✓ 公网预览图 200: ${publicThumbUrl}`);
|
||
|
||
assert.match(agentReplyLink, /^\[.+\]\(https?:\/\/.+\)$/);
|
||
console.log(`✓ Agent 应回复的可点击链接:\n ${agentReplyLink}`);
|
||
|
||
const cookie = await ensureJohnPassword();
|
||
console.log('✓ john 本地登录成功');
|
||
|
||
const { response: spaceRes, payload: spacePayload } = await request('/api/mindspace/v1/space', {
|
||
cookie,
|
||
});
|
||
assert.equal(spaceRes.status, 200, JSON.stringify(spacePayload));
|
||
|
||
const saveBody = {
|
||
title: '夏日鲜果指南 · 水果主题测试',
|
||
summary: 'TKMind 水果主题预览图 E2E',
|
||
content: html,
|
||
content_format: 'html',
|
||
page_type: 'html',
|
||
category_code: 'draft',
|
||
};
|
||
const created = await request('/api/mindspace/v1/pages', {
|
||
method: 'POST',
|
||
body: saveBody,
|
||
cookie,
|
||
});
|
||
assert.equal(created.response.status, 201, JSON.stringify(created.payload));
|
||
const pageId = created.payload?.data?.id ?? created.payload?.data?.page?.id;
|
||
assert.ok(pageId, '应创建页面记录');
|
||
|
||
await new Promise((r) => setTimeout(r, 800));
|
||
|
||
const thumbApi = await request(`/api/mindspace/v1/pages/${pageId}/thumbnail`, { cookie });
|
||
assert.equal(thumbApi.response.status, 200, '页面缩略图 API 应返回 200');
|
||
assert.match(String(thumbApi.payload), /width="540" height="720"/);
|
||
console.log(`✓ 我的空间缩略图 API 200: /api/mindspace/v1/pages/${pageId}/thumbnail`);
|
||
|
||
console.log('\n=== 全部通过 ===');
|
||
}
|
||
|
||
try {
|
||
await main();
|
||
} finally {
|
||
portal.kill('SIGTERM');
|
||
}
|