The Testing Academy · Class Notes Tuesday, 25 August (IST)
Live class · study guide

Export, import, and the class that replaces fifty exports

The last JavaScript session before Playwright proper. Two topics that turn out to be one: export and import, and then classes, because the honest answer to "how do I export fifty functions" is that you do not, you export one class. Ends with the decision that the batch moves to TypeScript, which the session defines as JavaScript plus rules.

By Pramod Dutta, The Testing Academy. Study notes from the live Playwright 3x class, rebuilt from the session recording. The code was cross-checked against chapters 19 and 20 of the LearningPlaywright3x repository and executed on Node v22.22.3 before publishing: every error message quoted here is the message Node produced. Where the repository and its own README disagree, that is recorded here rather than smoothed over.

01

Where this sits, and what changes after it

This was the last of the JavaScript run-up. The stated plan is to finish these fundamentals this week and start Playwright itself immediately after.

Two topics, and they are really one argument. Export and import comes first, it runs into a wall, and classes are the way through the wall.

The decision announced in this session: the batch moves to TypeScript. Not as a new language to learn, but as the same JavaScript with rules added. Everything already covered across twenty chapters stays valid. What changes is the file extension and a handful of keywords, and the framework files will be .ts from here on. TypeScript compiles back to JavaScript anyway, so the engine still runs JavaScript in the end.

02

Export and import, and why files need them

The setup: one file has things, another file wants them.

testutil.js export let BASE_URL export function formatUpper() let fname = "Pramod" no export keyword, so it never leaves blocked 155.js import { BASE_URL, formatUpper } from './testutil.js'; Verified: an importer asking for everything in that module sees exactly two names. The third is invisible.
The export keyword is the whole gate. No keyword, no access.
JavaScript
// testutil.js
export let BASE_URL = "https://app.vwo.com";

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

let fname = "Pramod";        // no export, so importers cannot reach it
JavaScript
// 155.js
import { BASE_URL, formatUpperCaseString } from './testutil.js';

console.log(BASE_URL);                          // https://app.vwo.com
console.log(formatUpperCaseString("Pramod"));   // PRAMOD

The analogy used in class is worth keeping because it draws the right boundary. Import and export is trade between countries, not family. There is no relationship between the two files. Inheritance is the one with a relationship, and that comes later.

Asked live and confirmed by running it: a variable without export is private to its file. Importing everything from testutil.js and listing the names returns BASE_URL and formatUpperCaseString only. fname is not hidden or undefined, it simply is not there.

03

Named, default, and the curly-brace rule

A file can export many things by name, and at most one thing as the default.

JavaScript
// logs/logger.js
export default function log(message) {
    console.log("[LOG] " + message);
}

export function logBetter(message) {
    console.log("[LOGS] " + message);
}
Named export export function logBetter() Import needs curly braces import { logBetter } from '...' Name must match exactly, case included. Any number of them per file. Rename with as. Default export export default function log() Import takes no braces import anyNameYouLike from '...' You choose the name; nothing has to match. Exactly one per file. No default in the file? Braces are required.
The braces are not decoration. They are how you say "find this exact name".
JavaScript
// 157.js - default import, no braces, and the name is yours to pick
import log from './logs/logger.js';
log('Starting');                    // [LOG] Starting

// the named one from the same file still needs braces
import { logBetter } from './logs/logger.js';

Asked in class: what happens if you write import log from './logger.js' when the file has no default export? You get an error, and the reason is the rule in one line: braces are for when there is no default, and no braces is for when there is. A file can offer both at once, and then you use whichever form matches the thing you want.

04

Aliases, and the star to avoid

Two modules can export the same name. as renames on the way in, which is what makes that survivable:

JavaScript
// 156_test.js - same export name from two different modules
import { BASE_URL as bul_util, formatTestName } from "./utils.js";
import { BASE_URL as bul_testtul, formatUpperCaseString } from "./testutil.js";

console.log(bul_util);                  // https://api.staging.com
console.log(bul_testtul);               // https://app.vwo.com
console.log(formatTestName("login"));   // TC_LOGIN

The alias explanation given was a childhood nickname: the same person, a shorter name for local use.

import * also exists, and the session's position on it was blunt.

import * as util from './util.js' f1f2f3 all three loaded, one used Memory you did not need, and a linting and type-check failure on the way in. import { f1 } from './util.js' f1 f2f3 one loaded, one used Say what you need and the tools can check that you needed it.
Star is the habit that makes a linter useful and then ignores it.

Linting, defined in passing and worth writing down. A linter is a program that checks your code follows the agreed rules, and ESLint is the one you will meet. The star import is a good example of what it is for: the code runs, so only a tool that reads for practice rather than correctness will tell you it is wrong.

05

The wall, and the class on the other side of it

A question from the floor drove the whole second half. If a utility file has ten functions, do you write export ten times? What about a hundred?

The answer was that you do not, and the tone was not gentle about it.

One export per function export function openBrowser()export function closeBrowser() export function clickElement()export function typeText() export function waitFor() ... and ninety-five more lines like it Every import site then lists every name it wants. instead One export, one class export class BrowserUtils { openBrowser() { }closeBrowser() { } clickElement() { }... every other action } The class binds the attributes and the behaviour together, so one name carries all of it.
This is the bridge: modules ask the question, classes answer it.

This is not theoretical. It is exactly how the framework the 2x batch built is wired: one export class LoginPage, and the spec imports that single name to reach every locator and action on the page.

06

Why classes exist at all

Before object orientation there was procedural programming: C, COBOL, and everything as a function. For a bank you would write a balance function, an add function, a subtract function, and the data floated separately.

The objection raised in class is that this does not match the world. We live somewhere with relationships, with things inherited from a parent, with information deliberately hidden, and with the same person behaving differently in different company. Procedural code had almost no way to express any of that, so the four ideas that became object orientation are inheritance, polymorphism, encapsulation and abstraction, with classes and objects underneath them.

07

Blueprint and building

class Person the blueprint, not real Attributes name age eyeColour Behaviour eat() sleep() work() takes no memory new object 1 name: "Pramod"eyeColour: "brown"sleep: 4 hours object 2 name: "Amit"eyeColour: "black"sleep: 8 hours Same keys every time. Different values each time. These are real, and these are what take memory.
A blueprint for a building is not a building. Two builders can work from one drawing.

CAB, the mnemonic given for it: Class = Attributes + Behaviour. Attributes are the data, behaviour is the functions, and a function that lives inside a class is called a method. That is the whole definition.

The consequence to keep: a class costs no memory. Memory is allocated when you create an object from it, and each object gets its own.

08

What `new` really does, and what the variable holds

JavaScript
class Person {
    #name;
    #age;

    eat() {}
    sleep() {}
}

let pramod = new Person();
let amit   = new Person();

The distinction the session was careful about: pramod is not the object. It is a reference pointing at an object. new Person() on the right is the object; the variable on the left just knows where it lives.

Two objects from the same class, so are pramod and amit the same thing? No. Run on Node 22: pramod === amit is false, while pramod instanceof Person is true. Every new builds a separate object with its own memory, and the two references point at different places.

Checked against the repository's own chapter 20 notes, and each of these is the real behaviour:

Claim What actually happens
Person() without new TypeError: Class constructor Person cannot be invoked without 'new'
Reading #name from outside Not possible. Object.keys() returns nothing and JSON.stringify() gives {}
A subclass reading the parent's #name SyntaxError: Private field '#name' must be declared in an enclosing class
Using a class before it is declared ReferenceError: Cannot access 'Later' before initialization

That last row is worth a second look, and it connects back to the hoisting and closures class. A class is not simply un-hoisted. It is hoisted and then held in the temporal dead zone, which is why you get "cannot access before initialization" rather than "is not defined". Same behaviour as let and const, and different from a function declaration, which really can be called before its line.

09

The hash, and the door into TypeScript

The # in #name is the JavaScript way of marking a field private, and the reaction it got in class is the point: anyone arriving from Java or Python looks at it and asks what it is, whereas everyone recognises the word private.

That is the argument for TypeScript in one symbol.

JavaScript class LoginPage { #username; login(user, pass) { } } TypeScript class LoginPage { } private username: Locator;async login(u: string): Promise<void> The structure is identical. Only the highlighted parts are TypeScript, and they compile away. Everything learned over the first twenty chapters still applies unchanged.
Shown live by deleting the annotations from a real page object until it was plain JavaScript again.

The framing used, and it is the one to repeat in an interview: TypeScript is JavaScript plus rules. JavaScript already had object literals that look a bit like classes, but nothing enforced their shape, and at a few thousand tests that stops being survivable. A .ts file compiles to JavaScript before it runs, so the engine never sees TypeScript at all.

10

Running chapter 19 yourself

Chapters 19 and 20 are written up in the LearningPlaywright3x README, pushed during this session. Two things to know before you follow the run commands.

The code folders are not in the repository yet. The README documents 19_Export_Import/155.js and 20_Class_Object_OOPs/158.js and its progress tracker marks both chapters complete, but the last code directory actually present is 18_Async_Await. Today's commit changed the README and nothing else. Type the examples from this page or from the README rather than waiting for a git pull to produce them.

The second is a version trap worth understanding rather than memorising. The README says ES modules need "type": "module" in package.json or an .mjs extension. On current Node that is no longer true:

Terminal
node 19_Export_Import/155.js

On Node 22.22.3 that runs fine with no package.json at all, because Node now detects a file using import and treats it as a module. On older Node the same file fails, and the error tells you the fix in the README's own words:

Code
Warning: Failed to load the ES module: 155.js.
Make sure to set "type": "module" in the nearest package.json file
or use the .mjs extension.

A trap found while checking this, in case you take the .mjs route on an older Node. Renaming only the file you run is not enough. With 155.mjs importing a still-.js testutil.js, Node treats the dependency as CommonJS and fails with SyntaxError: Named export 'BASE_URL' not found. The entry file and everything it imports have to agree. Renaming all of them to .mjs, or adding "type": "module" once, both work.

11

Questions from the floor

Can I change an imported function inside the file that imported it? Yes, though the session moved on quickly. The useful version of this is that imports are live bindings to the exporting module, not private copies.

Is this like importing libraries in Selenium? Yes, close enough to be a fair mental model.

Can there be more than one default export in a file? Syntactically you would be trying to name two things "the main one". There is exactly one default per file, and no limit at all on named exports.

Do we only ever have one file to import from? No. A project has as many modules as it needs, and a single file commonly imports from several, as the alias example does.

Everything so far has been JavaScript. How do we cope when classes switch to TypeScript? By recognising there is nothing new to learn in the switch. All twenty chapters are JavaScript, and TypeScript adds type annotations and a few keywords such as private in place of #. The demonstration was to strip the annotations off a real page object until it was ordinary JavaScript again.

Will we write a class for every page? Yes. Login, inventory, item detail, cart, both checkout steps and checkout complete each get a class. That is the Page Object Model, and it is why this session existed.

12

Tasks and announcements

  • Today's task: watch the Linux basics session on SDET Club, then write your own cheat sheet and post it in the comments. It covers more than forty commands. This came up because ./file.js and ../logs/logger.js in an import path are just relative paths, and that tripped people up.
  • AI Fluency part 1 is tonight at 8:00 PM IST, part 2 on Thursday at the same time. Attend live if you can; recordings follow. The companion notes are at AI Fluency: Framework and Foundations.
  • Finish Claude 101 and its certification if you have not, then post the certificate on LinkedIn. The recording sits in the SDET Masterclass and takes under an hour at 1.5x. Study guide: Claude 101.
  • Claude Code 101 is targeted for the first week of September.
  • Class notes board for this batch: the 3x Playwright and AI Mastery Eraser workspace.
  • Next up: the constructor, then inheritance, polymorphism, encapsulation and abstraction, all in TypeScript.