mirror of
https://github.com/jackwener/OpenCLI.git
synced 2026-09-14 18:25:42 +08:00
257e9ec4f8
# feat: add E2E testing infrastructure with real Chrome in CI ## What Establish a comprehensive E2E testing framework for opencli using **real Chrome + xvfb virtual display** in GitHub Actions CI. ## Changes ### E2E Test Suite (~52 test cases) - `public-commands.test.ts` — Public API commands (hackernews, v2ex) - `browser-public.test.ts` — Browser commands for public data across all 18 sites (21 tests) - `browser-auth.test.ts` — Graceful failure verification for login-required commands (14 tests) - `management.test.ts` — Full coverage of management commands (list/validate/verify/version/help) - `output-formats.test.ts` — Output format validation (json/yaml/csv/md) - `smoke/api-health.test.ts` — Scheduled API health checks ### Auto-detect Browser Mode - `buildMcpArgs` automatically selects mode based on `PLAYWRIGHT_MCP_EXTENSION_TOKEN`: - Token present → `--extension` (local user, connects to logged-in Chrome) - Token absent → standalone (CI launches its own browser) - No extra environment variables needed ### CI Pipeline - `e2e-headed.yml` — Real Chrome via `browser-actions/setup-chrome` + `xvfb-run` in headed mode - `ci.yml` — build + unit-test (2 shards) + smoke-test (scheduled/manual) - Browser commands that return empty data due to geo-blocking or bot detection gracefully warn + pass without blocking CI ### Documentation - New `TESTING.md` — Architecture, coverage, local setup, how to add tests, CI explanation - Updated `README.md` — Added Testing section with quick-start commands
91 lines
4.0 KiB
TypeScript
91 lines
4.0 KiB
TypeScript
/**
|
|
* E2E tests for login-required browser commands.
|
|
* These commands REQUIRE authentication (cookie/session).
|
|
* In CI (headless, no login), they should fail gracefully — NOT crash.
|
|
*
|
|
* These tests verify the error handling path, not the data extraction.
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import { runCli } from './helpers.js';
|
|
|
|
/**
|
|
* Verify a login-required command fails gracefully (no crash, no hang).
|
|
* Acceptable outcomes: exit code 1 with error message, OR timeout handled.
|
|
*/
|
|
async function expectGracefulAuthFailure(args: string[], label: string) {
|
|
const { stdout, stderr, code } = await runCli(args, { timeout: 60_000 });
|
|
// Should either fail with exit code 1 (error message) or succeed with empty data
|
|
// The key assertion: it should NOT hang forever or crash with unhandled exception
|
|
if (code !== 0) {
|
|
// Verify stderr has a meaningful error, not an unhandled crash
|
|
const output = stderr + stdout;
|
|
expect(output.length).toBeGreaterThan(0);
|
|
}
|
|
// If it somehow succeeds (e.g., partial public data), that's fine too
|
|
}
|
|
|
|
describe('login-required commands — graceful failure', () => {
|
|
|
|
// ── bilibili (requires cookie session) ──
|
|
it('bilibili me fails gracefully without login', async () => {
|
|
await expectGracefulAuthFailure(['bilibili', 'me', '-f', 'json'], 'bilibili me');
|
|
}, 60_000);
|
|
|
|
it('bilibili dynamic fails gracefully without login', async () => {
|
|
await expectGracefulAuthFailure(['bilibili', 'dynamic', '--limit', '3', '-f', 'json'], 'bilibili dynamic');
|
|
}, 60_000);
|
|
|
|
it('bilibili favorite fails gracefully without login', async () => {
|
|
await expectGracefulAuthFailure(['bilibili', 'favorite', '--limit', '3', '-f', 'json'], 'bilibili favorite');
|
|
}, 60_000);
|
|
|
|
it('bilibili history fails gracefully without login', async () => {
|
|
await expectGracefulAuthFailure(['bilibili', 'history', '--limit', '3', '-f', 'json'], 'bilibili history');
|
|
}, 60_000);
|
|
|
|
it('bilibili following fails gracefully without login', async () => {
|
|
await expectGracefulAuthFailure(['bilibili', 'following', '--limit', '3', '-f', 'json'], 'bilibili following');
|
|
}, 60_000);
|
|
|
|
// ── twitter (requires login) ──
|
|
it('twitter bookmarks fails gracefully without login', async () => {
|
|
await expectGracefulAuthFailure(['twitter', 'bookmarks', '--limit', '3', '-f', 'json'], 'twitter bookmarks');
|
|
}, 60_000);
|
|
|
|
it('twitter timeline fails gracefully without login', async () => {
|
|
await expectGracefulAuthFailure(['twitter', 'timeline', '--limit', '3', '-f', 'json'], 'twitter timeline');
|
|
}, 60_000);
|
|
|
|
it('twitter notifications fails gracefully without login', async () => {
|
|
await expectGracefulAuthFailure(['twitter', 'notifications', '--limit', '3', '-f', 'json'], 'twitter notifications');
|
|
}, 60_000);
|
|
|
|
// ── v2ex (requires login) ──
|
|
it('v2ex me fails gracefully without login', async () => {
|
|
await expectGracefulAuthFailure(['v2ex', 'me', '-f', 'json'], 'v2ex me');
|
|
}, 60_000);
|
|
|
|
it('v2ex notifications fails gracefully without login', async () => {
|
|
await expectGracefulAuthFailure(['v2ex', 'notifications', '--limit', '3', '-f', 'json'], 'v2ex notifications');
|
|
}, 60_000);
|
|
|
|
// ── xueqiu (requires login) ──
|
|
it('xueqiu feed fails gracefully without login', async () => {
|
|
await expectGracefulAuthFailure(['xueqiu', 'feed', '--limit', '3', '-f', 'json'], 'xueqiu feed');
|
|
}, 60_000);
|
|
|
|
it('xueqiu watchlist fails gracefully without login', async () => {
|
|
await expectGracefulAuthFailure(['xueqiu', 'watchlist', '-f', 'json'], 'xueqiu watchlist');
|
|
}, 60_000);
|
|
|
|
// ── xiaohongshu (requires login) ──
|
|
it('xiaohongshu feed fails gracefully without login', async () => {
|
|
await expectGracefulAuthFailure(['xiaohongshu', 'feed', '--limit', '3', '-f', 'json'], 'xiaohongshu feed');
|
|
}, 60_000);
|
|
|
|
it('xiaohongshu notifications fails gracefully without login', async () => {
|
|
await expectGracefulAuthFailure(['xiaohongshu', 'notifications', '--limit', '3', '-f', 'json'], 'xiaohongshu notifications');
|
|
}, 60_000);
|
|
});
|