Showing posts with label Japanese. Show all posts
Showing posts with label Japanese. Show all posts

Wednesday, 15 April 2026

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

This next episode, the rather pretentiously titled Bête Noire, deals with gaslighting. Tech? Well, there's plenty. Maybe even too much.

The Premise

Maria starts to question her reality when ex-schoolmate, Verity, joins her company Ditta (which had a brief mention in the previous episode; fun fact!). She soon finds out that reality is being manipulated with the intention of driving her to suicide.

The Characters

Sienna Kelley is Maria Skinner. She delivers one hell of a portrayal of an argumentative control freak who always needs to be right, the not-so-reformed bully who instinctively reverts to form once the conditions present themselves. It's sometimes subtle, but Kelley pulls it off. It helps that the argumentative control freak personality is absolutely a thing, and I know more than a few people like that.

Rosy McEwen is Verity Greene. It's also a compelling performance from her, perhaps even more so considering the rather unbelievable premise. She's a victim of bullying who finds near limitless power but decides to spend it tormenting her former bullies. I find this thoroughly unrelatable because in her shoes, I'd be busy exploring so many possibilities and cementing my position. Despite this, McEwen manages to elicit sympathy.

Ben Baily Smith as Gabe, the boss. I found his portrayal as a laid-back hill boss, utterly watchable. Gabe bikes to work and comes off as chummy and sensitive, and tries to be reasonable and tolerant. Which can be tough if you have a pushy mofo like Maria as a subordinate. Just saying!

Michael Workéyè plays Kae, Maria's boyfriend. He's nice and goofy, and really quite the emotionally available guy. Another guy who tries to be reasonable with Maria and puts up with her bullshit. I'm starting to see a pattern!

Ben Ashenden as Nick, from the Graphics department. Comes across as enthusiastically friendly where Verity's concerned. Perhaps even over-friendly. Later on, he pushes Maria's buttons in very annoying ways. Overall, a rather immature character, but the actor looked like he had fun.

Elena Sanz as Camille, focus group tester. She reminded me a little of Gemma from M3GAN. Thought she'd have a bigger role here, but it was not to be.

Hannah Griffiths as Luisa. There's this running gag where people constantly steal her almond milk from the fridge. It druves her nuts, and ties into the story.

Amber Grappy as Yudy, the kitchen head. She always has this confused look. I don't know if that's by design.

Ravi Aujla has a brief appearance as Ditta. Good-looking distinguished silver fox guy. Not really interesting otherwise.

The Mood

The episode is bright and colorful, and the closeup visuals of chocolate and confections sure add to the artistry. Later on, this does not change, though the mood takes a turn for the sinister, which somehow gets worse considering everything is so visually... cheerful. In effect, the entire episode looks very polished visually.

What I liked

The storyline concept was pretty creative, even if it strained credulity at times. The themes of bullying, gaslighting and arrested development are pretty relevant and dare I say, timeless.

I like the visuals where they tell us what day of the week it is. It's just so artistic.


The actual gaslighting was pretty good! It started off subtle, with this foreshadowing shot that it was Barney's rather than Bernies... also, there are apparently two versions of this episode being aired, with this as a gag!


... to something like this, removing nut allergies from existence! And using Google to reinforce it, is just too precious!




What I didn't

Of all the titles they could've gone with that would have actually made sense, "Bête Noire" doesn't exactly stand out as a solid choice.

Unlikeable characters. Both the protagonist and the antagonist are anything but likeable, and that's even before the reveal at the end that Maria was Verity's bully. Maria is pushy and argumentative. Verity comes off as a tragic victim of bullying who's unable to move past the trauma and as such is in a state of arrested development. No main character comes even close to being sympathetic here.


Unbelievable tech. I mean... something as limitless as the tech that Verity is using, basically runs on what looks like a mini server farm? I suppose it's marginally more believable than that little "quamputer" we saw in Joan Is Awful.

The scene where Verity alters reality so that Maria has always spoken "Chinese". Honestly, if she's "always spoken Chinese", she should speak it a lot better than the garbage gibberish that came out of Maria's mouth. Badly-spoken Mandarin has always been a pet peeve of mine in Hollywood. Couldn't they have used Japanese, or Korean? Something arguably less easy to get wrong? Geez!

Conclusion

A mixed bag. It was a good gaslighting-style story with just enough corniness to make it enjoyable. And even with the rubbish they tried to pass off as Mandarin, the good outweighed the bad here. Solid episode.

My Rating

7.5 / 10

Next

Hotel Reverie

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

Tuesday, 17 September 2024

App Review: Duolingo

Today, I want to review an app that I've been using for the past couple years. Duolingo is a language-learning app that provides lessons on reading, writing, pronounciation and grammar, for over forty different languages.


For the past year, I have been regularly using Duolingo to work on my Japanese. As with most other activities, prolonged use comes with diminishing returns. However, Duolingo still represents significant value for me, and I'd like to examine it in some detail.

The Premise

When you sign up for an account, Duolingo tests you in that language to assess how far along you are in your learning journey. From there, it places you either right at the beginning, or somewhere further along the path.



From there, you complete modules that have been grouped into two basic groups - reading and writing, and grammar.

In the case of Japanese, reading and writing is further sub-categorized into Hiragana, Katakana and Kanji. This will take the form of character recognition, and actual writing.

For grammar, it's more along the lines of forming sentences, learning new words and concepts, and translating from Japanese to English. And recently, they even added speaking lessons!



Duolingo is partially gamified. As the users learn, they gain XP which can be used for extra challenges and unlocking features like Personalized Planning. Users are also placed in Leagues - Copper, Silver, Gold, Sapphire, Ruby, Emerald, Amethyst, Pearl, Obsidian, Diamond - and even a sort of Elite-level league within the Diamond League.

The Aesthetics

The layouts are clean and straightforward, with a cutesey bent. Check out the art on these medals!


The animations are pretty cute, especially at the end of each lesson.

The Experience

Even just exploring the app for the first time, I navigated it with ease. I managed to find extra information by clicking on stuff during exercises, such as explanatory text.




Also, there were charts and wordy descriptions hidden away, accessible by clicking the correct screen elements to open them up. They were almost a bit too well hidden, because I only really discovered them half a year into using Duolingo.

All in all, this did not require a huge learning curve to use.

The Interface




The user tends to be clicking buttons on most of the screens. It's mostly arranging words to form sentences or multiple-choice questions.


In the case of writing exercises, there's a drawing component.


For speaking exercises, tap the mic and read the text provided. It's all pretty simple, hard to go wrong.

What I liked

This app keeps getting revised as the creators refine their teaching techniques, adding new question types every now and then.

I love this particular puzzle. It's mad (just like its name), yet so addictive.




This timed challenge isn't bad either.


This avatar maker seems totally superfluous if I'm being honest, but I totally love it!


I like the fact that Listening and Speaking exercises are totally optional and can be turned off when encountered.


While I'm not wild about the concept of Hearts (you need at least 1 Heart to do the lessons), there's at least a practice option that can be used without Hearts, to earn Hearts. I understand why it is the way it is, because Duolingo is ultimately a business and users need to be encouraged to use the Pro version. Doesn't stop it from being annoying on occasion!


Friend Quests are great. Nothing like teaming up with someone else once a week, to meet some objective or other. This is so cool!


Gems. While I think they're kind of superfluous, I can't seem to stop obsessively collecting them by doing challenges, completing Quests, watching ads, and so on. I tend to console myself that at least with all this ad watching, I'm making up for the fact that I haven't yet spent a cent on this app.


I live for Duolingo telling me I've extended my streak!


The concept of Super is nice. It adds considerable value to users who have money to spare.


Just the entire concept of Duolingo, to be honest. It's hard to have negative thoughts about an app whose intention is to educate. The intention is to be lauded, both in terms of intent and scope. Over forty languages is nothing to sniff at.

What I didn't

Some of the voices are extremely annoying. The voices of the kids - and this may be just my opinion - are high, shrill, and somehow louder.

I like encouraging my friends and all, but having a list of different emojis to do it with, feels like overkill.




Having a listening Daily Quest seems a little insensitive towards the deaf.


One main limitation of Duolingo is that it uses English almost exclusively as a base. If you are trying to learn English, then yes, you can use one of the other languages as a base. However, you can't, for example, learn Japanese using Mandarin.

Sometimes there are glitches in the interface. Mostly layout problems, probably easily fixed.

I think the concept of using English to type in romanization for sounds, is more than a little annoying when you factor in Autocorrect. Plus, I really don't think it adds much to the education.


Just the general gamification of the experience seems a little off, somehow. I suspect the app would be better with its exclusion. In particular, I detest the Leaderboard.




The entire concept of XP also gives me the eye-rolls. This goes for related functionality like Legendary, Saturday XP Bonus Hour,  and so on. A guy just wants to study Japanese, why do I need this? Why does anyone need this? It's a distraction, nothing more.

There's also an issue with Duolingo that has been making the rounds on Social Media a while now. And that's the inclusion of LGBT themes in the training material. Before anyone gets their panties in a bunch, I think inclusivity is a wonderful thing, and I certainly won't criticize it on a platform that I'm not even paying for. However, the heavy-handed way Duolingo goes about it, is problematic.


Take the above screenshot, for instance. It's that of a butch-looking woman (with a woman's voice) telling us about her wife. Now, butch lesbians exist because LGBT is a fact of life regardless of legality. Muslim users can complain all they like; it is what it is. But I do object to sentences such as "My older brother's husband is Canadian.".

You see, same-sex marriage has not been recognized in Japan as of now. That is also a fact of life. And to act like it's mainstream in Japan via means of the training sentences, strikes me as supremely disrespectful of another country's right to determine their own laws without outside interference. The makers of Duolingo are not Japanese, are not natively familiar with the culture, and have no business trying to impose their ideals here no matter how noble their stated intentions seem. As of last June, they've already gotten into trouble with Russia about this. 

Conclusion

This app is great for learning a new language. It won't give you native-level fluency, but it will absolutely take you from zero to some significant level of proficiency provided one uses it diligently and consistently. Duolingo is probably excellent as a supplementary means of learning. Or even a main means of learning if, like me, you're just a casual.

Great as the concept is, however, there remain several implementation problems that, in all fairness, are ironed out with regularity. With the regular addition of new features, inevitably, there will other implementation issues, but hey, it won't get stagnant. That's a huge plus!

Regardless of what I think about Duolingo's attempt at incorporating progressive politics (and make no mistake, I think it's misguided at best), Duolingo remains one of the most recognized language-learning aids today. And if they sensibly keep those politics as a feature rather than the feature, this should not change anytime soon.

My Rating

7.5 / 10

This app speaks to me!
T___T

Sunday, 14 April 2024

How worried should software developers be about Devin AI? (Part 1/2)

It's time to talk about a couple things, regarding AI. All of which have to do with some recent occurences in the software industry.

First of all, Devin AI. It's been the talk of the software industry in recent weeks.


Devin AI is the world's "first AI Software Engineer" produced by Cognition AI. It's an automated system that performs like your average web developer - code, trawl the web for solutions, read documentation, test code and produce reports, among a myriad of other tasks. Except that being a machine, it does all these a hundred times faster.

Users feed it prompts in natural language. It then produces codes, tests and documentation, based on those prompts. Sounding good, so far? Astounding, even. Words like "groundbreaking" and "game-changing" have been used. Of course, those same words were used when Bitcoin came on the scene, followed by NFTs, and then the Metaverse. I'm not saying that this development will go the same way, but recent history has made it hard not to be skeptical.

For software developers, Devin AI sounds like a useful tool, or a even a nice virtual member of the team. Something that can take care of the tedious grunt work while you concentrate on better things.

For employers, it looks like huge potential savings in terms of developer wages. Hey, why pay developers if an AI can take care of the bulk of their work? And if we're being honest, we all know a team member or two who could stand to be replaced by AI.

Reports tell us that Devin AI has managed to solve a whopping 14% of software problems it was given. This is not sarcasm; I know it doesn't sound terribly impressive especially since we don't know if those problems were FizzBuzz level or banking application level, but bear in mind that it can only get better from there.

The use of natural language to communicate requirements to Devin AI sounds potentially shaky. Seasoned software devs would probably know the correct terms to use in order to get results, but non-technical users are going to have a rough go of it. If even fellow human beings sometimes find it difficult to effectively communicate due to language and cultural barriers (even when they're using the same language and are from the same culture) I can't imagine it being any easier for AI. Granted, I could simply be suffering from a failure of imagination.

Jensen Huang

At Dubai earlier in February, Nvidia CEO Jensen Huang made waves with a speech, which seem to have raised some hackles in the software development industry.


"I'm going to say something and it's going to sound completely opposite of what people feel. You probably recall over the course of the last ten years, fifteen years, almost everybody who sits on a stage like this would tell you it is vital that your children learn computer science. Everybody should learn how to program and in fact it's almost exactly the opposite. It is our job to create computing technology such that nobody has to program and that the programming language is human. Everybody in the world is now a programmer. This is the miracle. This is the miracle of Artificial Intelligence. For the very first time we have closed the gap. The technology divide has been completely closed and this the reason why so many people can engage Artificial Intelligence. It is the reason why every single government, every single industrial conference, every single company is talking about Artificial Intelligence today, because for the very first time you can imagine everybody in your company being a technologist..."


I'll begin by saying that he certainly seems to be saying things that investors and shareholders are going to find appealing. As to how feasible it all is, well... at the risk of sounding like one of those developer elitists I hate, I'm tempted to dismiss that speech also because he doesn't seem to have any relevant software development process. Speechmakers waxing lyrical about things they don't know about is an unfortunate habit which isn't all that uncommon. But hey, for all I know, this guy could be a coding genius, so let's not go there.

Let's be honest. Software developers can be a tremendous pain in the ass to put up with. We can be temperamental, egoistical (a prime example being all the whining I hear online about the audacity of trying to automate our jobs), and have this utterly annoying air of superiority due to being the technical person in the room. And in cases of non-tech companies with very traditional wage structures, trying to pay us competitively while not compromising those structures can be an almighty struggle. Not gonna lie - I can absolutely see the appeal.

Being able to write apps without having to deal with software developers or do the work of learning to code? Pretty much the layperson's tech wet dream.

Then again, I would put significantly more stock in that speech if Jensen Huang were to put his money where his mouth was. Why stop at removing software developers? Why not replace his entire development team at Nvidia with Artificial Intelligence, to produce more Artificial Intelligence?

All this seems like marketing speak. Because there's no way Huang could have meant it all in earnest. My best guess is, this speech was meant for stakeholders who don't necessarily know any better. Sure, AI will generate code for us. That's not new. You know, whenever I want to make an API endpoint call, I use existing software to generate the code for me, in Python or JavaScript or PHP, because I can't be bothered to remember the entire damn sequence in multiple programming languages. Have I lost my job because I don't have to write that code any more? No, it simply means I can work faster and not get bogged down by minutia. Just because something else wrote the code for me doesn't mean I don't have to verify it. And in order to verify it, I need to be able to understand the code.

I've been learning Japanese kanji. Just for fun, you understand. Take the Japanese kanji below. It means "Sunday".
日曜日


The middle character is one of the more complicated ones I learned in the past year. Can I write it? Have I committed it to memory? No, and no. But do I need to? My phone types it out just fine, and I can recognize it right off the bat. Someone who has only studied Chinese characters (like me, two years ago) would read that entire sequence as "day bright day". But someone who hasn't studied Chinese characters or Japanese kanji wouldn't recognize it at all.

There's the analogy. Software devs looking at computer-generated code have a decent chance of understanding it. People who have had a bit of related training might misunderstand the code. If non-tech people look at code,  they're likely to see only gibberish. 

If AI is supposed to generate code, and no one has learned how to code, no one can understand what AI is doing. Still think people should not learn to code?

However, Huang has a real point when he implies that coding as we know it, is about to change forever. That's because with or without AI, the nature of coding has always evolved. In the 1960s, programmers were using punch cards on mainframes to code. Are we doing that now? Later on, languages like COBOL  came into being. How many of us are still using them now? Now we have frameworks and fancy tools to do most of the heavy lifting. We don't even need to indent our own code anymore - we have linters for that.

Get comfortable with change. It's exactly what our industry produces.

Next

Examining the pros and cons of AI.

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