diff --git a/assets/js/src/integration/wsforms.js b/assets/js/src/integration/wsforms.js index 0aee68dda..da7de2bca 100644 --- a/assets/js/src/integration/wsforms.js +++ b/assets/js/src/integration/wsforms.js @@ -7,15 +7,8 @@ const $ = window.jQuery; $( document ).on( - 'wsf-submit-success wsf-save-success', - function ( - event, - formObject, - formId, - formInstanceId, - formEl, - formCanvasEl - ) { + 'wsf-submit-success', + function ( event, formObject, formId, formInstanceId, formEl ) { // All the magic happens here. window.PUM.integrations.formSubmission( $( formEl ), { formProvider, diff --git a/classes/Integration/Form/Forminator.php b/classes/Integration/Form/Forminator.php index 320393178..c83976e14 100644 --- a/classes/Integration/Form/Forminator.php +++ b/classes/Integration/Form/Forminator.php @@ -119,6 +119,23 @@ public function get_form_selectlist() { * @param array $field_data_array Field data array. */ public function on_success( $entry, $form_id, $field_data_array ) { + if ( ! is_object( $entry ) && ! is_array( $entry ) ) { + return; + } + + $status = is_object( $entry ) && isset( $entry->status ) ? $entry->status : ( is_array( $entry ) && isset( $entry['status'] ) ? $entry['status'] : null ); + if ( null !== $status && 'active' !== $status ) { + return; + } + + if ( null === $status ) { + $is_spam = is_object( $entry ) && isset( $entry->is_spam ) ? $entry->is_spam : ( is_array( $entry ) && isset( $entry['is_spam'] ) ? $entry['is_spam'] : false ); + $draft_id = is_object( $entry ) && isset( $entry->draft_id ) ? $entry->draft_id : ( is_array( $entry ) && isset( $entry['draft_id'] ) ? $entry['draft_id'] : null ); + if ( $is_spam || ! empty( $draft_id ) ) { + return; + } + } + if ( ! $this->should_process_submission() ) { return; } diff --git a/classes/Integration/Form/WSForms.php b/classes/Integration/Form/WSForms.php index d350d3621..b5b812eb8 100644 --- a/classes/Integration/Form/WSForms.php +++ b/classes/Integration/Form/WSForms.php @@ -88,6 +88,18 @@ public function get_form_selectlist() { * @param \WS_Form_Submit $submit */ public function on_success( $submit ) { + if ( ! is_object( $submit ) + || ! isset( $submit->post_mode ) + || 'submit' !== $submit->post_mode + || ! empty( $submit->error ) + || ! empty( $submit->error_validation_actions ) + || ! isset( $submit->form_id ) + || ! is_numeric( $submit->form_id ) + || $submit->form_id <= 0 + ) { + return; + } + if ( ! $this->should_process_submission() ) { return; } diff --git a/tests/php/tests/FormSubmissionPhases_Test.php b/tests/php/tests/FormSubmissionPhases_Test.php index afab5a6bb..d1d3f48fc 100644 --- a/tests/php/tests/FormSubmissionPhases_Test.php +++ b/tests/php/tests/FormSubmissionPhases_Test.php @@ -345,6 +345,61 @@ public function get_form( $id ) { $this->assertNull( PUM_Integrations::$form_submission ); } + /** + * Forminator rejects explicit failures and legacy spam/draft entries. + */ + public function test_forminator_requires_active_or_legacy_success_entry() { + $observed = 0; + $this->observation_action = static function () use ( &$observed ) { + ++$observed; + }; + add_action( 'pum_integrated_form_submission', $this->observation_action ); + + $integration = new PUM_Integration_Form_Forminator(); + foreach ( [ 'draft', 'abandoned', 'spam' ] as $status ) { + $integration->on_success( (object) [ 'status' => $status ], 7, [] ); + } + $integration->on_success( null, 7, [] ); + $integration->on_success( 'invalid-entry', 7, [] ); + $integration->on_success( (object) [ 'is_spam' => true ], 7, [] ); + $integration->on_success( (object) [ 'draft_id' => 'draft-7' ], 7, [] ); + + $this->assertSame( 0, $observed ); + + $integration->on_success( (object) [ 'status' => 'active' ], 7, [] ); + $integration->on_success( new stdClass(), 7, [] ); + $this->assertSame( 2, $observed ); + } + + /** + * WS Form saves and failed validation never become conversions. + */ + public function test_ws_form_requires_one_valid_submit_receipt() { + $observed = 0; + $this->observation_action = static function () use ( &$observed ) { + ++$observed; + }; + add_action( 'pum_integrated_form_submission', $this->observation_action ); + + $integration = new PUM_Integration_Form_WSForms(); + $valid = [ + 'form_id' => 7, + 'post_mode' => 'submit', + 'error' => false, + 'error_validation_actions' => [], + ]; + + $integration->on_success( (object) array_merge( $valid, [ 'post_mode' => 'save' ] ) ); + $integration->on_success( (object) array_merge( $valid, [ 'error' => true ] ) ); + $integration->on_success( (object) array_merge( $valid, [ 'error_validation_actions' => [ 'field_1' => 'Required' ] ] ) ); + $integration->on_success( (object) array_merge( $valid, [ 'form_id' => 0 ] ) ); + + $this->assertSame( 0, $observed ); + + $integration->on_success( (object) $valid ); + $this->assertSame( 1, $observed ); + } + /** * Non-AJAX callbacks increment each Core metric exactly once. * diff --git a/tests/unit/wsforms-integration.test.js b/tests/unit/wsforms-integration.test.js new file mode 100644 index 000000000..5b6496004 --- /dev/null +++ b/tests/unit/wsforms-integration.test.js @@ -0,0 +1,34 @@ +describe( 'WS Form success integration', () => { + let eventName; + let handler; + let formSubmission; + + beforeEach( () => { + jest.resetModules(); + formSubmission = jest.fn(); + window.PUM = { integrations: { formSubmission } }; + window.jQuery = jest.fn( () => ( { + on: jest.fn( ( registeredEvent, callback ) => { + eventName = registeredEvent; + handler = callback; + } ), + } ) ); + + require( '../../assets/js/src/integration/wsforms' ); + } ); + + test( 'observes submit success and never registers save success', () => { + expect( eventName ).toBe( 'wsf-submit-success' ); + expect( eventName ).not.toContain( 'wsf-save-success' ); + + const formElement = document.createElement( 'form' ); + handler( {}, {}, 7, 'instance-1', formElement, {} ); + + expect( formSubmission ).toHaveBeenCalledTimes( 1 ); + expect( formSubmission ).toHaveBeenCalledWith( expect.anything(), { + formProvider: 'wsforms', + formId: 7, + formInstanceId: 'instance-1', + } ); + } ); +} );