Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
copySingleCode,
deleteCode,
duplicateCode,
getPasteSingleCodeWarningKeys,
pasteSingleCode,
sortCodes
} from './schemer-code-ops';
Expand Down Expand Up @@ -89,6 +90,109 @@ describe('schemer-code-ops', () => {
});
});

describe('getPasteSingleCodeWarningKeys', () => {
it('should warn when copied rules reference incompatible target structures', () => {
const copied: CodeData = {
id: 1,
type: 'FULL_CREDIT',
label: '',
score: 1,
ruleSets: [
{
valueArrayPos: 1,
ruleOperatorAnd: true,
rules: [
{ method: 'MATCH', parameters: ['A'], fragment: 0 },
{ method: 'NUMERIC_MATCH', parameters: ['1'] },
{ method: 'IS_TRUE' }
]
}
]
} as unknown as CodeData;

expect(
getPasteSingleCodeWarningKeys(
copied,
{ id: 'v1', sourceType: 'BASE', codes: [] } as unknown as never,
{
id: 'v1',
type: 'string',
multiple: false
} as unknown as never
)
).toEqual([
'code.paste-warning.array-reference',
'code.paste-warning.fragment-reference',
'code.paste-warning.numeric-rule',
'code.paste-warning.boolean-rule'
]);
});

it('should not warn when target supports copied rule references', () => {
const copied: CodeData = {
id: 1,
type: 'FULL_CREDIT',
label: '',
score: 1,
ruleSets: [
{
valueArrayPos: 0,
ruleOperatorAnd: true,
rules: [
{ method: 'NUMERIC_MATCH', parameters: ['1'], fragment: 0 }
]
}
]
} as unknown as CodeData;

expect(
getPasteSingleCodeWarningKeys(
copied,
{
id: 'v1',
sourceType: 'BASE',
fragmenting: '(\\d+)',
codes: []
} as unknown as never,
{
id: 'v1',
type: 'integer',
multiple: true
} as unknown as never
)
).toEqual([]);
});

it('should warn for NUMERIC_RANGE rules copied into non-numeric targets', () => {
const copied: CodeData = {
id: 1,
type: 'FULL_CREDIT',
label: '',
score: 1,
ruleSets: [
{
ruleOperatorAnd: true,
rules: [
{ method: 'NUMERIC_RANGE', parameters: ['1', '3'] }
]
}
]
} as unknown as CodeData;

expect(
getPasteSingleCodeWarningKeys(
copied,
{ id: 'v1', sourceType: 'BASE', codes: [] } as unknown as never,
{
id: 'v1',
type: 'string',
multiple: false
} as unknown as never
)
).toEqual(['code.paste-warning.numeric-rule']);
});
});

describe('addCode', () => {
it('should return no-access for RO', () => {
expect(addCode([], 'FULL_CREDIT', 'RO', orderOfCodeTypes)).toBe('code.error-message.no-access');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import {
CodeData,
CodeType,
RuleMethod,
VariableCodingData,
RuleSet
} from '@iqbspecs/coding-scheme/coding-scheme.interface';
import { VariableInfo } from '@iqbspecs/variable-info/variable-info.interface';

export type UserRoleType = 'RO' | 'RW_MINIMAL' | 'RW_MAXIMAL';

Expand All @@ -14,6 +17,18 @@ const residualTypes: CodeType[] = [
'INTENDED_INCOMPLETE'
];

const numericRuleMethods: RuleMethod[] = [
'NUMERIC_MATCH',
'NUMERIC_RANGE',
'NUMERIC_FULL_RANGE',
'NUMERIC_MIN',
'NUMERIC_MORE_THAN',
'NUMERIC_LESS_THAN',
'NUMERIC_MAX'
];

const booleanRuleMethods: RuleMethod[] = ['IS_TRUE', 'IS_FALSE'];

export const DEFAULT_RESIDUAL_MANUAL_INSTRUCTION =
'<p style="padding-left: 0; text-indent: 0; margin-bottom: 0; margin-top: 0">Alle anderen Antworten</p>';

Expand Down Expand Up @@ -49,6 +64,46 @@ export const canPasteSingleCodeInto = (
return true;
};

export const getPasteSingleCodeWarningKeys = (
copiedCode: CodeData | null,
targetCoding?: VariableCodingData | null,
targetVarInfo?: VariableInfo
): string[] => {
if (!copiedCode) return [];

const warningKeys = new Set<string>();
const ruleSets = copiedCode.ruleSets || [];
const rules = ruleSets.flatMap(ruleSet => ruleSet.rules || []);

const hasArrayReference = ruleSets.some(
ruleSet => typeof ruleSet.valueArrayPos !== 'undefined'
);
if (hasArrayReference && targetVarInfo && !targetVarInfo.multiple) {
warningKeys.add('code.paste-warning.array-reference');
}

const hasFragmentReference = rules.some(rule => typeof rule.fragment !== 'undefined');
if (hasFragmentReference && targetCoding && !targetCoding.fragmenting) {
warningKeys.add('code.paste-warning.fragment-reference');
}

const hasNumericRule = rules.some(rule => numericRuleMethods.includes(rule.method));
if (
hasNumericRule &&
targetVarInfo &&
!['integer', 'number'].includes(targetVarInfo.type)
) {
warningKeys.add('code.paste-warning.numeric-rule');
}

const hasBooleanRule = rules.some(rule => booleanRuleMethods.includes(rule.method));
if (hasBooleanRule && targetVarInfo && targetVarInfo.type !== 'boolean') {
warningKeys.add('code.paste-warning.boolean-rule');
}

return Array.from(warningKeys);
};

export const addCode = (
codeList: CodeData[],
codeType: CodeType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,22 @@ import {
CodeType,
VariableCodingData
} from '@iqbspecs/coding-scheme/coding-scheme.interface';
import { CodingSchemeFactory, CodingSchemeProblem } from '@iqb/responses';
import { SchemerService } from './schemer.service';

describe('SchemerService', () => {
let service: SchemerService;
const copiedCodeStorageKey = 'iqb-schemer-copied-code';

beforeEach(() => {
sessionStorage.removeItem(copiedCodeStorageKey);
service = new SchemerService();
});

afterEach(() => {
sessionStorage.removeItem(copiedCodeStorageKey);
});

describe('checkRenamedVarAliasOk', () => {
it('should return false when alias or codingScheme is missing', () => {
service.setCodingScheme(null);
Expand Down Expand Up @@ -88,6 +95,22 @@ describe('SchemerService', () => {
expect(service.copiedCode?.label).toBe('a');
});

it('should restore copied code from session storage in a fresh service instance', () => {
service.setUserRole('RW_MAXIMAL');
expect(service.copySingleCode({
id: 1,
type: 'FULL_CREDIT',
label: 'from-storage',
score: 1,
ruleSets: []
} as unknown as CodeData)).toBeTrue();

const freshService = new SchemerService();

expect(freshService.copiedCode?.label).toBe('from-storage');
expect(freshService.canPasteSingleCodeInto([])).toBeTrue();
});

it('canPasteSingleCodeInto should block when nothing copied or user is RO', () => {
service.setUserRole('RW_MAXIMAL');
expect(service.canPasteSingleCodeInto([])).toBeFalse();
Expand Down Expand Up @@ -284,6 +307,62 @@ describe('SchemerService', () => {
});
});

describe('getCodingProblemsForVarCoding', () => {
it('should return validation problems for the variable id or alias', () => {
service.setVarList([]);
service.setCodingScheme({
variableCodings: [
{ id: 'v1', alias: 'A', sourceType: 'BASE' } as unknown as VariableCodingData,
{ id: 'v2', alias: 'B', sourceType: 'BASE' } as unknown as VariableCodingData
]
} as unknown as never);
spyOn(CodingSchemeFactory, 'validate').and.returnValue([
{
variableId: 'A',
variableLabel: 'A',
type: 'RULE_PARAMETER_INVALID',
breaking: true,
code: '1'
},
{
variableId: 'v1',
variableLabel: 'A',
type: 'RULE_REGEX_INVALID',
breaking: true,
code: '2'
},
{
variableId: 'B',
variableLabel: 'B',
type: 'VACANT',
breaking: false
}
] as CodingSchemeProblem[]);

const problems = service.getCodingProblemsForVarCoding(
{ id: 'v1', alias: 'A', sourceType: 'BASE' } as unknown as VariableCodingData
);

expect(problems.map(problem => problem.type)).toEqual([
'RULE_PARAMETER_INVALID',
'RULE_REGEX_INVALID'
]);
});

it('should return an empty list when validation fails', () => {
service.setCodingScheme({
variableCodings: [
{ id: 'v1', alias: 'A', sourceType: 'BASE' } as unknown as VariableCodingData
]
} as unknown as never);
spyOn(CodingSchemeFactory, 'validate').and.throwError('boom');

expect(service.getCodingProblemsForVarCoding(
{ id: 'v1', alias: 'A', sourceType: 'BASE' } as unknown as VariableCodingData
)).toEqual([]);
});
});

describe('getVarInfoByCoding', () => {
beforeEach(() => {
service.setVarList([
Expand Down
Loading
Loading