@jackwener/opencli 0.6.1 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/.github/actions/setup-chrome/action.yml +26 -0
  2. package/.github/workflows/ci.yml +59 -3
  3. package/.github/workflows/e2e-headed.yml +37 -0
  4. package/README.md +19 -0
  5. package/TESTING.md +233 -0
  6. package/dist/bilibili.js +2 -2
  7. package/dist/browser.d.ts +1 -1
  8. package/dist/browser.js +12 -3
  9. package/dist/browser.test.js +56 -16
  10. package/dist/interceptor.test.d.ts +4 -0
  11. package/dist/interceptor.test.js +81 -0
  12. package/dist/output.test.d.ts +3 -0
  13. package/dist/output.test.js +60 -0
  14. package/dist/pipeline/executor.js +0 -6
  15. package/dist/pipeline/executor.test.d.ts +4 -0
  16. package/dist/pipeline/executor.test.js +145 -0
  17. package/dist/pipeline/steps/fetch.js +4 -3
  18. package/dist/registry.d.ts +2 -2
  19. package/package.json +4 -4
  20. package/src/bilibili.ts +2 -2
  21. package/src/browser.test.ts +54 -16
  22. package/src/browser.ts +11 -3
  23. package/src/clis/twitter/notifications.ts +1 -1
  24. package/src/engine.ts +2 -2
  25. package/src/interceptor.test.ts +94 -0
  26. package/src/output.test.ts +69 -4
  27. package/src/pipeline/executor.test.ts +161 -0
  28. package/src/pipeline/executor.ts +0 -5
  29. package/src/pipeline/steps/fetch.ts +4 -3
  30. package/src/registry.ts +2 -2
  31. package/tests/e2e/browser-auth.test.ts +90 -0
  32. package/tests/e2e/browser-public.test.ts +169 -0
  33. package/tests/e2e/helpers.ts +63 -0
  34. package/tests/e2e/management.test.ts +106 -0
  35. package/tests/e2e/output-formats.test.ts +48 -0
  36. package/tests/e2e/public-commands.test.ts +56 -0
  37. package/tests/smoke/api-health.test.ts +72 -0
  38. package/tsconfig.json +1 -0
  39. package/vitest.config.ts +1 -1
@@ -0,0 +1,48 @@
1
+ /**
2
+ * E2E tests for output format rendering.
3
+ * Uses hackernews (public, fast) as a stable data source.
4
+ */
5
+
6
+ import { describe, it, expect } from 'vitest';
7
+ import { runCli, parseJsonOutput } from './helpers.js';
8
+
9
+ const FORMATS = ['json', 'yaml', 'csv', 'md'] as const;
10
+
11
+ describe('output formats E2E', () => {
12
+ for (const fmt of FORMATS) {
13
+ it(`hackernews top -f ${fmt} produces valid output`, async () => {
14
+ const { stdout, code } = await runCli(['hackernews', 'top', '--limit', '2', '-f', fmt]);
15
+ expect(code).toBe(0);
16
+ expect(stdout.trim().length).toBeGreaterThan(0);
17
+
18
+ if (fmt === 'json') {
19
+ const data = parseJsonOutput(stdout);
20
+ expect(Array.isArray(data)).toBe(true);
21
+ expect(data.length).toBe(2);
22
+ }
23
+
24
+ if (fmt === 'yaml') {
25
+ expect(stdout).toContain('title:');
26
+ }
27
+
28
+ if (fmt === 'csv') {
29
+ // CSV should have a header row + data rows
30
+ const lines = stdout.trim().split('\n');
31
+ expect(lines.length).toBeGreaterThanOrEqual(2);
32
+ }
33
+
34
+ if (fmt === 'md') {
35
+ // Markdown table should have pipe characters
36
+ expect(stdout).toContain('|');
37
+ }
38
+ }, 30_000);
39
+ }
40
+
41
+ it('list -f csv produces valid csv', async () => {
42
+ const { stdout, code } = await runCli(['list', '-f', 'csv']);
43
+ expect(code).toBe(0);
44
+ const lines = stdout.trim().split('\n');
45
+ // Header + many data lines
46
+ expect(lines.length).toBeGreaterThan(50);
47
+ });
48
+ });
@@ -0,0 +1,56 @@
1
+ /**
2
+ * E2E tests for public API commands (browser: false).
3
+ * These commands use Node.js fetch directly — no browser needed.
4
+ */
5
+
6
+ import { describe, it, expect } from 'vitest';
7
+ import { runCli, parseJsonOutput } from './helpers.js';
8
+
9
+ describe('public commands E2E', () => {
10
+ // ── hackernews ──
11
+ it('hackernews top returns structured data', async () => {
12
+ const { stdout, code } = await runCli(['hackernews', 'top', '--limit', '3', '-f', 'json']);
13
+ expect(code).toBe(0);
14
+ const data = parseJsonOutput(stdout);
15
+ expect(Array.isArray(data)).toBe(true);
16
+ expect(data.length).toBe(3);
17
+ expect(data[0]).toHaveProperty('title');
18
+ expect(data[0]).toHaveProperty('score');
19
+ expect(data[0]).toHaveProperty('rank');
20
+ }, 30_000);
21
+
22
+ it('hackernews top respects --limit', async () => {
23
+ const { stdout, code } = await runCli(['hackernews', 'top', '--limit', '1', '-f', 'json']);
24
+ expect(code).toBe(0);
25
+ const data = parseJsonOutput(stdout);
26
+ expect(data.length).toBe(1);
27
+ }, 30_000);
28
+
29
+ // ── v2ex (public API, browser: false) ──
30
+ it('v2ex hot returns topics', async () => {
31
+ const { stdout, code } = await runCli(['v2ex', 'hot', '--limit', '3', '-f', 'json']);
32
+ expect(code).toBe(0);
33
+ const data = parseJsonOutput(stdout);
34
+ expect(Array.isArray(data)).toBe(true);
35
+ expect(data.length).toBeGreaterThanOrEqual(1);
36
+ expect(data[0]).toHaveProperty('title');
37
+ }, 30_000);
38
+
39
+ it('v2ex latest returns topics', async () => {
40
+ const { stdout, code } = await runCli(['v2ex', 'latest', '--limit', '3', '-f', 'json']);
41
+ expect(code).toBe(0);
42
+ const data = parseJsonOutput(stdout);
43
+ expect(Array.isArray(data)).toBe(true);
44
+ expect(data.length).toBeGreaterThanOrEqual(1);
45
+ }, 30_000);
46
+
47
+ it('v2ex topic returns topic detail', async () => {
48
+ // Topic 1000001 is a well-known V2EX topic
49
+ const { stdout, code } = await runCli(['v2ex', 'topic', '--id', '1000001', '-f', 'json']);
50
+ // May fail if V2EX rate-limits, but should return structured data
51
+ if (code === 0) {
52
+ const data = parseJsonOutput(stdout);
53
+ expect(data).toBeDefined();
54
+ }
55
+ }, 30_000);
56
+ });
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Smoke tests for external API health.
3
+ * Only run on schedule or manual dispatch — NOT on every push/PR.
4
+ * These verify that external APIs haven't changed their structure.
5
+ */
6
+
7
+ import { describe, it, expect } from 'vitest';
8
+ import { runCli, parseJsonOutput } from '../e2e/helpers.js';
9
+
10
+ describe('API health smoke tests', () => {
11
+
12
+ // ── Public API commands (should always work) ──
13
+ it('hackernews API is responsive and returns expected structure', async () => {
14
+ const { stdout, code } = await runCli(['hackernews', 'top', '--limit', '5', '-f', 'json']);
15
+ expect(code).toBe(0);
16
+ const data = parseJsonOutput(stdout);
17
+ expect(data.length).toBe(5);
18
+ for (const item of data) {
19
+ expect(item).toHaveProperty('title');
20
+ expect(item).toHaveProperty('score');
21
+ expect(item).toHaveProperty('author');
22
+ expect(item).toHaveProperty('rank');
23
+ }
24
+ }, 30_000);
25
+
26
+ it('v2ex hot API is responsive', async () => {
27
+ const { stdout, code } = await runCli(['v2ex', 'hot', '--limit', '3', '-f', 'json']);
28
+ expect(code).toBe(0);
29
+ const data = parseJsonOutput(stdout);
30
+ expect(data.length).toBeGreaterThanOrEqual(1);
31
+ expect(data[0]).toHaveProperty('title');
32
+ }, 30_000);
33
+
34
+ it('v2ex latest API is responsive', async () => {
35
+ const { stdout, code } = await runCli(['v2ex', 'latest', '--limit', '3', '-f', 'json']);
36
+ expect(code).toBe(0);
37
+ const data = parseJsonOutput(stdout);
38
+ expect(data.length).toBeGreaterThanOrEqual(1);
39
+ }, 30_000);
40
+
41
+ it('v2ex topic API is responsive', async () => {
42
+ const { stdout, code } = await runCli(['v2ex', 'topic', '--id', '1000001', '-f', 'json']);
43
+ if (code === 0) {
44
+ const data = parseJsonOutput(stdout);
45
+ expect(data).toBeDefined();
46
+ }
47
+ }, 30_000);
48
+
49
+ // ── Validate all adapters ──
50
+ it('all adapter definitions are valid', async () => {
51
+ const { stdout, code } = await runCli(['validate']);
52
+ expect(code).toBe(0);
53
+ expect(stdout).toContain('PASS');
54
+ });
55
+
56
+ // ── Command registry integrity ──
57
+ it('all expected sites are registered', async () => {
58
+ const { stdout, code } = await runCli(['list', '-f', 'json']);
59
+ expect(code).toBe(0);
60
+ const data = parseJsonOutput(stdout);
61
+ const sites = new Set(data.map((d: any) => d.site));
62
+ // Verify all 17 sites are present
63
+ for (const expected of [
64
+ 'hackernews', 'bbc', 'bilibili', 'v2ex', 'weibo', 'zhihu',
65
+ 'twitter', 'reddit', 'xueqiu', 'reuters', 'youtube',
66
+ 'smzdm', 'boss', 'ctrip', 'coupang', 'xiaohongshu',
67
+ 'yahoo-finance',
68
+ ]) {
69
+ expect(sites.has(expected)).toBe(true);
70
+ }
71
+ });
72
+ });
package/tsconfig.json CHANGED
@@ -6,6 +6,7 @@
6
6
  "outDir": "dist",
7
7
  "rootDir": "src",
8
8
  "strict": false,
9
+ "strictNullChecks": true,
9
10
  "esModuleInterop": true,
10
11
  "skipLibCheck": true,
11
12
  "forceConsistentCasingInFileNames": true,
package/vitest.config.ts CHANGED
@@ -2,6 +2,6 @@ import { defineConfig } from 'vitest/config';
2
2
 
3
3
  export default defineConfig({
4
4
  test: {
5
- include: ['src/**/*.test.ts'],
5
+ include: ['src/**/*.test.ts', 'tests/**/*.test.ts'],
6
6
  },
7
7
  });