Skip to content

Commit

Permalink
Adds tests for rank in scenario suite
Browse files Browse the repository at this point in the history
  • Loading branch information
latin-panda committed Jan 29, 2025
1 parent c51f1ac commit 372d8b8
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 12 deletions.
17 changes: 17 additions & 0 deletions packages/common/src/test/fixtures/xform-dsl/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,23 @@ export const proposed_selectDynamic: Proposed_selectDynamic = (

export { proposed_selectDynamic as selectDynamic };

type RankParameters = readonly [ref: string, nodesetRef: string];
type Rank = (...args: RankParameters) => XFormsElement;

export const rank: Rank = (...[ref, nodesetRef]: RankParameters): XFormsElement => {
const value = t('value ref="value"');
const label = t('label ref="label"');

const itemsetAttributes = new Map<string, string>();
itemsetAttributes.set('nodeset', nodesetRef);

const itemset = new TagXFormsElement('itemset', itemsetAttributes, [value, label]);
const rankAttributes = new Map<string, string>();
rankAttributes.set('ref', ref);

return new TagXFormsElement('odk:rank', rankAttributes, [itemset]) as XFormsElement;
};

export const group = (ref: string, ...children: XFormsElement[]): XFormsElement => {
return t(`group ref="${ref}"`, ...children);
};
Expand Down
4 changes: 0 additions & 4 deletions packages/scenario/src/answer/RankNodeAnswer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@ import type { JSONValue } from '@getodk/common/types/JSONValue.ts';
import type { RankNode } from '@getodk/xforms-engine';
import { ValueNodeAnswer } from './ValueNodeAnswer.ts';

/**
* Produces a value which may be **assigned** to a {@link RankNode}, e.g.
* as part of a test's "act" phase.
*/
export class RankNodeAnswer extends ValueNodeAnswer<RankNode> {
readonly stringValue: string;
readonly value: readonly string[];
Expand Down
22 changes: 22 additions & 0 deletions packages/scenario/src/answer/RankValuesAnswer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { JSONValue } from '@getodk/common/types/JSONValue.ts';
import type { RankNode } from '@getodk/xforms-engine';
import { ComparableAnswer } from './ComparableAnswer.ts';

/**
* Produces a value which may be **assigned** to a {@link RankNode}, e.g.
* as part of a test's "act" phase.
*/
export class RankValuesAnswer extends ComparableAnswer {

readonly stringValue: string;

constructor(readonly values: readonly string[]) {
super();

this.stringValue = values.join(' ');
}

override inspectValue(): JSONValue {
return this.values;
}
}
5 changes: 5 additions & 0 deletions packages/scenario/src/client/answerOf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import { SelectNodeAnswer } from '../answer/SelectNodeAnswer.ts';
import { TriggerNodeAnswer } from '../answer/TriggerNodeAnswer.ts';
import type { ValueNodeAnswer } from '../answer/ValueNodeAnswer.ts';
import { getNodeForReference } from './traversal.ts';
import { RankNodeAnswer } from '../answer/RankNodeAnswer.ts';

const isValueNode = (node: AnyNode) => {
return (
node.nodeType === 'model-value' ||
node.nodeType === 'rank' ||
node.nodeType === 'select' ||
node.nodeType === 'input' ||
node.nodeType === 'trigger'
Expand All @@ -27,6 +29,9 @@ export const answerOf = (instanceRoot: RootNode, reference: string): ValueNodeAn
case 'model-value':
return new ModelValueNodeAnswer(node);

case 'rank':
return new RankNodeAnswer(node);

case 'select':
return new SelectNodeAnswer(node);

Expand Down
23 changes: 15 additions & 8 deletions packages/scenario/src/jr/Scenario.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type { Accessor, Setter } from 'solid-js';
import { createMemo, createSignal, runWithOwner } from 'solid-js';
import { afterEach, assert, expect } from 'vitest';
import { SelectValuesAnswer } from '../answer/SelectValuesAnswer.ts';
import { RankValuesAnswer } from '../answer/RankValuesAnswer.ts';
import type { ValueNodeAnswer } from '../answer/ValueNodeAnswer.ts';
import { answerOf } from '../client/answerOf.ts';
import type { InitializeTestFormOptions, TestFormResource } from '../client/init.ts';
Expand Down Expand Up @@ -113,15 +114,15 @@ type GetQuestionAtIndexParameters<
expectedType?: ExpectedQuestionType | null
];

type AnswerSelectParameters = readonly [reference: string, ...selectionValues: string[]];
type AnswerItemCollectionParameters = readonly [reference: string, ...selectionValues: string[]];

// prettier-ignore
type AnswerParameters =
| AnswerSelectParameters
| AnswerItemCollectionParameters
| readonly [reference: string, value: unknown]
| readonly [value: unknown];

const isAnswerSelectParams = (args: AnswerParameters): args is AnswerSelectParameters => {
const isAnswerItemCollectionParams = (args: AnswerParameters): args is AnswerItemCollectionParameters => {
return args.length > 2 && args.every((arg) => typeof arg === 'string');
};

Expand Down Expand Up @@ -379,21 +380,27 @@ export class Scenario {
return this.setNonTerminalEventPosition(() => index, reference);
}

private answerSelect(reference: string, ...selectionValues: string[]): ValueNodeAnswer {
private answerItemCollectionQuestion(reference: string, ...selectionValues: string[]): ValueNodeAnswer {
const event = this.setPositionalStateToReference(reference);
const isSelect = isQuestionEventOfType(event, 'select');
const isRank = isQuestionEventOfType(event, 'rank');

if (!isQuestionEventOfType(event, 'select')) {
if (!(isSelect || isRank)) {
throw new Error(
`Cannot set selection values for reference ${reference}: event is type ${event.eventType}, node is type ${event.node?.nodeType}`
`Cannot set values for reference ${reference}: event is type ${event.eventType}, node is type ${event.node?.nodeType}`
);
}

if(isRank) {
return event.answerQuestion(new RankValuesAnswer(selectionValues));
}

return event.answerQuestion(new SelectValuesAnswer(selectionValues));
}

answer(...args: AnswerParameters): ValueNodeAnswer {
if (isAnswerSelectParams(args)) {
return this.answerSelect(...args);
if (isAnswerItemCollectionParams(args)) {
return this.answerItemCollectionQuestion(...args);
}

const [arg0, arg1, ...rest] = args;
Expand Down
125 changes: 125 additions & 0 deletions packages/scenario/test/rank.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import {
body,
head,
html,
instance,
item,
mainInstance,
model,
rank,
selectDynamic,
t,
title,
} from '@getodk/common/test/fixtures/xform-dsl/index.ts';
import { describe, it, expect } from 'vitest';
import { Scenario } from '../src/jr/Scenario.ts';
import { r } from '../src/jr/resource/ResourcePathHelper.ts';

describe('Rank', () => {
const getRankForm = () => {
return html(
head(
title('Rank form'),
model(
mainInstance(t("data id='rank'", t('rankQuestion'))),

instance(
'options',
item('option1', 'Option 1'),
item('option2', 'Option 2'),
item('option3', 'Option 3'),
item('option4', 'Option 4')
)
)
),
body(
rank(
'/data/rankQuestion',
"instance('options')/root/item"
)
)
);
};

const getRankWithChoiceFilterForm = () => {
return html(
head(
title('Rank with choice filter'),
model(
mainInstance(t("data id='rank'", t('rankQuestion'), t('selectQuestion'))),

instance(
'options',
item('option1', 'Option 1'),
item('option2', 'Option 2'),
item('option3', 'Option 3'),
item('option4', 'Option 4'),
item('option5', 'Option 5')
)
)
),
body(
selectDynamic(
'/data/selectQuestion',
"instance('options')/root/item"
),

rank(
'/data/rankQuestion',
"instance('options')/root/item[selected(/data/selectQuestion, value)]"
)
)
);
};

it('should update value when rank has <items>', async () => {
const RANK_QUESTION = '/data/rankWidget';
const scenario = await Scenario.init(r('rank-form.xml'));

expect(scenario.answerOf(RANK_QUESTION).getValue()).toBe('');

scenario.answer(RANK_QUESTION, 'A', 'E', 'C', 'B', 'D', 'F', 'G');

expect(scenario.answerOf(RANK_QUESTION).getValue()).toBe('A E C B D F G');
});

it('should update value when rank has <itemset>', async () => {
const RANK_QUESTION = '/data/rankQuestion';
const scenario = await Scenario.init('Rank form', getRankForm());

expect(scenario.answerOf(RANK_QUESTION).getValue()).toBe('');

scenario.answer(RANK_QUESTION, 'option1', 'option4', 'option2', 'option3');

expect(scenario.answerOf(RANK_QUESTION).getValue()).toBe('option1 option4 option2 option3');
});

it('should filter values when rank has choice-filter and it should update values when options are ranked', async () => {
const SELECT_QUESTION = '/data/selectQuestion';
const RANK_QUESTION = '/data/rankQuestion';
const scenario = await Scenario.init('Rank with choice filter', getRankWithChoiceFilterForm());

expect(scenario.answerOf(SELECT_QUESTION).getValue()).toBe('');
expect(scenario.answerOf(RANK_QUESTION).getValue()).toBe('');

scenario.answer(SELECT_QUESTION, 'option1', 'option4', 'option3', 'option2');
expect(scenario.answerOf(SELECT_QUESTION).getValue()).toBe('option1 option2 option3 option4');
expect(scenario.answerOf(RANK_QUESTION).getValue()).toBe('');

scenario.answer(RANK_QUESTION, 'option4', 'option1', 'option2', 'option3');
expect(scenario.answerOf(RANK_QUESTION).getValue()).toBe('option4 option1 option2 option3');

// Make rank not relevant
scenario.answer(SELECT_QUESTION, '');
expect(scenario.answerOf(SELECT_QUESTION).getValue()).toBe('');
expect(scenario.answerOf(RANK_QUESTION).getValue()).toBe('');

// Make rank relevant again
scenario.answer(SELECT_QUESTION, 'option1', 'option5');
expect(scenario.answerOf(SELECT_QUESTION).getValue()).toBe('option1 option5');
expect(scenario.answerOf(RANK_QUESTION).getValue()).toBe('option1 option5');

scenario.answer(RANK_QUESTION, 'option5', 'option1');
expect(scenario.answerOf(RANK_QUESTION).getValue()).toBe('option5 option1');
});
});

0 comments on commit 372d8b8

Please sign in to comment.