Jordan Does Integration Tests on Citadel Packaging

Demo code here

The next few posts are going to be on integration testing using Puppeteer. I really, really am loving Puppeteer. It’s a really powerful tool and it’s just a pleasure to use. In the next post I’m going to go over setting up this code (including Puppeteer!) on Digital Ocean. I really stand by the fact that I think Digital Ocean is probably one of the easiest places to get started with a clean cloud hosting solution. You get your own IP address and the billing is clean and understandable.

The goal with integration tests are to test the functionality of at least multiple parts of our website working together. I actually open up a browser instance with puppeteer and pretend to be a user of the website. If one of my tests fail, that should indicate that I have some failure in my website/code. Integration tests differ from unit tests in that they don’t isolate the failure to a specific unit but they do offer a more “real” experience of what the user is expecting to see.

At the time of writing this I have only six different tests. I am testing Citadel Packaging, an ecommerce site. The owners, Matt and Brandon, are good friends of mine since childhood. A site like this has a LOT of different moving parts and I could easily have 10 or 50 times this many tests. What is done here is just an example of what could be done. In a real life scenario, ideally I’d create tests around problem areas. If a bug comes up in an area, I would fix the bug and then build an automated test that would check to make sure it didn’t come back.

Puppeteer is all promise based so this allows us to use async/await. Check this for more explanation on async/await.

Set up before the tests

describe('Citadel Packaging', () => {
    let browser: Browser;
    let page: Page;

    before(async () => {
        browser = await puppeteer.launch({ headless: false });
        page = await browser.newPage();
        await page.setViewport({ width: 1920, height: 1080 });
    });

    after(async () => {
        await browser.close();
    });

I like to go over the top with the amount of describe blocks I have. When you have a suite of hundreds of tests it makes it a lot easier to find the problem when there are more describes to…describe what is wrong. I’m using Mochajs for my test framework and Chaijs as my assertion library. The before and after blocks are Mocha’s way of setting up your environment so you don’t have to duplicate these items in each test. Because Citadel Packaging has a responsive web design (good job, Matt) I also want to set the viewPort so that the html layout is what I expect.

Test 1: it should have 5 tabs

    describe('Base page layout', () => {
        it('should have 5 tabs', async () => {
            const url = 'https://www.citadelpackaging.com/';
            await page.goto(url);

            await page.waitForSelector('#menu-main-menu > li');
            const tabs = await page.$$('#menu-main-menu > li');

            expect(tabs.length).to.equal(5);
        });
    });

The section of this decribe is for the base page layout. I want to confirm that there are five tabs in the nav. I simply waitForSelector of the item I want to test as a way to confirm that the DOM has been loaded in, then I check all #menu-main-menu > li elements to ensure there are actually five.

Five items in the nav list

Test 2: it should have the “added” class when clicking add to cart button

    describe('Add to cart', () => {
        it('should have the "added" class when clicking add to cart button', async () => {
            const url = 'https://www.citadelpackaging.com/product-category/glass-containers/glass-bottles/';

            await page.goto(url);

            await page.waitForSelector('.add_to_cart_button.ajax_add_to_cart');

            const addToCartButton = await page.$('.add_to_cart_button.ajax_add_to_cart');

            let addToCartButtonClasses = '';
            if (addToCartButton) {
                await addToCartButton.click();

                await page.waitForSelector('.added');

                addToCartButtonClasses = await getPropertyByHandle(addToCartButton, 'className');
            }
            
            expect(addToCartButtonClasses).includes('added');
        });
    });

Making sure that the add to cart button is important to any ecommerce website and this test confirms that it has a least some of the functionality I expect. It should at least change the style of the add to cart button to tell the user that they successfully adding something to the cart.

This time I await page.waitForSelector('.add_to_cart_button.ajax_add_to_cart');. Once I confirm that it’s loaded in, I click it and then save off all the classes with a custom puppeteer helper function, addToCartButtonClasses = await getPropertyByHandle(addToCartButton, 'className');. I then just assert that in this string of classes that ‘added’ is included there.

Green check box for added

Test 3: it should show items in the cart after they have been aded

        it('should show items in the cart after they have been aded', async () => {

            const url = 'https://www.citadelpackaging.com/product-category/glass-containers/glass-bottles/';

            await page.goto(url);

            await page.waitForSelector('.number-item .item');

            let numberOfItemsInCart = await getPropertyBySelector(page, '.number-item .item', 'innerHTML');

            // Remove 'items' and parseInt
            if (numberOfItemsInCart) {
                numberOfItemsInCart = parseInt(numberOfItemsInCart.split(' ')[0]);
            }

            expect(numberOfItemsInCart).to.be.greaterThan(0);
        });

This and the next test are both part of another describe ‘After adding to cart’. It assumes that the previous test has been done and that I already have items in the cart. I think this would be a place that I would improve upon so that this describe block isn’t dependent upon another test.

After an item is added to the cart, I want to confirm that the cart has it. This time I just go to the html element that holds the quantity get the innerHTML. The innerHTML returned by .number-item .item is actually ‘xxx item(s)’ so I split off the end part and keep the second part, convert it to an integer and then because one “add to cart” actually probably adds a case pack, instead of checking if it’s 1, I just confirm that it’s greater than 0 since the number will be almost always larger than 1.

240 items in the cart after one click

Test 4: it should have 0 items in the cart after clicking the “Remove this item” icon

            const url = 'https://www.citadelpackaging.com/product-category/glass-containers/glass-bottles/';

            await page.goto(url);

            await page.waitForSelector('.number-item .item');

            const topFormCart = await page.$('.top-form-minicart');

            if (topFormCart) {
                try {
                    await topFormCart.hover();
                }
                catch (err) {
                    console.log('hover err', err);
                }
            }
            else {
                fail('Top form cart should be there');
            }
            await page.waitForSelector('.btn-remove .fa', { visible: true});
            const removeButton = await page.$('.btn-remove .fa');

            if (removeButton) {
                await removeButton.click();
            }
            else {
                fail('No remove button found. Failing.');
            }
            await page.waitForNavigation({ waitUntil: 'domcontentloaded' });

            let numberOfItemsInCart = await getPropertyBySelector(page, '.number-item .item', 'innerHTML');

            // Remove 'items' and parseInt
            if (numberOfItemsInCart) {
                numberOfItemsInCart = parseInt(numberOfItemsInCart.split(' ')[0]);
            }

            expect(numberOfItemsInCart).to.equal(0);

This test caught me on something that I didn’t know or at best had forgotten. The remove button for this test only shows up when you hover over the shopping cart and items that are visibility: hidden do not fire javascript click events. It was frustrating for me because I could see the element was there but clicking it would do nothing. It was more of angry whim that made me try hovering and then clicking that finally is what worked.

After clicking the remove button, I just check like I did in the previous tests that the number of items is now 0.

Hover required to click the remove

Test 5: it should have results when a search is done

        it('should have results when a search is done', async () => {
            const url = 'https://www.citadelpackaging.com/';
            await page.goto(url);

            await page.waitForSelector('#s');
            const searchInput = await page.$('#s');

            if (searchInput) {
                await searchInput.type('Boston round');
            }
            else {
                fail('Should have searchInput');
            }

            await page.waitForSelector('#searchform_special button');
            const searchButton = await page.$('#searchform_special button');

            if (searchButton) {
                await searchButton.click();
            }
            else {
                fail('Should have search button');
            }

            await page.waitForNavigation({ waitUntil: 'domcontentloaded' });

            const products = await page.$$('.item');

            expect(products.length).to.be.greaterThan(0);
        });

The search is an important part of any ecommerce site so I wanted to test that. I find the search with #s and then assuming I find it, I type my search query ‘Boston Round’. After this I check for the searchButton and click it. My wait for in this instance is a await page.waitForNavigation({ waitUntil: 'domcontentloaded' });. For more information on the DOMContentLoaded event see here.

Once it confirms that the results are loaded in, we just grab the products with $$('.item');. From a ‘Boston Round’ search we should always have results and so we just expect there to be more than 0 products.

Search results

Test 6: it should have a hamburger menu

This is my final describe and test. This describe is to test the responsive design of the website.

        it('should have a hamburger menu', async () => {
            await page.setViewport({ width: 400, height: 400 });

            const url = 'https://www.citadelpackaging.com/';
            await page.goto(url);

            await page.waitForSelector('.wrapper_vertical_menu.vertical_megamenu', { visible: true});

            const hamburgerMenu = await page.$('.wrapper_vertical_menu.vertical_megamenu');

            expect(hamburgerMenu).to.be.ok;                

        });

I setViewPort to a smaller window that will trigger the responsive design to kick in. Once it’s shrunk, I check to make sure that the hamburger menu is visible in the dom with await page.waitForSelector('.wrapper_vertical_menu.vertical_megamenu', { visible: true});. I then just check to make sure it’s truthy, which Chai asserts with expect(hamburgerMenu).to.be.ok;.

Hamburger menu is visible

And that’s it. Ta da.

1 thought on “Jordan Does Integration Tests on Citadel Packaging

Leave a Reply

Your email address will not be published. Required fields are marked *