-
-
Notifications
You must be signed in to change notification settings - Fork 304
feat: add AnswerSettings and QuestionSettingsHeader components #6059
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: unstable
Are you sure you want to change the base?
Changes from all commits
5f4baea
cf14a5d
b385729
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,16 +7,36 @@ | |
| > | ||
| {{ parseError }} | ||
| </p> | ||
| <component | ||
| :is="descriptor.editorComponent" | ||
| v-else | ||
| :key="descriptor.type" | ||
| :questionType="questionType" | ||
| :interaction="interaction" | ||
| :mode="mode" | ||
| :showAnswers="showAnswers" | ||
| @update:interaction="interaction => $emit('update:interaction', interaction)" | ||
| /> | ||
| <div v-else> | ||
| <Teleport | ||
| v-if="teleportTarget" | ||
| :to="teleportTarget" | ||
| > | ||
| <QuestionTypeSelector | ||
| v-if="mode === 'edit'" | ||
| :questionType="questionType" | ||
| :questionTypeOptions="typeOptions" | ||
| :settingsTargetId="settingsTargetId" | ||
| @update:questionType=" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpick: a two-statement handler that writes to a Related: |
||
| newType => { | ||
| questionType = newType; | ||
| $emit('update:questionType', newType); | ||
| } | ||
| " | ||
| /> | ||
| </Teleport> | ||
|
|
||
| <component | ||
| :is="descriptor.editorComponent" | ||
| :key="descriptor.type" | ||
| :questionType="questionType" | ||
| :interaction="interaction" | ||
| :mode="mode" | ||
| :showAnswers="showAnswers" | ||
| :teleportTarget="`#${settingsTargetId}`" | ||
| @update:interaction="interaction => $emit('update:interaction', interaction)" | ||
| /> | ||
| </div> | ||
| </div> | ||
|
|
||
| </template> | ||
|
|
@@ -26,13 +46,19 @@ | |
|
|
||
| import { computed, watch } from 'vue'; | ||
| import useInteractionDescriptor from '../../composables/useInteractionDescriptor'; | ||
| import QuestionTypeSelector from '../QuestionTypeSelector/index.vue'; | ||
|
|
||
| export default { | ||
| name: 'InteractionSection', | ||
|
|
||
| components: { | ||
| QuestionTypeSelector, | ||
| }, | ||
|
|
||
| setup(props, { emit }) { | ||
| const interactionRef = computed(() => props.interaction); | ||
| const { descriptor, questionType, parseError } = useInteractionDescriptor(interactionRef); | ||
| const { descriptor, questionType, typeOptions, parseError } = | ||
| useInteractionDescriptor(interactionRef); | ||
|
|
||
| watch( | ||
| questionType, | ||
|
|
@@ -42,7 +68,14 @@ | |
| { immediate: true }, | ||
| ); | ||
|
|
||
| return { descriptor, questionType, parseError }; | ||
| const settingsTargetId = computed(() => { | ||
| if (props.teleportTarget && props.teleportTarget.startsWith('#')) { | ||
| return `${props.teleportTarget.substring(1)}-answer-settings`; | ||
| } | ||
| return 'qti-interaction-settings'; | ||
| }); | ||
|
|
||
| return { descriptor, questionType, typeOptions, parseError, settingsTargetId }; | ||
| }, | ||
|
|
||
| props: { | ||
|
|
@@ -66,6 +99,10 @@ | |
| type: Boolean, | ||
| default: false, | ||
| }, | ||
| teleportTarget: { | ||
| type: String, | ||
| default: '', | ||
| }, | ||
| }, | ||
|
|
||
| emits: ['update:questionType', 'update:interaction'], | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,12 +26,15 @@ | |
| </div> | ||
| </div> | ||
|
|
||
| <div :id="`qti-question-settings-${index}`"></div> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpick: unique across questions in one |
||
|
|
||
| <div class="question-card-body"> | ||
| <InteractionSection | ||
| v-if="interactions.length > 0" | ||
| :interaction="interactions[0]" | ||
| :mode="mode" | ||
| :showAnswers="showAnswers" | ||
| :teleportTarget="`#qti-question-settings-${index}`" | ||
| @update:questionType="type => (currentQuestionType = type)" | ||
| @update:interaction="onUpdateInteraction" | ||
| /> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| import { render, screen, fireEvent } from '@testing-library/vue'; | ||
| import VueRouter from 'vue-router'; | ||
| import QuestionTypeSelector from '../index.vue'; | ||
| import { QuestionType } from '../../../constants'; | ||
| import { qtiEditorStrings as tr } from '../../../qtiEditorStrings'; | ||
|
|
||
| jest.mock('kolibri-design-system/lib/composables/useKResponsiveWindow', () => { | ||
| const { ref } = require('vue'); | ||
| return { | ||
| __esModule: true, | ||
| default: () => ({ windowIsSmall: ref(false) }), | ||
| }; | ||
| }); | ||
|
|
||
| const defaultProps = { | ||
| questionType: QuestionType.SINGLE_SELECT, | ||
| questionTypeOptions: [ | ||
| { | ||
| value: QuestionType.SINGLE_SELECT, | ||
| label: tr.$tr('singleSelectLabel'), | ||
| description: tr.$tr('singleChoiceDescription'), | ||
| }, | ||
| { | ||
| value: QuestionType.MULTI_SELECT, | ||
| label: tr.$tr('multiSelectLabel'), | ||
| description: tr.$tr('multipleSelectionDescription'), | ||
| }, | ||
| ], | ||
| mode: 'edit', | ||
| }; | ||
|
|
||
| const renderHeader = (props = {}) => | ||
| render(QuestionTypeSelector, { | ||
| props: { ...defaultProps, ...props }, | ||
| routes: new VueRouter(), | ||
| }); | ||
|
|
||
| describe('QuestionTypeSelector', () => { | ||
| it('renders the type meta-label in edit mode', () => { | ||
| renderHeader(); | ||
| expect(screen.getByText(tr.$tr('typeLabel'))).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('renders a KSelect with the selected option label (not raw enum)', () => { | ||
| renderHeader(); | ||
| expect(screen.getByText(tr.$tr('singleSelectLabel'))).toBeInTheDocument(); | ||
| expect(screen.queryByText(QuestionType.SINGLE_SELECT)).not.toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('renders the globe icon inside the KSelect via #display slot', () => { | ||
| renderHeader(); | ||
| expect(document.querySelector('.select-globe-icon')).not.toBeNull(); | ||
| expect(document.querySelector('.select-display-row')).not.toBeNull(); | ||
| }); | ||
|
|
||
| it('opens type info modal when info button clicked', async () => { | ||
| renderHeader(); | ||
|
|
||
| const helpButton = screen.getByRole('button', { name: tr.$tr('responseTypeInfoTitle') }); | ||
| await fireEvent.click(helpButton); | ||
|
|
||
| expect(screen.getByRole('dialog')).toBeInTheDocument(); | ||
| expect(screen.getByText(tr.$tr('singleChoiceDescription'))).toBeInTheDocument(); | ||
| expect(screen.getByText(tr.$tr('multipleSelectionDescription'))).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('closes type info modal when Close button clicked', async () => { | ||
| renderHeader(); | ||
|
|
||
| await fireEvent.click(screen.getByRole('button', { name: tr.$tr('responseTypeInfoTitle') })); | ||
| expect(screen.getByRole('dialog')).toBeInTheDocument(); | ||
|
|
||
| await fireEvent.click(screen.getByRole('button', { name: tr.$tr('closeBtnLabel') })); | ||
| expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('disables selector when only one option available', () => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: neither assertion touches More importantly, the component's one behaviour — |
||
| renderHeader({ | ||
| questionTypeOptions: [defaultProps.questionTypeOptions[0]], | ||
| }); | ||
|
|
||
| const hiddenInput = document.querySelector('input[type="hidden"]'); | ||
| expect(hiddenInput).not.toBeNull(); | ||
| expect(screen.getByText(tr.$tr('typeLabel'))).toBeInTheDocument(); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: this global registration is the only one —
jest_config/setup.jsregisters onlyActionLink, so in tests<Teleport>renders as an unknown element with its children in place, andVue.config.silent = truesuppresses the warning. That means the wholedescribe('Answer settings')block inChoiceInteractionEditor.spec.jspasses because the label rendered inline, not at the teleport target; target resolution, thesettingsTargetIdstring surgery, and the ordering of the two teleported blocks are all untested.It also makes these components silently depend on
shared/app.jshaving run, which is a bad fit for code living inshared/views/. ImportingTeleport from 'vue2-teleport'locally inInteractionSectionandChoiceInteractionEditor(and dropping the global registration) fixes both, and avoids squatting on a Vue 3 built-in name. Worth one integration assertion inQTIItemEditor.spec.jsthat the settings land inside#qti-question-settings-0.