Jest and Puppeteer for frontend e2e tests for React Apps


The following guide is a quick setup for how to get started with writing automated end to end tests using puppeteer and running them using jest for react app.

drawing

Install

If your app was created using create-react-app just run: npm install --save-dev puppeteer or yarn add --dev puppeteer

If your app was created without using create-react-app, run: npm install --save-dev jest jest-cli puppeteer or yarn add --dev jest jest-cli puppeteer

Running

In package.json have the following for create-react-apps (should be there by default so no changes) "scripts": { "test": "react-scripts test" } or this otherwise: "scripts": { "test": "jest" }

For running the tests npm test

Some Basic Code

Let's start by writing a very basic test. By default jest will assume all files with test in their filename are test files and start running those, so we can create a file called App.test.js and add the following:

const puppeteer = require('puppeteer');

let browser
let page

const appUrlBase = 'http://localhost:3000'  
const routes = {  
   public: {  
      register: `${appUrlBase}/register`,  
      login: `${appUrlBase}/login`,  
      noMatch: `${appUrlBase}/somethingrandom`,  
   },  
   private: {  
      home: `${appUrlBase}/home`,  
      account: `${appUrlBase}/account`,  
   },  
};

// control
beforeEach (async () => {  
    browser = await puppeteer.launch({
        headless: false, // headless mode set to false so browser opens up with visual feedback
        slowMo: 5, // how slow actions should be
    }); 
    page = await browser.newPage();  
 })

afterEach (() => {  
    browser.close()  
  })

// You can also define beforeAll and afterAll functions


describe('Login', () => {
    test('users can login', async () => {
        await page.goto(routes.public.login);
        await page.waitForSelector('.signin-form');
        await page.click('input[name=email]')
        await page.type('input[name=email]', 'yomi@mail.com')
        await page.click('input[name=password]')
        await page.type('input[name=password]', 'password')
        await page.click('button[type=submit]')
        await page.waitForSelector('[data-testid="homepage"]')
}, 1600000);
});

Posted by Horia Gug

A short QA horror story


T - 3hr

Slack in complete silence, unheard of in normal circumstances. It's Friday and although you opposed it many times it's another release Friday. Devices scattered around your workplace, your office machine, a machine with IE11 two to three iPhone models, couple of Android phones, there's notches there's no notches you got everything covered.

Check lists close to done 99% green, couple of more bugfixes soon coming in, it's now:

T - 2hr

You start reading the bug tickets, you know them by heart, check the pull requests, tests passing, every thing's as you'd like it to be. So you start spinning up that testing environment and plan out those final last checks. The road ahead couldn't be clearer in your mind.

You are half way through your mental check list, it's far too late for spreadsheets now, in your head every possible area of the application is mapped out and there's a check box next to each component just waiting for that check-mark.

The slack notification sound pops up, it's a developer, without a moment's hesitation you open it and read:

So, found anything yet?

You respond hastily and with typos:

Nothign yet but I'm not finished.

You close down Slack, your focus is completely set on the mental check list, you've done this a million times before, only this time everything should work, everything must work .. everything seems like it works, it is now:

T - 1hr

Check list done, the sigh of relief is cut short by a message from the release manager:

Hey, are you guys still releasing this today?


Think so, seems there's nothing wrong with it, I'll see now if I can break it somehow.


Fingers crossed.

You ponder, what could fingers crossed mean? Is it meant as an expectation to find breaking points and postpone the release or is it meant as hope that nothing breaks and everything works fine? You have no time for thoughts like this! Your mindset now shifted, it's no longer about those check marks, it's no longer about making sure it works, it's now about finding out if and how it breaks.

So you starting firing the big guns, throwing punches, launching cannon balls and having a rabid cat like approach to everything but to no avail, it all works, it all appears to work, you check the bug logging tool, nothing new, nothing escalated, no comments.

So are we releasing this then?

You overhear your team mates saying.

Looks like QA didn't find anything so let's do it.

The decision is made, it is now time to press the button, it's release time, it's:

T - 0:

It's gone out, there's relief, there's joy, excitement but some anxiety lingers. Your work mates are in the kitchen having beer so you join in. There's talks of anything but software, the atmosphere is relaxed but you somehow can't breathe. You walk outside, grab some air, open social media and try to take your mind off everything, it is way past time to go home already, so you go back to grab your stuff.

On your way up you finally realize the weekend is here and there's so much you could do, all the stress and anxiety somehow washed away by these liberating emotions, you quickly go back to your desk, open your machine and while it boots you are discretely shaking from the excitement of all the events you are just soon to discover and possibly attend.

The machine loads up and it still has the app opened, so you grab your mouse, you hover the X, the close tool-tip spawns up but you don't click it just yet. You instead start looking at it, and like an owl, turning your head side ways, suddenly say out loud:

Huh, think I've never tried clicking on that before

So you click it, you don't breathe, you don't move you're just standing there looking at how beautifully it crashes, it all crashes, just like the Agitato part of a Rachmaninoff piece all the parts are just making noise, it is now:

Too late.


Posted by Horia Gug

Dynamic disk to basic disk


You might encounter this problem as I have when I was moving away from using Windows as my main Operating System, looking to install a linux based distribution. The issue with having dynamic disks is that the the new OS system you want to install will not recognise them, and you cannot proceed with the install or resize them or anything.

WARNING: Following this guide will result in deleting all of the data on your hard drive. Make sure that your data is backed up before proceeding.

What do you need

If your use case is the same as mine, moving from Windows to another OS it's best that you have 2 USB pen drives ready, but don't fret if you just have one USB drive it will just make the process longer. So basically what you need is:

  • USB drive having a boot-able Windows 10 ISO burned to it. You can use Windows 7 for this too if you are still using it.
  • USB drive having the OS you want to install (if this is in fact what you are doing)
  • Some patience :)

Steps:

  1. More important than anything, backup your data!
  2. Having the Windows USB drive plugged in restart your machine into boot mode. Depending on your machine this is achieved by pressing a key like F2 or ESC during startup
  3. When presented with the boot options choose to boot from the USB drive
  4. In the Windows installer screen that shows up select your language and time settings and proceed.
  5. In the next screen select repair my computer
  6. Go to advanced options
  7. Go to Command Prompt
  8. Once the command prompt opens run the following command: diskpart
  9. Now run list disk. This will list all the disk on your machine. It should look something like this:

DISKPART> list disk

| Disk ###| Status | Size | Dyn | Gpt |
| ------- | ------ | ---- | --- | --- |
|  Disk 0 | Online | 80GB | Yes | --- |
  1. Select the disk you want to modify. In my example we use only one disk which shows up as Disk 0 so the command we're using is: select disk 0. You can choose which disk you want modified from the ones that show up in thee command executed above.

  2. Run the following command clean This will clean your hard drive of all it's data.

  3. At this point you will need to restart the command prompt in order to proceed. So close it by clicking on the X in the window, and reopen it by going through Repair my computer -> Advanced options -> Command Prompt as before
  4. You will need to select your disk again so as before run: diskpart select disk 0
  5. The next two steps might not be necessary but there are cases where clean does not remove the volumes on your disk so these will need to be deleted too. To list the volumes run the next command: detail disk 0
  6. For each volume on the disk type: select volume= <volumenumber> and then delete volume
  7. After that's done select the disk you want converted again select disk 0
  8. Finally run convert basic

Congratulations your disk is now just a basic (b..) disk! You can proceed now with installing another OS on your machine just fine. You can:

  1. Shut down your machine
  2. Plug in the second USB drive with your desired OS
  3. Install it :)

Posted by Horia Gug

Many To Many relationship in Flask


Many To Many relationship in Flask App using SQLAlchemy Flask-WTForm and QuerySelectMultipleField

In this example we'll be using two entities, post and categories for whom we'll be building a many to many relationship.

The many to many relationship adds an association table between the two model classes, this helper table is strongly recommended to not be a model but an actual table.

Let's start with our models:

models.py

categories = db.Table('categories',
                  db.Column('post_id', db.Integer, db.ForeignKey('post.id'), primary_key=True),
                  db.Column('category_id', db.Integer, db.ForeignKey('category.id'), primary_key=True))


class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    body = db.Column(db.String(250))
    categories = db.relationship('Category', secondary=categories,
                                  backref=db.backref('posts', lazy='subquery'))

class Category(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    category = db.Column(db.String(140))

The association table categories is a super simple table containing just two columns both ForeignKey references to the id's of the post and the category representing the connection between the two.

However just having an association table is not enough to connect the two entities together so the categories attribute in the Post model is added. Since this is a many to many relationship a secondary table is needed to be specified in the relationship, this is a reference to the association table. The back reference backref can be visualised like adding another field to the Category model.Lazy is set to subquery which just joins the two tables.

Remember to migrate and update your db after making alterations. flask db migrate -m "many to many"

flask db upgrade

Next let's move on to our forms. We'll be focusing on the form for creating a post and associating many categories to it.

forms.py

def possible_categories():
    return Category.query.all()

class PostForm(FlaskForm):
    body = TextAreaField('Say something', validators=[DataRequired()])
    categories = QuerySelectMultipleField('Categories', query_factory=possible_categories, get_label='category')
    submit = SubmitField('Submit')

How the QuerySelectMultipleField field works is that it will display a select drop-down field to choose between ORM results in a sqlalchemy query. For this the query_factory needs to be set to either a query or a function that returns ORM results, in our example the function possible_categories() is defined above that returns all the categories that are currently defined. For displaying we're using the category label as that will hold the category's name.

Moving on to the view function:

@app.route('/create_post', methods=['GET', 'POST'])
def create_post():
    form = PostForm()
    if request.method == 'POST':
        if form.validate_on_submit():
            new_post = Post(body=form.body.data)

            for categories in form.categories.data:
                new_post.categories.append(categories)

            db.session.add(new_post)
            db.session.commit()
            flash('New post added successfully!')
        else:
            flash_errors(form)
            flash('Blog post was not added.')
    return render_template('create_post.html', form=form)

Nothing too fancy here, we create a new post with the data inputted in the form, and then append each of the selected categories to it and save it in the db.

Lastly the html file. Simply:

create_post.html

<h1> Create A new blog post</h1>
<div class="row">
    <div class="col-md-4">
        {{ wtf.quick_form(form) }}
    </div>
</div>

The implementation of the create category form and view goes along the same lines so I'll leave it for the readers homework.


Posted by Horia Gug

Horia's Blog

Welcome to my blog! The idea for this sparked as a side project for me to learn flask but since deployment was a chapter in my learning path it got published!

About Me

I'm a software enthusiast currently working as a quality assurance automation specialist by day and software developer by night, currently living in Helsinki.

You can find me on Twitter , GitHub , or via email at: hg_hel@protonmail.com

Thank you for visiting and enjoy!

All Posts:

Jest and Puppeteer for frontend e2e tests for React Apps

A short QA horror story

Dynamic disk to basic disk

Many To Many relationship in Flask