diff --git a/api/util/StackQuestionLoader.php b/api/util/StackQuestionLoader.php
index ec9c26041a0a88900865300cbf85b25fd6063121..719ab1c729c2514589085422ef0da64a3ef67b4d 100644
--- a/api/util/StackQuestionLoader.php
+++ b/api/util/StackQuestionLoader.php
@@ -151,7 +151,8 @@ class StackQuestionLoader {
         );
         $question->options->set_option(
             'scientificnotation',
-            isset($xmldata->question->scientificnotation) ? (string) $xmldata->question->scientificnotation : get_config('qtype_stack', 'scientificnotation')
+            isset($xmldata->question->scientificnotation) ?
+                                (string) $xmldata->question->scientificnotation : get_config('qtype_stack', 'scientificnotation')
         );
 
         $inputmap = [];
diff --git a/doc/en/Authoring/Answer_Tests/Numerical.md b/doc/en/Authoring/Answer_Tests/Numerical.md
index f0c76179c213140a2660385d6b04d6c55f601d32..09e0f6633948e7bb0ab49432f4ae35c42cf8085e 100644
--- a/doc/en/Authoring/Answer_Tests/Numerical.md
+++ b/doc/en/Authoring/Answer_Tests/Numerical.md
@@ -59,7 +59,9 @@ This is a more liberal test.  Primarily it checks numerical accuracy, but it als
 Tests 
 
 1. whether the student's answer contains `opt` significant figures, and
-2. whether the answer is accurate to `opt` significant figures.   
+2. whether the answer is accurate to `opt` significant figures.
+
+Numerical accuracy here ensures the student's answer is within \(\pm .5\) of the last significant place.  So, for example, by design this test will consider \(9.8\) to be equivalent to \(10\) to 2 significant figures.  The student's answer has been written to 2sf, and the answer is within \(\pm 0.5\) of \(10\).  Indeed, by this test `9.6`, `9.7`, `9.8`, `9.9` and `10` will all pass this test.  But `10.1` etc will fail (3 significant figures), rather than accuracy.  (This may make this test, with default options, less useful but changing the behaviour now would be problematic as well!)
 
 If the option is a list `[n,m]` then we check the answer has been written to `n` significant figures, with an accuracy of up to `m` places.  If the answer is too far out then rounding feedback will not be given.   A common test would be to ask for \([n,n-1]\) to permit the student to enter the last digit incorrectly.
 
diff --git a/doc/en/Authoring/GeoGebra.md b/doc/en/Authoring/GeoGebra.md
index 323300ff0f3aa657d0414161ba51dbc9ca5b2576..acd97be9f86dc4929707d47be6623896167b277a 100644
--- a/doc/en/Authoring/GeoGebra.md
+++ b/doc/en/Authoring/GeoGebra.md
@@ -32,6 +32,25 @@ An example `[[geogebra]]` [question block](Question_blocks/index.md) is shown be
 
 This illustrates how the material_id is used.
 
+## Control the size of the applet
+
+There are two places where the size of the applet can be defined:
+
+Within the block, adding values to the GeoGebra parameters width and height will define the section of the applet that is to be shown.
+
+Within the block's header, adding values to the iframe parameters width and height will enlarge or reduce the size of the applet, or even distort it.
+
+```
+[[geogebra height="100px" width="175px"]]
+params["material_id"]="seehz3km";
+params["height"]=200;
+params["width"]=350;
+[[/geogebra]]
+```
+In the block's head, `width="80%" aspect-ratio="2/3"` could be used instead to define relative sizes and possible distortions if needed.
+
+If no size is defined the default is to have `width="500px" height="400px"` and these are also the dimensions used if values are missing and no aspect-ratio has been defined.
+
 ## Using the sub-tags "set", "watch" and "remember"
 
 The "set", "watch" and "remember" tags to the `[[geogebra]]` question block link Maxima values to GeoGebra objects in various ways.
diff --git a/doc/en/Authoring/Inputs.md b/doc/en/Authoring/Inputs.md
index 67815aa11241444c4328503667b503671aef8494..3e14c19f784f4021a5ad4d27324b69c1525d529d 100644
--- a/doc/en/Authoring/Inputs.md
+++ b/doc/en/Authoring/Inputs.md
@@ -351,6 +351,12 @@ Writing bespoke validators is an advanced feature, but offers two significant be
 2. Potential response tree authoring becomes much easier and more reliable because the validation acts as a "guard clause" only allowing correctly structured information through to the PRT.  This means type-checking need not be done in the PRT before assessment.
 3. The extra option `validator` is designed to allow you to choose extra expressions to be invalid.  The extra option `feedback` will simply print an additional message to students in the validation feedback.
 
+### Extra option: monospace ###
+
+This option is available for algebraic, numerical, units and varmatrix inputs. It controls if the student's answer is displayed using monospace font. `monospace` and `monospace:true` will force the input to use monospace. `monospace:false` will force proportional font.
+
+If `monospace` is not specified, then the CURRENT system default for the given input type will be used when the question is displayed. 
+
 ## Extra options ##
 
 In the future we are likely to add additional functionality via the _extra options_ fields.  This is because the form-based support becomes ever more complex, intimidating and difficult to navigate.
diff --git a/doc/en/Developer/Development_track.md b/doc/en/Developer/Development_track.md
index 9a4b5e265befef6e006729c47901026a01608242..985d8f7773032c0d709181c25810e4c648b1db06 100644
--- a/doc/en/Developer/Development_track.md
+++ b/doc/en/Developer/Development_track.md
@@ -14,14 +14,12 @@ This version will require moodle 4.0+. Moodle 3.x is no longer supported.
 5. Add in the `CT:...` and `RAW:...` options for test case construction to enable tests of invalid input (e.g. missing stars).
 6. STACK now has an [API](../Installation/API.md) to provide STACK questions as a web service.
 7. Improve the display of floats.  Numbers of decimal places are now respected in all parts of expressions, and floats such as `1.7E-9` are displayed at \(1.7 \times 10^{-9}\).   There is a new question option to choose between \(1.7 \times 10^{-9}\) and \(1.7E-9\).
-8. Release first version of the API for longer term support, and better support for ILIAS.
 
 TODO:
 
-1. Major code tidy: Moodle code style now requires (i) short forms of arrays, i.e. `[]` not `array()`, and (ii) commas at the end of all list items.
-2. Fix markdown problems. See issue #420.
-3. Fix [issue #879](https://github.com/maths/moodle-qtype_stack/issues/879)
-
+1. Fix markdown problems. See issue #420.
+2. Fix [issue #879](https://github.com/maths/moodle-qtype_stack/issues/879)
+3. Fix [issue #406](https://github.com/maths/moodle-qtype_stack/issues/406) (possibly for 4.7.0).
 
 ## Version 4.7.0
 
diff --git a/lang/en/qtype_stack.php b/lang/en/qtype_stack.php
index b1985f1f89c832cf2daa96efb5c4f70ece9d69f7..2472e204f78fd3fc9c1ec5613df096f8c5deccbf 100644
--- a/lang/en/qtype_stack.php
+++ b/lang/en/qtype_stack.php
@@ -151,6 +151,8 @@ $string['generalfeedback_help'] = 'General feedback is CASText. General feedback
 $string['generalfeedback_link'] = '%%WWWROOT%%/question/type/stack/doc/doc.php/Authoring/CASText.md#general_feedback';
 $string['showvalidation'] = 'Show the validation';
 $string['showvalidation_help'] = 'Displays any validation feedback from this input, including echoing back their expression in traditional two dimensional notation.   Syntax errors are always reported back.';
+$string['inputmonospace'] = 'Monospace font';
+$string['inputmonospace_help'] = 'Select the types of input to default to monospace font. This affects all questions, not just new ones. These defaults can be overridden for a particular input with extra option settings \'monospace\' and \'monospace:false\'.';
 $string['showvalidation_link'] = '%%WWWROOT%%/question/type/stack/doc/doc.php/Authoring/Inputs.md#Show_validation';
 $string['showvalidationno'] = 'No';
 $string['showvalidationyes'] = 'Yes, with variable list';
diff --git a/questiontestreport.php b/questiontestreport.php
index d0f53c7d2f45ebd79b2d6cd3f9a19674935eaedb..e5ce8c65f09ddc50055603776ee4e012be251d92 100644
--- a/questiontestreport.php
+++ b/questiontestreport.php
@@ -436,7 +436,7 @@ foreach (array_keys($summary) as $variant) {
             foreach ($prtreportinputs[$variant][$prt][$dat] as $inputsummary => $inum) {
                 $sumout .= str_pad($inum, strlen((string) $pad) + 1) . '(' .
                     str_pad(number_format((float) 100 * $inum / $tot, 2, '.', ''), 6, ' ', STR_PAD_LEFT) .
-                    '%); ' . $inputsummary . "\n";
+                    '%); ' . htmlentities($inputsummary, ENT_COMPAT) . "\n";
             }
             $sumout .= "\n";
         }
@@ -466,7 +466,7 @@ foreach (array_keys($summary) as $variant) {
                 foreach ($data as $dat => $num) {
                     $sumouti .= str_pad($num, strlen((string) $pad) + 1) . '(' .
                         str_pad(number_format((float) 100 * $num / $tot, 2, '.', ''), 6, ' ', STR_PAD_LEFT) .
-                        '%); ' . $dat . "\n";
+                        '%); ' . htmlentities($dat, ENT_COMPAT) . "\n";
                 }
                 $sumouti .= "\n";
             }
@@ -498,7 +498,7 @@ foreach ($inputreportsummary as $input => $idata) {
             foreach ($data as $dat => $num) {
                 $sumouti .= str_pad($num, strlen((string) $pad) + 1) . '(' .
                         str_pad(number_format((float) 100 * $num / $tot, 2, '.', ''), 6, ' ', STR_PAD_LEFT) .
-                        '%); ' . $dat . "\n";
+                        '%); ' . htmlentities($dat, ENT_COMPAT) . "\n";
             }
             $sumouti .= "\n";
         }
@@ -526,7 +526,7 @@ foreach ($summary as $variant => $vdata) {
         foreach ($vdata as $dat => $num) {
             $sumout .= str_pad($num, strlen((string) $pad) + 1) . '(' .
                     str_pad(number_format((float) 100 * $num / $tot, 2, '.', ''), 6, ' ', STR_PAD_LEFT) .
-                    '%); ' . $dat . "\n";
+                    '%); ' . htmlentities($dat, ENT_COMPAT) . "\n";
         }
     }
 }
diff --git a/renderer.php b/renderer.php
index 2fbe32c835ffa35dbc6c42c8bae1e8382639f837..83e5a9c9e475a4ce0dbe13e919fecfa067ee4469 100644
--- a/renderer.php
+++ b/renderer.php
@@ -32,7 +32,10 @@ require_once(__DIR__ . '/vle_specific.php');
  * @copyright 2012 The Open University
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
+// @codingStandardsIgnoreStart
+// There's a matching class name in the API.
 class qtype_stack_renderer extends qtype_renderer {
+// @codingStandardsIgnoreEnd
 
     public function formulation_and_controls(question_attempt $qa, question_display_options $options) {
         /* Return type should be @var qtype_stack_question $question. */
diff --git a/settings.php b/settings.php
index 5297f9d930eb5f5564dd90de2e04104610c16d57..e664a09f0fd65c4d83c1bfae659b96954282b8a4 100644
--- a/settings.php
+++ b/settings.php
@@ -208,6 +208,11 @@ $settings->add(new admin_setting_configselect('qtype_stack/inputshowvalidation',
         get_string('showvalidation_help', 'qtype_stack'), '1',
         stack_options::get_showvalidation_options()));
 
+$settings->add(new admin_setting_configmultiselect('qtype_stack/inputmonospace',
+        get_string('inputmonospace', 'qtype_stack'),
+        get_string('inputmonospace_help', 'qtype_stack'), [],
+        stack_options::get_monospace_options()));
+
 // Options for new questions.
 $settings->add(new admin_setting_heading('questionoptionsheading',
         get_string('settingdefaultquestionoptions', 'qtype_stack'),
diff --git a/stack/cas/castext2/blocks/parsons.block.php b/stack/cas/castext2/blocks/parsons.block.php
index 231e7c2224b2eddc0c4fc3d25684180104c6b1af..cdb2c9eda859d8a02b925054ad09ce747e4a51d6 100644
--- a/stack/cas/castext2/blocks/parsons.block.php
+++ b/stack/cas/castext2/blocks/parsons.block.php
@@ -345,6 +345,7 @@ class stack_cas_castext2_parsons extends stack_cas_castext2_block {
 
         // NOTE! List ordered by length. For the trimming logic.
         $validunits = [
+            
             'vmin', 'vmax', 'rem', 'em', 'ex', 'px', 'cm', 'mm',
             'in', 'pt', 'pc', 'ch', 'vh', 'vw', '%',
         ];
diff --git a/stack/input/algebraic/algebraic.class.php b/stack/input/algebraic/algebraic.class.php
index efa7163b18aad82e038b7c59f6eab2cb041bce43..823f4386f38a8e411eb8e3707539ea65e056e50c 100644
--- a/stack/input/algebraic/algebraic.class.php
+++ b/stack/input/algebraic/algebraic.class.php
@@ -33,6 +33,7 @@ class stack_algebraic_input extends stack_input {
         'checkvars' => 0,
         'validator' => false,
         'feedback' => false,
+        'monospace' => false,
     ];
 
     public function render(stack_input_state $state, $fieldname, $readonly, $tavalue) {
@@ -55,6 +56,9 @@ class stack_algebraic_input extends stack_input {
         if ($this->extraoptions['align'] === 'right') {
             $attributes['class'] = 'algebraic-right';
         }
+        if ($this->extraoptions['monospace']) {
+            $attributes['class'] .= ' input-monospace';
+        }
 
         $value = $this->contents_to_maxima($state->contents);
         if ($value == 'EMPTYANSWER') {
diff --git a/stack/input/inputbase.class.php b/stack/input/inputbase.class.php
index 445c805f8eb80b69241b931a27ecad031dc95672..07f945f8d9b7d534161da04eb8690de3ccff7e5d 100644
--- a/stack/input/inputbase.class.php
+++ b/stack/input/inputbase.class.php
@@ -178,6 +178,7 @@ abstract class stack_input {
      */
     protected function internal_construct() {
         $options = $this->get_parameter('options');
+        $setoptions = [];
         if (trim($options ?? '') != '') {
             $options = explode(',', $options);
             foreach ($options as $option) {
@@ -188,17 +189,42 @@ abstract class stack_input {
                     if ($arg === '') {
                         // Extra options with no argument set a Boolean flag.
                         $this->extraoptions[$option] = true;
+                    } else if ($arg === 'false') {
+                        $this->extraoptions[$option] = false;
+                    } else if ($arg === 'true') {
+                        $this->extraoptions[$option] = true;
                     } else {
                         $this->extraoptions[$option] = $arg;
                     }
+                    $setoptions[] = $option;
                 } else {
                     $this->errors[] = stack_string('inputoptionunknown', $option);
                 }
             }
         }
+        $this->set_defaults($setoptions);
         $this->validate_extra_options();
     }
 
+    /**
+     * Set extra options with defaults to the default if they have not been explicitly set.
+     *
+     * @param [] $setoptions - array of options that have been explicity set
+     * @return void
+     */
+    protected function set_defaults($setoptions) {
+        $optionswithdefaults = ['monospace'];
+        foreach ($optionswithdefaults as $currentoption) {
+            if (!array_key_exists($currentoption, $this->extraoptions) || array_search($currentoption, $setoptions) !== false) {
+                // Option not available for this input type or has been explicitly set.
+                continue;
+            }
+
+            $functionname = "is_{$currentoption}";
+            $this->extraoptions[$currentoption] = stack_options::$functionname(get_class($this));
+        }
+    }
+
     /**
      * Validate the individual extra options.
      */
@@ -356,6 +382,12 @@ abstract class stack_input {
                     }
                     break;
 
+                case 'monospace':
+                    if (!(is_bool($arg))) {
+                        $this->errors[] = stack_string('numericalinputoptboolerr', ['opt' => $option, 'val' => $arg]);
+                    }
+                    break;
+
                 case 'nounits':
                     if (!(is_bool($arg))) {
                         $this->errors[] = stack_string('numericalinputoptboolerr', ['opt' => $option, 'val' => $arg]);
@@ -1140,7 +1172,7 @@ abstract class stack_input {
     protected function validation_display($answer, $lvars, $caslines, $additionalvars, $valid, $errors,
                 $castextprocessor, $inertdisplayform, $ilines) {
 
-        $display = stack_maxima_format_casstring(htmlentities($this->contents_to_maxima($this->rawcontents)));
+        $display = stack_maxima_format_casstring(htmlentities($this->contents_to_maxima($this->rawcontents), ENT_COMPAT));
         if ($answer->is_correctly_evaluated()) {
             $display = '\[ ' . $inertdisplayform->get_display() . ' \]';
             if ($this->get_parameter('showValidation', 1) == 3) {
@@ -1599,7 +1631,7 @@ abstract class stack_input {
      * Returns the definition of this input as it should appear in an API response
      * @return array
      */
-    public abstract function render_api_data($tavalue);
+    abstract public function render_api_data($tavalue);
 
     /**
      * Returns the solution in the format used by the api
diff --git a/stack/input/notes/notes.class.php b/stack/input/notes/notes.class.php
index c606d30e150394d41bfa14833ca6831e3666c4df..17d0b0e6962e0e9db0260077eeb9f767d0295031 100644
--- a/stack/input/notes/notes.class.php
+++ b/stack/input/notes/notes.class.php
@@ -202,7 +202,7 @@ class stack_notes_input extends stack_input {
         $contents = $state->contents;
         $render = '';
         if (array_key_exists(0, $contents)) {
-            $render .= html_writer::tag('p', htmlentities($contents[0]));
+            $render .= html_writer::tag('p', htmlentities($contents[0], ENT_COMPAT));
         }
         $render .= html_writer::tag('p', stack_string('studentValidation_notes'), ['class' => 'stackinputnotice']);
         if ($lang !== null && $lang !== '') {
diff --git a/stack/input/numerical/numerical.class.php b/stack/input/numerical/numerical.class.php
index afbd7481a78bb32143149e61632dfa2422b5a471..bb38613a656e46eea52238020ced830b129b292f 100644
--- a/stack/input/numerical/numerical.class.php
+++ b/stack/input/numerical/numerical.class.php
@@ -49,6 +49,7 @@ class stack_numerical_input extends stack_input {
         'maxsf' => false,
         'align' => 'left',
         'validator' => false,
+        'monospace' => false,
     ];
 
     public function render(stack_input_state $state, $fieldname, $readonly, $tavalue) {
@@ -71,6 +72,9 @@ class stack_numerical_input extends stack_input {
         if ($this->extraoptions['align'] === 'right') {
             $attributes['class'] = 'numerical-right';
         }
+        if ($this->extraoptions['monospace']) {
+            $attributes['class'] .= ' input-monospace';
+        }
 
         $value = $this->contents_to_maxima($state->contents);
         if ($this->is_blank_response($state->contents)) {
diff --git a/stack/input/units/units.class.php b/stack/input/units/units.class.php
index 5b70fa222cc7e4532c4794d430d27af352135407..7890c17a356279182868aa80cda70e96ea90c028 100644
--- a/stack/input/units/units.class.php
+++ b/stack/input/units/units.class.php
@@ -42,6 +42,7 @@ class stack_units_input extends stack_input {
         'consolidatesubscripts' => false,
         'validator' => false,
         'feedback' => false,
+        'monospace' => false,
     ];
 
 
@@ -65,6 +66,9 @@ class stack_units_input extends stack_input {
         if ($this->extraoptions['align'] === 'right') {
             $attributes['class'] = 'algebraic-units-right';
         }
+        if ($this->extraoptions['monospace']) {
+            $attributes['class'] .= ' input-monospace';
+        }
 
         if ($state->contents == 'EMPTYANSWER') {
             // Active empty choices don't result in a syntax hint again (with that option set).
@@ -132,7 +136,7 @@ class stack_units_input extends stack_input {
             // The answer is essantially required to be a number and units, other types are rejected.
             'sameType'        => false,
             // Currently this can only be "negpow", or "mul".
-            'options'         => '',
+            'options'            => '',
         ];
     }
 
diff --git a/stack/input/varmatrix/varmatrix.class.php b/stack/input/varmatrix/varmatrix.class.php
index ebe503c3062a805cec065b4ca4e473b1c8aa0230..a3b91fd67bf35c7b31bfb8ef6075c751c8dc2960 100644
--- a/stack/input/varmatrix/varmatrix.class.php
+++ b/stack/input/varmatrix/varmatrix.class.php
@@ -31,6 +31,7 @@ class stack_varmatrix_input extends stack_input {
         'consolidatesubscripts' => false,
         'checkvars' => 0,
         'validator' => false,
+        'monospace' => false,
     ];
 
     protected function is_blank_response($contents) {
@@ -67,6 +68,10 @@ class stack_varmatrix_input extends stack_input {
             'style'          => 'width: '.$size.'em',
         ];
 
+        if ($this->extraoptions['monospace']) {
+            $attributes['class'] .= ' input-monospace';
+        }
+
         if ($this->is_blank_response($state->contents)) {
             $current = $this->maxima_to_raw_input($this->parameters['syntaxHint']);
             if ($this->parameters['syntaxAttribute'] == '1') {
diff --git a/stack/mathsoutput/mathsoutputapi.class.php b/stack/mathsoutput/mathsoutputapi.class.php
index ac919dfb92a3c1d18ca6c180dcb0e09fecd609fb..c78d10c416998d6a13d33b23d69f118617230dcb 100644
--- a/stack/mathsoutput/mathsoutputapi.class.php
+++ b/stack/mathsoutput/mathsoutputapi.class.php
@@ -14,6 +14,7 @@
 // You should have received a copy of the GNU General Public License
 // along with Stack.  If not, see <http://www.gnu.org/licenses/>.
 
+defined('MOODLE_INTERNAL') || die();
 
 require_once(__DIR__ . '/mathsoutputfilterbase.class.php');
 
diff --git a/stack/maxima/contrib/prooflib.mac b/stack/maxima/contrib/prooflib.mac
index a473e7c10924bcb382a6ab080fec0d6b5182f110..cc5fc15264e2c4ad6c1dd0bb746974a0039b6852 100644
--- a/stack/maxima/contrib/prooflib.mac
+++ b/stack/maxima/contrib/prooflib.mac
@@ -64,10 +64,6 @@ proofp(ex) := block(
   return(false)
 );
 
-s_test_case(proofp(proof(1,2,3)), true);
-s_test_case(proofp(proof_iff(1,2)), true);
-s_test_case(proofp(sin(x)), false);
-
 proof_validatep(ex) := block(
   if atom(ex) then return(true),
   if op(ex) = proof_opt then
@@ -84,19 +80,6 @@ proof_validatep(ex) := block(
   return(false)
 );
 
-s_test_case(proof_validatep(proof(1,2,3)), true);
-s_test_case(proof_validatep(proof(1,2,proof(4,5,6))), true);
-s_test_case(proof_validatep(proof(1,2,proof_iff(4,5))), true);
-/* proof_opt must have exactly one sub-proof. */
-s_test_case(proof_validatep(proof(1,2,proof_opt(4,5))), false);
-/* proof_iff must have exactly two sub-proofs. */
-s_test_case(proof_validatep(proof(1,2,proof_iff(4))), false);
-s_test_case(proof_validatep(proof(1,2,proof_iff(4,5,6))), false);
-/* proof_ind must have exactly four sub-proofs. */
-s_test_case(proof_validatep(proof_ind(1,proof(2,3),proof(4,5),6)), true);
-s_test_case(proof_validatep(proof_ind(1,proof(2,3),proof(4,5))), false);
-s_test_case(proof_validatep(proof(1,proof_opt(2),proof_iff(4,5))), true);
-
 /* Is this a type of proof which can reorder its arguments? */
 proof_commutep(ex):=block(
     if atom(ex) then false,
@@ -108,9 +91,6 @@ proof_commutep(ex):=block(
 /* Takes a proof tree and flattens this to a list. */
 proof_flatten(ex) := apply(proof, flatten(ev(ex, map(lambda([ex2], ex2="["), proof_types))));
 
-s_test_case(proof_flatten(proof_iff(proof(A,B),proof(C))), proof(A,B,C));
-s_test_case(proof_flatten(proof_c(proof(A,proof(B,C)),proof(D))), proof(A,B,C,D));
-
 /*
  * Create a normalised proof tree.
  * To establish equivalence of proof trees we compare the normalised form.
@@ -129,13 +109,6 @@ proof_normal(ex) := block(
   return(apply(op(ex), map(proof_normal, args(ex))))
 );
 
-s_test_case(proof_normal(proof_c(B,A,D,C)), proof_c(A,B,C,D));
-s_test_case(proof_normal(proof_iff(B,A)), proof_iff(A,B));
-s_test_case(proof_normal(proof_ind(D,C,B,A)), proof_ind(D,B,C,A));
-s_test_case(proof_normal(proof_cases(D,C,B,A)), proof_cases(D,A,B,C));
-s_test_case(proof_normal(proof_goal(D,C,B,A)), proof_goal(B,C,D,A));
-s_test_case(proof_normal(proof_iff(proof_c(proof_opt(C),A), B)), proof_iff(proof_c(A,C),B));
-
 /******************************************************************/
 /*                                                                */
 /*  Assessment functions                                          */
@@ -193,14 +166,6 @@ proof_remove_nullproof(ex):= block(
    apply(op(ex), map(proof_remove_nullproof, sublist(args(ex), lambda([ex2], not(is(ex2=nullproof)))))) 
 );
 
-s_test_case(proof_alternatives(proof(A,B,C,D)), [proof(A,B,C,D)]);
-s_test_case(proof_alternatives(proof_c(A,B)), [proof_c(A,B),proof_c(B,A)]);
-s_test_case(proof_alternatives(proof_iff(A,B)), [proof_iff(A,B),proof_iff(B,A)]);
-s_test_case(proof_alternatives(proof_ind(A,B,C,D)), [proof_ind(A,B,C,D),proof_ind(A,C,B,D)]);
-s_test_case(proof_alternatives(proof_cases(A,B,C)), [proof_cases(A,B,C),proof_cases(A,C,B)]);
-s_test_case(proof_alternatives(proof_goal(A,B,C)), [proof_goal(A,B,C),proof_goal(B,A,C)]);
-s_test_case(proof_alternatives(proof_iff(proof(proof_opt(A), B),C)), [proof_iff(proof(A,B),C),proof_iff(proof(B),C),proof_iff(C,proof(A,B)),proof_iff(C,proof(B))]);
-
 /******************************************************************/
 /*                                                                */
 /*  STACK Parson's block functions                                */
@@ -290,7 +255,6 @@ proof_keys_int(ex, proof_steps):= block(
  * Replace displayed LaTeX mathematics delimiters with inline.
  */
 proof_inline_maths(st) := ssubst("\\)", "\\]", ssubst("\\(", "\\[", st));
-s_test_case(proof_inline_maths("\\[ 3 = 2^{\\frac{p}{q}}\\]"), "\\( 3 = 2^{\\frac{p}{q}}\\)");
 
 /*
  * Prune out any narrative from the proof steps: used to display a proof without narrative.
@@ -418,13 +382,6 @@ proof_damerau_levenstein_tidy(L) := block(
   return(append([first(L)], proof_damerau_levenstein_tidy(rest(L))))
 );
 
-s_test_case(proof_damerau_levenstein([1,2,3],[1,2,3]), [0,[]]);
-s_test_case(proof_damerau_levenstein([1,2,3],[1,2,3,4]), [1,[dl_ok(1),dl_ok(2),dl_ok(3),dl_add(4)]]);
-s_test_case(proof_damerau_levenstein([1,3,4],[1,2,3,4]), [1,[dl_ok(1),dl_add(2),dl_ok(3),dl_ok(4)]]);
-s_test_case(proof_damerau_levenstein([3,4],[1,2,3,4]), [2,[dl_add(1),dl_add(2),dl_ok(3),dl_ok(4)]]);
-s_test_case(proof_damerau_levenstein([1,3,2,4],[1,2,3,4]), [1,[dl_ok(1),dl_swap(3,2),dl_swap_follow(2),dl_ok(4)]]);
-
-
 /*
   This function performs assessment of the student's proof.
   sa is the student's proof
diff --git a/stack/maxima/contrib/prooflib_test.mac b/stack/maxima/contrib/prooflib_test.mac
new file mode 100644
index 0000000000000000000000000000000000000000..cc5c1e7061589df044fff095ba1675794cc203d7
--- /dev/null
+++ b/stack/maxima/contrib/prooflib_test.mac
@@ -0,0 +1,73 @@
+/*  Author Chris Sangwin
+    University of Edinburgh
+    Copyright (C) 2023 Chris Sangwin
+
+    This program is free software: you can redistribute it or modify
+    it under the terms of the GNU General Public License version two.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+/******************************************************************/
+/*  Functions for representing, typesetting and assessing proof.  */
+/*  Mostly for use with Parsons problems.                         */
+/*                                                                */
+/*  Test cases.                                                   */
+/*                                                                */
+/*  Chris Sangwin, <C.J.Sangwin@ed.ac.uk>                         */
+/*  V1.0 May 2024                                                 */
+/*                                                                */
+/******************************************************************/
+
+s_test_case(proofp(proof(1,2,3)), true);
+s_test_case(proofp(proof_iff(1,2)), true);
+s_test_case(proofp(sin(x)), false);
+
+s_test_case(proof_validatep(proof(1,2,3)), true);
+s_test_case(proof_validatep(proof(1,2,proof(4,5,6))), true);
+s_test_case(proof_validatep(proof(1,2,proof_iff(4,5))), true);
+/* proof_opt must have exactly one sub-proof. */
+s_test_case(proof_validatep(proof(1,2,proof_opt(4,5))), false);
+/* proof_iff must have exactly two sub-proofs. */
+s_test_case(proof_validatep(proof(1,2,proof_iff(4))), false);
+s_test_case(proof_validatep(proof(1,2,proof_iff(4,5,6))), false);
+/* proof_ind must have exactly four sub-proofs. */
+s_test_case(proof_validatep(proof_ind(1,proof(2,3),proof(4,5),6)), true);
+s_test_case(proof_validatep(proof_ind(1,proof(2,3),proof(4,5))), false);
+s_test_case(proof_validatep(proof(1,proof_opt(2),proof_iff(4,5))), true);
+
+s_test_case(proof_flatten(proof_iff(proof(A,B),proof(C))), proof(A,B,C));
+s_test_case(proof_flatten(proof_c(proof(A,proof(B,C)),proof(D))), proof(A,B,C,D));
+
+s_test_case(proof_normal(proof_c(B,A,D,C)), proof_c(A,B,C,D));
+s_test_case(proof_normal(proof_iff(B,A)), proof_iff(A,B));
+s_test_case(proof_normal(proof_ind(D,C,B,A)), proof_ind(D,B,C,A));
+s_test_case(proof_normal(proof_cases(D,C,B,A)), proof_cases(D,A,B,C));
+s_test_case(proof_normal(proof_goal(D,C,B,A)), proof_goal(B,C,D,A));
+s_test_case(proof_normal(proof_iff(proof_c(proof_opt(C),A), B)), proof_iff(proof_c(A,C),B));
+
+s_test_case(proof_alternatives(proof(A,B,C,D)), [proof(A,B,C,D)]);
+s_test_case(proof_alternatives(proof_c(A,B)), [proof_c(A,B),proof_c(B,A)]);
+s_test_case(proof_alternatives(proof_iff(A,B)), [proof_iff(A,B),proof_iff(B,A)]);
+s_test_case(proof_alternatives(proof_ind(A,B,C,D)), [proof_ind(A,B,C,D),proof_ind(A,C,B,D)]);
+s_test_case(proof_alternatives(proof_cases(A,B,C)), [proof_cases(A,B,C),proof_cases(A,C,B)]);
+s_test_case(proof_alternatives(proof_goal(A,B,C)), [proof_goal(A,B,C),proof_goal(B,A,C)]);
+s_test_case(proof_alternatives(proof_iff(proof(proof_opt(A), B),C)), [proof_iff(proof(A,B),C),proof_iff(proof(B),C),proof_iff(C,proof(A,B)),proof_iff(C,proof(B))]);
+
+s_test_case(proof_parsons_interpret("{\"used\":[\"0\",\"3\",\"5\"],\"available\":[\"1\",\"2\",\"4\",\"6\",\"7\"]}"), proof("0","3","5"));
+
+s_test_case(proof_inline_maths("\\[ 3 = 2^{\\frac{p}{q}}\\]"), "\\( 3 = 2^{\\frac{p}{q}}\\)");
+
+/******************************************************************/
+
+s_test_case(proof_damerau_levenstein([1,2,3],[1,2,3]), [0,[]]);
+s_test_case(proof_damerau_levenstein([1,2,3],[1,2,3,4]), [1,[dl_ok(1),dl_ok(2),dl_ok(3),dl_add(4)]]);
+s_test_case(proof_damerau_levenstein([1,3,4],[1,2,3,4]), [1,[dl_ok(1),dl_add(2),dl_ok(3),dl_ok(4)]]);
+s_test_case(proof_damerau_levenstein([3,4],[1,2,3,4]), [2,[dl_add(1),dl_add(2),dl_ok(3),dl_ok(4)]]);
+s_test_case(proof_damerau_levenstein([1,3,2,4],[1,2,3,4]), [1,[dl_ok(1),dl_swap(3,2),dl_swap_follow(2),dl_ok(4)]]);
+
diff --git a/stack/maxima/contrib/validators.mac b/stack/maxima/contrib/validators.mac
index 6b048434d1b761c2f12b8d8a9ce0df9dcb6ac212..8c94f0807d5dc322d965f7ecc098706ea14a2811 100644
--- a/stack/maxima/contrib/validators.mac
+++ b/stack/maxima/contrib/validators.mac
@@ -29,8 +29,7 @@ validate_underscore(ex) := if is(sposition("_", string(ex)) = false) then ""
         else "Underscore characters are not permitted in this input.";
 
 /* Add in unit-test cases using STACK's s_test_case function.  At least two please! */
-s_test_case(validate_underscore(1+a1), "");
-s_test_case(validate_underscore(1+a_1), "Underscore characters are not permitted in this input.");
+/* Place test cases in validators_test.mac                                          */
 
 /* The student may not use a user-defined function, or arrays, anywhere in their input. */
 validate_nofunctions(ex):= block([op1,opp],
@@ -41,24 +40,11 @@ validate_nofunctions(ex):= block([op1,opp],
   apply(sconcat, map(validate_nofunctions, args(ex)))
 );
 
-s_test_case(validate_nofunctions(1+a1), "");
-s_test_case(validate_nofunctions(sin(n*x)), "");
-s_test_case(validate_nofunctions(-b#pm#sqrt(b^2-4*a*c)), "");
-s_test_case(validate_nofunctions(x(2)), "User-defined functions are not permitted in this input. In your answer \\(x\\) appears to be used as a function. ");
-s_test_case(validate_nofunctions(3*x(t)^2), "User-defined functions are not permitted in this input. In your answer \\(x\\) appears to be used as a function. ");
-s_test_case(validate_nofunctions(1+f(x+1)), "User-defined functions are not permitted in this input. In your answer \\(f\\) appears to be used as a function. ");
-s_test_case(validate_nofunctions(x(2)*y(3)), "User-defined functions are not permitted in this input. In your answer \\(x\\) appears to be used as a function. User-defined functions are not permitted in this input. In your answer \\(y\\) appears to be used as a function. ");
-
 /* The student may only use single-character variable names in their answer. */
 /* This is intended for use when Insert Stars is turned off, but we still want to indicate to students that they may have forgotten a star */
 validate_all_one_letter_variables(ex) := if not(is(ev(lmax(map(lambda([ex2],slength(string(ex2))),listofvars(ex))),simp)>1)) then ""
         else "Only single-character variable names are permitted in this input. Perhaps you forgot to use an asterisk (*) somewhere, or perhaps you used a Greek letter.";
 
-s_test_case(validate_all_one_letter_variables(1), "");
-s_test_case(validate_all_one_letter_variables((A*x+B)/(x^2+1) + C/x), "");
-s_test_case(validate_all_one_letter_variables((Ax+B)/(x^2+1) + C/x), "Only single-character variable names are permitted in this input. Perhaps you forgot to use an asterisk (*) somewhere, or perhaps you used a Greek letter.");
-s_test_case(validate_all_one_letter_variables((theta*x+B)/(x^2+1) + C/x), "Only single-character variable names are permitted in this input. Perhaps you forgot to use an asterisk (*) somewhere, or perhaps you used a Greek letter.");
-
 /* This provides more detailed feedback for students who try to enter fully closed or open intervals using [] or () instead of cc(a,b) or oo(a,b). */
 /* It is intended for early courses where students might be new to using this written notation and STACK. */
 /* This does not work well with "Check type of response" turned on, and provides slightly awkward feedback when students take a union of multiple intervals with incorrect syntax. */
@@ -68,11 +54,3 @@ validate_interval_syntax(ex):= block(
   else if is(safe_op(ex)="%union") then apply(sconcat, map(validate_interval_syntax, args(ex)))
   else return("")
 );
-
-s_test_case(validate_interval_syntax(cc(1,2)), "");
-s_test_case(validate_interval_syntax(oo(1,2)), "");
-s_test_case(validate_interval_syntax(%union(cc(1,2),oo(2,3))), "");
-s_test_case(validate_interval_syntax([1,2]), "To give a closed interval, use <code>cc(1,2)</code>, not <code>[1,2]</code>. ");
-s_test_case(validate_interval_syntax(ntuple(1,2)), "To give an open interval, use <code>oo(1,2)</code>, not <code>(1,2)</code>. ");
-s_test_case(validate_interval_syntax(%union([1,2],ntuple(2,3))), "To give a closed interval, use <code>cc(1,2)</code>, not <code>[1,2]</code>. To give an open interval, use <code>oo(2,3)</code>, not <code>(2,3)</code>. ");
-s_test_case(validate_interval_syntax(%union([1,2],%union(oo(1,2),[2,3]))), "To give a closed interval, use <code>cc(1,2)</code>, not <code>[1,2]</code>. To give a closed interval, use <code>cc(2,3)</code>, not <code>[2,3]</code>. ");
diff --git a/stack/maxima/contrib/validators_test.mac b/stack/maxima/contrib/validators_test.mac
new file mode 100644
index 0000000000000000000000000000000000000000..1a8b21aac4bc52122b612b31ebf3c6ffd6a81417
--- /dev/null
+++ b/stack/maxima/contrib/validators_test.mac
@@ -0,0 +1,49 @@
+/*  Author Chris Sangwin
+    University of Edinburgh
+    Copyright (C) 2023 Chris Sangwin
+
+    This program is free software: you can redistribute it or modify
+    it under the terms of the GNU General Public License version two.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+/****************************************************************/
+/*  Bespoke validators for STACK inputs: test cases             */
+/*                                                              */
+/*  Chris Sangwin, <C.J.Sangwin@ed.ac.uk>                       */
+/*  V1.0 June 2023                                              */
+/*                                                              */
+/*  Please use this file to add public bespoke validators.      */
+/*                                                              */
+/****************************************************************/
+
+s_test_case(validate_underscore(1+a1), "");
+s_test_case(validate_underscore(1+a_1), "Underscore characters are not permitted in this input.");
+
+s_test_case(validate_nofunctions(1+a1), "");
+s_test_case(validate_nofunctions(sin(n*x)), "");
+s_test_case(validate_nofunctions(-b#pm#sqrt(b^2-4*a*c)), "");
+s_test_case(validate_nofunctions(x(2)), "User-defined functions are not permitted in this input. In your answer \\(x\\) appears to be used as a function. ");
+s_test_case(validate_nofunctions(3*x(t)^2), "User-defined functions are not permitted in this input. In your answer \\(x\\) appears to be used as a function. ");
+s_test_case(validate_nofunctions(1+f(x+1)), "User-defined functions are not permitted in this input. In your answer \\(f\\) appears to be used as a function. ");
+s_test_case(validate_nofunctions(x(2)*y(3)), "User-defined functions are not permitted in this input. In your answer \\(x\\) appears to be used as a function. User-defined functions are not permitted in this input. In your answer \\(y\\) appears to be used as a function. ");
+
+s_test_case(validate_all_one_letter_variables(1), "");
+s_test_case(validate_all_one_letter_variables((A*x+B)/(x^2+1) + C/x), "");
+s_test_case(validate_all_one_letter_variables((Ax+B)/(x^2+1) + C/x), "Only single-character variable names are permitted in this input. Perhaps you forgot to use an asterisk (*) somewhere, or perhaps you used a Greek letter.");
+s_test_case(validate_all_one_letter_variables((theta*x+B)/(x^2+1) + C/x), "Only single-character variable names are permitted in this input. Perhaps you forgot to use an asterisk (*) somewhere, or perhaps you used a Greek letter.");
+
+s_test_case(validate_interval_syntax(cc(1,2)), "");
+s_test_case(validate_interval_syntax(oo(1,2)), "");
+s_test_case(validate_interval_syntax(%union(cc(1,2),oo(2,3))), "");
+s_test_case(validate_interval_syntax([1,2]), "To give a closed interval, use <code>cc(1,2)</code>, not <code>[1,2]</code>. ");
+s_test_case(validate_interval_syntax(ntuple(1,2)), "To give an open interval, use <code>oo(1,2)</code>, not <code>(1,2)</code>. ");
+s_test_case(validate_interval_syntax(%union([1,2],ntuple(2,3))), "To give a closed interval, use <code>cc(1,2)</code>, not <code>[1,2]</code>. To give an open interval, use <code>oo(2,3)</code>, not <code>(2,3)</code>. ");
+s_test_case(validate_interval_syntax(%union([1,2],%union(oo(1,2),[2,3]))), "To give a closed interval, use <code>cc(1,2)</code>, not <code>[1,2]</code>. To give a closed interval, use <code>cc(2,3)</code>, not <code>[2,3]</code>. ");
+
diff --git a/stack/maxima/contrib/vectorcalculus.mac b/stack/maxima/contrib/vectorcalculus.mac
index 28be777dc6bfb437bf24f8ff436ee6e71271e37a..2dc2890b5faa66ac80ece9654e6070f9f87f5581 100644
--- a/stack/maxima/contrib/vectorcalculus.mac
+++ b/stack/maxima/contrib/vectorcalculus.mac
@@ -36,16 +36,6 @@ grad(f, [vars]):= block([grad_vec],
     if return_vect then return(transpose(matrix(grad_vec))) else return(grad_vec)
 );
 
-s_test_case((return_vect:true, grad(x*y*z,[x,y,z])),matrix([y*z],[x*z],[x*y]));
-s_test_case((return_vect:true, grad(x*y*z,x,y,z)),matrix([y*z],[x*z],[x*y]));
-s_test_case((return_vect:true, grad(x*y*z)),matrix([y*z],[x*z],[x*y]));
-s_test_case((return_vect:false, grad(x*y*z,[x,y,z])),[y*z,x*z,x*y]);
-s_test_case((return_vect:false, grad(x*y*z,x,y,z)),[y*z,x*z,x*y]);
-s_test_case((return_vect:false, grad(x*y*z)),[y*z,x*z,x*y]);
-s_test_case((return_vect:false, grad(x^2 + x)),[2*x+1]);
-s_test_case((return_vect:true, grad(a+2*b+3*c+4*d+5*p)),matrix([1],[2],[3],[4],[5]));
-s_test_case((return_vect:true, grad(a+2*b+3*c+4*d+5*p,[p,d,c,b,a])),matrix([5],[4],[3],[2],[1]));
-
 /****************************************************************/
 /* Calculate the divergence of a vector-valued function         */
 /****************************************************************/
@@ -58,18 +48,6 @@ div(u, [vars]):= block([div_vec],
     return(apply("+", div_vec))
 );
 
-s_test_case(div([x^2*cos(y),y^3],[x,y]), 2*x*cos(y)+3*y^2);
-s_test_case(div(transpose(matrix([x^2*cos(y),y^3])),[x,y]), 2*x*cos(y)+3*y^2);
-s_test_case(div(matrix([x^2*cos(y),y^3]),[x,y]), 2*x*cos(y)+3*y^2);
-s_test_case(div([x^2*cos(y),y^3],[y,x]), -x^2*sin(y));
-s_test_case(div([y^3,x^2*cos(y)],[y,x]), 2*x*cos(y)+3*y^2);
-s_test_case(div([x^2*cos(y),y^3]), 2*x*cos(y)+3*y^2);
-s_test_case(div(transpose(matrix([x^2*cos(y),y^3]))), 2*x*cos(y)+3*y^2);
-s_test_case(div(matrix([x^2*cos(y),y^3])), 2*x*cos(y)+3*y^2);
-s_test_case(div([x^2*cos(y),y^3],x,y), 2*x*cos(y)+3*y^2);
-s_test_case(div(transpose(matrix([x^2*cos(y),y^3])),x,y), 2*x*cos(y)+3*y^2);
-s_test_case(div(matrix([x^2*cos(y),y^3]),x,y), 2*x*cos(y)+3*y^2);
-
 /****************************************************************/
 /* Calculate the curl of a vector-valued function               */
 /****************************************************************/
@@ -83,18 +61,6 @@ curl(u, [vars]):= block([cux, cuy, cuz],
     if return_vect then return(transpose(matrix([cux,cuy,cuz]))) else return([cux,cuy,cuz])
 );
 
-s_test_case((return_vect: true, curl([x*y*z,x*y*z,x*y*z],[x,y,z])),matrix([x*z-x*y],[x*y-y*z],[y*z-x*z]));
-s_test_case((return_vect: true, curl([x*y*z,x*y*z,x*y*z])),matrix([x*z-x*y],[x*y-y*z],[y*z-x*z]));
-s_test_case((return_vect: false, curl([x*y*z,x*y*z,x*y*z],[x,y,z])),[x*z-x*y,x*y-y*z,y*z-x*z]);
-s_test_case((return_vect: false, curl([x*y*z,x*y*z,x*y*z])),[x*z-x*y,x*y-y*z,y*z-x*z]);
-s_test_case((return_vect: true, curl([x*y*z,x*y*z,x*y*z],[y,z,x])),matrix([x*y-y*z],[y*z-x*z],[x*z-x*y]));
-s_test_case((return_vect: true, curl(matrix([x*y*z,x*y*z,x*y*z]),[x,y,z])),matrix([x*z-x*y],[x*y-y*z],[y*z-x*z]));
-s_test_case((return_vect: true, curl(matrix([x*y*z,x*y*z,x*y*z]),x,y,z)),matrix([x*z-x*y],[x*y-y*z],[y*z-x*z]));
-s_test_case((return_vect: true, curl(matrix([x*y*z,x*y*z,x*y*z]))),matrix([x*z-x*y],[x*y-y*z],[y*z-x*z]));
-s_test_case((return_vect: true, curl(matrix([x*y*z],[x*y*z],[x*y*z]),[x,y,z])),matrix([x*z-x*y],[x*y-y*z],[y*z-x*z]));
-s_test_case((return_vect: true, curl(matrix([x*y*z],[x*y*z],[x*y*z]),x,y,z)),matrix([x*z-x*y],[x*y-y*z],[y*z-x*z]));
-s_test_case((return_vect: true, curl(matrix([x*y*z],[x*y*z],[x*y*z]))),matrix([x*z-x*y],[x*y-y*z],[y*z-x*z]));
-
 /*******************************************************************/
 /* Calculate the directional derivative of a multivariate function */
 /*******************************************************************/
@@ -107,21 +73,3 @@ dir_deriv(f, u, [vars]):= block([unit_u, der],
     return(der)
 );
 
-s_test_case((return_vect: false, dir_deriv(x*y*z,[1,2,2],[x,y,z])),(y*z)/3+(2*x*z)/3+(2*x*y)/3);
-s_test_case((return_vect: true, dir_deriv(x*y*z,[1,2,2],[x,y,z])),(y*z)/3+(2*x*z)/3+(2*x*y)/3);
-s_test_case((return_vect: false, dir_deriv(x*y*z,[1,2,2])),(y*z)/3+(2*x*z)/3+(2*x*y)/3);
-s_test_case((return_vect: true, dir_deriv(x*y*z,[1,2,2])),(y*z)/3+(2*x*z)/3+(2*x*y)/3);
-s_test_case((return_vect: true, dir_deriv(x*y*z,[1,2,2],[y,z,x])),(2*y*z)/3+(x*z)/3+(2*x*y)/3);
-s_test_case((return_vect: true, dir_deriv(x*y*z,[1,2,2],x,y,z)),(y*z)/3+(2*x*z)/3+(2*x*y)/3);
-s_test_case((return_vect: false, dir_deriv(x*y*z,matrix([1,2,2]),[x,y,z])),(y*z)/3+(2*x*z)/3+(2*x*y)/3);
-s_test_case((return_vect: true, dir_deriv(x*y*z,matrix([1,2,2]),[x,y,z])),(y*z)/3+(2*x*z)/3+(2*x*y)/3);
-s_test_case((return_vect: false, dir_deriv(x*y*z,matrix([1,2,2]))),(y*z)/3+(2*x*z)/3+(2*x*y)/3);
-s_test_case((return_vect: true, dir_deriv(x*y*z,matrix([1,2,2]))),(y*z)/3+(2*x*z)/3+(2*x*y)/3);
-s_test_case((return_vect: true, dir_deriv(x*y*z,matrix([1,2,2]),[y,z,x])),(2*y*z)/3+(x*z)/3+(2*x*y)/3);
-s_test_case((return_vect: true, dir_deriv(x*y*z,matrix([1,2,2]),x,y,z)),(y*z)/3+(2*x*z)/3+(2*x*y)/3);
-s_test_case((return_vect: false, dir_deriv(x*y*z,transpose([1,2,2]),[x,y,z])),(y*z)/3+(2*x*z)/3+(2*x*y)/3);
-s_test_case((return_vect: true, dir_deriv(x*y*z,transpose([1,2,2]),[x,y,z])),(y*z)/3+(2*x*z)/3+(2*x*y)/3);
-s_test_case((return_vect: false, dir_deriv(x*y*z,transpose([1,2,2]))),(y*z)/3+(2*x*z)/3+(2*x*y)/3);
-s_test_case((return_vect: true, dir_deriv(x*y*z,transpose([1,2,2]))),(y*z)/3+(2*x*z)/3+(2*x*y)/3);
-s_test_case((return_vect: true, dir_deriv(x*y*z,transpose([1,2,2]),[y,z,x])),(2*y*z)/3+(x*z)/3+(2*x*y)/3);
-s_test_case((return_vect: true, dir_deriv(x*y*z,transpose([1,2,2]),x,y,z)),(y*z)/3+(2*x*z)/3+(2*x*y)/3);
diff --git a/stack/maxima/contrib/vectorcalculus_test.mac b/stack/maxima/contrib/vectorcalculus_test.mac
new file mode 100644
index 0000000000000000000000000000000000000000..5327e1e0b1404ed4457087eb61399911f2c3957f
--- /dev/null
+++ b/stack/maxima/contrib/vectorcalculus_test.mac
@@ -0,0 +1,77 @@
+/*  Author Luke Longworth
+    University of Canterbury
+    Copyright (C) 2024 Luke Longworth
+
+    This program is free software: you can redistribute it or modify
+    it under the terms of the GNU General Public License version two.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+    GNU General Public License for details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+/****************************************************************/
+/*  Vector calculus functions for STACK                         */
+/*                                                              */
+/*  Test cases.                                                 */
+/*                                                              */
+/*  V2.0 March 2024                                             */
+/*                                                              */
+/****************************************************************/
+
+s_test_case((return_vect:true, grad(x*y*z,[x,y,z])),matrix([y*z],[x*z],[x*y]));
+s_test_case((return_vect:true, grad(x*y*z,x,y,z)),matrix([y*z],[x*z],[x*y]));
+s_test_case((return_vect:true, grad(x*y*z)),matrix([y*z],[x*z],[x*y]));
+s_test_case((return_vect:false, grad(x*y*z,[x,y,z])),[y*z,x*z,x*y]);
+s_test_case((return_vect:false, grad(x*y*z,x,y,z)),[y*z,x*z,x*y]);
+s_test_case((return_vect:false, grad(x*y*z)),[y*z,x*z,x*y]);
+s_test_case((return_vect:false, grad(x^2 + x)),[2*x+1]);
+s_test_case((return_vect:true, grad(a+2*b+3*c+4*d+5*p)),matrix([1],[2],[3],[4],[5]));
+s_test_case((return_vect:true, grad(a+2*b+3*c+4*d+5*p,[p,d,c,b,a])),matrix([5],[4],[3],[2],[1]));
+
+s_test_case(div([x^2*cos(y),y^3],[x,y]), 2*x*cos(y)+3*y^2);
+s_test_case(div(transpose(matrix([x^2*cos(y),y^3])),[x,y]), 2*x*cos(y)+3*y^2);
+s_test_case(div(matrix([x^2*cos(y),y^3]),[x,y]), 2*x*cos(y)+3*y^2);
+s_test_case(div([x^2*cos(y),y^3],[y,x]), -x^2*sin(y));
+s_test_case(div([y^3,x^2*cos(y)],[y,x]), 2*x*cos(y)+3*y^2);
+s_test_case(div([x^2*cos(y),y^3]), 2*x*cos(y)+3*y^2);
+s_test_case(div(transpose(matrix([x^2*cos(y),y^3]))), 2*x*cos(y)+3*y^2);
+s_test_case(div(matrix([x^2*cos(y),y^3])), 2*x*cos(y)+3*y^2);
+s_test_case(div([x^2*cos(y),y^3],x,y), 2*x*cos(y)+3*y^2);
+s_test_case(div(transpose(matrix([x^2*cos(y),y^3])),x,y), 2*x*cos(y)+3*y^2);
+s_test_case(div(matrix([x^2*cos(y),y^3]),x,y), 2*x*cos(y)+3*y^2);
+
+s_test_case((return_vect: true, curl([x*y*z,x*y*z,x*y*z],[x,y,z])),matrix([x*z-x*y],[x*y-y*z],[y*z-x*z]));
+s_test_case((return_vect: true, curl([x*y*z,x*y*z,x*y*z])),matrix([x*z-x*y],[x*y-y*z],[y*z-x*z]));
+s_test_case((return_vect: false, curl([x*y*z,x*y*z,x*y*z],[x,y,z])),[x*z-x*y,x*y-y*z,y*z-x*z]);
+s_test_case((return_vect: false, curl([x*y*z,x*y*z,x*y*z])),[x*z-x*y,x*y-y*z,y*z-x*z]);
+s_test_case((return_vect: true, curl([x*y*z,x*y*z,x*y*z],[y,z,x])),matrix([x*y-y*z],[y*z-x*z],[x*z-x*y]));
+s_test_case((return_vect: true, curl(matrix([x*y*z,x*y*z,x*y*z]),[x,y,z])),matrix([x*z-x*y],[x*y-y*z],[y*z-x*z]));
+s_test_case((return_vect: true, curl(matrix([x*y*z,x*y*z,x*y*z]),x,y,z)),matrix([x*z-x*y],[x*y-y*z],[y*z-x*z]));
+s_test_case((return_vect: true, curl(matrix([x*y*z,x*y*z,x*y*z]))),matrix([x*z-x*y],[x*y-y*z],[y*z-x*z]));
+s_test_case((return_vect: true, curl(matrix([x*y*z],[x*y*z],[x*y*z]),[x,y,z])),matrix([x*z-x*y],[x*y-y*z],[y*z-x*z]));
+s_test_case((return_vect: true, curl(matrix([x*y*z],[x*y*z],[x*y*z]),x,y,z)),matrix([x*z-x*y],[x*y-y*z],[y*z-x*z]));
+s_test_case((return_vect: true, curl(matrix([x*y*z],[x*y*z],[x*y*z]))),matrix([x*z-x*y],[x*y-y*z],[y*z-x*z]));
+
+s_test_case((return_vect: false, dir_deriv(x*y*z,[1,2,2],[x,y,z])),(y*z)/3+(2*x*z)/3+(2*x*y)/3);
+s_test_case((return_vect: true, dir_deriv(x*y*z,[1,2,2],[x,y,z])),(y*z)/3+(2*x*z)/3+(2*x*y)/3);
+s_test_case((return_vect: false, dir_deriv(x*y*z,[1,2,2])),(y*z)/3+(2*x*z)/3+(2*x*y)/3);
+s_test_case((return_vect: true, dir_deriv(x*y*z,[1,2,2])),(y*z)/3+(2*x*z)/3+(2*x*y)/3);
+s_test_case((return_vect: true, dir_deriv(x*y*z,[1,2,2],[y,z,x])),(2*y*z)/3+(x*z)/3+(2*x*y)/3);
+s_test_case((return_vect: true, dir_deriv(x*y*z,[1,2,2],x,y,z)),(y*z)/3+(2*x*z)/3+(2*x*y)/3);
+s_test_case((return_vect: false, dir_deriv(x*y*z,matrix([1,2,2]),[x,y,z])),(y*z)/3+(2*x*z)/3+(2*x*y)/3);
+s_test_case((return_vect: true, dir_deriv(x*y*z,matrix([1,2,2]),[x,y,z])),(y*z)/3+(2*x*z)/3+(2*x*y)/3);
+s_test_case((return_vect: false, dir_deriv(x*y*z,matrix([1,2,2]))),(y*z)/3+(2*x*z)/3+(2*x*y)/3);
+s_test_case((return_vect: true, dir_deriv(x*y*z,matrix([1,2,2]))),(y*z)/3+(2*x*z)/3+(2*x*y)/3);
+s_test_case((return_vect: true, dir_deriv(x*y*z,matrix([1,2,2]),[y,z,x])),(2*y*z)/3+(x*z)/3+(2*x*y)/3);
+s_test_case((return_vect: true, dir_deriv(x*y*z,matrix([1,2,2]),x,y,z)),(y*z)/3+(2*x*z)/3+(2*x*y)/3);
+s_test_case((return_vect: false, dir_deriv(x*y*z,transpose([1,2,2]),[x,y,z])),(y*z)/3+(2*x*z)/3+(2*x*y)/3);
+s_test_case((return_vect: true, dir_deriv(x*y*z,transpose([1,2,2]),[x,y,z])),(y*z)/3+(2*x*z)/3+(2*x*y)/3);
+s_test_case((return_vect: false, dir_deriv(x*y*z,transpose([1,2,2]))),(y*z)/3+(2*x*z)/3+(2*x*y)/3);
+s_test_case((return_vect: true, dir_deriv(x*y*z,transpose([1,2,2]))),(y*z)/3+(2*x*z)/3+(2*x*y)/3);
+s_test_case((return_vect: true, dir_deriv(x*y*z,transpose([1,2,2]),[y,z,x])),(2*y*z)/3+(x*z)/3+(2*x*y)/3);
+s_test_case((return_vect: true, dir_deriv(x*y*z,transpose([1,2,2]),x,y,z)),(y*z)/3+(2*x*z)/3+(2*x*y)/3);
+
diff --git a/stack/maxima/stackmaxima.mac b/stack/maxima/stackmaxima.mac
index 7d746ba3d2c54277e21e4d6c627a843c3000df18..c41663ff8f2588bf3fe03228b307c34a0454d903 100644
--- a/stack/maxima/stackmaxima.mac
+++ b/stack/maxima/stackmaxima.mac
@@ -3334,4 +3334,4 @@ is_lang(code):=ev(is(%_STACK_LANG=code),simp=true)$
 
 /* Stack expects some output with the version number the output happens at */
 /* maximalocal.mac after additional library loading */
-stackmaximaversion:2024043000$
+stackmaximaversion:2024050600$
diff --git a/stack/maxima/stackstrings.mac b/stack/maxima/stackstrings.mac
index 0bb446768febb7b209193ac3a3b6017ced60fe32..585ac1fed67da82ed8a42c35bbaf28ea09fa875b 100644
--- a/stack/maxima/stackstrings.mac
+++ b/stack/maxima/stackstrings.mac
@@ -218,8 +218,42 @@ stackjson_stringify(obj) := block([tmp,r,l],
  if is(obj=und) then r:"null"
  else if is(obj=false) then r:"false"
  else if is(obj=true) then r:"true"
- else if stringp(obj) then (
-  r : sconcat("\"",simplode(map(stackjson_protect_escapes, charlist(obj))),"\"")
+ /* In the string case we do the following.*/
+ /*     1. Create a character list and protect escapes on each character. */
+ /*     2. Split the character list into batches of size 64 (maximum function argument limit in GCL). */
+ /*     3. Loop through each batch. */
+ /*           3a. Pass each batch to `sconcat` to create a batch string of length 64. */
+ /*         3b. Successively `sconcat` each batch on to the result string. */
+ /* Note that this can be achieved in a simpler way by using `simplode`, which was the case in the prior code. */
+ /* Prior code: `r : sconcat("\"",simplode(map(stackjson_protect_escapes, charlist(obj))),"\"")` */
+ /* However, there is evidence showing that this code is quadratic and that the batch optimisation helps to alleviate this. */
+ /* See here: https://docs.stack-assessment.org/en/Developer/Optimising_STACK_for_large_Maxima_variables/ for more details. */
+ else if stringp(obj) then block([char_list],
+  /* Create character list with escapes protected */
+  char_list: map(stackjson_protect_escapes, charlist(obj)),
+  /* Set batch size to 64, which is inferred from the maximum function argument limit in GCL-compiled Lisp. Other compilations (e.g., SBCL) are less limited in this respect. */
+  batch_size: 64,
+  /* Start the return string */
+  r: "\"",
+  /* Calculate the number of batches */
+  l : ev(ceiling(length(char_list)/batch_size), simp),
+  /* Loop through the batches */
+  i : 1,
+  while (ev(i <= l, simp)) do (
+    batch: [],
+    j: 1,
+    /* Create the batch by looping through the character list and push-popping */
+    while (ev(j <= batch_size and not emptyp(char_list), simp)) do (
+      push(pop(char_list), batch),
+      j: ev(j+1, simp)
+    ),
+    /* Pass the batch to sconcat, and then append the resultant batch string to the return string */
+    /* Note that reverse is required due to the push-popping when creating the batch */
+    r: sconcat(r, apply(sconcat, reverse(batch))),
+    i: ev(i + 1, simp)
+  ),
+  /* End the return string */
+  r: sconcat(r, "\"")
  ) else if is_stackmap(obj) then (
   l:[],
   for tmp in stackmap_keys(obj) do l:append(l,[sconcat(stackjson_stringify(tmp),":",stackjson_stringify(stackmap_get(obj,tmp)))]), 
diff --git a/stack/maximaparser/corrective_parser.php b/stack/maximaparser/corrective_parser.php
index 2c5422cd3bf6e24b01c5e1eb36e298915208c9cf..05d47c8443ffab2a11dc8ea3141d972969a76037 100644
--- a/stack/maximaparser/corrective_parser.php
+++ b/stack/maximaparser/corrective_parser.php
@@ -262,7 +262,7 @@ class maxima_corrective_parser {
     public static function handle_parse_error($exception, $string, &$errors, &$answernote, $decimals) {
         // @codingStandardsIgnoreStart
         // We also disallow backticks.
-        static $disallowedfinalchars = '/+*^#~=,_&`;:$-.<>';
+        static $disallowedfinalchars = '/+*^#~=,_&`;:$-.<>%';
         // @codingStandardsIgnoreEnd
 
         /**
diff --git a/stack/options.class.php b/stack/options.class.php
index 24ce2de1f305240e24f77b565ae17d6b79a6b83d..17b17e1fc1b2e5ce1e7dfb30a42dbdcd35452837 100644
--- a/stack/options.class.php
+++ b/stack/options.class.php
@@ -49,7 +49,7 @@ class stack_options {
                 'type'       => 'list',
                 'value'      => '*10',
                 'strict'     => true,
-                'values'     => array('*10', 'E'),
+                'values'     => ['*10', 'E'],
                 'caskey'     => 'texput_scientificnotation',
                 'castype'    => 'fun',
             ],
@@ -277,10 +277,10 @@ class stack_options {
      * @return array of choices for the scientific notation select menu.
      */
     public static function get_scientificnotation_options() {
-        return array(
+        return [
             '*10'  => get_string('scientificnotation_10', 'qtype_stack'),
             'E'    => get_string('scientificnotation_E', 'qtype_stack'),
-        );
+        ];
     }
 
     /**
@@ -353,4 +353,52 @@ class stack_options {
             '3' => get_string('showvalidationcompact', 'qtype_stack'),
         ];
     }
+
+    /**
+     * @return array of choices for the monospace input select menu.
+     */
+    public static function get_monospace_options() {
+        return [
+            // Options will appear in order listed, not key order.
+            // Keys need to match is_monospace() below.
+            '0' => get_string('inputtypealgebraic', 'qtype_stack'),
+            '1' => get_string('inputtypenumerical', 'qtype_stack'),
+            '2' => get_string('inputtypeunits', 'qtype_stack'),
+            '3' => get_string('inputtypevarmatrix', 'qtype_stack'),
+        ];
+    }
+
+    /**
+     * Get the monospace default for supplied input class.
+     * @return bool
+     *
+     * We have a class name in format 'stack_XXXX_input' where 'XXXX' is the input type.
+     * The monospace default config setting is a string in format '0,2,4' where the integers are
+     * the array keys from the option selection in get_monospace_options().
+     * We have to convert the input type to an integer and then check if it's in the config string.
+     */
+    public static function is_monospace($class) {
+        $options = [
+            // These need to match get_monospace_options() above.
+            '0' => 'algebraic',
+            '1' => 'numerical',
+            '2' => 'units',
+            '3' => 'varmatrix',
+        ];
+        $optionkey = array_search(explode('_', $class)[1], $options);
+        if ($optionkey === false) {
+            // This type of input not allowed to be monospace.
+            return false;
+        }
+
+        $monoinputkeys = explode(',', get_config('qtype_stack', 'inputmonospace'));
+
+        $key = array_search(strval($optionkey), $monoinputkeys, true);
+
+        if ($key === false) {
+            return false;
+        } else {
+            return true;
+        }
+    }
 }
diff --git a/styles.css b/styles.css
index ea7d42bd498a8c5fd3f28073dd65547827cdc34d..ac80eaf82d62b33281310f6c763c988cc21bcbd8 100644
--- a/styles.css
+++ b/styles.css
@@ -102,6 +102,9 @@ textarea[name=maximavars] {
     max-width: 100%;
     text-align: right;
 }
+.que.stack .input-monospace {
+    font-family: monospace;
+}
 
 .que.stack .formulation .stackprtfeedback {
     background: #fcf8e3;
diff --git a/tests/answertest_general_fixtures_test.php b/tests/answertest_general_fixtures_test.php
index 4271d53703100549a38e58723525a8174aa50614..5fd97e632bfb20ef76dcf03cf1f8360277cfcbbd 100644
--- a/tests/answertest_general_fixtures_test.php
+++ b/tests/answertest_general_fixtures_test.php
@@ -50,7 +50,7 @@ class answertest_general_fixtures_test extends qtype_stack_testcase {
         $this->assertTrue($passed, $anomalynote);
     }
 
-    public function answertest_fixtures() {
+    public static function answertest_fixtures():array {
 
         $tests = stack_answertest_test_data::get_all();
         $testdata = [];
diff --git a/tests/api_tests_stateful_test.php b/tests/api_tests_stateful_test.php
index 2f4d24226c2c2961d2cf4e7fead3b7bfe20c22b4..324daa861d1e730e32556e38e5b0cc65b3aef2de 100644
--- a/tests/api_tests_stateful_test.php
+++ b/tests/api_tests_stateful_test.php
@@ -50,6 +50,7 @@ require_once(__DIR__ . '/../stack/cas/evaluatable_object.interfaces.php');
  * @group qtype_stack
  * @group qtype_stateful
  * @group qtype_stack_compatibility
+ * @covers \qtype_stack
  */
 class api_tests_stateful_test extends \qtype_stack_testcase {
 
diff --git a/tests/behat/backup.feature b/tests/behat/backup.feature
index 606be43f3dbb169c137305fdcf1be4f93ffc05e0..e693de0ee04506902ff8b88b4be5b870c7e822c0 100644
--- a/tests/behat/backup.feature
+++ b/tests/behat/backup.feature
@@ -19,10 +19,16 @@ Feature: Test duplicating a quiz containing STACK questions
       | quiz       | Test quiz | C1     | quiz     |
     And quiz "Test quiz" contains the following questions:
       | Dropdown (shuffle)  | 1 |
+    And the following config values are set as admin:
+    | config | value |
+    | enableasyncbackup | true |
 
   @javascript
   Scenario: Backup and restore a course containing a STACK question
-    When I am on the "Course 1" course page logged in as admin
+    When I am logged in as admin
+    And I navigate to "Courses > Asynchronous backup/restore" in site administration
+    And I click on "Save changes" "button"
+    And I am on the "Course 1" course page logged in as admin
     And I backup "Course 1" course using this options:
       | Confirmation | Filename | test_backup.mbz |
     And I restore "test_backup.mbz" backup into a new course using this options:
diff --git a/tests/behat/restore_demo.feature b/tests/behat/restore_demo.feature
index 462d00c928752126512797270fd8658c72cd39be..80b26eb1e42cfa888f4dceeb23c1eb2eccd55ee9 100644
--- a/tests/behat/restore_demo.feature
+++ b/tests/behat/restore_demo.feature
@@ -8,7 +8,12 @@ Feature: Test restoring a backup including STACK questions
     Given the following "courses" exist:
       | fullname            | shortname |
       | Demonstrating STACK | STACK     |
+    And the following config values are set as admin:
+      | config | value |
+      | enableasyncbackup | true |
     And I log in as "admin"
+    And I navigate to "Courses > Asynchronous backup/restore" in site administration
+    And I click on "Save changes" "button"
     And I navigate to "Courses > Restore course" in site administration
 
   @javascript @_file_upload
diff --git a/tests/behat/restore_reveal_question.feature b/tests/behat/restore_reveal_question.feature
index 852db18a7548a1a326b3506bbc1ebc2876bb2267..028886d74e24fa84c54af0c907092ae63de8bf2b 100644
--- a/tests/behat/restore_reveal_question.feature
+++ b/tests/behat/restore_reveal_question.feature
@@ -8,7 +8,12 @@ Feature: Test restoring and testing an individual STACK question from the sample
     Given the following "courses" exist:
       | fullname            | shortname |
       | Demonstrating STACK | STACK     |
+    And the following config values are set as admin:
+      | config | value |
+      | enableasyncbackup | true |
     And I log in as "admin"
+    And I navigate to "Courses > Asynchronous backup/restore" in site administration
+    And I click on "Save changes" "button"
     And I navigate to "Courses > Restore course" in site administration
 
   @javascript @_file_upload
diff --git a/tests/cassession2_test.php b/tests/cassession2_test.php
index 4cd4e38b31351177ad0e9281b692f7408fc39dc9..af25a617a7412a28fc5f30ec5902d0c3a0302aa7 100644
--- a/tests/cassession2_test.php
+++ b/tests/cassession2_test.php
@@ -2844,6 +2844,58 @@ class cassession2_test extends qtype_stack_testcase {
 
     }
 
+    public function test_use_at() {
+
+        // This testcase arose as issue #762.
+        $cases = [
+            'k1:4',
+            'k2:5',
+            'k3:2',
+            'f1:x2^2-k1*u',
+            'f2:k2*x1-k3*x2+u',
+            'f3:x_1',
+            'u0: 1',
+            'f10:at(f1,[u=u0])',
+            'f20:at(f2,[u=u0])',
+            'eq1:f10=0',
+            'eq2:f20=0',
+            'sol:solve([eq1,eq2],[x1,x2])',
+            's1:sol[1]',
+            's2:sol[2]',
+            'x11:rhs(s1[1])',
+            'x12:rhs(s1[2])',
+            'x21:rhs(s2[1])',
+            'x22:rhs(s2[2])',
+            'P1:matrix([x11],[x12])',
+            'P2: matrix([x21],[x22])',
+            'var:[x1, x2]',
+            'inp:[u]',
+            'sys:[f1, f2]',
+            'A:jacobian(sys,var)',
+            'B:jacobian(sys, inp)',
+            'A1:at(A,[x1=x11,x2=x12, u=u0])',
+            'B1:at(B,[x1=x11,x2=x12, u=u0])',
+            'A2:at(A,[x1=x21, x2=x22, u=u0])',
+            'B2:at(B,[x1=x21, x2=x22, u=u0])',
+        ];
+
+        $s1 = [];
+        foreach ($cases as $case) {
+            $s1[] = stack_ast_container::make_from_teacher_source($case, '', new stack_cas_security(), []);
+        }
+        $options = new stack_options();
+        $options->set_option('simplify', true);
+        $session = new stack_cas_session2($s1, $options, 0);
+        $this->assertTrue($session->get_valid());
+
+        $session->instantiate();
+        $this->assertTrue($session->is_instantiated());
+        $v3 = $session->get_by_key('A2');
+        $this->assertEquals('matrix([0,4],[5,-2])', $v3->get_value());
+        $t1 = $session->get_by_key('B2');
+        $this->assertEquals('matrix([-4],[1])', $t1->get_value());
+    }
+
     public function test_s_test() {
 
         $cases = ['t1:s_assert(a,b)'];
diff --git a/tests/editform_test.php b/tests/editform_test.php
index f08efb7dfa97bc98be87f6553c6775231315e700..32743faf4894e37ef81c65ff02d646b70212ca7e 100644
--- a/tests/editform_test.php
+++ b/tests/editform_test.php
@@ -31,7 +31,7 @@ require_once(__DIR__ . '/../edit_stack_form.php');
  * @group qtype_stack
  * @covers \qtype_stack_edit_form
  */
-class editform_test extends \qtype_stack_edit_form {
+class editform_test_class extends \qtype_stack_edit_form {
 
     public function __construct($questiontext, $specificfeedback) {
         global $USER;
@@ -69,13 +69,13 @@ class editform_test extends \qtype_stack_edit_form {
  * @group qtype_stack
  * @covers \qtype_stack_edit_form
  */
-class qtype_stack_edit_form_test extends \advanced_testcase {
+class editform_test extends \advanced_testcase {
 
     protected function get_form($questiontext, $specificfeedback) {
         $this->setAdminUser();
         $this->resetAfterTest();
 
-        return new \qtype_stack_edit_form_testable($questiontext, $specificfeedback);
+        return new editform_test_class($questiontext, $specificfeedback);
     }
 
     public function test_get_input_names_from_question_text_default() {
diff --git a/tests/fixtures/answertestfixtures.class.php b/tests/fixtures/answertestfixtures.class.php
index 0d3d8d34eb9c182bcd5bc027fdc80ce7f5003f60..b5d0972920820ad63ba4972a9918d79dde21201c 100644
--- a/tests/fixtures/answertestfixtures.class.php
+++ b/tests/fixtures/answertestfixtures.class.php
@@ -2004,6 +2004,10 @@ class stack_answertest_test_data {
         ['NumSigFigs', '2', '5.3e21', '5.3e21', 1, '', ''],
         ['NumSigFigs', '2', '5.3e22', '5.3e22', 1, '', ''],
         ['NumSigFigs', '2', '5.3e20', '5.3e22', 0, 'ATNumSigFigs_VeryInaccurate.', ''],
+        // The next test cases were raised in issue #1108, but it's not a bug.
+        ['NumSigFigs', '2', '9.8', '10', 1, '', ''],
+        ['NumSigFigs', '2', '9.5', '10', 0, 'ATNumSigFigs_Inaccurate.', ''],
+        ['NumSigFigs', '2', '10.0', '10', 0, 'ATNumSigFigs_WrongDigits.', ''],
         ['NumSigFigs', '9', '6.02214086e23', '6.02214086e23', 1, '', ''],
         ['NumSigFigs', '9', '6.0221409e23', '6.02214086e23', 0, 'ATNumSigFigs_WrongDigits. ATNumSigFigs_Inaccurate.', ''],
         ['NumSigFigs', '9', '6.02214087e23', '6.02214086e23', 0, 'ATNumSigFigs_Inaccurate.', ''],
diff --git a/tests/fixtures/apifixtures.class.php b/tests/fixtures/apifixtures.class.php
index 79778474dcaab1e1f92aeb4cb13822e1f801fe29..ed1d53eb39601f7880297145fb2d84f3bc5d093e 100644
--- a/tests/fixtures/apifixtures.class.php
+++ b/tests/fixtures/apifixtures.class.php
@@ -21,8 +21,6 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-defined('MOODLE_INTERNAL') || die();
-
 class stack_api_test_data {
     protected static array $questiondata = [
         'matrices' =>
@@ -36,7 +34,8 @@ class stack_api_test_data {
                 <p> [[input:ans1]] [[validation:ans1]]</p>]]></text>
                     </questiontext>
                     <generalfeedback format="html">
-                    <text><![CDATA[<p>To multiply matrices \(A\) and \(B\) we need to remember that the \((i,j)\)th entry is the scalar product of the \(i\)th row of \(A\) with the \(j\)th column of \(B\).</p>
+                    <text><![CDATA[<p>To multiply matrices \(A\) and \(B\) we need to remember that the \((i,j)\)th entry
+                    is the scalar product of the \(i\)th row of \(A\) with the \(j\)th column of \(B\).</p>
                 <p>\[ {@A@}.{@B@} = {@C@} = {@D@}.\]</p>]]></text>
                     </generalfeedback>
                     <defaultgrade>5.0000000</defaultgrade>
@@ -52,7 +51,8 @@ class stack_api_test_data {
                 TB:ev(A*B,simp);
                 BT:transpose(B);
                 C:zeromatrix (first(matrix_size(A)), second(matrix_size(A)));
-                S:for a:1 thru first(matrix_size(A)) do for b:1 thru second(matrix_size(A)) do C[ev(a,simp),ev(b,simp)]:apply("+",zip_with("*",A[ev(a,simp)],BT[ev(b,simp)]));
+                S:for a:1 thru first(matrix_size(A)) do for b:1 thru second(matrix_size(A)) do
+                C[ev(a,simp),ev(b,simp)]:apply("+",zip_with("*",A[ev(a,simp)],BT[ev(b,simp)]));
                 D:ev(C,simp);
                 C:C;]]></text>
                     </questionvariables>
@@ -143,7 +143,8 @@ class stack_api_test_data {
                         <truenextnode>-1</truenextnode>
                         <trueanswernote>1-1-T </trueanswernote>
                         <truefeedback format="html">
-                        <text><![CDATA[<p>Remember, you do not multiply matrices by multiplying the corresponding entries! A quite different process is needed.</p>]]></text>
+                        <text><![CDATA[<p>Remember, you do not multiply matrices by multiplying the
+                        corresponding entries! A quite different process is needed.</p>]]></text>
                         </truefeedback>
                         <falsescoremode>=</falsescoremode>
                         <falsescore>0.0000000</falsescore>
@@ -246,7 +247,9 @@ class stack_api_test_data {
                  <text><![CDATA[<p>Find \[ \int {@p@} d{@v@}\] [[input:ans1]] [[validation:ans1]]</p>]]></text>
                </questiontext>
                <generalfeedback format="html">
-                 <text><![CDATA[<p>We can either do this question by inspection (i.e. spot the answer) or in a more formal manner by using the substitution \[ u = ({@v@}-{@a@}).\] Then, since \(\frac{d}{d{@v@}}u=1\) we have \[ \int {@p@} d{@v@} = \int u^{@n@} du = \frac{u^{@n+1@}}{@n+1@}+c = {@ta@}+c.\]</p>]]></text>
+                 <text><![CDATA[<p>We can either do this question by inspection (i.e. spot the answer) or in a
+                 more formal manner by using the substitution \[ u = ({@v@}-{@a@}).\] Then, since \(\frac{d}{d{@v@}}u=1\)
+                 we have \[ \int {@p@} d{@v@} = \int u^{@n@} du = \frac{u^{@n+1@}}{@n+1@}+c = {@ta@}+c.\]</p>]]></text>
                </generalfeedback>
                <defaultgrade>1.0000000</defaultgrade>
                <penalty>0.1000000</penalty>
@@ -381,7 +384,10 @@ class stack_api_test_data {
                 <text><![CDATA[<p>Find \[ \int {@p@} d{@v@}\] [[input:ans1]] [[validation:ans1]]</p>]]></text>
                 </questiontext>
                 <generalfeedback format="html">
-                <text><![CDATA[<p>We can either do this question by inspection (i.e. spot the answer) or in a more formal manner by using the substitution \[ u = ({@v@}-{@a@}).\] Then, since \(\frac{d}{d{@v@}}u=1\) we have \[ \int {@p@} d{@v@} = \int u^{@n@} du = \frac{u^{@n+1@}}{@n+1@}+c = {@ta@}+c.\]</p>]]></text>
+                <text><![CDATA[<p>We can either do this question by inspection (i.e. spot the answer)
+                or in a more formal manner by using the substitution \[ u = ({@v@}-{@a@}).\] Then,
+                since \(\frac{d}{d{@v@}}u=1\) we have \[ \int {@p@} d{@v@} = \int u^{@n@}
+                du = \frac{u^{@n+1@}}{@n+1@}+c = {@ta@}+c.\]</p>]]></text>
                 </generalfeedback>
                 <defaultgrade>1.0000000</defaultgrade>
                 <penalty>0.1000000</penalty>
@@ -535,12 +541,14 @@ class stack_api_test_data {
             <questiontext format="html">
               <text><![CDATA[<p></p>
 
-        <p>a) Two straight lines \(g\) and \(h\) are given by \(g:\ x+y=1\) and \(h:\ x-y=1\). What applies to the positional relationship of these lines?</p>
+        <p>a) Two straight lines \(g\) and \(h\) are given by \(g:\ x+y=1\) and \(h:\ x-y=1\).
+        What applies to the positional relationship of these lines?</p>
         <p>[[input:ans1]] [[validation:ans1]][[feedback:prt1]]</p>
 
         <hr>
 
-        <p style="margin-top:1em;">b) Now two straight lines \(\tilde g\) and \(\tilde h\) are given by \(\tilde g:\ t\,x+y=1,\quad \tilde h:\ x+t\,y=1\) with a real parameter \(t\).</p>
+        <p style="margin-top:1em;">b) Now two straight lines \(\tilde g\) and \(\tilde h\) are given by
+        \(\tilde g:\ t\,x+y=1,\quad \tilde h:\ x+t\,y=1\) with a real parameter \(t\).</p>
         <p> Determine the parameter \(t\) for the following cases.</p>
 
         <p style="margin-top: 1.5em">The lines are identical for \(t=\) [[input:ans2]] [[validation:ans2]][[feedback:prt2]]</p>
@@ -561,7 +569,9 @@ class stack_api_test_data {
             <questionvariables>
               <text><![CDATA[/*Stephan Bach, OTH Amberg-Weiden*/
 
-        ta1:[[a,false,"The lines are identical."], [b,false,"The lines are parallel (but not identical)."],[c,true,"The lines are perpendicular to each other."],[d,false,"The lines intersect but are not perpendicular to each other."]];
+        ta1:[[a,false,"The lines are identical."], [b,false,"The lines are parallel (but not identical)."],
+        [c,true,"The lines are perpendicular to each other."],[d,false,"The lines
+        intersect but are not perpendicular to each other."]];
         ta2:1;
         ta3:-1;
         ta4:0;]]></text>
@@ -579,13 +589,18 @@ class stack_api_test_data {
             <assumepositive>0</assumepositive>
             <assumereal>0</assumereal>
             <prtcorrect format="html">
-              <text><![CDATA[<p><img alt="Richtig" title="Richtig" src="https://moodle.oth-aw.de/theme/image.php/clean/core/1554451383/i/grade_correct">Correct answer, well done!</p>]]></text>
+              <text><![CDATA[<p><img alt="Richtig" title="Richtig"
+              src="https://moodle.oth-aw.de/theme/image.php/clean/core/1554451383/i/grade_correct">Correct
+              answer, well done!</p>]]></text>
             </prtcorrect>
             <prtpartiallycorrect format="html">
-              <text><![CDATA[<p><span style="font-size:24px;color:grey;">! </span>Your answer is partially correct.</p>]]></text>
+              <text><![CDATA[<p><span style="font-size:24px;color:grey;">!
+              </span>Your answer is partially correct.</p>]]></text>
             </prtpartiallycorrect>
             <prtincorrect format="html">
-              <text><![CDATA[<p><img alt="Falsch" title="Falsch" src="https://moodle.oth-aw.de/theme/image.php/clean/core/1554451383/i/grade_incorrect"> Wrong answer.</p>]]></text>
+              <text><![CDATA[<p><img alt="Falsch" title="Falsch"
+              src="https://moodle.oth-aw.de/theme/image.php/clean/core/1554451383/i/grade_incorrect">
+              Wrong answer.</p>]]></text>
             </prtincorrect>
             <multiplicationsign>dot</multiplicationsign>
             <sqrtsign>1</sqrtsign>
@@ -947,7 +962,9 @@ class stack_api_test_data {
 
         /*Plots*/
         gcol:[blue,red,red,red,red];
-        p:makelist( plot(g[i], [x,xmin,xmax], [y,ymin,ymax], [axes,solid], [box,false], [xtics,xmax+1,0,xmax+1],[ytics,ymax+1,0,ymax+1], [label,["x",xmax-dx,-dy], ["y",-dx,ymax-dy]], [color,gcol[i]]),i,1,5);
+        p:makelist( plot(g[i], [x,xmin,xmax], [y,ymin,ymax], [axes,solid], [box,false],
+        [xtics,xmax+1,0,xmax+1],[ytics,ymax+1,0,ymax+1], [label,["x",xmax-dx,-dy],
+        ["y",-dx,ymax-dy]], [color,gcol[i]]),i,1,5);
 
         /*Model answer*/
         ta:[[a,true,p[2]],[b,false,p[3]],[c,false,p[4]],[d,false,p[5]]];
@@ -956,7 +973,8 @@ class stack_api_test_data {
 
         /*For the answer note:*/
         gcol2:[blue,green,red,red,red];
-        p2:makelist( plot(g[i], [x,xmin,xmax], [y,ymin,ymax], [axes,solid], [box,false], [xtics,xmax+1,0,xmax+1],[ytics,ymax+1,0,ymax+1],[color,gcol2[i]], [size,200,200]),i,1,5);
+        p2:makelist( plot(g[i], [x,xmin,xmax], [y,ymin,ymax], [axes,solid], [box,false],
+        [xtics,xmax+1,0,xmax+1],[ytics,ymax+1,0,ymax+1],[color,gcol2[i]], [size,200,200]),i,1,5);
         p2:append([p2[1]], makelist(p2[n[i]+1],i,1,4) );]]></text>
             </questionvariables>
             <specificfeedback format="html">
@@ -990,13 +1008,17 @@ class stack_api_test_data {
             <assumepositive>0</assumepositive>
             <assumereal>0</assumereal>
             <prtcorrect format="html">
-              <text><![CDATA[<p><img alt="Richtig" title="Richtig" src="https://moodle.oth-aw.de/theme/image.php/clean/core/1554451383/i/grade_correct">Correct answer, well done!</p>]]></text>
+              <text><![CDATA[<p><img alt="Richtig" title="Richtig"
+              src="https://moodle.oth-aw.de/theme/image.php/clean/core/1554451383/i/grade_correct">Correct answer,
+              well done!</p>]]></text>
             </prtcorrect>
             <prtpartiallycorrect format="html">
               <text></text>
             </prtpartiallycorrect>
             <prtincorrect format="html">
-              <text><![CDATA[<p><img alt="Falsch" title="Falsch" src="https://moodle.oth-aw.de/theme/image.php/clean/core/1554451383/i/grade_incorrect"> Wrong answer.</p>]]></text>
+              <text><![CDATA[<p><img alt="Falsch" title="Falsch"
+              src="https://moodle.oth-aw.de/theme/image.php/clean/core/1554451383/i/grade_incorrect">
+              Wrong answer.</p>]]></text>
             </prtincorrect>
             <multiplicationsign>dot</multiplicationsign>
             <sqrtsign>1</sqrtsign>
@@ -1086,16 +1108,27 @@ class stack_api_test_data {
               <text>Interactivity: Drag points to be increasing</text>
             </name>
             <questiontext format="html">
-              <text><![CDATA[<p>Drag the points \(u_1,\ldots, u_8\) so that they show the first 8 terms of an increasing sequence.</p>
+              <text><![CDATA[<p>Drag the points \(u_1,\ldots, u_8\) so that
+              they show the first 8 terms of an increasing sequence.</p>
         <p style="display:none">[[input:da_ans1]] [[validation:da_ans1]]</p>
-        [[jsxgraph width="360px" height="360px" input-ref-da_ans1="inputans1"]] JXG.Options.axis.ticks.minorTicks = 0; var board = JXG.JSXGraph.initBoard(divid, { boundingbox: [-1, 10, 9, -10], axis: true, grid: true, showNavigation: false, showCopyright: false
-        }); /* State represented as a JS-object, first define default then try loading the stored values. */ var state = [1,1,1,1,1,1,1,1]; var stateInput = document.getElementById(inputans1); if (stateInput.value) {
+        [[jsxgraph width="360px" height="360px" input-ref-da_ans1="inputans1"]] JXG.Options.axis.ticks.minorTicks = 0;
+        var board = JXG.JSXGraph.initBoard(divid, { boundingbox: [-1, 10, 9, -10], axis: true,
+          grid: true, showNavigation: false, showCopyright: false
+        }); /* State represented as a JS-object, first define default then try loading the
+        stored values. */ var state = [1,1,1,1,1,1,1,1]; var stateInput =
+        document.getElementById(inputans1); if (stateInput.value) {
           if(stateInput.value != \'\') {
             state = JSON.parse(stateInput.value);
           }
-        } /* create a group of vertical lines x=i with a draggable point on each one */ var vline = []; var answer = []; for (let i of [1, 2, 3, 4, 5, 6, 7, 8]) { vline.push(board.create(\'line\', [i, -1, 0] /* given [c,a,b] plot ax+by+c=0
-        */ , { visible: false })); /* create the draggable points, each constrained to lie on one of the vertical lines, and using the existing state for the y-coordinate */ answer.push(board.create(\'glider\', [i, state[i-1], vline[i - 1]], { color: \'#003399\',
-        name: "u" + i, showInfobox: false })); } /* update the stored state when things change */ board.on(\'update\', function() { var vals = []; for (let pts of answer) { vals.push(pts.Y()); }; stateInput.value = "[" + vals + "]"; }); [[/jsxgraph]]]]></text>
+        } /* create a group of vertical lines x=i with a draggable point on each one */ var vline = [];
+        var answer = []; for (let i of [1, 2, 3, 4, 5, 6, 7, 8]) { vline.push(board.create(\'line\', [i, -1, 0] /*
+          given [c,a,b] plot ax+by+c=0
+        */ , { visible: false })); /* create the draggable points, each constrained to lie on one of the vertical lines,
+        and using the existing state for the y-coordinate */ answer.push(board.create(\'glider\',
+        [i, state[i-1], vline[i - 1]], { color: \'#003399\',
+        name: "u" + i, showInfobox: false })); } /* update the stored state when things change */
+        board.on(\'update\', function() { var vals = []; for (let pts of answer) { vals.push(pts.Y()); };
+        stateInput.value = "[" + vals + "]"; }); [[/jsxgraph]]]]></text>
             </questiontext>
             <generalfeedback format="html">
               <text></text>
@@ -1123,13 +1156,16 @@ class stack_api_test_data {
             <assumepositive>0</assumepositive>
             <assumereal>0</assumereal>
             <prtcorrect format="html">
-              <text><![CDATA[<span style="font-size: 1.5em; color:green;"><i class="fa fa-check"></i></span> Correct answer, well done.]]></text>
+              <text><![CDATA[<span style="font-size: 1.5em; color:green;"><i class="fa fa-check"></i>
+              </span> Correct answer, well done.]]></text>
             </prtcorrect>
             <prtpartiallycorrect format="html">
-              <text><![CDATA[<span style="font-size: 1.5em; color:orange;"><i class="fa fa-adjust"></i></span> Your answer is partially correct.]]></text>
+              <text><![CDATA[<span style="font-size: 1.5em; color:orange;"><i class="fa fa-adjust"></i>
+              </span> Your answer is partially correct.]]></text>
             </prtpartiallycorrect>
             <prtincorrect format="html">
-              <text><![CDATA[<span style="font-size: 1.5em; color:red;"><i class="fa fa-times"></i></span> Incorrect answer.]]></text>
+              <text><![CDATA[<span style="font-size: 1.5em; color:red;"><i class="fa fa-times"></i>
+              </span> Incorrect answer.]]></text>
             </prtincorrect>
             <multiplicationsign>dot</multiplicationsign>
             <sqrtsign>1</sqrtsign>
@@ -1231,7 +1267,8 @@ class stack_api_test_data {
             <questiontext format="html">
               <text><![CDATA[[[comment]]Use them like this in the question-text.[[/comment]]
         <p>Load the data from
-        <a href="[[textdownload name="data.csv"]]{@stack_csv_formatter(data,lab)@}[[/textdownload]]">this file</a> and calculate the mean of data set \(A\).</p>
+        <a href="[[textdownload name="data.csv"]]{@stack_csv_formatter(data,lab)@}[[/textdownload]]">this file</a>
+        and calculate the mean of data set \(A\).</p>
         <p>[[input:ans1]] [[validation:ans1]]</p>]]></text>
             </questiontext>
             <generalfeedback format="moodle_auto_format">
@@ -1264,13 +1301,16 @@ class stack_api_test_data {
             <assumepositive>0</assumepositive>
             <assumereal>0</assumereal>
             <prtcorrect format="html">
-              <text><![CDATA[<span style="font-size: 1.5em; color:green;"><i class="fa fa-check"></i></span> Correct answer, well done.]]></text>
+              <text><![CDATA[<span style="font-size: 1.5em; color:green;">
+              <i class="fa fa-check"></i></span> Correct answer, well done.]]></text>
             </prtcorrect>
             <prtpartiallycorrect format="html">
-              <text><![CDATA[<span style="font-size: 1.5em; color:orange;"><i class="fa fa-adjust"></i></span> Your answer is partially correct.]]></text>
+              <text><![CDATA[<span style="font-size: 1.5em; color:orange;">
+              <i class="fa fa-adjust"></i></span> Your answer is partially correct.]]></text>
             </prtpartiallycorrect>
             <prtincorrect format="html">
-              <text><![CDATA[<span style="font-size: 1.5em; color:red;"><i class="fa fa-times"></i></span> Incorrect answer.]]></text>
+              <text><![CDATA[<span style="font-size: 1.5em; color:red;"><i class="fa fa-times"></i></span>
+              Incorrect answer.]]></text>
             </prtincorrect>
             <decimals>.</decimals>
             <scientificnotation>*10</scientificnotation>
diff --git a/tests/fixtures/inputfixtures.class.php b/tests/fixtures/inputfixtures.class.php
index 1bbe86a9f49dd73f0f509dfd116003cd54c4d904..ce0a6f69610c569bba8f65dfa5e3b9f053cadaf5 100644
--- a/tests/fixtures/inputfixtures.class.php
+++ b/tests/fixtures/inputfixtures.class.php
@@ -97,6 +97,8 @@ class stack_inputvalidation_test_data {
         ['3E2', 'php_true', 'displaysci(3,0,2)', 'cas_true', '3 \times 10^{2}', '', ""],
         ['3e2', 'php_true', 'displaysci(3,0,2)', 'cas_true', '3 \times 10^{2}', '', ""],
         ['3e-2', 'php_true', 'displaysci(3,0,-2)', 'cas_true', '3 \times 10^{-2}', '', ""],
+        ['52%', 'php_false', '52%', '', '', 'finalChar', ""],
+        ['5.20%', 'php_false', '5.20%', '', '', 'finalChar', ""],
         ['3.67x10^2', 'php_true', 'dispdp(3.67,2)*x*10^2', 'cas_true', '3.67\cdot x\cdot 10^2', 'missing_stars', ""],
         ['1+i', 'php_true', '1+i', 'cas_true', '1+\mathrm{i}', '', ""],
         ['3-i', 'php_true', '3-i', 'cas_true', '3-\mathrm{i}', '', ""],
diff --git a/tests/input_algebraic_test.php b/tests/input_algebraic_test.php
index 98682b2b7ed2934eb9494b7663c746cff858423f..7e99025a96629dc32ff104b054b0558e4bb4c05c 100644
--- a/tests/input_algebraic_test.php
+++ b/tests/input_algebraic_test.php
@@ -1135,6 +1135,172 @@ class input_algebraic_test extends qtype_stack_testcase {
                 $el->render($state, 'stack1__ans1', false, null));
     }
 
+    public function test_validate_student_response_with_monospace() {
+        $options = new stack_options();
+        $el = stack_input_factory::make('algebraic', 'sans1', '1/2');
+        $el->set_parameter('options', 'align:right, monospace');
+        $state = $el->validate_student_response(['sans1' => 'sin(x)'], $options, '3.14', new stack_cas_security());
+        // In this case empty responses jump straight to score.
+        $this->assertEquals(stack_input::VALID, $state->status);
+        $this->assertEquals('sin(x)', $state->contentsmodified);
+        $this->assertEquals('\[ \sin \left( x \right) \]', $state->contentsdisplayed);
+        $this->assertEquals('', $state->errors);
+        $this->assertEquals('The answer <span class="filter_mathjaxloader_equation"><span class="nolink">' .
+                '\[ \[ \sin \left( x \right) \]</span></span> \), which can be typed as <code>sin(x)</code>,' .
+                ' would be correct.',
+                $el->get_teacher_answer_display($state->contentsmodified, $state->contentsdisplayed));
+        $this->assertEquals('<input type="text" name="stack1__ans1" id="stack1__ans1" size="16.5" ' .
+                'style="width: 13.6em" autocapitalize="none" spellcheck="false" ' .
+                'class="algebraic-right input-monospace" value="sin(x)" />',
+                $el->render($state, 'stack1__ans1', false, null));
+    }
+
+    public function test_validate_student_response_with_no_monospace_default_on() {
+        $options = new stack_options();
+        set_config('inputmonospace', '0,1,2', 'qtype_stack');
+        $el = stack_input_factory::make('algebraic', 'sans1', '1/2');
+        $el->set_parameter('options', 'align:right');
+        $state = $el->validate_student_response(['sans1' => 'sin(x)'], $options, '3.14', new stack_cas_security());
+        // In this case empty responses jump straight to score.
+        $this->assertEquals(stack_input::VALID, $state->status);
+        $this->assertEquals('sin(x)', $state->contentsmodified);
+        $this->assertEquals('\[ \sin \left( x \right) \]', $state->contentsdisplayed);
+        $this->assertEquals('', $state->errors);
+        $this->assertEquals('The answer <span class="filter_mathjaxloader_equation"><span class="nolink">' .
+                '\[ \[ \sin \left( x \right) \]</span></span> \), which can be typed as <code>sin(x)</code>,' .
+                ' would be correct.',
+                $el->get_teacher_answer_display($state->contentsmodified, $state->contentsdisplayed));
+        $this->assertEquals('<input type="text" name="stack1__ans1" id="stack1__ans1" size="16.5" ' .
+                'style="width: 13.6em" autocapitalize="none" spellcheck="false" ' .
+                'class="algebraic-right input-monospace" value="sin(x)" />',
+                $el->render($state, 'stack1__ans1', false, null));
+    }
+
+    public function test_validate_student_response_with_no_monospace_single_default_on() {
+        $options = new stack_options();
+        set_config('inputmonospace', '0', 'qtype_stack');
+        $el = stack_input_factory::make('algebraic', 'sans1', '1/2');
+        $el->set_parameter('options', 'align:right');
+        $state = $el->validate_student_response(['sans1' => 'sin(x)'], $options, '3.14', new stack_cas_security());
+        // In this case empty responses jump straight to score.
+        $this->assertEquals(stack_input::VALID, $state->status);
+        $this->assertEquals('sin(x)', $state->contentsmodified);
+        $this->assertEquals('\[ \sin \left( x \right) \]', $state->contentsdisplayed);
+        $this->assertEquals('', $state->errors);
+        $this->assertEquals('The answer <span class="filter_mathjaxloader_equation"><span class="nolink">' .
+                '\[ \[ \sin \left( x \right) \]</span></span> \), which can be typed as <code>sin(x)</code>,' .
+                ' would be correct.',
+                $el->get_teacher_answer_display($state->contentsmodified, $state->contentsdisplayed));
+        $this->assertEquals('<input type="text" name="stack1__ans1" id="stack1__ans1" size="16.5" ' .
+                'style="width: 13.6em" autocapitalize="none" spellcheck="false" ' .
+                'class="algebraic-right input-monospace" value="sin(x)" />',
+                $el->render($state, 'stack1__ans1', false, null));
+    }
+
+    public function test_validate_student_response_with_no_monospace_default_off() {
+        $options = new stack_options();
+        set_config('inputmonospace', '1,2', 'qtype_stack');
+        $el = stack_input_factory::make('algebraic', 'sans1', '1/2');
+        $el->set_parameter('options', 'align:right');
+        $state = $el->validate_student_response(['sans1' => 'sin(x)'], $options, '3.14', new stack_cas_security());
+        // In this case empty responses jump straight to score.
+        $this->assertEquals(stack_input::VALID, $state->status);
+        $this->assertEquals('sin(x)', $state->contentsmodified);
+        $this->assertEquals('\[ \sin \left( x \right) \]', $state->contentsdisplayed);
+        $this->assertEquals('', $state->errors);
+        $this->assertEquals('The answer <span class="filter_mathjaxloader_equation"><span class="nolink">' .
+                '\[ \[ \sin \left( x \right) \]</span></span> \), which can be typed as <code>sin(x)</code>,' .
+                ' would be correct.',
+                $el->get_teacher_answer_display($state->contentsmodified, $state->contentsdisplayed));
+        $this->assertEquals('<input type="text" name="stack1__ans1" id="stack1__ans1" size="16.5" ' .
+                'style="width: 13.6em" autocapitalize="none" spellcheck="false" ' .
+                'class="algebraic-right" value="sin(x)" />',
+                $el->render($state, 'stack1__ans1', false, null));
+    }
+
+    public function test_validate_student_response_with_monospace_true_default_off() {
+        $options = new stack_options();
+        set_config('inputmonospace', '1,2', 'qtype_stack');
+        $el = stack_input_factory::make('algebraic', 'sans1', '1/2');
+        $el->set_parameter('options', 'monospace:true, align:right');
+        $state = $el->validate_student_response(['sans1' => 'sin(x)'], $options, '3.14', new stack_cas_security());
+        // In this case empty responses jump straight to score.
+        $this->assertEquals(stack_input::VALID, $state->status);
+        $this->assertEquals('sin(x)', $state->contentsmodified);
+        $this->assertEquals('\[ \sin \left( x \right) \]', $state->contentsdisplayed);
+        $this->assertEquals('', $state->errors);
+        $this->assertEquals('The answer <span class="filter_mathjaxloader_equation"><span class="nolink">' .
+                '\[ \[ \sin \left( x \right) \]</span></span> \), which can be typed as <code>sin(x)</code>,' .
+                ' would be correct.',
+                $el->get_teacher_answer_display($state->contentsmodified, $state->contentsdisplayed));
+        $this->assertEquals('<input type="text" name="stack1__ans1" id="stack1__ans1" size="16.5" ' .
+                'style="width: 13.6em" autocapitalize="none" spellcheck="false" ' .
+                'class="algebraic-right input-monospace" value="sin(x)" />',
+                $el->render($state, 'stack1__ans1', false, null));
+    }
+
+    public function test_validate_student_response_with_monospace_false_default_on() {
+        $options = new stack_options();
+        set_config('inputmonospace', '0,1,2', 'qtype_stack');
+        $el = stack_input_factory::make('algebraic', 'sans1', '1/2');
+        $el->set_parameter('options', 'align:right, monospace:false');
+        $state = $el->validate_student_response(['sans1' => 'sin(x)'], $options, '3.14', new stack_cas_security());
+        // In this case empty responses jump straight to score.
+        $this->assertEquals(stack_input::VALID, $state->status);
+        $this->assertEquals('sin(x)', $state->contentsmodified);
+        $this->assertEquals('\[ \sin \left( x \right) \]', $state->contentsdisplayed);
+        $this->assertEquals('', $state->errors);
+        $this->assertEquals('The answer <span class="filter_mathjaxloader_equation"><span class="nolink">' .
+                '\[ \[ \sin \left( x \right) \]</span></span> \), which can be typed as <code>sin(x)</code>,' .
+                ' would be correct.',
+                $el->get_teacher_answer_display($state->contentsmodified, $state->contentsdisplayed));
+        $this->assertEquals('<input type="text" name="stack1__ans1" id="stack1__ans1" size="16.5" ' .
+                'style="width: 13.6em" autocapitalize="none" spellcheck="false" ' .
+                'class="algebraic-right" value="sin(x)" />',
+                $el->render($state, 'stack1__ans1', false, null));
+    }
+
+    public function test_validate_student_response_with_monospace_false_default_off() {
+        $options = new stack_options();
+        $el = stack_input_factory::make('algebraic', 'sans1', '1/2');
+        $el->set_parameter('options', 'align:right, monospace:false');
+        $state = $el->validate_student_response(['sans1' => 'sin(x)'], $options, '3.14', new stack_cas_security());
+        // In this case empty responses jump straight to score.
+        $this->assertEquals(stack_input::VALID, $state->status);
+        $this->assertEquals('sin(x)', $state->contentsmodified);
+        $this->assertEquals('\[ \sin \left( x \right) \]', $state->contentsdisplayed);
+        $this->assertEquals('', $state->errors);
+        $this->assertEquals('The answer <span class="filter_mathjaxloader_equation"><span class="nolink">' .
+                '\[ \[ \sin \left( x \right) \]</span></span> \), which can be typed as <code>sin(x)</code>,' .
+                ' would be correct.',
+                $el->get_teacher_answer_display($state->contentsmodified, $state->contentsdisplayed));
+        $this->assertEquals('<input type="text" name="stack1__ans1" id="stack1__ans1" size="16.5" ' .
+                'style="width: 13.6em" autocapitalize="none" spellcheck="false" ' .
+                'class="algebraic-right" value="sin(x)" />',
+                $el->render($state, 'stack1__ans1', false, null));
+    }
+
+    public function test_validate_student_response_with_monospace_default_on() {
+        $options = new stack_options();
+        set_config('inputmonospace', '0', 'qtype_stack');
+        $el = stack_input_factory::make('algebraic', 'sans1', '1/2');
+        $el->set_parameter('options', 'monospace:true, align:right');
+        $state = $el->validate_student_response(['sans1' => 'sin(x)'], $options, '3.14', new stack_cas_security());
+        // In this case empty responses jump straight to score.
+        $this->assertEquals(stack_input::VALID, $state->status);
+        $this->assertEquals('sin(x)', $state->contentsmodified);
+        $this->assertEquals('\[ \sin \left( x \right) \]', $state->contentsdisplayed);
+        $this->assertEquals('', $state->errors);
+        $this->assertEquals('The answer <span class="filter_mathjaxloader_equation"><span class="nolink">' .
+                '\[ \[ \sin \left( x \right) \]</span></span> \), which can be typed as <code>sin(x)</code>,' .
+                ' would be correct.',
+                $el->get_teacher_answer_display($state->contentsmodified, $state->contentsdisplayed));
+        $this->assertEquals('<input type="text" name="stack1__ans1" id="stack1__ans1" size="16.5" ' .
+                'style="width: 13.6em" autocapitalize="none" spellcheck="false" ' .
+                'class="algebraic-right input-monospace" value="sin(x)" />',
+                $el->render($state, 'stack1__ans1', false, null));
+    }
+
     public function test_validate_student_response_noununits() {
         $options = new stack_options();
         $el = stack_input_factory::make('algebraic', 'sans1', '9.81*m/s');
@@ -1826,7 +1992,7 @@ class input_algebraic_test extends qtype_stack_testcase {
         $el = stack_input_factory::make('algebraic', 'state', '3.14000E-10', $options);
         $el->set_parameter('forbidFloats', false);
 
-        $state = $el->validate_student_response(array('state' => '3.14000E-10'), $options, '3.14000E-10',
+        $state = $el->validate_student_response(['state' => '3.14000E-10'], $options, '3.14000E-10',
             new stack_cas_security());
         $this->assertEquals(stack_input::VALID, $state->status);
         $this->assertEquals('3.14000E-10', $state->contentsmodified);
@@ -1841,7 +2007,7 @@ class input_algebraic_test extends qtype_stack_testcase {
         $el = stack_input_factory::make('algebraic', 'state', '3.14000E-10', $options);
         $el->set_parameter('forbidFloats', false);
 
-        $state = $el->validate_student_response(array('state' => '3,14000E-10'), $options, '3.14000E-10',
+        $state = $el->validate_student_response(['state' => '3,14000E-10'], $options, '3.14000E-10',
             new stack_cas_security());
         $this->assertEquals(stack_input::VALID, $state->status);
         $this->assertEquals('3.14000E-10', $state->contentsmodified);
diff --git a/tests/input_numerical_test.php b/tests/input_numerical_test.php
index 20ca2bb1645d818e3f664caf0c688c1d0a26492f..eac617261b669fdcce05f232a886c27ea6ca6a49 100644
--- a/tests/input_numerical_test.php
+++ b/tests/input_numerical_test.php
@@ -651,6 +651,25 @@ class input_numerical_test extends qtype_stack_testcase {
                         'stack1__sans1', false, null));
     }
 
+    public function test_render_monospace_with_align() {
+        $el = stack_input_factory::make('numerical', 'ans1', '2');
+        $el->set_parameter('options', 'align:right, monospace:true');
+        $this->assertEquals('<input type="text" name="stack1__ans1" id="stack1__ans1" size="16.5" '
+                .'style="width: 13.6em" autocapitalize="none" spellcheck="false" ' .
+                'class="numerical-right input-monospace" value="" />',
+                $el->render(new stack_input_state(stack_input::VALID, [], '', '', '', '', ''),
+                        'stack1__ans1', false, null));
+    }
+
+    public function test_render_no_monospace_default_on() {
+        set_config('inputmonospace', '1', 'qtype_stack');
+        $el = stack_input_factory::make('numerical', 'ans1', '2');
+        $this->assertEquals('<input type="text" name="stack1__ans1" id="stack1__ans1" size="16.5" '
+                .'style="width: 13.6em" autocapitalize="none" spellcheck="false" class="numerical input-monospace" value="" />',
+                $el->render(new stack_input_state(stack_input::VALID, [], '', '', '', '', ''),
+                        'stack1__ans1', false, null));
+    }
+
     public function test_validate_student_letters_only() {
         $options = new stack_options();
         $el = stack_input_factory::make('numerical', 'sans1', '3.14159');
diff --git a/tests/input_units_test.php b/tests/input_units_test.php
index 23b056ae2cfc9c3bc8a77c118fa86badb50e85cb..ea4627db4e02f97e5c03e5da3214a9cc5ee21460 100644
--- a/tests/input_units_test.php
+++ b/tests/input_units_test.php
@@ -76,6 +76,28 @@ class input_units_test extends qtype_stack_testcase {
                         'stack1__input', true, null));
     }
 
+    public function test_render_monospace_with_align() {
+        $el = stack_input_factory::make('units', 'input', '9.81*m/s^2');
+        $el->set_parameter('options', 'align:right, monospace:true');
+        $this->assertEquals(
+                '<input type="text" name="stack1__input" id="stack1__input" size="16.5" style="width: 13.6em" '
+                .'autocapitalize="none" spellcheck="false" class="algebraic-units-right input-monospace" value="9.81*m/s^2" '
+                .'readonly="readonly" />',
+                $el->render(new stack_input_state(stack_input::VALID, ['9.81*m/s^2'], '', '', '', '', ''),
+                        'stack1__input', true, null));
+    }
+
+    public function test_render_no_monospace_default_on() {
+        set_config('inputmonospace', '2', 'qtype_stack');
+        $el = stack_input_factory::make('units', 'input', '9.81*m/s^2');
+        $this->assertEquals(
+                '<input type="text" name="stack1__input" id="stack1__input" size="16.5" style="width: 13.6em" '
+                .'autocapitalize="none" spellcheck="false" class="algebraic-units input-monospace" value="9.81*m/s^2" '
+                .'readonly="readonly" />',
+                $el->render(new stack_input_state(stack_input::VALID, ['9.81*m/s^2'], '', '', '', '', ''),
+                        'stack1__input', true, null));
+    }
+
     public function test_render_different_size() {
         $el = stack_input_factory::make('units', 'input', '-9.81*m/s^2');
         $el->set_parameter('boxWidth', 30);
diff --git a/tests/input_varmatrix_test.php b/tests/input_varmatrix_test.php
index da408523da292b6d5c5b4d5c4bf577dc67798604..d8b3b16fe473d181700b933f9c6431e5c6ef5afa 100644
--- a/tests/input_varmatrix_test.php
+++ b/tests/input_varmatrix_test.php
@@ -92,6 +92,26 @@ class input_varmatrix_test extends qtype_stack_testcase {
                 'ans1', false, null));
     }
 
+    public function test_render_monospace() {
+        $el = stack_input_factory::make('varmatrix', 'ans1', 'M');
+        $el->set_parameter('options', 'monospace:true');
+        $this->assertEquals('<div class="matrixsquarebrackets"><textarea name="ans1" id="ans1" autocapitalize="none" ' .
+                'spellcheck="false" class="varmatrixinput input-monospace" size="5.5" style="width: 4.6em" rows="5" cols="5">' .
+                '</textarea></div>',
+                $el->render(new stack_input_state(stack_input::BLANK, [], '', '', '', '', ''),
+                        'ans1', false, null));
+    }
+
+    public function test_render_no_monospace_default_on() {
+        set_config('inputmonospace', '3', 'qtype_stack');
+        $el = stack_input_factory::make('varmatrix', 'ans1', 'M');
+        $this->assertEquals('<div class="matrixsquarebrackets"><textarea name="ans1" id="ans1" autocapitalize="none" ' .
+                'spellcheck="false" class="varmatrixinput input-monospace" size="5.5" style="width: 4.6em" rows="5" cols="5">' .
+                '</textarea></div>',
+                $el->render(new stack_input_state(stack_input::BLANK, [], '', '', '', '', ''),
+                        'ans1', false, null));
+    }
+
     public function test_validate_student_response_na() {
         $options = new stack_options();
         $el = stack_input_factory::make('varmatrix', 'ans1', 'M');
diff --git a/tests/maxima_corrective_parser_test.php b/tests/maxima_corrective_parser_test.php
index b55eb253ccab122ce642271cf16cc075091ca1ed..6d6d4de6496fbf9118b34394366e267c849ee5b5 100644
--- a/tests/maxima_corrective_parser_test.php
+++ b/tests/maxima_corrective_parser_test.php
@@ -39,7 +39,10 @@ require_once(__DIR__ . '/fixtures/maximacorrectiveparser.class.php');
 class maxima_corrective_parser_test extends qtype_stack_testcase {
 
     /**
+     * @codingStandardsIgnoreStart
+     * Provider in another class/file throws false code check error.
      * @dataProvider maxima_corrective_parser_test_data::get_raw_test_data
+     * @codingStandardsIgnoreEnd
      */
     public function test_maxima_corrective_parser() {
 
diff --git a/tests/restore_logic_test.php b/tests/restore_logic_test.php
index fd9a454bb6ab3e74745f092536e1e08ce7ebd599..3420a213170389ef181da7b82d66401d48ca145f 100644
--- a/tests/restore_logic_test.php
+++ b/tests/restore_logic_test.php
@@ -28,7 +28,7 @@ require_once($CFG->dirroot . '/question/type/stack/backup/moodle2/restore_qtype_
  * @copyright  2017 The Open University
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class restore_logic_test extends \restore_qtype_stack_plugin {
+class restore_logic_test_class extends \restore_qtype_stack_plugin {
     private $log = '';
 
     public function __construct() {
@@ -77,7 +77,7 @@ class restore_logic_test extends \restore_qtype_stack_plugin {
  * @group qtype_stack
  * @covers \qtype_stack
  */
-class qtype_stack_restore_logic_testcase extends \advanced_testcase {
+class restore_logic_test extends \advanced_testcase {
 
     public function test_fix_prt_roots() {
         global $DB;
@@ -95,7 +95,7 @@ class qtype_stack_restore_logic_testcase extends \advanced_testcase {
         $DB->set_field('qtype_stack_prt_nodes', 'truenextnode', 7,
                 ['questionid' => $question->id, 'prtname' => 'oddeven', 'nodename' => 0]);
 
-        $restoreplugin = new \testable_restore_qtype_stack_plugin();
+        $restoreplugin = new restore_logic_test_class();
         $restoreplugin->after_execute_question();
 
         $this->assertStringContainsString('The PRT named "oddeven" is malformed', $restoreplugin->get_log());
diff --git a/tests/stack_utils_test.php b/tests/stack_utils_test.php
index 66fdb52971a16c9ceb00eee79265baa10e419ba7..ccdae0bb1d069a73b0c0d3bdd18f71498efb7d56 100644
--- a/tests/stack_utils_test.php
+++ b/tests/stack_utils_test.php
@@ -217,7 +217,7 @@ class stack_utils_test extends qtype_stack_testcase {
      *
      * @return array of test cases.
      */
-    public function count_missing_alttext_cases(): array {
+    public static function count_missing_alttext_cases(): array {
         return [
             [0, 'random <img alt="Hello world!" src="https://nowhere.com/images/image0.png" > stuff'],
             [0, 'random <IMG alt="Hello world!" src="https://nowhere.com/images/image0.png" > stuff'],
diff --git a/tests/studentinput_test.php b/tests/studentinput_test.php
index 8175644d6e45cfe0099bcd5908fe8898c818bd3e..4590e4328ef17a76122d5c6a74a5b3e7ee7424b2 100644
--- a/tests/studentinput_test.php
+++ b/tests/studentinput_test.php
@@ -40,7 +40,10 @@ require_once(__DIR__ . '/fixtures/inputfixtures.class.php');
 class studentinput_test extends qtype_stack_testcase {
 
     /**
+     * @codingStandardsIgnoreStart
+     * Provider in another class/file throws false code check error.
      * @dataProvider stack_inputvalidation_test_data::get_raw_test_data
+     * @codingStandardsIgnoreEnd
      */
     public function test_studentinput() {
         $test = stack_inputvalidation_test_data::test_from_raw(func_get_args(), 'typeless');
@@ -52,7 +55,10 @@ class studentinput_test extends qtype_stack_testcase {
     }
 
     /**
+     * @codingStandardsIgnoreStart
+     * Provider in another class/file throws false code check error.
      * @dataProvider stack_inputvalidation_test_data::get_raw_test_data_units
+     * @codingStandardsIgnoreEnd
      */
     public function test_studentinput_units() {
         $test = stack_inputvalidation_test_data::test_from_raw(func_get_args(), 'units');
@@ -64,7 +70,10 @@ class studentinput_test extends qtype_stack_testcase {
     }
 
     /**
+     * @codingStandardsIgnoreStart
+     * Provider in another class/file throws false code check error.
      * @dataProvider stack_inputvalidation_test_data::get_raw_test_data_decimals
+     * @codingStandardsIgnoreEnd
      */
     public function test_studentinput_decimals_british() {
         $test = stack_inputvalidation_test_data::test_decimals_from_raw(func_get_args(), 1);
@@ -76,7 +85,10 @@ class studentinput_test extends qtype_stack_testcase {
     }
 
     /**
+     * @codingStandardsIgnoreStart
+     * Provider in another class/file throws false code check error.
      * @dataProvider stack_inputvalidation_test_data::get_raw_test_data_decimals
+     * @codingStandardsIgnoreEnd
      */
     public function test_studentinput_decimals_continental() {
         $test = stack_inputvalidation_test_data::test_decimals_from_raw(func_get_args(), 2);
diff --git a/tests/subscript_test.php b/tests/subscript_test.php
index 039174d70b6483495c5462e9a28690fd4d7230ae..7a0752c3823c23b111a2e10485cbc609ab2dfbbd 100644
--- a/tests/subscript_test.php
+++ b/tests/subscript_test.php
@@ -49,7 +49,10 @@ require_once(__DIR__ . '/../stack/cas/ast.container.class.php');
 class subscript_test extends qtype_stack_testcase {
 
     /**
+     * @codingStandardsIgnoreStart
+     * Provider in another class/file throws false code check error.
      * @dataProvider stack_subscripts_test_data::get_raw_test_data
+     * @codingStandardsIgnoreEnd
      */
     public function test_subscripts() {
         $this->skip_if_old_maxima('5.40.0');
@@ -84,7 +87,10 @@ class subscript_test extends qtype_stack_testcase {
     }
 
     /**
+     * @codingStandardsIgnoreStart
+     * Provider in another class/file throws false code check error.
      * @dataProvider stack_subscripts_test_data::get_raw_test_data_legacy
+     * @codingStandardsIgnoreEnd
      */
     public function test_subscripts_legacy_maxima() {
         $this->skip_if_new_maxima('5.40.0');
diff --git a/tests/walkthrough_adaptive_test.php b/tests/walkthrough_adaptive_test.php
index a28b301d6a8455ec69e886f2c38e004ed0fc133a..57a9158886b734c3bd0d65a331dde2c9546c1a6c 100644
--- a/tests/walkthrough_adaptive_test.php
+++ b/tests/walkthrough_adaptive_test.php
@@ -4292,8 +4292,9 @@ class walkthrough_adaptive_test extends qtype_stack_walkthrough_test_base {
     }
 
     public function test_input_validator() {
-
+        global $USER;
         $this->resetAfterTest();
+        $USER->lang = '';
         set_config('lang', 'en');
 
         $q = test_question_maker::make_question('stack', 'validator');
@@ -4374,8 +4375,10 @@ class walkthrough_adaptive_test extends qtype_stack_walkthrough_test_base {
     }
 
     public function test_input_validator_jp() {
+        global $USER;
         // This language is not in the question, so should default back to English.
         $this->resetAfterTest();
+        $USER->lang = '';
         set_config('lang', 'jp');
 
         $q = test_question_maker::make_question('stack', 'validator');
@@ -4456,8 +4459,10 @@ class walkthrough_adaptive_test extends qtype_stack_walkthrough_test_base {
     }
 
     public function test_input_validator_fi() {
+        global $USER;
         // This language is in the question.
         $this->resetAfterTest();
+        $USER->lang = '';
         set_config('lang', 'fi');
 
         $q = test_question_maker::make_question('stack', 'validator');
@@ -4674,8 +4679,9 @@ class walkthrough_adaptive_test extends qtype_stack_walkthrough_test_base {
     }
 
     public function test_input_validator_texput() {
-
+        global $USER;
         $this->resetAfterTest();
+        $USER->lang = '';
         set_config('lang', 'en');
 
         $q = test_question_maker::make_question('stack', 'validator');
diff --git a/version.php b/version.php
index 2cdfb02f90272797bfbb83a639c9fce9198cc629..88419d4d5c06949ef1c5c97ee46ec91e9668f962 100644
--- a/version.php
+++ b/version.php
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2024043000;
+$plugin->version   = 2024050600;
 $plugin->requires  = 2022041900;
 $plugin->cron      = 0;
 $plugin->component = 'qtype_stack';