|

Day 8: JavaScript OOP and Inheritance — From Class to BasePage

Compact Page Object Model diagram: LoginPage extends BasePage

This is Day 8 of the 21-day JS to Playwright Framework series. One lesson a day. JavaScript first. TypeScript next. Playwright after that. A production framework by Day 21.

On Day 1 we put values in boxes. On Day 2 we compared them. On Day 3 we decided. On Day 4 we walked a list. On Day 5 we wrapped the walk in a function. On Day 6 we grouped values into objects. On Day 7 we waited — callbacks, promises, async/await. Today we name the thing that owns the behaviour: a class. Then we reuse that class with inheritance. Then we export the parent and the child so a spec can say new LoginPage() and call open() it never defined.

A Page Object is not a Playwright feature. Playwright does not ship BasePage. You write a class. You put goto, click, fill, and screenshot helpers on the parent. You put login(user, password) on the child. The spec imports the child. That is today’s whole job. If you skip this day, you will paste page.goto(baseURL) into forty specs, change the login URL once, and spend a Friday grepping.

I am Pramod Dutta. I teach SDETs in India for a living. Week six of my live batch is not “open Playwright and invent a POM from a blog.” It is class Person, then #apiKey, then static summary(), then class LoginPage extends BasePage, then three files in Exporting_Class/. I fetched every lab from LearningPlaywrightBatch on branch main. I am not inventing files. I quote the blobs below. Folder names, typos, empty skeletons — I keep them.

Two names you must type as they sit on GitHub:

  • EXPORT_IMPORT/152_Loggger.js — three gs.
  • Exporting_Class/Basepage.js — lowercase p in page. LoginPage.js imports ./Basepage.js. If you invent BasePage.js, the import breaks.

Hierarchial_Inheritance/174_HI.js is an 80-byte skeleton: Father, Son1, Son2. I will not invent methods for it. I say so when we reach it.

The root package.json on main has Playwright ^1.58.2 and does not set "type": "module". Files 153–163 and 164–172 and 174 are plain class scripts — node runs them. Files 150–152 plus logger.js / testutil.js / utils.js, and the three Exporting_Class files, use import / export. If Node says Cannot use import statement outside a module, that is this gap. I will not invent a folder package.json to hide it. Quote the files. When we reach Playwright later in this series, the test runner already understands ESM.

*Want to master this with real projects? Join the Playwright Automation Mastery course at The Testing Academy.*

Compact Page Object Model diagram: LoginPage extends BasePage

Contents

Why JavaScript OOP comes before Playwright POM

Playwright gives you page. A page is already an object. page.goto, page.locator, page.screenshot are methods on that object. Your job as an SDET is not to call those methods from every spec. Your job is to wrap the app so a login change lives in one class.

That wrap is Object-Oriented Programming:

  • A class is the blueprint. LoginPage is not one login. It is the shape of every login you will construct.
  • An object is one instance. new LoginPage() in this spec. Another new LoginPage() in the next spec. Same class. Two objects.
  • Attributes are the data — name, baseURL, #apiKey, this.page later.
  • Behaviours are the methods — open(), login(user), verify(), getBalance().
  • Encapsulation hides what a spec must not touch — the API key, the raw balance, the engine name.
  • Inheritance is class LoginPage extends BasePage. The child gets open() without rewriting it.
  • Method overriding is the child writing its own verify() so a list of pages can all be verified the same way from the spec.
  • Export / import is how a class in pages/ reaches a spec in tests/.

I have interviewed enough SDETs to see the pattern. Someone can record a login with codegen and still cannot explain super(), #balance, or why class C extends A, B is a SyntaxError in JavaScript. That gap shows up later as a 400-line spec, a copied open() in twelve page files, a password sitting in a public field, and an interview whiteboard that asks “what is a Page Object?” while they draw a function.

Day 8 is the fix. You will run a Car. You will hide a bank balance. You will export a logger. You will extend BasePage. You will override verify(). You will import LoginPage into 173_Test_2.js. Then I show you why Playwright cares.

Tomorrow (Day 9) we put types on this same shape — TypeScript interface, enum, generics, private / protected / public. Today the language is still JavaScript.

What you will be able to do after Day 8

By the end of this post you can:

  1. Write a class with attributes and behaviours, construct it with new, and say what this is.
  2. Tell a function from a method — a method is a function that lives on the class.
  3. Hold two instances of the same class with different data — two browsers, two API clients, two test cases.
  4. Hide a field with # and expose it only through a method. Explain why cred.#apiKey is a SyntaxError and cred.apiKey is undefined.
  5. Use static for data that belongs to the class, not the instance — a pass-count, a college name, later a shared default timeout.
  6. Encapsulate with get/set, including a guard (isCashier) so a setter is not a public hole.
  7. export a named binding and a default, import with and without as, and explain why fname in testutil.js cannot be imported.
  8. Write class Child extends Parent, call super() in the constructor, and call super.method() when you override.
  9. Walk a list of subclasses and call the same method name — polymorphism — the way a suite walks Unit / API / E2E or Login / Dashboard / Cart.
  10. Say out loud: JavaScript has no class C extends A, B. Mixins in 172.js are the classroom stand-in.
  11. Export BasePage and LoginPage as separate files and drive them from a spec. That is the Playwright POM you will write on Day 16.

That is the skill. Not the keyword class. The skill is one parent for shared page behaviour, one child per screen, one export per file, and a spec that never rewrites open().

The labs we are actually using

Clone the repo and stay on main:

git clone https://github.com/PramodDutta/LearningPlaywrightBatch.git
cd LearningPlaywrightBatch
git checkout main

Chapter 16 — OOP (chapter_16_OOps/)

EXPORT_IMPORT:

  • EXPORT_IMPORT/150_Export_import.js
  • EXPORT_IMPORT/151_Export_Import.js
  • EXPORT_IMPORT/152_Loggger.js (filename is spelled that way)

Shared modules sitting next to those folders:

  • logger.js
  • testutil.js
  • utils.js

CLASS_OBJECT:

  • CLASS_OBJECT/153_Class_Objects.js
  • CLASS_OBJECT/154_Car.js
  • CLASS_OBJECT/155_Class_Object_Browser.js
  • CLASS_OBJECT/156_Browser.js
  • CLASS_OBJECT/157_IQ.js
  • CLASS_OBJECT/158_Private_Public.js
  • CLASS_OBJECT/159_Static.js
  • CLASS_OBJECT/160_Static_p2.js

Encapsulation (nested under CLASS_OBJECT):

  • CLASS_OBJECT/Encapsulation/161_Pramod_Child.js
  • CLASS_OBJECT/Encapsulation/162_Car.js
  • CLASS_OBJECT/Encapsulation/163_Bank.js

Chapter 17 — Inheritance (chapter_17_OOPs_Inheritance/)

Single:

  • Single_Inheritance/164_Inheritance.js
  • Single_Inheritance/165_SI.js
  • Single_Inheritance/166_Method_Overriding.js
  • Single_Inheritance/167_MO_IQ.js
  • Single_Inheritance/168_MO_BIG.js
  • Single_Inheritance/169_PageObject.js
  • Single_Inheritance/170_RO.js

Then:

  • Multi_Level_Inheritance/171_MI.js
  • Multiple_Inheritance/172.js
  • Exporting_Class/Basepage.js
  • Exporting_Class/LoginPage.js
  • Exporting_Class/173_Test_2.js
  • Hierarchial_Inheritance/174_HI.js

I teach class and object first (153–160), then encapsulation (161–163), then the modules those classes will live in (150–152 plus the three helpers), then inheritance (164–174). The folder numbers in chapter 16 put EXPORT_IMPORT before CLASS_OBJECT. In the classroom I flip that order so import { LoginPage } has a class to import.

You need Node.js 18 or newer. No Playwright config today. The Page Object you build is still console.log. The shape is the same shape you will later fill with this.page.

Class and object — CAB, constructor, and new

Lab 153: a class is attributes plus behaviour

Lab: chapter_16_OOps/CLASS_OBJECT/153_Class_Objects.js

Exact file on main:

class Person {
    // Attribute
    name;
    email;
    salary


    // Behaviour
    sleep() { }
    eat() { }

}

// CAB -> Class contains attribute, behaviour

Run it:

node chapter_16_OOps/CLASS_OBJECT/153_Class_Objects.js

Nothing prints. That is the lesson. A class is a declaration. It does not run until you construct an object. name, email, salary are fields. sleep() and eat() are empty methods. The comment at the bottom is the mnemonic I want you to keep: CAB — Class contains Attribute, Behaviour.

SDETs coming from Java look for public class Person and a main. JavaScript does not ask for that. One class block. Fields may be listed without this at the top of the class. Methods are written as sleep() { } — no function keyword.

A Playwright page object starts the same way: fields for locators or for this.page, methods for actions. Empty methods are legal. They are also useless until you fill them. Lab 153 is the empty box. Lab 154 puts a constructor in it.

Lab 154: constructor, this, and one object

Lab: chapter_16_OOps/CLASS_OBJECT/154_Car.js

Exact file:

class Car {
    // Attribute
    // Constructor
    constructor(assigned_name) {
        this.name = assigned_name;
    }
    drive() {
        console.log("Driving the car " + this.name);
    }
    printDetailsCar() {
        console.log("Details of  the car " + this.name);
    }

}

let hyndai_car = new Car("i10");
hyndai_car.drive();
node chapter_16_OOps/CLASS_OBJECT/154_Car.js

You should see:

Driving the car i10

Four things happen in this file.

First, constructor(assigned_name) runs when you write new Car("i10"). The argument "i10" becomes assigned_name. this.name = assigned_name stores it on this object. this is the instance you just created, not the class.

Second, drive() reads this.name. Without the constructor, this.name is undefined, and you print Driving the car undefined. I see that bug in page objects every batch: someone forgets this.page = page and then wonders why this.page.goto throws.

Third, new is not optional. Car("i10") without new is not how you construct a class instance. You want new Car("i10"). The variable hyndai_car holds the object. The filename spelling in the variable is hyndai — I will not “fix” the lab.

Fourth, printDetailsCar() exists and is never called. A class can have methods you do not use in this file. That is fine. A LoginPage will have forgotPassword() long before a spec calls it.

Playwright mapping: new LoginPage(page) is this lab with a different name. The constructor receives the Playwright page. Methods use this.page. If you skip the assignment, every method is a crash.

Lab 155: two objects, and method versus function

Lab: chapter_16_OOps/CLASS_OBJECT/155_Class_Object_Browser.js

The filename says Browser. The class on main is TestCase. I quote the file, not the filename.

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();


// p0 - learn
// p1 - pratice
// p2 - test / implement
// p3 - IPL

// Function vs Method
// method is functions but inside the class :)
node chapter_16_OOps/CLASS_OBJECT/155_Class_Object_Browser.js
Login Test → PASS → P0

Same class. Two objects. loginTest_ref is P0 PASS. signupTest_ref is P1 FAIL. They do not share status. Changing one does not change the other. That is the point of new twice.

We only call display() on the login object. The signup object is constructed and silent. Construction is not execution.

The comment at the bottom is the interview line: a method is a function, but inside the class. display outside a class is a function. display() on TestCase is a method. Playwright’s expect is a function you import. page.goto is a method on the page object. Stop saying “I called the goto function.” You called a method.

The P0–P3 comments are classroom priority, including the joke p3 - IPL. I leave them. They are in the blob.

Playwright mapping: one TestCase class, many instances, is how a reporter thinks. One LoginPage class, many instances across tests, is how a suite thinks. Do not make a global loginPage that every test mutates. Construct per test, or receive it from a fixture later in this series.

Lab 156: a Browser class, and a method body that lies

Lab: chapter_16_OOps/CLASS_OBJECT/156_Browser.js

Exact file:

class Browser {

    // Param constructor (arguments)
    constructor(name) {
        this.name = name;
        this.isOpen = true;
        console.log(name + " launched");
    }

    startBrowser() {
        console.log("starting the browser")
    }
    closeBrowser() {
        console.log("starting the browser")
    }

}

let chrome = new Browser("Chrome");
let firefox = new Browser("Firefox");

console.log(chrome.isOpen);
node chapter_16_OOps/CLASS_OBJECT/156_Browser.js
Chrome launched
Firefox launched
true

The constructor has a side effect. The moment you new Browser("Chrome"), it prints Chrome launched and sets isOpen = true. You did not call startBrowser(). Construction launched. That is a teaching choice. In Playwright you do not want new LoginPage(page) to navigate. Construction stores page. open() or goto() navigates. Keep those jobs separate or every import of the class hits the network.

chrome and firefox are two objects. chrome.isOpen is true because the constructor set it. We never print firefox.isOpen. It is also true. Same class. Same initial field. Different name.

Look at closeBrowser(). The body says "starting the browser". That is a classroom leftover. I will not rewrite the file in this post. I will tell you: a method name is a contract. If the body does not match the name, your future self will trust the name and ship a lie. When you write async close() on a Playwright page object, close the page. Do not paste the start body a second time.

Lab 157: two API clients, one class

Lab: chapter_16_OOps/CLASS_OBJECT/157_IQ.js

Exact file:

class APIClient {
    constructor(baseURL) {
        this.baseURL = baseURL;
    }

    get(path) {
        return this.baseURL + path;
    }
}

let staging = new APIClient("https://staging.api.com");
let prod = new APIClient("https://prod.api.com");

console.log(staging.get("/users"));
console.log(prod.get("/users"));
node chapter_16_OOps/CLASS_OBJECT/157_IQ.js
https://staging.api.com/users
https://prod.api.com/users

This is the IQ I actually ask. Same get("/users"). Different objects. Different baseURL. The method does not know about staging or prod. It knows this.baseURL.

If you write get as a loose function with a global baseURL, switching env means editing the function. If you write a class, switching env means constructing with a different string. That is Day 3’s env branch, now sitting in a constructor.

Playwright mapping: your API helper in the advanced framework (Day 19 in this series) is this file with request.get instead of string concat. Restful Booker staging versus prod is two instances, not two copies of the helper.

Interview answer, one sentence: the class is the client; the object is the environment.

Private, public, and static

Lab 158: #apiKey is hidden; user is not

Lab: chapter_16_OOps/CLASS_OBJECT/158_Private_Public.js

Exact file:

// Private Fields (#) — Hidden Data
// PUBIC Fields 
class Credentials {
    #apiKey;
    user;

    constructor(user, key) {
        this.user = user; // public
        this.#apiKey = key;
    }
    // Custom made fuction by us
    pramodgetAuthHeader() {
        return "Bearer " + this.#apiKey;
    }
}

let cred = new Credentials("admin", "scret_key_1234");
console.log(cred.user);
// console.log(cred.apiKey); undefined
// console.log(cred.#apiKey); //error

console.log(cred.pramodgetAuthHeader());

// cred.apiKey is undefined
// (it doesn't exist). 
// cred.#apiKey would throw a SyntaxError. 
// The ONLY way to access it is through the public method getAuthHeader()
node chapter_16_OOps/CLASS_OBJECT/158_Private_Public.js
admin
Bearer scret_key_1234

Read the comments in the file. They are the lesson.

  • user is a public field. cred.user prints admin.
  • #apiKey is a private field. You declare it with # on the class. You assign with this.#apiKey = key.
  • cred.apiKey is not the private field. It is a missing public property. Value: undefined. No throw.
  • cred.#apiKey from outside the class is a SyntaxError. The # name is only legal inside Credentials.
  • The only way to read the key is pramodgetAuthHeader(), which concatenates "Bearer " plus the hidden value.

The comment at the bottom says getAuthHeader(). The method is named pramodgetAuthHeader. Classroom naming. I will not invent a rename. The idea holds: a public method is the door; the field is not.

The constructor argument is "scret_key_1234" — that spelling is in the blob.

Playwright mapping: storage state paths, API tokens, webhook secrets. They do not live as this.apiKey on a page object that a spec can overwrite. They live as #apiKey or, later in TypeScript, private. The spec calls getAuthHeader() or a fixture injects the header. A junior SDET logging console.log(cred) should not dump the secret. Private fields do not enumerate the same way public ones do.

If you only remember one line from 158: undefined means you looked at the wrong name; SyntaxError means you tried to reach through the wall.

Lab 159: static belongs to the class

Lab: chapter_16_OOps/CLASS_OBJECT/159_Static.js

Exact file:

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

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

    }
    non_static_display() {
        return this.name;

    }
    static summary() {
        return TestRunner.passCount + "/" + TestRunner.totalTests + " passed";
    }

}
// Flow of the Amazon Website
new TestRunner("Login", true);
new TestRunner("Signup", false);
new TestRunner("Cart", true);
new TestRunner("Checkout", true);

console.log(TestRunner.summary());

// You call static with ClassName.method(), NOT object.method().
node chapter_16_OOps/CLASS_OBJECT/159_Static.js
3/4 passed

Four constructions. Three true. One false (Signup). static totalTests and static passCount live on TestRunner, not on Login or Cart. Every new increments the class counters. TestRunner.summary() reads those counters.

The last comment is the rule: ClassName.method(), not object.method(). new TestRunner("Login", true).summary is not how this file calls it. We never even keep the objects. new TestRunner(...) four times, throw the instances away, ask the class for the score.

non_static_display() exists and is never called. It would need an instance: (new TestRunner("Login", true)).non_static_display(). Static summary() does not.

Playwright mapping: a custom reporter’s totals are static-shaped — they belong to the run, not to one test. A shared BasePage.defaultTimeout can be static. Do not put this.totalTests++ on a page instance and expect the next test to see it after the fixture tears the page down. Instance data dies with the instance. Static data lasts for the process.

Interview trap: this inside a static method is the class, not an instance. Lab 160 shows the confusion.

Lab 160: static field versus instance field

Lab: chapter_16_OOps/CLASS_OBJECT/160_Static_p2.js

Exact file:

class Student {
    static collegeName = "PW AT Batch";

    constructor(name) {
        this.name = name;
    }
    static display() {
        console.log(this.name + " are part of the ", Student.collegeName)
    }
}

let amit = new Student("amit");
let miti_jha = new Student("miti_jha");
let sumu = new Student("sumu");
let padmini = new Student("padmini");

console.log(Student.collegeName);
console.log(amit.name);
console.log(miti_jha.name);
node chapter_16_OOps/CLASS_OBJECT/160_Static_p2.js
PW AT Batch
amit
miti_jha

collegeName is one string for every student. amit.name is "amit". We construct four students and print two names plus the college.

Student.display() is never called in this file. If you call it, this.name inside a static method is the class’s name — Student — not "amit". The template " are part of the " is written for a classroom demo of that mix-up. I will not invent a call. I will tell you: do not read instance fields from a static method. Pass them in, or make the method non-static.

Playwright mapping: static baseURL = process.env.BASE_URL on a config class is lab 160. this.username on a page object is not static. Mixing them is how you print LoginPage are part of the https://app.vwo.com and then file a flaky-test ticket.

Encapsulation — hide the field, guard the door

Private fields hide. Encapsulation is the policy on top of hide: get, set, and sometimes refuse.

Lab 161: get and set a hidden child name

Lab: chapter_16_OOps/CLASS_OBJECT/Encapsulation/161_Pramod_Child.js

Exact file:

class Person {
    // Hide you childs
    #child1;
    #child2;

    constructor(name, ch1, ch2) {
        this.name = name;
        this.#child1 = ch1
        this.#child2 = ch2;
    }

    getChild1() {
        return this.#child1;
    }

    setChild1(changed_name) {
        this.#child1 = changed_name;
    }


}

let p = new Person("Pramod", "Vrad", "Jenny");

console.log(p.name);
// console.log(p.#child1);
console.log(p.getChild1());
p.setChild1("VIRAD");
console.log(p.getChild1());
node chapter_16_OOps/CLASS_OBJECT/Encapsulation/161_Pramod_Child.js
Pramod
Vrad
VIRAD

p.name is public. #child1 and #child2 are not. The commented console.log(p.#child1) would throw. We read through getChild1() and write through setChild1("VIRAD"). #child2 has no getter in this file. Encapsulated and unreachable from outside. That is allowed. You do not owe the world a getter for every private field.

The setter here has no guard. Anyone who can call setChild1 can change the value. Lab 163 adds the guard. Learn the door first. Then lock it.

Lab 162: same pattern on a car engine

Lab: chapter_16_OOps/CLASS_OBJECT/Encapsulation/162_Car.js

Exact file:

class Car {
    #engine;

    constructor(name, engineName) {
        this.name = name;
        this.#engine = engineName;
    }

    getEngine() {
        return this.#engine;
    }
    setEngine(nameEngine) {
        this.#engine = nameEngine;
    }

}

let tesla = new Car("Tesla", "V8");
console.log(tesla.getEngine());
tesla.setEngine("V9");
console.log(tesla.getEngine());
node chapter_16_OOps/CLASS_OBJECT/Encapsulation/162_Car.js
V8
V9

Same shape as 161. Public name. Private #engine. Get. Set. The object is tesla. The brand is public; the engine string is not. A spec that does tesla.engine gets undefined. A spec that does tesla.getEngine() gets "V8", then "V9".

Playwright mapping: getEngine() is getToken(). setEngine() is setToken(newToken) after a refresh. You do not let the spec assign pageObject.token = stolen. You let a method rotate it.

Lab 163: a setter that can say no

Lab: chapter_16_OOps/CLASS_OBJECT/Encapsulation/163_Bank.js

Exact file:

class ICICI {
    #balance;

    constructor(name, balance) {
        this.#balance = balance;
        this.name = name;
    }

    getBalance() {
        return this.#balance;
    }

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

let pramod = new ICICI("Pramod", 1000);
console.log(pramod.getBalance());
pramod.setBalance(10000000, false);
console.log(pramod.getBalance());

let pramod_father = new ICICI("Pramod", 2000);
console.log(pramod_father.getBalance());
pramod_father.setBalance(300000, true);
console.log(pramod_father.getBalance());
node chapter_16_OOps/CLASS_OBJECT/Encapsulation/163_Bank.js
1000
Not allowed
1000
2000
300000

This is the encapsulation lab I stay on until the room is quiet.

#balance starts at 1000 for pramod. setBalance(10000000, false) prints Not allowed and does not change the field. The next getBalance() is still 1000. pramod_father starts at 2000. setBalance(300000, true) is allowed. Balance becomes 300000.

A setter without a check is a public field with extra typing. A setter with isCashier is a policy. The field cannot be reached. The door can refuse.

Playwright mapping: setStorageState(path, { role: "admin" }). clearCookies only from a fixture, not from a random spec. setBaseURL only if process.env.ALLOW_URL_OVERRIDE === "1". The test that tries to point prod at localhost should hear Not allowed, not silently rewrite the suite.

Interview answer: encapsulation is not the #. Encapsulation is the method that decides whether the # changes.

Export and import — how a class leaves its file

A Page Object that lives in the spec file is a demo. A Page Object that lives in pages/LoginPage.js and is imported by tests/login.spec.js is a framework. Chapter 16 teaches the module step before we export a class. The helpers are tiny on purpose.

The three modules: testutil.js, utils.js, logger.js

chapter_16_OOps/testutil.js — exact file:

export let BASE_URL = "https://app.vwo.com";

export function formatUpperCaseString(sname) {
    return sname.toUpperCase();
}

let fname = "Pramod"; // This is not exported. That's why you cannot import it into other classes. 

Two named exports: BASE_URL and formatUpperCaseString. One local let fname = "Pramod". The comment is the lesson. If you do not export it, the other file cannot import it. There is no “I can see it in the same repo” rule. Modules are walls. fname is inside the wall.

chapter_16_OOps/utils.js — exact file:

export let BASE_URL = "https://api.staging.com";

export function formatTestName(name) {
    return "TC_" + name.toUpperCase();
}

Also a BASE_URL. Different value. https://app.vwo.com versus https://api.staging.com. Same export name, two files. That collision is why lab 151 uses as.

chapter_16_OOps/logger.js — exact file:

// Default Export -> Export One Main Thing

export default function log(message) {
    console.log("[LOG] " + message);
}

export function log2(message) {
    console.log("[LOGS] " + message);
}

A default export (log) and a named export (log2). One file can have one default and many named. The comment says “Export One Main Thing.” Default is the main door. Named is the side door.

Lab 150: named import from testutil.js

Lab: chapter_16_OOps/EXPORT_IMPORT/150_Export_import.js

Exact file:

import { BASE_URL, formatUpperCaseString } from "../testutil.js";

console.log(BASE_URL);
// console.log(fname);
let result = formatUpperCaseString("Pramod");
console.log(result);

The import path is ../testutil.js because this file sits in EXPORT_IMPORT/ and testutil.js sits in chapter_16_OOps/. The .js extension is required for Node ESM.

fname is commented. Uncomment it and you do not get "Pramod". You get a ReferenceError. It was never imported, and it was never exported.

If this file runs as ESM, you see:

https://app.vwo.com
PRAMOD

formatUpperCaseString is the Day 5 string habit (toUpperCase) sitting behind an export. A spec should import a helper, not copy sname.toUpperCase() into twelve files.

Lab 151: two modules, same name, as

Lab: chapter_16_OOps/EXPORT_IMPORT/151_Export_Import.js

Exact file:

import { BASE_URL as bul_util, formatTestName } from "../utils.js";
import { BASE_URL as bul_testtul, formatUpperCaseString } from "../testutil.js";

console.log(bul_util);
console.log(bul_testtul);
console.log(formatTestName("login"));

You cannot write two import { BASE_URL } lines and have both bindings called BASE_URL in one file. as renames at the door. bul_util is the staging API URL from utils.js. bul_testtul is the VWO URL from testutil.js — that alias spelling is in the blob.

If it runs as ESM:

https://api.staging.com
https://app.vwo.com
TC_LOGIN

formatTestName("login") prefixes TC_ and uppercases. That is how I want test names built in a report — one helper, not "TC_" + name pasted in the spec.

Playwright mapping: import { test, expect } from "@playwright/test" is a named import. import { LoginPage } from "../pages/LoginPage" is today’s habit. Two configs both exporting BASE_URL is why you alias. Do not silently import the wrong URL and then call the suite flaky.

Lab 152: default import of the logger

Lab: chapter_16_OOps/EXPORT_IMPORT/152_Loggger.js

Exact file — three gs in the filename:

// import { log ,log2 } from './logger.js'; // this is without the default
import log from '../logger.js';

log("starting the test cases")

Default import has no braces. import log from '../logger.js' takes the export default function log. The commented line is the named form: import { log, log2 } from './logger.js'. That commented path is ./logger.js (same folder). The live path is ../logger.js (parent). I will not invent a logger.js inside EXPORT_IMPORT/. The comment is a leftover. The working import is the parent file.

If it runs as ESM:

[LOG] starting the test cases

Braces versus no braces is an interview. I fail people on it because they then write import { LoginPage } from x when LoginPage was a default export, or the other way around, and they paste stack traces into Slack.

Playwright mapping: decide per page file. This series will use named export class LoginPage — see LoginPage.js later. Default is fine for a logger. Do not mix styles at random in one folder.

Single inheritance — the child gets the parent’s methods

Lab 164: LoginPage extends BasePage and defines nothing

Lab: chapter_17_OOPs_Inheritance/Single_Inheritance/164_Inheritance.js

Exact file:

// Inheritance in JavaScript

class BasePage {
    constructor(pageName) {
        this.pageName = pageName;
    }

    open() {
        console.log("Opening the page ");
    }
    close() {
        console.log("Closing the page ");
    }

}

class LoginPage extends BasePage {

}

let page = new LoginPage();
page.open();
page.close();

// LoginPage never defined open() or close() — it got them from BasePage. That's inheritance.
node chapter_17_OOPs_Inheritance/Single_Inheritance/164_Inheritance.js
Opening the page 
Closing the page 

LoginPage is an empty body. extends BasePage is the whole feature. new LoginPage() still constructs a LoginPage. open() and close() resolve on the parent. The comment in the file is the definition I want in your notes.

We pass no pageName. BasePage‘s constructor accepts pageName and would set this.pageName. It is undefined here. The methods do not use it. That is fine for this lab. Lab 165 shows super(...) when the child has its own constructor.

Playwright mapping: this is the first POM. Shared open / close (later goto, screenshot, waitForLoad) live on BasePage. LoginPage can start empty and still open. You add login() when the screen needs it — lab LoginPage.js.

Lab 165: super() in the constructor

Lab: chapter_17_OOPs_Inheritance/Single_Inheritance/165_SI.js

Exact file:

class Animal {
    constructor(name) {
        this.name = name;
    }

    eat() {
        console.log(this.name + " is eating");
    }

    sleep() {
        console.log(this.name + " is sleeping");
    }
}

class Dog extends Animal {
    constructor(name, breed) {
        super(name); //. It is used for the parent constructor. 
        this.breed = breed;
    }

    bark() {
        console.log(this.name, " is barking!")
    }


}

let dog = new Dog("Rex", "Labrador");
dog.eat();
dog.sleep();
dog.bark();
console.log(dog.breed);
node chapter_17_OOPs_Inheritance/Single_Inheritance/165_SI.js
Rex is eating
Rex is sleeping
Rex  is barking!
Labrador

Dog has its own constructor, so it must call super(name) before it touches this. That is a JavaScript rule, not a style choice. Skip super and you get Must call super constructor in derived class before accessing 'this'.

super(name) runs Animal‘s constructor. this.name is set. Then this.breed = breed is the child’s extra field. eat() and sleep() come from Animal. bark() is only on Dog. An Animal cannot bark. A Dog can eat.

Playwright mapping: class LoginPage extends BasePage { constructor(page) { super(page); this.username = page.getByLabel("Email"); } }. super(page) stores the shared page on the parent. Then the child adds locators. If you assign this.page = page in the child and never call super, you fight the language.

Interview line from the file comment: super() is for the parent constructor.

Method overriding — the child’s method wins

Lab 166: whoever’s object is present

Lab: chapter_17_OOPs_Inheritance/Single_Inheritance/166_Method_Overriding.js

Exact file:

class BaseTest {
    setup() {
        console.log("Base: open browser");
    }
}

class APITest extends BaseTest {
    setup() {
        console.log("APITest: open browser");
    }
}

let test = new APITest();
test.setup(); // whoever object is present, it will call that. 
node chapter_17_OOPs_Inheritance/Single_Inheritance/166_Method_Overriding.js
APITest: open browser

APITest.setup replaces BaseTest.setup. We do not see Base: open browser. The comment is the rule: whoever’s object is present, that method runs. new APITest() means the child’s setup. The parent version is still on the parent. You only reach it with super.setup() — next lab.

Playwright mapping: a BaseTest hook that opens a browser, an APITest that must not open a browser. Override setup. If you forget to override, the API suite launches Chromium for a REST call. I have seen that bill on a CI invoice.

Lab 167: super.setup() then the extra step

Lab: chapter_17_OOPs_Inheritance/Single_Inheritance/167_MO_IQ.js

Exact file:

class BaseTest {
    setup() {
        console.log("Base: open browser");

    }

    teardown() {
        console.log("Base: close browser");
    }
}

class UITest extends BaseTest {
    setup() {
        super.setup(); // UITest will help you to call your parent function. super() - Constrcutor, super.fname() - functions name
        console.log("UI: maximize window");
    }

    teardown() {
        console.log("UI: take screenshot");
        super.teardown();
    }

}

let test = new UITest();
test.setup();
console.log("---");
test.teardown();
node chapter_17_OOPs_Inheritance/Single_Inheritance/167_MO_IQ.js
Base: open browser
UI: maximize window
---
UI: take screenshot
Base: close browser

Order is the IQ.

setup: parent first (super.setup()), then child (maximize). You open the browser before you maximize it. Obvious. People still reverse it.

teardown: child first (screenshot), then parent (super.teardown()). You snap the window before you close it. Reverse that and the screenshot is a closed context.

The comment in the file: super() is the constructor; super.fname() is a parent method. Two different supers. Lab 165 is the first. This is the second.

Playwright mapping: async goto() { await super.goto("/login"); await this.username.waitFor(); }. Parent navigates. Child waits for the field that exists only on this screen. Teardown: screenshot in the child, page.close() in the parent. Same order as this lab.

Lab 168: one list, three execute() implementations

Lab: chapter_17_OOPs_Inheritance/Single_Inheritance/168_MO_BIG.js

Exact file:

class TestCase {
    execute() {
        console.log("Running generic test");
    }
}
class UnitTest extends TestCase {
    execute() {
        console.log("Running unit test — checking one function");
    }
}

class APITest extends TestCase {
    execute() {
        console.log("Running API test — sending HTTP request");
    }
}

class E2ETest extends TestCase {
    execute() {
        console.log("Running E2E test — opening browser");
    }
}

let tests = [new UnitTest(), new APITest(), new E2ETest()];

tests.forEach(function (test) {
    test.execute();
});
node chapter_17_OOPs_Inheritance/Single_Inheritance/168_MO_BIG.js
Running unit test — checking one function
Running API test — sending HTTP request
Running E2E test — opening browser

We never construct a bare TestCase. The array holds three subclasses. forEach calls execute() and does not if on the type. Each object runs its own body. That is polymorphism. Day 4’s forEach plus today’s override.

The generic "Running generic test" never prints. It is the fallback if someone constructs new TestCase(). Keep it. A missing override should still do something honest, or throw. Silent no-op is how a suite goes green with zero checks.

Playwright mapping: a runner that says for (const t of tests) await t.execute() — unit without a browser, API with request, E2E with page. One loop. Three classes. Do not write if (t.type === "api").

Lab 169: Page Object verify() — the POM you came for

Lab: chapter_17_OOPs_Inheritance/Single_Inheritance/169_PageObject.js

Exact file:

class BasePage {
    verify() {
        console.log("Verifying base page");
    }
}

class LoginPage extends BasePage {
    verify() {
        console.log("Verify: username field exists");
        console.log("Verify: password field exists");
        console.log("Verify: login button is visible");
    }
}

class DashboardPage extends BasePage {
    verify() {
        console.log("Verify: welcome message shown");
        console.log("Verify: sidebar menu loaded");
    }
}

class CartPage extends BasePage {
    verify() {
        console.log("Verify: cart items displayed");
        console.log("Verify: total price is correct");
    }
}

let pages = [new LoginPage(), new DashboardPage(), new CartPage()];

pages.forEach(function (page) {
    page.verify();
    console.log("---");
});
node chapter_17_OOPs_Inheritance/Single_Inheritance/169_PageObject.js
Verify: username field exists
Verify: password field exists
Verify: login button is visible
---
Verify: welcome message shown
Verify: sidebar menu loaded
---
Verify: cart items displayed
Verify: total price is correct
---

This is hierarchical inheritance and overriding in one file, before the 80-byte 174_HI.js skeleton. One parent. Three children. Each child replaces verify() with the assertions that belong on that screen.

A spec that does pages.forEach(p => p.verify()) does not know about username fields. It knows every page can verify(). When the product adds SettingsPage, you add a class, push an instance, and the loop does not change.

Playwright mapping: await expect(this.username).toBeVisible() instead of console.log. Same method name. Same loop. Day 16 of this series fills these bodies with locators. Today you learn why the method lives on the page class, not in the spec.

Do not put verify() only on LoginPage and copy it. Put a weak default on BasePage and override. The default "Verifying base page" is a reminder you forgot to override — it will show up in the log. That is useful.

Lab 170: the same idea for reports

Lab: chapter_17_OOPs_Inheritance/Single_Inheritance/170_RO.js

Exact file:

class Report {
    generate(data) {
        console.log("Raw data: " + data);
    }
}

class HTMLReport extends Report {
    generate(data) {
        console.log("<html><body>" + data + "</body></html>");
    }
}

class JSONReport extends Report {
    generate(data) {
        console.log('{"report": "' + data + '"}');
    }
}

class TextReport extends Report {
    generate(data) {
        console.log("=== REPORT ===\n" + data + "\n==============");
    }
}

let reports = [new HTMLReport(), new JSONReport(), new TextReport()];

reports.forEach(function (r) {
    r.generate("5 tests passed, 1 failed");
    console.log("---");
});
node chapter_17_OOPs_Inheritance/Single_Inheritance/170_RO.js
<html><body>5 tests passed, 1 failed</body></html>
---
{"report": "5 tests passed, 1 failed"}
---
=== REPORT ===
5 tests passed, 1 failed
==============
---

Same data. Three formats. generate(data) is the shared name. HTML wraps tags. JSON wraps a key. Text wraps a banner. The loop does not care.

Playwright mapping: HTML reporter, JSON reporter, Allure, a custom TTA reporter. The runner calls onEnd. Each reporter class overrides how it writes. You will meet reporters in the CLI week. The OOP is this file.

Multi-level, multiple (mixins), and hierarchical

Lab 171: BasePage → AuthPage → AdminPage

Lab: chapter_17_OOPs_Inheritance/Multi_Level_Inheritance/171_MI.js

Exact file:

// Grand Father -> Father -> Son
// BasePage -> AuthPape -> AdminPage

class BasePage {
    constructor(name) {
        this.name = name;
    }

    open() {
        console.log("[OPEN] " + this.name);
    }
}


class AuthPage extends BasePage {
    login(user) {
        console.log("[LOGIN] " + user);
    }
}

class AdminPage extends AuthPage {
    constructor() {
        super("Admin Panel");
    }

    manageUsers() {
        console.log("[ADMIN] Managing users");
    }
}

let admin = new AdminPage();
admin.open();
admin.login("superadmin");
admin.manageUsers();
node chapter_17_OOPs_Inheritance/Multi_Level_Inheritance/171_MI.js
[OPEN] Admin Panel
[LOGIN] superadmin
[ADMIN] Managing users

Three levels. AdminPage extends AuthPage extends BasePage. new AdminPage() calls super("Admin Panel"), which is AuthPage‘s inherited BasePage constructor. this.name becomes "Admin Panel". Then:

  • open() comes from the grandfather.
  • login() comes from the father.
  • manageUsers() is the son’s own.

The comment says AuthPape. The class is AuthPage. I will not invent a rename. Typos in comments are not new files.

When do you want three levels? When a set of pages share auth behaviour that generic pages do not need. AdminPage and later a SettingsPage might both extend AuthPage. A public LandingPage extends BasePage only. Do not make everything extend AuthPage “just in case.” Deep trees are how super.super fantasies start. JavaScript has no super.super. You call the parent. The parent calls its parent.

Playwright mapping: BasePage (goto, screenshot) → AuthenticatedPage (logout, nav bar) → AdminUsersPage (add user). Keep it to two or three levels. If you need a fourth, you probably wanted a helper or a mixin.

Lab 172: JavaScript will not extends A, B

Lab: chapter_17_OOPs_Inheritance/Multiple_Inheritance/172.js

Exact file:

// class C extends A, B { }  // ❌ SyntaxError

// Mixin concept can help you to perform the multiple inheritance. 

// Mixin 1: Adds logging ability
let LoggerMixin = function (Base) {
    return class extends Base {
        log(msg) {
            console.log("[Log] " + msg);
        }
    }
}

// Mixin 2: Adds screenshot ability
let ScreenshotMixin = function (Base) {
    return class extends Base {
        takeScreenshot() {
            console.log("[SCREENSHOT] captured");
        }
    };
};



// Base class
class TestCase {
    constructor(name) {
        this.name = name;
    }

    run() {
        console.log("Running: " + this.name);
    }
}

// Apply BOTH mixins
class SmartTest extends ScreenshotMixin(LoggerMixin(TestCase)) {
    constructor(name) {
        super(name);
    }
}

let t = new SmartTest("Login Flow");
t.run();
t.log("Test started");
t.takeScreenshot();
node chapter_17_OOPs_Inheritance/Multiple_Inheritance/172.js
Running: Login Flow
[Log] Test started
[SCREENSHOT] captured

The first line is the interview. Uncomment class C extends A, B and Node throws SyntaxError. Java has interfaces. C++ has multiple inheritance. JavaScript has one extends. To get logging and screenshots onto a TestCase without stuffing both into the parent, the classroom uses mixins: functions that take a class and return a new class that extends it.

Read the application order: ScreenshotMixin(LoggerMixin(TestCase)). Inside first: LoggerMixin(TestCase) returns a class with log. Then ScreenshotMixin(...) extends that result and adds takeScreenshot. SmartTest extends the outer result. new SmartTest("Login Flow") can run(), log(), and takeScreenshot().

Playwright mapping: you will be tempted to put log and screenshot on BasePage. That is fine for a small suite. When API tests need log but must not takeScreenshot, a mixin (or, later, a fixture) is the cleaner split. Day 17’s fixture layer is the grown-up version of this file. Learn the constraint first: one extends. Compose the rest.

I will not invent a third mixin. The file has two.

Lab 174: hierarchical skeleton — I will not fill it

Lab: chapter_17_OOPs_Inheritance/Hierarchial_Inheritance/174_HI.js

Exact file on main — 80 bytes:

class Father {

}

class Son1 extends Father {

}
class Son2 extends Father {

}

Folder name is spelled Hierarchial. File is 174_HI.js. Two children. One parent. Empty bodies. That is hierarchical inheritance: one base, many direct children. You already saw the filled version in 169_PageObject.jsLoginPage, DashboardPage, CartPage all extend BasePage.

I will not invent eat() on Father or play() on Son1. The repo left a shape. Lab 169 is the working picture. If an interviewer draws Father / Son1 / Son2, you draw the same tree and then you write 169.

Exporting the class — Basepage.js, LoginPage.js, 173_Test_2.js

This is the last kilometre. Three files. This is the Playwright POM folder layout with console.log instead of locators.

Basepage.js — named export of the parent

Lab: chapter_17_OOPs_Inheritance/Exporting_Class/Basepage.js

Exact file — note the filename: Basepage.js, not BasePage.js.

export class BasePage {
    constructor(name) {
        this.name = name;
    }

    open() {
        console.log("Opening " + this.name);
    }
}

export class BasePage is a named export. The class is BasePage. The file is Basepage.js. Those are different strings. Imports use the file string.

open() uses this.name. The constructor stores it. Any child that calls super("Login Page") gets that string into open().

LoginPage.js — import parent, export child

Lab: chapter_17_OOPs_Inheritance/Exporting_Class/LoginPage.js

Exact file:

import { BasePage } from "./Basepage.js";

export class LoginPage extends BasePage {
    constructor() {
        super("Login Page");
    }

    login(user) {
        console.log(user + " logged in");
    }
}

Named import of BasePage from ./Basepage.js. Named export of LoginPage. Constructor calls super("Login Page") — lab 165’s rule. login(user) is the child’s extra behaviour — lab 164 was empty; this file is not.

The spec will never import BasePage unless it needs a generic page. It imports LoginPage. The child carries the parent with it.

173_Test_2.js — the spec

Lab: chapter_17_OOPs_Inheritance/Exporting_Class/173_Test_2.js

Exact file:

import { LoginPage } from "./LoginPage.js";

let page = new LoginPage();
page.open();
page.login("admin");

If this trio runs as ESM:

Opening Login Page
admin logged in

The spec does not define open(). It does not know BasePage exists. It constructs LoginPage, opens, logs in. That is the Page Object Model. The test is a short story. The pages are the classes. The shared verbs live once.

There is no page.close() in this spec. BasePage in this folder did not define close — that was lab 164’s in-file BasePage. I will not add close() here. Different files. Different blobs.

How this becomes a Playwright Page Object

I will not invent a Playwright spec. I will map the files you already ran onto the files you will write from Day 10 onward.

Classroom fileFramework job
153 CABA page class has locators (attributes) and actions (behaviours).
154 constructor + thisconstructor(page) { this.page = page; }
155 two instancesOne LoginPage per test, not a shared mutable singleton.
156 BrowserConstruction is not navigation. new stores. open() goes.
157 APIClientnew APIClient(baseURL) per env. Day 19’s helper.
158 #apiKeyTokens stay private. Specs call a header method.
159 static countersReporter totals, not page fields.
160 static collegeShared baseURL or timeout on the class.
161–163 get/set + guardEncapsulate state. Refuse illegal writes.
150–152 export/importpages/*.js imported by tests/*.spec.js. Named vs default.
164 empty childA new screen can inherit open() on day one.
165 super(name)super(page) before this.username = ....
166–167 override + super.methodChild goto calls super.goto then waits. Teardown screenshots first.
168 Unit/API/E2EOne execute() name, three runners.
169 Login/Dashboard/CartOne verify() name, three screens. Hierarchical POM.
170 HTML/JSON/TextReporters. Same generate, different write.
171 three levelsBasePageAuthPageAdminPage. Stop at three.
172 mixinsYou cannot extends A, B. Compose logger + screenshot.
174 empty treeShape only. Use 169 as the filled tree.
Basepage.js + LoginPage.js + 173_Test_2.jsThe folder. Parent file, child file, spec file.

When Playwright arrives (Day 10), page is the library object. Your LoginPage has a page, or receives it. Your BasePage.open() becomes return this.page.goto(this.path). Your LoginPage.login("admin") becomes fill + click. The inheritance does not change. The exports do not change. The spec stays short.

That is why this day exists before npx playwright test. If you learn POM as “a Playwright tutorial folder named pages,” you will not be able to draw extends on a whiteboard without the docs. If you learn it as 164 + Exporting_Class, the tutorial is just filling methods.

Day 16 of this series returns here with fixtures and course projects (TTA Cart, Bank). Day 17 layers config, pages, fixtures, reporters. You are not late. You are early on purpose.

Recap — what Day 8 locked in

  • A class is CAB: attributes + behaviour. Lab 153. new creates an object. Labs 154–157.
  • A method is a function inside a class. Lab 155. Two objects do not share instance fields.
  • #field is private. Outside, obj.field is undefined; obj.#field is SyntaxError. Lab 158.
  • static is ClassName.member. Counters and shared names. Labs 159–160. Do not read this.name of an instance from a static method.
  • Encapsulation is get/set plus a guard. Labs 161–163. isCashier === false prints Not allowed and keeps 1000.
  • Named export / import { } / as. Default export default / import log from. fname is not exported. Labs 150–152, logger.js, testutil.js, utils.js. Filename 152_Loggger.js has three gs.
  • Single inheritance: empty LoginPage still open()s. Lab 164. Child constructor calls super(...). Lab 165.
  • Override replaces the parent method. Lab 166. super.method() calls it back. Setup parent-then-child. Teardown child-then-parent. Lab 167.
  • Polymorphism: one method name, a list of subclasses. Labs 168–170. That is a suite. That is a POM verify loop. That is a reporter.
  • Multi-level: BasePage → AuthPage → AdminPage. Lab 171. Comment typo AuthPape stays.
  • Multiple: SyntaxError. Mixins in 172.js. Hierarchical: 174_HI.js is empty; 169 is the picture.
  • Export the parent from Basepage.js (lowercase p). Export the child from LoginPage.js. Drive both from 173_Test_2.js. That is Playwright POM with the browser still imaginary.

FAQ

What is the difference between a class and an object in JavaScript?

A class is the blueprint. An object is one constructed instance. class Car in lab 154 is the blueprint. new Car("i10") is the object stored in hyndai_car. Two new TestCase(...) calls in lab 155 are two objects. They share the class. They do not share status. In Playwright, LoginPage is the class. new LoginPage(page) in a test is the object.

What is the difference between a function and a method?

A method is a function that lives on a class (or on an object). Lab 155’s comment: “method is functions but inside the class.” formatUpperCaseString in testutil.js is a function you import. drive() on Car is a method. page.goto is a method. Stop calling every callable a function in interviews if you can see the class.

How do JavaScript private fields work? Why is cred.apiKey undefined?

You declare #apiKey on the class. You read and write it only inside that class. From outside, cred.apiKey is a different name — a missing public property — so you get undefined, not the secret. cred.#apiKey is a SyntaxError. Lab 158. The door is a public method such as pramodgetAuthHeader().

When should I use static in a test framework?

When the data belongs to the class or the run, not to one instance. Lab 159’s totalTests / passCount / summary(). Lab 160’s collegeName. A default timeout, a default browser name, a process-wide counter. Do not store this.page as static. Do not call static methods as instance.summary() and expect it to be the design. The file says ClassName.method().

What is encapsulation in JavaScript classes?

Hiding fields (#balance) and routing every read/write through methods that can refuse. Labs 161–163. Getters read. Setters write. Lab 163’s setBalance(balance, isCashier) prints Not allowed when isCashier is false and leaves the balance at 1000. The # is the lock. The if is the policy. You need both.

How do export and import work for Page Objects?

Named: export class LoginPage and import { LoginPage } from "./LoginPage.js". Default: export default function log and import log from '../logger.js' — no braces. Two modules exporting BASE_URL need as (lab 151). What you do not export (fname in testutil.js) cannot be imported. 152_Loggger.js is the default-import lab. Basepage.js + LoginPage.js + 173_Test_2.js is the POM trio. File name Basepage.js must match the import path.

What is single inheritance vs multi-level vs hierarchical vs multiple?

Single: one child, one parent. LoginPage extends BasePage. Lab 164. Multi-level: a chain. AdminPage extends AuthPage extends BasePage. Lab 171. Hierarchical: one parent, many direct children. Son1 and Son2 extend Father in 174_HI.js (empty); filled in 169_PageObject.js. Multiple: one child, two parents. JavaScript cannot class C extends A, B. Lab 172 uses mixins instead.

What is method overriding, and when do I call super?

Overriding is the child writing a method with the same name. Lab 166: APITest.setup runs, not BaseTest.setup. You call super.setup() when you still want the parent body and extra work. Lab 167: setup does parent then child; teardown does child then parent (screenshot, then close). super() (constructor) and super.method() (method) are different.

How does BasePage / LoginPage become Playwright POM?

Basepage.js exports shared open(). LoginPage.js imports it, extends BasePage, calls super("Login Page"), adds login(user). 173_Test_2.js imports only LoginPage, constructs, opens, logs in. In Playwright you pass page into the constructor, and open() / login() call locator methods. The files stay three. The spec stays short. Day 16 fills this with real locators. Day 9 adds TypeScript types on the same shape.

What is Day 9 of this series?

TypeScript — types, interface, enum, generics, access modifiers, and a typed POM — from chapter_18_Typescript through chapter_22_Typescript_PRIVATE_PROTECTED_PUBLIC in the same LearningPlaywrightBatch repo. Today’s BasePage / LoginPage grows interfaces and private / protected. Same inheritance. Types on the doors.


<script type=”application/ld+json”> { “@context”: “https://schema.org”, “@type”: “FAQPage”, “mainEntity”: [ { “@type”: “Question”, “name”: “What is the difference between a class and an object in JavaScript?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “A class is the blueprint. An object is one constructed instance. class Car is the blueprint; new Car(\”i10\”) is the object. Two new TestCase(…) calls are two objects that share the class but not instance fields such as status. In Playwright, LoginPage is the class and new LoginPage(page) in a test is the object.” } }, { “@type”: “Question”, “name”: “What is the difference between a function and a method in JavaScript?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “A method is a function that lives on a class or object. formatUpperCaseString in a util module is a function you import. drive() on Car is a method. page.goto is a method on Playwright’s page object.” } }, { “@type”: “Question”, “name”: “How do JavaScript private class fields work?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Declare #apiKey on the class and read or write it only inside that class. From outside, cred.apiKey is a missing public property and is undefined. cred.#apiKey is a SyntaxError. The only supported access is a public method such as an auth-header helper.” } }, { “@type”: “Question”, “name”: “When should I use static in a JavaScript test framework?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “When the data belongs to the class or the whole run, not one instance: pass counts, a shared college-style name, a default timeout. Call ClassName.method(), not object.method(). Do not store this.page as static.” } }, { “@type”: “Question”, “name”: “What is encapsulation in JavaScript classes?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Hide fields with # and route reads and writes through methods that can refuse. A setter without a check is a public field with extra typing. setBalance(amount, isCashier) that prints Not allowed when isCashier is false is the policy.” } }, { “@type”: “Question”, “name”: “How do export and import work for Playwright Page Objects?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Use named export class LoginPage and import { LoginPage } from the page file. Default export has no braces. Alias with as when two modules export the same name. What you do not export cannot be imported. The POM trio is Basepage.js, LoginPage.js, and 173_Test_2.js. The filename Basepage.js must match the import path.” } }, { “@type”: “Question”, “name”: “What is single vs multi-level vs hierarchical vs multiple inheritance in JavaScript?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Single is one child and one parent (LoginPage extends BasePage). Multi-level is a chain (AdminPage extends AuthPage extends BasePage). Hierarchical is one parent and many direct children (Login, Dashboard, Cart). Multiple is one child and two parents; JavaScript cannot class C extends A, B and uses mixins instead.” } }, { “@type”: “Question”, “name”: “What is method overriding and when do I call super in JavaScript?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Overriding is the child writing a method with the same name; the child’s body runs for that object. Call super.method() when you still want the parent body plus extra work. Setup is usually parent then child. Teardown is usually child then parent so you screenshot before close. super() is the parent constructor; super.method() is a parent method.” } }, { “@type”: “Question”, “name”: “How does BasePage and LoginPage become a Playwright Page Object Model?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Basepage.js exports shared open(). LoginPage.js imports it, extends BasePage, calls super(\”Login Page\”), and adds login(user). 173_Test_2.js imports only LoginPage, constructs, opens, and logs in. In Playwright you pass page into the constructor and the methods call locators. The files stay three. The spec stays short.” } }, { “@type”: “Question”, “name”: “What is next after Day 8 of the JS to Playwright Framework series?”, “acceptedAnswer”: { “@type”: “Answer”, “text”: “Day 9 covers TypeScript types, interfaces, enums, generics, access modifiers, and a typed POM from LearningPlaywrightBatch chapters 18 through 22. Today’s BasePage and LoginPage grow interfaces and private/protected members.” } } ] } </script>

Tomorrow — Day 9: TypeScript types, interface, enum, generics, and typed POM

A class without types is a Page Object that accepts anything and fails at runtime. Tomorrow we type the doors.

Day 9 of this series takes chapter_18_Typescript through chapter_22_Typescript_PRIVATE_PROTECTED_PUBLIC from the same LearningPlaywrightBatch repo. You will put types on today’s this.name, write an interface for LoginPage, pick a browser with an enum, wrap an API body in a generic, and meet private / protected / public plus readonly, abstract, and override. File 190_REAL_PAGE_OBJECT_Interface.ts is today’s tree with TypeScript on it. File 201_PageObjectModel.ts is the typed POM. Same inheritance. Compilers on the fields.

Series hub (bookmark this): JavaScript → TypeScript → Playwright Advanced Framework — 21-Day Guide.

Master Playwright end to end

If you want these labs as a live classroom — with VWO login pages, Restful Booker clients, TTA Cart and Bank, and the framework we assemble on Day 21 — join Playwright Automation Mastery at The Testing Academy. Lifetime access. Real projects. A job-ready suite, not a folder of console.log.

*This is Day 8 of 21. Draft only. Not published.*

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.