diff --git a/task-launcher/cypress/e2e/math.cy.js b/task-launcher/cypress/e2e/math.cy.js index 11e38be6..9045e3b3 100644 --- a/task-launcher/cypress/e2e/math.cy.js +++ b/task-launcher/cypress/e2e/math.cy.js @@ -5,6 +5,6 @@ const math_url = 'http://localhost:8080/?task=egma-math'; describe('test math', () => { it('visits math and plays game', () => { cy.visit(math_url); - testAfc('alt', '.secondary'); + testAfc('alt', '.secondary, .image-medium, .primary'); }); }); diff --git a/task-launcher/src/tasks/math/timeline.ts b/task-launcher/src/tasks/math/timeline.ts index f23925d5..c29ff8a6 100644 --- a/task-launcher/src/tasks/math/timeline.ts +++ b/task-launcher/src/tasks/math/timeline.ts @@ -23,6 +23,7 @@ import { taskFinished, practiceTransition, feedback, + setupDownex, } from '../shared/trials'; import { getLayoutConfig } from './helpers/config'; import { taskStore } from '../../taskStore'; @@ -56,23 +57,17 @@ export default function buildMathTimeline(config: Record, mediaAsse const timeline = [preloadTrials, initialTimeline]; let corpus: StimulusType[] = taskStore().corpora.stimulus; + const downexCorpus: StimulusType[] = taskStore().corpora.downex; const translations: Record = taskStore().translations; const validationErrorMap: Record = {}; const { runCat, heavyInstructions } = taskStore(); - // block 3 is only for younger kids - if (!runCat) { - corpus = corpus.filter((trial) => { - return heavyInstructions ? trial.block_index === 3 : trial.block_index !== 3; - }); - } - taskStore('totalTrials', corpus.length); const layoutConfigMap: Record = {}; let i = 0; - for (const c of corpus) { + for (const c of [...downexCorpus, ...corpus]) { const { itemConfig, errorMessages } = getLayoutConfig(c, translations, mediaAssets, i); layoutConfigMap[c.itemId] = itemConfig; if (errorMessages.length) { @@ -217,79 +212,96 @@ export default function buildMathTimeline(config: Record, mediaAsse if (runCat) { // puts the CAT portion of the corpus into taskStore and removes instructions - const fullCorpus = prepareCorpus(corpus); - // until younger-kid version of math is implemented, combine heavy/light instructions - const allInstructionPractice = fullCorpus.ipLight.concat(fullCorpus.ipHeavy); - const instructions = allInstructionPractice.filter((trial) => trial.trialType == 'instructions'); - - // filter practice trials to only include appropriate trial types if downward extension - const excludedDownexPracticeTypes = [ - 'Addition', - 'Number Comparison', - 'Number Identification', - 'Counting', - 'Counting AFC', - ]; - - const practice = allInstructionPractice.filter((trial) => { - return trial.assessmentStage == 'practice_response' && !(trial.block_index === 3 && excludedDownexPracticeTypes.includes(trial.trialType)); - }); + const allCorpusParts = prepareCorpus(corpus, true, downexCorpus); + const olderKidInstructionPractice: StimulusType[] = allCorpusParts.ipLight; + const olderKidInstructions: StimulusType[] = olderKidInstructionPractice.filter((trial: StimulusType) => trial.trialType == 'instructions'); + let olderKidPractice: StimulusType[] = olderKidInstructionPractice.filter((trial: StimulusType) => trial.assessmentStage == 'practice_response'); - let allBlocks: StimulusType[][] = prepareMultiBlockCat(taskStore().corpora.stimulus); - let downexBlock = allBlocks[3]; - - // remove items from first block that are already in subsequent blocks - const nonDownexIds: string[] = []; - allBlocks - .slice(0, -1) - .flat() - .map((trial) => nonDownexIds.push(trial.itemId as string)); - - downexBlock = downexBlock.filter((trial: StimulusType) => { - return !nonDownexIds.includes(trial.itemId as string); - }); + let olderKidBlocks: StimulusType[][] = prepareMultiBlockCat(taskStore().corpora.stimulus); + taskStore('corpora', { stimulus: olderKidBlocks, downex: taskStore().corpora.downex }); + taskStore('totalTestTrials', 0); // add to this while building out each block - // move downex block to the beginning - allBlocks = [downexBlock, ...allBlocks.slice(0, 3)]; + // don't repeat instructions + const usedIds: string[] = []; - const newCorpora = { - practice: taskStore().corpora.practice, - stimulus: allBlocks, - }; - taskStore('corpora', newCorpora); // puts all blocks into taskStore - taskStore('totalTestTrials', 0); // add to this while building out each block + // first add downex trials to the timeline + if (heavyInstructions) { + const downexInstructionPractice: StimulusType[] = allCorpusParts.ipHeavy; + const downexInstructions: StimulusType[] = downexInstructionPractice.filter((trial) => trial.trialType == 'instructions'); + let downexPractice: StimulusType[] = downexInstructionPractice.filter((trial) => trial.assessmentStage == 'practice_response'); + + let downexBlock: StimulusType[] = allCorpusParts.downexCat; + + // remove items from first block that are already in subsequent blocks + const nonDownexIds: string[] = []; + olderKidBlocks.flat().map((trial) => nonDownexIds.push(trial.itemId as string)); + + downexBlock = downexBlock.filter((trial: StimulusType) => { + return !nonDownexIds.includes(trial.itemId as string); + }); + + // filter practice trials to only include appropriate trial types if downward extension + const excludedDownexPracticeTypes = [ + 'Addition', + 'Number Comparison', + 'Number Identification', + 'Counting', + 'Counting AFC', + ]; + + downexPractice = downexPractice.filter( + (trial) => !excludedDownexPracticeTypes.includes(trial.trialType), + ); + + const allowedIds = ['math-instructions1-heavy', 'math-intro1-heavy']; + + downexInstructions.forEach((trial) => { + if (allowedIds.includes(trial.itemId)) { + timeline.push({ ...fixationOnly, stimulus: '' }); + timeline.push(afcStimulusTemplate(trialConfig, trial)); + } + }); + + downexPractice.forEach((trial) => { + timeline.push({ ...fixationOnly, stimulus: '' }); + timeline.push(stimulusBlock(trial)); + }); + + timeline.push(practiceTransition()); - const numOfBlocks = allBlocks.length; - const trialProportionsPerBlock = [2, 2, 3, 3]; // divide by these numbers to get trials per block - for (let i = heavyInstructions ? 0 : 1; i < numOfBlocks; i++) { - // skip first block if not heavyInstructions + const numOfTrials = Math.floor(downexBlock.length / 2); + taskStore.transact('totalTestTrials', (oldVal: number) => (oldVal += numOfTrials)); + for (let j = 0; j < numOfTrials; j++) { + timeline.push({ ...setupDownex, stimulus: '' }); // select only from the current block + timeline.push(stimulusBlock()); + } + } + + const numOfBlocks = olderKidBlocks.length; + const trialProportionsPerBlock = [2, 3, 3]; // divide by these numbers to get trials per block + for (let i = 0; i < numOfBlocks; i++) { // push in block-specific instructions - let usedIDs: string[] = []; - const blockInstructions = instructions.filter((trial) => { - const trialBlock = trial.block_index === 3 ? 0 : Number(trial.block_index) + 1; + const blockInstructions = olderKidInstructions.filter((trial) => { let allowedIDs: string[]; // CAT only uses particular instructions from corpus switch (i) { case 0: - allowedIDs = ['math-instructions1-heavy', 'math-intro1-heavy']; - break; - case 1: allowedIDs = heavyInstructions ? ['math-intro2'] : ['math-instructions1', 'math-intro1']; break; - case 2: + case 1: allowedIDs = ['math-intro2']; break; - case 3: + case 2: allowedIDs = ['math-intro2', 'number-line-instruct1']; break; default: allowedIDs = []; } - const include = trialBlock === i && allowedIDs.includes(trial.itemId) && !usedIDs.includes(trial.itemId); + const include = allowedIDs.includes(trial.itemId) && !usedIds.includes(trial.itemId); if (include) { - usedIDs.push(trial.itemId); + usedIds.push(trial.itemId); } return include; @@ -301,10 +313,10 @@ export default function buildMathTimeline(config: Record, mediaAsse }); // push in block-specific practice trials - const blockPractice = practice.filter((trial) => { - const trialBlock = trial.block_index === 3 ? 0 : Number(trial.block_index) + 1; - return trialBlock === i; + const blockPractice = olderKidPractice.filter((trial) => { + return i === Number(trial.block_index); }); + blockPractice.forEach((trial) => { timeline.push({ ...fixationOnly, stimulus: '' }); timeline.push(stimulusBlock(trial)); @@ -315,7 +327,7 @@ export default function buildMathTimeline(config: Record, mediaAsse }); // final slider block - if (i === 3) { + if (i === 2) { timeline.push(repeatSliderPracticeBlock()); } @@ -323,20 +335,19 @@ export default function buildMathTimeline(config: Record, mediaAsse timeline.push(practiceTransition()); // push in random items at start of first block (after practice trials) - if (i === 1) { - fullCorpus.start.forEach((trial) => timeline.push(stimulusBlock(trial))); + if (i === 0) { + allCorpusParts.start.forEach((trial) => timeline.push(stimulusBlock(trial))); } - const numOfTrials = Math.floor(allBlocks[i].length / trialProportionsPerBlock[i]); + const numOfTrials = Math.floor(olderKidBlocks[i].length / trialProportionsPerBlock[i]); taskStore.transact('totalTestTrials', (oldVal: number) => (oldVal += numOfTrials)); for (let j = 0; j < numOfTrials; j++) { timeline.push({ ...setupStimulusFromBlock(i), stimulus: '' }); // select only from the current block timeline.push(stimulusBlock()); } - fullCorpus.unnormed.forEach((trial) => { - const trialBlock = trial.block_index === 3 ? 0 : trial.block_index + 1; - if (trialBlock === i) { + allCorpusParts.unnormed.forEach((trial) => { + if (i === Number(trial.block_index)) { timeline.push({ ...fixationOnly, stimulus: '' }); timeline.push(stimulusBlock(trial)); } @@ -349,7 +360,7 @@ export default function buildMathTimeline(config: Record, mediaAsse corpus.forEach((trial) => (trial.difficulty = NaN)); const newCorpora = { - practice: taskStore().corpora.practice, + downex: taskStore().corpora.downex, stimulus: corpus, }; taskStore('corpora', newCorpora); diff --git a/task-launcher/src/tasks/mental-rotation/timeline.ts b/task-launcher/src/tasks/mental-rotation/timeline.ts index aac52efe..bc7d7d2a 100644 --- a/task-launcher/src/tasks/mental-rotation/timeline.ts +++ b/task-launcher/src/tasks/mental-rotation/timeline.ts @@ -159,7 +159,7 @@ export default function buildMentalRotationTimeline(config: Record, ); taskStore('corpora', { - practice: taskStore().corpora.practice, + downex: taskStore().corpora.downex, stimulus: corpus, }); diff --git a/task-launcher/src/tasks/same-different-selection/catTimeline.ts b/task-launcher/src/tasks/same-different-selection/catTimeline.ts index 03ed6c99..09b905f6 100644 --- a/task-launcher/src/tasks/same-different-selection/catTimeline.ts +++ b/task-launcher/src/tasks/same-different-selection/catTimeline.ts @@ -20,7 +20,6 @@ import { taskFinished, } from '../shared/trials'; import { setTrialBlock } from './helpers/setTrialBlock'; -import { dataQualityScreen } from '../shared/trials/dataQuality'; import { initializeCat, jsPsych } from '../taskSetup'; import { legacyStimulus } from './trials/legacyStimulus'; @@ -29,12 +28,13 @@ export default function buildSameDifferentTimelineCat(config: Record, initializeCat(); - if (heavy) { - timeline.push(dataQualityScreen); - } timeline.push(taskFinished()); timeline.push(exitFullscreen); return { jsPsych, timeline }; diff --git a/task-launcher/src/tasks/shared/helpers/prepareCat.ts b/task-launcher/src/tasks/shared/helpers/prepareCat.ts index d2b01be2..48f19f17 100644 --- a/task-launcher/src/tasks/shared/helpers/prepareCat.ts +++ b/task-launcher/src/tasks/shared/helpers/prepareCat.ts @@ -4,42 +4,51 @@ import { cat } from '../../taskSetup'; import { jsPsych } from '../../taskSetup'; // separates trials from corpus into blocks depending on for heavy/light instructions and CAT -export function prepareCorpus(corpus: StimulusType[], randomStartBlock = true, fillInSdsDifficulty: boolean = false) { +export function prepareCorpus( + corpus: StimulusType[], + randomStartBlock = true, + downexCorpus?: StimulusType[], + fillInSdsDifficulty: boolean = false +) { const excludedTrialTypes = '3D'; // limit random starting items so that their difficulty is less than 0 const maxTrialDifficulty = 0; const cat: boolean = taskStore().runCat; let corpora; - const instructionPracticeTrials: StimulusType[] = corpus.filter( - (trial) => trial.trialType === 'instructions' || trial.assessmentStage === 'practice_response', - ); + let heavyInstructionPracticeTrials: StimulusType[] = []; + let downexTestTrials: StimulusType[] = []; - const heavyInstructionPracticeTrials: StimulusType[] = instructionPracticeTrials.filter( - (trial) => Number(trial.difficulty) < 0, - ); + if (downexCorpus) { + heavyInstructionPracticeTrials = downexCorpus.filter( + (trial) => trial.trialType === 'instructions' || trial.assessmentStage === 'practice_response', + ); + downexTestTrials = downexCorpus.filter((trial) => !heavyInstructionPracticeTrials.includes(trial)); + } - const lightInstructionPracticeTrials: StimulusType[] = instructionPracticeTrials.filter( - (trial) => Number(trial.difficulty) > 0 || trial.difficulty == null || isNaN(Number(trial.difficulty)), + const lightInstructionPracticeTrials: StimulusType[] = corpus.filter( + (trial) => trial.trialType === 'instructions' || trial.assessmentStage === 'practice_response', ); + const testTrials: StimulusType[] = corpus.filter((trial) => !lightInstructionPracticeTrials.includes(trial)); const corpusParts = { ipHeavy: heavyInstructionPracticeTrials, ipLight: lightInstructionPracticeTrials, - test: corpus.filter((trial) => !instructionPracticeTrials.includes(trial)), + test: testTrials, + downexTest: downexTestTrials, }; - // something same 1 trials inherit difficulty from the corresponding something same 2 trial - if (fillInSdsDifficulty) { - corpusParts.test.forEach((trial, index) => { - if (trial.trialType === 'something-same-1') { - const nextTrial = corpusParts.test[index + 1]; - if (nextTrial.trialType === 'something-same-2') { - trial.difficulty = nextTrial.difficulty; + // something same 1 trials inherit difficulty from the corresponding something same 2 trial + if (fillInSdsDifficulty) { + corpusParts.test.forEach((trial, index) => { + if (trial.trialType === 'something-same-1') { + const nextTrial = corpusParts.test[index + 1]; + if (nextTrial.trialType === 'something-same-2') { + trial.difficulty = nextTrial.difficulty; + } } - } - }); - } + }); + } // separate out normed/unnormed trials const unnormedTrials: StimulusType[] = corpusParts.test.filter( @@ -61,25 +70,35 @@ export function prepareCorpus(corpus: StimulusType[], randomStartBlock = true, f randomStartBlock ? normedTrials.filter((trial) => !startItems.includes(trial)) : normedTrials; + const downexUnnormedTrials: StimulusType[] = downexTestTrials.filter( + (trial) => trial.difficulty == null || isNaN(Number(trial.difficulty)), + ); + + const downexCatCorpus: StimulusType[] = downexTestTrials.filter( + trial => !downexUnnormedTrials.includes(trial), + ); + corpora = { - ipHeavy: corpusParts.ipHeavy, // heavy instruction/practice trials - ipLight: corpusParts.ipLight, // light instruction/practice + ipHeavy: corpusParts.ipHeavy, // downex instruction/practice trials + ipLight: corpusParts.ipLight, // older kid instruction/practice start: startItems, // 5 random items to be used in starting block (all under a certain max difficulty) unnormed: unnormedTrials, // all items without IRT parameters + downexUnnormed: downexUnnormedTrials, cat: catCorpus, // all normed items for CAT + downexCat: downexCatCorpus, }; if (cat) { // if cat is running, put only normed trials into taskStore const newCorpora = { - practice: taskStore().corpora.practice, + downex: downexTestTrials, stimulus: catCorpus, }; taskStore('corpora', newCorpora); } else { // if cat is not running, put entire test portion of corpus into taskStore but leave out instruction/practice const newCorpora = { - practice: taskStore().corpora.practice, + practice: downexTestTrials, stimulus: corpusParts.test, }; taskStore('corpora', newCorpora); diff --git a/task-launcher/src/tasks/shared/trials/afcStimulus.ts b/task-launcher/src/tasks/shared/trials/afcStimulus.ts index 90d927ac..5d114dca 100644 --- a/task-launcher/src/tasks/shared/trials/afcStimulus.ts +++ b/task-launcher/src/tasks/shared/trials/afcStimulus.ts @@ -243,7 +243,7 @@ function doOnLoad(layoutConfigMap: Record, trial?: Sti // flag correct answers with alt text for math if running a Cypress test if (window.Cypress && !isInstructionTrial) { - const choices: NodeListOf = document.querySelectorAll('.secondary'); + const choices: NodeListOf = document.querySelectorAll('.secondary, .image-medium, .primary'); choices[itemLayoutConfig.response.targetIndex].setAttribute('aria-label', 'correct'); } }