The Testing Academy · Class Notes Thursday, 27 August (IST)
Live class · study guide

Constructors, private fields, and the one keyword to avoid in Playwright

The middle of the OOP run. What a constructor actually is and who calls it, what this binds to when two objects exist, #private versus public fields, and static. Ends on the rule that matters most once you reach Playwright: static state and parallel tests do not mix, and the reason is sharper than "it breaks".

By Pramod Dutta, The Testing Academy. Study notes from the live Playwright 3x class, rebuilt from the session recording and cross-checked against the code pushed to LearningPlaywright3x during the session. Every claim was executed on Node v22.22.3 before publishing, and the advice about static in Playwright was tested against real Playwright workers rather than repeated, which changed how this page states it.

01

Where this sits

Second session on classes. Tuesday covered what a class and an object are; today fills in the parts that make one usable.

Everything below is in the repository, pushed during the session:

Folder Covers
20_Class_Object_OOPs/01_Class_Object/ Classes, objects, and constructors, files 158 to 161
20_Class_Object_OOPs/02_Public_Private/ Public and #private fields, files 162 to 164
20_Class_Object_OOPs/03_Static_JS/ Static fields and methods, files 165 to 168

Inheritance, polymorphism, encapsulation and abstraction come Saturday, then Playwright architecture and locators next week.

02

The constructor, and who calls it

A constructor is a special method that runs automatically when an object is created. You never call it yourself.

JavaScript
class TestCase {
    constructor(name, status, priority) {
        this.name = name;
        this.status = status;
        this.priority = priority;
    }

    display() {
        console.log(this.name + " -> " + this.status + " -> " + this.priority);
    }
}

let loginTest_ref  = new TestCase("Login Test",  "PASS", "P0");
let signupTest_ref = new TestCase("Signup Test", "FAIL", "P1");

loginTest_ref.display();    // Login Test -> PASS -> P0
signupTest_ref.display();   // Signup Test -> FAIL -> P1
new TestCase("Login Test", "PASS", "P0") 1. new2. constructor3. this.x = ...4. handed back empty object maderuns automaticallyfields assignedyou get the object You never write loginTest_ref.constructor(). The word new is what calls it. In Java the constructor is named after the class. In JavaScript the keyword is literally constructor. In Python it is __init__. Same job, three spellings.
The analogy from class: a name gets assigned the moment someone is born, without them asking.

Two kinds, and the rule that catches Java people:

  • A default constructor takes no parameters and just runs.
  • A parameterised constructor takes arguments and sets up the object from them.
  • JavaScript allows only one constructor per class. No overloading. If you want two ways to build an object, give the parameters defaults or write a static factory.
  • Writing no constructor at all is fine.
03

`this`, when there are two objects

this is the current object, decided at call time. That is what lets one class serve every instance.

class TestCase this.name = name written once new new loginTest_ref name: "Login Test" status: "PASS" signupTest_ref name: "Signup Test" status: "FAIL" During this call, this is this object. During this one, this is this one. Separate memory. Changing one object's name cannot touch the other's.
One template, many instances, and no collision. That is the whole point of the keyword.
04

Public and private fields

JavaScript
class Credentials {
    #apiKey;          // private, the hash is part of the name
    user;             // public

    constructor(user, key) {
        this.user = user;
        this.#apiKey = key;
    }

    getAuthHeader() {
        return "Bearer " + this.#apiKey;
    }
}

let cred = new Credentials("admin", "secret_key_1234");

Executed on Node 22, because the two failure modes are different and the difference is the interesting part:

From outside the class Result
cred.user "admin"
cred.apiKey undefined, not an error
cred.#apiKey SyntaxError: Private field '#apiKey' must be declared in an enclosing class
Object.keys(cred) ["user"]
JSON.stringify(cred) {"user":"admin"}
cred.getAuthHeader() "Bearer secret_key_1234"

The row worth remembering is the second one. cred.apiKey gives you undefined rather than complaining, because you asked for a public property called apiKey and there is no such thing; the private field is named #apiKey and the hash is part of the name. So a typo in a private field name fails silently, while reaching for the real one from outside fails loudly at parse time. The private field is genuinely invisible: it is absent from Object.keys and from JSON.stringify.

class Credentials user public #apiKey private a method inside can read both From outside cred.user "admin" cred.apiKey undefined, no error cred.#apiKey SyntaxError at parse time
Two different failures. Only one of them tells you something is wrong.
05

What JavaScript does not give you

Three things came up as questions, and all three have the same answer:

  • protected: TypeScript only.
  • readonly: TypeScript only.
  • A declared type: TypeScript only.

And one rule that surprises people coming from other languages: let and const are not allowed on class fields. Inside a class body you write the field name on its own. let name; there is a syntax error, though let outside the class is ordinary.

The session was blunt about why this matters: these gaps are the argument for TypeScript, and the conversion is smaller than people fear.

06

Static: belongs to the class, not the object

JavaScript
class TestRunner {
    static totalTests = 0;
    static passCount = 0;

    constructor(name, passed) {
        this.name = name;                 // per object
        TestRunner.totalTests++;          // shared
        if (passed) TestRunner.passCount++;
    }

    pramod_fn()      { return this.name; }                                  // non-static
    static summary() { return TestRunner.passCount + "/" + TestRunner.totalTests + " passed"; }
}

new TestRunner("Login", true);
new TestRunner("Signup", false);
new TestRunner("Cart", true);
new TestRunner("Checkout", true);

console.log(TestRunner.summary());   // 3/4 passed
TestRunner.totalTests one slot, on the class name: "Login"name: "Signup"name: "Cart"name: "Checkout" its own slotits own slotits own slotits own slot every object increments the same counter name is per object. totalTests is per class. That is the entire distinction.
Four objects, four names, one counter.

Interview question straight from file 166, where the line sits commented out. What happens if you call TestRunner.pramod_fn()? TypeError: TestRunner.pramod_fn is not a function. A non-static method belongs to objects, so you need an instance: new TestRunner("x", true).pramod_fn() returns "x". The reverse is why summary() is static, so it can be called without making an object first.

07

Static and Playwright: the rule to get right

Asked live: should Playwright page objects use static methods? The answer given was no, avoid static in Playwright, keep only the URL static, because shared state and parallel execution do not mix. The image used was one book between many students.

That advice is correct. The mechanism is worth being precise about, because it is not what most people assume, and I tested it rather than repeat it.

Playwright runs tests in separate worker processes. Each worker loads its own copy of your modules, so a static field is not shared across the suite at all. It is shared within a worker.

Two spec files, each with two tests, each incrementing TestRunner.totalTests. Four increments in total:

Text
--workers=2
  PID 36856  after t1: totalTests=1     PID 36856  after t1: totalTests=2
  PID 36857  after t2: totalTests=1     PID 36857  after t2: totalTests=2

--workers=1
  PID 36923  totalTests=1 -> 2 -> 3 -> 4
--workers=2, four increments worker process 1 its own TestRunner.totalTests reaches 2 worker process 2 its own TestRunner.totalTests reaches 2 Neither ever reaches 4. No error either. --workers=1, same four increments one worker process 1 -> 2 -> 3 -> 4, correct Same code, same suite, different answer.
Measured on Playwright 1.62.1, with the process ids printed to prove they are different processes.

The precise version of the advice. Static state in a Playwright suite does not force your tests to run serially, and it does not crash. It quietly gives each worker its own copy, so any static counter or cache reports a fraction of the truth, and the number changes when you change --workers. A test that passes on your laptop and fails in CI on a different worker count is the shape this bug takes. Keep the URL static, because a constant is safe to duplicate. Keep everything with state out of it.

08

The same class in TypeScript

The conversion shown in class, on a real page object:

TypeScript
export class LoginPage {
    static readonly URL = '/login';                 // a constant, safe to share

    private readonly username: Locator;             // # becomes private, plus a type
    private readonly password: Locator;

    constructor(page: Page) {
        this.username = page.locator('[data-test="username"]');
        this.password = page.locator('[data-test="password"]');
    }

    async login(user: string, pass: string): Promise<void> {   // async, non-static
        await this.username.fill(user);
        await this.password.fill(pass);
    }
}

Everything you already know carries over. What TypeScript adds is private in place of #, a type on each field, types on parameters and return values, and readonly. The structure is identical.

09

Questions from the floor

Can a constructor call another constructor? No, and not in Java either. Chaining to a parent constructor with super() is a different thing, and that is Saturday.

Do I need let on a class field? No, and it is a syntax error inside the class body. Outside a class, let is normal.

Can the constructor create a field that was never declared at the top? Yes in JavaScript, and in Python. Declaring the fields anyway is the better habit and is what TypeScript will make you do.

Is a global variable readable inside a class? Yes. A variable declared outside is in scope inside the class body. A bare name that was never declared is not, and gives ReferenceError: B is not defined.

Why is summary() static but pramod_fn() not? So summary() can be called on the class without creating an object. If it were not static you would need an instance just to print a total.

10

Tasks and announcements

  • Try the access questions yourself before Saturday. The habit being taught is to predict the answer, then run it. Files 163 and 166 are written as exactly these puzzles.
  • Saturday: inheritance, polymorphism, encapsulation and abstraction, finishing OOP.
  • Next week: Playwright itself, starting with architecture and locator strategies.
  • Claude Code 101 masterclass tonight, 8:00 PM IST, around 30 to 40 minutes. Study guide here: Claude Code 101.
  • Push your practice with git add, git commit -m "...", git push.
  • Code for today: 20_Class_Object_OOPs in the batch repository.
  • Previous: export, import and the first class.