diff --git a/tests/php/includes/fields/test-class-acf-field-checkbox.php b/tests/php/includes/fields/test-class-acf-field-checkbox.php index 18f00363..e46c28e1 100644 --- a/tests/php/includes/fields/test-class-acf-field-checkbox.php +++ b/tests/php/includes/fields/test-class-acf-field-checkbox.php @@ -227,4 +227,150 @@ public function test_all_choices_selected() { $this->assertIsArray( $result ); $this->assertCount( 3, $result ); } + + /** + * Test validate_value rejects empty custom values. + * + * Tests the validate_value custom value check: + * `if ( empty( $value ) && $value !== '0' ) { return __( 'Checkbox custom values cannot be empty...' ); }` + */ + public function test_validate_value_rejects_empty_custom_value() { + $field = $this->get_field( array( 'allow_custom' => 1 ) ); + + // With allow_custom enabled, empty values in array should fail. + $valid = $this->field_instance->validate_value( true, array( 'red', '' ), $field, 'acf[field_checkbox_test]' ); + + $this->assertIsString( $valid ); // Returns error message string. + $this->assertStringContainsString( 'cannot be empty', $valid ); + } + + /** + * Test validate_value allows '0' as custom value. + * + * Tests the validate_value '0' exception: + * `if ( empty( $value ) && $value !== '0' )` + */ + public function test_validate_value_allows_zero_string_custom() { + $field = $this->get_field( array( 'allow_custom' => 1 ) ); + + // '0' should be allowed even though empty() returns true for it. + $valid = $this->field_instance->validate_value( true, array( 'red', '0' ), $field, 'acf[field_checkbox_test]' ); + + $this->assertTrue( $valid ); + } + + /** + * Test validate_value skips validation without allow_custom. + * + * Tests the early return: + * `if ( ! is_array( $value ) || empty( $field['allow_custom'] ) ) { return $valid; }` + */ + public function test_validate_value_skips_without_allow_custom() { + $field = $this->get_field( array( 'allow_custom' => 0 ) ); + + // Without allow_custom, validation returns passed $valid unchanged. + $valid = $this->field_instance->validate_value( true, array( 'red', '' ), $field, 'acf[field_checkbox_test]' ); + + $this->assertTrue( $valid ); + } + + /** + * Test validate_value skips validation for non-array. + */ + public function test_validate_value_skips_for_non_array() { + $field = $this->get_field( array( 'allow_custom' => 1 ) ); + + // Non-array values return $valid unchanged. + $valid = $this->field_instance->validate_value( true, 'red', $field, 'acf[field_checkbox_test]' ); + + $this->assertTrue( $valid ); + } + + /** + * Test format_value returns empty array for null. + * + * Tests the acf_is_empty check: + * `if ( acf_is_empty( $value ) ) { return array(); }` + */ + public function test_format_value_null_returns_empty_array() { + $field = $this->get_field(); + + $result = $this->field_instance->format_value( null, $this->post_id, $field ); + + $this->assertIsArray( $result ); + $this->assertEmpty( $result ); + } + + /** + * Test format_value returns empty array for empty string. + */ + public function test_format_value_empty_string_returns_empty_array() { + $field = $this->get_field(); + + $result = $this->field_instance->format_value( '', $this->post_id, $field ); + + $this->assertIsArray( $result ); + $this->assertEmpty( $result ); + } + + /** + * Test format_value converts single value to array. + * + * Tests the acf_array conversion: + * `$value = acf_array( $value );` + */ + public function test_format_value_converts_string_to_array() { + $field = $this->get_field( array( 'return_format' => 'value' ) ); + + // Single string value should be converted to array. + $result = $this->field_instance->format_value( 'red', $this->post_id, $field ); + + $this->assertIsArray( $result ); + $this->assertContains( 'red', $result ); + } + + /** + * Test get_rest_schema includes enum without allow_custom. + * + * Tests the enum generation branch: + * `if ( ! empty( $field['allow_custom'] ) ) { return $schema; }` + */ + public function test_get_rest_schema_includes_enum() { + $field = $this->get_field( array( 'allow_custom' => 0 ) ); + + $schema = $this->field_instance->get_rest_schema( $field ); + + $this->assertArrayHasKey( 'items', $schema ); + $this->assertArrayHasKey( 'enum', $schema['items'] ); + } + + /** + * Test get_rest_schema excludes enum with allow_custom. + * + * Tests the early return when allow_custom is enabled. + */ + public function test_get_rest_schema_no_enum_with_allow_custom() { + $field = $this->get_field( array( 'allow_custom' => 1 ) ); + + $schema = $this->field_instance->get_rest_schema( $field ); + + // With allow_custom enabled, enum should NOT be set in items. + $this->assertArrayHasKey( 'items', $schema ); + $this->assertArrayNotHasKey( 'enum', $schema['items'] ); + } + + /** + * Test update_value returns empty array unchanged. + * + * Tests the early return: + * `if ( empty( $value ) ) { return $value; }` + */ + public function test_update_value_empty_array_returns_early() { + $field = $this->get_field(); + + $result = $this->field_instance->update_value( array(), $this->post_id, $field ); + + $this->assertIsArray( $result ); + $this->assertEmpty( $result ); + } } diff --git a/tests/php/includes/fields/test-class-acf-field-link.php b/tests/php/includes/fields/test-class-acf-field-link.php index fdac59b2..bb7e4eba 100644 --- a/tests/php/includes/fields/test-class-acf-field-link.php +++ b/tests/php/includes/fields/test-class-acf-field-link.php @@ -218,4 +218,179 @@ public function test_internal_url() { $this->assertEquals( '/about/', $result['url'] ); } + + /** + * Test get_link with array value (ACF 5.6.0+). + * + * Tests the array merge branch: + * `if ( is_array( $value ) ) { $link = array_merge( $link, $value ); }` + */ + public function test_get_link_array_format() { + $input = array( + 'title' => 'Custom Title', + 'url' => 'https://custom.com', + 'target' => '_blank', + ); + + $result = $this->field_instance->get_link( $input ); + + $this->assertEquals( 'Custom Title', $result['title'] ); + $this->assertEquals( 'https://custom.com', $result['url'] ); + $this->assertEquals( '_blank', $result['target'] ); + } + + /** + * Test get_link with string value (legacy ACF < 5.6.0). + * + * Tests the string branch: + * `elseif ( is_string( $value ) ) { $link['url'] = $value; }` + */ + public function test_get_link_string_format() { + $result = $this->field_instance->get_link( 'https://example.com/page' ); + + $this->assertEquals( '', $result['title'] ); + $this->assertEquals( 'https://example.com/page', $result['url'] ); + $this->assertEquals( '', $result['target'] ); + } + + /** + * Test get_link with empty string returns defaults. + */ + public function test_get_link_empty_string() { + $result = $this->field_instance->get_link( '' ); + + $this->assertEquals( '', $result['title'] ); + $this->assertEquals( '', $result['url'] ); + $this->assertEquals( '', $result['target'] ); + } + + /** + * Test get_link with partial array fills defaults. + */ + public function test_get_link_partial_array() { + $result = $this->field_instance->get_link( array( 'url' => 'https://partial.com' ) ); + + $this->assertEquals( '', $result['title'] ); + $this->assertEquals( 'https://partial.com', $result['url'] ); + $this->assertEquals( '', $result['target'] ); + } + + /** + * Test validate_value returns early when not required. + * + * Tests the early return: + * `if ( ! $field['required'] ) { return $valid; }` + */ + public function test_validate_value_not_required_returns_valid() { + $field = $this->get_field( array( 'required' => 0 ) ); + + // Even with empty value, should return passed $valid when not required. + $valid = $this->field_instance->validate_value( true, '', $field, 'acf[field_link_test]' ); + + $this->assertTrue( $valid ); + } + + /** + * Test validate_value with empty URL when required fails. + * + * Tests the URL check: + * `if ( empty( $value ) || empty( $value['url'] ) ) { return false; }` + */ + public function test_validate_value_empty_url_required_fails() { + $field = $this->get_field( array( 'required' => 1 ) ); + $link = array( + 'title' => 'Has title', + 'url' => '', // Empty URL. + 'target' => '', + ); + + $valid = $this->field_instance->validate_value( true, $link, $field, 'acf[field_link_test]' ); + + $this->assertFalse( $valid ); + } + + /** + * Test update_value converts empty URL array to empty string. + * + * Tests the empty check: + * `if ( empty( $value ) || empty( $value['url'] ) ) { $value = ''; }` + */ + public function test_update_value_empty_url_converts_to_empty_string() { + $field = $this->get_field(); + $link = array( + 'title' => 'Has title', + 'url' => '', + 'target' => '_blank', + ); + + $result = $this->field_instance->update_value( $link, $this->post_id, $field ); + + $this->assertEquals( '', $result ); + } + + /** + * Test update_value with null converts to empty string. + */ + public function test_update_value_null_converts_to_empty_string() { + $field = $this->get_field(); + + $result = $this->field_instance->update_value( null, $this->post_id, $field ); + + $this->assertEquals( '', $result ); + } + + /** + * Test update_value preserves valid link data. + */ + public function test_update_value_preserves_valid_link() { + $field = $this->get_field(); + $link = array( + 'title' => 'Title', + 'url' => 'https://valid.com', + 'target' => '_blank', + ); + + $result = $this->field_instance->update_value( $link, $this->post_id, $field ); + + $this->assertIsArray( $result ); + $this->assertEquals( 'https://valid.com', $result['url'] ); + } + + /** + * Test format_value with URL return_format. + * + * Tests the return_format branch: + * `if ( $field['return_format'] == 'url' ) { return $link['url']; }` + */ + public function test_format_value_url_return_format() { + $field = $this->get_field( array( 'return_format' => 'url' ) ); + $link = array( + 'title' => 'Title', + 'url' => 'https://url-only.com', + 'target' => '', + ); + + $result = $this->field_instance->format_value( $link, $this->post_id, $field ); + + $this->assertEquals( 'https://url-only.com', $result ); + } + + /** + * Test format_value with array return_format returns full link. + */ + public function test_format_value_array_return_format() { + $field = $this->get_field( array( 'return_format' => 'array' ) ); + $link = array( + 'title' => 'Full Title', + 'url' => 'https://array.com', + 'target' => '_self', + ); + + $result = $this->field_instance->format_value( $link, $this->post_id, $field ); + + $this->assertIsArray( $result ); + $this->assertEquals( 'Full Title', $result['title'] ); + $this->assertEquals( 'https://array.com', $result['url'] ); + $this->assertEquals( '_self', $result['target'] ); + } } diff --git a/tests/php/includes/fields/test-class-acf-field-oembed.php b/tests/php/includes/fields/test-class-acf-field-oembed.php index 95aaa82a..de4e2f89 100644 --- a/tests/php/includes/fields/test-class-acf-field-oembed.php +++ b/tests/php/includes/fields/test-class-acf-field-oembed.php @@ -136,4 +136,118 @@ public function test_validate_rest_value_valid() { $this->assertTrue( $valid ); } + + /** + * Test prepare_field sets default width when empty. + * + * Tests the default width logic: + * `if ( ! $field['width'] ) { $field['width'] = $this->width; }` + */ + public function test_prepare_field_sets_default_width() { + $field = $this->get_field( array( 'width' => '' ) ); + + $prepared = $this->field_instance->prepare_field( $field ); + + $this->assertEquals( 640, $prepared['width'] ); // Default is 640. + } + + /** + * Test prepare_field sets default height when empty. + * + * Tests the default height logic: + * `if ( ! $field['height'] ) { $field['height'] = $this->height; }` + */ + public function test_prepare_field_sets_default_height() { + $field = $this->get_field( array( 'height' => '' ) ); + + $prepared = $this->field_instance->prepare_field( $field ); + + $this->assertEquals( 390, $prepared['height'] ); // Default is 390. + } + + /** + * Test prepare_field preserves custom width. + */ + public function test_prepare_field_preserves_custom_width() { + $field = $this->get_field( array( 'width' => '800' ) ); + + $prepared = $this->field_instance->prepare_field( $field ); + + $this->assertEquals( '800', $prepared['width'] ); + } + + /** + * Test prepare_field preserves custom height. + */ + public function test_prepare_field_preserves_custom_height() { + $field = $this->get_field( array( 'height' => '600' ) ); + + $prepared = $this->field_instance->prepare_field( $field ); + + $this->assertEquals( '600', $prepared['height'] ); + } + + /** + * Test prepare_field with zero width uses default. + * + * Tests falsy check (0 is treated as empty). + */ + public function test_prepare_field_zero_width_uses_default() { + $field = $this->get_field( array( 'width' => 0 ) ); + + $prepared = $this->field_instance->prepare_field( $field ); + + $this->assertEquals( 640, $prepared['width'] ); + } + + /** + * Test prepare_field with zero height uses default. + */ + public function test_prepare_field_zero_height_uses_default() { + $field = $this->get_field( array( 'height' => 0 ) ); + + $prepared = $this->field_instance->prepare_field( $field ); + + $this->assertEquals( 390, $prepared['height'] ); + } + + /** + * Test format_value returns empty for null. + * + * Tests the early return: + * `if ( empty( $value ) ) { return $value; }` + */ + public function test_format_value_null_returns_early() { + $field = $this->get_field(); + + $result = $this->field_instance->format_value( null, $this->post_id, $field ); + + $this->assertNull( $result ); + } + + /** + * Test format_value returns empty for false. + */ + public function test_format_value_false_returns_early() { + $field = $this->get_field(); + + $result = $this->field_instance->format_value( false, $this->post_id, $field ); + + $this->assertFalse( $result ); + } + + /** + * Test get_rest_schema adds uri format. + * + * Tests the format addition: + * `$schema['format'] = 'uri';` + */ + public function test_get_rest_schema_includes_uri_format() { + $field = $this->get_field(); + + $schema = $this->field_instance->get_rest_schema( $field ); + + $this->assertArrayHasKey( 'format', $schema ); + $this->assertEquals( 'uri', $schema['format'] ); + } } diff --git a/tests/php/includes/fields/test-class-acf-field-radio.php b/tests/php/includes/fields/test-class-acf-field-radio.php index 3e38ca52..06b73945 100644 --- a/tests/php/includes/fields/test-class-acf-field-radio.php +++ b/tests/php/includes/fields/test-class-acf-field-radio.php @@ -193,4 +193,144 @@ public function test_format_value_unknown_choice() { // Unknown choices should return the value itself. $this->assertEquals( 'unknown', $result ); } + + /** + * Test load_value extracts single value from array. + * + * Tests the load_value array handling: + * `if ( is_array( $value ) ) { $value = array_pop( $value ); }` + */ + public function test_load_value_extracts_from_array() { + $field = $this->get_field(); + + // Radio is single-select, so if stored as array, should extract last value. + $result = $this->field_instance->load_value( array( 'red', 'green', 'blue' ), $this->post_id, $field ); + + $this->assertEquals( 'blue', $result ); + } + + /** + * Test load_value returns single value unchanged. + */ + public function test_load_value_single_unchanged() { + $field = $this->get_field(); + + $result = $this->field_instance->load_value( 'green', $this->post_id, $field ); + + $this->assertEquals( 'green', $result ); + } + + /** + * Test update_value allows numeric zero through. + * + * Tests the early return condition: + * `if ( ! $value && ! is_numeric( $value ) ) { return $value; }` + */ + public function test_update_value_allows_numeric_zero() { + $field = $this->get_field( + array( + 'choices' => array( + '0' => 'Zero', + '1' => 'One', + '2' => 'Two', + ), + ) + ); + + // Numeric 0 should pass through, not return early. + $result = $this->field_instance->update_value( 0, $this->post_id, $field ); + + $this->assertSame( 0, $result ); + } + + /** + * Test update_value allows string zero through. + */ + public function test_update_value_allows_string_zero() { + $field = $this->get_field( + array( + 'choices' => array( + '0' => 'Zero', + '1' => 'One', + ), + ) + ); + + $result = $this->field_instance->update_value( '0', $this->post_id, $field ); + + $this->assertSame( '0', $result ); + } + + /** + * Test update_value returns empty string early. + * + * Tests that non-numeric empty values return early. + */ + public function test_update_value_empty_returns_early() { + $field = $this->get_field(); + + $result = $this->field_instance->update_value( '', $this->post_id, $field ); + + $this->assertEquals( '', $result ); + } + + /** + * Test update_value returns null early. + */ + public function test_update_value_null_returns_early() { + $field = $this->get_field(); + + $result = $this->field_instance->update_value( null, $this->post_id, $field ); + + $this->assertNull( $result ); + } + + /** + * Test get_rest_schema includes enum without other_choice. + * + * Tests the enum generation: + * `if ( empty( $field['other_choice'] ) ) { return $schema; }` + */ + public function test_get_rest_schema_includes_enum() { + $field = $this->get_field( array( 'other_choice' => 0 ) ); + + $schema = $this->field_instance->get_rest_schema( $field ); + + $this->assertArrayHasKey( 'enum', $schema ); + } + + /** + * Test get_rest_schema excludes enum with other_choice. + * + * Tests the early return when other_choice is enabled: + * `if ( ! empty( $field['other_choice'] ) ) { return $schema; }` + */ + public function test_get_rest_schema_no_enum_with_other_choice() { + $field = $this->get_field( array( 'other_choice' => 1 ) ); + + $schema = $this->field_instance->get_rest_schema( $field ); + + // With other_choice enabled, enum should NOT be set. + $this->assertArrayNotHasKey( 'enum', $schema ); + } + + /** + * Test get_rest_schema includes null in enum when allow_null. + * + * Tests the allow_null enum addition: + * `if ( ! empty( $field['allow_null'] ) ) { $schema['enum'][] = null; }` + */ + public function test_get_rest_schema_enum_includes_null_when_allowed() { + $field = $this->get_field( + array( + 'allow_null' => 1, + 'other_choice' => 0, + ) + ); + + $schema = $this->field_instance->get_rest_schema( $field ); + + $this->assertArrayHasKey( 'enum', $schema ); + $this->assertContains( null, $schema['enum'] ); + } } diff --git a/tests/php/includes/fields/test-class-acf-field-repeater.php b/tests/php/includes/fields/test-class-acf-field-repeater.php index 02cfa9c5..bfb87d7b 100644 --- a/tests/php/includes/fields/test-class-acf-field-repeater.php +++ b/tests/php/includes/fields/test-class-acf-field-repeater.php @@ -42,16 +42,18 @@ protected function get_field_include_path() { protected function get_field( $overrides = array() ) { return array_merge( array( - 'key' => 'field_repeater_test', - 'name' => 'test_repeater', - 'type' => 'repeater', - 'label' => 'Test Repeater', - 'required' => 0, - 'min' => 0, - 'max' => 0, - 'layout' => 'table', - 'pagination' => 0, - 'sub_fields' => array( + 'ID' => 0, + 'key' => 'field_repeater_test', + 'name' => 'test_repeater', + 'type' => 'repeater', + 'label' => 'Test Repeater', + 'required' => 0, + 'min' => 0, + 'max' => 0, + 'layout' => 'table', + 'pagination' => 0, + 'button_label' => 'Add Row', + 'sub_fields' => array( array( 'key' => 'field_sub_text', 'name' => 'sub_text', @@ -468,4 +470,247 @@ public function test_validate_value_nested_repeater( $depth ) { $this->assertTrue( $valid ); } + + /** + * Test load_field casts min/max to integers. + * + * Tests the casting logic: + * `$field['min'] = (int) $field['min'];` + * `$field['max'] = (int) $field['max'];` + */ + public function test_load_field_casts_min_max_to_int() { + $field = $this->get_field( + array( + 'min' => '5', + 'max' => '10', + ) + ); + + $loaded = $this->field_instance->load_field( $field ); + + $this->assertSame( 5, $loaded['min'] ); + $this->assertSame( 10, $loaded['max'] ); + } + + /** + * Test load_field sets default rows_per_page when empty. + * + * Tests the rows_per_page default: + * `if ( empty( $field['rows_per_page'] ) || (int) $field['rows_per_page'] < 1 )` + */ + public function test_load_field_sets_default_rows_per_page() { + $field = $this->get_field( array( 'rows_per_page' => '' ) ); + + $loaded = $this->field_instance->load_field( $field ); + + $this->assertEquals( 20, $loaded['rows_per_page'] ); + } + + /** + * Test load_field sets default rows_per_page when zero. + */ + public function test_load_field_zero_rows_per_page_uses_default() { + $field = $this->get_field( array( 'rows_per_page' => 0 ) ); + + $loaded = $this->field_instance->load_field( $field ); + + $this->assertEquals( 20, $loaded['rows_per_page'] ); + } + + /** + * Test load_field sets default button_label. + * + * Tests the button_label default: + * `if ( '' === $field['button_label'] ) { $field['button_label'] = __( 'Add Row'... ); }` + */ + public function test_load_field_sets_default_button_label() { + $field = $this->get_field( array( 'button_label' => '' ) ); + + $loaded = $this->field_instance->load_field( $field ); + + $this->assertEquals( 'Add Row', $loaded['button_label'] ); + } + + /** + * Test load_field preserves custom button_label. + */ + public function test_load_field_preserves_custom_button_label() { + $field = $this->get_field( array( 'button_label' => 'Add Item' ) ); + + $loaded = $this->field_instance->load_field( $field ); + + $this->assertEquals( 'Add Item', $loaded['button_label'] ); + } + + /** + * Test format_value returns false for non-array. + * + * Tests the early return: + * `if ( ! is_array( $value ) ) { return false; }` + */ + public function test_format_value_non_array_returns_false() { + $field = $this->get_field(); + + $result = $this->field_instance->format_value( 'not an array', $this->post_id, $field ); + + $this->assertFalse( $result ); + } + + /** + * Test format_value returns false with no sub_fields. + * + * Tests the sub_fields check: + * `if ( empty( $field['sub_fields'] ) ) { return false; }` + */ + public function test_format_value_no_subfields_returns_false() { + $field = $this->get_field( array( 'sub_fields' => array() ) ); + $value = array( array( 'some_key' => 'some_value' ) ); + + $result = $this->field_instance->format_value( $value, $this->post_id, $field ); + + $this->assertFalse( $result ); + } + + /** + * Test update_row returns false for non-array row. + * + * Tests the early return: + * `if ( ! is_array( $row ) ) { return false; }` + */ + public function test_update_row_non_array_returns_false() { + $field = $this->get_field(); + + $result = $this->field_instance->update_row( 'not an array', 0, $field, $this->post_id ); + + $this->assertFalse( $result ); + } + + /** + * Test update_row returns false with no sub_fields. + * + * Tests the sub_fields check: + * `if ( empty( $field['sub_fields'] ) ) { return false; }` + */ + public function test_update_row_no_subfields_returns_false() { + $field = $this->get_field( array( 'sub_fields' => array() ) ); + $row = array( 'field_sub_text' => 'value' ); + + $result = $this->field_instance->update_row( $row, 0, $field, $this->post_id ); + + $this->assertFalse( $result ); + } + + /** + * Test delete_row returns false with no sub_fields. + * + * Tests the early return: + * `if ( empty( $field['sub_fields'] ) ) { return false; }` + */ + public function test_delete_row_no_subfields_returns_false() { + $field = $this->get_field( array( 'sub_fields' => array() ) ); + + $result = $this->field_instance->delete_row( 0, $field, $this->post_id ); + + $this->assertFalse( $result ); + } + + /** + * Test get_rest_schema includes minItems when min is set. + * + * Tests the minItems addition: + * `if ( ! empty( $field['min'] ) ) { $schema['minItems'] = (int) $field['min']; }` + */ + public function test_get_rest_schema_includes_min_items() { + $field = $this->get_field( array( 'min' => 2 ) ); + + $schema = $this->field_instance->get_rest_schema( $field ); + + $this->assertArrayHasKey( 'minItems', $schema ); + $this->assertEquals( 2, $schema['minItems'] ); + } + + /** + * Test get_rest_schema includes maxItems when max is set. + * + * Tests the maxItems addition: + * `if ( ! empty( $field['max'] ) ) { $schema['maxItems'] = (int) $field['max']; }` + */ + public function test_get_rest_schema_includes_max_items() { + $field = $this->get_field( array( 'max' => 5 ) ); + + $schema = $this->field_instance->get_rest_schema( $field ); + + $this->assertArrayHasKey( 'maxItems', $schema ); + $this->assertEquals( 5, $schema['maxItems'] ); + } + + /** + * Test format_value_for_rest returns null for empty. + * + * Tests the early return: + * `if ( empty( $value ) || ! is_array( $value ) || empty( $field['sub_fields'] ) ) { return null; }` + */ + public function test_format_value_for_rest_empty_returns_null() { + $field = $this->get_field(); + + $result = $this->field_instance->format_value_for_rest( array(), $this->post_id, $field ); + + $this->assertNull( $result ); + } + + /** + * Test format_value_for_rest returns null for non-array. + */ + public function test_format_value_for_rest_non_array_returns_null() { + $field = $this->get_field(); + + $result = $this->field_instance->format_value_for_rest( 'string', $this->post_id, $field ); + + $this->assertNull( $result ); + } + + /** + * Test format_value_for_rest returns null with no sub_fields. + */ + public function test_format_value_for_rest_no_subfields_returns_null() { + $field = $this->get_field( array( 'sub_fields' => array() ) ); + $value = array( array( 'key' => 'value' ) ); + + $result = $this->field_instance->format_value_for_rest( $value, $this->post_id, $field ); + + $this->assertNull( $result ); + } + + /** + * Test validate_any_field migrates column_width to wrapper. + * + * Tests the column_width migration: + * `if ( isset( $field['column_width'] ) ) { $field['wrapper']['width'] = ... }` + */ + public function test_validate_any_field_migrates_column_width() { + $field = array( + 'key' => 'field_test', + 'column_width' => '50', + 'wrapper' => array(), + ); + + $result = $this->field_instance->validate_any_field( $field ); + + $this->assertEquals( '50', $result['wrapper']['width'] ); + $this->assertArrayNotHasKey( 'column_width', $result ); + } + + /** + * Test update_value converts non-array to empty array. + * + * Tests the non-array handling: + * `if ( ! is_array( $value ) ) { $value = array(); }` + */ + public function test_update_value_non_array_converts_to_empty() { + $field = $this->get_field(); + + $result = $this->field_instance->update_value( 'not an array', $this->post_id, $field ); + + $this->assertEmpty( $result ); + } } diff --git a/tests/php/includes/fields/test-class-acf-field-text.php b/tests/php/includes/fields/test-class-acf-field-text.php index 1aa41d9e..cdc59d21 100644 --- a/tests/php/includes/fields/test-class-acf-field-text.php +++ b/tests/php/includes/fields/test-class-acf-field-text.php @@ -111,4 +111,108 @@ public function test_format_value_for_rest_empty() { $this->assertEmpty( $result ); } + + /** + * Test validate_value with no maxlength passes. + * + * Tests the condition: + * `if ( isset( $field['maxlength'] ) && $field['maxlength'] && ... )` + */ + public function test_validate_value_no_maxlength_passes() { + $field = $this->get_field( array( 'maxlength' => '' ) ); + + $valid = $this->field_instance->validate_value( true, 'Any length text is fine', $field, 'acf[field_text_test]' ); + + $this->assertTrue( $valid ); + } + + /** + * Test validate_value with zero maxlength passes. + * + * Tests that maxlength of 0 is treated as "no limit" (falsy check). + */ + public function test_validate_value_zero_maxlength_passes() { + $field = $this->get_field( array( 'maxlength' => 0 ) ); + + $valid = $this->field_instance->validate_value( true, 'Long text with zero maxlength', $field, 'acf[field_text_test]' ); + + $this->assertTrue( $valid ); + } + + /** + * Test validate_value at exact maxlength boundary passes. + */ + public function test_validate_value_exact_maxlength_passes() { + $field = $this->get_field( array( 'maxlength' => 10 ) ); + + // Exactly 10 characters. + $valid = $this->field_instance->validate_value( true, '1234567890', $field, 'acf[field_text_test]' ); + + $this->assertTrue( $valid ); + } + + /** + * Test validate_value one over maxlength fails. + */ + public function test_validate_value_one_over_maxlength_fails() { + $field = $this->get_field( array( 'maxlength' => 10 ) ); + + // 11 characters (one over). + $valid = $this->field_instance->validate_value( true, '12345678901', $field, 'acf[field_text_test]' ); + + $this->assertIsString( $valid ); + $this->assertStringContainsString( '10', $valid ); // Should mention the limit. + } + + /** + * Test validate_value preserves passed $valid state. + * + * Tests that the function returns the passed $valid when no maxlength issue. + */ + public function test_validate_value_preserves_valid_state() { + $field = $this->get_field( array( 'maxlength' => 100 ) ); + + // Pass false as initial $valid - should be preserved and returned. + $valid = $this->field_instance->validate_value( false, 'Short', $field, 'acf[field_text_test]' ); + + $this->assertFalse( $valid ); + } + + /** + * Test get_rest_schema includes maxLength when set. + * + * Tests the maxLength addition: + * `if ( ! empty( $field['maxlength'] ) ) { $schema['maxLength'] = (int) $field['maxlength']; }` + */ + public function test_get_rest_schema_includes_maxlength() { + $field = $this->get_field( array( 'maxlength' => 50 ) ); + + $schema = $this->field_instance->get_rest_schema( $field ); + + $this->assertArrayHasKey( 'maxLength', $schema ); + $this->assertEquals( 50, $schema['maxLength'] ); + } + + /** + * Test get_rest_schema excludes maxLength when empty. + */ + public function test_get_rest_schema_excludes_empty_maxlength() { + $field = $this->get_field( array( 'maxlength' => '' ) ); + + $schema = $this->field_instance->get_rest_schema( $field ); + + $this->assertArrayNotHasKey( 'maxLength', $schema ); + } + + /** + * Test get_rest_schema casts maxLength to integer. + */ + public function test_get_rest_schema_casts_maxlength_to_int() { + $field = $this->get_field( array( 'maxlength' => '25' ) ); + + $schema = $this->field_instance->get_rest_schema( $field ); + + $this->assertArrayHasKey( 'maxLength', $schema ); + $this->assertSame( 25, $schema['maxLength'] ); // Should be int, not string. + } } diff --git a/tests/php/includes/fields/test-class-acf-field-type-group.php b/tests/php/includes/fields/test-class-acf-field-type-group.php index e77ae054..91154372 100644 --- a/tests/php/includes/fields/test-class-acf-field-type-group.php +++ b/tests/php/includes/fields/test-class-acf-field-type-group.php @@ -336,4 +336,190 @@ public function test_nested_group() { $this->assertIsArray( $result ); $this->assertArrayHasKey( 'inner_group', $result ); } + + /** + * Test update_value returns null for non-array value. + * + * Tests the early return: + * `if ( ! acf_is_array( $value ) ) { return null; }` + */ + public function test_update_value_non_array_returns_null() { + $field = $this->get_field(); + + $result = $this->field_instance->update_value( 'not an array', $this->post_id, $field ); + + $this->assertNull( $result ); + } + + /** + * Test update_value returns null for null value. + */ + public function test_update_value_null_returns_null() { + $field = $this->get_field(); + + $result = $this->field_instance->update_value( null, $this->post_id, $field ); + + $this->assertNull( $result ); + } + + /** + * Test update_value returns null for no sub_fields. + * + * Tests the early return: + * `if ( empty( $field['sub_fields'] ) ) { return null; }` + */ + public function test_update_value_no_subfields_returns_null() { + $field = $this->get_field( array( 'sub_fields' => array() ) ); + $value = array( 'some_key' => 'some_value' ); + + $result = $this->field_instance->update_value( $value, $this->post_id, $field ); + + $this->assertNull( $result ); + } + + /** + * Test update_value returns empty string for valid input. + * + * Tests the final return: + * `return '';` + */ + public function test_update_value_valid_returns_empty_string() { + $field = $this->get_field(); + $value = array( + 'field_sub_text' => 'Hello', + 'field_sub_email' => 'test@example.com', + ); + + $result = $this->field_instance->update_value( $value, $this->post_id, $field ); + + $this->assertEquals( '', $result ); + } + + /** + * Test format_value_for_rest returns value unchanged for empty. + * + * Tests the early return: + * `if ( empty( $value ) || ! is_array( $value ) || empty( $field['sub_fields'] ) ) { return $value; }` + */ + public function test_format_value_for_rest_empty_returns_value() { + $field = $this->get_field(); + + $result = $this->field_instance->format_value_for_rest( '', $this->post_id, $field ); + + $this->assertEquals( '', $result ); + } + + /** + * Test format_value_for_rest returns value unchanged for non-array. + */ + public function test_format_value_for_rest_non_array_returns_value() { + $field = $this->get_field(); + + $result = $this->field_instance->format_value_for_rest( 'string value', $this->post_id, $field ); + + $this->assertEquals( 'string value', $result ); + } + + /** + * Test format_value_for_rest returns value unchanged when no sub_fields. + */ + public function test_format_value_for_rest_no_subfields_returns_value() { + $field = $this->get_field( array( 'sub_fields' => array() ) ); + $value = array( 'key' => 'value' ); + + $result = $this->field_instance->format_value_for_rest( $value, $this->post_id, $field ); + + $this->assertEquals( $value, $result ); + } + + /** + * Test prepare_field_for_db returns field unchanged when no sub_fields. + * + * Tests the early return: + * `if ( empty( $field['sub_fields'] ) ) { return $field; }` + */ + public function test_prepare_field_for_db_no_subfields_returns_unchanged() { + $field = $this->get_field( array( 'sub_fields' => array() ) ); + + $result = $this->field_instance->prepare_field_for_db( $field ); + + $this->assertEquals( $field, $result ); + } + + /** + * Test prepare_field_for_db prefixes sub_field names. + * + * Tests the name prefixing logic: + * `$sub_field['name'] = $field['name'] . '_' . $sub_field['_name'];` + */ + public function test_prepare_field_for_db_prefixes_subfield_names() { + $field = $this->get_field(); + + $result = $this->field_instance->prepare_field_for_db( $field ); + + // Check that sub_field names were prefixed with parent field name. + $this->assertEquals( 'test_group_sub_text', $result['sub_fields'][0]['name'] ); + $this->assertEquals( 'test_group_sub_email', $result['sub_fields'][1]['name'] ); + } + + /** + * Test validate_value returns early for empty value. + * + * Tests the early return: + * `if ( empty( $value ) ) { return $valid; }` + */ + public function test_validate_value_empty_returns_valid_unchanged() { + $field = $this->get_field(); + + // Pass true, should return true unchanged. + $result = $this->field_instance->validate_value( true, array(), $field, 'acf[test]' ); + $this->assertTrue( $result ); + + // Pass false, should return false unchanged. + $result = $this->field_instance->validate_value( false, array(), $field, 'acf[test]' ); + $this->assertFalse( $result ); + } + + /** + * Test validate_value returns early when no sub_fields. + * + * Tests the early return: + * `if ( empty( $field['sub_fields'] ) ) { return $valid; }` + */ + public function test_validate_value_no_subfields_returns_valid_unchanged() { + $field = $this->get_field( array( 'sub_fields' => array() ) ); + $value = array( 'some_key' => 'some_value' ); + + $result = $this->field_instance->validate_value( true, $value, $field, 'acf[test]' ); + + $this->assertTrue( $result ); + } + + /** + * Test load_value returns value unchanged when no sub_fields. + * + * Tests the early return: + * `if ( empty( $field['sub_fields'] ) ) { return $value; }` + */ + public function test_load_value_no_subfields_returns_value_unchanged() { + $field = $this->get_field( array( 'sub_fields' => array() ) ); + + $result = $this->field_instance->load_value( 'original', $this->post_id, $field ); + + $this->assertEquals( 'original', $result ); + } + + /** + * Test delete_value returns null when no sub_fields. + * + * Tests the early return: + * `if ( empty( $field['sub_fields'] ) ) { return null; }` + */ + public function test_delete_value_no_subfields_returns_null() { + $field = $this->get_field( array( 'sub_fields' => array() ) ); + + $result = $this->field_instance->delete_value( $this->post_id, 'test_group', $field ); + + $this->assertNull( $result ); + } } diff --git a/tests/php/includes/fields/test-class-acf-field-wysiwyg.php b/tests/php/includes/fields/test-class-acf-field-wysiwyg.php index 5be183fd..263ad17d 100644 --- a/tests/php/includes/fields/test-class-acf-field-wysiwyg.php +++ b/tests/php/includes/fields/test-class-acf-field-wysiwyg.php @@ -202,4 +202,114 @@ public function test_format_value_complex_html() { $this->assertStringContainsString( '', $result ); $this->assertStringContainsString( '
  • ', $result ); } + + /** + * Test format_value returns non-string unchanged. + * + * Tests the early return for non-strings: + * `if ( empty( $value ) || ! is_string( $value ) ) { return $value; }` + */ + public function test_format_value_returns_array_unchanged() { + $field = $this->get_field(); + $array = array( 'some', 'array' ); + + $result = $this->field_instance->format_value( $array, $this->post_id, $field, false ); + + $this->assertIsArray( $result ); + $this->assertEquals( $array, $result ); + } + + /** + * Test format_value returns integer unchanged. + */ + public function test_format_value_returns_integer_unchanged() { + $field = $this->get_field(); + + $result = $this->field_instance->format_value( 12345, $this->post_id, $field, false ); + + $this->assertSame( 12345, $result ); + } + + /** + * Test format_value returns null unchanged. + */ + public function test_format_value_returns_null_unchanged() { + $field = $this->get_field(); + + $result = $this->field_instance->format_value( null, $this->post_id, $field, false ); + + $this->assertNull( $result ); + } + + /** + * Test format_value escapes CDATA closing sequence. + * + * Tests the str_replace for CDATA: + * `return str_replace( ']]>', ']]>', $value );` + */ + public function test_format_value_escapes_cdata_closing() { + $field = $this->get_field(); + $content = '

    Some content with ]]> CDATA closing

    '; + + $result = $this->field_instance->format_value( $content, $this->post_id, $field, false ); + + $this->assertStringContainsString( ']]>', $result ); + $this->assertStringNotContainsString( ']]>', $result ); + } + + /** + * Test format_value with escape_html applies acf_esc_html filter. + * + * Tests the escape_html branch: + * `if ( $escape_html ) { add_filter( 'acf_the_content', 'acf_esc_html', 1 ); }` + */ + public function test_format_value_with_escape_html() { + $field = $this->get_field(); + $content = '

    Test content

    '; + + // With escape_html = true, content should be sanitized. + $result = $this->field_instance->format_value( $content, $this->post_id, $field, true ); + + // Result should be a string (escaped or not depending on filter availability). + $this->assertIsString( $result ); + } + + /** + * Test format_value without escape_html preserves HTML. + */ + public function test_format_value_without_escape_html() { + $field = $this->get_field(); + $content = ''; + + // With escape_html = false, script tags should be preserved. + $result = $this->field_instance->format_value( $content, $this->post_id, $field, false ); + + $this->assertIsString( $result ); + // Content goes through filters but should contain the script text. + $this->assertStringContainsString( 'alert', $result ); + } + + /** + * Test format_value returns false unchanged. + */ + public function test_format_value_returns_false_unchanged() { + $field = $this->get_field(); + + $result = $this->field_instance->format_value( false, $this->post_id, $field, false ); + + $this->assertFalse( $result ); + } + + /** + * Test format_value returns zero unchanged. + * + * Tests that numeric zero (empty but not string) returns early. + */ + public function test_format_value_returns_zero_unchanged() { + $field = $this->get_field(); + + $result = $this->field_instance->format_value( 0, $this->post_id, $field, false ); + + $this->assertSame( 0, $result ); + } } diff --git a/tests/php/includes/fields/test-class-acf-repeater-table.php b/tests/php/includes/fields/test-class-acf-repeater-table.php new file mode 100644 index 00000000..4d884bbb --- /dev/null +++ b/tests/php/includes/fields/test-class-acf-repeater-table.php @@ -0,0 +1,719 @@ + 'field_repeater_test', + 'name' => 'test_repeater', + 'type' => 'repeater', + 'label' => 'Test Repeater', + 'min' => 0, + 'max' => 0, + 'layout' => 'table', + 'button_label' => 'Add Row', + 'collapsed' => '', + 'rows_per_page' => 20, + 'pagination' => false, + 'prefix' => 'acf', + 'value' => array(), + 'sub_fields' => array( + array( + 'key' => 'field_sub_text', + 'name' => 'sub_text', + '_name' => 'sub_text', + 'type' => 'text', + 'label' => 'Sub Text', + 'required' => 0, + 'instructions' => '', + 'wrapper' => array( + 'width' => '', + 'class' => '', + 'id' => '', + ), + ), + ), + ), + $overrides + ); + } + + /** + * Test constructor disables pagination when parent_repeater is set. + * + * Tests the logic: + * `if ( ! empty( $this->field['parent_repeater'] ) || ! empty( $this->field['parent_layout'] ) ) { $this->field['pagination'] = false; }` + */ + public function test_constructor_disables_pagination_for_nested_repeater() { + $field = $this->get_field( + array( + 'pagination' => true, + 'parent_repeater' => 'field_parent', + ) + ); + + $table = new ACF_Repeater_Table( $field ); + + // Use reflection to access private property. + $reflection = new ReflectionClass( $table ); + $prop = $reflection->getProperty( 'field' ); + $prop->setAccessible( true ); + $internal_field = $prop->getValue( $table ); + + $this->assertFalse( $internal_field['pagination'] ); + } + + /** + * Test constructor disables pagination when parent_layout is set. + */ + public function test_constructor_disables_pagination_for_flexible_content_child() { + $field = $this->get_field( + array( + 'pagination' => true, + 'parent_layout' => 'layout_123', + ) + ); + + $table = new ACF_Repeater_Table( $field ); + + $reflection = new ReflectionClass( $table ); + $prop = $reflection->getProperty( 'field' ); + $prop->setAccessible( true ); + $internal_field = $prop->getValue( $table ); + + $this->assertFalse( $internal_field['pagination'] ); + } + + /** + * Test constructor sets pagination to false when empty. + * + * Tests the logic: + * `if ( empty( $this->field['pagination'] ) ) { $this->field['pagination'] = false; }` + */ + public function test_constructor_defaults_pagination_to_false() { + $field = $this->get_field(); + unset( $field['pagination'] ); + + $table = new ACF_Repeater_Table( $field ); + + $reflection = new ReflectionClass( $table ); + $prop = $reflection->getProperty( 'field' ); + $prop->setAccessible( true ); + $internal_field = $prop->getValue( $table ); + + $this->assertFalse( $internal_field['pagination'] ); + } + + /** + * Test setup hides order when max is 1. + * + * Tests the logic: + * `if ( 1 === (int) $this->field['max'] ) { $this->show_order = false; }` + */ + public function test_setup_hides_order_when_max_is_one() { + $field = $this->get_field( array( 'max' => 1 ) ); + + $table = new ACF_Repeater_Table( $field ); + + $reflection = new ReflectionClass( $table ); + $prop = $reflection->getProperty( 'show_order' ); + $prop->setAccessible( true ); + + $this->assertFalse( $prop->getValue( $table ) ); + } + + /** + * Test setup hides add/remove buttons when max <= min. + * + * Tests the logic: + * `if ( $this->field['max'] <= $this->field['min'] ) { $this->show_remove = false; $this->show_add = false; }` + */ + public function test_setup_hides_add_remove_when_max_equals_min() { + $field = $this->get_field( + array( + 'min' => 3, + 'max' => 3, + ) + ); + + $table = new ACF_Repeater_Table( $field ); + + $reflection = new ReflectionClass( $table ); + $show_add = $reflection->getProperty( 'show_add' ); + $show_remove = $reflection->getProperty( 'show_remove' ); + $show_add->setAccessible( true ); + $show_remove->setAccessible( true ); + + $this->assertFalse( $show_add->getValue( $table ) ); + $this->assertFalse( $show_remove->getValue( $table ) ); + } + + /** + * Test setup hides add/remove buttons when max < min. + */ + public function test_setup_hides_add_remove_when_max_less_than_min() { + $field = $this->get_field( + array( + 'min' => 5, + 'max' => 3, + ) + ); + + $table = new ACF_Repeater_Table( $field ); + + $reflection = new ReflectionClass( $table ); + $show_add = $reflection->getProperty( 'show_add' ); + $show_remove = $reflection->getProperty( 'show_remove' ); + $show_add->setAccessible( true ); + $show_remove->setAccessible( true ); + + $this->assertFalse( $show_add->getValue( $table ) ); + $this->assertFalse( $show_remove->getValue( $table ) ); + } + + /** + * Test setup defaults rows_per_page when empty. + * + * Tests the logic: + * `if ( empty( $this->field['rows_per_page'] ) ) { $this->field['rows_per_page'] = 20; }` + */ + public function test_setup_defaults_rows_per_page_when_empty() { + $field = $this->get_field( array( 'rows_per_page' => '' ) ); + + $table = new ACF_Repeater_Table( $field ); + + $reflection = new ReflectionClass( $table ); + $prop = $reflection->getProperty( 'field' ); + $prop->setAccessible( true ); + $internal_field = $prop->getValue( $table ); + + $this->assertEquals( 20, $internal_field['rows_per_page'] ); + } + + /** + * Test setup defaults rows_per_page when less than 1. + * + * Tests the logic: + * `if ( (int) $this->field['rows_per_page'] < 1 ) { $this->field['rows_per_page'] = 20; }` + */ + public function test_setup_defaults_rows_per_page_when_zero() { + $field = $this->get_field( array( 'rows_per_page' => 0 ) ); + + $table = new ACF_Repeater_Table( $field ); + + $reflection = new ReflectionClass( $table ); + $prop = $reflection->getProperty( 'field' ); + $prop->setAccessible( true ); + $internal_field = $prop->getValue( $table ); + + $this->assertEquals( 20, $internal_field['rows_per_page'] ); + } + + /** + * Test setup defaults rows_per_page when negative. + */ + public function test_setup_defaults_rows_per_page_when_negative() { + $field = $this->get_field( array( 'rows_per_page' => -5 ) ); + + $table = new ACF_Repeater_Table( $field ); + + $reflection = new ReflectionClass( $table ); + $prop = $reflection->getProperty( 'field' ); + $prop->setAccessible( true ); + $internal_field = $prop->getValue( $table ); + + $this->assertEquals( 20, $internal_field['rows_per_page'] ); + } + + /** + * Test setup adds collapsed-target class to collapsed sub_field. + * + * Tests the logic: + * `if ( $sub_field['key'] === $this->field['collapsed'] ) { $sub_field['wrapper']['class'] .= ' -collapsed-target'; }` + */ + public function test_setup_adds_collapsed_target_class() { + $field = $this->get_field( array( 'collapsed' => 'field_sub_text' ) ); + + $table = new ACF_Repeater_Table( $field ); + + $reflection = new ReflectionClass( $table ); + $prop = $reflection->getProperty( 'sub_fields' ); + $prop->setAccessible( true ); + $sub_fields = $prop->getValue( $table ); + + $this->assertStringContainsString( '-collapsed-target', $sub_fields[0]['wrapper']['class'] ); + } + + /** + * Test prepare_value pads array to min. + * + * Tests the logic: + * `if ( $this->field['min'] ) { $value = array_pad( $value, $this->field['min'], array() ); }` + */ + public function test_prepare_value_pads_to_min() { + $field = $this->get_field( + array( + 'min' => 3, + 'value' => array( array( 'field_sub_text' => 'row1' ) ), + ) + ); + + $table = new ACF_Repeater_Table( $field ); + + $reflection = new ReflectionClass( $table ); + $prop = $reflection->getProperty( 'value' ); + $prop->setAccessible( true ); + $value = $prop->getValue( $table ); + + // Should have 3 rows + acfcloneindex. + $this->assertCount( 4, $value ); + $this->assertArrayHasKey( 'acfcloneindex', $value ); + } + + /** + * Test prepare_value slices array to max. + * + * Tests the logic: + * `if ( $this->field['max'] ) { $value = array_slice( $value, 0, $this->field['max'] ); }` + */ + public function test_prepare_value_slices_to_max() { + $field = $this->get_field( + array( + 'max' => 2, + 'value' => array( + array( 'field_sub_text' => 'row1' ), + array( 'field_sub_text' => 'row2' ), + array( 'field_sub_text' => 'row3' ), + array( 'field_sub_text' => 'row4' ), + ), + ) + ); + + $table = new ACF_Repeater_Table( $field ); + + $reflection = new ReflectionClass( $table ); + $prop = $reflection->getProperty( 'value' ); + $prop->setAccessible( true ); + $value = $prop->getValue( $table ); + + // Should have 2 rows + acfcloneindex. + $this->assertCount( 3, $value ); + } + + /** + * Test prepare_value converts non-array to empty array. + * + * Tests the logic: + * `$value = is_array( $this->field['value'] ) ? $this->field['value'] : array();` + */ + public function test_prepare_value_converts_non_array() { + $field = $this->get_field( array( 'value' => 'not an array' ) ); + + $table = new ACF_Repeater_Table( $field ); + + $reflection = new ReflectionClass( $table ); + $prop = $reflection->getProperty( 'value' ); + $prop->setAccessible( true ); + $value = $prop->getValue( $table ); + + // Should only have acfcloneindex. + $this->assertCount( 1, $value ); + $this->assertArrayHasKey( 'acfcloneindex', $value ); + } + + /** + * Test that min/max are applied when pagination is disabled. + * + * This confirms the logic path where pagination is empty and pad/slice occurs. + * Compare with test_prepare_value_pads_to_min and test_prepare_value_slices_to_max + * which test the individual operations. + */ + public function test_prepare_value_applies_min_max_when_not_paginated() { + $field = $this->get_field( + array( + 'min' => 3, + 'max' => 5, + 'pagination' => false, + 'value' => array( array( 'field_sub_text' => 'row1' ) ), + ) + ); + + $table = new ACF_Repeater_Table( $field ); + + $reflection = new ReflectionClass( $table ); + $prop = $reflection->getProperty( 'value' ); + $prop->setAccessible( true ); + $value = $prop->getValue( $table ); + + // Should have 3 rows (padded to min) + acfcloneindex = 4. + $this->assertCount( 4, $value ); + } + + /** + * Test thead only renders for table layout. + * + * Tests the early return: + * `if ( 'table' !== $this->field['layout'] ) { return; }` + */ + public function test_thead_only_renders_for_table_layout() { + $field = $this->get_field( array( 'layout' => 'block' ) ); + + $table = new ACF_Repeater_Table( $field ); + + ob_start(); + $table->thead(); + $output = ob_get_clean(); + + $this->assertEmpty( $output ); + } + + /** + * Test thead renders for table layout. + */ + public function test_thead_renders_for_table_layout() { + $field = $this->get_field( array( 'layout' => 'table' ) ); + + $table = new ACF_Repeater_Table( $field ); + + ob_start(); + $table->thead(); + $output = ob_get_clean(); + + $this->assertStringContainsString( '', $output ); + $this->assertStringContainsString( 'acf-th', $output ); + } + + /** + * Test rows returns array when should_return is true. + */ + public function test_rows_returns_array_when_should_return() { + $field = $this->get_field( + array( + 'value' => array( + array( 'field_sub_text' => 'row1' ), + ), + ) + ); + + $table = new ACF_Repeater_Table( $field ); + + $result = $table->rows( true ); + + $this->assertIsArray( $result ); + } + + /** + * Test rows excludes acfcloneindex when should_return is true. + * + * Tests the logic: + * `if ( $should_return && isset( $this->value['acfcloneindex'] ) ) { unset( $this->value['acfcloneindex'] ); }` + */ + public function test_rows_excludes_clone_when_returning() { + $field = $this->get_field( + array( + 'value' => array( + array( 'field_sub_text' => 'row1' ), + ), + ) + ); + + $table = new ACF_Repeater_Table( $field ); + + $result = $table->rows( true ); + + $this->assertArrayNotHasKey( 'acfcloneindex', $result ); + } + + /** + * Test row_handle does not render when show_order is false. + * + * Tests the early return: + * `if ( ! $this->show_order ) { return; }` + */ + public function test_row_handle_respects_show_order() { + $field = $this->get_field( array( 'max' => 1 ) ); // max=1 sets show_order to false. + + $table = new ACF_Repeater_Table( $field ); + + ob_start(); + $table->row_handle( 0 ); + $output = ob_get_clean(); + + $this->assertEmpty( $output ); + } + + /** + * Test row_handle renders when show_order is true. + */ + public function test_row_handle_renders_when_show_order_true() { + $field = $this->get_field( array( 'max' => 0 ) ); // max=0 means unlimited, show_order remains true. + + $table = new ACF_Repeater_Table( $field ); + + ob_start(); + $table->row_handle( 0 ); + $output = ob_get_clean(); + + $this->assertStringContainsString( 'acf-row-handle', $output ); + $this->assertStringContainsString( 'acf-row-number', $output ); + } + + /** + * Test row_actions does not render when show_remove is false. + * + * Tests the early return: + * `if ( ! $this->show_remove ) { return; }` + */ + public function test_row_actions_respects_show_remove() { + $field = $this->get_field( + array( + 'min' => 3, + 'max' => 3, + ) + ); // max=min sets show_remove to false. + + $table = new ACF_Repeater_Table( $field ); + + ob_start(); + $table->row_actions(); + $output = ob_get_clean(); + + $this->assertEmpty( $output ); + } + + /** + * Test row_actions renders add/duplicate/remove buttons. + */ + public function test_row_actions_renders_buttons() { + $field = $this->get_field(); + + $table = new ACF_Repeater_Table( $field ); + + ob_start(); + $table->row_actions(); + $output = ob_get_clean(); + + $this->assertStringContainsString( 'add-row', $output ); + $this->assertStringContainsString( 'duplicate-row', $output ); + $this->assertStringContainsString( 'remove-row', $output ); + } + + /** + * Test table_actions does not render when show_add is false. + * + * Tests the early return: + * `if ( ! $this->show_add ) { return; }` + */ + public function test_table_actions_respects_show_add() { + $field = $this->get_field( + array( + 'min' => 3, + 'max' => 3, + ) + ); // max=min sets show_add to false. + + $table = new ACF_Repeater_Table( $field ); + + ob_start(); + $table->table_actions(); + $output = ob_get_clean(); + + $this->assertEmpty( $output ); + } + + /** + * Test table_actions renders add button with custom label. + */ + public function test_table_actions_renders_add_button() { + $field = $this->get_field( array( 'button_label' => 'Add New Item' ) ); + + $table = new ACF_Repeater_Table( $field ); + + ob_start(); + $table->table_actions(); + $output = ob_get_clean(); + + $this->assertStringContainsString( 'Add New Item', $output ); + $this->assertStringContainsString( 'acf-repeater-add-row', $output ); + } + + /** + * Test pagination does not render when pagination is disabled. + * + * Tests the early return: + * `if ( empty( $this->field['pagination'] ) ) { return; }` + */ + public function test_pagination_respects_pagination_setting() { + $field = $this->get_field( array( 'pagination' => false ) ); + + $table = new ACF_Repeater_Table( $field ); + + ob_start(); + $table->pagination(); + $output = ob_get_clean(); + + $this->assertEmpty( $output ); + } + + /** + * Test row uses correct element for different layouts. + * + * Tests the layout-specific logic: + * - 'row' layout uses div with -left class + * - 'block' layout uses div + * - 'table' layout uses td + */ + public function test_row_uses_correct_element_for_row_layout() { + $field = $this->get_field( array( 'layout' => 'row' ) ); + + $table = new ACF_Repeater_Table( $field ); + + $result = $table->row( 0, array(), true ); + + $this->assertStringContainsString( 'acf-fields -left', $result ); + } + + /** + * Test row uses correct element for block layout. + */ + public function test_row_uses_correct_element_for_block_layout() { + $field = $this->get_field( array( 'layout' => 'block' ) ); + + $table = new ACF_Repeater_Table( $field ); + + $result = $table->row( 0, array(), true ); + + $this->assertStringContainsString( 'class="acf-fields"', $result ); + } + + /** + * Test row adds acf-clone class for acfcloneindex. + * + * Tests the logic: + * `if ( 'acfcloneindex' === $i ) { $id = 'acfcloneindex'; $class .= ' acf-clone'; }` + */ + public function test_row_adds_clone_class_for_clone_index() { + $field = $this->get_field(); + + $table = new ACF_Repeater_Table( $field ); + + $result = $table->row( 'acfcloneindex', array(), true ); + + $this->assertStringContainsString( 'acf-clone', $result ); + $this->assertStringContainsString( 'data-id="acfcloneindex"', $result ); + } + + /** + * Test render outputs complete table structure. + */ + public function test_render_outputs_table_structure() { + $field = $this->get_field( + array( + 'value' => array( + array( 'field_sub_text' => 'row1' ), + ), + ) + ); + + $table = new ACF_Repeater_Table( $field ); + + ob_start(); + $table->render(); + $output = ob_get_clean(); + + $this->assertStringContainsString( 'class="acf-repeater', $output ); + $this->assertStringContainsString( '', $output ); + $this->assertStringContainsString( '', $output ); + $this->assertStringContainsString( 'acf-actions', $output ); + } + + /** + * Test row_handle includes pagination input when pagination enabled. + * + * Tests the logic: + * `if ( ! empty( $this->field['pagination'] ) ) { ... $input = sprintf(...) ... }` + */ + public function test_row_handle_includes_pagination_input() { + $field = $this->get_field( + array( + 'pagination' => true, + 'total_rows' => 100, + 'orig_name' => 'test_repeater', + ) + ); + + // Need to bypass the constructor's pagination disable for non-admin. + $table = new ACF_Repeater_Table( $field ); + + // Use reflection to force pagination on for testing. + $reflection = new ReflectionClass( $table ); + $field_prop = $reflection->getProperty( 'field' ); + $field_prop->setAccessible( true ); + $internal_field = $field_prop->getValue( $table ); + $internal_field['pagination'] = true; + $field_prop->setValue( $table, $internal_field ); + + ob_start(); + $table->row_handle( 0 ); + $output = ob_get_clean(); + + $this->assertStringContainsString( 'acf-order-input', $output ); + } + + /** + * Test thead includes sub_field width styling when set. + * + * Tests the logic: + * `if ( $sub_field['wrapper']['width'] ) { $attrs['data-width'] = ...; $attrs['style'] = ...; }` + */ + public function test_thead_includes_width_styling() { + $field = $this->get_field( array( 'layout' => 'table' ) ); + $field['sub_fields'][0]['wrapper']['width'] = '50'; + + $table = new ACF_Repeater_Table( $field ); + + ob_start(); + $table->thead(); + $output = ob_get_clean(); + + $this->assertStringContainsString( 'data-width="50"', $output ); + $this->assertStringContainsString( 'width: 50%', $output ); + } + + /** + * Test row_handle includes collapse icon when collapsed is set. + * + * Tests the logic: + * `if ( $this->field['collapsed'] ) : ?>