Am trying to provide test authors with a fluent PageModel api in TestCafe, like:
await MyApp // a Page Model class instance
.navigateTo(xyz) // clicks a button to navigate to a specific part in my app
.edit() // clicks the edit button
.setField(abc, 12.34)
.save()
.changeStatus('complete');
I had all the individual methods working as async methods that can be awaited individually, but that makes the code quite unreadable and as a result error prone.
However, whatever way I attempt to make the api fluent, it results in the following error:
Selector cannot implicitly resolve the test run in context of which it should be executed. If you need to call Selector from the Node.js API callback, pass the test controller manually via Selector's
.with({ boundTestRun: t })
method first. Note that you cannot execute Selector outside the test code.
The trick into making a fluent async api is imho switching from async functions to regular functions as methods and have those methods return a thenable 'this' value. And in order to prevent the await oscillating, the 'then' function needs to be removed once called (and then reinstalled when
A very basic example that reproduces the issue can be seen below:
import { Selector } from 'testcafe'
class MyPage {
queue: [];
async asyncTest() {
return await Selector(':focus').exists;
}
queuedTest() {
this.then = (resolve, reject) => {
delete this.then; // remove 'then' once thenable gets called to prevent endless loop
// calling hardcoded method, in a fluent api would processes whatever is on the queue and then resolve with something
resolve(this.asyncTest());
};
// In a real fluent api impl. there would be code here to put something into the queue
// to execute once the 'then' method gets called
// ...
return this;
}
}
fixture `Demo`
.page `https://google.com`;
test('demo', async () => {
const myPage = new MyPage();
console.log('BEFORE')
await myPage.asyncTest();
console.log('BETWEEN')
await myPage.queuedTest(); // Here it bombs out
console.log('AFTER')
});
Note that the sample above isn't showcasing a fluent api, it just demonstrates that calling methods that use Selectors through the 'then' function (which imho is key to creating a fluent api) results in the aforementioned error.
Note: I know what the error means and that the suggestion is to add .with({boundTestRun: t})
to the selector, but that would result in required boilerplate code and make things less maintainable.
Any thoughts appreciated P.
CodePudding user response:
In your example, a selector cannot be evaluated because it does not have access to the test controller (t). You can try to avoid directly evaluating selectors without assertion.
Here is my example of the chained Page Model (based on this article: Async Method Chaining in Node):
Page Model:
import { Selector, t } from 'testcafe';
export class MyPage {
constructor () {
this.queue = Promise.resolve();
this.developerName = Selector('#developer-name');
this.submitButton = Selector('#submit-button');
this.articleHeader = Selector('#article-header');
}
_chain (callback) {
this.queue = this.queue.then(callback);
return this;
}
then (callback) {
return callback(this.queue);
}
navigateTo (url) {
return this._chain(async () => await t.navigateTo(url));
}
typeName (name) {
return this._chain(async () => await t.typeText(this.developerName, name));
}
submit () {
return this._chain(async () => await t.click(this.submitButton));
}
checkName (name) {
return this._chain(async () => await t.expect(this.articleHeader.textContent).contains(name));
}
getHeader () {
this._chain(async () => console.log(await this.articleHeader.textContent));
return this;
}
}
Test:
import { MyPage } from "./page-model";
fixture`Page Model Tests`;
const page = new MyPage();
test('Test 1', async () => {
await page
.navigateTo('http://devexpress.github.io/testcafe/example/')
.typeName('John')
.submit()
.checkName('John')
.getHeader();
});