I’m writing a playwright test and figure out that my application have a lot of similar page structures. So, I try to create an abstract class call BaseApplicationPage
with the purpose to reuse them for pages in my application.
The abstract class look like this
export default abstract class BaseApplicationPage {
page: Page;
abstract path: string;
abstract pageTitleText: string;
protected constructor(page: Page) {
this.page = page;
}
async navigate(): Promise<void> {
await this.page.goto(`${process.env.TEST_URL}/${this.path}`);
await expect(this.page).toHaveURL(new RegExp(this.path));
}
}
and my child class look like this
export default class PageExample extends BaseApplicationPage{
page: Page;
pageTitleText: string = "example";
path: string = "example-value";
constructor(page: Page) {
super(page);
}
}
However, when I run test for the application, page
becomes undefined
test.describe.serial(`Test example`, async () => {
let page: Page;
test.beforeAll(async ({ browser }) => {
page = await setUpPage(browser);
});
test("Test view page @healthcheck", async () => {
const pageExample = new PageExample(page);
await pageExample.navigate();
});
});
TypeError: Cannot read properties of undefined (reading 'goto')
at ../pages/base-application-page.ts:14
12 |
13 | navigate = async (): Promise<void> => {
> 14 | await this.page.goto(`${process.env.TEST_URL}/${this.path}`);
| ^
I’ve made sure that page object isn’t undefined before calling new PageExample(page);
. However, it suddenly become undefined when it’s inside the constructor. Why is that?
>Solution :
Your child class redefines page
. It shouldn’t. Instead, it should just inherit it. Remove page: Page;
from your child class:
export default class PageExample extends BaseApplicationPage{
// *** no `page: Page;` here
pageTitleText: string = "example";
path: string = "example-value";
constructor(page: Page) {
super(page);
}
}
Your code may have worked in earlier versions of TypeScript or earlier versions of your project, but when public class fields were standardized in JavaScript, they were standardized slightly differently from the way TypeScript implemented them, and so TypeScript now has an option for how to handle this: useDefineForClassFields
, which defaults to true
if target
is "ES2022"
or higher (including "ESNext"
).
With TypeScript’s old semantics, the page
property would have been created via simple assignment in the parent constructor and not reassigned by the child. But with JavaScript’s standard semantics, the property gets redefined (as though the child class had Object.defineProperty(this, "page", { value: undefined, /*...flags...*/ });
.
As this is a JavaScript thing, you can see it in pure JavaScript as well:
class Parent {
value;
constructor(v) {
this.value = v;
}
}
class BadChild extends Parent {
value;
constructor(v) {
super(v);
}
}
class GoodChild extends Parent {
// No redeclaration
constructor(v) {
super(v);
}
}
const p = new Parent(42);
console.log(p.value); // 42
const b = new BadChild(42);
console.log(b.value); // undefined
const g = new GoodChild(42);
console.log(g.value); // 42
Notice how BadChild
redefines value
and so resets the property to undefined
, but GoodChild
inherits it.