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; });