Skip to content

Commit d1f44d0

Browse files
author
Soare Robert-Daniel
committed
dev: use E2E fixtures
1 parent 8575c20 commit d1f44d0

13 files changed

Lines changed: 624 additions & 253 deletions

bin/e2e-after-setup.sh

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
# Run after `npm run wp-env start`
2-
3-
# Add some woocommerce products.
4-
npm run wp-env run tests-cli bash ./wp-content/plugins/woocommerce-product-addon/bin/env/create-products.sh
1+
#!/usr/bin/env bash
52

3+
# Run after `npm run wp-env start`
4+
#
5+
# E2E setup is fixture-driven now, so no shared catalog bootstrap is required here.
6+
exit 0

tests/e2e/AGENTS.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# E2E Test Guidance
2+
3+
This folder contains Playwright tests for PPOM behavior in WordPress/WooCommerce.
4+
5+
## Default Approach
6+
7+
- Prefer stable server-side setup over admin UI setup.
8+
- Use the `fixtures/` helpers for storefront, cart, checkout, rendering, pricing, and conditional-logic tests.
9+
- Use `utils.js` only when the admin builder UI or attach modal UI is the feature under test.
10+
- Keep UI setup smoke coverage thin. Do not make every E2E test create fields by clicking through the dashboard.
11+
12+
## Use `fixtures/` When
13+
14+
- The test only needs PPOM data to exist.
15+
- You are validating product-page rendering, defaults, conditions, add-to-cart behavior, or checkout behavior.
16+
- You need products or categories created quickly and deterministically.
17+
18+
Current fixture modules are split by concern:
19+
20+
- `fixtures/woocommerce.js` for WooCommerce entities
21+
- `fixtures/ppom.js` for PPOM setup and attachments
22+
- `fixtures/fields.js` for PPOM field builders
23+
- `fixtures/internal.js` for shared request plumbing
24+
- `fixtures/index.js` as the public entrypoint
25+
26+
Current fixture helpers support:
27+
28+
- WooCommerce product creation through `wc/v3/products`
29+
- Product category creation through `wc/v3/products/categories`
30+
- PPOM group creation through `ppom_save_form_meta`
31+
- PPOM attachment through `ppom_attach_ppoms`
32+
33+
## Use `utils.js` When
34+
35+
- The admin builder itself is under test.
36+
- The attach modal behavior itself is under test.
37+
- The test needs to verify drag/drop, field ordering, modal controls, or admin-only interactions.
38+
39+
## Setup Rules
40+
41+
- Do not create products through wp-admin UI unless the UI is the thing being tested.
42+
- For product setup, prefer authenticated REST requests through `requestUtils`.
43+
- For PPOM group setup, prefer admin AJAX through the existing plugin callbacks instead of direct DB writes.
44+
- Do not persist raw fixture assumptions that bypass plugin save hooks unless the test explicitly targets a lower layer.
45+
- Admin UI specs can still use WooCommerce fixtures for prerequisite products and categories.
46+
47+
## Nonce Notes
48+
49+
- `ppom_form_nonce` is available on `wp-admin/admin.php?page=ppom&action=new`.
50+
- `ppom_attached_nonce` is not present on the main PPOM index page.
51+
- To attach groups, fetch the popup HTML from:
52+
`wp-admin/admin-ajax.php?action=ppom_get_products&ppom_id=<ppomId>`
53+
- Scrape the attach nonce from that response before calling `ppom_attach_ppoms`.
54+
55+
## Flake Reduction
56+
57+
- Avoid using `waitForTimeout()` for state setup unless there is no deterministic alternative.
58+
- Prefer request-level setup plus direct storefront assertions.
59+
- Keep random data unique enough to avoid collisions across reruns.
60+
- Avoid `console.log` in committed specs unless it is intentionally diagnostic.
61+
- Assert fixture write success before navigating to the storefront.
62+
63+
## Test Design
64+
65+
- Separate admin smoke tests from storefront behavior tests.
66+
- For conditions, pricing, and defaults, build the smallest field schema needed for the assertion.
67+
- Prefer one clear behavior per test.
68+
- If a flow depends on quantity, coupons, restore-from-session, or uploads, seed only the minimum data needed and assert the exact WooCommerce outcome.
69+
70+
## Verification
71+
72+
- If local E2E fails before tests start, check whether the WordPress env is reachable before debugging the spec.
73+
- The current suite expects the wp-env site to be available and global setup to authenticate successfully.
74+
- When changing fixtures, run the smallest spec set that exercises the changed helper first.
75+
- If `wp-admin/admin.php?page=ppom` returns `403` while `wp-admin/profile.php` still works, treat that as an environment bootstrap issue first.
76+
- In practice that usually means WooCommerce or PPOM is inactive in the local env, not that Playwright logged the user out.
77+
78+
## Practical Rule Of Thumb
79+
80+
- If the question is "does the product page behave correctly?", use fixtures.
81+
- If the question is "does the PPOM admin interface work correctly?", use UI helpers.

tests/e2e/config/global-setup.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,4 @@ async function globalSetup( config: FullConfig ) {
4141
await requestContext.dispose();
4242
}
4343

44-
export default globalSetup;
44+
export default globalSetup;

tests/e2e/fixtures/fields.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
function buildField( type, { title, dataName, ...overrides } ) {
2+
return {
3+
type,
4+
title,
5+
data_name: dataName,
6+
description: '',
7+
placeholder: '',
8+
error_message: '',
9+
width: '12',
10+
visibility: 'everyone',
11+
visibility_role: '',
12+
status: 'on',
13+
...overrides,
14+
};
15+
}
16+
17+
function buildOption( label, value, overrides = {} ) {
18+
return {
19+
option: label,
20+
id: value,
21+
price: '',
22+
...overrides,
23+
};
24+
}
25+
26+
function buildTextField( args ) {
27+
return buildField( 'text', args );
28+
}
29+
30+
function buildSelectField( { options = [], ...args } ) {
31+
return buildField( 'select', {
32+
...args,
33+
options: options.map( ( option ) =>
34+
buildOption( option.label, option.value, option.overrides )
35+
),
36+
} );
37+
}
38+
39+
function buildCheckboxField( { options = [], checked = [], ...args } ) {
40+
return buildField( 'checkbox', {
41+
...args,
42+
checked: checked.join( '\r\n' ),
43+
options: options.map( ( option ) =>
44+
buildOption( option.label, option.value, option.overrides )
45+
),
46+
} );
47+
}
48+
49+
export { buildCheckboxField, buildSelectField, buildTextField };

tests/e2e/fixtures/index.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export {
2+
attachPpomGroupToCategories,
3+
attachPpomGroupToProducts,
4+
createPpomGroup,
5+
createSimpleTextGroup,
6+
} from './ppom.js';
7+
export {
8+
buildCheckboxField,
9+
buildSelectField,
10+
buildTextField,
11+
} from './fields.js';
12+
export { createProductCategory, createSimpleProduct } from './woocommerce.js';

tests/e2e/fixtures/internal.js

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
function uniqueSuffix() {
2+
return `${ Date.now() }-${ Math.floor( Math.random() * 100000 ) }`;
3+
}
4+
5+
function appendNestedParams( params, prefix, value ) {
6+
if ( value === undefined || value === null ) {
7+
return;
8+
}
9+
10+
if ( Array.isArray( value ) ) {
11+
value.forEach( ( item, index ) => {
12+
appendNestedParams( params, `${ prefix }[${ index }]`, item );
13+
} );
14+
return;
15+
}
16+
17+
if ( typeof value === 'object' ) {
18+
Object.entries( value ).forEach( ( [ key, nestedValue ] ) => {
19+
appendNestedParams( params, `${ prefix }[${ key }]`, nestedValue );
20+
} );
21+
return;
22+
}
23+
24+
params.append( prefix, String( value ) );
25+
}
26+
27+
async function getAdminNonce( requestUtils, path, fieldName ) {
28+
const response = await requestUtils.request.get( path, {
29+
failOnStatusCode: false,
30+
} );
31+
const html = await response.text();
32+
33+
if ( ! response.ok() ) {
34+
const summary = html.replace( /\s+/g, ' ' ).trim().slice( 0, 160 );
35+
const environmentHint = html.includes(
36+
'Sorry, you are not allowed to access this page.'
37+
)
38+
? ' Ensure WooCommerce and PPOM are active in the E2E environment.'
39+
: '';
40+
41+
throw new Error(
42+
`Failed to load admin nonce source "${ path }": ${ response.status() } ${ response.statusText() }. ${ summary }${ environmentHint }`
43+
);
44+
}
45+
46+
const matcher = new RegExp(
47+
`name=["']${ fieldName }["'][^>]*value=["']([^"']+)["']`
48+
);
49+
const nonce = html.match( matcher )?.[ 1 ];
50+
51+
if ( ! nonce ) {
52+
const summary = html.replace( /\s+/g, ' ' ).trim().slice( 0, 160 );
53+
54+
throw new Error(
55+
`Failed to find admin nonce "${ fieldName }" on "${ path }". Response started with: ${ summary }`
56+
);
57+
}
58+
59+
return nonce;
60+
}
61+
62+
async function getAttachNonce( requestUtils, ppomId ) {
63+
return getAdminNonce(
64+
requestUtils,
65+
`wp-admin/admin-ajax.php?action=ppom_get_products&ppom_id=${ ppomId }`,
66+
'ppom_attached_nonce'
67+
);
68+
}
69+
70+
async function postAdminAjax( requestUtils, params ) {
71+
const response = await requestUtils.request.fetch(
72+
'wp-admin/admin-ajax.php',
73+
{
74+
method: 'POST',
75+
failOnStatusCode: true,
76+
headers: {
77+
'content-type':
78+
'application/x-www-form-urlencoded; charset=UTF-8',
79+
},
80+
data: params.toString(),
81+
}
82+
);
83+
const payload = await response.json();
84+
85+
if ( payload?.status !== 'success' ) {
86+
throw new Error(
87+
`Admin AJAX request failed: ${ JSON.stringify( payload ) }`
88+
);
89+
}
90+
91+
return payload;
92+
}
93+
94+
export {
95+
appendNestedParams,
96+
getAdminNonce,
97+
getAttachNonce,
98+
postAdminAjax,
99+
uniqueSuffix,
100+
};

tests/e2e/fixtures/ppom.js

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { buildTextField } from './fields.js';
2+
import {
3+
appendNestedParams,
4+
getAdminNonce,
5+
getAttachNonce,
6+
postAdminAjax,
7+
uniqueSuffix,
8+
} from './internal.js';
9+
10+
async function createPpomGroup( requestUtils, { groupName, fields } ) {
11+
const formNonce = await getAdminNonce(
12+
requestUtils,
13+
'wp-admin/admin.php?page=ppom&action=new',
14+
'ppom_form_nonce'
15+
);
16+
17+
const ppomFields = new URLSearchParams();
18+
fields.forEach( ( field, index ) => {
19+
appendNestedParams( ppomFields, `ppom[${ index + 1 }]`, field );
20+
} );
21+
22+
const params = new URLSearchParams();
23+
params.append( 'action', 'ppom_save_form_meta' );
24+
params.append( 'ppom_form_nonce', formNonce );
25+
params.append( 'productmeta_id', '' );
26+
params.append( 'product_id', '0' );
27+
params.append( 'productmeta_name', groupName );
28+
params.append( 'dynamic_price_hide', 'no' );
29+
params.append( 'send_file_attachment', '' );
30+
params.append( 'show_cart_thumb', 'no' );
31+
params.append( 'aviary_api_key', '' );
32+
params.append( 'productmeta_style', '' );
33+
params.append( 'productmeta_js', '' );
34+
params.append( 'ppom', ppomFields.toString() );
35+
36+
const payload = await postAdminAjax( requestUtils, params );
37+
const ppomId = Number( payload.productmeta_id );
38+
39+
if ( ! Number.isInteger( ppomId ) || ppomId <= 0 ) {
40+
throw new Error(
41+
`PPOM save did not return a valid productmeta_id: ${ JSON.stringify(
42+
payload
43+
) }`
44+
);
45+
}
46+
47+
return {
48+
...payload,
49+
ppomId,
50+
};
51+
}
52+
53+
async function createSimpleTextGroup(
54+
requestUtils,
55+
{
56+
groupName,
57+
fieldsNumber = 2,
58+
titlePrefix = 'Test',
59+
dataNamePrefix = 'test',
60+
} = {}
61+
) {
62+
const suffix = uniqueSuffix();
63+
const fields = Array.from( { length: fieldsNumber }, ( _, index ) =>
64+
buildTextField( {
65+
title: `${ titlePrefix } ${ index + 1 } ${ suffix }`,
66+
dataName: `${ dataNamePrefix }_${ index + 1 }_${ suffix }`,
67+
} )
68+
);
69+
70+
return createPpomGroup( requestUtils, {
71+
groupName: groupName ?? `PPOM Group ${ suffix }`,
72+
fields,
73+
} );
74+
}
75+
76+
async function attachPpomGroupToProducts( requestUtils, { ppomId, productIds } ) {
77+
const attachNonce = await getAttachNonce( requestUtils, ppomId );
78+
79+
const params = new URLSearchParams();
80+
params.append( 'action', 'ppom_attach_ppoms' );
81+
params.append( 'ppom_attached_nonce', attachNonce );
82+
params.append( 'ppom_id', String( ppomId ) );
83+
params.append( 'ppom-attach-to-products-initial', '' );
84+
85+
productIds.forEach( ( productId ) => {
86+
params.append( 'ppom-attach-to-products[]', String( productId ) );
87+
} );
88+
89+
return postAdminAjax( requestUtils, params );
90+
}
91+
92+
async function attachPpomGroupToCategories(
93+
requestUtils,
94+
{ ppomId, categorySlugs }
95+
) {
96+
const attachNonce = await getAttachNonce( requestUtils, ppomId );
97+
98+
const params = new URLSearchParams();
99+
params.append( 'action', 'ppom_attach_ppoms' );
100+
params.append( 'ppom_attached_nonce', attachNonce );
101+
params.append( 'ppom_id', String( ppomId ) );
102+
params.append( 'ppom-attach-to-products-initial', '' );
103+
104+
categorySlugs.forEach( ( slug ) => {
105+
params.append( 'ppom-attach-to-categories[]', slug );
106+
} );
107+
108+
return postAdminAjax( requestUtils, params );
109+
}
110+
111+
export {
112+
attachPpomGroupToCategories,
113+
attachPpomGroupToProducts,
114+
createPpomGroup,
115+
createSimpleTextGroup,
116+
};

0 commit comments

Comments
 (0)