/** * 任务:查看热门 * * 逻辑: * 1. 找到"查看热门"任务卡片 * 2. 点击 toComplete 打开热门页面 * 3. 点击第一个热门项目卡片 * 4. 在新页面停留后关闭 */ const path = require('path'); const logger = require(path.join(__dirname, '..', 'utils', 'logger')); const { openNewPage, openLinkPage, safeClose, waitPageReady, waitForElements, getPageInfo, } = require(path.join(__dirname, '..', 'utils', 'page')); const { runDebugSteps } = require(path.join(__dirname, '..', 'utils', 'debugSteps')); const PROJECT_CARD_SELECTORS = [ '.dynamics-grid article.article-card', '.dynamics-grid .article-card', 'article.article-card:has(h3)', ]; /** * 执行查看热门任务 * @param {import('playwright').Page} page - 主页面 */ async function dailyTrending(page, options = {}) { logger.info('👉 执行查看热门'); const ctx = { page, trendingTask: null, trendingPage: null, cards: [], detailPage: null, }; const steps = [ { name: '查找查看热门任务卡片', page: (state) => state.page, run: async (state) => { const tasks = await state.page.locator('.novice-task-item-box').all(); state.trendingTask = await findTaskByTitle(tasks, '查看热门'); if (!state.trendingTask) { throw new Error('未找到"查看热门"任务卡片'); } }, inspect: async (state) => ({ taskCardCount: await state.page.locator('.novice-task-item-box').count(), titleLabelCount: await state.page.locator('.task-card-title__label-text').count(), toCompleteCount: await state.page.locator('.toComplete').count(), }), }, { name: '点击去完成并打开热门页', page: (state) => state.trendingPage || state.page, run: async (state) => { state.trendingPage = await openNewPage( state.page.context(), () => state.trendingTask.locator('.toComplete').click() ); state.currentPage = state.trendingPage; }, inspect: async (state) => ({ taskToCompleteVisible: await state.trendingTask.locator('.toComplete').isVisible().catch(() => false), }), }, { name: '查找热门项目卡片', page: (state) => state.trendingPage, run: async (state) => { const targetPage = state.trendingPage; if (!targetPage || targetPage.isClosed()) { throw new Error('热门页面不存在或已经关闭'); } state.currentPage = targetPage; logger.info(`查找热门项目卡片,当前页面: ${targetPage.url()}`); // 等待热门项目区域出现 await targetPage .locator('.dynamics-grid') .waitFor({ state: 'visible', timeout: 15000, }); state.cards = await findProjectCards(targetPage); if (state.cards.length === 0) { throw new Error('未找到热门项目卡片'); } logger.info(`找到热门项目卡片: ${state.cards.length} 个`); }, inspect: async (state) => ({ currentUrl: state.trendingPage?.url(), dynamicsGridCount: await state.trendingPage .locator('.dynamics-grid') .count(), dynamicCardCount: await state.trendingPage .locator('.dynamics-grid .dynamic-card') .count(), repoCardCount: await state.trendingPage .locator('.dynamics-grid .home-repo-card') .count(), repoLinkCount: await state.trendingPage .locator('.dynamics-grid a.home-repo-card__link') .count(), dynamicsSourceLinkCount: await state.trendingPage .locator('a[href*="source_module=dynamics"]') .count(), }), }, { name: '打开第一个热门项目详情页', page: (state) => state.trendingPage, run: async (state) => { const firstCard = state.cards[0]; if (!firstCard) { throw new Error('没有找到可点击的热门项目卡片'); } await firstCard.scrollIntoViewIfNeeded(); const href = await firstCard.getAttribute('href'); logger.info(`准备点击热门项目: ${href}`); const context = state.trendingPage.context(); const [detailPage] = await Promise.all([ context.waitForEvent('page', { timeout: 15000, }), firstCard.click({ timeout: 15000, }), ]); state.detailPage = detailPage; state.currentPage = detailPage; await waitPageReady(detailPage); logger.info(`热门项目详情页已打开: ${detailPage.url()}`); }, inspect: async (state) => ({ cardCount: state.cards.length, firstCardHref: state.cards[0] ? await state.cards[0].getAttribute('href') : null, }), }, { name: '停留热门项目详情页', page: (state) => state.detailPage, run: async (state) => { if (!state.detailPage || state.detailPage.isClosed()) { throw new Error('热门项目详情页不存在或已关闭'); } await waitPageReady(state.detailPage); // 给页面曝光统计、埋点和任务接口留出时间 await state.detailPage.waitForTimeout(10000); logger.info( `已在热门项目详情页停留10秒: ${state.detailPage.url()}` ); }, inspect: async (state) => ({ detailPageUrl: state.detailPage?.url(), detailPageTitle: state.detailPage ? await state.detailPage.title().catch(() => '') : '', }), }, ]; try { await runDebugSteps('查看热门', steps, ctx, options); await ctx.page.waitForTimeout(5000); } finally { await safeClose(ctx.detailPage); await safeClose(ctx.trendingPage); } } async function findProjectCards(page) { logger.info(`findProjectCards 实际页面: ${page.url()}`); for (const selector of PROJECT_CARD_SELECTORS) { logger.info(`尝试选择器: "${selector}"`); const locator = page.locator(selector); try { await locator.first().waitFor({ state: 'visible', timeout: 5000, }); const count = await locator.count(); if (count > 0) { logger.info(`选择器 "${selector}" 找到 ${count} 个热门项目链接`); return Array.from( { length: count }, (_, index) => locator.nth(index) ); } } catch (_) { logger.info(`选择器 "${selector}" 未找到元素`); } } const info = await getPageInfo(page); throw new Error( `未找到热门项目卡片,页面结构可能已变化。` + `页面标题: ${info.title}, 页面URL: ${info.url}` ); } /** * 根据标题查找任务卡片 */ async function findTaskByTitle(tasks, title) { for (const task of tasks) { try { const text = await task.locator('.task-card-title__label-text').innerText(); if (text.includes(title)) { return task; } } catch (_) { // 跳过 } } return null; } module.exports = dailyTrending;