@dsh-workbench/shared 0.1.13 → 0.1.15
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/dist/components/CardHead.js +3 -2
- package/dist/components/Field.css +16 -0
- package/dist/components/Field.d.ts +8 -0
- package/dist/components/Field.js +8 -0
- package/dist/components/Link.css +22 -0
- package/dist/components/Link.d.ts +10 -0
- package/dist/components/Link.js +8 -0
- package/dist/components/LongTaskRow.css +0 -15
- package/dist/components/LongTaskRow.js +3 -4
- package/dist/components/TextField.css +5 -0
- package/dist/host/browser-find.d.ts +38 -0
- package/dist/host/browser-find.js +242 -0
- package/dist/host/browser-read.d.ts +26 -0
- package/dist/host/browser-read.js +240 -0
- package/dist/host/http.d.ts +6 -1
- package/dist/host/http.js +2 -1
- package/dist/host/opencli-runtime.d.ts +3 -0
- package/dist/host/opencli-runtime.js +10 -0
- package/dist/host/opencli-window.d.ts +2 -0
- package/dist/host/opencli-window.js +8 -0
- package/dist/host/opencli.js +2 -0
- package/dist/host.d.ts +2 -0
- package/dist/host.js +2 -0
- package/dist/i18n/en.js +3 -0
- package/dist/i18n/index.d.ts +6 -3
- package/dist/i18n/zh.d.ts +5 -2
- package/dist/i18n/zh.js +5 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/package.json +1 -1
- package/src/components/CardHead.test.tsx +81 -0
- package/src/components/CardHead.tsx +4 -3
- package/src/components/Field.css +16 -0
- package/src/components/Field.test.tsx +27 -0
- package/src/components/Field.tsx +19 -0
- package/src/components/Link.css +22 -0
- package/src/components/Link.test.tsx +37 -0
- package/src/components/Link.tsx +26 -0
- package/src/components/LongTaskRow.css +0 -15
- package/src/components/LongTaskRow.tsx +3 -4
- package/src/components/TextField.css +5 -0
- package/src/host/__tests__/browser-find.test.ts +319 -0
- package/src/host/__tests__/browser-read.test.ts +197 -0
- package/src/host/browser-find.ts +311 -0
- package/src/host/browser-read.ts +308 -0
- package/src/host/http.ts +8 -1
- package/src/host/opencli-runtime.ts +13 -0
- package/src/host/opencli-window.ts +11 -0
- package/src/host/opencli.ts +2 -0
- package/src/host.ts +2 -0
- package/src/i18n/en.ts +3 -0
- package/src/i18n/zh.ts +5 -2
- package/src/index.ts +2 -0
- package/dist/components/StateDot.d.ts +0 -5
- package/dist/components/StateDot.js +0 -10
- package/dist/host/__tests__/fakes.d.ts +0 -41
- package/dist/host/__tests__/fakes.js +0 -76
- package/dist/host/__tests__/preflight-fixtures.d.ts +0 -19
- package/dist/host/__tests__/preflight-fixtures.js +0 -47
- package/dist/host/context-inject.d.ts +0 -24
- package/dist/host/context-inject.js +0 -74
- package/dist/host/events.d.ts +0 -9
- package/dist/host/events.js +0 -70
- package/dist/host/mount-units.d.ts +0 -52
- package/dist/host/mount-units.js +0 -186
- package/dist/host/preflight.d.ts +0 -58
- package/dist/host/preflight.js +0 -233
- package/dist/host/probe-cache.d.ts +0 -6
- package/dist/host/probe-cache.js +0 -19
- package/dist/host/probe-libs.d.ts +0 -45
- package/dist/host/probe-libs.js +0 -61
- package/dist/host/probe-units.d.ts +0 -17
- package/dist/host/probe-units.js +0 -102
- package/dist/host/probe-verdict.d.ts +0 -7
- package/dist/host/probe-verdict.js +0 -143
- package/dist/host/tool-scope.d.ts +0 -32
- package/dist/host/tool-scope.js +0 -84
- package/dist/host/unit-readiness.d.ts +0 -7
- package/dist/host/unit-readiness.js +0 -5
- package/dist/host/unit-registry.d.ts +0 -9
- package/dist/host/unit-registry.js +0 -33
- package/dist/host/version-range.d.ts +0 -2
- package/dist/host/version-range.js +0 -73
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
.wb-field {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: var(--wb-len-6);
|
|
5
|
+
font-size: var(--dsw-font-xs-13-font-size);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.wb-fieldLabel {
|
|
9
|
+
color: var(--dsw-alias-label-secondary);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.wb-fieldHint {
|
|
13
|
+
font-size: var(--dsw-font-xxs-12-font-size);
|
|
14
|
+
line-height: var(--wb-line-18);
|
|
15
|
+
color: var(--dsw-alias-label-tertiary);
|
|
16
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { Field } from './Field.js';
|
|
3
|
+
|
|
4
|
+
function texts(node: unknown): string[] {
|
|
5
|
+
if (node === null || node === undefined || typeof node === 'boolean') return [];
|
|
6
|
+
if (typeof node === 'string') return [node];
|
|
7
|
+
if (typeof node === 'number') return [String(node)];
|
|
8
|
+
if (Array.isArray(node)) return node.flatMap((one) => texts(one));
|
|
9
|
+
const element = node as { props?: { children?: unknown } };
|
|
10
|
+
return texts(element.props?.children);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('Field', () => {
|
|
14
|
+
it('标签、内容、提示按顺序出现', () => {
|
|
15
|
+
expect(texts(Field({ label: '看几天', hint: '1..30', children: '输入框' }))).toEqual([
|
|
16
|
+
'看几天',
|
|
17
|
+
'输入框',
|
|
18
|
+
'1..30',
|
|
19
|
+
]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('没给提示就不多一个空节点(否则每行都空一截)', () => {
|
|
23
|
+
const node = Field({ label: '看几天', children: '输入框' }) as unknown as { props: { children: unknown[] } };
|
|
24
|
+
expect(texts(node)).toEqual(['看几天', '输入框']);
|
|
25
|
+
expect(node.props.children.filter((one) => one !== null)).toHaveLength(2);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ReactNode } from '../lib/react.js';
|
|
2
|
+
import { h } from '../lib/react.js';
|
|
3
|
+
import './Field.css';
|
|
4
|
+
|
|
5
|
+
export interface FieldProps {
|
|
6
|
+
readonly label: ReactNode;
|
|
7
|
+
readonly hint?: ReactNode | undefined;
|
|
8
|
+
readonly children?: ReactNode | undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function Field({ label, hint, children }: FieldProps) {
|
|
12
|
+
return (
|
|
13
|
+
<label className="wb-field">
|
|
14
|
+
<span className="wb-fieldLabel">{label}</span>
|
|
15
|
+
{children}
|
|
16
|
+
{hint === undefined ? null : <span className="wb-fieldHint">{hint}</span>}
|
|
17
|
+
</label>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
:where(.wb-link) {
|
|
2
|
+
margin-left: var(--wb-len-2);
|
|
3
|
+
display: inline-flex;
|
|
4
|
+
align-items: center;
|
|
5
|
+
gap: var(--wb-len-2);
|
|
6
|
+
font: inherit;
|
|
7
|
+
font-size: var(--dsw-font-xxs-12-font-size);
|
|
8
|
+
line-height: var(--wb-line-18);
|
|
9
|
+
color: var(--dsw-alias-label-primary);
|
|
10
|
+
text-decoration: none;
|
|
11
|
+
white-space: nowrap;
|
|
12
|
+
vertical-align: baseline;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
:where(.wb-link:hover) {
|
|
16
|
+
text-decoration: underline;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
:where(.wb-link:focus-visible) {
|
|
20
|
+
outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
|
|
21
|
+
outline-offset: -2px;
|
|
22
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { Link } from './Link.js';
|
|
3
|
+
|
|
4
|
+
interface AnyElement {
|
|
5
|
+
readonly type: unknown;
|
|
6
|
+
readonly props: Record<string, unknown>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function kids(node: AnyElement): unknown[] {
|
|
10
|
+
return (node.props['children'] as unknown[]).filter((one) => one !== null);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('Link', () => {
|
|
14
|
+
it('外链:新窗口 + noopener,自带那个外链小箭头(就这一套,不许去不许换)', () => {
|
|
15
|
+
const node = Link({ href: 'https://example.com', children: '去网关看账单' }) as unknown as AnyElement;
|
|
16
|
+
expect(node.props['className']).toBe('wb-link');
|
|
17
|
+
expect(node.props['href']).toBe('https://example.com');
|
|
18
|
+
expect(node.props['target']).toBe('_blank');
|
|
19
|
+
expect(node.props['rel']).toBe('noopener noreferrer');
|
|
20
|
+
expect(kids(node)).toHaveLength(2);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('同窗口(external: false)不给 target,也不带外链箭头', () => {
|
|
24
|
+
const node = Link({ href: '/x', children: '本地', external: false }) as unknown as AnyElement;
|
|
25
|
+
expect(node.props['target']).toBeUndefined();
|
|
26
|
+
expect(kids(node)).toHaveLength(1);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('title 给了才带上;className 只用来加布局(不改样子)', () => {
|
|
30
|
+
const bare = Link({ href: '/x', children: 'x' }) as unknown as AnyElement;
|
|
31
|
+
expect(bare.props['title']).toBeUndefined();
|
|
32
|
+
const titled = Link({ href: '/x', children: 'x', title: '去网关看账单' }) as unknown as AnyElement;
|
|
33
|
+
expect(titled.props['title']).toBe('去网关看账单');
|
|
34
|
+
const spaced = Link({ href: '/x', children: 'x', className: 'wb-longTaskLink' }) as unknown as AnyElement;
|
|
35
|
+
expect(spaced.props['className']).toBe('wb-link wb-longTaskLink');
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ReactNode } from '../lib/react.js';
|
|
2
|
+
import { h } from '../lib/react.js';
|
|
3
|
+
import { IconRightUpOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
4
|
+
import './Link.css';
|
|
5
|
+
|
|
6
|
+
export interface LinkProps {
|
|
7
|
+
readonly href: string;
|
|
8
|
+
readonly children?: ReactNode | undefined;
|
|
9
|
+
readonly title?: string | undefined;
|
|
10
|
+
readonly external?: boolean | undefined;
|
|
11
|
+
readonly className?: string | undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function Link({ href, children, title, external = true, className }: LinkProps) {
|
|
15
|
+
return (
|
|
16
|
+
<a
|
|
17
|
+
className={className === undefined ? 'wb-link' : `wb-link ${className}`}
|
|
18
|
+
href={href}
|
|
19
|
+
{...(title === undefined ? {} : { title })}
|
|
20
|
+
{...(external ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
|
|
21
|
+
>
|
|
22
|
+
{children}
|
|
23
|
+
{external ? <IconRightUpOutlineRegular size={12} /> : null}
|
|
24
|
+
</a>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -16,21 +16,6 @@
|
|
|
16
16
|
|
|
17
17
|
.wb-longTaskLink {
|
|
18
18
|
flex: none;
|
|
19
|
-
display: inline-flex;
|
|
20
|
-
align-items: center;
|
|
21
|
-
gap: var(--wb-len-2);
|
|
22
|
-
color: var(--dsw-alias-label-secondary);
|
|
23
|
-
text-decoration: none;
|
|
24
|
-
white-space: nowrap;
|
|
25
|
-
}
|
|
26
|
-
.wb-longTaskLink:hover,
|
|
27
|
-
.wb-longTaskLink:focus-visible {
|
|
28
|
-
color: var(--dsw-alias-label-primary);
|
|
29
|
-
text-decoration: none;
|
|
30
|
-
}
|
|
31
|
-
.wb-longTaskLink:focus-visible {
|
|
32
|
-
outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
|
|
33
|
-
outline-offset: -2px;
|
|
34
19
|
}
|
|
35
20
|
|
|
36
21
|
.wb-longTaskHeadline {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { longTaskDot, type LongTaskStatus } from '../api/long-task.js';
|
|
2
|
+
import { Link } from './Link.js';
|
|
2
3
|
import { Fragment, h, useState } from '../lib/react.js';
|
|
3
4
|
import {
|
|
4
5
|
CodeBlock,
|
|
5
6
|
DisclosureRow,
|
|
6
7
|
IconCodeOutlineRegular,
|
|
7
8
|
IconFlatListOutlineRegular,
|
|
8
|
-
IconRightUpOutlineRegular,
|
|
9
9
|
StateDot,
|
|
10
10
|
} from '@deepseek-ai/dsh-client-ui-primitives';
|
|
11
11
|
import './LongTaskRow.css';
|
|
@@ -30,10 +30,9 @@ export interface LongTaskRowProps {
|
|
|
30
30
|
|
|
31
31
|
function RowLink({ href, text }: LongTaskRowLink) {
|
|
32
32
|
return (
|
|
33
|
-
<
|
|
33
|
+
<Link href={href} className="wb-longTaskLink">
|
|
34
34
|
{text}
|
|
35
|
-
|
|
36
|
-
</a>
|
|
35
|
+
</Link>
|
|
37
36
|
);
|
|
38
37
|
}
|
|
39
38
|
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
browserFind,
|
|
4
|
+
cellRows,
|
|
5
|
+
cellRowsOf,
|
|
6
|
+
entriesOf,
|
|
7
|
+
reasonOf,
|
|
8
|
+
sameOrigin,
|
|
9
|
+
urlOf,
|
|
10
|
+
} from '../browser-find.js';
|
|
11
|
+
|
|
12
|
+
interface Step {
|
|
13
|
+
readonly stdout?: string;
|
|
14
|
+
readonly stderr?: string;
|
|
15
|
+
readonly exitCode?: number;
|
|
16
|
+
readonly timedOut?: boolean;
|
|
17
|
+
readonly throws?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface Plan {
|
|
21
|
+
readonly url?: string | readonly string[];
|
|
22
|
+
readonly find?: readonly Step[];
|
|
23
|
+
readonly open?: readonly Step[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function fakeShell(plan: Plan) {
|
|
27
|
+
const commands: string[] = [];
|
|
28
|
+
const counters = { url: 0, find: 0, open: 0 };
|
|
29
|
+
const pick = <T>(list: readonly T[] | undefined, index: number, fallback: T): T =>
|
|
30
|
+
(list ?? [])[Math.min(index, Math.max(0, (list ?? []).length - 1))] ?? fallback;
|
|
31
|
+
|
|
32
|
+
const shell = {
|
|
33
|
+
resolve(request: { command: string }) {
|
|
34
|
+
commands.push(request.command);
|
|
35
|
+
return request;
|
|
36
|
+
},
|
|
37
|
+
async execute(spec: { command: string }) {
|
|
38
|
+
const command = String(spec.command);
|
|
39
|
+
let step: Step = {};
|
|
40
|
+
let stdout = '';
|
|
41
|
+
if (command.includes("'get'") && command.includes("'url'")) {
|
|
42
|
+
const urls = typeof plan.url === 'string' ? [plan.url] : (plan.url ?? []);
|
|
43
|
+
stdout = pick(urls, counters.url, '') as string;
|
|
44
|
+
counters.url += 1;
|
|
45
|
+
} else if (command.includes("'find'")) {
|
|
46
|
+
step = pick(plan.find, counters.find, {});
|
|
47
|
+
stdout = step.stdout ?? '';
|
|
48
|
+
counters.find += 1;
|
|
49
|
+
} else if (command.includes("'open'")) {
|
|
50
|
+
step = pick(plan.open, counters.open, {});
|
|
51
|
+
stdout = step.stdout ?? JSON.stringify({ page: 'AE4021' });
|
|
52
|
+
counters.open += 1;
|
|
53
|
+
}
|
|
54
|
+
if (step.throws === true) throw new Error('spawn opencli ENOENT');
|
|
55
|
+
return {
|
|
56
|
+
result: async () => ({
|
|
57
|
+
exitCode: step.exitCode ?? 0,
|
|
58
|
+
timedOut: step.timedOut ?? false,
|
|
59
|
+
aborted: false,
|
|
60
|
+
stdout: { text: stdout, truncated: false },
|
|
61
|
+
stderr: { text: step.stderr ?? '', truncated: false },
|
|
62
|
+
}),
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
return { shell: shell as never, commands };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const FAST = { settleMs: 1, timeoutMs: 50 } as const;
|
|
70
|
+
const SITE = 'https://example.test/rates';
|
|
71
|
+
const OTHER = 'https://example.test/elsewhere';
|
|
72
|
+
const CSS = 'table tr';
|
|
73
|
+
const MATCH = JSON.stringify({ entries: [{ text: '美元\n673.30', tag: 'tr', nth: 1 }] });
|
|
74
|
+
const ZERO_MATCH = JSON.stringify({
|
|
75
|
+
error: {
|
|
76
|
+
code: 'selector_not_found',
|
|
77
|
+
hint: 'try a less specific selector',
|
|
78
|
+
message: 'CSS selector table tr matched 0 elements',
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('browserFind · 停在哪一页', () => {
|
|
83
|
+
it('已经在目标站点上:只问一次 get url,然后一次 find', async () => {
|
|
84
|
+
const { shell, commands } = fakeShell({ url: SITE, find: [{ stdout: MATCH }] });
|
|
85
|
+
const out = await browserFind({ shell, css: CSS, url: SITE, ...FAST });
|
|
86
|
+
|
|
87
|
+
expect(out.ok).toBe(true);
|
|
88
|
+
expect(out.count).toBe(1);
|
|
89
|
+
expect(out.entries[0]?.text).toBe('美元\n673.30');
|
|
90
|
+
expect(commands.filter((one) => one.includes("'find'"))).toHaveLength(1);
|
|
91
|
+
expect(commands.some((one) => one.includes("'open'"))).toBe(false);
|
|
92
|
+
expect(commands[0]).toContain("'get' 'url'");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('新会话还停在 about:blank:先 open 再读,命令走 find --css', async () => {
|
|
96
|
+
const { shell, commands } = fakeShell({
|
|
97
|
+
url: ['about:blank', SITE],
|
|
98
|
+
find: [{ stdout: MATCH }],
|
|
99
|
+
});
|
|
100
|
+
const out = await browserFind({ shell, css: CSS, url: SITE, ...FAST });
|
|
101
|
+
|
|
102
|
+
expect(out.ok).toBe(true);
|
|
103
|
+
expect(out.count).toBe(1);
|
|
104
|
+
expect(commands.some((one) => one.includes("'open'"))).toBe(true);
|
|
105
|
+
expect(commands.filter((one) => one.includes("'find'"))).toHaveLength(1);
|
|
106
|
+
expect(commands[1]).toContain("'open'");
|
|
107
|
+
expect(commands[1]).toContain(SITE);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('问不出来在哪一页(get url 空)时不乱开,直接找', async () => {
|
|
111
|
+
const { shell, commands } = fakeShell({ url: '', find: [{ stdout: MATCH }] });
|
|
112
|
+
const out = await browserFind({ shell, css: CSS, url: SITE, ...FAST });
|
|
113
|
+
|
|
114
|
+
expect(out.ok).toBe(true);
|
|
115
|
+
expect(commands.some((one) => one.includes("'open'"))).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('open 失败 → cli-exec-failed,带上 helpUrl 与原因', async () => {
|
|
119
|
+
const { shell } = fakeShell({
|
|
120
|
+
url: 'about:blank',
|
|
121
|
+
open: [{ stdout: JSON.stringify({ error: { message: 'browser session is not bound' } }), exitCode: 1 }],
|
|
122
|
+
});
|
|
123
|
+
const out = await browserFind({ shell, css: CSS, url: SITE, ...FAST });
|
|
124
|
+
|
|
125
|
+
expect(out.ok).toBe(false);
|
|
126
|
+
expect(out.code).toBe('cli-exec-failed');
|
|
127
|
+
expect(out.message).toContain('打不开');
|
|
128
|
+
expect(out.detail).toContain('not bound');
|
|
129
|
+
expect(out.helpUrl).toBe(SITE);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('open 那条命令直接抛(opencli 不在)也要报出来,不许把异常漏出去', async () => {
|
|
133
|
+
const { shell } = fakeShell({ url: 'about:blank', open: [{ throws: true }] });
|
|
134
|
+
const out = await browserFind({ shell, css: CSS, url: SITE, ...FAST });
|
|
135
|
+
|
|
136
|
+
expect(out.ok).toBe(false);
|
|
137
|
+
expect(out.code).toBe('cli-exec-failed');
|
|
138
|
+
expect(out.detail).toContain('ENOENT');
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('browserFind · 找到与找不到', () => {
|
|
143
|
+
it('页面上就是没有匹配元素 → ok,但 count 是 0(空不撒谎)', async () => {
|
|
144
|
+
const { shell } = fakeShell({ url: SITE, find: [{ stdout: JSON.stringify({ entries: [] }) }] });
|
|
145
|
+
const out = await browserFind({ shell, css: CSS, url: SITE, ...FAST });
|
|
146
|
+
|
|
147
|
+
expect(out.ok).toBe(true);
|
|
148
|
+
expect(out.count).toBe(0);
|
|
149
|
+
expect(out.message).toContain('没有匹配');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('在站点上但 find 失败(会话刚重启那类):安全网再 open 一次并重读', async () => {
|
|
153
|
+
const { shell, commands } = fakeShell({
|
|
154
|
+
url: SITE,
|
|
155
|
+
find: [{ stdout: ZERO_MATCH, exitCode: 1 }, { stdout: MATCH }],
|
|
156
|
+
});
|
|
157
|
+
const out = await browserFind({ shell, css: CSS, url: SITE, ...FAST });
|
|
158
|
+
|
|
159
|
+
expect(out.ok).toBe(true);
|
|
160
|
+
expect(out.count).toBe(1);
|
|
161
|
+
expect(commands.filter((one) => one.includes("'find'"))).toHaveLength(2);
|
|
162
|
+
expect(commands.some((one) => one.includes("'open'"))).toBe(true);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('一直失败:把 opencli 自己在 stdout JSON 里说的原因当 message(不是 stderr)', async () => {
|
|
166
|
+
const { shell } = fakeShell({
|
|
167
|
+
url: SITE,
|
|
168
|
+
find: [{ stdout: ZERO_MATCH, exitCode: 1 }, { stdout: ZERO_MATCH, exitCode: 1 }],
|
|
169
|
+
});
|
|
170
|
+
const out = await browserFind({ shell, css: CSS, url: SITE, ...FAST });
|
|
171
|
+
|
|
172
|
+
expect(out.ok).toBe(false);
|
|
173
|
+
expect(out.code).toBe('cli-exec-failed');
|
|
174
|
+
expect(out.message).toContain('matched 0 elements');
|
|
175
|
+
expect(out.hint).toBe('try a less specific selector');
|
|
176
|
+
expect(out.helpUrl).toBe(SITE);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('超时归成 cli-timeout,且不再折腾页面', async () => {
|
|
180
|
+
const { shell, commands } = fakeShell({
|
|
181
|
+
url: SITE,
|
|
182
|
+
find: [{ timedOut: true, exitCode: 124 }],
|
|
183
|
+
});
|
|
184
|
+
const out = await browserFind({ shell, css: CSS, url: SITE, ...FAST });
|
|
185
|
+
|
|
186
|
+
expect(out.ok).toBe(false);
|
|
187
|
+
expect(out.code).toBe('cli-timeout');
|
|
188
|
+
expect(out.detail).toContain('秒内返回');
|
|
189
|
+
expect(commands.some((one) => one.includes("'open'"))).toBe(false);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('url 没给:只 find,不问页面也不开', async () => {
|
|
193
|
+
const { shell, commands } = fakeShell({ find: [{ stdout: MATCH }] });
|
|
194
|
+
const out = await browserFind({ shell, css: CSS, ...FAST });
|
|
195
|
+
|
|
196
|
+
expect(out.ok).toBe(true);
|
|
197
|
+
expect(commands).toHaveLength(1);
|
|
198
|
+
expect(commands[0]).toContain("'find'");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('open:false:只 find(不说自己在哪一页,也不开)', async () => {
|
|
202
|
+
const { shell, commands } = fakeShell({
|
|
203
|
+
url: SITE,
|
|
204
|
+
find: [{ stdout: ZERO_MATCH, exitCode: 1 }, { stdout: MATCH }],
|
|
205
|
+
});
|
|
206
|
+
const out = await browserFind({ shell, css: CSS, url: SITE, open: false, ...FAST });
|
|
207
|
+
|
|
208
|
+
expect(out.ok).toBe(false);
|
|
209
|
+
expect(commands).toHaveLength(1);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('opencli 根本没起来(find 那条抛)→ cli-exec-failed,带照做的话', async () => {
|
|
213
|
+
const { shell } = fakeShell({ url: SITE, find: [{ throws: true }] });
|
|
214
|
+
const out = await browserFind({ shell, css: CSS, url: SITE, ...FAST });
|
|
215
|
+
|
|
216
|
+
expect(out.ok).toBe(false);
|
|
217
|
+
expect(out.code).toBe('cli-exec-failed');
|
|
218
|
+
expect(out.detail).toContain('ENOENT');
|
|
219
|
+
expect(out.hint).toContain('opencli');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('session / command / limit / textMax 都能覆盖(命令行每个 token 都加引号)', async () => {
|
|
223
|
+
const { shell, commands } = fakeShell({ url: SITE, find: [{ stdout: MATCH }] });
|
|
224
|
+
await browserFind({
|
|
225
|
+
shell,
|
|
226
|
+
css: CSS,
|
|
227
|
+
url: SITE,
|
|
228
|
+
session: 'dsh-fx-rate',
|
|
229
|
+
command: '/usr/local/bin/opencli',
|
|
230
|
+
limit: 60,
|
|
231
|
+
textMax: 400,
|
|
232
|
+
...FAST,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const findLine = commands.find((one) => one.includes("'find'")) ?? '';
|
|
236
|
+
expect(commands[0]).toContain("'/usr/local/bin/opencli'");
|
|
237
|
+
expect(commands[0]).toContain("'dsh-fx-rate'");
|
|
238
|
+
expect(findLine).toContain("'--limit' '60'");
|
|
239
|
+
expect(findLine).toContain("'--text-max' '400'");
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe('cellRowsOf / reasonOf / sameOrigin', () => {
|
|
244
|
+
it('按换行切格、丢掉空行、按 minCells 过滤', () => {
|
|
245
|
+
const rows = cellRowsOf([
|
|
246
|
+
{ text: '货币名称\n现汇买入价\n\n现钞买入价\n现汇卖出价' },
|
|
247
|
+
{ text: '只有一个' },
|
|
248
|
+
{ text: undefined },
|
|
249
|
+
]);
|
|
250
|
+
|
|
251
|
+
expect(rows).toHaveLength(1);
|
|
252
|
+
expect(rows[0]).toEqual(['货币名称', '现汇买入价', '现钞买入价', '现汇卖出价']);
|
|
253
|
+
expect(cellRowsOf([{ text: '一\n二' }], 3)).toEqual([]);
|
|
254
|
+
expect(cellRows('a\n b \n\n')).toEqual(['a', 'b']);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('reasonOf 优先取 error.message,没有才退回到最后一行', () => {
|
|
258
|
+
expect(reasonOf(JSON.stringify({ error: { message: 'matched 0 elements' } }), '')).toBe(
|
|
259
|
+
'matched 0 elements',
|
|
260
|
+
);
|
|
261
|
+
expect(reasonOf('', 'opencli: daemon not ready\nboom')).toBe('boom');
|
|
262
|
+
expect(reasonOf('not json', '')).toBe('not json');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('entriesOf 只认有 text 的项,缺 nth 用序号补', () => {
|
|
266
|
+
const entries = entriesOf({
|
|
267
|
+
entries: [{ text: 'a', tag: 'tr' }, { tag: 'tr' }, 'nope', { text: 'b', tag: 'tr', nth: 9 }],
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
expect(entries).toHaveLength(3);
|
|
271
|
+
expect(entries[0]).toEqual({ text: 'a', tag: 'tr', nth: 0 });
|
|
272
|
+
expect(entries[2]).toEqual({ text: 'b', tag: 'tr', nth: 9 });
|
|
273
|
+
expect(entriesOf(undefined)).toEqual([]);
|
|
274
|
+
expect(entriesOf({ entries: 'nope' })).toEqual([]);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('urlOf 认裸 URL、认带引号的、也认 {url} 的 JSON(判错的代价是每次多开一次页面)', () => {
|
|
278
|
+
expect(urlOf('https://a.test/x\n')).toBe('https://a.test/x');
|
|
279
|
+
expect(urlOf('URL: https://a.test/x')).toBe('https://a.test/x');
|
|
280
|
+
expect(urlOf('"https://a.test/x"')).toBe('https://a.test/x');
|
|
281
|
+
expect(urlOf('{"url":"https://a.test/x","title":"t"}')).toBe('https://a.test/x');
|
|
282
|
+
expect(urlOf('')).toBe('');
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('sameOrigin 只看同源;空串或不是 URL 一律不算', () => {
|
|
286
|
+
expect(sameOrigin('https://a.test/x', 'https://a.test/y')).toBe(true);
|
|
287
|
+
expect(sameOrigin('https://a.test/x', 'https://b.test/y')).toBe(false);
|
|
288
|
+
expect(sameOrigin('about:blank', SITE)).toBe(false);
|
|
289
|
+
expect(sameOrigin('', SITE)).toBe(false);
|
|
290
|
+
expect(sameOrigin('not a url', SITE)).toBe(false);
|
|
291
|
+
expect(OTHER).toContain('elsewhere');
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
function backgroundless(commands: readonly string[]): readonly string[] {
|
|
296
|
+
return commands
|
|
297
|
+
.filter((one) => one.includes("'browser'"))
|
|
298
|
+
.filter(
|
|
299
|
+
(one) => !one.includes("'--window' 'background'") || !one.includes("'OPENCLI_WINDOW=background'"),
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
describe('browserFind · 后台静默(问页 + 读 + 打开都不许抢焦点)', () => {
|
|
304
|
+
it('已经在目标站点上:get url + find 两条全带', async () => {
|
|
305
|
+
const { shell, commands } = fakeShell({ url: SITE, find: [{ stdout: MATCH }] });
|
|
306
|
+
await browserFind({ shell, css: CSS, url: SITE, ...FAST });
|
|
307
|
+
|
|
308
|
+
expect(commands).toHaveLength(2);
|
|
309
|
+
expect(backgroundless(commands)).toEqual([]);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('不在目标站点上:get url + open + find 三条全带', async () => {
|
|
313
|
+
const { shell, commands } = fakeShell({ url: ['about:blank', SITE], find: [{ stdout: MATCH }] });
|
|
314
|
+
await browserFind({ shell, css: CSS, url: SITE, ...FAST });
|
|
315
|
+
|
|
316
|
+
expect(commands.filter((one) => one.includes("'open'"))).toHaveLength(1);
|
|
317
|
+
expect(backgroundless(commands)).toEqual([]);
|
|
318
|
+
});
|
|
319
|
+
});
|