@tx5dr/plugin-api 1.7.12 → 2.5.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/README.md +475 -17
- package/dist/__tests__/adif.test.js +107 -0
- package/dist/__tests__/adif.test.js.map +1 -1
- package/dist/__tests__/capability-context.test.d.ts +2 -0
- package/dist/__tests__/capability-context.test.d.ts.map +1 -0
- package/dist/__tests__/capability-context.test.js +99 -0
- package/dist/__tests__/capability-context.test.js.map +1 -0
- package/dist/__tests__/testing-utils.test.js +234 -15
- package/dist/__tests__/testing-utils.test.js.map +1 -1
- package/dist/capabilities.d.ts +38 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +50 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/compatibility.d.ts +15 -0
- package/dist/compatibility.d.ts.map +1 -0
- package/dist/compatibility.js +71 -0
- package/dist/compatibility.js.map +1 -0
- package/dist/contest-logbook-ui/assets/contest-log.css +1 -0
- package/dist/contest-logbook-ui/assets/contest-log.js +40 -0
- package/dist/contest-logbook-ui/contest-log.html +13 -0
- package/dist/context.d.ts +125 -54
- package/dist/context.d.ts.map +1 -1
- package/dist/definition.d.ts +98 -21
- package/dist/definition.d.ts.map +1 -1
- package/dist/definition.js +8 -1
- package/dist/definition.js.map +1 -1
- package/dist/ft8/StandardQSOPluginRuntime.d.ts +142 -0
- package/dist/ft8/StandardQSOPluginRuntime.d.ts.map +1 -0
- package/dist/ft8/StandardQSOPluginRuntime.js +1592 -0
- package/dist/ft8/StandardQSOPluginRuntime.js.map +1 -0
- package/dist/ft8/index.d.ts +22 -0
- package/dist/ft8/index.d.ts.map +1 -0
- package/dist/ft8/index.js +176 -0
- package/dist/ft8/index.js.map +1 -0
- package/dist/helpers.d.ts +417 -132
- package/dist/helpers.d.ts.map +1 -1
- package/dist/hooks.d.ts +56 -34
- package/dist/hooks.d.ts.map +1 -1
- package/dist/host-dependencies.d.ts +101 -0
- package/dist/host-dependencies.d.ts.map +1 -1
- package/dist/index.d.ts +20 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime.d.ts +539 -13
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +9 -1
- package/dist/runtime.js.map +1 -1
- package/dist/settings.d.ts +31 -0
- package/dist/settings.d.ts.map +1 -1
- package/dist/simulation.d.ts +60 -0
- package/dist/simulation.d.ts.map +1 -0
- package/dist/simulation.js +2 -0
- package/dist/simulation.js.map +1 -0
- package/dist/sync.d.ts +98 -6
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +6 -0
- package/dist/sync.js.map +1 -1
- package/dist/testing/index.d.ts +74 -12
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +372 -105
- package/dist/testing/index.js.map +1 -1
- package/dist/toolkit/contest/CabrilloBuilder.d.ts +7 -0
- package/dist/toolkit/contest/CabrilloBuilder.d.ts.map +1 -0
- package/dist/toolkit/contest/CabrilloBuilder.js +11 -0
- package/dist/toolkit/contest/CabrilloBuilder.js.map +1 -0
- package/dist/toolkit/contest/ContestCategorySchema.d.ts +11 -0
- package/dist/toolkit/contest/ContestCategorySchema.d.ts.map +1 -0
- package/dist/toolkit/contest/ContestCategorySchema.js +6 -0
- package/dist/toolkit/contest/ContestCategorySchema.js.map +1 -0
- package/dist/toolkit/contest/ContestLogbook.d.ts +223 -0
- package/dist/toolkit/contest/ContestLogbook.d.ts.map +1 -0
- package/dist/toolkit/contest/ContestLogbook.js +845 -0
- package/dist/toolkit/contest/ContestLogbook.js.map +1 -0
- package/dist/toolkit/contest/ContestQsoEnvelopeAdapter.d.ts +28 -0
- package/dist/toolkit/contest/ContestQsoEnvelopeAdapter.d.ts.map +1 -0
- package/dist/toolkit/contest/ContestQsoEnvelopeAdapter.js +79 -0
- package/dist/toolkit/contest/ContestQsoEnvelopeAdapter.js.map +1 -0
- package/dist/toolkit/contest/ContestQsoProjector.d.ts +12 -0
- package/dist/toolkit/contest/ContestQsoProjector.d.ts.map +1 -0
- package/dist/toolkit/contest/ContestQsoProjector.js +13 -0
- package/dist/toolkit/contest/ContestQsoProjector.js.map +1 -0
- package/dist/toolkit/contest/ContestSessionNotifier.d.ts +9 -0
- package/dist/toolkit/contest/ContestSessionNotifier.d.ts.map +1 -0
- package/dist/toolkit/contest/ContestSessionNotifier.js +13 -0
- package/dist/toolkit/contest/ContestSessionNotifier.js.map +1 -0
- package/dist/toolkit/contest/ContestSessionRepository.d.ts +21 -0
- package/dist/toolkit/contest/ContestSessionRepository.d.ts.map +1 -0
- package/dist/toolkit/contest/ContestSessionRepository.js +27 -0
- package/dist/toolkit/contest/ContestSessionRepository.js.map +1 -0
- package/dist/toolkit/contest/DefaultContestSession.d.ts +105 -0
- package/dist/toolkit/contest/DefaultContestSession.d.ts.map +1 -0
- package/dist/toolkit/contest/DefaultContestSession.js +387 -0
- package/dist/toolkit/contest/DefaultContestSession.js.map +1 -0
- package/dist/toolkit/contest/DefaultContestWorkbench.d.ts +100 -0
- package/dist/toolkit/contest/DefaultContestWorkbench.d.ts.map +1 -0
- package/dist/toolkit/contest/DefaultContestWorkbench.js +33 -0
- package/dist/toolkit/contest/DefaultContestWorkbench.js.map +1 -0
- package/dist/toolkit/contest/FT8ContestDefinition.d.ts +83 -0
- package/dist/toolkit/contest/FT8ContestDefinition.d.ts.map +1 -0
- package/dist/toolkit/contest/FT8ContestDefinition.js +167 -0
- package/dist/toolkit/contest/FT8ContestDefinition.js.map +1 -0
- package/dist/toolkit/contest/FT8ContestModules.d.ts +178 -0
- package/dist/toolkit/contest/FT8ContestModules.d.ts.map +1 -0
- package/dist/toolkit/contest/FT8ContestModules.js +247 -0
- package/dist/toolkit/contest/FT8ContestModules.js.map +1 -0
- package/dist/toolkit/contest/FT8ContestPlugin.d.ts +55 -0
- package/dist/toolkit/contest/FT8ContestPlugin.d.ts.map +1 -0
- package/dist/toolkit/contest/FT8ContestPlugin.js +322 -0
- package/dist/toolkit/contest/FT8ContestPlugin.js.map +1 -0
- package/dist/toolkit/contest/FT8ContestTestKit.d.ts +12 -0
- package/dist/toolkit/contest/FT8ContestTestKit.d.ts.map +1 -0
- package/dist/toolkit/contest/FT8ContestTestKit.js +60 -0
- package/dist/toolkit/contest/FT8ContestTestKit.js.map +1 -0
- package/dist/toolkit/contest/contest-toolkit.test.d.ts +2 -0
- package/dist/toolkit/contest/contest-toolkit.test.d.ts.map +1 -0
- package/dist/toolkit/contest/contest-toolkit.test.js +1196 -0
- package/dist/toolkit/contest/contest-toolkit.test.js.map +1 -0
- package/dist/toolkit/contest/index.d.ts +14 -0
- package/dist/toolkit/contest/index.d.ts.map +1 -0
- package/dist/toolkit/contest/index.js +14 -0
- package/dist/toolkit/contest/index.js.map +1 -0
- package/dist/toolkit/index.d.ts +3 -0
- package/dist/toolkit/index.d.ts.map +1 -0
- package/dist/toolkit/index.js +3 -0
- package/dist/toolkit/index.js.map +1 -0
- package/dist/toolkit/parallel-qso/AuthorizationLease.d.ts +15 -0
- package/dist/toolkit/parallel-qso/AuthorizationLease.d.ts.map +1 -0
- package/dist/toolkit/parallel-qso/AuthorizationLease.js +25 -0
- package/dist/toolkit/parallel-qso/AuthorizationLease.js.map +1 -0
- package/dist/toolkit/parallel-qso/BoundedCallSessionController.d.ts +40 -0
- package/dist/toolkit/parallel-qso/BoundedCallSessionController.d.ts.map +1 -0
- package/dist/toolkit/parallel-qso/BoundedCallSessionController.js +81 -0
- package/dist/toolkit/parallel-qso/BoundedCallSessionController.js.map +1 -0
- package/dist/toolkit/parallel-qso/BoundedCallSessionController.test.d.ts +2 -0
- package/dist/toolkit/parallel-qso/BoundedCallSessionController.test.d.ts.map +1 -0
- package/dist/toolkit/parallel-qso/BoundedCallSessionController.test.js +31 -0
- package/dist/toolkit/parallel-qso/BoundedCallSessionController.test.js.map +1 -0
- package/dist/toolkit/parallel-qso/CompletionTracker.d.ts +20 -0
- package/dist/toolkit/parallel-qso/CompletionTracker.d.ts.map +1 -0
- package/dist/toolkit/parallel-qso/CompletionTracker.js +20 -0
- package/dist/toolkit/parallel-qso/CompletionTracker.js.map +1 -0
- package/dist/toolkit/parallel-qso/LaneFrequencyController.d.ts +19 -0
- package/dist/toolkit/parallel-qso/LaneFrequencyController.d.ts.map +1 -0
- package/dist/toolkit/parallel-qso/LaneFrequencyController.js +23 -0
- package/dist/toolkit/parallel-qso/LaneFrequencyController.js.map +1 -0
- package/dist/toolkit/parallel-qso/ParallelQSOCoordinator.d.ts +134 -0
- package/dist/toolkit/parallel-qso/ParallelQSOCoordinator.d.ts.map +1 -0
- package/dist/toolkit/parallel-qso/ParallelQSOCoordinator.js +554 -0
- package/dist/toolkit/parallel-qso/ParallelQSOCoordinator.js.map +1 -0
- package/dist/toolkit/parallel-qso/ParallelQSOCoordinator.test.d.ts +2 -0
- package/dist/toolkit/parallel-qso/ParallelQSOCoordinator.test.d.ts.map +1 -0
- package/dist/toolkit/parallel-qso/ParallelQSOCoordinator.test.js +470 -0
- package/dist/toolkit/parallel-qso/ParallelQSOCoordinator.test.js.map +1 -0
- package/dist/toolkit/parallel-qso/PostCompletionRecoveryLease.d.ts +20 -0
- package/dist/toolkit/parallel-qso/PostCompletionRecoveryLease.d.ts.map +1 -0
- package/dist/toolkit/parallel-qso/PostCompletionRecoveryLease.js +29 -0
- package/dist/toolkit/parallel-qso/PostCompletionRecoveryLease.js.map +1 -0
- package/dist/toolkit/parallel-qso/ProtocolLane.d.ts +50 -0
- package/dist/toolkit/parallel-qso/ProtocolLane.d.ts.map +1 -0
- package/dist/toolkit/parallel-qso/ProtocolLane.js +2 -0
- package/dist/toolkit/parallel-qso/ProtocolLane.js.map +1 -0
- package/dist/toolkit/parallel-qso/controllers.test.d.ts +2 -0
- package/dist/toolkit/parallel-qso/controllers.test.d.ts.map +1 -0
- package/dist/toolkit/parallel-qso/controllers.test.js +36 -0
- package/dist/toolkit/parallel-qso/controllers.test.js.map +1 -0
- package/dist/toolkit/parallel-qso/index.d.ts +8 -0
- package/dist/toolkit/parallel-qso/index.d.ts.map +1 -0
- package/dist/toolkit/parallel-qso/index.js +8 -0
- package/dist/toolkit/parallel-qso/index.js.map +1 -0
- package/dist/utils/adif.d.ts +1 -1
- package/dist/utils/adif.d.ts.map +1 -1
- package/dist/utils/adif.js +96 -51
- package/dist/utils/adif.js.map +1 -1
- package/dist/utils/qso-text-fields.d.ts +7 -1
- package/dist/utils/qso-text-fields.d.ts.map +1 -1
- package/dist/utils/qso-text-fields.js +74 -6
- package/dist/utils/qso-text-fields.js.map +1 -1
- package/package.json +28 -6
- package/src/bridge.d.ts +3 -0
- package/tokens.css +51 -0
package/dist/testing/index.js
CHANGED
|
@@ -39,21 +39,49 @@ function createMockHostDependencies() {
|
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
41
|
// ===== Factory: KVStore =====
|
|
42
|
+
function cloneJsonValue(value) {
|
|
43
|
+
const serialized = JSON.stringify(value);
|
|
44
|
+
if (serialized === undefined)
|
|
45
|
+
return undefined;
|
|
46
|
+
return JSON.parse(serialized);
|
|
47
|
+
}
|
|
48
|
+
function cloneStructuredValue(value) {
|
|
49
|
+
if (Buffer.isBuffer(value))
|
|
50
|
+
return Buffer.from(value);
|
|
51
|
+
return structuredClone(value);
|
|
52
|
+
}
|
|
42
53
|
export function createMockKVStore(initial) {
|
|
43
|
-
const
|
|
54
|
+
const clonedInitial = initial ? cloneJsonValue(initial) : {};
|
|
55
|
+
const data = new Map(Object.entries(clonedInitial));
|
|
44
56
|
return {
|
|
45
57
|
_data: data,
|
|
46
58
|
get(key, defaultValue) {
|
|
47
|
-
return (data.has(key) ? data.get(key) : defaultValue);
|
|
59
|
+
return (data.has(key) ? cloneJsonValue(data.get(key)) : defaultValue);
|
|
48
60
|
},
|
|
49
61
|
set(key, value) {
|
|
50
|
-
|
|
62
|
+
const clonedEntry = cloneJsonValue({ [key]: value });
|
|
63
|
+
if (!Object.prototype.hasOwnProperty.call(clonedEntry, key)) {
|
|
64
|
+
data.delete(key);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
data.set(key, clonedEntry[key]);
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
update(key, reducer) {
|
|
71
|
+
const current = data.has(key) ? cloneJsonValue(data.get(key)) : undefined;
|
|
72
|
+
const next = reducer(current);
|
|
73
|
+
const clonedEntry = cloneJsonValue({ [key]: next });
|
|
74
|
+
if (!Object.prototype.hasOwnProperty.call(clonedEntry, key))
|
|
75
|
+
data.delete(key);
|
|
76
|
+
else
|
|
77
|
+
data.set(key, clonedEntry[key]);
|
|
78
|
+
return data.has(key) ? cloneJsonValue(data.get(key)) : undefined;
|
|
51
79
|
},
|
|
52
80
|
delete(key) {
|
|
53
81
|
data.delete(key);
|
|
54
82
|
},
|
|
55
83
|
getAll() {
|
|
56
|
-
return Object.fromEntries(data);
|
|
84
|
+
return cloneJsonValue(Object.fromEntries(data));
|
|
57
85
|
},
|
|
58
86
|
async flush() {
|
|
59
87
|
// no-op in mock
|
|
@@ -95,7 +123,7 @@ export function createMockTimers() {
|
|
|
95
123
|
},
|
|
96
124
|
};
|
|
97
125
|
}
|
|
98
|
-
// ===== Factory:
|
|
126
|
+
// ===== Factory: Operator snapshot and commands =====
|
|
99
127
|
const DEFAULT_MODE = {
|
|
100
128
|
name: 'FT8',
|
|
101
129
|
slotMs: 15000,
|
|
@@ -104,79 +132,125 @@ const DEFAULT_MODE = {
|
|
|
104
132
|
transmitTiming: 1180,
|
|
105
133
|
encodeAdvance: 400,
|
|
106
134
|
};
|
|
107
|
-
export function
|
|
135
|
+
export function createMockOperatorSnapshot(overrides) {
|
|
136
|
+
const mode = cloneStructuredValue(overrides?.mode ?? DEFAULT_MODE);
|
|
137
|
+
const transmitCycles = cloneStructuredValue(overrides?.transmitCycles ?? [0]);
|
|
138
|
+
const automation = cloneStructuredValue(overrides?.automation ?? null);
|
|
139
|
+
const getOtherOperators = overrides?.getOtherOperators ?? (() => []);
|
|
108
140
|
return {
|
|
109
|
-
id: 'operator-0',
|
|
110
|
-
isTransmitting: false,
|
|
111
|
-
callsign: 'W1AW',
|
|
112
|
-
grid: 'FN31',
|
|
113
|
-
frequency: 1500,
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
notifyStateChanged() { },
|
|
134
|
-
...overrides,
|
|
141
|
+
id: overrides?.id ?? 'operator-0',
|
|
142
|
+
isTransmitting: overrides?.isTransmitting ?? false,
|
|
143
|
+
callsign: overrides?.callsign ?? 'W1AW',
|
|
144
|
+
grid: overrides?.grid ?? 'FN31',
|
|
145
|
+
frequency: overrides?.frequency ?? 1500,
|
|
146
|
+
maxConcurrentStreams: overrides?.maxConcurrentStreams ?? 3,
|
|
147
|
+
get mode() { return cloneStructuredValue(mode); },
|
|
148
|
+
get transmitCycles() { return cloneStructuredValue(transmitCycles); },
|
|
149
|
+
get automation() { return cloneStructuredValue(automation); },
|
|
150
|
+
getOtherOperators: () => cloneStructuredValue(getOtherOperators()),
|
|
151
|
+
hasWorkedCallsign: overrides?.hasWorkedCallsign ?? (async () => false),
|
|
152
|
+
isTargetBeingWorkedByOthers: overrides?.isTargetBeingWorkedByOthers ?? (() => false),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
export function createMockOperatorCommandPort(submit) {
|
|
156
|
+
const commands = [];
|
|
157
|
+
return {
|
|
158
|
+
_commands: commands,
|
|
159
|
+
async submit(command) {
|
|
160
|
+
const snapshot = cloneStructuredValue(command);
|
|
161
|
+
commands.push(snapshot);
|
|
162
|
+
await submit?.(cloneStructuredValue(snapshot));
|
|
163
|
+
return { epoch: commands.length, outcome: 'completed' };
|
|
164
|
+
},
|
|
135
165
|
};
|
|
136
166
|
}
|
|
137
167
|
// ===== Factory: RadioControl =====
|
|
138
|
-
export function
|
|
139
|
-
const
|
|
140
|
-
|
|
168
|
+
export function createMockRadioView(overrides) {
|
|
169
|
+
const mode = cloneStructuredValue(overrides?.mode ?? {
|
|
170
|
+
engineMode: 'digital',
|
|
171
|
+
mode: 'FT8',
|
|
172
|
+
radioMode: 'USB',
|
|
173
|
+
descriptor: DEFAULT_MODE,
|
|
174
|
+
});
|
|
141
175
|
return {
|
|
142
|
-
frequency: 14074000,
|
|
143
|
-
band: '20m',
|
|
144
|
-
mode
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
176
|
+
frequency: overrides?.frequency ?? 14074000,
|
|
177
|
+
band: overrides?.band ?? '20m',
|
|
178
|
+
get mode() { return cloneStructuredValue(mode); },
|
|
179
|
+
isConnected: overrides?.isConnected ?? true,
|
|
180
|
+
isSimulation: overrides?.isSimulation ?? false,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
export function createMockRadioCapabilitiesView() {
|
|
184
|
+
const snapshot = { descriptors: [], capabilities: [] };
|
|
185
|
+
return {
|
|
186
|
+
getSnapshot: () => cloneStructuredValue(snapshot),
|
|
187
|
+
getState: (id) => cloneStructuredValue(snapshot.capabilities.find((capability) => capability.id === id) ?? null),
|
|
188
|
+
refresh: async () => cloneStructuredValue(snapshot),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
export function createMockRadioCommandPort() {
|
|
192
|
+
return { submit: async () => { } };
|
|
193
|
+
}
|
|
194
|
+
export function createMockRadioTunerCommandPort() {
|
|
195
|
+
return { submit: async () => { } };
|
|
196
|
+
}
|
|
197
|
+
export function createMockRadioPowerView() {
|
|
198
|
+
const state = { state: 'awake', stage: 'idle' };
|
|
199
|
+
return {
|
|
200
|
+
getSupport: async (profileId = 'mock-profile') => ({
|
|
201
|
+
profileId,
|
|
202
|
+
canPowerOn: true,
|
|
203
|
+
canPowerOff: true,
|
|
204
|
+
supportedStates: ['off', 'standby', 'operate'],
|
|
205
|
+
}),
|
|
206
|
+
getState: () => cloneStructuredValue(state),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
export function createMockRadioPowerCommandPort() {
|
|
210
|
+
return {
|
|
211
|
+
submit: async (command) => ({
|
|
212
|
+
success: true,
|
|
213
|
+
target: command.state,
|
|
214
|
+
state: command.state === 'off' ? 'off' : 'awake',
|
|
215
|
+
}),
|
|
169
216
|
};
|
|
170
217
|
}
|
|
171
218
|
// ===== Factory: LogbookAccess =====
|
|
172
219
|
export function createMockLogbookAccess(overrides) {
|
|
220
|
+
const commit = async (record) => cloneStructuredValue(record);
|
|
221
|
+
const update = async (qsoId, updates) => commit({
|
|
222
|
+
callsign: updates.callsign ?? 'N0CALL',
|
|
223
|
+
frequency: updates.frequency ?? 14_074_000,
|
|
224
|
+
mode: updates.mode ?? 'FT8',
|
|
225
|
+
startTime: updates.startTime ?? 0,
|
|
226
|
+
messageHistory: updates.messageHistory ?? [],
|
|
227
|
+
...updates,
|
|
228
|
+
id: qsoId,
|
|
229
|
+
});
|
|
230
|
+
const readQsoSnapshot = async () => ({ revision: 'mock-revision', records: [] });
|
|
231
|
+
const applyQsoBatch = async (mutations) => ({
|
|
232
|
+
revision: 'mock-revision',
|
|
233
|
+
outcomes: await Promise.all(mutations.map(async (mutation, inputIndex) => {
|
|
234
|
+
if (mutation.type === 'add') {
|
|
235
|
+
return { inputIndex, status: 'added', record: await commit(mutation.record) };
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
inputIndex,
|
|
239
|
+
status: 'updated',
|
|
240
|
+
record: await update(mutation.qsoId, mutation.updates),
|
|
241
|
+
};
|
|
242
|
+
})),
|
|
243
|
+
});
|
|
173
244
|
const callsignAccess = {
|
|
174
245
|
callsign: 'N0CALL',
|
|
175
246
|
getLogBookId: async () => 'logbook-N0CALL',
|
|
247
|
+
awaitReady: async () => { },
|
|
176
248
|
queryQSOs: async () => [],
|
|
249
|
+
readQsoSnapshot,
|
|
177
250
|
countQSOs: async () => 0,
|
|
178
|
-
addQSO:
|
|
179
|
-
updateQSO:
|
|
251
|
+
addQSO: commit,
|
|
252
|
+
updateQSO: update,
|
|
253
|
+
applyQsoBatch,
|
|
180
254
|
getStatistics: async () => null,
|
|
181
255
|
notifyUpdated: async () => { },
|
|
182
256
|
};
|
|
@@ -185,10 +259,12 @@ export function createMockLogbookAccess(overrides) {
|
|
|
185
259
|
hasWorkedDXCC: async () => false,
|
|
186
260
|
hasWorkedGrid: async () => false,
|
|
187
261
|
queryQSOs: async () => [],
|
|
262
|
+
readQsoSnapshot,
|
|
188
263
|
countQSOs: async () => 0,
|
|
189
264
|
forCallsign: () => callsignAccess,
|
|
190
|
-
addQSO:
|
|
191
|
-
updateQSO:
|
|
265
|
+
addQSO: commit,
|
|
266
|
+
updateQSO: update,
|
|
267
|
+
applyQsoBatch,
|
|
192
268
|
notifyUpdated: async () => { },
|
|
193
269
|
...overrides,
|
|
194
270
|
};
|
|
@@ -206,33 +282,38 @@ export function createMockBandAccess(overrides) {
|
|
|
206
282
|
// ===== Factory: UIBridge =====
|
|
207
283
|
export function createMockUIBridge() {
|
|
208
284
|
const sentData = new Map();
|
|
285
|
+
const events = [];
|
|
209
286
|
return {
|
|
210
287
|
_sentData: sentData,
|
|
288
|
+
_events: events,
|
|
211
289
|
send(panelId, data) {
|
|
212
290
|
const existing = sentData.get(panelId) ?? [];
|
|
213
|
-
existing.push(data);
|
|
291
|
+
existing.push(cloneJsonValue(data));
|
|
214
292
|
sentData.set(panelId, existing);
|
|
215
293
|
},
|
|
216
|
-
setPanelMeta(
|
|
217
|
-
|
|
294
|
+
setPanelMeta(panelId, meta) {
|
|
295
|
+
events.push({ type: 'panel-meta', id: panelId, data: cloneJsonValue(meta) });
|
|
218
296
|
},
|
|
219
|
-
setPanelContributions(
|
|
220
|
-
|
|
297
|
+
setPanelContributions(groupId, panels) {
|
|
298
|
+
events.push({ type: 'panel-contributions', id: groupId, data: cloneJsonValue(panels) });
|
|
221
299
|
},
|
|
222
|
-
clearPanelContributions(
|
|
223
|
-
|
|
300
|
+
clearPanelContributions(groupId) {
|
|
301
|
+
events.push({ type: 'panel-contributions', id: groupId, data: [] });
|
|
302
|
+
},
|
|
303
|
+
refreshOperatorProjection() {
|
|
304
|
+
events.push({ type: 'operator-projection-refresh', id: 'operator' });
|
|
224
305
|
},
|
|
225
306
|
registerPageHandler(_handler) {
|
|
226
307
|
// no-op in mock
|
|
227
308
|
},
|
|
228
|
-
pushToSession(
|
|
229
|
-
|
|
309
|
+
pushToSession(pageSessionId, action, data) {
|
|
310
|
+
events.push({ type: 'session-push', id: `${pageSessionId}:${action}`, data: cloneJsonValue(data) });
|
|
230
311
|
},
|
|
231
312
|
listActivePageSessions(_pageId) {
|
|
232
313
|
return [];
|
|
233
314
|
},
|
|
234
|
-
pushToPage(
|
|
235
|
-
|
|
315
|
+
pushToPage(pageId, action, data) {
|
|
316
|
+
events.push({ type: 'page-push', id: `${pageId}:${action}`, data: cloneJsonValue(data) });
|
|
236
317
|
},
|
|
237
318
|
};
|
|
238
319
|
}
|
|
@@ -240,8 +321,11 @@ export function createMockUIBridge() {
|
|
|
240
321
|
export function createMockFileStore() {
|
|
241
322
|
const storage = new Map();
|
|
242
323
|
return {
|
|
243
|
-
async write(p, data) { storage.set(p, data); },
|
|
244
|
-
async read(p) {
|
|
324
|
+
async write(p, data) { storage.set(p, Buffer.from(data)); },
|
|
325
|
+
async read(p) {
|
|
326
|
+
const data = storage.get(p);
|
|
327
|
+
return data ? Buffer.from(data) : null;
|
|
328
|
+
},
|
|
245
329
|
async delete(p) { return storage.delete(p); },
|
|
246
330
|
async list(prefix) {
|
|
247
331
|
const keys = Array.from(storage.keys());
|
|
@@ -268,7 +352,9 @@ export function createMockNetworkControl() {
|
|
|
268
352
|
async _emitMessage(data, remote) {
|
|
269
353
|
if (!messageHandler)
|
|
270
354
|
return;
|
|
271
|
-
const payload = typeof data === 'string'
|
|
355
|
+
const payload = typeof data === 'string'
|
|
356
|
+
? Buffer.from(data, 'utf8')
|
|
357
|
+
: new Uint8Array(data);
|
|
272
358
|
await messageHandler(payload, {
|
|
273
359
|
address: remote?.address ?? '127.0.0.1',
|
|
274
360
|
port: remote?.port ?? 2237,
|
|
@@ -283,7 +369,11 @@ export function createMockNetworkControl() {
|
|
|
283
369
|
binds.push(options);
|
|
284
370
|
},
|
|
285
371
|
async send(data, port, host) {
|
|
286
|
-
sent.push({
|
|
372
|
+
sent.push({
|
|
373
|
+
data: typeof data === 'string' ? data : new Uint8Array(data),
|
|
374
|
+
port,
|
|
375
|
+
host,
|
|
376
|
+
});
|
|
287
377
|
},
|
|
288
378
|
onMessage(handler) {
|
|
289
379
|
messageHandler = handler;
|
|
@@ -304,6 +394,77 @@ export function createMockNetworkControl() {
|
|
|
304
394
|
},
|
|
305
395
|
};
|
|
306
396
|
}
|
|
397
|
+
/**
|
|
398
|
+
* Creates a mock {@link PluginEventBus} for unit testing plugin code.
|
|
399
|
+
*
|
|
400
|
+
* All published messages are recorded in `_published` for assertion.
|
|
401
|
+
* The internal `_subscriptions` map lets tests inspect active subscriptions.
|
|
402
|
+
*
|
|
403
|
+
* @param options - Optional configuration for publisher metadata.
|
|
404
|
+
* @returns A mock event bus with inspection helpers.
|
|
405
|
+
*
|
|
406
|
+
* @example
|
|
407
|
+
* ```ts
|
|
408
|
+
* const bus = createMockEventBus({ owner: { pluginName: 'my-plugin' } });
|
|
409
|
+
* bus.subscribe('topic', handler);
|
|
410
|
+
* bus.publish('topic', { value: 42 });
|
|
411
|
+
* expect(bus._published).toHaveLength(1);
|
|
412
|
+
* expect(bus._published[0].publisher.pluginName).toBe('my-plugin');
|
|
413
|
+
* ```
|
|
414
|
+
*/
|
|
415
|
+
export function createMockEventBus(options) {
|
|
416
|
+
const subscriptions = new Map();
|
|
417
|
+
const published = [];
|
|
418
|
+
const ownerName = options?.owner?.pluginName ?? 'mock-plugin';
|
|
419
|
+
const ownerScope = options?.owner?.instanceScope ?? 'operator';
|
|
420
|
+
const ownerId = ownerScope === 'operator'
|
|
421
|
+
? (options?.owner?.operatorId ?? 'operator-0')
|
|
422
|
+
: undefined;
|
|
423
|
+
return {
|
|
424
|
+
_subscriptions: subscriptions,
|
|
425
|
+
_published: published,
|
|
426
|
+
publish(topic, payload) {
|
|
427
|
+
const message = cloneStructuredValue({
|
|
428
|
+
topic,
|
|
429
|
+
payload: cloneStructuredValue(payload),
|
|
430
|
+
timestamp: Date.now(),
|
|
431
|
+
publisher: {
|
|
432
|
+
pluginName: ownerName,
|
|
433
|
+
instanceScope: ownerScope,
|
|
434
|
+
operatorId: ownerId,
|
|
435
|
+
},
|
|
436
|
+
});
|
|
437
|
+
published.push(cloneStructuredValue(message));
|
|
438
|
+
const handlers = [...(subscriptions.get(topic) ?? [])];
|
|
439
|
+
for (const handler of handlers) {
|
|
440
|
+
try {
|
|
441
|
+
void Promise.resolve(handler(cloneStructuredValue(message))).catch(() => undefined);
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
// Match the production bus: subscriber failures do not reach publishers.
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
subscribe(topic, handler) {
|
|
449
|
+
const handlers = subscriptions.get(topic) ?? [];
|
|
450
|
+
if (!handlers.includes(handler)) {
|
|
451
|
+
handlers.push(handler);
|
|
452
|
+
subscriptions.set(topic, handlers);
|
|
453
|
+
}
|
|
454
|
+
return () => {
|
|
455
|
+
const current = subscriptions.get(topic);
|
|
456
|
+
if (!current)
|
|
457
|
+
return;
|
|
458
|
+
const next = current.filter((candidate) => candidate !== handler);
|
|
459
|
+
if (next.length === 0) {
|
|
460
|
+
subscriptions.delete(topic);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
subscriptions.set(topic, next);
|
|
464
|
+
};
|
|
465
|
+
},
|
|
466
|
+
};
|
|
467
|
+
}
|
|
307
468
|
// ===== Factory: HostSettingsControl =====
|
|
308
469
|
export function createMockHostSettingsControl(overrides) {
|
|
309
470
|
const ft8 = {
|
|
@@ -337,33 +498,58 @@ export function createMockHostSettingsControl(overrides) {
|
|
|
337
498
|
const ntp = { servers: ['pool.ntp.org'], defaultServers: ['pool.ntp.org'] };
|
|
338
499
|
return {
|
|
339
500
|
ft8: {
|
|
340
|
-
async get() { return ft8; },
|
|
341
|
-
async update(patch) {
|
|
501
|
+
async get() { return cloneStructuredValue(ft8); },
|
|
502
|
+
async update(patch) {
|
|
503
|
+
Object.assign(ft8, cloneStructuredValue(patch));
|
|
504
|
+
return cloneStructuredValue(ft8);
|
|
505
|
+
},
|
|
342
506
|
},
|
|
343
507
|
decodeWindows: {
|
|
344
|
-
async get() { return decodeWindows; },
|
|
345
|
-
async update(settings) {
|
|
508
|
+
async get() { return cloneStructuredValue(decodeWindows); },
|
|
509
|
+
async update(settings) {
|
|
510
|
+
Object.assign(decodeWindows, cloneStructuredValue(settings));
|
|
511
|
+
return cloneStructuredValue(decodeWindows);
|
|
512
|
+
},
|
|
346
513
|
},
|
|
347
514
|
realtime: {
|
|
348
|
-
async get() { return realtime; },
|
|
349
|
-
async update(settings) {
|
|
515
|
+
async get() { return cloneStructuredValue(realtime); },
|
|
516
|
+
async update(settings) {
|
|
517
|
+
Object.assign(realtime, cloneStructuredValue(settings));
|
|
518
|
+
return cloneStructuredValue(realtime);
|
|
519
|
+
},
|
|
350
520
|
},
|
|
351
521
|
frequencyPresets: {
|
|
352
|
-
async get() { return frequencyPresets; },
|
|
353
|
-
async update(presets) {
|
|
354
|
-
|
|
522
|
+
async get() { return cloneStructuredValue(frequencyPresets); },
|
|
523
|
+
async update(presets) {
|
|
524
|
+
frequencyPresets.presets = cloneStructuredValue(presets);
|
|
525
|
+
frequencyPresets.isCustomized = true;
|
|
526
|
+
return cloneStructuredValue(frequencyPresets);
|
|
527
|
+
},
|
|
528
|
+
async reset() {
|
|
529
|
+
frequencyPresets.isCustomized = false;
|
|
530
|
+
return cloneStructuredValue(frequencyPresets);
|
|
531
|
+
},
|
|
355
532
|
},
|
|
356
533
|
station: {
|
|
357
|
-
async get() { return station; },
|
|
358
|
-
async update(patch) {
|
|
534
|
+
async get() { return cloneStructuredValue(station); },
|
|
535
|
+
async update(patch) {
|
|
536
|
+
Object.assign(station, cloneStructuredValue(patch));
|
|
537
|
+
return cloneStructuredValue(station);
|
|
538
|
+
},
|
|
359
539
|
},
|
|
360
540
|
pskReporter: {
|
|
361
|
-
async get() { return pskReporter; },
|
|
362
|
-
async update(patch) {
|
|
541
|
+
async get() { return cloneStructuredValue(pskReporter); },
|
|
542
|
+
async update(patch) {
|
|
543
|
+
Object.assign(pskReporter, cloneStructuredValue(patch));
|
|
544
|
+
return cloneStructuredValue(pskReporter);
|
|
545
|
+
},
|
|
363
546
|
},
|
|
364
547
|
ntp: {
|
|
365
|
-
async get() { return ntp; },
|
|
366
|
-
async update(request) {
|
|
548
|
+
async get() { return cloneStructuredValue(ntp); },
|
|
549
|
+
async update(request) {
|
|
550
|
+
ntp.servers = cloneStructuredValue(request.servers);
|
|
551
|
+
return cloneStructuredValue(ntp);
|
|
552
|
+
},
|
|
367
553
|
},
|
|
368
554
|
...overrides,
|
|
369
555
|
};
|
|
@@ -378,7 +564,7 @@ export function createMockContext(options) {
|
|
|
378
564
|
const mode = opts.mode
|
|
379
565
|
? { ...DEFAULT_MODE, ...opts.mode }
|
|
380
566
|
: DEFAULT_MODE;
|
|
381
|
-
const operator =
|
|
567
|
+
const operator = createMockOperatorSnapshot({
|
|
382
568
|
id: opts.operatorId ?? 'operator-0',
|
|
383
569
|
callsign: opts.callsign ?? 'W1AW',
|
|
384
570
|
grid: opts.grid ?? 'FN31',
|
|
@@ -386,33 +572,114 @@ export function createMockContext(options) {
|
|
|
386
572
|
mode,
|
|
387
573
|
...opts.operator,
|
|
388
574
|
});
|
|
389
|
-
const radio =
|
|
575
|
+
const radio = createMockRadioView(opts.radio);
|
|
390
576
|
const logbook = createMockLogbookAccess(opts.logbook);
|
|
577
|
+
const logbookSessions = opts.logbookSessions ?? {
|
|
578
|
+
async open(descriptor) {
|
|
579
|
+
const access = logbook.forCallsign(descriptor.stationCallsign);
|
|
580
|
+
return {
|
|
581
|
+
...access,
|
|
582
|
+
id: `plugin-session-${descriptor.sessionKey}`,
|
|
583
|
+
title: descriptor.title,
|
|
584
|
+
async destroy() { },
|
|
585
|
+
};
|
|
586
|
+
},
|
|
587
|
+
async destroy() { },
|
|
588
|
+
};
|
|
589
|
+
const readOnlyLogbook = {
|
|
590
|
+
hasWorked: logbook.hasWorked,
|
|
591
|
+
hasWorkedDXCC: logbook.hasWorkedDXCC,
|
|
592
|
+
hasWorkedGrid: logbook.hasWorkedGrid,
|
|
593
|
+
queryQSOs: logbook.queryQSOs,
|
|
594
|
+
readQsoSnapshot: logbook.readQsoSnapshot,
|
|
595
|
+
countQSOs: logbook.countQSOs,
|
|
596
|
+
forCallsign(callsign) {
|
|
597
|
+
const access = logbook.forCallsign(callsign);
|
|
598
|
+
return {
|
|
599
|
+
callsign: access.callsign,
|
|
600
|
+
getLogBookId: access.getLogBookId,
|
|
601
|
+
awaitReady: access.awaitReady,
|
|
602
|
+
queryQSOs: access.queryQSOs,
|
|
603
|
+
readQsoSnapshot: access.readQsoSnapshot,
|
|
604
|
+
countQSOs: access.countQSOs,
|
|
605
|
+
getStatistics: access.getStatistics,
|
|
606
|
+
};
|
|
607
|
+
},
|
|
608
|
+
};
|
|
391
609
|
const band = createMockBandAccess(opts.band);
|
|
392
610
|
const settings = createMockHostSettingsControl(opts.settings);
|
|
393
611
|
const files = createMockFileStore();
|
|
394
612
|
const network = opts.network ?? createMockNetworkControl();
|
|
613
|
+
const eventBus = opts.eventBus ?? createMockEventBus();
|
|
395
614
|
const hostDependencies = opts.hostDependencies
|
|
396
615
|
?? (opts.permissions?.includes('host:hamlib') ? createMockHostDependencies() : {});
|
|
397
616
|
const logbookSync = { register() { } };
|
|
617
|
+
const configState = cloneJsonValue(opts.config ?? {});
|
|
398
618
|
return {
|
|
399
|
-
|
|
619
|
+
pluginApiVersion: opts.pluginApiVersion ?? '2.5.0',
|
|
620
|
+
get config() {
|
|
621
|
+
return cloneStructuredValue(configState);
|
|
622
|
+
},
|
|
400
623
|
async updateConfig(patch) {
|
|
401
|
-
|
|
624
|
+
Object.assign(configState, cloneJsonValue(patch));
|
|
402
625
|
},
|
|
403
626
|
store: { global: globalStore, operator: operatorStore },
|
|
627
|
+
digitalMessagePreflight: {
|
|
628
|
+
async check(request) {
|
|
629
|
+
const text = request.text.trim().toUpperCase().replace(/\s+/g, ' ');
|
|
630
|
+
return text
|
|
631
|
+
? { encodable: true, requestedText: text, transmittedText: text }
|
|
632
|
+
: { encodable: false, requestedText: text, reason: 'empty' };
|
|
633
|
+
},
|
|
634
|
+
},
|
|
404
635
|
log,
|
|
405
636
|
timers,
|
|
406
637
|
operator,
|
|
407
638
|
radio,
|
|
408
|
-
logbook,
|
|
409
639
|
band,
|
|
410
640
|
ui,
|
|
411
|
-
settings,
|
|
412
|
-
hostDependencies,
|
|
413
641
|
files,
|
|
414
|
-
|
|
415
|
-
|
|
642
|
+
...((opts.permissions?.includes('logbook:read')
|
|
643
|
+
|| opts.permissions?.includes('logbook:write')
|
|
644
|
+
|| opts.permissions?.includes('logbook:session')) ? {
|
|
645
|
+
logbook: {
|
|
646
|
+
...(opts.permissions?.includes('logbook:write')
|
|
647
|
+
? logbook
|
|
648
|
+
: opts.permissions?.includes('logbook:read') ? readOnlyLogbook : {}),
|
|
649
|
+
...(opts.permissions?.includes('logbook:session') ? { sessions: logbookSessions } : {}),
|
|
650
|
+
},
|
|
651
|
+
} : {}),
|
|
652
|
+
...(opts.permissions?.includes('logbook:sync') ? { logbookSync } : {}),
|
|
653
|
+
...((opts.permissions ?? []).some(permission => permission.startsWith('settings:')) ? {
|
|
654
|
+
settings: {
|
|
655
|
+
...(opts.permissions?.includes('settings:ft8') ? { ft8: settings.ft8 } : {}),
|
|
656
|
+
...(opts.permissions?.includes('settings:decode-windows') ? { decodeWindows: settings.decodeWindows } : {}),
|
|
657
|
+
...(opts.permissions?.includes('settings:realtime') ? { realtime: settings.realtime } : {}),
|
|
658
|
+
...(opts.permissions?.includes('settings:frequency-presets') ? { frequencyPresets: settings.frequencyPresets } : {}),
|
|
659
|
+
...(opts.permissions?.includes('settings:station') ? { station: settings.station } : {}),
|
|
660
|
+
...(opts.permissions?.includes('settings:psk-reporter') ? { pskReporter: settings.pskReporter } : {}),
|
|
661
|
+
...(opts.permissions?.includes('settings:ntp') ? { ntp: settings.ntp } : {}),
|
|
662
|
+
},
|
|
663
|
+
} : {}),
|
|
664
|
+
...(opts.permissions?.includes('operator:transmit-control') ? {
|
|
665
|
+
operatorCommands: opts.operatorCommands ?? createMockOperatorCommandPort(),
|
|
666
|
+
} : {}),
|
|
667
|
+
...(opts.permissions?.includes('radio:read') ? {
|
|
668
|
+
radioCapabilities: opts.radioCapabilities ?? createMockRadioCapabilitiesView(),
|
|
669
|
+
radioPower: opts.radioPower ?? createMockRadioPowerView(),
|
|
670
|
+
} : {}),
|
|
671
|
+
...(opts.permissions?.includes('radio:control') ? {
|
|
672
|
+
radioCommands: opts.radioCommands ?? createMockRadioCommandPort(),
|
|
673
|
+
} : {}),
|
|
674
|
+
...(opts.permissions?.includes('radio:tuner-control') ? {
|
|
675
|
+
radioTunerCommands: opts.radioTunerCommands ?? createMockRadioTunerCommandPort(),
|
|
676
|
+
} : {}),
|
|
677
|
+
...(opts.permissions?.includes('radio:power') ? {
|
|
678
|
+
radioPowerCommands: opts.radioPowerCommands ?? createMockRadioPowerCommandPort(),
|
|
679
|
+
} : {}),
|
|
680
|
+
...(opts.permissions?.includes('host:hamlib') ? { hostDependencies } : {}),
|
|
681
|
+
...(opts.permissions?.includes('network') ? { network, fetch: globalThis.fetch } : {}),
|
|
682
|
+
...(opts.permissions?.includes('plugin:event-bus') ? { eventBus } : {}),
|
|
416
683
|
};
|
|
417
684
|
}
|
|
418
685
|
// ===== Data factories =====
|