diff --git a/src/Data/Connection/FormFieldsConnectionResolver.php b/src/Data/Connection/FormFieldsConnectionResolver.php index 038f78ea..0e1f788b 100644 --- a/src/Data/Connection/FormFieldsConnectionResolver.php +++ b/src/Data/Connection/FormFieldsConnectionResolver.php @@ -67,7 +67,7 @@ protected function loader_name(): string { */ public function is_valid_offset( $offset ) { foreach ( $this->form_fields as $field ) { - if ( $field->id === $offset ) { + if ( (int) $field->id === (int) $offset ) { return true; } } @@ -87,7 +87,41 @@ public function get_ids_from_query() { $ids = []; foreach ( $queried as $item ) { - $ids[ $item->id ] = $item->id; + $ids[ (int) $item->id ] = $item->id; + } + + return $ids; + } + + /** + * {@inheritDoc} + */ + public function apply_cursors_to_ids( array $ids ) { + if ( empty( $ids ) ) { + return []; + } + $args = $this->get_args(); + + // First we slice the array from the front. + if ( ! empty( $args['after'] ) ) { + $offset = $this->get_offset_for_cursor( $args['after'] ); + $index = $this->get_array_index_for_offset( $offset, $ids ); + + if ( false !== $index ) { + // We want to start with the first id after the index. + $ids = array_slice( $ids, $index + 1, null, true ); + } + } + + // Then we slice the array from the back. + if ( ! empty( $args['before'] ) ) { + $offset = $this->get_offset_for_cursor( $args['before'] ); + $index = $this->get_array_index_for_offset( $offset, $ids ); + + if ( false !== $index ) { + // We want to end with the id before the index. + $ids = array_slice( $ids, 0, $index, true ); + } } return $ids; @@ -97,25 +131,31 @@ public function get_ids_from_query() { * {@inheritDoc} */ protected function prepare_args( array $args ): array { - // Ensure that the ids are an array. - if ( isset( $args['where']['ids'] ) && ! is_array( $args['where']['ids'] ) ) { - $args['where']['ids'] = [ $args['where']['ids'] ]; + // Ensure that the ids are an array of ints. + if ( isset( $args['where']['ids'] ) ) { + if ( ! is_array( $args['where']['ids'] ) ) { + $args['where']['ids'] = [ $args['where']['ids'] ]; + } // Sanitize the IDs. $args['where']['ids'] = array_map( 'absint', $args['where']['ids'] ); } - // Ensure that Admin labels are an array. - if ( isset( $args['where']['adminLabels'] ) && ! is_array( $args['where']['adminLabels'] ) ) { - $args['where']['adminLabels'] = [ $args['where']['adminLabels'] ]; + // Ensure that Admin labels are an array of strings. + if ( isset( $args['where']['adminLabels'] ) ) { + if ( ! is_array( $args['where']['adminLabels'] ) ) { + $args['where']['adminLabels'] = [ $args['where']['adminLabels'] ]; + } // Sanitize the Admin labels. $args['where']['adminLabels'] = array_map( 'sanitize_text_field', $args['where']['adminLabels'] ); } - // Ensure that Field types are an array. - if ( isset( $args['where']['fieldTypes'] ) && ! is_array( $args['where']['fieldTypes'] ) ) { - $args['where']['fieldTypes'] = [ $args['where']['fieldTypes'] ]; + // Ensure that Field types are an array of strings. + if ( isset( $args['where']['fieldTypes'] ) ) { + if ( ! is_array( $args['where']['fieldTypes'] ) ) { + $args['where']['fieldTypes'] = [ $args['where']['fieldTypes'] ]; + } // Sanitize the Field types. $args['where']['fieldTypes'] = array_map( 'sanitize_text_field', $args['where']['fieldTypes'] ); @@ -149,22 +189,40 @@ protected function query( array $query_args ): array { // Filter by IDs. if ( ! empty( $query_args['ids'] ) ) { - $fields = array_filter( $fields, static fn ( $field ) => in_array( (int) $field['id'], $query_args['ids'], true ) ); + $fields = array_filter( $fields, static fn ( \GF_Field $field ) => isset( $field->id ) && in_array( (int) $field->id, $query_args['ids'], true ) ); } // Filter by Admin labels. if ( ! empty( $query_args['adminLabels'] ) ) { - $fields = array_filter( $fields, static fn ( $field ) => in_array( $field['adminLabel'], $query_args['adminLabels'], true ) ); + $fields = array_filter( $fields, static fn ( \GF_Field $field ) => isset( $field->adminLabel ) && in_array( $field->adminLabel, $query_args['adminLabels'], true ) ); } // Filter by Field types. if ( ! empty( $query_args['fieldTypes'] ) ) { - $fields = array_filter( $fields, static fn ( $field ) => in_array( $field['type'], $query_args['fieldTypes'], true ) ); + $fields = array_filter( $fields, static fn ( \GF_Field $field ) => isset( $field->type ) && in_array( $field->type, $query_args['fieldTypes'], true ) ); } // Filter by Page number. + $has_page_number = false; if ( ! empty( $query_args['pageNumber'] ) ) { - $fields = array_filter( $fields, static fn ( $field ) => $query_args['pageNumber'] === (int) $field['pageNumber'] ); + $filtered_fields = array_filter( + $fields, + static function ( \GF_Field $field ) use ( $query_args, &$has_page_number ) { + if ( ! isset( $field->pageNumber ) ) { + return false; + } + + // Set the flag to true if the page number is found. + $has_page_number = true; + + return $query_args['pageNumber'] === (int) $field->pageNumber; + } + ); + + // Dont use filtered fileds if the form isnt paged. + if ( $has_page_number || 1 < $query_args['pageNumber'] ) { + $fields = $filtered_fields; + } } return $fields; diff --git a/tests/wpunit/EmailFieldTest.php b/tests/wpunit/EmailFieldTest.php index 6fffa6be..f4d1fd97 100644 --- a/tests/wpunit/EmailFieldTest.php +++ b/tests/wpunit/EmailFieldTest.php @@ -70,15 +70,15 @@ public function generate_fields(): array { // Mimic the Form Field model. // $field->inputs = [ - // [ - // 'autocompleteAttribute' => $field->autocompleteAttribute ?? null, - // 'defaultValue' => $field->defaultValue ?? null, - // 'customLabel' => $field->customLabel ?? null, - // 'id' => $field->id ?? null, - // 'label' => $field->label ?? null, - // 'name' => $field->inputName ?? null, - // 'placeholder' => $field->placeholder ?? null, - // ] + // [ + // 'autocompleteAttribute' => $field->autocompleteAttribute ?? null, + // 'defaultValue' => $field->defaultValue ?? null, + // 'customLabel' => $field->customLabel ?? null, + // 'id' => $field->id ?? null, + // 'label' => $field->label ?? null, + // 'name' => $field->inputName ?? null, + // 'placeholder' => $field->placeholder ?? null, + // ] // ]; return [ $field ]; diff --git a/tests/wpunit/EmailFieldWithConfirmationTest.php b/tests/wpunit/EmailFieldWithConfirmationTest.php index 754c1da4..69f2472e 100644 --- a/tests/wpunit/EmailFieldWithConfirmationTest.php +++ b/tests/wpunit/EmailFieldWithConfirmationTest.php @@ -58,7 +58,7 @@ public function field_helper() { * Generates the form fields from factory. Must be wrappend in an array. */ public function generate_fields(): array { - $field = $this->factory->field->create( + $field = $this->factory->field->create( array_merge( $this->property_helper->values, [ 'emailConfirmEnabled' => true ], diff --git a/tests/wpunit/FormConnectionQueriesTest.php b/tests/wpunit/FormConnectionQueriesTest.php index b3781c16..6f77fbbd 100644 --- a/tests/wpunit/FormConnectionQueriesTest.php +++ b/tests/wpunit/FormConnectionQueriesTest.php @@ -222,7 +222,7 @@ public function testBackwardPagination() { $this->assertEquals( true, $actual['data']['gfForms']['pageInfo']['hasNextPage'] ); /** - * Test the last two results are equal to `last:2`. + * Test the last two results are equal to `first:2`. */ $variables = [ 'first' => 2, diff --git a/tests/wpunit/FormFieldConnectionQueriesTest.php b/tests/wpunit/FormFieldConnectionQueriesTest.php new file mode 100644 index 00000000..93f802c4 --- /dev/null +++ b/tests/wpunit/FormFieldConnectionQueriesTest.php @@ -0,0 +1,437 @@ +admin->ID ); + + $this->fields = $this->generate_fields( 6 ); + $this->form_id = $this->factory->form->create( + array_merge( + [ 'fields' => $this->fields ], + $this->tester->getFormDefaultArgs() + ) + ); + + $this->clearSchema(); + } + + /** + * Run after each test. + */ + public function tearDown(): void { + // Your tear down methods here. + $this->factory->form->delete( $this->form_id ); + + // Then... + parent::tearDown(); + } + + private function generate_fields( int $count = 1 ): array { + $fields = []; + + for ( $i = 0; $i < $count; $i++ ) { + // Fields should cycle between text, number, and radio fields. + $property_helper_type = ''; + + switch ( $i % 3 ) { + case 1: + $property_helper_type = 'NumberField'; + break; + case 2: + $property_helper_type = 'RadioField'; + break; + case 0: + default: + $property_helper_type = 'TextField'; + break; + } + + $fields[] = $this->factory->field->create( + array_merge( + $this->tester->getPropertyHelper( $property_helper_type )->values, + [ + 'id' => $i + 1, + 'adminLabel' => lcfirst( $property_helper_type ), + ] + ) + ); + } + + return $fields; + } + + private function create_fields_for_paged_form( int $pages = 1 ): array { + $fields = []; + $field_count = 0; + for ( $i = 0; $i < $pages; $i++ ) { + $fields[] = $this->factory->field->create( + array_merge( + $this->tester->getPropertyHelper( 'TextField' )->values, + [ + 'id' => ++$field_count, + ] + ) + ); + $fields[] = $this->factory->field->create( + array_merge( + $this->tester->getPropertyHelper( 'NumberField' )->values, + [ + 'id' => ++$field_count, + ] + ) + ); + $fields[] = $this->factory->field->create( + array_merge( + $this->tester->getPropertyHelper( 'RadioField' )->values, + [ + 'id' => ++$field_count, + ] + ) + ); + + // Add a page field if we are not on the last page. + if ( $i < $pages - 1 ) { + $fields[] = $this->factory->field->create( + array_merge( + $this->tester->getPropertyHelper( 'PageField' )->values, + [ + 'id' => ++$field_count, + ] + ) + ); + } + } + return $fields; + } + + public function getQuery(): string { + return ' + query FormFields($formId: ID!, $first: Int, $last: Int, $after: String, $before: String, $where: GfFormToFormFieldConnectionWhereArgs) { + gfForm(id: $formId, idType: DATABASE_ID) { + formFields( + first: $first + last: $last + after: $after + before: $before + where: $where + ) { + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + edges { + cursor + node { + id + databaseId + } + } + nodes { + id + databaseId + type + ... on GfFieldWithAdminLabelSetting { + adminLabel + } + } + } + } + } + '; + } + + public function testForwardPagination(): void { + $query = $this->getQuery(); + + $form = GFAPI::get_form( $this->form_id ); + $wp_query = $form['fields']; + + /** + * Test the first two results. + */ + + // Set the variables to use in the GraphQL query. + $variables = [ + 'formId' => $this->form_id, + 'first' => 2, + ]; + + // Run the GraphQL Query. + $expected = array_slice( $wp_query, 0, 2, false ); + $actual = $this->graphql( compact( 'query', 'variables' ) ); + + $this->assertValidPagination( $expected, $actual ); + $this->assertEquals( false, $actual['data']['gfForm']['formFields']['pageInfo']['hasPreviousPage'] ); + $this->assertEquals( true, $actual['data']['gfForm']['formFields']['pageInfo']['hasNextPage'] ); + + /** + * Test with empty offset. + */ + $variables['after'] = ''; + $expected = $actual; + + $actual = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertEqualSets( $expected, $actual ); + + /** + * Test the next two results. + */ + + // Set the variables to use in the GraphQL query. + $variables['after'] = $actual['data']['gfForm']['formFields']['pageInfo']['endCursor']; + + // Run the GraphQL Query. + $expected = array_slice( $wp_query, 2, 2, false ); + $actual = $this->graphql( compact( 'query', 'variables' ) ); + + $this->assertValidPagination( $expected, $actual ); + $this->assertEquals( true, $actual['data']['gfForm']['formFields']['pageInfo']['hasPreviousPage'] ); + $this->assertEquals( true, $actual['data']['gfForm']['formFields']['pageInfo']['hasNextPage'] ); + + /** + * Test the last two results. + */ + + // Set the variables to use in the GraphQL query. + $variables['after'] = $actual['data']['gfForm']['formFields']['pageInfo']['endCursor']; + + // Run the GraphQL Query. + $expected = array_slice( $wp_query, 4, 2, false ); + $actual = $this->graphql( compact( 'query', 'variables' ) ); + + $this->assertValidPagination( $expected, $actual ); + $this->assertEquals( true, $actual['data']['gfForm']['formFields']['pageInfo']['hasPreviousPage'] ); + $this->assertEquals( false, $actual['data']['gfForm']['formFields']['pageInfo']['hasNextPage'] ); + + /** + * Test the last two results are equal to `last:2`. + */ + $variables = [ + 'formId' => $this->form_id, + 'last' => 2, + ]; + $expected = $actual; + + $actual = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertEqualSets( $expected, $actual ); + } + + public function testBackwardPagination() { + $query = $this->getQuery(); + + $form = GFAPI::get_form( $this->form_id ); + $wp_query = $form['fields']; + $wp_query = array_reverse( $wp_query ); + + /** + * Test the first two results. + */ + + // Set the variables to use in the GraphQL query. + $variables = [ + 'formId' => $this->form_id, + 'last' => 2, + ]; + + // Run the GraphQL Query. + $expected = array_slice( $wp_query, 0, 2, false ); + $expected = array_reverse( $expected ); + + $actual = $this->graphql( compact( 'query', 'variables' ) ); + + $this->assertValidPagination( $expected, $actual ); + $this->assertEquals( true, $actual['data']['gfForm']['formFields']['pageInfo']['hasPreviousPage'] ); + $this->assertEquals( false, $actual['data']['gfForm']['formFields']['pageInfo']['hasNextPage'] ); + + /** + * Test with empty offset. + */ + $variables['before'] = ''; + $expected = $actual; + + $actual = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertEqualSets( $expected, $actual ); + + /** + * Test the next two results. + */ + + // Set the variables to use in the GraphQL query. + $variables['before'] = $actual['data']['gfForm']['formFields']['pageInfo']['startCursor']; + + // Run the GraphQL Query. + $expected = array_slice( $wp_query, 2, 2, false ); + $expected = array_reverse( $expected ); + + $actual = $this->graphql( compact( 'query', 'variables' ) ); + + $this->assertValidPagination( $expected, $actual ); + $this->assertEquals( true, $actual['data']['gfForm']['formFields']['pageInfo']['hasPreviousPage'] ); + $this->assertEquals( true, $actual['data']['gfForm']['formFields']['pageInfo']['hasNextPage'] ); + + /** + * Test the last two results. + */ + + // Set the variables to use in the GraphQL query. + $variables['before'] = $actual['data']['gfForm']['formFields']['pageInfo']['startCursor']; + + // Run the GraphQL Query. + $expected = array_slice( $wp_query, 4, 2, false ); + $expected = array_reverse( $expected ); + + $actual = $this->graphql( compact( 'query', 'variables' ) ); + + $this->assertValidPagination( $expected, $actual ); + $this->assertEquals( false, $actual['data']['gfForm']['formFields']['pageInfo']['hasPreviousPage'] ); + $this->assertEquals( true, $actual['data']['gfForm']['formFields']['pageInfo']['hasNextPage'] ); + + /** + * Test the first two results are equal to `first:2`. + */ + $variables = [ + 'formId' => $this->form_id, + 'first' => 2, + ]; + $expected = $actual; + + $actual = $this->graphql( compact( 'query', 'variables' ) ); + $this->assertEqualSets( $expected, $actual ); + } + + public function testIdsWhereArgs() { + $form_field_id_four = $this->fields[3]->id; + $form_field_id_five = $this->fields[4]->id; + + $query = $this->getQuery(); + + codecept_debug( 'form_field_id_four: ' . $form_field_id_four ); + codecept_debug( 'form_field_id_five: ' . $form_field_id_five ); + + $variables = [ + 'formId' => $this->form_id, + 'where' => [ + 'ids' => [ $form_field_id_four, $form_field_id_five ], + ], + ]; + + $actual = $this->graphql( compact( 'query', 'variables' ) ); + + $this->assertResponseIsValid( $actual ); + $this->assertArrayNotHasKey( 'errors', $actual ); + $this->assertCount( 2, $actual['data']['gfForm']['formFields']['edges'] ); + $this->assertEquals( $form_field_id_four, $actual['data']['gfForm']['formFields']['edges'][0]['node']['databaseId'] ); + $this->assertEquals( $form_field_id_five, $actual['data']['gfForm']['formFields']['edges'][1]['node']['databaseId'] ); + } + + public function testAdminLabelsWhereArgs() { + $query = $this->getQuery(); + + $variables = [ + 'formId' => $this->form_id, + 'where' => [ + 'adminLabels' => [ 'numberField' ], + ], + ]; + + $actual = $this->graphql( compact( 'query', 'variables' ) ); + + $this->assertResponseIsValid( $actual ); + $this->assertArrayNotHasKey( 'errors', $actual ); + $this->assertCount( 2, $actual['data']['gfForm']['formFields']['edges'] ); + $this->assertEquals( 'numberField', $actual['data']['gfForm']['formFields']['nodes'][0]['adminLabel'] ); + + // Test with multiple admin labels. + $variables['where']['adminLabels'] = [ 'numberField', 'radioField' ]; + + $actual = $this->graphql( compact( 'query', 'variables' ) ); + + $this->assertResponseIsValid( $actual ); + $this->assertArrayNotHasKey( 'errors', $actual ); + $this->assertCount( 4, $actual['data']['gfForm']['formFields']['edges'] ); + $this->assertEquals( 'numberField', $actual['data']['gfForm']['formFields']['nodes'][0]['adminLabel'] ); + $this->assertEquals( 'radioField', $actual['data']['gfForm']['formFields']['nodes'][1]['adminLabel'] ); + } + + public function testFieldTypesWhereArgs() { + $query = $this->getQuery(); + + $variables = [ + 'formId' => $this->form_id, + 'where' => [ + 'fieldTypes' => [ 'NUMBER' ], + ], + ]; + + $actual = $this->graphql( compact( 'query', 'variables' ) ); + + $this->assertResponseIsValid( $actual ); + $this->assertArrayNotHasKey( 'errors', $actual ); + $this->assertCount( 2, $actual['data']['gfForm']['formFields']['edges'] ); + $this->assertEquals( 'NUMBER', $actual['data']['gfForm']['formFields']['nodes'][0]['type'] ); + + // Test with multiple admin labels. + $variables['where']['fieldTypes'] = [ 'NUMBER', 'RADIO' ]; + + $actual = $this->graphql( compact( 'query', 'variables' ) ); + + $this->assertResponseIsValid( $actual ); + $this->assertArrayNotHasKey( 'errors', $actual ); + $this->assertCount( 4, $actual['data']['gfForm']['formFields']['edges'] ); + $this->assertEquals( 'NUMBER', $actual['data']['gfForm']['formFields']['nodes'][0]['type'] ); + $this->assertEquals( 'RADIO', $actual['data']['gfForm']['formFields']['nodes'][1]['type'] ); + } + + /** + * Common assertions for testing pagination. + * + * @param array $expected Expected results from GFAPI. + * @param array $actual Actual results from GraphQL. + */ + private function assertValidPagination( array $expected, array $actual ): void { + $this->assertResponseIsValid( $actual ); + $this->assertArrayNotHasKey( 'errors', $actual ); + + $this->assertArrayHasKey( 'data', $actual ); + + $first_field_id = $expected[0]['id']; + $second_field_id = $expected[1]['id']; + + $start_cursor = $this->toRelayId( 'arrayconnection', $first_field_id ); + $end_cursor = $this->toRelayId( 'arrayconnection', $second_field_id ); + + $this->assertCount( 2, $actual['data']['gfForm']['formFields']['edges'] ); + $this->assertCount( 2, $actual['data']['gfForm']['formFields']['nodes'] ); + + $this->assertEquals( $first_field_id, $actual['data']['gfForm']['formFields']['edges'][0]['node']['databaseId'] ); + $this->assertEquals( $first_field_id, $actual['data']['gfForm']['formFields']['nodes'][0]['databaseId'] ); + $this->assertEquals( $start_cursor, $actual['data']['gfForm']['formFields']['edges'][0]['cursor'] ); + $this->assertEquals( $start_cursor, $actual['data']['gfForm']['formFields']['pageInfo']['startCursor'] ); + + $this->assertEquals( $second_field_id, $actual['data']['gfForm']['formFields']['edges'][1]['node']['databaseId'] ); + $this->assertEquals( $second_field_id, $actual['data']['gfForm']['formFields']['nodes'][1]['databaseId'] ); + $this->assertEquals( $end_cursor, $actual['data']['gfForm']['formFields']['edges'][1]['cursor'] ); + $this->assertEquals( $end_cursor, $actual['data']['gfForm']['formFields']['pageInfo']['endCursor'] ); + } +}