diff --git a/assets/js/src/config.js b/assets/js/src/config.js deleted file mode 100644 index 0477cb8b..00000000 --- a/assets/js/src/config.js +++ /dev/null @@ -1,2 +0,0 @@ -/* global wcgai */ -export const config = wcgai.config; diff --git a/assets/js/src/index.js b/assets/js/src/index.js index 7f198bf2..1ef34fb8 100644 --- a/assets/js/src/index.js +++ b/assets/js/src/index.js @@ -1,6 +1,32 @@ -// Initialize tracking for classic WooCommerce pages -import { trackClassicPages } from './integrations/classic'; -window.wcgai.trackClassicPages = trackClassicPages; +import { setupEventHandlers } from './tracker'; +import { classicTracking } from './integrations/classic'; +import { blocksTracking } from './integrations/blocks'; -// Initialize tracking for Block based WooCommerce pages -import './integrations/blocks'; +// Wait for 'ga4w:ready' event if `window.ga4w` is not there yet. +if ( window.ga4w ) { + initializeTracking(); +} else { + document.addEventListener( 'ga4w:ready', initializeTracking ); + + // Warn if there is still nothing after the document is fully loded. + if ( document.readyState === 'complete' ) { + warnIfDataMissing(); + } else { + window.addEventListener( 'load', warnIfDataMissing ); + } +} +function initializeTracking() { + const getEventHandler = setupEventHandlers( window.ga4w.settings ); + + classicTracking( getEventHandler, window.ga4w.data ); + blocksTracking( getEventHandler ); +} + +function warnIfDataMissing() { + if ( ! window.ga4w ) { + // eslint-disable-next-line no-console -- It's not an error, as one may load the script later, but we'd like to warn developers if it's about to be missing. + console.warn( + 'Google Analytics for WooCommerce: Configuration and tracking data not found after the page was fully loaded. Make sure the `woocommerce-google-analytics-integration-data` script gets eventually loaded.' + ); + } +} diff --git a/assets/js/src/integrations/blocks.js b/assets/js/src/integrations/blocks.js index d9503f1e..30d6c64b 100644 --- a/assets/js/src/integrations/blocks.js +++ b/assets/js/src/integrations/blocks.js @@ -1,48 +1,50 @@ import { removeAction } from '@wordpress/hooks'; import { addUniqueAction } from '../utils'; -import { tracker } from '../tracker'; import { ACTION_PREFIX, NAMESPACE } from '../constants'; -addUniqueAction( - `${ ACTION_PREFIX }-product-render`, - NAMESPACE, - tracker.eventHandler( 'view_item' ) -); +// We add actions asynchronosly, to make sure handlers will have the config available. +export const blocksTracking = ( getEventHandler ) => { + addUniqueAction( + `${ ACTION_PREFIX }-product-render`, + NAMESPACE, + getEventHandler( 'view_item' ) + ); -addUniqueAction( - `${ ACTION_PREFIX }-cart-remove-item`, - NAMESPACE, - tracker.eventHandler( 'remove_from_cart' ) -); + addUniqueAction( + `${ ACTION_PREFIX }-cart-remove-item`, + NAMESPACE, + getEventHandler( 'remove_from_cart' ) + ); -addUniqueAction( - `${ ACTION_PREFIX }-checkout-render-checkout-form`, - NAMESPACE, - tracker.eventHandler( 'begin_checkout' ) -); + addUniqueAction( + `${ ACTION_PREFIX }-checkout-render-checkout-form`, + NAMESPACE, + getEventHandler( 'begin_checkout' ) + ); -// These actions only works for All Products Block -addUniqueAction( - `${ ACTION_PREFIX }-cart-add-item`, - NAMESPACE, - ( { product } ) => { - tracker.eventHandler( 'add_to_cart' )( { product } ); - } -); + // These actions only works for All Products Block + addUniqueAction( + `${ ACTION_PREFIX }-cart-add-item`, + NAMESPACE, + ( { product } ) => { + getEventHandler( 'add_to_cart' )( { product } ); + } + ); -addUniqueAction( - `${ ACTION_PREFIX }-product-list-render`, - NAMESPACE, - tracker.eventHandler( 'view_item_list' ) -); + addUniqueAction( + `${ ACTION_PREFIX }-product-list-render`, + NAMESPACE, + getEventHandler( 'view_item_list' ) + ); -addUniqueAction( - `${ ACTION_PREFIX }-product-view-link`, - NAMESPACE, - tracker.eventHandler( 'select_content' ) -); + addUniqueAction( + `${ ACTION_PREFIX }-product-view-link`, + NAMESPACE, + getEventHandler( 'select_content' ) + ); +}; -/** +/* * Remove additional actions added by WooCommerce Core which are either * not supported by Google Analytics for WooCommerce or are redundant * since Google retired Universal Analytics. diff --git a/assets/js/src/integrations/classic.js b/assets/js/src/integrations/classic.js index 020155be..566bab12 100644 --- a/assets/js/src/integrations/classic.js +++ b/assets/js/src/integrations/classic.js @@ -1,4 +1,3 @@ -import { tracker } from '../tracker'; import { getProductFromID } from '../utils'; /** @@ -13,6 +12,7 @@ import { getProductFromID } from '../utils'; * * It also handles some Block events that are not fired reliably for `woocommerce/all-products` block. * + * @param {Function} getEventHandler * @param {Object} data - The tracking data from the current page load, containing the following properties: * @param {Object} data.events - An object containing the events to be instantly tracked. * @param {Object} data.cart - The cart object. @@ -21,26 +21,21 @@ import { getProductFromID } from '../utils'; * @param {Object} data.added_to_cart - The product added to cart. * @param {Object} data.order - The order object. */ -export function trackClassicPages( { - events, - cart, - products, - product, - added_to_cart: addedToCart, - order, -} ) { +export function classicTracking( + getEventHandler, + { events, cart, products, product, added_to_cart: addedToCart, order } +) { // Instantly track the events listed in the `events` object. - const eventData = { - storeCart: cart, - products, - product, - order, - }; Object.values( events ?? {} ).forEach( ( eventName ) => { if ( eventName === 'add_to_cart' ) { - tracker.eventHandler( eventName )( { product: addedToCart } ); + getEventHandler( eventName )( { product: addedToCart } ); } else { - tracker.eventHandler( eventName )( eventData ); + getEventHandler( eventName )( { + storeCart: cart, + products, + product, + order, + } ); } } ); @@ -70,7 +65,7 @@ export function trackClassicPages( { return; } - tracker.eventHandler( 'add_to_cart' )( { product: productToHandle } ); + getEventHandler( 'add_to_cart' )( { product: productToHandle } ); }; /** @@ -92,7 +87,7 @@ export function trackClassicPages( { * @param {HTMLElement|Object} element - The HTML element clicked on to trigger this event */ function removeFromCartHandler( element ) { - tracker.eventHandler( 'remove_from_cart' )( { + getEventHandler( 'remove_from_cart' )( { product: getProductFromID( parseInt( element.target.dataset.product_id ), products, @@ -159,7 +154,7 @@ export function trackClassicPages( { return; } - tracker.eventHandler( 'select_content' )( { + getEventHandler( 'select_content' )( { product: getProductFromID( parseInt( productId ), products, @@ -207,7 +202,7 @@ export function trackClassicPages( { if ( isAddToCartButton ) { // Add to cart. - tracker.eventHandler( 'add_to_cart' )( { + getEventHandler( 'add_to_cart' )( { product: getProductFromID( parseInt( productId ), products, @@ -216,7 +211,7 @@ export function trackClassicPages( { } ); } else if ( viewLink || button || nameLink ) { // Product image or add-to-cart-like button. - tracker.eventHandler( 'select_content' )( { + getEventHandler( 'select_content' )( { product: getProductFromID( parseInt( productId ), products, diff --git a/assets/js/src/tracker/index.js b/assets/js/src/tracker/index.js index 671418c8..3156cd08 100644 --- a/assets/js/src/tracker/index.js +++ b/assets/js/src/tracker/index.js @@ -1,75 +1,37 @@ -import { config } from '../config'; import * as formatters from './data-formatting'; -let instance; - /** - * A tracking utility for initializing a GA4 and tracking accepted events. + * Get a new event handler constructing function, based on given settings. * - * @class + * @param {Object} settings - The settings object. + * @param {Array} settings.events - The list of supported events. + * @param {string} settings.tracker_function_name - The name of the global function to call for tracking. + * @return {function(string): Function} - A function to get event handlers for specific events. */ -class Tracker { +export function setupEventHandlers( { + events, + tracker_function_name: trackerFunctionName, +} ) { /** - * Constructs a new instance of the Tracker class. + * Returns an event handler for a specified event name. * - * @throws {Error} If an instance of the Tracker already exists. - */ - constructor() { - if ( instance ) { - throw new Error( 'Cannot instantiate more than one Tracker' ); - } - instance = this; - - window.dataLayer = window.dataLayer || []; - - function gtag() { - window.dataLayer.push( arguments ); - } - - window[ config.tracker_function_name ] = gtag; - - // Set up default consent state, denying all for EEA visitors. - for ( const mode of config.consent_modes || [] ) { - gtag( 'consent', 'default', mode ); - } - - gtag( 'js', new Date() ); - gtag( 'set', `developer_id.${ config.developer_id }`, true ); - gtag( 'config', config.gtag_id, { - allow_google_signals: config.allow_google_signals, - link_attribution: config.link_attribution, - anonymize_ip: config.anonymize_ip, - logged_in: config.logged_in, - linker: config.linker, - custom_map: config.custom_map, - } ); - } - - /** - * Creates and returns an event handler for a specified event name. - * - * @param {string} name The name of the event. + * @param {string} eventName The name of the event. * @return {function(*): void} Function for processing and tracking the event. * @throws {Error} If the event name is not supported. */ - eventHandler( name ) { + function getEventHandler( eventName ) { /* eslint import/namespace: [ 'error', { allowComputed: true } ] */ - const formatter = formatters[ name ]; + const formatter = formatters[ eventName ]; if ( typeof formatter !== 'function' ) { - throw new Error( `Event ${ name } is not supported.` ); + throw new Error( `Event ${ eventName } is not supported.` ); } - return function trackerEventHandler( data ) { + return function eventHandler( data ) { const eventData = formatter( data ); - if ( config.events.includes( name ) && eventData ) { - window[ config.tracker_function_name ]( - 'event', - name, - eventData - ); + if ( events.includes( eventName ) && eventData ) { + window[ trackerFunctionName ]( 'event', eventName, eventData ); } }; } + return getEventHandler; } - -export const tracker = Object.freeze( new Tracker() ); diff --git a/assets/js/src/utils/index.js b/assets/js/src/utils/index.js index 7a3c0f22..04e2aea0 100644 --- a/assets/js/src/utils/index.js +++ b/assets/js/src/utils/index.js @@ -1,5 +1,4 @@ import { addAction, removeAction } from '@wordpress/hooks'; -import { config } from '../config.js'; /** * Formats data into the productFieldObject shape. @@ -85,15 +84,15 @@ export const addUniqueAction = ( hookName, namespace, callback ) => { * @return {string} - The product ID */ export const getProductId = ( product ) => { - const identifier = + const productIdentifier = product.extensions?.woocommerce_google_analytics_integration ?.identifier; - if ( identifier !== undefined ) { - return identifier; + if ( productIdentifier !== undefined ) { + return productIdentifier; } - if ( config.identifier === 'product_sku' ) { + if ( window.ga4w?.settings?.identifier === 'product_sku' ) { return product.sku ? product.sku : '#' + product.id; } diff --git a/includes/class-wc-google-gtag-js.php b/includes/class-wc-google-gtag-js.php index 212e3819..3a95a49e 100644 --- a/includes/class-wc-google-gtag-js.php +++ b/includes/class-wc-google-gtag-js.php @@ -16,7 +16,10 @@ class WC_Google_Gtag_JS extends WC_Abstract_Google_Analytics_JS { /** @var string $script_handle Handle for the front end JavaScript file */ public $script_handle = 'woocommerce-google-analytics-integration'; - /** @var string $script_handle Handle for the event data inline script */ + /** @var string $gtag_script_handle Handle for the gtag setup script */ + public $gtag_script_handle = 'woocommerce-google-analytics-integration-gtag'; + + /** @var string $data_script_handle Handle for the event data inline script */ public $data_script_handle = 'woocommerce-google-analytics-integration-data'; /** @var string $script_data Data required for frontend event tracking */ @@ -70,6 +73,43 @@ private function register_scripts(): void { 'strategy' => 'async', ) ); + + wp_register_script( + $this->gtag_script_handle, + '', + array(), + null, + array( + 'in_footer' => false, + ) + ); + + wp_add_inline_script( + $this->gtag_script_handle, + apply_filters( + 'woocommerce_gtag_snippet', + sprintf( + '/* Google Analytics for WooCommerce (gtag.js) */ + window.dataLayer = window.dataLayer || []; + function %2$s(){dataLayer.push(arguments);} + // Set up default consent state. + for ( const mode of %4$s || [] ) { + %2$s( "consent", "default", mode ); + } + %2$s("js", new Date()); + %2$s("set", "developer_id.%3$s", true); + %2$s("config", "%1$s", %5$s);', + esc_js( $this->get( 'ga_id' ) ), + esc_js( $this->tracker_function_name() ), + esc_js( static::DEVELOPER_ID ), + json_encode( $this->get_consent_modes() ), + json_encode( $this->get_site_tag_config() ) + ) + ) + ); + + wp_enqueue_script( $this->gtag_script_handle ); + wp_register_script( $this->script_handle, Plugin::get_instance()->get_js_asset_url( 'main.js' ), @@ -93,20 +133,10 @@ public function enquque_tracker(): void { // tracker.js needs to be executed ASAP, the remaining bits for main.js could be deffered, // but to reduce the traffic, we ship it all together. wp_enqueue_script( $this->script_handle ); - // Provide tracker's configuration. - wp_add_inline_script( - $this->script_handle, - sprintf( - 'var wcgai = {config: %s};', - wp_json_encode( $this->get_analytics_config() ) - ), - 'before' - ); } /** - * Feed classic tracking with event data via inline script. - * Make sure it's added at the bottom of the page, so all the data is collected. + * Add all event data via an inline script in the footer to ensure all the data is collected in time. * * @return void */ @@ -124,8 +154,15 @@ public function inline_script_data(): void { wp_add_inline_script( $this->data_script_handle, sprintf( - 'wcgai.trackClassicPages( %s );', - $this->get_script_data() + 'window.ga4w = { data: %1$s, settings: %2$s }; document.dispatchEvent(new Event("ga4w:ready"));', + $this->get_script_data(), + wp_json_encode( + array( + 'tracker_function_name' => $this->tracker_function_name(), + 'events' => $this->get_enabled_events(), + 'identifier' => $this->get( 'ga_product_identifier' ), + ), + ), ) ); @@ -226,29 +263,22 @@ public static function tracker_function_name(): string { * * @return array */ - public function get_analytics_config(): array { - $defaults = array( - 'gtag_id' => self::get( 'ga_id' ), - 'tracker_function_name' => self::tracker_function_name(), - 'track_404' => 'yes' === self::get( 'ga_404_tracking_enabled' ), - 'allow_google_signals' => 'yes' === self::get( 'ga_support_display_advertising' ), - 'logged_in' => is_user_logged_in(), - 'linker' => array( - 'domains' => ! empty( self::get( 'ga_linker_cross_domains' ) ) ? array_map( 'esc_js', explode( ',', self::get( 'ga_linker_cross_domains' ) ) ) : array(), - 'allow_incoming' => 'yes' === self::get( 'ga_linker_allow_incoming_enabled' ), - ), - 'custom_map' => array( - 'dimension1' => 'logged_in', + public function get_site_tag_config(): array { + return apply_filters( + 'woocommerce_ga_gtag_config', + array( + 'track_404' => 'yes' === $this->get( 'ga_404_tracking_enabled' ), + 'allow_google_signals' => 'yes' === $this->get( 'ga_support_display_advertising' ), + 'logged_in' => is_user_logged_in(), + 'linker' => array( + 'domains' => ! empty( $this->get( 'ga_linker_cross_domains' ) ) ? array_map( 'esc_js', explode( ',', $this->get( 'ga_linker_cross_domains' ) ) ) : array(), + 'allow_incoming' => 'yes' === $this->get( 'ga_linker_allow_incoming_enabled' ), + ), + 'custom_map' => array( + 'dimension1' => 'logged_in', + ), ), - 'events' => self::get_enabled_events(), - 'identifier' => self::get( 'ga_product_identifier' ), - 'consent_modes' => self::get_consent_modes(), ); - - $config = apply_filters( 'woocommerce_ga_gtag_config', $defaults ); - $config['developer_id'] = self::DEVELOPER_ID; - - return $config; } /** diff --git a/tests/e2e/specs/js-scripts/admin-user.test.js b/tests/e2e/specs/js-scripts/admin-user.test.js index 659e3134..8aa3d998 100644 --- a/tests/e2e/specs/js-scripts/admin-user.test.js +++ b/tests/e2e/specs/js-scripts/admin-user.test.js @@ -22,12 +22,6 @@ test.describe( 'JavaScript loaded', () => { test( 'No tracking for logged in admin user', async ( { page } ) => { await page.goto( 'shop' ); - await expect( - page.locator( - '#woocommerce-google-analytics-integration-js-before' - ) - ).not.toBeAttached(); - await expect( page.locator( '#woocommerce-google-analytics-integration-js' ) ).not.toBeAttached(); @@ -37,5 +31,8 @@ test.describe( 'JavaScript loaded', () => { '#woocommerce-google-analytics-integration-data-js-after' ) ).not.toBeAttached(); + + const dataLayer = await page.evaluate( () => window.dataLayer ); + expect( dataLayer ).toBeUndefined(); } ); } ); diff --git a/tests/e2e/specs/js-scripts/customer-logged-in.test.js b/tests/e2e/specs/js-scripts/customer-logged-in.test.js index 63ed0e24..486f4513 100644 --- a/tests/e2e/specs/js-scripts/customer-logged-in.test.js +++ b/tests/e2e/specs/js-scripts/customer-logged-in.test.js @@ -23,12 +23,6 @@ test.describe( 'JavaScript loaded', () => { test( 'Tracking loaded for logged in customer', async ( { page } ) => { await page.goto( 'shop' ); - await expect( - page.locator( - '#woocommerce-google-analytics-integration-js-before' - ) - ).toBeAttached(); - await expect( page.locator( '#woocommerce-google-analytics-integration-js' ) ).toBeAttached(); diff --git a/tests/e2e/specs/js-scripts/guest.test.js b/tests/e2e/specs/js-scripts/guest.test.js index 0eebdd1c..9da0c4c3 100644 --- a/tests/e2e/specs/js-scripts/guest.test.js +++ b/tests/e2e/specs/js-scripts/guest.test.js @@ -20,12 +20,6 @@ test.describe( 'JavaScript loaded', () => { test( 'Tracking loaded for guest customer', async ( { page } ) => { await page.goto( 'shop' ); - await expect( - page.locator( - '#woocommerce-google-analytics-integration-js-before' - ) - ).toBeAttached(); - await expect( page.locator( '#woocommerce-google-analytics-integration-js' ) ).toBeAttached(); diff --git a/tests/e2e/specs/js-scripts/load-order.test.js b/tests/e2e/specs/js-scripts/load-order.test.js new file mode 100644 index 00000000..5f75d7f9 --- /dev/null +++ b/tests/e2e/specs/js-scripts/load-order.test.js @@ -0,0 +1,62 @@ +/** + * External dependencies + */ +const { test, expect } = require( '@playwright/test' ); + +/** + * Internal dependencies + */ +import { setSettings, clearSettings } from '../../utils/api'; +import { getEventData, trackGtagEvent } from '../../utils/track-event'; + +test.describe( 'JavaScript file position', () => { + test.beforeAll( async () => { + await setSettings(); + } ); + + test.afterAll( async () => { + await clearSettings(); + } ); + + test( 'Tracking is functional if main.js is loaded in the header', async ( { + page, + } ) => { + const event = trackGtagEvent( page, 'view_item_list' ); + + await page.goto( 'shop?move_mainjs_to=head' ); + + await expect( + page.locator( + 'head #woocommerce-google-analytics-integration-head-js' + ) + ).toBeAttached(); + + await expect( + page.locator( '#woocommerce-google-analytics-integration-js' ) + ).toHaveCount( 0 ); + + await event.then( ( request ) => { + const data = getEventData( request, 'view_item_list' ); + expect( data[ 'ep.item_list_name' ] ).toEqual( 'Viewing products' ); + } ); + } ); + + test( 'Tracking is functional if main.js is loaded after the inline script data', async ( { + page, + } ) => { + const event = trackGtagEvent( page, 'view_item_list' ); + + await page.goto( 'shop?move_mainjs_to=after_inline_data' ); + + await expect( + page.locator( + '#woocommerce-google-analytics-integration-data-js-after + #woocommerce-google-analytics-integration-js' + ) + ).toBeAttached(); + + await event.then( ( request ) => { + const data = getEventData( request, 'view_item_list' ); + expect( data[ 'ep.item_list_name' ] ).toEqual( 'Viewing products' ); + } ); + } ); +} ); diff --git a/tests/e2e/test-snippets/test-snippets.php b/tests/e2e/test-snippets/test-snippets.php index 61497199..a0ff0af8 100644 --- a/tests/e2e/test-snippets/test-snippets.php +++ b/tests/e2e/test-snippets/test-snippets.php @@ -9,6 +9,9 @@ namespace Automattic\WooCommerce\GoogleListingsAndAds\Snippets; +use WC_Google_Analytics_Integration; +use WC_Google_Gtag_JS; + /* * Customize/disable the gtag consent mode, to make testing easier by granting everything by default. * It's a hack to avoid specifying region for E2E environment, but it tests the customization of consent mode. @@ -24,6 +27,63 @@ function ( $modes ) { } ); +/** + * Snippet to allow the main.js file to be moved either to the page head or to + * late in the footer after the extension inline data has been added to the page. + * + * This allows basic E2E tests to confirm tracking works regardless of when the + * script is loaded. This is important because some third-party plugins will + * change the load order in unexpected ways which has previously caused problems. + */ +add_action( + 'wp_enqueue_scripts', + function () { + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + if ( isset( $_GET['move_mainjs_to'] ) ) { + // main.js is a dependency of the inline data script so we need to make sure it doesn't load + add_filter( + 'script_loader_src', + function ( $src, $handle ) { + if ( $handle === WC_Google_Gtag_JS::get_instance()->script_handle ) { + $src = ''; + } + return $src; + }, + 10, + 2 + ); + + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + switch ( $_GET['move_mainjs_to'] ) { + case 'head': + wp_enqueue_script( + WC_Google_Gtag_JS::get_instance()->script_handle . '-head', + WC_Google_Analytics_Integration::get_instance()->get_js_asset_url( 'main.js' ), + array( + ...WC_Google_Analytics_Integration::get_instance()->get_js_asset_dependencies( 'main' ), + 'google-tag-manager', + ), + WC_Google_Analytics_Integration::get_instance()->get_js_asset_version( 'main' ), + false + ); + break; + case 'after_inline_data': + add_action( + 'wp_footer', + function () { + printf( + '', // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript + WC_Google_Analytics_Integration::get_instance()->get_js_asset_url( 'main.js' ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + ); + }, + 9999 + ); + break; + } + } + } +); + /* * Mimic the behavior of Google Listings & Ads or other plugins, * adding some inline events before `wp_enqueue_scripts.` @@ -40,3 +100,18 @@ function () { } } ); + +/** + * Snippet to bypass the WooCommerce dependency in Google Listings & Ads because + * in wp-env WooCommerce is installed in the directory woocommerce-trunk-nightly + */ +add_action( + 'wp_plugin_dependencies_slug', + function ( $slug ) { + if ( 'woocommerce' === $slug ) { + $slug = ''; + } + + return $slug; + } +); diff --git a/tests/e2e/utils/customer.js b/tests/e2e/utils/customer.js index 1097159a..31d806a9 100644 --- a/tests/e2e/utils/customer.js +++ b/tests/e2e/utils/customer.js @@ -66,8 +66,8 @@ export async function variableProductAddToCart( page, productID ) { */ export async function relatedProductAddToCart( page ) { const addToCart = ( await page.locator( '.related.products' ).isVisible() ) - ? '.related.products .add_to_cart_button' - : '.wp-block-woocommerce-related-products .add_to_cart_button'; + ? '.related.products .add_to_cart_button.product_type_simple' + : '.wp-block-woocommerce-related-products .add_to_cart_button.product_type_simple'; const addToCartButton = await page.locator( addToCart ).first(); await addToCartButton.click();