Skip to content
Open
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 @@ -259,6 +259,57 @@ export const ASSOCIATE_ITEM_XML = `<?xml version="1.0" encoding="UTF-8"?>
</qti-item-body>
</qti-assessment-item>`;

/**
* Demo item 8: match interaction — learner matches animals to their traits.
* Uses cardinality="multiple" and base-type="directedPair".
* Bat has two answers, "Can fly" and "Mammal" are each shared by two rows, and
* "Reptile" is a distractor.
*/
export const MATCH_ITEM_XML = `<?xml version="1.0" encoding="UTF-8"?>
<qti-assessment-item
xmlns="http://www.imsglobal.org/xsd/imsqtiasi_v3p0"
identifier="item-match"
title="Match each animal with its traits"
adaptive="false"
time-dependent="false"
xml:lang="en"
>
<qti-response-declaration
identifier="RESPONSE"
cardinality="multiple"
base-type="directedPair"
>
<qti-correct-response>
<qti-value>row_bat choice_mammal</qti-value>
<qti-value>row_bat choice_fly</qti-value>
<qti-value>row_eagle choice_bird</qti-value>
<qti-value>row_eagle choice_fly</qti-value>
<qti-value>row_whale choice_mammal</qti-value>
</qti-correct-response>
</qti-response-declaration>

<qti-item-body>
<qti-match-interaction
response-identifier="RESPONSE"
shuffle="true"
max-associations="5"
>
<qti-prompt><p>Match each animal with its traits:</p></qti-prompt>
<qti-simple-match-set>
<qti-simple-associable-choice identifier="row_bat" match-max="2">Bat</qti-simple-associable-choice>
<qti-simple-associable-choice identifier="row_eagle" match-max="2">Eagle</qti-simple-associable-choice>
<qti-simple-associable-choice identifier="row_whale" match-max="1">Whale</qti-simple-associable-choice>
</qti-simple-match-set>
<qti-simple-match-set>
<qti-simple-associable-choice identifier="choice_mammal" match-max="2">Mammal</qti-simple-associable-choice>
<qti-simple-associable-choice identifier="choice_fly" match-max="2">Can fly</qti-simple-associable-choice>
<qti-simple-associable-choice identifier="choice_bird" match-max="1">Bird</qti-simple-associable-choice>
<qti-simple-associable-choice identifier="choice_reptile" match-max="1">Reptile</qti-simple-associable-choice>
</qti-simple-match-set>
</qti-match-interaction>
</qti-item-body>
</qti-assessment-item>`;

/**
* Hardcoded items covering different states:
* - item-1: single-select choice interaction
Expand All @@ -268,6 +319,7 @@ export const ASSOCIATE_ITEM_XML = `<?xml version="1.0" encoding="UTF-8"?>
* - item-free-response: free-response text-entry (no correct answer)
* - item-ordering: ordering interaction (planets by distance from the Sun)
* - item-associate: associate interaction (countries to capitals, with distractors)
* - item-match: match interaction (animals to traits, with shared answers and a distractor)
*/
export const INITIAL_ASSESSMENTS = [
{
Expand Down Expand Up @@ -305,4 +357,9 @@ export const INITIAL_ASSESSMENTS = [
type: AssessmentItemTypes.QTI,
raw_data: ASSOCIATE_ITEM_XML,
},
{
assessment_id: 'demo-item-match',
type: AssessmentItemTypes.QTI,
raw_data: MATCH_ITEM_XML,
},
];
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import {
CHOICE_ITEM_DOCUMENT_NO_PROMPT,
CHOICE_ITEM_DOCUMENT_NO_CORRECT_ANSWER,
NO_INTERACTION_ITEM_DOCUMENT,
VALID_MATCH_ITEM_DOCUMENT,
MATCH_THREE_SETS_XML,
MATCH_XML,
} from '../utils/testingFixtures';

const codesOf = errors => errors.map(error => error.code);
Expand Down Expand Up @@ -37,6 +40,22 @@ describe('validateQtiItem', () => {
expect(validateQtiItem(undefined)).toEqual([{ code: ValidationError.NO_INTERACTION }]);
});

describe('match interaction', () => {
it('returns no errors for a complete match item', () => {
expect(validateQtiItem(VALID_MATCH_ITEM_DOCUMENT)).toEqual([]);
});

it('runs the match validator', () => {
const noPrompt = VALID_MATCH_ITEM_DOCUMENT.replace(/<qti-prompt>.*<\/qti-prompt>/, '');
expect(codesOf(validateQtiItem(noPrompt))).toContain(ValidationError.PROMPT_REQUIRED);
});

it('reports a match interaction without exactly two match sets as unparseable', () => {
const threeSets = VALID_MATCH_ITEM_DOCUMENT.replace(MATCH_XML, MATCH_THREE_SETS_XML);
expect(validateQtiItem(threeSets)).toEqual([{ code: ValidationError.PARSE_ERROR }]);
});
});

it('reports unparseable XML', () => {
expect(validateQtiItem('<qti-assessment-item><oops>')).toEqual([
{ code: ValidationError.PARSE_ERROR },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,23 @@ describe('ClickableRegion', () => {

expect(emitted().click).toHaveLength(1);
});

it('focuses its button when asked to', async () => {
render(
{
components: { ClickableRegion },
template: `
<div>
<button @click="$refs.region.focus()">Elsewhere</button>
<ClickableRegion ref="region" ariaLabel="Test label" />
</div>
`,
},
{ routes: new VueRouter() },
);

await fireEvent.click(screen.getByRole('button', { name: 'Elsewhere' }));

expect(screen.getByRole('button', { name: 'Test label' })).toHaveFocus();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
>
<button
v-if="!suppressed"
ref="button"
type="button"
class="overlay-button"
:class="{ 'is-text-target': textCursor }"
:aria-label="ariaLabel"
@click.stop="onClick"
></button>
Expand All @@ -25,17 +27,27 @@

<script>

import { ref } from 'vue';

export default {
name: 'ClickableRegion',
setup(props, { emit }) {
const button = ref(null);

function onClick(event) {
if (props.suppressed) return;
if (event && event.stopPropagation) {
event.stopPropagation();
}
emit('click', event);
}
return { onClick };
return {
button,
onClick,
// Public: lets a parent move focus to the region.
// eslint-disable-next-line vue/no-unused-properties
focus: () => button.value?.focus(),
};
},
props: {
ariaLabel: {
Expand All @@ -46,6 +58,11 @@
type: Boolean,
default: false,
},
/** A text cursor and no hover tint, for a region that opens an editor where it is clicked */
textCursor: {
type: Boolean,
default: false,
},
},
emits: ['click'],
};
Expand Down Expand Up @@ -75,10 +92,14 @@
border-radius: inherit;
outline: none;

&:hover {
&:hover:not(.is-text-target) {
background-color: v-bind('$themeTokens.fineLine');
}

&.is-text-target {
cursor: text;
}

&:focus-visible {
outline: 2px solid v-bind('$themeTokens.focusOutline');
outline-offset: 2px;
Expand Down
Loading
Loading