Thursday, 30 June 2022

Is the written component overrated in professional communication?

Communication is one of the most important parts of the software developer profession. Or indeed, just about any profession. Instructions get passed along, feedback is received and along the chain of events that happen, information mutates. This information may affect what other operations take place, and this in turn has an ever-widening effect on things further down the chain.

So yes, communication is vital. But just speaking of communication alone would be a scope too large for a single blogpot or article. Thus, for today, I would like to speak on something I discovered about verbal and written communication, as a software developer.

Written or verbal?

Written communication is generally preferred because there is a "paper" trail that can be referred to if clarification is required. Verbal communication is considered transient and informal. No corporation, for instance, would consider a verbal exchange a legally-binding contract. No team lead would ever rely solely on verbal communication to convey instructions. The main mode of communication is almost invariably task specifications written in a Kanban card, or even an email.

With written communication, one of the advantages is that people generally are able to carefully consider their words before hitting the Send button (not that everybody does, regrettably). Therefore, the consensus seems to be that written communication is generally clearer.

However...

Is written communication truly clearer than verbal? There are some instances where this is not the case.

Recently there was an incident in the company where I worked. My colleague encountered a problem with the system I was managing. To her credit, she went by the book - sent me screenshots, even a video, and what she thought was a clear explanation of what she had expected as output, and what was wrong with the output she had received.

The problem was with the last part - I understood not a single thing she was saying in her email. Even discounting typos and grammatical errors, I could not get a sense of what she was trying to tell me. After a couple of frustrating exchanges through email, we communicated via a video call. Within minutes of her speaking, the lightbulb went off in my head. Now when I looked back at her emails and her written descriptions, I knew exactly what she was saying.

Hearing words
makes a difference.

Was what she had written in the emails, different from what she had verbally said? Now that I had a chance to do a side-by-side comparison, not so much. The difference was mainly in inflection and tone - both of which were missing in her written emails, and probably in most written media. However, in our local lingo, both inflection and tone convey a multitude of different meanings, and as such they play a huge part in the communication process. Listening to her speak was simply not the same as reading her writing. Probably because she tended to write the way she spoke - another cardinal sin of communication, but one we won't get into today.

In another example...

I had a similar experience on the Clubhouse app. I was in a room where this woman from Trinidad And Tobago in the continent of Africa, was reading from a novel set in that area. I had previously read a bit of the novel, but given up halfway because the dialogue, on the written page, was incomprehensible to me.

However, as I heard her say the words of the dialogue, which was in the Trini slang, using her Trini accent, suddenly it all made perfect sense.

I don't know exactly why that would be the case. I do know that if I were to read the dialogue again, it would be from a position where, having already heard it, I would now know what it meant.

The Communicative Conclusion

Verbal communication can be more powerful than people realize. With audio, one can convey some other dimension in the communication, pieces which may be missing in a written medium. One might argue that being more proficient at written English might solve the problem. It will mitigate the problem, certainly, but it will never replace what verbal communication brings to the table.

Let's have this in writing!
T___T

Friday, 24 June 2022

Web Tutorial: The Japanese Language Trainer App (Part 4/4)

The first test we will write for this app, is for App. So create App.test.js in the src directory.

We begin by importing these.

src/App.test.js
import React from "react";
import { render, screen } from '@testing-library/react';
import App from './App';


And the very first test, is for App to render. We test to see if that string appears in the screen. This, by itself, isn't a great test, but it will do for a start.

src/App.test.js
import React from "react";
import { render, screen } from '@testing-library/react';
import App from './App';

describe("App", () => {
    it('renders', () => {
        render(<App />);
        expect(screen.queryByText('Welcome to J-Trainer!')).toBeInTheDocument();
    });
});


Run npm test in the command line interface. This test should succeed.




Now we will test the Header component. Create Header.test.js in the Header directory of the components directory. We begin with the appropriate import statements.

src/components/Header/Header.test.js
import React from "react";
import { render, screen } from '@testing-library/react';
import userEvent from "@testing-library/user-event";
import Header from './Header';


We define the getters and setters, because these are passed to the components.

src/components/Header/Header.test.js
import React from "react";
import { render, screen } from '@testing-library/react';
import userEvent from "@testing-library/user-event";
import Header from './Header';

let maxRemaining;

let started;
let setStarted = (val)=> {
    started = val;
};

let charset;
let setCharset = (val)=> {
    charset = val;
};

let remaining;
let setRemaining = (val)=> {
    remaining = val;
};

let usedQuestions;
let setUsedQuestions = (val)=> {
    usedQuestions = val;
};

let answer;
let setAnswer = (val)=> {
    answer = val;
};

let result;
let setResult = (val)=> {
    result = val;
};

let question;
let setQuestion = (val)=> {
    question = val;
};


We then use the describe() function, and pass in "Header" and a callback as arguments.

src/components/Header/Header.test.js
let question;
let setQuestion = (val)=> {
    question = val;
};

describe("Header", () => {

});


The tests have an explanatory string passed in as an argument, and a callback. This makes it easy to see what the tests should test for.

src/components/Header/Header.test.js
describe("Header", () => {
    it("should render with BEGIN button if not started and no charset selected", () => {

    });
    
    it("should render with CANCEL button if started, charset selected, has questions remaining", () => {

    });

    it("should render with RESTART button if started, charset selected, has no questions remaining", () => {

    });

});


Time for the first test. We make sure that the operating conditions are as close to the actual operating conditions, by setting these variables. Note that started is now false and charset is null.

src/components/Header/Header.test.js
it("should render with BEGIN button if not started and no charset selected", () => {
    maxRemaining = 20;
    setCharset(null);
    setStarted(false);
    setUsedQuestions([]);
    setRemaining(maxRemaining);

});


We render the Header component with all the required attributes.

src/components/Header/Header.test.js
it("should render with BEGIN button if not started and no charset selected", () => {
    maxRemaining = 20;
    setCharset(null);
    setStarted(false);
    setUsedQuestions([]);
    setRemaining(maxRemaining);

    render(
    <Header
      charset={ charset }
      started={ started }
      remaining={ remaining }
      usedQuestions={ usedQuestions }
      maxRemaining={ maxRemaining }
      setStarted={ setStarted }
      setCharset={ setCharset }
      setRemaining={ setRemaining }
      setAnswer={ setAnswer }
      setResult={ setResult }
      setQuestion={ setQuestion }
      setUsedQuestions={ setUsedQuestions }
    />        
    );

});


And then we have two asserts. Use the expect() function. Ensure that the button's text is "BEGIN ➤" because started is false and it is disabled because charset is null.

src/components/Header/Header.test.js
it("should render with BEGIN button if not started and no charset selected", () => {
    maxRemaining = 20;
    setCharset(null);
    setStarted(false);
    setUsedQuestions([]);
    setRemaining(maxRemaining);

    render(
    <Header
      charset={ charset }
      started={ started }
      remaining={ remaining }
      usedQuestions={ usedQuestions }
      maxRemaining={ maxRemaining }
      setStarted={ setStarted }
      setCharset={ setCharset }
      setRemaining={ setRemaining }
      setAnswer={ setAnswer }
      setResult={ setResult }
      setQuestion={ setQuestion }
      setUsedQuestions={ setUsedQuestions }
    />        
    );

    expect(screen.getByTestId("BtnStartEnd").textContent).toBe("BEGIN ➤");
    expect(screen.getByTestId("BtnStartEnd")).toBeDisabled();

});


We will also need to add this to the button.

src/components/Header.js

if (started) {
    return (
        <>
            <div className="Title">
                <h1>J-Trainer</h1>
            </div>
            <div className="Controls">
                <div>
                    <br />
                    <button data-testid="BtnStartEnd" onClick={ ()=>{BtnEnd_click();}}>
                        { remaining === 0 ? "RESTART ➤ " : "CANCEL ☓" }
                    </button>
                </div>
            </div>
        </>            
    );
} else {
    return (
        <>
            <div className="Title">
                <h1>J-Trainer</h1>
            </div>
            <div className="Controls">
                <div>
                <input type="radio" name="charset" onClick={()=>{SetCurrentCharset('hiragana');}}  /> Hiragana <br />
                <input type="radio" name="charset" onClick={()=>{SetCurrentCharset('katakana');}} /> Katakana <br />
                <br />
                    <button data-testid="BtnStartEnd" onClick={ ()=>{BtnStart_click();}} disabled={ charset === null ? true : false }>
                        BEGIN ➤
                    </button>
                </div>
            </div>
        </>
    );          
}  


Now for the next test! Again, pass what we need to. Just make sure that remaining is more than 0 and charset is not null (even an empty array counts), and started is true.

src/components/Header/Header.test.js
it("should render with CANCEL button if started, charset selected, has questions remaining", () => {
    maxRemaining = 20;
    setCharset([]);
    setStarted(true);
    setUsedQuestions([]);
    setRemaining(maxRemaining);

});


Next, render the Header component with the appropriate attributes.

src/components/Header/Header.test.js
it("should render with CANCEL button if started, charset selected, has questions remaining", () => {
    maxRemaining = 20;
    setCharset([]);
    setStarted(true);
    setUsedQuestions([]);
    setRemaining(maxRemaining);

    render(
    <Header
      charset={ charset }
      started={ started }
      remaining={ remaining }
      usedQuestions={ usedQuestions }
      maxRemaining={ maxRemaining }
      setStarted={ setStarted }
      setCharset={ setCharset }
      setRemaining={ setRemaining }
      setAnswer={ setAnswer }
      setResult={ setResult }
      setQuestion={ setQuestion }
      setUsedQuestions={ setUsedQuestions }
    />        
    );

});


Then assert that we will expect "CANCEL ☓" to be the button's text.

src/components/Header/Header.test.js
it("should render with CANCEL button if started, charset selected, has questions remaining", () => {
    maxRemaining = 20;
    setCharset([]);
    setStarted(true);
    setUsedQuestions([]);
    setRemaining(maxRemaining);

    render(
    <Header
      charset={ charset }
      started={ started }
      remaining={ remaining }
      usedQuestions={ usedQuestions }
      maxRemaining={ maxRemaining }
      setStarted={ setStarted }
      setCharset={ setCharset }
      setRemaining={ setRemaining }
      setAnswer={ setAnswer }
      setResult={ setResult }
      setQuestion={ setQuestion }
      setUsedQuestions={ setUsedQuestions }
    />        
    );

    expect(screen.getByTestId("BtnStartEnd").textContent).toBe("CANCEL ☓");
});


I feel like I shouldn't have to explain this last test. It's basically a repeat of the last two, but with a different assert.

src/components/Header/Header.test.js
it("should render with RESTART button if started, charset selected, has no questions remaining", () => {
    maxRemaining = 20;
    setCharset([]);
    setStarted(true);
    setUsedQuestions([]);
    setRemaining(0);

    render(
    <Header
      charset={ charset }
      started={ started }
      remaining={ remaining }
      usedQuestions={ usedQuestions }
      maxRemaining={ maxRemaining }
      setStarted={ setStarted }
      setCharset={ setCharset }
      setRemaining={ setRemaining }
      setAnswer={ setAnswer }
      setResult={ setResult }
      setQuestion={ setQuestion }
      setUsedQuestions={ setUsedQuestions }
    />        
    );

    expect(screen.getByTestId("BtnStartEnd").textContent).toBe("RESTART ➤ ");

});    


Look at the console. You should see this!




Now let's start testing the Display component. Create Display.test.js in the Display directory, and begin with the usual imports. We also want to import the utility GetCharSet because it will be used in the testing.

src/components/Display/Display.test.js
import React from "react";
import { render, screen } from '@testing-library/react';
import Display from './Display';

import GetCharset from '../../utils/GetCharset';


Then we create the variables and their setters.

src/components/Display/Display.test.js
import React from "react";
import { render, screen } from '@testing-library/react';
import Display from './Display';

import GetCharset from '../../utils/GetCharset';

let maxRemaining;

let started;
let setStarted = (val)=> {
    started = val;
};

let charset;
let setCharset = (val)=> {
    charset = val;
};

let remaining;
let setRemaining = (val)=> {
    remaining = val;
};

let usedQuestions;
let setUsedQuestions = (val)=> {
    usedQuestions = val;
};

let answer;
let setAnswer = (val)=> {
    answer = val;
};

let result;
let setResult = (val)=> {
    result = val;
};

let question;
let setQuestion = (val)=> {
    question = val;
};


And use the describe() function, passing in the name of the component, and a callback.

src/components/Display/Display.test.js
let question;
let setQuestion = (val)=> {
    question = val;
};

describe("Display", () => {

});


There are five tests in this. The strings passed in are descriptive of the tests.

src/components/Display/Display.test.js
describe("Display", () => {
    it("should render with instructions and welcome message if not started", () => {

    });

    it("should render results if started and no questions remaining", () => {

    });    

    it("should render remaining if started and has questions remaining", () => {

    });    

    it("should render answer and tick if correct", () => {

    });    

    it("should render answer and cross if incorrect", () => {

    });    

});


Just to save you some time, each of these tests will render the Display component, with the same attributes. It's the values of the attributes that will vary.

src/components/Display/Display.test.js
describe("Display", () => {
    it("should render with instructions and welcome message if not started", () => {
            render(
            <Display
              charset={ charset }
              started={ started }
              remaining={ remaining }
              maxRemaining={ maxRemaining }
              answer={ answer }
              question={ question }
              usedQuestions={ usedQuestions }
              result={ result }
              setStarted={ setStarted }
              setRemaining={ setRemaining }
              setAnswer={ setAnswer }
              setQuestion={ setQuestion }
              setUsedQuestions={ setUsedQuestions }
            />
            );

    });

    it("should render results if started and no questions remaining", () => {
            render(
            <Display
              charset={ charset }
              started={ started }
              remaining={ remaining }
              maxRemaining={ maxRemaining }
              answer={ answer }
              question={ question }
              usedQuestions={ usedQuestions }
              result={ result }
              setStarted={ setStarted }
              setRemaining={ setRemaining }
              setAnswer={ setAnswer }
              setQuestion={ setQuestion }
              setUsedQuestions={ setUsedQuestions }
            />
            );

    });    

    it("should render remaining if started and has questions remaining", () => {
            render(
            <Display
              charset={ charset }
              started={ started }
              remaining={ remaining }
              maxRemaining={ maxRemaining }
              answer={ answer }
              question={ question }
              usedQuestions={ usedQuestions }
              result={ result }
              setStarted={ setStarted }
              setRemaining={ setRemaining }
              setAnswer={ setAnswer }
              setQuestion={ setQuestion }
              setUsedQuestions={ setUsedQuestions }
            />
            );

    });    

    it("should render answer and tick if correct", () => {
            render(
            <Display
              charset={ charset }
              started={ started }
              remaining={ remaining }
              maxRemaining={ maxRemaining }
              answer={ answer }
              question={ question }
              usedQuestions={ usedQuestions }
              result={ result }
              setStarted={ setStarted }
              setRemaining={ setRemaining }
              setAnswer={ setAnswer }
              setQuestion={ setQuestion }
              setUsedQuestions={ setUsedQuestions }
            />
            );

    });    

    it("should render answer and cross if incorrect", () => {
            render(
            <Display
              charset={ charset }
              started={ started }
              remaining={ remaining }
              maxRemaining={ maxRemaining }
              answer={ answer }
              question={ question }
              usedQuestions={ usedQuestions }
              result={ result }
              setStarted={ setStarted }
              setRemaining={ setRemaining }
              setAnswer={ setAnswer }
              setQuestion={ setQuestion }
              setUsedQuestions={ setUsedQuestions }
            />
            );

    });    
});


For the first test, we need started to be false. The rest doesn't really matter, but for the sake of neatness, we will set the others. And at the end of this test, we assert that "Welcome to J-Trainer!" is part of the document.

src/components/Display/Display.test.js
it("should render with instructions and welcome message if not started", () => {
    maxRemaining = 20;
    setCharset(null);
    setStarted(false);
    setUsedQuestions([]);
    setRemaining(maxRemaining);

        
    render(
    <Display
      charset={ charset }
      started={ started }
      remaining={ remaining }
      maxRemaining={ maxRemaining }
      answer={ answer }
      question={ question }
      usedQuestions={ usedQuestions }
      result={ result }
      setStarted={ setStarted }
      setRemaining={ setRemaining }
      setAnswer={ setAnswer }
      setQuestion={ setQuestion }
      setUsedQuestions={ setUsedQuestions }
    />
    );
    
    expect(screen.queryByText("Welcome to J-Trainer!")).toBeInTheDocument();
});


For the next test, started should be true and we should set remaining to 0. Now we set result to 1 as well. At the end, we should assert that the string "Practice more..." appears because 1 is a very poor result. And also we assert that result and maxRemaining will appear in the document in the string pattern specified.

src/components/Display/Display.test.js
it("should render results if started and no questions remaining", () => {
    maxRemaining = 20;
    setCharset(GetCharset("hiragana"));
    setStarted(true);
    setUsedQuestions([]);
    setRemaining(0);
    setResult(1);


    render(
    <Display
      charset={ charset }
      started={ started }
      remaining={ remaining }
      maxRemaining={ maxRemaining }
      answer={ answer }
      question={ question }
      usedQuestions={ usedQuestions }
      result={ result }
      setStarted={ setStarted }
      setRemaining={ setRemaining }
      setAnswer={ setAnswer }
      setQuestion={ setQuestion }
      setUsedQuestions={ setUsedQuestions }
    />
    );
    
    expect(screen.queryByText("Practice more...")).toBeInTheDocument();
    expect(screen.queryByText(result + " / " + maxRemaining)).toBeInTheDocument(); 
   
});


For this test, we set started to true and make sure that remaining is greater than 0. In this case, we use 19. We assert that the button will show 1 less than remaining, which is 18.

Also, we use the character set for Hiragana. For that, we use GetCharset(). We set question to an integer (use 0 because that's safe enough). So the char property of the element in charset pointed to by question should appear in the document.

src/components/Display/Display.test.js
it("should render remaining if started and has questions remaining", () => {
    maxRemaining = 20;
    setCharset(GetCharset("hiragana"));
    setStarted(true);
    setUsedQuestions([]);
    setRemaining(19);
    setResult(1);
    setQuestion(0);

        
    render(
    <Display
      charset={ charset }
      started={ started }
      remaining={ remaining }
      maxRemaining={ maxRemaining }
      answer={ answer }
      question={ question }
      usedQuestions={ usedQuestions }
      result={ result }
      setStarted={ setStarted }
      setRemaining={ setRemaining }
      setAnswer={ setAnswer }
      setQuestion={ setQuestion }
      setUsedQuestions={ setUsedQuestions }
    />
    );
    
    expect(screen.getByTestId("BtnRemaining").textContent).toBe("18 REMAINING ➤");
    expect(screen.queryByText(charset[question].char)).toBeInTheDocument();    

});


And we want to make sure "btnRemaining" is added as a testing id.

src/components/Display.js

if (started && remaining > 0 && charset[question] !== undefined) {
    return (
        <>
            <div className="Character">
                { charset[question].char }
            </div>
            <div className={ answer === "" ? "Result Hide" : "Result"}>
                <span className={ charset[question].romaji === answer ? "Correct" : "Wrong" }>
                    { charset[question].romaji === answer ? "✓" : "✗" }
                </span>
                { answer }
            </div>
            <div className="Remaining">
                <button data-testid="BtnRemaining" onClick={ ()=>{BtnNext_click();}} disabled={answer === ""}>
                    { remaining - 1 } REMAINING ➤
                </button>
            </div>
        </>           
    );
}


Now for this next test, we use Hiragana again. Set question to 0. And set answer to be the romaji property of the first element (index 0) of charset. And assert that answer will appear in the document, and since it is the correct answer, we display the "✓".

src/components/Display/Display.test.js
it("should render answer and tick if correct", () => {
    maxRemaining = 20;
    setCharset(GetCharset("hiragana"));
    setStarted(true);
    setUsedQuestions([]);
    setRemaining(19);
    setResult(1);
    setQuestion(0);
    setAnswer(charset[0].romaji);


    render(
    <Display
      charset={ charset }
      started={ started }
      remaining={ remaining }
      maxRemaining={ maxRemaining }
      answer={ answer }
      question={ question }
      usedQuestions={ usedQuestions }
      result={ result }
      setStarted={ setStarted }
      setRemaining={ setRemaining }
      setAnswer={ setAnswer }
      setQuestion={ setQuestion }
      setUsedQuestions={ setUsedQuestions }
    />
    );
    
    expect(screen.queryByText(answer)).toBeInTheDocument();
    expect(screen.queryByText("✓")).toBeInTheDocument();    

});


And here we have the opposite of the previous test. The variables are the same for the previous test, except that answer will be the romaji property of the second element (index 1) of the charset array, which is blatantly incorrect since question is set to 0. Thus, we assert that "✗" will appear in the document.

src/components/Display/Display.test.js
it("should render answer and cross if incorrect", () => {
    maxRemaining = 20;
    setCharset(GetCharset("hiragana"));
    setStarted(true);
    setUsedQuestions([]);
    setRemaining(19);
    setResult(1);
    setQuestion(0);
    setAnswer(charset[1].romaji);


    render(
    <Display
      charset={ charset }
      started={ started }
      remaining={ remaining }
      maxRemaining={ maxRemaining }
      answer={ answer }
      question={ question }
      usedQuestions={ usedQuestions }
      result={ result }
      setStarted={ setStarted }
      setRemaining={ setRemaining }
      setAnswer={ setAnswer }
      setQuestion={ setQuestion }
      setUsedQuestions={ setUsedQuestions }
    />
    );
    
    expect(screen.queryByText(answer)).toBeInTheDocument();
    expect(screen.queryByText("✗")).toBeInTheDocument();

});    


There you go, that's what the testing suite should show!




Finally, we test the Keyboard component. For that, create Keyboard.test.js in the Keyboard directory. We begin with the usual imports and variable declarations, and because we will be making use of charset, we import GetCharset as well.

src/components/Keyboard/Keyboard.test.js
import React from "react";
import { render, screen } from '@testing-library/react';
import userEvent from "@testing-library/user-event";
import Keyboard from './Keyboard';

import GetCharset from '../../utils/GetCharset';

let result;
let setResult = (val)=> {
    result = val;
};

let started;
let setStarted = (val)=> {
    started = val;
};

let charset;
let setCharset = (val)=> {
    charset = val;
};

let remaining;
let setRemaining = (val)=> {
    remaining = val;
};

let usedQuestions;
let setUsedQuestions = (val)=> {
    usedQuestions = val;
};

let answer;
let setAnswer = (val)=> {
    answer = val;
};

let question;
let setQuestion = (val)=> {
    question = val;
};


We describe the Keyboard component test with five tests, each with a description and callback.

src/components/Keyboard/Keyboard.test.js
import React from "react";
import { render, screen } from '@testing-library/react';
import userEvent from "@testing-library/user-event";
import Keyboard from './Keyboard';

import GetCharset from '../../utils/GetCharset';

let result;
let setResult = (val)=> {
    result = val;
};

let started;
let setStarted = (val)=> {
    started = val;
};

let charset;
let setCharset = (val)=> {
    charset = val;
};

let remaining;
let setRemaining = (val)=> {
    remaining = val;
};

let usedQuestions;
let setUsedQuestions = (val)=> {
    usedQuestions = val;
};

let answer;
let setAnswer = (val)=> {
    answer = val;
};

let question;
let setQuestion = (val)=> {
    question = val;
};

describe("Keyboard", () => {
    it("should render keys if started and there are remaining questions", () => {

    });

    it("should not render keys if not started", () => {

    });

    it("should not render keys if started and there are no remaining questions", () => {

    });    

    it("should set result + 1 if key pressed correctly", () => {

    });    

    it("should set result + 0 if key pressed incorrectly", () => {

    });
});


As with the last component, we will render the component in the tests, with the requisite data.

src/components/Keyboard/Keyboard.test.js
describe("Keyboard", () => {
    it("should render keys if started and there are remaining questions", () => {
            render(
            <Keyboard
              charset={ charset }
              remaining={ remaining }
              started={ started }
              answer={ answer }
              question={ question }
              result={ result }
              setAnswer={ setAnswer }
              setResult={ setResult }
            />
            );

    });

    it("should not render keys if not started", () => {
            render(
            <Keyboard
              charset={ charset }
              remaining={ remaining }
              started={ started }
              answer={ answer }
              question={ question }
              result={ result }
              setAnswer={ setAnswer }
              setResult={ setResult }
            />
            );

    });

    it("should not render keys if started and there are no remaining questions", () => {
            render(
            <Keyboard
              charset={ charset }
              remaining={ remaining }
              started={ started }
              answer={ answer }
              question={ question }
              result={ result }
              setAnswer={ setAnswer }
              setResult={ setResult }
            />
            );

    });    

    it("should set result + 1 if key pressed correctly", () => {
            render(
            <Keyboard
              charset={ charset }
              remaining={ remaining }
              started={ started }
              answer={ answer }
              question={ question }
              result={ result }
              setAnswer={ setAnswer }
              setResult={ setResult }
            />
            );

    });    

    it("should set result + 0 if key pressed incorrectly", () => {
            render(
            <Keyboard
              charset={ charset }
              remaining={ remaining }
              started={ started }
              answer={ answer }
              question={ question }
              result={ result }
              setAnswer={ setAnswer }
              setResult={ setResult }
            />
            );

    });
});


For this test, started is set to true, remaining is set to any value above 0, and we also set charset. We then assert that the following test ids are found in the document. We'll just use "shi" and "yu", two random answers in the entire set.

src/components/Keyboard/Keyboard.test.js
it("should render keys if started and there are remaining questions", () => {
    setRemaining(1);
    setStarted(true);
    setCharset(GetCharset("hiragana"));


    render(
    <Keyboard
      charset={ charset }
      remaining={ remaining }
      started={ started }
      answer={ answer }
      question={ question }
      result={ result }
      setAnswer={ setAnswer }
      setResult={ setResult }
    />
    );
    
    expect(screen.getByTestId("btnAnswer_shi")).toBeInTheDocument();
    expect(screen.getByTestId("btnAnswer_yu")).toBeInTheDocument(); 
   
});


We need to insert these test ids. So every button in the Keyboard component will have that testing id, which is "btnAnswer_" with the answer appended.

src/components/Keyboard.js
const answers = keyItems.map((item, index) => (
    <div
        key={'romaji_' + index}
        className={item === '' ? 'Answer Hide' : ( answer === '' ? 'Answer' : 'Answer Greyed')}
        onClick={()=>{BtnAnswer_click(item);}}
        data-testid={'btnAnswer_' + item}
    >
        {item}                    
    </div>
    )
);


For this test, we test the opposite. started is set to false, remaining is set to 0 (you could set it to any value technically, but it would make no sense), and we set charset. And we will ensure that the testing ids are not in there by making sure they are null. Again, we will use "shi" and "yu", though you can pretty much use any of the romaji properties in the charset array.

src/components/Keyboard/Keyboard.test.js
it("should not render keys if not started", () => {
    setRemaining(0);
    setStarted(false);
    setCharset(GetCharset("hiragana"));


    render(
    <Keyboard
      charset={ charset }
      remaining={ remaining }
      started={ started }
      answer={ answer }
      question={ question }
      result={ result }
      setAnswer={ setAnswer }
      setResult={ setResult }
    />
    );

    expect(screen.queryByText("shi")).toBe(null);
    expect(screen.queryByText("yu")).toBe(null);

});


For this test, we also test the opposite. started is set to true, remaining is set to 0, and we set charset. And we will ensure that the testing ids are not in there by making sure they are null. Again, we will use "shi" and "yu".

src/components/Keyboard/Keyboard.test.js
it("should not render keys if started and there are no remaining questions", () => {
    setRemaining(0);
    setStarted(true);
    charset = GetCharset("hiragana");


    render(
    <Keyboard
      charset={ charset }
      remaining={ remaining }
      started={ started }
      answer={ answer }
      question={ question }
      result={ result }
      setAnswer={ setAnswer }
      setResult={ setResult }
    />
    );

    expect(screen.queryByText("shi")).toBe(null);
    expect(screen.queryByText("yu")).toBe(null);

});    

Now this test. Set started to true, set remaining to any number greater than 0, set charset, set question to 0, and set result to, say, 5. These last two values will be important. After rendering the component, simulate a click event on the "a" button. "a" is the romaji property at index 0 of charset. Then we assert answer to be "a", and result to be (5 + 1) 6.

src/components/Keyboard/Keyboard.test.js
it("should set result + 1 if key pressed correctly", () => {
    setRemaining(1);
    setStarted(true);
    setCharset(GetCharset("hiragana"));
    setQuestion(0);
    setResult(5);


    render(
    <Keyboard
      charset={ charset }
      remaining={ remaining }
      started={ started }
      answer={ answer }
      question={ question }
      result={ result }
      setAnswer={ setAnswer }
      setResult={ setResult }
    />
    );

    userEvent.click(screen.getByTestId("btnAnswer_a"));
    expect(answer).toBe("a");
    expect(result).toBe(6);

});    


And finally, this test! We do what we did before, but deliberately simulate a click event on the button "ka", which is not at index 0. Naturally, we assert that the answer is "ka". Since we've set result to 10, we assert that result should still be 10.
it("should set result + 0 if key pressed incorrectly", () => {
    setRemaining(1);
    setStarted(true);
    setCharset(GetCharset("hiragana"));
    setQuestion(0);
    setResult(10);


    render(
    <Keyboard
      charset={ charset }
      remaining={ remaining }
      started={ started }
      answer={ answer }
      question={ question }
      result={ result }
      setAnswer={ setAnswer }
      setResult={ setResult }
    />
    );

    userEvent.click(screen.getByTestId("btnAnswer_ka"));
    expect(answer).toBe("ka");
    expect(result).toBe(10);

});


Yep, and now we have a full suite!




Final words

This app was fun to write. Not so fun to explain, but it actually was useful for teaching me to read and write in Japanese, and level up in my ReactJS.

shi yu around,
T___T

Wednesday, 22 June 2022

Web Tutorial: The Japanese language Trainer App (Part 3/4)

Right now, the display and the keyboard are up. We will not need any more components and utilities. What we will need, are button events that facilitate answers and progress of the Japanese-learning exercise.

In the answers object, add a click event, BtnAnswer_click(). item has to be passed in as an argument.

src/components/Keyboard/Keyboard.js
const answers = keyItems.map((item, index) => (
    <div
        key={'romaji_' + index}
        className={item === '' ? 'Answer Hide' : ( answer === '' ? 'Answer' : 'Answer Greyed')}
        onClick={()=>{BtnAnswer_click(item);}}
    >
        {item}                    
    </div>
    )
);


And here, let's define it.

src/components/Keyboard/Keyboard.js
let setAnswer = props.setAnswer;
let setResult = props.setResult;

const BtnAnswer_click = (ans)=> {

};


const keyItems = GetKeys();


We only want stuff to happen if a button with an actual answer was clicked on. So just in case, we first use an If block to ensure that ans is not an empty string. If so, we set answer to ans.

src/components/Keyboard/Keyboard.js
const BtnAnswer_click = (ans)=> {
    if (ans !== "") {
        setAnswer(ans);
    }

};


We now check if this is the correct answer. We verify that by comparing ans to the romaji property of the current element of charset, pointed to by question. If it's a match, increment result.

src/components/Keyboard/Keyboard.js
const BtnAnswer_click = (ans)=> {
    if (ans !== "") {
        setAnswer(ans);

        if (ans === charset[question].romaji) {
            setResult(result + 1)
        }

    }
};


Try it! Right now, the correct answer is "ko". I'm going to click on it.




We see the result. It is correct! Note that the keyboaad buttons are greyed out now that you have provided an answer.




Try this again by refreshing the app. The correct answer is "zu", but let's select "shi".




We see the result. It is wrong!




Now we are going to provide a click handler for the orange button that goes to the next question. In the Display component, add this function call to the button.

src/components/Display/Display.js
<div className="Remaining">
    <button onClick={ ()=>{BtnNext_click();}} disabled={answer === ""}>
        { remaining - 1 } REMAINING ➤
    </button>
</div>


Let's create this function.

src/components/Display/Display.js
let setQuestion = props.setQuestion;
let setUsedQuestions = props.setUsedQuestions;

const BtnNext_click = (ans)=> {

};


const ManageQuestionList = ()=> {


If remaining is still more than 0, decrement remaining.

src/components/Display/Display.js
const BtnNext_click = (ans)=> {
    if (remaining > 0) {
        setRemaining(remaining - 1);
    }

};


Then reset answer to an empty string, and run ManageQuestionList().

src/components/Display/Display.js
const BtnNext_click = (ans)=> {
    if (remaining > 0) {
        setRemaining(remaining - 1);
    }

    setAnswer("");
    ManageQuestionList();

};


Now if you click on the orange button, the number decrements, and you get a new random character. And the keyboard buttons are enabled!




Hold on, we're not done yet. Add this If block. It checks if started is true and remaining is 0, meaning the user has done all 20 questions.

src/components/Display/Display.js
if (started && remaining > 0 && charset[question] !== undefined) {
    return (
        <>
            <div className="Character">
                { charset[question].char }
            </div>
            <div className={ answer === "" ? "Result Hide" : "Result"}>
                <span className={ charset[question].romaji === answer ? "Correct" : "Wrong" }>
                    { charset[question].romaji === answer ? "✓" : "✗" }
                </span>
                { answer }
            </div>
            <div className="Remaining">
                <button data-testid="BtnRemaining" onClick={ ()=>{BtnNext_click();}} disabled={answer === ""}>
                    { remaining - 1 } REMAINING ➤
                </button>
            </div>
        </>           
    );
}

if (started && remaining === 0) {

}


return (
    <>
        <br />
        <h3>Welcome to J-Trainer!</h3>
        <ol>
            <li>Select your character set.</li>
            <li>Click <button className="btnSmall">BEGIN ➤ </button> to start.</li>
            <li>For every character that is displayed, select the correct pronounciation. Note that some characters may have the same pronounciation as others.</li>
            <li>Click the <button className="btnSmall">REMAINING ➤ </button> button to continue after the results are displayed.</li>
        </ol>
    </>            
);


We will return this. There is a div styled using CSS class Result. There is a h1 tag where we show how many correct answers (result) out of maxRemaining, which is 20. Also, if the percentage of correct answers is more than 50, we style it using Correct and Wrong if otherwise. The naming is a bad practice, but I can't be arsed to change it.

src/components/Display/Display.js
if (started && remaining === 0) {
    return (
        <>
            <div className="Result">
                <h1 className={ (result / maxRemaining * 100) > 50 ? "Correct" : "Wrong" }>
                    { result } / { maxRemaining }
                </h1>
            </div>
        </>           
    );

}


Below that, we have a h3 tag. Again, the styling is based on whether the percentage of correct answers is over 50.

src/components/Display/Display.js
if (started && remaining === 0) {
    return (
        <>
            <div className="Result">
                <h1 className={ (result / maxRemaining * 100) > 50 ? "Correct" : "Wrong" }>
                    { result } / { maxRemaining }
                </h1>
                <h3 className={ (result / maxRemaining * 100) > 50 ? "Correct" : "Wrong" }>
                    
                </h3>

            </div>
        </>           
    );
}


And we have different messages based on whether the percentage is 100, below 50 or otherwise.

src/components/Display/Display.js
if (started && remaining === 0) {
    return (
        <>
            <div className="Result">
                <h1 className={ (result / maxRemaining * 100) > 50 ? "Correct" : "Wrong" }>
                    { result } / { maxRemaining }
                </h1>
                <h3 className={ (result / maxRemaining * 100) > 50 ? "Correct" : "Wrong" }>
                    { (result / maxRemaining * 100) == 100 ? "よかった! Perfect score!" : ((result / maxRemaining * 100) < 50 ? "Practice more..." : "すごいですね!") }
                </h3>
            </div>
        </>           
    );
}


Here are the messages you get for different scenarios.









Next

The app is functioning, and now we will write tests for the interface.

Monday, 20 June 2022

Web Tutorial: The Japanese Language Trainer App (Part 2/4)

Now that we have a default view for the Display component, let's add alternative ones. We begin by adding a case for if the exercise has started, there are remaining questions and a question has been defined.

src/components/Display/Display.js
function Display(props) {
    let charset = props.charset;
    let started = props.started;
    let remaining = props.remaining;
    let maxRemaining = props.maxRemaining;
    let answer = props.answer;
    let question = props.question;
    let result = props.result;
    let usedQuestions = props.usedQuestions;
    let setRemaining = props.setRemaining;
    let setAnswer = props.setAnswer;
    let setQuestion = props.setQuestion;
    let setUsedQuestions = props.setUsedQuestions;

    if (started && remaining > 0 && charset[question] !== undefined) {
        return (
           
        );
    }

    
    return (
        <>
            <br />
            <h3>Welcome to J-Trainer!</h3>
            <ol>
                <li>Select your character set.</li>
                <li>Click <button className="btnSmall">BEGIN ➤ </button> to start.</li>
                <li>For every character that is displayed, select the correct pronounciation. Note that some characters may have the same pronounciation as others.</li>
                <li>Click the <button className="btnSmall">REMAINING ➤ </button> button to continue after the results are displayed.</li>
            </ol>
        </>            
    );
}


In that case, we have three divs, styled using CSS classes Character, Result (and Hide depending on whether answer is an empty string) and Remaining. The last one will have a button which will show how many questions are remaining, less the one question that is on display.

src/components/Display/Display.js
if (started && remaining > 0 && charset[question] !== undefined) {
    return (
        <>
            <div className="Character">

            </div>
            <div className={ answer === "" ? "Result Hide" : "Result"}>

            </div>
            <div className="Remaining">
                <button>
                    { remaining - 1 } REMAINING ➤
                </button>
            </div>
        </>  
        
    );
}


In the first div, we will display the char property of the element of charset pointed to by question. In the second div, we display the answer.

src/components/Display/Display.js
if (started && remaining > 0 && charset[question] !== undefined) {
    return (
        <>
            <div className="Character">
                { charset[question].char }
            </div>
            <div className={ answer === "" ? "Result Hide" : "Result"}>
                { answer }
            </div>
            <div className="Remaining">
                <button>
                    { remaining - 1 } REMAINING ➤
                </button>
            </div>
        </>           
    );
}


We will also add a span tag, and the CSS class is Correct or Wrong based on the comparison of answer with the romaji property of the element of charset pointed to by question.

src/components/Display/Display.js
if (started && remaining > 0 && charset[question] !== undefined) {
    return (
        <>
            <div className="Character">
                { charset[question].char }
            </div>
            <div className={ answer === "" ? "Result Hide" : "Result"}>
                <span className={ charset[question].romaji === answer ? "Correct" : "Wrong" }>

                </span>

                { answer }
            </div>
            <div className="Remaining">
                <button>
                    { remaining - 1 } REMAINING ➤
                </button>
            </div>
        </>           
    );
}


A tick or cross is also displayed based on the same criteria.

src/components/Display/Display.js
if (started && remaining > 0 && charset[question] !== undefined) {
    return (
        <>
            <div className="Character">
                { charset[question].char }
            </div>
            <div className={ answer === "" ? "Result Hide" : "Result"}>
                <span className={ charset[question].romaji === answer ? "Correct" : "Wrong" }>
                    { charset[question].romaji === answer ? "✓" : "✗" }
                </span>
                { answer }
            </div>
            <div className="Remaining">
                <button>
                    { remaining - 1 } REMAINING ➤
                </button>
            </div>
        </>           
    );
}


Now, you won't see anything new yet. That's because question is null at this point, thus the element of charset pointed to by question is going to be undefined. We need to set a question, and will do so by adding this If block to check if question is null and charset is not. If so, we run ManageQuestionList().

src/components/Display/Display.js
let setQuestion = props.setQuestion;
let setUsedQuestions = props.setUsedQuestions;

if (question === null && charset !== null) {
    ManageQuestionList();
}

    
if (started && remaining > 0 && charset[question] !== undefined) {
    return (
        <>
            <div className="Character">
                { charset[question].char }
            </div>
            <div className={ answer === "" ? "Result Hide" : "Result"}>
                <span className={ charset[question].romaji === answer ? "Correct" : "Wrong" }>
                    { charset[question].romaji === answer ? "✓" : "✗" }
                </span>
                { answer }
            </div>
            <div className="Remaining">
                <button>
                    { remaining - 1 } REMAINING ➤
                </button>
            </div>
        </>           
    );
}


And here we define the function.

src/components/Display/Display.js
let setQuestion = props.setQuestion;
let setUsedQuestions = props.setUsedQuestions;

const ManageQuestionList = ()=> {

}

    
if (question === null && charset !== null) {
    ManageQuestionList();
}
    
if (started && remaining > 0 && charset[question] !== undefined) {
    return (
        <>
            <div className="Character">
                { charset[question].char }
            </div>
            <div className={ answer === "" ? "Result Hide" : "Result"}>
                <span className={ charset[question].romaji === answer ? "Correct" : "Wrong" }>
                    { charset[question].romaji === answer ? "✓" : "✗" }
                </span>
                { answer }
            </div>
            <div className="Remaining">
                <button>
                    { remaining - 1 } REMAINING ➤
                </button>
            </div>
        </>           
    );
}


In here, we define index by getting one random number from 0 to the size of charset.

src/components/Display/Display.js
const ManageQuestionList = ()=> {
    var index = Math.floor(Math.random() * charset.length);
}


And then we check if index is in the array usedQuestions. We keep getting that random number as long as it already exists in usedQuestions. This is so that we don't get repeats in the same exercise.

src/components/Display/Display.js
const ManageQuestionList = ()=> {
    var index = Math.floor(Math.random() * charset.length);

    while (usedQuestions.indexOf(index) !== -1) {
        index = Math.floor(Math.random() * charset.length);
    }

}


Now we use the setter setQuestion() to set question to the value of index.

src/components/Display/Display.js
const ManageQuestionList = ()=> {
    var index = Math.floor(Math.random() * charset.length);

    while (usedQuestions.indexOf(index) !== -1) {
        index = Math.floor(Math.random() * charset.length);
    }

    setQuestion(index);
}


And we make sure usedQuestions has index in it.

src/components/Display/Display.js
const ManageQuestionList = ()=> {
    var index = Math.floor(Math.random() * charset.length);

    while (usedQuestions.indexOf(index) !== -1) {
        index = Math.floor(Math.random() * charset.length);
    }

    setQuestion(index);
    let questionList = usedQuestions;
    questionList.push(index);
    setUsedQuestions(questionList);

}


So yeah! Let's try this again. You should now see that tiny character, randomly picked from charset. This will also depend on whether you chose "Hiragana" or "Katakana".




This obviously needs styling! We go to Display.css next. For the Character CSS class, what I have done is enlarge the text, frame it in a light grey circular border, and adjust the layout.

src/components/Display/Display.css
.Character{
    font-size: 10em;
    text-align: center;
    width: 220px;
    height: 220px;
    border-radius: 50%;
    background-color: #AAAAAA;
    color: #FFFFFF;
    margin: 0 auto 0 auto;
    padding: 0px;
}


For Result, it's pretty much just for appearance as well.

src/components/Display/Display.css

.Character{
    font-size: 10em;
    text-align: center;
    width: 220px;
    height: 220px;
    border-radius: 50%;
    background-color: #AAAAAA;
    color: #FFFFFF;
    margin: 0 auto 0 auto;
    padding: 0px;
}

.Result{
    font-size: 2em;
    font-weight: bold;
    text-align: center;
}


I have set Remaining to center text. You should see the button centered just so. The CSS classes Correct and Wrong have been assigned colors. You won't see the results just yet though.

src/components/Display/Display.css
.Character{
    font-size: 10em;
    text-align: center;
    width: 220px;
    height: 220px;
    border-radius: 50%;
    background-color: #AAAAAA;
    color: #FFFFFF;
    margin: 0 auto 0 auto;
    padding: 0px;
}

.Result{
    font-size: 2em;
    font-weight: bold;
    text-align: center;
}

.Remaining{
    text-align: center;
}

.Correct{
    color: #009900;
}

.Wrong{
    color: #990000;
}


And now you see this!




The Keyboard component

What we need next is a way for the user to input the correct answer. For that, we will have a keyboard with buttons, all labelled with the eighty-six different possible pronounciations in both Hiragana and Katakana.

So create the Keyboard directory inside the components directory, and create Keyboard.js, Keyboard.css and index.js. For index.js, we are doing just what we did for the other index files.

src/components/Keyboard/index.js
export { default } from './Keyboard';


In Keyboard.js, we import the CSS file, define Keyboard as a function, and export Keyboard as a component.

src/components/Keyboard/Keyboard.js
import './Keyboard.css';

function Keyboard(props) {

}

export default Keyboard;


And here, we define variables based on the various values passed from props.

src/components/Keyboard/Keyboard.js
import './Keyboard.css';

function Keyboard(props) {
    let charset = props.charset;
    let started = props.started;
    let answer = props.answer;
    let question = props.question;
    let remaining = props.remaining;
    let result = props.result;
    let setAnswer = props.setAnswer;
    let setResult = props.setResult;

}

export default Keyboard;


We define keyItems by calling GetKeys().

src/components/Keyboard/Keyboard.js
import './Keyboard.css';

function Keyboard(props) {
    let charset = props.charset;
    let started = props.started;
    let answer = props.answer;
    let question = props.question;
    let remaining = props.remaining;
    let result = props.result;
    let setAnswer = props.setAnswer;
    let setResult = props.setResult;
    
    const keyItems = GetKeys();
}

export default Keyboard;


We are going to import GetKeys from a utility we are going to write next.


src/components/Keyboard/Keyboard.js
import './Keyboard.css';

import GetKeys from '../../utils/GetKeys';

function Keyboard(props) {
    let charset = props.charset;
    let started = props.started;
    let answer = props.answer;
    let question = props.question;
    let remaining = props.remaining;
    let result = props.result;
    let setAnswer = props.setAnswer;
    let setResult = props.setResult;
    
    const keyItems = GetKeys();
}

export default Keyboard;


Create this file in the utils directory. We need to define GetKeys() and export it. Then within the function, define keys as an array and return it.

src/utils/GetKeys.js
const GetKeys = () => {
    let keys = [];

    return keys;
}

export default GetKeys;    


This is the layout that we are going to use. There are 15 columns and 6 rows. This is just a big-ass one-dimensional array of (15 x 6 = 90) elements, but I have visually arranged the code so we can see what the final layout will look like. Note that there are several empty strings among the array elements. These represent the gaps.

src/utils/GetKeys.js
const GetKeys = () => {
    let keys = [];

    keys = [
        "pa", "ba", "da", "za", "ga", "wa", "ra", "ya", "ma", "ha", "na", "ta", "sa", "ka", "a",
        "pi", "bi", "", "ji", "gi", "", "ri", "", "mi", "hi", "ni", "chi", "shi", "ki", "i",
        "pu", "bu", "", "zu", "gu", "", "ru", "yu", "mu", "fu", "nu", "tsu", "su", "ku", "u",
        "pe", "be", "de", "ze", "ge", "", "re", "", "me", "he", "ne", "te", "se", "ke", "e",
        "po", "bo", "do", "zo", "go", "wo", "ro", "yo", "mo", "ho", "no", "to", "so", "ko", "o",
        "", "", "", "", "", "n", "", "", "", "", "", "", "", "", ""
    ];


    return keys;
}

export default GetKeys;    


Now back to the Keyboard component. Now we are going to define answers as an entire HTML block that we will be generating from keyItems, using the map() method. (Here's some information on this method.)

src/components/Keyboard/Keyboard.js
function Keyboard(props) {
    let charset = props.charset;
    let started = props.started;
    let answer = props.answer;
    let question = props.question;
    let remaining = props.remaining;
    let result = props.result;
    let setAnswer = props.setAnswer;
    let setResult = props.setResult;
    
    const keyItems = GetKeys();
    
    const answers = keyItems.map((item, index) => (

    );

}


We want a div for every element in keyItems, which, as we've defined, is an array. Putting in item within the div will give us divs with the strings from keyItems.

src/components/Keyboard/Keyboard.js
const answers = keyItems.map((item, index) => (
    <div>
        {item}                    
    </div>

    )
);


We will add a key for each element just to avoid the warnings ReactJS will inevitably give us otherwise. The CSS class will be Answer and Hide if item is an empty string (remember the empty strings we put in the GetKeys() function?) and if not, we do a further check if answer is an empty string. If the question has not been answered, the CSS class is Answer, but if it has, we grey it out using Greyed as well.

src/components/Keyboard/Keyboard.js
const answers = keyItems.map((item, index) => (
    <div
        key={'romaji_' + index}
        className={item === '' ? 'Answer Hide' : ( answer === '' ? 'Answer' : 'Answer Greyed')}

    >
        {item}                    
    </div>
    )
);


We next check if started is true and there are questions remaining. If so, we display answers. If not, we don't.

src/components/Keyboard/Keyboard.js
    const answers = keyItems.map((item, index) => (
        <div
            key={'romaji_' + index}
            className={item === '' ? 'Answer Hide' : ( answer === '' ? 'Answer' : 'Answer Greyed')}
        >
            {item}                    
        </div>
        )
    );

    if (started && remaining > 0) {
        return (
            <>
                { answers }
            </>            
        );
    } else {
        return (
            <>
            </>            
        );
    }

}

export default Keyboard;


In App.js, add an import statement.

src/App.js
import React, { useState } from 'react';
import './App.css';
import Header from './components/Header';
import Display from './components/Display';
import Keyboard from './components/Keyboard';

function App() {


Inside the div styled using Bottom, include the Keyboard component and pass in the properties.

src/App.js
<div className="Bottom">
    <Keyboard
      charset={ charset }
      remaining={ remaining }
      started={ started }
      answer={ answer }
      question={ question }
      result={ result }
      setAnswer={ setAnswer }
      setResult={ setResult }
    />

</div>


Obviously not ideal. Time for some CSS!




Each divs should be 35 pixels across. This is important because the width of the div is 600 pixels, and you want the row to only have 15 buttons. With top and right margins set to 3 pixels, this should be adequate. float property is set to left, and cursor is pointer to indicate that this div is clickable. The rest is aesthetics - round borders, grey borders and text, and a white background.

src/components/Keyboard/Keyboard.css
.Answer {
    width: 35px;
    height: 30px;
    float: left;
    cursor: pointer;
    border: 1px solid #444444;
    border-radius: 5px;
    color: #444444;
    background-color: #FFFFFF;
    text-align: center;
    font-weight: bold;
    margin-top: 3px;
    margin-right: 3px;
}


We have a hover pseudoselector for the button to change color upon a mouseover. The Greyed class has cursor set to default and the background changed, to show that the button is no longer clickable.

src/components/Keyboard/Keyboard.css
.Answer {
    width: 35px;
    height: 30px;
    float: left;
    cursor: pointer;
    border: 1px solid #444444;
    border-radius: 5px;
    color: #444444;
    background-color: #FFFFFF;
    text-align: center;
    font-weight: bold;
    margin-top: 3px;
    margin-right: 3px;
}

.Answer:hover {
    color: #FFFFFF;    
    background-color: #444444;
}

.Greyed, .Greyed:hover {
    border: 1px solid #AAAAAA;
    color: #AAAAAA;
    background-color: #FFFFFF;
    cursor: default;
}


And here's your keyboard! I have moused over "ya" to show you the effect.




Next

Allowing users to input answers, and showing the results.