Clever use of attribute selectors to simplify puppeteer front-end automated test code development

For information on how to use Puppeteer to build automated testing projects, you can refer to the Getting Started Guide: Using Puppeteer for Front-end Automated Testing. This article is intended to share a simplifying technique when writing test code.

Applicable scenarios

It is suitable for B-side system pages with many form operations and relatively uniform operation modes. For example, simple interactions such as adding, deleting, modifying, and checking can be performed.
antd form example
For example, the form in the antd component. If you imitate form operations in automated testing, it is generally customary to use id selectors or class selectors. For example:

 await page.waitForTimeout(1000);
    const Username = await page.waitForSelector("#Username");
    await Username.type("2023");
const Password = await page.waitForSelector("#Password");
    await Password.type("2023");
    
    const submit = await page.waitForSelector(
      ".ant-form .ant-form-item .ant-btn-primary"
    ); //Query button
    submit.click();

Each page must separately find the hierarchical position, id and class name of each element. If there are many repetitive parts in the functional operations of multiple pages, we have to write similar codes over and over again. This process is not only extremely boring, but also adds a lot of burden to the development work.

Improvement: Use attribute selectors to abstract the operation process

Since the operation process of many B-side pages is the same, only the ids of the elements are different. Then we can choose to abandon the unique id selector and replace it with a more general attribute selector. By adding characteristic attributes to the front-end business code structure to abstract the user’s behavior and operation sequence, a set of general test codes can be developed in the automated test code to adapt to multiple A different page.

Business form code:
A query retrieval form:

<Form layout="inline" form={<!-- -->form}>
            <FormItem
                name='title'
                label='title'
                test='queryForm-input'
            >
                <Input/>
            </FormItem>
            <FormItem
                name='type'
                label='type'
                test='queryForm-select'
            >
                <Select
                >
                    {<!-- -->options}
                </Select>

            </FormItem>
            <FormItem
                name='startDate'
                label='start date'
                test='queryForm-datePicker'
            >
                <DatePicker
                    format="YYYY-MM-DD"
                />
            </FormItem>
            <Button test='queryForm-queryButton' onClick={<!-- -->handleSubmit}>
                    Inquire
            </Button>
</Form>

A Modal pop-up form:

<Form form={<!-- -->form}>
            <FormItem
                name='title'
                label='title'
                test='modalForm-input'
            >
                <Input/>
            </FormItem>
            <FormItem
                name='type'
                label='type'
                test='modalForm-select'
            >
                <Select
                >
                    {<!-- -->options}
                </Select>

            </FormItem>
            <FormItem
                name='startDate'
                label='start date'
                test='modalForm-datePicker'
            >
                <DatePicker
                    format="YYYY-MM-DD"
                />
            </FormItem>
</Form>

According to the interaction process, the query function is tested first, so the automated test code can be written as:

//Query form test
//Get all input boxes
    let inputs = await page.$$("div[test='queryForm-input'] input");
    //Get all selection boxes
    let selects = await page.$$("div[test='queryForm-select'] input");
    //Get all date selection boxes
    let datePickers = await page.$$("div[test='queryForm-datePicker'] input");
    //Get query button
    let queryButton = await page.waitForSelector(
      "button[test='queryForm-queryButton']"
    );
    //Traverse the input input
    for (let i = 0; i < inputs.length; i + + ) {<!-- -->
      let input = inputs[i];
      await input.type("2023");
    }
    //Traverse the selection box and click on the options one by one
    for (let i = 0; i < selects.length; i + + ) {<!-- -->
      let select = selects[i];
      await select.click();
      await page.waitForTimeout(1000);
      let options = await page.$$(".rc-virtual-list-holder"); //Multiple drop-down box options exist, take the group
      let option = options[options.length - 1]; //Just take the last or first option
      await option.click();
    }
    //Traverse the date selection box and select dates one by one
    for (let i = 0; i < datePickers.length; i + + ) {<!-- -->
      let datePicker = datePickers[i];
      await datePicker.click();
      let todayButtons = await page.$$(".ant-picker-footer"); //Date selection box--get the group today
      let todayButton = todayButtons[todayButtons.length - 1];
      await todayButton.click();
    }
    await page.waitForTimeout(1000); //After selecting the component of the selected type, it is best to wait 1s before proceeding with subsequent operations.
    queryButton.click();

Some other page elements also set similar attributes according to their operating characteristics. For example, the action button in the last column of the table:

const columns = [
  {<!-- -->
    title: 'Ordinary column',
    dataIndex: 'No',
   },
   {<!-- -->
       title: 'Operation column',
       render:text=>{<!-- -->
       return [
<Button
           onClick={<!-- -->onClick}
            test="editButton"
          >
            edit
        </Button>
]
}
}
]

Simulation of clicking the edit button:

let editButtons = await page.$$("button[test='editButton']");
let editButton = editButtons[0];
await editButton?.click();

If the behavior of this button results in a pop-up window, you can test the pop-up window interaction according to the attribute selector of the pop-up window form above:

 let modals = await page.$$("body div .ant-modal-root .ant-modal-wrap");
 //If multiple pop-up windows have been opened on the page, the last pop-up window obtained is the currently open pop-up window.
 let lastModal = modals[modals.length - 1];
 if (lastModal) {<!-- -->
   let inputs = await page.$$("div[test='modalForm-input'] input");
   // console.log('ii', inputs.length)
   // return
   let selects = await page.$$("div[test='modalForm-select'] input");
   let datePickers = await page.$$("div[test='modalForm-datePicker'] input");
   for (let i = 0; i < inputs.length; i + + ) {<!-- -->
     let input = inputs[i];
     await input.type("2023");
   }
   for (let i = 0; i < selects.length; i + + ) {<!-- -->
     let select = selects[i];
     await select.click();
     await page.waitForTimeout(1000);
     let options = await page.$$(".rc-virtual-list-holder"); //Drop-down box options
     let option = options[options.length - 1]; //Take the last option
     await option.click();
   }
   for (let i = 0; i < datePickers.length; i + + ) {<!-- -->
     let datePicker = datePickers[i];
     await datePicker.click();
     let todayButtons = await page.$$(".ant-picker-footer"); //Date selection box--get the group today
     let todayButton = todayButtons[todayButtons.length - 1];
     await todayButton.click();
   }
   await page.waitForTimeout(1000);
   let sureBtn = await lastModal.$(".ant-modal-footer .ant-btn-primary");
   await sureBtn?.click();
   await page.waitForTimeout(1000);
 }

After writing the basic general test method, when we add a test to a new page, we only need to configure the routing of the page.
Each parameter in the following routing file is determined according to the actual situation of the project. After obtaining the following data in the main function, you can flexibly process it according to your own needs.

import {<!-- --> QueryTest } from "../testFunc/queryTest"; //Introduce common methods
const isTestValue = true;

export default {<!-- -->
  routes: [
    {<!-- -->
      level1Name: "Level Menu",
      name: "menu name",
      path: "/menupath",
      funcName: new QueryTest(),
      params: "Parameters passed to this general method",
      isTest: isTestValue
    },
    {<!-- -->
      level1Name: "Level Menu",
      name: "Menu name 2",
      path: "/menupath2",
      funcName: new QueryTest(),
      params: "Parameters passed to this general method",
      isTest: isTestValue
    },
  ]
}

Summary

  1. Benefits of using a common approach to automated testing:
    In addition to eliminating a lot of repetitive work, it is also more flexible. If a page needs to develop test code separately, you can also introduce a common method into its private method and then call it, which can meet more diverse testing needs.
  2. Disadvantages: It is intrusive to business code, and attributes without business meaning need to be added to the business code. At this point, we need to consider whether the projects and projects we develop allow this kind of operation.