diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/edit/ActivityDuration.vue b/contentcuration/contentcuration/frontend/channelEdit/components/edit/ActivityDuration.vue
index debdbeadc3..071892dc3f 100644
--- a/contentcuration/contentcuration/frontend/channelEdit/components/edit/ActivityDuration.vue
+++ b/contentcuration/contentcuration/frontend/channelEdit/components/edit/ActivityDuration.vue
@@ -96,6 +96,9 @@
},
computed: {
showRequiredLabel() {
+ if (this.audioVideoUpload) {
+ return false;
+ }
return (
this.selectedDuration !== DurationDropdownMap.EXACT_TIME &&
this.selectedCompletion === CompletionDropdownMap.completeDuration
@@ -121,7 +124,7 @@
return this.convertToMinutes(this.value);
},
set(value) {
- this.handleValidateMinutes(value);
+ this.handleUpdatedInput(value);
},
},
maxRange() {
@@ -152,20 +155,17 @@
},
},
created() {
- this.handleValidateMinutes = debounce(this.validateMinutes, 500);
+ this.handleUpdatedInput = debounce(this.handleInput, 500);
},
methods: {
convertToMinutes(seconds) {
return Math.floor(seconds / 60);
},
- validateMinutes(value) {
- if (this.selectedDuration === DurationDropdownMap.EXACT_TIME) {
- this.$emit('input', value * 60);
- } else {
- if (value >= this.minRange && value <= this.maxRange) {
- this.$emit('input', value * 60);
- }
- }
+ convertToSeconds(minutes) {
+ return minutes * 60;
+ },
+ handleInput(value) {
+ this.$emit('input', this.convertToSeconds(value));
},
},
$trs: {
diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/edit/CompletionOptions.vue b/contentcuration/contentcuration/frontend/channelEdit/components/edit/CompletionOptions.vue
index 8a1571b454..5cc78523ac 100644
--- a/contentcuration/contentcuration/frontend/channelEdit/components/edit/CompletionOptions.vue
+++ b/contentcuration/contentcuration/frontend/channelEdit/components/edit/CompletionOptions.vue
@@ -2,7 +2,7 @@
-
+
- import ActivityDuration from './ActivityDuration.vue';
import MasteryCriteriaGoal from './MasteryCriteriaGoal';
+ import ActivityDuration from './ActivityDuration.vue';
import MasteryCriteriaMofNFields from './MasteryCriteriaMofNFields';
import {
CompletionCriteriaModels,
@@ -146,7 +148,7 @@
},
practiceQuizzesAllowed: {
type: Boolean,
- default: false,
+ default: true,
},
value: {
type: Object,
@@ -161,8 +163,8 @@
// required state because we need to know the completion to determine durationDropdown model
currentCompletionDropdown: null,
mastery_model: null,
- m: 3,
- n: 5,
+ m: null,
+ n: null,
};
},
computed: {
@@ -176,62 +178,83 @@
}
return false;
},
- showCompletionDropdown() {
+ hideCompletionDropdown() {
/*
This condition can be removed once practice quizzes are fully implemented in 0.16
- */
- return !(this.kind === ContentKindsNames.EXERCISE && !this.practiceQuizzesAllowed);
+ Named "hide" instead of "show" because "show" is the default behavior
+ */
+ return this.practiceQuizzesAllowed;
},
audioVideoResource() {
return this.kind === ContentKindsNames.AUDIO || this.kind === ContentKindsNames.VIDEO;
},
hideDurationDropdown() {
// named "hide" instead of "show" because "show" is the default behavior
- return (
- this.currentCompletionDropdown === CompletionDropdownMap.reference ||
- (this.value.model === CompletionCriteriaModels.REFERENCE &&
- !this.currentCompletionDropdown &&
- this.audioVideoResource) ||
- //should be hidden if model is reference and we're getting this from the BE
- this.currentCompletionDropdown === CompletionDropdownMap.determinedByResource
- );
+ if (this.value) {
+ if (this.kind === ContentKindsNames.H5P) {
+ return true;
+ }
+ return (
+ this.currentCompletionDropdown === CompletionDropdownMap.reference ||
+ (this.value.model === CompletionCriteriaModels.REFERENCE &&
+ !this.currentCompletionDropdown &&
+ this.audioVideoResource) ||
+ //should be hidden if model is reference and we're getting this from the BE
+ this.currentCompletionDropdown === CompletionDropdownMap.determinedByResource
+ );
+ }
+ return false;
},
showReferenceHint() {
/*
The reference hint should be shown only when "Reference" is selected
*/
- if (this.kind === ContentKindsNames.H5P || this.kind === ContentKindsNames.HTML5) {
- if (this.currentCompletionDropdown === CompletionDropdownMap.determinedByResource) {
- return false;
+ if (this.value) {
+ if (this.kind === ContentKindsNames.H5P || this.kind === ContentKindsNames.HTML5) {
+ if (this.currentCompletionDropdown === CompletionDropdownMap.determinedByResource) {
+ return false;
+ }
+ if (
+ this.value.model === CompletionCriteriaModels.REFERENCE &&
+ this.currentCompletionDropdown === CompletionDropdownMap.completeDuration
+ ) {
+ return true;
+ }
}
- if (
- this.value.model === CompletionCriteriaModels.REFERENCE &&
- this.currentCompletionDropdown === CompletionDropdownMap.completeDuration
- ) {
- return true;
+ if (this.audioVideoResource) {
+ return this.value.model === CompletionCriteriaModels.REFERENCE;
}
+ return (
+ this.value.model === CompletionCriteriaModels.REFERENCE &&
+ this.currentCompletionDropdown !== CompletionDropdownMap.completeDuration
+ );
}
- return (
- (this.durationDropdown === DurationDropdownMap.REFERENCE &&
- this.currentCompletionDropdown === CompletionDropdownMap.allContent) ||
- this.currentCompletionDropdown === CompletionDropdownMap.reference ||
- (this.value.model === CompletionCriteriaModels.REFERENCE &&
- this.currentCompletionDropdown !== CompletionDropdownMap.completeDuration)
- );
+ return false;
},
showActivityDurationInput() {
/* The `ActivityDuration` component should visible when:
- Long activity, short activity, or exact time are chosen if it is not an AV resource
- Long activity or short activity are chosen if it is an AV resource
+ - Long activity, short activity, or exact time are chosen in HTML5
*/
- const switchingFromReferenceBetweenAllContentViewedAndCompleteDuration =
- this.value.suggested_duration === null || this.value.suggested_duration_type === null;
+ if (this.value) {
+ const switchingFromReferenceBetweenAllContentViewedAndCompleteDuration =
+ this.value.suggested_duration === null || this.value.suggested_duration_type === null;
- if (!this.audioVideoResource) {
- return !(
- this.value.model === CompletionCriteriaModels.REFERENCE ||
- switchingFromReferenceBetweenAllContentViewedAndCompleteDuration
- );
+ if (!this.audioVideoResource) {
+ if (this.kind === ContentKindsNames.HTML5 || this.kind === ContentKindsNames.H5P) {
+ if (this.value.model !== CompletionCriteriaModels.REFERENCE) {
+ if (!this.currentCompletionDropdown) {
+ return true;
+ }
+ return this.currentCompletionDropdown === CompletionDropdownMap.completeDuration;
+ }
+ }
+ return !(
+ this.value.model === CompletionCriteriaModels.REFERENCE ||
+ switchingFromReferenceBetweenAllContentViewedAndCompleteDuration
+ );
+ }
}
return this.audioVideoResource && this.value.model !== CompletionCriteriaModels.REFERENCE;
},
@@ -259,10 +282,15 @@
}
if (this.kind === ContentKindsNames.HTML5) {
- if (!this.value['model']) {
+ if (
+ !this.value['model'] ||
+ this.value.model === CompletionCriteriaModels.APPROX_TIME ||
+ this.value.model === CompletionCriteriaModels.TIME ||
+ this.value.model === CompletionCriteriaModels.REFERENCE
+ ) {
return CompletionDropdownMap.completeDuration;
}
- return this.value.model;
+ return CompletionDropdownMap.determinedByResource;
}
if (this.kind === ContentKindsNames.H5P) {
@@ -330,6 +358,16 @@
}
}
+ // FOR H5P/HTML5
+ if (this.kind === ContentKindsNames.HTML5 || this.kind === ContentKindsNames.H5P) {
+ if (value === CompletionDropdownMap.determinedByResource) {
+ update.completion_criteria = {
+ model: this.value.model,
+ threshold: this.value.threshold,
+ };
+ }
+ }
+
// FOR EXERCISES
if (this.kind === ContentKindsNames.EXERCISE) {
if (value === CompletionDropdownMap.practiceQuiz) {
@@ -477,7 +515,8 @@
completionDropdownIsAllContentViewed() {
return (
(this.completionDropdown === null &&
- this.currentCompletionDropdown === CompletionDropdownMap.nt &&
+ this.currentCompletionDropdown === CompletionDropdownMap.allContent) ||
+ (this.completionDropdown === CompletionDropdownMap.allContent &&
this.currentCompletionDropdown === null) ||
(this.completionDropdown === CompletionDropdownMap.allContent &&
this.currentCompletionDropdown === CompletionDropdownMap.allContent)
@@ -485,40 +524,42 @@
},
durationDropdown: {
get() {
- const defaultStateForAudioVideo =
- this.value.suggested_duration === null &&
- this.value.suggested_duration_type === null &&
- this.audioVideoResource;
- if (
- this.value.model === CompletionCriteriaModels.REFERENCE ||
- (this.currentCompletionDropdown === CompletionDropdownMap.completeDuration &&
- this.currentDurationDropdown === DurationDropdownMap.REFERENCE)
- ) {
- return DurationDropdownMap.REFERENCE;
- } else if (this.value.model === CompletionCriteriaModels.PAGES) {
- if (this.isLongActivity) {
- return DurationDropdownMap.LONG_ACTIVITY;
- }
- if (this.isShortActivity) {
- return DurationDropdownMap.SHORT_ACTIVITY;
- }
- if (this.isExactTime) {
- return DurationDropdownMap.EXACT_TIME;
- }
- } else if (
- this.value.model === CompletionCriteriaModels.TIME ||
- defaultStateForAudioVideo
- ) {
- return DurationDropdownMap.EXACT_TIME;
- } else {
- if (this.isLongActivity) {
- return DurationDropdownMap.LONG_ACTIVITY;
- }
- if (this.isShortActivity) {
- return DurationDropdownMap.SHORT_ACTIVITY;
- }
- if (this.isExactTime) {
+ if (this.value) {
+ const defaultStateForAudioVideo =
+ this.value.suggested_duration === null &&
+ !this.value.suggested_duration_type &&
+ this.audioVideoResource;
+ if (
+ this.value.model === CompletionCriteriaModels.REFERENCE ||
+ (this.currentCompletionDropdown === CompletionDropdownMap.completeDuration &&
+ this.currentDurationDropdown === DurationDropdownMap.REFERENCE)
+ ) {
+ return DurationDropdownMap.REFERENCE;
+ } else if (this.value.model === CompletionCriteriaModels.PAGES) {
+ if (this.isLongActivity) {
+ return DurationDropdownMap.LONG_ACTIVITY;
+ }
+ if (this.isShortActivity) {
+ return DurationDropdownMap.SHORT_ACTIVITY;
+ }
+ if (this.isExactTime) {
+ return DurationDropdownMap.EXACT_TIME;
+ }
+ } else if (
+ this.value.model === CompletionCriteriaModels.TIME ||
+ defaultStateForAudioVideo
+ ) {
return DurationDropdownMap.EXACT_TIME;
+ } else {
+ if (this.isLongActivity) {
+ return DurationDropdownMap.LONG_ACTIVITY;
+ }
+ if (this.isShortActivity) {
+ return DurationDropdownMap.SHORT_ACTIVITY;
+ }
+ if (this.isExactTime) {
+ return DurationDropdownMap.EXACT_TIME;
+ }
}
}
return '';
@@ -543,21 +584,17 @@
}
if (duration === DurationDropdownMap.SHORT_ACTIVITY) {
update.suggested_duration_type = SuggestedDurationTypesMap.APPROX_TIME;
- const roundedValue = Math.round(this.value.suggested_duration / 300) * 300;
- if (roundedValue > SHORT_LONG_ACTIVITY_MIDPOINT || roundedValue <= 0) {
- update.suggested_duration = DEFAULT_SHORT_ACTIVITY;
- } else {
- update.suggested_duration = roundedValue;
- }
+ update.suggested_duration = this.handleMinutesInputFromActivityDuration(
+ this.value.suggested_duration,
+ duration
+ );
}
if (duration === DurationDropdownMap.LONG_ACTIVITY) {
update.suggested_duration_type = SuggestedDurationTypesMap.APPROX_TIME;
- const roundedValue = Math.round(this.value.suggested_duration / 600) * 600;
- if (roundedValue < SHORT_LONG_ACTIVITY_MIDPOINT || roundedValue > 7200) {
- update.suggested_duration = DEFAULT_LONG_ACTIVITY;
- } else {
- update.suggested_duration = roundedValue;
- }
+ update.suggested_duration = this.handleMinutesInputFromActivityDuration(
+ this.value.suggested_duration,
+ duration
+ );
}
update.completion_criteria = {
model: CompletionCriteriaModels.PAGES,
@@ -566,16 +603,16 @@
} else if (
this.isSwitchingFromAllContentToCompleteDuration ||
this.requiresAudioVideoDefault ||
- this.completionDropdownIsCompleteDuration
+ this.completionDropdownIsCompleteDuration ||
+ this.kind === ContentKindsNames.HTML5 ||
+ this.kind === ContentKindsNames.H5P
) {
if (duration === DurationDropdownMap.SHORT_ACTIVITY) {
update.suggested_duration_type = SuggestedDurationTypesMap.APPROX_TIME;
- const roundedValue = Math.round(this.value.suggested_duration / 300) * 300;
- if (roundedValue > SHORT_LONG_ACTIVITY_MIDPOINT || roundedValue <= 0) {
- update.suggested_duration = DEFAULT_SHORT_ACTIVITY;
- } else {
- update.suggested_duration = roundedValue;
- }
+ update.suggested_duration = this.handleMinutesInputFromActivityDuration(
+ this.value.suggested_duration,
+ duration
+ );
update.completion_criteria = {
model: CompletionCriteriaModels.APPROX_TIME,
threshold: update.suggested_duration,
@@ -583,12 +620,10 @@
}
if (duration === DurationDropdownMap.LONG_ACTIVITY) {
update.suggested_duration_type = SuggestedDurationTypesMap.APPROX_TIME;
- const roundedValue = Math.round(this.value.suggested_duration / 600) * 600;
- if (roundedValue < SHORT_LONG_ACTIVITY_MIDPOINT || roundedValue > 7200) {
- update.suggested_duration = DEFAULT_LONG_ACTIVITY;
- } else {
- update.suggested_duration = roundedValue;
- }
+ update.suggested_duration = this.handleMinutesInputFromActivityDuration(
+ this.value.suggested_duration,
+ duration
+ );
update.completion_criteria = {
model: CompletionCriteriaModels.APPROX_TIME,
threshold: update.suggested_duration,
@@ -606,13 +641,10 @@
if (this.value.model === CompletionCriteriaModels.MASTERY) {
if (duration === DurationDropdownMap.SHORT_ACTIVITY) {
- update.suggested_duration_type = SuggestedDurationTypesMap.APPROX_TIME;
- const roundedValue = Math.round(this.value.suggested_duration / 300) * 300;
- if (roundedValue > SHORT_LONG_ACTIVITY_MIDPOINT || roundedValue <= 0) {
- update.suggested_duration = DEFAULT_SHORT_ACTIVITY;
- } else {
- update.suggested_duration = roundedValue;
- }
+ update.suggested_duration = this.handleMinutesInputFromActivityDuration(
+ this.value.suggested_duration,
+ duration
+ );
update.completion_criteria = {
model: this.value.model,
threshold: this.value.threshold,
@@ -640,6 +672,7 @@
};
}
}
+
this.handleInput(update);
},
},
@@ -682,11 +715,13 @@
audio: [CompletionDropdownMap.completeDuration, CompletionDropdownMap.reference],
video: [CompletionDropdownMap.completeDuration, CompletionDropdownMap.reference],
};
-
- return CompletionOptionsDropdownMap[this.kind].map(model => ({
- text: this.$tr(model),
- value: CompletionDropdownMap[model],
- }));
+ if (this.kind) {
+ return CompletionOptionsDropdownMap[this.kind].map(model => ({
+ text: this.$tr(model),
+ value: CompletionDropdownMap[model],
+ }));
+ }
+ return [];
},
allPossibleDurationOptions() {
//this is used because of this Vuetify issue for dropdowns with multiple values: https://github.com/vuetifyjs/vuetify/issues/11529
@@ -743,17 +778,22 @@
}
},
completionRules() {
- return this.required ? getCompletionValidators().map(translateValidator) : [];
+ if (this.kind) {
+ return this.required ? getCompletionValidators().map(translateValidator) : [];
+ }
+ return false;
},
durationRules() {
const defaultStateForDocument = this.currentCompletionDropdown === null;
- const allContentViewedIsChosenInCompletionDropdown =
- this.currentCompletionDropdown === CompletionDropdownMap.allContent ||
- (this.value.model === CompletionCriteriaModels.PAGES &&
- this.currentCompletionDropdown === CompletionDropdownMap.allContent);
+ if (this.value) {
+ const allContentViewedIsChosenInCompletionDropdown =
+ this.currentCompletionDropdown === CompletionDropdownMap.allContent ||
+ (this.value.model === CompletionCriteriaModels.PAGES &&
+ this.currentCompletionDropdown === CompletionDropdownMap.allContent);
- if (defaultStateForDocument || allContentViewedIsChosenInCompletionDropdown) {
- return [];
+ if (defaultStateForDocument || allContentViewedIsChosenInCompletionDropdown) {
+ return [];
+ }
}
return getDurationValidators().map(translateValidator);
},
@@ -762,6 +802,27 @@
trackClick(label) {
this.$analytics.trackClick('channel_editor_modal_details', label);
},
+ handleMinutesInputFromActivityDuration(minutes, duration) {
+ let suggested_duration;
+ let roundedValue;
+ if (duration === DurationDropdownMap.SHORT_ACTIVITY) {
+ roundedValue = Math.round(minutes / 300) * 300;
+ if (roundedValue > SHORT_LONG_ACTIVITY_MIDPOINT || roundedValue <= 0) {
+ suggested_duration = DEFAULT_SHORT_ACTIVITY;
+ } else {
+ suggested_duration = roundedValue;
+ }
+ }
+ if (duration === DurationDropdownMap.LONG_ACTIVITY) {
+ roundedValue = Math.round(minutes / 600) * 600;
+ if (roundedValue < SHORT_LONG_ACTIVITY_MIDPOINT || roundedValue > 7200) {
+ suggested_duration = DEFAULT_LONG_ACTIVITY;
+ } else {
+ suggested_duration = roundedValue;
+ }
+ }
+ return suggested_duration;
+ },
handleInput({
completion_criteria,
suggested_duration,
@@ -787,6 +848,7 @@
if (suggested_duration_type === undefined) {
data['suggested_duration_type'] = this.value['suggested_duration_type'];
}
+
this.$emit('input', data);
},
isUnique(value) {
diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/edit/MasteryCriteriaGoal.vue b/contentcuration/contentcuration/frontend/channelEdit/components/edit/MasteryCriteriaGoal.vue
index f56ba17306..60832864e6 100644
--- a/contentcuration/contentcuration/frontend/channelEdit/components/edit/MasteryCriteriaGoal.vue
+++ b/contentcuration/contentcuration/frontend/channelEdit/components/edit/MasteryCriteriaGoal.vue
@@ -13,6 +13,7 @@
:placeholder="placeholder"
:required="required"
:readonly="readonly"
+ :disabled="disabled"
:rules="masteryRules"
menu-props="offsetY"
class="mb-2"
@@ -53,6 +54,10 @@
type: Boolean,
default: true,
},
+ disabled: {
+ type: Boolean,
+ default: false,
+ },
readonly: {
type: Boolean,
default: false,
diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/edit/MasteryCriteriaMofNFields.vue b/contentcuration/contentcuration/frontend/channelEdit/components/edit/MasteryCriteriaMofNFields.vue
index 9ba5a5cf2e..dcb8ddaf94 100644
--- a/contentcuration/contentcuration/frontend/channelEdit/components/edit/MasteryCriteriaMofNFields.vue
+++ b/contentcuration/contentcuration/frontend/channelEdit/components/edit/MasteryCriteriaMofNFields.vue
@@ -13,6 +13,7 @@
:required="mRequired"
:placeholder="mPlaceholder"
:readonly="readonly"
+ :disabled="disabled"
:rules="mRules"
@keypress="isIntegerInput($event)"
@paste="isIntegerPaste($event)"
@@ -33,6 +34,7 @@
:required="nRequired"
:readonly="readonly"
:placeholder="nPlaceholder"
+ :disabled="disabled"
:rules="nRules"
@keypress="isIntegerInput($event)"
@paste="isIntegerPaste($event)"
@@ -78,6 +80,10 @@
type: Boolean,
default: false,
},
+ disabled: {
+ type: Boolean,
+ default: false,
+ },
mRequired: {
type: Boolean,
default: true,
diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/edit/__tests__/activityDuration.spec.js b/contentcuration/contentcuration/frontend/channelEdit/components/edit/__tests__/activityDuration.spec.js
new file mode 100644
index 0000000000..31dcb2414b
--- /dev/null
+++ b/contentcuration/contentcuration/frontend/channelEdit/components/edit/__tests__/activityDuration.spec.js
@@ -0,0 +1,185 @@
+import Vue from 'vue';
+import Vuetify from 'vuetify';
+import { shallowMount, mount } from '@vue/test-utils';
+import ActivityDuration from '../ActivityDuration.vue';
+
+Vue.use(Vuetify);
+
+describe('ActivityDuration', () => {
+ it('smoke test', () => {
+ const wrapper = shallowMount(ActivityDuration);
+ expect(wrapper.isVueInstance()).toBe(true);
+ });
+
+ describe(`minutes input`, () => {
+ const shortActivityMin = 1;
+ const shortActivityMax = 30;
+ const longActivityMin = 31;
+ const longActivityMax = 120;
+ describe(`default state for audio/video resources`, () => {
+ it(`should display a static upload time when 'Exact time to complete' for audio/video resources as initial state`, () => {
+ const defaultValue = '17:12';
+ const wrapper = shallowMount(ActivityDuration);
+ expect(wrapper.vm.defaultUploadTime).toEqual(defaultValue);
+ });
+ it(`should display the file's time at upload when 'Exact time to complete' is chosen in Completion dropdown`, () => {
+ // TODO: defaultValue will need to be changed when file-upload-duration is implemented
+ const defaultValue = '17:12';
+ const wrapper = shallowMount(ActivityDuration, {
+ propsData: { duration: 123 },
+ });
+ expect(wrapper.props('duration')).toEqual(123);
+ expect(wrapper.vm.defaultUploadTime).not.toEqual(defaultValue);
+ expect(wrapper.vm.defaultUploadTime).toEqual(123);
+ });
+ it(`should display a "stand-in" at upload if file's time at upload is not available when 'Exact time to complete' is chosen in Completion dropdown`, () => {
+ // TODO: defaultValue will need to be changed when file-upload-duration is implemented
+ const defaultValue = '17:12';
+ const wrapper = shallowMount(ActivityDuration, {
+ propsData: { duration: null },
+ });
+ expect(wrapper.props('duration')).toEqual(null);
+ expect(wrapper.vm.defaultUploadTime).toEqual(defaultValue);
+ expect(wrapper.vm.defaultUploadTime).not.toEqual(null);
+ });
+ });
+
+ describe(`convert seconds to minutes for display`, () => {
+ it(`should display the seconds passed down from parent as minutes`, () => {
+ const wrapper = shallowMount(ActivityDuration, {
+ propsData: { value: 600 },
+ });
+ expect(wrapper.vm.minutes).toBe(10);
+ wrapper.setProps({ value: 4821 });
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.minutes).toBe(80);
+ });
+ });
+ });
+
+ describe(`convert minutes to seconds to emit to parent`, () => {
+ it(`should emit time to parent`, () => {
+ const seconds = 2400;
+ const wrapper = mount(ActivityDuration);
+ wrapper.vm.minutes = 40;
+ // Ensure debounced function gets called
+ wrapper.vm.handleUpdatedInput.flush();
+
+ return Vue.nextTick().then(() => {
+ const emittedTime = wrapper.emitted('input').pop()[0];
+ expect(emittedTime).toEqual(seconds);
+ });
+ });
+ });
+
+ describe(`in 'Short activity'`, () => {
+ const wrapper = mount(ActivityDuration, {
+ propsData: {
+ selectedDuration: 'shortActivity',
+ },
+ });
+ it(`should increment by 5-minute intervals`, () => {
+ expect(wrapper.html()).toContain(`step="5"`);
+ expect(wrapper.html()).not.toContain(`step="10"`);
+ });
+ it(`minimum accepted input should be 1 minute`, () => {
+ expect(wrapper.vm.minRange).toBe(shortActivityMin);
+ expect(wrapper.vm.minRange).not.toBe(longActivityMin);
+ });
+ it(`maximum accepted input should be 30 minutes`, () => {
+ expect(wrapper.vm.maxRange).toBe(shortActivityMax);
+ expect(wrapper.vm.maxRange).not.toBe(longActivityMax);
+ });
+ });
+
+ describe(`in 'Long activity'`, () => {
+ const wrapper = mount(ActivityDuration, {
+ propsData: {
+ selectedDuration: 'longActivity',
+ },
+ });
+ it(`should increment by 10-minute intervals`, () => {
+ expect(wrapper.html()).toContain(`step="10"`);
+ expect(wrapper.html()).not.toContain(`step="5"`);
+ });
+ it(`minimum accepted input should be 31 minutes`, () => {
+ expect(wrapper.vm.minRange).not.toBe(shortActivityMin);
+ expect(wrapper.vm.minRange).toBe(longActivityMin);
+ });
+ it(`maximum accepted input should be 120 minutes`, () => {
+ expect(wrapper.vm.maxRange).not.toBe(shortActivityMax);
+ expect(wrapper.vm.maxRange).toBe(longActivityMax);
+ });
+ });
+
+ describe(`hints`, () => {
+ const optionalHint =
+ '(Optional) Duration until resource is marked as complete. This value will not be shown to learners.';
+ const requiredHint =
+ 'Duration until resource is marked as complete. This value will not be shown to learners.';
+ describe(`audio/video resource`, () => {
+ it(`should display optional hint when 'Short activity' is chosen`, () => {
+ const wrapper = shallowMount(ActivityDuration, {
+ propsData: {
+ audioVideoUpload: true,
+ selectedDuration: 'shortActivity',
+ },
+ });
+ expect(wrapper.html()).toContain(optionalHint);
+ });
+ it(`should display optional hint when 'Long activity' is chosen`, () => {
+ const wrapper = shallowMount(ActivityDuration, {
+ propsData: {
+ audioVideoUpload: true,
+ selectedDuration: 'longActivity',
+ },
+ });
+ expect(wrapper.html()).toContain(optionalHint);
+ });
+ it(`should not display any hints when 'Exact time to complete' is chosen`, () => {
+ const wrapper = shallowMount(ActivityDuration, {
+ propsData: {
+ audioVideoUpload: true,
+ selectedDuration: 'exactTime',
+ },
+ });
+ expect(wrapper.html()).not.toContain(requiredHint);
+ expect(wrapper.html()).not.toContain(optionalHint);
+ });
+ });
+ describe(`all other resources`, () => {
+ it(`should display optional hint when 'All content viewed' is selected in the Completion dropdown and 'Short/Long activity' is chosen`, () => {
+ const wrapper = shallowMount(ActivityDuration, {
+ propsData: {
+ audioVideoUpload: false,
+ selectedCompletion: 'allContent',
+ selectedDuration: 'shortActivity',
+ },
+ });
+ expect(wrapper.html()).toContain(optionalHint);
+ });
+ it(`should display required hint when 'Complete duration' is selected in the Completion dropdown and 'Short/Long activity' is chosen`, () => {
+ const wrapper = shallowMount(ActivityDuration, {
+ propsData: {
+ audioVideoUpload: false,
+ selectedCompletion: 'completeDuration',
+ selectedDuration: 'shortActivity',
+ },
+ });
+ expect(wrapper.html()).not.toContain('(Optional)');
+ expect(wrapper.html()).toContain(requiredHint);
+ });
+ it(`should not display any hints when 'Exact time to complete' is chosen`, () => {
+ const wrapper = shallowMount(ActivityDuration, {
+ propsData: {
+ audioVideoUpload: false,
+ selectedDuration: 'exactTime',
+ },
+ });
+ expect(wrapper.html()).not.toContain(requiredHint);
+ expect(wrapper.html()).not.toContain(optionalHint);
+ });
+ });
+ });
+ });
+});
diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/edit/__tests__/completionOptions.spec.js b/contentcuration/contentcuration/frontend/channelEdit/components/edit/__tests__/completionOptions.spec.js
new file mode 100644
index 0000000000..6166085567
--- /dev/null
+++ b/contentcuration/contentcuration/frontend/channelEdit/components/edit/__tests__/completionOptions.spec.js
@@ -0,0 +1,786 @@
+import Vue from 'vue';
+import Vuetify from 'vuetify';
+import { shallowMount, mount } from '@vue/test-utils';
+import CompletionOptions from '../CompletionOptions.vue';
+
+Vue.use(Vuetify);
+
+describe('CompletionOptions', () => {
+ it('smoke test', () => {
+ const wrapper = shallowMount(CompletionOptions);
+ expect(wrapper.isVueInstance()).toBe(true);
+ });
+ describe(`completion dropdown`, () => {
+ it(`renders the completion dropdown`, () => {
+ const wrapper = mount(CompletionOptions);
+ const dropdown = wrapper.find({ ref: 'completion' });
+ expect(dropdown.exists()).toBe(true);
+ });
+ describe(`initial, default states`, () => {
+ describe(`audio/video`, () => {
+ it(`'Complete duration' should be displayed by default`, () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'audio',
+ value: { model: null },
+ },
+ });
+ expect(wrapper.vm.completionDropdown).toBe('completeDuration');
+ });
+ it(`'Complete duration' should be displayed if 'Exact time'`, () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'audio',
+ value: { model: 'time', threshold: 600, suggested_duration_type: 'time' },
+ },
+ });
+ expect(wrapper.vm.completionDropdown).toBe('completeDuration');
+ });
+ it(`'Complete duration' should be displayed if 'Short activity'`, () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'audio',
+ value: {
+ model: 'approx_time',
+ threshold: 600,
+ suggested_duration_type: 'approx_time',
+ },
+ },
+ });
+ expect(wrapper.vm.completionDropdown).toBe('completeDuration');
+ });
+ it(`'Complete duration' should be displayed if 'Long activity'`, () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'audio',
+ value: {
+ model: 'approx_time',
+ threshold: 6000,
+ suggested_duration_type: 'approx_time',
+ },
+ },
+ });
+ expect(wrapper.vm.completionDropdown).toBe('completeDuration');
+ });
+ it(`'Reference' should be displayed if the model in the backend is 'reference'`, () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'audio',
+ value: { model: 'reference' },
+ },
+ });
+ expect(wrapper.vm.completionDropdown).toBe('reference');
+ });
+ });
+ describe(`document`, () => {
+ it(`'All content viewed' should be displayed by default`, () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'document',
+ value: { model: null },
+ },
+ });
+ expect(wrapper.vm.completionDropdown).toBe('allContent');
+ });
+ it(`'All content viewed' should be displayed if the model in the backend is 'reference'`, () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'document',
+ value: { model: 'reference' },
+ },
+ });
+ expect(wrapper.vm.completionDropdown).toBe('allContent');
+ });
+ it(`'All content viewed' should be displayed if the model in the backend is 'pages'`, () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'document',
+ value: { model: 'pages', threshold: '100%' },
+ },
+ });
+ expect(wrapper.vm.completionDropdown).toBe('allContent');
+ });
+ it(`'Complete duration' should be displayed if 'exact time'`, () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'document',
+ value: {
+ model: 'time',
+ threshold: 1234,
+ suggested_duration: 1234,
+ suggested_duration_type: 'time',
+ },
+ },
+ });
+ expect(wrapper.vm.completionDropdown).toBe('completeDuration');
+ });
+ it(`'Complete duration' should be displayed if 'short activity'`, () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'document',
+ value: {
+ model: 'approx_time',
+ threshold: 1234,
+ suggested_duration: 1234,
+ suggested_duration_type: 'approx_time',
+ },
+ },
+ });
+ expect(wrapper.vm.completionDropdown).toBe('completeDuration');
+ });
+ it(`'Complete duration' should be displayed if 'long activity'`, () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'document',
+ value: {
+ model: 'time',
+ threshold: 1234567,
+ suggested_duration: 1234567,
+ suggested_duration_type: 'time',
+ },
+ },
+ });
+ expect(wrapper.vm.completionDropdown).toBe('completeDuration');
+ });
+ });
+ describe(`exercise`, () => {
+ it(`'Practice until goal is met' should be displayed by default if 'practice quiz' is enabled `, () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'exercise',
+ value: { model: null, threshold: { m: null, n: null } },
+ },
+ });
+ expect(wrapper.vm.completionDropdown).toBe('goal');
+ });
+ it(`Completion dropdown should not be displayed if 'practice quiz' is not enabled while 'Practice until goal is met' is set in background`, () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'exercise',
+ value: { model: null, threshold: { m: null, n: null }, modality: 'QUIZ' },
+ practiceQuizzesAllowed: false,
+ },
+ });
+ expect(wrapper.find({ ref: 'completion' }).exists()).toBe(false);
+ expect(wrapper.vm.completionDropdown).toBe('goal');
+ });
+ });
+ describe(`html5 or h5p`, () => {
+ it(`'Complete duration' should be displayed by default for html5`, () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'html5',
+ value: { model: null },
+ },
+ });
+ expect(wrapper.vm.completionDropdown).toBe('completeDuration');
+ });
+ it(`'Determined by this resource' should be displayed if there is no model in the backend for h5p`, () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'h5p',
+ value: { model: null },
+ },
+ });
+ expect(wrapper.vm.completionDropdown).toBe('determinedByResource');
+ });
+ });
+ });
+ describe(`changing states`, () => {
+ describe('emitted events', () => {
+ it('input should be emitted when completion dropdown is updated', async () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'document',
+ value: { model: null },
+ },
+ });
+ wrapper.find({ ref: 'completion' }).vm.$emit('input', 'allContent');
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.emitted('input')).toBeTruthy();
+ });
+ });
+ describe(`audio/video`, () => {
+ it(`'Duration dropdown' is not visible and reference hint is visible when 'Reference' is selected`, () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'audio',
+ value: { model: 'reference' },
+ },
+ });
+ expect(wrapper.vm.showReferenceHint).toBe(true);
+ expect(wrapper.find({ ref: 'duration' }).exists()).toBe(false);
+ });
+ });
+ describe(`exercise`, () => {
+ it(`Goal and MofN components should not be displayed when switching to PQ from PUGIM`, async () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'exercise',
+ value: { model: 'mastery', threshold: { m: 3, n: 5 }, modality: 'QUIZ' },
+ },
+ });
+ wrapper.find({ ref: 'completion' }).vm.$emit('input', 'practiceQuiz');
+ await wrapper.vm.$nextTick();
+ expect(wrapper.vm.showMasteryCriteriaGoalDropdown).toBe(false);
+ expect(wrapper.vm.showMofN).toBe(false);
+ });
+ });
+ });
+ });
+ describe(`duration dropdown`, () => {
+ it(`renders the duration dropdown`, () => {
+ const wrapper = mount(CompletionOptions);
+ const dropdown = wrapper.find({ ref: 'duration' });
+ expect(dropdown.exists()).toBe(true);
+ });
+ describe(`default states`, () => {
+ it(`'Exact time to complete' should be displayed by default for audio or video`, () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'audio',
+ value: { suggested_duration: null },
+ },
+ });
+ expect(wrapper.vm.durationDropdown).toBe('exactTime');
+ });
+ it(`duration dropdown is empty by default for documents`, () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'document',
+ value: { suggested_duration: null },
+ },
+ });
+ expect(wrapper.vm.durationDropdown).toBe('');
+ });
+ it(`duration dropdown is empty by default for exercises`, () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'exercise',
+ value: { model: 'mastery', threshold: { m: 3, n: 5 }, suggested_duration: null },
+ },
+ });
+ expect(wrapper.vm.durationDropdown).toBe('');
+ });
+ it(`'Reference' is disabled for exercises`, async () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'exercise',
+ value: { model: 'mastery', threshold: { m: 3, n: 5 } },
+ },
+ });
+ wrapper.find({ ref: 'completion' }).vm.$emit('input', 'completeDuration');
+ await wrapper.vm.$nextTick();
+
+ const clickableDurationDropdown = wrapper.vm.selectableDurationOptions;
+ expect(clickableDurationDropdown.length).toBe(3);
+ });
+ it(`duration dropdown is empty by default for html5`, () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'html5',
+ value: { suggested_duration: null },
+ },
+ });
+ expect(wrapper.vm.durationDropdown).toBe('');
+ });
+ it(`duration dropdown is hidden by default for h5p`, () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'h5p',
+ value: { suggested_duration: null },
+ },
+ });
+ const dropdown = wrapper.find({ ref: 'duration' });
+ expect(dropdown.exists()).toBe(false);
+ });
+ });
+ describe('emitted events', () => {
+ it('input should be emitted when duration dropdown is updated', async () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'document',
+ value: { model: null },
+ },
+ });
+ wrapper.find({ ref: 'duration' }).vm.$emit('input', 'shortActivity');
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.emitted('input')).toBeTruthy();
+ });
+ });
+ describe(`audio/video`, () => {
+ it(`minutes input is displayed when 'Short activity' is selected`, async () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'audio',
+ value: { model: null },
+ },
+ });
+ wrapper.find({ ref: 'duration' }).vm.$emit('input', 'shortActivity');
+ await wrapper.vm.$nextTick();
+ expect(wrapper.find({ ref: 'activity_duration' }).exists()).toBe(true);
+ expect(wrapper.vm.showActivityDurationInput).toBe(true);
+ });
+ it(`minutes input is displayed when 'Long activity' is selected`, async () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'audio',
+ value: { model: null },
+ },
+ });
+ wrapper.find({ ref: 'duration' }).vm.$emit('input', 'longActivity');
+ await wrapper.vm.$nextTick();
+ expect(wrapper.find({ ref: 'activity_duration' }).exists()).toBe(true);
+ expect(wrapper.vm.showActivityDurationInput).toBe(true);
+ });
+ });
+ describe(`document`, () => {
+ describe(`'All content viewed' is selected as completion`, () => {
+ it(`minutes input is displayed when 'Short activity' is selected`, async () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'document',
+ value: { model: 'pages' },
+ },
+ });
+ wrapper.find({ ref: 'duration' }).vm.$emit('input', 'shortActivity');
+ await wrapper.vm.$nextTick();
+ expect(wrapper.find({ ref: 'activity_duration' }).exists()).toBe(true);
+ expect(wrapper.vm.showActivityDurationInput).toBe(true);
+ });
+ it(`minutes input is displayed when 'Long activity' is selected`, async () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'document',
+ value: { model: 'pages' },
+ },
+ });
+ wrapper.find({ ref: 'duration' }).vm.$emit('input', 'longActivity');
+ await wrapper.vm.$nextTick();
+ expect(wrapper.find({ ref: 'activity_duration' }).exists()).toBe(true);
+ expect(wrapper.vm.showActivityDurationInput).toBe(true);
+ });
+ it(`minutes input is displayed when 'Exact time' is selected`, async () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'document',
+ value: { model: 'pages' },
+ },
+ });
+ wrapper.find({ ref: 'duration' }).vm.$emit('input', 'exactTime');
+ await wrapper.vm.$nextTick();
+ expect(wrapper.find({ ref: 'activity_duration' }).exists()).toBe(true);
+ expect(wrapper.vm.showActivityDurationInput).toBe(true);
+ });
+ it(`minutes input is hidden and reference hint is displayed when 'Reference' is selected`, () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'document',
+ value: { model: 'reference' },
+ },
+ });
+ expect(wrapper.find({ ref: 'activity_duration' }).exists()).toBe(false);
+ expect(wrapper.vm.showActivityDurationInput).toBe(false);
+ expect(wrapper.vm.showReferenceHint).toBe(true);
+ });
+ });
+ describe(`'Complete duration' is selected as completion`, () => {
+ it(`minutes input is displayed when 'Short activity' is selected`, async () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'document',
+ value: { model: 'approx_time' },
+ },
+ });
+ wrapper.find({ ref: 'duration' }).vm.$emit('input', 'shortActivity');
+ await wrapper.vm.$nextTick();
+ expect(wrapper.find({ ref: 'activity_duration' }).exists()).toBe(true);
+ expect(wrapper.vm.showActivityDurationInput).toBe(true);
+ });
+ it(`minutes input is displayed when 'Long activity' is selected`, async () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'document',
+ value: { model: 'approx_time' },
+ },
+ });
+ wrapper.find({ ref: 'duration' }).vm.$emit('input', 'longActivity');
+ await wrapper.vm.$nextTick();
+ expect(wrapper.find({ ref: 'activity_duration' }).exists()).toBe(true);
+ expect(wrapper.vm.showActivityDurationInput).toBe(true);
+ });
+ it(`minutes input is displayed when 'Exact time' is selected`, async () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'document',
+ value: { model: 'time' },
+ },
+ });
+ wrapper.find({ ref: 'duration' }).vm.$emit('input', 'exactTime');
+ await wrapper.vm.$nextTick();
+ expect(wrapper.find({ ref: 'activity_duration' }).exists()).toBe(true);
+ expect(wrapper.vm.showActivityDurationInput).toBe(true);
+ });
+ it(`'Reference' is disabled`, async () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'document',
+ value: { model: 'time' },
+ },
+ });
+ wrapper.find({ ref: 'completion' }).vm.$emit('input', 'completeDuration');
+ await wrapper.vm.$nextTick();
+
+ const clickableDurationDropdown = wrapper.vm.selectableDurationOptions;
+ const reference = clickableDurationDropdown.filter(
+ option => option.value === 'reference'
+ );
+ expect(clickableDurationDropdown.length).toBe(4);
+ expect(reference[0].disabled).toBe(true);
+ });
+ });
+ describe(`switching between 'All content viewed (ACV)' and 'Complete duration (CD)'`, () => {
+ it(`Duration dropdown and minutes input stay the same when switching betweeen 'Short activity' in ACV and CD`, async () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'document',
+ value: {
+ model: 'approx_time',
+ suggested_duration: 1200,
+ suggested_duration_type: 'approx_time',
+ },
+ },
+ });
+ expect(wrapper.vm.durationDropdown).toBe('shortActivity');
+ wrapper.find({ ref: 'completion' }).vm.$emit('input', 'allContent');
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.durationDropdown).toBe('shortActivity');
+ expect(wrapper.find({ ref: 'activity_duration' }).exists()).toBe(true);
+ expect(wrapper.vm.showActivityDurationInput).toBe(true);
+ expect(wrapper.vm.minutes).toBe(1200);
+ });
+ it(`Duration dropdown and minutes input stay the same when switching betweeen 'Long activity' in ACV and CD`, async () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'document',
+ value: {
+ model: 'approx_time',
+ suggested_duration: 6000,
+ suggested_duration_type: 'approx_time',
+ },
+ },
+ });
+ expect(wrapper.vm.durationDropdown).toBe('longActivity');
+ wrapper.find({ ref: 'completion' }).vm.$emit('input', 'allContent');
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.durationDropdown).toBe('longActivity');
+ expect(wrapper.find({ ref: 'activity_duration' }).exists()).toBe(true);
+ expect(wrapper.vm.showActivityDurationInput).toBe(true);
+ expect(wrapper.vm.minutes).toBe(6000);
+ });
+ it(`Duration dropdown and minutes input stay the same when switching betweeen 'Exact time' in ACV and CD`, async () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'document',
+ value: {
+ model: 'pages',
+ threshold: '100%',
+ suggested_duration: 1234,
+ suggested_duration_type: 'time',
+ },
+ },
+ });
+ expect(wrapper.vm.durationDropdown).toBe('exactTime');
+ wrapper.find({ ref: 'completion' }).vm.$emit('input', 'completeDuration');
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.durationDropdown).toBe('exactTime');
+ expect(wrapper.find({ ref: 'activity_duration' }).exists()).toBe(true);
+ expect(wrapper.vm.showActivityDurationInput).toBe(true);
+ expect(wrapper.vm.minutes).toBe(1234);
+ });
+ });
+ });
+ describe(`exercise`, () => {
+ describe(`when completion dropdown is 'Practice until goal is met'`, () => {
+ it(`minutes input is displayed when 'Short activity' is selected`, async () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'exercise',
+ value: { model: 'mastery', threshold: { m: 3, n: 5 } },
+ },
+ });
+ wrapper.find({ ref: 'duration' }).vm.$emit('input', 'shortActivity');
+ await wrapper.vm.$nextTick();
+ expect(wrapper.find({ ref: 'activity_duration' }).exists()).toBe(true);
+ expect(wrapper.vm.showActivityDurationInput).toBe(true);
+ });
+ it(`minutes input is displayed when 'Long activity' is selected`, async () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'exercise',
+ value: { model: 'mastery', threshold: { m: 3, n: 5 } },
+ },
+ });
+ wrapper.find({ ref: 'duration' }).vm.$emit('input', 'longActivity');
+ await wrapper.vm.$nextTick();
+ expect(wrapper.find({ ref: 'activity_duration' }).exists()).toBe(true);
+ expect(wrapper.vm.showActivityDurationInput).toBe(true);
+ });
+ it(`minutes input is displayed when 'Exact time' is selected`, async () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'exercise',
+ value: { model: 'mastery', threshold: { m: 3, n: 5 } },
+ },
+ });
+ wrapper.find({ ref: 'duration' }).vm.$emit('input', 'exactTime');
+ await wrapper.vm.$nextTick();
+ expect(wrapper.find({ ref: 'activity_duration' }).exists()).toBe(true);
+ expect(wrapper.vm.showActivityDurationInput).toBe(true);
+ });
+ });
+ describe(`when completion dropdown is 'Practice quiz'`, () => {
+ it(`minutes input is displayed when 'Short activity' is selected`, async () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'exercise',
+ value: { model: 'mastery', threshold: { m: 3, n: 5 }, modality: 'QUIZ' },
+ },
+ });
+ wrapper.find({ ref: 'duration' }).vm.$emit('input', 'shortActivity');
+ await wrapper.vm.$nextTick();
+ expect(wrapper.find({ ref: 'activity_duration' }).exists()).toBe(true);
+ expect(wrapper.vm.showActivityDurationInput).toBe(true);
+ });
+ it(`minutes input is displayed when 'Long activity' is selected`, async () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'exercise',
+ value: { model: 'mastery', threshold: { m: 3, n: 5 }, modality: 'QUIZ' },
+ },
+ });
+ wrapper.find({ ref: 'duration' }).vm.$emit('input', 'longActivity');
+ await wrapper.vm.$nextTick();
+ expect(wrapper.find({ ref: 'activity_duration' }).exists()).toBe(true);
+ expect(wrapper.vm.showActivityDurationInput).toBe(true);
+ });
+ it(`minutes input is displayed when 'Exact time' is selected`, async () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'exercise',
+ value: { model: 'mastery', threshold: { m: 3, n: 5 }, modality: 'QUIZ' },
+ },
+ });
+ wrapper.find({ ref: 'duration' }).vm.$emit('input', 'exactTime');
+ await wrapper.vm.$nextTick();
+ expect(wrapper.find({ ref: 'activity_duration' }).exists()).toBe(true);
+ expect(wrapper.vm.showActivityDurationInput).toBe(true);
+ });
+ });
+ describe(`switching between 'Practice until goal is met (PUGIM)' and 'Practice quiz (PQ)'`, () => {
+ it(`Duration dropdown and minutes input stay the same when switching betweeen 'Short activity' in PUGIM and PQ`, async () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'exercise',
+ value: {
+ model: 'mastery',
+ modality: 'QUIZ',
+ suggested_duration: 1200,
+ suggested_duration_type: 'approx_time',
+ threshold: { m: 3, n: 5 },
+ },
+ },
+ });
+ expect(wrapper.vm.durationDropdown).toBe('shortActivity');
+ wrapper.find({ ref: 'completion' }).vm.$emit('input', 'goal');
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.durationDropdown).toBe('shortActivity');
+ expect(wrapper.find({ ref: 'activity_duration' }).exists()).toBe(true);
+ expect(wrapper.vm.showActivityDurationInput).toBe(true);
+ expect(wrapper.vm.minutes).toBe(1200);
+ });
+ it(`Duration dropdown and minutes input stay the same when switching betweeen 'Long activity' in PUGIM and PQ`, async () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'exercise',
+ value: {
+ model: 'mastery',
+ modality: 'QUIZ',
+ suggested_duration: 6000,
+ suggested_duration_type: 'approx_time',
+ threshold: { m: 3, n: 5 },
+ },
+ },
+ });
+ expect(wrapper.vm.durationDropdown).toBe('longActivity');
+ wrapper.find({ ref: 'completion' }).vm.$emit('input', 'goal');
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.durationDropdown).toBe('longActivity');
+ expect(wrapper.find({ ref: 'activity_duration' }).exists()).toBe(true);
+ expect(wrapper.vm.showActivityDurationInput).toBe(true);
+ expect(wrapper.vm.minutes).toBe(6000);
+ });
+ it(`Duration dropdown and minutes input stay the same when switching betweeen 'Exact time' in PUGIM and PQ`, async () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'exercise',
+ value: {
+ model: 'mastery',
+ modality: 'QUIZ',
+ suggested_duration: 1234,
+ suggested_duration_type: 'time',
+ threshold: { m: 3, n: 5 },
+ },
+ },
+ });
+ expect(wrapper.vm.durationDropdown).toBe('exactTime');
+ wrapper.find({ ref: 'completion' }).vm.$emit('input', 'completeDuration');
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.durationDropdown).toBe('exactTime');
+ expect(wrapper.find({ ref: 'activity_duration' }).exists()).toBe(true);
+ expect(wrapper.vm.showActivityDurationInput).toBe(true);
+ expect(wrapper.vm.minutes).toBe(1234);
+ });
+ });
+ });
+ describe(`html5 or h5p`, () => {
+ describe(`when completion dropdown is 'Determined by this resource'`, () => {
+ it(`minutes input is displayed when 'Short activity' is selected`, async () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'html5',
+ value: { suggested_duration: null, model: 'approx_time' },
+ },
+ });
+ wrapper.find({ ref: 'duration' }).vm.$emit('input', 'shortActivity');
+ await wrapper.vm.$nextTick();
+ expect(wrapper.find({ ref: 'activity_duration' }).exists()).toBe(true);
+ expect(wrapper.vm.showActivityDurationInput).toBe(true);
+ });
+ it(`minutes input is displayed when 'Long activity' is selected`, async () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'html5',
+ value: { suggested_duration: null, model: 'approx_time' },
+ },
+ });
+ wrapper.find({ ref: 'duration' }).vm.$emit('input', 'longActivity');
+ await wrapper.vm.$nextTick();
+ expect(wrapper.find({ ref: 'activity_duration' }).exists()).toBe(true);
+ expect(wrapper.vm.showActivityDurationInput).toBe(true);
+ });
+ it(`minutes input is displayed when 'Exact time' is selected`, async () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'html5',
+ value: { suggested_duration: null, model: 'time' },
+ },
+ });
+ wrapper.find({ ref: 'duration' }).vm.$emit('input', 'exactTime');
+ await wrapper.vm.$nextTick();
+ expect(wrapper.find({ ref: 'activity_duration' }).exists()).toBe(true);
+ expect(wrapper.vm.showActivityDurationInput).toBe(true);
+ });
+ it(`minutes input is hidden and reference hint is displayed when 'Reference' is selected`, () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'html5',
+ value: { suggested_duration: null, model: 'reference' },
+ },
+ });
+ expect(wrapper.find({ ref: 'activity_duration' }).exists()).toBe(false);
+ expect(wrapper.vm.showActivityDurationInput).toBe(false);
+ expect(wrapper.vm.showReferenceHint).toBe(true);
+ });
+ });
+ });
+ });
+ describe(`minutes input`, () => {
+ //Note: while the 'ActivityDuration' component itself is in another component,
+ //the logic to get the data ready for the BE is in this component
+ describe(`correct handling of values for switching from 'Exact time' to 'Short activity' or 'Long activity'`, () => {
+ it(`displays default 'Short activity' value when input > the max allowed for 'Short activity'`, () => {
+ const shortActivityDefaultValue = 600;
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'document',
+ value: {
+ model: 'pages',
+ threshold: '100%',
+ suggested_duration: 3060,
+ suggested_duration_type: 'time',
+ },
+ },
+ });
+
+ expect(wrapper.vm.handleMinutesInputFromActivityDuration(3060, `shortActivity`)).toBe(
+ shortActivityDefaultValue
+ );
+ });
+ it(`displays default 'Long activity' value when input < the min allowed for 'Long activity'`, () => {
+ const longActivityDefaultValue = 3000;
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'document',
+ value: {
+ model: 'pages',
+ threshold: '100%',
+ suggested_duration: 50,
+ suggested_duration_type: 'time',
+ },
+ },
+ });
+
+ expect(wrapper.vm.handleMinutesInputFromActivityDuration(50, `longActivity`)).toBe(
+ longActivityDefaultValue
+ );
+ });
+ });
+ describe(`correct handling of values for switching from 'Short activity' or 'Long activity' to 'Exact Time'`, () => {
+ it(`displays 'Long activity' value`, async () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'document',
+ value: {
+ model: 'pages',
+ threshold: '100%',
+ suggested_duration: 4200,
+ suggested_duration_type: 'approx_time',
+ },
+ },
+ });
+ wrapper.find({ ref: 'duration' }).vm.$emit('input', 'exactTime');
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.currentDurationDropdown).toBe('exactTime');
+ expect(wrapper.vm.minutes).toBe(4200);
+ });
+ it(`displays 'Short activity' value`, async () => {
+ const wrapper = mount(CompletionOptions, {
+ propsData: {
+ kind: 'document',
+ value: {
+ model: 'pages',
+ threshold: '100%',
+ suggested_duration: 200,
+ suggested_duration_type: 'approx_time',
+ },
+ },
+ });
+ wrapper.find({ ref: 'duration' }).vm.$emit('input', 'exactTime');
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.currentDurationDropdown).toBe('exactTime');
+ expect(wrapper.vm.minutes).toBe(200);
+ });
+ });
+ });
+});
diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/edit/__tests__/masteryCriteriaGoal.spec.js b/contentcuration/contentcuration/frontend/channelEdit/components/edit/__tests__/masteryCriteriaGoal.spec.js
new file mode 100644
index 0000000000..49cc0159b5
--- /dev/null
+++ b/contentcuration/contentcuration/frontend/channelEdit/components/edit/__tests__/masteryCriteriaGoal.spec.js
@@ -0,0 +1,97 @@
+import { mount } from '@vue/test-utils';
+import TestForm from '../../../../shared/views/__tests__/TestForm.vue';
+import MasteryCriteriaGoal from '../MasteryCriteriaGoal.vue';
+import { constantStrings } from 'shared/mixins';
+import MasteryModels from 'shared/leUtils/MasteryModels';
+
+document.body.setAttribute('data-app', true); // Vuetify prints a warning without this
+
+describe('MasteryCriteriaGoal', () => {});
+function makeWrapper() {
+ return mount(TestForm, {
+ slots: {
+ testComponent: MasteryCriteriaGoal,
+ },
+ });
+}
+
+describe('masteryCriteriaGoal', () => {
+ let formWrapper;
+ let wrapper;
+ let modelInput;
+
+ beforeEach(() => {
+ formWrapper = makeWrapper();
+ wrapper = formWrapper.find(MasteryCriteriaGoal);
+ wrapper.setProps({ value: { mastery_model: 'm_of_n' } });
+ modelInput = wrapper.find({ ref: 'masteryModel' }).find('input');
+ });
+
+ describe('on load', () => {
+ MasteryModels.forEach(model => {
+ it(`${model} mastery option should be an option to select`, () => {
+ expect(wrapper.find('.v-list').text()).toContain(constantStrings.$tr(model));
+ });
+ });
+ it('should render according to masteryModel prop', () => {
+ function test(model) {
+ wrapper.setProps({ value: { mastery_model: model } });
+ expect(wrapper.vm.$refs.masteryModel.value).toEqual(model);
+ }
+ MasteryModels.forEach(test);
+ });
+ });
+ describe('props', () => {
+ beforeEach(() => {});
+ it('setting readonly should prevent any edits', () => {
+ wrapper.setProps({ readonly: true });
+ wrapper.vm.$nextTick(() => {
+ expect(modelInput.attributes('readonly')).toEqual('readonly');
+ });
+ });
+ it('setting required to false should make fields not required (required by default)', () => {
+ expect(modelInput.attributes('required')).toEqual('required');
+
+ wrapper.setProps({ required: false });
+ wrapper.vm.$nextTick(() => {
+ expect(modelInput.attributes('required')).toBeFalsy();
+ });
+ });
+ it('setting disabled should make fields disabled', () => {
+ expect(modelInput.attributes('disabled')).toBeFalsy();
+
+ wrapper.setProps({ disabled: true });
+ wrapper.vm.$nextTick(() => {
+ expect(modelInput.attributes('disabled')).toEqual('disabled');
+ });
+ });
+ });
+ describe('emitted events', () => {
+ it('input should be emitted when masteryModel is updated', () => {
+ expect(wrapper.emitted('input')).toBeFalsy();
+ modelInput.setValue('do_all');
+ expect(wrapper.emitted('input')).toBeTruthy();
+ expect(wrapper.emitted('input')[0][0].mastery_model).toEqual('do_all');
+ });
+ });
+ describe('validation', () => {
+ it('should flag empty required mastery models', () => {
+ wrapper.setProps({ value: { mastery_model: null } });
+ formWrapper.vm.validate();
+ expect(
+ wrapper
+ .find({ ref: 'masteryModel' })
+ .find('.error--text')
+ .exists()
+ ).toBe(true);
+ wrapper.setProps({ required: false });
+ formWrapper.vm.validate();
+ expect(
+ wrapper
+ .find({ ref: 'masteryModel' })
+ .find('.error--text')
+ .exists()
+ ).toBe(false);
+ });
+ });
+});
diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/edit/__tests__/masteryCriteriaMofNFields.spec.js b/contentcuration/contentcuration/frontend/channelEdit/components/edit/__tests__/masteryCriteriaMofNFields.spec.js
new file mode 100644
index 0000000000..573009eaf8
--- /dev/null
+++ b/contentcuration/contentcuration/frontend/channelEdit/components/edit/__tests__/masteryCriteriaMofNFields.spec.js
@@ -0,0 +1,176 @@
+import { mount } from '@vue/test-utils';
+import TestForm from '../../../../shared/views/__tests__/TestForm.vue';
+import MasteryCriteriaMofNFields from '../MasteryCriteriaMofNFields.vue';
+
+document.body.setAttribute('data-app', true); // Vuetify prints a warning without this
+
+describe('MasteryCriteriaMofNFields', () => {});
+function makeWrapper() {
+ return mount(TestForm, {
+ slots: {
+ testComponent: MasteryCriteriaMofNFields,
+ },
+ });
+}
+
+describe('masteryCriteriaMofNFields', () => {
+ let formWrapper;
+ let wrapper;
+ let mInput;
+ let nInput;
+
+ beforeEach(() => {
+ formWrapper = makeWrapper();
+ wrapper = formWrapper.find(MasteryCriteriaMofNFields);
+ wrapper.setProps({ showMofN: true });
+ wrapper.setProps({ value: { mastery_model: 'm_of_n' } });
+ wrapper.vm.$nextTick(() => {
+ mInput = wrapper.find({ ref: 'mValue' }).find('input');
+ nInput = wrapper.find({ ref: 'nValue' }).find('input');
+ });
+ });
+
+ describe('on load', () => {
+ it('should render according to masteryModel prop', () => {
+ const model = 'm_of_n';
+ expect(wrapper.find({ ref: 'mValue' }).exists()).toBe(model === 'm_of_n');
+ expect(wrapper.find({ ref: 'nValue' }).exists()).toBe(model === 'm_of_n');
+ });
+ it('should render correct mValue and nValue props', () => {
+ wrapper.setProps({ value: { m: 10, n: 20 } });
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.vm.$refs.mValue.value).toEqual(10);
+ expect(wrapper.vm.$refs.nValue.value).toEqual(20);
+ });
+ });
+ });
+ describe('props', () => {
+ beforeEach(() => {});
+ it('setting readonly should prevent any edits', () => {
+ wrapper.setProps({ readonly: true });
+ return wrapper.vm.$nextTick(() => {
+ expect(mInput.attributes('readonly')).toEqual('readonly');
+ expect(nInput.attributes('readonly')).toEqual('readonly');
+ });
+ });
+ it('setting required to false should make fields not required (required by default)', () => {
+ expect(mInput.attributes('required')).toEqual('required');
+ expect(nInput.attributes('required')).toEqual('required');
+
+ wrapper.setProps({ mRequired: false, nRequired: false });
+ return wrapper.vm.$nextTick(() => {
+ expect(mInput.attributes('required')).toBeFalsy();
+ expect(nInput.attributes('required')).toBeFalsy();
+ });
+ });
+ it('setting disabled should make fields disabled', () => {
+ expect(mInput.attributes('disabled')).toBeFalsy();
+ expect(nInput.attributes('disabled')).toBeFalsy();
+
+ wrapper.setProps({ disabled: true });
+ return wrapper.vm.$nextTick(() => {
+ expect(mInput.attributes('disabled')).toEqual('disabled');
+ expect(nInput.attributes('disabled')).toEqual('disabled');
+ });
+ });
+ });
+ describe('emitted events', () => {
+ it('input should be emitted when mValue is updated', () => {
+ expect(wrapper.emitted('input')).toBeFalsy();
+ mInput.setValue(10);
+ expect(wrapper.emitted('input')).toBeTruthy();
+ expect(wrapper.emitted('input')[0][0].m).toEqual(10);
+ });
+ it('input should be emitted when nValue is updated', () => {
+ expect(wrapper.emitted('input')).toBeFalsy();
+ nInput.setValue(10);
+ expect(wrapper.emitted('input')).toBeTruthy();
+ expect(wrapper.emitted('input')[0][0].n).toEqual(10);
+ });
+ });
+ describe('validation', () => {
+ it('should flag empty n and m values', () => {
+ formWrapper.vm.validate();
+ expect(
+ wrapper
+ .find({ ref: 'mValue' })
+ .find('.error--text')
+ .exists()
+ ).toBe(true);
+ expect(
+ wrapper
+ .find({ ref: 'nValue' })
+ .find('.error--text')
+ .exists()
+ ).toBe(true);
+ wrapper.setProps({ mRequired: false, nRequired: false });
+ formWrapper.vm.validate();
+ expect(
+ wrapper
+ .find({ ref: 'mValue' })
+ .find('.error--text')
+ .exists()
+ ).toBe(false);
+ expect(
+ wrapper
+ .find({ ref: 'nValue' })
+ .find('.error--text')
+ .exists()
+ ).toBe(false);
+ });
+ it('should flag if m is not a whole number', () => {
+ wrapper.setProps({ value: { mastery_model: 'm_of_n', m: 0.1231, n: 10 } });
+ formWrapper.vm.validate();
+ expect(
+ wrapper
+ .find({ ref: 'mValue' })
+ .find('.error--text')
+ .exists()
+ ).toBe(true);
+ wrapper.setProps({ value: { mastery_model: 'm_of_n', m: 1, n: 10 } });
+ formWrapper.vm.validate();
+ expect(
+ wrapper
+ .find({ ref: 'mValue' })
+ .find('.error--text')
+ .exists()
+ ).toBe(false);
+ });
+ it('should flag if m < 1', () => {
+ wrapper.setProps({ value: { mastery_model: 'm_of_n', m: 0, n: 10 } });
+ formWrapper.vm.validate();
+ expect(
+ wrapper
+ .find({ ref: 'mValue' })
+ .find('.error--text')
+ .exists()
+ ).toBe(true);
+ wrapper.setProps({ value: { mastery_model: 'm_of_n', m: 1, n: 10 } });
+ formWrapper.vm.validate();
+ expect(
+ wrapper
+ .find({ ref: 'mValue' })
+ .find('.error--text')
+ .exists()
+ ).toBe(false);
+ });
+ it('should flag if m > n', () => {
+ wrapper.setProps({ value: { mastery_model: 'm_of_n', m: 2, n: 1 } });
+ formWrapper.vm.validate();
+ expect(
+ wrapper
+ .find({ ref: 'mValue' })
+ .find('.error--text')
+ .exists()
+ ).toBe(true);
+ wrapper.setProps({ value: { mastery_model: 'm_of_n', m: 2, n: 2 } });
+ formWrapper.vm.validate();
+ expect(
+ wrapper
+ .find({ ref: 'mValue' })
+ .find('.error--text')
+ .exists()
+ ).toBe(false);
+ });
+ });
+});