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}
+
+
+
+ {
+ 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}
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);