Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

517 lines
21 KiB
JavaScript

#!/usr/bin/env node
/**
* News & Subscriptions UI Tests
*
* Tests for the news feed page and subscription management.
*
* Run: node test_news_subscriptions_ci.js
*/
const { setupTest, teardownTest, TestResults, log, navigateTo, withTimeout, findActionButton } = require('./test_lib');
const { seedSubscription, deleteSubscription } = require('./test_lib/fixtures');
// ============================================================================
// News Feed Page Tests
// ============================================================================
const NewsFeedTests = {
async newsPageLoads(page, baseUrl) {
await navigateTo(page, `${baseUrl}/news`);
const result = await page.evaluate(() => {
return {
hasNewsContent: !!document.querySelector('.news-container, .ldr-news, #news-feed, .news-feed'),
hasHeader: !!document.querySelector('h1, .news-header, .page-title'),
headerText: document.querySelector('h1, .news-header, .page-title')?.textContent?.trim(),
hasCards: document.querySelectorAll('.news-card, .news-item, .article-card, [data-news-id]').length,
hasEmptyState: !!document.querySelector('.ldr-empty-state, .no-news, .alert-info')
};
});
const passed = result.hasNewsContent || result.hasHeader || result.hasCards > 0 || result.hasEmptyState;
return {
passed,
message: passed
? `News page loaded (header: "${result.headerText}", cards: ${result.hasCards})`
: 'News page failed to load'
};
},
async newsCardStructure(page, baseUrl) {
await navigateTo(page, `${baseUrl}/news`);
const result = await page.evaluate(() => {
const cards = document.querySelectorAll('.news-card, .news-item, .article-card, [data-news-id]');
if (cards.length === 0) return { hasCards: false };
const firstCard = cards[0];
return {
hasCards: true,
cardCount: cards.length,
hasTitle: !!firstCard.querySelector('.card-title, h3, h4, .title, .news-title'),
hasSummary: !!firstCard.querySelector('.card-text, .summary, .description, p'),
hasSource: !!firstCard.querySelector('.source, .publication, .meta'),
hasDate: !!firstCard.querySelector('.date, time, .timestamp'),
hasActions: !!firstCard.querySelector('.card-actions, .actions, button, .btn')
};
});
if (!result.hasCards) {
return { passed: null, skipped: true, message: 'No news cards to test structure' };
}
const hasRequiredParts = result.hasTitle;
return {
passed: hasRequiredParts,
message: hasRequiredParts
? `News cards: ${result.cardCount} found (title=${result.hasTitle}, summary=${result.hasSummary}, actions=${result.hasActions})`
: 'News cards missing required title element'
};
},
async newsCardVoteButtons(page, baseUrl) {
await navigateTo(page, `${baseUrl}/news`);
const result = await page.evaluate(() => {
const cards = document.querySelectorAll('.news-card, .news-item, .article-card');
if (cards.length === 0) return { hasCards: false };
const firstCard = cards[0];
const upvoteBtn = firstCard.querySelector(
'.upvote, ' +
'.vote-up, ' +
'[data-vote="up"], ' +
'button[onclick*="upvote"], ' +
'.fa-thumbs-up, ' +
'.bi-hand-thumbs-up'
);
const downvoteBtn = firstCard.querySelector(
'.downvote, ' +
'.vote-down, ' +
'[data-vote="down"], ' +
'button[onclick*="downvote"], ' +
'.fa-thumbs-down, ' +
'.bi-hand-thumbs-down'
);
return {
hasCards: true,
hasUpvote: !!upvoteBtn,
hasDownvote: !!downvoteBtn
};
});
if (!result.hasCards) {
return { passed: null, skipped: true, message: 'No news cards to test vote buttons' };
}
const hasVoting = result.hasUpvote || result.hasDownvote;
if (!hasVoting) {
return { passed: null, skipped: true, message: 'No voting buttons found on news cards' };
}
return {
passed: true,
message: `Vote buttons found (upvote=${result.hasUpvote}, downvote=${result.hasDownvote})`
};
},
async deeperResearchButton(page, baseUrl) {
await navigateTo(page, `${baseUrl}/news`);
const result = await page.evaluate(() => {
const cards = document.querySelectorAll('.news-card, .news-item, .article-card');
if (cards.length === 0) return { hasCards: false };
const buttons = Array.from(document.querySelectorAll('button, a.btn'));
const researchBtn = buttons.find(b =>
b.textContent?.toLowerCase().includes('research') ||
b.textContent?.toLowerCase().includes('deeper') ||
b.textContent?.toLowerCase().includes('investigate')
);
return {
hasCards: true,
hasResearchButton: !!researchBtn,
buttonText: researchBtn?.textContent?.trim()
};
});
if (!result.hasCards) {
return { passed: null, skipped: true, message: 'No news cards to test deeper research button' };
}
if (!result.hasResearchButton) {
return { passed: null, skipped: true, message: 'No deeper research button found' };
}
return {
passed: true,
message: `Deeper research button found ("${result.buttonText}")`
};
},
async newsCategoryFilter(page, baseUrl) {
await navigateTo(page, `${baseUrl}/news`);
const result = await page.evaluate(() => {
const categoryFilter = document.querySelector(
'select[name*="category"], ' +
'.category-filter, ' +
'.category-tabs, ' +
'[data-category]'
);
const categoryBadges = document.querySelectorAll('.category-badge, .category-tag, .badge[data-category]');
return {
hasFilter: !!categoryFilter,
hasCategoryBadges: categoryBadges.length > 0,
badgeCount: categoryBadges.length
};
});
if (!result.hasFilter && !result.hasCategoryBadges) {
return { passed: null, skipped: true, message: 'No category filter found' };
}
return {
passed: true,
message: result.hasFilter
? 'Category filter dropdown found'
: `${result.badgeCount} category badges found`
};
}
};
// ============================================================================
// Subscriptions Page Tests
// ============================================================================
const SubscriptionsPageTests = {
async subscriptionsPageLoads(page, baseUrl) {
await navigateTo(page, `${baseUrl}/news/subscriptions`);
const result = await page.evaluate(() => {
return {
hasContent: !!document.querySelector('.subscriptions-container, .ldr-subscriptions, #subscriptions'),
hasHeader: !!document.querySelector('h1, .subscriptions-header, .page-title'),
headerText: document.querySelector('h1, .subscriptions-header, .page-title')?.textContent?.trim(),
subscriptionCount: document.querySelectorAll('.subscription-card, .subscription-item, [data-subscription-id]').length,
hasEmptyState: !!document.querySelector('.ldr-empty-state, .no-subscriptions')
};
});
const passed = result.hasContent || result.hasHeader || result.subscriptionCount > 0 || result.hasEmptyState;
return {
passed,
message: passed
? `Subscriptions page loaded (header: "${result.headerText}", subscriptions: ${result.subscriptionCount})`
: 'Subscriptions page failed to load'
};
},
async subscriptionStatsDisplay(page, baseUrl) {
await navigateTo(page, `${baseUrl}/news/subscriptions`);
const result = await page.evaluate(() => {
const statsSection = document.querySelector('.stats-overview, .subscription-stats, .stats');
const statBadges = document.querySelectorAll('.stat-badge, .badge, .ldr-stat-item');
// Look for common stat indicators
const pageText = document.body.textContent?.toLowerCase() || '';
const hasTotal = pageText.includes('total');
const hasActive = pageText.includes('active');
const hasPaused = pageText.includes('paused') || pageText.includes('inactive');
return {
hasStatsSection: !!statsSection,
statCount: statBadges.length,
hasTotal,
hasActive,
hasPaused
};
});
const hasAnyStats = result.hasStatsSection || (result.hasTotal && result.hasActive);
if (!hasAnyStats) {
return { passed: null, skipped: true, message: 'No subscription stats display found' };
}
return {
passed: true,
message: `Subscription stats found (total=${result.hasTotal}, active=${result.hasActive}, paused=${result.hasPaused})`
};
},
async createSubscriptionButton(page, baseUrl) {
await navigateTo(page, `${baseUrl}/news/subscriptions`);
const { found, text } = await findActionButton(page);
return {
passed: found,
message: found
? `Create subscription button found ("${text}")`
: 'No create subscription button found'
};
},
async subscriptionCardStructure(page, baseUrl) {
// Subscription cards render client-side (subscriptions.js::
// createSubscriptionCard) as .ldr-subscription-card[data-subscription-id]
// with .ldr-card-header h4 (title) + .ldr-query-text + .ldr-status-badge,
// an edit link (a[href*="/edit"]) and a .btn-danger delete button. The old
// test scanned for `.subscription-card` (no such class) and SKIPped on an
// empty DB. Seed a subscription and assert the real card markup.
await navigateTo(page, `${baseUrl}/news/subscriptions`);
const seeded = await seedSubscription(page);
if (!seeded) {
return { passed: false, message: 'Could not seed subscription for card-structure test' };
}
try {
// navigateTo no-ops (already on this path); reload to render the card.
await page.reload({ waitUntil: 'domcontentloaded' });
await page.waitForSelector(`.ldr-subscription-card[data-subscription-id="${seeded.id}"]`, { timeout: 10000 });
const result = await page.evaluate((id) => {
const card = document.querySelector(`.ldr-subscription-card[data-subscription-id="${id}"]`);
if (!card) return { found: false };
return {
found: true,
hasTitle: !!card.querySelector('.ldr-card-header h4'),
hasQuery: !!card.querySelector('.ldr-query-text'),
hasStatus: !!card.querySelector('.ldr-status-badge'),
hasEdit: !!card.querySelector('a[href*="/edit"]'),
hasDelete: !!card.querySelector('.btn-danger'),
};
}, seeded.id);
const passed = result.found && result.hasTitle && result.hasQuery
&& result.hasStatus && result.hasEdit && result.hasDelete;
return {
passed,
message: passed
? 'Subscription card renders real structure (title + query + status badge + edit + delete)'
: `Subscription card structure incomplete (${JSON.stringify(result)})`
};
} finally {
await deleteSubscription(page, seeded.id);
}
},
async subscriptionFormPage(page, baseUrl) {
// The Create Subscription flow is page-navigation, not a modal:
// #create-subscription-btn on /news/subscriptions has a JS handler
// that runs `window.location.href = '/news/subscriptions/new'`.
// Earlier this method ("subscriptionFormModal") asserted a modal
// and accidentally passed when the navigation landed on the new
// page (which happens to contain a form). Rename + rewrite to
// test the real flow: navigate directly to /new and assert the
// create form is present and reachable.
await navigateTo(page, `${baseUrl}/news/subscriptions/new`);
const result = await page.evaluate(() => {
const form = document.querySelector('form:not(#logout-form)');
const query = document.querySelector('#subscription-query');
return {
hasForm: !!form,
hasQueryField: !!query,
queryRequired: !!query?.required,
};
});
const passed = result.hasForm && result.hasQueryField;
return {
passed,
message: passed
? `Subscription create form reachable (queryRequired=${result.queryRequired})`
: `Subscription create form missing (form=${result.hasForm}, query=${result.hasQueryField})`
};
},
async subscriptionFormFields(page, baseUrl) {
// The form lives at /news/subscriptions/new and uses
// `subscription-*` ids (#subscription-query, #subscription-model,
// #subscription-search-engine, ...). The previous version
// looked for `name*="query"` / `#query` / etc., which match
// nothing on the actual page, so this test always reported a
// misleading SKIP ("Could not find subscription form fields").
await navigateTo(page, `${baseUrl}/news/subscriptions/new`);
const result = await page.evaluate(() => {
const form = document.querySelector('form:not(#logout-form)');
if (!form) return { hasForm: false };
return {
hasForm: true,
hasQueryField: !!form.querySelector('#subscription-query'),
hasModelField: !!form.querySelector('#subscription-model'),
hasSearchEngineField: !!form.querySelector('#subscription-search-engine'),
hasStrategyField: !!form.querySelector('#subscription-strategy'),
hasActiveToggle: !!form.querySelector('#subscription-active'),
};
});
if (!result.hasForm) {
return { passed: false, message: 'Subscription form not present at /news/subscriptions/new' };
}
// The query textarea is the core required field. Other fields
// (model, search engine, strategy, active toggle) round out the
// expected schema; we treat all four as required signals so the
// test fails loudly if the form skeleton ever drops a section.
const passed = result.hasQueryField &&
result.hasModelField &&
result.hasSearchEngineField &&
result.hasStrategyField &&
result.hasActiveToggle;
return {
passed,
message: passed
? 'Subscription form has query, model, search-engine, strategy, active toggle'
: `Subscription form missing fields: query=${result.hasQueryField}, model=${result.hasModelField}, search=${result.hasSearchEngineField}, strategy=${result.hasStrategyField}, active=${result.hasActiveToggle}`
};
}
};
// ============================================================================
// News API Tests
// ============================================================================
const NewsApiTests = {
async newsFeedApiResponds(page, baseUrl) {
await navigateTo(page, `${baseUrl}/news`);
const result = await page.evaluate(async (url) => {
try {
const response = await fetch(`${url}/api/news/feed`);
if (!response.ok) return { ok: false, status: response.status };
const data = await response.json();
return {
ok: true,
status: response.status,
itemCount: Array.isArray(data) ? data.length : (data.items?.length || 0)
};
} catch (e) {
return { ok: false, error: e.message };
}
}, baseUrl);
if (!result.ok && result.status === 404) {
return { passed: null, skipped: true, message: 'News feed API endpoint not found' };
}
return {
passed: result.ok,
message: result.ok
? `News feed API responds (${result.itemCount} items)`
: `News feed API failed: ${result.error || 'status ' + result.status}`
};
},
async subscriptionsApiResponds(page, baseUrl) {
await navigateTo(page, `${baseUrl}/news/subscriptions`);
const result = await page.evaluate(async (url) => {
try {
const response = await fetch(`${url}/api/news/subscriptions`);
if (!response.ok) return { ok: false, status: response.status };
const data = await response.json();
return {
ok: true,
status: response.status,
subscriptionCount: Array.isArray(data) ? data.length : (data.subscriptions?.length || 0)
};
} catch (e) {
return { ok: false, error: e.message };
}
}, baseUrl);
if (!result.ok && result.status === 404) {
return { passed: null, skipped: true, message: 'Subscriptions API endpoint not found' };
}
return {
passed: result.ok,
message: result.ok
? `Subscriptions API responds (${result.subscriptionCount} subscriptions)`
: `Subscriptions API failed: ${result.error || 'status ' + result.status}`
};
}
};
// ============================================================================
// Main Test Runner
// ============================================================================
async function main() {
log.section('News & Subscriptions Tests');
const ctx = await setupTest({ authenticate: true });
const results = new TestResults('News & Subscriptions Tests');
const { page } = ctx;
const { baseUrl } = ctx.config;
const subTestTimeout = ctx.config.isCI ? 60000 : 30000;
async function run(category, name, testFn) {
try {
const result = await withTimeout(
testFn(page, baseUrl),
subTestTimeout,
`${category}/${name}`
);
if (result.skipped) {
results.skip(category, name, result.message);
} else {
results.add(category, name, result.passed, result.message);
}
} catch (error) {
results.add(category, name, false, `Error: ${error.message}`);
}
}
try {
// News Feed Tests
log.section('News Feed');
await run('News', 'News Page Loads', (p, u) => NewsFeedTests.newsPageLoads(p, u));
await run('News', 'News Card Structure', (p, u) => NewsFeedTests.newsCardStructure(p, u));
await run('News', 'News Card Vote Buttons', (p, u) => NewsFeedTests.newsCardVoteButtons(p, u));
await run('News', 'Deeper Research Button', (p, u) => NewsFeedTests.deeperResearchButton(p, u));
await run('News', 'Category Filter', (p, u) => NewsFeedTests.newsCategoryFilter(p, u));
// Subscriptions Page Tests
log.section('Subscriptions Page');
await run('Subscriptions', 'Subscriptions Page Loads', (p, u) => SubscriptionsPageTests.subscriptionsPageLoads(p, u));
await run('Subscriptions', 'Subscription Stats Display', (p, u) => SubscriptionsPageTests.subscriptionStatsDisplay(p, u));
await run('Subscriptions', 'Create Subscription Button', (p, u) => SubscriptionsPageTests.createSubscriptionButton(p, u));
await run('Subscriptions', 'Subscription Card Structure', (p, u) => SubscriptionsPageTests.subscriptionCardStructure(p, u));
await run('Subscriptions', 'Subscription Form Page', (p, u) => SubscriptionsPageTests.subscriptionFormPage(p, u));
await run('Subscriptions', 'Subscription Form Fields', (p, u) => SubscriptionsPageTests.subscriptionFormFields(p, u));
// API Tests
log.section('News APIs');
await run('API', 'News Feed API Responds', (p, u) => NewsApiTests.newsFeedApiResponds(p, u));
await run('API', 'Subscriptions API Responds', (p, u) => NewsApiTests.subscriptionsApiResponds(p, u));
} catch (error) {
log.error(`Fatal error: ${error.message}`);
console.error(error.stack);
} finally {
results.print();
results.save();
await teardownTest(ctx);
process.exit(results.exitCode());
}
}
// Run if executed directly
if (require.main === module) {
main().catch(error => {
console.error('Test runner failed:', error);
process.exit(1);
});
}
module.exports = { NewsFeedTests, SubscriptionsPageTests, NewsApiTests };