diff --git a/src/graphql_relay/connection/array_connection.py b/src/graphql_relay/connection/array_connection.py index f25cdce..413eb19 100644 --- a/src/graphql_relay/connection/array_connection.py +++ b/src/graphql_relay/connection/array_connection.py @@ -149,8 +149,12 @@ def connection_from_array_slice( pageInfo=page_info_type( startCursor=first_edge_cursor, endCursor=last_edge_cursor, - hasPreviousPage=isinstance(last, int) and start_offset > lower_bound, - hasNextPage=isinstance(first, int) and end_offset < upper_bound, + hasPreviousPage=start_offset > lower_bound + if isinstance(last, int) + else (0 <= after_offset < array_length if after is not None else False), + hasNextPage=end_offset < upper_bound + if isinstance(first, int) + else (0 <= before_offset < array_length if before is not None else False), ), ) diff --git a/tests/connection/test_array_connection.py b/tests/connection/test_array_connection.py index 33c89ee..db6e5f6 100644 --- a/tests/connection/test_array_connection.py +++ b/tests/connection/test_array_connection.py @@ -111,7 +111,8 @@ def respects_first_and_after(): pageInfo=PageInfo( startCursor=cursor_c, endCursor=cursor_d, - hasPreviousPage=False, + # `after` was given, so a previous page necessarily exists. + hasPreviousPage=True, hasNextPage=True, ), ) @@ -123,7 +124,8 @@ def respects_first_and_after_with_long_first(): pageInfo=PageInfo( startCursor=cursor_c, endCursor=cursor_e, - hasPreviousPage=False, + # `after` was given, so a previous page necessarily exists. + hasPreviousPage=True, hasNextPage=False, ), ) @@ -136,7 +138,8 @@ def respects_last_and_before(): startCursor=cursor_b, endCursor=cursor_c, hasPreviousPage=True, - hasNextPage=False, + # `before` was given, so a next page necessarily exists. + hasNextPage=True, ), ) @@ -148,7 +151,8 @@ def respects_last_and_before_with_long_last(): startCursor=cursor_a, endCursor=cursor_c, hasPreviousPage=False, - hasNextPage=False, + # `before` was given, so a next page necessarily exists. + hasNextPage=True, ), ) @@ -162,7 +166,8 @@ def respects_first_and_after_and_before_too_few(): pageInfo=PageInfo( startCursor=cursor_b, endCursor=cursor_c, - hasPreviousPage=False, + # `after` was given, so a previous page necessarily exists. + hasPreviousPage=True, hasNextPage=True, ), ) @@ -177,7 +182,8 @@ def respects_first_and_after_and_before_too_many(): pageInfo=PageInfo( startCursor=cursor_b, endCursor=cursor_d, - hasPreviousPage=False, + # `after` was given, so a previous page necessarily exists. + hasPreviousPage=True, hasNextPage=False, ), ) @@ -192,7 +198,8 @@ def respects_first_and_after_and_before_exactly_right(): pageInfo=PageInfo( startCursor=cursor_b, endCursor=cursor_d, - hasPreviousPage=False, + # `after` was given, so a previous page necessarily exists. + hasPreviousPage=True, hasNextPage=False, ), ) @@ -208,7 +215,8 @@ def respects_last_and_after_and_before_too_few(): startCursor=cursor_c, endCursor=cursor_d, hasPreviousPage=True, - hasNextPage=False, + # `before` was given, so a next page necessarily exists. + hasNextPage=True, ), ) @@ -223,7 +231,8 @@ def respects_last_and_after_and_before_too_many(): startCursor=cursor_b, endCursor=cursor_d, hasPreviousPage=False, - hasNextPage=False, + # `before` was given, so a next page necessarily exists. + hasNextPage=True, ), ) @@ -238,10 +247,49 @@ def respects_last_and_after_and_before_exactly_right(): startCursor=cursor_b, endCursor=cursor_d, hasPreviousPage=False, - hasNextPage=False, + # `before` was given, so a next page necessarily exists. + hasNextPage=True, ), ) + def describe_has_previous_next_page_regression(): + """Regression tests for pageInfo.hasPreviousPage/hasNextPage being + gated solely on `last`/`first` and ignoring `after`/`before`.""" + + array_0_to_19 = list(range(20)) + + def has_previous_page_true_on_second_page_of_forward_pagination(): + first_page = connection_from_array(array_0_to_19, dict(first=5)) + assert first_page.pageInfo.hasPreviousPage is False + + second_page = connection_from_array( + array_0_to_19, + dict(first=5, after=first_page.pageInfo.endCursor), + ) + # An `after` cursor was explicitly given, guaranteeing a + # previous page exists, even though `last` was never passed. + assert second_page.pageInfo.hasPreviousPage is True + + def has_previous_page_false_on_first_page_of_forward_pagination(): + first_page = connection_from_array(array_0_to_19, dict(first=5)) + assert first_page.pageInfo.hasPreviousPage is False + + def has_next_page_true_on_penultimate_page_of_backward_pagination(): + last_page = connection_from_array(array_0_to_19, dict(last=5)) + assert last_page.pageInfo.hasNextPage is False + + previous_page = connection_from_array( + array_0_to_19, + dict(last=5, before=last_page.pageInfo.startCursor), + ) + # A `before` cursor was explicitly given, guaranteeing a next + # page exists, even though `first` was never passed. + assert previous_page.pageInfo.hasNextPage is True + + def has_next_page_false_on_last_page_of_backward_pagination(): + last_page = connection_from_array(array_0_to_19, dict(last=5)) + assert last_page.pageInfo.hasNextPage is False + def describe_cursor_edge_cases(): def throws_an_error_if_first_smaller_than_zero(): with raises(ValueError) as exc_info: @@ -317,8 +365,12 @@ def returns_no_elements_if_cursors_cross(): pageInfo=PageInfo( startCursor=None, endCursor=None, - hasPreviousPage=False, - hasNextPage=False, + # Both `after` and `before` point to valid, existing + # elements, so elements exist both before `after` and + # after `before`, even though the crossed cursors leave + # no edges in the returned slice itself. + hasPreviousPage=True, + hasNextPage=True, ), ) @@ -483,7 +535,9 @@ def works_with_a_just_right_array_slice(): pageInfo=PageInfo( startCursor=cursor_b, endCursor=cursor_c, - hasPreviousPage=False, + # `after` points at an existing element (A), so a previous + # page necessarily exists. + hasPreviousPage=True, hasNextPage=True, ), ) @@ -500,7 +554,7 @@ def works_with_an_oversized_array_slice_left_side(): pageInfo=PageInfo( startCursor=cursor_b, endCursor=cursor_c, - hasPreviousPage=False, + hasPreviousPage=True, hasNextPage=True, ), ) @@ -517,7 +571,7 @@ def works_with_an_oversized_array_slice_right_side(): pageInfo=PageInfo( startCursor=cursor_c, endCursor=cursor_c, - hasPreviousPage=False, + hasPreviousPage=True, hasNextPage=True, ), ) @@ -534,7 +588,7 @@ def works_with_an_oversized_array_slice_both_sides(): pageInfo=PageInfo( startCursor=cursor_c, endCursor=cursor_c, - hasPreviousPage=False, + hasPreviousPage=True, hasNextPage=True, ), ) @@ -551,7 +605,7 @@ def works_with_an_undersized_array_slice_left_side(): pageInfo=PageInfo( startCursor=cursor_d, endCursor=cursor_e, - hasPreviousPage=False, + hasPreviousPage=True, hasNextPage=False, ), ) @@ -568,7 +622,7 @@ def works_with_an_undersized_array_slice_right_side(): pageInfo=PageInfo( startCursor=cursor_c, endCursor=cursor_d, - hasPreviousPage=False, + hasPreviousPage=True, hasNextPage=True, ), ) @@ -585,7 +639,7 @@ def works_with_an_undersized_array_slice_both_sides(): pageInfo=PageInfo( startCursor=cursor_d, endCursor=cursor_d, - hasPreviousPage=False, + hasPreviousPage=True, hasNextPage=True, ), )