@selvajs/ui 4.7.0-beta.2 → 4.7.0
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/compute/AppLayout.svelte +1 -1
- package/dist/components/compute/AppLayout.svelte.d.ts +1 -1
- package/dist/components/compute/ComputeApp.svelte +2 -2
- package/dist/components/preview/InputControl.svelte +20 -4
- package/dist/components/preview/InputControl.svelte.d.ts +6 -1
- package/dist/components/preview/TabContent.svelte +1 -1
- package/dist/components/preview/TabContent.svelte.d.ts +1 -1
- package/dist/components/preview/TabLayout.svelte +1 -1
- package/dist/components/preview/TabLayout.svelte.d.ts +1 -1
- package/dist/compute/createSolveSession.svelte.d.ts +6 -2
- package/dist/compute/createSolveSession.svelte.js +8 -2
- package/package.json +4 -4
- package/src/lib/components/compute/AppLayout.svelte +1 -1
- package/src/lib/components/compute/ComputeApp.svelte +2 -2
- package/src/lib/components/preview/InputControl.svelte +20 -4
- package/src/lib/components/preview/TabContent.svelte +1 -1
- package/src/lib/components/preview/TabLayout.svelte +1 -1
- package/src/lib/compute/createSolveSession.svelte.ts +15 -4
- package/src/lib/compute/createSolveSession.test.ts +70 -0
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
isViewerFullscreen?: boolean;
|
|
25
25
|
oncalculate?: () => void;
|
|
26
26
|
values: Record<string, unknown>;
|
|
27
|
-
onValueChange: (id: string, val: SupportedTypes) => void | Promise<void>;
|
|
27
|
+
onValueChange: (id: string, val: SupportedTypes, forceSolve?: boolean) => void | Promise<void>;
|
|
28
28
|
onLoadValues?: (values: Record<string, unknown>) => void | Promise<void>;
|
|
29
29
|
panelActions?: ActionButton[];
|
|
30
30
|
showSaveButton?: boolean;
|
|
@@ -12,7 +12,7 @@ interface Props {
|
|
|
12
12
|
isViewerFullscreen?: boolean;
|
|
13
13
|
oncalculate?: () => void;
|
|
14
14
|
values: Record<string, unknown>;
|
|
15
|
-
onValueChange: (id: string, val: SupportedTypes) => void | Promise<void>;
|
|
15
|
+
onValueChange: (id: string, val: SupportedTypes, forceSolve?: boolean) => void | Promise<void>;
|
|
16
16
|
onLoadValues?: (values: Record<string, unknown>) => void | Promise<void>;
|
|
17
17
|
panelActions?: ActionButton[];
|
|
18
18
|
showSaveButton?: boolean;
|
|
@@ -133,8 +133,8 @@
|
|
|
133
133
|
});
|
|
134
134
|
});
|
|
135
135
|
|
|
136
|
-
function handleValueChange(id: string, val: unknown) {
|
|
137
|
-
session.setValue(id, val);
|
|
136
|
+
function handleValueChange(id: string, val: unknown, forceSolve?: boolean) {
|
|
137
|
+
session.setValue(id, val, forceSolve);
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
function handleCalculate() {
|
|
@@ -33,7 +33,12 @@
|
|
|
33
33
|
item: InputLayoutItem;
|
|
34
34
|
value?: SupportedTypes;
|
|
35
35
|
displayName?: string;
|
|
36
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Commit a value. `forceSolve` requests a solve even in manual-solve mode — used for
|
|
38
|
+
* system-initiated reconciliation (e.g. pruning a vanished dynamic-list selection) where
|
|
39
|
+
* leaving the previous output on screen would misrepresent the now-changed input.
|
|
40
|
+
*/
|
|
41
|
+
onChange: (paramId: string, value: SupportedTypes, forceSolve?: boolean) => void;
|
|
37
42
|
disabled?: boolean;
|
|
38
43
|
/** Runtime-computed options for a dynamic value list input (name -> value). */
|
|
39
44
|
dynamicOptions?: Record<string, string>;
|
|
@@ -108,7 +113,10 @@
|
|
|
108
113
|
|
|
109
114
|
// When a dynamic value list recomputes, a previously-selected value may no longer be an
|
|
110
115
|
// available option. Prune the stale selection so the control shows a valid option (or empty)
|
|
111
|
-
// instead of rendering the orphaned raw value as its own label.
|
|
116
|
+
// instead of rendering the orphaned raw value as its own label. This is a system-initiated
|
|
117
|
+
// change (the user didn't pick the new option), so force a solve — otherwise manual-solve
|
|
118
|
+
// schemas would keep the prior output on screen, making it look like the auto-picked option
|
|
119
|
+
// produced it.
|
|
112
120
|
$effect(() => {
|
|
113
121
|
if (!isDynamicValueListWidget(item) || !dynamicListHasOptions) return;
|
|
114
122
|
const validValues = new Set(Object.values(dynamicListOptions));
|
|
@@ -116,9 +124,17 @@
|
|
|
116
124
|
// directly from an effect trips Svelte's binding-ownership check.
|
|
117
125
|
if (Array.isArray(value)) {
|
|
118
126
|
const pruned = value.filter((v) => typeof v === 'string' && validValues.has(v));
|
|
119
|
-
if (pruned.length !== value.length)
|
|
127
|
+
if (pruned.length !== value.length) {
|
|
128
|
+
// Fall back to first option when checklist becomes fully empty after pruning
|
|
129
|
+
onChange(
|
|
130
|
+
item.paramId,
|
|
131
|
+
pruned.length > 0 ? pruned : [Object.values(dynamicListOptions)[0]],
|
|
132
|
+
true
|
|
133
|
+
);
|
|
134
|
+
}
|
|
120
135
|
} else if (typeof value === 'string' && value && !validValues.has(value)) {
|
|
121
|
-
|
|
136
|
+
// Fall back to first available option instead of clearing to empty
|
|
137
|
+
onChange(item.paramId, Object.values(dynamicListOptions)[0], true);
|
|
122
138
|
}
|
|
123
139
|
});
|
|
124
140
|
|
|
@@ -3,7 +3,12 @@ interface Props {
|
|
|
3
3
|
item: InputLayoutItem;
|
|
4
4
|
value?: SupportedTypes;
|
|
5
5
|
displayName?: string;
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Commit a value. `forceSolve` requests a solve even in manual-solve mode — used for
|
|
8
|
+
* system-initiated reconciliation (e.g. pruning a vanished dynamic-list selection) where
|
|
9
|
+
* leaving the previous output on screen would misrepresent the now-changed input.
|
|
10
|
+
*/
|
|
11
|
+
onChange: (paramId: string, value: SupportedTypes, forceSolve?: boolean) => void;
|
|
7
12
|
disabled?: boolean;
|
|
8
13
|
/** Runtime-computed options for a dynamic value list input (name -> value). */
|
|
9
14
|
dynamicOptions?: Record<string, string>;
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
values: Record<string, unknown>;
|
|
21
21
|
collapsedGroups: Record<string, boolean>;
|
|
22
22
|
onToggleGroup: (groupId: string) => void;
|
|
23
|
-
onValueChange: (paramId: string, value: SupportedTypes) => void;
|
|
23
|
+
onValueChange: (paramId: string, value: SupportedTypes, forceSolve?: boolean) => void;
|
|
24
24
|
inputs: SchemaInput[];
|
|
25
25
|
outputs: DiscoveredOutput[];
|
|
26
26
|
/** Computed value list options keyed by the target dynamic-value-list input id. */
|
|
@@ -4,7 +4,7 @@ interface Props {
|
|
|
4
4
|
values: Record<string, unknown>;
|
|
5
5
|
collapsedGroups: Record<string, boolean>;
|
|
6
6
|
onToggleGroup: (groupId: string) => void;
|
|
7
|
-
onValueChange: (paramId: string, value: SupportedTypes) => void;
|
|
7
|
+
onValueChange: (paramId: string, value: SupportedTypes, forceSolve?: boolean) => void;
|
|
8
8
|
inputs: SchemaInput[];
|
|
9
9
|
outputs: DiscoveredOutput[];
|
|
10
10
|
/** Computed value list options keyed by the target dynamic-value-list input id. */
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
interface Props {
|
|
11
11
|
schema: UISchema;
|
|
12
12
|
values: Record<string, unknown>;
|
|
13
|
-
onValueChange: (paramId: string, value: SupportedTypes) => void;
|
|
13
|
+
onValueChange: (paramId: string, value: SupportedTypes, forceSolve?: boolean) => void;
|
|
14
14
|
/** Filter tabs by panel position. 'left' shows unpositioned + left tabs, 'right' shows right tabs only. */
|
|
15
15
|
panelFilter?: 'left' | 'right';
|
|
16
16
|
/** Externally request a specific tab to be active (e.g. from collapsed strip click) */
|
|
@@ -2,7 +2,7 @@ import type { UISchema, SupportedTypes } from '@selvajs/schemas';
|
|
|
2
2
|
interface Props {
|
|
3
3
|
schema: UISchema;
|
|
4
4
|
values: Record<string, unknown>;
|
|
5
|
-
onValueChange: (paramId: string, value: SupportedTypes) => void;
|
|
5
|
+
onValueChange: (paramId: string, value: SupportedTypes, forceSolve?: boolean) => void;
|
|
6
6
|
/** Filter tabs by panel position. 'left' shows unpositioned + left tabs, 'right' shows right tabs only. */
|
|
7
7
|
panelFilter?: 'left' | 'right';
|
|
8
8
|
/** Externally request a specific tab to be active (e.g. from collapsed strip click) */
|
|
@@ -19,8 +19,12 @@ export interface SolveSession {
|
|
|
19
19
|
readonly hasPendingChanges: boolean;
|
|
20
20
|
readonly hasNeverSolved: boolean;
|
|
21
21
|
readonly isSolving: boolean;
|
|
22
|
-
/**
|
|
23
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Records a value change and dispatches a solve unless the schema is manual-solve.
|
|
24
|
+
* `forceSolve` overrides manual mode for system-initiated reconciliation (e.g. a dynamic
|
|
25
|
+
* value list pruning a vanished selection), so the output can't lag behind the new value.
|
|
26
|
+
*/
|
|
27
|
+
setValue(id: string, value: unknown, forceSolve?: boolean): void;
|
|
24
28
|
/** Explicit "calculate" — dispatches a solve with the current values. */
|
|
25
29
|
solve(): void;
|
|
26
30
|
/** Merges incoming values, then solves (auto) or marks dirty (manual). */
|
|
@@ -46,10 +46,16 @@ export function createSolveSession(args) {
|
|
|
46
46
|
get isSolving() {
|
|
47
47
|
return args.driver.isSolving;
|
|
48
48
|
},
|
|
49
|
-
setValue(id, value) {
|
|
49
|
+
setValue(id, value, forceSolve = false) {
|
|
50
50
|
const { shouldSolve } = applyValueChange(state, id, value, currentSchema?.instanceSolve);
|
|
51
|
-
if (shouldSolve)
|
|
51
|
+
if (shouldSolve || forceSolve) {
|
|
52
|
+
// A forced solve reconciles the deferred output; clear the dirty flags it raised.
|
|
53
|
+
if (forceSolve && !shouldSolve) {
|
|
54
|
+
state.pendingValues = {};
|
|
55
|
+
state.hasPendingChanges = false;
|
|
56
|
+
}
|
|
52
57
|
dispatch();
|
|
58
|
+
}
|
|
53
59
|
},
|
|
54
60
|
solve() {
|
|
55
61
|
dispatch();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@selvajs/ui",
|
|
3
|
-
"version": "4.7.0
|
|
3
|
+
"version": "4.7.0",
|
|
4
4
|
"description": "Shared UI components and utilities for Selva applications",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"**/*.css"
|
|
38
38
|
],
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"@selvajs/compute": "^2.1.0
|
|
40
|
+
"@selvajs/compute": "^2.1.0",
|
|
41
41
|
"@sveltejs/kit": "^2",
|
|
42
42
|
"bits-ui": "^2.18.0",
|
|
43
43
|
"svelte": "^5",
|
|
@@ -71,8 +71,8 @@
|
|
|
71
71
|
"rimraf": "^6.0.1",
|
|
72
72
|
"svelte": "5.55.5",
|
|
73
73
|
"tailwind-variants": "^3.2.2",
|
|
74
|
-
"vitest": "^3.2.
|
|
75
|
-
"@selvajs/config": "0.0.
|
|
74
|
+
"vitest": "^3.2.6",
|
|
75
|
+
"@selvajs/config": "0.0.1",
|
|
76
76
|
"@selvajs/schemas": "4.5.0"
|
|
77
77
|
},
|
|
78
78
|
"scripts": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
isViewerFullscreen?: boolean;
|
|
25
25
|
oncalculate?: () => void;
|
|
26
26
|
values: Record<string, unknown>;
|
|
27
|
-
onValueChange: (id: string, val: SupportedTypes) => void | Promise<void>;
|
|
27
|
+
onValueChange: (id: string, val: SupportedTypes, forceSolve?: boolean) => void | Promise<void>;
|
|
28
28
|
onLoadValues?: (values: Record<string, unknown>) => void | Promise<void>;
|
|
29
29
|
panelActions?: ActionButton[];
|
|
30
30
|
showSaveButton?: boolean;
|
|
@@ -133,8 +133,8 @@
|
|
|
133
133
|
});
|
|
134
134
|
});
|
|
135
135
|
|
|
136
|
-
function handleValueChange(id: string, val: unknown) {
|
|
137
|
-
session.setValue(id, val);
|
|
136
|
+
function handleValueChange(id: string, val: unknown, forceSolve?: boolean) {
|
|
137
|
+
session.setValue(id, val, forceSolve);
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
function handleCalculate() {
|
|
@@ -33,7 +33,12 @@
|
|
|
33
33
|
item: InputLayoutItem;
|
|
34
34
|
value?: SupportedTypes;
|
|
35
35
|
displayName?: string;
|
|
36
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Commit a value. `forceSolve` requests a solve even in manual-solve mode — used for
|
|
38
|
+
* system-initiated reconciliation (e.g. pruning a vanished dynamic-list selection) where
|
|
39
|
+
* leaving the previous output on screen would misrepresent the now-changed input.
|
|
40
|
+
*/
|
|
41
|
+
onChange: (paramId: string, value: SupportedTypes, forceSolve?: boolean) => void;
|
|
37
42
|
disabled?: boolean;
|
|
38
43
|
/** Runtime-computed options for a dynamic value list input (name -> value). */
|
|
39
44
|
dynamicOptions?: Record<string, string>;
|
|
@@ -108,7 +113,10 @@
|
|
|
108
113
|
|
|
109
114
|
// When a dynamic value list recomputes, a previously-selected value may no longer be an
|
|
110
115
|
// available option. Prune the stale selection so the control shows a valid option (or empty)
|
|
111
|
-
// instead of rendering the orphaned raw value as its own label.
|
|
116
|
+
// instead of rendering the orphaned raw value as its own label. This is a system-initiated
|
|
117
|
+
// change (the user didn't pick the new option), so force a solve — otherwise manual-solve
|
|
118
|
+
// schemas would keep the prior output on screen, making it look like the auto-picked option
|
|
119
|
+
// produced it.
|
|
112
120
|
$effect(() => {
|
|
113
121
|
if (!isDynamicValueListWidget(item) || !dynamicListHasOptions) return;
|
|
114
122
|
const validValues = new Set(Object.values(dynamicListOptions));
|
|
@@ -116,9 +124,17 @@
|
|
|
116
124
|
// directly from an effect trips Svelte's binding-ownership check.
|
|
117
125
|
if (Array.isArray(value)) {
|
|
118
126
|
const pruned = value.filter((v) => typeof v === 'string' && validValues.has(v));
|
|
119
|
-
if (pruned.length !== value.length)
|
|
127
|
+
if (pruned.length !== value.length) {
|
|
128
|
+
// Fall back to first option when checklist becomes fully empty after pruning
|
|
129
|
+
onChange(
|
|
130
|
+
item.paramId,
|
|
131
|
+
pruned.length > 0 ? pruned : [Object.values(dynamicListOptions)[0]],
|
|
132
|
+
true
|
|
133
|
+
);
|
|
134
|
+
}
|
|
120
135
|
} else if (typeof value === 'string' && value && !validValues.has(value)) {
|
|
121
|
-
|
|
136
|
+
// Fall back to first available option instead of clearing to empty
|
|
137
|
+
onChange(item.paramId, Object.values(dynamicListOptions)[0], true);
|
|
122
138
|
}
|
|
123
139
|
});
|
|
124
140
|
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
values: Record<string, unknown>;
|
|
21
21
|
collapsedGroups: Record<string, boolean>;
|
|
22
22
|
onToggleGroup: (groupId: string) => void;
|
|
23
|
-
onValueChange: (paramId: string, value: SupportedTypes) => void;
|
|
23
|
+
onValueChange: (paramId: string, value: SupportedTypes, forceSolve?: boolean) => void;
|
|
24
24
|
inputs: SchemaInput[];
|
|
25
25
|
outputs: DiscoveredOutput[];
|
|
26
26
|
/** Computed value list options keyed by the target dynamic-value-list input id. */
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
interface Props {
|
|
11
11
|
schema: UISchema;
|
|
12
12
|
values: Record<string, unknown>;
|
|
13
|
-
onValueChange: (paramId: string, value: SupportedTypes) => void;
|
|
13
|
+
onValueChange: (paramId: string, value: SupportedTypes, forceSolve?: boolean) => void;
|
|
14
14
|
/** Filter tabs by panel position. 'left' shows unpositioned + left tabs, 'right' shows right tabs only. */
|
|
15
15
|
panelFilter?: 'left' | 'right';
|
|
16
16
|
/** Externally request a specific tab to be active (e.g. from collapsed strip click) */
|
|
@@ -35,8 +35,12 @@ export interface SolveSession {
|
|
|
35
35
|
readonly hasPendingChanges: boolean;
|
|
36
36
|
readonly hasNeverSolved: boolean;
|
|
37
37
|
readonly isSolving: boolean;
|
|
38
|
-
/**
|
|
39
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Records a value change and dispatches a solve unless the schema is manual-solve.
|
|
40
|
+
* `forceSolve` overrides manual mode for system-initiated reconciliation (e.g. a dynamic
|
|
41
|
+
* value list pruning a vanished selection), so the output can't lag behind the new value.
|
|
42
|
+
*/
|
|
43
|
+
setValue(id: string, value: unknown, forceSolve?: boolean): void;
|
|
40
44
|
/** Explicit "calculate" — dispatches a solve with the current values. */
|
|
41
45
|
solve(): void;
|
|
42
46
|
/** Merges incoming values, then solves (auto) or marks dirty (manual). */
|
|
@@ -100,9 +104,16 @@ export function createSolveSession(args: SolveSessionArgs): SolveSession {
|
|
|
100
104
|
return args.driver.isSolving;
|
|
101
105
|
},
|
|
102
106
|
|
|
103
|
-
setValue(id, value) {
|
|
107
|
+
setValue(id, value, forceSolve = false) {
|
|
104
108
|
const { shouldSolve } = applyValueChange(state, id, value, currentSchema?.instanceSolve);
|
|
105
|
-
if (shouldSolve)
|
|
109
|
+
if (shouldSolve || forceSolve) {
|
|
110
|
+
// A forced solve reconciles the deferred output; clear the dirty flags it raised.
|
|
111
|
+
if (forceSolve && !shouldSolve) {
|
|
112
|
+
state.pendingValues = {};
|
|
113
|
+
state.hasPendingChanges = false;
|
|
114
|
+
}
|
|
115
|
+
dispatch();
|
|
116
|
+
}
|
|
106
117
|
},
|
|
107
118
|
|
|
108
119
|
solve() {
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createSolveSession, type SolveDriver } from './createSolveSession.svelte';
|
|
3
|
+
import type { UISchema } from '@selvajs/schemas';
|
|
4
|
+
|
|
5
|
+
// Covers the reactive wrapper's dispatch decisions — specifically the `forceSolve` path
|
|
6
|
+
// added for dynamic-value-list reconciliation. The pure transition logic is pinned in
|
|
7
|
+
// solve-session-core.test.ts; this file pins how the rune shell turns those decisions into
|
|
8
|
+
// driver.solve() calls. Rune module, so it runs under the svelte vitest plugin.
|
|
9
|
+
|
|
10
|
+
function schema(instanceSolve?: boolean): UISchema {
|
|
11
|
+
return {
|
|
12
|
+
id: 'test',
|
|
13
|
+
name: 'Test',
|
|
14
|
+
instanceSolve,
|
|
15
|
+
inputs: [{ id: 'a', paramId: 'a', paramType: 'text', default: 'x' }],
|
|
16
|
+
outputs: [{ id: 'out' }],
|
|
17
|
+
layout: { type: 'flat', groups: [{ items: [] }] }
|
|
18
|
+
} as unknown as UISchema;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Records every solve dispatch with the snapshot of values it received.
|
|
22
|
+
function recordingDriver(): SolveDriver & { solves: Record<string, unknown>[] } {
|
|
23
|
+
const solves: Record<string, unknown>[] = [];
|
|
24
|
+
return {
|
|
25
|
+
solves,
|
|
26
|
+
solve(values) {
|
|
27
|
+
solves.push(values);
|
|
28
|
+
},
|
|
29
|
+
cancel() {},
|
|
30
|
+
get isSolving() {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('createSolveSession.setValue', () => {
|
|
37
|
+
it('auto-solve mode dispatches on every value change', () => {
|
|
38
|
+
const driver = recordingDriver();
|
|
39
|
+
const session = createSolveSession({ schema: schema(true), scopeKey: 's', driver });
|
|
40
|
+
session.setValue('a', 'y');
|
|
41
|
+
expect(driver.solves.length).toBe(1);
|
|
42
|
+
expect(driver.solves[0].a).toBe('y');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('manual mode defers: marks pending, no dispatch', () => {
|
|
46
|
+
const driver = recordingDriver();
|
|
47
|
+
const session = createSolveSession({ schema: schema(false), scopeKey: 's', driver });
|
|
48
|
+
session.setValue('a', 'y');
|
|
49
|
+
expect(driver.solves.length).toBe(0);
|
|
50
|
+
expect(session.hasPendingChanges).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('forceSolve dispatches even in manual mode and clears the dirty flags', () => {
|
|
54
|
+
const driver = recordingDriver();
|
|
55
|
+
const session = createSolveSession({ schema: schema(false), scopeKey: 's', driver });
|
|
56
|
+
// Reconcile a system-initiated change (e.g. a pruned dynamic-list selection).
|
|
57
|
+
session.setValue('a', 'reconciled', true);
|
|
58
|
+
expect(driver.solves.length).toBe(1);
|
|
59
|
+
expect(driver.solves[0].a).toBe('reconciled');
|
|
60
|
+
// The forced solve produces the matching output, so nothing is left pending.
|
|
61
|
+
expect(session.hasPendingChanges).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('forceSolve in auto mode still dispatches exactly once', () => {
|
|
65
|
+
const driver = recordingDriver();
|
|
66
|
+
const session = createSolveSession({ schema: schema(true), scopeKey: 's', driver });
|
|
67
|
+
session.setValue('a', 'y', true);
|
|
68
|
+
expect(driver.solves.length).toBe(1);
|
|
69
|
+
});
|
|
70
|
+
});
|