Skip to main content

Actions

Actions are named extension points where test packages register reusable capabilities for other packages to discover and call at runtime. If you've used WordPress hooks, you already know the pattern.

The WordPress Analogy

In WordPress, do_action('woocommerce_payment_complete') fires every callback hooked into that name. Actions in QIT work the same way:

  • A provider registers an action: "I can do makePurchase"
  • A consumer discovers all actions: "Give me every makePurchase"
  • Adding a new provider requires zero code changes in the consumer

How It Works

1. Provider Registers an Action

The Stripe team creates a utility package that knows how to complete a purchase through Stripe:

// stripe/payments/flows/pay.ts
export default async function(page, opts) {
await page.fill('#stripe-card-number', '4242424242424242');
await page.fill('#stripe-card-expiry', '12/30');
await page.fill('#stripe-card-cvc', '123');
await page.click('#place_order');
await page.waitForURL('**/order-received/**');
}

They declare it in their manifest:

{
"package": "stripe/payments",
"package_type": "utility",
"requires": { "plugins": ["woocommerce-gateway-stripe"] },
"actions": {
"makePurchase": "./flows/pay.ts"
},
"test": {
"phases": {
"globalSetup": ["./bootstrap/setup-stripe.sh"]
}
}
}

Each action maps a name to a file. The file's export default is the action implementation. No metadata in the manifest; the JSDoc and TypeScript types in the source file are the contract.

2. Consumer Discovers Actions at Runtime

Dale's product plugin tests iterate over every available makePurchase action:

import qit from '@woocommerce/qit-runtime';

const woo = qit.package('woocommerce/core-utils');

for (const makePurchase of qit.actions('makePurchase')) {
test.describe(`Checkout via ${makePurchase.provider}`, () => {
test('customer can purchase widget', async ({ page }) => {
await woo.loginAs(page, 'customer');
await page.goto('/product/fancy-widget');
await page.click('text=Add to cart');
await makePurchase(page, { amount: 29.99 });
await expect(page.locator('.order-received')).toBeVisible();
});
});
}

Each action function has a .provider property ('stripe/payments') for display in test names and CTRF results.

3. Adding a Gateway = Zero Code Changes

# Today: test with Stripe
qit run:e2e my-product-plugin \
--test-package woocommerce/core-utils \
--test-package stripe/payments \
--test-package dale/product-tests

# Tomorrow: add PayPal, no code changes in dale/product-tests
qit run:e2e my-product-plugin \
--test-package woocommerce/core-utils \
--test-package stripe/payments \
--test-package paypal/payments \
--test-package dale/product-tests

The second run produces twice the test cases. Dale's code didn't change.

Manifest Schema

The actions field maps action names to relative file paths:

"actions": {
"makePurchase": "./flows/pay.ts",
"refundOrder": "./flows/refund.ts"
}

Rules:

  • Action names must be camelCase identifiers: ^[a-zA-Z][a-zA-Z0-9_]*$
  • Paths must be relative (start with ./)
  • Each file must have an export default (that's the action implementation)
  • Both .js and .ts files work (Playwright registers its TypeScript transpiler)

When No Provider Exists

If no loaded package provides an action, qit.actions() returns an empty array. The for...of loop produces zero iterations (zero test cases, not a failure). This is by design: adding capabilities is additive, removing them is a no-op.

// If no payment gateway package is loaded:
qit.actions('makePurchase') // => []
// The for loop runs 0 times, 0 test cases generated
// CTRF shows the test file ran but produced no tests (visible, not an error)

Actions vs qit.package()

Both let packages share code. The difference is the relationship:

qit.actions()qit.package()
RelationshipAnonymous; consumer doesn't know providersDirect; consumer names the package
CardinalityMultiple implementations, iterateOne package, use directly
Manifestactions field requiredNo manifest field; barrel exports
Use case"Test against every payment gateway""Use WooCommerce's login helper"
WordPress analogydo_action() / apply_filters()Direct function call

Rule of thumb: If you'd write a for loop, it's qit.actions(). If you'd write a variable assignment, it's qit.package().

Ecosystem Patterns

The pattern extends across the WooCommerce ecosystem:

ActionProviderConsumer
makePurchasePayment gateways (Stripe, PayPal, Braintree)Product plugins testing checkout
calculateShippingShipping plugins (FedEx, UPS, DHL)Product plugins testing shipping rates
calculateTaxTax plugins (Avalara, TaxJar)Any plugin testing tax calculation
verifyEmailSentEmail plugins (Mailchimp, SendGrid)Plugins testing order notifications