microsoft / playwright

Playwright is a framework for Web Testing and Automation. It allows testing Chromium, Firefox and WebKit with a single API.
https://playwright.dev
Apache License 2.0
64.32k stars 3.49k forks source link

[Feature] ElementHandle Extension Methods (or, OMG Extensions) #1072

Closed tiffon closed 4 years ago

tiffon commented 4 years ago

It would be sick to be able to enhance ElementHandles (and maybe other things) with additional functionality, akin to extension methods in C# (which are also sick).

For some context, back in the days of yore I had to convert the E2E tests for a web-based IDE project (built in React) from Ruby to JS / WebdriverIO. The Ruby stuff followed the page-model approach, which gets unwieldy fast, IMO. And, since the project source was in JS and already had a (React) component hierarchy, I figured to shift the tests from monolithic(-ish) page-models to something that reflected the component hierarchy. Not sure if that's an anti-pattern or not, but it was great and allowed us to very comfortably invest heavily in E2E testing, including publishing packages that our downstream teams used to interface with the foundational elements of the IDE we were responsible for.

The basic idea was to write WebdriverIO interfaces for significant components. For instance, an alert / notification component (Alert.js) could have a WebdriverIO interface (Alert.webdriver.js) that exported functions:

These were intended to be highly composable. So, if you say had a strack-trace alert (StackTraceAlert.js) when something goes awry, you might have (StackTraceAlert.webdriver.js) which composes the above and exports functions:

Then, you might write a test for it that verifies the stack trace, the copy button and maybe that it's type 'error' not 'info'.

import copyTextBtn from '.../common/components/CopyTextButton.webdriver'
import stackAlert from '.../common/components/StackTraceAlert.webdriver'

it('shows a strack trace alert when there is an uncaught exception', () => {
    expect(stackAlert.countVisible()).toBe(0)

    browser.execute(() => setTimeout(() => throw new Error('OMG an error'), 10))
    browser.waitUntil(() => stackAlert.countVisible() > 0, 5000, 'stack alert not seen')

    const alertElem = stackAlert.mustFind(/omg/gi)
    const copyBtn = copyTextBtn.mustFindWithin(alertElem)
    expect(copyBtn.isEnabled()).toBe(true)
})

(We built a lot of assertions into these interfaces. E.g. the find() and mustFind() functions in StackTraceAlert.webdriver would verify the found element was indeed an error alert since that was a constant built-in expectation of the component.)

And, this is kind of a lame example because it's just testing a simple component. But, when testing user-flows or things that require complex flows to set up the context, it's a handy simplification.

Okay, it's a bit cumbersome to need to pass ElementHandles into utility functions:

import alert_ from '.../common/components/Alert.webdriver'
// ...

const elem = alert_.getActiveAlerts()[0]
expect(alert_.getTitle(alertElem)).toBe('Some Title')

I mean, it's fine, and I preferred that over writing classes, but extension methods might be an interesting possibility:

import alert_ from '.../common/components/Alert.webdriver'
// ...

const alertElem = alert_.getActiveAlerts()[0]

// alertElem is an ElementHandle but it has been enhanced
// before it was returned from `getActiveAlerts()`

expect(alertElem.title()).toBe('Some Title')

// or maybe
expect(alertElem.alert.title()).toBe('Some Title')

// or
expect(alertElem.title).toBe('Some Title')

Maybe enhancements like this can happen in a way that is similar to protypal inheritance. So, for instance, StackTraceAlert.webdriver wouldn't need to add a .close() extension method because its extensions are on top of Alert.webdriver which already added .close() method.

The above APIs are just trying to show the idea and they probably aren't the best examples. But, I can say we definitely found organizing our E2E tests like this to be very liberating. And, some form of support for it Playwright would be really powerful (IMO).

Also, since Playwright is written in TypeScript, ElementHandles enhanced with extension methods could all be typed, too, which is just too good to be true.

Lastly, I'd be interested in contributing to the effort if this or similar functionality ends up getting the green-light.

pavelfeldman commented 4 years ago

I am a big fan of layering as opposed to the aspect injection. Any reason what you describe should not be a layer on top of Playwright that exposes component.element: ElementHandle?

tiffon commented 4 years ago

Nope, that would work, too. I just wanted to drop the suggestion. Will close as this can be revisited, later, should there be any interest in it.