Showing posts with label japan. Show all posts
Showing posts with label japan. Show all posts

Saturday, 1 November 2025

The year I finally achieved that JLPT certification

Some say "better late than never". I got a huge dose of that this year as I obtained my Japanese Language Proficiency Test (JLPT) N5 Certificate.

Why was this overdue? Well, you see, because I actually began on this journey at the tender age of 15. I was a Polytechnic student at the time, and this was a supplementary module. At first, things went well. Having received education in Chinese, learning the basic sounds of Japanese was a breeze. Learning the Hiragana and Katakana writing systems wasn't much of a stretch as well; in fact it was significantly easier than reading and writing Chinese characters. Until we got to Kanji, which essentially was Chinese characters.

Ugh, more Chinese.

At which point I lost all interest.

Also, at this time, I had begun an obsession with writing code, and building stuff. Japanese was a pretty language, but it just didn't fit into my world the same way C++, SQL or even QBasic, did.

And the rest, as they say, is history. Even after graduation, not only did I neglect whatever little Japanese I had learned, the next few decades were a disastrous collection of bad career choices, even worse lifestyle choices, and sobering life lessons.

COVID-19 and Clubhouse

Fast-forward to 2021. COVID-19 had the world in its loathsome grip and I, like many others, sought human interaction online. That was when I discovered the Clubhouse app. Through it, I encountered numerous communities, not all of which spoke English, Mandarin, or even Cantonese.

A few of these were Japanese speakers. And some were even offering Japanese lessons. I dove right in, relearning all the basics I had forgotten. Many times, I would simply listen in, try to make sense of the conversation, and Google words that came up. Then I started engaging, hesitantly, in the conversations. I even participated in Mandarin-Japanese exchanges!

Little cultural exchanges.

It was slow going. I wrote an app to facilitate relearning Hiragana and Katakana. I practiced writing every day, religiously. But there was this feeling that I could do more.

That was when someone suggested that I take the JLPT. And from that day forth, I took things up a notch.

Training in earnest

My next step was to install Duolingo on my phone. For the next few years, I faithfully partook of the exercises daily. At the same time, I went onto YouTube to search for JLPT Listening exercises. I didn't really have a sense of how far I had progressed; I just knew I needed to keep going. I've never been brilliant; but what I am good at is being consistent.

One year later, I downloaded the Migii app, and paid the subscription fees for accessing sample JLPT exam questions. With Duolingo, the returns were diminishing. I needed to train a different set of linguistic muscles - the kind used for passing the exam. Duolingo had brought me to a certain point where I could read the Japanese exam questions without too much difficulty... now I had to practice answering them.

The next milestone was hit another year later when, while watching a Japanese TV show, with English subtitles, I noticed something odd. Almost constantly, the subtitles would read "It's OK". Even though "It's OK" was a perfectly reasonable thing to say in the context of the current point of the story, it was a pretty poor translation.

Phrase Literal Meaning English Translation
daijobu It's OK. "It's OK."
dou itashimashite You're welcome. "It's OK."
shinpai suna Don't worry. "It's OK."
anshin shite Relax. "It's OK."
ki ni shinai de Pay it no mind. "It's OK."


At this point, I found myself thinking - wow, these translators are so goddamn lazy! They just use "It's OK" for everything!

And my next thought was - hey, if I know this much, shouldn't I be taking the JLPT already?!

The JLPT Test Voucher

That was when I registered for the JLPT on the Japanese Cultural Society of Singapore website. and paid a hundred Singapore dollars for the privilege. A month later, the JLPT test voucher arrived in the mail. The train was in motion; there was no going back.

Meanwhile, I continued training religiously. With Listening practice, especially. Online chatter had it that the recordings would be played only once, so I trained myself similarly, by not rewinding and replaying.

The JLPT N5 Exam

It was July when the day arrived. I head for the Singapore Management Institute where the exam was held, and stood in line with what looked like mostly Burmese folks. Interesting.

The invigilator who took charge of the exam room I had been assigned to, seemed to be Japanese going by the way she spoke English.

Shading answers with a pencil.

The proceedings were charmingly old-school. We were given question and answer sheets. It was all multiple-choice, and the correct answer had to be shaded with a pencil for feeding into a scanning machine. Despite my numerous certificates, I hadn't done this since... 2015? This was because my last couple certifications were earned from doing the coursework and presentations, rather than standardized tests.

The toughest moment in the exam came from the Choukai, which was the Listening portion. Despite my best efforts, my concentration slipped at various points. It was with considerable relief that I handed in my question and answer sheets, and headed off.

In August, I logged on to the JLPT portal to check my results. I had passed, and my results were more than decent. I actually scored higher on Listening than other sections!

The real value of all this

Last September, I got my actual physical certification through the mail. It was a foregone conclusion by that time, but I still felt that little thrill of pride. Job well done, bro, I told my reflection. Not such a big deal in the larger picture, but we've got to celebrate our wins even if they're small. Especially if said win took thirty-three friggin' years to achieve.

My results.

The JLPT N5 Certification isn't going to change my life. As far as professional cred goes, it's barely a blip. Achieving the JLPT N5 Certification probably puts me on par with the average Japanese toddler where the language is concerned. As for the value of understanding multiple languages? That's not much of a flex. This is Southeast Asia; just about everyone and their dog is multilingual.

No; at my age, the act of learning is arguably more important than what's being learned. It helps stave off dementia.

The real value is knowing and affirming, that with sufficient motivation and putting in the time and effort, I can learn pretty much whatever I choose to learn. And that is powerful stuff. In an age where things are constantly and rapidly evolving, the ability to learn shit has become vital - not just in the tech industry, but for life itself.

In hindsight, I should have realized this. How did I learn ASP? PHP? Ruby? VueJS? D3? All by picking up a book, watching videos, visiting websites, and constant practice. Most of the time, it really is that simple. The methods vary, but at the end of the day, it's about the willingness to put in the hours.

The linguistic journey continues!

I do want to see if I can achieve the next rung, the JLPT N4 Certification. Again, I'm not really sure why. Just for the hell of it, I guess. It's not like I plan to visit Japan. It's not like I realistically have anything to prove to anyone.

On the other hand, Korean does seem pretty interesting.

Decisions, decisions...

Ganbarimashoo!
T___T

Thursday, 19 June 2025

Failure is an eventuality, not a possibility

Some years ago, I recounted a story I'd heard to a friend.

In ancient Japan, the Lord of a territory had two sons. The first son was an impressive physical specimen - in archery, swordsmanship, wrestling, and he never lost. The second son was competent, but compared to his brother, he often came up short. It was expected that the Lord would pass on the leadership of the clan to his first son.

To the first son's consternation, however, when the time came for the Lord to retire, it was the second son who inherited the territory. The first son demanded to know why, and his father explained.

Inheriting leadership.

While the first son had gained a reputation for excellence and never having tasted defeat, the second son gained a reputation for persistence. He had an understanding of both his strengths and his shortcomings, and had learned from his mistakes - traits that all capable leaders need to have. More importantly, he understood how to recover from failure.

A different takeaway? 

Somehow, my buddy's takeaway from this was that the Lord had made a foolish decision - one looks at track record and it was unquestionable that the first son should be the one to lead. From this, I can only conclude that this was because my friend had never been part of the tech industry, or its very specialized cousin, the cybersecurity industry.

You see, techies generally know (any techies who don't know this should probably find another, less taxing line of work) that in the world of cybersecurity, defense is always playing catch-up to attack. After all, a defense can only be devised for an attack method that has already been invented. Thus, if your system has never gone down due to a cybersecurity attack, it's usually down to two reasons. One, there are targets more tempting than you. You're simply not worth the effort. Two, you've just been lucky.

Anyone feel lucky?

Neither reason has anything to do with the excellence of your defenses. That's just not the world we live in.

Thus the real test is not whether one can remain undefeated in perpetuity but whether one can recover from defeat, and how quickly. That is why recovery plans are integral to I.T Security policies. Prevention strategies are also part of I.T Security policies, yes... but the general understanding is that it's not a matter of whether your system will be the victim of a strong and determined attack, but when.

In fact, one should be extremely aware of those who claim to have never failed or made a mistake. If they're telling the truth (and that's a huge if) it raises red flags as to their adaptability and resilience.

In this industry, you're judged by how well you recover from failure. Not by never having encountered it.

An MMA example 

There's a real-world example in Mixed Martial Arts that illustrates this perfectly.

Take Conor McGregor. I'm not a big fan of the man by any means. He seems like a jerk with a big mouth. Where it concerns his job as a fighting athlete, however, I have considerably more respect for him than I do for Ronda Rousey. (That's not to say I think I could take her; she'd probably kick my ass in ten seconds flat).

You see, they're both legends in the sport. Their athleticism and skill. Their personalities. The way they popularized the sport. However, there is one significant difference. Ronda Rousey dominated the women's circuit for years, until she encountered two defeats. After the first defeat, she went to pieces and it was a while before she came back. The second defeat, at the capable hands of Amanda Nunes, was an utter annihilation and ended in her retirement.



McGregor, on the other hand, has lost a hell of a lot more fights than Rousey. However, each time, he's always come back strong, louder and more obnoxious than ever, none the worse for wear. Never has he allowed a defeat to end his career the way it did for Rousey. While McGregor was used to defeat and took it in stride, a couple of defeats was all it took for Rousey to crumble. Because she had never tasted it and didn't know how to handle it.

While McGregor knew he wasn't infallible (and had made peace with the fact), Rousey allowed herself to believe she was.

That's why I feel that an experience that incorporates failures (and the lessons we learn from them) are infinitely more useful than an experience with nothing but success.

Conclusion 

True value lies in adaptability, not invincibility.

It's a matter of when, rather than if, you taste defeat. And when that happens, what really matters is how you react to it. Nobody is invincible and nothing is perfect. And until we come to terms with that fact, we're doomed to repeated failure.

For the win,
T___T

Monday, 20 February 2023

Film Review: Black Mirror Series Three (Part 2/3)

This episode we will be discussing is Playtest, and it's very gamer-centric. It's also different in the sense that there's none of that Zed-eyes type stuff that appeared during The Entire History Of You, White Christmas and Nosedive.

This does not, however, mean that we will be going low-tech. If anything, the tech in here arguably even more futuristic, dealing with uploads to the brain as opposed to "just" making copies and recordings.

The Premise

Cooper is paid to playtest a new gaming concept by gaming company Saito Geimu - an immersive horror virtual reality game. This being Black Mirror, something of course goes horribly and ironically wrong.

The Characters

Wyatt Russell is Cooper Redfield. The script paints him as a nice sensitive guy, if sometimes unethical. Personally, I think he did fine even though his very prominent chin got a bit distracting in the early stages. Thankfully that beard later on took care of that. And guess what - he plays U.S Agent in The Falcon and the Winter Soldier! Dude was definitely working out after this episode, because he looks kind of doughy here.

Hannah John-Kamen as Sonja, whom I last saw in Ant-Man and the Wasp and Resident Evil: Welcome to Raccoon City. She's all right here. Seems like a poor man's Emily Blunt.

Wunmi Mosaku as Katie. Friendly and professional, sometimes dry and sarcastic. I enjoyed that performance.

Ken Yamamura as Shou Saito, the Japanese game designer. At first glance, he comes across as warm, friendly and enthusiastic about his craft. Near the end, when we see the real him, he's more sociopathic genius than anything. When the playtest fails and Cooper dies, he looks more bored than anything.

Elizabeth Moynihan appears near the end as Cooper's mother, who is suffering from dementia. She cuts a pretty tragic figure.

The Mood

Things start out bright, cheery, and dare I say, pedestrian. Midway through, it goes into clinically white, which seems to be a pattern with Black Mirror, and things soon become dark and sinister once Cooper begins his playtesting.

What I liked

The tech visuals are nice. The equipment, the interfaces and all, don't look over-the-top. It's smooth, minimalistic design.


Cooper and Sonja has this American-vs-Brit thing going. Cooper even has this line, which was cheesy, but elicited a chuckle. Wyatt Russell really sells it too.

All right. You are a top bird. I don't know what that means, but I heard somebody say it once, and you're it.


That Whack-a-Mole game is something else!


Since the system has scanned Cooper's head, everything he is experiencing from that point onwards contains little nods to stuff that went on earlier. For example, in the game, he is reading a book with this chapter. Earlier on, Cooper and Sonja met in a place called The Raven Inn. Man, talk about callbacks!




Hot damn, that twist at the end! That was a shocker, for sure.

What I didn't

The entire ten to twenty minute montage of Cooper leaving home, going through his travels, up to the point he finally begins playtesting, just feels like filler.

A Redfield in a haunted mansion? Seems to tread overly familiar ground here. What's more, Sonja's actress actually plays Jill Valentine in Resident Evil: Welcome to Raccoon City years later!

Conclusion

Great, great story. Started off a little lame and sad, but the last few minutes of it made it all worthwhile. Definitely a worthy entry in Black Mirror. Unlike many of the other tales, there's no overt moral to this tale and that's a little disappointing. The horror in this episode is more classic horror and jump scares, not the more cerebral kind Black Mirror usually gives us.

My Rating

9 / 10

Next

Shut Up and Dance

Tuesday, 11 October 2022

TeochewThunder: Year Eight (Part 1/2)

2022 so far has had me working from home, just like 2021. The main difference that my wife has been back from China since the last quarter of the previous year. And my efforts to acclimatize to the inevitable changes in my lifestyle could fill an entire book, never mind a blogpost.

Suffice to say, my blogging and coding time has met a few roadblocks, because there are times my duties as a husband take precedence. As they should. I'm happy to say, however, that I've managed to stay pretty constant, even though the average length of my blogposts has significantly decreased. Part of that could also be due to a conscious effort towards brevity. Honestly, I take no pleasure in droning on like some pompous, oblivious jackass.

No more commute!

This is the third year in a row I spent the majority of my time working from home. This gave me greater freedom to blog, or at least research new ideas. At least, the time I saved on commuting definitely contributed.

As soon as the workday is done, I can go right to coding, or blogging, instead of waiting to get home, and probably getting physically drained from the commute. It can't be overstated how much this has improved my work on the blog.

Trello

If anything good came from the utter shitshow that was my job at this company, it was that I took careful note of all the tools they used, and adopted those practices as much as I could. One of these tools was Trello. It's no exaggeration to say that it practically runs my life now.


I have written a blogpost about how I use Trello outside of the workplace. It helps greatly in keeping consistent, which is a giant step in trying to achieve any kind of success, no matter how small. Perhaps I'll elaborate on this sometime later in the year.

My blogging and coding tasks are now scheduled via Trello. I'm having a blast, though a one-man experience for Trello probably is not conducive to training for a real team environment. Still, it's something.

Audio Social Media

A large part of last year and the entirety of this year was spent weaning myself off of Facebook and using the app known as Clubhouse. And to a lesser extent, LinkedIn. If I'm to be perfectly honest, my distancing from Facebook began more than a few years back, and my introduction to Clubhouse pretty much accelerated the process.


On Clubhouse, I've found myself moving in several circles - American, African, Japanese, Chinese and Malaysian - and the subsequent conversations I've had, have spawned a multitude of ideas. These ideas sometimes translate into blogposts (see Ten Lessons From The Art Of War Applied To Software Development and Ten Pieces Of African Wisdom In Software Development) or code projects (notably The Japanese Language Trainer App). Upon the relaxation of border controls, I've even had friends from Clubhouse come to Singapore to meet up offline.

All in all, my time on Clubhouse has been pretty fruitful, despite it often looking otherwise.

LinkedIn

As mentioned previously, there has been a conscious move from Facebook to LinkedIn. A large part of this reason is audience.

While I used to share completed blogposts on Facebook, at some point I began to realize that I was preaching to the wrong crowd. Facebook users pretty much just want to watch funny cat videos and share memes. The content of my blogposts can sometimes be very technical, and consequently pretty dry. I've found that sharing my blogposts on LinkedIn, where the majority of my connections are professional rather than personal, have gained greater traction.


Now, when I have blogposts that add value to my professional profile, I share them on LinkedIn. My rather more spicy rants about employers have been confined to Facebook, where they gain plenty of traction on their own.

Next

Let's take a look at how popular some of this year's blogposts were.

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.