1

I want to run a script that will wait for the promise to return the value.

$dusk->script('return //something');

The javascript function I want to run is a promise.

fetch(url).then(r => r.blob()).then(blob => {
    var reader = new FileReader();
    reader.onload = function() {
        var b64 = reader.result.replace(/^data:.+;base64,/, '');
        console.log(b64); // I want to return this value
    };
    reader.readAsDataURL(blob);
});

I want to return the b64 variable, but this won't happen.

$b64 = $dusk->script("
    var b64;

    fetch(url).then(r => r.blob()).then(blob => {
        var reader = new FileReader();
        reader.onload = function() {
            b64 = reader.result.replace(/^data:.+;base64,/, ''); 
        };
        reader.readAsDataURL(blob);
    });        

    // When `b64` is ready, I want to return it
    return b64;
");
senty
  • 12,385
  • 28
  • 130
  • 260
  • Possible duplicate of [How do I return the response from an asynchronous call?](https://stackoverflow.com/questions/14220321/how-do-i-return-the-response-from-an-asynchronous-call) – Shilly Apr 18 '19 at 08:36
  • So basically you want to wrap the reader.onload() in another promise and return that promise. Then the onload() can resolve that promise. SO you can continue the promsie chain with another .then() – Shilly Apr 18 '19 at 08:38
  • I couldn't figure it out, that's why I asked. How can I wrap `reader.onload = function() {}` in another promise? I'd appreciate an answer that'd work in dusk/selenium. – senty Apr 18 '19 at 08:38

1 Answers1

2

This is one possibility. I would assume it works with dusk/selenium as well. The clue is that the onload function is basically a callback, so we want to use that callback to resolve the promise. If you want sync-looking code, async/await could do that for you as well, but the end result is the same, a promise gets returned so you can continue the promise chain.

fetch(url)
    .then(r => r.blob())
    .then(blob => new Promise(( resolve ) => {
        var reader = new FileReader();
        reader.onload = function() {
            var b64 = reader.result.replace(/^data:.+;base64,/, '');
            resolve( b64 );
        };
        reader.readAsDataURL(blob);
    })
    .then( b64 => {
        // do something with b64
    });

This does imply that you can't return it to the outer $b64 = though.

Edit: So instead of

$b64 = fetch(url)
  .then( response => ... )
  .then( blob => ... )
  .then( image => ... );
renderImage( $b64 );

You would want to do

fetch(url)
  .then( response => ... )
  .then( blob => ... )
  .then( image => ... )
  .then( renderImage );

Or:

var $b64 = fetch(url)
  .then( response => ... )
  .then( blob => ... )
  .then( image => ... );
$b64.then( renderImage );

Edit 2:

Do not wait for the actual Promise, wait for the result of the promise to show up on the screen, so as per the dusk docs, you give the promise 5 seconds to resolve before dusk throws an error.

If the Promise would render the result on the page, try detecting a change in the image tag the image will be rendered to.

$dusk->script("
    fetch(url).then(r => r.blob()).then(blob => {
        var reader = new FileReader();
        reader.onload = function() {
            var b64 = reader.result.replace(/^data:.+;base64,/, '');
            document.querySelector( 'body' ).classList.add( 'loaded_image' );
        };
        reader.readAsDataURL(blob);
    });        
");

$dusk->waitUntil('body.loaded_image');

Solution:

$dusk->script('
     var url = document.getElementById("img_file").getAttribute("src");

     fetch(url)
         .then(r => r.blob())
         .then(blob => new Promise(( resolve ) => {
             var reader = new FileReader();
             reader.onload = function() {
                 var b64 = reader.result.replace(/^data:.+;base64,/, "");
                 resolve( b64 );
             };
             reader.readAsDataURL(blob);
         }))
        .then( b64 => {
            $("body").append(`<input id="b64string" value="${b64}">`);
         });
');

// wait until ajax to be finished
$dusk->waitUntil("!$.active", 30);

$b64Img = $dusk->script("return document.getElementById('b64string').value;"); // this returns array

dd($b64Img[0]); // works!
senty
  • 12,385
  • 28
  • 130
  • 260
Shilly
  • 8,511
  • 1
  • 18
  • 24
  • If I write return b64 in the end (after the last `then()`), wouldn't it just return undefined? Don't I need to return inside the last `then()`? – senty Apr 18 '19 at 08:43
  • That's the point, unless you use async/await syntax, you can't return to an outer variable. you need to do whatever you want to do with $b64 inside the `.then()` handler. The easiest way is just pass a function to `.then()` that will do whatever you want to do with `$b64 = ....`. Or rewrite it all using async/await syntax, so the return works as writtten. Maybe dusk has some syntax sugar as well that let's you unpack then resolved result. – Shilly Apr 18 '19 at 08:44
  • That's where I get confused. How do you mean async/await syntax? I'm pretty confused how to achieve that. I looked into dusk but [_"Each script() call is a separate execution context"_](https://github.com/laravel/dusk/issues/594) - so I won't be able to access it afterwards either; thus need to do it in `$dusk->script()` – senty Apr 18 '19 at 08:47
  • Oh, do you mean, inside the last `then()`, call another function that'll return the b64 variable - let me try that – senty Apr 18 '19 at 08:49
  • If it's confusing, I would not use it. In the end async/await is just syntax sugar to write promises. – Shilly Apr 18 '19 at 08:50
  • What do you want to use the $b64 variable for? I would just keep the outer $b64 ( the one you ant to return to ) as the promise. And then when I want to use $b64, I would just write `$b64.then( b64 => { b64.someMethod() }`. instead of `$b64.someMethod();`. The point is, is there something preventing you from continueing the chain after you want to 'return'? – Shilly Apr 18 '19 at 08:50
  • It's just encoded base64 string of an image, which I'll use it only for once, juts for checking (I'm using Dusk to interact with a 3rd party website, not for testing). I'll use the `$b64` in php to do some checks, but I need to get that variable from javascript :/ I totally understand the code you've written, but not the last step or your last comment. Where you've added `// do something with b64`, if I call another function in javascript, again I won't be able to return b64. – senty Apr 18 '19 at 08:51
  • I have updated my answer with a short snippet. Basically you do not want ot assign the return value to a variable, but just continue using the promise chain. – Shilly Apr 18 '19 at 08:55
  • Oh, I see where you're going - I think you got me wrong. I want to pass the b64 string from javascript to php variable. That's why I need to return the value. The goal is passing the `b64` string from javascript to php – senty Apr 18 '19 at 08:56
  • No idea if that is even possible. Doesn't Dust or selenium offer you methods to continue an async method in php? That's not somehting you can do with javascript or Promsies to begin with. That's something the php will have to solve. The javascript call will give you a Promise, no matter what, unless you make everything sync. So it seems unlikely to me dusk does not have things to handle those. Let's look at the docs. – Shilly Apr 18 '19 at 08:57
  • Won't it be easier to fetch the url and create the image from it fully in PHP? Why do you have to combine javascript and PHP code this way? – Shilly Apr 18 '19 at 09:01
  • Absolutely, but the images are protected by session so I can't access the images with php `file_get_contents()`. I think I may have an idea, maybe I can create a DOM element with the b64 as an attribute, so I can access it afterwards via another `$dusk->script()` – senty Apr 18 '19 at 09:03
  • Have you tried `waitUntil( 'someReferenceToTheLoadedImage' ) after the `$dusk->script()` call? As described in the link you posted in the 3rd comment? That's what I would try now that I understand better. Find a property or attribute of the image or like change the javascript code to put some classname somewhere after b64 has been loaded. – Shilly Apr 18 '19 at 09:05
  • I have added the code I would try to get working then. Inside the onload() handler, add a class or an element or something else that dusk can check for to the DOM. In the example I use the class loaded_image on the body because I do not know if the blob image will actually be rendered inside a tag or not. – Shilly Apr 18 '19 at 09:11
  • Huge huge thanks! I added an edit for my working approach in your answer! You really gave me a way to think it :) – senty Apr 18 '19 at 09:23
  • Glad that you found a working solution. Have a nice day and hf coding. – Shilly Apr 18 '19 at 09:24
  • Thanks, you too! :) – senty Apr 18 '19 at 09:25