Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions tests/e2e/specs/gtag-events/blocks-pages.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from '../../utils/api';
import {
blockProductAddToCart,
checkout,
relatedProductAddToCart,
simpleProductAddToCart,
variableProductAddToCart,
Expand Down Expand Up @@ -823,6 +824,60 @@ test.describe( 'GTag events on block pages', () => {
} );
} );

// The purchase event itself is built server-side from the order, so its data
// does not depend on which checkout a shopper used. What this test guards is
// the block-specific surface around it: placing an order through the
// `woocommerce/checkout` block and the block order-confirmation page still
// firing `woocommerce_thankyou`, which is where the purchase event is
// enqueued. Those pieces can break independently of the classic checkout when
// WooCommerce updates, so the block path is verified on its own here.
test( 'Purchase event is sent after completing the block checkout', async ( {
page,
} ) => {
// Add the simple product twice and one variable product so quantity and
// variation handling are both exercised in the order.
await simpleProductAddToCart( page, simpleProductID );
await simpleProductAddToCart( page, simpleProductID );
await variableProductAddToCart( page, variableProductID );

const event = trackGtagEvent( page, 'purchase', 'checkout' );
const orderID = await checkout( page );

await event.then( ( request ) => {
const data = getEventData( request, 'purchase' );
expect( data.product1 ).toEqual( {
id: simpleProductID.toString(),
nm: 'Simple product',
ca: 'Uncategorized',
qt: '2',
pr: simpleProductPrice.toString(),
} );
expect( data.product2 ).toEqual( {
id: variableProductID.toString(),
nm: 'Variable product',
ca: 'Uncategorized',
qt: '1',
pr: '18.99',
va: 'colour: Green, size: Medium',
} );

expect( data[ 'ep.transaction_id' ] ).toEqual( orderID );
expect( data[ 'ep.affiliation' ] ).toEqual(
'WooCommerce E2E Test Suite'
);

const shipping = 10;
const total =
simpleProductPrice + simpleProductPrice + 18.99 + shipping;
expect( data.cu ).toEqual( 'USD' );
expect( data[ 'epn.value' ] ).toEqual(
total.toFixed( 2 ).toString()
);
expect( data[ 'epn.tax' ] ).toEqual( '0' );
expect( data[ 'epn.shipping' ] ).toEqual( shipping.toString() );
} );
} );

test( 'Add to cart event is sent from a product collection block shop page', async ( {
page,
} ) => {
Expand Down
11 changes: 9 additions & 2 deletions tests/e2e/specs/gtag-events/classic-pages.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
import {
blockProductAddToCart,
checkout,
classicCheckout,
simpleProductAddToCart,
variableProductAddToCart,
} from '../../utils/customer';
Expand Down Expand Up @@ -813,16 +814,22 @@ test.describe( 'GTag events on classic pages', () => {
}
} );

test( 'Purchase event is sent on order complete page', async ( {
// The default checkout page is the block checkout, so the block purchase
// test in the blocks spec covers that form. This test drives the purchase
// through the classic `[woocommerce_checkout]` shortcode form instead
// (a wc-ajax=checkout submission rather than the Store API).
test( 'Purchase event is sent after completing the classic shortcode checkout', async ( {
page,
} ) => {
await createClassicCheckoutPage();

// Add simple product twice, and one variable product.
await simpleProductAddToCart( page, simpleProductID );
await simpleProductAddToCart( page, simpleProductID );
await variableProductAddToCart( page, variableProductID );

const event = trackGtagEvent( page, 'purchase', 'checkout' );
const orderID = await checkout( page );
const orderID = await classicCheckout( page );

await event.then( ( request ) => {
const data = getEventData( request, 'purchase' );
Expand Down
149 changes: 149 additions & 0 deletions tests/e2e/specs/gtag-events/disabled-events.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/**
* External dependencies
*/
const { test, expect } = require( '@playwright/test' );

/**
* Internal dependencies
*/
import {
createSimpleProduct,
setSettings,
clearSettings,
} from '../../utils/api';
import {
blockProductAddToCart,
storeApiAddToCart,
storeApiBatchDecreaseCartQuantity,
} from '../../utils/customer';
import { trackGtagEvent } from '../../utils/track-event';

/**
* Collects the names of every gtag event sent to google-analytics.com/g/collect
* after this is attached. Used to assert that a disabled event never fires:
* unlike trackGtagEvent(), which waits for a request, asserting an absence needs
* to observe the traffic and then check what was (not) seen.
*
* @param {import('@playwright/test').Page} page
*
* @return {string[]} A live array of event names, appended to as requests arrive.
*/
function collectGtagEventNames( page ) {
const names = [];

page.on( 'request', ( request ) => {
const url = request.url();
if ( ! url.includes( 'google-analytics.com/g/collect' ) ) {
return;
}

// A single event is sent in the query string, multiple events in the body.
const queryEvent = new URL( url ).searchParams.get( 'en' );
if ( queryEvent ) {
names.push( queryEvent );
}

for ( const line of ( request.postData() || '' ).split( /\r?\n/ ) ) {
const bodyEvent = new URLSearchParams( line ).get( 'en' );
if ( bodyEvent ) {
names.push( bodyEvent );
}
}
} );

return names;
}

test.describe( 'Disabled event tracking', () => {
let productID;

test.beforeAll( async () => {
productID = await createSimpleProduct();
} );

test.afterAll( async () => {
await clearSettings();
} );

test( 'No ecommerce events fire when all event tracking is disabled', async ( {
page,
} ) => {
await setSettings( {
ga_ecommerce_tracking_enabled: 'no',
ga_event_tracking_enabled: 'no',
ga_enhanced_remove_from_cart_enabled: 'no',
ga_enhanced_product_impression_enabled: 'no',
ga_enhanced_product_click_enabled: 'no',
ga_enhanced_product_detail_view_enabled: 'no',
ga_enhanced_checkout_process_enabled: 'no',
} );

const eventNames = collectGtagEventNames( page );

await page.goto( 'shop?orderby=date' );
await blockProductAddToCart( page, productID );

// The tracking scripts still load with everything disabled; only the list
// of events the tracker will emit is empty. Asserting that proves the
// disabled state propagated to the client rather than the scripts simply
// failing to load (which would also produce no events).
const enabledEvents = await page.evaluate(
() => window.ga4w?.settings?.events
);
expect( enabledEvents ).toEqual( [] );

// page_view is gtag's own config hit, not gated by the plugin toggles, so
// it always fires. Awaiting one on a fresh navigation is a deterministic
// barrier: any leaked ecommerce event from the interactions above would
// have been collected before it.
const pageView = trackGtagEvent( page, 'page_view' );
await page.goto( 'shop' );
await pageView;

// Assert against every event the plugin can send rather than a
// whitelist of the collected names, because gtag emits automatic
// events of its own (page_view, session_start, user_engagement).
const pluginEvents = [
'purchase',
'add_to_cart',
'remove_from_cart',
'view_item_list',
'select_content',
'view_item',
'begin_checkout',
'add_shipping_info',
'add_payment_info',
];
expect(
eventNames.filter( ( name ) => pluginEvents.includes( name ) )
).toEqual( [] );
} );

test( 'Disabling add to cart leaves the other events working', async ( {
page,
} ) => {
await setSettings( { ga_event_tracking_enabled: 'no' } );

const eventNames = collectGtagEventNames( page );

await page.goto( 'shop?orderby=date' );

// Add two units (add_to_cart is disabled, so it must stay silent), then
// remove one. The remove_from_cart event is still enabled and fires after
// the add would have, so awaiting it is a deterministic barrier: once it
// arrives, a non-suppressed add_to_cart would already have been collected.
await storeApiAddToCart( page, productID, 2 );
const removeEvent = trackGtagEvent( page, 'remove_from_cart' );
await storeApiBatchDecreaseCartQuantity( page, productID, 1 );
await removeEvent;

const enabledEvents = await page.evaluate(
() => window.ga4w?.settings?.events
);
expect( enabledEvents ).toContain( 'remove_from_cart' );
expect( enabledEvents ).not.toContain( 'add_to_cart' );

expect( eventNames ).toContain( 'remove_from_cart' );
expect( eventNames ).not.toContain( 'add_to_cart' );
} );
} );
95 changes: 95 additions & 0 deletions tests/e2e/specs/gtag-events/product-identifier.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/**
* External dependencies
*/
const { test, expect } = require( '@playwright/test' );

/**
* Internal dependencies
*/
import {
createSimpleProduct,
setSettings,
clearSettings,
} from '../../utils/api';
import { storeApiAddToCart } from '../../utils/customer';
import { getEventData, trackGtagEvent } from '../../utils/track-event';

const config = require( '../../config/default' );
const simpleProductPrice = parseFloat( config.products.simple.regular_price );

// The product identifier is a merchant setting: events can report a product by
// its WooCommerce id (the default, already exercised across the other specs) or
// by its SKU. The SKU value is resolved server-side into the event data and
// falls back to a "#id" form when a product has no SKU, so it can break when
// store settings or WooCommerce versions change. These tests pin that end-to-end
// behavior with the setting switched to SKU.
test.describe( 'Product identifier setting (SKU)', () => {
let skuProductID, noSkuProductID;
const sku = `E2E-IDENT-${ Date.now() }`;

test.beforeAll( async () => {
await setSettings( { ga_product_identifier: 'product_sku' } );
skuProductID = await createSimpleProduct( { sku } );
// The default product config has no SKU, so this exercises the fallback.
noSkuProductID = await createSimpleProduct();
} );

test.afterAll( async () => {
await clearSettings();
} );

test( 'View item reports the product SKU', async ( { page } ) => {
const event = trackGtagEvent( page, 'view_item' );

await page.goto( `?p=${ skuProductID }` );

await event.then( ( request ) => {
const data = getEventData( request, 'view_item' );
expect( data.product1 ).toMatchObject( {
id: sku,
nm: 'Simple product',
pr: simpleProductPrice.toString(),
} );
} );
} );

test( 'View item falls back to the prefixed id when the product has no SKU', async ( {
page,
} ) => {
const event = trackGtagEvent( page, 'view_item' );

await page.goto( `?p=${ noSkuProductID }` );

await event.then( ( request ) => {
const data = getEventData( request, 'view_item' );
expect( data.product1 ).toMatchObject( {
id: `#${ noSkuProductID }`,
nm: 'Simple product',
pr: simpleProductPrice.toString(),
} );
} );
} );

// The block add-to-cart flow carries the identifier in the Store API cart
// item's extensions, which is a separate resolution path from the
// server-rendered single product page above. This guards the SKU on the
// block storefront specifically.
test( 'Store API add to cart reports the product SKU', async ( {
page,
} ) => {
await page.goto( 'shop?orderby=date' );

const event = trackGtagEvent( page, 'add_to_cart' );
await storeApiAddToCart( page, skuProductID );

await event.then( ( request ) => {
const data = getEventData( request, 'add_to_cart' );
expect( data.product1 ).toMatchObject( {
id: sku,
nm: 'Simple product',
qt: '1',
pr: simpleProductPrice.toString(),
} );
} );
} );
} );
Loading