Goals:
- page objects should be cross-ecosystem, cross-technology
- simple API
- interop with Ember, and WebDriver (via adapters, which means testing-library would be supported, if anyone wanted to use that).
- all async/awaitable
- componentizable -- encourage separation of structure from the actual page object creation.
- all properties lazily evaluated
Nomenclature:
- Adapter: Converts meaning in to behavior -- "click" means different things to ember and webdriver
- Page Object: a literal object that represents how one would interact with the page
Proposed API:
import { setAdapter, create } from 'unknown package';
import { EmberAdapter } from 'unknown package/ember';
setAdapter(EmberAdapter);
const definition = {
login: {
username: '[data-test-username]',
password: '[data-test-password]',
submit: '[data-test-submit]',
},
list: {
scope: '[data-test-list]',
rows: collection('[data-test-row]', {
click: '[data-test-row-clicker]'
});
}
};
const page = create(definition);
// Usage
await page.login.username.click();
page.login.username.isFocussed; // true
await page.login.username.fillIn('text'); // username is now "text"
page.login.username.dom // <input type='text' data-test-username ... >
page.login.submit.fillIn('text'); // Error!: buttons are not fillable
await page.login.submit.click();
// for typescript, types are optional
// by default, all methods and properties will be available, and will throw runtime errors
// but the type of the interactable will still auto-complete for all methods and properties.
interface Definition {
login: {
submit: ButtonInteractor;
}
};
const typedPage = create<Definition>(definition);
// by default, every entry in the page object would be of type "OmniInteractor"
Usage with qunit-dom
assert.dom(page.login.username.dom).hasClass(...);
A few thoughts:
Adapters
Definitely a good idea. Questions:
scope
How are they scoped? I would propose globally, but replaceable. So the happy path would be calling
setAdapter()
somewhere global liketest-helper.js
in Ember. But if mixing different kinds of tests (similar to old Ember testing where acceptance and integration tests has different APIs for interacting with the DOM), could callsetAdapter()
inbeforeEach
? Another idea that's maybe overkill in the initial implementation but maybe worth considering is apushAdapter()
/popAdapter()
API, which would allow calling frombeforeEach
/afterEach
hooks, or using Ember's style,setupSomeKindOfPageObject(hooks);
.auto-selection
It seems like it wouldn't be too hard to detect certain test environments so you wouldn't have to explicitly set a test adapter if using a vanilla version of one of those environments. In this case,
pushAdapter()
/popAdapter()
functionality is probably more important for overriding the auto-selection for specific tests.Lazy vs. eager evaluation
Meaning, when accessing a page sub-object, do we immediately look up its DOM element, or wait until a method is invoked on it that requires its DOM element?
do we lazy evaluate?
ember-cli-page-object
does, and It enables scenarios like:I think we probably want lazy evaluation, especially because of the following scenario:
collections
The lazy evaluation question gets complicated with collections. In your example above, what type does
page.list.rows
return? If it's anArray
then we're not lazy evaluating. If we do lazy evaluate, then it has to be some array wrapper or proxy thingy or something. This is whyember-cli-page-object
collections implement a few specificEmberArray
-like methods (or maybe the full immutable interface, I forget). I've always found this a bit un-ergonomic, but don't have a great idea for how to improve it (since I'm very skeptical that eager evaluation would provide a better overall experience).Writing page objects
properties/methods
It's not clear to me from your write-up what properties/methods are available on page sub-objects. In
ember-cli-page-object
, there are some default properties and methods available on all sub-objects (which I think they call components), but outside of those the page object author defines what's available on a sub-object by sub-object basis, including the name, e.g.where there is no page sub-object for the submit button, just an action on the form named
submit
that ends up clicking on a particular descendant DOM element of the form page (sub-)object's DOM element.Part of the value proposition of
ember-cli-page-object
is the level of indirection allowed by this naming, sort of like behavior-driven testing, so if the functionality of your page doesn't change, but the UI gets significantly refactored you're more likely to be able to just reimplement your page object behind the same page object "API" and not have to change any of your tests. In practice, I've found this benefit of fairly limited value, as it tends to be more trouble than it's worth (or than it feels like it's worth) to write page objects whose behaviors are sufficiently abstracted from the DOM structure.But anyway, this whole question is unclear to me from your snippet above. When I do
who/what determines what properties/methods are available on
page.bar
?custom properties/methods
Do we support custom properties? This is related to the above question, but if I want to implement the equivalent of a Javascript getter, where maybe I'm aggregating something across a collection of sub-elements, or accessing some really specific thing like calling
bbox()
on an SVG element, can I do that in the page object framework? Or do I just write a helper outside of the page object to get the value?Similarly for compound actions? It's fairly common for me to do something like
Would we support that? I think if the framework did something similar to
ember-cli-page-object
and allows methods and getters on the definition, and invoked them withthis
properly set to the instantiated page object, it would work. But this should be defined/specified.re-usable sub-objects/components
It's a pretty common use case to have a sub-object (or component in
ember-cli-page-object
's nomenclature) defined separately from a page, e.g. if it represents a reusable component rendered in multiple places in the application. Perhaps we just followember-cli-page-object
's lead, which I think is implied by your example in having the definition be a separate structure from the created page object. But it's certainly worth being clear on this.differences from
ember-cli-page-object
Other than the framework-independence, what goals/requirements differ from
ember-cli-page-object
? Would it make more sense to useember-cli-page-object
as a starting point and remove/modify to meet differing requirements rather than green-fielding?