You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

135 lines
8.2 KiB

const assert = require('node:assert/strict');
const { readFileSync } = require('node:fs');
const { join } = require('node:path');
const { chromium, request } = require('playwright');
if (process.env.YOUTHOME_LIVE_BACKEND !== '1') {
console.error('Set YOUTHOME_LIVE_BACKEND=1 to run the write-and-cleanup test.');
process.exit(1);
}
const webBase = process.env.YOUTHOME_WEB_URL || 'http://127.0.0.1:4173/pages';
const apiBase = process.env.YOUTHOME_BACKEND_URL || 'http://127.0.0.1:8009';
const png = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIHWP4z8DwHwAFgAI/ScL/nwAAAABJRU5ErkJggg==', 'base64');
const video = readFileSync(join(__dirname, 'fixtures', 'demo-video.mp4'));
(async () => {
const browser = await chromium.launch({ channel: 'msedge', headless: true });
const api = await request.newContext({ baseURL: apiBase, timeout: 120000 });
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
const phone = `199${String(Date.now()).slice(-8)}`;
const password = `Webtest${String(Date.now()).slice(-6)}a`;
const uploads = new Set();
const pageErrors = [];
let token;
let videoId;
let activityId;
let commentId;
page.on('pageerror', (error) => pageErrors.push(error.message));
page.on('response', async (response) => {
if (!response.url().includes('/api/file/upload/') || !response.ok()) return;
try {
const body = await response.json();
const url = body.data?.url || body.url;
if (url) uploads.add(url);
} catch (error) { /* a failed upload is asserted by the publishing flow */ }
});
const cleanup = async (label, path) => {
try {
const response = await api.delete(path, { headers: { access_token: token } });
console.log(`${label} cleanup: HTTP ${response.status()}`);
if (!response.ok()) process.exitCode = 1;
} catch (error) {
console.error(`${label} cleanup failed: ${error.message}`);
process.exitCode = 1;
}
};
try {
const registration = await api.post('/api/auth/register', {
data: { phone, password, captcha: '111111', universityName: '复旦大学', nickname: '网页发布回归', platform: 'WEB' }
});
const registered = await registration.json();
assert.equal(registered.code, 200, JSON.stringify(registered));
token = registered.data.token;
await page.goto(`${webBase}/login.html?next=publish-video.html&demo=0`);
await page.locator('#panel-password input[type="tel"]').fill(phone);
await page.locator('#panel-password input[type="password"]').fill(password);
await page.locator('#panel-password .btn-primary').click();
await page.waitForURL('**/publish-video.html');
await page.locator('.web-video-input').setInputFiles({ name: 'demo-video.mp4', mimeType: 'video/mp4', buffer: video });
await page.locator('.web-video-preview').waitFor({ state: 'visible' });
await page.locator('input[placeholder*="视频标题"]').fill('真实接口视频回归');
await page.locator('textarea[placeholder*="介绍一下"]').fill('真实接口视频简介回归');
await page.locator('.web-cover-input').setInputFiles({ name: 'cover.png', mimeType: 'image/png', buffer: png });
await page.locator('.cover-thumb.has-cover').waitFor();
await page.locator('.web-publish-actions .btn-primary').click();
await page.waitForURL('**/video-detail.html?id=*', { timeout: 120000 });
videoId = new URL(page.url()).searchParams.get('id');
await page.waitForFunction(() => document.querySelector('.video-title')?.textContent === '真实接口视频回归');
assert.ok((await page.locator('.video-desc').innerText()).includes('真实接口视频简介回归'));
await page.locator('.video-player-wrap video').waitFor();
await page.waitForFunction(() => document.querySelector('.video-player-wrap video')?.videoWidth > 0, null, { timeout: 30000 });
await page.locator('.comment-textarea').fill('真实接口视频评论');
await page.locator('.comment-submit-btn').click();
await page.locator('.comment-list .comment-item').filter({ hasText: '真实接口视频评论' }).waitFor();
const commentsResponse = await api.get(`/api/campus/moments/comments?momentId=${encodeURIComponent(videoId)}&page=1&size=50`, { headers: { access_token: token } });
const commentsResult = await commentsResponse.json();
commentId = commentsResult.data?.items?.find((item) => item.content === '真实接口视频评论')?.id;
assert.ok(commentId, JSON.stringify(commentsResult));
console.log('Real video publish, description and comment passed');
await page.goto(`${webBase}/activity-create.html`);
await page.locator('#activity-title').fill('真实接口活动回归');
await page.locator('#activity-desc').fill('真实接口活动介绍回归');
await page.locator('#activity-type').selectOption('sports');
await page.locator('.web-activity-cover-input').setInputFiles({ name: 'activity-cover.png', mimeType: 'image/png', buffer: png });
await page.locator('#cover-upload.has-cover').waitFor();
await page.evaluate(() => {
const date = new Date(Date.now() + 3 * 86400000);
const pad = (value) => String(value).padStart(2, '0');
const day = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
document.getElementById('start-time').value = `${day} 18:00`;
document.getElementById('end-time').value = `${day} 19:00`;
document.getElementById('registration-deadline').value = `${day} 17:00`;
});
await page.locator('#location-name').fill('复旦大学体育馆');
await page.locator('#location-address').fill('上海市杨浦区邯郸路220号');
await page.locator('.btn-next').click();
await page.waitForURL('**/activity-detail.html?id=*', { timeout: 120000 });
activityId = new URL(page.url()).searchParams.get('id');
await page.waitForFunction(() => document.querySelector('.cover-title')?.textContent.trim() === '真实接口活动回归');
assert.ok((await page.locator('.side-info-item').filter({ hasText: '报名截止' }).innerText()).includes('17:00'));
const createdResponse = await api.get('/api/campus/activities/created?page=1&size=100', { headers: { access_token: token } });
const createdData = await createdResponse.json();
const createdActivity = createdData.data?.items?.find((item) => item.id === activityId);
console.log('Created activity media fields:', JSON.stringify(Object.fromEntries(Object.entries(createdActivity || {}).filter(([key]) => /cover|image|media|banner/i.test(key)))));
assert.ok(createdActivity?.coverImage);
const coverState = await page.locator('.activity-detail-cover').evaluate(async (element) => {
const background = getComputedStyle(element).backgroundImage;
const url = background.match(/^url\(["']?(.+?)["']?\)$/)?.[1];
if (!url) return { background, loaded: false };
const image = new Image(); image.src = url;
try { await image.decode(); return { background, loaded: image.naturalWidth > 0 }; }
catch (error) { return { background, loaded: false }; }
});
console.log('Activity cover state:', JSON.stringify(coverState));
assert.ok(coverState.loaded);
assert.deepEqual(pageErrors, []);
console.log('Real activity publish, deadline and detail passed');
} finally {
if (token && commentId && videoId) await cleanup('Comment', `/api/campus/moments/comments?momentId=${encodeURIComponent(videoId)}&commentId=${encodeURIComponent(commentId)}`);
if (token && videoId) await cleanup('Video', `/api/campus/moments?id=${encodeURIComponent(videoId)}`);
if (token && activityId) await cleanup('Activity', `/api/campus/activities?id=${encodeURIComponent(activityId)}`);
if (token) for (const url of uploads) await cleanup('File', `/api/file/delete?url=${encodeURIComponent(url)}`);
if (token) {
try {
const response = await api.post('/api/users/deactivate?confirm=true', { headers: { access_token: token } });
console.log(`Temporary account cleanup: HTTP ${response.status()}`);
if (!response.ok()) process.exitCode = 1;
} catch (error) { console.error(`Temporary account cleanup failed: ${error.message}`); process.exitCode = 1; }
}
await api.dispose();
await browser.close();
}
})().catch((error) => { console.error(error); process.exitCode = 1; });