@jackwener/opencli 1.7.22 → 1.8.1
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.
- package/README.md +35 -194
- package/README.zh-CN.md +42 -260
- package/cli-manifest.json +8160 -4392
- package/clis/12306/me.js +73 -0
- package/clis/12306/orders.js +96 -0
- package/clis/12306/passengers.js +90 -0
- package/clis/12306/price.js +166 -0
- package/clis/12306/stations.js +66 -0
- package/clis/12306/train.js +91 -0
- package/clis/12306/trains.js +119 -0
- package/clis/12306/utils.js +272 -0
- package/clis/12306/utils.test.js +331 -0
- package/clis/36kr/article.js +6 -3
- package/clis/36kr/article.test.js +46 -0
- package/clis/_atlassian/shared.js +577 -0
- package/clis/_atlassian/shared.test.js +170 -0
- package/clis/apple-podcasts/commands.test.js +20 -0
- package/clis/apple-podcasts/search.js +2 -2
- package/clis/barchart/greeks.js +144 -56
- package/clis/barchart/greeks.test.js +138 -0
- package/clis/bilibili/comment.js +125 -0
- package/clis/bilibili/comment.test.js +153 -0
- package/clis/bilibili/comments.js +116 -21
- package/clis/bilibili/comments.test.js +77 -18
- package/clis/bilibili/subtitle.js +76 -31
- package/clis/bilibili/subtitle.test.js +156 -9
- package/clis/bilibili/summary.js +167 -0
- package/clis/bilibili/summary.test.js +210 -0
- package/clis/bilibili/utils.js +63 -5
- package/clis/bilibili/utils.test.js +45 -1
- package/clis/booking/booking.test.js +356 -0
- package/clis/booking/search.js +351 -0
- package/clis/chatgpt/envelope.test.js +108 -0
- package/clis/chatgpt/image.js +2 -2
- package/clis/chatgpt/image.test.js +6 -0
- package/clis/chatgpt/utils.js +148 -41
- package/clis/chatgpt/utils.test.js +92 -2
- package/clis/chess/analyze.js +35 -0
- package/clis/chess/analyze.test.js +79 -0
- package/clis/chess/game.js +114 -0
- package/clis/chess/game.test.js +178 -0
- package/clis/chess/games.js +67 -0
- package/clis/chess/games.test.js +164 -0
- package/clis/chess/stats.js +32 -0
- package/clis/chess/stats.test.js +79 -0
- package/clis/chess/utils.js +170 -0
- package/clis/chess/utils.test.js +230 -0
- package/clis/confluence/commands.test.js +195 -0
- package/clis/confluence/create.js +39 -0
- package/clis/confluence/page.js +23 -0
- package/clis/confluence/search.js +34 -0
- package/clis/confluence/shared.js +173 -0
- package/clis/confluence/update.js +38 -0
- package/clis/douyin/_shared/browser-fetch.js +44 -20
- package/clis/douyin/_shared/browser-fetch.test.js +22 -1
- package/clis/douyin/_shared/evaluate-result.js +16 -0
- package/clis/douyin/_shared/tos-upload.js +105 -69
- package/clis/douyin/_shared/vod-upload.js +212 -0
- package/clis/douyin/_shared/vod-upload.test.js +38 -0
- package/clis/douyin/delete.js +137 -4
- package/clis/douyin/delete.test.js +90 -1
- package/clis/douyin/hashtag.js +84 -23
- package/clis/douyin/hashtag.test.js +113 -0
- package/clis/douyin/publish-upload-id.test.js +170 -0
- package/clis/douyin/publish.js +88 -42
- package/clis/douyin/user-videos.js +9 -2
- package/clis/douyin/user-videos.test.js +43 -0
- package/clis/flomo/memos.js +228 -0
- package/clis/flomo/memos.test.js +144 -0
- package/clis/geogebra/add-circle.js +46 -0
- package/clis/geogebra/add-line.js +35 -0
- package/clis/geogebra/add-point.js +27 -0
- package/clis/geogebra/add-polygon.js +25 -0
- package/clis/geogebra/eval.js +35 -0
- package/clis/geogebra/geogebra.test.js +175 -0
- package/clis/geogebra/hexagon.js +62 -0
- package/clis/geogebra/info.js +72 -0
- package/clis/geogebra/list.js +35 -0
- package/clis/geogebra/triangle.js +60 -0
- package/clis/geogebra/utils.js +271 -0
- package/clis/gitee/search.js +2 -2
- package/clis/gitee/search.test.js +65 -0
- package/clis/jike/post.js +27 -17
- package/clis/jike/read.test.js +86 -0
- package/clis/jike/topic.js +32 -19
- package/clis/jike/user.js +33 -20
- package/clis/jira/attachments.js +28 -0
- package/clis/jira/commands.test.js +287 -0
- package/clis/jira/comments.js +28 -0
- package/clis/jira/issue.js +28 -0
- package/clis/jira/links.js +28 -0
- package/clis/jira/search.js +47 -0
- package/clis/jira/shared.js +256 -0
- package/clis/lesswrong/comments.js +1 -1
- package/clis/lesswrong/curated.js +1 -1
- package/clis/lesswrong/frontpage.js +1 -1
- package/clis/lesswrong/frontpage.test.js +37 -0
- package/clis/lesswrong/new.js +1 -1
- package/clis/lesswrong/read.js +1 -1
- package/clis/lesswrong/sequences.js +1 -1
- package/clis/lesswrong/shortform.js +1 -1
- package/clis/lesswrong/tag.js +1 -1
- package/clis/lesswrong/top-month.js +1 -1
- package/clis/lesswrong/top-week.js +1 -1
- package/clis/lesswrong/top-year.js +1 -1
- package/clis/lesswrong/top.js +1 -1
- package/clis/linkedin/connect.js +401 -0
- package/clis/linkedin/connect.test.js +213 -0
- package/clis/linkedin/inbox.js +234 -0
- package/clis/linkedin/inbox.test.js +152 -0
- package/clis/linkedin/job-detail.js +167 -0
- package/clis/linkedin/job-detail.test.js +38 -0
- package/clis/linkedin/jobs-preferences.js +113 -0
- package/clis/linkedin/jobs-preferences.test.js +43 -0
- package/clis/linkedin/people-search.js +262 -0
- package/clis/linkedin/people-search.test.js +216 -0
- package/clis/linkedin/post-analytics.js +74 -0
- package/clis/linkedin/post-analytics.test.js +40 -0
- package/clis/linkedin/posts-core.js +241 -0
- package/clis/linkedin/posts.js +22 -0
- package/clis/linkedin/posts.test.js +40 -0
- package/clis/linkedin/profile-analytics.js +104 -0
- package/clis/linkedin/profile-analytics.test.js +67 -0
- package/clis/linkedin/profile-experience.js +671 -0
- package/clis/linkedin/profile-experience.test.js +152 -0
- package/clis/linkedin/profile-projects.js +311 -0
- package/clis/linkedin/profile-projects.test.js +111 -0
- package/clis/linkedin/profile-read.js +148 -0
- package/clis/linkedin/profile-read.test.js +77 -0
- package/clis/linkedin/safe-send.js +357 -0
- package/clis/linkedin/safe-send.test.js +204 -0
- package/clis/linkedin/salesnav-inbox.js +210 -0
- package/clis/linkedin/salesnav-inbox.test.js +113 -0
- package/clis/linkedin/salesnav-message.js +360 -0
- package/clis/linkedin/salesnav-message.test.js +172 -0
- package/clis/linkedin/salesnav-search.js +186 -0
- package/clis/linkedin/salesnav-search.test.js +76 -0
- package/clis/linkedin/salesnav-thread.js +212 -0
- package/clis/linkedin/salesnav-thread.test.js +79 -0
- package/clis/linkedin/sent-invitations.js +92 -0
- package/clis/linkedin/sent-invitations.test.js +62 -0
- package/clis/linkedin/services-read.js +213 -0
- package/clis/linkedin/services-read.test.js +105 -0
- package/clis/linkedin/shared.js +124 -0
- package/clis/linkedin/thread-snapshot.js +214 -0
- package/clis/linkedin/thread-snapshot.test.js +89 -0
- package/clis/linkedin/timeline.js +14 -7
- package/clis/linkedin-learning/course.js +138 -0
- package/clis/linkedin-learning/course.test.js +114 -0
- package/clis/linkedin-learning/search.js +155 -0
- package/clis/linkedin-learning/search.test.js +144 -0
- package/clis/linkedin-learning/trending.js +133 -0
- package/clis/linkedin-learning/trending.test.js +123 -0
- package/clis/notebooklm/add-source.js +269 -0
- package/clis/notebooklm/add-source.test.js +97 -0
- package/clis/notebooklm/create.js +76 -0
- package/clis/notebooklm/create.test.js +58 -0
- package/clis/notebooklm/generate-audio.js +91 -0
- package/clis/notebooklm/generate-audio.test.js +63 -0
- package/clis/notebooklm/generate-slides.js +106 -0
- package/clis/notebooklm/generate-slides.test.js +75 -0
- package/clis/notebooklm/open.test.js +10 -10
- package/clis/notebooklm/rpc.js +20 -6
- package/clis/notebooklm/rpc.test.js +27 -1
- package/clis/notebooklm/utils.js +100 -24
- package/clis/notebooklm/utils.test.js +60 -1
- package/clis/notebooklm/write-note.js +103 -0
- package/clis/notebooklm/write-note.test.js +70 -0
- package/clis/pixiv/detail.js +41 -34
- package/clis/pixiv/detail.test.js +93 -0
- package/clis/pixiv/user.js +36 -31
- package/clis/pixiv/user.test.js +100 -0
- package/clis/pixiv/utils.js +56 -7
- package/clis/powerchina/search.js +3 -3
- package/clis/powerchina/search.test.js +27 -1
- package/clis/reddit/extract-media.test.js +149 -0
- package/clis/reddit/frontpage.js +47 -9
- package/clis/reddit/frontpage.test.js +34 -0
- package/clis/reddit/home.js +31 -1
- package/clis/reddit/home.test.js +46 -3
- package/clis/reddit/hot.js +32 -1
- package/clis/reddit/hot.test.js +15 -1
- package/clis/reddit/popular.js +39 -1
- package/clis/reddit/popular.test.js +26 -0
- package/clis/reddit/saved.js +1 -1
- package/clis/reddit/search.js +38 -1
- package/clis/reddit/search.test.js +26 -0
- package/clis/reddit/subreddit.js +52 -7
- package/clis/reddit/subreddit.test.js +31 -0
- package/clis/reddit/subscribed.js +165 -0
- package/clis/reddit/subscribed.test.js +168 -0
- package/clis/reddit/upvoted.js +1 -1
- package/clis/suno/commands.test.js +188 -0
- package/clis/suno/download.js +140 -0
- package/clis/suno/download.test.js +151 -0
- package/clis/suno/generate.js +231 -0
- package/clis/suno/generate.test.js +252 -0
- package/clis/suno/list.js +79 -0
- package/clis/suno/status.js +63 -0
- package/clis/suno/utils.js +549 -0
- package/clis/suno/utils.test.js +329 -0
- package/clis/twitter/device-follow.js +193 -0
- package/clis/twitter/device-follow.test.js +287 -0
- package/clis/twitter/download.js +443 -73
- package/clis/twitter/download.test.js +457 -0
- package/clis/twitter/followers.js +6 -2
- package/clis/twitter/followers.test.js +19 -1
- package/clis/twitter/following.js +14 -5
- package/clis/twitter/following.test.js +29 -0
- package/clis/twitter/likes.js +12 -4
- package/clis/twitter/likes.test.js +26 -1
- package/clis/twitter/list-add.js +1 -1
- package/clis/twitter/list-create.js +155 -0
- package/clis/twitter/list-create.test.js +169 -0
- package/clis/twitter/list-remove.js +13 -6
- package/clis/twitter/list-remove.test.js +74 -0
- package/clis/twitter/list-tweets.js +6 -2
- package/clis/twitter/list-tweets.test.js +41 -1
- package/clis/twitter/lists.js +31 -4
- package/clis/twitter/lists.test.js +152 -16
- package/clis/twitter/notifications.js +4 -4
- package/clis/twitter/post.js +62 -4
- package/clis/twitter/post.test.js +35 -3
- package/clis/twitter/profile.js +81 -28
- package/clis/twitter/profile.test.js +113 -2
- package/clis/twitter/quote.js +9 -4
- package/clis/twitter/reply.js +13 -10
- package/clis/twitter/reply.test.js +41 -0
- package/clis/twitter/search.js +7 -3
- package/clis/twitter/search.test.js +41 -0
- package/clis/twitter/shared.js +155 -0
- package/clis/twitter/shared.test.js +465 -1
- package/clis/twitter/thread.js +10 -2
- package/clis/twitter/thread.test.js +58 -0
- package/clis/twitter/timeline.js +6 -2
- package/clis/twitter/timeline.test.js +2 -0
- package/clis/twitter/tweets.js +3 -2
- package/clis/twitter/tweets.test.js +1 -1
- package/clis/twitter/utils.js +53 -16
- package/clis/upwork/detail.js +132 -0
- package/clis/upwork/feed.js +109 -0
- package/clis/upwork/search.js +115 -0
- package/clis/upwork/upwork.test.js +566 -0
- package/clis/upwork/utils.js +323 -0
- package/clis/weibo/delete.js +172 -0
- package/clis/weibo/delete.test.js +94 -0
- package/clis/weibo/publish.js +37 -14
- package/clis/weibo/publish.test.js +14 -5
- package/clis/weibo/user-posts.js +234 -0
- package/clis/weibo/user-posts.test.js +92 -0
- package/clis/weread/book-search.js +438 -0
- package/clis/weread/book-search.test.js +242 -0
- package/clis/weread/search-regression.test.js +98 -11
- package/clis/weread/search.js +32 -9
- package/clis/weread-official/book.js +135 -0
- package/clis/weread-official/commands.test.js +385 -0
- package/clis/weread-official/discover.js +107 -0
- package/clis/weread-official/list-apis.js +95 -0
- package/clis/weread-official/notes.js +171 -0
- package/clis/weread-official/readdata.js +158 -0
- package/clis/weread-official/review.js +93 -0
- package/clis/weread-official/search.js +106 -0
- package/clis/weread-official/shelf.js +97 -0
- package/clis/weread-official/utils.js +293 -0
- package/clis/weread-official/utils.test.js +242 -0
- package/clis/wikipedia/trending.js +7 -3
- package/clis/wikipedia/trending.test.js +57 -0
- package/clis/xianyu/chat.js +24 -109
- package/clis/xianyu/chat.test.js +5 -0
- package/clis/xianyu/im.js +322 -0
- package/clis/xianyu/im.test.js +253 -0
- package/clis/xianyu/inbox.js +96 -0
- package/clis/xianyu/messages.js +91 -0
- package/clis/xianyu/reply.js +82 -0
- package/clis/xiaohongshu/creator-note-detail.js +166 -28
- package/clis/xiaohongshu/creator-note-detail.test.js +196 -36
- package/clis/xiaohongshu/creator-notes-summary.js +2 -1
- package/clis/xiaohongshu/creator-notes-summary.test.js +7 -0
- package/clis/xiaohongshu/creator-notes.js +252 -2
- package/clis/xiaohongshu/creator-notes.test.js +90 -1
- package/clis/xiaohongshu/creator-stats.js +2 -1
- package/clis/xiaohongshu/creator-stats.test.js +24 -0
- package/clis/xiaohongshu/delete-note.js +260 -0
- package/clis/xiaohongshu/delete-note.test.js +172 -0
- package/clis/xiaohongshu/download.js +97 -39
- package/clis/xiaohongshu/download.test.js +201 -0
- package/clis/xiaohongshu/publish.js +48 -8
- package/clis/xiaohongshu/publish.test.js +65 -10
- package/clis/xiaohongshu/user-helpers.test.js +41 -0
- package/clis/xiaohongshu/user.js +27 -4
- package/clis/xiaoyuzhou/download.js +1 -1
- package/clis/xiaoyuzhou/transcript.js +1 -1
- package/clis/youdao/note.js +258 -0
- package/clis/youdao/note.test.js +99 -0
- package/clis/youtube/transcript.js +397 -24
- package/clis/youtube/transcript.test.js +196 -6
- package/clis/zhihu/answer-comments.js +280 -0
- package/clis/zhihu/answer-comments.test.js +287 -0
- package/clis/zhihu/answer-detail.js +2 -19
- package/clis/zhihu/answer-detail.test.js +8 -0
- package/clis/zhihu/collection.js +17 -16
- package/clis/zhihu/collection.test.js +50 -3
- package/clis/zhihu/download.js +1 -1
- package/clis/zhihu/question.js +42 -17
- package/clis/zhihu/question.test.js +113 -11
- package/clis/zhihu/search.js +195 -43
- package/clis/zhihu/search.test.js +198 -0
- package/clis/zhihu/text.js +29 -0
- package/clis/zhihu/text.test.js +24 -0
- package/dist/src/browser/errors.js +4 -2
- package/dist/src/browser/errors.test.js +6 -0
- package/dist/src/browser/network-cache.js +13 -1
- package/dist/src/browser/network-cache.test.js +17 -0
- package/dist/src/browser/page.js +30 -4
- package/dist/src/browser/page.test.js +42 -0
- package/dist/src/browser/utils.d.ts +1 -1
- package/dist/src/cli-argv-preprocess.d.ts +26 -0
- package/dist/src/cli-argv-preprocess.js +138 -0
- package/dist/src/cli-argv-preprocess.test.js +79 -0
- package/dist/src/convention-audit.js +15 -8
- package/dist/src/convention-audit.test.js +21 -0
- package/dist/src/download/index.js +13 -1
- package/dist/src/download/index.test.js +23 -1
- package/dist/src/download/media-download.js +15 -2
- package/dist/src/download/media-download.test.d.ts +1 -0
- package/dist/src/download/media-download.test.js +112 -0
- package/dist/src/download/progress.js +2 -2
- package/dist/src/download/progress.test.js +12 -1
- package/dist/src/electron-apps.js +1 -1
- package/dist/src/electron-apps.test.js +7 -2
- package/dist/src/errors.d.ts +17 -0
- package/dist/src/errors.js +22 -0
- package/dist/src/external-clis.yaml +8 -0
- package/dist/src/main.js +14 -2
- package/dist/src/output.js +11 -1
- package/dist/src/output.test.js +6 -0
- package/dist/src/registry.js +1 -0
- package/dist/src/registry.test.js +11 -0
- package/dist/src/utils.d.ts +43 -0
- package/dist/src/utils.js +97 -0
- package/dist/src/utils.test.d.ts +1 -0
- package/dist/src/utils.test.js +155 -0
- package/package.json +8 -2
- package/scripts/silent-column-drop-baseline.json +0 -52
- package/scripts/typed-error-lint-baseline.json +28 -380
- package/clis/slock/_utils.js +0 -12
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { describe, expect, it, afterEach, vi } from 'vitest';
|
|
2
|
+
import { __test__ } from './shared.js';
|
|
3
|
+
import { CommandExecutionError } from '@jackwener/opencli/errors';
|
|
4
|
+
|
|
5
|
+
const ENV_KEYS = [
|
|
6
|
+
'ATLASSIAN_CONFLUENCE_BASE_URL',
|
|
7
|
+
'ATLASSIAN_DEPLOYMENT',
|
|
8
|
+
'ATLASSIAN_EMAIL',
|
|
9
|
+
'ATLASSIAN_API_TOKEN',
|
|
10
|
+
'ATLASSIAN_PAT',
|
|
11
|
+
'ATLASSIAN_JIRA_BASE_URL',
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
function clearEnv() {
|
|
15
|
+
for (const key of ENV_KEYS) delete process.env[key];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
clearEnv();
|
|
20
|
+
vi.unstubAllGlobals();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('atlassian shared helpers', () => {
|
|
24
|
+
it('infers Confluence Cloud and appends /wiki', () => {
|
|
25
|
+
clearEnv();
|
|
26
|
+
process.env.ATLASSIAN_CONFLUENCE_BASE_URL = 'https://example.atlassian.net';
|
|
27
|
+
process.env.ATLASSIAN_EMAIL = 'bot@example.com';
|
|
28
|
+
process.env.ATLASSIAN_API_TOKEN = 'secret';
|
|
29
|
+
const config = __test__.getConfluenceConfig();
|
|
30
|
+
expect(config.deployment).toBe('cloud');
|
|
31
|
+
expect(config.baseUrl).toBe('https://example.atlassian.net/wiki');
|
|
32
|
+
expect(config.authHeaders.Authorization).toMatch(/^Basic /);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('uses Data Center PAT as bearer auth', () => {
|
|
36
|
+
clearEnv();
|
|
37
|
+
process.env.ATLASSIAN_JIRA_BASE_URL = 'https://jira.example.com';
|
|
38
|
+
process.env.ATLASSIAN_DEPLOYMENT = 'datacenter';
|
|
39
|
+
process.env.ATLASSIAN_PAT = 'pat-123';
|
|
40
|
+
const config = __test__.getJiraConfig();
|
|
41
|
+
expect(config.deployment).toBe('datacenter');
|
|
42
|
+
expect(config.authHeaders.Authorization).toBe('Bearer pat-123');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('converts Jira ADF to Markdown', () => {
|
|
46
|
+
const markdown = __test__.adfToMarkdown({
|
|
47
|
+
type: 'doc',
|
|
48
|
+
content: [
|
|
49
|
+
{
|
|
50
|
+
type: 'paragraph',
|
|
51
|
+
content: [
|
|
52
|
+
{ type: 'text', text: 'Broken ', marks: [{ type: 'strong' }] },
|
|
53
|
+
{ type: 'text', text: 'checkout', marks: [{ type: 'link', attrs: { href: 'https://example.com' } }] },
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
type: 'bulletList',
|
|
58
|
+
content: [{ type: 'listItem', content: [{ type: 'paragraph', content: [{ type: 'text', text: 'retry payment' }] }] }],
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
});
|
|
62
|
+
expect(markdown).toContain('**Broken **');
|
|
63
|
+
expect(markdown).toContain('[checkout](https://example.com)');
|
|
64
|
+
expect(markdown).toContain('- retry payment');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('escapes pipe characters inside ADF table cells', () => {
|
|
68
|
+
const markdown = __test__.adfToMarkdown({
|
|
69
|
+
type: 'doc',
|
|
70
|
+
content: [{
|
|
71
|
+
type: 'table',
|
|
72
|
+
content: [
|
|
73
|
+
{
|
|
74
|
+
type: 'tableRow',
|
|
75
|
+
content: [
|
|
76
|
+
{ type: 'tableHeader', content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Service' }] }] },
|
|
77
|
+
{ type: 'tableHeader', content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Notes' }] }] },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
type: 'tableRow',
|
|
82
|
+
content: [
|
|
83
|
+
{ type: 'tableCell', content: [{ type: 'paragraph', content: [{ type: 'text', text: 'payments' }] }] },
|
|
84
|
+
{ type: 'tableCell', content: [{ type: 'paragraph', content: [{ type: 'text', text: 'a | b' }] }] },
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
}],
|
|
89
|
+
});
|
|
90
|
+
expect(markdown).toContain('Service | Notes');
|
|
91
|
+
expect(markdown).toContain('--- | ---');
|
|
92
|
+
expect(markdown).toContain('payments | a \\| b');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('converts nested HTML to Markdown through the shared Turndown converter', () => {
|
|
96
|
+
const markdown = __test__.htmlToMarkdown('<ul><li><strong>Root</strong><ul><li>Child</li></ul></li></ul><table><tr><th>A</th></tr><tr><td>B</td></tr></table>');
|
|
97
|
+
expect(markdown).toContain('**Root**');
|
|
98
|
+
expect(markdown).toContain('Child');
|
|
99
|
+
expect(markdown).toContain('A');
|
|
100
|
+
expect(markdown).toContain('B');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('converts Markdown to conservative Confluence storage XHTML', () => {
|
|
104
|
+
const storage = __test__.markdownToConfluenceStorage([
|
|
105
|
+
'# RCA',
|
|
106
|
+
'',
|
|
107
|
+
'- Impacted checkout',
|
|
108
|
+
'',
|
|
109
|
+
'| Service | Status |',
|
|
110
|
+
'| --- | --- |',
|
|
111
|
+
'| payments | fixed |',
|
|
112
|
+
].join('\n'));
|
|
113
|
+
expect(storage).toContain('<h1>RCA</h1>');
|
|
114
|
+
expect(storage).toContain('<ul>');
|
|
115
|
+
expect(storage).toContain('<table>');
|
|
116
|
+
expect(storage).toContain('<td>fixed</td>');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('preserves nested Markdown lists in Confluence storage XHTML', () => {
|
|
120
|
+
const storage = __test__.markdownToConfluenceStorage([
|
|
121
|
+
'- Parent',
|
|
122
|
+
' - Child',
|
|
123
|
+
'- Next',
|
|
124
|
+
].join('\n'));
|
|
125
|
+
const compact = storage.replace(/\s*\n\s*/g, '');
|
|
126
|
+
expect(compact).toContain('<ul><li>Parent<ul><li>Child</li></ul></li><li>Next</li></ul>');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('sends JSON requests with configured auth headers', async () => {
|
|
130
|
+
const fetchMock = vi.fn(async () => new Response(JSON.stringify({ ok: true }), { status: 200 }));
|
|
131
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
132
|
+
const data = await __test__.atlassianRequest({
|
|
133
|
+
product: 'jira',
|
|
134
|
+
baseUrl: 'https://jira.example.com',
|
|
135
|
+
deployment: 'datacenter',
|
|
136
|
+
authHeaders: { Authorization: 'Bearer token' },
|
|
137
|
+
}, '/rest/api/2/myself', { label: 'jira myself' });
|
|
138
|
+
expect(data).toEqual({ ok: true });
|
|
139
|
+
expect(fetchMock.mock.calls[0][0]).toBe('https://jira.example.com/rest/api/2/myself');
|
|
140
|
+
expect(fetchMock.mock.calls[0][1].headers.Authorization).toBe('Bearer token');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('maps auth and rate-limit responses to typed errors', async () => {
|
|
144
|
+
vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ message: 'bad token' }), { status: 401 })));
|
|
145
|
+
await expect(__test__.atlassianRequest({
|
|
146
|
+
product: 'jira',
|
|
147
|
+
baseUrl: 'https://jira.example.com',
|
|
148
|
+
deployment: 'datacenter',
|
|
149
|
+
authHeaders: { Authorization: 'Bearer token' },
|
|
150
|
+
}, '/rest/api/2/myself', { label: 'jira myself' })).rejects.toMatchObject({ code: 'AUTH_REQUIRED' });
|
|
151
|
+
|
|
152
|
+
vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({ message: 'slow down' }), { status: 429 })));
|
|
153
|
+
await expect(__test__.atlassianRequest({
|
|
154
|
+
product: 'jira',
|
|
155
|
+
baseUrl: 'https://jira.example.com',
|
|
156
|
+
deployment: 'datacenter',
|
|
157
|
+
authHeaders: { Authorization: 'Bearer token' },
|
|
158
|
+
}, '/rest/api/2/myself', { label: 'jira myself' })).rejects.toMatchObject({ code: 'COMMAND_EXEC' });
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('fails typed when a successful Atlassian REST response is not JSON', async () => {
|
|
162
|
+
vi.stubGlobal('fetch', vi.fn(async () => new Response('<html>login</html>', { status: 200, headers: { 'content-type': 'text/html' } })));
|
|
163
|
+
await expect(__test__.atlassianRequest({
|
|
164
|
+
product: 'jira',
|
|
165
|
+
baseUrl: 'https://jira.example.com',
|
|
166
|
+
deployment: 'datacenter',
|
|
167
|
+
authHeaders: { Authorization: 'Bearer token' },
|
|
168
|
+
}, '/rest/api/2/myself', { label: 'jira myself' })).rejects.toBeInstanceOf(CommandExecutionError);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -41,6 +41,26 @@ describe('apple-podcasts search command', () => {
|
|
|
41
41
|
}),
|
|
42
42
|
]);
|
|
43
43
|
});
|
|
44
|
+
it('emits empty-string for missing trackCount and primaryGenreName instead of a sentinel', async () => {
|
|
45
|
+
const cmd = getRegistry().get('apple-podcasts/search');
|
|
46
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
47
|
+
ok: true,
|
|
48
|
+
json: () => Promise.resolve({
|
|
49
|
+
results: [
|
|
50
|
+
{
|
|
51
|
+
collectionId: 99,
|
|
52
|
+
collectionName: 'No-Meta Show',
|
|
53
|
+
artistName: 'Anon Host',
|
|
54
|
+
collectionViewUrl: 'https://example.com/p/99',
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
60
|
+
const result = await cmd.func({ query: 'no-meta', limit: 1 });
|
|
61
|
+
expect(result[0].episodes).toBe('');
|
|
62
|
+
expect(result[0].genre).toBe('');
|
|
63
|
+
});
|
|
44
64
|
});
|
|
45
65
|
describe('apple-podcasts top command', () => {
|
|
46
66
|
beforeEach(() => {
|
|
@@ -23,8 +23,8 @@ cli({
|
|
|
23
23
|
id: p.collectionId,
|
|
24
24
|
title: p.collectionName,
|
|
25
25
|
author: p.artistName,
|
|
26
|
-
episodes: p.trackCount ?? '
|
|
27
|
-
genre: p.primaryGenreName ?? '
|
|
26
|
+
episodes: p.trackCount ?? '',
|
|
27
|
+
genre: p.primaryGenreName ?? '',
|
|
28
28
|
url: p.collectionViewUrl || '',
|
|
29
29
|
}));
|
|
30
30
|
},
|
package/clis/barchart/greeks.js
CHANGED
|
@@ -4,6 +4,47 @@
|
|
|
4
4
|
* Auth: CSRF token from <meta name="csrf-token"> + session cookies.
|
|
5
5
|
*/
|
|
6
6
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
7
|
+
import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
8
|
+
|
|
9
|
+
const DEFAULT_LIMIT = 10;
|
|
10
|
+
const MIN_LIMIT = 1;
|
|
11
|
+
const MAX_LIMIT = 100;
|
|
12
|
+
|
|
13
|
+
function normalizeSymbol(value) {
|
|
14
|
+
const symbol = String(value ?? '').trim().toUpperCase();
|
|
15
|
+
if (!symbol) throw new ArgumentError('symbol is required');
|
|
16
|
+
return symbol;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normalizeExpiration(value) {
|
|
20
|
+
const expiration = String(value ?? '').trim();
|
|
21
|
+
if (!expiration) return '';
|
|
22
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(expiration)) {
|
|
23
|
+
throw new ArgumentError('--expiration must use YYYY-MM-DD format');
|
|
24
|
+
}
|
|
25
|
+
const parsed = new Date(`${expiration}T00:00:00Z`);
|
|
26
|
+
if (Number.isNaN(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== expiration) {
|
|
27
|
+
throw new ArgumentError('--expiration must be a valid calendar date');
|
|
28
|
+
}
|
|
29
|
+
return expiration;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseLimit(value) {
|
|
33
|
+
if (value === undefined || value === null || value === '') return DEFAULT_LIMIT;
|
|
34
|
+
const limit = Number(value);
|
|
35
|
+
if (!Number.isInteger(limit) || limit < MIN_LIMIT || limit > MAX_LIMIT) {
|
|
36
|
+
throw new ArgumentError(`--limit must be an integer between ${MIN_LIMIT} and ${MAX_LIMIT}`);
|
|
37
|
+
}
|
|
38
|
+
return limit;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function unwrapBrowserResult(value) {
|
|
42
|
+
if (value && typeof value === 'object' && 'session' in value && 'data' in value) {
|
|
43
|
+
return value.data;
|
|
44
|
+
}
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
|
|
7
48
|
cli({
|
|
8
49
|
site: 'barchart',
|
|
9
50
|
name: 'greeks',
|
|
@@ -14,19 +55,19 @@ cli({
|
|
|
14
55
|
args: [
|
|
15
56
|
{ name: 'symbol', required: true, positional: true, help: 'Stock ticker (e.g. AAPL)' },
|
|
16
57
|
{ name: 'expiration', type: 'str', help: 'Expiration date (YYYY-MM-DD). Defaults to the nearest available expiration.' },
|
|
17
|
-
{ name: 'limit', type: 'int', default:
|
|
58
|
+
{ name: 'limit', type: 'int', default: DEFAULT_LIMIT, help: 'Number of near-the-money strikes per type (1-100)' },
|
|
18
59
|
],
|
|
19
60
|
columns: [
|
|
20
61
|
'type', 'strike', 'last', 'iv', 'delta', 'gamma', 'theta', 'vega', 'rho',
|
|
21
62
|
'volume', 'openInterest', 'expiration',
|
|
22
63
|
],
|
|
23
64
|
func: async (page, kwargs) => {
|
|
24
|
-
const symbol = kwargs.symbol
|
|
25
|
-
const expiration = kwargs.expiration
|
|
26
|
-
const limit = kwargs.limit
|
|
65
|
+
const symbol = normalizeSymbol(kwargs.symbol);
|
|
66
|
+
const expiration = normalizeExpiration(kwargs.expiration);
|
|
67
|
+
const limit = parseLimit(kwargs.limit);
|
|
27
68
|
await page.goto(`https://www.barchart.com/stocks/quotes/${encodeURIComponent(symbol)}/options`);
|
|
28
69
|
await page.wait(4);
|
|
29
|
-
const data = await page.evaluate(`
|
|
70
|
+
const data = unwrapBrowserResult(await page.evaluate(`
|
|
30
71
|
(async () => {
|
|
31
72
|
const sym = ${JSON.stringify(symbol)};
|
|
32
73
|
const expDate = ${JSON.stringify(expiration)};
|
|
@@ -45,39 +86,53 @@ cli({
|
|
|
45
86
|
+ '&fields=' + fields + '&raw=1';
|
|
46
87
|
if (expDate) url += '&expirationDate=' + encodeURIComponent(expDate);
|
|
47
88
|
const resp = await fetch(url, { credentials: 'include', headers });
|
|
48
|
-
if (resp.ok) {
|
|
49
|
-
|
|
50
|
-
|
|
89
|
+
if (!resp.ok) {
|
|
90
|
+
return { ok: false, reason: 'http', status: resp.status, statusText: resp.statusText || '' };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const d = await resp.json();
|
|
94
|
+
const allItems = d?.data;
|
|
95
|
+
if (!Array.isArray(allItems)) {
|
|
96
|
+
return { ok: false, reason: 'malformed' };
|
|
97
|
+
}
|
|
98
|
+
let items = allItems;
|
|
51
99
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
100
|
+
if (!expDate) {
|
|
101
|
+
const expirations = items
|
|
102
|
+
.map(i => (i.raw || i).expirationDate || null)
|
|
103
|
+
.filter(Boolean)
|
|
104
|
+
.sort((a, b) => {
|
|
105
|
+
const aTime = Date.parse(a);
|
|
106
|
+
const bTime = Date.parse(b);
|
|
107
|
+
if (Number.isNaN(aTime) && Number.isNaN(bTime)) return 0;
|
|
108
|
+
if (Number.isNaN(aTime)) return 1;
|
|
109
|
+
if (Number.isNaN(bTime)) return -1;
|
|
110
|
+
return aTime - bTime;
|
|
111
|
+
});
|
|
112
|
+
const nearestExpiration = expirations[0];
|
|
113
|
+
if (nearestExpiration) {
|
|
114
|
+
items = items.filter(i => ((i.raw || i).expirationDate || null) === nearestExpiration);
|
|
68
115
|
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Separate calls and puts, sort by distance from current price.
|
|
119
|
+
const calls = items
|
|
120
|
+
.filter(i => ((i.raw || i).optionType || '').toLowerCase() === 'call')
|
|
121
|
+
.sort((a, b) => Math.abs((a.raw || a).percentFromLast || 999) - Math.abs((b.raw || b).percentFromLast || 999))
|
|
122
|
+
.slice(0, limit);
|
|
123
|
+
const puts = items
|
|
124
|
+
.filter(i => ((i.raw || i).optionType || '').toLowerCase() === 'put')
|
|
125
|
+
.sort((a, b) => Math.abs((a.raw || a).percentFromLast || 999) - Math.abs((b.raw || b).percentFromLast || 999))
|
|
126
|
+
.slice(0, limit);
|
|
127
|
+
const selected = [...calls, ...puts];
|
|
69
128
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
.sort((a, b) => Math.abs((a.raw || a).percentFromLast || 999) - Math.abs((b.raw || b).percentFromLast || 999))
|
|
74
|
-
.slice(0, limit);
|
|
75
|
-
const puts = items
|
|
76
|
-
.filter(i => ((i.raw || i).optionType || '').toLowerCase() === 'put')
|
|
77
|
-
.sort((a, b) => Math.abs((a.raw || a).percentFromLast || 999) - Math.abs((b.raw || b).percentFromLast || 999))
|
|
78
|
-
.slice(0, limit);
|
|
129
|
+
if (items.length > 0 && selected.length === 0) {
|
|
130
|
+
return { ok: false, reason: 'malformed', message: 'options rows did not include call or put identities' };
|
|
131
|
+
}
|
|
79
132
|
|
|
80
|
-
|
|
133
|
+
return {
|
|
134
|
+
ok: true,
|
|
135
|
+
rows: selected.map(i => {
|
|
81
136
|
const r = i.raw || i;
|
|
82
137
|
return {
|
|
83
138
|
type: r.optionType,
|
|
@@ -93,28 +148,61 @@ cli({
|
|
|
93
148
|
openInterest: r.openInterest,
|
|
94
149
|
expiration: r.expirationDate,
|
|
95
150
|
};
|
|
96
|
-
})
|
|
97
|
-
}
|
|
98
|
-
} catch(e) {
|
|
99
|
-
|
|
100
|
-
|
|
151
|
+
})
|
|
152
|
+
};
|
|
153
|
+
} catch(e) {
|
|
154
|
+
return { ok: false, reason: 'exception', message: e?.message || String(e) };
|
|
155
|
+
}
|
|
101
156
|
})()
|
|
102
|
-
`);
|
|
103
|
-
if (!data ||
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
157
|
+
`));
|
|
158
|
+
if (!data || data.ok !== true) {
|
|
159
|
+
if (data?.reason === 'http') {
|
|
160
|
+
throw new CommandExecutionError(`Barchart greeks request failed: HTTP ${data.status}${data.statusText ? ` ${data.statusText}` : ''}`);
|
|
161
|
+
}
|
|
162
|
+
if (data?.reason === 'malformed') {
|
|
163
|
+
throw new CommandExecutionError(`Barchart greeks returned an unreadable options payload${data.message ? `: ${data.message}` : ''}`);
|
|
164
|
+
}
|
|
165
|
+
if (data?.reason === 'exception') {
|
|
166
|
+
throw new CommandExecutionError(`Barchart greeks request failed: ${data.message || 'unknown error'}`);
|
|
167
|
+
}
|
|
168
|
+
throw new CommandExecutionError(`Failed to fetch Barchart greeks for ${symbol}`);
|
|
169
|
+
}
|
|
170
|
+
if (!Array.isArray(data.rows)) {
|
|
171
|
+
throw new CommandExecutionError('Barchart greeks returned an unreadable options payload');
|
|
172
|
+
}
|
|
173
|
+
if (data.rows.length === 0) {
|
|
174
|
+
throw new EmptyResultError('barchart greeks', `No option greeks were returned for ${symbol}. Confirm the symbol, expiration, and Barchart login state.`);
|
|
175
|
+
}
|
|
176
|
+
return data.rows.map(r => {
|
|
177
|
+
if (!r || typeof r !== 'object' || Array.isArray(r)) {
|
|
178
|
+
throw new CommandExecutionError('Barchart greeks returned a malformed option row');
|
|
179
|
+
}
|
|
180
|
+
const type = String(r.type || '').trim();
|
|
181
|
+
const expirationValue = String(r.expiration || '').trim();
|
|
182
|
+
if (!/^(call|put)$/i.test(type) || r.strike === undefined || r.strike === null || r.strike === '' || !expirationValue) {
|
|
183
|
+
throw new CommandExecutionError('Barchart greeks returned a malformed option row identity');
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
type,
|
|
187
|
+
strike: r.strike,
|
|
188
|
+
last: r.last != null ? Number(Number(r.last).toFixed(2)) : null,
|
|
189
|
+
iv: r.iv != null ? Number(Number(r.iv).toFixed(2)) + '%' : null,
|
|
190
|
+
delta: r.delta != null ? Number(Number(r.delta).toFixed(4)) : null,
|
|
191
|
+
gamma: r.gamma != null ? Number(Number(r.gamma).toFixed(4)) : null,
|
|
192
|
+
theta: r.theta != null ? Number(Number(r.theta).toFixed(4)) : null,
|
|
193
|
+
vega: r.vega != null ? Number(Number(r.vega).toFixed(4)) : null,
|
|
194
|
+
rho: r.rho != null ? Number(Number(r.rho).toFixed(4)) : null,
|
|
195
|
+
volume: r.volume,
|
|
196
|
+
openInterest: r.openInterest,
|
|
197
|
+
expiration: expirationValue,
|
|
198
|
+
};
|
|
199
|
+
});
|
|
119
200
|
},
|
|
120
201
|
});
|
|
202
|
+
|
|
203
|
+
export const __test__ = {
|
|
204
|
+
normalizeSymbol,
|
|
205
|
+
normalizeExpiration,
|
|
206
|
+
parseLimit,
|
|
207
|
+
unwrapBrowserResult,
|
|
208
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { getRegistry } from '@jackwener/opencli/registry';
|
|
3
|
+
import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
4
|
+
import './greeks.js';
|
|
5
|
+
|
|
6
|
+
const { normalizeExpiration, normalizeSymbol, parseLimit, unwrapBrowserResult } = await import('./greeks.js').then((m) => m.__test__);
|
|
7
|
+
|
|
8
|
+
function makePage(evaluateResult) {
|
|
9
|
+
return {
|
|
10
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
11
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
12
|
+
evaluate: vi.fn().mockResolvedValue(evaluateResult),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('barchart greeks command', () => {
|
|
17
|
+
const command = getRegistry().get('barchart/greeks');
|
|
18
|
+
|
|
19
|
+
it('registers with the expected shape', () => {
|
|
20
|
+
expect(command).toBeDefined();
|
|
21
|
+
expect(command.access).toBe('read');
|
|
22
|
+
expect(command.browser).toBe(true);
|
|
23
|
+
expect(command.columns).toEqual([
|
|
24
|
+
'type', 'strike', 'last', 'iv', 'delta', 'gamma', 'theta', 'vega', 'rho',
|
|
25
|
+
'volume', 'openInterest', 'expiration',
|
|
26
|
+
]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('maps returned option rows without changing the declared output shape', async () => {
|
|
30
|
+
const page = makePage({
|
|
31
|
+
session: 'site:barchart',
|
|
32
|
+
data: {
|
|
33
|
+
ok: true,
|
|
34
|
+
rows: [
|
|
35
|
+
{
|
|
36
|
+
type: 'Call',
|
|
37
|
+
strike: 190,
|
|
38
|
+
last: 3.456,
|
|
39
|
+
iv: 21.234,
|
|
40
|
+
delta: 0.56789,
|
|
41
|
+
gamma: 0.01234,
|
|
42
|
+
theta: -0.12345,
|
|
43
|
+
vega: 0.23456,
|
|
44
|
+
rho: 0.03456,
|
|
45
|
+
volume: 123,
|
|
46
|
+
openInterest: 456,
|
|
47
|
+
expiration: '2026-06-19',
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const rows = await command.func(page, { symbol: 'aapl', limit: 1 });
|
|
54
|
+
|
|
55
|
+
expect(page.goto).toHaveBeenCalledWith('https://www.barchart.com/stocks/quotes/AAPL/options');
|
|
56
|
+
expect(page.wait).toHaveBeenCalledWith(4);
|
|
57
|
+
expect(rows).toEqual([
|
|
58
|
+
{
|
|
59
|
+
type: 'Call',
|
|
60
|
+
strike: 190,
|
|
61
|
+
last: 3.46,
|
|
62
|
+
iv: '21.23%',
|
|
63
|
+
delta: 0.5679,
|
|
64
|
+
gamma: 0.0123,
|
|
65
|
+
theta: -0.1235,
|
|
66
|
+
vega: 0.2346,
|
|
67
|
+
rho: 0.0346,
|
|
68
|
+
volume: 123,
|
|
69
|
+
openInterest: 456,
|
|
70
|
+
expiration: '2026-06-19',
|
|
71
|
+
},
|
|
72
|
+
]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('validates args before browser navigation and unwraps bridge envelopes', async () => {
|
|
76
|
+
expect(normalizeSymbol(' aapl ')).toBe('AAPL');
|
|
77
|
+
expect(normalizeExpiration('2026-06-19')).toBe('2026-06-19');
|
|
78
|
+
expect(parseLimit(undefined)).toBe(10);
|
|
79
|
+
expect(parseLimit(100)).toBe(100);
|
|
80
|
+
expect(unwrapBrowserResult({ session: 'site:barchart', data: { ok: true } })).toEqual({ ok: true });
|
|
81
|
+
|
|
82
|
+
await expect(command.func(makePage({ ok: true, rows: [] }), { symbol: '', limit: 1 }))
|
|
83
|
+
.rejects.toBeInstanceOf(ArgumentError);
|
|
84
|
+
await expect(command.func(makePage({ ok: true, rows: [] }), { symbol: 'AAPL', expiration: '2026-02-30', limit: 1 }))
|
|
85
|
+
.rejects.toBeInstanceOf(ArgumentError);
|
|
86
|
+
await expect(command.func(makePage({ ok: true, rows: [] }), { symbol: 'AAPL', limit: 101 }))
|
|
87
|
+
.rejects.toBeInstanceOf(ArgumentError);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('embeds expiration and limit in the browser-side request script', async () => {
|
|
91
|
+
const page = makePage({
|
|
92
|
+
ok: true,
|
|
93
|
+
rows: [{
|
|
94
|
+
type: 'Put',
|
|
95
|
+
strike: 185,
|
|
96
|
+
last: null,
|
|
97
|
+
iv: null,
|
|
98
|
+
delta: null,
|
|
99
|
+
gamma: null,
|
|
100
|
+
theta: null,
|
|
101
|
+
vega: null,
|
|
102
|
+
rho: null,
|
|
103
|
+
volume: 0,
|
|
104
|
+
openInterest: 0,
|
|
105
|
+
expiration: '2026-07-17',
|
|
106
|
+
}],
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await command.func(page, { symbol: 'MSFT', expiration: '2026-07-17', limit: 7 });
|
|
110
|
+
const script = page.evaluate.mock.calls[0][0];
|
|
111
|
+
|
|
112
|
+
expect(script).toContain('const expDate = "2026-07-17"');
|
|
113
|
+
expect(script).toContain('const limit = 7');
|
|
114
|
+
expect(script).toContain("url += '&expirationDate=' + encodeURIComponent(expDate)");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('throws CommandExecutionError for HTTP, malformed, exception, and missing payload states', async () => {
|
|
118
|
+
await expect(command.func(makePage({ ok: false, reason: 'http', status: 403, statusText: 'Forbidden' }), { symbol: 'AAPL' }))
|
|
119
|
+
.rejects.toBeInstanceOf(CommandExecutionError);
|
|
120
|
+
await expect(command.func(makePage({ ok: false, reason: 'malformed' }), { symbol: 'AAPL' }))
|
|
121
|
+
.rejects.toBeInstanceOf(CommandExecutionError);
|
|
122
|
+
await expect(command.func(makePage({ ok: false, reason: 'exception', message: 'network down' }), { symbol: 'AAPL' }))
|
|
123
|
+
.rejects.toBeInstanceOf(CommandExecutionError);
|
|
124
|
+
await expect(command.func(makePage({ ok: false, reason: 'malformed', message: 'options rows did not include call or put identities' }), { symbol: 'AAPL' }))
|
|
125
|
+
.rejects.toThrow('call or put identities');
|
|
126
|
+
await expect(command.func(makePage(null), { symbol: 'AAPL' }))
|
|
127
|
+
.rejects.toBeInstanceOf(CommandExecutionError);
|
|
128
|
+
await expect(command.func(makePage({ ok: true, rows: 'bad' }), { symbol: 'AAPL' }))
|
|
129
|
+
.rejects.toBeInstanceOf(CommandExecutionError);
|
|
130
|
+
await expect(command.func(makePage({ ok: true, rows: [{ type: 'Call', strike: null, expiration: '' }] }), { symbol: 'AAPL' }))
|
|
131
|
+
.rejects.toThrow('malformed option row identity');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('throws EmptyResultError when Barchart returns no greeks rows', async () => {
|
|
135
|
+
await expect(command.func(makePage({ ok: true, rows: [] }), { symbol: 'AAPL' }))
|
|
136
|
+
.rejects.toBeInstanceOf(EmptyResultError);
|
|
137
|
+
});
|
|
138
|
+
});
|