Chapter 26

Online Testing


CONTENTS

If your site is designed to teach-and more and more sites are appearing that are, you may want to offer online testing for whatever material you present, such as home schooling, and so on.

Document Layout

To keep the JavaScript code and database in the browser from page to page, it's necessary to design the testing engine with frames, placing your JavaScript code in the parent frame, and the control code in one of the child frames.

Persistent JavaScript Across Documents
Even if you don't plan on using frames, you need to if you're designing a pageset that requires JavaScript variables to keep their values as you move from page to page. Netscape Navigator 3.0 handles the write() method of the document object a little differently than its predecessor (or Internet Explorer, for that matter). When using document.write() to send the entire contents of a document (from <HTML> to </HTML>) to a window, all of the previous window content is destroyed. This includes any JavaScript code that the document being replaced may have contained. In other words, If you had a document with JavaScript code that wrote out a new document into the same window, the code would delete itself before the new document data was processed. If the code were to call other JavaScript functions within the document, and make these calls after an initial document.write(), the browser would present you with an alert box saying that the function in question isn't defined-Navigator having deleted it from memory.
There are two ways around this anomaly:
  1. Have the document.write() statements write out the JavaScript code, as well as the regular HTML.
  2. Use frames, keeping the code persistent in a frame that never gets overwritten.

Listing 26.1 shows the frame layout used here, which looks like what's displayed in figure 26.1, consisting of the following:

Figure 26.1 : The top and middle frames are actually dynamically generated html documents. All the work is done by the bottom frame.


Listing 26.1  Basic Quiz Frame Structure
<FRAMESET ROWS="42,*,38">
   <FRAME NAME="banner" SRC="title.htm" NORESIZE SCROLLING=NO
          MARGINHEIGHT=0>
   <FRAME NAME="body" SRC="cover.htm">
   <FRAME NAME="nav" SRC="nav1.htm" NORESIZE SCROLLING=NO
          MARGINHEIGHT=2>
</FRAMESET>

As you learned in chapter 17, "Creating Online Catalogs," even though you have two frames that will display dynamic HTML documents, you still need to initialize them within the <FRAMESET> tag with "dummy" or placeholder files. If you don't, the frame is created, but not completed (from a JavaScript standpoint), meaning you can't access it to change its contents. This isn't really a problem, as it gives you a good reason to create a home page structure for your quiz, welcoming the user to the test.

The Question Base

Chapter 15, "Managing a Database," introduced the basis of database manipulation with JavaScript, and those same principles apply here. Listing 26.2 shows the database format chosen, which contains the following:


Listing 26.2  Question Database
Questions    = new initArray(3);
Questions[1] = new Question("What year was Windows 95 released?", 
                            "1994", "1985", "2095", "1995", "4");
Questions[2] = new Question("What was the first computer?", 
                            "Altair", "IBM-PC", "Macintosh", "TRS-80", "1");
Questions[3] = new Question("Which of the following is <B>not</B> a browser:", 
                            "Internet Explorer", 
                            "Navigator", 
                            "WebThing", 
                            "Pioneer", "3");

function initArray(iSize) {
   this.length = iSize;

   for(var i=1; i<=iSize; i++)
      this[i] = null;

   return this;
}

function Question(strQ, strA, strB, strC, strD, strCorrect) {
   this.question     = strQ;
   this.ans1         = strA;
   this.ans2         = strB;
   this.ans3         = strC;
   this.ans4         = strD;
   this.correct      = strCorrect;

   return this;
}

NOTE
You may have spotted the third question, which has HTML embedded within its body (the <B> tag bolds the word not). Because the question is treated as a string that is processed just like any other HTML text, you can get as fancy as you want and embed font color and size changes, italicizing, and even <IMG> tag references.

Notice that the correct property of the Question object is numeric, even though the buttons displayed to the user show A, B, C,…. You can define the correct answer either way, by letter or number, just as long as you remember to use the same representation throughout the rest of your code, as will be the case in the next section.

Displaying a Question

Displaying a given question involves nothing more than standard HTML formatting, as shown by listing 26.3.


Listing 26.3  Displaying a Question
function DrawQuestion(iWhich) {
   var bodyDoc = window.body.document;
   var bannDoc = window.banner.document;

   bannDoc.write("<HTML>" +
                 "<HEAD><TITLE>Question " +
                 iWhich + "</TITLE></HEAD>" +
                 "<BODY BGCOLOR=olive>" +
                 "<CENTER>" +
                 "<H1><FONT COLOR=yellow>" +
                 "Question #" + iWhich + 
                 "</FONT></H1>" +
                 "</CENTER></BODY></HTML>");
   bannDoc.close();

   bodyDoc.write("<HTML>" +
                 "<HEAD><TITLE>Question " + 
                 iWhich + "</TITLE></HEAD>" +
                 "<BODY BGCOLOR=#ffffff>");

   bodyDoc.write("<P>");

   bodyDoc.write("<I><FONT SIZE=+2>" + 
                 Questions[iWhich].question + 
                 "</FONT></I>");

   bodyDoc.write("<OL TYPE=A>");

   for(var i=1; i<=4; i++) {
      var tStr = eval("Questions[iWhich].ans" + i);

      bodyDoc.write("<LI>" + tStr + "</LI>");
   }

   bodyDoc.write("</OL></BODY></HTML>");

   bodyDoc.close();

   if(window.nav.location.href.toString().indexOf("nav2.htm") == -1) {
      window.nav.location.href = "nav2.htm";
   }

   iQuestion = iWhich;
}

NOTE
Remember, the ordered list tag, <OL>, can be configured to display its list elements in a variety of ways-numeric, alphabetic, as roman numerals, even controlling upper- or lowercase through the STYLE attribute. In this example, STYLE is set to display the list as uppercase alphabetic selections, corresponding with the names of the multiple-choice buttons.


TIP
Most of the time, JavaScript is capable of identifying what major object elements you are using based on context, without having to explicitly define the entire object heirarchy. For example, under most conditions:
var bodyDoc = window.body.document;
could be abbreviated to:
var bodyDoc = body.document;
However, as of JavaScript 1.1, the version running under Navigator 3.0, when you're trying to reference a frame element, as in this example, omitting the window. object reference causes JavaScript to try to associate body as a property of document, not window. Adding the window reference helps JavaScript identify the appropriate object structure, and everything works correctly.
When scripting in JavaScript, if you encounter a xxx is not an object or xxx not defined message, check to see if you're making shortcut assumptions and not specifying the complete object path (from window, or whatever the base object may be). In many cases, including the complete path corrects the problem.

The DrawQuestion() function performs the following different steps:

  1. The banner frame is updated to reflect the new question number.
  2. The question itself is formatted, with the font size increased slightly from the default for clarity. It also helps to fill up the screen when the question is short and the user is surfing at a high resolution.
  3. The multiple-choice answers are formatted by stepping through the list of possible solutions.
  4. Finally, the document currently loaded in the nav frame is looked at and the right navigation bar is loaded, if necessary.

This last step bears further discussion. When the user first enters the question area, he or she is presented with the Welcome screen. At this point, the nav frame contains one button: Begin. Once the user has entered the quiz, however, the nav frame needs to contain the multiple-choice selection buttons, so a new page needs to be loaded, but only with the first question. Reloading the page doesn't do any harm, but because the page itself doesn't change between questions, reloading it is unnecessary.

To prevent unnecessary reloads, the line:

if(window.nav.location.href.toString().indexOf("nav2.htm") == -1) {

checks the href property of the location object for nav2.htm, the name of the correct document file. If another file is loaded within the nav frame, then a reload is performed to load the correct document.

Another thing worth noting is how the multiple-choice answers are obtained, which also explains why the correct property stores the correct answer as a number not a letter, and why the Answer() function expects the same numeric parameter. The for loop:

for(var i=1; i<=4; i++) {
   var tStr = eval("Questions[iWhich].ans" + i);
   bodyDoc.write("<LI>" + tStr + "</LI>");
}

constructs a JavaScript string like:

Questions[iWhich].ansX

where X is replaced with a number between 1 and 4, matching the four answer properties of the Question object. Then, the eval() function is used to force JavaScript to evaluate the string like a regular statement-the end result being the value stored in the associated property.

Handling the Answer

When the user clicks one of the answer buttons, which are formatted by the HTML source in listing 26.4, the Answer() function is fired.


Listing 26.4  The Answer Buttons
<CENTER><FORM><TABLE BORDER=3 CELLPADDING=0 CELLSPACING=0>
   <TR>
      <TD>
         <INPUT TYPE=BUTTON VALUE="A"
                onClick="parent.Answer(1);">
      </TD><TD>
         <INPUT TYPE=BUTTON VALUE="B"
                onClick="parent.Answer(2);">
      </TD><TD>
         <INPUT TYPE=BUTTON VALUE="C"
                onClick="parent.Answer(3);">
      </TD><TD>
         <INPUT TYPE=BUTTON VALUE="D"
                onClick="parent.Answer(4);">
      </TD>
   </TR>
</TABLE></FORM></CENTER>

The job of Answer() (see listing 26.5) is to do the following:

  1. Determine whether the user's answer is correct.
  2. Display an appropriate response (Good Job! or Not quite, …)
  3. If the user was correct, increment a counter to keep track of right answers.
  4. Instruct the question engine to display the next question, if there is a next question; otherwise, display the user's final score.

Listing 26.5  Processing a User's Answer
var   iQuestion   = 0;
var   iCorrect    = 0;

...

function Answer(strWhich) {
   var strAlpha = "ABCD";
   var iRight   = Questions[iQuestion].correct;

   if(strWhich == iRight) {
      alert("Correct!");
      iCorrect++;
   } else {
      alert("I'm sorry.\n" +
            "The correct answer was '" +
            strAlpha.substring(iRight-1,iRight) + "'.");
   }

   if(iQuestion == Questions.length) {
      FinalScore();
   } else {
      DrawQuestion(iQuestion+1);
   }
}

When the user gets a question wrong, he or she is informed what the correct answer was before moving on to the next question. Because the correct answer is stored numerically and the answers are listed alphabetically, a means of converting from a 3 to a C, for example, is needed. The method used here is to utilize a temporary string containing the correct answer letters:

var strAlpha = "ABCD";

The value of the correct answer is then retrieved from the Questions[] array (where it's stored as a number):

var iRight   = Questions[iQuestion].correct;

and used as an index to extract the correct character from strAlpha:

strAlpha.substring(iRight-1,iRight));

The end result is an alert() dialog box similar to the one shown in figure 26.2.

Figure 26.2 : The substring trick used here converts the numeric version of the correct answer to its alphabetic equivalent.

Adding It All Up

After the user has finished the quiz, it's time to tally the score, inform the user, and optionally, give him or her the opportunity to submit his or her final score to the server for processing. This is the responsibility of the FinalScore() function.

FinalScore() (see listing 26.6) replaces the documents in all three frames:

  1. The banner frame becomes an All done message.
  2. The body frame displays the user's score, and provides a means for the user to enter her name and submit her score.
  3. The nav frame becomes a flat-color window with no buttons.

Listing 26.6  Displaying the Final Score
function FinalScore() {
   var bannDoc = window.banner.document;
   bannDoc.write("<HTML>" +
                 "<HEAD><TITLE>All Done!" +
                 "</TITLE></HEAD>" +
                 "<BODY BGCOLOR=olive>" +
                 "<CENTER>" +
                 "<H1><FONT COLOR=yellow>" +
                 "All Done!</FONT></H1>" +
                 "</CENTER></BODY></HTML>");
   bannDoc.close();

   var navDoc = window.nav.document;
   navDoc.write("<HTML>" +
                "<HEAD><TITLE></TITLE></HEAD>" +
                "<BODY BGCOLOR=olive>" +
                "</BODY></HTML>");
   navDoc.close();

   window.body.location.href = "final.htm";
}

The middle frame document, which displays the score information, is the only document that's actually loaded from the server. The top and bottom frames are dynamically created.

NOTE
For security reasons, Netscape has restricted the amount of manipulation you can perform on form data within JavaScript through the onSubmit event. This is done in an attempt to keep the unscrupulous from creating silent submission forms that attempt to retrieve your e-mail address without your knowledge.

The middle frame pulls data from the parent document to both display and transmit to the server. Listing 26.7 shows the code that drives this document.


Listing 26.7  Submitting the Data
<HTML>
<HEAD>
</HEAD>
<BODY BGCOLOR=white>

<CENTER>

<H2>Your Score:</H2>

<HR>

<SCRIPT LANGUAGE="JavaScript">
<!--
document.write("Total problems: " + parent.Questions.length);
document.write("<P>");
document.write("Correct problems: " + parent.iCorrect);
document.write("<HR>");
// -->
</SCRIPT>

If you wish to submit your score, please fill in the data
below and press <B>Submit</B>

<P>

<FORM METHOD=POST ACTION="mailto:sjwalter@visi.com">

<SCRIPT LANGUAGE="JavaScript">
<!--

document.write('<INPUT TYPE=HIDDEN NAME="total" ' +
               'VALUE="' + parent.Questions.length + 
               '">');

document.write('<INPUT TYPE=HIDDEN NAME="correct" ' +
               'VALUE="' + parent.iCorrect + '">');

// -->
</SCRIPT>

Your name: <INPUT TYPE=TEXT NAME="realname" VALUE="">

<P>

<TABLE BORDER=3 CELLPADDING=0 CELLSPACING=0>
   <TR><TD>
         <INPUT TYPE=SUBMIT VALUE="Submit">
   </TD></TR>
</TABLE>

</FORM>

</CENTER>
</BODY>
</HTML>

As you can see, JavaScript is used to load the data into the hidden fields during the construction of the form.

NOTE
While you could use the onLoad event, which is fired after a document is completely loaded and before the page is displayed to the user, it's not currently supported by Internet Explorer.

From Here…

With the growing popularity of home schooling, the ability to provide online testing is going to become more of a selling point for an educationally oriented Web site. The structure presented here is easily expanded to handle just about anything you care to teach.

For more information on related topics, check out: