Skip to content
All projects

Case study

Essential Addons: measuring coverage against the plugin's own source

78 spec files covering 77 of the 101 widgets Essential Addons ships, on WordPress 7.0 and PHP 8.2 in Docker. Coverage is counted against the plugin's Elements directory rather than estimated, and one widget family needed a must-use plugin before it could be tested at all.

widgets covered
77 / 101widgets covered
active installs
1M+active installs
page builder scripts
80+page builder scripts
  • Playwright
  • TypeScript
  • Docker
  • WordPress 7.0
  • PHP 8.2
  • WooCommerce
  • GitHub Actions

The problem

Essential Addons is the most installed Elementor addon pack on wordpress.org, north of a million active sites. It ships 101 widgets across the free and Pro plugins, and every one of them is a separate rendering surface with its own controls, its own front-end JS and its own ways to break.

The number that matters for a suite like this is not how many tests exist. It is what fraction of the product they touch, and whether anyone can answer that question without guessing.

The constraints

  • 101 widgets, split across two plugins. 60 free, 41 Pro, and Pro is a private repo.
  • Every widget needs a page before it can be tested. An Elementor widget does not exist until it has been placed on a page with real settings.
  • Some widgets need a shop. The WooCommerce family needs products, a cart, a checkout and a single-product page that behaves like a real store.
  • The widget list moves. New widgets ship. A coverage number written by hand is wrong within a month.

The decisions, and what I rejected

Count coverage against the source, not against a spreadsheet

COVERAGE.md is generated against the plugin’s own includes/Elements/ directories in both the free and Pro checkouts. That gives a real denominator: 101 widgets, 60 free and 41 Pro. Against it, 77 are covered: 42 free and 35 Pro. 70% of free, 85% of Pro, 76% overall, with the 24 uncovered widgets named individually rather than left implied.

The alternative was a hand-maintained list, which is the same thing except wrong. If the denominator comes from the plugin source, adding a widget upstream moves the coverage number down and the gap shows up on its own.

One provisioning script per widget

There are 74 setup-*-page.php scripts under scripts/, one per widget page: setup-advanced-accordion-page.php, setup-flip-box-page.php, setup-woo-checkout-page.php, and so on. Each builds the Elementor page for its widget with real settings baked in.

That is a lot of files, and the alternative, a generic page builder driven by a config file, is tempting until you look at the widgets. A countdown needs a target date, a pricing table needs tiers, an event calendar needs events, a data table needs rows. The shared abstraction would have been mostly exceptions. One explicit script per widget is more code and less cleverness, and a failing widget leads straight to the file that set it up.

Write a must-use plugin to make WooCommerce widgets testable

Essential Addons has a family of single-product widgets: Add To Cart, Product Images, Product Price, Product Rating. They resolve their product through Helper::get_product(), which calls wc_get_product( get_the_ID() ), so they only work while the global $post is the product, which is exactly the WooCommerce single-product page.

The catch is that WooCommerce’s single-product template never calls the_content(). Elementor renders through a the_content filter, so the _elementor_data saved on the product post is never rendered at all. The widgets could not be placed anywhere they would run.

The fix is mu-plugins/ea-test-product-context.php: hook woocommerce_after_single_product_summary at priority 999, fetch the Elementor document for the product, and invoke the frontend renderer by hand.

It is worth being precise about what that is. It is test infrastructure that changes how the page under test renders, which is a thing to be suspicious of. It is justified here because it does not alter the widgets or their output. It only puts them in the context WooCommerce would have given them if its template rendered Elementor content. Without it, an entire widget family stays untested.

What it cost, and what it caught

Cost. 125 commits between April and June 2026. Four workers in CI, half the local core count outside it, fullyParallel on, a 15-second action timeout and 30 seconds for navigation. Two retries in CI and none locally, a deliberate split, since a retry in CI buys a green pipeline through infrastructure noise while a retry on a developer’s machine just hides a real failure from the person best placed to debug it.

The environment is pinned: wordpress:7.0.0-php8.2-apache, MySQL 8.0, a matching wordpress:cli-php8.2 container, with custom php.ini and mysql.cnf mounted in. The WordPress 7.0 move was a deliberate upgrade in June 2026, not drift.

Caught. The visible fixes in the history are widget rendering defects found by the suite and then corrected upstream: a Flip Carousel image bug and a Filterable Gallery Pro layout bug, both in late April 2026.

The less visible return is the coverage number itself. “76% of widgets, and here are the 24 that are not covered” is a different conversation from “we have automated tests”, because it can be argued with.

What I deliberately did not automate

  • The 24 uncovered widgets, named in COVERAGE.md rather than quietly omitted. The free plugin sits at 70% and Pro at 85%; the gap is a queue, not a claim.
  • Elementor’s editor itself. Pages are provisioned by script. The suite tests what Essential Addons renders, not whether Elementor’s canvas works.
  • Cross-browser. Chromium only.
  • Visual regression. No screenshot diffing. For 101 widgets that would generate more false alarms than findings, and the widgets that matter have structural assertions instead.