diff --git a/admin/class-ajax.php b/admin/class-ajax.php index ecae7831..c292294c 100644 --- a/admin/class-ajax.php +++ b/admin/class-ajax.php @@ -7,6 +7,7 @@ namespace EDAC\Admin; +use EDAC\Admin\OptIn\Email_Opt_In; use EDAC\Inc\Summary_Generator; /** @@ -33,7 +34,7 @@ public function init_hooks() { add_action( 'wp_ajax_edac_update_simplified_summary', array( $this, 'simplified_summary' ) ); add_action( 'wp_ajax_edac_dismiss_welcome_cta_ajax', array( $this, 'dismiss_welcome_cta' ) ); add_action( 'wp_ajax_edac_dismiss_dashboard_cta_ajax', array( $this, 'dismiss_dashboard_cta' ) ); - add_action( 'wp_ajax_edac_email_opt_in_ajax', array( $this, 'email_opt_in' ) ); + ( new Email_Opt_In() )->register_ajax_handlers(); } /** @@ -313,25 +314,25 @@ function ( $a, $b ) { $html .= '
'; - $html .= '
'; + $html .= '
'; - $html .= '

'; - $html .= '' . $rule['count'] . ' '; - $html .= esc_html( $rule['title'] ); + $html .= '

'; + $html .= '' . $rule['count'] . ' '; + $html .= esc_html( $rule['title'] ); if ( $count_ignored > 0 ) { $html .= '' . $count_ignored . ' Ignored Items'; } - $html .= '

'; - $html .= ''; - $html .= ( $expand_rule ) ? '' : ''; + $html .= ''; + $html .= ''; + $html .= ( $expand_rule ) ? '' : ''; - $html .= '
'; + $html .= '
'; if ( $results ) { $html .= '
'; - $html .= + $html .= '
- +
@@ -175,7 +177,7 @@

-
+
@@ -206,7 +208,7 @@ - + - + diff --git a/src/emailOptIn/index.js b/src/emailOptIn/index.js index ce605184..23374c7f 100644 --- a/src/emailOptIn/index.js +++ b/src/emailOptIn/index.js @@ -1,5 +1,7 @@ /* eslint-disable */ +import {initOptInModal} from "./modal"; + const edac_on_submit_ok = function () { const data = { action: "edac_email_opt_in_ajax", nonce: edac_email_opt_in_form.nonce }; const queryString = Object.keys(data) @@ -498,4 +500,8 @@ window._load_script = function (url, callback, isSubmit) { return false; }; addEvent(form_to_submit, 'submit', form_submit); + // Added in plugin version 1.11.0 and triggers through `window.onload` event. + if ( window.edac_email_opt_in_form.showModal ) { + initOptInModal(); + } })(); diff --git a/src/emailOptIn/modal.js b/src/emailOptIn/modal.js new file mode 100644 index 00000000..cbc31c77 --- /dev/null +++ b/src/emailOptIn/modal.js @@ -0,0 +1,45 @@ +/** + * Handle the opt-in modal for first time visitors to welcome page. + * + * This relies on the Thickbox library that is included in WordPress core which relies on jQuery. + */ + +/* global tb_show */ + +import { createFocusTrap } from 'focus-trap'; + +// Ensure the global variable is defined. +window.edac_email_opt_in_form = window.edac_email_opt_in_form || {}; + +export const initOptInModal = () => { + window.onload = function() { + tb_show( 'Accessibility Checker', '#TB_inline?width=600&inlineId=edac-opt-in-modal', null ); + + // a small delay is needed to ensure the modal is fully loaded before creating a focus trap. + setTimeout( + function() { + const modal = document.getElementById( 'TB_window' ) + .querySelector( '.tb-close-icon' ) + .setAttribute( 'aria-hidden', 'true' ); + + const focusTrap = createFocusTrap( modal ); + focusTrap.activate(); + + jQuery( document ).one( + 'tb_unload', + function() { + onModalClose( focusTrap ); + } + ); + }, + 200 + ); + }; +}; + +const onModalClose = ( focusTrap ) => { + focusTrap.deactivate(); + + fetch( window.edac_email_opt_in_form.ajaxurl + '?action=edac_email_opt_in_closed_modal_ajax' ) + .then( ( r ) => r.json() ); +}; diff --git a/src/emailOptIn/sass/email-opt-in.scss b/src/emailOptIn/sass/email-opt-in.scss index c459e9e2..417ee123 100644 --- a/src/emailOptIn/sass/email-opt-in.scss +++ b/src/emailOptIn/sass/email-opt-in.scss @@ -1,116 +1,9 @@ -/* +#_form_1_ { + padding: 0; + margin: 0; + input { + width: 100%; + } +} -// This is the original css provided by ActiveCampaign. -@import url(https://fonts.bunny.net/css?family=ibm-plex-sans:400,600); - #_form_1_ { font-size:14px; line-height:1.6; font-family:arial, helvetica, sans-serif; margin:0; } - #_form_1_ * { outline:0; } - ._form_hide { display:none; visibility:hidden; } - ._form_show { display:block; visibility:visible; } - #_form_1_._form-top { top:0; } - #_form_1_._form-bottom { bottom:0; } - #_form_1_._form-left { left:0; } - #_form_1_._form-right { right:0; } - #_form_1_ input[type="text"],#_form_1_ input[type="tel"],#_form_1_ input[type="date"],#_form_1_ textarea { padding:6px; height:auto; border:#979797 1px solid; border-radius:4px; color:#000 !important; font-size:14px; -webkit-box-sizing:border-box; -moz-box-sizing:border-box; box-sizing:border-box; } - #_form_1_ textarea { resize:none; } - #_form_1_ ._submit { -webkit-appearance:none; cursor:pointer; font-family:arial, sans-serif; font-size:14px; text-align:center; background:#015798 !important; border:0 !important; -moz-border-radius:4px !important; -webkit-border-radius:4px !important; border-radius:4px !important; color:#fff !important; padding:10px !important; } - #_form_1_ ._submit:disabled { cursor:not-allowed; opacity:0.4; } - #_form_1_ ._submit.processing { position:relative; } - #_form_1_ ._submit.processing::before { content:''; width:1em; height:1em; position:absolute; z-index:1; top:50%; left:50%; border:double 3px transparent; border-radius:50%; background-image:linear-gradient(#015798, #015798), conic-gradient(#015798, #fff); background-origin:border-box; background-clip:content-box, border-box; animation:1200ms ease 0s infinite normal none running _spin; } - #_form_1_ ._submit.processing::after { content:''; position:absolute; top:0; bottom:0; left:0; right:0; background:#015798 !important; border:0 !important; -moz-border-radius:4px !important; -webkit-border-radius:4px !important; border-radius:4px !important; color:#fff !important; padding:10px !important; } - @keyframes _spin { 0% { transform:translate(-50%, -50%) rotate(90deg); } - 100% { transform:translate(-50%, -50%) rotate(450deg); } - } - #_form_1_ ._close-icon { cursor:pointer; background-image:url('https://d226aj4ao1t61q.cloudfront.net/esfkyjh1u_forms-close-dark.png'); background-repeat:no-repeat; background-size:14.2px 14.2px; position:absolute; display:block; top:11px; right:9px; overflow:hidden; width:16.2px; height:16.2px; } - #_form_1_ ._close-icon:before { position:relative; } - //#_form_1_ ._form-body { margin-bottom:30px; } - #_form_1_ ._form-image-left { width:150px; float:left; } - #_form_1_ ._form-content-right { margin-left:164px; } - #_form_1_ ._form-branding { color:#fff; font-size:10px; clear:both; text-align:left; margin-top:30px; font-weight:100; } - #_form_1_ ._form-branding ._logo { display:block; width:130px; height:14px; margin-top:6px; background-image:url('https://d226aj4ao1t61q.cloudfront.net/hh9ujqgv5_aclogo_li.png'); background-size:130px auto; background-repeat:no-repeat; } - #_form_1_ .form-sr-only { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0, 0, 0, 0); border:0; } - #_form_1_ ._form-label,#_form_1_ ._form_element ._form-label { font-weight:bold; margin-bottom:5px; display:block; } - #_form_1_._dark ._form-branding { color:#333; } - #_form_1_._dark ._form-branding ._logo { background-image:url('https://d226aj4ao1t61q.cloudfront.net/jftq2c8s_aclogo_dk.png'); } - #_form_1_ ._form_element { position:relative; margin-bottom:10px; font-size:0; max-width:100%; } - #_form_1_ ._form_element * { font-size:14px; } - #_form_1_ ._form_element._clear { clear:both; width:100%; float:none; } - #_form_1_ ._form_element._clear:after { clear:left; } - #_form_1_ ._form_element input[type="text"],#_form_1_ ._form_element input[type="date"],#_form_1_ ._form_element select,#_form_1_ ._form_element textarea:not(.g-recaptcha-response) { display:block; width:100%; -webkit-box-sizing:border-box; -moz-box-sizing:border-box; box-sizing:border-box; font-family:inherit; } - #_form_1_ ._field-wrapper { position:relative; } - #_form_1_ ._inline-style { float:left; } - #_form_1_ ._inline-style input[type="text"] { width:150px; } - #_form_1_ ._inline-style:not(._clear) + ._inline-style:not(._clear) { margin-left:20px; } - #_form_1_ ._form_element img._form-image { max-width:100%; } - #_form_1_ ._form_element ._form-fieldset { border:0; padding:0.01em 0 0 0; margin:0; min-width:0; } - #_form_1_ ._clear-element { clear:left; } - #_form_1_ ._full_width { width:100%; } - #_form_1_ ._form_full_field { display:block; width:100%; margin-bottom:10px; } - #_form_1_ input[type="text"]._has_error,#_form_1_ textarea._has_error { border:#f37c7b 1px solid; } - #_form_1_ input[type="checkbox"]._has_error { outline:#f37c7b 1px solid; } - #_form_1_ ._error { display:block; position:absolute; font-size:14px; z-index:10000001; } - #_form_1_ ._error._above { padding-bottom:4px; bottom:39px; right:0; } - #_form_1_ ._error._below { padding-top:8px; top:100%; right:0; } - #_form_1_ ._error._above ._error-arrow { bottom:-4px; right:15px; border-left:8px solid transparent; border-right:8px solid transparent; border-top:8px solid #fdd; } - #_form_1_ ._error._below ._error-arrow { top:0; right:15px; border-left:8px solid transparent; border-right:8px solid transparent; border-bottom:8px solid #fdd; } - #_form_1_ ._error-inner { padding:12px 12px 12px 36px; background-color:#fdd; background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M16 8C16 12.4183 12.4183 16 8 16C3.58172 16 0 12.4183 0 8C0 3.58172 3.58172 0 8 0C12.4183 0 16 3.58172 16 8ZM9 3V9H7V3H9ZM9 13V11H7V13H9Z' fill='%23CA0000'/%3E%3C/svg%3E"); background-repeat:no-repeat; background-position:12px center; font-size:14px; font-family:arial, sans-serif; font-weight:600; line-height:16px; color:#000; text-align:center; text-decoration:none; -webkit-border-radius:4px; -moz-border-radius:4px; border-radius:4px; box-shadow:0px 1px 4px rgba(31, 33, 41, 0.298295); } - #_form_1_ ._error-inner._form_error { margin-bottom:5px; text-align:left; } - #_form_1_ ._button-wrapper ._error-inner._form_error { position:static; } - #_form_1_ ._error-inner._no_arrow { margin-bottom:10px; } - #_form_1_ ._error-arrow { position:absolute; width:0; height:0; } - #_form_1_ ._error-html { margin-bottom:10px; } - .pika-single { z-index:10000001 !important; } - #_form_1_ input[type="text"].datetime_date { width:69%; display:inline; } - #_form_1_ select.datetime_time { width:29%; display:inline; height:32px; } - #_form_1_ input[type="date"].datetime_date { width:69%; display:inline-flex; } - #_form_1_ input[type="time"].datetime_time { width:29%; display:inline-flex; } - @media all and (min-width:320px) and (max-width:667px) { ::-webkit-scrollbar { display:none; } - #_form_1_ { margin:0; width:100%; min-width:100%; max-width:100%; box-sizing:border-box; } - #_form_1_ * { -webkit-box-sizing:border-box; -moz-box-sizing:border-box; box-sizing:border-box; font-size:1em; } - #_form_1_ ._form-content { margin:0; width:100%; } - #_form_1_ ._form-inner { display:block; min-width:100%; } - #_form_1_ ._form-title,#_form_1_ ._inline-style { margin-top:0; margin-right:0; margin-left:0; } - #_form_1_ ._form-title { font-size:1.2em; } - #_form_1_ ._form_element { margin:0 0 20px; padding:0; width:100%; } - #_form_1_ ._form-element,#_form_1_ ._inline-style,#_form_1_ input[type="text"],#_form_1_ label,#_form_1_ p,#_form_1_ textarea:not(.g-recaptcha-response) { float:none; display:block; width:100%; } - #_form_1_ ._row._checkbox-radio label { display:inline; } - #_form_1_ ._row,#_form_1_ p,#_form_1_ label { margin-bottom:0.7em; width:100%; } - #_form_1_ ._row input[type="checkbox"],#_form_1_ ._row input[type="radio"] { margin:0 !important; vertical-align:middle !important; } - #_form_1_ ._row input[type="checkbox"] + span label { display:inline; } - #_form_1_ ._row span label { margin:0 !important; width:initial !important; vertical-align:middle !important; } - #_form_1_ ._form-image { max-width:100%; height:auto !important; } - #_form_1_ input[type="text"] { padding-left:10px; padding-right:10px; font-size:16px; line-height:1.3em; -webkit-appearance:none; } - #_form_1_ input[type="radio"],#_form_1_ input[type="checkbox"] { display:inline-block; width:1.3em; height:1.3em; font-size:1em; margin:0 0.3em 0 0; vertical-align:baseline; } - #_form_1_ button[type="submit"] { padding:20px; font-size:1.5em; } - #_form_1_ ._inline-style { margin:20px 0 0 !important; } - } - #_form_1_ { position:relative; text-align:left; margin:25px auto 0; padding-top:20px; padding-right:20px; padding-bottom:20px; padding-left:20px; -webkit-box-sizing:border-box; -moz-box-sizing:border-box; box-sizing:border-box; *zoom:1; background:#fff !important; border:0px solid #b0b0b0 !important; max-width:500px; -moz-border-radius:0px !important; -webkit-border-radius:0px !important; border-radius:0px !important; color:#000 !important; } - #_form_1_._inline-form,#_form_1_._inline-form ._form-content,#_form_1_._inline-form input,#_form_1_._inline-form ._submit,#_form_1_._inline-form ._form-label,#_form_1_._inline-form ._html-code :not(h1):not(h2):not(h3):not(h4):not(h5):not(h6) { font-family:"IBM Plex Sans", Helvetica, sans-serif; } - #_form_1_._inline-form ._form-title { font-size:22px; line-height:normal; font-weight:600; margin-bottom:0; } - #_form_1_:before,#_form_1_:after { content:" "; display:table; } - #_form_1_:after { clear:both; } - #_form_1_._inline-style { width:auto; display:inline-block; } - #_form_1_._inline-style input[type="text"],#_form_1_._inline-style input[type="date"] { padding:10px 12px; } - #_form_1_._inline-style button._inline-style { position:relative; top:27px; } - #_form_1_._inline-style p { margin:0; } - #_form_1_._inline-style ._button-wrapper { position:relative; margin:27px 12.5px 0 20px; } - #_form_1_ ._form-thank-you { position:relative; left:0; right:0; text-align:center; font-size:18px; } - @media all and (min-width:320px) and (max-width:667px) { #_form_1_._inline-form._inline-style ._inline-style._button-wrapper { margin-top:20px !important; margin-left:0 !important; } - } - #_form_1_ .iti.iti--allow-dropdown.iti--separate-dial-code { width:100%; } - #_form_1_ .iti input { width:100%; height:32px; border:#979797 1px solid; border-radius:4px; } - #_form_1_ .iti--separate-dial-code .iti__selected-flag { background-color:#fff; border-radius:4px; } - #_form_1_ .iti--separate-dial-code .iti__selected-flag:hover { background-color:rgba(0, 0, 0, 0.05); } - #_form_1_ .iti__country-list { border-radius:4px; margin-top:4px; min-width:460px; } - #_form_1_ .iti__country-list--dropup { margin-bottom:4px; } - #_form_1_ .phone-error-hidden { display:none; } - #_form_1_ .phone-error { color:#e40e49; } - #_form_1_ .phone-input-error { border:1px solid #e40e49 !important; } -*/ - - - #_form_1_ { padding: 0; margin: 0;} - #_form_1_ input { width: 100%; } - - - \ No newline at end of file diff --git a/tests/phpunit/Admin/OptIn/EmailOptInTest.php b/tests/phpunit/Admin/OptIn/EmailOptInTest.php new file mode 100644 index 00000000..adb7982f --- /dev/null +++ b/tests/phpunit/Admin/OptIn/EmailOptInTest.php @@ -0,0 +1,223 @@ +current_user_id = $this->factory()->user->create(); + + if ( is_wp_error( $this->current_user_id ) ) { + $this->fail( $this->current_user_id->get_error_message() ); + } + + wp_set_current_user( $this->current_user_id ); + } + + /** + * Delete the user to clean up after tests. + */ + protected function tearDown(): void { + wp_delete_user( $this->current_user_id ); + } + + /** + * Test that the user_already_subscribed method returns false when the user has not subscribed. + */ + public function test_user_already_subscribed_returns_false_when_user_has_not_subscribed() { + $this->assertFalse( Email_Opt_In::user_already_subscribed() ); + } + + /** + * Test that the user_already_subscribed method returns true when the user has subscribed. + */ + public function test_user_already_subscribed_returns_true_when_user_is_subscribed() { + update_user_meta( $this->current_user_id, Email_Opt_In::EDAC_USER_OPTIN_META_KEY, true ); + $this->assertTrue( Email_Opt_In::user_already_subscribed() ); + } + + /** + * Test that the should_show_modal method returns true when the user has not seen the modal. + */ + public function test_should_show_modal_returns_true_when_user_has_not_seen_it_yet() { + $this->assertTrue( Email_Opt_In::should_show_modal() ); + } + + /** + * Test that the should_show_modal method returns false when the user has seen the modal. + */ + public function test_should_show_modal_returns_false_when_user_has_seen_it() { + update_user_meta( $this->current_user_id, Email_Opt_In::EDAC_USER_OPTIN_SEEN_MODAL_KEY, true ); + $this->assertFalse( Email_Opt_In::should_show_modal() ); + } + + /** + * Test that the enqueue_scripts method registers the scripts and styles needed for the opt-in modal. + */ + public function test_enqueue_scripts_registers_thickbox_when_user_has_not_seen_modal() { + $email_opt_in = new Email_Opt_In(); + $email_opt_in->enqueue_scripts(); + $this->assertTrue( wp_script_is( 'email-opt-in-form', 'enqueued' ) ); + $this->assertTrue( wp_style_is( 'email-opt-in-form', 'enqueued' ) ); + // check that admin footer has been hooked and thickbox is enqueued. + $this->assertEquals( 10, has_action( 'admin_footer', array( $email_opt_in, 'render_modal' ) ) ); + $this->assertTrue( wp_script_is( 'thickbox', 'enqueued' ) ); + } + + /** + * Test that the enqueue_scripts method does not add thickbox when the user has seen the modal. + */ + public function test_enqueue_scripts_does_not_add_thickbox_when_user_has_seen_modal() { + update_user_meta( $this->current_user_id, Email_Opt_In::EDAC_USER_OPTIN_SEEN_MODAL_KEY, true ); + $email_opt_in = new Email_Opt_In(); + $email_opt_in->enqueue_scripts(); + $this->assertTrue( wp_script_is( 'email-opt-in-form', 'enqueued' ) ); + $this->assertTrue( wp_style_is( 'email-opt-in-form', 'enqueued' ) ); + + // check that admin footer has NOT been hooked. + $this->assertFalse( has_action( 'admin_footer', array( $email_opt_in, 'render_modal' ) ) ); + } + + /** + * Test that the modal markup renders. + */ + public function test_form_renders_modal() { + $email_opt_in = new Email_Opt_In(); + $email_opt_in->render_modal(); + $this->expectOutputRegex( '/
ID, 'first_name', true ) ) { + update_user_meta( $maybe_existing_user->ID, 'first_name', self::TEST_USER_FIRST_NAME ); + } + + $user_id = $maybe_existing_user ? $maybe_existing_user->ID : $this->factory()->user->create( + array( + 'user_email' => self::TEST_USER_EMAIL, + 'first_name' => self::TEST_USER_FIRST_NAME, + ) + ); + wp_set_current_user( $user_id ); + $email_opt_in = new Email_Opt_In(); + $email_opt_in->render_form(); + $this->expectOutputRegex( '/.*?value="' . self::TEST_USER_EMAIL . '".*?/' ); + $this->expectOutputRegex( '/.*?value="' . self::TEST_USER_FIRST_NAME . '".*?/' ); + } + + /** + * Test that the ajax handlers are registered. + */ + public function test_ajax_handlers_registered() { + $email_opt_in = new Email_Opt_In(); + $email_opt_in->register_ajax_handlers(); + $this->assertEquals( 10, has_action( 'wp_ajax_edac_email_opt_in_ajax', array( $email_opt_in, 'handle_email_opt_in' ) ) ); + $this->assertEquals( 10, has_action( 'wp_ajax_edac_email_opt_in_closed_modal_ajax', array( $email_opt_in, 'handle_email_opt_in_closed_modal' ) ) ); + } + + /** + * Test that the email opt-in ajax handler creates the user meta. + */ + public function test_handle_email_opt_in() { + // mock the action in the $_POST global. + $_POST['action'] = 'edac_email_opt_in_ajax'; + $email_opt_in = new Email_Opt_In(); + + $this->filter_ajax_die_handler_to_exception_instead_of_actual_die(); + try { + ob_start(); + $email_opt_in->handle_email_opt_in(); + } catch ( Exception $e ) { + // We expected this, just clear the buffer. + ob_end_clean(); + } + + $this->assertTrue( + (bool) get_user_meta( + get_current_user_id(), + Email_Opt_In::EDAC_USER_OPTIN_META_KEY, + true + ) + ); + } + + /** + * Test that the email opt-in closed modal ajax handler creates the user meta. + */ + public function test_handle_email_opt_in_closed_modal() { + // mock the action in the $_POST global. + $_POST['action'] = 'handle_email_opt_in_closed_modal_ajax'; + $email_opt_in = new Email_Opt_In(); + + $this->filter_ajax_die_handler_to_exception_instead_of_actual_die(); + try { + ob_start(); + $email_opt_in->handle_email_opt_in_closed_modal(); + } catch ( Exception $e ) { + // We expected this, just clear the buffer. + ob_end_clean(); + } + + $this->assertTrue( + (bool) get_user_meta( + get_current_user_id(), + Email_Opt_In::EDAC_USER_OPTIN_SEEN_MODAL_KEY, + true + ) + ); + } + + /** + * Filter the wp_die handler to throw an exception instead of actually dying, + * so that we can test the ajax handlers. + */ + private function filter_ajax_die_handler_to_exception_instead_of_actual_die() { + add_filter( 'wp_doing_ajax', '__return_true' ); + add_filter( + 'wp_die_ajax_handler', + array( $this, 'throw_exception_instead_of_dying' ), + 1, + 1 + ); + } + + /** + * Throw an exception instead of actually dying. + * + * @return Closure + */ + public function throw_exception_instead_of_dying() { + return function ( $message ) { + // phpcs:ignore WordPress.Security -- this is just a test. + throw new Exception( $message ); + }; + } +} diff --git a/tests/phpunit/helper-functions/UrlExistsTest.php b/tests/phpunit/helper-functions/UrlExistsTest.php index e2bda1a1..a6e9c9ad 100644 --- a/tests/phpunit/helper-functions/UrlExistsTest.php +++ b/tests/phpunit/helper-functions/UrlExistsTest.php @@ -17,7 +17,7 @@ class UrlExistsTest extends WP_UnitTestCase { */ public function test_url_exists() { $url = 'https://httpbin.org/status/200'; - $this->assertTrue( edac_url_exists( $url ) ); + $this->assertTrue( edac_url_exists( $url, 20 ) ); } /**