1

Lets say I have a class containing page objects. If I instantiate that class in a test, then I can use the page objects as you normally would with playwright. I would like to get the css value as a string from one of those page objects so I can use it in a custom function.

This should give you an idea of what I mean

    export class PageObjects{
        constructor(page){
            this.obj1 = page.locator('#table tbody tr')
        }
    }

    test('Get Values and header names from a table', async({page})=> {
        // Page objects
        const pageObjects = new PageObjects(page)

        // I want the css from the page object?? in other words  
        // how do I pull out the '#table tbody tr' css value as a string?
        // in other words I want cssString value to be #table tbody tr
        // I have tried _selector but it just returns an empty string
        cssString = pageObjects.obj1.??????


    })

is there a way to get the css value out of a locator?

  • Why do you need to do this? Seems like a possible [xy problem](https://meta.stackexchange.com/a/233676/399876). It's not clear what the purpose of `PageObjects` is or why you need to be able to pull out the argument that its locator was called with. I don't think Playwright offers any public interface for doing this, nor should it, absent a motivating use case. – ggorlen Apr 06 '23 at 02:39
  • I answered this question below but basically it can be useful if you have to develop custom tools to deal with similar chunks of code like tables. I run tests on a set of applications each of which has similar tables. This allows me to get a handle on the table like a tbody and then pull out all the data and headers since that portion of the code is generic for all the tables. It means I can import one function and use one locator per table to pull out an organize data. This effectively means I don't have to create dozens of extra locators and pass many arguments to a function. – Scott Littleton Apr 06 '23 at 16:09
  • Katalon for example has this capability. You can refer to its version of a locator object and put the method of mylocator.findPropertyValue('css') and it will give you the css as a string – Scott Littleton Apr 06 '23 at 16:09
  • Thanks for clarifying, but I don't quite follow why you need to access the argument to achieve that abstraction. Can you not simply bind the locator inside your own wrapper function, or parameterize the argument? – ggorlen Apr 06 '23 at 16:17
  • Wait--are you trying to get the CSS itself or the argument that the locator was called with? I might be misunderstanding your question. If you're trying to get the CSS style, then are you looking for the live/dynamic style at the time of the call, or something from the stylesheet or inlined? Are you looking for `getComputedStyle` from [Get a CSS value with JavaScript](https://stackoverflow.com/questions/6338217/get-a-css-value-with-javascript)? – ggorlen Apr 06 '23 at 16:27
  • yes I was trying to get the argument the locator was called with. I think I will just have to make it a public property in the page object class so I can refer to it that way instead of trying to use locators with it. I think this is probably why Katalon has this capability since its written in Java and they probably just make all the css selectors into properties that get called with getters when you use .findPropertyValue('css'). Otherwise the css selectors are just used as you normally would in selenium as page objects – Scott Littleton Apr 06 '23 at 17:02

3 Answers3

1

In general ,this is not recommended approach for maintaining locators.

Test should be least exposed to locators directly to avoid any future maintenance efforts in mutiple test files as when they change.

Use Page Object Model which is a software design pattern for better code abstraction, reusability and long term maintenance.

In Page Object Model, all the application functionality is captured as black boxes in the form of page object methods which also encapsulate UI objects(CSS/Xpath) as well so that they can be added or updated from a single place of reference- DRY principle.

Full Code Example:

Page Object:

// playwright-dev-page.js
const { expect } = require('@playwright/test');

exports.PlaywrightDevPage = class PlaywrightDevPage {

  /**
   * @param {import('@playwright/test').Page} page
   */
  constructor(page) {
    this.page = page;
    this.getStartedLink = page.locator('a', { hasText: 'Get started' });
    this.gettingStartedHeader = page.locator('h1', { hasText: 'Installation' });
    this.pomLink = page.locator('li', { hasText: 'Guides' }).locator('a', { hasText: 'Page Object Model' });
    this.tocList = page.locator('article div.markdown ul > li > a');
  }

  async goto() {
    await this.page.goto('https://playwright.dev');
  }

  async getStarted() {
    await this.getStartedLink.first().click();
    await expect(this.gettingStartedHeader).toBeVisible();
  }

  async pageObjectModel() {
    await this.getStarted();
    await this.pomLink.click();
  }
}

Test:

    // example.spec.js
const { test, expect } = require('@playwright/test');
const { PlaywrightDevPage } = require('./playwright-dev-page');

test('getting started should contain table of contents', async ({ page }) => {
  const playwrightDev = new PlaywrightDevPage(page);
  await playwrightDev.goto();
  await playwrightDev.getStarted();
  await expect(playwrightDev.tocList).toHaveText([
    `How to install Playwright`,
    `What's Installed`,
    `How to run the example test`,
    `How to open the HTML test report`,
    `Write tests using web first assertions, page fixtures and locators`,
    `Run single test, multiple tests, headed mode`,
    `Generate tests with Codegen`,
    `See a trace of your tests`
  ]);
});

test('should show Page Object Model article', async ({ page }) => {
  const playwrightDev = new PlaywrightDevPage(page);
  await playwrightDev.goto();
  await playwrightDev.pageObjectModel();
  await expect(page.locator('article')).toContainText('Page Object Model is a common pattern');
});
Vishal Aggarwal
  • 1,929
  • 1
  • 13
  • 23
  • Hi, thank you for your response. The example above is not my implementation in an actual test. I just used it as the simplest way to get the point across for the question. In reality I can just make css strings public in the page object and refer to them if necessary. But it would be convenient if there was a way to get the css from the locator. I sometimes need it for advanced custom functions that refer to other elements within a set. Like if I get the tbody and need to splice out data from a table and pull in the headers to make an object. This is often easier than creating many locators. – Scott Littleton Apr 06 '23 at 15:12
  • Katalon for example has this capability. You can refer to its version of a locator object and put the method of mylocator.findPropertyValue('css') and it will give you the css. I work on a set of applications that are similar and involve a lot of tables so I can make use of just having one locator that then refers to other parts of the table that are generic in their implementation. This saves me from having to make dozens of extra locator objects and importing them all into a test file. Instead I can just use one locator, get its css string and hide the complexity in one imported function. – Scott Littleton Apr 06 '23 at 15:15
  • I agree that OP's approach is probably misguided and appreciate the POM pattern which has its use case, but "Test should not be exposed to an locator directly." seems overly harsh. Even the code you posted from the POM doc you linked uses a locator directly in a test. There's a cost to abstracting, and many tests would be much more readable without unnecessary POMs getting between you and the locators that are ultimately doing the work. – ggorlen Apr 06 '23 at 16:14
  • @ggorlen I did not mean to be harsh in any way but if it comes across that way , I will change the wording. The reason I posted because as an general approach , its very costly from maintenance perspective to define and use locators in tests directly. It looks like OP has an exceptional scenario. – Vishal Aggarwal Apr 06 '23 at 16:39
  • It's not necessarily unmaintainable to write locators inline in tests. It's potentially just as costly to write a bunch of poorly-thought out fancy abstractions that wind up hiding what the test does and take a lot of brainpower to understand (and maintain). That said, the POM pattern isn't unreasonable. It really depends on the use case. It's hard to tell what's going on with OP's scenario--there's not enough details to make a design recommendation one way or another, other than to point out to them that whatever they're doing looks strange, as you've correctly done here. – ggorlen Apr 06 '23 at 16:41
  • I am not sure why we are discussing about "poorly-thought out fancy abstractions" , that is totally out of context here. One bad approach does not justify another one. – Vishal Aggarwal Apr 06 '23 at 16:46
  • Yes, that's why it seems shortsighted to prohibit locators from being in tests. There's not enough information to make any absolute statements. Locators are often great in tests. That may or may not apply to this case. It's impossible to say for sure. Abstractions are not always good and not always bad--they're tools that can be adapted to match a situation, but we have to know about the situation first before being definitive. – ggorlen Apr 06 '23 at 17:12
  • I've been working with a heavily-abstracted test codebase written by a "very clever engineer", and it's a mess. I have to dip through 2-3 imports, constructors and factories just to find a locator. Good blog post: https://kentcdodds.com/blog/aha-testing -- TL;DR don't hastily abstract and try to find the right balance between DRY and no abstractions (i.e. locators in tests always, as applies to Playwright). I'm fine with the POM approach but it can get out of hand quickly. – ggorlen Apr 06 '23 at 17:22
  • The way I look at it is when we provide an answer to any stackoverflow question , we can provide it on 2 level. First which directly focuses on the problem under discussion and solves it by any possible way to resolve OP's issue at hand. – Vishal Aggarwal Apr 06 '23 at 17:47
  • There is also an second layer where we think broadly in terms of all future users coming to this post on various level of experiences and with slightly different scenarios but in the same problem space. At this layer we start thinking in terms of common denominator which is the most common scenario which more or less every user in that space faces and provide something which hopefully will help many ,particularly newly starting in that space. – Vishal Aggarwal Apr 06 '23 at 17:47
  • And sometimes the second layer becomes more important to highlight when its in direct conflict with the first one. – Vishal Aggarwal Apr 06 '23 at 17:50
  • I agree with all that. Again, good answer (I like the POM pattern suggestion and doc reference), and you've offered a useful pattern here, so +1. Thanks for the discussion. – ggorlen Apr 06 '23 at 22:36
  • Thank you for the discussion and extremely good link to an invaluable post and blog. – Vishal Aggarwal Apr 06 '23 at 22:56
0

The answer is that you cannot get the string argument passed to the locator in Playwright. But the point was really to use the css selector string to manipulate the DOM. So if you need to manipulate the HTML elements inside some specific element then you can first set up a page object class and define that element as a Locator. Then you can import the class into your test and instantiate the class. Then you can call the locator as a property on the class. Once you have done this then you can use myLocator.evaluate( el=>{}) to access and manipulate the DOM.

    // In your pageObject file
    export class PageObjs{
        obj1: Locator;
        constructor(page){
            this.obj1 = page.locator('#idOfElement')
        }
    }

    // In your test

    test('Test Description', async({page})=>{

        const pageObjects = new PageObjs(page);

        pageObjects.obj1.evaluate( el => {
            let tableDataCells = el.document.querySelectorAll('td')
            // do stuff in the dom
            let list = tableDataCells.map(()=>{})
            return list
        })

    })
-1

I'm not sure I follow the motivation for this pattern (Playwright already has a Page abstraction--why do you want to wrap it with another--unless you're going for a POM as described in this answer?) and it smells like a potential xy problem, but I don't see why you can't set that argument as a varible on your instance:

export class PageObject {
  constructor(page) {
    this.selector = '#table tbody tr';
    this.locator = page.locator(this.selector);
  }
}

// ...

const pageObject = new PageObject();
console.log(pageObject.selector);

I've taken the liberty to rename a couple of variables: plural usually refers to collections like arrays and objects, so PageObjects would be typed PageObject[], and obj1 is an unclear name.

It's a bit strange to hardcode that selector. This effectively makes the class single purpose only, so you might consider making that a parameter, depending on your use case.

Even if you are able to find a property on Playwright locators holding the argument, I wouldn't rely on it, since there is no documented public property in the API.

If you want the argument in one place, it's easy enough to hardcode it as part of a global selector config, or bind it in a wrapper function if some aspects of the selector may change.

I suggest taking a step back and re-approaching your design from the top down. Start from your goals and inquire whether this really the right abstraction for achieving them. Often, if things seem hard to do and you find you're battling your design or the language, it's probably not the right approach.

Generally, the best approach is to not write abstractions immediately. Then, once the cut points emerge, you can write accurate, non-premature and robust abstractions that solve clear problems.


If I misunderstood the question and you want to get the CSS style of the elements, you can use getComputedStyle:

const style = await page
  .locator("#table tbody tr")
  .evaluateAll(els =>
    els.map(e =>
      Object.fromEntries(
        [...getComputedStyle(e)].map(k => [
          k,
          getComputedStyle(e)[k],
        ])
      )
    )
  );
ggorlen
  • 44,755
  • 7
  • 76
  • 106
  • Say you have 12 tables in multiple applications that are all similar but just a bit different. You need to pull all the data from on of those tables depending upon the test. You also need to organize the data from those tables into something you can use like an array of objects with key value pairs where the headers work as the keys for each object that represents a row in the table. – Scott Littleton Apr 06 '23 at 16:55
  • You can do this the typical way by creating multiple locators for each table or you can just use the get the css for one locator and use that to grab the table you want and then chop it up and organize it as you like with javascript. Its really more of a why would you want to do it the expected way and have to write a lot of extra locators and potentially rewrite code over and over. So its more of a question of keeping this dry and efficient. It doesn't look like Playwright has this feature yet. – Scott Littleton Apr 06 '23 at 16:56
  • They have it in katalon where you just do mylocator.findPropertyValue('css') and if gives you the css to locate the object like '#myElement'. This allows you to use locators in the typical way or if necessary make customized tool to deal with repetitive things. I'm sure there are other use cases but I generally only need to use this for specific tasks that deal with ripping data out of similar forms and tables using just one locator per form or table. – Scott Littleton Apr 06 '23 at 16:56
  • Why is adding a separate property for the argument not a workable solution for you? I understand that you have repetition you're dealing with. That's totally normal, and it often makes sense to write an abstraction of some sort. But, again, I don't see why you'd need to pull the arguments from a locator call in order to eliminate repetition. Other strategies are possible. I don't work with Katalon, but that's a different library and Playwright uses different patterns. I suggest stepping back from the Y and focusing on the X. If you show your use case I can help design the abstraction. – ggorlen Apr 06 '23 at 17:02
  • "chop [the css selector] up and organize it as you like with javascript" sounds awfully brittle, but again, I'm not really sure what you're doing so I can't say more than "don't try to be too clever when testing". – ggorlen Apr 06 '23 at 17:07
  • I was looking at the problem again and I think I see what you mean. Its better to use a locator and then use .evaluate(el=>{}) to get access to the DOM. So, now I'm basically just using a page object for a table and then using that to get access to the DOM so I can do all the usual stuff with querySelector and querySelectorAll. Thank you for your help – Scott Littleton Apr 06 '23 at 22:20