Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG] Error: locator.isVisible: SyntaxError: Invalid flags supplied to RegExp constructor ' >> internal:role=button' #26974

Closed
1 task done
viveleroi opened this issue Sep 9, 2023 · 5 comments · Fixed by #27002
Assignees
Labels

Comments

@viveleroi
Copy link

I'm reworking a locator in a POM and it seems like Playwright is mangling a regexp I pass as a filter. I have a tree POM with a method for selecting a specific node by it's text. For convenience I want to turn a basic string into a regex. The regex alone works fine, but when passed to the RegExp constructor, playwright throws an error. It seems like Playwright is somehow altering it.

I originally asked for help in discord and was asked to file an issue here.

System info

  • Playwright Version: 1.37.1
  • Operating System: Windows 11
  • Browser: Chromium
  • Other info: Node v18.17.0

Source code

  • I provided exact source code that allows reproducing the issue locally.

See below

Steps

  • [Run the test]

Expected

No error. My RegExp constructor works fine in vanilla JS so why would it be a problem in Playwright?

const text = "Engineer's Log"
console.log(text.match(new RegExp(`^${text}$`))) // prints match array
console.log(text.match(/^Engineer's Log$/)) // prints match array

Actual

// In a Playwright test:
const text = "Engineer's Log"
this.locator('div').filter({ hasText: new RegExp(`^${text}$`)) // errors 
this.locator('div').filter({ hasText: /^Engineer's Log$/ // works
Error: locator.isVisible: SyntaxError: Invalid flags supplied to RegExp constructor ' >> internal:role=button'

  checking visibility of locator('_react=Tree').locator('div').filter({ hasText: '/^Engineer\'s Log$/ >> internal:role=button' })

This only errors when there's a single quote in my text string. Everything without a single quote works fine.

@pavelfeldman
Copy link
Member

Objects that you pass are equivalent, so I'm not sure what it going on there - note that your snippets are missing closing brackets. Anyhow, following works fine:

test('regex', async ({ page }) => {
  const text = "Engineer's Log"
  await expect(page.locator('div').filter({ hasText: new RegExp(`^${text}$`) })).toBeHidden();
  await expect(page.locator('div').filter({ hasText: /^Engineer's Log$/ })).toBeHidden();
});

I'm not sure what this object you have in POM and why it has a locator method, but I would guess you have your own implementation of locator that has a bug?

@viveleroi
Copy link
Author

viveleroi commented Sep 10, 2023

No, nothing custom just a reference to an existing locator. We use POMs for specific components too not just entire pages, so this is a for a Tree component as mentioned.

export abstract class AbstractPOM {
  readonly locator: Locator

  constructor(locator: Locator) {
    this.locator = locator
  }
}

export class TreePOM extends AbstractPOM {
  getNode(text: RegExp | string): TreeNodePOM {
    return new TreeNodePOM(
      this.locator.locator('div').filter({ hasText: isString(text) ? new RegExp(`^${text}$`) : text })
    )
  }
}

That's all, the locator logic is 100% Playwright.

I normally have a loop that finds/expands tree nodes from string/regexp but for simplicity I broke this down (the test would fail because I'm looking for engineer's log in two places but I am seeing the regex get manipulated by playwright related to the error above. However, this doesn't trigger the error yet the text input is exactly the same... so I don't know where the breakdown is.

const treeNode = new TreePOM(page.locator('_react=Tree')).getNode("Engineer's Log")

const text = "Engineer's Log"
await expect(treeNode.locator.locator('div').filter({ hasText: new RegExp(`^${text}$`) })).toBeVisible()

For some reason in this case, playwright is escaping the regex differently:

'/^Engineer\'s Log$/ >> internal:role=button' // the erroring version above
/^Engineer's Log$\/ >> div >> internal:has-text=\/^Engineer's Log$/ the working version here
  - waiting for locator('_react=Tree').locator('div').filter({ hasText: /^Engineer's Log$\/ >> div >> internal:has-text=\/^Engineer's Log$/ })


  34 |
  35 |     const text = "Engineer's Log"
> 36 |     await expect(treeNode.locator.locator('div').filter({ hasText: new RegExp(`^${text}$`) })).toBeVisible()

I even modified my getNode function to hard-code the regex example:

  getNode(text: RegExp | string): TreeNodePOM {
    let regex = new RegExp(`^${text}$`)

    if (typeof text !== 'string') {
      text = "Engineer's Log"
      regex = new RegExp(`^${text}$`)
    }

    return new TreeNodePOM(this.locator.locator('div').filter({ hasText: regex }))
  }

Here's the loop code I use that triggers the error, but it's still just passing a string of "Engineer's Log" to the same getNode/regex code above.

export class SubmodulePOM {
  readonly page: Page

  readonly navigationTree: TreePOM

  private readonly path: string

  /**
   * Constructor
   *
   * @param page - The page object
   * @param path - The url path
   */
  constructor(page: Page, path: string, mainGridLocator = '_react=MainGrid') {
    this.page = page
    this.path = path

    this.navigationTree = new TreePOM(this.page.locator('_react=Tree'))
  }

  async goto(): Promise<void> {
    await this.page.goto(this.path)
  }

  async navigateViaTree(nodes: ReadonlyArray<RegExp | string> = []): Promise<void> {
    for (const node of nodes) {
      const treeNode = this.navigationTree.getNode(node)

      // We use await inside a loop because these must be synchronous
      /* eslint-disable no-await-in-loop */
      await treeNode.isVisible()
      const isLeaf = await treeNode.isLeaf()
      await (isLeaf ? treeNode.select() : treeNode.toggle())
      /* eslint-enable no-await-in-loop */
    }
  }
}

Then in my test I'd use:

    const textLogs = new TextLogsPOM(page)
    await textLogs.goto()

    // Expand tree nodes and select a leaf to load data into the main grid
    await textLogs.navigateViaTree(['USNS Evers', 'Engine', new RegExp("^Engineer's Log$")]) // this breaks the regex too

@viveleroi
Copy link
Author

viveleroi commented Sep 10, 2023

Ok, narrowed down further. The regex error occurs when I expand tree nodes in loop, but it does not occur when I expand each node by itself, even though my regex is exactly the same. I ditched all of our POMs and made the most basic example I can.

test.describe('text logs', () => {
  test('regex', async ({ page }) => {
    await page.goto('text-logs')

    const treeLocator = page.locator('_react=Tree')

    const nodes = ['USNS Evers', 'Engine', /^Engineer's Log$/]
    for (const node of nodes) {
      const treeNode = treeLocator.locator('div').filter({ hasText: isString(node) ? new RegExp(`^${node}$`) : node })

      await treeNode.isVisible()
      await treeNode.getByRole('button').click()
    }
  })
})

Error: locator.click: SyntaxError: Invalid flags supplied to RegExp constructor ' >> internal:role=button'

Reminder, this is only for things with a single quote. The errors work fine as regex:

const nodes = [/^USNS Evers$/, /^Engine$/, /^Engineer's Log$/]

The error is specific to the one with a quote:

Error: locator.click: SyntaxError: Invalid flags supplied to RegExp constructor ' >> internal:role=button'

    at new RegExp (<anonymous>)
    at createTextMatcher (<anonymous>:5774:16)
    at Object.queryAll (<anonymous>:4805:29)
    at InjectedScript._queryEngineAll (<anonymous>:4750:49)
    at InjectedScript.querySelectorAll (<anonymous>:4737:30)
    at eval (eval at evaluate (:201:30), <anonymous>:4:35)
    at UtilityScript.evaluate (<anonymous>:203:17)
    at UtilityScript.<anonymous> (<anonymous>:1:44)
=========================== logs ===========================
waiting for locator('_react=Tree').locator('div').filter({ hasText: '/^Engineer\'s Log$/ >> internal:role=button' })

Doing everything one by one works fine:

    const a = treeLocator.locator('div').filter({ hasText: /^USNS Evers$/ })
    await a.isVisible()
    await a.getByRole('button').click()

    const b = treeLocator.locator('div').filter({ hasText: /^Engine$/ })
    await b.isVisible()
    await b.getByRole('button').click()

    const c = treeLocator.locator('div').filter({ hasText: /^Engineer's Log$/ })
    await c.isVisible()
    await c.click()

@pavelfeldman
Copy link
Member

pavelfeldman commented Sep 10, 2023

Doing everything one by one works fine

I still can't repro this, but are you sure doing it one by one works? You seem to be forgetting getByRole in the third iteration. If it works with getByRole as well, it would mean you found a bug in JavaScript!

@pavelfeldman
Copy link
Member

pavelfeldman commented Sep 10, 2023

Ok, I was able to repro it. Internal repro:

console.log(asLocator('javascript', `div >> internal:has-text=/a'b/ >> internal:role=button`));

Results in

locator('div').filter({ hasText: '/a\'b/ >> internal:role=button' })

Should be

locator('div').filter({ hasText: /a'b/ }).getByRole('button')

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants