The Testing Academy · Class Notes Saturday, 29 August (IST)
Live class · study guide

Encapsulation, inheritance, and the super that quietly returns undefined

The end of the OOP run. Encapsulation taught as control rather than hiding, with a bank account that only a cashier can change. Then inheritance: the five shapes, what super reaches and what it silently does not, and method overriding. All of it lands on the BasePage to LoginPage shape you will write in Playwright next month.

By Pramod Dutta, The Testing Academy. Study notes from the live Playwright 3x class, rebuilt from the session recording and cross-checked against chapter 21 of LearningPlaywright3x, pushed during the session. Every example was executed on Node v22.22.3 before publishing, including one behaviour the class flagged as odd, where running it gives a cleaner explanation than the one offered live.

01

Where this sits

Last session covered constructors, private fields and static. This one finishes OOP with encapsulation and inheritance, and it is the last JavaScript-only session: TypeScript and then Playwright itself come next.

Today's code is in 21_OOPs_Ecapsulation/, files 169 to 175, pushed during the session.

02

Encapsulation is control, not hiding

The textbook definition is "hide the internals" or "wrap data and functions together", and the session was blunt that this explains nothing. The analogy used instead: your family is a class, your children are fields, and a stranger is outside code. The stranger does not get to reach a child directly. They go through you.

So the useful definition is not about hiding. It is about deciding who is allowed to read and who is allowed to write.

class ICICI #balance getBalance setBalance the only two doors in Outside code read anyone write only if isCashier direct SyntaxError The class decides the rules, not the caller.
Reading is open. Writing has a condition. That asymmetry is the whole idea.
JavaScript
class ICICI {
    #balance;

    constructor(name, balance) {
        this.#balance = balance;
        this.name = name;             // public, no hash
    }

    getBalance() { return this.#balance; }

    setBalance(balance, isCashier) {
        if (isCashier) {
            this.#balance = balance;
        } else {
            console.log("Not allowed");
        }
    }
}

Run on Node 22, exactly as the file does it:

Call Result
pramod.getBalance() 1000
pramod.setBalance(10000000, false) prints Not allowed, balance unchanged
pramod.getBalance() still 1000
pramod.setBalance(300000, true) balance becomes 300000
pramod.#balance from outside SyntaxError: Private field '#balance' must be declared in an enclosing class

Three rules, in the order they were given. Encapsulation works inside a class. Keep the variables private. Reach them only through getters and setters. And a fourth worth adding: getters and setters are for private fields. Writing them for a field that is already public is ceremony with no effect, which is why name in the example has none.

03

The real setter is not always the one called set

The second example is the one to take to an interview.

JavaScript
class TestCase {
    #status = "not run";
    static #count = 0;

    constructor(name) {
        this.name = name;
        TestCase.#count++;
    }

    run(pass) { this.#status = pass ? "PASSED" : "FAILED"; }

    getStatus()       { return this.#status; }
    static getCount() { return TestCase.#count; }
}

There is no setStatus. You cannot set the status directly at all, and that is deliberate: run() is the real setter. A status is something a test earns by running, not something a caller announces. Encapsulation is what lets you say that in code rather than in a code review.

Three things verified by running it. tc.run(true) then tc.getStatus() gives "PASSED". After four objects, TestCase.getCount() gives 4, because the counter is on the class. And TestCase.#count from outside is a SyntaxError, because a static private field is still private.

The trap: tc.getCount() throws TypeError: tc.getCount is not a function. getCount is static, so it lives on the class and not on instances. Same rule as last session's pramod_fn, pointing the other way.

04

Inheritance, and the five shapes

SingleMulti-levelHierarchicalMultipleHybrid today today later not in JavaScript: a class extends one parent Single and hierarchical are the two that matter, because hierarchical is the shape a Playwright framework has.
Five names, two that you will actually write this month.

The analogy for why a child can use a parent's methods: you are allowed to use your father's house.

05

What `super` reaches, and what it does not

JavaScript
class Animal {
    constructor(name) { this.name = name; }
    eat()  { return this.name + " is eating"; }
    foo()  { return "Animal.foo"; }
}

class Dog extends Animal {
    constructor(name, breed) {
        super(name);              // calls the parent constructor
        this.breed = breed;
    }
    bark() { return this.name + " barks, and " + super.foo(); }   // calls the parent method
}

Verified on Node 22 with new Dog("Rex", "Labrador"):

Expression Result
d.name "Rex", so super(name) reached the parent constructor
d.eat() "Rex is eating", inherited method, using the field the parent set
d.bark() "Rex barks, and Animal.foo", so super.foo() reaches the parent method
super.name inside Dog undefined

That last row was flagged in class as a quirk. Running it gives a cleaner reason.

super. looks up the prototype chain. Instance fields are not on it. this.name is an own property of the object, created when the constructor ran. super.name looks on Animal.prototype, where no name exists. Confirmed both halves: the object has an own name, and Animal.prototype does not have one. So super reaches methods and the parent constructor, and never a field assigned with this. The rule is not "as of now", it is structural.

06

Method overriding

If the child defines a method the parent already has, the child's wins when you call it on a child object.

JavaScript
class BaseTest { setup() { return "BaseTest.setup"; } }
class UITest extends BaseTest { setup() { return "UITest.setup"; } }
class APITest extends BaseTest { }                  // no override

new UITest().setup();    // "UITest.setup"
new APITest().setup();   // "BaseTest.setup"

Both verified. The phrase used in class was जिसकी लाठी उसकी भैंस, whoever holds the stick owns the buffalo: whichever class the object belongs to, that class's method wins. Remove the override and it falls back up the chain.

07

The shape you will write in Playwright

This is why the session ends here rather than on a dog.

What you practised today BaseTest UITestAPITestE2ETest setup and teardown written once is What you will write next month BasePage LoginPageInventoryPageCartPage page, locator wrapper and logger wired once Same diagram, different nouns. Hierarchical inheritance is what a page object model is.
The 2x batch already built the right-hand side. You now know why it is shaped that way.

Two more things the session settled:

  • Stick to plain getX() and setX() methods. JavaScript also has get and set keyword accessors, and they work, but the class is standardising on ordinary methods because JavaScript offers several syntaxes for the same thing and consistency is worth more than cleverness here.
  • Private methods are a TypeScript topic. Private fields with # are JavaScript today; the private keyword on methods comes with the TypeScript conversion.
08

Tasks and announcements