child class get undefined value when calling parent function

Advertisements

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.

Leave a ReplyCancel reply