@dsh-workbench/shared 0.3.6 → 0.3.7
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/api/context-ref.d.ts +0 -12
- package/dist/api/context-ref.js +0 -40
- package/dist/api/facts.d.ts +0 -6
- package/dist/api/facts.js +0 -8
- package/dist/components/ConfirmBar.css +26 -0
- package/dist/components/ConfirmBar.d.ts +36 -0
- package/dist/components/ConfirmBar.js +26 -0
- package/dist/components/Form.d.ts +7 -5
- package/dist/components/Form.js +5 -3
- package/dist/components/FormNodes.d.ts +17 -13
- package/dist/components/FormNodes.js +13 -5
- package/dist/components/FormTypes.d.ts +39 -14
- package/dist/components/MockStrip.css +11 -1
- package/dist/components/StateDot.d.ts +5 -0
- package/dist/components/StateDot.js +10 -0
- package/dist/config/constants.d.ts +2 -0
- package/dist/config/constants.js +2 -0
- package/dist/host/__tests__/fakes.d.ts +41 -0
- package/dist/host/__tests__/fakes.js +76 -0
- package/dist/host/__tests__/preflight-fixtures.d.ts +19 -0
- package/dist/host/__tests__/preflight-fixtures.js +47 -0
- package/dist/host/board-fact.d.ts +8 -0
- package/dist/host/board-fact.js +16 -0
- package/dist/host/browser-read.js +2 -1
- package/dist/host/context-inject.d.ts +24 -0
- package/dist/host/context-inject.js +74 -0
- package/dist/host/context-ref.d.ts +13 -0
- package/dist/host/context-ref.js +41 -0
- package/dist/host/diy-debug.d.ts +80 -0
- package/dist/host/diy-debug.js +88 -0
- package/dist/host/diy-record.d.ts +135 -0
- package/dist/host/diy-record.js +162 -0
- package/dist/host/events.d.ts +9 -0
- package/dist/host/events.js +70 -0
- package/dist/host/facts.d.ts +6 -0
- package/dist/host/facts.js +9 -0
- package/dist/host/git.js +1 -1
- package/dist/host/http.d.ts +2 -2
- package/dist/host/http.js +3 -3
- package/dist/host/long-task.d.ts +5 -0
- package/dist/host/long-task.js +3 -0
- package/dist/host/mount-units.d.ts +52 -0
- package/dist/host/mount-units.js +186 -0
- package/dist/host/opencli-runtime.js +1 -1
- package/dist/host/opencli-window.js +1 -1
- package/dist/host/opencli.js +2 -1
- package/dist/host/panel-state.d.ts +120 -0
- package/dist/host/panel-state.js +176 -0
- package/dist/host/plugin-source.js +1 -1
- package/dist/host/preflight.d.ts +58 -0
- package/dist/host/preflight.js +233 -0
- package/dist/host/probe-cache.d.ts +6 -0
- package/dist/host/probe-cache.js +19 -0
- package/dist/host/probe-libs.d.ts +45 -0
- package/dist/host/probe-libs.js +61 -0
- package/dist/host/probe-units.d.ts +17 -0
- package/dist/host/probe-units.js +102 -0
- package/dist/host/probe-verdict.d.ts +7 -0
- package/dist/host/probe-verdict.js +143 -0
- package/dist/host/shell-text.d.ts +13 -0
- package/dist/host/shell-text.js +20 -0
- package/dist/host/tool-scope.d.ts +32 -0
- package/dist/host/tool-scope.js +84 -0
- package/dist/host/unit-declaration.d.ts +65 -0
- package/dist/host/unit-declaration.js +213 -0
- package/dist/host/unit-readiness.d.ts +7 -0
- package/dist/host/unit-readiness.js +5 -0
- package/dist/host/unit-registry.d.ts +9 -0
- package/dist/host/unit-registry.js +33 -0
- package/dist/host/version-range.d.ts +2 -0
- package/dist/host/version-range.js +73 -0
- package/dist/host.d.ts +7 -4
- package/dist/host.js +7 -4
- package/dist/i18n/en.js +4 -0
- package/dist/i18n/index.d.ts +5 -1
- package/dist/i18n/zh.d.ts +4 -0
- package/dist/i18n/zh.js +4 -0
- package/dist/index.d.ts +1 -4
- package/dist/index.js +1 -4
- package/dist/lib/board-fact.d.ts +0 -7
- package/dist/lib/board-fact.js +0 -15
- package/dist/lib/diy-record.d.ts +38 -1
- package/dist/lib/diy-record.js +66 -0
- package/dist/lib/text.d.ts +0 -13
- package/dist/lib/text.js +0 -20
- package/dist/testing/declaration.js +1 -1
- package/package.json +1 -1
- package/src/api/__tests__/facts.test.ts +0 -13
- package/src/api/context-ref.ts +0 -39
- package/src/api/facts.ts +0 -9
- package/src/components/ConfirmBar.css +26 -0
- package/src/components/ConfirmBar.test.tsx +130 -0
- package/src/components/ConfirmBar.tsx +63 -0
- package/src/components/Form.test.tsx +15 -0
- package/src/components/Form.tsx +7 -5
- package/src/components/FormNodes.tsx +50 -26
- package/src/components/FormTypes.test.ts +31 -0
- package/src/components/FormTypes.ts +46 -15
- package/src/components/FormWiring.test.tsx +1 -0
- package/src/config/constants.ts +3 -0
- package/src/host/__tests__/board-fact.test.ts +47 -0
- package/src/{lib → host/__tests__}/diy-debug.test.ts +1 -1
- package/src/{lib → host/__tests__}/diy-record.test.ts +137 -1
- package/src/host/__tests__/facts.test.ts +13 -0
- package/src/host/__tests__/long-task.test.ts +42 -0
- package/src/{lib → host/__tests__}/panel-state.test.ts +1 -1
- package/src/host/__tests__/preflight-route.test.ts +33 -0
- package/src/host/__tests__/shell-text.test.ts +44 -0
- package/src/host/board-fact.ts +14 -0
- package/src/host/browser-read.ts +2 -1
- package/src/host/context-ref.ts +40 -0
- package/src/{lib → host}/diy-record.ts +95 -1
- package/src/host/facts.ts +10 -0
- package/src/host/git.ts +1 -1
- package/src/host/http.ts +4 -4
- package/src/host/long-task.ts +9 -0
- package/src/host/opencli-runtime.ts +1 -1
- package/src/host/opencli-window.ts +1 -1
- package/src/host/opencli.ts +2 -1
- package/src/host/plugin-source.ts +1 -1
- package/src/host/shell-text.ts +21 -0
- package/src/{api → host}/unit-declaration.ts +1 -1
- package/src/host.ts +7 -4
- package/src/i18n/en.ts +4 -0
- package/src/i18n/zh.ts +4 -0
- package/src/index.ts +1 -4
- package/src/lib/board-fact.test.ts +1 -33
- package/src/lib/board-fact.ts +0 -13
- package/src/lib/text.test.ts +1 -43
- package/src/lib/text.ts +0 -22
- package/src/testing/declaration.ts +1 -1
- package/dist/lib/lint-probe.d.ts +0 -1
- package/dist/lib/lint-probe.js +0 -6
- /package/src/{api → host}/__tests__/unit-declaration.test.ts +0 -0
- /package/src/{lib → host}/diy-debug.ts +0 -0
- /package/src/{lib → host}/panel-state.ts +0 -0
package/src/components/Form.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { h, useEffect, useRef, useState } from '../lib/react.js';
|
|
2
|
-
import type { FormProps } from './FormTypes.js';
|
|
2
|
+
import type { FormProps, FormValues } from './FormTypes.js';
|
|
3
3
|
import { FormActions } from './FormActions.js';
|
|
4
4
|
import { FormRows } from './FormRows.js';
|
|
5
5
|
import { Note } from './Note.js';
|
|
@@ -16,13 +16,15 @@ import './Form.css';
|
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* 整张表单:`fields` 进,行、错误、操作区、(被挡住的)提交都由它安排 —— 它自己一个值都不存,
|
|
19
|
-
*
|
|
19
|
+
* 值仍归调用点(`value` 与 `onChange` 写在字段配置的同一格里,`Form` 只是把两者原样递给那一格的 `control`)。
|
|
20
|
+
* 写了 `Form<Values>` 就按 `Values` 把每一格的键与值类型钉住(`Values` 里会 `visible: false` 的键写成可选键);
|
|
21
|
+
* 不写就是默认档,键值都不钉。
|
|
20
22
|
* 还没提交过时一格错都不显示;第一次提交之后每次渲染重算,改好了当场消。
|
|
21
23
|
* 提交那一拍永远先算一遍:有错就不调 `onSubmit`,并把第一个有错的 `FormRow` 带进视口、聚焦它里面的控件。
|
|
22
24
|
* 回车与点提交钮走的是同一条路:真 `<form>` 的隐式提交在**只有一个文本框**时也会 fire,所以 `busy` / `disabled` 时回车同样不提交。
|
|
23
25
|
* 调用点只管数据与语义:行怎么排、错误摆哪、什么时候校验、按钮在哪,这里没有口子。
|
|
24
|
-
* @param props.fields - 每格的键 / 标签 / 值 / 校验 / 控件;`visible: false` 的那格当它不存在(不渲染、不校验、不进 `values`)。
|
|
25
|
-
* @param props.onSubmit - 校验全过的那一拍,回的是从 `fields`
|
|
26
|
+
* @param props.fields - 每格的键 / 标签 / 值 / 值怎么改(`onChange`)/ 校验 / 控件;`visible: false` 的那格当它不存在(不渲染、不校验、不进 `values`)。
|
|
27
|
+
* @param props.onSubmit - 校验全过的那一拍,回的是从 `fields` 现取的键值表(按 `visibleFieldsOf` 过滤过,不可见那格不在里面);被挡住时不会被调。
|
|
26
28
|
* @param props.submit - 提交钮文案;`busyLabel` 是提交中换的那句,不给就沿用 `label`。
|
|
27
29
|
* @param props.cancel - 取消钮;给了就排在提交钮左边,`busy` 时一起禁用。
|
|
28
30
|
* @param props.extra - 其余动作(default / danger),排在最左,`busy` 时一起禁用。
|
|
@@ -31,7 +33,7 @@ import './Form.css';
|
|
|
31
33
|
* @param props.problem - 整表级那句错(提交失败),落在行与操作区之间;不给就不画。
|
|
32
34
|
* @returns 一个 `<form>`(`noValidate`:报错形状归 `FormRow`,不走浏览器的气泡)。
|
|
33
35
|
*/
|
|
34
|
-
export function Form(props: FormProps) {
|
|
36
|
+
export function Form<Values extends object = FormValues>(props: FormProps<Values>) {
|
|
35
37
|
const { fields, onSubmit, submit, cancel, extra, busy = false, disabled = false, problem } = props;
|
|
36
38
|
const [done, setDone] = useState(false);
|
|
37
39
|
const [reveal, setReveal] = useState(0);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ReactNode } from '../lib/react.js';
|
|
2
2
|
import { h } from '../lib/react.js';
|
|
3
3
|
import type { FormAction } from './FormActions.js';
|
|
4
|
-
import type { FormField,
|
|
4
|
+
import type { FormField, FormFieldOf, FormValues } from './FormTypes.js';
|
|
5
5
|
import { FormRow } from './FormRows.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -9,7 +9,9 @@ import { FormRow } from './FormRows.js';
|
|
|
9
9
|
* @param fields - 表单的字段清单。
|
|
10
10
|
* @returns 保序的新数组,`visible` 不给或为 `true` 的都在;全不可见时给空数组。
|
|
11
11
|
*/
|
|
12
|
-
export function visibleFieldsOf
|
|
12
|
+
export function visibleFieldsOf<Values extends object>(
|
|
13
|
+
fields: readonly FormFieldOf<Values>[],
|
|
14
|
+
): readonly FormFieldOf<Values>[] {
|
|
13
15
|
return fields.filter((field) => field.visible !== false);
|
|
14
16
|
}
|
|
15
17
|
|
|
@@ -19,7 +21,7 @@ export function visibleFieldsOf(fields: readonly FormField[]): readonly FormFiel
|
|
|
19
21
|
* @param fields - 表单的字段清单(通常是已经过滤过可见性的那份)。
|
|
20
22
|
* @returns 出问题的键的清单、按首次出现顺序去重;缺失的键一律记成空串(报的时候印成 `(空 key)`),空表给空数组。
|
|
21
23
|
*/
|
|
22
|
-
export function duplicateKeysOf(fields: readonly
|
|
24
|
+
export function duplicateKeysOf(fields: readonly { readonly key: string }[]): readonly string[] {
|
|
23
25
|
const seen = new Set<string>();
|
|
24
26
|
const duplicated = new Set<string>();
|
|
25
27
|
for (const field of fields) {
|
|
@@ -39,7 +41,7 @@ export function duplicateKeysOf(fields: readonly FormField[]): readonly string[]
|
|
|
39
41
|
* 形式与理由同 `FormActions` 里那颗多出来的 `primary`:装配的小错不该让 `CardBoundary` 把用户的整张卡变空白。
|
|
40
42
|
* @param fields - 表单的字段清单(通常是已经过滤过可见性的那份)。
|
|
41
43
|
*/
|
|
42
|
-
export function reportDuplicateKeys(fields: readonly
|
|
44
|
+
export function reportDuplicateKeys(fields: readonly { readonly key: string }[]): void {
|
|
43
45
|
const duplicated = duplicateKeysOf(fields);
|
|
44
46
|
if (duplicated.length === 0) return;
|
|
45
47
|
const named = duplicated.map((key) => (key === '' ? '(空 key)' : key));
|
|
@@ -54,10 +56,12 @@ export function reportDuplicateKeys(fields: readonly FormField[]): void {
|
|
|
54
56
|
* @param fields - 表单的字段清单;取的就是它们的 `value`,`value` 是数组的那格(多选)原样带出、不复制。
|
|
55
57
|
* @returns 键为字段 `key`、值为该格 `value` 的表;`fields` 为空时给空表,不是 `undefined`。
|
|
56
58
|
*/
|
|
57
|
-
export function formValuesOf
|
|
58
|
-
|
|
59
|
+
export function formValuesOf<Values extends object>(
|
|
60
|
+
fields: readonly FormFieldOf<Values>[],
|
|
61
|
+
): Readonly<Values> {
|
|
62
|
+
const values: Record<string, unknown> = {};
|
|
59
63
|
for (const field of fields) values[field.key] = field.value;
|
|
60
|
-
return values
|
|
64
|
+
return values as Readonly<Values>;
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
/**
|
|
@@ -68,19 +72,26 @@ export function formValuesOf(fields: readonly FormField[]): Readonly<Record<stri
|
|
|
68
72
|
* @param show - 提交过没有;false 时全给 `undefined`,true 时逐格调 `field.validate` 并按真值收。
|
|
69
73
|
* @returns 键为 `key`、值为那句文案或 `undefined` 的表;没给 `validate` 的格子也是 `undefined`。
|
|
70
74
|
*/
|
|
71
|
-
export function formProblemsOf(
|
|
72
|
-
fields: readonly
|
|
75
|
+
export function formProblemsOf<Values extends object>(
|
|
76
|
+
fields: readonly FormFieldOf<Values>[],
|
|
73
77
|
show: boolean,
|
|
74
78
|
): Readonly<Record<string, ReactNode | undefined>> {
|
|
75
79
|
const values = formValuesOf(fields);
|
|
76
80
|
const problems: Record<string, ReactNode | undefined> = {};
|
|
77
81
|
for (const field of fields) {
|
|
78
|
-
const found = show ?
|
|
82
|
+
const found = show ? problemOf(field, values) : undefined;
|
|
79
83
|
problems[field.key] = found ? found : undefined;
|
|
80
84
|
}
|
|
81
85
|
return problems;
|
|
82
86
|
}
|
|
83
87
|
|
|
88
|
+
function problemOf<K extends string, V, Vals extends object>(
|
|
89
|
+
field: FormField<K, V, Vals>,
|
|
90
|
+
values: Readonly<Vals>,
|
|
91
|
+
): ReactNode | undefined {
|
|
92
|
+
return field.validate?.(field.value, values);
|
|
93
|
+
}
|
|
94
|
+
|
|
84
95
|
/**
|
|
85
96
|
* 按字段顺序取第一处错误 —— 提交被挡住时要带进视口的就是这一格。
|
|
86
97
|
* 判的是**真值**(与 `formProblemsOf` 同一口径):假值那种"错误文案"渲染出来本来就是空的,不算错。
|
|
@@ -88,19 +99,19 @@ export function formProblemsOf(
|
|
|
88
99
|
* @param problems - `formProblemsOf` 那张表,也可以是调用点自己拼的(同样按真值判)。
|
|
89
100
|
* @returns 第一格有错的字段;一格都没错时给 `undefined`。
|
|
90
101
|
*/
|
|
91
|
-
export function formFirstProblemOf(
|
|
92
|
-
fields: readonly
|
|
102
|
+
export function formFirstProblemOf<Values extends object>(
|
|
103
|
+
fields: readonly FormFieldOf<Values>[],
|
|
93
104
|
problems: Readonly<Record<string, ReactNode | undefined>>,
|
|
94
|
-
):
|
|
105
|
+
): FormFieldOf<Values> | undefined {
|
|
95
106
|
return fields.find((field) => Boolean(problems[field.key]));
|
|
96
107
|
}
|
|
97
108
|
|
|
98
109
|
/**
|
|
99
110
|
* 提交那一拍的结果:有错就挡住(并点出第一处错在哪一格),没错就把键值表交出去。
|
|
100
111
|
*/
|
|
101
|
-
export type FormSubmitOutcome =
|
|
102
|
-
| { readonly blocked: true; readonly first:
|
|
103
|
-
| { readonly blocked: false; readonly values: Readonly<
|
|
112
|
+
export type FormSubmitOutcome<Values extends object = FormValues> =
|
|
113
|
+
| { readonly blocked: true; readonly first: FormFieldOf<Values> }
|
|
114
|
+
| { readonly blocked: false; readonly values: Readonly<Values> };
|
|
104
115
|
|
|
105
116
|
/**
|
|
106
117
|
* 算提交那一拍该干什么 —— `Form` 的提交钮点下去、以及 `<form>` 的 Enter 提交,走的都是它。
|
|
@@ -109,7 +120,9 @@ export type FormSubmitOutcome =
|
|
|
109
120
|
* @returns `blocked: true` 时带上是按字段顺序数第一格有错的字段(拿它去 `revealFirstProblem`);
|
|
110
121
|
* `blocked: false` 时带上从 `fields` 现取的键值表。空表一定给 `blocked: false` 与空 values。
|
|
111
122
|
*/
|
|
112
|
-
export function formSubmitOf
|
|
123
|
+
export function formSubmitOf<Values extends object>(
|
|
124
|
+
fields: readonly FormFieldOf<Values>[],
|
|
125
|
+
): FormSubmitOutcome<Values> {
|
|
113
126
|
const problems = formProblemsOf(fields, true);
|
|
114
127
|
const first = formFirstProblemOf(fields, problems);
|
|
115
128
|
if (first !== undefined) return { blocked: true, first };
|
|
@@ -170,8 +183,8 @@ export function formActionsOf(args: {
|
|
|
170
183
|
* @param args.busy - 提交中:每格的绑定 `disabled` 为真。
|
|
171
184
|
* @returns `FormRow` 组成的数组;没有可画的格子时给空数组。
|
|
172
185
|
*/
|
|
173
|
-
export function formRowsOf(args: {
|
|
174
|
-
readonly fields: readonly
|
|
186
|
+
export function formRowsOf<Values extends object>(args: {
|
|
187
|
+
readonly fields: readonly FormFieldOf<Values>[];
|
|
175
188
|
readonly problems: Readonly<Record<string, ReactNode | undefined>>;
|
|
176
189
|
readonly showProblem: boolean;
|
|
177
190
|
readonly busy: boolean;
|
|
@@ -183,11 +196,22 @@ export function formRowsOf(args: {
|
|
|
183
196
|
hint: field.hint,
|
|
184
197
|
problem: args.showProblem ? args.problems[field.key] : undefined,
|
|
185
198
|
layout: field.layout,
|
|
186
|
-
}, field.
|
|
187
|
-
id: `wb-form-${field.key}`,
|
|
188
|
-
name: field.key,
|
|
189
|
-
invalid: args.showProblem && Boolean(args.problems[field.key]),
|
|
190
|
-
disabled: args.busy || field.disabled === true,
|
|
191
|
-
})),
|
|
199
|
+
}, controlOf(field, args.problems, args.showProblem, args.busy)),
|
|
192
200
|
);
|
|
193
|
-
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function controlOf<K extends string, V, Vals extends object>(
|
|
204
|
+
field: FormField<K, V, Vals>,
|
|
205
|
+
problems: Readonly<Record<string, ReactNode | undefined>>,
|
|
206
|
+
showProblem: boolean,
|
|
207
|
+
busy: boolean,
|
|
208
|
+
): ReactNode {
|
|
209
|
+
return field.control({
|
|
210
|
+
id: `wb-form-${field.key}`,
|
|
211
|
+
name: field.key,
|
|
212
|
+
invalid: showProblem && Boolean(problems[field.key]),
|
|
213
|
+
disabled: busy || field.disabled === true,
|
|
214
|
+
value: field.value,
|
|
215
|
+
onChange: field.onChange,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, expectTypeOf, it } from 'vitest';
|
|
2
|
+
import type { FormFieldOf, FormProps } from './FormTypes.js';
|
|
3
|
+
|
|
4
|
+
type Draft = { readonly env: string; readonly branch?: string };
|
|
5
|
+
|
|
6
|
+
type Submitted = Parameters<FormProps<Draft>['onSubmit']>[0];
|
|
7
|
+
type EnvField = Extract<FormFieldOf<Draft>, { readonly key: 'env' }>;
|
|
8
|
+
type BranchField = Extract<FormFieldOf<Draft>, { readonly key: 'branch' }>;
|
|
9
|
+
type AcceptedKeys = Pick<FormFieldOf<Draft>, 'key'>['key'];
|
|
10
|
+
type StrangerKeyAccepted = { readonly key: 'nope' } extends Pick<FormFieldOf<Draft>, 'key'> ? true : false;
|
|
11
|
+
|
|
12
|
+
const draft: FormFieldOf<Draft> = {
|
|
13
|
+
key: 'env',
|
|
14
|
+
label: '环境',
|
|
15
|
+
value: 'stage',
|
|
16
|
+
onChange: (next) => void next.trim(),
|
|
17
|
+
control: (binding) => binding.value,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
describe('Form<Values> 把每一格的键与值钉住(这几条由 tsc 核,`vitest run` 跑的是最后那条 expect)', () => {
|
|
21
|
+
it('★ 键只取 Values 的键、值只取那一键的类型;会隐藏的键写成可选键,提交表跟着带上 undefined', () => {
|
|
22
|
+
expectTypeOf<AcceptedKeys>().toEqualTypeOf<'env' | 'branch'>();
|
|
23
|
+
expectTypeOf<StrangerKeyAccepted>().toEqualTypeOf<false>();
|
|
24
|
+
expectTypeOf<EnvField['value']>().toEqualTypeOf<string>();
|
|
25
|
+
expectTypeOf<EnvField['onChange']>().toEqualTypeOf<(next: string) => void>();
|
|
26
|
+
expectTypeOf<BranchField['value']>().toEqualTypeOf<string | undefined>();
|
|
27
|
+
expectTypeOf<Submitted>().toEqualTypeOf<Readonly<Draft>>();
|
|
28
|
+
expectTypeOf<Submitted['branch']>().toEqualTypeOf<string | undefined>();
|
|
29
|
+
expect(draft.key).toBe('env');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -2,38 +2,65 @@ import type { ReactNode } from '../lib/react.js';
|
|
|
2
2
|
import type { FormAction } from './FormActions.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* 默认档一格控件的值:单值(`TextField` / `SelectField` / `ChoiceGroup` 单选),或一组值(`ChoiceGroup` 多选)。
|
|
6
|
+
* 写了 `Form<Values>` 的类型参数之后,每一格的值类型由 `Values[那一格的键]` 定 —— 这个联合只是**没写类型参数**时的默认档。
|
|
6
7
|
*/
|
|
7
8
|
export type FormValue = string | readonly string[];
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
|
-
*
|
|
11
|
+
* 默认档的 `Values`:键是任意 `string`、值是 `FormValue` —— 不写 `Form<Values>` 的类型参数时就是它,
|
|
12
|
+
* 于是 `fields` 的键与值都不钉(老写法照旧能编),代价是每格拿到的都是那个联合、要自己收窄。
|
|
11
13
|
*/
|
|
12
|
-
export
|
|
14
|
+
export type FormValues = Readonly<Record<string, FormValue>>;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* `Form` 递给控件那一格的绑定:`id` / `name` / `invalid` / `disabled` 由 `Form` 定;
|
|
18
|
+
* `value` / `onChange` 就是这一格自己那份配置里的那两个**原样转手**(`Form` 不持有值)——
|
|
19
|
+
* 控件接上即可,别再自己接 model:值只写在字段配置里那一次。
|
|
20
|
+
*/
|
|
21
|
+
export interface FormFieldBinding<K extends string = string, V = FormValue> {
|
|
13
22
|
readonly id: string;
|
|
14
|
-
readonly name:
|
|
23
|
+
readonly name: K;
|
|
15
24
|
readonly invalid: boolean;
|
|
16
25
|
readonly disabled: boolean;
|
|
26
|
+
readonly value: V;
|
|
27
|
+
readonly onChange: (next: V) => void;
|
|
17
28
|
}
|
|
18
29
|
|
|
19
30
|
/**
|
|
20
|
-
*
|
|
31
|
+
* 一格字段的配置:键、标签、值、值怎么改(`onChange`)、怎么算错(`validate`)、控件本身 ——
|
|
32
|
+
* 排布、时机、阻断都不写在这里,那些归 `Form`。`value` 与 `onChange` 就在同一格里,`control` 从绑定直接取
|
|
33
|
+
* (`(binding) => …({ value: binding.value, onChange: binding.onChange })`),值不再有第二处。
|
|
21
34
|
* `validate` 是纯函数、同步、只读 `values`:返回**真值**才算这一格错,假值(`undefined` / `null` / `false` / 空串)
|
|
22
35
|
* 一律当"没错"—— 所以 `(value) => value.trim() === '' && '必填'` 这种用 `&&` 算文案的写法是对的,
|
|
23
36
|
* 它的返回值就是错误行那句话。
|
|
24
37
|
*/
|
|
25
|
-
export interface FormField
|
|
26
|
-
|
|
38
|
+
export interface FormField<
|
|
39
|
+
K extends string = string,
|
|
40
|
+
V = FormValue,
|
|
41
|
+
Vals extends object = FormValues,
|
|
42
|
+
> {
|
|
43
|
+
readonly key: K;
|
|
27
44
|
readonly label: ReactNode;
|
|
28
45
|
readonly hint?: ReactNode | undefined;
|
|
29
46
|
readonly layout?: 'inline' | 'stacked' | undefined;
|
|
30
|
-
readonly value:
|
|
47
|
+
readonly value: V;
|
|
48
|
+
readonly onChange: (next: V) => void;
|
|
31
49
|
readonly visible?: boolean | undefined;
|
|
32
50
|
readonly disabled?: boolean | undefined;
|
|
33
|
-
readonly validate?: (value:
|
|
34
|
-
readonly control: (binding: FormFieldBinding) => ReactNode;
|
|
51
|
+
readonly validate?: (value: V, values: Readonly<Vals>) => ReactNode | undefined;
|
|
52
|
+
readonly control: (binding: FormFieldBinding<K, V>) => ReactNode;
|
|
35
53
|
}
|
|
36
54
|
|
|
55
|
+
/**
|
|
56
|
+
* 把 `Values` 的每一键映射成一格字段 —— 写 `Form<Values>` 时 `fields` 就是这个联合的数组:
|
|
57
|
+
* `{ key: 'env', … }` 那一格的 `value` / `onChange` / `validate` 只认 `Values['env']`,
|
|
58
|
+
* `control` 拿到的绑定也只带它。要"键值都不钉"的松档就干脆别写类型参数(默认档 `FormValues`),别手写这个类型。
|
|
59
|
+
*/
|
|
60
|
+
export type FormFieldOf<Values extends object> = {
|
|
61
|
+
[K in keyof Values & string]: FormField<K, Values[K], Values>;
|
|
62
|
+
}[keyof Values & string];
|
|
63
|
+
|
|
37
64
|
/**
|
|
38
65
|
* 提交钮:文案归调用点,位置、禁用与忙态归 `Form`。
|
|
39
66
|
*/
|
|
@@ -51,15 +78,19 @@ export interface FormCancel {
|
|
|
51
78
|
}
|
|
52
79
|
|
|
53
80
|
/**
|
|
54
|
-
* `Form`
|
|
81
|
+
* `Form` 的契约:`fields` 配置进,排布、校验时机、提交阻断、操作区全归组件。
|
|
82
|
+
* `Values` 是**那张值表**的形状(不写就是默认档 `FormValues`):`fields` 的键只能取它的键,
|
|
83
|
+
* 每格的 `value` / `onChange` / `validate` 只认它那一键的值类型,`onSubmit` 与 `validate` 第二参收到的也是它。
|
|
84
|
+
* **会 `visible: false` 的键写成可选键**(`branch?: string`)—— 那一格不渲染、不校验、不进 `values`(规则五),
|
|
85
|
+
* 可选键让"它可能不在表里"这件事在类型上也成立;永远可见的键照常必填。
|
|
55
86
|
*/
|
|
56
|
-
export interface FormProps {
|
|
57
|
-
readonly fields: readonly
|
|
58
|
-
readonly onSubmit: (values: Readonly<
|
|
87
|
+
export interface FormProps<Values extends object = FormValues> {
|
|
88
|
+
readonly fields: readonly FormFieldOf<Values>[];
|
|
89
|
+
readonly onSubmit: (values: Readonly<Values>) => void;
|
|
59
90
|
readonly submit: FormSubmit;
|
|
60
91
|
readonly cancel?: FormCancel | undefined;
|
|
61
92
|
readonly extra?: readonly FormAction[] | undefined;
|
|
62
93
|
readonly busy?: boolean | undefined;
|
|
63
94
|
readonly disabled?: boolean | undefined;
|
|
64
95
|
readonly problem?: ReactNode | undefined;
|
|
65
|
-
}
|
|
96
|
+
}
|
|
@@ -7,6 +7,7 @@ function field(key: string, value: FormValue, validate?: FormField['validate']):
|
|
|
7
7
|
key,
|
|
8
8
|
label: key,
|
|
9
9
|
value,
|
|
10
|
+
onChange: () => {},
|
|
10
11
|
control: (binding) => `${key}:${binding.id}:${String(binding.invalid)}:${String(binding.disabled)}`,
|
|
11
12
|
...(validate === undefined ? {} : { validate }),
|
|
12
13
|
};
|
package/src/config/constants.ts
CHANGED
|
@@ -73,6 +73,9 @@ export const OPENCLI_MAX_CONCURRENT = 4;
|
|
|
73
73
|
/** 启动预热站点那一跳的超时(毫秒);超时按未热对待,不挡面板。 */
|
|
74
74
|
export const WARM_TIMEOUT_MS = 20_000;
|
|
75
75
|
|
|
76
|
+
/** 体检结果算「还新鲜」的时长(毫秒):期内再来的强制体检直接回上一次那份快照。 */
|
|
77
|
+
export const PREFLIGHT_FRESH_MS = 30_000;
|
|
78
|
+
|
|
76
79
|
/** 装 / 卸模块包的超时(毫秒):pnpm 与取包内容都按它算。 */
|
|
77
80
|
export const INSTALL_TIMEOUT_MS = 300_000;
|
|
78
81
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { BoardProbe } from '../../lib/board-fact.js';
|
|
3
|
+
import { boardFactOf } from '../board-fact.js';
|
|
4
|
+
|
|
5
|
+
const WORKSHOP = '/w/workbench/diy';
|
|
6
|
+
|
|
7
|
+
const probe = (over: Partial<BoardProbe> = {}): BoardProbe => ({
|
|
8
|
+
workbench: true,
|
|
9
|
+
where: WORKSHOP,
|
|
10
|
+
bar: true,
|
|
11
|
+
board: WORKSHOP,
|
|
12
|
+
open: true,
|
|
13
|
+
drawn: true,
|
|
14
|
+
...over,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('板事实:一份判据,两边共用', () => {
|
|
18
|
+
it('★★ 画着的就是工坊那块板(含子路径)⇒ workshop', () => {
|
|
19
|
+
expect(boardFactOf(probe(), WORKSHOP)).toBe('workshop');
|
|
20
|
+
expect(boardFactOf(probe({ board: `${WORKSHOP}/packages` }), WORKSHOP)).toBe('workshop');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('★★ 画的是别的项目的板 ⇒ other(前缀像但没落在里面也算 other)', () => {
|
|
24
|
+
expect(boardFactOf(probe({ board: '/w/project' }), WORKSHOP)).toBe('other');
|
|
25
|
+
expect(boardFactOf(probe({ board: `${WORKSHOP}-other` }), WORKSHOP)).toBe('other');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('★★ 那块板没画在屏上 ⇒ hidden:不挂、没声明开着、量出来不可见,三条里任一条不成立都算', () => {
|
|
29
|
+
expect(boardFactOf(probe({ bar: false }), WORKSHOP)).toBe('hidden');
|
|
30
|
+
expect(boardFactOf(probe({ open: false }), WORKSHOP)).toBe('hidden');
|
|
31
|
+
expect(boardFactOf(probe({ drawn: false }), WORKSHOP)).toBe('hidden');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('★★ 工作台没答话(旧外壳 / 夹具页)⇒ unknown:放行,但不许当成"就是工坊板"', () => {
|
|
35
|
+
expect(boardFactOf(probe({ workbench: false }), WORKSHOP)).toBe('unknown');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('★ 画着板但说不清是哪块、或不知道工坊在哪 ⇒ unknown', () => {
|
|
39
|
+
expect(boardFactOf(probe({ board: '' }), WORKSHOP)).toBe('unknown');
|
|
40
|
+
expect(boardFactOf(probe(), null)).toBe('unknown');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('★★ 顺序要紧:没画在屏上,就别去猜是哪块板', () => {
|
|
44
|
+
expect(boardFactOf(probe({ board: WORKSHOP, drawn: false }), WORKSHOP)).toBe('hidden');
|
|
45
|
+
expect(boardFactOf(probe({ workbench: false, bar: false }), WORKSHOP)).toBe('unknown');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { DEBUG_STALE_MS, createDebugStore, parseDebugIntent, type DebugPact } from '
|
|
2
|
+
import { DEBUG_STALE_MS, createDebugStore, parseDebugIntent, type DebugPact } from '../diy-debug.js';
|
|
3
3
|
|
|
4
4
|
const pact = (sessionId: string, now = 1000, unit = 'demo'): DebugPact => ({
|
|
5
5
|
sessionId,
|
|
@@ -4,9 +4,10 @@ import {
|
|
|
4
4
|
createRecordStore,
|
|
5
5
|
parseRecordIntent,
|
|
6
6
|
recordOwnerText,
|
|
7
|
+
shotDueOf,
|
|
7
8
|
type RecordPact,
|
|
8
9
|
type RecordState,
|
|
9
|
-
} from '
|
|
10
|
+
} from '../diy-record.js';
|
|
10
11
|
|
|
11
12
|
const pact = (sessionId: string, now = 1000, unit = 'demo'): RecordPact => ({
|
|
12
13
|
sessionId,
|
|
@@ -169,3 +170,138 @@ describe('用户在左栏按的三个键:意图词表只此一份', () => {
|
|
|
169
170
|
}
|
|
170
171
|
});
|
|
171
172
|
});
|
|
173
|
+
|
|
174
|
+
describe('按计数落图的台账:落成才前进,失败如实记、同一个计数只重试一次', () => {
|
|
175
|
+
const shot = (n: number, extra: Partial<RecordState['shots'][number]> = {}) => ({
|
|
176
|
+
n,
|
|
177
|
+
at: 1500 + n,
|
|
178
|
+
path: `/w/diy/shots/rec-shot-demo-${String(n)}.png`,
|
|
179
|
+
...extra,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('★ begin 时台账是空的(还没人按过「截图」)', () => {
|
|
183
|
+
const store = createRecordStore();
|
|
184
|
+
const state = held(store.begin(pact('s1', 1000)));
|
|
185
|
+
|
|
186
|
+
expect(state.shots).toEqual([]);
|
|
187
|
+
expect(state.shotsSeen).toBe(0);
|
|
188
|
+
expect(state.shotTries).toBeNull();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('★★ 落成一张 ⇒ 台账一条、shotsSeen 跟到那个计数、重试数清零', () => {
|
|
192
|
+
const store = createRecordStore();
|
|
193
|
+
store.begin(pact('s1', 1000));
|
|
194
|
+
store.shotFailed('s1', shot(1, { error: '第一次没成' }), 1501);
|
|
195
|
+
|
|
196
|
+
const state = held(store.shotTaken('s1', shot(1), 1502));
|
|
197
|
+
|
|
198
|
+
expect(state.shots).toEqual([shot(1)]);
|
|
199
|
+
expect(state.shotsSeen).toBe(1);
|
|
200
|
+
expect(state.shotTries).toBeNull();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('★★ 落失败 ⇒ 那一条带 error 原文、shotsSeen 一步不前进、同一个计数重试一次就试满了', () => {
|
|
204
|
+
const store = createRecordStore();
|
|
205
|
+
store.begin(pact('s1', 1000));
|
|
206
|
+
|
|
207
|
+
const first = held(store.shotFailed('s1', shot(1, { error: 'opencli:截不下来' }), 1501));
|
|
208
|
+
expect(first.shots).toEqual([shot(1, { error: 'opencli:截不下来' })]);
|
|
209
|
+
expect(first.shotsSeen).toBe(0);
|
|
210
|
+
expect(first.shotTries).toEqual({ n: 1, tries: 1 });
|
|
211
|
+
|
|
212
|
+
const retried = held(store.shotFailed('s1', shot(1, { error: '还是截不下来' }), 1601));
|
|
213
|
+
expect(retried.shots).toEqual([shot(1, { error: '还是截不下来' })]);
|
|
214
|
+
expect(retried.shotTries).toEqual({ n: 1, tries: 2 });
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('★★ shotDueOf:没涨不拍 / 涨了才拍 / 试满两次就放下 / 他再按一下(计数换新)重来', () => {
|
|
218
|
+
const store = createRecordStore();
|
|
219
|
+
const state = held(store.begin(pact('s1', 1000)));
|
|
220
|
+
|
|
221
|
+
expect(shotDueOf(state, 0)).toBeNull();
|
|
222
|
+
expect(shotDueOf(state, 1)).toBe(1);
|
|
223
|
+
expect(shotDueOf(state, 2.9)).toBe(2);
|
|
224
|
+
expect(shotDueOf(state, Number.NaN)).toBeNull();
|
|
225
|
+
|
|
226
|
+
const done = held(store.shotTaken('s1', shot(1), 1500));
|
|
227
|
+
expect(shotDueOf(done, 1)).toBeNull();
|
|
228
|
+
expect(shotDueOf(done, 2)).toBe(2);
|
|
229
|
+
|
|
230
|
+
const failed = held(store.shotFailed('s1', shot(2, { error: '没成' }), 1501));
|
|
231
|
+
expect(shotDueOf(failed, 2)).toBe(2);
|
|
232
|
+
|
|
233
|
+
const tried = held(store.shotFailed('s1', shot(2, { error: '还是没成' }), 1502));
|
|
234
|
+
expect(shotDueOf(tried, 2)).toBeNull();
|
|
235
|
+
expect(shotDueOf(tried, 3)).toBe(3);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('★ 台账按计数排好,且同一张只留最新那条(重试不叠行)', () => {
|
|
239
|
+
const store = createRecordStore();
|
|
240
|
+
store.begin(pact('s1', 1000));
|
|
241
|
+
store.shotTaken('s1', shot(2), 1500);
|
|
242
|
+
store.shotFailed('s1', shot(1, { error: '没成' }), 1501);
|
|
243
|
+
|
|
244
|
+
const state = held(store.shotFailed('s1', shot(1, { error: '重试也没成' }), 1502));
|
|
245
|
+
|
|
246
|
+
expect(state.shots.map((one) => one.n)).toEqual([1, 2]);
|
|
247
|
+
expect(state.shots).toHaveLength(2);
|
|
248
|
+
expect(state.shots[0]?.error).toBe('重试也没成');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('★ 台账不是谁都能写的:别人的会话 / 没有那一场,当场拒', () => {
|
|
252
|
+
const store = createRecordStore();
|
|
253
|
+
|
|
254
|
+
expect(store.shotTaken('s1', shot(1), 1000)).toEqual({ ok: false, code: 'none' });
|
|
255
|
+
expect(store.shotFailed('s1', shot(1), 1000)).toEqual({ ok: false, code: 'none' });
|
|
256
|
+
|
|
257
|
+
store.begin(pact('s1', 1000));
|
|
258
|
+
expect(store.shotTaken('s2', shot(1), 1100)).toEqual({ ok: false, code: 'not-owner' });
|
|
259
|
+
expect(store.shotFailed('s2', shot(1), 1100)).toEqual({ ok: false, code: 'not-owner' });
|
|
260
|
+
expect(store.live(1100)?.shots).toEqual([]);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
describe('门铃那一格:命中记一次,超时一个字不动', () => {
|
|
265
|
+
it('★★ begin 时 0 次;命中一次 +1,并把这一次等了多少毫秒记下', () => {
|
|
266
|
+
const store = createRecordStore();
|
|
267
|
+
const state = held(store.begin(pact('s1', 1000)));
|
|
268
|
+
|
|
269
|
+
expect(state.beats).toBe(0);
|
|
270
|
+
expect(state.beatMs).toBe(0);
|
|
271
|
+
|
|
272
|
+
const first = held(store.beat('s1', 42, 1200));
|
|
273
|
+
expect(first.beats).toBe(1);
|
|
274
|
+
expect(first.beatMs).toBe(42);
|
|
275
|
+
expect(first.at).toBe(1200);
|
|
276
|
+
|
|
277
|
+
const second = held(store.beat('s1', 7, 1300));
|
|
278
|
+
expect(second.beats).toBe(2);
|
|
279
|
+
expect(second.beatMs).toBe(7);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('★★ 跟着相位走不丢(started / finished 都保留),收工连它一起清;别人的会话不许写', () => {
|
|
283
|
+
const store = createRecordStore();
|
|
284
|
+
store.begin(pact('s1', 1000));
|
|
285
|
+
store.beat('s1', 12, 1100);
|
|
286
|
+
store.started('s1', 2000, 2001);
|
|
287
|
+
store.finished('s1', 3000, 3002);
|
|
288
|
+
|
|
289
|
+
const live = store.live(3002);
|
|
290
|
+
expect(live?.beats).toBe(1);
|
|
291
|
+
expect(live?.beatMs).toBe(12);
|
|
292
|
+
|
|
293
|
+
expect(store.beat('s2', 5, 3100)).toEqual({ ok: false, code: 'not-owner' });
|
|
294
|
+
expect(held(store.beat('s1', 5, 3200)).beats).toBe(2);
|
|
295
|
+
expect(store.stop('s1', 3300).ok).toBe(true);
|
|
296
|
+
expect(store.live(3300)).toBeNull();
|
|
297
|
+
expect(store.beat('s1', 5, 3400)).toEqual({ ok: false, code: 'none' });
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('★ 负数 / 小数收成正经毫秒(读侧那一行不出现 "-0 毫秒" 这种话)', () => {
|
|
301
|
+
const store = createRecordStore();
|
|
302
|
+
store.begin(pact('s1', 1000));
|
|
303
|
+
|
|
304
|
+
expect(held(store.beat('s1', -3, 1100)).beatMs).toBe(0);
|
|
305
|
+
expect(held(store.beat('s1', 4.7, 1200)).beatMs).toBe(4);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { isFactScope, scopePrefixOf } from '../../api/facts.js';
|
|
3
|
+
import { unitFactScope } from '../facts.js';
|
|
4
|
+
|
|
5
|
+
describe('unitFactScope', () => {
|
|
6
|
+
it('命名空间 = 模块 id + `*`("这个模块的数据全作废"只写一个字符串)', () => {
|
|
7
|
+
expect(unitFactScope('cmdb')).toBe('cmdb*');
|
|
8
|
+
expect(isFactScope('cmdb*')).toBe(true);
|
|
9
|
+
expect(isFactScope('*')).toBe(false);
|
|
10
|
+
expect(isFactScope('cmdb/summary')).toBe(false);
|
|
11
|
+
expect(scopePrefixOf('cmdb*')).toBe('cmdb');
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -140,6 +140,48 @@ describe('createLongTaskRegistry.stop(停止只是转发给生产方,认不
|
|
|
140
140
|
});
|
|
141
141
|
});
|
|
142
142
|
|
|
143
|
+
describe('createLongTaskRegistry.live(外壳侧栏那颗 loading 点读的:不分工作区,只给活着的)', () => {
|
|
144
|
+
it('★★ 两个工作区各一趟在跑 ⇒ 两趟都在;收尾的 / 停掉的不在', async () => {
|
|
145
|
+
const h = harness();
|
|
146
|
+
const runs = createLongTaskRegistry(h.deps);
|
|
147
|
+
const a = runs.start({ workspace: '/w/a', target: 'release', session: 's-1' });
|
|
148
|
+
const b = runs.start({ workspace: '/w/b', target: 'board' });
|
|
149
|
+
|
|
150
|
+
expect(runs.live().map((one) => one.id)).toEqual([a.id, b.id]);
|
|
151
|
+
expect(runs.live().map((one) => one.session)).toEqual(['s-1', undefined]);
|
|
152
|
+
|
|
153
|
+
h.done({});
|
|
154
|
+
await flush();
|
|
155
|
+
expect(runs.live().map((one) => one.id)).toEqual([a.id]);
|
|
156
|
+
|
|
157
|
+
runs.stop(a.id);
|
|
158
|
+
expect(runs.live()).toEqual([]);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('★ 要登录那种也算活着(与 longTaskDot 同一口径)', () => {
|
|
162
|
+
const h = harness();
|
|
163
|
+
const runs = createLongTaskRegistry(h.deps);
|
|
164
|
+
runs.start({ workspace: '/w/a', target: 'release' });
|
|
165
|
+
|
|
166
|
+
h.step({ kind: 'start', text: '未登录', loginUrl: 'https://x/login' });
|
|
167
|
+
|
|
168
|
+
expect(runs.live().map((one) => one.status)).toEqual(['needs-login']);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('★★ 交回的是 base 快照(工作区 / 目标 / 来源会话都在),模块那层 view 投影不参与', () => {
|
|
172
|
+
const h = harness({ view: () => ({ hidden: true }) });
|
|
173
|
+
const runs = createLongTaskRegistry(h.deps);
|
|
174
|
+
runs.start({ workspace: '/w/a', target: 'release', session: 's-1' });
|
|
175
|
+
|
|
176
|
+
expect(runs.live()[0]).toMatchObject({
|
|
177
|
+
workspace: '/w/a',
|
|
178
|
+
target: 'release',
|
|
179
|
+
session: 's-1',
|
|
180
|
+
status: 'running',
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
143
185
|
describe('sharedLongTaskRegistry(那一份跨热重载存活)', () => {
|
|
144
186
|
it('★★ 槽里那份**上一版的注册表**(四个方法、没有 stop)仍被认回:升级那次重载不重建空表', () => {
|
|
145
187
|
const scope = `test-${String(Date.now())}-${Math.random()}`;
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
createPanelStateStore,
|
|
6
6
|
parseCardsReport,
|
|
7
7
|
parseCrashReport,
|
|
8
|
-
} from '
|
|
8
|
+
} from '../panel-state.js';
|
|
9
9
|
|
|
10
10
|
function card(unit: string, route: string, phase: 'idle' | 'loading' | 'ready' | 'error', at: number) {
|
|
11
11
|
return { unit, route, phase, at };
|