#How do I implement this weird interface that extends `Promise`?
109 messages · Page 1 of 1 (latest)
test('Submit a form', async t => {
await t
// automatically dismiss dialog boxes
.setNativeDialogHandler(() => true)
// drag the pizza size slider
.drag('.noUi-handle', 100, 0)
// select the toppings
.click('.next-step')
.click('label[for="pepperoni"]')
.click('#step2 .next-step')
// fill the address form
.click('.confirm-address')
.typeText('#phone-input', '+1-541-754-3001')
.click('#step3 .next-step')
// zoom into the iframe map
.switchToIframe('.restaurant-location iframe')
.click('button[title="Zoom in"]')
// submit the order
.switchToMainWindow()
.click('.complete-order');
});
This project has its own declaration file and I'm trying to figure out how to, ahem, steal it
because I really like this API
So that t is a TestController
it looks like this:
interface TestController {
// ...
click(selector: string | Selector | NodeSnapshot | SelectorPromise | ((...args: any[]) => Node | Node[] | NodeList | HTMLCollection),
options?: ClickActionOptions): TestControllerPromise;
}```
And that TestControllerPromise is really weird and I don't know how I could implement it:
interface TestControllerPromise<T=any> extends TestController, Promise<T> {
}
On one hand, I don't know how a function could ever return this interface, but on the other hand, the testcafe implementation seems to pull it off: I can do await t.click(...).click(...) in testcafe with no problem
Here's my attempt at implementing my own click:
click(
selector:
| Selector,
): TestControllerPromise {
return new Promise<void>((resolve) => {
resolve();
}).then<TestController>(() => {
selector.$.trigger("click");
return this;
}) as TestControllerPromise;
}
But mine doesn't chain the same way: after every call to click(...), a Promise<TestController> is returned. This prevents me from doing await t.click(...).click(...). I can only do something like const firstClick = await t.click(...); const secondClick = await firstClick.click(...)
So how would I implement my click differently so it could change the way testcafe's example does?
Preview:```ts
const foo = {
then(callback: (result: number) => void) {
callback(42)
},
}
;(async () => {
console.log(await foo)
})()```
hm, I think I figured out how to get it to compile without type assertions:
click(
selector: Selector
): TestControllerPromise {
return Object.assign(
new Promise<void>((resolve) => {
resolve();
}).then<TestController>(() => {
selector.$.trigger('click');
return this;
}),
this
);
}
Very strange. I guess I assumed that Promise was frozen. I didn't know you could do things like this.
You can either return a promise which you then attach more methods to it
Or you can use the fact that await works on all thenable objects (not strictly promises)
So as long as the object contains a then method with the correct signature, you can await it.
I didn't know that
You can click the run in TS playground and see 42 being logged out.
yep
Okay I need to take a little side tangent: Promise constructors execute synchronously, right? Will a thenable object's then() function execute asynchronously?
I guess if I put an infinite loop in it, that would answer my question
async/await is purely syntactic sugar.
When you write code:
console.log(1)
const result = await foo
console.log(2)
You can think of it as your code getting transformed into:
console.log(1)
foo.then(result => {
console.log(2)
})
And then executed.
got it
What foo.then(callback) needs to do, is to simply execute the callback whenever it's finished.
oh wait
so then calls the callback like the Promise constructor?
why does the mdn documentation use this example:
myPromise
.then((value) => `${value} and bar`)
.then((value) => `${value} and bar again`)
.then((value) => `${value} and again`)
.then((value) => `${value} and again`)
.then((value) => {
console.log(value);
})
.catch((err) => {
console.error(err);
});
Am I blind? I don't see any then implementations calling a callback
.then can decide to call the callback whenever it feels like it, eg when the work is actually finished.
That makes sense to me. I don't understand why this mdn example doesn't ever seem to call the callback though
I'm okay accepting what you're saying is fact and assuming mdn made the worst example imaginable
myPromise.then decides when the callback is called.
new Promise() constructor basically wraps it up nicely for you.
right
Preview:```ts
let _callback: (value: string) => void
setTimeout(() => {
_callback("foo")
}, 1000)
const myPromise = {
then(callback: (value: string) => void) {
_callback = callback
},
}
myPromise.then(value =>
console.log(${value} and bar)
)```
Here's a very simplified implementation of new Promise that illustrates the idea
It's the equivalent to MDN's example but with only the first then.
Actual implementation would be a lot more complicated since then needs to return a promise like so it can be chained, but yeah you get the idea.
but... neither call the callback (except that setTimeout function)
whatever. I think that MDN example is terrible, especially considering it talks about thennable functions after it
The resolve calls the callback.
Preview:```ts
function newPromise(
executor: (resolve: (value: string) => void) => void
) {
let _callback: (value: string) => void
executor(value => _callback(value))
return {
then(callback: (value: string) => void) {
_callback = ca
...```
Here's what it looks like when properly wrapped together.
I understand what you are saying out of context, but I don't understand how it explains MDN's example:
myPromise
.then((value) => `${value} and bar`)
.then((value) => `${value} and bar again`)
.then((value) => `${value} and again`)
.then((value) => `${value} and again`)
.then((value) => {
console.log(value);
})
.catch((err) => {
console.error(err);
});
oh wait
one sec
The Playground lets you write TypeScript or JavaScript online in a safe and sharable way.
@shrewd perch Here's a shortened URL of your playground link! You can remove the full link from your message.
Preview:```ts
function newPromise(
executor: (
resolve1: (value1: string) => void
) => void
) {
let _callback1: (value2: string) => void
executor(value3 => _callback1(value3))
return {
then(callback2: (value4: string) => void) {
_callback1 = cal
...```
Anyway, I don't think I can do this without changing the TestControllerPromise interface. It extends Promise, and Promise has more than just then().
Yeah for practicality it's easier to just attach methods to the promise, than to reimplement everything.
I was attempting to do that here ^
click(
selector: Selector
): TestControllerPromise {
return Object.assign(
new Promise<void>((resolve) => {
resolve();
}).then<TestController>(() => {
selector.$.trigger('click');
return this;
}),
this
);
}
am I doing that wrong?
Hmm, not sure what the best implementation is.
shoot. I think I misunderstood this originally and that caused all the confusion
I'm completely on the same page now
Preview:```ts
const wait = () =>
new Promise(resolve => setTimeout(resolve, 1000))
const methods = {
click(this: Promise<void>, arg: string) {
return attachMethods(
this.then(() => wait()).then(() =>
console.log("click", arg)
)
)
},
drag(this: Promise<void>, arg: string) {
return attachMet
...```
My attempt at it, you can abstract it further but I leave it at that.
neat, thanks
The one part that confuses me is where the this: Promise<void> comes from. It would make sense to me if the bottom had this implementation:
(async function() {
await form().click('foo').drag('bar').click('baz').drag('qux')
console.log('done')
})()
But you used an arrow function, so I'm not sure what this is or where it comes from
my gut tells me that's a JavaScript concept I'm unfamiliar with
;(async () => {
// ...
})()
This is just an IIFE, it's only used here to execute async code because TS playground doesn't support it for some reason.
The this: Promise<void> in each method, comes from:
Object.assign(target, methods)
// ^^^^^^
Yeah that part I understand, I just don't know where else the this could come from, especially as a promise
That's blowing my mind. I thought this is like the context in which the code is running and you have to use a bind or call or apply (I can't member which off the top my head) to reassign it. Why does Object.assign(target, methods) influence this?
Is this just standard this usage in javascript? I must've missed this way of assigning it
I think I get it
man that's nuanced (IMO)
When you do foo.bar(), this becomes foo inside of bar's code.
I had no idea. I mean, I did, but I thought only when foo is a function or a non-primitive, non-literal object
I could've never implemented this API without knowing that. Thanks!
ohhhh
drag is a method
I don't know how I'm gone this long without realizing you can create methods that way
I thought every function within a { } literal was a... function
actually, I didn't even know this was valid syntax
const methods = {
click(this: Promise<void>, arg: string) {
return attachMethods(this.then(() => wait()).then(() => console.log('click', arg)))
},
...```
I thought you had to do this inside an object literal:
const methods = {
click: (arg: string) => {
return attachMethods(this.then(() => wait()).then(() => console.log('click', arg)))
},
or
const methods = {
click: function (arg: string) {
return attachMethods(this.then(() => wait()).then(() => console.log('click', arg)))
},
wow. I don't know how I went so long without knowing this
Actually, as far as I can tell, your syntax is equivalent to my last example?
err
I mean if I give it this first param: this: Promise<void>,
Tbf, if you don't write code with this (which I don't), it's not exactly the most useful thing 😄
Not exactly, in my code it chains the current promise, whereas in yours it executes immediately.
yeah I usually avoid this and classes
Anyway, it seems to me your solution has kind of gone full circle. It's chaining thens to the form's returned promise
I think I get this. Thank you!
By the way, I noticed the type of this changed depending on whether or not I included it as a parameter in these methods. I don't have the understanding to know if I need to learn more about typescript or more about JavaScript to understand that. Alternatively, it could have just been the compiler was confused
Preview:```ts
const wait = () =>
new Promise(resolve => setTimeout(resolve, 1000))
const methods = {
click(arg: string) {
return attachMethods(
this.then(() => wait()).then(() =>
console.log("click", arg)
)
)
},
drag(this: Promise<void>, arg: string) {
return attachMet
...```
Yeah, the this parameter in a function/method is just a stub to tell TS what this should be
And TS will type check when you try to call.
so removing that param had no effect on the generated JS?
Nope, you can take a look at the JS output in TS playground.
ah, right. It's not even there
Okay thanks again
I've been trying to figure out how testcafe's api lets me do things like this but your example doesn't:
const x = await form().click("foo");
await x.drag("bar").click("baz").drag("qux");
Turns out, they cheated. I'd never noticed before, but in testcafe, that x is an any
That said, they are doing something different, because in their API, that chains as though it were one statement. The example stops after the first line
Make the promise return another form() instead of void.
Preview:```ts
type TestControllerPromise = Promise<any> &
typeof methods
const wait = () =>
new Promise(resolve => setTimeout(resolve, 1000))
const methods: {
click: (
this: Promise<void>,
arg: string
) => TestControllerPromise
drag: (
this: Promise<void>,
arg: string
) => TestControllerPromise
} = {
click(
...```
I probably didn't put the form() everywhere I needed
Sorry, this isn't a requirement:
const xxx = await t
.click(Selector('.passage button').withText('a'))
.expect(Selector('.flash-message').innerText)
.eql('a')
await xxx
.click(Selector('.passage button').withText('b'))
.expect(Selector('.flash-message').innerText)
.eql('b')
.click(Selector('.passage button').withText('c'))
.expect(Selector('.flash-message').innerText)
.eql('c')
// TypeError: Cannot read properties of undefined (reading 'click')
I guess I've never tried the above, I only thought I had. But this works:
await t
.click(Selector('.passage button').withText('a'))
.expect(Selector('.flash-message').innerText)
.eql('a')
await t
.click(Selector('.passage button').withText('b'))
.expect(Selector('.flash-message').innerText)
.eql('b')
.click(Selector('.passage button').withText('c'))
.expect(Selector('.flash-message').innerText)
.eql('c')
I suppose that's much easier to implement