const assert = require('node:assert/strict'); const { readFileSync } = require('node:fs'); const { join } = require('node:path'); const { chromium } = require('playwright'); const base = process.env.YOUTHOME_WEB_URL || 'http://127.0.0.1:4173/pages'; const png = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIHWP4z8DwHwAFgAI/ScL/nwAAAABJRU5ErkJggg==', 'base64'); const localDate = (offsetMinutes) => { const date = new Date(Date.now() + offsetMinutes * 60000); const pad = (value) => String(value).padStart(2, '0'); return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`; }; (async () => { const browser = await chromium.launch({ channel: 'msedge', headless: true }); const page = await browser.newPage({ viewport: { width: 1440, height: 900 }, locale: 'zh-CN' }); const errors = []; const apiCalls = []; page.on('pageerror', (error) => errors.push(error.stack || error.message)); page.on('request', (request) => { if (request.url().includes('/api/')) apiCalls.push(request.url()); }); try { await page.goto(`${base}/feed.html?demo=1`); assert.equal(await page.evaluate(() => window.YouthomeApi.source()), 'local'); assert.ok(await page.evaluate(() => window.YouthomeLocal.account())); await page.goto(`${base}/publish-post.html`); await page.locator('.post-textarea').fill('离线演示动态:发布后可以查看详情。'); await page.locator('.web-publish-actions .btn-primary').click(); await page.waitForURL('**/post-detail.html?moment=*'); await page.waitForFunction(() => document.querySelector('.post-detail-content')?.textContent.includes('离线演示动态')); assert.ok((await page.locator('.post-detail-content').innerText()).includes('离线演示动态')); await page.goto(`${base}/publish-video.html`); await page.locator('.web-video-input').setInputFiles({ name: 'demo-video.mp4', mimeType: 'video/mp4', buffer: readFileSync(join(__dirname, 'fixtures', 'demo-video.mp4')) }); await page.locator('.web-video-preview').waitFor({ state: 'visible' }); await page.waitForFunction(() => document.querySelector('.web-video-preview')?.videoWidth > 0); await page.locator('input[placeholder*="视频标题"]').fill('离线演示视频'); await page.locator('textarea[placeholder*="介绍一下"]').fill('可播放的视频内容'); await page.getByText('从视频截取').click(); await page.locator('.cover-thumb.has-cover').waitFor(); await page.locator('.web-publish-actions .btn-primary').click(); await page.waitForURL('**/video-detail.html?id=*'); await page.locator('.video-player-wrap video').waitFor(); await page.waitForFunction(() => document.querySelector('.video-player-wrap video')?.videoWidth > 0); assert.ok((await page.locator('.video-title').first().innerText()).includes('离线演示视频')); assert.equal((await page.locator('.video-desc').innerText()).includes('可播放的视频内容'), true); await page.locator('.comment-textarea').fill('视频下的演示评论'); await page.locator('.comment-submit-btn').click(); await page.locator('.comment-list .comment-item').filter({ hasText: '视频下的演示评论' }).waitFor(); assert.equal(await page.locator('.video-actions .action-btn').nth(1).locator('.count').innerText(), '1'); await page.reload(); await page.locator('.comment-list .comment-item').filter({ hasText: '视频下的演示评论' }).waitFor(); await page.goto(`${base}/video.html`); assert.ok((await page.locator('.web-live-video').first().innerText()).includes('离线演示视频')); await page.goto(`${base}/publish-video.html`); await page.locator('.web-video-input').setInputFiles({ name: 'demo-video.mp4', mimeType: 'video/mp4', buffer: readFileSync(join(__dirname, 'fixtures', 'demo-video.mp4')) }); await page.locator('input[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-secondary').click(); await page.locator('.web-toast').getByText('视频草稿已保存').waitFor(); await page.reload(); await page.locator('.web-video-preview').waitFor({ state: 'visible' }); await page.locator('.cover-thumb.has-cover').waitFor(); assert.equal(await page.locator('input[placeholder*="视频标题"]').inputValue(), '上传封面演示视频'); await page.locator('.web-publish-actions .btn-primary').click(); await page.waitForURL('**/video-detail.html?id=*'); await page.locator('.video-player-wrap video').waitFor(); assert.ok((await page.locator('.video-title').first().innerText()).includes('上传封面演示视频')); await page.goto(`${base}/activity-create.html`); await page.getByRole('button', { name: '选择活动开始时间' }).click(); await page.locator('.web-date-dialog').waitFor(); await page.getByRole('button', { name: '确定' }).click(); assert.ok(await page.locator('#start-time').inputValue()); 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: '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}T18:00`; document.getElementById('end-time').value = `${day}T19:30`; document.getElementById('registration-deadline').value = `${day}T17:00`; }); await page.locator('#location-name').fill('校园体育馆'); await page.locator('.location-locate-btn').click(); await page.locator('.web-map-dialog .web-map-canvas').waitFor(); assert.equal(await page.locator('.web-map-dialog').evaluate((node) => node.open), true); await page.locator('.web-map-dialog [aria-label="关闭"]').click(); await page.locator('.btn-draft').click(); await page.locator('.web-toast').getByText('活动草稿已保存').waitFor(); await page.reload(); assert.equal(await page.locator('#activity-title').inputValue(), '离线演示组局活动'); await page.locator('#cover-upload.has-cover').waitFor(); assert.ok(await page.locator('#registration-deadline').inputValue()); await page.locator('.btn-preview').click(); await page.locator('.web-activity-preview').waitFor(); await page.getByText('关闭预览').click(); await page.locator('.btn-next').click(); await page.waitForURL('**/activity-detail.html?id=*'); assert.equal((await page.locator('.cover-title').innerText()).trim(), '离线演示组局活动'); assert.ok((await page.locator('.side-info-item').filter({ hasText: '报名截止' }).innerText()).includes('2026')); await page.goto(`${base}/activities.html`); assert.ok((await page.locator('.web-live-activity').first().innerText()).includes('离线演示组局活动')); await page.setViewportSize({ width: 390, height: 844 }); for (const name of ['publish-post', 'publish-video', 'activity-create']) { await page.goto(`${base}/${name}.html`); const overflow = await page.evaluate(() => document.documentElement.scrollWidth - innerWidth); assert.ok(overflow <= 0, `${name} overflows by ${overflow}px at 390px`); } await page.getByRole('button', { name: '选择活动开始时间' }).click(); assert.ok(await page.locator('.web-date-dialog').evaluate((node) => node.getBoundingClientRect().right <= innerWidth)); await page.locator('.web-date-dialog [aria-label="关闭"]').click(); await page.locator('.location-locate-btn').click(); assert.ok(await page.locator('.web-map-dialog').evaluate((node) => node.getBoundingClientRect().right <= innerWidth)); await page.locator('.web-map-dialog [aria-label="关闭"]').click(); assert.deepEqual(apiCalls, []); assert.deepEqual(errors, []); await page.goto(`${base}/login.html?demo=0`); assert.equal(await page.evaluate(() => window.YouthomeApi.source()), 'api'); assert.equal(await page.evaluate(() => window.YouthomeApi.token()), null); console.log('offline post, video, cover capture, activity publish and detail passed'); } finally { await browser.close(); } })().catch((error) => { console.error(error); process.exitCode = 1; });