spanish-quizzer

An app to quiz you on Spanish vocabulary and verb conjugations
git clone https://git.ashermorgan.net/spanish-quizzer/
Log | Files | Refs | README

commit 5b7e779179bb81e6f88fe8289dbd198358cf51a4
parent 0463eb13b405375a5b666fd2dd12ffaf549e069e
Author: AsherMorgan <59518073+AsherMorgan@users.noreply.github.com>
Date:   Wed, 21 Oct 2020 12:22:57 -0700

Group all quizzer settings into one object.

Diffstat:
Mindex.html | 3+--
Mjs/home.js | 32+++++++++++++-------------------
Mjs/quizzer.js | 46++++++++++++++++++++--------------------------
Mjs/settings.js | 81+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Mtests/test.app.js | 28++++++++++------------------
Mtests/test.quizzer.js | 39+++++++++++++++------------------------
Mtests/test.settings.js | 109++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
7 files changed, 170 insertions(+), 168 deletions(-)

diff --git a/index.html b/index.html @@ -51,8 +51,7 @@ </settings> <quizzer id="quizzer" v-show="state === 'quizzer'" :active="state === 'quizzer'" hidden - :starting-index="promptIndex" :starting-prompts="prompts" @new-prompt="updateProgress" - :prompt-type="promptType" :input-type="inputType" :on-missed-prompt="onMissedPrompt" :repeat-prompts="repeatPrompts"> + :starting-index="promptIndex" :starting-prompts="prompts" :settings="settings" @new-prompt="updateProgress"> </quizzer> </main> diff --git a/js/home.js b/js/home.js @@ -13,12 +13,14 @@ function loadVue() { data: { state: "home", // Can be either "home", "settings", or "quizzer" - category: "home", // Can be either "verbs" or "vocab" + category: "verbs", // Can be either "verbs" or "vocab" - promptType: "Text", - inputType: "Text", - onMissedPrompt: "Correct me", - repeatPrompts: "Never", + settings: { + promptType: "Text", + inputType: "Text", + onMissedPrompt: "Correct me", + repeatPrompts: "Never", + }, prompts: [], promptIndex: 0, @@ -59,14 +61,11 @@ function loadVue() { * Perform validation checks and then start the quizzer. * @param {Array} prompts - The list of prompts. * @param {Number} promptIndex - The index of the current prompt. - * @param {String} promptType - The prompt type. - * @param {String} inputType - The input type. - * @param {String} onMissedPrompt - The onMissedPrompt setting value. - * @param {String} repeatPrompts - The repeat prompts setting value. + * @param {Object} settings - The user's settings. */ - StartSession: function(prompts, promptIndex, promptType, inputType, onMissedPrompt, repeatPrompts) { + StartSession: function(prompts, promptIndex, settings) { // Validate browser for voice input - if (this.inputType !== "Text") { + if (this.settings.inputType !== "Text") { if (typeof InstallTrigger !== "undefined") { // Browser is Firefox alert("You must enable speech recognition in about:config."); @@ -79,21 +78,16 @@ function loadVue() { } // Give iOS devices ringer warning for prompt audio - if (this.promptType !== "Text") { + if (this.settings.promptType !== "Text") { if (!!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform)) { alert("Please make sure your ringer is on in order to hear audio prompts."); } } - // Copy over prompts and promptIndex + // Copy over parameters this.prompts = prompts; this.promptIndex = promptIndex; - - // Copy over settings - this.promptType = promptType - this.inputType = inputType - this.onMissedPrompt = onMissedPrompt - this.repeatPrompts = repeatPrompts + this.settings = settings; // Show and hide elements (also enables the quizzer) this.state = "quizzer"; diff --git a/js/quizzer.js b/js/quizzer.js @@ -15,22 +15,16 @@ let quizzer = Vue.component("quizzer", { type: Number, default: 0, }, - - promptType: { - type: String, - default: "Text", - }, - inputType: { - type: String, - default: "Text", - }, - onMissedPrompt: { - type: String, - default: "Correct me", - }, - repeatPrompts: { - type: String, - default: "Never", + settings: { + type: Object, + default: function() { + return { + promptType: "Text", + inputType: "Text", + onMissedPrompt: "Correct me", + repeatPrompts: "Never", + } + }, }, }, @@ -91,12 +85,12 @@ let quizzer = Vue.component("quizzer", { this.responce = ""; // Read prompt - if (this.promptType !== "Text") { + if (this.settings.promptType !== "Text") { Read(this.prompt[1], this.prompt[0]); } // Get voice input - if (this.inputType !== "Text") { + if (this.settings.inputType !== "Text") { // Create recognition object var recognition = new (window.SpeechRecognition || window.webkitSpeechRecognition || window.mozSpeechRecognition || window.msSpeechRecognition)(); @@ -166,7 +160,7 @@ let quizzer = Vue.component("quizzer", { } // Give user feedback - if (!correct && (this.onMissedPrompt === "Correct me" || this.onMissedPrompt === "Tell me")) { + if (!correct && (this.settings.onMissedPrompt === "Correct me" || this.settings.onMissedPrompt === "Tell me")) { // Show and hide elements this.congratsActive = false; this.responceActive = false; @@ -177,7 +171,7 @@ let quizzer = Vue.component("quizzer", { } catch { } } - else if (!correct && this.onMissedPrompt === "Ignore it") { + else if (!correct && this.settings.onMissedPrompt === "Ignore it") { this.Continue(); } else { @@ -196,7 +190,7 @@ let quizzer = Vue.component("quizzer", { } // Repeat prompt - switch (this.repeatPrompts) + switch (this.settings.repeatPrompts) { case "Never": // Don't repeat @@ -264,28 +258,28 @@ let quizzer = Vue.component("quizzer", { <section> <label id="quizzerPromptType" for="quizzerPrompt">{{ prompt[0] }}</label> - <span id="quizzerPrompt" :lang="getLang(prompt[0])" @click="Read(prompt[1], prompt[0]);">{{ promptType === "Audio" ? "Click to hear again" : prompt[1] }}</span> + <span id="quizzerPrompt" :lang="getLang(prompt[0])" @click="Read(prompt[1], prompt[0]);">{{ settings.promptType === "Audio" ? "Click to hear again" : prompt[1] }}</span> </section> <section> <label id="quizzerInputType" for="quizzerInput">{{ prompt[2] }}</label> - <input id="quizzerInput" ref="input" type="text" v-model="responce" :readonly="!responceActive || inputType === 'Voice'" + <input id="quizzerInput" ref="input" type="text" v-model="responce" :readonly="!responceActive || settings.inputType === 'Voice'" @keyup.ctrl.enter.exact="Reset();" @keyup.enter.exact="Enter();" :lang="getLang(prompt[2])" autocomplete="off" spellcheck="false" autocorrect="off" placeholder="Type the answer"> </section> <div id="quizzerButtons"> - <button v-if="responceActive" :disabled="inputType === 'Voice'" @click="Submit();">Submit</button> + <button v-if="responceActive" :disabled="settings.inputType === 'Voice'" @click="Submit();">Submit</button> <button v-else @click="Continue();">Continue</button> <button @click="Reset();">Skip</button> </div> <div id="quizzerFeedback" ref="feedback" v-show="!responceActive" class="bad"> - <span v-if="onMissedPrompt === 'Correct me'"> + <span v-if="settings.onMissedPrompt === 'Correct me'"> The correct answer is <span id="quizzerFeedbackTerm" @click="Read(prompt[3], prompt[2]);">{{ prompt[3].toLowerCase() }}</span>. </span> - <span v-if="onMissedPrompt === 'Tell me'"> + <span v-if="settings.onMissedPrompt === 'Tell me'"> Incorrect. </span> </div> diff --git a/js/settings.js b/js/settings.js @@ -1,7 +1,7 @@ let settings = Vue.component("settings", { props: { category: { - type: Boolean, + type: String, default: "verbs", }, }, @@ -11,10 +11,12 @@ let settings = Vue.component("settings", { darkTheme: document.body.classList.contains("dark"), verbFilters: [], vocabFilters: [], - promptType: localStorage.getItem("promptType") || "Text", - inputType: localStorage.getItem("inputType") || "Text", - onMissedPrompt: localStorage.getItem("onMissedPrompt") || "Correct me", - repeatPrompts: localStorage.getItem("repeatPrompts") || "Never", + settings: { + promptType: "Text", + inputType: "Text", + onMissedPrompt: "Correct me", + repeatPrompts: "Never", + }, }; }, @@ -211,7 +213,7 @@ let settings = Vue.component("settings", { } // Start quizzer - this.$emit("start-session", prompts, promptIndex, this.promptType, this.inputType, this.onMissedPrompt, this.repeatPrompts); + this.$emit("start-session", prompts, promptIndex, this.settings); }, /** @@ -246,7 +248,7 @@ let settings = Vue.component("settings", { } // Start quizzer - this.$emit("start-session", prompts, promptIndex, this.promptType, this.inputType, this.onMissedPrompt, this.repeatPrompts); + this.$emit("start-session", prompts, promptIndex, this.settings); }, /** @@ -281,41 +283,42 @@ let settings = Vue.component("settings", { }, /** - * Update the promptType setting in localStorage. - * @param {String} value - The prompt type. + * Update setting in localStorage. + * @param {String} value - The settings object. */ - promptType: function(value) { - localStorage.setItem("promptType", value); - }, - - /** - * Update the inputType setting in localStorage. - * @param {String} value - The input type. - */ - inputType: function(value) { - localStorage.setItem("inputType", value); - }, - - /** - * Update the onMissedPrompt setting in localStorage. - * @param {String} value - The onMissedPrompt setting value. - */ - onMissedPrompt: function(value) { - localStorage.setItem("onMissedPrompt", value); - }, - - /** - * Update the repeatPrompts setting in localStorage. - * @param {String} value - The repeat prompts setting value. - */ - repeatPrompts: function(value) { - localStorage.setItem("repeatPrompts", value); + settings: { + handler: function(value) { + localStorage.setItem("settings", JSON.stringify(value)); + }, + deep: true, }, }, created: function() { // Add keyup handler window.addEventListener("keyup", this.keyup); + + // Parse settings + let parsedSettings + try { + parsedSettings = JSON.parse(localStorage.getItem("settings")); + } + catch { return; } + if (!parsedSettings) { return; } + + // Load settings + if (parsedSettings.promptType && ["Text", "Audio", "Both"].includes(parsedSettings.promptType)) { + this.settings.promptType = parsedSettings.promptType; + } + if (parsedSettings.inputType && ["Text", "Voice", "Either"].includes(parsedSettings.inputType)) { + this.settings.inputType = parsedSettings.inputType; + } + if (parsedSettings.onMissedPrompt && ["Correct me", "Tell me", "Ignore it"].includes(parsedSettings.onMissedPrompt)) { + this.settings.onMissedPrompt = parsedSettings.onMissedPrompt; + } + if (parsedSettings.repeatPrompts && ["Never", "Immediately", "5 prompts later", "At the end"].includes(parsedSettings.repeatPrompts)) { + this.settings.repeatPrompts = parsedSettings.repeatPrompts; + } }, destroyed: function() { @@ -415,7 +418,7 @@ let settings = Vue.component("settings", { </div> <div> <label for="settingsPromptType">Prompt type</label> - <select id="settingsPromptType" v-model="promptType"> + <select id="settingsPromptType" v-model="settings.promptType"> <option>Text</option> <option>Audio</option> <option>Both</option> @@ -423,7 +426,7 @@ let settings = Vue.component("settings", { </div> <div> <label for="settingsInputType">Input type</label> - <select id="settingsInputType" v-model="inputType"> + <select id="settingsInputType" v-model="settings.inputType"> <option>Text</option> <option>Voice</option> <option>Either</option> @@ -431,7 +434,7 @@ let settings = Vue.component("settings", { </div> <div> <label for="settingsRepeatPrompts">When I miss a prompt</label> - <select id="settingsRepeatPrompts" v-model="onMissedPrompt"> + <select id="settingsRepeatPrompts" v-model="settings.onMissedPrompt"> <option>Correct me</option> <option>Tell me</option> <option>Ignore it</option> @@ -439,7 +442,7 @@ let settings = Vue.component("settings", { </div> <div> <label for="settingsRepeatPrompts">Repeat missed prompts</label> - <select id="settingsRepeatPrompts" v-model="repeatPrompts"> + <select id="settingsRepeatPrompts" v-model="settings.repeatPrompts"> <option>Never</option> <option>Immediately</option> <option>5 prompts later</option> diff --git a/tests/test.app.js b/tests/test.app.js @@ -9,20 +9,15 @@ describe("App", function() { expect(app.state).to.equal("home"); }); - it("PromptType should be 'Text'", function() { - expect(app.promptType).to.equal("Text"); + it("Category should be 'verbs'", function() { + expect(app.category).to.equal("verbs"); }); - - it("InputType should be 'Text'", function() { - expect(app.inputType).to.equal("Text"); - }); - - it("OnMissedPrompt should be 'Correct me'", function() { - expect(app.onMissedPrompt).to.equal("Correct me"); - }); - - it("RepeatPrompts should be 'Never'", function() { - expect(app.repeatPrompts).to.equal("Never"); + + it("Settings should be correct", function() { + expect(app.settings.promptType).to.equal("Text"); + expect(app.settings.inputType).to.equal("Text"); + expect(app.settings.onMissedPrompt).to.equal("Correct me"); + expect(app.settings.repeatPrompts).to.equal("Never"); }); it("Prompts should be empty", function() { @@ -70,15 +65,12 @@ describe("App", function() { describe("StartSession method", function() { it("Should import parameter values", function() { // Call StartSession - app.StartSession([1, 2, 3], 4, "a", "b", "c", "d"); + app.StartSession([1, 2, 3], 4, "test settings"); // Assert parameters imported expect(app.prompts).to.have.members([1, 2, 3]); expect(app.promptIndex).to.equal(4); - expect(app.promptType).to.equal("a"); - expect(app.inputType).to.equal("b"); - expect(app.onMissedPrompt).to.equal("c"); - expect(app.repeatPrompts).to.equal("d"); + expect(app.settings).to.equal("test settings"); }); it("Should set state to 'quizzer'", function() { diff --git a/tests/test.quizzer.js b/tests/test.quizzer.js @@ -18,20 +18,11 @@ describe("Quizzer", function() { expect(Quizzer.startingIndex).to.equal(0); }); - it("PromptType should be 'Text'", function() { - expect(Quizzer.promptType).to.equal("Text"); - }); - - it("InputType should be 'Text'", function() { - expect(Quizzer.inputType).to.equal("Text"); - }); - - it("OnMissedPrompt should be 'Correct me'", function() { - expect(Quizzer.onMissedPrompt).to.equal("Correct me"); - }); - - it("RepeatPrompts should be 'Never'", function() { - expect(Quizzer.repeatPrompts).to.equal("Never"); + it("Settings should be correct", function() { + expect(Quizzer.settings.promptType).to.equal("Text"); + expect(Quizzer.settings.inputType).to.equal("Text"); + expect(Quizzer.settings.onMissedPrompt).to.equal("Correct me"); + expect(Quizzer.settings.repeatPrompts).to.equal("Never"); }); it("Prompts should be empty", function() { @@ -153,7 +144,7 @@ describe("Quizzer", function() { it("Should call Continue if onMissedPrompt is set to 'Ignore it'", function() { // Initialize variables Quizzer.active = true; - Quizzer.repeatPrompts = "At the end"; + Quizzer.settings.repeatPrompts = "At the end"; Quizzer.onMissedPrompt = "Ignore it"; Quizzer.prompts = [["A1", "A2", "A3", "A4"], ["B1", "B2", "B3", "B4"]] Quizzer.responce = "A5"; @@ -277,7 +268,7 @@ describe("Quizzer", function() { // Initialize variables Quizzer.prompts = [["A1", "A2", "A3", "A4"], ["B1", "B2", "B3", "B4"]]; Quizzer.index = 0; - Quizzer.repeatPrompts = "At the end"; + Quizzer.settings.repeatPrompts = "At the end"; Quizzer.responceActive = false; // Will be changed if Reset is called // Run Continue @@ -295,7 +286,7 @@ describe("Quizzer", function() { Quizzer.active = true; Quizzer.prompts = [["A1", "A2", "A3", "A4"], ["B1", "B2", "B3", "B4"]]; Quizzer.index = 0; - Quizzer.repeatPrompts = "Never"; + Quizzer.settings.repeatPrompts = "Never"; // Run Continue Quizzer.Continue(); @@ -311,7 +302,7 @@ describe("Quizzer", function() { Quizzer.active = true; Quizzer.prompts = [["A1", "A2", "A3", "A4"], ["B1", "B2", "B3", "B4"]]; Quizzer.index = 0; - Quizzer.repeatPrompts = "test"; + Quizzer.settings.repeatPrompts = "test"; // Run Continue Quizzer.Continue(); @@ -327,7 +318,7 @@ describe("Quizzer", function() { Quizzer.active = true; Quizzer.prompts = [["A1", "A2", "A3", "A4"], ["B1", "B2", "B3", "B4"]]; Quizzer.index = 0; - Quizzer.repeatPrompts = "Immediately"; + Quizzer.settings.repeatPrompts = "Immediately"; // Run Continue Quizzer.Continue(); @@ -351,7 +342,7 @@ describe("Quizzer", function() { ["G1", "G2", "G3", "G4"], ]; Quizzer.index = 0; - Quizzer.repeatPrompts = "5 prompts later"; + Quizzer.settings.repeatPrompts = "5 prompts later"; // Run Continue Quizzer.Continue(); @@ -380,7 +371,7 @@ describe("Quizzer", function() { ["G1", "G2", "G3", "G4"], ]; Quizzer.index = 0; - Quizzer.repeatPrompts = "At the end"; + Quizzer.settings.repeatPrompts = "At the end"; // Run Continue Quizzer.Continue(); @@ -402,7 +393,7 @@ describe("Quizzer", function() { // Initialize variables Quizzer.prompts = [["A1", "A2", "A3", "A4"], ["B1", "B2", "B3", "B4"]]; // Will change if Continue is called Quizzer.index = 0; // Will be changed if Reset is called - Quizzer.repeatPrompts = "At the end"; + Quizzer.settings.repeatPrompts = "At the end"; // Run Enter Quizzer.Enter(); @@ -419,7 +410,7 @@ describe("Quizzer", function() { Quizzer.prompts = [["A1", "A2", "A3", "A4"], ["B1", "B2", "B3", "B4"]]; // Will change if Continue is called Quizzer.index = 0; // Will be changed if Reset is called Quizzer.responce = "A4"; - Quizzer.repeatPrompts = "At the end"; + Quizzer.settings.repeatPrompts = "At the end"; Quizzer.responceActive = true; // Run Enter @@ -436,7 +427,7 @@ describe("Quizzer", function() { Quizzer.active = true; Quizzer.prompts = [["A1", "A2", "A3", "A4"], ["B1", "B2", "B3", "B4"]]; // Will change if Continue is called Quizzer.index = 0; // Will be changed if Reset is called - Quizzer.repeatPrompts = "At the end"; + Quizzer.settings.repeatPrompts = "At the end"; Quizzer.responceActive = false; // Run Enter diff --git a/tests/test.settings.js b/tests/test.settings.js @@ -17,6 +17,66 @@ describe("Settings", function() { it("VocabFilters should be empty", function() { expect(Settings.vocabFilters.length).to.equal(0); }); + + it("Settings should be loaded", function() { + // Save original setting from localStorage + let originalValue = localStorage.getItem("settings"); + + // Set localStorage settings + localStorage.setItem("settings", "{\"promptType\":\"Audio\",\"inputType\":\"Voice\",\"onMissedPrompt\":\"Tell me\",\"repeatPrompts\":\"5 prompts later\"}") + + // (re)Create settings component + Settings = new settings(); + + // Assert settings loaded + expect(Settings.settings.promptType).to.equal("Audio"); + expect(Settings.settings.inputType).to.equal("Voice"); + expect(Settings.settings.onMissedPrompt).to.equal("Tell me"); + expect(Settings.settings.repeatPrompts).to.equal("5 prompts later"); + + // Restore original setting to localStorage + localStorage.setItem("settings", originalValue); + }); + + it("Invalid individual settings should not be loaded", function() { + // Save original setting from localStorage + let originalValue = localStorage.getItem("settings"); + + // Set localStorage settings + localStorage.setItem("settings", "{\"promptType\":\"Audio\",\"inputType\":\"test\",\"onMissedPrompt\":null}") + + // (re)Create settings component + Settings = new settings(); + + // Assert default settings loaded + expect(Settings.settings.promptType).to.equal("Audio"); // promptType wasn't invalid, so it should still be loaded + expect(Settings.settings.inputType).to.equal("Text"); + expect(Settings.settings.onMissedPrompt).to.equal("Correct me"); + expect(Settings.settings.repeatPrompts).to.equal("Never"); + + // Restore original setting to localStorage + localStorage.setItem("settings", originalValue); + }); + + it("Invalid JSON settings should not be loaded", function() { + // Save original setting from localStorage + let originalValue = localStorage.getItem("settings"); + + // Set localStorage settings + localStorage.setItem("settings", "asdf") + + // (re)Create settings component + Settings = new settings(); + + // Assert default settings loaded + expect(Settings.settings.promptType).to.equal("Text"); + expect(Settings.settings.inputType).to.equal("Text"); + expect(Settings.settings.onMissedPrompt).to.equal("Correct me"); + expect(Settings.settings.repeatPrompts).to.equal("Never"); + + // Restore original setting to localStorage + localStorage.setItem("settings", originalValue); + }); }); describe("AddFilter method", function() { @@ -338,54 +398,23 @@ describe("Settings", function() { }); }); - describe("PromptType watch", function() { - it("Should update setting in localStorage", async function() { - // Save original setting from localStorage - let originalValue = localStorage.getItem("promptType"); - - // Set promptType - Settings.promptType = "test"; - await Settings.$nextTick(); - - // Assert localStorage setting updated - expect(localStorage.getItem("promptType")).to.equal("test"); - - // Restore original setting to localStorage - localStorage.setItem("promptType", originalValue); - }); - }); - - describe("InputType watch", function() { - it("Should update setting in localStorage", async function() { - // Save original setting from localStorage - let originalValue = localStorage.getItem("inputType"); - - // Set inputType - Settings.inputType = "test"; - await Settings.$nextTick(); - - // Assert localStorage setting updated - expect(localStorage.getItem("inputType")).to.equal("test"); - - // Restore original setting to localStorage - localStorage.setItem("inputType", originalValue); - }); - }); - - describe("RepeatPrompts watch", function() { + describe("Settings watch", function() { it("Should update setting in localStorage", async function() { // Save original setting from localStorage - let originalValue = localStorage.getItem("repeatPrompts"); + let originalValue = localStorage.getItem("settings"); - // Set repeatPrompts - Settings.repeatPrompts = "test"; + // Set settings + Settings.settings.promptType = "A"; + Settings.settings.inputType = "B"; + Settings.settings.onMissedPrompt = "C"; + Settings.settings.repeatPrompts = "D"; await Settings.$nextTick(); // Assert localStorage setting updated - expect(localStorage.getItem("repeatPrompts")).to.equal("test"); + expect(localStorage.getItem("settings")).to.equal("{\"promptType\":\"A\",\"inputType\":\"B\",\"onMissedPrompt\":\"C\",\"repeatPrompts\":\"D\"}"); // Restore original setting to localStorage - localStorage.setItem("repeatPrompts", originalValue); + localStorage.setItem("settings", originalValue); }); });