diff --git a/functions/index.js b/functions/index.js index 233a361..73c4732 100644 --- a/functions/index.js +++ b/functions/index.js @@ -297,6 +297,95 @@ exports.createProgress = functions.https.onCall((data, context) => { }); }); +/** + * Creates new progress document using the incorrect answers from another progess document. + * @param {string} data The progress ID of the existing progress document to use. + * @return {string} The ID of the created progress document. +*/ +exports.createProgressWithIncorrect = functions.https.onCall((data, context) => { + const uid = LOCAL_TESTING ? "M3JPrFRH6Fdo8XMUbF0l2zVZUCH3" : context.auth.uid; + + if (context.app == undefined && !LOCAL_TESTING) { + throw new functions.https.HttpsError( + "failed-precondition", + "The function must be called from an App Check verified app."); + } + + return db.runTransaction(async (transaction) => { + if (typeof data !== "string") { + throw new functions.https.HttpsError("invalid-argument", "Progress ID must be a string"); + } + + const oldProgressDocId = db.collection("progress").doc(data); + return transaction.get(oldProgressDocId) + .then(async (doc) => { + if (!doc.exists) throw new functions.https.HttpsError("invalid-argument", "Progress record doesn't exist"); + if (doc.data().uid !== uid) throw new functions.https.HttpsError("permission-denied", "Can't use other users' progress records"); + if (doc.data().incorrect.length < 1) throw new functions.https.HttpsError("failed-precondition", "Progress record must have at least one incorrect answer"); + + let progressData = doc.data(); + let dataToSet = { + correct: [], + incorrect: [], + questions: shuffleArray([... new Set(progressData.incorrect)]), + duration: null, + progress: 0, + start_time: Date.now(), + set_title: progressData.set_title, + uid: progressData.uid, + switch_language: progressData.switch_language, + mode: progressData.mode, + current_correct: [], + typo: false, + setIds: progressData.setIds, + }; + if (progressData.mode === "lives") { + dataToSet.lives = progressData.start_lives; + dataToSet.start_lives = progressData.start_lives; + } + + const newProgressDocId = db.collection("progress").doc(); + let batches = [db.batch()]; + let promises = []; + + dataToSet.questions.map(async (vocabId, index) => { + if (index % 248 === 0) { + batches.push(db.batch()); + } + let currentBatchIndex = batches.length - 1; + promises.push(transaction.get(oldProgressDocId.collection("terms").doc(vocabId)) + .then((termDoc) => { + return batches[currentBatchIndex].set( + newProgressDocId.collection("terms").doc(vocabId), + termDoc.data() + ); + })); + promises.push(transaction.get(oldProgressDocId.collection("definitions").doc(vocabId)) + .then((termDoc) => { + return batches[currentBatchIndex].set( + newProgressDocId.collection("definitions").doc(vocabId), + termDoc.data() + ); + })); + }); + + batches[batches.length - 1].set( + newProgressDocId, + dataToSet + ); + + await Promise.all(promises); + + await Promise.all(batches.map((batch) => batch.commit())); + + return newProgressDocId.id; + }) + .catch((error) => { + throw new functions.https.HttpsError("unknown", "Can't create new progress record from existing one"); + }); + }); +}); + /** * Processes a response to a question in a vocab set. * @param {string} progressId The ID of the progress file to retrieve the prompt from. diff --git a/src/Progress.js b/src/Progress.js index cbfda9f..0821038 100644 --- a/src/Progress.js +++ b/src/Progress.js @@ -3,18 +3,60 @@ import { withRouter } from "react-router-dom"; import { HomeRounded as HomeRoundedIcon, ArrowForwardRounded as ArrowForwardRoundedIcon, SettingsRounded as SettingsRoundedIcon, CloseRounded as CloseRoundedIcon, PeopleRounded as PeopleRoundedIcon, QuestionAnswerRounded as QuestionAnswerRoundedIcon } from "@material-ui/icons"; import NavBar from "./NavBar"; import Button from "./Button"; -import LinkButton from "./LinkButton"; import Error404 from "./Error404"; import SettingsContent from "./SettingsContent"; import Footer from "./Footer"; import LineChart from './LineChart'; import ProgressStats from './ProgressStats'; +import ConfirmationDialog from "./ConfirmationDialog"; import "./css/PopUp.css"; import "./css/Progress.css"; import "./css/Chart.css"; export default withRouter(class Progress extends React.Component { + changeableStateItems = { + loading: false, + canProceed: true, + canStartTest: true, + showTestRestart: false, + showIncorrectTestStart: false, + progressInaccessible: false, + correct: 0, + incorrect: 0, + totalQuestions: 0, + progress: 0, + setTitle: "", + switchLanguage: false, + answerInput: "", + currentPrompt: "", + currentSound: false, + currentSetOwner: "", + nextPrompt: "", + nextSound: false, + nextSetOwner: "", + currentAnswerStatus: null, + currentCorrect: [], + moreAnswers: true, + duration: 0, + incorrectAnswers: {}, + showSettings: false, + soundInput: this.props.sound, + themeInput: this.props.theme, + coloredEdgesInput: this.props.coloredEdges, + setIds: [], + attemptNumber: 1, + attemptHistory: {}, + questions: [], + originalTotalQuestions: 1, + lives: 1, + startLives: null, + setComplete: false, + averagePercentage: null, + pageLoaded: false, + startTime: null, + } + constructor(props) { super(props); this.state = { @@ -22,9 +64,9 @@ export default withRouter(class Progress extends React.Component { db: props.db, functions: { processAnswer: props.functions.httpsCallable("processAnswer"), + createProgress: props.functions.httpsCallable("createProgress"), + createProgressWithIncorrect: props.functions.httpsCallable("createProgressWithIncorrect"), }, - loading: false, - canProceed: true, navbarItems: [ { type: "button", @@ -39,40 +81,7 @@ export default withRouter(class Progress extends React.Component { hideTextMobile: true, } ], - progressInaccessible: false, - correct: 0, - incorrect: 0, - totalQuestions: 0, - progress: 0, - setTitle: "", - switchLanguage: false, - answerInput: "", - currentPrompt: "", - currentSound: false, - currentSetOwner: "", - nextPrompt: "", - nextSound: false, - nextSetOwner: "", - currentAnswerStatus: null, - currentCorrect: [], - moreAnswers: true, - duration: 0, - incorrectAnswers: {}, - showSettings: false, - soundInput: this.props.sound, - themeInput: this.props.theme, - coloredEdgesInput: this.props.coloredEdges, - setIds: [], - attemptNumber: 1, - attemptHistory: {}, - questions: [], - originalTotalQuestions: 1, - lives: 1, - startLives: null, - setComplete: false, - averagePercentage: null, - pageLoaded: false, - startTime: null, + ...this.changeableStateItems, }; let isMounted = true; @@ -87,6 +96,10 @@ export default withRouter(class Progress extends React.Component { } async componentDidMount() { + this.unlisten = this.props.history.listen((location, action) => { + if (location.pathname.startsWith("/progress/")) this.setState(this.changeableStateItems, () => this.componentDidMount()); + }); + const progressId = this.props.match.params.progressId; const progressRef = this.state.db.collection("progress").doc(progressId); @@ -129,94 +142,95 @@ export default withRouter(class Progress extends React.Component { ]; }); - if (!newState.progressInaccessible && !setDone) { - let nextPromptRef; - if (!newState.switchLanguage) { - nextPromptRef = progressRef - .collection("terms") - .doc(newState.questions[newState.progress]); - } else { - nextPromptRef = progressRef - .collection("definitions") - .doc(newState.questions[newState.progress]); - } - - await nextPromptRef.get().then((doc) => { - newState.currentPrompt = doc.data().item; - newState.currentSound = doc.data().sound === true; - }).catch((error) => { - newState.progressInaccessible = true; - console.log(`Progress data inaccessible: ${error}`); - }); - } else if (setDone) { - newState.moreAnswers = false; - newState.currentAnswerStatus = true; - newState.duration = duration; - - let promises = []; - - promises.push(this.state.db.collection("progress") - .where("uid", "==", this.state.user.uid) - .where("setIds", "==", newState.setIds) - .orderBy("start_time") - .get() - .then((querySnapshot) => { - newState.attemptNumber = querySnapshot.docs.map((doc) => doc.id).indexOf(this.props.match.params.progressId) + 1; - if (querySnapshot.docs.length > 1) - newState.attemptHistory = querySnapshot.docs.filter((doc) => doc.data().duration !== null) - .map((doc) => { - if (doc.id === this.props.match.params.progressId) newState.startTime = doc.data().start_time; - return { - x: new Date(doc.data().start_time), - y: (doc.data().correct.length / doc.data().questions.length * 100), - } - }); - })); - - promises.push(this.state.db.collection("completed_progress") - .doc(setIds.sort().join("__")) - .get() - .then((completedProgressDoc) => { - newState.averagePercentage = (completedProgressDoc.data().total_percentage / completedProgressDoc.data().attempts).toFixed(2); + if (!newState.progressInaccessible) { + if (!setDone) { + let nextPromptRef; + if (!newState.switchLanguage) { + nextPromptRef = progressRef + .collection("terms") + .doc(newState.questions[newState.progress]); + } else { + nextPromptRef = progressRef + .collection("definitions") + .doc(newState.questions[newState.progress]); + } + + await nextPromptRef.get().then((doc) => { + newState.currentPrompt = doc.data().item; + newState.currentSound = doc.data().sound === true; }).catch((error) => { - console.log(`Couldn't get average percentage: ${error}`); - newState.averagePercentage = null; - })); + newState.progressInaccessible = true; + console.log(`Progress data inaccessible: ${error}`); + }); + } else { + newState.moreAnswers = false; + newState.currentAnswerStatus = true; + newState.duration = duration; - if (incorrectAnswers.length > 0) { - newState.incorrectAnswers = {}; - - promises.push(Promise.all(incorrectAnswers.map((vocabId) => { - if (newState.incorrectAnswers[vocabId]) { - return newState.incorrectAnswers[vocabId].count++; - } else { - newState.incorrectAnswers[vocabId] = { - count: 1, - }; + let promises = []; + promises.push(this.state.db.collection("progress") + .where("uid", "==", this.state.user.uid) + .where("setIds", "==", newState.setIds) + .orderBy("start_time") + .get() + .then((querySnapshot) => { + newState.attemptNumber = querySnapshot.docs.map((doc) => doc.id).indexOf(this.props.match.params.progressId) + 1; + if (querySnapshot.docs.length > 1) + newState.attemptHistory = querySnapshot.docs.filter((doc) => doc.data().duration !== null) + .map((doc) => { + if (doc.id === this.props.match.params.progressId) newState.startTime = doc.data().start_time; + return { + x: new Date(doc.data().start_time), + y: (doc.data().correct.length / doc.data().questions.length * 100), + } + }); + })); - return Promise.all([ - progressRef.collection("terms") - .doc(vocabId) - .get().then((termDoc) => { - newState.switchLanguage ? newState.incorrectAnswers[vocabId].answer = termDoc.data().item.split("/") : newState.incorrectAnswers[vocabId].prompt = termDoc.data().item; - }), - progressRef.collection("definitions") - .doc(vocabId) - .get().then((definitionDoc) => { - newState.switchLanguage ? newState.incorrectAnswers[vocabId].prompt = definitionDoc.data().item : newState.incorrectAnswers[vocabId].answer = definitionDoc.data().item.split("/"); - }) - ]); - } - })).catch((error) => { - console.log(`Couldn't retrieve incorrect answers: ${error}`); - })); + promises.push(this.state.db.collection("completed_progress") + .doc(setIds.sort().join("__")) + .get() + .then((completedProgressDoc) => { + newState.averagePercentage = (completedProgressDoc.data().total_percentage / completedProgressDoc.data().attempts).toFixed(2); + }).catch((error) => { + console.log(`Couldn't get average percentage: ${error}`); + newState.averagePercentage = null; + })); + + if (incorrectAnswers.length > 0) { + newState.incorrectAnswers = {}; + + promises.push(Promise.all(incorrectAnswers.map((vocabId) => { + if (newState.incorrectAnswers[vocabId]) { + return newState.incorrectAnswers[vocabId].count++; + } else { + newState.incorrectAnswers[vocabId] = { + count: 1, + }; + + return Promise.all([ + progressRef.collection("terms") + .doc(vocabId) + .get().then((termDoc) => { + newState.switchLanguage ? newState.incorrectAnswers[vocabId].answer = termDoc.data().item.split("/") : newState.incorrectAnswers[vocabId].prompt = termDoc.data().item; + }), + progressRef.collection("definitions") + .doc(vocabId) + .get().then((definitionDoc) => { + newState.switchLanguage ? newState.incorrectAnswers[vocabId].prompt = definitionDoc.data().item : newState.incorrectAnswers[vocabId].answer = definitionDoc.data().item.split("/"); + }) + ]); + } + })).catch((error) => { + console.log(`Couldn't retrieve incorrect answers: ${error}`); + })); + } + + await Promise.all(promises); } - - await Promise.all(promises); } this.setState(newState, () => { - if (!setDone) this.answerInput.focus(); + if (!newState.progressInaccessible && !setDone) this.answerInput.focus(); }); this.props.page.load(); @@ -230,6 +244,7 @@ export default withRouter(class Progress extends React.Component { componentWillUnmount() { this.isMounted = false; this.props.page.unload(); + this.unlisten(); } showSettings = () => { @@ -277,7 +292,7 @@ export default withRouter(class Progress extends React.Component { } } - showNextItem = () => { + proceed = () => { if (this.state.canProceed) { if (this.state.currentAnswerStatus === null) { this.processAnswer(); @@ -342,6 +357,7 @@ export default withRouter(class Progress extends React.Component { loading: false, canProceed: true, typo: false, + canStartTest: true, }; if (data.correct) { @@ -519,11 +535,98 @@ export default withRouter(class Progress extends React.Component { return `${hours}:${minutes}:${seconds}`; } + startLoading = () => { + this.setState({ + canStartTest: false, + loading: true, + }); + } + + stopLoading = () => { + this.setState({ + canStartTest: true, + loading: false, + }); + } + + recreateSameTest = () => { + if (!this.state.loading) { + this.state.functions.createProgress({ + sets: this.state.setIds, + switch_language: this.state.switchLanguage, + mode: this.state.mode, + limit: this.state.mode === "questions" ? this.state.progress - this.state.incorrect + : this.state.mode === "lives" ? this.state.lives + : 1, + }).then((result) => { + const progressId = result.data; + this.stopLoading(); + this.props.history.push("/progress/" + progressId); + + this.props.logEvent("restart_test", { + progress_id: progressId, + }); + }).catch((error) => { + console.log(`Couldn't start test: ${error}`); + this.stopLoading(); + }); + + this.startLoading(); + } + } + + createTestWithIncorrect = () => { + if (!this.state.loading) { + this.state.functions.createProgressWithIncorrect(this.props.match.params.progressId).then((result) => { + const progressId = result.data; + this.stopLoading(); + this.props.history.push("/progress/" + progressId); + + this.props.logEvent("start_test_with_incorrect", { + progress_id: progressId, + }); + }).catch((error) => { + console.log(`Couldn't create test with incorrect answers: ${error}`); + this.stopLoading(); + }); + + this.startLoading(); + } + } + + showTestRestart = () => { + if (this.state.canStartTest) { + this.setState({ + showTestRestart: true, + }); + } + } + + hideTestRestart = () => { + this.setState({ + showTestRestart: false, + }); + } + + showIncorrectTestStart = () => { + if (this.state.canStartTest) { + this.setState({ + showIncorrectTestStart: true, + }); + } + } + + hideIncorrectTestStart = () => { + this.setState({ + showIncorrectTestStart: false, + }); + } + render() { return (
{ - this.state.pageLoaded && + this.props.page.loaded && (this.state.progressInaccessible ? @@ -537,7 +640,7 @@ export default withRouter(class Progress extends React.Component {

{this.state.currentPrompt}

e.preventDefault()} > - + }
+ +
+ + { + this.state.incorrect > 0 && + + } +
+ { this.state.incorrectAnswers && Object.keys(this.state.incorrectAnswers).length > 0 && <> @@ -663,15 +783,6 @@ export default withRouter(class Progress extends React.Component { } - -
- - Done - -
:
@@ -679,7 +790,7 @@ export default withRouter(class Progress extends React.Component {

{this.state.currentPrompt}

e.preventDefault()} > - + } + { + this.state.showTestRestart && + + } + { + this.state.showIncorrectTestStart && + + } ) }
diff --git a/src/css/Progress.css b/src/css/Progress.css index 2f4440a..0d127bc 100644 --- a/src/css/Progress.css +++ b/src/css/Progress.css @@ -53,22 +53,18 @@ } .progress-end-button-container { - position: fixed; - left: 0; - right: 0; - margin-left: auto; - margin-right: auto; - max-width: 1080px; - bottom: 0; - width: 100%; - height: 0; + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: flex-start; + word-wrap: break-word; + align-items: center; + column-gap: 8px; + row-gap: 8px; } .progress-end-button-container > .button { - margin: 48px; - position: absolute; - right: 0; - bottom: 0; + margin: 0; } .progress-settings-overlay-content { @@ -133,12 +129,6 @@ } -@media screen and (max-width: 720px) { - .progress-end-button-container > .button { - margin: 24px; - } -} - @media screen and (max-width: 660px) { .progress-settings-overlay-content > .settings-themes-container { width: 100%; @@ -158,9 +148,6 @@ } @media screen and (max-height: 600px) { - .progress-end-button-container > .button { - margin: 24px; - } .progress-settings-overlay-content { justify-content: flex-start; } diff --git a/test/functions.test.js b/test/functions.test.js index c589469..a939e06 100644 --- a/test/functions.test.js +++ b/test/functions.test.js @@ -31,6 +31,10 @@ const soundOne = true; const vocabTwo = "vocab_02"; const termTwo = "term_02"; const definitionTwo = "definition_02"; +const vocabThree = "vocab_03"; +const termThree = "term_03"; +const definitionThree = "definition_03"; +const soundThree = true; const soundTwo = true; const groupOne = "group_01"; const groupTwo = "group_02"; @@ -39,11 +43,11 @@ const doubleDefinitionTwo = "definition/02"; const punctuationDefinitionOne = "definition .,()-_'\"01"; const progressVocabOne = userOne + "__" + vocabOne; const progressVocabTwo = userOne + "__" + vocabTwo; -const vocabThree = "vocab_03"; const vocabFour = "vocab_04"; const progressVocabThree = userOne + "__" + vocabThree; const progressVocabFour = userOne + "__" + vocabFour; const incorrectAnswer = "incorrect"; +const progressOne = "progress_01"; async function deleteCollection(db, collectionPath, batchSize) { const collectionRef = db.collection(collectionPath); @@ -2069,6 +2073,8 @@ describe("Parandum Cloud Functions", function () { const groupId = await createGroup(groupOne); const groupDocId = firestore.collection("groups").doc(groupId); + await new Promise(res => setTimeout(res, 1000)); + const snapGroupAfter = await groupDocId.get().then((doc) => doc.data()); const userGroupDocId = firestore.collection("users").doc(userOne).collection("groups").doc(groupId); @@ -2091,6 +2097,421 @@ describe("Parandum Cloud Functions", function () { }); }); + it("createProgressWithIncorrect correctly creates new progress record from progress record with incorrect answers in questions mode", async () => { + const createProgressWithIncorrect = test.wrap(cloudFunctions.createProgressWithIncorrect); + + const vocabIdOne = `${setOne}__${vocabOne}`; + const vocabIdTwo = `${setOne}__${vocabTwo}`; + const vocabIdThree = `${setTwo}__${vocabThree}`; + + const progressData = { + correct: [vocabIdTwo], + incorrect: [vocabIdTwo, vocabIdTwo, vocabIdOne], + questions: [vocabIdOne, vocabIdTwo, vocabIdThree], + duration: null, + progress: 1, + start_time: 1627308670962, + set_title: `${setOne} & ${setTwo}`, + uid: userOne, + switch_language: false, + mode: "questions", + current_correct: [], + typo: false, + setIds: [setOne, setTwo], + }; + const termDataOne = { + term: termOne, + sound: soundOne, + }; + const termDataTwo = { + item: termTwo, + sound: soundTwo, + }; + const termDataThree = { + term: termThree, + sound: soundThree, + }; + const definitionDataOne = { + item: definitionOne, + sound: soundOne, + }; + const definitionDataTwo = { + item: definitionTwo, + sound: soundTwo, + }; + const definitionDataThree = { + item: definitionThree, + sound: soundThree, + }; + + const progressDocId = firestore.collection("progress").doc(progressOne); + + await progressDocId.set(progressData); + await progressDocId + .collection("terms").doc(vocabIdOne).set(termDataOne); + await progressDocId + .collection("terms").doc(vocabIdTwo).set(termDataTwo); + await progressDocId + .collection("terms").doc(vocabIdThree).set(termDataThree); + await progressDocId + .collection("definitions").doc(vocabIdOne).set(definitionDataOne); + await progressDocId + .collection("definitions").doc(vocabIdTwo).set(definitionDataTwo); + await progressDocId + .collection("definitions").doc(vocabIdThree).set(definitionDataThree); + + const returnData = await createProgressWithIncorrect(progressOne); + assert.strictEqual(typeof returnData, "string"); + await progressDocId.get((doc) => { + const data = doc.data(); + assert.deepStrictEqual(data.correct, []); + assert.deepStrictEqual(data.incorrect, []); + assert.deepStrictEqual(data.correct, []); + hamjest.assertThat(data.questions, hamjest.anyOf( + hamjest.is([vocabOne, vocabTwo, vocabThree]), + hamjest.is([vocabOne, vocabThree, vocabTwo]), + hamjest.is([vocabTwo, vocabThree, vocabOne]), + hamjest.is([vocabTwo, vocabOne, vocabThree]), + hamjest.is([vocabOne, vocabTwo, vocabThree]), + hamjest.is([vocabOne, vocabThree, vocabTwo]) + )); + assert.strictEqual(data.duration, null); + assert.strictEqual(data.progress, 0); + assert.strictEqual(data.start_time, 1627308670962); + assert.strictEqual(data.set_title, `${setOne} & ${setTwo}`); + assert.strictEqual(data.uid, userOne); + assert.strictEqual(data.switch_language, false); + assert.strictEqual(data.mode, "questions"); + assert.deepStrictEqual(data.current_correct, []); + assert.strictEqual(data.typo, false); + assert.deepStrictEqual(data.setIds, []); + + }); + await progressDocId + .collection("terms").doc(`${setOne}__${vocabOne}`).get() + .then((doc) => assert.deepStrictEqual(doc.data(), termDataOne)); + await progressDocId + .collection("terms").doc(`${setOne}__${vocabTwo}`).get() + .then((doc) => assert.deepStrictEqual(doc.data(), termDataTwo)); + await progressDocId + .collection("terms").doc(`${setTwo}__${vocabThree}`).get() + .then((doc) => assert.deepStrictEqual(doc.data(), termDataThree)); + await progressDocId + .collection("definitions").doc(`${setOne}__${vocabOne}`).get() + .then((doc) => assert.deepStrictEqual(doc.data(), definitionDataOne)); + await progressDocId + .collection("definitions").doc(`${setOne}__${vocabTwo}`).get() + .then((doc) => assert.deepStrictEqual(doc.data(), definitionDataTwo)); + await progressDocId + .collection("definitions").doc(`${setTwo}__${vocabThree}`).get() + .then((doc) => assert.deepStrictEqual(doc.data(), definitionDataThree)); + }); + + it("createProgressWithIncorrect correctly creates new progress record from progress record with incorrect answers in lives mode", async () => { + const createProgressWithIncorrect = test.wrap(cloudFunctions.createProgressWithIncorrect); + + const vocabIdOne = `${setOne}__${vocabOne}`; + const vocabIdTwo = `${setOne}__${vocabTwo}`; + const vocabIdThree = `${setTwo}__${vocabThree}`; + + const progressData = { + correct: [vocabIdTwo], + incorrect: [vocabIdTwo, vocabIdTwo, vocabIdOne], + questions: [vocabIdOne, vocabIdTwo, vocabIdThree], + duration: null, + progress: 1, + start_time: 1627308670962, + set_title: `${setOne} & ${setTwo}`, + uid: userOne, + switch_language: false, + mode: "lives", + current_correct: [], + typo: false, + setIds: [setOne, setTwo], + lives: 2, + start_lives: 5, + }; + const termDataOne = { + term: termOne, + sound: soundOne, + }; + const termDataTwo = { + item: termTwo, + sound: soundTwo, + }; + const termDataThree = { + term: termThree, + sound: soundThree, + }; + const definitionDataOne = { + item: definitionOne, + sound: soundOne, + }; + const definitionDataTwo = { + item: definitionTwo, + sound: soundTwo, + }; + const definitionDataThree = { + item: definitionThree, + sound: soundThree, + }; + + const progressDocId = firestore.collection("progress").doc(progressOne); + + await progressDocId.set(progressData); + await progressDocId + .collection("terms").doc(vocabIdOne).set(termDataOne); + await progressDocId + .collection("terms").doc(vocabIdTwo).set(termDataTwo); + await progressDocId + .collection("terms").doc(vocabIdThree).set(termDataThree); + await progressDocId + .collection("definitions").doc(vocabIdOne).set(definitionDataOne); + await progressDocId + .collection("definitions").doc(vocabIdTwo).set(definitionDataTwo); + await progressDocId + .collection("definitions").doc(vocabIdThree).set(definitionDataThree); + + const returnData = await createProgressWithIncorrect(progressOne); + assert.strictEqual(typeof returnData, "string"); + await progressDocId.get((doc) => { + const data = doc.data(); + assert.deepStrictEqual(data.correct, []); + assert.deepStrictEqual(data.incorrect, []); + assert.deepStrictEqual(data.correct, []); + hamjest.assertThat(data.questions, hamjest.anyOf( + hamjest.is([vocabOne, vocabTwo, vocabThree]), + hamjest.is([vocabOne, vocabThree, vocabTwo]), + hamjest.is([vocabTwo, vocabThree, vocabOne]), + hamjest.is([vocabTwo, vocabOne, vocabThree]), + hamjest.is([vocabOne, vocabTwo, vocabThree]), + hamjest.is([vocabOne, vocabThree, vocabTwo]) + )); + assert.strictEqual(data.duration, null); + assert.strictEqual(data.progress, 0); + assert.strictEqual(data.start_time, 1627308670962); + assert.strictEqual(data.set_title, `${setOne} & ${setTwo}`); + assert.strictEqual(data.uid, userOne); + assert.strictEqual(data.switch_language, false); + assert.strictEqual(data.mode, "questions"); + assert.deepStrictEqual(data.current_correct, []); + assert.strictEqual(data.typo, false); + assert.deepStrictEqual(data.setIds, []); + assert.strictEqual(data.lives, 5); + assert.strictEqual(data.start_lives, 5); + }); + await progressDocId + .collection("terms").doc(`${setOne}__${vocabOne}`).get() + .then((doc) => assert.deepStrictEqual(doc.data(), termDataOne)); + await progressDocId + .collection("terms").doc(`${setOne}__${vocabTwo}`).get() + .then((doc) => assert.deepStrictEqual(doc.data(), termDataTwo)); + await progressDocId + .collection("terms").doc(`${setTwo}__${vocabThree}`).get() + .then((doc) => assert.deepStrictEqual(doc.data(), termDataThree)); + await progressDocId + .collection("definitions").doc(`${setOne}__${vocabOne}`).get() + .then((doc) => assert.deepStrictEqual(doc.data(), definitionDataOne)); + await progressDocId + .collection("definitions").doc(`${setOne}__${vocabTwo}`).get() + .then((doc) => assert.deepStrictEqual(doc.data(), definitionDataTwo)); + await progressDocId + .collection("definitions").doc(`${setTwo}__${vocabThree}`).get() + .then((doc) => assert.deepStrictEqual(doc.data(), definitionDataThree)); + }); + + it("createProgressWithIncorrect won't create new progress record when old progress record belongs to different user", async () => { + const createProgressWithIncorrect = test.wrap(cloudFunctions.createProgressWithIncorrect); + + const progressData = { + correct: [vocabTwo], + incorrect: [vocabTwo, vocabTwo, vocabOne], + questions: [vocabOne, vocabTwo, vocabThree], + duration: null, + progress: 1, + start_time: 1627308670962, + set_title: `${setOne} & ${setTwo}`, + uid: userTwo, + switch_language: false, + mode: "questions", + current_correct: [], + typo: false, + setIds: [setOne, setTwo], + }; + const termDataOne = { + term: termOne, + sound: soundOne, + }; + const termDataTwo = { + item: termTwo, + sound: soundTwo, + }; + const termDataThree = { + term: termThree, + sound: soundThree, + }; + const definitionDataOne = { + item: definitionOne, + sound: soundOne, + }; + const definitionDataTwo = { + item: definitionTwo, + sound: soundTwo, + }; + const definitionDataThree = { + item: definitionThree, + sound: soundThree, + }; + + const progressDocId = firestore.collection("progress").doc(progressOne); + + await progressDocId.set(progressData); + await progressDocId + .collection("terms").doc(`${setOne}__${vocabOne}`).set(termDataOne); + await progressDocId + .collection("terms").doc(`${setOne}__${vocabTwo}`).set(termDataTwo); + await progressDocId + .collection("terms").doc(`${setTwo}__${vocabThree}`).set(termDataThree); + await progressDocId + .collection("definitions").doc(`${setOne}__${vocabOne}`).set(definitionDataOne); + await progressDocId + .collection("definitions").doc(`${setOne}__${vocabTwo}`).set(definitionDataTwo); + await progressDocId + .collection("definitions").doc(`${setTwo}__${vocabThree}`).set(definitionDataThree); + + firebase.assertFails(createProgressWithIncorrect(progressOne)); + }); + + it("createProgressWithIncorrect won't create new progress record when progress ID argument isn't a string", async () => { + const createProgressWithIncorrect = test.wrap(cloudFunctions.createProgressWithIncorrect); + + const progressData = { + correct: [vocabTwo], + incorrect: [vocabTwo, vocabTwo, vocabOne], + questions: [vocabOne, vocabTwo, vocabThree], + duration: null, + progress: 1, + start_time: 1627308670962, + set_title: `${setOne} & ${setTwo}`, + uid: userTwo, + switch_language: false, + mode: "questions", + current_correct: [], + typo: false, + setIds: [setOne, setTwo], + }; + const termDataOne = { + term: termOne, + sound: soundOne, + }; + const termDataTwo = { + item: termTwo, + sound: soundTwo, + }; + const termDataThree = { + term: termThree, + sound: soundThree, + }; + const definitionDataOne = { + item: definitionOne, + sound: soundOne, + }; + const definitionDataTwo = { + item: definitionTwo, + sound: soundTwo, + }; + const definitionDataThree = { + item: definitionThree, + sound: soundThree, + }; + + const progressDocId = firestore.collection("progress").doc("1"); + + await progressDocId.set(progressData); + await progressDocId + .collection("terms").doc(`${setOne}__${vocabOne}`).set(termDataOne); + await progressDocId + .collection("terms").doc(`${setOne}__${vocabTwo}`).set(termDataTwo); + await progressDocId + .collection("terms").doc(`${setTwo}__${vocabThree}`).set(termDataThree); + await progressDocId + .collection("definitions").doc(`${setOne}__${vocabOne}`).set(definitionDataOne); + await progressDocId + .collection("definitions").doc(`${setOne}__${vocabTwo}`).set(definitionDataTwo); + await progressDocId + .collection("definitions").doc(`${setTwo}__${vocabThree}`).set(definitionDataThree); + + firebase.assertFails(createProgressWithIncorrect(1)); + }); + + it("createProgressWithIncorrect won't create new progress record when old progress record doesn't exist", async () => { + const createProgressWithIncorrect = test.wrap(cloudFunctions.createProgressWithIncorrect); + + firebase.assertFails(createProgressWithIncorrect("invalid")); + }); + + it("createProgressWithIncorrect won't create new progress record when old progress record has no incorrect answers", async () => { + const createProgressWithIncorrect = test.wrap(cloudFunctions.createProgressWithIncorrect); + + const progressData = { + correct: [vocabTwo], + incorrect: [], + questions: [vocabOne, vocabTwo, vocabThree], + duration: null, + progress: 1, + start_time: 1627308670962, + set_title: `${setOne} & ${setTwo}`, + uid: userOne, + switch_language: false, + mode: "questions", + current_correct: [], + typo: false, + setIds: [setOne, setTwo], + }; + const termDataOne = { + term: termOne, + sound: soundOne, + }; + const termDataTwo = { + item: termTwo, + sound: soundTwo, + }; + const termDataThree = { + term: termThree, + sound: soundThree, + }; + const definitionDataOne = { + item: definitionOne, + sound: soundOne, + }; + const definitionDataTwo = { + item: definitionTwo, + sound: soundTwo, + }; + const definitionDataThree = { + item: definitionThree, + sound: soundThree, + }; + + const progressDocId = firestore.collection("progress").doc(progressOne); + + await progressDocId.set(progressData); + await progressDocId + .collection("terms").doc(`${setOne}__${vocabOne}`).set(termDataOne); + await progressDocId + .collection("terms").doc(`${setOne}__${vocabTwo}`).set(termDataTwo); + await progressDocId + .collection("terms").doc(`${setTwo}__${vocabThree}`).set(termDataThree); + await progressDocId + .collection("definitions").doc(`${setOne}__${vocabOne}`).set(definitionDataOne); + await progressDocId + .collection("definitions").doc(`${setOne}__${vocabTwo}`).set(definitionDataTwo); + await progressDocId + .collection("definitions").doc(`${setTwo}__${vocabThree}`).set(definitionDataThree); + + firebase.assertFails(createProgressWithIncorrect(progressOne)); + }); + /*xit("getGroupMembers returns group members correctly", async () => { const getGroupMembers = test.wrap(cloudFunctions.getGroupMembers);