mirror of
https://github.com/wp-graphql/wp-graphql-woocommerce.git
synced 2026-08-14 12:53:44 +02:00
* feat: WC Settings API, compatibility refactor, HPOS fix, CI improvements WC Settings GraphQL API: - Add WCSetting interface with common fields and type-safe resolveType - Add concrete types: WCStringSetting, WCArraySetting, WCRelativeDateSetting, WCImageWidthSetting with typed value/default fields - Add WCSettingGroup type with nested settings field - Add wcSettingGroups and wcSettings root query fields (admin only) - Add updateWCSetting and updateWCSettings mutations - Dynamic WCSettingTypeEnum collected from registered WC settings - Filters: graphql_woocommerce_setting_type_map, graphql_woocommerce_setting_types - Register WC admin settings for GraphQL requests via Compatibility class Compatibility refactor: - Consolidate ACF, JWT Auth, and QL Search filters into Compatibility class - Remove class-acf-schema-filters.php, class-jwt-auth-schema-filters.php, functions.php HPOS compatibility fix: - Replace hardcoded directory name check with dirname(__DIR__) === WP_PLUGIN_DIR - Works with any plugin folder name while still skipping nested vendor installs CI improvements: - Remove STRIPE_API_PUBLISHABLE_KEY restriction from coverage job - Add HPOS to coverage matrix entries - Sort type registry and includes alphabetically Other fixes: - Fix Settings_Mutation::validate_setting_checkbox_field to be static - Update ShippingZone settings field type to WCStringSetting Closes #864, closes #969 * refactor: Rename and split core classes, add order cursor pagination tests Class renames: - WooCommerce_Filters → WooCommerce (class-woocommerce.php), setup() → init() - Core_Schema_Filters → Post_Types (class-post-types.php) Class split: - Extract taxonomy registration from Core_Schema_Filters into Taxonomies (class-taxonomies.php) Access functions: - Add wc_graphql_resolve_product_type() for interface resolveType callbacks - Add wc_graphql_is_session_handler_disabled() - Add wc_graphql_enabled_authorizing_url_fields() - Add wc_graphql_get_authorizing_url_nonce_param_name() - Replace direct class references with global functions in type-registry, compatibility, protected-router, and all product interface files Stripe gateway compatibility: - Move woographql_stripe_gateway_args from WooCommerce to Compatibility class - Rename to woocommerce_gateway_stripe_args Tests: - Add OrderCursorPaginationTest (6 tests) covering COT cursor-based pagination: forward/backward, date ordering ASC/DESC, cursor integrity * fix: Use proper expectedField/expectedNode assertions in cursor pagination tests Replace empty assertQuerySuccessful([]) calls and manual lodashGet assertions with expectedField, expectedNode, and not()->expectedNode() for proper GraphQL response validation.
485 lines
13 KiB
PHP
485 lines
13 KiB
PHP
<?php
|
|
|
|
class OrderCursorPaginationTest extends \Tests\WPGraphQL\WooCommerce\TestCase\WooGraphQLTestCase {
|
|
private $order_ids = [];
|
|
|
|
public function setUp(): void {
|
|
parent::setUp();
|
|
|
|
$this->loginAsShopManager();
|
|
|
|
// Create 5 orders with staggered dates and different totals.
|
|
for ( $i = 0; $i < 5; $i++ ) {
|
|
$this->order_ids[] = $this->factory->order->createNew(
|
|
[
|
|
'status' => 'completed',
|
|
'customer_id' => $this->customer,
|
|
]
|
|
);
|
|
|
|
// Stagger dates so ordering is deterministic.
|
|
$order = wc_get_order( $this->order_ids[ $i ] );
|
|
$order->set_date_created( gmdate( 'Y-m-d H:i:s', strtotime( "+{$i} minutes" ) ) );
|
|
$order->set_total( ( $i + 1 ) * 10 );
|
|
$order->save();
|
|
}
|
|
}
|
|
|
|
public function testForwardPaginationWithFirstAfter() {
|
|
$query = '
|
|
query ($first: Int, $after: String) {
|
|
orders(first: $first, after: $after) {
|
|
nodes {
|
|
databaseId
|
|
}
|
|
pageInfo {
|
|
hasNextPage
|
|
endCursor
|
|
}
|
|
}
|
|
}
|
|
';
|
|
|
|
// Page 1: 2 orders.
|
|
$variables = [ 'first' => 2 ];
|
|
$response = $this->graphql( compact( 'query', 'variables' ) );
|
|
|
|
$this->assertQuerySuccessful(
|
|
$response,
|
|
[
|
|
$this->expectedField( 'orders.nodes.0.databaseId', static::NOT_FALSY ),
|
|
$this->expectedField( 'orders.nodes.1.databaseId', static::NOT_FALSY ),
|
|
$this->expectedField( 'orders.pageInfo.hasNextPage', true ),
|
|
$this->expectedField( 'orders.pageInfo.endCursor', static::NOT_FALSY ),
|
|
]
|
|
);
|
|
|
|
$page1_nodes = $this->lodashGet( $response, 'data.orders.nodes' );
|
|
$end_cursor = $this->lodashGet( $response, 'data.orders.pageInfo.endCursor' );
|
|
|
|
// Page 2.
|
|
$variables = [
|
|
'first' => 2,
|
|
'after' => $end_cursor,
|
|
];
|
|
$response = $this->graphql( compact( 'query', 'variables' ) );
|
|
|
|
$expected = [
|
|
$this->expectedField( 'orders.nodes.0.databaseId', static::NOT_FALSY ),
|
|
$this->expectedField( 'orders.nodes.1.databaseId', static::NOT_FALSY ),
|
|
];
|
|
foreach ( $page1_nodes as $node ) {
|
|
$expected[] = $this->not()->expectedNode(
|
|
'orders.nodes',
|
|
[ $this->expectedField( 'databaseId', $node['databaseId'] ) ]
|
|
);
|
|
}
|
|
|
|
$this->assertQuerySuccessful( $response, $expected );
|
|
|
|
$page2_nodes = $this->lodashGet( $response, 'data.orders.nodes' );
|
|
$end_cursor = $this->lodashGet( $response, 'data.orders.pageInfo.endCursor' );
|
|
|
|
// Page 3: should have 1 remaining.
|
|
$variables = [
|
|
'first' => 2,
|
|
'after' => $end_cursor,
|
|
];
|
|
$response = $this->graphql( compact( 'query', 'variables' ) );
|
|
|
|
$expected = [
|
|
$this->expectedField( 'orders.nodes.0.databaseId', static::NOT_FALSY ),
|
|
$this->expectedField( 'orders.pageInfo.hasNextPage', false ),
|
|
];
|
|
foreach ( array_merge( $page1_nodes, $page2_nodes ) as $node ) {
|
|
$expected[] = $this->not()->expectedNode(
|
|
'orders.nodes',
|
|
[ $this->expectedField( 'databaseId', $node['databaseId'] ) ]
|
|
);
|
|
}
|
|
|
|
$this->assertQuerySuccessful( $response, $expected );
|
|
}
|
|
|
|
public function testBackwardPaginationWithLastBefore() {
|
|
$query = '
|
|
query ($last: Int, $before: String) {
|
|
orders(last: $last, before: $before) {
|
|
nodes {
|
|
databaseId
|
|
}
|
|
pageInfo {
|
|
hasPreviousPage
|
|
startCursor
|
|
}
|
|
}
|
|
}
|
|
';
|
|
|
|
// Last 2 orders.
|
|
$variables = [ 'last' => 2 ];
|
|
$response = $this->graphql( compact( 'query', 'variables' ) );
|
|
|
|
$this->assertQuerySuccessful(
|
|
$response,
|
|
[
|
|
$this->expectedField( 'orders.nodes.0.databaseId', static::NOT_FALSY ),
|
|
$this->expectedField( 'orders.nodes.1.databaseId', static::NOT_FALSY ),
|
|
$this->expectedField( 'orders.pageInfo.hasPreviousPage', true ),
|
|
$this->expectedField( 'orders.pageInfo.startCursor', static::NOT_FALSY ),
|
|
]
|
|
);
|
|
|
|
$page1_nodes = $this->lodashGet( $response, 'data.orders.nodes' );
|
|
$start_cursor = $this->lodashGet( $response, 'data.orders.pageInfo.startCursor' );
|
|
|
|
// Previous page.
|
|
$variables = [
|
|
'last' => 2,
|
|
'before' => $start_cursor,
|
|
];
|
|
$response = $this->graphql( compact( 'query', 'variables' ) );
|
|
|
|
$expected = [
|
|
$this->expectedField( 'orders.nodes.0.databaseId', static::NOT_FALSY ),
|
|
$this->expectedField( 'orders.nodes.1.databaseId', static::NOT_FALSY ),
|
|
];
|
|
foreach ( $page1_nodes as $node ) {
|
|
$expected[] = $this->not()->expectedNode(
|
|
'orders.nodes',
|
|
[ $this->expectedField( 'databaseId', $node['databaseId'] ) ]
|
|
);
|
|
}
|
|
|
|
$this->assertQuerySuccessful( $response, $expected );
|
|
}
|
|
|
|
public function testCursorPaginationReturnsConsistentResults() {
|
|
$query = '
|
|
query {
|
|
orders(first: 100) {
|
|
nodes {
|
|
databaseId
|
|
}
|
|
}
|
|
}
|
|
';
|
|
|
|
$response = $this->graphql( compact( 'query' ) );
|
|
|
|
$expected = [];
|
|
foreach ( $this->order_ids as $order_id ) {
|
|
$expected[] = $this->expectedNode(
|
|
'orders.nodes',
|
|
[ $this->expectedField( 'databaseId', $order_id ) ]
|
|
);
|
|
}
|
|
|
|
$this->assertQuerySuccessful( $response, $expected );
|
|
}
|
|
|
|
public function testOrderConnectionWithDateOrdering() {
|
|
$query = '
|
|
query {
|
|
orders(first: 5, where: { orderby: { field: DATE, order: ASC } }) {
|
|
nodes {
|
|
databaseId
|
|
date
|
|
}
|
|
}
|
|
}
|
|
';
|
|
|
|
$response = $this->graphql( compact( 'query' ) );
|
|
|
|
$this->assertQuerySuccessful(
|
|
$response,
|
|
[
|
|
$this->expectedField( 'orders.nodes.0.databaseId', static::NOT_FALSY ),
|
|
$this->expectedField( 'orders.nodes.0.date', static::NOT_FALSY ),
|
|
]
|
|
);
|
|
|
|
$nodes = $this->lodashGet( $response, 'data.orders.nodes' );
|
|
$dates = array_column( $nodes, 'date' );
|
|
$sorted = $dates;
|
|
sort( $sorted );
|
|
$this->assertSame( $sorted, $dates, 'Orders should be sorted by date ascending.' );
|
|
}
|
|
|
|
public function testOrderConnectionWithDescDateOrdering() {
|
|
$query = '
|
|
query {
|
|
orders(first: 5, where: { orderby: { field: DATE, order: DESC } }) {
|
|
nodes {
|
|
databaseId
|
|
date
|
|
}
|
|
}
|
|
}
|
|
';
|
|
|
|
$response = $this->graphql( compact( 'query' ) );
|
|
|
|
$this->assertQuerySuccessful(
|
|
$response,
|
|
[
|
|
$this->expectedField( 'orders.nodes.0.databaseId', static::NOT_FALSY ),
|
|
$this->expectedField( 'orders.nodes.0.date', static::NOT_FALSY ),
|
|
]
|
|
);
|
|
|
|
$nodes = $this->lodashGet( $response, 'data.orders.nodes' );
|
|
$dates = array_column( $nodes, 'date' );
|
|
$sorted = $dates;
|
|
rsort( $sorted );
|
|
$this->assertSame( $sorted, $dates, 'Orders should be sorted by date descending.' );
|
|
}
|
|
|
|
public function testForwardPaginationWithDateOrderingMaintainsCursorIntegrity() {
|
|
$query = '
|
|
query ($first: Int, $after: String) {
|
|
orders(first: $first, after: $after, where: { orderby: { field: DATE, order: ASC } }) {
|
|
nodes {
|
|
databaseId
|
|
date
|
|
}
|
|
pageInfo {
|
|
hasNextPage
|
|
endCursor
|
|
}
|
|
}
|
|
}
|
|
';
|
|
|
|
// Page 1.
|
|
$variables = [ 'first' => 2 ];
|
|
$response = $this->graphql( compact( 'query', 'variables' ) );
|
|
|
|
$this->assertQuerySuccessful(
|
|
$response,
|
|
[
|
|
$this->expectedField( 'orders.nodes.0.date', static::NOT_FALSY ),
|
|
$this->expectedField( 'orders.nodes.1.date', static::NOT_FALSY ),
|
|
$this->expectedField( 'orders.pageInfo.endCursor', static::NOT_FALSY ),
|
|
]
|
|
);
|
|
|
|
$page1_nodes = $this->lodashGet( $response, 'data.orders.nodes' );
|
|
$end_cursor = $this->lodashGet( $response, 'data.orders.pageInfo.endCursor' );
|
|
|
|
// Page 2.
|
|
$variables = [
|
|
'first' => 2,
|
|
'after' => $end_cursor,
|
|
];
|
|
$response = $this->graphql( compact( 'query', 'variables' ) );
|
|
|
|
$expected = [
|
|
$this->expectedField( 'orders.nodes.0.date', static::NOT_FALSY ),
|
|
];
|
|
foreach ( $page1_nodes as $node ) {
|
|
$expected[] = $this->not()->expectedNode(
|
|
'orders.nodes',
|
|
[ $this->expectedField( 'databaseId', $node['databaseId'] ) ]
|
|
);
|
|
}
|
|
|
|
$this->assertQuerySuccessful( $response, $expected );
|
|
|
|
$page2_nodes = $this->lodashGet( $response, 'data.orders.nodes' );
|
|
$last_date_page1 = end( $page1_nodes )['date'];
|
|
$first_date_page2 = $page2_nodes[0]['date'];
|
|
$this->assertLessThanOrEqual(
|
|
$first_date_page2,
|
|
$last_date_page1,
|
|
'Cursor pagination should maintain date ordering across pages.'
|
|
);
|
|
}
|
|
|
|
public function testCursorPaginationOrderedByTotal() {
|
|
$query = '
|
|
query ($first: Int, $after: String) {
|
|
orders(first: $first, after: $after, where: { orderby: { field: TOTAL, order: ASC } }) {
|
|
nodes {
|
|
databaseId
|
|
total(format: RAW)
|
|
}
|
|
pageInfo {
|
|
hasNextPage
|
|
endCursor
|
|
}
|
|
}
|
|
}
|
|
';
|
|
|
|
// Page 1.
|
|
$variables = [ 'first' => 2 ];
|
|
$response = $this->graphql( compact( 'query', 'variables' ) );
|
|
|
|
$this->assertQuerySuccessful(
|
|
$response,
|
|
[
|
|
$this->expectedField( 'orders.nodes.0.databaseId', static::NOT_FALSY ),
|
|
$this->expectedField( 'orders.nodes.0.total', static::NOT_FALSY ),
|
|
$this->expectedField( 'orders.nodes.1.databaseId', static::NOT_FALSY ),
|
|
$this->expectedField( 'orders.pageInfo.hasNextPage', true ),
|
|
$this->expectedField( 'orders.pageInfo.endCursor', static::NOT_FALSY ),
|
|
]
|
|
);
|
|
|
|
$page1_nodes = $this->lodashGet( $response, 'data.orders.nodes' );
|
|
$end_cursor = $this->lodashGet( $response, 'data.orders.pageInfo.endCursor' );
|
|
|
|
// Page 2 — triggers COT cursor compare_with() for _order_total column.
|
|
$variables = [
|
|
'first' => 2,
|
|
'after' => $end_cursor,
|
|
];
|
|
$response = $this->graphql( compact( 'query', 'variables' ) );
|
|
|
|
$expected = [
|
|
$this->expectedField( 'orders.nodes.0.databaseId', static::NOT_FALSY ),
|
|
$this->expectedField( 'orders.nodes.1.databaseId', static::NOT_FALSY ),
|
|
];
|
|
foreach ( $page1_nodes as $node ) {
|
|
$expected[] = $this->not()->expectedNode(
|
|
'orders.nodes',
|
|
[ $this->expectedField( 'databaseId', $node['databaseId'] ) ]
|
|
);
|
|
}
|
|
|
|
$this->assertQuerySuccessful( $response, $expected );
|
|
|
|
$page2_nodes = $this->lodashGet( $response, 'data.orders.nodes' );
|
|
$last_total_page1 = (float) end( $page1_nodes )['total'];
|
|
$first_total_page2 = (float) $page2_nodes[0]['total'];
|
|
$this->assertLessThanOrEqual(
|
|
$first_total_page2,
|
|
$last_total_page1,
|
|
'Cursor pagination should maintain total ordering across pages.'
|
|
);
|
|
}
|
|
|
|
public function testCursorPaginationOrderedByTotalDesc() {
|
|
$query = '
|
|
query ($first: Int, $after: String) {
|
|
orders(first: $first, after: $after, where: { orderby: { field: TOTAL, order: DESC } }) {
|
|
nodes {
|
|
databaseId
|
|
total(format: RAW)
|
|
}
|
|
pageInfo {
|
|
hasNextPage
|
|
endCursor
|
|
}
|
|
}
|
|
}
|
|
';
|
|
|
|
// Page 1.
|
|
$variables = [ 'first' => 2 ];
|
|
$response = $this->graphql( compact( 'query', 'variables' ) );
|
|
|
|
$this->assertQuerySuccessful(
|
|
$response,
|
|
[
|
|
$this->expectedField( 'orders.nodes.0.databaseId', static::NOT_FALSY ),
|
|
$this->expectedField( 'orders.nodes.0.total', static::NOT_FALSY ),
|
|
$this->expectedField( 'orders.pageInfo.endCursor', static::NOT_FALSY ),
|
|
]
|
|
);
|
|
|
|
$page1_nodes = $this->lodashGet( $response, 'data.orders.nodes' );
|
|
$end_cursor = $this->lodashGet( $response, 'data.orders.pageInfo.endCursor' );
|
|
|
|
// Page 2.
|
|
$variables = [
|
|
'first' => 2,
|
|
'after' => $end_cursor,
|
|
];
|
|
$response = $this->graphql( compact( 'query', 'variables' ) );
|
|
|
|
$expected = [
|
|
$this->expectedField( 'orders.nodes.0.databaseId', static::NOT_FALSY ),
|
|
];
|
|
foreach ( $page1_nodes as $node ) {
|
|
$expected[] = $this->not()->expectedNode(
|
|
'orders.nodes',
|
|
[ $this->expectedField( 'databaseId', $node['databaseId'] ) ]
|
|
);
|
|
}
|
|
|
|
$this->assertQuerySuccessful( $response, $expected );
|
|
|
|
$page2_nodes = $this->lodashGet( $response, 'data.orders.nodes' );
|
|
$last_total_page1 = (float) end( $page1_nodes )['total'];
|
|
$first_total_page2 = (float) $page2_nodes[0]['total'];
|
|
$this->assertGreaterThanOrEqual(
|
|
$first_total_page2,
|
|
$last_total_page1,
|
|
'Cursor pagination should maintain descending total ordering across pages.'
|
|
);
|
|
}
|
|
|
|
public function testCursorPaginationOrderedByDateCompleted() {
|
|
// Set completed dates on orders.
|
|
foreach ( $this->order_ids as $i => $order_id ) {
|
|
$order = wc_get_order( $order_id );
|
|
$order->set_date_completed( gmdate( 'Y-m-d H:i:s', strtotime( "+{$i} hours" ) ) );
|
|
$order->save();
|
|
}
|
|
|
|
$query = '
|
|
query ($first: Int, $after: String) {
|
|
orders(first: $first, after: $after, where: { orderby: { field: DATE_COMPLETED, order: ASC } }) {
|
|
nodes {
|
|
databaseId
|
|
}
|
|
pageInfo {
|
|
hasNextPage
|
|
endCursor
|
|
}
|
|
}
|
|
}
|
|
';
|
|
|
|
// Page 1.
|
|
$variables = [ 'first' => 2 ];
|
|
$response = $this->graphql( compact( 'query', 'variables' ) );
|
|
|
|
$this->assertQuerySuccessful( $response,
|
|
[
|
|
$this->expectedField( 'orders.nodes.0.databaseId', static::NOT_FALSY ),
|
|
$this->expectedField( 'orders.nodes.1.databaseId', static::NOT_FALSY ),
|
|
$this->expectedField( 'orders.pageInfo.endCursor', static::NOT_FALSY ),
|
|
]
|
|
);
|
|
|
|
$page1_nodes = $this->lodashGet( $response, 'data.orders.nodes' );
|
|
$end_cursor = $this->lodashGet( $response, 'data.orders.pageInfo.endCursor' );
|
|
|
|
// Page 2 — triggers COT cursor compare_with() for _date_completed column.
|
|
$variables = [
|
|
'first' => 2,
|
|
'after' => $end_cursor,
|
|
];
|
|
$response = $this->graphql( compact( 'query', 'variables' ) );
|
|
|
|
$this->assertQuerySuccessful(
|
|
$response,
|
|
array_map(
|
|
function( $order ) {
|
|
return $this->not()->expectedNode(
|
|
'orders.nodes',
|
|
[
|
|
$this->expectedField( 'databaseId', $order['databaseId'] ),
|
|
]
|
|
);
|
|
},
|
|
$page1_nodes
|
|
)
|
|
);
|
|
}
|
|
}
|