diff --git a/projects/ngx-coding-components/src/lib/translations/de.json b/projects/ngx-coding-components/src/lib/translations/de.json
index 47c6963..6eb5ab7 100644
--- a/projects/ngx-coding-components/src/lib/translations/de.json
+++ b/projects/ngx-coding-components/src/lib/translations/de.json
@@ -252,7 +252,18 @@
"TAKE_NOT_REACHED_AS_VALUE_CHANGED": "Nicht erreicht = Wert geändert",
"TAKE_EMPTY_AS_VALID": "Leerer Antwortwert ist gültig",
"SORT": "Sortiere Werte",
- "SOLVER_EXPRESSION": "Ausdruck für Solver"
+ "SOLVER_EXPRESSION": "Ausdruck für Solver",
+ "solver-test": {
+ "title": "Solver-Test",
+ "action": "Testen",
+ "result": "Ergebnis",
+ "no-sources": "Keine Quellvariable ausgewählt.",
+ "error-expression-missing": "Bitte einen Solver-Ausdruck eingeben.",
+ "error-sources-missing": "Bitte mindestens eine Quellvariable auswählen.",
+ "error-unselected-source": "Der Ausdruck verweist auf nicht ausgewählte Quelle(n)",
+ "error-invalid-value": "Bitte numerischen Testwert prüfen",
+ "error-evaluation": "Der Ausdruck konnte nicht ausgewertet werden. Bitte Syntax, Variablennamen und numerische Testwerte prüfen."
+ }
},
"processing": {
"label": "Allgemeine Parameter für die Verarbeitung der Antwort",
diff --git a/projects/ngx-coding-components/src/lib/var-coding/dialogs/edit-source-parameters-dialog.component.html b/projects/ngx-coding-components/src/lib/var-coding/dialogs/edit-source-parameters-dialog.component.html
index 22b5a2f..98ac826 100644
--- a/projects/ngx-coding-components/src/lib/var-coding/dialogs/edit-source-parameters-dialog.component.html
+++ b/projects/ngx-coding-components/src/lib/var-coding/dialogs/edit-source-parameters-dialog.component.html
@@ -46,8 +46,45 @@
{{ (newVariableMode ? 'varList.add' : 'derive-processing.pr
@if (data.sourceType === 'SOLVER') {
{{ 'derive-processing.SOLVER_EXPRESSION' | translate }}
-
+
+
+
+
+
+ @if (selectedSolverSources.length > 0) {
+
+ @for (source of selectedSolverSources; track source.id) {
+
+ {{ source.label }}
+
+
+ }
+
+ } @else {
+
+ {{ 'derive-processing.solver-test.no-sources' | translate }}
+
+ }
+
+ @if (solverTestResult) {
+
+ {{ solverTestResult.message }}
+
+ }
+
}
diff --git a/projects/ngx-coding-components/src/lib/var-coding/dialogs/edit-source-parameters-dialog.component.spec.ts b/projects/ngx-coding-components/src/lib/var-coding/dialogs/edit-source-parameters-dialog.component.spec.ts
index b0ca687..4d8c8ad 100644
--- a/projects/ngx-coding-components/src/lib/var-coding/dialogs/edit-source-parameters-dialog.component.spec.ts
+++ b/projects/ngx-coding-components/src/lib/var-coding/dialogs/edit-source-parameters-dialog.component.spec.ts
@@ -1,9 +1,21 @@
import { SourceType, VariableSourceParameters, SourceProcessingType } from
'@iqbspecs/coding-scheme/coding-scheme.interface';
+import { TranslateService } from '@ngx-translate/core';
import { EditSourceParametersDialog, EditSourceParametersDialogData } from './edit-source-parameters-dialog.component';
import { SchemerService } from '../../services/schemer.service';
describe('EditSourceParametersDialog', () => {
+ const solverRef = (variableName: string): string => `\${${variableName}}`;
+
+ const translations: Record = {
+ 'derive-processing.solver-test.result': 'Ergebnis',
+ 'derive-processing.solver-test.error-expression-missing': 'Bitte einen Solver-Ausdruck eingeben.',
+ 'derive-processing.solver-test.error-sources-missing': 'Bitte mindestens eine Quellvariable auswählen.',
+ 'derive-processing.solver-test.error-unselected-source': 'Der Ausdruck verweist auf nicht ausgewählte Quelle(n)',
+ 'derive-processing.solver-test.error-invalid-value': 'Bitte numerischen Testwert prüfen',
+ 'derive-processing.solver-test.error-evaluation': 'Der Ausdruck konnte nicht ausgewertet werden.'
+ };
+
const createDialog = (options: {
selfId?: string;
selfAlias?: string;
@@ -21,7 +33,7 @@ describe('EditSourceParametersDialog', () => {
}
const data: EditSourceParametersDialogData = {
- selfId: options.selfId ?? 'v1',
+ selfId: options.selfId ?? 'd1',
selfAlias: options.selfAlias ?? 'V1',
sourceType: (options.sourceType as SourceType) ?? 'COPY_VALUE',
sourceParameters: baseSourceParameters as VariableSourceParameters,
@@ -29,19 +41,34 @@ describe('EditSourceParametersDialog', () => {
};
const hasCodingSchemeOverride = Object.prototype.hasOwnProperty.call(options, 'codingScheme');
+ const codingScheme = hasCodingSchemeOverride ?
+ options.codingScheme :
+ {
+ variableCodings: [
+ { id: 'v1', alias: 'V1', sourceType: 'BASE' },
+ { id: 'v2', alias: 'V2', sourceType: 'DERIVE' },
+ { id: 'v3', alias: 'V3', sourceType: 'BASE_NO_VALUE' }
+ ]
+ };
const schemerService = {
- codingScheme: hasCodingSchemeOverride ?
- options.codingScheme :
- {
- variableCodings: [
- { id: 'v1', alias: 'V1', sourceType: 'BASE' },
- { id: 'v2', alias: 'V2', sourceType: 'DERIVE' },
- { id: 'v3', alias: 'V3', sourceType: 'BASE_NO_VALUE' }
- ]
- }
+ codingScheme,
+ getVariableAliasById: (varId: string) => {
+ const variableCoding = codingScheme?.variableCodings.find(
+ variable => (variable as { id: string }).id === varId
+ ) as { id: string; alias?: string } | undefined;
+ return variableCoding?.alias || variableCoding?.id || '?';
+ }
} as unknown as SchemerService;
- return new EditSourceParametersDialog(data, schemerService);
+ const translateService = {
+ instant: (key: string) => translations[key] || key
+ } as unknown as TranslateService;
+
+ return new EditSourceParametersDialog(
+ data,
+ schemerService,
+ translateService
+ );
};
it('should set newVariableMode when selfId is missing', () => {
@@ -117,5 +144,98 @@ describe('EditSourceParametersDialog', () => {
dialog.toggleAllSelection();
expect(dialog.selectedSources.value).toEqual(['v2']);
+ expect(dialog.data.deriveSources).toEqual(['v2']);
+ });
+
+ it('runSolverTest should evaluate the current expression with test values', () => {
+ const dialog = createDialog({
+ selfAlias: 'D',
+ sourceType: 'SOLVER',
+ sourceParameters: {
+ solverExpression: `${solverRef('V1')} + ${solverRef('V2')}`
+ },
+ deriveSources: ['v1', 'v2']
+ });
+
+ dialog.solverTestValues['v1'] = '2';
+ dialog.solverTestValues['v2'] = '3';
+ dialog.runSolverTest();
+
+ expect(dialog.solverTestResult).toEqual({
+ type: 'success',
+ message: 'Ergebnis: 5'
+ });
+ });
+
+ it('runSolverTest should show missing selected sources for referenced variables', () => {
+ const dialog = createDialog({
+ selfAlias: 'D',
+ sourceType: 'SOLVER',
+ sourceParameters: {
+ solverExpression: `${solverRef('V1')} + ${solverRef('V2')}`
+ },
+ deriveSources: ['v1']
+ });
+
+ dialog.solverTestValues['v1'] = '2';
+ dialog.runSolverTest();
+
+ expect(dialog.solverTestResult?.type).toBe('error');
+ expect(dialog.solverTestResult?.message).toContain(
+ 'Der Ausdruck verweist auf nicht ausgewählte Quelle(n)'
+ );
+ expect(dialog.solverTestResult?.message).toContain('V2');
+ });
+
+ it('runSolverTest should report non-numeric test values', () => {
+ const dialog = createDialog({
+ selfAlias: 'D',
+ sourceType: 'SOLVER',
+ sourceParameters: { solverExpression: `${solverRef('V1')} + 1` },
+ deriveSources: ['v1']
+ });
+
+ dialog.solverTestValues['v1'] = 'abc';
+ dialog.runSolverTest();
+
+ expect(dialog.solverTestResult?.type).toBe('error');
+ expect(dialog.solverTestResult?.message).toContain(
+ 'Bitte numerischen Testwert prüfen'
+ );
+ expect(dialog.solverTestResult?.message).toContain('V1');
+ });
+
+ it('runSolverTest should report syntax and evaluation errors', () => {
+ const dialog = createDialog({
+ selfAlias: 'D',
+ sourceType: 'SOLVER',
+ sourceParameters: { solverExpression: `${solverRef('V1')} +` },
+ deriveSources: ['v1']
+ });
+
+ dialog.solverTestValues['v1'] = '2';
+ dialog.runSolverTest();
+
+ expect(dialog.solverTestResult).toEqual({
+ type: 'error',
+ message: 'Der Ausdruck konnte nicht ausgewertet werden.'
+ });
+ });
+
+ it('updateDeriveSources should keep solver test values aligned with selected sources', () => {
+ const dialog = createDialog({
+ selfAlias: 'D',
+ sourceType: 'SOLVER',
+ deriveSources: ['v1']
+ });
+
+ dialog.solverTestValues['v1'] = '4';
+ dialog.solverTestResult = { type: 'success', message: 'Ergebnis: 4' };
+ dialog.selectedSources.setValue(['v2']);
+ dialog.updateDeriveSources();
+
+ expect(dialog.data.deriveSources).toEqual(['v2']);
+ expect(dialog.solverTestValues).toEqual({ v2: '' });
+ expect(dialog.solverTestResult).toBeNull();
});
});
diff --git a/projects/ngx-coding-components/src/lib/var-coding/dialogs/edit-source-parameters-dialog.component.ts b/projects/ngx-coding-components/src/lib/var-coding/dialogs/edit-source-parameters-dialog.component.ts
index 3804253..d1ddd30 100644
--- a/projects/ngx-coding-components/src/lib/var-coding/dialogs/edit-source-parameters-dialog.component.ts
+++ b/projects/ngx-coding-components/src/lib/var-coding/dialogs/edit-source-parameters-dialog.component.ts
@@ -6,8 +6,9 @@ import {
MatDialogActions,
MatDialogClose
} from '@angular/material/dialog';
-import { TranslateModule } from '@ngx-translate/core';
+import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { MatButton } from '@angular/material/button';
+import { MatIcon } from '@angular/material/icon';
import { MatFormField, MatLabel } from '@angular/material/form-field';
import { MatInput } from '@angular/material/input';
import { MatCheckbox } from '@angular/material/checkbox';
@@ -26,6 +27,8 @@ import {
VariableCodingData,
VariableSourceParameters
} from '@iqbspecs/coding-scheme/coding-scheme.interface';
+import { Response } from '@iqbspecs/response/response.interface';
+import { CodingFactory, CodingSchemeFactory } from '@iqb/responses';
import {
SchemerService,
VARIABLE_NAME_CHECK_PATTERN
@@ -39,6 +42,11 @@ export interface EditSourceParametersDialogData {
deriveSources: string[];
}
+type SolverTestResult = {
+ type: 'success' | 'error';
+ message: string;
+};
+
@Component({
templateUrl: 'edit-source-parameters-dialog.component.html',
standalone: true,
@@ -50,6 +58,7 @@ export interface EditSourceParametersDialogData {
MatButton,
MatDialogClose,
TranslateModule,
+ MatIcon,
FormsModule,
MatFormField,
MatInput,
@@ -61,6 +70,54 @@ export interface EditSourceParametersDialogData {
NgForOf,
ReactiveFormsModule,
NgIf
+ ],
+ styles: [
+ `
+ .solver-test-area {
+ border-top: 1px solid rgba(0, 0, 0, 0.12);
+ margin-top: 8px;
+ padding-top: 16px;
+ }
+
+ .solver-test-header {
+ align-items: center;
+ display: flex;
+ gap: 16px;
+ justify-content: space-between;
+ margin-bottom: 12px;
+ }
+
+ .solver-test-title {
+ font-weight: 600;
+ }
+
+ .solver-test-values {
+ display: grid;
+ gap: 8px 12px;
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+ }
+
+ .solver-test-result {
+ border-radius: 4px;
+ margin-top: 8px;
+ padding: 8px 12px;
+ }
+
+ .solver-test-result-ok {
+ background: #e8f5e9;
+ color: #1b5e20;
+ }
+
+ .solver-test-result-error {
+ background: #ffebee;
+ color: #b71c1c;
+ }
+
+ .solver-test-hint {
+ color: rgba(0, 0, 0, 0.6);
+ margin-bottom: 8px;
+ }
+ `
]
})
export class EditSourceParametersDialog {
@@ -78,9 +135,13 @@ export class EditSourceParametersDialog {
selectedSources = new FormControl();
possibleNewSources: ReadonlyMap = new Map([]);
newVariableMode = false;
+ solverTestValues: Record = {};
+ solverTestResult: SolverTestResult | null = null;
+
constructor(
@Inject(MAT_DIALOG_DATA) public data: EditSourceParametersDialogData,
- public schemerService: SchemerService
+ public schemerService: SchemerService,
+ private translateService: TranslateService
) {
this.newVariableMode = !this.data.selfId;
@@ -101,6 +162,13 @@ export class EditSourceParametersDialog {
this.updatePossibleNewSources();
}
+ get selectedSolverSources(): { id: string; label: string }[] {
+ return (this.data.deriveSources || []).map(sourceId => ({
+ id: sourceId,
+ label: this.possibleNewSources.get(sourceId) || sourceId
+ }));
+ }
+
updatePossibleNewSources(): void {
const codingScheme = this.schemerService.codingScheme;
@@ -124,6 +192,7 @@ export class EditSourceParametersDialog {
);
this.selectedSources.setValue(this.data.deriveSources);
+ this.syncSolverTestValues();
}
updatePossibleDeriveProcessing(): void {
@@ -144,6 +213,7 @@ export class EditSourceParametersDialog {
this.possibleDeriveProcessing =
processingOptions[this.data.sourceType] || [];
+ this.clearSolverTestResult();
}
alterProcessing(processingId: SourceProcessingType, checked: boolean): void {
@@ -163,11 +233,185 @@ export class EditSourceParametersDialog {
}
updateDeriveSources() {
- this.data.deriveSources = this.selectedSources.value;
+ this.data.deriveSources = this.selectedSources.value || [];
+ this.syncSolverTestValues();
+ this.clearSolverTestResult();
}
toggleAllSelection() {
this.selectedSources.setValue(Array.from(this.possibleNewSources.keys()));
+ this.updateDeriveSources();
+ }
+
+ clearSolverTestResult(): void {
+ this.solverTestResult = null;
+ }
+
+ runSolverTest(): void {
+ this.updateDeriveSources();
+
+ const expression = this.data.sourceParameters.solverExpression || '';
+ if (!expression.trim()) {
+ this.setSolverTestError(
+ 'derive-processing.solver-test.error-expression-missing'
+ );
+ return;
+ }
+
+ if (!this.data.deriveSources.length) {
+ this.setSolverTestError(
+ 'derive-processing.solver-test.error-sources-missing'
+ );
+ return;
+ }
+
+ const variableCodings = this.getVariableCodingsForSolverTest();
+ const referencedVariables = EditSourceParametersDialog.getReferencedSolverVariables(
+ expression,
+ variableCodings
+ );
+ const missingSources = referencedVariables.filter(
+ variableId => !this.data.deriveSources.includes(variableId)
+ );
+
+ if (missingSources.length > 0) {
+ this.solverTestResult = {
+ type: 'error',
+ message: `${this.tr(
+ 'derive-processing.solver-test.error-unselected-source'
+ )}: ${missingSources.map(sourceId => this.getSourceLabel(sourceId))
+ .join(', ')}`
+ };
+ return;
+ }
+
+ const invalidNumericSources = referencedVariables.filter(
+ sourceId => CodingFactory.getValueAsNumber(
+ this.solverTestValues[sourceId] || ''
+ ) === null
+ );
+
+ if (invalidNumericSources.length > 0) {
+ this.solverTestResult = {
+ type: 'error',
+ message: `${this.tr(
+ 'derive-processing.solver-test.error-invalid-value'
+ )}: ${invalidNumericSources.map(sourceId => this.getSourceLabel(sourceId))
+ .join(', ')}`
+ };
+ return;
+ }
+
+ try {
+ const result = CodingSchemeFactory.deriveValue(
+ variableCodings,
+ this.getSolverTestCoding(),
+ this.getSolverTestResponses()
+ );
+
+ if (result.status === 'VALUE_CHANGED') {
+ this.solverTestResult = {
+ type: 'success',
+ message: `${this.tr('derive-processing.solver-test.result')}: ${
+ EditSourceParametersDialog.formatSolverTestValue(result.value)
+ }`
+ };
+ return;
+ }
+ } catch {
+ // fall through to the shared error message
+ }
+
+ this.setSolverTestError(
+ 'derive-processing.solver-test.error-evaluation'
+ );
+ }
+
+ private syncSolverTestValues(): void {
+ const selectedSourceIds = new Set(this.data.deriveSources || []);
+ const nextValues: Record = {};
+
+ selectedSourceIds.forEach(sourceId => {
+ nextValues[sourceId] = this.solverTestValues[sourceId] || '';
+ });
+
+ this.solverTestValues = nextValues;
+ }
+
+ private getVariableCodingsForSolverTest(): VariableCodingData[] {
+ const sourceCodings = this.schemerService.codingScheme?.variableCodings ||
+ [];
+ const solverCoding = this.getSolverTestCoding();
+ const hasSelfCoding = sourceCodings.some(coding => coding.id === solverCoding.id);
+
+ if (hasSelfCoding) {
+ return sourceCodings.map(
+ coding => (coding.id === solverCoding.id ? solverCoding : coding)
+ );
+ }
+
+ return [...sourceCodings, solverCoding];
+ }
+
+ private getSolverTestCoding(): VariableCodingData {
+ return {
+ id: this.data.selfId || this.data.selfAlias || '__solver_test__',
+ alias: this.data.selfAlias,
+ sourceType: 'SOLVER',
+ sourceParameters: {
+ ...this.data.sourceParameters,
+ processing: this.data.sourceParameters.processing || [],
+ solverExpression: this.data.sourceParameters.solverExpression || ''
+ },
+ deriveSources: [...this.data.deriveSources]
+ };
+ }
+
+ private getSolverTestResponses(): Response[] {
+ return this.data.deriveSources.map(sourceId => ({
+ id: sourceId,
+ value: this.solverTestValues[sourceId] || '',
+ status: 'VALUE_CHANGED'
+ }));
+ }
+
+ private static getReferencedSolverVariables(
+ expression: string,
+ variableCodings: VariableCodingData[]
+ ): string[] {
+ const variableIdsByAlias = new Map(
+ variableCodings
+ .filter(coding => Boolean(coding.alias))
+ .map(coding => [coding.alias as string, coding.id])
+ );
+ const references = Array.from(expression.matchAll(/\$\{(\s*[\w,-]+\s*)}/g))
+ .map(match => match[1].trim())
+ .map(variableName => variableIdsByAlias.get(variableName) ||
+ variableName);
+
+ return [...new Set(references)];
+ }
+
+ private getSourceLabel(sourceId: string): string {
+ const sourceAlias = this.schemerService.getVariableAliasById(sourceId);
+ return this.possibleNewSources.get(sourceId) ||
+ (sourceAlias === '?' ? sourceId : sourceAlias);
+ }
+
+ private static formatSolverTestValue(value: unknown): string {
+ if (typeof value === 'string') return value;
+ return JSON.stringify(value) || '';
+ }
+
+ private setSolverTestError(translationKey: string): void {
+ this.solverTestResult = {
+ type: 'error',
+ message: this.tr(translationKey)
+ };
+ }
+
+ private tr(translationKey: string): string {
+ return this.translateService.instant(translationKey) as string;
}
// eslint-disable-next-line class-methods-use-this