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.
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.
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
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.
`this`, when there are two objects
this is the current object, decided at call time. That is what lets one class serve every instance.
Public and private fields
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.
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.
Static: belongs to the class, not the object
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
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.
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:
--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
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.
The same class in TypeScript
The conversion shown in class, on a real page object:
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.
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.
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.