#How do I implement this weird interface that extends `Promise`?

109 messages · Page 1 of 1 (latest)

shrewd perch
#

I'm trying to implement an API similar to TestCafe's. Here's an example of what that code looks like:

#
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?

cursive scaffoldBOT
#
nonspicyburrito#0

Preview:```ts
const foo = {
then(callback: (result: number) => void) {
callback(42)
},
}

;(async () => {
console.log(await foo)
})()```

shrewd perch
#

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.

copper quest
#

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.

copper quest
shrewd perch
#

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

copper quest
#

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.

shrewd perch
#

got it

copper quest
#

What foo.then(callback) needs to do, is to simply execute the callback whenever it's finished.

shrewd perch
#

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

copper quest
#

.then can decide to call the callback whenever it feels like it, eg when the work is actually finished.

shrewd perch
#

I'm okay accepting what you're saying is fact and assuming mdn made the worst example imaginable

copper quest
#

myPromise.then decides when the callback is called.

#

new Promise() constructor basically wraps it up nicely for you.

shrewd perch
#

right

cursive scaffoldBOT
#
nonspicyburrito#0

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)
)```

copper quest
#

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.

shrewd perch
#

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

copper quest
#

The resolve calls the callback.

cursive scaffoldBOT
#
nonspicyburrito#0

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
...```

copper quest
#

Here's what it looks like when properly wrapped together.

shrewd perch
#

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

#
cursive scaffoldBOT
#

@shrewd perch Here's a shortened URL of your playground link! You can remove the full link from your message.

bawdyinkslinger#0

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
...```

shrewd perch
copper quest
#

Yeah for practicality it's easier to just attach methods to the promise, than to reimplement everything.

shrewd perch
#
  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?

copper quest
#

Hmm, not sure what the best implementation is.

shrewd perch
#

I'm completely on the same page now

cursive scaffoldBOT
#
nonspicyburrito#0

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
...```

copper quest
#

My attempt at it, you can abstract it further but I leave it at that.

shrewd perch
#

neat, thanks

shrewd perch
#

my gut tells me that's a JavaScript concept I'm unfamiliar with

copper quest
#
;(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)
//            ^^^^^^
shrewd perch
#

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)

copper quest
#

When you do foo.bar(), this becomes foo inside of bar's code.

shrewd perch
#

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>,

copper quest
copper quest
shrewd perch
#

hm, one is a property, one is a method

shrewd perch
#

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

cursive scaffoldBOT
#
bawdyinkslinger#0

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
...```

copper quest
#

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.

shrewd perch
#

so removing that param had no effect on the generated JS?

copper quest
#

Nope, you can take a look at the JS output in TS playground.

shrewd perch
#

ah, right. It's not even there

shrewd perch
shrewd perch
#

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

copper quest
#

Make the promise return another form() instead of void.

shrewd perch
#

oh, nice

#

oops

#

it wasn't working, I reverted it to one line and forgot

cursive scaffoldBOT
#
bawdyinkslinger#0

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(
...```

shrewd perch
#

I probably didn't put the form() everywhere I needed

shrewd perch
#

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