Uploading Images in Behat both at Domain Level and UI Level

Posted: 2016-08-11 22:36:38

The goal here is as follows

  @fileCleanup @javascript
  Scenario: Uploading File
    Given I already made a Segmentation and am editing it
    Then I add file and the system will process it
    And I can not add another file till it is done
    And when the system is done I can add another file

In this case they are large files and I want to get an MVP for the users to interact with so one file at a time is enough for now.

The tricky part is that I am running Selenium on my Mac and running behat in my VM there is no file path connection between the two. This can also be an issue when using Saucelabs and https://crossbrowsertesting.com/.

UI Level

I am using the Flow Angular library so it is JavaScript heavy (some notes below). Otherwise your work is even easier.

My step to set things up Given I already made a Segmentation and am editing it just adds that resource to the system and then I visit it.

From there I hit Then I add file and the system will process it and this is where it all begins

    /**
     * @Then I add file and the system will process it
     */
    public function iAddFileAndTheSystemWillProcessIt()
    {
        $this->visit('/segments/files/' . $this->segment_uuid);
        sleep(2); //load up the page
        $this->assertPageContainsText('Upload File'); //make sure the button is there

        /**
         * Here is the meat of it, or tofu, I have to deal with the fact I am running 
         * Selenium on a different machine than my VM's filesystem
         * So I inject this into the WebDriverSession
         * Which only exists because this Scenario is marked `@javascript`
         */
        $localFile = base_path('features/fixtures/data_columns_not_snaked.xls');
        $tempZip = tempnam('', 'WebDriverZip');
        $zip = new \ZipArchive();
        $zip->open($tempZip, \ZipArchive::CREATE);
        $zip->addFile($localFile, basename($localFile));
        $zip->close();

        $remotePath = $this->getSession()->getDriver()->getWebDriverSession()->file([
            'file' => base64_encode(file_get_contents($tempZip))
        ]);
         /** end the hard part **/

        $this->attachFileToField('image_upload', $remotePath); //see Flow notes at the bottom of this page

        $this->assertPageNotContainsText('Upload File');

        unlink($tempZip);

        sleep(5); //Later on I will move this to `spin` see notes below
        $this->assertPageContainsText('data_columns_not_snaked');

    }

Let me show that again but now more simple just a normal input button

    /**
     * @Then I add file and the system will process it
     */
    public function iAddFileAndTheSystemWillProcessIt()
    {
        $this->visit('/segments/files/' . $this->segment_uuid);
        sleep(2); //load up the page
        $this->assertPageContainsText('Upload File'); //make sure the button is there

        /**
         * Here is the meat of it, or tofu, I have to deal with the fact I am running 
         * Selenium on a different machine than my VM's filesystem
         * So I inject this into the WebDriverSession
         * Which only exists because this Scenario is marked `@javascript`
         */
        $localFile = base_path('features/fixtures/data_columns_not_snaked.xls'); 
        $tempZip = tempnam('', 'WebDriverZip');
        $zip = new \ZipArchive();
        $zip->open($tempZip, \ZipArchive::CREATE);
        $zip->addFile($localFile, basename($localFile));
        $zip->close();

        $remotePath = $this->getSession()->getDriver()->getWebDriverSession()->file([
            'file' => base64_encode(file_get_contents($tempZip))
        ]);
         /** end the hard part **/

        $this->attachFileToField('image_upload', $remotePath); //the input field

         /** prove the button is gone during this process **/ 
        $this->assertPageNotContainsText('Upload File');

        unlink($tempZip);

        $this->pressButton('Upload File'); //dealing with normal input button

    }

Domain Level

Since my Controller gets the info from the incoming Request and hands it to the Repository class, I like to test my classes outside the Controller then plug them in (see here for more on that), there is typically no need to pass the full Request to the Repository. But in this case I went about passing the entire Request to the Repository and doing some checking in there as well.

    /**
     * @Then I should be able to upload an image file
     */
    public function iShouldBeAbleToUploadAnImageFile()
    {
        $request = new \Illuminate\Http\Request();
        $file = new \Symfony\Component\HttpFoundation\FileBag();
        $path = base_path('features/fixtures/data_columns_not_snaked.xls');
        $originalName = 'data_columns_not_snaked.xls';
        /** note `true` passed in to `UploadedFile` it defines this as a test request **/
        $upload = new \Illuminate\Http\UploadedFile($path, $originalName, null, null, null, true);
        $file->set('profile_image', $upload);
        $request->files = $file;

        $this->repo = new \App\Repositories\ProfileRepository();
        $results = $this->repo->uploadUserProfileImage($request);

        PHPUnit::assertTrue($results, "Repo did not return true");

        PHPUnit::assertTrue(File::exists(public_path('storage/' . $this->user->id . '/data_columns_not_snaked.xls')), "File Not found");
    }

And now that is passing as well.

Thats It

I am now testing both the UI and the Domain level part of this Application.

Note

Using Flow Library https://github.com/flowjs/ng-flow makes it easy to process large files in PHP for me.

One thing I had to do was do was set flow-attrs to the directive so I can get a name in there to target field.

<div class="btn btn-xs btn-primary" flow-attrs="{name:'image_upload'}" flow-btn ng-if="vm.file_uploading">
  <i class="fa fa-cloud-upload"></i> Upload File
</div>

Link to Spin on Behat docs and StackOverflow for a better solution than sleep

http://stackoverflow.com/questions/28510155/how-to-get-the-mink-selenium-2-driver-to-wait-for-the-page-to-load-with-behat

http://docs.behat.org/en/v2.5/cookbook/using_spin_functions.html

And a bit more on that http://www.tentacode.net/10-tips-with-behat-and-mink